aimux-cli 0.1.16 → 0.1.18

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 (299) hide show
  1. package/README.md +184 -67
  2. package/bin/aimux-dev +10 -0
  3. package/dist/alert-display.d.ts +21 -0
  4. package/dist/alert-display.js +86 -0
  5. package/dist/alert-display.js.map +1 -0
  6. package/dist/attachment-store.d.ts +0 -7
  7. package/dist/attachment-store.js +2 -86
  8. package/dist/attachment-store.js.map +1 -1
  9. package/dist/builtin-metadata-watchers.js +4 -4
  10. package/dist/builtin-metadata-watchers.js.map +1 -1
  11. package/dist/claude-hooks.d.ts +1 -0
  12. package/dist/claude-hooks.js +25 -0
  13. package/dist/claude-hooks.js.map +1 -1
  14. package/dist/config.d.ts +19 -13
  15. package/dist/config.js +28 -14
  16. package/dist/config.js.map +1 -1
  17. package/dist/connection-targets.d.ts +8 -0
  18. package/dist/connection-targets.js +28 -0
  19. package/dist/connection-targets.js.map +1 -0
  20. package/dist/credentials.d.ts +12 -0
  21. package/dist/credentials.js +49 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/daemon.d.ts +23 -0
  24. package/dist/daemon.js +391 -66
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/dashboard/index.d.ts +13 -10
  27. package/dist/dashboard/index.js +3 -26
  28. package/dist/dashboard/index.js.map +1 -1
  29. package/dist/dashboard/order.d.ts +22 -0
  30. package/dist/dashboard/order.js +55 -0
  31. package/dist/dashboard/order.js.map +1 -0
  32. package/dist/dashboard/pending-actions.d.ts +39 -10
  33. package/dist/dashboard/pending-actions.js +166 -36
  34. package/dist/dashboard/pending-actions.js.map +1 -1
  35. package/dist/dashboard/quick-jump.d.ts +2 -1
  36. package/dist/dashboard/quick-jump.js +7 -4
  37. package/dist/dashboard/quick-jump.js.map +1 -1
  38. package/dist/dashboard/session-actions.d.ts +4 -4
  39. package/dist/dashboard/session-actions.js +1 -1
  40. package/dist/dashboard/session-actions.js.map +1 -1
  41. package/dist/dashboard/session-registry.d.ts +4 -3
  42. package/dist/dashboard/session-registry.js +16 -50
  43. package/dist/dashboard/session-registry.js.map +1 -1
  44. package/dist/dashboard/state.d.ts +1 -1
  45. package/dist/dashboard/state.js.map +1 -1
  46. package/dist/dashboard/ui-state-store.d.ts +16 -1
  47. package/dist/dashboard/ui-state-store.js +73 -2
  48. package/dist/dashboard/ui-state-store.js.map +1 -1
  49. package/dist/debug-state.d.ts +97 -0
  50. package/dist/debug-state.js +541 -0
  51. package/dist/debug-state.js.map +1 -0
  52. package/dist/debug.d.ts +38 -0
  53. package/dist/debug.js +219 -15
  54. package/dist/debug.js.map +1 -1
  55. package/dist/default-plugins/gh-pr-context.d.ts +2 -1
  56. package/dist/default-plugins/gh-pr-context.js +17 -11
  57. package/dist/default-plugins/gh-pr-context.js.map +1 -1
  58. package/dist/default-plugins/transcript-length.js +15 -2
  59. package/dist/default-plugins/transcript-length.js.map +1 -1
  60. package/dist/fast-control.js +37 -19
  61. package/dist/fast-control.js.map +1 -1
  62. package/dist/http-client.js +31 -2
  63. package/dist/http-client.js.map +1 -1
  64. package/dist/local-ui-server.d.ts +22 -0
  65. package/dist/local-ui-server.js +186 -0
  66. package/dist/local-ui-server.js.map +1 -0
  67. package/dist/login-flow.d.ts +7 -0
  68. package/dist/login-flow.js +120 -0
  69. package/dist/login-flow.js.map +1 -0
  70. package/dist/main.js +821 -151
  71. package/dist/main.js.map +1 -1
  72. package/dist/managed-launch-env.js +14 -0
  73. package/dist/managed-launch-env.js.map +1 -1
  74. package/dist/metadata-server.d.ts +36 -36
  75. package/dist/metadata-server.js +638 -137
  76. package/dist/metadata-server.js.map +1 -1
  77. package/dist/metadata-store.d.ts +4 -1
  78. package/dist/metadata-store.js +30 -2
  79. package/dist/metadata-store.js.map +1 -1
  80. package/dist/multiplexer/agent-io-methods.d.ts +2 -10
  81. package/dist/multiplexer/agent-io-methods.js +12 -43
  82. package/dist/multiplexer/agent-io-methods.js.map +1 -1
  83. package/dist/multiplexer/archives.js +8 -9
  84. package/dist/multiplexer/archives.js.map +1 -1
  85. package/dist/multiplexer/dashboard-control.js +45 -13
  86. package/dist/multiplexer/dashboard-control.js.map +1 -1
  87. package/dist/multiplexer/dashboard-interaction.d.ts +8 -2
  88. package/dist/multiplexer/dashboard-interaction.js +187 -28
  89. package/dist/multiplexer/dashboard-interaction.js.map +1 -1
  90. package/dist/multiplexer/dashboard-model.d.ts +10 -3
  91. package/dist/multiplexer/dashboard-model.js +417 -35
  92. package/dist/multiplexer/dashboard-model.js.map +1 -1
  93. package/dist/multiplexer/dashboard-ops.d.ts +9 -7
  94. package/dist/multiplexer/dashboard-ops.js +178 -68
  95. package/dist/multiplexer/dashboard-ops.js.map +1 -1
  96. package/dist/multiplexer/dashboard-state-methods.d.ts +2 -1
  97. package/dist/multiplexer/dashboard-state-methods.js +3 -2
  98. package/dist/multiplexer/dashboard-state-methods.js.map +1 -1
  99. package/dist/multiplexer/dashboard-tail-methods.d.ts +22 -10
  100. package/dist/multiplexer/dashboard-tail-methods.js +164 -47
  101. package/dist/multiplexer/dashboard-tail-methods.js.map +1 -1
  102. package/dist/multiplexer/dashboard-view-methods.d.ts +1 -1
  103. package/dist/multiplexer/dashboard-view-methods.js +23 -8
  104. package/dist/multiplexer/dashboard-view-methods.js.map +1 -1
  105. package/dist/multiplexer/graveyard-view-model.d.ts +9 -1
  106. package/dist/multiplexer/graveyard-view-model.js +39 -0
  107. package/dist/multiplexer/graveyard-view-model.js.map +1 -1
  108. package/dist/multiplexer/index.d.ts +15 -12
  109. package/dist/multiplexer/index.js +64 -43
  110. package/dist/multiplexer/index.js.map +1 -1
  111. package/dist/multiplexer/notifications.js +107 -24
  112. package/dist/multiplexer/notifications.js.map +1 -1
  113. package/dist/multiplexer/persistence-methods.d.ts +31 -4
  114. package/dist/multiplexer/persistence-methods.js +304 -308
  115. package/dist/multiplexer/persistence-methods.js.map +1 -1
  116. package/dist/multiplexer/runtime-lifecycle-methods.d.ts +8 -10
  117. package/dist/multiplexer/runtime-lifecycle-methods.js +104 -86
  118. package/dist/multiplexer/runtime-lifecycle-methods.js.map +1 -1
  119. package/dist/multiplexer/runtime-state.d.ts +8 -10
  120. package/dist/multiplexer/runtime-state.js +82 -145
  121. package/dist/multiplexer/runtime-state.js.map +1 -1
  122. package/dist/multiplexer/runtime-sync.d.ts +2 -10
  123. package/dist/multiplexer/runtime-sync.js +3 -18
  124. package/dist/multiplexer/runtime-sync.js.map +1 -1
  125. package/dist/multiplexer/service-state-snapshot.d.ts +2 -4
  126. package/dist/multiplexer/service-state-snapshot.js +4 -51
  127. package/dist/multiplexer/service-state-snapshot.js.map +1 -1
  128. package/dist/multiplexer/services.d.ts +1 -0
  129. package/dist/multiplexer/services.js +55 -5
  130. package/dist/multiplexer/services.js.map +1 -1
  131. package/dist/multiplexer/session-capture.d.ts +1 -0
  132. package/dist/multiplexer/session-capture.js +24 -0
  133. package/dist/multiplexer/session-capture.js.map +1 -0
  134. package/dist/multiplexer/session-launch.d.ts +4 -1
  135. package/dist/multiplexer/session-launch.js +152 -63
  136. package/dist/multiplexer/session-launch.js.map +1 -1
  137. package/dist/multiplexer/session-runtime-core.d.ts +8 -20
  138. package/dist/multiplexer/session-runtime-core.js +40 -135
  139. package/dist/multiplexer/session-runtime-core.js.map +1 -1
  140. package/dist/multiplexer/subscreens.js +10 -3
  141. package/dist/multiplexer/subscreens.js.map +1 -1
  142. package/dist/multiplexer/worktree-graveyard.d.ts +0 -1
  143. package/dist/multiplexer/worktree-graveyard.js +15 -16
  144. package/dist/multiplexer/worktree-graveyard.js.map +1 -1
  145. package/dist/multiplexer/worktrees.js +96 -40
  146. package/dist/multiplexer/worktrees.js.map +1 -1
  147. package/dist/notification-context.js +8 -4
  148. package/dist/notification-context.js.map +1 -1
  149. package/dist/notifications.js +163 -101
  150. package/dist/notifications.js.map +1 -1
  151. package/dist/notify.d.ts +4 -0
  152. package/dist/notify.js +14 -0
  153. package/dist/notify.js.map +1 -1
  154. package/dist/paths.d.ts +32 -7
  155. package/dist/paths.js +82 -58
  156. package/dist/paths.js.map +1 -1
  157. package/dist/pending-actions.d.ts +5 -0
  158. package/dist/pending-actions.js +14 -0
  159. package/dist/pending-actions.js.map +1 -0
  160. package/dist/plugin-runtime.js +9 -2
  161. package/dist/plugin-runtime.js.map +1 -1
  162. package/dist/project-events.d.ts +1 -10
  163. package/dist/project-events.js +0 -10
  164. package/dist/project-events.js.map +1 -1
  165. package/dist/project-scanner.d.ts +2 -3
  166. package/dist/project-scanner.js +58 -129
  167. package/dist/project-scanner.js.map +1 -1
  168. package/dist/project-service-manifest.d.ts +1 -3
  169. package/dist/project-service-manifest.js +1 -3
  170. package/dist/project-service-manifest.js.map +1 -1
  171. package/dist/relay-client.d.ts +30 -0
  172. package/dist/relay-client.js +191 -0
  173. package/dist/relay-client.js.map +1 -0
  174. package/dist/remote-access.d.ts +16 -0
  175. package/dist/remote-access.js +91 -0
  176. package/dist/remote-access.js.map +1 -0
  177. package/dist/runtime-core/exchange-derived.d.ts +2 -0
  178. package/dist/runtime-core/exchange-derived.js +154 -0
  179. package/dist/runtime-core/exchange-derived.js.map +1 -0
  180. package/dist/runtime-core/exchange-import.d.ts +24 -0
  181. package/dist/runtime-core/exchange-import.js +318 -0
  182. package/dist/runtime-core/exchange-import.js.map +1 -0
  183. package/dist/runtime-core/exchange-store.d.ts +157 -0
  184. package/dist/runtime-core/exchange-store.js +453 -0
  185. package/dist/runtime-core/exchange-store.js.map +1 -0
  186. package/dist/runtime-core/topology-services.d.ts +38 -0
  187. package/dist/runtime-core/topology-services.js +171 -0
  188. package/dist/runtime-core/topology-services.js.map +1 -0
  189. package/dist/runtime-core/topology-sessions.d.ts +52 -0
  190. package/dist/runtime-core/topology-sessions.js +239 -0
  191. package/dist/runtime-core/topology-sessions.js.map +1 -0
  192. package/dist/runtime-core/topology-store.d.ts +171 -0
  193. package/dist/runtime-core/topology-store.js +420 -0
  194. package/dist/runtime-core/topology-store.js.map +1 -0
  195. package/dist/runtime-core/topology-worktrees.d.ts +60 -0
  196. package/dist/runtime-core/topology-worktrees.js +200 -0
  197. package/dist/runtime-core/topology-worktrees.js.map +1 -0
  198. package/dist/runtime-migration.d.ts +69 -0
  199. package/dist/runtime-migration.js +399 -0
  200. package/dist/runtime-migration.js.map +1 -0
  201. package/dist/session-bootstrap.d.ts +8 -6
  202. package/dist/session-bootstrap.js +51 -158
  203. package/dist/session-bootstrap.js.map +1 -1
  204. package/dist/session-runtime.d.ts +2 -0
  205. package/dist/session-runtime.js +1 -0
  206. package/dist/session-runtime.js.map +1 -1
  207. package/dist/session-semantics.d.ts +12 -4
  208. package/dist/session-semantics.js +14 -0
  209. package/dist/session-semantics.js.map +1 -1
  210. package/dist/shell-hooks.js +32 -10
  211. package/dist/shell-hooks.js.map +1 -1
  212. package/dist/shell-state.d.ts +2 -0
  213. package/dist/shell-state.js +26 -1
  214. package/dist/shell-state.js.map +1 -1
  215. package/dist/statusline-model.d.ts +10 -2
  216. package/dist/statusline-model.js +106 -30
  217. package/dist/statusline-model.js.map +1 -1
  218. package/dist/task-workflow.d.ts +6 -9
  219. package/dist/task-workflow.js +37 -84
  220. package/dist/task-workflow.js.map +1 -1
  221. package/dist/tasks.d.ts +6 -33
  222. package/dist/tasks.js +46 -88
  223. package/dist/tasks.js.map +1 -1
  224. package/dist/team.d.ts +29 -0
  225. package/dist/team.js +40 -0
  226. package/dist/team.js.map +1 -1
  227. package/dist/threads.d.ts +6 -35
  228. package/dist/threads.js +89 -98
  229. package/dist/threads.js.map +1 -1
  230. package/dist/tmux/inbox-popup.js +37 -15
  231. package/dist/tmux/inbox-popup.js.map +1 -1
  232. package/dist/tmux/runtime-manager.d.ts +3 -0
  233. package/dist/tmux/runtime-manager.js +21 -4
  234. package/dist/tmux/runtime-manager.js.map +1 -1
  235. package/dist/tmux/statusline.js +49 -9
  236. package/dist/tmux/statusline.js.map +1 -1
  237. package/dist/tmux/window-open.js +1 -2
  238. package/dist/tmux/window-open.js.map +1 -1
  239. package/dist/tool-output-watchers.d.ts +0 -18
  240. package/dist/tool-output-watchers.js +0 -322
  241. package/dist/tool-output-watchers.js.map +1 -1
  242. package/dist/tui/screens/dashboard-renderers.js +37 -25
  243. package/dist/tui/screens/dashboard-renderers.js.map +1 -1
  244. package/dist/tui/screens/overlay-renderers.d.ts +2 -0
  245. package/dist/tui/screens/overlay-renderers.js +37 -1
  246. package/dist/tui/screens/overlay-renderers.js.map +1 -1
  247. package/dist/tui/screens/subscreen-renderers.js +7 -0
  248. package/dist/tui/screens/subscreen-renderers.js.map +1 -1
  249. package/dist/worktree.js +17 -0
  250. package/dist/worktree.js.map +1 -1
  251. package/dist-ui/_expo/static/css/web-30453ede1678c16acb08b97e83e8646d.css +1 -0
  252. package/dist-ui/_expo/static/js/web/entry-477c745b2adc79367a4380ecf07d9ff6.js +14620 -0
  253. package/dist-ui/assets/assets/images/icon.a5413dcd2e811c9f2317d01a28118d8a.png +0 -0
  254. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
  255. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
  256. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
  257. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
  258. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
  259. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
  260. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
  261. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
  262. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
  263. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
  264. package/dist-ui/assets/node_modules/@react-navigation/elements/lib/module/assets/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
  265. package/dist-ui/assets/node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
  266. package/dist-ui/assets/node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
  267. package/dist-ui/assets/node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
  268. package/dist-ui/assets/node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
  269. package/dist-ui/assets/node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
  270. package/dist-ui/assets/node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
  271. package/dist-ui/assets/node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
  272. package/dist-ui/favicon.ico +0 -0
  273. package/dist-ui/index.html +38 -0
  274. package/dist-ui/metadata.json +1 -0
  275. package/package.json +28 -12
  276. package/dist/agent-message-parts.d.ts +0 -17
  277. package/dist/agent-message-parts.js +0 -31
  278. package/dist/agent-message-parts.js.map +0 -1
  279. package/dist/instance-directory.d.ts +0 -32
  280. package/dist/instance-directory.js +0 -82
  281. package/dist/instance-directory.js.map +0 -1
  282. package/dist/instance-registry.d.ts +0 -39
  283. package/dist/instance-registry.js +0 -208
  284. package/dist/instance-registry.js.map +0 -1
  285. package/dist/multiplexer/session-actions.d.ts +0 -40
  286. package/dist/multiplexer/session-actions.js +0 -110
  287. package/dist/multiplexer/session-actions.js.map +0 -1
  288. package/dist/orchestration-dispatcher.d.ts +0 -25
  289. package/dist/orchestration-dispatcher.js +0 -59
  290. package/dist/orchestration-dispatcher.js.map +0 -1
  291. package/dist/session-input-operations.d.ts +0 -19
  292. package/dist/session-input-operations.js +0 -46
  293. package/dist/session-input-operations.js.map +0 -1
  294. package/dist/session-message-history.d.ts +0 -27
  295. package/dist/session-message-history.js +0 -105
  296. package/dist/session-message-history.js.map +0 -1
  297. package/dist/task-dispatcher.d.ts +0 -64
  298. package/dist/task-dispatcher.js +0 -213
  299. package/dist/task-dispatcher.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -1,19 +1,92 @@
1
1
  import { createServer } from "node:http";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve as pathResolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { writeJsonAtomic } from "./atomic-write.js";
6
- import { getDaemonInfoPath, getDaemonStatePath, getProjectIdFor } from "./paths.js";
6
+ import { getDaemonInfoPath, getDaemonStatePath, getDaemonStdioLogPath, getProjectIdFor, getProjectServiceStdioLogPathFor, } from "./paths.js";
7
7
  import { listDesktopProjects } from "./project-scanner.js";
8
8
  import { loadMetadataEndpoint } from "./metadata-store.js";
9
9
  import { requestJson } from "./http-client.js";
10
- const DAEMON_PORT = 43190;
11
- const DAEMON_HOST = "127.0.0.1";
10
+ import { getLoggingConfig, log } from "./debug.js";
11
+ import { RelayClient } from "./relay-client.js";
12
+ import { loadCredentials, setRemoteEnabled } from "./credentials.js";
13
+ import { assertRemoteAccessAllowed, parseRemoteActor } from "./remote-access.js";
14
+ const DEFAULT_DAEMON_PORT = 43190;
15
+ const DEFAULT_DAEMON_HOST = "127.0.0.1";
12
16
  const DAEMON_STARTUP_TIMEOUT_MS = 10_000;
13
17
  const PROJECT_SERVICE_STARTUP_GRACE_MS = 15_000;
18
+ const PROJECT_SERVICE_TERM_GRACE_MS = 2_000;
19
+ const PROJECT_SERVICE_KILL_GRACE_MS = 3_000;
20
+ const PROJECT_SERVICE_EXIT_POLL_MS = 50;
21
+ const PROXY_TIMEOUT_MS = 10_000;
22
+ // `::1` is intentionally excluded — building http://::1:port is invalid (IPv6
23
+ // needs brackets) and metadata services bind to 127.0.0.1 anyway.
24
+ const PROXY_ALLOWED_HOSTS = new Set(["127.0.0.1", "localhost"]);
25
+ const CORS_ALLOWED_ORIGINS = new Set([
26
+ "http://localhost:8081",
27
+ "http://127.0.0.1:8081",
28
+ "http://localhost:8091",
29
+ "http://127.0.0.1:8091",
30
+ "http://localhost:43192",
31
+ "http://127.0.0.1:43192",
32
+ ]);
33
+ export function getDaemonHost() {
34
+ const host = process.env.AIMUX_DAEMON_HOST?.trim();
35
+ const resolved = host || DEFAULT_DAEMON_HOST;
36
+ if (resolved !== "127.0.0.1" && resolved !== "localhost") {
37
+ throw new Error(`AIMUX_DAEMON_HOST must be loopback (127.0.0.1 or localhost), got ${resolved}`);
38
+ }
39
+ return resolved;
40
+ }
41
+ export function getDaemonPort() {
42
+ const raw = process.env.AIMUX_DAEMON_PORT?.trim();
43
+ if (!raw)
44
+ return DEFAULT_DAEMON_PORT;
45
+ const port = Number(raw);
46
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
47
+ throw new Error(`AIMUX_DAEMON_PORT must be an integer between 1 and 65535, got ${raw}`);
48
+ }
49
+ return port;
50
+ }
51
+ function getDaemonBaseUrl(port = getDaemonPort()) {
52
+ return `http://${getDaemonHost()}:${port}`;
53
+ }
14
54
  function ensureParent(path) {
15
55
  mkdirSync(dirname(path), { recursive: true });
16
56
  }
57
+ function loggingChildEnv() {
58
+ const logging = getLoggingConfig();
59
+ if (!logging.enabled)
60
+ return process.env;
61
+ return {
62
+ ...process.env,
63
+ AIMUX_LOG: "1",
64
+ AIMUX_LOG_LEVEL: logging.level,
65
+ AIMUX_LOG_CATEGORIES: logging.categories.join(","),
66
+ };
67
+ }
68
+ function loggingChildStdio(path) {
69
+ const logging = getLoggingConfig();
70
+ if (!logging.enabled)
71
+ return { stdio: "ignore", close: () => { } };
72
+ try {
73
+ ensureParent(path);
74
+ const stdout = openSync(path, "a");
75
+ const stderr = openSync(path, "a");
76
+ let closed = false;
77
+ const close = () => {
78
+ if (closed)
79
+ return;
80
+ closed = true;
81
+ closeSync(stdout);
82
+ closeSync(stderr);
83
+ };
84
+ return { stdio: ["ignore", stdout, stderr], close };
85
+ }
86
+ catch {
87
+ return { stdio: "ignore", close: () => { } };
88
+ }
89
+ }
17
90
  function saveJson(path, value) {
18
91
  try {
19
92
  writeJsonAtomic(path, value);
@@ -64,6 +137,32 @@ function send(res, status, body) {
64
137
  res.setHeader("connection", "close");
65
138
  res.end(payload);
66
139
  }
140
+ function setCorsHeaders(req, res) {
141
+ const origin = req.headers.origin;
142
+ if (origin && !isAllowedCorsOrigin(origin))
143
+ return false;
144
+ if (origin) {
145
+ res.setHeader("Access-Control-Allow-Origin", origin);
146
+ res.setHeader("Vary", "Origin");
147
+ }
148
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
149
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
150
+ return true;
151
+ }
152
+ function isAllowedCorsOrigin(origin) {
153
+ if (CORS_ALLOWED_ORIGINS.has(origin))
154
+ return true;
155
+ try {
156
+ const parsed = new URL(origin);
157
+ return parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ function rejectCors(res) {
164
+ send(res, 403, { ok: false, error: "origin not allowed" });
165
+ }
67
166
  export function loadDaemonInfo() {
68
167
  const info = loadJson(getDaemonInfoPath(), null);
69
168
  if (!info)
@@ -75,6 +174,11 @@ export async function stopDaemon(signal = "SIGTERM") {
75
174
  if (!info)
76
175
  return null;
77
176
  const state = loadDaemonState();
177
+ log.info("stopping daemon", "daemon", {
178
+ pid: info.pid,
179
+ signal,
180
+ projectCount: Object.keys(state.projects).length,
181
+ });
78
182
  for (const entry of Object.values(state.projects)) {
79
183
  try {
80
184
  process.kill(entry.pid, signal);
@@ -123,7 +227,7 @@ export async function requestDaemonJson(path, init) {
123
227
  if (!info) {
124
228
  throw new Error("aimux daemon is not running");
125
229
  }
126
- const { status, json } = await requestJson(`http://${DAEMON_HOST}:${info.port}${path}`, {
230
+ const { status, json } = await requestJson(`${getDaemonBaseUrl(info.port)}${path}`, {
127
231
  method: init?.method,
128
232
  headers: init?.headers,
129
233
  body: init?.body,
@@ -140,29 +244,49 @@ export async function ensureDaemonRunning() {
140
244
  await requestDaemonJson("/health");
141
245
  return existing;
142
246
  }
143
- catch {
247
+ catch (error) {
248
+ log.warn("stored daemon info failed health check", "daemon", {
249
+ pid: existing.pid,
250
+ error: error instanceof Error ? error.message : String(error),
251
+ });
144
252
  clearFile(getDaemonInfoPath());
145
253
  }
146
254
  }
147
255
  try {
148
- const { status, json } = await requestJson(`http://${DAEMON_HOST}:${DAEMON_PORT}/health`);
256
+ const { status, json } = await requestJson(`${getDaemonBaseUrl()}/health`);
149
257
  if (status >= 200 && status < 300 && json?.ok !== false && typeof json?.pid === "number") {
150
258
  const adopted = {
151
259
  pid: json.pid,
152
- port: typeof json?.port === "number" ? json.port : DAEMON_PORT,
260
+ port: typeof json?.port === "number" ? json.port : getDaemonPort(),
153
261
  startedAt: loadDaemonInfo()?.startedAt ?? new Date().toISOString(),
154
262
  updatedAt: new Date().toISOString(),
155
263
  };
156
264
  saveJson(getDaemonInfoPath(), adopted);
265
+ log.info("adopted existing daemon on default port", "daemon", { ...adopted });
157
266
  return adopted;
158
267
  }
159
268
  }
160
- catch { }
161
- const child = spawn(process.execPath, [process.argv[1], "daemon", "run"], {
162
- detached: true,
163
- stdio: "ignore",
164
- });
269
+ catch (error) {
270
+ log.debug("default daemon health probe failed", "daemon", {
271
+ error: error instanceof Error ? error.message : String(error),
272
+ });
273
+ }
274
+ const stdio = loggingChildStdio(getDaemonStdioLogPath());
275
+ let child;
276
+ try {
277
+ child = spawn(process.execPath, [process.argv[1], "daemon", "run"], {
278
+ detached: true,
279
+ env: loggingChildEnv(),
280
+ stdio: stdio.stdio,
281
+ });
282
+ }
283
+ catch (error) {
284
+ stdio.close();
285
+ throw error;
286
+ }
287
+ child.once("exit", stdio.close);
165
288
  child.unref();
289
+ log.info("spawned daemon", "daemon", { pid: child.pid });
166
290
  const deadline = Date.now() + DAEMON_STARTUP_TIMEOUT_MS;
167
291
  while (Date.now() < deadline) {
168
292
  const info = loadDaemonInfo();
@@ -203,14 +327,16 @@ export async function projectServiceStatus(projectRoot) {
203
327
  }
204
328
  export class AimuxDaemon {
205
329
  server = null;
330
+ relayClient = null;
206
331
  children = new Map();
332
+ projectEnsurePromises = new Map();
207
333
  state = loadDaemonState();
208
334
  async start() {
209
335
  if (this.server)
210
336
  return;
211
337
  saveJson(getDaemonInfoPath(), {
212
338
  pid: process.pid,
213
- port: DAEMON_PORT,
339
+ port: getDaemonPort(),
214
340
  startedAt: new Date().toISOString(),
215
341
  updatedAt: new Date().toISOString(),
216
342
  });
@@ -222,16 +348,57 @@ export class AimuxDaemon {
222
348
  });
223
349
  });
224
350
  });
351
+ const host = getDaemonHost();
352
+ const port = getDaemonPort();
225
353
  await new Promise((resolve, reject) => {
226
354
  this.server.once("error", reject);
227
- this.server.listen(DAEMON_PORT, DAEMON_HOST, () => {
355
+ this.server.listen(port, host, () => {
228
356
  this.server?.off("error", reject);
229
357
  resolve();
230
358
  });
231
359
  });
232
360
  this.refreshState();
361
+ log.info("daemon started", "daemon", { pid: process.pid, host, port });
362
+ this.connectRelayIfConfigured();
363
+ }
364
+ // Resolve relay config from stored credentials (`aimux login`), with env-var
365
+ // overrides for advanced/CI use. Connects only when remote access is enabled.
366
+ connectRelayIfConfigured(options = {}) {
367
+ const status = this.relayClient?.getStatus().status;
368
+ if (!options.force && this.relayClient && status !== "auth_failed" && status !== "disconnected")
369
+ return;
370
+ if (this.relayClient) {
371
+ this.relayClient.disconnect();
372
+ this.relayClient = null;
373
+ }
374
+ const creds = loadCredentials();
375
+ const relayUrl = process.env.AIMUX_RELAY_URL ?? creds?.relayUrl;
376
+ const relayToken = process.env.AIMUX_RELAY_TOKEN ?? creds?.token;
377
+ const hasEnvOverride = Boolean(process.env.AIMUX_RELAY_URL || process.env.AIMUX_RELAY_TOKEN);
378
+ const enabled = hasEnvOverride ? Boolean(relayUrl && relayToken) : Boolean(creds?.remoteEnabled);
379
+ if (relayUrl && relayToken && enabled) {
380
+ this.relayClient = new RelayClient(relayUrl, relayToken, this);
381
+ this.relayClient.connect();
382
+ }
383
+ }
384
+ getRelayStatus() {
385
+ return this.relayClient?.getStatus() ?? { status: "off" };
386
+ }
387
+ enableRelay() {
388
+ setRemoteEnabled(true);
389
+ this.connectRelayIfConfigured({ force: true });
390
+ return this.getRelayStatus();
391
+ }
392
+ disableRelay() {
393
+ setRemoteEnabled(false);
394
+ this.relayClient?.disconnect();
395
+ this.relayClient = null;
396
+ return { status: "off" };
233
397
  }
234
398
  stop() {
399
+ log.info("daemon stopping child services", "daemon", { projectCount: Object.keys(this.state.projects).length });
400
+ this.relayClient?.disconnect();
401
+ this.relayClient = null;
235
402
  for (const entry of Object.values(this.state.projects)) {
236
403
  try {
237
404
  process.kill(entry.pid, "SIGTERM");
@@ -267,16 +434,25 @@ export class AimuxDaemon {
267
434
  saveJson(getDaemonStatePath(), this.state);
268
435
  saveJson(getDaemonInfoPath(), {
269
436
  pid: process.pid,
270
- port: DAEMON_PORT,
437
+ port: getDaemonPort(),
271
438
  startedAt: loadDaemonInfo()?.startedAt ?? new Date().toISOString(),
272
439
  updatedAt: new Date().toISOString(),
273
440
  });
274
441
  }
275
442
  spawnProjectService(projectRoot, projectId) {
276
- const child = spawn(process.execPath, [process.argv[1], "__project-service-internal"], {
277
- cwd: projectRoot,
278
- stdio: "ignore",
279
- });
443
+ const stdio = loggingChildStdio(getProjectServiceStdioLogPathFor(projectRoot));
444
+ let child;
445
+ try {
446
+ child = spawn(process.execPath, [process.argv[1], "__project-service-internal"], {
447
+ cwd: projectRoot,
448
+ env: loggingChildEnv(),
449
+ stdio: stdio.stdio,
450
+ });
451
+ }
452
+ catch (error) {
453
+ stdio.close();
454
+ throw error;
455
+ }
280
456
  this.children.set(projectId, child);
281
457
  const now = new Date().toISOString();
282
458
  const state = {
@@ -287,8 +463,23 @@ export class AimuxDaemon {
287
463
  updatedAt: now,
288
464
  };
289
465
  this.state.projects[projectId] = state;
290
- child.on("exit", () => {
291
- this.children.delete(projectId);
466
+ log.info("spawned project service", "daemon", {
467
+ projectId,
468
+ projectRoot,
469
+ pid: state.pid,
470
+ });
471
+ child.on("exit", (code, signal) => {
472
+ stdio.close();
473
+ log.warn("project service exited", "daemon", {
474
+ projectId,
475
+ projectRoot,
476
+ pid: state.pid,
477
+ code,
478
+ signal,
479
+ });
480
+ if (this.children.get(projectId) === child) {
481
+ this.children.delete(projectId);
482
+ }
292
483
  const current = this.state.projects[projectId];
293
484
  if (current?.pid === state.pid) {
294
485
  delete this.state.projects[projectId];
@@ -301,6 +492,18 @@ export class AimuxDaemon {
301
492
  async ensureProject(projectRoot) {
302
493
  const resolvedRoot = pathResolve(projectRoot);
303
494
  const projectId = getProjectIdFor(resolvedRoot);
495
+ const existingEnsure = this.projectEnsurePromises.get(projectId);
496
+ if (existingEnsure)
497
+ return existingEnsure;
498
+ const ensure = this.ensureProjectUnlocked(resolvedRoot, projectId).finally(() => {
499
+ if (this.projectEnsurePromises.get(projectId) === ensure) {
500
+ this.projectEnsurePromises.delete(projectId);
501
+ }
502
+ });
503
+ this.projectEnsurePromises.set(projectId, ensure);
504
+ return ensure;
505
+ }
506
+ async ensureProjectUnlocked(resolvedRoot, projectId) {
304
507
  const existing = this.state.projects[projectId];
305
508
  if (existing && isPidAlive(existing.pid)) {
306
509
  const startedAtMs = Date.parse(existing.startedAt);
@@ -319,13 +522,12 @@ export class AimuxDaemon {
319
522
  if (withinStartupGrace) {
320
523
  return refreshExisting();
321
524
  }
322
- try {
323
- process.kill(existing.pid, "SIGTERM");
324
- }
325
- catch { }
326
- delete this.state.projects[projectId];
327
- this.refreshState();
328
- return this.spawnProjectService(resolvedRoot, projectId);
525
+ log.warn("project service missing metadata endpoint after startup grace", "daemon", {
526
+ projectId,
527
+ projectRoot: resolvedRoot,
528
+ pid: existing.pid,
529
+ });
530
+ return this.replaceProjectServiceAfterExit(resolvedRoot, projectId, existing, refreshExisting);
329
531
  }
330
532
  try {
331
533
  const { status, json } = await requestJson(`http://${endpoint.host}:${endpoint.port}/health`, {
@@ -335,22 +537,95 @@ export class AimuxDaemon {
335
537
  throw new Error(json?.error || `health request failed: ${status}`);
336
538
  }
337
539
  }
338
- catch {
540
+ catch (error) {
339
541
  if (withinStartupGrace) {
340
542
  return refreshExisting();
341
543
  }
342
- try {
343
- process.kill(existing.pid, "SIGTERM");
344
- }
345
- catch { }
346
- delete this.state.projects[projectId];
347
- this.refreshState();
348
- return this.spawnProjectService(resolvedRoot, projectId);
544
+ log.warn("project service health check failed", "daemon", {
545
+ projectId,
546
+ projectRoot: resolvedRoot,
547
+ pid: existing.pid,
548
+ error: error instanceof Error ? error.message : String(error),
549
+ });
550
+ return this.replaceProjectServiceAfterExit(resolvedRoot, projectId, existing, refreshExisting);
349
551
  }
350
552
  return refreshExisting();
351
553
  }
352
554
  return this.spawnProjectService(resolvedRoot, projectId);
353
555
  }
556
+ async replaceProjectServiceAfterExit(resolvedRoot, projectId, existing, refreshExisting) {
557
+ const stopped = await this.terminateProjectService(projectId, existing);
558
+ if (!stopped) {
559
+ log.warn("project service did not exit before replacement deadline", "daemon", {
560
+ projectId,
561
+ projectRoot: resolvedRoot,
562
+ pid: existing.pid,
563
+ });
564
+ return refreshExisting();
565
+ }
566
+ const current = this.state.projects[projectId];
567
+ if (current?.pid === existing.pid) {
568
+ delete this.state.projects[projectId];
569
+ }
570
+ const child = this.children.get(projectId);
571
+ if (child?.pid === existing.pid) {
572
+ this.children.delete(projectId);
573
+ }
574
+ this.refreshState();
575
+ return this.spawnProjectService(resolvedRoot, projectId);
576
+ }
577
+ async terminateProjectService(projectId, existing) {
578
+ const child = this.children.get(projectId);
579
+ log.info("terminating project service", "daemon", {
580
+ projectId,
581
+ projectRoot: existing.projectRoot,
582
+ pid: existing.pid,
583
+ });
584
+ try {
585
+ process.kill(existing.pid, "SIGTERM");
586
+ }
587
+ catch {
588
+ return true;
589
+ }
590
+ if (await this.waitForProjectServiceExit(existing.pid, child, PROJECT_SERVICE_TERM_GRACE_MS)) {
591
+ return true;
592
+ }
593
+ try {
594
+ process.kill(existing.pid, "SIGKILL");
595
+ }
596
+ catch {
597
+ return true;
598
+ }
599
+ return this.waitForProjectServiceExit(existing.pid, child, PROJECT_SERVICE_KILL_GRACE_MS);
600
+ }
601
+ async waitForProjectServiceExit(pid, child, timeoutMs) {
602
+ if (!isPidAlive(pid))
603
+ return true;
604
+ return new Promise((resolve) => {
605
+ let settled = false;
606
+ const cleanups = [];
607
+ const finish = (value) => {
608
+ if (settled)
609
+ return;
610
+ settled = true;
611
+ for (const cleanup of cleanups)
612
+ cleanup();
613
+ resolve(value);
614
+ };
615
+ const onExit = () => finish(true);
616
+ if (child) {
617
+ child.once("exit", onExit);
618
+ cleanups.push(() => child.off("exit", onExit));
619
+ }
620
+ const interval = setInterval(() => {
621
+ if (!isPidAlive(pid))
622
+ finish(true);
623
+ }, PROJECT_SERVICE_EXIT_POLL_MS);
624
+ cleanups.push(() => clearInterval(interval));
625
+ const timeout = setTimeout(() => finish(!isPidAlive(pid)), timeoutMs);
626
+ cleanups.push(() => clearTimeout(timeout));
627
+ });
628
+ }
354
629
  stopProject(projectRoot) {
355
630
  const projectId = getProjectIdFor(pathResolve(projectRoot));
356
631
  const existing = this.state.projects[projectId];
@@ -361,18 +636,34 @@ export class AimuxDaemon {
361
636
  }
362
637
  catch { }
363
638
  delete this.state.projects[projectId];
364
- this.children.delete(projectId);
639
+ if (this.children.get(projectId)?.pid === existing.pid) {
640
+ this.children.delete(projectId);
641
+ }
365
642
  this.refreshState();
366
643
  return existing;
367
644
  }
368
- async handle(req, res) {
645
+ async routeRequest(method, path, body, headers) {
369
646
  this.refreshState();
370
- const url = new URL(req.url ?? "/", `http://${DAEMON_HOST}:${DAEMON_PORT}`);
371
- if (req.method === "GET" && url.pathname === "/health") {
372
- send(res, 200, { ok: true, pid: process.pid, port: DAEMON_PORT });
373
- return;
647
+ const routeUrl = new URL(path, getDaemonBaseUrl());
648
+ const pathname = routeUrl.pathname;
649
+ const actor = parseRemoteActor(headers);
650
+ const access = assertRemoteAccessAllowed(actor, method, pathname, routeUrl.searchParams);
651
+ if (!access.ok) {
652
+ return { status: access.status ?? 403, body: { ok: false, error: access.error ?? "remote access denied" } };
653
+ }
654
+ if (method === "GET" && pathname === "/health") {
655
+ return { status: 200, body: { ok: true, pid: process.pid, port: getDaemonPort() } };
656
+ }
657
+ if (method === "GET" && pathname === "/relay/status") {
658
+ return { status: 200, body: { ok: true, relay: this.getRelayStatus() } };
659
+ }
660
+ if (method === "POST" && pathname === "/relay/enable") {
661
+ return { status: 200, body: { ok: true, relay: this.enableRelay() } };
374
662
  }
375
- if (req.method === "GET" && url.pathname === "/projects") {
663
+ if (method === "POST" && pathname === "/relay/disable") {
664
+ return { status: 200, body: { ok: true, relay: this.disableRelay() } };
665
+ }
666
+ if (method === "GET" && pathname === "/projects") {
376
667
  const liveById = this.state.projects;
377
668
  const projects = listDesktopProjects().map((project) => ({
378
669
  ...project,
@@ -380,36 +671,70 @@ export class AimuxDaemon {
380
671
  serviceAlive: Boolean(liveById[project.id]),
381
672
  serviceEndpoint: project.path ? loadMetadataEndpoint(project.path) : null,
382
673
  }));
383
- send(res, 200, { ok: true, projects });
384
- return;
674
+ return { status: 200, body: { ok: true, projects } };
385
675
  }
386
- if (req.method === "GET" && url.pathname.startsWith("/projects/")) {
387
- const projectId = decodeURIComponent(url.pathname.slice("/projects/".length));
676
+ if (method === "GET" && pathname.startsWith("/projects/")) {
677
+ const projectId = decodeURIComponent(pathname.slice("/projects/".length));
388
678
  const project = this.state.projects[projectId] ?? null;
389
- send(res, 200, { ok: true, project });
390
- return;
679
+ return { status: 200, body: { ok: true, project } };
391
680
  }
392
- if (req.method === "POST" && url.pathname === "/projects/ensure") {
393
- const body = (await readJson(req));
394
- if (!body.projectRoot) {
395
- send(res, 400, { ok: false, error: "projectRoot is required" });
396
- return;
681
+ if (method === "POST" && pathname === "/projects/ensure") {
682
+ const b = body;
683
+ if (!b?.projectRoot) {
684
+ return { status: 400, body: { ok: false, error: "projectRoot is required" } };
397
685
  }
398
- const project = await this.ensureProject(body.projectRoot);
399
- send(res, 200, { ok: true, project });
400
- return;
686
+ const project = await this.ensureProject(b.projectRoot);
687
+ return { status: 200, body: { ok: true, project } };
401
688
  }
402
- if (req.method === "POST" && url.pathname === "/projects/stop") {
403
- const body = (await readJson(req));
404
- if (!body.projectRoot) {
405
- send(res, 400, { ok: false, error: "projectRoot is required" });
406
- return;
689
+ if (method === "POST" && pathname === "/projects/stop") {
690
+ const b = body;
691
+ if (!b?.projectRoot) {
692
+ return { status: 400, body: { ok: false, error: "projectRoot is required" } };
693
+ }
694
+ const project = this.stopProject(b.projectRoot);
695
+ return { status: 200, body: { ok: true, project } };
696
+ }
697
+ const proxyMatch = pathname.match(/^\/proxy\/([^/]+)\/(\d+)(\/.*)/);
698
+ if (proxyMatch) {
699
+ const [, host, portStr, subPath] = proxyMatch;
700
+ if (!PROXY_ALLOWED_HOSTS.has(host)) {
701
+ return { status: 403, body: { ok: false, error: "proxy host not allowed" } };
702
+ }
703
+ try {
704
+ const { status, json } = await requestJson(`http://${host}:${portStr}${subPath}${routeUrl.search}`, {
705
+ method,
706
+ headers,
707
+ body: body !== undefined ? body : undefined,
708
+ timeoutMs: PROXY_TIMEOUT_MS,
709
+ });
710
+ return { status, body: json };
711
+ }
712
+ catch (err) {
713
+ const message = err instanceof Error ? err.message : "Proxy error";
714
+ const status = /timed? out|timeout/i.test(message) ? 504 : 502;
715
+ return { status, body: { ok: false, error: message } };
407
716
  }
408
- const project = this.stopProject(body.projectRoot);
409
- send(res, 200, { ok: true, project });
717
+ }
718
+ return { status: 404, body: { ok: false, error: "not found" } };
719
+ }
720
+ async handle(req, res) {
721
+ // No auth on the HTTP server: the daemon binds to 127.0.0.1, and remote
722
+ // app requests come in over the relay (which performs Clerk/HS256 verify
723
+ // before forwarding) and call routeRequest() directly in-process. Local
724
+ // CLI is trusted with the daemon's user.
725
+ if (!setCorsHeaders(req, res)) {
726
+ rejectCors(res);
727
+ return;
728
+ }
729
+ if (req.method === "OPTIONS") {
730
+ res.statusCode = 204;
731
+ res.end();
410
732
  return;
411
733
  }
412
- send(res, 404, { ok: false, error: "not found" });
734
+ const url = new URL(req.url ?? "/", getDaemonBaseUrl());
735
+ const body = req.method === "POST" ? await readJson(req) : undefined;
736
+ const result = await this.routeRequest(req.method ?? "GET", `${url.pathname}${url.search}`, body);
737
+ send(res, result.status, result.body);
413
738
  }
414
739
  }
415
740
  //# sourceMappingURL=daemon.js.map