@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.
- package/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/index-CRB_Qi32.js +304 -0
- package/dist/client/assets/index-lmwuA3r2.css +1 -0
- package/dist/client/index.html +16 -0
- package/dist/server/server/adapters/codex/agent-identity.js +9 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +300 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +107 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +90 -0
- package/dist/server/server/adapters/codex/internal-event-parser.js +56 -0
- package/dist/server/server/adapters/codex/io-errors.js +5 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +58 -0
- package/dist/server/server/adapters/codex/limits.js +10 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/path-policy.js +88 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +100 -0
- package/dist/server/server/adapters/codex/rollout-decoder.js +286 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +65 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +257 -0
- package/dist/server/server/adapters/codex/tool-normalizer.js +58 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +123 -0
- package/dist/server/server/api/session-api-mapper.js +194 -0
- package/dist/server/server/application/session-read-service.js +109 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +103 -0
- package/dist/server/server/config.js +145 -0
- package/dist/server/server/create-hub.js +64 -0
- package/dist/server/server/domain/project-directory.js +5 -0
- package/dist/server/server/domain/session-domain.js +2 -0
- package/dist/server/server/domain/session-text.js +45 -0
- package/dist/server/server/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +63 -0
- package/dist/server/server/http/create-server.js +148 -0
- package/dist/server/server/http/query-params.js +19 -0
- package/dist/server/server/http/request-body.js +48 -0
- package/dist/server/server/http/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +101 -0
- package/dist/server/server/http/session-management-routes.js +90 -0
- package/dist/server/server/http/session-path.js +13 -0
- package/dist/server/server/http/session-read-routes.js +198 -0
- package/dist/server/server/http/session-updates-routes.js +84 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +74 -0
- package/dist/server/server/interaction/interaction-service.js +247 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +60 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +194 -0
- package/dist/server/server/interaction/tmux-launcher.js +75 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +101 -0
- package/dist/server/server/interaction/tmux-service.js +269 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +41 -0
- package/dist/server/server/management/operation-service.js +553 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-manager.js +59 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +304 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/opaque-cursor.js +79 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +151 -0
- package/dist/server/server/repository/session-query-criteria.js +2 -0
- package/dist/server/server/repository/timeline-prefix-index.js +342 -0
- package/dist/server/server/repository/timeline-prefix-registry.js +55 -0
- package/dist/server/server/security/opaque-id.js +13 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +147 -0
- package/dist/server/server/updates/update-revision.js +25 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +12 -0
- package/dist/server/shared/uploads.js +2 -0
- package/package.json +79 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { SessionApiMapper } from "../api/session-api-mapper.js";
|
|
3
|
+
import { CatalogSnapshotStore, DEFAULT_CATALOG_FRESHNESS_MS, } from "../repository/catalog-snapshot-store.js";
|
|
4
|
+
import { SessionQueries } from "../repository/session-queries.js";
|
|
5
|
+
export class SessionReadService {
|
|
6
|
+
runtime;
|
|
7
|
+
#store;
|
|
8
|
+
#queries;
|
|
9
|
+
#mapper;
|
|
10
|
+
constructor(sources, freshnessMs = DEFAULT_CATALOG_FRESHNESS_MS, now = performance.now.bind(performance), dependencies = {}) {
|
|
11
|
+
const { resolveNickname, homes, ...storeDependencies } = dependencies;
|
|
12
|
+
this.#mapper = new SessionApiMapper(resolveNickname, homes);
|
|
13
|
+
this.#store = new CatalogSnapshotStore(sources, freshnessMs, now, storeDependencies);
|
|
14
|
+
this.#queries = new SessionQueries();
|
|
15
|
+
}
|
|
16
|
+
async managementSnapshot() {
|
|
17
|
+
const snapshot = await this.#store.refresh();
|
|
18
|
+
return {
|
|
19
|
+
sessions: [...snapshot.sessions.values()].map(({ normalized }) => normalized.session),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async refresh() {
|
|
23
|
+
await this.#store.refresh();
|
|
24
|
+
}
|
|
25
|
+
async list() {
|
|
26
|
+
const snapshot = await this.#store.current();
|
|
27
|
+
const sessions = [...snapshot.sessions.values()].map(({ normalized }) => normalized.session);
|
|
28
|
+
const result = this.#mapper.list(sessions);
|
|
29
|
+
if (!this.runtime)
|
|
30
|
+
return result;
|
|
31
|
+
const states = await Promise.all(sessions.map((session) => this.runtime.evaluate(session, sessions, false)));
|
|
32
|
+
const byId = new Map(sessions.map((session, index) => [session.id, states[index].status]));
|
|
33
|
+
result.sessions.forEach((session) => {
|
|
34
|
+
session.status = byId.get(session.id);
|
|
35
|
+
});
|
|
36
|
+
result.revision = createHash("sha256")
|
|
37
|
+
.update(JSON.stringify([result.revision, result.sessions.map(({ id, status }) => [id, status])]))
|
|
38
|
+
.digest("base64url");
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
async getSession(id) {
|
|
42
|
+
const snapshot = await this.#store.current();
|
|
43
|
+
const result = this.#queries.session(snapshot, id);
|
|
44
|
+
if (result === null)
|
|
45
|
+
return null;
|
|
46
|
+
const response = this.#mapper.detail(result);
|
|
47
|
+
await this.#runtimeDetail(response.session, snapshot);
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
async getItems(id, query) {
|
|
51
|
+
const snapshot = await this.#store.current();
|
|
52
|
+
const result = this.#queries.items(snapshot, id, query);
|
|
53
|
+
if (result === null)
|
|
54
|
+
return null;
|
|
55
|
+
const response = this.#mapper.itemPage(result);
|
|
56
|
+
await this.#runtimeDetail(response.session, snapshot);
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
async getSessionUpdates(id, cursor) {
|
|
60
|
+
const snapshot = await this.#store.current();
|
|
61
|
+
const context = this.#queries.updates(snapshot, id, cursor);
|
|
62
|
+
if (context === null)
|
|
63
|
+
return null;
|
|
64
|
+
const normalized = snapshot.sessions.get(id).normalized;
|
|
65
|
+
const session = this.#mapper.sessionDetail(context.session);
|
|
66
|
+
await this.#runtimeDetail(session, snapshot);
|
|
67
|
+
return {
|
|
68
|
+
session,
|
|
69
|
+
cursor: context.cursor,
|
|
70
|
+
hasMore: context.hasMore,
|
|
71
|
+
interactionSession: {
|
|
72
|
+
archived: normalized.session.archived,
|
|
73
|
+
interaction: normalized.interaction ?? null,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async getToolDetail(id, itemId, query) {
|
|
78
|
+
const snapshot = await this.#store.current();
|
|
79
|
+
const result = this.#queries.toolDetail(snapshot, id, itemId, query);
|
|
80
|
+
return result === null ? null : this.#mapper.toolDetail(result);
|
|
81
|
+
}
|
|
82
|
+
async getDirectiveDetail(id, itemId, query) {
|
|
83
|
+
const snapshot = await this.#store.current();
|
|
84
|
+
const result = this.#queries.directiveDetail(snapshot, id, itemId, query);
|
|
85
|
+
return result === null ? null : this.#mapper.directiveDetail(result);
|
|
86
|
+
}
|
|
87
|
+
async getInternalDetail(id, itemId, query) {
|
|
88
|
+
const snapshot = await this.#store.current();
|
|
89
|
+
const result = this.#queries.internalDetail(snapshot, id, itemId, query);
|
|
90
|
+
return result === null ? null : this.#mapper.internalDetail(result);
|
|
91
|
+
}
|
|
92
|
+
async #runtimeDetail(detail, snapshot) {
|
|
93
|
+
if (!this.runtime)
|
|
94
|
+
return;
|
|
95
|
+
const sessions = [...snapshot.sessions.values()].map(({ normalized }) => normalized.session);
|
|
96
|
+
Object.assign(detail, await this.runtime.evaluate(snapshot.sessions.get(detail.id).normalized.session, sessions));
|
|
97
|
+
}
|
|
98
|
+
async getInteractionSession(id) {
|
|
99
|
+
const snapshot = await this.#store.current();
|
|
100
|
+
const normalized = snapshot.sessions.get(id)?.normalized;
|
|
101
|
+
return normalized === undefined
|
|
102
|
+
? null
|
|
103
|
+
: {
|
|
104
|
+
archived: normalized.session.archived,
|
|
105
|
+
interaction: normalized.interaction ?? null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=session-read-service.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CATALOG_PROBE_INTERVAL_MS, CATALOG_WAIT_TIMEOUT_MS, MAX_CATALOG_WAITERS, } from "./config.js";
|
|
2
|
+
export class CatalogCapacityError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** One probe loop for every waiting directory request. No loop when idle. */
|
|
5
|
+
export class SessionCatalogService {
|
|
6
|
+
sessions;
|
|
7
|
+
#waiters = new Set();
|
|
8
|
+
#timer;
|
|
9
|
+
#probe = null;
|
|
10
|
+
#closed = false;
|
|
11
|
+
#probing = false;
|
|
12
|
+
constructor(sessions) {
|
|
13
|
+
this.sessions = sessions;
|
|
14
|
+
}
|
|
15
|
+
async wait(after, signal) {
|
|
16
|
+
this.#assertOpen(signal);
|
|
17
|
+
const initial = await this.#read();
|
|
18
|
+
this.#assertOpen(signal);
|
|
19
|
+
if (initial.revision !== after)
|
|
20
|
+
return initial;
|
|
21
|
+
if (this.#waiters.size >= MAX_CATALOG_WAITERS)
|
|
22
|
+
throw new CatalogCapacityError("Too many catalog requests are waiting; retry shortly");
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
signal?.removeEventListener("abort", abort);
|
|
27
|
+
this.#waiters.delete(waiter);
|
|
28
|
+
if (this.#waiters.size === 0) {
|
|
29
|
+
clearTimeout(this.#timer);
|
|
30
|
+
this.#timer = undefined;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const waiter = {
|
|
34
|
+
after,
|
|
35
|
+
finish: (result) => {
|
|
36
|
+
cleanup();
|
|
37
|
+
resolve(result);
|
|
38
|
+
},
|
|
39
|
+
fail: (reason) => {
|
|
40
|
+
cleanup();
|
|
41
|
+
reject(reason);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const abort = () => waiter.fail(new DOMException("Aborted", "AbortError"));
|
|
45
|
+
const timeout = setTimeout(() => waiter.finish(null), CATALOG_WAIT_TIMEOUT_MS);
|
|
46
|
+
this.#waiters.add(waiter);
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
this.#schedule();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
#assertOpen(signal) {
|
|
52
|
+
if (this.#closed || signal?.aborted)
|
|
53
|
+
throw new DOMException("Aborted", "AbortError");
|
|
54
|
+
}
|
|
55
|
+
#read(refresh = false) {
|
|
56
|
+
if (this.#probe !== null)
|
|
57
|
+
return this.#probe;
|
|
58
|
+
this.#probe = (async () => {
|
|
59
|
+
if (refresh)
|
|
60
|
+
await this.sessions.refresh();
|
|
61
|
+
return this.sessions.list();
|
|
62
|
+
})();
|
|
63
|
+
const probe = this.#probe;
|
|
64
|
+
void probe.then(() => {
|
|
65
|
+
if (this.#probe === probe)
|
|
66
|
+
this.#probe = null;
|
|
67
|
+
}, () => {
|
|
68
|
+
if (this.#probe === probe)
|
|
69
|
+
this.#probe = null;
|
|
70
|
+
});
|
|
71
|
+
return probe;
|
|
72
|
+
}
|
|
73
|
+
#schedule() {
|
|
74
|
+
if (this.#closed || this.#probing || this.#timer !== undefined || this.#waiters.size === 0)
|
|
75
|
+
return;
|
|
76
|
+
this.#timer = setTimeout(async () => {
|
|
77
|
+
this.#timer = undefined;
|
|
78
|
+
this.#probing = true;
|
|
79
|
+
try {
|
|
80
|
+
const snapshot = await this.#read(true);
|
|
81
|
+
for (const waiter of this.#waiters)
|
|
82
|
+
if (waiter.after !== snapshot.revision)
|
|
83
|
+
waiter.finish(snapshot);
|
|
84
|
+
}
|
|
85
|
+
catch (reason) {
|
|
86
|
+
for (const waiter of this.#waiters)
|
|
87
|
+
waiter.fail(reason);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.#probing = false;
|
|
91
|
+
this.#schedule();
|
|
92
|
+
}
|
|
93
|
+
}, CATALOG_PROBE_INTERVAL_MS);
|
|
94
|
+
}
|
|
95
|
+
close() {
|
|
96
|
+
this.#closed = true;
|
|
97
|
+
clearTimeout(this.#timer);
|
|
98
|
+
this.#timer = undefined;
|
|
99
|
+
for (const waiter of this.#waiters)
|
|
100
|
+
waiter.finish(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=session-catalog-service.js.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
export const LOOPBACK_HOST = "127.0.0.1";
|
|
7
|
+
function nonEmptyOption(name, value) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
const normalized = value.trim();
|
|
11
|
+
if (normalized.length === 0) {
|
|
12
|
+
throw new Error(`--${name} must not be empty`);
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function parsePort(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return 4173;
|
|
19
|
+
const port = Number(value);
|
|
20
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
21
|
+
throw new Error("--port must be an integer between 0 and 65535");
|
|
22
|
+
}
|
|
23
|
+
return port;
|
|
24
|
+
}
|
|
25
|
+
export function loadConfig(args = process.argv.slice(2)) {
|
|
26
|
+
const { values } = parseArgs({
|
|
27
|
+
args: [...args],
|
|
28
|
+
allowPositionals: false,
|
|
29
|
+
strict: true,
|
|
30
|
+
options: {
|
|
31
|
+
"codex-home": { type: "string", multiple: true },
|
|
32
|
+
host: { type: "string" },
|
|
33
|
+
port: { type: "string" },
|
|
34
|
+
ssl: { type: "boolean" },
|
|
35
|
+
"ssl-cert": { type: "string" },
|
|
36
|
+
"ssl-key": { type: "string" },
|
|
37
|
+
"ssl-ca": { type: "string" },
|
|
38
|
+
help: { type: "boolean" },
|
|
39
|
+
"tmux-socket": { type: "string" },
|
|
40
|
+
"upload-dir": { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (values.help)
|
|
44
|
+
return { kind: "help" };
|
|
45
|
+
const host = nonEmptyOption("host", values.host) ?? LOOPBACK_HOST;
|
|
46
|
+
const homes = parseHomes(values["codex-home"] ?? ["default=~/.codex"]);
|
|
47
|
+
const certificatePath = nonEmptyOption("ssl-cert", values["ssl-cert"]);
|
|
48
|
+
const privateKeyPath = nonEmptyOption("ssl-key", values["ssl-key"]);
|
|
49
|
+
const certificateAuthorityPath = nonEmptyOption("ssl-ca", values["ssl-ca"]);
|
|
50
|
+
let tls;
|
|
51
|
+
if (values.ssl) {
|
|
52
|
+
if (!certificatePath || !privateKeyPath) {
|
|
53
|
+
throw new Error("--ssl requires both --ssl-cert and --ssl-key");
|
|
54
|
+
}
|
|
55
|
+
tls = {
|
|
56
|
+
enabled: true,
|
|
57
|
+
certificatePath: resolve(certificatePath),
|
|
58
|
+
privateKeyPath: resolve(privateKeyPath),
|
|
59
|
+
...(certificateAuthorityPath
|
|
60
|
+
? { certificateAuthorityPath: resolve(certificateAuthorityPath) }
|
|
61
|
+
: {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (certificatePath || privateKeyPath || certificateAuthorityPath) {
|
|
66
|
+
throw new Error("--ssl-cert, --ssl-key, and --ssl-ca require --ssl");
|
|
67
|
+
}
|
|
68
|
+
tls = { enabled: false };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: "run",
|
|
72
|
+
config: {
|
|
73
|
+
host,
|
|
74
|
+
port: parsePort(values.port),
|
|
75
|
+
homes,
|
|
76
|
+
clientDirectory: fileURLToPath(new URL("../../client/", import.meta.url)),
|
|
77
|
+
tls,
|
|
78
|
+
interactionEnabled: true,
|
|
79
|
+
...(values["upload-dir"] !== undefined
|
|
80
|
+
? { uploadDirectory: resolveUploadDirectory(values["upload-dir"]) }
|
|
81
|
+
: {}),
|
|
82
|
+
...(values["tmux-socket"] !== undefined
|
|
83
|
+
? { tmuxSocket: resolve(nonEmptyOption("tmux-socket", values["tmux-socket"])) }
|
|
84
|
+
: {}),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function commandLineHelp() {
|
|
89
|
+
return `Usage: codex-hub [options]
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
--codex-home <nickname=path>
|
|
93
|
+
Codex home; repeat for multiple homes (default: default=~/.codex)
|
|
94
|
+
--host <host> Listen host (default: 127.0.0.1)
|
|
95
|
+
--port <port> Listen port, or 0 for a free port (default: 4173)
|
|
96
|
+
--ssl Enable TLS; the port will accept HTTPS only
|
|
97
|
+
--ssl-cert <path> PEM server certificate or certificate chain
|
|
98
|
+
--ssl-key <path> PEM server private key
|
|
99
|
+
--ssl-ca <path> PEM CA bundle; enables mandatory client certificates
|
|
100
|
+
--tmux-socket <path> tmux socket (default: tmux default selection)
|
|
101
|
+
--upload-dir <path> Store uploads here (default: a persistent process temporary directory)
|
|
102
|
+
--help Show this help
|
|
103
|
+
|
|
104
|
+
TLS example:
|
|
105
|
+
codex-hub --ssl --ssl-cert server.crt --ssl-key server.key
|
|
106
|
+
|
|
107
|
+
mTLS example:
|
|
108
|
+
codex-hub --ssl --ssl-cert server.crt --ssl-key server.key --ssl-ca ca.crt`;
|
|
109
|
+
}
|
|
110
|
+
function parseHomes(values) {
|
|
111
|
+
const names = new Set();
|
|
112
|
+
const paths = new Set();
|
|
113
|
+
return values.map((value) => {
|
|
114
|
+
const separator = value.indexOf("=");
|
|
115
|
+
const nickname = value.slice(0, separator).trim();
|
|
116
|
+
const homePath = value.slice(separator + 1).trim();
|
|
117
|
+
if (separator < 0 || !nickname || !homePath) {
|
|
118
|
+
throw new Error("--codex-home requires nickname=path with a non-empty nickname and path");
|
|
119
|
+
}
|
|
120
|
+
const path = resolve(homePath.startsWith("~/") ? resolve(homedir(), homePath.slice(2)) : homePath);
|
|
121
|
+
let canonical = path;
|
|
122
|
+
try {
|
|
123
|
+
canonical = realpathSync(path);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!(error &&
|
|
127
|
+
typeof error === "object" &&
|
|
128
|
+
"code" in error &&
|
|
129
|
+
["ENOENT", "EACCES", "EPERM"].includes(String(error.code))))
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (names.has(nickname))
|
|
133
|
+
throw new Error(`Duplicate home nickname: ${nickname}`);
|
|
134
|
+
if (paths.has(canonical))
|
|
135
|
+
throw new Error("Duplicate home path");
|
|
136
|
+
names.add(nickname);
|
|
137
|
+
paths.add(canonical);
|
|
138
|
+
return { nickname, path };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function resolveUploadDirectory(value) {
|
|
142
|
+
const path = nonEmptyOption("upload-dir", value);
|
|
143
|
+
return resolve(path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createCodexSessionSource } from "./adapters/codex/codex-session-source.js";
|
|
2
|
+
import { createCodexSessionManager } from "./adapters/codex/session-manager.js";
|
|
3
|
+
import { SessionReadService, } from "./application/session-read-service.js";
|
|
4
|
+
import { SessionCatalogService } from "./catalog/session-catalog-service.js";
|
|
5
|
+
import { SessionInteractionService } from "./interaction/interaction-service.js";
|
|
6
|
+
import { paneBindings } from "./interaction/pane-binding-manager.js";
|
|
7
|
+
import { SessionRuntime } from "./interaction/session-runtime.js";
|
|
8
|
+
import { TmuxInteractionService } from "./interaction/tmux-service.js";
|
|
9
|
+
import { OperationService } from "./management/operation-service.js";
|
|
10
|
+
import { SessionManagerRegistry } from "./management/session-manager.js";
|
|
11
|
+
import { SessionUpdatesService } from "./updates/session-updates-service.js";
|
|
12
|
+
function createSessionReadService(sources, dependencies) {
|
|
13
|
+
return new SessionReadService(sources, undefined, undefined, dependencies);
|
|
14
|
+
}
|
|
15
|
+
export async function createCodexSessionReadService(codexHome) {
|
|
16
|
+
const source = await createCodexSessionSource(codexHome, paneBindings);
|
|
17
|
+
return createSessionReadService([source], {
|
|
18
|
+
resolveNickname: (session) => source.nicknameFor(session),
|
|
19
|
+
homes: [{ sourceInstanceId: source.descriptor.sourceInstanceId, nickname: "default" }],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Compose reading and management using the adapter's published source identity. */
|
|
23
|
+
export async function createCodexHub(homes, socket, interactionEnabled = true) {
|
|
24
|
+
const sources = await Promise.all(homes.map((home) => createCodexSessionSource(home.path, paneBindings)));
|
|
25
|
+
const sessions = createSessionReadService(sources, {
|
|
26
|
+
resolveNickname: (session) => sources
|
|
27
|
+
.find((source) => source.descriptor.sourceInstanceId === session.origin.sourceInstanceId)
|
|
28
|
+
?.nicknameFor(session) ?? null,
|
|
29
|
+
homes: sources.map((source, index) => ({
|
|
30
|
+
sourceInstanceId: source.descriptor.sourceInstanceId,
|
|
31
|
+
nickname: homes[index].nickname,
|
|
32
|
+
})),
|
|
33
|
+
});
|
|
34
|
+
const managers = new SessionManagerRegistry(sources[0].descriptor.sourceInstanceId);
|
|
35
|
+
sources.forEach((source, index) => managers.register(source.descriptor.sourceInstanceId, createCodexSessionManager(homes[index].path, socket)));
|
|
36
|
+
const transport = new TmuxInteractionService();
|
|
37
|
+
const interaction = new SessionInteractionService(sessions, interactionEnabled, transport, paneBindings);
|
|
38
|
+
const runtime = new SessionRuntime(transport, managers, paneBindings);
|
|
39
|
+
sessions.runtime = runtime;
|
|
40
|
+
interaction.runtime = runtime;
|
|
41
|
+
const cleanup = [() => interaction.close()];
|
|
42
|
+
let closed = false;
|
|
43
|
+
const close = () => {
|
|
44
|
+
if (closed)
|
|
45
|
+
return;
|
|
46
|
+
closed = true;
|
|
47
|
+
for (const release of cleanup)
|
|
48
|
+
release();
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const catalog = new SessionCatalogService(sessions);
|
|
52
|
+
cleanup.push(() => catalog.close());
|
|
53
|
+
const updates = new SessionUpdatesService(sessions, { interaction });
|
|
54
|
+
cleanup.push(() => updates.close());
|
|
55
|
+
const operations = new OperationService(sessions, interaction, managers, undefined, undefined, paneBindings, runtime);
|
|
56
|
+
cleanup.push(() => operations.close());
|
|
57
|
+
return { sessions, managers, interaction, catalog, updates, operations, close };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
close();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=create-hub.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,63 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { HttpQueryError } from "./query-params.js";
|
|
3
|
+
import { RequestBodyError } from "./request-body.js";
|
|
4
|
+
import { sendJson } from "./router.js";
|
|
5
|
+
import { createSessionInteractionRoutes } from "./session-interaction-routes.js";
|
|
6
|
+
import { createSessionManagementRoutes } from "./session-management-routes.js";
|
|
7
|
+
import { createSessionReadRoutes } from "./session-read-routes.js";
|
|
8
|
+
import { createSessionUpdatesRoutes } from "./session-updates-routes.js";
|
|
9
|
+
import { createUploadRoutes } from "./upload-routes.js";
|
|
10
|
+
const CONSOLE_ERROR_LOGGER = {
|
|
11
|
+
error(message, context) {
|
|
12
|
+
console.error(`${message} [requestId=${context.requestId}]`, context.error);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export function createApiRouter(dependencies, options = {}) {
|
|
16
|
+
const logger = options.logger ?? CONSOLE_ERROR_LOGGER;
|
|
17
|
+
const requestIdFactory = options.requestId ?? randomUUID;
|
|
18
|
+
const handlers = [
|
|
19
|
+
createUploadRoutes(dependencies.uploadDirectory),
|
|
20
|
+
createSessionManagementRoutes(dependencies.operations),
|
|
21
|
+
createSessionUpdatesRoutes(dependencies.updates),
|
|
22
|
+
createSessionInteractionRoutes(dependencies.interaction),
|
|
23
|
+
createSessionReadRoutes(dependencies),
|
|
24
|
+
];
|
|
25
|
+
const router = async (request, response) => {
|
|
26
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
27
|
+
if (!url.pathname.startsWith("/api/"))
|
|
28
|
+
return false;
|
|
29
|
+
const headOnly = request.method === "HEAD";
|
|
30
|
+
const requestId = requestIdFactory();
|
|
31
|
+
for (const handler of handlers.filter((candidate) => candidate.matches(url))) {
|
|
32
|
+
try {
|
|
33
|
+
if (await handler.handle(request, response, url))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof RequestBodyError || error instanceof HttpQueryError) {
|
|
38
|
+
sendJson(response, error instanceof RequestBodyError ? error.status : 400, {
|
|
39
|
+
error: {
|
|
40
|
+
code: error instanceof RequestBodyError ? error.code : "invalid_query",
|
|
41
|
+
message: error.message,
|
|
42
|
+
},
|
|
43
|
+
}, headOnly);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (handler.mapError(error, request, response))
|
|
47
|
+
return true;
|
|
48
|
+
logger.error("Session API request failed", { requestId, error });
|
|
49
|
+
sendJson(response, 500, {
|
|
50
|
+
error: {
|
|
51
|
+
code: "internal_error",
|
|
52
|
+
message: "The local session reader could not complete the request",
|
|
53
|
+
requestId,
|
|
54
|
+
},
|
|
55
|
+
}, headOnly);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
return router;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=api-router.js.map
|