@ekkos/cli 0.2.16 → 0.2.18
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/agent/daemon.d.ts +18 -1
- package/dist/agent/daemon.js +91 -7
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +4 -3
- package/dist/commands/run.js +94 -4
- package/dist/commands/setup-remote.js +4 -4
- package/dist/commands/setup.js +93 -16
- package/package.json +1 -1
- package/templates/agents/railway-manager.md +38 -2
- package/templates/ekkos-manifest.json +1 -1
- package/templates/hooks/user-prompt-submit.sh +6 -0
package/dist/agent/daemon.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export declare class AgentDaemon {
|
|
|
21
21
|
private ptyRunner;
|
|
22
22
|
private currentSessionId;
|
|
23
23
|
private running;
|
|
24
|
+
private outputBuffer;
|
|
25
|
+
private currentSessionName;
|
|
26
|
+
private isAutoClearInProgress;
|
|
27
|
+
private lastContextWallTime;
|
|
28
|
+
private readonly CONTEXT_WALL_COOLDOWN;
|
|
24
29
|
constructor(config: DaemonConfig);
|
|
25
30
|
/**
|
|
26
31
|
* Start the daemon
|
|
@@ -63,9 +68,21 @@ export declare class AgentDaemon {
|
|
|
63
68
|
*/
|
|
64
69
|
private handlePTYExit;
|
|
65
70
|
/**
|
|
66
|
-
* Send PTY output to server
|
|
71
|
+
* Send PTY output to server (with auto-continue detection)
|
|
67
72
|
*/
|
|
68
73
|
private sendOutput;
|
|
74
|
+
/**
|
|
75
|
+
* Trigger auto /clear + /continue when context wall is hit
|
|
76
|
+
*/
|
|
77
|
+
private triggerAutoContinue;
|
|
78
|
+
/**
|
|
79
|
+
* Wait for Claude's idle prompt ("> ")
|
|
80
|
+
*/
|
|
81
|
+
private waitForIdlePrompt;
|
|
82
|
+
/**
|
|
83
|
+
* Sleep helper
|
|
84
|
+
*/
|
|
85
|
+
private sleep;
|
|
69
86
|
/**
|
|
70
87
|
* Handle WebSocket close
|
|
71
88
|
*/
|
package/dist/agent/daemon.js
CHANGED
|
@@ -51,9 +51,15 @@ const os = __importStar(require("os"));
|
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
53
|
const pty_runner_1 = require("./pty-runner");
|
|
54
|
-
const RELAY_URL = process.env.RELAY_WS_URL || 'wss://
|
|
54
|
+
const RELAY_URL = process.env.RELAY_WS_URL || 'wss://ekkos-relay-production.up.railway.app';
|
|
55
55
|
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
56
56
|
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 32000, 60000]; // Exponential backoff
|
|
57
|
+
// Auto-continue: Context wall detection pattern
|
|
58
|
+
const CONTEXT_WALL_REGEX = /context limit reached.*\/(compact|clear)\b.*to continue/i;
|
|
59
|
+
// Session name detection pattern (3-word slug: word-word-word)
|
|
60
|
+
const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
|
|
61
|
+
// Idle prompt detection - Claude shows "> " when ready for input
|
|
62
|
+
const IDLE_PROMPT_REGEX = />\s*$/;
|
|
57
63
|
class AgentDaemon {
|
|
58
64
|
constructor(config) {
|
|
59
65
|
this.ws = null;
|
|
@@ -62,6 +68,12 @@ class AgentDaemon {
|
|
|
62
68
|
this.ptyRunner = null;
|
|
63
69
|
this.currentSessionId = null;
|
|
64
70
|
this.running = false;
|
|
71
|
+
// Auto-continue state
|
|
72
|
+
this.outputBuffer = '';
|
|
73
|
+
this.currentSessionName = null;
|
|
74
|
+
this.isAutoClearInProgress = false;
|
|
75
|
+
this.lastContextWallTime = 0;
|
|
76
|
+
this.CONTEXT_WALL_COOLDOWN = 30000; // 30 seconds between auto-clears
|
|
65
77
|
this.config = config;
|
|
66
78
|
}
|
|
67
79
|
/**
|
|
@@ -140,7 +152,7 @@ class AgentDaemon {
|
|
|
140
152
|
this.log('Registered with relay');
|
|
141
153
|
break;
|
|
142
154
|
case 'session_start':
|
|
143
|
-
this.handleSessionStart(message.sessionId);
|
|
155
|
+
this.handleSessionStart(message.sessionId, message.cwd);
|
|
144
156
|
break;
|
|
145
157
|
case 'session_end':
|
|
146
158
|
this.handleSessionEnd(message.sessionId);
|
|
@@ -159,20 +171,26 @@ class AgentDaemon {
|
|
|
159
171
|
/**
|
|
160
172
|
* Handle session start request
|
|
161
173
|
*/
|
|
162
|
-
handleSessionStart(sessionId) {
|
|
163
|
-
this.log(`Session start request: ${sessionId}`);
|
|
174
|
+
handleSessionStart(sessionId, cwd) {
|
|
175
|
+
this.log(`Session start request: ${sessionId}${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
164
176
|
// Kill existing session if any
|
|
165
177
|
if (this.ptyRunner) {
|
|
166
178
|
this.ptyRunner.kill();
|
|
167
179
|
this.ptyRunner = null;
|
|
168
180
|
}
|
|
169
181
|
this.currentSessionId = sessionId;
|
|
170
|
-
//
|
|
182
|
+
// Reset auto-continue state
|
|
183
|
+
this.outputBuffer = '';
|
|
184
|
+
this.currentSessionName = null;
|
|
185
|
+
this.isAutoClearInProgress = false;
|
|
186
|
+
this.lastContextWallTime = 0;
|
|
187
|
+
// Start PTY with ekkos run (skip -d flag to avoid double init)
|
|
171
188
|
this.ptyRunner = new pty_runner_1.PTYRunner({
|
|
172
189
|
command: 'ekkos',
|
|
173
|
-
args: ['run', '-
|
|
190
|
+
args: ['run', '-b'], // -b for bypass permissions (skip -d to avoid double spawn)
|
|
174
191
|
onData: (data) => this.sendOutput(data),
|
|
175
192
|
onExit: (code) => this.handlePTYExit(code),
|
|
193
|
+
cwd: cwd || process.env.HOME, // Use specified cwd or fall back to home
|
|
176
194
|
verbose: this.config.verbose,
|
|
177
195
|
});
|
|
178
196
|
this.ptyRunner.start();
|
|
@@ -225,14 +243,80 @@ class AgentDaemon {
|
|
|
225
243
|
this.currentSessionId = null;
|
|
226
244
|
}
|
|
227
245
|
/**
|
|
228
|
-
* Send PTY output to server
|
|
246
|
+
* Send PTY output to server (with auto-continue detection)
|
|
229
247
|
*/
|
|
230
248
|
sendOutput(data) {
|
|
249
|
+
// Always forward output to server
|
|
231
250
|
this.sendMessage({
|
|
232
251
|
type: 'output',
|
|
233
252
|
sessionId: this.currentSessionId || undefined,
|
|
234
253
|
data,
|
|
235
254
|
});
|
|
255
|
+
// Auto-continue: Buffer output and detect context wall
|
|
256
|
+
this.outputBuffer += data;
|
|
257
|
+
// Keep buffer manageable (last 2KB)
|
|
258
|
+
if (this.outputBuffer.length > 2048) {
|
|
259
|
+
this.outputBuffer = this.outputBuffer.slice(-2048);
|
|
260
|
+
}
|
|
261
|
+
// Extract session name from output (e.g., "qix-fox-use" in hook output)
|
|
262
|
+
const sessionMatch = this.outputBuffer.match(SESSION_NAME_REGEX);
|
|
263
|
+
if (sessionMatch) {
|
|
264
|
+
this.currentSessionName = sessionMatch[1];
|
|
265
|
+
}
|
|
266
|
+
// Detect context wall
|
|
267
|
+
if (CONTEXT_WALL_REGEX.test(this.outputBuffer) && !this.isAutoClearInProgress) {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
if (now - this.lastContextWallTime > this.CONTEXT_WALL_COOLDOWN) {
|
|
270
|
+
this.lastContextWallTime = now;
|
|
271
|
+
this.triggerAutoContinue();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Trigger auto /clear + /continue when context wall is hit
|
|
277
|
+
*/
|
|
278
|
+
async triggerAutoContinue() {
|
|
279
|
+
if (this.isAutoClearInProgress || !this.ptyRunner)
|
|
280
|
+
return;
|
|
281
|
+
this.isAutoClearInProgress = true;
|
|
282
|
+
this.log('Auto-continue: Context wall detected, initiating /clear + /continue');
|
|
283
|
+
// Wait for idle prompt
|
|
284
|
+
await this.waitForIdlePrompt();
|
|
285
|
+
// Type /clear
|
|
286
|
+
this.log('Auto-continue: Sending /clear');
|
|
287
|
+
this.ptyRunner.write('/clear\n');
|
|
288
|
+
// Wait for clear to complete
|
|
289
|
+
await this.sleep(2000);
|
|
290
|
+
await this.waitForIdlePrompt();
|
|
291
|
+
// Type /continue with session name
|
|
292
|
+
const continueCmd = this.currentSessionName
|
|
293
|
+
? `/continue ${this.currentSessionName}\n`
|
|
294
|
+
: '/continue\n';
|
|
295
|
+
this.log(`Auto-continue: Sending ${continueCmd.trim()}`);
|
|
296
|
+
this.ptyRunner.write(continueCmd);
|
|
297
|
+
// Reset state
|
|
298
|
+
this.outputBuffer = '';
|
|
299
|
+
this.isAutoClearInProgress = false;
|
|
300
|
+
this.log('Auto-continue: Complete');
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Wait for Claude's idle prompt ("> ")
|
|
304
|
+
*/
|
|
305
|
+
async waitForIdlePrompt(timeout = 10000) {
|
|
306
|
+
const startTime = Date.now();
|
|
307
|
+
while (Date.now() - startTime < timeout) {
|
|
308
|
+
if (IDLE_PROMPT_REGEX.test(this.outputBuffer)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await this.sleep(100);
|
|
312
|
+
}
|
|
313
|
+
this.log('Auto-continue: Timeout waiting for idle prompt');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Sleep helper
|
|
317
|
+
*/
|
|
318
|
+
sleep(ms) {
|
|
319
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
236
320
|
}
|
|
237
321
|
/**
|
|
238
322
|
* Handle WebSocket close
|
package/dist/agent/pty-runner.js
CHANGED
|
@@ -82,7 +82,7 @@ class PTYRunner {
|
|
|
82
82
|
name: 'xterm-256color',
|
|
83
83
|
cols: this.config.cols || 80,
|
|
84
84
|
rows: this.config.rows || 24,
|
|
85
|
-
cwd: process.cwd(),
|
|
85
|
+
cwd: this.config.cwd || process.cwd(),
|
|
86
86
|
env: {
|
|
87
87
|
...process.env,
|
|
88
88
|
TERM: 'xterm-256color',
|
|
@@ -101,11 +101,12 @@ class PTYRunner {
|
|
|
101
101
|
*/
|
|
102
102
|
startWithSpawn() {
|
|
103
103
|
const isWindows = os.platform() === 'win32';
|
|
104
|
+
const cwd = this.config.cwd || process.cwd();
|
|
104
105
|
if (isWindows) {
|
|
105
106
|
// Windows: Use cmd.exe
|
|
106
107
|
this.spawnProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', this.config.command, ...this.config.args], {
|
|
107
108
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
-
cwd
|
|
109
|
+
cwd,
|
|
109
110
|
env: process.env,
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -116,7 +117,7 @@ class PTYRunner {
|
|
|
116
117
|
: ['-q', '-c', `${this.config.command} ${this.config.args.join(' ')}`, '/dev/null'];
|
|
117
118
|
this.spawnProcess = (0, child_process_1.spawn)('script', scriptArgs, {
|
|
118
119
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
-
cwd
|
|
120
|
+
cwd,
|
|
120
121
|
env: {
|
|
121
122
|
...process.env,
|
|
122
123
|
TERM: 'xterm-256color',
|
package/dist/commands/run.js
CHANGED
|
@@ -256,15 +256,105 @@ const MEMORY_API_URL = 'https://mcp.ekkos.dev';
|
|
|
256
256
|
const isWindows = os.platform() === 'win32';
|
|
257
257
|
// Pinned Claude Code version for ekkos run
|
|
258
258
|
// 2.1.6 has the old context calculation (95% of full 200K, not effective window)
|
|
259
|
+
// NOTE: Homebrew global installs may be broken, but npm installs work fine
|
|
259
260
|
const PINNED_CLAUDE_VERSION = '2.1.6';
|
|
261
|
+
// ekkOS-managed Claude installation path
|
|
262
|
+
const EKKOS_CLAUDE_DIR = path.join(os.homedir(), '.ekkos', 'claude-code');
|
|
263
|
+
const EKKOS_CLAUDE_BIN = path.join(EKKOS_CLAUDE_DIR, 'node_modules', '.bin', 'claude');
|
|
264
|
+
/**
|
|
265
|
+
* Check if a Claude installation matches our required version
|
|
266
|
+
*/
|
|
267
|
+
function checkClaudeVersion(claudePath) {
|
|
268
|
+
try {
|
|
269
|
+
const version = (0, child_process_1.execSync)(`"${claudePath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
270
|
+
// Version output is like "2.1.6 (Claude Code)" - extract the version number
|
|
271
|
+
const match = version.match(/^(\d+\.\d+\.\d+)/);
|
|
272
|
+
if (match) {
|
|
273
|
+
return match[1] === PINNED_CLAUDE_VERSION;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Install Claude Code to ekkOS-managed directory
|
|
283
|
+
* This gives us full control over the version without npx auto-update messages
|
|
284
|
+
*/
|
|
285
|
+
function installEkkosClaudeVersion() {
|
|
286
|
+
console.log(chalk_1.default.cyan(`\n📦 Installing Claude Code v${PINNED_CLAUDE_VERSION} to ~/.ekkos/claude-code...`));
|
|
287
|
+
console.log(chalk_1.default.gray(' (This is a one-time setup for optimal context window behavior)\n'));
|
|
288
|
+
try {
|
|
289
|
+
// Create directory if needed
|
|
290
|
+
if (!fs.existsSync(EKKOS_CLAUDE_DIR)) {
|
|
291
|
+
fs.mkdirSync(EKKOS_CLAUDE_DIR, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
// Initialize package.json if needed
|
|
294
|
+
const packageJsonPath = path.join(EKKOS_CLAUDE_DIR, 'package.json');
|
|
295
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
296
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify({
|
|
297
|
+
name: 'ekkos-claude-code',
|
|
298
|
+
version: '1.0.0',
|
|
299
|
+
private: true,
|
|
300
|
+
description: 'ekkOS-managed Claude Code installation'
|
|
301
|
+
}, null, 2));
|
|
302
|
+
}
|
|
303
|
+
// Install specific version
|
|
304
|
+
(0, child_process_1.execSync)(`npm install @anthropic-ai/claude-code@${PINNED_CLAUDE_VERSION}`, {
|
|
305
|
+
cwd: EKKOS_CLAUDE_DIR,
|
|
306
|
+
stdio: 'inherit'
|
|
307
|
+
});
|
|
308
|
+
console.log(chalk_1.default.green(`\n✓ Claude Code v${PINNED_CLAUDE_VERSION} installed successfully!`));
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.error(chalk_1.default.red(`\n✗ Failed to install Claude Code: ${err.message}`));
|
|
313
|
+
console.log(chalk_1.default.yellow(' Falling back to npx (will show update message)\n'));
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
260
317
|
/**
|
|
261
318
|
* Resolve full path to claude executable
|
|
262
|
-
* Returns
|
|
319
|
+
* Returns direct path if found with correct version, otherwise 'npx:VERSION'
|
|
320
|
+
*
|
|
321
|
+
* IMPORTANT: We MUST use version 2.1.6 specifically because Anthropic changed
|
|
322
|
+
* context window calculation after this version. 2.1.6 uses 95% of full 200K,
|
|
323
|
+
* newer versions use a different (more restrictive) effective window.
|
|
324
|
+
*
|
|
325
|
+
* Priority:
|
|
326
|
+
* 1. ekkOS-managed installation (~/.ekkos/claude-code) - CLEANEST, auto-installed
|
|
327
|
+
* 2. Homebrew/global install IF version matches 2.1.6
|
|
328
|
+
* 3. npx with pinned version (fallback, shows update message)
|
|
263
329
|
*/
|
|
264
330
|
function resolveClaudePath() {
|
|
265
|
-
// PRIORITY 1:
|
|
266
|
-
|
|
267
|
-
|
|
331
|
+
// PRIORITY 1: ekkOS-managed installation (cleanest - no update messages)
|
|
332
|
+
if (fs.existsSync(EKKOS_CLAUDE_BIN) && checkClaudeVersion(EKKOS_CLAUDE_BIN)) {
|
|
333
|
+
return EKKOS_CLAUDE_BIN;
|
|
334
|
+
}
|
|
335
|
+
// PRIORITY 2: Check Homebrew and global installations - only use if version matches
|
|
336
|
+
const candidatePaths = [
|
|
337
|
+
// Homebrew
|
|
338
|
+
'/opt/homebrew/bin/claude', // macOS Apple Silicon
|
|
339
|
+
'/usr/local/bin/claude', // macOS Intel
|
|
340
|
+
'/home/linuxbrew/.linuxbrew/bin/claude', // Linux (system)
|
|
341
|
+
path.join(os.homedir(), '.linuxbrew/bin/claude'), // Linux (user)
|
|
342
|
+
// Global npm install
|
|
343
|
+
path.join(os.homedir(), '.npm-global/bin/claude'),
|
|
344
|
+
path.join(os.homedir(), '.local/bin/claude'),
|
|
345
|
+
];
|
|
346
|
+
for (const p of candidatePaths) {
|
|
347
|
+
if (fs.existsSync(p) && checkClaudeVersion(p)) {
|
|
348
|
+
return p; // Direct path with correct version
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// PRIORITY 3: Auto-install to ekkOS-managed directory
|
|
352
|
+
if (installEkkosClaudeVersion()) {
|
|
353
|
+
if (fs.existsSync(EKKOS_CLAUDE_BIN)) {
|
|
354
|
+
return EKKOS_CLAUDE_BIN;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// PRIORITY 4: Fall back to npx with pinned version (shows update message)
|
|
268
358
|
return `npx:${PINNED_CLAUDE_VERSION}`;
|
|
269
359
|
}
|
|
270
360
|
/**
|
|
@@ -61,7 +61,7 @@ const init_1 = require("./init");
|
|
|
61
61
|
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
62
62
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
63
63
|
const PLATFORM_URL = process.env.PLATFORM_URL || 'https://platform.ekkos.dev';
|
|
64
|
-
const
|
|
64
|
+
const RELAY_API_URL = process.env.RELAY_URL || 'https://ekkos-relay-production.up.railway.app';
|
|
65
65
|
const POLLING_INTERVAL = 2000; // 2 seconds
|
|
66
66
|
const POLLING_TIMEOUT = 600000; // 10 minutes
|
|
67
67
|
/**
|
|
@@ -110,7 +110,7 @@ function getOrCreateDeviceInfo() {
|
|
|
110
110
|
* Request device pairing code from server
|
|
111
111
|
*/
|
|
112
112
|
async function requestPairingCode(deviceInfo, authToken) {
|
|
113
|
-
const response = await fetch(`${
|
|
113
|
+
const response = await fetch(`${RELAY_API_URL}/api/v1/relay/pair`, {
|
|
114
114
|
method: 'POST',
|
|
115
115
|
headers: {
|
|
116
116
|
'Authorization': `Bearer ${authToken}`,
|
|
@@ -140,7 +140,7 @@ async function requestPairingCode(deviceInfo, authToken) {
|
|
|
140
140
|
async function pollForApproval(deviceCode, authToken) {
|
|
141
141
|
const startTime = Date.now();
|
|
142
142
|
while (Date.now() - startTime < POLLING_TIMEOUT) {
|
|
143
|
-
const response = await fetch(`${
|
|
143
|
+
const response = await fetch(`${RELAY_API_URL}/api/v1/relay/pair/${deviceCode}`, {
|
|
144
144
|
headers: {
|
|
145
145
|
'Authorization': `Bearer ${authToken}`,
|
|
146
146
|
},
|
|
@@ -338,7 +338,7 @@ async function verifyConnection(deviceInfo, authToken) {
|
|
|
338
338
|
// Wait a bit for agent to start
|
|
339
339
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
340
340
|
// Check device status
|
|
341
|
-
const response = await fetch(`${
|
|
341
|
+
const response = await fetch(`${RELAY_API_URL}/api/v1/relay/devices/${(await (0, state_1.getState)())?.userId}`, {
|
|
342
342
|
headers: {
|
|
343
343
|
'Authorization': `Bearer ${authToken}`,
|
|
344
344
|
},
|
package/dist/commands/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ const fs_1 = require("fs");
|
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
12
12
|
const ora_1 = __importDefault(require("ora"));
|
|
13
|
+
const hooks_js_1 = require("./hooks.js");
|
|
13
14
|
const EKKOS_API_URL = 'https://mcp.ekkos.dev';
|
|
14
15
|
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.ekkos');
|
|
15
16
|
const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
|
|
@@ -137,6 +138,7 @@ async function setup(options) {
|
|
|
137
138
|
message: 'Which IDE are you setting up?',
|
|
138
139
|
choices: [
|
|
139
140
|
{ name: 'Claude Code (CLI)', value: 'claude-code' },
|
|
141
|
+
{ name: 'Claude Desktop (MCP)', value: 'claude-desktop' },
|
|
140
142
|
{ name: 'Cursor', value: 'cursor' },
|
|
141
143
|
{ name: 'Windsurf (Cascade)', value: 'windsurf' },
|
|
142
144
|
{ name: 'VSCode (Copilot)', value: 'vscode' }
|
|
@@ -180,6 +182,9 @@ async function setupIDE(ide, apiKey, config) {
|
|
|
180
182
|
case 'claude-code':
|
|
181
183
|
await setupClaudeCode(apiKey);
|
|
182
184
|
break;
|
|
185
|
+
case 'claude-desktop':
|
|
186
|
+
await setupClaudeDesktop(apiKey);
|
|
187
|
+
break;
|
|
183
188
|
case 'cursor':
|
|
184
189
|
await setupCursor(apiKey);
|
|
185
190
|
break;
|
|
@@ -203,24 +208,74 @@ async function setupIDE(ide, apiKey, config) {
|
|
|
203
208
|
async function setupClaudeCode(apiKey) {
|
|
204
209
|
const claudeDir = (0, path_1.join)((0, os_1.homedir)(), '.claude');
|
|
205
210
|
const hooksDir = (0, path_1.join)(claudeDir, 'hooks');
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
const stateDir = (0, path_1.join)(claudeDir, 'state');
|
|
212
|
+
// Create directories
|
|
213
|
+
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
214
|
+
(0, fs_1.mkdirSync)(hooksDir, { recursive: true });
|
|
215
|
+
(0, fs_1.mkdirSync)(stateDir, { recursive: true });
|
|
216
|
+
// Check for existing custom hooks (don't have EKKOS_MANAGED=1 marker)
|
|
217
|
+
const isWindows = (0, os_1.platform)() === 'win32';
|
|
218
|
+
const hookExt = isWindows ? '.ps1' : '.sh';
|
|
219
|
+
const hookFiles = ['user-prompt-submit', 'stop', 'session-start', 'assistant-response'];
|
|
220
|
+
let hasCustomHooks = false;
|
|
221
|
+
for (const hookName of hookFiles) {
|
|
222
|
+
const hookPath = (0, path_1.join)(hooksDir, `${hookName}${hookExt}`);
|
|
223
|
+
if ((0, fs_1.existsSync)(hookPath)) {
|
|
224
|
+
const content = (0, fs_1.readFileSync)(hookPath, 'utf-8');
|
|
225
|
+
if (!content.includes('EKKOS_MANAGED=1')) {
|
|
226
|
+
hasCustomHooks = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (hasCustomHooks) {
|
|
232
|
+
// User has custom hooks - don't overwrite, use minimal approach
|
|
233
|
+
console.log(chalk_1.default.yellow(' Detected custom hooks - preserving your hooks'));
|
|
234
|
+
console.log(chalk_1.default.gray(' Run `ekkos hooks install --global` to upgrade to managed hooks'));
|
|
235
|
+
console.log(chalk_1.default.gray(' (This will overwrite existing hooks with full-featured versions)'));
|
|
236
|
+
// Still save API key for existing hooks to use
|
|
208
237
|
}
|
|
238
|
+
else {
|
|
239
|
+
// No custom hooks OR all managed - safe to install full templates
|
|
240
|
+
try {
|
|
241
|
+
await (0, hooks_js_1.hooksInstall)({ global: true, verbose: false });
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
// Fallback: if manifest-driven install fails, generate basic hooks
|
|
245
|
+
console.log(chalk_1.default.yellow(' Note: Could not install hooks from templates'));
|
|
246
|
+
console.log(chalk_1.default.gray(' Generating basic hooks instead...'));
|
|
247
|
+
await generateBasicHooks(hooksDir, apiKey);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Save API key to config for hooks to use
|
|
251
|
+
const ekkosConfigDir = (0, path_1.join)((0, os_1.homedir)(), '.ekkos');
|
|
252
|
+
(0, fs_1.mkdirSync)(ekkosConfigDir, { recursive: true });
|
|
253
|
+
const configPath = (0, path_1.join)(ekkosConfigDir, 'config.json');
|
|
254
|
+
let existingConfig = {};
|
|
255
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
256
|
+
try {
|
|
257
|
+
existingConfig = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
|
|
258
|
+
}
|
|
259
|
+
catch { }
|
|
260
|
+
}
|
|
261
|
+
// Update config with API key (hookApiKey for hooks, apiKey for compatibility)
|
|
262
|
+
existingConfig.hookApiKey = apiKey;
|
|
263
|
+
existingConfig.apiKey = apiKey;
|
|
264
|
+
existingConfig.updatedAt = new Date().toISOString();
|
|
265
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(existingConfig, null, 2));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Generate basic inline hooks as fallback when templates aren't available
|
|
269
|
+
*/
|
|
270
|
+
async function generateBasicHooks(hooksDir, apiKey) {
|
|
209
271
|
const isWindows = (0, os_1.platform)() === 'win32';
|
|
210
272
|
if (isWindows) {
|
|
211
|
-
// Windows: PowerShell hooks
|
|
212
273
|
const promptSubmitHook = generatePromptSubmitHookPS(apiKey);
|
|
213
|
-
|
|
214
|
-
(0, fs_1.writeFileSync)(promptSubmitPath, promptSubmitHook);
|
|
274
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(hooksDir, 'user-prompt-submit.ps1'), promptSubmitHook);
|
|
215
275
|
const stopHook = generateStopHookPS(apiKey);
|
|
216
|
-
|
|
217
|
-
(0, fs_1.writeFileSync)(stopPath, stopHook);
|
|
218
|
-
// Create wrapper batch files for Windows
|
|
219
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(hooksDir, 'user-prompt-submit.cmd'), `@echo off\npowershell -ExecutionPolicy Bypass -File "%~dp0user-prompt-submit.ps1"`);
|
|
220
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(hooksDir, 'stop.cmd'), `@echo off\npowershell -ExecutionPolicy Bypass -File "%~dp0stop.ps1"`);
|
|
276
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(hooksDir, 'stop.ps1'), stopHook);
|
|
221
277
|
}
|
|
222
278
|
else {
|
|
223
|
-
// Unix: Bash hooks
|
|
224
279
|
const promptSubmitHook = generatePromptSubmitHook(apiKey);
|
|
225
280
|
const promptSubmitPath = (0, path_1.join)(hooksDir, 'user-prompt-submit.sh');
|
|
226
281
|
(0, fs_1.writeFileSync)(promptSubmitPath, promptSubmitHook);
|
|
@@ -230,11 +285,6 @@ async function setupClaudeCode(apiKey) {
|
|
|
230
285
|
(0, fs_1.writeFileSync)(stopPath, stopHook);
|
|
231
286
|
(0, fs_1.chmodSync)(stopPath, '755');
|
|
232
287
|
}
|
|
233
|
-
// Create state directory
|
|
234
|
-
const stateDir = (0, path_1.join)(claudeDir, 'state');
|
|
235
|
-
if (!(0, fs_1.existsSync)(stateDir)) {
|
|
236
|
-
(0, fs_1.mkdirSync)(stateDir, { recursive: true });
|
|
237
|
-
}
|
|
238
288
|
}
|
|
239
289
|
async function setupCursor(apiKey) {
|
|
240
290
|
// Cursor uses .cursorrules for system prompt
|
|
@@ -320,6 +370,33 @@ async function setupVSCode(apiKey) {
|
|
|
320
370
|
// Note: Full VSCode integration requires extension
|
|
321
371
|
console.log(chalk_1.default.yellow(' Note: VSCode requires the ekkOS extension for full integration'));
|
|
322
372
|
}
|
|
373
|
+
async function setupClaudeDesktop(apiKey) {
|
|
374
|
+
// Claude Desktop - configure MCP server
|
|
375
|
+
const claudeDir = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'Application Support', 'Claude');
|
|
376
|
+
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
377
|
+
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
378
|
+
}
|
|
379
|
+
// Create/update MCP config
|
|
380
|
+
const configPath = (0, path_1.join)(claudeDir, 'claude_desktop_config.json');
|
|
381
|
+
let config = {};
|
|
382
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
383
|
+
try {
|
|
384
|
+
config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
|
|
385
|
+
}
|
|
386
|
+
catch { }
|
|
387
|
+
}
|
|
388
|
+
// Preserve existing preferences
|
|
389
|
+
config.mcpServers = config.mcpServers || {};
|
|
390
|
+
config.mcpServers['ekkos-memory'] = {
|
|
391
|
+
command: 'npx',
|
|
392
|
+
args: ['-y', '@ekkos/mcp-server@latest'],
|
|
393
|
+
env: {
|
|
394
|
+
EKKOS_API_KEY: apiKey
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
398
|
+
console.log(chalk_1.default.yellow(' Note: Restart Claude Desktop to load the MCP server'));
|
|
399
|
+
}
|
|
323
400
|
function generatePromptSubmitHook(apiKey) {
|
|
324
401
|
return `#!/bin/bash
|
|
325
402
|
# ekkOS Hook: UserPromptSubmit
|
package/package.json
CHANGED
|
@@ -68,10 +68,30 @@ railway variables set KEY=value -s pm2-workers
|
|
|
68
68
|
|
|
69
69
|
## EKKOS SERVICES
|
|
70
70
|
|
|
71
|
-
### Railway
|
|
72
|
-
**Project**: imaginative-vision
|
|
71
|
+
### Railway Project: `imaginative-vision`
|
|
73
72
|
**Environment**: production
|
|
74
73
|
|
|
74
|
+
There are **3 services** in this Railway project:
|
|
75
|
+
|
|
76
|
+
| Service | Purpose | Domain |
|
|
77
|
+
|---------|---------|--------|
|
|
78
|
+
| `ekkos-relay` | WebSocket relay for remote terminal | relay.ekkos.dev |
|
|
79
|
+
| `pm2-workers` | PM2-managed background workers | pm2.ekkos.dev |
|
|
80
|
+
| `cloud-terminal` | Cloud PTY service for browser terminals | (internal) |
|
|
81
|
+
|
|
82
|
+
### Service: `ekkos-relay`
|
|
83
|
+
WebSocket relay server connecting browsers to devices/cloud terminals.
|
|
84
|
+
- Source: `apps/relay/`
|
|
85
|
+
- Health: `/health`
|
|
86
|
+
- Endpoints: `/api/v1/relay/device`, `/api/v1/relay/browser`, `/api/v1/relay/cloud`
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
railway logs -s ekkos-relay --lines 50
|
|
90
|
+
railway up -s ekkos-relay
|
|
91
|
+
railway redeploy -s ekkos-relay
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Service: `pm2-workers`
|
|
75
95
|
PM2-managed workers:
|
|
76
96
|
| Worker | Purpose |
|
|
77
97
|
|--------|---------|
|
|
@@ -79,6 +99,22 @@ PM2-managed workers:
|
|
|
79
99
|
| `working-memory-processor` | WM → DB batch sync |
|
|
80
100
|
| `slow-loop-processor` | Pattern extraction (if enabled) |
|
|
81
101
|
|
|
102
|
+
```bash
|
|
103
|
+
railway logs -s pm2-workers --lines 50
|
|
104
|
+
railway run -s pm2-workers -- pm2 status
|
|
105
|
+
railway run -s pm2-workers -- pm2 restart all
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Service: `cloud-terminal`
|
|
109
|
+
Cloud-based PTY service for browser terminal access.
|
|
110
|
+
- Source: `apps/cloud-terminal/`
|
|
111
|
+
- Connects to relay via WebSocket
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
railway logs -s cloud-terminal --lines 50
|
|
115
|
+
railway redeploy -s cloud-terminal
|
|
116
|
+
```
|
|
117
|
+
|
|
82
118
|
### Vercel Services (NOT on Railway)
|
|
83
119
|
| Service | URL |
|
|
84
120
|
|---------|-----|
|
|
@@ -159,6 +159,12 @@ if [ -z "$RAW_SESSION_ID" ] || [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESS
|
|
|
159
159
|
if [ -f "$STATE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
|
|
160
160
|
RAW_SESSION_ID=$(node "$JSON_PARSE_HELPER" "$STATE_FILE" '.session_id' 2>/dev/null || echo "unknown")
|
|
161
161
|
fi
|
|
162
|
+
|
|
163
|
+
# VSCode extension fallback: Extract session ID from transcript path
|
|
164
|
+
# Path format: ~/.claude/projects/<project>/<session-uuid>.jsonl
|
|
165
|
+
if [ "$RAW_SESSION_ID" = "unknown" ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
166
|
+
RAW_SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
|
|
167
|
+
fi
|
|
162
168
|
fi
|
|
163
169
|
|
|
164
170
|
# ═══════════════════════════════════════════════════════════════════════════
|