@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/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
|
+
}
|
package/hooks/capture.ts
ADDED
|
@@ -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
|
+
}
|
package/hooks/recall.ts
ADDED
|
@@ -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
package/tools/search.ts
ADDED
|
@@ -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
|
+
}
|