@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 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
+ }
@@ -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
+ }