@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,91 @@
1
+ import { createClaudeSessionSource } from "./adapters/claude/claude-session-source.js";
2
+ import { createClaudeSessionManager } from "./adapters/claude/session-manager.js";
3
+ import { createCodexSessionSource } from "./adapters/codex/codex-session-source.js";
4
+ import { createCodexSessionManager } from "./adapters/codex/session-manager.js";
5
+ import { CatalogInteractionSessionReader } from "./application/interaction-session-reader.js";
6
+ import { SessionReadService } from "./application/session-read-service.js";
7
+ import { SessionCatalogService } from "./catalog/session-catalog-service.js";
8
+ import { SessionInteractionService } from "./interaction/interaction-service.js";
9
+ import { PaneBindingManager } from "./interaction/pane-binding-manager.js";
10
+ import { SessionRuntime } from "./interaction/session-runtime.js";
11
+ import { TmuxInteractionService } from "./interaction/tmux-service.js";
12
+ import { OperationService } from "./management/operation-service.js";
13
+ import { managementCapabilities, SessionManagerRegistry, } from "./management/session-manager.js";
14
+ import { CatalogSnapshotStore } from "./repository/catalog-snapshot-store.js";
15
+ import { SessionUpdatesService } from "./updates/session-updates-service.js";
16
+ /** Compose reading and management using the adapter's published Agent nickname. */
17
+ export async function createAhub({ agents, socket, claudeReadyScript, interactionEnabled = true, uploads, }) {
18
+ const paneBindings = new PaneBindingManager();
19
+ const adapters = await Promise.all(agents.map((agent) => createAdapter(agent, socket, claudeReadyScript)));
20
+ const sources = adapters.map((adapter) => adapter.source);
21
+ const managers = new SessionManagerRegistry(sources[0].descriptor.agent);
22
+ for (const { source, manager } of adapters)
23
+ managers.register(source.descriptor.agent, manager);
24
+ const transport = new TmuxInteractionService();
25
+ const runtime = new SessionRuntime(transport, managers, paneBindings);
26
+ const store = new CatalogSnapshotStore(sources, undefined, undefined, {
27
+ publishBindings: (events) => {
28
+ for (const event of events)
29
+ paneBindings.submit(event);
30
+ },
31
+ });
32
+ const interaction = new SessionInteractionService(new CatalogInteractionSessionReader(store), transport, {
33
+ enabled: interactionEnabled,
34
+ bindings: paneBindings,
35
+ runtime,
36
+ });
37
+ const sessions = new SessionReadService(sources, {
38
+ runtime,
39
+ store,
40
+ interaction,
41
+ resolveDisplayTitle: (session) => sources
42
+ .find((source) => source.descriptor.agent === session.origin.agent)
43
+ ?.displayTitleFor?.(session) ?? null,
44
+ agents: sources.map((source) => ({
45
+ nickname: source.descriptor.agent,
46
+ runtimeName: source.descriptor.displayName,
47
+ management: managementCapabilities(managers.get(source.descriptor.agent)),
48
+ })),
49
+ });
50
+ const cleanup = [() => interaction.close()];
51
+ let closed = false;
52
+ const close = () => {
53
+ if (closed)
54
+ return;
55
+ closed = true;
56
+ for (const release of cleanup)
57
+ release();
58
+ };
59
+ try {
60
+ const catalog = new SessionCatalogService(sessions);
61
+ cleanup.push(() => catalog.close());
62
+ const updates = new SessionUpdatesService(sessions);
63
+ cleanup.push(() => updates.close());
64
+ const operations = new OperationService(sessions, interaction, managers, {
65
+ bindings: paneBindings,
66
+ runtime,
67
+ uploads,
68
+ });
69
+ cleanup.push(() => operations.close());
70
+ return { sessions, managers, interaction, catalog, updates, operations, close };
71
+ }
72
+ catch (error) {
73
+ close();
74
+ throw error;
75
+ }
76
+ }
77
+ async function createAdapter(agent, socket, claudeReadyScript) {
78
+ switch (agent.adapter ?? "codex") {
79
+ case "claude":
80
+ return {
81
+ source: await createClaudeSessionSource(agent.path, { agent: agent.nickname }),
82
+ manager: createClaudeSessionManager(agent.path, socket, claudeReadyScript),
83
+ };
84
+ case "codex":
85
+ return {
86
+ source: await createCodexSessionSource(agent.path, { agent: agent.nickname }),
87
+ manager: createCodexSessionManager(agent.path, socket),
88
+ };
89
+ }
90
+ }
91
+ //# sourceMappingURL=create-hub.js.map
@@ -0,0 +1,65 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ /** A complete owner directory is atomically installed; stale owners move to permanent tombstones.
5
+ * Keeping tombstones prevents a second reclaimer from renaming a new owner's lock. */
6
+ export class DirectoryLock {
7
+ token = randomUUID();
8
+ attempt;
9
+ target;
10
+ held = false;
11
+ constructor(root, name) {
12
+ mkdirSync(root, { recursive: true });
13
+ this.target = join(root, name);
14
+ this.attempt = join(root, `${name}-attempt-${this.token}`);
15
+ mkdirSync(this.attempt);
16
+ writeFileSync(join(this.attempt, "owner.json"), JSON.stringify({ pid: process.pid, token: this.token }));
17
+ }
18
+ acquire() {
19
+ for (;;) {
20
+ try {
21
+ renameSync(this.attempt, this.target);
22
+ this.held = true;
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ if (!["EEXIST", "ENOTEMPTY"].includes(String(error.code)))
27
+ throw error;
28
+ }
29
+ let owner;
30
+ try {
31
+ owner = JSON.parse(readFileSync(join(this.target, "owner.json"), "utf8"));
32
+ }
33
+ catch (error) {
34
+ if (error.code === "ENOENT")
35
+ continue;
36
+ throw error;
37
+ }
38
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0 || !/^[a-f0-9-]{36}$/.test(owner.token))
39
+ throw new Error("Invalid directory lock owner");
40
+ try {
41
+ process.kill(owner.pid, 0);
42
+ return false;
43
+ }
44
+ catch (error) {
45
+ if (error.code !== "ESRCH")
46
+ throw error;
47
+ }
48
+ try {
49
+ renameSync(this.target, `${this.target}-dead-${owner.token}`);
50
+ }
51
+ catch (error) {
52
+ if (!["ENOENT", "EEXIST", "ENOTEMPTY"].includes(String(error.code)))
53
+ throw error;
54
+ }
55
+ }
56
+ }
57
+ close() {
58
+ if (this.held) {
59
+ renameSync(this.target, this.attempt);
60
+ this.held = false;
61
+ }
62
+ rmSync(this.attempt, { recursive: true, force: true });
63
+ }
64
+ }
65
+ //# sourceMappingURL=directory-lock.js.map
@@ -0,0 +1,5 @@
1
+ import { isAbsolute } from "node:path";
2
+ export function validProjectDirectory(value) {
3
+ return typeof value === "string" && isAbsolute(value) && !/[\0\r\n]/.test(value);
4
+ }
5
+ //# sourceMappingURL=project-directory.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-domain.js.map
@@ -0,0 +1,45 @@
1
+ import { fromMarkdown } from "mdast-util-from-markdown";
2
+ import { toString } from "mdast-util-to-string";
3
+ export const MAX_PREVIEW_CHARS = 256;
4
+ export const MAX_SESSION_TITLE_CHARS = 80;
5
+ const MAX_TITLE_HEADING_DEPTH = 64;
6
+ export function truncateText(value, limit) {
7
+ if (value.length <= limit)
8
+ return { text: value, truncated: false };
9
+ let end = limit;
10
+ const previous = value.charCodeAt(end - 1);
11
+ const next = value.charCodeAt(end);
12
+ const splitsSurrogatePair = previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff;
13
+ if (splitsSurrogatePair)
14
+ end -= 1;
15
+ return { text: value.slice(0, end), truncated: true };
16
+ }
17
+ export function normalizeSessionTitle(value) {
18
+ if (value === null)
19
+ return null;
20
+ const firstLine = value
21
+ .split(/\r?\n/)
22
+ .map((line) => line.trim())
23
+ .find(Boolean);
24
+ return firstLine === undefined ? null : truncateText(firstLine, MAX_SESSION_TITLE_CHARS).text;
25
+ }
26
+ export function sessionTitleFromMarkdown(value) {
27
+ const heading = firstTitleHeading(fromMarkdown(value), 0);
28
+ return normalizeSessionTitle(heading ?? value);
29
+ }
30
+ function firstTitleHeading(node, depth) {
31
+ if (node.type === "heading" && (node.depth === 1 || node.depth === 2)) {
32
+ const text = toString(node).trim();
33
+ if (text.length > 0)
34
+ return text;
35
+ }
36
+ if (depth >= MAX_TITLE_HEADING_DEPTH || !("children" in node))
37
+ return null;
38
+ for (const child of node.children) {
39
+ const heading = firstTitleHeading(child, depth + 1);
40
+ if (heading !== null)
41
+ return heading;
42
+ }
43
+ return null;
44
+ }
45
+ //# sourceMappingURL=session-text.js.map
@@ -0,0 +1,103 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { HostingStore } from "./store.js";
5
+ import { ResourceValidationError } from "./validation-error.js";
6
+ export const DIFF_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
7
+ export class DiffStore {
8
+ hosting;
9
+ constructor(root) {
10
+ this.hosting = new HostingStore(root);
11
+ }
12
+ directory(id) {
13
+ return join(this.hosting.root, "diffs", id);
14
+ }
15
+ async metadata(id) {
16
+ if (!DIFF_ID.test(id))
17
+ return null;
18
+ try {
19
+ const directory = this.directory(id);
20
+ const metadata = JSON.parse(await readFile(join(directory, "metadata.json"), "utf8"));
21
+ if (metadata.id !== id ||
22
+ metadata.url !== `/diffs/${id}` ||
23
+ typeof metadata.title !== "string" ||
24
+ !Number.isFinite(Date.parse(metadata.createdAt)))
25
+ throw new Error("Invalid diff metadata");
26
+ return metadata;
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return null;
31
+ throw error;
32
+ }
33
+ }
34
+ async exists(id) {
35
+ if (!(await this.metadata(id)))
36
+ return false;
37
+ try {
38
+ return (await lstat(join(this.directory(id), "raw.diff"))).isFile();
39
+ }
40
+ catch (error) {
41
+ if (error.code === "ENOENT")
42
+ return false;
43
+ throw error;
44
+ }
45
+ }
46
+ async get(id) {
47
+ const metadata = await this.metadata(id);
48
+ if (!metadata)
49
+ return null;
50
+ try {
51
+ return { ...metadata, raw: await readFile(join(this.directory(id), "raw.diff"), "utf8") };
52
+ }
53
+ catch (error) {
54
+ if (error.code === "ENOENT")
55
+ return null;
56
+ throw error;
57
+ }
58
+ }
59
+ async list() {
60
+ let names;
61
+ try {
62
+ names = await readdir(join(this.hosting.root, "diffs"));
63
+ }
64
+ catch (error) {
65
+ if (error.code === "ENOENT")
66
+ return [];
67
+ throw error;
68
+ }
69
+ const entries = await Promise.all(names.filter((id) => DIFF_ID.test(id)).map((id) => this.metadata(id)));
70
+ return entries.filter((entry) => entry !== null);
71
+ }
72
+ async publish(file, title) {
73
+ if (!(await lstat(file)).isFile())
74
+ throw new ResourceValidationError("Diff source must be a regular file");
75
+ const raw = await readFile(file);
76
+ return this.hosting.locked(async () => {
77
+ const id = randomUUID();
78
+ const metadata = {
79
+ id,
80
+ title: title ?? basename(file),
81
+ createdAt: new Date().toISOString(),
82
+ url: `/diffs/${id}`,
83
+ };
84
+ const temporary = this.directory(`.tmp-${id}`);
85
+ await mkdir(temporary, { recursive: true });
86
+ try {
87
+ await writeFile(join(temporary, "raw.diff"), raw);
88
+ await writeFile(join(temporary, "metadata.json"), JSON.stringify(metadata));
89
+ await rename(temporary, this.directory(id));
90
+ }
91
+ finally {
92
+ await rm(temporary, { recursive: true, force: true });
93
+ }
94
+ return metadata;
95
+ });
96
+ }
97
+ async delete(id) {
98
+ if (!DIFF_ID.test(id))
99
+ throw new ResourceValidationError("Invalid diff UUID");
100
+ await this.hosting.locked(() => rm(this.directory(id), { recursive: true, force: true }));
101
+ }
102
+ }
103
+ //# sourceMappingURL=diff-store.js.map
@@ -0,0 +1,168 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { extname, join, resolve, sep } from "node:path";
4
+ import { decodePath, isMissingFile, reportRequestError, requestUrl, } from "../http/request-errors.js";
5
+ import { sendJson } from "../http/router.js";
6
+ import { DiffStore } from "./diff-store.js";
7
+ import { HostingStore } from "./store.js";
8
+ const types = {
9
+ ".html": "text/html; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".mjs": "text/javascript; charset=utf-8",
13
+ ".json": "application/json",
14
+ ".svg": "image/svg+xml",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".avif": "image/avif",
21
+ ".ico": "image/x-icon",
22
+ ".woff": "font/woff",
23
+ ".woff2": "font/woff2",
24
+ ".ttf": "font/ttf",
25
+ ".otf": "font/otf",
26
+ ".wasm": "application/wasm",
27
+ ".xml": "application/xml",
28
+ ".pdf": "application/pdf",
29
+ ".webmanifest": "application/manifest+json",
30
+ ".wav": "audio/wav",
31
+ ".txt": "text/plain; charset=utf-8",
32
+ ".mp4": "video/mp4",
33
+ ".webm": "video/webm",
34
+ ".mp3": "audio/mpeg",
35
+ };
36
+ export function hostingRoutes(directory, options = {}) {
37
+ const store = directory === undefined ? undefined : new HostingStore(directory);
38
+ const diffs = directory === undefined ? undefined : new DiffStore(directory);
39
+ return async (request, response) => {
40
+ const url = requestUrl(request);
41
+ const path = url.pathname;
42
+ const head = request.method === "HEAD";
43
+ if (path === "/api/v1/capabilities" && request.method === "GET") {
44
+ sendJson(response, 200, { hosting: !!store });
45
+ return true;
46
+ }
47
+ if (path.startsWith("/api/v1/diffs")) {
48
+ response.setHeader("Cache-Control", "private, no-store");
49
+ const match = /^\/api\/v1\/diffs\/([^/]+)$/.exec(path);
50
+ const entry = diffs && match && request.method === "GET" ? await diffs.get(match[1]) : null;
51
+ if (entry) {
52
+ sendJson(response, 200, entry);
53
+ }
54
+ else if (diffs && match && request.method === "DELETE" && (await diffs.exists(match[1]))) {
55
+ await diffs.delete(match[1]);
56
+ response.statusCode = 204;
57
+ response.end();
58
+ }
59
+ else {
60
+ sendJson(response, 404, {
61
+ error: { code: "not_found", message: "Diff not found or already cleaned up" },
62
+ });
63
+ }
64
+ return true;
65
+ }
66
+ if (!path.startsWith("/web/") && !path.startsWith("/api/v1/artifacts"))
67
+ return false;
68
+ const missing = () => sendJson(response, 404, { error: { code: "not_found", message: "Artifact not found" } }, head);
69
+ if (!store) {
70
+ missing();
71
+ return true;
72
+ }
73
+ if (path === "/api/v1/artifacts" && request.method === "GET") {
74
+ const groups = new Map();
75
+ for (const { artifact } of await store.entries()) {
76
+ const list = groups.get(artifact.project) ?? [];
77
+ list.push(artifact);
78
+ groups.set(artifact.project, list);
79
+ }
80
+ sendJson(response, 200, {
81
+ projects: [...groups].map(([directory, artifacts]) => ({ directory, artifacts })),
82
+ });
83
+ return true;
84
+ }
85
+ const deletion = /^\/api\/v1\/artifacts\/([a-f0-9]{32})$/.exec(path);
86
+ if (deletion && request.method === "DELETE") {
87
+ if (await store.delete(deletion[1])) {
88
+ response.statusCode = 204;
89
+ response.end();
90
+ }
91
+ else
92
+ missing();
93
+ return true;
94
+ }
95
+ const match = /^\/web\/([a-f0-9]{32})\/(?:([a-f0-9]{32})\/(.*))?$/.exec(path);
96
+ if (!match || (request.method !== "GET" && !head)) {
97
+ missing();
98
+ return true;
99
+ }
100
+ response.removeHeader("Content-Security-Policy");
101
+ response.setHeader("Cache-Control", "private, no-store");
102
+ const entry = await store.get(match[1]);
103
+ if (!entry) {
104
+ missing();
105
+ return true;
106
+ }
107
+ if (!match[2]) {
108
+ response.statusCode = 302;
109
+ response.setHeader("Location", `/web/${entry.artifact.id}/${entry.artifact.snapshotId}/${url.search}`);
110
+ response.end();
111
+ return true;
112
+ }
113
+ try {
114
+ const relative = decodePath(match[3]);
115
+ if (relative.split(/[\\/]/).some((part) => part === "..") || relative.includes("\0")) {
116
+ missing();
117
+ return true;
118
+ }
119
+ const snapshotRoot = resolve(entry.directory, "snapshots", match[2]);
120
+ const root = await realpath(snapshotRoot);
121
+ if (root !== snapshotRoot) {
122
+ missing();
123
+ return true;
124
+ }
125
+ let file = await realpath(resolve(root, relative || "index.html"));
126
+ if (!file.startsWith(root + sep)) {
127
+ missing();
128
+ return true;
129
+ }
130
+ if ((await stat(file)).isDirectory()) {
131
+ if (!path.endsWith("/")) {
132
+ response.statusCode = 302;
133
+ response.setHeader("Location", path + "/" + url.search);
134
+ response.end();
135
+ return true;
136
+ }
137
+ file = await realpath(join(file, "index.html"));
138
+ }
139
+ if (!file.startsWith(root + sep)) {
140
+ missing();
141
+ return true;
142
+ }
143
+ const info = await stat(file);
144
+ if (!info.isFile()) {
145
+ missing();
146
+ return true;
147
+ }
148
+ response.setHeader("Content-Type", types[extname(file).toLowerCase()] ?? "application/octet-stream");
149
+ response.setHeader("Content-Length", info.size);
150
+ if (head)
151
+ response.end();
152
+ else
153
+ createReadStream(file)
154
+ .on("error", (error) => {
155
+ response.destroy();
156
+ reportRequestError(request, response, error, options);
157
+ })
158
+ .pipe(response);
159
+ }
160
+ catch (error) {
161
+ if (!isMissingFile(error))
162
+ throw error;
163
+ missing();
164
+ }
165
+ return true;
166
+ };
167
+ }
168
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { copyFile, lstat, mkdir, readdir, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, join, resolve, sep } from "node:path";
6
+ import { DirectoryLock } from "../directory-lock.js";
7
+ import { ResourceValidationError } from "./validation-error.js";
8
+ export const ID = /^[a-f0-9]{32}$/;
9
+ export const defaultHostingDirectory = join(homedir(), ".agent-hub", "hosting");
10
+ function parseArtifact(json) {
11
+ const value = JSON.parse(json);
12
+ if (!value ||
13
+ !ID.test(value.id) ||
14
+ !ID.test(value.snapshotId) ||
15
+ typeof value.project !== "string" ||
16
+ typeof value.source !== "string" ||
17
+ typeof value.name !== "string" ||
18
+ typeof value.title !== "string" ||
19
+ !Number.isFinite(Date.parse(value.createdAt)) ||
20
+ !Number.isFinite(Date.parse(value.updatedAt)) ||
21
+ value.url !== `/web/${value.id}/`)
22
+ throw new Error("Invalid artifact metadata");
23
+ return value;
24
+ }
25
+ async function copySnapshot(from, to) {
26
+ const info = await lstat(from);
27
+ if (info.isDirectory()) {
28
+ await mkdir(to);
29
+ for (const item of await readdir(from)) {
30
+ await copySnapshot(join(from, item), join(to, item));
31
+ }
32
+ return;
33
+ }
34
+ if (!info.isFile())
35
+ throw new ResourceValidationError(`Only regular files and directories can be published: ${from}`);
36
+ await copyFile(from, to, constants.COPYFILE_EXCL);
37
+ }
38
+ function inside(child, parent) {
39
+ return child === parent || child.startsWith(parent.endsWith(sep) ? parent : parent + sep);
40
+ }
41
+ export class HostingStore {
42
+ root;
43
+ constructor(root = defaultHostingDirectory) {
44
+ if (!root.trim())
45
+ throw new ResourceValidationError("Hosting directory must not be empty");
46
+ this.root = resolve(root.startsWith("~/") ? join(homedir(), root.slice(2)) : root);
47
+ }
48
+ async locked(action) {
49
+ const lock = new DirectoryLock(this.root, ".write-lock");
50
+ try {
51
+ while (!lock.acquire())
52
+ await new Promise((done) => setTimeout(done, 30));
53
+ return await action();
54
+ }
55
+ finally {
56
+ lock.close();
57
+ }
58
+ }
59
+ async get(id) {
60
+ if (!ID.test(id))
61
+ return undefined;
62
+ const directory = join(this.root, "artifacts", id);
63
+ let directoryExists = false;
64
+ try {
65
+ const canonicalDirectory = join(await realpath(this.root), "artifacts", id);
66
+ if ((await realpath(directory)) !== canonicalDirectory)
67
+ throw new Error("Artifact aliases are not allowed");
68
+ if (!(await lstat(directory)).isDirectory())
69
+ throw new Error("Invalid artifact directory");
70
+ directoryExists = true;
71
+ const artifact = parseArtifact(await readFile(join(directory, "artifact.json"), "utf8"));
72
+ if (artifact.id !== id)
73
+ throw new Error("Artifact identity does not match directory");
74
+ return { artifact, directory: canonicalDirectory };
75
+ }
76
+ catch (error) {
77
+ const code = error.code;
78
+ if (code && code !== "ENOENT" && code !== "ENOTDIR" && code !== "ELOOP")
79
+ throw error;
80
+ if (directoryExists || error.code !== "ENOENT")
81
+ console.warn(`Skipping damaged hosting entry ${directory}: ${String(error)}`);
82
+ return undefined;
83
+ }
84
+ }
85
+ async entries() {
86
+ let children;
87
+ try {
88
+ children = await readdir(join(this.root, "artifacts"), { withFileTypes: true });
89
+ }
90
+ catch (error) {
91
+ if (error.code === "ENOENT")
92
+ return [];
93
+ throw error;
94
+ }
95
+ const results = [];
96
+ for (const child of children) {
97
+ if (!child.isDirectory() || !ID.test(child.name))
98
+ continue;
99
+ const entry = await this.get(child.name);
100
+ if (entry)
101
+ results.push(entry);
102
+ }
103
+ return results;
104
+ }
105
+ async publish(sourcePath, projectPathInput, nameInput, title) {
106
+ const source = await realpath(resolve(sourcePath));
107
+ const project = await realpath(resolve(projectPathInput));
108
+ if (!(await lstat(source)).isDirectory() || !(await lstat(project)).isDirectory())
109
+ throw new ResourceValidationError("Source and project must be directories");
110
+ await mkdir(this.root, { recursive: true });
111
+ const root = await realpath(this.root);
112
+ if (inside(source, root) || inside(root, source))
113
+ throw new ResourceValidationError("Source must not overlap hosting storage");
114
+ const name = nameInput ?? basename(source);
115
+ if (!name.trim())
116
+ throw new ResourceValidationError("Artifact name must not be empty");
117
+ if (!(await lstat(join(source, "index.html"))).isFile())
118
+ throw new ResourceValidationError("Source requires index.html");
119
+ return this.locked(async () => {
120
+ const previous = (await this.entries()).find(({ artifact }) => artifact.project === project && artifact.name === name)?.artifact;
121
+ const id = previous?.id ?? randomUUID().replaceAll("-", "");
122
+ const directory = join(root, "artifacts", id);
123
+ await mkdir(join(directory, "snapshots"), { recursive: true });
124
+ const snapshotId = randomUUID().replaceAll("-", "");
125
+ const temp = join(directory, "snapshots", `.tmp-${snapshotId}`);
126
+ const metadataTemp = join(directory, `.artifact-${snapshotId}.json`);
127
+ try {
128
+ await copySnapshot(source, temp);
129
+ await rename(temp, join(directory, "snapshots", snapshotId));
130
+ const now = new Date().toISOString();
131
+ const artifact = {
132
+ id,
133
+ name,
134
+ title: title ?? previous?.title ?? name,
135
+ project,
136
+ source,
137
+ snapshotId,
138
+ createdAt: previous?.createdAt ?? now,
139
+ updatedAt: now,
140
+ url: `/web/${id}/`,
141
+ };
142
+ await writeFile(metadataTemp, JSON.stringify(artifact));
143
+ await rename(metadataTemp, join(directory, "artifact.json"));
144
+ return artifact;
145
+ }
146
+ finally {
147
+ await rm(temp, { recursive: true, force: true });
148
+ await rm(metadataTemp, { force: true });
149
+ }
150
+ });
151
+ }
152
+ async delete(id) {
153
+ return this.locked(async () => {
154
+ const entry = await this.get(id);
155
+ if (!entry)
156
+ return false;
157
+ await rm(entry.directory, { recursive: true });
158
+ return true;
159
+ });
160
+ }
161
+ }
162
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,4 @@
1
+ /** Invalid caller-supplied resource input, independent of transport. */
2
+ export class ResourceValidationError extends Error {
3
+ }
4
+ //# sourceMappingURL=validation-error.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api-route-handler.js.map