@ekkos/cli 0.2.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 (135) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +129 -0
  2. package/dist/cache/LocalSessionStore.js +688 -0
  3. package/dist/cache/capture.d.ts +26 -0
  4. package/dist/cache/capture.js +461 -0
  5. package/dist/cache/index.d.ts +7 -0
  6. package/dist/cache/index.js +23 -0
  7. package/dist/cache/types.d.ts +147 -0
  8. package/dist/cache/types.js +40 -0
  9. package/dist/commands/init.d.ts +9 -0
  10. package/dist/commands/init.js +478 -0
  11. package/dist/commands/run.d.ts +12 -0
  12. package/dist/commands/run.js +829 -0
  13. package/dist/commands/setup.d.ts +6 -0
  14. package/dist/commands/setup.js +658 -0
  15. package/dist/commands/status.d.ts +1 -0
  16. package/dist/commands/status.js +109 -0
  17. package/dist/commands/test.d.ts +1 -0
  18. package/dist/commands/test.js +157 -0
  19. package/dist/deploy/agents.d.ts +15 -0
  20. package/dist/deploy/agents.js +72 -0
  21. package/dist/deploy/hooks.d.ts +16 -0
  22. package/dist/deploy/hooks.js +121 -0
  23. package/dist/deploy/index.d.ts +7 -0
  24. package/dist/deploy/index.js +24 -0
  25. package/dist/deploy/instructions.d.ts +12 -0
  26. package/dist/deploy/instructions.js +36 -0
  27. package/dist/deploy/mcp.d.ts +19 -0
  28. package/dist/deploy/mcp.js +109 -0
  29. package/dist/deploy/plugins.d.ts +19 -0
  30. package/dist/deploy/plugins.js +62 -0
  31. package/dist/deploy/settings.d.ts +8 -0
  32. package/dist/deploy/settings.js +84 -0
  33. package/dist/deploy/skills.d.ts +19 -0
  34. package/dist/deploy/skills.js +60 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +71 -0
  37. package/dist/restore/RestoreOrchestrator.d.ts +48 -0
  38. package/dist/restore/RestoreOrchestrator.js +481 -0
  39. package/dist/restore/index.d.ts +4 -0
  40. package/dist/restore/index.js +20 -0
  41. package/dist/utils/platform.d.ts +29 -0
  42. package/dist/utils/platform.js +65 -0
  43. package/dist/utils/session-words.json +119 -0
  44. package/dist/utils/state.d.ts +57 -0
  45. package/dist/utils/state.js +186 -0
  46. package/dist/utils/templates.d.ts +24 -0
  47. package/dist/utils/templates.js +118 -0
  48. package/package.json +48 -0
  49. package/templates/CLAUDE.md +287 -0
  50. package/templates/README.md +378 -0
  51. package/templates/agents/README.md +182 -0
  52. package/templates/agents/code-reviewer.md +166 -0
  53. package/templates/agents/debug-detective.md +169 -0
  54. package/templates/agents/ekkOS_Vercel.md +99 -0
  55. package/templates/agents/extension-manager.md +229 -0
  56. package/templates/agents/git-companion.md +185 -0
  57. package/templates/agents/github-test-agent.md +321 -0
  58. package/templates/agents/railway-manager.md +179 -0
  59. package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
  60. package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
  61. package/templates/claude-plugins/README.md +587 -0
  62. package/templates/claude-plugins/agents/code-reviewer.json +14 -0
  63. package/templates/claude-plugins/agents/debug-detective.json +15 -0
  64. package/templates/claude-plugins/agents/git-companion.json +14 -0
  65. package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
  66. package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
  67. package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
  68. package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
  69. package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
  70. package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
  71. package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
  72. package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
  73. package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
  74. package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
  75. package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
  76. package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
  77. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
  78. package/templates/claude-plugins-admin/README.md +446 -0
  79. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
  80. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
  81. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
  82. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
  83. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
  84. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
  85. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
  86. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
  87. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
  88. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
  89. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
  90. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
  91. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
  92. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
  93. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
  94. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
  95. package/templates/commands/continue.md +47 -0
  96. package/templates/cursor-hooks/after-agent-response.sh +117 -0
  97. package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
  98. package/templates/cursor-hooks/hooks.json +20 -0
  99. package/templates/cursor-hooks/lib/contract.sh +320 -0
  100. package/templates/cursor-hooks/stop.sh +75 -0
  101. package/templates/cursor-rules/ekkos-memory.md +187 -0
  102. package/templates/hooks/assistant-response.sh +96 -0
  103. package/templates/hooks/hooks.json +28 -0
  104. package/templates/hooks/lib/contract.sh +320 -0
  105. package/templates/hooks/lib/state.sh +158 -0
  106. package/templates/hooks/session-start.ps1 +41 -0
  107. package/templates/hooks/session-start.sh +318 -0
  108. package/templates/hooks/stop.ps1 +16 -0
  109. package/templates/hooks/stop.sh +989 -0
  110. package/templates/hooks/user-prompt-submit.ps1 +174 -0
  111. package/templates/hooks/user-prompt-submit.sh +587 -0
  112. package/templates/hooks-node/lib/state.js +187 -0
  113. package/templates/hooks-node/stop.js +416 -0
  114. package/templates/hooks-node/user-prompt-submit.js +337 -0
  115. package/templates/plan-template.md +306 -0
  116. package/templates/rules/00-hooks-contract.mdc +89 -0
  117. package/templates/rules/30-ekkos-core.mdc +188 -0
  118. package/templates/rules/31-ekkos-messages.mdc +78 -0
  119. package/templates/skills/continue/SKILL.md +169 -0
  120. package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
  121. package/templates/skills/ekkOS_Learn/Skill.md +265 -0
  122. package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
  123. package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
  124. package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
  125. package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
  126. package/templates/skills/ekkOS_Safety/Skill.md +265 -0
  127. package/templates/skills/ekkOS_Schema/Skill.md +251 -0
  128. package/templates/skills/ekkOS_Summary/Skill.md +257 -0
  129. package/templates/skills/ekkOS_Vault/Skill.md +287 -0
  130. package/templates/skills/permissions/Skill.md +322 -0
  131. package/templates/spec-template.md +159 -0
  132. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  133. package/templates/windsurf-hooks/hooks.json +10 -0
  134. package/templates/windsurf-hooks/lib/contract.sh +320 -0
  135. package/templates/windsurf-rules/ekkos-memory.md +129 -0
@@ -0,0 +1,829 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.run = run;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const os = __importStar(require("os"));
44
+ const child_process_1 = require("child_process");
45
+ const state_1 = require("../utils/state");
46
+ // Try to load node-pty (may fail on Node 24+)
47
+ let pty = null;
48
+ try {
49
+ pty = require('node-pty');
50
+ }
51
+ catch {
52
+ // node-pty not available, will use spawn fallback
53
+ }
54
+ function getConfig(options) {
55
+ return {
56
+ slashOpenDelayMs: options.slashOpenDelayMs ??
57
+ parseInt(process.env.EKKOS_SLASH_OPEN_DELAY_MS || '1000', 10),
58
+ charDelayMs: options.charDelayMs ??
59
+ parseInt(process.env.EKKOS_CHAR_DELAY_MS || '25', 10),
60
+ postEnterDelayMs: options.postEnterDelayMs ??
61
+ parseInt(process.env.EKKOS_POST_ENTER_DELAY_MS || '500', 10),
62
+ clearWaitMs: options.clearWaitMs ??
63
+ parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '5000', 10),
64
+ idlePromptMs: parseInt(process.env.EKKOS_IDLE_PROMPT_MS || '250', 10),
65
+ paletteRetryMs: parseInt(process.env.EKKOS_PALETTE_RETRY_MS || '500', 10),
66
+ debugLogPath: options.debugLogPath ??
67
+ process.env.EKKOS_DEBUG_LOG_PATH ??
68
+ path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log')
69
+ };
70
+ }
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+ // PATTERN MATCHING
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+ // ANSI escape sequence regex (covers all common codes)
75
+ const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
76
+ /**
77
+ * Strip ANSI escape codes from string
78
+ */
79
+ function stripAnsi(str) {
80
+ return str.replace(ANSI_REGEX, '');
81
+ }
82
+ /**
83
+ * Normalize string for pattern matching (strip ANSI, collapse whitespace, lowercase)
84
+ */
85
+ function normalizeForMatch(str) {
86
+ return stripAnsi(str)
87
+ .replace(/\r/g, '')
88
+ .replace(/\s+/g, ' ')
89
+ .toLowerCase();
90
+ }
91
+ // Context limit detection patterns
92
+ const CONTEXT_WALL_REGEX = /context limit reached.*\/(compact|clear)\b.*to continue/i;
93
+ // Idle prompt detection - Claude shows "> " when ready for input
94
+ const IDLE_PROMPT_REGEX = />\s*$/;
95
+ // Interrupted state detection - Claude shows this when Ctrl+C during generation
96
+ const INTERRUPTED_REGEX = /interrupted.*what should claude do instead/i;
97
+ // Command palette indicator - when / is typed, Claude shows a menu
98
+ const PALETTE_INDICATOR_REGEX = /\/(clear|continue|compact|help|bug|config)/i;
99
+ // ═══════════════════════════════════════════════════════════════════════════
100
+ // LOGGING (FILE ONLY DURING TUI - NO TERMINAL CORRUPTION)
101
+ // ═══════════════════════════════════════════════════════════════════════════
102
+ let _debugLogPath = path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log');
103
+ /**
104
+ * Set the debug log path (called once during init)
105
+ */
106
+ function setDebugLogPath(p) {
107
+ _debugLogPath = p;
108
+ }
109
+ /**
110
+ * Log to file only (not terminal) during TUI operation
111
+ * This is the ONLY logging function used after Claude spawns
112
+ */
113
+ function dlog(...args) {
114
+ try {
115
+ fs.appendFileSync(_debugLogPath, `[${new Date().toISOString()}] ${args.map(String).join(' ')}\n`);
116
+ }
117
+ catch {
118
+ // Ignore log errors - never corrupt TUI
119
+ }
120
+ }
121
+ // ═══════════════════════════════════════════════════════════════════════════
122
+ // SLASH COMMAND INJECTION (THE SINGLE BLESSED PATH)
123
+ // ═══════════════════════════════════════════════════════════════════════════
124
+ /**
125
+ * Type text character by character (human-like typing)
126
+ */
127
+ async function typeSlowly(shell, text, charDelayMs) {
128
+ for (const char of text) {
129
+ shell.write(char);
130
+ await sleep(charDelayMs);
131
+ }
132
+ }
133
+ /**
134
+ * Wait for idle prompt - returns true if prompt is idle, false if interrupted state detected
135
+ *
136
+ * The readiness gate: waits until the output buffer shows an idle "> " prompt
137
+ * that hasn't changed for idlePromptMs. This prevents injecting while Claude
138
+ * is busy generating ("Channelling...").
139
+ */
140
+ async function waitForIdlePrompt(getOutputBuffer, config, maxWaitMs = 10000) {
141
+ const startTime = Date.now();
142
+ let lastOutput = '';
143
+ let stableTime = 0;
144
+ while (Date.now() - startTime < maxWaitMs) {
145
+ const currentOutput = getOutputBuffer();
146
+ const normalized = normalizeForMatch(currentOutput);
147
+ // Check for interrupted state - abort injection
148
+ if (INTERRUPTED_REGEX.test(normalized)) {
149
+ dlog('Detected interrupted state - aborting injection');
150
+ return { ready: false, interrupted: true };
151
+ }
152
+ // Check if output has been stable
153
+ if (currentOutput === lastOutput) {
154
+ stableTime += 50;
155
+ if (stableTime >= config.idlePromptMs) {
156
+ // Check if we have an idle prompt
157
+ if (IDLE_PROMPT_REGEX.test(stripAnsi(currentOutput))) {
158
+ dlog('Idle prompt detected - ready to inject');
159
+ return { ready: true, interrupted: false };
160
+ }
161
+ }
162
+ }
163
+ else {
164
+ stableTime = 0;
165
+ lastOutput = currentOutput;
166
+ }
167
+ await sleep(50);
168
+ }
169
+ dlog('Idle prompt wait timed out - proceeding anyway');
170
+ return { ready: true, interrupted: false };
171
+ }
172
+ /**
173
+ * Run a slash command by typing it like a human
174
+ *
175
+ * This is the SINGLE BLESSED PATH for command injection.
176
+ * Flow: Ctrl+U → / → wait for palette → type command → Enter
177
+ *
178
+ * Includes safeguards:
179
+ * - Readiness gate (waits for idle prompt)
180
+ * - Palette retry (if palette doesn't appear)
181
+ * - Interrupted state detection (aborts if user Ctrl+C'd)
182
+ */
183
+ async function runSlashCommand(shell, command, config, getOutputBuffer, arg) {
184
+ dlog(`runSlashCommand: /${command}${arg ? ' ' + arg : ''}`);
185
+ // STEP 1: Clear current input line
186
+ shell.write('\x15'); // Ctrl+U
187
+ await sleep(60);
188
+ dlog('Input line cleared (Ctrl+U)');
189
+ // STEP 2: Type / to open command palette
190
+ await typeSlowly(shell, '/', config.charDelayMs);
191
+ dlog('Typed / to open palette');
192
+ // STEP 3: Wait for palette to open
193
+ await sleep(config.slashOpenDelayMs);
194
+ // STEP 4: Check if palette opened (look for command indicators in buffer)
195
+ const bufferAfterSlash = getOutputBuffer();
196
+ if (!PALETTE_INDICATOR_REGEX.test(stripAnsi(bufferAfterSlash))) {
197
+ // Palette might not have opened - retry once
198
+ dlog('Palette indicator not detected, retrying with extra wait');
199
+ await sleep(config.paletteRetryMs);
200
+ // Type / again in case first one was eaten
201
+ shell.write('\x15'); // Clear line again
202
+ await sleep(60);
203
+ await typeSlowly(shell, '/', config.charDelayMs);
204
+ await sleep(config.slashOpenDelayMs);
205
+ }
206
+ // STEP 5: Type the command
207
+ await typeSlowly(shell, command, config.charDelayMs);
208
+ dlog(`Typed command: ${command}`);
209
+ // STEP 6: Type argument if provided
210
+ if (arg) {
211
+ await typeSlowly(shell, ' ' + arg, config.charDelayMs);
212
+ dlog(`Typed argument: ${arg}`);
213
+ }
214
+ // STEP 7: Wait briefly then send Enter
215
+ await sleep(60);
216
+ shell.write('\r'); // Enter (carriage return works better than \n in PTY)
217
+ dlog('Sent Enter');
218
+ // STEP 8: Wait for command to start executing
219
+ await sleep(config.postEnterDelayMs);
220
+ return { success: true };
221
+ }
222
+ // Memory API URL
223
+ const MEMORY_API_URL = 'https://mcp.ekkos.dev';
224
+ const isWindows = os.platform() === 'win32';
225
+ // Pinned Claude Code version for ekkos run
226
+ // 2.1.6 has the old context calculation (95% of full 200K, not effective window)
227
+ const PINNED_CLAUDE_VERSION = '2.1.6';
228
+ /**
229
+ * Resolve full path to claude executable
230
+ * Returns 'npx:VERSION' to signal we should use npx with pinned version
231
+ */
232
+ function resolveClaudePath() {
233
+ // PRIORITY 1: Use pinned version via npx
234
+ // This ensures ekkos run always uses the version with better context behavior
235
+ // Return special marker that spawn logic will handle
236
+ return `npx:${PINNED_CLAUDE_VERSION}`;
237
+ }
238
+ /**
239
+ * Original resolve function for fallback to global install
240
+ */
241
+ function resolveGlobalClaudePath() {
242
+ // Windows global paths
243
+ if (isWindows) {
244
+ const windowsPaths = [
245
+ path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
246
+ path.join(process.env.LOCALAPPDATA || '', 'npm', 'claude.cmd'),
247
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'claude.cmd'),
248
+ path.join(os.homedir(), '.npm-global', 'claude.cmd')
249
+ ];
250
+ for (const p of windowsPaths) {
251
+ if (fs.existsSync(p)) {
252
+ return p;
253
+ }
254
+ }
255
+ try {
256
+ const result = (0, child_process_1.execSync)('where claude', { encoding: 'utf-8' }).trim().split('\n')[0];
257
+ if (result && fs.existsSync(result)) {
258
+ return result;
259
+ }
260
+ }
261
+ catch {
262
+ // Ignore errors
263
+ }
264
+ return 'claude';
265
+ }
266
+ // PRIORITY 2: Unix global paths
267
+ const commonPaths = [
268
+ '/opt/homebrew/bin/claude',
269
+ '/usr/local/bin/claude',
270
+ path.join(os.homedir(), '.local/bin/claude'),
271
+ path.join(os.homedir(), '.npm-global/bin/claude')
272
+ ];
273
+ for (const p of commonPaths) {
274
+ if (fs.existsSync(p)) {
275
+ return p;
276
+ }
277
+ }
278
+ try {
279
+ const result = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
280
+ if (result && fs.existsSync(result)) {
281
+ return result;
282
+ }
283
+ }
284
+ catch {
285
+ // Ignore errors
286
+ }
287
+ return 'claude';
288
+ }
289
+ /**
290
+ * Sleep helper
291
+ */
292
+ function sleep(ms) {
293
+ return new Promise(resolve => setTimeout(resolve, ms));
294
+ }
295
+ /**
296
+ * Emergency capture before clear - saves current state to ekkOS
297
+ */
298
+ async function emergencyCapture(transcriptPath, sessionId) {
299
+ const authToken = (0, state_1.getAuthToken)();
300
+ if (!authToken || !transcriptPath || !fs.existsSync(transcriptPath)) {
301
+ return;
302
+ }
303
+ try {
304
+ const transcript = fs.readFileSync(transcriptPath, 'utf-8');
305
+ const data = JSON.parse(transcript);
306
+ // Extract last user and assistant messages
307
+ let lastUser = '';
308
+ let lastAssistant = '';
309
+ if (Array.isArray(data)) {
310
+ for (let i = data.length - 1; i >= 0; i--) {
311
+ const msg = data[i];
312
+ if (msg.type === 'human' && !lastUser) {
313
+ lastUser = typeof msg.message?.content === 'string'
314
+ ? msg.message.content
315
+ : JSON.stringify(msg.message?.content || '');
316
+ }
317
+ if (msg.type === 'assistant' && !lastAssistant) {
318
+ lastAssistant = typeof msg.message?.content === 'string'
319
+ ? msg.message.content
320
+ : JSON.stringify(msg.message?.content || '');
321
+ }
322
+ if (lastUser && lastAssistant)
323
+ break;
324
+ }
325
+ }
326
+ // Send to ekkOS capture endpoint
327
+ const response = await fetch(`${MEMORY_API_URL}/api/v1/memory/capture`, {
328
+ method: 'POST',
329
+ headers: {
330
+ 'Authorization': `Bearer ${authToken}`,
331
+ 'Content-Type': 'application/json'
332
+ },
333
+ body: JSON.stringify({
334
+ user_query: lastUser,
335
+ assistant_response: lastAssistant,
336
+ session_id: sessionId,
337
+ metadata: {
338
+ emergency_capture: true,
339
+ reason: 'context_wall_hit',
340
+ timestamp: new Date().toISOString()
341
+ }
342
+ })
343
+ });
344
+ if (response.ok) {
345
+ dlog('Context captured to ekkOS');
346
+ }
347
+ }
348
+ catch (err) {
349
+ // Silent fail - don't block the clear process
350
+ dlog('Warning: Could not capture context');
351
+ }
352
+ }
353
+ async function run(options) {
354
+ const verbose = options.verbose || false;
355
+ const bypass = options.bypass || false;
356
+ // Get injection config (from options or env vars)
357
+ const config = getConfig(options);
358
+ setDebugLogPath(config.debugLogPath);
359
+ // ══════════════════════════════════════════════════════════════════════════
360
+ // STARTUP BANNER WITH COLOR PULSE ANIMATION
361
+ // ══════════════════════════════════════════════════════════════════════════
362
+ const logoLines = [
363
+ ' ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ▄▄',
364
+ ' ▄▄ ▄▄ ▄███████▄ █████▀▀▀ █ █ ▀ █',
365
+ '▄█▀█▄ ██ ▄█▀ ██ ▄█▀ ███ ███ ▀████▄',
366
+ '██▄█▀ ████ ████ ███▄▄▄███ ▀████',
367
+ '▀█▄▄▄ ██ ▀█▄ ██ ▀█▄ ▀█████▀ ███████▀',
368
+ ' ▄▄▄▄▄▄▄▄'
369
+ ];
370
+ // Color pulse sequence (magenta → cyan → blue → magenta cycle)
371
+ const pulseColors = [
372
+ chalk_1.default.magenta,
373
+ chalk_1.default.hex('#FF69B4'), // Hot pink
374
+ chalk_1.default.cyan,
375
+ chalk_1.default.hex('#00CED1'), // Dark cyan
376
+ chalk_1.default.blue,
377
+ chalk_1.default.hex('#8A2BE2'), // Blue violet
378
+ chalk_1.default.magenta
379
+ ];
380
+ // Print initial logo
381
+ console.log('');
382
+ logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
383
+ // Animate: pulse through colors
384
+ const PULSE_CYCLES = 2;
385
+ const FRAME_DELAY_MS = 80;
386
+ for (let cycle = 0; cycle < PULSE_CYCLES; cycle++) {
387
+ for (const colorFn of pulseColors) {
388
+ // Move cursor up to overwrite logo (6 lines)
389
+ process.stdout.write('\x1B[6A');
390
+ // Reprint with new color
391
+ logoLines.forEach(line => console.log(colorFn(line)));
392
+ await sleep(FRAME_DELAY_MS);
393
+ }
394
+ }
395
+ // Final frame: settle on magenta
396
+ process.stdout.write('\x1B[6A');
397
+ logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
398
+ // ══════════════════════════════════════════════════════════════════════════
399
+ // SPARKLE EFFECT - Random characters flash white/cyan
400
+ // ══════════════════════════════════════════════════════════════════════════
401
+ const sparkleChars = ['▄', '█', '▀'];
402
+ const sparkleColors = [chalk_1.default.white, chalk_1.default.whiteBright, chalk_1.default.cyanBright, chalk_1.default.yellowBright];
403
+ const SPARKLE_FRAMES = 40; // ~3.2 seconds of sparkles
404
+ const SPARKLE_DELAY_MS = 80;
405
+ const SPARKLES_PER_FRAME = 3;
406
+ for (let frame = 0; frame < SPARKLE_FRAMES; frame++) {
407
+ // Create a copy of logo lines for this frame
408
+ const frameLines = logoLines.map(line => [...line]);
409
+ // Add random sparkles
410
+ for (let s = 0; s < SPARKLES_PER_FRAME; s++) {
411
+ const lineIdx = Math.floor(Math.random() * frameLines.length);
412
+ const line = frameLines[lineIdx];
413
+ // Find positions with sparkle-able characters
414
+ const sparklePositions = [];
415
+ for (let i = 0; i < line.length; i++) {
416
+ if (sparkleChars.includes(line[i])) {
417
+ sparklePositions.push(i);
418
+ }
419
+ }
420
+ if (sparklePositions.length > 0) {
421
+ const pos = sparklePositions[Math.floor(Math.random() * sparklePositions.length)];
422
+ // Mark this position for sparkle (we'll handle coloring below)
423
+ frameLines[lineIdx][pos] = { char: line[pos], sparkle: true };
424
+ }
425
+ }
426
+ // Move cursor up and render frame
427
+ process.stdout.write('\x1B[6A');
428
+ for (const line of frameLines) {
429
+ let output = '';
430
+ for (const char of line) {
431
+ if (char && typeof char === 'object' && char.sparkle) {
432
+ const sparkleColor = sparkleColors[Math.floor(Math.random() * sparkleColors.length)];
433
+ output += sparkleColor(char.char);
434
+ }
435
+ else {
436
+ output += chalk_1.default.magenta(char);
437
+ }
438
+ }
439
+ console.log(output);
440
+ }
441
+ await sleep(SPARKLE_DELAY_MS);
442
+ }
443
+ // Final settle: clean magenta
444
+ process.stdout.write('\x1B[6A');
445
+ logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
446
+ console.log('');
447
+ console.log(chalk_1.default.cyan.bold(' Auto-Continue Wrapper'));
448
+ console.log(chalk_1.default.gray(' Monitors for context limit and auto-restores sessions'));
449
+ console.log(chalk_1.default.gray(' Uses full 200K context window - triggers on actual limit'));
450
+ if (bypass) {
451
+ console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
452
+ }
453
+ if (verbose) {
454
+ console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
455
+ console.log(chalk_1.default.gray(` ⏱ Timing: slash=${config.slashOpenDelayMs}ms, char=${config.charDelayMs}ms, clear=${config.clearWaitMs}ms`));
456
+ }
457
+ console.log('');
458
+ // Ensure .ekkos directory exists
459
+ (0, state_1.ensureEkkosDir)();
460
+ // Clear any stale flags
461
+ (0, state_1.clearAutoClearFlag)();
462
+ // Track state
463
+ let currentSession = options.session || (0, state_1.getCurrentSessionName)();
464
+ let isAutoClearInProgress = false;
465
+ let transcriptPath = null;
466
+ let currentSessionId = null;
467
+ // Output buffer for pattern detection
468
+ let outputBuffer = '';
469
+ // Debounce tracking to prevent double triggers
470
+ let lastDetectionTime = 0;
471
+ const DETECTION_COOLDOWN = 30000; // 30 seconds cooldown
472
+ // Resolve claude path
473
+ const rawClaudePath = resolveClaudePath();
474
+ // Handle npx:VERSION format for pinned version
475
+ const isNpxMode = rawClaudePath.startsWith('npx:');
476
+ const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
477
+ const claudePath = isNpxMode ? 'npx' : rawClaudePath;
478
+ if (verbose) {
479
+ if (isNpxMode) {
480
+ console.log(chalk_1.default.gray(` 🤖 Using claude-code@${pinnedVersion} via npx (pinned for better context)`));
481
+ }
482
+ else {
483
+ console.log(chalk_1.default.gray(` 🤖 Using claude at: ${claudePath}`));
484
+ }
485
+ if (currentSession) {
486
+ console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
487
+ }
488
+ }
489
+ // Build args - prepend package name if using npx
490
+ const args = [];
491
+ if (isNpxMode) {
492
+ args.push(`@anthropic-ai/claude-code@${pinnedVersion}`);
493
+ }
494
+ if (bypass) {
495
+ args.push('--dangerously-skip-permissions');
496
+ }
497
+ // Determine which mode to use
498
+ const usePty = pty !== null;
499
+ if (verbose) {
500
+ console.log(chalk_1.default.gray(` 💻 PTY mode: ${usePty ? 'node-pty' : 'spawn+script (fallback)'}`));
501
+ console.log('');
502
+ }
503
+ let shell;
504
+ if (usePty && pty) {
505
+ // Use node-pty for proper PTY control
506
+ try {
507
+ const ptyShell = pty.spawn(claudePath, args, {
508
+ name: 'xterm-256color',
509
+ cols: process.stdout.columns || 80,
510
+ rows: process.stdout.rows || 24,
511
+ cwd: process.cwd(),
512
+ env: process.env
513
+ });
514
+ shell = {
515
+ write: (data) => ptyShell.write(data),
516
+ kill: () => ptyShell.kill(),
517
+ resize: (cols, rows) => ptyShell.resize(cols, rows),
518
+ onData: (callback) => ptyShell.onData(callback),
519
+ onExit: (callback) => ptyShell.onExit(callback)
520
+ };
521
+ // Handle terminal resize
522
+ process.stdout.on('resize', () => {
523
+ shell.resize?.(process.stdout.columns || 80, process.stdout.rows || 24);
524
+ });
525
+ }
526
+ catch (err) {
527
+ console.log(chalk_1.default.yellow(`node-pty failed, falling back to spawn+script mode`));
528
+ if (verbose) {
529
+ console.log(chalk_1.default.gray(`Error: ${err.message}`));
530
+ }
531
+ // Fall through to spawn mode
532
+ return runWithSpawn(claudePath, args, options, {
533
+ currentSession,
534
+ isAutoClearInProgress,
535
+ transcriptPath,
536
+ currentSessionId,
537
+ outputBuffer
538
+ });
539
+ }
540
+ }
541
+ else {
542
+ // Fallback: use spawn+script for PTY emulation
543
+ return runWithSpawn(claudePath, args, options, {
544
+ currentSession,
545
+ isAutoClearInProgress,
546
+ transcriptPath,
547
+ currentSessionId,
548
+ outputBuffer
549
+ });
550
+ }
551
+ // Forward user input to PTY (named function so we can pause/resume)
552
+ const onStdinData = (data) => {
553
+ shell.write(data.toString());
554
+ };
555
+ if (process.stdin.isTTY) {
556
+ process.stdin.setRawMode(true);
557
+ }
558
+ process.stdin.resume();
559
+ process.stdin.on('data', onStdinData);
560
+ // Helper to get current output buffer (for readiness checks)
561
+ const getOutputBuffer = () => outputBuffer;
562
+ // Handle context wall detection
563
+ async function handleContextWall() {
564
+ // Debounce check - prevent double triggers (BEFORE pausing stdin)
565
+ const now = Date.now();
566
+ if (now - lastDetectionTime < DETECTION_COOLDOWN) {
567
+ dlog('Skipping - within debounce cooldown');
568
+ return;
569
+ }
570
+ if (isAutoClearInProgress) {
571
+ dlog('Already in progress, skipping');
572
+ return;
573
+ }
574
+ // CRITICAL: Capture session name IMMEDIATELY before any async operations
575
+ // This prevents shell.onData from overwriting it with the NEW session after /clear
576
+ let sessionToRestore = currentSession;
577
+ if (!sessionToRestore) {
578
+ const state = (0, state_1.getState)();
579
+ sessionToRestore = state?.sessionName || null;
580
+ const sessionId = state?.sessionId || null;
581
+ // If still no session name but we have an ID, generate it
582
+ if (!sessionToRestore && sessionId) {
583
+ sessionToRestore = (0, state_1.uuidToWords)(sessionId);
584
+ }
585
+ }
586
+ const sessionDisplay = sessionToRestore || 'unknown-session';
587
+ dlog(`Session to restore (captured before clear): ${sessionDisplay}`);
588
+ // CRITICAL: Clear buffer and set flags immediately
589
+ outputBuffer = '';
590
+ lastDetectionTime = now;
591
+ isAutoClearInProgress = true;
592
+ // PAUSE STDIN: Prevent user keystrokes from interleaving with injection
593
+ process.stdin.off('data', onStdinData);
594
+ dlog('Stdin paused during injection');
595
+ try {
596
+ // Log to FILE only (not terminal) during TUI operation
597
+ dlog('════════════════════════════════════════════════════════════');
598
+ dlog('CONTEXT LIMIT REACHED - AUTO-RESTORE');
599
+ dlog(`Session to restore: ${sessionDisplay}`);
600
+ dlog('════════════════════════════════════════════════════════════');
601
+ // Emergency capture (do this first before any UI manipulation)
602
+ dlog('Capturing current state...');
603
+ if (transcriptPath && currentSessionId) {
604
+ await emergencyCapture(transcriptPath, currentSessionId);
605
+ }
606
+ // READINESS GATE: Wait for idle prompt before injecting
607
+ dlog('Waiting for idle prompt...');
608
+ const readiness = await waitForIdlePrompt(getOutputBuffer, config);
609
+ if (readiness.interrupted) {
610
+ dlog('Interrupted state detected - aborting auto-restore');
611
+ dlog('User will need to manually /clear and /continue');
612
+ isAutoClearInProgress = false;
613
+ return;
614
+ }
615
+ // STEP 1: Run /clear command (with trailing space for clean completion)
616
+ dlog('Running /clear command...');
617
+ const clearResult = await runSlashCommand(shell, 'clear ', config, getOutputBuffer);
618
+ if (!clearResult.success) {
619
+ dlog(`/clear failed: ${clearResult.reason}`);
620
+ }
621
+ // STEP 2: Wait for /clear to complete
622
+ dlog(`Waiting ${config.clearWaitMs}ms for /clear to complete...`);
623
+ await sleep(config.clearWaitMs);
624
+ dlog('Wait complete.');
625
+ // READINESS GATE: Wait for idle prompt again before /continue
626
+ dlog('Waiting for idle prompt after /clear...');
627
+ const readiness2 = await waitForIdlePrompt(getOutputBuffer, config);
628
+ if (readiness2.interrupted) {
629
+ dlog('Interrupted state after /clear - aborting');
630
+ isAutoClearInProgress = false;
631
+ return;
632
+ }
633
+ // STEP 3: Run /continue with the ORIGINAL session name (captured before /clear)
634
+ dlog(`Running /continue ${sessionDisplay}...`);
635
+ const continueResult = await runSlashCommand(shell, 'continue', config, getOutputBuffer, sessionDisplay);
636
+ if (!continueResult.success) {
637
+ dlog(`/continue failed: ${continueResult.reason}`);
638
+ }
639
+ dlog('════════════════════════════════════════════════════════════');
640
+ dlog('AUTO-RESTORE COMPLETE');
641
+ dlog('════════════════════════════════════════════════════════════');
642
+ // Cooldown before allowing another trigger
643
+ setTimeout(() => {
644
+ isAutoClearInProgress = false;
645
+ }, DETECTION_COOLDOWN);
646
+ }
647
+ catch (error) {
648
+ dlog(`Error in handleContextWall: ${error.message}`);
649
+ dlog(`Stack: ${error.stack}`);
650
+ isAutoClearInProgress = false;
651
+ }
652
+ finally {
653
+ // RESUME STDIN: Always restore user input capability
654
+ process.stdin.on('data', onStdinData);
655
+ dlog('Stdin resumed');
656
+ }
657
+ }
658
+ // Monitor PTY output
659
+ shell.onData((data) => {
660
+ // Pass through to terminal
661
+ process.stdout.write(data);
662
+ // Accumulate for pattern matching
663
+ outputBuffer += data;
664
+ // Keep buffer manageable (last 5KB)
665
+ if (outputBuffer.length > 5000) {
666
+ outputBuffer = outputBuffer.slice(-2000);
667
+ }
668
+ // Try to extract transcript path from output (Claude shows it on startup)
669
+ const transcriptMatch = data.match(/transcript[_\s]?(?:path)?[:\s]+([^\s\n]+\.json)/i);
670
+ if (transcriptMatch) {
671
+ transcriptPath = transcriptMatch[1];
672
+ dlog(`Detected transcript: ${transcriptPath}`);
673
+ }
674
+ // Try to extract session ID from output
675
+ const sessionMatch = data.match(/session[_\s]?(?:id)?[:\s]+([a-f0-9-]{36})/i);
676
+ if (sessionMatch) {
677
+ currentSessionId = sessionMatch[1];
678
+ currentSession = (0, state_1.uuidToWords)(currentSessionId);
679
+ (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
680
+ dlog(`Session detected: ${currentSession}`);
681
+ }
682
+ // Check for context wall patterns (ANSI-stripped + regex for robustness)
683
+ if (!isAutoClearInProgress) {
684
+ const normalized = normalizeForMatch(outputBuffer);
685
+ if (CONTEXT_WALL_REGEX.test(normalized)) {
686
+ dlog('Context wall detected via regex');
687
+ handleContextWall().catch(err => {
688
+ dlog(`Error during auto-clear: ${err.message}`);
689
+ isAutoClearInProgress = false;
690
+ });
691
+ }
692
+ }
693
+ });
694
+ // Handle PTY exit
695
+ shell.onExit(({ exitCode }) => {
696
+ (0, state_1.clearAutoClearFlag)();
697
+ // Restore terminal
698
+ if (process.stdin.isTTY) {
699
+ process.stdin.setRawMode(false);
700
+ }
701
+ process.stdin.pause();
702
+ // Log exit to file (not terminal - TUI already gone at this point)
703
+ dlog(`Claude exited with code ${exitCode}`);
704
+ process.exit(exitCode);
705
+ });
706
+ // Cleanup on exit signals
707
+ const cleanup = () => {
708
+ (0, state_1.clearAutoClearFlag)();
709
+ if (process.stdin.isTTY) {
710
+ process.stdin.setRawMode(false);
711
+ }
712
+ process.stdin.pause();
713
+ shell.kill();
714
+ process.exit(0);
715
+ };
716
+ process.on('SIGINT', cleanup);
717
+ process.on('SIGTERM', cleanup);
718
+ }
719
+ /**
720
+ * Fallback implementation using spawn+script for PTY emulation
721
+ * Used when node-pty is not available (e.g., Node 24+)
722
+ */
723
+ async function runWithSpawn(claudePath, args, options, state) {
724
+ const verbose = options.verbose || false;
725
+ let { currentSession, isAutoClearInProgress, transcriptPath, currentSessionId, outputBuffer } = state;
726
+ // Debounce tracking
727
+ let lastDetectionTime = 0;
728
+ const DETECTION_COOLDOWN = 30000;
729
+ console.log(chalk_1.default.gray('Using spawn fallback mode'));
730
+ console.log(chalk_1.default.yellow('Note: Auto-inject requires manual /clear + /continue'));
731
+ console.log('');
732
+ let claude;
733
+ if (isWindows) {
734
+ // On Windows, spawn Claude directly (no script equivalent)
735
+ // PTY features may be limited
736
+ console.log(chalk_1.default.gray('Windows mode: running Claude directly'));
737
+ claude = (0, child_process_1.spawn)(claudePath, args, {
738
+ stdio: ['inherit', 'pipe', 'inherit'],
739
+ cwd: process.cwd(),
740
+ env: process.env,
741
+ shell: true
742
+ });
743
+ }
744
+ else {
745
+ // Use script command for PTY on Unix
746
+ const scriptCmd = process.platform === 'darwin'
747
+ ? ['script', '-q', '/dev/null', claudePath, ...args]
748
+ : ['script', '-q', '-c', `${claudePath} ${args.join(' ')}`, '/dev/null'];
749
+ claude = (0, child_process_1.spawn)(scriptCmd[0], scriptCmd.slice(1), {
750
+ stdio: ['inherit', 'pipe', 'inherit'],
751
+ cwd: process.cwd(),
752
+ env: process.env
753
+ });
754
+ }
755
+ // Ensure claude was spawned
756
+ if (!claude) {
757
+ console.error(chalk_1.default.red('Failed to spawn Claude'));
758
+ process.exit(1);
759
+ }
760
+ // For Windows direct spawn without piped stdout
761
+ if (isWindows && !claude.stdout) {
762
+ console.log(chalk_1.default.yellow('Running in basic mode - context detection disabled'));
763
+ claude.on('exit', (code) => {
764
+ (0, state_1.clearAutoClearFlag)();
765
+ process.exit(code || 0);
766
+ });
767
+ return;
768
+ }
769
+ // Monitor output
770
+ if (claude.stdout) {
771
+ claude.stdout.on('data', async (data) => {
772
+ const chunk = data.toString();
773
+ process.stdout.write(chunk);
774
+ outputBuffer += chunk;
775
+ if (outputBuffer.length > 5000) {
776
+ outputBuffer = outputBuffer.slice(-2000);
777
+ }
778
+ // Extract session info
779
+ const sessionMatch = chunk.match(/session[_\s]?(?:id)?[:\s]+([a-f0-9-]{36})/i);
780
+ if (sessionMatch) {
781
+ currentSessionId = sessionMatch[1];
782
+ currentSession = (0, state_1.uuidToWords)(currentSessionId);
783
+ (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
784
+ }
785
+ // Detect context wall - show manual instructions (ANSI-stripped + regex)
786
+ const now = Date.now();
787
+ if (!isAutoClearInProgress && (now - lastDetectionTime >= DETECTION_COOLDOWN)) {
788
+ const normalized = normalizeForMatch(outputBuffer);
789
+ if (CONTEXT_WALL_REGEX.test(normalized)) {
790
+ // CRITICAL: Clear buffer immediately
791
+ outputBuffer = '';
792
+ lastDetectionTime = now;
793
+ isAutoClearInProgress = true;
794
+ if (!currentSession) {
795
+ const savedState = (0, state_1.getState)();
796
+ currentSession = savedState?.sessionName || 'unknown-session';
797
+ }
798
+ // Log to file only - don't corrupt TUI
799
+ dlog('════════════════════════════════════════════════════════════');
800
+ dlog('CONTEXT LIMIT REACHED (spawn fallback mode)');
801
+ dlog(`Session: ${currentSession}`);
802
+ dlog('Manual restore required: /clear then /continue ' + currentSession);
803
+ dlog('════════════════════════════════════════════════════════════');
804
+ setTimeout(() => {
805
+ isAutoClearInProgress = false;
806
+ // Buffer already cleared at detection time
807
+ }, DETECTION_COOLDOWN);
808
+ }
809
+ }
810
+ });
811
+ }
812
+ claude.on('exit', (code) => {
813
+ (0, state_1.clearAutoClearFlag)();
814
+ dlog(`Claude exited with code ${code}`);
815
+ process.exit(code || 0);
816
+ });
817
+ claude.on('error', (err) => {
818
+ dlog(`Error: ${err.message}`);
819
+ process.exit(1);
820
+ });
821
+ // Cleanup
822
+ const cleanup = () => {
823
+ (0, state_1.clearAutoClearFlag)();
824
+ claude.kill();
825
+ process.exit(0);
826
+ };
827
+ process.on('SIGINT', cleanup);
828
+ process.on('SIGTERM', cleanup);
829
+ }