@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,184 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { diffLines } from "./terminal-preview-diff.js";
3
+ import { createTmuxPreviewListener, } from "./tmux-preview-listener.js";
4
+ export class TerminalPreviewService {
5
+ capture;
6
+ listen;
7
+ #states = new Map();
8
+ #identity = randomUUID();
9
+ #closed = false;
10
+ constructor(capture, listen = createTmuxPreviewListener()) {
11
+ this.capture = capture;
12
+ this.listen = listen;
13
+ }
14
+ async captureFresh(binding, assertOwner) {
15
+ assertOwner();
16
+ const result = await this.capture(binding, assertOwner);
17
+ assertOwner();
18
+ return { ...result, capturedAt: new Date().toISOString() };
19
+ }
20
+ subscribe(binding, assertOwner, frame, fail, signal) {
21
+ if (this.#closed || signal.aborted)
22
+ throw new DOMException("Aborted", "AbortError");
23
+ const key = JSON.stringify([binding.socketPath, binding.sessionName]);
24
+ let state = this.#states.get(key);
25
+ const fresh = !state;
26
+ if (!state) {
27
+ state = { key, binding, viewers: new Set(), dirty: true, listening: false, disposed: false };
28
+ this.#states.set(key, state);
29
+ }
30
+ const current = state;
31
+ const close = () => {
32
+ signal.removeEventListener("abort", close);
33
+ current.viewers.delete(viewer);
34
+ if (!current.viewers.size)
35
+ this.#dispose(current);
36
+ };
37
+ const viewer = { assertOwner, frame, fail, close, initialized: false };
38
+ current.viewers.add(viewer);
39
+ signal.addEventListener("abort", close, { once: true });
40
+ try {
41
+ assertOwner();
42
+ if (fresh) {
43
+ current.subscription = this.listen(binding, () => {
44
+ current.dirty = true;
45
+ this.#schedule(current);
46
+ }, (error) => this.#fail(current, error));
47
+ // A listener may fail synchronously during setup.
48
+ if (current.disposed)
49
+ current.subscription.close();
50
+ }
51
+ void current
52
+ .subscription.ready.then(async () => {
53
+ if (current.disposed || !current.viewers.has(viewer))
54
+ return;
55
+ current.listening = true;
56
+ if (!current.snapshot)
57
+ await this.#capture(current);
58
+ if (current.disposed || !current.viewers.has(viewer))
59
+ return;
60
+ assertOwner();
61
+ if (!viewer.initialized) {
62
+ viewer.initialized = true;
63
+ frame({ type: "snapshot", ...current.snapshot });
64
+ }
65
+ })
66
+ .catch((error) => this.#fail(current, error));
67
+ }
68
+ catch (error) {
69
+ this.#fail(current, error);
70
+ }
71
+ return close;
72
+ }
73
+ invalidate() {
74
+ for (const state of this.#states.values()) {
75
+ try {
76
+ this.#assertOwner(state);
77
+ }
78
+ catch (error) {
79
+ this.#fail(state, error);
80
+ }
81
+ }
82
+ }
83
+ close() {
84
+ this.#closed = true;
85
+ for (const state of this.#states.values())
86
+ this.#fail(state, new DOMException("Aborted", "AbortError"));
87
+ }
88
+ #assertOwner(state) {
89
+ if (state.disposed)
90
+ throw new DOMException("Aborted", "AbortError");
91
+ for (const viewer of state.viewers)
92
+ viewer.assertOwner();
93
+ }
94
+ #schedule(state) {
95
+ if (!state.listening || state.disposed || state.task || state.timer)
96
+ return;
97
+ state.timer = setTimeout(() => {
98
+ state.timer = undefined;
99
+ void this.#capture(state).catch(() => { });
100
+ }, 350);
101
+ }
102
+ #capture(state) {
103
+ if (state.task)
104
+ return state.task;
105
+ clearTimeout(state.timer);
106
+ state.timer = undefined;
107
+ state.dirty = false;
108
+ state.task = (async () => {
109
+ const result = await this.captureFresh(state.binding, () => this.#assertOwner(state));
110
+ const previous = state.snapshot;
111
+ if (previous &&
112
+ previous.content === result.content &&
113
+ previous.truncated === result.truncated)
114
+ return;
115
+ const snapshot = {
116
+ ...result,
117
+ capturedAt: result.capturedAt,
118
+ revision: createHash("sha256")
119
+ .update(JSON.stringify([this.#identity, state.key, result.content, result.truncated]))
120
+ .digest("base64url"),
121
+ };
122
+ state.snapshot = snapshot;
123
+ const full = { type: "snapshot", ...snapshot };
124
+ let update = full;
125
+ if (previous) {
126
+ const operations = diffLines(previous.content, snapshot.content);
127
+ if (operations !== null) {
128
+ const delta = {
129
+ type: "delta",
130
+ baseRevision: previous.revision,
131
+ revision: snapshot.revision,
132
+ operations,
133
+ truncated: snapshot.truncated,
134
+ capturedAt: snapshot.capturedAt,
135
+ };
136
+ const size = bytes(delta);
137
+ if (size <= 8192 && size < bytes(full))
138
+ update = delta;
139
+ }
140
+ }
141
+ for (const viewer of [...state.viewers]) {
142
+ if (!state.viewers.has(viewer))
143
+ continue;
144
+ viewer.assertOwner();
145
+ const message = viewer.initialized ? update : full;
146
+ viewer.initialized = true;
147
+ viewer.frame(message);
148
+ }
149
+ })()
150
+ .catch((error) => {
151
+ this.#fail(state, error);
152
+ throw error;
153
+ })
154
+ .finally(() => {
155
+ state.task = undefined;
156
+ if (state.dirty)
157
+ this.#schedule(state);
158
+ });
159
+ return state.task;
160
+ }
161
+ #fail(state, error) {
162
+ if (state.disposed)
163
+ return;
164
+ const viewers = [...state.viewers];
165
+ this.#dispose(state);
166
+ for (const viewer of viewers) {
167
+ viewer.close();
168
+ viewer.fail(error);
169
+ }
170
+ }
171
+ #dispose(state) {
172
+ if (state.disposed)
173
+ return;
174
+ state.disposed = true;
175
+ clearTimeout(state.timer);
176
+ state.subscription?.close();
177
+ if (this.#states.get(state.key) === state)
178
+ this.#states.delete(state.key);
179
+ }
180
+ }
181
+ function bytes(value) {
182
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
183
+ }
184
+ //# sourceMappingURL=terminal-preview-service.js.map
@@ -0,0 +1,84 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { SessionLaunchError } from "../management/session-manager.js";
4
+ import { TmuxInteractionService } from "./tmux-service.js";
5
+ import { tmuxTarget, validTmuxSessionName } from "./tmux-target.js";
6
+ const execute = promisify(execFile);
7
+ export function shellQuote(value) {
8
+ return "'" + value.replace(/'/g, "'\\''") + "'";
9
+ }
10
+ export function attachCommand(socket, session) {
11
+ return [
12
+ "tmux",
13
+ ...(socket ? ["-S", shellQuote(socket)] : []),
14
+ "attach-session",
15
+ "-t",
16
+ shellQuote(tmuxTarget(session)),
17
+ ].join(" ");
18
+ }
19
+ /** Launch argv directly; user values are never interpreted by a shell. */
20
+ export class TmuxLauncher {
21
+ socketPath;
22
+ transport = new TmuxInteractionService();
23
+ constructor(socketPath) {
24
+ this.socketPath = socketPath;
25
+ }
26
+ async launch(id, directory, command) {
27
+ const sessionName = `ahub-${id}`;
28
+ if (!validTmuxSessionName(sessionName))
29
+ throw new SessionLaunchError("not_started", "Invalid tmux session name", null);
30
+ const fallbackAttach = attachCommand(this.socketPath, sessionName);
31
+ let stdout;
32
+ try {
33
+ ({ stdout } = await execute("tmux", [
34
+ ...(this.socketPath ? ["-S", this.socketPath] : []),
35
+ "new-session",
36
+ "-d",
37
+ "-P",
38
+ "-F",
39
+ "#{socket_path}",
40
+ "-s",
41
+ sessionName,
42
+ "-e",
43
+ `AHUB_TMUX_SESSION_NAME=${sessionName}`,
44
+ "-x",
45
+ "120",
46
+ "-y",
47
+ "40",
48
+ "-c",
49
+ directory,
50
+ ...command,
51
+ ], { timeout: 5_000 }));
52
+ }
53
+ catch (error) {
54
+ const uncertain = !!error &&
55
+ typeof error === "object" &&
56
+ (("killed" in error && error.killed) ||
57
+ ("code" in error && error.code === "timeout") ||
58
+ ("signal" in error && error.signal != null));
59
+ throw new SessionLaunchError(uncertain ? "unknown" : "not_started", error instanceof Error ? error.message : String(error), uncertain ? fallbackAttach : null);
60
+ }
61
+ const socketPath = stdout.replace(/\n$/, "");
62
+ if (!socketPath.startsWith("/") || socketPath.includes("\n")) {
63
+ throw new SessionLaunchError("unknown", "Invalid launched tmux target", fallbackAttach);
64
+ }
65
+ const target = { socketPath, sessionName };
66
+ try {
67
+ await execute("tmux", [
68
+ "-S",
69
+ socketPath,
70
+ "set-option",
71
+ "-p",
72
+ "-t",
73
+ tmuxTarget(sessionName),
74
+ "remain-on-exit",
75
+ "off",
76
+ ], { timeout: 5_000 });
77
+ return target;
78
+ }
79
+ catch (error) {
80
+ throw new SessionLaunchError("unknown", error instanceof Error ? error.message : String(error), attachCommand(socketPath, sessionName), target);
81
+ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=tmux-launcher.js.map
@@ -0,0 +1,107 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lstatSync } from "node:fs";
3
+ import { ownedSocket, tmuxTarget, validTmuxSessionName } from "./tmux-target.js";
4
+ /** One control client per socket/session; protocol payload is never interpreted as commands. */
5
+ export function createTmuxPreviewListener() {
6
+ const connections = new Map();
7
+ return (binding, change, fail) => {
8
+ if (!binding.socketPath.startsWith("/") || !validTmuxSessionName(binding.sessionName))
9
+ throw new Error("Invalid tmux binding");
10
+ const key = JSON.stringify([binding.socketPath, binding.sessionName]);
11
+ let connection = connections.get(key);
12
+ if (!connection) {
13
+ if (!ownedSocket(lstatSync(binding.socketPath)))
14
+ throw new Error("Unsafe tmux socket");
15
+ const child = spawn("tmux", [
16
+ "-S",
17
+ binding.socketPath,
18
+ "-C",
19
+ "attach-session",
20
+ "-f",
21
+ // tmux may use this client as the context for separate send commands.
22
+ // Marking it read-only also prevents those commands from writing.
23
+ "ignore-size",
24
+ "-E",
25
+ "-t",
26
+ tmuxTarget(binding.sessionName),
27
+ ], { stdio: ["pipe", "pipe", "pipe"] });
28
+ const subscribers = new Set();
29
+ let buffer = "";
30
+ let settled = false;
31
+ let stopped = false;
32
+ let resolveReady;
33
+ let rejectReady;
34
+ const ready = new Promise((resolve, reject) => {
35
+ resolveReady = resolve;
36
+ rejectReady = reject;
37
+ });
38
+ void ready.catch(() => { });
39
+ const timer = setTimeout(() => failure(new Error("tmux listener startup timed out")), 5000);
40
+ function failure(error) {
41
+ if (stopped)
42
+ return;
43
+ stopped = true;
44
+ clearTimeout(timer);
45
+ connections.delete(key);
46
+ rejectReady(error);
47
+ child.kill();
48
+ for (const subscriber of subscribers)
49
+ subscriber.fail(error);
50
+ subscribers.clear();
51
+ }
52
+ child.stdout.setEncoding("utf8");
53
+ child.stdout.on("data", (chunk) => {
54
+ buffer += chunk;
55
+ let end;
56
+ while ((end = buffer.indexOf("\n")) >= 0) {
57
+ const line = buffer.slice(0, end).replace(/\r$/, "");
58
+ buffer = buffer.slice(end + 1);
59
+ if (line.startsWith("%end ") && !settled) {
60
+ settled = true;
61
+ clearTimeout(timer);
62
+ resolveReady();
63
+ }
64
+ if (line.startsWith("%error") || line.startsWith("%exit")) {
65
+ failure(new Error("tmux control connection ended"));
66
+ return;
67
+ }
68
+ const output = /^%(?:output|extended-output) /.test(line);
69
+ if (output)
70
+ for (const subscriber of subscribers)
71
+ subscriber.change(true);
72
+ else if (/^%(?:layout-change|window-add|window-close|unlinked-window-close|sessions-changed|session-changed|session-window-changed|window-pane-changed|pane-mode-changed)\b/.test(line))
73
+ for (const subscriber of subscribers)
74
+ subscriber.change(false);
75
+ }
76
+ if (buffer.length > 2_000_000)
77
+ failure(new Error("tmux control line exceeds limit"));
78
+ });
79
+ child.stderr.resume();
80
+ child.on("error", failure);
81
+ child.stdin.on("error", failure);
82
+ child.on("close", () => failure(new Error("tmux control connection closed")));
83
+ connection = {
84
+ subscribe(change, fail) {
85
+ const subscriber = { change, fail };
86
+ subscribers.add(subscriber);
87
+ return {
88
+ ready,
89
+ close() {
90
+ subscribers.delete(subscriber);
91
+ if (!subscribers.size && !stopped) {
92
+ stopped = true;
93
+ clearTimeout(timer);
94
+ rejectReady(new Error("tmux control subscription closed"));
95
+ connections.delete(key);
96
+ child.kill();
97
+ }
98
+ },
99
+ };
100
+ },
101
+ };
102
+ connections.set(key, connection);
103
+ }
104
+ return connection.subscribe(change, fail);
105
+ };
106
+ }
107
+ //# sourceMappingURL=tmux-preview-listener.js.map