codex-computer-use-mcp 0.3.4 → 0.5.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.
Files changed (51) hide show
  1. package/README.md +39 -117
  2. package/SECURITY.md +5 -63
  3. package/dist/audit.d.ts +1 -7
  4. package/dist/audit.js +4 -12
  5. package/dist/audit.js.map +1 -1
  6. package/dist/code-executor.d.ts +23 -0
  7. package/dist/code-executor.js +216 -0
  8. package/dist/code-executor.js.map +1 -0
  9. package/dist/code-worker.d.ts +1 -0
  10. package/dist/code-worker.js +120 -0
  11. package/dist/code-worker.js.map +1 -0
  12. package/dist/direct-broker.d.ts +18 -30
  13. package/dist/direct-broker.js +87 -161
  14. package/dist/direct-broker.js.map +1 -1
  15. package/dist/direct-service.d.ts +13 -21
  16. package/dist/direct-service.js +82 -299
  17. package/dist/direct-service.js.map +1 -1
  18. package/dist/mcp-elicitation.d.ts +1 -2
  19. package/dist/mcp-elicitation.js +18 -23
  20. package/dist/mcp-elicitation.js.map +1 -1
  21. package/dist/mcp-server.js +34 -38
  22. package/dist/mcp-server.js.map +1 -1
  23. package/dist/session-executor.d.ts +22 -0
  24. package/dist/session-executor.js +89 -0
  25. package/dist/session-executor.js.map +1 -0
  26. package/dist/tools.d.ts +743 -81
  27. package/dist/tools.js +55 -178
  28. package/dist/tools.js.map +1 -1
  29. package/dist/version.d.ts +1 -0
  30. package/dist/version.js +3 -0
  31. package/dist/version.js.map +1 -0
  32. package/integrations/pi/index.ts +146 -261
  33. package/package.json +13 -26
  34. package/ARCHITECTURE.md +0 -162
  35. package/CHANGELOG.md +0 -19
  36. package/CONTRIBUTING.md +0 -33
  37. package/MIGRATION.md +0 -134
  38. package/PROOF.md +0 -166
  39. package/dist/config.d.ts +0 -12
  40. package/dist/config.js +0 -10
  41. package/dist/config.js.map +0 -1
  42. package/dist/lock.d.ts +0 -17
  43. package/dist/lock.js +0 -137
  44. package/dist/lock.js.map +0 -1
  45. package/dist/system.d.ts +0 -25
  46. package/dist/system.js +0 -213
  47. package/dist/system.js.map +0 -1
  48. package/docs/adr/0001-standalone-code-mode-mcp.md +0 -45
  49. package/integrations/pi/README.md +0 -73
  50. package/integrations/pi/mcp.json.example +0 -15
  51. package/npm-shrinkwrap.json +0 -4827
@@ -1,237 +1,128 @@
1
+ import { mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
1
3
  import path from "node:path";
2
- import { StringEnum } from "@earendil-works/pi-ai";
3
- import { getAgentDir, truncateHead, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ DEFAULT_MAX_BYTES,
6
+ DEFAULT_MAX_LINES,
7
+ formatSize,
8
+ getAgentDir,
9
+ truncateHead,
10
+ type ExtensionAPI,
11
+ } from "@earendil-works/pi-coding-agent";
4
12
  import { Text } from "@earendil-works/pi-tui";
5
- import { Type, type TSchema } from "typebox";
6
- import { executeDirectTool, getDirectStatus } from "../../dist/direct-service.js";
13
+ import { z } from "zod";
14
+ import { getDirectStatus } from "../../dist/direct-service.js";
7
15
  import {
8
- createOfficialDirectToolSession,
9
16
  type DirectBrokerElicitationRequest,
10
17
  type DirectBrokerElicitationResponse,
11
- type OfficialDirectToolSession,
12
18
  } from "../../dist/direct-broker.js";
13
- import { OFFICIAL_TOOL_METADATA, type DirectMethod } from "../../dist/tools.js";
14
-
15
- const App = Type.String({ description: "App name, full app path, or unambiguous bundle identifier" });
16
-
17
- const ToolParameters: Record<DirectMethod, TSchema> = {
18
- list_apps: Type.Object({}, { additionalProperties: false }),
19
- get_app_state: Type.Object({ app: App }, { additionalProperties: false }),
20
- click: Type.Object({
21
- app: App,
22
- click_count: Type.Optional(Type.Integer({ description: "Number of clicks. Defaults to 1" })),
23
- element_index: Type.Optional(Type.String({ description: "Element index to click" })),
24
- mouse_button: Type.Optional(StringEnum(["left", "right", "middle"] as const, { description: "Mouse button to click. Defaults to left." })),
25
- x: Type.Optional(Type.Number({ description: "X coordinate in screenshot pixel coordinates" })),
26
- y: Type.Optional(Type.Number({ description: "Y coordinate in screenshot pixel coordinates" })),
27
- }, { additionalProperties: false }),
28
- perform_secondary_action: Type.Object({
29
- app: App,
30
- element_index: Type.String({ description: "Element identifier" }),
31
- action: Type.String({ description: "Secondary accessibility action name" }),
32
- }, { additionalProperties: false }),
33
- set_value: Type.Object({
34
- app: App,
35
- element_index: Type.String({ description: "Element identifier" }),
36
- value: Type.String({ description: "Value to assign" }),
37
- }, { additionalProperties: false }),
38
- select_text: Type.Object({
39
- app: Type.String({ description: "App name or bundle identifier" }),
40
- element_index: Type.String({ description: "Text element identifier" }),
41
- text: Type.String({ description: "Target text as shown in the accessibility tree" }),
42
- prefix: Type.Optional(Type.String({ description: "Optional text immediately before the target, used to disambiguate repeated matches" })),
43
- selection: Type.Optional(StringEnum(["text", "cursor_before", "cursor_after"] as const, { description: "Whether to select the text or place the cursor before or after it. Defaults to text." })),
44
- suffix: Type.Optional(Type.String({ description: "Optional text immediately after the target, used to disambiguate repeated matches" })),
45
- }, { additionalProperties: false }),
46
- scroll: Type.Object({
47
- app: App,
48
- element_index: Type.String({ description: "Element identifier" }),
49
- direction: Type.String({ description: "Scroll direction: up, down, left, or right" }),
50
- pages: Type.Optional(Type.Number({ description: "Number of pages to scroll. Fractional values are supported. Defaults to 1" })),
51
- }, { additionalProperties: false }),
52
- drag: Type.Object({
53
- app: App,
54
- from_x: Type.Number({ description: "Start X coordinate" }),
55
- from_y: Type.Number({ description: "Start Y coordinate" }),
56
- to_x: Type.Number({ description: "End X coordinate" }),
57
- to_y: Type.Number({ description: "End Y coordinate" }),
58
- }, { additionalProperties: false }),
59
- press_key: Type.Object({ app: App, key: Type.String({ description: "Key or key combination to press" }) }, { additionalProperties: false }),
60
- type_text: Type.Object({ app: App, text: Type.String({ description: "Literal text to type" }) }, { additionalProperties: false }),
61
- };
62
-
63
- function toolDescription(method: DirectMethod): string {
64
- return OFFICIAL_TOOL_METADATA[method].description;
65
- }
66
-
67
- function titleFor(method: DirectMethod): string {
68
- return method.split("_").map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join(" ");
69
- }
70
-
71
- const INSPECTION_METHODS = new Set<DirectMethod>(["list_apps", "get_app_state"]);
72
- export const INSPECTION_TOOL_NAMES = [
73
- "computer_use_list_apps",
74
- "computer_use_get_app_state",
75
- ] as const;
76
- export const INTERACTION_TOOL_NAMES = [
77
- "computer_use_click",
78
- "computer_use_perform_secondary_action",
79
- "computer_use_set_value",
80
- "computer_use_select_text",
81
- "computer_use_scroll",
82
- "computer_use_drag",
83
- "computer_use_press_key",
84
- "computer_use_type_text",
85
- ] as const;
86
-
87
- export const COMPUTER_USE_TOOL_NAMES = [
88
- ...INSPECTION_TOOL_NAMES,
89
- ...INTERACTION_TOOL_NAMES,
90
- ] as const;
91
-
92
- export function setInitialComputerUseTools(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">): void {
93
- pi.setActiveTools([...new Set([...pi.getActiveTools(), ...COMPUTER_USE_TOOL_NAMES])]);
94
- }
95
-
96
- interface SessionExecutorDependencies {
97
- createSession?: typeof createOfficialDirectToolSession;
98
- executeTool?: typeof executeDirectTool;
19
+ import { ComputerUseCodeExecutor } from "../../dist/code-executor.js";
20
+ import { type JsonObject } from "../../dist/tools.js";
21
+
22
+ const jsonObjectSchema = z.record(z.string(), z.json());
23
+ const codeParameters = {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ required: ["code"],
27
+ properties: {
28
+ code: {
29
+ type: "string",
30
+ description: "JavaScript body to execute. Use await sky.<method>(args), emit(value), emitImage(screenshot), and store for state shared across calls.",
31
+ },
32
+ },
33
+ } as const;
34
+
35
+ const codeDescription = `Run JavaScript that composes OpenAI's official signed macOS Computer Use methods in one call. No nested model is used.
36
+
37
+ Available globals:
38
+ - sky.list_apps() -> text app inventory
39
+ - sky.get_app_state({ app, disableDiff? }) -> { app, text, screenshot }
40
+ - sky.click({ app, element_index?, x?, y?, mouse_button?, click_count? })
41
+ - sky.perform_secondary_action({ app, element_index, action })
42
+ - sky.set_value({ app, element_index, value })
43
+ - sky.select_text({ app, element_index, text, prefix?, suffix?, selection? })
44
+ - sky.scroll({ app, element_index, direction, pages? })
45
+ - sky.drag({ app, from_x, from_y, to_x, to_y })
46
+ - sky.press_key({ app, key })
47
+ - sky.type_text({ app, text })
48
+ - emit(value) returns text or JSON to Pi
49
+ - emitImage(state.screenshot) returns a screenshot to Pi
50
+ - store is a persistent JSON object shared across calls
51
+
52
+ get_app_state may return an accessibility-tree diff after the first inspection. Pass disableDiff: true when you need a fresh full tree.
53
+
54
+ Example:
55
+ const state = await sky.get_app_state({ app: "TextEdit" });
56
+ emit(state.text);
57
+
58
+ Batch known actions sequentially, then inspect again before deciding the next step.`;
59
+
60
+ interface PiContentResult {
61
+ content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }>;
62
+ fullOutputPath?: string;
99
63
  }
100
64
 
101
- /** Keep the signed app-server/client alive across the required state -> action pair. */
102
- export class PiDirectSessionExecutor {
103
- private session: OfficialDirectToolSession | undefined;
104
- private queue: Promise<void> = Promise.resolve();
105
- private readonly dependencies: SessionExecutorDependencies;
106
-
107
- constructor(dependencies: SessionExecutorDependencies = {}) {
108
- this.dependencies = dependencies;
109
- }
110
-
111
- private async closeSession(): Promise<void> {
112
- const session = this.session;
113
- this.session = undefined;
114
- await session?.close();
65
+ export async function toPiContent(content: JsonObject[]): Promise<PiContentResult> {
66
+ const textBlocks = content.filter((block) => block.type !== "image").map((block) => String(block.text ?? ""));
67
+ const fullText = textBlocks.join("\n\n");
68
+ const aggregate = truncateHead(fullText, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
69
+ if (!aggregate.truncated) {
70
+ return {
71
+ content: content.map((block) => block.type === "image"
72
+ ? { type: "image", data: String(block.data), mimeType: String(block.mimeType) }
73
+ : { type: "text", text: String(block.text ?? "") }),
74
+ };
115
75
  }
116
76
 
117
- async execute(
118
- method: DirectMethod,
119
- params: Record<string, unknown>,
120
- dependencies: Parameters<typeof executeDirectTool>[1],
121
- ): Promise<Awaited<ReturnType<typeof executeDirectTool>>> {
122
- const previous = this.queue;
123
- let release!: () => void;
124
- this.queue = new Promise<void>((resolve) => { release = resolve; });
125
- await previous;
126
- try {
127
- const executeTool = this.dependencies.executeTool ?? executeDirectTool;
128
- if (method === "list_apps") return await executeTool({ method, arguments: params }, dependencies);
129
- if (method === "get_app_state") {
130
- await this.closeSession();
131
- const createSession = this.dependencies.createSession ?? createOfficialDirectToolSession;
132
- try {
133
- const response = await executeTool(
134
- { method, arguments: params },
135
- {
136
- ...dependencies,
137
- callTool: async (directMethod, args, options) => {
138
- this.session = await createSession({ supportsOpenAiFormElicitation: true });
139
- return this.session.call(directMethod, args, options);
140
- },
141
- },
142
- );
143
- if (response.isError) await this.closeSession();
144
- return response;
145
- } catch (error) {
146
- await this.closeSession();
147
- throw error;
148
- }
149
- }
150
- const session = this.session;
151
- try {
152
- return await executeTool(
153
- { method, arguments: params },
154
- session ? {
155
- ...dependencies,
156
- callTool: async (directMethod, args, options) => {
157
- const result = await session.call(directMethod, args, options);
158
- await this.closeSession();
159
- return { ...result, brokerCleanupVerified: true };
160
- },
161
- } : dependencies,
162
- );
163
- } finally {
164
- await this.closeSession();
165
- }
166
- } finally {
167
- release();
168
- }
169
- }
170
-
171
- async close(): Promise<void> {
172
- await this.queue;
173
- await this.closeSession();
174
- }
175
- }
176
-
177
- function toPiContent(content: Array<Record<string, unknown>>): Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> {
178
- const result: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
77
+ const tempDir = await mkdtemp(path.join(tmpdir(), "pi-computer-use-"));
78
+ const fullOutputPath = path.join(tempDir, "output.txt");
79
+ await writeFile(fullOutputPath, fullText, { encoding: "utf8", mode: 0o600 });
80
+ const suffix = `\n\n[Official Computer Use text truncated: showing ${aggregate.outputLines} of ${aggregate.totalLines} lines (${formatSize(aggregate.outputBytes)} of ${formatSize(aggregate.totalBytes)}). Full output saved to: ${fullOutputPath}]`;
81
+ const result: PiContentResult = { content: [], fullOutputPath };
82
+ let textIndex = 0;
83
+ let textOffset = 0;
84
+ let noticeAdded = false;
179
85
  for (const block of content) {
180
86
  if (block.type === "image") {
181
- result.push({ type: "image", data: String(block.data), mimeType: String(block.mimeType) });
87
+ result.content.push({ type: "image", data: String(block.data), mimeType: String(block.mimeType) });
182
88
  continue;
183
89
  }
184
- const original = String(block.text ?? "");
185
- const truncated = truncateHead(original, { maxLines: 2_000, maxBytes: 50 * 1024 });
186
- const suffix = truncated.truncated
187
- ? `\n\n[Official Computer Use text truncated in-memory: ${truncated.outputLines}/${truncated.totalLines} lines, ${truncated.outputBytes}/${truncated.totalBytes} bytes. No full-output file was written.]`
90
+ const blockText = String(block.text ?? "");
91
+ const start = textOffset + (textIndex > 0 ? 2 : 0);
92
+ const end = start + blockText.length;
93
+ const retained = aggregate.content.length > start
94
+ ? blockText.slice(0, Math.min(end, aggregate.content.length) - start)
188
95
  : "";
189
- result.push({ type: "text", text: `${truncated.content}${suffix}` });
96
+ const cutoffHere: boolean = !noticeAdded && aggregate.content.length < end;
97
+ if (retained || cutoffHere) {
98
+ result.content.push({ type: "text", text: `${retained}${cutoffHere ? suffix : ""}` });
99
+ noticeAdded ||= cutoffHere;
100
+ }
101
+ textOffset = end;
102
+ textIndex += 1;
190
103
  }
191
104
  return result;
192
105
  }
193
106
 
194
- function errorText(content: Array<Record<string, unknown>>): string {
195
- return content
196
- .filter((block) => block.type === "text")
197
- .map((block) => String(block.text ?? ""))
198
- .join("\n")
199
- .slice(0, 2_000) || "Official Computer Use returned an error";
200
- }
201
-
202
- function isRecord(value: unknown): value is Record<string, unknown> {
203
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
204
- }
205
-
206
- function initialFormContent(schema: unknown): Record<string, unknown> {
207
- if (!isRecord(schema) || !isRecord(schema.properties)) return {};
208
- const content: Record<string, unknown> = {};
209
- for (const [key, rawProperty] of Object.entries(schema.properties)) {
210
- if (!isRecord(rawProperty)) continue;
211
- if (rawProperty.default !== undefined) content[key] = rawProperty.default;
212
- else if (Array.isArray(rawProperty.enum) && rawProperty.enum.length > 0) content[key] = rawProperty.enum[0];
213
- else if (rawProperty.type === "boolean") content[key] = false;
214
- else if (rawProperty.type === "number" || rawProperty.type === "integer") content[key] = 0;
215
- else if (rawProperty.type === "array") content[key] = [];
216
- else content[key] = "";
217
- }
218
- return content;
107
+ interface PiElicitationContext {
108
+ hasUI: boolean;
109
+ ui: {
110
+ select(title: string, options: string[]): Promise<string | undefined>;
111
+ editor(title: string, prefill?: string): Promise<string | undefined>;
112
+ notify(message: string, type?: "info" | "warning" | "error"): void;
113
+ };
219
114
  }
220
115
 
221
116
  export async function handleOfficialElicitation(
222
117
  request: DirectBrokerElicitationRequest,
223
- ctx: { hasUI: boolean; ui: {
224
- select(title: string, options: string[]): Promise<string | undefined>;
225
- editor(title: string, prefill?: string): Promise<string | undefined>;
226
- notify(message: string, type?: "info" | "warning" | "error"): void;
227
- } },
118
+ ctx: PiElicitationContext,
228
119
  openUrl: (url: string) => Promise<boolean>,
229
120
  ): Promise<DirectBrokerElicitationResponse> {
230
121
  if (!ctx.hasUI) return { action: "cancel" };
231
- const message = typeof request.message === "string" ? request.message : "Official Computer Use requests input";
122
+ const message = request.message ?? "Official Computer Use requests input";
232
123
 
233
124
  if (request.mode === "url") {
234
- if (typeof request.url !== "string") return { action: "cancel" };
125
+ if (!request.url) return { action: "cancel" };
235
126
  const choice = await ctx.ui.select(`${message}\n${request.url}`, ["Open URL", "Decline", "Cancel"]);
236
127
  if (choice === "Decline") return { action: "decline" };
237
128
  if (choice !== "Open URL") return { action: "cancel" };
@@ -247,19 +138,21 @@ export async function handleOfficialElicitation(
247
138
  return { action: "cancel" };
248
139
  }
249
140
  const openAiForm = request.mode === "openai/form";
250
- if (request.requestedSchema === undefined || (!openAiForm && !isRecord(request.requestedSchema))) return { action: "cancel" };
141
+ if (request.requestedSchema === undefined || (!openAiForm && !jsonObjectSchema.safeParse(request.requestedSchema).success)) {
142
+ return { action: "cancel" };
143
+ }
251
144
  const choice = await ctx.ui.select(message, ["Respond", "Decline", "Cancel"]);
252
145
  if (choice === "Decline") return { action: "decline" };
253
146
  if (choice !== "Respond") return { action: "cancel" };
254
- let prefill = JSON.stringify(initialFormContent(request.requestedSchema), null, 2);
255
147
  const title = `${message}\nSchema: ${JSON.stringify(request.requestedSchema)}`;
148
+ let prefill = "{}";
256
149
  while (true) {
257
150
  const edited = await ctx.ui.editor(title, prefill);
258
151
  if (edited === undefined) return { action: "cancel" };
259
152
  prefill = edited;
260
153
  try {
261
- const content = JSON.parse(edited);
262
- if (!openAiForm && !isRecord(content)) throw new Error("Response must be a JSON object");
154
+ const parsed = JSON.parse(edited);
155
+ const content = openAiForm ? parsed : jsonObjectSchema.parse(parsed);
263
156
  return { action: "accept", content };
264
157
  } catch (error) {
265
158
  ctx.ui.notify(error instanceof Error ? error.message : "Response must be valid JSON", "error");
@@ -269,62 +162,54 @@ export async function handleOfficialElicitation(
269
162
 
270
163
  export default function directComputerUse(pi: ExtensionAPI) {
271
164
  const stateRoot = process.env.CODEX_COMPUTER_USE_HOME || path.join(getAgentDir(), "direct-computer-use");
272
- const sessionExecutor = new PiDirectSessionExecutor();
165
+ const codeExecutor = new ComputerUseCodeExecutor();
273
166
 
274
167
  pi.registerCommand("computer-use-status", {
275
- description: "Show the direct-call architecture, durable no-permissions policy, signed broker, and private audit path",
168
+ description: "Show Computer Use status",
276
169
  handler: async (_args, ctx) => {
277
- const status = await getDirectStatus(stateRoot);
278
- ctx.ui.notify(JSON.stringify(status, null, 2), "info");
170
+ ctx.ui.notify(JSON.stringify(getDirectStatus(stateRoot), null, 2), "info");
279
171
  },
280
172
  });
281
173
 
282
- const methods = Object.keys(ToolParameters) as DirectMethod[];
283
- for (const method of methods) {
284
- const piName = `computer_use_${method}`;
285
- const inspectionPromptMetadata = INSPECTION_METHODS.has(method) ? {
286
- promptSnippet: `${titleFor(method)} through the official signed macOS Computer Use service`,
287
- promptGuidelines: [
288
- `${piName} is a direct typed tool: choose its arguments yourself; it does not invoke a nested planner or model.`,
289
- `Call computer_use_get_app_state once per assistant turn before interacting with an app.`,
290
- ],
291
- } : {};
292
- pi.registerTool({
293
- name: piName,
294
- label: titleFor(method),
295
- description: toolDescription(method),
296
- ...inspectionPromptMetadata,
297
- parameters: ToolParameters[method] as any,
298
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
299
- const response = await sessionExecutor.execute(
300
- method,
301
- params as Record<string, unknown>,
302
- {
303
- stateRoot,
304
- signal,
305
- supportsOpenAiFormElicitation: true,
306
- onElicitation: (request) => handleOfficialElicitation(
307
- request,
308
- ctx,
309
- async (url) => {
310
- const result = await pi.exec("/usr/bin/open", ["--", url], { signal, timeout: 15_000 });
311
- return result.code === 0;
312
- },
313
- ),
314
- onProgress: (message) => onUpdate?.({ content: [{ type: "text", text: message }], details: { status: "running" } }),
315
- },
316
- );
317
- if (response.isError) throw new Error(errorText(response.content));
318
- return { content: toPiContent(response.content), details: response.details };
319
- },
320
- renderCall(args, theme) {
321
- const renderedArgs = args as Record<string, unknown>;
322
- const app = typeof renderedArgs.app === "string" ? ` ${renderedArgs.app}` : "";
323
- return new Text(theme.fg("toolTitle", theme.bold(`${piName} `)) + theme.fg("accent", app.trim()), 0, 0);
324
- },
325
- });
326
- }
174
+ pi.registerTool({
175
+ name: "computer_use",
176
+ label: "Computer Use",
177
+ description: codeDescription,
178
+ promptSnippet: "Run composable JavaScript against OpenAI's official signed macOS Computer Use surface",
179
+ promptGuidelines: [
180
+ "Use computer_use for macOS app UI work, composing known sequential actions in one JavaScript call and emitting only the state needed for the next decision.",
181
+ ],
182
+ // SAFETY: Pi accepts this standard JSON Schema object as a custom-tool parameter schema.
183
+ parameters: codeParameters as any,
184
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
185
+ const { code } = z.object({ code: z.string() }).parse(params);
186
+ const result = await codeExecutor.execute(code, {
187
+ stateRoot,
188
+ signal,
189
+ supportsOpenAiFormElicitation: true,
190
+ onElicitation: (request) => handleOfficialElicitation(
191
+ request,
192
+ ctx,
193
+ async (url) => (await pi.exec("/usr/bin/open", ["--", url], { signal, timeout: 15_000 })).code === 0,
194
+ ),
195
+ });
196
+ const rendered = await toPiContent(result.content);
197
+ const details: JsonObject = { calls: result.calls };
198
+ if (result.error) details.error = result.error;
199
+ if (rendered.fullOutputPath) details.fullOutputPath = rendered.fullOutputPath;
200
+ return { content: rendered.content, details };
201
+ },
202
+ renderCall(args, theme) {
203
+ const parsed = z.object({ code: z.string() }).safeParse(args);
204
+ const firstLine = parsed.success
205
+ ? parsed.data.code.split("\n").map((line) => line.trim()).find(Boolean)?.slice(0, 100)
206
+ : undefined;
207
+ const label = theme.fg("toolTitle", theme.bold("computer_use"));
208
+ return new Text(firstLine ? `${label} ${theme.fg("dim", firstLine)}` : label, 0, 0);
209
+ },
210
+ });
327
211
 
328
- pi.on("session_start", () => setInitialComputerUseTools(pi));
329
- pi.on("session_shutdown", () => sessionExecutor.close());
212
+ pi.on("session_start", () => pi.setActiveTools([...new Set([...pi.getActiveTools(), "computer_use"])]));
213
+ pi.on("agent_settled", () => codeExecutor.close());
214
+ pi.on("session_shutdown", () => codeExecutor.close());
330
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-computer-use-mcp",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Expose the official macOS Computer Use tools directly to Pi and MCP clients without a nested model.",
5
5
  "author": "Thomas Mustier",
6
6
  "license": "MIT",
@@ -28,57 +28,44 @@
28
28
  },
29
29
  "files": [
30
30
  "dist",
31
- "integrations/pi",
32
- "npm-shrinkwrap.json",
31
+ "integrations/pi/index.ts",
33
32
  "README.md",
34
- "CHANGELOG.md",
35
- "PROOF.md",
36
- "ARCHITECTURE.md",
37
- "MIGRATION.md",
38
33
  "SECURITY.md",
39
- "docs/adr",
40
- "CONTRIBUTING.md",
41
34
  "LICENSE"
42
35
  ],
43
36
  "scripts": {
44
37
  "build": "rm -rf dist && tsc -p tsconfig.json",
45
- "check": "tsc -p tsconfig.json --noEmit",
38
+ "lint": "oxlint --deny-warnings",
39
+ "check": "npm run lint && tsc -p tsconfig.json --noEmit",
40
+ "pretest": "npm run build",
46
41
  "test": "node --test test/*.test.ts",
47
- "prepare": "npm run build",
48
- "prepublishOnly": "npm run check && npm run check:pi && npm test && npm run build",
49
- "check:pi": "npx tsc --ignoreConfig --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck --types node integrations/pi/index.ts"
42
+ "prepack": "npm run build",
43
+ "prepublishOnly": "npm run check && npm run check:pi && npm test",
44
+ "check:pi": "npm run build && npx tsc --ignoreConfig --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck --types node integrations/pi/index.ts"
50
45
  },
51
46
  "dependencies": {
52
47
  "@modelcontextprotocol/sdk": "1.29.0",
53
- "@types/node": "24.13.3",
54
- "typescript": "7.0.2",
55
48
  "zod": "4.4.3"
56
49
  },
57
50
  "peerDependencies": {
58
- "@earendil-works/pi-ai": "*",
59
51
  "@earendil-works/pi-coding-agent": "*",
60
- "@earendil-works/pi-tui": "*",
61
- "typebox": "*"
52
+ "@earendil-works/pi-tui": "*"
62
53
  },
63
54
  "peerDependenciesMeta": {
64
- "@earendil-works/pi-ai": {
65
- "optional": true
66
- },
67
55
  "@earendil-works/pi-coding-agent": {
68
56
  "optional": true
69
57
  },
70
58
  "@earendil-works/pi-tui": {
71
59
  "optional": true
72
- },
73
- "typebox": {
74
- "optional": true
75
60
  }
76
61
  },
77
62
  "devDependencies": {
78
- "@earendil-works/pi-ai": "0.80.7",
79
63
  "@earendil-works/pi-coding-agent": "0.80.7",
80
64
  "@earendil-works/pi-tui": "0.80.7",
81
- "typebox": "1.1.38"
65
+ "@oxlint/plugins": "1.79.0",
66
+ "@types/node": "24.13.3",
67
+ "oxlint": "1.79.0",
68
+ "typescript": "7.0.2"
82
69
  },
83
70
  "engines": {
84
71
  "node": ">=22"