@ekkos/cli 0.2.18 → 0.3.3
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/LICENSE +21 -0
- package/dist/capture/eviction-client.d.ts +139 -0
- package/dist/capture/eviction-client.js +454 -0
- package/dist/capture/index.d.ts +2 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/jsonl-rewriter.d.ts +96 -0
- package/dist/capture/jsonl-rewriter.js +1369 -0
- package/dist/capture/transcript-repair.d.ts +50 -0
- package/dist/capture/transcript-repair.js +308 -0
- package/dist/commands/doctor.js +23 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +1229 -293
- package/dist/commands/usage.d.ts +7 -0
- package/dist/commands/usage.js +214 -0
- package/dist/cron/index.d.ts +7 -0
- package/dist/cron/index.js +13 -0
- package/dist/cron/promoter.d.ts +70 -0
- package/dist/cron/promoter.js +403 -0
- package/dist/index.js +24 -3
- package/dist/lib/usage-monitor.d.ts +47 -0
- package/dist/lib/usage-monitor.js +124 -0
- package/dist/lib/usage-parser.d.ts +72 -0
- package/dist/lib/usage-parser.js +238 -0
- package/dist/restore/RestoreOrchestrator.d.ts +4 -0
- package/dist/restore/RestoreOrchestrator.js +118 -30
- package/package.json +12 -12
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +2 -2
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/session-start.sh +0 -0
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -215
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript Repair - Handles orphan tool_result recovery
|
|
3
|
+
*
|
|
4
|
+
* When ccDNA's validate mode detects orphan tool_results (tool_result without
|
|
5
|
+
* matching tool_use), this module repairs the transcript:
|
|
6
|
+
*
|
|
7
|
+
* 1. ROLLBACK (preferred): Restore from backup if valid
|
|
8
|
+
* 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
|
|
9
|
+
*
|
|
10
|
+
* This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
|
|
11
|
+
*/
|
|
12
|
+
type RepairAction = 'none' | 'rollback' | 'surgical_repair' | 'failed';
|
|
13
|
+
export interface RepairResult {
|
|
14
|
+
action: RepairAction;
|
|
15
|
+
orphansFound: number;
|
|
16
|
+
removedLines?: number;
|
|
17
|
+
backupUsed?: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
|
|
22
|
+
* appears earlier in the transcript (in-order).
|
|
23
|
+
*/
|
|
24
|
+
export declare function countOrphansInJsonl(jsonlPath: string): {
|
|
25
|
+
orphans: number;
|
|
26
|
+
orphanIds: string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Main entry point: repair or rollback a transcript with orphan tool_results.
|
|
30
|
+
*
|
|
31
|
+
* Strategy:
|
|
32
|
+
* 1. Check if there are orphans (exit early if none)
|
|
33
|
+
* 2. Try rollback to newest valid backup
|
|
34
|
+
* 3. If no valid backup, surgically remove orphan lines
|
|
35
|
+
*/
|
|
36
|
+
export declare function repairOrRollbackTranscript(jsonlPath: string): RepairResult;
|
|
37
|
+
/**
|
|
38
|
+
* Quick validation check - can be called before operations to verify health.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isTranscriptHealthy(jsonlPath: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Get detailed transcript health info.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getTranscriptHealth(jsonlPath: string): {
|
|
45
|
+
exists: boolean;
|
|
46
|
+
healthy: boolean;
|
|
47
|
+
orphans: number;
|
|
48
|
+
orphanIds: string[];
|
|
49
|
+
};
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transcript Repair - Handles orphan tool_result recovery
|
|
4
|
+
*
|
|
5
|
+
* When ccDNA's validate mode detects orphan tool_results (tool_result without
|
|
6
|
+
* matching tool_use), this module repairs the transcript:
|
|
7
|
+
*
|
|
8
|
+
* 1. ROLLBACK (preferred): Restore from backup if valid
|
|
9
|
+
* 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
|
|
10
|
+
*
|
|
11
|
+
* This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.countOrphansInJsonl = countOrphansInJsonl;
|
|
48
|
+
exports.repairOrRollbackTranscript = repairOrRollbackTranscript;
|
|
49
|
+
exports.isTranscriptHealthy = isTranscriptHealthy;
|
|
50
|
+
exports.getTranscriptHealth = getTranscriptHealth;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
// Debug logging to the same file as eviction
|
|
55
|
+
function debugLog(category, msg, data) {
|
|
56
|
+
try {
|
|
57
|
+
const logDir = path.join(os.homedir(), '.ekkos', 'logs');
|
|
58
|
+
if (!fs.existsSync(logDir))
|
|
59
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
60
|
+
const logPath = path.join(logDir, 'eviction-debug.log');
|
|
61
|
+
const ts = new Date().toISOString();
|
|
62
|
+
const line = `[${ts}] [REPAIR:${category}] ${msg}${data ? '\n ' + JSON.stringify(data, null, 2).replace(/\n/g, '\n ') : ''}`;
|
|
63
|
+
fs.appendFileSync(logPath, line + '\n');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Silent fail
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
|
|
71
|
+
* appears earlier in the transcript (in-order).
|
|
72
|
+
*/
|
|
73
|
+
function countOrphansInJsonl(jsonlPath) {
|
|
74
|
+
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
75
|
+
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
76
|
+
const seenToolUses = new Set();
|
|
77
|
+
const orphanIds = [];
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
let obj;
|
|
80
|
+
try {
|
|
81
|
+
obj = JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const blocks = obj?.message?.content;
|
|
87
|
+
if (!Array.isArray(blocks))
|
|
88
|
+
continue;
|
|
89
|
+
for (const b of blocks) {
|
|
90
|
+
const block = b;
|
|
91
|
+
if (block?.type === 'tool_use' && typeof block?.id === 'string') {
|
|
92
|
+
seenToolUses.add(block.id);
|
|
93
|
+
}
|
|
94
|
+
else if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
|
|
95
|
+
if (!seenToolUses.has(block.tool_use_id)) {
|
|
96
|
+
orphanIds.push(block.tool_use_id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { orphans: orphanIds.length, orphanIds };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Find plausible backups for a jsonl file.
|
|
105
|
+
* Our jsonl-rewriter uses: ${filePath}.backup (single backup)
|
|
106
|
+
*/
|
|
107
|
+
function findBackups(jsonlPath) {
|
|
108
|
+
const dir = path.dirname(jsonlPath);
|
|
109
|
+
const base = path.basename(jsonlPath);
|
|
110
|
+
if (!fs.existsSync(dir))
|
|
111
|
+
return [];
|
|
112
|
+
const candidates = [];
|
|
113
|
+
// Primary backup: exact .backup suffix (from jsonl-rewriter.ts)
|
|
114
|
+
const primaryBackup = `${jsonlPath}.backup`;
|
|
115
|
+
if (fs.existsSync(primaryBackup)) {
|
|
116
|
+
candidates.push(primaryBackup);
|
|
117
|
+
}
|
|
118
|
+
// Also check for other backup patterns in case user has manual backups
|
|
119
|
+
const files = fs.readdirSync(dir);
|
|
120
|
+
const additionalBackups = files
|
|
121
|
+
.filter(f => f.startsWith(base) &&
|
|
122
|
+
f !== base &&
|
|
123
|
+
(f.includes('.bak') || f.includes('.backup') || f.includes('.old') || f.includes('.prev')))
|
|
124
|
+
.map(f => path.join(dir, f))
|
|
125
|
+
.filter(f => !candidates.includes(f)) // Don't duplicate
|
|
126
|
+
.sort((a, b) => {
|
|
127
|
+
// Sort by mtime, newest first
|
|
128
|
+
const am = fs.statSync(a).mtimeMs;
|
|
129
|
+
const bm = fs.statSync(b).mtimeMs;
|
|
130
|
+
return bm - am;
|
|
131
|
+
});
|
|
132
|
+
return [...candidates, ...additionalBackups];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Atomically replace target file with source file.
|
|
136
|
+
* Keeps the corrupted version for forensics.
|
|
137
|
+
*/
|
|
138
|
+
function atomicReplace(target, source) {
|
|
139
|
+
const dir = path.dirname(target);
|
|
140
|
+
const tmp = path.join(dir, `${path.basename(target)}.tmp-repair-${Date.now()}`);
|
|
141
|
+
fs.copyFileSync(source, tmp);
|
|
142
|
+
const corrupt = path.join(dir, `${path.basename(target)}.corrupt-${Date.now()}`);
|
|
143
|
+
fs.renameSync(target, corrupt);
|
|
144
|
+
fs.renameSync(tmp, target);
|
|
145
|
+
debugLog('ATOMIC_REPLACE', 'Replaced corrupt file', {
|
|
146
|
+
target,
|
|
147
|
+
source,
|
|
148
|
+
corruptBackup: corrupt,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Surgical repair: drop any line that contains a tool_result whose tool_use_id
|
|
153
|
+
* has not appeared earlier in the file. This unbricks at the cost of losing those results.
|
|
154
|
+
*/
|
|
155
|
+
function surgicalRepair(jsonlPath) {
|
|
156
|
+
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
157
|
+
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
158
|
+
const seenToolUses = new Set();
|
|
159
|
+
const kept = [];
|
|
160
|
+
let removed = 0;
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
let obj;
|
|
163
|
+
try {
|
|
164
|
+
obj = JSON.parse(line);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Keep non-JSON lines (shouldn't happen, but safe)
|
|
168
|
+
kept.push(line);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const blocks = obj?.message?.content;
|
|
172
|
+
if (!Array.isArray(blocks)) {
|
|
173
|
+
kept.push(line);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// First pass: collect tool_use IDs from this line
|
|
177
|
+
const lineToolUseIds = [];
|
|
178
|
+
for (const b of blocks) {
|
|
179
|
+
const block = b;
|
|
180
|
+
if (block?.type === 'tool_use' && typeof block?.id === 'string') {
|
|
181
|
+
lineToolUseIds.push(block.id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Second pass: check for orphan tool_results
|
|
185
|
+
let hasOrphanResult = false;
|
|
186
|
+
for (const b of blocks) {
|
|
187
|
+
const block = b;
|
|
188
|
+
if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
|
|
189
|
+
if (!seenToolUses.has(block.tool_use_id)) {
|
|
190
|
+
hasOrphanResult = true;
|
|
191
|
+
debugLog('SURGICAL_DROP', 'Dropping line with orphan tool_result', {
|
|
192
|
+
tool_use_id: block.tool_use_id,
|
|
193
|
+
linePreview: line.slice(0, 200),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (hasOrphanResult) {
|
|
199
|
+
removed++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// If kept, update seen tool uses AFTER keeping (order matters)
|
|
203
|
+
for (const id of lineToolUseIds) {
|
|
204
|
+
seenToolUses.add(id);
|
|
205
|
+
}
|
|
206
|
+
kept.push(line);
|
|
207
|
+
}
|
|
208
|
+
// Write atomically
|
|
209
|
+
const dir = path.dirname(jsonlPath);
|
|
210
|
+
const tmp = path.join(dir, `${path.basename(jsonlPath)}.tmp-surgical-${Date.now()}`);
|
|
211
|
+
fs.writeFileSync(tmp, kept.join('\n') + '\n', 'utf-8');
|
|
212
|
+
const corrupt = path.join(dir, `${path.basename(jsonlPath)}.corrupt-${Date.now()}`);
|
|
213
|
+
fs.renameSync(jsonlPath, corrupt);
|
|
214
|
+
fs.renameSync(tmp, jsonlPath);
|
|
215
|
+
const { orphans: orphansAfter } = countOrphansInJsonl(jsonlPath);
|
|
216
|
+
debugLog('SURGICAL_COMPLETE', 'Surgical repair finished', {
|
|
217
|
+
removed,
|
|
218
|
+
orphansAfter,
|
|
219
|
+
corruptBackup: corrupt,
|
|
220
|
+
});
|
|
221
|
+
return { removed, orphansAfter };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Main entry point: repair or rollback a transcript with orphan tool_results.
|
|
225
|
+
*
|
|
226
|
+
* Strategy:
|
|
227
|
+
* 1. Check if there are orphans (exit early if none)
|
|
228
|
+
* 2. Try rollback to newest valid backup
|
|
229
|
+
* 3. If no valid backup, surgically remove orphan lines
|
|
230
|
+
*/
|
|
231
|
+
function repairOrRollbackTranscript(jsonlPath) {
|
|
232
|
+
debugLog('REPAIR_START', '═══════════════════════════════════════════════════════════', {
|
|
233
|
+
action: 'TRANSCRIPT_REPAIR_INITIATED',
|
|
234
|
+
jsonlPath,
|
|
235
|
+
timestamp: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
238
|
+
debugLog('REPAIR_ABORT', 'JSONL file not found', { jsonlPath });
|
|
239
|
+
return { action: 'failed', orphansFound: 0, reason: 'jsonl_not_found' };
|
|
240
|
+
}
|
|
241
|
+
const initial = countOrphansInJsonl(jsonlPath);
|
|
242
|
+
debugLog('REPAIR_SCAN', 'Initial orphan count', {
|
|
243
|
+
orphans: initial.orphans,
|
|
244
|
+
orphanIds: initial.orphanIds,
|
|
245
|
+
});
|
|
246
|
+
if (initial.orphans === 0) {
|
|
247
|
+
debugLog('REPAIR_CLEAN', 'No orphans found - transcript is healthy');
|
|
248
|
+
return { action: 'none', orphansFound: 0 };
|
|
249
|
+
}
|
|
250
|
+
// 1) Try rollback to newest valid backup
|
|
251
|
+
const backups = findBackups(jsonlPath);
|
|
252
|
+
debugLog('REPAIR_BACKUPS', `Found ${backups.length} backup candidates`, { backups });
|
|
253
|
+
for (const backup of backups) {
|
|
254
|
+
try {
|
|
255
|
+
const v = countOrphansInJsonl(backup);
|
|
256
|
+
debugLog('REPAIR_CHECK_BACKUP', 'Validating backup', {
|
|
257
|
+
backup,
|
|
258
|
+
orphans: v.orphans,
|
|
259
|
+
});
|
|
260
|
+
if (v.orphans === 0) {
|
|
261
|
+
atomicReplace(jsonlPath, backup);
|
|
262
|
+
debugLog('REPAIR_ROLLBACK_SUCCESS', 'Rolled back to valid backup', { backup });
|
|
263
|
+
return { action: 'rollback', orphansFound: initial.orphans, backupUsed: backup };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
debugLog('REPAIR_BACKUP_ERROR', 'Failed to check backup', {
|
|
268
|
+
backup,
|
|
269
|
+
error: err.message,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// 2) Surgical repair fallback
|
|
274
|
+
debugLog('REPAIR_SURGICAL', 'No valid backup found - attempting surgical repair');
|
|
275
|
+
const { removed, orphansAfter } = surgicalRepair(jsonlPath);
|
|
276
|
+
if (orphansAfter === 0) {
|
|
277
|
+
return { action: 'surgical_repair', orphansFound: initial.orphans, removedLines: removed };
|
|
278
|
+
}
|
|
279
|
+
debugLog('REPAIR_FAILED', 'Surgical repair incomplete - orphans remain', {
|
|
280
|
+
orphansAfter,
|
|
281
|
+
removed,
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
action: 'failed',
|
|
285
|
+
orphansFound: initial.orphans,
|
|
286
|
+
removedLines: removed,
|
|
287
|
+
reason: 'orphans_remain_after_surgical',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Quick validation check - can be called before operations to verify health.
|
|
292
|
+
*/
|
|
293
|
+
function isTranscriptHealthy(jsonlPath) {
|
|
294
|
+
if (!fs.existsSync(jsonlPath))
|
|
295
|
+
return true; // Non-existent is not unhealthy
|
|
296
|
+
const { orphans } = countOrphansInJsonl(jsonlPath);
|
|
297
|
+
return orphans === 0;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get detailed transcript health info.
|
|
301
|
+
*/
|
|
302
|
+
function getTranscriptHealth(jsonlPath) {
|
|
303
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
304
|
+
return { exists: false, healthy: true, orphans: 0, orphanIds: [] };
|
|
305
|
+
}
|
|
306
|
+
const { orphans, orphanIds } = countOrphansInJsonl(jsonlPath);
|
|
307
|
+
return { exists: true, healthy: orphans === 0, orphans, orphanIds };
|
|
308
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -21,10 +21,17 @@ const fs_1 = require("fs");
|
|
|
21
21
|
const child_process_1 = require("child_process");
|
|
22
22
|
const chalk_1 = __importDefault(require("chalk"));
|
|
23
23
|
const hooks_1 = require("./hooks");
|
|
24
|
+
// ekkOS-managed Claude installation path (same as run.ts)
|
|
25
|
+
const EKKOS_CLAUDE_BIN = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'claude-code', 'node_modules', '.bin', 'claude');
|
|
24
26
|
/**
|
|
25
|
-
* Check if a command exists in PATH
|
|
27
|
+
* Check if a command exists in PATH or managed location
|
|
28
|
+
* For 'claude', prioritizes ekkOS-managed installation
|
|
26
29
|
*/
|
|
27
30
|
function commandExists(cmd) {
|
|
31
|
+
// For claude, check managed path first
|
|
32
|
+
if (cmd === 'claude' && (0, fs_1.existsSync)(EKKOS_CLAUDE_BIN)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
28
35
|
try {
|
|
29
36
|
const which = (0, os_1.platform)() === 'win32' ? 'where' : 'which';
|
|
30
37
|
(0, child_process_1.execSync)(`${which} ${cmd}`, { stdio: 'ignore' });
|
|
@@ -36,8 +43,23 @@ function commandExists(cmd) {
|
|
|
36
43
|
}
|
|
37
44
|
/**
|
|
38
45
|
* Get command version safely
|
|
46
|
+
* For 'claude', prioritizes ekkOS-managed installation
|
|
39
47
|
*/
|
|
40
48
|
function getVersion(cmd, versionFlag = '--version') {
|
|
49
|
+
// For claude, try managed path first
|
|
50
|
+
if (cmd === 'claude' && (0, fs_1.existsSync)(EKKOS_CLAUDE_BIN)) {
|
|
51
|
+
try {
|
|
52
|
+
const output = (0, child_process_1.execSync)(`"${EKKOS_CLAUDE_BIN}" ${versionFlag}`, {
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
56
|
+
}).trim();
|
|
57
|
+
return output.split('\n')[0];
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fall through to PATH lookup
|
|
61
|
+
}
|
|
62
|
+
}
|
|
41
63
|
try {
|
|
42
64
|
const output = (0, child_process_1.execSync)(`${cmd} ${versionFlag}`, {
|
|
43
65
|
encoding: 'utf-8',
|