frontmcp 1.2.0 → 1.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 (108) hide show
  1. package/package.json +4 -4
  2. package/src/commands/build/exec/bin-meta.d.ts +49 -0
  3. package/src/commands/build/exec/bin-meta.js +68 -0
  4. package/src/commands/build/exec/bin-meta.js.map +1 -0
  5. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js +195 -3
  6. package/src/commands/build/exec/cli-runtime/generate-cli-entry.js.map +1 -1
  7. package/src/commands/build/exec/cli-runtime/plugin-emitter.d.ts +160 -0
  8. package/src/commands/build/exec/cli-runtime/plugin-emitter.js +512 -0
  9. package/src/commands/build/exec/cli-runtime/plugin-emitter.js.map +1 -0
  10. package/src/commands/build/exec/cli-runtime/schema-extractor.d.ts +13 -1
  11. package/src/commands/build/exec/cli-runtime/schema-extractor.js +29 -3
  12. package/src/commands/build/exec/cli-runtime/schema-extractor.js.map +1 -1
  13. package/src/commands/build/exec/cli-runtime/skill-md-compose.d.ts +25 -0
  14. package/src/commands/build/exec/cli-runtime/skill-md-compose.js +63 -0
  15. package/src/commands/build/exec/cli-runtime/skill-md-compose.js.map +1 -0
  16. package/src/commands/build/exec/index.js +26 -0
  17. package/src/commands/build/exec/index.js.map +1 -1
  18. package/src/commands/dev/bridge/child-supervisor.d.ts +48 -0
  19. package/src/commands/dev/bridge/child-supervisor.js +228 -0
  20. package/src/commands/dev/bridge/child-supervisor.js.map +1 -0
  21. package/src/commands/dev/bridge/errors.d.ts +23 -0
  22. package/src/commands/dev/bridge/errors.js +34 -0
  23. package/src/commands/dev/bridge/errors.js.map +1 -0
  24. package/src/commands/dev/bridge/index.d.ts +30 -0
  25. package/src/commands/dev/bridge/index.js +220 -0
  26. package/src/commands/dev/bridge/index.js.map +1 -0
  27. package/src/commands/dev/bridge/log.d.ts +29 -0
  28. package/src/commands/dev/bridge/log.js +82 -0
  29. package/src/commands/dev/bridge/log.js.map +1 -0
  30. package/src/commands/dev/bridge/state-machine.d.ts +56 -0
  31. package/src/commands/dev/bridge/state-machine.js +245 -0
  32. package/src/commands/dev/bridge/state-machine.js.map +1 -0
  33. package/src/commands/dev/bridge/stdio-framer.d.ts +47 -0
  34. package/src/commands/dev/bridge/stdio-framer.js +128 -0
  35. package/src/commands/dev/bridge/stdio-framer.js.map +1 -0
  36. package/src/commands/dev/bridge/upstream-client.d.ts +49 -0
  37. package/src/commands/dev/bridge/upstream-client.js +159 -0
  38. package/src/commands/dev/bridge/upstream-client.js.map +1 -0
  39. package/src/commands/dev/bridge/watcher.d.ts +30 -0
  40. package/src/commands/dev/bridge/watcher.js +87 -0
  41. package/src/commands/dev/bridge/watcher.js.map +1 -0
  42. package/src/commands/dev/dev.d.ts +18 -1
  43. package/src/commands/dev/dev.js +134 -14
  44. package/src/commands/dev/dev.js.map +1 -1
  45. package/src/commands/dev/inspector.d.ts +13 -1
  46. package/src/commands/dev/inspector.js +77 -3
  47. package/src/commands/dev/inspector.js.map +1 -1
  48. package/src/commands/dev/port.d.ts +23 -0
  49. package/src/commands/dev/port.js +87 -0
  50. package/src/commands/dev/port.js.map +1 -0
  51. package/src/commands/dev/register.d.ts +1 -1
  52. package/src/commands/dev/register.js +28 -4
  53. package/src/commands/dev/register.js.map +1 -1
  54. package/src/commands/dev/test.d.ts +26 -1
  55. package/src/commands/dev/test.js +181 -64
  56. package/src/commands/dev/test.js.map +1 -1
  57. package/src/commands/eject/mcp-client.d.ts +25 -0
  58. package/src/commands/eject/mcp-client.js +74 -0
  59. package/src/commands/eject/mcp-client.js.map +1 -0
  60. package/src/commands/eject/register.d.ts +9 -0
  61. package/src/commands/eject/register.js +56 -0
  62. package/src/commands/eject/register.js.map +1 -0
  63. package/src/commands/install/install-claude-plugin.d.ts +13 -0
  64. package/src/commands/install/install-claude-plugin.js +327 -0
  65. package/src/commands/install/install-claude-plugin.js.map +1 -0
  66. package/src/commands/install/register.d.ts +16 -0
  67. package/src/commands/install/register.js +70 -0
  68. package/src/commands/install/register.js.map +1 -0
  69. package/src/commands/scaffold/create.js +44 -0
  70. package/src/commands/scaffold/create.js.map +1 -1
  71. package/src/commands/skills/from-entry.d.ts +31 -0
  72. package/src/commands/skills/from-entry.js +68 -0
  73. package/src/commands/skills/from-entry.js.map +1 -0
  74. package/src/commands/skills/install.d.ts +12 -0
  75. package/src/commands/skills/install.js +173 -8
  76. package/src/commands/skills/install.js.map +1 -1
  77. package/src/commands/skills/register.js +7 -3
  78. package/src/commands/skills/register.js.map +1 -1
  79. package/src/config/frontmcp-config.loader.d.ts +28 -0
  80. package/src/config/frontmcp-config.loader.js +146 -67
  81. package/src/config/frontmcp-config.loader.js.map +1 -1
  82. package/src/config/frontmcp-config.resolve.d.ts +67 -0
  83. package/src/config/frontmcp-config.resolve.js +118 -0
  84. package/src/config/frontmcp-config.resolve.js.map +1 -0
  85. package/src/config/frontmcp-config.schema.d.ts +207 -0
  86. package/src/config/frontmcp-config.schema.js +217 -1
  87. package/src/config/frontmcp-config.schema.js.map +1 -1
  88. package/src/config/frontmcp-config.types.d.ts +133 -0
  89. package/src/config/frontmcp-config.types.js.map +1 -1
  90. package/src/config/index.d.ts +2 -1
  91. package/src/config/index.js +3 -1
  92. package/src/config/index.js.map +1 -1
  93. package/src/core/args.d.ts +13 -0
  94. package/src/core/args.js.map +1 -1
  95. package/src/core/bridge.js +39 -0
  96. package/src/core/bridge.js.map +1 -1
  97. package/src/core/cli.d.ts +0 -6
  98. package/src/core/cli.js +23 -3
  99. package/src/core/cli.js.map +1 -1
  100. package/src/core/help.d.ts +1 -1
  101. package/src/core/help.js +27 -6
  102. package/src/core/help.js.map +1 -1
  103. package/src/core/program.d.ts +1 -1
  104. package/src/core/program.js +56 -12
  105. package/src/core/program.js.map +1 -1
  106. package/src/core/project-commands.d.ts +44 -0
  107. package/src/core/project-commands.js +216 -0
  108. package/src/core/project-commands.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dev stdio bridge entry point (issue #399).\n *\n * Wires the framer (stdio in/out), state machine (buffer + reload FSM),\n * watcher (file-change source), child supervisor (user-code lifecycle),\n * and upstream client (forwarding to the child) into a single\n * long-lived process.\n *\n * Lifetime:\n *\n * 1. Parse options, resolve entry + log file path.\n * 2. Pin a stable session id (uuid) so the same id survives child\n * restarts.\n * 3. Construct logger; open log file.\n * 4. Construct state machine + framer + watcher + supervisor +\n * upstream client (transport per `--serve`).\n * 5. Spawn the first child, wait for ready, transition state to Ready.\n * 6. Forward frames in both directions; watcher events trigger\n * controlled restart.\n * 7. SIGINT/SIGTERM → flush buffer with `dev_server_unreachable`,\n * tear down child + watcher, exit cleanly.\n */\n\nimport type { ChildProcess } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { randomUUID } from '@frontmcp/utils';\n\nimport type { ParsedArgs } from '../../../core/args';\nimport { resolveEntry } from '../../../shared/fs';\nimport { createChildSupervisor, type ChildSupervisor, type SupervisorMode } from './child-supervisor';\nimport { createBridgeLogger, type BridgeLogger } from './log';\nimport { createBridgeStateMachine, type BridgeStateMachine } from './state-machine';\nimport { createStdioFramer, type JsonRpcFrame, type StdioFramer } from './stdio-framer';\nimport { createHttpUpstream, createPipeUpstream, type UpstreamClient } from './upstream-client';\nimport { createDevWatcher } from './watcher';\n\ninterface RuntimeBridgeOptions {\n entry: string;\n mode: SupervisorMode;\n port: number;\n bufferSize: number;\n reloadDeadlineMs: number;\n logFile: string;\n}\n\nconst DEFAULT_PORT = 3000;\nconst DEFAULT_LOG_FILE = path.join('.frontmcp', 'dev.log');\n\nfunction normalizeOptions(opts: ParsedArgs, entry: string): RuntimeBridgeOptions {\n const mode: SupervisorMode = opts.serve ? 'pipe' : 'http';\n const port = typeof opts.port === 'number' ? opts.port : DEFAULT_PORT;\n const bufferSize = typeof opts.bufferSize === 'number' && opts.bufferSize > 0 ? opts.bufferSize : 8;\n const reloadDeadlineMs =\n typeof opts.reloadDeadlineMs === 'number' && opts.reloadDeadlineMs > 0 ? opts.reloadDeadlineMs : 30_000;\n const logFile = typeof opts.logFile === 'string' && opts.logFile.length > 0 ? opts.logFile : DEFAULT_LOG_FILE;\n return { entry, mode, port, bufferSize, reloadDeadlineMs, logFile };\n}\n\nexport async function runDevBridge(opts: ParsedArgs): Promise<void> {\n const cwd = process.cwd();\n const entry = await resolveEntry(cwd, opts.entry);\n const runtime = normalizeOptions(opts, entry);\n\n const log = await createBridgeLogger({ filePath: runtime.logFile });\n log.info('bridge-start', {\n entry: runtime.entry,\n mode: runtime.mode,\n port: runtime.port,\n bufferSize: runtime.bufferSize,\n reloadDeadlineMs: runtime.reloadDeadlineMs,\n });\n\n // Pinned session id — child reads from FRONTMCP_DEV_FORCE_SESSION_ID so\n // session continuity works across restarts (memory or Redis store both OK).\n const sessionId = randomUUID();\n\n // Only `upstream` is reassigned during runtime (on every child restart).\n // The rest are constructed exactly once below and referenced through\n // closures that fire after all bindings exist.\n let upstream: UpstreamClient | undefined;\n\n function buildUpstreamForChild(child: ChildProcess): UpstreamClient {\n if (runtime.mode === 'http') {\n return createHttpUpstream({\n url: `http://127.0.0.1:${runtime.port}/`,\n log,\n sessionId,\n onFrame: (frame: JsonRpcFrame) => fsm.relayUpstream(frame),\n });\n }\n return createPipeUpstream({\n child,\n log,\n sessionId,\n onFrame: (frame: JsonRpcFrame) => fsm.relayUpstream(frame),\n });\n }\n\n // ─── construct framer + FSM. Closures bind to each other by reference,\n // so referencing `fsm`/`framer` inside a callback executed at runtime\n // is safe even though `framer` is declared first textually. ───\n const framer = createStdioFramer({\n input: process.stdin,\n output: process.stdout,\n log,\n onFrame: (frame) => fsm.enqueue(frame),\n });\n\n const fsm = createBridgeStateMachine({\n log,\n bufferSize: runtime.bufferSize,\n reloadDeadlineMs: runtime.reloadDeadlineMs,\n respond: (frame) => framer.write(frame),\n forward: async (frame) => {\n if (!upstream) {\n log.warn('forward-without-upstream', { method: frame.method });\n return;\n }\n await upstream.send(frame);\n },\n });\n\n framer.start();\n\n // ─── supervisor → boots first child, then attaches upstream ───\n const supervisor = createChildSupervisor({\n mode: runtime.mode,\n entry: runtime.entry,\n log,\n sessionId,\n port: runtime.mode === 'http' ? runtime.port : undefined,\n onReady: async (child) => {\n // Close any previous upstream (reload path). A rejection here MUST\n // NOT block re-binding — the child is up and ready, and leaving the\n // bridge without an upstream would strand every subsequent RPC.\n try {\n await upstream?.close();\n } catch (err) {\n log.error('upstream-stop-error', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n upstream = buildUpstreamForChild(child);\n fsm.onChildReady();\n },\n onExit: (reason) => {\n // Don't drop the close() promise — an in-flight HTTP request or SSE\n // body read can reject (e.g. AbortError when we abort it ourselves),\n // and an unhandled rejection here would crash the bridge on the\n // next tick. Hand the rejection to the logger; the child is dead\n // either way so we always proceed to onChildExit.\n const closingUpstream = upstream;\n upstream = undefined;\n void closingUpstream?.close().catch((err: unknown) => {\n log.error('upstream-stop-error', {\n error: err instanceof Error ? err.message : String(err),\n });\n });\n fsm.onChildExit(reason);\n },\n });\n\n fsm.onBootStart();\n\n try {\n await supervisor.start();\n } catch (err) {\n log.error('initial-boot-failed', { error: (err as Error).message });\n fsm.onReloadDeadline();\n // Stay running so the watcher can retry once the user fixes the source.\n }\n\n // ─── watcher → restart on file change ───\n // Watch the project root (cwd), not just the entry's directory: shared\n // helpers, `frontmcp.config.ts`, and `tsconfig.json` all live above\n // `src/main.ts` and must trigger a reload too. The recursive watcher\n // already debounces and filters via `shouldIgnore` so the wider scope\n // doesn't generate spurious reloads.\n const watcher = createDevWatcher({\n rootDir: cwd,\n log,\n onChange: (trigger) => {\n fsm.onWatcherEvent(trigger);\n void (async () => {\n // Defensive — `supervisor` is captured in this closure after the\n // initial assignment above, but a runtime check keeps the\n // non-null assertion off the call site.\n if (!supervisor) {\n log.error('restart-failed', { error: 'supervisor not initialised', trigger });\n return;\n }\n try {\n await supervisor.restart();\n } catch (err) {\n log.error('restart-failed', { error: (err as Error).message });\n }\n })();\n },\n });\n watcher.start();\n\n // ─── teardown wiring ───\n let stopping = false;\n async function shutdown(signal: NodeJS.Signals): Promise<void> {\n if (stopping) return;\n stopping = true;\n log.info('bridge-stop', { signal });\n try {\n await fsm.stop();\n } catch (err) {\n log.error('fsm-stop-error', { error: (err as Error).message });\n }\n try {\n watcher.stop();\n } catch (err) {\n log.error('watcher-stop-error', { error: (err as Error).message });\n }\n try {\n await upstream?.close();\n } catch (err) {\n log.error('upstream-stop-error', { error: (err as Error).message });\n }\n try {\n await supervisor.stop();\n } catch (err) {\n log.error('supervisor-stop-error', { error: (err as Error).message });\n }\n framer.stop();\n await log.close();\n process.exit(0);\n }\n\n process.once('SIGINT', () => void shutdown('SIGINT'));\n process.once('SIGTERM', () => void shutdown('SIGTERM'));\n\n // Bridge runs until SIGINT/SIGTERM — keep the event loop alive via the\n // open stdin + watcher.\n}\n\nexport { type BridgeLogger, type BridgeStateMachine, type ChildSupervisor, type StdioFramer, type UpstreamClient };\n"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * File logger for the dev bridge (issue #399).
3
+ *
4
+ * The bridge owns `process.stdout` for JSON-RPC frames — every log line
5
+ * MUST go to a file (or stderr as fallback) so a single `console.log`
6
+ * never corrupts the wire. Append-only, line-buffered, no rotation in v1
7
+ * (operators can layer `logrotate`).
8
+ */
9
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
10
+ export interface BridgeLogger {
11
+ readonly path: string | undefined;
12
+ debug(message: string, data?: Record<string, unknown>): void;
13
+ info(message: string, data?: Record<string, unknown>): void;
14
+ warn(message: string, data?: Record<string, unknown>): void;
15
+ error(message: string, data?: Record<string, unknown>): void;
16
+ reloadEvent(kind: string, data?: Record<string, unknown>): void;
17
+ close(): Promise<void>;
18
+ }
19
+ interface CreateLoggerOptions {
20
+ filePath?: string;
21
+ /** When true, write the line to stderr as well — useful when --color is set. */
22
+ alsoStderr?: boolean;
23
+ }
24
+ /**
25
+ * Create the file logger. When `filePath` is undefined or opening the file
26
+ * fails, falls back to stderr (stdout is reserved for JSON-RPC frames).
27
+ */
28
+ export declare function createBridgeLogger(options?: CreateLoggerOptions): Promise<BridgeLogger>;
29
+ export {};
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * File logger for the dev bridge (issue #399).
4
+ *
5
+ * The bridge owns `process.stdout` for JSON-RPC frames — every log line
6
+ * MUST go to a file (or stderr as fallback) so a single `console.log`
7
+ * never corrupts the wire. Append-only, line-buffered, no rotation in v1
8
+ * (operators can layer `logrotate`).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createBridgeLogger = createBridgeLogger;
12
+ const tslib_1 = require("tslib");
13
+ const fs = tslib_1.__importStar(require("node:fs"));
14
+ const utils_1 = require("@frontmcp/utils");
15
+ function formatLine(level, message, data) {
16
+ const ts = new Date().toISOString();
17
+ const payload = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : '';
18
+ return `${ts} bridge ${level} ${message}${payload}\n`;
19
+ }
20
+ /**
21
+ * Create the file logger. When `filePath` is undefined or opening the file
22
+ * fails, falls back to stderr (stdout is reserved for JSON-RPC frames).
23
+ */
24
+ async function createBridgeLogger(options = {}) {
25
+ const filePath = options.filePath;
26
+ let stream;
27
+ let resolvedPath;
28
+ if (filePath) {
29
+ try {
30
+ const absolutePath = (0, utils_1.pathResolve)(filePath);
31
+ const dir = (0, utils_1.dirname)(absolutePath);
32
+ await (0, utils_1.ensureDir)(dir);
33
+ stream = fs.createWriteStream(filePath, { flags: 'a' });
34
+ // Async write failures (disk full, FD revoked) surface here, not
35
+ // from the synchronous `stream.write(...)` call. Without this
36
+ // handler Node turns the event into an unhandled 'error' that
37
+ // crashes the bridge. Drop the stream and fall back to stderr
38
+ // for the remainder of the session.
39
+ stream.on('error', (err) => {
40
+ process.stderr.write(formatLine('error', 'logger-stream-error', { error: err.message }));
41
+ stream = undefined;
42
+ resolvedPath = undefined;
43
+ });
44
+ resolvedPath = absolutePath;
45
+ }
46
+ catch {
47
+ // Stream creation failed (read-only FS, permission denied, …).
48
+ // Fall back to stderr-only logging; stdout stays clean.
49
+ stream = undefined;
50
+ resolvedPath = undefined;
51
+ }
52
+ }
53
+ function write(level, message, data) {
54
+ const line = formatLine(level, message, data);
55
+ if (stream) {
56
+ try {
57
+ stream.write(line);
58
+ }
59
+ catch {
60
+ // Best-effort: a write failure mid-session falls back to stderr.
61
+ process.stderr.write(line);
62
+ }
63
+ }
64
+ if (!stream || options.alsoStderr) {
65
+ process.stderr.write(line);
66
+ }
67
+ }
68
+ return {
69
+ path: resolvedPath,
70
+ debug: (msg, data) => write('debug', msg, data),
71
+ info: (msg, data) => write('info', msg, data),
72
+ warn: (msg, data) => write('warn', msg, data),
73
+ error: (msg, data) => write('error', msg, data),
74
+ reloadEvent: (kind, data) => write('info', `reload-${kind}`, data),
75
+ close: () => new Promise((resolve) => {
76
+ if (!stream)
77
+ return resolve();
78
+ stream.end(() => resolve());
79
+ }),
80
+ };
81
+ }
82
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/log.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * File logger for the dev bridge (issue #399).\n *\n * The bridge owns `process.stdout` for JSON-RPC frames — every log line\n * MUST go to a file (or stderr as fallback) so a single `console.log`\n * never corrupts the wire. Append-only, line-buffered, no rotation in v1\n * (operators can layer `logrotate`).\n */\n\nimport * as fs from 'node:fs';\n\nimport { dirname, ensureDir, pathResolve } from '@frontmcp/utils';\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface BridgeLogger {\n readonly path: string | undefined;\n debug(message: string, data?: Record<string, unknown>): void;\n info(message: string, data?: Record<string, unknown>): void;\n warn(message: string, data?: Record<string, unknown>): void;\n error(message: string, data?: Record<string, unknown>): void;\n reloadEvent(kind: string, data?: Record<string, unknown>): void;\n close(): Promise<void>;\n}\n\ninterface CreateLoggerOptions {\n filePath?: string;\n /** When true, write the line to stderr as well — useful when --color is set. */\n alsoStderr?: boolean;\n}\n\nfunction formatLine(level: LogLevel, message: string, data?: Record<string, unknown>): string {\n const ts = new Date().toISOString();\n const payload = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : '';\n return `${ts} bridge ${level} ${message}${payload}\\n`;\n}\n\n/**\n * Create the file logger. When `filePath` is undefined or opening the file\n * fails, falls back to stderr (stdout is reserved for JSON-RPC frames).\n */\nexport async function createBridgeLogger(options: CreateLoggerOptions = {}): Promise<BridgeLogger> {\n const filePath = options.filePath;\n let stream: fs.WriteStream | undefined;\n let resolvedPath: string | undefined;\n\n if (filePath) {\n try {\n const absolutePath = pathResolve(filePath);\n const dir = dirname(absolutePath);\n await ensureDir(dir);\n stream = fs.createWriteStream(filePath, { flags: 'a' });\n // Async write failures (disk full, FD revoked) surface here, not\n // from the synchronous `stream.write(...)` call. Without this\n // handler Node turns the event into an unhandled 'error' that\n // crashes the bridge. Drop the stream and fall back to stderr\n // for the remainder of the session.\n stream.on('error', (err) => {\n process.stderr.write(formatLine('error', 'logger-stream-error', { error: err.message }));\n stream = undefined;\n resolvedPath = undefined;\n });\n resolvedPath = absolutePath;\n } catch {\n // Stream creation failed (read-only FS, permission denied, …).\n // Fall back to stderr-only logging; stdout stays clean.\n stream = undefined;\n resolvedPath = undefined;\n }\n }\n\n function write(level: LogLevel, message: string, data?: Record<string, unknown>): void {\n const line = formatLine(level, message, data);\n if (stream) {\n try {\n stream.write(line);\n } catch {\n // Best-effort: a write failure mid-session falls back to stderr.\n process.stderr.write(line);\n }\n }\n if (!stream || options.alsoStderr) {\n process.stderr.write(line);\n }\n }\n\n return {\n path: resolvedPath,\n debug: (msg, data) => write('debug', msg, data),\n info: (msg, data) => write('info', msg, data),\n warn: (msg, data) => write('warn', msg, data),\n error: (msg, data) => write('error', msg, data),\n reloadEvent: (kind, data) => write('info', `reload-${kind}`, data),\n close: () =>\n new Promise<void>((resolve) => {\n if (!stream) return resolve();\n stream.end(() => resolve());\n }),\n };\n}\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Bridge state machine (issue #399).
3
+ *
4
+ * Idle → Booting → Ready ⇄ Reloading
5
+ * ↓ deadline
6
+ * Degraded
7
+ *
8
+ * Owns the request buffer that absorbs inbound JSON-RPC frames while the
9
+ * upstream child is mid-restart. When buffered requests exceed the
10
+ * configured cap (default 8), the FSM synthesises an immediate
11
+ * `dev_buffer_full` response so clients never silently lose frames.
12
+ *
13
+ * The FSM is transport-agnostic — `child-supervisor` and `upstream-client`
14
+ * call into it; this module never touches sockets, child processes, or
15
+ * file descriptors.
16
+ */
17
+ import type { BridgeLogger } from './log';
18
+ import type { JsonRpcFrame } from './stdio-framer';
19
+ export type BridgeState = 'Idle' | 'Booting' | 'Ready' | 'Reloading' | 'Degraded' | 'Stopping';
20
+ export interface InflightRequest {
21
+ /** Original frame (kept for replay if the child dies before responding). */
22
+ frame: JsonRpcFrame;
23
+ /** Wall-clock when the bridge forwarded the frame to upstream. */
24
+ forwardedAt: number;
25
+ }
26
+ export interface BridgeStateMachineOptions {
27
+ log: BridgeLogger;
28
+ bufferSize: number;
29
+ reloadDeadlineMs: number;
30
+ /** Send a JSON-RPC frame back to the client (stdio out). */
31
+ respond(frame: JsonRpcFrame): void | Promise<void>;
32
+ /** Forward a request frame to the upstream child. Implemented by the supervisor. */
33
+ forward(frame: JsonRpcFrame): void | Promise<void>;
34
+ }
35
+ export interface BridgeStateMachine {
36
+ readonly state: BridgeState;
37
+ /** Bridge has begun launching the child — buffer inbound frames. */
38
+ onBootStart(): void;
39
+ /** Child reports ready — drain the buffer through `forward`. */
40
+ onChildReady(): void;
41
+ /** Child exited unexpectedly. If we were Ready, transition to Reloading. */
42
+ onChildExit(reason: string): void;
43
+ /** Watcher fired. Buffer inbound, start reload timer. */
44
+ onWatcherEvent(trigger: string): void;
45
+ /** Reload deadline elapsed without a ready signal. */
46
+ onReloadDeadline(): void;
47
+ /** Inbound JSON-RPC frame from stdin. Routes to `forward` or buffers. */
48
+ enqueue(frame: JsonRpcFrame): Promise<void>;
49
+ /** Outbound JSON-RPC frame from upstream — relay to client. */
50
+ relayUpstream(frame: JsonRpcFrame): Promise<void>;
51
+ /** SIGTERM/SIGINT — flush buffer with `dev_server_unreachable` and stop. */
52
+ stop(): Promise<void>;
53
+ /** Number of frames currently buffered (testing hook). */
54
+ bufferDepth(): number;
55
+ }
56
+ export declare function createBridgeStateMachine(options: BridgeStateMachineOptions): BridgeStateMachine;
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /**
3
+ * Bridge state machine (issue #399).
4
+ *
5
+ * Idle → Booting → Ready ⇄ Reloading
6
+ * ↓ deadline
7
+ * Degraded
8
+ *
9
+ * Owns the request buffer that absorbs inbound JSON-RPC frames while the
10
+ * upstream child is mid-restart. When buffered requests exceed the
11
+ * configured cap (default 8), the FSM synthesises an immediate
12
+ * `dev_buffer_full` response so clients never silently lose frames.
13
+ *
14
+ * The FSM is transport-agnostic — `child-supervisor` and `upstream-client`
15
+ * call into it; this module never touches sockets, child processes, or
16
+ * file descriptors.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.createBridgeStateMachine = createBridgeStateMachine;
20
+ const errors_1 = require("./errors");
21
+ function createBridgeStateMachine(options) {
22
+ const { log, bufferSize, reloadDeadlineMs, respond, forward } = options;
23
+ let state = 'Idle';
24
+ const buffer = [];
25
+ const inflight = new Map();
26
+ let reloadTimer;
27
+ // Monotonic token bumped every time the FSM leaves Ready. The async
28
+ // drain loop started in onChildReady() captures the token at start
29
+ // and bails out as soon as it changes — without this guard a watcher
30
+ // event mid-drain would let buffered frames forward into the old
31
+ // child (now being killed), producing stranded inflight entries and
32
+ // potentially duplicate responses.
33
+ let readyGen = 0;
34
+ function transition(next, info) {
35
+ if (state === next)
36
+ return;
37
+ log.info('state-transition', { from: state, to: next, ...info });
38
+ state = next;
39
+ }
40
+ function clearReloadTimer() {
41
+ if (reloadTimer) {
42
+ clearTimeout(reloadTimer);
43
+ reloadTimer = undefined;
44
+ }
45
+ }
46
+ async function flushBufferAsResponses(code, reason, data) {
47
+ while (buffer.length > 0) {
48
+ const f = buffer.shift();
49
+ if (!f)
50
+ break;
51
+ const id = f.id ?? null;
52
+ // Notifications (no id) can't get a response — drop with a log line.
53
+ if (id === null) {
54
+ log.warn('drop-notification', { method: f.method, reason });
55
+ continue;
56
+ }
57
+ await respond((0, errors_1.makeDevError)(id, code, { reason, ...data }));
58
+ }
59
+ }
60
+ async function failInflightAsResponses(reason, data) {
61
+ for (const [id, req] of inflight) {
62
+ const idVal = typeof id === 'number' ? id : id;
63
+ await respond((0, errors_1.makeDevError)(idVal, errors_1.DEV_SERVER_UNREACHABLE, { reason, ...data, method: req.frame.method }));
64
+ }
65
+ inflight.clear();
66
+ }
67
+ return {
68
+ get state() {
69
+ return state;
70
+ },
71
+ bufferDepth: () => buffer.length,
72
+ onBootStart() {
73
+ transition('Booting');
74
+ },
75
+ onChildReady() {
76
+ clearReloadTimer();
77
+ transition('Ready', { bufferDepth: buffer.length });
78
+ // Drain buffered requests in FIFO; preserve order on the wire.
79
+ const drain = [...buffer];
80
+ buffer.length = 0;
81
+ const drainGen = readyGen;
82
+ void (async () => {
83
+ for (const f of drain) {
84
+ // Bail out if the FSM left Ready (watcher event, child exit,
85
+ // or stop()) — otherwise this drain would forward into a
86
+ // child that's already being killed and duplicate responses
87
+ // already sent by `failInflightAsResponses`.
88
+ if (readyGen !== drainGen)
89
+ return;
90
+ const requestId = typeof f.id === 'string' || typeof f.id === 'number' ? f.id : null;
91
+ // Re-mark as inflight when forwarding; relayUpstream will clear it.
92
+ if (requestId !== null) {
93
+ inflight.set(requestId, { frame: f, forwardedAt: Date.now() });
94
+ }
95
+ try {
96
+ await forward(f);
97
+ }
98
+ catch (err) {
99
+ log.error('forward-failed', { error: err.message });
100
+ // Mirror the live-enqueue failure path: a request that never
101
+ // reached the child must still resolve on the wire, otherwise
102
+ // the MCP client sits on `Calling…` forever. Notifications
103
+ // (`requestId === null`) have no caller waiting on a response.
104
+ // Only respond if we still own this generation — otherwise
105
+ // failInflightAsResponses() already sent the error.
106
+ if (requestId !== null && readyGen === drainGen) {
107
+ inflight.delete(requestId);
108
+ await respond((0, errors_1.makeDevError)(requestId, errors_1.DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));
109
+ }
110
+ }
111
+ }
112
+ })();
113
+ },
114
+ onChildExit(reason) {
115
+ log.warn('child-exit', { reason, state });
116
+ if (state === 'Stopping')
117
+ return; // expected
118
+ // Treat as a reload trigger if we were Ready; otherwise stay where we are.
119
+ if (state === 'Ready' || state === 'Booting') {
120
+ // Invalidate any in-progress drain that captured the prior readyGen.
121
+ readyGen++;
122
+ transition('Reloading', { trigger: 'child-exit', reason });
123
+ // Inflight requests will never get a real response — synthesise one.
124
+ void failInflightAsResponses('child_exit', { reason });
125
+ scheduleReloadDeadline();
126
+ }
127
+ else if (state === 'Reloading') {
128
+ // Two exits in a row — keep waiting for boot
129
+ }
130
+ },
131
+ onWatcherEvent(trigger) {
132
+ if (state === 'Stopping' || state === 'Degraded')
133
+ return;
134
+ log.reloadEvent('start', { trigger });
135
+ // Invalidate any in-progress drain so it stops forwarding into
136
+ // the child that's about to be killed.
137
+ readyGen++;
138
+ transition('Reloading', { trigger });
139
+ // Inflight requests will likely be killed when supervisor kills the
140
+ // child. Respond now so the client spinner clears immediately.
141
+ void failInflightAsResponses('reload', { trigger });
142
+ scheduleReloadDeadline();
143
+ },
144
+ onReloadDeadline() {
145
+ log.error('reload-deadline-elapsed', { bufferDepth: buffer.length });
146
+ transition('Degraded', { reason: 'reload_deadline' });
147
+ // Deadline path → DEV_RELOAD_DEADLINE (not DEV_SERVER_UNREACHABLE).
148
+ // The two map to distinct public error codes so clients can
149
+ // distinguish "watcher reload took too long" from "child crashed".
150
+ void flushBufferAsResponses(errors_1.DEV_RELOAD_DEADLINE, 'deadline', { deadlineMs: reloadDeadlineMs });
151
+ },
152
+ async enqueue(frame) {
153
+ const isRequest = frame.id !== undefined && frame.id !== null;
154
+ if (state === 'Ready') {
155
+ if (isRequest) {
156
+ inflight.set(frame.id, { frame, forwardedAt: Date.now() });
157
+ }
158
+ try {
159
+ await forward(frame);
160
+ }
161
+ catch (err) {
162
+ log.error('forward-failed', { error: err.message });
163
+ if (isRequest) {
164
+ inflight.delete(frame.id);
165
+ await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));
166
+ }
167
+ }
168
+ return;
169
+ }
170
+ if (state === 'Degraded' || state === 'Stopping') {
171
+ // Both states are terminal for new traffic — Degraded is post-
172
+ // deadline (user code is broken), Stopping means SIGINT/SIGTERM
173
+ // already fired. Buffering here is wrong because nothing will
174
+ // drain it. Reject so the client spinner clears immediately.
175
+ if (isRequest) {
176
+ await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_SERVER_UNREACHABLE, {
177
+ reason: state === 'Stopping' ? 'stopping' : 'degraded',
178
+ }));
179
+ }
180
+ else {
181
+ log.warn('drop-notification', {
182
+ method: frame.method,
183
+ reason: state === 'Stopping' ? 'stopping' : 'degraded',
184
+ });
185
+ }
186
+ return;
187
+ }
188
+ // Idle / Booting / Reloading → buffer (will drain on onChildReady)
189
+ if (buffer.length >= bufferSize) {
190
+ if (isRequest) {
191
+ await respond((0, errors_1.makeDevError)(frame.id ?? null, errors_1.DEV_BUFFER_FULL, { capacity: bufferSize }));
192
+ }
193
+ else {
194
+ log.warn('drop-notification', { method: frame.method, reason: 'buffer_full' });
195
+ }
196
+ return;
197
+ }
198
+ buffer.push(frame);
199
+ },
200
+ async relayUpstream(frame) {
201
+ // Clear inflight if this is a response to a known id
202
+ if (frame.id !== undefined && frame.id !== null && (frame.result !== undefined || frame.error !== undefined)) {
203
+ inflight.delete(frame.id);
204
+ }
205
+ await respond(frame);
206
+ },
207
+ async stop() {
208
+ // Invalidate any in-progress drain — same reason as the watcher /
209
+ // child-exit paths: avoid forwarding into a child we're tearing
210
+ // down.
211
+ readyGen++;
212
+ transition('Stopping');
213
+ clearReloadTimer();
214
+ // Inflight + buffered: respond once so the client's pending RPCs don't
215
+ // dangle past the bridge exit.
216
+ await failInflightAsResponses('stopping');
217
+ await flushBufferAsResponses(errors_1.DEV_SERVER_UNREACHABLE, 'stopping');
218
+ },
219
+ };
220
+ function scheduleReloadDeadline() {
221
+ clearReloadTimer();
222
+ reloadTimer = setTimeout(() => {
223
+ if (state === 'Reloading' || state === 'Booting') {
224
+ // Mark deadline reached — supervisor stays alive, watcher retries.
225
+ log.error('reload-deadline-fired');
226
+ transition('Degraded', { reason: 'reload_deadline' });
227
+ // Drain buffer with deadline-shaped error.
228
+ void (async () => {
229
+ while (buffer.length > 0) {
230
+ const f = buffer.shift();
231
+ if (!f)
232
+ break;
233
+ const id = f.id ?? null;
234
+ if (id === null) {
235
+ log.warn('drop-notification', { method: f.method, reason: 'deadline' });
236
+ continue;
237
+ }
238
+ await respond((0, errors_1.makeDevError)(id, errors_1.DEV_RELOAD_DEADLINE, { deadlineMs: reloadDeadlineMs }));
239
+ }
240
+ })();
241
+ }
242
+ }, reloadDeadlineMs).unref();
243
+ }
244
+ }
245
+ //# sourceMappingURL=state-machine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-machine.js","sourceRoot":"","sources":["../../../../../src/commands/dev/bridge/state-machine.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;AA+CH,4DAuOC;AApRD,qCAAsG;AA6CtG,SAAgB,wBAAwB,CAAC,OAAkC;IACzE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACxE,IAAI,KAAK,GAAgB,MAAM,CAAC;IAChC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoC,CAAC;IAC7D,IAAI,WAAuC,CAAC;IAC5C,oEAAoE;IACpE,mEAAmE;IACnE,qEAAqE;IACrE,iEAAiE;IACjE,oEAAoE;IACpE,mCAAmC;IACnC,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,SAAS,UAAU,CAAC,IAAiB,EAAE,IAA8B;QACnE,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO;QAC3B,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QACjE,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;IAED,SAAS,gBAAgB;QACvB,IAAI,WAAW,EAAE,CAAC;YAChB,YAAY,CAAC,WAAW,CAAC,CAAC;YAC1B,WAAW,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,KAAK,UAAU,sBAAsB,CAAC,IAAY,EAAE,MAAc,EAAE,IAA8B;QAChG,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,CAAC;gBAAE,MAAM;YACd,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC;YACxB,qEAAqE;YACrE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBAChB,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC5D,SAAS;YACX,CAAC;YACD,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,IAA8B;QACnF,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,EAAa,CAAC;YAC3D,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,KAAK,EAAE,+BAAsB,EAAE,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC5G,CAAC;QACD,QAAQ,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,OAAO;QACL,IAAI,KAAK;YACP,OAAO,KAAK,CAAC;QACf,CAAC;QACD,WAAW,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM;QAEhC,WAAW;YACT,UAAU,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC;QAED,YAAY;YACV,gBAAgB,EAAE,CAAC;YACnB,UAAU,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YACpD,+DAA+D;YAC/D,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;YAC1B,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAClB,MAAM,QAAQ,GAAG,QAAQ,CAAC;YAC1B,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;oBACtB,6DAA6D;oBAC7D,yDAAyD;oBACzD,4DAA4D;oBAC5D,6CAA6C;oBAC7C,IAAI,QAAQ,KAAK,QAAQ;wBAAE,OAAO;oBAClC,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;oBACrF,oEAAoE;oBACpE,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;wBACvB,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBACjE,CAAC;oBACD,IAAI,CAAC;wBACH,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;oBACnB,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;wBAC/D,6DAA6D;wBAC7D,8DAA8D;wBAC9D,2DAA2D;wBAC3D,+DAA+D;wBAC/D,2DAA2D;wBAC3D,oDAAoD;wBACpD,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;4BAChD,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;4BAC3B,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,SAAS,EAAE,+BAAsB,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;wBAC/F,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QAED,WAAW,CAAC,MAAM;YAChB,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,UAAU;gBAAE,OAAO,CAAC,WAAW;YAC7C,2EAA2E;YAC3E,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBAC7C,qEAAqE;gBACrE,QAAQ,EAAE,CAAC;gBACX,UAAU,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC3D,qEAAqE;gBACrE,KAAK,uBAAuB,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;gBACvD,sBAAsB,EAAE,CAAC;YAC3B,CAAC;iBAAM,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;gBACjC,6CAA6C;YAC/C,CAAC;QACH,CAAC;QAED,cAAc,CAAC,OAAO;YACpB,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU;gBAAE,OAAO;YACzD,GAAG,CAAC,WAAW,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YACtC,+DAA+D;YAC/D,uCAAuC;YACvC,QAAQ,EAAE,CAAC;YACX,UAAU,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YACrC,oEAAoE;YACpE,+DAA+D;YAC/D,KAAK,uBAAuB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YACpD,sBAAsB,EAAE,CAAC;QAC3B,CAAC;QAED,gBAAgB;YACd,GAAG,CAAC,KAAK,CAAC,yBAAyB,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YACrE,UAAU,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC;YACtD,oEAAoE;YACpE,4DAA4D;YAC5D,mEAAmE;YACnE,KAAK,sBAAsB,CAAC,4BAAmB,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACjG,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,KAAmB;YAC/B,MAAM,SAAS,GAAG,KAAK,CAAC,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI,CAAC;YAE9D,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;gBACtB,IAAI,SAAS,EAAE,CAAC;oBACd,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAqB,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAChF,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC/D,IAAI,SAAS,EAAE,CAAC;wBACd,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAqB,CAAC,CAAC;wBAC7C,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,KAAK,CAAC,EAAE,IAAI,IAAI,EAAE,+BAAsB,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;oBACtG,CAAC;gBACH,CAAC;gBACD,OAAO;YACT,CAAC;YAED,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;gBACjD,+DAA+D;gBAC/D,gEAAgE;gBAChE,8DAA8D;gBAC9D,6DAA6D;gBAC7D,IAAI,SAAS,EAAE,CAAC;oBACd,MAAM,OAAO,CACX,IAAA,qBAAY,EAAC,KAAK,CAAC,EAAE,IAAI,IAAI,EAAE,+BAAsB,EAAE;wBACrD,MAAM,EAAE,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;qBACvD,CAAC,CACH,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE;wBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;wBACpB,MAAM,EAAE,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;qBACvD,CAAC,CAAC;gBACL,CAAC;gBACD,OAAO;YACT,CAAC;YAED,mEAAmE;YACnE,IAAI,MAAM,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;gBAChC,IAAI,SAAS,EAAE,CAAC;oBACd,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,KAAK,CAAC,EAAE,IAAI,IAAI,EAAE,wBAAe,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;gBAC3F,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;gBACjF,CAAC;gBACD,OAAO;YACT,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,KAAmB;YACrC,qDAAqD;YACrD,IAAI,KAAK,CAAC,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,CAAC;gBAC7G,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC5B,CAAC;YACD,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QAED,KAAK,CAAC,IAAI;YACR,kEAAkE;YAClE,gEAAgE;YAChE,QAAQ;YACR,QAAQ,EAAE,CAAC;YACX,UAAU,CAAC,UAAU,CAAC,CAAC;YACvB,gBAAgB,EAAE,CAAC;YACnB,uEAAuE;YACvE,+BAA+B;YAC/B,MAAM,uBAAuB,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,sBAAsB,CAAC,+BAAsB,EAAE,UAAU,CAAC,CAAC;QACnE,CAAC;KACF,CAAC;IAEF,SAAS,sBAAsB;QAC7B,gBAAgB,EAAE,CAAC;QACnB,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACjD,mEAAmE;gBACnE,GAAG,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;gBACnC,UAAU,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC;gBACtD,2CAA2C;gBAC3C,KAAK,CAAC,KAAK,IAAI,EAAE;oBACf,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACzB,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;wBACzB,IAAI,CAAC,CAAC;4BAAE,MAAM;wBACd,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC;wBACxB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;4BAChB,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;4BACxE,SAAS;wBACX,CAAC;wBACD,MAAM,OAAO,CAAC,IAAA,qBAAY,EAAC,EAAE,EAAE,4BAAmB,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;oBACzF,CAAC;gBACH,CAAC,CAAC,EAAE,CAAC;YACP,CAAC;QACH,CAAC,EAAE,gBAAgB,CAAC,CAAC,KAAK,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC","sourcesContent":["/**\n * Bridge state machine (issue #399).\n *\n * Idle → Booting → Ready ⇄ Reloading\n * ↓ deadline\n * Degraded\n *\n * Owns the request buffer that absorbs inbound JSON-RPC frames while the\n * upstream child is mid-restart. When buffered requests exceed the\n * configured cap (default 8), the FSM synthesises an immediate\n * `dev_buffer_full` response so clients never silently lose frames.\n *\n * The FSM is transport-agnostic — `child-supervisor` and `upstream-client`\n * call into it; this module never touches sockets, child processes, or\n * file descriptors.\n */\n\nimport { DEV_BUFFER_FULL, DEV_RELOAD_DEADLINE, DEV_SERVER_UNREACHABLE, makeDevError } from './errors';\nimport type { BridgeLogger } from './log';\nimport type { JsonRpcFrame } from './stdio-framer';\n\nexport type BridgeState = 'Idle' | 'Booting' | 'Ready' | 'Reloading' | 'Degraded' | 'Stopping';\n\nexport interface InflightRequest {\n /** Original frame (kept for replay if the child dies before responding). */\n frame: JsonRpcFrame;\n /** Wall-clock when the bridge forwarded the frame to upstream. */\n forwardedAt: number;\n}\n\nexport interface BridgeStateMachineOptions {\n log: BridgeLogger;\n bufferSize: number;\n reloadDeadlineMs: number;\n /** Send a JSON-RPC frame back to the client (stdio out). */\n respond(frame: JsonRpcFrame): void | Promise<void>;\n /** Forward a request frame to the upstream child. Implemented by the supervisor. */\n forward(frame: JsonRpcFrame): void | Promise<void>;\n}\n\nexport interface BridgeStateMachine {\n readonly state: BridgeState;\n /** Bridge has begun launching the child — buffer inbound frames. */\n onBootStart(): void;\n /** Child reports ready — drain the buffer through `forward`. */\n onChildReady(): void;\n /** Child exited unexpectedly. If we were Ready, transition to Reloading. */\n onChildExit(reason: string): void;\n /** Watcher fired. Buffer inbound, start reload timer. */\n onWatcherEvent(trigger: string): void;\n /** Reload deadline elapsed without a ready signal. */\n onReloadDeadline(): void;\n /** Inbound JSON-RPC frame from stdin. Routes to `forward` or buffers. */\n enqueue(frame: JsonRpcFrame): Promise<void>;\n /** Outbound JSON-RPC frame from upstream — relay to client. */\n relayUpstream(frame: JsonRpcFrame): Promise<void>;\n /** SIGTERM/SIGINT — flush buffer with `dev_server_unreachable` and stop. */\n stop(): Promise<void>;\n /** Number of frames currently buffered (testing hook). */\n bufferDepth(): number;\n}\n\nexport function createBridgeStateMachine(options: BridgeStateMachineOptions): BridgeStateMachine {\n const { log, bufferSize, reloadDeadlineMs, respond, forward } = options;\n let state: BridgeState = 'Idle';\n const buffer: JsonRpcFrame[] = [];\n const inflight = new Map<string | number, InflightRequest>();\n let reloadTimer: NodeJS.Timeout | undefined;\n // Monotonic token bumped every time the FSM leaves Ready. The async\n // drain loop started in onChildReady() captures the token at start\n // and bails out as soon as it changes — without this guard a watcher\n // event mid-drain would let buffered frames forward into the old\n // child (now being killed), producing stranded inflight entries and\n // potentially duplicate responses.\n let readyGen = 0;\n\n function transition(next: BridgeState, info?: Record<string, unknown>): void {\n if (state === next) return;\n log.info('state-transition', { from: state, to: next, ...info });\n state = next;\n }\n\n function clearReloadTimer(): void {\n if (reloadTimer) {\n clearTimeout(reloadTimer);\n reloadTimer = undefined;\n }\n }\n\n async function flushBufferAsResponses(code: number, reason: string, data?: Record<string, unknown>): Promise<void> {\n while (buffer.length > 0) {\n const f = buffer.shift();\n if (!f) break;\n const id = f.id ?? null;\n // Notifications (no id) can't get a response — drop with a log line.\n if (id === null) {\n log.warn('drop-notification', { method: f.method, reason });\n continue;\n }\n await respond(makeDevError(id, code, { reason, ...data }));\n }\n }\n\n async function failInflightAsResponses(reason: string, data?: Record<string, unknown>): Promise<void> {\n for (const [id, req] of inflight) {\n const idVal = typeof id === 'number' ? id : (id as string);\n await respond(makeDevError(idVal, DEV_SERVER_UNREACHABLE, { reason, ...data, method: req.frame.method }));\n }\n inflight.clear();\n }\n\n return {\n get state() {\n return state;\n },\n bufferDepth: () => buffer.length,\n\n onBootStart() {\n transition('Booting');\n },\n\n onChildReady() {\n clearReloadTimer();\n transition('Ready', { bufferDepth: buffer.length });\n // Drain buffered requests in FIFO; preserve order on the wire.\n const drain = [...buffer];\n buffer.length = 0;\n const drainGen = readyGen;\n void (async () => {\n for (const f of drain) {\n // Bail out if the FSM left Ready (watcher event, child exit,\n // or stop()) — otherwise this drain would forward into a\n // child that's already being killed and duplicate responses\n // already sent by `failInflightAsResponses`.\n if (readyGen !== drainGen) return;\n const requestId = typeof f.id === 'string' || typeof f.id === 'number' ? f.id : null;\n // Re-mark as inflight when forwarding; relayUpstream will clear it.\n if (requestId !== null) {\n inflight.set(requestId, { frame: f, forwardedAt: Date.now() });\n }\n try {\n await forward(f);\n } catch (err) {\n log.error('forward-failed', { error: (err as Error).message });\n // Mirror the live-enqueue failure path: a request that never\n // reached the child must still resolve on the wire, otherwise\n // the MCP client sits on `Calling…` forever. Notifications\n // (`requestId === null`) have no caller waiting on a response.\n // Only respond if we still own this generation — otherwise\n // failInflightAsResponses() already sent the error.\n if (requestId !== null && readyGen === drainGen) {\n inflight.delete(requestId);\n await respond(makeDevError(requestId, DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));\n }\n }\n }\n })();\n },\n\n onChildExit(reason) {\n log.warn('child-exit', { reason, state });\n if (state === 'Stopping') return; // expected\n // Treat as a reload trigger if we were Ready; otherwise stay where we are.\n if (state === 'Ready' || state === 'Booting') {\n // Invalidate any in-progress drain that captured the prior readyGen.\n readyGen++;\n transition('Reloading', { trigger: 'child-exit', reason });\n // Inflight requests will never get a real response — synthesise one.\n void failInflightAsResponses('child_exit', { reason });\n scheduleReloadDeadline();\n } else if (state === 'Reloading') {\n // Two exits in a row — keep waiting for boot\n }\n },\n\n onWatcherEvent(trigger) {\n if (state === 'Stopping' || state === 'Degraded') return;\n log.reloadEvent('start', { trigger });\n // Invalidate any in-progress drain so it stops forwarding into\n // the child that's about to be killed.\n readyGen++;\n transition('Reloading', { trigger });\n // Inflight requests will likely be killed when supervisor kills the\n // child. Respond now so the client spinner clears immediately.\n void failInflightAsResponses('reload', { trigger });\n scheduleReloadDeadline();\n },\n\n onReloadDeadline() {\n log.error('reload-deadline-elapsed', { bufferDepth: buffer.length });\n transition('Degraded', { reason: 'reload_deadline' });\n // Deadline path → DEV_RELOAD_DEADLINE (not DEV_SERVER_UNREACHABLE).\n // The two map to distinct public error codes so clients can\n // distinguish \"watcher reload took too long\" from \"child crashed\".\n void flushBufferAsResponses(DEV_RELOAD_DEADLINE, 'deadline', { deadlineMs: reloadDeadlineMs });\n },\n\n async enqueue(frame: JsonRpcFrame) {\n const isRequest = frame.id !== undefined && frame.id !== null;\n\n if (state === 'Ready') {\n if (isRequest) {\n inflight.set(frame.id as string | number, { frame, forwardedAt: Date.now() });\n }\n try {\n await forward(frame);\n } catch (err) {\n log.error('forward-failed', { error: (err as Error).message });\n if (isRequest) {\n inflight.delete(frame.id as string | number);\n await respond(makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, { reason: 'forward_failed' }));\n }\n }\n return;\n }\n\n if (state === 'Degraded' || state === 'Stopping') {\n // Both states are terminal for new traffic — Degraded is post-\n // deadline (user code is broken), Stopping means SIGINT/SIGTERM\n // already fired. Buffering here is wrong because nothing will\n // drain it. Reject so the client spinner clears immediately.\n if (isRequest) {\n await respond(\n makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, {\n reason: state === 'Stopping' ? 'stopping' : 'degraded',\n }),\n );\n } else {\n log.warn('drop-notification', {\n method: frame.method,\n reason: state === 'Stopping' ? 'stopping' : 'degraded',\n });\n }\n return;\n }\n\n // Idle / Booting / Reloading → buffer (will drain on onChildReady)\n if (buffer.length >= bufferSize) {\n if (isRequest) {\n await respond(makeDevError(frame.id ?? null, DEV_BUFFER_FULL, { capacity: bufferSize }));\n } else {\n log.warn('drop-notification', { method: frame.method, reason: 'buffer_full' });\n }\n return;\n }\n buffer.push(frame);\n },\n\n async relayUpstream(frame: JsonRpcFrame) {\n // Clear inflight if this is a response to a known id\n if (frame.id !== undefined && frame.id !== null && (frame.result !== undefined || frame.error !== undefined)) {\n inflight.delete(frame.id);\n }\n await respond(frame);\n },\n\n async stop() {\n // Invalidate any in-progress drain — same reason as the watcher /\n // child-exit paths: avoid forwarding into a child we're tearing\n // down.\n readyGen++;\n transition('Stopping');\n clearReloadTimer();\n // Inflight + buffered: respond once so the client's pending RPCs don't\n // dangle past the bridge exit.\n await failInflightAsResponses('stopping');\n await flushBufferAsResponses(DEV_SERVER_UNREACHABLE, 'stopping');\n },\n };\n\n function scheduleReloadDeadline(): void {\n clearReloadTimer();\n reloadTimer = setTimeout(() => {\n if (state === 'Reloading' || state === 'Booting') {\n // Mark deadline reached — supervisor stays alive, watcher retries.\n log.error('reload-deadline-fired');\n transition('Degraded', { reason: 'reload_deadline' });\n // Drain buffer with deadline-shaped error.\n void (async () => {\n while (buffer.length > 0) {\n const f = buffer.shift();\n if (!f) break;\n const id = f.id ?? null;\n if (id === null) {\n log.warn('drop-notification', { method: f.method, reason: 'deadline' });\n continue;\n }\n await respond(makeDevError(id, DEV_RELOAD_DEADLINE, { deadlineMs: reloadDeadlineMs }));\n }\n })();\n }\n }, reloadDeadlineMs).unref();\n }\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Newline-delimited JSON-RPC framer for the dev bridge (issue #399).
3
+ *
4
+ * Reads `process.stdin` and yields complete JSON-RPC frames. Writes
5
+ * complete frames to `process.stdout` (atomic per `\n` boundary). Parser
6
+ * state is preserved across chunks so frames split arbitrarily on the
7
+ * wire still parse.
8
+ *
9
+ * MCP stdio framing per spec is newline-delimited JSON (`\n`-terminated
10
+ * UTF-8). We do NOT implement LSP-style `Content-Length` framing — it's
11
+ * not in the MCP spec and adding it would silently shadow real JSON
12
+ * bodies that happen to start with `C`.
13
+ */
14
+ import type { Readable, Writable } from 'node:stream';
15
+ import type { BridgeLogger } from './log';
16
+ export interface JsonRpcFrame {
17
+ jsonrpc: '2.0';
18
+ id?: string | number | null;
19
+ method?: string;
20
+ params?: unknown;
21
+ result?: unknown;
22
+ error?: {
23
+ code: number;
24
+ message: string;
25
+ data?: unknown;
26
+ };
27
+ }
28
+ export interface StdioFramerOptions {
29
+ input: Readable;
30
+ output: Writable;
31
+ log: BridgeLogger;
32
+ onFrame: (frame: JsonRpcFrame) => void | Promise<void>;
33
+ }
34
+ export interface StdioFramer {
35
+ start(): void;
36
+ /** Write a single frame as a newline-terminated JSON line. Resolves on `'drain'` when backpressure kicks in. */
37
+ write(frame: JsonRpcFrame): Promise<void>;
38
+ stop(): void;
39
+ }
40
+ /**
41
+ * Build a newline-delimited JSON framer bound to the supplied streams.
42
+ *
43
+ * Parse errors (malformed JSON between newlines) emit a `-32700` Parse
44
+ * error response back on the output stream and continue — a malformed
45
+ * frame must not kill the bridge.
46
+ */
47
+ export declare function createStdioFramer(options: StdioFramerOptions): StdioFramer;