@ekkos/cli 0.2.18 → 1.0.0

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 (98) 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/eviction-client.d.ts +139 -0
  9. package/dist/capture/eviction-client.js +454 -0
  10. package/dist/capture/index.d.ts +2 -0
  11. package/dist/capture/index.js +2 -0
  12. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  13. package/dist/capture/jsonl-rewriter.js +1369 -0
  14. package/dist/capture/transcript-repair.d.ts +51 -0
  15. package/dist/capture/transcript-repair.js +319 -0
  16. package/dist/commands/agent.d.ts +6 -0
  17. package/dist/commands/agent.js +244 -0
  18. package/dist/commands/dashboard.d.ts +25 -0
  19. package/dist/commands/dashboard.js +1175 -0
  20. package/dist/commands/doctor.js +23 -1
  21. package/dist/commands/run.d.ts +5 -0
  22. package/dist/commands/run.js +1605 -516
  23. package/dist/commands/setup-remote.js +146 -37
  24. package/dist/commands/swarm-dashboard.d.ts +20 -0
  25. package/dist/commands/swarm-dashboard.js +735 -0
  26. package/dist/commands/swarm-setup.d.ts +10 -0
  27. package/dist/commands/swarm-setup.js +956 -0
  28. package/dist/commands/swarm.d.ts +46 -0
  29. package/dist/commands/swarm.js +441 -0
  30. package/dist/commands/test-claude.d.ts +16 -0
  31. package/dist/commands/test-claude.js +156 -0
  32. package/dist/commands/usage/blocks.d.ts +8 -0
  33. package/dist/commands/usage/blocks.js +60 -0
  34. package/dist/commands/usage/daily.d.ts +9 -0
  35. package/dist/commands/usage/daily.js +96 -0
  36. package/dist/commands/usage/dashboard.d.ts +8 -0
  37. package/dist/commands/usage/dashboard.js +104 -0
  38. package/dist/commands/usage/formatters.d.ts +41 -0
  39. package/dist/commands/usage/formatters.js +147 -0
  40. package/dist/commands/usage/index.d.ts +13 -0
  41. package/dist/commands/usage/index.js +87 -0
  42. package/dist/commands/usage/monthly.d.ts +8 -0
  43. package/dist/commands/usage/monthly.js +66 -0
  44. package/dist/commands/usage/session.d.ts +11 -0
  45. package/dist/commands/usage/session.js +193 -0
  46. package/dist/commands/usage/weekly.d.ts +9 -0
  47. package/dist/commands/usage/weekly.js +61 -0
  48. package/dist/commands/usage.d.ts +7 -0
  49. package/dist/commands/usage.js +214 -0
  50. package/dist/cron/index.d.ts +7 -0
  51. package/dist/cron/index.js +13 -0
  52. package/dist/cron/promoter.d.ts +70 -0
  53. package/dist/cron/promoter.js +403 -0
  54. package/dist/deploy/instructions.d.ts +5 -2
  55. package/dist/deploy/instructions.js +11 -8
  56. package/dist/index.js +262 -5
  57. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  58. package/dist/lib/tmux-scrollbar.js +296 -0
  59. package/dist/lib/usage-monitor.d.ts +47 -0
  60. package/dist/lib/usage-monitor.js +124 -0
  61. package/dist/lib/usage-parser.d.ts +162 -0
  62. package/dist/lib/usage-parser.js +583 -0
  63. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  64. package/dist/restore/RestoreOrchestrator.js +118 -30
  65. package/dist/utils/log-rotate.d.ts +18 -0
  66. package/dist/utils/log-rotate.js +74 -0
  67. package/dist/utils/platform.d.ts +2 -0
  68. package/dist/utils/platform.js +3 -1
  69. package/dist/utils/session-binding.d.ts +5 -0
  70. package/dist/utils/session-binding.js +46 -0
  71. package/dist/utils/state.js +4 -0
  72. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  73. package/dist/utils/verify-remote-terminal.js +415 -0
  74. package/package.json +9 -2
  75. package/templates/CLAUDE.md +135 -23
  76. package/templates/ekkos-manifest.json +5 -5
  77. package/templates/hooks/lib/contract.sh +43 -31
  78. package/templates/hooks/lib/count-tokens.cjs +86 -0
  79. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  80. package/templates/hooks/lib/state.sh +53 -1
  81. package/templates/hooks/stop.sh +150 -388
  82. package/templates/hooks/user-prompt-submit.sh +353 -443
  83. package/templates/windsurf-hooks/README.md +212 -0
  84. package/templates/windsurf-hooks/hooks.json +9 -2
  85. package/templates/windsurf-hooks/install.sh +148 -0
  86. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  87. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  88. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  89. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  90. package/templates/agents/README.md +0 -182
  91. package/templates/agents/code-reviewer.md +0 -166
  92. package/templates/agents/debug-detective.md +0 -169
  93. package/templates/agents/ekkOS_Vercel.md +0 -99
  94. package/templates/agents/extension-manager.md +0 -229
  95. package/templates/agents/git-companion.md +0 -185
  96. package/templates/agents/github-test-agent.md +0 -321
  97. package/templates/agents/railway-manager.md +0 -215
  98. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
package/dist/index.js CHANGED
@@ -42,12 +42,18 @@ const init_1 = require("./commands/init");
42
42
  const test_1 = require("./commands/test");
43
43
  const status_1 = require("./commands/status");
44
44
  const run_1 = require("./commands/run");
45
+ const test_claude_1 = require("./commands/test-claude");
45
46
  const doctor_1 = require("./commands/doctor");
46
47
  const stream_1 = require("./commands/stream");
47
48
  const hooks_1 = require("./commands/hooks");
48
49
  const setup_remote_1 = require("./commands/setup-remote");
49
50
  const agent_1 = require("./commands/agent");
50
51
  const state_1 = require("./utils/state");
52
+ const index_1 = require("./commands/usage/index");
53
+ const dashboard_1 = require("./commands/dashboard");
54
+ const swarm_1 = require("./commands/swarm");
55
+ const swarm_dashboard_1 = require("./commands/swarm-dashboard");
56
+ const swarm_setup_1 = require("./commands/swarm-setup");
51
57
  const chalk_1 = __importDefault(require("chalk"));
52
58
  const fs = __importStar(require("fs"));
53
59
  const path = __importStar(require("path"));
@@ -58,7 +64,158 @@ commander_1.program
58
64
  .name('ekkos')
59
65
  .description('ekkOS memory CLI for AI coding assistants')
60
66
  .version(pkg.version)
61
- .addHelpText('beforeAll', chalk_1.default.cyan('\n made by ekkOS_ with ❤️\n'));
67
+ .addHelpText('before', [
68
+ '',
69
+ chalk_1.default.cyan.bold(' ekkOS_') + chalk_1.default.gray(` v${pkg.version}`) + chalk_1.default.cyan(' — Memory for AI coding assistants'),
70
+ chalk_1.default.gray(' https://docs.ekkos.dev'),
71
+ '',
72
+ ].join('\n'))
73
+ .addHelpText('after', [
74
+ '',
75
+ chalk_1.default.cyan.bold('Examples:'),
76
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos')} ${chalk_1.default.gray('Start Claude Code with ekkOS memory (default: run)')}`,
77
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos init')} ${chalk_1.default.gray('First-time setup — authenticate + configure your IDE')}`,
78
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run --dashboard')} ${chalk_1.default.gray('Launch with live usage dashboard (tmux split)')}`,
79
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
80
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
81
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
82
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos swarm launch -t "build X"')} ${chalk_1.default.gray('Launch parallel workers on a task')}`,
83
+ '',
84
+ chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
85
+ '',
86
+ ].join('\n'))
87
+ .configureHelp({
88
+ formatHelp: (cmd, helper) => {
89
+ // Only customize root help — subcommands use default formatting
90
+ if (cmd.parent) {
91
+ // Fall back to default formatting for subcommands
92
+ const termWidth = helper.padWidth(cmd, helper);
93
+ const helpWidth = helper.helpWidth || 80;
94
+ const itemIndentWidth = 2;
95
+ const itemSeparatorWidth = 2;
96
+ let output = '';
97
+ // Description
98
+ const desc = helper.commandDescription(cmd);
99
+ if (desc) {
100
+ output += desc + '\n\n';
101
+ }
102
+ // Usage
103
+ output += chalk_1.default.cyan.bold('Usage:') + ' ' + helper.commandUsage(cmd) + '\n';
104
+ // Arguments
105
+ const args = helper.visibleArguments(cmd);
106
+ if (args.length > 0) {
107
+ output += '\n' + chalk_1.default.cyan.bold('Arguments:') + '\n';
108
+ for (const arg of args) {
109
+ const term = helper.argumentTerm(arg);
110
+ const desc = helper.argumentDescription(arg);
111
+ output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
112
+ }
113
+ }
114
+ // Options
115
+ const opts = helper.visibleOptions(cmd);
116
+ if (opts.length > 0) {
117
+ output += '\n' + chalk_1.default.cyan.bold('Options:') + '\n';
118
+ for (const opt of opts) {
119
+ const term = helper.optionTerm(opt);
120
+ const desc = helper.optionDescription(opt);
121
+ output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
122
+ }
123
+ }
124
+ // Subcommands
125
+ const cmds = helper.visibleCommands(cmd);
126
+ if (cmds.length > 0) {
127
+ output += '\n' + chalk_1.default.cyan.bold('Commands:') + '\n';
128
+ for (const sub of cmds) {
129
+ const term = helper.subcommandTerm(sub);
130
+ const desc = helper.subcommandDescription(sub);
131
+ output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
132
+ }
133
+ }
134
+ return output;
135
+ }
136
+ // ── Root help: grouped command listing ──
137
+ const commands = helper.visibleCommands(cmd);
138
+ const cmdMap = new Map();
139
+ for (const sub of commands) {
140
+ cmdMap.set(sub.name(), {
141
+ term: helper.subcommandTerm(sub),
142
+ desc: helper.subcommandDescription(sub),
143
+ });
144
+ }
145
+ const groups = [
146
+ {
147
+ title: 'Getting Started',
148
+ icon: '▸',
149
+ commands: [
150
+ { name: 'init', desc: 'Authenticate and configure your IDE (Claude, Cursor, Windsurf)' },
151
+ { name: 'status', desc: 'Show memory status and installation info' },
152
+ { name: 'test', desc: 'Test connection to ekkOS memory API' },
153
+ { name: 'doctor', desc: 'Check system prerequisites (Node, PTY, Claude, MCP)' },
154
+ ],
155
+ },
156
+ {
157
+ title: 'Running',
158
+ icon: '▸',
159
+ commands: [
160
+ { name: 'run', desc: 'Launch Claude Code with ekkOS memory + auto-continue', note: 'default' },
161
+ { name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
162
+ { name: 'sessions', desc: 'List active Claude Code sessions' },
163
+ ],
164
+ },
165
+ {
166
+ title: 'Monitoring & Usage',
167
+ icon: '▸',
168
+ commands: [
169
+ { name: 'usage', desc: 'Token usage and cost tracking (daily, weekly, monthly, session)' },
170
+ { name: 'dashboard', desc: 'Live TUI dashboard for session monitoring' },
171
+ { name: 'stream', desc: 'Stream capture status and management' },
172
+ { name: 'hooks', desc: 'Install, verify, and manage ekkOS hooks' },
173
+ ],
174
+ },
175
+ {
176
+ title: 'Remote Terminal',
177
+ icon: '▸',
178
+ commands: [
179
+ { name: 'setup-remote', desc: 'Set up remote access (run Claude on your PC from anywhere)' },
180
+ { name: 'agent', desc: 'Manage the remote terminal agent (start, stop, status, logs)' },
181
+ ],
182
+ },
183
+ {
184
+ title: 'Swarm (Multi-Agent)',
185
+ icon: '▸',
186
+ commands: [
187
+ { name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
188
+ ],
189
+ },
190
+ ];
191
+ const padCmd = 18;
192
+ let output = '';
193
+ output += chalk_1.default.cyan.bold('Usage:') + ` ekkos ${chalk_1.default.gray('[command] [options]')}\n`;
194
+ for (const group of groups) {
195
+ output += '\n' + chalk_1.default.cyan.bold(`${group.icon} ${group.title}`) + '\n';
196
+ for (const c of group.commands) {
197
+ const nameStr = c.note
198
+ ? chalk_1.default.green(c.name) + chalk_1.default.gray(` (${c.note})`)
199
+ : chalk_1.default.green(c.name);
200
+ // Compute visible length for padding (strip ANSI)
201
+ const visLen = c.note ? c.name.length + ` (${c.note})`.length : c.name.length;
202
+ const pad = Math.max(1, padCmd - visLen);
203
+ output += ` ${nameStr}${' '.repeat(pad)}${chalk_1.default.gray(c.desc)}\n`;
204
+ }
205
+ }
206
+ // Options
207
+ const opts = helper.visibleOptions(cmd);
208
+ if (opts.length > 0) {
209
+ output += '\n' + chalk_1.default.cyan.bold('Options:') + '\n';
210
+ for (const opt of opts) {
211
+ const term = helper.optionTerm(opt);
212
+ const desc = helper.optionDescription(opt);
213
+ output += ` ${chalk_1.default.green(term.padEnd(22))}${desc}\n`;
214
+ }
215
+ }
216
+ return output;
217
+ },
218
+ });
62
219
  // Main init command (combined auth + setup)
63
220
  commander_1.program
64
221
  .command('init')
@@ -88,15 +245,39 @@ commander_1.program
88
245
  .option('-v, --verbose', 'Show debug output')
89
246
  .option('-d, --doctor', 'Run diagnostics before starting')
90
247
  .option('-r, --research', 'Auto-run research agent on startup (scans arXiv for new AI papers)')
91
- .option('--no-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
248
+ .option('--skip-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
249
+ .option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
250
+ .option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
251
+ .option('--dashboard', 'Launch with live usage dashboard in an isolated 60/40 tmux split (requires tmux)')
252
+ .option('--kickstart', 'Auto-send "test" on load to create session immediately (used internally by --dashboard)')
253
+ .option('--add-dir <dirs...>', 'Additional directories Claude Code can access (outside working directory)')
92
254
  .action((options) => {
93
255
  (0, run_1.run)({
94
256
  session: options.session,
95
257
  bypass: options.bypass,
96
258
  verbose: options.verbose,
97
259
  doctor: options.doctor,
98
- noInject: options.noInject,
99
- research: options.research
260
+ noInject: options.skipInject,
261
+ research: options.research,
262
+ noDna: options.skipDna,
263
+ noProxy: options.skipProxy,
264
+ dashboard: options.dashboard,
265
+ kickstart: options.kickstart,
266
+ addDirs: options.addDir,
267
+ });
268
+ });
269
+ // Test Claude — bare proxy test (no CLI wrapper)
270
+ commander_1.program
271
+ .command('test-claude')
272
+ .description('Launch Claude Code with proxy only (no ccDNA, no PTY, no injections) for debugging')
273
+ .option('--no-proxy', 'Skip proxy too (completely vanilla Claude)')
274
+ .option('--no-hooks', 'Temporarily disable all hooks during test')
275
+ .option('-v, --verbose', 'Show debug output')
276
+ .action((options) => {
277
+ (0, test_claude_1.testClaude)({
278
+ noProxy: options.proxy === false,
279
+ noHooks: options.hooks === false,
280
+ verbose: options.verbose,
100
281
  });
101
282
  });
102
283
  // Doctor command - check system prerequisites
@@ -169,6 +350,10 @@ hooksCmd
169
350
  .action((options) => {
170
351
  (0, hooks_1.hooksStatus)({ verbose: options.verbose });
171
352
  });
353
+ // Usage command - track Claude Code token usage and costs (powered by ccusage)
354
+ (0, index_1.registerUsageCommand)(commander_1.program);
355
+ // Dashboard command - live TUI for monitoring session usage
356
+ commander_1.program.addCommand(dashboard_1.dashboardCommand);
172
357
  // Sessions command - list active Claude Code sessions (swarm support)
173
358
  commander_1.program
174
359
  .command('sessions')
@@ -201,7 +386,7 @@ commander_1.program
201
386
  }
202
387
  }
203
388
  });
204
- // Deprecated setup command (redirects to init)
389
+ // Deprecated setup command (redirects to init) — hidden from root help by custom formatHelp
205
390
  commander_1.program
206
391
  .command('setup')
207
392
  .description('[DEPRECATED] Use "ekkos init" instead')
@@ -290,4 +475,76 @@ agentCmd
290
475
  .action((options) => {
291
476
  (0, agent_1.agentLogs)({ follow: options.follow });
292
477
  });
478
+ agentCmd
479
+ .command('health')
480
+ .description('Check agent daemon health and diagnose connection issues')
481
+ .option('-j, --json', 'Output machine-readable JSON')
482
+ .action((options) => {
483
+ (0, agent_1.agentHealth)({ json: options.json });
484
+ });
485
+ // Swarm command - manage Q-learning routing
486
+ const swarmCmd = commander_1.program
487
+ .command('swarm')
488
+ .description('Manage Swarm Q-learning model routing');
489
+ swarmCmd
490
+ .command('status')
491
+ .description('Show Q-table stats (states, visits, epsilon, top actions)')
492
+ .action(swarm_1.swarmStatus);
493
+ swarmCmd
494
+ .command('reset')
495
+ .description('Clear Q-table from Redis (routing reverts to static rules)')
496
+ .action(swarm_1.swarmReset);
497
+ swarmCmd
498
+ .command('export')
499
+ .description('Export Q-table to .swarm/q-learning-model.json')
500
+ .action(swarm_1.swarmExport);
501
+ swarmCmd
502
+ .command('import')
503
+ .description('Import Q-table from .swarm/q-learning-model.json into Redis')
504
+ .action(swarm_1.swarmImport);
505
+ swarmCmd
506
+ .command('launch')
507
+ .description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
508
+ .option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
509
+ .option('-t, --task <task>', 'Task description to decompose and execute')
510
+ .option('--no-bypass', 'Disable bypass permissions mode')
511
+ .option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
512
+ .option('--no-queen', 'Skip launching the Python Queen coordinator')
513
+ .option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
514
+ .option('-v, --verbose', 'Show debug output')
515
+ .action((options) => {
516
+ // Auto-open wizard when --task is missing
517
+ if (!options.task) {
518
+ (0, swarm_setup_1.swarmSetup)();
519
+ return;
520
+ }
521
+ (0, swarm_1.swarmLaunch)({
522
+ workers: options.workers || 4,
523
+ task: options.task,
524
+ bypass: options.bypass !== false,
525
+ noDecompose: options.decompose === false,
526
+ noQueen: options.queen === false,
527
+ queenStrategy: options.queenStrategy,
528
+ verbose: options.verbose,
529
+ });
530
+ });
531
+ swarmCmd
532
+ .command('setup')
533
+ .description('Interactive TUI wizard for configuring and launching a swarm')
534
+ .action(() => {
535
+ (0, swarm_setup_1.swarmSetup)();
536
+ });
537
+ swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
538
+ // Handle `-help` (single dash) — rewrite to `--help` for Commander compatibility
539
+ const helpIdx = process.argv.indexOf('-help');
540
+ if (helpIdx !== -1) {
541
+ process.argv[helpIdx] = '--help';
542
+ }
543
+ // Default to `run` if no command specified (e.g. `ekkos`, `ekkos -b --dashboard`)
544
+ const knownCommands = commander_1.program.commands.map(c => c.name());
545
+ const userArgs = process.argv.slice(2);
546
+ const hasCommand = userArgs.some(a => knownCommands.includes(a) || a === 'help' || a === '--help' || a === '-h' || a === '--version' || a === '-V');
547
+ if (!hasCommand) {
548
+ process.argv.splice(2, 0, 'run');
549
+ }
293
550
  commander_1.program.parse();
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tmux-scrollbar.ts
4
+ *
5
+ * A thin visual scrollbar (2 columns wide) that runs in its own narrow tmux pane.
6
+ * It tracks the scroll position of a target pane (Claude Code) and renders a
7
+ * proportional scrollbar thumb. Supports click-to-jump and drag-to-scroll.
8
+ *
9
+ * Usage: node tmux-scrollbar.js <target-pane>
10
+ * Example: node tmux-scrollbar.js :.0
11
+ *
12
+ * Layout: [Claude Code (68%)] [Scrollbar (2col)] [Dashboard (30%)]
13
+ */
14
+ export {};
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * tmux-scrollbar.ts
5
+ *
6
+ * A thin visual scrollbar (2 columns wide) that runs in its own narrow tmux pane.
7
+ * It tracks the scroll position of a target pane (Claude Code) and renders a
8
+ * proportional scrollbar thumb. Supports click-to-jump and drag-to-scroll.
9
+ *
10
+ * Usage: node tmux-scrollbar.js <target-pane>
11
+ * Example: node tmux-scrollbar.js :.0
12
+ *
13
+ * Layout: [Claude Code (68%)] [Scrollbar (2col)] [Dashboard (30%)]
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ const child_process_1 = require("child_process");
17
+ const TARGET_PANE = process.argv[2] || ':.0';
18
+ const POLL_MS = 200;
19
+ // ── ANSI escape sequences ──
20
+ const ESC = '\x1b';
21
+ const CSI = `${ESC}[`;
22
+ const ansi = {
23
+ clear: `${CSI}2J`,
24
+ home: `${CSI}H`,
25
+ hideCursor: `${CSI}?25l`,
26
+ showCursor: `${CSI}?25h`,
27
+ // SGR extended mouse protocol: supports coordinates > 223
28
+ enableMouse: `${CSI}?1000h${CSI}?1002h${CSI}?1006h`,
29
+ disableMouse: `${CSI}?1000l${CSI}?1002l${CSI}?1006l`,
30
+ reset: `${CSI}0m`,
31
+ moveTo: (row, col) => `${CSI}${row};${col}H`,
32
+ // 256-color mode
33
+ fg256: (n) => `${CSI}38;5;${n}m`,
34
+ bg256: (n) => `${CSI}48;5;${n}m`,
35
+ };
36
+ // ── Color scheme (matches dashboard cyan theme) ──
37
+ const COLORS = {
38
+ thumbFg: ansi.fg256(44), // Cyan thumb
39
+ thumbBg: ansi.bg256(23), // Dark cyan background
40
+ thumbActiveFg: ansi.fg256(51), // Bright cyan when dragging
41
+ thumbActiveBg: ansi.bg256(30), // Brighter dark cyan when dragging
42
+ trackFg: ansi.fg256(236), // Very dark gray track dots
43
+ trackBg: ansi.bg256(233), // Near-black background
44
+ labelFg: ansi.fg256(240), // Dim label text
45
+ };
46
+ // ── State ──
47
+ let lastRenderedFrame = '';
48
+ let isDragging = false;
49
+ let pollTimer;
50
+ function getHeight() {
51
+ return process.stdout.rows || 40;
52
+ }
53
+ /**
54
+ * Query tmux for the target pane's scroll position.
55
+ * Returns { inMode, scrollPos, historySize, paneHeight }
56
+ */
57
+ function getPaneInfo() {
58
+ try {
59
+ const raw = (0, child_process_1.execSync)(`tmux display-message -p -t "${TARGET_PANE}" ` +
60
+ `'#{pane_in_mode}|#{scroll_position}|#{history_size}|#{pane_height}'`, { encoding: 'utf-8', timeout: 800, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
61
+ const [mode, pos, hist, ph] = raw.split('|');
62
+ return {
63
+ inMode: mode === '1',
64
+ scrollPos: parseInt(pos) || 0,
65
+ historySize: parseInt(hist) || 0,
66
+ paneHeight: parseInt(ph) || 40,
67
+ };
68
+ }
69
+ catch {
70
+ return { inMode: false, scrollPos: 0, historySize: 0, paneHeight: 40 };
71
+ }
72
+ }
73
+ /**
74
+ * Render the scrollbar to stdout using ANSI escape codes.
75
+ * Only redraws if the frame has changed (diffing).
76
+ */
77
+ function render() {
78
+ const height = getHeight();
79
+ const info = getPaneInfo();
80
+ // Total scrollable content
81
+ const totalLines = info.historySize + info.paneHeight;
82
+ if (totalLines <= 0)
83
+ return;
84
+ // Thumb size proportional to viewport/total
85
+ const viewportRatio = Math.min(1, info.paneHeight / totalLines);
86
+ const thumbSize = Math.max(2, Math.round(viewportRatio * height));
87
+ // Thumb position
88
+ let thumbTop;
89
+ if (!info.inMode || info.historySize === 0) {
90
+ // Live output — thumb at bottom
91
+ thumbTop = height - thumbSize;
92
+ }
93
+ else {
94
+ // Copy mode — scroll_position = lines from bottom
95
+ const scrollFraction = Math.min(1, info.scrollPos / Math.max(1, info.historySize));
96
+ thumbTop = Math.round((1 - scrollFraction) * (height - thumbSize));
97
+ }
98
+ // Clamp
99
+ thumbTop = Math.max(0, Math.min(height - thumbSize, thumbTop));
100
+ // Build frame string
101
+ const fgColor = isDragging ? COLORS.thumbActiveFg : COLORS.thumbFg;
102
+ const bgColor = isDragging ? COLORS.thumbActiveBg : COLORS.thumbBg;
103
+ let frame = ansi.home;
104
+ for (let y = 0; y < height; y++) {
105
+ const isThumb = y >= thumbTop && y < thumbTop + thumbSize;
106
+ frame += ansi.moveTo(y + 1, 1);
107
+ if (isThumb) {
108
+ frame += `${fgColor}${bgColor}██${ansi.reset}`;
109
+ }
110
+ else {
111
+ frame += `${COLORS.trackFg}${COLORS.trackBg}░░${ansi.reset}`;
112
+ }
113
+ }
114
+ // Only write if changed
115
+ if (frame !== lastRenderedFrame) {
116
+ process.stdout.write(frame);
117
+ lastRenderedFrame = frame;
118
+ }
119
+ }
120
+ /**
121
+ * Scroll the target pane to a position based on scrollbar click location.
122
+ */
123
+ function scrollToPosition(y) {
124
+ const height = getHeight();
125
+ const info = getPaneInfo();
126
+ if (info.historySize <= 0)
127
+ return;
128
+ const ratio = Math.max(0, Math.min(1, y / Math.max(1, height - 1)));
129
+ // If clicking at the very bottom, exit copy-mode for live output
130
+ if (ratio >= 0.95) {
131
+ try {
132
+ if (info.inMode) {
133
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X cancel`, {
134
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
135
+ });
136
+ }
137
+ }
138
+ catch { /* ignore */ }
139
+ lastRenderedFrame = '';
140
+ render();
141
+ return;
142
+ }
143
+ // Target line from top of scrollback
144
+ const targetLine = Math.round(ratio * info.historySize);
145
+ try {
146
+ // Enter copy-mode if not already
147
+ if (!info.inMode) {
148
+ (0, child_process_1.execSync)(`tmux copy-mode -t "${TARGET_PANE}"`, {
149
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
150
+ });
151
+ }
152
+ // Jump to target line using goto-line (tmux 3.1+)
153
+ // Fallback: history-top + cursor-down for older tmux
154
+ try {
155
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X goto-line ${targetLine}`, {
156
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
157
+ });
158
+ }
159
+ catch {
160
+ // Fallback for older tmux: go to top, then move down
161
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X history-top`, {
162
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
163
+ });
164
+ if (targetLine > 0) {
165
+ // Move in chunks to avoid command-line length limits
166
+ let remaining = targetLine;
167
+ while (remaining > 0) {
168
+ const chunk = Math.min(remaining, 5000);
169
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X -N ${chunk} cursor-down`, {
170
+ timeout: 1000, stdio: ['pipe', 'pipe', 'pipe'],
171
+ });
172
+ remaining -= chunk;
173
+ }
174
+ }
175
+ }
176
+ }
177
+ catch { /* ignore scroll errors */ }
178
+ lastRenderedFrame = '';
179
+ render();
180
+ }
181
+ /**
182
+ * Refocus the target pane (Claude Code) after scrollbar interaction.
183
+ */
184
+ function refocusTarget() {
185
+ try {
186
+ (0, child_process_1.execSync)(`tmux select-pane -t "${TARGET_PANE}"`, {
187
+ timeout: 300, stdio: ['pipe', 'pipe', 'pipe'],
188
+ });
189
+ }
190
+ catch { /* ignore */ }
191
+ }
192
+ // ── Mouse event handling ──
193
+ function handleInput(data) {
194
+ const str = data.toString();
195
+ // Parse SGR mouse events: ESC [ < button ; x ; y M (press) / m (release)
196
+ const events = str.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g);
197
+ for (const match of events) {
198
+ const button = parseInt(match[1]);
199
+ const y = parseInt(match[3]) - 1; // Convert to 0-indexed
200
+ const isPress = match[4] === 'M';
201
+ if (button === 0) {
202
+ // Left mouse button
203
+ if (isPress) {
204
+ isDragging = true;
205
+ scrollToPosition(y);
206
+ }
207
+ else {
208
+ // Release
209
+ if (isDragging) {
210
+ isDragging = false;
211
+ lastRenderedFrame = '';
212
+ render();
213
+ // Return focus to Claude Code pane after click
214
+ refocusTarget();
215
+ }
216
+ }
217
+ }
218
+ else if (button === 32 && isDragging) {
219
+ // Left button drag (SGR reports button 32 for motion with button 0 held)
220
+ scrollToPosition(y);
221
+ }
222
+ else if (button === 64) {
223
+ // Scroll wheel up — forward to target pane
224
+ try {
225
+ const info = getPaneInfo();
226
+ if (!info.inMode) {
227
+ (0, child_process_1.execSync)(`tmux copy-mode -t "${TARGET_PANE}" -e`, {
228
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
229
+ });
230
+ }
231
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X -N 5 scroll-up`, {
232
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
233
+ });
234
+ }
235
+ catch { /* ignore */ }
236
+ lastRenderedFrame = '';
237
+ render();
238
+ }
239
+ else if (button === 65) {
240
+ // Scroll wheel down — forward to target pane
241
+ try {
242
+ (0, child_process_1.execSync)(`tmux send-keys -t "${TARGET_PANE}" -X -N 5 scroll-down`, {
243
+ timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
244
+ });
245
+ }
246
+ catch { /* ignore */ }
247
+ lastRenderedFrame = '';
248
+ render();
249
+ }
250
+ return; // Processed mouse event
251
+ }
252
+ // Keyboard input
253
+ if (str === 'q' || str === '\x03') {
254
+ // Quit on 'q' or Ctrl-C
255
+ shutdown();
256
+ }
257
+ }
258
+ // ── Lifecycle ──
259
+ function setup() {
260
+ // Hide cursor, enable mouse tracking, clear screen
261
+ process.stdout.write(ansi.hideCursor + ansi.enableMouse + ansi.clear);
262
+ // Raw mode for stdin (capture mouse events)
263
+ if (process.stdin.setRawMode) {
264
+ process.stdin.setRawMode(true);
265
+ }
266
+ process.stdin.resume();
267
+ process.stdin.on('data', handleInput);
268
+ // Handle terminal resize
269
+ process.stdout.on('resize', () => {
270
+ lastRenderedFrame = '';
271
+ render();
272
+ });
273
+ }
274
+ function cleanup() {
275
+ process.stdout.write(ansi.showCursor + ansi.disableMouse + ansi.reset + ansi.clear);
276
+ if (process.stdin.setRawMode) {
277
+ try {
278
+ process.stdin.setRawMode(false);
279
+ }
280
+ catch { /* ignore */ }
281
+ }
282
+ }
283
+ function shutdown() {
284
+ clearInterval(pollTimer);
285
+ cleanup();
286
+ process.exit(0);
287
+ }
288
+ // Graceful exit
289
+ process.on('exit', cleanup);
290
+ process.on('SIGINT', shutdown);
291
+ process.on('SIGTERM', shutdown);
292
+ process.on('SIGHUP', shutdown);
293
+ // ── Main ──
294
+ setup();
295
+ render();
296
+ pollTimer = setInterval(render, POLL_MS);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Simple usage monitoring for ekkOS embedding costs
3
+ * Tracks daily usage to prevent runaway costs
4
+ */
5
+ interface UsageRecord {
6
+ date: string;
7
+ embeddings: number;
8
+ cost: number;
9
+ model: string;
10
+ }
11
+ interface DailyUsage {
12
+ date: string;
13
+ totalEmbeddings: number;
14
+ totalCost: number;
15
+ byModel: Record<string, {
16
+ count: number;
17
+ cost: number;
18
+ }>;
19
+ }
20
+ /**
21
+ * Load usage records
22
+ */
23
+ export declare function loadUsage(): Promise<UsageRecord[]>;
24
+ /**
25
+ * Record an embedding generation
26
+ */
27
+ export declare function recordEmbedding(model?: string, tokens?: number): Promise<void>;
28
+ /**
29
+ * Get usage for a specific date
30
+ */
31
+ export declare function getUsageForDate(date: string): Promise<DailyUsage>;
32
+ /**
33
+ * Get usage for the last N days
34
+ */
35
+ export declare function getUsageForDays(days: number): Promise<DailyUsage[]>;
36
+ /**
37
+ * Check if usage exceeds threshold
38
+ */
39
+ export declare function checkThreshold(dailyLimit?: number, costLimit?: number): Promise<{
40
+ exceeded: boolean;
41
+ usage: DailyUsage;
42
+ }>;
43
+ /**
44
+ * Clear old usage data (keep last 90 days)
45
+ */
46
+ export declare function pruneOldUsage(): Promise<void>;
47
+ export {};