@someonegg/codex-hub 0.0.0-stage → 0.1.1

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -2
  3. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/dist/client/assets/index-CRB_Qi32.js +304 -0
  63. package/dist/client/assets/index-lmwuA3r2.css +1 -0
  64. package/dist/client/index.html +16 -0
  65. package/dist/server/server/adapters/codex/agent-identity.js +9 -0
  66. package/dist/server/server/adapters/codex/catalog-source.js +2 -0
  67. package/dist/server/server/adapters/codex/codex-session-source.js +300 -0
  68. package/dist/server/server/adapters/codex/identity-resolver.js +107 -0
  69. package/dist/server/server/adapters/codex/interaction-parser.js +90 -0
  70. package/dist/server/server/adapters/codex/internal-event-parser.js +56 -0
  71. package/dist/server/server/adapters/codex/io-errors.js +5 -0
  72. package/dist/server/server/adapters/codex/jsonl-catalog-source.js +58 -0
  73. package/dist/server/server/adapters/codex/limits.js +10 -0
  74. package/dist/server/server/adapters/codex/message-normalizer.js +121 -0
  75. package/dist/server/server/adapters/codex/path-policy.js +88 -0
  76. package/dist/server/server/adapters/codex/response-item-parser.js +100 -0
  77. package/dist/server/server/adapters/codex/rollout-decoder.js +286 -0
  78. package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
  79. package/dist/server/server/adapters/codex/session-manager.js +65 -0
  80. package/dist/server/server/adapters/codex/session-normalizer.js +257 -0
  81. package/dist/server/server/adapters/codex/tool-normalizer.js +58 -0
  82. package/dist/server/server/adapters/codex/user-input-normalizer.js +123 -0
  83. package/dist/server/server/api/session-api-mapper.js +194 -0
  84. package/dist/server/server/application/session-read-service.js +109 -0
  85. package/dist/server/server/application/session-reader.js +2 -0
  86. package/dist/server/server/catalog/config.js +4 -0
  87. package/dist/server/server/catalog/session-catalog-service.js +103 -0
  88. package/dist/server/server/config.js +145 -0
  89. package/dist/server/server/create-hub.js +64 -0
  90. package/dist/server/server/domain/project-directory.js +5 -0
  91. package/dist/server/server/domain/session-domain.js +2 -0
  92. package/dist/server/server/domain/session-text.js +45 -0
  93. package/dist/server/server/http/api-route-handler.js +2 -0
  94. package/dist/server/server/http/api-router.js +63 -0
  95. package/dist/server/server/http/create-server.js +148 -0
  96. package/dist/server/server/http/query-params.js +19 -0
  97. package/dist/server/server/http/request-body.js +48 -0
  98. package/dist/server/server/http/router.js +16 -0
  99. package/dist/server/server/http/security.js +19 -0
  100. package/dist/server/server/http/session-interaction-routes.js +101 -0
  101. package/dist/server/server/http/session-management-routes.js +90 -0
  102. package/dist/server/server/http/session-path.js +13 -0
  103. package/dist/server/server/http/session-read-routes.js +198 -0
  104. package/dist/server/server/http/session-updates-routes.js +84 -0
  105. package/dist/server/server/http/terminal-preview-websocket.js +179 -0
  106. package/dist/server/server/http/upload-routes.js +74 -0
  107. package/dist/server/server/interaction/interaction-service.js +247 -0
  108. package/dist/server/server/interaction/message.js +19 -0
  109. package/dist/server/server/interaction/pane-binding-manager.js +60 -0
  110. package/dist/server/server/interaction/reconcile-session-state.js +21 -0
  111. package/dist/server/server/interaction/session-runtime.js +86 -0
  112. package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
  113. package/dist/server/server/interaction/terminal-preview-service.js +194 -0
  114. package/dist/server/server/interaction/tmux-launcher.js +75 -0
  115. package/dist/server/server/interaction/tmux-preview-listener.js +101 -0
  116. package/dist/server/server/interaction/tmux-service.js +269 -0
  117. package/dist/server/server/interaction/transport.js +9 -0
  118. package/dist/server/server/main.js +41 -0
  119. package/dist/server/server/management/operation-service.js +553 -0
  120. package/dist/server/server/management/session-exit-service.js +50 -0
  121. package/dist/server/server/management/session-manager.js +59 -0
  122. package/dist/server/server/repository/catalog-snapshot-store.js +304 -0
  123. package/dist/server/server/repository/diagnostic-logger.js +70 -0
  124. package/dist/server/server/repository/opaque-cursor.js +79 -0
  125. package/dist/server/server/repository/refresh-coordinator.js +14 -0
  126. package/dist/server/server/repository/session-queries.js +151 -0
  127. package/dist/server/server/repository/session-query-criteria.js +2 -0
  128. package/dist/server/server/repository/timeline-prefix-index.js +342 -0
  129. package/dist/server/server/repository/timeline-prefix-registry.js +55 -0
  130. package/dist/server/server/security/opaque-id.js +13 -0
  131. package/dist/server/server/source/session-source.js +2 -0
  132. package/dist/server/server/updates/session-updates-service.js +147 -0
  133. package/dist/server/server/updates/update-revision.js +25 -0
  134. package/dist/server/shared/api-contract.js +2 -0
  135. package/dist/server/shared/domain.js +2 -0
  136. package/dist/server/shared/interaction.js +12 -0
  137. package/dist/server/shared/uploads.js +2 -0
  138. package/package.json +79 -4
@@ -0,0 +1,148 @@
1
+ import { createReadStream, readFileSync, realpathSync } from "node:fs";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { createServer as createHttpServer, } from "node:http";
4
+ import { createServer as createHttpsServer } from "node:https";
5
+ import { extname, resolve, sep } from "node:path";
6
+ import { emptyApiRouter, sendJson } from "./router.js";
7
+ import { applySecurityHeaders } from "./security.js";
8
+ const CONTENT_TYPES = {
9
+ ".css": "text/css; charset=utf-8",
10
+ ".html": "text/html; charset=utf-8",
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ };
15
+ function sendText(response, status, message, headOnly) {
16
+ response.statusCode = status;
17
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
18
+ response.setHeader("Content-Length", Buffer.byteLength(message));
19
+ response.end(headOnly ? undefined : message);
20
+ }
21
+ async function serveFile(response, filePath, headOnly) {
22
+ try {
23
+ const info = await stat(filePath);
24
+ if (!info.isFile())
25
+ return false;
26
+ response.statusCode = 200;
27
+ response.setHeader("Content-Type", CONTENT_TYPES[extname(filePath)] ?? "application/octet-stream");
28
+ response.setHeader("Content-Length", info.size);
29
+ if (headOnly) {
30
+ response.end();
31
+ }
32
+ else {
33
+ createReadStream(filePath)
34
+ .on("error", () => response.destroy())
35
+ .pipe(response);
36
+ }
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ export function createServer(config, apiRouter = emptyApiRouter, closeServices = () => { }, upgrades) {
44
+ const clientRoot = realpathSync(config.clientDirectory);
45
+ const clientRootPrefix = `${clientRoot}${sep}`;
46
+ const requestListener = async (request, response) => {
47
+ applySecurityHeaders(response, request);
48
+ const headOnly = request.method === "HEAD";
49
+ try {
50
+ if (await apiRouter(request, response))
51
+ return;
52
+ if (request.method !== "GET" && !headOnly) {
53
+ response.setHeader("Allow", "GET, HEAD");
54
+ sendJson(response, 405, {
55
+ error: { code: "method_not_allowed", message: "Method is not allowed for this resource" },
56
+ });
57
+ return;
58
+ }
59
+ const url = new URL(request.url ?? "/", "http://localhost");
60
+ const decodedPath = decodeURIComponent(url.pathname);
61
+ const sessionPage = /^\/sessions\/[A-Za-z0-9_-]{20,100}\/?$/.test(decodedPath);
62
+ const relativePath = decodedPath === "/" ? "index.html" : sessionPage ? "index.html" : decodedPath.slice(1);
63
+ const requestedPath = resolve(clientRoot, relativePath);
64
+ let canonicalPath;
65
+ try {
66
+ canonicalPath = await realpath(requestedPath);
67
+ }
68
+ catch {
69
+ sendText(response, 404, "Not found", headOnly);
70
+ return;
71
+ }
72
+ if (canonicalPath.startsWith(clientRootPrefix) &&
73
+ (await serveFile(response, canonicalPath, headOnly))) {
74
+ return;
75
+ }
76
+ sendText(response, 404, "Not found", headOnly);
77
+ }
78
+ catch {
79
+ sendJson(response, 400, { error: { code: "bad_request", message: "Malformed request" } }, headOnly);
80
+ }
81
+ };
82
+ if (!config.tls.enabled) {
83
+ const server = createHttpServer(requestListener);
84
+ return attachLifecycle(server, closeServices, upgrades);
85
+ }
86
+ const requireClientCertificate = config.tls.certificateAuthorityPath !== undefined;
87
+ const server = createHttpsServer({
88
+ cert: readFileSync(config.tls.certificatePath),
89
+ key: readFileSync(config.tls.privateKeyPath),
90
+ ...(config.tls.certificateAuthorityPath
91
+ ? { ca: readFileSync(config.tls.certificateAuthorityPath) }
92
+ : {}),
93
+ minVersion: "TLSv1.2",
94
+ requestCert: requireClientCertificate,
95
+ rejectUnauthorized: requireClientCertificate,
96
+ }, requestListener);
97
+ return attachLifecycle(server, closeServices, upgrades);
98
+ }
99
+ /** Own startup cleanup as well as the server's pre-drain shutdown hook. */
100
+ export async function startServer(config, apiRouter, closeServices, upgrades) {
101
+ try {
102
+ const server = createServer(config, apiRouter, closeServices, upgrades);
103
+ await listenServer(server, config.port, config.host);
104
+ return server;
105
+ }
106
+ catch (error) {
107
+ upgrades?.close();
108
+ closeServices();
109
+ throw error;
110
+ }
111
+ }
112
+ export function listenServer(server, port, host) {
113
+ return new Promise((resolve, reject) => {
114
+ const cleanup = () => {
115
+ server.removeListener("error", onError);
116
+ server.removeListener("listening", onListening);
117
+ };
118
+ const onError = (error) => {
119
+ cleanup();
120
+ reject(error);
121
+ };
122
+ const onListening = () => {
123
+ cleanup();
124
+ resolve();
125
+ };
126
+ server.once("error", onError);
127
+ server.once("listening", onListening);
128
+ server.listen(port, host);
129
+ });
130
+ }
131
+ function attachLifecycle(server, closeServices, upgrades) {
132
+ server.on("upgrade", (request, socket, head) => {
133
+ if (upgrades)
134
+ upgrades.handle(request, socket, head);
135
+ else
136
+ socket.destroy();
137
+ });
138
+ const close = server.close.bind(server);
139
+ const mutable = server;
140
+ mutable.close = (callback) => {
141
+ upgrades?.close();
142
+ closeServices();
143
+ close(callback);
144
+ return server;
145
+ };
146
+ return server;
147
+ }
148
+ //# sourceMappingURL=create-server.js.map
@@ -0,0 +1,19 @@
1
+ export class HttpQueryError extends Error {
2
+ }
3
+ export function optional(params, name) {
4
+ const values = params.getAll(name);
5
+ if (values.length > 1)
6
+ invalid(`${name} must appear once`);
7
+ return values[0];
8
+ }
9
+ export function only(params, allowed) {
10
+ const accepted = new Set(allowed);
11
+ for (const name of params.keys()) {
12
+ if (!accepted.has(name))
13
+ invalid(`${name} is not supported`);
14
+ }
15
+ }
16
+ export function invalid(message) {
17
+ throw new HttpQueryError(message);
18
+ }
19
+ //# sourceMappingURL=query-params.js.map
@@ -0,0 +1,48 @@
1
+ import { MAX_INTERACTION_MESSAGE_BYTES } from "../../shared/interaction.js";
2
+ const MAX_INTERACTION_JSON_BYTES = MAX_INTERACTION_MESSAGE_BYTES * 6 + 1_024;
3
+ export class RequestBodyError extends Error {
4
+ status;
5
+ code;
6
+ constructor(status, code, message) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+ export async function readJsonObject(request) {
13
+ const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
14
+ if (contentType !== "application/json") {
15
+ invalidBody("Content-Type must be application/json");
16
+ }
17
+ const declared = Number(request.headers["content-length"]);
18
+ if (Number.isFinite(declared) && declared > MAX_INTERACTION_JSON_BYTES) {
19
+ bodyTooLarge();
20
+ }
21
+ const chunks = [];
22
+ let bytes = 0;
23
+ for await (const chunk of request) {
24
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
25
+ bytes += value.byteLength;
26
+ if (bytes > MAX_INTERACTION_JSON_BYTES)
27
+ bodyTooLarge();
28
+ chunks.push(value);
29
+ }
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
33
+ }
34
+ catch {
35
+ invalidBody("Request body must be valid JSON");
36
+ }
37
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
38
+ invalidBody("Request body must be a JSON object");
39
+ }
40
+ return parsed;
41
+ }
42
+ function invalidBody(message) {
43
+ throw new RequestBodyError(400, "invalid_json", message);
44
+ }
45
+ function bodyTooLarge() {
46
+ throw new RequestBodyError(413, "body_too_large", "Request body is too large");
47
+ }
48
+ //# sourceMappingURL=request-body.js.map
@@ -0,0 +1,16 @@
1
+ export function sendJson(response, status, body, headOnly = false) {
2
+ const json = JSON.stringify(body);
3
+ response.statusCode = status;
4
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
5
+ response.setHeader("Cache-Control", "no-store");
6
+ response.setHeader("Content-Length", Buffer.byteLength(json));
7
+ response.end(headOnly ? undefined : json);
8
+ }
9
+ export const emptyApiRouter = (request, response) => {
10
+ const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
11
+ if (!pathname.startsWith("/api/"))
12
+ return false;
13
+ sendJson(response, 404, { error: { code: "not_found", message: "API endpoint not found" } }, request.method === "HEAD");
14
+ return true;
15
+ };
16
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1,19 @@
1
+ const SECURITY_HEADERS = {
2
+ "Content-Security-Policy": "default-src 'self'; base-uri 'none'; font-src 'self' data:; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'",
3
+ "X-Content-Type-Options": "nosniff",
4
+ "Referrer-Policy": "no-referrer",
5
+ "X-Frame-Options": "DENY",
6
+ };
7
+ export function applySecurityHeaders(response, request) {
8
+ const secure = "encrypted" in request.socket && request.socket.encrypted;
9
+ const host = request.headers.host;
10
+ // Explicit WebSocket sources are needed by browsers that do not map 'self' to ws/wss.
11
+ if (host && /^[a-zA-Z0-9.:[\]-]+$/.test(host)) {
12
+ response.setHeader("Content-Security-Policy", `${SECURITY_HEADERS["Content-Security-Policy"]}; connect-src 'self' ${secure ? "wss" : "ws"}://${host}`);
13
+ }
14
+ for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
15
+ if (!response.hasHeader(name))
16
+ response.setHeader(name, value);
17
+ }
18
+ }
19
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1,101 @@
1
+ import { INTERACTION_KEYS, MAX_INTERACTION_KEY_SEQUENCE_LENGTH, } from "../../shared/api-contract.js";
2
+ import { SessionInteractionError, } from "../interaction/interaction-service.js";
3
+ import { MessageValidationError, normalizeMessage } from "../interaction/message.js";
4
+ import { readJsonObject, RequestBodyError } from "./request-body.js";
5
+ import { sendJson } from "./router.js";
6
+ import { SESSION_ID_PATTERN, SESSION_ROOT, sessionPath } from "./session-path.js";
7
+ const INTERACTION_PATH = new RegExp(`^${SESSION_ROOT}/${SESSION_ID_PATTERN}/(?:terminal-preview|messages|keys|handoff-pane-binding)$`);
8
+ const INTERACTION_KEY_SET = new Set(INTERACTION_KEYS);
9
+ export function createSessionInteractionRoutes(interaction) {
10
+ return {
11
+ matches: (url) => INTERACTION_PATH.test(url.pathname),
12
+ async handle(request, response, url) {
13
+ const { id, segments: [operation = ""], } = sessionPath(url.pathname);
14
+ if (operation === "terminal-preview") {
15
+ response.setHeader("Upgrade", "websocket");
16
+ sendJson(response, 426, {
17
+ error: { code: "websocket_required", message: "Terminal preview requires WebSocket" },
18
+ }, request.method === "HEAD");
19
+ return true;
20
+ }
21
+ if (request.method !== "POST")
22
+ return false;
23
+ if (interaction === undefined)
24
+ return interactionUnavailable(response);
25
+ if (operation === "handoff-pane-binding") {
26
+ const body = await readJsonObject(request);
27
+ if (Object.keys(body).length !== 0)
28
+ invalidBody("Expected an empty object");
29
+ await interaction.handoffPaneBinding(id);
30
+ return noContent(response);
31
+ }
32
+ if (operation === "messages") {
33
+ const body = await readJsonObject(request);
34
+ if (Object.keys(body).length !== 1 || typeof body.message !== "string") {
35
+ invalidBody("body must contain only a string message field");
36
+ }
37
+ normalizeMessage(body.message);
38
+ await interaction.sendMessage(id, body.message);
39
+ return noContent(response);
40
+ }
41
+ if (operation === "keys") {
42
+ const body = await readJsonObject(request);
43
+ if (Object.keys(body).length !== 1 || !Array.isArray(body.keys)) {
44
+ invalidBody("body must contain only a keys array");
45
+ }
46
+ if (body.keys.length === 0 ||
47
+ body.keys.length > MAX_INTERACTION_KEY_SEQUENCE_LENGTH ||
48
+ !body.keys.every(isInteractionKey)) {
49
+ invalidBody(`keys must contain 1-${MAX_INTERACTION_KEY_SEQUENCE_LENGTH} supported interaction keys`);
50
+ }
51
+ await interaction.sendKeys(id, body.keys);
52
+ return noContent(response);
53
+ }
54
+ return false;
55
+ },
56
+ mapError(error, request, response) {
57
+ const headOnly = request.method === "HEAD";
58
+ if (error instanceof MessageValidationError && error.code === "invalid_message") {
59
+ sendJson(response, 400, {
60
+ error: { code: "invalid_message", message: error.message },
61
+ }, headOnly);
62
+ return true;
63
+ }
64
+ if (error instanceof SessionInteractionError) {
65
+ const status = error.code === "session_not_found"
66
+ ? 404
67
+ : error.code === "operation_result_unknown"
68
+ ? 504
69
+ : error.code === "interaction_failed"
70
+ ? 502
71
+ : 409;
72
+ sendJson(response, status, {
73
+ error: { code: error.code, message: error.message },
74
+ }, headOnly);
75
+ return true;
76
+ }
77
+ return false;
78
+ },
79
+ };
80
+ }
81
+ function invalidBody(message) {
82
+ throw new RequestBodyError(400, "invalid_json", message);
83
+ }
84
+ function isInteractionKey(value) {
85
+ return typeof value === "string" && INTERACTION_KEY_SET.has(value);
86
+ }
87
+ function noContent(response) {
88
+ response.statusCode = 204;
89
+ response.end();
90
+ return true;
91
+ }
92
+ function interactionUnavailable(response) {
93
+ sendJson(response, 409, {
94
+ error: {
95
+ code: "interaction_not_supported",
96
+ message: "Interaction is not enabled",
97
+ },
98
+ });
99
+ return true;
100
+ }
101
+ //# sourceMappingURL=session-interaction-routes.js.map
@@ -0,0 +1,90 @@
1
+ import { ManagementError } from "../management/operation-service.js";
2
+ import { readJsonObject, RequestBodyError } from "./request-body.js";
3
+ import { sendJson } from "./router.js";
4
+ import { SESSION_ID_PATTERN, SESSION_ROOT, sessionPath } from "./session-path.js";
5
+ const OPERATION_PATH = /^\/api\/v1\/operations\/[^/]+$/;
6
+ const SESSION_PATH = new RegExp(`^${SESSION_ROOT}/${SESSION_ID_PATTERN}(?:/(?:archive|exit))?$`);
7
+ export function createSessionManagementRoutes(operations) {
8
+ return {
9
+ matches: (url) => OPERATION_PATH.test(url.pathname) ||
10
+ url.pathname === SESSION_ROOT ||
11
+ SESSION_PATH.test(url.pathname),
12
+ async handle(request, response, url) {
13
+ if (!operations)
14
+ return false;
15
+ if (OPERATION_PATH.test(url.pathname)) {
16
+ if (request.method !== "GET")
17
+ return false;
18
+ const operationId = url.pathname.slice("/api/v1/operations/".length);
19
+ const operation = await operations.get(operationId);
20
+ if (!operation) {
21
+ throw new ManagementError(404, "Operation not found; the server may have restarted or the record expired");
22
+ }
23
+ sendJson(response, 200, operation);
24
+ return true;
25
+ }
26
+ const create = url.pathname === SESSION_ROOT;
27
+ const archive = url.pathname.endsWith("/archive");
28
+ const exit = url.pathname.endsWith("/exit");
29
+ const method = create || archive || exit ? "POST" : "DELETE";
30
+ if (request.method !== method)
31
+ return false;
32
+ const protocol = "encrypted" in request.socket && request.socket.encrypted ? "https" : "http";
33
+ if (request.headers.origin &&
34
+ request.headers.origin !== `${protocol}://${request.headers.host}`) {
35
+ throw new ManagementError(403, "Cross-origin management requests are not allowed");
36
+ }
37
+ const body = await readJsonObject(request);
38
+ if (create) {
39
+ if (typeof body.requestId !== "string")
40
+ throw new RequestBodyError(400, "invalid_json", "requestId is required");
41
+ if (body.resumeSessionId !== undefined) {
42
+ if (typeof body.resumeSessionId !== "string" ||
43
+ !body.resumeSessionId.trim() ||
44
+ Object.keys(body).some((key) => !["resumeSessionId", "yolo", "requestId"].includes(key)) ||
45
+ (body.yolo !== undefined && typeof body.yolo !== "boolean"))
46
+ throw new RequestBodyError(400, "invalid_json", "Resume accepts only resumeSessionId, yolo and requestId");
47
+ sendJson(response, 202, operations.create({
48
+ resumeSessionId: body.resumeSessionId,
49
+ yolo: body.yolo,
50
+ requestId: body.requestId,
51
+ }));
52
+ return true;
53
+ }
54
+ if (Object.keys(body).some((key) => !["directory", "worktree", "yolo", "requestId", "sourceInstanceId"].includes(key)) ||
55
+ (body.sourceInstanceId !== undefined && typeof body.sourceInstanceId !== "string") ||
56
+ typeof body.directory !== "string" ||
57
+ typeof body.worktree !== "boolean" ||
58
+ (body.yolo !== undefined && typeof body.yolo !== "boolean")) {
59
+ throw new RequestBodyError(400, "invalid_json", "directory and worktree are required; yolo must be a boolean if provided");
60
+ }
61
+ sendJson(response, 202, operations.create({
62
+ ...(typeof body.sourceInstanceId === "string"
63
+ ? { sourceInstanceId: body.sourceInstanceId }
64
+ : {}),
65
+ directory: body.directory,
66
+ worktree: body.worktree,
67
+ yolo: body.yolo ?? false,
68
+ requestId: body.requestId,
69
+ }));
70
+ }
71
+ else {
72
+ if (Object.keys(body).length !== 0)
73
+ throw new RequestBodyError(400, "invalid_json", "Expected an empty object");
74
+ const { id: sessionId } = sessionPath(url.pathname);
75
+ sendJson(response, 202, operations.manage(exit ? "exit" : archive ? "archive" : "delete", sessionId));
76
+ }
77
+ return true;
78
+ },
79
+ mapError(error, request, response) {
80
+ if (error instanceof ManagementError) {
81
+ sendJson(response, error.status, {
82
+ error: { code: "management_error", message: error.message },
83
+ }, request.method === "HEAD");
84
+ return true;
85
+ }
86
+ return false;
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=session-management-routes.js.map
@@ -0,0 +1,13 @@
1
+ export const SESSION_ROOT = "/api/v1/sessions";
2
+ export const SESSION_ID_PATTERN = "[A-Za-z0-9_-]{20,100}";
3
+ export function isOpaqueId(value) {
4
+ return new RegExp(`^${SESSION_ID_PATTERN}$`).test(value);
5
+ }
6
+ export function sessionPath(pathname) {
7
+ const [id = "", ...segments] = pathname.slice(SESSION_ROOT.length + 1).split("/");
8
+ return { id, segments };
9
+ }
10
+ export function readPathname(request, url) {
11
+ return request.url?.startsWith("/") ? request.url.split("?")[0] : url.pathname;
12
+ }
13
+ //# sourceMappingURL=session-path.js.map
@@ -0,0 +1,198 @@
1
+ import { CatalogCapacityError, } from "../catalog/session-catalog-service.js";
2
+ import { reconcileSessionState } from "../interaction/reconcile-session-state.js";
3
+ import { RepositoryQueryError } from "../repository/session-queries.js";
4
+ import { withUpdateRevision } from "../updates/update-revision.js";
5
+ import { invalid, only, optional } from "./query-params.js";
6
+ import { sendJson } from "./router.js";
7
+ import { isOpaqueId, readPathname, SESSION_ROOT, sessionPath } from "./session-path.js";
8
+ export function createSessionReadRoutes(dependencies) {
9
+ return {
10
+ matches: () => true,
11
+ async handle(request, response, url) {
12
+ const pathname = readPathname(request, url);
13
+ const headOnly = request.method === "HEAD";
14
+ const readMethod = request.method === "GET" || headOnly;
15
+ if (!readMethod)
16
+ return false;
17
+ if (pathname === SESSION_ROOT) {
18
+ const query = parseListQuery(url.searchParams);
19
+ const abort = new AbortController();
20
+ const disconnect = () => {
21
+ if (!response.writableEnded)
22
+ abort.abort();
23
+ };
24
+ request.once("aborted", disconnect);
25
+ response.once("close", disconnect);
26
+ response.setHeader("Cache-Control", "no-store");
27
+ try {
28
+ const result = await dependencies.catalog.wait(headOnly ? undefined : query.after, abort.signal);
29
+ if (abort.signal.aborted || response.destroyed)
30
+ return true;
31
+ if (result === null) {
32
+ response.statusCode = 204;
33
+ response.end();
34
+ }
35
+ else
36
+ sendJson(response, 200, result, headOnly);
37
+ }
38
+ finally {
39
+ request.removeListener("aborted", disconnect);
40
+ response.removeListener("close", disconnect);
41
+ }
42
+ return true;
43
+ }
44
+ if (!pathname.startsWith(`${SESSION_ROOT}/`)) {
45
+ return notFound(response, headOnly);
46
+ }
47
+ const { id, segments } = sessionPath(pathname);
48
+ if (!isOpaqueId(id))
49
+ return notFound(response, headOnly);
50
+ const itemId = segments.length === 3 && segments[0] === "items" ? decodeItemId(segments[1] ?? "") : "";
51
+ if (segments.length === 0) {
52
+ if ([...url.searchParams].length > 0) {
53
+ invalid("session metadata does not accept query parameters");
54
+ }
55
+ const result = await dependencies.sessions.getSession(id);
56
+ if (result === null) {
57
+ return notFound(response, headOnly, "session_not_found");
58
+ }
59
+ const interaction = await describeInteraction(dependencies.interaction, id);
60
+ sendJson(response, 200, withUpdateRevision(reconcileSessionState({
61
+ ...result,
62
+ interaction,
63
+ }), dependencies.updates.revision.bind(dependencies.updates)), headOnly);
64
+ return true;
65
+ }
66
+ if (segments.length === 1 && segments[0] === "items") {
67
+ const result = await dependencies.sessions.getItems(id, parseItemQuery(url.searchParams));
68
+ if (result === null) {
69
+ return notFound(response, headOnly, "session_not_found");
70
+ }
71
+ const interaction = await describeInteraction(dependencies.interaction, id);
72
+ sendJson(response, 200, withUpdateRevision(reconcileSessionState({
73
+ ...result,
74
+ interaction,
75
+ }), dependencies.updates.revision.bind(dependencies.updates)), headOnly);
76
+ return true;
77
+ }
78
+ if (segments.length === 3 && segments[0] === "items" && segments[2] === "tool") {
79
+ const result = await dependencies.sessions.getToolDetail(id, itemId, parseToolQuery(url.searchParams));
80
+ if (result === null) {
81
+ return notFound(response, headOnly, "tool_not_found");
82
+ }
83
+ sendJson(response, 200, result, headOnly);
84
+ return true;
85
+ }
86
+ if (segments.length === 3 && segments[0] === "items" && segments[2] === "internal") {
87
+ const result = await dependencies.sessions.getInternalDetail(id, itemId, parseInternalQuery(url.searchParams));
88
+ if (result === null) {
89
+ return notFound(response, headOnly, "internal_not_found");
90
+ }
91
+ sendJson(response, 200, result, headOnly);
92
+ return true;
93
+ }
94
+ if (segments.length === 3 && segments[0] === "items" && segments[2] === "directive") {
95
+ const result = await dependencies.sessions.getDirectiveDetail(id, itemId, parseDirectiveQuery(url.searchParams));
96
+ if (result === null) {
97
+ return notFound(response, headOnly, "directive_not_found");
98
+ }
99
+ sendJson(response, 200, result, headOnly);
100
+ return true;
101
+ }
102
+ return notFound(response, headOnly);
103
+ },
104
+ mapError(error, request, response) {
105
+ if (error instanceof CatalogCapacityError) {
106
+ response.setHeader("Retry-After", "2");
107
+ sendJson(response, 429, { error: { code: "catalog_capacity_exceeded", message: error.message } }, request.method === "HEAD");
108
+ return true;
109
+ }
110
+ if (error instanceof DOMException && error.name === "AbortError") {
111
+ if (!response.destroyed) {
112
+ response.statusCode = 204;
113
+ response.end();
114
+ }
115
+ return true;
116
+ }
117
+ if (!(error instanceof RepositoryQueryError))
118
+ return false;
119
+ const status = error.code === "timeline_changed" ? 409 : 400;
120
+ sendJson(response, status, {
121
+ error: { code: error.code, message: error.message },
122
+ }, request.method === "HEAD");
123
+ return true;
124
+ },
125
+ };
126
+ }
127
+ async function describeInteraction(interaction, sessionId) {
128
+ if (interaction === undefined)
129
+ return { supported: false, attachCommand: null };
130
+ return ((await interaction.describe(sessionId)) ?? { supported: false, attachCommand: null });
131
+ }
132
+ function parseListQuery(params) {
133
+ only(params, ["after"]);
134
+ const after = optional(params, "after");
135
+ if (after !== undefined && !/^[A-Za-z0-9_-]{43}$/.test(after))
136
+ invalid("after is malformed");
137
+ return after === undefined ? {} : { after };
138
+ }
139
+ function parseItemQuery(params) {
140
+ only(params, ["limit", "cursor"]);
141
+ const query = {};
142
+ const cursor = optional(params, "cursor");
143
+ if (cursor !== undefined)
144
+ query.cursor = cursor;
145
+ const limit = optional(params, "limit");
146
+ if (limit !== undefined)
147
+ query.limit = integer(limit, "limit");
148
+ return query;
149
+ }
150
+ function parseToolQuery(params) {
151
+ only(params, ["cursor"]);
152
+ return { cursor: requiredCursor(params, "tool detail") };
153
+ }
154
+ function parseDirectiveQuery(params) {
155
+ only(params, ["cursor"]);
156
+ return { cursor: requiredCursor(params, "directive detail") };
157
+ }
158
+ function parseInternalQuery(params) {
159
+ only(params, ["cursor"]);
160
+ return { cursor: requiredCursor(params, "internal detail") };
161
+ }
162
+ function requiredCursor(params, resource) {
163
+ const cursor = optional(params, "cursor");
164
+ if (cursor === undefined)
165
+ invalid(`cursor is required for ${resource}`);
166
+ return cursor;
167
+ }
168
+ function integer(value, name) {
169
+ if (!/^\d+$/.test(value))
170
+ invalid(`${name} must be an integer`);
171
+ const result = Number(value);
172
+ if (!Number.isSafeInteger(result)) {
173
+ invalid(`${name} is outside the supported range`);
174
+ }
175
+ return result;
176
+ }
177
+ function decodeItemId(value) {
178
+ let decoded;
179
+ try {
180
+ decoded = decodeURIComponent(value);
181
+ }
182
+ catch {
183
+ return invalid("item ID is malformed");
184
+ }
185
+ if (!decoded ||
186
+ Buffer.byteLength(decoded, "utf8") > 1024 ||
187
+ /[\u0000-\u001f\u007f-\u009f/\\]/u.test(decoded) ||
188
+ decoded === "." ||
189
+ decoded === "..") {
190
+ invalid("item ID is malformed");
191
+ }
192
+ return decoded;
193
+ }
194
+ function notFound(response, headOnly, code = "not_found") {
195
+ sendJson(response, 404, { error: { code, message: "API resource not found" } }, headOnly);
196
+ return true;
197
+ }
198
+ //# sourceMappingURL=session-read-routes.js.map