@howaboua/pi-codex-conversion 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -4,7 +4,7 @@ Codex-oriented adapter for [Pi](https://github.com/badlogic/pi-mono).
4
4
 
5
5
  This package replaces Pi's default Codex/GPT experience with a narrower Codex-like surface while staying close to Pi's own runtime and prompt construction:
6
6
 
7
- - swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, and `view_image`
7
+ - swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, `view_image`, and native OpenAI Codex Responses `web_search` on `openai-codex`
8
8
  - preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
9
9
  - renders exec activity with Codex-style command and background-terminal labels
10
10
 
@@ -18,6 +18,7 @@ When the adapter is active, the LLM sees these tools:
18
18
  - `write_stdin` — continue or poll a running exec session
19
19
  - `apply_patch` — patch tool
20
20
  - `view_image` — image-only wrapper around Pi's native image reading, enabled only for image-capable models
21
+ - `web_search` — native OpenAI Codex Responses web search, enabled only on the `openai-codex` provider
21
22
 
22
23
  Notably:
23
24
 
@@ -53,6 +54,7 @@ npm run check
53
54
  - `write_stdin({ session_id, chars: "" })` renders like `Waited for background terminal`
54
55
  - `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
55
56
  - `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
57
+ - `web_search` is surfaced only on `openai-codex`, and the adapter rewrites it into the native OpenAI Responses `type: "web_search"` payload instead of executing a local function tool
56
58
 
57
59
  Raw command output is still available by expanding the tool result.
58
60
 
@@ -90,6 +92,7 @@ That keeps the prompt much closer to `pi-mono` while still steering the model to
90
92
  - Adapter mode activates automatically for OpenAI `gpt*` and `codex*` models.
91
93
  - When you switch away from those models, Pi restores the previous active tool set.
92
94
  - `view_image` resolves paths against the active session cwd and only exposes `detail: "original"` for Codex-family image-capable models.
95
+ - `web_search` is exposed only for the `openai-codex` provider and is forwarded as the native OpenAI Codex Responses web search tool.
93
96
  - `apply_patch` paths stay restricted to the current working directory.
94
97
  - `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
95
98
  - PTY output handling applies basic terminal rewrite semantics (`\r`, `\b`, erase-in-line, and common escape cleanup) so interactive redraws replay sensibly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -6,6 +6,11 @@ export interface CodexLikeModelDescriptor {
6
6
  id: string;
7
7
  }
8
8
 
9
+ export function isOpenAICodexModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
10
+ if (!model) return false;
11
+ return (model.provider ?? "").toLowerCase() === "openai-codex";
12
+ }
13
+
9
14
  // Keep model detection intentionally conservative. The adapter replaces the
10
15
  // system prompt and tool surface, so false positives are worse than misses.
11
16
  export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
@@ -14,9 +19,14 @@ export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null
14
19
  const provider = (model.provider ?? "").toLowerCase();
15
20
  const api = (model.api ?? "").toLowerCase();
16
21
  const id = (model.id ?? "").toLowerCase();
17
- return provider.includes("codex") || api.includes("codex") || id.includes("codex") || (provider.includes("openai") && id.includes("gpt"));
22
+ const isCopilotGpt = (provider.includes("copilot") || api.includes("copilot")) && id.includes("gpt");
23
+ return provider.includes("codex") || api.includes("codex") || id.includes("codex") || (provider.includes("openai") && id.includes("gpt")) || isCopilotGpt;
18
24
  }
19
25
 
20
26
  export function isCodexLikeContext(ctx: ExtensionContext): boolean {
21
27
  return isCodexLikeModel(ctx.model);
22
28
  }
29
+
30
+ export function isOpenAICodexContext(ctx: ExtensionContext): boolean {
31
+ return isOpenAICodexModel(ctx.model);
32
+ }
@@ -5,3 +5,4 @@ export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
5
5
 
6
6
  export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
7
7
  export const VIEW_IMAGE_TOOL_NAME = "view_image";
8
+ export const WEB_SEARCH_TOOL_NAME = "web_search";
package/src/index.ts CHANGED
@@ -1,21 +1,30 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME } from "./adapter/tool-set.ts";
2
+ import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
3
3
  import { registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
4
- import { isCodexLikeContext } from "./adapter/codex-model.ts";
4
+ import { isCodexLikeContext, isOpenAICodexContext } from "./adapter/codex-model.ts";
5
5
  import { createExecCommandTracker } from "./tools/exec-command-state.ts";
6
6
  import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
7
7
  import { createExecSessionManager } from "./tools/exec-session-manager.ts";
8
8
  import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from "./prompt/build-system-prompt.ts";
9
9
  import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
10
+ import {
11
+ WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
12
+ payloadContainsWebSearchTool,
13
+ registerWebSearchMessageRenderer,
14
+ registerWebSearchTool,
15
+ rewriteNativeWebSearchTool,
16
+ supportsNativeWebSearch,
17
+ } from "./tools/web-search-tool.ts";
10
18
  import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
11
19
 
12
20
  interface AdapterState {
13
21
  enabled: boolean;
14
22
  previousToolNames?: string[];
15
23
  promptSkills: PromptSkill[];
24
+ pendingWebSearchCount: number;
16
25
  }
17
26
 
18
- const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
27
+ const ADAPTER_TOOL_NAMES = [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME, WEB_SEARCH_TOOL_NAME];
19
28
 
20
29
  function getCommandArg(args: unknown): string | undefined {
21
30
  if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
@@ -26,12 +35,14 @@ function getCommandArg(args: unknown): string | undefined {
26
35
 
27
36
  export default function codexConversion(pi: ExtensionAPI) {
28
37
  const tracker = createExecCommandTracker();
29
- const state: AdapterState = { enabled: false, promptSkills: [] };
38
+ const state: AdapterState = { enabled: false, promptSkills: [], pendingWebSearchCount: 0 };
30
39
  const sessions = createExecSessionManager();
31
40
 
32
41
  registerApplyPatchTool(pi);
33
42
  registerExecCommandTool(pi, tracker, sessions);
34
43
  registerWriteStdinTool(pi, sessions);
44
+ registerWebSearchTool(pi);
45
+ registerWebSearchMessageRenderer(pi);
35
46
 
36
47
  sessions.onSessionExit((_sessionId, command) => {
37
48
  tracker.recordCommandFinished(command);
@@ -72,6 +83,41 @@ export default function codexConversion(pi: ExtensionAPI) {
72
83
  }),
73
84
  };
74
85
  });
86
+
87
+ pi.on("context", async (event) => {
88
+ return {
89
+ messages: event.messages.filter(
90
+ (message) => !(message.role === "custom" && message.customType === WEB_SEARCH_ACTIVITY_MESSAGE_TYPE),
91
+ ),
92
+ };
93
+ });
94
+
95
+ pi.on("turn_start", async () => {
96
+ state.pendingWebSearchCount = 0;
97
+ });
98
+
99
+ pi.on("before_provider_request", async (event, ctx) => {
100
+ if (!isOpenAICodexContext(ctx)) {
101
+ return undefined;
102
+ }
103
+ if (payloadContainsWebSearchTool(event.payload)) {
104
+ state.pendingWebSearchCount += 1;
105
+ }
106
+ return rewriteNativeWebSearchTool(event.payload, ctx.model);
107
+ });
108
+
109
+ pi.on("agent_end", async () => {
110
+ if (state.pendingWebSearchCount <= 0) {
111
+ return;
112
+ }
113
+ pi.sendMessage({
114
+ customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
115
+ content: "",
116
+ display: true,
117
+ details: { count: state.pendingWebSearchCount },
118
+ });
119
+ state.pendingWebSearchCount = 0;
120
+ });
75
121
  }
76
122
 
77
123
  function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
@@ -99,9 +145,12 @@ function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterSt
99
145
  }
100
146
 
101
147
  function disableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
148
+ const previousToolNames = state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES;
149
+ const restoredTools = restoreTools(previousToolNames, pi.getActiveTools());
150
+ if (state.enabled || hasAdapterTools(pi.getActiveTools())) {
151
+ pi.setActiveTools(restoredTools);
152
+ }
102
153
  if (state.enabled) {
103
- const previousToolNames = state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES;
104
- pi.setActiveTools(restoreTools(previousToolNames, pi.getActiveTools()));
105
154
  state.enabled = false;
106
155
  }
107
156
  setStatus(ctx, false);
@@ -113,10 +162,14 @@ function setStatus(ctx: ExtensionContext, enabled: boolean): void {
113
162
  }
114
163
 
115
164
  function getAdapterToolNames(ctx: ExtensionContext): string[] {
165
+ const toolNames = [...CORE_ADAPTER_TOOL_NAMES];
116
166
  if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
117
- return [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
167
+ toolNames.push(VIEW_IMAGE_TOOL_NAME);
168
+ }
169
+ if (supportsNativeWebSearch(ctx.model)) {
170
+ toolNames.push(WEB_SEARCH_TOOL_NAME);
118
171
  }
119
- return [...CORE_ADAPTER_TOOL_NAMES];
172
+ return toolNames;
120
173
  }
121
174
 
122
175
  export function mergeAdapterTools(activeTools: string[], adapterTools: string[]): string[] {
@@ -133,3 +186,7 @@ export function restoreTools(previousTools: string[], activeTools: string[]): st
133
186
  }
134
187
  return restored;
135
188
  }
189
+
190
+ function hasAdapterTools(activeTools: string[]): boolean {
191
+ return activeTools.some((toolName) => ADAPTER_TOOL_NAMES.includes(toolName));
192
+ }
@@ -32,6 +32,14 @@ export function renderWriteStdinCall(
32
32
  return text;
33
33
  }
34
34
 
35
+ export function renderWebSearchActivity(count: number, theme: RenderTheme, expanded = false): string {
36
+ let text = `${theme.fg("dim", "•")} ${theme.bold("Searched the web")}`;
37
+ if (expanded && count > 1) {
38
+ text += `\n${theme.fg("dim", " └ ")}${theme.fg("muted", `${count} web searches in this turn`)}`;
39
+ }
40
+ return text;
41
+ }
42
+
35
43
  function renderExplorationText(actions: ShellAction[], state: ExecCommandStatus, theme: RenderTheme): string {
36
44
  const header = state === "running" ? "Exploring" : "Explored";
37
45
  let text = `${theme.fg("dim", "•")} ${theme.bold(header)}`;
@@ -0,0 +1,123 @@
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Box, Text } from "@mariozechner/pi-tui";
4
+ import { isOpenAICodexModel } from "../adapter/codex-model.ts";
5
+ import { renderWebSearchActivity } from "./codex-rendering.ts";
6
+
7
+ export const WEB_SEARCH_UNSUPPORTED_MESSAGE = "web_search is only available with the openai-codex provider";
8
+ const WEB_SEARCH_LOCAL_EXECUTION_MESSAGE =
9
+ "web_search is a native openai-codex provider tool and should not execute locally";
10
+ export const WEB_SEARCH_ACTIVITY_MESSAGE_TYPE = "codex-web-search";
11
+
12
+ const WEB_SEARCH_PARAMETERS = Type.Object({}, { additionalProperties: false });
13
+
14
+ interface FunctionToolPayload {
15
+ type?: unknown;
16
+ name?: unknown;
17
+ }
18
+
19
+ interface ResponsesPayload {
20
+ tools?: unknown[];
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface WebSearchActivityDetails {
25
+ count: number;
26
+ }
27
+
28
+ export function supportsNativeWebSearch(model: ExtensionContext["model"]): boolean {
29
+ return isOpenAICodexModel(model);
30
+ }
31
+
32
+ function isWebSearchFunctionTool(tool: unknown): tool is FunctionToolPayload {
33
+ return !!tool && typeof tool === "object" && (tool as FunctionToolPayload).type === "function" && (tool as FunctionToolPayload).name === "web_search";
34
+ }
35
+
36
+ export function rewriteNativeWebSearchTool(payload: unknown, model: ExtensionContext["model"]): unknown {
37
+ if (!supportsNativeWebSearch(model) || !payload || typeof payload !== "object") {
38
+ return payload;
39
+ }
40
+
41
+ const tools = (payload as ResponsesPayload).tools;
42
+ if (!Array.isArray(tools)) {
43
+ return payload;
44
+ }
45
+
46
+ let rewritten = false;
47
+ const nextTools = tools.map((tool) => {
48
+ if (!isWebSearchFunctionTool(tool)) {
49
+ return tool;
50
+ }
51
+ rewritten = true;
52
+ // Match Codex's native tool shape rather than exposing a synthetic function tool.
53
+ return {
54
+ type: "web_search",
55
+ external_web_access: true,
56
+ };
57
+ });
58
+
59
+ if (!rewritten) {
60
+ return payload;
61
+ }
62
+
63
+ return {
64
+ ...(payload as ResponsesPayload),
65
+ tools: nextTools,
66
+ };
67
+ }
68
+
69
+ export function payloadContainsWebSearchTool(payload: unknown): boolean {
70
+ if (!payload || typeof payload !== "object") {
71
+ return false;
72
+ }
73
+ const tools = (payload as ResponsesPayload).tools;
74
+ if (!Array.isArray(tools)) {
75
+ return false;
76
+ }
77
+ return tools.some(
78
+ (tool) =>
79
+ !!tool &&
80
+ typeof tool === "object" &&
81
+ (("type" in tool && (tool as { type?: unknown }).type === "web_search") || isWebSearchFunctionTool(tool)),
82
+ );
83
+ }
84
+
85
+ export function createWebSearchTool(): ToolDefinition<typeof WEB_SEARCH_PARAMETERS> {
86
+ return {
87
+ name: "web_search",
88
+ label: "web_search",
89
+ description: "Search the internet for sources related to the prompt.",
90
+ promptSnippet: "Search the internet for sources related to the prompt.",
91
+ parameters: WEB_SEARCH_PARAMETERS,
92
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
93
+ if (!supportsNativeWebSearch(ctx.model)) {
94
+ throw new Error(WEB_SEARCH_UNSUPPORTED_MESSAGE);
95
+ }
96
+ throw new Error(WEB_SEARCH_LOCAL_EXECUTION_MESSAGE);
97
+ },
98
+ renderCall(_args, theme) {
99
+ return new Text(`${theme.fg("toolTitle", theme.bold("web_search"))}`, 0, 0);
100
+ },
101
+ renderResult(result, { expanded }, theme) {
102
+ if (!expanded) {
103
+ return undefined;
104
+ }
105
+ const textBlock = result.content.find((item) => item.type === "text");
106
+ const text = textBlock?.type === "text" ? textBlock.text : "(no output)";
107
+ return new Text(theme.fg("dim", text), 0, 0);
108
+ },
109
+ };
110
+ }
111
+
112
+ export function registerWebSearchTool(pi: ExtensionAPI): void {
113
+ pi.registerTool(createWebSearchTool());
114
+ }
115
+
116
+ export function registerWebSearchMessageRenderer(pi: ExtensionAPI): void {
117
+ pi.registerMessageRenderer<WebSearchActivityDetails>(WEB_SEARCH_ACTIVITY_MESSAGE_TYPE, (message, { expanded }, theme) => {
118
+ const count = typeof message.details?.count === "number" ? message.details.count : 1;
119
+ const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
120
+ box.addChild(new Text(renderWebSearchActivity(count, theme, expanded), 0, 0));
121
+ return box;
122
+ });
123
+ }