@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,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);
@@ -1,3 +1,36 @@
1
+ export interface ActiveSession {
2
+ sessionId: string;
3
+ sessionName: string;
4
+ pid: number;
5
+ startedAt: string;
6
+ projectPath: string;
7
+ lastHeartbeat: string;
8
+ }
9
+ interface SessionNameResolution {
10
+ uuid: string;
11
+ sessionName: string;
12
+ projectPath: string;
13
+ encodedProjectPath: string;
14
+ startedAt?: string;
15
+ }
16
+ /** Detect ekkOS 3-word session names like "lit-lex-zip" */
17
+ export declare function isEkkosSessionName(name: string): boolean;
18
+ /** Resolve an ekkOS session name to a JSONL UUID */
19
+ export declare function resolveSessionName(name: string): SessionNameResolution | null;
20
+ /** Parse a single JSONL file and return SessionUsage for an ekkOS-named session */
21
+ export declare function getSessionUsageByName(name: string): Promise<SessionUsage | null>;
22
+ /** List ekkOS sessions with lightweight cost data (for session list view) */
23
+ export declare function listEkkosSessions(limit?: number): Promise<{
24
+ name: string;
25
+ uuid: string;
26
+ projectPath: string;
27
+ startedAt: string;
28
+ lastHeartbeat: string;
29
+ cost: number;
30
+ tokens: number;
31
+ models: string[];
32
+ turnCount: number;
33
+ }[]>;
1
34
  export interface TurnMetrics {
2
35
  turn_number: number;
3
36
  timestamp: string;
@@ -37,9 +70,10 @@ export interface CcusageSession {
37
70
  outputTokens: number;
38
71
  cacheCreationTokens: number;
39
72
  cacheReadTokens: number;
40
- totalTokens: number;
73
+ cost: number;
41
74
  totalCost: number;
42
75
  lastActivity: string;
76
+ versions?: string[];
43
77
  modelsUsed: string[];
44
78
  modelBreakdowns: {
45
79
  modelName: string;
@@ -51,15 +85,54 @@ export interface CcusageSession {
51
85
  }[];
52
86
  projectPath: string;
53
87
  }
54
- export interface CcusageOutput {
55
- sessions: CcusageSession[];
88
+ /** Daily usage entry from ccusage */
89
+ export interface CcusageDailyUsage {
90
+ date: string;
91
+ inputTokens: number;
92
+ outputTokens: number;
93
+ cacheCreationTokens: number;
94
+ cacheReadTokens: number;
95
+ cost: number;
96
+ totalCost: number;
97
+ modelsUsed: string[];
98
+ modelBreakdowns: {
99
+ modelName: string;
100
+ inputTokens: number;
101
+ outputTokens: number;
102
+ cacheCreationTokens: number;
103
+ cacheReadTokens: number;
104
+ cost: number;
105
+ }[];
106
+ }
107
+ /** Weekly usage entry from ccusage */
108
+ export interface CcusageWeeklyUsage {
109
+ week: string;
110
+ inputTokens: number;
111
+ outputTokens: number;
112
+ cacheCreationTokens: number;
113
+ cacheReadTokens: number;
114
+ totalCost: number;
115
+ modelsUsed: string[];
116
+ modelBreakdowns: CcusageDailyUsage['modelBreakdowns'];
117
+ }
118
+ /** Monthly usage entry from ccusage */
119
+ export interface CcusageMonthlyUsage {
120
+ month: string;
121
+ inputTokens: number;
122
+ outputTokens: number;
123
+ cacheCreationTokens: number;
124
+ cacheReadTokens: number;
125
+ cost: number;
126
+ totalCost: number;
127
+ modelsUsed: string[];
128
+ modelBreakdowns: CcusageDailyUsage['modelBreakdowns'];
56
129
  }
57
130
  /**
58
- * Get all sessions using ccusage
131
+ * Get all sessions using ccusage library
59
132
  */
60
133
  export declare function getAllSessions(instanceId?: string): Promise<SessionUsage[]>;
61
134
  /**
62
- * Get specific session usage using ccusage
135
+ * Get specific session usage using ccusage library
63
136
  */
64
137
  export declare function getSessionUsage(sessionId: string, instanceId?: string): Promise<SessionUsage | null>;
65
138
  /**
@@ -70,3 +143,20 @@ export declare function getSessionName(sessionId: string): string;
70
143
  * Get current session ID from active Claude Code process
71
144
  */
72
145
  export declare function getCurrentSessionId(): string | null;
146
+ /**
147
+ * Load daily usage data via ccusage library
148
+ */
149
+ export declare function getDailyUsage(): Promise<CcusageDailyUsage[]>;
150
+ /**
151
+ * Load weekly usage data via ccusage library
152
+ */
153
+ export declare function getWeeklyUsage(): Promise<CcusageWeeklyUsage[]>;
154
+ /**
155
+ * Load monthly usage data via ccusage library
156
+ */
157
+ export declare function getMonthlyUsage(): Promise<CcusageMonthlyUsage[]>;
158
+ /**
159
+ * Load bucket (5-hour block) usage data via ccusage library
160
+ */
161
+ export declare function getBucketUsage(): Promise<any[]>;
162
+ export {};