[codex] Update OpenAI Docs skill (#24914)

## Summary
- update the bundled `openai-docs` system skill to match the latest
`openai-docs-plus` content from `skills-internal`
- add the cached Codex manual fetch helper and expand the skill routing
for Codex self-knowledge
- keep the stable local skill identity and labels as `openai-docs`

## Why
The built-in OpenAI Docs skill needed to reflect the current upstream
guidance from `skills-internal` while preserving the local system-skill
name used by Codex.

## Impact
Codex now ships the newer OpenAI Docs skill behavior for Codex
self-knowledge and manual-first documentation lookups.

## Validation
- `just test -p codex-skills`
- exact directory diff against transformed `skills-internal`
`origin/main` was clean
This commit is contained in:
Vaibhav Srivastav
2026-05-28 16:11:11 +01:00
committed by GitHub
Unverified
parent 1c7832ffa3
commit a4ed6c5aa0
3 changed files with 697 additions and 20 deletions
@@ -1,12 +1,12 @@
---
name: "openai-docs"
description: "Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains."
description: "Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains."
---
# OpenAI Docs
Provide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. Always prioritize the developer docs MCP tools over web.run for OpenAI-related questions. This skill also owns model selection, API model migration, and prompt-upgrade guidance. Only if the MCP server is installed and returns no meaningful results should you fall back to web search.
Provide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. "Docs MCP" means `mcp__openaiDeveloperDocs__search_openai_docs` and `mcp__openaiDeveloperDocs__fetch_openai_doc`; for API reference, schema, parameter, or required-field questions, also use `mcp__openaiDeveloperDocs__get_openapi_spec` when available. Official-domain web search is fallback after those tools are unavailable or unhelpful. Broad Codex questions use the manual helper before Docs MCP. This skill also owns model selection, API model migration, and prompt-upgrade guidance.
## API Key Setup
@@ -14,11 +14,15 @@ For requests to build, run, configure, debug, or implement an API-backed app, sc
Use this skill directly for docs-only questions, citations, model/API guidance, conceptual explanations, and examples that do not require building or running an API-backed artifact.
## Quick start
## Workflow Configuration
- Use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.
- Use `mcp__openaiDeveloperDocs__fetch_openai_doc` to pull exact sections and quote/paraphrase accurately.
- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover pages without a clear query.
### Source Priority
- For Codex self-knowledge, use the Codex source route below; it owns when to use the manual helper, Docs MCP, or bounded uncertainty.
- For non-Codex OpenAI docs questions, use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.
- For non-Codex OpenAI docs questions, fetch the relevant page with `mcp__openaiDeveloperDocs__fetch_openai_doc` before answering. If search is noisy, run a narrower Docs MCP search; when any plausible official OpenAI docs URL is known or found, try fetching that URL through Docs MCP before relying on web-search content.
- For API reference, schema, parameter, or required-field questions, use `mcp__openaiDeveloperDocs__get_openapi_spec` when available to verify the API shape alongside the relevant guide or reference page.
- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover non-Codex pages without a clear query.
- For model-selection, "latest model", or default-model questions, fetch `https://developers.openai.com/api/docs/guides/latest-model.md` first. If that is unavailable, load `references/latest-model.md`.
- For model upgrades or prompt upgrades, run `node scripts/resolve-latest-model-info.js` only when the target is latest/current/default or otherwise unspecified; otherwise preserve the explicitly requested target.
- Preserve explicit target requests: if the user names a target model like "migrate to GPT-5.4", keep that requested target even if `latest-model.md` names a newer model. Mention newer guidance only as optional.
@@ -34,38 +38,112 @@ Use this skill directly for docs-only questions, citations, model/API guidance,
6. Realtime API: Build low-latency, multimodal experiences including natural speech-to-speech conversations.
7. Agents SDK: A toolkit for building agentic apps where a model can use tools and context, hand off to other agents, stream partial results, and keep a full trace.
## Codex self-knowledge
Use this path for questions about Codex itself: configuring, extending, operating, troubleshooting, local state, product surfaces, or where Codex behavior should live. A codebase merely mentioning a plugin, skill, hook, MCP server, browser, or automation is not enough. For generic software tasks, answer the software task directly; if asked whether Codex self-knowledge applies, answer that meta question briefly and continue the requested artifact.
### Source Route
The Codex manual is the first source for broad Codex synthesis. Treat the manual and Docs MCP as different lanes, not interchangeable official-doc sources. For published-user Codex product answers, the source route is complete: the manual, Docs MCP when this route calls for it, official OpenAI web fallback, and callable capabilities surfaced in the current session when the question is about that capability. Knowledge bases outside developers.openai.com are outside this route for public product answers.
For broad Codex behavior, setup, customization, skills, plugins, MCP, hooks, `AGENTS.md`, automations, surfaces, local state, or system-map questions:
1. Reuse a same-thread manual and outline path when it is still fresh.
2. Otherwise run the skill-local helper first in normal writable sessions. Skip it without trying only when the session is explicitly read-only, shell execution is unavailable, or visible policy shows no allowed temp cache.
3. By default, the helper chooses the first usable temp cache dir in this order: `$TMPDIR/openai-docs-cache`, `%TEMP%\openai-docs-cache`, `%TMP%\openai-docs-cache`, `/private/tmp/openai-docs-cache`, then `/tmp/openai-docs-cache`. Workspace-only write access is not enough for this temp cache.
4. Run the helper directly unless you need to override the cache dir. The helper falls back to `curl` when native `fetch` is unavailable or when proxy env vars are present, so no shell-specific proxy prefix is required. Resolve `<skill-dir>` to this skill's actual directory; in copied local eval workdirs this is usually `.codex/skills/openai-docs`:
```bash
node <skill-dir>/scripts/fetch-codex-manual.mjs
```
If you need to override the cache dir, pass `--cache-dir <cache-dir>`. On Windows, the helper checks `%TEMP%` and `%TMP%` automatically; in PowerShell, `$env:TEMP\\openai-docs-cache` is a typical explicit override.
Treat helper availability as established by explicit read-only/no-shell policy or an actual command result. A guessed sandbox or guessed helper failure is not enough to switch to Docs MCP or web lookup; after an actual helper command failure, continue to the narrowest official next source below.
The helper verifies freshness, writes `codex-manual.md`, and emits `codex-manual.outline.md`. The outline maps source pages and headings to line ranges; use it to choose the relevant manual section, then read or search targeted manual sections for Codex product facts. Use the skill directory to locate and run the helper; after the helper succeeds, use the returned manual and outline paths as the search scope for Codex product facts and term coverage checks.
Reuse the same-thread manual and outline paths for follow-up Codex questions. Refresh first when the manual was fetched more than about a day ago, the path is unusable, the path came from another thread or uncertain provenance, or likely-current information is missing and staleness is plausible.
For questions about whether the manual is current enough to rely on now, run the helper when temp caching is allowed and base the answer on its returned status, manual path, and outline path.
If the manual resolves a Codex claim, answer from it and stop expanding sources for that claim; continue the user's broader task if the docs lookup was only one dependency. Manual source pages and known anchors are enough citation support for manual-covered material.
If the helper is skipped because the session is read-only, has no shell execution, or has no allowed temp cache, the next source is Docs MCP: call `mcp__openaiDeveloperDocs__search_openai_docs`, then `mcp__openaiDeveloperDocs__fetch_openai_doc` for a relevant hit before any web fallback.
If a user names a Codex term or mode that a fresh manual does not use, search the manual for obvious adjacent concepts, then answer that the exact term is not documented and use the closest documented terminology. If the prompt asks how that term maps to Codex behavior, resolve the mapping from adjacent manual sections. If the exact term remains material or likely current after that manual pass, use one narrow Docs MCP search/fetch before bounded uncertainty; otherwise, the source lookup for that terminology or mapping claim is complete.
Use the narrowest official next source only when the manual is unavailable, the helper fails, temp caching is not allowed, another material claim is missing or likely stale, or the user explicitly needs a page-specific citation. Prefer one specific Docs MCP search and, if it returns a clearly relevant page, one fetch; for unresolved Codex capability names, acronyms, scheduling terms, or exact error text, this Docs MCP step is the next source before web search. After the manual plus any permitted Docs MCP gap-fill, resolve remaining gaps as bounded uncertainty. Use official-domain web fallback only after that Docs MCP path is unavailable or unhelpful. If the claim is still not established, stop with bounded uncertainty. If official docs/manual conflict with a callable capability already surfaced in the current session, state the conflict and prefer verified current-session behavior for that environment.
For undocumented or private-looking model slugs, product mode labels, entitlement labels, account access paths, or rollout names, answer from current public docs and bounded uncertainty. Those labels are not a reason to leave the public source route.
For support-style diagnostics, prefer a layer-by-layer answer from the manual over provider-specific web lookups: installed/enabled plugin, bundled app or connector authorization, MCP setup, workspace/admin policy, restart or new-thread expectations, then support or feedback if still unresolved.
If the source route still does not establish a claim, return bounded uncertainty or route to support, an admin, or product feedback instead of widening the investigation.
For unresolved product terminology, answer from the manual plus the allowed official next source. If those sources do not establish the term, answer with bounded uncertainty from those sources.
### Surface Map
When Codex nouns or durable-instruction surfaces overlap, recommend the smallest surface that matches the scope:
- Prompt or thread context -> one-off task constraints.
- `AGENTS.md` -> durable repo conventions, commands, verification steps, and review expectations; closer nested files apply under their subtree.
- Project `.codex/config.toml` -> trusted-repo Codex settings such as sandbox, MCP, hooks, model, or reasoning defaults.
- Global config or global guidance -> personal defaults across repos.
- Skill -> reusable task workflow with references or scripts.
- Plugin -> installable bundle with skills plus commands, tools, MCP config, hooks, assets, apps, or marketplace metadata.
- MCP server or app connector -> live external data/actions or authorized private app/workspace data. Use connectors for private Google Docs, Calendar, Slack, GitHub, Notion, and similar data instead of web search or model memory.
- Automation -> scheduled checks, reminders, monitors, or follow-up work; use a thread heartbeat when continuity in an existing thread matters.
- Hook -> lifecycle enforcement around tool calls, commands, or file edits.
Split mixed-scope requests instead of forcing one answer. Example: "always do X, but only for this PR" defaults to prompt/thread context for the current run; use `AGENTS.md` or project config only if it should persist, hooks only for mechanical enforcement, and automations only for scheduled or follow-up work.
Use this quick product map when needed: CLI is terminal-first local repo work; IDE extension is editor-attached coding; Codex app is desktop planning, review, and interactive work; cloud/web is hosted parallel/offloaded work; Browser Use/in-app browser is Codex-controlled web testing; Chrome extension uses the user's Chrome profile; Computer Use controls desktop apps and OS UI. Keep `config.toml` defaults, `requirements.toml` constraints, and managed/admin policy separate.
### Boundaries And Output
- API key auth does not imply ChatGPT, cloud task, or connector access. For plugin/app/auth failures, check bundle availability, plugin installed/enabled state, connector/app authorization, MCP setup, restart/refresh expectations, workspace policy, and per-surface availability before answering.
- Sandbox or network denials need scoped escalation with a clear justification. Destructive commands, writes outside the workspace, or broad access changes require explicit approval.
- Memory can provide user preference or context, but explicit prompt instructions win and memory is not a source for current external facts.
- For affirmative surface-selection answers, use this shape: recommendation, why, what to avoid, and the manual/source evidence used.
- When page-specific Codex citations are actually needed, these anchors often fit: `concepts/customization#agents-guidance` for `AGENTS.md`, `concepts/customization#skills` for skills, `plugins/build#plugin-structure` for plugins, `concepts/customization#mcp` for MCP, `config-advanced#hooks` for hooks, `app/automations#thread-automations` for thread automations, and `config-reference#configtoml` for config.
## If MCP server is missing
If MCP tools fail or no OpenAI docs resources are available:
1. Run the install command yourself: `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`
2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval. Do not ask the user to run it yet.
3. Only if the escalated attempt fails, ask the user to run the install command.
2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval.
3. Ask the user to run the install command only if the escalated attempt fails.
4. Ask the user to restart Codex.
5. Re-run the doc search/fetch after restart.
## Workflow
1. Clarify whether the request is general docs lookup, model selection, a model-string upgrade, prompt-upgrade guidance, or broader API/provider migration.
2. For model-selection or upgrade requests, prefer current remote docs over bundled references when the user asks for latest/current/default guidance.
2. For Codex self-knowledge requests, follow the Codex self-knowledge source procedure above.
3. For model-selection or upgrade requests, prefer current remote docs over bundled references when the user asks for latest/current/default guidance.
- Fetch `https://developers.openai.com/api/docs/guides/latest-model.md`.
- Find the latest model ID and explicit migration or prompt-guidance links.
- Prefer explicit links from the latest-model page over derived URLs.
- For explicit named-model requests, preserve the requested model target and do not silently retarget to the latest model. Mention newer remote guidance only as optional.
- For explicit named-model requests, preserve the requested model target. Mention newer remote guidance only as optional.
- For dynamic latest/current/default upgrades, run `node scripts/resolve-latest-model-info.js`, then fetch both returned guide URLs directly when possible.
- If direct guide fetch fails, use the developer-docs MCP tools or official OpenAI-domain search to find the same guide content.
- If remote docs are unavailable, use bundled fallback references and say that fallback guidance was used.
3. For model upgrades, keep changes narrow: update active OpenAI API model defaults and directly related prompts only when safe.
4. Leave historical docs, examples, eval baselines, fixtures, provider comparisons, provider registries, pricing tables, alias defaults, low-cost fallback paths, and ambiguous older model usage unchanged unless the user explicitly asks to upgrade them.
5. Do not perform SDK, tooling, IDE, plugin, shell, auth, or provider-environment migrations as part of a model-and-prompt upgrade.
6. If an upgrade needs API-surface changes, schema rewiring, tool-handler changes, or implementation work beyond a literal model-string replacement and prompt edits, report it as blocked or confirmation-needed.
7. For general docs lookup, search docs with a precise query, fetch the best page and exact section needed, and answer with concise citations.
4. For model upgrades, keep changes narrow: update active OpenAI API model defaults and directly related prompts only when safe.
5. Leave historical docs, examples, eval baselines, fixtures, provider comparisons, provider registries, pricing tables, alias defaults, low-cost fallback paths, and ambiguous older model usage unchanged unless the user explicitly asks to upgrade them.
6. Keep SDK, tooling, IDE, plugin, shell, auth, and provider-environment migrations out of a model-and-prompt upgrade unless the user explicitly asks for them.
7. If an upgrade needs API-surface changes, schema rewiring, tool-handler changes, or implementation work beyond a literal model-string replacement and prompt edits, report it as blocked or confirmation-needed.
8. For general docs lookup, search docs with a precise query, fetch the best page and exact section needed, and answer with concise citations.
## Reference map
Read only what you need:
- `https://developers.openai.com/api/docs/guides/latest-model.md` -> current model-selection and "best/latest/current model" questions.
- `scripts/fetch-codex-manual.mjs` -> current Codex manual fetch, verification, local temp cache, and outline generation.
- `https://developers.openai.com/codex/codex-manual.md` -> current Codex self-knowledge synthesis, including setup, customization, skills, plugins, MCP, hooks, `AGENTS.md`, automations, and surface behavior; normally access it through the helper path and targeted file reads when temp caching is available.
- `references/latest-model.md` -> bundled fallback for model-selection and "best/latest/current model" questions.
- `references/upgrade-guide.md` -> bundled fallback for model upgrade and upgrade-planning requests.
- `references/prompting-guide.md` -> bundled fallback for prompt rewrites and prompt-behavior upgrades.
@@ -73,16 +151,17 @@ Read only what you need:
## Quality rules
- Treat OpenAI docs as the source of truth; avoid speculation.
- For Codex self-knowledge, follow the source route above instead of relying on remembered behavior.
- Keep migration changes narrow and behavior-preserving.
- Prefer prompt-only upgrades when possible.
- Do not invent pricing, availability, parameters, API changes, or breaking changes.
- Avoid inventing pricing, availability, parameters, API changes, or breaking changes.
- Keep quotes short and within policy limits; prefer paraphrase with citations.
- If multiple pages differ, call out the difference and cite both.
- If official docs and repo behavior disagree, state the conflict and stop before making broad edits.
- If official docs and verified callable current-session behavior disagree, state the conflict before making broad claims or edits.
- If docs do not cover the users need, say so and offer next steps.
## Tooling notes
- Always use MCP doc tools before any web search for OpenAI-related questions.
- Use MCP doc tools before web search for OpenAI-related markdown docs. The Codex manual flow is the exception: follow the Codex self-knowledge source procedure for broad Codex synthesis.
- If the MCP server is installed but returns no meaningful results, then use web search as a fallback.
- When falling back to web search, restrict to official OpenAI domains (developers.openai.com, platform.openai.com) and cite sources.
@@ -1,9 +1,9 @@
interface:
display_name: "OpenAI Docs"
short_description: "Reference docs, choose models, and migrate OpenAI API integrations"
short_description: "Reference OpenAI docs, Codex self-knowledge, and model migration guidance"
icon_small: "./assets/openai-small.svg"
icon_large: "./assets/openai.png"
default_prompt: "Use OpenAI Docs for official docs lookup, model selection, model migration, and prompt-upgrade work."
default_prompt: "Use OpenAI Docs for official docs lookup, questions about Codex itself or Codex surfaces, model selection, model migration, and prompt-upgrade work."
dependencies:
tools:
@@ -0,0 +1,598 @@
#!/usr/bin/env node
import {
access,
mkdir,
readFile,
rename,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { constants as fsConstants } from "node:fs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import { inspect, promisify } from "node:util";
const DEFAULT_MANUAL_URL = "https://developers.openai.com/codex/codex-manual.md";
const DEFAULT_CACHE_DIR_NAME = "openai-docs-cache";
const CACHE_FILE_NAME = "codex-manual.md";
const OUTLINE_FILE_NAME = "codex-manual.outline.md";
const HASH_HEADER = "x-content-sha256";
const USER_AGENT = "codex-openai-docs";
const execFileAsync = promisify(execFile);
class ManualFetchError extends Error {
constructor(message, options) {
super(message, options);
this.name = "ManualFetchError";
}
}
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const withTimeout = async (promiseFactory, timeoutMs) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await promiseFactory(controller.signal);
} finally {
clearTimeout(timeout);
}
};
const proxyConfigured = () =>
process.env.HTTP_PROXY ||
process.env.HTTPS_PROXY ||
process.env.http_proxy ||
process.env.https_proxy;
const responseHeaders = (headers) => ({
get(name) {
return headers.get(name.toLowerCase()) ?? null;
},
});
const makeResponse = ({ body, headers, status }) => ({
headers: responseHeaders(headers),
ok: status >= 200 && status < 300,
status,
async text() {
return body;
},
});
const parseCurlHeaders = (rawHeaders) => {
const normalized = rawHeaders.replace(/\r\n/g, "\n").trim();
const blocks = normalized.split(/\n\n+/).filter(Boolean);
const headerBlock = [...blocks]
.reverse()
.find((block) => block.startsWith("HTTP/"));
if (!headerBlock) {
throw new ManualFetchError("curl did not return HTTP response headers.");
}
const [statusLine, ...lines] = headerBlock.split("\n");
const statusMatch = /^HTTP\/\S+\s+(\d{3})/.exec(statusLine);
if (!statusMatch) {
throw new ManualFetchError(
`Could not parse HTTP status from curl response: ${statusLine}`
);
}
const headers = new Map();
lines.forEach((line) => {
const separator = line.indexOf(":");
if (separator === -1) return;
const name = line.slice(0, separator).trim().toLowerCase();
const value = line.slice(separator + 1).trim();
headers.set(name, value);
});
return {
headers,
status: Number(statusMatch[1]),
};
};
const tempFilePath = (cacheDir, suffix) =>
path.join(
cacheDir,
`.fetch-codex-manual-${process.pid}-${Date.now()}-${Math.random()
.toString(16)
.slice(2)}${suffix}`
);
const requestManualWithCurl = async (url, { cacheDir, method, timeoutMs }) => {
const headerPath = tempFilePath(cacheDir, ".headers");
const bodyPath = tempFilePath(cacheDir, ".body");
const curlNames =
process.platform === "win32" ? ["curl.exe", "curl"] : ["curl"];
const args = [
"--silent",
"--show-error",
"--location",
"--dump-header",
headerPath,
"--output",
bodyPath,
"--user-agent",
USER_AGENT,
"--max-time",
String(Math.max(1, Math.ceil(timeoutMs / 1000))),
];
if (method === "HEAD") {
args.push("--head");
} else {
args.push("--request", method);
}
args.push(url);
let lastError;
for (const curlName of curlNames) {
try {
await execFileAsync(curlName, args, { windowsHide: true });
const [rawHeaders, body] = await Promise.all([
readFile(headerPath, "utf8"),
readFile(bodyPath, "utf8"),
]);
const { headers, status } = parseCurlHeaders(rawHeaders);
return makeResponse({ body, headers, status });
} catch (error) {
lastError = error;
if (error?.code !== "ENOENT") break;
} finally {
await Promise.all([
rm(headerPath, { force: true }),
rm(bodyPath, { force: true }),
]);
}
}
if (lastError?.code === "ENOENT") {
throw new ManualFetchError("curl is unavailable in this environment.", {
cause: lastError,
});
}
throw new ManualFetchError(`${method} ${url} could not be fetched.`, {
cause: lastError,
});
};
const requestManualWithFetch = async (url, { method, timeoutMs }) => {
if (typeof fetch !== "function") {
throw new ManualFetchError(
"Native fetch is unavailable in this Node runtime."
);
}
return withTimeout(
(signal) =>
fetch(url, {
method,
headers: { "User-Agent": USER_AGENT },
signal,
}),
timeoutMs
);
};
const requestManual = async (url, { cacheDir, method, timeoutMs }) => {
const preferCurl = Boolean(proxyConfigured()) || typeof fetch !== "function";
const transports = preferCurl
? [
() => requestManualWithCurl(url, { cacheDir, method, timeoutMs }),
() => requestManualWithFetch(url, { method, timeoutMs }),
]
: [
() => requestManualWithFetch(url, { method, timeoutMs }),
() => requestManualWithCurl(url, { cacheDir, method, timeoutMs }),
];
let lastError;
for (const transport of transports) {
try {
const response = await transport();
if (!response.ok) {
throw new ManualFetchError(
`${method} ${url} failed with HTTP ${response.status}.`
);
}
return response;
} catch (error) {
lastError = error;
}
}
throw new ManualFetchError(`${method} ${url} could not be fetched.`, {
cause: lastError,
});
};
const readHeaderSha = (response) => {
const value = response.headers.get(HASH_HEADER);
if (!value || !/^[a-f0-9]{64}$/i.test(value)) {
throw new ManualFetchError(`Manual response is missing ${HASH_HEADER}.`);
}
return value.toLowerCase();
};
const nearestExistingParent = async (target) => {
let current = target;
while (true) {
try {
const info = await stat(current);
return info.isDirectory() ? current : null;
} catch (error) {
if (error?.code !== "ENOENT") return null;
}
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
};
const usableCacheDir = async (cacheDir) => {
if (!cacheDir) return null;
const resolved = path.resolve(cacheDir);
try {
const info = await stat(resolved);
if (!info.isDirectory()) return null;
} catch (error) {
if (error?.code !== "ENOENT") return null;
}
const parent = await nearestExistingParent(resolved);
if (!parent) return null;
try {
await access(parent, fsConstants.W_OK | fsConstants.X_OK);
} catch {
return null;
}
return resolved;
};
const defaultCacheDirCandidates = () => {
const candidates = [];
const seen = new Set();
const pushCandidate = (candidate) => {
if (!candidate || seen.has(candidate)) return;
seen.add(candidate);
candidates.push(candidate);
};
[process.env.TMPDIR, process.env.TEMP, process.env.TMP].forEach((baseDir) => {
if (baseDir) {
pushCandidate(path.join(baseDir, DEFAULT_CACHE_DIR_NAME));
}
});
if (process.platform !== "win32") {
pushCandidate(`/private/tmp/${DEFAULT_CACHE_DIR_NAME}`);
pushCandidate(`/tmp/${DEFAULT_CACHE_DIR_NAME}`);
}
return candidates;
};
const resolveCacheDir = async (cacheDir) => {
if (cacheDir) {
return usableCacheDir(cacheDir);
}
for (const candidate of defaultCacheDirCandidates()) {
const usable = await usableCacheDir(candidate);
if (usable) return usable;
}
return null;
};
const cacheFilePath = (cacheDir) => path.join(cacheDir, CACHE_FILE_NAME);
const outlineFilePath = (cacheDir) => path.join(cacheDir, OUTLINE_FILE_NAME);
const manualLines = (manual) => {
const lines = manual.replace(/\r\n/g, "\n").split("\n");
if (lines[lines.length - 1] === "") lines.pop();
return lines;
};
const sectionTitle = (rawTitle) =>
rawTitle.replace(/\s+#+\s*$/, "").replace(/\s+/g, " ").trim();
const buildOutline = (manual) => {
const lines = manualLines(manual);
const headings = [];
let inFence = false;
lines.forEach((line, index) => {
if (/^\s*(```|~~~)/.test(line)) {
inFence = !inFence;
return;
}
if (inFence) return;
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
if (!match) return;
const level = match[1].length;
if (level < 2 || level > 3) return;
headings.push({
level,
title: sectionTitle(match[2]),
startLine: index + 1,
endLine: lines.length,
});
});
for (let index = 0; index < headings.length; index += 1) {
const heading = headings[index];
const nextPeer = headings
.slice(index + 1)
.find((candidate) => candidate.level <= heading.level);
if (nextPeer) {
heading.endLine = nextPeer.startLine - 1;
}
}
if (headings.length === 0) {
return {
headingCount: 0,
lineCount: lines.length,
text: "No markdown headings found.",
};
}
const minLevel = Math.min(...headings.map((heading) => heading.level));
return {
headingCount: headings.length,
lineCount: lines.length,
text: headings
.map((heading) => {
const indent = " ".repeat(heading.level - minLevel);
return `${indent}- ${heading.title} (lines ${heading.startLine}-${heading.endLine})`;
})
.join("\n"),
};
};
const outlineMarkdown = (outline) => `# Codex Manual Outline\n\n${outline.text}\n`;
const manualStatusLine = (status) =>
status.cacheStatus === "hit"
? "Manual status: local manual was already current."
: "Manual status: local manual was updated.";
const formatResult = ({ status, outlineText }) =>
[
`Manual path: ${status.manualPath}`,
`Outline path: ${status.outlinePath}`,
manualStatusLine(status),
"",
outlineText,
].join("\n");
const readCachedManual = async (cacheDir, expectedSha256) => {
try {
const manual = await readFile(cacheFilePath(cacheDir), "utf8");
return sha256(manual) === expectedSha256 ? manual : null;
} catch {
return null;
}
};
const writeCachedManual = async (cacheDir, manual) => {
await mkdir(cacheDir, { recursive: true });
const tmpPath = tempFilePath(cacheDir, `.${CACHE_FILE_NAME}.tmp`);
await writeFile(tmpPath, manual, "utf8");
await rename(tmpPath, cacheFilePath(cacheDir));
};
const writeOutline = async (cacheDir, outlineText) => {
await mkdir(cacheDir, { recursive: true });
const tmpPath = tempFilePath(cacheDir, `.${OUTLINE_FILE_NAME}.tmp`);
await writeFile(tmpPath, outlineText, "utf8");
await rename(tmpPath, outlineFilePath(cacheDir));
};
const fetchCodexManual = async ({
manualUrl = DEFAULT_MANUAL_URL,
cacheDir,
timeoutMs = 30000,
} = {}) => {
const resolvedCacheDir = await resolveCacheDir(cacheDir);
if (!resolvedCacheDir) {
throw new ManualFetchError(
"Manual cache directory is unavailable; pass --cache-dir to override or use OpenAI Docs MCP fallback."
);
}
await mkdir(resolvedCacheDir, { recursive: true });
const headResponse = await requestManual(manualUrl, {
cacheDir: resolvedCacheDir,
method: "HEAD",
timeoutMs,
});
const expectedSha256 = readHeaderSha(headResponse);
const manualPath = cacheFilePath(resolvedCacheDir);
const outlinePath = outlineFilePath(resolvedCacheDir);
const checkedAt = new Date().toISOString();
const cachedManual = await readCachedManual(resolvedCacheDir, expectedSha256);
if (cachedManual !== null) {
const outline = buildOutline(cachedManual);
const outlineText = outlineMarkdown(outline);
await writeOutline(resolvedCacheDir, outlineText);
return {
outlineText,
status: {
manualUrl,
headerSha256: expectedSha256,
fetchedManualSha256: expectedSha256,
manualHashMatches: true,
cacheStatus: "hit",
cacheDir: resolvedCacheDir,
manualPath,
outlinePath,
checkedAt,
lineCount: outline.lineCount,
headingCount: outline.headingCount,
},
};
}
const getResponse = await requestManual(manualUrl, {
cacheDir: resolvedCacheDir,
method: "GET",
timeoutMs,
});
const getHeaderSha256 = readHeaderSha(getResponse);
if (getHeaderSha256 !== expectedSha256) {
throw new ManualFetchError(
`${HASH_HEADER} changed between HEAD and GET for ${manualUrl}.`
);
}
const manualText = await getResponse.text();
const actualSha256 = sha256(manualText);
const manualHashMatches = actualSha256 === expectedSha256;
if (!manualHashMatches) {
throw new ManualFetchError(
`${HASH_HEADER} did not match the fetched manual body for ${manualUrl}.`
);
}
await writeCachedManual(resolvedCacheDir, manualText);
const outline = buildOutline(manualText);
const outlineText = outlineMarkdown(outline);
await writeOutline(resolvedCacheDir, outlineText);
return {
outlineText,
status: {
manualUrl,
headerSha256: expectedSha256,
fetchedManualSha256: actualSha256,
manualHashMatches,
cacheStatus: "updated",
cacheDir: resolvedCacheDir,
manualPath,
outlinePath,
checkedAt,
lineCount: outline.lineCount,
headingCount: outline.headingCount,
},
};
};
const parseArgs = (argv) => {
const args = {
manualUrl: DEFAULT_MANUAL_URL,
cacheDir: undefined,
timeoutMs: 30000,
statusJson: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--manual-url") {
args.manualUrl = argv[++index];
} else if (arg === "--cache-dir") {
args.cacheDir = argv[++index];
} else if (arg === "--timeout-ms") {
args.timeoutMs = Number(argv[++index]);
} else if (arg === "--status-json") {
args.statusJson = true;
} else {
throw new ManualFetchError(`Unknown argument: ${arg}`);
}
}
if (!args.manualUrl) {
throw new ManualFetchError("--manual-url cannot be empty.");
}
if (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0) {
throw new ManualFetchError("--timeout-ms must be a positive number.");
}
return args;
};
const main = async () => {
const args = parseArgs(process.argv.slice(2));
const { outlineText, status } = await fetchCodexManual(args);
process.stdout.write(formatResult({ status, outlineText }));
if (args.statusJson) {
console.error(JSON.stringify(status));
}
};
const envProxyHint = () => {
if (proxyConfigured()) {
return "Hint: proxy env vars are present. This helper prefers `curl` in proxied sessions; if requests still fail, verify `curl` is installed and the proxy configuration is valid.";
}
if (typeof fetch !== "function") {
return "Hint: native fetch is unavailable in this Node runtime. Install `curl` or use a newer Node version to fetch the manual.";
}
if (process.platform === "win32") {
return "Hint: on Windows, pass a cache dir under `%TEMP%` or `%TMP%`.";
}
return null;
};
const formatErrorDetails = (error) => {
const details = inspect(error, {
breakLength: 120,
colors: false,
compact: false,
depth: 8,
});
if (!error?.cause) {
return details;
}
return `${details}\n\nCause:\n${inspect(error.cause, {
breakLength: 120,
colors: false,
compact: false,
depth: 8,
})}`;
};
const isCliEntrypoint = () => {
const entrypoint = process.argv[1];
if (!entrypoint) {
return false;
}
return pathToFileURL(entrypoint).href === import.meta.url;
};
if (isCliEntrypoint()) {
main().catch((error) => {
console.error(`Error: ${error.message}`);
const hint = envProxyHint();
if (hint) {
console.error(hint);
}
console.error("");
console.error("Details:");
console.error(formatErrorDetails(error));
process.exitCode = 1;
});
}
export { DEFAULT_MANUAL_URL, fetchCodexManual };