@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 +27 -7
- package/commands/cli.ts +4 -0
- package/commands/onboarding.ts +470 -0
- package/config.ts +34 -1
- package/hooks/capture.ts +8 -3
- package/hooks/recall.ts +6 -0
- package/index.ts +141 -5
- package/messages.ts +15 -0
- package/openclaw.plugin.json +6 -0
- package/package.json +1 -1
- package/tools/search.ts +1 -1
- package/tools/store.ts +7 -6
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
|
|
78
|
-
|
|
79
|
-
openclaw cortex
|
|
80
|
-
openclaw cortex
|
|
81
|
-
openclaw cortex
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
"
|
|
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 =
|
|
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
|
-
|
|
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
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
const
|
|
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 ${
|
|
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()
|