pi-codex-tools 0.3.0 → 0.4.0

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 CHANGED
@@ -6,6 +6,23 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-10-09
10
+
11
+ ### Fixed
12
+
13
+ - Clear theme-colored patch preview caches on invalidation so existing calls repaint after theme changes without another execution.
14
+
15
+ ### Changed
16
+
17
+ - Add `codex_files` namespace metadata and accurate mutation/destruction hints without changing grammar, activation, model-only exposure or native file tools.
18
+ - Remove the obsolete direct-compaction grammar event-bus adapter. Public server-side compaction preserves the ordinary request's grammar declarations.
19
+ - Update the shared Pi development and contract-test baseline to 1.1.0; require Node.js >=22.19.0 to match the host runtime. Pi remains a host-supplied peer dependency.
20
+ - Register `apply_patch` with model-only exposure, excluding nested calls through codemode.
21
+ - Hide selected native `edit` and `write` declarations through Pi's public loadout hook while keeping their implementations, activation, and exposure intact for nested calls.
22
+ - Preserve tool selection across reloads and model switches, including explicit activation and deactivation. Excluded tools remain excluded; older Pi runtimes retain the legacy selection policy.
23
+ - Preserve approval wrappers regardless of extension load order or registration time, and leave native file tools intact when another extension overrides `apply_patch`.
24
+ - On legacy Pi runtimes, hide reactivated file tools again on each supported-model switch while retaining them for restoration on unsupported models.
25
+
9
26
  ## [0.3.0] - 2026-09-18
10
27
 
11
28
  ### Changed
package/CONTRIBUTING.md CHANGED
@@ -33,3 +33,9 @@ Before opening a pull request:
33
33
  ## Code of conduct
34
34
 
35
35
  This project follows the Contributor Covenant Code of Conduct.
36
+ ## Pi 1.1.0 terminal audit
37
+
38
+ From the monorepo root, run `node --import tsx --test tests/tui-contracts.test.mjs`.
39
+ The suite uses actual regular/fullscreen tool shells, theme invalidation, resize,
40
+ Unicode and host-owned output padding. See `tests/TUI_AUDIT.md` for remaining
41
+ physical-terminal and export acceptance checks.
package/README.md CHANGED
@@ -5,10 +5,11 @@ Give grammar-capable OpenAI/Codex models the Codex `apply_patch` tool in Pi with
5
5
  ## What it adds
6
6
 
7
7
  - **Raw `apply_patch`** — sends Codex's Lark grammar as an OpenAI custom tool, so patches are not JSON-wrapped.
8
+ - **Model-only exposure** — `apply_patch` is available directly to the model, never through codemode or other nested tool calls. Requires Pi 0.99.1 or newer.
8
9
  - **Capability-based activation** — requires `openai-codex-responses` or `openai-responses` plus `model.compat.supportsOpenAIGrammarTools === true`; model names alone are never enough.
9
10
  - **Pi-style filesystem access** — accepts relative or absolute paths and follows symlinked files and directories, including macOS `/tmp`. Uses Node filesystem APIs without a native binding or platform gate.
10
11
  - **Validated patches** — limits patches to 1 MiB and target-file reads to 64 MiB, preflights all hunks, and serializes writes with Pi's mutation queue.
11
- - **Model switching** — supported models replace Pi's `edit` and `write` tools with `apply_patch`; other active tools are preserved. Switching back restores only the file tools that were active before the switch.
12
+ - **Native editing through codemode** — supported models see `apply_patch` instead of native `edit` and `write` declarations. Selected native tools remain active and callable through codemode, without replacing their implementations. Model switches and reloads preserve tool selection and approval wrappers. Keep your usual `defaultTools` selection; no separate exposure extension is needed.
12
13
  - **Sequential patch calls** — the extension marks patch execution sequential while leaving provider-side parallel tool calls enabled.
13
14
  - **Streaming progress** — while a patch is generated, the TUI shows a live, color-coded glimpse of the content being written (new-file content, or `+`/`-` lines for updates) plus a running `+added -removed` tally and a per-file roster for multi-file patches. It reuses Pi's shared diff rendering and mirrors the built-in `write`/`edit` previews; patch execution is unchanged.
14
15
 
@@ -26,7 +27,7 @@ pi -e /path/to/pi-mono/packages/pi-codex-tools
26
27
 
27
28
  ## Scope decisions
28
29
 
29
- The current Codex source does not define separate `read_file` or `write_file` tools: file inspection is normally done through shell commands and file mutation through `apply_patch`. This package keeps Pi's bounded `read` and `bash` tools, and uses `apply_patch` in place of Pi's `edit` and `write` tools for supported models. Filesystem access uses the local user's permissions, like native Pi tools; it is not a sandbox. `apply_patch` requires a Pi model runtime that advertises `compat.supportsOpenAIGrammarTools`; older runtimes leave the tool inactive.
30
+ The current Codex source does not define separate `read_file` or `write_file` tools: file inspection is normally done through shell commands and file mutation through `apply_patch`. This package keeps Pi's bounded `read` and `bash` tools, and presents `apply_patch` in place of native `edit` and `write` declarations for supported models. Filesystem access uses the local user's permissions, like native Pi tools; it is not a sandbox. `apply_patch` requires a Pi model runtime that advertises `compat.supportsOpenAIGrammarTools`; older runtimes leave the tool inactive.
30
31
 
31
32
  | Codex surface | Decision |
32
33
  | --- | --- |
@@ -41,15 +42,58 @@ These choices are based on the Codex tool specifications in `codex-rs/core/src/t
41
42
 
42
43
  ## Compatibility notes
43
44
 
45
+ The Pi 1.1.0 metadata contract groups `apply_patch` under `codex_files`, without
46
+ renaming it or changing exposure. Its advisory hints are mutating, destructive,
47
+ non-idempotent, and local-only. There is no public output schema: it intentionally
48
+ remains model-only, not discoverable/callable from scripts. Metadata does not
49
+ replace approvals or change native file-tool implementations.
50
+
51
+ While this package's `apply_patch` is active on Pi 0.99.1 or newer, its public
52
+ `prepareLoadout` hook hides selected native `edit` and `write` declarations from
53
+ model requests. The tools keep their original implementations and `direct`
54
+ exposure, and remain in `pi.getActiveTools()`. Activate `codemode` to call them
55
+ from scripts; `describeTool("edit")` and `describeTool("write")` provide their
56
+ schemas. The `apply_patch` description points to these helpers when codemode is
57
+ active, including in codemode's `on` mode where direct tools are not listed
58
+ inline.
59
+
60
+ Explicitly activating a native file tool makes it callable but does not reveal
61
+ its declaration while `apply_patch` is active. Deactivating it removes nested
62
+ access too. Switching to an unsupported model stops hiding native declarations;
63
+ other loadout hooks, such as codemode's `only` mode, still apply. Reloads preserve
64
+ the active selection without a package-owned snapshot.
65
+
66
+ Tools omitted from `defaultTools` or an explicit `--tools` selection are not
67
+ introduced into codemode. Other extensions' file-tool implementations, including
68
+ approval wrappers registered during or after `session_start`, are not replaced
69
+ or hidden by this package. If another extension replaces `apply_patch`, this
70
+ package's loadout hook does not apply to that replacement.
71
+
72
+ Older Pi runtimes without exposure metadata retain the legacy behavior:
73
+ supported models replace active file tools with `apply_patch`, without
74
+ registering codemode overrides. They do not offer this package's nested native
75
+ editing route; upgrade Pi for that capability. The fallback restores its saved
76
+ file-tool selection when leaving supported models. Older Pi cannot distinguish
77
+ an explicit deactivation of an already-hidden tool from leaving it unchanged.
78
+ To disable such a tool, switch to an unsupported model before changing the
79
+ selection, or upgrade Pi to preserve explicit deactivation while `apply_patch`
80
+ is active.
81
+
44
82
  ### GPT-6 Astra
45
83
 
84
+ A Pi 1.1.0 virtual-model selection does not advertise the physical request's
85
+ grammar capability to this extension. `apply_patch` stays unavailable even when
86
+ the router chooses Astra; selected native `edit` and `write` declarations remain
87
+ available, subject to other loadout hooks. The extension does not infer support
88
+ from a virtual model name or reactivate excluded tools.
89
+
46
90
  Pi 0.85.1's model catalog advertises grammar-tool support for `gpt-6-astra` on both `openai-responses` and `openai-codex-responses`. The extension uses that capability directly, with no model-name allowlist or JSON wrapper. Tests cover the pinned Pi transports, streamed raw calls, execution, and result replay using mocked HTTP responses; they do not certify live account access.
47
91
 
48
92
  The [GPT-6 guide](https://developers.openai.com/api/docs/guides/latest-model?model=gpt-6-astra) also describes async tool calls, mid-turn steering, and reasoning updates. Those belong to the provider/session runtime and are not enabled by this extension. Patch execution remains sequential; provider-side parallel tool calling remains enabled.
49
93
 
50
- With an updated `pi-codex-compaction` installed, the package supplies its owned
51
- grammar metadata through Pi's public event bus. This keeps raw `apply_patch`
52
- calls and results intact in direct Codex compaction requests, including Astra.
94
+ With `pi-codex-compaction` installed, public server-side compaction uses Pi's
95
+ ordinary effective request, including this tool's grammar, calls and results.
96
+ The former direct-compaction grammar event-bus adapter has been removed.
53
97
  No private Pi registry is patched.
54
98
 
55
99
  ### Filesystem behavior
@@ -75,7 +119,17 @@ Like native Pi tools, normal path-based I/O does not protect against another pro
75
119
 
76
120
  These behaviors intentionally match Codex `apply_patch`.
77
121
 
78
- The provider contract is runtime-specific: use Pi 0.83.0 or newer for OpenAI grammar-tool support. For a manual smoke test, start Pi with this extension and a model that advertises `supportsOpenAIGrammarTools`, then ask it to create and update a disposable file through a symlinked directory (on macOS, `/tmp` is suitable). Verify that changes appear as raw `apply_patch` calls, the referent changes, and the symlink remains. Switch to an unsupported model and verify that the original file tools return.
122
+ The provider contract is runtime-specific: use Pi 0.99.1 or newer for model-only tool exposure and the loadout hook; the metadata contract and development/integration tests use Pi 1.1.0.
123
+
124
+ For a manual smoke test:
125
+
126
+ 1. Start Pi with this extension, the normal file tools, `codemode`, and a model that advertises `supportsOpenAIGrammarTools`.
127
+ 2. Ask it to create and update a disposable file through a symlinked directory (on macOS, `/tmp` is suitable). Verify raw `apply_patch` calls, changed referent content, and an intact symlink.
128
+ 3. Verify that `describeTool("edit")` and `describeTool("write")` work inside codemode and that scripts can edit a disposable file. `apply_patch` must not be callable inside codemode.
129
+ 4. Run `/reload` and repeat the nested-editing check.
130
+ 5. Switch to an unsupported model and verify that the selected native file tools remain usable. With codemode in `on` mode, their direct declarations return.
131
+
132
+ Automated contract tests cover provider declarations, codemode's `on` and `only` modes, reloads, exclusions, and approval wrappers registered in either extension load order.
79
133
 
80
134
  ## Development
81
135
 
package/SECURITY.md CHANGED
@@ -19,6 +19,19 @@ Report privately through [GitHub Security Advisories](https://github.com/jvm/pi-
19
19
 
20
20
  Pi extensions execute with the same permissions as the local user running Pi. Review installed extensions and only install packages from sources you trust.
21
21
 
22
+ On modern Pi, this package hides selected native `edit` and `write` declarations
23
+ with the public loadout hook. It does not replace their implementations or alter
24
+ their activation or exposure. Approval wrappers remain effective even when
25
+ registered by a later-loaded extension during or after session startup. Nested
26
+ calls use Pi's normal tool validation and `tool_call`/`tool_result` hooks.
27
+
28
+ Hiding a declaration is presentation, not an execution or permission boundary:
29
+ active tools remain executable. Tools excluded from the active selection are
30
+ not introduced into codemode by this package. Older Pi runtimes without exposure
31
+ metadata retain the legacy tool-selection policy without nested-editing
32
+ overrides. Tool selections are not an OS-level sandbox; `apply_patch` itself can
33
+ modify files when enabled.
34
+
22
35
  `apply_patch` does not access the network or credential APIs. Like Pi's native `edit` and `write` tools, it accepts relative or absolute paths, follows symlinks for reads/writes, and can modify files outside the current working directory with the local user's permissions. It can read credential-containing files when a patch targets them. Deleting a symlink removes the link, not its referent; moving a symlink source copies its referent's updated content and removes the source link.
23
36
 
24
37
  The tool uses Node filesystem APIs without a platform-specific native binding. This deliberately replaces the previous no-follow policy with Pi-style filesystem access. Path canonicalization is used for preflight identity and queue keys, not as a security boundary. There is no workspace confinement or protection against another process swapping path components between resolution and I/O. Use an OS-level sandbox or restricted user account when filesystem isolation is required.
@@ -12,6 +12,11 @@ class ApplyPatchCallComponent extends Text {
12
12
  constructor() {
13
13
  super("", 0, 0);
14
14
  }
15
+
16
+ override invalidate(): void {
17
+ this.cache = undefined;
18
+ super.invalidate();
19
+ }
15
20
  }
16
21
 
17
22
  function readPatchArg(args: unknown): string {
@@ -19,10 +24,9 @@ function readPatchArg(args: unknown): string {
19
24
  }
20
25
 
21
26
  const APPLY_PATCH = "apply_patch";
22
- const EDIT = "edit";
23
- const WRITE = "write";
24
- const REPLACED_TOOLS = [EDIT, WRITE] as const;
25
- type ReplacedTool = (typeof REPLACED_TOOLS)[number];
27
+ const FILE_TOOLS = ["edit", "write"] as const;
28
+ type FileTool = (typeof FILE_TOOLS)[number];
29
+ const APPLY_PATCH_DESCRIPTION = "Apply a Codex patch to files. This is a FREEFORM tool: send the patch text directly, never as JSON.";
26
30
 
27
31
  const APPLY_PATCH_PARAMETERS = createFreeformInputSchema(
28
32
  "patch",
@@ -38,8 +42,15 @@ export default function piCodexTools(pi: ExtensionAPI): void {
38
42
 
39
43
  registerGrammarTool({
40
44
  name: APPLY_PATCH,
45
+ namespace: {
46
+ name: "codex_files",
47
+ description: "Direct model-only grammar-based file patches.",
48
+ instructions: "apply_patch stays model-only and requires a grammar-capable OpenAI model. Send raw patch text directly; never call it from codemode or replace native file tools to alter visibility. File changes may overwrite or delete local data and remain subject to approval hooks.",
49
+ },
50
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
51
+ exposure: "model-only",
41
52
  label: APPLY_PATCH,
42
- description: "Apply a Codex patch to files. This is a FREEFORM tool: send the patch text directly, never as JSON.",
53
+ description: APPLY_PATCH_DESCRIPTION,
43
54
  promptSnippet: "Apply Codex-format file patches without JSON wrapping",
44
55
  promptGuidelines: [
45
56
  "Use apply_patch for file changes when it is available.",
@@ -50,6 +61,26 @@ export default function piCodexTools(pi: ExtensionAPI): void {
50
61
  parameters: APPLY_PATCH_PARAMETERS,
51
62
  constrainedSampling: createOpenAILarkSampling(APPLY_PATCH_GRAMMAR),
52
63
  executionMode: "sequential",
64
+ prepareLoadout(loadout) {
65
+ const tools = pi.getAllTools();
66
+ // Hide declarations, not implementations or activation. Registering native
67
+ // replacements would outrank approval tools registered by later extensions.
68
+ const hiddenDeclarations = FILE_TOOLS.filter((name) =>
69
+ loadout.declared.some((tool) => tool.name === name)
70
+ && tools.some((tool) => tool.name === name && tool.sourceInfo?.path === `builtin:${name}`),
71
+ );
72
+ if (hiddenDeclarations.length === 0) return;
73
+ return {
74
+ hiddenDeclarations,
75
+ // Codemode's "on" mode does not list direct tools in its own description.
76
+ // Keep the hidden tools discoverable without overriding codemode's hook.
77
+ descriptions: loadout.declared.some((tool) => tool.name === "codemode") ? {
78
+ [APPLY_PATCH]: `${APPLY_PATCH_DESCRIPTION}\n\nNative ${hiddenDeclarations.join(" and ")} remain callable through codemode. Use ${
79
+ hiddenDeclarations.map((name) => `describeTool("${name}")`).join(" and ")
80
+ } for their schemas and usage guidance.`,
81
+ } : undefined,
82
+ };
83
+ },
53
84
  renderCall(args, theme, context) {
54
85
  const component =
55
86
  context.lastComponent instanceof ApplyPatchCallComponent ? context.lastComponent : new ApplyPatchCallComponent();
@@ -89,43 +120,31 @@ export default function piCodexTools(pi: ExtensionAPI): void {
89
120
  },
90
121
  });
91
122
 
92
- let replacedToolsWasActive: Record<ReplacedTool, boolean> | undefined;
93
-
94
- pi.events?.on("pi-codex-compaction:tools:v1", (value) => {
95
- const data = value as {
96
- model?: ExtensionContext["model"];
97
- tools?: Array<{ name: string; parameters: unknown; constrainedSampling?: OpenAIGrammarSampling }>;
98
- } | undefined;
99
- if (!data || !supportsOpenAIGrammarTools(data.model) || !Array.isArray(data.tools)) return;
100
- for (const tool of data.tools) {
101
- // Do not attach our grammar to another extension's apply_patch override.
102
- if (tool.name === APPLY_PATCH && tool.parameters === APPLY_PATCH_PARAMETERS) {
103
- tool.constrainedSampling = createOpenAILarkSampling(APPLY_PATCH_GRAMMAR);
104
- }
105
- }
106
- });
123
+ let legacyReplacedTools: Set<FileTool> | undefined;
107
124
 
108
125
  function synchronizeTools(ctx: ExtensionContext): void {
109
126
  if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
110
127
 
111
128
  const active = new Set(pi.getActiveTools());
129
+ const tools = typeof pi.getAllTools === "function" ? pi.getAllTools() : [];
130
+ // Exposure metadata and prepareLoadout arrived together. Do not infer runtime
131
+ // support or this extension's ownership from an overridable apply_patch tool.
132
+ const supportsLoadout = tools.some((tool) => tool.exposure !== undefined);
112
133
  if (supportsOpenAIGrammarTools(ctx.model)) {
113
- if (replacedToolsWasActive === undefined) {
114
- replacedToolsWasActive = {
115
- edit: active.has(EDIT),
116
- write: active.has(WRITE),
117
- };
134
+ if (!supportsLoadout) {
135
+ // Older runtimes cannot hide declarations independently of activation.
136
+ // Re-hide later activations on every supported-model switch, retaining
137
+ // them for restoration when leaving the supported models.
138
+ legacyReplacedTools ??= new Set();
139
+ for (const name of FILE_TOOLS) {
140
+ if (active.delete(name)) legacyReplacedTools.add(name);
141
+ }
118
142
  }
119
- for (const tool of REPLACED_TOOLS) active.delete(tool);
120
143
  active.add(APPLY_PATCH);
121
144
  } else {
122
145
  active.delete(APPLY_PATCH);
123
- if (replacedToolsWasActive) {
124
- for (const tool of REPLACED_TOOLS) {
125
- if (replacedToolsWasActive[tool]) active.add(tool);
126
- }
127
- }
128
- replacedToolsWasActive = undefined;
146
+ for (const name of legacyReplacedTools ?? []) active.add(name);
147
+ legacyReplacedTools = undefined;
129
148
  }
130
149
  pi.setActiveTools([...active]);
131
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codex-tools",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Codex-compatible apply_patch tooling for Pi's grammar-capable OpenAI models.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -56,19 +56,19 @@
56
56
  "typebox": "*"
57
57
  },
58
58
  "devDependencies": {
59
- "@earendil-works/pi-ai": "^0.85.1",
60
- "@earendil-works/pi-coding-agent": "^0.85.1",
61
- "@earendil-works/pi-tui": "^0.85.1",
62
- "@types/node": "^26.2.0",
63
- "tsx": "^4.23.5",
64
- "typebox": "^1.3.10",
59
+ "@earendil-works/pi-ai": "1.1.0",
60
+ "@earendil-works/pi-coding-agent": "1.1.0",
61
+ "@earendil-works/pi-tui": "1.1.0",
62
+ "@types/node": "^26.6.3",
63
+ "tsx": "^4.23.15",
64
+ "typebox": "^1.3.34",
65
65
  "typescript": "^7.0.2"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"
69
69
  },
70
70
  "engines": {
71
- "node": ">=20.6.0"
71
+ "node": ">=22.19.0"
72
72
  },
73
73
  "dependencies": {
74
74
  "@mocito/install-telemetry": "0.1.1"