@someonegg/agent-hub 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -2
  3. package/README.zh-CN.md +99 -0
  4. package/README.zh-TW.md +99 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/client/assets/index-4ejyszaV.css +1 -0
  66. package/dist/client/assets/index-CKKT4cMK.js +309 -0
  67. package/dist/client/favicon.ico +0 -0
  68. package/dist/client/favicon.svg +6 -0
  69. package/dist/client/icons/icon-192.png +0 -0
  70. package/dist/client/icons/icon-512.png +0 -0
  71. package/dist/client/icons/icon-maskable-512.png +0 -0
  72. package/dist/client/index.html +22 -0
  73. package/dist/client/manifest.webmanifest +21 -0
  74. package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
  75. package/dist/server/server/adapters/claude/identity.js +28 -0
  76. package/dist/server/server/adapters/claude/interaction.js +68 -0
  77. package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
  78. package/dist/server/server/adapters/claude/ready-script.js +25 -0
  79. package/dist/server/server/adapters/claude/session-manager.js +106 -0
  80. package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
  81. package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
  82. package/dist/server/server/adapters/codex/catalog-source.js +2 -0
  83. package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
  84. package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
  85. package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
  86. package/dist/server/server/adapters/codex/io-errors.js +2 -0
  87. package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
  88. package/dist/server/server/adapters/codex/limits.js +2 -0
  89. package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
  90. package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
  91. package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
  92. package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
  93. package/dist/server/server/adapters/codex/session-manager.js +51 -0
  94. package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
  95. package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
  96. package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
  97. package/dist/server/server/adapters/codex/value-readers.js +4 -0
  98. package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
  99. package/dist/server/server/adapters/shared/io-errors.js +13 -0
  100. package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
  101. package/dist/server/server/adapters/shared/limits.js +4 -0
  102. package/dist/server/server/adapters/shared/session-management.js +43 -0
  103. package/dist/server/server/adapters/shared/source-cache.js +23 -0
  104. package/dist/server/server/api/session-api-mapper.js +65 -0
  105. package/dist/server/server/application/interaction-session-reader.js +17 -0
  106. package/dist/server/server/application/session-read-service.js +86 -0
  107. package/dist/server/server/application/session-reader.js +2 -0
  108. package/dist/server/server/application/session-response.js +11 -0
  109. package/dist/server/server/catalog/config.js +4 -0
  110. package/dist/server/server/catalog/session-catalog-service.js +112 -0
  111. package/dist/server/server/cli/command-error.js +27 -0
  112. package/dist/server/server/cli/main.js +42 -0
  113. package/dist/server/server/cli/output.js +52 -0
  114. package/dist/server/server/cli/resource.js +104 -0
  115. package/dist/server/server/cli/session-client.js +64 -0
  116. package/dist/server/server/cli/session-follow.js +58 -0
  117. package/dist/server/server/cli/session-output.js +184 -0
  118. package/dist/server/server/cli/session-wait.js +44 -0
  119. package/dist/server/server/cli/session.js +318 -0
  120. package/dist/server/server/cli/skill.js +37 -0
  121. package/dist/server/server/config.js +196 -0
  122. package/dist/server/server/create-hub.js +91 -0
  123. package/dist/server/server/directory-lock.js +65 -0
  124. package/dist/server/server/domain/project-directory.js +5 -0
  125. package/dist/server/server/domain/session-domain.js +2 -0
  126. package/dist/server/server/domain/session-text.js +45 -0
  127. package/dist/server/server/hosting/diff-store.js +103 -0
  128. package/dist/server/server/hosting/routes.js +168 -0
  129. package/dist/server/server/hosting/store.js +162 -0
  130. package/dist/server/server/hosting/validation-error.js +4 -0
  131. package/dist/server/server/http/api-route-handler.js +2 -0
  132. package/dist/server/server/http/api-router.js +54 -0
  133. package/dist/server/server/http/create-server.js +218 -0
  134. package/dist/server/server/http/query-params.js +19 -0
  135. package/dist/server/server/http/request-body.js +48 -0
  136. package/dist/server/server/http/request-errors.js +59 -0
  137. package/dist/server/server/http/router.js +16 -0
  138. package/dist/server/server/http/security.js +19 -0
  139. package/dist/server/server/http/session-interaction-routes.js +128 -0
  140. package/dist/server/server/http/session-management-routes.js +105 -0
  141. package/dist/server/server/http/session-path.js +10 -0
  142. package/dist/server/server/http/session-read-routes.js +143 -0
  143. package/dist/server/server/http/terminal-preview-websocket.js +179 -0
  144. package/dist/server/server/http/upload-routes.js +124 -0
  145. package/dist/server/server/interaction/interaction-service.js +276 -0
  146. package/dist/server/server/interaction/message.js +19 -0
  147. package/dist/server/server/interaction/pane-binding-manager.js +62 -0
  148. package/dist/server/server/interaction/pane-observation.js +31 -0
  149. package/dist/server/server/interaction/reconcile-session-state.js +21 -0
  150. package/dist/server/server/interaction/runtime-capabilities.js +2 -0
  151. package/dist/server/server/interaction/session-runtime.js +86 -0
  152. package/dist/server/server/interaction/terminal-activity-service.js +81 -0
  153. package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
  154. package/dist/server/server/interaction/terminal-preview-service.js +184 -0
  155. package/dist/server/server/interaction/tmux-launcher.js +84 -0
  156. package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
  157. package/dist/server/server/interaction/tmux-service.js +334 -0
  158. package/dist/server/server/interaction/tmux-target.js +13 -0
  159. package/dist/server/server/interaction/transport.js +9 -0
  160. package/dist/server/server/main.js +54 -0
  161. package/dist/server/server/management/operation-service.js +307 -0
  162. package/dist/server/server/management/operation-state.js +3 -0
  163. package/dist/server/server/management/session-exit-service.js +50 -0
  164. package/dist/server/server/management/session-launch-executor.js +164 -0
  165. package/dist/server/server/management/session-lifecycle-executor.js +158 -0
  166. package/dist/server/server/management/session-manager.js +64 -0
  167. package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
  168. package/dist/server/server/repository/diagnostic-logger.js +70 -0
  169. package/dist/server/server/repository/refresh-coordinator.js +14 -0
  170. package/dist/server/server/repository/session-queries.js +72 -0
  171. package/dist/server/server/security/opaque-id.js +17 -0
  172. package/dist/server/server/source/session-source.js +2 -0
  173. package/dist/server/server/updates/session-updates-service.js +151 -0
  174. package/dist/server/server/updates/update-revision.js +22 -0
  175. package/dist/server/server/uploads/store.js +304 -0
  176. package/dist/server/server/version.js +21 -0
  177. package/dist/server/shared/api-contract.js +2 -0
  178. package/dist/server/shared/diff.js +158 -0
  179. package/dist/server/shared/domain.js +2 -0
  180. package/dist/server/shared/interaction.js +22 -0
  181. package/dist/server/shared/task-name.js +6 -0
  182. package/dist/server/shared/uploads.js +8 -0
  183. package/package.json +98 -4
  184. package/skills/ahub-control/SKILL.md +93 -0
  185. package/skills/ahub-publish/SKILL.md +54 -0
@@ -0,0 +1,334 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstat } from "node:fs/promises";
4
+ import { createConnection } from "node:net";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { normalizeMessage } from "./message.js";
7
+ import { ownedSocket, tmuxTarget, validTmuxSessionName } from "./tmux-target.js";
8
+ import { InteractionTransportError, } from "./transport.js";
9
+ export const MAX_TERMINAL_PREVIEW_BYTES = 256_000;
10
+ const DEFAULT_TIMEOUT_MS = 5_000;
11
+ const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
12
+ export class TmuxInteractionService {
13
+ runner;
14
+ timeoutMs;
15
+ #queues = new Map();
16
+ constructor(runner = new SpawnTmuxCommandRunner(), timeoutMs = DEFAULT_TIMEOUT_MS) {
17
+ this.runner = runner;
18
+ this.timeoutMs = timeoutMs;
19
+ }
20
+ #queries = new Map();
21
+ async connect(attempt) {
22
+ const binding = await this.resolveTarget(attempt);
23
+ if (!binding)
24
+ throw disconnected();
25
+ return binding;
26
+ }
27
+ resolveTarget(attempt) {
28
+ if (!attempt.valid ||
29
+ !attempt.socketPath.startsWith("/") ||
30
+ !validTmuxSessionName(attempt.sessionName))
31
+ return Promise.resolve(null);
32
+ const { socketPath, sessionName } = attempt;
33
+ let query = this.#queries.get(socketPath);
34
+ if (!query) {
35
+ query = this.#query(socketPath);
36
+ this.#queries.set(socketPath, query);
37
+ // Share only in-flight observations, never completed results.
38
+ void query.then(() => this.#queries.delete(socketPath), () => this.#queries.delete(socketPath));
39
+ }
40
+ return query.then((sessions) => sessions.has(sessionName) ? { socketPath, sessionName } : null);
41
+ }
42
+ #waiting = [];
43
+ #active = 0;
44
+ async #query(socketPath) {
45
+ while (this.#active >= 8)
46
+ await new Promise((resolve) => this.#waiting.push(resolve));
47
+ this.#active++;
48
+ try {
49
+ return await this.#querySessions(socketPath);
50
+ }
51
+ finally {
52
+ this.#active--;
53
+ this.#waiting.shift()?.();
54
+ }
55
+ }
56
+ async #querySessions(socketPath) {
57
+ try {
58
+ await validateSocket(socketPath);
59
+ }
60
+ catch (error) {
61
+ if (error instanceof InteractionTransportError && error.code === "disconnected")
62
+ return new Set();
63
+ throw error;
64
+ }
65
+ let result;
66
+ try {
67
+ result = await this.runner.run(socketPath, ["list-sessions", "-F", "#{session_name}"], undefined, this.timeoutMs);
68
+ }
69
+ catch (error) {
70
+ if (error instanceof InteractionTransportError &&
71
+ error.code === "command_failed" &&
72
+ !(await socketListening(socketPath, this.timeoutMs)))
73
+ return new Set();
74
+ throw error;
75
+ }
76
+ return new Set(result.stdout.split("\n").filter(Boolean));
77
+ }
78
+ killSession(binding, assertAvailable = () => { }) {
79
+ return this.#serialize(binding, async () => {
80
+ await this.#revalidate(binding);
81
+ assertAvailable();
82
+ await this.#run(binding, ["kill-session", "-t", tmuxTarget(binding.sessionName)]);
83
+ });
84
+ }
85
+ sendMessage(binding, message, assertAvailable = () => { }) {
86
+ const normalized = normalizeMessage(message);
87
+ return this.#serialize(binding, async () => {
88
+ await this.#revalidate(binding);
89
+ assertAvailable();
90
+ const bufferName = `ahub-${process.pid}-${randomUUID()}`;
91
+ let bufferMayExist = true;
92
+ try {
93
+ await this.#run(binding, ["load-buffer", "-b", bufferName, "-"], normalized);
94
+ assertAvailable();
95
+ await this.#run(binding, [
96
+ "paste-buffer",
97
+ "-p",
98
+ "-d",
99
+ "-b",
100
+ bufferName,
101
+ "-t",
102
+ tmuxTarget(binding.sessionName),
103
+ ]);
104
+ bufferMayExist = false;
105
+ assertAvailable();
106
+ await this.#run(binding, ["send-keys", "-t", tmuxTarget(binding.sessionName), "Enter"]);
107
+ }
108
+ finally {
109
+ if (bufferMayExist) {
110
+ try {
111
+ await this.#run(binding, ["delete-buffer", "-b", bufferName]);
112
+ }
113
+ catch {
114
+ // Preserve the original failure; the buffer may not exist or may already be deleted.
115
+ }
116
+ }
117
+ }
118
+ });
119
+ }
120
+ sendKeys(binding, steps, assertAvailable = () => { }) {
121
+ return this.#serialize(binding, async () => {
122
+ for (const step of steps) {
123
+ if (step.delayMs > 0)
124
+ await delay(step.delayMs);
125
+ await this.#revalidate(binding);
126
+ assertAvailable();
127
+ await this.#run(binding, [
128
+ "send-keys",
129
+ "-t",
130
+ tmuxTarget(binding.sessionName),
131
+ ...step.keys,
132
+ ]);
133
+ }
134
+ });
135
+ }
136
+ captureTerminal(binding, assertAvailable = () => { }) {
137
+ return this.#serialize(binding, async () => {
138
+ const result = await this.#capture(binding, ["capture-pane", "-p", "-t", tmuxTarget(binding.sessionName)], assertAvailable);
139
+ return { content: result.stdout, truncated: result.truncated === true };
140
+ });
141
+ }
142
+ /** Read cursor and screen in one tmux command queue; discard moving or truncated captures. */
143
+ captureTerminalWithCursor(binding) {
144
+ return this.#serialize(binding, async () => {
145
+ const target = tmuxTarget(binding.sessionName);
146
+ const position = [
147
+ "display-message",
148
+ "-p",
149
+ "-t",
150
+ target,
151
+ "#{cursor_x},#{cursor_y},#{pane_width},#{pane_height}",
152
+ ];
153
+ const result = await this.#capture(binding, [
154
+ ...position,
155
+ ";",
156
+ "capture-pane",
157
+ "-p",
158
+ "-t",
159
+ target,
160
+ ";",
161
+ ...position,
162
+ ]);
163
+ if (result.truncated)
164
+ return null;
165
+ const lines = result.stdout.split("\n");
166
+ if (lines.pop() !== "")
167
+ return null;
168
+ const before = lines.shift();
169
+ const after = lines.pop();
170
+ if (!before || before !== after || !/^\d+,\d+,\d+,\d+$/.test(before))
171
+ return null;
172
+ const [cursorX, cursorY, width, height] = before.split(",").map(Number);
173
+ if (![cursorX, cursorY, width, height].every(Number.isSafeInteger) ||
174
+ width <= 0 ||
175
+ height <= 0 ||
176
+ cursorX >= width ||
177
+ cursorY >= height ||
178
+ lines.length !== height)
179
+ return null;
180
+ return { content: lines.join("\n"), cursorX, cursorY };
181
+ });
182
+ }
183
+ async #capture(binding, args, assertAvailable = () => { }) {
184
+ if (!binding.socketPath.startsWith("/") || !validTmuxSessionName(binding.sessionName))
185
+ throw disconnected();
186
+ await validateSocket(binding.socketPath);
187
+ assertAvailable();
188
+ try {
189
+ return await this.runner.run(binding.socketPath, args, undefined, this.timeoutMs, {
190
+ maxOutputBytes: MAX_TERMINAL_PREVIEW_BYTES,
191
+ truncateStart: true,
192
+ });
193
+ }
194
+ catch (error) {
195
+ // Only confirmed absence replaces the capture error; failed probes prove nothing.
196
+ const target = await this.resolveTarget({ valid: true, ordinal: 0, ...binding }).catch(() => undefined);
197
+ if (target === null)
198
+ throw disconnected();
199
+ throw error;
200
+ }
201
+ }
202
+ /** Query errors never establish that the selected session exited. */
203
+ async inspect(binding) {
204
+ return (await this.resolveTarget({ valid: true, ordinal: 0, ...binding })) ? "alive" : "exited";
205
+ }
206
+ async #revalidate(binding) {
207
+ if (!(await this.resolveTarget({ valid: true, ordinal: 0, ...binding })))
208
+ throw disconnected();
209
+ }
210
+ async #run(binding, args, input) {
211
+ await this.runner.run(binding.socketPath, args, input, this.timeoutMs);
212
+ }
213
+ #serialize(binding, operation) {
214
+ const key = `${binding.socketPath}\0${binding.sessionName}`;
215
+ const previous = this.#queues.get(key) ?? Promise.resolve();
216
+ const current = previous.catch(() => undefined).then(operation);
217
+ const settled = current.then(() => undefined, () => undefined);
218
+ this.#queues.set(key, settled);
219
+ void settled.finally(() => {
220
+ if (this.#queues.get(key) === settled)
221
+ this.#queues.delete(key);
222
+ });
223
+ return current;
224
+ }
225
+ }
226
+ async function validateSocket(socketPath) {
227
+ try {
228
+ if (!ownedSocket(await lstat(socketPath)))
229
+ throw new InteractionTransportError("command_failed", "Unsafe tmux socket");
230
+ }
231
+ catch (error) {
232
+ if (error instanceof InteractionTransportError)
233
+ throw error;
234
+ if (error.code === "ENOENT")
235
+ throw disconnected();
236
+ throw error;
237
+ }
238
+ }
239
+ function disconnected() {
240
+ return new InteractionTransportError("disconnected", "tmux target is not connected");
241
+ }
242
+ export class SpawnTmuxCommandRunner {
243
+ run(socketPath, args, input, timeoutMs, options) {
244
+ return new Promise((resolve, reject) => {
245
+ const child = spawn("tmux", ["-S", socketPath, ...args], {
246
+ shell: false,
247
+ stdio: ["pipe", "pipe", "pipe"],
248
+ });
249
+ let stdout = Buffer.alloc(0);
250
+ const stderr = [];
251
+ let outputBytes = 0;
252
+ let truncated = false;
253
+ let settled = false;
254
+ const finish = (callback) => {
255
+ if (settled)
256
+ return;
257
+ settled = true;
258
+ clearTimeout(timer);
259
+ callback();
260
+ };
261
+ const timer = setTimeout(() => {
262
+ child.kill("SIGKILL");
263
+ finish(() => reject(new InteractionTransportError("timeout", "tmux operation timed out; the result is unknown")));
264
+ }, timeoutMs);
265
+ const collectStdout = (chunk) => {
266
+ if (options?.truncateStart) {
267
+ stdout = Buffer.concat([stdout, chunk]);
268
+ if (stdout.byteLength > options.maxOutputBytes) {
269
+ stdout = trimLeadingPartialUtf8(stdout.subarray(stdout.byteLength - options.maxOutputBytes));
270
+ truncated = true;
271
+ }
272
+ return;
273
+ }
274
+ outputBytes += chunk.byteLength;
275
+ if (outputBytes <= MAX_COMMAND_OUTPUT_BYTES)
276
+ stdout = Buffer.concat([stdout, chunk]);
277
+ else
278
+ child.kill("SIGKILL");
279
+ };
280
+ const collectStderr = (chunk) => {
281
+ outputBytes += chunk.byteLength;
282
+ if (outputBytes <= MAX_COMMAND_OUTPUT_BYTES)
283
+ stderr.push(chunk);
284
+ else
285
+ child.kill("SIGKILL");
286
+ };
287
+ child.stdout.on("data", collectStdout);
288
+ child.stderr.on("data", collectStderr);
289
+ child.once("error", (error) => finish(() => reject(new InteractionTransportError("command_failed", error.message))));
290
+ child.stdin.once("error", (error) => finish(() => reject(new InteractionTransportError("command_failed", error.message))));
291
+ child.once("close", (code) => finish(() => {
292
+ if (outputBytes > MAX_COMMAND_OUTPUT_BYTES || code !== 0) {
293
+ reject(new InteractionTransportError("command_failed", Buffer.concat(stderr).toString("utf8").trim() || "tmux command failed"));
294
+ return;
295
+ }
296
+ resolve({ stdout: stdout.toString("utf8"), truncated });
297
+ }));
298
+ if (input === undefined)
299
+ child.stdin.end();
300
+ else
301
+ child.stdin.end(input, "utf8");
302
+ });
303
+ }
304
+ }
305
+ function trimLeadingPartialUtf8(buffer) {
306
+ let start = 0;
307
+ while (start < buffer.byteLength && (buffer[start] & 0xc0) === 0x80)
308
+ start += 1;
309
+ return buffer.subarray(start);
310
+ }
311
+ // A refused connection establishes a stopped server even when tmux left its socket file.
312
+ // Timeouts, permissions and other socket failures remain unknown.
313
+ function socketListening(path, timeoutMs) {
314
+ return new Promise((resolve, reject) => {
315
+ const socket = createConnection({ path });
316
+ socket.setTimeout(timeoutMs);
317
+ socket.once("connect", () => {
318
+ socket.destroy();
319
+ resolve(true);
320
+ });
321
+ socket.once("timeout", () => {
322
+ socket.destroy();
323
+ reject(new InteractionTransportError("timeout", "Socket validation timed out"));
324
+ });
325
+ socket.once("error", (error) => {
326
+ socket.destroy();
327
+ if (error.code === "ECONNREFUSED" || error.code === "ENOENT")
328
+ resolve(false);
329
+ else
330
+ reject(error);
331
+ });
332
+ });
333
+ }
334
+ //# sourceMappingURL=tmux-service.js.map
@@ -0,0 +1,13 @@
1
+ /** Hub sessions have exactly one window and pane; topology is a user contract. */
2
+ export function validTmuxSessionName(value) {
3
+ return /^ahub-[a-fA-F0-9]{8}-(?:[a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}$/.test(value);
4
+ }
5
+ /** A trailing colon disambiguates session names for both session and pane commands. */
6
+ export function tmuxTarget(sessionName) {
7
+ return `=${sessionName}:`;
8
+ }
9
+ /** Only unix sockets owned by the current user may be controlled. */
10
+ export function ownedSocket(stats) {
11
+ return stats.isSocket() && typeof process.getuid === "function" && stats.uid === process.getuid();
12
+ }
13
+ //# sourceMappingURL=tmux-target.js.map
@@ -0,0 +1,9 @@
1
+ export class InteractionTransportError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "InteractionTransportError";
7
+ }
8
+ }
9
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { commandLineHelp, loadConfig } from "./config.js";
3
+ import { createAhub } from "./create-hub.js";
4
+ import { createApiRouter } from "./http/api-router.js";
5
+ import { startServer } from "./http/create-server.js";
6
+ import { createTerminalPreviewWebSocket } from "./http/terminal-preview-websocket.js";
7
+ import { uploadDirectory } from "./http/upload-routes.js";
8
+ import { UploadStore } from "./uploads/store.js";
9
+ import { packageVersion } from "./version.js";
10
+ async function main() {
11
+ const command = loadConfig();
12
+ if (command.kind === "help") {
13
+ console.log(commandLineHelp());
14
+ return;
15
+ }
16
+ if (command.kind === "version") {
17
+ console.log(await packageVersion());
18
+ return;
19
+ }
20
+ const { config } = command;
21
+ const uploads = new UploadStore(await uploadDirectory(config.uploadDirectory));
22
+ await uploads.initialize();
23
+ const hub = await createAhub({
24
+ agents: config.agents,
25
+ socket: config.tmuxSocket,
26
+ claudeReadyScript: config.claudeReadyScript,
27
+ interactionEnabled: config.interactionEnabled,
28
+ uploads,
29
+ });
30
+ const server = await startServer(config, createApiRouter({ ...hub, uploads }), hub.close, createTerminalPreviewWebSocket(hub.interaction));
31
+ const address = server.address();
32
+ const port = typeof address === "object" && address ? address.port : config.port;
33
+ const host = config.host.includes(":") ? `[${config.host}]` : config.host;
34
+ const protocol = config.tls.enabled ? "https" : "http";
35
+ const mutualTls = config.tls.enabled && config.tls.certificateAuthorityPath
36
+ ? " (client certificates required)"
37
+ : "";
38
+ console.log(`Agent Hub listening on ${protocol}://${host}:${port}${mutualTls}`);
39
+ function close() {
40
+ server.close((error) => {
41
+ if (error) {
42
+ console.error("Failed to stop Agent Hub");
43
+ process.exitCode = 1;
44
+ }
45
+ });
46
+ }
47
+ process.once("SIGINT", close);
48
+ process.once("SIGTERM", close);
49
+ }
50
+ main().catch((error) => {
51
+ console.error(error instanceof Error ? error.message : "Failed to start Agent Hub");
52
+ process.exitCode = 1;
53
+ });
54
+ //# sourceMappingURL=main.js.map