@provos/ironcurtain 0.7.2 → 0.9.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 (259) hide show
  1. package/README.md +29 -100
  2. package/dist/auth/auth-command.d.ts +15 -0
  3. package/dist/auth/auth-command.js +424 -0
  4. package/dist/auth/auth-command.js.map +1 -0
  5. package/dist/auth/oauth-flow.d.ts +46 -0
  6. package/dist/auth/oauth-flow.js +279 -0
  7. package/dist/auth/oauth-flow.js.map +1 -0
  8. package/dist/auth/oauth-provider.d.ts +55 -0
  9. package/dist/auth/oauth-provider.js +73 -0
  10. package/dist/auth/oauth-provider.js.map +1 -0
  11. package/dist/auth/oauth-registry.d.ts +34 -0
  12. package/dist/auth/oauth-registry.js +69 -0
  13. package/dist/auth/oauth-registry.js.map +1 -0
  14. package/dist/auth/oauth-token-provider.d.ts +52 -0
  15. package/dist/auth/oauth-token-provider.js +159 -0
  16. package/dist/auth/oauth-token-provider.js.map +1 -0
  17. package/dist/auth/oauth-token-store.d.ts +30 -0
  18. package/dist/auth/oauth-token-store.js +93 -0
  19. package/dist/auth/oauth-token-store.js.map +1 -0
  20. package/dist/auth/providers/google-scopes.d.ts +35 -0
  21. package/dist/auth/providers/google-scopes.js +183 -0
  22. package/dist/auth/providers/google-scopes.js.map +1 -0
  23. package/dist/auth/providers/google.d.ts +2 -0
  24. package/dist/auth/providers/google.js +28 -0
  25. package/dist/auth/providers/google.js.map +1 -0
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config/config-command.js +39 -1
  29. package/dist/config/config-command.js.map +1 -1
  30. package/dist/config/constitution-readonly.md +14 -11
  31. package/dist/config/constitution-user-base.md +15 -0
  32. package/dist/config/first-start.js +4 -1
  33. package/dist/config/first-start.js.map +1 -1
  34. package/dist/config/generated/test-scenarios.json +1 -1
  35. package/dist/config/generated/tool-annotations.json +10617 -296
  36. package/dist/config/generated-readonly/compiled-policy.json +787 -244
  37. package/dist/config/index.d.ts +2 -2
  38. package/dist/config/index.js +8 -0
  39. package/dist/config/index.js.map +1 -1
  40. package/dist/config/mcp-servers.json +31 -2
  41. package/dist/config/paths.d.ts +20 -0
  42. package/dist/config/paths.js +43 -0
  43. package/dist/config/paths.js.map +1 -1
  44. package/dist/config/types.d.ts +8 -0
  45. package/dist/config/user-config.d.ts +29 -0
  46. package/dist/config/user-config.js +30 -0
  47. package/dist/config/user-config.js.map +1 -1
  48. package/dist/cron/constitution-generator.d.ts +2 -1
  49. package/dist/cron/constitution-generator.js +35 -36
  50. package/dist/cron/constitution-generator.js.map +1 -1
  51. package/dist/cron/headless-transport.d.ts +4 -0
  52. package/dist/cron/headless-transport.js +1 -1
  53. package/dist/cron/headless-transport.js.map +1 -1
  54. package/dist/daemon/ironcurtain-daemon.js +7 -1
  55. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  56. package/dist/docker/adapters/claude-code.js +32 -5
  57. package/dist/docker/adapters/claude-code.js.map +1 -1
  58. package/dist/docker/agent-adapter.d.ts +40 -0
  59. package/dist/docker/claude-md-seed.d.ts +27 -0
  60. package/dist/docker/claude-md-seed.js +42 -0
  61. package/dist/docker/claude-md-seed.js.map +1 -0
  62. package/dist/docker/docker-agent-session.d.ts +3 -1
  63. package/dist/docker/docker-agent-session.js +51 -28
  64. package/dist/docker/docker-agent-session.js.map +1 -1
  65. package/dist/docker/docker-infrastructure.d.ts +15 -1
  66. package/dist/docker/docker-infrastructure.js +105 -9
  67. package/dist/docker/docker-infrastructure.js.map +1 -1
  68. package/dist/docker/docker-manager.d.ts +2 -0
  69. package/dist/docker/docker-manager.js +9 -1
  70. package/dist/docker/docker-manager.js.map +1 -1
  71. package/dist/docker/mitm-proxy.d.ts +65 -0
  72. package/dist/docker/mitm-proxy.js +288 -31
  73. package/dist/docker/mitm-proxy.js.map +1 -1
  74. package/dist/docker/package-types.d.ts +127 -0
  75. package/dist/docker/package-types.js +8 -0
  76. package/dist/docker/package-types.js.map +1 -0
  77. package/dist/docker/package-validator.d.ts +41 -0
  78. package/dist/docker/package-validator.js +90 -0
  79. package/dist/docker/package-validator.js.map +1 -0
  80. package/dist/docker/platform.d.ts +6 -6
  81. package/dist/docker/platform.js +8 -6
  82. package/dist/docker/platform.js.map +1 -1
  83. package/dist/docker/proxy-tools.d.ts +80 -0
  84. package/dist/docker/proxy-tools.js +248 -0
  85. package/dist/docker/proxy-tools.js.map +1 -0
  86. package/dist/docker/pty-session.d.ts +15 -0
  87. package/dist/docker/pty-session.js +253 -73
  88. package/dist/docker/pty-session.js.map +1 -1
  89. package/dist/docker/pty-types.d.ts +28 -0
  90. package/dist/docker/pty-types.js +2 -0
  91. package/dist/docker/pty-types.js.map +1 -1
  92. package/dist/docker/registry-proxy.d.ts +142 -0
  93. package/dist/docker/registry-proxy.js +856 -0
  94. package/dist/docker/registry-proxy.js.map +1 -0
  95. package/dist/escalation/escalation-watcher.d.ts +3 -1
  96. package/dist/escalation/escalation-watcher.js +6 -2
  97. package/dist/escalation/escalation-watcher.js.map +1 -1
  98. package/dist/escalation/listener-command.js +24 -10
  99. package/dist/escalation/listener-command.js.map +1 -1
  100. package/dist/escalation/listener-lock.d.ts +5 -0
  101. package/dist/escalation/listener-lock.js +14 -0
  102. package/dist/escalation/listener-lock.js.map +1 -1
  103. package/dist/index.js +16 -3
  104. package/dist/index.js.map +1 -1
  105. package/dist/memory/auto-save.d.ts +25 -0
  106. package/dist/memory/auto-save.js +89 -0
  107. package/dist/memory/auto-save.js.map +1 -0
  108. package/dist/memory/memory-annotations.d.ts +47 -0
  109. package/dist/memory/memory-annotations.js +87 -0
  110. package/dist/memory/memory-annotations.js.map +1 -0
  111. package/dist/memory/memory-prompt.d.ts +12 -0
  112. package/dist/memory/memory-prompt.js +20 -0
  113. package/dist/memory/memory-prompt.js.map +1 -0
  114. package/dist/memory/resolve-memory-path.d.ts +20 -0
  115. package/dist/memory/resolve-memory-path.js +30 -0
  116. package/dist/memory/resolve-memory-path.js.map +1 -0
  117. package/dist/mux/mux-app.d.ts +4 -0
  118. package/dist/mux/mux-app.js +246 -28
  119. package/dist/mux/mux-app.js.map +1 -1
  120. package/dist/mux/mux-command.d.ts +2 -2
  121. package/dist/mux/mux-command.js +14 -22
  122. package/dist/mux/mux-command.js.map +1 -1
  123. package/dist/mux/mux-escalation-manager.d.ts +9 -3
  124. package/dist/mux/mux-escalation-manager.js +69 -19
  125. package/dist/mux/mux-escalation-manager.js.map +1 -1
  126. package/dist/mux/mux-input-handler.d.ts +59 -3
  127. package/dist/mux/mux-input-handler.js +270 -32
  128. package/dist/mux/mux-input-handler.js.map +1 -1
  129. package/dist/mux/mux-renderer.d.ts +19 -1
  130. package/dist/mux/mux-renderer.js +466 -11
  131. package/dist/mux/mux-renderer.js.map +1 -1
  132. package/dist/mux/persona-scanner.d.ts +19 -0
  133. package/dist/mux/persona-scanner.js +53 -0
  134. package/dist/mux/persona-scanner.js.map +1 -0
  135. package/dist/mux/pty-bridge.d.ts +8 -0
  136. package/dist/mux/pty-bridge.js +19 -1
  137. package/dist/mux/pty-bridge.js.map +1 -1
  138. package/dist/mux/session-scanner.d.ts +18 -0
  139. package/dist/mux/session-scanner.js +65 -0
  140. package/dist/mux/session-scanner.js.map +1 -0
  141. package/dist/mux/types.d.ts +36 -1
  142. package/dist/mux/types.js +14 -1
  143. package/dist/mux/types.js.map +1 -1
  144. package/dist/persona/persona-command.js +130 -93
  145. package/dist/persona/persona-command.js.map +1 -1
  146. package/dist/persona/persona-prompt.d.ts +7 -7
  147. package/dist/persona/persona-prompt.js +14 -29
  148. package/dist/persona/persona-prompt.js.map +1 -1
  149. package/dist/persona/resolve.js +5 -2
  150. package/dist/persona/resolve.js.map +1 -1
  151. package/dist/pipeline/annotate.js +1 -7
  152. package/dist/pipeline/annotate.js.map +1 -1
  153. package/dist/pipeline/compile.d.ts +5 -2
  154. package/dist/pipeline/compile.js +23 -11
  155. package/dist/pipeline/compile.js.map +1 -1
  156. package/dist/pipeline/constitution-compiler.d.ts +302 -3
  157. package/dist/pipeline/constitution-compiler.js +285 -23
  158. package/dist/pipeline/constitution-compiler.js.map +1 -1
  159. package/dist/pipeline/generate-with-repair.d.ts +2 -2
  160. package/dist/pipeline/generate-with-repair.js +2 -2
  161. package/dist/pipeline/handwritten-scenarios.d.ts +20 -7
  162. package/dist/pipeline/handwritten-scenarios.js +20 -41
  163. package/dist/pipeline/handwritten-scenarios.js.map +1 -1
  164. package/dist/pipeline/list-resolver.d.ts +18 -20
  165. package/dist/pipeline/list-resolver.js +28 -36
  166. package/dist/pipeline/list-resolver.js.map +1 -1
  167. package/dist/pipeline/pipeline-runner.d.ts +102 -13
  168. package/dist/pipeline/pipeline-runner.js +497 -237
  169. package/dist/pipeline/pipeline-runner.js.map +1 -1
  170. package/dist/pipeline/pipeline-shared.d.ts +7 -9
  171. package/dist/pipeline/pipeline-shared.js +8 -18
  172. package/dist/pipeline/pipeline-shared.js.map +1 -1
  173. package/dist/pipeline/policy-verifier.d.ts +32 -5
  174. package/dist/pipeline/policy-verifier.js +137 -8
  175. package/dist/pipeline/policy-verifier.js.map +1 -1
  176. package/dist/pipeline/proxy-mcp-connections.d.ts +25 -0
  177. package/dist/pipeline/proxy-mcp-connections.js +121 -0
  178. package/dist/pipeline/proxy-mcp-connections.js.map +1 -0
  179. package/dist/pipeline/refresh-lists.js +21 -13
  180. package/dist/pipeline/refresh-lists.js.map +1 -1
  181. package/dist/pipeline/scenario-generator.d.ts +29 -49
  182. package/dist/pipeline/scenario-generator.js +180 -143
  183. package/dist/pipeline/scenario-generator.js.map +1 -1
  184. package/dist/pipeline/scenario-schema-validator.d.ts +51 -0
  185. package/dist/pipeline/scenario-schema-validator.js +132 -0
  186. package/dist/pipeline/scenario-schema-validator.js.map +1 -0
  187. package/dist/pipeline/tool-annotator.d.ts +9 -8
  188. package/dist/pipeline/tool-annotator.js +48 -95
  189. package/dist/pipeline/tool-annotator.js.map +1 -1
  190. package/dist/pipeline/types.d.ts +31 -24
  191. package/dist/sandbox/index.d.ts +26 -10
  192. package/dist/sandbox/index.js +151 -40
  193. package/dist/sandbox/index.js.map +1 -1
  194. package/dist/servers/fetch-server.js +10 -5
  195. package/dist/servers/fetch-server.js.map +1 -1
  196. package/dist/session/agent-session.d.ts +8 -20
  197. package/dist/session/agent-session.js +22 -70
  198. package/dist/session/agent-session.js.map +1 -1
  199. package/dist/session/base-transport.d.ts +17 -1
  200. package/dist/session/base-transport.js +25 -1
  201. package/dist/session/base-transport.js.map +1 -1
  202. package/dist/session/cli-transport.d.ts +5 -0
  203. package/dist/session/cli-transport.js +48 -25
  204. package/dist/session/cli-transport.js.map +1 -1
  205. package/dist/session/index.d.ts +25 -1
  206. package/dist/session/index.js +57 -8
  207. package/dist/session/index.js.map +1 -1
  208. package/dist/session/prompts.js +8 -9
  209. package/dist/session/prompts.js.map +1 -1
  210. package/dist/session/session-manager.d.ts +6 -0
  211. package/dist/session/session-manager.js +12 -0
  212. package/dist/session/session-manager.js.map +1 -1
  213. package/dist/session/types.d.ts +11 -1
  214. package/dist/session/types.js.map +1 -1
  215. package/dist/session/workspace-validation.js +37 -6
  216. package/dist/session/workspace-validation.js.map +1 -1
  217. package/dist/signal/signal-bot-daemon.js +24 -8
  218. package/dist/signal/signal-bot-daemon.js.map +1 -1
  219. package/dist/signal/signal-transport.d.ts +5 -2
  220. package/dist/signal/signal-transport.js +3 -3
  221. package/dist/signal/signal-transport.js.map +1 -1
  222. package/dist/trusted-process/approval-whitelist.d.ts +117 -0
  223. package/dist/trusted-process/approval-whitelist.js +237 -0
  224. package/dist/trusted-process/approval-whitelist.js.map +1 -0
  225. package/dist/trusted-process/gworkspace-credentials.d.ts +40 -0
  226. package/dist/trusted-process/gworkspace-credentials.js +45 -0
  227. package/dist/trusted-process/gworkspace-credentials.js.map +1 -0
  228. package/dist/trusted-process/index.d.ts +11 -1
  229. package/dist/trusted-process/index.js +83 -53
  230. package/dist/trusted-process/index.js.map +1 -1
  231. package/dist/trusted-process/mcp-proxy-server.d.ts +23 -0
  232. package/dist/trusted-process/mcp-proxy-server.js +409 -94
  233. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  234. package/dist/trusted-process/policy-engine.d.ts +12 -3
  235. package/dist/trusted-process/policy-engine.js +33 -23
  236. package/dist/trusted-process/policy-engine.js.map +1 -1
  237. package/dist/trusted-process/policy-types.d.ts +17 -0
  238. package/dist/trusted-process/sandbox-integration.d.ts +23 -0
  239. package/dist/trusted-process/sandbox-integration.js +115 -3
  240. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  241. package/dist/trusted-process/token-file-refresher.d.ts +69 -0
  242. package/dist/trusted-process/token-file-refresher.js +120 -0
  243. package/dist/trusted-process/token-file-refresher.js.map +1 -0
  244. package/dist/types/argument-roles.d.ts +7 -1
  245. package/dist/types/argument-roles.js +50 -0
  246. package/dist/types/argument-roles.js.map +1 -1
  247. package/dist/types/audit.d.ts +11 -0
  248. package/docker/Dockerfile.base +22 -0
  249. package/docker/Dockerfile.base.arm64 +44 -4
  250. package/docker/Dockerfile.claude-code +2 -2
  251. package/docker/entrypoint-claude-code.sh +24 -6
  252. package/package.json +8 -2
  253. package/src/config/constitution-user-base.md +15 -0
  254. package/src/config/generated/test-scenarios.json +1 -1
  255. package/src/config/generated/tool-annotations.json +10617 -296
  256. package/src/config/mcp-servers.json +31 -2
  257. package/dist/pipeline/mcp-connections.d.ts +0 -16
  258. package/dist/pipeline/mcp-connections.js +0 -61
  259. package/dist/pipeline/mcp-connections.js.map +0 -1
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Memory MCP server name constant and server configuration builder.
3
+ *
4
+ * The memory server is registered as a "trusted server" in the PolicyEngine,
5
+ * bypassing annotation lookup and policy evaluation entirely. No annotations
6
+ * or policy rules are needed here.
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ export const MEMORY_SERVER_NAME = 'memory';
10
+ /**
11
+ * Resolved path to the memory-mcp-server entry point (computed once).
12
+ *
13
+ * Uses Node module resolution so the path works both in development
14
+ * (workspace symlink) and production (installed from npm).
15
+ */
16
+ export const MEMORY_SERVER_ENTRY = createRequire(import.meta.url).resolve('@provos/memory-mcp-server');
17
+ /**
18
+ * Verifies that a server config named 'memory' matches the exact shape
19
+ * produced by buildMemoryServerConfig(). If the config exists but doesn't
20
+ * match, the process cannot safely trust it — this is a fatal error.
21
+ *
22
+ * Returns true if the config is verified as our memory server,
23
+ * false if no 'memory' server is configured.
24
+ * Throws if a 'memory' server exists but has an unexpected shape.
25
+ */
26
+ export function verifyMemoryServerConfig(mcpServers) {
27
+ if (!(MEMORY_SERVER_NAME in mcpServers))
28
+ return false;
29
+ const config = mcpServers[MEMORY_SERVER_NAME];
30
+ if (config.command === 'node' && config.args.length === 1 && config.args[0] === MEMORY_SERVER_ENTRY) {
31
+ return true;
32
+ }
33
+ throw new Error(`MCP server "${MEMORY_SERVER_NAME}" has unexpected config ` +
34
+ `(command=${JSON.stringify(config.command)}, args=${JSON.stringify(config.args)}). ` +
35
+ `Expected command="node" with args=[${JSON.stringify(MEMORY_SERVER_ENTRY)}]. ` +
36
+ `The memory server config appears tampered or misconfigured.`);
37
+ }
38
+ /**
39
+ * Builds the set of trusted server names from the MCP server config.
40
+ * Currently only the memory server can be trusted, verified by exact
41
+ * command/args match. Throws if a 'memory' server exists with an
42
+ * unexpected shape.
43
+ */
44
+ export function buildTrustedServerSet(mcpServers) {
45
+ const trusted = new Set();
46
+ if (verifyMemoryServerConfig(mcpServers)) {
47
+ trusted.add(MEMORY_SERVER_NAME);
48
+ }
49
+ return trusted;
50
+ }
51
+ /**
52
+ * Builds the MCP server config for the memory server.
53
+ *
54
+ * The server runs as a Node.js subprocess using the compiled
55
+ * memory-mcp-server package entry point. Configuration is passed
56
+ * via environment variables.
57
+ */
58
+ export function buildMemoryServerConfig(opts) {
59
+ const env = {
60
+ MEMORY_DB_PATH: opts.dbPath,
61
+ };
62
+ if (opts.namespace) {
63
+ env.MEMORY_NAMESPACE = opts.namespace;
64
+ }
65
+ // LLM config for summarization, dedup, and compaction.
66
+ // Falls back to Anthropic API key via their OpenAI-compatible endpoint.
67
+ if (opts.llmBaseUrl) {
68
+ env.MEMORY_LLM_BASE_URL = opts.llmBaseUrl;
69
+ }
70
+ if (opts.llmApiKey) {
71
+ env.MEMORY_LLM_API_KEY = opts.llmApiKey;
72
+ }
73
+ else if (opts.anthropicApiKey) {
74
+ env.MEMORY_LLM_API_KEY = opts.anthropicApiKey;
75
+ if (!opts.llmBaseUrl) {
76
+ env.MEMORY_LLM_BASE_URL = 'https://api.anthropic.com/v1/';
77
+ }
78
+ }
79
+ return {
80
+ command: 'node',
81
+ args: [MEMORY_SERVER_ENTRY],
82
+ env,
83
+ description: 'Persistent memory with semantic search',
84
+ sandbox: false,
85
+ };
86
+ }
87
+ //# sourceMappingURL=memory-annotations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-annotations.js","sourceRoot":"","sources":["../../src/memory/memory-annotations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;AAEvG;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAA2C;IAClF,IAAI,CAAC,CAAC,kBAAkB,IAAI,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtD,MAAM,MAAM,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,mBAAmB,EAAE,CAAC;QACpG,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,KAAK,CACb,eAAe,kBAAkB,0BAA0B;QACzD,YAAY,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK;QACpF,sCAAsC,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,KAAK;QAC9E,6DAA6D,CAChE,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAA2C;IAC/E,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,IAAI,wBAAwB,CAAC,UAAU,CAAC,EAAE,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAMvC;IACC,MAAM,GAAG,GAA2B;QAClC,cAAc,EAAE,IAAI,CAAC,MAAM;KAC5B,CAAC;IAEF,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,GAAG,CAAC,gBAAgB,GAAG,IAAI,CAAC,SAAS,CAAC;IACxC,CAAC;IAED,uDAAuD;IACvD,wEAAwE;IACxE,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,GAAG,CAAC,mBAAmB,GAAG,IAAI,CAAC,UAAU,CAAC;IAC5C,CAAC;IACD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC,SAAS,CAAC;IAC1C,CAAC;SAAM,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;QAChC,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC,eAAe,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,GAAG,CAAC,mBAAmB,GAAG,+BAA+B,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,CAAC,mBAAmB,CAAC;QAC3B,GAAG;QACH,WAAW,EAAE,wCAAwC;QACrD,OAAO,EAAE,KAAK;KACf,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Re-exports the memory system prompt builder from the memory MCP server package.
3
+ *
4
+ * This thin wrapper centralizes the import so that persona-prompt.ts and
5
+ * session/index.ts don't need to know the package export path.
6
+ */
7
+ export { buildMemorySystemPrompt } from '@provos/memory-mcp-server/prompts';
8
+ /**
9
+ * Rewrites raw MCP tool names (memory_context, memory_store, …) to the
10
+ * Code Mode namespace-aliased format (memory.context, memory.store, …).
11
+ */
12
+ export declare function adaptMemoryToolNames(prompt: string): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Re-exports the memory system prompt builder from the memory MCP server package.
3
+ *
4
+ * This thin wrapper centralizes the import so that persona-prompt.ts and
5
+ * session/index.ts don't need to know the package export path.
6
+ */
7
+ export { buildMemorySystemPrompt } from '@provos/memory-mcp-server/prompts';
8
+ /**
9
+ * Rewrites raw MCP tool names (memory_context, memory_store, …) to the
10
+ * Code Mode namespace-aliased format (memory.context, memory.store, …).
11
+ */
12
+ export function adaptMemoryToolNames(prompt) {
13
+ return prompt
14
+ .replaceAll('memory_context', 'memory.context')
15
+ .replaceAll('memory_store', 'memory.store')
16
+ .replaceAll('memory_recall', 'memory.recall')
17
+ .replaceAll('memory_forget', 'memory.forget')
18
+ .replaceAll('memory_inspect', 'memory.inspect');
19
+ }
20
+ //# sourceMappingURL=memory-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-prompt.js","sourceRoot":"","sources":["../../src/memory/memory-prompt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAE5E;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO,MAAM;SACV,UAAU,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;SAC9C,UAAU,CAAC,cAAc,EAAE,cAAc,CAAC;SAC1C,UAAU,CAAC,eAAe,EAAE,eAAe,CAAC;SAC5C,UAAU,CAAC,eAAe,EAAE,eAAe,CAAC;SAC5C,UAAU,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolves the SQLite database path for the memory MCP server
3
+ * based on the session context (persona or cron job).
4
+ *
5
+ * Memory is only available for persona and cron job sessions,
6
+ * not for ad-hoc default sessions.
7
+ */
8
+ /**
9
+ * Returns the absolute path to the memory SQLite database.
10
+ *
11
+ * - Persona sessions: ~/.ironcurtain/personas/{name}/memory.db
12
+ * - Cron jobs (no persona): ~/.ironcurtain/jobs/{jobId}/memory.db
13
+ *
14
+ * Uses the established path helpers and validators (createPersonaName,
15
+ * getJobDir) to ensure consistent validation and path construction.
16
+ */
17
+ export declare function resolveMemoryDbPath(opts: {
18
+ persona?: string;
19
+ jobId?: string;
20
+ }): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Resolves the SQLite database path for the memory MCP server
3
+ * based on the session context (persona or cron job).
4
+ *
5
+ * Memory is only available for persona and cron job sessions,
6
+ * not for ad-hoc default sessions.
7
+ */
8
+ import { resolve } from 'node:path';
9
+ import { getJobDir } from '../config/paths.js';
10
+ import { getPersonaDir } from '../persona/resolve.js';
11
+ import { createPersonaName } from '../persona/types.js';
12
+ /**
13
+ * Returns the absolute path to the memory SQLite database.
14
+ *
15
+ * - Persona sessions: ~/.ironcurtain/personas/{name}/memory.db
16
+ * - Cron jobs (no persona): ~/.ironcurtain/jobs/{jobId}/memory.db
17
+ *
18
+ * Uses the established path helpers and validators (createPersonaName,
19
+ * getJobDir) to ensure consistent validation and path construction.
20
+ */
21
+ export function resolveMemoryDbPath(opts) {
22
+ if (opts.persona) {
23
+ return resolve(getPersonaDir(createPersonaName(opts.persona)), 'memory.db');
24
+ }
25
+ if (opts.jobId) {
26
+ return resolve(getJobDir(opts.jobId), 'memory.db');
27
+ }
28
+ throw new Error('resolveMemoryDbPath requires either persona or jobId');
29
+ }
30
+ //# sourceMappingURL=resolve-memory-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-memory-path.js","sourceRoot":"","sources":["../../src/memory/resolve-memory-path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA0C;IAC5E,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,OAAO,CAAC,aAAa,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;AAC1E,CAAC"}
@@ -17,6 +17,10 @@ export interface MuxAppOptions {
17
17
  readonly autoSpawn?: boolean;
18
18
  /** Protected paths for workspace validation. */
19
19
  readonly protectedPaths?: string[];
20
+ /** Unique mux instance ID for session ownership. */
21
+ readonly muxId?: string;
22
+ /** PID of this mux process (for orphan detection by other mux instances). */
23
+ readonly muxPid?: number;
20
24
  }
21
25
  /**
22
26
  * Creates and returns a MuxApp.
@@ -12,6 +12,10 @@ import { createMuxRenderer } from './mux-renderer.js';
12
12
  import { writeTrustedUserContext } from './trusted-input.js';
13
13
  import { createPasteInterceptor } from './paste-interceptor.js';
14
14
  import { validateWorkspacePath } from '../session/workspace-validation.js';
15
+ import { scanResumableSessions } from './session-scanner.js';
16
+ import { scanPersonas } from './persona-scanner.js';
17
+ import ora from 'ora';
18
+ import chalk from 'chalk';
15
19
  import * as logger from '../logger.js';
16
20
  /**
17
21
  * Creates and returns a MuxApp.
@@ -49,23 +53,29 @@ export function createMuxApp(options) {
49
53
  // If running a compiled JS file or via an installed bin, use it directly
50
54
  return { bin: script || 'ironcurtain', prefixArgs: [] };
51
55
  }
52
- async function spawnSession(workspacePath) {
56
+ async function spawnSession(opts) {
53
57
  const { columns } = process.stdout;
54
58
  const ptyRows = renderer.layout.ptyViewportRows;
55
59
  const ptyCols = columns || 80;
60
+ const sessionAgent = opts?.agentOverride ?? agent;
56
61
  const { bin, prefixArgs } = resolveIroncurtainBin();
57
62
  const bridge = await createPtyBridge({
58
63
  cols: ptyCols,
59
64
  rows: ptyRows,
60
65
  ironcurtainBin: bin,
61
66
  prefixArgs,
62
- agent,
63
- workspacePath,
67
+ agent: sessionAgent,
68
+ workspacePath: opts?.workspacePath,
69
+ resumeSessionId: opts?.resumeSessionId,
70
+ persona: opts?.persona,
71
+ muxId: options.muxId,
72
+ muxPid: options.muxPid,
64
73
  });
65
74
  const tab = {
66
75
  number: nextTabNumber++,
67
76
  bridge,
68
- label: agent,
77
+ label: opts?.persona ? `${sessionAgent} (${opts.persona})` : sessionAgent,
78
+ persona: opts?.persona,
69
79
  status: 'running',
70
80
  escalationAvailable: false,
71
81
  scrollOffset: null,
@@ -96,7 +106,7 @@ export function createMuxApp(options) {
96
106
  if (registration && tab.status === 'running') {
97
107
  tab.escalationAvailable = true;
98
108
  escalationManager.addSession(registration);
99
- tab.label = registration.label;
109
+ tab.label = tab.persona ? `${registration.label} [${tab.persona}]` : registration.label;
100
110
  renderer.redrawTabBar();
101
111
  }
102
112
  else if (!registration) {
@@ -125,7 +135,8 @@ export function createMuxApp(options) {
125
135
  }
126
136
  tabs.splice(index, 1);
127
137
  if (tabs.length === 0) {
128
- doShutdown();
138
+ activeTabIndex = 0;
139
+ renderer.fullRedraw();
129
140
  return;
130
141
  }
131
142
  if (activeTabIndex >= tabs.length) {
@@ -151,6 +162,27 @@ export function createMuxApp(options) {
151
162
  logger.info(message);
152
163
  renderer.showMessage(message);
153
164
  }
165
+ function navigateEscalationTab(direction) {
166
+ const eps = inputHandler.escalationPickerState;
167
+ if (!eps)
168
+ return;
169
+ const sortedNums = escalationManager.sortedDisplayNumbers();
170
+ if (sortedNums.length === 0)
171
+ return;
172
+ const currentIdx = sortedNums.indexOf(eps.focusedDisplayNumber);
173
+ let newIdx;
174
+ if (currentIdx === -1) {
175
+ // Focused escalation was resolved/expired -- snap to first
176
+ newIdx = 0;
177
+ }
178
+ else if (direction === 'next') {
179
+ newIdx = (currentIdx + 1) % sortedNums.length;
180
+ }
181
+ else {
182
+ newIdx = (currentIdx - 1 + sortedNums.length) % sortedNums.length;
183
+ }
184
+ eps.focusedDisplayNumber = sortedNums[newIdx];
185
+ }
154
186
  async function handleAction(action) {
155
187
  switch (action.kind) {
156
188
  case 'none':
@@ -206,15 +238,16 @@ export function createMuxApp(options) {
206
238
  validatedPath = validateWorkspacePath(action.workspacePath, protectedPaths);
207
239
  }
208
240
  catch (err) {
209
- inputHandler.enterBrowseWithError(action.workspacePath, err instanceof Error ? err.message : String(err));
241
+ inputHandler.enterBrowseWithError(action.workspacePath, err instanceof Error ? err.message : String(err), action.persona);
210
242
  renderer.fullRedraw();
211
243
  break;
212
244
  }
213
245
  }
214
- const tab = await spawnSession(validatedPath);
246
+ const tab = await spawnSession({ workspacePath: validatedPath, persona: action.persona });
215
247
  activeTabIndex = tabs.length - 1;
248
+ const personaPrefix = action.persona ? `persona "${action.persona}" ` : '';
216
249
  const suffix = validatedPath ? ` in ${validatedPath}` : '';
217
- showMessage(`Spawned session #${tab.number}${suffix}`);
250
+ showMessage(`Spawned ${personaPrefix}session #${tab.number}${suffix}`);
218
251
  inputHandler.exitPickerMode();
219
252
  renderer.fullRedraw();
220
253
  break;
@@ -222,9 +255,62 @@ export function createMuxApp(options) {
222
255
  case 'picker-cancel':
223
256
  renderer.fullRedraw();
224
257
  break;
258
+ case 'resume-spawn': {
259
+ const resumeTab = await spawnSession({
260
+ resumeSessionId: action.sessionId,
261
+ agentOverride: action.agent,
262
+ });
263
+ activeTabIndex = tabs.length - 1;
264
+ showMessage(`Resuming session ${action.sessionId.substring(0, 8)} as tab #${resumeTab.number}`);
265
+ renderer.fullRedraw();
266
+ break;
267
+ }
268
+ case 'persona-spawn': {
269
+ const personaTab = await spawnSession({ persona: action.persona });
270
+ activeTabIndex = tabs.length - 1;
271
+ showMessage(`Spawned persona "${action.persona}" as tab #${personaTab.number}`);
272
+ renderer.fullRedraw();
273
+ break;
274
+ }
225
275
  case 'redraw-picker':
226
276
  renderer.redrawCommandArea();
227
277
  break;
278
+ case 'escalation-open': {
279
+ if (escalationManager.pendingCount === 0) {
280
+ showMessage('No pending escalations');
281
+ break;
282
+ }
283
+ const sortedNums = escalationManager.sortedDisplayNumbers();
284
+ const previousMode = inputHandler.mode === 'command' ? 'command' : 'pty';
285
+ inputHandler.enterEscalationPickerMode(sortedNums[0], previousMode);
286
+ renderer.fullRedraw();
287
+ break;
288
+ }
289
+ case 'escalation-dismiss': {
290
+ const sortedNums = escalationManager.sortedDisplayNumbers();
291
+ const highestPending = sortedNums.length > 0 ? sortedNums[sortedNums.length - 1] : 0;
292
+ inputHandler.dismissEscalationPicker(highestPending, action.targetMode);
293
+ renderer.fullRedraw();
294
+ break;
295
+ }
296
+ case 'escalation-navigate': {
297
+ navigateEscalationTab(action.direction);
298
+ renderer.redrawCommandArea();
299
+ break;
300
+ }
301
+ case 'escalation-resolve': {
302
+ const message = escalationManager.resolve(action.displayNumber, action.decision, action.whitelist);
303
+ showMessage(message);
304
+ // onChange callback (fired synchronously by resolve) handles focus adjustment,
305
+ // auto-close, and redraws — no additional redraw needed here.
306
+ break;
307
+ }
308
+ case 'escalation-resolve-all': {
309
+ const message = escalationManager.resolveAll(action.decision, action.whitelist);
310
+ showMessage(message);
311
+ // onChange callback handles auto-close and redraws.
312
+ break;
313
+ }
228
314
  case 'scroll-up':
229
315
  case 'scroll-down': {
230
316
  const active = getActiveTab();
@@ -243,8 +329,13 @@ export function createMuxApp(options) {
243
329
  function handleCommand(command, args) {
244
330
  switch (command) {
245
331
  case 'approve':
246
- case 'deny': {
247
- const decision = command === 'approve' ? 'approved' : 'denied';
332
+ case 'approve+':
333
+ case 'deny':
334
+ case 'deny+': {
335
+ const baseCommand = command.endsWith('+') ? command.slice(0, -1) : command;
336
+ // Only approve+ enables whitelisting; deny+ is treated as plain deny.
337
+ const withWhitelist = command === 'approve+';
338
+ const decision = baseCommand === 'approve' ? 'approved' : 'denied';
248
339
  const arg = args[0];
249
340
  if (!arg) {
250
341
  showMessage(`Usage: /${command} <number> or /${command} all`);
@@ -252,7 +343,7 @@ export function createMuxApp(options) {
252
343
  }
253
344
  let message;
254
345
  if (arg === 'all') {
255
- message = escalationManager.resolveAll(decision);
346
+ message = escalationManager.resolveAll(decision, withWhitelist);
256
347
  }
257
348
  else {
258
349
  const num = parseInt(arg, 10);
@@ -260,7 +351,7 @@ export function createMuxApp(options) {
260
351
  showMessage('Invalid escalation number');
261
352
  break;
262
353
  }
263
- message = escalationManager.resolve(num, decision);
354
+ message = escalationManager.resolve(num, decision, withWhitelist);
264
355
  }
265
356
  showMessage(message);
266
357
  renderer.redrawTabBar();
@@ -268,7 +359,48 @@ export function createMuxApp(options) {
268
359
  break;
269
360
  }
270
361
  case 'new': {
271
- inputHandler.enterPickerMode();
362
+ const personaArg = args[0];
363
+ if (personaArg) {
364
+ // Validate persona before spawning
365
+ const personas = scanPersonas();
366
+ const match = personas.find((p) => p.name === personaArg);
367
+ if (!match) {
368
+ showMessage(`Unknown persona: "${personaArg}"`);
369
+ break;
370
+ }
371
+ if (!match.compiled) {
372
+ showMessage(`Persona "${personaArg}" is not compiled. Run: ironcurtain persona compile ${personaArg}`);
373
+ break;
374
+ }
375
+ void handleAction({ kind: 'persona-spawn', persona: personaArg });
376
+ break;
377
+ }
378
+ const personas = scanPersonas();
379
+ inputHandler.enterPickerMode(personas);
380
+ renderer.fullRedraw();
381
+ break;
382
+ }
383
+ case 'resume': {
384
+ const directId = args[0];
385
+ if (directId) {
386
+ // Direct resume by session ID
387
+ const sessions = scanResumableSessions();
388
+ const match = sessions.find((s) => s.sessionId.startsWith(directId));
389
+ if (!match) {
390
+ showMessage(`No resumable session matching "${directId}"`);
391
+ }
392
+ else {
393
+ void handleAction({ kind: 'resume-spawn', sessionId: match.sessionId, agent: match.agent });
394
+ }
395
+ break;
396
+ }
397
+ // Open the resume picker
398
+ const sessions = scanResumableSessions();
399
+ if (sessions.length === 0) {
400
+ showMessage('No resumable sessions found');
401
+ break;
402
+ }
403
+ inputHandler.enterResumePickerMode(sessions);
272
404
  renderer.fullRedraw();
273
405
  break;
274
406
  }
@@ -310,17 +442,15 @@ export function createMuxApp(options) {
310
442
  showMessage(`Unknown command: /${command}`);
311
443
  }
312
444
  }
445
+ let resolveShutdown = null;
313
446
  function doShutdown() {
314
447
  if (!running)
315
448
  return;
316
449
  running = false;
317
450
  pasteInterceptor?.uninstall();
318
- for (const tab of tabs) {
319
- if (tab.bridge.alive) {
320
- tab.bridge.kill();
321
- }
322
- }
323
451
  escalationManager.stop();
452
+ // Exit fullscreen and destroy renderer before any async work
453
+ // so the normal terminal is restored immediately.
324
454
  if (term) {
325
455
  term.grabInput(false);
326
456
  term.hideCursor(false);
@@ -328,6 +458,47 @@ export function createMuxApp(options) {
328
458
  term.styleReset();
329
459
  }
330
460
  renderer.destroy();
461
+ // Collect alive tabs that need to be waited on
462
+ const aliveTabs = tabs.filter((t) => t.bridge.alive);
463
+ if (aliveTabs.length === 0) {
464
+ resolveShutdown?.();
465
+ return;
466
+ }
467
+ // Show a spinner on the real terminal while children shut down
468
+ const spinner = ora({
469
+ text: `Shutting down ${aliveTabs.length} PTY session${aliveTabs.length > 1 ? 's' : ''}...`,
470
+ stream: process.stderr,
471
+ discardStdin: false,
472
+ }).start();
473
+ // Kill all children
474
+ for (const tab of aliveTabs) {
475
+ tab.bridge.kill();
476
+ }
477
+ // Wait for all children to exit, with a safety timeout
478
+ let remaining = aliveTabs.length;
479
+ let settled = false;
480
+ const settle = () => {
481
+ if (settled)
482
+ return;
483
+ settled = true;
484
+ clearTimeout(deadline);
485
+ spinner.succeed(chalk.dim('All sessions ended'));
486
+ resolveShutdown?.();
487
+ };
488
+ const deadline = setTimeout(() => {
489
+ if (settled)
490
+ return;
491
+ settled = true;
492
+ spinner.warn(chalk.dim(`Timed out waiting for ${remaining} session(s)`));
493
+ resolveShutdown?.();
494
+ }, 10_000);
495
+ for (const tab of aliveTabs) {
496
+ tab.bridge.onExit(() => {
497
+ remaining--;
498
+ if (remaining === 0)
499
+ settle();
500
+ });
501
+ }
331
502
  }
332
503
  return {
333
504
  async start() {
@@ -347,7 +518,7 @@ export function createMuxApp(options) {
347
518
  void handleAction(action);
348
519
  });
349
520
  pasteInterceptor.install();
350
- escalationManager = createMuxEscalationManager();
521
+ escalationManager = createMuxEscalationManager({ muxId: options.muxId });
351
522
  const { columns, rows } = process.stdout;
352
523
  const cols = columns || 80;
353
524
  const totalRows = rows || 24;
@@ -361,15 +532,18 @@ export function createMuxApp(options) {
361
532
  getEscalationState: () => escalationManager.state,
362
533
  getPendingCount: () => escalationManager.pendingCount,
363
534
  getPickerState: () => inputHandler.pickerState,
535
+ getResumePickerState: () => inputHandler.resumePickerState,
536
+ getPersonaPickerState: () => inputHandler.personaPickerState,
537
+ getEscalationPickerState: () => inputHandler.escalationPickerState,
364
538
  getScrollOffset: () => {
365
539
  const active = getActiveTab();
366
540
  return active?.scrollOffset ?? null;
367
541
  },
368
542
  });
369
- term.on('key', (key) => {
543
+ term.on('key', (key, _matches, data) => {
370
544
  if (!running)
371
545
  return;
372
- const action = inputHandler.handleKey(key);
546
+ const action = inputHandler.handleKey(key, data.code);
373
547
  void handleAction(action);
374
548
  });
375
549
  // Mouse events come through a separate 'mouse' emitter, not 'key'
@@ -397,6 +571,51 @@ export function createMuxApp(options) {
397
571
  renderer.fullRedraw();
398
572
  });
399
573
  escalationManager.onChange(() => {
574
+ const pendingCount = escalationManager.pendingCount;
575
+ const mode = inputHandler.mode;
576
+ // Auto-close: picker is open but nothing left
577
+ if (mode === 'escalation-picker' && pendingCount === 0) {
578
+ inputHandler.exitEscalationPickerMode();
579
+ renderer.fullRedraw();
580
+ return;
581
+ }
582
+ // Auto-open: new escalation arrived, picker not already open
583
+ if (pendingCount > 0 && mode !== 'escalation-picker') {
584
+ const sortedNums = escalationManager.sortedDisplayNumbers();
585
+ const highestPending = sortedNums[sortedNums.length - 1];
586
+ // Only auto-open if the user hasn't dismissed, OR if a genuinely new
587
+ // escalation arrived (higher display number than when they dismissed).
588
+ const shouldAutoOpen = !inputHandler.escalationDismissed || highestPending > inputHandler.escalationDismissedAtNumber;
589
+ if (shouldAutoOpen) {
590
+ const previousMode = mode === 'pty' ? 'pty' : mode === 'command' ? 'command' : 'pty'; // if in another picker, treat as pty
591
+ // If user is in another picker, cancel it first
592
+ if (mode === 'picker') {
593
+ inputHandler.exitPickerMode();
594
+ }
595
+ else if (mode === 'resume-picker') {
596
+ inputHandler.exitResumePickerMode();
597
+ }
598
+ else if (mode === 'persona-picker') {
599
+ inputHandler.exitPersonaPickerMode();
600
+ }
601
+ inputHandler.enterEscalationPickerMode(highestPending, previousMode);
602
+ renderer.fullRedraw();
603
+ // BEL is already emitted by the escalation manager's onEscalation handler.
604
+ return;
605
+ }
606
+ }
607
+ // Live update while picker is open: re-validate focused tab
608
+ if (mode === 'escalation-picker') {
609
+ const eps = inputHandler.escalationPickerState;
610
+ if (eps && !escalationManager.state.pendingEscalations.has(eps.focusedDisplayNumber)) {
611
+ // Focused escalation was resolved or expired -- snap to nearest
612
+ const sortedNums = escalationManager.sortedDisplayNumbers();
613
+ if (sortedNums.length > 0) {
614
+ eps.focusedDisplayNumber = sortedNums[0];
615
+ }
616
+ // If sortedNums is empty, auto-close above will handle it on next tick
617
+ }
618
+ }
400
619
  renderer.redrawTabBar();
401
620
  renderer.redrawCommandArea();
402
621
  });
@@ -433,12 +652,11 @@ export function createMuxApp(options) {
433
652
  }
434
653
  renderer.fullRedraw();
435
654
  await new Promise((resolve) => {
436
- const checkInterval = setInterval(() => {
437
- if (!running) {
438
- clearInterval(checkInterval);
439
- resolve();
440
- }
441
- }, 100);
655
+ if (!running) {
656
+ resolve();
657
+ return;
658
+ }
659
+ resolveShutdown = resolve;
442
660
  });
443
661
  },
444
662
  // eslint-disable-next-line @typescript-eslint/require-await