@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -1,140 +0,0 @@
1
- /**
2
- * Self-contained HTML page for `agents serve`. No framework, no external assets
3
- * — a single inline <style> + <script> that subscribes to /events (SSE) and
4
- * re-renders the panels on each snapshot. Terminal-coded per the agents-cli
5
- * brand: #0a0a0a bg, #a3e635 lime accent, JetBrains Mono.
6
- */
7
- export function renderPage() {
8
- return `<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="utf-8" />
12
- <meta name="viewport" content="width=device-width, initial-scale=1" />
13
- <title>agents serve</title>
14
- <style>
15
- :root {
16
- --bg: #0a0a0a; --panel: #121212; --border: #262626; --fg: #e5e5e5;
17
- --dim: #737373; --accent: #a3e635; --add: #4ade80; --del: #f87171; --hunk: #60a5fa;
18
- }
19
- * { box-sizing: border-box; }
20
- body {
21
- margin: 0; background: var(--bg); color: var(--fg);
22
- font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
23
- font-size: 13px; line-height: 1.5;
24
- }
25
- header {
26
- position: sticky; top: 0; z-index: 10; background: var(--bg);
27
- border-bottom: 1px solid var(--border); padding: 12px 20px;
28
- display: flex; align-items: center; gap: 12px;
29
- }
30
- header .mark { color: var(--accent); font-weight: 700; letter-spacing: .5px; }
31
- header .status { color: var(--dim); }
32
- header .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--del); margin-right: 6px; vertical-align: middle; }
33
- header .dot.live { background: var(--accent); }
34
- main { padding: 20px; max-width: 1100px; margin: 0 auto; display: grid; gap: 24px; }
35
- h2 { font-size: 14px; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 0 0 12px; text-transform: uppercase; letter-spacing: 1px; }
36
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 14px; margin-bottom: 12px; }
37
- .row { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
38
- .muted { color: var(--dim); }
39
- .badge { font-size: 11px; padding: 1px 7px; border-radius: 10px; border: 1px solid var(--border); }
40
- .badge.running { color: var(--accent); border-color: var(--accent); }
41
- .badge.completed { color: var(--add); border-color: var(--add); }
42
- .badge.failed { color: var(--del); border-color: var(--del); }
43
- a { color: var(--hunk); text-decoration: none; }
44
- a:hover { text-decoration: underline; }
45
- details { margin-top: 8px; }
46
- summary { cursor: pointer; color: var(--dim); }
47
- pre.diff { overflow-x: auto; background: #0d0d0d; border: 1px solid var(--border); border-radius: 4px; padding: 10px; margin: 8px 0 0; white-space: pre; }
48
- pre.diff .a { color: var(--add); } pre.diff .d { color: var(--del); } pre.diff .h { color: var(--hunk); } pre.diff .m { color: var(--dim); }
49
- .err { color: var(--del); }
50
- .empty { color: var(--dim); font-style: italic; }
51
- table { width: 100%; border-collapse: collapse; }
52
- td, th { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
53
- th { color: var(--dim); font-weight: 500; }
54
- </style>
55
- </head>
56
- <body>
57
- <header>
58
- <span class="mark">agents serve</span>
59
- <span class="status"><span class="dot" id="dot"></span><span id="conn">connecting…</span></span>
60
- <span class="status" id="ts"></span>
61
- </header>
62
- <main>
63
- <section><h2>Teams &amp; Diffs</h2><div id="teams"></div></section>
64
- <section><h2>Routines</h2><div id="routines"></div></section>
65
- <section><h2>Cloud</h2><div id="cloud"></div></section>
66
- </main>
67
- <script>
68
- const $ = (id) => document.getElementById(id);
69
- const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
70
- // href allowlist: only http(s) URLs are linkable. esc() blocks attribute
71
- // breakout, but a javascript:/data: URL would still execute on click — so a
72
- // non-http(s) pr_url renders as an inert '#' rather than a live link.
73
- const safeUrl = (u) => { const s = String(u == null ? '' : u); return /^https?:\\/\\//i.test(s) ? s : '#'; };
74
- function colorDiff(text) {
75
- return esc(text).split('\\n').map((line) => {
76
- if (line.startsWith('+')) return '<span class="a">' + line + '</span>';
77
- if (line.startsWith('-')) return '<span class="d">' + line + '</span>';
78
- if (line.startsWith('@@')) return '<span class="h">' + line + '</span>';
79
- if (line.startsWith('diff ') || line.startsWith('index ')) return '<span class="m">' + line + '</span>';
80
- return line;
81
- }).join('\\n');
82
- }
83
- function panelError(el, error) { el.innerHTML = '<div class="card err">error: ' + esc(error) + '</div>'; }
84
-
85
- function renderTeams(res) {
86
- const el = $('teams');
87
- if (!res.ok) return panelError(el, res.error);
88
- if (!res.data.length) { el.innerHTML = '<div class="empty">no teams</div>'; return; }
89
- el.innerHTML = res.data.map((t) => {
90
- const agents = t.agents.map((a) =>
91
- '<div class="row"><span>' + esc(a.name || a.agent_id.slice(0,8)) +
92
- ' <span class="muted">' + esc(a.agent_type) + '</span>' +
93
- (a.pr_url ? ' <a href="' + esc(safeUrl(a.pr_url)) + '" target="_blank">PR</a>' : '') +
94
- '</span><span class="badge ' + esc(a.status) + '">' + esc(a.status) + '</span></div>'
95
- ).join('');
96
- const worktrees = t.worktrees.map((w) => {
97
- const body = w.diff
98
- ? '<details><summary>diff · ' + esc(w.worktree_name || w.worktree_path) + '</summary><pre class="diff">' + colorDiff(w.diff) + '</pre></details>'
99
- : '<div class="muted">' + esc(w.worktree_name || w.worktree_path) + ' — no uncommitted changes</div>';
100
- return body;
101
- }).join('');
102
- return '<div class="card"><div class="row"><strong>' + esc(t.task_name) + '</strong>' +
103
- '<span class="muted">' + t.running + ' running · ' + t.completed + ' done · ' + t.failed + ' failed</span></div>' +
104
- agents + worktrees + '</div>';
105
- }).join('');
106
- }
107
- function renderRoutines(res) {
108
- const el = $('routines');
109
- if (!res.ok) return panelError(el, res.error);
110
- if (!res.data.length) { el.innerHTML = '<div class="empty">no routines</div>'; return; }
111
- el.innerHTML = '<div class="card"><table><tr><th>name</th><th>schedule</th><th>agent</th><th>enabled</th></tr>' +
112
- res.data.map((j) => '<tr><td>' + esc(j.name) + '</td><td class="muted">' + esc(j.schedule) + '</td><td>' +
113
- esc(j.workflow ? 'wf:' + j.workflow : j.agent) + '</td><td>' + (j.enabled ? 'yes' : '<span class="muted">no</span>') + '</td></tr>').join('') +
114
- '</table></div>';
115
- }
116
- function renderCloud(res) {
117
- const el = $('cloud');
118
- if (!res.ok) return panelError(el, res.error);
119
- if (!res.data.length) { el.innerHTML = '<div class="empty">no cloud tasks</div>'; return; }
120
- el.innerHTML = '<div class="card"><table><tr><th>id</th><th>provider</th><th>status</th><th>repo</th><th></th></tr>' +
121
- res.data.map((c) => '<tr><td>' + esc(c.id.slice(0,10)) + '</td><td>' + esc(c.provider) + '</td><td><span class="badge ' + esc(c.status) + '">' +
122
- esc(c.status) + '</span></td><td class="muted">' + esc(c.repo || '') + '</td><td>' +
123
- (c.prUrl ? '<a href="' + esc(safeUrl(c.prUrl)) + '" target="_blank">PR</a>' : '') + '</td></tr>').join('') +
124
- '</table></div>';
125
- }
126
- function render(state) {
127
- renderTeams(state.teams); renderRoutines(state.routines); renderCloud(state.cloud);
128
- $('ts').textContent = 'updated ' + new Date(state.generated_at).toLocaleTimeString();
129
- }
130
- function setConn(live) {
131
- $('dot').className = 'dot' + (live ? ' live' : '');
132
- $('conn').textContent = live ? 'live' : 'reconnecting…';
133
- }
134
- const src = new EventSource('/events');
135
- src.addEventListener('state', (e) => { setConn(true); render(JSON.parse(e.data)); });
136
- src.onerror = () => setConn(false);
137
- </script>
138
- </body>
139
- </html>`;
140
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Read-only localhost HTTP + Server-Sent-Events server for `agents serve`.
3
- *
4
- * There is deliberately NO framework and NO mutation surface:
5
- * - binds 127.0.0.1 only (loopback) — never reachable off the machine;
6
- * - answers GET only (every other method → 405);
7
- * - routes: `GET /` → HTML page, `GET /api/state` → JSON snapshot,
8
- * `GET /events` → SSE stream that re-pushes the snapshot on an interval.
9
- *
10
- * The whole thing is a viewer over data other commands already own
11
- * ({@link assembleState}); it writes nothing.
12
- *
13
- * The GET routing is exported as {@link handleServeGet} so the authenticated
14
- * `--control` variant (see control.ts) can reuse it verbatim rather than
15
- * duplicate the state/SSE surface.
16
- */
17
- import type http from 'http';
18
- import type { ServeState } from './data.js';
19
- /** Loopback address — the server binds here and nowhere else. */
20
- export declare const SERVE_HOST = "127.0.0.1";
21
- /**
22
- * DNS-rebinding guard. The socket binds loopback, but binding alone does not
23
- * stop a remote page: an attacker can point a hostname's DNS at 127.0.0.1 and
24
- * drive the victim's browser to `GET /api/state`, which exposes uncommitted
25
- * `git diff HEAD` of every worktree plus routine + cloud config. A browser
26
- * always sends a `Host` header naming the site it *thinks* it reached, so we
27
- * trust only a Host that is itself loopback. A missing Host (HTTP/1.0 or a raw
28
- * non-browser client like curl-without-Host) is not a rebind vector and is
29
- * allowed; any explicit non-loopback Host is rejected.
30
- */
31
- export declare function isAllowedServeHost(hostHeader: string | undefined): boolean;
32
- /** Default port for `agents serve`. */
33
- export declare const DEFAULT_SERVE_PORT = 4477;
34
- /** Default SSE push cadence. */
35
- export declare const DEFAULT_INTERVAL_MS = 3000;
36
- export interface ServeOptions {
37
- /** Project root for project-scoped routine discovery. Defaults to process.cwd(). */
38
- cwd?: string;
39
- /** SSE push cadence in ms. Defaults to {@link DEFAULT_INTERVAL_MS}. */
40
- intervalMs?: number;
41
- /**
42
- * Snapshot data source. Defaults to {@link assembleState}. Read-only DI seam
43
- * so tests can drive SSE lifecycle timing deterministically (e.g. gate the
44
- * first snapshot to reproduce a mid-snapshot disconnect); production never
45
- * sets it.
46
- */
47
- snapshot?: () => Promise<ServeState>;
48
- }
49
- /**
50
- * Resolved GET-route context: the snapshot source and SSE cadence, with all
51
- * defaults applied. Built once by {@link resolveServeContext}.
52
- */
53
- export interface ServeContext {
54
- snapshot: () => Promise<ServeState>;
55
- intervalMs: number;
56
- }
57
- /** Apply defaults to {@link ServeOptions}, producing a {@link ServeContext}. */
58
- export declare function resolveServeContext(opts?: ServeOptions): ServeContext;
59
- /**
60
- * Handle the shared read-only GET surface (`/`, `/api/state`, `/events`).
61
- * Returns `true` when the route was recognized and a response was written (or
62
- * begun, for SSE); `false` when the path is unknown so the caller can 404 or
63
- * try its own routes first. Extracted so the `--control` server reuses the
64
- * exact same state/SSE behavior — including the load-bearing SSE close/timer
65
- * ordering below — without duplicating it.
66
- */
67
- export declare function handleServeGet(url: string, req: http.IncomingMessage, res: http.ServerResponse, ctx: ServeContext): Promise<boolean>;
68
- /**
69
- * Create (but do not start) the read-only serve server. Caller invokes
70
- * `.listen(port, SERVE_HOST)`. Returned so tests can bind an ephemeral port.
71
- */
72
- export declare function createServeServer(opts?: ServeOptions): http.Server;
73
- /**
74
- * Start the serve server on `port`, bound to loopback. Resolves with the
75
- * actual bound port (useful when `port === 0` for tests).
76
- */
77
- export declare function startServeServer(port?: number, opts?: ServeOptions): Promise<{
78
- server: http.Server;
79
- port: number;
80
- }>;
@@ -1,145 +0,0 @@
1
- import { createServer } from 'http';
2
- import { assembleState } from './data.js';
3
- import { renderPage } from './page.js';
4
- /** Loopback address — the server binds here and nowhere else. */
5
- export const SERVE_HOST = '127.0.0.1';
6
- /**
7
- * DNS-rebinding guard. The socket binds loopback, but binding alone does not
8
- * stop a remote page: an attacker can point a hostname's DNS at 127.0.0.1 and
9
- * drive the victim's browser to `GET /api/state`, which exposes uncommitted
10
- * `git diff HEAD` of every worktree plus routine + cloud config. A browser
11
- * always sends a `Host` header naming the site it *thinks* it reached, so we
12
- * trust only a Host that is itself loopback. A missing Host (HTTP/1.0 or a raw
13
- * non-browser client like curl-without-Host) is not a rebind vector and is
14
- * allowed; any explicit non-loopback Host is rejected.
15
- */
16
- export function isAllowedServeHost(hostHeader) {
17
- if (!hostHeader)
18
- return true;
19
- const host = hostHeader.replace(/:\d+$/, '').toLowerCase().replace(/^\[|\]$/g, '');
20
- return host === 'localhost' || host === '127.0.0.1' || host === '::1';
21
- }
22
- /** Default port for `agents serve`. */
23
- export const DEFAULT_SERVE_PORT = 4477;
24
- /** Default SSE push cadence. */
25
- export const DEFAULT_INTERVAL_MS = 3000;
26
- /** Apply defaults to {@link ServeOptions}, producing a {@link ServeContext}. */
27
- export function resolveServeContext(opts = {}) {
28
- const cwd = opts.cwd ?? process.cwd();
29
- return {
30
- snapshot: opts.snapshot ?? (() => assembleState(cwd)),
31
- intervalMs: opts.intervalMs ?? DEFAULT_INTERVAL_MS,
32
- };
33
- }
34
- /**
35
- * Handle the shared read-only GET surface (`/`, `/api/state`, `/events`).
36
- * Returns `true` when the route was recognized and a response was written (or
37
- * begun, for SSE); `false` when the path is unknown so the caller can 404 or
38
- * try its own routes first. Extracted so the `--control` server reuses the
39
- * exact same state/SSE behavior — including the load-bearing SSE close/timer
40
- * ordering below — without duplicating it.
41
- */
42
- export async function handleServeGet(url, req, res, ctx) {
43
- if (url === '/' || url === '/index.html') {
44
- const html = renderPage();
45
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
46
- res.end(html);
47
- return true;
48
- }
49
- if (url === '/api/state') {
50
- try {
51
- const state = await ctx.snapshot();
52
- res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
53
- res.end(JSON.stringify(state));
54
- }
55
- catch (err) {
56
- res.writeHead(500, { 'content-type': 'application/json' });
57
- res.end(JSON.stringify({ error: String(err?.message ?? err) }));
58
- }
59
- return true;
60
- }
61
- if (url === '/events') {
62
- res.writeHead(200, {
63
- 'content-type': 'text/event-stream',
64
- 'cache-control': 'no-cache',
65
- connection: 'keep-alive',
66
- });
67
- let closed = false;
68
- // Declare the timer first and register the disconnect cleanup BEFORE the
69
- // first `await push()`. `assembleState()` (team scan + git diffs) can be
70
- // slow; if the client disconnects during it, the one-shot 'close' must
71
- // already have a listener — otherwise it fires into the void, the interval
72
- // starts anyway, and we leak a timer writing to a destroyed socket forever.
73
- let timer;
74
- req.on('close', () => {
75
- closed = true;
76
- if (timer)
77
- clearInterval(timer);
78
- });
79
- const push = async () => {
80
- if (closed)
81
- return;
82
- try {
83
- const state = await ctx.snapshot();
84
- if (closed)
85
- return;
86
- res.write(`event: state\ndata: ${JSON.stringify(state)}\n\n`);
87
- }
88
- catch (err) {
89
- if (!closed)
90
- res.write(`event: error\ndata: ${JSON.stringify({ error: String(err) })}\n\n`);
91
- }
92
- };
93
- await push(); // immediate first frame
94
- // Only arm the interval if the client is still connected. If 'close' fired
95
- // during the first push, `closed` is already true (and the handler ran
96
- // while `timer` was undefined), so starting an interval here would leak.
97
- if (!closed)
98
- timer = setInterval(push, ctx.intervalMs);
99
- return true;
100
- }
101
- return false;
102
- }
103
- /**
104
- * Create (but do not start) the read-only serve server. Caller invokes
105
- * `.listen(port, SERVE_HOST)`. Returned so tests can bind an ephemeral port.
106
- */
107
- export function createServeServer(opts = {}) {
108
- const ctx = resolveServeContext(opts);
109
- const server = createServer(async (req, res) => {
110
- // DNS-rebind guard: reject any explicit non-loopback Host before doing work.
111
- if (!isAllowedServeHost(req.headers.host)) {
112
- res.writeHead(403, { 'content-type': 'text/plain' });
113
- res.end('forbidden: non-local Host');
114
- return;
115
- }
116
- // Read-only: reject anything that could mutate. Only GET is served.
117
- if (req.method !== 'GET') {
118
- res.writeHead(405, { 'content-type': 'text/plain', allow: 'GET' });
119
- res.end('method not allowed');
120
- return;
121
- }
122
- const url = (req.url || '/').split('?')[0];
123
- const handled = await handleServeGet(url, req, res, ctx);
124
- if (!handled) {
125
- res.writeHead(404, { 'content-type': 'text/plain' });
126
- res.end('not found');
127
- }
128
- });
129
- return server;
130
- }
131
- /**
132
- * Start the serve server on `port`, bound to loopback. Resolves with the
133
- * actual bound port (useful when `port === 0` for tests).
134
- */
135
- export function startServeServer(port = DEFAULT_SERVE_PORT, opts = {}) {
136
- const server = createServeServer(opts);
137
- return new Promise((resolve, reject) => {
138
- server.once('error', reject);
139
- server.listen(port, SERVE_HOST, () => {
140
- const addr = server.address();
141
- const boundPort = typeof addr === 'object' && addr ? addr.port : port;
142
- resolve({ server, port: boundPort });
143
- });
144
- });
145
- }
@@ -1,43 +0,0 @@
1
- /** Directory holding per-session NDJSON capture files. */
2
- export declare function streamDir(): string;
3
- /** Capture-file path for a session id (id is sanitized to a safe filename). */
4
- export declare function streamLogPath(sessionId: string): string;
5
- /** A normalized event handed to the phone. `raw` is the untouched harness line. */
6
- export interface StreamEvent {
7
- /** UI-facing discriminator: message | tool_use | tool_result | result | error | system | unknown. */
8
- type: string;
9
- raw: unknown;
10
- }
11
- /**
12
- * Normalize one harness JSON line to a {@link StreamEvent}. Schemas differ per
13
- * agent (there is no unified enum in the CLI), so we key off the common `type`
14
- * field (Claude stream-json: `assistant` | `user` | `result` | `system`; a
15
- * tool-use lives inside an `assistant` message's content) and carry `raw`
16
- * through untouched so the client can render richer detail without the anchor
17
- * having to model every agent.
18
- */
19
- export declare function normalizeEvent(obj: unknown): StreamEvent;
20
- /** A parsed event paired with the byte offset immediately AFTER its line. */
21
- export interface OffsetEvent {
22
- event: StreamEvent;
23
- /** Resume point: absolute byte offset just past this event's newline. */
24
- offset: number;
25
- }
26
- /** Result of one offset-tail read of a capture file. */
27
- export interface ReadResult {
28
- events: OffsetEvent[];
29
- /** Byte offset advanced ONLY past complete newline-terminated lines. */
30
- newOffset: number;
31
- /** True once a terminal event (result/error) has been parsed. */
32
- done: boolean;
33
- }
34
- /**
35
- * Read complete NDJSON lines from `fromOffset`, byte-accurately. Each event
36
- * carries the exact byte offset past its own newline so an SSE client can set
37
- * `Last-Event-ID` per event and resume with neither loss nor duplication even
38
- * if the connection drops mid-batch. A trailing partial line (no newline yet)
39
- * is left unconsumed so the offset never splits a multi-byte event — the next
40
- * read picks it up once the harness flushes the newline. Non-JSON preamble
41
- * lines are skipped, not fatal.
42
- */
43
- export declare function readNewEvents(file: string, fromOffset: number): ReadResult;
@@ -1,116 +0,0 @@
1
- /**
2
- * Live NDJSON event bridge for the iOS cockpit (RUSH-1732).
3
- *
4
- * A control-mode run (`defaultRunner`) is launched with `--json`, so the agent
5
- * harness emits one JSON event per line. We capture that stream to a per-session
6
- * logfile and replay it to the phone over SSE at `GET /api/session/:id/stream`,
7
- * normalized to the small event set the UI needs. The read is **offset-tailed**
8
- * — the same resumable pattern `hosts/progress.ts` uses for remote logs — so a
9
- * phone that drops mid-run reconnects with `?offset=<bytes>` (or the standard
10
- * `Last-Event-ID` header) and never loses or double-counts an event.
11
- *
12
- * Scope: this streams runs whose executor is the anchor itself (no `--device`).
13
- * Streaming a run offloaded to another box reuses `pullRemoteLogDelta` and is a
14
- * follow-up; see the changelog fragment.
15
- */
16
- import fs from 'fs';
17
- import path from 'path';
18
- import { getCacheDir } from '../state.js';
19
- /** Directory holding per-session NDJSON capture files. */
20
- export function streamDir() {
21
- return path.join(getCacheDir(), 'serve', 'streams');
22
- }
23
- /** Capture-file path for a session id (id is sanitized to a safe filename). */
24
- export function streamLogPath(sessionId) {
25
- const safe = sessionId.replace(/[^a-zA-Z0-9._-]/g, '_');
26
- return path.join(streamDir(), `${safe}.ndjson`);
27
- }
28
- /** Event types that terminate a run's stream (close the SSE). */
29
- const TERMINAL = new Set(['result', 'error']);
30
- /**
31
- * Whether the run has ended — i.e. the file's LAST complete (newline-terminated)
32
- * line is a terminal event. This is independent of any read offset, so a client
33
- * that resumes from an offset already at/past the terminal event still learns
34
- * the run is done and the SSE closes (instead of hanging on a `done:false` read
35
- * that finds no new lines). Cheap: `readNewEvents` already has the full buffer.
36
- */
37
- function fileIsDone(buf) {
38
- const lastNl = buf.lastIndexOf(0x0a);
39
- if (lastNl < 0)
40
- return false;
41
- const lines = buf.subarray(0, lastNl + 1).toString('utf-8').split('\n');
42
- for (let i = lines.length - 1; i >= 0; i--) {
43
- const s = lines[i].trim();
44
- if (!s)
45
- continue; // skip trailing blank lines
46
- try {
47
- return TERMINAL.has(normalizeEvent(JSON.parse(s)).type);
48
- }
49
- catch {
50
- return false; // last complete line is a partial/non-JSON write — not done
51
- }
52
- }
53
- return false;
54
- }
55
- /**
56
- * Normalize one harness JSON line to a {@link StreamEvent}. Schemas differ per
57
- * agent (there is no unified enum in the CLI), so we key off the common `type`
58
- * field (Claude stream-json: `assistant` | `user` | `result` | `system`; a
59
- * tool-use lives inside an `assistant` message's content) and carry `raw`
60
- * through untouched so the client can render richer detail without the anchor
61
- * having to model every agent.
62
- */
63
- export function normalizeEvent(obj) {
64
- const rec = obj;
65
- const t = rec && typeof rec.type === 'string' ? rec.type : 'unknown';
66
- return { type: t, raw: obj };
67
- }
68
- /**
69
- * Read complete NDJSON lines from `fromOffset`, byte-accurately. Each event
70
- * carries the exact byte offset past its own newline so an SSE client can set
71
- * `Last-Event-ID` per event and resume with neither loss nor duplication even
72
- * if the connection drops mid-batch. A trailing partial line (no newline yet)
73
- * is left unconsumed so the offset never splits a multi-byte event — the next
74
- * read picks it up once the harness flushes the newline. Non-JSON preamble
75
- * lines are skipped, not fatal.
76
- */
77
- export function readNewEvents(file, fromOffset) {
78
- let buf;
79
- try {
80
- buf = fs.readFileSync(file);
81
- }
82
- catch {
83
- // File not created yet (run still starting) — nothing to read, hold offset.
84
- return { events: [], newOffset: fromOffset, done: false };
85
- }
86
- // `done` reflects the whole file's terminal state, so a resume at/past the
87
- // terminal event still closes the stream rather than hanging forever.
88
- const done = fileIsDone(buf);
89
- if (fromOffset >= buf.length)
90
- return { events: [], newOffset: fromOffset, done };
91
- const slice = buf.subarray(fromOffset);
92
- const lastNl = slice.lastIndexOf(0x0a);
93
- if (lastNl < 0)
94
- return { events: [], newOffset: fromOffset, done }; // no complete line yet
95
- const events = [];
96
- let lineStart = 0; // byte index within slice
97
- for (let i = 0; i <= lastNl; i++) {
98
- if (slice[i] !== 0x0a)
99
- continue;
100
- const lineBuf = slice.subarray(lineStart, i); // excludes the newline
101
- const endOffset = fromOffset + i + 1; // absolute byte offset just past '\n'
102
- lineStart = i + 1;
103
- const s = lineBuf.toString('utf-8').trim();
104
- if (!s)
105
- continue;
106
- let obj;
107
- try {
108
- obj = JSON.parse(s);
109
- }
110
- catch {
111
- continue; // skip non-JSON (banner/preamble) lines
112
- }
113
- events.push({ event: normalizeEvent(obj), offset: endOffset });
114
- }
115
- return { events, newOffset: fromOffset + lastNl + 1, done };
116
- }
@@ -1,35 +0,0 @@
1
- /** One issued token, stored by hash only. */
2
- export interface ControlTokenRecord {
3
- /** Short public id for display / revocation (not secret). */
4
- id: string;
5
- /** SHA-256 hex of the raw token. */
6
- hash: string;
7
- /** Human label, e.g. "muqsit-iphone". */
8
- label: string;
9
- createdAt: string;
10
- }
11
- /**
12
- * Mint a new control token, persist its hash, and return the raw token ONCE.
13
- * The caller must surface it to the operator immediately — it cannot be
14
- * recovered later.
15
- */
16
- export declare function addControlToken(label: string): {
17
- id: string;
18
- token: string;
19
- };
20
- /**
21
- * Ensure at least one token exists. Returns the raw token only when it had to
22
- * mint one (first `--control` boot); otherwise `{ created: false }` and the
23
- * existing tokens stand.
24
- */
25
- export declare function ensureControlToken(label?: string): {
26
- created: true;
27
- id: string;
28
- token: string;
29
- } | {
30
- created: false;
31
- };
32
- /** True when `presented` matches a stored token hash (constant-time compare). */
33
- export declare function verifyControlToken(presented: string | undefined): boolean;
34
- /** List issued tokens (hashes only — safe to display). */
35
- export declare function listControlTokens(): ControlTokenRecord[];
@@ -1,85 +0,0 @@
1
- /**
2
- * Bearer-token store for the authenticated `agents serve --control` server.
3
- *
4
- * Only the SHA-256 *hash* of each token is written to disk — the raw token is
5
- * shown once at creation (like an API key) and never persisted, so a leaked
6
- * store file cannot be replayed. This is the anchor-side credential the iOS
7
- * cockpit presents on every request; `agents devices pair-ios` (Phase 3) mints
8
- * additional per-device tokens through {@link addControlToken}.
9
- *
10
- * Store path: `<cache>/serve/control-tokens.json` — under `.cache/`, which is
11
- * gitignored, so no credential material lands in a version-controlled repo.
12
- */
13
- import fs from 'fs';
14
- import path from 'path';
15
- import { createHash, randomBytes, timingSafeEqual } from 'crypto';
16
- import { getCacheDir } from '../state.js';
17
- function storePath() {
18
- return path.join(getCacheDir(), 'serve', 'control-tokens.json');
19
- }
20
- function readStore() {
21
- try {
22
- const raw = fs.readFileSync(storePath(), 'utf-8');
23
- const parsed = JSON.parse(raw);
24
- if (parsed && Array.isArray(parsed.tokens))
25
- return parsed;
26
- }
27
- catch {
28
- // Missing/corrupt store → start empty; a fresh token gets minted below.
29
- }
30
- return { tokens: [] };
31
- }
32
- function writeStore(store) {
33
- const p = storePath();
34
- fs.mkdirSync(path.dirname(p), { recursive: true });
35
- // 0600: readable only by the owner — it holds token hashes. `mode` on
36
- // writeFileSync is honored only at *creation*, so chmod every write to
37
- // self-heal if the perms were ever widened externally (this is a credential
38
- // file, even if only hashes).
39
- fs.writeFileSync(p, JSON.stringify(store, null, 2), { mode: 0o600 });
40
- fs.chmodSync(p, 0o600);
41
- }
42
- function sha256(s) {
43
- return createHash('sha256').update(s).digest('hex');
44
- }
45
- /**
46
- * Mint a new control token, persist its hash, and return the raw token ONCE.
47
- * The caller must surface it to the operator immediately — it cannot be
48
- * recovered later.
49
- */
50
- export function addControlToken(label) {
51
- const store = readStore();
52
- const id = randomBytes(4).toString('hex');
53
- const token = randomBytes(32).toString('hex');
54
- store.tokens.push({ id, hash: sha256(token), label, createdAt: new Date().toISOString() });
55
- writeStore(store);
56
- return { id, token };
57
- }
58
- /**
59
- * Ensure at least one token exists. Returns the raw token only when it had to
60
- * mint one (first `--control` boot); otherwise `{ created: false }` and the
61
- * existing tokens stand.
62
- */
63
- export function ensureControlToken(label = 'default') {
64
- const store = readStore();
65
- if (store.tokens.length > 0)
66
- return { created: false };
67
- const { id, token } = addControlToken(label);
68
- return { created: true, id, token };
69
- }
70
- /** True when `presented` matches a stored token hash (constant-time compare). */
71
- export function verifyControlToken(presented) {
72
- if (!presented)
73
- return false;
74
- const want = Buffer.from(sha256(presented), 'hex');
75
- for (const rec of readStore().tokens) {
76
- const have = Buffer.from(rec.hash, 'hex');
77
- if (have.length === want.length && timingSafeEqual(have, want))
78
- return true;
79
- }
80
- return false;
81
- }
82
- /** List issued tokens (hashes only — safe to display). */
83
- export function listControlTokens() {
84
- return readStore().tokens;
85
- }