@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,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.*