amicus 1.0.0
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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/bin/amicus.js +382 -0
- package/electron/assets/icon.png +0 -0
- package/electron/assets/icon.svg +5 -0
- package/electron/fold.js +163 -0
- package/electron/ipc-setup.js +176 -0
- package/electron/load-failsafe.js +85 -0
- package/electron/main.js +468 -0
- package/electron/preload-setup.js +38 -0
- package/electron/preload.js +33 -0
- package/electron/setup-ui-alias-script.js +218 -0
- package/electron/setup-ui-aliases.js +85 -0
- package/electron/setup-ui-keys-script.js +115 -0
- package/electron/setup-ui-keys.js +97 -0
- package/electron/setup-ui-model.js +138 -0
- package/electron/setup-ui-styles.js +327 -0
- package/electron/setup-ui.js +465 -0
- package/electron/summary.js +118 -0
- package/electron/toolbar.js +229 -0
- package/electron/window-position.js +35 -0
- package/package.json +98 -0
- package/scripts/postinstall.js +193 -0
- package/scripts/setup-hooks.js +42 -0
- package/skill/SKILL.md +976 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
- package/skills/second-opinion/MODEL-NOTES.md +104 -0
- package/skills/second-opinion/SKILL.md +389 -0
- package/src/cli-handlers.js +188 -0
- package/src/cli.js +400 -0
- package/src/conflict.js +144 -0
- package/src/context-compression.js +102 -0
- package/src/context.js +199 -0
- package/src/drift.js +144 -0
- package/src/environment.js +157 -0
- package/src/headless.js +742 -0
- package/src/index.js +106 -0
- package/src/jsonl-parser.js +180 -0
- package/src/mcp-server.js +625 -0
- package/src/mcp-tools.js +407 -0
- package/src/opencode-client.js +615 -0
- package/src/prompt-builder.js +355 -0
- package/src/prompts/cowork-agent-prompt.js +118 -0
- package/src/session-manager.js +414 -0
- package/src/session.js +180 -0
- package/src/sidecar/context-builder.js +297 -0
- package/src/sidecar/continue.js +212 -0
- package/src/sidecar/crash-handler.js +56 -0
- package/src/sidecar/fanout-leg.js +107 -0
- package/src/sidecar/fanout-output.js +46 -0
- package/src/sidecar/fanout.js +236 -0
- package/src/sidecar/interactive.js +217 -0
- package/src/sidecar/models.js +135 -0
- package/src/sidecar/progress.js +218 -0
- package/src/sidecar/read.js +183 -0
- package/src/sidecar/resume.js +221 -0
- package/src/sidecar/session-utils.js +288 -0
- package/src/sidecar/setup-window.js +79 -0
- package/src/sidecar/setup.js +280 -0
- package/src/sidecar/start.js +251 -0
- package/src/utils/agent-mapping.js +138 -0
- package/src/utils/alias-audit.js +98 -0
- package/src/utils/alias-resolver.js +77 -0
- package/src/utils/api-key-store.js +259 -0
- package/src/utils/api-key-validation.js +97 -0
- package/src/utils/auth-json.js +109 -0
- package/src/utils/config.js +291 -0
- package/src/utils/curated-models.js +82 -0
- package/src/utils/env-compat.js +38 -0
- package/src/utils/env-loader.js +54 -0
- package/src/utils/idle-watchdog.js +225 -0
- package/src/utils/input-validators.js +127 -0
- package/src/utils/lifecycle.js +43 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/mcp-discovery.js +194 -0
- package/src/utils/mcp-validators.js +78 -0
- package/src/utils/model-catalog.js +103 -0
- package/src/utils/model-fetcher.js +179 -0
- package/src/utils/model-validator.js +207 -0
- package/src/utils/path-setup.js +41 -0
- package/src/utils/port-pid.js +39 -0
- package/src/utils/prompt-source.js +53 -0
- package/src/utils/result-schema.js +261 -0
- package/src/utils/server-setup.js +93 -0
- package/src/utils/session-abort.js +53 -0
- package/src/utils/session-lock.js +95 -0
- package/src/utils/shared-server.js +216 -0
- package/src/utils/start-helpers.js +76 -0
- package/src/utils/thinking-validators.js +92 -0
- package/src/utils/update-notifier-loader.js +18 -0
- package/src/utils/updater.js +157 -0
- package/src/utils/validators.js +300 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Module
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: Section 8.1 What Gets Persisted, Section 7.4 Metadata Tracking
|
|
5
|
+
* Manages persistence of sidecar session data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Session status constants
|
|
13
|
+
*/
|
|
14
|
+
const SESSION_STATUS = {
|
|
15
|
+
RUNNING: 'running',
|
|
16
|
+
COMPLETE: 'complete',
|
|
17
|
+
ERROR: 'error',
|
|
18
|
+
TIMEOUT: 'timeout'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Canonical session dir name — new sessions are written here. */
|
|
22
|
+
const SESSIONS_DIR = 'amicus_sessions';
|
|
23
|
+
// DEPRECATED(amicus-shim): legacy session dir read for pre-rebrand sessions.
|
|
24
|
+
// Remove in a future revision — see docs/SHIMS.md.
|
|
25
|
+
const LEGACY_SESSIONS_DIR = 'sidecar_sessions';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the canonical session directory path for a task (used for WRITES).
|
|
29
|
+
* Spec Reference: §8.1 Session directory structure
|
|
30
|
+
*
|
|
31
|
+
* @param {string} projectDir - Project directory path
|
|
32
|
+
* @param {string} taskId - Sidecar task ID
|
|
33
|
+
* @returns {string} Path to the session directory
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* getSessionDir('/path/to/project', 'abc123')
|
|
37
|
+
* // Returns: '/path/to/project/.claude/amicus_sessions/abc123'
|
|
38
|
+
*/
|
|
39
|
+
function getSessionDir(projectDir, taskId) {
|
|
40
|
+
return path.join(projectDir, '.claude', SESSIONS_DIR, taskId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve an EXISTING session dir for reads: prefer amicus, fall back to legacy.
|
|
45
|
+
* Backward-compat shim so pre-rebrand `.claude/sidecar_sessions/` stay visible.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} projectDir - Project directory path
|
|
48
|
+
* @param {string} taskId - Sidecar task ID
|
|
49
|
+
* @returns {string} Path to the resolved session directory (defaults to the new path)
|
|
50
|
+
*/
|
|
51
|
+
function resolveExistingSessionDir(projectDir, taskId) {
|
|
52
|
+
const current = getSessionDir(projectDir, taskId);
|
|
53
|
+
if (fs.existsSync(current)) { return current; }
|
|
54
|
+
const legacy = path.join(projectDir, '.claude', LEGACY_SESSIONS_DIR, taskId);
|
|
55
|
+
if (fs.existsSync(legacy)) { return legacy; }
|
|
56
|
+
return current; // default to the new path
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a new sidecar session
|
|
61
|
+
* Spec Reference: §8.1 What Gets Persisted
|
|
62
|
+
*
|
|
63
|
+
* Creates the session directory structure:
|
|
64
|
+
* .claude/amicus_sessions/<taskId>/
|
|
65
|
+
* ├── metadata.json
|
|
66
|
+
* └── conversation.jsonl
|
|
67
|
+
*
|
|
68
|
+
* @param {string} projectDir - Project directory path
|
|
69
|
+
* @param {string} taskId - Unique task identifier
|
|
70
|
+
* @param {object} metadata - Session metadata
|
|
71
|
+
* @param {string} metadata.model - Model being used (e.g., "google/gemini-2.5")
|
|
72
|
+
* @param {string} metadata.project - Project path
|
|
73
|
+
* @param {string} [metadata.briefing] - Task briefing
|
|
74
|
+
* @param {string} [metadata.mode] - Mode: 'interactive' or 'headless'
|
|
75
|
+
* @param {string} [metadata.thinking='medium'] - Thinking/reasoning intensity level
|
|
76
|
+
* @throws {Error} If session already exists
|
|
77
|
+
*/
|
|
78
|
+
function createSession(projectDir, taskId, metadata) {
|
|
79
|
+
const sessionDir = getSessionDir(projectDir, taskId);
|
|
80
|
+
|
|
81
|
+
// Check if session already exists
|
|
82
|
+
if (fs.existsSync(sessionDir)) {
|
|
83
|
+
throw new Error(`Session ${taskId} already exists`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create session directory
|
|
87
|
+
fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
88
|
+
|
|
89
|
+
// Build metadata per spec §7.4
|
|
90
|
+
const sessionMetadata = {
|
|
91
|
+
taskId,
|
|
92
|
+
model: metadata.model,
|
|
93
|
+
project: metadata.project || projectDir,
|
|
94
|
+
briefing: metadata.briefing || '',
|
|
95
|
+
mode: metadata.mode || 'interactive',
|
|
96
|
+
thinking: metadata.thinking || 'medium',
|
|
97
|
+
status: SESSION_STATUS.RUNNING,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
completedAt: null,
|
|
100
|
+
// File tracking per spec §7.4
|
|
101
|
+
filesRead: [],
|
|
102
|
+
filesWritten: [],
|
|
103
|
+
conflicts: [],
|
|
104
|
+
contextDrift: null
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Write metadata.json
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(sessionDir, 'metadata.json'),
|
|
110
|
+
JSON.stringify(sessionMetadata, null, 2),
|
|
111
|
+
{ mode: 0o600 }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Create empty conversation.jsonl
|
|
115
|
+
fs.writeFileSync(path.join(sessionDir, 'conversation.jsonl'), '', { mode: 0o600 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Update session metadata
|
|
120
|
+
*
|
|
121
|
+
* @param {string} projectDir - Project directory path
|
|
122
|
+
* @param {string} taskId - Task identifier
|
|
123
|
+
* @param {object} updates - Fields to update
|
|
124
|
+
* @throws {Error} If session not found
|
|
125
|
+
*/
|
|
126
|
+
function updateSession(projectDir, taskId, updates) {
|
|
127
|
+
const sessionDir = getSessionDir(projectDir, taskId);
|
|
128
|
+
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
129
|
+
|
|
130
|
+
if (!fs.existsSync(metaPath)) {
|
|
131
|
+
throw new Error(`Session ${taskId} not found`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Read existing metadata
|
|
135
|
+
const metadata = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
136
|
+
|
|
137
|
+
// Merge updates
|
|
138
|
+
// For array fields, we append rather than replace
|
|
139
|
+
if (updates.filesRead) {
|
|
140
|
+
metadata.filesRead = [...new Set([...metadata.filesRead, ...updates.filesRead])];
|
|
141
|
+
delete updates.filesRead;
|
|
142
|
+
}
|
|
143
|
+
if (updates.filesWritten) {
|
|
144
|
+
metadata.filesWritten = [...new Set([...metadata.filesWritten, ...updates.filesWritten])];
|
|
145
|
+
delete updates.filesWritten;
|
|
146
|
+
}
|
|
147
|
+
if (updates.conflicts) {
|
|
148
|
+
metadata.conflicts = [...metadata.conflicts, ...updates.conflicts];
|
|
149
|
+
delete updates.conflicts;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Apply remaining updates
|
|
153
|
+
Object.assign(metadata, updates);
|
|
154
|
+
|
|
155
|
+
// Write updated metadata
|
|
156
|
+
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2), { mode: 0o600 });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get session metadata
|
|
161
|
+
*
|
|
162
|
+
* @param {string} projectDir - Project directory path
|
|
163
|
+
* @param {string} taskId - Task identifier
|
|
164
|
+
* @returns {object|null} Session metadata or null if not found
|
|
165
|
+
*/
|
|
166
|
+
function getSession(projectDir, taskId) {
|
|
167
|
+
const sessionDir = getSessionDir(projectDir, taskId);
|
|
168
|
+
const metaPath = path.join(sessionDir, 'metadata.json');
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(metaPath)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Save a message to the conversation log
|
|
179
|
+
* Spec Reference: §8.2 Conversation Capture
|
|
180
|
+
*
|
|
181
|
+
* @param {string} projectDir - Project directory path
|
|
182
|
+
* @param {string} taskId - Task identifier
|
|
183
|
+
* @param {object} message - Message to save
|
|
184
|
+
* @param {string} message.role - Message role ('user', 'assistant', 'system')
|
|
185
|
+
* @param {string} message.content - Message content
|
|
186
|
+
* @param {string} [message.timestamp] - ISO timestamp (auto-generated if not provided)
|
|
187
|
+
* @throws {Error} If session not found
|
|
188
|
+
*/
|
|
189
|
+
function saveConversation(projectDir, taskId, message) {
|
|
190
|
+
const sessionDir = getSessionDir(projectDir, taskId);
|
|
191
|
+
const convPath = path.join(sessionDir, 'conversation.jsonl');
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(sessionDir)) {
|
|
194
|
+
throw new Error(`Session ${taskId} not found`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Ensure timestamp is present
|
|
198
|
+
const messageWithTimestamp = {
|
|
199
|
+
...message,
|
|
200
|
+
timestamp: message.timestamp || new Date().toISOString()
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Append to conversation.jsonl
|
|
204
|
+
fs.appendFileSync(convPath, JSON.stringify(messageWithTimestamp) + '\n', { mode: 0o600 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Save the session summary
|
|
209
|
+
* Spec Reference: §8.1 summary.md
|
|
210
|
+
*
|
|
211
|
+
* Also updates session status to complete with completedAt timestamp.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} projectDir - Project directory path
|
|
214
|
+
* @param {string} taskId - Task identifier
|
|
215
|
+
* @param {string} summary - Summary content (markdown)
|
|
216
|
+
* @throws {Error} If session not found
|
|
217
|
+
*/
|
|
218
|
+
function saveSummary(projectDir, taskId, summary) {
|
|
219
|
+
const sessionDir = getSessionDir(projectDir, taskId);
|
|
220
|
+
const summaryPath = path.join(sessionDir, 'summary.md');
|
|
221
|
+
|
|
222
|
+
if (!fs.existsSync(sessionDir)) {
|
|
223
|
+
throw new Error(`Session ${taskId} not found`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Write summary file
|
|
227
|
+
fs.writeFileSync(summaryPath, summary, { mode: 0o600 });
|
|
228
|
+
|
|
229
|
+
// Update session status
|
|
230
|
+
updateSession(projectDir, taskId, {
|
|
231
|
+
status: SESSION_STATUS.COMPLETE,
|
|
232
|
+
completedAt: new Date().toISOString()
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get the sub-agent session directory path
|
|
238
|
+
*
|
|
239
|
+
* @param {string} projectDir - Project directory path
|
|
240
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
241
|
+
* @param {string} subagentId - Sub-agent ID
|
|
242
|
+
* @returns {string} Path to the sub-agent session directory
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* getSubagentDir('/path/to/project', 'abc123', 'subagent-xyz')
|
|
246
|
+
* // Returns: '/path/to/project/.claude/amicus_sessions/abc123/subagents/subagent-xyz'
|
|
247
|
+
*/
|
|
248
|
+
function getSubagentDir(projectDir, parentTaskId, subagentId) {
|
|
249
|
+
return path.join(getSessionDir(projectDir, parentTaskId), 'subagents', subagentId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create a sub-agent session
|
|
254
|
+
*
|
|
255
|
+
* Creates the sub-agent directory structure:
|
|
256
|
+
* .claude/amicus_sessions/<parentTaskId>/subagents/<subagentId>/
|
|
257
|
+
* ├── metadata.json
|
|
258
|
+
* └── conversation.jsonl
|
|
259
|
+
*
|
|
260
|
+
* @param {string} projectDir - Project directory path
|
|
261
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
262
|
+
* @param {string} subagentId - Sub-agent ID
|
|
263
|
+
* @param {object} metadata - Sub-agent metadata
|
|
264
|
+
* @param {string} metadata.agentType - Agent type (general, explore, security, test)
|
|
265
|
+
* @param {string} metadata.briefing - Task briefing
|
|
266
|
+
* @returns {string} Path to the created sub-agent directory
|
|
267
|
+
*/
|
|
268
|
+
function createSubagentSession(projectDir, parentTaskId, subagentId, metadata) {
|
|
269
|
+
const subagentDir = getSubagentDir(projectDir, parentTaskId, subagentId);
|
|
270
|
+
|
|
271
|
+
// Create sub-agent directory
|
|
272
|
+
fs.mkdirSync(subagentDir, { recursive: true, mode: 0o700 });
|
|
273
|
+
|
|
274
|
+
// Build sub-agent metadata
|
|
275
|
+
const subagentMetadata = {
|
|
276
|
+
subagentId,
|
|
277
|
+
parentTaskId,
|
|
278
|
+
agentType: metadata.agentType,
|
|
279
|
+
briefing: metadata.briefing,
|
|
280
|
+
status: SESSION_STATUS.RUNNING,
|
|
281
|
+
createdAt: new Date().toISOString(),
|
|
282
|
+
completedAt: null
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Write metadata
|
|
286
|
+
fs.writeFileSync(
|
|
287
|
+
path.join(subagentDir, 'metadata.json'),
|
|
288
|
+
JSON.stringify(subagentMetadata, null, 2),
|
|
289
|
+
{ mode: 0o600 }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Initialize empty conversation file
|
|
293
|
+
fs.writeFileSync(path.join(subagentDir, 'conversation.jsonl'), '', { mode: 0o600 });
|
|
294
|
+
|
|
295
|
+
return subagentDir;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Update a sub-agent session
|
|
300
|
+
*
|
|
301
|
+
* @param {string} projectDir - Project directory path
|
|
302
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
303
|
+
* @param {string} subagentId - Sub-agent ID
|
|
304
|
+
* @param {object} updates - Fields to update
|
|
305
|
+
*/
|
|
306
|
+
function updateSubagentSession(projectDir, parentTaskId, subagentId, updates) {
|
|
307
|
+
const subagentDir = getSubagentDir(projectDir, parentTaskId, subagentId);
|
|
308
|
+
const metadataPath = path.join(subagentDir, 'metadata.json');
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(metadataPath)) {
|
|
311
|
+
throw new Error(`Sub-agent ${subagentId} not found`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
315
|
+
const updated = { ...metadata, ...updates };
|
|
316
|
+
fs.writeFileSync(metadataPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get a sub-agent session
|
|
321
|
+
*
|
|
322
|
+
* @param {string} projectDir - Project directory path
|
|
323
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
324
|
+
* @param {string} subagentId - Sub-agent ID
|
|
325
|
+
* @returns {object|null} Sub-agent metadata or null if not found
|
|
326
|
+
*/
|
|
327
|
+
function getSubagentSession(projectDir, parentTaskId, subagentId) {
|
|
328
|
+
const metadataPath = path.join(getSubagentDir(projectDir, parentTaskId, subagentId), 'metadata.json');
|
|
329
|
+
|
|
330
|
+
if (!fs.existsSync(metadataPath)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* List all sub-agents for a parent session
|
|
339
|
+
*
|
|
340
|
+
* @param {string} projectDir - Project directory path
|
|
341
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
342
|
+
* @param {object} [filter] - Optional filter options
|
|
343
|
+
* @param {string} [filter.status] - Filter by status
|
|
344
|
+
* @param {string} [filter.agentType] - Filter by agent type
|
|
345
|
+
* @returns {object[]} Array of sub-agent metadata
|
|
346
|
+
*/
|
|
347
|
+
function listSubagents(projectDir, parentTaskId, filter = {}) {
|
|
348
|
+
const subagentsDir = path.join(getSessionDir(projectDir, parentTaskId), 'subagents');
|
|
349
|
+
|
|
350
|
+
if (!fs.existsSync(subagentsDir)) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const subagentIds = fs.readdirSync(subagentsDir).filter(name => {
|
|
355
|
+
const stat = fs.statSync(path.join(subagentsDir, name));
|
|
356
|
+
return stat.isDirectory();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
let subagents = subagentIds.map(id => {
|
|
360
|
+
const metadata = getSubagentSession(projectDir, parentTaskId, id);
|
|
361
|
+
return metadata;
|
|
362
|
+
}).filter(Boolean);
|
|
363
|
+
|
|
364
|
+
// Apply filters
|
|
365
|
+
if (filter.status) {
|
|
366
|
+
subagents = subagents.filter(s => s.status === filter.status);
|
|
367
|
+
}
|
|
368
|
+
if (filter.agentType) {
|
|
369
|
+
subagents = subagents.filter(s => s.agentType === filter.agentType);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return subagents;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Save sub-agent summary
|
|
377
|
+
*
|
|
378
|
+
* @param {string} projectDir - Project directory path
|
|
379
|
+
* @param {string} parentTaskId - Parent sidecar task ID
|
|
380
|
+
* @param {string} subagentId - Sub-agent ID
|
|
381
|
+
* @param {string} summary - Summary content
|
|
382
|
+
*/
|
|
383
|
+
function saveSubagentSummary(projectDir, parentTaskId, subagentId, summary) {
|
|
384
|
+
const subagentDir = getSubagentDir(projectDir, parentTaskId, subagentId);
|
|
385
|
+
const summaryPath = path.join(subagentDir, 'summary.md');
|
|
386
|
+
|
|
387
|
+
fs.writeFileSync(summaryPath, summary, { mode: 0o600 });
|
|
388
|
+
|
|
389
|
+
// Update sub-agent status
|
|
390
|
+
updateSubagentSession(projectDir, parentTaskId, subagentId, {
|
|
391
|
+
status: SESSION_STATUS.COMPLETE,
|
|
392
|
+
completedAt: new Date().toISOString()
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
createSession,
|
|
398
|
+
updateSession,
|
|
399
|
+
getSession,
|
|
400
|
+
saveConversation,
|
|
401
|
+
saveSummary,
|
|
402
|
+
getSessionDir,
|
|
403
|
+
resolveExistingSessionDir,
|
|
404
|
+
SESSIONS_DIR,
|
|
405
|
+
LEGACY_SESSIONS_DIR,
|
|
406
|
+
SESSION_STATUS,
|
|
407
|
+
// Sub-agent functions
|
|
408
|
+
getSubagentDir,
|
|
409
|
+
createSubagentSession,
|
|
410
|
+
updateSubagentSession,
|
|
411
|
+
getSubagentSession,
|
|
412
|
+
listSubagents,
|
|
413
|
+
saveSubagentSummary
|
|
414
|
+
};
|
package/src/session.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Resolver
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: §5.1 Session Resolution, §5.2 Claude Code Conversation Storage
|
|
5
|
+
* Resolves Claude Code session files using primary (explicit ID) and fallback (most recent) strategies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Encode a project path for use as a directory name
|
|
14
|
+
* Spec Reference: §5.2 Path Encoding
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectPath - Absolute project path (e.g., /Users/john/myproject)
|
|
17
|
+
* @returns {string} Encoded path suitable for directory name (e.g., -Users-john-myproject)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* encodeProjectPath('/Users/john/myproject')
|
|
21
|
+
* // Returns: '-Users-john-myproject'
|
|
22
|
+
*/
|
|
23
|
+
function encodeProjectPath(projectPath) {
|
|
24
|
+
// Replace slashes, backslashes, the drive-letter colon, and underscores with
|
|
25
|
+
// dashes (matching Claude Code behavior). On Windows: C:\Users\x -> C--Users-x.
|
|
26
|
+
return projectPath.replace(/[/\\:_]/g, '-');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decode an encoded path back to original format
|
|
31
|
+
*
|
|
32
|
+
* @param {string} encodedPath - Encoded path (e.g., -Users-john-myproject)
|
|
33
|
+
* @returns {string} Decoded path with dashes converted back to slashes
|
|
34
|
+
*/
|
|
35
|
+
function decodeProjectPath(encodedPath) {
|
|
36
|
+
// Replace dashes with slashes
|
|
37
|
+
return encodedPath.replace(/-/g, '/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the session directory path for a project
|
|
42
|
+
* Spec Reference: §5.2 Claude Code Conversation Storage
|
|
43
|
+
*
|
|
44
|
+
* @param {string} projectPath - Absolute project path
|
|
45
|
+
* @param {string} [homeDir] - Optional home directory override (for testing)
|
|
46
|
+
* @returns {string} Full path to the session directory
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* getSessionDirectory('/Users/john/myproject')
|
|
50
|
+
* // Returns: '~/.claude/projects/-Users-john-myproject'
|
|
51
|
+
*/
|
|
52
|
+
function getSessionDirectory(projectPath, homeDir = os.homedir()) {
|
|
53
|
+
const encodedPath = encodeProjectPath(projectPath);
|
|
54
|
+
return path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract session ID from a filename
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filename - Session filename (e.g., abc123.jsonl or abc123)
|
|
61
|
+
* @returns {string} Session ID without extension
|
|
62
|
+
*/
|
|
63
|
+
function getSessionId(filename) {
|
|
64
|
+
return filename.replace(/\.jsonl$/, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a Claude Code session file
|
|
69
|
+
* Spec Reference: §5.1 Session Resolution (Primary + Fallback)
|
|
70
|
+
*
|
|
71
|
+
* Primary: Explicit session ID passed via --session
|
|
72
|
+
* Fallback: Most recently modified .jsonl file
|
|
73
|
+
*
|
|
74
|
+
* @param {string} projectDir - Path to the session directory
|
|
75
|
+
* @param {string} [sessionArg] - Session ID argument (explicit ID, 'current', or undefined)
|
|
76
|
+
* @returns {{ path: string|null, method: string, warning?: string }}
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Explicit session
|
|
80
|
+
* resolveSession('/path/to/sessions', 'abc123-def456')
|
|
81
|
+
* // Returns: { path: '/path/to/sessions/abc123-def456.jsonl', method: 'explicit' }
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Fallback to most recent
|
|
85
|
+
* resolveSession('/path/to/sessions', 'current')
|
|
86
|
+
* // Returns: { path: '/path/to/sessions/most-recent.jsonl', method: 'fallback' }
|
|
87
|
+
*/
|
|
88
|
+
function resolveSession(projectDir, sessionArg) {
|
|
89
|
+
// Check if directory exists
|
|
90
|
+
if (!fs.existsSync(projectDir)) {
|
|
91
|
+
return { path: null, method: 'error' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Primary: explicit session ID
|
|
95
|
+
if (sessionArg && sessionArg !== 'current' && sessionArg !== '') {
|
|
96
|
+
// Handle both full filename and just the UUID
|
|
97
|
+
const filename = sessionArg.endsWith('.jsonl') ? sessionArg : `${sessionArg}.jsonl`;
|
|
98
|
+
const sessionPath = path.join(projectDir, filename);
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(sessionPath)) {
|
|
101
|
+
return { path: sessionPath, method: 'explicit' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Explicit session not found, fall back to most recent with warning
|
|
105
|
+
const fallback = findMostRecentSession(projectDir);
|
|
106
|
+
if (fallback.path) {
|
|
107
|
+
return {
|
|
108
|
+
path: fallback.path,
|
|
109
|
+
method: 'fallback',
|
|
110
|
+
warning: `Session ${sessionArg} not found, falling back to most recent. For reliability, pass --session <id> explicitly.`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { path: null, method: 'error', warning: `Session ${sessionArg} not found and no fallback available.` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback: most recently modified .jsonl file
|
|
118
|
+
return findMostRecentSession(projectDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find the most recently modified .jsonl file in a directory
|
|
123
|
+
* Also checks for ambiguity (multiple recent sessions)
|
|
124
|
+
*
|
|
125
|
+
* @param {string} projectDir - Path to the session directory
|
|
126
|
+
* @returns {{ path: string|null, method: string, warning?: string }}
|
|
127
|
+
*/
|
|
128
|
+
function findMostRecentSession(projectDir) {
|
|
129
|
+
let files;
|
|
130
|
+
try {
|
|
131
|
+
files = fs.readdirSync(projectDir);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { path: null, method: 'error' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get all .jsonl files with their modification times
|
|
137
|
+
const sessions = files
|
|
138
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
139
|
+
.map(f => {
|
|
140
|
+
const filePath = path.join(projectDir, f);
|
|
141
|
+
try {
|
|
142
|
+
const stat = fs.statSync(filePath);
|
|
143
|
+
return {
|
|
144
|
+
name: f,
|
|
145
|
+
path: filePath,
|
|
146
|
+
mtime: stat.mtime.getTime()
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.sort((a, b) => b.mtime - a.mtime); // Sort by mtime descending
|
|
154
|
+
|
|
155
|
+
if (sessions.length === 0) {
|
|
156
|
+
return { path: null, method: 'fallback' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check for ambiguity: multiple sessions modified in last 5 minutes
|
|
160
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
161
|
+
const recentSessions = sessions.filter(s => s.mtime > fiveMinutesAgo);
|
|
162
|
+
|
|
163
|
+
if (recentSessions.length > 1) {
|
|
164
|
+
return {
|
|
165
|
+
path: sessions[0].path,
|
|
166
|
+
method: 'fallback',
|
|
167
|
+
warning: `${recentSessions.length} active sessions detected. Using most recent. For reliability, pass --session <id> explicitly.`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { path: sessions[0].path, method: 'fallback' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
encodeProjectPath,
|
|
176
|
+
decodeProjectPath,
|
|
177
|
+
getSessionDirectory,
|
|
178
|
+
getSessionId,
|
|
179
|
+
resolveSession
|
|
180
|
+
};
|