@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,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
|
+
});
|