@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/.github/workflows/publish.yaml +40 -0
- package/README.md +172 -0
- package/client.ts +214 -0
- package/commands/cli.ts +97 -0
- package/commands/onboarding.ts +485 -0
- package/commands/slash.ts +138 -0
- package/config.ts +128 -0
- package/context.ts +191 -0
- package/hooks/capture.ts +101 -0
- package/hooks/recall.ts +46 -0
- package/index.ts +212 -0
- package/log.ts +48 -0
- package/messages.ts +88 -0
- package/openclaw.plugin.json +74 -0
- package/package.json +27 -0
- package/session.ts +11 -0
- package/tools/delete.ts +54 -0
- package/tools/get.ts +57 -0
- package/tools/list.ts +56 -0
- package/tools/search.ts +64 -0
- package/tools/store.ts +116 -0
- package/tsconfig.json +23 -0
- package/types/hydra.ts +166 -0
- package/types/openclaw.d.ts +19 -0
|
@@ -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
|
+
}
|