mstro-app 0.3.0 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -19
- package/bin/mstro.js +62 -174
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +4 -3
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +2 -2
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +6 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +36 -4
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +1 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +2 -2
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +3 -2
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +6 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +85 -114
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +3 -3
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/server.js +3 -2
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -2
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/files.js +7 -7
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/pathUtils.js +1 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/platform.d.ts +2 -2
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +10 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +32 -4
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-utils.d.ts +4 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +48 -23
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +17 -17
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +3 -3
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +10 -10
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.js +1 -1
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +12 -11
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +1 -1
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.d.ts +22 -2
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +2 -2
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/paths.d.ts +0 -12
- package/dist/server/utils/paths.d.ts.map +1 -1
- package/dist/server/utils/paths.js +0 -12
- package/dist/server/utils/paths.js.map +1 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/package.json +4 -3
- package/server/README.md +0 -1
- package/server/cli/headless/claude-invoker.ts +21 -16
- package/server/cli/headless/mcp-config.ts +8 -8
- package/server/cli/headless/runner.ts +32 -4
- package/server/cli/headless/types.ts +1 -1
- package/server/cli/improvisation-session-manager.ts +8 -7
- package/server/index.ts +15 -9
- package/server/mcp/README.md +0 -5
- package/server/mcp/bouncer-integration.ts +116 -188
- package/server/mcp/security-audit.ts +4 -4
- package/server/mcp/server.ts +6 -5
- package/server/services/analytics.ts +3 -3
- package/server/services/files.ts +13 -13
- package/server/services/pathUtils.ts +2 -2
- package/server/services/platform.ts +5 -5
- package/server/services/sentry.ts +1 -1
- package/server/services/terminal/pty-manager.ts +36 -9
- package/server/services/websocket/file-explorer-handlers.ts +1 -1
- package/server/services/websocket/file-utils.ts +52 -28
- package/server/services/websocket/git-handlers.ts +34 -34
- package/server/services/websocket/git-pr-handlers.ts +6 -6
- package/server/services/websocket/git-worktree-handlers.ts +20 -20
- package/server/services/websocket/handler.ts +2 -2
- package/server/services/websocket/session-handlers.ts +31 -30
- package/server/services/websocket/tab-handlers.ts +1 -1
- package/server/services/websocket/terminal-handlers.ts +2 -2
- package/server/services/websocket/types.ts +2 -0
- package/server/utils/agent-manager.ts +6 -6
- package/server/utils/paths.ts +0 -14
- package/server/utils/port-manager.ts +1 -1
- package/bin/configure-claude.js +0 -298
- package/dist/server/mcp/bouncer-cli.d.ts +0 -3
- package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
- package/dist/server/mcp/bouncer-cli.js +0 -99
- package/dist/server/mcp/bouncer-cli.js.map +0 -1
- package/hooks/bouncer.sh +0 -145
- package/server/cli/headless/output-utils.test.ts +0 -225
- package/server/cli/headless/stall-assessor.test.ts +0 -165
- package/server/cli/headless/tool-watchdog.test.ts +0 -429
- package/server/mcp/bouncer-cli.ts +0 -127
- package/server/mcp/bouncer-integration.test.ts +0 -161
- package/server/mcp/security-patterns.test.ts +0 -258
- package/server/services/platform.test.ts +0 -1304
- package/server/services/websocket/autocomplete.test.ts +0 -194
- package/server/services/websocket/handler.test.ts +0 -20
|
@@ -21,6 +21,10 @@ import type {
|
|
|
21
21
|
ToolUseEvent,
|
|
22
22
|
} from './types.js';
|
|
23
23
|
|
|
24
|
+
/** Parsed JSON from Claude CLI stream — structure varies by event type */
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: external CLI stream JSON with heterogeneous shapes
|
|
26
|
+
type StreamJson = any;
|
|
27
|
+
|
|
24
28
|
export interface ClaudeInvokerOptions {
|
|
25
29
|
config: ResolvedHeadlessConfig;
|
|
26
30
|
runningProcesses: Map<number, ChildProcess>;
|
|
@@ -281,7 +285,7 @@ interface StreamHandlerContext {
|
|
|
281
285
|
}
|
|
282
286
|
|
|
283
287
|
function handleSessionCapture(
|
|
284
|
-
parsed:
|
|
288
|
+
parsed: StreamJson,
|
|
285
289
|
captured: { claudeSessionId?: string }
|
|
286
290
|
): void {
|
|
287
291
|
if (parsed.type === 'system' && parsed.subtype === 'init' && parsed.session_id) {
|
|
@@ -292,7 +296,7 @@ function handleSessionCapture(
|
|
|
292
296
|
}
|
|
293
297
|
}
|
|
294
298
|
|
|
295
|
-
function handleThinkingDelta(event:
|
|
299
|
+
function handleThinkingDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
296
300
|
if (
|
|
297
301
|
event.type !== 'content_block_delta' ||
|
|
298
302
|
event.delta?.type !== 'thinking_delta' ||
|
|
@@ -324,7 +328,7 @@ function handleThinkingDelta(event: any, ctx: StreamHandlerContext): string {
|
|
|
324
328
|
return updated;
|
|
325
329
|
}
|
|
326
330
|
|
|
327
|
-
function handleTextDelta(event:
|
|
331
|
+
function handleTextDelta(event: StreamJson, ctx: StreamHandlerContext): string {
|
|
328
332
|
if (
|
|
329
333
|
event.type !== 'content_block_delta' ||
|
|
330
334
|
event.delta?.type !== 'text_delta' ||
|
|
@@ -366,7 +370,7 @@ function handleTextDelta(event: any, ctx: StreamHandlerContext): string {
|
|
|
366
370
|
return updated;
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
function handleToolStart(event:
|
|
373
|
+
function handleToolStart(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
370
374
|
if (
|
|
371
375
|
event.type !== 'content_block_start' ||
|
|
372
376
|
event.content_block?.type !== 'tool_use'
|
|
@@ -399,7 +403,7 @@ function handleToolStart(event: any, ctx: StreamHandlerContext): void {
|
|
|
399
403
|
}
|
|
400
404
|
}
|
|
401
405
|
|
|
402
|
-
function handleToolInputDelta(event:
|
|
406
|
+
function handleToolInputDelta(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
403
407
|
if (
|
|
404
408
|
event.type !== 'content_block_delta' ||
|
|
405
409
|
event.delta?.type !== 'input_json_delta'
|
|
@@ -420,7 +424,7 @@ function handleToolInputDelta(event: any, ctx: StreamHandlerContext): void {
|
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
|
|
423
|
-
function handleToolComplete(event:
|
|
427
|
+
function handleToolComplete(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
424
428
|
if (event.type !== 'content_block_stop') {
|
|
425
429
|
return;
|
|
426
430
|
}
|
|
@@ -431,7 +435,7 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
|
|
|
431
435
|
return;
|
|
432
436
|
}
|
|
433
437
|
|
|
434
|
-
let completeInput:
|
|
438
|
+
let completeInput: Record<string, unknown> = {};
|
|
435
439
|
try {
|
|
436
440
|
completeInput = JSON.parse(toolBuffer.inputJson);
|
|
437
441
|
} catch (_e) {
|
|
@@ -460,7 +464,7 @@ function handleToolComplete(event: any, ctx: StreamHandlerContext): void {
|
|
|
460
464
|
}
|
|
461
465
|
|
|
462
466
|
/** Accumulate input tokens from a message_start event. Returns true if any tokens were added. */
|
|
463
|
-
function handleMessageStartTokens(event:
|
|
467
|
+
function handleMessageStartTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
464
468
|
if (event.type !== 'message_start' || !event.message?.usage) return false;
|
|
465
469
|
const usage = event.message.usage;
|
|
466
470
|
ctx.currentStepOutputTokens = 0;
|
|
@@ -488,7 +492,7 @@ function handleMessageStartTokens(event: any, ctx: StreamHandlerContext): boolea
|
|
|
488
492
|
* message_delta event are cumulative" and there can be "one or more message_delta
|
|
489
493
|
* events" per message. We track the delta from the previous value to avoid
|
|
490
494
|
* double-counting when multiple message_delta events fire per step. */
|
|
491
|
-
function handleMessageDeltaTokens(event:
|
|
495
|
+
function handleMessageDeltaTokens(event: StreamJson, ctx: StreamHandlerContext): boolean {
|
|
492
496
|
if (event.type !== 'message_delta' || !event.usage) return false;
|
|
493
497
|
if (typeof event.usage.output_tokens !== 'number') return false;
|
|
494
498
|
const increment = event.usage.output_tokens - ctx.currentStepOutputTokens;
|
|
@@ -500,7 +504,7 @@ function handleMessageDeltaTokens(event: any, ctx: StreamHandlerContext): boolea
|
|
|
500
504
|
return true;
|
|
501
505
|
}
|
|
502
506
|
|
|
503
|
-
function handleTokenUsage(event:
|
|
507
|
+
function handleTokenUsage(event: StreamJson, ctx: StreamHandlerContext): void {
|
|
504
508
|
const changed = handleMessageStartTokens(event, ctx) || handleMessageDeltaTokens(event, ctx);
|
|
505
509
|
if (changed) {
|
|
506
510
|
ctx.lastTokenActivityTime = Date.now();
|
|
@@ -514,7 +518,7 @@ function handleTokenUsage(event: any, ctx: StreamHandlerContext): void {
|
|
|
514
518
|
* accumulated stream-based counts which may be incomplete (e.g., when extended thinking
|
|
515
519
|
* suppresses stream_event emissions).
|
|
516
520
|
*/
|
|
517
|
-
function handleResultTokenUsage(parsed:
|
|
521
|
+
function handleResultTokenUsage(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
518
522
|
if (!parsed.usage) return;
|
|
519
523
|
const u = parsed.usage;
|
|
520
524
|
const input = (typeof u.input_tokens === 'number' ? u.input_tokens : 0)
|
|
@@ -533,7 +537,7 @@ function handleResultTokenUsage(parsed: any, ctx: StreamHandlerContext): void {
|
|
|
533
537
|
}
|
|
534
538
|
}
|
|
535
539
|
|
|
536
|
-
function handleToolResult(parsed:
|
|
540
|
+
function handleToolResult(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
537
541
|
if (parsed.type !== 'user' || !parsed.message?.content) {
|
|
538
542
|
return;
|
|
539
543
|
}
|
|
@@ -583,7 +587,7 @@ function processStreamLines(
|
|
|
583
587
|
return remainder;
|
|
584
588
|
}
|
|
585
589
|
|
|
586
|
-
function processStreamEvent(parsed:
|
|
590
|
+
function processStreamEvent(parsed: StreamJson, ctx: StreamHandlerContext): void {
|
|
587
591
|
// Handle error events from Claude CLI (API errors, model errors, etc.)
|
|
588
592
|
if (parsed.type === 'error') {
|
|
589
593
|
const errorMessage = parsed.error?.message || parsed.message || JSON.stringify(parsed);
|
|
@@ -870,12 +874,13 @@ function onToolStart(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
|
870
874
|
/** Handle tool_complete events. Extracted to reduce cognitive complexity. */
|
|
871
875
|
function onToolComplete(event: ToolUseEvent, s: ToolTrackingState): void {
|
|
872
876
|
const id = event.toolId!;
|
|
873
|
-
|
|
874
|
-
s.
|
|
877
|
+
const input = event.completeInput ?? {};
|
|
878
|
+
s.counters.lastToolInputSummary = summarizeToolInput(input);
|
|
879
|
+
s.toolIdToInput.set(id, input);
|
|
875
880
|
if (!s.watchdog) return;
|
|
876
881
|
const toolName = s.toolIdToName.get(id);
|
|
877
882
|
if (toolName) {
|
|
878
|
-
s.watchdog.startWatch(id, toolName,
|
|
883
|
+
s.watchdog.startWatch(id, toolName, input, () => { s.onTimeout(id); });
|
|
879
884
|
}
|
|
880
885
|
}
|
|
881
886
|
|
|
@@ -12,8 +12,8 @@ import { MCP_SERVER_PATH, MSTRO_ROOT } from '../../utils/paths.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* Load user's MCP servers from ~/.claude.json (global + project-level)
|
|
14
14
|
*/
|
|
15
|
-
function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string,
|
|
16
|
-
const servers: Record<string,
|
|
15
|
+
function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string, unknown> {
|
|
16
|
+
const servers: Record<string, unknown> = {};
|
|
17
17
|
const claudeConfigPath = join(homedir(), '.claude.json');
|
|
18
18
|
|
|
19
19
|
if (!existsSync(claudeConfigPath)) {
|
|
@@ -29,7 +29,7 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
|
|
|
29
29
|
|
|
30
30
|
if (claudeConfig.projects && typeof claudeConfig.projects === 'object') {
|
|
31
31
|
for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
|
|
32
|
-
const projectServers = (projectConfig as
|
|
32
|
+
const projectServers = (projectConfig as Record<string, unknown>)?.mcpServers;
|
|
33
33
|
if (workingDir.startsWith(projectPath) && typeof projectServers === 'object') {
|
|
34
34
|
Object.assign(servers, projectServers);
|
|
35
35
|
}
|
|
@@ -39,8 +39,8 @@ function loadUserMcpServers(workingDir: string, verbose: boolean): Record<string
|
|
|
39
39
|
if (verbose) {
|
|
40
40
|
console.log(`[${new Date().toISOString()}] Loaded ${Object.keys(servers).length} user MCP servers from ~/.claude.json`);
|
|
41
41
|
}
|
|
42
|
-
} catch (parseError:
|
|
43
|
-
console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError.message}`);
|
|
42
|
+
} catch (parseError: unknown) {
|
|
43
|
+
console.error(`[${new Date().toISOString()}] Failed to parse ~/.claude.json: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return servers;
|
|
@@ -57,7 +57,7 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const mcpServers: Record<string,
|
|
60
|
+
const mcpServers: Record<string, unknown> = {
|
|
61
61
|
'mstro-bouncer': {
|
|
62
62
|
command: 'npx',
|
|
63
63
|
args: ['tsx', MCP_SERVER_PATH],
|
|
@@ -80,8 +80,8 @@ export function generateMcpConfig(workingDir: string, verbose: boolean = false):
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return configPath;
|
|
83
|
-
} catch (error:
|
|
84
|
-
console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error.message}`);
|
|
83
|
+
} catch (error: unknown) {
|
|
84
|
+
console.error(`[${new Date().toISOString()}] Failed to generate MCP config: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -175,12 +175,40 @@ export class HeadlessRunner {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
|
-
* Cleanup on exit
|
|
178
|
+
* Cleanup on exit — SIGTERM all tracked processes, then SIGKILL stragglers after 5s
|
|
179
179
|
*/
|
|
180
180
|
cleanup(): void {
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
if (this.runningProcesses.size === 0) return;
|
|
182
|
+
|
|
183
|
+
const pids = new Set<number>();
|
|
184
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
185
|
+
pids.add(pid);
|
|
186
|
+
try { proc.kill('SIGTERM'); } catch { /* already dead */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// SIGKILL fallback after 5 seconds for any process that didn't exit
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
192
|
+
if (pids.has(pid) && !proc.killed) {
|
|
193
|
+
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.runningProcesses.clear();
|
|
197
|
+
}, 5000);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sweep for zombie processes — entries in runningProcesses whose underlying
|
|
202
|
+
* process has already exited but whose 'close' event was missed.
|
|
203
|
+
*/
|
|
204
|
+
sweepZombies(): number {
|
|
205
|
+
let swept = 0;
|
|
206
|
+
for (const [pid, proc] of this.runningProcesses) {
|
|
207
|
+
if (proc.exitCode !== null || proc.killed) {
|
|
208
|
+
this.runningProcesses.delete(pid);
|
|
209
|
+
swept++;
|
|
210
|
+
}
|
|
183
211
|
}
|
|
184
|
-
|
|
212
|
+
return swept;
|
|
185
213
|
}
|
|
186
214
|
}
|
|
@@ -114,7 +114,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
114
114
|
private currentRunner: HeadlessRunner | null = null;
|
|
115
115
|
private options: ImprovisationOptions;
|
|
116
116
|
private pendingApproval?: {
|
|
117
|
-
plan:
|
|
117
|
+
plan: unknown;
|
|
118
118
|
resolve: (approved: boolean) => void;
|
|
119
119
|
};
|
|
120
120
|
private outputQueue: Array<{ text: string; timestamp: number }> = [];
|
|
@@ -129,7 +129,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
129
129
|
/** Timestamp when current execution started (for accurate elapsed time across reconnects) */
|
|
130
130
|
private _executionStartTimestamp: number | undefined;
|
|
131
131
|
/** Buffered events during current execution, for replay on reconnect */
|
|
132
|
-
private executionEventLog: Array<{ type: string; data:
|
|
132
|
+
private executionEventLog: Array<{ type: string; data: unknown; timestamp: number }> = [];
|
|
133
133
|
/** Set by cancel() to signal the retry loop to exit */
|
|
134
134
|
private _cancelled: boolean = false;
|
|
135
135
|
|
|
@@ -383,19 +383,20 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
383
383
|
this.emitMovementComplete(movement, result, _execStart, sequenceNumber);
|
|
384
384
|
return movement;
|
|
385
385
|
|
|
386
|
-
} catch (error:
|
|
386
|
+
} catch (error: unknown) {
|
|
387
387
|
this._isExecuting = false;
|
|
388
388
|
this._executionStartTimestamp = undefined;
|
|
389
389
|
this.executionEventLog = [];
|
|
390
390
|
this.currentRunner = null;
|
|
391
391
|
this.emit('onMovementError', error);
|
|
392
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
392
393
|
trackEvent(AnalyticsEvents.IMPROVISE_MOVEMENT_ERROR, {
|
|
393
|
-
error_message:
|
|
394
|
+
error_message: errorMessage.slice(0, 200),
|
|
394
395
|
sequence_number: this.history.movements.length + 1,
|
|
395
396
|
duration_ms: Date.now() - _execStart,
|
|
396
397
|
model: this.options.model || 'default',
|
|
397
398
|
});
|
|
398
|
-
this.queueOutput(`\n❌ Error: ${
|
|
399
|
+
this.queueOutput(`\n❌ Error: ${errorMessage}\n`);
|
|
399
400
|
this.flushOutputQueue();
|
|
400
401
|
throw error;
|
|
401
402
|
} finally {
|
|
@@ -1510,7 +1511,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1510
1511
|
* Request user approval for a plan
|
|
1511
1512
|
* Returns a promise that resolves when the user approves/rejects
|
|
1512
1513
|
*/
|
|
1513
|
-
async requestApproval(plan:
|
|
1514
|
+
async requestApproval(plan: unknown): Promise<boolean> {
|
|
1514
1515
|
return new Promise((resolve) => {
|
|
1515
1516
|
this.pendingApproval = { plan, resolve };
|
|
1516
1517
|
this.emit('onApprovalRequired', plan);
|
|
@@ -1559,7 +1560,7 @@ export class ImprovisationSessionManager extends EventEmitter {
|
|
|
1559
1560
|
* Get buffered execution events for replay on reconnect.
|
|
1560
1561
|
* Only meaningful while isExecuting is true.
|
|
1561
1562
|
*/
|
|
1562
|
-
getExecutionEventLog(): Array<{ type: string; data:
|
|
1563
|
+
getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
|
|
1563
1564
|
return this.executionEventLog;
|
|
1564
1565
|
}
|
|
1565
1566
|
|
package/server/index.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomBytes } from 'node:crypto'
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import type { IncomingMessage } from 'node:http'
|
|
10
|
+
import type { IncomingMessage, Server } from 'node:http'
|
|
11
11
|
import { homedir } from 'node:os'
|
|
12
12
|
import { basename, join } from 'node:path'
|
|
13
13
|
import { serve } from '@hono/node-server'
|
|
14
|
-
import { Hono } from 'hono'
|
|
14
|
+
import { type Context, Hono, type Next } from 'hono'
|
|
15
15
|
import { cors } from 'hono/cors'
|
|
16
16
|
import { logger } from 'hono/logger'
|
|
17
17
|
import { type WebSocket as NodeWebSocket, WebSocketServer } from 'ws'
|
|
@@ -26,10 +26,10 @@ import {
|
|
|
26
26
|
import { AnalyticsEvents, initAnalytics, shutdownAnalytics, trackEvent } from './services/analytics.js'
|
|
27
27
|
import { AuthService } from './services/auth.js'
|
|
28
28
|
import { FileService } from './services/files.js'
|
|
29
|
-
import { InstanceRegistry } from './services/instances.js'
|
|
29
|
+
import { InstanceRegistry, type MstroInstance } from './services/instances.js'
|
|
30
30
|
import { PlatformConnection } from './services/platform.js'
|
|
31
31
|
import { captureException, flushSentry, initSentry } from './services/sentry.js'
|
|
32
|
-
import { getPTYManager } from './services/terminal/pty-manager.js'
|
|
32
|
+
import { getPTYManager, reloadPty } from './services/terminal/pty-manager.js'
|
|
33
33
|
import { WebSocketImproviseHandler } from './services/websocket/index.js'
|
|
34
34
|
import type { WSContext } from './services/websocket/types.js'
|
|
35
35
|
import { findAvailablePort } from './utils/port.js'
|
|
@@ -126,7 +126,7 @@ const fileService = new FileService(WORKING_DIR)
|
|
|
126
126
|
const wsHandler = new WebSocketImproviseHandler()
|
|
127
127
|
|
|
128
128
|
// Instance registration deferred to startServer() when port is known
|
|
129
|
-
let _currentInstance:
|
|
129
|
+
let _currentInstance: MstroInstance | undefined
|
|
130
130
|
|
|
131
131
|
// Global middleware
|
|
132
132
|
// In production, restrict CORS to block cross-origin browser requests to localhost.
|
|
@@ -149,7 +149,7 @@ app.use('*', logger())
|
|
|
149
149
|
// Authentication Middleware
|
|
150
150
|
// ========================================
|
|
151
151
|
|
|
152
|
-
const authMiddleware = async (c:
|
|
152
|
+
const authMiddleware = async (c: Context, next: Next) => {
|
|
153
153
|
// Skip auth for health check and config
|
|
154
154
|
const publicPaths = ['/health', '/api/config']
|
|
155
155
|
if (publicPaths.some(path => c.req.path.startsWith(path))) {
|
|
@@ -207,6 +207,12 @@ app.route('/api/improvise', createImproviseRoutes(WORKING_DIR))
|
|
|
207
207
|
app.route('/api/files', createFileRoutes(fileService))
|
|
208
208
|
app.route('/api/notifications', createNotificationRoutes(WORKING_DIR))
|
|
209
209
|
|
|
210
|
+
// Reload node-pty after setup-terminal compiles the native module
|
|
211
|
+
app.post('/api/reload-pty', async (c) => {
|
|
212
|
+
const success = await reloadPty()
|
|
213
|
+
return c.json({ success, available: success })
|
|
214
|
+
})
|
|
215
|
+
|
|
210
216
|
// ========================================
|
|
211
217
|
// Static File Serving (Production Only)
|
|
212
218
|
// ========================================
|
|
@@ -257,7 +263,7 @@ function wrapWebSocket(ws: NodeWebSocket, workingDir: string): WSContext {
|
|
|
257
263
|
* This allows messages from the web (via platform) to be handled by the same wsHandler
|
|
258
264
|
*/
|
|
259
265
|
function createPlatformRelayContext(
|
|
260
|
-
platformSend: (message:
|
|
266
|
+
platformSend: (message: unknown) => void,
|
|
261
267
|
workingDir: string
|
|
262
268
|
): WSContext {
|
|
263
269
|
return {
|
|
@@ -299,7 +305,7 @@ async function startServer() {
|
|
|
299
305
|
})
|
|
300
306
|
|
|
301
307
|
// Create WebSocket server attached to the HTTP server
|
|
302
|
-
const wss = new WebSocketServer({ server: server as
|
|
308
|
+
const wss = new WebSocketServer({ server: server as Server })
|
|
303
309
|
|
|
304
310
|
wss.on('connection', (ws: NodeWebSocket, req: IncomingMessage) => {
|
|
305
311
|
const url = new URL(req.url || '/', `http://localhost:${PORT}`)
|
|
@@ -354,7 +360,7 @@ async function startServer() {
|
|
|
354
360
|
|
|
355
361
|
// Queue for messages that arrive before relay context is ready
|
|
356
362
|
// This handles race conditions where initTab arrives before web_connected
|
|
357
|
-
let pendingRelayMessages:
|
|
363
|
+
let pendingRelayMessages: unknown[] = []
|
|
358
364
|
|
|
359
365
|
// Connect to platform
|
|
360
366
|
const platformConnection = new PlatformConnection(WORKING_DIR, {
|
package/server/mcp/README.md
CHANGED
|
@@ -22,7 +22,6 @@ MCP (Model Context Protocol) server that provides tool approval decisions for Cl
|
|
|
22
22
|
- **bouncer-integration.ts** — Core 2-layer security review logic. Orchestrates pattern check → AI analysis flow.
|
|
23
23
|
- **security-patterns.ts** — Pattern definitions: CRITICAL_THREATS, SAFE_OPERATIONS, NEEDS_AI_REVIEW, SENSITIVE_PATHS.
|
|
24
24
|
- **security-audit.ts** — Audit logging to `~/.mstro/logs/bouncer-audit.jsonl` (JSON Lines format).
|
|
25
|
-
- **bouncer-cli.ts** — Shell-callable wrapper invoked by `~/.claude/hooks/bouncer.sh`. Reads JSON from stdin, outputs decision to stdout.
|
|
26
25
|
|
|
27
26
|
## Usage
|
|
28
27
|
|
|
@@ -49,10 +48,6 @@ claude --print \
|
|
|
49
48
|
|
|
50
49
|
The MCP config is auto-generated by the headless runner at `~/.mstro/mcp-config.json`. It includes the bouncer server plus any user-configured MCP servers from `~/.claude.json`.
|
|
51
50
|
|
|
52
|
-
### Hook Integration
|
|
53
|
-
|
|
54
|
-
When installed via `mstro configure-hooks`, a shell script at `~/.claude/hooks/bouncer.sh` calls `bouncer-cli.ts` as a PreToolUse hook. This provides security for both mstro sessions and standalone Claude Code usage.
|
|
55
|
-
|
|
56
51
|
## Environment Variables
|
|
57
52
|
|
|
58
53
|
| Variable | Default | Description |
|