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
package/src/context.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Filtering Module
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: Section 5.3 Context Filtering Algorithm
|
|
5
|
+
* Extracts and filters context from Claude Code sessions for sidecar use.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { readJSONL, formatContext } = require('./jsonl-parser');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a duration string into milliseconds
|
|
12
|
+
* Spec Reference: §4.1 --context-since option
|
|
13
|
+
*
|
|
14
|
+
* @param {string} durationStr - Duration string (e.g., "30m", "2h", "1d")
|
|
15
|
+
* @returns {number} Duration in milliseconds, or 0 if invalid
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* parseDuration('30m') // Returns: 1800000 (30 * 60 * 1000)
|
|
19
|
+
* parseDuration('2h') // Returns: 7200000 (2 * 60 * 60 * 1000)
|
|
20
|
+
* parseDuration('1d') // Returns: 86400000 (24 * 60 * 60 * 1000)
|
|
21
|
+
*/
|
|
22
|
+
function parseDuration(durationStr) {
|
|
23
|
+
if (!durationStr || typeof durationStr !== 'string') {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const match = durationStr.match(/^(\d+)([mhd])$/);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = parseInt(match[1], 10);
|
|
33
|
+
const unit = match[2];
|
|
34
|
+
|
|
35
|
+
const multipliers = {
|
|
36
|
+
m: 60 * 1000, // minutes to ms
|
|
37
|
+
h: 60 * 60 * 1000, // hours to ms
|
|
38
|
+
d: 24 * 60 * 60 * 1000 // days to ms
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return value * multipliers[unit];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Estimate token count from text
|
|
46
|
+
* Spec Reference: §5.3 "~4 chars per token"
|
|
47
|
+
*
|
|
48
|
+
* @param {string} text - Text to estimate tokens for
|
|
49
|
+
* @returns {number} Estimated token count
|
|
50
|
+
*/
|
|
51
|
+
function estimateTokens(text) {
|
|
52
|
+
if (!text || typeof text !== 'string') {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
// Spec specifies ~4 chars per token
|
|
56
|
+
return Math.floor(text.length / 4);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract the last N user turns from messages
|
|
61
|
+
* A "turn" is defined as a user message plus all subsequent messages until the next user message.
|
|
62
|
+
*
|
|
63
|
+
* @param {object[]} messages - Array of message objects
|
|
64
|
+
* @param {number} n - Number of turns to keep
|
|
65
|
+
* @returns {object[]} Messages from the last N turns
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Given messages: [user1, assistant1, user2, assistant2, user3, assistant3]
|
|
69
|
+
* // takeLastNTurns(messages, 2) returns: [user2, assistant2, user3, assistant3]
|
|
70
|
+
*/
|
|
71
|
+
function takeLastNTurns(messages, n) {
|
|
72
|
+
if (!messages || messages.length === 0 || n <= 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find indices of all user messages (turns)
|
|
77
|
+
const userIndices = [];
|
|
78
|
+
for (let i = 0; i < messages.length; i++) {
|
|
79
|
+
if (messages[i].type === 'user') {
|
|
80
|
+
userIndices.push(i);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// If we have fewer turns than requested, return all messages
|
|
85
|
+
if (userIndices.length <= n) {
|
|
86
|
+
return messages;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get the starting index for the last N turns
|
|
90
|
+
const startTurnIndex = userIndices.length - n;
|
|
91
|
+
const startIdx = userIndices[startTurnIndex];
|
|
92
|
+
|
|
93
|
+
// Return all messages from that point
|
|
94
|
+
return messages.slice(startIdx);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Filter messages by time
|
|
99
|
+
*
|
|
100
|
+
* @param {object[]} messages - Array of message objects
|
|
101
|
+
* @param {number} sinceMs - Duration in milliseconds
|
|
102
|
+
* @returns {object[]} Messages within the time window
|
|
103
|
+
*/
|
|
104
|
+
function filterByTime(messages, sinceMs) {
|
|
105
|
+
const cutoff = Date.now() - sinceMs;
|
|
106
|
+
|
|
107
|
+
return messages.filter(msg => {
|
|
108
|
+
if (!msg.timestamp) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
112
|
+
return msgTime >= cutoff;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Truncate context from start to fit within token limit
|
|
118
|
+
* Spec Reference: §5.3 "Truncate from start if over limit, prepend [Earlier context truncated...]"
|
|
119
|
+
*
|
|
120
|
+
* @param {string} context - Formatted context string
|
|
121
|
+
* @param {number} maxTokens - Maximum token limit
|
|
122
|
+
* @returns {string} Truncated context with notice if needed
|
|
123
|
+
*/
|
|
124
|
+
function truncateToTokenLimit(context, maxTokens) {
|
|
125
|
+
const maxChars = maxTokens * 4; // ~4 chars per token
|
|
126
|
+
|
|
127
|
+
if (context.length <= maxChars) {
|
|
128
|
+
return context;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Truncate from start, keeping the end (most recent)
|
|
132
|
+
const truncated = context.slice(-maxChars);
|
|
133
|
+
|
|
134
|
+
// Prepend truncation notice
|
|
135
|
+
return '[Earlier context truncated...]\n\n' + truncated;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Filter context from a Claude Code session
|
|
140
|
+
* Spec Reference: §5.3 Context Filtering Algorithm
|
|
141
|
+
*
|
|
142
|
+
* @param {string} sessionPath - Path to the session JSONL file
|
|
143
|
+
* @param {object} options - Filtering options
|
|
144
|
+
* @param {number} [options.turns=50] - Max conversation turns to include
|
|
145
|
+
* @param {string} [options.since] - Time filter (e.g., "2h"). Overrides turns if specified.
|
|
146
|
+
* @param {number} [options.maxTokens=80000] - Hard cap on context tokens
|
|
147
|
+
* @returns {string} Filtered and formatted context string
|
|
148
|
+
*
|
|
149
|
+
* @throws {Error} If session file cannot be read
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // Filter last 50 turns, max 80000 tokens
|
|
153
|
+
* filterContext('/path/to/session.jsonl', { turns: 50, maxTokens: 80000 })
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* // Filter by time (last 2 hours), overrides turns
|
|
157
|
+
* filterContext('/path/to/session.jsonl', { since: '2h', turns: 50, maxTokens: 80000 })
|
|
158
|
+
*/
|
|
159
|
+
function filterContext(sessionPath, options = {}) {
|
|
160
|
+
const {
|
|
161
|
+
turns = 50,
|
|
162
|
+
since,
|
|
163
|
+
maxTokens = 80000
|
|
164
|
+
} = options;
|
|
165
|
+
|
|
166
|
+
// Read and parse the session file
|
|
167
|
+
let messages = readJSONL(sessionPath);
|
|
168
|
+
|
|
169
|
+
if (messages.length === 0) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Apply filtering: time-based filter takes precedence over turns
|
|
174
|
+
if (since) {
|
|
175
|
+
const sinceMs = parseDuration(since);
|
|
176
|
+
if (sinceMs > 0) {
|
|
177
|
+
messages = filterByTime(messages, sinceMs);
|
|
178
|
+
}
|
|
179
|
+
} else if (turns) {
|
|
180
|
+
messages = takeLastNTurns(messages, turns);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Format messages into context string
|
|
184
|
+
let context = formatContext(messages);
|
|
185
|
+
|
|
186
|
+
// Truncate to token limit if needed
|
|
187
|
+
if (estimateTokens(context) > maxTokens) {
|
|
188
|
+
context = truncateToTokenLimit(context, maxTokens);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return context;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
filterContext,
|
|
196
|
+
parseDuration,
|
|
197
|
+
estimateTokens,
|
|
198
|
+
takeLastNTurns
|
|
199
|
+
};
|
package/src/drift.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Drift Detection Module
|
|
3
|
+
*
|
|
4
|
+
* Spec Reference: Section 7.3 Context Drift Indicator
|
|
5
|
+
* Detects and reports context staleness in sidecar sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { readJSONL } = require('./jsonl-parser');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Count user turns in a session since a given time
|
|
13
|
+
* A "turn" is defined as a user message.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} sessionPath - Path to the session JSONL file
|
|
16
|
+
* @param {Date|string} sinceTime - Count turns after this time
|
|
17
|
+
* @returns {number} Number of user turns since the specified time
|
|
18
|
+
*/
|
|
19
|
+
function countTurnsSince(sessionPath, sinceTime) {
|
|
20
|
+
// Handle non-existent file
|
|
21
|
+
if (!fs.existsSync(sessionPath)) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Normalize time to Date
|
|
26
|
+
const since = sinceTime instanceof Date ? sinceTime : new Date(sinceTime);
|
|
27
|
+
|
|
28
|
+
let messages;
|
|
29
|
+
try {
|
|
30
|
+
messages = readJSONL(sessionPath);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Count user messages after sinceTime
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const msg of messages) {
|
|
38
|
+
if (msg.type !== 'user') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Skip messages without timestamps
|
|
43
|
+
if (!msg.timestamp) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const msgTime = new Date(msg.timestamp);
|
|
48
|
+
if (msgTime > since) {
|
|
49
|
+
count++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if drift is significant
|
|
58
|
+
* Spec Reference: §7.3 "Significant threshold: >10 min OR >5 turns"
|
|
59
|
+
*
|
|
60
|
+
* @param {number} ageMinutes - Age of the sidecar session in minutes
|
|
61
|
+
* @param {number} mainTurns - Number of turns in main session since sidecar started
|
|
62
|
+
* @returns {boolean} True if drift is significant
|
|
63
|
+
*/
|
|
64
|
+
function isDriftSignificant(ageMinutes, mainTurns) {
|
|
65
|
+
// Spec §7.3: isSignificant: ageMinutes > 10 || mainTurns > 5
|
|
66
|
+
return ageMinutes > 10 || mainTurns > 5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calculate context drift for a sidecar session
|
|
71
|
+
* Spec Reference: §7.3 Context Drift Indicator
|
|
72
|
+
*
|
|
73
|
+
* @param {Date|string} sessionStartTime - When the sidecar session started
|
|
74
|
+
* @param {string} mainSessionPath - Path to the main Claude Code session JSONL
|
|
75
|
+
* @returns {object} Drift information
|
|
76
|
+
* @returns {number} returns.ageMinutes - Minutes since sidecar started
|
|
77
|
+
* @returns {number} returns.mainTurns - Turns in main session since sidecar started
|
|
78
|
+
* @returns {boolean} returns.isSignificant - Whether drift exceeds thresholds
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const drift = calculateDrift(sessionStartTime, '/path/to/main-session.jsonl');
|
|
82
|
+
* // Returns: { ageMinutes: 23, mainTurns: 15, isSignificant: true }
|
|
83
|
+
*/
|
|
84
|
+
function calculateDrift(sessionStartTime, mainSessionPath) {
|
|
85
|
+
// Normalize session start time
|
|
86
|
+
const startTime = sessionStartTime instanceof Date
|
|
87
|
+
? sessionStartTime
|
|
88
|
+
: new Date(sessionStartTime);
|
|
89
|
+
|
|
90
|
+
// Calculate age in minutes
|
|
91
|
+
const ageMs = Date.now() - startTime.getTime();
|
|
92
|
+
const ageMinutes = Math.round(ageMs / 60000);
|
|
93
|
+
|
|
94
|
+
// Count turns in main session since sidecar started
|
|
95
|
+
const mainTurns = countTurnsSince(mainSessionPath, startTime);
|
|
96
|
+
|
|
97
|
+
// Determine if drift is significant per spec §7.3
|
|
98
|
+
const isSignificant = isDriftSignificant(ageMinutes, mainTurns);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
ageMinutes,
|
|
102
|
+
mainTurns,
|
|
103
|
+
isSignificant
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format drift warning for summary output
|
|
109
|
+
* Spec Reference: §7.3 Context Drift Indicator format
|
|
110
|
+
*
|
|
111
|
+
* @param {object} drift - Drift object from calculateDrift
|
|
112
|
+
* @returns {string} Formatted drift indicator string
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* formatDriftWarning({ ageMinutes: 23, mainTurns: 15, isSignificant: true })
|
|
116
|
+
* // Returns:
|
|
117
|
+
* // 📍 **Context Age:** 23 minutes (15 conversation turns in main session)
|
|
118
|
+
* //
|
|
119
|
+
* // ⚠️ **Drift Warning:** Main session has continued significantly since this
|
|
120
|
+
* // sidecar started. Verify recommendations against current project state.
|
|
121
|
+
*/
|
|
122
|
+
function formatDriftWarning(drift) {
|
|
123
|
+
if (!drift) {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = [
|
|
128
|
+
`\uD83D\uDCCD **Context Age:** ${drift.ageMinutes} minutes (${drift.mainTurns} conversation turns in main session)`
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (drift.isSignificant) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('\u26A0\uFE0F **Drift Warning:** Main session has continued significantly since this sidecar started. Verify recommendations against current project state.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
calculateDrift,
|
|
141
|
+
formatDriftWarning,
|
|
142
|
+
countTurnsSince,
|
|
143
|
+
isDriftSignificant
|
|
144
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Detection Module
|
|
3
|
+
*
|
|
4
|
+
* Detects the client type (code-local, code-web, cowork), determines
|
|
5
|
+
* display availability, and resolves the session root directory based
|
|
6
|
+
* on platform and CLI arguments.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { logger } = require('./utils/logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid client types for the --client flag
|
|
15
|
+
* @type {string[]}
|
|
16
|
+
*/
|
|
17
|
+
const VALID_CLIENTS = ['code-local', 'code-web', 'cowork'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Infer the client type from args and platform
|
|
21
|
+
*
|
|
22
|
+
* @param {object} args - Parsed CLI arguments
|
|
23
|
+
* @param {string} [args.client] - Explicit client type
|
|
24
|
+
* @param {string} platform - OS platform (e.g., 'darwin', 'linux', 'win32')
|
|
25
|
+
* @returns {string} The inferred client type
|
|
26
|
+
* @throws {Error} If args.client is provided but not a valid value
|
|
27
|
+
*/
|
|
28
|
+
function inferClient(args, platform) {
|
|
29
|
+
if (args.client) {
|
|
30
|
+
if (!VALID_CLIENTS.includes(args.client)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid client '${args.client}'. Valid values: ${VALID_CLIENTS.join(', ')}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
logger.debug('Using explicit client', { client: args.client });
|
|
36
|
+
return args.client;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (platform === 'darwin') {
|
|
40
|
+
logger.debug('Detected code-local on macOS');
|
|
41
|
+
return 'code-local';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) {
|
|
45
|
+
logger.debug('Detected code-local via display env var', {
|
|
46
|
+
DISPLAY: process.env.DISPLAY || null,
|
|
47
|
+
WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY || null
|
|
48
|
+
});
|
|
49
|
+
return 'code-local';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.debug('No display detected, defaulting to code-web', { platform });
|
|
53
|
+
return 'code-web';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Encode a filesystem path for use as a directory name.
|
|
58
|
+
* Replaces /, \, the drive-letter colon, and _ with dashes (matches Claude Code).
|
|
59
|
+
*
|
|
60
|
+
* @param {string} cwdPath - The working directory path
|
|
61
|
+
* @returns {string} Encoded path safe for directory names
|
|
62
|
+
*/
|
|
63
|
+
function encodePath(cwdPath) {
|
|
64
|
+
return cwdPath.replace(/[/\\:_]/g, '-');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the cowork local-agent-mode-sessions root for the given platform.
|
|
69
|
+
* Cowork stores session audit logs inside Claude Desktop's Application Support
|
|
70
|
+
* under local-agent-mode-sessions/<org>/<user>/local_<id>/audit.jsonl.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} platform - OS platform
|
|
73
|
+
* @returns {string} Cowork session root directory
|
|
74
|
+
*/
|
|
75
|
+
function getCoworkRoot(platform) {
|
|
76
|
+
const homedir = os.homedir();
|
|
77
|
+
|
|
78
|
+
if (platform === 'darwin') {
|
|
79
|
+
return path.join(homedir, 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (platform === 'win32') {
|
|
83
|
+
const appdata = process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming');
|
|
84
|
+
return path.join(appdata, 'Claude', 'local-agent-mode-sessions');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Linux and other Unix-like systems
|
|
88
|
+
return path.join(homedir, '.config', 'Claude', 'local-agent-mode-sessions');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the session root directory based on client type and platform
|
|
93
|
+
*
|
|
94
|
+
* @param {object} args - Parsed CLI arguments
|
|
95
|
+
* @param {string} [args.sessionDir] - Explicit session directory
|
|
96
|
+
* @param {string} args.client - Client type (code-local, code-web, cowork)
|
|
97
|
+
* @param {string} [args.cwd] - Working directory (defaults to process.cwd())
|
|
98
|
+
* @param {string} platform - OS platform
|
|
99
|
+
* @returns {string} Resolved session root directory path
|
|
100
|
+
* @throws {Error} If client is code-web and --session-dir is not provided
|
|
101
|
+
*/
|
|
102
|
+
function getSessionRoot(args, platform) {
|
|
103
|
+
if (args.sessionDir) {
|
|
104
|
+
logger.debug('Using explicit session directory', { sessionDir: args.sessionDir });
|
|
105
|
+
return args.sessionDir;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { client } = args;
|
|
109
|
+
|
|
110
|
+
if (client === 'code-local') {
|
|
111
|
+
const cwd = args.cwd || process.cwd();
|
|
112
|
+
const encodedPath = encodePath(cwd);
|
|
113
|
+
const sessionRoot = path.join(os.homedir(), '.claude', 'projects', encodedPath);
|
|
114
|
+
logger.debug('Resolved code-local session root', { cwd, sessionRoot });
|
|
115
|
+
return sessionRoot;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (client === 'code-web') {
|
|
119
|
+
throw new Error('--session-dir is required when --client is code-web');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (client === 'cowork') {
|
|
123
|
+
const sessionRoot = getCoworkRoot(platform);
|
|
124
|
+
logger.debug('Resolved cowork session root', { platform, sessionRoot });
|
|
125
|
+
return sessionRoot;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new Error(`Unknown client type: ${client}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect the full environment configuration
|
|
133
|
+
*
|
|
134
|
+
* @param {object} args - Parsed CLI arguments
|
|
135
|
+
* @param {string} [args.client] - Explicit client type
|
|
136
|
+
* @param {string} [args.sessionDir] - Explicit session directory
|
|
137
|
+
* @param {string} [args.cwd] - Working directory
|
|
138
|
+
* @param {string} platform - OS platform
|
|
139
|
+
* @returns {{ client: string, hasDisplay: boolean, sessionRoot: string }}
|
|
140
|
+
*/
|
|
141
|
+
function detectEnvironment(args, platform) {
|
|
142
|
+
const client = inferClient(args, platform);
|
|
143
|
+
const hasDisplay = client !== 'code-web';
|
|
144
|
+
const resolvedArgs = { ...args, client };
|
|
145
|
+
const sessionRoot = getSessionRoot(resolvedArgs, platform);
|
|
146
|
+
|
|
147
|
+
logger.info('Environment detected', { client, hasDisplay, sessionRoot });
|
|
148
|
+
|
|
149
|
+
return { client, hasDisplay, sessionRoot };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
inferClient,
|
|
154
|
+
getSessionRoot,
|
|
155
|
+
detectEnvironment,
|
|
156
|
+
VALID_CLIENTS
|
|
157
|
+
};
|