@usecortex_ai/openclaw-cortex-ai 0.0.1 → 0.1.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/.github/workflows/publish.yaml +40 -0
- package/README.md +47 -27
- package/client.ts +11 -3
- package/commands/cli.ts +4 -0
- package/commands/onboarding.ts +442 -0
- package/commands/slash.ts +4 -4
- package/config.ts +8 -1
- package/hooks/capture.ts +76 -60
- package/hooks/recall.ts +6 -0
- package/index.ts +30 -9
- package/log.ts +27 -4
- package/messages.ts +88 -0
- package/openclaw.plugin.json +6 -0
- package/package.json +1 -1
- package/session.ts +10 -2
- package/tools/delete.ts +54 -0
- package/tools/get.ts +57 -0
- package/tools/list.ts +56 -0
- package/tools/search.ts +1 -1
- package/tools/store.ts +68 -14
- package/types/cortex.ts +2 -0
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
|
-
const DEFAULT_SUB_TENANT = "cortex-openclaw"
|
|
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,6 +89,10 @@ 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
|
}
|
package/hooks/capture.ts
CHANGED
|
@@ -1,49 +1,11 @@
|
|
|
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 {
|
|
4
|
+
import { extractAllTurns, filterIgnoredTurns } from "../messages.ts"
|
|
5
|
+
import { toHookSourceId } from "../session.ts"
|
|
5
6
|
import type { ConversationTurn } from "../types/cortex.ts"
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
}
|
|
8
|
+
const MAX_HOOK_TURNS = -1
|
|
47
9
|
|
|
48
10
|
function removeInjectedBlocks(text: string): string {
|
|
49
11
|
return text.replace(/<cortex-context>[\s\S]*?<\/cortex-context>\s*/g, "").trim()
|
|
@@ -51,35 +13,89 @@ function removeInjectedBlocks(text: string): string {
|
|
|
51
13
|
|
|
52
14
|
export function createIngestionHook(
|
|
53
15
|
client: CortexClient,
|
|
54
|
-
|
|
55
|
-
getSessionKey: () => string | undefined,
|
|
16
|
+
cfg: CortexPluginConfig,
|
|
56
17
|
) {
|
|
57
|
-
return async (event: Record<string, unknown
|
|
58
|
-
|
|
18
|
+
return async (event: Record<string, unknown>, sessionId: string | undefined) => {
|
|
19
|
+
try {
|
|
20
|
+
log.debug(`[capture] hook fired — success=${event.success} msgs=${Array.isArray(event.messages) ? event.messages.length : "N/A"} sid=${sessionId ?? "none"}`)
|
|
59
21
|
|
|
60
|
-
|
|
61
|
-
|
|
22
|
+
if (!event.success) {
|
|
23
|
+
log.debug("[capture] skipped — event.success is falsy")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(event.messages) || event.messages.length === 0) {
|
|
27
|
+
log.debug("[capture] skipped — no messages in event")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
62
30
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
31
|
+
if (!sessionId) {
|
|
32
|
+
log.debug("[capture] skipped — no session id available")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
66
35
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!sourceId) {
|
|
70
|
-
log.debug("ingestion skipped — no session key")
|
|
71
|
-
return
|
|
72
|
-
}
|
|
36
|
+
const rawTurns = extractAllTurns(event.messages)
|
|
37
|
+
const allTurns = filterIgnoredTurns(rawTurns, cfg.ignoreTerm)
|
|
73
38
|
|
|
74
|
-
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
if (allTurns.length === 0) {
|
|
44
|
+
log.debug(`[capture] skipped — no user-assistant turns found in ${event.messages.length} messages`)
|
|
45
|
+
const roles = event.messages
|
|
46
|
+
.slice(-5)
|
|
47
|
+
.map((m) => (m && typeof m === "object" ? (m as Record<string, unknown>).role : "?"))
|
|
48
|
+
log.debug(`[capture] last 5 message roles: ${JSON.stringify(roles)}`)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const recentTurns = MAX_HOOK_TURNS === -1 ? allTurns : allTurns.slice(-MAX_HOOK_TURNS)
|
|
53
|
+
const turns: ConversationTurn[] = recentTurns.map((t) => ({
|
|
54
|
+
user: removeInjectedBlocks(t.user),
|
|
55
|
+
assistant: removeInjectedBlocks(t.assistant),
|
|
56
|
+
})).filter((t) => t.user.length >= 5 && t.assistant.length >= 5)
|
|
57
|
+
|
|
58
|
+
if (turns.length === 0) {
|
|
59
|
+
log.debug("[capture] skipped — all turns too short after cleaning")
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourceId = toHookSourceId(sessionId)
|
|
64
|
+
|
|
65
|
+
const now = new Date()
|
|
66
|
+
const timestamp = now.toISOString()
|
|
67
|
+
const readableTime = now.toLocaleString("en-US", {
|
|
68
|
+
weekday: "short",
|
|
69
|
+
year: "numeric",
|
|
70
|
+
month: "short",
|
|
71
|
+
day: "numeric",
|
|
72
|
+
hour: "2-digit",
|
|
73
|
+
minute: "2-digit",
|
|
74
|
+
timeZoneName: "short",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const annotatedTurns = turns.map((t, i) => ({
|
|
78
|
+
user: i === 0 ? `[Temporal details: ${readableTime}]\n\n${t.user}` : t.user,
|
|
79
|
+
assistant: t.assistant,
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
log.debug(`[capture] ingesting ${annotatedTurns.length} turns (of ${allTurns.length} total) @ ${timestamp} -> ${sourceId}`)
|
|
75
83
|
|
|
76
|
-
try {
|
|
77
84
|
await client.ingestConversation(
|
|
78
|
-
|
|
85
|
+
annotatedTurns,
|
|
79
86
|
sourceId,
|
|
87
|
+
{
|
|
88
|
+
metadata: {
|
|
89
|
+
captured_at: timestamp,
|
|
90
|
+
source: "openclaw_hook",
|
|
91
|
+
turn_count: annotatedTurns.length,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
80
94
|
)
|
|
95
|
+
|
|
96
|
+
log.debug("[capture] ingestion succeeded")
|
|
81
97
|
} catch (err) {
|
|
82
|
-
log.error("
|
|
98
|
+
log.error("[capture] hook error", err)
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
}
|
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,11 +1,15 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import { CortexClient } from "./client.ts"
|
|
3
3
|
import { registerCliCommands } from "./commands/cli.ts"
|
|
4
|
+
import { registerOnboardingCli, registerOnboardingSlashCommands } from "./commands/onboarding.ts"
|
|
4
5
|
import { registerSlashCommands } from "./commands/slash.ts"
|
|
5
6
|
import { cortexConfigSchema, parseConfig } 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"
|
|
10
|
+
import { registerDeleteTool } from "./tools/delete.ts"
|
|
11
|
+
import { registerGetTool } from "./tools/get.ts"
|
|
12
|
+
import { registerListTool } from "./tools/list.ts"
|
|
9
13
|
import { registerSearchTool } from "./tools/search.ts"
|
|
10
14
|
import { registerStoreTool } from "./tools/store.ts"
|
|
11
15
|
|
|
@@ -13,40 +17,57 @@ export default {
|
|
|
13
17
|
id: "openclaw-cortex-ai",
|
|
14
18
|
name: "Cortex AI",
|
|
15
19
|
description:
|
|
16
|
-
"
|
|
20
|
+
"State-of-the-art agentic memory for OpenClaw powered by Cortex AI — auto-capture, recall, and graph-enriched context",
|
|
17
21
|
kind: "memory" as const,
|
|
18
22
|
configSchema: cortexConfigSchema,
|
|
19
23
|
|
|
20
24
|
register(api: OpenClawPluginApi) {
|
|
21
25
|
const cfg = parseConfig(api.pluginConfig)
|
|
22
26
|
|
|
23
|
-
log.
|
|
27
|
+
log.init(api.logger, cfg.debug)
|
|
24
28
|
|
|
25
29
|
const client = new CortexClient(cfg.apiKey, cfg.tenantId, cfg.subTenantId)
|
|
26
30
|
|
|
27
|
-
let
|
|
28
|
-
|
|
31
|
+
let activeSessionId: string | undefined
|
|
32
|
+
let conversationMessages: unknown[] = []
|
|
33
|
+
const getSessionId = () => activeSessionId
|
|
34
|
+
const getMessages = () => conversationMessages
|
|
29
35
|
|
|
30
36
|
registerSearchTool(api, client, cfg)
|
|
31
|
-
registerStoreTool(api, client, cfg,
|
|
37
|
+
registerStoreTool(api, client, cfg, getSessionId, getMessages)
|
|
38
|
+
registerListTool(api, client, cfg)
|
|
39
|
+
registerDeleteTool(api, client, cfg)
|
|
40
|
+
registerGetTool(api, client, cfg)
|
|
32
41
|
|
|
33
42
|
if (cfg.autoRecall) {
|
|
34
43
|
const onRecall = createRecallHook(client, cfg)
|
|
35
44
|
api.on(
|
|
36
45
|
"before_agent_start",
|
|
37
46
|
(event: Record<string, unknown>, ctx: Record<string, unknown>) => {
|
|
38
|
-
if (ctx.
|
|
47
|
+
if (ctx.sessionId) activeSessionId = ctx.sessionId as string
|
|
48
|
+
if (Array.isArray(event.messages)) conversationMessages = event.messages
|
|
49
|
+
log.debug(`[session] before_agent_start — sid=${activeSessionId ?? "none"} msgs=${conversationMessages.length}`)
|
|
39
50
|
return onRecall(event)
|
|
40
51
|
},
|
|
41
52
|
)
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
if (cfg.autoCapture) {
|
|
45
|
-
|
|
56
|
+
const captureHandler = createIngestionHook(client, cfg)
|
|
57
|
+
api.on(
|
|
58
|
+
"agent_end",
|
|
59
|
+
(event: Record<string, unknown>, ctx: Record<string, unknown>) => {
|
|
60
|
+
if (ctx.sessionId) activeSessionId = ctx.sessionId as string
|
|
61
|
+
if (Array.isArray(event.messages)) conversationMessages = event.messages
|
|
62
|
+
log.debug(`[session] agent_end — sid=${activeSessionId ?? "none"} msgs=${conversationMessages.length} ctxKeys=${Object.keys(ctx).join(",")}`)
|
|
63
|
+
return captureHandler(event, activeSessionId)
|
|
64
|
+
},
|
|
65
|
+
)
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
registerSlashCommands(api, client, cfg,
|
|
49
|
-
|
|
68
|
+
registerSlashCommands(api, client, cfg, getSessionId)
|
|
69
|
+
registerOnboardingSlashCommands(api, client, cfg)
|
|
70
|
+
registerCliCommands(api, client, cfg, registerOnboardingCli(cfg))
|
|
50
71
|
|
|
51
72
|
api.registerService({
|
|
52
73
|
id: "openclaw-cortex-ai",
|
package/log.ts
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
+
export type LoggerBackend = {
|
|
2
|
+
info(msg: string): void
|
|
3
|
+
warn(msg: string): void
|
|
4
|
+
error(msg: string): void
|
|
5
|
+
debug?(msg: string): void
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
const TAG = "[cortex-ai]"
|
|
2
9
|
|
|
10
|
+
let _backend: LoggerBackend | null = null
|
|
3
11
|
let _debug = false
|
|
4
12
|
|
|
5
13
|
export const log = {
|
|
14
|
+
init(backend: LoggerBackend, debug: boolean) {
|
|
15
|
+
_backend = backend
|
|
16
|
+
_debug = debug
|
|
17
|
+
},
|
|
18
|
+
|
|
6
19
|
setDebug(enabled: boolean) {
|
|
7
20
|
_debug = enabled
|
|
8
21
|
},
|
|
9
22
|
|
|
10
23
|
info(...args: unknown[]) {
|
|
11
|
-
|
|
24
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
25
|
+
if (_backend) _backend.info(msg)
|
|
26
|
+
else console.log(msg)
|
|
12
27
|
},
|
|
13
28
|
|
|
14
29
|
warn(...args: unknown[]) {
|
|
15
|
-
|
|
30
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
31
|
+
if (_backend) _backend.warn(msg)
|
|
32
|
+
else console.warn(msg)
|
|
16
33
|
},
|
|
17
34
|
|
|
18
35
|
error(...args: unknown[]) {
|
|
19
|
-
|
|
36
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
37
|
+
if (_backend) _backend.error(msg)
|
|
38
|
+
else console.error(msg)
|
|
20
39
|
},
|
|
21
40
|
|
|
22
41
|
debug(...args: unknown[]) {
|
|
23
|
-
if (_debug)
|
|
42
|
+
if (!_debug) return
|
|
43
|
+
const msg = `${TAG} ${args.map(String).join(" ")}`
|
|
44
|
+
if (_backend?.debug) _backend.debug(msg)
|
|
45
|
+
else if (_backend) _backend.info(msg)
|
|
46
|
+
else console.debug(msg)
|
|
24
47
|
},
|
|
25
48
|
}
|
package/messages.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ConversationTurn } from "./types/cortex.ts"
|
|
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
|
+
|
|
18
|
+
export function textFromMessage(msg: Record<string, unknown>): string {
|
|
19
|
+
const content = msg.content
|
|
20
|
+
if (typeof content === "string") return content
|
|
21
|
+
if (Array.isArray(content)) {
|
|
22
|
+
return content
|
|
23
|
+
.filter(
|
|
24
|
+
(b) =>
|
|
25
|
+
b &&
|
|
26
|
+
typeof b === "object" &&
|
|
27
|
+
(b as Record<string, unknown>).type === "text",
|
|
28
|
+
)
|
|
29
|
+
.map((b) => (b as Record<string, unknown>).text as string)
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join("\n")
|
|
32
|
+
}
|
|
33
|
+
return ""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractAllTurns(messages: unknown[]): ConversationTurn[] {
|
|
37
|
+
const turns: ConversationTurn[] = []
|
|
38
|
+
let currentUserText: string | null = null
|
|
39
|
+
let currentAssistantText: string | null = null
|
|
40
|
+
|
|
41
|
+
for (const msg of messages) {
|
|
42
|
+
if (!msg || typeof msg !== "object") continue
|
|
43
|
+
const m = msg as Record<string, unknown>
|
|
44
|
+
const text = textFromMessage(m)
|
|
45
|
+
|
|
46
|
+
if (m.role === "user") {
|
|
47
|
+
if (!text) continue
|
|
48
|
+
if (currentUserText && currentAssistantText) {
|
|
49
|
+
turns.push({ user: currentUserText, assistant: currentAssistantText })
|
|
50
|
+
}
|
|
51
|
+
currentUserText = text
|
|
52
|
+
currentAssistantText = "no-message"
|
|
53
|
+
} else if (m.role === "assistant") {
|
|
54
|
+
if (!text) continue
|
|
55
|
+
currentAssistantText = text
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (currentUserText && currentAssistantText) {
|
|
60
|
+
turns.push({ user: currentUserText, assistant: currentAssistantText })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return turns
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getLatestTurn(messages: unknown[]): ConversationTurn | null {
|
|
67
|
+
let userIdx = -1
|
|
68
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
69
|
+
const m = messages[i]
|
|
70
|
+
if (m && typeof m === "object" && (m as Record<string, unknown>).role === "user") {
|
|
71
|
+
userIdx = i
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (userIdx < 0) return null
|
|
76
|
+
|
|
77
|
+
const userText = textFromMessage(messages[userIdx] as Record<string, unknown>)
|
|
78
|
+
if (!userText) return null
|
|
79
|
+
|
|
80
|
+
for (let i = userIdx + 1; i < messages.length; i++) {
|
|
81
|
+
const m = messages[i]
|
|
82
|
+
if (m && typeof m === "object" && (m as Record<string, unknown>).role === "assistant") {
|
|
83
|
+
const aText = textFromMessage(m as Record<string, unknown>)
|
|
84
|
+
if (aText) return { user: userText, assistant: aText }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
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.
|
|
3
|
+
"version": "0.1.1",
|
|
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/session.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
export function toSourceId(
|
|
2
|
-
return `
|
|
1
|
+
export function toSourceId(sessionId: string): string {
|
|
2
|
+
return `sess_${sessionId}`
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toHookSourceId(sessionId: string): string {
|
|
6
|
+
return `hook_${sessionId}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toToolSourceId(sessionId: string): string {
|
|
10
|
+
return `tool_${sessionId}`
|
|
3
11
|
}
|
package/tools/delete.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
7
|
+
export function registerDeleteTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: CortexClient,
|
|
10
|
+
_cfg: CortexPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "cortex_delete_memory",
|
|
15
|
+
label: "Cortex Delete Memory",
|
|
16
|
+
description:
|
|
17
|
+
"Delete a specific memory from Cortex by its memory ID. Use this when the user explicitly asks you to forget something or remove a specific piece of stored information. Always confirm the memory ID before deleting.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
memory_id: Type.String({
|
|
20
|
+
description: "The unique ID of the memory to delete",
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
async execute(
|
|
24
|
+
_toolCallId: string,
|
|
25
|
+
params: { memory_id: string },
|
|
26
|
+
) {
|
|
27
|
+
log.debug(`delete tool: memory_id=${params.memory_id}`)
|
|
28
|
+
|
|
29
|
+
const res = await client.deleteMemory(params.memory_id)
|
|
30
|
+
|
|
31
|
+
if (res.user_memory_deleted) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text" as const,
|
|
36
|
+
text: `Successfully deleted memory: ${params.memory_id}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text" as const,
|
|
46
|
+
text: `Memory ${params.memory_id} was not found or has already been deleted.`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ name: "cortex_delete_memory" },
|
|
53
|
+
)
|
|
54
|
+
}
|
package/tools/get.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
|
|
7
|
+
export function registerGetTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: CortexClient,
|
|
10
|
+
_cfg: CortexPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "cortex_get_content",
|
|
15
|
+
label: "Cortex Get Content",
|
|
16
|
+
description:
|
|
17
|
+
"Fetch the full content of a specific source from Cortex by its source ID. Use this to retrieve the complete text of a memory source when you need more details than what's shown in search results.",
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
source_id: Type.String({
|
|
20
|
+
description: "The unique source ID to fetch content for",
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
async execute(
|
|
24
|
+
_toolCallId: string,
|
|
25
|
+
params: { source_id: string },
|
|
26
|
+
) {
|
|
27
|
+
log.debug(`get tool: source_id=${params.source_id}`)
|
|
28
|
+
|
|
29
|
+
const res = await client.fetchContent(params.source_id)
|
|
30
|
+
|
|
31
|
+
if (!res.success || res.error) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text" as const,
|
|
36
|
+
text: `Failed to fetch source ${params.source_id}: ${res.error ?? "unknown error"}`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = res.content ?? res.content_base64 ?? "(no text content available)"
|
|
43
|
+
const preview = content.length > 3000 ? `${content.slice(0, 3000)}…\n\n[Content truncated, showing first 3000 characters]` : content
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text" as const,
|
|
49
|
+
text: `Source: ${params.source_id}\n\n${preview}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{ name: "cortex_get_content" },
|
|
56
|
+
)
|
|
57
|
+
}
|
package/tools/list.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
|
|
7
|
+
export function registerListTool(
|
|
8
|
+
api: OpenClawPluginApi,
|
|
9
|
+
client: CortexClient,
|
|
10
|
+
_cfg: CortexPluginConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerTool(
|
|
13
|
+
{
|
|
14
|
+
name: "cortex_list_memories",
|
|
15
|
+
label: "Cortex List Memories",
|
|
16
|
+
description:
|
|
17
|
+
"List all user memories stored in Cortex. Returns memory IDs and content summaries. Use this when the user asks what you remember about them or wants to see their stored information.",
|
|
18
|
+
parameters: Type.Object({}),
|
|
19
|
+
async execute(_toolCallId: string, _params: Record<string, never>) {
|
|
20
|
+
log.debug("list tool: fetching all memories")
|
|
21
|
+
|
|
22
|
+
const res = await client.listMemories()
|
|
23
|
+
const memories = res.user_memories ?? []
|
|
24
|
+
|
|
25
|
+
if (memories.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text" as const,
|
|
30
|
+
text: "No memories stored yet.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const lines = memories.map((m, i) => {
|
|
37
|
+
const preview =
|
|
38
|
+
m.memory_content.length > 100
|
|
39
|
+
? `${m.memory_content.slice(0, 100)}…`
|
|
40
|
+
: m.memory_content
|
|
41
|
+
return `${i + 1}. [ID: ${m.memory_id}]\n ${preview}`
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text" as const,
|
|
48
|
+
text: `Found ${memories.length} memories:\n\n${lines.join("\n\n")}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{ name: "cortex_list_memories" },
|
|
55
|
+
)
|
|
56
|
+
}
|
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(
|