dsh-codex-connect 0.1.0-alpha.4.19 → 0.1.0-alpha.4.21

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/lib/bin.js CHANGED
@@ -1,9 +1,374 @@
1
1
  #!/usr/bin/env node
2
- import { L as diagnoseOpenAICodex, c as migrateOpenAICodexSearchHistory, ct as normalizeTrustedOrigin, ft as loginOpenAICodex, mt as openAICodexAuthStatus, pt as logoutOpenAICodex, st as OpenAICodexTrustedOriginsStore, vt as openAICodexAuthPath, z as CODEX_CONNECT_VERSION } from "./src-CiOi4nFr.js";
2
+ import { B as DSH_PLUGIN_API_PACKAGES, Dt as logoutOpenAICodex, Et as loginOpenAICodex, J as readInstalledPackageVersion, Mt as OpenAICodexCredentialStore, Nt as openAICodexAuthPath, Ot as openAICodexAuthStatus, St as normalizeTrustedOrigin, c as migrateOpenAICodexSearchHistory, j as CODEX_CONNECT_VERSION, jt as OPENAI_CODEX_PROVIDER, k as diagnoseOpenAICodex, kt as openAICodexModelCatalog, l as OPENAI_CODEX_BASE_URL, pt as normalizeOpenAICodexProxyUrl, q as evaluateCompatibility, xt as OpenAICodexTrustedOriginsStore } from "./src-Cxq99TvF.js";
3
+ import { createHash } from "node:crypto";
4
+ import { Agent, ProxyAgent, fetch } from "undici";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { realpathSync } from "node:fs";
5
7
  import { spawn } from "node:child_process";
6
8
  import { createInterface } from "node:readline/promises";
9
+ //#region src/capability-probe.ts
10
+ /** Bounded, standalone Responses probe; never used by conversation routing. */
11
+ /** A security limit, not a model output-token setting. */
12
+ const MAX_PROBE_RESPONSE_BYTES = 65536;
13
+ function record(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function completedResponse(value, model) {
17
+ if (!record(value) || value["status"] !== "completed" || !Array.isArray(value["output"])) return false;
18
+ if (value["model"] !== model) return false;
19
+ return value["output"].some((item) => record(item) && item["type"] === "message" && item["role"] === "assistant" && Array.isArray(item["content"]) && item["content"].some((part) => record(part) && part["type"] === "output_text" && typeof part["text"] === "string" && part["text"].trim().length > 0));
20
+ }
21
+ /** Inspect complete SSE frames, not substrings inside error text or schema errors. */
22
+ function completedStream(text, model) {
23
+ let completed = false;
24
+ let data = [];
25
+ for (const line of text.split(/\r\n|\r|\n/u)) if (line === "") {
26
+ if (data.length === 0) continue;
27
+ const payload = data.join("\n");
28
+ data = [];
29
+ if (payload === "[DONE]") continue;
30
+ let event;
31
+ try {
32
+ event = JSON.parse(payload);
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (!record(event)) return false;
37
+ if (event["type"] === "error" || event["type"] === "response.failed" || event["type"] === "response.incomplete") return false;
38
+ if (event["type"] === "response.completed" || event["type"] === "response.done") {
39
+ if (completed || !completedResponse(event["response"], model)) return false;
40
+ completed = true;
41
+ }
42
+ } else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /u, ""));
43
+ return completed && data.length === 0;
44
+ }
45
+ /**
46
+ * Send one fixed, stateless prompt to the first-party Responses endpoint.
47
+ * No refresh, redirects, retries, session ids, or optional capabilities are used.
48
+ * The deadline covers headers and EOF; owned sockets are destroyed before return.
49
+ * @param request - resolved model, OAuth credential, and network policy.
50
+ * @param createDispatcher - owned connection factory; tests use an offline dispatcher.
51
+ * @returns bounded evidence, never upstream body text or exception messages.
52
+ */
53
+ async function probeCodexResponses(request, createDispatcher = (proxyUrl) => proxyUrl === void 0 ? new Agent() : new ProxyAgent(proxyUrl)) {
54
+ const dispatcher = createDispatcher(request.proxyUrl);
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), request.timeoutMs);
57
+ let httpStatus;
58
+ try {
59
+ const response = await fetch(`${OPENAI_CODEX_BASE_URL}/responses`, {
60
+ dispatcher,
61
+ method: "POST",
62
+ redirect: "manual",
63
+ signal: controller.signal,
64
+ headers: {
65
+ authorization: `Bearer ${request.access}`,
66
+ "chatgpt-account-id": request.accountId,
67
+ "content-type": "application/json",
68
+ accept: "text/event-stream",
69
+ originator: "deepseek-harness"
70
+ },
71
+ body: JSON.stringify({
72
+ model: request.model,
73
+ instructions: "You are a connectivity diagnostic. Reply with only ok.",
74
+ input: [{
75
+ role: "user",
76
+ content: [{
77
+ type: "input_text",
78
+ text: "Reply with only ok."
79
+ }]
80
+ }],
81
+ stream: true,
82
+ store: false
83
+ })
84
+ });
85
+ httpStatus = response.status;
86
+ if (!response.ok) {
87
+ await response.body?.cancel();
88
+ return {
89
+ outcome: [
90
+ 400,
91
+ 401,
92
+ 403,
93
+ 404,
94
+ 405,
95
+ 422
96
+ ].includes(httpStatus) ? "http-rejected" : "transient",
97
+ httpStatus
98
+ };
99
+ }
100
+ if (httpStatus !== 200 || !response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream") || response.body === null) {
101
+ await response.body?.cancel();
102
+ return {
103
+ outcome: "incomplete",
104
+ httpStatus
105
+ };
106
+ }
107
+ const reader = response.body.getReader();
108
+ const chunks = [];
109
+ let size = 0;
110
+ try {
111
+ while (true) {
112
+ const { value, done } = await reader.read();
113
+ if (done) break;
114
+ size += value.byteLength;
115
+ if (size > MAX_PROBE_RESPONSE_BYTES) {
116
+ await reader.cancel();
117
+ return {
118
+ outcome: "incomplete",
119
+ httpStatus
120
+ };
121
+ }
122
+ chunks.push(value);
123
+ }
124
+ } finally {
125
+ reader.releaseLock();
126
+ }
127
+ return {
128
+ outcome: completedStream(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)), request.model) ? "completed" : "incomplete",
129
+ httpStatus
130
+ };
131
+ } catch {
132
+ return {
133
+ outcome: controller.signal.aborted ? "timeout" : "network-error",
134
+ ...httpStatus === void 0 ? {} : { httpStatus }
135
+ };
136
+ } finally {
137
+ clearTimeout(timer);
138
+ await dispatcher.destroy();
139
+ }
140
+ }
141
+ //#endregion
142
+ //#region src/capability-diagnostics.ts
143
+ /** Evidence-scoped diagnostics, separate from model routing and durable sessions. */
144
+ function result(status, reason, action) {
145
+ return {
146
+ status,
147
+ reason,
148
+ action
149
+ };
150
+ }
151
+ function safeVersion(value) {
152
+ return value !== null && value !== void 0 && /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(value) ? value : null;
153
+ }
154
+ function observed(evidence) {
155
+ switch (evidence.outcome) {
156
+ case "completed": return result("supported", "completed-response-for-selected-model", "This proves only the fixed standalone prompt; test the active Harness profile separately.");
157
+ case "http-rejected": switch (evidence.httpStatus) {
158
+ case 401: return result("rejected", "http-401", "Sign in again, then explicitly repeat the probe.");
159
+ case 403: return result("rejected", "http-403", "Check account, model, and network access policy; signing in may not resolve a policy denial.");
160
+ case 404: return result("rejected", "http-404", "The selected route was not found; check endpoint availability and plugin updates.");
161
+ default: return result("rejected", "request-rejected", "The server rejected this diagnostic request; check the selected model and request compatibility. No optional capability was tested.");
162
+ }
163
+ case "transient": return result("unknown", "transient-or-redirect-response", "Check network, proxy, quota, or server availability before retrying; no provider fallback was attempted.");
164
+ case "incomplete": return result("unknown", "no-complete-matching-response", "HTTP success or schema recognition is insufficient; repeat the probe after checking the endpoint and selected model.");
165
+ case "timeout": return result("unknown", "probe-deadline", "The probe exceeded its deadline and may have consumed quota; check the network before retrying.");
166
+ case "network-error": return result("unknown", "network-or-stream-error", "Check the network or pass an explicit --proxy; no server rejection was confirmed.");
167
+ }
168
+ }
169
+ /**
170
+ * Reusable diagnostic operation with a single-entry, lazy-expiring memory cache.
171
+ * It registers no background hooks, expiry timers, or model-visible session events.
172
+ * The network provider owns its request deadline and connection cleanup.
173
+ * Cache identity includes credentials, model, network policy, and installed versions.
174
+ */
175
+ var CodexCapabilityDiagnostics = class {
176
+ cacheTtlMs;
177
+ deps;
178
+ cache;
179
+ /**
180
+ * @param cacheTtlMs - finite cache lifetime; zero disables reuse, maximum 60 seconds.
181
+ * @param deps - owned metadata and probe operations; defaults perform local reads only until requested.
182
+ */
183
+ constructor(cacheTtlMs, deps = {
184
+ diagnose: diagnoseOpenAICodex,
185
+ readVersion: readInstalledPackageVersion,
186
+ catalog: openAICodexModelCatalog,
187
+ credentials: new OpenAICodexCredentialStore(),
188
+ probe: probeCodexResponses,
189
+ now: Date.now
190
+ }) {
191
+ this.cacheTtlMs = cacheTtlMs;
192
+ this.deps = deps;
193
+ if (!Number.isFinite(cacheTtlMs) || cacheTtlMs < 0 || cacheTtlMs > 6e4) throw new TypeError("Diagnostic cache lifetime must be between 0 and 60000 ms");
194
+ }
195
+ /**
196
+ * Read metadata; optionally send one fixed request using an unexpired stored token.
197
+ * No credential refresh/write occurs. Unsupported local versions prevent probing.
198
+ * @param request - resolved command arguments, with explicit network consent.
199
+ * @returns a secret-free report whose unknown entries never authorize a capability.
200
+ */
201
+ async inspect(request) {
202
+ const local = await this.deps.diagnose();
203
+ const versions = { node: safeVersion(local.node) };
204
+ for (const [name, entry] of Object.entries(local.compatibility.packages)) versions[name] = safeVersion(entry.installed);
205
+ for (const name of DSH_PLUGIN_API_PACKAGES) versions[name] = safeVersion(await this.deps.readVersion(name));
206
+ const compatibility = evaluateCompatibility({
207
+ nodeVersion: versions["node"] ?? null,
208
+ packageVersions: versions
209
+ });
210
+ const missing = Object.values(versions).some((value) => value === null) || !/^v?\d+\.\d+\.\d+$/u.test(versions["node"] ?? "");
211
+ const runtime = DSH_PLUGIN_API_PACKAGES.some((name) => versions[name] !== null && versions[name] !== "0.1.1-rc.2") || compatibility.status === "incompatible" ? result("rejected", "declared-version-mismatch", "Use DSH API 0.1.1-rc.2 and pi-ai 0.82.1 together. DSH 0.1.0-rc.7 requires Codex Connect 0.1.0-alpha.4.14.") : missing || compatibility.status === "unknown" ? result("unknown", "version-metadata-unavailable", "Run this command from the plugin installation in the intended profile.") : result("supported", "declared-host-versions-match", "Host package versions satisfy the declared requirements; this is not a live profile or browser compatibility test.");
212
+ const model = this.deps.catalog().find((item) => item.id === request.model)?.id ?? null;
213
+ const unknownNetwork = result("unknown", "not-probed", "Run capabilities --model <catalog-id> --probe explicitly; this sends a fixed short request and may consume quota.");
214
+ const checks = {
215
+ runtime,
216
+ oauth: local.credentialFile.state === "owner-only" ? result("unknown", "credential-metadata-only", "A private credential file does not prove authorization; use an explicit probe.") : result("rejected", "credential-file-unusable", "Sign in or repair owner-only credential-file permissions; no credential content was read."),
217
+ responses: { ...unknownNetwork },
218
+ transport: result("unknown", `configured-sse-not-probed`, unknownNetwork.action),
219
+ model: model === null ? result(request.model === void 0 ? "unknown" : "rejected", "model-not-selected-from-catalog", "Select an exact model id from the installed Codex provider catalog; unknown ids are not probed.") : result("unknown", "catalog-is-not-entitlement", unknownNetwork.action),
220
+ providerFallback: result("rejected", "no-automatic-provider-failover", "Select another provider explicitly. SSE is already selected; WebSocket-to-SSE fallback is inactive. Authentication, request, and partial-stream errors do not authorize provider switching."),
221
+ contextManagement: result("unknown", "no-successful-optional-operation", "Schema recognition is not capability evidence; keep optional context management disabled."),
222
+ continuation: result("unknown", "no-continuation-round-trip", "A stateless probe does not verify continuation, Fork, or restart; retain Harness-owned history."),
223
+ nativeCompaction: result("rejected", "no-native-compaction-integration", "Keep Harness text-summary compaction; track the typed-operation and durable-replay prerequisites in Issue #65."),
224
+ websocketReuse: result("rejected", "finite-sse-policy", "Keep finite SSE selected; cached WebSocket lifecycle work is outside this diagnostic.")
225
+ };
226
+ const report = {
227
+ schemaVersion: 1,
228
+ package: "dsh-codex-connect",
229
+ version: CODEX_CONNECT_VERSION,
230
+ scope: "standalone-route-only",
231
+ model,
232
+ network: request.proxyUrl === void 0 ? "direct" : "explicit-proxy",
233
+ versions,
234
+ checks,
235
+ probe: { state: request.probe ? "skipped" : "not-requested" }
236
+ };
237
+ if (runtime.status !== "supported" || model === null || checks.oauth.status === "rejected") {
238
+ this.cache = void 0;
239
+ return report;
240
+ }
241
+ if (!request.probe) return report;
242
+ let credential;
243
+ try {
244
+ credential = await this.deps.credentials.read(OPENAI_CODEX_PROVIDER);
245
+ } catch {
246
+ checks.oauth = result("rejected", "credential-unreadable", "Repair the credential file or sign in again; diagnostic output omits parser details.");
247
+ this.cache = void 0;
248
+ return report;
249
+ }
250
+ if (credential?.type !== "oauth" || typeof credential.accountId !== "string") {
251
+ checks.oauth = result("rejected", "credential-missing", "Sign in before explicitly probing the route.");
252
+ this.cache = void 0;
253
+ return report;
254
+ }
255
+ if (credential.expires <= this.deps.now()) {
256
+ checks.oauth = result("unknown", "access-token-expired", "Use the normal sign-in or refresh flow, then repeat the probe; diagnostics never refresh or write credentials.");
257
+ this.cache = void 0;
258
+ return report;
259
+ }
260
+ const key = createHash("sha256").update(JSON.stringify([
261
+ credential.access,
262
+ credential.accountId,
263
+ model,
264
+ request.proxyUrl,
265
+ request.timeoutMs,
266
+ versions
267
+ ])).digest("hex");
268
+ const cached = this.cache;
269
+ const age = cached === void 0 ? Infinity : this.deps.now() - cached.observedAt;
270
+ let evidence;
271
+ if (cached?.key === key && age >= 0 && age < this.cacheTtlMs) {
272
+ evidence = cached.evidence;
273
+ report.probe = {
274
+ state: "cached",
275
+ observedAt: cached.observedAt
276
+ };
277
+ } else {
278
+ this.cache = void 0;
279
+ evidence = await this.deps.probe({
280
+ model,
281
+ access: credential.access,
282
+ accountId: credential.accountId,
283
+ proxyUrl: request.proxyUrl,
284
+ timeoutMs: request.timeoutMs
285
+ });
286
+ const observedAt = this.deps.now();
287
+ report.probe = {
288
+ state: "fresh",
289
+ observedAt
290
+ };
291
+ if (evidence.outcome === "completed" || evidence.outcome === "http-rejected") this.cache = {
292
+ key,
293
+ observedAt,
294
+ evidence
295
+ };
296
+ }
297
+ if (evidence.httpStatus !== void 0) report.probe.httpStatus = evidence.httpStatus;
298
+ checks.responses = observed(evidence);
299
+ checks.transport = { ...checks.responses };
300
+ if (evidence.outcome === "completed") {
301
+ checks.oauth = result("supported", "authorized-completed-response", "Authorization succeeded for this model and fixed request at the recorded time only.");
302
+ checks.model = { ...checks.responses };
303
+ } else if (evidence.httpStatus === 401 || evidence.httpStatus === 403) checks.oauth = { ...checks.responses };
304
+ return report;
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/capability-cli.ts
309
+ /** Opt-in standalone capability diagnostics; ordinary doctor output stays unchanged. */
310
+ /**
311
+ * Run the separate capability command without booting Harness or changing settings.
312
+ * @param args - flags after capabilities; unknown or unsafe flags are not echoed.
313
+ * @returns 0 for supported primary checks, 1 for rejection, or 2 for unknown/invalid input.
314
+ */
315
+ async function runCapabilityCommand(args) {
316
+ const request = {
317
+ model: void 0,
318
+ probe: false,
319
+ proxyUrl: void 0,
320
+ timeoutMs: 3e4
321
+ };
322
+ let json = false;
323
+ const seen = /* @__PURE__ */ new Set();
324
+ const invalid = () => {
325
+ process.stderr.write("Usage: dsh-codex-connect capabilities [--model <catalog-id>] [--probe] [--proxy <http(s)-origin>] [--timeout-ms <1..60000>] [--json]\n");
326
+ return 2;
327
+ };
328
+ for (let index = 0; index < args.length; index += 1) {
329
+ const flag = args[index];
330
+ if (seen.has(flag)) return invalid();
331
+ seen.add(flag);
332
+ if (flag === "--json") json = true;
333
+ else if (flag === "--probe") request.probe = true;
334
+ else if (flag === "--model" || flag === "--proxy" || flag === "--timeout-ms") {
335
+ const value = args[++index];
336
+ if (value === void 0 || value.startsWith("--")) return invalid();
337
+ if (flag === "--model") request.model = value;
338
+ if (flag === "--proxy") {
339
+ request.proxyUrl = normalizeOpenAICodexProxyUrl(value);
340
+ if (request.proxyUrl === void 0) return invalid();
341
+ }
342
+ if (flag === "--timeout-ms") {
343
+ if (!/^\d+$/u.test(value) || Number(value) < 1 || Number(value) > 6e4) return invalid();
344
+ request.timeoutMs = Number(value);
345
+ }
346
+ } else return invalid();
347
+ }
348
+ if (request.probe && request.model === void 0) return invalid();
349
+ try {
350
+ const report = await new CodexCapabilityDiagnostics(6e4).inspect(request);
351
+ process.stdout.write(json ? `${JSON.stringify(report)}\n` : [
352
+ `Codex Connect ${report.version}: standalone route diagnostics`,
353
+ "Scope: local host versions and the selected standalone route; not the active Harness profile.",
354
+ `Model: ${report.model ?? "not selected from catalog"}; network: ${report.network}; probe: ${report.probe.state}`,
355
+ ...Object.entries(report.checks).map(([id, check]) => `${id}: ${check.status} (${check.reason})\n ${check.action}`),
356
+ ""
357
+ ].join("\n"));
358
+ const primary = [
359
+ report.checks.runtime,
360
+ report.checks.oauth,
361
+ report.checks.responses,
362
+ report.checks.transport,
363
+ report.checks.model
364
+ ];
365
+ return primary.some((check) => check.status === "rejected") ? 1 : primary.some((check) => check.status === "unknown") ? 2 : 0;
366
+ } catch {
367
+ process.stderr.write("Codex Connect diagnostics could not inspect this installation. Check local package and credential-file access.\n");
368
+ return 2;
369
+ }
370
+ }
371
+ //#endregion
7
372
  //#region src/bin.ts
8
373
  /** Standalone credential CLI for the optional OpenAI Codex bundle. */
9
374
  const JSON_SCHEMA_VERSION = 1;
@@ -69,6 +434,7 @@ function printHelp() {
69
434
  " dsh-codex-connect trust-origin <origin>",
70
435
  " dsh-codex-connect trusted-origins [--json]",
71
436
  " dsh-codex-connect untrust-origin <origin>",
437
+ " dsh-codex-connect capabilities [--model <catalog-id>] [--probe] [--proxy <http(s)-origin>] [--timeout-ms <1..60000>] [--json]",
72
438
  "",
73
439
  " doctor inspect secret-free runtime and OAuth file metadata",
74
440
  " login sign in with a separate ChatGPT OAuth session",
@@ -116,6 +482,7 @@ async function run(argv) {
116
482
  return 0;
117
483
  }
118
484
  const [rawAction, ...flags] = argv;
485
+ if (rawAction === "capabilities") return runCapabilityCommand(flags);
119
486
  if (![
120
487
  "doctor",
121
488
  "login",