@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.
- package/dist/commands/init.js +15 -3
- package/dist/commands/run.js +10 -7
- package/dist/commands/setup.js +47 -0
- package/dist/deploy/skills.d.ts +8 -0
- package/dist/deploy/skills.js +26 -0
- package/dist/utils/platform.d.ts +1 -0
- package/dist/utils/platform.js +2 -1
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.ps1 +4 -4
- package/templates/hooks/session-start.ps1 +2 -2
- package/templates/hooks/stop.ps1 +167 -4
- package/templates/hooks/user-prompt-submit.ps1 +33 -8
- package/templates/windsurf-skills/README.md +58 -0
- package/templates/windsurf-skills/ekkos-continue/SKILL.md +81 -0
- package/templates/windsurf-skills/ekkos-golden-loop/SKILL.md +225 -0
- package/templates/windsurf-skills/ekkos-insights/SKILL.md +138 -0
- package/templates/windsurf-skills/ekkos-recall/SKILL.md +96 -0
- package/templates/windsurf-skills/ekkos-safety/SKILL.md +89 -0
- package/templates/windsurf-skills/ekkos-vault/SKILL.md +86 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
package/dist/commands/init.js
CHANGED
|
@@ -332,16 +332,28 @@ async function deployForCursor(apiKey, userId) {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
async function deployForWindsurf(apiKey, userId) {
|
|
335
|
-
|
|
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
|
-
|
|
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
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
1510
|
-
|
|
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
|
-
|
|
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;
|
package/dist/commands/setup.js
CHANGED
|
@@ -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');
|
package/dist/deploy/skills.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/deploy/skills.js
CHANGED
|
@@ -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
|
*/
|
package/dist/utils/platform.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/utils/platform.js
CHANGED
|
@@ -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,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-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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://
|
|
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
|
|
package/templates/hooks/stop.ps1
CHANGED
|
@@ -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://
|
|
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://
|
|
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://
|
|
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
|
}
|