dsh-codex-subscription 2.4.2 → 2.5.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/lib/index.js CHANGED
@@ -14,9 +14,9 @@ import { constants, readFileSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
+ import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
17
18
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
18
19
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
19
- import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
20
20
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
21
21
  import z from "@deepseek-ai/schemastery";
22
22
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
@@ -166,8 +166,7 @@ const IMAGE_FEATURE_DEFAULTS = Object.freeze({
166
166
  imageViewer: true,
167
167
  imageAnnotations: true,
168
168
  imageSketch: false,
169
- imageSketchAgent: false,
170
- imageSketchAgentPreview: false
169
+ imageSketchAgent: false
171
170
  });
172
171
  function readImageFeatures(value = {}) {
173
172
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -1980,2071 +1979,2106 @@ const sketchCommandArray = {
1980
1979
  })
1981
1980
  };
1982
1981
  //#endregion
1983
- //#region src/sketch-agent-tool.js
1984
- function createSketchAgentTool(bridge, attachments) {
1985
- return defineTool({
1986
- name: "codex_sketch",
1987
- description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1988
- parameters: {
1989
- action: {
1990
- type: "string",
1991
- required: true,
1992
- enum: [
1993
- "inspect",
1994
- "apply",
1995
- "preview",
1996
- "save",
1997
- "finish"
1998
- ]
1999
- },
2000
- runId: {
2001
- type: "string",
2002
- description: "From inspect; required for all other actions. Never reuse a stopped run."
2003
- },
2004
- documentId: {
2005
- type: "string",
2006
- description: "From inspect; required except for inspect."
2007
- },
2008
- revision: {
2009
- type: "integer",
2010
- description: "From latest response; required for apply/save/finish."
2011
- },
2012
- requestId: {
2013
- type: "string",
2014
- description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
2015
- },
2016
- commands: {
2017
- oneOf: [sketchCommandArray, { type: "string" }],
2018
- description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
2019
- },
2020
- name: {
2021
- type: "string",
2022
- description: "Draft name for save/finish."
2023
- },
2024
- offset: {
2025
- type: "integer",
2026
- description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
2027
- },
2028
- objectId: {
2029
- type: "string",
2030
- description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
2031
- },
2032
- layer: {
2033
- type: "integer",
2034
- description: "inspect only: layer containing objectId."
2035
- }
2036
- },
2037
- timeoutMs: 25e3,
2038
- isConcurrencySafe: () => false,
2039
- async execute(args, exec) {
2040
- const sessionId = exec.agent?.id;
2041
- if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
2042
- const request = { ...args };
2043
- if (args.action === "apply") try {
2044
- request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
2045
- if (!Array.isArray(request.commands)) throw Error();
2046
- } catch {
2047
- throw Error("commands must be a native array or JSON array string");
2048
- }
2049
- const value = await bridge.request(sessionId, request, exec.signal);
2050
- if (value.png) {
2051
- if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
2052
- const image = await attachments.saveImage({
2053
- data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
2054
- mediaType: "image/png",
2055
- name: "sketch-preview.png"
2056
- });
2057
- const { png, ...snapshot } = value;
2058
- return {
2059
- ...snapshot,
2060
- image
2061
- };
2062
- }
2063
- return value;
2064
- },
2065
- output: {
2066
- schema: {
2067
- type: "object",
2068
- additionalProperties: true
2069
- },
2070
- render: (_args, value) => [{
2071
- type: "text",
2072
- text: JSON.stringify({
2073
- ...value,
2074
- image: void 0
2075
- })
2076
- }, ...value.image ? [{
2077
- type: "image",
2078
- attachment: value.image
2079
- }] : []]
2080
- }
2081
- });
1982
+ //#region src/subscription-credentials.js
1983
+ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
1984
+ for (let attempt = 0; attempt < 2; attempt++) {
1985
+ const auth = await getAuth({ signal });
1986
+ signal?.throwIfAborted();
1987
+ if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
1988
+ const credential = await readCredential({ signal });
1989
+ signal?.throwIfAborted();
1990
+ if (credential?.type !== "oauth") break;
1991
+ if (credential.access === auth.auth.apiKey) return {
1992
+ access: credential.access,
1993
+ accountId: credential.accountId
1994
+ };
1995
+ }
1996
+ return {
1997
+ access: void 0,
1998
+ accountId: void 0
1999
+ };
2082
2000
  }
2083
- //#endregion
2084
- //#region src/sketch-codec-route.js
2085
- function registerSketchCodec(connection) {
2086
- let source;
2087
- return connection.fetch.register({
2088
- path: "/api/codex-subscription/sketch-psd-worker",
2089
- methods: ["GET"],
2090
- requestBody: "buffered",
2091
- async fetch() {
2092
- source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2093
- return new Response(source, { headers: {
2094
- "content-type": "text/javascript; charset=utf-8",
2095
- "cache-control": "no-store"
2096
- } });
2097
- }
2098
- });
2001
+ const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
2002
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
2003
+ const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0;
2004
+ function decodeOriginalImageRef(value) {
2005
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger$1(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger$1(value.width) || !positiveInteger$1(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
2006
+ return {
2007
+ assetId: value.assetId,
2008
+ mediaType: value.mediaType,
2009
+ bytes: value.bytes,
2010
+ width: value.width,
2011
+ height: value.height,
2012
+ name: value.name,
2013
+ sha256: value.sha256
2014
+ };
2099
2015
  }
2100
- //#endregion
2101
- //#region src/account-vault.js
2102
- const VERSION = 1;
2103
- const DEFAULT_LABEL = "Account 1";
2104
- const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2105
- const EMAIL_MAX_LENGTH = 254;
2106
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2107
- /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2108
- function normalizeAccountEmail(value) {
2109
- if (typeof value !== "string") return void 0;
2110
- const email = value.trim();
2111
- return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2016
+ function decodeImagePresentation(value) {
2017
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
2018
+ const original = decodeOriginalImageRef(value.original);
2019
+ return original === void 0 ? void 0 : { original };
2112
2020
  }
2113
- function decodeJwtPayload(access) {
2114
- if (typeof access !== "string") return void 0;
2115
- const encoded = access.split(".")[1];
2116
- if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2117
- try {
2118
- return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2021
+ function originalImageRefsEqual(left, right) {
2022
+ const a = decodeOriginalImageRef(left);
2023
+ const b = decodeOriginalImageRef(right);
2024
+ return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
2025
+ }
2026
+ /** Resolve only an exact original reference copied into a DSH fork prefix. */
2027
+ function inheritedOriginalImageRef(session, assetId) {
2028
+ const parentSession = session?.header?.parentSession;
2029
+ const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
2030
+ if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
2031
+ let events = session?.events;
2032
+ if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
2033
+ events = session.snapshotEvents();
2119
2034
  } catch {
2120
2035
  return;
2121
2036
  }
2037
+ if (!Array.isArray(events)) return void 0;
2038
+ for (const event of events) {
2039
+ if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
2040
+ const original = decodeImagePresentation(event.data?.meta)?.original;
2041
+ if (original?.assetId === assetId) return original;
2042
+ }
2122
2043
  }
2123
- function emailFromAccessToken(access) {
2124
- const payload = decodeJwtPayload(access);
2125
- return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
2044
+ //#endregion
2045
+ //#region src/version.js
2046
+ const PACKAGE_VERSION = "2.5.1";
2047
+ const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2048
+ //#endregion
2049
+ //#region src/codex-images.js
2050
+ const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
2051
+ const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
2052
+ const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
2053
+ const MAX_REFERENCE_IMAGES = 5;
2054
+ const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
2055
+ const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
2056
+ "auto",
2057
+ "transparent",
2058
+ "opaque"
2059
+ ]);
2060
+ const PNG_SIGNATURE = Buffer.from([
2061
+ 137,
2062
+ 80,
2063
+ 78,
2064
+ 71,
2065
+ 13,
2066
+ 10,
2067
+ 26,
2068
+ 10
2069
+ ]);
2070
+ const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
2071
+ const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
2072
+ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
2073
+ const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
2074
+ const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2075
+ const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2076
+ const NETWORK_REASONS = {
2077
+ UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
2078
+ UND_ERR_BODY_TIMEOUT: "the image response stalled",
2079
+ UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
2080
+ UND_ERR_SOCKET: "the connection closed unexpectedly",
2081
+ ECONNRESET: "the connection was reset",
2082
+ ETIMEDOUT: "the connection timed out",
2083
+ ENOTFOUND: "DNS lookup failed",
2084
+ ECONNREFUSED: "the connection was refused",
2085
+ EAI_AGAIN: "DNS lookup failed"
2086
+ };
2087
+ function requestFailureReason(error) {
2088
+ const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
2089
+ if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
2090
+ if (error?.name === "TimeoutError") return ": the image request timed out";
2091
+ return "";
2126
2092
  }
2127
- /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2128
- function sanitizeOAuthCredential(value) {
2129
- const credential = assertOAuthCredential$1(value);
2130
- const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2131
- if (email === void 0) {
2132
- delete credential.email;
2133
- return credential;
2093
+ function normalizeImageOptions(args) {
2094
+ const quality = nonEmpty$2(args?.quality) ?? "auto";
2095
+ const background = nonEmpty$2(args?.background) ?? "auto";
2096
+ const size = nonEmpty$2(args?.size) ?? "auto";
2097
+ validateImageQuality(resolveImageModel(args?.model), quality);
2098
+ if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
2099
+ if (size !== "auto") {
2100
+ const match = /^(\d+)x(\d+)$/u.exec(size);
2101
+ const width = Number(match?.[1]);
2102
+ const height = Number(match?.[2]);
2103
+ const short = Math.min(width, height);
2104
+ const long = Math.max(width, height);
2105
+ const pixels = width * height;
2106
+ if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
2134
2107
  }
2135
2108
  return {
2136
- ...credential,
2137
- email
2138
- };
2139
- }
2140
- function assertOAuthCredential$1(value) {
2141
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2142
- return clone$1(value);
2109
+ quality,
2110
+ background,
2111
+ size
2112
+ };
2143
2113
  }
2144
- function parseOAuthCredential$1(value) {
2114
+ function encodedLimit(decodedBytes) {
2115
+ return Math.ceil(decodedBytes / 3) * 4;
2116
+ }
2117
+ function validBase64Body(value, end) {
2118
+ for (let index = 0; index < end; index += 1) {
2119
+ const code = value.charCodeAt(index);
2120
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
2121
+ }
2122
+ return true;
2123
+ }
2124
+ async function readJsonWithin(response, maximumBytes) {
2125
+ const contentLength = Number(response.headers.get("content-length"));
2126
+ if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
2127
+ if (response.body === null) throw new Error("Codex returned an unreadable image response");
2128
+ const reader = response.body.getReader();
2129
+ const chunks = [];
2130
+ let bytes = 0;
2131
+ while (true) {
2132
+ const { done, value } = await reader.read();
2133
+ if (done) break;
2134
+ bytes += value.byteLength;
2135
+ if (bytes > maximumBytes) {
2136
+ await reader.cancel();
2137
+ throw new Error("Codex image response exceeds the image size limit");
2138
+ }
2139
+ chunks.push(value);
2140
+ }
2141
+ const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
2145
2142
  try {
2146
- return assertOAuthCredential$1(JSON.parse(value));
2147
- } catch (error) {
2148
- if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2149
- throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2143
+ return JSON.parse(body);
2144
+ } catch {
2145
+ throw new Error("Codex returned an unreadable image response");
2150
2146
  }
2151
2147
  }
2152
- function normalizeLabel(value) {
2153
- if (typeof value !== "string") throw new Error("Codex account label must be text");
2154
- const label = value.trim().replace(/\s+/gu, " ");
2155
- if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2156
- return label;
2148
+ /** Strictly decode one PNG returned by the subscription backend. */
2149
+ function decodeCodexPng(value, maximumBytes) {
2150
+ const encoded = nonEmpty$2(value);
2151
+ const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
2152
+ if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
2153
+ const decodedBytes = encoded.length / 4 * 3 - padding;
2154
+ if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
2155
+ const data = Buffer.from(encoded, "base64");
2156
+ if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
2157
+ return new Uint8Array(data);
2157
2158
  }
2158
- function assertVaultRecord(record) {
2159
- if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2160
- const ids = /* @__PURE__ */ new Set();
2161
- const accounts = record.payload.accounts.map((account) => {
2162
- if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2163
- ids.add(account.id);
2164
- return {
2165
- id: account.id,
2166
- label: normalizeLabel(account.label),
2167
- credential: sanitizeOAuthCredential(account.credential)
2168
- };
2159
+ function imageReference(value) {
2160
+ const originalDimensions = record$4(value.originalDimensions) ? {
2161
+ width: value.originalDimensions.width,
2162
+ height: value.originalDimensions.height
2163
+ } : void 0;
2164
+ return {
2165
+ attachmentId: value.attachmentId,
2166
+ mediaType: value.mediaType,
2167
+ bytes: value.bytes,
2168
+ width: value.width,
2169
+ height: value.height,
2170
+ ...value.name === void 0 ? {} : { name: value.name },
2171
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
2172
+ };
2173
+ }
2174
+ function normalizeAttachmentId(value) {
2175
+ if (typeof value !== "string") return void 0;
2176
+ if (FULL_ATTACHMENT_ID.test(value)) return value;
2177
+ const bare = BARE_ATTACHMENT_DIGEST.exec(value);
2178
+ return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
2179
+ }
2180
+ function invalidAttachmentId(value) {
2181
+ const attachmentId = value?.attachmentId;
2182
+ if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
2183
+ throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
2184
+ }
2185
+ function referenceOf(value, attachments) {
2186
+ const attachmentId = normalizeAttachmentId(value?.attachmentId);
2187
+ if (attachmentId === void 0) invalidAttachmentId(value);
2188
+ if (!record$4(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$4(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
2189
+ return imageReference({
2190
+ ...value,
2191
+ attachmentId
2169
2192
  });
2170
- if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2171
- const legacyAccountId = record.payload.legacyAccountId;
2172
- if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
2193
+ }
2194
+ function sessionImageReferences(messages) {
2195
+ const references = /* @__PURE__ */ new Map();
2196
+ const visit = (content) => {
2197
+ if (!Array.isArray(content)) return;
2198
+ for (const block of content) if (block?.type === "image" && record$4(block.attachment)) {
2199
+ const id = normalizeAttachmentId(block.attachment.attachmentId);
2200
+ if (id !== void 0) references.set(id, block.attachment);
2201
+ } else if (block?.type === "tool-result") visit(block.content);
2202
+ };
2203
+ for (const message of messages ?? []) visit(message?.content);
2204
+ return references;
2205
+ }
2206
+ async function readSessionImage(attachmentId, attachments, signal, messages) {
2207
+ const id = normalizeAttachmentId(attachmentId);
2208
+ if (id === void 0) invalidAttachmentId({ attachmentId });
2209
+ const selected = sessionImageReferences(messages).get(id);
2210
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Copy attachmentId from the image block of the user message; never pass a path or filename.");
2211
+ const stored = await attachments.readImage(referenceOf(selected, attachments), signal);
2173
2212
  return {
2174
- version: VERSION,
2175
- activeId: record.payload.activeId,
2176
- legacyAccountId,
2177
- accounts
2213
+ data: stored.data,
2214
+ mediaType: stored.ref.mediaType,
2215
+ name: stored.ref.name
2178
2216
  };
2179
2217
  }
2180
- const grant = (payload) => ({
2181
- kind: "grant",
2182
- payload
2183
- });
2184
- var PendingOAuthCredentialStore = class {
2185
- #credential;
2186
- async read(providerId) {
2187
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2188
- return clone$1(this.#credential);
2189
- }
2190
- async list() {
2191
- return this.#credential === void 0 ? [] : [{
2192
- providerId: "openai-codex",
2193
- type: "oauth"
2194
- }];
2195
- }
2196
- async modify(providerId, update) {
2197
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2198
- const next = await update(clone$1(this.#credential));
2199
- if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2200
- return clone$1(this.#credential);
2201
- }
2202
- async delete(providerId) {
2203
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2204
- this.#credential = void 0;
2205
- }
2206
- credential() {
2207
- return clone$1(this.#credential);
2208
- }
2209
- };
2210
- /**
2211
- * Multi-account owner state stored in DSH's atomic plugin credential record.
2212
- * The old single-account reference remains as a rollback source and is kept in
2213
- * sync whenever that imported account rotates its refresh token.
2214
- */
2215
- var DshOAuthAccountVault = class {
2216
- #tail = Promise.resolve();
2217
- constructor(credentials, options) {
2218
- if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2219
- this.credentials = credentials;
2220
- this.key = options.key;
2221
- this.legacyRef = options.legacyRef;
2222
- this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2223
- this.createId = options.createId ?? randomUUID;
2224
- this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
2225
- }
2226
- #enqueue(operation) {
2227
- const current = this.#tail.catch(() => void 0).then(operation);
2228
- this.#tail = current.catch(() => void 0);
2229
- return current;
2230
- }
2231
- async #legacyCredential() {
2232
- for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2233
- const hit = await this.credentials.resolve(ref);
2234
- if (hit?.value === void 0 || hit.value === "") continue;
2235
- return {
2236
- ref,
2237
- credential: parseOAuthCredential$1(hit.value)
2238
- };
2239
- }
2240
- }
2241
- async #ensurePayload() {
2242
- const existing = await this.credentials.readRecord(this.key);
2243
- if (existing !== void 0) return assertVaultRecord(existing);
2244
- const legacy = await this.#legacyCredential();
2245
- if (legacy === void 0) return void 0;
2246
- const id = this.createId();
2247
- return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2248
- if (current !== void 0) return Promise.resolve(current);
2249
- return Promise.resolve(grant({
2250
- version: VERSION,
2251
- activeId: id,
2252
- legacyAccountId: id,
2253
- accounts: [{
2254
- id,
2255
- label: DEFAULT_LABEL,
2256
- credential: legacy.credential
2257
- }]
2258
- }));
2259
- }));
2260
- }
2261
- async #modifyPayload(update) {
2262
- await this.#ensurePayload();
2263
- let previousLegacy;
2264
- const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2265
- if (current === void 0) throw new Error("Codex account vault is not signed in");
2266
- const payload = assertVaultRecord(current);
2267
- previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2268
- const next = await update(clone$1(payload));
2269
- return grant(next);
2270
- }));
2271
- const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2272
- try {
2273
- if (legacy === void 0) {
2274
- if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2275
- } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2276
- } catch {
2277
- this.onLegacySyncFailure();
2278
- }
2279
- return payload;
2280
- }
2281
- list() {
2282
- return this.#enqueue(async () => {
2283
- const payload = await this.#ensurePayload();
2284
- if (payload === void 0) return [];
2285
- return payload.accounts.map((account) => ({
2286
- id: account.id,
2287
- label: account.label,
2288
- active: account.id === payload.activeId,
2289
- expiresAt: account.credential.expires,
2290
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2291
- }));
2292
- });
2293
- }
2294
- readActive() {
2295
- return this.#enqueue(async () => {
2296
- const payload = await this.#ensurePayload();
2297
- return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2298
- });
2218
+ async function editImages(values, attachments, signal, messages) {
2219
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
2220
+ const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
2221
+ const references = values.map((value) => {
2222
+ const id = normalizeAttachmentId(value?.attachmentId);
2223
+ if (id === void 0) invalidAttachmentId(value);
2224
+ if (available === void 0) return referenceOf(value, attachments);
2225
+ const selected = available.get(id);
2226
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
2227
+ return referenceOf(selected, attachments);
2228
+ });
2229
+ if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
2230
+ const images = [];
2231
+ let totalBytes = 0;
2232
+ for (const reference of references) {
2233
+ const stored = await attachments.readImage(reference, signal);
2234
+ totalBytes += stored.data.byteLength;
2235
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
2236
+ images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
2299
2237
  }
2300
- activeId() {
2301
- return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
2302
- }
2303
- add(label, credential) {
2304
- return this.#enqueue(async () => {
2305
- const normalizedLabel = normalizeLabel(label);
2306
- const validated = sanitizeOAuthCredential(credential);
2307
- await this.#ensurePayload();
2308
- const id = this.createId();
2309
- const account = (await this.#modifyPayload((current) => ({
2310
- ...current,
2311
- activeId: id,
2312
- accounts: [...current.accounts, {
2313
- id,
2314
- label: normalizedLabel,
2315
- credential: validated
2316
- }]
2317
- }))).accounts.find((candidate) => candidate.id === id);
2318
- return {
2319
- id,
2320
- label: account.label,
2321
- active: true,
2322
- expiresAt: account.credential.expires,
2323
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2324
- };
2325
- });
2326
- }
2327
- select(id) {
2328
- return this.#enqueue(async () => {
2329
- await this.#modifyPayload((current) => {
2330
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2331
- return {
2332
- ...current,
2333
- activeId: id
2334
- };
2335
- });
2336
- });
2337
- }
2338
- modifyActive(update) {
2339
- return this.#enqueue(async () => {
2340
- if (await this.#ensurePayload() === void 0) {
2341
- const initial = await update(void 0);
2342
- if (initial === void 0) return void 0;
2343
- const credential = sanitizeOAuthCredential(initial);
2344
- await this.credentials.set(this.legacyRef, JSON.stringify(credential));
2345
- await this.#ensurePayload();
2346
- return clone$1(credential);
2347
- }
2348
- let result;
2349
- await this.#modifyPayload(async (current) => {
2350
- const index = current.accounts.findIndex((account) => account.id === current.activeId);
2351
- const previous = clone$1(current.accounts[index].credential);
2352
- const next = await update(previous);
2353
- if (next === void 0) {
2354
- result = previous;
2355
- return current;
2356
- }
2357
- const credential = sanitizeOAuthCredential(next);
2358
- if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
2359
- const accounts = [...current.accounts];
2360
- accounts[index] = {
2361
- ...accounts[index],
2362
- credential
2363
- };
2364
- result = clone$1(credential);
2365
- return {
2366
- ...current,
2367
- accounts
2368
- };
2369
- });
2370
- return result;
2371
- });
2372
- }
2373
- deleteAll() {
2374
- return this.#enqueue(async () => {
2375
- await this.credentials.deleteRecord(this.key);
2376
- await this.credentials.unset(this.legacyRef);
2377
- for (const ref of this.legacyRefs) await this.credentials.unset(ref);
2378
- });
2379
- }
2380
- remove(id) {
2381
- return this.#enqueue(async () => {
2382
- await this.#modifyPayload((current) => {
2383
- if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2384
- if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
2385
- const accounts = current.accounts.filter((account) => account.id !== id);
2386
- return {
2387
- ...current,
2388
- activeId: current.activeId === id ? accounts[0].id : current.activeId,
2389
- legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
2390
- accounts
2391
- };
2392
- });
2393
- });
2394
- }
2395
- };
2396
- //#endregion
2397
- //#region src/credential-store.js
2398
- const PROVIDER$1 = "openai-codex";
2399
- const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
2400
- const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
2401
- function assertProvider(providerId) {
2402
- if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
2238
+ return images;
2403
2239
  }
2404
- function assertOAuthCredential(value) {
2405
- if (value === void 0) return void 0;
2406
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
2407
- return clone(value);
2240
+ function imageContent(value) {
2241
+ const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
2242
+ return [{
2243
+ type: "text",
2244
+ text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
2245
+ }, {
2246
+ type: "image",
2247
+ attachment: imageReference(value.image)
2248
+ }];
2408
2249
  }
2409
- function parseOAuthCredential(value) {
2410
- try {
2411
- return assertOAuthCredential(JSON.parse(value));
2412
- } catch (error) {
2413
- if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
2414
- throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
2415
- }
2250
+ function imageOutputSchema() {
2251
+ return {
2252
+ type: "object",
2253
+ additionalProperties: false,
2254
+ properties: {
2255
+ image: {
2256
+ type: "object",
2257
+ required: true,
2258
+ additionalProperties: false,
2259
+ properties: {
2260
+ attachmentId: {
2261
+ type: "string",
2262
+ required: true
2263
+ },
2264
+ mediaType: {
2265
+ type: "string",
2266
+ enum: ["image/png"],
2267
+ required: true
2268
+ },
2269
+ bytes: {
2270
+ type: "integer",
2271
+ required: true
2272
+ },
2273
+ width: {
2274
+ type: "integer",
2275
+ required: true
2276
+ },
2277
+ height: {
2278
+ type: "integer",
2279
+ required: true
2280
+ },
2281
+ name: { type: "string" },
2282
+ originalDimensions: {
2283
+ type: "object",
2284
+ additionalProperties: false,
2285
+ properties: {
2286
+ width: {
2287
+ type: "integer",
2288
+ required: true
2289
+ },
2290
+ height: {
2291
+ type: "integer",
2292
+ required: true
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ },
2298
+ original: {
2299
+ type: "object",
2300
+ required: true,
2301
+ additionalProperties: false,
2302
+ properties: {
2303
+ assetId: {
2304
+ type: "string",
2305
+ required: true
2306
+ },
2307
+ mediaType: {
2308
+ type: "string",
2309
+ enum: ["image/png"],
2310
+ required: true
2311
+ },
2312
+ bytes: {
2313
+ type: "integer",
2314
+ required: true
2315
+ },
2316
+ width: {
2317
+ type: "integer",
2318
+ required: true
2319
+ },
2320
+ height: {
2321
+ type: "integer",
2322
+ required: true
2323
+ },
2324
+ name: {
2325
+ type: "string",
2326
+ required: true
2327
+ },
2328
+ sha256: {
2329
+ type: "string",
2330
+ required: true
2331
+ }
2332
+ }
2333
+ },
2334
+ background: { type: "string" },
2335
+ requestedModel: { type: "string" },
2336
+ reportedModel: { type: "string" },
2337
+ requestedSize: { type: "string" },
2338
+ localPath: {
2339
+ type: "string",
2340
+ required: true,
2341
+ description: "Absolute path to the original PNG on the DSH host."
2342
+ },
2343
+ quality: { type: "string" },
2344
+ size: { type: "string" }
2345
+ }
2346
+ };
2416
2347
  }
2417
- /**
2418
- * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
2419
- * Refresh/login/logout operations are serialized so an older refresh response
2420
- * cannot overwrite a newer rotated token.
2421
- */
2422
- var DshOAuthCredentialStore = class {
2423
- #chains = /* @__PURE__ */ new Map();
2424
- constructor(credentials, ref, legacyRefs = [], options = {}) {
2425
- if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
2426
- const expirySkewMs = options.expirySkewMs ?? 0;
2427
- if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
2428
- this.credentials = credentials;
2429
- this.ref = ref;
2430
- this.legacyRefs = Object.freeze([...legacyRefs]);
2431
- this.expirySkewMs = expirySkewMs;
2432
- this.vault = options.vault;
2433
- }
2434
- #enqueue(providerId, operation, options) {
2435
- assertProvider(providerId);
2436
- const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
2437
- abortIfNeeded(options);
2438
- return operation();
2439
- });
2440
- const tail = current.catch(() => void 0);
2441
- this.#chains.set(providerId, tail);
2442
- tail.finally(() => {
2443
- if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
2444
- });
2445
- return current;
2446
- }
2447
- async #read(providerId, options) {
2448
- assertProvider(providerId);
2449
- abortIfNeeded(options);
2450
- if (this.vault !== void 0) {
2451
- const current = await this.vault.readActive();
2452
- if (current === void 0) return void 0;
2453
- return this.expirySkewMs === 0 ? current : {
2454
- ...current,
2455
- expires: current.expires - this.expirySkewMs
2348
+ function responseMetadata(value) {
2349
+ const data = Array.isArray(value?.data) ? value.data[0] : void 0;
2350
+ const encoded = record$4(data) ? data.b64_json : void 0;
2351
+ if (typeof encoded !== "string") throw new Error("Codex returned no image data");
2352
+ return {
2353
+ encoded,
2354
+ background: nonEmpty$2(value.background),
2355
+ quality: nonEmpty$2(value.quality),
2356
+ size: nonEmpty$2(value.size),
2357
+ reportedModel: nonEmpty$2(value.model)
2358
+ };
2359
+ }
2360
+ /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
2361
+ function createCodexImageTool(options) {
2362
+ const fetchImage = options.fetch ?? fetch;
2363
+ const attachments = options.attachments;
2364
+ return defineTool({
2365
+ name: CODEX_IMAGE_TOOL_NAME,
2366
+ description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
2367
+ parameters: {
2368
+ model: {
2369
+ type: "string",
2370
+ enum: Object.keys(IMAGE_MODELS),
2371
+ description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
2372
+ },
2373
+ prompt: {
2374
+ type: "string",
2375
+ required: true,
2376
+ description: "A complete, production-ready description of the image to generate."
2377
+ },
2378
+ size: {
2379
+ type: "string",
2380
+ description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
2381
+ },
2382
+ quality: {
2383
+ type: "string",
2384
+ enum: [
2385
+ "auto",
2386
+ "low",
2387
+ "medium",
2388
+ "high",
2389
+ "xhigh",
2390
+ "max"
2391
+ ],
2392
+ description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
2393
+ },
2394
+ background: {
2395
+ type: "string",
2396
+ enum: [
2397
+ "auto",
2398
+ "transparent",
2399
+ "opaque"
2400
+ ],
2401
+ description: "Optional background mode. Request transparent only when the user needs transparency."
2402
+ },
2403
+ referenceImages: {
2404
+ type: "array",
2405
+ description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
2406
+ items: {
2407
+ type: "object",
2408
+ additionalProperties: false,
2409
+ properties: {
2410
+ attachmentId: {
2411
+ type: "string",
2412
+ required: true,
2413
+ description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
2414
+ },
2415
+ mediaType: {
2416
+ type: "string",
2417
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2418
+ },
2419
+ bytes: {
2420
+ type: "integer",
2421
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2422
+ },
2423
+ width: {
2424
+ type: "integer",
2425
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2426
+ },
2427
+ height: {
2428
+ type: "integer",
2429
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2430
+ },
2431
+ name: { type: "string" },
2432
+ originalDimensions: {
2433
+ type: "object",
2434
+ additionalProperties: false,
2435
+ properties: {
2436
+ width: {
2437
+ type: "integer",
2438
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2439
+ },
2440
+ height: {
2441
+ type: "integer",
2442
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2443
+ }
2444
+ }
2445
+ }
2446
+ }
2447
+ }
2448
+ }
2449
+ },
2450
+ output: {
2451
+ schema: imageOutputSchema(),
2452
+ render: (_args, value) => imageContent(value),
2453
+ presentationMeta: (_args, value) => ({
2454
+ kind: "codex-subscription-image",
2455
+ schemaVersion: 1,
2456
+ original: value.original,
2457
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
2458
+ ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
2459
+ ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
2460
+ })
2461
+ },
2462
+ timeoutMs: 300 * 1e3,
2463
+ isConcurrencySafe: () => false,
2464
+ async execute(args, exec) {
2465
+ assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
2466
+ const defaults = readImageDefaults(options.getFeatures?.());
2467
+ args = {
2468
+ ...args,
2469
+ model: args.model ?? defaults.imageModel,
2470
+ quality: args.quality ?? defaults.imageQuality
2456
2471
  };
2457
- }
2458
- let hit = await this.credentials.resolve(this.ref);
2459
- if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
2460
- const legacy = await this.credentials.resolve(legacyRef);
2461
- if (legacy?.value === void 0 || legacy.value === "") continue;
2462
- const migrated = parseOAuthCredential(legacy.value);
2463
- await this.credentials.set(this.ref, JSON.stringify(migrated));
2464
- await this.credentials.unset(legacyRef);
2465
- hit = { value: JSON.stringify(migrated) };
2466
- break;
2467
- }
2468
- abortIfNeeded(options);
2469
- if (hit?.value === void 0 || hit.value === "") return void 0;
2470
- const credential = parseOAuthCredential(hit.value);
2471
- return this.expirySkewMs === 0 ? credential : {
2472
- ...credential,
2473
- expires: credential.expires - this.expirySkewMs
2474
- };
2475
- }
2476
- read(providerId, options) {
2477
- return this.#enqueue(providerId, () => this.#read(providerId, options), options);
2478
- }
2479
- async list(options) {
2480
- abortIfNeeded(options);
2481
- return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
2482
- providerId: PROVIDER$1,
2483
- type: "oauth"
2484
- }];
2485
- }
2486
- modify(providerId, update, options) {
2487
- return this.#enqueue(providerId, async () => {
2488
- if (this.vault !== void 0) {
2489
- const next = await this.vault.modifyActive(async (current) => {
2490
- const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
2491
- ...current,
2492
- expires: current.expires - this.expirySkewMs
2493
- };
2494
- const updated = await update(clone(visible));
2495
- return updated === void 0 ? void 0 : assertOAuthCredential(updated);
2472
+ const prompt = nonEmpty$2(args.prompt);
2473
+ if (prompt === void 0) throw new Error("prompt must be a non-empty string");
2474
+ const imageOptions = normalizeImageOptions(args);
2475
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
2476
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
2477
+ if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
2478
+ const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
2479
+ const editing = args.referenceImages !== void 0;
2480
+ const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
2481
+ const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
2482
+ let response;
2483
+ assertImageOperation(options.getFeatures?.(), editing);
2484
+ try {
2485
+ response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
2486
+ method: "POST",
2487
+ redirect: "error",
2488
+ headers: {
2489
+ authorization: `Bearer ${access}`,
2490
+ "chatgpt-account-id": accountId,
2491
+ accept: "application/json",
2492
+ "content-type": "application/json",
2493
+ originator: "pi",
2494
+ "x-codex-image-turn-id": String(exec.callId),
2495
+ "user-agent": USER_AGENT
2496
+ },
2497
+ body: JSON.stringify({
2498
+ ...images === void 0 ? {} : { images },
2499
+ prompt,
2500
+ background: imageOptions.background,
2501
+ model: resolveImageModel(args.model),
2502
+ quality: imageOptions.quality,
2503
+ size: imageOptions.size
2504
+ }),
2505
+ signal: exec.signal
2496
2506
  });
2497
- abortIfNeeded(options);
2498
- return clone(next);
2507
+ } catch (error) {
2508
+ if (exec.signal.aborted) throw exec.signal.reason;
2509
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
2499
2510
  }
2500
- const current = await this.#read(providerId, options);
2501
- const next = await update(clone(current));
2502
- abortIfNeeded(options);
2503
- if (next === void 0) return current;
2504
- const validated = assertOAuthCredential(next);
2505
- await this.credentials.set(this.ref, JSON.stringify(validated));
2506
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2507
- abortIfNeeded(options);
2508
- return clone(validated);
2509
- }, options);
2510
- }
2511
- delete(providerId, options) {
2512
- return this.#enqueue(providerId, async () => {
2513
- if (this.vault !== void 0) {
2514
- await this.vault.deleteAll();
2515
- abortIfNeeded(options);
2516
- return;
2511
+ if (!response.ok) {
2512
+ if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
2513
+ if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
2514
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
2517
2515
  }
2518
- await this.credentials.unset(this.ref);
2519
- for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
2520
- abortIfNeeded(options);
2521
- }, options);
2522
- }
2523
- };
2524
- /** Return only account state that is safe to expose to the browser client. */
2525
- function createCodexAuthService(models, store, options = {}) {
2526
- const runLogin = options.runLogin ?? ((run) => run());
2527
- const accountVault = options.accountVault;
2528
- const createLoginModels = options.createLoginModels;
2529
- const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
2530
- return Object.freeze({
2531
- async status(options) {
2532
- const current = await store.read(PROVIDER$1, options);
2533
- const accounts = await accountVault?.list();
2534
- if (current === void 0) return {
2535
- authenticated: false,
2536
- provider: PROVIDER$1,
2537
- ...accounts === void 0 ? {} : { accounts }
2538
- };
2539
- return {
2540
- authenticated: true,
2541
- provider: PROVIDER$1,
2542
- type: "oauth",
2543
- expiresAt: current.expires,
2544
- ...accounts === void 0 ? {} : { accounts }
2516
+ const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
2517
+ const data = decodeCodexPng(metadata.encoded, maximumBytes);
2518
+ const sessionId = exec.agent?.id;
2519
+ if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
2520
+ const original = await options.originalImages.save(String(sessionId), data);
2521
+ let ref;
2522
+ try {
2523
+ ref = await attachments.saveImage({
2524
+ data,
2525
+ mediaType: "image/png",
2526
+ name: "codex-generated.png"
2527
+ });
2528
+ } catch (error) {
2529
+ await options.originalImages.remove(original);
2530
+ throw error;
2531
+ }
2532
+ const result = {
2533
+ requestedModel: resolveImageModel(args.model),
2534
+ requestedSize: imageOptions.size,
2535
+ ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
2536
+ image: imageReference(ref),
2537
+ original,
2538
+ localPath: options.originalImages.originalPath(original.assetId),
2539
+ ...metadata.background === void 0 ? {} : { background: metadata.background },
2540
+ ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
2541
+ size: `${original.width}x${original.height}`
2545
2542
  };
2543
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
2544
+ content: imageContent(result),
2545
+ source: { kind: "codex-subscription-image" }
2546
+ }));
2547
+ return result;
2548
+ }
2549
+ });
2550
+ }
2551
+ //#endregion
2552
+ //#region src/sketch-agent-tool.js
2553
+ const MAX_REFERENCE_BYTES = 14e5;
2554
+ function createSketchAgentTool(bridge, attachments, getSessionMessages = () => []) {
2555
+ return defineTool({
2556
+ name: "codex_sketch",
2557
+ description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches and preview between stages. When the user attached a reference image, call reference with its attachmentId once after inspect to put it on the board as a hidden picture layer. When a reference picture is on the board, keep its layer hidden while drawing (compare shows it anyway), measure its key positions with preview grid, draw big shapes first, and after each stage check preview compare and fix the largest offset before adding detail. For repeated details such as hair strands or lace, generate the SVG with a script instead of placing each one by hand. and call finish to save the finished draft and release the editing lock. save is only a checkpoint. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
2558
+ parameters: {
2559
+ action: {
2560
+ type: "string",
2561
+ required: true,
2562
+ enum: [
2563
+ "inspect",
2564
+ "apply",
2565
+ "reference",
2566
+ "preview",
2567
+ "save",
2568
+ "finish"
2569
+ ]
2570
+ },
2571
+ runId: {
2572
+ type: "string",
2573
+ description: "From inspect; required for all other actions. Never reuse a stopped run."
2574
+ },
2575
+ documentId: {
2576
+ type: "string",
2577
+ description: "From inspect; required except for inspect."
2578
+ },
2579
+ revision: {
2580
+ type: "integer",
2581
+ description: "From latest response; required for apply/reference/save/finish."
2582
+ },
2583
+ requestId: {
2584
+ type: "string",
2585
+ description: "Unique id for apply/reference/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
2586
+ },
2587
+ commands: {
2588
+ oneOf: [sketchCommandArray, { type: "string" }],
2589
+ description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
2590
+ },
2591
+ name: {
2592
+ type: "string",
2593
+ description: "Draft name for save/finish."
2594
+ },
2595
+ attachmentId: {
2596
+ type: "string",
2597
+ description: "reference only: the attachmentId of an image the user attached to this session (copy it from the image block, never a path or filename). It is added to the board as a hidden picture layer, ready for preview compare."
2598
+ },
2599
+ compare: {
2600
+ type: "boolean",
2601
+ description: "preview only: blend the drawing 50/50 with the reference picture layer on the board, so offsets show as ghosting. Needs a picture layer; if there is none, ask the user to add the reference with the pictures button."
2602
+ },
2603
+ grid: {
2604
+ type: "boolean",
2605
+ description: "preview only: overlay a labelled grid every 100 canvas pixels, to read positions and sizes instead of guessing them."
2606
+ },
2607
+ offset: {
2608
+ type: "integer",
2609
+ description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
2610
+ },
2611
+ objectId: {
2612
+ type: "string",
2613
+ description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
2614
+ },
2615
+ layer: {
2616
+ type: "integer",
2617
+ description: "inspect only: layer containing objectId."
2618
+ }
2546
2619
  },
2547
- login(interaction, input = {}) {
2548
- if (input.label !== void 0) {
2549
- if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
2550
- return runLogin(async () => {
2551
- const pending = createPendingStore();
2552
- await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
2553
- const credential = pending.credential();
2554
- if (credential === void 0) throw new Error("Codex login did not return credentials");
2555
- await accountVault.add(input.label, credential);
2620
+ timeoutMs: 25e3,
2621
+ isConcurrencySafe: () => false,
2622
+ async execute(args, exec) {
2623
+ const sessionId = exec.agent?.id;
2624
+ if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
2625
+ const request = { ...args };
2626
+ delete request.attachmentId;
2627
+ if (args.action === "apply") try {
2628
+ request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
2629
+ if (!Array.isArray(request.commands)) throw Error();
2630
+ } catch {
2631
+ throw Error("commands must be a native array or JSON array string");
2632
+ }
2633
+ if (args.action === "reference") {
2634
+ const image = await readSessionImage(args.attachmentId, attachments, exec.signal, await getSessionMessages(sessionId));
2635
+ if (image.data.byteLength > MAX_REFERENCE_BYTES) throw Error("The reference image is too large to send to the board; ask the user to add it with the pictures button on the board");
2636
+ request.image = `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
2637
+ request.name = image.name ?? "reference";
2638
+ }
2639
+ const value = await bridge.request(sessionId, request, exec.signal);
2640
+ if (value.png) {
2641
+ if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
2642
+ const image = await attachments.saveImage({
2643
+ data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
2644
+ mediaType: "image/png",
2645
+ name: "sketch-preview.png"
2556
2646
  });
2647
+ const { png, ...snapshot } = value;
2648
+ return {
2649
+ ...snapshot,
2650
+ image
2651
+ };
2557
2652
  }
2558
- return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
2559
- },
2560
- async select(id) {
2561
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2562
- await accountVault.select(id);
2563
- return this.status();
2564
- },
2565
- async remove(id) {
2566
- if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
2567
- await accountVault.remove(id);
2568
- return this.status();
2653
+ return value;
2569
2654
  },
2570
- logout(options) {
2571
- return models.logout(PROVIDER$1, options);
2655
+ output: {
2656
+ schema: {
2657
+ type: "object",
2658
+ additionalProperties: true
2659
+ },
2660
+ render: (_args, value) => [{
2661
+ type: "text",
2662
+ text: JSON.stringify({
2663
+ ...value,
2664
+ image: void 0
2665
+ })
2666
+ }, ...value.image ? [{
2667
+ type: "image",
2668
+ attachment: value.image
2669
+ }] : []]
2572
2670
  }
2573
2671
  });
2574
2672
  }
2575
2673
  //#endregion
2576
- //#region src/external-url.js
2577
- const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
2578
- /** Validate the only external origin this plugin may launch. */
2579
- function assertCodexAuthUrl(value) {
2580
- let url;
2674
+ //#region src/sketch-codec-route.js
2675
+ function registerSketchCodec(connection) {
2676
+ let source;
2677
+ return connection.fetch.register({
2678
+ path: "/api/codex-subscription/sketch-psd-worker",
2679
+ methods: ["GET"],
2680
+ requestBody: "buffered",
2681
+ async fetch() {
2682
+ source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2683
+ return new Response(source, { headers: {
2684
+ "content-type": "text/javascript; charset=utf-8",
2685
+ "cache-control": "no-store"
2686
+ } });
2687
+ }
2688
+ });
2689
+ }
2690
+ //#endregion
2691
+ //#region src/account-vault.js
2692
+ const VERSION = 1;
2693
+ const DEFAULT_LABEL = "Account 1";
2694
+ const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2695
+ const EMAIL_MAX_LENGTH = 254;
2696
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2697
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2698
+ function normalizeAccountEmail(value) {
2699
+ if (typeof value !== "string") return void 0;
2700
+ const email = value.trim();
2701
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2702
+ }
2703
+ function decodeJwtPayload(access) {
2704
+ if (typeof access !== "string") return void 0;
2705
+ const encoded = access.split(".")[1];
2706
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2581
2707
  try {
2582
- url = new URL(value);
2708
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2583
2709
  } catch {
2584
- throw new Error("Codex auth URL is invalid");
2710
+ return;
2585
2711
  }
2586
- if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
2587
- if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
2588
- return url.href;
2589
2712
  }
2590
- /** Return a shell-free native opener command for the current desktop. */
2591
- function commandForCodexAuthUrl(value, platform = process.platform) {
2592
- const url = assertCodexAuthUrl(value);
2593
- if (platform === "win32") return {
2594
- file: "rundll32.exe",
2595
- args: ["url.dll,FileProtocolHandler", url],
2596
- shell: false
2597
- };
2598
- if (platform === "darwin") return {
2599
- file: "open",
2600
- args: [url],
2601
- shell: false
2602
- };
2603
- if (platform === "linux") return {
2604
- file: "xdg-open",
2605
- args: [url],
2606
- shell: false
2713
+ function emailFromAccessToken(access) {
2714
+ const payload = decodeJwtPayload(access);
2715
+ return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
2716
+ }
2717
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2718
+ function sanitizeOAuthCredential(value) {
2719
+ const credential = assertOAuthCredential$1(value);
2720
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2721
+ if (email === void 0) {
2722
+ delete credential.email;
2723
+ return credential;
2724
+ }
2725
+ return {
2726
+ ...credential,
2727
+ email
2607
2728
  };
2608
- throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
2609
2729
  }
2610
- function openCodexAuthUrl(value, options = {}) {
2611
- const command = commandForCodexAuthUrl(value, options.platform);
2612
- const spawnProcess = options.spawn ?? spawn;
2613
- return new Promise((resolve, reject) => {
2614
- const child = spawnProcess(command.file, command.args, {
2615
- detached: true,
2616
- stdio: "ignore",
2617
- windowsHide: true,
2618
- shell: command.shell
2619
- });
2620
- child.once("error", reject);
2621
- child.once("spawn", () => {
2622
- child.unref();
2623
- resolve();
2624
- });
2625
- });
2730
+ function assertOAuthCredential$1(value) {
2731
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2732
+ return clone$1(value);
2626
2733
  }
2627
- //#endregion
2628
- //#region src/login-coordinator.js
2629
- const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
2630
- const TERMINAL_PHASES = /* @__PURE__ */ new Set([
2631
- "authenticated",
2632
- "failed",
2633
- "cancelled"
2634
- ]);
2635
- const publicClone = (value) => structuredClone(value);
2636
- const asObject = (value) => value !== null && typeof value === "object" ? value : {};
2637
- const ok = (value) => ({
2638
- ok: true,
2639
- value
2640
- });
2641
- const badRequest = (message) => ({
2642
- ok: false,
2643
- error: {
2644
- code: "bad-request",
2645
- message,
2646
- details: { issues: [] }
2647
- }
2648
- });
2649
- const accountStatusError = (message) => ({
2650
- ok: false,
2651
- error: {
2652
- code: "internal",
2653
- message,
2654
- details: { issues: [] }
2734
+ function parseOAuthCredential$1(value) {
2735
+ try {
2736
+ return assertOAuthCredential$1(JSON.parse(value));
2737
+ } catch (error) {
2738
+ if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2739
+ throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2655
2740
  }
2656
- });
2657
- const classifyAccountStatusError = (error) => {
2658
- const message = error instanceof Error ? error.message : "";
2659
- if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
2660
- if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
2661
- const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
2662
- if (error?.name === "TimeoutError" || [
2663
- "TIMEOUT",
2664
- "ETIMEDOUT",
2665
- "UND_ERR_CONNECT_TIMEOUT"
2666
- ].includes(code)) return ["transport", "Codex account status service is unavailable"];
2667
- if ([
2668
- "ECONNRESET",
2669
- "ECONNREFUSED",
2670
- "ENOTFOUND",
2671
- "EAI_AGAIN",
2672
- "NETWORK",
2673
- "NETWORK_ERROR",
2674
- "TRANSPORT"
2675
- ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
2676
- return ["unknown", "Could not read Codex account status"];
2677
- };
2678
- const deferred = () => {
2679
- let resolve;
2680
- let reject;
2741
+ }
2742
+ function normalizeLabel(value) {
2743
+ if (typeof value !== "string") throw new Error("Codex account label must be text");
2744
+ const label = value.trim().replace(/\s+/gu, " ");
2745
+ if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2746
+ return label;
2747
+ }
2748
+ function assertVaultRecord(record) {
2749
+ if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2750
+ const ids = /* @__PURE__ */ new Set();
2751
+ const accounts = record.payload.accounts.map((account) => {
2752
+ if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2753
+ ids.add(account.id);
2754
+ return {
2755
+ id: account.id,
2756
+ label: normalizeLabel(account.label),
2757
+ credential: sanitizeOAuthCredential(account.credential)
2758
+ };
2759
+ });
2760
+ if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2761
+ const legacyAccountId = record.payload.legacyAccountId;
2762
+ if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
2681
2763
  return {
2682
- promise: new Promise((onResolve, onReject) => {
2683
- resolve = onResolve;
2684
- reject = onReject;
2685
- }),
2686
- resolve,
2687
- reject
2764
+ version: VERSION,
2765
+ activeId: record.payload.activeId,
2766
+ legacyAccountId,
2767
+ accounts
2688
2768
  };
2689
- };
2690
- const publicPrompt = (prompt) => ({
2691
- type: prompt.type,
2692
- message: String(prompt.message ?? ""),
2693
- ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
2694
- });
2695
- function classifyLoginFailure(error) {
2696
- const message = error instanceof Error ? error.message : "";
2697
- if (/token exchange failed/iu.test(message)) return "token-exchange";
2698
- if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
2699
- if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
2700
- if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
2701
- if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
2702
- return "provider";
2703
2769
  }
2704
- /** Own one host-side login without exposing tokens to the browser client. */
2705
- var CodexLoginCoordinator = class {
2706
- #sessions = /* @__PURE__ */ new Map();
2707
- #activeId;
2708
- constructor(auth, options = {}) {
2709
- this.auth = auth;
2710
- this.createId = options.createId ?? (() => crypto.randomUUID());
2770
+ const grant = (payload) => ({
2771
+ kind: "grant",
2772
+ payload
2773
+ });
2774
+ var PendingOAuthCredentialStore = class {
2775
+ #credential;
2776
+ async read(providerId) {
2777
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2778
+ return clone$1(this.#credential);
2711
2779
  }
2712
- async accountStatus(options) {
2713
- return publicClone(await this.auth.status(options));
2780
+ async list() {
2781
+ return this.#credential === void 0 ? [] : [{
2782
+ providerId: "openai-codex",
2783
+ type: "oauth"
2784
+ }];
2714
2785
  }
2715
- supportState() {
2716
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2717
- if (active === void 0) return { phase: "idle" };
2718
- return {
2719
- method: active.view.method,
2720
- phase: active.view.phase,
2721
- ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
2722
- };
2786
+ async modify(providerId, update) {
2787
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2788
+ const next = await update(clone$1(this.#credential));
2789
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2790
+ return clone$1(this.#credential);
2723
2791
  }
2724
- async start({ method, label }) {
2725
- if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
2726
- if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
2727
- const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
2728
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
2729
- active.view = {
2730
- id: active.view.id,
2731
- provider: "openai-codex",
2732
- method: active.view.method,
2733
- phase: "cancelled",
2734
- authenticated: false
2735
- };
2736
- active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
2737
- }
2738
- if (active !== void 0) this.#sessions.delete(active.view.id);
2739
- const id = this.createId();
2740
- const ready = deferred();
2741
- const controller = new AbortController();
2742
- const session = {
2743
- controller,
2744
- prompt: void 0,
2745
- ready,
2746
- view: {
2747
- id,
2748
- provider: "openai-codex",
2749
- method,
2750
- phase: "starting",
2751
- authenticated: false
2752
- }
2753
- };
2754
- this.#sessions.set(id, session);
2755
- this.#activeId = id;
2756
- const publishReady = () => ready.resolve(publicClone(session.view));
2757
- const interaction = {
2758
- signal: controller.signal,
2759
- prompt: async (prompt) => {
2760
- controller.signal.throwIfAborted();
2761
- if (prompt.type === "select") return method;
2762
- if (![
2763
- "manual_code",
2764
- "text",
2765
- "secret"
2766
- ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
2767
- const answer = deferred();
2768
- session.prompt = answer;
2769
- session.view = {
2770
- ...session.view,
2771
- phase: "waiting_input",
2772
- prompt: publicPrompt(prompt)
2773
- };
2774
- const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
2775
- controller.signal.addEventListener("abort", abortPrompt, { once: true });
2776
- prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
2777
- publishReady();
2778
- try {
2779
- return await answer.promise;
2780
- } finally {
2781
- controller.signal.removeEventListener("abort", abortPrompt);
2782
- prompt.signal?.removeEventListener("abort", abortPrompt);
2783
- if (session.prompt === answer) session.prompt = void 0;
2784
- }
2785
- },
2786
- notify: (event) => {
2787
- if (controller.signal.aborted) return;
2788
- if (event.type === "auth_url") session.view = {
2789
- ...session.view,
2790
- phase: "waiting_browser",
2791
- authUrl: assertCodexAuthUrl(event.url),
2792
- ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
2793
- };
2794
- else if (event.type === "device_code") session.view = {
2795
- ...session.view,
2796
- phase: "waiting_device",
2797
- deviceCode: {
2798
- userCode: event.userCode,
2799
- verificationUri: assertCodexAuthUrl(event.verificationUri),
2800
- ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
2801
- ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
2802
- }
2803
- };
2804
- else session.view = {
2805
- ...session.view,
2806
- message: String(event.message ?? "")
2807
- };
2808
- publishReady();
2809
- }
2810
- };
2811
- session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
2812
- if (controller.signal.aborted) return;
2813
- const status = await this.auth.status();
2814
- session.view = {
2815
- id,
2816
- provider: "openai-codex",
2817
- method,
2818
- phase: "authenticated",
2819
- authenticated: status.authenticated === true,
2820
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2821
- };
2822
- }).catch(async (error) => {
2823
- if (controller.signal.aborted) {
2824
- session.view = {
2825
- id,
2826
- provider: "openai-codex",
2827
- method,
2828
- phase: "cancelled",
2829
- authenticated: false
2830
- };
2831
- return;
2832
- }
2833
- try {
2834
- if (label !== void 0) throw error;
2835
- const status = await this.auth.status();
2836
- if (status.authenticated === true) {
2837
- session.view = {
2838
- id,
2839
- provider: "openai-codex",
2840
- method,
2841
- phase: "authenticated",
2842
- authenticated: true,
2843
- ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2844
- };
2845
- return;
2846
- }
2847
- } catch {}
2848
- session.view = {
2849
- id,
2850
- provider: "openai-codex",
2851
- method,
2852
- phase: "failed",
2853
- authenticated: false,
2854
- error: "Codex login failed"
2855
- };
2856
- session.hostError = error;
2857
- }).finally(publishReady);
2858
- return ready.promise;
2792
+ async delete(providerId) {
2793
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2794
+ this.#credential = void 0;
2859
2795
  }
2860
- read(id) {
2861
- const session = this.#sessions.get(id);
2862
- if (session === void 0) throw new Error("unknown Codex login");
2863
- return publicClone(session.view);
2796
+ credential() {
2797
+ return clone$1(this.#credential);
2864
2798
  }
2865
- async submit({ id, value }) {
2866
- const session = this.#sessions.get(id);
2867
- if (session === void 0) throw new Error("unknown Codex login");
2868
- if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
2869
- if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
2870
- const answer = session.prompt;
2871
- session.prompt = void 0;
2872
- session.view = {
2873
- ...session.view,
2874
- phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
2875
- prompt: void 0
2876
- };
2877
- answer.resolve(value);
2878
- return this.read(id);
2799
+ };
2800
+ /**
2801
+ * Multi-account owner state stored in DSH's atomic plugin credential record.
2802
+ * The old single-account reference remains as a rollback source and is kept in
2803
+ * sync whenever that imported account rotates its refresh token.
2804
+ */
2805
+ var DshOAuthAccountVault = class {
2806
+ #tail = Promise.resolve();
2807
+ constructor(credentials, options) {
2808
+ if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2809
+ this.credentials = credentials;
2810
+ this.key = options.key;
2811
+ this.legacyRef = options.legacyRef;
2812
+ this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2813
+ this.createId = options.createId ?? randomUUID;
2814
+ this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
2879
2815
  }
2880
- async cancel(id) {
2881
- const session = this.#sessions.get(id);
2882
- if (session === void 0) throw new Error("unknown Codex login");
2883
- if (!TERMINAL_PHASES.has(session.view.phase)) {
2884
- session.view = {
2885
- id,
2886
- provider: "openai-codex",
2887
- method: session.view.method,
2888
- phase: "cancelled",
2889
- authenticated: false
2816
+ #enqueue(operation) {
2817
+ const current = this.#tail.catch(() => void 0).then(operation);
2818
+ this.#tail = current.catch(() => void 0);
2819
+ return current;
2820
+ }
2821
+ async #legacyCredential() {
2822
+ for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2823
+ const hit = await this.credentials.resolve(ref);
2824
+ if (hit?.value === void 0 || hit.value === "") continue;
2825
+ return {
2826
+ ref,
2827
+ credential: parseOAuthCredential$1(hit.value)
2890
2828
  };
2891
- session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
2892
2829
  }
2893
- return this.read(id);
2894
2830
  }
2895
- async logout(options) {
2896
- if (this.#activeId !== void 0) {
2897
- const active = this.#sessions.get(this.#activeId);
2898
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
2831
+ async #ensurePayload() {
2832
+ const existing = await this.credentials.readRecord(this.key);
2833
+ if (existing !== void 0) return assertVaultRecord(existing);
2834
+ const legacy = await this.#legacyCredential();
2835
+ if (legacy === void 0) return void 0;
2836
+ const id = this.createId();
2837
+ return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2838
+ if (current !== void 0) return Promise.resolve(current);
2839
+ return Promise.resolve(grant({
2840
+ version: VERSION,
2841
+ activeId: id,
2842
+ legacyAccountId: id,
2843
+ accounts: [{
2844
+ id,
2845
+ label: DEFAULT_LABEL,
2846
+ credential: legacy.credential
2847
+ }]
2848
+ }));
2849
+ }));
2850
+ }
2851
+ async #modifyPayload(update) {
2852
+ await this.#ensurePayload();
2853
+ let previousLegacy;
2854
+ const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2855
+ if (current === void 0) throw new Error("Codex account vault is not signed in");
2856
+ const payload = assertVaultRecord(current);
2857
+ previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2858
+ const next = await update(clone$1(payload));
2859
+ return grant(next);
2860
+ }));
2861
+ const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2862
+ try {
2863
+ if (legacy === void 0) {
2864
+ if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2865
+ } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2866
+ } catch {
2867
+ this.onLegacySyncFailure();
2899
2868
  }
2900
- await this.auth.logout(options);
2901
- return this.accountStatus(options);
2869
+ return payload;
2902
2870
  }
2903
- async selectAccount(id) {
2904
- return publicClone(await this.auth.select(id));
2871
+ list() {
2872
+ return this.#enqueue(async () => {
2873
+ const payload = await this.#ensurePayload();
2874
+ if (payload === void 0) return [];
2875
+ return payload.accounts.map((account) => ({
2876
+ id: account.id,
2877
+ label: account.label,
2878
+ active: account.id === payload.activeId,
2879
+ expiresAt: account.credential.expires,
2880
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2881
+ }));
2882
+ });
2905
2883
  }
2906
- async removeAccount(id) {
2907
- return publicClone(await this.auth.remove(id));
2884
+ readActive() {
2885
+ return this.#enqueue(async () => {
2886
+ const payload = await this.#ensurePayload();
2887
+ return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2888
+ });
2908
2889
  }
2909
- };
2910
- /** Map the loopback-only DSH Connection channel onto the coordinator. */
2911
- function createCodexRpcHandler(coordinator, options = {}) {
2912
- const openExternal = options.openExternal;
2913
- return async (endpoint, payload, signal) => {
2914
- try {
2915
- signal.throwIfAborted();
2916
- const input = asObject(payload);
2917
- if (endpoint === "status") try {
2918
- return ok(await coordinator.accountStatus({ signal }));
2919
- } catch (error) {
2920
- if (signal.aborted) throw error;
2921
- const [, message] = classifyAccountStatusError(error);
2922
- return accountStatusError(message);
2923
- }
2924
- if (endpoint === "login/start") {
2925
- const started = await coordinator.start({
2926
- method: input.method,
2927
- label: input.label
2928
- });
2929
- if (input.openExternal !== true) return ok(started);
2930
- const url = started.authUrl ?? started.deviceCode?.verificationUri;
2931
- if (typeof url !== "string" || openExternal === void 0) return ok({
2932
- ...started,
2933
- externalOpened: false
2934
- });
2935
- try {
2936
- await openExternal(url);
2937
- return ok({
2938
- ...started,
2939
- externalOpened: true
2940
- });
2941
- } catch {
2942
- return ok({
2943
- ...started,
2944
- externalOpened: false
2945
- });
2946
- }
2947
- }
2948
- if (endpoint === "login/status") return ok(coordinator.read(input.id));
2949
- if (endpoint === "login/submit") return ok(await coordinator.submit({
2950
- id: input.id,
2951
- value: input.value
2952
- }));
2953
- if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
2954
- if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
2955
- if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
2956
- if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
2957
- return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
2958
- } catch (error) {
2959
- if (signal.aborted) throw error;
2960
- const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
2961
- return badRequest(message);
2962
- }
2963
- };
2964
- }
2965
- //#endregion
2966
- //#region src/transport-failure.js
2967
- function normalizeTransportEvent(event, signal) {
2968
- if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
2969
- const message = event.error?.errorMessage;
2970
- if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
2971
- return {
2972
- ...event,
2973
- error: {
2974
- ...event.error,
2975
- errorMessage: `Network transport failure: ${message}`
2976
- }
2977
- };
2978
- }
2979
- //#endregion
2980
- //#region src/pi-ai-runtime.js
2981
- const FAST_SERVICE_TIER = "priority";
2982
- const ULTRAFAST_SERVICE_TIER = "ultrafast";
2983
- /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
2984
- function withInputImageDetail(payload, detail) {
2985
- if (!Array.isArray(payload.input)) return payload;
2986
- let changed = false;
2987
- const input = payload.input.map((item) => {
2988
- const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
2989
- if (field === void 0) return item;
2990
- let itemChanged = false;
2991
- const parts = item[field].map((part) => {
2992
- if (part?.type !== "input_image" || part.detail === detail) return part;
2993
- itemChanged = true;
2890
+ activeId() {
2891
+ return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
2892
+ }
2893
+ add(label, credential) {
2894
+ return this.#enqueue(async () => {
2895
+ const normalizedLabel = normalizeLabel(label);
2896
+ const validated = sanitizeOAuthCredential(credential);
2897
+ await this.#ensurePayload();
2898
+ const id = this.createId();
2899
+ const account = (await this.#modifyPayload((current) => ({
2900
+ ...current,
2901
+ activeId: id,
2902
+ accounts: [...current.accounts, {
2903
+ id,
2904
+ label: normalizedLabel,
2905
+ credential: validated
2906
+ }]
2907
+ }))).accounts.find((candidate) => candidate.id === id);
2994
2908
  return {
2995
- ...part,
2996
- detail
2909
+ id,
2910
+ label: account.label,
2911
+ active: true,
2912
+ expiresAt: account.credential.expires,
2913
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2997
2914
  };
2998
2915
  });
2999
- if (!itemChanged) return item;
3000
- changed = true;
3001
- return {
3002
- ...item,
3003
- [field]: parts
3004
- };
3005
- });
3006
- return changed ? {
3007
- ...payload,
3008
- input
3009
- } : payload;
2916
+ }
2917
+ select(id) {
2918
+ return this.#enqueue(async () => {
2919
+ await this.#modifyPayload((current) => {
2920
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2921
+ return {
2922
+ ...current,
2923
+ activeId: id
2924
+ };
2925
+ });
2926
+ });
2927
+ }
2928
+ modifyActive(update) {
2929
+ return this.#enqueue(async () => {
2930
+ if (await this.#ensurePayload() === void 0) {
2931
+ const initial = await update(void 0);
2932
+ if (initial === void 0) return void 0;
2933
+ const credential = sanitizeOAuthCredential(initial);
2934
+ await this.credentials.set(this.legacyRef, JSON.stringify(credential));
2935
+ await this.#ensurePayload();
2936
+ return clone$1(credential);
2937
+ }
2938
+ let result;
2939
+ await this.#modifyPayload(async (current) => {
2940
+ const index = current.accounts.findIndex((account) => account.id === current.activeId);
2941
+ const previous = clone$1(current.accounts[index].credential);
2942
+ const next = await update(previous);
2943
+ if (next === void 0) {
2944
+ result = previous;
2945
+ return current;
2946
+ }
2947
+ const credential = sanitizeOAuthCredential(next);
2948
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
2949
+ const accounts = [...current.accounts];
2950
+ accounts[index] = {
2951
+ ...accounts[index],
2952
+ credential
2953
+ };
2954
+ result = clone$1(credential);
2955
+ return {
2956
+ ...current,
2957
+ accounts
2958
+ };
2959
+ });
2960
+ return result;
2961
+ });
2962
+ }
2963
+ deleteAll() {
2964
+ return this.#enqueue(async () => {
2965
+ await this.credentials.deleteRecord(this.key);
2966
+ await this.credentials.unset(this.legacyRef);
2967
+ for (const ref of this.legacyRefs) await this.credentials.unset(ref);
2968
+ });
2969
+ }
2970
+ remove(id) {
2971
+ return this.#enqueue(async () => {
2972
+ await this.#modifyPayload((current) => {
2973
+ if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
2974
+ if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
2975
+ const accounts = current.accounts.filter((account) => account.id !== id);
2976
+ return {
2977
+ ...current,
2978
+ activeId: current.activeId === id ? accounts[0].id : current.activeId,
2979
+ legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
2980
+ accounts
2981
+ };
2982
+ });
2983
+ });
2984
+ }
2985
+ };
2986
+ //#endregion
2987
+ //#region src/credential-store.js
2988
+ const PROVIDER$1 = "openai-codex";
2989
+ const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
2990
+ const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
2991
+ function assertProvider(providerId) {
2992
+ if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
2993
+ }
2994
+ function assertOAuthCredential(value) {
2995
+ if (value === void 0) return void 0;
2996
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex credential store received a malformed OAuth credential");
2997
+ return clone(value);
2998
+ }
2999
+ function parseOAuthCredential(value) {
3000
+ try {
3001
+ return assertOAuthCredential(JSON.parse(value));
3002
+ } catch (error) {
3003
+ if (error?.message === "Codex credential store received a malformed OAuth credential") throw error;
3004
+ throw new Error("Codex credential store contains malformed OAuth JSON", { cause: error });
3005
+ }
3010
3006
  }
3011
3007
  /**
3012
- * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
3013
- * PiAiAdapter to pass the access token resolved by the host credential store.
3014
- *
3015
- * PiAiAdapter owns a request-local Models collection backed by the same DSH
3016
- * credential store as this provider. A pure OAuth provider ignores its
3017
- * `apiKey` request override and otherwise fails before dispatch with "Provider
3018
- * is not configured". This non-interactive bridge teaches that collection how
3019
- * to consume only the already-refreshed token for this request; login, refresh,
3020
- * persistence, headers, transport, and model behavior remain owned by the
3021
- * original provider.
3008
+ * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
3009
+ * Refresh/login/logout operations are serialized so an older refresh response
3010
+ * cannot overwrite a newer rotated token.
3022
3011
  */
3023
- function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
3024
- const provider = createOpenAICodexProvider();
3025
- const requestToken = Object.freeze({
3026
- name: "DSH-managed Codex OAuth request token",
3027
- async resolve({ credential }) {
3028
- const token = credential?.type === "api_key" ? credential.key : void 0;
3029
- if (typeof token !== "string" || token.length === 0) return void 0;
3030
- return {
3031
- auth: { apiKey: token },
3032
- source: "DSH-managed OAuth request"
3012
+ var DshOAuthCredentialStore = class {
3013
+ #chains = /* @__PURE__ */ new Map();
3014
+ constructor(credentials, ref, legacyRefs = [], options = {}) {
3015
+ if (credentials === void 0 || credentials === null) throw new Error("Codex OAuth requires the DSH credentials service");
3016
+ const expirySkewMs = options.expirySkewMs ?? 0;
3017
+ if (!Number.isFinite(expirySkewMs) || expirySkewMs < 0) throw new Error("Codex OAuth expiry skew must be a non-negative finite number");
3018
+ this.credentials = credentials;
3019
+ this.ref = ref;
3020
+ this.legacyRefs = Object.freeze([...legacyRefs]);
3021
+ this.expirySkewMs = expirySkewMs;
3022
+ this.vault = options.vault;
3023
+ }
3024
+ #enqueue(providerId, operation, options) {
3025
+ assertProvider(providerId);
3026
+ const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
3027
+ abortIfNeeded(options);
3028
+ return operation();
3029
+ });
3030
+ const tail = current.catch(() => void 0);
3031
+ this.#chains.set(providerId, tail);
3032
+ tail.finally(() => {
3033
+ if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
3034
+ });
3035
+ return current;
3036
+ }
3037
+ async #read(providerId, options) {
3038
+ assertProvider(providerId);
3039
+ abortIfNeeded(options);
3040
+ if (this.vault !== void 0) {
3041
+ const current = await this.vault.readActive();
3042
+ if (current === void 0) return void 0;
3043
+ return this.expirySkewMs === 0 ? current : {
3044
+ ...current,
3045
+ expires: current.expires - this.expirySkewMs
3033
3046
  };
3034
3047
  }
3035
- });
3036
- const modelMetadata = (model) => catalog?.metadata(model?.id);
3037
- const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
3038
- const withPreferences = (model, options = {}) => {
3039
- const metadata = modelMetadata(model);
3040
- const requestedVerbosity = resolveOutputVerbosity();
3041
- const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
3042
- const speedMode = resolveSpeedMode();
3043
- const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
3044
- const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
3045
- const onPayload = options.onPayload;
3046
- return {
3047
- ...options,
3048
- ...textVerbosity === void 0 ? {} : { textVerbosity },
3049
- ...serviceTier === void 0 ? {} : { serviceTier },
3050
- async onPayload(payload, requestModel) {
3051
- const preferred = {
3052
- ...payload,
3053
- ...textVerbosity === void 0 ? {} : { text: {
3054
- ...payload.text ?? {},
3055
- verbosity: textVerbosity
3056
- } },
3057
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3058
- };
3059
- const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
3060
- const next = await onPayload?.(managed, requestModel);
3061
- const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
3062
- return {
3063
- ...detailed,
3064
- ...textVerbosity === void 0 ? {} : { text: {
3065
- ...detailed.text ?? {},
3066
- verbosity: textVerbosity
3067
- } },
3068
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3069
- };
3070
- }
3071
- };
3072
- };
3073
- const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
3074
- const maximum = modelContextMaximum(model);
3075
- const mode = resolveContextMode();
3076
- if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
3077
- if (mode === "extended") {
3078
- const contextWindow = maximum;
3079
- return {
3080
- ...model,
3081
- contextWindow
3082
- };
3048
+ let hit = await this.credentials.resolve(this.ref);
3049
+ if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
3050
+ const legacy = await this.credentials.resolve(legacyRef);
3051
+ if (legacy?.value === void 0 || legacy.value === "") continue;
3052
+ const migrated = parseOAuthCredential(legacy.value);
3053
+ await this.credentials.set(this.ref, JSON.stringify(migrated));
3054
+ await this.credentials.unset(legacyRef);
3055
+ hit = { value: JSON.stringify(migrated) };
3056
+ break;
3083
3057
  }
3084
- const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
3085
- return {
3086
- ...model,
3087
- contextWindow: requested
3088
- };
3089
- });
3090
- const networkIterable = (factory, options) => {
3091
- let iterator;
3092
- let prepared;
3093
- const step = async (method, value) => {
3094
- const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3095
- const result = await runNetwork("model", () => {
3096
- iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3097
- return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3098
- done: true,
3099
- value
3100
- }));
3101
- }, compaction?.networkOptions(request.network) ?? request.network);
3102
- return result.done ? result : {
3103
- ...result,
3104
- value: normalizeTransportEvent(result.value, request.options?.signal)
3105
- };
3106
- };
3107
- return {
3108
- [Symbol.asyncIterator]() {
3109
- return this;
3110
- },
3111
- next: (value) => step("next", value),
3112
- return: (value) => iterator ? step("return", value) : Promise.resolve({
3113
- done: true,
3114
- value
3115
- }),
3116
- throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3117
- };
3118
- };
3119
- return Object.freeze({
3120
- ...provider,
3121
- auth: Object.freeze({
3122
- ...provider.auth,
3123
- apiKey: requestToken
3124
- }),
3125
- getModels,
3126
- stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3127
- streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
3128
- });
3129
- }
3130
- Object.freeze([
3131
- "0.82.1",
3132
- "0.85.1",
3133
- "0.87.1"
3134
- ]);
3135
- //#endregion
3136
- //#region src/subscription-credentials.js
3137
- async function readSubscriptionCredentials(getAuth, readCredential, signal) {
3138
- for (let attempt = 0; attempt < 2; attempt++) {
3139
- const auth = await getAuth({ signal });
3140
- signal?.throwIfAborted();
3141
- if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
3142
- const credential = await readCredential({ signal });
3143
- signal?.throwIfAborted();
3144
- if (credential?.type !== "oauth") break;
3145
- if (credential.access === auth.auth.apiKey) return {
3146
- access: credential.access,
3147
- accountId: credential.accountId
3058
+ abortIfNeeded(options);
3059
+ if (hit?.value === void 0 || hit.value === "") return void 0;
3060
+ const credential = parseOAuthCredential(hit.value);
3061
+ return this.expirySkewMs === 0 ? credential : {
3062
+ ...credential,
3063
+ expires: credential.expires - this.expirySkewMs
3148
3064
  };
3149
3065
  }
3150
- return {
3151
- access: void 0,
3152
- accountId: void 0
3153
- };
3154
- }
3155
- //#endregion
3156
- //#region src/version.js
3157
- const PACKAGE_VERSION = "2.4.2";
3158
- const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
3159
- //#endregion
3160
- //#region src/model-catalog.js
3161
- const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3162
- const LEVELS = [
3163
- "off",
3164
- "minimal",
3165
- "low",
3166
- "medium",
3167
- "high",
3168
- "xhigh",
3169
- "max"
3170
- ];
3171
- const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3172
- const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3173
- const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3174
- const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3175
- function upgradeModelId(value) {
3176
- return (typeof value.upgrade === "string" ? nonEmpty$2(value.upgrade) : record$4(value.upgrade) ? nonEmpty$2(value.upgrade.model) : void 0) ?? (record$4(value.upgrade_info) ? nonEmpty$2(value.upgrade_info.model) : void 0) ?? (record$4(value.upgradeInfo) ? nonEmpty$2(value.upgradeInfo.model) : void 0);
3177
- }
3178
- function retirementMillis(value) {
3179
- for (const source of [
3180
- value,
3181
- value.upgrade,
3182
- value.upgrade_info,
3183
- value.upgradeInfo
3184
- ]) {
3185
- if (!record$4(source)) continue;
3186
- for (const raw of [source.retirement_at, source.retirementAt]) {
3187
- const seconds = positiveInteger$1(raw);
3188
- if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3189
- if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3190
- const parsed = Date.parse(raw);
3191
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
3066
+ read(providerId, options) {
3067
+ return this.#enqueue(providerId, () => this.#read(providerId, options), options);
3068
+ }
3069
+ async list(options) {
3070
+ abortIfNeeded(options);
3071
+ return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
3072
+ providerId: PROVIDER$1,
3073
+ type: "oauth"
3074
+ }];
3075
+ }
3076
+ modify(providerId, update, options) {
3077
+ return this.#enqueue(providerId, async () => {
3078
+ if (this.vault !== void 0) {
3079
+ const next = await this.vault.modifyActive(async (current) => {
3080
+ const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
3081
+ ...current,
3082
+ expires: current.expires - this.expirySkewMs
3083
+ };
3084
+ const updated = await update(clone(visible));
3085
+ return updated === void 0 ? void 0 : assertOAuthCredential(updated);
3086
+ });
3087
+ abortIfNeeded(options);
3088
+ return clone(next);
3089
+ }
3090
+ const current = await this.#read(providerId, options);
3091
+ const next = await update(clone(current));
3092
+ abortIfNeeded(options);
3093
+ if (next === void 0) return current;
3094
+ const validated = assertOAuthCredential(next);
3095
+ await this.credentials.set(this.ref, JSON.stringify(validated));
3096
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
3097
+ abortIfNeeded(options);
3098
+ return clone(validated);
3099
+ }, options);
3100
+ }
3101
+ delete(providerId, options) {
3102
+ return this.#enqueue(providerId, async () => {
3103
+ if (this.vault !== void 0) {
3104
+ await this.vault.deleteAll();
3105
+ abortIfNeeded(options);
3106
+ return;
3107
+ }
3108
+ await this.credentials.unset(this.ref);
3109
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
3110
+ abortIfNeeded(options);
3111
+ }, options);
3112
+ }
3113
+ };
3114
+ /** Return only account state that is safe to expose to the browser client. */
3115
+ function createCodexAuthService(models, store, options = {}) {
3116
+ const runLogin = options.runLogin ?? ((run) => run());
3117
+ const accountVault = options.accountVault;
3118
+ const createLoginModels = options.createLoginModels;
3119
+ const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
3120
+ return Object.freeze({
3121
+ async status(options) {
3122
+ const current = await store.read(PROVIDER$1, options);
3123
+ const accounts = await accountVault?.list();
3124
+ if (current === void 0) return {
3125
+ authenticated: false,
3126
+ provider: PROVIDER$1,
3127
+ ...accounts === void 0 ? {} : { accounts }
3128
+ };
3129
+ return {
3130
+ authenticated: true,
3131
+ provider: PROVIDER$1,
3132
+ type: "oauth",
3133
+ expiresAt: current.expires,
3134
+ ...accounts === void 0 ? {} : { accounts }
3135
+ };
3136
+ },
3137
+ login(interaction, input = {}) {
3138
+ if (input.label !== void 0) {
3139
+ if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
3140
+ return runLogin(async () => {
3141
+ const pending = createPendingStore();
3142
+ await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
3143
+ const credential = pending.credential();
3144
+ if (credential === void 0) throw new Error("Codex login did not return credentials");
3145
+ await accountVault.add(input.label, credential);
3146
+ });
3192
3147
  }
3148
+ return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
3149
+ },
3150
+ async select(id) {
3151
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
3152
+ await accountVault.select(id);
3153
+ return this.status();
3154
+ },
3155
+ async remove(id) {
3156
+ if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
3157
+ await accountVault.remove(id);
3158
+ return this.status();
3159
+ },
3160
+ logout(options) {
3161
+ return models.logout(PROVIDER$1, options);
3193
3162
  }
3163
+ });
3164
+ }
3165
+ //#endregion
3166
+ //#region src/external-url.js
3167
+ const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
3168
+ /** Validate the only external origin this plugin may launch. */
3169
+ function assertCodexAuthUrl(value) {
3170
+ let url;
3171
+ try {
3172
+ url = new URL(value);
3173
+ } catch {
3174
+ throw new Error("Codex auth URL is invalid");
3194
3175
  }
3176
+ if (url.protocol !== "https:") throw new Error("Codex auth URL must use HTTPS");
3177
+ if (url.origin !== OPENAI_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Codex auth URL must use the OpenAI auth origin");
3178
+ return url.href;
3195
3179
  }
3196
- function retirementNotice(value, now = Date.now(), targetDisplayName) {
3197
- if (!record$4(value) || !Number.isFinite(now)) return void 0;
3198
- const at = retirementMillis(value);
3199
- if (at === void 0 || at <= now) return void 0;
3200
- const date = new Date(at);
3201
- if (!Number.isFinite(date.getTime())) return void 0;
3202
- const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3203
- const upgradeTo = upgradeModelId(value);
3204
- const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$2(targetDisplayName) ?? upgradeTo;
3205
- return {
3206
- text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3207
- at,
3208
- upgradeTo,
3209
- dateText
3180
+ /** Return a shell-free native opener command for the current desktop. */
3181
+ function commandForCodexAuthUrl(value, platform = process.platform) {
3182
+ const url = assertCodexAuthUrl(value);
3183
+ if (platform === "win32") return {
3184
+ file: "rundll32.exe",
3185
+ args: ["url.dll,FileProtocolHandler", url],
3186
+ shell: false
3210
3187
  };
3211
- }
3212
- function reasoningMap(levels) {
3213
- const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$2(record$4(level) ? level.effort : void 0)).filter(Boolean));
3214
- const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3215
- if (supported.has("none")) map.off = "none";
3216
- for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3217
- return map;
3218
- }
3219
- const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3220
- const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3221
- function unsupportedCapabilities(value) {
3222
- const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3223
- const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3224
- const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3225
- "auto",
3226
- "default",
3227
- "standard",
3228
- "fast",
3229
- "priority",
3230
- "ultrafast"
3231
- ].includes(tier));
3232
- return {
3233
- ...reasoning.length ? { reasoning } : {},
3234
- ...inputs.length ? { inputs } : {},
3235
- ...speeds.length ? { speeds } : {}
3188
+ if (platform === "darwin") return {
3189
+ file: "open",
3190
+ args: [url],
3191
+ shell: false
3192
+ };
3193
+ if (platform === "linux") return {
3194
+ file: "xdg-open",
3195
+ args: [url],
3196
+ shell: false
3236
3197
  };
3198
+ throw new Error(`Codex auth URL opener is unsupported on ${platform}`);
3237
3199
  }
3238
- function visibleModel(value, displayNames, now) {
3239
- if (!record$4(value)) return void 0;
3240
- const id = nonEmpty$2(value.slug);
3241
- const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3242
- if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3243
- const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3244
- const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3245
- const unsupported = unsupportedCapabilities({
3246
- ...value,
3247
- supported_reasoning_levels: supported
3200
+ function openCodexAuthUrl(value, options = {}) {
3201
+ const command = commandForCodexAuthUrl(value, options.platform);
3202
+ const spawnProcess = options.spawn ?? spawn;
3203
+ return new Promise((resolve, reject) => {
3204
+ const child = spawnProcess(command.file, command.args, {
3205
+ detached: true,
3206
+ stdio: "ignore",
3207
+ windowsHide: true,
3208
+ shell: command.shell
3209
+ });
3210
+ child.once("error", reject);
3211
+ child.once("spawn", () => {
3212
+ child.unref();
3213
+ resolve();
3214
+ });
3248
3215
  });
3249
- const upgradeTo = upgradeModelId(value);
3250
- const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3251
- const notice = retirement?.text;
3252
- const description = nonEmpty$2(value.description);
3253
- return {
3254
- ...Object.keys(unsupported).length ? { unsupported } : {},
3255
- id,
3256
- name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$2(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3257
- description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3258
- ...retirement === void 0 ? {} : { retirement: {
3259
- at: retirement.at,
3260
- upgradeTo: retirement.upgradeTo
3261
- } },
3262
- priority: Number.isFinite(value.priority) ? value.priority : 0,
3263
- input: input.length > 0 ? input : ["text"],
3264
- contextWindow: positiveInteger$1(value.context_window) ?? positiveInteger$1(value.max_context_window),
3265
- ...positiveInteger$1(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3266
- reasoning: supported.length > 0,
3267
- thinkingLevelMap: reasoningMap(supported),
3268
- supportVerbosity: value.support_verbosity === true,
3269
- defaultVerbosity: [
3270
- "low",
3271
- "medium",
3272
- "high"
3273
- ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3274
- supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3275
- supportsUltrafast: speedTierIds(value).includes("ultrafast")
3276
- };
3277
3216
  }
3278
- function parseOfficialModelCatalog(value, now = Date.now()) {
3279
- if (!record$4(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3280
- const displayNames = /* @__PURE__ */ new Map();
3281
- for (const model of value.models) {
3282
- if (!record$4(model)) continue;
3283
- const id = nonEmpty$2(model.slug);
3284
- const name = nonEmpty$2(model.display_name);
3285
- if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
3217
+ //#endregion
3218
+ //#region src/login-coordinator.js
3219
+ const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser", "device_code"]);
3220
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
3221
+ "authenticated",
3222
+ "failed",
3223
+ "cancelled"
3224
+ ]);
3225
+ const publicClone = (value) => structuredClone(value);
3226
+ const asObject = (value) => value !== null && typeof value === "object" ? value : {};
3227
+ const ok = (value) => ({
3228
+ ok: true,
3229
+ value
3230
+ });
3231
+ const badRequest = (message) => ({
3232
+ ok: false,
3233
+ error: {
3234
+ code: "bad-request",
3235
+ message,
3236
+ details: { issues: [] }
3286
3237
  }
3287
- const seen = /* @__PURE__ */ new Set();
3288
- return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3289
- }
3290
- function mergeModel(baseModels, remote) {
3291
- const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3292
- if (base === void 0) return void 0;
3238
+ });
3239
+ const accountStatusError = (message) => ({
3240
+ ok: false,
3241
+ error: {
3242
+ code: "internal",
3243
+ message,
3244
+ details: { issues: [] }
3245
+ }
3246
+ });
3247
+ const classifyAccountStatusError = (error) => {
3248
+ const message = error instanceof Error ? error.message : "";
3249
+ if (/malformed (?:OAuth|grant|account vault)|received a malformed OAuth|contains malformed OAuth/iu.test(message)) return ["credential-malformed", "Codex account credentials are malformed"];
3250
+ if (/credential|account vault|readRecord|credential store|credentials service/iu.test(message)) return ["credential-unavailable", "Codex account credentials are unavailable"];
3251
+ const code = typeof error?.code === "string" ? error.code.toUpperCase() : "";
3252
+ if (error?.name === "TimeoutError" || [
3253
+ "TIMEOUT",
3254
+ "ETIMEDOUT",
3255
+ "UND_ERR_CONNECT_TIMEOUT"
3256
+ ].includes(code)) return ["transport", "Codex account status service is unavailable"];
3257
+ if ([
3258
+ "ECONNRESET",
3259
+ "ECONNREFUSED",
3260
+ "ENOTFOUND",
3261
+ "EAI_AGAIN",
3262
+ "NETWORK",
3263
+ "NETWORK_ERROR",
3264
+ "TRANSPORT"
3265
+ ].includes(code) || error?.name === "NetworkError") return ["transport", "Codex account status service is unavailable"];
3266
+ return ["unknown", "Could not read Codex account status"];
3267
+ };
3268
+ const deferred = () => {
3269
+ let resolve;
3270
+ let reject;
3293
3271
  return {
3294
- ...base,
3295
- id: remote.id,
3296
- name: remote.name,
3297
- ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3298
- input: remote.input,
3299
- reasoning: remote.reasoning,
3300
- thinkingLevelMap: remote.thinkingLevelMap,
3301
- ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3302
- ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3303
- ...base.id === remote.id ? {} : { cost: {
3304
- input: 0,
3305
- output: 0,
3306
- cacheRead: 0,
3307
- cacheWrite: 0
3308
- } }
3272
+ promise: new Promise((onResolve, onReject) => {
3273
+ resolve = onResolve;
3274
+ reject = onReject;
3275
+ }),
3276
+ resolve,
3277
+ reject
3309
3278
  };
3279
+ };
3280
+ const publicPrompt = (prompt) => ({
3281
+ type: prompt.type,
3282
+ message: String(prompt.message ?? ""),
3283
+ ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
3284
+ });
3285
+ function classifyLoginFailure(error) {
3286
+ const message = error instanceof Error ? error.message : "";
3287
+ if (/token exchange failed/iu.test(message)) return "token-exchange";
3288
+ if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
3289
+ if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
3290
+ if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
3291
+ if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
3292
+ return "provider";
3310
3293
  }
3311
- function createOfficialModelCatalog(options = {}) {
3312
- const fetchCatalog = options.fetch ?? fetch;
3313
- const getNow = options.now ?? Date.now;
3314
- const scheduleTimeout = options.setTimeout ?? setTimeout;
3315
- const cancelTimeout = options.clearTimeout ?? clearTimeout;
3316
- const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3317
- let models;
3318
- let metadata = /* @__PURE__ */ new Map();
3319
- let etag;
3320
- let rawCatalog;
3321
- let revision = 0;
3322
- let refreshing;
3323
- let generation = 0;
3324
- let refreshStatus = "idle";
3325
- const refresh = ({ signal } = {}) => {
3326
- if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3327
- if (refreshing?.generation === generation) return refreshing.promise;
3328
- const currentGeneration = generation;
3329
- refreshStatus = "refreshing";
3330
- let outcome = "idle";
3331
- const controller = new AbortController();
3332
- const abort = () => {
3333
- if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3294
+ /** Own one host-side login without exposing tokens to the browser client. */
3295
+ var CodexLoginCoordinator = class {
3296
+ #sessions = /* @__PURE__ */ new Map();
3297
+ #activeId;
3298
+ constructor(auth, options = {}) {
3299
+ this.auth = auth;
3300
+ this.createId = options.createId ?? (() => crypto.randomUUID());
3301
+ }
3302
+ async accountStatus(options) {
3303
+ return publicClone(await this.auth.status(options));
3304
+ }
3305
+ supportState() {
3306
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
3307
+ if (active === void 0) return { phase: "idle" };
3308
+ return {
3309
+ method: active.view.method,
3310
+ phase: active.view.phase,
3311
+ ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
3334
3312
  };
3335
- signal?.addEventListener("abort", abort, { once: true });
3336
- const requestSignal = controller.signal;
3337
- let timer;
3338
- const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3339
- const work = (async () => {
3340
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3341
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3342
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3343
- const headers = {
3344
- authorization: `Bearer ${access}`,
3345
- "chatgpt-account-id": accountId,
3346
- accept: "application/json",
3347
- originator: "pi",
3348
- "user-agent": USER_AGENT,
3349
- ...etag === void 0 ? {} : { "if-none-match": etag }
3313
+ }
3314
+ async start({ method, label }) {
3315
+ if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
3316
+ if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
3317
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
3318
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
3319
+ active.view = {
3320
+ id: active.view.id,
3321
+ provider: "openai-codex",
3322
+ method: active.view.method,
3323
+ phase: "cancelled",
3324
+ authenticated: false
3350
3325
  };
3351
- const response = await fetchCatalog(CODEX_MODELS_URL, {
3352
- method: "GET",
3353
- redirect: "error",
3354
- headers,
3355
- signal: requestSignal
3356
- });
3357
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3358
- if (response.status === 304) {
3359
- outcome = "ok";
3360
- if (rawCatalog === void 0) return false;
3361
- const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3362
- const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3363
- if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3364
- models = next;
3365
- metadata = new Map(remote.map((model) => [model.id, model]));
3366
- revision += 1;
3367
- options.onUpdated?.();
3368
- return true;
3326
+ active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
3327
+ }
3328
+ if (active !== void 0) this.#sessions.delete(active.view.id);
3329
+ const id = this.createId();
3330
+ const ready = deferred();
3331
+ const controller = new AbortController();
3332
+ const session = {
3333
+ controller,
3334
+ prompt: void 0,
3335
+ ready,
3336
+ view: {
3337
+ id,
3338
+ provider: "openai-codex",
3339
+ method,
3340
+ phase: "starting",
3341
+ authenticated: false
3369
3342
  }
3370
- if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3371
- const raw = await response.json();
3372
- const remote = parseOfficialModelCatalog(raw, getNow());
3373
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3374
- if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3375
- const baseModels = options.baseModels();
3376
- const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3377
- if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3378
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3379
- models = next;
3380
- rawCatalog = raw;
3381
- metadata = new Map(remote.map((model) => [model.id, model]));
3382
- etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
3383
- revision += 1;
3384
- outcome = "ok";
3385
- options.onUpdated?.();
3386
- return true;
3387
- })();
3388
- let rejectAborted;
3389
- const abortPromise = new Promise((_, reject) => {
3390
- rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3391
- if (requestSignal.aborted) rejectAborted();
3392
- else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3393
- });
3394
- timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3395
- timer.unref?.();
3396
- const promise = Promise.race([work, abortPromise]).catch((error) => {
3397
- outcome = "failed";
3398
- throw error;
3399
- }).finally(() => {
3400
- cancelTimeout(timer);
3401
- signal?.removeEventListener("abort", abort);
3402
- requestSignal.removeEventListener("abort", rejectAborted);
3403
- if (refreshing?.promise === promise) {
3404
- refreshing = void 0;
3405
- refreshStatus = outcome;
3343
+ };
3344
+ this.#sessions.set(id, session);
3345
+ this.#activeId = id;
3346
+ const publishReady = () => ready.resolve(publicClone(session.view));
3347
+ const interaction = {
3348
+ signal: controller.signal,
3349
+ prompt: async (prompt) => {
3350
+ controller.signal.throwIfAborted();
3351
+ if (prompt.type === "select") return method;
3352
+ if (![
3353
+ "manual_code",
3354
+ "text",
3355
+ "secret"
3356
+ ].includes(prompt.type)) throw new Error(`unsupported Codex auth prompt: ${String(prompt.type)}`);
3357
+ const answer = deferred();
3358
+ session.prompt = answer;
3359
+ session.view = {
3360
+ ...session.view,
3361
+ phase: "waiting_input",
3362
+ prompt: publicPrompt(prompt)
3363
+ };
3364
+ const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
3365
+ controller.signal.addEventListener("abort", abortPrompt, { once: true });
3366
+ prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
3367
+ publishReady();
3368
+ try {
3369
+ return await answer.promise;
3370
+ } finally {
3371
+ controller.signal.removeEventListener("abort", abortPrompt);
3372
+ prompt.signal?.removeEventListener("abort", abortPrompt);
3373
+ if (session.prompt === answer) session.prompt = void 0;
3374
+ }
3375
+ },
3376
+ notify: (event) => {
3377
+ if (controller.signal.aborted) return;
3378
+ if (event.type === "auth_url") session.view = {
3379
+ ...session.view,
3380
+ phase: "waiting_browser",
3381
+ authUrl: assertCodexAuthUrl(event.url),
3382
+ ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
3383
+ };
3384
+ else if (event.type === "device_code") session.view = {
3385
+ ...session.view,
3386
+ phase: "waiting_device",
3387
+ deviceCode: {
3388
+ userCode: event.userCode,
3389
+ verificationUri: assertCodexAuthUrl(event.verificationUri),
3390
+ ...typeof event.intervalSeconds === "number" ? { intervalSeconds: event.intervalSeconds } : {},
3391
+ ...typeof event.expiresInSeconds === "number" ? { expiresInSeconds: event.expiresInSeconds } : {}
3392
+ }
3393
+ };
3394
+ else session.view = {
3395
+ ...session.view,
3396
+ message: String(event.message ?? "")
3397
+ };
3398
+ publishReady();
3399
+ }
3400
+ };
3401
+ session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
3402
+ if (controller.signal.aborted) return;
3403
+ const status = await this.auth.status();
3404
+ session.view = {
3405
+ id,
3406
+ provider: "openai-codex",
3407
+ method,
3408
+ phase: "authenticated",
3409
+ authenticated: status.authenticated === true,
3410
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
3411
+ };
3412
+ }).catch(async (error) => {
3413
+ if (controller.signal.aborted) {
3414
+ session.view = {
3415
+ id,
3416
+ provider: "openai-codex",
3417
+ method,
3418
+ phase: "cancelled",
3419
+ authenticated: false
3420
+ };
3421
+ return;
3406
3422
  }
3407
- });
3408
- refreshing = {
3409
- generation: currentGeneration,
3410
- promise,
3411
- cancel: () => controller.abort()
3423
+ try {
3424
+ if (label !== void 0) throw error;
3425
+ const status = await this.auth.status();
3426
+ if (status.authenticated === true) {
3427
+ session.view = {
3428
+ id,
3429
+ provider: "openai-codex",
3430
+ method,
3431
+ phase: "authenticated",
3432
+ authenticated: true,
3433
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
3434
+ };
3435
+ return;
3436
+ }
3437
+ } catch {}
3438
+ session.view = {
3439
+ id,
3440
+ provider: "openai-codex",
3441
+ method,
3442
+ phase: "failed",
3443
+ authenticated: false,
3444
+ error: "Codex login failed"
3445
+ };
3446
+ session.hostError = error;
3447
+ }).finally(publishReady);
3448
+ return ready.promise;
3449
+ }
3450
+ read(id) {
3451
+ const session = this.#sessions.get(id);
3452
+ if (session === void 0) throw new Error("unknown Codex login");
3453
+ return publicClone(session.view);
3454
+ }
3455
+ async submit({ id, value }) {
3456
+ const session = this.#sessions.get(id);
3457
+ if (session === void 0) throw new Error("unknown Codex login");
3458
+ if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
3459
+ if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
3460
+ const answer = session.prompt;
3461
+ session.prompt = void 0;
3462
+ session.view = {
3463
+ ...session.view,
3464
+ phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
3465
+ prompt: void 0
3412
3466
  };
3413
- return promise;
3414
- };
3415
- return Object.freeze({
3416
- refresh,
3417
- getModels: () => models ?? [],
3418
- metadata: (modelId) => metadata.get(modelId),
3419
- revision: () => revision,
3420
- capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3421
- model: model.id,
3422
- ...structuredClone(model.unsupported)
3423
- })),
3424
- status: () => ({
3425
- source: models === void 0 ? "unavailable" : "online",
3426
- refresh: refreshStatus
3427
- }),
3428
- clear() {
3429
- generation += 1;
3430
- const flight = refreshing;
3431
- refreshing = void 0;
3432
- flight?.cancel();
3433
- models = void 0;
3434
- rawCatalog = void 0;
3435
- metadata = /* @__PURE__ */ new Map();
3436
- etag = void 0;
3437
- refreshStatus = "idle";
3438
- revision += 1;
3439
- options.onUpdated?.();
3467
+ answer.resolve(value);
3468
+ return this.read(id);
3469
+ }
3470
+ async cancel(id) {
3471
+ const session = this.#sessions.get(id);
3472
+ if (session === void 0) throw new Error("unknown Codex login");
3473
+ if (!TERMINAL_PHASES.has(session.view.phase)) {
3474
+ session.view = {
3475
+ id,
3476
+ provider: "openai-codex",
3477
+ method: session.view.method,
3478
+ phase: "cancelled",
3479
+ authenticated: false
3480
+ };
3481
+ session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
3440
3482
  }
3441
- });
3442
- }
3443
- //#endregion
3444
- //#region src/codex-search.js
3445
- const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
3446
- const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
3447
- const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
3448
- const DEFAULT_MODEL = "gpt-5.6-luna";
3449
- const MAX_OUTPUT_TOKENS = 4096;
3450
- const MAX_SOURCE_DATE = 64;
3451
- const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3452
- const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
3453
- const displayText = (value) => {
3454
- const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
3455
- if (text === void 0 || text.length === 0) return void 0;
3456
- return text;
3457
- };
3458
- const boundedDisplayText = (value, maximum) => {
3459
- const text = displayText(value);
3460
- if (text === void 0 || text.length <= maximum) return text;
3461
- return `${text.slice(0, maximum - 1)}…`;
3462
- };
3463
- function sourceOf(value) {
3464
- if (!record$3(value)) return void 0;
3465
- const url = nonEmpty$1(value.url);
3466
- if (url === void 0) return void 0;
3467
- let parsed;
3468
- try {
3469
- parsed = new URL(url);
3470
- } catch {
3471
- return;
3483
+ return this.read(id);
3472
3484
  }
3473
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
3474
- const title = displayText(value.title) ?? parsed.hostname;
3475
- const snippet = displayText(value.snippet);
3476
- const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
3477
- return {
3478
- url,
3479
- title,
3480
- ...snippet === void 0 ? {} : { snippet },
3481
- ...publishedAt === void 0 ? {} : { publishedAt }
3482
- };
3483
- }
3484
- function parseSearchResponse(value) {
3485
- if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
3486
- const seen = /* @__PURE__ */ new Set();
3487
- const sources = [];
3488
- for (const result of value.results ?? []) {
3489
- const source = sourceOf(result);
3490
- if (source === void 0 || seen.has(source.url)) continue;
3491
- seen.add(source.url);
3492
- sources.push(source);
3485
+ async logout(options) {
3486
+ if (this.#activeId !== void 0) {
3487
+ const active = this.#sessions.get(this.#activeId);
3488
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
3489
+ }
3490
+ await this.auth.logout(options);
3491
+ return this.accountStatus(options);
3493
3492
  }
3494
- return {
3495
- sources,
3496
- truncated: false
3497
- };
3498
- }
3499
- /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
3500
- function createCodexSearchProvider(options) {
3501
- const fetchSearch = options.fetch ?? fetch;
3502
- return Object.freeze({
3503
- id: CODEX_SEARCH_PROVIDER_ID,
3504
- available: () => true,
3505
- async search(request, signal) {
3506
- signal?.throwIfAborted();
3507
- const preferences = readCapabilitySettings(options.resolvePreferences?.());
3508
- if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
3509
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
3510
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
3511
- const model = nonEmpty$1(options.resolveModel?.()) ?? DEFAULT_MODEL;
3512
- const id = nonEmpty$1(options.resolveSessionId?.()) ?? randomUUID();
3513
- let response;
3514
- try {
3515
- response = await fetchSearch(CODEX_SEARCH_URL, {
3516
- method: "POST",
3517
- redirect: "error",
3518
- headers: {
3519
- authorization: `Bearer ${access}`,
3520
- "chatgpt-account-id": accountId,
3521
- accept: "application/json",
3522
- "content-type": "application/json",
3523
- originator: "pi",
3524
- "user-agent": USER_AGENT
3525
- },
3526
- body: JSON.stringify({
3527
- id,
3528
- model,
3529
- input: request.query,
3530
- commands: {
3531
- search_query: [{ q: request.query }],
3532
- response_length: "short"
3533
- },
3534
- settings: {
3535
- allowed_callers: ["direct"],
3536
- external_web_access: preferences.searchMode === "live"
3537
- },
3538
- max_output_tokens: MAX_OUTPUT_TOKENS
3539
- }),
3540
- signal
3541
- });
3542
- } catch (error) {
3543
- if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
3544
- throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
3545
- }
3546
- if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
3547
- let value;
3548
- try {
3549
- value = await response.json();
3493
+ async selectAccount(id) {
3494
+ return publicClone(await this.auth.select(id));
3495
+ }
3496
+ async removeAccount(id) {
3497
+ return publicClone(await this.auth.remove(id));
3498
+ }
3499
+ };
3500
+ /** Map the loopback-only DSH Connection channel onto the coordinator. */
3501
+ function createCodexRpcHandler(coordinator, options = {}) {
3502
+ const openExternal = options.openExternal;
3503
+ return async (endpoint, payload, signal) => {
3504
+ try {
3505
+ signal.throwIfAborted();
3506
+ const input = asObject(payload);
3507
+ if (endpoint === "status") try {
3508
+ return ok(await coordinator.accountStatus({ signal }));
3550
3509
  } catch (error) {
3551
- throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
3510
+ if (signal.aborted) throw error;
3511
+ const [, message] = classifyAccountStatusError(error);
3512
+ return accountStatusError(message);
3552
3513
  }
3553
- try {
3554
- const result = parseSearchResponse(value);
3555
- if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
3556
- const hostname = new URL(source.url).hostname.toLowerCase();
3557
- return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
3514
+ if (endpoint === "login/start") {
3515
+ const started = await coordinator.start({
3516
+ method: input.method,
3517
+ label: input.label
3558
3518
  });
3559
- return result;
3560
- } catch (error) {
3561
- throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
3519
+ if (input.openExternal !== true) return ok(started);
3520
+ const url = started.authUrl ?? started.deviceCode?.verificationUri;
3521
+ if (typeof url !== "string" || openExternal === void 0) return ok({
3522
+ ...started,
3523
+ externalOpened: false
3524
+ });
3525
+ try {
3526
+ await openExternal(url);
3527
+ return ok({
3528
+ ...started,
3529
+ externalOpened: true
3530
+ });
3531
+ } catch {
3532
+ return ok({
3533
+ ...started,
3534
+ externalOpened: false
3535
+ });
3536
+ }
3562
3537
  }
3538
+ if (endpoint === "login/status") return ok(coordinator.read(input.id));
3539
+ if (endpoint === "login/submit") return ok(await coordinator.submit({
3540
+ id: input.id,
3541
+ value: input.value
3542
+ }));
3543
+ if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
3544
+ if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
3545
+ if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
3546
+ if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
3547
+ return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
3548
+ } catch (error) {
3549
+ if (signal.aborted) throw error;
3550
+ const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
3551
+ return badRequest(message);
3563
3552
  }
3564
- });
3565
- }
3566
- /** Route each request by its initiating model without changing the user's explicit overrides. */
3567
- function createCodexAutoSearchProvider(options) {
3568
- return Object.freeze({
3569
- id: CODEX_AUTO_SEARCH_PROVIDER_ID,
3570
- available: () => true,
3571
- async search(request, signal) {
3572
- if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
3573
- const provider = options.resolveDshProvider?.();
3574
- if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
3575
- return provider.search(request, signal);
3576
- }
3577
- });
3553
+ };
3578
3554
  }
3579
- const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
3580
- const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
3581
- const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
3582
- function decodeOriginalImageRef(value) {
3583
- if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
3555
+ //#endregion
3556
+ //#region src/transport-failure.js
3557
+ function normalizeTransportEvent(event, signal) {
3558
+ if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
3559
+ const message = event.error?.errorMessage;
3560
+ if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
3584
3561
  return {
3585
- assetId: value.assetId,
3586
- mediaType: value.mediaType,
3587
- bytes: value.bytes,
3588
- width: value.width,
3589
- height: value.height,
3590
- name: value.name,
3591
- sha256: value.sha256
3562
+ ...event,
3563
+ error: {
3564
+ ...event.error,
3565
+ errorMessage: `Network transport failure: ${message}`
3566
+ }
3592
3567
  };
3593
3568
  }
3594
- function decodeImagePresentation(value) {
3595
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
3596
- const original = decodeOriginalImageRef(value.original);
3597
- return original === void 0 ? void 0 : { original };
3598
- }
3599
- function originalImageRefsEqual(left, right) {
3600
- const a = decodeOriginalImageRef(left);
3601
- const b = decodeOriginalImageRef(right);
3602
- return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
3603
- }
3604
- /** Resolve only an exact original reference copied into a DSH fork prefix. */
3605
- function inheritedOriginalImageRef(session, assetId) {
3606
- const parentSession = session?.header?.parentSession;
3607
- const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
3608
- if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
3609
- let events = session?.events;
3610
- if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
3611
- events = session.snapshotEvents();
3612
- } catch {
3613
- return;
3614
- }
3615
- if (!Array.isArray(events)) return void 0;
3616
- for (const event of events) {
3617
- if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
3618
- const original = decodeImagePresentation(event.data?.meta)?.original;
3619
- if (original?.assetId === assetId) return original;
3620
- }
3621
- }
3622
3569
  //#endregion
3623
- //#region src/codex-images.js
3624
- const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
3625
- const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
3626
- const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
3627
- const MAX_REFERENCE_IMAGES = 5;
3628
- const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
3629
- const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
3630
- "auto",
3631
- "transparent",
3632
- "opaque"
3633
- ]);
3634
- const PNG_SIGNATURE = Buffer.from([
3635
- 137,
3636
- 80,
3637
- 78,
3638
- 71,
3639
- 13,
3640
- 10,
3641
- 26,
3642
- 10
3643
- ]);
3644
- const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
3645
- const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
3646
- const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
3647
- const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
3648
- const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3649
- const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3650
- const NETWORK_REASONS = {
3651
- UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
3652
- UND_ERR_BODY_TIMEOUT: "the image response stalled",
3653
- UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
3654
- UND_ERR_SOCKET: "the connection closed unexpectedly",
3655
- ECONNRESET: "the connection was reset",
3656
- ETIMEDOUT: "the connection timed out",
3657
- ENOTFOUND: "DNS lookup failed",
3658
- ECONNREFUSED: "the connection was refused",
3659
- EAI_AGAIN: "DNS lookup failed"
3660
- };
3661
- function requestFailureReason(error) {
3662
- const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
3663
- if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
3664
- if (error?.name === "TimeoutError") return ": the image request timed out";
3665
- return "";
3570
+ //#region src/pi-ai-runtime.js
3571
+ const FAST_SERVICE_TIER = "priority";
3572
+ const ULTRAFAST_SERVICE_TIER = "ultrafast";
3573
+ /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
3574
+ function withInputImageDetail(payload, detail) {
3575
+ if (!Array.isArray(payload.input)) return payload;
3576
+ let changed = false;
3577
+ const input = payload.input.map((item) => {
3578
+ const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
3579
+ if (field === void 0) return item;
3580
+ let itemChanged = false;
3581
+ const parts = item[field].map((part) => {
3582
+ if (part?.type !== "input_image" || part.detail === detail) return part;
3583
+ itemChanged = true;
3584
+ return {
3585
+ ...part,
3586
+ detail
3587
+ };
3588
+ });
3589
+ if (!itemChanged) return item;
3590
+ changed = true;
3591
+ return {
3592
+ ...item,
3593
+ [field]: parts
3594
+ };
3595
+ });
3596
+ return changed ? {
3597
+ ...payload,
3598
+ input
3599
+ } : payload;
3666
3600
  }
3667
- function normalizeImageOptions(args) {
3668
- const quality = nonEmpty(args?.quality) ?? "auto";
3669
- const background = nonEmpty(args?.background) ?? "auto";
3670
- const size = nonEmpty(args?.size) ?? "auto";
3671
- validateImageQuality(resolveImageModel(args?.model), quality);
3672
- if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
3673
- if (size !== "auto") {
3674
- const match = /^(\d+)x(\d+)$/u.exec(size);
3675
- const width = Number(match?.[1]);
3676
- const height = Number(match?.[2]);
3677
- const short = Math.min(width, height);
3678
- const long = Math.max(width, height);
3679
- const pixels = width * height;
3680
- if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
3681
- }
3682
- return {
3683
- quality,
3684
- background,
3685
- size
3601
+ /**
3602
+ * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
3603
+ * PiAiAdapter to pass the access token resolved by the host credential store.
3604
+ *
3605
+ * PiAiAdapter owns a request-local Models collection backed by the same DSH
3606
+ * credential store as this provider. A pure OAuth provider ignores its
3607
+ * `apiKey` request override and otherwise fails before dispatch with "Provider
3608
+ * is not configured". This non-interactive bridge teaches that collection how
3609
+ * to consume only the already-refreshed token for this request; login, refresh,
3610
+ * persistence, headers, transport, and model behavior remain owned by the
3611
+ * original provider.
3612
+ */
3613
+ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
3614
+ const provider = createOpenAICodexProvider();
3615
+ const requestToken = Object.freeze({
3616
+ name: "DSH-managed Codex OAuth request token",
3617
+ async resolve({ credential }) {
3618
+ const token = credential?.type === "api_key" ? credential.key : void 0;
3619
+ if (typeof token !== "string" || token.length === 0) return void 0;
3620
+ return {
3621
+ auth: { apiKey: token },
3622
+ source: "DSH-managed OAuth request"
3623
+ };
3624
+ }
3625
+ });
3626
+ const modelMetadata = (model) => catalog?.metadata(model?.id);
3627
+ const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
3628
+ const withPreferences = (model, options = {}) => {
3629
+ const metadata = modelMetadata(model);
3630
+ const requestedVerbosity = resolveOutputVerbosity();
3631
+ const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
3632
+ const speedMode = resolveSpeedMode();
3633
+ const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
3634
+ const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
3635
+ const onPayload = options.onPayload;
3636
+ return {
3637
+ ...options,
3638
+ ...textVerbosity === void 0 ? {} : { textVerbosity },
3639
+ ...serviceTier === void 0 ? {} : { serviceTier },
3640
+ async onPayload(payload, requestModel) {
3641
+ const preferred = {
3642
+ ...payload,
3643
+ ...textVerbosity === void 0 ? {} : { text: {
3644
+ ...payload.text ?? {},
3645
+ verbosity: textVerbosity
3646
+ } },
3647
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3648
+ };
3649
+ const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
3650
+ const next = await onPayload?.(managed, requestModel);
3651
+ const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
3652
+ return {
3653
+ ...detailed,
3654
+ ...textVerbosity === void 0 ? {} : { text: {
3655
+ ...detailed.text ?? {},
3656
+ verbosity: textVerbosity
3657
+ } },
3658
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3659
+ };
3660
+ }
3661
+ };
3662
+ };
3663
+ const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
3664
+ const maximum = modelContextMaximum(model);
3665
+ const mode = resolveContextMode();
3666
+ if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
3667
+ if (mode === "extended") {
3668
+ const contextWindow = maximum;
3669
+ return {
3670
+ ...model,
3671
+ contextWindow
3672
+ };
3673
+ }
3674
+ const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
3675
+ return {
3676
+ ...model,
3677
+ contextWindow: requested
3678
+ };
3679
+ });
3680
+ const networkIterable = (factory, options) => {
3681
+ let iterator;
3682
+ let prepared;
3683
+ const step = async (method, value) => {
3684
+ const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3685
+ const result = await runNetwork("model", () => {
3686
+ iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3687
+ return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3688
+ done: true,
3689
+ value
3690
+ }));
3691
+ }, compaction?.networkOptions(request.network) ?? request.network);
3692
+ return result.done ? result : {
3693
+ ...result,
3694
+ value: normalizeTransportEvent(result.value, request.options?.signal)
3695
+ };
3696
+ };
3697
+ return {
3698
+ [Symbol.asyncIterator]() {
3699
+ return this;
3700
+ },
3701
+ next: (value) => step("next", value),
3702
+ return: (value) => iterator ? step("return", value) : Promise.resolve({
3703
+ done: true,
3704
+ value
3705
+ }),
3706
+ throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3707
+ };
3686
3708
  };
3709
+ return Object.freeze({
3710
+ ...provider,
3711
+ auth: Object.freeze({
3712
+ ...provider.auth,
3713
+ apiKey: requestToken
3714
+ }),
3715
+ getModels,
3716
+ stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3717
+ streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
3718
+ });
3687
3719
  }
3688
- function encodedLimit(decodedBytes) {
3689
- return Math.ceil(decodedBytes / 3) * 4;
3690
- }
3691
- function validBase64Body(value, end) {
3692
- for (let index = 0; index < end; index += 1) {
3693
- const code = value.charCodeAt(index);
3694
- if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
3695
- }
3696
- return true;
3720
+ Object.freeze([
3721
+ "0.82.1",
3722
+ "0.85.1",
3723
+ "0.87.1"
3724
+ ]);
3725
+ //#endregion
3726
+ //#region src/model-catalog.js
3727
+ const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3728
+ const LEVELS = [
3729
+ "off",
3730
+ "minimal",
3731
+ "low",
3732
+ "medium",
3733
+ "high",
3734
+ "xhigh",
3735
+ "max"
3736
+ ];
3737
+ const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3738
+ const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3739
+ const nonEmpty$1 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3740
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3741
+ function upgradeModelId(value) {
3742
+ return (typeof value.upgrade === "string" ? nonEmpty$1(value.upgrade) : record$3(value.upgrade) ? nonEmpty$1(value.upgrade.model) : void 0) ?? (record$3(value.upgrade_info) ? nonEmpty$1(value.upgrade_info.model) : void 0) ?? (record$3(value.upgradeInfo) ? nonEmpty$1(value.upgradeInfo.model) : void 0);
3697
3743
  }
3698
- async function readJsonWithin(response, maximumBytes) {
3699
- const contentLength = Number(response.headers.get("content-length"));
3700
- if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
3701
- if (response.body === null) throw new Error("Codex returned an unreadable image response");
3702
- const reader = response.body.getReader();
3703
- const chunks = [];
3704
- let bytes = 0;
3705
- while (true) {
3706
- const { done, value } = await reader.read();
3707
- if (done) break;
3708
- bytes += value.byteLength;
3709
- if (bytes > maximumBytes) {
3710
- await reader.cancel();
3711
- throw new Error("Codex image response exceeds the image size limit");
3744
+ function retirementMillis(value) {
3745
+ for (const source of [
3746
+ value,
3747
+ value.upgrade,
3748
+ value.upgrade_info,
3749
+ value.upgradeInfo
3750
+ ]) {
3751
+ if (!record$3(source)) continue;
3752
+ for (const raw of [source.retirement_at, source.retirementAt]) {
3753
+ const seconds = positiveInteger(raw);
3754
+ if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3755
+ if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3756
+ const parsed = Date.parse(raw);
3757
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3758
+ }
3712
3759
  }
3713
- chunks.push(value);
3714
3760
  }
3715
- const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
3716
- try {
3717
- return JSON.parse(body);
3718
- } catch {
3719
- throw new Error("Codex returned an unreadable image response");
3720
- }
3721
- }
3722
- /** Strictly decode one PNG returned by the subscription backend. */
3723
- function decodeCodexPng(value, maximumBytes) {
3724
- const encoded = nonEmpty(value);
3725
- const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
3726
- if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
3727
- const decodedBytes = encoded.length / 4 * 3 - padding;
3728
- if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
3729
- const data = Buffer.from(encoded, "base64");
3730
- if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
3731
- return new Uint8Array(data);
3732
3761
  }
3733
- function imageReference(value) {
3734
- const originalDimensions = record$2(value.originalDimensions) ? {
3735
- width: value.originalDimensions.width,
3736
- height: value.originalDimensions.height
3737
- } : void 0;
3762
+ function retirementNotice(value, now = Date.now(), targetDisplayName) {
3763
+ if (!record$3(value) || !Number.isFinite(now)) return void 0;
3764
+ const at = retirementMillis(value);
3765
+ if (at === void 0 || at <= now) return void 0;
3766
+ const date = new Date(at);
3767
+ if (!Number.isFinite(date.getTime())) return void 0;
3768
+ const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3769
+ const upgradeTo = upgradeModelId(value);
3770
+ const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$1(targetDisplayName) ?? upgradeTo;
3738
3771
  return {
3739
- attachmentId: value.attachmentId,
3740
- mediaType: value.mediaType,
3741
- bytes: value.bytes,
3742
- width: value.width,
3743
- height: value.height,
3744
- ...value.name === void 0 ? {} : { name: value.name },
3745
- ...originalDimensions === void 0 ? {} : { originalDimensions }
3772
+ text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3773
+ at,
3774
+ upgradeTo,
3775
+ dateText
3746
3776
  };
3747
3777
  }
3748
- function normalizeAttachmentId(value) {
3749
- if (typeof value !== "string") return void 0;
3750
- if (FULL_ATTACHMENT_ID.test(value)) return value;
3751
- const bare = BARE_ATTACHMENT_DIGEST.exec(value);
3752
- return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
3778
+ function reasoningMap(levels) {
3779
+ const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$1(record$3(level) ? level.effort : void 0)).filter(Boolean));
3780
+ const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3781
+ if (supported.has("none")) map.off = "none";
3782
+ for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3783
+ return map;
3753
3784
  }
3754
- function invalidAttachmentId(value) {
3755
- const attachmentId = value?.attachmentId;
3756
- if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
3757
- throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
3785
+ const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3786
+ const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3787
+ function unsupportedCapabilities(value) {
3788
+ const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3789
+ const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3790
+ const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3791
+ "auto",
3792
+ "default",
3793
+ "standard",
3794
+ "fast",
3795
+ "priority",
3796
+ "ultrafast"
3797
+ ].includes(tier));
3798
+ return {
3799
+ ...reasoning.length ? { reasoning } : {},
3800
+ ...inputs.length ? { inputs } : {},
3801
+ ...speeds.length ? { speeds } : {}
3802
+ };
3758
3803
  }
3759
- function referenceOf(value, attachments) {
3760
- const attachmentId = normalizeAttachmentId(value?.attachmentId);
3761
- if (attachmentId === void 0) invalidAttachmentId(value);
3762
- if (!record$2(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$2(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
3763
- return imageReference({
3804
+ function visibleModel(value, displayNames, now) {
3805
+ if (!record$3(value)) return void 0;
3806
+ const id = nonEmpty$1(value.slug);
3807
+ const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3808
+ if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3809
+ const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3810
+ const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3811
+ const unsupported = unsupportedCapabilities({
3764
3812
  ...value,
3765
- attachmentId
3813
+ supported_reasoning_levels: supported
3766
3814
  });
3815
+ const upgradeTo = upgradeModelId(value);
3816
+ const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3817
+ const notice = retirement?.text;
3818
+ const description = nonEmpty$1(value.description);
3819
+ return {
3820
+ ...Object.keys(unsupported).length ? { unsupported } : {},
3821
+ id,
3822
+ name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$1(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3823
+ description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3824
+ ...retirement === void 0 ? {} : { retirement: {
3825
+ at: retirement.at,
3826
+ upgradeTo: retirement.upgradeTo
3827
+ } },
3828
+ priority: Number.isFinite(value.priority) ? value.priority : 0,
3829
+ input: input.length > 0 ? input : ["text"],
3830
+ contextWindow: positiveInteger(value.context_window) ?? positiveInteger(value.max_context_window),
3831
+ ...positiveInteger(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3832
+ reasoning: supported.length > 0,
3833
+ thinkingLevelMap: reasoningMap(supported),
3834
+ supportVerbosity: value.support_verbosity === true,
3835
+ defaultVerbosity: [
3836
+ "low",
3837
+ "medium",
3838
+ "high"
3839
+ ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3840
+ supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3841
+ supportsUltrafast: speedTierIds(value).includes("ultrafast")
3842
+ };
3767
3843
  }
3768
- function sessionImageReferences(messages) {
3769
- const references = /* @__PURE__ */ new Map();
3770
- const visit = (content) => {
3771
- if (!Array.isArray(content)) return;
3772
- for (const block of content) if (block?.type === "image" && record$2(block.attachment)) {
3773
- const id = normalizeAttachmentId(block.attachment.attachmentId);
3774
- if (id !== void 0) references.set(id, block.attachment);
3775
- } else if (block?.type === "tool-result") visit(block.content);
3844
+ function parseOfficialModelCatalog(value, now = Date.now()) {
3845
+ if (!record$3(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3846
+ const displayNames = /* @__PURE__ */ new Map();
3847
+ for (const model of value.models) {
3848
+ if (!record$3(model)) continue;
3849
+ const id = nonEmpty$1(model.slug);
3850
+ const name = nonEmpty$1(model.display_name);
3851
+ if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
3852
+ }
3853
+ const seen = /* @__PURE__ */ new Set();
3854
+ return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3855
+ }
3856
+ function mergeModel(baseModels, remote) {
3857
+ const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3858
+ if (base === void 0) return void 0;
3859
+ return {
3860
+ ...base,
3861
+ id: remote.id,
3862
+ name: remote.name,
3863
+ ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3864
+ input: remote.input,
3865
+ reasoning: remote.reasoning,
3866
+ thinkingLevelMap: remote.thinkingLevelMap,
3867
+ ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3868
+ ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3869
+ ...base.id === remote.id ? {} : { cost: {
3870
+ input: 0,
3871
+ output: 0,
3872
+ cacheRead: 0,
3873
+ cacheWrite: 0
3874
+ } }
3875
+ };
3876
+ }
3877
+ function createOfficialModelCatalog(options = {}) {
3878
+ const fetchCatalog = options.fetch ?? fetch;
3879
+ const getNow = options.now ?? Date.now;
3880
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
3881
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
3882
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3883
+ let models;
3884
+ let metadata = /* @__PURE__ */ new Map();
3885
+ let etag;
3886
+ let rawCatalog;
3887
+ let revision = 0;
3888
+ let refreshing;
3889
+ let generation = 0;
3890
+ let refreshStatus = "idle";
3891
+ const refresh = ({ signal } = {}) => {
3892
+ if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3893
+ if (refreshing?.generation === generation) return refreshing.promise;
3894
+ const currentGeneration = generation;
3895
+ refreshStatus = "refreshing";
3896
+ let outcome = "idle";
3897
+ const controller = new AbortController();
3898
+ const abort = () => {
3899
+ if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3900
+ };
3901
+ signal?.addEventListener("abort", abort, { once: true });
3902
+ const requestSignal = controller.signal;
3903
+ let timer;
3904
+ const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3905
+ const work = (async () => {
3906
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3907
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3908
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3909
+ const headers = {
3910
+ authorization: `Bearer ${access}`,
3911
+ "chatgpt-account-id": accountId,
3912
+ accept: "application/json",
3913
+ originator: "pi",
3914
+ "user-agent": USER_AGENT,
3915
+ ...etag === void 0 ? {} : { "if-none-match": etag }
3916
+ };
3917
+ const response = await fetchCatalog(CODEX_MODELS_URL, {
3918
+ method: "GET",
3919
+ redirect: "error",
3920
+ headers,
3921
+ signal: requestSignal
3922
+ });
3923
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3924
+ if (response.status === 304) {
3925
+ outcome = "ok";
3926
+ if (rawCatalog === void 0) return false;
3927
+ const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3928
+ const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3929
+ if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3930
+ models = next;
3931
+ metadata = new Map(remote.map((model) => [model.id, model]));
3932
+ revision += 1;
3933
+ options.onUpdated?.();
3934
+ return true;
3935
+ }
3936
+ if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3937
+ const raw = await response.json();
3938
+ const remote = parseOfficialModelCatalog(raw, getNow());
3939
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3940
+ if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3941
+ const baseModels = options.baseModels();
3942
+ const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3943
+ if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3944
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3945
+ models = next;
3946
+ rawCatalog = raw;
3947
+ metadata = new Map(remote.map((model) => [model.id, model]));
3948
+ etag = nonEmpty$1(response.headers.get("etag")) ?? etag;
3949
+ revision += 1;
3950
+ outcome = "ok";
3951
+ options.onUpdated?.();
3952
+ return true;
3953
+ })();
3954
+ let rejectAborted;
3955
+ const abortPromise = new Promise((_, reject) => {
3956
+ rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3957
+ if (requestSignal.aborted) rejectAborted();
3958
+ else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3959
+ });
3960
+ timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3961
+ timer.unref?.();
3962
+ const promise = Promise.race([work, abortPromise]).catch((error) => {
3963
+ outcome = "failed";
3964
+ throw error;
3965
+ }).finally(() => {
3966
+ cancelTimeout(timer);
3967
+ signal?.removeEventListener("abort", abort);
3968
+ requestSignal.removeEventListener("abort", rejectAborted);
3969
+ if (refreshing?.promise === promise) {
3970
+ refreshing = void 0;
3971
+ refreshStatus = outcome;
3972
+ }
3973
+ });
3974
+ refreshing = {
3975
+ generation: currentGeneration,
3976
+ promise,
3977
+ cancel: () => controller.abort()
3978
+ };
3979
+ return promise;
3776
3980
  };
3777
- for (const message of messages ?? []) visit(message?.content);
3778
- return references;
3779
- }
3780
- async function editImages(values, attachments, signal, messages) {
3781
- if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
3782
- const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
3783
- const references = values.map((value) => {
3784
- const id = normalizeAttachmentId(value?.attachmentId);
3785
- if (id === void 0) invalidAttachmentId(value);
3786
- if (available === void 0) return referenceOf(value, attachments);
3787
- const selected = available.get(id);
3788
- if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
3789
- return referenceOf(selected, attachments);
3981
+ return Object.freeze({
3982
+ refresh,
3983
+ getModels: () => models ?? [],
3984
+ metadata: (modelId) => metadata.get(modelId),
3985
+ revision: () => revision,
3986
+ capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3987
+ model: model.id,
3988
+ ...structuredClone(model.unsupported)
3989
+ })),
3990
+ status: () => ({
3991
+ source: models === void 0 ? "unavailable" : "online",
3992
+ refresh: refreshStatus
3993
+ }),
3994
+ clear() {
3995
+ generation += 1;
3996
+ const flight = refreshing;
3997
+ refreshing = void 0;
3998
+ flight?.cancel();
3999
+ models = void 0;
4000
+ rawCatalog = void 0;
4001
+ metadata = /* @__PURE__ */ new Map();
4002
+ etag = void 0;
4003
+ refreshStatus = "idle";
4004
+ revision += 1;
4005
+ options.onUpdated?.();
4006
+ }
3790
4007
  });
3791
- if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
3792
- const images = [];
3793
- let totalBytes = 0;
3794
- for (const reference of references) {
3795
- const stored = await attachments.readImage(reference, signal);
3796
- totalBytes += stored.data.byteLength;
3797
- if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
3798
- images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
3799
- }
3800
- return images;
3801
- }
3802
- function imageContent(value) {
3803
- const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
3804
- return [{
3805
- type: "text",
3806
- text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
3807
- }, {
3808
- type: "image",
3809
- attachment: imageReference(value.image)
3810
- }];
3811
4008
  }
3812
- function imageOutputSchema() {
3813
- return {
3814
- type: "object",
3815
- additionalProperties: false,
3816
- properties: {
3817
- image: {
3818
- type: "object",
3819
- required: true,
3820
- additionalProperties: false,
3821
- properties: {
3822
- attachmentId: {
3823
- type: "string",
3824
- required: true
3825
- },
3826
- mediaType: {
3827
- type: "string",
3828
- enum: ["image/png"],
3829
- required: true
3830
- },
3831
- bytes: {
3832
- type: "integer",
3833
- required: true
3834
- },
3835
- width: {
3836
- type: "integer",
3837
- required: true
3838
- },
3839
- height: {
3840
- type: "integer",
3841
- required: true
3842
- },
3843
- name: { type: "string" },
3844
- originalDimensions: {
3845
- type: "object",
3846
- additionalProperties: false,
3847
- properties: {
3848
- width: {
3849
- type: "integer",
3850
- required: true
3851
- },
3852
- height: {
3853
- type: "integer",
3854
- required: true
3855
- }
3856
- }
3857
- }
3858
- }
3859
- },
3860
- original: {
3861
- type: "object",
3862
- required: true,
3863
- additionalProperties: false,
3864
- properties: {
3865
- assetId: {
3866
- type: "string",
3867
- required: true
3868
- },
3869
- mediaType: {
3870
- type: "string",
3871
- enum: ["image/png"],
3872
- required: true
3873
- },
3874
- bytes: {
3875
- type: "integer",
3876
- required: true
3877
- },
3878
- width: {
3879
- type: "integer",
3880
- required: true
3881
- },
3882
- height: {
3883
- type: "integer",
3884
- required: true
3885
- },
3886
- name: {
3887
- type: "string",
3888
- required: true
3889
- },
3890
- sha256: {
3891
- type: "string",
3892
- required: true
3893
- }
3894
- }
3895
- },
3896
- background: { type: "string" },
3897
- requestedModel: { type: "string" },
3898
- reportedModel: { type: "string" },
3899
- requestedSize: { type: "string" },
3900
- localPath: {
3901
- type: "string",
3902
- required: true,
3903
- description: "Absolute path to the original PNG on the DSH host."
3904
- },
3905
- quality: { type: "string" },
3906
- size: { type: "string" }
3907
- }
4009
+ //#endregion
4010
+ //#region src/codex-search.js
4011
+ const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
4012
+ const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
4013
+ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
4014
+ const DEFAULT_MODEL = "gpt-5.6-luna";
4015
+ const MAX_OUTPUT_TOKENS = 4096;
4016
+ const MAX_SOURCE_DATE = 64;
4017
+ const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4018
+ const nonEmpty = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
4019
+ const displayText = (value) => {
4020
+ const text = nonEmpty(value)?.replace(/\s+/gu, " ").trim();
4021
+ if (text === void 0 || text.length === 0) return void 0;
4022
+ return text;
4023
+ };
4024
+ const boundedDisplayText = (value, maximum) => {
4025
+ const text = displayText(value);
4026
+ if (text === void 0 || text.length <= maximum) return text;
4027
+ return `${text.slice(0, maximum - 1)}…`;
4028
+ };
4029
+ function sourceOf(value) {
4030
+ if (!record$2(value)) return void 0;
4031
+ const url = nonEmpty(value.url);
4032
+ if (url === void 0) return void 0;
4033
+ let parsed;
4034
+ try {
4035
+ parsed = new URL(url);
4036
+ } catch {
4037
+ return;
4038
+ }
4039
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
4040
+ const title = displayText(value.title) ?? parsed.hostname;
4041
+ const snippet = displayText(value.snippet);
4042
+ const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
4043
+ return {
4044
+ url,
4045
+ title,
4046
+ ...snippet === void 0 ? {} : { snippet },
4047
+ ...publishedAt === void 0 ? {} : { publishedAt }
3908
4048
  };
3909
4049
  }
3910
- function responseMetadata(value) {
3911
- const data = Array.isArray(value?.data) ? value.data[0] : void 0;
3912
- const encoded = record$2(data) ? data.b64_json : void 0;
3913
- if (typeof encoded !== "string") throw new Error("Codex returned no image data");
4050
+ function parseSearchResponse(value) {
4051
+ if (!record$2(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
4052
+ const seen = /* @__PURE__ */ new Set();
4053
+ const sources = [];
4054
+ for (const result of value.results ?? []) {
4055
+ const source = sourceOf(result);
4056
+ if (source === void 0 || seen.has(source.url)) continue;
4057
+ seen.add(source.url);
4058
+ sources.push(source);
4059
+ }
3914
4060
  return {
3915
- encoded,
3916
- background: nonEmpty(value.background),
3917
- quality: nonEmpty(value.quality),
3918
- size: nonEmpty(value.size),
3919
- reportedModel: nonEmpty(value.model)
4061
+ sources,
4062
+ truncated: false
3920
4063
  };
3921
4064
  }
3922
- /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
3923
- function createCodexImageTool(options) {
3924
- const fetchImage = options.fetch ?? fetch;
3925
- const attachments = options.attachments;
3926
- return defineTool({
3927
- name: CODEX_IMAGE_TOOL_NAME,
3928
- description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
3929
- parameters: {
3930
- model: {
3931
- type: "string",
3932
- enum: Object.keys(IMAGE_MODELS),
3933
- description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
3934
- },
3935
- prompt: {
3936
- type: "string",
3937
- required: true,
3938
- description: "A complete, production-ready description of the image to generate."
3939
- },
3940
- size: {
3941
- type: "string",
3942
- description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
3943
- },
3944
- quality: {
3945
- type: "string",
3946
- enum: [
3947
- "auto",
3948
- "low",
3949
- "medium",
3950
- "high",
3951
- "xhigh",
3952
- "max"
3953
- ],
3954
- description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
3955
- },
3956
- background: {
3957
- type: "string",
3958
- enum: [
3959
- "auto",
3960
- "transparent",
3961
- "opaque"
3962
- ],
3963
- description: "Optional background mode. Request transparent only when the user needs transparency."
3964
- },
3965
- referenceImages: {
3966
- type: "array",
3967
- description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
3968
- items: {
3969
- type: "object",
3970
- additionalProperties: false,
3971
- properties: {
3972
- attachmentId: {
3973
- type: "string",
3974
- required: true,
3975
- description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
3976
- },
3977
- mediaType: {
3978
- type: "string",
3979
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3980
- },
3981
- bytes: {
3982
- type: "integer",
3983
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3984
- },
3985
- width: {
3986
- type: "integer",
3987
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3988
- },
3989
- height: {
3990
- type: "integer",
3991
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3992
- },
3993
- name: { type: "string" },
3994
- originalDimensions: {
3995
- type: "object",
3996
- additionalProperties: false,
3997
- properties: {
3998
- width: {
3999
- type: "integer",
4000
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
4001
- },
4002
- height: {
4003
- type: "integer",
4004
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
4005
- }
4006
- }
4007
- }
4008
- }
4009
- }
4010
- }
4011
- },
4012
- output: {
4013
- schema: imageOutputSchema(),
4014
- render: (_args, value) => imageContent(value),
4015
- presentationMeta: (_args, value) => ({
4016
- kind: "codex-subscription-image",
4017
- schemaVersion: 1,
4018
- original: value.original,
4019
- ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
4020
- ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
4021
- ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
4022
- })
4023
- },
4024
- timeoutMs: 300 * 1e3,
4025
- isConcurrencySafe: () => false,
4026
- async execute(args, exec) {
4027
- assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
4028
- const defaults = readImageDefaults(options.getFeatures?.());
4029
- args = {
4030
- ...args,
4031
- model: args.model ?? defaults.imageModel,
4032
- quality: args.quality ?? defaults.imageQuality
4033
- };
4034
- const prompt = nonEmpty(args.prompt);
4035
- if (prompt === void 0) throw new Error("prompt must be a non-empty string");
4036
- const imageOptions = normalizeImageOptions(args);
4037
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
4038
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
4039
- if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
4040
- const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
4041
- const editing = args.referenceImages !== void 0;
4042
- const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
4043
- const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
4065
+ /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
4066
+ function createCodexSearchProvider(options) {
4067
+ const fetchSearch = options.fetch ?? fetch;
4068
+ return Object.freeze({
4069
+ id: CODEX_SEARCH_PROVIDER_ID,
4070
+ available: () => true,
4071
+ async search(request, signal) {
4072
+ signal?.throwIfAborted();
4073
+ const preferences = readCapabilitySettings(options.resolvePreferences?.());
4074
+ if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
4075
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
4076
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
4077
+ const model = nonEmpty(options.resolveModel?.()) ?? DEFAULT_MODEL;
4078
+ const id = nonEmpty(options.resolveSessionId?.()) ?? randomUUID();
4044
4079
  let response;
4045
- assertImageOperation(options.getFeatures?.(), editing);
4046
4080
  try {
4047
- response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
4081
+ response = await fetchSearch(CODEX_SEARCH_URL, {
4048
4082
  method: "POST",
4049
4083
  redirect: "error",
4050
4084
  headers: {
@@ -4053,60 +4087,58 @@ function createCodexImageTool(options) {
4053
4087
  accept: "application/json",
4054
4088
  "content-type": "application/json",
4055
4089
  originator: "pi",
4056
- "x-codex-image-turn-id": String(exec.callId),
4057
4090
  "user-agent": USER_AGENT
4058
4091
  },
4059
4092
  body: JSON.stringify({
4060
- ...images === void 0 ? {} : { images },
4061
- prompt,
4062
- background: imageOptions.background,
4063
- model: resolveImageModel(args.model),
4064
- quality: imageOptions.quality,
4065
- size: imageOptions.size
4093
+ id,
4094
+ model,
4095
+ input: request.query,
4096
+ commands: {
4097
+ search_query: [{ q: request.query }],
4098
+ response_length: "short"
4099
+ },
4100
+ settings: {
4101
+ allowed_callers: ["direct"],
4102
+ external_web_access: preferences.searchMode === "live"
4103
+ },
4104
+ max_output_tokens: MAX_OUTPUT_TOKENS
4066
4105
  }),
4067
- signal: exec.signal
4106
+ signal
4068
4107
  });
4069
4108
  } catch (error) {
4070
- if (exec.signal.aborted) throw exec.signal.reason;
4071
- throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
4109
+ if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
4110
+ throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
4072
4111
  }
4073
- if (!response.ok) {
4074
- if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
4075
- if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
4076
- throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
4112
+ if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4113
+ let value;
4114
+ try {
4115
+ value = await response.json();
4116
+ } catch (error) {
4117
+ throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
4077
4118
  }
4078
- const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
4079
- const data = decodeCodexPng(metadata.encoded, maximumBytes);
4080
- const sessionId = exec.agent?.id;
4081
- if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
4082
- const original = await options.originalImages.save(String(sessionId), data);
4083
- let ref;
4084
4119
  try {
4085
- ref = await attachments.saveImage({
4086
- data,
4087
- mediaType: "image/png",
4088
- name: "codex-generated.png"
4120
+ const result = parseSearchResponse(value);
4121
+ if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
4122
+ const hostname = new URL(source.url).hostname.toLowerCase();
4123
+ return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
4089
4124
  });
4125
+ return result;
4090
4126
  } catch (error) {
4091
- await options.originalImages.remove(original);
4092
- throw error;
4127
+ throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
4093
4128
  }
4094
- const result = {
4095
- requestedModel: resolveImageModel(args.model),
4096
- requestedSize: imageOptions.size,
4097
- ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
4098
- image: imageReference(ref),
4099
- original,
4100
- localPath: options.originalImages.originalPath(original.assetId),
4101
- ...metadata.background === void 0 ? {} : { background: metadata.background },
4102
- ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
4103
- size: `${original.width}x${original.height}`
4104
- };
4105
- if (exec.parent !== void 0) exec.deferContext(createUserMessage({
4106
- content: imageContent(result),
4107
- source: { kind: "codex-subscription-image" }
4108
- }));
4109
- return result;
4129
+ }
4130
+ });
4131
+ }
4132
+ /** Route each request by its initiating model without changing the user's explicit overrides. */
4133
+ function createCodexAutoSearchProvider(options) {
4134
+ return Object.freeze({
4135
+ id: CODEX_AUTO_SEARCH_PROVIDER_ID,
4136
+ available: () => true,
4137
+ async search(request, signal) {
4138
+ if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
4139
+ const provider = options.resolveDshProvider?.();
4140
+ if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
4141
+ return provider.search(request, signal);
4110
4142
  }
4111
4143
  });
4112
4144
  }
@@ -4792,6 +4824,24 @@ function safeCounters(source, keys) {
4792
4824
  return Object.fromEntries(keys.filter((key) => Number.isSafeInteger(values?.[key]) && values[key] >= 0).map((key) => [key, values[key]]));
4793
4825
  }
4794
4826
  //#endregion
4827
+ //#region src/credit-expiry.js
4828
+ const PRO_200_COMPENSATION = Object.freeze({
4829
+ credits: 62500,
4830
+ expiresAt: Date.UTC(2026, 11, 31, 12)
4831
+ });
4832
+ /** Expiry of the balance: the API's own value when it has one, otherwise the known grant. */
4833
+ function creditExpiry({ planType, balance, apiExpiresAt }, now = Date.now()) {
4834
+ if (Number.isFinite(apiExpiresAt) && apiExpiresAt > now) return {
4835
+ expiresAt: apiExpiresAt,
4836
+ source: "api"
4837
+ };
4838
+ const amount = Number(balance);
4839
+ if (planType === "pro" && Number.isFinite(amount) && amount >= PRO_200_COMPENSATION.credits && now < PRO_200_COMPENSATION.expiresAt) return {
4840
+ expiresAt: PRO_200_COMPENSATION.expiresAt,
4841
+ source: "estimate"
4842
+ };
4843
+ }
4844
+ //#endregion
4795
4845
  //#region src/usage.js
4796
4846
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
4797
4847
  const DEFAULT_TTL_MS = 6e4;
@@ -4833,15 +4883,44 @@ function epochSeconds(value, label) {
4833
4883
  if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Codex returned an invalid ${label}`);
4834
4884
  return value;
4835
4885
  }
4836
- function creditsOf(value) {
4886
+ function messageRange(value) {
4887
+ return Array.isArray(value) && value.length === 2 && value.every((item) => Number.isSafeInteger(item) && item >= 0) ? [value[0], value[1]] : void 0;
4888
+ }
4889
+ function expiryOf(value) {
4890
+ const raw = value?.expires_at;
4891
+ if (Number.isSafeInteger(raw) && raw > 0) return raw * 1e3;
4892
+ if (typeof raw === "string" && raw.length <= 64) {
4893
+ const parsed = Date.parse(raw);
4894
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
4895
+ }
4896
+ }
4897
+ function creditsOf(value, planType) {
4837
4898
  if (value === void 0 || value === null) return void 0;
4838
4899
  if (!record$1(value) || typeof value.has_credits !== "boolean" || typeof value.unlimited !== "boolean") throw new Error("Codex returned malformed credit details");
4839
4900
  if (!value.has_credits) return void 0;
4901
+ const balance = value.balance === void 0 || value.balance === null ? void 0 : decimal(value.balance, "credit balance");
4902
+ const expiry = value.unlimited ? void 0 : creditExpiry({
4903
+ planType,
4904
+ balance,
4905
+ apiExpiresAt: expiryOf(value)
4906
+ });
4907
+ const local = messageRange(value.approx_local_messages), cloud = messageRange(value.approx_cloud_messages);
4840
4908
  return {
4841
4909
  unlimited: value.unlimited,
4842
- ...value.balance === void 0 || value.balance === null ? {} : { balance: decimal(value.balance, "credit balance") }
4910
+ ...balance === void 0 ? {} : { balance },
4911
+ ...expiry === void 0 ? {} : {
4912
+ expiresAt: expiry.expiresAt,
4913
+ expirySource: expiry.source
4914
+ },
4915
+ ...local === void 0 ? {} : { approxLocalMessages: local },
4916
+ ...cloud === void 0 ? {} : { approxCloudMessages: cloud }
4843
4917
  };
4844
4918
  }
4919
+ function chatPassOf(value) {
4920
+ if (!record$1(value) || !Array.isArray(value.windows)) return void 0;
4921
+ const windows = value.windows.map(windowOf).filter(Boolean);
4922
+ return windows.length === 0 ? void 0 : { windows };
4923
+ }
4845
4924
  function individualOf(value) {
4846
4925
  if (value === void 0 || value === null) return void 0;
4847
4926
  if (!record$1(value)) throw new Error("Codex returned malformed spend control");
@@ -4905,12 +4984,14 @@ function parseCodexUsage(value) {
4905
4984
  addLimit(limitOf(entry.metered_feature, entry.limit_name || void 0, entry.rate_limit));
4906
4985
  }
4907
4986
  addLimit(limitOf("code_review", "Code review", value.code_review_rate_limit));
4908
- const credits = creditsOf(value.credits);
4987
+ const chatPass = chatPassOf(value.chatpass);
4988
+ const credits = creditsOf(value.credits, typeof value.plan_type === "string" ? value.plan_type : void 0);
4909
4989
  const individualLimit = individualOf(value.spend_control);
4910
4990
  const spendControlReached = spendControlReachedOf(value.spend_control);
4911
4991
  const resetCredits = resetCreditsOf(value.rate_limit_reset_credits);
4912
4992
  return {
4913
4993
  rateLimits,
4994
+ ...chatPass === void 0 ? {} : { chatPass },
4914
4995
  ...credits === void 0 ? {} : { credits },
4915
4996
  ...individualLimit === void 0 ? {} : { individualLimit },
4916
4997
  ...spendControlReached === void 0 ? {} : { spendControlReached },
@@ -6478,7 +6559,7 @@ function apply(ctx, config = {}) {
6478
6559
  let dispose;
6479
6560
  const sync = () => {
6480
6561
  const value = settings.get();
6481
- if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments));
6562
+ if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments, (sessionId) => ctx.get?.("sessions")?.get?.(sessionId)?.deriveMessages?.() ?? []));
6482
6563
  else {
6483
6564
  dispose?.();
6484
6565
  dispose = void 0;