@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.
- package/dist/cache/LocalSessionStore.d.ts +129 -0
- package/dist/cache/LocalSessionStore.js +688 -0
- package/dist/cache/capture.d.ts +26 -0
- package/dist/cache/capture.js +461 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.js +23 -0
- package/dist/cache/types.d.ts +147 -0
- package/dist/cache/types.js +40 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +478 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +829 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +658 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +109 -0
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +157 -0
- package/dist/deploy/agents.d.ts +15 -0
- package/dist/deploy/agents.js +72 -0
- package/dist/deploy/hooks.d.ts +16 -0
- package/dist/deploy/hooks.js +121 -0
- package/dist/deploy/index.d.ts +7 -0
- package/dist/deploy/index.js +24 -0
- package/dist/deploy/instructions.d.ts +12 -0
- package/dist/deploy/instructions.js +36 -0
- package/dist/deploy/mcp.d.ts +19 -0
- package/dist/deploy/mcp.js +109 -0
- package/dist/deploy/plugins.d.ts +19 -0
- package/dist/deploy/plugins.js +62 -0
- package/dist/deploy/settings.d.ts +8 -0
- package/dist/deploy/settings.js +84 -0
- package/dist/deploy/skills.d.ts +19 -0
- package/dist/deploy/skills.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +71 -0
- package/dist/restore/RestoreOrchestrator.d.ts +48 -0
- package/dist/restore/RestoreOrchestrator.js +481 -0
- package/dist/restore/index.d.ts +4 -0
- package/dist/restore/index.js +20 -0
- package/dist/utils/platform.d.ts +29 -0
- package/dist/utils/platform.js +65 -0
- package/dist/utils/session-words.json +119 -0
- package/dist/utils/state.d.ts +57 -0
- package/dist/utils/state.js +186 -0
- package/dist/utils/templates.d.ts +24 -0
- package/dist/utils/templates.js +118 -0
- package/package.json +48 -0
- package/templates/CLAUDE.md +287 -0
- package/templates/README.md +378 -0
- package/templates/agents/README.md +182 -0
- package/templates/agents/code-reviewer.md +166 -0
- package/templates/agents/debug-detective.md +169 -0
- package/templates/agents/ekkOS_Vercel.md +99 -0
- package/templates/agents/extension-manager.md +229 -0
- package/templates/agents/git-companion.md +185 -0
- package/templates/agents/github-test-agent.md +321 -0
- package/templates/agents/railway-manager.md +179 -0
- package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
- package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
- package/templates/claude-plugins/README.md +587 -0
- package/templates/claude-plugins/agents/code-reviewer.json +14 -0
- package/templates/claude-plugins/agents/debug-detective.json +15 -0
- package/templates/claude-plugins/agents/git-companion.json +14 -0
- package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
- package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
- package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
- package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
- package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
- package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
- package/templates/claude-plugins-admin/README.md +446 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
- package/templates/commands/continue.md +47 -0
- package/templates/cursor-hooks/after-agent-response.sh +117 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
- package/templates/cursor-hooks/hooks.json +20 -0
- package/templates/cursor-hooks/lib/contract.sh +320 -0
- package/templates/cursor-hooks/stop.sh +75 -0
- package/templates/cursor-rules/ekkos-memory.md +187 -0
- package/templates/hooks/assistant-response.sh +96 -0
- package/templates/hooks/hooks.json +28 -0
- package/templates/hooks/lib/contract.sh +320 -0
- package/templates/hooks/lib/state.sh +158 -0
- package/templates/hooks/session-start.ps1 +41 -0
- package/templates/hooks/session-start.sh +318 -0
- package/templates/hooks/stop.ps1 +16 -0
- package/templates/hooks/stop.sh +989 -0
- package/templates/hooks/user-prompt-submit.ps1 +174 -0
- package/templates/hooks/user-prompt-submit.sh +587 -0
- package/templates/hooks-node/lib/state.js +187 -0
- package/templates/hooks-node/stop.js +416 -0
- package/templates/hooks-node/user-prompt-submit.js +337 -0
- package/templates/plan-template.md +306 -0
- package/templates/rules/00-hooks-contract.mdc +89 -0
- package/templates/rules/30-ekkos-core.mdc +188 -0
- package/templates/rules/31-ekkos-messages.mdc +78 -0
- package/templates/skills/continue/SKILL.md +169 -0
- package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
- package/templates/skills/ekkOS_Learn/Skill.md +265 -0
- package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
- package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
- package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
- package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
- package/templates/skills/ekkOS_Safety/Skill.md +265 -0
- package/templates/skills/ekkOS_Schema/Skill.md +251 -0
- package/templates/skills/ekkOS_Summary/Skill.md +257 -0
- package/templates/skills/ekkOS_Vault/Skill.md +287 -0
- package/templates/skills/permissions/Skill.md +322 -0
- package/templates/spec-template.md +159 -0
- package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
- package/templates/windsurf-hooks/hooks.json +10 -0
- package/templates/windsurf-hooks/lib/contract.sh +320 -0
- 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
|
+
}
|