@ekkos/cli 0.2.18 → 0.3.3

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/capture/eviction-client.d.ts +139 -0
  3. package/dist/capture/eviction-client.js +454 -0
  4. package/dist/capture/index.d.ts +2 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  7. package/dist/capture/jsonl-rewriter.js +1369 -0
  8. package/dist/capture/transcript-repair.d.ts +50 -0
  9. package/dist/capture/transcript-repair.js +308 -0
  10. package/dist/commands/doctor.js +23 -1
  11. package/dist/commands/run.d.ts +2 -0
  12. package/dist/commands/run.js +1229 -293
  13. package/dist/commands/usage.d.ts +7 -0
  14. package/dist/commands/usage.js +214 -0
  15. package/dist/cron/index.d.ts +7 -0
  16. package/dist/cron/index.js +13 -0
  17. package/dist/cron/promoter.d.ts +70 -0
  18. package/dist/cron/promoter.js +403 -0
  19. package/dist/index.js +24 -3
  20. package/dist/lib/usage-monitor.d.ts +47 -0
  21. package/dist/lib/usage-monitor.js +124 -0
  22. package/dist/lib/usage-parser.d.ts +72 -0
  23. package/dist/lib/usage-parser.js +238 -0
  24. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  25. package/dist/restore/RestoreOrchestrator.js +118 -30
  26. package/package.json +12 -12
  27. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  28. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  29. package/templates/cursor-hooks/stop.sh +0 -0
  30. package/templates/ekkos-manifest.json +2 -2
  31. package/templates/hooks/assistant-response.sh +0 -0
  32. package/templates/hooks/session-start.sh +0 -0
  33. package/templates/plan-template.md +0 -0
  34. package/templates/spec-template.md +0 -0
  35. package/templates/agents/README.md +0 -182
  36. package/templates/agents/code-reviewer.md +0 -166
  37. package/templates/agents/debug-detective.md +0 -169
  38. package/templates/agents/ekkOS_Vercel.md +0 -99
  39. package/templates/agents/extension-manager.md +0 -229
  40. package/templates/agents/git-companion.md +0 -185
  41. package/templates/agents/github-test-agent.md +0 -321
  42. package/templates/agents/railway-manager.md +0 -215
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Transcript Repair - Handles orphan tool_result recovery
3
+ *
4
+ * When ccDNA's validate mode detects orphan tool_results (tool_result without
5
+ * matching tool_use), this module repairs the transcript:
6
+ *
7
+ * 1. ROLLBACK (preferred): Restore from backup if valid
8
+ * 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
9
+ *
10
+ * This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
11
+ */
12
+ type RepairAction = 'none' | 'rollback' | 'surgical_repair' | 'failed';
13
+ export interface RepairResult {
14
+ action: RepairAction;
15
+ orphansFound: number;
16
+ removedLines?: number;
17
+ backupUsed?: string;
18
+ reason?: string;
19
+ }
20
+ /**
21
+ * Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
22
+ * appears earlier in the transcript (in-order).
23
+ */
24
+ export declare function countOrphansInJsonl(jsonlPath: string): {
25
+ orphans: number;
26
+ orphanIds: string[];
27
+ };
28
+ /**
29
+ * Main entry point: repair or rollback a transcript with orphan tool_results.
30
+ *
31
+ * Strategy:
32
+ * 1. Check if there are orphans (exit early if none)
33
+ * 2. Try rollback to newest valid backup
34
+ * 3. If no valid backup, surgically remove orphan lines
35
+ */
36
+ export declare function repairOrRollbackTranscript(jsonlPath: string): RepairResult;
37
+ /**
38
+ * Quick validation check - can be called before operations to verify health.
39
+ */
40
+ export declare function isTranscriptHealthy(jsonlPath: string): boolean;
41
+ /**
42
+ * Get detailed transcript health info.
43
+ */
44
+ export declare function getTranscriptHealth(jsonlPath: string): {
45
+ exists: boolean;
46
+ healthy: boolean;
47
+ orphans: number;
48
+ orphanIds: string[];
49
+ };
50
+ export {};
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * Transcript Repair - Handles orphan tool_result recovery
4
+ *
5
+ * When ccDNA's validate mode detects orphan tool_results (tool_result without
6
+ * matching tool_use), this module repairs the transcript:
7
+ *
8
+ * 1. ROLLBACK (preferred): Restore from backup if valid
9
+ * 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
10
+ *
11
+ * This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.countOrphansInJsonl = countOrphansInJsonl;
48
+ exports.repairOrRollbackTranscript = repairOrRollbackTranscript;
49
+ exports.isTranscriptHealthy = isTranscriptHealthy;
50
+ exports.getTranscriptHealth = getTranscriptHealth;
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const os = __importStar(require("os"));
54
+ // Debug logging to the same file as eviction
55
+ function debugLog(category, msg, data) {
56
+ try {
57
+ const logDir = path.join(os.homedir(), '.ekkos', 'logs');
58
+ if (!fs.existsSync(logDir))
59
+ fs.mkdirSync(logDir, { recursive: true });
60
+ const logPath = path.join(logDir, 'eviction-debug.log');
61
+ const ts = new Date().toISOString();
62
+ const line = `[${ts}] [REPAIR:${category}] ${msg}${data ? '\n ' + JSON.stringify(data, null, 2).replace(/\n/g, '\n ') : ''}`;
63
+ fs.appendFileSync(logPath, line + '\n');
64
+ }
65
+ catch {
66
+ // Silent fail
67
+ }
68
+ }
69
+ /**
70
+ * Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
71
+ * appears earlier in the transcript (in-order).
72
+ */
73
+ function countOrphansInJsonl(jsonlPath) {
74
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
75
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
76
+ const seenToolUses = new Set();
77
+ const orphanIds = [];
78
+ for (const line of lines) {
79
+ let obj;
80
+ try {
81
+ obj = JSON.parse(line);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ const blocks = obj?.message?.content;
87
+ if (!Array.isArray(blocks))
88
+ continue;
89
+ for (const b of blocks) {
90
+ const block = b;
91
+ if (block?.type === 'tool_use' && typeof block?.id === 'string') {
92
+ seenToolUses.add(block.id);
93
+ }
94
+ else if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
95
+ if (!seenToolUses.has(block.tool_use_id)) {
96
+ orphanIds.push(block.tool_use_id);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return { orphans: orphanIds.length, orphanIds };
102
+ }
103
+ /**
104
+ * Find plausible backups for a jsonl file.
105
+ * Our jsonl-rewriter uses: ${filePath}.backup (single backup)
106
+ */
107
+ function findBackups(jsonlPath) {
108
+ const dir = path.dirname(jsonlPath);
109
+ const base = path.basename(jsonlPath);
110
+ if (!fs.existsSync(dir))
111
+ return [];
112
+ const candidates = [];
113
+ // Primary backup: exact .backup suffix (from jsonl-rewriter.ts)
114
+ const primaryBackup = `${jsonlPath}.backup`;
115
+ if (fs.existsSync(primaryBackup)) {
116
+ candidates.push(primaryBackup);
117
+ }
118
+ // Also check for other backup patterns in case user has manual backups
119
+ const files = fs.readdirSync(dir);
120
+ const additionalBackups = files
121
+ .filter(f => f.startsWith(base) &&
122
+ f !== base &&
123
+ (f.includes('.bak') || f.includes('.backup') || f.includes('.old') || f.includes('.prev')))
124
+ .map(f => path.join(dir, f))
125
+ .filter(f => !candidates.includes(f)) // Don't duplicate
126
+ .sort((a, b) => {
127
+ // Sort by mtime, newest first
128
+ const am = fs.statSync(a).mtimeMs;
129
+ const bm = fs.statSync(b).mtimeMs;
130
+ return bm - am;
131
+ });
132
+ return [...candidates, ...additionalBackups];
133
+ }
134
+ /**
135
+ * Atomically replace target file with source file.
136
+ * Keeps the corrupted version for forensics.
137
+ */
138
+ function atomicReplace(target, source) {
139
+ const dir = path.dirname(target);
140
+ const tmp = path.join(dir, `${path.basename(target)}.tmp-repair-${Date.now()}`);
141
+ fs.copyFileSync(source, tmp);
142
+ const corrupt = path.join(dir, `${path.basename(target)}.corrupt-${Date.now()}`);
143
+ fs.renameSync(target, corrupt);
144
+ fs.renameSync(tmp, target);
145
+ debugLog('ATOMIC_REPLACE', 'Replaced corrupt file', {
146
+ target,
147
+ source,
148
+ corruptBackup: corrupt,
149
+ });
150
+ }
151
+ /**
152
+ * Surgical repair: drop any line that contains a tool_result whose tool_use_id
153
+ * has not appeared earlier in the file. This unbricks at the cost of losing those results.
154
+ */
155
+ function surgicalRepair(jsonlPath) {
156
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
157
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
158
+ const seenToolUses = new Set();
159
+ const kept = [];
160
+ let removed = 0;
161
+ for (const line of lines) {
162
+ let obj;
163
+ try {
164
+ obj = JSON.parse(line);
165
+ }
166
+ catch {
167
+ // Keep non-JSON lines (shouldn't happen, but safe)
168
+ kept.push(line);
169
+ continue;
170
+ }
171
+ const blocks = obj?.message?.content;
172
+ if (!Array.isArray(blocks)) {
173
+ kept.push(line);
174
+ continue;
175
+ }
176
+ // First pass: collect tool_use IDs from this line
177
+ const lineToolUseIds = [];
178
+ for (const b of blocks) {
179
+ const block = b;
180
+ if (block?.type === 'tool_use' && typeof block?.id === 'string') {
181
+ lineToolUseIds.push(block.id);
182
+ }
183
+ }
184
+ // Second pass: check for orphan tool_results
185
+ let hasOrphanResult = false;
186
+ for (const b of blocks) {
187
+ const block = b;
188
+ if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
189
+ if (!seenToolUses.has(block.tool_use_id)) {
190
+ hasOrphanResult = true;
191
+ debugLog('SURGICAL_DROP', 'Dropping line with orphan tool_result', {
192
+ tool_use_id: block.tool_use_id,
193
+ linePreview: line.slice(0, 200),
194
+ });
195
+ }
196
+ }
197
+ }
198
+ if (hasOrphanResult) {
199
+ removed++;
200
+ continue;
201
+ }
202
+ // If kept, update seen tool uses AFTER keeping (order matters)
203
+ for (const id of lineToolUseIds) {
204
+ seenToolUses.add(id);
205
+ }
206
+ kept.push(line);
207
+ }
208
+ // Write atomically
209
+ const dir = path.dirname(jsonlPath);
210
+ const tmp = path.join(dir, `${path.basename(jsonlPath)}.tmp-surgical-${Date.now()}`);
211
+ fs.writeFileSync(tmp, kept.join('\n') + '\n', 'utf-8');
212
+ const corrupt = path.join(dir, `${path.basename(jsonlPath)}.corrupt-${Date.now()}`);
213
+ fs.renameSync(jsonlPath, corrupt);
214
+ fs.renameSync(tmp, jsonlPath);
215
+ const { orphans: orphansAfter } = countOrphansInJsonl(jsonlPath);
216
+ debugLog('SURGICAL_COMPLETE', 'Surgical repair finished', {
217
+ removed,
218
+ orphansAfter,
219
+ corruptBackup: corrupt,
220
+ });
221
+ return { removed, orphansAfter };
222
+ }
223
+ /**
224
+ * Main entry point: repair or rollback a transcript with orphan tool_results.
225
+ *
226
+ * Strategy:
227
+ * 1. Check if there are orphans (exit early if none)
228
+ * 2. Try rollback to newest valid backup
229
+ * 3. If no valid backup, surgically remove orphan lines
230
+ */
231
+ function repairOrRollbackTranscript(jsonlPath) {
232
+ debugLog('REPAIR_START', '═══════════════════════════════════════════════════════════', {
233
+ action: 'TRANSCRIPT_REPAIR_INITIATED',
234
+ jsonlPath,
235
+ timestamp: new Date().toISOString(),
236
+ });
237
+ if (!fs.existsSync(jsonlPath)) {
238
+ debugLog('REPAIR_ABORT', 'JSONL file not found', { jsonlPath });
239
+ return { action: 'failed', orphansFound: 0, reason: 'jsonl_not_found' };
240
+ }
241
+ const initial = countOrphansInJsonl(jsonlPath);
242
+ debugLog('REPAIR_SCAN', 'Initial orphan count', {
243
+ orphans: initial.orphans,
244
+ orphanIds: initial.orphanIds,
245
+ });
246
+ if (initial.orphans === 0) {
247
+ debugLog('REPAIR_CLEAN', 'No orphans found - transcript is healthy');
248
+ return { action: 'none', orphansFound: 0 };
249
+ }
250
+ // 1) Try rollback to newest valid backup
251
+ const backups = findBackups(jsonlPath);
252
+ debugLog('REPAIR_BACKUPS', `Found ${backups.length} backup candidates`, { backups });
253
+ for (const backup of backups) {
254
+ try {
255
+ const v = countOrphansInJsonl(backup);
256
+ debugLog('REPAIR_CHECK_BACKUP', 'Validating backup', {
257
+ backup,
258
+ orphans: v.orphans,
259
+ });
260
+ if (v.orphans === 0) {
261
+ atomicReplace(jsonlPath, backup);
262
+ debugLog('REPAIR_ROLLBACK_SUCCESS', 'Rolled back to valid backup', { backup });
263
+ return { action: 'rollback', orphansFound: initial.orphans, backupUsed: backup };
264
+ }
265
+ }
266
+ catch (err) {
267
+ debugLog('REPAIR_BACKUP_ERROR', 'Failed to check backup', {
268
+ backup,
269
+ error: err.message,
270
+ });
271
+ }
272
+ }
273
+ // 2) Surgical repair fallback
274
+ debugLog('REPAIR_SURGICAL', 'No valid backup found - attempting surgical repair');
275
+ const { removed, orphansAfter } = surgicalRepair(jsonlPath);
276
+ if (orphansAfter === 0) {
277
+ return { action: 'surgical_repair', orphansFound: initial.orphans, removedLines: removed };
278
+ }
279
+ debugLog('REPAIR_FAILED', 'Surgical repair incomplete - orphans remain', {
280
+ orphansAfter,
281
+ removed,
282
+ });
283
+ return {
284
+ action: 'failed',
285
+ orphansFound: initial.orphans,
286
+ removedLines: removed,
287
+ reason: 'orphans_remain_after_surgical',
288
+ };
289
+ }
290
+ /**
291
+ * Quick validation check - can be called before operations to verify health.
292
+ */
293
+ function isTranscriptHealthy(jsonlPath) {
294
+ if (!fs.existsSync(jsonlPath))
295
+ return true; // Non-existent is not unhealthy
296
+ const { orphans } = countOrphansInJsonl(jsonlPath);
297
+ return orphans === 0;
298
+ }
299
+ /**
300
+ * Get detailed transcript health info.
301
+ */
302
+ function getTranscriptHealth(jsonlPath) {
303
+ if (!fs.existsSync(jsonlPath)) {
304
+ return { exists: false, healthy: true, orphans: 0, orphanIds: [] };
305
+ }
306
+ const { orphans, orphanIds } = countOrphansInJsonl(jsonlPath);
307
+ return { exists: true, healthy: orphans === 0, orphans, orphanIds };
308
+ }
@@ -21,10 +21,17 @@ const fs_1 = require("fs");
21
21
  const child_process_1 = require("child_process");
22
22
  const chalk_1 = __importDefault(require("chalk"));
23
23
  const hooks_1 = require("./hooks");
24
+ // ekkOS-managed Claude installation path (same as run.ts)
25
+ const EKKOS_CLAUDE_BIN = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'claude-code', 'node_modules', '.bin', 'claude');
24
26
  /**
25
- * Check if a command exists in PATH
27
+ * Check if a command exists in PATH or managed location
28
+ * For 'claude', prioritizes ekkOS-managed installation
26
29
  */
27
30
  function commandExists(cmd) {
31
+ // For claude, check managed path first
32
+ if (cmd === 'claude' && (0, fs_1.existsSync)(EKKOS_CLAUDE_BIN)) {
33
+ return true;
34
+ }
28
35
  try {
29
36
  const which = (0, os_1.platform)() === 'win32' ? 'where' : 'which';
30
37
  (0, child_process_1.execSync)(`${which} ${cmd}`, { stdio: 'ignore' });
@@ -36,8 +43,23 @@ function commandExists(cmd) {
36
43
  }
37
44
  /**
38
45
  * Get command version safely
46
+ * For 'claude', prioritizes ekkOS-managed installation
39
47
  */
40
48
  function getVersion(cmd, versionFlag = '--version') {
49
+ // For claude, try managed path first
50
+ if (cmd === 'claude' && (0, fs_1.existsSync)(EKKOS_CLAUDE_BIN)) {
51
+ try {
52
+ const output = (0, child_process_1.execSync)(`"${EKKOS_CLAUDE_BIN}" ${versionFlag}`, {
53
+ encoding: 'utf-8',
54
+ timeout: 10000,
55
+ stdio: ['pipe', 'pipe', 'pipe']
56
+ }).trim();
57
+ return output.split('\n')[0];
58
+ }
59
+ catch {
60
+ // Fall through to PATH lookup
61
+ }
62
+ }
41
63
  try {
42
64
  const output = (0, child_process_1.execSync)(`${cmd} ${versionFlag}`, {
43
65
  encoding: 'utf-8',
@@ -5,6 +5,8 @@ interface RunOptions {
5
5
  doctor?: boolean;
6
6
  noInject?: boolean;
7
7
  research?: boolean;
8
+ noDna?: boolean;
9
+ noProxy?: boolean;
8
10
  slashOpenDelayMs?: number;
9
11
  charDelayMs?: number;
10
12
  postEnterDelayMs?: number;