@usecortex_ai/openclaw-cortex-ai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/client.ts +206 -0
- package/commands/cli.ts +93 -0
- package/commands/slash.ts +138 -0
- package/config.ts +95 -0
- package/context.ts +191 -0
- package/hooks/capture.ts +85 -0
- package/hooks/recall.ts +40 -0
- package/index.ts +57 -0
- package/log.ts +25 -0
- package/openclaw.plugin.json +68 -0
- package/package.json +24 -0
- package/session.ts +3 -0
- package/tools/search.ts +64 -0
- package/tools/store.ts +62 -0
- package/tsconfig.json +23 -0
- package/types/cortex.ts +164 -0
- package/types/openclaw.d.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Cortex AI — OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
Long-term memory for OpenClaw powered by [Cortex AI](https://usecortex.ai). Automatically captures conversations, recalls relevant context with knowledge-graph connections, and injects them before every AI turn.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @usecortex_ai/openclaw-cortex-ai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Restart OpenClaw after installing.
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Two required values: your Cortex API key and tenant ID.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export CORTEX_OPENCLAW_API_KEY="your-api-key"
|
|
19
|
+
export CORTEX_OPENCLAW_TENANT_ID="your-tenant-id"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or configure directly in `openclaw.json`:
|
|
23
|
+
|
|
24
|
+
```json5
|
|
25
|
+
{
|
|
26
|
+
"plugins": {
|
|
27
|
+
"entries": {
|
|
28
|
+
"openclaw-cortex-ai": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"config": {
|
|
31
|
+
"apiKey": "${CORTEX_OPENCLAW_API_KEY}",
|
|
32
|
+
"tenantId": "${CORTEX_OPENCLAW_TENANT_ID}"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
| Key | Type | Default | Description |
|
|
43
|
+
| ------------------ | --------- | ------------------ | -------------------------------------------------------------------------- |
|
|
44
|
+
| `subTenantId` | `string` | `"cortex-openclaw"`| Sub-tenant for data partitioning within your tenant |
|
|
45
|
+
| `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn |
|
|
46
|
+
| `autoCapture` | `boolean` | `true` | Store conversation exchanges after every AI turn |
|
|
47
|
+
| `maxRecallResults` | `number` | `10` | Max memory chunks injected into context per turn |
|
|
48
|
+
| `recallMode` | `string` | `"fast"` | `"fast"` or `"thinking"` (deeper personalised recall with graph traversal) |
|
|
49
|
+
| `graphContext` | `boolean` | `true` | Include knowledge graph relations in recalled context |
|
|
50
|
+
| `debug` | `boolean` | `false` | Verbose debug logs |
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
- **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.
|
|
57
|
+
|
|
58
|
+
## Slash Commands
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
| -------------------------- | ------------------------------------------------- |
|
|
62
|
+
| `/cortex-remember <text>` | Save something to Cortex memory |
|
|
63
|
+
| `/cortex-recall <query>` | Search memories with relevance scores |
|
|
64
|
+
| `/cortex-list` | List all stored user memories |
|
|
65
|
+
| `/cortex-delete <id>` | Delete a specific memory by its ID |
|
|
66
|
+
| `/cortex-get <source_id>` | Fetch the full content of a source |
|
|
67
|
+
|
|
68
|
+
## AI Tools
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
| ---------------- | --------------------------------------------------------- |
|
|
72
|
+
| `cortex_store` | Save information to long-term memory |
|
|
73
|
+
| `cortex_search` | Search memories with graph-enriched results |
|
|
74
|
+
|
|
75
|
+
## CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
openclaw cortex search <query> # Search memories
|
|
79
|
+
openclaw cortex list # List all user memories
|
|
80
|
+
openclaw cortex delete <id> # Delete a memory
|
|
81
|
+
openclaw cortex get <source_id> # Fetch source content
|
|
82
|
+
openclaw cortex status # Show plugin configuration
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Context Injection
|
|
86
|
+
|
|
87
|
+
Recalled context is injected inside `<cortex-context>` tags containing:
|
|
88
|
+
|
|
89
|
+
- **Entity Paths** — Knowledge graph paths connecting entities relevant to the query
|
|
90
|
+
- **Context Chunks** — Retrieved memory chunks with source titles, graph relations, and linked extra context
|
package/client.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { log } from "./log.ts"
|
|
2
|
+
import type {
|
|
3
|
+
AddMemoryRequest,
|
|
4
|
+
AddMemoryResponse,
|
|
5
|
+
ConversationTurn,
|
|
6
|
+
DeleteMemoryResponse,
|
|
7
|
+
FetchContentRequest,
|
|
8
|
+
FetchContentResponse,
|
|
9
|
+
ListDataRequest,
|
|
10
|
+
ListMemoriesResponse,
|
|
11
|
+
ListSourcesResponse,
|
|
12
|
+
RecallRequest,
|
|
13
|
+
RecallResponse,
|
|
14
|
+
} from "./types/cortex.ts"
|
|
15
|
+
|
|
16
|
+
const API_BASE = "https://api.usecortex.ai"
|
|
17
|
+
|
|
18
|
+
const INGEST_INSTRUCTIONS =
|
|
19
|
+
"Focus on extracting user preferences, habits, opinions, likes, dislikes, " +
|
|
20
|
+
"goals, and recurring themes. Capture any stated or implied personal context " +
|
|
21
|
+
"that would help personalise future interactions."
|
|
22
|
+
|
|
23
|
+
export class CortexClient {
|
|
24
|
+
private apiKey: string
|
|
25
|
+
private tenantId: string
|
|
26
|
+
private subTenantId: string
|
|
27
|
+
|
|
28
|
+
constructor(apiKey: string, tenantId: string, subTenantId: string) {
|
|
29
|
+
this.apiKey = apiKey
|
|
30
|
+
this.tenantId = tenantId
|
|
31
|
+
this.subTenantId = subTenantId
|
|
32
|
+
log.info(`connected (tenant=${tenantId}, sub=${subTenantId})`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private headers(): Record<string, string> {
|
|
36
|
+
return {
|
|
37
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async post<T>(path: string, body: unknown): Promise<T> {
|
|
43
|
+
const url = `${API_BASE}${path}`
|
|
44
|
+
log.debug("POST", path, body)
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: this.headers(),
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
})
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const text = await res.text().catch(() => "")
|
|
52
|
+
throw new Error(`Cortex ${path} → ${res.status}: ${text}`)
|
|
53
|
+
}
|
|
54
|
+
return res.json() as Promise<T>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async del<T>(path: string, params: Record<string, string>): Promise<T> {
|
|
58
|
+
const qs = new URLSearchParams(params).toString()
|
|
59
|
+
const url = `${API_BASE}${path}?${qs}`
|
|
60
|
+
log.debug("DELETE", path, params)
|
|
61
|
+
const res = await fetch(url, {
|
|
62
|
+
method: "DELETE",
|
|
63
|
+
headers: this.headers(),
|
|
64
|
+
})
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const text = await res.text().catch(() => "")
|
|
67
|
+
throw new Error(`Cortex ${path} → ${res.status}: ${text}`)
|
|
68
|
+
}
|
|
69
|
+
return res.json() as Promise<T>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Ingest ---
|
|
73
|
+
|
|
74
|
+
async ingestConversation(
|
|
75
|
+
turns: ConversationTurn[],
|
|
76
|
+
sourceId: string,
|
|
77
|
+
userName?: string,
|
|
78
|
+
): Promise<AddMemoryResponse> {
|
|
79
|
+
const payload: AddMemoryRequest = {
|
|
80
|
+
memories: [
|
|
81
|
+
{
|
|
82
|
+
user_assistant_pairs: turns,
|
|
83
|
+
infer: true,
|
|
84
|
+
source_id: sourceId,
|
|
85
|
+
user_name: userName ?? "User",
|
|
86
|
+
custom_instructions: INGEST_INSTRUCTIONS,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
tenant_id: this.tenantId,
|
|
90
|
+
sub_tenant_id: this.subTenantId,
|
|
91
|
+
upsert: true,
|
|
92
|
+
}
|
|
93
|
+
return this.post<AddMemoryResponse>("/memories/add_memory", payload)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async ingestText(
|
|
97
|
+
text: string,
|
|
98
|
+
opts?: {
|
|
99
|
+
sourceId?: string
|
|
100
|
+
title?: string
|
|
101
|
+
infer?: boolean
|
|
102
|
+
isMarkdown?: boolean
|
|
103
|
+
customInstructions?: string
|
|
104
|
+
},
|
|
105
|
+
): Promise<AddMemoryResponse> {
|
|
106
|
+
const shouldInfer = opts?.infer ?? true
|
|
107
|
+
const payload: AddMemoryRequest = {
|
|
108
|
+
memories: [
|
|
109
|
+
{
|
|
110
|
+
text,
|
|
111
|
+
infer: shouldInfer,
|
|
112
|
+
is_markdown: opts?.isMarkdown ?? false,
|
|
113
|
+
...(shouldInfer && {
|
|
114
|
+
custom_instructions: opts?.customInstructions ?? INGEST_INSTRUCTIONS,
|
|
115
|
+
}),
|
|
116
|
+
...(opts?.sourceId && { source_id: opts.sourceId }),
|
|
117
|
+
...(opts?.title && { title: opts.title }),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
tenant_id: this.tenantId,
|
|
121
|
+
sub_tenant_id: this.subTenantId,
|
|
122
|
+
upsert: true,
|
|
123
|
+
}
|
|
124
|
+
return this.post<AddMemoryResponse>("/memories/add_memory", payload)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Recall ---
|
|
128
|
+
|
|
129
|
+
async recall(
|
|
130
|
+
query: string,
|
|
131
|
+
opts?: {
|
|
132
|
+
maxResults?: number
|
|
133
|
+
mode?: "fast" | "thinking"
|
|
134
|
+
graphContext?: boolean
|
|
135
|
+
recencyBias?: number
|
|
136
|
+
},
|
|
137
|
+
): Promise<RecallResponse> {
|
|
138
|
+
const payload: RecallRequest = {
|
|
139
|
+
tenant_id: this.tenantId,
|
|
140
|
+
sub_tenant_id: this.subTenantId,
|
|
141
|
+
query,
|
|
142
|
+
max_results: opts?.maxResults ?? 10,
|
|
143
|
+
mode: opts?.mode ?? "thinking",
|
|
144
|
+
alpha: 0.8,
|
|
145
|
+
recency_bias: opts?.recencyBias ?? 0,
|
|
146
|
+
graph_context: opts?.graphContext ?? true,
|
|
147
|
+
}
|
|
148
|
+
return this.post<RecallResponse>("/recall/recall_preferences", payload)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- List ---
|
|
152
|
+
|
|
153
|
+
async listMemories(): Promise<ListMemoriesResponse> {
|
|
154
|
+
const payload: ListDataRequest = {
|
|
155
|
+
tenant_id: this.tenantId,
|
|
156
|
+
sub_tenant_id: this.subTenantId,
|
|
157
|
+
kind: "memories",
|
|
158
|
+
}
|
|
159
|
+
return this.post<ListMemoriesResponse>("/list/data", payload)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async listSources(sourceIds?: string[]): Promise<ListSourcesResponse> {
|
|
163
|
+
const payload: ListDataRequest = {
|
|
164
|
+
tenant_id: this.tenantId,
|
|
165
|
+
sub_tenant_id: this.subTenantId,
|
|
166
|
+
kind: "memories",
|
|
167
|
+
...(sourceIds && { source_ids: sourceIds }),
|
|
168
|
+
}
|
|
169
|
+
return this.post<ListSourcesResponse>("/list/data", payload)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Delete ---
|
|
173
|
+
|
|
174
|
+
async deleteMemory(memoryId: string): Promise<DeleteMemoryResponse> {
|
|
175
|
+
return this.del<DeleteMemoryResponse>("/memories/delete_memory", {
|
|
176
|
+
tenant_id: this.tenantId,
|
|
177
|
+
memory_id: memoryId,
|
|
178
|
+
sub_tenant_id: this.subTenantId,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Fetch Content ---
|
|
183
|
+
|
|
184
|
+
async fetchContent(
|
|
185
|
+
sourceId: string,
|
|
186
|
+
mode: "content" | "url" | "both" = "content",
|
|
187
|
+
): Promise<FetchContentResponse> {
|
|
188
|
+
const payload: FetchContentRequest = {
|
|
189
|
+
tenant_id: this.tenantId,
|
|
190
|
+
sub_tenant_id: this.subTenantId,
|
|
191
|
+
source_id: sourceId,
|
|
192
|
+
mode,
|
|
193
|
+
}
|
|
194
|
+
return this.post<FetchContentResponse>("/fetch/content", payload)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Accessors ---
|
|
198
|
+
|
|
199
|
+
getTenantId(): string {
|
|
200
|
+
return this.tenantId
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getSubTenantId(): string {
|
|
204
|
+
return this.subTenantId
|
|
205
|
+
}
|
|
206
|
+
}
|
package/commands/cli.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import type { CortexClient } from "../client.ts"
|
|
3
|
+
import type { CortexPluginConfig } from "../config.ts"
|
|
4
|
+
|
|
5
|
+
export function registerCliCommands(
|
|
6
|
+
api: OpenClawPluginApi,
|
|
7
|
+
client: CortexClient,
|
|
8
|
+
cfg: CortexPluginConfig,
|
|
9
|
+
): void {
|
|
10
|
+
api.registerCli(
|
|
11
|
+
({ program }: { program: any }) => {
|
|
12
|
+
const root = program
|
|
13
|
+
.command("cortex")
|
|
14
|
+
.description("Cortex AI memory commands")
|
|
15
|
+
|
|
16
|
+
root
|
|
17
|
+
.command("search")
|
|
18
|
+
.argument("<query>", "Search query")
|
|
19
|
+
.option("--limit <n>", "Max results", "10")
|
|
20
|
+
.action(async (query: string, opts: { limit: string }) => {
|
|
21
|
+
const limit = Number.parseInt(opts.limit, 10) || 10
|
|
22
|
+
const res = await client.recall(query, {
|
|
23
|
+
maxResults: limit,
|
|
24
|
+
mode: cfg.recallMode,
|
|
25
|
+
graphContext: cfg.graphContext,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (!res.chunks || res.chunks.length === 0) {
|
|
29
|
+
console.log("No memories found.")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const chunk of res.chunks) {
|
|
34
|
+
const score = chunk.relevancy_score != null
|
|
35
|
+
? ` (${(chunk.relevancy_score * 100).toFixed(0)}%)`
|
|
36
|
+
: ""
|
|
37
|
+
const title = chunk.source_title ? `[${chunk.source_title}] ` : ""
|
|
38
|
+
console.log(`- ${title}${chunk.chunk_content.slice(0, 200)}${score}`)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
root
|
|
43
|
+
.command("list")
|
|
44
|
+
.description("List all user memories")
|
|
45
|
+
.action(async () => {
|
|
46
|
+
const res = await client.listMemories()
|
|
47
|
+
const memories = res.user_memories ?? []
|
|
48
|
+
if (memories.length === 0) {
|
|
49
|
+
console.log("No memories stored.")
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
for (const m of memories) {
|
|
53
|
+
console.log(`[${m.memory_id}] ${m.memory_content.slice(0, 150)}`)
|
|
54
|
+
}
|
|
55
|
+
console.log(`\nTotal: ${memories.length}`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
root
|
|
59
|
+
.command("delete")
|
|
60
|
+
.argument("<memory_id>", "Memory ID to delete")
|
|
61
|
+
.action(async (memoryId: string) => {
|
|
62
|
+
const res = await client.deleteMemory(memoryId)
|
|
63
|
+
console.log(res.user_memory_deleted ? `Deleted: ${memoryId}` : `Not found: ${memoryId}`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
root
|
|
67
|
+
.command("get")
|
|
68
|
+
.argument("<source_id>", "Source ID to fetch")
|
|
69
|
+
.action(async (sourceId: string) => {
|
|
70
|
+
const res = await client.fetchContent(sourceId)
|
|
71
|
+
if (!res.success || res.error) {
|
|
72
|
+
console.error(`Error: ${res.error ?? "unknown"}`)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
console.log(res.content ?? res.content_base64 ?? "(no text content)")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
root
|
|
79
|
+
.command("status")
|
|
80
|
+
.description("Show plugin configuration")
|
|
81
|
+
.action(() => {
|
|
82
|
+
console.log(`Tenant: ${client.getTenantId()}`)
|
|
83
|
+
console.log(`Sub-Tenant: ${client.getSubTenantId()}`)
|
|
84
|
+
console.log(`Auto-Recall: ${cfg.autoRecall}`)
|
|
85
|
+
console.log(`Auto-Capture: ${cfg.autoCapture}`)
|
|
86
|
+
console.log(`Recall Mode: ${cfg.recallMode}`)
|
|
87
|
+
console.log(`Graph: ${cfg.graphContext}`)
|
|
88
|
+
console.log(`Max Results: ${cfg.maxRecallResults}`)
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
{ commands: ["cortex"] },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import type { CortexClient } from "../client.ts"
|
|
3
|
+
import type { CortexPluginConfig } from "../config.ts"
|
|
4
|
+
import { log } from "../log.ts"
|
|
5
|
+
import { toSourceId } from "../session.ts"
|
|
6
|
+
|
|
7
|
+
function preview(text: string, max = 80): string {
|
|
8
|
+
return text.length > max ? `${text.slice(0, max)}…` : text
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerSlashCommands(
|
|
12
|
+
api: OpenClawPluginApi,
|
|
13
|
+
client: CortexClient,
|
|
14
|
+
cfg: CortexPluginConfig,
|
|
15
|
+
getSessionKey: () => string | undefined,
|
|
16
|
+
): void {
|
|
17
|
+
api.registerCommand({
|
|
18
|
+
name: "cortex-remember",
|
|
19
|
+
description: "Save a piece of information to Cortex memory",
|
|
20
|
+
acceptsArgs: true,
|
|
21
|
+
requireAuth: true,
|
|
22
|
+
handler: async (ctx: { args?: string }) => {
|
|
23
|
+
const text = ctx.args?.trim()
|
|
24
|
+
if (!text) return { text: "Usage: /cortex-remember <text to store>" }
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const sk = getSessionKey()
|
|
28
|
+
const sourceId = sk ? toSourceId(sk) : undefined
|
|
29
|
+
await client.ingestText(text, { sourceId, title: "Manual Memory", infer: true })
|
|
30
|
+
return { text: `Saved: "${preview(text, 60)}"` }
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.error("/cortex-remember", err)
|
|
33
|
+
return { text: "Failed to save. Check logs." }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
api.registerCommand({
|
|
39
|
+
name: "cortex-recall",
|
|
40
|
+
description: "Search your Cortex memories",
|
|
41
|
+
acceptsArgs: true,
|
|
42
|
+
requireAuth: true,
|
|
43
|
+
handler: async (ctx: { args?: string }) => {
|
|
44
|
+
const query = ctx.args?.trim()
|
|
45
|
+
if (!query) return { text: "Usage: /cortex-recall <query>" }
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const res = await client.recall(query, {
|
|
49
|
+
maxResults: cfg.maxRecallResults,
|
|
50
|
+
mode: cfg.recallMode,
|
|
51
|
+
graphContext: cfg.graphContext,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (!res.chunks || res.chunks.length === 0) {
|
|
55
|
+
return { text: `No memories found for "${query}"` }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const lines = res.chunks.slice(0, 10).map((c, i) => {
|
|
59
|
+
const score = c.relevancy_score != null ? ` (${Math.round(c.relevancy_score * 100)}%)` : ""
|
|
60
|
+
const title = c.source_title ? ` [${c.source_title}]` : ""
|
|
61
|
+
return `${i + 1}.${title} ${preview(c.chunk_content, 120)}${score}`
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return { text: `Found ${res.chunks.length} chunks:\n\n${lines.join("\n")}` }
|
|
65
|
+
} catch (err) {
|
|
66
|
+
log.error("/cortex-recall", err)
|
|
67
|
+
return { text: "Recall failed. Check logs." }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
api.registerCommand({
|
|
73
|
+
name: "cortex-list",
|
|
74
|
+
description: "List all stored user memories",
|
|
75
|
+
acceptsArgs: false,
|
|
76
|
+
requireAuth: true,
|
|
77
|
+
handler: async () => {
|
|
78
|
+
try {
|
|
79
|
+
const res = await client.listMemories()
|
|
80
|
+
const memories = res.user_memories ?? []
|
|
81
|
+
if (memories.length === 0) return { text: "No memories stored yet." }
|
|
82
|
+
|
|
83
|
+
const lines = memories.map(
|
|
84
|
+
(m, i) => `${i + 1}. [${m.memory_id}] ${preview(m.memory_content, 100)}`,
|
|
85
|
+
)
|
|
86
|
+
return { text: `${memories.length} memories:\n\n${lines.join("\n")}` }
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log.error("/cortex-list", err)
|
|
89
|
+
return { text: "Failed to list memories. Check logs." }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
api.registerCommand({
|
|
95
|
+
name: "cortex-delete",
|
|
96
|
+
description: "Delete a specific memory by its ID",
|
|
97
|
+
acceptsArgs: true,
|
|
98
|
+
requireAuth: true,
|
|
99
|
+
handler: async (ctx: { args?: string }) => {
|
|
100
|
+
const memoryId = ctx.args?.trim()
|
|
101
|
+
if (!memoryId) return { text: "Usage: /cortex-delete <memory_id>" }
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const res = await client.deleteMemory(memoryId)
|
|
105
|
+
if (res.user_memory_deleted) {
|
|
106
|
+
return { text: `Deleted memory: ${memoryId}` }
|
|
107
|
+
}
|
|
108
|
+
return { text: `Memory ${memoryId} was not found or already deleted.` }
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.error("/cortex-delete", err)
|
|
111
|
+
return { text: "Delete failed. Check logs." }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
api.registerCommand({
|
|
117
|
+
name: "cortex-get",
|
|
118
|
+
description: "Fetch the content of a specific source by its ID",
|
|
119
|
+
acceptsArgs: true,
|
|
120
|
+
requireAuth: true,
|
|
121
|
+
handler: async (ctx: { args?: string }) => {
|
|
122
|
+
const sourceId = ctx.args?.trim()
|
|
123
|
+
if (!sourceId) return { text: "Usage: /cortex-get <source_id>" }
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const res = await client.fetchContent(sourceId)
|
|
127
|
+
if (!res.success || res.error) {
|
|
128
|
+
return { text: `Could not fetch source ${sourceId}: ${res.error ?? "unknown error"}` }
|
|
129
|
+
}
|
|
130
|
+
const content = res.content ?? res.content_base64 ?? "(no text content)"
|
|
131
|
+
return { text: `Source: ${sourceId}\n\n${preview(content, 2000)}` }
|
|
132
|
+
} catch (err) {
|
|
133
|
+
log.error("/cortex-get", err)
|
|
134
|
+
return { text: "Fetch failed. Check logs." }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type CortexPluginConfig = {
|
|
2
|
+
apiKey: string
|
|
3
|
+
tenantId: string
|
|
4
|
+
subTenantId: string
|
|
5
|
+
autoRecall: boolean
|
|
6
|
+
autoCapture: boolean
|
|
7
|
+
maxRecallResults: number
|
|
8
|
+
recallMode: "fast" | "thinking"
|
|
9
|
+
graphContext: boolean
|
|
10
|
+
debug: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const KNOWN_KEYS = new Set([
|
|
14
|
+
"apiKey",
|
|
15
|
+
"tenantId",
|
|
16
|
+
"subTenantId",
|
|
17
|
+
"autoRecall",
|
|
18
|
+
"autoCapture",
|
|
19
|
+
"maxRecallResults",
|
|
20
|
+
"recallMode",
|
|
21
|
+
"graphContext",
|
|
22
|
+
"debug",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SUB_TENANT = "cortex-openclaw"
|
|
26
|
+
|
|
27
|
+
function envOrNull(name: string): string | undefined {
|
|
28
|
+
return typeof process !== "undefined" ? process.env[name] : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveEnvVars(value: string): string {
|
|
32
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name: string) => {
|
|
33
|
+
const val = envOrNull(name)
|
|
34
|
+
if (!val) throw new Error(`Environment variable ${name} is not set`)
|
|
35
|
+
return val
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseConfig(raw: unknown): CortexPluginConfig {
|
|
40
|
+
const cfg =
|
|
41
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
42
|
+
? (raw as Record<string, unknown>)
|
|
43
|
+
: {}
|
|
44
|
+
|
|
45
|
+
const unknown = Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k))
|
|
46
|
+
if (unknown.length > 0) {
|
|
47
|
+
throw new Error(`cortex-ai: unrecognized config keys: ${unknown.join(", ")}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const apiKey =
|
|
51
|
+
typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
|
|
52
|
+
? resolveEnvVars(cfg.apiKey)
|
|
53
|
+
: envOrNull("CORTEX_OPENCLAW_API_KEY")
|
|
54
|
+
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"cortex-ai: apiKey is required — set it in plugin config or via CORTEX_OPENCLAW_API_KEY env var",
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tenantId =
|
|
62
|
+
typeof cfg.tenantId === "string" && cfg.tenantId.length > 0
|
|
63
|
+
? resolveEnvVars(cfg.tenantId)
|
|
64
|
+
: envOrNull("CORTEX_OPENCLAW_TENANT_ID")
|
|
65
|
+
|
|
66
|
+
if (!tenantId) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"cortex-ai: tenantId is required — set it in plugin config or via CORTEX_OPENCLAW_TENANT_ID env var",
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const subTenantId =
|
|
73
|
+
typeof cfg.subTenantId === "string" && cfg.subTenantId.length > 0
|
|
74
|
+
? cfg.subTenantId
|
|
75
|
+
: DEFAULT_SUB_TENANT
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
apiKey,
|
|
79
|
+
tenantId,
|
|
80
|
+
subTenantId,
|
|
81
|
+
autoRecall: (cfg.autoRecall as boolean) ?? true,
|
|
82
|
+
autoCapture: (cfg.autoCapture as boolean) ?? true,
|
|
83
|
+
maxRecallResults: (cfg.maxRecallResults as number) ?? 10,
|
|
84
|
+
recallMode:
|
|
85
|
+
cfg.recallMode === "thinking"
|
|
86
|
+
? ("thinking" as const)
|
|
87
|
+
: ("fast" as const),
|
|
88
|
+
graphContext: (cfg.graphContext as boolean) ?? true,
|
|
89
|
+
debug: (cfg.debug as boolean) ?? false,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const cortexConfigSchema = {
|
|
94
|
+
parse: parseConfig,
|
|
95
|
+
}
|