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.
- package/PRIVACY.md +126 -0
- package/README.md +24 -23
- package/bin/commands/login.js +79 -49
- package/bin/mstro.js +240 -37
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +133 -27
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +23 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +20 -1
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +30 -24
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +19 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +28 -1
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +221 -29
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +0 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +13 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +13 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +2 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +50 -3
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +36 -0
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-handlers.js +797 -0
- package/dist/server/services/websocket/git-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.js +299 -0
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler-context.d.ts +32 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
- package/dist/server/services/websocket/handler-context.js +4 -0
- package/dist/server/services/websocket/handler-context.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +27 -359
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +67 -2328
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +10 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/session-handlers.js +507 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -0
- package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/settings-handlers.js +125 -0
- package/dist/server/services/websocket/settings-handlers.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-handlers.js +131 -0
- package/dist/server/services/websocket/tab-handlers.js.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.js +220 -0
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +63 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/server/README.md +176 -159
- package/server/cli/headless/claude-invoker.ts +155 -31
- package/server/cli/headless/output-utils.test.ts +225 -0
- package/server/cli/headless/runner.ts +25 -0
- package/server/cli/headless/stall-assessor.test.ts +165 -0
- package/server/cli/headless/stall-assessor.ts +25 -0
- package/server/cli/headless/tool-watchdog.test.ts +429 -0
- package/server/cli/headless/tool-watchdog.ts +33 -25
- package/server/cli/headless/types.ts +10 -1
- package/server/cli/improvisation-session-manager.ts +277 -30
- package/server/index.ts +0 -4
- package/server/mcp/README.md +59 -67
- package/server/mcp/bouncer-integration.test.ts +161 -0
- package/server/mcp/security-patterns.test.ts +258 -0
- package/server/services/analytics.ts +13 -1
- package/server/services/platform.ts +12 -1
- package/server/services/terminal/pty-manager.ts +53 -3
- package/server/services/websocket/autocomplete.test.ts +194 -0
- package/server/services/websocket/file-explorer-handlers.ts +587 -0
- package/server/services/websocket/git-handlers.ts +924 -0
- package/server/services/websocket/git-pr-handlers.ts +363 -0
- package/server/services/websocket/git-worktree-handlers.ts +403 -0
- package/server/services/websocket/handler-context.ts +44 -0
- package/server/services/websocket/handler.test.ts +1 -1
- package/server/services/websocket/handler.ts +83 -2678
- package/server/services/websocket/index.ts +1 -1
- package/server/services/websocket/session-handlers.ts +574 -0
- package/server/services/websocket/settings-handlers.ts +150 -0
- package/server/services/websocket/tab-handlers.ts +150 -0
- package/server/services/websocket/terminal-handlers.ts +277 -0
- package/server/services/websocket/types.ts +135 -0
- 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 (
|
|
12
|
-
* mstro login #
|
|
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 -
|
|
198
|
-
log('
|
|
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 (
|
|
201
|
-
log(' mstro login
|
|
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('
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
*
|
|
519
|
+
* Auto-login if not authenticated. Exits on failure.
|
|
353
520
|
*/
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
log('');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
*
|
|
535
|
+
* Prompt for bouncer setup if not configured
|
|
536
|
+
* Returns true if runConfigureHooks was called (async exit path)
|
|
364
537
|
*/
|
|
365
|
-
async function
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
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
|
|
377
|
-
if (parsed.type === 'result'
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|