@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,151 @@
1
+ export const UPDATES_PROBE_INTERVAL_MS = 1_500;
2
+ export const UPDATES_WAIT_TIMEOUT_MS = 25_000;
3
+ export const MAX_UPDATES_WAITERS = 100;
4
+ export const MAX_UPDATES_WAITERS_PER_SESSION = 10;
5
+ export class SessionUpdatesError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = "SessionUpdatesError";
11
+ }
12
+ }
13
+ /**
14
+ * Bounded long-poll waiting for message reads. Plain (non-waiting) reads bypass
15
+ * this service and call the session reader directly.
16
+ */
17
+ export class SessionUpdatesService {
18
+ sessions;
19
+ #probeIntervalMs;
20
+ #waitTimeoutMs;
21
+ #maxWaiters;
22
+ #maxWaitersPerSession;
23
+ #groups = new Map();
24
+ #waiterCount = 0;
25
+ #closed = false;
26
+ constructor(sessions, options = {}) {
27
+ this.sessions = sessions;
28
+ this.#probeIntervalMs = options.probeIntervalMs ?? UPDATES_PROBE_INTERVAL_MS;
29
+ this.#waitTimeoutMs = options.waitTimeoutMs ?? UPDATES_WAIT_TIMEOUT_MS;
30
+ this.#maxWaiters = options.maxWaiters ?? MAX_UPDATES_WAITERS;
31
+ this.#maxWaitersPerSession = options.maxWaitersPerSession ?? MAX_UPDATES_WAITERS_PER_SESSION;
32
+ }
33
+ async wait(sessionId, query, signal) {
34
+ if (this.#closed || signal?.aborted)
35
+ throw abortError();
36
+ let group = this.#groups.get(sessionId);
37
+ if (this.#waiterCount >= this.#maxWaiters ||
38
+ (group?.waiters.size ?? 0) >= this.#maxWaitersPerSession)
39
+ throw new SessionUpdatesError("updates_capacity_exceeded", "Too many automatic update requests are already waiting");
40
+ if (!group) {
41
+ group = { waiters: new Set(), pending: null };
42
+ this.#groups.set(sessionId, group);
43
+ }
44
+ const current = group;
45
+ this.#waiterCount++;
46
+ return new Promise((resolve, reject) => {
47
+ let settled = false;
48
+ const cleanup = () => {
49
+ settled = true;
50
+ clearTimeout(timeout);
51
+ signal?.removeEventListener("abort", abort);
52
+ current.waiters.delete(waiter);
53
+ this.#waiterCount--;
54
+ if (current.waiters.size === 0) {
55
+ clearTimeout(current.timer);
56
+ if (this.#groups.get(sessionId) === current)
57
+ this.#groups.delete(sessionId);
58
+ }
59
+ };
60
+ const waiter = {
61
+ query,
62
+ finish: (value) => {
63
+ if (!settled) {
64
+ cleanup();
65
+ resolve(value);
66
+ }
67
+ },
68
+ fail: (error) => {
69
+ if (!settled) {
70
+ cleanup();
71
+ reject(error);
72
+ }
73
+ },
74
+ };
75
+ const abort = () => waiter.fail(abortError());
76
+ const timeout = setTimeout(() => waiter.finish(null), this.#waitTimeoutMs);
77
+ current.waiters.add(waiter);
78
+ signal?.addEventListener("abort", abort, { once: true });
79
+ // Only in-flight initial reads are reused, never completed runtime results.
80
+ void this.#read(sessionId, current).then((read) => {
81
+ if (current.waiters.has(waiter))
82
+ this.#evaluate(waiter, read);
83
+ this.#schedule(sessionId, current);
84
+ }, (error) => waiter.fail(error));
85
+ });
86
+ }
87
+ #evaluate(waiter, read) {
88
+ try {
89
+ const result = read(waiter.query);
90
+ if (waiter.query.stateRevision === undefined ||
91
+ result.stateRevision !== waiter.query.stateRevision ||
92
+ result.items.length > 0 ||
93
+ result.hasMore)
94
+ waiter.finish(result);
95
+ }
96
+ catch (error) {
97
+ waiter.fail(error);
98
+ }
99
+ }
100
+ #read(sessionId, group) {
101
+ if (group.pending)
102
+ return group.pending;
103
+ const pending = this.sessions.readMessages(sessionId).then((read) => {
104
+ if (!read)
105
+ throw new SessionUpdatesError("session_not_found", "Session not found");
106
+ return read.page;
107
+ });
108
+ group.pending = pending;
109
+ const clearPending = () => {
110
+ group.pending = null;
111
+ };
112
+ void pending.then(clearPending, clearPending);
113
+ return pending;
114
+ }
115
+ #schedule(sessionId, group) {
116
+ if (this.#closed || !group.waiters.size || group.timer !== undefined || group.pending)
117
+ return;
118
+ group.timer = setTimeout(async () => {
119
+ group.timer = undefined;
120
+ // Requests joining during this read share its immutable snapshot.
121
+ const waiters = [...group.waiters];
122
+ try {
123
+ const read = await this.#read(sessionId, group);
124
+ for (const waiter of waiters)
125
+ if (group.waiters.has(waiter))
126
+ this.#evaluate(waiter, read);
127
+ }
128
+ catch (error) {
129
+ for (const waiter of waiters)
130
+ waiter.fail(error);
131
+ }
132
+ finally {
133
+ this.#schedule(sessionId, group);
134
+ }
135
+ }, this.#probeIntervalMs);
136
+ }
137
+ close() {
138
+ if (this.#closed)
139
+ return;
140
+ this.#closed = true;
141
+ for (const group of this.#groups.values()) {
142
+ clearTimeout(group.timer);
143
+ for (const waiter of group.waiters)
144
+ waiter.fail(abortError());
145
+ }
146
+ }
147
+ }
148
+ function abortError() {
149
+ return new DOMException("The operation was aborted", "AbortError");
150
+ }
151
+ //# sourceMappingURL=session-updates-service.js.map
@@ -0,0 +1,22 @@
1
+ import { createHash } from "node:crypto";
2
+ export const createUpdateRevision = ({ updatedAt, ...session }, interaction) => createHash("sha256")
3
+ .update(canonicalJson({ session, interaction }))
4
+ .digest("base64url");
5
+ export function withUpdateRevision(value, createRevision) {
6
+ return {
7
+ ...value,
8
+ stateRevision: createRevision(value.session, value.interaction),
9
+ };
10
+ }
11
+ function canonicalJson(value) {
12
+ if (Array.isArray(value))
13
+ return `[${value.map(canonicalJson).join(",")}]`;
14
+ if (value !== null && typeof value === "object") {
15
+ return `{${Object.entries(value)
16
+ .sort(([left], [right]) => left.localeCompare(right))
17
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
18
+ .join(",")}}`;
19
+ }
20
+ return JSON.stringify(value);
21
+ }
22
+ //# sourceMappingURL=update-revision.js.map
@@ -0,0 +1,304 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
3
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
4
+ import sharp from "sharp";
5
+ import { supportsUploadThumbnail } from "../../shared/uploads.js";
6
+ import { isOpaqueId } from "../security/opaque-id.js";
7
+ const ATTACHMENT_ID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
8
+ export function validAttachmentId(id) {
9
+ return ATTACHMENT_ID_PATTERN.test(id);
10
+ }
11
+ export class UploadStore {
12
+ directory;
13
+ constructor(directory) {
14
+ this.directory = directory;
15
+ }
16
+ thumbnails = new Map();
17
+ thumbnailQueue = Promise.resolve();
18
+ async thumbnail(id, attachmentId) {
19
+ const attachment = await this.get(id, attachmentId);
20
+ if (!attachment || !supportsUploadThumbnail(attachment.type))
21
+ return null;
22
+ const path = join(this.folder(id), attachmentId + ".thumb.webp");
23
+ const existing = this.thumbnails.get(path);
24
+ if (existing)
25
+ return existing;
26
+ const task = (async () => {
27
+ await this.safeFile(path, this.folder(id));
28
+ try {
29
+ return await readFile(path);
30
+ }
31
+ catch (error) {
32
+ if (error.code !== "ENOENT")
33
+ throw error;
34
+ }
35
+ const generation = this.thumbnailQueue.then(async () => {
36
+ // Revalidate after waiting in the queue: deletion must not resurrect files.
37
+ const current = await this.get(id, attachmentId);
38
+ if (!current)
39
+ return null;
40
+ let data;
41
+ try {
42
+ data = await sharp(current.path, { limitInputPixels: 40_000_000 })
43
+ .rotate()
44
+ .resize(360, 280, { fit: "inside", withoutEnlargement: true })
45
+ .webp({ quality: 80 })
46
+ .toBuffer();
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ const temporary = path + ".pending";
52
+ try {
53
+ await writeFile(temporary, data, { mode: 0o600, flag: "wx" });
54
+ await rename(temporary, path);
55
+ }
56
+ finally {
57
+ await rm(temporary, { force: true });
58
+ }
59
+ return data;
60
+ });
61
+ this.thumbnailQueue = generation.catch(() => undefined);
62
+ return generation;
63
+ })();
64
+ this.thumbnails.set(path, task);
65
+ try {
66
+ return await task;
67
+ }
68
+ finally {
69
+ if (this.thumbnails.get(path) === task)
70
+ this.thumbnails.delete(path);
71
+ }
72
+ }
73
+ async waitForThumbnails(id, attachmentId) {
74
+ const folder = this.folder(id);
75
+ await Promise.allSettled([...this.thumbnails]
76
+ .filter(([path]) => attachmentId
77
+ ? path === join(folder, attachmentId + ".thumb.webp")
78
+ : dirname(path) === folder)
79
+ .map(([, task]) => task));
80
+ }
81
+ folder(id) {
82
+ if (!isOpaqueId(id))
83
+ throw new Error("Invalid session ID");
84
+ return resolve(this.directory, id);
85
+ }
86
+ async cleanup(id) {
87
+ const folder = this.folder(id);
88
+ await this.safeFolder(folder);
89
+ const tombstone = folder + ".deleting";
90
+ await this.safeFolder(tombstone);
91
+ await rename(folder, tombstone).catch((error) => {
92
+ if (error.code !== "ENOENT")
93
+ throw error;
94
+ });
95
+ await this.waitForThumbnails(id);
96
+ await rm(tombstone, { recursive: true, force: true });
97
+ }
98
+ async upload(id, filename, type, chunks) {
99
+ const folder = this.folder(id);
100
+ await mkdir(folder, { recursive: true, mode: 0o700 });
101
+ await this.safeFolder(folder);
102
+ const attachmentId = randomUUID();
103
+ const extension = extname(filename);
104
+ const path = join(folder, attachmentId + (/^\.[a-zA-Z0-9]{1,16}$/.test(extension) ? extension : ""));
105
+ const file = await open(path, "wx", 0o600);
106
+ let complete = false;
107
+ try {
108
+ let size = 0;
109
+ for await (const chunk of chunks) {
110
+ size += chunk.byteLength;
111
+ await file.writeFile(chunk);
112
+ }
113
+ await file.sync();
114
+ await file.close();
115
+ const attachment = {
116
+ id: attachmentId,
117
+ sessionId: id,
118
+ filename,
119
+ size,
120
+ type,
121
+ uploadedAt: new Date().toISOString(),
122
+ path,
123
+ };
124
+ const temporary = join(folder, attachmentId + ".meta.pending");
125
+ await writeFile(temporary, JSON.stringify(attachment), { mode: 0o600 });
126
+ await rename(temporary, join(folder, attachmentId + ".meta.json"));
127
+ complete = true;
128
+ if (supportsUploadThumbnail(type)) {
129
+ void this.thumbnail(id, attachmentId).catch(() => undefined);
130
+ }
131
+ return attachment;
132
+ }
133
+ finally {
134
+ if (!complete) {
135
+ await file.close().catch(() => undefined);
136
+ await rm(path, { force: true });
137
+ await rm(join(folder, attachmentId + ".meta.pending"), { force: true });
138
+ }
139
+ }
140
+ }
141
+ async initialize() {
142
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
143
+ for (const folder of await readdir(this.directory, { withFileTypes: true })) {
144
+ if (!folder.isDirectory())
145
+ continue;
146
+ try {
147
+ if (folder.name.endsWith(".deleting") && isOpaqueId(folder.name.slice(0, -9))) {
148
+ await this.safeFolder(join(this.directory, folder.name));
149
+ await rm(join(this.directory, folder.name), { recursive: true, force: true });
150
+ }
151
+ else if (isOpaqueId(folder.name)) {
152
+ await this.recover(join(this.directory, folder.name));
153
+ }
154
+ }
155
+ catch (error) {
156
+ // Recovery is best effort and must not prevent startup.
157
+ console.error("Upload recovery failed", error);
158
+ }
159
+ }
160
+ }
161
+ async recover(folder) {
162
+ await this.safeFolder(folder);
163
+ const names = await readdir(folder);
164
+ for (const name of names) {
165
+ if (name.endsWith(".meta.deleting")) {
166
+ await this.finishDeletion(join(folder, name));
167
+ }
168
+ else if (name.endsWith(".thumb.webp.pending") ||
169
+ (name.endsWith(".thumb.webp") && !names.includes(name.slice(0, 36) + ".meta.json")) ||
170
+ name.endsWith(".meta.pending") ||
171
+ (/^[a-f0-9-]{36}(?:\.[a-zA-Z0-9]{1,16})?$/.test(name) &&
172
+ !names.includes(name.slice(0, 36) + ".meta.json") &&
173
+ !names.includes(name.slice(0, 36) + ".meta.deleting"))) {
174
+ await rm(join(folder, name), { force: true });
175
+ }
176
+ }
177
+ }
178
+ async list(id) {
179
+ const folder = this.folder(id);
180
+ await this.safeFolder(folder);
181
+ const names = await readdir(folder).catch((error) => {
182
+ if (error.code === "ENOENT")
183
+ return [];
184
+ throw error;
185
+ });
186
+ const entries = [];
187
+ for (const name of names) {
188
+ if (!name.endsWith(".meta.json"))
189
+ continue;
190
+ try {
191
+ entries.push(await this.metadata(join(folder, name), id, name.slice(0, -10)));
192
+ }
193
+ catch (error) {
194
+ if (error.code !== "ENOENT")
195
+ throw error;
196
+ }
197
+ }
198
+ entries.sort((a, b) => b.uploadedAt.localeCompare(a.uploadedAt) || b.id.localeCompare(a.id));
199
+ return entries;
200
+ }
201
+ async get(id, attachmentId) {
202
+ const folder = this.folder(id);
203
+ if (!validAttachmentId(attachmentId))
204
+ throw new Error("Invalid attachment ID");
205
+ try {
206
+ return await this.metadata(join(folder, attachmentId + ".meta.json"), id, attachmentId);
207
+ }
208
+ catch (error) {
209
+ if (error.code === "ENOENT")
210
+ return null;
211
+ throw error;
212
+ }
213
+ }
214
+ async delete(id, attachmentId) {
215
+ const folder = this.folder(id);
216
+ if (!validAttachmentId(attachmentId))
217
+ throw new Error("Invalid attachment ID");
218
+ await this.get(id, attachmentId);
219
+ const metadata = join(folder, attachmentId + ".meta.json");
220
+ const tombstone = join(folder, attachmentId + ".meta.deleting");
221
+ await rename(metadata, tombstone).catch((error) => {
222
+ if (error.code !== "ENOENT")
223
+ throw error;
224
+ });
225
+ await this.waitForThumbnails(id, attachmentId);
226
+ await this.finishDeletion(tombstone);
227
+ }
228
+ async safeFolder(folder) {
229
+ let root;
230
+ try {
231
+ root = await realpath(this.directory);
232
+ }
233
+ catch (error) {
234
+ if (error.code === "ENOENT")
235
+ return;
236
+ throw error;
237
+ }
238
+ if (dirname(resolve(folder)) !== resolve(this.directory))
239
+ throw new Error("Invalid upload directory");
240
+ try {
241
+ const stat = await lstat(folder);
242
+ if (!stat.isDirectory() || stat.isSymbolicLink() || dirname(await realpath(folder)) !== root)
243
+ throw new Error("Unsafe upload directory");
244
+ }
245
+ catch (error) {
246
+ if (error.code !== "ENOENT")
247
+ throw error;
248
+ }
249
+ }
250
+ async safeFile(path, folder) {
251
+ await this.safeFolder(folder);
252
+ if (dirname(resolve(path)) !== resolve(folder))
253
+ throw new Error("Invalid upload path");
254
+ try {
255
+ const stat = await lstat(path);
256
+ if (!stat.isFile() ||
257
+ stat.isSymbolicLink() ||
258
+ dirname(await realpath(path)) !== (await realpath(folder)))
259
+ throw new Error("Unsafe upload file");
260
+ }
261
+ catch (error) {
262
+ if (error.code !== "ENOENT")
263
+ throw error;
264
+ }
265
+ }
266
+ async metadata(path, id, attachmentId) {
267
+ const folder = this.folder(id);
268
+ if (!validAttachmentId(attachmentId))
269
+ throw new Error("Invalid attachment metadata ID");
270
+ await this.safeFile(path, folder);
271
+ const value = JSON.parse(await readFile(path, "utf8"));
272
+ if (!value ||
273
+ value.id !== attachmentId ||
274
+ value.sessionId !== id ||
275
+ typeof value.filename !== "string" ||
276
+ typeof value.type !== "string" ||
277
+ !Number.isSafeInteger(value.size) ||
278
+ value.size < 0 ||
279
+ typeof value.uploadedAt !== "string" ||
280
+ !Number.isFinite(Date.parse(value.uploadedAt)) ||
281
+ typeof value.path !== "string" ||
282
+ !isAbsolute(value.path) ||
283
+ dirname(resolve(value.path)) !== folder ||
284
+ !new RegExp("^" + attachmentId + "(?:\\.[a-zA-Z0-9]{1,16})?$").test(basename(value.path)))
285
+ throw new Error("Corrupt upload metadata");
286
+ await this.safeFile(value.path, folder);
287
+ return value;
288
+ }
289
+ async finishDeletion(tombstone) {
290
+ let attachment;
291
+ try {
292
+ attachment = await this.metadata(tombstone, basename(dirname(tombstone)), basename(tombstone).replace(/\.meta\.deleting$/, ""));
293
+ }
294
+ catch (error) {
295
+ if (error.code === "ENOENT")
296
+ return;
297
+ throw error;
298
+ }
299
+ await rm(attachment.path, { force: true });
300
+ await rm(join(dirname(tombstone), attachment.id + ".thumb.webp"), { force: true });
301
+ await rm(tombstone, { force: true });
302
+ }
303
+ }
304
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,21 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export async function packageVersion() {
3
+ let directory = new URL("./", import.meta.url);
4
+ while (true) {
5
+ try {
6
+ const manifest = JSON.parse(await readFile(new URL("package.json", directory), "utf8"));
7
+ if (manifest.name === "@someonegg/agent-hub" && typeof manifest.version === "string") {
8
+ return manifest.version;
9
+ }
10
+ }
11
+ catch (error) {
12
+ if (error.code !== "ENOENT")
13
+ throw error;
14
+ }
15
+ const parent = new URL("../", directory);
16
+ if (parent.href === directory.href)
17
+ throw new Error("Cannot locate the Hub package version");
18
+ directory = parent;
19
+ }
20
+ }
21
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,2 @@
1
+ export * from "./interaction.js";
2
+ //# sourceMappingURL=api-contract.js.map
@@ -0,0 +1,158 @@
1
+ const pathEscapes = {
2
+ a: "\x07",
3
+ b: "\b",
4
+ f: "\f",
5
+ v: "\v",
6
+ n: "\n",
7
+ t: "\t",
8
+ r: "\r",
9
+ };
10
+ /** Git quotes paths as C strings, including octal-escaped UTF-8 bytes. */
11
+ function decodePath(value) {
12
+ if (!value.startsWith('"'))
13
+ return value;
14
+ const bytes = [];
15
+ const inner = value.slice(1, value.lastIndexOf('"'));
16
+ for (let i = 0; i < inner.length; i++) {
17
+ if (inner[i] === "\\") {
18
+ const octal = /^[0-7]{1,3}/.exec(inner.slice(i + 1));
19
+ if (octal) {
20
+ bytes.push(parseInt(octal[0], 8));
21
+ i += octal[0].length;
22
+ continue;
23
+ }
24
+ const escape = inner[++i];
25
+ bytes.push(...new TextEncoder().encode(pathEscapes[escape] ?? escape));
26
+ }
27
+ else {
28
+ const point = String.fromCodePoint(inner.codePointAt(i));
29
+ bytes.push(...new TextEncoder().encode(point));
30
+ i += point.length - 1;
31
+ }
32
+ }
33
+ return new TextDecoder().decode(new Uint8Array(bytes));
34
+ }
35
+ function pathName(value) {
36
+ return decodePath(value).replace(/^[ab]\//, "");
37
+ }
38
+ /** Only repeated paths can be inferred reliably without extended headers. */
39
+ function headerPath(value) {
40
+ const quoted = /^"(?:[^"\\]|\\.)*" /.exec(value);
41
+ let oldPath;
42
+ let newPath;
43
+ if (quoted) {
44
+ oldPath = value.slice(0, quoted[0].length - 1);
45
+ newPath = value.slice(quoted[0].length);
46
+ }
47
+ else {
48
+ const quotedNew = / ("(?:[^"\\]|\\.)*")$/.exec(value);
49
+ // Unquoted, identical paths have equal lengths, regardless of embedded spaces.
50
+ const separator = quotedNew?.index ?? (value.length - 1) / 2;
51
+ if (!Number.isInteger(separator) || value[separator] !== " ")
52
+ return null;
53
+ oldPath = value.slice(0, separator);
54
+ newPath = value.slice(separator + 1);
55
+ }
56
+ const oldName = decodePath(oldPath);
57
+ const newName = decodePath(newPath);
58
+ return oldName.startsWith("a/") &&
59
+ newName.startsWith("b/") &&
60
+ oldName.slice(2) === newName.slice(2)
61
+ ? newName.slice(2)
62
+ : null;
63
+ }
64
+ export function parseDiff(raw) {
65
+ const files = [];
66
+ let file;
67
+ let pathResolved = false;
68
+ let oldLine = 0;
69
+ let newLine = 0;
70
+ let oldRemaining = 0;
71
+ let newRemaining = 0;
72
+ let inHunk = false;
73
+ let warning = false;
74
+ const finishHunk = () => {
75
+ if (oldRemaining || newRemaining)
76
+ warning = true;
77
+ inHunk = false;
78
+ oldRemaining = newRemaining = 0;
79
+ };
80
+ const finishFile = () => {
81
+ finishHunk();
82
+ if (file && !pathResolved)
83
+ warning = true;
84
+ };
85
+ const lines = raw.split("\n");
86
+ if (lines.at(-1) === "")
87
+ lines.pop();
88
+ for (const text of lines) {
89
+ if (text.startsWith("diff --git ")) {
90
+ finishFile();
91
+ const path = headerPath(text.slice(11));
92
+ pathResolved = path !== null;
93
+ file = {
94
+ path: path ?? text.slice(11),
95
+ lines: [],
96
+ additions: 0,
97
+ deletions: 0,
98
+ };
99
+ files.push(file);
100
+ }
101
+ if (!file) {
102
+ if (text.trim())
103
+ warning = true;
104
+ continue;
105
+ }
106
+ const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(text);
107
+ if (hunk) {
108
+ finishHunk();
109
+ oldLine = Number(hunk[1]);
110
+ newLine = Number(hunk[3]);
111
+ oldRemaining = Number(hunk[2] ?? 1);
112
+ newRemaining = Number(hunk[4] ?? 1);
113
+ inHunk = true;
114
+ file.lines.push({ text, kind: "meta" });
115
+ continue;
116
+ }
117
+ if (inHunk && (oldRemaining || newRemaining) && /^[ +\-]/.test(text)) {
118
+ const kind = text[0] === "+" ? "add" : text[0] === "-" ? "remove" : "context";
119
+ const line = { text: text.slice(1), kind };
120
+ if (kind !== "add") {
121
+ line.old = oldLine++;
122
+ oldRemaining--;
123
+ }
124
+ if (kind !== "remove") {
125
+ line.next = newLine++;
126
+ newRemaining--;
127
+ }
128
+ if (oldRemaining < 0 || newRemaining < 0)
129
+ warning = true;
130
+ if (kind === "add")
131
+ file.additions++;
132
+ if (kind === "remove")
133
+ file.deletions++;
134
+ file.lines.push(line);
135
+ continue;
136
+ }
137
+ if (/^(\+\+\+|---) /.test(text)) {
138
+ const path = text.slice(4);
139
+ if (path !== "/dev/null") {
140
+ file.path = pathName(path.replace(/\t$/, ""));
141
+ pathResolved = true;
142
+ }
143
+ }
144
+ const renamed = /^(?:rename|copy) to (.*)$/.exec(text);
145
+ if (renamed) {
146
+ file.path = decodePath(renamed[1]);
147
+ pathResolved = true;
148
+ }
149
+ if (!/^(diff --git |index |--- |\+\+\+ |new file mode |deleted file mode |old mode |new mode |similarity index |dissimilarity index |rename from |rename to |copy from |copy to |Binary files .* differ$|\$)/.test(text))
150
+ warning = true;
151
+ if (inHunk && /^[+\- ]/.test(text))
152
+ warning = true;
153
+ file.lines.push({ text, kind: "meta" });
154
+ }
155
+ finishFile();
156
+ return { files, warning: warning || files.length === 0 };
157
+ }
158
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1,22 @@
1
+ export const MAX_INTERACTION_MESSAGE_BYTES = 256 * 1024;
2
+ export const MAX_INTERACTION_KEY_SEQUENCE_LENGTH = 64;
3
+ export const INTERACTION_KEYS = [
4
+ "enter",
5
+ "up",
6
+ "down",
7
+ "left",
8
+ "right",
9
+ "interrupt",
10
+ "BTab",
11
+ "0",
12
+ "1",
13
+ "2",
14
+ "3",
15
+ "4",
16
+ "5",
17
+ "6",
18
+ "7",
19
+ "8",
20
+ "9",
21
+ ];
22
+ //# sourceMappingURL=interaction.js.map