@marcoscale98/pi-extensible-workflows 5.14.1-fork.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/CHANGELOG.md +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { URL } from "node:url";
|
|
6
|
+
import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError } from "../../src/trajectory-contracts.js";
|
|
7
|
+
import { sameFilesystemPath } from "../../src/paths.js";
|
|
8
|
+
const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
type Socket = import("node:stream").Duplex;
|
|
11
|
+
type ClientKind = "publisher" | "browser";
|
|
12
|
+
type Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; backpressured: boolean; superseded: boolean };
|
|
13
|
+
type State = { type: "state"; publishers: readonly unknown[]; updatedAt: number; initial?: boolean; truncated?: boolean };
|
|
14
|
+
type PendingRequest = { requestId: string; kind: "transcript" | "action"; browser: Client; publisherId: string; publisher: Client; target: { runId?: string; agentId?: string; subagentId?: string; revision?: number }; timer: ReturnType<typeof setTimeout> };
|
|
15
|
+
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
16
|
+
const MAX_PENDING_REQUESTS = 128;
|
|
17
|
+
const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
|
|
18
|
+
const ACTION_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
|
+
const TIMING_ENTRY_TYPE = "pi-workflows:tool-timing";
|
|
20
|
+
function isTimingEntry(value: unknown): boolean {
|
|
21
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType === TIMING_ENTRY_TYPE);
|
|
22
|
+
}
|
|
23
|
+
function compactTranscript(value: unknown): unknown {
|
|
24
|
+
if (Array.isArray(value)) return value.filter(isTimingEntry);
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { revision: 0, status: "missing", timing: [] };
|
|
26
|
+
const record = value as Record<string, unknown>;
|
|
27
|
+
return { ...record, ...(Array.isArray(record.timing) ? { timing: record.timing.filter(isTimingEntry) } : {}) };
|
|
28
|
+
}
|
|
29
|
+
function compactRun(run: unknown): unknown {
|
|
30
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return run;
|
|
31
|
+
const record = run as { transcripts?: unknown };
|
|
32
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return { ...record, transcripts: {} };
|
|
33
|
+
const transcripts: Record<string, unknown> = {};
|
|
34
|
+
for (const [id, entries] of Object.entries(record.transcripts as Record<string, unknown>)) transcripts[id] = compactTranscript(entries);
|
|
35
|
+
return { ...record, transcripts };
|
|
36
|
+
}
|
|
37
|
+
function compactSubagent(subagent: unknown): unknown {
|
|
38
|
+
if (!subagent || typeof subagent !== "object" || Array.isArray(subagent)) return subagent;
|
|
39
|
+
const record = subagent as { transcript?: unknown };
|
|
40
|
+
return { ...record, transcript: compactTranscript(record.transcript) };
|
|
41
|
+
}
|
|
42
|
+
function compactPublishers(publishers: readonly unknown[]): unknown[] {
|
|
43
|
+
return publishers.map((publisher) => {
|
|
44
|
+
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
|
|
45
|
+
const value = publisher as { runs?: unknown; subagents?: unknown };
|
|
46
|
+
return { ...value, ...(Array.isArray(value.runs) ? { runs: value.runs.map(compactRun) } : {}), ...(Array.isArray(value.subagents) ? { subagents: value.subagents.map(compactSubagent) } : {}) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const MAX_METADATA_STRING_BYTES = 64 * 1024;
|
|
50
|
+
const MAX_METADATA_ARRAY_ENTRIES = 256;
|
|
51
|
+
const MAX_METADATA_OBJECT_KEYS = 64;
|
|
52
|
+
const METADATA_ARRAY_KEYS = new Set(["agents", "runs", "subagents"]);
|
|
53
|
+
const METADATA_OBJECT_KEYS = new Set(["transcripts"]);
|
|
54
|
+
function boundedMetadata(value: unknown, key = "", depth = 0): unknown {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
const bytes = Buffer.from(value);
|
|
57
|
+
return bytes.length > MAX_METADATA_STRING_BYTES ? bytes.subarray(0, MAX_METADATA_STRING_BYTES).toString("utf8") : value;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
|
|
60
|
+
if (depth >= 12) return undefined;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
const array = value as readonly unknown[];
|
|
63
|
+
const entries = key === "attemptDetails" ? array.slice(-8) : METADATA_ARRAY_KEYS.has(key) ? array : array.length <= MAX_METADATA_ARRAY_ENTRIES ? array : [...array.slice(0, 32), ...array.slice(-MAX_METADATA_ARRAY_ENTRIES + 32)];
|
|
64
|
+
return entries.map((entry) => boundedMetadata(entry, "", depth + 1));
|
|
65
|
+
}
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
const result: Record<string, unknown> = {};
|
|
68
|
+
const properties = METADATA_OBJECT_KEYS.has(key) ? Object.keys(value).sort() : Object.keys(value).sort().slice(0, MAX_METADATA_OBJECT_KEYS);
|
|
69
|
+
for (const property of properties) result[property] = boundedMetadata((value as Record<string, unknown>)[property], property, depth + 1);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function minimalPublisher(publisher: unknown): unknown {
|
|
75
|
+
return boundedMetadata(publisher);
|
|
76
|
+
}
|
|
77
|
+
function tinyPublisher(publisher: unknown): unknown {
|
|
78
|
+
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return { id: "publisher" };
|
|
79
|
+
const value = publisher as Record<string, unknown>;
|
|
80
|
+
return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined, themes: value.themes, connected: value.connected === true };
|
|
81
|
+
}
|
|
82
|
+
function encodePublisherList(publishers: readonly unknown[], updatedAt: number, maxBytes: number, initial: boolean, truncated: boolean, projector: (publisher: unknown) => unknown, fallback?: (publisher: unknown) => unknown): { serialized: string; truncated: boolean } {
|
|
83
|
+
const prefix = `{"type":"state","publishers":[`;
|
|
84
|
+
const suffix = (isTruncated: boolean): string => `],"updatedAt":${String(updatedAt)}${initial ? ",\"initial\":true" : ""}${isTruncated ? ",\"truncated\":true" : ""}}`;
|
|
85
|
+
const markerBytes = Buffer.byteLength(suffix(true)) - Buffer.byteLength(suffix(false));
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
let bytes = Buffer.byteLength(prefix) + Buffer.byteLength(suffix(false));
|
|
88
|
+
let wasTruncated = truncated;
|
|
89
|
+
for (const publisher of publishers) {
|
|
90
|
+
let serialized: string;
|
|
91
|
+
try { serialized = JSON.stringify(projector(publisher)); } catch { wasTruncated = true; continue; }
|
|
92
|
+
let nextBytes = bytes + Buffer.byteLength(serialized) + (parts.length ? 1 : 0);
|
|
93
|
+
if (nextBytes + markerBytes >= maxBytes && fallback !== undefined) {
|
|
94
|
+
try { serialized = JSON.stringify(fallback(publisher)); } catch { wasTruncated = true; continue; }
|
|
95
|
+
nextBytes = bytes + Buffer.byteLength(serialized) + (parts.length ? 1 : 0);
|
|
96
|
+
}
|
|
97
|
+
if (nextBytes + markerBytes >= maxBytes) { wasTruncated = true; continue; }
|
|
98
|
+
parts.push(serialized);
|
|
99
|
+
bytes = nextBytes;
|
|
100
|
+
}
|
|
101
|
+
const serialized = `${prefix}${parts.join(",")}${suffix(wasTruncated)}`;
|
|
102
|
+
if (Buffer.byteLength(serialized) < maxBytes) return { serialized, truncated: wasTruncated };
|
|
103
|
+
return { serialized: `${prefix}${suffix(true)}`, truncated: true };
|
|
104
|
+
}
|
|
105
|
+
function encodeState(state: State, maxBytes: number): string {
|
|
106
|
+
const initial = state.initial === true;
|
|
107
|
+
const compactedPublishers = compactPublishers(state.publishers);
|
|
108
|
+
const full = encodePublisherList(compactedPublishers, state.updatedAt, maxBytes, initial, state.truncated === true, (publisher) => publisher);
|
|
109
|
+
if (!full.truncated) return full.serialized;
|
|
110
|
+
return encodePublisherList(compactedPublishers, state.updatedAt, maxBytes, initial, true, minimalPublisher, tinyPublisher).serialized;
|
|
111
|
+
}
|
|
112
|
+
function encodeApplication(value: unknown, maxBytes: number): string {
|
|
113
|
+
let serialized: string;
|
|
114
|
+
try { serialized = JSON.stringify(value); } catch { return "{}"; }
|
|
115
|
+
if (Buffer.byteLength(serialized) < maxBytes) return serialized;
|
|
116
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
117
|
+
const record = value as Record<string, unknown>;
|
|
118
|
+
if (record.type === "transcript") serialized = JSON.stringify({ type: "transcript", ...(typeof record.requestId === "string" ? { requestId: record.requestId } : {}), ...(typeof record.publisherId === "string" ? { publisherId: record.publisherId } : {}), ...(typeof record.runId === "string" ? { runId: record.runId } : {}), ...(typeof record.agentId === "string" ? { agentId: record.agentId } : {}), ...(typeof record.subagentId === "string" ? { subagentId: record.subagentId } : {}), ok: false, status: "oversized", revision: typeof record.revision === "number" ? record.revision : 0, error: "Transcript is too large" });
|
|
119
|
+
else if (record.type === "action-result") serialized = JSON.stringify({ type: "action-result", ...(typeof record.requestId === "string" ? { requestId: record.requestId } : {}), ok: false, error: "Trajectory action result is too large" });
|
|
120
|
+
else serialized = JSON.stringify({ type: "error", error: "Trajectory message is too large" });
|
|
121
|
+
}
|
|
122
|
+
return Buffer.byteLength(serialized) < maxBytes ? serialized : "{}";
|
|
123
|
+
}
|
|
124
|
+
function frame(payload: string, maxBytes: number): Buffer {
|
|
125
|
+
const body = Buffer.from(payload);
|
|
126
|
+
if (body.length >= maxBytes) throw new Error("Trajectory WebSocket frame is too large");
|
|
127
|
+
if (body.length < 126) return Buffer.concat([Buffer.from([0x81, body.length]), body]);
|
|
128
|
+
if (body.length <= 0xffff) { const header = Buffer.alloc(4); header[0] = 0x81; header[1] = 126; header.writeUInt16BE(body.length, 2); return Buffer.concat([header, body]); }
|
|
129
|
+
const header = Buffer.alloc(10); header[0] = 0x81; header[1] = 127; header.writeBigUInt64BE(BigInt(body.length), 2); return Buffer.concat([header, body]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isState(value: unknown): value is State {
|
|
133
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "state");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeFrame(client: Client, packet: Buffer, state: boolean): void {
|
|
137
|
+
if (state && client.backpressured) { client.pendingState = packet; return; }
|
|
138
|
+
try {
|
|
139
|
+
if (state) client.pendingState = undefined;
|
|
140
|
+
if (!client.socket.write(packet)) client.backpressured = true;
|
|
141
|
+
} catch { client.socket.destroy(); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function send(client: Client, value: unknown, maxBytes: number): void {
|
|
145
|
+
try {
|
|
146
|
+
const state = isState(value);
|
|
147
|
+
const packet = frame(state ? encodeState(value, maxBytes) : encodeApplication(value, maxBytes), maxBytes);
|
|
148
|
+
writeFrame(client, packet, state);
|
|
149
|
+
} catch { client.socket.destroy(); }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFrames(client: Client, chunk: Buffer, maxBytes: number): readonly string[] {
|
|
153
|
+
client.buffer = Buffer.concat([client.buffer, chunk]);
|
|
154
|
+
if (client.buffer.length > maxBytes + 14) throw new Error("Trajectory WebSocket buffer is too large");
|
|
155
|
+
const messages: string[] = [];
|
|
156
|
+
while (client.buffer.length >= 2) {
|
|
157
|
+
const first = client.buffer[0] ?? 0;
|
|
158
|
+
const second = client.buffer[1] ?? 0;
|
|
159
|
+
const opcode = first & 0x0f;
|
|
160
|
+
const masked = (second & 0x80) !== 0;
|
|
161
|
+
if ((first & 0x70) !== 0 || (first & 0x80) === 0 || !masked) throw new Error("Invalid Trajectory WebSocket frame");
|
|
162
|
+
let offset = 2;
|
|
163
|
+
let length = second & 0x7f;
|
|
164
|
+
if (length === 126) { if (client.buffer.length < 4) break; length = client.buffer.readUInt16BE(2); offset = 4; }
|
|
165
|
+
else if (length === 127) { if (client.buffer.length < 10) break; const longLength = client.buffer.readBigUInt64BE(2); if (longLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Trajectory WebSocket frame is too large"); length = Number(longLength); offset = 10; }
|
|
166
|
+
if (length >= maxBytes) throw new Error("Trajectory WebSocket frame is too large");
|
|
167
|
+
if (opcode >= 0x8 && (length > 125 || (first & 0x80) === 0)) throw new Error("Invalid Trajectory WebSocket control frame");
|
|
168
|
+
if (client.buffer.length < offset + 4 + length) break;
|
|
169
|
+
const mask = client.buffer.subarray(offset, offset + 4); offset += 4;
|
|
170
|
+
const data = client.buffer.subarray(offset, offset + length);
|
|
171
|
+
client.buffer = client.buffer.subarray(offset + length);
|
|
172
|
+
if (opcode === 0x8) { client.socket.end(); break; }
|
|
173
|
+
if (opcode === 0x9) { const pong = Buffer.alloc(2 + length); pong[0] = 0x8a; pong[1] = length; for (let index = 0; index < length; index += 1) pong[index + 2] = (data[index] ?? 0) ^ (mask[index % 4] ?? 0); writeFrame(client, pong, false); continue; }
|
|
174
|
+
if (opcode === 0xA) continue;
|
|
175
|
+
if (opcode !== 0x1) throw new Error("Unsupported Trajectory WebSocket frame");
|
|
176
|
+
const decoded = Buffer.alloc(length);
|
|
177
|
+
for (let index = 0; index < length; index += 1) decoded[index] = (data[index] ?? 0) ^ (mask[index % 4] ?? 0);
|
|
178
|
+
messages.push(decoded.toString("utf8"));
|
|
179
|
+
}
|
|
180
|
+
return messages;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeJson(response: ServerResponse, status: number, value: unknown): void {
|
|
184
|
+
const body = JSON.stringify(value);
|
|
185
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(body), "cache-control": "no-store" });
|
|
186
|
+
response.end(body);
|
|
187
|
+
}
|
|
188
|
+
function authorized(request: IncomingMessage, port: number): boolean {
|
|
189
|
+
const origin = request.headers.origin;
|
|
190
|
+
return origin === undefined || origin === `http://127.0.0.1:${String(port)}` || origin === `http://localhost:${String(port)}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
type TrajectoryServerOptions = { maxFrameBytes?: number; fingerprint?: string };
|
|
194
|
+
export function createTrajectoryServer(port: number, lockPath: string, options: TrajectoryServerOptions = {}): Server {
|
|
195
|
+
const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
|
|
196
|
+
const serverFingerprint = options.fingerprint ?? "";
|
|
197
|
+
const clients = new Set<Client>();
|
|
198
|
+
const publishers = new Map<string, { client: Client; value: Record<string, unknown>; generation: number }>();
|
|
199
|
+
let latest: State = { type: "state", publishers: [], updatedAt: Date.now(), initial: true };
|
|
200
|
+
const pending = new Map<Client, Map<string, PendingRequest>>();
|
|
201
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
202
|
+
let closed = false;
|
|
203
|
+
const emit = (client: Client, value: unknown) => { send(client, value, maxFrameBytes); };
|
|
204
|
+
const pendingFor = (browser: Client): Map<string, PendingRequest> => { let requests = pending.get(browser); if (!requests) { requests = new Map(); pending.set(browser, requests); } return requests; };
|
|
205
|
+
const hasPendingRequest = (requests: ReadonlyMap<string, PendingRequest>, requestId: string): boolean => [...requests.values()].some((request) => request.requestId === requestId);
|
|
206
|
+
const publisherRequestIdFor = (requestId: string): string => [...pending.values()].some((requests) => requests.has(requestId)) ? randomUUID() : requestId;
|
|
207
|
+
const errorResponse = (_publisherRequestId: string, request: PendingRequest, status: string, error: string): unknown => request.kind === "action" ? { type: "action-result", requestId: request.requestId, ok: false, error } : { type: "transcript", requestId: request.requestId, publisherId: request.publisherId, ...(request.target.runId === undefined ? {} : { runId: request.target.runId }), ...(request.target.agentId === undefined ? {} : { agentId: request.target.agentId }), ...(request.target.subagentId === undefined ? {} : { subagentId: request.target.subagentId }), ok: false, status, revision: request.target.revision ?? 0, error };
|
|
208
|
+
const clearPending = (client: Client, error: string): void => {
|
|
209
|
+
const own = pending.get(client);
|
|
210
|
+
if (own) { for (const [requestId, request] of own) { clearTimeout(request.timer); emit(client, errorResponse(requestId, request, "disconnected", error)); } own.clear(); pending.delete(client); }
|
|
211
|
+
for (const [browser, requests] of pending) { for (const [requestId, request] of requests) { if (request.publisher !== client) continue; clearTimeout(request.timer); requests.delete(requestId); emit(browser, errorResponse(requestId, request, "disconnected", error)); } if (!requests.size) pending.delete(browser); }
|
|
212
|
+
};
|
|
213
|
+
const rejectRequest = (browser: Client, requestId: string, request: PendingRequest, error: string): void => {
|
|
214
|
+
const requests = pending.get(browser);
|
|
215
|
+
if (!requests || requests.get(requestId) !== request) return;
|
|
216
|
+
requests.delete(requestId);
|
|
217
|
+
if (!requests.size) pending.delete(browser);
|
|
218
|
+
emit(browser, errorResponse(requestId, request, "failed", error));
|
|
219
|
+
};
|
|
220
|
+
const scheduleIdleExit = () => {
|
|
221
|
+
if (closed || [...publishers.values()].some(({ value }) => value.connected === true) || idleTimer !== undefined) return;
|
|
222
|
+
idleTimer = setTimeout(() => {
|
|
223
|
+
server.closeAllConnections();
|
|
224
|
+
for (const client of clients) client.socket.destroy();
|
|
225
|
+
server.close(() => {
|
|
226
|
+
void rm(lockPath, { force: true }).then(() => { process.exit(0); }).catch(() => { process.exit(1); });
|
|
227
|
+
});
|
|
228
|
+
}, TRAJECTORY_IDLE_EXIT_MS);
|
|
229
|
+
idleTimer.unref();
|
|
230
|
+
};
|
|
231
|
+
const cancelIdleExit = () => {
|
|
232
|
+
if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; }
|
|
233
|
+
};
|
|
234
|
+
const broadcast = (value: unknown) => {
|
|
235
|
+
if (isState(value)) {
|
|
236
|
+
try {
|
|
237
|
+
const stateFrame = frame(encodeState(value, maxFrameBytes), maxFrameBytes);
|
|
238
|
+
for (const client of clients) if (client.kind === "browser") writeFrame(client, stateFrame, true);
|
|
239
|
+
} catch {
|
|
240
|
+
for (const client of clients) if (client.kind === "browser") client.socket.destroy();
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const client of clients) if (client.kind === "browser") emit(client, value);
|
|
245
|
+
};
|
|
246
|
+
const publishState = () => {
|
|
247
|
+
const values = [...publishers.values()].map(({ value, generation }) => ({ ...value, generation }));
|
|
248
|
+
latest = { type: "state", publishers: values, updatedAt: Date.now() };
|
|
249
|
+
broadcast(latest);
|
|
250
|
+
};
|
|
251
|
+
const disconnect = (client: Client) => {
|
|
252
|
+
clearPending(client, client.kind === "publisher" ? "Publisher is disconnected" : "Trajectory browser disconnected");
|
|
253
|
+
clients.delete(client);
|
|
254
|
+
if (client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) {
|
|
255
|
+
publishers.delete(client.publisherId);
|
|
256
|
+
publishState();
|
|
257
|
+
scheduleIdleExit();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const handleMessage = (client: Client, raw: string) => {
|
|
261
|
+
let value: unknown;
|
|
262
|
+
try { value = JSON.parse(raw); } catch { return; }
|
|
263
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
264
|
+
const message = value as Record<string, unknown>;
|
|
265
|
+
if (client.superseded) return;
|
|
266
|
+
if (message.type === "publisher:detach" && client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) { clearPending(client, "Publisher is disconnected"); publishers.delete(client.publisherId); publishState(); scheduleIdleExit(); return; }
|
|
267
|
+
if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); return; }
|
|
268
|
+
if (client.kind === "publisher" && message.type === "publisher:attach" && typeof message.publisherId === "string") {
|
|
269
|
+
cancelIdleExit();
|
|
270
|
+
client.publisherId = message.publisherId;
|
|
271
|
+
const previous = publishers.get(message.publisherId);
|
|
272
|
+
if (previous && previous.client !== client) {
|
|
273
|
+
previous.client.superseded = true;
|
|
274
|
+
clearPending(previous.client, "Publisher replaced");
|
|
275
|
+
emit(previous.client, { type: "publisher:replaced" });
|
|
276
|
+
previous.client.socket.end();
|
|
277
|
+
}
|
|
278
|
+
const generation = (previous?.generation ?? 0) + 1;
|
|
279
|
+
publishers.set(message.publisherId, { client, value: { ...(previous?.value ?? { id: message.publisherId }), id: message.publisherId, connected: true }, generation });
|
|
280
|
+
publishState();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (client.kind === "publisher" && message.type === "publisher:state" && typeof message.publisher === "object" && message.publisher !== null) {
|
|
284
|
+
const publisher = message.publisher as Record<string, unknown>;
|
|
285
|
+
const id = typeof publisher.id === "string" ? publisher.id : client.publisherId;
|
|
286
|
+
if (!id || client.publisherId !== id || publishers.get(id)?.client !== client) return;
|
|
287
|
+
cancelIdleExit();
|
|
288
|
+
const previous = publishers.get(id);
|
|
289
|
+
const generation = previous?.generation ?? 1;
|
|
290
|
+
const runs = Array.isArray(message.runs) ? message.runs : [];
|
|
291
|
+
const subagents = Array.isArray(message.subagents) ? message.subagents : [];
|
|
292
|
+
publishers.set(id, { client, value: { ...publisher, connected: true, runs, subagents, generation, ...(message.truncated === true || publisher.truncated === true ? { truncated: true } : {}) }, generation });
|
|
293
|
+
publishState();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (client.kind === "publisher" && (message.type === "publisher:action-result" || message.type === "publisher:transcript-result") && typeof message.requestId === "string") {
|
|
297
|
+
for (const [browser, requests] of pending) {
|
|
298
|
+
const request = requests.get(message.requestId);
|
|
299
|
+
if (!request || request.publisher !== client || request.publisherId !== message.publisherId || message.type !== (request.kind === "action" ? "publisher:action-result" : "publisher:transcript-result")) continue;
|
|
300
|
+
const responseRevision = message.requestedRevision ?? message.revision;
|
|
301
|
+
const targetMatches = request.kind === "action" || (request.target.runId === message.runId && request.target.agentId === message.agentId && request.target.subagentId === message.subagentId && (request.target.revision === undefined || request.target.revision === responseRevision));
|
|
302
|
+
if (!targetMatches) return;
|
|
303
|
+
clearTimeout(request.timer);
|
|
304
|
+
requests.delete(message.requestId);
|
|
305
|
+
if (!requests.size) pending.delete(browser);
|
|
306
|
+
if (request.kind === "action") emit(browser, { type: "action-result", requestId: request.requestId, ...(message.ok === true ? { ok: true, ...(message.result === undefined ? {} : { result: message.result }) } : { ok: false, error: typeof message.error === "string" ? message.error : "Trajectory action failed" }) });
|
|
307
|
+
else emit(browser, { type: "transcript", requestId: request.requestId, publisherId: request.publisherId, ...(request.target.runId === undefined ? {} : { runId: request.target.runId }), ...(request.target.agentId === undefined ? {} : { agentId: request.target.agentId }), ...(request.target.subagentId === undefined ? {} : { subagentId: request.target.subagentId }), ok: message.ok === true, status: typeof message.status === "string" ? message.status : message.ok === true ? "available" : "failed", revision: typeof message.revision === "number" ? message.revision : request.target.revision ?? 0, ...(message.ok === true ? { entries: Array.isArray(message.entries) ? message.entries : [] } : { error: typeof message.error === "string" ? message.error : "Transcript failed" }) });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (client.kind === "browser" && message.type === "ui:transcript") {
|
|
313
|
+
const validId = (value: unknown): value is string => typeof value === "string" && value.length >= 1 && value.length <= 200;
|
|
314
|
+
if (!validId(message.publisherId)) return;
|
|
315
|
+
const runRequest = validId(message.runId) && validId(message.agentId) && message.subagentId === undefined;
|
|
316
|
+
const subagentRequest = validId(message.subagentId) && message.runId === undefined && message.agentId === undefined;
|
|
317
|
+
if (!runRequest && !subagentRequest) return;
|
|
318
|
+
const requestId = validId(message.requestId) ? message.requestId : randomUUID();
|
|
319
|
+
const revision = message.revision === undefined ? undefined : typeof message.revision === "number" && Number.isSafeInteger(message.revision) && message.revision >= 0 ? message.revision : undefined;
|
|
320
|
+
const target = publishers.get(message.publisherId);
|
|
321
|
+
if (!target || !target.value.connected) { emit(client, { type: "transcript", requestId, publisherId: message.publisherId, ...(runRequest ? { runId: message.runId, agentId: message.agentId } : { subagentId: message.subagentId }), ok: false, status: "disconnected", revision: revision ?? 0, error: "Publisher is disconnected" }); return; }
|
|
322
|
+
// Legacy publishers sent transcript bodies in state. New publishers send metadata and use the RPC below.
|
|
323
|
+
let legacyEntries: unknown[] | undefined;
|
|
324
|
+
if (runRequest && Array.isArray(target.value.runs)) {
|
|
325
|
+
for (const item of target.value.runs) {
|
|
326
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
327
|
+
const record = item as { run?: { id?: unknown }; transcripts?: unknown };
|
|
328
|
+
if (record.run?.id !== message.runId || !record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) continue;
|
|
329
|
+
const found = (record.transcripts as Record<string, unknown>)[message.agentId as string];
|
|
330
|
+
if (Array.isArray(found)) legacyEntries = found;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
} else if (subagentRequest && Array.isArray(target.value.subagents)) {
|
|
334
|
+
for (const item of target.value.subagents) {
|
|
335
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
336
|
+
const record = item as { id?: unknown; transcript?: unknown };
|
|
337
|
+
if (record.id === message.subagentId && Array.isArray(record.transcript)) { legacyEntries = record.transcript; break; }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (legacyEntries !== undefined) {
|
|
341
|
+
const reply = { type: "transcript", requestId, publisherId: message.publisherId, ...(runRequest ? { runId: message.runId, agentId: message.agentId } : { subagentId: message.subagentId }), ok: true, status: legacyEntries.length ? "available" : "empty", revision: revision ?? 1, entries: legacyEntries };
|
|
342
|
+
if (Buffer.byteLength(JSON.stringify(reply)) > maxFrameBytes) emit(client, { ...reply, ok: false, status: "oversized", entries: undefined, error: "Transcript is too large" }); else emit(client, reply);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const requests = pendingFor(client);
|
|
346
|
+
const requestTarget = runRequest ? { runId: message.runId as string, agentId: message.agentId as string, ...(revision === undefined ? {} : { revision }) } : { subagentId: message.subagentId as string, ...(revision === undefined ? {} : { revision }) };
|
|
347
|
+
if (requests.size >= MAX_PENDING_REQUESTS || hasPendingRequest(requests, requestId)) { emit(client, { type: "transcript", requestId, publisherId: message.publisherId, ...(runRequest ? { runId: message.runId, agentId: message.agentId } : { subagentId: message.subagentId }), ok: false, status: "failed", revision: revision ?? 0, error: "Too many pending Trajectory requests" }); return; }
|
|
348
|
+
const publisherRequestId = publisherRequestIdFor(requestId);
|
|
349
|
+
const request: PendingRequest = { requestId, kind: "transcript", browser: client, publisherId: message.publisherId, publisher: target.client, target: requestTarget, timer: setTimeout(() => { rejectRequest(client, publisherRequestId, request, "Trajectory request timed out"); }, TRANSCRIPT_REQUEST_TIMEOUT_MS) };
|
|
350
|
+
requests.set(publisherRequestId, request);
|
|
351
|
+
emit(target.client, { type: "publisher:transcript", requestId: publisherRequestId, publisherId: message.publisherId, ...(runRequest ? { runId: message.runId, agentId: message.agentId } : { subagentId: message.subagentId }), ...(revision === undefined ? {} : { revision }) });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (client.kind !== "browser" || message.type !== "ui:action" || typeof message.publisherId !== "string") return;
|
|
355
|
+
const requestId = typeof message.requestId === "string" && message.requestId.length >= 1 && message.requestId.length <= 200 ? message.requestId : randomUUID();
|
|
356
|
+
if (!isTrajectoryAction(message.action)) { emit(client, { type: "action-result", requestId, ok: false, error: "Unsupported Trajectory action" }); return; }
|
|
357
|
+
if (!isTrajectoryTarget(message.target)) { emit(client, { type: "action-result", requestId, ok: false, error: "Invalid Trajectory action target" }); return; }
|
|
358
|
+
const actionError = trajectoryActionError(message.action, message.target);
|
|
359
|
+
if (actionError !== undefined) { emit(client, { type: "action-result", requestId, ok: false, error: actionError }); return; }
|
|
360
|
+
const target = publishers.get(message.publisherId);
|
|
361
|
+
if (!target || !target.value.connected) { emit(client, { type: "action-result", requestId, ok: false, error: "Publisher is disconnected" }); return; }
|
|
362
|
+
const publisherRequestId = publisherRequestIdFor(requestId);
|
|
363
|
+
const outbound = { type: "publisher:action", requestId: publisherRequestId, action: message.action, target: message.target, ...(typeof message.name === "string" ? { name: message.name } : {}), ...(message.payload === undefined ? {} : { payload: message.payload }) };
|
|
364
|
+
if (Buffer.byteLength(JSON.stringify(outbound)) >= maxFrameBytes) { emit(client, { type: "action-result", requestId, ok: false, error: "Trajectory action is too large" }); return; }
|
|
365
|
+
const requests = pendingFor(client);
|
|
366
|
+
if (requests.size >= MAX_PENDING_REQUESTS || hasPendingRequest(requests, requestId)) { emit(client, { type: "action-result", requestId, ok: false, error: "Too many pending Trajectory requests" }); return; }
|
|
367
|
+
const request: PendingRequest = { requestId, kind: "action", browser: client, publisherId: message.publisherId, publisher: target.client, target: {}, timer: setTimeout(() => { rejectRequest(client, publisherRequestId, request, "Trajectory request timed out"); }, ACTION_REQUEST_TIMEOUT_MS) };
|
|
368
|
+
requests.set(publisherRequestId, request);
|
|
369
|
+
emit(target.client, outbound);
|
|
370
|
+
};
|
|
371
|
+
const server = createServer((request, response) => {
|
|
372
|
+
let url: URL;
|
|
373
|
+
try { url = new URL(request.url ?? "/", `http://127.0.0.1:${String(port)}`); }
|
|
374
|
+
catch { writeJson(response, 400, { error: "Invalid request" }); return; }
|
|
375
|
+
if (!authorized(request, port)) { writeJson(response, 403, { error: "Forbidden" }); return; }
|
|
376
|
+
const path = url.pathname;
|
|
377
|
+
if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true }); return; }
|
|
378
|
+
if (request.method === "GET" && (path === "/" || path === "/index.html")) {
|
|
379
|
+
void readFile(new URL("./assets/index.html", import.meta.url)).then((html) => {
|
|
380
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": html.byteLength, "cache-control": "no-store" });
|
|
381
|
+
response.end(html);
|
|
382
|
+
}).catch(() => { writeJson(response, 500, { error: "Trajectory UI is unavailable" }); });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (request.method === "GET" && path === "/marked.min.js") {
|
|
386
|
+
void readFile(new URL("./assets/marked.min.js", import.meta.url)).then((script) => {
|
|
387
|
+
response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "content-length": script.byteLength, "cache-control": "no-store" });
|
|
388
|
+
response.end(script);
|
|
389
|
+
}).catch(() => { writeJson(response, 500, { error: "Trajectory markdown renderer is unavailable" }); });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (request.method === "GET" && path === "/morphdom.min.js") {
|
|
393
|
+
void readFile(new URL("./assets/morphdom.min.js", import.meta.url)).then((script) => {
|
|
394
|
+
response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "content-length": script.byteLength, "cache-control": "no-store" });
|
|
395
|
+
response.end(script);
|
|
396
|
+
}).catch(() => { writeJson(response, 500, { error: "Trajectory DOM diffing library is unavailable" }); });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (request.method === "GET" && (path === "/favicon.png" || path === "/favicon.ico")) {
|
|
400
|
+
void readFile(new URL("./assets/favicon.png", import.meta.url)).then((icon) => {
|
|
401
|
+
response.writeHead(200, { "content-type": "image/png", "content-length": icon.byteLength, "cache-control": "no-store" });
|
|
402
|
+
response.end(icon);
|
|
403
|
+
}).catch(() => { writeJson(response, 404, { error: "Not found" }); });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
writeJson(response, 404, { error: "Not found" });
|
|
407
|
+
});
|
|
408
|
+
server.on("upgrade", (request, socket) => {
|
|
409
|
+
let url: URL;
|
|
410
|
+
try { url = new URL(request.url ?? "/", `http://127.0.0.1:${String(port)}`); }
|
|
411
|
+
catch { socket.destroy(); return; }
|
|
412
|
+
const key = request.headers["sec-websocket-key"];
|
|
413
|
+
if (!authorized(request, port) || url.pathname !== "/ws" || typeof key !== "string") { socket.destroy(); return; }
|
|
414
|
+
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
415
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
416
|
+
const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, backpressured: false, superseded: false };
|
|
417
|
+
socket.on("drain", () => {
|
|
418
|
+
const pendingState = client.pendingState;
|
|
419
|
+
client.pendingState = undefined;
|
|
420
|
+
client.backpressured = false;
|
|
421
|
+
if (pendingState !== undefined) writeFrame(client, pendingState, true);
|
|
422
|
+
});
|
|
423
|
+
clients.add(client);
|
|
424
|
+
socket.on("data", (chunk: unknown) => {
|
|
425
|
+
if (!Buffer.isBuffer(chunk)) return;
|
|
426
|
+
try { for (const message of parseFrames(client, chunk, maxFrameBytes)) handleMessage(client, message); } catch { socket.destroy(); }
|
|
427
|
+
});
|
|
428
|
+
socket.on("close", () => { disconnect(client); });
|
|
429
|
+
socket.on("error", () => { disconnect(client); });
|
|
430
|
+
});
|
|
431
|
+
server.once("listening", () => { void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt: Date.now() })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
|
|
432
|
+
server.on("close", () => { closed = true; if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; } });
|
|
433
|
+
return server;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function main(): Promise<void> {
|
|
437
|
+
const args = new Map<string, string>();
|
|
438
|
+
for (let index = 2; index + 1 < process.argv.length; index += 2) args.set(process.argv[index] ?? "", process.argv[index + 1] ?? "");
|
|
439
|
+
const port = Number(args.get("--port"));
|
|
440
|
+
const lockPath = args.get("--lock");
|
|
441
|
+
const fingerprint = args.get("--fingerprint");
|
|
442
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535 || !lockPath || !fingerprint) throw new Error("Invalid Trajectory server arguments");
|
|
443
|
+
const server = createTrajectoryServer(port, lockPath, { fingerprint });
|
|
444
|
+
await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", () => { resolve(); }); });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) void main().catch((error: unknown) => { console.error(error); process.exitCode = 1; });
|