@roamcode.ai/server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/auth.d.ts +63 -0
- package/dist/auth.js +133 -0
- package/dist/claude-auth-service.d.ts +76 -0
- package/dist/claude-auth-service.js +217 -0
- package/dist/claude-latest-service.d.ts +34 -0
- package/dist/claude-latest-service.js +61 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +59 -0
- package/dist/data-dir.d.ts +42 -0
- package/dist/data-dir.js +70 -0
- package/dist/diag.d.ts +43 -0
- package/dist/diag.js +83 -0
- package/dist/fs-service.d.ts +90 -0
- package/dist/fs-service.js +290 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +44 -0
- package/dist/managed-runtime.d.ts +51 -0
- package/dist/managed-runtime.js +411 -0
- package/dist/managed-update-helper.d.ts +2 -0
- package/dist/managed-update-helper.js +34 -0
- package/dist/mcp-send.d.ts +33 -0
- package/dist/mcp-send.js +107 -0
- package/dist/origin-check.d.ts +37 -0
- package/dist/origin-check.js +101 -0
- package/dist/pane-status.d.ts +61 -0
- package/dist/pane-status.js +145 -0
- package/dist/providers/claude-metadata-service.d.ts +58 -0
- package/dist/providers/claude-metadata-service.js +352 -0
- package/dist/providers/claude-provider.d.ts +11 -0
- package/dist/providers/claude-provider.js +166 -0
- package/dist/providers/codex-activity.d.ts +21 -0
- package/dist/providers/codex-activity.js +122 -0
- package/dist/providers/codex-app-server-client.d.ts +90 -0
- package/dist/providers/codex-app-server-client.js +485 -0
- package/dist/providers/codex-latest-service.d.ts +50 -0
- package/dist/providers/codex-latest-service.js +174 -0
- package/dist/providers/codex-metadata-service.d.ts +161 -0
- package/dist/providers/codex-metadata-service.js +686 -0
- package/dist/providers/codex-profile-client.d.ts +16 -0
- package/dist/providers/codex-profile-client.js +52 -0
- package/dist/providers/codex-profile-security.d.ts +23 -0
- package/dist/providers/codex-profile-security.js +161 -0
- package/dist/providers/codex-provider.d.ts +15 -0
- package/dist/providers/codex-provider.js +174 -0
- package/dist/providers/codex-thread-coordinator.d.ts +18 -0
- package/dist/providers/codex-thread-coordinator.js +93 -0
- package/dist/providers/codex-thread-persistence.d.ts +9 -0
- package/dist/providers/codex-thread-persistence.js +45 -0
- package/dist/providers/codex-thread-resolver.d.ts +59 -0
- package/dist/providers/codex-thread-resolver.js +322 -0
- package/dist/providers/options.d.ts +7 -0
- package/dist/providers/options.js +155 -0
- package/dist/providers/provider-artifacts.d.ts +3 -0
- package/dist/providers/provider-artifacts.js +30 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +23 -0
- package/dist/providers/types.d.ts +95 -0
- package/dist/providers/types.js +8 -0
- package/dist/push-dispatch.d.ts +81 -0
- package/dist/push-dispatch.js +100 -0
- package/dist/push-store.d.ts +25 -0
- package/dist/push-store.js +79 -0
- package/dist/rate-limit.d.ts +52 -0
- package/dist/rate-limit.js +72 -0
- package/dist/server-config.d.ts +60 -0
- package/dist/server-config.js +77 -0
- package/dist/service-install.d.ts +60 -0
- package/dist/service-install.js +221 -0
- package/dist/session-defaults.d.ts +26 -0
- package/dist/session-defaults.js +60 -0
- package/dist/session-store.d.ts +81 -0
- package/dist/session-store.js +654 -0
- package/dist/start.d.ts +31 -0
- package/dist/start.js +372 -0
- package/dist/static-routes.d.ts +101 -0
- package/dist/static-routes.js +188 -0
- package/dist/terminal-capability.d.ts +5 -0
- package/dist/terminal-capability.js +27 -0
- package/dist/terminal-manager.d.ts +224 -0
- package/dist/terminal-manager.js +917 -0
- package/dist/terminal-process.d.ts +85 -0
- package/dist/terminal-process.js +238 -0
- package/dist/terminal-shared.d.ts +36 -0
- package/dist/terminal-shared.js +43 -0
- package/dist/tmux-list.d.ts +11 -0
- package/dist/tmux-list.js +39 -0
- package/dist/transport.d.ts +123 -0
- package/dist/transport.js +1559 -0
- package/dist/updater.d.ts +161 -0
- package/dist/updater.js +451 -0
- package/dist/usage-service.d.ts +118 -0
- package/dist/usage-service.js +173 -0
- package/dist/vapid.d.ts +17 -0
- package/dist/vapid.js +31 -0
- package/dist/web-push-send.d.ts +20 -0
- package/dist/web-push-send.js +21 -0
- package/dist/ws-ticket.d.ts +47 -0
- package/dist/ws-ticket.js +62 -0
- package/package.json +55 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { type ZodType } from "zod";
|
|
3
|
+
export declare const CODEX_METADATA_ERROR_CODE: "CODEX_METADATA_UNAVAILABLE";
|
|
4
|
+
export declare const DEFAULT_CODEX_APP_SERVER_TIMEOUT_MS = 15000;
|
|
5
|
+
export declare const DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_LINE_BYTES: number;
|
|
6
|
+
export declare const DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_BUFFER_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_CODEX_APP_SERVER_MAX_STDERR_BYTES: number;
|
|
8
|
+
type DiagnosticIssue = "exit" | "malformed_json" | "notification_listener" | "spawn" | "stderr_limit" | "stdout_limit" | "transport_error" | "unknown_message" | "write_error";
|
|
9
|
+
export interface CodexMetadataDiagnostics {
|
|
10
|
+
readonly lastIssue?: DiagnosticIssue;
|
|
11
|
+
readonly stderrBytes: number;
|
|
12
|
+
readonly stderrTruncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface CodexAppServerNotification {
|
|
15
|
+
readonly method: string;
|
|
16
|
+
readonly params?: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface WritableTransport extends EventEmitter {
|
|
19
|
+
write(value: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
|
|
20
|
+
end(): void;
|
|
21
|
+
}
|
|
22
|
+
type ReadableTransport = EventEmitter;
|
|
23
|
+
/** The intentionally small child-process surface used by the protocol client and its fake tests. */
|
|
24
|
+
export interface CodexAppServerTransport extends EventEmitter {
|
|
25
|
+
readonly stdin: WritableTransport;
|
|
26
|
+
readonly stdout: ReadableTransport;
|
|
27
|
+
readonly stderr: ReadableTransport;
|
|
28
|
+
kill(signal?: NodeJS.Signals): boolean | void;
|
|
29
|
+
}
|
|
30
|
+
export type SpawnCodexAppServerTransport = (command: string, args: readonly string[], options?: {
|
|
31
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
32
|
+
}) => CodexAppServerTransport;
|
|
33
|
+
export interface CodexAppServerClientOptions {
|
|
34
|
+
readonly codexBin?: string;
|
|
35
|
+
/** Selects one validated `$CODEX_HOME/<name>.config.toml` layer before app-server starts. */
|
|
36
|
+
readonly profile?: string;
|
|
37
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
38
|
+
readonly spawnTransport?: SpawnCodexAppServerTransport;
|
|
39
|
+
readonly timeoutMs?: number;
|
|
40
|
+
readonly maxStdoutLineBytes?: number;
|
|
41
|
+
readonly maxStdoutBufferBytes?: number;
|
|
42
|
+
readonly maxStderrBytes?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class CodexMetadataUnavailableError extends Error {
|
|
45
|
+
readonly code: "CODEX_METADATA_UNAVAILABLE";
|
|
46
|
+
constructor();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Bounded JSON-lines client for the auxiliary Codex app-server metadata channel.
|
|
50
|
+
* It never participates in terminal streaming and deliberately knows no method-specific result schemas.
|
|
51
|
+
*/
|
|
52
|
+
export declare class CodexAppServerClient {
|
|
53
|
+
readonly limits: {
|
|
54
|
+
readonly timeoutMs: number;
|
|
55
|
+
readonly maxStdoutLineBytes: number;
|
|
56
|
+
readonly maxStdoutBufferBytes: number;
|
|
57
|
+
readonly maxStderrBytes: number;
|
|
58
|
+
};
|
|
59
|
+
private readonly codexBin;
|
|
60
|
+
private readonly profile?;
|
|
61
|
+
private readonly env?;
|
|
62
|
+
private readonly spawnTransport;
|
|
63
|
+
private readonly notificationListeners;
|
|
64
|
+
private active?;
|
|
65
|
+
private startPromise?;
|
|
66
|
+
private stopPromise?;
|
|
67
|
+
private nextRequestId;
|
|
68
|
+
private nextGeneration;
|
|
69
|
+
private lastDiagnostics;
|
|
70
|
+
constructor(options?: CodexAppServerClientOptions);
|
|
71
|
+
get diagnostics(): CodexMetadataDiagnostics;
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
request<T>(method: string, params: unknown, schema: ZodType<T>): Promise<T>;
|
|
74
|
+
onNotification(listener: (notification: CodexAppServerNotification) => void): () => void;
|
|
75
|
+
stop(): Promise<void>;
|
|
76
|
+
private createGeneration;
|
|
77
|
+
private installListeners;
|
|
78
|
+
private sendRequest;
|
|
79
|
+
private writeFrame;
|
|
80
|
+
private flushWrites;
|
|
81
|
+
private onStdout;
|
|
82
|
+
private onMessage;
|
|
83
|
+
private resolveResponse;
|
|
84
|
+
private rejectResponse;
|
|
85
|
+
private onStderr;
|
|
86
|
+
private failGeneration;
|
|
87
|
+
private removeListeners;
|
|
88
|
+
private recordIssue;
|
|
89
|
+
}
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const CODEX_METADATA_ERROR_CODE = "CODEX_METADATA_UNAVAILABLE";
|
|
5
|
+
export const DEFAULT_CODEX_APP_SERVER_TIMEOUT_MS = 15_000;
|
|
6
|
+
export const DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_LINE_BYTES = 1024 * 1024;
|
|
7
|
+
export const DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
export const DEFAULT_CODEX_APP_SERVER_MAX_STDERR_BYTES = 64 * 1024;
|
|
9
|
+
export class CodexMetadataUnavailableError extends Error {
|
|
10
|
+
code = CODEX_METADATA_ERROR_CODE;
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Codex metadata is unavailable");
|
|
13
|
+
this.name = "CodexMetadataUnavailableError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const JsonRpcSuccessSchema = z.object({
|
|
17
|
+
jsonrpc: z.literal("2.0").optional(),
|
|
18
|
+
id: z.number().int().nonnegative(),
|
|
19
|
+
result: z.unknown(),
|
|
20
|
+
});
|
|
21
|
+
const JsonRpcErrorSchema = z.object({
|
|
22
|
+
jsonrpc: z.literal("2.0").optional(),
|
|
23
|
+
id: z.number().int().nonnegative(),
|
|
24
|
+
error: z.object({
|
|
25
|
+
code: z.number().int(),
|
|
26
|
+
message: z.string(),
|
|
27
|
+
data: z.unknown().optional(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const JsonRpcNotificationSchema = z.object({
|
|
31
|
+
jsonrpc: z.literal("2.0").optional(),
|
|
32
|
+
method: z.string().min(1),
|
|
33
|
+
params: z.unknown().optional(),
|
|
34
|
+
});
|
|
35
|
+
const InitializeResultSchema = z.unknown();
|
|
36
|
+
function unavailable() {
|
|
37
|
+
return new CodexMetadataUnavailableError();
|
|
38
|
+
}
|
|
39
|
+
// Node writable streams may invoke a failed write callback and emit `error` afterwards. This
|
|
40
|
+
// closure-free sink deliberately stays on a dead transport so that late stream errors can never
|
|
41
|
+
// become uncaught host-process exceptions after the generation's active listeners are removed.
|
|
42
|
+
const absorbLateTransportError = () => { };
|
|
43
|
+
function positiveInteger(value, fallback) {
|
|
44
|
+
return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
45
|
+
}
|
|
46
|
+
function defaultSpawnTransport(command, args, options) {
|
|
47
|
+
return spawn(command, [...args], { env: options?.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bounded JSON-lines client for the auxiliary Codex app-server metadata channel.
|
|
51
|
+
* It never participates in terminal streaming and deliberately knows no method-specific result schemas.
|
|
52
|
+
*/
|
|
53
|
+
export class CodexAppServerClient {
|
|
54
|
+
limits;
|
|
55
|
+
codexBin;
|
|
56
|
+
profile;
|
|
57
|
+
env;
|
|
58
|
+
spawnTransport;
|
|
59
|
+
notificationListeners = new Set();
|
|
60
|
+
active;
|
|
61
|
+
startPromise;
|
|
62
|
+
stopPromise;
|
|
63
|
+
nextRequestId = 1;
|
|
64
|
+
nextGeneration = 1;
|
|
65
|
+
lastDiagnostics = { stderrBytes: 0, stderrTruncated: false };
|
|
66
|
+
constructor(options = {}) {
|
|
67
|
+
this.codexBin = options.codexBin ?? "codex";
|
|
68
|
+
this.env = options.env ? { ...options.env } : undefined;
|
|
69
|
+
if (options.profile !== undefined &&
|
|
70
|
+
(options.profile.length === 0 ||
|
|
71
|
+
options.profile.length > 128 ||
|
|
72
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.profile))) {
|
|
73
|
+
throw unavailable();
|
|
74
|
+
}
|
|
75
|
+
this.profile = options.profile;
|
|
76
|
+
this.spawnTransport = options.spawnTransport ?? defaultSpawnTransport;
|
|
77
|
+
const maxStdoutLineBytes = positiveInteger(options.maxStdoutLineBytes, DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_LINE_BYTES);
|
|
78
|
+
this.limits = {
|
|
79
|
+
timeoutMs: positiveInteger(options.timeoutMs, DEFAULT_CODEX_APP_SERVER_TIMEOUT_MS),
|
|
80
|
+
maxStdoutLineBytes,
|
|
81
|
+
maxStdoutBufferBytes: positiveInteger(options.maxStdoutBufferBytes, DEFAULT_CODEX_APP_SERVER_MAX_STDOUT_BUFFER_BYTES),
|
|
82
|
+
maxStderrBytes: positiveInteger(options.maxStderrBytes, DEFAULT_CODEX_APP_SERVER_MAX_STDERR_BYTES),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
get diagnostics() {
|
|
86
|
+
const generation = this.active;
|
|
87
|
+
return generation
|
|
88
|
+
? {
|
|
89
|
+
...this.lastDiagnostics,
|
|
90
|
+
stderrBytes: generation.stderrBytes,
|
|
91
|
+
stderrTruncated: generation.stderrTruncated,
|
|
92
|
+
}
|
|
93
|
+
: { ...this.lastDiagnostics };
|
|
94
|
+
}
|
|
95
|
+
start() {
|
|
96
|
+
if (this.active?.phase === "running" && !this.active.closed)
|
|
97
|
+
return Promise.resolve();
|
|
98
|
+
if (this.startPromise)
|
|
99
|
+
return this.startPromise;
|
|
100
|
+
if (this.stopPromise)
|
|
101
|
+
return this.stopPromise.then(() => this.start());
|
|
102
|
+
let transport;
|
|
103
|
+
try {
|
|
104
|
+
transport = this.spawnTransport(this.codexBin, this.profile ? ["--profile", this.profile, "app-server", "--stdio"] : ["app-server", "--stdio"], { env: this.env ? { ...this.env } : undefined });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
this.recordIssue("spawn");
|
|
108
|
+
return Promise.reject(unavailable());
|
|
109
|
+
}
|
|
110
|
+
const generation = this.createGeneration(transport);
|
|
111
|
+
this.active = generation;
|
|
112
|
+
this.lastDiagnostics = { stderrBytes: 0, stderrTruncated: false };
|
|
113
|
+
this.installListeners(generation);
|
|
114
|
+
const promise = this.sendRequest(generation, "initialize", {
|
|
115
|
+
clientInfo: { name: "roamcode", title: "RoamCode", version: "0.0.0" },
|
|
116
|
+
capabilities: {},
|
|
117
|
+
}, InitializeResultSchema)
|
|
118
|
+
.then(() => this.writeFrame(generation, { method: "initialized", params: {} }))
|
|
119
|
+
.then(() => {
|
|
120
|
+
if (generation.closed || this.active !== generation)
|
|
121
|
+
throw unavailable();
|
|
122
|
+
generation.phase = "running";
|
|
123
|
+
})
|
|
124
|
+
.catch(() => {
|
|
125
|
+
if (!generation.closed)
|
|
126
|
+
this.failGeneration(generation, "transport_error", true);
|
|
127
|
+
throw unavailable();
|
|
128
|
+
});
|
|
129
|
+
this.startPromise = promise;
|
|
130
|
+
void promise.then(() => {
|
|
131
|
+
if (this.startPromise === promise)
|
|
132
|
+
this.startPromise = undefined;
|
|
133
|
+
}, () => {
|
|
134
|
+
if (this.startPromise === promise)
|
|
135
|
+
this.startPromise = undefined;
|
|
136
|
+
});
|
|
137
|
+
return promise;
|
|
138
|
+
}
|
|
139
|
+
request(method, params, schema) {
|
|
140
|
+
const generation = this.active;
|
|
141
|
+
if (!generation || generation.closed || generation.phase !== "running" || !method) {
|
|
142
|
+
return Promise.reject(unavailable());
|
|
143
|
+
}
|
|
144
|
+
return this.sendRequest(generation, method, params, schema);
|
|
145
|
+
}
|
|
146
|
+
onNotification(listener) {
|
|
147
|
+
this.notificationListeners.add(listener);
|
|
148
|
+
return () => this.notificationListeners.delete(listener);
|
|
149
|
+
}
|
|
150
|
+
stop() {
|
|
151
|
+
if (this.stopPromise)
|
|
152
|
+
return this.stopPromise;
|
|
153
|
+
const promise = Promise.resolve();
|
|
154
|
+
this.stopPromise = promise;
|
|
155
|
+
const generation = this.active;
|
|
156
|
+
if (generation)
|
|
157
|
+
this.failGeneration(generation, "transport_error", true, true);
|
|
158
|
+
void promise.then(() => {
|
|
159
|
+
if (this.stopPromise === promise)
|
|
160
|
+
this.stopPromise = undefined;
|
|
161
|
+
});
|
|
162
|
+
return promise;
|
|
163
|
+
}
|
|
164
|
+
createGeneration(transport) {
|
|
165
|
+
const generation = {
|
|
166
|
+
number: this.nextGeneration++,
|
|
167
|
+
transport,
|
|
168
|
+
pending: new Map(),
|
|
169
|
+
writeQueue: [],
|
|
170
|
+
stdoutBuffer: Buffer.alloc(0),
|
|
171
|
+
stderrBytes: 0,
|
|
172
|
+
stderrTruncated: false,
|
|
173
|
+
phase: "starting",
|
|
174
|
+
closed: false,
|
|
175
|
+
writing: false,
|
|
176
|
+
listeners: {
|
|
177
|
+
stdoutData: (chunk) => this.onStdout(generation, chunk),
|
|
178
|
+
stderrData: (chunk) => this.onStderr(generation, chunk),
|
|
179
|
+
exit: () => this.failGeneration(generation, "exit", false),
|
|
180
|
+
error: () => this.failGeneration(generation, "transport_error", true),
|
|
181
|
+
stdinError: () => this.failGeneration(generation, "write_error", true),
|
|
182
|
+
stdoutError: () => this.failGeneration(generation, "transport_error", true),
|
|
183
|
+
stderrError: () => this.failGeneration(generation, "transport_error", true),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
return generation;
|
|
187
|
+
}
|
|
188
|
+
installListeners(generation) {
|
|
189
|
+
const { transport, listeners } = generation;
|
|
190
|
+
transport.on("error", absorbLateTransportError);
|
|
191
|
+
transport.stdin.on("error", absorbLateTransportError);
|
|
192
|
+
transport.stdout.on("error", absorbLateTransportError);
|
|
193
|
+
transport.stderr.on("error", absorbLateTransportError);
|
|
194
|
+
transport.stdout.on("data", listeners.stdoutData);
|
|
195
|
+
transport.stderr.on("data", listeners.stderrData);
|
|
196
|
+
transport.on("exit", listeners.exit);
|
|
197
|
+
transport.on("error", listeners.error);
|
|
198
|
+
transport.stdin.on("error", listeners.stdinError);
|
|
199
|
+
transport.stdout.on("error", listeners.stdoutError);
|
|
200
|
+
transport.stderr.on("error", listeners.stderrError);
|
|
201
|
+
}
|
|
202
|
+
sendRequest(generation, method, params, schema) {
|
|
203
|
+
if (generation.closed || this.active !== generation)
|
|
204
|
+
return Promise.reject(unavailable());
|
|
205
|
+
const id = this.nextRequestId++;
|
|
206
|
+
const response = new Promise((resolve, reject) => {
|
|
207
|
+
const timer = setTimeout(() => this.failGeneration(generation, "transport_error", true), this.limits.timeoutMs);
|
|
208
|
+
generation.pending.set(id, {
|
|
209
|
+
schema: schema,
|
|
210
|
+
resolve: (value) => resolve(value),
|
|
211
|
+
reject,
|
|
212
|
+
timer,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
const written = this.writeFrame(generation, { id, method, params }).catch(() => {
|
|
216
|
+
if (!generation.closed) {
|
|
217
|
+
this.failGeneration(generation, "write_error", true);
|
|
218
|
+
}
|
|
219
|
+
throw unavailable();
|
|
220
|
+
});
|
|
221
|
+
return Promise.all([written, response]).then(([, value]) => value);
|
|
222
|
+
}
|
|
223
|
+
writeFrame(generation, value) {
|
|
224
|
+
if (generation.closed || this.active !== generation)
|
|
225
|
+
return Promise.reject(unavailable());
|
|
226
|
+
let frame;
|
|
227
|
+
try {
|
|
228
|
+
frame = `${JSON.stringify(value)}\n`;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return Promise.reject(unavailable());
|
|
232
|
+
}
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const timer = setTimeout(() => {
|
|
235
|
+
if (!generation.closed)
|
|
236
|
+
this.failGeneration(generation, "write_error", true);
|
|
237
|
+
}, this.limits.timeoutMs);
|
|
238
|
+
generation.writeQueue.push({ frame, resolve, reject, timer });
|
|
239
|
+
this.flushWrites(generation);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
flushWrites(generation) {
|
|
243
|
+
if (generation.closed || generation.writing)
|
|
244
|
+
return;
|
|
245
|
+
const entry = generation.writeQueue[0];
|
|
246
|
+
if (!entry)
|
|
247
|
+
return;
|
|
248
|
+
generation.writing = true;
|
|
249
|
+
let callbackDone = false;
|
|
250
|
+
let drainDone = false;
|
|
251
|
+
let writeReturned = false;
|
|
252
|
+
let settled = false;
|
|
253
|
+
const finish = () => {
|
|
254
|
+
if (settled || generation.closed || !writeReturned || !callbackDone || !drainDone)
|
|
255
|
+
return;
|
|
256
|
+
settled = true;
|
|
257
|
+
generation.writeQueue.shift();
|
|
258
|
+
generation.writing = false;
|
|
259
|
+
clearTimeout(entry.timer);
|
|
260
|
+
entry.resolve();
|
|
261
|
+
this.flushWrites(generation);
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
const accepted = generation.transport.stdin.write(entry.frame, (error) => {
|
|
265
|
+
if (generation.closed)
|
|
266
|
+
return;
|
|
267
|
+
if (error) {
|
|
268
|
+
this.failGeneration(generation, "write_error", true);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
callbackDone = true;
|
|
272
|
+
finish();
|
|
273
|
+
});
|
|
274
|
+
writeReturned = true;
|
|
275
|
+
if (generation.closed)
|
|
276
|
+
return;
|
|
277
|
+
if (accepted) {
|
|
278
|
+
drainDone = true;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const onDrain = () => {
|
|
282
|
+
if (generation.drainListener === onDrain)
|
|
283
|
+
generation.drainListener = undefined;
|
|
284
|
+
drainDone = true;
|
|
285
|
+
finish();
|
|
286
|
+
};
|
|
287
|
+
generation.drainListener = onDrain;
|
|
288
|
+
generation.transport.stdin.once("drain", onDrain);
|
|
289
|
+
}
|
|
290
|
+
finish();
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
this.failGeneration(generation, "write_error", true);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
onStdout(generation, value) {
|
|
297
|
+
if (generation.closed || this.active !== generation)
|
|
298
|
+
return;
|
|
299
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
300
|
+
if (generation.stdoutBuffer.length + chunk.length > this.limits.maxStdoutBufferBytes) {
|
|
301
|
+
this.failGeneration(generation, "stdout_limit", true);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
generation.stdoutBuffer = Buffer.concat([generation.stdoutBuffer, chunk]);
|
|
305
|
+
for (;;) {
|
|
306
|
+
const newline = generation.stdoutBuffer.indexOf(0x0a);
|
|
307
|
+
if (newline < 0) {
|
|
308
|
+
if (generation.stdoutBuffer.length > this.limits.maxStdoutLineBytes) {
|
|
309
|
+
this.failGeneration(generation, "stdout_limit", true);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let line = generation.stdoutBuffer.subarray(0, newline);
|
|
314
|
+
generation.stdoutBuffer = generation.stdoutBuffer.subarray(newline + 1);
|
|
315
|
+
if (line.at(-1) === 0x0d)
|
|
316
|
+
line = line.subarray(0, -1);
|
|
317
|
+
if (line.length > this.limits.maxStdoutLineBytes) {
|
|
318
|
+
this.failGeneration(generation, "stdout_limit", true);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (line.length === 0)
|
|
322
|
+
continue;
|
|
323
|
+
let parsed;
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(line.toString("utf8"));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
this.failGeneration(generation, "malformed_json", true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.onMessage(generation, parsed);
|
|
332
|
+
if (generation.closed)
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
onMessage(generation, message) {
|
|
337
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
338
|
+
this.recordIssue("unknown_message", generation);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const hasId = Object.hasOwn(message, "id");
|
|
342
|
+
const hasResult = Object.hasOwn(message, "result");
|
|
343
|
+
const hasError = Object.hasOwn(message, "error");
|
|
344
|
+
const hasMethod = Object.hasOwn(message, "method");
|
|
345
|
+
if (hasId && hasResult && !hasError && !hasMethod) {
|
|
346
|
+
const success = JsonRpcSuccessSchema.safeParse(message);
|
|
347
|
+
if (success.success) {
|
|
348
|
+
this.resolveResponse(generation, success.data.id, success.data.result);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (hasId && hasError && !hasResult && !hasMethod) {
|
|
353
|
+
const failure = JsonRpcErrorSchema.safeParse(message);
|
|
354
|
+
if (failure.success) {
|
|
355
|
+
this.rejectResponse(generation, failure.data.id);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (hasMethod && !hasId && !hasResult && !hasError) {
|
|
360
|
+
const notification = JsonRpcNotificationSchema.safeParse(message);
|
|
361
|
+
if (!notification.success) {
|
|
362
|
+
this.recordIssue("unknown_message", generation);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const value = {
|
|
366
|
+
method: notification.data.method,
|
|
367
|
+
...(Object.hasOwn(notification.data, "params") ? { params: notification.data.params } : {}),
|
|
368
|
+
};
|
|
369
|
+
for (const listener of this.notificationListeners) {
|
|
370
|
+
try {
|
|
371
|
+
listener(value);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
this.recordIssue("notification_listener", generation);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.recordIssue("unknown_message", generation);
|
|
380
|
+
}
|
|
381
|
+
resolveResponse(generation, id, result) {
|
|
382
|
+
const pending = generation.pending.get(id);
|
|
383
|
+
if (!pending) {
|
|
384
|
+
this.recordIssue("unknown_message", generation);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
generation.pending.delete(id);
|
|
388
|
+
clearTimeout(pending.timer);
|
|
389
|
+
try {
|
|
390
|
+
const parsed = pending.schema.safeParse(result);
|
|
391
|
+
if (parsed.success)
|
|
392
|
+
pending.resolve(parsed.data);
|
|
393
|
+
else
|
|
394
|
+
pending.reject(unavailable());
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
pending.reject(unavailable());
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
rejectResponse(generation, id) {
|
|
401
|
+
const pending = generation.pending.get(id);
|
|
402
|
+
if (!pending) {
|
|
403
|
+
this.recordIssue("unknown_message", generation);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
generation.pending.delete(id);
|
|
407
|
+
clearTimeout(pending.timer);
|
|
408
|
+
pending.reject(unavailable());
|
|
409
|
+
}
|
|
410
|
+
onStderr(generation, value) {
|
|
411
|
+
if (generation.closed || this.active !== generation)
|
|
412
|
+
return;
|
|
413
|
+
const bytes = Buffer.isBuffer(value) ? value.length : Buffer.byteLength(value);
|
|
414
|
+
const remaining = this.limits.maxStderrBytes - generation.stderrBytes;
|
|
415
|
+
generation.stderrBytes += Math.min(remaining, bytes);
|
|
416
|
+
if (bytes > remaining) {
|
|
417
|
+
generation.stderrTruncated = true;
|
|
418
|
+
this.recordIssue("stderr_limit", generation);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
failGeneration(generation, issue, kill, endStdin = false) {
|
|
422
|
+
if (generation.closed)
|
|
423
|
+
return;
|
|
424
|
+
generation.closed = true;
|
|
425
|
+
generation.writing = false;
|
|
426
|
+
if (this.active === generation)
|
|
427
|
+
this.startPromise = undefined;
|
|
428
|
+
this.recordIssue(issue, generation);
|
|
429
|
+
this.lastDiagnostics = {
|
|
430
|
+
...this.lastDiagnostics,
|
|
431
|
+
stderrBytes: generation.stderrBytes,
|
|
432
|
+
stderrTruncated: generation.stderrTruncated,
|
|
433
|
+
};
|
|
434
|
+
if (this.active === generation)
|
|
435
|
+
this.active = undefined;
|
|
436
|
+
this.removeListeners(generation);
|
|
437
|
+
const error = unavailable();
|
|
438
|
+
for (const pending of generation.pending.values()) {
|
|
439
|
+
clearTimeout(pending.timer);
|
|
440
|
+
pending.reject(error);
|
|
441
|
+
}
|
|
442
|
+
generation.pending.clear();
|
|
443
|
+
for (const write of generation.writeQueue.splice(0)) {
|
|
444
|
+
clearTimeout(write.timer);
|
|
445
|
+
write.reject(error);
|
|
446
|
+
}
|
|
447
|
+
if (endStdin) {
|
|
448
|
+
try {
|
|
449
|
+
generation.transport.stdin.end();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// The same stable unavailable error already rejects every waiter.
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (kill) {
|
|
456
|
+
try {
|
|
457
|
+
generation.transport.kill("SIGKILL");
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// The transport is already unavailable.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
removeListeners(generation) {
|
|
465
|
+
const { transport, listeners } = generation;
|
|
466
|
+
transport.stdout.removeListener("data", listeners.stdoutData);
|
|
467
|
+
transport.stderr.removeListener("data", listeners.stderrData);
|
|
468
|
+
transport.removeListener("exit", listeners.exit);
|
|
469
|
+
transport.removeListener("error", listeners.error);
|
|
470
|
+
transport.stdin.removeListener("error", listeners.stdinError);
|
|
471
|
+
transport.stdout.removeListener("error", listeners.stdoutError);
|
|
472
|
+
transport.stderr.removeListener("error", listeners.stderrError);
|
|
473
|
+
if (generation.drainListener) {
|
|
474
|
+
transport.stdin.removeListener("drain", generation.drainListener);
|
|
475
|
+
generation.drainListener = undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
recordIssue(issue, generation) {
|
|
479
|
+
this.lastDiagnostics = {
|
|
480
|
+
lastIssue: issue,
|
|
481
|
+
stderrBytes: generation?.stderrBytes ?? this.lastDiagnostics.stderrBytes,
|
|
482
|
+
stderrTruncated: generation?.stderrTruncated ?? this.lastDiagnostics.stderrTruncated,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const CODEX_VERSION_UNAVAILABLE: "CODEX_VERSION_UNAVAILABLE";
|
|
2
|
+
export declare const CODEX_VERSION_TIMEOUT_MS = 5000;
|
|
3
|
+
export declare const CODEX_VERSION_MAX_OUTPUT_BYTES = 1024;
|
|
4
|
+
export declare const CODEX_LATEST_MAX_RESPONSE_BYTES = 16384;
|
|
5
|
+
export declare const DEFAULT_CODEX_VERSION_CACHE_TTL_MS: number;
|
|
6
|
+
export type CodexInstallProvenance = "npm" | "chatgpt" | "homebrew" | "unknown";
|
|
7
|
+
export interface BoundedVersionRunOptions {
|
|
8
|
+
readonly timeoutMs: number;
|
|
9
|
+
readonly maxOutputBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export interface BoundedLatestFetchOptions {
|
|
12
|
+
readonly timeoutMs: number;
|
|
13
|
+
readonly maxResponseBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export type RunCodexVersion = (args: readonly string[], options: BoundedVersionRunOptions) => Promise<{
|
|
16
|
+
readonly code: number;
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
readonly stderr: string;
|
|
19
|
+
}>;
|
|
20
|
+
export type DetectCodexProvenance = () => CodexInstallProvenance | Promise<CodexInstallProvenance>;
|
|
21
|
+
export type FetchNpmLatest = (packageName: string, options: BoundedLatestFetchOptions) => Promise<string>;
|
|
22
|
+
export interface CodexLatestServiceOptions {
|
|
23
|
+
readonly runVersion: RunCodexVersion;
|
|
24
|
+
readonly detectProvenance: DetectCodexProvenance;
|
|
25
|
+
readonly fetchNpmLatest: FetchNpmLatest;
|
|
26
|
+
readonly now?: () => number;
|
|
27
|
+
readonly cacheTtlMs?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CodexVersionInfo {
|
|
30
|
+
readonly installed: string;
|
|
31
|
+
readonly provenance: CodexInstallProvenance;
|
|
32
|
+
readonly latest?: string;
|
|
33
|
+
readonly updateAvailable?: boolean;
|
|
34
|
+
readonly updateHint?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class CodexVersionUnavailableError extends Error {
|
|
37
|
+
readonly code: "CODEX_VERSION_UNAVAILABLE";
|
|
38
|
+
constructor();
|
|
39
|
+
}
|
|
40
|
+
export declare function parseCodexVersion(stdout: string): string;
|
|
41
|
+
export declare class CodexLatestService {
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly now;
|
|
44
|
+
private readonly cacheTtlMs;
|
|
45
|
+
private cache?;
|
|
46
|
+
private inFlight?;
|
|
47
|
+
constructor(options: CodexLatestServiceOptions);
|
|
48
|
+
getVersion(force?: boolean): Promise<CodexVersionInfo>;
|
|
49
|
+
private loadVersion;
|
|
50
|
+
}
|