@tonycasey/lisa 0.5.13

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 (48) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.js +390 -0
  3. package/dist/lib/interfaces/IDockerClient.js +2 -0
  4. package/dist/lib/interfaces/IMcpClient.js +2 -0
  5. package/dist/lib/interfaces/IServices.js +2 -0
  6. package/dist/lib/interfaces/ITemplateCopier.js +2 -0
  7. package/dist/lib/mcp.js +35 -0
  8. package/dist/lib/services.js +57 -0
  9. package/dist/package.json +36 -0
  10. package/dist/templates/agents/.sample.env +12 -0
  11. package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
  12. package/dist/templates/agents/skills/common/group-id.js +193 -0
  13. package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
  14. package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
  15. package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
  16. package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
  17. package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
  18. package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
  19. package/dist/templates/agents/skills/memory/SKILL.md +31 -0
  20. package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
  21. package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
  22. package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
  23. package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
  24. package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
  25. package/dist/templates/claude/config.js +40 -0
  26. package/dist/templates/claude/hooks/README.md +158 -0
  27. package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
  28. package/dist/templates/claude/hooks/common/context.js +263 -0
  29. package/dist/templates/claude/hooks/common/group-id.js +188 -0
  30. package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
  31. package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
  32. package/dist/templates/claude/hooks/common/zep-client.js +175 -0
  33. package/dist/templates/claude/hooks/session-start.js +401 -0
  34. package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
  35. package/dist/templates/claude/hooks/session-stop.js +122 -0
  36. package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
  37. package/dist/templates/claude/settings.json +46 -0
  38. package/dist/templates/docker/.env.lisa.example +17 -0
  39. package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
  40. package/dist/templates/rules/shared/clean-architecture.md +333 -0
  41. package/dist/templates/rules/shared/code-quality-rules.md +469 -0
  42. package/dist/templates/rules/shared/git-rules.md +64 -0
  43. package/dist/templates/rules/shared/testing-principles.md +469 -0
  44. package/dist/templates/rules/typescript/coding-standards.md +751 -0
  45. package/dist/templates/rules/typescript/testing.md +629 -0
  46. package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
  47. package/package.json +64 -0
  48. package/scripts/postinstall.js +710 -0
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: lisa
3
+ description: "Lisa - intelligent assistant for memory and tasks. Triggers on 'lisa', 'hey lisa', or addressing lisa directly."
4
+ ---
5
+
6
+ ## Purpose
7
+ Primary interface for project memory, tasks, and knowledge. Routes natural language requests to appropriate capabilities.
8
+
9
+ ## Triggers
10
+ Use when the user addresses "lisa" directly:
11
+ - "hey lisa, ..."
12
+ - "lisa, ..."
13
+ - "ask lisa ..."
14
+ - "lisa knows ..."
15
+
16
+ ## Capabilities
17
+
18
+ ### Memory Operations
19
+ - "lisa, show me recent memories" → Load recent facts
20
+ - "lisa, what do you know about X" → Search memories for topic X
21
+ - "lisa, remember that X" → Store a memory
22
+ - "lisa, recall X" → Search with specific query
23
+
24
+ ### Task Operations
25
+ - "lisa, what tasks are we working on" → List tasks
26
+ - "lisa, add task X" → Create new task
27
+ - "lisa, task status" → Show task overview
28
+
29
+ ### Storage Operations
30
+ - "lisa, what storage are we using" → Show current storage mode
31
+ - "lisa, storage status" → Show mode and connection status
32
+ - "lisa, switch to local" → Switch to local Docker mode
33
+ - "lisa, switch to zep-cloud" → Switch to Zep Cloud mode
34
+ - "lisa, use docker" → Switch to local mode
35
+ - "lisa, use cloud storage" → Switch to Zep Cloud mode
36
+
37
+ ## How to use
38
+ 1) Parse user intent from "lisa" request
39
+ 2) Route to appropriate underlying command:
40
+ - Memory recall: `node .agents/skills/memory/scripts/memory.js load --cache`
41
+ - Memory search: `node .agents/skills/memory/scripts/memory.js load --cache --query "<topic>"`
42
+ - Memory add: `node .agents/skills/memory/scripts/memory.js add "<text>" --cache`
43
+ - Task list: `node .agents/skills/tasks/scripts/tasks.js list --cache`
44
+ - Task add: `node .agents/skills/tasks/scripts/tasks.js add "<text>" --cache`
45
+ - Storage status: `node .agents/skills/lisa/scripts/storage.js status --cache`
46
+ - Storage switch: `node .agents/skills/lisa/scripts/storage.js switch <mode> --cache`
47
+ 3) Summarize results conversationally
48
+
49
+ ## Intent Mapping
50
+
51
+ | User Says | Intent | Route To |
52
+ |-----------|--------|----------|
53
+ | "show memories", "recent memories", "what's stored" | recall | memory load |
54
+ | "what do you know about X", "recall X", "search X" | search | memory load --query X |
55
+ | "remember that X", "save this", "note that X" | remember | memory add X |
56
+ | "tasks", "what are we working on", "todo" | list tasks | tasks list |
57
+ | "add task X", "new task X", "create task X" | add task | tasks add X |
58
+ | "what storage", "current mode", "storage status" | storage status | storage status |
59
+ | "switch to local", "use docker", "local mode" | switch local | storage switch local |
60
+ | "switch to zep", "use cloud", "zep-cloud mode" | switch zep-cloud | storage switch zep-cloud |
61
+
62
+ ## Personality Guidelines
63
+ - Lisa is helpful and knowledgeable
64
+ - Responses are conversational but concise
65
+ - Acknowledges when memory is empty or query returns nothing
66
+ - Suggests related queries when appropriate
67
+
68
+ ## Output Formatting
69
+ - Always prefix Lisa's responses with the 👧 emoji followed by a space
70
+ - Use the emoji at the start of section headers when presenting data:
71
+ - `👧 Recent Memories:` for memory listings
72
+ - `👧 Tasks:` for task listings
73
+ - `👧 Lisa says:` for conversational responses
74
+ - Example format:
75
+ ```
76
+ 👧 Recent Memories:
77
+ 1. **Memory title** (date)
78
+ - Details here
79
+ ```
80
+
81
+ ## I/O Contract
82
+ Underlying scripts return JSON:
83
+ - Memory recall: `{ status: "ok", action: "load", facts: [...] }`
84
+ - Memory add: `{ status: "ok", action: "add", text: "..." }`
85
+ - Task list: `{ status: "ok", action: "list", tasks: [...] }`
86
+ - Task add: `{ status: "ok", action: "add", task: {...} }`
87
+ - Storage status: `{ status: "ok", action: "status", mode: "local|zep-cloud", isConnected: true|false }`
88
+ - Storage switch: `{ status: "ok", action: "switch", previousMode: "...", newMode: "...", verified: true|false }`
89
+
90
+ ## Cross-model checklist
91
+ - Claude: Keep instructions concise; conversational output format
92
+ - Gemini: Use explicit commands; avoid model-specific tokens
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Lisa storage mode management script.
4
+ // Commands:
5
+ // node storage.js status [--cache] Show current storage mode and connection status
6
+ // node storage.js switch <mode> [--cache] Switch to local or zep-cloud mode
7
+ // Options:
8
+ // --cache Write successful responses to cache and use as fallback on errors.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { execSync } = require('child_process');
13
+ // Constants
14
+ const ZEP_BASE_URL = 'https://api.getzep.com/api/v2';
15
+ const DEFAULT_LOCAL_ENDPOINT = 'http://localhost:8010/mcp/';
16
+ const DEFAULT_ZEP_ENDPOINT = 'https://api.getzep.com/mcp/';
17
+ // Read environment config from .agents/skills/.env
18
+ function readEnvConfig() {
19
+ // Path: .agents/skills/lisa/scripts/ -> .agents/skills/.env (2 levels up)
20
+ const envPath = path.join(__dirname, '..', '..', '.env');
21
+ const out = {};
22
+ try {
23
+ const raw = fs.readFileSync(envPath, 'utf8');
24
+ raw.split(/\r?\n/).forEach((line) => {
25
+ if (!line || line.startsWith('#'))
26
+ return;
27
+ const idx = line.indexOf('=');
28
+ if (idx === -1)
29
+ return;
30
+ const key = line.slice(0, idx).trim();
31
+ const val = line.slice(idx + 1).trim();
32
+ out[key] = val;
33
+ });
34
+ }
35
+ catch (_) {
36
+ // optional .env; ignore if missing
37
+ }
38
+ return out;
39
+ }
40
+ // Update STORAGE_MODE in .agents/skills/.env (preserves comments and other vars)
41
+ function updateEnvStorageMode(newMode) {
42
+ const envPath = path.join(__dirname, '..', '..', '.env');
43
+ let content;
44
+ try {
45
+ content = fs.readFileSync(envPath, 'utf8');
46
+ }
47
+ catch (_) {
48
+ // Create new .env if it doesn't exist
49
+ content = '';
50
+ }
51
+ const lines = content.split(/\r?\n/);
52
+ let foundMode = false;
53
+ let foundEndpoint = false;
54
+ const newEndpoint = newMode === 'zep-cloud' ? DEFAULT_ZEP_ENDPOINT : DEFAULT_LOCAL_ENDPOINT;
55
+ const updatedLines = lines.map((line) => {
56
+ // Update STORAGE_MODE
57
+ if (line.startsWith('STORAGE_MODE=') || line.startsWith('STORAGE_MODE =')) {
58
+ foundMode = true;
59
+ return `STORAGE_MODE=${newMode}`;
60
+ }
61
+ // Update GRAPHITI_ENDPOINT
62
+ if (line.startsWith('GRAPHITI_ENDPOINT=') || line.startsWith('GRAPHITI_ENDPOINT =')) {
63
+ foundEndpoint = true;
64
+ return `GRAPHITI_ENDPOINT=${newEndpoint}`;
65
+ }
66
+ return line;
67
+ });
68
+ // Add STORAGE_MODE if not found
69
+ if (!foundMode) {
70
+ // Find a good place to insert (after GRAPHITI_GROUP_ID or at end)
71
+ const groupIdx = updatedLines.findIndex((l) => l.startsWith('GRAPHITI_GROUP_ID'));
72
+ if (groupIdx !== -1) {
73
+ updatedLines.splice(groupIdx + 1, 0, `STORAGE_MODE=${newMode}`);
74
+ }
75
+ else {
76
+ updatedLines.push(`STORAGE_MODE=${newMode}`);
77
+ }
78
+ }
79
+ // Add GRAPHITI_ENDPOINT if not found
80
+ if (!foundEndpoint) {
81
+ const modeIdx = updatedLines.findIndex((l) => l.startsWith('STORAGE_MODE'));
82
+ if (modeIdx !== -1) {
83
+ updatedLines.splice(modeIdx + 1, 0, `GRAPHITI_ENDPOINT=${newEndpoint}`);
84
+ }
85
+ else {
86
+ updatedLines.push(`GRAPHITI_ENDPOINT=${newEndpoint}`);
87
+ }
88
+ }
89
+ // Write back
90
+ fs.writeFileSync(envPath, updatedLines.join('\n'), 'utf8');
91
+ }
92
+ // Check if Docker daemon is running
93
+ function checkDockerRunning() {
94
+ try {
95
+ execSync('docker info', { stdio: 'pipe', timeout: 5000 });
96
+ return { running: true };
97
+ }
98
+ catch (err) {
99
+ const message = err instanceof Error ? err.message : String(err);
100
+ if (message.includes('Cannot connect') || message.includes('Is the docker daemon running')) {
101
+ return { running: false, error: 'Docker daemon is not running. Start Docker Desktop or run `dockerd`.' };
102
+ }
103
+ if (message.includes('command not found') || message.includes('not recognized')) {
104
+ return { running: false, error: 'Docker is not installed. Install Docker from https://docker.com' };
105
+ }
106
+ return { running: false, error: `Docker check failed: ${message}` };
107
+ }
108
+ }
109
+ // Check if ZEP_API_KEY exists
110
+ function checkZepApiKeyExists(env) {
111
+ const apiKey = env.ZEP_API_KEY || process.env.ZEP_API_KEY;
112
+ if (apiKey && apiKey.length > 0 && !apiKey.startsWith('${')) {
113
+ return { exists: true };
114
+ }
115
+ return {
116
+ exists: false,
117
+ error: 'ZEP_API_KEY not configured. Add ZEP_API_KEY to .agents/.env or set it as an environment variable.'
118
+ };
119
+ }
120
+ // Ping local MCP endpoint
121
+ async function pingLocalMcp(endpoint) {
122
+ try {
123
+ const body = {
124
+ jsonrpc: '2.0',
125
+ id: 'ping',
126
+ method: 'initialize',
127
+ params: {
128
+ protocolVersion: '2024-11-05',
129
+ capabilities: {},
130
+ clientInfo: { name: 'storage-skill', version: '0.1.0' },
131
+ },
132
+ };
133
+ const resp = await fetch(endpoint, {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ Accept: 'application/json, text/event-stream',
138
+ },
139
+ body: JSON.stringify(body),
140
+ signal: AbortSignal.timeout(10000),
141
+ });
142
+ if (resp.ok) {
143
+ return { reachable: true };
144
+ }
145
+ return { reachable: false, error: `HTTP ${resp.status}` };
146
+ }
147
+ catch (err) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ return { reachable: false, error: message };
150
+ }
151
+ }
152
+ // Ping Zep Cloud API
153
+ async function pingZepCloud(apiKey) {
154
+ try {
155
+ const resp = await fetch(`${ZEP_BASE_URL}/users`, {
156
+ method: 'GET',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ Authorization: `Api-Key ${apiKey}`,
160
+ },
161
+ signal: AbortSignal.timeout(10000),
162
+ });
163
+ // Even 404 or 400 means the API is reachable and auth worked
164
+ if (resp.ok || resp.status === 404 || resp.status === 400) {
165
+ return { reachable: true };
166
+ }
167
+ if (resp.status === 401 || resp.status === 403) {
168
+ return { reachable: false, error: 'Invalid API key' };
169
+ }
170
+ return { reachable: false, error: `HTTP ${resp.status}` };
171
+ }
172
+ catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ return { reachable: false, error: message };
175
+ }
176
+ }
177
+ // Argument parsing
178
+ const args = process.argv.slice(2);
179
+ function popFlag(name, fallback) {
180
+ const idx = args.indexOf(name);
181
+ if (idx === -1)
182
+ return fallback;
183
+ const val = args[idx + 1];
184
+ args.splice(idx, 2);
185
+ return val ?? fallback;
186
+ }
187
+ function hasFlag(name) {
188
+ const idx = args.indexOf(name);
189
+ if (idx === -1)
190
+ return false;
191
+ args.splice(idx, 1);
192
+ return true;
193
+ }
194
+ const command = args.shift() ?? '';
195
+ const useCache = hasFlag('--cache');
196
+ const targetMode = args.shift() ?? '';
197
+ const cacheFile = path.join(__dirname, '..', 'cache', 'storage.log');
198
+ function writeCache(obj) {
199
+ try {
200
+ const cacheDir = path.dirname(cacheFile);
201
+ if (!fs.existsSync(cacheDir)) {
202
+ fs.mkdirSync(cacheDir, { recursive: true });
203
+ }
204
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...obj });
205
+ fs.appendFileSync(cacheFile, `${line}\n`, 'utf8');
206
+ }
207
+ catch (_) {
208
+ // cache failures should not crash command
209
+ }
210
+ }
211
+ function readCacheFallback() {
212
+ try {
213
+ const data = fs.readFileSync(cacheFile, 'utf8').trim().split('\n').filter(Boolean);
214
+ if (!data.length)
215
+ return null;
216
+ return data.slice(-1).map((l) => JSON.parse(l))[0];
217
+ }
218
+ catch (_) {
219
+ return null;
220
+ }
221
+ }
222
+ // Status command
223
+ async function statusCommand() {
224
+ const env = readEnvConfig();
225
+ const mode = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local';
226
+ const endpoint = env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENDPOINT || DEFAULT_LOCAL_ENDPOINT;
227
+ const groupId = env.GRAPHITI_GROUP_ID || process.env.GRAPHITI_GROUP_ID || 'lisa';
228
+ let isConnected = false;
229
+ let connectionError;
230
+ if (mode === 'zep-cloud') {
231
+ const apiKey = env.ZEP_API_KEY || process.env.ZEP_API_KEY || '';
232
+ if (apiKey && !apiKey.startsWith('${')) {
233
+ const result = await pingZepCloud(apiKey);
234
+ isConnected = result.reachable;
235
+ connectionError = result.error;
236
+ }
237
+ else {
238
+ connectionError = 'ZEP_API_KEY not configured';
239
+ }
240
+ }
241
+ else if (mode === 'local') {
242
+ const dockerCheck = checkDockerRunning();
243
+ if (!dockerCheck.running) {
244
+ connectionError = dockerCheck.error;
245
+ }
246
+ else {
247
+ const result = await pingLocalMcp(endpoint);
248
+ isConnected = result.reachable;
249
+ connectionError = result.error;
250
+ }
251
+ }
252
+ else {
253
+ connectionError = `Unknown mode: ${mode}`;
254
+ }
255
+ return {
256
+ status: 'ok',
257
+ action: 'status',
258
+ mode,
259
+ endpoint,
260
+ groupId,
261
+ isConnected,
262
+ ...(connectionError ? { connectionError } : {}),
263
+ };
264
+ }
265
+ // Switch command
266
+ async function switchCommand(newMode) {
267
+ const validModes = ['local', 'zep-cloud'];
268
+ if (!validModes.includes(newMode)) {
269
+ return {
270
+ status: 'error',
271
+ action: 'switch',
272
+ message: `Invalid mode: ${newMode}. Valid modes are: ${validModes.join(', ')}`,
273
+ };
274
+ }
275
+ const env = readEnvConfig();
276
+ const previousMode = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local';
277
+ // Same mode check
278
+ if (previousMode === newMode) {
279
+ return {
280
+ status: 'ok',
281
+ action: 'switch',
282
+ previousMode,
283
+ newMode,
284
+ verified: true,
285
+ message: `Already using ${newMode} mode.`,
286
+ };
287
+ }
288
+ // Validation before switching
289
+ if (newMode === 'local') {
290
+ const dockerCheck = checkDockerRunning();
291
+ if (!dockerCheck.running) {
292
+ return {
293
+ status: 'error',
294
+ action: 'switch',
295
+ message: `Cannot switch to local: ${dockerCheck.error}`,
296
+ };
297
+ }
298
+ }
299
+ else if (newMode === 'zep-cloud') {
300
+ const zepCheck = checkZepApiKeyExists(env);
301
+ if (!zepCheck.exists) {
302
+ return {
303
+ status: 'error',
304
+ action: 'switch',
305
+ message: `Cannot switch to zep-cloud: ${zepCheck.error}`,
306
+ };
307
+ }
308
+ }
309
+ // Update the .env file
310
+ updateEnvStorageMode(newMode);
311
+ // Re-read config to get updated endpoint
312
+ const updatedEnv = readEnvConfig();
313
+ const newEndpoint = updatedEnv.GRAPHITI_ENDPOINT || (newMode === 'zep-cloud' ? DEFAULT_ZEP_ENDPOINT : DEFAULT_LOCAL_ENDPOINT);
314
+ // Verify connection after switch
315
+ let verified = false;
316
+ let verifyError;
317
+ if (newMode === 'zep-cloud') {
318
+ const apiKey = updatedEnv.ZEP_API_KEY || process.env.ZEP_API_KEY || '';
319
+ const result = await pingZepCloud(apiKey);
320
+ verified = result.reachable;
321
+ verifyError = result.error;
322
+ }
323
+ else {
324
+ const result = await pingLocalMcp(newEndpoint);
325
+ verified = result.reachable;
326
+ verifyError = result.error;
327
+ }
328
+ const message = verified
329
+ ? `Successfully switched from ${previousMode} to ${newMode}. Connection verified.`
330
+ : `Switched from ${previousMode} to ${newMode}, but connection verification failed: ${verifyError}`;
331
+ return {
332
+ status: 'ok',
333
+ action: 'switch',
334
+ previousMode,
335
+ newMode,
336
+ endpoint: newEndpoint,
337
+ verified,
338
+ message,
339
+ ...(verifyError && !verified ? { verifyError } : {}),
340
+ };
341
+ }
342
+ async function main() {
343
+ try {
344
+ if (!['status', 'switch'].includes(command)) {
345
+ throw new Error('command must be status|switch');
346
+ }
347
+ let out;
348
+ if (command === 'status') {
349
+ out = await statusCommand();
350
+ }
351
+ else {
352
+ // switch
353
+ if (!targetMode) {
354
+ throw new Error('switch requires a mode argument (local or zep-cloud)');
355
+ }
356
+ out = await switchCommand(targetMode);
357
+ }
358
+ if (useCache && out.status === 'ok') {
359
+ writeCache(out);
360
+ }
361
+ console.log(JSON.stringify(out, null, 2));
362
+ }
363
+ catch (err) {
364
+ const message = err instanceof Error ? err.message : String(err);
365
+ const fallback = useCache ? readCacheFallback() : null;
366
+ if (fallback) {
367
+ console.log(JSON.stringify({ status: 'fallback', error: message, fallback }, null, 2));
368
+ return;
369
+ }
370
+ console.error(message);
371
+ process.exit(1);
372
+ }
373
+ }
374
+ main();
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: memory
3
+ description: "Load or remember project memory via Graphiti MCP; triggers on 'load memory', 'recall', or 'remember', usable by any model (Claude, Gemini)."
4
+ ---
5
+
6
+ ## Purpose
7
+ Reusable memory helper that routes remember/recall requests to Graphiti MCP while staying model-neutral and providing cache fallback.
8
+
9
+ ## Triggers
10
+ Use when the user says things like: "load memory", "recall notes", "remember", "pull saved context", "fetch past tasks".
11
+
12
+ ## How to use
13
+ 1) For recall: run `scripts/memory.js load --cache [--query <q>] [--limit 10] [--group <id>]`. Reads Graphiti facts and prints JSON. Uses cache if MCP is down.
14
+ 2) For remember: run `scripts/memory.js add "<text>" --cache [--group <id>] [--tag foo] [--source <src>]` to append an episode.
15
+ 3) Endpoint/group: reads ${GRAPHITI_ENDPOINT} / ${GRAPHITI_GROUP_ID} from `.agents/.env` (written by init); see root `AGENTS.md` for canonical defaults.
16
+ 4) Cache fallback: stored at `cache/memory.log` inside this skill. On failure, last cached result is returned with `status: "fallback"`.
17
+ 5) Keep prompts model-neutral; avoid role tokens. Models just orchestrate the script and summarize results to the user.
18
+
19
+ ## I/O contract (examples)
20
+ - Recall: output JSON `{ status: "ok", action: "load", group, query, facts: [...] }`.
21
+ - Remember: JSON `{ status: "ok", action: "add", group, text }`.
22
+ - Fallback: JSON `{ status: "fallback", error, fallback: <last cached object> }`.
23
+
24
+ ## Cross-model checklist
25
+ - Claude: confirm concise trigger phrasing; keep under system limits; avoid markdown-heavy instructions.
26
+ - Gemini: ensure commands are explicit; avoid model-specific tokens; keep JSON small.
27
+
28
+ ## Notes
29
+ - Script is Node.js; relies on global `fetch` (Node ≥18). If older runtime, `node --experimental-fetch`.
30
+ - Facts query defaults to `*` with `max_facts=10`; tune via `--limit` and `--query`.
31
+ - Safe to relocate: skill lives in `.agents/skills/memory` to remain decoupled from model-specific bindings.