@meetopenbot/codex 1.1.0 → 1.2.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 (3) hide show
  1. package/README.md +6 -7
  2. package/dist/index.js +217 -51
  3. package/package.json +10 -11
package/README.md CHANGED
@@ -2,22 +2,21 @@
2
2
 
3
3
  OpenAI Codex runtime plugin for OpenBot. Each `agent:invoke` starts or resumes a Codex SDK thread in the channel workspace.
4
4
 
5
- ## Config
5
+ ## Auth
6
+
7
+ On cloud, `authMode: credits` (the default) uses your workspace credit balance via OpenBot. For BYOK, set `authMode: byok` and add `OPENAI_API_KEY` (or `CODEX_API_KEY`) under workspace settings — there is no API key field in plugin config.
6
8
 
7
- Set an API key with one of:
9
+ Locally, Codex always uses BYOK from `CODEX_API_KEY` / `OPENAI_API_KEY`.
8
10
 
9
- - `apiKey` in plugin options
10
- - `CODEX_API_KEY`
11
- - `OPENAI_API_KEY`
11
+ ## Config
12
12
 
13
13
  Optional:
14
14
 
15
+ - `authMode` — cloud only: `credits` (default) or `byok`
15
16
  - `model` — Codex model id (e.g. `gpt-5.6`). Omit to use the CLI default.
16
17
  - `sandboxMode` — `read-only` | `workspace-write` | `danger-full-access` (default `workspace-write`)
17
18
  - `approvalPolicy` — `never` | `on-request` | `on-failure` | `untrusted` (default `never`)
18
- - `workingDirectory` — override the channel cwd
19
19
  - `skipGitRepoCheck` — default `true` for non-git channel workspaces
20
- - `baseURL` — custom OpenAI-compatible endpoint
21
20
  - `codexPathOverride` — path to a local `codex` binary (`CODEX_PATH` / `CODEX_CLI_PATH` also work)
22
21
 
23
22
  Codex conversation state is stored as `codexThreadId` on the OpenBot thread, not as the OpenBot thread id.
package/dist/index.js CHANGED
@@ -3,11 +3,86 @@ import {
3
3
  definePlugin,
4
4
  shouldHandleInvoke,
5
5
  agentOutput,
6
- uiWidget
6
+ uiWidget,
7
+ toolTraceWidget,
8
+ buildDiffWidget,
9
+ diffFileFromWrite,
10
+ resolveRunDiffFiles,
11
+ snapshotWorkspace
7
12
  } from "@meetopenbot/plugin-sdk";
13
+ import { readFileSync } from "node:fs";
14
+ import { join } from "node:path";
8
15
  import {
9
16
  Codex
10
17
  } from "@openai/codex-sdk";
18
+
19
+ // cloud-mode.ts
20
+ var isCloudMode = () => process.env.OPENBOT_CLOUD_MODE === "1";
21
+ var defaultAuthMode = () => isCloudMode() ? "credits" : "byok";
22
+ function resolveAuthMode(config) {
23
+ if (config.authMode === "byok" || config.authMode === "credits") {
24
+ return config.authMode;
25
+ }
26
+ return defaultAuthMode();
27
+ }
28
+
29
+ // credits-auth.ts
30
+ var INTEGRATIONS_TOKEN_HEADER = "x-openbot-integrations-token";
31
+ var CREDITS_API_KEY_PLACEHOLDER = "openbot-credits";
32
+ var CREDITS_PROVIDER_ID = "openbot_credits";
33
+ function resolveCreditsAuthConfig() {
34
+ const baseUrl = process.env.OPENBOT_INTEGRATIONS_BASE_URL?.trim();
35
+ const token = process.env.OPENBOT_INTEGRATIONS_TOKEN?.trim();
36
+ if (!baseUrl || !token) return void 0;
37
+ return { baseUrl: baseUrl.replace(/\/$/, ""), token };
38
+ }
39
+ function creditsProviderBaseUrl(config) {
40
+ return `${config.baseUrl}/openai/v1`;
41
+ }
42
+ function buildCreditsCodexConfig(config) {
43
+ return {
44
+ model_provider: CREDITS_PROVIDER_ID,
45
+ model_providers: {
46
+ [CREDITS_PROVIDER_ID]: {
47
+ name: "OpenBot Credits",
48
+ base_url: creditsProviderBaseUrl(config),
49
+ env_key: "CODEX_API_KEY",
50
+ wire_api: "responses",
51
+ supports_websockets: false,
52
+ http_headers: {
53
+ [INTEGRATIONS_TOKEN_HEADER]: config.token
54
+ }
55
+ }
56
+ }
57
+ };
58
+ }
59
+ function isCreditsErrorMessage(message) {
60
+ const lower = message.toLowerCase();
61
+ return lower.includes("insufficient_credits") || lower.includes("insufficient credits") || lower.includes("402");
62
+ }
63
+ function isAuthErrorMessage(message) {
64
+ const lower = message.toLowerCase();
65
+ return lower.includes("api key") || lower.includes("apikey") || lower.includes("401") || lower.includes("unauthorized") || lower.includes("authentication") || lower.includes("not logged in") || lower.includes("login");
66
+ }
67
+ function isIntegrationsProviderError(message) {
68
+ const lower = message.toLowerCase();
69
+ return lower.includes("provider api key not configured") || lower.includes("503") && lower.includes("provider");
70
+ }
71
+ var CREDITS_NOT_CONFIGURED_MESSAGE = "OpenBot Credits is not configured on this runtime. The cloud host must set OPENBOT_INTEGRATIONS_BASE_URL and OPENBOT_INTEGRATIONS_TOKEN (try redeploying the workspace).";
72
+ var CREDITS_PROVIDER_UNAVAILABLE_MESSAGE = "OpenBot Credits could not reach OpenAI \u2014 the platform provider API key is not configured yet. Try again later or switch this agent to BYOK mode.";
73
+ var CREDITS_AUTH_FAILED_MESSAGE = "Codex could not authenticate via OpenBot Credits. Check your workspace credit balance in settings, or switch this agent to BYOK mode.";
74
+ function creditsErrorMessage(message) {
75
+ if (isIntegrationsProviderError(message)) {
76
+ return CREDITS_PROVIDER_UNAVAILABLE_MESSAGE;
77
+ }
78
+ if (isCreditsErrorMessage(message)) {
79
+ return "Insufficient workspace credits. Add credits in workspace settings or switch this agent to BYOK mode.";
80
+ }
81
+ if (isAuthErrorMessage(message)) return CREDITS_AUTH_FAILED_MESSAGE;
82
+ return void 0;
83
+ }
84
+
85
+ // index.ts
11
86
  var SANDBOX_MODES = [
12
87
  "read-only",
13
88
  "workspace-write",
@@ -28,21 +103,8 @@ var LEGACY_APPROVAL = {
28
103
  always: "on-request",
29
104
  automatic: "on-request"
30
105
  };
31
- var AUTH_ERROR_PATTERNS = [
32
- "api key",
33
- "apikey",
34
- "unauthorized",
35
- "401",
36
- "authentication",
37
- "not logged in",
38
- "login"
39
- ];
40
106
  var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
41
107
  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
108
  var readPersistedThreadId = (state) => {
47
109
  const source = state.threadDetails?.state ?? state.channelDetails?.state;
48
110
  const record = asRecord(source);
@@ -81,18 +143,41 @@ var parseModel = (value) => {
81
143
  var itemWidget = (item) => {
82
144
  switch (item.type) {
83
145
  case "command_execution":
84
- return { title: "Command", body: item.command };
146
+ return {
147
+ title: "Command",
148
+ body: [item.command, item.aggregated_output].filter(Boolean).join("\n\n")
149
+ };
85
150
  case "file_change":
86
151
  return {
87
152
  title: "File change",
88
153
  body: item.changes.map((change) => `${change.kind} ${change.path}`).join("\n")
89
154
  };
155
+ case "mcp_tool_call":
156
+ return {
157
+ title: `${item.server}: ${item.tool}`,
158
+ body: item.error?.message ?? (item.result ? JSON.stringify(item.result, null, 2) : "")
159
+ };
160
+ case "web_search":
161
+ return { title: "Web search", body: item.query };
90
162
  case "error":
91
163
  return { title: "Error", body: item.message };
92
164
  default:
93
165
  return null;
94
166
  }
95
167
  };
168
+ var collectCodexChange = (path, kind, cwd, files) => {
169
+ if (kind === "add") {
170
+ try {
171
+ files.set(path, diffFileFromWrite(path, readFileSync(join(cwd, path), "utf8")));
172
+ return;
173
+ } catch {
174
+ }
175
+ }
176
+ files.set(path, {
177
+ path,
178
+ status: kind === "delete" ? "deleted" : kind === "add" ? "added" : "modified"
179
+ });
180
+ };
96
181
  var errorText = (error) => error instanceof Error ? error.message : "Codex request failed.";
97
182
  var buildApiKeyWidget = (agentId, threadId, reason) => uiWidget({
98
183
  agentId,
@@ -127,11 +212,14 @@ var plugin = definePlugin({
127
212
  configSchema: {
128
213
  type: "object",
129
214
  properties: {
130
- apiKey: {
131
- type: "string",
132
- description: "OpenAI API key (falls back to CODEX_API_KEY / OPENAI_API_KEY)",
133
- format: "password"
134
- },
215
+ ...isCloudMode() ? {
216
+ authMode: {
217
+ type: "string",
218
+ description: "Credits \u2014 use your workspace credit balance via OpenBot. BYOK \u2014 bring your own OpenAI API key.",
219
+ enum: ["credits", "byok"],
220
+ default: "credits"
221
+ }
222
+ } : {},
135
223
  model: {
136
224
  type: "string",
137
225
  description: "Codex model id (e.g. gpt-5.6). Leave empty to use the CLI default."
@@ -148,19 +236,11 @@ var plugin = definePlugin({
148
236
  description: "When to require approval: never | on-request | on-failure | untrusted",
149
237
  default: "never"
150
238
  },
151
- workingDirectory: {
152
- type: "string",
153
- description: "Override the channel working directory"
154
- },
155
239
  skipGitRepoCheck: {
156
240
  type: "boolean",
157
241
  description: "Skip the Codex git-repo check (needed for non-git channel workspaces)",
158
242
  default: true
159
243
  },
160
- baseURL: {
161
- type: "string",
162
- description: "Custom OpenAI-compatible endpoint"
163
- },
164
244
  codexPathOverride: {
165
245
  type: "string",
166
246
  description: "Path to a local Codex CLI binary"
@@ -169,24 +249,27 @@ var plugin = definePlugin({
169
249
  },
170
250
  factory: (context) => {
171
251
  const config = context.config;
172
- const apiKey = asString(config.apiKey) ?? process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
252
+ const authMode = resolveAuthMode(config);
173
253
  const codexPathOverride = asString(config.codexPathOverride) ?? process.env.CODEX_PATH ?? process.env.CODEX_CLI_PATH;
174
254
  const model = parseModel(config.model);
175
255
  const sandboxMode = parseSandboxMode(config.sandboxMode);
176
256
  const approvalPolicy = parseApprovalPolicy(config.approvalPolicy);
177
257
  const skipGitRepoCheck = config.skipGitRepoCheck !== false;
178
- const workingDirectoryOverride = asString(config.workingDirectory);
179
- const baseUrl = asString(config.baseURL);
180
- let client;
181
- const getClient = () => {
182
- if (!client) {
183
- client = new Codex({
184
- ...apiKey && { apiKey },
258
+ const createClient = () => {
259
+ if (authMode === "credits") {
260
+ const credits = resolveCreditsAuthConfig();
261
+ if (!credits) return CREDITS_NOT_CONFIGURED_MESSAGE;
262
+ return new Codex({
263
+ apiKey: credits.token || CREDITS_API_KEY_PLACEHOLDER,
185
264
  ...codexPathOverride && { codexPathOverride },
186
- ...baseUrl && { baseUrl }
265
+ config: buildCreditsCodexConfig(credits)
187
266
  });
188
267
  }
189
- return client;
268
+ const apiKey = process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
269
+ return new Codex({
270
+ ...asString(apiKey) && { apiKey: asString(apiKey) },
271
+ ...codexPathOverride && { codexPathOverride }
272
+ });
190
273
  };
191
274
  return (builder) => {
192
275
  builder.on("agent:invoke", async function* (event, ctx) {
@@ -201,7 +284,16 @@ var plugin = definePlugin({
201
284
  });
202
285
  return;
203
286
  }
204
- const workingDirectory = workingDirectoryOverride || ctx.state.channelDetails?.cwd || process.cwd();
287
+ const clientOrError = createClient();
288
+ if (typeof clientOrError === "string") {
289
+ yield agentOutput({
290
+ agentId: context.agentId,
291
+ content: clientOrError,
292
+ threadId: openbotThreadId
293
+ });
294
+ return;
295
+ }
296
+ const workingDirectory = ctx.state.channelDetails?.cwd || process.cwd();
205
297
  const threadOptions = {
206
298
  workingDirectory,
207
299
  skipGitRepoCheck,
@@ -210,8 +302,16 @@ var plugin = definePlugin({
210
302
  ...model && { model }
211
303
  };
212
304
  const savedId = readPersistedThreadId(ctx.state);
213
- const thread = savedId ? getClient().resumeThread(savedId, threadOptions) : getClient().startThread(threadOptions);
305
+ const thread = savedId ? clientOrError.resumeThread(savedId, threadOptions) : clientOrError.startThread(threadOptions);
214
306
  const fail = function* (message) {
307
+ if (authMode === "credits") {
308
+ yield agentOutput({
309
+ agentId: context.agentId,
310
+ content: creditsErrorMessage(message) ?? `Error: ${message}`,
311
+ threadId: openbotThreadId
312
+ });
313
+ return;
314
+ }
215
315
  if (isAuthErrorMessage(message)) {
216
316
  yield buildApiKeyWidget(context.agentId, openbotThreadId, message);
217
317
  }
@@ -225,6 +325,8 @@ var plugin = definePlugin({
225
325
  const { events } = await thread.runStreamed(content, {
226
326
  signal: context.abortSignal
227
327
  });
328
+ const snapshot = snapshotWorkspace(workingDirectory);
329
+ const changedFiles = /* @__PURE__ */ new Map();
228
330
  for await (const chunk of events) {
229
331
  if (chunk.type === "thread.started") {
230
332
  await persistThreadId(ctx.state, context.storage, chunk.thread_id);
@@ -238,34 +340,98 @@ var plugin = definePlugin({
238
340
  yield* fail(chunk.message);
239
341
  return;
240
342
  }
241
- if (chunk.type !== "item.completed") continue;
343
+ if (chunk.type !== "item.started" && chunk.type !== "item.completed") continue;
242
344
  if (chunk.item.type === "agent_message") {
243
- yield agentOutput({
244
- agentId: context.agentId,
245
- content: chunk.item.text,
246
- threadId: openbotThreadId
247
- });
345
+ if (chunk.type === "item.completed") {
346
+ yield agentOutput({
347
+ agentId: context.agentId,
348
+ content: chunk.item.text,
349
+ threadId: openbotThreadId
350
+ });
351
+ }
248
352
  continue;
249
353
  }
354
+ if (chunk.type === "item.completed" && chunk.item.type === "file_change" && chunk.item.status === "completed") {
355
+ for (const change of chunk.item.changes) {
356
+ collectCodexChange(change.path, change.kind, workingDirectory, changedFiles);
357
+ }
358
+ }
250
359
  const widget = itemWidget(chunk.item);
251
360
  if (!widget) continue;
252
361
  yield uiWidget({
253
362
  agentId: context.agentId,
254
363
  threadId: openbotThreadId,
255
- widget: {
256
- kind: "message",
364
+ widget: toolTraceWidget({
257
365
  widgetId: `codex_${chunk.item.id}`,
366
+ groupId: "codex:tools",
258
367
  title: widget.title,
259
368
  body: widget.body,
260
- variant: "basic",
261
- display: "collapsed"
262
- }
369
+ state: chunk.type === "item.completed" && (chunk.item.type === "command_execution" && chunk.item.status === "failed" || chunk.item.type === "file_change" && chunk.item.status === "failed" || chunk.item.type === "mcp_tool_call" && chunk.item.status === "failed" || chunk.item.type === "error") ? "error" : chunk.type === "item.completed" ? "submitted" : void 0
370
+ })
371
+ });
372
+ }
373
+ const diff = buildDiffWidget({
374
+ widgetId: `codex-diff:${openbotThreadId ?? "run"}:${Date.now()}`,
375
+ files: resolveRunDiffFiles({
376
+ snapshot,
377
+ fallback: changedFiles.values()
378
+ })
379
+ });
380
+ if (diff) {
381
+ yield uiWidget({
382
+ agentId: context.agentId,
383
+ threadId: openbotThreadId,
384
+ widget: diff
263
385
  });
264
386
  }
265
387
  } catch (error) {
266
388
  yield* fail(errorText(error));
267
389
  }
268
390
  });
391
+ builder.on("client:ui:widget:response", async function* (event, ctx) {
392
+ const { metadata, values, widgetId } = event.data ?? {};
393
+ if (!metadata || metadata.type !== "api_key_request") return;
394
+ if (metadata.source !== "codex") return;
395
+ const apiKey = values?.apiKey;
396
+ if (typeof apiKey !== "string" || !apiKey) return;
397
+ const envVar = typeof metadata.envVar === "string" ? metadata.envVar : "OPENAI_API_KEY";
398
+ const storage = context.storage;
399
+ if (!storage) {
400
+ yield agentOutput({
401
+ agentId: context.agentId,
402
+ content: "[codex] no storage available; cannot persist API key.",
403
+ threadId: ctx.state.threadId
404
+ });
405
+ return;
406
+ }
407
+ try {
408
+ await storage.createVariable({ key: envVar, value: apiKey, secret: true });
409
+ process.env[envVar] = apiKey;
410
+ process.env.CODEX_API_KEY = apiKey;
411
+ yield uiWidget({
412
+ agentId: context.agentId,
413
+ widget: {
414
+ widgetId: widgetId ?? `codex_api_key_saved_${Date.now()}`,
415
+ kind: "message",
416
+ title: "API Key Saved",
417
+ body: `Saved ${envVar} as a workspace variable. You can now continue the conversation.`,
418
+ state: "submitted"
419
+ }
420
+ });
421
+ yield agentOutput({
422
+ agentId: context.agentId,
423
+ content: "Saved OpenAI API key to workspace variables. Re-send your last message to retry.",
424
+ threadId: ctx.state.threadId
425
+ });
426
+ } catch (error) {
427
+ const errorMessage = error instanceof Error ? error.message : String(error);
428
+ yield agentOutput({
429
+ agentId: context.agentId,
430
+ content: `[codex] failed to save API key: ${errorMessage}`,
431
+ threadId: ctx.state.threadId
432
+ });
433
+ }
434
+ });
269
435
  };
270
436
  }
271
437
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/codex",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "OpenAI Codex agent plugin for OpenBot",
6
6
  "main": "./dist/index.js",
@@ -17,20 +17,19 @@
17
17
  "files": [
18
18
  "dist"
19
19
  ],
20
- "scripts": {
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"
25
- },
26
20
  "dependencies": {
27
- "@meetopenbot/plugin-sdk": "workspace:^",
28
- "@openai/codex-sdk": "0.148.0"
21
+ "@openai/codex-sdk": "0.148.0",
22
+ "@meetopenbot/plugin-sdk": "^0.2.0"
29
23
  },
30
24
  "devDependencies": {
31
25
  "@types/node": "^25.6.0",
32
26
  "esbuild": "^0.21.0",
33
27
  "zod": "^4.4.3"
34
28
  },
35
- "types": "./dist/index.d.ts"
36
- }
29
+ "types": "./dist/index.d.ts",
30
+ "scripts": {
31
+ "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",
32
+ "dev": "esbuild index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@meetopenbot/plugin-sdk --external:@openai/codex-sdk --external:zod --watch",
33
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions --module ESNext --moduleResolution Bundler --target ES2022 --skipLibCheck index.ts"
34
+ }
35
+ }