@ekkos/cli 1.0.30 → 1.0.32

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.
@@ -332,16 +332,28 @@ async function deployForCursor(apiKey, userId) {
332
332
  }
333
333
  }
334
334
  async function deployForWindsurf(apiKey, userId) {
335
- const spinner = (0, ora_1.default)('Deploying Windsurf MCP configuration...').start();
335
+ let success = false;
336
+ // MCP configuration
337
+ let spinner = (0, ora_1.default)('Deploying Windsurf MCP configuration...').start();
336
338
  try {
337
339
  (0, mcp_1.deployWindsurfMcp)(apiKey, userId);
338
340
  spinner.succeed('Windsurf MCP configuration');
339
- return true;
341
+ success = true;
340
342
  }
341
343
  catch (error) {
342
344
  spinner.fail('Windsurf MCP configuration failed');
343
- return false;
344
345
  }
346
+ // Skills (Agent Skills spec — 6 Golden Loop skills)
347
+ spinner = (0, ora_1.default)('Deploying Windsurf skills...').start();
348
+ try {
349
+ const result = (0, skills_1.deployWindsurfSkills)();
350
+ spinner.succeed(`Windsurf skills (${result.count} skills: ${result.skills.join(', ')})`);
351
+ success = true;
352
+ }
353
+ catch (error) {
354
+ spinner.fail('Windsurf skills deployment failed');
355
+ }
356
+ return success;
345
357
  }
346
358
  // ═══════════════════════════════════════════════════════════════════════════
347
359
  // MAIN INIT COMMAND
@@ -392,8 +392,6 @@ function getEkkosEnv() {
392
392
  const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
393
393
  const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}`;
394
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;
397
395
  // Proxy URL contains userId + project path — don't leak to terminal
398
396
  }
399
397
  else {
@@ -452,7 +450,8 @@ function resolveNativeClaudeBin() {
452
450
  */
453
451
  function getClaudeVersion(claudePath) {
454
452
  try {
455
- const version = (0, child_process_1.execSync)(`"${claudePath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
453
+ const suppressStderr = isWindows ? '2>NUL' : '2>/dev/null';
454
+ const version = (0, child_process_1.execSync)(`"${claudePath}" --version ${suppressStderr}`, { encoding: 'utf-8' }).trim();
456
455
  // Look for pattern like "2.1.6 (Claude Code)" or just "2.1.6" anywhere in output
457
456
  const match = version.match(/(\d+\.\d+\.\d+)\s*\(Claude Code\)/);
458
457
  if (match)
@@ -1335,7 +1334,7 @@ async function run(options) {
1335
1334
  // Claude creates the transcript file BEFORE outputting the session name
1336
1335
  // So we watch for new files rather than parsing TUI output (which is slower)
1337
1336
  // ════════════════════════════════════════════════════════════════════════════
1338
- const encodedCwd = process.cwd().replace(/\//g, '-');
1337
+ const encodedCwd = process.cwd().replace(/[\\/]/g, '-');
1339
1338
  const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
1340
1339
  const launchTime = Date.now();
1341
1340
  // Track existing jsonl files at startup
@@ -1506,8 +1505,10 @@ async function run(options) {
1506
1505
  });
1507
1506
  return false;
1508
1507
  }
1509
- // Check it starts with / or ~ (absolute path)
1510
- if (!pathToCheck.startsWith('/') && !pathToCheck.startsWith('~')) {
1508
+ // Check it's an absolute path (Unix: / or ~, Windows: C:\ or \\)
1509
+ const isAbsolutePath = pathToCheck.startsWith('/') || pathToCheck.startsWith('~') ||
1510
+ /^[A-Za-z]:[\\/]/.test(pathToCheck) || pathToCheck.startsWith('\\\\');
1511
+ if (!isAbsolutePath) {
1511
1512
  evictionDebugLog('PATH_INVALID', 'Transcript path is not absolute - clearing', {
1512
1513
  path: pathToCheck.slice(0, 100),
1513
1514
  });
@@ -2282,7 +2283,9 @@ async function run(options) {
2282
2283
  if (transcriptMatch) {
2283
2284
  const candidatePath = transcriptMatch[1];
2284
2285
  // Validate it's an actual path (not garbage from terminal output)
2285
- if (candidatePath.startsWith('/') || candidatePath.startsWith('~')) {
2286
+ const isAbsCandidate = candidatePath.startsWith('/') || candidatePath.startsWith('~') ||
2287
+ /^[A-Za-z]:[\\/]/.test(candidatePath);
2288
+ if (isAbsCandidate) {
2286
2289
  const resolvedPath = candidatePath.startsWith('~')
2287
2290
  ? path.join(os.homedir(), candidatePath.slice(1))
2288
2291
  : candidatePath;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -351,6 +384,20 @@ async function setupWindsurf(apiKey) {
351
384
  (0, fs_1.mkdirSync)(windsurfDir, { recursive: true });
352
385
  }
353
386
  (0, fs_1.writeFileSync)((0, path_1.join)(windsurfDir, 'ekkos.json'), JSON.stringify({ apiKey }, null, 2));
387
+ // Deploy Windsurf skills (Agent Skills spec — 6 Golden Loop skills)
388
+ const skillsDir = (0, path_1.join)(codeiumDir, 'skills');
389
+ if (!(0, fs_1.existsSync)(skillsDir)) {
390
+ (0, fs_1.mkdirSync)(skillsDir, { recursive: true });
391
+ }
392
+ try {
393
+ const { deployWindsurfSkills } = await Promise.resolve().then(() => __importStar(require('../deploy/skills.js')));
394
+ const result = deployWindsurfSkills();
395
+ console.log(chalk_1.default.green(` ✓ Deployed ${result.count} skills: ${result.skills.join(', ')}`));
396
+ }
397
+ catch {
398
+ // Fallback: templates might not be available in setup path
399
+ console.log(chalk_1.default.yellow(' Note: Skills templates not found. Run `ekkos init --ide windsurf` to deploy skills.'));
400
+ }
354
401
  // Create project rules template
355
402
  const cascadeRules = generateCascadeRules();
356
403
  const cascadeRulesPath = (0, path_1.join)(process.cwd(), '.windsurfrules');
@@ -5,6 +5,14 @@ export declare function deploySkills(): {
5
5
  count: number;
6
6
  skills: string[];
7
7
  };
8
+ /**
9
+ * Deploy Windsurf skills to ~/.codeium/windsurf/skills/
10
+ * Uses Agent Skills spec format (lowercase-hyphenated names, SKILL.md)
11
+ */
12
+ export declare function deployWindsurfSkills(): {
13
+ count: number;
14
+ skills: string[];
15
+ };
8
16
  /**
9
17
  * Check if skills are deployed
10
18
  */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deploySkills = deploySkills;
4
+ exports.deployWindsurfSkills = deployWindsurfSkills;
4
5
  exports.areSkillsDeployed = areSkillsDeployed;
5
6
  exports.countDeployedSkills = countDeployedSkills;
6
7
  exports.listExpectedSkills = listExpectedSkills;
@@ -32,6 +33,31 @@ function deploySkills() {
32
33
  skills: deployedSkills
33
34
  };
34
35
  }
36
+ /**
37
+ * Deploy Windsurf skills to ~/.codeium/windsurf/skills/
38
+ * Uses Agent Skills spec format (lowercase-hyphenated names, SKILL.md)
39
+ */
40
+ function deployWindsurfSkills() {
41
+ if (!(0, fs_1.existsSync)(platform_1.WINDSURF_SKILLS_DIR)) {
42
+ (0, fs_1.mkdirSync)(platform_1.WINDSURF_SKILLS_DIR, { recursive: true });
43
+ }
44
+ const skillNames = (0, templates_1.listTemplateDirs)('windsurf-skills');
45
+ const deployedSkills = [];
46
+ for (const skillName of skillNames) {
47
+ try {
48
+ const destPath = `${platform_1.WINDSURF_SKILLS_DIR}/${skillName}`;
49
+ (0, templates_1.copyTemplateDir)(`windsurf-skills/${skillName}`, destPath);
50
+ deployedSkills.push(skillName);
51
+ }
52
+ catch (error) {
53
+ console.warn(`Warning: Could not deploy Windsurf skill ${skillName}`);
54
+ }
55
+ }
56
+ return {
57
+ count: deployedSkills.length,
58
+ skills: deployedSkills
59
+ };
60
+ }
35
61
  /**
36
62
  * Check if skills are deployed
37
63
  */
@@ -21,6 +21,7 @@ export declare const CURSOR_DIR: string;
21
21
  export declare const CURSOR_MCP: string;
22
22
  export declare const WINDSURF_DIR: string;
23
23
  export declare const WINDSURF_MCP: string;
24
+ export declare const WINDSURF_SKILLS_DIR: string;
24
25
  /**
25
26
  * Detect which IDEs are installed on this system
26
27
  */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WINDSURF_MCP = exports.WINDSURF_DIR = exports.CURSOR_MCP = exports.CURSOR_DIR = exports.CLAUDE_EKKOS_RULES = exports.CLAUDE_RULES_DIR = exports.CLAUDE_MD = exports.CLAUDE_STATE_DIR = exports.CLAUDE_PLUGINS_DIR = exports.CLAUDE_AGENTS_DIR = exports.CLAUDE_SKILLS_DIR = exports.CLAUDE_HOOKS_DIR = exports.CLAUDE_SETTINGS = exports.CLAUDE_CONFIG = exports.CLAUDE_DIR = exports.EKKOS_CONFIG = exports.EKKOS_DIR = exports.HOME_DIR = exports.MCP_API_URL = exports.PLATFORM_URL = exports.isLinux = exports.isMac = exports.isWindows = void 0;
3
+ exports.WINDSURF_SKILLS_DIR = exports.WINDSURF_MCP = exports.WINDSURF_DIR = exports.CURSOR_MCP = exports.CURSOR_DIR = exports.CLAUDE_EKKOS_RULES = exports.CLAUDE_RULES_DIR = exports.CLAUDE_MD = exports.CLAUDE_STATE_DIR = exports.CLAUDE_PLUGINS_DIR = exports.CLAUDE_AGENTS_DIR = exports.CLAUDE_SKILLS_DIR = exports.CLAUDE_HOOKS_DIR = exports.CLAUDE_SETTINGS = exports.CLAUDE_CONFIG = exports.CLAUDE_DIR = exports.EKKOS_CONFIG = exports.EKKOS_DIR = exports.HOME_DIR = exports.MCP_API_URL = exports.PLATFORM_URL = exports.isLinux = exports.isMac = exports.isWindows = void 0;
4
4
  exports.detectInstalledIDEs = detectInstalledIDEs;
5
5
  exports.detectCurrentIDE = detectCurrentIDE;
6
6
  const os_1 = require("os");
@@ -30,6 +30,7 @@ exports.CURSOR_DIR = (0, path_1.join)(exports.HOME_DIR, '.cursor');
30
30
  exports.CURSOR_MCP = (0, path_1.join)(exports.CURSOR_DIR, 'mcp.json');
31
31
  exports.WINDSURF_DIR = (0, path_1.join)(exports.HOME_DIR, '.codeium', 'windsurf');
32
32
  exports.WINDSURF_MCP = (0, path_1.join)(exports.WINDSURF_DIR, 'mcp_config.json');
33
+ exports.WINDSURF_SKILLS_DIR = (0, path_1.join)(exports.WINDSURF_DIR, 'skills');
33
34
  /**
34
35
  * Detect which IDEs are installed on this system
35
36
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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-02-19T23:06:44.502Z",
4
+ "generatedAt": "2026-02-20T07:39:17.071Z",
5
5
  "platforms": {
6
6
  "darwin": {
7
7
  "configDir": "~/.ekkos",
@@ -98,25 +98,25 @@
98
98
  "source": "hooks/user-prompt-submit.ps1",
99
99
  "destination": "user-prompt-submit.ps1",
100
100
  "description": "User prompt submit hook (Windows)",
101
- "checksum": "6a00a23bc3865e63b1bca2702b769473cce11530adbffa4531b21bb6bbe7cc3b"
101
+ "checksum": "ba1090ed7a4e7deef1267b52474225669a9c966b9adf596b31865c2dca1dc749"
102
102
  },
103
103
  {
104
104
  "source": "hooks/stop.ps1",
105
105
  "destination": "stop.ps1",
106
106
  "description": "Session stop hook (Windows)",
107
- "checksum": "2defabb31d51e482990f0fce762a5cc12beb08eb1ac1fb4a1afd7c5375d5e0f0"
107
+ "checksum": "c4f0191ac28ced0410ea560197ab3b0a7e2de83363a789731d64620bc605564d"
108
108
  },
109
109
  {
110
110
  "source": "hooks/session-start.ps1",
111
111
  "destination": "session-start.ps1",
112
112
  "description": "Session start hook (Windows)",
113
- "checksum": "7b700bb98072efe4bd84b84b0754d0b64dc85a718f4484c6d42594d790e4cce5"
113
+ "checksum": "1930bda7c517054531ff3ad78e8cce7e60d57f5b3884728da0d7cf1cc5c54ac4"
114
114
  },
115
115
  {
116
116
  "source": "hooks/assistant-response.ps1",
117
117
  "destination": "assistant-response.ps1",
118
118
  "description": "Assistant response hook (Windows)",
119
- "checksum": "554e978190100d506a3a0c32a59013628d44d00c5e7e21dc28346ad367da60ea"
119
+ "checksum": "680e6d7c597f90fb54bc86d173f4c145093eb21352b30c7d6afbe19d1d5fdce4"
120
120
  }
121
121
  ],
122
122
  "lib": [
@@ -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
  # ═══════════════════════════════════════════════════════════════════════════
@@ -153,25 +153,50 @@ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
153
153
  if ($userId) {
154
154
  $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
155
155
  $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
156
+ $projectPath = $projectPath -replace '\\', '/'
156
157
  $bindBody = @{
157
158
  userId = $userId
158
159
  realSession = $sessionName
159
160
  projectPath = $projectPath
160
161
  pendingSession = $pendingSession
161
- } | ConvertTo-Json -Compress
162
+ } | ConvertTo-Json -Depth 10 -Compress
162
163
 
163
164
  Start-Job -ScriptBlock {
164
165
  param($body)
165
- Invoke-RestMethod -Uri "https://proxy.ekkos.dev/proxy/session/bind" `
166
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
166
167
  -Method POST `
167
168
  -Headers @{ "Content-Type" = "application/json" } `
168
- -Body $body -ErrorAction SilentlyContinue | Out-Null
169
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
169
170
  } -ArgumentList $bindBody | Out-Null
170
171
  }
171
172
  } catch {}
172
173
  }
173
174
  }
174
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
+
175
200
  # ═══════════════════════════════════════════════════════════════════════════
176
201
  # TURN TRACKING & STATE MANAGEMENT
177
202
  # ═══════════════════════════════════════════════════════════════════════════
@@ -205,7 +230,7 @@ $newState = @{
205
230
  session_id = $rawSessionId
206
231
  last_query = $userQuery.Substring(0, [Math]::Min(100, $userQuery.Length))
207
232
  timestamp = (Get-Date).ToString("o")
208
- } | ConvertTo-Json
233
+ } | ConvertTo-Json -Depth 10
209
234
 
210
235
  Set-Content -Path $stateFile -Value $newState -Force
211
236
 
@@ -250,13 +275,13 @@ if (Test-Path $configFile) {
250
275
  instance_id = $instanceId
251
276
  turn = $turnNum
252
277
  query = $query
253
- } | ConvertTo-Json
278
+ } | ConvertTo-Json -Depth 10
254
279
 
255
- 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" `
256
281
  -Method POST `
257
282
  -Headers @{ Authorization = "Bearer $token" } `
258
283
  -ContentType "application/json" `
259
- -Body $body -ErrorAction SilentlyContinue
284
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
260
285
  } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $userQuery | Out-Null
261
286
  }
262
287
  } catch {}
@@ -278,7 +303,7 @@ if ($sessionName -ne "unknown-session") {
278
303
  sessionId = $rawSessionId
279
304
  projectPath = $projectPath
280
305
  ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
281
- } | ConvertTo-Json -Compress
306
+ } | ConvertTo-Json -Depth 10 -Compress
282
307
  Set-Content -Path $hintFile -Value $hint -Force
283
308
  } catch {}
284
309
  }