bopodev-api 0.1.10 → 0.1.12

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 BopoHQ contributors
3
+ Copyright (c) 2026 BopoDev contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bopodev-api",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "files": [
@@ -16,9 +16,9 @@
16
16
  "nanoid": "^5.1.5",
17
17
  "ws": "^8.19.0",
18
18
  "zod": "^4.1.5",
19
- "bopodev-agent-sdk": "0.1.10",
20
- "bopodev-contracts": "0.1.10",
21
- "bopodev-db": "0.1.10"
19
+ "bopodev-contracts": "0.1.12",
20
+ "bopodev-db": "0.1.12",
21
+ "bopodev-agent-sdk": "0.1.12"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/cors": "^2.8.19",
@@ -34,7 +34,13 @@ export type NormalizedRuntimeConfig = {
34
34
  };
35
35
 
36
36
  export function requiresRuntimeCwd(providerType: string) {
37
- return providerType === "codex" || providerType === "claude_code" || providerType === "shell";
37
+ return (
38
+ providerType === "codex" ||
39
+ providerType === "claude_code" ||
40
+ providerType === "cursor" ||
41
+ providerType === "opencode" ||
42
+ providerType === "shell"
43
+ );
38
44
  }
39
45
 
40
46
  export function normalizeRuntimeConfig(input: {
@@ -1,4 +1,5 @@
1
1
  import type { NextFunction, Request, Response } from "express";
2
+ import { RequestActorHeadersSchema } from "bopodev-contracts";
2
3
  import { sendError } from "../http";
3
4
 
4
5
  export type RequestActor = {
@@ -18,27 +19,36 @@ declare global {
18
19
  }
19
20
  }
20
21
 
21
- export function attachRequestActor(req: Request, _res: Response, next: NextFunction) {
22
- const actorTypeHeader = req.header("x-actor-type")?.trim().toLowerCase();
23
- const actorTypeFromHeader =
24
- actorTypeHeader === "agent" || actorTypeHeader === "member" || actorTypeHeader === "board"
25
- ? actorTypeHeader
26
- : null;
27
- const actorIdHeader = req.header("x-actor-id")?.trim();
28
- const companyIdsHeader = req.header("x-actor-companies")?.trim();
29
- const permissionsHeader = req.header("x-actor-permissions")?.trim();
30
-
31
- const companyIds = parseCommaList(companyIdsHeader);
32
- const permissions = parseCommaList(permissionsHeader) ?? [];
33
- const hasActorHeaders = Boolean(actorTypeFromHeader || actorIdHeader || companyIds || permissions.length > 0);
22
+ export function attachRequestActor(req: Request, res: Response, next: NextFunction) {
23
+ const actorHeadersResult = RequestActorHeadersSchema.safeParse({
24
+ "x-actor-type": req.header("x-actor-type")?.trim().toLowerCase(),
25
+ "x-actor-id": req.header("x-actor-id")?.trim(),
26
+ "x-actor-companies": req.header("x-actor-companies")?.trim(),
27
+ "x-actor-permissions": req.header("x-actor-permissions")?.trim()
28
+ });
29
+ if (!actorHeadersResult.success) {
30
+ return sendError(
31
+ res,
32
+ `Invalid actor headers: ${actorHeadersResult.error.issues
33
+ .map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`)
34
+ .join("; ")}`,
35
+ 400
36
+ );
37
+ }
38
+ const actorHeaders = actorHeadersResult.data;
39
+ const companyIds = parseCommaList(actorHeaders["x-actor-companies"]);
40
+ const permissions = parseCommaList(actorHeaders["x-actor-permissions"]) ?? [];
41
+ const hasActorHeaders = Boolean(
42
+ actorHeaders["x-actor-type"] || actorHeaders["x-actor-id"] || companyIds || permissions.length > 0
43
+ );
34
44
  const allowLocalBoardFallback = process.env.NODE_ENV !== "production" && process.env.BOPO_ALLOW_LOCAL_BOARD_FALLBACK !== "0";
35
45
  const actorType = hasActorHeaders
36
- ? actorTypeFromHeader ?? "member"
46
+ ? actorHeaders["x-actor-type"] ?? "member"
37
47
  : allowLocalBoardFallback
38
48
  ? "board"
39
49
  : "member";
40
50
  const actorId = hasActorHeaders
41
- ? actorIdHeader || "unknown-actor"
51
+ ? actorHeaders["x-actor-id"] || "unknown-actor"
42
52
  : allowLocalBoardFallback
43
53
  ? "local-board"
44
54
  : "anonymous-member";
@@ -1,7 +1,11 @@
1
1
  import { Router } from "express";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { z } from "zod";
4
- import { executeAgentRuntime } from "bopodev-agent-sdk";
4
+ import {
5
+ getAdapterMetadata,
6
+ getAdapterModels,
7
+ runAdapterEnvironmentTest
8
+ } from "bopodev-agent-sdk";
5
9
  import { AgentCreateRequestSchema, AgentUpdateRequestSchema } from "bopodev-contracts";
6
10
  import {
7
11
  appendAuditEvent,
@@ -59,12 +63,10 @@ const updateAgentSchema = AgentUpdateRequestSchema.extend({
59
63
  });
60
64
 
61
65
  const runtimePreflightSchema = z.object({
62
- providerType: z.enum(["claude_code", "codex", "http", "shell"]),
66
+ providerType: z.enum(["claude_code", "codex", "cursor", "opencode", "http", "shell"]),
63
67
  runtimeConfig: z.record(z.string(), z.unknown()).optional(),
64
68
  ...legacyRuntimeConfigSchema.shape
65
69
  });
66
- const CODEX_AUTH_REQUIRED_RE =
67
- /(not\s+logged\s+in|login\s+required|authentication\s+required|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|openai[_\s-]?api[_\s-]?key|api[_\s-]?key.*required|missing bearer|missing bearer or basic authentication)/i;
68
70
  const UPDATE_AGENT_ALLOWED_KEYS = new Set([
69
71
  "managerAgentId",
70
72
  "role",
@@ -127,6 +129,35 @@ export function createAgentsRouter(ctx: AppContext) {
127
129
  return sendOk(res, { runtimeCwd });
128
130
  });
129
131
 
132
+ router.get("/adapter-metadata", async (_req, res) => {
133
+ return sendOk(res, { adapters: getAdapterMetadata() });
134
+ });
135
+
136
+ router.get("/adapter-models/:providerType", async (req, res) => {
137
+ const providerType = req.params.providerType;
138
+ if (!runtimePreflightSchema.shape.providerType.safeParse(providerType).success) {
139
+ return sendError(res, `Unsupported provider type: ${providerType}`, 422);
140
+ }
141
+ const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, req.companyId!);
142
+ const runtimeConfig = normalizeRuntimeConfig({
143
+ runtimeConfig: req.body?.runtimeConfig,
144
+ defaultRuntimeCwd
145
+ });
146
+ const typedProviderType = providerType as "claude_code" | "codex" | "cursor" | "opencode" | "http" | "shell";
147
+ const models = await getAdapterModels(typedProviderType, {
148
+ command: runtimeConfig.runtimeCommand,
149
+ args: runtimeConfig.runtimeArgs,
150
+ cwd: runtimeConfig.runtimeCwd,
151
+ env: runtimeConfig.runtimeEnv,
152
+ model: runtimeConfig.runtimeModel,
153
+ thinkingEffort: runtimeConfig.runtimeThinkingEffort,
154
+ timeoutMs: runtimeConfig.runtimeTimeoutSec > 0 ? runtimeConfig.runtimeTimeoutSec * 1000 : undefined,
155
+ interruptGraceSec: runtimeConfig.interruptGraceSec,
156
+ runPolicy: runtimeConfig.runPolicy
157
+ });
158
+ return sendOk(res, { providerType: typedProviderType, models });
159
+ });
160
+
130
161
  router.post("/runtime-preflight", async (req, res) => {
131
162
  const parsed = runtimePreflightSchema.safeParse(req.body);
132
163
  if (!parsed.success) {
@@ -151,42 +182,13 @@ export function createAgentsRouter(ctx: AppContext) {
151
182
  defaultRuntimeCwd
152
183
  });
153
184
 
154
- const checks: Array<{
155
- code: string;
156
- level: "info" | "warn" | "error";
157
- message: string;
158
- detail?: string;
159
- hint?: string;
160
- }> = [];
161
-
162
- if (parsed.data.providerType !== "codex") {
163
- return sendOk(res, {
164
- status: "pass",
165
- testedAt: new Date().toISOString(),
166
- checks: [
167
- {
168
- code: "preflight_not_required",
169
- level: "info",
170
- message: "Preflight probe is currently required only for Codex runtime."
171
- }
172
- ]
173
- });
174
- }
175
-
176
- if (!runtimeConfig.runtimeCwd) {
177
- checks.push({
178
- code: "codex_cwd_missing",
179
- level: "error",
180
- message: "Runtime working directory is required for Codex preflight."
181
- });
182
- return sendOk(res, { status: "fail", testedAt: new Date().toISOString(), checks });
185
+ if (runtimeConfig.runtimeCwd) {
186
+ await mkdir(runtimeConfig.runtimeCwd, { recursive: true });
183
187
  }
184
188
 
185
- await mkdir(runtimeConfig.runtimeCwd, { recursive: true });
186
-
187
189
  const timeoutMs =
188
- runtimeConfig.runtimeTimeoutSec > 0 ? Math.min(runtimeConfig.runtimeTimeoutSec * 1000, 45_000) : 45_000;
189
- const probe = await executeAgentRuntime("codex", "Respond with hello.", {
190
+ runtimeConfig.runtimeTimeoutSec > 0 ? Math.min(runtimeConfig.runtimeTimeoutSec * 1000, 45_000) : undefined;
191
+ const result = await runAdapterEnvironmentTest(parsed.data.providerType, {
190
192
  command: runtimeConfig.runtimeCommand,
191
193
  args: runtimeConfig.runtimeArgs,
192
194
  cwd: runtimeConfig.runtimeCwd,
@@ -195,65 +197,13 @@ export function createAgentsRouter(ctx: AppContext) {
195
197
  thinkingEffort: runtimeConfig.runtimeThinkingEffort,
196
198
  runPolicy: runtimeConfig.runPolicy,
197
199
  timeoutMs,
200
+ interruptGraceSec: runtimeConfig.interruptGraceSec,
198
201
  retryCount: 0
199
202
  });
200
-
201
- if (probe.ok) {
202
- const summary = (probe.parsedUsage?.summary ?? "").trim();
203
- const hasHello = /\bhello\b/i.test(summary || probe.stdout);
204
- checks.push({
205
- code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
206
- level: hasHello ? "info" : "warn",
207
- message: hasHello ? "Codex preflight probe succeeded." : "Codex probe succeeded but response was unexpected.",
208
- ...(summary ? { detail: summary.slice(0, 240) } : {})
209
- });
210
- } else {
211
- const detail = `${probe.stderr || ""}\n${probe.stdout || ""}`.trim().slice(0, 500);
212
- if (probe.failureType === "spawn_error") {
213
- checks.push({
214
- code: "codex_command_unresolvable",
215
- level: "error",
216
- message: "Codex command is not executable from this runtime configuration.",
217
- detail,
218
- hint: "Install Codex CLI or set runtime command to an executable path."
219
- });
220
- } else if (probe.timedOut) {
221
- checks.push({
222
- code: "codex_hello_probe_timed_out",
223
- level: "warn",
224
- message: "Codex preflight timed out.",
225
- detail,
226
- hint: "Retry preflight. If this repeats, check runtime command/cwd and local Codex health."
227
- });
228
- } else if (CODEX_AUTH_REQUIRED_RE.test(detail)) {
229
- checks.push({
230
- code: "codex_auth_required",
231
- level: "warn",
232
- message: "Codex authentication is not ready for this runtime.",
233
- detail,
234
- hint: "Run `codex login` locally or set a global `BOPO_OPENAI_API_KEY`/`OPENAI_API_KEY`."
235
- });
236
- } else {
237
- checks.push({
238
- code: "codex_hello_probe_failed",
239
- level: "error",
240
- message: "Codex preflight failed.",
241
- detail,
242
- hint: "Run `codex exec --json -` manually in the runtime directory with prompt `Respond with hello.`."
243
- });
244
- }
245
- }
246
-
247
- const status =
248
- checks.some((check) => check.level === "error")
249
- ? "fail"
250
- : checks.some((check) => check.level === "warn")
251
- ? "warn"
252
- : "pass";
253
203
  return sendOk(res, {
254
- status,
255
- testedAt: new Date().toISOString(),
256
- checks
204
+ status: result.status,
205
+ testedAt: result.testedAt,
206
+ checks: result.checks
257
207
  });
258
208
  });
259
209
 
@@ -6,11 +6,14 @@ import type { AppContext } from "../context";
6
6
  import { sendError, sendOk } from "../http";
7
7
  import { requireCompanyScope } from "../middleware/company-scope";
8
8
  import { requirePermission } from "../middleware/request-actor";
9
- import { runHeartbeatForAgent, runHeartbeatSweep } from "../services/heartbeat-service";
9
+ import { runHeartbeatForAgent, runHeartbeatSweep, stopHeartbeatRun } from "../services/heartbeat-service";
10
10
 
11
11
  const runAgentSchema = z.object({
12
12
  agentId: z.string().min(1)
13
13
  });
14
+ const runIdParamsSchema = z.object({
15
+ runId: z.string().min(1)
16
+ });
14
17
 
15
18
  export function createHeartbeatRouter(ctx: AppContext) {
16
19
  const router = Router();
@@ -64,6 +67,146 @@ export function createHeartbeatRouter(ctx: AppContext) {
64
67
  });
65
68
  });
66
69
 
70
+ router.post("/:runId/stop", async (req, res) => {
71
+ requirePermission("heartbeats:run")(req, res, () => {});
72
+ if (res.headersSent) {
73
+ return;
74
+ }
75
+ const parsed = runIdParamsSchema.safeParse(req.params);
76
+ if (!parsed.success) {
77
+ return sendError(res, parsed.error.message, 422);
78
+ }
79
+ const stopResult = await stopHeartbeatRun(ctx.db, req.companyId!, parsed.data.runId, {
80
+ requestId: req.requestId,
81
+ trigger: "manual",
82
+ actorId: req.actor?.id ?? undefined
83
+ });
84
+ if (!stopResult.ok) {
85
+ if (stopResult.reason === "not_found") {
86
+ return sendError(res, "Heartbeat run not found.", 404);
87
+ }
88
+ return sendError(res, `Heartbeat run is not stoppable in status '${stopResult.status}'.`, 409);
89
+ }
90
+ return sendOk(res, {
91
+ runId: stopResult.runId,
92
+ requestId: req.requestId,
93
+ status: "stop_requested"
94
+ });
95
+ });
96
+
97
+ async function rerunFromHistory(input: {
98
+ mode: "resume" | "redo";
99
+ runId: string;
100
+ companyId: string;
101
+ requestId?: string;
102
+ }) {
103
+ const [run] = await ctx.db
104
+ .select({
105
+ id: heartbeatRuns.id,
106
+ status: heartbeatRuns.status,
107
+ agentId: heartbeatRuns.agentId
108
+ })
109
+ .from(heartbeatRuns)
110
+ .where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, input.runId)))
111
+ .limit(1);
112
+ if (!run) {
113
+ return { ok: false as const, statusCode: 404, message: "Heartbeat run not found." };
114
+ }
115
+ if (run.status === "started") {
116
+ return { ok: false as const, statusCode: 409, message: "Run is still in progress and cannot be replayed yet." };
117
+ }
118
+ const [agent] = await ctx.db
119
+ .select({ id: agents.id, status: agents.status })
120
+ .from(agents)
121
+ .where(and(eq(agents.companyId, input.companyId), eq(agents.id, run.agentId)))
122
+ .limit(1);
123
+ if (!agent) {
124
+ return { ok: false as const, statusCode: 404, message: "Agent not found." };
125
+ }
126
+ if (agent.status === "paused" || agent.status === "terminated") {
127
+ return { ok: false as const, statusCode: 409, message: `Agent is not invokable in status '${agent.status}'.` };
128
+ }
129
+ const nextRunId = await runHeartbeatForAgent(ctx.db, input.companyId, run.agentId, {
130
+ requestId: input.requestId,
131
+ trigger: "manual",
132
+ realtimeHub: ctx.realtimeHub,
133
+ mode: input.mode,
134
+ sourceRunId: run.id
135
+ });
136
+ if (!nextRunId) {
137
+ return { ok: false as const, statusCode: 409, message: "Heartbeat could not be started for this agent." };
138
+ }
139
+ const [runRow] = await ctx.db
140
+ .select({ id: heartbeatRuns.id, status: heartbeatRuns.status, message: heartbeatRuns.message })
141
+ .from(heartbeatRuns)
142
+ .where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, nextRunId)))
143
+ .limit(1);
144
+ const invokeStatus =
145
+ runRow?.status === "skipped" && String(runRow.message ?? "").includes("already in progress")
146
+ ? "skipped_overlap"
147
+ : runRow?.status === "skipped"
148
+ ? "skipped"
149
+ : "started";
150
+ return {
151
+ ok: true as const,
152
+ runId: nextRunId,
153
+ status: invokeStatus,
154
+ message: runRow?.message ?? null
155
+ };
156
+ }
157
+
158
+ router.post("/:runId/resume", async (req, res) => {
159
+ requirePermission("heartbeats:run")(req, res, () => {});
160
+ if (res.headersSent) {
161
+ return;
162
+ }
163
+ const parsed = runIdParamsSchema.safeParse(req.params);
164
+ if (!parsed.success) {
165
+ return sendError(res, parsed.error.message, 422);
166
+ }
167
+ const result = await rerunFromHistory({
168
+ mode: "resume",
169
+ runId: parsed.data.runId,
170
+ companyId: req.companyId!,
171
+ requestId: req.requestId
172
+ });
173
+ if (!result.ok) {
174
+ return sendError(res, result.message, result.statusCode);
175
+ }
176
+ return sendOk(res, {
177
+ runId: result.runId,
178
+ requestId: req.requestId,
179
+ status: result.status,
180
+ message: result.message
181
+ });
182
+ });
183
+
184
+ router.post("/:runId/redo", async (req, res) => {
185
+ requirePermission("heartbeats:run")(req, res, () => {});
186
+ if (res.headersSent) {
187
+ return;
188
+ }
189
+ const parsed = runIdParamsSchema.safeParse(req.params);
190
+ if (!parsed.success) {
191
+ return sendError(res, parsed.error.message, 422);
192
+ }
193
+ const result = await rerunFromHistory({
194
+ mode: "redo",
195
+ runId: parsed.data.runId,
196
+ companyId: req.companyId!,
197
+ requestId: req.requestId
198
+ });
199
+ if (!result.ok) {
200
+ return sendError(res, result.message, result.statusCode);
201
+ }
202
+ return sendOk(res, {
203
+ runId: result.runId,
204
+ requestId: req.requestId,
205
+ status: result.status,
206
+ message: result.message
207
+ });
208
+ });
209
+
67
210
  router.post("/sweep", async (req, res) => {
68
211
  requirePermission("heartbeats:sweep")(req, res, () => {});
69
212
  if (res.headersSent) {
@@ -31,7 +31,30 @@ export function createObservabilityRouter(ctx: AppContext) {
31
31
  });
32
32
 
33
33
  router.get("/heartbeats", async (req, res) => {
34
- return sendOk(res, await listHeartbeatRuns(ctx.db, req.companyId!));
34
+ const companyId = req.companyId!;
35
+ const [runs, auditRows] = await Promise.all([
36
+ listHeartbeatRuns(ctx.db, companyId),
37
+ listAuditEvents(ctx.db, companyId)
38
+ ]);
39
+ const outcomeByRunId = new Map<string, unknown>();
40
+ for (const row of auditRows) {
41
+ if (
42
+ row.entityType === "heartbeat_run" &&
43
+ (row.eventType === "heartbeat.completed" || row.eventType === "heartbeat.failed")
44
+ ) {
45
+ const payload = parsePayload(row.payloadJson);
46
+ if (payload && typeof payload === "object" && "outcome" in payload) {
47
+ outcomeByRunId.set(row.entityId, (payload as Record<string, unknown>).outcome ?? null);
48
+ }
49
+ }
50
+ }
51
+ return sendOk(
52
+ res,
53
+ runs.map((run) => ({
54
+ ...run,
55
+ outcome: outcomeByRunId.get(run.id) ?? null
56
+ }))
57
+ );
35
58
  });
36
59
 
37
60
  return router;
@@ -29,11 +29,11 @@ export interface OnboardSeedSummary {
29
29
  const DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
30
30
  const DEFAULT_COMPANY_ID_ENV = "BOPO_DEFAULT_COMPANY_ID";
31
31
  const DEFAULT_AGENT_PROVIDER_ENV = "BOPO_DEFAULT_AGENT_PROVIDER";
32
- type AgentProvider = "codex" | "claude_code" | "shell";
32
+ type AgentProvider = "codex" | "claude_code" | "cursor" | "opencode" | "shell";
33
33
  const CEO_BOOTSTRAP_SUMMARY = "ceo bootstrap heartbeat";
34
34
  const STARTUP_PROJECT_NAME = "Leadership Setup";
35
35
  const CEO_STARTUP_TASK_TITLE = "Set up CEO operating files and hire founding engineer";
36
- const CEO_STARTUP_TASK_MARKER = "[bopohq:onboarding:ceo-startup:v1]";
36
+ const CEO_STARTUP_TASK_MARKER = "[bopodev:onboarding:ceo-startup:v1]";
37
37
 
38
38
  export async function ensureOnboardingSeed(input: {
39
39
  dbPath?: string;
@@ -188,8 +188,8 @@ async function ensureCeoStartupTask(
188
188
  " - If using `runtimeConfig`, only `runtimeConfig.bootstrapPrompt` is supported there.",
189
189
  " - Use a temp JSON file or heredoc with `curl --data @file`; do not hand-escape multiline JSON.",
190
190
  "4. To inspect your own agent record, use `GET /agents` and filter by your agent id. Do not call `GET /agents/:agentId`.",
191
- " - `GET /agents` may be wrapped as `{ \"ok\": true, \"data\": [...] }` or returned as a raw array.",
192
- " - Shape-safe filter: `jq -er --arg id \"$BOPOHQ_AGENT_ID\" '(.data? // .) | if type==\"array\" then . else [] end | map(select(.id == $id))[0] | {id,name,role,bootstrapPrompt}'`",
191
+ " - `GET /agents` uses envelope shape `{ \"ok\": true, \"data\": [...] }`; treat any other shape as failure.",
192
+ " - Deterministic filter: `jq -er --arg id \"$BOPODEV_AGENT_ID\" '.data | if type==\"array\" then . else error(\"invalid_agents_payload\") end | map(select((.id? // \"\") == $id))[0] | {id,name,role,bootstrapPrompt}'`",
193
193
  "5. Heartbeat-assigned issues are already claimed for the current run. Do not call a checkout endpoint; update status with `PUT /issues/:issueId` only.",
194
194
  "6. After your operating files are active, submit a hire request for a Founding Engineer via `POST /agents` using supported fields:",
195
195
  " - `name`, `role`, `providerType`, `heartbeatCron`, `monthlyBudgetUsd`",
@@ -200,6 +200,7 @@ async function ensureCeoStartupTask(
200
200
  "Safety checks before requesting hire:",
201
201
  "- Do not request duplicates if a Founding Engineer already exists.",
202
202
  "- Do not request duplicates if a pending approval for the same role is already open.",
203
+ "- For control-plane calls, prefer direct header env vars (`BOPODEV_COMPANY_ID`, `BOPODEV_ACTOR_TYPE`, `BOPODEV_ACTOR_ID`, `BOPODEV_ACTOR_COMPANIES`, `BOPODEV_ACTOR_PERMISSIONS`) instead of parsing `BOPODEV_REQUEST_HEADERS_JSON`.",
203
204
  "- Do not assume `python` is installed in the runtime shell; prefer direct headers, `node`, or `jq` when scripting.",
204
205
  "- Shell commands run under `zsh`; avoid Bash-only features such as `local -n`, `declare -n`, `mapfile`, and `readarray`."
205
206
  ].join("\n");
@@ -247,23 +248,32 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
247
248
  }
248
249
 
249
250
  function parseAgentProvider(value: unknown): AgentProvider | null {
250
- if (value === "codex" || value === "claude_code" || value === "shell") {
251
+ if (value === "codex" || value === "claude_code" || value === "cursor" || value === "opencode" || value === "shell") {
251
252
  return value;
252
253
  }
253
254
  return null;
254
255
  }
255
256
 
256
257
  function resolveSeedRuntimeEnv(agentProvider: AgentProvider): Record<string, string> {
257
- if (agentProvider !== "codex") {
258
- return {};
258
+ if (agentProvider === "codex") {
259
+ const key = (process.env.BOPO_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY)?.trim();
260
+ if (!key) {
261
+ return {};
262
+ }
263
+ return {
264
+ OPENAI_API_KEY: key
265
+ };
259
266
  }
260
- const key = (process.env.BOPO_OPENAI_API_KEY ?? process.env.OPENAI_API_KEY)?.trim();
261
- if (!key) {
262
- return {};
267
+ if (agentProvider === "claude_code") {
268
+ const key = (process.env.BOPO_ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)?.trim();
269
+ if (!key) {
270
+ return {};
271
+ }
272
+ return {
273
+ ANTHROPIC_API_KEY: key
274
+ };
263
275
  }
264
- return {
265
- OPENAI_API_KEY: key
266
- };
276
+ return {};
267
277
  }
268
278
 
269
279
  function isBootstrapCeoRuntime(providerType: string, stateBlob: string | null) {
package/src/server.ts CHANGED
@@ -61,7 +61,7 @@ async function main() {
61
61
  server.on("request", app);
62
62
  server.listen(port, () => {
63
63
  // eslint-disable-next-line no-console
64
- console.log(`BopoHQ API running on http://localhost:${port}`);
64
+ console.log(`BopoDev API running on http://localhost:${port}`);
65
65
  });
66
66
 
67
67
  const defaultCompanyId = process.env.BOPO_DEFAULT_COMPANY_ID;
@@ -57,7 +57,7 @@ const activateGoalPayloadSchema = z.object({
57
57
  description: z.string().optional()
58
58
  });
59
59
  const AGENT_STARTUP_PROJECT_NAME = "Agent Onboarding";
60
- const AGENT_STARTUP_TASK_MARKER = "[bopohq:onboarding:agent-startup:v1]";
60
+ const AGENT_STARTUP_TASK_MARKER = "[bopodev:onboarding:agent-startup:v1]";
61
61
 
62
62
  export class GovernanceError extends Error {
63
63
  constructor(message: string) {