@usecortex_ai/openclaw-cortex-ai 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -47,6 +47,7 @@ Or configure directly in `openclaw.json`:
47
47
  | `maxRecallResults` | `number` | `10` | Max memory chunks injected into context per turn |
48
48
  | `recallMode` | `string` | `"fast"` | `"fast"` or `"thinking"` (deeper personalised recall with graph traversal) |
49
49
  | `graphContext` | `boolean` | `true` | Include knowledge graph relations in recalled context |
50
+ | `ignoreTerm` | `string` | `"cortex-ignore"` | Messages containing this term are excluded from recall & capture |
50
51
  | `debug` | `boolean` | `false` | Verbose debug logs |
51
52
 
52
53
  ## How It Works
@@ -54,10 +55,28 @@ Or configure directly in `openclaw.json`:
54
55
  - **Auto-Recall** — Before every AI turn, queries Cortex (`/recall/recall_preferences`) for relevant memories and injects graph-enriched context (entity paths, chunk relations, extra context).
55
56
  - **Auto-Capture** — After every AI turn, the last user/assistant exchange is sent to Cortex (`/memories/add_memory`) as conversation pairs with `infer: true` and `upsert: true`. The session ID is used as `source_id` so Cortex groups exchanges per session and builds a knowledge graph automatically.
56
57
 
58
+ ## Interactive Onboarding
59
+
60
+ Run the interactive CLI wizard to configure Cortex AI:
61
+
62
+ ```bash
63
+ # Basic onboarding (API key, tenant ID, sub-tenant, ignore term)
64
+ openclaw cortex onboard
65
+
66
+ # Advanced onboarding (all options including recall mode, graph context, etc.)
67
+ openclaw cortex onboard --advanced
68
+ ```
69
+
70
+ The wizard guides you through configuration with colored prompts, validates inputs, and outputs:
71
+ - `.env` file lines for credentials
72
+ - Plugin config JSON for non-default settings
73
+ - A summary table with masked sensitive values
74
+
57
75
  ## Slash Commands
58
76
 
59
77
  | Command | Description |
60
78
  | --------------------------- | ------------------------------------- |
79
+ | `/cortex-onboard` | Show current configuration status |
61
80
  | `/cortex-remember <text>` | Save something to Cortex memory |
62
81
  | `/cortex-recall <query>` | Search memories with relevance scores |
63
82
  | `/cortex-list` | List all stored user memories |
@@ -74,11 +93,13 @@ Or configure directly in `openclaw.json`:
74
93
  ## CLI
75
94
 
76
95
  ```bash
77
- openclaw cortex search <query> # Search memories
78
- # List all user memories
79
- openclaw cortex delete <id> # Delete a memory
80
- openclaw cortex get <source_id> # Fetch source content
81
- openclaw cortex status # Show plugin configuration
96
+ openclaw cortex onboard # Interactive onboarding wizard
97
+ openclaw cortex onboard --advanced # Advanced onboarding wizard
98
+ openclaw cortex search <query> # Search memories
99
+ openclaw cortex list # List all user memories
100
+ openclaw cortex delete <id> # Delete a memory
101
+ openclaw cortex get <source_id> # Fetch source content
102
+ openclaw cortex status # Show plugin configuration
82
103
  ```
83
104
 
84
105
  ## Context Injection
@@ -86,5 +107,4 @@ openclaw cortex status # Show plugin configuration
86
107
  Recalled context is injected inside `<cortex-context>` tags containing:
87
108
 
88
109
  - **Entity Paths** — Knowledge graph paths connecting entities relevant to the query
89
- - **
90
- Context Chunks** — Retrieved memory chunks with source titles, graph relations, and linked extra context
110
+ - **Context Chunks** — Retrieved memory chunks with source titles, graph relations, and linked extra context
package/commands/cli.ts CHANGED
@@ -6,6 +6,7 @@ export function registerCliCommands(
6
6
  api: OpenClawPluginApi,
7
7
  client: CortexClient,
8
8
  cfg: CortexPluginConfig,
9
+ onboardingRegistrar?: (root: any) => void,
9
10
  ): void {
10
11
  api.registerCli(
11
12
  ({ program }: { program: any }) => {
@@ -86,7 +87,10 @@ export function registerCliCommands(
86
87
  console.log(`Recall Mode: ${cfg.recallMode}`)
87
88
  console.log(`Graph: ${cfg.graphContext}`)
88
89
  console.log(`Max Results: ${cfg.maxRecallResults}`)
90
+ console.log(`Ignore Term: ${cfg.ignoreTerm}`)
89
91
  })
92
+
93
+ if (onboardingRegistrar) onboardingRegistrar(root)
90
94
  },
91
95
  { commands: ["cortex"] },
92
96
  )
@@ -0,0 +1,470 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
3
+ import * as readline from "node:readline"
4
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
5
+ import type { CortexClient } from "../client.ts"
6
+ import type { CortexPluginConfig } from "../config.ts"
7
+ import { log } from "../log.ts"
8
+
9
+ // ── Defaults (used when config is not yet available) ──
10
+
11
+ const DEFAULTS = {
12
+ subTenantId: "cortex-openclaw-plugin",
13
+ ignoreTerm: "cortex-ignore",
14
+ autoRecall: true,
15
+ autoCapture: true,
16
+ maxRecallResults: 10,
17
+ recallMode: "fast" as const,
18
+ graphContext: true,
19
+ debug: false,
20
+ }
21
+
22
+ // ── ANSI helpers ──
23
+
24
+ const c = {
25
+ reset: "\x1b[0m",
26
+ bold: "\x1b[1m",
27
+ dim: "\x1b[2m",
28
+ cyan: "\x1b[36m",
29
+ green: "\x1b[32m",
30
+ yellow: "\x1b[33m",
31
+ red: "\x1b[31m",
32
+ magenta: "\x1b[35m",
33
+ white: "\x1b[37m",
34
+ bgCyan: "\x1b[46m",
35
+ bgGreen: "\x1b[42m",
36
+ black: "\x1b[30m",
37
+ }
38
+
39
+ function mask(value: string, visible = 4): string {
40
+ if (value.length <= visible) return "****"
41
+ return `${"*".repeat(value.length - visible)}${value.slice(-visible)}`
42
+ }
43
+
44
+ // ── Prompt primitives ──
45
+
46
+ function createRl(): readline.Interface {
47
+ return readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout,
50
+ })
51
+ }
52
+
53
+ function ask(rl: readline.Interface, question: string): Promise<string> {
54
+ return new Promise((resolve) => rl.question(question, resolve))
55
+ }
56
+
57
+ async function promptText(
58
+ rl: readline.Interface,
59
+ label: string,
60
+ opts?: { default?: string; required?: boolean; secret?: boolean },
61
+ ): Promise<string> {
62
+ const def = opts?.default
63
+ const hint = def ? `${c.dim} (${def})${c.reset}` : opts?.required ? `${c.red} *${c.reset}` : ""
64
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${hint}${c.dim} ›${c.reset} `
65
+
66
+ while (true) {
67
+ const raw = await ask(rl, prefix)
68
+ const value = raw.trim()
69
+ if (value) return value
70
+ if (def) return def
71
+ if (opts?.required) {
72
+ console.log(` ${c.red}This field is required.${c.reset}`)
73
+ continue
74
+ }
75
+ return ""
76
+ }
77
+ }
78
+
79
+ async function promptChoice(
80
+ rl: readline.Interface,
81
+ label: string,
82
+ choices: string[],
83
+ defaultChoice: string,
84
+ ): Promise<string> {
85
+ const tags = choices
86
+ .map((ch) => (ch === defaultChoice ? `${c.green}${c.bold}${ch}${c.reset}` : `${c.dim}${ch}${c.reset}`))
87
+ .join(`${c.dim} / ${c.reset}`)
88
+
89
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset} ${tags}${c.dim} ›${c.reset} `
90
+
91
+ while (true) {
92
+ const raw = await ask(rl, prefix)
93
+ const value = raw.trim().toLowerCase()
94
+ if (!value) return defaultChoice
95
+ const match = choices.find((ch) => ch.toLowerCase() === value)
96
+ if (match) return match
97
+ console.log(` ${c.yellow}Choose one of: ${choices.join(", ")}${c.reset}`)
98
+ }
99
+ }
100
+
101
+ async function promptBool(
102
+ rl: readline.Interface,
103
+ label: string,
104
+ defaultVal: boolean,
105
+ ): Promise<boolean> {
106
+ const hint = defaultVal
107
+ ? `${c.dim} (${c.green}Y${c.reset}${c.dim}/n)${c.reset}`
108
+ : `${c.dim} (y/${c.green}N${c.reset}${c.dim})${c.reset}`
109
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${hint}${c.dim} ›${c.reset} `
110
+
111
+ const raw = await ask(rl, prefix)
112
+ const value = raw.trim().toLowerCase()
113
+ if (!value) return defaultVal
114
+ return value === "y" || value === "yes" || value === "true"
115
+ }
116
+
117
+ async function promptNumber(
118
+ rl: readline.Interface,
119
+ label: string,
120
+ defaultVal: number,
121
+ min: number,
122
+ max: number,
123
+ ): Promise<number> {
124
+ const prefix = ` ${c.cyan}?${c.reset} ${c.bold}${label}${c.reset}${c.dim} (${defaultVal}) [${min}–${max}] ›${c.reset} `
125
+
126
+ while (true) {
127
+ const raw = await ask(rl, prefix)
128
+ const value = raw.trim()
129
+ if (!value) return defaultVal
130
+ const n = Number.parseInt(value, 10)
131
+ if (!Number.isNaN(n) && n >= min && n <= max) return n
132
+ console.log(` ${c.yellow}Enter a number between ${min} and ${max}.${c.reset}`)
133
+ }
134
+ }
135
+
136
+ // ── Banner ──
137
+
138
+ function printBanner(): void {
139
+ console.log()
140
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ${c.reset}`)
141
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ◆ Cortex AI — Onboard ${c.reset}`)
142
+ console.log(` ${c.bgCyan}${c.black}${c.bold} ${c.reset}`)
143
+ console.log()
144
+ }
145
+
146
+ function printSection(title: string): void {
147
+ console.log()
148
+ console.log(` ${c.magenta}${c.bold}── ${title} ${"─".repeat(Math.max(0, 40 - title.length))}${c.reset}`)
149
+ console.log()
150
+ }
151
+
152
+ function printSummaryRow(label: string, value: string, sensitive = false): void {
153
+ const display = sensitive ? mask(value) : value
154
+ console.log(` ${c.dim}│${c.reset} ${c.bold}${label.padEnd(18)}${c.reset} ${c.cyan}${display}${c.reset}`)
155
+ }
156
+
157
+ function printSuccess(msg: string): void {
158
+ console.log()
159
+ console.log(` ${c.bgGreen}${c.black}${c.bold} ✓ ${c.reset} ${c.green}${msg}${c.reset}`)
160
+ console.log()
161
+ }
162
+
163
+ // ── Config output ──
164
+
165
+ type WizardResult = {
166
+ apiKey: string
167
+ tenantId: string
168
+ subTenantId: string
169
+ ignoreTerm: string
170
+ autoRecall?: boolean
171
+ autoCapture?: boolean
172
+ maxRecallResults?: number
173
+ recallMode?: "fast" | "thinking"
174
+ graphContext?: boolean
175
+ debug?: boolean
176
+ }
177
+
178
+ function buildConfigObj(result: WizardResult): Record<string, unknown> {
179
+ const obj: Record<string, unknown> = {}
180
+
181
+ obj.apiKey = result.apiKey
182
+ obj.tenantId = result.tenantId
183
+
184
+ if (result.subTenantId !== DEFAULTS.subTenantId) {
185
+ obj.subTenantId = result.subTenantId
186
+ }
187
+ if (result.ignoreTerm !== DEFAULTS.ignoreTerm) {
188
+ obj.ignoreTerm = result.ignoreTerm
189
+ }
190
+ if (result.autoRecall !== undefined && result.autoRecall !== DEFAULTS.autoRecall) {
191
+ obj.autoRecall = result.autoRecall
192
+ }
193
+ if (result.autoCapture !== undefined && result.autoCapture !== DEFAULTS.autoCapture) {
194
+ obj.autoCapture = result.autoCapture
195
+ }
196
+ if (result.maxRecallResults !== undefined && result.maxRecallResults !== DEFAULTS.maxRecallResults) {
197
+ obj.maxRecallResults = result.maxRecallResults
198
+ }
199
+ if (result.recallMode !== undefined && result.recallMode !== DEFAULTS.recallMode) {
200
+ obj.recallMode = result.recallMode
201
+ }
202
+ if (result.graphContext !== undefined && result.graphContext !== DEFAULTS.graphContext) {
203
+ obj.graphContext = result.graphContext
204
+ }
205
+ if (result.debug !== undefined && result.debug !== DEFAULTS.debug) {
206
+ obj.debug = result.debug
207
+ }
208
+
209
+ return obj
210
+ }
211
+
212
+ // ── Persist to ~/.openclaw/openclaw.json ──
213
+
214
+ const OPENCLAW_CONFIG_PATH = path.join(
215
+ process.env.HOME ?? process.env.USERPROFILE ?? "~",
216
+ ".openclaw",
217
+ "openclaw.json",
218
+ )
219
+
220
+ function persistConfig(configObj: Record<string, unknown>): boolean {
221
+ try {
222
+ const raw = fs.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8")
223
+ const root = JSON.parse(raw)
224
+
225
+ if (!root.plugins) root.plugins = {}
226
+ if (!root.plugins.entries) root.plugins.entries = {}
227
+ if (!root.plugins.entries["openclaw-cortex-ai"]) {
228
+ root.plugins.entries["openclaw-cortex-ai"] = { enabled: true }
229
+ }
230
+
231
+ root.plugins.entries["openclaw-cortex-ai"].config = configObj
232
+
233
+ fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(root, null, 2) + "\n")
234
+ return true
235
+ } catch {
236
+ return false
237
+ }
238
+ }
239
+
240
+ // ── Wizards ──
241
+
242
+ async function runBasicWizard(cfg?: CortexPluginConfig): Promise<void> {
243
+ const rl = createRl()
244
+
245
+ try {
246
+ printBanner()
247
+ console.log(` ${c.dim}Configure the essential settings for Cortex AI.${c.reset}`)
248
+ console.log(` ${c.dim}Press Enter to accept defaults shown in parentheses.${c.reset}`)
249
+
250
+ printSection("Credentials")
251
+
252
+ const apiKey = await promptText(rl, "API Key", {
253
+ required: true,
254
+ secret: true,
255
+ })
256
+
257
+ const tenantId = await promptText(rl, "Tenant ID", {
258
+ required: true,
259
+ })
260
+
261
+ printSection("Customisation")
262
+
263
+ const subTenantId = await promptText(rl, "Sub-Tenant ID", {
264
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
265
+ })
266
+
267
+ const ignoreTerm = await promptText(rl, "Ignore Term", {
268
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
269
+ })
270
+
271
+ const result: WizardResult = { apiKey, tenantId, subTenantId, ignoreTerm }
272
+ const configObj = buildConfigObj(result)
273
+
274
+ // ── Summary ──
275
+
276
+ printSection("Summary")
277
+
278
+ console.log(` ${c.dim}┌${"─".repeat(50)}${c.reset}`)
279
+ printSummaryRow("API Key", apiKey, true)
280
+ printSummaryRow("Tenant ID", tenantId)
281
+ printSummaryRow("Sub-Tenant ID", subTenantId)
282
+ printSummaryRow("Ignore Term", ignoreTerm)
283
+ console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
284
+
285
+ // ── Persist config ──
286
+
287
+ const saved = await promptBool(rl, `Write config to ${OPENCLAW_CONFIG_PATH}?`, true)
288
+
289
+ if (saved && persistConfig(configObj)) {
290
+ printSuccess("Config saved! Restart the gateway (`openclaw gateway restart`) to apply.")
291
+ } else if (saved) {
292
+ console.log(` ${c.red}Failed to write config. Add manually:${c.reset}`)
293
+ console.log()
294
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
295
+ console.log(` ${c.cyan}${line}${c.reset}`)
296
+ }
297
+ } else {
298
+ console.log()
299
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-cortex-ai.config:${c.reset}`)
300
+ console.log()
301
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
302
+ console.log(` ${c.cyan}${line}${c.reset}`)
303
+ }
304
+ }
305
+
306
+ console.log()
307
+ console.log(` ${c.dim}Run \`cortex onboard --advanced\` to fine-tune all options.${c.reset}`)
308
+ } finally {
309
+ rl.close()
310
+ }
311
+ }
312
+
313
+ async function runAdvancedWizard(cfg?: CortexPluginConfig): Promise<void> {
314
+ const rl = createRl()
315
+
316
+ try {
317
+ printBanner()
318
+ console.log(` ${c.dim}Full configuration wizard — customise every option.${c.reset}`)
319
+ console.log(` ${c.dim}Press Enter to accept defaults shown in parentheses.${c.reset}`)
320
+
321
+ printSection("Credentials")
322
+
323
+ const apiKey = await promptText(rl, "API Key", {
324
+ required: true,
325
+ secret: true,
326
+ })
327
+
328
+ const tenantId = await promptText(rl, "Tenant ID", {
329
+ required: true,
330
+ })
331
+
332
+ const subTenantId = await promptText(rl, "Sub-Tenant ID", {
333
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
334
+ })
335
+
336
+ printSection("Behaviour")
337
+
338
+ const autoRecall = await promptBool(rl, "Enable Auto-Recall?", cfg?.autoRecall ?? DEFAULTS.autoRecall)
339
+ const autoCapture = await promptBool(rl, "Enable Auto-Capture?", cfg?.autoCapture ?? DEFAULTS.autoCapture)
340
+ const ignoreTerm = await promptText(rl, "Ignore Term", {
341
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
342
+ })
343
+
344
+ printSection("Recall Settings")
345
+
346
+ const maxRecallResults = await promptNumber(
347
+ rl, "Max Recall Results", cfg?.maxRecallResults ?? DEFAULTS.maxRecallResults, 1, 50,
348
+ )
349
+ const recallMode = await promptChoice(
350
+ rl, "Recall Mode", ["fast", "thinking"], cfg?.recallMode ?? DEFAULTS.recallMode,
351
+ ) as "fast" | "thinking"
352
+ const graphContext = await promptBool(rl, "Enable Graph Context?", cfg?.graphContext ?? DEFAULTS.graphContext)
353
+
354
+ printSection("Debug")
355
+
356
+ const debug = await promptBool(rl, "Enable Debug Logging?", cfg?.debug ?? DEFAULTS.debug)
357
+
358
+ const result: WizardResult = {
359
+ apiKey,
360
+ tenantId,
361
+ subTenantId,
362
+ ignoreTerm,
363
+ autoRecall,
364
+ autoCapture,
365
+ maxRecallResults,
366
+ recallMode,
367
+ graphContext,
368
+ debug,
369
+ }
370
+
371
+ // ── Summary ──
372
+
373
+ printSection("Summary")
374
+
375
+ console.log(` ${c.dim}┌${"─".repeat(50)}${c.reset}`)
376
+ printSummaryRow("API Key", apiKey, true)
377
+ printSummaryRow("Tenant ID", tenantId)
378
+ printSummaryRow("Sub-Tenant ID", subTenantId)
379
+ printSummaryRow("Auto-Recall", String(autoRecall))
380
+ printSummaryRow("Auto-Capture", String(autoCapture))
381
+ printSummaryRow("Ignore Term", ignoreTerm)
382
+ printSummaryRow("Max Results", String(maxRecallResults))
383
+ printSummaryRow("Recall Mode", recallMode)
384
+ printSummaryRow("Graph Context", String(graphContext))
385
+ printSummaryRow("Debug", String(debug))
386
+ console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
387
+
388
+ // ── Persist config ──
389
+
390
+ const configObj = buildConfigObj(result)
391
+ const saved = await promptBool(rl, `Write config to ${OPENCLAW_CONFIG_PATH}?`, true)
392
+
393
+ if (saved && persistConfig(configObj)) {
394
+ printSuccess("Config saved! Restart the gateway (`openclaw gateway restart`) to apply.")
395
+ } else if (saved) {
396
+ console.log(` ${c.red}Failed to write config. Add manually:${c.reset}`)
397
+ console.log()
398
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
399
+ console.log(` ${c.cyan}${line}${c.reset}`)
400
+ }
401
+ } else {
402
+ console.log()
403
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-cortex-ai.config:${c.reset}`)
404
+ console.log()
405
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
406
+ console.log(` ${c.cyan}${line}${c.reset}`)
407
+ }
408
+ }
409
+ } finally {
410
+ rl.close()
411
+ }
412
+ }
413
+
414
+ // ── Registration (CLI + Slash) ──
415
+
416
+ export function registerOnboardingCli(
417
+ cfg?: CortexPluginConfig,
418
+ ): (root: any) => void {
419
+ return (root: any) => {
420
+ root
421
+ .command("onboard")
422
+ .description("Interactive Cortex AI onboarding wizard")
423
+ .option("--advanced", "Configure all options (credentials, behaviour, recall, debug)")
424
+ .action(async (opts: { advanced?: boolean }) => {
425
+ if (opts.advanced) {
426
+ await runAdvancedWizard(cfg)
427
+ } else {
428
+ await runBasicWizard(cfg)
429
+ }
430
+ })
431
+ }
432
+ }
433
+
434
+ export function registerOnboardingSlashCommands(
435
+ api: OpenClawPluginApi,
436
+ client: CortexClient,
437
+ cfg: CortexPluginConfig,
438
+ ): void {
439
+ api.registerCommand({
440
+ name: "cortex-onboard",
441
+ description: "Show Cortex plugin config status (run `cortex onboard` in CLI for interactive wizard)",
442
+ acceptsArgs: false,
443
+ requireAuth: false,
444
+ handler: async () => {
445
+ try {
446
+ const lines: string[] = [
447
+ "=== Cortex AI — Current Config ===",
448
+ "",
449
+ ` API Key: ${cfg.apiKey ? `${mask(cfg.apiKey)} ✓` : "NOT SET ✗"}`,
450
+ ` Tenant ID: ${cfg.tenantId ? `${mask(cfg.tenantId, 8)} ✓` : "NOT SET ✗"}`,
451
+ ` Sub-Tenant: ${client.getSubTenantId()}`,
452
+ ` Ignore Term: ${cfg.ignoreTerm}`,
453
+ ` Auto-Recall: ${cfg.autoRecall}`,
454
+ ` Auto-Capture: ${cfg.autoCapture}`,
455
+ ` Recall Mode: ${cfg.recallMode}`,
456
+ ` Graph Context: ${cfg.graphContext}`,
457
+ ` Max Results: ${cfg.maxRecallResults}`,
458
+ ` Debug: ${cfg.debug}`,
459
+ "",
460
+ "Tip: Run `cortex onboard` in the CLI for an interactive configuration wizard,",
461
+ " or `cortex onboard --advanced` for all options.",
462
+ ]
463
+ return { text: lines.join("\n") }
464
+ } catch (err) {
465
+ log.error("/cortex-onboard", err)
466
+ return { text: "Failed to show status. Check logs." }
467
+ }
468
+ },
469
+ })
470
+ }
package/config.ts CHANGED
@@ -7,6 +7,7 @@ export type CortexPluginConfig = {
7
7
  maxRecallResults: number
8
8
  recallMode: "fast" | "thinking"
9
9
  graphContext: boolean
10
+ ignoreTerm: string
10
11
  debug: boolean
11
12
  }
12
13
 
@@ -19,10 +20,12 @@ const KNOWN_KEYS = new Set([
19
20
  "maxRecallResults",
20
21
  "recallMode",
21
22
  "graphContext",
23
+ "ignoreTerm",
22
24
  "debug",
23
25
  ])
24
26
 
25
27
  const DEFAULT_SUB_TENANT = "cortex-openclaw-plugin"
28
+ const DEFAULT_IGNORE_TERM = "cortex-ignore"
26
29
 
27
30
  function envOrNull(name: string): string | undefined {
28
31
  return typeof process !== "undefined" ? process.env[name] : undefined
@@ -86,10 +89,40 @@ export function parseConfig(raw: unknown): CortexPluginConfig {
86
89
  ? ("thinking" as const)
87
90
  : ("fast" as const),
88
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,
89
96
  debug: (cfg.debug as boolean) ?? false,
90
97
  }
91
98
  }
92
99
 
100
+ export function tryParseConfig(raw: unknown): CortexPluginConfig | 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(`cortex-ai: unrecognized config keys: ${unknown.join(", ")}`)
121
+ }
122
+
123
+ return cfg
124
+ }
125
+
93
126
  export const cortexConfigSchema = {
94
- parse: parseConfig,
127
+ parse: parseConfigSoft,
95
128
  }
package/hooks/capture.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CortexClient } from "../client.ts"
2
2
  import type { CortexPluginConfig } from "../config.ts"
3
3
  import { log } from "../log.ts"
4
- import { extractAllTurns } from "../messages.ts"
4
+ import { extractAllTurns, filterIgnoredTurns } from "../messages.ts"
5
5
  import { toHookSourceId } from "../session.ts"
6
6
  import type { ConversationTurn } from "../types/cortex.ts"
7
7
 
@@ -13,7 +13,7 @@ function removeInjectedBlocks(text: string): string {
13
13
 
14
14
  export function createIngestionHook(
15
15
  client: CortexClient,
16
- _cfg: CortexPluginConfig,
16
+ cfg: CortexPluginConfig,
17
17
  ) {
18
18
  return async (event: Record<string, unknown>, sessionId: string | undefined) => {
19
19
  try {
@@ -33,7 +33,12 @@ export function createIngestionHook(
33
33
  return
34
34
  }
35
35
 
36
- const allTurns = extractAllTurns(event.messages)
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
+ }
37
42
 
38
43
  if (allTurns.length === 0) {
39
44
  log.debug(`[capture] skipped — no user-assistant turns found in ${event.messages.length} messages`)
package/hooks/recall.ts CHANGED
@@ -2,6 +2,7 @@ import type { CortexClient } from "../client.ts"
2
2
  import type { CortexPluginConfig } from "../config.ts"
3
3
  import { buildRecalledContext, envelopeForInjection } from "../context.ts"
4
4
  import { log } from "../log.ts"
5
+ import { containsIgnoreTerm } from "../messages.ts"
5
6
 
6
7
  export function createRecallHook(
7
8
  client: CortexClient,
@@ -11,6 +12,11 @@ export function createRecallHook(
11
12
  const prompt = event.prompt as string | undefined
12
13
  if (!prompt || prompt.length < 5) return
13
14
 
15
+ if (containsIgnoreTerm(prompt, cfg.ignoreTerm)) {
16
+ log.debug(`recall skipped — prompt contains ignore term "${cfg.ignoreTerm}"`)
17
+ return
18
+ }
19
+
14
20
  log.debug(`recall query (${prompt.length} chars)`)
15
21
 
16
22
  try {
package/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
2
  import { CortexClient } from "./client.ts"
3
- import { registerCliCommands } from "./commands/cli.ts"
3
+ import type { CortexPluginConfig } from "./config.ts"
4
+ import { registerOnboardingCli as createOnboardingCliRegistrar, registerOnboardingSlashCommands } from "./commands/onboarding.ts"
4
5
  import { registerSlashCommands } from "./commands/slash.ts"
5
- import { cortexConfigSchema, parseConfig } from "./config.ts"
6
+ import { cortexConfigSchema, tryParseConfig } from "./config.ts"
6
7
  import { createIngestionHook } from "./hooks/capture.ts"
7
8
  import { createRecallHook } from "./hooks/recall.ts"
8
9
  import { log } from "./log.ts"
@@ -12,17 +13,45 @@ import { registerListTool } from "./tools/list.ts"
12
13
  import { registerSearchTool } from "./tools/search.ts"
13
14
  import { registerStoreTool } from "./tools/store.ts"
14
15
 
16
+ const NOT_CONFIGURED_MSG =
17
+ "[cortex-ai] Not configured. Run `openclaw cortex onboard` to set up credentials."
18
+
15
19
  export default {
16
20
  id: "openclaw-cortex-ai",
17
21
  name: "Cortex AI",
18
22
  description:
19
- "Long-term memory for OpenClaw powered by Cortex AI — auto-capture, recall, and graph-enriched context",
23
+ "State-of-the-art agentic memory for OpenClaw powered by Cortex AI — auto-capture, recall, and graph-enriched context",
20
24
  kind: "memory" as const,
21
25
  configSchema: cortexConfigSchema,
22
26
 
23
27
  register(api: OpenClawPluginApi) {
24
- const cfg = parseConfig(api.pluginConfig)
28
+ const cfg = tryParseConfig(api.pluginConfig)
29
+ const cliClient = cfg ? new CortexClient(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("cortex")
37
+ .description("Cortex AI memory commands")
38
+
39
+ createOnboardingCliRegistrar(cfg ?? undefined)(root)
40
+ registerCortexCliCommands(root, cliClient, cfg)
41
+ },
42
+ { commands: ["cortex"] },
43
+ )
25
44
 
45
+ if (!cfg) {
46
+ api.registerService({
47
+ id: "openclaw-cortex-ai",
48
+ start: () => console.log(NOT_CONFIGURED_MSG),
49
+ stop: () => {},
50
+ })
51
+ return
52
+ }
53
+
54
+ // Full plugin registration — credentials present
26
55
  log.init(api.logger, cfg.debug)
27
56
 
28
57
  const client = new CortexClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId)
@@ -65,7 +94,7 @@ export default {
65
94
  }
66
95
 
67
96
  registerSlashCommands(api, client, cfg, getSessionId)
68
- registerCliCommands(api, client, cfg)
97
+ registerOnboardingSlashCommands(api, client, cfg)
69
98
 
70
99
  api.registerService({
71
100
  id: "openclaw-cortex-ai",
@@ -74,3 +103,110 @@ export default {
74
103
  })
75
104
  },
76
105
  }
106
+
107
+ /**
108
+ * Register all `cortex *` CLI subcommands.
109
+ * Commands other than `onboard` guard on valid credentials at runtime.
110
+ */
111
+ function registerCortexCliCommands(
112
+ root: any,
113
+ client: CortexClient | null,
114
+ cfg: CortexPluginConfig | null,
115
+ ): void {
116
+ const requireCreds = (): { client: CortexClient; cfg: CortexPluginConfig } | 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
+ }
package/messages.ts CHANGED
@@ -1,5 +1,20 @@
1
1
  import type { ConversationTurn } from "./types/cortex.ts"
2
2
 
3
+ export function containsIgnoreTerm(text: string, ignoreTerm: string): boolean {
4
+ return text.toLowerCase().includes(ignoreTerm.toLowerCase())
5
+ }
6
+
7
+ export function filterIgnoredTurns(
8
+ turns: ConversationTurn[],
9
+ ignoreTerm: string,
10
+ ): ConversationTurn[] {
11
+ return turns.filter(
12
+ (t) =>
13
+ !containsIgnoreTerm(t.user, ignoreTerm) &&
14
+ !containsIgnoreTerm(t.assistant, ignoreTerm),
15
+ )
16
+ }
17
+
3
18
  export function textFromMessage(msg: Record<string, unknown>): string {
4
19
  const content = msg.content
5
20
  if (typeof content === "string") return content
@@ -43,6 +43,11 @@
43
43
  "help": "Include knowledge graph relations in recalled context (default: true)",
44
44
  "advanced": true
45
45
  },
46
+ "ignoreTerm": {
47
+ "label": "Ignore Term",
48
+ "placeholder": "cortex-ignore",
49
+ "help": "Messages containing this term will be excluded from recall and capture (default: cortex-ignore)"
50
+ },
46
51
  "debug": {
47
52
  "label": "Debug Logging",
48
53
  "help": "Enable verbose debug logs for API calls and responses",
@@ -61,6 +66,7 @@
61
66
  "maxRecallResults": { "type": "number", "minimum": 1, "maximum": 50 },
62
67
  "recallMode": { "type": "string", "enum": ["fast", "thinking"] },
63
68
  "graphContext": { "type": "boolean" },
69
+ "ignoreTerm": { "type": "string" },
64
70
  "debug": { "type": "boolean" }
65
71
  },
66
72
  "required": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usecortex_ai/openclaw-cortex-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Cortex AI — the State-of-the-art agentic memory system with auto-capture, recall, and knowledge graph context for open-claw",
6
6
  "license": "MIT",
package/tools/search.ts CHANGED
@@ -17,7 +17,7 @@ export function registerSearchTool(
17
17
  name: "cortex_search",
18
18
  label: "Cortex Search",
19
19
  description:
20
- "Search through Cortex long-term memories. Returns relevant chunks with graph-enriched context.",
20
+ "Search through Cortex AI memories. Returns relevant chunks with graph-enriched context.",
21
21
  parameters: Type.Object({
22
22
  query: Type.String({ description: "Search query" }),
23
23
  limit: Type.Optional(
package/tools/store.ts CHANGED
@@ -3,7 +3,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
3
  import type { CortexClient } from "../client.ts"
4
4
  import type { CortexPluginConfig } from "../config.ts"
5
5
  import { log } from "../log.ts"
6
- import { extractAllTurns } from "../messages.ts"
6
+ import { extractAllTurns, filterIgnoredTurns } from "../messages.ts"
7
7
  import { toToolSourceId } from "../session.ts"
8
8
  import type { ConversationTurn } from "../types/cortex.ts"
9
9
 
@@ -16,7 +16,7 @@ function removeInjectedBlocks(text: string): string {
16
16
  export function registerStoreTool(
17
17
  api: OpenClawPluginApi,
18
18
  client: CortexClient,
19
- _cfg: CortexPluginConfig,
19
+ cfg: CortexPluginConfig,
20
20
  getSessionId: () => string | undefined,
21
21
  getMessages: () => unknown[],
22
22
  ): void {
@@ -25,7 +25,7 @@ export function registerStoreTool(
25
25
  name: "cortex_store",
26
26
  label: "Cortex Store",
27
27
  description:
28
- "Save the full conversation history to Cortex long-term memory. Use this to persist facts, preferences, or decisions the user wants remembered. The complete chat history will be sent for context-rich storage.",
28
+ "Save the full conversation history to Cortex AI memory. Use this to persist facts, preferences, or decisions the user wants remembered. The complete chat history will be sent for context-rich storage.",
29
29
  parameters: Type.Object({
30
30
  text: Type.String({
31
31
  description: "A brief summary or note about what is being saved",
@@ -46,14 +46,15 @@ export function registerStoreTool(
46
46
 
47
47
  log.debug(`[store] tool called — sid=${sid ?? "none"} msgs=${messages.length} text="${params.text.slice(0, 50)}"`)
48
48
 
49
- const allTurns = extractAllTurns(messages)
50
- const recentTurns = allTurns.slice(-MAX_STORE_TURNS)
49
+ const rawTurns = extractAllTurns(messages)
50
+ const filteredTurns = filterIgnoredTurns(rawTurns, cfg.ignoreTerm)
51
+ const recentTurns = filteredTurns.slice(-MAX_STORE_TURNS)
51
52
  const turns: ConversationTurn[] = recentTurns.map((t) => ({
52
53
  user: removeInjectedBlocks(t.user),
53
54
  assistant: removeInjectedBlocks(t.assistant),
54
55
  }))
55
56
 
56
- log.debug(`[store] extracted ${allTurns.length} total turns, using last ${turns.length} (MAX_STORE_TURNS=${MAX_STORE_TURNS})`)
57
+ log.debug(`[store] extracted ${rawTurns.length} total turns, ${rawTurns.length - filteredTurns.length} ignored, using last ${turns.length} (MAX_STORE_TURNS=${MAX_STORE_TURNS})`)
57
58
 
58
59
  if (turns.length > 0 && sourceId) {
59
60
  const now = new Date()