@compilr-dev/cli 0.6.1 → 0.6.3

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 (64) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +12 -0
  3. package/dist/commands-v2/handlers/core.js +2 -2
  4. package/dist/commands-v2/handlers/index.d.ts +1 -0
  5. package/dist/commands-v2/handlers/index.js +5 -2
  6. package/dist/commands-v2/handlers/perf.d.ts +9 -0
  7. package/dist/commands-v2/handlers/perf.js +66 -0
  8. package/dist/commands-v2/handlers/project.js +2 -3
  9. package/dist/compilr-diff-companion.vsix +0 -0
  10. package/dist/index.js +14 -8
  11. package/dist/repl-v2.js +5 -1
  12. package/dist/session/project-session-manager.js +1 -1
  13. package/dist/slash-autocomplete.js +18 -16
  14. package/dist/tabbed-menu.js +8 -7
  15. package/dist/ui/base/overlay-base.js +2 -1
  16. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +12 -7
  17. package/dist/ui/overlay/impl/config-overlay-v2.js +2 -2
  18. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +12 -8
  19. package/dist/ui/overlay/impl/pending-overlay-v2.js +4 -1
  20. package/dist/ui/overlay-manager.js +7 -6
  21. package/dist/ui/terminal-render-item.js +2 -1
  22. package/dist/ui/terminal-renderer.js +1 -2
  23. package/dist/ui/terminal-ui.js +6 -4
  24. package/dist/ui/terminal.d.ts +9 -0
  25. package/dist/ui/terminal.js +28 -15
  26. package/dist/utils/update-checker.d.ts +6 -1
  27. package/dist/utils/update-checker.js +16 -1
  28. package/package.json +5 -4
  29. package/dist/.tsbuildinfo.app +0 -1
  30. package/dist/.tsbuildinfo.data +0 -1
  31. package/dist/.tsbuildinfo.domain +0 -1
  32. package/dist/.tsbuildinfo.foundation +0 -1
  33. package/dist/guide/guide-content.d.ts +0 -23
  34. package/dist/guide/guide-content.js +0 -196
  35. package/dist/multi-agent/activity.d.ts +0 -21
  36. package/dist/multi-agent/activity.js +0 -34
  37. package/dist/multi-agent/agent-selection.d.ts +0 -55
  38. package/dist/multi-agent/agent-selection.js +0 -90
  39. package/dist/multi-agent/artifacts.d.ts +0 -197
  40. package/dist/multi-agent/artifacts.js +0 -379
  41. package/dist/multi-agent/collision-utils.d.ts +0 -16
  42. package/dist/multi-agent/collision-utils.js +0 -28
  43. package/dist/multi-agent/context-resolver.d.ts +0 -97
  44. package/dist/multi-agent/context-resolver.js +0 -316
  45. package/dist/multi-agent/mention-parser.d.ts +0 -64
  46. package/dist/multi-agent/mention-parser.js +0 -146
  47. package/dist/multi-agent/shared-context.d.ts +0 -293
  48. package/dist/multi-agent/shared-context.js +0 -671
  49. package/dist/multi-agent/skill-requirements.d.ts +0 -66
  50. package/dist/multi-agent/skill-requirements.js +0 -178
  51. package/dist/multi-agent/task-assignment.d.ts +0 -69
  52. package/dist/multi-agent/task-assignment.js +0 -123
  53. package/dist/multi-agent/task-suggestion.d.ts +0 -31
  54. package/dist/multi-agent/task-suggestion.js +0 -72
  55. package/dist/multi-agent/team-agent.d.ts +0 -201
  56. package/dist/multi-agent/team-agent.js +0 -488
  57. package/dist/multi-agent/team.d.ts +0 -286
  58. package/dist/multi-agent/team.js +0 -610
  59. package/dist/multi-agent/tool-config.d.ts +0 -110
  60. package/dist/multi-agent/tool-config.js +0 -661
  61. package/dist/multi-agent/types.d.ts +0 -211
  62. package/dist/multi-agent/types.js +0 -617
  63. package/dist/tools/guide-tool.d.ts +0 -12
  64. package/dist/tools/guide-tool.js +0 -59
package/CHANGELOG.md ADDED
@@ -0,0 +1,104 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@compilr-dev/cli` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ > **Beta notice:** versions in the `0.x` range may contain breaking changes
9
+ > between minors. Read each release entry before upgrading.
10
+
11
+ ---
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.6.2] — 2026-04-25
16
+
17
+ ### Fixed
18
+ - Respect the [`NO_COLOR`](https://no-color.org) standard — `NO_COLOR=1 compilr ...`
19
+ now produces uncolored output instead of being silently overridden by
20
+ `FORCE_COLOR=3`. Restores accessibility for screen readers, low-contrast
21
+ terminals, log redirection, and CI environments.
22
+
23
+ ### Changed
24
+ - Lower minimum supported Node from `>=22.0.0` to `>=20.0.0` — Node 20 is the
25
+ active LTS line and the codebase uses no v22-only APIs. Restores compatibility
26
+ for users still on Node 20 LTS.
27
+
28
+ ### Added
29
+ - Documented opt-outs for the npm registry update check. Set any of the
30
+ following to skip the network call on startup:
31
+ - `COMPILR_NO_UPDATE_CHECK=1` (project-specific)
32
+ - `NO_UPDATE_NOTIFIER=1` (de-facto standard from `update-notifier`)
33
+ - `CI=true` (auto-respected in CI)
34
+ - `CHANGELOG.md` shipped in the published tarball (this file).
35
+
36
+ ### Removed
37
+ - TypeScript incremental build caches (`*.tsbuildinfo`) no longer ship in the
38
+ npm tarball. Tarball is now ~760 KB compressed (previously ~1 MB) — about
39
+ ~660 KB lighter on disk after install.
40
+
41
+ ---
42
+
43
+ ## [0.6.1] — 2026-04-21
44
+
45
+ ### Fixed
46
+ - Resolved `npm audit` vulnerabilities in transitive deps (`hono`, `protobufjs`).
47
+
48
+ ## [0.6.0] — 2026-04-19
49
+
50
+ ### Changed
51
+ - **Major:** CLI now builds on `createCompilrAgent` from `@compilr-dev/sdk`
52
+ (universal agent runtime). All agent setup, tool assembly, capability
53
+ loading, and team coordination flow through the SDK. CLI source dropped
54
+ from ~3.5 K LOC of agent wiring to a thin adapter.
55
+ - Logging is now structured JSON via `@compilr-dev/logger` (Pino) — no more
56
+ raw `console.log` from diagnostic paths. Logs are scrubbed for secrets.
57
+
58
+ ### Added
59
+ - Plan Mode V2 — permission-blocking + tool-based transitions for safer
60
+ multi-step planning workflows. Plan can now be approved/edited from
61
+ inside the CLI.
62
+ - `/app` command — terminal app model viewer overlay (entities, fields,
63
+ relationships, layout, config). Aligned columns, no emojis.
64
+ - Custom agent wizard Step 5 — free-text custom instructions added per agent.
65
+ - Template support in custom agent wizard — `Ctrl+T` to save as template,
66
+ list UI to load existing templates.
67
+ - Guide tool migrated to SDK (`createGuideTool({ environment, additionalEntries })`).
68
+ - Removed two-repo and guided-workflow options from the wizard — both are
69
+ always single-repo + flexible now.
70
+
71
+ ### Fixed
72
+ - Credential encryption now survives container restarts (master key
73
+ derivation hardened).
74
+ - `/app` overlay — aligned columns, layout/config in summary, no emojis.
75
+ - Agent IDs may contain dashes.
76
+
77
+ ## [0.5.x] — 2026-03 → 2026-04
78
+
79
+ Highlights from the 0.5 line:
80
+
81
+ - Migrated more code paths to `@compilr-dev/sdk` (anchors, MCP, permissions,
82
+ SQLite repositories, capability loading).
83
+ - Pin API replaces the older anchor API for project state injection.
84
+ - Multi-terminal session awareness, dynamic capability loading with
85
+ auto-load on filtered tool use, observation masking, tool input
86
+ compaction, and tool result auto-delegation.
87
+ - Multi-LLM support across 9 providers (Claude, OpenAI, Gemini, Groq,
88
+ Fireworks, Together, Perplexity, OpenRouter, Ollama).
89
+ - Multi-agent teams (delegate, delegate_background, $mention routing,
90
+ /team dashboard, background queues).
91
+
92
+ For commit-level detail of the 0.5 line, see git log between v0.5.0 and v0.6.0.
93
+
94
+ ---
95
+
96
+ ## Earlier versions
97
+
98
+ The `0.4.x` and prior series were rapid pre-MVP iterations. See the git
99
+ history for details.
100
+
101
+ [Unreleased]: https://github.com/compilr-dev/cli/compare/v0.6.2...HEAD
102
+ [0.6.2]: https://github.com/compilr-dev/cli/compare/v0.6.1...v0.6.2
103
+ [0.6.1]: https://github.com/compilr-dev/cli/compare/v0.6.0...v0.6.1
104
+ [0.6.0]: https://github.com/compilr-dev/cli/compare/v0.5.17...v0.6.0
package/README.md CHANGED
@@ -217,6 +217,18 @@ export GOOGLE_API_KEY="..." # Google Gemini
217
217
 
218
218
  Environment variables take priority over stored keys.
219
219
 
220
+ ### Behavior Environment Variables
221
+
222
+ The CLI honors several standard environment variables for accessibility,
223
+ privacy, and CI use:
224
+
225
+ | Variable | Effect |
226
+ |---|---|
227
+ | `NO_COLOR=1` | Disable all ANSI colors. Honors the [no-color.org](https://no-color.org) standard. |
228
+ | `COMPILR_NO_UPDATE_CHECK=1` | Skip the npm registry update check at startup. |
229
+ | `NO_UPDATE_NOTIFIER=1` | Same as above. De-facto standard from `update-notifier`. |
230
+ | `CI=true` | Auto-skips the update check (no nag in CI environments). |
231
+
220
232
  ### Project Configuration
221
233
 
222
234
  Create a `COMPILR.md` file in your project root to provide context:
@@ -171,8 +171,8 @@ export const menuCommand = {
171
171
  console.log(s.muted(' Run: npm update -g @compilr-dev/cli'));
172
172
  console.log('');
173
173
  }
174
- // Dashboard loop - returns to dashboard after sub-commands until user exits or continues
175
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
174
+ // Dashboard loop returns to dashboard after sub-commands until user exits or continues
175
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop; exit happens via internal break/return
176
176
  while (true) {
177
177
  const dashboard = new DashboardOverlayV2();
178
178
  const result = await ctx.ui.showOverlay(dashboard);
@@ -22,4 +22,5 @@ export * from './terminals.js';
22
22
  export * from './notifications.js';
23
23
  export * from './mcp.js';
24
24
  export * from './delegations.js';
25
+ export * from './perf.js';
25
26
  export declare const allCommands: CommandHandlerV2[];
@@ -11,7 +11,7 @@ import { debugCommands } from './debug.js';
11
11
  import { sessionCommands } from './session.js';
12
12
  import { resetCommands } from './reset.js';
13
13
  import { teamCommands } from './team.js';
14
- import { gamesCommand } from './games.js';
14
+ // import { gamesCommand } from './games.js'; // Temporarily disabled — Tetris copyright concerns
15
15
  import { tasksCommand } from './tasks.js';
16
16
  import { backgroundCommands } from './background.js';
17
17
  import { filterCommands } from './filter.js';
@@ -21,6 +21,7 @@ import { terminalsCommands } from './terminals.js';
21
21
  import { notificationsCommands } from './notifications.js';
22
22
  import { mcpCommands } from './mcp.js';
23
23
  import { delegationsCommands } from './delegations.js';
24
+ import { perfCommands } from './perf.js';
24
25
  // Re-export individual modules
25
26
  export * from './core.js';
26
27
  export * from './settings.js';
@@ -40,6 +41,7 @@ export * from './terminals.js';
40
41
  export * from './notifications.js';
41
42
  export * from './mcp.js';
42
43
  export * from './delegations.js';
44
+ export * from './perf.js';
43
45
  // All commands combined
44
46
  export const allCommands = [
45
47
  ...coreCommands,
@@ -50,7 +52,7 @@ export const allCommands = [
50
52
  ...sessionCommands,
51
53
  ...resetCommands,
52
54
  ...teamCommands,
53
- gamesCommand,
55
+ // gamesCommand, // Temporarily disabled — Tetris copyright concerns
54
56
  tasksCommand,
55
57
  ...backgroundCommands,
56
58
  ...filterCommands,
@@ -60,4 +62,5 @@ export const allCommands = [
60
62
  ...notificationsCommands,
61
63
  ...mcpCommands,
62
64
  ...delegationsCommands,
65
+ ...perfCommands,
63
66
  ];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * /perf Command
3
+ *
4
+ * Display the most recent CLI startup-performance profile.
5
+ * The profile is captured automatically and written to
6
+ * ~/.compilr-dev/startup-perf.log on every launch.
7
+ */
8
+ import type { CommandHandlerV2 } from '../types.js';
9
+ export declare const perfCommands: CommandHandlerV2[];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * /perf Command
3
+ *
4
+ * Display the most recent CLI startup-performance profile.
5
+ * The profile is captured automatically and written to
6
+ * ~/.compilr-dev/startup-perf.log on every launch.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import pc from 'picocolors';
11
+ import { getDataPath } from '../../settings/paths.js';
12
+ const perfCommand = {
13
+ name: 'perf',
14
+ description: 'Show the latest startup-performance profile',
15
+ details: 'Display the timing breakdown captured during this CLI launch. ' +
16
+ 'Useful for diagnosing slow startups (cold cache, heavy MCP servers, ' +
17
+ 'large project DBs). The profile is overwritten on every launch.',
18
+ examples: [{ code: '/perf', description: 'Show the most recent startup profile' }],
19
+ execute(_args, ctx) {
20
+ const logPath = path.join(getDataPath(), 'startup-perf.log');
21
+ if (!fs.existsSync(logPath)) {
22
+ ctx.ui.print({
23
+ type: 'info',
24
+ message: 'No startup profile found. (Profile is written on the next launch.)',
25
+ });
26
+ return Promise.resolve(true);
27
+ }
28
+ let content;
29
+ try {
30
+ content = fs.readFileSync(logPath, 'utf-8');
31
+ }
32
+ catch (err) {
33
+ ctx.ui.print({
34
+ type: 'warning',
35
+ message: `Could not read startup profile: ${String(err)}`,
36
+ });
37
+ return Promise.resolve(true);
38
+ }
39
+ if (!content.trim()) {
40
+ ctx.ui.print({ type: 'info', message: 'Startup profile is empty.' });
41
+ return Promise.resolve(true);
42
+ }
43
+ // Print the profile, dimming the path footer.
44
+ const lines = content.split('\n');
45
+ const out = [];
46
+ for (const line of lines) {
47
+ if (line.startsWith('===')) {
48
+ out.push(pc.bold(line));
49
+ }
50
+ else if (line.startsWith('Slow steps')) {
51
+ out.push(pc.yellow(line));
52
+ }
53
+ else if (line.startsWith('Total:')) {
54
+ out.push(pc.cyan(line));
55
+ }
56
+ else {
57
+ out.push(line);
58
+ }
59
+ }
60
+ out.push('');
61
+ out.push(pc.dim(` Profile: ${logPath}`));
62
+ ctx.ui.print({ type: 'raw-lines', lines: out });
63
+ return Promise.resolve(true);
64
+ },
65
+ };
66
+ export const perfCommands = [perfCommand];
@@ -178,9 +178,8 @@ export const initCommand = {
178
178
  'Type to enter values in input steps',
179
179
  ],
180
180
  async execute(_args, ctx) {
181
- // Show onboarding wizard (same as first-run experience)
182
- // Loop if user chooses to configure keys
183
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
181
+ // Show onboarding wizard (same as first-run experience).
182
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop; exit happens via internal break/return
184
183
  while (true) {
185
184
  const wizard = new OnboardingWizardOverlayV2();
186
185
  const result = await ctx.ui.showOverlay(wizard);
Binary file
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // Force color output for all chalk-based libraries (cli-highlight, marked-terminal, etc.)
3
- // This must be set BEFORE any imports that use chalk
4
- process.env.FORCE_COLOR = '3';
3
+ // This must be set BEFORE any imports that use chalk.
4
+ // Respect the NO_COLOR standard (https://no-color.org) never override an explicit user opt-out.
5
+ if (!process.env.NO_COLOR) {
6
+ process.env.FORCE_COLOR = '3';
7
+ }
5
8
  /**
6
9
  * @compilr-dev/cli
7
10
  *
@@ -85,7 +88,7 @@ function showHelp() {
85
88
  console.log(`
86
89
  @compilr-dev/cli v${VERSION}
87
90
 
88
- Usage: npm run dev [options]
91
+ Usage: compilr [options]
89
92
 
90
93
  Options:
91
94
  --provider, -p <provider> LLM provider (claude, openai, gemini, ollama)
@@ -100,13 +103,16 @@ Environment Variables:
100
103
  ANTHROPIC_API_KEY Required for Claude provider
101
104
  OPENAI_API_KEY Required for OpenAI provider
102
105
  GOOGLE_AI_API_KEY Required for Gemini provider
106
+ NO_COLOR Disable ANSI colors
107
+ COMPILR_NO_UPDATE_CHECK Skip the npm registry update check at startup
108
+ CI Auto-skips the update check
103
109
 
104
110
  Examples:
105
- npm run dev
106
- npm run dev -- --model claude-sonnet-4-6
107
- npm run dev -- --provider gemini --model gemini-2.0-flash
108
- npm run dev -- --minimal --show-filtering
109
- npm run dev -- --update
111
+ compilr
112
+ compilr --model claude-sonnet-4-6
113
+ compilr --provider gemini --model gemini-2.0-flash
114
+ compilr --minimal --show-filtering
115
+ compilr --update
110
116
  `);
111
117
  }
112
118
  // =============================================================================
package/dist/repl-v2.js CHANGED
@@ -3358,8 +3358,12 @@ export class ReplV2 {
3358
3358
  // Only run main() when executed directly (not when imported)
3359
3359
  if (import.meta.url === `file://${process.argv[1]}`) {
3360
3360
  const repl = new ReplV2();
3361
- // Handle SIGINT
3361
+ // Handle SIGINT — restore raw mode before exit so the parent shell
3362
+ // doesn't inherit a broken terminal state.
3362
3363
  process.on('SIGINT', () => {
3364
+ if (process.stdin.isTTY) {
3365
+ process.stdin.setRawMode(false);
3366
+ }
3363
3367
  console.log('\n\nInterrupted\n');
3364
3368
  process.exit(0);
3365
3369
  });
@@ -387,7 +387,7 @@ export class ProjectSessionManager {
387
387
  else if (Array.isArray(lastUserMsg.content)) {
388
388
  const textBlock = lastUserMsg.content.find((b) => b.type === 'text');
389
389
  if (textBlock?.type === 'text') {
390
- preview = textBlock.text;
390
+ preview = (textBlock).text;
391
391
  }
392
392
  }
393
393
  }
@@ -9,6 +9,7 @@
9
9
  * - Proper wrapping for long input
10
10
  */
11
11
  import pc from 'picocolors';
12
+ import { ttyWrite } from './ui/terminal.js';
12
13
  // ANSI escape codes for terminal control
13
14
  const ANSI = {
14
15
  HIDE_CURSOR: '\x1B[?25l',
@@ -83,7 +84,7 @@ function renderDropdown(state, promptLen, currentLine, totalLines, hasSeparators
83
84
  if (hasSeparators)
84
85
  linesToMoveDown += 1;
85
86
  if (linesToMoveDown > 0) {
86
- process.stdout.write(ANSI.MOVE_DOWN(linesToMoveDown));
87
+ ttyWrite(ANSI.MOVE_DOWN(linesToMoveDown));
87
88
  }
88
89
  process.stdout.write('\n');
89
90
  const visible = state.matches.slice(0, MAX_VISIBLE);
@@ -98,8 +99,8 @@ function renderDropdown(state, promptLen, currentLine, totalLines, hasSeparators
98
99
  // Move back up to cursor position
99
100
  const linesRendered = visible.length;
100
101
  const linesToMoveUp = linesRendered + linesToMoveDown + 1;
101
- process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
102
- process.stdout.write(ANSI.MOVE_TO_COLUMN(promptLen + state.cursorPos + 1));
102
+ ttyWrite(ANSI.MOVE_UP(linesToMoveUp));
103
+ ttyWrite(ANSI.MOVE_TO_COLUMN(promptLen + state.cursorPos + 1));
103
104
  return linesRendered;
104
105
  }
105
106
  /**
@@ -113,16 +114,17 @@ function clearDropdown(dropdownLines, currentLine, totalLines, hasSeparators) {
113
114
  if (hasSeparators)
114
115
  linesToMoveDown += 1;
115
116
  if (linesToMoveDown > 0) {
116
- process.stdout.write(ANSI.MOVE_DOWN(linesToMoveDown));
117
+ ttyWrite(ANSI.MOVE_DOWN(linesToMoveDown));
117
118
  }
118
119
  process.stdout.write('\n');
119
120
  // Clear dropdown lines
120
121
  for (let i = 0; i < dropdownLines; i++) {
121
- process.stdout.write(ANSI.CLEAR_LINE + '\n');
122
+ ttyWrite(ANSI.CLEAR_LINE);
123
+ process.stdout.write('\n');
122
124
  }
123
125
  // Move back up
124
126
  const linesToMoveUp = dropdownLines + linesToMoveDown + 1;
125
- process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
127
+ ttyWrite(ANSI.MOVE_UP(linesToMoveUp));
126
128
  }
127
129
  /**
128
130
  * Create interactive input with autocomplete
@@ -198,13 +200,13 @@ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, g
198
200
  const termWidth = getTerminalWidth();
199
201
  // Clear previous render
200
202
  if (linesAboveCursor > 0) {
201
- process.stdout.write('\r');
202
- process.stdout.write(ANSI.MOVE_UP(linesAboveCursor));
203
+ ttyWrite('\r');
204
+ ttyWrite(ANSI.MOVE_UP(linesAboveCursor));
203
205
  }
204
206
  else {
205
- process.stdout.write('\r');
207
+ ttyWrite('\r');
206
208
  }
207
- process.stdout.write(ANSI.CLEAR_TO_END_OF_SCREEN);
209
+ ttyWrite(ANSI.CLEAR_TO_END_OF_SCREEN);
208
210
  // Render todos
209
211
  renderedTodoLines = renderTodoSection();
210
212
  // Render top separator
@@ -246,10 +248,10 @@ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, g
246
248
  }
247
249
  linesToMoveUp += (currentLinePhysical - 1 - cursorPhysicalRow);
248
250
  if (linesToMoveUp > 0) {
249
- process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
251
+ ttyWrite(ANSI.MOVE_UP(linesToMoveUp));
250
252
  }
251
253
  const cursorCol = (cursorAbsPos % termWidth) + 1;
252
- process.stdout.write(ANSI.MOVE_TO_COLUMN(cursorCol));
254
+ ttyWrite(ANSI.MOVE_TO_COLUMN(cursorCol));
253
255
  // Track cursor position for next render
254
256
  linesAboveCursor = physicalLinesRendered + physicalLinesBeforeCursor + cursorPhysicalRow;
255
257
  }
@@ -339,9 +341,9 @@ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, g
339
341
  dropdownLines = 0;
340
342
  // Clear display
341
343
  if (linesAboveCursor > 0) {
342
- process.stdout.write('\r' + ANSI.MOVE_UP(linesAboveCursor));
344
+ ttyWrite('\r' + ANSI.MOVE_UP(linesAboveCursor));
343
345
  }
344
- process.stdout.write('\r' + ANSI.CLEAR_TO_END_OF_SCREEN);
346
+ ttyWrite('\r' + ANSI.CLEAR_TO_END_OF_SCREEN);
345
347
  // Print clean input
346
348
  for (let i = 0; i < state.lines.length; i++) {
347
349
  const linePrompt = i === 0 ? prompt : pc.dim(' \\ ');
@@ -494,7 +496,7 @@ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, g
494
496
  if (isLeftArrow) {
495
497
  if (state.cursorPos > 0) {
496
498
  state.cursorPos--;
497
- process.stdout.write('\x1B[D');
499
+ ttyWrite('\x1B[D');
498
500
  }
499
501
  else if (state.currentLine > 0) {
500
502
  state.currentLine--;
@@ -507,7 +509,7 @@ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, g
507
509
  if (isRightArrow) {
508
510
  if (state.cursorPos < state.lines[state.currentLine].length) {
509
511
  state.cursorPos++;
510
- process.stdout.write('\x1B[C');
512
+ ttyWrite('\x1B[C');
511
513
  }
512
514
  else if (state.currentLine < state.lines.length - 1) {
513
515
  state.currentLine++;
@@ -7,6 +7,7 @@
7
7
  import pc from 'picocolors';
8
8
  import { getCustomCommandRegistry } from './commands/index.js';
9
9
  import { truncate } from './ui/base/index.js';
10
+ import { ttyWrite } from './ui/terminal.js';
10
11
  // ANSI escape codes
11
12
  const ANSI = {
12
13
  HIDE_CURSOR: '\x1B[?25l',
@@ -201,11 +202,11 @@ function render(state, prevLineCount) {
201
202
  // Clear previous render
202
203
  if (prevLineCount > 0) {
203
204
  // Move to column 1, move up to first line, clear everything below
204
- process.stdout.write('\r');
205
+ ttyWrite('\r');
205
206
  if (prevLineCount > 1) {
206
- process.stdout.write(ANSI.MOVE_UP(prevLineCount - 1));
207
+ ttyWrite(ANSI.MOVE_UP(prevLineCount - 1));
207
208
  }
208
- process.stdout.write(ANSI.CLEAR_TO_END);
209
+ ttyWrite(ANSI.CLEAR_TO_END);
209
210
  }
210
211
  // Render new content
211
212
  process.stdout.write(lines.join('\n'));
@@ -283,7 +284,7 @@ export function showHelpMenu() {
283
284
  // Start on a new line (menu appears below the prompt)
284
285
  process.stdout.write('\n');
285
286
  // Hide cursor during menu display
286
- process.stdout.write(ANSI.HIDE_CURSOR);
287
+ ttyWrite(ANSI.HIDE_CURSOR);
287
288
  // Store original raw mode state
288
289
  const wasRawMode = process.stdin.isRaw;
289
290
  // Enable raw mode
@@ -298,13 +299,13 @@ export function showHelpMenu() {
298
299
  // lineCount lines with lineCount-1 newlines between them, plus the initial \n we added
299
300
  // So cursor is (lineCount-1)+1 = lineCount lines below where "You: /help" ended
300
301
  if (lineCount > 0) {
301
- process.stdout.write(ANSI.MOVE_UP(lineCount));
302
- process.stdout.write(ANSI.CLEAR_TO_END);
302
+ ttyWrite(ANSI.MOVE_UP(lineCount));
303
+ ttyWrite(ANSI.CLEAR_TO_END);
303
304
  }
304
305
  // Print newline so next prompt appears on fresh line
305
306
  process.stdout.write('\n');
306
307
  // Show cursor
307
- process.stdout.write(ANSI.SHOW_CURSOR);
308
+ ttyWrite(ANSI.SHOW_CURSOR);
308
309
  // Restore raw mode to original state
309
310
  if (process.stdin.isTTY && !wasRawMode) {
310
311
  process.stdin.setRawMode(false);
@@ -42,6 +42,7 @@ import * as terminal from '../terminal.js';
42
42
  import { getStyles } from '../../themes/index.js';
43
43
  import { OverlayLifecycle } from './overlay-lifecycle.js';
44
44
  import { debugLog } from '../../utils/debug-log.js';
45
+ import { ttyWrite } from '../terminal.js';
45
46
  // =============================================================================
46
47
  // BaseOverlay Abstract Class
47
48
  // =============================================================================
@@ -150,7 +151,7 @@ export class BaseOverlay {
150
151
  this.state.maxLineCount = 0;
151
152
  // Explicitly move cursor to home and save position
152
153
  // This ensures we're at (0,0) regardless of what clearScreen() did
153
- process.stdout.write('\x1b[H');
154
+ ttyWrite('\x1b[H');
154
155
  terminal.saveCursor();
155
156
  }
156
157
  // ===========================================================================
@@ -14,6 +14,7 @@ import { markedTerminal } from 'marked-terminal';
14
14
  import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
15
15
  import { getCurrentTheme } from '../../../themes/index.js';
16
16
  import * as terminal from '../../terminal.js';
17
+ import { ttyWrite } from '../../terminal.js';
17
18
  import { ARTIFACT_TYPE_LABELS } from '../../constants/labels.js';
18
19
  // =============================================================================
19
20
  // Alternate Screen Buffer Management
@@ -22,14 +23,14 @@ let inAlternateScreen = false;
22
23
  function enterAlternateScreen() {
23
24
  if (inAlternateScreen)
24
25
  return;
25
- process.stdout.write('\x1b[?1049h'); // Switch to alternate screen
26
- process.stdout.write('\x1b[H'); // Move cursor to home
26
+ ttyWrite('\x1b[?1049h'); // Switch to alternate screen
27
+ ttyWrite('\x1b[H'); // Move cursor to home
27
28
  inAlternateScreen = true;
28
29
  }
29
30
  function exitAlternateScreen() {
30
31
  if (!inAlternateScreen)
31
32
  return;
32
- process.stdout.write('\x1b[?1049l'); // Switch back to main screen
33
+ ttyWrite('\x1b[?1049l'); // Switch back to main screen
33
34
  inAlternateScreen = false;
34
35
  }
35
36
  // Ensure we exit alternate screen on process termination
@@ -42,10 +43,14 @@ function setupAlternateScreenCleanup() {
42
43
  cleanup();
43
44
  process.exit(130);
44
45
  });
45
- process.on('SIGTERM', () => {
46
- cleanup();
47
- process.exit(143);
48
- });
46
+ // Windows doesn't deliver SIGTERM Node silently ignores the listener
47
+ // there, but we add the platform guard for clarity.
48
+ if (process.platform !== 'win32') {
49
+ process.on('SIGTERM', () => {
50
+ cleanup();
51
+ process.exit(143);
52
+ });
53
+ }
49
54
  }
50
55
  setupAlternateScreenCleanup();
51
56
  // =============================================================================
@@ -611,9 +611,9 @@ export class ConfigOverlayV2 extends BaseOverlayV2 {
611
611
  */
612
612
  toggleOrCycleItem() {
613
613
  const item = this.state.configItems[this.state.selectedItem];
614
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
614
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- selectedItem can briefly point past array end during async re-render
615
615
  if (!item)
616
- return null; // Defensive check
616
+ return null;
617
617
  if (item.type === 'boolean') {
618
618
  const newValue = !item.value;
619
619
  item.value = newValue;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import chalk from 'chalk';
15
15
  import hljs from 'highlight.js';
16
+ import { ttyWrite } from '../../terminal.js';
16
17
  // =============================================================================
17
18
  // Alternate Screen Buffer Management
18
19
  // =============================================================================
@@ -20,14 +21,14 @@ let inAlternateScreen = false;
20
21
  function enterAlternateScreen() {
21
22
  if (inAlternateScreen)
22
23
  return;
23
- process.stdout.write('\x1b[?1049h'); // Switch to alternate screen
24
- process.stdout.write('\x1b[H'); // Move cursor to home
24
+ ttyWrite('\x1b[?1049h'); // Switch to alternate screen
25
+ ttyWrite('\x1b[H'); // Move cursor to home
25
26
  inAlternateScreen = true;
26
27
  }
27
28
  function exitAlternateScreen() {
28
29
  if (!inAlternateScreen)
29
30
  return;
30
- process.stdout.write('\x1b[?1049l'); // Switch back to main screen
31
+ ttyWrite('\x1b[?1049l'); // Switch back to main screen
31
32
  inAlternateScreen = false;
32
33
  }
33
34
  // Ensure we exit alternate screen on process termination
@@ -42,11 +43,14 @@ function setupAlternateScreenCleanup() {
42
43
  cleanup();
43
44
  process.exit(130);
44
45
  });
45
- // Handle termination
46
- process.on('SIGTERM', () => {
47
- cleanup();
48
- process.exit(143);
49
- });
46
+ // Handle termination (Windows doesn't deliver SIGTERM — Node silently
47
+ // ignores the listener there, but we add the platform guard for clarity)
48
+ if (process.platform !== 'win32') {
49
+ process.on('SIGTERM', () => {
50
+ cleanup();
51
+ process.exit(143);
52
+ });
53
+ }
50
54
  // Handle uncaught exceptions
51
55
  process.on('uncaughtException', (err) => {
52
56
  cleanup();
@@ -203,7 +203,10 @@ export class PendingOverlayV2 extends BaseOverlayV2 {
203
203
  const agentLabel = `$${req.agentId}`;
204
204
  const ctx = req.context;
205
205
  const filePath = ctx?.input
206
- ? (ctx.input.filePath ?? ctx.input.file_path ?? ctx.input.path ?? '')
206
+ ? (() => {
207
+ const input = ctx.input;
208
+ return (input.filePath ?? input.file_path ?? input.path ?? '');
209
+ })()
207
210
  : '';
208
211
  const fileName = filePath.split('/').pop() || filePath;
209
212
  // Header