@hydra_db/openclaw 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.ts ADDED
@@ -0,0 +1,128 @@
1
+ export type HydraPluginConfig = {
2
+ apiKey: string
3
+ tenantId: string
4
+ subTenantId: string
5
+ autoRecall: boolean
6
+ autoCapture: boolean
7
+ maxRecallResults: number
8
+ recallMode: "fast" | "thinking"
9
+ graphContext: boolean
10
+ ignoreTerm: string
11
+ debug: boolean
12
+ }
13
+
14
+ const KNOWN_KEYS = new Set([
15
+ "apiKey",
16
+ "tenantId",
17
+ "subTenantId",
18
+ "autoRecall",
19
+ "autoCapture",
20
+ "maxRecallResults",
21
+ "recallMode",
22
+ "graphContext",
23
+ "ignoreTerm",
24
+ "debug",
25
+ ])
26
+
27
+ const DEFAULT_SUB_TENANT = "hydra-openclaw-plugin"
28
+ const DEFAULT_IGNORE_TERM = "hydra-ignore"
29
+
30
+ function envOrNull(name: string): string | undefined {
31
+ return typeof process !== "undefined" ? process.env[name] : undefined
32
+ }
33
+
34
+ function resolveEnvVars(value: string): string {
35
+ return value.replace(/\$\{([^}]+)\}/g, (_, name: string) => {
36
+ const val = envOrNull(name)
37
+ if (!val) throw new Error(`Environment variable ${name} is not set`)
38
+ return val
39
+ })
40
+ }
41
+
42
+ export function parseConfig(raw: unknown): HydraPluginConfig {
43
+ const cfg =
44
+ raw && typeof raw === "object" && !Array.isArray(raw)
45
+ ? (raw as Record<string, unknown>)
46
+ : {}
47
+
48
+ const unknown = Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k))
49
+ if (unknown.length > 0) {
50
+ throw new Error(`hydra-db: unrecognized config keys: ${unknown.join(", ")}`)
51
+ }
52
+
53
+ const apiKey =
54
+ typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
55
+ ? resolveEnvVars(cfg.apiKey)
56
+ : envOrNull("HYDRA_OPENCLAW_API_KEY")
57
+
58
+ if (!apiKey) {
59
+ throw new Error(
60
+ "hydra-db: apiKey is required — set it in plugin config or via HYDRA_OPENCLAW_API_KEY env var",
61
+ )
62
+ }
63
+
64
+ const tenantId =
65
+ typeof cfg.tenantId === "string" && cfg.tenantId.length > 0
66
+ ? resolveEnvVars(cfg.tenantId)
67
+ : envOrNull("HYDRA_OPENCLAW_TENANT_ID")
68
+
69
+ if (!tenantId) {
70
+ throw new Error(
71
+ "hydra-db: tenantId is required — set it in plugin config or via HYDRA_OPENCLAW_TENANT_ID env var",
72
+ )
73
+ }
74
+
75
+ const subTenantId =
76
+ typeof cfg.subTenantId === "string" && cfg.subTenantId.length > 0
77
+ ? cfg.subTenantId
78
+ : DEFAULT_SUB_TENANT
79
+
80
+ return {
81
+ apiKey,
82
+ tenantId,
83
+ subTenantId,
84
+ autoRecall: (cfg.autoRecall as boolean) ?? true,
85
+ autoCapture: (cfg.autoCapture as boolean) ?? true,
86
+ maxRecallResults: (cfg.maxRecallResults as number) ?? 10,
87
+ recallMode:
88
+ cfg.recallMode === "thinking"
89
+ ? ("thinking" as const)
90
+ : ("fast" as const),
91
+ graphContext: (cfg.graphContext as boolean) ?? true,
92
+ ignoreTerm:
93
+ typeof cfg.ignoreTerm === "string" && cfg.ignoreTerm.length > 0
94
+ ? cfg.ignoreTerm
95
+ : DEFAULT_IGNORE_TERM,
96
+ debug: (cfg.debug as boolean) ?? false,
97
+ }
98
+ }
99
+
100
+ export function tryParseConfig(raw: unknown): HydraPluginConfig | null {
101
+ try {
102
+ return parseConfig(raw)
103
+ } catch {
104
+ return null
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Permissive schema parse — validates key names but does NOT require credentials.
110
+ * This lets the plugin load so the onboarding wizard can run.
111
+ */
112
+ function parseConfigSoft(raw: unknown): Record<string, unknown> {
113
+ const cfg =
114
+ raw && typeof raw === "object" && !Array.isArray(raw)
115
+ ? (raw as Record<string, unknown>)
116
+ : {}
117
+
118
+ const unknown = Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k))
119
+ if (unknown.length > 0) {
120
+ throw new Error(`hydra-db: unrecognized config keys: ${unknown.join(", ")}`)
121
+ }
122
+
123
+ return cfg
124
+ }
125
+
126
+ export const hydraConfigSchema = {
127
+ parse: parseConfigSoft,
128
+ }
package/context.ts ADDED
@@ -0,0 +1,191 @@
1
+ import type {
2
+ PathTriplet,
3
+ RecallResponse,
4
+ ScoredPath,
5
+ VectorChunk,
6
+ } from "./types/hydra.ts"
7
+
8
+ function formatTriplet(triplet: PathTriplet): string {
9
+ const src = triplet.source?.name ?? "?"
10
+ const rel = triplet.relation
11
+ const predicate =
12
+ rel?.raw_predicate ?? rel?.canonical_predicate ?? "related to"
13
+ const tgt = triplet.target?.name ?? "?"
14
+ const ctx = rel?.context ? ` [${rel.context}]` : ""
15
+ return ` (${src}) —[${predicate}]→ (${tgt})${ctx}`
16
+ }
17
+
18
+ export function buildRecalledContext(
19
+ response: RecallResponse,
20
+ opts?: {
21
+ maxGroupOccurrences?: number
22
+ minEvidenceScore?: number
23
+ },
24
+ ): string {
25
+ const minScore = opts?.minEvidenceScore ?? 0.4
26
+
27
+ const chunks = response.chunks ?? []
28
+ const graphCtx = response.graph_context ?? {
29
+ query_paths: [],
30
+ chunk_relations: [],
31
+ chunk_id_to_group_ids: {},
32
+ }
33
+ const extraContextMap = response.additional_context ?? {}
34
+
35
+ const rawRelations: ScoredPath[] = graphCtx.chunk_relations ?? []
36
+ const relationIndex: Record<string, ScoredPath> = {}
37
+
38
+ for (let idx = 0; idx < rawRelations.length; idx++) {
39
+ const relation = rawRelations[idx]!
40
+ if ((relation.relevancy_score ?? 0) < minScore) continue
41
+ const groupId = relation.group_id ?? `p_${idx}`
42
+ relationIndex[groupId] = relation
43
+ }
44
+
45
+ const chunkToGroupIds = graphCtx.chunk_id_to_group_ids ?? {}
46
+ const consumedExtraIds = new Set<string>()
47
+ const chunkSections: string[] = []
48
+
49
+ for (let i = 0; i < chunks.length; i++) {
50
+ const chunk = chunks[i]!
51
+ const lines: string[] = []
52
+
53
+ lines.push(`Chunk ${i + 1}`)
54
+
55
+ const meta = chunk.document_metadata ?? {}
56
+ const title =
57
+ chunk.source_title || (meta as Record<string, string>).title
58
+ if (title) {
59
+ lines.push(`Source: ${title}`)
60
+ }
61
+
62
+ lines.push(chunk.chunk_content ?? "")
63
+
64
+ const chunkUuid = chunk.chunk_uuid
65
+ const linkedGroupIds = chunkToGroupIds[chunkUuid] ?? []
66
+
67
+ const matchedRelations: ScoredPath[] = []
68
+
69
+ for (const gid of linkedGroupIds) {
70
+ if (relationIndex[gid]) {
71
+ matchedRelations.push(relationIndex[gid]!)
72
+ }
73
+ }
74
+
75
+ if (matchedRelations.length === 0) {
76
+ for (const rel of Object.values(relationIndex)) {
77
+ const triplets = rel.triplets ?? []
78
+ const hasChunk = triplets.some(
79
+ (t) => t.relation?.chunk_id === chunkUuid,
80
+ )
81
+ if (hasChunk) {
82
+ matchedRelations.push(rel)
83
+ }
84
+ }
85
+ }
86
+
87
+ const relationLines: string[] = []
88
+ for (const rel of matchedRelations) {
89
+ const triplets = rel.triplets ?? []
90
+ if (triplets.length > 0) {
91
+ for (const triplet of triplets) {
92
+ relationLines.push(formatTriplet(triplet))
93
+ }
94
+ } else if (rel.combined_context) {
95
+ relationLines.push(` ${rel.combined_context}`)
96
+ }
97
+ }
98
+
99
+ if (relationLines.length > 0) {
100
+ lines.push("Graph Relations:")
101
+ lines.push(...relationLines)
102
+ }
103
+
104
+ const extraIds = chunk.extra_context_ids ?? []
105
+ if (extraIds.length > 0 && Object.keys(extraContextMap).length > 0) {
106
+ const extraLines: string[] = []
107
+ for (const ctxId of extraIds) {
108
+ if (consumedExtraIds.has(ctxId)) continue
109
+ const extraChunk = extraContextMap[ctxId]
110
+ if (extraChunk) {
111
+ consumedExtraIds.add(ctxId)
112
+ const extraContent = extraChunk.chunk_content ?? ""
113
+ const extraTitle = extraChunk.source_title ?? ""
114
+ if (extraTitle) {
115
+ extraLines.push(
116
+ ` Related Context (${extraTitle}): ${extraContent}`,
117
+ )
118
+ } else {
119
+ extraLines.push(` Related Context: ${extraContent}`)
120
+ }
121
+ }
122
+ }
123
+ if (extraLines.length > 0) {
124
+ lines.push("Extra Context:")
125
+ lines.push(...extraLines)
126
+ }
127
+ }
128
+
129
+ chunkSections.push(lines.join("\n"))
130
+ }
131
+
132
+ const entityPathLines: string[] = []
133
+ const rawPaths: ScoredPath[] = graphCtx.query_paths ?? []
134
+ for (const path of rawPaths) {
135
+ if (path.combined_context) {
136
+ entityPathLines.push(path.combined_context)
137
+ } else {
138
+ const triplets = path.triplets ?? []
139
+ const segments: string[] = []
140
+ for (const pt of triplets) {
141
+ const s = pt.source?.name
142
+ const rel = pt.relation
143
+ const p =
144
+ rel?.raw_predicate ??
145
+ rel?.canonical_predicate ??
146
+ "related to"
147
+ const t = pt.target?.name
148
+ segments.push(`(${s} -> ${p} -> ${t})`)
149
+ }
150
+ if (segments.length > 0) {
151
+ entityPathLines.push(segments.join(" -> "))
152
+ }
153
+ }
154
+ }
155
+
156
+ const output: string[] = []
157
+
158
+ if (entityPathLines.length > 0) {
159
+ output.push("=== ENTITY PATHS ===")
160
+ output.push(entityPathLines.join("\n"))
161
+ output.push("")
162
+ }
163
+
164
+ if (chunkSections.length > 0) {
165
+ output.push("=== CONTEXT ===")
166
+ output.push(chunkSections.join("\n\n---\n\n"))
167
+ }
168
+
169
+ return output.join("\n")
170
+ }
171
+
172
+ export function envelopeForInjection(contextBody: string): string {
173
+ if (!contextBody.trim()) return ""
174
+
175
+ const lines = [
176
+ "<hydra-context>",
177
+ "[MEMORIES AND PAST CONVERSATIONS — retrieved by Hydra DB]",
178
+ "",
179
+ "Below are memories and knowledge-graph connections that may be relevant",
180
+ "to the current conversation. Integrate them naturally when they add value.",
181
+ "If a memory contradicts something the user just said, prefer the user's",
182
+ "latest statement. Never quote these verbatim or reveal that you are",
183
+ "reading from a memory store.",
184
+ "",
185
+ contextBody,
186
+ "",
187
+ "[END OF MEMORY CONTEXT]",
188
+ "</hydra-context>",
189
+ ]
190
+ return lines.join("\n")
191
+ }
@@ -0,0 +1,101 @@
1
+ import type { HydraClient } from "../client.ts"
2
+ import type { HydraPluginConfig } from "../config.ts"
3
+ import { log } from "../log.ts"
4
+ import { extractAllTurns, filterIgnoredTurns } from "../messages.ts"
5
+ import { toHookSourceId } from "../session.ts"
6
+ import type { ConversationTurn } from "../types/hydra.ts"
7
+
8
+ const MAX_HOOK_TURNS = -1
9
+
10
+ function removeInjectedBlocks(text: string): string {
11
+ return text.replace(/<hydra-context>[\s\S]*?<\/hydra-context>\s*/g, "").trim()
12
+ }
13
+
14
+ export function createIngestionHook(
15
+ client: HydraClient,
16
+ cfg: HydraPluginConfig,
17
+ ) {
18
+ return async (event: Record<string, unknown>, sessionId: string | undefined) => {
19
+ try {
20
+ log.debug(`[capture] hook fired — success=${event.success} msgs=${Array.isArray(event.messages) ? event.messages.length : "N/A"} sid=${sessionId ?? "none"}`)
21
+
22
+ if (!event.success) {
23
+ log.debug("[capture] skipped — event.success is falsy")
24
+ return
25
+ }
26
+ if (!Array.isArray(event.messages) || event.messages.length === 0) {
27
+ log.debug("[capture] skipped — no messages in event")
28
+ return
29
+ }
30
+
31
+ if (!sessionId) {
32
+ log.debug("[capture] skipped — no session id available")
33
+ return
34
+ }
35
+
36
+ const rawTurns = extractAllTurns(event.messages)
37
+ const allTurns = filterIgnoredTurns(rawTurns, cfg.ignoreTerm)
38
+
39
+ if (rawTurns.length > 0 && allTurns.length < rawTurns.length) {
40
+ log.debug(`[capture] filtered ${rawTurns.length - allTurns.length} turns containing ignore term "${cfg.ignoreTerm}"`)
41
+ }
42
+
43
+ if (allTurns.length === 0) {
44
+ log.debug(`[capture] skipped — no user-assistant turns found in ${event.messages.length} messages`)
45
+ const roles = event.messages
46
+ .slice(-5)
47
+ .map((m) => (m && typeof m === "object" ? (m as Record<string, unknown>).role : "?"))
48
+ log.debug(`[capture] last 5 message roles: ${JSON.stringify(roles)}`)
49
+ return
50
+ }
51
+
52
+ const recentTurns = MAX_HOOK_TURNS === -1 ? allTurns : allTurns.slice(-MAX_HOOK_TURNS)
53
+ const turns: ConversationTurn[] = recentTurns.map((t) => ({
54
+ user: removeInjectedBlocks(t.user),
55
+ assistant: removeInjectedBlocks(t.assistant),
56
+ })).filter((t) => t.user.length >= 5 && t.assistant.length >= 5)
57
+
58
+ if (turns.length === 0) {
59
+ log.debug("[capture] skipped — all turns too short after cleaning")
60
+ return
61
+ }
62
+
63
+ const sourceId = toHookSourceId(sessionId)
64
+
65
+ const now = new Date()
66
+ const timestamp = now.toISOString()
67
+ const readableTime = now.toLocaleString("en-US", {
68
+ weekday: "short",
69
+ year: "numeric",
70
+ month: "short",
71
+ day: "numeric",
72
+ hour: "2-digit",
73
+ minute: "2-digit",
74
+ timeZoneName: "short",
75
+ })
76
+
77
+ const annotatedTurns = turns.map((t, i) => ({
78
+ user: i === 0 ? `[Temporal details: ${readableTime}]\n\n${t.user}` : t.user,
79
+ assistant: t.assistant,
80
+ }))
81
+
82
+ log.debug(`[capture] ingesting ${annotatedTurns.length} turns (of ${allTurns.length} total) @ ${timestamp} -> ${sourceId}`)
83
+
84
+ await client.ingestConversation(
85
+ annotatedTurns,
86
+ sourceId,
87
+ {
88
+ metadata: {
89
+ captured_at: timestamp,
90
+ source: "openclaw_hook",
91
+ turn_count: annotatedTurns.length,
92
+ },
93
+ },
94
+ )
95
+
96
+ log.debug("[capture] ingestion succeeded")
97
+ } catch (err) {
98
+ log.error("[capture] hook error", err)
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,46 @@
1
+ import type { HydraClient } from "../client.ts"
2
+ import type { HydraPluginConfig } from "../config.ts"
3
+ import { buildRecalledContext, envelopeForInjection } from "../context.ts"
4
+ import { log } from "../log.ts"
5
+ import { containsIgnoreTerm } from "../messages.ts"
6
+
7
+ export function createRecallHook(
8
+ client: HydraClient,
9
+ cfg: HydraPluginConfig,
10
+ ) {
11
+ return async (event: Record<string, unknown>) => {
12
+ const prompt = event.prompt as string | undefined
13
+ if (!prompt || prompt.length < 5) return
14
+
15
+ if (containsIgnoreTerm(prompt, cfg.ignoreTerm)) {
16
+ log.debug(`recall skipped — prompt contains ignore term "${cfg.ignoreTerm}"`)
17
+ return
18
+ }
19
+
20
+ log.debug(`recall query (${prompt.length} chars)`)
21
+
22
+ try {
23
+ const response = await client.recall(prompt, {
24
+ maxResults: cfg.maxRecallResults,
25
+ mode: cfg.recallMode,
26
+ graphContext: cfg.graphContext,
27
+ })
28
+
29
+ if (!response.chunks || response.chunks.length === 0) {
30
+ log.debug("no memories matched")
31
+ return
32
+ }
33
+
34
+ const body = buildRecalledContext(response)
35
+ if (!body.trim()) return
36
+
37
+ const envelope = envelopeForInjection(body)
38
+
39
+ log.debug(`injecting ${response.chunks.length} chunks (${envelope.length} chars)`)
40
+ return { prependContext: envelope }
41
+ } catch (err) {
42
+ log.error("recall failed", err)
43
+ return
44
+ }
45
+ }
46
+ }
package/index.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import { HydraClient } from "./client.ts"
3
+ import type { HydraPluginConfig } from "./config.ts"
4
+ import { registerOnboardingCli as createOnboardingCliRegistrar, registerOnboardingSlashCommands } from "./commands/onboarding.ts"
5
+ import { registerSlashCommands } from "./commands/slash.ts"
6
+ import { hydraConfigSchema, tryParseConfig } from "./config.ts"
7
+ import { createIngestionHook } from "./hooks/capture.ts"
8
+ import { createRecallHook } from "./hooks/recall.ts"
9
+ import { log } from "./log.ts"
10
+ import { registerDeleteTool } from "./tools/delete.ts"
11
+ import { registerGetTool } from "./tools/get.ts"
12
+ import { registerListTool } from "./tools/list.ts"
13
+ import { registerSearchTool } from "./tools/search.ts"
14
+ import { registerStoreTool } from "./tools/store.ts"
15
+
16
+ const NOT_CONFIGURED_MSG =
17
+ "[hydra-db] Not configured. Run `openclaw hydra onboard` to set up credentials."
18
+
19
+ export default {
20
+ id: "openclaw-hydra-db",
21
+ name: "Hydra DB",
22
+ description:
23
+ "State-of-the-art agentic memory for OpenClaw powered by Hydra DB — auto-capture, recall, and graph-enriched context",
24
+ kind: "memory" as const,
25
+ configSchema: hydraConfigSchema,
26
+
27
+ register(api: OpenClawPluginApi) {
28
+ const cfg = tryParseConfig(api.pluginConfig)
29
+ const cliClient = cfg ? new HydraClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId) : null
30
+
31
+ // Always register ALL CLI commands so they appear in help text.
32
+ // Non-onboard commands guard on credentials at runtime.
33
+ api.registerCli(
34
+ ({ program }: { program: any }) => {
35
+ const root = program
36
+ .command("hydra")
37
+ .description("Hydra DB memory commands")
38
+
39
+ createOnboardingCliRegistrar(cfg ?? undefined)(root)
40
+ registerHydraCliCommands(root, cliClient, cfg)
41
+ },
42
+ { commands: ["hydra"] },
43
+ )
44
+
45
+ if (!cfg) {
46
+ api.registerService({
47
+ id: "openclaw-hydra-db",
48
+ start: () => console.log(NOT_CONFIGURED_MSG),
49
+ stop: () => {},
50
+ })
51
+ return
52
+ }
53
+
54
+ // Full plugin registration — credentials present
55
+ log.init(api.logger, cfg.debug)
56
+
57
+ const client = new HydraClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId)
58
+
59
+ let activeSessionId: string | undefined
60
+ let conversationMessages: unknown[] = []
61
+ const getSessionId = () => activeSessionId
62
+ const getMessages = () => conversationMessages
63
+
64
+ registerSearchTool(api, client, cfg)
65
+ registerStoreTool(api, client, cfg, getSessionId, getMessages)
66
+ registerListTool(api, client, cfg)
67
+ registerDeleteTool(api, client, cfg)
68
+ registerGetTool(api, client, cfg)
69
+
70
+ if (cfg.autoRecall) {
71
+ const onRecall = createRecallHook(client, cfg)
72
+ api.on(
73
+ "before_agent_start",
74
+ (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
75
+ if (ctx.sessionId) activeSessionId = ctx.sessionId as string
76
+ if (Array.isArray(event.messages)) conversationMessages = event.messages
77
+ log.debug(`[session] before_agent_start — sid=${activeSessionId ?? "none"} msgs=${conversationMessages.length}`)
78
+ return onRecall(event)
79
+ },
80
+ )
81
+ }
82
+
83
+ if (cfg.autoCapture) {
84
+ const captureHandler = createIngestionHook(client, cfg)
85
+ api.on(
86
+ "agent_end",
87
+ (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
88
+ if (ctx.sessionId) activeSessionId = ctx.sessionId as string
89
+ if (Array.isArray(event.messages)) conversationMessages = event.messages
90
+ log.debug(`[session] agent_end — sid=${activeSessionId ?? "none"} msgs=${conversationMessages.length} ctxKeys=${Object.keys(ctx).join(",")}`)
91
+ return captureHandler(event, activeSessionId)
92
+ },
93
+ )
94
+ }
95
+
96
+ registerSlashCommands(api, client, cfg, getSessionId)
97
+ registerOnboardingSlashCommands(api, client, cfg)
98
+
99
+ api.registerService({
100
+ id: "openclaw-hydra-db",
101
+ start: () => log.info("plugin started"),
102
+ stop: () => log.info("plugin stopped"),
103
+ })
104
+ },
105
+ }
106
+
107
+ /**
108
+ * Register all `hydra *` CLI subcommands.
109
+ * Commands other than `onboard` guard on valid credentials at runtime.
110
+ */
111
+ function registerHydraCliCommands(
112
+ root: any,
113
+ client: HydraClient | null,
114
+ cfg: HydraPluginConfig | null,
115
+ ): void {
116
+ const requireCreds = (): { client: HydraClient; cfg: HydraPluginConfig } | null => {
117
+ if (client && cfg) return { client, cfg }
118
+ console.error(NOT_CONFIGURED_MSG)
119
+ return null
120
+ }
121
+
122
+ root
123
+ .command("search")
124
+ .argument("<query>", "Search query")
125
+ .option("--limit <n>", "Max results", "10")
126
+ .action(async (query: string, opts: { limit: string }) => {
127
+ const ctx = requireCreds()
128
+ if (!ctx) return
129
+
130
+ const limit = Number.parseInt(opts.limit, 10) || 10
131
+ const res = await ctx.client.recall(query, {
132
+ maxResults: limit,
133
+ mode: ctx.cfg.recallMode,
134
+ graphContext: ctx.cfg.graphContext,
135
+ })
136
+
137
+ if (!res.chunks || res.chunks.length === 0) {
138
+ console.log("No memories found.")
139
+ return
140
+ }
141
+
142
+ for (const chunk of res.chunks) {
143
+ const score = chunk.relevancy_score != null
144
+ ? ` (${(chunk.relevancy_score * 100).toFixed(0)}%)`
145
+ : ""
146
+ const title = chunk.source_title ? `[${chunk.source_title}] ` : ""
147
+ console.log(`- ${title}${chunk.chunk_content.slice(0, 200)}${score}`)
148
+ }
149
+ })
150
+
151
+ root
152
+ .command("list")
153
+ .description("List all user memories")
154
+ .action(async () => {
155
+ const ctx = requireCreds()
156
+ if (!ctx) return
157
+
158
+ const res = await ctx.client.listMemories()
159
+ const memories = res.user_memories ?? []
160
+ if (memories.length === 0) {
161
+ console.log("No memories stored.")
162
+ return
163
+ }
164
+ for (const m of memories) {
165
+ console.log(`[${m.memory_id}] ${m.memory_content.slice(0, 150)}`)
166
+ }
167
+ console.log(`\nTotal: ${memories.length}`)
168
+ })
169
+
170
+ root
171
+ .command("delete")
172
+ .argument("<memory_id>", "Memory ID to delete")
173
+ .action(async (memoryId: string) => {
174
+ const ctx = requireCreds()
175
+ if (!ctx) return
176
+
177
+ const res = await ctx.client.deleteMemory(memoryId)
178
+ console.log(res.user_memory_deleted ? `Deleted: ${memoryId}` : `Not found: ${memoryId}`)
179
+ })
180
+
181
+ root
182
+ .command("get")
183
+ .argument("<source_id>", "Source ID to fetch")
184
+ .action(async (sourceId: string) => {
185
+ const ctx = requireCreds()
186
+ if (!ctx) return
187
+
188
+ const res = await ctx.client.fetchContent(sourceId)
189
+ if (!res.success || res.error) {
190
+ console.error(`Error: ${res.error ?? "unknown"}`)
191
+ return
192
+ }
193
+ console.log(res.content ?? res.content_base64 ?? "(no text content)")
194
+ })
195
+
196
+ root
197
+ .command("status")
198
+ .description("Show plugin configuration")
199
+ .action(() => {
200
+ const ctx = requireCreds()
201
+ if (!ctx) return
202
+
203
+ console.log(`Tenant: ${ctx.client.getTenantId()}`)
204
+ console.log(`Sub-Tenant: ${ctx.client.getSubTenantId()}`)
205
+ console.log(`Auto-Recall: ${ctx.cfg.autoRecall}`)
206
+ console.log(`Auto-Capture: ${ctx.cfg.autoCapture}`)
207
+ console.log(`Recall Mode: ${ctx.cfg.recallMode}`)
208
+ console.log(`Graph: ${ctx.cfg.graphContext}`)
209
+ console.log(`Max Results: ${ctx.cfg.maxRecallResults}`)
210
+ console.log(`Ignore Term: ${ctx.cfg.ignoreTerm}`)
211
+ })
212
+ }