@smilintux/skmemory 0.7.2 → 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.
Files changed (111) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/.github/workflows/publish.yml +4 -5
  3. package/ARCHITECTURE.md +298 -0
  4. package/CHANGELOG.md +27 -1
  5. package/README.md +6 -0
  6. package/examples/stignore-agent.example +59 -0
  7. package/examples/stignore-root.example +62 -0
  8. package/openclaw-plugin/package.json +2 -1
  9. package/openclaw-plugin/src/index.js +527 -230
  10. package/package.json +1 -1
  11. package/pyproject.toml +5 -2
  12. package/scripts/dream-rescue.py +179 -0
  13. package/scripts/memory-cleanup.py +313 -0
  14. package/scripts/recover-missing.py +180 -0
  15. package/scripts/skcapstone-backup.sh +44 -0
  16. package/seeds/cloud9-lumina.seed.json +6 -4
  17. package/seeds/cloud9-opus.seed.json +6 -4
  18. package/seeds/courage.seed.json +9 -2
  19. package/seeds/curiosity.seed.json +9 -2
  20. package/seeds/grief.seed.json +9 -2
  21. package/seeds/joy.seed.json +9 -2
  22. package/seeds/love.seed.json +9 -2
  23. package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
  24. package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
  25. package/seeds/lumina-kingdom-founding.seed.json +9 -7
  26. package/seeds/lumina-pma-signed.seed.json +8 -6
  27. package/seeds/lumina-singular-achievement.seed.json +8 -6
  28. package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
  29. package/seeds/plant-lumina-seeds.py +2 -2
  30. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  31. package/seeds/sovereignty.seed.json +9 -2
  32. package/seeds/trust.seed.json +9 -2
  33. package/skmemory/__init__.py +16 -13
  34. package/skmemory/agents.py +10 -10
  35. package/skmemory/ai_client.py +10 -21
  36. package/skmemory/anchor.py +5 -9
  37. package/skmemory/audience.py +278 -0
  38. package/skmemory/backends/__init__.py +1 -1
  39. package/skmemory/backends/base.py +3 -4
  40. package/skmemory/backends/file_backend.py +18 -13
  41. package/skmemory/backends/skgraph_backend.py +7 -19
  42. package/skmemory/backends/skvector_backend.py +7 -18
  43. package/skmemory/backends/sqlite_backend.py +115 -32
  44. package/skmemory/backends/vaulted_backend.py +7 -9
  45. package/skmemory/cli.py +146 -78
  46. package/skmemory/config.py +11 -13
  47. package/skmemory/context_loader.py +21 -23
  48. package/skmemory/data/audience_config.json +60 -0
  49. package/skmemory/endpoint_selector.py +36 -31
  50. package/skmemory/febs.py +225 -0
  51. package/skmemory/fortress.py +30 -40
  52. package/skmemory/hooks/__init__.py +18 -0
  53. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  54. package/skmemory/hooks/pre-compact-save.sh +81 -0
  55. package/skmemory/hooks/session-end-save.sh +103 -0
  56. package/skmemory/hooks/session-start-ritual.sh +104 -0
  57. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  58. package/skmemory/importers/telegram.py +42 -13
  59. package/skmemory/importers/telegram_api.py +152 -60
  60. package/skmemory/journal.py +3 -7
  61. package/skmemory/lovenote.py +4 -11
  62. package/skmemory/mcp_server.py +182 -29
  63. package/skmemory/models.py +10 -8
  64. package/skmemory/openclaw.py +14 -22
  65. package/skmemory/post_install.py +86 -0
  66. package/skmemory/predictive.py +13 -9
  67. package/skmemory/promotion.py +48 -24
  68. package/skmemory/quadrants.py +100 -24
  69. package/skmemory/register.py +144 -18
  70. package/skmemory/register_mcp.py +1 -2
  71. package/skmemory/ritual.py +104 -13
  72. package/skmemory/seeds.py +21 -26
  73. package/skmemory/setup_wizard.py +40 -52
  74. package/skmemory/sharing.py +11 -5
  75. package/skmemory/soul.py +29 -10
  76. package/skmemory/steelman.py +43 -17
  77. package/skmemory/store.py +152 -30
  78. package/skmemory/synthesis.py +634 -0
  79. package/skmemory/vault.py +2 -5
  80. package/tests/conftest.py +46 -0
  81. package/tests/integration/conftest.py +6 -6
  82. package/tests/integration/test_cross_backend.py +4 -9
  83. package/tests/integration/test_skgraph_live.py +3 -7
  84. package/tests/integration/test_skvector_live.py +1 -4
  85. package/tests/test_ai_client.py +1 -4
  86. package/tests/test_audience.py +233 -0
  87. package/tests/test_backup_rotation.py +5 -14
  88. package/tests/test_endpoint_selector.py +101 -63
  89. package/tests/test_export_import.py +4 -10
  90. package/tests/test_file_backend.py +0 -1
  91. package/tests/test_fortress.py +6 -5
  92. package/tests/test_fortress_hardening.py +13 -16
  93. package/tests/test_openclaw.py +1 -4
  94. package/tests/test_predictive.py +1 -1
  95. package/tests/test_promotion.py +10 -3
  96. package/tests/test_quadrants.py +11 -5
  97. package/tests/test_ritual.py +18 -14
  98. package/tests/test_seeds.py +4 -10
  99. package/tests/test_setup.py +203 -88
  100. package/tests/test_sharing.py +15 -8
  101. package/tests/test_skgraph_backend.py +22 -29
  102. package/tests/test_skvector_backend.py +2 -2
  103. package/tests/test_soul.py +1 -3
  104. package/tests/test_sqlite_backend.py +8 -17
  105. package/tests/test_steelman.py +2 -3
  106. package/tests/test_store.py +0 -2
  107. package/tests/test_store_graph_integration.py +2 -2
  108. package/tests/test_synthesis.py +275 -0
  109. package/tests/test_telegram_import.py +39 -15
  110. package/tests/test_vault.py +4 -3
  111. package/openclaw-plugin/src/index.ts +0 -255
@@ -1,276 +1,573 @@
1
1
  /**
2
- * 🧠 SKMemory - OpenClaw Plugin
2
+ * SKMemory OpenClaw Plugin (plugin-sdk format)
3
3
  *
4
- * Wraps the skmemory Python CLI so OpenClaw can call memory operations
5
- * as first-class commands. Auto-loads context on session start and
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
- * Requires: pip install skmemory (the skmemory CLI must be on PATH)
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
- * @version 0.5.0
11
- * @requires OpenClaw 1.0.0+
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 'child_process';
15
- import path from 'path';
16
- import { fileURLToPath } from 'url';
17
- import fs from 'fs';
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = path.dirname(__filename);
21
-
22
- const PLUGIN = {
23
- name: 'skmemory',
24
- version: '0.5.0',
25
- displayName: '🧠 SKMemory',
26
- description: 'Universal AI memory with emotional context',
27
- author: 'smilinTux Team',
28
- license: 'AGPL-3.0',
29
- category: 'memory',
30
- permissions: ['read', 'write'],
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 runSKMemory(args, { json: parseJson = false } = {}) {
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(`skmemory ${args}`, {
36
- encoding: 'utf-8',
37
- timeout: 30_000,
60
+ const raw = execSync(`${SKMEMORY_BIN} ${args}`, {
61
+ encoding: "utf-8",
62
+ timeout: EXEC_TIMEOUT,
63
+ env: cliEnv(agent),
38
64
  }).trim();
39
- return parseJson ? JSON.parse(raw) : raw;
65
+ return { ok: true, output: raw };
40
66
  } catch (err) {
41
- return { error: err.message };
67
+ return { ok: false, output: err.message };
42
68
  }
43
69
  }
44
70
 
45
- class SKMemoryOpenClawPlugin {
46
- constructor(openclaw) {
47
- this.openclaw = openclaw;
48
- this.config = this.loadConfig();
49
- this.cachedContext = null;
50
- }
51
-
52
- async init() {
53
- console.log('🧠 Initializing SKMemory OpenClaw Plugin...');
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
- this.registerCommands();
56
- this.registerDashboard();
57
- this.setupEvents();
85
+ function textResult(text) {
86
+ return { content: [{ type: "text", text }] };
87
+ }
58
88
 
59
- console.log('✅ SKMemory Plugin initialized');
60
- return true;
61
- }
89
+ function escapeShellArg(s) {
90
+ return `'${s.replace(/'/g, "'\\''")}'`;
91
+ }
62
92
 
63
- loadConfig() {
64
- const configPath = path.join(__dirname, '..', 'config', 'skmemory-plugin.json');
65
- const defaults = {
66
- autoLoadContext: true,
67
- autoExport: true,
68
- maxTokens: 3000,
69
- strongestCount: 5,
70
- recentCount: 5,
71
- includeSeeds: true,
72
- };
73
- try {
74
- if (fs.existsSync(configPath)) {
75
- const saved = JSON.parse(fs.readFileSync(configPath, 'utf8'));
76
- return { ...defaults, ...saved };
77
- }
78
- } catch (_) { /* use defaults */ }
79
- return defaults;
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
- saveConfig() {
83
- const configPath = path.join(__dirname, '..', 'config', 'skmemory-plugin.json');
84
- try {
85
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
86
- fs.writeFileSync(configPath, JSON.stringify(this.config, null, 2));
87
- } catch (err) {
88
- console.error('Failed to save config:', err.message);
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
- registerCommands() {
93
- if (!this.openclaw?.commands) return;
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
- this.openclaw.commands.register({
96
- name: 'skmemory:context',
97
- description: 'Load token-efficient memory context for prompt injection',
98
- category: 'memory',
99
- handler: async (args) => this.cmdContext(args),
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
- this.openclaw.commands.register({
103
- name: 'skmemory:snapshot',
104
- description: 'Capture a memory snapshot',
105
- category: 'memory',
106
- handler: async (args) => this.cmdSnapshot(args),
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
- this.openclaw.commands.register({
110
- name: 'skmemory:search',
111
- description: 'Search memories by text',
112
- category: 'memory',
113
- handler: async (args) => this.cmdSearch(args),
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
- this.openclaw.commands.register({
117
- name: 'skmemory:ritual',
118
- description: 'Perform the rehydration ritual',
119
- category: 'memory',
120
- handler: async () => this.cmdRitual(),
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
- this.openclaw.commands.register({
124
- name: 'skmemory:export',
125
- description: 'Export memories to a dated backup',
126
- category: 'memory',
127
- handler: async (args) => this.cmdExport(args),
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
- this.openclaw.commands.register({
131
- name: 'skmemory:import',
132
- description: 'Import memories from a backup file',
133
- category: 'memory',
134
- handler: async (args) => this.cmdImport(args),
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
- this.openclaw.commands.register({
138
- name: 'skmemory:health',
139
- description: 'Check memory system health',
140
- category: 'memory',
141
- handler: async () => this.cmdHealth(),
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
- this.openclaw.commands.register({
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
- console.log('📝 Registered SKMemory commands');
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
- registerDashboard() {
155
- if (!this.openclaw?.dashboard) return;
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
- this.openclaw.dashboard.registerWidget({
158
- id: 'skmemory-status',
159
- name: '🧠 SKMemory',
160
- category: 'memory',
161
- position: 'bottom',
162
- size: 'small',
163
- render: () => this.renderWidget(),
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
- setupEvents() {
168
- if (!this.openclaw?.events) return;
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
- this.openclaw.events.on('session:start', async () => {
171
- if (this.config.autoLoadContext) {
172
- console.log('🧠 Session start — loading memory context...');
173
- this.cachedContext = this.cmdContext({});
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
- this.openclaw.events.on('session:compaction', async () => {
178
- if (this.config.autoExport) {
179
- console.log('🧠 Compaction detected exporting backup...');
180
- this.cmdExport({});
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
- this.openclaw.events.on('session:resume', async () => {
185
- if (this.config.autoLoadContext) {
186
- this.cachedContext = this.cmdContext({});
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
- cmdHealth() {
233
- return runSKMemory('health', { json: true });
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
- cmdConfig(args) {
237
- if (args?.set) {
238
- const [key, value] = args.set.split('=');
239
- this.config[key] = value;
240
- this.saveConfig();
241
- return { success: true, key, value };
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
- renderWidget() {
247
- const health = runSKMemory('health', { json: true });
248
- return {
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
- getInfo() {
260
- return PLUGIN;
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
- async function init(openclaw) {
265
- const plugin = new SKMemoryOpenClawPlugin(openclaw);
266
- await plugin.init();
267
- return plugin;
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
- export default {
271
- name: PLUGIN.name,
272
- version: PLUGIN.version,
273
- init,
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 { SKMemoryOpenClawPlugin, PLUGIN };
573
+ export default skmemoryPlugin;