@ekkos/cli 0.3.3 → 1.0.1

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 (81) hide show
  1. package/README.md +57 -0
  2. package/dist/agent/daemon.d.ts +27 -0
  3. package/dist/agent/daemon.js +254 -29
  4. package/dist/agent/health-check.d.ts +35 -0
  5. package/dist/agent/health-check.js +243 -0
  6. package/dist/agent/pty-runner.d.ts +1 -0
  7. package/dist/agent/pty-runner.js +6 -1
  8. package/dist/capture/transcript-repair.d.ts +1 -0
  9. package/dist/capture/transcript-repair.js +12 -1
  10. package/dist/commands/agent.d.ts +6 -0
  11. package/dist/commands/agent.js +244 -0
  12. package/dist/commands/dashboard.d.ts +25 -0
  13. package/dist/commands/dashboard.js +1175 -0
  14. package/dist/commands/run.d.ts +3 -0
  15. package/dist/commands/run.js +503 -350
  16. package/dist/commands/setup-remote.js +146 -37
  17. package/dist/commands/swarm-dashboard.d.ts +20 -0
  18. package/dist/commands/swarm-dashboard.js +735 -0
  19. package/dist/commands/swarm-setup.d.ts +10 -0
  20. package/dist/commands/swarm-setup.js +956 -0
  21. package/dist/commands/swarm.d.ts +46 -0
  22. package/dist/commands/swarm.js +441 -0
  23. package/dist/commands/test-claude.d.ts +16 -0
  24. package/dist/commands/test-claude.js +156 -0
  25. package/dist/commands/usage/blocks.d.ts +8 -0
  26. package/dist/commands/usage/blocks.js +60 -0
  27. package/dist/commands/usage/daily.d.ts +9 -0
  28. package/dist/commands/usage/daily.js +96 -0
  29. package/dist/commands/usage/dashboard.d.ts +8 -0
  30. package/dist/commands/usage/dashboard.js +104 -0
  31. package/dist/commands/usage/formatters.d.ts +41 -0
  32. package/dist/commands/usage/formatters.js +147 -0
  33. package/dist/commands/usage/index.d.ts +13 -0
  34. package/dist/commands/usage/index.js +87 -0
  35. package/dist/commands/usage/monthly.d.ts +8 -0
  36. package/dist/commands/usage/monthly.js +66 -0
  37. package/dist/commands/usage/session.d.ts +11 -0
  38. package/dist/commands/usage/session.js +193 -0
  39. package/dist/commands/usage/weekly.d.ts +9 -0
  40. package/dist/commands/usage/weekly.js +61 -0
  41. package/dist/deploy/instructions.d.ts +5 -2
  42. package/dist/deploy/instructions.js +11 -8
  43. package/dist/index.js +256 -20
  44. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  45. package/dist/lib/tmux-scrollbar.js +296 -0
  46. package/dist/lib/usage-parser.d.ts +95 -5
  47. package/dist/lib/usage-parser.js +416 -71
  48. package/dist/utils/log-rotate.d.ts +18 -0
  49. package/dist/utils/log-rotate.js +74 -0
  50. package/dist/utils/platform.d.ts +2 -0
  51. package/dist/utils/platform.js +3 -1
  52. package/dist/utils/session-binding.d.ts +5 -0
  53. package/dist/utils/session-binding.js +46 -0
  54. package/dist/utils/state.js +4 -0
  55. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  56. package/dist/utils/verify-remote-terminal.js +415 -0
  57. package/package.json +16 -11
  58. package/templates/CLAUDE.md +135 -23
  59. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  60. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  61. package/templates/cursor-hooks/stop.sh +0 -0
  62. package/templates/ekkos-manifest.json +5 -5
  63. package/templates/hooks/assistant-response.sh +0 -0
  64. package/templates/hooks/lib/contract.sh +43 -31
  65. package/templates/hooks/lib/count-tokens.cjs +86 -0
  66. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  67. package/templates/hooks/lib/state.sh +53 -1
  68. package/templates/hooks/session-start.sh +0 -0
  69. package/templates/hooks/stop.sh +150 -388
  70. package/templates/hooks/user-prompt-submit.sh +353 -443
  71. package/templates/plan-template.md +0 -0
  72. package/templates/spec-template.md +0 -0
  73. package/templates/windsurf-hooks/README.md +212 -0
  74. package/templates/windsurf-hooks/hooks.json +9 -2
  75. package/templates/windsurf-hooks/install.sh +148 -0
  76. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  77. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  78. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  79. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  80. package/LICENSE +0 -21
  81. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Swarm CLI Commands
3
+ *
4
+ * Q-Learning model routing + multi-agent parallel task execution.
5
+ *
6
+ * Commands:
7
+ * ekkos swarm status — Show Q-table stats
8
+ * ekkos swarm reset — Clear Q-table from Redis
9
+ * ekkos swarm export — Dump Q-table to .swarm/q-learning-model.json
10
+ * ekkos swarm import — Load .swarm/q-learning-model.json into Redis
11
+ * ekkos swarm launch — Launch parallel workers on a decomposed task
12
+ */
13
+ export declare function swarmStatus(): Promise<void>;
14
+ export declare function swarmReset(): Promise<void>;
15
+ export declare function swarmExport(): Promise<void>;
16
+ export declare function swarmImport(): Promise<void>;
17
+ export interface SwarmLaunchOptions {
18
+ workers: number;
19
+ task: string;
20
+ bypass?: boolean;
21
+ noDecompose?: boolean;
22
+ noQueen?: boolean;
23
+ verbose?: boolean;
24
+ queenStrategy?: string;
25
+ modelTiers?: ('opus' | 'sonnet' | 'haiku')[];
26
+ interactive?: boolean;
27
+ }
28
+ /**
29
+ * Launch N parallel workers in a tmux session, each working on a subtask.
30
+ *
31
+ * Architecture:
32
+ * - Uses tmux WINDOWS (not panes) — compatible with Python Queen discovery
33
+ * - Window 0: Swarm Dashboard (blessed TUI showing all workers)
34
+ * - Windows 1-N: Workers running `ekkos run -b` (proxy + infinite context)
35
+ * - Each worker gets full ekkOS stack: proxy, hooks, ccDNA, context eviction
36
+ *
37
+ * Flow:
38
+ * 1. Decompose task via Gemini Flash (free, fast)
39
+ * 2. Create tmux session: dashboard window + N worker windows
40
+ * 3. Each worker window runs `ekkos run -b` (ekkOS CLI, not vanilla Claude)
41
+ * 4. Wait for Claude Code to initialize (PTY + proxy + hooks)
42
+ * 5. Send subtask to each worker via tmux send-keys
43
+ * 6. Start swarm dashboard in window 0
44
+ * 7. Attach to tmux session (dashboard is default view)
45
+ */
46
+ export declare function swarmLaunch(options: SwarmLaunchOptions): Promise<string | void>;
@@ -0,0 +1,441 @@
1
+ "use strict";
2
+ /**
3
+ * Swarm CLI Commands
4
+ *
5
+ * Q-Learning model routing + multi-agent parallel task execution.
6
+ *
7
+ * Commands:
8
+ * ekkos swarm status — Show Q-table stats
9
+ * ekkos swarm reset — Clear Q-table from Redis
10
+ * ekkos swarm export — Dump Q-table to .swarm/q-learning-model.json
11
+ * ekkos swarm import — Load .swarm/q-learning-model.json into Redis
12
+ * ekkos swarm launch — Launch parallel workers on a decomposed task
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.swarmStatus = swarmStatus;
19
+ exports.swarmReset = swarmReset;
20
+ exports.swarmExport = swarmExport;
21
+ exports.swarmImport = swarmImport;
22
+ exports.swarmLaunch = swarmLaunch;
23
+ const chalk_1 = __importDefault(require("chalk"));
24
+ const ora_1 = __importDefault(require("ora"));
25
+ const os_1 = require("os");
26
+ const path_1 = require("path");
27
+ const fs_1 = require("fs");
28
+ const child_process_1 = require("child_process");
29
+ const PROXY_API_URL = process.env.EKKOS_PROXY_URL || 'https://proxy.ekkos.dev';
30
+ const CONFIG_FILE = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'config.json');
31
+ const SWARM_DIR = (0, path_1.join)(process.cwd(), '.swarm');
32
+ const SWARM_FILE = (0, path_1.join)(SWARM_DIR, 'q-learning-model.json');
33
+ function getUserId() {
34
+ try {
35
+ if ((0, fs_1.existsSync)(CONFIG_FILE)) {
36
+ const config = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
37
+ return config.userId || null;
38
+ }
39
+ }
40
+ catch { }
41
+ return null;
42
+ }
43
+ async function swarmStatus() {
44
+ const userId = getUserId();
45
+ if (!userId) {
46
+ console.log(chalk_1.default.red('Not authenticated. Run `ekkos init` first.'));
47
+ process.exit(1);
48
+ }
49
+ const spinner = (0, ora_1.default)('Fetching Q-table stats...').start();
50
+ try {
51
+ const res = await fetch(`${PROXY_API_URL}/swarm/status/${userId}`);
52
+ if (!res.ok)
53
+ throw new Error(`HTTP ${res.status}`);
54
+ const stats = await res.json();
55
+ spinner.stop();
56
+ console.log('');
57
+ console.log(chalk_1.default.cyan.bold('🐝 Swarm Q-Learning Status'));
58
+ console.log(chalk_1.default.gray('─'.repeat(50)));
59
+ console.log('');
60
+ console.log(` ${chalk_1.default.gray('States explored:')} ${chalk_1.default.bold(String(stats.totalStates))}/48`);
61
+ console.log(` ${chalk_1.default.gray('Total visits:')} ${chalk_1.default.bold(String(stats.totalVisits))}`);
62
+ console.log(` ${chalk_1.default.gray('Total updates:')} ${chalk_1.default.bold(String(stats.totalUpdates))}`);
63
+ console.log(` ${chalk_1.default.gray('Epsilon (ε):')} ${chalk_1.default.bold(stats.epsilon.toFixed(4))} ${chalk_1.default.gray(`(${stats.epsilon > 0.1 ? 'exploring' : 'exploiting'})`)}`);
64
+ console.log(` ${chalk_1.default.gray('Last updated:')} ${stats.lastUpdated ? new Date(stats.lastUpdated).toLocaleString() : 'never'}`);
65
+ if (stats.topStates && stats.topStates.length > 0) {
66
+ console.log('');
67
+ console.log(chalk_1.default.gray(' Top states by visits:'));
68
+ console.log('');
69
+ console.log(` ${chalk_1.default.gray('State')} ${chalk_1.default.gray('Visits')} ${chalk_1.default.gray('Best')} ${chalk_1.default.gray('Q-Values (opus/sonnet/haiku)')}`);
70
+ for (const s of stats.topStates) {
71
+ const tierColor = s.bestAction === 'opus' ? chalk_1.default.magenta : s.bestAction === 'sonnet' ? chalk_1.default.blue : chalk_1.default.green;
72
+ const qStr = s.qValues.map((v) => v.toFixed(2)).join(' / ');
73
+ console.log(` ${chalk_1.default.white(s.key.padEnd(24))} ${String(s.visits).padStart(6)} ${tierColor(s.bestAction.padEnd(8))} ${chalk_1.default.gray(qStr)}`);
74
+ }
75
+ }
76
+ else {
77
+ console.log('');
78
+ console.log(chalk_1.default.gray(' No Q-table data yet. Start a session to begin learning.'));
79
+ }
80
+ console.log('');
81
+ }
82
+ catch (err) {
83
+ spinner.fail(`Failed to fetch stats: ${err.message}`);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ async function swarmReset() {
88
+ const userId = getUserId();
89
+ if (!userId) {
90
+ console.log(chalk_1.default.red('Not authenticated. Run `ekkos init` first.'));
91
+ process.exit(1);
92
+ }
93
+ const spinner = (0, ora_1.default)('Resetting Q-table...').start();
94
+ try {
95
+ const res = await fetch(`${PROXY_API_URL}/swarm/reset/${userId}`, { method: 'DELETE' });
96
+ if (!res.ok)
97
+ throw new Error(`HTTP ${res.status}`);
98
+ spinner.succeed(chalk_1.default.green('Q-table cleared. Routing will use static rules until new data is learned.'));
99
+ }
100
+ catch (err) {
101
+ spinner.fail(`Failed to reset: ${err.message}`);
102
+ process.exit(1);
103
+ }
104
+ }
105
+ async function swarmExport() {
106
+ const userId = getUserId();
107
+ if (!userId) {
108
+ console.log(chalk_1.default.red('Not authenticated. Run `ekkos init` first.'));
109
+ process.exit(1);
110
+ }
111
+ const spinner = (0, ora_1.default)('Exporting Q-table...').start();
112
+ try {
113
+ const res = await fetch(`${PROXY_API_URL}/swarm/export/${userId}`);
114
+ if (!res.ok)
115
+ throw new Error(`HTTP ${res.status}`);
116
+ const data = await res.json();
117
+ // Ensure .swarm directory exists
118
+ if (!(0, fs_1.existsSync)(SWARM_DIR)) {
119
+ (0, fs_1.mkdirSync)(SWARM_DIR, { recursive: true });
120
+ }
121
+ (0, fs_1.writeFileSync)(SWARM_FILE, JSON.stringify(data, null, 2));
122
+ spinner.succeed(chalk_1.default.green(`Q-table exported to ${SWARM_FILE}`));
123
+ const entries = Object.keys(data.entries || {}).length;
124
+ console.log(chalk_1.default.gray(` ${entries} states, ${data.stats?.totalUpdates || 0} updates`));
125
+ }
126
+ catch (err) {
127
+ spinner.fail(`Failed to export: ${err.message}`);
128
+ process.exit(1);
129
+ }
130
+ }
131
+ async function swarmImport() {
132
+ const userId = getUserId();
133
+ if (!userId) {
134
+ console.log(chalk_1.default.red('Not authenticated. Run `ekkos init` first.'));
135
+ process.exit(1);
136
+ }
137
+ if (!(0, fs_1.existsSync)(SWARM_FILE)) {
138
+ console.log(chalk_1.default.red(`No Q-table file found at ${SWARM_FILE}`));
139
+ console.log(chalk_1.default.gray(' Run `ekkos swarm export` first, or place a q-learning-model.json in .swarm/'));
140
+ process.exit(1);
141
+ }
142
+ const spinner = (0, ora_1.default)('Importing Q-table...').start();
143
+ try {
144
+ const raw = (0, fs_1.readFileSync)(SWARM_FILE, 'utf-8');
145
+ const data = JSON.parse(raw);
146
+ const res = await fetch(`${PROXY_API_URL}/swarm/import/${userId}`, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify(data),
150
+ });
151
+ if (!res.ok)
152
+ throw new Error(`HTTP ${res.status}`);
153
+ const result = await res.json();
154
+ spinner.succeed(chalk_1.default.green(`Imported ${result.imported} states into Q-table`));
155
+ }
156
+ catch (err) {
157
+ spinner.fail(`Failed to import: ${err.message}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ /**
162
+ * Locate the ekkos-swarm Python app directory.
163
+ * Checks: EKKOS_SWARM_DIR env → monorepo relative paths.
164
+ */
165
+ function findSwarmDir() {
166
+ // Env override
167
+ const envDir = process.env.EKKOS_SWARM_DIR;
168
+ if (envDir && (0, fs_1.existsSync)(envDir))
169
+ return envDir;
170
+ // Monorepo relative paths (from cwd or from CLI package)
171
+ const candidates = [
172
+ (0, path_1.join)(process.cwd(), 'apps', 'ekkos-swarm'),
173
+ (0, path_1.join)(__dirname, '..', '..', '..', '..', 'apps', 'ekkos-swarm'),
174
+ ];
175
+ for (const dir of candidates) {
176
+ if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'cli', 'main.py')))
177
+ return dir;
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * Find Python3 inside the swarm venv.
183
+ */
184
+ function findPythonPath(swarmDir) {
185
+ const venvPython = (0, path_1.join)(swarmDir, '.venv', 'bin', 'python3');
186
+ if ((0, fs_1.existsSync)(venvPython))
187
+ return venvPython;
188
+ return null;
189
+ }
190
+ /**
191
+ * Launch N parallel workers in a tmux session, each working on a subtask.
192
+ *
193
+ * Architecture:
194
+ * - Uses tmux WINDOWS (not panes) — compatible with Python Queen discovery
195
+ * - Window 0: Swarm Dashboard (blessed TUI showing all workers)
196
+ * - Windows 1-N: Workers running `ekkos run -b` (proxy + infinite context)
197
+ * - Each worker gets full ekkOS stack: proxy, hooks, ccDNA, context eviction
198
+ *
199
+ * Flow:
200
+ * 1. Decompose task via Gemini Flash (free, fast)
201
+ * 2. Create tmux session: dashboard window + N worker windows
202
+ * 3. Each worker window runs `ekkos run -b` (ekkOS CLI, not vanilla Claude)
203
+ * 4. Wait for Claude Code to initialize (PTY + proxy + hooks)
204
+ * 5. Send subtask to each worker via tmux send-keys
205
+ * 6. Start swarm dashboard in window 0
206
+ * 7. Attach to tmux session (dashboard is default view)
207
+ */
208
+ async function swarmLaunch(options) {
209
+ const { workers, task, bypass = true, noDecompose = false, noQueen = false, verbose = false, queenStrategy, modelTiers, interactive = false, } = options;
210
+ const log = interactive ? (..._args) => { } : console.log;
211
+ // Validate
212
+ if (workers < 2 || workers > 8) {
213
+ log(chalk_1.default.red('Worker count must be between 2 and 8.'));
214
+ if (!interactive)
215
+ process.exit(1);
216
+ throw new Error('Worker count must be between 2 and 8');
217
+ }
218
+ if (!task || task.trim().length === 0) {
219
+ log(chalk_1.default.red('Task is required. Use --task "describe what to do"'));
220
+ if (!interactive)
221
+ process.exit(1);
222
+ throw new Error('Task is required');
223
+ }
224
+ // Check tmux
225
+ try {
226
+ (0, child_process_1.execSync)('which tmux', { stdio: 'pipe' });
227
+ }
228
+ catch {
229
+ log(chalk_1.default.red('tmux is required for swarm launch.'));
230
+ log(chalk_1.default.gray(' Install: brew install tmux'));
231
+ if (!interactive)
232
+ process.exit(1);
233
+ throw new Error('tmux is required');
234
+ }
235
+ log('');
236
+ log(chalk_1.default.cyan.bold('🐝 Swarm Launch'));
237
+ log(chalk_1.default.gray('─'.repeat(50)));
238
+ log(` ${chalk_1.default.gray('Task:')} ${chalk_1.default.white(task)}`);
239
+ log(` ${chalk_1.default.gray('Workers:')} ${chalk_1.default.bold(String(workers))}`);
240
+ log('');
241
+ const cwd = process.cwd();
242
+ const ekkosCmd = process.argv[1];
243
+ const launchTs = Date.now();
244
+ // ── Step 1: Decompose task into subtasks ──────────────────────────────
245
+ let subtasks;
246
+ if (noDecompose) {
247
+ subtasks = Array.from({ length: workers }, (_, i) => `You are worker ${i + 1} of ${workers} in a parallel swarm. ` +
248
+ `Focus on your portion and avoid overlapping with other workers. ` +
249
+ `Task: ${task}`);
250
+ log(chalk_1.default.gray(' Skipping decomposition (--no-decompose)'));
251
+ }
252
+ else {
253
+ const spinner = interactive ? { start: () => spinner, succeed: (_m) => { }, fail: (_m) => { } } : (0, ora_1.default)('Decomposing task into subtasks...').start();
254
+ try {
255
+ const res = await fetch(`${PROXY_API_URL}/swarm/decompose`, {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json' },
258
+ body: JSON.stringify({
259
+ task,
260
+ workerCount: workers,
261
+ context: `Working directory: ${cwd}`,
262
+ }),
263
+ });
264
+ if (!res.ok) {
265
+ const errData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
266
+ throw new Error(errData.error || `HTTP ${res.status}`);
267
+ }
268
+ const data = await res.json();
269
+ subtasks = data.subtasks;
270
+ if (!subtasks || subtasks.length === 0) {
271
+ throw new Error('No subtasks returned');
272
+ }
273
+ spinner.succeed(chalk_1.default.green(`Decomposed into ${subtasks.length} subtasks`));
274
+ for (let i = 0; i < subtasks.length; i++) {
275
+ const color = [chalk_1.default.magenta, chalk_1.default.blue, chalk_1.default.green, chalk_1.default.yellow, chalk_1.default.cyan, chalk_1.default.red, chalk_1.default.white, chalk_1.default.gray][i % 8];
276
+ log(` ${color(`Worker ${i + 1}:`)} ${subtasks[i].slice(0, 80)}${subtasks[i].length > 80 ? '...' : ''}`);
277
+ }
278
+ log('');
279
+ }
280
+ catch (err) {
281
+ spinner.fail(`Decomposition failed: ${err.message}`);
282
+ log(chalk_1.default.yellow(' Falling back to sending same task to all workers.'));
283
+ subtasks = Array.from({ length: workers }, (_, i) => `You are worker ${i + 1} of ${workers} in a parallel swarm. Task: ${task}`);
284
+ }
285
+ }
286
+ // ── Step 2: Create tmux session with windows ──────────────────────────
287
+ // Uses WINDOWS (not panes) — Queen discovers agents via `tmux list-windows`
288
+ const tmuxSession = `ekkos-swarm-${Date.now().toString(36)}`;
289
+ const runArgs = ['run'];
290
+ if (bypass)
291
+ runArgs.push('-b');
292
+ const runCommand = `node "${ekkosCmd}" ${runArgs.join(' ')}`;
293
+ const noopSpinner = { start: () => noopSpinner, succeed: (_m) => { }, fail: (_m) => { } };
294
+ const launchSpinner = interactive ? noopSpinner : (0, ora_1.default)('Creating swarm tmux session...').start();
295
+ try {
296
+ // Ensure .swarm directory
297
+ if (!(0, fs_1.existsSync)(SWARM_DIR))
298
+ (0, fs_1.mkdirSync)(SWARM_DIR, { recursive: true });
299
+ // Window 0: Dashboard (default view when attaching)
300
+ (0, child_process_1.execSync)(`tmux new-session -d -s "${tmuxSession}" -c "${cwd}" -n "dashboard"`, { stdio: 'pipe' });
301
+ // Configure tmux options
302
+ const tmuxOpts = [
303
+ 'set-option -g mouse on',
304
+ 'set-option -g history-limit 50000',
305
+ 'set-option -g set-clipboard on',
306
+ 'set-option -g allow-passthrough on',
307
+ 'set-option -g mode-keys vi',
308
+ 'set-option -g escape-time 0',
309
+ 'set-option -g focus-events on',
310
+ 'set-option -g default-terminal "xterm-256color"',
311
+ ];
312
+ for (const opt of tmuxOpts) {
313
+ try {
314
+ (0, child_process_1.execSync)(`tmux ${opt} -t "${tmuxSession}"`, { stdio: 'pipe' });
315
+ }
316
+ catch { }
317
+ }
318
+ // Worker windows: 1 through N (named worker-1, worker-2, etc.)
319
+ for (let i = 0; i < workers; i++) {
320
+ (0, child_process_1.execSync)(`tmux new-window -t "${tmuxSession}" -c "${cwd}" -n "worker-${i + 1}"`, { stdio: 'pipe' });
321
+ }
322
+ launchSpinner.succeed(chalk_1.default.green(`Created ${workers + 1} windows (dashboard + ${workers} workers)`));
323
+ // ── Step 3: Start ekkos run in each worker window ───────────────
324
+ const startSpinner = interactive ? noopSpinner : (0, ora_1.default)('Starting ekkos run in workers...').start();
325
+ for (let i = 0; i < workers; i++) {
326
+ // Pass model tier as env var per worker (if specified)
327
+ const tierEnv = modelTiers && modelTiers[i] ? `EKKOS_MODEL_TIER=${modelTiers[i]} ` : '';
328
+ const workerCmd = `${tierEnv}${runCommand}`;
329
+ (0, child_process_1.spawnSync)('tmux', ['send-keys', '-t', `${tmuxSession}:worker-${i + 1}`, workerCmd, 'Enter'], { stdio: 'pipe' });
330
+ }
331
+ startSpinner.succeed(chalk_1.default.green(`Started ${workers} ekkos instances (proxy + infinite context)`));
332
+ // ── Step 4: Write swarm metadata ────────────────────────────────
333
+ const metaPath = (0, path_1.join)(SWARM_DIR, 'active-swarm.json');
334
+ // queenEnabled is set later, but metadata is written here before queen launch.
335
+ // We'll update metadata after queen launch in step 6b.
336
+ const swarmMeta = {
337
+ tmuxSession,
338
+ workers,
339
+ task,
340
+ subtasks,
341
+ startedAt: new Date().toISOString(),
342
+ launchTs,
343
+ cwd,
344
+ queenEnabled: false,
345
+ queenStrategy: queenStrategy || 'adaptive-default',
346
+ modelTiers: modelTiers || [],
347
+ };
348
+ (0, fs_1.writeFileSync)(metaPath, JSON.stringify(swarmMeta, null, 2));
349
+ // ── Step 5: Wait for Claude Code to initialize ──────────────────
350
+ const waitSpinner = interactive ? noopSpinner : (0, ora_1.default)('Waiting for Claude Code to initialize (15s)...').start();
351
+ await new Promise(resolve => setTimeout(resolve, 15000));
352
+ waitSpinner.succeed(chalk_1.default.green('Workers initialized'));
353
+ // ── Step 6: Send subtasks to each worker ────────────────────────
354
+ const sendSpinner = interactive ? noopSpinner : (0, ora_1.default)('Sending subtasks to workers...').start();
355
+ for (let i = 0; i < workers; i++) {
356
+ const subtask = subtasks[i] || subtasks[subtasks.length - 1];
357
+ // Send text first, then Enter separately after a delay.
358
+ // Claude Code treats combined text+Enter from send-keys as a paste,
359
+ // which puts the text in the prompt but doesn't submit it.
360
+ const result = (0, child_process_1.spawnSync)('tmux', [
361
+ 'send-keys', '-t', `${tmuxSession}:worker-${i + 1}`,
362
+ subtask,
363
+ ], { stdio: 'pipe' });
364
+ if (result.status !== 0) {
365
+ const errMsg = result.stderr?.toString().trim() || 'unknown error';
366
+ log(chalk_1.default.yellow(` Warning: failed to send to worker-${i + 1}: ${errMsg}`));
367
+ }
368
+ else if (verbose) {
369
+ log(chalk_1.default.gray(` Sent to worker-${i + 1}: ${subtask.slice(0, 60)}...`));
370
+ }
371
+ }
372
+ // Wait briefly for Claude Code to process the pasted text, then send Enter
373
+ await new Promise(resolve => setTimeout(resolve, 500));
374
+ for (let i = 0; i < workers; i++) {
375
+ (0, child_process_1.spawnSync)('tmux', [
376
+ 'send-keys', '-t', `${tmuxSession}:worker-${i + 1}`,
377
+ 'Enter',
378
+ ], { stdio: 'pipe' });
379
+ }
380
+ sendSpinner.succeed(chalk_1.default.green(`Dispatched ${workers} subtasks`));
381
+ // ── Step 6b: Start Python Queen coordinator ──────────────────────
382
+ let queenEnabled = false;
383
+ if (!noQueen) {
384
+ const swarmDir = findSwarmDir();
385
+ const pythonPath = swarmDir ? findPythonPath(swarmDir) : null;
386
+ if (swarmDir && pythonPath) {
387
+ try {
388
+ (0, child_process_1.execSync)(`tmux new-window -t "${tmuxSession}" -c "${swarmDir}" -n "queen"`, { stdio: 'pipe' });
389
+ const strategyEnv = queenStrategy ? `EKKOS_QUEEN_STRATEGY=${queenStrategy} ` : '';
390
+ const metaEnv = `EKKOS_SWARM_META=${metaPath} `;
391
+ const queenCmd = `${metaEnv}${strategyEnv}${pythonPath} -m cli.main start ${tmuxSession}`;
392
+ (0, child_process_1.spawnSync)('tmux', ['send-keys', '-t', `${tmuxSession}:queen`, queenCmd, 'Enter'], { stdio: 'pipe' });
393
+ queenEnabled = true;
394
+ log(chalk_1.default.green(' 👑 Queen coordinator started'));
395
+ }
396
+ catch (err) {
397
+ log(chalk_1.default.yellow(` ⚠ Queen window failed: ${err.message}`));
398
+ }
399
+ }
400
+ else {
401
+ const reason = !swarmDir ? 'apps/ekkos-swarm/ not found' : '.venv/bin/python3 not found';
402
+ log(chalk_1.default.yellow(` ⚠ Skipping Queen (${reason})`));
403
+ }
404
+ }
405
+ // Update metadata with queen status
406
+ if (queenEnabled) {
407
+ swarmMeta.queenEnabled = true;
408
+ (0, fs_1.writeFileSync)(metaPath, JSON.stringify(swarmMeta, null, 2));
409
+ }
410
+ // ── Step 7: Start swarm dashboard in window 0 ───────────────────
411
+ const dashCmd = `node "${ekkosCmd}" swarm dashboard --launch-ts ${launchTs}`;
412
+ (0, child_process_1.spawnSync)('tmux', ['send-keys', '-t', `${tmuxSession}:dashboard`, dashCmd, 'Enter'], { stdio: 'pipe' });
413
+ log('');
414
+ log(chalk_1.default.cyan(' Attaching to swarm session...'));
415
+ log(chalk_1.default.gray(' Ctrl+B 0 Dashboard (overview)'));
416
+ log(chalk_1.default.gray(' Ctrl+B 1-N Worker windows'));
417
+ if (queenEnabled) {
418
+ log(chalk_1.default.gray(` Ctrl+B ${workers + 1} Queen coordinator`));
419
+ }
420
+ log(chalk_1.default.gray(' Ctrl+B N/P Next/prev window'));
421
+ log(chalk_1.default.gray(' Ctrl+B D Detach (workers keep running)'));
422
+ log('');
423
+ // ── Step 8: Attach to tmux session (dashboard is window 0) ──────
424
+ (0, child_process_1.execSync)(`tmux select-window -t "${tmuxSession}:dashboard"`, { stdio: 'pipe' });
425
+ // In interactive mode, return the session name so the wizard can attach
426
+ if (interactive) {
427
+ return tmuxSession;
428
+ }
429
+ (0, child_process_1.execSync)(`tmux attach -t "${tmuxSession}"`, { stdio: 'inherit' });
430
+ }
431
+ catch (err) {
432
+ if (interactive)
433
+ throw err;
434
+ launchSpinner.fail(`Swarm launch failed: ${err.message}`);
435
+ try {
436
+ (0, child_process_1.execSync)(`tmux kill-session -t "${tmuxSession}"`, { stdio: 'pipe' });
437
+ }
438
+ catch { }
439
+ process.exit(1);
440
+ }
441
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ekkos test-claude — Bare proxy test (no CLI wrapper)
3
+ *
4
+ * Launches vanilla Claude Code with ONLY the proxy URL set.
5
+ * No ccDNA, no PTY wrapper, no injection, no context management.
6
+ *
7
+ * Purpose: Isolate whether fast context growth is caused by:
8
+ * A) The proxy (IPC, injections, tool compression)
9
+ * B) The ekkos CLI wrapper (ccDNA, PTY, run.ts logic)
10
+ */
11
+ export interface TestClaudeOptions {
12
+ noProxy?: boolean;
13
+ noHooks?: boolean;
14
+ verbose?: boolean;
15
+ }
16
+ export declare function testClaude(options: TestClaudeOptions): Promise<void>;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * ekkos test-claude — Bare proxy test (no CLI wrapper)
4
+ *
5
+ * Launches vanilla Claude Code with ONLY the proxy URL set.
6
+ * No ccDNA, no PTY wrapper, no injection, no context management.
7
+ *
8
+ * Purpose: Isolate whether fast context growth is caused by:
9
+ * A) The proxy (IPC, injections, tool compression)
10
+ * B) The ekkos CLI wrapper (ccDNA, PTY, run.ts logic)
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.testClaude = testClaude;
50
+ const child_process_1 = require("child_process");
51
+ const fs = __importStar(require("fs"));
52
+ const os = __importStar(require("os"));
53
+ const path = __importStar(require("path"));
54
+ const chalk_1 = __importDefault(require("chalk"));
55
+ const state_1 = require("../utils/state");
56
+ const EKKOS_PROXY_URL = 'https://proxy.ekkos.dev';
57
+ async function testClaude(options) {
58
+ const verbose = options.verbose || false;
59
+ console.log(chalk_1.default.cyan('\n ekkOS test-claude — Bare Proxy Test'));
60
+ console.log(chalk_1.default.gray(' ══════════════════════════════════'));
61
+ console.log(chalk_1.default.green(' Proxy: ') + (options.noProxy ? chalk_1.default.yellow('NO') : chalk_1.default.green('YES')));
62
+ console.log(chalk_1.default.green(' CLI: ') + chalk_1.default.yellow('NO (no ccDNA, no PTY, no inject)'));
63
+ console.log(chalk_1.default.green(' Hooks: ') + (options.noHooks ? chalk_1.default.yellow('NO (disabled)') : chalk_1.default.gray('YES (same .claude/hooks/)')));
64
+ console.log('');
65
+ // Resolve Claude path
66
+ const claudePath = resolveClaudePath();
67
+ if (!claudePath) {
68
+ console.error(chalk_1.default.red(' Claude Code not found. Install with: npm install -g @anthropic-ai/claude-code'));
69
+ process.exit(1);
70
+ }
71
+ // Build env — only set ANTHROPIC_BASE_URL if proxy mode
72
+ /* eslint-disable no-restricted-syntax */
73
+ const env = { ...process.env };
74
+ /* eslint-enable no-restricted-syntax */
75
+ if (!options.noProxy) {
76
+ const ekkosConfig = (0, state_1.getConfig)();
77
+ const userId = ekkosConfig?.userId || 'anonymous';
78
+ const projectPath = process.cwd();
79
+ const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
80
+ const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/_pending?project=${projectPathEncoded}`;
81
+ env.ANTHROPIC_BASE_URL = proxyUrl;
82
+ console.log(chalk_1.default.gray(` Proxy URL: ${proxyUrl.replace(userId, userId.slice(0, 8) + '...')}`));
83
+ }
84
+ else {
85
+ console.log(chalk_1.default.gray(' Direct to Anthropic (no proxy)'));
86
+ }
87
+ console.log(chalk_1.default.gray(` Claude: ${claudePath}`));
88
+ console.log('');
89
+ // Disable hooks by temporarily renaming the hooks directory
90
+ const hooksDir = path.join(os.homedir(), '.claude', 'hooks');
91
+ const hooksBackup = path.join(os.homedir(), '.claude', 'hooks.bak-test');
92
+ let hooksDisabled = false;
93
+ if (options.noHooks && fs.existsSync(hooksDir) && !fs.existsSync(hooksBackup)) {
94
+ fs.renameSync(hooksDir, hooksBackup);
95
+ hooksDisabled = true;
96
+ console.log(chalk_1.default.gray(' Hooks temporarily disabled (renamed to hooks.bak-test)'));
97
+ }
98
+ // Also disable project-level hooks
99
+ const projectHooksDir = path.join(process.cwd(), '.claude', 'hooks');
100
+ const projectHooksBackup = path.join(process.cwd(), '.claude', 'hooks.bak-test');
101
+ let projectHooksDisabled = false;
102
+ if (options.noHooks && fs.existsSync(projectHooksDir) && !fs.existsSync(projectHooksBackup)) {
103
+ fs.renameSync(projectHooksDir, projectHooksBackup);
104
+ projectHooksDisabled = true;
105
+ console.log(chalk_1.default.gray(' Project hooks temporarily disabled'));
106
+ }
107
+ // Restore hooks on exit (always, even on crash)
108
+ const restoreHooks = () => {
109
+ if (hooksDisabled && fs.existsSync(hooksBackup)) {
110
+ try {
111
+ fs.renameSync(hooksBackup, hooksDir);
112
+ }
113
+ catch { /* ignore */ }
114
+ }
115
+ if (projectHooksDisabled && fs.existsSync(projectHooksBackup)) {
116
+ try {
117
+ fs.renameSync(projectHooksBackup, projectHooksDir);
118
+ }
119
+ catch { /* ignore */ }
120
+ }
121
+ };
122
+ process.on('SIGINT', restoreHooks);
123
+ process.on('SIGTERM', restoreHooks);
124
+ process.on('exit', restoreHooks);
125
+ console.log('');
126
+ // Spawn Claude directly — no PTY, no wrapper, just exec
127
+ const child = (0, child_process_1.spawn)(claudePath, [], {
128
+ env,
129
+ stdio: 'inherit',
130
+ cwd: process.cwd(),
131
+ });
132
+ child.on('exit', (code) => {
133
+ restoreHooks();
134
+ process.exit(code || 0);
135
+ });
136
+ child.on('error', (err) => {
137
+ restoreHooks();
138
+ console.error(chalk_1.default.red(`Failed to start Claude: ${err.message}`));
139
+ process.exit(1);
140
+ });
141
+ }
142
+ function resolveClaudePath() {
143
+ // 1. ekkOS managed installation
144
+ const ekkosClaudeBin = path.join(os.homedir(), '.ekkos', 'claude-code', 'node_modules', '.bin', 'claude');
145
+ if (fs.existsSync(ekkosClaudeBin))
146
+ return ekkosClaudeBin;
147
+ // 2. Homebrew
148
+ try {
149
+ const brewPath = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
150
+ if (brewPath && fs.existsSync(brewPath))
151
+ return brewPath;
152
+ }
153
+ catch { /* ignore */ }
154
+ // 3. npx fallback
155
+ return 'npx';
156
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ekkos usage blocks [--json]
3
+ *
4
+ * Show 5-hour billing block analysis
5
+ */
6
+ export declare function blocksCommand(options: {
7
+ json?: boolean;
8
+ }): Promise<void>;