mstro-app 0.2.0 → 0.3.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 (114) hide show
  1. package/PRIVACY.md +126 -0
  2. package/README.md +24 -23
  3. package/bin/commands/login.js +79 -49
  4. package/bin/mstro.js +240 -37
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +133 -27
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +23 -0
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
  12. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  13. package/dist/server/cli/headless/stall-assessor.js +20 -1
  14. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  15. package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
  16. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  17. package/dist/server/cli/headless/tool-watchdog.js +30 -24
  18. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  19. package/dist/server/cli/headless/types.d.ts +19 -1
  20. package/dist/server/cli/headless/types.d.ts.map +1 -1
  21. package/dist/server/cli/improvisation-session-manager.d.ts +28 -1
  22. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  23. package/dist/server/cli/improvisation-session-manager.js +221 -29
  24. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  25. package/dist/server/index.js +0 -3
  26. package/dist/server/index.js.map +1 -1
  27. package/dist/server/services/analytics.d.ts.map +1 -1
  28. package/dist/server/services/analytics.js +13 -1
  29. package/dist/server/services/analytics.js.map +1 -1
  30. package/dist/server/services/platform.d.ts.map +1 -1
  31. package/dist/server/services/platform.js +13 -1
  32. package/dist/server/services/platform.js.map +1 -1
  33. package/dist/server/services/terminal/pty-manager.d.ts +2 -0
  34. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  35. package/dist/server/services/terminal/pty-manager.js +50 -3
  36. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  37. package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
  38. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
  39. package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
  40. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
  41. package/dist/server/services/websocket/git-handlers.d.ts +36 -0
  42. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
  43. package/dist/server/services/websocket/git-handlers.js +797 -0
  44. package/dist/server/services/websocket/git-handlers.js.map +1 -0
  45. package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
  46. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
  47. package/dist/server/services/websocket/git-pr-handlers.js +299 -0
  48. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
  49. package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
  50. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
  51. package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
  52. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
  53. package/dist/server/services/websocket/handler-context.d.ts +32 -0
  54. package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
  55. package/dist/server/services/websocket/handler-context.js +4 -0
  56. package/dist/server/services/websocket/handler-context.js.map +1 -0
  57. package/dist/server/services/websocket/handler.d.ts +27 -359
  58. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  59. package/dist/server/services/websocket/handler.js +67 -2328
  60. package/dist/server/services/websocket/handler.js.map +1 -1
  61. package/dist/server/services/websocket/index.d.ts +1 -1
  62. package/dist/server/services/websocket/index.d.ts.map +1 -1
  63. package/dist/server/services/websocket/index.js.map +1 -1
  64. package/dist/server/services/websocket/session-handlers.d.ts +10 -0
  65. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
  66. package/dist/server/services/websocket/session-handlers.js +507 -0
  67. package/dist/server/services/websocket/session-handlers.js.map +1 -0
  68. package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
  69. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
  70. package/dist/server/services/websocket/settings-handlers.js +125 -0
  71. package/dist/server/services/websocket/settings-handlers.js.map +1 -0
  72. package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
  73. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
  74. package/dist/server/services/websocket/tab-handlers.js +131 -0
  75. package/dist/server/services/websocket/tab-handlers.js.map +1 -0
  76. package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
  77. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
  78. package/dist/server/services/websocket/terminal-handlers.js +220 -0
  79. package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
  80. package/dist/server/services/websocket/types.d.ts +63 -2
  81. package/dist/server/services/websocket/types.d.ts.map +1 -1
  82. package/package.json +4 -2
  83. package/server/README.md +176 -159
  84. package/server/cli/headless/claude-invoker.ts +155 -31
  85. package/server/cli/headless/output-utils.test.ts +225 -0
  86. package/server/cli/headless/runner.ts +25 -0
  87. package/server/cli/headless/stall-assessor.test.ts +165 -0
  88. package/server/cli/headless/stall-assessor.ts +25 -0
  89. package/server/cli/headless/tool-watchdog.test.ts +429 -0
  90. package/server/cli/headless/tool-watchdog.ts +33 -25
  91. package/server/cli/headless/types.ts +10 -1
  92. package/server/cli/improvisation-session-manager.ts +277 -30
  93. package/server/index.ts +0 -4
  94. package/server/mcp/README.md +59 -67
  95. package/server/mcp/bouncer-integration.test.ts +161 -0
  96. package/server/mcp/security-patterns.test.ts +258 -0
  97. package/server/services/analytics.ts +13 -1
  98. package/server/services/platform.ts +12 -1
  99. package/server/services/terminal/pty-manager.ts +53 -3
  100. package/server/services/websocket/autocomplete.test.ts +194 -0
  101. package/server/services/websocket/file-explorer-handlers.ts +587 -0
  102. package/server/services/websocket/git-handlers.ts +924 -0
  103. package/server/services/websocket/git-pr-handlers.ts +363 -0
  104. package/server/services/websocket/git-worktree-handlers.ts +403 -0
  105. package/server/services/websocket/handler-context.ts +44 -0
  106. package/server/services/websocket/handler.test.ts +1 -1
  107. package/server/services/websocket/handler.ts +83 -2678
  108. package/server/services/websocket/index.ts +1 -1
  109. package/server/services/websocket/session-handlers.ts +574 -0
  110. package/server/services/websocket/settings-handlers.ts +150 -0
  111. package/server/services/websocket/tab-handlers.ts +150 -0
  112. package/server/services/websocket/terminal-handlers.ts +277 -0
  113. package/server/services/websocket/types.ts +135 -0
  114. package/bin/release.sh +0 -110
package/bin/mstro.js CHANGED
@@ -8,8 +8,8 @@
8
8
  * Main entry point for the Mstro AI assistant.
9
9
  *
10
10
  * Usage:
11
- * mstro # Start Mstro (auto-finds available port)
12
- * mstro login # Authenticate this device
11
+ * mstro # Start Mstro (logs in automatically if needed)
12
+ * mstro login # Re-authenticate this device
13
13
  * mstro logout # Sign out
14
14
  * mstro whoami # Show current user
15
15
  * mstro status # Show connection status
@@ -35,10 +35,27 @@ const CLIENT_ROOT = resolve(__dirname, '..');
35
35
  const pkg = JSON.parse(readFileSync(join(CLIENT_ROOT, 'package.json'), 'utf-8'));
36
36
 
37
37
  // Check for updates (runs async in background, notifies on next run)
38
+ // update-notifier initializes lastUpdateCheck to Date.now(), which means the
39
+ // first check won't happen until 24h after install. We detect first-run by
40
+ // checking if the configstore has never stored an update result, and if so
41
+ // reset the timestamp to force an immediate background check.
38
42
  const notifier = updateNotifier({
39
43
  pkg,
40
44
  updateCheckInterval: 1000 * 60 * 60 * 24 // Check daily
41
45
  });
46
+ try {
47
+ if (notifier.config && !notifier.config.has('update') && !notifier.update) {
48
+ const lastCheck = notifier.config.get('lastUpdateCheck');
49
+ // If lastUpdateCheck was just set (within the last 30s), this is a fresh
50
+ // configstore — reset it to 0 so the library spawns a check immediately.
51
+ if (lastCheck && (Date.now() - lastCheck) < 30_000) {
52
+ notifier.config.set('lastUpdateCheck', 0);
53
+ notifier.check();
54
+ }
55
+ }
56
+ } catch {
57
+ // Non-critical — don't let update check logic crash the CLI
58
+ }
42
59
 
43
60
  // Capture the user's original working directory before any cwd changes
44
61
  const USER_CWD = process.cwd();
@@ -49,6 +66,7 @@ const MSTRO_FIRST_RUN_FLAG = join(MSTRO_CONFIG_DIR, '.configured');
49
66
  const CLAUDE_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
50
67
  const CLAUDE_HOOKS_DIR = join(homedir(), '.claude', 'hooks');
51
68
  const BOUNCER_HOOK_FILE = join(CLAUDE_HOOKS_DIR, 'bouncer.sh');
69
+ const PTY_SETUP_DISMISSED_FLAG = join(MSTRO_CONFIG_DIR, '.pty-setup-dismissed');
52
70
 
53
71
  /**
54
72
  * Mark Mstro as configured by writing the first-run flag file
@@ -193,18 +211,163 @@ async function promptBouncerSetup() {
193
211
  }
194
212
  }
195
213
 
214
+ /**
215
+ * Check if node-pty native module is loadable
216
+ */
217
+ async function isNodePtyAvailable() {
218
+ try {
219
+ await import('node-pty');
220
+ return true;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Check if user has dismissed the pty setup prompt
228
+ */
229
+ function hasUserDismissedPtySetup() {
230
+ return existsSync(PTY_SETUP_DISMISSED_FLAG);
231
+ }
232
+
233
+ /**
234
+ * Mark pty setup prompt as dismissed
235
+ */
236
+ function markPtySetupDismissed() {
237
+ if (!existsSync(MSTRO_CONFIG_DIR)) {
238
+ mkdirSync(MSTRO_CONFIG_DIR, { recursive: true, mode: 0o700 });
239
+ }
240
+ writeFileSync(PTY_SETUP_DISMISSED_FLAG, new Date().toISOString());
241
+ }
242
+
243
+ /**
244
+ * Show a one-line warning that node-pty is not available
245
+ */
246
+ function showPtyWarning() {
247
+ log(' Terminal support not available. Run: mstro setup-terminal', colors.dim);
248
+ }
249
+
250
+ /**
251
+ * Get platform-specific build tool instructions
252
+ */
253
+ function getPtyBuildInstructions() {
254
+ const os = process.platform;
255
+ if (os === 'darwin') {
256
+ return ' Install Xcode Command Line Tools: xcode-select --install';
257
+ }
258
+ if (os === 'win32') {
259
+ return ' Install Windows Build Tools: npm install -g windows-build-tools';
260
+ }
261
+ return ' Debian/Ubuntu: sudo apt install build-essential python3\n' +
262
+ ' Fedora/RHEL: sudo dnf install gcc-c++ make python3\n' +
263
+ ' Arch: sudo pacman -S base-devel python';
264
+ }
265
+
266
+ /**
267
+ * Attempt to rebuild/install node-pty from CLIENT_ROOT
268
+ * Returns true if npm command succeeded, false otherwise
269
+ */
270
+ function attemptPtyRebuild() {
271
+ return new Promise((resolve) => {
272
+ const nodePtyDir = join(CLIENT_ROOT, 'node_modules', 'node-pty');
273
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
274
+ const command = existsSync(nodePtyDir) ? 'rebuild' : 'install';
275
+ const args = command === 'rebuild'
276
+ ? ['rebuild', 'node-pty']
277
+ : ['install', 'node-pty', '--no-save'];
278
+
279
+ log(`\n ${command === 'rebuild' ? 'Rebuilding' : 'Installing'} node-pty...`, colors.dim);
280
+
281
+ const child = spawn(npmCmd, args, {
282
+ cwd: CLIENT_ROOT,
283
+ stdio: 'inherit',
284
+ });
285
+
286
+ child.on('error', (err) => {
287
+ log(` Error: ${err.message}`, colors.red);
288
+ resolve(false);
289
+ });
290
+
291
+ child.on('exit', (code) => {
292
+ resolve(code === 0);
293
+ });
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Prompt user to set up node-pty for terminal support
299
+ * Returns: 'configure' | 'skip' | 'never'
300
+ */
301
+ async function promptPtySetup() {
302
+ log('\n Terminal Support\n', colors.bold + colors.cyan);
303
+ log(' Mstro includes a web terminal that lets you open a shell', colors.dim);
304
+ log(' directly in your browser. This requires compiling a native module (node-pty).\n', colors.dim);
305
+
306
+ const isInteractive = process.stdin.isTTY;
307
+
308
+ if (!isInteractive) {
309
+ log(' Non-interactive mode: skipping terminal setup.', colors.yellow);
310
+ log(' Run "mstro setup-terminal" to enable terminal support.\n', colors.dim);
311
+ return 'skip';
312
+ }
313
+
314
+ log(' Set up terminal support now?', colors.bold);
315
+ log(' [Y] Yes, compile now (requires build tools)', colors.dim);
316
+ log(' [n] Not now (ask again next time)', colors.dim);
317
+ log(' [d] Don\'t show this again\n', colors.dim);
318
+
319
+ const answer = await prompt(' Your choice [Y/n/d]: ');
320
+ const choice = answer.toLowerCase();
321
+
322
+ if (choice === '' || choice === 'y' || choice === 'yes') {
323
+ return 'configure';
324
+ }
325
+ if (choice === 'd' || choice === 'dont' || choice === "don't") {
326
+ log('\n Got it! You can set up later with: mstro setup-terminal\n', colors.dim);
327
+ markPtySetupDismissed();
328
+ return 'never';
329
+ }
330
+ log('\n Skipping for now. Will ask again next time.', colors.yellow);
331
+ log(' You can also set up with: mstro setup-terminal\n', colors.dim);
332
+ return 'skip';
333
+ }
334
+
335
+ /**
336
+ * Run the pty rebuild and show results
337
+ */
338
+ async function runPtySetup() {
339
+ const success = await attemptPtyRebuild();
340
+
341
+ if (success) {
342
+ const available = await isNodePtyAvailable();
343
+ if (available) {
344
+ log('\n Terminal support enabled successfully!\n', colors.bold + colors.green);
345
+ return true;
346
+ }
347
+ log('\n node-pty installed but failed to load.', colors.red);
348
+ }
349
+
350
+ log('\n Could not compile node-pty automatically.\n', colors.yellow);
351
+ log(' You may need to install build tools first:\n', colors.bold);
352
+ log(getPtyBuildInstructions(), colors.dim);
353
+ log('');
354
+ log(' After installing build tools, run: mstro setup-terminal\n', colors.dim);
355
+ return false;
356
+ }
357
+
196
358
  function showHelp() {
197
- log('\n Mstro - No-code AI Workspace\n', colors.bold + colors.cyan);
198
- log(' Run Claude Code workflows from your laptop, cloud VM, or any machine.\n', colors.dim);
359
+ log('\n Mstro - Run Claude Code from any browser\n', colors.bold + colors.cyan);
360
+ log(' Streams live Claude Code sessions from your machine to mstro.app.\n', colors.dim);
199
361
  log(' Usage:', colors.bold);
200
- log(' mstro Start Mstro (auto-finds available port)', colors.dim);
201
- log(' mstro login Authenticate this device with mstro.app', colors.dim);
362
+ log(' mstro Start Mstro (logs in automatically if needed)', colors.dim);
363
+ log(' mstro login Re-authenticate this device with mstro.app', colors.dim);
202
364
  log(' mstro logout Sign out of mstro.app', colors.dim);
203
365
  log(' mstro whoami Show current user and device info', colors.dim);
204
366
  log(' mstro status Show connection and auth status', colors.dim);
205
367
  log(' mstro telemetry [on|off] Enable/disable anonymous telemetry', colors.dim);
206
368
  log(' mstro -p 4105 Start on specific port (overrides auto port)', colors.dim);
207
369
  log(' mstro configure-hooks Configure Claude Code security hooks', colors.dim);
370
+ log(' mstro setup-terminal Enable web terminal (compiles native module)', colors.dim);
208
371
  log(' mstro --version Show version number', colors.dim);
209
372
  log(' mstro --help Show this help message', colors.dim);
210
373
  log('');
@@ -214,7 +377,7 @@ function showHelp() {
214
377
  log(' --verbose, -v Enable verbose output', colors.dim);
215
378
  log('');
216
379
  log(' Authentication:', colors.bold);
217
- log(' Run "mstro login" to connect this device to your mstro.app account.', colors.dim);
380
+ log(' Running "mstro" will prompt you to log in automatically if needed.', colors.dim);
218
381
  log(' Once logged in, machines sync automatically with your web dashboard.', colors.dim);
219
382
  log('');
220
383
  log(' Security:', colors.bold);
@@ -321,14 +484,18 @@ function parsePort(args) {
321
484
  * Show update notification if available
322
485
  */
323
486
  function showUpdateNotification() {
324
- if (notifier.update && semverGt(notifier.update.latest, notifier.update.current)) {
325
- const { current, latest, type } = notifier.update;
326
- const updateCmd = 'npm i -g mstro@latest';
327
-
328
- log('');
329
- log(` ${colors.yellow}Update available:${colors.reset} ${colors.dim}${current}${colors.reset} → ${colors.green}${latest}${colors.reset} ${colors.dim}(${type})${colors.reset}`);
330
- log(` Run: ${colors.cyan}${updateCmd}${colors.reset}`);
331
- log('');
487
+ try {
488
+ if (notifier.update && semverGt(notifier.update.latest, notifier.update.current)) {
489
+ const { current, latest, type } = notifier.update;
490
+ const updateCmd = `npm i -g ${pkg.name}`;
491
+
492
+ log('');
493
+ log(` ${colors.yellow}Update available:${colors.reset} ${colors.dim}${current}${colors.reset} → ${colors.green}${latest}${colors.reset} ${colors.dim}(${type})${colors.reset}`);
494
+ log(` Run: ${colors.cyan}${updateCmd}${colors.reset}`);
495
+ log('');
496
+ }
497
+ } catch {
498
+ // Don't let a corrupted cache or invalid semver crash the CLI
332
499
  }
333
500
  }
334
501
 
@@ -349,36 +516,61 @@ function isLoggedIn() {
349
516
  }
350
517
 
351
518
  /**
352
- * Show login required message
519
+ * Auto-login if not authenticated. Exits on failure.
353
520
  */
354
- function showLoginRequired() {
355
- log('\n Authentication required', colors.bold + colors.yellow);
356
- log('');
357
- log(' You must be logged in to use mstro.', colors.dim);
358
- log(' Run "mstro login" to authenticate this device.', colors.dim);
359
- log('');
521
+ async function ensureLoggedIn() {
522
+ if (isLoggedIn()) return;
523
+ log('\n Not logged in — starting authentication...\n', colors.bold + colors.cyan);
524
+ try {
525
+ const { login } = await import('./commands/login.js');
526
+ await login(args, { inline: true });
527
+ } catch (err) {
528
+ log(`\n Login failed: ${err.message}`, colors.red);
529
+ log(' Run "mstro login" to try again.\n', colors.dim);
530
+ process.exit(1);
531
+ }
360
532
  }
361
533
 
362
534
  /**
363
- * Check if node-pty is loadable (native module compiled correctly)
535
+ * Prompt for bouncer setup if not configured
536
+ * Returns true if runConfigureHooks was called (async exit path)
364
537
  */
365
- async function startServer(envOverrides) {
366
- if (!isLoggedIn()) {
367
- showLoginRequired();
368
- process.exit(1);
538
+ async function ensureBouncerSetup() {
539
+ if (isBouncerConfigured()) return false;
540
+ if (hasUserDismissedSetup()) {
541
+ showBouncerWarning();
542
+ return false;
369
543
  }
544
+ const choice = await promptBouncerSetup();
545
+ if (choice === 'configure') {
546
+ runConfigureHooks(true);
547
+ return true;
548
+ }
549
+ return false;
550
+ }
370
551
 
371
- if (!isBouncerConfigured()) {
372
- if (hasUserDismissedSetup()) {
373
- showBouncerWarning();
374
- } else {
375
- const choice = await promptBouncerSetup();
376
- if (choice === 'configure') {
377
- runConfigureHooks(true);
378
- return;
379
- }
380
- }
552
+ /**
553
+ * Prompt for node-pty setup if not available
554
+ */
555
+ async function ensurePtySetup() {
556
+ const ptyAvailable = await isNodePtyAvailable();
557
+ if (ptyAvailable) return;
558
+ if (hasUserDismissedPtySetup()) {
559
+ showPtyWarning();
560
+ return;
561
+ }
562
+ const choice = await promptPtySetup();
563
+ if (choice === 'configure') {
564
+ await runPtySetup();
381
565
  }
566
+ }
567
+
568
+ async function startServer(envOverrides) {
569
+ await ensureLoggedIn();
570
+
571
+ if (await ensureBouncerSetup()) return;
572
+
573
+ await ensurePtySetup();
382
574
 
383
575
  showUpdateNotification();
384
576
  runNpmScript('start', [], envOverrides);
@@ -417,6 +609,16 @@ async function main() {
417
609
  await telemetry(args.slice(args.indexOf('telemetry') + 1));
418
610
  }],
419
611
  ['configure-hooks', () => runConfigureHooks(false)],
612
+ ['setup-terminal', async () => {
613
+ log('\n Mstro Terminal Setup\n', colors.bold + colors.cyan);
614
+ const alreadyAvailable = await isNodePtyAvailable();
615
+ if (alreadyAvailable) {
616
+ log(' node-pty is already available. Terminal support is enabled!\n', colors.green);
617
+ return;
618
+ }
619
+ const success = await runPtySetup();
620
+ process.exit(success ? 0 : 1);
621
+ }],
420
622
  ]);
421
623
 
422
624
  // Flag-based commands
@@ -441,6 +643,7 @@ async function main() {
441
643
  const handler = subcommand ? commands.get(subcommand) : undefined;
442
644
  if (handler) {
443
645
  await handler();
646
+ showUpdateNotification();
444
647
  return;
445
648
  }
446
649
 
@@ -1 +1 @@
1
- {"version":3,"file":"claude-invoker.d.ts","sourceRoot":"","sources":["../../../../server/cli/headless/claude-invoker.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAO9D,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EAGvB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC7C;AAm5BD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CA0I1B"}
1
+ {"version":3,"file":"claude-invoker.d.ts","sourceRoot":"","sources":["../../../../server/cli/headless/claude-invoker.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAO9D,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EAGvB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC7C;AAmgCD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CA0H1B"}
@@ -12,6 +12,15 @@ import { detectErrorInStderr, } from './output-utils.js';
12
12
  import { buildMultimodalMessage } from './prompt-utils.js';
13
13
  import { assessStall, assessToolTimeout, classifyError } from './stall-assessor.js';
14
14
  import { ToolWatchdog } from './tool-watchdog.js';
15
+ // ========== Signal Helpers ==========
16
+ /** Map a Node.js signal name to its numeric value for exit code computation */
17
+ function signalToNumber(signal) {
18
+ const map = {
19
+ SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6,
20
+ SIGKILL: 9, SIGTERM: 15, SIGUSR1: 10, SIGUSR2: 12,
21
+ };
22
+ return map[signal];
23
+ }
15
24
  // ========== Stall Detection Helpers ==========
16
25
  /** Summarize a tool's input for stall assessment context */
17
26
  function summarizeToolInput(input) {
@@ -316,6 +325,8 @@ function handleToolComplete(event, ctx) {
316
325
  toolInput: completeInput,
317
326
  startTime: toolBuffer.startTime
318
327
  });
328
+ // Clean up the input buffer — it's no longer needed after accumulation
329
+ ctx.toolInputBuffers.delete(index);
319
330
  if (ctx.config.toolUseCallback) {
320
331
  ctx.config.toolUseCallback({
321
332
  type: 'tool_complete',
@@ -326,6 +337,77 @@ function handleToolComplete(event, ctx) {
326
337
  });
327
338
  }
328
339
  }
340
+ /** Accumulate input tokens from a message_start event. Returns true if any tokens were added. */
341
+ function handleMessageStartTokens(event, ctx) {
342
+ if (event.type !== 'message_start' || !event.message?.usage)
343
+ return false;
344
+ const usage = event.message.usage;
345
+ ctx.currentStepOutputTokens = 0;
346
+ let changed = false;
347
+ if (typeof usage.input_tokens === 'number') {
348
+ ctx.apiTokenUsage.inputTokens += usage.input_tokens;
349
+ changed = true;
350
+ }
351
+ if (typeof usage.cache_creation_input_tokens === 'number') {
352
+ ctx.apiTokenUsage.inputTokens += usage.cache_creation_input_tokens;
353
+ changed = true;
354
+ }
355
+ if (typeof usage.cache_read_input_tokens === 'number') {
356
+ ctx.apiTokenUsage.inputTokens += usage.cache_read_input_tokens;
357
+ changed = true;
358
+ }
359
+ verboseLog(ctx.config.verbose, `[TOKENS] message_start: input=${usage.input_tokens ?? 0} cache_create=${usage.cache_creation_input_tokens ?? 0} cache_read=${usage.cache_read_input_tokens ?? 0} → total_input=${ctx.apiTokenUsage.inputTokens}`);
360
+ return changed;
361
+ }
362
+ /** Accumulate output tokens from a message_delta event. Returns true if any tokens were added.
363
+ * message_delta carries CUMULATIVE output token count for the current step.
364
+ * Per Anthropic docs: "The token counts shown in the usage field of the
365
+ * message_delta event are cumulative" and there can be "one or more message_delta
366
+ * events" per message. We track the delta from the previous value to avoid
367
+ * double-counting when multiple message_delta events fire per step. */
368
+ function handleMessageDeltaTokens(event, ctx) {
369
+ if (event.type !== 'message_delta' || !event.usage)
370
+ return false;
371
+ if (typeof event.usage.output_tokens !== 'number')
372
+ return false;
373
+ const increment = event.usage.output_tokens - ctx.currentStepOutputTokens;
374
+ verboseLog(ctx.config.verbose, `[TOKENS] message_delta: output=${event.usage.output_tokens} (step_prev=${ctx.currentStepOutputTokens} increment=${increment}) → total_output=${ctx.apiTokenUsage.outputTokens + Math.max(increment, 0)}`);
375
+ if (increment <= 0)
376
+ return false;
377
+ ctx.apiTokenUsage.outputTokens += increment;
378
+ ctx.currentStepOutputTokens = event.usage.output_tokens;
379
+ return true;
380
+ }
381
+ function handleTokenUsage(event, ctx) {
382
+ const changed = handleMessageStartTokens(event, ctx) || handleMessageDeltaTokens(event, ctx);
383
+ if (changed) {
384
+ ctx.lastTokenActivityTime = Date.now();
385
+ ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
386
+ }
387
+ }
388
+ /**
389
+ * Extract definitive token usage from the result event emitted at the end of a Claude session.
390
+ * The result event's `usage` field contains the authoritative total — it overrides any
391
+ * accumulated stream-based counts which may be incomplete (e.g., when extended thinking
392
+ * suppresses stream_event emissions).
393
+ */
394
+ function handleResultTokenUsage(parsed, ctx) {
395
+ if (!parsed.usage)
396
+ return;
397
+ const u = parsed.usage;
398
+ const input = (typeof u.input_tokens === 'number' ? u.input_tokens : 0)
399
+ + (typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : 0)
400
+ + (typeof u.cache_read_input_tokens === 'number' ? u.cache_read_input_tokens : 0);
401
+ const output = typeof u.output_tokens === 'number' ? u.output_tokens : 0;
402
+ if (input > 0 || output > 0) {
403
+ verboseLog(ctx.config.verbose, `[TOKENS] Result event usage: input=${input} output=${output} ` +
404
+ `(stream accumulated: input=${ctx.apiTokenUsage.inputTokens} output=${ctx.apiTokenUsage.outputTokens})`);
405
+ // Replace with authoritative counts from the result event
406
+ ctx.apiTokenUsage = { inputTokens: input, outputTokens: output };
407
+ ctx.lastTokenActivityTime = Date.now();
408
+ ctx.config.tokenUsageCallback?.({ ...ctx.apiTokenUsage });
409
+ }
410
+ }
329
411
  function handleToolResult(parsed, ctx) {
330
412
  if (parsed.type !== 'user' || !parsed.message?.content) {
331
413
  return;
@@ -373,11 +455,14 @@ function processStreamEvent(parsed, ctx) {
373
455
  ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_ERROR]] ${errorMessage}\n`);
374
456
  return;
375
457
  }
376
- // Handle result events that contain error info
377
- if (parsed.type === 'result' && parsed.is_error) {
378
- const errorMessage = parsed.error || parsed.result || 'Unknown error in result';
379
- ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_RESULT_ERROR]] ${errorMessage}\n`);
380
- return;
458
+ // Handle result events extract definitive token usage and surface errors
459
+ if (parsed.type === 'result') {
460
+ handleResultTokenUsage(parsed, ctx);
461
+ if (parsed.is_error) {
462
+ const errorMessage = parsed.error || parsed.result || 'Unknown error in result';
463
+ ctx.config.outputCallback?.(`\n[[MSTRO_ERROR:CLAUDE_RESULT_ERROR]] ${errorMessage}\n`);
464
+ return;
465
+ }
381
466
  }
382
467
  if (parsed.type === 'stream_event' && parsed.event) {
383
468
  const event = parsed.event;
@@ -386,6 +471,7 @@ function processStreamEvent(parsed, ctx) {
386
471
  handleToolStart(event, ctx);
387
472
  handleToolInputDelta(event, ctx);
388
473
  handleToolComplete(event, ctx);
474
+ handleTokenUsage(event, ctx);
389
475
  }
390
476
  handleToolResult(parsed, ctx);
391
477
  }
@@ -489,10 +575,17 @@ async function runStallCheckTick(state, opts) {
489
575
  const now = Date.now();
490
576
  const silenceMs = now - state.lastActivityTime;
491
577
  const totalElapsed = now - opts.perfStart;
578
+ const tokenSilenceMs = now - opts.lastTokenActivityTime;
492
579
  if (totalElapsed >= opts.stallHardCapMs) {
493
580
  terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config, `\n[[MSTRO_ERROR:EXECUTION_STALLED]] Hard time limit reached (${Math.round(opts.stallHardCapMs / 60000)} min total). Terminating process.\n`);
494
581
  return;
495
582
  }
583
+ // Token activity pushes the kill deadline forward — tokens flowing means
584
+ // the process is alive even if stdout is silent (e.g. silent thinking).
585
+ if (tokenSilenceMs < 60_000 && now < state.currentKillDeadline) {
586
+ const killMs = opts.config.stallKillMs ?? 1_800_000;
587
+ state.currentKillDeadline = Math.max(state.currentKillDeadline, now + killMs);
588
+ }
496
589
  if (now >= state.currentKillDeadline) {
497
590
  terminateStallProcess(opts.claudeProcess, opts.stallCheckInterval, opts.config, `\n[[MSTRO_ERROR:EXECUTION_STALLED]] No output for ${Math.round(silenceMs / 60_000)} minutes. Terminating process.\n`);
498
591
  return;
@@ -508,6 +601,7 @@ async function runStallCheckTick(state, opts) {
508
601
  pendingToolNames: new Set(opts.pendingTools.values()),
509
602
  totalToolCalls: opts.totalToolCalls,
510
603
  elapsedTotalMs: totalElapsed,
604
+ tokenSilenceMs,
511
605
  };
512
606
  if (opts.stallAssessEnabled && state.extensionsGranted < opts.maxExtensions) {
513
607
  state.assessmentInProgress = true;
@@ -597,8 +691,12 @@ function setupToolTracking(config, stallState, ctx, sessionCapture, prompt, perf
597
691
  ? new ToolWatchdog({
598
692
  profiles: config.toolTimeoutProfiles,
599
693
  verbose: config.verbose,
600
- onTiebreaker: async (toolName, toolInput, elapsedMs) => {
601
- return assessToolTimeout(toolName, toolInput, elapsedMs, config.claudeCommand, config.verbose);
694
+ onTiebreaker: async (toolName, toolInput, elapsedMs, tokenSilenceMs) => {
695
+ return assessToolTimeout(toolName, toolInput, elapsedMs, config.claudeCommand, config.verbose, tokenSilenceMs);
696
+ },
697
+ getTokenSilenceMs: () => {
698
+ const last = ctx.lastTokenActivityTime;
699
+ return last > 0 ? Date.now() - last : undefined;
602
700
  },
603
701
  })
604
702
  : null;
@@ -694,6 +792,9 @@ export async function executeClaudeCommand(prompt, _movementId, _sessionNumber,
694
792
  nativeTimeoutDetector: new NativeTimeoutDetector(),
695
793
  resumeAssessmentActive: isResumeMode,
696
794
  resumeAssessmentBuffer: '',
795
+ apiTokenUsage: { inputTokens: 0, outputTokens: 0 },
796
+ currentStepOutputTokens: 0,
797
+ lastTokenActivityTime: Date.now(),
697
798
  };
698
799
  // Stall detection state (mutable object shared with runStallCheckTick)
699
800
  const stallState = {
@@ -750,42 +851,47 @@ export async function executeClaudeCommand(prompt, _movementId, _sessionNumber,
750
851
  runStallCheckTick(stallState, {
751
852
  perfStart, stallWarningMs, stallHardCapMs, maxExtensions, stallAssessEnabled,
752
853
  toolWatchdogActive, prompt, pendingTools, lastToolInputSummary: toolCounters.lastToolInputSummary, totalToolCalls: toolCounters.totalToolCalls,
753
- claudeProcess, stallCheckInterval, config,
854
+ claudeProcess, stallCheckInterval, config, lastTokenActivityTime: ctx.lastTokenActivityTime,
754
855
  });
755
856
  }, 10_000);
756
857
  // Wire up the kill context now that stallCheckInterval exists
757
858
  toolTracking.setKillContext(claudeProcess, stallCheckInterval);
758
859
  return new Promise((resolve, reject) => {
759
- claudeProcess.on('close', async (code) => {
860
+ claudeProcess.on('close', async (code, signal) => {
760
861
  clearInterval(stallCheckInterval);
761
862
  watchdog?.clearAll();
762
- const postTimeout = flushNativeTimeoutBuffers(ctx);
763
863
  await classifyUnmatchedStderr(stderr, errorAlreadySurfaced, code, config);
764
- const resumeBuffered = ctx.resumeAssessmentActive ? (ctx.resumeAssessmentBuffer || undefined) : undefined;
765
- if (claudeProcess.pid) {
864
+ if (claudeProcess.pid)
766
865
  runningProcesses.delete(claudeProcess.pid);
767
- }
768
- resolve({
769
- output: stdout,
770
- error: stderr || undefined,
771
- exitCode: code || 0,
772
- assistantResponse: ctx.accumulatedAssistantResponse || undefined,
773
- thinkingOutput: ctx.accumulatedThinking || undefined,
774
- toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
775
- claudeSessionId: sessionCapture.claudeSessionId,
776
- nativeTimeoutCount: ctx.nativeTimeoutDetector.timeoutCount || undefined,
777
- postTimeoutOutput: postTimeout,
778
- resumeBufferedOutput: resumeBuffered,
779
- });
866
+ resolve(buildCloseResult(ctx, stdout, stderr, code, signal, sessionCapture));
780
867
  });
781
868
  claudeProcess.on('error', (error) => {
782
869
  clearInterval(stallCheckInterval);
783
870
  watchdog?.clearAll();
784
- if (claudeProcess.pid) {
871
+ if (claudeProcess.pid)
785
872
  runningProcesses.delete(claudeProcess.pid);
786
- }
787
873
  handleSpawnError(error, config, reject);
788
874
  });
789
875
  });
790
876
  }
877
+ function buildCloseResult(ctx, stdout, stderr, code, signal, sessionCapture) {
878
+ const postTimeout = flushNativeTimeoutBuffers(ctx);
879
+ const resumeBuffered = ctx.resumeAssessmentActive ? (ctx.resumeAssessmentBuffer || undefined) : undefined;
880
+ const exitCode = code ?? (signal ? 128 + (signalToNumber(signal) ?? 0) : 0);
881
+ const hasTokenUsage = ctx.apiTokenUsage.inputTokens > 0 || ctx.apiTokenUsage.outputTokens > 0;
882
+ return {
883
+ output: stdout,
884
+ error: stderr || undefined,
885
+ exitCode,
886
+ signalName: signal || undefined,
887
+ assistantResponse: ctx.accumulatedAssistantResponse || undefined,
888
+ thinkingOutput: ctx.accumulatedThinking || undefined,
889
+ toolUseHistory: ctx.accumulatedToolUse.length > 0 ? ctx.accumulatedToolUse : undefined,
890
+ claudeSessionId: sessionCapture.claudeSessionId,
891
+ nativeTimeoutCount: ctx.nativeTimeoutDetector.timeoutCount || undefined,
892
+ postTimeoutOutput: postTimeout,
893
+ resumeBufferedOutput: resumeBuffered,
894
+ apiTokenUsage: hasTokenUsage ? { ...ctx.apiTokenUsage } : undefined,
895
+ };
896
+ }
791
897
  //# sourceMappingURL=claude-invoker.js.map