@ekkos/cli 1.3.2 → 1.3.6

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 (123) hide show
  1. package/dist/capture/jsonl-rewriter.d.ts +1 -1
  2. package/dist/capture/jsonl-rewriter.js +3 -3
  3. package/dist/capture/transcript-repair.d.ts +2 -2
  4. package/dist/capture/transcript-repair.js +2 -2
  5. package/dist/commands/claw.d.ts +13 -0
  6. package/dist/commands/claw.js +253 -0
  7. package/dist/commands/dashboard.js +617 -83
  8. package/dist/commands/doctor.d.ts +3 -3
  9. package/dist/commands/doctor.js +6 -79
  10. package/dist/commands/gemini.d.ts +19 -0
  11. package/dist/commands/gemini.js +193 -0
  12. package/dist/commands/init.js +2 -25
  13. package/dist/commands/run.d.ts +0 -1
  14. package/dist/commands/run.js +147 -241
  15. package/dist/commands/scan.d.ts +21 -0
  16. package/dist/commands/scan.js +386 -0
  17. package/dist/commands/swarm-dashboard.js +156 -28
  18. package/dist/commands/swarm.d.ts +1 -1
  19. package/dist/commands/swarm.js +1 -1
  20. package/dist/commands/test-claude.d.ts +2 -2
  21. package/dist/commands/test-claude.js +3 -3
  22. package/dist/deploy/index.d.ts +0 -2
  23. package/dist/deploy/index.js +0 -2
  24. package/dist/deploy/settings.d.ts +2 -2
  25. package/dist/deploy/settings.js +42 -4
  26. package/dist/deploy/skills.js +1 -2
  27. package/dist/index.js +79 -19
  28. package/dist/lib/usage-parser.js +5 -4
  29. package/dist/utils/proxy-url.d.ts +12 -1
  30. package/dist/utils/proxy-url.js +16 -1
  31. package/dist/utils/templates.js +1 -1
  32. package/package.json +4 -6
  33. package/templates/CLAUDE.md +49 -107
  34. package/dist/agent/daemon.d.ts +0 -130
  35. package/dist/agent/daemon.js +0 -606
  36. package/dist/agent/health-check.d.ts +0 -35
  37. package/dist/agent/health-check.js +0 -243
  38. package/dist/agent/pty-runner.d.ts +0 -53
  39. package/dist/agent/pty-runner.js +0 -190
  40. package/dist/commands/agent.d.ts +0 -50
  41. package/dist/commands/agent.js +0 -544
  42. package/dist/commands/setup-remote.d.ts +0 -20
  43. package/dist/commands/setup-remote.js +0 -582
  44. package/dist/commands/synk.d.ts +0 -7
  45. package/dist/commands/synk.js +0 -339
  46. package/dist/cron/index.d.ts +0 -7
  47. package/dist/cron/index.js +0 -13
  48. package/dist/cron/promoter.d.ts +0 -70
  49. package/dist/cron/promoter.js +0 -403
  50. package/dist/synk/api.d.ts +0 -22
  51. package/dist/synk/api.js +0 -133
  52. package/dist/synk/auth.d.ts +0 -7
  53. package/dist/synk/auth.js +0 -30
  54. package/dist/synk/config.d.ts +0 -18
  55. package/dist/synk/config.js +0 -37
  56. package/dist/synk/daemon/control-client.d.ts +0 -11
  57. package/dist/synk/daemon/control-client.js +0 -101
  58. package/dist/synk/daemon/control-server.d.ts +0 -24
  59. package/dist/synk/daemon/control-server.js +0 -91
  60. package/dist/synk/daemon/run.d.ts +0 -14
  61. package/dist/synk/daemon/run.js +0 -338
  62. package/dist/synk/encryption.d.ts +0 -17
  63. package/dist/synk/encryption.js +0 -133
  64. package/dist/synk/index.d.ts +0 -13
  65. package/dist/synk/index.js +0 -36
  66. package/dist/synk/machine-client.d.ts +0 -42
  67. package/dist/synk/machine-client.js +0 -218
  68. package/dist/synk/persistence.d.ts +0 -51
  69. package/dist/synk/persistence.js +0 -211
  70. package/dist/synk/qr.d.ts +0 -5
  71. package/dist/synk/qr.js +0 -33
  72. package/dist/synk/session-bridge.d.ts +0 -58
  73. package/dist/synk/session-bridge.js +0 -171
  74. package/dist/synk/session-client.d.ts +0 -46
  75. package/dist/synk/session-client.js +0 -240
  76. package/dist/synk/types.d.ts +0 -574
  77. package/dist/synk/types.js +0 -74
  78. package/dist/utils/verify-remote-terminal.d.ts +0 -10
  79. package/dist/utils/verify-remote-terminal.js +0 -415
  80. package/templates/README.md +0 -378
  81. package/templates/claude-plugins/PHASE2_COMPLETION.md +0 -346
  82. package/templates/claude-plugins/PLUGIN_PROPOSALS.md +0 -1776
  83. package/templates/claude-plugins/README.md +0 -587
  84. package/templates/claude-plugins/agents/code-reviewer.json +0 -14
  85. package/templates/claude-plugins/agents/debug-detective.json +0 -15
  86. package/templates/claude-plugins/agents/git-companion.json +0 -14
  87. package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +0 -8
  88. package/templates/claude-plugins/blog-manager/commands/blog.md +0 -691
  89. package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +0 -8
  90. package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +0 -434
  91. package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +0 -8
  92. package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +0 -282
  93. package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +0 -8
  94. package/templates/claude-plugins/memory-lens/commands/memory-search.md +0 -181
  95. package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +0 -8
  96. package/templates/claude-plugins/pattern-coach/commands/forge.md +0 -365
  97. package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +0 -8
  98. package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +0 -582
  99. package/templates/commands/continue.md +0 -47
  100. package/templates/cursor-rules/ekkos-memory.md +0 -127
  101. package/templates/ekkos-manifest.json +0 -223
  102. package/templates/helpers/json-parse.cjs +0 -101
  103. package/templates/plan-template.md +0 -306
  104. package/templates/shared/hooks-enabled.json +0 -22
  105. package/templates/shared/session-words.json +0 -45
  106. package/templates/skills/ekkOS_Deep_Recall/Skill.md +0 -282
  107. package/templates/skills/ekkOS_Learn/Skill.md +0 -265
  108. package/templates/skills/ekkOS_Memory_First/Skill.md +0 -206
  109. package/templates/skills/ekkOS_Plan_Assist/Skill.md +0 -302
  110. package/templates/skills/ekkOS_Preferences/Skill.md +0 -247
  111. package/templates/skills/ekkOS_Reflect/Skill.md +0 -257
  112. package/templates/skills/ekkOS_Safety/Skill.md +0 -265
  113. package/templates/skills/ekkOS_Schema/Skill.md +0 -251
  114. package/templates/skills/ekkOS_Summary/Skill.md +0 -257
  115. package/templates/spec-template.md +0 -159
  116. package/templates/windsurf-rules/ekkos-memory.md +0 -127
  117. package/templates/windsurf-skills/README.md +0 -58
  118. package/templates/windsurf-skills/ekkos-continue/SKILL.md +0 -81
  119. package/templates/windsurf-skills/ekkos-golden-loop/SKILL.md +0 -225
  120. package/templates/windsurf-skills/ekkos-insights/SKILL.md +0 -138
  121. package/templates/windsurf-skills/ekkos-recall/SKILL.md +0 -96
  122. package/templates/windsurf-skills/ekkos-safety/SKILL.md +0 -89
  123. package/templates/windsurf-skills/ekkos-vault/SKILL.md +0 -86
@@ -0,0 +1,386 @@
1
+ "use strict";
2
+ /**
3
+ * ekkos scan
4
+ *
5
+ * Scans the current directory's repo structure, discovers systems,
6
+ * and seeds them into ekkOS system_registry via the memory API.
7
+ *
8
+ * Usage:
9
+ * ekkos scan Scan cwd and seed registry
10
+ * ekkos scan --compile Also trigger a compile pass after seeding
11
+ * ekkos scan --dry-run Show what would be seeded without calling API
12
+ * ekkos scan --path /my/repo Scan a specific directory
13
+ *
14
+ * Reuses discovery logic from apps/memory/workers/context-compiler/registry-seed.ts
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.scan = scan;
21
+ const fs_1 = require("fs");
22
+ const path_1 = require("path");
23
+ const chalk_1 = __importDefault(require("chalk"));
24
+ const ora_1 = __importDefault(require("ora"));
25
+ const platform_js_1 = require("../utils/platform.js");
26
+ // ── Excluded directories ─────────────────────────────────────────────────
27
+ const EXCLUDED_DIRS = new Set([
28
+ 'node_modules', '.next', 'dist', 'out', 'build', '.turbo', '.cache',
29
+ '.git', '.github', '.vscode', '.idea', '.claude', '.windsurf',
30
+ 'coverage', '__pycache__', '.pytest_cache', '.mypy_cache',
31
+ 'vendor', '.vercel', '.svelte-kit', 'target', 'tmp', 'temp',
32
+ 'logs', 'downloads', 'favicon', 'public', 'resources',
33
+ ]);
34
+ const EXCLUDED_PREFIXES = ['.', '_'];
35
+ // ── Domain detection ─────────────────────────────────────────────────────
36
+ const DOMAIN_MAP = {
37
+ apps: 'platform',
38
+ packages: 'packages',
39
+ workers: 'workers',
40
+ extensions: 'extensions',
41
+ scripts: 'infra',
42
+ services: 'services',
43
+ lib: 'memory',
44
+ supabase: 'infra',
45
+ 'mcp-servers': 'tools',
46
+ templates: 'tools',
47
+ docs: 'content',
48
+ content: 'content',
49
+ monitoring: 'infra',
50
+ tests: 'infra',
51
+ e2e: 'infra',
52
+ src: 'core',
53
+ api: 'api',
54
+ components: 'frontend',
55
+ pages: 'frontend',
56
+ };
57
+ function detectDomain(dirPath) {
58
+ const topLevel = dirPath.split('/')[0];
59
+ return DOMAIN_MAP[topLevel] || 'other';
60
+ }
61
+ // ── System ID generation ─────────────────────────────────────────────────
62
+ function toSystemId(dirPath) {
63
+ return dirPath
64
+ .toLowerCase()
65
+ .replace(/\//g, '-')
66
+ .replace(/[^a-z0-9-]/g, '')
67
+ .replace(/-+/g, '-')
68
+ .replace(/^-|-$/g, '')
69
+ .slice(0, 64);
70
+ }
71
+ function toHumanName(dirPath) {
72
+ const parts = dirPath.split('/');
73
+ return parts[parts.length - 1]
74
+ .replace(/-/g, ' ')
75
+ .replace(/\b\w/g, c => c.toUpperCase());
76
+ }
77
+ // ── Directory scanner ────────────────────────────────────────────────────
78
+ function isIncludable(fullPath) {
79
+ const name = (0, path_1.basename)(fullPath);
80
+ if (EXCLUDED_DIRS.has(name))
81
+ return false;
82
+ if (EXCLUDED_PREFIXES.some(p => name.startsWith(p)))
83
+ return false;
84
+ // Must contain at least 1 source file or meaningful config
85
+ try {
86
+ const entries = (0, fs_1.readdirSync)(fullPath);
87
+ const sourceFiles = entries.filter(e => {
88
+ const ext = e.split('.').pop()?.toLowerCase();
89
+ return ['ts', 'tsx', 'js', 'jsx', 'mjs', 'py', 'rs', 'go', 'sql', 'mts'].includes(ext || '');
90
+ });
91
+ const configFiles = entries.filter(e => ['package.json', 'tsconfig.json', 'Cargo.toml', 'pyproject.toml', 'wrangler.toml', 'go.mod'].includes(e));
92
+ return sourceFiles.length >= 1 || configFiles.length >= 1;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ // Container directories that always scan children
99
+ const CONTAINER_DIRS = new Set([
100
+ 'apps', 'packages', 'workers', 'extensions', 'services', 'mcp-servers',
101
+ ]);
102
+ function scanDirectory(repoRoot, currentPath, depth, maxDepth, parentId) {
103
+ if (depth > maxDepth)
104
+ return [];
105
+ const results = [];
106
+ const relPath = (0, path_1.relative)(repoRoot, currentPath) || '.';
107
+ const dirName = (0, path_1.basename)(currentPath);
108
+ // Skip root — just descend into children
109
+ if (relPath === '.') {
110
+ let entries;
111
+ try {
112
+ entries = (0, fs_1.readdirSync)(currentPath);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ for (const entry of entries) {
118
+ const full = (0, path_1.join)(currentPath, entry);
119
+ try {
120
+ if ((0, fs_1.statSync)(full).isDirectory() && !EXCLUDED_DIRS.has(entry) && !entry.startsWith('.')) {
121
+ results.push(...scanDirectory(repoRoot, full, depth + 1, maxDepth, null));
122
+ }
123
+ }
124
+ catch { /* permission denied, etc. */ }
125
+ }
126
+ return results;
127
+ }
128
+ // Container directories (apps/, packages/, etc.) — always scan children, never register themselves
129
+ const isContainer = CONTAINER_DIRS.has(relPath) || CONTAINER_DIRS.has(dirName);
130
+ if (isContainer && depth <= 1) {
131
+ try {
132
+ const entries = (0, fs_1.readdirSync)(currentPath);
133
+ for (const e of entries) {
134
+ const full = (0, path_1.join)(currentPath, e);
135
+ try {
136
+ if ((0, fs_1.statSync)(full).isDirectory() && !EXCLUDED_DIRS.has(e) && !e.startsWith('.')) {
137
+ results.push(...scanDirectory(repoRoot, full, depth + 1, maxDepth, null));
138
+ }
139
+ }
140
+ catch { /* ignore */ }
141
+ }
142
+ }
143
+ catch { /* ignore */ }
144
+ return results;
145
+ }
146
+ if (!isIncludable(currentPath))
147
+ return [];
148
+ const systemId = toSystemId(relPath);
149
+ const domain = detectDomain(relPath);
150
+ // Detect description from package.json if available
151
+ let description = `System at ${relPath}`;
152
+ const pkgPath = (0, path_1.join)(currentPath, 'package.json');
153
+ if ((0, fs_1.existsSync)(pkgPath)) {
154
+ try {
155
+ const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
156
+ if (pkg.description)
157
+ description = pkg.description;
158
+ }
159
+ catch { /* ignore */ }
160
+ }
161
+ const entry = {
162
+ system_id: systemId,
163
+ name: toHumanName(relPath),
164
+ description,
165
+ directory_path: relPath,
166
+ domain,
167
+ status: 'active',
168
+ parent_system_id: parentId,
169
+ metadata: {},
170
+ tags: [domain],
171
+ aliases: [],
172
+ };
173
+ results.push(entry);
174
+ // Scan subdirectories for nested systems
175
+ const topLevel = relPath.split('/')[0];
176
+ const isNestedContainer = CONTAINER_DIRS.has(topLevel);
177
+ if (isNestedContainer && depth <= 3) {
178
+ try {
179
+ const entries = (0, fs_1.readdirSync)(currentPath);
180
+ for (const e of entries) {
181
+ const full = (0, path_1.join)(currentPath, e);
182
+ try {
183
+ if ((0, fs_1.statSync)(full).isDirectory() && !EXCLUDED_DIRS.has(e) && !e.startsWith('.')) {
184
+ results.push(...scanDirectory(repoRoot, full, depth + 1, maxDepth, systemId));
185
+ }
186
+ }
187
+ catch { /* ignore */ }
188
+ }
189
+ }
190
+ catch { /* ignore */ }
191
+ }
192
+ return results;
193
+ }
194
+ // ── Boundary Precedence ──────────────────────────────────────────────────
195
+ function assignParentSystems(systems) {
196
+ const sorted = [...systems].sort((a, b) => {
197
+ const depthA = a.directory_path.split('/').length;
198
+ const depthB = b.directory_path.split('/').length;
199
+ if (depthA !== depthB)
200
+ return depthA - depthB;
201
+ return a.system_id.localeCompare(b.system_id);
202
+ });
203
+ for (let i = 0; i < sorted.length; i++) {
204
+ const system = sorted[i];
205
+ if (system.parent_system_id)
206
+ continue;
207
+ let nearestParent = null;
208
+ let nearestDepth = -1;
209
+ for (let j = 0; j < sorted.length; j++) {
210
+ if (i === j)
211
+ continue;
212
+ const candidate = sorted[j];
213
+ const candidateDepth = candidate.directory_path.split('/').length;
214
+ if (system.directory_path.startsWith(candidate.directory_path + '/') &&
215
+ candidateDepth > nearestDepth) {
216
+ nearestParent = candidate;
217
+ nearestDepth = candidateDepth;
218
+ }
219
+ }
220
+ if (nearestParent) {
221
+ system.parent_system_id = nearestParent.system_id;
222
+ }
223
+ }
224
+ return sorted;
225
+ }
226
+ // ── Load API key from config ─────────────────────────────────────────────
227
+ function loadApiKey() {
228
+ try {
229
+ if ((0, fs_1.existsSync)(platform_js_1.EKKOS_CONFIG)) {
230
+ const config = JSON.parse((0, fs_1.readFileSync)(platform_js_1.EKKOS_CONFIG, 'utf-8'));
231
+ return config.apiKey || null;
232
+ }
233
+ }
234
+ catch { /* ignore */ }
235
+ // Fall back to env var
236
+ return process.env.EKKOS_API_KEY || null;
237
+ }
238
+ // ── Detect git repo root ─────────────────────────────────────────────────
239
+ function findGitRoot(startPath) {
240
+ let current = (0, path_1.resolve)(startPath);
241
+ while (current !== '/') {
242
+ if ((0, fs_1.existsSync)((0, path_1.join)(current, '.git'))) {
243
+ return current;
244
+ }
245
+ const parent = (0, path_1.resolve)(current, '..');
246
+ if (parent === current)
247
+ break;
248
+ current = parent;
249
+ }
250
+ return null;
251
+ }
252
+ // ── Main scan command ────────────────────────────────────────────────────
253
+ async function scan(options) {
254
+ const startTime = Date.now();
255
+ const targetPath = (0, path_1.resolve)(options.path || process.cwd());
256
+ const isDryRun = options.dryRun ?? false;
257
+ const shouldCompile = options.compile ?? false;
258
+ console.log('');
259
+ console.log(chalk_1.default.cyan.bold(' ekkOS Scan'));
260
+ console.log(chalk_1.default.gray(' ─'.repeat(25)));
261
+ console.log('');
262
+ // Check if in a git repo
263
+ const gitRoot = findGitRoot(targetPath);
264
+ const repoRoot = gitRoot || targetPath;
265
+ if (gitRoot) {
266
+ console.log(chalk_1.default.gray(` Git root: ${gitRoot}`));
267
+ }
268
+ else {
269
+ console.log(chalk_1.default.yellow(` No git repo found — scanning ${targetPath}`));
270
+ }
271
+ console.log('');
272
+ // Phase 1: Scan repo structure
273
+ const scanSpinner = (0, ora_1.default)('Scanning repo structure...').start();
274
+ let systems;
275
+ try {
276
+ const rawSystems = scanDirectory(repoRoot, repoRoot, 0, 4, null);
277
+ systems = assignParentSystems(rawSystems);
278
+ scanSpinner.succeed(`Found ${chalk_1.default.bold(systems.length.toString())} systems`);
279
+ }
280
+ catch (err) {
281
+ scanSpinner.fail('Scan failed');
282
+ console.error(chalk_1.default.red(` ${err instanceof Error ? err.message : err}`));
283
+ process.exit(1);
284
+ }
285
+ // Show discovered systems
286
+ if (systems.length > 0) {
287
+ console.log('');
288
+ // Group by domain
289
+ const byDomain = new Map();
290
+ for (const s of systems) {
291
+ const group = byDomain.get(s.domain) || [];
292
+ group.push(s);
293
+ byDomain.set(s.domain, group);
294
+ }
295
+ for (const [domain, entries] of byDomain) {
296
+ console.log(chalk_1.default.cyan(` ${domain}`));
297
+ for (const s of entries) {
298
+ const indent = s.parent_system_id ? ' ' : ' ';
299
+ const arrow = s.parent_system_id ? '└─' : '──';
300
+ console.log(chalk_1.default.gray(`${indent}${arrow} `) + chalk_1.default.white(s.system_id) + chalk_1.default.gray(` → ${s.directory_path}`));
301
+ }
302
+ }
303
+ console.log('');
304
+ }
305
+ // Dry run — stop here
306
+ if (isDryRun) {
307
+ console.log(chalk_1.default.yellow(' Dry run — no changes made'));
308
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
309
+ console.log(chalk_1.default.gray(` Done in ${duration}s`));
310
+ console.log('');
311
+ return;
312
+ }
313
+ // Phase 2: Load API key
314
+ const apiKey = loadApiKey();
315
+ if (!apiKey) {
316
+ console.log(chalk_1.default.red(' No API key found.'));
317
+ console.log(chalk_1.default.gray(' Run `ekkos init` first, or set EKKOS_API_KEY env var.'));
318
+ console.log('');
319
+ process.exit(1);
320
+ }
321
+ // Phase 3: Seed registry via API
322
+ const seedSpinner = (0, ora_1.default)('Seeding registry...').start();
323
+ try {
324
+ const apiUrl = process.env.EKKOS_API_URL || platform_js_1.MCP_API_URL;
325
+ const response = await fetch(`${apiUrl}/api/v1/living-docs/seed`, {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Authorization': `Bearer ${apiKey}`,
329
+ 'Content-Type': 'application/json',
330
+ },
331
+ body: JSON.stringify({
332
+ systems,
333
+ compile: shouldCompile,
334
+ }),
335
+ });
336
+ if (!response.ok) {
337
+ const errBody = await response.text();
338
+ seedSpinner.fail('Seed failed');
339
+ console.error(chalk_1.default.red(` API returned ${response.status}: ${errBody}`));
340
+ process.exit(1);
341
+ }
342
+ const result = await response.json();
343
+ if (!result.ok) {
344
+ seedSpinner.fail('Seed failed');
345
+ console.error(chalk_1.default.red(` ${result.error || 'Unknown error'}`));
346
+ process.exit(1);
347
+ }
348
+ seedSpinner.succeed(`Seeded ${chalk_1.default.bold(result.total.toString())} systems` +
349
+ (result.inserted > 0 ? chalk_1.default.green(` (${result.inserted} new)`) : '') +
350
+ (result.updated > 0 ? chalk_1.default.gray(` (${result.updated} updated)`) : ''));
351
+ // Show any errors
352
+ if (result.errors && result.errors.length > 0) {
353
+ console.log(chalk_1.default.yellow(` ${result.errors.length} errors:`));
354
+ for (const e of result.errors.slice(0, 5)) {
355
+ console.log(chalk_1.default.gray(` - ${e}`));
356
+ }
357
+ if (result.errors.length > 5) {
358
+ console.log(chalk_1.default.gray(` ... and ${result.errors.length - 5} more`));
359
+ }
360
+ }
361
+ // Compile pass result
362
+ if (result.compile) {
363
+ if (result.compile.triggered) {
364
+ console.log(chalk_1.default.green(' Compile pass triggered'));
365
+ }
366
+ else {
367
+ console.log(chalk_1.default.yellow(` Compile skipped: ${result.compile.reason || result.compile.error || 'unknown'}`));
368
+ }
369
+ }
370
+ }
371
+ catch (err) {
372
+ seedSpinner.fail('Seed failed');
373
+ if (err instanceof TypeError && err.cause?.code === 'ECONNREFUSED') {
374
+ console.error(chalk_1.default.red(' Could not connect to ekkOS API. Is it running?'));
375
+ }
376
+ else {
377
+ console.error(chalk_1.default.red(` ${err instanceof Error ? err.message : err}`));
378
+ }
379
+ process.exit(1);
380
+ }
381
+ // Summary
382
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
383
+ console.log('');
384
+ console.log(chalk_1.default.green(` Done in ${duration}s`));
385
+ console.log('');
386
+ }
@@ -63,11 +63,12 @@ const commander_1 = require("commander");
63
63
  const state_js_1 = require("../utils/state.js");
64
64
  // ── Pricing (per MTok) ──
65
65
  const MODEL_PRICING = {
66
- 'claude-opus-4-6': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
67
- 'claude-opus-4-5-20250620': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
66
+ 'claude-opus-4-6': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
67
+ 'claude-opus-4-5-20250620': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
68
+ 'claude-sonnet-4-6': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
68
69
  'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
69
70
  'claude-sonnet-4-5-20250514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
70
- 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
71
+ 'claude-haiku-4-5-20251001': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
71
72
  };
72
73
  function getModelPricing(modelId) {
73
74
  if (MODEL_PRICING[modelId])
@@ -332,11 +333,20 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
332
333
  const USAGE_H = 4;
333
334
  const FOOTER_H = 3;
334
335
  const FIXED_H = HEADER_H + WORKERS_H + USAGE_H + FOOTER_H;
336
+ function resolveChartRatio(height) {
337
+ if (height >= 62)
338
+ return 0.22;
339
+ if (height >= 48)
340
+ return 0.25;
341
+ if (height >= 36)
342
+ return 0.28;
343
+ return 0.32;
344
+ }
335
345
  function calcLayout() {
336
- const H = Math.max(30, screen.height);
346
+ const H = Math.max(30, screen.height || 30);
337
347
  const remaining = Math.max(10, H - FIXED_H);
338
- const chartH = Math.max(CHART_H_MIN, Math.floor(remaining * 0.30));
339
- const tableH = Math.max(4, remaining - chartH);
348
+ const chartH = Math.max(CHART_H_MIN, Math.floor(remaining * resolveChartRatio(H)));
349
+ const tableH = Math.max(5, remaining - chartH);
340
350
  return {
341
351
  header: { top: 0, height: HEADER_H },
342
352
  workers: { top: HEADER_H, height: WORKERS_H },
@@ -368,15 +378,20 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
368
378
  border: { type: 'line' },
369
379
  label: ' Workers ',
370
380
  });
371
- const tokenChart = contrib.line({
372
- top: layout.chart.top, left: 0, width: W, height: layout.chart.height,
373
- label: ' Tokens/Turn (K) ',
374
- showLegend: true,
375
- legend: { width: 12 },
376
- style: { line: 'green', text: 'white', baseline: 'white', border: { fg: 'yellow' } },
377
- border: { type: 'line', fg: 'yellow' },
378
- xLabelPadding: 0, xPadding: 1, wholeNumbersOnly: false,
379
- });
381
+ function createTokenChart(top, left, width, height) {
382
+ return contrib.line({
383
+ top, left, width, height,
384
+ label: ' Tokens/Turn (K) ',
385
+ showLegend: true,
386
+ legend: { width: 12 },
387
+ style: { line: 'green', text: 'white', baseline: 'white', border: { fg: 'yellow' } },
388
+ border: { type: 'line', fg: 'yellow' },
389
+ xLabelPadding: 0, xPadding: 1, wholeNumbersOnly: false,
390
+ });
391
+ }
392
+ let tokenChart = createTokenChart(layout.chart.top, 0, W, layout.chart.height);
393
+ let chartLayoutW = 0;
394
+ let chartLayoutH = 0;
380
395
  const turnBox = blessed.box({
381
396
  top: layout.table.top, left: 0, width: W, height: layout.table.height,
382
397
  content: '',
@@ -413,22 +428,77 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
413
428
  screen.append(footerBox);
414
429
  function applyLayout() {
415
430
  layout = calcLayout();
431
+ const fullWidth = Math.max(24, screen.width || 80);
432
+ const H_PAD = 1;
433
+ const contentWidth = Math.max(20, fullWidth - (H_PAD * 2));
416
434
  headerBox.top = layout.header.top;
435
+ headerBox.left = H_PAD;
436
+ headerBox.width = contentWidth;
417
437
  headerBox.height = layout.header.height;
418
438
  workersBox.top = layout.workers.top;
439
+ workersBox.left = H_PAD;
440
+ workersBox.width = contentWidth;
419
441
  workersBox.height = layout.workers.height;
420
- tokenChart.top = layout.chart.top;
421
- tokenChart.height = layout.chart.height;
442
+ if (chartLayoutW !== contentWidth || chartLayoutH !== layout.chart.height) {
443
+ try {
444
+ screen.remove(tokenChart);
445
+ }
446
+ catch { }
447
+ try {
448
+ tokenChart.destroy?.();
449
+ }
450
+ catch { }
451
+ tokenChart = createTokenChart(layout.chart.top, H_PAD, contentWidth, layout.chart.height);
452
+ chartLayoutW = contentWidth;
453
+ chartLayoutH = layout.chart.height;
454
+ screen.append(tokenChart);
455
+ }
456
+ else {
457
+ tokenChart.top = layout.chart.top;
458
+ tokenChart.left = H_PAD;
459
+ tokenChart.width = contentWidth;
460
+ tokenChart.height = layout.chart.height;
461
+ }
422
462
  turnBox.top = layout.table.top;
463
+ turnBox.left = H_PAD;
464
+ turnBox.width = contentWidth;
423
465
  turnBox.height = layout.table.height;
424
466
  windowBox.top = layout.usage.top;
467
+ windowBox.left = H_PAD;
468
+ windowBox.width = contentWidth;
425
469
  windowBox.height = layout.usage.height;
426
470
  footerBox.top = layout.footer.top;
471
+ footerBox.left = H_PAD;
472
+ footerBox.width = contentWidth;
427
473
  footerBox.height = layout.footer.height;
474
+ if (lastChartSeries) {
475
+ try {
476
+ tokenChart.setData(lastChartSeries);
477
+ }
478
+ catch { }
479
+ }
428
480
  }
429
481
  // ── State ──
430
482
  let lastWorkers = [];
483
+ let lastChartSeries = null;
431
484
  let sparkleTimer;
485
+ let lastLayoutW = screen.width || 0;
486
+ let lastLayoutH = screen.height || 0;
487
+ function ensureLayoutSynced() {
488
+ const w = screen.width || 0;
489
+ const h = screen.height || 0;
490
+ if (w === lastLayoutW && h === lastLayoutH)
491
+ return;
492
+ lastLayoutW = w;
493
+ lastLayoutH = h;
494
+ try {
495
+ screen.realloc?.();
496
+ }
497
+ catch { }
498
+ applyLayout();
499
+ }
500
+ // Apply once so initial render uses the final, padded geometry.
501
+ applyLayout();
432
502
  // ── Logo animation ──
433
503
  function renderLogoWave() {
434
504
  try {
@@ -446,11 +516,11 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
446
516
  const rawLogoLen = 14; // " ekkOS_ Swarm " roughly
447
517
  const infoStr = ` ${lastWorkers.length}W ${totalTurns}t $${totalCost.toFixed(2)} ${durStr} `;
448
518
  const rawInfoLen = infoStr.length + 1;
449
- const boxW = screen.width - 2;
519
+ const boxW = Math.max(10, headerBox.width - 2);
450
520
  const pad = Math.max(1, boxW - rawLogoLen - rawInfoLen);
451
521
  headerBox.setLabel(logoStr + '\u2500'.repeat(pad) + infoStr);
452
522
  // Header content: task description
453
- const maxW = screen.width - 6;
523
+ const maxW = Math.max(8, headerBox.width - 6);
454
524
  const truncTask = taskStr.length > maxW ? taskStr.slice(0, maxW - 3) + '...' : taskStr;
455
525
  headerBox.setContent(` {gray-fg}Task:{/gray-fg} ${truncTask}`);
456
526
  waveOffset = (waveOffset + 1) % WAVE_COLORS.length;
@@ -471,7 +541,7 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
471
541
  workersBox.setContent(' {gray-fg}No workers detected yet...{/gray-fg}');
472
542
  return;
473
543
  }
474
- const boxW = Math.max(20, screen.width - 4);
544
+ const boxW = Math.max(20, workersBox.width - 2);
475
545
  const WORKER_COLORS = ['magenta', 'blue', 'green', 'yellow', 'cyan', 'red', 'white', 'gray'];
476
546
  const lines = [];
477
547
  // Row 1: Worker names + context bars
@@ -510,8 +580,9 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
510
580
  // ── Combined turn table ──
511
581
  function renderTurnTable(workers) {
512
582
  const WORKER_COLORS = ['magenta', 'blue', 'green', 'yellow', 'cyan', 'red', 'white', 'gray'];
513
- const w = Math.max(20, screen.width - 4);
583
+ const w = Math.max(18, turnBox.width - 4);
514
584
  const div = '{gray-fg}|{/gray-fg}';
585
+ const lastScrollPerc = turnBox.getScrollPerc();
515
586
  // Columns: W(2) Turn(4) Model(7) Context(7) CacheRd(flex) CacheWr(flex) Out(flex) Cost(7)
516
587
  const colW = 2;
517
588
  const colNum = 4;
@@ -540,8 +611,7 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
540
611
  }
541
612
  // Sort by timestamp descending (newest first)
542
613
  allTurns.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
543
- const visibleRows = Math.max(1, layout.table.height - 4);
544
- const rows = allTurns.slice(0, visibleRows).map(t => {
614
+ const rows = allTurns.map(t => {
545
615
  const wColor = WORKER_COLORS[(t.workerIndex - 1) % WORKER_COLORS.length];
546
616
  const mTag = modelTag(t.routedModel);
547
617
  const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
@@ -557,6 +627,9 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
557
627
  costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
558
628
  });
559
629
  turnBox.setContent([header, separator, ...rows].join('\n'));
630
+ if (lastScrollPerc > 0) {
631
+ turnBox.setScrollPerc(lastScrollPerc);
632
+ }
560
633
  }
561
634
  // ── Combined chart ──
562
635
  function renderChart(workers) {
@@ -576,16 +649,68 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
576
649
  });
577
650
  }
578
651
  if (series.length > 0) {
652
+ lastChartSeries = series;
579
653
  tokenChart.setData(series);
580
654
  }
581
655
  }
582
656
  // ── Usage window (Anthropic OAuth) ──
657
+ function extractClaudeOauthAccessToken(rawBlob) {
658
+ const raw = (rawBlob || '').trim();
659
+ if (!raw)
660
+ return null;
661
+ const candidates = new Set([raw]);
662
+ if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
663
+ const decoded = Buffer.from(raw, 'hex').toString('utf8');
664
+ candidates.add(decoded);
665
+ candidates.add(decoded.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim());
666
+ }
667
+ for (const candidate of candidates) {
668
+ if (!candidate)
669
+ continue;
670
+ try {
671
+ const parsed = JSON.parse(candidate);
672
+ const token = parsed?.claudeAiOauth?.accessToken;
673
+ if (typeof token === 'string' && token.length > 0)
674
+ return token;
675
+ }
676
+ catch { }
677
+ try {
678
+ const parsed = JSON.parse(`{${candidate}}`);
679
+ const token = parsed?.claudeAiOauth?.accessToken;
680
+ if (typeof token === 'string' && token.length > 0)
681
+ return token;
682
+ }
683
+ catch { }
684
+ const oauthMatch = candidate.match(/"claudeAiOauth"\s*:\s*(\{[\s\S]*?\})/);
685
+ if (oauthMatch) {
686
+ try {
687
+ const oauth = JSON.parse(oauthMatch[1]);
688
+ const token = oauth?.accessToken;
689
+ if (typeof token === 'string' && token.length > 0)
690
+ return token;
691
+ }
692
+ catch { }
693
+ const tokenMatch = oauthMatch[1].match(/"accessToken"\s*:\s*"([^"]+)"/);
694
+ if (tokenMatch?.[1])
695
+ return tokenMatch[1];
696
+ }
697
+ }
698
+ return null;
699
+ }
583
700
  async function fetchAnthropicUsage() {
584
701
  try {
585
- const { execSync } = require('child_process');
586
- const credsJson = execSync('security find-generic-password -s "Claude Code-credentials" -w', { encoding: 'utf-8', timeout: 5000 }).trim();
587
- const creds = JSON.parse(credsJson);
588
- const token = creds?.claudeAiOauth?.accessToken;
702
+ let token = null;
703
+ if (process.platform === 'darwin') {
704
+ const { execSync } = require('child_process');
705
+ const credsBlob = execSync('security find-generic-password -s "Claude Code-credentials" -w', { encoding: 'utf-8', timeout: 5000 }).trim();
706
+ token = extractClaudeOauthAccessToken(credsBlob);
707
+ }
708
+ if (!token) {
709
+ const credsPath = path.join(os.homedir(), '.claude', '.credentials.json');
710
+ if (fs.existsSync(credsPath)) {
711
+ token = extractClaudeOauthAccessToken(fs.readFileSync(credsPath, 'utf-8'));
712
+ }
713
+ }
589
714
  if (!token)
590
715
  return null;
591
716
  const resp = await fetch('https://api.anthropic.com/api/oauth/usage', {
@@ -664,6 +789,7 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
664
789
  // ── Main update loop ──
665
790
  function updateDashboard() {
666
791
  try {
792
+ ensureLayoutSynced();
667
793
  const workers = discoverWorkers(launchTs);
668
794
  lastWorkers = workers;
669
795
  renderWorkerCards(workers);
@@ -691,7 +817,9 @@ async function launchSwarmDashboard(launchTs, refreshMs) {
691
817
  screen.on('resize', () => {
692
818
  try {
693
819
  screen.realloc?.();
694
- applyLayout();
820
+ lastLayoutW = 0;
821
+ lastLayoutH = 0;
822
+ ensureLayoutSynced();
695
823
  updateDashboard();
696
824
  }
697
825
  catch { }