aimux-cli 0.1.15 → 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 +824 -152
  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 +31 -15
  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
@@ -1,9 +1,10 @@
1
1
  import { createServer } from "node:http";
2
- import { createHash } from "node:crypto";
3
- import { readFileSync, writeFileSync } from "node:fs";
4
- import { basename } from "node:path";
5
- import { getDashboardClientUiStatePath, getProjectId, getProjectStateDir } from "./paths.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { getDashboardClientUiStatePath, getPlansDir, getProjectId, getProjectStateDir } from "./paths.js";
6
6
  import { updateSessionMetadata, clearSessionLogs, saveMetadataEndpoint, loadMetadataState, } from "./metadata-store.js";
7
+ import { contextualizeAlertInput, mergeDisplayContext, metadataDisplayContext, } from "./alert-display.js";
7
8
  import { notifyAlert } from "./notify.js";
8
9
  import { clearNotifications, listNotifications, markNotificationsRead, unreadNotificationCount, } from "./notifications.js";
9
10
  import { updateNotificationContext } from "./notification-context.js";
@@ -11,84 +12,60 @@ import { AgentTracker } from "./agent-tracker.js";
11
12
  import { createThread, listThreadSummaries, markThreadSeen, readMessages, readThread, setThreadStatus, } from "./threads.js";
12
13
  import { sendDirectMessage, sendThreadMessage } from "./orchestration.js";
13
14
  import { acceptHandoff, approveReview, acceptTask, assignTask, blockTask, completeHandoff, completeTask, reopenTask, requestTaskChanges, sendHandoff, } from "./orchestration-actions.js";
15
+ import { readAllTasks, readTask } from "./tasks.js";
14
16
  import { buildWorkflowEntries } from "./workflow.js";
15
17
  import { markLastUsed } from "./last-used.js";
16
18
  import { formatRelativeRecency } from "./recency.js";
17
- import { getAttachment, getAttachmentContent, ingestAttachmentFromBase64, ingestAttachmentFromPath, } from "./attachment-store.js";
19
+ import { getAttachment, getAttachmentContent } from "./attachment-store.js";
18
20
  import { ProjectEventBus } from "./project-events.js";
19
21
  import { getProjectServiceManifest } from "./project-service-manifest.js";
20
22
  import { applyShellStateTransition } from "./shell-state.js";
23
+ import { isTeammateSession, selectDirectTeammates } from "./team.js";
21
24
  import { listSwitchableAgentItems, resolveAttentionAgent, resolveNextAgent, resolvePrevAgent, serializeFastControlItem, } from "./fast-control.js";
22
25
  import { TmuxRuntimeManager } from "./tmux/runtime-manager.js";
23
26
  import { openTargetForClient } from "./tmux/window-open.js";
24
27
  import { getDashboardCommandSpec } from "./dashboard/command-spec.js";
25
- function compactSessionId(sessionId) {
26
- const compact = sessionId.replace(/-[a-z0-9]{4,}$/i, "");
27
- return compact || sessionId;
28
+ import { createRuntimeExchangeStore, } from "./runtime-core/exchange-store.js";
29
+ import { listTopologySessionStates } from "./runtime-core/topology-sessions.js";
30
+ const LIBRARY_DOC_ALLOWLIST = [
31
+ { path: "AGENTS.md", kind: "instructions", title: "AGENTS.md" },
32
+ { path: "CLAUDE.md", kind: "adapter", title: "CLAUDE.md" },
33
+ { path: "CODEX.md", kind: "adapter", title: "CODEX.md" },
34
+ { path: "README.md", kind: "project", title: "README.md" },
35
+ ];
36
+ function isLibraryPathExposed(path) {
37
+ const normalized = path.replaceAll("\\", "/").toLowerCase();
38
+ return !normalized.startsWith(".aimux/") && !normalized.endsWith("config.json");
28
39
  }
29
- function metadataDisplayContext(metadata) {
30
- return {
31
- label: metadata?.label,
32
- worktreePath: metadata?.context?.worktreePath,
33
- worktreeName: metadata?.context?.worktreeName,
34
- branch: metadata?.context?.branch,
35
- };
36
- }
37
- function mergeDisplayContext(base, override) {
38
- return {
39
- label: override.label ?? base.label,
40
- command: override.command ?? base.command,
41
- worktreePath: override.worktreePath ?? base.worktreePath,
42
- worktreeName: override.worktreeName ?? base.worktreeName,
43
- branch: override.branch ?? base.branch,
44
- };
45
- }
46
- function displayWorktreeLabel(context) {
47
- const worktreeName = context.worktreeName?.trim();
48
- const branch = context.branch?.trim();
49
- if (worktreeName)
50
- return worktreeName;
51
- if (branch)
52
- return branch;
53
- const path = context.worktreePath?.trim();
54
- return path ? basename(path) : undefined;
55
- }
56
- function sessionAlertSubject(sessionId, context) {
57
- if (!sessionId)
58
- return undefined;
59
- const label = context?.label?.trim() || context?.command?.trim() || compactSessionId(sessionId);
60
- const worktree = context ? displayWorktreeLabel(context) : undefined;
61
- return worktree ? `${label} @ ${worktree}` : label;
62
- }
63
- function sessionAlertTitle(kind, sessionId, fallback, context) {
64
- const title = fallback?.trim();
65
- const subject = sessionAlertSubject(sessionId, context);
66
- if (!subject)
67
- return title || "aimux";
68
- if (kind === "needs_input")
69
- return `${subject} needs input`;
70
- if (kind === "blocked") {
71
- if (!title || (sessionId && title === `${sessionId} is blocked`))
72
- return `${subject} is blocked`;
73
- return title;
74
- }
75
- if (kind === "task_failed") {
76
- if (!title || (sessionId && title === `${sessionId} errored`))
77
- return `${subject} errored`;
78
- return title;
79
- }
80
- if (kind === "task_done") {
81
- if (!title || (sessionId && title === `${sessionId} finished`))
82
- return `${subject} finished`;
83
- return title;
84
- }
85
- if (!title)
86
- return subject;
87
- if (title.includes(subject))
88
- return title;
89
- if (sessionId && title.includes(sessionId))
90
- return title.replace(sessionId, subject);
91
- return `${subject}: ${title}`;
40
+ function listLibraryDocuments(projectRoot = process.cwd()) {
41
+ return LIBRARY_DOC_ALLOWLIST.flatMap((entry) => {
42
+ if (!isLibraryPathExposed(entry.path))
43
+ return [];
44
+ try {
45
+ const fullPath = join(projectRoot, entry.path);
46
+ if (!existsSync(fullPath))
47
+ return [];
48
+ const stat = statSync(fullPath);
49
+ if (!stat.isFile())
50
+ return [];
51
+ const content = readFileSync(fullPath, "utf8");
52
+ return [
53
+ {
54
+ id: entry.path,
55
+ title: entry.title,
56
+ path: entry.path,
57
+ kind: entry.kind,
58
+ size: stat.size,
59
+ updatedAt: stat.mtime.toISOString(),
60
+ content: content.slice(0, 40_000),
61
+ truncated: content.length > 40_000,
62
+ },
63
+ ];
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ });
92
69
  }
93
70
  function dashboardClientKeyFromSession(sessionName) {
94
71
  return sessionName.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -136,6 +113,7 @@ function markActiveWindowFocused(tmux, projectRoot, currentClientSession, curren
136
113
  return false;
137
114
  updateNotificationContext("tui", {
138
115
  focused: true,
116
+ screen: match.metadata.kind === "service" ? "service" : "agent",
139
117
  sessionId: match.metadata.sessionId,
140
118
  panelOpen: false,
141
119
  });
@@ -162,6 +140,17 @@ function desiredPort() {
162
140
  const hash = createHash("sha1").update(getProjectId()).digest("hex").slice(0, 6);
163
141
  return 43000 + (parseInt(hash, 16) % 10000);
164
142
  }
143
+ // Plan paths join this directly into `${sessionId}.md`, so restrict to a
144
+ // conservative charset (no whitespace, no separators, no traversal) and
145
+ // cap length so we don't produce surprising filenames.
146
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
147
+ function validateSessionId(raw) {
148
+ if (!SESSION_ID_PATTERN.test(raw))
149
+ return { ok: false };
150
+ if (raw.includes(".."))
151
+ return { ok: false };
152
+ return { ok: true, value: raw };
153
+ }
165
154
  async function readJson(req) {
166
155
  const chunks = [];
167
156
  for await (const chunk of req) {
@@ -192,6 +181,153 @@ function sendSseEvent(res, event, data) {
192
181
  res.write(`event: ${event}\n`);
193
182
  res.write(`data: ${JSON.stringify(data)}\n\n`);
194
183
  }
184
+ function topologyDesktopSessionList(statuses) {
185
+ return listTopologySessionStates({ statuses }).map((session) => ({
186
+ ...session,
187
+ status: session.status ?? "offline",
188
+ team: session.team,
189
+ }));
190
+ }
191
+ function firstLine(value) {
192
+ return (value
193
+ .split(/\r?\n/)
194
+ .map((line) => line.trim())
195
+ .find(Boolean) ?? "");
196
+ }
197
+ function teammateTaskDescription(body) {
198
+ const explicitTitle = typeof body.title === "string" ? body.title.trim() : "";
199
+ if (explicitTitle)
200
+ return explicitTitle;
201
+ const explicitDescription = typeof body.description === "string" ? body.description.trim() : "";
202
+ if (explicitDescription)
203
+ return explicitDescription;
204
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
205
+ const bodyText = typeof body.body === "string" ? body.body.trim() : "";
206
+ const text = bodyText || prompt;
207
+ const line = firstLine(text);
208
+ return line ? line.slice(0, 120) : "Teammate task";
209
+ }
210
+ function teammateTaskPrompt(body) {
211
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
212
+ if (prompt)
213
+ return prompt;
214
+ const text = typeof body.body === "string" ? body.body.trim() : "";
215
+ return text || undefined;
216
+ }
217
+ function optionalString(value) {
218
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
219
+ }
220
+ function optionalStringOrFirst(value) {
221
+ if (typeof value === "string")
222
+ return optionalString(value);
223
+ if (!Array.isArray(value))
224
+ return undefined;
225
+ return value.map(optionalString).find(Boolean);
226
+ }
227
+ function runtimeInboxEntries(input = {}) {
228
+ const exchange = createRuntimeExchangeStore().read();
229
+ const threadById = new Map(exchange.threads.map((thread) => [thread.id, thread]));
230
+ const taskById = new Map(exchange.tasks.map((task) => [task.id, task]));
231
+ const latestMessageByThread = new Map();
232
+ for (const message of exchange.messages) {
233
+ const existing = latestMessageByThread.get(message.threadId);
234
+ if (!existing || existing.ts < message.ts)
235
+ latestMessageByThread.set(message.threadId, message);
236
+ }
237
+ const entries = exchange.inbox
238
+ .filter((entry) => !input.participantId || entry.participantId === input.participantId)
239
+ .filter((entry) => input.includeNotifications || threadById.get(entry.subjectId)?.tags?.includes("notification") !== true)
240
+ .filter((entry) => input.includeDone || entry.state !== "done")
241
+ .filter((entry) => !input.unreadOnly || entry.state !== "done")
242
+ .map((entry) => runtimeInboxEntry(entry, threadById, taskById, latestMessageByThread))
243
+ .filter((entry) => Boolean(entry));
244
+ return entries.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
245
+ }
246
+ function runtimeInboxEntry(entry, threadById, taskById, latestMessageByThread) {
247
+ if (entry.subjectKind === "thread" || entry.subjectKind === "handoff" || entry.subjectKind === "message") {
248
+ const thread = threadById.get(entry.subjectId);
249
+ if (!thread)
250
+ return undefined;
251
+ const message = latestMessageByThread.get(thread.id);
252
+ return {
253
+ id: entry.id,
254
+ subjectKind: entry.subjectKind,
255
+ subjectId: entry.subjectId,
256
+ participantId: entry.participantId,
257
+ sessionId: entry.participantId,
258
+ title: thread.title,
259
+ subtitle: `${thread.kind} · ${thread.status}`,
260
+ body: message?.body ?? thread.title,
261
+ createdAt: entry.updatedAt,
262
+ unread: entry.state !== "done",
263
+ state: entry.state,
264
+ urgency: entry.urgency,
265
+ };
266
+ }
267
+ const task = taskById.get(entry.subjectId);
268
+ if (!task)
269
+ return undefined;
270
+ return {
271
+ id: entry.id,
272
+ subjectKind: entry.subjectKind,
273
+ subjectId: entry.subjectId,
274
+ participantId: entry.participantId,
275
+ sessionId: entry.participantId,
276
+ title: task.description,
277
+ subtitle: `${task.type ?? "task"} · ${task.status}`,
278
+ body: task.result ?? task.error ?? task.prompt,
279
+ createdAt: entry.updatedAt,
280
+ unread: entry.state !== "done",
281
+ state: entry.state,
282
+ urgency: entry.urgency,
283
+ };
284
+ }
285
+ function markRuntimeInboxRead(input = {}) {
286
+ const store = createRuntimeExchangeStore();
287
+ const exchange = store.read();
288
+ const threadById = new Map(exchange.threads.map((thread) => [thread.id, thread]));
289
+ const entries = store
290
+ .read()
291
+ .inbox.filter((entry) => !input.id || entry.id === input.id)
292
+ .filter((entry) => !input.participantId || entry.participantId === input.participantId)
293
+ .filter((entry) => input.includeNotifications || threadById.get(entry.subjectId)?.tags?.includes("notification") !== true);
294
+ let updated = 0;
295
+ for (const entry of entries) {
296
+ if (entry.state !== "done")
297
+ updated += 1;
298
+ if (entry.subjectKind === "thread" || entry.subjectKind === "handoff" || entry.subjectKind === "message") {
299
+ markThreadSeen(entry.subjectId, entry.participantId);
300
+ }
301
+ }
302
+ store.update((exchange) => ({
303
+ ...exchange,
304
+ inbox: exchange.inbox.map((entry) => (!input.id || entry.id === input.id) &&
305
+ (!input.participantId || entry.participantId === input.participantId) &&
306
+ (input.includeNotifications || threadById.get(entry.subjectId)?.tags?.includes("notification") !== true)
307
+ ? { ...entry, state: "done" }
308
+ : entry),
309
+ }));
310
+ return updated;
311
+ }
312
+ function teammateApiRecord(session) {
313
+ return {
314
+ id: session.id,
315
+ sessionId: session.id,
316
+ tool: session.command,
317
+ command: session.command,
318
+ label: session.team?.label ?? session.label,
319
+ role: session.team?.role,
320
+ status: session.status,
321
+ worktreePath: session.worktreePath,
322
+ headline: session.headline,
323
+ preview: session.preview,
324
+ createdAt: session.createdAt,
325
+ lastUsedAt: session.lastUsedAt,
326
+ pending: session.pending,
327
+ pendingAction: session.pendingAction,
328
+ team: session.team,
329
+ };
330
+ }
195
331
  export class MetadataServer {
196
332
  options;
197
333
  server = null;
@@ -241,6 +377,57 @@ export class MetadataServer {
241
377
  notifyChange() {
242
378
  this.options.onChange?.();
243
379
  }
380
+ resolveDirectTeammates(parentSessionId) {
381
+ if (!parentSessionId.trim()) {
382
+ return { ok: false, status: 400, error: "parentSessionId is required" };
383
+ }
384
+ const topologySessions = topologyDesktopSessionList(["running", "idle", "offline"]);
385
+ const sessions = topologySessions.filter((session) => !isTeammateSession(session));
386
+ const teammates = topologySessions.filter(isTeammateSession);
387
+ const parent = [...sessions, ...teammates].find((session) => session.id === parentSessionId);
388
+ if (!parent) {
389
+ return { ok: false, status: 404, error: `parent agent "${parentSessionId}" not found` };
390
+ }
391
+ if (isTeammateSession(parent)) {
392
+ return { ok: false, status: 400, error: "teammate agents cannot create or delegate to nested teams" };
393
+ }
394
+ return {
395
+ ok: true,
396
+ parent,
397
+ teammates: selectDirectTeammates(teammates, parentSessionId),
398
+ };
399
+ }
400
+ resolveDirectTeammate(parentSessionId, teammateSessionId) {
401
+ if (!teammateSessionId.trim()) {
402
+ return { ok: false, status: 400, error: "teammateSessionId is required" };
403
+ }
404
+ const resolved = this.resolveDirectTeammates(parentSessionId);
405
+ if (!resolved.ok)
406
+ return resolved;
407
+ const teammate = resolved.teammates.find((session) => session.id === teammateSessionId);
408
+ if (!teammate) {
409
+ return {
410
+ ok: false,
411
+ status: 404,
412
+ error: `teammate "${teammateSessionId}" is not attached to parent "${parentSessionId}"`,
413
+ };
414
+ }
415
+ return { ok: true, parent: resolved.parent, teammate };
416
+ }
417
+ resolveDirectGraveyardTeammate(parentSessionId, teammateSessionId) {
418
+ const resolved = this.resolveDirectTeammates(parentSessionId);
419
+ if (!resolved.ok)
420
+ return resolved;
421
+ const teammate = selectDirectTeammates(topologyDesktopSessionList(["graveyard"]), resolved.parent.id).find((session) => session.id === teammateSessionId);
422
+ if (!teammate) {
423
+ return {
424
+ ok: false,
425
+ status: 404,
426
+ error: `graveyard teammate "${teammateSessionId}" is not attached to parent "${parentSessionId}"`,
427
+ };
428
+ }
429
+ return { ok: true, parent: resolved.parent, teammate };
430
+ }
244
431
  listen(port) {
245
432
  return new Promise((resolve, reject) => {
246
433
  if (!this.server)
@@ -258,11 +445,7 @@ export class MetadataServer {
258
445
  }
259
446
  emitAlert(input) {
260
447
  const displayContext = this.resolveSessionAlertDisplayContext(input.sessionId, input.worktreePath);
261
- this.eventBus.publishAlert({
262
- ...input,
263
- title: sessionAlertTitle(input.kind, input.sessionId, input.title, displayContext),
264
- worktreePath: input.worktreePath ?? displayContext?.worktreePath,
265
- });
448
+ this.eventBus.publishAlert(contextualizeAlertInput(input, displayContext));
266
449
  }
267
450
  resolveSessionAlertDisplayContext(sessionId, worktreePath) {
268
451
  if (!sessionId)
@@ -343,6 +526,14 @@ export class MetadataServer {
343
526
  return [...new Set(fallbackRecipients.map((value) => value?.trim()).filter(Boolean))];
344
527
  }
345
528
  async handle(req, res) {
529
+ res.setHeader("Access-Control-Allow-Origin", "*");
530
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
531
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
532
+ if (req.method === "OPTIONS") {
533
+ res.statusCode = 204;
534
+ res.end();
535
+ return;
536
+ }
346
537
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
347
538
  if (req.method === "GET" && url.pathname === "/events") {
348
539
  const sessionFilter = url.searchParams.get("sessionId")?.trim() || null;
@@ -452,6 +643,26 @@ export class MetadataServer {
452
643
  });
453
644
  return;
454
645
  }
646
+ if (req.method === "GET" && url.pathname === "/library") {
647
+ send(res, 200, {
648
+ ok: true,
649
+ documents: listLibraryDocuments(),
650
+ });
651
+ return;
652
+ }
653
+ if (req.method === "GET" && url.pathname === "/inbox") {
654
+ const unreadOnly = url.searchParams.get("unread") === "1";
655
+ const participantId = url.searchParams.get("participant")?.trim() || undefined;
656
+ const includeDone = url.searchParams.get("includeDone") === "1";
657
+ const includeNotifications = url.searchParams.get("includeNotifications") === "1";
658
+ const inbox = runtimeInboxEntries({ unreadOnly, participantId, includeDone, includeNotifications });
659
+ send(res, 200, {
660
+ ok: true,
661
+ inbox,
662
+ unreadCount: inbox.filter((entry) => entry.unread).length,
663
+ });
664
+ return;
665
+ }
455
666
  if (req.method === "GET" && url.pathname === "/health") {
456
667
  send(res, 200, {
457
668
  ok: true,
@@ -518,6 +729,15 @@ export class MetadataServer {
518
729
  send(res, 200, buildWorkflowEntries(url.searchParams.get("participant") ?? "user"));
519
730
  return;
520
731
  }
732
+ if (req.method === "GET" && url.pathname === "/tasks") {
733
+ const sessionId = url.searchParams.get("session")?.trim();
734
+ const status = url.searchParams.get("status")?.trim();
735
+ const tasks = readAllTasks()
736
+ .filter((task) => !sessionId || task.assignedTo === sessionId || task.assignedBy === sessionId)
737
+ .filter((task) => !status || task.status === status);
738
+ send(res, 200, { ok: true, tasks });
739
+ return;
740
+ }
521
741
  if (req.method === "POST" && url.pathname === "/usage/mark") {
522
742
  const body = (await readJson(req));
523
743
  const itemId = body.itemId?.trim() || "";
@@ -635,7 +855,14 @@ export class MetadataServer {
635
855
  return;
636
856
  }
637
857
  if (req.method === "GET" && url.pathname.startsWith("/threads/")) {
638
- const threadId = decodeURIComponent(url.pathname.slice("/threads/".length));
858
+ let threadId;
859
+ try {
860
+ threadId = decodeURIComponent(url.pathname.slice("/threads/".length));
861
+ }
862
+ catch {
863
+ send(res, 400, { ok: false, error: "invalid threadId" });
864
+ return;
865
+ }
639
866
  const thread = readThread(threadId);
640
867
  if (!thread) {
641
868
  send(res, 404, { ok: false, error: "thread not found" });
@@ -644,7 +871,92 @@ export class MetadataServer {
644
871
  send(res, 200, { thread, messages: readMessages(threadId) });
645
872
  return;
646
873
  }
874
+ if (req.method === "GET" && url.pathname.startsWith("/tasks/")) {
875
+ let taskId;
876
+ try {
877
+ taskId = decodeURIComponent(url.pathname.slice("/tasks/".length));
878
+ }
879
+ catch {
880
+ send(res, 400, { ok: false, error: "invalid taskId" });
881
+ return;
882
+ }
883
+ const task = readTask(taskId);
884
+ if (!task) {
885
+ send(res, 404, { ok: false, error: "task not found" });
886
+ return;
887
+ }
888
+ const thread = task.threadId ? readThread(task.threadId) : undefined;
889
+ const messages = task.threadId ? readMessages(task.threadId) : [];
890
+ send(res, 200, { ok: true, task, thread, messages });
891
+ return;
892
+ }
647
893
  try {
894
+ if (req.method === "GET" && url.pathname.startsWith("/plans/")) {
895
+ let raw;
896
+ try {
897
+ raw = decodeURIComponent(url.pathname.slice("/plans/".length));
898
+ }
899
+ catch {
900
+ send(res, 400, { ok: false, error: "invalid sessionId" });
901
+ return;
902
+ }
903
+ const validation = validateSessionId(raw);
904
+ if (!validation.ok) {
905
+ send(res, 400, { ok: false, error: "invalid sessionId" });
906
+ return;
907
+ }
908
+ const sessionId = validation.value;
909
+ const planPath = join(getPlansDir(), `${sessionId}.md`);
910
+ try {
911
+ const content = readFileSync(planPath, "utf8");
912
+ send(res, 200, { ok: true, sessionId, content });
913
+ }
914
+ catch (err) {
915
+ if (err?.code === "ENOENT") {
916
+ send(res, 404, { ok: false, error: "Plan not found" });
917
+ return;
918
+ }
919
+ send(res, 500, { ok: false, error: "Failed to read plan" });
920
+ return;
921
+ }
922
+ return;
923
+ }
924
+ if (req.method === "PUT" && url.pathname.startsWith("/plans/")) {
925
+ let raw;
926
+ try {
927
+ raw = decodeURIComponent(url.pathname.slice("/plans/".length));
928
+ }
929
+ catch {
930
+ send(res, 400, { ok: false, error: "invalid sessionId" });
931
+ return;
932
+ }
933
+ const validation = validateSessionId(raw);
934
+ if (!validation.ok) {
935
+ send(res, 400, { ok: false, error: "invalid sessionId" });
936
+ return;
937
+ }
938
+ const sessionId = validation.value;
939
+ const body = (await readJson(req));
940
+ if (typeof body.content !== "string") {
941
+ send(res, 400, { ok: false, error: "content must be a string" });
942
+ return;
943
+ }
944
+ const planPath = join(getPlansDir(), `${sessionId}.md`);
945
+ const dir = dirname(planPath);
946
+ const tmpPath = join(dir, `.${sessionId}.${randomUUID()}.tmp`);
947
+ try {
948
+ mkdirSync(dir, { recursive: true });
949
+ writeFileSync(tmpPath, body.content, "utf8");
950
+ renameSync(tmpPath, planPath);
951
+ }
952
+ catch {
953
+ rmSync(tmpPath, { force: true });
954
+ send(res, 500, { ok: false, error: "Failed to write plan" });
955
+ return;
956
+ }
957
+ send(res, 200, { ok: true, sessionId });
958
+ return;
959
+ }
648
960
  if (req.method === "POST" && url.pathname === "/set-status") {
649
961
  const body = (await readJson(req));
650
962
  updateSessionMetadata(body.session, (current) => ({
@@ -728,7 +1040,7 @@ export class MetadataServer {
728
1040
  const currentClientSession = body.currentClientSession?.trim() || url.searchParams.get("currentClientSession")?.trim() || undefined;
729
1041
  const clientTty = body.clientTty?.trim() || url.searchParams.get("clientTty")?.trim() || undefined;
730
1042
  const desktop = this.options.desktop.getState();
731
- const session = (desktop.sessions ?? []).find((entry) => entry.id === sessionId);
1043
+ const session = [...(desktop.sessions ?? []), ...(desktop.teammates ?? [])].find((entry) => entry.id === sessionId);
732
1044
  const service = (desktop.services ?? []).find((entry) => entry.id === sessionId);
733
1045
  const tmux = new TmuxRuntimeManager();
734
1046
  const openWindowId = (windowId, itemId) => {
@@ -989,7 +1301,7 @@ export class MetadataServer {
989
1301
  this.emitAlert({
990
1302
  kind: "notification",
991
1303
  sessionId: body.session,
992
- title: sessionAlertTitle("notification", body.session, body.event.source),
1304
+ title: body.event.source ?? "notification",
993
1305
  message: body.event.message || "Agent notification.",
994
1306
  dedupeKey: body.event.message ? `notify:${body.session}:${body.event.message}` : undefined,
995
1307
  cooldownMs: 15_000,
@@ -1092,7 +1404,7 @@ export class MetadataServer {
1092
1404
  this.emitAlert({
1093
1405
  kind,
1094
1406
  sessionId,
1095
- title: sessionAlertTitle(kind, sessionId, body.title),
1407
+ title: body.title ?? "",
1096
1408
  message: [body.subtitle?.trim(), body.message?.trim() || body.title?.trim() || "aimux"]
1097
1409
  .filter(Boolean)
1098
1410
  .join(" — "),
@@ -1132,12 +1444,23 @@ export class MetadataServer {
1132
1444
  send(res, 200, { ok: true, cleared });
1133
1445
  return;
1134
1446
  }
1447
+ if (req.method === "POST" && (url.pathname === "/inbox/read" || url.pathname === "/inbox/clear")) {
1448
+ const body = (await readJson(req));
1449
+ const updated = markRuntimeInboxRead({
1450
+ id: body.id?.trim() || undefined,
1451
+ participantId: body.participant?.trim() || body.sessionId?.trim() || undefined,
1452
+ includeNotifications: body.includeNotifications === true,
1453
+ });
1454
+ send(res, 200, { ok: true, updated });
1455
+ return;
1456
+ }
1135
1457
  if (req.method === "POST" && url.pathname === "/shell-state") {
1136
1458
  const body = (await readJson(req));
1137
1459
  const result = applyShellStateTransition({
1138
1460
  state: body.state,
1139
1461
  sessionId: body.sessionId,
1140
1462
  tool: body.tool,
1463
+ command: body.command,
1141
1464
  tracker: this.tracker,
1142
1465
  emitAlert: (input) => this.emitAlert(input),
1143
1466
  });
@@ -1209,7 +1532,12 @@ export class MetadataServer {
1209
1532
  }
1210
1533
  if (req.method === "POST" && url.pathname === "/threads/mark-seen") {
1211
1534
  const body = (await readJson(req));
1212
- const thread = markThreadSeen(body.threadId, body.session);
1535
+ const sessionId = (body.session ?? body.sessionId ?? "").trim();
1536
+ if (!sessionId) {
1537
+ send(res, 400, { ok: false, error: "session is required" });
1538
+ return;
1539
+ }
1540
+ const thread = markThreadSeen(body.threadId, sessionId);
1213
1541
  if (!thread) {
1214
1542
  send(res, 404, { ok: false, error: "thread not found" });
1215
1543
  return;
@@ -1238,7 +1566,9 @@ export class MetadataServer {
1238
1566
  ? this.options.actions.sendHandoff(body)
1239
1567
  : sendHandoff({
1240
1568
  from: body.from?.trim() || "user",
1241
- to: body.to ?? [],
1569
+ to: body.to?.length
1570
+ ? body.to
1571
+ : [body.assignee, body.tool].map(optionalString).filter((value) => Boolean(value)),
1242
1572
  body: body.body,
1243
1573
  title: body.title,
1244
1574
  worktreePath: body.worktreePath,
@@ -1287,7 +1617,7 @@ export class MetadataServer {
1287
1617
  const body = (await readJson(req));
1288
1618
  const result = await assignTask({
1289
1619
  from: body.from?.trim() || "user",
1290
- to: body.to?.trim(),
1620
+ to: optionalStringOrFirst(body.to),
1291
1621
  assignee: body.assignee?.trim(),
1292
1622
  tool: body.tool?.trim(),
1293
1623
  description: body.description,
@@ -1373,6 +1703,207 @@ export class MetadataServer {
1373
1703
  send(res, 200, { ok: true, ...result });
1374
1704
  return;
1375
1705
  }
1706
+ if (req.method === "GET" && url.pathname === "/agents/teammates") {
1707
+ const parentSessionId = url.searchParams.get("parentSessionId")?.trim() ?? "";
1708
+ const result = this.resolveDirectTeammates(parentSessionId);
1709
+ if (!result.ok) {
1710
+ send(res, result.status, { ok: false, error: result.error });
1711
+ return;
1712
+ }
1713
+ send(res, 200, {
1714
+ ok: true,
1715
+ parentSessionId: result.parent.id,
1716
+ teammates: result.teammates.map(teammateApiRecord),
1717
+ });
1718
+ return;
1719
+ }
1720
+ if (req.method === "POST" && url.pathname === "/agents/teammates/create") {
1721
+ const body = (await readJson(req));
1722
+ const parentSessionId = body.parentSessionId?.trim() ?? "";
1723
+ if (!parentSessionId) {
1724
+ send(res, 400, { ok: false, error: "parentSessionId is required" });
1725
+ return;
1726
+ }
1727
+ body.parentSessionId = parentSessionId;
1728
+ if (this.options.desktop?.getState) {
1729
+ const resolved = this.resolveDirectTeammates(parentSessionId);
1730
+ if (!resolved.ok) {
1731
+ send(res, resolved.status, { ok: false, error: resolved.error });
1732
+ return;
1733
+ }
1734
+ }
1735
+ const initialTaskPrompt = body.initialTask ? teammateTaskPrompt(body.initialTask) : undefined;
1736
+ if (body.initialTask && !initialTaskPrompt) {
1737
+ send(res, 400, { ok: false, error: "initialTask requires body or prompt" });
1738
+ return;
1739
+ }
1740
+ if (!this.options.lifecycle?.createTeammateAgent) {
1741
+ send(res, 501, { ok: false, error: "teammate creation not supported by this service" });
1742
+ return;
1743
+ }
1744
+ const result = await this.options.lifecycle.createTeammateAgent({
1745
+ parentSessionId: body.parentSessionId,
1746
+ role: body.role,
1747
+ label: body.label,
1748
+ tool: body.tool,
1749
+ sessionId: body.sessionId,
1750
+ worktreePath: body.worktreePath,
1751
+ open: body.open,
1752
+ extraArgs: body.extraArgs,
1753
+ order: body.order,
1754
+ });
1755
+ const taskResult = body.initialTask && initialTaskPrompt
1756
+ ? await assignTask({
1757
+ from: result.parentSessionId,
1758
+ to: result.sessionId,
1759
+ description: teammateTaskDescription(body.initialTask),
1760
+ prompt: initialTaskPrompt,
1761
+ worktreePath: optionalString(body.initialTask.worktreePath) ?? optionalString(body.worktreePath),
1762
+ })
1763
+ : undefined;
1764
+ if (taskResult) {
1765
+ this.emitAssignedTaskAlert(taskResult);
1766
+ }
1767
+ this.options.onChange?.();
1768
+ send(res, 200, { ok: true, ...result, task: taskResult?.task, thread: taskResult?.thread });
1769
+ return;
1770
+ }
1771
+ if (req.method === "POST" && url.pathname === "/agents/teammates/tasks") {
1772
+ const body = (await readJson(req));
1773
+ const parentSessionId = body.parentSessionId?.trim() ?? "";
1774
+ const teammateSessionId = body.teammateSessionId?.trim() ?? "";
1775
+ if (!teammateSessionId) {
1776
+ send(res, 400, { ok: false, error: "teammateSessionId is required" });
1777
+ return;
1778
+ }
1779
+ const resolved = this.resolveDirectTeammates(parentSessionId);
1780
+ if (!resolved.ok) {
1781
+ send(res, resolved.status, { ok: false, error: resolved.error });
1782
+ return;
1783
+ }
1784
+ const teammate = resolved.teammates.find((session) => session.id === teammateSessionId);
1785
+ if (!teammate) {
1786
+ send(res, 404, {
1787
+ ok: false,
1788
+ error: `teammate "${teammateSessionId}" is not attached to parent "${parentSessionId}"`,
1789
+ });
1790
+ return;
1791
+ }
1792
+ const prompt = teammateTaskPrompt(body);
1793
+ if (!prompt) {
1794
+ send(res, 400, { ok: false, error: "teammate task requires body or prompt" });
1795
+ return;
1796
+ }
1797
+ const result = await assignTask({
1798
+ from: resolved.parent.id,
1799
+ to: teammate.id,
1800
+ description: teammateTaskDescription(body),
1801
+ prompt,
1802
+ worktreePath: optionalString(body.worktreePath) ??
1803
+ optionalString(teammate.worktreePath) ??
1804
+ optionalString(resolved.parent.worktreePath),
1805
+ });
1806
+ this.emitAssignedTaskAlert(result);
1807
+ this.options.onChange?.();
1808
+ send(res, 200, { ok: true, parentSessionId: resolved.parent.id, teammateSessionId: teammate.id, ...result });
1809
+ return;
1810
+ }
1811
+ if (req.method === "POST" && url.pathname === "/agents/teammates/send") {
1812
+ send(res, 410, {
1813
+ ok: false,
1814
+ error: "raw teammate send has been removed; create durable teammate work with /agents/teammates/tasks",
1815
+ });
1816
+ return;
1817
+ }
1818
+ if (req.method === "POST" && url.pathname === "/agents/teammates/stop") {
1819
+ const body = (await readJson(req));
1820
+ const resolved = this.resolveDirectTeammate(body.parentSessionId?.trim() ?? "", body.teammateSessionId?.trim() ?? "");
1821
+ if (!resolved.ok) {
1822
+ send(res, resolved.status, { ok: false, error: resolved.error });
1823
+ return;
1824
+ }
1825
+ if (!this.options.lifecycle?.stopAgent) {
1826
+ send(res, 501, { ok: false, error: "agent stop not supported by this service" });
1827
+ return;
1828
+ }
1829
+ const result = await this.options.lifecycle.stopAgent({ sessionId: resolved.teammate.id });
1830
+ this.options.onChange?.();
1831
+ send(res, 200, {
1832
+ ok: true,
1833
+ parentSessionId: resolved.parent.id,
1834
+ teammateSessionId: resolved.teammate.id,
1835
+ ...result,
1836
+ });
1837
+ return;
1838
+ }
1839
+ if (req.method === "POST" && url.pathname === "/agents/teammates/resume") {
1840
+ const body = (await readJson(req));
1841
+ const resolved = this.resolveDirectTeammate(body.parentSessionId?.trim() ?? "", body.teammateSessionId?.trim() ?? "");
1842
+ if (!resolved.ok) {
1843
+ send(res, resolved.status, { ok: false, error: resolved.error });
1844
+ return;
1845
+ }
1846
+ if (!this.options.desktop?.resumeAgent) {
1847
+ send(res, 501, { ok: false, error: "agent resume not supported by this service" });
1848
+ return;
1849
+ }
1850
+ const result = await this.options.desktop.resumeAgent({
1851
+ sessionId: resolved.teammate.id,
1852
+ session: resolved.teammate,
1853
+ });
1854
+ this.options.onChange?.();
1855
+ send(res, 200, {
1856
+ ok: true,
1857
+ parentSessionId: resolved.parent.id,
1858
+ teammateSessionId: resolved.teammate.id,
1859
+ ...result,
1860
+ });
1861
+ return;
1862
+ }
1863
+ if (req.method === "POST" && url.pathname === "/agents/teammates/kill") {
1864
+ const body = (await readJson(req));
1865
+ const resolved = this.resolveDirectTeammate(body.parentSessionId?.trim() ?? "", body.teammateSessionId?.trim() ?? "");
1866
+ if (!resolved.ok) {
1867
+ send(res, resolved.status, { ok: false, error: resolved.error });
1868
+ return;
1869
+ }
1870
+ if (!this.options.lifecycle?.killAgent) {
1871
+ send(res, 501, { ok: false, error: "agent kill not supported by this service" });
1872
+ return;
1873
+ }
1874
+ const result = await this.options.lifecycle.killAgent({
1875
+ sessionId: resolved.teammate.id,
1876
+ });
1877
+ this.options.onChange?.();
1878
+ send(res, 200, {
1879
+ ok: true,
1880
+ parentSessionId: resolved.parent.id,
1881
+ teammateSessionId: resolved.teammate.id,
1882
+ ...result,
1883
+ });
1884
+ return;
1885
+ }
1886
+ if (req.method === "POST" && url.pathname === "/agents/teammates/resurrect") {
1887
+ const body = (await readJson(req));
1888
+ const resolved = this.resolveDirectGraveyardTeammate(body.parentSessionId?.trim() ?? "", body.teammateSessionId?.trim() ?? "");
1889
+ if (!resolved.ok) {
1890
+ send(res, resolved.status, { ok: false, error: resolved.error });
1891
+ return;
1892
+ }
1893
+ if (!this.options.desktop?.resurrectGraveyard) {
1894
+ send(res, 501, { ok: false, error: "agent graveyard resurrection not supported by this service" });
1895
+ return;
1896
+ }
1897
+ const result = await this.options.desktop.resurrectGraveyard({ sessionId: resolved.teammate.id });
1898
+ this.options.onChange?.();
1899
+ send(res, 200, {
1900
+ ok: true,
1901
+ parentSessionId: resolved.parent.id,
1902
+ teammateSessionId: resolved.teammate.id,
1903
+ ...result,
1904
+ });
1905
+ return;
1906
+ }
1376
1907
  if (req.method === "POST" && url.pathname === "/agents/fork") {
1377
1908
  const body = (await readJson(req));
1378
1909
  if (!this.options.lifecycle?.forkAgent) {
@@ -1401,7 +1932,7 @@ export class MetadataServer {
1401
1932
  send(res, 501, { ok: false, error: "agent resume not supported by this service" });
1402
1933
  return;
1403
1934
  }
1404
- const result = await this.options.desktop.resumeAgent(body);
1935
+ const result = await this.options.desktop.resumeAgent({ sessionId: body.sessionId });
1405
1936
  this.options.onChange?.();
1406
1937
  send(res, 200, { ok: true, ...result });
1407
1938
  return;
@@ -1424,10 +1955,6 @@ export class MetadataServer {
1424
1955
  return;
1425
1956
  }
1426
1957
  const result = await this.options.lifecycle.renameAgent(body);
1427
- updateSessionMetadata(body.sessionId, (current) => ({
1428
- ...current,
1429
- label: body.label?.trim() || undefined,
1430
- }));
1431
1958
  this.options.onChange?.();
1432
1959
  send(res, 200, { ok: true, ...result });
1433
1960
  return;
@@ -1449,45 +1976,30 @@ export class MetadataServer {
1449
1976
  send(res, 501, { ok: false, error: "agent kill not supported by this service" });
1450
1977
  return;
1451
1978
  }
1452
- const result = await this.options.lifecycle.killAgent(body);
1979
+ const result = await this.options.lifecycle.killAgent({ sessionId: body.sessionId });
1453
1980
  this.options.onChange?.();
1454
1981
  send(res, 200, { ok: true, ...result });
1455
1982
  return;
1456
1983
  }
1457
1984
  if (req.method === "POST" && url.pathname === "/agents/input") {
1458
1985
  const body = (await readJson(req));
1459
- if (!this.options.lifecycle?.writeAgentInput) {
1460
- send(res, 501, { ok: false, error: "agent input not supported by this service" });
1986
+ const sessionId = body.sessionId?.trim() ?? "";
1987
+ if (!sessionId) {
1988
+ send(res, 400, { ok: false, error: "sessionId is required" });
1461
1989
  return;
1462
1990
  }
1463
- const result = await this.options.lifecycle.writeAgentInput(body);
1464
- if (this.options.lifecycle.readAgentHistory) {
1465
- try {
1466
- const history = await this.options.lifecycle.readAgentHistory({ sessionId: body.sessionId, lastN: 20 });
1467
- this.eventBus.publishHistoryUpdate({
1468
- sessionId: history.sessionId,
1469
- messages: history.messages,
1470
- lastN: history.lastN,
1471
- });
1472
- }
1473
- catch {
1474
- // History update is best-effort; the write result should still succeed.
1475
- }
1991
+ const text = typeof body.text === "string" ? body.text : "";
1992
+ if (!text.trim()) {
1993
+ send(res, 400, { ok: false, error: "text is required" });
1994
+ return;
1995
+ }
1996
+ if (!this.options.lifecycle?.sendAgentInput) {
1997
+ send(res, 501, { ok: false, error: "agent input not supported by this service" });
1998
+ return;
1476
1999
  }
2000
+ const result = await this.options.lifecycle.sendAgentInput({ sessionId, text });
1477
2001
  this.options.onChange?.();
1478
- send(res, 200, { ok: result.accepted, ...result });
1479
- return;
1480
- }
1481
- if (req.method === "POST" && url.pathname === "/attachments") {
1482
- const body = (await readJson(req));
1483
- const attachment = body.path?.trim()
1484
- ? ingestAttachmentFromPath(body.path)
1485
- : ingestAttachmentFromBase64({
1486
- filename: body.filename,
1487
- mimeType: body.mimeType,
1488
- contentBase64: String(body.contentBase64 ?? ""),
1489
- });
1490
- send(res, 200, { ok: true, attachment });
2002
+ send(res, 200, { ok: true, ...result });
1491
2003
  return;
1492
2004
  }
1493
2005
  const attachmentContentMatch = url.pathname.match(/^\/attachments\/([^/]+)\/content$/);
@@ -1531,23 +2043,7 @@ export class MetadataServer {
1531
2043
  return;
1532
2044
  }
1533
2045
  if (req.method === "GET" && url.pathname === "/agents/history") {
1534
- const sessionId = url.searchParams.get("sessionId")?.trim();
1535
- const lastNRaw = url.searchParams.get("lastN");
1536
- if (!sessionId) {
1537
- send(res, 400, { ok: false, error: "sessionId is required" });
1538
- return;
1539
- }
1540
- if (!this.options.lifecycle?.readAgentHistory) {
1541
- send(res, 501, { ok: false, error: "agent history not supported by this service" });
1542
- return;
1543
- }
1544
- const lastN = lastNRaw === null || lastNRaw.trim() === "" ? undefined : Number.parseInt(lastNRaw, 10);
1545
- if (lastNRaw !== null && Number.isNaN(lastN)) {
1546
- send(res, 400, { ok: false, error: "lastN must be an integer" });
1547
- return;
1548
- }
1549
- const result = await this.options.lifecycle.readAgentHistory({ sessionId, lastN });
1550
- send(res, 200, { ok: true, ...result });
2046
+ send(res, 410, { ok: false, error: "agent message history requires the runtime core replacement" });
1551
2047
  return;
1552
2048
  }
1553
2049
  if (req.method === "POST" && url.pathname === "/worktrees/create") {
@@ -1663,11 +2159,16 @@ export class MetadataServer {
1663
2159
  }
1664
2160
  if (req.method === "POST" && url.pathname === "/graveyard/resurrect") {
1665
2161
  const body = (await readJson(req));
2162
+ const sessionId = (body.sessionId ?? body.id ?? "").trim();
2163
+ if (!sessionId) {
2164
+ send(res, 400, { ok: false, error: "sessionId is required" });
2165
+ return;
2166
+ }
1666
2167
  if (!this.options.desktop?.resurrectGraveyard) {
1667
- send(res, 501, { ok: false, error: "graveyard resurrect not supported by this service" });
2168
+ send(res, 501, { ok: false, error: "agent graveyard resurrection not supported by this service" });
1668
2169
  return;
1669
2170
  }
1670
- const result = await this.options.desktop.resurrectGraveyard(body);
2171
+ const result = await this.options.desktop.resurrectGraveyard({ sessionId });
1671
2172
  this.options.onChange?.();
1672
2173
  send(res, 200, { ok: true, ...result });
1673
2174
  return;
@@ -1675,7 +2176,7 @@ export class MetadataServer {
1675
2176
  if (req.method === "POST" && url.pathname === "/graveyard/worktrees/resurrect") {
1676
2177
  const body = (await readJson(req));
1677
2178
  if (!this.options.desktop?.resurrectGraveyardWorktree) {
1678
- send(res, 501, { ok: false, error: "worktree graveyard resurrect not supported by this service" });
2179
+ send(res, 501, { ok: false, error: "worktree graveyard resurrection not supported by this service" });
1679
2180
  return;
1680
2181
  }
1681
2182
  const result = await this.options.desktop.resurrectGraveyardWorktree(body);