@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,533 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Lightweight Graphiti memory helper usable by any model.
4
+ // Commands:
5
+ // node memory.js add "text to remember" [--group <id>] [--tag foo] [--type <type>] [--source <src>] [--cache]
6
+ // node memory.js load [--group <id>] [--query <q>] [--limit N] [--cache]
7
+ // Options:
8
+ // --endpoint <url> : MCP endpoint (default env GRAPHITI_ENDPOINT or http://localhost:8010/mcp/)
9
+ // --type <type> : Entity type (decision, pattern, bug, etc.) - maps to tag
10
+ // --cache : write successful responses to cache/memory.log and use it as fallback on errors.
11
+ // Modes:
12
+ // local : Local Docker MCP server (default)
13
+ // zep-cloud : Zep Cloud native REST API (no Docker required)
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ // ============================================================================
19
+ // Group ID Utilities (inline to avoid import complexity in deployed skills)
20
+ // ============================================================================
21
+ const MAX_GROUP_ID_LENGTH = 128;
22
+ /**
23
+ * Normalize a path to a valid group ID string.
24
+ * Works cross-platform (Windows and Unix).
25
+ * /Users/tony.casey/Repos/api -> users-tony_casey-repos-api
26
+ * C:\dev\lisa -> c-dev-lisa
27
+ */
28
+ function normalizePathToGroupId(absolutePath) {
29
+ let normalized = absolutePath
30
+ .toLowerCase()
31
+ .replace(/^[a-z]:/i, (match) => match.charAt(0)) // C: -> c
32
+ .replace(/^\//, '') // Remove leading slash (Unix)
33
+ .replace(/\\/g, '-') // Backslash to dash (Windows)
34
+ .replace(/\//g, '-') // Forward slash to dash (Unix)
35
+ .replace(/\./g, '_') // Dots to underscores
36
+ .replace(/^-+/, '') // Remove leading dashes
37
+ .replace(/-+/g, '-'); // Collapse multiple dashes
38
+ if (normalized.length > MAX_GROUP_ID_LENGTH) {
39
+ normalized = normalized.slice(-MAX_GROUP_ID_LENGTH);
40
+ }
41
+ return normalized;
42
+ }
43
+ /**
44
+ * Get the current folder's group ID.
45
+ */
46
+ function getCurrentGroupId(cwd = process.cwd()) {
47
+ return normalizePathToGroupId(cwd);
48
+ }
49
+ /**
50
+ * Check if we're on Windows
51
+ */
52
+ function isWindows() {
53
+ return os.platform() === 'win32';
54
+ }
55
+ /**
56
+ * Get the root boundary for hierarchical traversal.
57
+ * - Windows: drive root (e.g., C:\) or home directory, whichever is deeper
58
+ * - Unix: home directory or /
59
+ */
60
+ function getRootBoundary(cwd = process.cwd()) {
61
+ const homeDir = os.homedir();
62
+ if (isWindows()) {
63
+ // On Windows, check if cwd is under home directory
64
+ const cwdLower = cwd.toLowerCase();
65
+ const homeLower = homeDir.toLowerCase();
66
+ if (cwdLower.startsWith(homeLower)) {
67
+ return homeDir; // Under home, use home as boundary
68
+ }
69
+ // Not under home (e.g., C:\dev\), use drive root as boundary
70
+ const driveRoot = path.parse(cwd).root; // e.g., "C:\"
71
+ return driveRoot;
72
+ }
73
+ // Unix: use home directory if under it, otherwise use /
74
+ if (cwd.startsWith(homeDir)) {
75
+ return homeDir;
76
+ }
77
+ return '/';
78
+ }
79
+ /**
80
+ * Get hierarchical group IDs from current folder up to root boundary.
81
+ * Returns array ordered from most specific (current) to least specific (root).
82
+ * Works cross-platform (Windows and Unix).
83
+ */
84
+ function getHierarchicalGroupIds(cwd = process.cwd()) {
85
+ const rootBoundary = getRootBoundary(cwd);
86
+ const groups = [];
87
+ let currentPath = path.resolve(cwd);
88
+ const maxDepth = 10; // Safety limit
89
+ let depth = 0;
90
+ while (depth < maxDepth) {
91
+ groups.push(normalizePathToGroupId(currentPath));
92
+ // Stop if we've reached the root boundary
93
+ if (currentPath.toLowerCase() === rootBoundary.toLowerCase()) {
94
+ break;
95
+ }
96
+ const parentPath = path.dirname(currentPath);
97
+ // Stop if we can't go up anymore (reached filesystem root)
98
+ if (parentPath === currentPath) {
99
+ break;
100
+ }
101
+ currentPath = parentPath;
102
+ depth++;
103
+ }
104
+ return groups;
105
+ }
106
+ // ============================================================================
107
+ // Zep Cloud Native API Client (for zep-cloud mode)
108
+ // ============================================================================
109
+ const ZEP_BASE_URL = 'https://api.getzep.com/api/v2';
110
+ async function zepFetch(apiKey, urlPath, options = {}, timeoutMs = 15000) {
111
+ const url = `${ZEP_BASE_URL}${urlPath}`;
112
+ const resp = await fetch(url, {
113
+ ...options,
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ Authorization: `Api-Key ${apiKey}`,
117
+ ...(options.headers || {}),
118
+ },
119
+ signal: AbortSignal.timeout(timeoutMs),
120
+ });
121
+ const text = await resp.text();
122
+ let data;
123
+ try {
124
+ data = text ? JSON.parse(text) : {};
125
+ }
126
+ catch (_err) {
127
+ throw new Error(`Invalid JSON from Zep (${resp.status}): ${text.slice(0, 200)}`);
128
+ }
129
+ if (!resp.ok) {
130
+ // Zep returns errors in 'message' field, not 'error.message'
131
+ const errorMsg = data.message ||
132
+ data.error?.message ||
133
+ data.error?.detail ||
134
+ `HTTP ${resp.status}`;
135
+ throw new Error(errorMsg);
136
+ }
137
+ return data;
138
+ }
139
+ /**
140
+ * Zep v3 uses a thread-based approach:
141
+ * 1. Ensure user exists
142
+ * 2. Get or create a thread for the project
143
+ * 3. Add messages to the thread
144
+ * Zep extracts facts automatically from messages
145
+ */
146
+ async function zepEnsureUser(apiKey, userId) {
147
+ try {
148
+ // Try to create user - will fail if exists, which is fine
149
+ await zepFetch(apiKey, '/users', {
150
+ method: 'POST',
151
+ body: JSON.stringify({
152
+ user_id: userId,
153
+ first_name: 'Lisa',
154
+ last_name: 'Memory',
155
+ }),
156
+ });
157
+ }
158
+ catch (err) {
159
+ // User already exists is ok (400 with "user already exists")
160
+ if (!(err instanceof Error && err.message.includes('already exists'))) {
161
+ throw err;
162
+ }
163
+ }
164
+ return { user_id: userId };
165
+ }
166
+ async function zepGetOrCreateThread(apiKey, threadId, userId) {
167
+ try {
168
+ // Try to create thread - will fail if exists
169
+ await zepFetch(apiKey, '/threads', {
170
+ method: 'POST',
171
+ body: JSON.stringify({
172
+ thread_id: threadId,
173
+ user_id: userId,
174
+ metadata: { project: threadId, created_by: 'lisa' },
175
+ }),
176
+ });
177
+ }
178
+ catch (err) {
179
+ // Thread already exists is ok
180
+ if (!(err instanceof Error && err.message.includes('already exists'))) {
181
+ throw err;
182
+ }
183
+ }
184
+ return { thread_id: threadId };
185
+ }
186
+ async function zepAddMemory(apiKey, text, groupId, source) {
187
+ // Use groupId as both user_id and thread_id base for consistent storage
188
+ const userId = `lisa-${groupId}`;
189
+ const threadId = `lisa-memory-${groupId}`;
190
+ // Ensure user and thread exist
191
+ await zepEnsureUser(apiKey, userId);
192
+ await zepGetOrCreateThread(apiKey, threadId, userId);
193
+ // Add message to thread (Zep extracts facts automatically)
194
+ const result = await zepFetch(apiKey, `/threads/${encodeURIComponent(threadId)}/messages`, {
195
+ method: 'POST',
196
+ body: JSON.stringify({
197
+ messages: [
198
+ {
199
+ role: 'user',
200
+ role_type: 'user',
201
+ content: `[${source}] ${text}`,
202
+ },
203
+ ],
204
+ }),
205
+ });
206
+ return { message_uuid: result.message_uuids?.[0] };
207
+ }
208
+ async function zepSearchFacts(apiKey, query, groupIds, limit) {
209
+ const allFacts = [];
210
+ // Search across all group IDs (hierarchical)
211
+ for (const groupId of groupIds) {
212
+ const userId = `lisa-${groupId}`;
213
+ try {
214
+ const result = await zepFetch(apiKey, '/graph/search', {
215
+ method: 'POST',
216
+ body: JSON.stringify({
217
+ user_id: userId,
218
+ query,
219
+ limit: Math.ceil(limit / groupIds.length), // Distribute limit across groups
220
+ search_scope: 'facts',
221
+ }),
222
+ });
223
+ // Transform edges to facts format
224
+ const facts = (result.edges || []).map((edge) => ({
225
+ uuid: edge.uuid,
226
+ name: edge.name,
227
+ fact: edge.fact,
228
+ created_at: edge.created_at,
229
+ }));
230
+ allFacts.push(...facts);
231
+ }
232
+ catch (_err) {
233
+ // Group might not exist yet, continue to next
234
+ }
235
+ }
236
+ // Sort by created_at descending and limit
237
+ allFacts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
238
+ return { facts: allFacts.slice(0, limit) };
239
+ }
240
+ // ============================================================================
241
+ // End Zep Cloud Client
242
+ // ============================================================================
243
+ // Entity type to tag mapping
244
+ const TYPE_MAP = {
245
+ // Code & Architecture
246
+ 'decision': 'code:decision',
247
+ 'pattern': 'code:pattern',
248
+ 'dependency': 'code:dependency',
249
+ 'tech-debt': 'code:tech-debt',
250
+ // Context & History
251
+ 'bug': 'context:bug',
252
+ 'rationale': 'context:rationale',
253
+ 'failed': 'context:failed',
254
+ 'quirk': 'context:quirk',
255
+ // External
256
+ 'feedback': 'external:feedback',
257
+ 'incident': 'external:incident',
258
+ 'contract': 'external:contract',
259
+ // People & Process
260
+ 'contributor': 'people:contributor',
261
+ 'review': 'people:review',
262
+ 'blocker': 'people:blocker',
263
+ 'estimate': 'people:estimate',
264
+ // Project
265
+ 'scope-in': 'project:scope-in',
266
+ 'scope-out': 'project:scope-out',
267
+ 'milestone': 'project:milestone',
268
+ 'init-review': 'type:init-review',
269
+ };
270
+ // Auto-detect prefixes in text
271
+ const PREFIX_MAP = {
272
+ 'DECISION:': 'code:decision',
273
+ 'PATTERN:': 'code:pattern',
274
+ 'TECH-DEBT:': 'code:tech-debt',
275
+ 'BUG:': 'context:bug',
276
+ 'RATIONALE:': 'context:rationale',
277
+ 'FAILED:': 'context:failed',
278
+ 'INCIDENT:': 'external:incident',
279
+ 'BLOCKER:': 'people:blocker',
280
+ 'SCOPE-IN:': 'project:scope-in',
281
+ 'SCOPE-OUT:': 'project:scope-out',
282
+ 'INIT-REVIEW:': 'type:init-review',
283
+ };
284
+ function detectPrefixTag(text) {
285
+ for (const [prefix, tag] of Object.entries(PREFIX_MAP)) {
286
+ if (text.toUpperCase().startsWith(prefix)) {
287
+ return tag;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ const args = process.argv.slice(2);
293
+ const env = (() => {
294
+ // Read from .agents/skills/.env (2 levels up from memory/scripts/)
295
+ const envPath = path.join(__dirname, '..', '..', '.env');
296
+ const out = {};
297
+ try {
298
+ const raw = fs.readFileSync(envPath, 'utf8');
299
+ raw.split(/\r?\n/).forEach((line) => {
300
+ if (!line || line.startsWith('#'))
301
+ return;
302
+ const idx = line.indexOf('=');
303
+ if (idx === -1)
304
+ return;
305
+ const key = line.slice(0, idx).trim();
306
+ const val = line.slice(idx + 1).trim();
307
+ out[key] = val;
308
+ });
309
+ }
310
+ catch (_) {
311
+ // optional .env; ignore if missing
312
+ }
313
+ return out;
314
+ })();
315
+ function popFlag(name, fallback) {
316
+ const idx = args.indexOf(name);
317
+ if (idx === -1)
318
+ return fallback;
319
+ const val = args[idx + 1];
320
+ args.splice(idx, 2);
321
+ return val ?? fallback;
322
+ }
323
+ function hasFlag(name) {
324
+ const idx = args.indexOf(name);
325
+ if (idx === -1)
326
+ return false;
327
+ args.splice(idx, 1);
328
+ return true;
329
+ }
330
+ const command = args.shift() ?? '';
331
+ const endpoint = popFlag('--endpoint', env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENDPOINT || 'http://localhost:8010/mcp/');
332
+ // Group ID: explicit --group > env > folder-based (current directory)
333
+ const explicitGroup = popFlag('--group', null);
334
+ const envGroup = env.GRAPHITI_GROUP_ID || process.env.GRAPHITI_GROUP_ID;
335
+ const groupId = explicitGroup || envGroup || getCurrentGroupId();
336
+ // Track if group was explicitly configured (CLI or env) vs auto-detected
337
+ const hasConfiguredGroup = !!(explicitGroup || envGroup);
338
+ const query = popFlag('--query', '');
339
+ const limit = Number(popFlag('--limit', '10')) || 10;
340
+ const explicitTag = popFlag('--tag', null);
341
+ const entityType = popFlag('--type', null);
342
+ const source = popFlag('--source', 'skill:load-memory');
343
+ const useCache = hasFlag('--cache');
344
+ const payload = args.join(' ').trim();
345
+ const cacheFile = path.join(__dirname, '..', 'cache', 'memory.log');
346
+ // Mode detection
347
+ const graphitiMode = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local';
348
+ const zepApiKey = env.ZEP_API_KEY || process.env.ZEP_API_KEY || '';
349
+ const isZepCloud = graphitiMode === 'zep-cloud';
350
+ // Resolve tag: explicit --tag > --type mapping > prefix detection > undefined
351
+ function resolveTag(text) {
352
+ if (explicitTag)
353
+ return explicitTag;
354
+ if (entityType && TYPE_MAP[entityType])
355
+ return TYPE_MAP[entityType];
356
+ const prefixTag = detectPrefixTag(text);
357
+ if (prefixTag)
358
+ return prefixTag;
359
+ return undefined;
360
+ }
361
+ async function initialize() {
362
+ const body = {
363
+ jsonrpc: '2.0',
364
+ id: 'init',
365
+ method: 'initialize',
366
+ params: {
367
+ protocolVersion: '2024-11-05',
368
+ capabilities: {
369
+ experimental: {},
370
+ prompts: { listChanged: false },
371
+ resources: { subscribe: false, listChanged: false },
372
+ tools: { listChanged: false },
373
+ },
374
+ clientInfo: { name: 'memory-skill', version: '0.1.0' },
375
+ },
376
+ };
377
+ const resp = await fetch(endpoint, {
378
+ method: 'POST',
379
+ headers: {
380
+ 'Content-Type': 'application/json',
381
+ // Graphiti requires clients to accept both JSON responses and event streams.
382
+ Accept: 'application/json, text/event-stream',
383
+ },
384
+ body: JSON.stringify(body),
385
+ });
386
+ if (!resp.ok)
387
+ throw new Error(`initialize failed: ${resp.status}`);
388
+ const sid = resp.headers.get('mcp-session-id');
389
+ if (!sid)
390
+ throw new Error('missing mcp-session-id');
391
+ return sid;
392
+ }
393
+ async function rpcCall(method, params, sessionId) {
394
+ const payload = method === 'initialize' || method === 'ping' || method.startsWith('tools/')
395
+ ? { jsonrpc: '2.0', id: '1', method, params }
396
+ : { jsonrpc: '2.0', id: '1', method: 'tools/call', params: { name: method, arguments: params } };
397
+ const resp = await fetch(endpoint, {
398
+ method: 'POST',
399
+ headers: {
400
+ 'Content-Type': 'application/json',
401
+ 'MCP-SESSION-ID': sessionId,
402
+ Accept: 'application/json, text/event-stream',
403
+ },
404
+ body: JSON.stringify(payload),
405
+ });
406
+ let text = await resp.text();
407
+ // MCP servers may wrap JSON in Server-Sent Events; unwrap the data line if present.
408
+ if (text.startsWith('event:')) {
409
+ const dataLine = text.split('\n').find((l) => l.startsWith('data:'));
410
+ if (dataLine)
411
+ text = dataLine.slice(5).trim();
412
+ }
413
+ let data;
414
+ try {
415
+ data = JSON.parse(text);
416
+ }
417
+ catch (err) {
418
+ throw new Error(`bad JSON: ${text.slice(0, 160)}`);
419
+ }
420
+ if (!resp.ok || data.error)
421
+ throw new Error(data?.error?.message || `HTTP ${resp.status}`);
422
+ return data.result?.structuredContent?.result || data.result || data;
423
+ }
424
+ async function addMemory(sessionId) {
425
+ if (!payload)
426
+ throw new Error('add requires text payload');
427
+ const resolvedTag = resolveTag(payload);
428
+ const params = {
429
+ name: payload.slice(0, 80),
430
+ episode_body: payload,
431
+ source,
432
+ group_id: groupId,
433
+ tags: resolvedTag ? [resolvedTag] : undefined,
434
+ };
435
+ await rpcCall('add_memory', params, sessionId);
436
+ return { status: 'ok', action: 'add', group: groupId, text: payload, tag: resolvedTag };
437
+ }
438
+ async function loadMemory(sessionId) {
439
+ // Use configured group (CLI or env) if available, otherwise use hierarchical groups
440
+ const groupIds = hasConfiguredGroup ? [groupId] : getHierarchicalGroupIds();
441
+ const params = { query: query || '*', max_facts: limit, group_ids: groupIds };
442
+ const result = await rpcCall('search_memory_facts', params, sessionId);
443
+ const facts = result?.facts || result?.result?.facts || [];
444
+ return { status: 'ok', action: 'load', group: groupId, groups: groupIds, query, facts };
445
+ }
446
+ // ============================================================================
447
+ // Zep Cloud Mode Functions (no MCP/Docker required)
448
+ // ============================================================================
449
+ async function addMemoryZep() {
450
+ if (!payload)
451
+ throw new Error('add requires text payload');
452
+ if (!zepApiKey)
453
+ throw new Error('ZEP_API_KEY required for zep-cloud mode');
454
+ const resolvedTag = resolveTag(payload);
455
+ // Include tag in the text for Zep (Zep extracts facts from message content)
456
+ const textWithTag = resolvedTag ? `[${resolvedTag}] ${payload}` : payload;
457
+ const result = await zepAddMemory(zepApiKey, textWithTag, groupId, source);
458
+ return {
459
+ status: 'ok',
460
+ action: 'add',
461
+ group: groupId,
462
+ text: payload,
463
+ tag: resolvedTag,
464
+ message_uuid: result.message_uuid,
465
+ mode: 'zep-cloud',
466
+ };
467
+ }
468
+ async function loadMemoryZep() {
469
+ if (!zepApiKey)
470
+ throw new Error('ZEP_API_KEY required for zep-cloud mode');
471
+ // Use configured group (CLI or env) if available, otherwise use hierarchical groups
472
+ const groupIds = hasConfiguredGroup ? [groupId] : getHierarchicalGroupIds();
473
+ const result = await zepSearchFacts(zepApiKey, query || '*', groupIds, limit);
474
+ return {
475
+ status: 'ok',
476
+ action: 'load',
477
+ group: groupId,
478
+ groups: groupIds,
479
+ query,
480
+ facts: result.facts,
481
+ mode: 'zep-cloud',
482
+ };
483
+ }
484
+ function writeCache(obj) {
485
+ try {
486
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...obj });
487
+ fs.appendFileSync(cacheFile, `${line}\n`, 'utf8');
488
+ }
489
+ catch (err) {
490
+ // cache failures should not crash command
491
+ }
492
+ }
493
+ function readCacheFallback() {
494
+ try {
495
+ const data = fs.readFileSync(cacheFile, 'utf8').trim().split('\n').filter(Boolean);
496
+ if (!data.length)
497
+ return null;
498
+ return data.slice(-1).map((l) => JSON.parse(l))[0];
499
+ }
500
+ catch (err) {
501
+ return null;
502
+ }
503
+ }
504
+ async function main() {
505
+ try {
506
+ if (!['add', 'load'].includes(command))
507
+ throw new Error('command must be add|load');
508
+ let out;
509
+ if (isZepCloud) {
510
+ // Zep Cloud mode: use native REST API (no Docker/MCP required)
511
+ out = command === 'add' ? await addMemoryZep() : await loadMemoryZep();
512
+ }
513
+ else {
514
+ // MCP mode: local (requires Docker MCP server)
515
+ const sid = await initialize();
516
+ out = command === 'add' ? await addMemory(sid) : await loadMemory(sid);
517
+ }
518
+ if (useCache)
519
+ writeCache(out);
520
+ console.log(JSON.stringify(out, null, 2));
521
+ }
522
+ catch (err) {
523
+ const message = err instanceof Error ? err.message : String(err);
524
+ const fallback = useCache ? readCacheFallback() : null;
525
+ if (fallback) {
526
+ console.log(JSON.stringify({ status: 'fallback', error: message, fallback }, null, 2));
527
+ return;
528
+ }
529
+ console.error(message);
530
+ process.exit(1);
531
+ }
532
+ }
533
+ main();
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: prompt
3
+ description: "Capture each prompt into Graphiti memory (episodes) for this repo/user."
4
+ ---
5
+
6
+ ## Purpose
7
+ Store every prompt (and optional role/kind) as a memory episode in Graphiti MCP, so agents can recall recent context.
8
+
9
+ ## Triggers
10
+ - "load prompt", "capture prompt", or any per-prompt hook integration
11
+ - Use when you want every prompt stored automatically.
12
+
13
+ ## Usage
14
+ - `node scripts/prompt.js add --text "..." [--role user|assistant] [--kind Direction|Decision|Requirement|Observation] [--force]`
15
+ - By default uses `GRAPHITI_ENDPOINT` and `GRAPHITI_GROUP_ID` from `.agents/.env`. Falls back to `http://localhost:8010/mcp/` and `lisa`.
16
+
17
+ ## Notes
18
+ - Adds a fingerprint tag to avoid duplicate prompts unless `--force` is passed.
19
+ - Output: JSON with status/action.