@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.
- package/README.md +42 -0
- package/dist/cli.js +390 -0
- package/dist/lib/interfaces/IDockerClient.js +2 -0
- package/dist/lib/interfaces/IMcpClient.js +2 -0
- package/dist/lib/interfaces/IServices.js +2 -0
- package/dist/lib/interfaces/ITemplateCopier.js +2 -0
- package/dist/lib/mcp.js +35 -0
- package/dist/lib/services.js +57 -0
- package/dist/package.json +36 -0
- package/dist/templates/agents/.sample.env +12 -0
- package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
- package/dist/templates/agents/skills/common/group-id.js +193 -0
- package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
- package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
- package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
- package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
- package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
- package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
- package/dist/templates/agents/skills/memory/SKILL.md +31 -0
- package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
- package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
- package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
- package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
- package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
- package/dist/templates/claude/config.js +40 -0
- package/dist/templates/claude/hooks/README.md +158 -0
- package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
- package/dist/templates/claude/hooks/common/context.js +263 -0
- package/dist/templates/claude/hooks/common/group-id.js +188 -0
- package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
- package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
- package/dist/templates/claude/hooks/common/zep-client.js +175 -0
- package/dist/templates/claude/hooks/session-start.js +401 -0
- package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
- package/dist/templates/claude/hooks/session-stop.js +122 -0
- package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
- package/dist/templates/claude/settings.json +46 -0
- package/dist/templates/docker/.env.lisa.example +17 -0
- package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
- package/dist/templates/rules/shared/clean-architecture.md +333 -0
- package/dist/templates/rules/shared/code-quality-rules.md +469 -0
- package/dist/templates/rules/shared/git-rules.md +64 -0
- package/dist/templates/rules/shared/testing-principles.md +469 -0
- package/dist/templates/rules/typescript/coding-standards.md +751 -0
- package/dist/templates/rules/typescript/testing.md +629 -0
- package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
- package/package.json +64 -0
- 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.
|