@ekkos/cli 0.2.15 → 0.2.17

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
  },
@@ -137,6 +137,7 @@ async function setup(options) {
137
137
  message: 'Which IDE are you setting up?',
138
138
  choices: [
139
139
  { name: 'Claude Code (CLI)', value: 'claude-code' },
140
+ { name: 'Claude Desktop (MCP)', value: 'claude-desktop' },
140
141
  { name: 'Cursor', value: 'cursor' },
141
142
  { name: 'Windsurf (Cascade)', value: 'windsurf' },
142
143
  { name: 'VSCode (Copilot)', value: 'vscode' }
@@ -180,6 +181,9 @@ async function setupIDE(ide, apiKey, config) {
180
181
  case 'claude-code':
181
182
  await setupClaudeCode(apiKey);
182
183
  break;
184
+ case 'claude-desktop':
185
+ await setupClaudeDesktop(apiKey);
186
+ break;
183
187
  case 'cursor':
184
188
  await setupCursor(apiKey);
185
189
  break;
@@ -320,6 +324,33 @@ async function setupVSCode(apiKey) {
320
324
  // Note: Full VSCode integration requires extension
321
325
  console.log(chalk_1.default.yellow(' Note: VSCode requires the ekkOS extension for full integration'));
322
326
  }
327
+ async function setupClaudeDesktop(apiKey) {
328
+ // Claude Desktop - configure MCP server
329
+ const claudeDir = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'Application Support', 'Claude');
330
+ if (!(0, fs_1.existsSync)(claudeDir)) {
331
+ (0, fs_1.mkdirSync)(claudeDir, { recursive: true });
332
+ }
333
+ // Create/update MCP config
334
+ const configPath = (0, path_1.join)(claudeDir, 'claude_desktop_config.json');
335
+ let config = {};
336
+ if ((0, fs_1.existsSync)(configPath)) {
337
+ try {
338
+ config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
339
+ }
340
+ catch { }
341
+ }
342
+ // Preserve existing preferences
343
+ config.mcpServers = config.mcpServers || {};
344
+ config.mcpServers['ekkos-memory'] = {
345
+ command: 'npx',
346
+ args: ['-y', '@ekkos/mcp-server@latest'],
347
+ env: {
348
+ EKKOS_API_KEY: apiKey
349
+ }
350
+ };
351
+ (0, fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
352
+ console.log(chalk_1.default.yellow(' Note: Restart Claude Desktop to load the MCP server'));
353
+ }
323
354
  function generatePromptSubmitHook(apiKey) {
324
355
  return `#!/bin/bash
325
356
  # ekkOS Hook: UserPromptSubmit
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "ekkos": "dist/index.js",
8
+ "cli": "dist/index.js",
8
9
  "ekkos-capture": "dist/cache/capture.js"
9
10
  },
10
11
  "scripts": {
@@ -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",