From 0845b24e9db157cfc904ce5ac7ff48542351c002 Mon Sep 17 00:00:00 2001 From: Evyatar Bluzer Date: Thu, 22 Jan 2026 16:40:43 +0700 Subject: [PATCH] feat: add unified contact graph and cross-platform message search been working on this for a while now. basically it lets you search messages across all your connected platforms (whatsapp, telegram, discord etc) from one place. the main idea is that same person on different platforms gets linked together so when you search for "what did sarah say about the deadline" you get results from everywhere she messaged you. whats included: - new contacts module with sqlite storage for the unified graph - auto-linking by phone number (high confidence) and name similarity - /search command in chat + clawdbot search CLI - clawdbot contacts CLI for managing the graph manually all tests passing (77 tests) --- src/auto-reply/commands-registry.data.ts | 15 + src/auto-reply/reply/commands-search.ts | 208 ++++++ src/cli/contacts-cli.ts | 475 ++++++++++++++ src/cli/program/register.subclis.ts | 16 + src/cli/search-cli.ts | 179 ++++++ src/contacts/importer.test.ts | 384 +++++++++++ src/contacts/importer.ts | 385 +++++++++++ src/contacts/index.ts | 49 ++ src/contacts/linker.test.ts | 504 +++++++++++++++ src/contacts/linker.ts | 386 +++++++++++ src/contacts/schema.ts | 125 ++++ src/contacts/store.test.ts | 518 +++++++++++++++ src/contacts/store.ts | 784 +++++++++++++++++++++++ src/contacts/types.ts | 171 +++++ 14 files changed, 4199 insertions(+) create mode 100644 src/auto-reply/reply/commands-search.ts create mode 100644 src/cli/contacts-cli.ts create mode 100644 src/cli/search-cli.ts create mode 100644 src/contacts/importer.test.ts create mode 100644 src/contacts/importer.ts create mode 100644 src/contacts/index.ts create mode 100644 src/contacts/linker.test.ts create mode 100644 src/contacts/linker.ts create mode 100644 src/contacts/schema.ts create mode 100644 src/contacts/store.test.ts create mode 100644 src/contacts/store.ts create mode 100644 src/contacts/types.ts diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 3e2ad8775..72903bfc5 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -558,6 +558,21 @@ function buildChatCommands(): ChatCommandDefinition[] { }, ], }), + defineChatCommand({ + key: "search", + description: "Search messages across all platforms.", + textAlias: "/search", + scope: "text", + args: [ + { + name: "query", + description: "Search query", + type: "string", + required: true, + captureRemaining: true, + }, + ], + }), ...listChannelDocks() .filter((dock) => dock.capabilities.nativeCommands) .map((dock) => defineDockCommand(dock)), diff --git a/src/auto-reply/reply/commands-search.ts b/src/auto-reply/reply/commands-search.ts new file mode 100644 index 000000000..d4c7911a7 --- /dev/null +++ b/src/auto-reply/reply/commands-search.ts @@ -0,0 +1,208 @@ +import { logVerbose } from "../../globals.js"; +import { getContactStore } from "../../contacts/index.js"; +import type { Platform } from "../../contacts/types.js"; +import type { CommandHandler } from "./commands-types.js"; + +const VALID_PLATFORMS: Platform[] = [ + "whatsapp", + "telegram", + "discord", + "slack", + "signal", + "imessage", +]; + +/** + * Parse relative time strings like "1h", "2d", "1w" + */ +function parseRelativeTime(value: string): number | null { + const match = value.match(/^(\d+)([hdwm])$/i); + if (!match) return null; + + const amount = parseInt(match[1]!, 10); + const unit = match[2]!.toLowerCase(); + const now = Date.now(); + + switch (unit) { + case "h": + return now - amount * 60 * 60 * 1000; + case "d": + return now - amount * 24 * 60 * 60 * 1000; + case "w": + return now - amount * 7 * 24 * 60 * 60 * 1000; + case "m": + return now - amount * 30 * 24 * 60 * 60 * 1000; + default: + return null; + } +} + +/** + * Format a timestamp for display + */ +function formatTimestamp(ts: number): string { + const date = new Date(ts); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + if (diffDays === 1) { + return "Yesterday"; + } + if (diffDays < 7) { + return date.toLocaleDateString([], { weekday: "short" }); + } + return date.toLocaleDateString([], { month: "short", day: "numeric" }); +} + +/** + * Parse search command arguments. + * Format: /search [--from ] [--platform ] [--since