@ekkos/cli 1.0.29 → 1.0.31

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.
@@ -350,9 +350,8 @@ function getEkkosEnv() {
350
350
  // Let Claude Code use its own default max_tokens (don't override)
351
351
  };
352
352
  /* eslint-enable no-restricted-syntax */
353
- // Check if proxy is disabled via env var or options
354
- // eslint-disable-next-line no-restricted-syntax -- Feature flag, not API key
355
- const proxyDisabled = process.env.EKKOS_DISABLE_PROXY === '1' || !proxyModeEnabled;
353
+ // Check if proxy is disabled (proxyModeEnabled already includes CLI + env decisions)
354
+ const proxyDisabled = !proxyModeEnabled;
356
355
  if (!proxyDisabled) {
357
356
  env.EKKOS_PROXY_MODE = '1';
358
357
  // Enable ultra-minimal mode by default (30%→20% eviction for constant-cost infinite context)
@@ -393,6 +392,8 @@ function getEkkosEnv() {
393
392
  const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
394
393
  const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}`;
395
394
  env.ANTHROPIC_BASE_URL = proxyUrl;
395
+ // Newer Claude Code internals also read this key for API/file routes.
396
+ env.CLAUDE_CODE_API_BASE_URL = proxyUrl;
396
397
  // Proxy URL contains userId + project path — don't leak to terminal
397
398
  }
398
399
  else {
@@ -884,13 +885,18 @@ async function run(options) {
884
885
  const verbose = options.verbose || false;
885
886
  const bypass = options.bypass || false;
886
887
  const noInject = options.noInject || false;
887
- // Set proxy mode based on options (used by getEkkosEnv)
888
- proxyModeEnabled = !(options.noProxy || false);
888
+ // Set proxy mode based on options + env gate (used by getEkkosEnv)
889
+ const proxyDisabledByEnv = process.env.EKKOS_DISABLE_PROXY === '1';
890
+ proxyModeEnabled = !(options.noProxy || false || proxyDisabledByEnv);
889
891
  if (proxyModeEnabled) {
890
892
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
891
893
  }
894
+ else if (proxyDisabledByEnv) {
895
+ console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled by EKKOS_DISABLE_PROXY=1'));
896
+ console.log(chalk_1.default.gray(' Unset EKKOS_DISABLE_PROXY to re-enable proxy routing.'));
897
+ }
892
898
  else if (verbose) {
893
- console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
899
+ console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--skip-proxy)'));
894
900
  }
895
901
  // ══════════════════════════════════════════════════════════════════════════
896
902
  // DASHBOARD MODE: tmux (Mac/Linux) or Windows Terminal (Windows)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -207,7 +207,7 @@ if ($captureCmd -and $rawSessionId -ne "unknown") {
207
207
  $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
208
208
  if ($toolMatches.Count -gt 0) {
209
209
  $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
210
- $toolsJson = $tools | ConvertTo-Json -Compress
210
+ $toolsJson = $tools | ConvertTo-Json -Depth 10 -Compress
211
211
  }
212
212
 
213
213
  Start-Job -ScriptBlock {
@@ -240,13 +240,13 @@ if (Test-Path $configFile) {
240
240
  turn = $turnNum
241
241
  response = $response.Substring(0, [Math]::Min(5000, $response.Length))
242
242
  pattern_ids = $patterns
243
- } | ConvertTo-Json
243
+ } | ConvertTo-Json -Depth 10
244
244
 
245
- Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
245
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
246
246
  -Method POST `
247
247
  -Headers @{ Authorization = "Bearer $token" } `
248
248
  -ContentType "application/json" `
249
- -Body $body -ErrorAction SilentlyContinue
249
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
250
250
  } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $assistantResponse, $patternIds | Out-Null
251
251
  }
252
252
  } catch {}
@@ -112,7 +112,7 @@ $state = @{
112
112
  session_name = $sessionName
113
113
  instance_id = $EkkosInstanceId
114
114
  started_at = (Get-Date).ToString("o")
115
- } | ConvertTo-Json
115
+ } | ConvertTo-Json -Depth 10
116
116
 
117
117
  Set-Content -Path $stateFile -Value $state -Force
118
118
 
@@ -122,7 +122,7 @@ $sessionData = @{
122
122
  session_id = $sessionId
123
123
  session_name = $sessionName
124
124
  instance_id = $EkkosInstanceId
125
- } | ConvertTo-Json
125
+ } | ConvertTo-Json -Depth 10
126
126
 
127
127
  Set-Content -Path $sessionFile -Value $sessionData -Force
128
128
 
@@ -127,19 +127,20 @@ if ((Test-Path $configFile) -and $sessionName -ne "unknown-session") {
127
127
  $projectPath = (Get-Location).Path
128
128
  $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
129
129
 
130
+ $projectPath = $projectPath -replace '\\', '/'
130
131
  $bindBody = @{
131
132
  userId = $userId
132
133
  realSession = $sessionName
133
134
  projectPath = $projectPath
134
135
  pendingSession = $pendingSession
135
- } | ConvertTo-Json
136
+ } | ConvertTo-Json -Depth 10
136
137
 
137
138
  Start-Job -ScriptBlock {
138
139
  param($body, $token)
139
- Invoke-RestMethod -Uri "https://proxy.ekkos.dev/proxy/session/bind" `
140
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
140
141
  -Method POST `
141
142
  -Headers @{ "Content-Type" = "application/json" } `
142
- -Body $body -ErrorAction SilentlyContinue | Out-Null
143
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
143
144
  } -ArgumentList $bindBody, $authToken | Out-Null
144
145
  }
145
146
  } catch {}
@@ -152,7 +153,6 @@ if ((Test-Path $configFile) -and $sessionName -ne "unknown-session") {
152
153
  $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
153
154
  if ($captureCmd -and $rawSessionId -ne "unknown") {
154
155
  try {
155
- # ACK format: ekkos-capture ack <session_id> <turn_id> --instance=ID
156
156
  Start-Job -ScriptBlock {
157
157
  param($instanceId, $sessionId, $turnNum)
158
158
  try {
@@ -162,6 +162,169 @@ if ($captureCmd -and $rawSessionId -ne "unknown") {
162
162
  } catch {}
163
163
  }
164
164
 
165
+ # ═══════════════════════════════════════════════════════════════════════════
166
+ # CHECK INTERRUPTION - Skip capture if user cancelled
167
+ # ═══════════════════════════════════════════════════════════════════════════
168
+ $isInterrupted = $false
169
+ $stopReason = ""
170
+ if ($inputJson) {
171
+ try {
172
+ $stopInput = $inputJson | ConvertFrom-Json
173
+ $isInterrupted = $stopInput.interrupted -eq $true
174
+ $stopReason = $stopInput.stop_reason
175
+ } catch {}
176
+ }
177
+
178
+ if ($isInterrupted -or $stopReason -eq "user_cancelled" -or $stopReason -eq "interrupted") {
179
+ if (Test-Path $stateFile) { Remove-Item $stateFile -Force }
180
+ Write-Output "ekkOS session ended (interrupted)"
181
+ exit 0
182
+ }
183
+
184
+ # ═══════════════════════════════════════════════════════════════════════════
185
+ # EXTRACT CONVERSATION FROM TRANSCRIPT
186
+ # Mirrors stop.sh: Extract last user query, assistant response, file changes
187
+ # ═══════════════════════════════════════════════════════════════════════════
188
+ $transcriptPath = ""
189
+ if ($inputJson) {
190
+ try {
191
+ $stopInput2 = $inputJson | ConvertFrom-Json
192
+ $transcriptPath = $stopInput2.transcript_path
193
+ } catch {}
194
+ }
195
+
196
+ $lastUser = ""
197
+ $lastAssistant = ""
198
+ $fileChangesJson = "[]"
199
+
200
+ if ($transcriptPath -and (Test-Path $transcriptPath)) {
201
+ try {
202
+ $extraction = node -e @"
203
+ const fs = require('fs');
204
+ const lines = fs.readFileSync(process.argv[1], 'utf8').split('\n').filter(Boolean);
205
+ const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
206
+
207
+ let lastUser = '', lastUserTime = '';
208
+ for (let i = entries.length - 1; i >= 0; i--) {
209
+ const e = entries[i];
210
+ if (e.type === 'user') {
211
+ const content = e.message?.content;
212
+ if (typeof content === 'string' && !content.startsWith('<')) {
213
+ lastUser = content; lastUserTime = e.timestamp || ''; break;
214
+ } else if (Array.isArray(content)) {
215
+ const textPart = content.find(c => c.type === 'text' && !c.text?.startsWith('<'));
216
+ if (textPart) { lastUser = textPart.text; lastUserTime = e.timestamp || ''; break; }
217
+ }
218
+ }
219
+ }
220
+
221
+ let lastAssistant = '';
222
+ for (let i = entries.length - 1; i >= 0; i--) {
223
+ const e = entries[i];
224
+ if (e.type === 'assistant' && (!lastUserTime || e.timestamp >= lastUserTime)) {
225
+ const content = e.message?.content;
226
+ if (typeof content === 'string') { lastAssistant = content; break; }
227
+ else if (Array.isArray(content)) {
228
+ const parts = content.map(c => {
229
+ if (c.type === 'text') return c.text;
230
+ if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
231
+ return '';
232
+ }).filter(Boolean);
233
+ lastAssistant = parts.join('\n'); break;
234
+ }
235
+ }
236
+ }
237
+
238
+ const fileChanges = [];
239
+ entries.filter(e => e.type === 'assistant').forEach(e => {
240
+ const content = e.message?.content;
241
+ if (Array.isArray(content)) {
242
+ content.filter(c => c.type === 'tool_use' && ['Edit', 'Write', 'Read'].includes(c.name)).forEach(c => {
243
+ fileChanges.push({tool: c.name, path: (c.input?.file_path || c.input?.path || '').replace(/\\\\/g, '/'), action: c.name.toLowerCase()});
244
+ });
245
+ }
246
+ });
247
+
248
+ console.log(JSON.stringify({
249
+ user: lastUser,
250
+ assistant: lastAssistant.substring(0, 50000),
251
+ fileChanges: fileChanges.slice(0, 20)
252
+ }));
253
+ "@ $transcriptPath 2>$null
254
+
255
+ if ($extraction) {
256
+ $parsed = $extraction | ConvertFrom-Json
257
+ $lastUser = $parsed.user
258
+ $lastAssistant = $parsed.assistant
259
+ $fileChangesJson = ($parsed.fileChanges | ConvertTo-Json -Depth 10 -Compress)
260
+ if (-not $fileChangesJson) { $fileChangesJson = "[]" }
261
+ }
262
+ } catch {}
263
+ }
264
+
265
+ # ═══════════════════════════════════════════════════════════════════════════
266
+ # CAPTURE TO BOTH Working Sessions (Redis) AND Episodic Memory (Supabase)
267
+ # Mirrors stop.sh dual-write at lines 271-356
268
+ # ═══════════════════════════════════════════════════════════════════════════
269
+ if ($lastUser -and $lastAssistant -and $authToken) {
270
+ $modelUsed = "claude-sonnet-4-5"
271
+ if ($inputJson) {
272
+ try {
273
+ $stopInput3 = $inputJson | ConvertFrom-Json
274
+ if ($stopInput3.model) { $modelUsed = $stopInput3.model }
275
+ } catch {}
276
+ }
277
+
278
+ $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
279
+ $projectPath = ((Get-Location).Path) -replace '\\', '/'
280
+
281
+ # 1. WORKING SESSIONS (Redis)
282
+ Start-Job -ScriptBlock {
283
+ param($token, $sessionName, $turnNum, $userQuery, $agentResponse, $model)
284
+ $body = @{
285
+ session_name = $sessionName
286
+ turn_number = $turnNum
287
+ user_query = $userQuery
288
+ agent_response = $agentResponse.Substring(0, [Math]::Min(50000, $agentResponse.Length))
289
+ model = $model
290
+ tools_used = @()
291
+ files_referenced = @()
292
+ } | ConvertTo-Json -Depth 10
293
+
294
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
295
+ -Method POST `
296
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
297
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
298
+ -ErrorAction SilentlyContinue | Out-Null
299
+ } -ArgumentList $authToken, $sessionName, $turn, $lastUser, $lastAssistant, $modelUsed | Out-Null
300
+
301
+ # 2. EPISODIC MEMORY (Supabase)
302
+ Start-Job -ScriptBlock {
303
+ param($token, $userQuery, $agentResponse, $sessionId, $userId, $fileChanges, $model, $ts, $turnNum, $sessionName)
304
+ $body = @{
305
+ user_query = $userQuery
306
+ assistant_response = $agentResponse
307
+ session_id = $sessionId
308
+ user_id = if ($userId) { $userId } else { "system" }
309
+ file_changes = @()
310
+ metadata = @{
311
+ source = "claude-code"
312
+ model_used = $model
313
+ captured_at = $ts
314
+ turn_number = $turnNum
315
+ session_name = $sessionName
316
+ minimal_hook = $true
317
+ }
318
+ } | ConvertTo-Json -Depth 10
319
+
320
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/memory/capture" `
321
+ -Method POST `
322
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
323
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
324
+ -ErrorAction SilentlyContinue | Out-Null
325
+ } -ArgumentList $authToken, $lastUser, $lastAssistant, $rawSessionId, $userId, $fileChangesJson, $modelUsed, $timestamp, $turn, $sessionName | Out-Null
326
+ }
327
+
165
328
  # ═══════════════════════════════════════════════════════════════════════════
166
329
  # CLEAN UP STATE FILES
167
330
  # ═══════════════════════════════════════════════════════════════════════════
@@ -38,15 +38,13 @@ function Load-SessionWords {
38
38
  }
39
39
 
40
40
  if (-not (Test-Path $wordsFile)) {
41
- Write-Error "No session-words.json found. Run 'ekkos hooks install --global' to fix."
42
- exit 1
41
+ return $null
43
42
  }
44
43
 
45
44
  try {
46
45
  $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
47
46
  } catch {
48
- Write-Error "Failed to parse $wordsFile. Check JSON syntax: $_"
49
- exit 1
47
+ return $null
50
48
  }
51
49
  }
52
50
 
@@ -155,25 +153,50 @@ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
155
153
  if ($userId) {
156
154
  $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
157
155
  $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
156
+ $projectPath = $projectPath -replace '\\', '/'
158
157
  $bindBody = @{
159
158
  userId = $userId
160
159
  realSession = $sessionName
161
160
  projectPath = $projectPath
162
161
  pendingSession = $pendingSession
163
- } | ConvertTo-Json -Compress
162
+ } | ConvertTo-Json -Depth 10 -Compress
164
163
 
165
164
  Start-Job -ScriptBlock {
166
165
  param($body)
167
- Invoke-RestMethod -Uri "https://proxy.ekkos.dev/proxy/session/bind" `
166
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
168
167
  -Method POST `
169
168
  -Headers @{ "Content-Type" = "application/json" } `
170
- -Body $body -ErrorAction SilentlyContinue | Out-Null
169
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
171
170
  } -ArgumentList $bindBody | Out-Null
172
171
  }
173
172
  } catch {}
174
173
  }
175
174
  }
176
175
 
176
+ # ═══════════════════════════════════════════════════════════════════════════
177
+ # SESSION CURRENT: Update Redis with current session name
178
+ # ═══════════════════════════════════════════════════════════════════════════
179
+ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
180
+ $configFile2 = Join-Path $EkkosConfigDir "config.json"
181
+ if (Test-Path $configFile2) {
182
+ try {
183
+ $config2 = Get-Content $configFile2 -Raw | ConvertFrom-Json
184
+ $sessionToken = $config2.hookApiKey
185
+ if (-not $sessionToken) { $sessionToken = $config2.apiKey }
186
+ if ($sessionToken) {
187
+ $sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10
188
+ Start-Job -ScriptBlock {
189
+ param($body, $token)
190
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/session/current" `
191
+ -Method POST `
192
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
193
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
194
+ } -ArgumentList $sessionBody, $sessionToken | Out-Null
195
+ }
196
+ } catch {}
197
+ }
198
+ }
199
+
177
200
  # ═══════════════════════════════════════════════════════════════════════════
178
201
  # TURN TRACKING & STATE MANAGEMENT
179
202
  # ═══════════════════════════════════════════════════════════════════════════
@@ -207,7 +230,7 @@ $newState = @{
207
230
  session_id = $rawSessionId
208
231
  last_query = $userQuery.Substring(0, [Math]::Min(100, $userQuery.Length))
209
232
  timestamp = (Get-Date).ToString("o")
210
- } | ConvertTo-Json
233
+ } | ConvertTo-Json -Depth 10
211
234
 
212
235
  Set-Content -Path $stateFile -Value $newState -Force
213
236
 
@@ -252,13 +275,13 @@ if (Test-Path $configFile) {
252
275
  instance_id = $instanceId
253
276
  turn = $turnNum
254
277
  query = $query
255
- } | ConvertTo-Json
278
+ } | ConvertTo-Json -Depth 10
256
279
 
257
- Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/fast-capture" `
280
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/fast-capture" `
258
281
  -Method POST `
259
282
  -Headers @{ Authorization = "Bearer $token" } `
260
283
  -ContentType "application/json" `
261
- -Body $body -ErrorAction SilentlyContinue
284
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
262
285
  } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $userQuery | Out-Null
263
286
  }
264
287
  } catch {}
@@ -280,7 +303,7 @@ if ($sessionName -ne "unknown-session") {
280
303
  sessionId = $rawSessionId
281
304
  projectPath = $projectPath
282
305
  ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
283
- } | ConvertTo-Json -Compress
306
+ } | ConvertTo-Json -Depth 10 -Compress
284
307
  Set-Content -Path $hintFile -Value $hint -Force
285
308
  } catch {}
286
309
  }