@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,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const { ZEP_API_KEY, DEFAULT_GROUP_ID } = require('../../config');
4
+ const ZEP_BASE_URL = 'https://api.getzep.com/api/v2';
5
+ class ZepClientError extends Error {
6
+ constructor(message, status) {
7
+ super(message);
8
+ this.name = 'ZepClientError';
9
+ this.status = status;
10
+ }
11
+ }
12
+ function getHeaders() {
13
+ if (!ZEP_API_KEY) {
14
+ throw new ZepClientError('ZEP_API_KEY is required for Zep Cloud');
15
+ }
16
+ return {
17
+ 'Content-Type': 'application/json',
18
+ Authorization: `Api-Key ${ZEP_API_KEY}`,
19
+ };
20
+ }
21
+ async function zepFetch(path, options = {}, timeoutMs = 15000) {
22
+ const url = `${ZEP_BASE_URL}${path}`;
23
+ const resp = await fetch(url, {
24
+ ...options,
25
+ headers: {
26
+ ...getHeaders(),
27
+ ...(options.headers || {}),
28
+ },
29
+ signal: AbortSignal.timeout(timeoutMs),
30
+ });
31
+ const text = await resp.text();
32
+ let data;
33
+ try {
34
+ data = text ? JSON.parse(text) : {};
35
+ }
36
+ catch (_err) {
37
+ throw new ZepClientError(`Invalid JSON from Zep (status ${resp.status}): ${text.slice(0, 200)}`, resp.status);
38
+ }
39
+ if (!resp.ok) {
40
+ const errorMsg = data.error?.message || data.error?.detail || `HTTP ${resp.status}`;
41
+ throw new ZepClientError(errorMsg, resp.status);
42
+ }
43
+ return data || data.data;
44
+ }
45
+ /**
46
+ * Add data to a user's knowledge graph
47
+ * Supports text, JSON, or message types
48
+ */
49
+ async function addData(params) {
50
+ const body = {
51
+ user_id: params.user_id,
52
+ graph_id: params.graph_id,
53
+ type: params.type,
54
+ data: params.data,
55
+ source: params.source || 'lisa-memory',
56
+ };
57
+ return zepFetch('/graph/add', {
58
+ method: 'POST',
59
+ body: JSON.stringify(body),
60
+ });
61
+ }
62
+ /**
63
+ * Search the knowledge graph for facts, nodes, or episodes
64
+ */
65
+ async function search(params) {
66
+ const body = {
67
+ query: params.query,
68
+ user_id: params.user_id,
69
+ graph_id: params.graph_id,
70
+ limit: params.limit || 10,
71
+ search_scope: params.search_scope || 'facts',
72
+ };
73
+ return zepFetch('/graph/search', {
74
+ method: 'POST',
75
+ body: JSON.stringify(body),
76
+ });
77
+ }
78
+ /**
79
+ * Ensure a user exists in Zep (creates if not present)
80
+ */
81
+ async function ensureUser(userId, metadata) {
82
+ try {
83
+ // Try to get the user first
84
+ await zepFetch(`/users/${encodeURIComponent(userId)}`, { method: 'GET' });
85
+ }
86
+ catch (err) {
87
+ // User doesn't exist, create them
88
+ if (err instanceof ZepClientError && err.status === 404) {
89
+ await zepFetch('/users', {
90
+ method: 'POST',
91
+ body: JSON.stringify({
92
+ user_id: userId,
93
+ metadata: metadata || {},
94
+ }),
95
+ });
96
+ }
97
+ else {
98
+ throw err;
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Get user's nodes from the knowledge graph
104
+ */
105
+ async function getUserNodes(userId, limit = 50) {
106
+ const result = await zepFetch(`/graph/node/user/${encodeURIComponent(userId)}`, {
107
+ method: 'POST',
108
+ body: JSON.stringify({ limit }),
109
+ });
110
+ return result.nodes || [];
111
+ }
112
+ /**
113
+ * Add memory (convenience wrapper matching MCP interface)
114
+ * @param text The text content to store
115
+ * @param options Additional options
116
+ */
117
+ async function addMemory(text, options = {}) {
118
+ const groupId = options.groupId || DEFAULT_GROUP_ID;
119
+ // Use graph_id for group-based storage (not user-specific)
120
+ const result = await addData({
121
+ graph_id: groupId,
122
+ type: 'text',
123
+ data: text,
124
+ source: options.source || 'lisa-memory',
125
+ });
126
+ return {
127
+ status: 'ok',
128
+ episode_uuid: result.episode_uuid,
129
+ };
130
+ }
131
+ /**
132
+ * Search memory facts (convenience wrapper matching MCP interface)
133
+ * @param query Search query
134
+ * @param options Additional options
135
+ */
136
+ async function searchMemoryFacts(query, options = {}) {
137
+ const groupId = options.groupId || DEFAULT_GROUP_ID;
138
+ const result = await search({
139
+ query,
140
+ graph_id: groupId,
141
+ limit: options.limit || 10,
142
+ search_scope: 'facts',
143
+ });
144
+ return {
145
+ facts: result.facts || [],
146
+ };
147
+ }
148
+ /**
149
+ * Check if Zep Cloud is configured and reachable
150
+ */
151
+ async function healthCheck() {
152
+ try {
153
+ if (!ZEP_API_KEY) {
154
+ return { status: 'error', message: 'ZEP_API_KEY not configured' };
155
+ }
156
+ // Try to list users as a health check
157
+ await zepFetch('/users', { method: 'GET' });
158
+ return { status: 'ok', message: 'Zep Cloud connected' };
159
+ }
160
+ catch (err) {
161
+ const message = err instanceof Error ? err.message : String(err);
162
+ return { status: 'error', message };
163
+ }
164
+ }
165
+ module.exports = {
166
+ addMemory,
167
+ searchMemoryFacts,
168
+ addData,
169
+ search,
170
+ ensureUser,
171
+ getUserNodes,
172
+ healthCheck,
173
+ ZepClientError,
174
+ ZEP_BASE_URL,
175
+ };
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * Claude Code - Session Start Hook
6
+ *
7
+ * Loads memory context from Graphiti MCP at the start of a new Claude session.
8
+ * This ensures Claude has access to prior work, tasks, and project context.
9
+ *
10
+ * Configuration: .claude/settings.json -> hooks.SessionStart
11
+ */
12
+ const { rpcCall, withGroup, getCurrentGroupId, getHierarchicalGroupIds } = require('./common/mcp-client');
13
+ const { detectRepo, detectBranch, repoTags, getUserName, getProjectAliases } = require('./common/context');
14
+ const { detectFolderMetadata, formatFolderMetadata } = require('./common/group-id');
15
+ // Configuration for recent memories display
16
+ const RECENT_HOURS = 24;
17
+ const MAX_RECENT_MEMORIES = 5;
18
+ // Low-level relationship types to exclude (system noise, not meaningful work)
19
+ const EXCLUDED_RELATIONSHIPS = new Set([
20
+ // System/debug noise
21
+ 'USER_SUBMITS_DIRECTION', 'DIRECTION_IS_TOPIC',
22
+ 'EXPANDED_ENTITY_TYPES_TRACKED',
23
+ // Overly granular
24
+ 'TESTS', 'ASSESSES',
25
+ ]);
26
+ function getTaskId(tags = []) {
27
+ const t = tags.find((x) => x.startsWith('task_id:'));
28
+ return t ? t.replace('task_id:', '') : null;
29
+ }
30
+ function getTaskNum(tags = []) {
31
+ const t = tags.find((x) => x.startsWith('task_num:'));
32
+ return t ? t.replace('task_num:', '') : null;
33
+ }
34
+ function getTaskStatus(tags = []) {
35
+ const t = tags.find((x) => x.startsWith('status:'));
36
+ return t ? t.replace('status:', '').toLowerCase() : 'unknown';
37
+ }
38
+ function pickLatest(a = {}, b = {}) {
39
+ const ad = a.created_at ? new Date(a.created_at).getTime() : 0;
40
+ const bd = b.created_at ? new Date(b.created_at).getTime() : 0;
41
+ return bd > ad ? b : a;
42
+ }
43
+ /**
44
+ * Filter memories to meaningful ones from the last N hours (excludes noise)
45
+ */
46
+ function filterRecentMemories(memories, hoursAgo = RECENT_HOURS) {
47
+ const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
48
+ return memories.filter((m) => {
49
+ // Must have timestamp
50
+ if (!m.created_at)
51
+ return false;
52
+ const created = new Date(m.created_at);
53
+ if (created < cutoff)
54
+ return false;
55
+ // Exclude known noise relationship types
56
+ if (m.name && EXCLUDED_RELATIONSHIPS.has(m.name)) {
57
+ return false;
58
+ }
59
+ return true;
60
+ });
61
+ }
62
+ /**
63
+ * Format a date relative to now (today, yesterday, or date)
64
+ */
65
+ function formatRelativeDate(date) {
66
+ const now = new Date();
67
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
68
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
69
+ const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
70
+ const time = date.toLocaleTimeString('en-US', {
71
+ hour: '2-digit',
72
+ minute: '2-digit',
73
+ hour12: false,
74
+ });
75
+ if (dateOnly.getTime() === today.getTime()) {
76
+ return `Today ${time}`;
77
+ }
78
+ else if (dateOnly.getTime() === yesterday.getTime()) {
79
+ return `Yesterday ${time}`;
80
+ }
81
+ else {
82
+ const month = date.toLocaleString('en-US', { month: 'short' });
83
+ const day = date.getDate();
84
+ return `${month} ${day} ${time}`;
85
+ }
86
+ }
87
+ // Time window for grouping memories (in minutes)
88
+ const GROUP_WINDOW_MINUTES = 5;
89
+ /**
90
+ * Extract common theme from a group of memory facts
91
+ */
92
+ function extractGroupSummary(memories) {
93
+ if (memories.length === 1) {
94
+ return memories[0].fact || memories[0].name || '<unknown>';
95
+ }
96
+ // Get all fact texts
97
+ const facts = memories.map((m) => m.fact || m.name || '').filter(Boolean);
98
+ if (!facts.length)
99
+ return `${memories.length} items`;
100
+ // Find common prefix/theme by looking for repeated phrases
101
+ const words = facts[0].split(/\s+/);
102
+ let commonPrefix = '';
103
+ // Find longest common prefix of words
104
+ for (let i = 0; i < Math.min(words.length, 8); i++) {
105
+ const prefix = words.slice(0, i + 1).join(' ');
106
+ const allMatch = facts.every((f) => f.startsWith(prefix));
107
+ if (allMatch) {
108
+ commonPrefix = prefix;
109
+ }
110
+ else {
111
+ break;
112
+ }
113
+ }
114
+ // Clean up the prefix (remove trailing articles, prepositions)
115
+ commonPrefix = commonPrefix.replace(/\s+(the|a|an|is|are|was|were|includes?|has|have|with|for|to|of|in|on|at)$/i, '');
116
+ if (commonPrefix.length > 15) {
117
+ return `${commonPrefix} (${memories.length} items)`;
118
+ }
119
+ // Fallback: use first fact truncated
120
+ const firstFact = facts[0];
121
+ if (firstFact.length > 60) {
122
+ return `${firstFact.slice(0, 57)}... (+${memories.length - 1} more)`;
123
+ }
124
+ return `${firstFact} (+${memories.length - 1} more)`;
125
+ }
126
+ /**
127
+ * Group memories by time window and create summaries
128
+ */
129
+ function groupMemoriesByTime(memories, windowMinutes = GROUP_WINDOW_MINUTES) {
130
+ if (!memories.length)
131
+ return [];
132
+ // Sort by created_at descending (most recent first)
133
+ const sorted = [...memories].sort((a, b) => {
134
+ const ad = a.created_at ? new Date(a.created_at).getTime() : 0;
135
+ const bd = b.created_at ? new Date(b.created_at).getTime() : 0;
136
+ return bd - ad;
137
+ });
138
+ const groups = [];
139
+ let currentGroup = [];
140
+ let groupStartTime = null;
141
+ for (const memory of sorted) {
142
+ const memTime = memory.created_at ? new Date(memory.created_at).getTime() : 0;
143
+ if (groupStartTime === null) {
144
+ // Start new group
145
+ groupStartTime = memTime;
146
+ currentGroup = [memory];
147
+ }
148
+ else if (groupStartTime - memTime <= windowMinutes * 60 * 1000) {
149
+ // Within window, add to current group
150
+ currentGroup.push(memory);
151
+ }
152
+ else {
153
+ // Outside window, save current group and start new one
154
+ groups.push({
155
+ timestamp: new Date(groupStartTime),
156
+ memories: currentGroup,
157
+ summary: extractGroupSummary(currentGroup),
158
+ });
159
+ groupStartTime = memTime;
160
+ currentGroup = [memory];
161
+ }
162
+ }
163
+ // Don't forget the last group
164
+ if (currentGroup.length && groupStartTime !== null) {
165
+ groups.push({
166
+ timestamp: new Date(groupStartTime),
167
+ memories: currentGroup,
168
+ summary: extractGroupSummary(currentGroup),
169
+ });
170
+ }
171
+ return groups;
172
+ }
173
+ /**
174
+ * Format memory groups as summary lines with relative dates
175
+ * Grouped by time, limited to max groups
176
+ */
177
+ function formatMemorySummary(memories, limit = MAX_RECENT_MEMORIES) {
178
+ const groups = groupMemoriesByTime(memories);
179
+ // Take top N groups
180
+ const topGroups = groups.slice(0, limit);
181
+ // Format each group
182
+ return topGroups.map((group) => {
183
+ const dateStr = formatRelativeDate(group.timestamp);
184
+ return ` ${dateStr} - ${group.summary}`;
185
+ });
186
+ }
187
+ async function main() {
188
+ const repo = detectRepo();
189
+ const branch = detectBranch();
190
+ const aliases = getProjectAliases(); // Get all aliases including folder name
191
+ const user = getUserName();
192
+ // Folder-based group hierarchy
193
+ const currentGroupId = getCurrentGroupId();
194
+ const hierarchicalGroups = getHierarchicalGroupIds();
195
+ const folderMetadata = detectFolderMetadata();
196
+ const folderType = formatFolderMetadata(folderMetadata);
197
+ const cwd = process.cwd();
198
+ let sessionId = null;
199
+ let facts = [];
200
+ const nodes = [];
201
+ let initReview = null;
202
+ // Load init-review memory first (codebase summary)
203
+ try {
204
+ const initParams = {
205
+ query: 'init-review',
206
+ max_facts: 1,
207
+ order: 'desc',
208
+ group_ids: hierarchicalGroups,
209
+ tags: ['type:init-review'],
210
+ };
211
+ const [initResp, sid] = await rpcCall('search_memory_facts', initParams, sessionId);
212
+ sessionId = sid;
213
+ const initFacts = initResp?.result?.facts || initResp?.facts || [];
214
+ if (initFacts.length > 0) {
215
+ // Get the most recent init-review
216
+ const fact = initFacts[0];
217
+ initReview = fact.fact || fact.name || null;
218
+ }
219
+ }
220
+ catch (_err) {
221
+ // Silently continue if init-review load fails
222
+ }
223
+ // Load recent facts/nodes from memory using hierarchical groups
224
+ try {
225
+ const allFacts = [];
226
+ const seenUuids = new Set();
227
+ // Query with hierarchical groups (current folder + all parents)
228
+ const recentParams = { query: '*', max_facts: 100, order: 'desc', group_ids: hierarchicalGroups };
229
+ const [recentResp, sid] = await rpcCall('search_memory_facts', recentParams, sessionId);
230
+ sessionId = sid;
231
+ const recentFacts = recentResp?.result?.facts || recentResp?.facts || [];
232
+ for (const fact of recentFacts) {
233
+ const uuid = fact.uuid || `${fact.name}-${fact.fact}`;
234
+ if (!seenUuids.has(uuid)) {
235
+ seenUuids.add(uuid);
236
+ allFacts.push(fact);
237
+ }
238
+ }
239
+ // Also query by repo aliases to catch any repo-specific memories
240
+ for (const alias of aliases) {
241
+ const baseParams = { query: alias, tags: repoTags({ repo: alias, branch }) };
242
+ const factParams = { ...baseParams, max_facts: 50, order: 'desc', group_ids: hierarchicalGroups };
243
+ const [factResp] = await rpcCall('search_memory_facts', factParams, sessionId);
244
+ const aliasedFacts = factResp?.result?.facts || factResp?.facts || [];
245
+ for (const fact of aliasedFacts) {
246
+ const uuid = fact.uuid || `${fact.name}-${fact.fact}`;
247
+ if (!seenUuids.has(uuid)) {
248
+ seenUuids.add(uuid);
249
+ allFacts.push(fact);
250
+ }
251
+ }
252
+ }
253
+ facts = allFacts;
254
+ // Fall back to nodes if no facts found
255
+ if (!facts.length) {
256
+ for (const alias of aliases) {
257
+ const baseParams = { query: alias, tags: repoTags({ repo: alias, branch }) };
258
+ const nodeParams = { ...baseParams, max_nodes: 20, group_ids: hierarchicalGroups };
259
+ const [nodeResp] = await rpcCall('search_nodes', nodeParams, sessionId);
260
+ const aliasedNodes = nodeResp?.result?.nodes || nodeResp?.nodes || [];
261
+ for (const node of aliasedNodes) {
262
+ const uuid = node.uuid || `${node.name}-${node.fact}`;
263
+ if (!seenUuids.has(uuid)) {
264
+ seenUuids.add(uuid);
265
+ nodes.push(node);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ catch (_err) {
272
+ // Silently continue if memory load fails - don't block session start
273
+ }
274
+ // Load tasks for this repo (query all aliases with hierarchical groups)
275
+ const taskNodes = [];
276
+ try {
277
+ const seenTaskUuids = new Set();
278
+ for (const alias of aliases) {
279
+ const taskParams = {
280
+ query: 'task',
281
+ tags: ['type:task', ...repoTags({ repo: alias, branch })],
282
+ max_nodes: 200,
283
+ group_ids: hierarchicalGroups,
284
+ };
285
+ const [taskResp] = await rpcCall('search_nodes', taskParams, sessionId);
286
+ const aliasedTasks = taskResp?.result?.nodes || taskResp?.nodes || [];
287
+ for (const task of aliasedTasks) {
288
+ const uuid = task.uuid || `${task.name}-${task.fact}`;
289
+ if (!seenTaskUuids.has(uuid)) {
290
+ seenTaskUuids.add(uuid);
291
+ taskNodes.push(task);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ catch (_err) {
297
+ // Silently continue if task load fails
298
+ }
299
+ // Process items
300
+ const repoLabel = `${repo}${branch ? ' (' + branch + ')' : ''}`;
301
+ const items = facts.length ? facts : nodes;
302
+ // Filter to last 24 hours and format for display
303
+ const recentItems = filterRecentMemories(items, RECENT_HOURS);
304
+ const recentFormatted = formatMemorySummary(recentItems, MAX_RECENT_MEMORIES);
305
+ // Deduplicate tasks by key (task_num or task_id)
306
+ const tasksByKey = new Map();
307
+ taskNodes.forEach((n) => {
308
+ const key = getTaskNum(n.tags) || getTaskId(n.tags);
309
+ if (!key)
310
+ return;
311
+ const existing = tasksByKey.get(key);
312
+ const latest = existing ? pickLatest(existing, n) : n;
313
+ tasksByKey.set(key, latest);
314
+ });
315
+ const tasks = Array.from(tasksByKey.entries()).map(([key, n]) => {
316
+ const status = getTaskStatus(n.tags);
317
+ const title = n.name || n.fact || n.uuid || '<untitled>';
318
+ const blocked = (n.tags || []).filter((t) => t.startsWith('blocked_by:')).map((t) => t.replace('blocked_by:', ''));
319
+ return { key, status, title, blocked, created_at: n.created_at };
320
+ });
321
+ tasks.sort((a, b) => {
322
+ const ad = a.created_at ? new Date(a.created_at).getTime() : 0;
323
+ const bd = b.created_at ? new Date(b.created_at).getTime() : 0;
324
+ return bd - ad;
325
+ });
326
+ // Count tasks by status
327
+ const counts = {
328
+ ready: 0,
329
+ 'in-progress': 0,
330
+ blocked: 0,
331
+ done: 0,
332
+ closed: 0,
333
+ unknown: 0,
334
+ };
335
+ tasks.forEach((t) => {
336
+ const key = counts[t.status] === undefined ? 'unknown' : t.status;
337
+ counts[key] += 1;
338
+ });
339
+ const active = tasks.filter((t) => t.status === 'in-progress');
340
+ const ready = tasks.filter((t) => t.status === 'ready');
341
+ // Build output - show folder context with type
342
+ const lines = [];
343
+ lines.push(`Memory loaded for session start.`);
344
+ // Show folder path with detected type (e.g., "TypeScript/React project")
345
+ const folderDisplay = cwd.replace(process.env.HOME || '', '~');
346
+ lines.push(`User: ${user} | Folder: ${folderDisplay} (${folderType})`);
347
+ lines.push(`Repo: ${repoLabel}`);
348
+ // Show init-review (codebase summary) if available
349
+ if (initReview) {
350
+ lines.push('');
351
+ lines.push('Codebase Summary:');
352
+ lines.push(` ${initReview}`);
353
+ lines.push('');
354
+ }
355
+ // Show recent memories from last 24 hours
356
+ if (recentFormatted.length) {
357
+ lines.push(`Recent memories (last ${RECENT_HOURS}h):`);
358
+ lines.push(...recentFormatted);
359
+ }
360
+ else if (items.length) {
361
+ lines.push(`Recent memories (last ${RECENT_HOURS}h): none (older memories exist)`);
362
+ }
363
+ if (tasks.length) {
364
+ const summaryParts = [];
365
+ if (counts['in-progress'])
366
+ summaryParts.push(`${counts['in-progress']} in-progress`);
367
+ if (counts.ready)
368
+ summaryParts.push(`${counts.ready} ready`);
369
+ if (counts.blocked)
370
+ summaryParts.push(`${counts.blocked} blocked`);
371
+ if (counts.done)
372
+ summaryParts.push(`${counts.done} done`);
373
+ if (counts.closed)
374
+ summaryParts.push(`${counts.closed} closed`);
375
+ lines.push(`Tasks: ${summaryParts.join(', ') || 'none active'}`);
376
+ if (active.length) {
377
+ lines.push(`Active: ${active[0].key} - ${active[0].title}`);
378
+ }
379
+ if (ready.length) {
380
+ const readyList = ready.slice(0, 2).map((t) => `${t.key} - ${t.title}`).join(' | ');
381
+ lines.push(`Ready: ${readyList}`);
382
+ }
383
+ }
384
+ else {
385
+ lines.push('Tasks: none found for this repo');
386
+ }
387
+ // Output goes to Claude as system-reminder context (stdout)
388
+ console.log(lines.join('\n'));
389
+ // Visible confirmation to user (stderr) - brief summary
390
+ const itemCount = items.length;
391
+ const taskCount = tasks.length;
392
+ const summary = itemCount || taskCount
393
+ ? `${itemCount} memories, ${taskCount} tasks`
394
+ : 'no prior context';
395
+ console.error(`[Memory loaded: ${summary}]`);
396
+ }
397
+ main().catch((err) => {
398
+ // Don't block session start on errors - just log and exit cleanly
399
+ console.log(`Memory load skipped: ${err.message}`);
400
+ process.exit(0);
401
+ });