@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.
@@ -0,0 +1,485 @@
1
+ import * as fs from "node:fs"
2
+ import * as os from "node:os"
3
+ import * as path from "node:path"
4
+ import * as readline from "node:readline"
5
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
6
+ import type { HydraClient } from "../client.ts"
7
+ import type { HydraPluginConfig } from "../config.ts"
8
+ import { log } from "../log.ts"
9
+
10
+ // ── Defaults (used when config is not yet available) ──
11
+
12
+ const DEFAULTS = {
13
+ subTenantId: "hydra-openclaw-plugin",
14
+ ignoreTerm: "hydra-ignore",
15
+ autoRecall: true,
16
+ autoCapture: true,
17
+ maxRecallResults: 10,
18
+ recallMode: "fast" as const,
19
+ graphContext: true,
20
+ debug: false,
21
+ }
22
+
23
+ // ── ANSI helpers ──
24
+
25
+ const c = {
26
+ reset: "\x1b[0m",
27
+ bold: "\x1b[1m",
28
+ dim: "\x1b[2m",
29
+ cyan: "\x1b[36m",
30
+ green: "\x1b[32m",
31
+ yellow: "\x1b[33m",
32
+ red: "\x1b[31m",
33
+ magenta: "\x1b[35m",
34
+ white: "\x1b[37m",
35
+ bgCyan: "\x1b[46m",
36
+ bgGreen: "\x1b[42m",
37
+ black: "\x1b[30m",
38
+ }
39
+
40
+ function mask(value: string, visible = 4): string {
41
+ if (value.length <= visible) return "****"
42
+ return `${"*".repeat(value.length - visible)}${value.slice(-visible)}`
43
+ }
44
+
45
+ // ── Prompt primitives ──
46
+
47
+ function createRl(): readline.Interface {
48
+ return readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ })
52
+ }
53
+
54
+ function ask(rl: readline.Interface, question: string): Promise<string> {
55
+ return new Promise((resolve) => rl.question(question, resolve))
56
+ }
57
+
58
+ async function promptText(
59
+ rl: readline.Interface,
60
+ label: string,
61
+ opts?: { default?: string; required?: boolean; secret?: boolean },
62
+ ): Promise<string> {
63
+ const def = opts?.default
64
+ const hint = def ? `${c.dim} (${def})${c.reset}` : opts?.required ? `${c.red} *${c.reset}` : ""
65
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${hint}${c.dim} ›${c.reset} `
66
+
67
+ while (true) {
68
+ const raw = await ask(rl, prefix)
69
+ const value = raw.trim()
70
+ if (value) return value
71
+ if (def) return def
72
+ if (opts?.required) {
73
+ console.log(` ${c.red}This field is required.${c.reset}`)
74
+ continue
75
+ }
76
+ return ""
77
+ }
78
+ }
79
+
80
+ async function promptChoice(
81
+ rl: readline.Interface,
82
+ label: string,
83
+ choices: string[],
84
+ defaultChoice: string,
85
+ ): Promise<string> {
86
+ const tags = choices
87
+ .map((ch) => (ch === defaultChoice ? `${c.green}${c.bold}${ch}${c.reset}` : `${c.dim}${ch}${c.reset}`))
88
+ .join(`${c.dim} / ${c.reset}`)
89
+
90
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset} ${tags}${c.dim} ›${c.reset} `
91
+
92
+ while (true) {
93
+ const raw = await ask(rl, prefix)
94
+ const value = raw.trim().toLowerCase()
95
+ if (!value) return defaultChoice
96
+ const match = choices.find((ch) => ch.toLowerCase() === value)
97
+ if (match) return match
98
+ console.log(` ${c.yellow}Choose one of: ${choices.join(", ")}${c.reset}`)
99
+ }
100
+ }
101
+
102
+ async function promptBool(
103
+ rl: readline.Interface,
104
+ label: string,
105
+ defaultVal: boolean,
106
+ ): Promise<boolean> {
107
+ const hint = defaultVal
108
+ ? `${c.dim} (${c.green}Y${c.reset}${c.dim}/n)${c.reset}`
109
+ : `${c.dim} (y/${c.green}N${c.reset}${c.dim})${c.reset}`
110
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${hint}${c.dim} ›${c.reset} `
111
+
112
+ const raw = await ask(rl, prefix)
113
+ const value = raw.trim().toLowerCase()
114
+ if (!value) return defaultVal
115
+ return value === "y" || value === "yes" || value === "true"
116
+ }
117
+
118
+ async function promptNumber(
119
+ rl: readline.Interface,
120
+ label: string,
121
+ defaultVal: number,
122
+ min: number,
123
+ max: number,
124
+ ): Promise<number> {
125
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${c.dim} (${defaultVal}) [${min}–${max}] ›${c.reset} `
126
+
127
+ while (true) {
128
+ const raw = await ask(rl, prefix)
129
+ const value = raw.trim()
130
+ if (!value) return defaultVal
131
+ const n = Number.parseInt(value, 10)
132
+ if (!Number.isNaN(n) && n >= min && n <= max) return n
133
+ console.log(` ${c.yellow}Enter a number between ${min} and ${max}.${c.reset}`)
134
+ }
135
+ }
136
+
137
+ // ── Banner ──
138
+
139
+ function printBanner(): void {
140
+ console.log()
141
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ${c.reset}`)
142
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ◆ Hydra DB — Onboard ${c.reset}`)
143
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ${c.reset}`)
144
+ console.log()
145
+ }
146
+
147
+ function printSection(title: string): void {
148
+ console.log()
149
+ console.log(` ${c.magenta}${c.bold}── ${title} ${"─".repeat(Math.max(0, 40 - title.length))}${c.reset}`)
150
+ console.log()
151
+ }
152
+
153
+ function printSummaryRow(label: string, value: string, sensitive = false): void {
154
+ const display = sensitive ? mask(value) : value
155
+ console.log(` ${c.dim}│${c.reset} ${c.bold}${label.padEnd(18)}${c.reset} ${c.cyan}${display}${c.reset}`)
156
+ }
157
+
158
+ function printSuccess(msg: string): void {
159
+ console.log()
160
+ console.log(` ${c.bgGreen}${c.black}${c.bold} ✓ ${c.reset} ${c.green}${msg}${c.reset}`)
161
+ console.log()
162
+ }
163
+
164
+ // ── Config output ──
165
+
166
+ type WizardResult = {
167
+ apiKey: string
168
+ tenantId: string
169
+ subTenantId: string
170
+ ignoreTerm: string
171
+ autoRecall?: boolean
172
+ autoCapture?: boolean
173
+ maxRecallResults?: number
174
+ recallMode?: "fast" | "thinking"
175
+ graphContext?: boolean
176
+ debug?: boolean
177
+ }
178
+
179
+ function buildConfigObj(result: WizardResult): Record<string, unknown> {
180
+ const obj: Record<string, unknown> = {}
181
+
182
+ obj.apiKey = result.apiKey
183
+ obj.tenantId = result.tenantId
184
+
185
+ if (result.subTenantId !== DEFAULTS.subTenantId) {
186
+ obj.subTenantId = result.subTenantId
187
+ }
188
+ if (result.ignoreTerm !== DEFAULTS.ignoreTerm) {
189
+ obj.ignoreTerm = result.ignoreTerm
190
+ }
191
+ if (result.autoRecall !== undefined && result.autoRecall !== DEFAULTS.autoRecall) {
192
+ obj.autoRecall = result.autoRecall
193
+ }
194
+ if (result.autoCapture !== undefined && result.autoCapture !== DEFAULTS.autoCapture) {
195
+ obj.autoCapture = result.autoCapture
196
+ }
197
+ if (result.maxRecallResults !== undefined && result.maxRecallResults !== DEFAULTS.maxRecallResults) {
198
+ obj.maxRecallResults = result.maxRecallResults
199
+ }
200
+ if (result.recallMode !== undefined && result.recallMode !== DEFAULTS.recallMode) {
201
+ obj.recallMode = result.recallMode
202
+ }
203
+ if (result.graphContext !== undefined && result.graphContext !== DEFAULTS.graphContext) {
204
+ obj.graphContext = result.graphContext
205
+ }
206
+ if (result.debug !== undefined && result.debug !== DEFAULTS.debug) {
207
+ obj.debug = result.debug
208
+ }
209
+
210
+ return obj
211
+ }
212
+
213
+ // ── Persist to openclaw.json ──
214
+ // Mirrors openclaw's own path resolution (src/config/paths.ts):
215
+ // 1. $OPENCLAW_CONFIG_PATH (explicit override)
216
+ // 2. $OPENCLAW_STATE_DIR/openclaw.json
217
+ // 3. $OPENCLAW_HOME/.openclaw/openclaw.json
218
+ // 4. os.homedir()/.openclaw/openclaw.json (default)
219
+
220
+ function resolveOpenClawConfigPath(): string {
221
+ if (process.env.OPENCLAW_CONFIG_PATH) {
222
+ return process.env.OPENCLAW_CONFIG_PATH
223
+ }
224
+ if (process.env.OPENCLAW_STATE_DIR) {
225
+ return path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json")
226
+ }
227
+ if (process.env.OPENCLAW_HOME) {
228
+ return path.join(process.env.OPENCLAW_HOME, ".openclaw", "openclaw.json")
229
+ }
230
+ return path.join(os.homedir(), ".openclaw", "openclaw.json")
231
+ }
232
+
233
+ const OPENCLAW_CONFIG_PATH = resolveOpenClawConfigPath()
234
+
235
+ function persistConfig(configObj: Record<string, unknown>): boolean {
236
+ try {
237
+ const raw = fs.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8")
238
+ const root = JSON.parse(raw)
239
+
240
+ if (!root.plugins) root.plugins = {}
241
+ if (!root.plugins.entries) root.plugins.entries = {}
242
+ if (!root.plugins.entries["openclaw-hydra-db"]) {
243
+ root.plugins.entries["openclaw-hydra-db"] = { enabled: true }
244
+ }
245
+
246
+ root.plugins.entries["openclaw-hydra-db"].config = configObj
247
+
248
+ fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(root, null, 2) + "\n")
249
+ return true
250
+ } catch {
251
+ return false
252
+ }
253
+ }
254
+
255
+ // ── Wizards ──
256
+
257
+ async function runBasicWizard(cfg?: HydraPluginConfig): Promise<void> {
258
+ const rl = createRl()
259
+
260
+ try {
261
+ printBanner()
262
+ console.log(` ${c.dim}Configure the essential settings for Hydra DB.${c.reset}`)
263
+ console.log(` ${c.dim}Press Enter to accept defaults shown in parentheses.${c.reset}`)
264
+
265
+ printSection("Credentials")
266
+
267
+ const apiKey = await promptText(rl, "API Key", {
268
+ required: true,
269
+ secret: true,
270
+ })
271
+
272
+ const tenantId = await promptText(rl, "Tenant ID", {
273
+ required: true,
274
+ })
275
+
276
+ printSection("Customisation")
277
+
278
+ const subTenantId = await promptText(rl, "Sub-Tenant ID", {
279
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
280
+ })
281
+
282
+ const ignoreTerm = await promptText(rl, "Ignore Term", {
283
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
284
+ })
285
+
286
+ const result: WizardResult = { apiKey, tenantId, subTenantId, ignoreTerm }
287
+ const configObj = buildConfigObj(result)
288
+
289
+ // ── Summary ──
290
+
291
+ printSection("Summary")
292
+
293
+ console.log(` ${c.dim}┌${"─".repeat(50)}${c.reset}`)
294
+ printSummaryRow("API Key", apiKey, true)
295
+ printSummaryRow("Tenant ID", tenantId)
296
+ printSummaryRow("Sub-Tenant ID", subTenantId)
297
+ printSummaryRow("Ignore Term", ignoreTerm)
298
+ console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
299
+
300
+ // ── Persist config ──
301
+
302
+ const saved = await promptBool(rl, `Write config to ${OPENCLAW_CONFIG_PATH}?`, true)
303
+
304
+ if (saved && persistConfig(configObj)) {
305
+ printSuccess("Config saved! Restart the gateway (`openclaw gateway restart`) to apply.")
306
+ } else if (saved) {
307
+ console.log(` ${c.red}Failed to write config. Add manually:${c.reset}`)
308
+ console.log()
309
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
310
+ console.log(` ${c.cyan}${line}${c.reset}`)
311
+ }
312
+ } else {
313
+ console.log()
314
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-hydra-db.config:${c.reset}`)
315
+ console.log()
316
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
317
+ console.log(` ${c.cyan}${line}${c.reset}`)
318
+ }
319
+ }
320
+
321
+ console.log()
322
+ console.log(` ${c.dim}Run \`hydra onboard --advanced\` to fine-tune all options.${c.reset}`)
323
+ } finally {
324
+ rl.close()
325
+ }
326
+ }
327
+
328
+ async function runAdvancedWizard(cfg?: HydraPluginConfig): Promise<void> {
329
+ const rl = createRl()
330
+
331
+ try {
332
+ printBanner()
333
+ console.log(` ${c.dim}Full configuration wizard — customise every option.${c.reset}`)
334
+ console.log(` ${c.dim}Press Enter to accept defaults shown in parentheses.${c.reset}`)
335
+
336
+ printSection("Credentials")
337
+
338
+ const apiKey = await promptText(rl, "API Key", {
339
+ required: true,
340
+ secret: true,
341
+ })
342
+
343
+ const tenantId = await promptText(rl, "Tenant ID", {
344
+ required: true,
345
+ })
346
+
347
+ const subTenantId = await promptText(rl, "Sub-Tenant ID", {
348
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
349
+ })
350
+
351
+ printSection("Behaviour")
352
+
353
+ const autoRecall = await promptBool(rl, "Enable Auto-Recall?", cfg?.autoRecall ?? DEFAULTS.autoRecall)
354
+ const autoCapture = await promptBool(rl, "Enable Auto-Capture?", cfg?.autoCapture ?? DEFAULTS.autoCapture)
355
+ const ignoreTerm = await promptText(rl, "Ignore Term", {
356
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
357
+ })
358
+
359
+ printSection("Recall Settings")
360
+
361
+ const maxRecallResults = await promptNumber(
362
+ rl, "Max Recall Results", cfg?.maxRecallResults ?? DEFAULTS.maxRecallResults, 1, 50,
363
+ )
364
+ const recallMode = await promptChoice(
365
+ rl, "Recall Mode", ["fast", "thinking"], cfg?.recallMode ?? DEFAULTS.recallMode,
366
+ ) as "fast" | "thinking"
367
+ const graphContext = await promptBool(rl, "Enable Graph Context?", cfg?.graphContext ?? DEFAULTS.graphContext)
368
+
369
+ printSection("Debug")
370
+
371
+ const debug = await promptBool(rl, "Enable Debug Logging?", cfg?.debug ?? DEFAULTS.debug)
372
+
373
+ const result: WizardResult = {
374
+ apiKey,
375
+ tenantId,
376
+ subTenantId,
377
+ ignoreTerm,
378
+ autoRecall,
379
+ autoCapture,
380
+ maxRecallResults,
381
+ recallMode,
382
+ graphContext,
383
+ debug,
384
+ }
385
+
386
+ // ── Summary ──
387
+
388
+ printSection("Summary")
389
+
390
+ console.log(` ${c.dim}┌${"─".repeat(50)}${c.reset}`)
391
+ printSummaryRow("API Key", apiKey, true)
392
+ printSummaryRow("Tenant ID", tenantId)
393
+ printSummaryRow("Sub-Tenant ID", subTenantId)
394
+ printSummaryRow("Auto-Recall", String(autoRecall))
395
+ printSummaryRow("Auto-Capture", String(autoCapture))
396
+ printSummaryRow("Ignore Term", ignoreTerm)
397
+ printSummaryRow("Max Results", String(maxRecallResults))
398
+ printSummaryRow("Recall Mode", recallMode)
399
+ printSummaryRow("Graph Context", String(graphContext))
400
+ printSummaryRow("Debug", String(debug))
401
+ console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
402
+
403
+ // ── Persist config ──
404
+
405
+ const configObj = buildConfigObj(result)
406
+ const saved = await promptBool(rl, `Write config to ${OPENCLAW_CONFIG_PATH}?`, true)
407
+
408
+ if (saved && persistConfig(configObj)) {
409
+ printSuccess("Config saved! Restart the gateway (`openclaw gateway restart`) to apply.")
410
+ } else if (saved) {
411
+ console.log(` ${c.red}Failed to write config. Add manually:${c.reset}`)
412
+ console.log()
413
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
414
+ console.log(` ${c.cyan}${line}${c.reset}`)
415
+ }
416
+ } else {
417
+ console.log()
418
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-hydra-db.config:${c.reset}`)
419
+ console.log()
420
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
421
+ console.log(` ${c.cyan}${line}${c.reset}`)
422
+ }
423
+ }
424
+ } finally {
425
+ rl.close()
426
+ }
427
+ }
428
+
429
+ // ── Registration (CLI + Slash) ──
430
+
431
+ export function registerOnboardingCli(
432
+ cfg?: HydraPluginConfig,
433
+ ): (root: any) => void {
434
+ return (root: any) => {
435
+ root
436
+ .command("onboard")
437
+ .description("Interactive Hydra DB onboarding wizard")
438
+ .option("--advanced", "Configure all options (credentials, behaviour, recall, debug)")
439
+ .action(async (opts: { advanced?: boolean }) => {
440
+ if (opts.advanced) {
441
+ await runAdvancedWizard(cfg)
442
+ } else {
443
+ await runBasicWizard(cfg)
444
+ }
445
+ })
446
+ }
447
+ }
448
+
449
+ export function registerOnboardingSlashCommands(
450
+ api: OpenClawPluginApi,
451
+ client: HydraClient,
452
+ cfg: HydraPluginConfig,
453
+ ): void {
454
+ api.registerCommand({
455
+ name: "hydra-onboard",
456
+ description: "Show Hydra plugin config status (run `hydra onboard` in CLI for interactive wizard)",
457
+ acceptsArgs: false,
458
+ requireAuth: false,
459
+ handler: async () => {
460
+ try {
461
+ const lines: string[] = [
462
+ "=== Hydra DB — Current Config ===",
463
+ "",
464
+ ` API Key: ${cfg.apiKey ? `${mask(cfg.apiKey)} ✓` : "NOT SET ✗"}`,
465
+ ` Tenant ID: ${cfg.tenantId ? `${mask(cfg.tenantId, 8)} ✓` : "NOT SET ✗"}`,
466
+ ` Sub-Tenant: ${client.getSubTenantId()}`,
467
+ ` Ignore Term: ${cfg.ignoreTerm}`,
468
+ ` Auto-Recall: ${cfg.autoRecall}`,
469
+ ` Auto-Capture: ${cfg.autoCapture}`,
470
+ ` Recall Mode: ${cfg.recallMode}`,
471
+ ` Graph Context: ${cfg.graphContext}`,
472
+ ` Max Results: ${cfg.maxRecallResults}`,
473
+ ` Debug: ${cfg.debug}`,
474
+ "",
475
+ "Tip: Run `hydra onboard` in the CLI for an interactive configuration wizard,",
476
+ " or `hydra onboard --advanced` for all options.",
477
+ ]
478
+ return { text: lines.join("\n") }
479
+ } catch (err) {
480
+ log.error("/hydra-onboard", err)
481
+ return { text: "Failed to show status. Check logs." }
482
+ }
483
+ },
484
+ })
485
+ }
@@ -0,0 +1,138 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import type { HydraClient } from "../client.ts"
3
+ import type { HydraPluginConfig } from "../config.ts"
4
+ import { log } from "../log.ts"
5
+ import { toToolSourceId } from "../session.ts"
6
+
7
+ function preview(text: string, max = 80): string {
8
+ return text.length > max ? `${text.slice(0, max)}…` : text
9
+ }
10
+
11
+ export function registerSlashCommands(
12
+ api: OpenClawPluginApi,
13
+ client: HydraClient,
14
+ cfg: HydraPluginConfig,
15
+ getSessionId: () => string | undefined,
16
+ ): void {
17
+ api.registerCommand({
18
+ name: "hydra-remember",
19
+ description: "Save a piece of information to Hydra memory",
20
+ acceptsArgs: true,
21
+ requireAuth: true,
22
+ handler: async (ctx: { args?: string }) => {
23
+ const text = ctx.args?.trim()
24
+ if (!text) return { text: "Usage: /hydra-remember <text to store>" }
25
+
26
+ try {
27
+ const sid = getSessionId()
28
+ const sourceId = sid ? toToolSourceId(sid) : undefined
29
+ await client.ingestText(text, { sourceId, title: "Manual Memory", infer: true })
30
+ return { text: `Saved: "${preview(text, 60)}"` }
31
+ } catch (err) {
32
+ log.error("/hydra-remember", err)
33
+ return { text: "Failed to save. Check logs." }
34
+ }
35
+ },
36
+ })
37
+
38
+ api.registerCommand({
39
+ name: "hydra-recall",
40
+ description: "Search your Hydra memories",
41
+ acceptsArgs: true,
42
+ requireAuth: true,
43
+ handler: async (ctx: { args?: string }) => {
44
+ const query = ctx.args?.trim()
45
+ if (!query) return { text: "Usage: /hydra-recall <query>" }
46
+
47
+ try {
48
+ const res = await client.recall(query, {
49
+ maxResults: cfg.maxRecallResults,
50
+ mode: cfg.recallMode,
51
+ graphContext: cfg.graphContext,
52
+ })
53
+
54
+ if (!res.chunks || res.chunks.length === 0) {
55
+ return { text: `No memories found for "${query}"` }
56
+ }
57
+
58
+ const lines = res.chunks.slice(0, 10).map((c, i) => {
59
+ const score = c.relevancy_score != null ? ` (${Math.round(c.relevancy_score * 100)}%)` : ""
60
+ const title = c.source_title ? ` [${c.source_title}]` : ""
61
+ return `${i + 1}.${title} ${preview(c.chunk_content, 120)}${score}`
62
+ })
63
+
64
+ return { text: `Found ${res.chunks.length} chunks:\n\n${lines.join("\n")}` }
65
+ } catch (err) {
66
+ log.error("/hydra-recall", err)
67
+ return { text: "Recall failed. Check logs." }
68
+ }
69
+ },
70
+ })
71
+
72
+ api.registerCommand({
73
+ name: "hydra-list",
74
+ description: "List all stored user memories",
75
+ acceptsArgs: false,
76
+ requireAuth: true,
77
+ handler: async () => {
78
+ try {
79
+ const res = await client.listMemories()
80
+ const memories = res.user_memories ?? []
81
+ if (memories.length === 0) return { text: "No memories stored yet." }
82
+
83
+ const lines = memories.map(
84
+ (m, i) => `${i + 1}. [${m.memory_id}] ${preview(m.memory_content, 100)}`,
85
+ )
86
+ return { text: `${memories.length} memories:\n\n${lines.join("\n")}` }
87
+ } catch (err) {
88
+ log.error("/hydra-list", err)
89
+ return { text: "Failed to list memories. Check logs." }
90
+ }
91
+ },
92
+ })
93
+
94
+ api.registerCommand({
95
+ name: "hydra-delete",
96
+ description: "Delete a specific memory by its ID",
97
+ acceptsArgs: true,
98
+ requireAuth: true,
99
+ handler: async (ctx: { args?: string }) => {
100
+ const memoryId = ctx.args?.trim()
101
+ if (!memoryId) return { text: "Usage: /hydra-delete <memory_id>" }
102
+
103
+ try {
104
+ const res = await client.deleteMemory(memoryId)
105
+ if (res.user_memory_deleted) {
106
+ return { text: `Deleted memory: ${memoryId}` }
107
+ }
108
+ return { text: `Memory ${memoryId} was not found or already deleted.` }
109
+ } catch (err) {
110
+ log.error("/hydra-delete", err)
111
+ return { text: "Delete failed. Check logs." }
112
+ }
113
+ },
114
+ })
115
+
116
+ api.registerCommand({
117
+ name: "hydra-get",
118
+ description: "Fetch the content of a specific source by its ID",
119
+ acceptsArgs: true,
120
+ requireAuth: true,
121
+ handler: async (ctx: { args?: string }) => {
122
+ const sourceId = ctx.args?.trim()
123
+ if (!sourceId) return { text: "Usage: /hydra-get <source_id>" }
124
+
125
+ try {
126
+ const res = await client.fetchContent(sourceId)
127
+ if (!res.success || res.error) {
128
+ return { text: `Could not fetch source ${sourceId}: ${res.error ?? "unknown error"}` }
129
+ }
130
+ const content = res.content ?? res.content_base64 ?? "(no text content)"
131
+ return { text: `Source: ${sourceId}\n\n${preview(content, 2000)}` }
132
+ } catch (err) {
133
+ log.error("/hydra-get", err)
134
+ return { text: "Fetch failed. Check logs." }
135
+ }
136
+ },
137
+ })
138
+ }