@usecortex_ai/openclaw-cortex-ai 0.1.1 → 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.
@@ -1,9 +1,24 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
1
3
  import * as readline from "node:readline"
2
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
5
  import type { CortexClient } from "../client.ts"
4
6
  import type { CortexPluginConfig } from "../config.ts"
5
7
  import { log } from "../log.ts"
6
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
+
7
22
  // ── ANSI helpers ──
8
23
 
9
24
  const c = {
@@ -160,57 +175,71 @@ type WizardResult = {
160
175
  debug?: boolean
161
176
  }
162
177
 
163
- function buildConfigJson(result: WizardResult): string {
178
+ function buildConfigObj(result: WizardResult): Record<string, unknown> {
164
179
  const obj: Record<string, unknown> = {}
165
180
 
166
- if (result.apiKey && !result.apiKey.startsWith("$")) {
167
- obj.apiKey = result.apiKey
168
- }
169
- if (result.tenantId && !result.tenantId.startsWith("$")) {
170
- obj.tenantId = result.tenantId
171
- }
172
- if (result.subTenantId !== "cortex-openclaw-plugin") {
181
+ obj.apiKey = result.apiKey
182
+ obj.tenantId = result.tenantId
183
+
184
+ if (result.subTenantId !== DEFAULTS.subTenantId) {
173
185
  obj.subTenantId = result.subTenantId
174
186
  }
175
- if (result.ignoreTerm !== "cortex-ignore") {
187
+ if (result.ignoreTerm !== DEFAULTS.ignoreTerm) {
176
188
  obj.ignoreTerm = result.ignoreTerm
177
189
  }
178
- if (result.autoRecall !== undefined && result.autoRecall !== true) {
190
+ if (result.autoRecall !== undefined && result.autoRecall !== DEFAULTS.autoRecall) {
179
191
  obj.autoRecall = result.autoRecall
180
192
  }
181
- if (result.autoCapture !== undefined && result.autoCapture !== true) {
193
+ if (result.autoCapture !== undefined && result.autoCapture !== DEFAULTS.autoCapture) {
182
194
  obj.autoCapture = result.autoCapture
183
195
  }
184
- if (result.maxRecallResults !== undefined && result.maxRecallResults !== 10) {
196
+ if (result.maxRecallResults !== undefined && result.maxRecallResults !== DEFAULTS.maxRecallResults) {
185
197
  obj.maxRecallResults = result.maxRecallResults
186
198
  }
187
- if (result.recallMode !== undefined && result.recallMode !== "fast") {
199
+ if (result.recallMode !== undefined && result.recallMode !== DEFAULTS.recallMode) {
188
200
  obj.recallMode = result.recallMode
189
201
  }
190
- if (result.graphContext !== undefined && result.graphContext !== true) {
202
+ if (result.graphContext !== undefined && result.graphContext !== DEFAULTS.graphContext) {
191
203
  obj.graphContext = result.graphContext
192
204
  }
193
- if (result.debug !== undefined && result.debug !== false) {
205
+ if (result.debug !== undefined && result.debug !== DEFAULTS.debug) {
194
206
  obj.debug = result.debug
195
207
  }
196
208
 
197
- return JSON.stringify(obj, null, 2)
209
+ return obj
198
210
  }
199
211
 
200
- function buildEnvLines(result: WizardResult): string[] {
201
- const lines: string[] = []
202
- if (result.apiKey && !result.apiKey.startsWith("$")) {
203
- lines.push(`CORTEX_OPENCLAW_API_KEY=${result.apiKey}`)
204
- }
205
- if (result.tenantId && !result.tenantId.startsWith("$")) {
206
- lines.push(`CORTEX_OPENCLAW_TENANT_ID=${result.tenantId}`)
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
207
237
  }
208
- return lines
209
238
  }
210
239
 
211
240
  // ── Wizards ──
212
241
 
213
- async function runBasicWizard(cfg: CortexPluginConfig): Promise<void> {
242
+ async function runBasicWizard(cfg?: CortexPluginConfig): Promise<void> {
214
243
  const rl = createRl()
215
244
 
216
245
  try {
@@ -232,14 +261,15 @@ async function runBasicWizard(cfg: CortexPluginConfig): Promise<void> {
232
261
  printSection("Customisation")
233
262
 
234
263
  const subTenantId = await promptText(rl, "Sub-Tenant ID", {
235
- default: cfg.subTenantId,
264
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
236
265
  })
237
266
 
238
267
  const ignoreTerm = await promptText(rl, "Ignore Term", {
239
- default: cfg.ignoreTerm,
268
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
240
269
  })
241
270
 
242
271
  const result: WizardResult = { apiKey, tenantId, subTenantId, ignoreTerm }
272
+ const configObj = buildConfigObj(result)
243
273
 
244
274
  // ── Summary ──
245
275
 
@@ -252,35 +282,35 @@ async function runBasicWizard(cfg: CortexPluginConfig): Promise<void> {
252
282
  printSummaryRow("Ignore Term", ignoreTerm)
253
283
  console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
254
284
 
255
- // ── Output config ──
285
+ // ── Persist config ──
256
286
 
257
- const envLines = buildEnvLines(result)
258
- if (envLines.length > 0) {
259
- console.log()
260
- console.log(` ${c.yellow}${c.bold}Add to your .env file:${c.reset}`)
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}`)
261
293
  console.log()
262
- for (const line of envLines) {
263
- console.log(` ${c.green}${line}${c.reset}`)
294
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
295
+ console.log(` ${c.cyan}${line}${c.reset}`)
264
296
  }
265
- }
266
-
267
- const json = buildConfigJson(result)
268
- if (json !== "{}") {
297
+ } else {
269
298
  console.log()
270
- console.log(` ${c.yellow}${c.bold}Plugin config (openclaw.plugin.json / settings):${c.reset}`)
299
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-cortex-ai.config:${c.reset}`)
271
300
  console.log()
272
- for (const line of json.split("\n")) {
301
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
273
302
  console.log(` ${c.cyan}${line}${c.reset}`)
274
303
  }
275
304
  }
276
305
 
277
- printSuccess("Onboarding complete! Run `cortex onboard --advanced` to fine-tune all options.")
306
+ console.log()
307
+ console.log(` ${c.dim}Run \`cortex onboard --advanced\` to fine-tune all options.${c.reset}`)
278
308
  } finally {
279
309
  rl.close()
280
310
  }
281
311
  }
282
312
 
283
- async function runAdvancedWizard(cfg: CortexPluginConfig): Promise<void> {
313
+ async function runAdvancedWizard(cfg?: CortexPluginConfig): Promise<void> {
284
314
  const rl = createRl()
285
315
 
286
316
  try {
@@ -300,30 +330,30 @@ async function runAdvancedWizard(cfg: CortexPluginConfig): Promise<void> {
300
330
  })
301
331
 
302
332
  const subTenantId = await promptText(rl, "Sub-Tenant ID", {
303
- default: cfg.subTenantId,
333
+ default: cfg?.subTenantId ?? DEFAULTS.subTenantId,
304
334
  })
305
335
 
306
336
  printSection("Behaviour")
307
337
 
308
- const autoRecall = await promptBool(rl, "Enable Auto-Recall?", cfg.autoRecall)
309
- const autoCapture = await promptBool(rl, "Enable Auto-Capture?", cfg.autoCapture)
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)
310
340
  const ignoreTerm = await promptText(rl, "Ignore Term", {
311
- default: cfg.ignoreTerm,
341
+ default: cfg?.ignoreTerm ?? DEFAULTS.ignoreTerm,
312
342
  })
313
343
 
314
344
  printSection("Recall Settings")
315
345
 
316
346
  const maxRecallResults = await promptNumber(
317
- rl, "Max Recall Results", cfg.maxRecallResults, 1, 50,
347
+ rl, "Max Recall Results", cfg?.maxRecallResults ?? DEFAULTS.maxRecallResults, 1, 50,
318
348
  )
319
349
  const recallMode = await promptChoice(
320
- rl, "Recall Mode", ["fast", "thinking"], cfg.recallMode,
350
+ rl, "Recall Mode", ["fast", "thinking"], cfg?.recallMode ?? DEFAULTS.recallMode,
321
351
  ) as "fast" | "thinking"
322
- const graphContext = await promptBool(rl, "Enable Graph Context?", cfg.graphContext)
352
+ const graphContext = await promptBool(rl, "Enable Graph Context?", cfg?.graphContext ?? DEFAULTS.graphContext)
323
353
 
324
354
  printSection("Debug")
325
355
 
326
- const debug = await promptBool(rl, "Enable Debug Logging?", cfg.debug)
356
+ const debug = await promptBool(rl, "Enable Debug Logging?", cfg?.debug ?? DEFAULTS.debug)
327
357
 
328
358
  const result: WizardResult = {
329
359
  apiKey,
@@ -355,29 +385,27 @@ async function runAdvancedWizard(cfg: CortexPluginConfig): Promise<void> {
355
385
  printSummaryRow("Debug", String(debug))
356
386
  console.log(` ${c.dim}└${"─".repeat(50)}${c.reset}`)
357
387
 
358
- // ── Output config ──
388
+ // ── Persist config ──
359
389
 
360
- const envLines = buildEnvLines(result)
361
- if (envLines.length > 0) {
362
- console.log()
363
- console.log(` ${c.yellow}${c.bold}Add to your .env file:${c.reset}`)
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}`)
364
397
  console.log()
365
- for (const line of envLines) {
366
- console.log(` ${c.green}${line}${c.reset}`)
398
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
399
+ console.log(` ${c.cyan}${line}${c.reset}`)
367
400
  }
368
- }
369
-
370
- const json = buildConfigJson(result)
371
- if (json !== "{}") {
401
+ } else {
372
402
  console.log()
373
- console.log(` ${c.yellow}${c.bold}Plugin config (openclaw.plugin.json / settings):${c.reset}`)
403
+ console.log(` ${c.yellow}${c.bold}Add to openclaw.json plugins.entries.openclaw-cortex-ai.config:${c.reset}`)
374
404
  console.log()
375
- for (const line of json.split("\n")) {
405
+ for (const line of JSON.stringify(configObj, null, 2).split("\n")) {
376
406
  console.log(` ${c.cyan}${line}${c.reset}`)
377
407
  }
378
408
  }
379
-
380
- printSuccess("Onboarding complete! All options configured.")
381
409
  } finally {
382
410
  rl.close()
383
411
  }
@@ -386,7 +414,7 @@ async function runAdvancedWizard(cfg: CortexPluginConfig): Promise<void> {
386
414
  // ── Registration (CLI + Slash) ──
387
415
 
388
416
  export function registerOnboardingCli(
389
- cfg: CortexPluginConfig,
417
+ cfg?: CortexPluginConfig,
390
418
  ): (root: any) => void {
391
419
  return (root: any) => {
392
420
  root
package/config.ts CHANGED
@@ -97,6 +97,32 @@ export function parseConfig(raw: unknown): CortexPluginConfig {
97
97
  }
98
98
  }
99
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
+
100
126
  export const cortexConfigSchema = {
101
- parse: parseConfig,
127
+ parse: parseConfigSoft,
102
128
  }
package/index.ts CHANGED
@@ -1,9 +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"
4
- import { registerOnboardingCli, registerOnboardingSlashCommands } from "./commands/onboarding.ts"
3
+ import type { CortexPluginConfig } from "./config.ts"
4
+ import { registerOnboardingCli as createOnboardingCliRegistrar, registerOnboardingSlashCommands } from "./commands/onboarding.ts"
5
5
  import { registerSlashCommands } from "./commands/slash.ts"
6
- import { cortexConfigSchema, parseConfig } from "./config.ts"
6
+ import { cortexConfigSchema, tryParseConfig } from "./config.ts"
7
7
  import { createIngestionHook } from "./hooks/capture.ts"
8
8
  import { createRecallHook } from "./hooks/recall.ts"
9
9
  import { log } from "./log.ts"
@@ -13,6 +13,9 @@ import { registerListTool } from "./tools/list.ts"
13
13
  import { registerSearchTool } from "./tools/search.ts"
14
14
  import { registerStoreTool } from "./tools/store.ts"
15
15
 
16
+ const NOT_CONFIGURED_MSG =
17
+ "[cortex-ai] Not configured. Run `openclaw cortex onboard` to set up credentials."
18
+
16
19
  export default {
17
20
  id: "openclaw-cortex-ai",
18
21
  name: "Cortex AI",
@@ -22,8 +25,33 @@ export default {
22
25
  configSchema: cortexConfigSchema,
23
26
 
24
27
  register(api: OpenClawPluginApi) {
25
- 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
+ )
26
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
27
55
  log.init(api.logger, cfg.debug)
28
56
 
29
57
  const client = new CortexClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId)
@@ -67,7 +95,6 @@ export default {
67
95
 
68
96
  registerSlashCommands(api, client, cfg, getSessionId)
69
97
  registerOnboardingSlashCommands(api, client, cfg)
70
- registerCliCommands(api, client, cfg, registerOnboardingCli(cfg))
71
98
 
72
99
  api.registerService({
73
100
  id: "openclaw-cortex-ai",
@@ -76,3 +103,110 @@ export default {
76
103
  })
77
104
  },
78
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usecortex_ai/openclaw-cortex-ai",
3
- "version": "0.1.1",
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",