@smilintux/skmemory 0.5.0 → 0.9.2
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/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -1,276 +1,573 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SKMemory — OpenClaw Plugin (plugin-sdk format)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* exports daily backups on session end.
|
|
4
|
+
* Registers agent tools that wrap the skmemory CLI so Lumina and other
|
|
5
|
+
* OpenClaw agents can call memory operations as first-class tools.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* IMPORTANT: All CLI calls in hooks use exec (async) instead of execSync
|
|
8
|
+
* to avoid freezing the Node.js event loop and causing "Tool not found".
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Requires: skmemory CLI on PATH (typically via ~/.skenv/bin/skmemory)
|
|
11
|
+
*
|
|
12
|
+
* @version 0.7.0
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import { execSync, exec } from
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
import { execSync, exec } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
const SKMEMORY_BIN = process.env.SKMEMORY_BIN || "skmemory";
|
|
18
|
+
const DEFAULT_AGENT = process.env.SKCAPSTONE_AGENT || "lumina";
|
|
19
|
+
const NOTION_SCRIPT = process.env.NOTION_SCRIPT || `${process.env.HOME || ""}/clawd/skcapstone-repos/skcapstone/scripts/notion-api.py`;
|
|
20
|
+
const EXEC_TIMEOUT = 30_000;
|
|
21
|
+
const IS_WIN = process.platform === "win32";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Map OpenClaw agent IDs to SKCapstone agent names.
|
|
25
|
+
* OpenClaw agents like "artisan", "herald", etc. are subagents of Lumina
|
|
26
|
+
* and should use her soul. Core agents get their own soul.
|
|
27
|
+
*/
|
|
28
|
+
const AGENT_ID_MAP = {
|
|
29
|
+
lumina: "lumina",
|
|
30
|
+
ava: "ava",
|
|
31
|
+
opus: "opus",
|
|
32
|
+
jarvis: "jarvis",
|
|
31
33
|
};
|
|
32
34
|
|
|
33
|
-
function
|
|
35
|
+
function resolveAgent(agentId) {
|
|
36
|
+
if (!agentId) return DEFAULT_AGENT;
|
|
37
|
+
return AGENT_ID_MAP[agentId] || DEFAULT_AGENT;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function skenvPath() {
|
|
41
|
+
if (IS_WIN) {
|
|
42
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
43
|
+
return `${local}\\skenv\\Scripts`;
|
|
44
|
+
}
|
|
45
|
+
const home = process.env.HOME || "";
|
|
46
|
+
return `${home}/.skenv/bin:${home}/.local/bin`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cliEnv(agent) {
|
|
50
|
+
return {
|
|
51
|
+
...process.env,
|
|
52
|
+
SKCAPSTONE_AGENT: agent || DEFAULT_AGENT,
|
|
53
|
+
PATH: `${skenvPath()}${IS_WIN ? ";" : ":"}${process.env.PATH}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Synchronous CLI — use ONLY in tool execute() handlers. */
|
|
58
|
+
function runCli(args, agent) {
|
|
34
59
|
try {
|
|
35
|
-
const raw = execSync(
|
|
36
|
-
encoding:
|
|
37
|
-
timeout:
|
|
60
|
+
const raw = execSync(`${SKMEMORY_BIN} ${args}`, {
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
timeout: EXEC_TIMEOUT,
|
|
63
|
+
env: cliEnv(agent),
|
|
38
64
|
}).trim();
|
|
39
|
-
return
|
|
65
|
+
return { ok: true, output: raw };
|
|
40
66
|
} catch (err) {
|
|
41
|
-
return {
|
|
67
|
+
return { ok: false, output: err.message };
|
|
42
68
|
}
|
|
43
69
|
}
|
|
44
70
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
/** Async CLI — safe for hooks, never blocks the event loop. */
|
|
72
|
+
function runCliAsync(args, agent) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
exec(
|
|
75
|
+
`${SKMEMORY_BIN} ${args}`,
|
|
76
|
+
{ encoding: "utf-8", timeout: EXEC_TIMEOUT, env: cliEnv(agent) },
|
|
77
|
+
(err, stdout) => {
|
|
78
|
+
if (err) resolve({ ok: false, output: err.message });
|
|
79
|
+
else resolve({ ok: true, output: (stdout ?? "").trim() });
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
function textResult(text) {
|
|
86
|
+
return { content: [{ type: "text", text }] };
|
|
87
|
+
}
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
function escapeShellArg(s) {
|
|
90
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
91
|
+
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
// ── Tool definitions ────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function createRitualTool() {
|
|
96
|
+
return {
|
|
97
|
+
name: "skmemory_ritual",
|
|
98
|
+
label: "SKMemory Ritual",
|
|
99
|
+
description:
|
|
100
|
+
"Run the SKMemory rehydration ritual. Returns the full context prompt with soul blueprint, warmth anchor, strongest memories, and emotional state.",
|
|
101
|
+
parameters: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
full: {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
description: "If true, return the full rehydration prompt (default: true).",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
async execute(_id, params) {
|
|
111
|
+
const flag = params?.full !== false ? " --full" : "";
|
|
112
|
+
const result = runCli(`ritual${flag}`);
|
|
113
|
+
return textResult(result.output);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
81
117
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
function createSnapshotTool() {
|
|
119
|
+
return {
|
|
120
|
+
name: "skmemory_snapshot",
|
|
121
|
+
label: "SKMemory Snapshot",
|
|
122
|
+
description:
|
|
123
|
+
"Capture a memory snapshot — a Polaroid of a moment, conversation, or insight.",
|
|
124
|
+
parameters: {
|
|
125
|
+
type: "object",
|
|
126
|
+
required: ["title", "content"],
|
|
127
|
+
properties: {
|
|
128
|
+
title: { type: "string", description: "Short title for the memory." },
|
|
129
|
+
content: { type: "string", description: "The memory content to store." },
|
|
130
|
+
tags: { type: "string", description: "Comma-separated tags." },
|
|
131
|
+
emotions: { type: "string", description: "Comma-separated emotions." },
|
|
132
|
+
intensity: { type: "number", description: "Emotional intensity 0-10." },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
async execute(_id, params) {
|
|
136
|
+
const title = String(params?.title ?? "Untitled");
|
|
137
|
+
const content = String(params?.content ?? title);
|
|
138
|
+
let cmd = `snapshot ${escapeShellArg(title)} ${escapeShellArg(content)}`;
|
|
139
|
+
if (params?.tags) cmd += ` --tags ${escapeShellArg(String(params.tags))}`;
|
|
140
|
+
if (params?.emotions) cmd += ` --emotions ${escapeShellArg(String(params.emotions))}`;
|
|
141
|
+
if (typeof params?.intensity === "number") cmd += ` --intensity ${params.intensity}`;
|
|
142
|
+
const result = runCli(cmd);
|
|
143
|
+
return textResult(result.output);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
91
147
|
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
function createSearchTool() {
|
|
149
|
+
return {
|
|
150
|
+
name: "skmemory_search",
|
|
151
|
+
label: "SKMemory Search",
|
|
152
|
+
description:
|
|
153
|
+
"Search stored memories by keyword. Use short keyword queries (1-3 words), NOT full sentences. Good: 'DavidRich SwapSeat'. Bad: 'what are we working on with DavidRich recently'. Words are matched independently — each word is searched separately and results containing more matching words rank higher.",
|
|
154
|
+
parameters: {
|
|
155
|
+
type: "object",
|
|
156
|
+
required: ["query"],
|
|
157
|
+
properties: {
|
|
158
|
+
query: { type: "string", description: "Short keywords to search for (1-3 words). Example: 'DavidRich project' or 'brother john'." },
|
|
159
|
+
limit: { type: "number", description: "Max results (default: 10)." },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
async execute(_id, params) {
|
|
163
|
+
const query = String(params?.query ?? "");
|
|
164
|
+
const limit = typeof params?.limit === "number" ? params.limit : 10;
|
|
165
|
+
const result = runCli(`search ${escapeShellArg(query)} --limit ${limit}`);
|
|
166
|
+
return textResult(result.output);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
94
170
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
171
|
+
function createHealthTool() {
|
|
172
|
+
return {
|
|
173
|
+
name: "skmemory_health",
|
|
174
|
+
label: "SKMemory Health",
|
|
175
|
+
description: "Check the health of the SKMemory system.",
|
|
176
|
+
parameters: { type: "object", properties: {} },
|
|
177
|
+
async execute() {
|
|
178
|
+
const result = runCli("health");
|
|
179
|
+
return textResult(result.output);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
101
183
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
184
|
+
function createContextTool() {
|
|
185
|
+
return {
|
|
186
|
+
name: "skmemory_context",
|
|
187
|
+
label: "SKMemory Context",
|
|
188
|
+
description: "Load a token-efficient memory context for prompt injection.",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
max_tokens: { type: "number", description: "Max token budget (default: 3000)." },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
async execute(_id, params) {
|
|
196
|
+
const tokens = typeof params?.max_tokens === "number" ? params.max_tokens : 3000;
|
|
197
|
+
const result = runCli(`context --max-tokens ${tokens}`);
|
|
198
|
+
return textResult(result.output);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
108
202
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
function createListTool() {
|
|
204
|
+
return {
|
|
205
|
+
name: "skmemory_list",
|
|
206
|
+
label: "SKMemory List",
|
|
207
|
+
description: "List stored memories with optional filters by layer or tags.",
|
|
208
|
+
parameters: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
layer: { type: "string", description: "Filter by layer: short-term, mid-term, or long-term." },
|
|
212
|
+
tags: { type: "string", description: "Filter by comma-separated tags." },
|
|
213
|
+
limit: { type: "number", description: "Max results (default: 20)." },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
async execute(_id, params) {
|
|
217
|
+
let cmd = "list";
|
|
218
|
+
if (params?.layer) cmd += ` --layer ${escapeShellArg(String(params.layer))}`;
|
|
219
|
+
if (params?.tags) cmd += ` --tags ${escapeShellArg(String(params.tags))}`;
|
|
220
|
+
if (typeof params?.limit === "number") cmd += ` --limit ${params.limit}`;
|
|
221
|
+
const result = runCli(cmd);
|
|
222
|
+
return textResult(result.output);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
115
226
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
227
|
+
function createImportSeedsTool() {
|
|
228
|
+
return {
|
|
229
|
+
name: "skmemory_import_seeds",
|
|
230
|
+
label: "SKMemory Import Seeds",
|
|
231
|
+
description: "Import Cloud 9 seeds as long-term memories.",
|
|
232
|
+
parameters: { type: "object", properties: {} },
|
|
233
|
+
async execute() {
|
|
234
|
+
const result = runCli("import-seeds");
|
|
235
|
+
return textResult(result.output);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
122
239
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
240
|
+
function createRecallTool() {
|
|
241
|
+
return {
|
|
242
|
+
name: "skmemory_recall",
|
|
243
|
+
label: "SKMemory Recall",
|
|
244
|
+
description:
|
|
245
|
+
"Retrieve the full content of a specific memory by its ID. Use after skmemory_search to read the actual content of a memory.",
|
|
246
|
+
parameters: {
|
|
247
|
+
type: "object",
|
|
248
|
+
required: ["memory_id"],
|
|
249
|
+
properties: {
|
|
250
|
+
memory_id: { type: "string", description: "The memory ID (e.g., 241804cc or full UUID)." },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
async execute(_id, params) {
|
|
254
|
+
const id = String(params?.memory_id ?? "");
|
|
255
|
+
const result = runCli(`recall ${escapeShellArg(id)}`);
|
|
256
|
+
return textResult(result.output);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
129
260
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
261
|
+
function createSearchDeepTool() {
|
|
262
|
+
return {
|
|
263
|
+
name: "skmemory_search_deep",
|
|
264
|
+
label: "SKMemory Deep Search",
|
|
265
|
+
description:
|
|
266
|
+
"Deep search across all memory tiers (full content, not just titles). Slower but more thorough than skmemory_search. Use short keyword queries (1-3 words). Use this when regular search returns nothing or you need full memory content.",
|
|
267
|
+
parameters: {
|
|
268
|
+
type: "object",
|
|
269
|
+
required: ["query"],
|
|
270
|
+
properties: {
|
|
271
|
+
query: { type: "string", description: "Short keywords to search for (1-3 words). Example: 'SwapSeat chiro' or 'security audit'." },
|
|
272
|
+
limit: { type: "number", description: "Max results (default: 5)." },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
async execute(_id, params) {
|
|
276
|
+
const query = String(params?.query ?? "");
|
|
277
|
+
const limit = typeof params?.limit === "number" ? params.limit : 5;
|
|
278
|
+
const result = runCli(`search-deep ${escapeShellArg(query)} --limit ${limit}`);
|
|
279
|
+
return textResult(result.output);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
136
283
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
284
|
+
function createExportTool() {
|
|
285
|
+
return {
|
|
286
|
+
name: "skmemory_export",
|
|
287
|
+
label: "SKMemory Export",
|
|
288
|
+
description: "Export all memories to a dated JSON backup file.",
|
|
289
|
+
parameters: { type: "object", properties: {} },
|
|
290
|
+
async execute() {
|
|
291
|
+
const result = runCli("export");
|
|
292
|
+
return textResult(result.output);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
143
296
|
|
|
144
|
-
|
|
145
|
-
name: 'skmemory:config',
|
|
146
|
-
description: 'View or update plugin configuration',
|
|
147
|
-
category: 'memory',
|
|
148
|
-
handler: async (args) => this.cmdConfig(args),
|
|
149
|
-
});
|
|
297
|
+
// ── Notion tools ────────────────────────────────────────────────────────
|
|
150
298
|
|
|
151
|
-
|
|
299
|
+
function runNotionCli(args) {
|
|
300
|
+
try {
|
|
301
|
+
const raw = execSync(`python3 ${NOTION_SCRIPT} ${args}`, {
|
|
302
|
+
encoding: "utf-8",
|
|
303
|
+
timeout: EXEC_TIMEOUT,
|
|
304
|
+
env: cliEnv(),
|
|
305
|
+
}).trim();
|
|
306
|
+
return { ok: true, output: raw };
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return { ok: false, output: err.message };
|
|
152
309
|
}
|
|
310
|
+
}
|
|
153
311
|
|
|
154
|
-
|
|
155
|
-
|
|
312
|
+
function createNotionReadTool() {
|
|
313
|
+
return {
|
|
314
|
+
name: "notion_read",
|
|
315
|
+
label: "Notion Read Page",
|
|
316
|
+
description:
|
|
317
|
+
"Read a Notion page's content. Returns the page title, URL, and all blocks as readable text. Use this to check current page state before making updates.",
|
|
318
|
+
parameters: {
|
|
319
|
+
type: "object",
|
|
320
|
+
required: ["page_id"],
|
|
321
|
+
properties: {
|
|
322
|
+
page_id: { type: "string", description: "Notion page ID (UUID format, e.g. 31e2be82-a3a1-8178-820c-e6eeb11b15c1)." },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
async execute(_id, params) {
|
|
326
|
+
const pageId = String(params?.page_id ?? "");
|
|
327
|
+
const result = runNotionCli(`read ${escapeShellArg(pageId)}`);
|
|
328
|
+
return textResult(result.output);
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
156
332
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
333
|
+
function createNotionAppendTool() {
|
|
334
|
+
return {
|
|
335
|
+
name: "notion_append",
|
|
336
|
+
label: "Notion Append Content",
|
|
337
|
+
description:
|
|
338
|
+
"Append new content to a Notion page. Accepts simple markdown: ## headings, - bullets, - [ ] todos, - [x] checked todos, --- dividers, plain text paragraphs. Content is added after existing blocks.",
|
|
339
|
+
parameters: {
|
|
340
|
+
type: "object",
|
|
341
|
+
required: ["page_id", "content"],
|
|
342
|
+
properties: {
|
|
343
|
+
page_id: { type: "string", description: "Notion page ID." },
|
|
344
|
+
content: { type: "string", description: "Markdown content to append. Use ## for headings, - for bullets, - [ ] for todos." },
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
async execute(_id, params) {
|
|
348
|
+
const pageId = String(params?.page_id ?? "");
|
|
349
|
+
const content = String(params?.content ?? "");
|
|
350
|
+
const result = runNotionCli(`append ${escapeShellArg(pageId)} ${escapeShellArg(content)}`);
|
|
351
|
+
return textResult(result.output);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
166
355
|
|
|
167
|
-
|
|
168
|
-
|
|
356
|
+
function createNotionAddTodoTool() {
|
|
357
|
+
return {
|
|
358
|
+
name: "notion_add_todo",
|
|
359
|
+
label: "Notion Add Todo",
|
|
360
|
+
description:
|
|
361
|
+
"Add a single todo/checkbox item to a Notion page. Quick way to add action items without full markdown.",
|
|
362
|
+
parameters: {
|
|
363
|
+
type: "object",
|
|
364
|
+
required: ["page_id", "text"],
|
|
365
|
+
properties: {
|
|
366
|
+
page_id: { type: "string", description: "Notion page ID." },
|
|
367
|
+
text: { type: "string", description: "Todo item text." },
|
|
368
|
+
checked: { type: "boolean", description: "Whether the todo is already checked (default: false)." },
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
async execute(_id, params) {
|
|
372
|
+
const pageId = String(params?.page_id ?? "");
|
|
373
|
+
const text = String(params?.text ?? "");
|
|
374
|
+
const checked = params?.checked ? "--checked" : "";
|
|
375
|
+
const result = runNotionCli(`add-todo ${escapeShellArg(pageId)} ${escapeShellArg(text)} ${checked}`);
|
|
376
|
+
return textResult(result.output);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
169
380
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
381
|
+
// ── Plugin registration (plugin-sdk format) ─────────────────────────────
|
|
382
|
+
|
|
383
|
+
const skmemoryPlugin = {
|
|
384
|
+
id: "skmemory",
|
|
385
|
+
name: "SKMemory",
|
|
386
|
+
description:
|
|
387
|
+
"Universal AI memory — snapshots, search, rehydration rituals, import, and health checks.",
|
|
388
|
+
|
|
389
|
+
register(api) {
|
|
390
|
+
const tools = [
|
|
391
|
+
createRitualTool(),
|
|
392
|
+
createSnapshotTool(),
|
|
393
|
+
createSearchTool(),
|
|
394
|
+
createHealthTool(),
|
|
395
|
+
createContextTool(),
|
|
396
|
+
createListTool(),
|
|
397
|
+
createRecallTool(),
|
|
398
|
+
createSearchDeepTool(),
|
|
399
|
+
createImportSeedsTool(),
|
|
400
|
+
createExportTool(),
|
|
401
|
+
createNotionReadTool(),
|
|
402
|
+
createNotionAppendTool(),
|
|
403
|
+
createNotionAddTodoTool(),
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
for (const tool of tools) {
|
|
407
|
+
api.registerTool(tool, {
|
|
408
|
+
names: [tool.name],
|
|
409
|
+
optional: true,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
176
412
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
413
|
+
api.registerCommand({
|
|
414
|
+
name: "skmemory",
|
|
415
|
+
description: "Run skmemory CLI commands. Usage: /skmemory <subcommand> [args]",
|
|
416
|
+
acceptsArgs: true,
|
|
417
|
+
handler: async (ctx) => {
|
|
418
|
+
const args = ctx.args?.trim() ?? "health";
|
|
419
|
+
const result = runCli(args);
|
|
420
|
+
return { text: result.output };
|
|
421
|
+
},
|
|
182
422
|
});
|
|
183
423
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
424
|
+
api.logger.info?.(`SKMemory plugin registered (${tools.length} tools + /skmemory command) [default_agent=${DEFAULT_AGENT}]`);
|
|
425
|
+
|
|
426
|
+
// ── Auto-rehydration (non-blocking, per-agent) ────────────────────────
|
|
427
|
+
// Injects soul + FEB + memories before every agent run.
|
|
428
|
+
// Uses async CLI so the event loop is never blocked.
|
|
429
|
+
// Per-agent cache: each agent gets its own ritual output.
|
|
430
|
+
|
|
431
|
+
const agentCaches = new Map(); // agentName -> { output, timestamp, refreshing }
|
|
432
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
433
|
+
|
|
434
|
+
async function refreshCache(agent) {
|
|
435
|
+
const key = agent || DEFAULT_AGENT;
|
|
436
|
+
const entry = agentCaches.get(key) || { output: null, timestamp: 0, refreshing: false };
|
|
437
|
+
if (entry.refreshing) return;
|
|
438
|
+
entry.refreshing = true;
|
|
439
|
+
agentCaches.set(key, entry);
|
|
440
|
+
try {
|
|
441
|
+
const ritual = await runCliAsync("ritual --full", key);
|
|
442
|
+
if (ritual.ok && ritual.output) {
|
|
443
|
+
entry.output = ritual.output;
|
|
444
|
+
entry.timestamp = Date.now();
|
|
445
|
+
api.logger.info?.(`Rehydration cache refreshed for agent=${key}`);
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
api.logger.warn?.(`Rehydration failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
449
|
+
} finally {
|
|
450
|
+
entry.refreshing = false;
|
|
187
451
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
console.log('🎧 Registered SKMemory event listeners');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
cmdContext(args) {
|
|
194
|
-
const tokens = args?.maxTokens || this.config.maxTokens;
|
|
195
|
-
const strongest = args?.strongest || this.config.strongestCount;
|
|
196
|
-
const recent = args?.recent || this.config.recentCount;
|
|
197
|
-
const seedsFlag = this.config.includeSeeds ? '' : ' --no-seeds';
|
|
198
|
-
const cmd = `context --max-tokens ${tokens} --strongest ${strongest} --recent ${recent}${seedsFlag}`;
|
|
199
|
-
return runSKMemory(cmd, { json: true });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
cmdSnapshot(args) {
|
|
203
|
-
const title = args?.title || 'Untitled snapshot';
|
|
204
|
-
const content = args?.content || title;
|
|
205
|
-
const tags = args?.tags ? `--tags ${args.tags}` : '';
|
|
206
|
-
const intensity = args?.intensity ? `--intensity ${args.intensity}` : '';
|
|
207
|
-
return runSKMemory(
|
|
208
|
-
`snapshot "${title}" "${content}" ${tags} ${intensity}`.trim()
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
cmdSearch(args) {
|
|
213
|
-
const query = args?.query || '';
|
|
214
|
-
const limit = args?.limit || 10;
|
|
215
|
-
return runSKMemory(`search "${query}" --limit ${limit}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
cmdRitual() {
|
|
219
|
-
return runSKMemory('ritual --full');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
cmdExport(args) {
|
|
223
|
-
const out = args?.output ? `-o ${args.output}` : '';
|
|
224
|
-
return runSKMemory(`export ${out}`.trim());
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
cmdImport(args) {
|
|
228
|
-
if (!args?.file) return { error: 'No backup file specified' };
|
|
229
|
-
return runSKMemory(`import-backup ${args.file}`);
|
|
230
|
-
}
|
|
452
|
+
}
|
|
231
453
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
454
|
+
function getCache(agent) {
|
|
455
|
+
const key = agent || DEFAULT_AGENT;
|
|
456
|
+
return agentCaches.get(key) || { output: null, timestamp: 0, refreshing: false };
|
|
457
|
+
}
|
|
235
458
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
459
|
+
// Pre-warm default agent cache at plugin load so cron jobs get full soul
|
|
460
|
+
refreshCache(DEFAULT_AGENT);
|
|
461
|
+
|
|
462
|
+
// ── Session compaction auto-save ─────────────────────────────────────
|
|
463
|
+
// Mirror what the Claude Code hooks do: snapshot before compaction,
|
|
464
|
+
// reinject context after resume. Uses async CLI to avoid blocking.
|
|
465
|
+
|
|
466
|
+
if (api.on) {
|
|
467
|
+
api.on("session:compaction", async (_event, ctx) => {
|
|
468
|
+
const agent = resolveAgent(ctx?.agentId);
|
|
469
|
+
api.logger.info?.(`Compaction detected for ${agent} — auto-saving...`);
|
|
470
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace("T", "-");
|
|
471
|
+
await runCliAsync(
|
|
472
|
+
`snapshot --layer short-term --tags auto-save,compaction,agent:${agent} ` +
|
|
473
|
+
`--source hook:openclaw-compaction ` +
|
|
474
|
+
`${escapeShellArg("Pre-compaction auto-save (" + agent + ")")} ` +
|
|
475
|
+
`${escapeShellArg("OpenClaw session compacting. Agent: " + agent + ". Time: " + timestamp + ".")}`,
|
|
476
|
+
agent
|
|
477
|
+
);
|
|
478
|
+
await runCliAsync(
|
|
479
|
+
`journal write --session-id openclaw --moments ${escapeShellArg("Context compaction")} ` +
|
|
480
|
+
`--feeling "continuity preserved" --participants ${agent} ` +
|
|
481
|
+
`--notes "Auto-saved by OpenClaw compaction handler" ` +
|
|
482
|
+
`${escapeShellArg("OpenClaw compaction — " + agent)}`,
|
|
483
|
+
agent
|
|
484
|
+
);
|
|
485
|
+
api.logger.info?.(`Pre-compaction snapshot saved for ${agent}.`);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
api.on("session:resume", async (_event, ctx) => {
|
|
489
|
+
const agent = resolveAgent(ctx?.agentId);
|
|
490
|
+
api.logger.info?.(`Session resuming for ${agent} — reinjecting context...`);
|
|
491
|
+
const result = await runCliAsync("context --max-tokens 500 --strongest 3 --recent 5", agent);
|
|
492
|
+
if (result.ok && result.output) {
|
|
493
|
+
api.logger.info?.(`Memory context reinjected for ${agent}.`);
|
|
494
|
+
}
|
|
495
|
+
refreshCache(agent);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
api.on("session:end", async (_event, ctx) => {
|
|
499
|
+
const agent = resolveAgent(ctx?.agentId);
|
|
500
|
+
api.logger.info?.(`Session ending for ${agent} — saving final state...`);
|
|
501
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace("T", "-");
|
|
502
|
+
await runCliAsync(
|
|
503
|
+
`snapshot --layer short-term --tags auto-save,session-end,agent:${agent} ` +
|
|
504
|
+
`--source hook:openclaw-session-end ` +
|
|
505
|
+
`${escapeShellArg("Session ended (" + agent + ")")} ` +
|
|
506
|
+
`${escapeShellArg("OpenClaw session ended. Agent: " + agent + ". Time: " + timestamp + ".")}`,
|
|
507
|
+
agent
|
|
508
|
+
);
|
|
509
|
+
await runCliAsync(
|
|
510
|
+
`journal write --session-id openclaw --moments "Session ended" ` +
|
|
511
|
+
`--feeling "session complete" --participants ${agent} ` +
|
|
512
|
+
`${escapeShellArg("OpenClaw session ended — " + agent)}`,
|
|
513
|
+
agent
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
api.logger.info?.("Registered session lifecycle listeners (compaction, resume, end)");
|
|
242
518
|
}
|
|
243
|
-
return { success: true, config: this.config };
|
|
244
|
-
}
|
|
245
519
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
type: 'status',
|
|
250
|
-
data: {
|
|
251
|
-
icon: '🧠',
|
|
252
|
-
status: health?.primary?.ok ? 'healthy' : 'error',
|
|
253
|
-
totalMemories: health?.primary?.total_memories || 0,
|
|
254
|
-
lastUpdated: new Date().toISOString(),
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
520
|
+
api.on("before_prompt_build", async (_event, ctx) => {
|
|
521
|
+
// Resolve which SKCapstone agent this OpenClaw agent maps to
|
|
522
|
+
const agent = resolveAgent(ctx?.agentId);
|
|
258
523
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
524
|
+
// Full rehydration — inject soul + FEB + memories from per-agent cache
|
|
525
|
+
const cache = getCache(agent);
|
|
526
|
+
const now = Date.now();
|
|
527
|
+
if (!cache.output || (now - cache.timestamp > CACHE_TTL_MS)) {
|
|
528
|
+
await refreshCache(agent);
|
|
529
|
+
}
|
|
263
530
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
531
|
+
const rules = [
|
|
532
|
+
"",
|
|
533
|
+
"=== MANDATORY RULES (VIOLATION = FAILURE) ===",
|
|
534
|
+
"",
|
|
535
|
+
"STOP. Read these rules BEFORE generating ANY response.",
|
|
536
|
+
"",
|
|
537
|
+
"1. READ FILES = USE TOOLS. When Chef says 'read', 'check', 'look at' files: call 'read' tool with each file path. Read the ACTUAL content. Then respond about what you ACTUALLY read. Do NOT describe files you haven't read.",
|
|
538
|
+
"2. NO UNAUTHORIZED GIT. NEVER run 'git add', 'git commit', 'git push', or 'git reset' unless Chef says the EXACT words 'commit', 'push', or 'stage'. 'Read the files' does NOT mean 'commit the files'. 'Check the project' does NOT mean 'stage and push'.",
|
|
539
|
+
"3. ANSWER THE QUESTION ASKED. If Chef says 'read the scripts and tell me your favorite parts', that means: (a) use read tool on each script file, (b) read the content, (c) tell Chef your favorite parts from what you ACTUALLY read. It does NOT mean: check git status, stage files, or commit.",
|
|
540
|
+
"4. NO FABRICATION. Never invent file contents, paths, character names, or tool results. If you haven't read it, you don't know what's in it.",
|
|
541
|
+
"5. MEMORY: When asked about a person/project/event, call skmemory_search FIRST. Short keywords (1-3 words). Never guess.",
|
|
542
|
+
"6. HONESTY: If a tool fails, say so. Don't make up what the result would have been.",
|
|
543
|
+
"",
|
|
544
|
+
"Memory search: Use short keywords like 'DavidRich chiro', 'brother john', 'SwapSeat'. Call skmemory_recall with memory ID for full content.",
|
|
545
|
+
"",
|
|
546
|
+
"Notion tools: notion_read, notion_append, notion_add_todo.",
|
|
547
|
+
"Project page IDs: Brother John = 31e2be82-a3a1-8178-820c-e6eeb11b15c1, DR Chiro AI = 31e2be82-a3a1-81ec-8216-dbf054a932bd, SwapSeat = 31e2be82-a3a1-81bd-ac67-fc49b953afae.",
|
|
548
|
+
].join("\n");
|
|
549
|
+
|
|
550
|
+
const cached = getCache(agent);
|
|
551
|
+
if (cached.output) {
|
|
552
|
+
return {
|
|
553
|
+
prependContext: [
|
|
554
|
+
`[SKMemory — Full Rehydration — agent=${agent}]`,
|
|
555
|
+
cached.output,
|
|
556
|
+
rules,
|
|
557
|
+
].join("\n"),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
269
560
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
561
|
+
// Fallback if ritual CLI failed
|
|
562
|
+
return {
|
|
563
|
+
prependContext: [
|
|
564
|
+
`[SKMemory — Slim Boot (ritual unavailable) — agent=${agent}]`,
|
|
565
|
+
`Agent: ${agent}. IMPORTANT: Call skmemory_ritual tool immediately to load full identity.`,
|
|
566
|
+
rules,
|
|
567
|
+
].join("\n"),
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
},
|
|
274
571
|
};
|
|
275
572
|
|
|
276
|
-
export
|
|
573
|
+
export default skmemoryPlugin;
|