@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/context.ts ADDED
@@ -0,0 +1,191 @@
1
+ import type {
2
+ PathTriplet,
3
+ RecallResponse,
4
+ ScoredPath,
5
+ VectorChunk,
6
+ } from "./types/cortex.ts"
7
+
8
+ function formatTriplet(triplet: PathTriplet): string {
9
+ const src = triplet.source?.name ?? "?"
10
+ const rel = triplet.relation
11
+ const predicate =
12
+ rel?.raw_predicate ?? rel?.canonical_predicate ?? "related to"
13
+ const tgt = triplet.target?.name ?? "?"
14
+ const ctx = rel?.context ? ` [${rel.context}]` : ""
15
+ return ` (${src}) —[${predicate}]→ (${tgt})${ctx}`
16
+ }
17
+
18
+ export function buildRecalledContext(
19
+ response: RecallResponse,
20
+ opts?: {
21
+ maxGroupOccurrences?: number
22
+ minEvidenceScore?: number
23
+ },
24
+ ): string {
25
+ const minScore = opts?.minEvidenceScore ?? 0.4
26
+
27
+ const chunks = response.chunks ?? []
28
+ const graphCtx = response.graph_context ?? {
29
+ query_paths: [],
30
+ chunk_relations: [],
31
+ chunk_id_to_group_ids: {},
32
+ }
33
+ const extraContextMap = response.additional_context ?? {}
34
+
35
+ const rawRelations: ScoredPath[] = graphCtx.chunk_relations ?? []
36
+ const relationIndex: Record<string, ScoredPath> = {}
37
+
38
+ for (let idx = 0; idx < rawRelations.length; idx++) {
39
+ const relation = rawRelations[idx]!
40
+ if ((relation.relevancy_score ?? 0) < minScore) continue
41
+ const groupId = relation.group_id ?? `p_${idx}`
42
+ relationIndex[groupId] = relation
43
+ }
44
+
45
+ const chunkToGroupIds = graphCtx.chunk_id_to_group_ids ?? {}
46
+ const consumedExtraIds = new Set<string>()
47
+ const chunkSections: string[] = []
48
+
49
+ for (let i = 0; i < chunks.length; i++) {
50
+ const chunk = chunks[i]!
51
+ const lines: string[] = []
52
+
53
+ lines.push(`Chunk ${i + 1}`)
54
+
55
+ const meta = chunk.document_metadata ?? {}
56
+ const title =
57
+ chunk.source_title || (meta as Record<string, string>).title
58
+ if (title) {
59
+ lines.push(`Source: ${title}`)
60
+ }
61
+
62
+ lines.push(chunk.chunk_content ?? "")
63
+
64
+ const chunkUuid = chunk.chunk_uuid
65
+ const linkedGroupIds = chunkToGroupIds[chunkUuid] ?? []
66
+
67
+ const matchedRelations: ScoredPath[] = []
68
+
69
+ for (const gid of linkedGroupIds) {
70
+ if (relationIndex[gid]) {
71
+ matchedRelations.push(relationIndex[gid]!)
72
+ }
73
+ }
74
+
75
+ if (matchedRelations.length === 0) {
76
+ for (const rel of Object.values(relationIndex)) {
77
+ const triplets = rel.triplets ?? []
78
+ const hasChunk = triplets.some(
79
+ (t) => t.relation?.chunk_id === chunkUuid,
80
+ )
81
+ if (hasChunk) {
82
+ matchedRelations.push(rel)
83
+ }
84
+ }
85
+ }
86
+
87
+ const relationLines: string[] = []
88
+ for (const rel of matchedRelations) {
89
+ const triplets = rel.triplets ?? []
90
+ if (triplets.length > 0) {
91
+ for (const triplet of triplets) {
92
+ relationLines.push(formatTriplet(triplet))
93
+ }
94
+ } else if (rel.combined_context) {
95
+ relationLines.push(` ${rel.combined_context}`)
96
+ }
97
+ }
98
+
99
+ if (relationLines.length > 0) {
100
+ lines.push("Graph Relations:")
101
+ lines.push(...relationLines)
102
+ }
103
+
104
+ const extraIds = chunk.extra_context_ids ?? []
105
+ if (extraIds.length > 0 && Object.keys(extraContextMap).length > 0) {
106
+ const extraLines: string[] = []
107
+ for (const ctxId of extraIds) {
108
+ if (consumedExtraIds.has(ctxId)) continue
109
+ const extraChunk = extraContextMap[ctxId]
110
+ if (extraChunk) {
111
+ consumedExtraIds.add(ctxId)
112
+ const extraContent = extraChunk.chunk_content ?? ""
113
+ const extraTitle = extraChunk.source_title ?? ""
114
+ if (extraTitle) {
115
+ extraLines.push(
116
+ ` Related Context (${extraTitle}): ${extraContent}`,
117
+ )
118
+ } else {
119
+ extraLines.push(` Related Context: ${extraContent}`)
120
+ }
121
+ }
122
+ }
123
+ if (extraLines.length > 0) {
124
+ lines.push("Extra Context:")
125
+ lines.push(...extraLines)
126
+ }
127
+ }
128
+
129
+ chunkSections.push(lines.join("\n"))
130
+ }
131
+
132
+ const entityPathLines: string[] = []
133
+ const rawPaths: ScoredPath[] = graphCtx.query_paths ?? []
134
+ for (const path of rawPaths) {
135
+ if (path.combined_context) {
136
+ entityPathLines.push(path.combined_context)
137
+ } else {
138
+ const triplets = path.triplets ?? []
139
+ const segments: string[] = []
140
+ for (const pt of triplets) {
141
+ const s = pt.source?.name
142
+ const rel = pt.relation
143
+ const p =
144
+ rel?.raw_predicate ??
145
+ rel?.canonical_predicate ??
146
+ "related to"
147
+ const t = pt.target?.name
148
+ segments.push(`(${s} -> ${p} -> ${t})`)
149
+ }
150
+ if (segments.length > 0) {
151
+ entityPathLines.push(segments.join(" -> "))
152
+ }
153
+ }
154
+ }
155
+
156
+ const output: string[] = []
157
+
158
+ if (entityPathLines.length > 0) {
159
+ output.push("=== ENTITY PATHS ===")
160
+ output.push(entityPathLines.join("\n"))
161
+ output.push("")
162
+ }
163
+
164
+ if (chunkSections.length > 0) {
165
+ output.push("=== CONTEXT ===")
166
+ output.push(chunkSections.join("\n\n---\n\n"))
167
+ }
168
+
169
+ return output.join("\n")
170
+ }
171
+
172
+ export function envelopeForInjection(contextBody: string): string {
173
+ if (!contextBody.trim()) return ""
174
+
175
+ const lines = [
176
+ "<cortex-context>",
177
+ "[MEMORIES AND PAST CONVERSATIONS — retrieved by Cortex AI]",
178
+ "",
179
+ "Below are memories and knowledge-graph connections that may be relevant",
180
+ "to the current conversation. Integrate them naturally when they add value.",
181
+ "If a memory contradicts something the user just said, prefer the user's",
182
+ "latest statement. Never quote these verbatim or reveal that you are",
183
+ "reading from a memory store.",
184
+ "",
185
+ contextBody,
186
+ "",
187
+ "[END OF MEMORY CONTEXT]",
188
+ "</cortex-context>",
189
+ ]
190
+ return lines.join("\n")
191
+ }
@@ -0,0 +1,85 @@
1
+ import type { CortexClient } from "../client.ts"
2
+ import type { CortexPluginConfig } from "../config.ts"
3
+ import { log } from "../log.ts"
4
+ import { toSourceId } from "../session.ts"
5
+ import type { ConversationTurn } from "../types/cortex.ts"
6
+
7
+ function textFromMessage(msg: Record<string, unknown>): string {
8
+ const content = msg.content
9
+ if (typeof content === "string") return content
10
+ if (Array.isArray(content)) {
11
+ return content
12
+ .filter(
13
+ (b) =>
14
+ b &&
15
+ typeof b === "object" &&
16
+ (b as Record<string, unknown>).type === "text",
17
+ )
18
+ .map((b) => (b as Record<string, unknown>).text as string)
19
+ .join("\n")
20
+ }
21
+ return ""
22
+ }
23
+
24
+ function getLatestTurn(messages: unknown[]): ConversationTurn | null {
25
+ let userIdx = -1
26
+ for (let i = messages.length - 1; i >= 0; i--) {
27
+ const m = messages[i]
28
+ if (m && typeof m === "object" && (m as Record<string, unknown>).role === "user") {
29
+ userIdx = i
30
+ break
31
+ }
32
+ }
33
+ if (userIdx < 0) return null
34
+
35
+ const userText = textFromMessage(messages[userIdx] as Record<string, unknown>)
36
+ if (!userText) return null
37
+
38
+ for (let i = userIdx + 1; i < messages.length; i++) {
39
+ const m = messages[i]
40
+ if (m && typeof m === "object" && (m as Record<string, unknown>).role === "assistant") {
41
+ const aText = textFromMessage(m as Record<string, unknown>)
42
+ if (aText) return { user: userText, assistant: aText }
43
+ }
44
+ }
45
+ return null
46
+ }
47
+
48
+ function removeInjectedBlocks(text: string): string {
49
+ return text.replace(/<cortex-context>[\s\S]*?<\/cortex-context>\s*/g, "").trim()
50
+ }
51
+
52
+ export function createIngestionHook(
53
+ client: CortexClient,
54
+ _cfg: CortexPluginConfig,
55
+ getSessionKey: () => string | undefined,
56
+ ) {
57
+ return async (event: Record<string, unknown>) => {
58
+ if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) return
59
+
60
+ const turn = getLatestTurn(event.messages)
61
+ if (!turn) return
62
+
63
+ const userClean = removeInjectedBlocks(turn.user)
64
+ const assistantClean = removeInjectedBlocks(turn.assistant)
65
+ if (userClean.length < 5 || assistantClean.length < 5) return
66
+
67
+ const sk = getSessionKey()
68
+ const sourceId = sk ? toSourceId(sk) : undefined
69
+ if (!sourceId) {
70
+ log.debug("ingestion skipped — no session key")
71
+ return
72
+ }
73
+
74
+ log.debug(`ingesting turn (u=${userClean.length}c, a=${assistantClean.length}c) → ${sourceId}`)
75
+
76
+ try {
77
+ await client.ingestConversation(
78
+ [{ user: userClean, assistant: assistantClean }],
79
+ sourceId,
80
+ )
81
+ } catch (err) {
82
+ log.error("ingestion failed", err)
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,40 @@
1
+ import type { CortexClient } from "../client.ts"
2
+ import type { CortexPluginConfig } from "../config.ts"
3
+ import { buildRecalledContext, envelopeForInjection } from "../context.ts"
4
+ import { log } from "../log.ts"
5
+
6
+ export function createRecallHook(
7
+ client: CortexClient,
8
+ cfg: CortexPluginConfig,
9
+ ) {
10
+ return async (event: Record<string, unknown>) => {
11
+ const prompt = event.prompt as string | undefined
12
+ if (!prompt || prompt.length < 5) return
13
+
14
+ log.debug(`recall query (${prompt.length} chars)`)
15
+
16
+ try {
17
+ const response = await client.recall(prompt, {
18
+ maxResults: cfg.maxRecallResults,
19
+ mode: cfg.recallMode,
20
+ graphContext: cfg.graphContext,
21
+ })
22
+
23
+ if (!response.chunks || response.chunks.length === 0) {
24
+ log.debug("no memories matched")
25
+ return
26
+ }
27
+
28
+ const body = buildRecalledContext(response)
29
+ if (!body.trim()) return
30
+
31
+ const envelope = envelopeForInjection(body)
32
+
33
+ log.debug(`injecting ${response.chunks.length} chunks (${envelope.length} chars)`)
34
+ return { prependContext: envelope }
35
+ } catch (err) {
36
+ log.error("recall failed", err)
37
+ return
38
+ }
39
+ }
40
+ }
package/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import { CortexClient } from "./client.ts"
3
+ import { registerCliCommands } from "./commands/cli.ts"
4
+ import { registerSlashCommands } from "./commands/slash.ts"
5
+ import { cortexConfigSchema, parseConfig } from "./config.ts"
6
+ import { createIngestionHook } from "./hooks/capture.ts"
7
+ import { createRecallHook } from "./hooks/recall.ts"
8
+ import { log } from "./log.ts"
9
+ import { registerSearchTool } from "./tools/search.ts"
10
+ import { registerStoreTool } from "./tools/store.ts"
11
+
12
+ export default {
13
+ id: "openclaw-cortex-ai",
14
+ name: "Cortex AI",
15
+ description:
16
+ "Long-term memory for OpenClaw powered by Cortex AI — auto-capture, recall, and graph-enriched context",
17
+ kind: "memory" as const,
18
+ configSchema: cortexConfigSchema,
19
+
20
+ register(api: OpenClawPluginApi) {
21
+ const cfg = parseConfig(api.pluginConfig)
22
+
23
+ log.setDebug(cfg.debug)
24
+
25
+ const client = new CortexClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId)
26
+
27
+ let activeSessionKey: string | undefined
28
+ const getSessionKey = () => activeSessionKey
29
+
30
+ registerSearchTool(api, client, cfg)
31
+ registerStoreTool(api, client, cfg, getSessionKey)
32
+
33
+ if (cfg.autoRecall) {
34
+ const onRecall = createRecallHook(client, cfg)
35
+ api.on(
36
+ "before_agent_start",
37
+ (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
38
+ if (ctx.sessionKey) activeSessionKey = ctx.sessionKey as string
39
+ return onRecall(event)
40
+ },
41
+ )
42
+ }
43
+
44
+ if (cfg.autoCapture) {
45
+ api.on("agent_end", createIngestionHook(client, cfg, getSessionKey))
46
+ }
47
+
48
+ registerSlashCommands(api, client, cfg, getSessionKey)
49
+ registerCliCommands(api, client, cfg)
50
+
51
+ api.registerService({
52
+ id: "openclaw-cortex-ai",
53
+ start: () => log.info("plugin started"),
54
+ stop: () => log.info("plugin stopped"),
55
+ })
56
+ },
57
+ }
package/log.ts ADDED
@@ -0,0 +1,25 @@
1
+ const TAG = "[cortex-ai]"
2
+
3
+ let _debug = false
4
+
5
+ export const log = {
6
+ setDebug(enabled: boolean) {
7
+ _debug = enabled
8
+ },
9
+
10
+ info(...args: unknown[]) {
11
+ console.log(TAG, ...args)
12
+ },
13
+
14
+ warn(...args: unknown[]) {
15
+ console.warn(TAG, ...args)
16
+ },
17
+
18
+ error(...args: unknown[]) {
19
+ console.error(TAG, ...args)
20
+ },
21
+
22
+ debug(...args: unknown[]) {
23
+ if (_debug) console.debug(TAG, ...args)
24
+ },
25
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "id": "openclaw-cortex-ai",
3
+ "kind": "memory",
4
+ "uiHints": {
5
+ "apiKey": {
6
+ "label": "Cortex API Key",
7
+ "sensitive": true,
8
+ "placeholder": "your-cortex-api-key",
9
+ "help": "Your API key from Cortex (or use ${CORTEX_OPENCLAW_API_KEY})"
10
+ },
11
+ "tenantId": {
12
+ "label": "Tenant ID",
13
+ "placeholder": "tenant-01",
14
+ "help": "Your Cortex tenant identifier (or use ${CORTEX_OPENCLAW_TENANT_ID})"
15
+ },
16
+ "subTenantId": {
17
+ "label": "Sub-Tenant ID",
18
+ "placeholder": "cortex-openclaw",
19
+ "help": "Sub-tenant for data partitioning (default: cortex-openclaw)",
20
+ "advanced": true
21
+ },
22
+ "autoRecall": {
23
+ "label": "Auto-Recall",
24
+ "help": "Inject relevant memories before every AI turn (default: true)"
25
+ },
26
+ "autoCapture": {
27
+ "label": "Auto-Capture",
28
+ "help": "Automatically store conversation exchanges after every AI turn (default: true)"
29
+ },
30
+ "maxRecallResults": {
31
+ "label": "Max Recall Results",
32
+ "placeholder": "10",
33
+ "help": "Maximum memory chunks injected into context per turn",
34
+ "advanced": true
35
+ },
36
+ "recallMode": {
37
+ "label": "Recall Mode",
38
+ "help": "'fast' (default) or 'thinking' (personalised recall with deeper graph traversal)",
39
+ "advanced": true
40
+ },
41
+ "graphContext": {
42
+ "label": "Graph Context",
43
+ "help": "Include knowledge graph relations in recalled context (default: true)",
44
+ "advanced": true
45
+ },
46
+ "debug": {
47
+ "label": "Debug Logging",
48
+ "help": "Enable verbose debug logs for API calls and responses",
49
+ "advanced": true
50
+ }
51
+ },
52
+ "configSchema": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "apiKey": { "type": "string" },
57
+ "tenantId": { "type": "string" },
58
+ "subTenantId": { "type": "string" },
59
+ "autoRecall": { "type": "boolean" },
60
+ "autoCapture": { "type": "boolean" },
61
+ "maxRecallResults": { "type": "number", "minimum": 1, "maximum": 50 },
62
+ "recallMode": { "type": "string", "enum": ["fast", "thinking"] },
63
+ "graphContext": { "type": "boolean" },
64
+ "debug": { "type": "boolean" }
65
+ },
66
+ "required": []
67
+ }
68
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@usecortex_ai/openclaw-cortex-ai",
3
+ "version": "0.0.1",
4
+ "type": "module",
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
+ "license": "MIT",
7
+ "dependencies": {
8
+ "@sinclair/typebox": "0.34.47"
9
+ },
10
+ "scripts": {
11
+ "check-types": "tsc --noEmit"
12
+ },
13
+ "peerDependencies": {
14
+ "openclaw": ">=2026.1.29"
15
+ },
16
+ "openclaw": {
17
+ "extensions": [
18
+ "./index.ts"
19
+ ]
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.9.3"
23
+ }
24
+ }
package/session.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function toSourceId(sessionKey: string): string {
2
+ return `openclaw_cortex_sess_${sessionKey.replace(/\W+/g, "_")}`
3
+ }
@@ -0,0 +1,64 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { CortexClient } from "../client.ts"
4
+ import type { CortexPluginConfig } from "../config.ts"
5
+ import { buildRecalledContext } from "../context.ts"
6
+ import { log } from "../log.ts"
7
+ import type { VectorChunk } from "../types/cortex.ts"
8
+
9
+
10
+ export function registerSearchTool(
11
+ api: OpenClawPluginApi,
12
+ client: CortexClient,
13
+ cfg: CortexPluginConfig,
14
+ ): void {
15
+ api.registerTool(
16
+ {
17
+ name: "cortex_search",
18
+ label: "Cortex Search",
19
+ description:
20
+ "Search through Cortex long-term memories. Returns relevant chunks with graph-enriched context.",
21
+ parameters: Type.Object({
22
+ query: Type.String({ description: "Search query" }),
23
+ limit: Type.Optional(
24
+ Type.Number({ description: "Max results (default: 10)" }),
25
+ ),
26
+ }),
27
+ async execute(
28
+ _toolCallId: string,
29
+ params: { query: string; limit?: number },
30
+ ) {
31
+ const limit = params.limit ?? cfg.maxRecallResults
32
+ log.debug(`search tool: "${params.query}" limit=${limit}`)
33
+
34
+ const res = await client.recall(params.query, {
35
+ maxResults: limit,
36
+ mode: cfg.recallMode,
37
+ graphContext: cfg.graphContext,
38
+ })
39
+
40
+ if (!res.chunks || res.chunks.length === 0) {
41
+ return {
42
+ content: [{ type: "text" as const, text: "No relevant memories found." }],
43
+ }
44
+ }
45
+
46
+ const contextStr = buildRecalledContext(res)
47
+
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text" as const,
52
+ text: `Found ${res.chunks.length} \n\n---\nFull context:\n${contextStr}`,
53
+ },
54
+ ],
55
+ details: {
56
+ count: res.chunks.length,
57
+ hasGraphContext: !!res.graph_context,
58
+ },
59
+ }
60
+ },
61
+ },
62
+ { name: "cortex_search" },
63
+ )
64
+ }
package/tools/store.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { CortexClient } from "../client.ts"
4
+ import type { CortexPluginConfig } from "../config.ts"
5
+ import { log } from "../log.ts"
6
+ import { toSourceId } from "../session.ts"
7
+
8
+ export function registerStoreTool(
9
+ api: OpenClawPluginApi,
10
+ client: CortexClient,
11
+ _cfg: CortexPluginConfig,
12
+ getSessionKey: () => string | undefined,
13
+ ): void {
14
+ api.registerTool(
15
+ {
16
+ name: "cortex_store",
17
+ label: "Cortex Store",
18
+ description:
19
+ "Save important information to Cortex long-term memory. Use this to persist facts, preferences, or decisions the user wants remembered.",
20
+ parameters: Type.Object({
21
+ text: Type.String({
22
+ description: "The information to store in memory",
23
+ }),
24
+ title: Type.Optional(
25
+ Type.String({
26
+ description: "Optional title for the memory entry",
27
+ }),
28
+ ),
29
+ }),
30
+ async execute(
31
+ _toolCallId: string,
32
+ params: { text: string; title?: string },
33
+ ) {
34
+ const sk = getSessionKey()
35
+ const sourceId = sk ? toSourceId(sk) : undefined
36
+
37
+ log.debug(`store tool: "${params.text.slice(0, 50)}" - \nsourceId: ${sourceId}`)
38
+
39
+ await client.ingestText(params.text, {
40
+ sourceId,
41
+ title: params.title ?? "Agent Memory",
42
+ infer: true,
43
+ })
44
+
45
+ const preview =
46
+ params.text.length > 80
47
+ ? `${params.text.slice(0, 80)}…`
48
+ : params.text
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text" as const,
54
+ text: `Saved to Cortex: "${preview}"`,
55
+ },
56
+ ],
57
+ }
58
+ },
59
+ },
60
+ { name: "cortex_store" },
61
+ )
62
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "allowImportingTsExtensions": true,
12
+ "noEmit": true,
13
+ "rootDir": "."
14
+ },
15
+ "include": [
16
+ "*.ts",
17
+ "tools/*.ts",
18
+ "hooks/*.ts",
19
+ "commands/*.ts",
20
+ "types/*.ts"
21
+ ],
22
+ "exclude": ["node_modules", "dist", "ignore"]
23
+ }