@howaboua/pi-codex-conversion 1.5.4-dev.19.703be35 → 1.5.4-dev.20.3249745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,127 @@
1
+ # Changelog
2
+
3
+ ## 1.5.4
4
+
5
+ - Added `/codex` settings UI.
6
+ - Added saved global config at `~/.pi/agent/pi-codex-conversion.json`.
7
+ - Added toggles for fast mode, native web search, native image generation, and using the adapter on all models.
8
+ - Added verbosity control for Responses API providers.
9
+ - Added footer status details for active Codex settings.
10
+ - Added quick links from the settings UI to GitHub, Discord, and issue filing.
11
+ - Updated Pi development dependencies to 0.74.1.
12
+
13
+ ## 1.5.3
14
+
15
+ - Improved exploration output for skill reads so `SKILL.md` activity is easier to understand.
16
+
17
+ ## 1.5.2
18
+
19
+ - Streamed partial `exec_command` updates while commands are still running.
20
+ - Improved background terminal responsiveness and display state.
21
+
22
+ ## 1.5.1
23
+
24
+ - Cleaned up the Codex adapter prompt and tool surface.
25
+ - Fixed skill prompt injection after reload.
26
+ - Fixed adapter tool restore behavior when switching models.
27
+ - Simplified tool descriptions and README wording.
28
+ - Bundled `apply_patch` and moved publishing to GitHub Actions.
29
+
30
+ ## 1.5.0
31
+
32
+ - Aligned the Codex provider with Pi 0.73 and Pi 0.74 package/API changes.
33
+ - Updated package scope for the Earendil Pi packages.
34
+ - Removed a noisy web search startup note.
35
+
36
+ ## 1.0.29
37
+
38
+ - Aligned with Pi 0.72.
39
+ - Fixed cached websocket transport behavior.
40
+ - Fixed thinking-level mapping and runtime compatibility issues.
41
+
42
+ ## 1.0.28
43
+
44
+ - Aligned with Pi 0.70.5 Codex provider changes.
45
+
46
+ ## 1.0.27
47
+
48
+ - Marked Codex websocket failures as retryable connection errors.
49
+
50
+ ## 1.0.26
51
+
52
+ - Retried stale Codex websocket reuse.
53
+
54
+ ## 1.0.25
55
+
56
+ - Sanitized Codex image generation history before sending follow-up requests.
57
+
58
+ ## 1.0.24
59
+
60
+ - Updated the adapter for Pi 0.70 compatibility.
61
+ - Fixed Codex websocket close race handling.
62
+
63
+ ## 1.0.23
64
+
65
+ - Hotfix to remove a stale Codex max token field.
66
+
67
+ ## 1.0.22
68
+
69
+ - Hotfix to omit unsupported Codex max output tokens.
70
+
71
+ ## 1.0.21
72
+
73
+ - Hardened Codex provider streaming and image handling.
74
+ - Preserved Codex image generation calls in conversation history.
75
+ - Aligned websocket client behavior with Pi's Codex provider.
76
+ - Future-proofed GPT-5 reasoning effort clamping.
77
+
78
+ ## 1.0.20
79
+
80
+ - Updated for Pi 0.69 typebox changes.
81
+ - Replicated Pi Codex websocket transport handling.
82
+ - Fixed Codex SSE parsing, websocket auth, stream indexing, and websocket caching.
83
+ - Moved image path guidance into prompt/tool text.
84
+ - Hardened runtime behavior and activity ordering.
85
+
86
+ ## 1.0.19
87
+
88
+ - Added native Codex web search and image generation support.
89
+ - Fixed Codex custom provider packaging and session handling.
90
+ - Restored Pi's default shell renderer for `apply_patch`.
91
+
92
+ ## 1.0.18
93
+
94
+ - Aligned the extension with Pi 0.67.3 APIs.
95
+ - Fixed `prepareArguments` validation regressions.
96
+
97
+ ## 1.0.17
98
+
99
+ - Improved `apply_patch` fuzzy matching safety.
100
+ - Continued applying independent patch actions after file failures.
101
+ - Blocked dependent patch actions after earlier failures.
102
+ - Tightened delete matching and path canonicalization.
103
+ - Improved section-anchor matching and partial move failure reporting.
104
+
105
+ ## 1.0.12
106
+
107
+ - Added structured `apply_patch` recovery hints.
108
+ - Improved `apply_patch` failure rendering.
109
+ - Capped exec session buffers at 256 MiB.
110
+
111
+ ## 1.0.11
112
+
113
+ - Hotfix to show `apply_patch` failures after arguments complete.
114
+ - Hotfix to hide incomplete `apply_patch` previews.
115
+
116
+ ## 1.0.10
117
+
118
+ - Rendered partial `apply_patch` failures inline.
119
+ - Added PTY polling guardrails for `write_stdin`.
120
+ - Clamped tiny `exec_command` waits for non-interactive runs.
121
+ - Clarified `write_stdin` polling behavior in the README.
122
+
123
+ ## 1.0.9
124
+
125
+ - Initial public release of the Codex-style Pi adapter.
126
+ - Added Codex-style shell tools, resumable exec sessions, patch editing, and tool rendering.
127
+ - Forced bash when Pi is launched under fish while preserving fish-derived `PATH`.
package/README.md CHANGED
@@ -9,22 +9,14 @@ GPT/Codex models are strongest when the tool surface looks like the Codex CLI th
9
9
 
10
10
  The point is to give the model tools it already knows how to use well: shell-first inspection, resumable command sessions, and large one-shot patch edits instead of piecemeal read/edit/write steps.
11
11
 
12
+ You can also opt into using the adapter on every provider/model. YMMV: Codex-tuned models are still the best fit, but the shell/patch workflow can help elsewhere too. The extension also has a small `/codex` settings UI for toggling adapter behavior, web search, image generation, fast mode, and verbosity. See [Settings](#settings).
13
+
12
14
  ## Install
13
15
 
14
16
  ```bash
15
17
  pi install npm:@howaboua/pi-codex-conversion
16
18
  ```
17
19
 
18
- ## Development checkout
19
-
20
- The Git checkout is mostly for development and mirrors the maintainer workflow. If you run it directly, you may need to build the bundled `apply_patch` binary for your platform.
21
-
22
- Run the current checkout without installing globally:
23
-
24
- ```bash
25
- pi --no-extensions --no-skills -e /path/to/pi-codex-conversion
26
- ```
27
-
28
20
  ![Available tools](./available-tools.png)
29
21
 
30
22
  ## Active tools in adapter mode
@@ -45,6 +37,26 @@ Notably:
45
37
  - file creation and edits should default to `apply_patch`
46
38
  - Pi may still expose additional runtime tools such as `parallel`; the prompt is written to tolerate that instead of assuming a fixed four-tool universe
47
39
 
40
+ ## Settings
41
+
42
+ Use `/codex` to change adapter settings.
43
+
44
+ - `/codex all` — use the Codex tool and prompt adapter on every model
45
+ - `/codex fast` — toggle priority service tier for the OpenAI Codex provider
46
+ - `/codex search` — toggle native Codex web search
47
+ - `/codex image` — toggle native Codex image generation
48
+ - `/codex low`, `/codex medium`, `/codex high` — set Responses API verbosity
49
+
50
+ Settings are saved globally in `~/.pi/agent/pi-codex-conversion.json`.
51
+
52
+ When `all` is on, non-Codex providers get the shell, patch, skill, and prompt-adapter behavior, but keep their normal Pi provider path. Native web search, native image generation, and priority service tier stay limited to the OpenAI Codex provider. Verbosity is applied to Responses API providers.
53
+
54
+ The footer shows the active state, for example:
55
+
56
+ ```text
57
+ Codex adapter V: low • web search • image gen
58
+ ```
59
+
48
60
  ## What changes in Pi
49
61
 
50
62
  - Adapter mode activates automatically for OpenAI `gpt*` and `codex*` models, then restores the previous tool set when you switch away.
@@ -72,6 +84,16 @@ Raw command output is still available by expanding the tool result.
72
84
  - Shell `apply_patch` is also available inside `exec_command`, but the dedicated `apply_patch` tool is preferred unless you are chaining edits with other shell steps.
73
85
  - Native `web_search` and `image_generation` are forwarded to OpenAI Codex Responses tools rather than executed as local function tools.
74
86
 
87
+ ## Development checkout
88
+
89
+ The Git checkout is mostly for development and mirrors the maintainer workflow. If you run it directly, you may need to build the bundled `apply_patch` binary for your platform.
90
+
91
+ Run the current checkout without installing globally:
92
+
93
+ ```bash
94
+ pi --no-extensions --no-skills -e /path/to/pi-codex-conversion
95
+ ```
96
+
75
97
  ## License
76
98
 
77
99
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.5.4-dev.19.703be35",
3
+ "version": "1.5.4-dev.20.3249745",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -33,6 +33,7 @@
33
33
  "bin/apply_patch.cmd",
34
34
  "vendor/apply-patch/**",
35
35
  "available-tools.png",
36
+ "CHANGELOG.md",
36
37
  "README.md",
37
38
  "LICENSE"
38
39
  ],
@@ -0,0 +1,109 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isCodexLikeContext, isOpenAICodexContext, isResponsesContext } from "./codex-model.ts";
3
+ import type { CodexConversionConfig } from "./config.ts";
4
+ import type { AdapterState } from "./state.ts";
5
+ import {
6
+ CORE_ADAPTER_TOOL_NAMES,
7
+ DEFAULT_TOOL_NAMES,
8
+ IMAGE_GENERATION_TOOL_NAME,
9
+ STATUS_KEY,
10
+ VIEW_IMAGE_TOOL_NAME,
11
+ WEB_SEARCH_TOOL_NAME,
12
+ buildStatusText,
13
+ } from "./tool-set.ts";
14
+ import { supportsNativeImageGeneration } from "../tools/image-generation-tool.ts";
15
+ import { supportsNativeWebSearch } from "../tools/web-search-tool.ts";
16
+
17
+ const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
18
+
19
+ export function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
20
+ if (shouldUseCodexAdapter(ctx, state.config)) {
21
+ enableAdapter(pi, ctx, state);
22
+ } else {
23
+ disableAdapter(pi, ctx, state);
24
+ }
25
+ }
26
+
27
+ export function shouldUseCodexAdapter(ctx: ExtensionContext, config: CodexConversionConfig): boolean {
28
+ return config.useOnAllModels || isCodexLikeContext(ctx);
29
+ }
30
+
31
+ function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
32
+ const toolNames = mergeAdapterTools(pi.getActiveTools(), getAdapterToolNames(ctx, state.config));
33
+ if (!state.enabled) {
34
+ // Preserve the previous active set once so switching away from Codex-like
35
+ // models restores the user's existing Pi tool configuration. Strip adapter
36
+ // tools in case a fresh session starts from persisted/mixed active tools.
37
+ state.previousToolNames = stripAdapterTools(pi.getActiveTools());
38
+ state.enabled = true;
39
+ }
40
+ pi.setActiveTools(toolNames);
41
+ setStatus(ctx, true, state.config);
42
+ }
43
+
44
+ function disableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
45
+ const previousToolNames = state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES;
46
+ const restoredTools = restoreTools(previousToolNames, pi.getActiveTools());
47
+ if (state.enabled || hasAdapterTools(pi.getActiveTools())) {
48
+ pi.setActiveTools(restoredTools);
49
+ }
50
+ if (state.enabled) {
51
+ state.enabled = false;
52
+ }
53
+ setStatus(ctx, false, state.config);
54
+ }
55
+
56
+ function setStatus(ctx: ExtensionContext, enabled: boolean, config: CodexConversionConfig): void {
57
+ if (!ctx.hasUI) return;
58
+ const statusConfig = getStatusConfig(ctx, config);
59
+ ctx.ui.setStatus(STATUS_KEY, enabled ? buildStatusText(statusConfig) : undefined);
60
+ }
61
+
62
+ function getStatusConfig(ctx: ExtensionContext, config: CodexConversionConfig): Parameters<typeof buildStatusText>[0] {
63
+ const showOpenAICodexFlags = isOpenAICodexContext(ctx);
64
+ const showResponsesVerbosity = isResponsesContext(ctx);
65
+ return {
66
+ useOnAllModels: config.useOnAllModels,
67
+ fast: showOpenAICodexFlags && config.fast,
68
+ webSearch: showOpenAICodexFlags && config.webSearch && supportsNativeWebSearch(ctx.model),
69
+ imageGeneration: showOpenAICodexFlags && config.imageGeneration && supportsNativeImageGeneration(ctx.model),
70
+ ...(showResponsesVerbosity ? { verbosity: config.verbosity } : {}),
71
+ };
72
+ }
73
+
74
+ function getAdapterToolNames(ctx: ExtensionContext, config: CodexConversionConfig): string[] {
75
+ const toolNames = [...CORE_ADAPTER_TOOL_NAMES];
76
+ if (config.webSearch && supportsNativeWebSearch(ctx.model)) {
77
+ toolNames.push(WEB_SEARCH_TOOL_NAME);
78
+ }
79
+ if (config.imageGeneration && supportsNativeImageGeneration(ctx.model)) {
80
+ toolNames.push(IMAGE_GENERATION_TOOL_NAME);
81
+ }
82
+ if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
83
+ toolNames.push(VIEW_IMAGE_TOOL_NAME);
84
+ }
85
+ return toolNames;
86
+ }
87
+
88
+ export function mergeAdapterTools(activeTools: string[], adapterTools: string[]): string[] {
89
+ const preservedTools = activeTools.filter((toolName) => !DEFAULT_TOOL_NAMES.includes(toolName) && !ADAPTER_TOOL_NAMES.includes(toolName));
90
+ return [...adapterTools, ...preservedTools];
91
+ }
92
+
93
+ export function restoreTools(previousTools: string[], activeTools: string[]): string[] {
94
+ const restored = stripAdapterTools(previousTools);
95
+ for (const toolName of activeTools) {
96
+ if (!ADAPTER_TOOL_NAMES.includes(toolName) && !restored.includes(toolName)) {
97
+ restored.push(toolName);
98
+ }
99
+ }
100
+ return restored;
101
+ }
102
+
103
+ export function stripAdapterTools(toolNames: string[]): string[] {
104
+ return toolNames.filter((toolName) => !ADAPTER_TOOL_NAMES.includes(toolName));
105
+ }
106
+
107
+ function hasAdapterTools(activeTools: string[]): boolean {
108
+ return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
109
+ }
@@ -11,6 +11,11 @@ export function isOpenAICodexModel(model: Partial<CodexLikeModelDescriptor> | nu
11
11
  return (model.provider ?? "").toLowerCase() === "openai-codex";
12
12
  }
13
13
 
14
+ export function isResponsesModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
15
+ if (!model) return false;
16
+ return (model.api ?? "").toLowerCase().includes("responses");
17
+ }
18
+
14
19
  // Keep model detection intentionally conservative. The adapter replaces the
15
20
  // system prompt and tool surface, so false positives are worse than misses.
16
21
  export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
@@ -30,3 +35,7 @@ export function isCodexLikeContext(ctx: ExtensionContext): boolean {
30
35
  export function isOpenAICodexContext(ctx: ExtensionContext): boolean {
31
36
  return isOpenAICodexModel(ctx.model);
32
37
  }
38
+
39
+ export function isResponsesContext(ctx: ExtensionContext): boolean {
40
+ return isResponsesModel(ctx.model);
41
+ }
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+
5
+ export type CodexVerbosity = "low" | "medium" | "high";
6
+
7
+ export interface CodexConversionConfig {
8
+ fast: boolean;
9
+ imageGeneration: boolean;
10
+ useOnAllModels: boolean;
11
+ webSearch: boolean;
12
+ verbosity: CodexVerbosity;
13
+ }
14
+
15
+ export const CODEX_CONVERSION_CONFIG_BASENAME = "pi-codex-conversion.json";
16
+ export const DEFAULT_CODEX_CONVERSION_CONFIG: CodexConversionConfig = {
17
+ fast: false,
18
+ imageGeneration: true,
19
+ useOnAllModels: false,
20
+ webSearch: true,
21
+ verbosity: "low",
22
+ };
23
+
24
+ function isObject(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+
28
+ export function normalizeCodexVerbosity(value: unknown): CodexVerbosity | undefined {
29
+ if (typeof value !== "string") return undefined;
30
+ const normalized = value.trim().toLowerCase();
31
+ return normalized === "low" || normalized === "medium" || normalized === "high" ? normalized : undefined;
32
+ }
33
+
34
+ export function getCodexConversionConfigPath(agentDir: string = getAgentDir()): string {
35
+ return join(agentDir, CODEX_CONVERSION_CONFIG_BASENAME);
36
+ }
37
+
38
+ export function readCodexConversionConfig(configPath: string = getCodexConversionConfigPath()): CodexConversionConfig {
39
+ if (!existsSync(configPath)) {
40
+ writeCodexConversionConfig(DEFAULT_CODEX_CONVERSION_CONFIG, configPath);
41
+ return { ...DEFAULT_CODEX_CONVERSION_CONFIG };
42
+ }
43
+
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
46
+ if (!isObject(parsed)) return { ...DEFAULT_CODEX_CONVERSION_CONFIG };
47
+ return {
48
+ fast: typeof parsed.fast === "boolean" ? parsed.fast : DEFAULT_CODEX_CONVERSION_CONFIG.fast,
49
+ imageGeneration: typeof parsed.imageGeneration === "boolean" ? parsed.imageGeneration : DEFAULT_CODEX_CONVERSION_CONFIG.imageGeneration,
50
+ useOnAllModels: typeof parsed.useOnAllModels === "boolean" ? parsed.useOnAllModels : DEFAULT_CODEX_CONVERSION_CONFIG.useOnAllModels,
51
+ webSearch: typeof parsed.webSearch === "boolean" ? parsed.webSearch : DEFAULT_CODEX_CONVERSION_CONFIG.webSearch,
52
+ verbosity: normalizeCodexVerbosity(parsed.verbosity) ?? DEFAULT_CODEX_CONVERSION_CONFIG.verbosity,
53
+ };
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ console.warn(`[pi-codex-conversion] Failed to read ${configPath}: ${message}`);
57
+ return { ...DEFAULT_CODEX_CONVERSION_CONFIG };
58
+ }
59
+ }
60
+
61
+ export function writeCodexConversionConfig(
62
+ config: CodexConversionConfig,
63
+ configPath: string = getCodexConversionConfigPath(),
64
+ ): { ok: true } | { ok: false; error: string } {
65
+ try {
66
+ mkdirSync(dirname(configPath), { recursive: true });
67
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
68
+ return { ok: true };
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.warn(`[pi-codex-conversion] Failed to write ${configPath}: ${message}`);
72
+ return { ok: false, error: message };
73
+ }
74
+ }
75
+
76
+ export function applyCodexRequestParams(
77
+ payload: unknown,
78
+ config: CodexConversionConfig,
79
+ options: { serviceTier?: boolean; verbosity?: boolean } = { serviceTier: true, verbosity: true },
80
+ ): unknown {
81
+ if (!isObject(payload)) return payload;
82
+ const text = isObject(payload.text) ? payload.text : {};
83
+ return {
84
+ ...payload,
85
+ ...(options.serviceTier && config.fast ? { service_tier: "priority" } : {}),
86
+ ...(options.verbosity ? { text: { ...text, verbosity: config.verbosity } } : {}),
87
+ };
88
+ }
@@ -0,0 +1,23 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isOpenAICodexContext, isResponsesContext } from "./codex-model.ts";
3
+ import { applyCodexRequestParams } from "./config.ts";
4
+ import type { AdapterState } from "./state.ts";
5
+ import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
6
+ import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
7
+ import { shouldUseCodexAdapter } from "./activation.ts";
8
+
9
+ export function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): unknown | undefined {
10
+ if (!shouldUseCodexAdapter(ctx, state.config) || (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx))) {
11
+ return undefined;
12
+ }
13
+
14
+ const isOpenAICodex = isOpenAICodexContext(ctx);
15
+ const webSearchPayload = isOpenAICodex && state.config.webSearch ? rewriteNativeWebSearchTool(payload, ctx.model) : payload;
16
+ const imageGenerationPayload = isOpenAICodex && state.config.imageGeneration
17
+ ? rewriteNativeImageGenerationTool(webSearchPayload, ctx.model)
18
+ : webSearchPayload;
19
+ return applyCodexRequestParams(imageGenerationPayload, state.config, {
20
+ serviceTier: isOpenAICodex,
21
+ verbosity: true,
22
+ });
23
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ export function getCodexSkillPaths(cwd: string, home: string = homedir()): string[] {
6
+ const skillPaths = [join(home, ".agents", "skills")];
7
+ let currentDir = resolve(cwd);
8
+ while (true) {
9
+ skillPaths.push(join(currentDir, ".agents", "skills"));
10
+ const parentDir = resolve(currentDir, "..");
11
+ if (parentDir === currentDir) {
12
+ break;
13
+ }
14
+ currentDir = parentDir;
15
+ }
16
+ return skillPaths.filter((path) => existsSync(path));
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { PromptSkill } from "../prompt/build-system-prompt.ts";
2
+ import type { CodexConversionConfig } from "./config.ts";
3
+
4
+ export interface AdapterState {
5
+ enabled: boolean;
6
+ cwd: string;
7
+ previousToolNames?: string[];
8
+ promptSkills: PromptSkill[];
9
+ config: CodexConversionConfig;
10
+ }
@@ -1,6 +1,19 @@
1
1
  export const STATUS_KEY = "codex-adapter";
2
2
  export const STATUS_TEXT = "\u001b[38;2;0;76;255mCodex adapter\u001b[0m";
3
3
 
4
+ export function buildStatusText(options: { verbosity?: string; webSearch: boolean; imageGeneration: boolean; fast: boolean; useOnAllModels: boolean }): string {
5
+ const extras = [
6
+ options.useOnAllModels ? "all models" : undefined,
7
+ options.webSearch ? "web search" : undefined,
8
+ options.imageGeneration ? "image gen" : undefined,
9
+ options.fast ? "fast" : undefined,
10
+ ]
11
+ .filter(Boolean)
12
+ .join(" • ");
13
+ const verbosity = options.verbosity === "medium" ? "mid" : options.verbosity === "high" ? "hi" : options.verbosity;
14
+ return `${STATUS_TEXT}${verbosity ? ` V: ${verbosity}` : ""}${extras ? ` • ${extras}` : ""}`;
15
+ }
16
+
4
17
  export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
5
18
 
6
19
  export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
@@ -0,0 +1,68 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ normalizeCodexVerbosity,
4
+ readCodexConversionConfig,
5
+ writeCodexConversionConfig,
6
+ type CodexConversionConfig,
7
+ } from "../adapter/config.ts";
8
+ import { syncAdapter } from "../adapter/activation.ts";
9
+ import type { AdapterState } from "../adapter/state.ts";
10
+ import { openCodexSettingsScreen } from "./ui.ts";
11
+
12
+ const CODEX_COMMAND_COMPLETIONS = ["all", "fast", "search", "image", "low", "medium", "high"] as const;
13
+
14
+ export function registerCodexCommand(pi: ExtensionAPI, state: AdapterState): void {
15
+ function saveAndApply(ctx: ExtensionContext, nextConfig: CodexConversionConfig): boolean {
16
+ const writeResult = writeCodexConversionConfig(nextConfig);
17
+ if (!writeResult.ok) {
18
+ ctx.ui.notify(`Failed to save Codex settings: ${writeResult.error}`, "error");
19
+ return false;
20
+ }
21
+ state.config = nextConfig;
22
+ syncAdapter(pi, ctx, state);
23
+ return true;
24
+ }
25
+
26
+ pi.registerCommand("codex", {
27
+ description: "Configure Codex adapter settings",
28
+ getArgumentCompletions: (prefix) =>
29
+ CODEX_COMMAND_COMPLETIONS.filter((item) => item.startsWith(prefix.trim().toLowerCase())).map((value) => ({ label: value, value })),
30
+ handler: async (args, ctx) => {
31
+ state.config = readCodexConversionConfig();
32
+ const arg = args.trim().toLowerCase();
33
+ const nextConfig = getCommandConfigUpdate(arg, state.config);
34
+ if (nextConfig) {
35
+ saveAndApply(ctx, nextConfig);
36
+ return;
37
+ }
38
+
39
+ if (arg) {
40
+ ctx.ui.notify("Usage: /codex, /codex all, /codex fast, /codex search, /codex image, /codex low|medium|high", "warning");
41
+ return;
42
+ }
43
+
44
+ if (!ctx.hasUI) {
45
+ ctx.ui.notify(formatCodexSettings(state.config), "info");
46
+ return;
47
+ }
48
+
49
+ await openCodexSettingsScreen(ctx, {
50
+ initialConfig: state.config,
51
+ onChange: (config) => saveAndApply(ctx, config),
52
+ });
53
+ },
54
+ });
55
+ }
56
+
57
+ function getCommandConfigUpdate(arg: string, config: CodexConversionConfig): CodexConversionConfig | undefined {
58
+ if (arg === "fast") return { ...config, fast: !config.fast };
59
+ if (arg === "all") return { ...config, useOnAllModels: !config.useOnAllModels };
60
+ if (arg === "search") return { ...config, webSearch: !config.webSearch };
61
+ if (arg === "image") return { ...config, imageGeneration: !config.imageGeneration };
62
+ const verbosity = normalizeCodexVerbosity(arg);
63
+ return verbosity ? { ...config, verbosity } : undefined;
64
+ }
65
+
66
+ function formatCodexSettings(config: CodexConversionConfig): string {
67
+ return `Codex settings: all models ${config.useOnAllModels ? "on" : "off"}, fast ${config.fast ? "on" : "off"}, web search ${config.webSearch ? "on" : "off"}, image generation ${config.imageGeneration ? "on" : "off"}, verbosity ${config.verbosity}`;
68
+ }
@@ -0,0 +1,16 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export const GITHUB_URL = "https://github.com/IgorWarzocha/pi-codex-conversion";
4
+ export const CHANGELOG_URL = `${GITHUB_URL}/blob/master/CHANGELOG.md`;
5
+ export const DISCORD_URL = "https://discord.com/channels/1456806362351669492/1482388023994748948";
6
+ export const ISSUE_URL = `${GITHUB_URL}/issues/new`;
7
+
8
+ export function openExternalUrl(url: string): void {
9
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
10
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
11
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
12
+ child.on("error", (error) => {
13
+ console.warn(`[pi-codex-conversion] Failed to open ${url}: ${error.message}`);
14
+ });
15
+ child.unref();
16
+ }
@@ -0,0 +1,92 @@
1
+ import { DynamicBorder, getSettingsListTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Container, SettingsList, Spacer, Text, type SettingItem } from "@earendil-works/pi-tui";
3
+ import { DEFAULT_CODEX_CONVERSION_CONFIG, normalizeCodexVerbosity, type CodexConversionConfig } from "../adapter/config.ts";
4
+ import { CHANGELOG_URL, DISCORD_URL, GITHUB_URL, ISSUE_URL, openExternalUrl } from "./links.ts";
5
+
6
+ export interface CodexSettingsScreenOptions {
7
+ initialConfig: CodexConversionConfig;
8
+ onChange: (nextConfig: CodexConversionConfig) => boolean;
9
+ }
10
+
11
+ export async function openCodexSettingsScreen(ctx: ExtensionContext, options: CodexSettingsScreenOptions): Promise<void> {
12
+ let draft = { ...options.initialConfig };
13
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
14
+ const buildItems = (): SettingItem[] => [
15
+ { id: "useOnAllModels", label: "Use on all models", currentValue: draft.useOnAllModels ? "on" : "off", values: ["off", "on"] },
16
+ { id: "fast", label: "Fast mode", currentValue: draft.fast ? "on" : "off", values: ["off", "on"] },
17
+ { id: "webSearch", label: "Web search", currentValue: draft.webSearch ? "on" : "off", values: ["off", "on"] },
18
+ { id: "imageGeneration", label: "Image generation", currentValue: draft.imageGeneration ? "on" : "off", values: ["off", "on"] },
19
+ { id: "verbosity", label: "Verbosity", currentValue: draft.verbosity, values: ["low", "medium", "high"] },
20
+ ];
21
+
22
+ const container = new Container();
23
+ const panel = new Box(1, 0);
24
+ panel.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
25
+ let settingsList: SettingsList;
26
+ settingsList = new SettingsList(buildItems(), 6, getSettingsListTheme(), (id, value) => {
27
+ const nextDraft = { ...draft };
28
+ const previousValue = buildItems().find((item) => item.id === id)?.currentValue;
29
+ if (id === "useOnAllModels") nextDraft.useOnAllModels = value === "on";
30
+ if (id === "fast") nextDraft.fast = value === "on";
31
+ if (id === "webSearch") nextDraft.webSearch = value === "on";
32
+ if (id === "imageGeneration") nextDraft.imageGeneration = value === "on";
33
+ if (id === "verbosity") nextDraft.verbosity = normalizeCodexVerbosity(value) ?? DEFAULT_CODEX_CONVERSION_CONFIG.verbosity;
34
+ if (options.onChange(nextDraft)) {
35
+ draft = nextDraft;
36
+ } else if (previousValue !== undefined) {
37
+ settingsList.updateValue(id, previousValue);
38
+ }
39
+ tui.requestRender();
40
+ }, () => done(undefined));
41
+ panel.addChild(settingsList);
42
+ panel.addChild(new DynamicBorder((text) => theme.fg("dim", text)));
43
+ panel.addChild(
44
+ new Text(
45
+ [
46
+ `${theme.bold("g")} github ${theme.fg("dim", GITHUB_URL)}`,
47
+ `${theme.bold("c")} changes ${theme.fg("dim", CHANGELOG_URL)}`,
48
+ `${theme.bold("d")} discord ${theme.fg("dim", DISCORD_URL)}`,
49
+ `${theme.bold("i")} issue ${theme.fg("dim", ISSUE_URL)}`,
50
+ ].join("\n"),
51
+ 0,
52
+ 0,
53
+ ),
54
+ );
55
+ panel.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
56
+ container.addChild(new Spacer(1));
57
+ container.addChild(panel);
58
+
59
+ return {
60
+ render: (width: number) => container.render(width),
61
+ invalidate: () => container.invalidate(),
62
+ handleInput: (data: string) => {
63
+ if (handleLinkKey(data, ctx)) return;
64
+ settingsList.handleInput?.(data);
65
+ tui.requestRender();
66
+ },
67
+ };
68
+ });
69
+ }
70
+
71
+ function handleLinkKey(data: string, ctx: ExtensionContext): boolean {
72
+ const target = getLinkTarget(data);
73
+ if (!target) return false;
74
+ openExternalUrl(target.url);
75
+ ctx.ui.notify(target.message, "info");
76
+ return true;
77
+ }
78
+
79
+ function getLinkTarget(data: string): { url: string; message: string } | undefined {
80
+ switch (data) {
81
+ case "g":
82
+ return { url: GITHUB_URL, message: "Opened GitHub" };
83
+ case "c":
84
+ return { url: CHANGELOG_URL, message: "Opened changelog" };
85
+ case "d":
86
+ return { url: DISCORD_URL, message: "Opened Discord" };
87
+ case "i":
88
+ return { url: ISSUE_URL, message: "Opened issue form" };
89
+ default:
90
+ return undefined;
91
+ }
92
+ }
package/src/index.ts CHANGED
@@ -1,19 +1,6 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { existsSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
2
  import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
6
- import {
7
- CORE_ADAPTER_TOOL_NAMES,
8
- DEFAULT_TOOL_NAMES,
9
- IMAGE_GENERATION_TOOL_NAME,
10
- STATUS_KEY,
11
- STATUS_TEXT,
12
- VIEW_IMAGE_TOOL_NAME,
13
- WEB_SEARCH_TOOL_NAME,
14
- } from "./adapter/tool-set.ts";
15
3
  import { clearApplyPatchRenderState, registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
16
- import { isCodexLikeContext, isOpenAICodexContext } from "./adapter/codex-model.ts";
17
4
  import { createExecCommandTracker } from "./tools/exec-command-state.ts";
18
5
  import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
19
6
  import { createExecSessionManager } from "./tools/exec-session-manager.ts";
@@ -22,26 +9,18 @@ import {
22
9
  WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
23
10
  registerOpenAICodexCustomProvider,
24
11
  } from "./providers/openai-codex-custom-provider.ts";
25
- import { registerImageGenerationTool, rewriteNativeImageGenerationTool, supportsNativeImageGeneration } from "./tools/image-generation-tool.ts";
26
- import { buildCodexSystemPrompt, extractPiPromptSkills, resolvePromptSkills, type PromptSkill } from "./prompt/build-system-prompt.ts";
12
+ import { registerImageGenerationTool } from "./tools/image-generation-tool.ts";
13
+ import { buildCodexSystemPrompt, extractPiPromptSkills, resolvePromptSkills } from "./prompt/build-system-prompt.ts";
27
14
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
28
- import {
29
- registerWebSearchTool,
30
- rewriteNativeWebSearchTool,
31
- supportsNativeWebSearch,
32
- WEB_SEARCH_SESSION_NOTE_TYPE,
33
- } from "./tools/web-search-tool.ts";
15
+ import { registerWebSearchTool, WEB_SEARCH_SESSION_NOTE_TYPE } from "./tools/web-search-tool.ts";
34
16
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
35
17
  import { ensureBundledApplyPatchOnPath } from "./tools/apply-patch-binary.ts";
36
-
37
- interface AdapterState {
38
- enabled: boolean;
39
- cwd: string;
40
- previousToolNames?: string[];
41
- promptSkills: PromptSkill[];
42
- }
43
-
44
- const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, WEB_SEARCH_TOOL_NAME, IMAGE_GENERATION_TOOL_NAME, VIEW_IMAGE_TOOL_NAME];
18
+ import { readCodexConversionConfig } from "./adapter/config.ts";
19
+ import { syncAdapter, mergeAdapterTools, restoreTools, stripAdapterTools, shouldUseCodexAdapter } from "./adapter/activation.ts";
20
+ import { rewriteCodexProviderRequest } from "./adapter/provider-request.ts";
21
+ import { getCodexSkillPaths } from "./adapter/skills.ts";
22
+ import type { AdapterState } from "./adapter/state.ts";
23
+ import { registerCodexCommand } from "./codex-settings/command.ts";
45
24
 
46
25
  function getCommandArg(args: unknown): string | undefined {
47
26
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -63,7 +42,7 @@ function isToolCallOnlyAssistantMessage(message: unknown): boolean {
63
42
  export default function codexConversion(pi: ExtensionAPI) {
64
43
  ensureBundledApplyPatchOnPath();
65
44
  const tracker = createExecCommandTracker();
66
- const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [] };
45
+ const state: AdapterState = { enabled: false, cwd: process.cwd(), promptSkills: [], config: readCodexConversionConfig() };
67
46
  const sessions = createExecSessionManager();
68
47
 
69
48
  registerOpenAICodexCustomProvider(pi, {
@@ -74,6 +53,7 @@ export default function codexConversion(pi: ExtensionAPI) {
74
53
  registerWriteStdinTool(pi, sessions);
75
54
  registerImageGenerationTool(pi);
76
55
  registerWebSearchTool(pi);
56
+ registerCodexCommand(pi, state);
77
57
 
78
58
  sessions.onSessionExit((sessionId) => {
79
59
  tracker.recordSessionFinished(sessionId);
@@ -81,6 +61,9 @@ export default function codexConversion(pi: ExtensionAPI) {
81
61
 
82
62
  pi.on("session_start", async (_event, ctx) => {
83
63
  state.cwd = ctx.cwd;
64
+ state.config = readCodexConversionConfig();
65
+ state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
66
+ registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
84
67
  clearApplyPatchRenderState();
85
68
  tracker.clear();
86
69
  syncAdapter(pi, ctx, state);
@@ -93,6 +76,8 @@ export default function codexConversion(pi: ExtensionAPI) {
93
76
 
94
77
  pi.on("model_select", async (_event, ctx) => {
95
78
  state.cwd = ctx.cwd;
79
+ state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
80
+ registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
96
81
  syncAdapter(pi, ctx, state);
97
82
  });
98
83
 
@@ -123,7 +108,7 @@ export default function codexConversion(pi: ExtensionAPI) {
123
108
  });
124
109
 
125
110
  pi.on("before_agent_start", async (event, ctx) => {
126
- if (!isCodexLikeContext(ctx)) {
111
+ if (!shouldUseCodexAdapter(ctx, state.config)) {
127
112
  return undefined;
128
113
  }
129
114
  const skills = resolvePromptSkills(event.systemPromptOptions?.skills, state.promptSkills);
@@ -137,10 +122,7 @@ export default function codexConversion(pi: ExtensionAPI) {
137
122
 
138
123
  pi.on("before_provider_request", async (event, ctx) => {
139
124
  state.cwd = ctx.cwd;
140
- if (!isOpenAICodexContext(ctx)) {
141
- return undefined;
142
- }
143
- return rewriteNativeImageGenerationTool(rewriteNativeWebSearchTool(event.payload, ctx.model), ctx.model);
125
+ return rewriteCodexProviderRequest(event.payload, ctx, state);
144
126
  });
145
127
 
146
128
  pi.on("context", async (event) => {
@@ -158,95 +140,4 @@ export default function codexConversion(pi: ExtensionAPI) {
158
140
  });
159
141
  }
160
142
 
161
- export function getCodexSkillPaths(cwd: string, home: string = homedir()): string[] {
162
- const skillPaths = [join(home, ".agents", "skills")];
163
- let currentDir = resolve(cwd);
164
- while (true) {
165
- skillPaths.push(join(currentDir, ".agents", "skills"));
166
- const parentDir = resolve(currentDir, "..");
167
- if (parentDir === currentDir) {
168
- break;
169
- }
170
- currentDir = parentDir;
171
- }
172
- return skillPaths.filter((path) => existsSync(path));
173
- }
174
-
175
- function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
176
- state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
177
-
178
- registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
179
-
180
- if (isCodexLikeContext(ctx)) {
181
- enableAdapter(pi, ctx, state);
182
- } else {
183
- disableAdapter(pi, ctx, state);
184
- }
185
- }
186
-
187
- function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
188
- const toolNames = mergeAdapterTools(pi.getActiveTools(), getAdapterToolNames(ctx));
189
- if (!state.enabled) {
190
- // Preserve the previous active set once so switching away from Codex-like
191
- // models restores the user's existing Pi tool configuration. Strip adapter
192
- // tools in case a fresh session starts from persisted/mixed active tools.
193
- state.previousToolNames = stripAdapterTools(pi.getActiveTools());
194
- state.enabled = true;
195
- }
196
- pi.setActiveTools(toolNames);
197
- setStatus(ctx, true);
198
- }
199
-
200
- function disableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
201
- const previousToolNames = state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES;
202
- const restoredTools = restoreTools(previousToolNames, pi.getActiveTools());
203
- if (state.enabled || hasAdapterTools(pi.getActiveTools())) {
204
- pi.setActiveTools(restoredTools);
205
- }
206
- if (state.enabled) {
207
- state.enabled = false;
208
- }
209
- setStatus(ctx, false);
210
- }
211
-
212
- function setStatus(ctx: ExtensionContext, enabled: boolean): void {
213
- if (!ctx.hasUI) return;
214
- ctx.ui.setStatus(STATUS_KEY, enabled ? STATUS_TEXT : undefined);
215
- }
216
-
217
- function getAdapterToolNames(ctx: ExtensionContext): string[] {
218
- const toolNames = [...CORE_ADAPTER_TOOL_NAMES];
219
- if (supportsNativeWebSearch(ctx.model)) {
220
- toolNames.push(WEB_SEARCH_TOOL_NAME);
221
- }
222
- if (supportsNativeImageGeneration(ctx.model)) {
223
- toolNames.push(IMAGE_GENERATION_TOOL_NAME);
224
- }
225
- if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
226
- toolNames.push(VIEW_IMAGE_TOOL_NAME);
227
- }
228
- return toolNames;
229
- }
230
-
231
- export function mergeAdapterTools(activeTools: string[], adapterTools: string[]): string[] {
232
- const preservedTools = activeTools.filter((toolName) => !DEFAULT_TOOL_NAMES.includes(toolName) && !ADAPTER_TOOL_NAMES.includes(toolName));
233
- return [...adapterTools, ...preservedTools];
234
- }
235
-
236
- export function restoreTools(previousTools: string[], activeTools: string[]): string[] {
237
- const restored = stripAdapterTools(previousTools);
238
- for (const toolName of activeTools) {
239
- if (!ADAPTER_TOOL_NAMES.includes(toolName) && !restored.includes(toolName)) {
240
- restored.push(toolName);
241
- }
242
- }
243
- return restored;
244
- }
245
-
246
- export function stripAdapterTools(toolNames: string[]): string[] {
247
- return toolNames.filter((toolName) => !ADAPTER_TOOL_NAMES.includes(toolName));
248
- }
249
-
250
- function hasAdapterTools(activeTools: string[]): boolean {
251
- return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
252
- }
143
+ export { getCodexSkillPaths, mergeAdapterTools, restoreTools, stripAdapterTools };