helixmind 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/cli/agent/loop.d.ts.map +1 -1
  2. package/dist/cli/agent/loop.js +18 -11
  3. package/dist/cli/agent/loop.js.map +1 -1
  4. package/dist/cli/agent/permissions.d.ts +16 -0
  5. package/dist/cli/agent/permissions.d.ts.map +1 -1
  6. package/dist/cli/agent/permissions.js +74 -5
  7. package/dist/cli/agent/permissions.js.map +1 -1
  8. package/dist/cli/agent/sandbox.d.ts.map +1 -1
  9. package/dist/cli/agent/sandbox.js +3 -0
  10. package/dist/cli/agent/sandbox.js.map +1 -1
  11. package/dist/cli/brain/archive.d.ts.map +1 -1
  12. package/dist/cli/brain/archive.js +48 -18
  13. package/dist/cli/brain/archive.js.map +1 -1
  14. package/dist/cli/brain/control-protocol.d.ts +17 -0
  15. package/dist/cli/brain/control-protocol.d.ts.map +1 -1
  16. package/dist/cli/brain/control-protocol.js +79 -0
  17. package/dist/cli/brain/control-protocol.js.map +1 -1
  18. package/dist/cli/brain/path-guard.d.ts +2 -0
  19. package/dist/cli/brain/path-guard.d.ts.map +1 -0
  20. package/dist/cli/brain/path-guard.js +94 -0
  21. package/dist/cli/brain/path-guard.js.map +1 -0
  22. package/dist/cli/brain/relay-client.d.ts +2 -0
  23. package/dist/cli/brain/relay-client.d.ts.map +1 -1
  24. package/dist/cli/brain/relay-client.js +58 -8
  25. package/dist/cli/brain/relay-client.js.map +1 -1
  26. package/dist/cli/brain/server.d.ts.map +1 -1
  27. package/dist/cli/brain/server.js +235 -23
  28. package/dist/cli/brain/server.js.map +1 -1
  29. package/dist/cli/brain/stdout-capture.d.ts +11 -0
  30. package/dist/cli/brain/stdout-capture.d.ts.map +1 -1
  31. package/dist/cli/brain/stdout-capture.js +42 -1
  32. package/dist/cli/brain/stdout-capture.js.map +1 -1
  33. package/dist/cli/brain/web-chat-handler.d.ts.map +1 -1
  34. package/dist/cli/brain/web-chat-handler.js +7 -0
  35. package/dist/cli/brain/web-chat-handler.js.map +1 -1
  36. package/dist/cli/checkpoints/revert.d.ts +5 -0
  37. package/dist/cli/checkpoints/revert.d.ts.map +1 -1
  38. package/dist/cli/checkpoints/revert.js +100 -11
  39. package/dist/cli/checkpoints/revert.js.map +1 -1
  40. package/dist/cli/checkpoints/store.d.ts +3 -0
  41. package/dist/cli/checkpoints/store.d.ts.map +1 -1
  42. package/dist/cli/checkpoints/store.js.map +1 -1
  43. package/dist/cli/commands/chat.d.ts.map +1 -1
  44. package/dist/cli/commands/chat.js +138 -73
  45. package/dist/cli/commands/chat.js.map +1 -1
  46. package/dist/cli/config/store.js +1 -1
  47. package/dist/cli/config/store.js.map +1 -1
  48. package/dist/cli/core/input.d.ts +0 -1
  49. package/dist/cli/core/input.d.ts.map +1 -1
  50. package/dist/cli/core/input.js +9 -17
  51. package/dist/cli/core/input.js.map +1 -1
  52. package/dist/cli/jarvis/autonomy.d.ts +13 -1
  53. package/dist/cli/jarvis/autonomy.d.ts.map +1 -1
  54. package/dist/cli/jarvis/autonomy.js +33 -1
  55. package/dist/cli/jarvis/autonomy.js.map +1 -1
  56. package/dist/cli/jarvis/core-ethics.d.ts +15 -0
  57. package/dist/cli/jarvis/core-ethics.d.ts.map +1 -1
  58. package/dist/cli/jarvis/core-ethics.js +110 -9
  59. package/dist/cli/jarvis/core-ethics.js.map +1 -1
  60. package/dist/cli/jarvis/daemon.d.ts.map +1 -1
  61. package/dist/cli/jarvis/daemon.js +8 -0
  62. package/dist/cli/jarvis/daemon.js.map +1 -1
  63. package/dist/cli/jarvis/instance-lock.d.ts.map +1 -1
  64. package/dist/cli/jarvis/instance-lock.js +132 -27
  65. package/dist/cli/jarvis/instance-lock.js.map +1 -1
  66. package/dist/cli/jarvis/learning.d.ts +6 -0
  67. package/dist/cli/jarvis/learning.d.ts.map +1 -1
  68. package/dist/cli/jarvis/learning.js +17 -3
  69. package/dist/cli/jarvis/learning.js.map +1 -1
  70. package/dist/cli/jarvis/notifications.d.ts.map +1 -1
  71. package/dist/cli/jarvis/notifications.js +75 -10
  72. package/dist/cli/jarvis/notifications.js.map +1 -1
  73. package/dist/cli/jarvis/onboarding.d.ts.map +1 -1
  74. package/dist/cli/jarvis/onboarding.js +32 -2
  75. package/dist/cli/jarvis/onboarding.js.map +1 -1
  76. package/dist/cli/jarvis/queue.d.ts +5 -0
  77. package/dist/cli/jarvis/queue.d.ts.map +1 -1
  78. package/dist/cli/jarvis/queue.js +23 -6
  79. package/dist/cli/jarvis/queue.js.map +1 -1
  80. package/dist/cli/jarvis/scheduler.d.ts +8 -0
  81. package/dist/cli/jarvis/scheduler.d.ts.map +1 -1
  82. package/dist/cli/jarvis/scheduler.js +32 -4
  83. package/dist/cli/jarvis/scheduler.js.map +1 -1
  84. package/dist/cli/jarvis/sentiment.d.ts +11 -0
  85. package/dist/cli/jarvis/sentiment.d.ts.map +1 -1
  86. package/dist/cli/jarvis/sentiment.js +40 -6
  87. package/dist/cli/jarvis/sentiment.js.map +1 -1
  88. package/dist/cli/jarvis/skills.d.ts +35 -0
  89. package/dist/cli/jarvis/skills.d.ts.map +1 -1
  90. package/dist/cli/jarvis/skills.js +144 -0
  91. package/dist/cli/jarvis/skills.js.map +1 -1
  92. package/dist/cli/jarvis/triggers.d.ts.map +1 -1
  93. package/dist/cli/jarvis/triggers.js +65 -9
  94. package/dist/cli/jarvis/triggers.js.map +1 -1
  95. package/dist/cli/jarvis/types.d.ts +10 -0
  96. package/dist/cli/jarvis/types.d.ts.map +1 -1
  97. package/dist/cli/jarvis/world-model.d.ts +4 -0
  98. package/dist/cli/jarvis/world-model.d.ts.map +1 -1
  99. package/dist/cli/jarvis/world-model.js +23 -1
  100. package/dist/cli/jarvis/world-model.js.map +1 -1
  101. package/dist/cli/providers/anthropic.d.ts +5 -1
  102. package/dist/cli/providers/anthropic.d.ts.map +1 -1
  103. package/dist/cli/providers/anthropic.js +133 -36
  104. package/dist/cli/providers/anthropic.js.map +1 -1
  105. package/dist/cli/providers/model-limits.d.ts +8 -0
  106. package/dist/cli/providers/model-limits.d.ts.map +1 -1
  107. package/dist/cli/providers/model-limits.js +42 -0
  108. package/dist/cli/providers/model-limits.js.map +1 -1
  109. package/dist/cli/providers/ollama.d.ts +2 -0
  110. package/dist/cli/providers/ollama.d.ts.map +1 -1
  111. package/dist/cli/providers/ollama.js +8 -0
  112. package/dist/cli/providers/ollama.js.map +1 -1
  113. package/dist/cli/providers/openai.d.ts +5 -1
  114. package/dist/cli/providers/openai.d.ts.map +1 -1
  115. package/dist/cli/providers/openai.js +66 -19
  116. package/dist/cli/providers/openai.js.map +1 -1
  117. package/dist/cli/providers/rate-limiter.d.ts +58 -20
  118. package/dist/cli/providers/rate-limiter.d.ts.map +1 -1
  119. package/dist/cli/providers/rate-limiter.js +212 -96
  120. package/dist/cli/providers/rate-limiter.js.map +1 -1
  121. package/dist/cli/providers/registry.d.ts.map +1 -1
  122. package/dist/cli/providers/registry.js +22 -13
  123. package/dist/cli/providers/registry.js.map +1 -1
  124. package/dist/cli/providers/types.d.ts +16 -5
  125. package/dist/cli/providers/types.d.ts.map +1 -1
  126. package/dist/spiral/cloud/content-extractor.d.ts.map +1 -1
  127. package/dist/spiral/cloud/content-extractor.js +6 -4
  128. package/dist/spiral/cloud/content-extractor.js.map +1 -1
  129. package/dist/spiral/cloud/search-provider.d.ts.map +1 -1
  130. package/dist/spiral/cloud/search-provider.js +7 -2
  131. package/dist/spiral/cloud/search-provider.js.map +1 -1
  132. package/dist/spiral/cloud/web-enricher.d.ts.map +1 -1
  133. package/dist/spiral/cloud/web-enricher.js +10 -2
  134. package/dist/spiral/cloud/web-enricher.js.map +1 -1
  135. package/dist/spiral/compression.d.ts +5 -2
  136. package/dist/spiral/compression.d.ts.map +1 -1
  137. package/dist/spiral/compression.js +23 -6
  138. package/dist/spiral/compression.js.map +1 -1
  139. package/dist/spiral/engine.d.ts +7 -1
  140. package/dist/spiral/engine.d.ts.map +1 -1
  141. package/dist/spiral/engine.js +83 -35
  142. package/dist/spiral/engine.js.map +1 -1
  143. package/dist/storage/database.d.ts.map +1 -1
  144. package/dist/storage/database.js +41 -0
  145. package/dist/storage/database.js.map +1 -1
  146. package/dist/storage/nodes.d.ts +6 -0
  147. package/dist/storage/nodes.d.ts.map +1 -1
  148. package/dist/storage/nodes.js +9 -0
  149. package/dist/storage/nodes.js.map +1 -1
  150. package/dist/storage/vectors.d.ts +1 -0
  151. package/dist/storage/vectors.d.ts.map +1 -1
  152. package/dist/storage/vectors.js +39 -7
  153. package/dist/storage/vectors.js.map +1 -1
  154. package/package.json +3 -3
@@ -2246,7 +2246,6 @@ export async function chatCommand(options) {
2246
2246
  });
2247
2247
  // Suggestion panel is now handled by InputManager (built-in arrow navigation, Tab/Enter/ESC)
2248
2248
  // Legacy compat stubs:
2249
- let panelJustClosed = false;
2250
2249
  function replaceReadlineInput(text) {
2251
2250
  inputMgr.setLine(text);
2252
2251
  }
@@ -2531,15 +2530,11 @@ export async function chatCommand(options) {
2531
2530
  // The panel opens/updates/closes automatically as the user types slash commands.
2532
2531
  // === ESC detection (single ESC = stop for normal agents) ===
2533
2532
  // Double-ESC (rewind browser) is handled by the raw data listener below.
2534
- // Skip if a full-screen browser (Rewind/Plan) is open it handles ESC itself.
2535
- // Skip if ESC was used to close the suggestion panel (panelJustClosed flag)
2536
- //
2533
+ // When the slash-suggestion panel is open, InputManager consumes ESC
2534
+ // internally before this handler fires, so no extra guard is needed.
2537
2535
  // Special modes (Jarvis, autonomous): ESC is handled ENTIRELY by the raw
2538
2536
  // data listener below — the keypress handler does NOTHING for them.
2539
- // This prevents conflicts between the two handlers and avoids cursor jumps
2540
- // from hint messages. See raw data listener for: quick double-ESC → Rewind,
2541
- // deliberate double-ESC (>1s gap) → stop special mode.
2542
- if (key.name === 'escape' && !fullScreenBrowserOpen && !panelJustClosed) {
2537
+ if (key.name === 'escape' && !fullScreenBrowserOpen && !inputMgr.isSuggestionOpen) {
2543
2538
  const jarvisRunning = jarvisDaemonSession && jarvisDaemonSession.status === 'running';
2544
2539
  const specialMode = jarvisRunning || autonomousMode;
2545
2540
  const normalRunning = agentRunning || sessionMgr.hasBackgroundTasks;
@@ -2691,8 +2686,24 @@ export async function chatCommand(options) {
2691
2686
  // because arrow key \x1b[A could arrive just after a real ESC press
2692
2687
  });
2693
2688
  async function openRewindBrowser() {
2694
- if (agentRunning || fullScreenBrowserOpen)
2689
+ // Don't re-enter if already open
2690
+ if (fullScreenBrowserOpen)
2695
2691
  return;
2692
+ // If an agent is running (main, swarm, or background), stop it first.
2693
+ // Previous behavior silently refused to open Rewind while agent was
2694
+ // running, which made coalesced ESC-ESC bursts feel broken.
2695
+ if (agentRunning || sessionMgr.hasBackgroundTasks) {
2696
+ activity.stop('Stopped');
2697
+ agentController.abort();
2698
+ sessionMgr.abortAll();
2699
+ if (activeSwarm) {
2700
+ activeSwarm.abort();
2701
+ activeSwarm = null;
2702
+ }
2703
+ typeAheadBuffer.length = 0;
2704
+ agentRunning = false;
2705
+ autonomousMode = false;
2706
+ }
2696
2707
  const allCps = checkpointStore.getAll();
2697
2708
  if (allCps.length === 0) {
2698
2709
  renderInfo(chalk.dim('No checkpoints yet \u2014 start chatting to create rewind points.'));
@@ -2713,45 +2724,49 @@ export async function chatCommand(options) {
2713
2724
  process.stdin.removeAllListeners('keypress');
2714
2725
  let didRevertWithMessage = false;
2715
2726
  try {
2716
- const browserResult = await runCheckpointBrowser({
2717
- store: checkpointStore,
2718
- agentHistory,
2719
- simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
2720
- isPaused: false,
2721
- });
2722
- if (browserResult.action === 'revert') {
2723
- const r = browserResult.result;
2724
- process.stdout.write('\n');
2725
- if (r.messagesRemoved > 0)
2726
- renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
2727
- if (r.filesReverted > 0)
2728
- renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
2729
- if (browserResult.messageText) {
2730
- inputMgr.setLine(browserResult.messageText);
2731
- didRevertWithMessage = true;
2727
+ try {
2728
+ const browserResult = await runCheckpointBrowser({
2729
+ store: checkpointStore,
2730
+ agentHistory,
2731
+ simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
2732
+ isPaused: false,
2733
+ });
2734
+ if (browserResult.action === 'revert') {
2735
+ const r = browserResult.result;
2736
+ process.stdout.write('\n');
2737
+ if (r.messagesRemoved > 0)
2738
+ renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
2739
+ if (r.filesReverted > 0)
2740
+ renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
2741
+ if (browserResult.messageText) {
2742
+ inputMgr.setLine(browserResult.messageText);
2743
+ didRevertWithMessage = true;
2744
+ }
2732
2745
  }
2733
2746
  }
2747
+ catch (err) {
2748
+ // Surface the error instead of swallowing it silently (CHECK-CHATFLOW-009).
2749
+ renderError(`Rewind failed: ${err instanceof Error ? err.message : String(err)}`);
2750
+ }
2734
2751
  }
2735
- catch {
2736
- // Browser closed unexpectedly
2737
- }
2738
- // Restore all saved stdin data+keypress listeners
2739
- for (const listener of savedDataListeners) {
2740
- process.stdin.on('data', listener);
2741
- }
2742
- for (const listener of savedKeypressListeners) {
2743
- process.stdin.on('keypress', listener);
2744
- }
2745
- // Clear readline buffer unless a revert populated it with the message text
2746
- if (!didRevertWithMessage) {
2747
- inputMgr.setLine('');
2752
+ finally {
2753
+ // Guaranteed listener restore, even if the browser or revert threw.
2754
+ for (const listener of savedDataListeners) {
2755
+ process.stdin.on('data', listener);
2756
+ }
2757
+ for (const listener of savedKeypressListeners) {
2758
+ process.stdin.on('keypress', listener);
2759
+ }
2760
+ if (!didRevertWithMessage) {
2761
+ inputMgr.setLine('');
2762
+ }
2763
+ fullScreenBrowserOpen = false;
2764
+ chrome.activate();
2765
+ // Drain phantom line events for 500ms (sub-menu shared stdin)
2766
+ drainUntil = Date.now() + 500;
2767
+ rl.resume();
2768
+ showPrompt();
2748
2769
  }
2749
- fullScreenBrowserOpen = false;
2750
- chrome.activate();
2751
- // Drain phantom line events for 500ms (sub-menu shared stdin)
2752
- drainUntil = Date.now() + 500;
2753
- rl.resume();
2754
- showPrompt();
2755
2770
  }
2756
2771
  }
2757
2772
  // === Bracketed Paste helpers ===
@@ -3068,8 +3083,24 @@ export async function chatCommand(options) {
3068
3083
  spiralEngine = await ensureSpiralEngine(newScope);
3069
3084
  }, brainScope, (scope) => getSpiralEngine(scope ?? brainScope), async (action, goal) => {
3070
3085
  if (action === 'stop') {
3071
- // Stop all background sessions + autonomous mode
3086
+ // FIX: CHATFLOW-003 /stop must also interrupt a running main agent.
3087
+ // Previously only background sessions / autonomous mode were stopped,
3088
+ // and a /stop typed during main agent work sat in the type-ahead
3089
+ // queue and fired after the agent finished on its own.
3072
3090
  const running = sessionMgr.running;
3091
+ let stoppedSomething = false;
3092
+ if (agentRunning) {
3093
+ activity.stop('Stopped');
3094
+ agentController.abort();
3095
+ if (activeSwarm) {
3096
+ activeSwarm.abort();
3097
+ activeSwarm = null;
3098
+ }
3099
+ agentRunning = false;
3100
+ typeAheadBuffer.length = 0;
3101
+ renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 Main agent interrupted.'));
3102
+ stoppedSomething = true;
3103
+ }
3073
3104
  if (running.length > 0) {
3074
3105
  for (const s of running) {
3075
3106
  s.abort();
@@ -3077,14 +3108,16 @@ export async function chatCommand(options) {
3077
3108
  }
3078
3109
  autonomousMode = false;
3079
3110
  updateStatusBar();
3111
+ stoppedSomething = true;
3080
3112
  }
3081
- else if (autonomousMode) {
3113
+ if (autonomousMode) {
3082
3114
  autonomousMode = false;
3083
3115
  agentController.abort();
3084
3116
  renderInfo('\u23F9 Stopping autonomous mode...');
3117
+ stoppedSomething = true;
3085
3118
  }
3086
- else {
3087
- renderInfo('No background sessions running.');
3119
+ if (!stoppedSomething) {
3120
+ renderInfo('Nothing running to stop.');
3088
3121
  }
3089
3122
  return;
3090
3123
  }
@@ -3364,18 +3397,20 @@ export async function chatCommand(options) {
3364
3397
  // Start Telegram bot alongside daemon (if configured)
3365
3398
  startTelegramBot();
3366
3399
  (async () => {
3367
- // Create daemon-specific permissions based on Jarvis autonomy level.
3368
- // L3+ = skipPermissions (auto-allow writes, ask for dangerous)
3369
- // L5 = yolo (auto-allow everything including shell commands)
3400
+ // FIX: JARVIS-CRITICAL-2 no longer auto-enable YOLO/skip-permissions
3401
+ // based on autonomy level. Autonomy levels gate WHICH tools Jarvis may
3402
+ // attempt (enforced by core-ethics.assertCanExecute), not whether the
3403
+ // permission prompt fires. Silent auto-approval of writes and shell
3404
+ // commands was a structural safety defect. If a user wants unsupervised
3405
+ // operation, they must explicitly set --yolo / --skip-permissions at
3406
+ // CLI startup; those flags do propagate into daemon permissions below.
3370
3407
  const daemonPermissions = new PermissionManager();
3371
3408
  daemonPermissions.setReadline(rl);
3372
- const aLevel = jarvisAutonomy.getLevel();
3373
- if (aLevel >= 5) {
3409
+ // Inherit ONLY the user's explicit CLI flags, never the autonomy level.
3410
+ if (options.yolo)
3374
3411
  daemonPermissions.setYolo(true);
3375
- }
3376
- else if (aLevel >= 3) {
3412
+ if (options.skipPermissions)
3377
3413
  daemonPermissions.setSkipPermissions(true);
3378
- }
3379
3414
  try {
3380
3415
  await runJarvisDaemon(jarvisQueue, {
3381
3416
  sendMessage: async (prompt) => {
@@ -3744,8 +3779,31 @@ export async function chatCommand(options) {
3744
3779
  }
3745
3780
  }
3746
3781
  agentRunning = false;
3747
- // Keep simple message history for state persistence
3782
+ // Keep simple message history for state persistence.
3783
+ // FIX: CHATFLOW-001 — also persist the assistant's reply, otherwise
3784
+ // saveState() writes a user-only transcript and checkpoint browser shows
3785
+ // empty agent turns. Extract the last assistant text block from the
3786
+ // updated agentHistory.
3748
3787
  messages.push({ role: 'user', content: input });
3788
+ for (let i = agentHistory.length - 1; i >= 0; i--) {
3789
+ const m = agentHistory[i];
3790
+ if (m.role !== 'assistant')
3791
+ continue;
3792
+ let assistantText = '';
3793
+ if (typeof m.content === 'string') {
3794
+ assistantText = m.content;
3795
+ }
3796
+ else if (Array.isArray(m.content)) {
3797
+ assistantText = m.content
3798
+ .filter((b) => b?.type === 'text' && typeof b.text === 'string')
3799
+ .map((b) => b.text)
3800
+ .join('');
3801
+ }
3802
+ if (assistantText.trim()) {
3803
+ messages.push({ role: 'assistant', content: assistantText });
3804
+ }
3805
+ break;
3806
+ }
3749
3807
  // Process any type-ahead input that was buffered during agent work
3750
3808
  // Skip if agent was aborted (ESC already cleared the buffer, but guard against race)
3751
3809
  while (typeAheadBuffer.length > 0 && !agentController.isAborted) {
@@ -3869,26 +3927,32 @@ export async function chatCommand(options) {
3869
3927
  }, PASTE_THRESHOLD_MS);
3870
3928
  });
3871
3929
  // Handle Esc to discard paste buffer
3872
- if (process.stdin.isTTY) {
3873
- const origKeypress = process.stdin.listeners('keypress');
3874
- // Insert paste-cancel before the existing ESC handler
3875
- process.stdin.prependListener('keypress', (_str, key) => {
3876
- if (key?.name === 'escape' && (pasteBuffer.length > 0 || pendingPasteText || inputMgr.hasPasteBlock)) {
3877
- pasteBuffer = [];
3878
- pendingPasteText = null;
3879
- if (pasteTimer) {
3880
- clearTimeout(pasteTimer);
3881
- pasteTimer = null;
3882
- }
3883
- inputMgr.clearPasteBlock();
3884
- showPrompt();
3930
+ // NOTE: paste-cancel is stored in a named const so rl.on('close') can
3931
+ // remove it cleanly. Without removal, re-entering the chat command in
3932
+ // the same process would stack this prependListener, causing duplicate
3933
+ // ESC handling and phantom paste-cancels.
3934
+ const pasteCancelListener = (_str, key) => {
3935
+ if (key?.name === 'escape' && (pasteBuffer.length > 0 || pendingPasteText || inputMgr.hasPasteBlock)) {
3936
+ pasteBuffer = [];
3937
+ pendingPasteText = null;
3938
+ if (pasteTimer) {
3939
+ clearTimeout(pasteTimer);
3940
+ pasteTimer = null;
3885
3941
  }
3886
- });
3942
+ inputMgr.clearPasteBlock();
3943
+ showPrompt();
3944
+ }
3945
+ };
3946
+ if (process.stdin.isTTY) {
3947
+ process.stdin.prependListener('keypress', pasteCancelListener);
3887
3948
  }
3888
3949
  // Type-ahead during LLM streaming is handled by InputManager._renderCurrentLine()
3889
3950
  // (muted path renders dim text with proper cursor positioning via renderInput).
3890
3951
  rl.on('close', async () => {
3891
3952
  clearInterval(footerTimer);
3953
+ if (process.stdin.isTTY) {
3954
+ process.stdin.removeListener('keypress', pasteCancelListener);
3955
+ }
3892
3956
  chrome.deactivate();
3893
3957
  if (spiralEngine) {
3894
3958
  // Persist session buffer (goals, entities, decisions) into spiral brain
@@ -4185,11 +4249,12 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
4185
4249
  renderError('Rate limit reached. Waiting and retrying automatically next time.');
4186
4250
  renderInfo(chalk.dim(' Tip: Use /compact to reduce spiral nodes, or wait a moment before retrying.'));
4187
4251
  }
4188
- else if (errMsg.includes('authentication') || errMsg.includes('401') || errMsg.includes('invalid.*key')) {
4252
+ else if (/authentication|401|invalid[^a-z]*key/i.test(errMsg)) {
4253
+ // FIX: CHATFLOW-004 — use real regex instead of literal "invalid.*key" substring.
4189
4254
  renderError('Authentication failed. Your API key may be invalid or expired.');
4190
4255
  renderInfo(chalk.dim(' Fix: /keys to update your API key.'));
4191
4256
  }
4192
- else if (errMsg.includes('ENOTFOUND') || errMsg.includes('ECONNREFUSED') || errMsg.includes('network')) {
4257
+ else if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|fetch failed|offline/i.test(errMsg)) {
4193
4258
  renderError('Network error — cannot reach the API server.');
4194
4259
  renderInfo(chalk.dim(' Check your internet connection and try again.'));
4195
4260
  }