@ekkos/cli 0.2.3 → 0.2.5
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/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +418 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +100 -22
- package/dist/index.js +13 -1
- package/dist/utils/state.d.ts +9 -0
- package/dist/utils/state.js +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type GateStatus = 'PASS' | 'FAIL' | 'WARN';
|
|
2
|
+
interface Check {
|
|
3
|
+
name: string;
|
|
4
|
+
passed: boolean;
|
|
5
|
+
detail?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Gate {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
status: GateStatus;
|
|
11
|
+
checks: Check[];
|
|
12
|
+
fix?: string;
|
|
13
|
+
}
|
|
14
|
+
interface DoctorReport {
|
|
15
|
+
platform: string;
|
|
16
|
+
gates: Gate[];
|
|
17
|
+
canProceed: boolean;
|
|
18
|
+
nodeVersion?: string;
|
|
19
|
+
claudeVersion?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run diagnostic checks and return report
|
|
23
|
+
*/
|
|
24
|
+
export declare function runDiagnostics(): DoctorReport;
|
|
25
|
+
/**
|
|
26
|
+
* Main doctor command
|
|
27
|
+
*/
|
|
28
|
+
export declare function doctor(options?: {
|
|
29
|
+
fix?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDiagnostics = runDiagnostics;
|
|
7
|
+
exports.doctor = doctor;
|
|
8
|
+
const os_1 = require("os");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
/**
|
|
12
|
+
* Check if a command exists in PATH
|
|
13
|
+
*/
|
|
14
|
+
function commandExists(cmd) {
|
|
15
|
+
try {
|
|
16
|
+
const which = (0, os_1.platform)() === 'win32' ? 'where' : 'which';
|
|
17
|
+
(0, child_process_1.execSync)(`${which} ${cmd}`, { stdio: 'ignore' });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get command version safely
|
|
26
|
+
*/
|
|
27
|
+
function getVersion(cmd, versionFlag = '--version') {
|
|
28
|
+
try {
|
|
29
|
+
const output = (0, child_process_1.execSync)(`${cmd} ${versionFlag}`, {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
timeout: 10000,
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
33
|
+
}).trim();
|
|
34
|
+
return output.split('\n')[0];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if node-pty can be loaded
|
|
42
|
+
*/
|
|
43
|
+
function checkPty() {
|
|
44
|
+
try {
|
|
45
|
+
// Try to require node-pty
|
|
46
|
+
require('node-pty');
|
|
47
|
+
return { available: true };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// Check for prebuilt alternative
|
|
51
|
+
try {
|
|
52
|
+
require('node-pty-prebuilt-multiarch');
|
|
53
|
+
return { available: true };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {
|
|
57
|
+
available: false,
|
|
58
|
+
error: error?.message || 'Cannot load node-pty'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check MCP configuration via claude mcp list
|
|
65
|
+
*/
|
|
66
|
+
function checkMcpConfig() {
|
|
67
|
+
try {
|
|
68
|
+
const output = (0, child_process_1.execSync)('claude mcp list', {
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
72
|
+
});
|
|
73
|
+
// Parse server names from output
|
|
74
|
+
const servers = [];
|
|
75
|
+
const lines = output.split('\n');
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
// Look for server names (varies by Claude version)
|
|
78
|
+
if (line.includes('ekkos') || line.includes('memory')) {
|
|
79
|
+
servers.push(line.trim());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const hasEkkos = output.toLowerCase().includes('ekkos') ||
|
|
83
|
+
output.toLowerCase().includes('memory');
|
|
84
|
+
return { configured: hasEkkos, servers };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
configured: false,
|
|
89
|
+
servers: [],
|
|
90
|
+
error: error?.message || 'Failed to check MCP config'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if bash is available (for hooks on Windows)
|
|
96
|
+
*/
|
|
97
|
+
function checkBash() {
|
|
98
|
+
if ((0, os_1.platform)() !== 'win32')
|
|
99
|
+
return true;
|
|
100
|
+
try {
|
|
101
|
+
(0, child_process_1.execSync)('bash --version', { stdio: 'ignore', timeout: 5000 });
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Run diagnostic checks and return report
|
|
110
|
+
*/
|
|
111
|
+
function runDiagnostics() {
|
|
112
|
+
const isWindows = (0, os_1.platform)() === 'win32';
|
|
113
|
+
const gates = [];
|
|
114
|
+
// Get basic info
|
|
115
|
+
const nodeVersion = getVersion('node') || 'Not found';
|
|
116
|
+
const claudeVersion = getVersion('claude');
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
// GATE 1: Interactive Claude Works
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
|
+
const claudeChecks = [];
|
|
121
|
+
let claudeGatePass = true;
|
|
122
|
+
// Check 1.1: claude command exists
|
|
123
|
+
const claudeExists = commandExists('claude');
|
|
124
|
+
claudeChecks.push({
|
|
125
|
+
name: 'Claude CLI installed',
|
|
126
|
+
passed: claudeExists,
|
|
127
|
+
detail: claudeExists ? claudeVersion || 'Found' : 'Not in PATH'
|
|
128
|
+
});
|
|
129
|
+
if (!claudeExists)
|
|
130
|
+
claudeGatePass = false;
|
|
131
|
+
// Check 1.2: claude --version works
|
|
132
|
+
if (claudeExists) {
|
|
133
|
+
const versionWorks = claudeVersion !== null;
|
|
134
|
+
claudeChecks.push({
|
|
135
|
+
name: 'Claude responds to --version',
|
|
136
|
+
passed: versionWorks,
|
|
137
|
+
detail: versionWorks ? claudeVersion : 'No response'
|
|
138
|
+
});
|
|
139
|
+
if (!versionWorks)
|
|
140
|
+
claudeGatePass = false;
|
|
141
|
+
}
|
|
142
|
+
// Check 1.3: On Windows, PTY is required for interactive mode
|
|
143
|
+
// This is checked in Gate 2, but we note it here
|
|
144
|
+
if (isWindows) {
|
|
145
|
+
claudeChecks.push({
|
|
146
|
+
name: 'Interactive mode (requires PTY)',
|
|
147
|
+
passed: true, // Will be validated in Gate 2
|
|
148
|
+
detail: 'See PTY gate below'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
gates.push({
|
|
152
|
+
id: 'interactive-claude',
|
|
153
|
+
title: 'Interactive Claude',
|
|
154
|
+
status: claudeGatePass ? 'PASS' : 'FAIL',
|
|
155
|
+
checks: claudeChecks,
|
|
156
|
+
fix: claudeGatePass ? undefined : 'npm install -g @anthropic-ai/claude-code'
|
|
157
|
+
});
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// GATE 2: PTY Works (Windows-critical)
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
+
const ptyChecks = [];
|
|
162
|
+
let ptyGatePass = true;
|
|
163
|
+
// Check Node version (20.x or 22.x recommended)
|
|
164
|
+
const nodeMatch = nodeVersion.match(/v?(\d+)\./);
|
|
165
|
+
const nodeMajor = nodeMatch ? parseInt(nodeMatch[1], 10) : 0;
|
|
166
|
+
const nodeOk = nodeMajor >= 18 && nodeMajor <= 22;
|
|
167
|
+
ptyChecks.push({
|
|
168
|
+
name: 'Node.js version',
|
|
169
|
+
passed: nodeOk,
|
|
170
|
+
detail: nodeOk
|
|
171
|
+
? `${nodeVersion} (OK)`
|
|
172
|
+
: `${nodeVersion} (Need 20.x or 22.x LTS for PTY support)`
|
|
173
|
+
});
|
|
174
|
+
if (!nodeOk && isWindows)
|
|
175
|
+
ptyGatePass = false;
|
|
176
|
+
// Check PTY availability
|
|
177
|
+
const ptyResult = checkPty();
|
|
178
|
+
ptyChecks.push({
|
|
179
|
+
name: 'node-pty loadable',
|
|
180
|
+
passed: ptyResult.available,
|
|
181
|
+
detail: ptyResult.available ? 'ConPTY available' : ptyResult.error
|
|
182
|
+
});
|
|
183
|
+
if (!ptyResult.available && isWindows)
|
|
184
|
+
ptyGatePass = false;
|
|
185
|
+
// On macOS/Linux, PTY is typically available, mark as PASS with note
|
|
186
|
+
if (!isWindows && !ptyResult.available) {
|
|
187
|
+
ptyChecks.push({
|
|
188
|
+
name: 'Fallback available',
|
|
189
|
+
passed: true,
|
|
190
|
+
detail: 'Unix script(1) fallback available'
|
|
191
|
+
});
|
|
192
|
+
ptyGatePass = true; // Unix has fallback
|
|
193
|
+
}
|
|
194
|
+
gates.push({
|
|
195
|
+
id: 'pty',
|
|
196
|
+
title: 'PTY (Terminal)',
|
|
197
|
+
status: ptyGatePass ? 'PASS' : (isWindows ? 'FAIL' : 'WARN'),
|
|
198
|
+
checks: ptyChecks,
|
|
199
|
+
fix: ptyGatePass ? undefined : isWindows
|
|
200
|
+
? 'npm install node-pty-prebuilt-multiarch OR install VS Build Tools and run: npm rebuild node-pty'
|
|
201
|
+
: undefined
|
|
202
|
+
});
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// GATE 3: MCP Works
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
const mcpChecks = [];
|
|
207
|
+
let mcpGatePass = true;
|
|
208
|
+
// Check MCP config
|
|
209
|
+
const mcpResult = checkMcpConfig();
|
|
210
|
+
if (mcpResult.error && mcpResult.error.includes('not found')) {
|
|
211
|
+
// Claude not installed, can't check MCP
|
|
212
|
+
mcpChecks.push({
|
|
213
|
+
name: 'MCP configuration',
|
|
214
|
+
passed: false,
|
|
215
|
+
detail: 'Claude CLI required to check MCP'
|
|
216
|
+
});
|
|
217
|
+
mcpGatePass = false;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
mcpChecks.push({
|
|
221
|
+
name: 'ekkOS MCP server configured',
|
|
222
|
+
passed: mcpResult.configured,
|
|
223
|
+
detail: mcpResult.configured
|
|
224
|
+
? `Found: ${mcpResult.servers.join(', ') || 'ekkOS server'}`
|
|
225
|
+
: 'No ekkOS server in claude mcp list'
|
|
226
|
+
});
|
|
227
|
+
if (!mcpResult.configured)
|
|
228
|
+
mcpGatePass = false;
|
|
229
|
+
}
|
|
230
|
+
// Check bash on Windows (for hooks, WARN only)
|
|
231
|
+
if (isWindows) {
|
|
232
|
+
const bashOk = checkBash();
|
|
233
|
+
mcpChecks.push({
|
|
234
|
+
name: 'Bash available (for hooks)',
|
|
235
|
+
passed: bashOk,
|
|
236
|
+
detail: bashOk ? 'Git Bash/WSL detected' : 'PowerShell hooks will be used'
|
|
237
|
+
});
|
|
238
|
+
// Don't fail gate for missing bash, just note it
|
|
239
|
+
}
|
|
240
|
+
gates.push({
|
|
241
|
+
id: 'mcp',
|
|
242
|
+
title: 'MCP Configuration',
|
|
243
|
+
status: mcpGatePass ? 'PASS' : 'FAIL',
|
|
244
|
+
checks: mcpChecks,
|
|
245
|
+
fix: mcpGatePass ? undefined :
|
|
246
|
+
'claude mcp add ekkos-memory npx -y @ekkos/cli@latest mcp && claude mcp start ekkos-memory'
|
|
247
|
+
});
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
|
+
// Calculate overall status
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
+
const allPass = gates.every(g => g.status === 'PASS');
|
|
252
|
+
const hasBlocker = gates.some(g => g.status === 'FAIL');
|
|
253
|
+
return {
|
|
254
|
+
platform: isWindows ? 'Windows' : (0, os_1.platform)() === 'darwin' ? 'macOS' : 'Linux',
|
|
255
|
+
gates,
|
|
256
|
+
canProceed: !hasBlocker,
|
|
257
|
+
nodeVersion,
|
|
258
|
+
claudeVersion: claudeVersion || undefined
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Display the doctor report
|
|
263
|
+
*/
|
|
264
|
+
function displayReport(report) {
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(chalk_1.default.cyan.bold('🩺 ekkOS Doctor'));
|
|
267
|
+
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
268
|
+
console.log('');
|
|
269
|
+
// Platform info
|
|
270
|
+
console.log(chalk_1.default.gray(`Platform: ${report.platform}`));
|
|
271
|
+
console.log(chalk_1.default.gray(`Node: ${report.nodeVersion}`));
|
|
272
|
+
if (report.claudeVersion) {
|
|
273
|
+
console.log(chalk_1.default.gray(`Claude: ${report.claudeVersion}`));
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
// Display each gate
|
|
277
|
+
for (const gate of report.gates) {
|
|
278
|
+
const icon = gate.status === 'PASS' ? chalk_1.default.green('✓')
|
|
279
|
+
: gate.status === 'WARN' ? chalk_1.default.yellow('○')
|
|
280
|
+
: chalk_1.default.red('✗');
|
|
281
|
+
const color = gate.status === 'PASS' ? chalk_1.default.green
|
|
282
|
+
: gate.status === 'WARN' ? chalk_1.default.yellow
|
|
283
|
+
: chalk_1.default.red;
|
|
284
|
+
console.log(`${icon} ${color.bold(gate.title)}`);
|
|
285
|
+
for (const check of gate.checks) {
|
|
286
|
+
const checkIcon = check.passed ? chalk_1.default.green(' ✓') : chalk_1.default.red(' ✗');
|
|
287
|
+
console.log(`${checkIcon} ${check.name}`);
|
|
288
|
+
if (check.detail) {
|
|
289
|
+
console.log(chalk_1.default.gray(` ${check.detail}`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (gate.fix && gate.status === 'FAIL') {
|
|
293
|
+
console.log(chalk_1.default.yellow(` → Fix: ${gate.fix}`));
|
|
294
|
+
}
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
// Summary
|
|
298
|
+
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
299
|
+
const blockers = report.gates.filter(g => g.status === 'FAIL').length;
|
|
300
|
+
const warnings = report.gates.filter(g => g.status === 'WARN').length;
|
|
301
|
+
if (blockers > 0) {
|
|
302
|
+
console.log(chalk_1.default.red.bold(`BLOCKERS: ${blockers}`));
|
|
303
|
+
console.log(chalk_1.default.yellow('Run the fix commands above to resolve issues.'));
|
|
304
|
+
}
|
|
305
|
+
else if (warnings > 0) {
|
|
306
|
+
console.log(chalk_1.default.yellow.bold(`WARNINGS: ${warnings}`));
|
|
307
|
+
console.log(chalk_1.default.green('ekkOS can run, but some features may be limited.'));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.log(chalk_1.default.green.bold('✓ All systems operational'));
|
|
311
|
+
console.log(chalk_1.default.green('ekkOS is ready to use.'));
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Attempt safe auto-fixes
|
|
317
|
+
*/
|
|
318
|
+
async function attemptAutoFixes(report) {
|
|
319
|
+
const fixed = [];
|
|
320
|
+
const manual = [];
|
|
321
|
+
for (const gate of report.gates) {
|
|
322
|
+
if (gate.status === 'PASS')
|
|
323
|
+
continue;
|
|
324
|
+
switch (gate.id) {
|
|
325
|
+
case 'interactive-claude':
|
|
326
|
+
// Try to install Claude Code (safe, non-admin)
|
|
327
|
+
console.log(chalk_1.default.yellow('\nAttempting to install Claude Code...'));
|
|
328
|
+
try {
|
|
329
|
+
(0, child_process_1.execSync)('npm install -g @anthropic-ai/claude-code', {
|
|
330
|
+
stdio: 'inherit',
|
|
331
|
+
timeout: 120000
|
|
332
|
+
});
|
|
333
|
+
fixed.push('Claude Code installed');
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
manual.push('npm install -g @anthropic-ai/claude-code');
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
case 'pty':
|
|
340
|
+
// Try prebuilt PTY first (safe, non-admin)
|
|
341
|
+
console.log(chalk_1.default.yellow('\nAttempting to install prebuilt PTY...'));
|
|
342
|
+
try {
|
|
343
|
+
// Try homebridge prebuilt first
|
|
344
|
+
(0, child_process_1.execSync)('npm install -g @homebridge/node-pty-prebuilt-multiarch', {
|
|
345
|
+
stdio: 'inherit',
|
|
346
|
+
timeout: 60000
|
|
347
|
+
});
|
|
348
|
+
fixed.push('Prebuilt PTY installed');
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
manual.push('npm install -g @homebridge/node-pty-prebuilt-multiarch');
|
|
352
|
+
manual.push('If that fails: Install VS Build Tools, then: npm rebuild node-pty');
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
case 'mcp':
|
|
356
|
+
// Try to configure MCP (safe)
|
|
357
|
+
console.log(chalk_1.default.yellow('\nAttempting to configure MCP...'));
|
|
358
|
+
try {
|
|
359
|
+
(0, child_process_1.execSync)('claude mcp add ekkos-memory npx -y @ekkos/cli@latest mcp', {
|
|
360
|
+
stdio: 'inherit',
|
|
361
|
+
timeout: 30000
|
|
362
|
+
});
|
|
363
|
+
(0, child_process_1.execSync)('claude mcp start ekkos-memory', {
|
|
364
|
+
stdio: 'inherit',
|
|
365
|
+
timeout: 10000
|
|
366
|
+
});
|
|
367
|
+
fixed.push('MCP configured');
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
manual.push('claude mcp add ekkos-memory npx -y @ekkos/cli@latest mcp');
|
|
371
|
+
manual.push('claude mcp start ekkos-memory');
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return { fixed, manual };
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Main doctor command
|
|
380
|
+
*/
|
|
381
|
+
async function doctor(options = {}) {
|
|
382
|
+
const report = runDiagnostics();
|
|
383
|
+
// JSON output mode
|
|
384
|
+
if (options.json) {
|
|
385
|
+
console.log(JSON.stringify(report, null, 2));
|
|
386
|
+
process.exit(report.canProceed ? 0 : 1);
|
|
387
|
+
}
|
|
388
|
+
displayReport(report);
|
|
389
|
+
// Fix mode - attempt safe auto-fixes
|
|
390
|
+
if (options.fix && !report.canProceed) {
|
|
391
|
+
console.log(chalk_1.default.cyan.bold('\n🔧 Attempting auto-fixes...\n'));
|
|
392
|
+
const { fixed, manual } = await attemptAutoFixes(report);
|
|
393
|
+
if (fixed.length > 0) {
|
|
394
|
+
console.log(chalk_1.default.green('\n✓ Auto-fixed:'));
|
|
395
|
+
for (const f of fixed) {
|
|
396
|
+
console.log(chalk_1.default.green(` • ${f}`));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (manual.length > 0) {
|
|
400
|
+
console.log(chalk_1.default.yellow('\n⚠ Manual steps required:'));
|
|
401
|
+
for (const m of manual) {
|
|
402
|
+
console.log(chalk_1.default.white(` ${m}`));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Re-run diagnostics to check if fixes worked
|
|
406
|
+
console.log(chalk_1.default.cyan('\n🔄 Re-checking...'));
|
|
407
|
+
const recheck = runDiagnostics();
|
|
408
|
+
displayReport(recheck);
|
|
409
|
+
if (recheck.canProceed) {
|
|
410
|
+
console.log(chalk_1.default.green.bold('✓ All issues resolved!'));
|
|
411
|
+
}
|
|
412
|
+
process.exit(recheck.canProceed ? 0 : 1);
|
|
413
|
+
}
|
|
414
|
+
// Exit with error code if blockers
|
|
415
|
+
if (!report.canProceed) {
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
}
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -43,6 +43,7 @@ const path = __importStar(require("path"));
|
|
|
43
43
|
const os = __importStar(require("os"));
|
|
44
44
|
const child_process_1 = require("child_process");
|
|
45
45
|
const state_1 = require("../utils/state");
|
|
46
|
+
const doctor_1 = require("./doctor");
|
|
46
47
|
// Try to load node-pty (may fail on Node 24+)
|
|
47
48
|
let pty = null;
|
|
48
49
|
try {
|
|
@@ -54,15 +55,15 @@ catch {
|
|
|
54
55
|
function getConfig(options) {
|
|
55
56
|
return {
|
|
56
57
|
slashOpenDelayMs: options.slashOpenDelayMs ??
|
|
57
|
-
parseInt(process.env.EKKOS_SLASH_OPEN_DELAY_MS || '
|
|
58
|
+
parseInt(process.env.EKKOS_SLASH_OPEN_DELAY_MS || '500', 10), // was 1000
|
|
58
59
|
charDelayMs: options.charDelayMs ??
|
|
59
60
|
parseInt(process.env.EKKOS_CHAR_DELAY_MS || '25', 10),
|
|
60
61
|
postEnterDelayMs: options.postEnterDelayMs ??
|
|
61
|
-
parseInt(process.env.EKKOS_POST_ENTER_DELAY_MS || '
|
|
62
|
+
parseInt(process.env.EKKOS_POST_ENTER_DELAY_MS || '300', 10), // was 500
|
|
62
63
|
clearWaitMs: options.clearWaitMs ??
|
|
63
|
-
parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '
|
|
64
|
+
parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '2500', 10), // was 5000
|
|
64
65
|
idlePromptMs: parseInt(process.env.EKKOS_IDLE_PROMPT_MS || '250', 10),
|
|
65
|
-
paletteRetryMs: parseInt(process.env.EKKOS_PALETTE_RETRY_MS || '
|
|
66
|
+
paletteRetryMs: parseInt(process.env.EKKOS_PALETTE_RETRY_MS || '400', 10), // was 500
|
|
66
67
|
debugLogPath: options.debugLogPath ??
|
|
67
68
|
process.env.EKKOS_DEBUG_LOG_PATH ??
|
|
68
69
|
path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log')
|
|
@@ -362,6 +363,26 @@ async function emergencyCapture(transcriptPath, sessionId) {
|
|
|
362
363
|
async function run(options) {
|
|
363
364
|
const verbose = options.verbose || false;
|
|
364
365
|
const bypass = options.bypass || false;
|
|
366
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
367
|
+
// PRE-FLIGHT DIAGNOSTICS (--doctor flag)
|
|
368
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
369
|
+
if (options.doctor) {
|
|
370
|
+
console.log(chalk_1.default.cyan('\n🩺 Running pre-flight diagnostics...\n'));
|
|
371
|
+
const report = (0, doctor_1.runDiagnostics)();
|
|
372
|
+
// Display summary
|
|
373
|
+
for (const gate of report.gates) {
|
|
374
|
+
const icon = gate.status === 'PASS' ? chalk_1.default.green('✓')
|
|
375
|
+
: gate.status === 'WARN' ? chalk_1.default.yellow('○')
|
|
376
|
+
: chalk_1.default.red('✗');
|
|
377
|
+
console.log(`${icon} ${gate.title}: ${gate.status}`);
|
|
378
|
+
}
|
|
379
|
+
console.log('');
|
|
380
|
+
if (!report.canProceed) {
|
|
381
|
+
console.log(chalk_1.default.red('Blockers detected. Run `ekkos doctor` for details.'));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
console.log(chalk_1.default.green('✓ All checks passed. Starting Claude...\n'));
|
|
385
|
+
}
|
|
365
386
|
// Get injection config (from options or env vars)
|
|
366
387
|
const config = getConfig(options);
|
|
367
388
|
setDebugLogPath(config.debugLogPath);
|
|
@@ -481,6 +502,9 @@ async function run(options) {
|
|
|
481
502
|
let lastSeenSessionName = null;
|
|
482
503
|
let lastSeenSessionAt = 0;
|
|
483
504
|
const RECENCY_WINDOW_MS = 15000; // 15s - session name must be recent to trust
|
|
505
|
+
// Track if we've EVER observed a session in THIS process run
|
|
506
|
+
// This is the authoritative flag - if false, don't trust persisted state
|
|
507
|
+
let observedSessionThisRun = false;
|
|
484
508
|
// Output buffer for pattern detection
|
|
485
509
|
let outputBuffer = '';
|
|
486
510
|
// Debounce tracking to prevent double triggers
|
|
@@ -513,6 +537,35 @@ async function run(options) {
|
|
|
513
537
|
}
|
|
514
538
|
// Determine which mode to use
|
|
515
539
|
const usePty = pty !== null;
|
|
540
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
541
|
+
// WINDOWS: HARD FAIL WITHOUT PTY
|
|
542
|
+
// Without node-pty/ConPTY, auto-continue cannot work on Windows.
|
|
543
|
+
// Claude switches to --print mode without a real PTY, breaking the TUI.
|
|
544
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
545
|
+
if (isWindows && !usePty) {
|
|
546
|
+
console.log('');
|
|
547
|
+
console.log(chalk_1.default.red.bold('❌ ekkos run requires PTY support on Windows'));
|
|
548
|
+
console.log('');
|
|
549
|
+
console.log(chalk_1.default.yellow('Without node-pty (ConPTY), Claude Code runs in --print mode'));
|
|
550
|
+
console.log(chalk_1.default.yellow('which prevents auto-continue from working.'));
|
|
551
|
+
console.log('');
|
|
552
|
+
console.log(chalk_1.default.cyan('To fix:'));
|
|
553
|
+
console.log('');
|
|
554
|
+
console.log(chalk_1.default.white(' Option 1: Use Node 20 or 22 LTS (recommended)'));
|
|
555
|
+
console.log(chalk_1.default.gray(' winget install OpenJS.NodeJS.LTS'));
|
|
556
|
+
console.log(chalk_1.default.gray(' npm install -g @ekkos/cli'));
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log(chalk_1.default.white(' Option 2: Install prebuilt PTY'));
|
|
559
|
+
console.log(chalk_1.default.gray(' npm install node-pty-prebuilt-multiarch'));
|
|
560
|
+
console.log('');
|
|
561
|
+
console.log(chalk_1.default.white(' Option 3: Build node-pty from source'));
|
|
562
|
+
console.log(chalk_1.default.gray(' 1. Install VS Build Tools (Desktop C++ workload)'));
|
|
563
|
+
console.log(chalk_1.default.gray(' 2. npm rebuild node-pty --build-from-source'));
|
|
564
|
+
console.log('');
|
|
565
|
+
console.log(chalk_1.default.gray('Run `ekkos doctor` for detailed diagnostics.'));
|
|
566
|
+
console.log('');
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
516
569
|
if (verbose) {
|
|
517
570
|
console.log(chalk_1.default.gray(` 💻 PTY mode: ${usePty ? 'node-pty' : 'spawn+script (fallback)'}`));
|
|
518
571
|
console.log('');
|
|
@@ -589,23 +642,46 @@ async function run(options) {
|
|
|
589
642
|
return;
|
|
590
643
|
}
|
|
591
644
|
// ════════════════════════════════════════════════════════════════════════
|
|
592
|
-
// SESSION SELECTION:
|
|
593
|
-
//
|
|
594
|
-
//
|
|
645
|
+
// SESSION SELECTION: observedSessionThisRun pattern
|
|
646
|
+
//
|
|
647
|
+
// The key insight: ONLY trust session names we've actually observed in THIS
|
|
648
|
+
// process run. On Windows (without PTY), Claude may exit immediately without
|
|
649
|
+
// emitting any session info, leaving persisted state stale.
|
|
650
|
+
//
|
|
651
|
+
// Priority:
|
|
652
|
+
// 1. lastSeenSessionName (if recent AND observed this run)
|
|
653
|
+
// 2. currentSession (if observed this run)
|
|
654
|
+
// 3. Most recent session from local cache (if NOT observed this run)
|
|
655
|
+
// 4. Persisted state (last resort)
|
|
595
656
|
// ════════════════════════════════════════════════════════════════════════
|
|
596
657
|
let sessionToRestore = null;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
658
|
+
if (observedSessionThisRun) {
|
|
659
|
+
// We've seen a session in THIS process - trust in-memory state
|
|
660
|
+
dlog(`observedSessionThisRun=true - trusting in-memory state`);
|
|
661
|
+
// PRIORITY 1: Most recently observed session name from TUI output
|
|
662
|
+
// Only trust if seen within RECENCY_WINDOW_MS (15 seconds)
|
|
663
|
+
if (lastSeenSessionName && (Date.now() - lastSeenSessionAt) < RECENCY_WINDOW_MS) {
|
|
664
|
+
sessionToRestore = lastSeenSessionName;
|
|
665
|
+
dlog(`Using lastSeenSessionName (${Date.now() - lastSeenSessionAt}ms ago): ${sessionToRestore}`);
|
|
666
|
+
}
|
|
667
|
+
// PRIORITY 2: currentSession (in-memory, confirmed this run)
|
|
668
|
+
if (!sessionToRestore && currentSession) {
|
|
669
|
+
sessionToRestore = currentSession;
|
|
670
|
+
dlog(`Using currentSession (in-memory, observed this run): ${sessionToRestore}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// We've NOT observed any session this run - don't trust persisted state
|
|
675
|
+
// This prevents the "groovy-koala-saves" bug on Windows
|
|
676
|
+
dlog(`observedSessionThisRun=false - using cache fallback`);
|
|
677
|
+
// PRIORITY 3: Most recent session from local cache index
|
|
678
|
+
const recentSession = (0, state_1.getMostRecentSession)();
|
|
679
|
+
if (recentSession) {
|
|
680
|
+
sessionToRestore = recentSession.sessionName;
|
|
681
|
+
dlog(`Using most recent session from cache: ${sessionToRestore} (${recentSession.lastActive})`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// PRIORITY 4: Persisted state (last resort)
|
|
609
685
|
if (!sessionToRestore) {
|
|
610
686
|
const state = (0, state_1.getState)();
|
|
611
687
|
sessionToRestore = state?.sessionName || null;
|
|
@@ -615,7 +691,7 @@ async function run(options) {
|
|
|
615
691
|
sessionToRestore = (0, state_1.uuidToWords)(sessionId);
|
|
616
692
|
}
|
|
617
693
|
if (sessionToRestore) {
|
|
618
|
-
dlog(`Using persisted state (
|
|
694
|
+
dlog(`Using persisted state (last resort): ${sessionToRestore}`);
|
|
619
695
|
}
|
|
620
696
|
}
|
|
621
697
|
const sessionDisplay = sessionToRestore || 'unknown-session';
|
|
@@ -729,8 +805,9 @@ async function run(options) {
|
|
|
729
805
|
lastSeenSessionName = detectedSession;
|
|
730
806
|
lastSeenSessionAt = Date.now();
|
|
731
807
|
currentSession = lastSeenSessionName;
|
|
808
|
+
observedSessionThisRun = true; // Mark that we've seen a session in THIS process
|
|
732
809
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
733
|
-
dlog(`Session detected from status line: ${currentSession}`);
|
|
810
|
+
dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
|
|
734
811
|
}
|
|
735
812
|
else {
|
|
736
813
|
// Same session, just update timestamp
|
|
@@ -746,8 +823,9 @@ async function run(options) {
|
|
|
746
823
|
lastSeenSessionName = detectedSession;
|
|
747
824
|
lastSeenSessionAt = Date.now();
|
|
748
825
|
currentSession = lastSeenSessionName;
|
|
826
|
+
observedSessionThisRun = true; // Mark that we've seen a session in THIS process
|
|
749
827
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
750
|
-
dlog(`Session detected from generic match: ${currentSession}`);
|
|
828
|
+
dlog(`Session detected from generic match: ${currentSession} (observedSessionThisRun=true)`);
|
|
751
829
|
}
|
|
752
830
|
else {
|
|
753
831
|
lastSeenSessionAt = Date.now();
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const init_1 = require("./commands/init");
|
|
|
9
9
|
const test_1 = require("./commands/test");
|
|
10
10
|
const status_1 = require("./commands/status");
|
|
11
11
|
const run_1 = require("./commands/run");
|
|
12
|
+
const doctor_1 = require("./commands/doctor");
|
|
12
13
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
14
|
commander_1.program
|
|
14
15
|
.name('ekkos')
|
|
@@ -42,13 +43,24 @@ commander_1.program
|
|
|
42
43
|
.option('-s, --session <name>', 'Session name to restore on clear')
|
|
43
44
|
.option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
|
|
44
45
|
.option('-v, --verbose', 'Show debug output')
|
|
46
|
+
.option('-d, --doctor', 'Run diagnostics before starting')
|
|
45
47
|
.action((options) => {
|
|
46
48
|
(0, run_1.run)({
|
|
47
49
|
session: options.session,
|
|
48
50
|
bypass: options.bypass,
|
|
49
|
-
verbose: options.verbose
|
|
51
|
+
verbose: options.verbose,
|
|
52
|
+
doctor: options.doctor
|
|
50
53
|
});
|
|
51
54
|
});
|
|
55
|
+
// Doctor command - check system prerequisites
|
|
56
|
+
commander_1.program
|
|
57
|
+
.command('doctor')
|
|
58
|
+
.description('Check system prerequisites for ekkOS (Node, PTY, Claude, MCP)')
|
|
59
|
+
.option('-f, --fix', 'Attempt safe auto-fixes and show commands for manual fixes')
|
|
60
|
+
.option('-j, --json', 'Output machine-readable JSON report')
|
|
61
|
+
.action((options) => {
|
|
62
|
+
(0, doctor_1.doctor)({ fix: options.fix, json: options.json });
|
|
63
|
+
});
|
|
52
64
|
// Deprecated setup command (redirects to init)
|
|
53
65
|
commander_1.program
|
|
54
66
|
.command('setup')
|
package/dist/utils/state.d.ts
CHANGED
|
@@ -55,3 +55,12 @@ export declare function parseAutoClearFlag(): {
|
|
|
55
55
|
session: string;
|
|
56
56
|
timestamp: number;
|
|
57
57
|
} | null;
|
|
58
|
+
/**
|
|
59
|
+
* Get the most recent session from the local cache index
|
|
60
|
+
* Used when no session was observed in the current process run
|
|
61
|
+
*/
|
|
62
|
+
export declare function getMostRecentSession(): {
|
|
63
|
+
sessionName: string;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
lastActive: string;
|
|
66
|
+
} | null;
|
package/dist/utils/state.js
CHANGED
|
@@ -46,6 +46,7 @@ exports.getConfig = getConfig;
|
|
|
46
46
|
exports.getAuthToken = getAuthToken;
|
|
47
47
|
exports.clearAutoClearFlag = clearAutoClearFlag;
|
|
48
48
|
exports.parseAutoClearFlag = parseAutoClearFlag;
|
|
49
|
+
exports.getMostRecentSession = getMostRecentSession;
|
|
49
50
|
const fs = __importStar(require("fs"));
|
|
50
51
|
const path = __importStar(require("path"));
|
|
51
52
|
const os = __importStar(require("os"));
|
|
@@ -184,3 +185,38 @@ function parseAutoClearFlag() {
|
|
|
184
185
|
}
|
|
185
186
|
return null;
|
|
186
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Get the most recent session from the local cache index
|
|
190
|
+
* Used when no session was observed in the current process run
|
|
191
|
+
*/
|
|
192
|
+
function getMostRecentSession() {
|
|
193
|
+
const indexPath = path.join(exports.EKKOS_DIR, 'cache', 'sessions', 'index.json');
|
|
194
|
+
try {
|
|
195
|
+
if (!fs.existsSync(indexPath)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const indexContent = fs.readFileSync(indexPath, 'utf-8');
|
|
199
|
+
const index = JSON.parse(indexContent);
|
|
200
|
+
// Find the most recently active session
|
|
201
|
+
let mostRecent = null;
|
|
202
|
+
let latestTime = 0;
|
|
203
|
+
for (const [sessionName, data] of Object.entries(index)) {
|
|
204
|
+
// Skip test sessions
|
|
205
|
+
if (sessionName.startsWith('test-'))
|
|
206
|
+
continue;
|
|
207
|
+
const activeTime = new Date(data.last_active_ts).getTime();
|
|
208
|
+
if (activeTime > latestTime) {
|
|
209
|
+
latestTime = activeTime;
|
|
210
|
+
mostRecent = {
|
|
211
|
+
sessionName,
|
|
212
|
+
sessionId: data.session_id,
|
|
213
|
+
lastActive: data.last_active_ts
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return mostRecent;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|