@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.
@@ -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
  */
@@ -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://mcp.ekkos.dev';
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
- // Start PTY with ekkos run -d
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', '-d', '-b'], // -d for doctor check, -b for bypass permissions
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
@@ -11,6 +11,7 @@ interface PTYRunnerConfig {
11
11
  onExit: (code: number) => void;
12
12
  cols?: number;
13
13
  rows?: number;
14
+ cwd?: string;
14
15
  verbose?: boolean;
15
16
  }
16
17
  export declare class PTYRunner {
@@ -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: process.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: process.cwd(),
120
+ cwd,
120
121
  env: {
121
122
  ...process.env,
122
123
  TERM: 'xterm-256color',
@@ -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 'npx:VERSION' to signal we should use npx with pinned version
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: Use pinned version via npx
266
- // This ensures ekkos run always uses the version with better context behavior
267
- // Return special marker that spawn logic will handle
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 MEMORY_API_URL = process.env.MEMORY_API_URL || 'https://mcp.ekkos.dev';
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(`${MEMORY_API_URL}/api/v1/relay/pair`, {
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(`${MEMORY_API_URL}/api/v1/relay/pair/${deviceCode}`, {
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(`${MEMORY_API_URL}/api/v1/relay/devices/${(await (0, state_1.getState)())?.userId}`, {
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
  },
@@ -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
- if (!(0, fs_1.existsSync)(hooksDir)) {
207
- (0, fs_1.mkdirSync)(hooksDir, { recursive: true });
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
- const promptSubmitPath = (0, path_1.join)(hooksDir, 'user-prompt-submit.ps1');
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
- const stopPath = (0, path_1.join)(hooksDir, 'stop.ps1');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -68,10 +68,30 @@ railway variables set KEY=value -s pm2-workers
68
68
 
69
69
  ## EKKOS SERVICES
70
70
 
71
- ### Railway Service: `pm2-workers`
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
  |---------|-----|
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://ekkos.dev/schemas/manifest-v1.json",
3
3
  "manifestVersion": "1.0.0",
4
- "generatedAt": "2026-01-21T08:07:39.857Z",
4
+ "generatedAt": "2026-01-22T06:19:22.744Z",
5
5
  "platforms": {
6
6
  "darwin": {
7
7
  "configDir": "~/.ekkos",
@@ -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
  # ═══════════════════════════════════════════════════════════════════════════