@meetopenbot/codex 1.0.12 → 1.1.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/README.md CHANGED
@@ -1,37 +1,23 @@
1
- # openbot-plugin-codex
1
+ # @meetopenbot/codex
2
2
 
3
- Minimal Codex plugin scaffold for OpenBot.
3
+ OpenAI Codex runtime plugin for OpenBot. Each `agent:invoke` starts or resumes a Codex SDK thread in the channel workspace.
4
4
 
5
- ## What is included
5
+ ## Config
6
6
 
7
- - A plugin registry export (`plugin`) compatible with OpenBot
8
- - One tool definition: `codex_run`
9
- - One action handler: `action:codex_run`
10
- - `@openai/codex-sdk` integration via `Codex` + `thread.run(...)`
7
+ Set an API key with one of:
11
8
 
12
- ## Local usage
13
-
14
- 1. Install dependencies:
15
-
16
- `npm install`
17
-
18
- 2. Build:
19
-
20
- `npm run build`
21
-
22
- 3. Load `dist/index.js` from your OpenBot plugin registry/runtime.
23
-
24
- ## Runtime config
25
-
26
- Set an API key using one of the following:
27
-
28
- - `CODEX_API_KEY` environment variable
29
- - `OPENAI_API_KEY` environment variable
30
9
  - `apiKey` in plugin options
10
+ - `CODEX_API_KEY`
11
+ - `OPENAI_API_KEY`
12
+
13
+ Optional:
31
14
 
32
- Optional plugin options:
15
+ - `model` — Codex model id (e.g. `gpt-5.6`). Omit to use the CLI default.
16
+ - `sandboxMode` — `read-only` | `workspace-write` | `danger-full-access` (default `workspace-write`)
17
+ - `approvalPolicy` — `never` | `on-request` | `on-failure` | `untrusted` (default `never`)
18
+ - `workingDirectory` — override the channel cwd
19
+ - `skipGitRepoCheck` — default `true` for non-git channel workspaces
20
+ - `baseURL` — custom OpenAI-compatible endpoint
21
+ - `codexPathOverride` — path to a local `codex` binary (`CODEX_PATH` / `CODEX_CLI_PATH` also work)
33
22
 
34
- - `model` (defaults to `gpt-5-codex`)
35
- - `baseURL` (for custom OpenAI-compatible endpoints)
36
- - `workingDirectory`, `skipGitRepoCheck`, `sandboxMode`, `approvalPolicy`
37
- - `networkAccessEnabled`, `webSearchMode`, `threadId`
23
+ Codex conversation state is stored as `codexThreadId` on the OpenBot thread, not as the OpenBot thread id.
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@meetopenbot/plugin-sdk';
2
+ declare const plugin: Plugin;
3
+ export { plugin };
4
+ export default plugin;
package/dist/index.js CHANGED
@@ -8,180 +8,269 @@ import {
8
8
  import {
9
9
  Codex
10
10
  } from "@openai/codex-sdk";
11
- var codex_default = definePlugin({
11
+ var SANDBOX_MODES = [
12
+ "read-only",
13
+ "workspace-write",
14
+ "danger-full-access"
15
+ ];
16
+ var APPROVAL_MODES = [
17
+ "never",
18
+ "on-request",
19
+ "on-failure",
20
+ "untrusted"
21
+ ];
22
+ var LEGACY_SANDBOX = {
23
+ "workspace-read": "read-only",
24
+ "full-read": "read-only",
25
+ "full-write": "danger-full-access"
26
+ };
27
+ var LEGACY_APPROVAL = {
28
+ always: "on-request",
29
+ automatic: "on-request"
30
+ };
31
+ var AUTH_ERROR_PATTERNS = [
32
+ "api key",
33
+ "apikey",
34
+ "unauthorized",
35
+ "401",
36
+ "authentication",
37
+ "not logged in",
38
+ "login"
39
+ ];
40
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
41
+ var asString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
42
+ var isAuthErrorMessage = (message) => {
43
+ const lower = message.toLowerCase();
44
+ return AUTH_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
45
+ };
46
+ var readPersistedThreadId = (state) => {
47
+ const source = state.threadDetails?.state ?? state.channelDetails?.state;
48
+ const record = asRecord(source);
49
+ return asString(record.codexThreadId);
50
+ };
51
+ var persistThreadId = async (state, storage, codexThreadId) => {
52
+ if (!storage || !state.channelId) return;
53
+ const patch = { codexThreadId };
54
+ if (state.threadId) {
55
+ await storage.patchThreadState({
56
+ channelId: state.channelId,
57
+ threadId: state.threadId,
58
+ state: patch
59
+ });
60
+ return;
61
+ }
62
+ await storage.patchChannelState({ channelId: state.channelId, state: patch });
63
+ };
64
+ var parseSandboxMode = (value) => {
65
+ const raw = asString(value);
66
+ if (!raw) return "workspace-write";
67
+ if (SANDBOX_MODES.includes(raw)) return raw;
68
+ return LEGACY_SANDBOX[raw] ?? "workspace-write";
69
+ };
70
+ var parseApprovalPolicy = (value) => {
71
+ const raw = asString(value);
72
+ if (!raw) return "never";
73
+ if (APPROVAL_MODES.includes(raw)) return raw;
74
+ return LEGACY_APPROVAL[raw] ?? "never";
75
+ };
76
+ var parseModel = (value) => {
77
+ const raw = asString(value);
78
+ if (!raw) return void 0;
79
+ return raw.split("/").pop() || void 0;
80
+ };
81
+ var itemWidget = (item) => {
82
+ switch (item.type) {
83
+ case "command_execution":
84
+ return { title: "Command", body: item.command };
85
+ case "file_change":
86
+ return {
87
+ title: "File change",
88
+ body: item.changes.map((change) => `${change.kind} ${change.path}`).join("\n")
89
+ };
90
+ case "error":
91
+ return { title: "Error", body: item.message };
92
+ default:
93
+ return null;
94
+ }
95
+ };
96
+ var errorText = (error) => error instanceof Error ? error.message : "Codex request failed.";
97
+ var buildApiKeyWidget = (agentId, threadId, reason) => uiWidget({
98
+ agentId,
99
+ threadId,
100
+ widget: {
101
+ kind: "form",
102
+ widgetId: `codex_api_key_request_${Date.now()}`,
103
+ title: "OpenAI API Key Required",
104
+ description: `Codex could not authenticate (${reason}). Provide an OpenAI API key to continue. You can get one from the OpenAI dashboard. The key is stored as a workspace variable on your machine and never leaves your local runtime.`,
105
+ fields: [
106
+ {
107
+ id: "apiKey",
108
+ label: "API Key",
109
+ type: "password",
110
+ placeholder: "sk-...",
111
+ required: true
112
+ }
113
+ ],
114
+ submitLabel: "Save API Key",
115
+ metadata: {
116
+ type: "api_key_request",
117
+ provider: "openai",
118
+ envVar: "OPENAI_API_KEY",
119
+ source: "codex"
120
+ }
121
+ }
122
+ });
123
+ var plugin = definePlugin({
12
124
  id: "codex",
13
125
  name: "Codex",
14
- description: "Codex integration tools for OpenBot",
126
+ description: "OpenAI Codex agent. Uses the Codex SDK to read code, edit files, and run shell commands inside the channel's workspace.",
15
127
  configSchema: {
16
128
  type: "object",
17
129
  properties: {
18
- apiKey: { type: "string", description: "Codex API Key", format: "password" },
19
- baseURL: { type: "string", description: "Custom OpenAI-compatible endpoint" },
20
- codexPathOverride: { type: "string", description: "Path to codex CLI" },
21
- model: { type: "string", description: "Model to use", default: "gpt-5-codex" },
22
- workingDirectory: { type: "string", description: "Working directory for Codex" },
23
- skipGitRepoCheck: { type: "boolean", description: "Skip git repo check", default: true },
130
+ apiKey: {
131
+ type: "string",
132
+ description: "OpenAI API key (falls back to CODEX_API_KEY / OPENAI_API_KEY)",
133
+ format: "password"
134
+ },
135
+ model: {
136
+ type: "string",
137
+ description: "Codex model id (e.g. gpt-5.6). Leave empty to use the CLI default."
138
+ },
24
139
  sandboxMode: {
25
140
  type: "string",
26
- enum: ["workspace-read", "workspace-write", "full-read", "full-write"],
27
- description: "Sandbox mode",
141
+ enum: [...SANDBOX_MODES],
142
+ description: "Filesystem sandbox: read-only | workspace-write | danger-full-access",
28
143
  default: "workspace-write"
29
144
  },
30
145
  approvalPolicy: {
31
146
  type: "string",
32
- enum: ["always", "never", "automatic"],
33
- description: "Approval policy",
147
+ enum: [...APPROVAL_MODES],
148
+ description: "When to require approval: never | on-request | on-failure | untrusted",
34
149
  default: "never"
35
150
  },
36
- networkAccessEnabled: { type: "boolean", description: "Enable network access" },
37
- webSearchMode: {
151
+ workingDirectory: {
152
+ type: "string",
153
+ description: "Override the channel working directory"
154
+ },
155
+ skipGitRepoCheck: {
156
+ type: "boolean",
157
+ description: "Skip the Codex git-repo check (needed for non-git channel workspaces)",
158
+ default: true
159
+ },
160
+ baseURL: {
161
+ type: "string",
162
+ description: "Custom OpenAI-compatible endpoint"
163
+ },
164
+ codexPathOverride: {
38
165
  type: "string",
39
- enum: ["always", "never", "automatic"],
40
- description: "Web search mode"
166
+ description: "Path to a local Codex CLI binary"
41
167
  }
42
168
  }
43
169
  },
44
170
  factory: (context) => {
45
171
  const config = context.config;
46
- const env = globalThis?.process?.env || {};
47
- const apiKey = config.apiKey ?? env.CODEX_API_KEY ?? env.OPENAI_API_KEY;
48
- const codexPathOverride = config.codexPathOverride ?? env.CODEX_PATH ?? env.CODEX_CLI_PATH;
49
- const model = config.model?.split("/").pop() || "gpt-5-codex";
172
+ const apiKey = asString(config.apiKey) ?? process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
173
+ const codexPathOverride = asString(config.codexPathOverride) ?? process.env.CODEX_PATH ?? process.env.CODEX_CLI_PATH;
174
+ const model = parseModel(config.model);
175
+ const sandboxMode = parseSandboxMode(config.sandboxMode);
176
+ const approvalPolicy = parseApprovalPolicy(config.approvalPolicy);
177
+ const skipGitRepoCheck = config.skipGitRepoCheck !== false;
178
+ const workingDirectoryOverride = asString(config.workingDirectory);
179
+ const baseUrl = asString(config.baseURL);
50
180
  let client;
51
181
  const getClient = () => {
52
182
  if (!client) {
53
183
  client = new Codex({
54
- apiKey,
184
+ ...apiKey && { apiKey },
55
185
  ...codexPathOverride && { codexPathOverride },
56
- ...config.baseURL && { baseUrl: config.baseURL }
186
+ ...baseUrl && { baseUrl }
57
187
  });
58
188
  }
59
189
  return client;
60
190
  };
61
- let thread = null;
62
- const getThread = (state, meta) => {
63
- if (thread) return thread;
64
- const workingDirectory = config.workingDirectory || state?.channelDetails?.cwd || globalThis?.process?.cwd() || "/tmp";
65
- const threadOptions = {
66
- model,
67
- workingDirectory,
68
- skipGitRepoCheck: config.skipGitRepoCheck ?? true,
69
- sandboxMode: config.sandboxMode ?? "workspace-write",
70
- approvalPolicy: config.approvalPolicy ?? "never",
71
- ...typeof config.networkAccessEnabled === "boolean" && {
72
- networkAccessEnabled: config.networkAccessEnabled
73
- },
74
- ...config.webSearchMode && {
75
- webSearchMode: config.webSearchMode
76
- }
77
- };
78
- const threadId = state?.threadId || meta?.threadId;
79
- thread = threadId ? getClient().resumeThread(threadId, threadOptions) : getClient().startThread(threadOptions);
80
- if (!threadId && state) {
81
- state.threadId = thread.id;
82
- }
83
- return thread;
84
- };
85
191
  return (builder) => {
86
192
  builder.on("agent:invoke", async function* (event, ctx) {
87
193
  if (!shouldHandleInvoke(event, context.agentId)) return;
88
- const { content } = event.data || {};
194
+ const content = asString(event.data?.content);
195
+ const openbotThreadId = event.meta?.threadId || ctx.state.threadId;
89
196
  if (!content) {
90
197
  yield agentOutput({
91
198
  agentId: context.agentId,
92
199
  content: "No content provided.",
93
- threadId: event.meta?.threadId
200
+ threadId: openbotThreadId
94
201
  });
95
202
  return;
96
203
  }
97
- try {
98
- const turn = await getThread(ctx?.state, event.meta).runStreamed(content);
99
- for await (const chunk of turn.events) {
100
- if (chunk.type === "item.completed") {
101
- const { item } = chunk;
102
- if (item.type === "agent_message") {
103
- yield agentOutput({
104
- agentId: context.agentId,
105
- content: item.text,
106
- threadId: event.meta?.threadId
107
- });
108
- continue;
109
- }
110
- let title = "";
111
- let body = "";
112
- switch (item.type) {
113
- case "reasoning":
114
- title = "Reasoning";
115
- body = item.text;
116
- break;
117
- case "command_execution":
118
- title = "Command Execution";
119
- body = `Executing: ${item.command}`;
120
- break;
121
- case "file_change":
122
- title = "File Change";
123
- body = item.changes.map((change) => `${change.path}: ${change.kind || change.action}`).join("\n");
124
- break;
125
- case "mcp_tool_call":
126
- title = `Tool: ${item.tool}`;
127
- body = `Arguments: ${JSON.stringify(item.arguments, null, 2)}`;
128
- if (item.result) {
129
- body += `
130
-
131
- Result: ${JSON.stringify(
132
- item.result.structured_content || item.result.content,
133
- null,
134
- 2
135
- )}`;
136
- }
137
- if (item.error) {
138
- body += `
139
-
140
- Error: ${item.error.message}`;
141
- }
142
- break;
143
- case "web_search":
144
- title = "Web Search";
145
- body = `Searching: ${item.query}`;
146
- break;
147
- case "todo_list":
148
- title = "Todo List";
149
- body = item.items.map(
150
- (todo) => `- ${todo.text} (${todo.completed ? "completed" : "pending"})`
151
- ).join("\n");
152
- break;
153
- case "error":
154
- title = "Error";
155
- body = item.message;
156
- break;
157
- }
158
- if (title && body) {
159
- yield uiWidget({
160
- agentId: context.agentId,
161
- threadId: event.meta?.threadId,
162
- widget: {
163
- kind: "message",
164
- title,
165
- body,
166
- // @ts-ignore
167
- variant: "basic",
168
- display: "collapsed"
169
- }
170
- });
171
- }
172
- }
204
+ const workingDirectory = workingDirectoryOverride || ctx.state.channelDetails?.cwd || process.cwd();
205
+ const threadOptions = {
206
+ workingDirectory,
207
+ skipGitRepoCheck,
208
+ sandboxMode,
209
+ approvalPolicy,
210
+ ...model && { model }
211
+ };
212
+ const savedId = readPersistedThreadId(ctx.state);
213
+ const thread = savedId ? getClient().resumeThread(savedId, threadOptions) : getClient().startThread(threadOptions);
214
+ const fail = function* (message) {
215
+ if (isAuthErrorMessage(message)) {
216
+ yield buildApiKeyWidget(context.agentId, openbotThreadId, message);
173
217
  }
174
- } catch (error) {
175
218
  yield agentOutput({
176
219
  agentId: context.agentId,
177
- content: `Error: ${error?.message || "Codex request failed."}`,
178
- threadId: event.meta?.threadId
220
+ content: `Error: ${message}`,
221
+ threadId: openbotThreadId
222
+ });
223
+ };
224
+ try {
225
+ const { events } = await thread.runStreamed(content, {
226
+ signal: context.abortSignal
179
227
  });
228
+ for await (const chunk of events) {
229
+ if (chunk.type === "thread.started") {
230
+ await persistThreadId(ctx.state, context.storage, chunk.thread_id);
231
+ continue;
232
+ }
233
+ if (chunk.type === "turn.failed") {
234
+ yield* fail(chunk.error.message);
235
+ return;
236
+ }
237
+ if (chunk.type === "error") {
238
+ yield* fail(chunk.message);
239
+ return;
240
+ }
241
+ if (chunk.type !== "item.completed") continue;
242
+ if (chunk.item.type === "agent_message") {
243
+ yield agentOutput({
244
+ agentId: context.agentId,
245
+ content: chunk.item.text,
246
+ threadId: openbotThreadId
247
+ });
248
+ continue;
249
+ }
250
+ const widget = itemWidget(chunk.item);
251
+ if (!widget) continue;
252
+ yield uiWidget({
253
+ agentId: context.agentId,
254
+ threadId: openbotThreadId,
255
+ widget: {
256
+ kind: "message",
257
+ widgetId: `codex_${chunk.item.id}`,
258
+ title: widget.title,
259
+ body: widget.body,
260
+ variant: "basic",
261
+ display: "collapsed"
262
+ }
263
+ });
264
+ }
265
+ } catch (error) {
266
+ yield* fail(errorText(error));
180
267
  }
181
268
  });
182
269
  };
183
270
  }
184
271
  });
272
+ var plugin_codex_default = plugin;
185
273
  export {
186
- codex_default as default
274
+ plugin_codex_default as default,
275
+ plugin
187
276
  };
package/package.json CHANGED
@@ -1,30 +1,36 @@
1
1
  {
2
2
  "name": "@meetopenbot/codex",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "description": "Codex tools plugin for OpenBot",
5
+ "description": "OpenAI Codex agent plugin for OpenBot",
6
6
  "main": "./dist/index.js",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
10
  "exports": {
11
- ".": "./dist/index.js"
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
12
16
  },
13
17
  "files": [
14
- "dist",
15
- "assets"
18
+ "dist"
16
19
  ],
17
20
  "scripts": {
18
- "build": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod",
19
- "prepublishOnly": "npm run build"
21
+ "build": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod && node ../../scripts/write-plugin-declaration.mjs",
22
+ "dev": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod --watch",
23
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions --module ESNext --moduleResolution Bundler --target ES2022 --skipLibCheck index.ts",
24
+ "prepack": "pnpm build"
20
25
  },
21
26
  "dependencies": {
22
- "@meetopenbot/plugin-sdk": "^0.1.2",
23
- "@openai/codex-sdk": "^0.138.0"
27
+ "@meetopenbot/plugin-sdk": "workspace:^",
28
+ "@openai/codex-sdk": "0.148.0"
24
29
  },
25
30
  "devDependencies": {
26
31
  "@types/node": "^25.6.0",
27
32
  "esbuild": "^0.21.0",
28
33
  "zod": "^4.4.3"
29
- }
34
+ },
35
+ "types": "./dist/index.d.ts"
30
36
  }
package/assets/icon.svg DELETED
@@ -1 +0,0 @@
1
- <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>