@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.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/LICENSE +0 -21
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
package/dist/commands/run.js
CHANGED
|
@@ -76,50 +76,63 @@ function findCcdnaPath() {
|
|
|
76
76
|
* @param claudePath - Path to Claude Code to patch (if different from default)
|
|
77
77
|
*/
|
|
78
78
|
function applyCcdnaPatches(verbose, claudePath) {
|
|
79
|
+
// DISABLED: ccDNA patching is currently corrupting cli.js (JSON parse error at position 7945)
|
|
80
|
+
// See: https://github.com/anthropics/ekkos/issues/2856
|
|
81
|
+
// The patching process is injecting code that breaks the minified cli.js
|
|
82
|
+
// Temporarily disabled until ccDNA is fixed upstream
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(chalk_1.default.gray(' ccDNA patching disabled (see issue #2856)'));
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
// Original implementation (disabled):
|
|
88
|
+
/*
|
|
79
89
|
const ccdnaPath = findCcdnaPath();
|
|
80
90
|
if (!ccdnaPath) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
if (verbose) {
|
|
92
|
+
console.log(chalk.gray(' ccDNA not found - skipping patches'));
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
85
95
|
}
|
|
96
|
+
|
|
86
97
|
// Read ccDNA version from package.json FIRST
|
|
87
98
|
let ccdnaVersion = 'unknown';
|
|
88
99
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const pkgPath = path.join(path.dirname(ccdnaPath), '..', 'package.json');
|
|
101
|
+
if (fs.existsSync(pkgPath)) {
|
|
102
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
103
|
+
ccdnaVersion = pkg.version || 'unknown';
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore version detection errors
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
// Set env var to tell ccDNA which Claude to patch
|
|
111
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
112
|
+
const env = { ...process.env };
|
|
113
|
+
if (claudePath) {
|
|
114
|
+
// ccDNA checks CCDNA_CC_INSTALLATION_PATH to override default detection
|
|
115
|
+
env.CCDNA_CC_INSTALLATION_PATH = claudePath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Run ccDNA in apply mode (non-interactive)
|
|
119
|
+
execSync(`node "${ccdnaPath}" -a`, {
|
|
120
|
+
stdio: verbose ? 'inherit' : 'pipe',
|
|
121
|
+
timeout: 30000, // 30 second timeout
|
|
122
|
+
env,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (verbose) {
|
|
126
|
+
console.log(chalk.green(` ✓ ccDNA v${ccdnaVersion} patches applied`));
|
|
127
|
+
}
|
|
128
|
+
return ccdnaVersion;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (verbose) {
|
|
131
|
+
console.log(chalk.yellow(` ⚠ ccDNA patch failed: ${(err as Error).message}`));
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
*/
|
|
123
136
|
}
|
|
124
137
|
/**
|
|
125
138
|
* Restore original Claude Code (remove ccDNA patches) on exit
|
|
@@ -164,20 +177,27 @@ function restoreCcdnaPatches(verbose, claudePath) {
|
|
|
164
177
|
}
|
|
165
178
|
}
|
|
166
179
|
const state_1 = require("../utils/state");
|
|
180
|
+
const session_binding_1 = require("../utils/session-binding");
|
|
167
181
|
const doctor_1 = require("./doctor");
|
|
168
182
|
const stream_tailer_1 = require("../capture/stream-tailer");
|
|
169
183
|
const jsonl_rewriter_1 = require("../capture/jsonl-rewriter");
|
|
170
184
|
const transcript_repair_1 = require("../capture/transcript-repair");
|
|
171
185
|
// Try to load node-pty (may fail on Node 24+)
|
|
186
|
+
// IMPORTANT: This must be awaited in run() to avoid racey false fallbacks.
|
|
172
187
|
let pty = null;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
let ptyLoadPromise = null;
|
|
189
|
+
async function loadPty() {
|
|
190
|
+
if (pty)
|
|
191
|
+
return pty;
|
|
192
|
+
if (!ptyLoadPromise) {
|
|
193
|
+
ptyLoadPromise = Promise.resolve().then(() => __importStar(require('node-pty'))).then(mod => {
|
|
194
|
+
pty = mod;
|
|
195
|
+
return mod;
|
|
196
|
+
})
|
|
197
|
+
.catch(() => null);
|
|
198
|
+
}
|
|
199
|
+
return ptyLoadPromise;
|
|
200
|
+
}
|
|
181
201
|
function getConfig(options) {
|
|
182
202
|
/* eslint-disable no-restricted-syntax -- Config timing values, not API keys */
|
|
183
203
|
return {
|
|
@@ -230,9 +250,9 @@ const PALETTE_INDICATOR_REGEX = /\/(clear|continue|compact|help|bug|config)/i;
|
|
|
230
250
|
// SESSION NAME DETECTION (3-word slug: word-word-word)
|
|
231
251
|
// Claude prints session name in footer: · Turn N · groovy-koala-saves · 📅
|
|
232
252
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
-
// Strong signal:
|
|
234
|
-
//
|
|
235
|
-
const SESSION_NAME_IN_STATUS_REGEX =
|
|
253
|
+
// Strong signal: explicit turn footer emitted by Claude/ekkOS status line.
|
|
254
|
+
// Requires "Turn <n> · <session> ·" to avoid matching arbitrary slug text.
|
|
255
|
+
const SESSION_NAME_IN_STATUS_REGEX = /turn\s+\d+\s*·\s*([a-z]+-[a-z]+-[a-z]+)\s*·/i;
|
|
236
256
|
// Weaker signal: any 3-word slug (word-word-word pattern)
|
|
237
257
|
const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
|
|
238
258
|
// Orphan tool_result marker emitted by ccDNA validate mode
|
|
@@ -413,7 +433,7 @@ const isWindows = os.platform() === 'win32';
|
|
|
413
433
|
// 'latest' = use latest version, or specify like '2.1.33' for specific version
|
|
414
434
|
// Core ekkOS patches (eviction, context management) work with all recent versions
|
|
415
435
|
// Cosmetic patches may fail on newer versions but don't affect functionality
|
|
416
|
-
const PINNED_CLAUDE_VERSION = '2.1.
|
|
436
|
+
const PINNED_CLAUDE_VERSION = '2.1.45';
|
|
417
437
|
// Max output tokens for Claude responses
|
|
418
438
|
// Default: 16384 (safe for Sonnet 4.5)
|
|
419
439
|
// Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
|
|
@@ -421,7 +441,7 @@ const PINNED_CLAUDE_VERSION = '2.1.33';
|
|
|
421
441
|
const EKKOS_MAX_OUTPUT_TOKENS = process.env.EKKOS_MAX_OUTPUT_TOKENS || '16384';
|
|
422
442
|
// Default proxy URL for context eviction
|
|
423
443
|
// eslint-disable-next-line no-restricted-syntax -- Config URL, not API key
|
|
424
|
-
const EKKOS_PROXY_URL = process.env.EKKOS_PROXY_URL || 'https://
|
|
444
|
+
const EKKOS_PROXY_URL = process.env.EKKOS_PROXY_URL || 'https://proxy.ekkos.dev';
|
|
425
445
|
// Track proxy mode for getEkkosEnv (set by run() based on options)
|
|
426
446
|
let proxyModeEnabled = true;
|
|
427
447
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -450,7 +470,7 @@ function getEkkosEnv() {
|
|
|
450
470
|
/* eslint-disable no-restricted-syntax -- System env spreading, not API key access */
|
|
451
471
|
const env = {
|
|
452
472
|
...process.env,
|
|
453
|
-
|
|
473
|
+
// Let Claude Code use its own default max_tokens (don't override)
|
|
454
474
|
};
|
|
455
475
|
/* eslint-enable no-restricted-syntax */
|
|
456
476
|
// Check if proxy is disabled via env var or options
|
|
@@ -464,10 +484,14 @@ function getEkkosEnv() {
|
|
|
464
484
|
// This fixes the mismatch where CLI generated one name but Claude Code used another
|
|
465
485
|
// The hook calls POST /proxy/session/bind with Claude's actual session name
|
|
466
486
|
if (!cliSessionName) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
487
|
+
const pendingSeed = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
488
|
+
cliSessionName = `_pending-${pendingSeed}`; // Unique placeholder per CLI run
|
|
489
|
+
cliSessionId = `pending-${pendingSeed}`;
|
|
490
|
+
console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName} (will bind to Claude session)`));
|
|
491
|
+
}
|
|
492
|
+
env.EKKOS_PENDING_SESSION = cliSessionName;
|
|
493
|
+
if (cliSessionId)
|
|
494
|
+
env.EKKOS_PENDING_SESSION_ID = cliSessionId;
|
|
471
495
|
// Get full userId from config (NOT the truncated version from auth token)
|
|
472
496
|
// Config has full UUID like "d4532ba0-0a86-42ce-bab4-22aa62b55ce6"
|
|
473
497
|
// This matches the turns/ R2 structure: turns/{fullUserId}/{sessionName}/
|
|
@@ -492,7 +516,7 @@ function getEkkosEnv() {
|
|
|
492
516
|
const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
|
|
493
517
|
const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}`;
|
|
494
518
|
env.ANTHROPIC_BASE_URL = proxyUrl;
|
|
495
|
-
|
|
519
|
+
// Proxy URL contains userId + project path — don't leak to terminal
|
|
496
520
|
}
|
|
497
521
|
else {
|
|
498
522
|
env.EKKOS_PROXY_MODE = '0';
|
|
@@ -783,6 +807,85 @@ function cleanupInstanceFile(instanceId) {
|
|
|
783
807
|
// Ignore cleanup errors
|
|
784
808
|
}
|
|
785
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Launch ekkos run + dashboard in isolated tmux panes (60/40 split)
|
|
812
|
+
*/
|
|
813
|
+
function launchWithDashboard(options) {
|
|
814
|
+
const tmuxSession = `ekkos-${Date.now().toString(36)}`;
|
|
815
|
+
const launchTime = Date.now();
|
|
816
|
+
// Build the ekkos run command WITHOUT --dashboard (prevent recursion)
|
|
817
|
+
const runArgs = ['run'];
|
|
818
|
+
if (options.session)
|
|
819
|
+
runArgs.push('-s', options.session);
|
|
820
|
+
if (options.bypass)
|
|
821
|
+
runArgs.push('-b');
|
|
822
|
+
if (options.verbose)
|
|
823
|
+
runArgs.push('-v');
|
|
824
|
+
if (options.doctor)
|
|
825
|
+
runArgs.push('-d');
|
|
826
|
+
if (options.research)
|
|
827
|
+
runArgs.push('-r');
|
|
828
|
+
if (options.noInject)
|
|
829
|
+
runArgs.push('--skip-inject');
|
|
830
|
+
if (options.noDna)
|
|
831
|
+
runArgs.push('--skip-dna');
|
|
832
|
+
if (options.noProxy)
|
|
833
|
+
runArgs.push('--skip-proxy');
|
|
834
|
+
runArgs.push('--kickstart'); // Auto-send "test" to create session immediately for dashboard
|
|
835
|
+
const ekkosCmd = process.argv[1]; // Path to ekkos CLI
|
|
836
|
+
const cwd = process.cwd();
|
|
837
|
+
const termCols = process.stdout.columns ?? 160;
|
|
838
|
+
const termRows = process.stdout.rows ?? 48;
|
|
839
|
+
// Write a marker file with launch timestamp + CWD so dashboard knows to wait for NEW session
|
|
840
|
+
const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
|
|
841
|
+
try {
|
|
842
|
+
fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
|
|
843
|
+
}
|
|
844
|
+
catch { }
|
|
845
|
+
const runCommand = `node "${ekkosCmd}" ${runArgs.join(' ')}`;
|
|
846
|
+
// Use --wait-for-new flag to wait for a session that started AFTER this launch
|
|
847
|
+
const dashCommand = `node "${ekkosCmd}" dashboard --wait-for-new --refresh 2000`;
|
|
848
|
+
try {
|
|
849
|
+
// Pane 0 (left): start with inert command (no interactive shell startup noise).
|
|
850
|
+
// Claude is launched AFTER split so Ink gets final pane geometry at startup.
|
|
851
|
+
(0, child_process_1.execSync)(`tmux new-session -d -s "${tmuxSession}" -x ${termCols} -y ${termRows} -n "claude" 'sleep 86400'`, { stdio: 'pipe' });
|
|
852
|
+
const applyTmuxOpt = (cmd) => {
|
|
853
|
+
try {
|
|
854
|
+
(0, child_process_1.execSync)(`tmux ${cmd}`, { stdio: 'pipe' });
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
if (options.verbose) {
|
|
858
|
+
console.log(chalk_1.default.gray(` tmux option skipped: ${cmd} (${err.message})`));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
// Session/window isolation and quality-of-life settings
|
|
863
|
+
applyTmuxOpt(`set-option -t "${tmuxSession}" mouse on`);
|
|
864
|
+
applyTmuxOpt(`set-window-option -t "${tmuxSession}" history-limit 100000`);
|
|
865
|
+
applyTmuxOpt(`set-window-option -t "${tmuxSession}" mode-keys vi`);
|
|
866
|
+
applyTmuxOpt(`set-window-option -t "${tmuxSession}:claude" synchronize-panes off`);
|
|
867
|
+
// Keep pane geometry synced to the active client so vertical window resizing
|
|
868
|
+
// immediately gives dashboard/table more rows.
|
|
869
|
+
applyTmuxOpt(`set-window-option -t "${tmuxSession}:claude" window-size latest`);
|
|
870
|
+
applyTmuxOpt(`set-window-option -t "${tmuxSession}:claude" aggressive-resize on`);
|
|
871
|
+
applyTmuxOpt(`set-option -t "${tmuxSession}" remain-on-exit off`);
|
|
872
|
+
applyTmuxOpt(`set-option -t "${tmuxSession}" escape-time 0`);
|
|
873
|
+
// Pane 1 (right): dashboard at 40% width
|
|
874
|
+
(0, child_process_1.execSync)(`tmux split-window -t "${tmuxSession}:claude" -h -p 40 -c "${cwd}" '${dashCommand}'`, { stdio: 'pipe' });
|
|
875
|
+
// Launch Claude in left pane AFTER split to avoid initial Ink width mis-detection
|
|
876
|
+
(0, child_process_1.execSync)(`tmux respawn-pane -k -t "${tmuxSession}:claude.0" '${runCommand}'`, { stdio: 'pipe' });
|
|
877
|
+
// Keep focus on left pane so Claude retains immediate keyboard/mouse interaction
|
|
878
|
+
(0, child_process_1.execSync)(`tmux select-pane -t "${tmuxSession}:claude.0"`, { stdio: 'pipe' });
|
|
879
|
+
console.log(chalk_1.default.cyan('\n Dashboard launched in right pane (40%)'));
|
|
880
|
+
console.log(chalk_1.default.gray(' Pane switch: Ctrl+B then o'));
|
|
881
|
+
// Attach to tmux session
|
|
882
|
+
(0, child_process_1.execSync)(`tmux attach -t "${tmuxSession}"`, { stdio: 'inherit' });
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
console.log(chalk_1.default.red(`tmux error: ${err.message}`));
|
|
886
|
+
console.log(chalk_1.default.gray('Falling back to normal mode. Run "ekkos dashboard --latest" in another terminal.'));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
786
889
|
async function run(options) {
|
|
787
890
|
const verbose = options.verbose || false;
|
|
788
891
|
const bypass = options.bypass || false;
|
|
@@ -795,6 +898,23 @@ async function run(options) {
|
|
|
795
898
|
else if (verbose) {
|
|
796
899
|
console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
|
|
797
900
|
}
|
|
901
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
902
|
+
// DASHBOARD MODE: Launch via tmux with isolated dashboard pane (60/40)
|
|
903
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
904
|
+
if (options.dashboard) {
|
|
905
|
+
try {
|
|
906
|
+
const tmuxPath = (0, child_process_1.execSync)('which tmux', { encoding: 'utf-8' }).trim();
|
|
907
|
+
if (tmuxPath) {
|
|
908
|
+
launchWithDashboard(options);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
console.log(chalk_1.default.yellow(' tmux not found. Install: brew install tmux'));
|
|
914
|
+
console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
|
|
915
|
+
console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
798
918
|
// Generate instance ID for this run
|
|
799
919
|
const instanceId = generateInstanceId();
|
|
800
920
|
// eslint-disable-next-line no-restricted-syntax -- Instance tracking, not API key
|
|
@@ -875,8 +995,14 @@ async function run(options) {
|
|
|
875
995
|
if (bypass) {
|
|
876
996
|
earlyArgs.push('--dangerously-skip-permissions');
|
|
877
997
|
}
|
|
878
|
-
|
|
879
|
-
|
|
998
|
+
if (options.addDirs && options.addDirs.length > 0) {
|
|
999
|
+
for (const dir of options.addDirs) {
|
|
1000
|
+
earlyArgs.push('--add-dir', dir);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Check PTY availability early (deterministic, no async race)
|
|
1004
|
+
const loadedPty = await loadPty();
|
|
1005
|
+
const usePty = loadedPty !== null;
|
|
880
1006
|
// ══════════════════════════════════════════════════════════════════════════
|
|
881
1007
|
// CONCURRENT STARTUP: Spawn Claude while animation runs
|
|
882
1008
|
// Buffer output until animation completes, then flush
|
|
@@ -925,187 +1051,205 @@ async function run(options) {
|
|
|
925
1051
|
// ══════════════════════════════════════════════════════════════════════════
|
|
926
1052
|
// STARTUP BANNER WITH COLOR PULSE ANIMATION
|
|
927
1053
|
// ══════════════════════════════════════════════════════════════════════════
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
// Final frame: settle on magenta
|
|
962
|
-
process.stdout.write('\x1B[6A');
|
|
963
|
-
logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
|
|
964
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
965
|
-
// SPARKLE EFFECT - Random characters flash white/cyan
|
|
966
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
967
|
-
const sparkleChars = ['▄', '█', '▀'];
|
|
968
|
-
const sparkleColors = [chalk_1.default.white, chalk_1.default.whiteBright, chalk_1.default.cyanBright, chalk_1.default.yellowBright];
|
|
969
|
-
const SPARKLE_FRAMES = 40; // ~3.2 seconds of sparkles
|
|
970
|
-
const SPARKLE_DELAY_MS = 80;
|
|
971
|
-
const SPARKLES_PER_FRAME = 3;
|
|
972
|
-
for (let frame = 0; frame < SPARKLE_FRAMES; frame++) {
|
|
973
|
-
// Create a copy of logo lines for this frame
|
|
974
|
-
const frameLines = logoLines.map(line => [...line]);
|
|
975
|
-
// Add random sparkles
|
|
976
|
-
for (let s = 0; s < SPARKLES_PER_FRAME; s++) {
|
|
977
|
-
const lineIdx = Math.floor(Math.random() * frameLines.length);
|
|
978
|
-
const line = frameLines[lineIdx];
|
|
979
|
-
// Find positions with sparkle-able characters
|
|
980
|
-
const sparklePositions = [];
|
|
981
|
-
for (let i = 0; i < line.length; i++) {
|
|
982
|
-
const charItem = line[i];
|
|
983
|
-
if (typeof charItem === 'string' && sparkleChars.includes(charItem)) {
|
|
984
|
-
sparklePositions.push(i);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
if (sparklePositions.length > 0) {
|
|
988
|
-
const pos = sparklePositions[Math.floor(Math.random() * sparklePositions.length)];
|
|
989
|
-
const charAtPos = line[pos];
|
|
990
|
-
// Mark this position for sparkle (we'll handle coloring below)
|
|
991
|
-
if (typeof charAtPos === 'string') {
|
|
992
|
-
frameLines[lineIdx][pos] = { char: charAtPos, sparkle: true };
|
|
993
|
-
}
|
|
1054
|
+
const skipFancyIntro = process.env.EKKOS_REMOTE_SESSION === '1' || process.env.EKKOS_NO_SPLASH === '1';
|
|
1055
|
+
if (!skipFancyIntro) {
|
|
1056
|
+
const logoLines = [
|
|
1057
|
+
' ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ▄▄',
|
|
1058
|
+
' ▄▄ ▄▄ ▄███████▄ █████▀▀▀ █ █ ▀ █',
|
|
1059
|
+
'▄█▀█▄ ██ ▄█▀ ██ ▄█▀ ███ ███ ▀████▄',
|
|
1060
|
+
'██▄█▀ ████ ████ ███▄▄▄███ ▀████',
|
|
1061
|
+
'▀█▄▄▄ ██ ▀█▄ ██ ▀█▄ ▀█████▀ ███████▀',
|
|
1062
|
+
' ▄▄▄▄▄▄▄▄'
|
|
1063
|
+
];
|
|
1064
|
+
// Color pulse sequence (magenta → cyan → blue → magenta cycle)
|
|
1065
|
+
const pulseColors = [
|
|
1066
|
+
chalk_1.default.magenta,
|
|
1067
|
+
chalk_1.default.hex('#FF69B4'), // Hot pink
|
|
1068
|
+
chalk_1.default.cyan,
|
|
1069
|
+
chalk_1.default.hex('#00CED1'), // Dark cyan
|
|
1070
|
+
chalk_1.default.blue,
|
|
1071
|
+
chalk_1.default.hex('#8A2BE2'), // Blue violet
|
|
1072
|
+
chalk_1.default.magenta
|
|
1073
|
+
];
|
|
1074
|
+
// Print initial logo
|
|
1075
|
+
console.log('');
|
|
1076
|
+
logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
|
|
1077
|
+
// Animate: pulse through colors
|
|
1078
|
+
const PULSE_CYCLES = 2;
|
|
1079
|
+
const FRAME_DELAY_MS = 80;
|
|
1080
|
+
for (let cycle = 0; cycle < PULSE_CYCLES; cycle++) {
|
|
1081
|
+
for (const colorFn of pulseColors) {
|
|
1082
|
+
// Move cursor up to overwrite logo (6 lines)
|
|
1083
|
+
process.stdout.write('\x1B[6A');
|
|
1084
|
+
// Reprint with new color
|
|
1085
|
+
logoLines.forEach(line => console.log(colorFn(line)));
|
|
1086
|
+
await sleep(FRAME_DELAY_MS);
|
|
994
1087
|
}
|
|
995
1088
|
}
|
|
996
|
-
//
|
|
1089
|
+
// Final frame: settle on magenta
|
|
997
1090
|
process.stdout.write('\x1B[6A');
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1091
|
+
logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
|
|
1092
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1093
|
+
// SPARKLE EFFECT - Random characters flash white/cyan
|
|
1094
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1095
|
+
const sparkleChars = ['▄', '█', '▀'];
|
|
1096
|
+
const sparkleColors = [chalk_1.default.white, chalk_1.default.whiteBright, chalk_1.default.cyanBright, chalk_1.default.yellowBright];
|
|
1097
|
+
const SPARKLE_FRAMES = 40; // ~3.2 seconds of sparkles
|
|
1098
|
+
const SPARKLE_DELAY_MS = 80;
|
|
1099
|
+
const SPARKLES_PER_FRAME = 3;
|
|
1100
|
+
for (let frame = 0; frame < SPARKLE_FRAMES; frame++) {
|
|
1101
|
+
// Track sparkle positions for this frame (lineIdx -> Set<charIdx>)
|
|
1102
|
+
const sparkleMap = new Map();
|
|
1103
|
+
// Pick random sparkle positions
|
|
1104
|
+
for (let s = 0; s < SPARKLES_PER_FRAME; s++) {
|
|
1105
|
+
const lineIdx = Math.floor(Math.random() * logoLines.length);
|
|
1106
|
+
const line = logoLines[lineIdx];
|
|
1107
|
+
// Find sparkle-able character positions in this line
|
|
1108
|
+
const sparklePositions = [];
|
|
1109
|
+
for (let i = 0; i < line.length; i++) {
|
|
1110
|
+
if (sparkleChars.includes(line[i])) {
|
|
1111
|
+
sparklePositions.push(i);
|
|
1112
|
+
}
|
|
1004
1113
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1114
|
+
if (sparklePositions.length > 0) {
|
|
1115
|
+
const pos = sparklePositions[Math.floor(Math.random() * sparklePositions.length)];
|
|
1116
|
+
if (!sparkleMap.has(lineIdx)) {
|
|
1117
|
+
sparkleMap.set(lineIdx, new Set());
|
|
1118
|
+
}
|
|
1119
|
+
sparkleMap.get(lineIdx).add(pos);
|
|
1007
1120
|
}
|
|
1008
1121
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
// Phase 1: Typewriter effect for title
|
|
1025
|
-
process.stdout.write(' ');
|
|
1026
|
-
for (let i = 0; i < titleText.length; i++) {
|
|
1027
|
-
const char = titleText[i];
|
|
1028
|
-
// Flash white then settle to orange
|
|
1029
|
-
process.stdout.write(whiteShine(char));
|
|
1030
|
-
await sleep(25);
|
|
1031
|
-
process.stdout.write('\b' + chalk_1.default.hex('#FF6B35').bold(char));
|
|
1032
|
-
}
|
|
1033
|
-
console.log('');
|
|
1034
|
-
// Phase 2: Shine sweep across title (3 passes)
|
|
1035
|
-
const SHINE_PASSES = 3;
|
|
1036
|
-
const SHINE_WIDTH = 4;
|
|
1037
|
-
for (let pass = 0; pass < SHINE_PASSES; pass++) {
|
|
1038
|
-
for (let shinePos = -SHINE_WIDTH; shinePos <= titleText.length + SHINE_WIDTH; shinePos++) {
|
|
1039
|
-
process.stdout.write('\x1B[1A'); // Move up one line
|
|
1040
|
-
process.stdout.write('\r '); // Return to start
|
|
1041
|
-
let output = '';
|
|
1042
|
-
for (let i = 0; i < titleText.length; i++) {
|
|
1043
|
-
const distFromShine = Math.abs(i - shinePos);
|
|
1044
|
-
if (distFromShine === 0) {
|
|
1045
|
-
output += whiteShine.bold(titleText[i]);
|
|
1046
|
-
}
|
|
1047
|
-
else if (distFromShine === 1) {
|
|
1048
|
-
output += chalk_1.default.hex('#FFFFFF')(titleText[i]);
|
|
1049
|
-
}
|
|
1050
|
-
else if (distFromShine === 2) {
|
|
1051
|
-
output += chalk_1.default.hex('#FFD700')(titleText[i]);
|
|
1052
|
-
}
|
|
1053
|
-
else if (distFromShine === 3) {
|
|
1054
|
-
output += chalk_1.default.hex('#FFA500')(titleText[i]);
|
|
1122
|
+
// Render frame with sparkles
|
|
1123
|
+
process.stdout.write('\x1B[6A'); // Move cursor up 6 lines
|
|
1124
|
+
for (let lineIdx = 0; lineIdx < logoLines.length; lineIdx++) {
|
|
1125
|
+
const line = logoLines[lineIdx];
|
|
1126
|
+
const sparkles = sparkleMap.get(lineIdx);
|
|
1127
|
+
let output = '';
|
|
1128
|
+
for (let charIdx = 0; charIdx < line.length; charIdx++) {
|
|
1129
|
+
const char = line[charIdx];
|
|
1130
|
+
if (sparkles && sparkles.has(charIdx)) {
|
|
1131
|
+
const sparkleColor = sparkleColors[Math.floor(Math.random() * sparkleColors.length)];
|
|
1132
|
+
output += sparkleColor(char);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
output += chalk_1.default.magenta(char);
|
|
1136
|
+
}
|
|
1055
1137
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1138
|
+
console.log(output);
|
|
1139
|
+
}
|
|
1140
|
+
await sleep(SPARKLE_DELAY_MS);
|
|
1141
|
+
}
|
|
1142
|
+
// Final settle: clean magenta
|
|
1143
|
+
process.stdout.write('\x1B[6A');
|
|
1144
|
+
logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
|
|
1145
|
+
console.log('');
|
|
1146
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1147
|
+
// ANIMATED TITLE: "Cognitive Continuity Engine" with orange/white shine
|
|
1148
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
1149
|
+
const titleText = 'Cognitive Continuity Engine';
|
|
1150
|
+
const taglineText = 'Context is finite. Intelligence isn\'t.';
|
|
1151
|
+
// Color palette for shine effect
|
|
1152
|
+
const whiteShine = chalk_1.default.whiteBright;
|
|
1153
|
+
// Phase 1: Typewriter effect for title
|
|
1154
|
+
process.stdout.write(' ');
|
|
1155
|
+
for (let i = 0; i < titleText.length; i++) {
|
|
1156
|
+
const char = titleText[i];
|
|
1157
|
+
// Flash white then settle to orange
|
|
1158
|
+
process.stdout.write(whiteShine(char));
|
|
1159
|
+
await sleep(25);
|
|
1160
|
+
process.stdout.write('\b' + chalk_1.default.hex('#FF6B35').bold(char));
|
|
1161
|
+
}
|
|
1162
|
+
console.log('');
|
|
1163
|
+
// Phase 2: Shine sweep across title (3 passes)
|
|
1164
|
+
const SHINE_PASSES = 3;
|
|
1165
|
+
const SHINE_WIDTH = 4;
|
|
1166
|
+
for (let pass = 0; pass < SHINE_PASSES; pass++) {
|
|
1167
|
+
for (let shinePos = -SHINE_WIDTH; shinePos <= titleText.length + SHINE_WIDTH; shinePos++) {
|
|
1168
|
+
process.stdout.write('\x1B[1A'); // Move up one line
|
|
1169
|
+
process.stdout.write('\r '); // Return to start
|
|
1170
|
+
let output = '';
|
|
1171
|
+
for (let i = 0; i < titleText.length; i++) {
|
|
1172
|
+
const distFromShine = Math.abs(i - shinePos);
|
|
1173
|
+
if (distFromShine === 0) {
|
|
1174
|
+
output += whiteShine.bold(titleText[i]);
|
|
1175
|
+
}
|
|
1176
|
+
else if (distFromShine === 1) {
|
|
1177
|
+
output += chalk_1.default.hex('#FFFFFF')(titleText[i]);
|
|
1178
|
+
}
|
|
1179
|
+
else if (distFromShine === 2) {
|
|
1180
|
+
output += chalk_1.default.hex('#FFD700')(titleText[i]);
|
|
1181
|
+
}
|
|
1182
|
+
else if (distFromShine === 3) {
|
|
1183
|
+
output += chalk_1.default.hex('#FFA500')(titleText[i]);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
output += chalk_1.default.hex('#FF6B35').bold(titleText[i]);
|
|
1187
|
+
}
|
|
1058
1188
|
}
|
|
1189
|
+
process.stdout.write(output + '\n'); // Write and move down for next frame
|
|
1190
|
+
await sleep(15);
|
|
1059
1191
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
// Final title state
|
|
1065
|
-
process.stdout.write('\x1B[1A\r');
|
|
1066
|
-
console.log(' ' + chalk_1.default.hex('#FF6B35').bold(titleText));
|
|
1067
|
-
// Phase 3: Tagline fade-in with shimmer
|
|
1068
|
-
await sleep(100);
|
|
1069
|
-
// Build up tagline with wave effect
|
|
1070
|
-
const taglineColors = [
|
|
1071
|
-
chalk_1.default.hex('#444444'),
|
|
1072
|
-
chalk_1.default.hex('#666666'),
|
|
1073
|
-
chalk_1.default.hex('#888888'),
|
|
1074
|
-
chalk_1.default.hex('#AAAAAA'),
|
|
1075
|
-
chalk_1.default.hex('#CCCCCC'),
|
|
1076
|
-
chalk_1.default.hex('#EEEEEE'),
|
|
1077
|
-
chalk_1.default.gray,
|
|
1078
|
-
];
|
|
1079
|
-
for (let wave = 0; wave < taglineColors.length; wave++) {
|
|
1080
|
-
process.stdout.write('\r ');
|
|
1081
|
-
process.stdout.write(taglineColors[wave](taglineText));
|
|
1082
|
-
await sleep(40);
|
|
1083
|
-
}
|
|
1084
|
-
console.log('');
|
|
1085
|
-
// Phase 4: Quick orange accent pulse on tagline
|
|
1086
|
-
for (let pulse = 0; pulse < 2; pulse++) {
|
|
1087
|
-
await sleep(80);
|
|
1192
|
+
}
|
|
1193
|
+
// Final title state
|
|
1088
1194
|
process.stdout.write('\x1B[1A\r');
|
|
1089
|
-
console.log(' ' + chalk_1.default.hex('#
|
|
1090
|
-
|
|
1195
|
+
console.log(' ' + chalk_1.default.hex('#FF6B35').bold(titleText));
|
|
1196
|
+
// Phase 3: Tagline fade-in with shimmer
|
|
1197
|
+
await sleep(100);
|
|
1198
|
+
// Build up tagline with wave effect
|
|
1199
|
+
const taglineColors = [
|
|
1200
|
+
chalk_1.default.hex('#444444'),
|
|
1201
|
+
chalk_1.default.hex('#666666'),
|
|
1202
|
+
chalk_1.default.hex('#888888'),
|
|
1203
|
+
chalk_1.default.hex('#AAAAAA'),
|
|
1204
|
+
chalk_1.default.hex('#CCCCCC'),
|
|
1205
|
+
chalk_1.default.hex('#EEEEEE'),
|
|
1206
|
+
chalk_1.default.gray,
|
|
1207
|
+
];
|
|
1208
|
+
for (let wave = 0; wave < taglineColors.length; wave++) {
|
|
1209
|
+
process.stdout.write('\r ');
|
|
1210
|
+
process.stdout.write(taglineColors[wave](taglineText));
|
|
1211
|
+
await sleep(40);
|
|
1212
|
+
}
|
|
1213
|
+
console.log('');
|
|
1214
|
+
// Phase 4: Quick orange accent pulse on tagline
|
|
1215
|
+
for (let pulse = 0; pulse < 2; pulse++) {
|
|
1216
|
+
await sleep(80);
|
|
1217
|
+
process.stdout.write('\x1B[1A\r');
|
|
1218
|
+
console.log(' ' + chalk_1.default.hex('#FF8C00')(taglineText));
|
|
1219
|
+
await sleep(80);
|
|
1220
|
+
process.stdout.write('\x1B[1A\r');
|
|
1221
|
+
console.log(' ' + chalk_1.default.gray(taglineText));
|
|
1222
|
+
}
|
|
1223
|
+
// Final tagline state with subtle orange tint
|
|
1091
1224
|
process.stdout.write('\x1B[1A\r');
|
|
1092
|
-
console.log(' ' + chalk_1.default.
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1225
|
+
console.log(' ' + chalk_1.default.hex('#B8860B')(taglineText));
|
|
1226
|
+
console.log('');
|
|
1227
|
+
if (bypass) {
|
|
1228
|
+
console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
|
|
1229
|
+
}
|
|
1230
|
+
if (noDna) {
|
|
1231
|
+
console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
|
|
1232
|
+
}
|
|
1233
|
+
if (verbose) {
|
|
1234
|
+
console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
|
|
1235
|
+
console.log(chalk_1.default.gray(` ⏱ Timing: clear=${config.clearWaitMs}ms, idleMax=${config.maxIdleWaitMs}ms (~${Math.round((config.clearWaitMs + config.maxIdleWaitMs * 2 + 1700) / 1000)}s total)`));
|
|
1236
|
+
}
|
|
1237
|
+
console.log('');
|
|
1103
1238
|
}
|
|
1104
|
-
|
|
1105
|
-
console.log(
|
|
1106
|
-
console.log(chalk_1.default.
|
|
1239
|
+
else {
|
|
1240
|
+
console.log('');
|
|
1241
|
+
console.log(chalk_1.default.cyan(' ekkOS remote session ready'));
|
|
1242
|
+
if (bypass) {
|
|
1243
|
+
console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
|
|
1244
|
+
}
|
|
1245
|
+
if (noDna) {
|
|
1246
|
+
console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
|
|
1247
|
+
}
|
|
1248
|
+
if (verbose) {
|
|
1249
|
+
console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
|
|
1250
|
+
}
|
|
1251
|
+
console.log('');
|
|
1107
1252
|
}
|
|
1108
|
-
console.log('');
|
|
1109
1253
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1110
1254
|
// ANIMATION COMPLETE: Mark ready and flush buffered Claude output
|
|
1111
1255
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -1118,8 +1262,9 @@ async function run(options) {
|
|
|
1118
1262
|
await sleep(100);
|
|
1119
1263
|
process.stdout.write('\r' + ' '.repeat(30) + '\r'); // Clear the line
|
|
1120
1264
|
}
|
|
1121
|
-
// Track state
|
|
1122
|
-
|
|
1265
|
+
// Track state — only use explicit -s option; never inherit stale session from state.json
|
|
1266
|
+
// The real session name will be detected from Claude Code's output (line ~2552)
|
|
1267
|
+
let currentSession = options.session || null;
|
|
1123
1268
|
// Write initial instance file
|
|
1124
1269
|
const startedAt = new Date().toISOString();
|
|
1125
1270
|
writeInstanceFile(instanceId, {
|
|
@@ -1199,7 +1344,8 @@ async function run(options) {
|
|
|
1199
1344
|
catch {
|
|
1200
1345
|
dlog('[TRANSCRIPT] Project dir does not exist yet');
|
|
1201
1346
|
}
|
|
1202
|
-
// Poll for new transcript file every 500ms for up to 30 seconds
|
|
1347
|
+
// Poll for new transcript file every 500ms for up to 30 seconds.
|
|
1348
|
+
// Safety rule: do NOT guess using "most recent" files; that can cross-bind sessions.
|
|
1203
1349
|
let transcriptPollInterval = null;
|
|
1204
1350
|
function pollForNewTranscript() {
|
|
1205
1351
|
if (transcriptPath) {
|
|
@@ -1212,9 +1358,9 @@ async function run(options) {
|
|
|
1212
1358
|
}
|
|
1213
1359
|
// Stop after 30 seconds
|
|
1214
1360
|
if (Date.now() - launchTime > 30000) {
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
if (!transcriptPath) {
|
|
1361
|
+
// Local mode fallback: transcript maintenance features need a file path.
|
|
1362
|
+
// In proxy mode this is intentionally disabled to avoid cross-session mixing.
|
|
1363
|
+
if (!proxyModeEnabled && !transcriptPath) {
|
|
1218
1364
|
try {
|
|
1219
1365
|
const files = fs.readdirSync(projectDir);
|
|
1220
1366
|
const jsonlFiles = files
|
|
@@ -1228,23 +1374,23 @@ async function run(options) {
|
|
|
1228
1374
|
if (jsonlFiles.length > 0) {
|
|
1229
1375
|
transcriptPath = jsonlFiles[0].path;
|
|
1230
1376
|
currentSessionId = jsonlFiles[0].name.replace('.jsonl', '');
|
|
1231
|
-
dlog(`[TRANSCRIPT]
|
|
1232
|
-
evictionDebugLog('TRANSCRIPT_SET', 'Polling timeout fallback - using most recent jsonl', {
|
|
1233
|
-
transcriptPath,
|
|
1234
|
-
currentSessionId,
|
|
1235
|
-
fileCount: jsonlFiles.length,
|
|
1236
|
-
});
|
|
1377
|
+
dlog(`[TRANSCRIPT] Local-mode timeout fallback: ${transcriptPath}`);
|
|
1237
1378
|
startStreamTailer(transcriptPath, currentSessionId);
|
|
1238
1379
|
}
|
|
1239
|
-
else {
|
|
1240
|
-
dlog('[TRANSCRIPT] TIMEOUT FALLBACK: No jsonl files found in project dir');
|
|
1241
|
-
}
|
|
1242
1380
|
}
|
|
1243
|
-
catch
|
|
1244
|
-
|
|
1381
|
+
catch {
|
|
1382
|
+
// Ignore local-mode timeout errors
|
|
1245
1383
|
}
|
|
1246
1384
|
}
|
|
1247
|
-
|
|
1385
|
+
if (proxyModeEnabled) {
|
|
1386
|
+
dlog('[TRANSCRIPT] Polling timeout - no safe transcript candidate found');
|
|
1387
|
+
}
|
|
1388
|
+
else if (transcriptPath) {
|
|
1389
|
+
dlog('[TRANSCRIPT] Polling timeout - local fallback transcript selected');
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
dlog('[TRANSCRIPT] Polling timeout - no transcript candidate found');
|
|
1393
|
+
}
|
|
1248
1394
|
if (transcriptPollInterval) {
|
|
1249
1395
|
clearInterval(transcriptPollInterval);
|
|
1250
1396
|
transcriptPollInterval = null;
|
|
@@ -1277,27 +1423,6 @@ async function run(options) {
|
|
|
1277
1423
|
return;
|
|
1278
1424
|
}
|
|
1279
1425
|
}
|
|
1280
|
-
// Also check for recently modified files (in case we missed the creation)
|
|
1281
|
-
const recentFiles = jsonlFiles
|
|
1282
|
-
.map(f => ({ name: f, path: path.join(projectDir, f), mtime: fs.statSync(path.join(projectDir, f)).mtimeMs }))
|
|
1283
|
-
.filter(f => f.mtime > launchTime - 2000) // Modified within 2s of launch
|
|
1284
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
1285
|
-
if (recentFiles.length > 0) {
|
|
1286
|
-
const newest = recentFiles[0];
|
|
1287
|
-
transcriptPath = newest.path;
|
|
1288
|
-
currentSessionId = newest.name.replace('.jsonl', '');
|
|
1289
|
-
dlog(`[TRANSCRIPT] FAST DETECT: Recent transcript found! ${transcriptPath}`);
|
|
1290
|
-
evictionDebugLog('TRANSCRIPT_SET', 'Fast poll found recent file', {
|
|
1291
|
-
transcriptPath,
|
|
1292
|
-
currentSessionId,
|
|
1293
|
-
elapsedMs: Date.now() - launchTime
|
|
1294
|
-
});
|
|
1295
|
-
startStreamTailer(transcriptPath, currentSessionId);
|
|
1296
|
-
if (transcriptPollInterval) {
|
|
1297
|
-
clearInterval(transcriptPollInterval);
|
|
1298
|
-
transcriptPollInterval = null;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
1426
|
}
|
|
1302
1427
|
catch {
|
|
1303
1428
|
// Project dir doesn't exist yet, keep polling
|
|
@@ -1318,6 +1443,8 @@ async function run(options) {
|
|
|
1318
1443
|
// Track if we've EVER observed a session in THIS process run
|
|
1319
1444
|
// This is the authoritative flag - if false, don't trust persisted state
|
|
1320
1445
|
let observedSessionThisRun = false;
|
|
1446
|
+
let boundProxySession = null;
|
|
1447
|
+
let bindingSessionInFlight = null;
|
|
1321
1448
|
// Output buffer for pattern detection
|
|
1322
1449
|
let outputBuffer = '';
|
|
1323
1450
|
// Debounce tracking to prevent double triggers
|
|
@@ -1391,6 +1518,48 @@ async function run(options) {
|
|
|
1391
1518
|
}
|
|
1392
1519
|
return true;
|
|
1393
1520
|
}
|
|
1521
|
+
function resolveTranscriptFromSessionId(source) {
|
|
1522
|
+
if (!currentSessionId || transcriptPath)
|
|
1523
|
+
return;
|
|
1524
|
+
const candidate = path.join(projectDir, `${currentSessionId}.jsonl`);
|
|
1525
|
+
if (!fs.existsSync(candidate))
|
|
1526
|
+
return;
|
|
1527
|
+
transcriptPath = candidate;
|
|
1528
|
+
evictionDebugLog('TRANSCRIPT_SET', `Set from session ID (${source})`, {
|
|
1529
|
+
transcriptPath,
|
|
1530
|
+
currentSessionId,
|
|
1531
|
+
});
|
|
1532
|
+
dlog(`[TRANSCRIPT] Resolved by session ID (${source}): ${candidate}`);
|
|
1533
|
+
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
1534
|
+
}
|
|
1535
|
+
function bindRealSessionToProxy(sessionName, source) {
|
|
1536
|
+
if (!proxyModeEnabled)
|
|
1537
|
+
return;
|
|
1538
|
+
if (!sessionName || sessionName === '_pending' || sessionName === 'pending' || sessionName.startsWith('_pending-'))
|
|
1539
|
+
return;
|
|
1540
|
+
if (boundProxySession === sessionName || bindingSessionInFlight === sessionName)
|
|
1541
|
+
return;
|
|
1542
|
+
bindingSessionInFlight = sessionName;
|
|
1543
|
+
void (async () => {
|
|
1544
|
+
const maxAttempts = 3;
|
|
1545
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1546
|
+
const pendingSession = cliSessionName && cliSessionName.startsWith('_pending') ? cliSessionName : undefined;
|
|
1547
|
+
const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd(), pendingSession);
|
|
1548
|
+
if (success) {
|
|
1549
|
+
boundProxySession = sessionName;
|
|
1550
|
+
bindingSessionInFlight = null;
|
|
1551
|
+
cliSessionName = sessionName;
|
|
1552
|
+
dlog(`[SESSION_BIND] Bound ${sessionName} from ${source} (attempt ${attempt}/${maxAttempts})`);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (attempt < maxAttempts) {
|
|
1556
|
+
await sleep(200 * attempt);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
dlog(`[SESSION_BIND] Failed to bind ${sessionName} from ${source}`);
|
|
1560
|
+
bindingSessionInFlight = null;
|
|
1561
|
+
})();
|
|
1562
|
+
}
|
|
1394
1563
|
/**
|
|
1395
1564
|
* Check if there are in-flight tool calls (tool_uses without matching tool_results)
|
|
1396
1565
|
* CRITICAL: We must NOT evict while tools are in-flight or we'll orphan tool_results
|
|
@@ -1444,6 +1613,12 @@ async function run(options) {
|
|
|
1444
1613
|
async function handleTurnEnd() {
|
|
1445
1614
|
if (!transcriptPath || isAutoClearInProgress)
|
|
1446
1615
|
return;
|
|
1616
|
+
if (proxyModeEnabled) {
|
|
1617
|
+
// Proxy is the SOLE eviction authority. It strips old turns before forwarding to Anthropic,
|
|
1618
|
+
// keeping API payloads small and consistent for prompt cache hits.
|
|
1619
|
+
// CLI does NOT touch the local JSONL - Claude Code sends full history, proxy handles the rest.
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1447
1622
|
// DEFENSIVE: Validate path before using
|
|
1448
1623
|
if (!validateTranscriptPath(transcriptPath)) {
|
|
1449
1624
|
evictionDebugLog('TURN_END_ABORT', 'Invalid transcriptPath detected - resetting to null', {
|
|
@@ -1472,13 +1647,12 @@ async function run(options) {
|
|
|
1472
1647
|
dlog(`🧹 Turn-end: Cleaned ${cleanResult.cleaned} junk lines`);
|
|
1473
1648
|
evictionDebugLog('CONTINUOUS_CLEAN', `Cleaned ${cleanResult.cleaned} junk lines`);
|
|
1474
1649
|
}
|
|
1475
|
-
// Then run eviction if needed (
|
|
1476
|
-
// CRITICAL: When proxy mode is enabled, the proxy does seamless eviction - local JSONL eviction must be disabled
|
|
1650
|
+
// Then run eviction if needed (local mode only - proxy handles eviction in proxy mode)
|
|
1477
1651
|
// eslint-disable-next-line no-restricted-syntax -- Feature flag, not API key
|
|
1478
1652
|
const evictionDisabled = process.env.EKKOS_DISABLE_EVICTION === '1' || proxyModeEnabled;
|
|
1479
1653
|
if (evictionDisabled) {
|
|
1480
1654
|
evictionDebugLog('EVICTION_DISABLED', proxyModeEnabled
|
|
1481
|
-
? '
|
|
1655
|
+
? 'Proxy is sole eviction authority - CLI does not touch local JSONL'
|
|
1482
1656
|
: 'Eviction disabled via EKKOS_DISABLE_EVICTION=1');
|
|
1483
1657
|
}
|
|
1484
1658
|
else if ((0, jsonl_rewriter_1.needsEviction)(lastContextPercent)) {
|
|
@@ -2148,17 +2322,8 @@ async function run(options) {
|
|
|
2148
2322
|
// Also update global state for backwards compatibility
|
|
2149
2323
|
(0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
|
|
2150
2324
|
dlog(`Session detected from UUID: ${currentSession}`);
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const encodedCwd = process.cwd().replace(/\//g, '-');
|
|
2154
|
-
const possibleTranscript = path.join(os.homedir(), '.claude', 'projects', encodedCwd, `${currentSessionId}.jsonl`);
|
|
2155
|
-
if (fs.existsSync(possibleTranscript)) {
|
|
2156
|
-
transcriptPath = possibleTranscript;
|
|
2157
|
-
evictionDebugLog('TRANSCRIPT_SET', 'Set from session ID', { transcriptPath, source: 'sessionId' });
|
|
2158
|
-
dlog(`Found transcript from session ID: ${transcriptPath}`);
|
|
2159
|
-
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2325
|
+
resolveTranscriptFromSessionId('session-id-from-output');
|
|
2326
|
+
bindRealSessionToProxy(currentSession, 'session-id-from-output');
|
|
2162
2327
|
}
|
|
2163
2328
|
// ════════════════════════════════════════════════════════════════════════
|
|
2164
2329
|
// SESSION NAME DETECTION (PRIMARY METHOD)
|
|
@@ -2175,69 +2340,14 @@ async function run(options) {
|
|
|
2175
2340
|
dlog(`Session rejected (invalid words): ${detectedSession}`);
|
|
2176
2341
|
}
|
|
2177
2342
|
else if (detectedSession !== lastSeenSessionName) {
|
|
2178
|
-
//
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
currentSession
|
|
2182
|
-
|
|
2183
|
-
// Update THIS process's session entry (not global state.json)
|
|
2184
|
-
(0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
|
|
2185
|
-
// Also update global state for backwards compatibility
|
|
2186
|
-
(0, state_1.updateState)({ sessionName: currentSession });
|
|
2187
|
-
dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
|
|
2188
|
-
// Try to start stream tailer - scan for matching transcript file
|
|
2189
|
-
dlog(`[TRANSCRIPT_SCAN] streamTailer=${!!streamTailer}, transcriptPath=${transcriptPath || 'NULL'}`);
|
|
2190
|
-
if (!streamTailer) {
|
|
2191
|
-
const encodedCwd = process.cwd().replace(/\//g, '-');
|
|
2192
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
|
|
2193
|
-
dlog(`[TRANSCRIPT_SCAN] Scanning projectDir: ${projectDir}`);
|
|
2194
|
-
try {
|
|
2195
|
-
const files = fs.readdirSync(projectDir);
|
|
2196
|
-
dlog(`[TRANSCRIPT_SCAN] Found ${files.length} files in projectDir`);
|
|
2197
|
-
// Find most recent .jsonl file (likely current session)
|
|
2198
|
-
const jsonlFiles = files
|
|
2199
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
2200
|
-
.map(f => ({
|
|
2201
|
-
name: f,
|
|
2202
|
-
path: path.join(projectDir, f),
|
|
2203
|
-
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
|
|
2204
|
-
}))
|
|
2205
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
2206
|
-
dlog(`[TRANSCRIPT_SCAN] Found ${jsonlFiles.length} .jsonl files`);
|
|
2207
|
-
if (jsonlFiles.length > 0) {
|
|
2208
|
-
transcriptPath = jsonlFiles[0].path;
|
|
2209
|
-
currentSessionId = jsonlFiles[0].name.replace('.jsonl', '');
|
|
2210
|
-
dlog(`[TRANSCRIPT_SCAN] SUCCESS! transcriptPath=${transcriptPath}`);
|
|
2211
|
-
evictionDebugLog('TRANSCRIPT_SET', 'Set from session name detection', { transcriptPath, currentSessionId });
|
|
2212
|
-
startStreamTailer(transcriptPath, currentSessionId, currentSession);
|
|
2213
|
-
}
|
|
2214
|
-
else {
|
|
2215
|
-
dlog(`[TRANSCRIPT_SCAN] No jsonl files found!`);
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
catch (err) {
|
|
2219
|
-
dlog(`[TRANSCRIPT_SCAN] ERROR: ${err.message}`);
|
|
2220
|
-
}
|
|
2343
|
+
// Do not allow session switching from parsed output once a run has
|
|
2344
|
+
// already locked onto a session. This prevents rebinding from echoed
|
|
2345
|
+
// status/footer text in model responses (e.g. pasted transcripts).
|
|
2346
|
+
if (observedSessionThisRun && currentSession && detectedSession !== currentSession) {
|
|
2347
|
+
dlog(`Ignoring session switch from status line: ${currentSession} -> ${detectedSession}`);
|
|
2221
2348
|
}
|
|
2222
2349
|
else {
|
|
2223
|
-
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
else {
|
|
2227
|
-
// Same session, just update timestamp
|
|
2228
|
-
lastSeenSessionAt = Date.now();
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
else {
|
|
2232
|
-
// Weaker signal: any 3-word slug (only if no status match)
|
|
2233
|
-
const anyMatch = plain.match(SESSION_NAME_REGEX);
|
|
2234
|
-
if (anyMatch) {
|
|
2235
|
-
const detectedSession = anyMatch[1].toLowerCase();
|
|
2236
|
-
// Validate against word lists (SOURCE OF TRUTH)
|
|
2237
|
-
if (!isValidSessionName(detectedSession)) {
|
|
2238
|
-
dlog(`Session rejected (invalid words): ${detectedSession}`);
|
|
2239
|
-
}
|
|
2240
|
-
else if (detectedSession !== lastSeenSessionName) {
|
|
2350
|
+
// Only update if different (avoid log spam)
|
|
2241
2351
|
lastSeenSessionName = detectedSession;
|
|
2242
2352
|
lastSeenSessionAt = Date.now();
|
|
2243
2353
|
currentSession = lastSeenSessionName;
|
|
@@ -2246,13 +2356,27 @@ async function run(options) {
|
|
|
2246
2356
|
(0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
|
|
2247
2357
|
// Also update global state for backwards compatibility
|
|
2248
2358
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
2249
|
-
dlog(`Session detected from
|
|
2359
|
+
dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
|
|
2360
|
+
bindRealSessionToProxy(currentSession, 'status-line');
|
|
2361
|
+
resolveTranscriptFromSessionId('status-line');
|
|
2250
2362
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2363
|
+
}
|
|
2364
|
+
else {
|
|
2365
|
+
// Same session, just update timestamp
|
|
2366
|
+
lastSeenSessionAt = Date.now();
|
|
2367
|
+
if (boundProxySession !== detectedSession) {
|
|
2368
|
+
bindRealSessionToProxy(detectedSession, 'status-line-refresh');
|
|
2253
2369
|
}
|
|
2254
2370
|
}
|
|
2255
2371
|
}
|
|
2372
|
+
else {
|
|
2373
|
+
// Weak signal: any 3-word slug can appear in arbitrary output.
|
|
2374
|
+
// Ignore it to avoid accidental cross-session rebinding.
|
|
2375
|
+
const anyMatch = plain.match(SESSION_NAME_REGEX);
|
|
2376
|
+
if (anyMatch && !observedSessionThisRun) {
|
|
2377
|
+
dlog(`Ignoring weak session candidate (awaiting strong signal): ${anyMatch[1].toLowerCase()}`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2256
2380
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2257
2381
|
// TURN-END EVICTION - Track context % and run cleanup when Claude goes idle
|
|
2258
2382
|
// This is MUCH safer than mid-stream eviction because:
|
|
@@ -2261,7 +2385,7 @@ async function run(options) {
|
|
|
2261
2385
|
// 3. Claude Code is between operations
|
|
2262
2386
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2263
2387
|
// ════════════════════════════════════════════════════════════════════════
|
|
2264
|
-
// CONTEXT % CALCULATION -
|
|
2388
|
+
// CONTEXT % CALCULATION - Local mode only (proxy handles its own token tracking)
|
|
2265
2389
|
// ════════════════════════════════════════════════════════════════════════
|
|
2266
2390
|
if (!proxyModeEnabled) {
|
|
2267
2391
|
// Track context percentage - PRIMARY: calculate from JSONL file size
|
|
@@ -2288,8 +2412,7 @@ async function run(options) {
|
|
|
2288
2412
|
}
|
|
2289
2413
|
}
|
|
2290
2414
|
// ════════════════════════════════════════════════════════════════════════
|
|
2291
|
-
//
|
|
2292
|
-
// handleTurnEnd() has internal check to skip threshold eviction when proxy is on
|
|
2415
|
+
// TURN-END MAINTENANCE - Local mode only (proxy is sole eviction authority)
|
|
2293
2416
|
// ════════════════════════════════════════════════════════════════════════
|
|
2294
2417
|
// Detect idle prompt (turn end) and schedule cleanup
|
|
2295
2418
|
const strippedOutput = stripAnsi(outputBuffer);
|
|
@@ -2303,7 +2426,7 @@ async function run(options) {
|
|
|
2303
2426
|
outputBufferEnd: strippedOutput.slice(-100),
|
|
2304
2427
|
});
|
|
2305
2428
|
}
|
|
2306
|
-
if (idlePromptDetected && transcriptPath && !isAutoClearInProgress) {
|
|
2429
|
+
if (!proxyModeEnabled && idlePromptDetected && transcriptPath && !isAutoClearInProgress) {
|
|
2307
2430
|
// Cancel any existing timer
|
|
2308
2431
|
if (turnEndTimeout) {
|
|
2309
2432
|
clearTimeout(turnEndTimeout);
|
|
@@ -2317,7 +2440,7 @@ async function run(options) {
|
|
|
2317
2440
|
}, TURN_END_STABLE_MS);
|
|
2318
2441
|
}
|
|
2319
2442
|
// SLIDING WINDOW: Inject /clear after eviction to force transcript reload
|
|
2320
|
-
if (idlePromptDetected && pendingClearAfterEviction && !isAutoClearInProgress) {
|
|
2443
|
+
if (!proxyModeEnabled && idlePromptDetected && pendingClearAfterEviction && !isAutoClearInProgress) {
|
|
2321
2444
|
pendingClearAfterEviction = false;
|
|
2322
2445
|
isAutoClearInProgress = true;
|
|
2323
2446
|
dlog('🔄 SLIDING WINDOW: Injecting /clear to reload evicted transcript');
|
|
@@ -2350,8 +2473,8 @@ async function run(options) {
|
|
|
2350
2473
|
}
|
|
2351
2474
|
})();
|
|
2352
2475
|
}
|
|
2353
|
-
// BACKUP: Context wall detection - emergency evict
|
|
2354
|
-
if (!isAutoClearInProgress && transcriptPath) {
|
|
2476
|
+
// BACKUP: Context wall detection - emergency evict (all modes)
|
|
2477
|
+
if (!proxyModeEnabled && !isAutoClearInProgress && transcriptPath) {
|
|
2355
2478
|
const normalized = normalizeForMatch(outputBuffer);
|
|
2356
2479
|
if (CONTEXT_WALL_REGEX.test(normalized)) {
|
|
2357
2480
|
dlog('⚠️ CONTEXT WALL - emergency evict to 50%');
|
|
@@ -2368,6 +2491,36 @@ async function run(options) {
|
|
|
2368
2491
|
}
|
|
2369
2492
|
});
|
|
2370
2493
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2494
|
+
// KICKSTART MODE: Auto-send "test" to create session immediately
|
|
2495
|
+
// Used by --dashboard to eliminate wait for first user message
|
|
2496
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
2497
|
+
if (options.kickstart) {
|
|
2498
|
+
dlog('Kickstart mode enabled - will auto-send "test" to create session');
|
|
2499
|
+
setTimeout(async () => {
|
|
2500
|
+
dlog('Starting kickstart injection...');
|
|
2501
|
+
const readiness = await waitForIdlePrompt(getOutputBuffer, config);
|
|
2502
|
+
if (!readiness.ready || readiness.interrupted) {
|
|
2503
|
+
dlog('Claude not ready for kickstart - aborting');
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
// PAUSE STDIN during injection
|
|
2507
|
+
process.stdin.off('data', onStdinData);
|
|
2508
|
+
dlog('Stdin paused during kickstart');
|
|
2509
|
+
try {
|
|
2510
|
+
shell.write('\x15'); // Ctrl+U - clear any existing input
|
|
2511
|
+
await sleep(60);
|
|
2512
|
+
await typeSlowly(shell, 'test', config.charDelayMs);
|
|
2513
|
+
await sleep(100);
|
|
2514
|
+
shell.write('\r'); // Enter
|
|
2515
|
+
dlog('Kickstart "test" sent - session should be created');
|
|
2516
|
+
}
|
|
2517
|
+
finally {
|
|
2518
|
+
process.stdin.on('data', onStdinData);
|
|
2519
|
+
dlog('Stdin resumed after kickstart');
|
|
2520
|
+
}
|
|
2521
|
+
}, 3000); // 3s for Claude to initialize
|
|
2522
|
+
}
|
|
2523
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
2371
2524
|
// RESEARCH MODE: Auto-type research prompt after Claude is ready
|
|
2372
2525
|
// Triggers: `ekkos run -r` or `ekkos run --research`
|
|
2373
2526
|
// Works like /clear continue - waits for idle prompt, then injects text
|