@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,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Model-neutral task helper for Graphiti MCP with cache fallback.
|
|
4
|
+
// Commands:
|
|
5
|
+
// node tasks.js list [--group <id>] [--limit N] [--cache] [--endpoint <url>]
|
|
6
|
+
// node tasks.js add "task text" [--status todo|doing|done] [--tag foo] [--group <id>] [--cache] [--endpoint <url>]
|
|
7
|
+
// Modes:
|
|
8
|
+
// local : Local Docker MCP server (default)
|
|
9
|
+
// zep-cloud : Zep Cloud native REST API (no Docker required)
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Group ID Utilities (inline to avoid import complexity in deployed skills)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const MAX_GROUP_ID_LENGTH = 128;
|
|
18
|
+
function normalizePathToGroupId(absolutePath) {
|
|
19
|
+
let normalized = absolutePath
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/^\//, '')
|
|
22
|
+
.replace(/\//g, '-')
|
|
23
|
+
.replace(/\./g, '_'); // Graphiti requires alphanumeric, dashes, underscores only
|
|
24
|
+
if (normalized.length > MAX_GROUP_ID_LENGTH) {
|
|
25
|
+
normalized = normalized.slice(-MAX_GROUP_ID_LENGTH);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
function getCurrentGroupId(cwd = process.cwd()) {
|
|
30
|
+
return normalizePathToGroupId(cwd);
|
|
31
|
+
}
|
|
32
|
+
function getHierarchicalGroupIds(cwd = process.cwd()) {
|
|
33
|
+
const homeDir = os.homedir();
|
|
34
|
+
const groups = [];
|
|
35
|
+
let currentPath = path.resolve(cwd);
|
|
36
|
+
while (currentPath.length >= homeDir.length) {
|
|
37
|
+
groups.push(normalizePathToGroupId(currentPath));
|
|
38
|
+
if (currentPath === homeDir)
|
|
39
|
+
break;
|
|
40
|
+
const parentPath = path.dirname(currentPath);
|
|
41
|
+
if (parentPath === currentPath)
|
|
42
|
+
break;
|
|
43
|
+
currentPath = parentPath;
|
|
44
|
+
}
|
|
45
|
+
return groups;
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Zep Cloud Native API Client (for zep-cloud mode)
|
|
49
|
+
// ============================================================================
|
|
50
|
+
const ZEP_BASE_URL = 'https://api.getzep.com/api/v2';
|
|
51
|
+
async function zepFetch(apiKey, urlPath, options = {}, timeoutMs = 15000) {
|
|
52
|
+
const url = `${ZEP_BASE_URL}${urlPath}`;
|
|
53
|
+
const resp = await fetch(url, {
|
|
54
|
+
...options,
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
Authorization: `Api-Key ${apiKey}`,
|
|
58
|
+
...(options.headers || {}),
|
|
59
|
+
},
|
|
60
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
61
|
+
});
|
|
62
|
+
const text = await resp.text();
|
|
63
|
+
let data;
|
|
64
|
+
try {
|
|
65
|
+
data = text ? JSON.parse(text) : {};
|
|
66
|
+
}
|
|
67
|
+
catch (_err) {
|
|
68
|
+
throw new Error(`Invalid JSON from Zep (${resp.status}): ${text.slice(0, 200)}`);
|
|
69
|
+
}
|
|
70
|
+
if (!resp.ok) {
|
|
71
|
+
const errorMsg = data.message ||
|
|
72
|
+
data.error?.message ||
|
|
73
|
+
data.error?.detail ||
|
|
74
|
+
`HTTP ${resp.status}`;
|
|
75
|
+
throw new Error(errorMsg);
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
async function zepEnsureUser(apiKey, userId) {
|
|
80
|
+
try {
|
|
81
|
+
await zepFetch(apiKey, '/users', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
user_id: userId,
|
|
85
|
+
first_name: 'Lisa',
|
|
86
|
+
last_name: 'Tasks',
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!(err instanceof Error && err.message.includes('already exists'))) {
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { user_id: userId };
|
|
96
|
+
}
|
|
97
|
+
async function zepGetOrCreateThread(apiKey, threadId, userId) {
|
|
98
|
+
try {
|
|
99
|
+
await zepFetch(apiKey, '/threads', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
thread_id: threadId,
|
|
103
|
+
user_id: userId,
|
|
104
|
+
metadata: { project: threadId, type: 'tasks', created_by: 'lisa' },
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (!(err instanceof Error && err.message.includes('already exists'))) {
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { thread_id: threadId };
|
|
114
|
+
}
|
|
115
|
+
async function zepAddTask(apiKey, taskObj, groupId) {
|
|
116
|
+
const userId = `lisa-${groupId}`;
|
|
117
|
+
const threadId = `lisa-tasks-${groupId}`;
|
|
118
|
+
await zepEnsureUser(apiKey, userId);
|
|
119
|
+
await zepGetOrCreateThread(apiKey, threadId, userId);
|
|
120
|
+
// Store task as JSON in message content for later retrieval
|
|
121
|
+
const result = await zepFetch(apiKey, `/threads/${encodeURIComponent(threadId)}/messages`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
messages: [
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
role_type: 'user',
|
|
128
|
+
content: `TASK: ${JSON.stringify(taskObj)}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
return { message_uuid: result.message_uuids?.[0] };
|
|
134
|
+
}
|
|
135
|
+
async function zepGetMessages(apiKey, groupIds, limit) {
|
|
136
|
+
const allMessages = [];
|
|
137
|
+
// Fetch from all hierarchical groups
|
|
138
|
+
for (const gid of groupIds) {
|
|
139
|
+
const threadId = `lisa-tasks-${gid}`;
|
|
140
|
+
try {
|
|
141
|
+
const result = await zepFetch(apiKey, `/threads/${encodeURIComponent(threadId)}/messages?limit=${Math.ceil(limit / groupIds.length)}`);
|
|
142
|
+
allMessages.push(...(result.messages || []));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// Thread may not exist yet, continue to next
|
|
146
|
+
if (!(err instanceof Error && (err.message.includes('not found') || err.message.includes('404')))) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Sort by created_at descending
|
|
152
|
+
allMessages.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
153
|
+
return allMessages.slice(0, limit);
|
|
154
|
+
}
|
|
155
|
+
function parseTasksFromZepMessages(messages, repo, assignee) {
|
|
156
|
+
return messages
|
|
157
|
+
.map((m) => {
|
|
158
|
+
const content = m.content || '';
|
|
159
|
+
if (!content.startsWith('TASK:'))
|
|
160
|
+
return null;
|
|
161
|
+
const jsonStr = content.slice(5).trim();
|
|
162
|
+
try {
|
|
163
|
+
const obj = JSON.parse(jsonStr);
|
|
164
|
+
if (obj && obj.type === 'task') {
|
|
165
|
+
return {
|
|
166
|
+
title: obj.title,
|
|
167
|
+
status: obj.status,
|
|
168
|
+
repo: obj.repo || repo,
|
|
169
|
+
assignee: obj.assignee || assignee,
|
|
170
|
+
notes: obj.notes,
|
|
171
|
+
tag: obj.tag,
|
|
172
|
+
message_uuid: m.uuid,
|
|
173
|
+
created_at: m.created_at,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (_) {
|
|
178
|
+
// Not valid JSON, try to extract title
|
|
179
|
+
return {
|
|
180
|
+
title: jsonStr.slice(0, 120),
|
|
181
|
+
status: 'unknown',
|
|
182
|
+
repo,
|
|
183
|
+
assignee,
|
|
184
|
+
message_uuid: m.uuid,
|
|
185
|
+
created_at: m.created_at,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
})
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// End Zep Cloud Client
|
|
194
|
+
// ============================================================================
|
|
195
|
+
const args = process.argv.slice(2);
|
|
196
|
+
const env = (() => {
|
|
197
|
+
// Read from .agents/skills/.env (2 levels up from tasks/scripts/)
|
|
198
|
+
const envPath = path.join(__dirname, '..', '..', '.env');
|
|
199
|
+
const out = {};
|
|
200
|
+
try {
|
|
201
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
202
|
+
raw.split(/\r?\n/).forEach((line) => {
|
|
203
|
+
if (!line || line.startsWith('#'))
|
|
204
|
+
return;
|
|
205
|
+
const idx = line.indexOf('=');
|
|
206
|
+
if (idx === -1)
|
|
207
|
+
return;
|
|
208
|
+
const key = line.slice(0, idx).trim();
|
|
209
|
+
const val = line.slice(idx + 1).trim();
|
|
210
|
+
out[key] = val;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (_) {
|
|
214
|
+
// optional .env; ignore if missing
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
})();
|
|
218
|
+
function popFlag(name, fallback) {
|
|
219
|
+
const idx = args.indexOf(name);
|
|
220
|
+
if (idx === -1)
|
|
221
|
+
return fallback;
|
|
222
|
+
const val = args[idx + 1];
|
|
223
|
+
args.splice(idx, 2);
|
|
224
|
+
return val ?? fallback;
|
|
225
|
+
}
|
|
226
|
+
function hasFlag(name) {
|
|
227
|
+
const idx = args.indexOf(name);
|
|
228
|
+
if (idx === -1)
|
|
229
|
+
return false;
|
|
230
|
+
args.splice(idx, 1);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
const command = args.shift() ?? '';
|
|
234
|
+
const endpoint = popFlag('--endpoint', env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENDPOINT || 'http://localhost:8010/mcp/');
|
|
235
|
+
// Group ID: explicit --group > env > folder-based (current directory)
|
|
236
|
+
const explicitGroup = popFlag('--group', null);
|
|
237
|
+
const groupId = explicitGroup || env.GRAPHITI_GROUP_ID || process.env.GRAPHITI_GROUP_ID || getCurrentGroupId();
|
|
238
|
+
const limit = Number(popFlag('--limit', '20')) || 20;
|
|
239
|
+
const status = popFlag('--status', 'todo');
|
|
240
|
+
const tag = popFlag('--tag', null);
|
|
241
|
+
const repo = popFlag('--repo', path.basename(process.cwd()) || 'unknown');
|
|
242
|
+
const assignee = (popFlag('--assignee', process.env.USER || 'unknown') || 'unknown');
|
|
243
|
+
const notes = popFlag('--notes', '');
|
|
244
|
+
const useCache = hasFlag('--cache');
|
|
245
|
+
const payload = args.join(' ').trim();
|
|
246
|
+
const cacheFile = path.join(__dirname, '..', 'cache', 'tasks.log');
|
|
247
|
+
// Mode detection
|
|
248
|
+
const graphitiMode = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local';
|
|
249
|
+
const zepApiKey = env.ZEP_API_KEY || process.env.ZEP_API_KEY || '';
|
|
250
|
+
const isZepCloud = graphitiMode === 'zep-cloud';
|
|
251
|
+
async function initialize() {
|
|
252
|
+
const body = {
|
|
253
|
+
jsonrpc: '2.0',
|
|
254
|
+
id: 'init',
|
|
255
|
+
method: 'initialize',
|
|
256
|
+
params: {
|
|
257
|
+
protocolVersion: '2024-11-05',
|
|
258
|
+
capabilities: {
|
|
259
|
+
experimental: {},
|
|
260
|
+
prompts: { listChanged: false },
|
|
261
|
+
resources: { subscribe: false, listChanged: false },
|
|
262
|
+
tools: { listChanged: false },
|
|
263
|
+
},
|
|
264
|
+
clientInfo: { name: 'tasks-skill', version: '0.1.0' },
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
const resp = await fetch(endpoint, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
|
|
270
|
+
body: JSON.stringify(body),
|
|
271
|
+
});
|
|
272
|
+
if (!resp.ok)
|
|
273
|
+
throw new Error(`initialize failed: ${resp.status}`);
|
|
274
|
+
const sid = resp.headers.get('mcp-session-id');
|
|
275
|
+
if (!sid)
|
|
276
|
+
throw new Error('missing mcp-session-id');
|
|
277
|
+
return sid;
|
|
278
|
+
}
|
|
279
|
+
async function rpcCall(method, params, sessionId) {
|
|
280
|
+
const payload = method === 'initialize' || method === 'ping' || method.startsWith('tools/')
|
|
281
|
+
? { jsonrpc: '2.0', id: '1', method, params }
|
|
282
|
+
: { jsonrpc: '2.0', id: '1', method: 'tools/call', params: { name: method, arguments: params } };
|
|
283
|
+
const resp = await fetch(endpoint, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: { 'Content-Type': 'application/json', 'MCP-SESSION-ID': sessionId, Accept: 'application/json, text/event-stream' },
|
|
286
|
+
body: JSON.stringify(payload),
|
|
287
|
+
});
|
|
288
|
+
let text = await resp.text();
|
|
289
|
+
if (text.startsWith('event:')) {
|
|
290
|
+
const dataLine = text.split('\n').find((l) => l.startsWith('data:'));
|
|
291
|
+
if (dataLine)
|
|
292
|
+
text = dataLine.slice(5).trim();
|
|
293
|
+
}
|
|
294
|
+
let data;
|
|
295
|
+
try {
|
|
296
|
+
data = JSON.parse(text);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
throw new Error(`bad JSON: ${text.slice(0, 160)}`);
|
|
300
|
+
}
|
|
301
|
+
if (!resp.ok || data.error)
|
|
302
|
+
throw new Error(data?.error?.message || `HTTP ${resp.status}`);
|
|
303
|
+
return data.result?.structuredContent?.result || data.result || data;
|
|
304
|
+
}
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// MCP Mode Functions (local Docker)
|
|
307
|
+
// ============================================================================
|
|
308
|
+
async function addTask(sessionId) {
|
|
309
|
+
if (!payload)
|
|
310
|
+
throw new Error('add requires task text (title)');
|
|
311
|
+
const taskObj = { type: 'task', title: payload, status, repo, assignee, notes, tag };
|
|
312
|
+
const params = {
|
|
313
|
+
name: `TASK: ${payload.slice(0, 60)}`,
|
|
314
|
+
episode_body: JSON.stringify(taskObj),
|
|
315
|
+
source: 'json',
|
|
316
|
+
group_id: groupId,
|
|
317
|
+
tags: tag ? [tag] : undefined,
|
|
318
|
+
};
|
|
319
|
+
const result = await rpcCall('add_memory', params, sessionId);
|
|
320
|
+
return { status: 'ok', action: 'add', task: taskObj, group: groupId, result };
|
|
321
|
+
}
|
|
322
|
+
async function updateTask(sessionId) {
|
|
323
|
+
if (!payload)
|
|
324
|
+
throw new Error('update requires task text (title)');
|
|
325
|
+
const taskObj = { type: 'task', title: payload, status, repo, assignee, notes, tag, updated: true };
|
|
326
|
+
const params = {
|
|
327
|
+
name: `TASK UPDATE: ${payload.slice(0, 60)}`,
|
|
328
|
+
episode_body: JSON.stringify(taskObj),
|
|
329
|
+
source: 'json',
|
|
330
|
+
group_id: groupId,
|
|
331
|
+
tags: tag ? [tag] : undefined,
|
|
332
|
+
};
|
|
333
|
+
const result = await rpcCall('add_memory', params, sessionId);
|
|
334
|
+
return { status: 'ok', action: 'update', task: taskObj, group: groupId, result };
|
|
335
|
+
}
|
|
336
|
+
function parseTasksFromEpisodes(episodes) {
|
|
337
|
+
return episodes
|
|
338
|
+
.map((e) => {
|
|
339
|
+
const content = e.content || e.episode_body || '';
|
|
340
|
+
let obj = null;
|
|
341
|
+
try {
|
|
342
|
+
obj = JSON.parse(content);
|
|
343
|
+
}
|
|
344
|
+
catch (_) { /* ignore */ }
|
|
345
|
+
if (obj && obj.type === 'task') {
|
|
346
|
+
return {
|
|
347
|
+
title: obj.title,
|
|
348
|
+
status: obj.status,
|
|
349
|
+
repo: obj.repo,
|
|
350
|
+
assignee: obj.assignee,
|
|
351
|
+
notes: obj.notes,
|
|
352
|
+
tag: obj.tag,
|
|
353
|
+
episode_uuid: e.uuid,
|
|
354
|
+
created_at: e.created_at,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (typeof content === 'string' && content.trim().toUpperCase().startsWith('TASK')) {
|
|
358
|
+
return { title: content.slice(0, 120), status: 'unknown', repo, assignee, episode_uuid: e.uuid, created_at: e.created_at };
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
})
|
|
362
|
+
.filter(Boolean);
|
|
363
|
+
}
|
|
364
|
+
async function listTasks(sessionId) {
|
|
365
|
+
// Use hierarchical groups (current folder + parents) unless explicit group specified
|
|
366
|
+
const groupIds = explicitGroup ? [explicitGroup] : getHierarchicalGroupIds();
|
|
367
|
+
const params = { group_ids: groupIds, max_episodes: limit };
|
|
368
|
+
const result = await rpcCall('get_episodes', params, sessionId);
|
|
369
|
+
const episodes = result?.episodes || result?.result?.episodes || [];
|
|
370
|
+
const tasks = parseTasksFromEpisodes(episodes);
|
|
371
|
+
return { status: 'ok', action: 'list', group: groupId, groups: groupIds, tasks };
|
|
372
|
+
}
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Zep Cloud Mode Functions (no MCP/Docker required)
|
|
375
|
+
// ============================================================================
|
|
376
|
+
async function addTaskZep() {
|
|
377
|
+
if (!payload)
|
|
378
|
+
throw new Error('add requires task text (title)');
|
|
379
|
+
if (!zepApiKey)
|
|
380
|
+
throw new Error('ZEP_API_KEY required for zep-cloud mode');
|
|
381
|
+
const taskObj = { type: 'task', title: payload, status, repo, assignee, notes, tag };
|
|
382
|
+
const result = await zepAddTask(zepApiKey, taskObj, groupId);
|
|
383
|
+
return {
|
|
384
|
+
status: 'ok',
|
|
385
|
+
action: 'add',
|
|
386
|
+
task: taskObj,
|
|
387
|
+
group: groupId,
|
|
388
|
+
message_uuid: result.message_uuid,
|
|
389
|
+
mode: 'zep-cloud',
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function updateTaskZep() {
|
|
393
|
+
if (!payload)
|
|
394
|
+
throw new Error('update requires task text (title)');
|
|
395
|
+
if (!zepApiKey)
|
|
396
|
+
throw new Error('ZEP_API_KEY required for zep-cloud mode');
|
|
397
|
+
const taskObj = { type: 'task', title: payload, status, repo, assignee, notes, tag, updated: true };
|
|
398
|
+
const result = await zepAddTask(zepApiKey, taskObj, groupId);
|
|
399
|
+
return {
|
|
400
|
+
status: 'ok',
|
|
401
|
+
action: 'update',
|
|
402
|
+
task: taskObj,
|
|
403
|
+
group: groupId,
|
|
404
|
+
message_uuid: result.message_uuid,
|
|
405
|
+
mode: 'zep-cloud',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async function listTasksZep() {
|
|
409
|
+
if (!zepApiKey)
|
|
410
|
+
throw new Error('ZEP_API_KEY required for zep-cloud mode');
|
|
411
|
+
// Use hierarchical groups (current folder + parents) unless explicit group specified
|
|
412
|
+
const groupIds = explicitGroup ? [explicitGroup] : getHierarchicalGroupIds();
|
|
413
|
+
const messages = await zepGetMessages(zepApiKey, groupIds, limit);
|
|
414
|
+
const tasks = parseTasksFromZepMessages(messages, repo, assignee);
|
|
415
|
+
return {
|
|
416
|
+
status: 'ok',
|
|
417
|
+
action: 'list',
|
|
418
|
+
group: groupId,
|
|
419
|
+
groups: groupIds,
|
|
420
|
+
tasks,
|
|
421
|
+
mode: 'zep-cloud',
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Cache Functions
|
|
426
|
+
// ============================================================================
|
|
427
|
+
function writeCache(obj) {
|
|
428
|
+
try {
|
|
429
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...obj });
|
|
430
|
+
fs.appendFileSync(cacheFile, `${line}\n`, 'utf8');
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
// ignore cache errors
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function readCacheFallback() {
|
|
437
|
+
try {
|
|
438
|
+
const data = fs.readFileSync(cacheFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
439
|
+
if (!data.length)
|
|
440
|
+
return null;
|
|
441
|
+
return data.slice(-1).map((l) => JSON.parse(l))[0];
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Main
|
|
449
|
+
// ============================================================================
|
|
450
|
+
async function main() {
|
|
451
|
+
try {
|
|
452
|
+
if (!['add', 'list', 'update'].includes(command))
|
|
453
|
+
throw new Error('command must be add|list|update');
|
|
454
|
+
let out;
|
|
455
|
+
if (isZepCloud) {
|
|
456
|
+
// Zep Cloud mode: use native REST API (no Docker/MCP required)
|
|
457
|
+
out =
|
|
458
|
+
command === 'add'
|
|
459
|
+
? await addTaskZep()
|
|
460
|
+
: command === 'update'
|
|
461
|
+
? await updateTaskZep()
|
|
462
|
+
: await listTasksZep();
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
// MCP mode: local (requires Docker MCP server)
|
|
466
|
+
const sid = await initialize();
|
|
467
|
+
out =
|
|
468
|
+
command === 'add'
|
|
469
|
+
? await addTask(sid)
|
|
470
|
+
: command === 'update'
|
|
471
|
+
? await updateTask(sid)
|
|
472
|
+
: await listTasks(sid);
|
|
473
|
+
}
|
|
474
|
+
if (useCache)
|
|
475
|
+
writeCache(out);
|
|
476
|
+
console.log(JSON.stringify(out, null, 2));
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
480
|
+
const fallback = useCache ? readCacheFallback() : null;
|
|
481
|
+
if (fallback) {
|
|
482
|
+
console.log(JSON.stringify({ status: 'fallback', error: message, fallback }, null, 2));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
console.error(message);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
main();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
// Load .env from .agents/.env if present
|
|
6
|
+
const envPath = path.join(process.cwd(), '.agents', '.env');
|
|
7
|
+
const env = {};
|
|
8
|
+
try {
|
|
9
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
10
|
+
raw.split(/\r?\n/).forEach((line) => {
|
|
11
|
+
if (!line || line.startsWith('#'))
|
|
12
|
+
return;
|
|
13
|
+
const idx = line.indexOf('=');
|
|
14
|
+
if (idx === -1)
|
|
15
|
+
return;
|
|
16
|
+
env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (_) {
|
|
20
|
+
/* optional */
|
|
21
|
+
}
|
|
22
|
+
const PROJECT_ROOT = process.cwd();
|
|
23
|
+
const PROMPT_SKILL_PATH = path.join(PROJECT_ROOT, '.agents/skills/prompt/scripts/prompt.js');
|
|
24
|
+
const DEV_DIR = path.join(PROJECT_ROOT, '.dev');
|
|
25
|
+
const MCP_ENDPOINT = env.GRAPHITI_ENDPOINT || process.env.GRAPHITI_ENDPOINT || 'http://localhost:8010/mcp/';
|
|
26
|
+
const DEFAULT_GROUP_ID = env.GRAPHITI_GROUP_ID || process.env.GRAPHITI_GROUP_ID || 'lisa';
|
|
27
|
+
const ZEP_API_KEY = env.ZEP_API_KEY || process.env.ZEP_API_KEY || '';
|
|
28
|
+
const STORAGE_MODE = env.STORAGE_MODE || process.env.STORAGE_MODE || 'local';
|
|
29
|
+
// Helper to check if using Zep Cloud native API (not MCP)
|
|
30
|
+
const isZepCloudMode = () => STORAGE_MODE === 'zep-cloud';
|
|
31
|
+
module.exports = {
|
|
32
|
+
PROJECT_ROOT,
|
|
33
|
+
PROMPT_SKILL_PATH,
|
|
34
|
+
DEV_DIR,
|
|
35
|
+
MCP_ENDPOINT,
|
|
36
|
+
DEFAULT_GROUP_ID,
|
|
37
|
+
ZEP_API_KEY,
|
|
38
|
+
STORAGE_MODE,
|
|
39
|
+
isZepCloudMode,
|
|
40
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
p# Claude Code Hooks
|
|
2
|
+
|
|
3
|
+
This directory contains hook scripts that run automatically during Claude Code sessions.
|
|
4
|
+
|
|
5
|
+
## Available Hooks
|
|
6
|
+
|
|
7
|
+
### user-prompt-submit.js
|
|
8
|
+
**Triggers:** Before user prompt is submitted (optional)
|
|
9
|
+
**Purpose:** Validate, enhance, or log user prompts
|
|
10
|
+
**Configuration:** `.claude/settings.json` → `hooks.userPromptSubmit`
|
|
11
|
+
|
|
12
|
+
**What it does:**
|
|
13
|
+
- Validates prompts for destructive operations
|
|
14
|
+
- Suggests relevant project files to reference
|
|
15
|
+
- Logs prompts for analytics
|
|
16
|
+
- Can cancel prompt submission by exiting with non-zero status
|
|
17
|
+
|
|
18
|
+
**Note:** This hook is optional and must be explicitly configured.
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Hooks are configured in `.claude/settings.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"sessionHooks": {
|
|
27
|
+
"start": ".claude/hooks/session-start.js",
|
|
28
|
+
"end": ".claude/hooks/session-end.js"
|
|
29
|
+
},
|
|
30
|
+
"hooks": {
|
|
31
|
+
"userPromptSubmit": ".claude/hooks/user-prompt-submit.js"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Customization
|
|
37
|
+
|
|
38
|
+
### Disabling Hooks
|
|
39
|
+
|
|
40
|
+
To disable a hook, remove its entry from `settings.json` or comment it out:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"sessionHooks": {
|
|
45
|
+
"start": ".claude/hooks/session-start.js"
|
|
46
|
+
// "end": ".claude/hooks/session-end.js" // Disabled
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Modifying Hooks
|
|
52
|
+
|
|
53
|
+
You can modify these hook files to customize behavior for your project:
|
|
54
|
+
|
|
55
|
+
1. **Edit the hook file** in `.claude/hooks/`
|
|
56
|
+
2. **Test the hook** by running it directly: `node .claude/hooks/session-start.js`
|
|
57
|
+
3. **Restart Claude Code** to apply changes
|
|
58
|
+
|
|
59
|
+
### Creating Custom Hooks
|
|
60
|
+
|
|
61
|
+
You can create additional hooks for Claude Code:
|
|
62
|
+
|
|
63
|
+
1. **Create a new `.js` file** in `.claude/hooks/`
|
|
64
|
+
2. **Make it executable** (Unix): `chmod +x .claude/hooks/your-hook.js`
|
|
65
|
+
3. **Add configuration** to `.claude/settings.json`
|
|
66
|
+
4. **Test thoroughly** to ensure it doesn't block Claude Code operation
|
|
67
|
+
|
|
68
|
+
## Hook Exit Codes
|
|
69
|
+
|
|
70
|
+
- **Exit 0:** Hook succeeded, continue normal operation
|
|
71
|
+
- **Exit 1:** Hook failed, may cancel operation (depends on hook type)
|
|
72
|
+
|
|
73
|
+
## Debugging Hooks
|
|
74
|
+
|
|
75
|
+
If a hook isn't working:
|
|
76
|
+
|
|
77
|
+
1. **Run it manually:** `node .claude/hooks/session-start.js`
|
|
78
|
+
2. **Check console output** for errors
|
|
79
|
+
3. **Verify paths** in the hook script are correct
|
|
80
|
+
4. **Check permissions** (Unix/Mac: hooks should be executable)
|
|
81
|
+
5. **Review Claude Code logs** for hook execution errors
|
|
82
|
+
|
|
83
|
+
## Session State Files
|
|
84
|
+
|
|
85
|
+
Hooks create temporary state files in `.dev/`:
|
|
86
|
+
|
|
87
|
+
- `.session-state.json` - Current session state (deleted at session end)
|
|
88
|
+
- `.session-stats.json` - Cumulative session statistics
|
|
89
|
+
- `.prompt-log.jsonl` - Prompt log (if user-prompt-submit hook is enabled)
|
|
90
|
+
|
|
91
|
+
These files are typically ignored by git (see `.gitignore`).
|
|
92
|
+
|
|
93
|
+
## Best Practices
|
|
94
|
+
|
|
95
|
+
1. **Keep hooks fast** - They run on every session start/end
|
|
96
|
+
2. **Handle errors gracefully** - Don't block Claude Code if something fails
|
|
97
|
+
3. **Log important events** - Help users understand what hooks are doing
|
|
98
|
+
4. **Test thoroughly** - Broken hooks can disrupt your workflow
|
|
99
|
+
5. **Version control** - Commit hooks if they're project-specific
|
|
100
|
+
|
|
101
|
+
## Example Use Cases
|
|
102
|
+
|
|
103
|
+
### Custom Session Start
|
|
104
|
+
Load environment-specific configuration:
|
|
105
|
+
```javascript
|
|
106
|
+
// In session-start.js
|
|
107
|
+
const env = process.env.NODE_ENV || 'development';
|
|
108
|
+
console.log(`🌍 Environment: ${env}`);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Custom Session End
|
|
112
|
+
Run code quality checks:
|
|
113
|
+
```javascript
|
|
114
|
+
// In session-end.js
|
|
115
|
+
const { execSync } = require('child_process');
|
|
116
|
+
try {
|
|
117
|
+
execSync('npm run lint', { stdio: 'ignore' });
|
|
118
|
+
console.log('✅ Linting passed');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.log('⚠️ Linting failed - consider fixing before next session');
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Custom Prompt Enhancement
|
|
125
|
+
Auto-reference coding standards:
|
|
126
|
+
```javascript
|
|
127
|
+
// In user-prompt-submit.js
|
|
128
|
+
if (prompt.includes('create') || prompt.includes('implement')) {
|
|
129
|
+
console.log('💡 Remember to follow coding standards in .dev/rules/');
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Troubleshooting
|
|
134
|
+
|
|
135
|
+
### Hook doesn't run
|
|
136
|
+
- Check `.claude/settings.json` configuration
|
|
137
|
+
- Verify hook file path is correct (relative to project root)
|
|
138
|
+
- Ensure hook file exists and is readable
|
|
139
|
+
|
|
140
|
+
### Hook runs but fails
|
|
141
|
+
- Run manually to see error messages
|
|
142
|
+
- Check if required dependencies are installed
|
|
143
|
+
- Verify file paths used in the hook
|
|
144
|
+
|
|
145
|
+
### Hook blocks Claude Code
|
|
146
|
+
- Check if hook exits with non-zero status
|
|
147
|
+
- Look for infinite loops or long-running operations
|
|
148
|
+
- Add timeout handling in your custom hooks
|
|
149
|
+
|
|
150
|
+
## Resources
|
|
151
|
+
|
|
152
|
+
- [Claude Code Documentation](https://docs.anthropic.com/claude-code)
|
|
153
|
+
- [AI Dotfiles Manager](https://github.com/TonyCasey/ai-dotfiles-manager)
|
|
154
|
+
- [Session Hooks Guide](../.dev/hooks/)
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
*These hooks are installed by ai-dotfiles-manager and can be customized for your project.*
|