broods 0.2.1 → 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/dist/cli/index.js CHANGED
@@ -2,18 +2,19 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli/index.ts
5
- import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
5
+ import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, writeFile as writeFile4 } from "fs/promises";
6
6
  import { createHash as createHash2 } from "crypto";
7
7
  import { basename as basename2, dirname as dirname2, join as join4, relative as relative2, resolve as resolve4 } from "path";
8
8
  import { watch } from "fs";
9
9
  import { spawn as spawn2 } from "child_process";
10
- import { tmpdir } from "os";
10
+ import { tmpdir as tmpdir2 } from "os";
11
11
  import { performance } from "perf_hooks";
12
12
 
13
13
  // src/manifest.ts
14
14
  import { pathToFileURL } from "url";
15
15
  import { createHash } from "crypto";
16
- import { readdir, readFile as readFile2 } from "fs/promises";
16
+ import { mkdtemp, readdir, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
17
+ import { tmpdir } from "os";
17
18
  import { basename, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
18
19
 
19
20
  // src/config.ts
@@ -23,6 +24,9 @@ import { dirname, join } from "path";
23
24
  var PROJECT_DIR = "broods";
24
25
  var GENERATED_DIR = "_generated";
25
26
  var USER_CONFIG_PATH = join(homedir(), ".broods", "config.json");
27
+ var BROODS_APEX_DOMAIN = "broods.app";
28
+ var DASHBOARD_HOST_PREFIX = "dashboard";
29
+ var GATEWAY_HOST_PREFIX = "gateway";
26
30
  async function readStoredAuth() {
27
31
  const envToken = process.env.BROODS_TOKEN;
28
32
  const envConvexUrl = process.env.BROODS_BASE_URL;
@@ -50,6 +54,20 @@ async function writeStoredAuth(config) {
50
54
  function stripTrailingSlash(value) {
51
55
  return value.replace(/\/+$/, "");
52
56
  }
57
+ function gatewayUrlForDashboard(dashboardUrl) {
58
+ try {
59
+ const url = new URL(dashboardUrl);
60
+ if (!url.hostname.startsWith(`${DASHBOARD_HOST_PREFIX}.`))
61
+ return;
62
+ const rest = url.hostname.slice(DASHBOARD_HOST_PREFIX.length + 1);
63
+ if (rest !== BROODS_APEX_DOMAIN && !rest.endsWith(`.${BROODS_APEX_DOMAIN}`))
64
+ return;
65
+ url.hostname = `${GATEWAY_HOST_PREFIX}.${rest}`;
66
+ return stripTrailingSlash(url.origin);
67
+ } catch {
68
+ return;
69
+ }
70
+ }
53
71
 
54
72
  // src/runtime-config.ts
55
73
  import { existsSync, readFileSync, statSync } from "fs";
@@ -137,25 +155,27 @@ function readStoredAuthSync() {
137
155
  var RESOURCE_MARKER = Symbol.for("broods.resource");
138
156
  var CONFIG_MARKER = Symbol.for("broods.config");
139
157
  var CHANNEL_MARKER = Symbol.for("broods.channel");
140
- var env = new Proxy(function env2(name) {
158
+ var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
159
+ var env = new Proxy((name) => {
160
+ if (!ENV_NAME_PATTERN.test(name)) {
161
+ throw new Error("env name must match /^[A-Z][A-Z0-9_]*$/ and be at most 64 characters.");
162
+ }
141
163
  return { __beeblastEnv: true, name };
142
164
  }, {
143
165
  get(target, property, receiver) {
144
- if (typeof property === "string")
145
- return { __beeblastEnv: true, name: property };
166
+ if (typeof property === "string" && ENV_NAME_PATTERN.test(property)) {
167
+ throw new Error(`env.${property} is not supported; use env("${property}")`);
168
+ }
146
169
  return Reflect.get(target, property, receiver);
147
170
  }
148
171
  });
149
- function defineResource(kind, input) {
150
- if (input.config === undefined) {
151
- throw new Error(`Resource "${input.name}" must include config`);
152
- }
172
+ function defineResource(kind, name, description, config) {
153
173
  return {
154
174
  [RESOURCE_MARKER]: true,
155
175
  kind,
156
- name: input.name,
157
- ...input.description ? { description: input.description } : {},
158
- config: input.config
176
+ name,
177
+ ...description ? { description } : {},
178
+ config
159
179
  };
160
180
  }
161
181
  function defineChannel(type, config) {
@@ -193,25 +213,32 @@ function defineBroods(config) {
193
213
  return { [CONFIG_MARKER]: true, config };
194
214
  }
195
215
  function defineAgent(input) {
196
- return defineResource("agent", input);
216
+ const { name, description, ...config } = input;
217
+ return defineResource("agent", name, description, config);
197
218
  }
198
219
  function defineWorkspace(input) {
199
- return defineResource("workspace", input);
220
+ const { name, description, ...config } = input;
221
+ return defineResource("workspace", name, description, config);
200
222
  }
201
223
  function defineSandbox(input) {
202
- return defineResource("sandbox", input);
224
+ const { name, description, ...config } = input;
225
+ return defineResource("sandbox", name, description, config);
203
226
  }
204
227
  function defineSkill(input) {
205
- return defineResource("skill", input);
228
+ const { name, description, ...config } = input;
229
+ return defineResource("skill", name, description, config);
206
230
  }
207
231
  function defineTool(input) {
208
- return defineResource("tool", input);
232
+ const { name, ...config } = input;
233
+ return defineResource("tool", name, undefined, config);
209
234
  }
210
235
  function definePolicy(input) {
211
- return defineResource("policy", input);
236
+ const { name, description, ...config } = input;
237
+ return defineResource("policy", name, description, config);
212
238
  }
213
239
  function defineCron(input) {
214
- return defineResource("cron", input);
240
+ const { name, description, ...config } = input;
241
+ return defineResource("cron", name, description, config);
215
242
  }
216
243
  function isResource(value) {
217
244
  return Boolean(value && typeof value === "object" && value[RESOURCE_MARKER]);
@@ -224,8 +251,20 @@ function isBroodsConfig(value) {
224
251
  }
225
252
 
226
253
  // src/manifest.ts
227
- var MAX_BUNDLE_FILE_BYTES = 1e6;
228
- var MAX_BUNDLE_TOTAL_BYTES = 5000000;
254
+ var SDK_STUB_SOURCE = `const passthrough = (input) => input;
255
+ export const defineTool = passthrough;
256
+ export const defineAgent = passthrough;
257
+ export const defineWorkspace = passthrough;
258
+ export const defineSandbox = passthrough;
259
+ export const defineSkill = passthrough;
260
+ export const definePolicy = passthrough;
261
+ export const defineCron = passthrough;
262
+ export const defineBroods = passthrough;
263
+ export const env = (name) => ({ __beeblastEnv: true, name });
264
+ export default {};
265
+ `;
266
+ var MAX_BUNDLE_FILE_BYTES = 1e7;
267
+ var MAX_BUNDLE_TOTAL_BYTES = 20000000;
229
268
  var MAX_BUNDLE_FILES = 200;
230
269
  var SKIPPED_BUNDLE_DIRECTORIES = new Set(["node_modules", ".git"]);
231
270
  var UNSAFE_BUNDLE_FILE_NAMES = [
@@ -252,7 +291,11 @@ async function compileProject(options = {}) {
252
291
  const files = await listTypeScriptFiles(root);
253
292
  const exports = await loadExports(files);
254
293
  const config = await findConfig(exports, cwd, options.project);
255
- const resourceExports = exports.filter((entry) => isResource(entry.value)).map((entry) => ({ exportName: entry.exportName, resource: entry.value }));
294
+ const resourceExports = exports.filter((entry) => isResource(entry.value)).map((entry) => ({
295
+ exportName: entry.exportName,
296
+ file: entry.file,
297
+ resource: entry.value
298
+ }));
256
299
  const resources = resourceExports.map((entry) => entry.resource);
257
300
  assertUniqueResources(resources);
258
301
  const channels = compileChannels(resourceExports, exports);
@@ -266,7 +309,7 @@ async function compileProject(options = {}) {
266
309
  assertSupportedWorkspaceSandboxMounts(resources);
267
310
  const resourceAliases = aliasesForResources(resourceExports);
268
311
  const environment = resolveEnvironment(config, options.environment ?? (options.useRuntimeEnvironment === false ? undefined : process.env.BROODS_ENVIRONMENT), options.command ?? "dev");
269
- const manifestResources = (await Promise.all(resources.map((resource) => toManifestResources(resource, root)))).flat().sort((a, b) => `${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`));
312
+ const manifestResources = (await Promise.all(resourceExports.map((entry) => toManifestResources(entry, root)))).flat().sort((a, b) => `${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`));
270
313
  return {
271
314
  config,
272
315
  resources,
@@ -282,25 +325,25 @@ async function compileProject(options = {}) {
282
325
  }
283
326
  function collectEnvRefNames(manifest) {
284
327
  const names = new Set;
285
- function walk(value) {
286
- if (Array.isArray(value)) {
287
- for (const entry of value)
288
- walk(entry);
328
+ for (const resource of manifest.resources)
329
+ collectEnvRefNamesFromValue(resource.config, names);
330
+ return [...names].sort();
331
+ }
332
+ function collectEnvRefNamesFromValue(value, names) {
333
+ if (Array.isArray(value)) {
334
+ for (const entry of value)
335
+ collectEnvRefNamesFromValue(entry, names);
336
+ return;
337
+ }
338
+ if (value && typeof value === "object") {
339
+ const record = value;
340
+ if (record.__beeblastEnv === true && typeof record.name === "string") {
341
+ names.add(record.name);
289
342
  return;
290
343
  }
291
- if (value && typeof value === "object") {
292
- const record = value;
293
- if (record.__beeblastEnv === true && typeof record.name === "string") {
294
- names.add(record.name);
295
- return;
296
- }
297
- for (const entry of Object.values(record))
298
- walk(entry);
299
- }
344
+ for (const entry of Object.values(record))
345
+ collectEnvRefNamesFromValue(entry, names);
300
346
  }
301
- for (const resource of manifest.resources)
302
- walk(resource.config);
303
- return [...names].sort();
304
347
  }
305
348
  function resolveEnvironment(config, explicit, command) {
306
349
  if (explicit)
@@ -333,7 +376,11 @@ async function loadExports(files) {
333
376
  for (const file of files) {
334
377
  const href = `${pathToFileURL(file).href}?t=${Date.now()}`;
335
378
  const mod = await import(href);
336
- values.push(...Object.entries(mod).map(([exportName, value]) => ({ exportName, value })));
379
+ values.push(...Object.entries(mod).map(([exportName, value]) => ({
380
+ exportName,
381
+ file,
382
+ value
383
+ })));
337
384
  }
338
385
  return values;
339
386
  }
@@ -550,7 +597,7 @@ function supportsS3WorkspaceMount(sandbox) {
550
597
  return Boolean(options && typeof options === "object" && !Array.isArray(options) && options.mountAwsS3Buckets === true);
551
598
  }
552
599
  function sandboxProvider(sandbox) {
553
- return typeof sandbox.config.provider === "string" ? sandbox.config.provider : "lambda";
600
+ return typeof sandbox.config.provider === "string" ? sandbox.config.provider : "sandbox";
554
601
  }
555
602
  function assertUniqueResources(resources) {
556
603
  const seen = new Set;
@@ -640,7 +687,8 @@ function capitalize(value) {
640
687
  function isValidIdentifier(value) {
641
688
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
642
689
  }
643
- async function toManifestResources(resource, projectRoot) {
690
+ async function toManifestResources(entry, projectRoot) {
691
+ const resource = entry.resource;
644
692
  if (resource.kind === "agent") {
645
693
  const normalized = normalizeAgentConfig(resource, projectRoot);
646
694
  return [
@@ -653,19 +701,22 @@ async function toManifestResources(resource, projectRoot) {
653
701
  }
654
702
  ];
655
703
  }
656
- return [{
657
- kind: resource.kind,
658
- name: resource.name,
659
- ...resource.description ? { description: resource.description } : {},
660
- config: await normalizeConfig(resource, projectRoot)
661
- }];
704
+ return [
705
+ {
706
+ kind: resource.kind,
707
+ name: resource.name,
708
+ ...resource.description ? { description: resource.description } : {},
709
+ config: await normalizeConfig(entry, projectRoot)
710
+ }
711
+ ];
662
712
  }
663
- async function normalizeConfig(resource, projectRoot) {
713
+ async function normalizeConfig(entry, projectRoot) {
714
+ const resource = entry.resource;
664
715
  if (resource.kind === "skill") {
665
716
  return await normalizeSkillConfig(resource.config, projectRoot);
666
717
  }
667
718
  if (resource.kind === "tool") {
668
- return await normalizeToolConfig(resource.config, projectRoot);
719
+ return await normalizeToolConfig(entry, resource.config, projectRoot);
669
720
  }
670
721
  if (resource.kind === "policy") {
671
722
  const config = { ...resource.config };
@@ -679,14 +730,24 @@ async function normalizeConfig(resource, projectRoot) {
679
730
  config.name = config.name ?? resource.name;
680
731
  delete config.agent;
681
732
  if (typeof config.input === "string") {
682
- config.events = [{ role: "user", content: [{ type: "text", text: config.input }] }];
733
+ config.events = [
734
+ { role: "user", content: [{ type: "text", text: config.input }] }
735
+ ];
683
736
  delete config.input;
684
737
  }
685
738
  return rewriteValues(config);
686
739
  }
687
740
  return rewriteValues(resource.config);
688
741
  }
689
- var KNOWN_PROVIDER_NAMES = ["google", "openai", "anthropic", "bedrock", "gateway", "minimax", "custom"];
742
+ var KNOWN_PROVIDER_NAMES = [
743
+ "google",
744
+ "openai",
745
+ "anthropic",
746
+ "bedrock",
747
+ "gateway",
748
+ "minimax",
749
+ "custom"
750
+ ];
690
751
  var KNOWN_PROVIDER_SETTING_KEYS = new Set([
691
752
  "apiKey",
692
753
  "base_url",
@@ -786,11 +847,13 @@ function normalizeInlineAgentHooks(agentName, hooks) {
786
847
  if (typeof handler !== "function") {
787
848
  throw new Error(`Agent "${agentName}" config.hooks.${name} must be a function`);
788
849
  }
789
- return [{
790
- name,
791
- event: INLINE_AGENT_HOOK_EVENTS[name],
792
- source: toHookFunctionExpression(handler.toString())
793
- }];
850
+ return [
851
+ {
852
+ name,
853
+ event: INLINE_AGENT_HOOK_EVENTS[name],
854
+ source: toHookFunctionExpression(handler.toString())
855
+ }
856
+ ];
794
857
  });
795
858
  if (entries.length === 0)
796
859
  return;
@@ -904,26 +967,43 @@ async function normalizeSkillConfig(config, projectRoot) {
904
967
  files
905
968
  };
906
969
  }
907
- async function normalizeToolConfig(config, projectRoot) {
908
- const bundlePath = resolveContainedResourcePath(projectRoot, config.path, "Tool");
970
+ async function normalizeToolConfig(entry, config, projectRoot) {
971
+ const defaultConfigEnvRefs = new Set;
972
+ collectEnvRefNamesFromValue(config.defaultConfig, defaultConfigEnvRefs);
973
+ if (defaultConfigEnvRefs.size > 0) {
974
+ throw new Error(`Tool "${entry.resource.name}" defaultConfig cannot contain env("NAME") references; put environment values in the agent's tools.<tool>.config so they stay encrypted and environment-scoped`);
975
+ }
976
+ const bundlePath = config.path ? resolveContainedResourcePath(projectRoot, config.path, "Tool") : entry.file;
909
977
  const manifestPath = relative(projectRoot, bundlePath).split("\\").join("/");
910
978
  assertSafeBundlePath(manifestPath, "Tool");
911
979
  const sourceSize = Buffer.byteLength(await readFile2(bundlePath));
912
980
  if (sourceSize > MAX_BUNDLE_FILE_BYTES) {
913
981
  throw new Error(`Tool source ${manifestPath} is too large (${sourceSize} bytes, max ${MAX_BUNDLE_FILE_BYTES})`);
914
982
  }
915
- const build = await Bun.build({
916
- entrypoints: [bundlePath],
917
- root: projectRoot,
918
- target: "node",
919
- format: "esm",
920
- minify: false
921
- });
922
- if (!build.success || build.outputs.length !== 1) {
923
- const details = build.logs.map((entry) => entry.message).filter(Boolean).join("; ");
924
- throw new Error(`Tool bundle ${manifestPath} failed to build${details ? `: ${details}` : ""}`);
983
+ const shimDir = await mkdtemp(join3(tmpdir(), "broods-tool-"));
984
+ const shimPath = join3(shimDir, "tool-adapter.mjs");
985
+ await writeFile2(shimPath, toolShimSource(bundlePath, config.path, entry), "utf8");
986
+ if (!config.path) {
987
+ await writeFile2(join3(shimDir, "broods-stub.mjs"), SDK_STUB_SOURCE, "utf8");
925
988
  }
926
- const bundle = await build.outputs[0].text();
989
+ let bundle;
990
+ try {
991
+ const build = await Bun.build({
992
+ entrypoints: [shimPath],
993
+ target: "node",
994
+ format: "esm",
995
+ minify: false,
996
+ plugins: config.path ? [] : [sdkStubPlugin(shimDir)]
997
+ });
998
+ if (!build.success || build.outputs.length !== 1) {
999
+ const details = build.logs.map((entry2) => entry2.message).filter(Boolean).join("; ");
1000
+ throw new Error(`Tool bundle ${manifestPath} failed to build${details ? `: ${details}` : ""}`);
1001
+ }
1002
+ bundle = await build.outputs[0].text();
1003
+ } finally {
1004
+ await rm(shimDir, { recursive: true, force: true });
1005
+ }
1006
+ bundle = normalizeBundleComments(bundle, bundlePath, manifestPath);
927
1007
  const bundleSize = Buffer.byteLength(bundle);
928
1008
  if (bundleSize > MAX_BUNDLE_FILE_BYTES) {
929
1009
  throw new Error(`Tool bundle ${manifestPath} is too large (${bundleSize} bytes, max ${MAX_BUNDLE_FILE_BYTES})`);
@@ -948,7 +1028,10 @@ function normalizeWorkspaceRef(entry, agentName) {
948
1028
  if (typeof name !== "string") {
949
1029
  throw new Error(`Agent ${agentName} workspace ref must be a defineWorkspace(...) resource or its name`);
950
1030
  }
951
- const normalized = { name, workspaceId: name };
1031
+ const normalized = {
1032
+ name,
1033
+ workspaceId: name
1034
+ };
952
1035
  if ("sandbox" in entry) {
953
1036
  const sandbox = entry.sandbox;
954
1037
  normalized.sandbox = sandbox === null ? null : isResource(sandbox) ? sandbox.name : sandbox;
@@ -1048,21 +1131,49 @@ function contentTypeForPath(path) {
1048
1131
  return "text/plain; charset=utf-8";
1049
1132
  return "application/octet-stream";
1050
1133
  }
1134
+ function sdkStubPlugin(shimDir) {
1135
+ const stub = join3(shimDir, "broods-stub.mjs");
1136
+ return {
1137
+ name: "broods-sdk-stub",
1138
+ setup(build) {
1139
+ build.onResolve({ filter: /^broods(\/.*)?$/ }, () => ({ path: stub }));
1140
+ }
1141
+ };
1142
+ }
1143
+ function toolShimSource(bundlePath, explicitPath, entry) {
1144
+ const from = JSON.stringify(bundlePath);
1145
+ if (explicitPath) {
1146
+ return `import __toolModule from ${from};
1147
+ const __impl = typeof __toolModule === "function" ? __toolModule() : __toolModule;
1148
+ export default { name: __impl.name, execute: (input, options) => __impl.execute(options.context, input) };
1149
+ `;
1150
+ }
1151
+ return `import { ${entry.exportName} as __tool } from ${from};
1152
+ export default { name: ${JSON.stringify(entry.resource.name)}, execute: (input, options) => __tool.execute(input, options) };
1153
+ `;
1154
+ }
1155
+ function escapeRegExp(value) {
1156
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1157
+ }
1158
+ function normalizeBundleComments(bundle, bundlePath, manifestPath) {
1159
+ const sourceComment = new RegExp(`^// .*${escapeRegExp(basename(bundlePath))}$`, "gm");
1160
+ return bundle.replace(/^\/\/ .*tool-adapter\.mjs$/gm, () => "// tool-adapter.mjs").replace(/^\/\/ .*broods-stub\.mjs$/gm, () => "// broods-stub.mjs").replace(sourceComment, () => `// ${manifestPath}`);
1161
+ }
1051
1162
  function normalizeProjectName(name) {
1052
1163
  return name.replace(/^@/, "").replace(/\//g, "-").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
1053
1164
  }
1054
1165
 
1055
1166
  // src/codegen.ts
1056
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1167
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1057
1168
  import { resolve as resolve3 } from "path";
1058
1169
  async function writeGeneratedFiles(manifest, ids, cwd = process.cwd(), aliases = {}, endpoint = null, channels = []) {
1059
1170
  const dir = resolve3(cwd, PROJECT_DIR, GENERATED_DIR);
1060
1171
  const scopedIds = idsForManifest(ids, manifest);
1061
1172
  await mkdir2(dir, { recursive: true });
1062
- await writeFile2(resolve3(dir, "ids.ts"), idsFile(scopedIds), "utf8");
1063
- await writeFile2(resolve3(dir, "api.ts"), apiFile(manifest, scopedIds, aliases, endpoint, channels), "utf8");
1064
- await writeFile2(resolve3(dir, "resources.ts"), resourcesFile(manifest), "utf8");
1065
- await writeFile2(resolve3(dir, "dataModel.ts"), dataModelFile(), "utf8");
1173
+ await writeFile3(resolve3(dir, "ids.ts"), idsFile(scopedIds), "utf8");
1174
+ await writeFile3(resolve3(dir, "api.ts"), apiFile(manifest, scopedIds, aliases, endpoint, channels), "utf8");
1175
+ await writeFile3(resolve3(dir, "resources.ts"), resourcesFile(manifest), "utf8");
1176
+ await writeFile3(resolve3(dir, "dataModel.ts"), dataModelFile(), "utf8");
1066
1177
  }
1067
1178
  function idsForManifest(ids, manifest) {
1068
1179
  const namesByKind = {
@@ -1169,7 +1280,9 @@ class BroodsSyncClient {
1169
1280
  this.fetchImpl = options.fetch ?? fetch;
1170
1281
  }
1171
1282
  async getManifest(project, environment) {
1172
- const response = await this.request(project, environment, "/manifest", { method: "GET" });
1283
+ const response = await this.request(project, environment, "/manifest", {
1284
+ method: "GET"
1285
+ });
1173
1286
  if (response.status === 404)
1174
1287
  return null;
1175
1288
  await assertOk(response, "Fetch manifest failed");
@@ -1185,7 +1298,9 @@ class BroodsSyncClient {
1185
1298
  return await response.json();
1186
1299
  }
1187
1300
  async getRuntimeKey(project, environment) {
1188
- const response = await this.request(project, environment, "/runtime-key", { method: "GET" });
1301
+ const response = await this.request(project, environment, "/runtime-key", {
1302
+ method: "GET"
1303
+ });
1189
1304
  if (response.status === 404)
1190
1305
  return null;
1191
1306
  await assertOk(response, "Fetch runtime key failed");
@@ -1209,7 +1324,9 @@ class BroodsSyncClient {
1209
1324
  await assertOk(response, "Set environment variable failed");
1210
1325
  }
1211
1326
  async listEnv(project, environment) {
1212
- const response = await this.request(project, environment, "/env", { method: "GET" });
1327
+ const response = await this.request(project, environment, "/env", {
1328
+ method: "GET"
1329
+ });
1213
1330
  await assertOk(response, "List environment variables failed");
1214
1331
  const payload = await response.json();
1215
1332
  return payload.variables ?? [];
@@ -1274,7 +1391,10 @@ class BroodsSyncClient {
1274
1391
  }
1275
1392
  }
1276
1393
  function diffManifests(local, remote) {
1277
- const remoteResources = new Map((remote?.resources ?? []).map((entry) => [`${entry.kind}:${entry.name}`, entry]));
1394
+ const remoteResources = new Map((remote?.resources ?? []).map((entry) => [
1395
+ `${entry.kind}:${entry.name}`,
1396
+ entry
1397
+ ]));
1278
1398
  const localResources = new Map(local.resources.map((entry) => [`${entry.kind}:${entry.name}`, entry]));
1279
1399
  const entries = [];
1280
1400
  const unmatchedLocal = [];
@@ -1284,7 +1404,11 @@ function diffManifests(local, remote) {
1284
1404
  if (!remoteResource) {
1285
1405
  unmatchedLocal.push({ key, resource });
1286
1406
  } else if (stableJson(snapshotResource(remoteResource)) !== stableJson(snapshotResource(resource))) {
1287
- entries.push({ operation: "update", kind: resource.kind, name: resource.name });
1407
+ entries.push({
1408
+ operation: "update",
1409
+ kind: resource.kind,
1410
+ name: resource.name
1411
+ });
1288
1412
  }
1289
1413
  }
1290
1414
  for (const [key, resource] of remoteResources) {
@@ -1309,12 +1433,20 @@ function diffManifests(local, remote) {
1309
1433
  }
1310
1434
  for (const { key, resource } of unmatchedLocal) {
1311
1435
  if (!renamedLocalKeys.has(key)) {
1312
- entries.push({ operation: "create", kind: resource.kind, name: resource.name });
1436
+ entries.push({
1437
+ operation: "create",
1438
+ kind: resource.kind,
1439
+ name: resource.name
1440
+ });
1313
1441
  }
1314
1442
  }
1315
1443
  for (const { key, resource } of unmatchedRemote) {
1316
1444
  if (!renamedRemoteKeys.has(key)) {
1317
- entries.push({ operation: "delete", kind: resource.kind, name: resource.name });
1445
+ entries.push({
1446
+ operation: "delete",
1447
+ kind: resource.kind,
1448
+ name: resource.name
1449
+ });
1318
1450
  }
1319
1451
  }
1320
1452
  return entries.sort((a, b) => diffSortKey(a).localeCompare(diffSortKey(b)));
@@ -1353,7 +1485,10 @@ function normalizeEnvRefs(value) {
1353
1485
  if (record.__beeblastEnv === true && typeof record.name === "string") {
1354
1486
  return `\${${record.name}}`;
1355
1487
  }
1356
- return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, normalizeEnvRefs(entry)]));
1488
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [
1489
+ key,
1490
+ normalizeEnvRefs(entry)
1491
+ ]));
1357
1492
  }
1358
1493
  return value;
1359
1494
  }
@@ -1426,6 +1561,15 @@ async function* readSseStream(body) {
1426
1561
  // src/client.ts
1427
1562
  var DEFAULT_CORE_BASE_URL = "https://gateway.broods.app";
1428
1563
 
1564
+ class IngressAcceptedError extends Error {
1565
+ accepted;
1566
+ constructor(accepted) {
1567
+ super(`Ingress ${accepted.eventId} was accepted asynchronously with status ${accepted.status ?? "queued"}`);
1568
+ this.accepted = accepted;
1569
+ this.name = "IngressAcceptedError";
1570
+ }
1571
+ }
1572
+
1429
1573
  class BroodsClient {
1430
1574
  baseUrl;
1431
1575
  apiKey;
@@ -1484,6 +1628,9 @@ class BroodsClient {
1484
1628
  const response = await this.openStream(body, targetUrl);
1485
1629
  if (!response.ok)
1486
1630
  throw new Error(`Run failed: ${response.status} ${await response.text()}`);
1631
+ if (response.status === 202) {
1632
+ throw new IngressAcceptedError(normalizeAsyncAccepted(await response.json(), body));
1633
+ }
1487
1634
  if (!response.body)
1488
1635
  throw new Error("Run response has no body");
1489
1636
  for await (const data of readSseStream(response.body)) {
@@ -1541,7 +1688,7 @@ class BroodsClient {
1541
1688
  if (options.signal?.aborted)
1542
1689
  throw new Error("Async status polling aborted.");
1543
1690
  const payload = await this.getAsyncStatus(status, options);
1544
- if (payload.status === "awaiting_approval" || payload.status === "completed" || payload.status === "failed" || payload.status === "not_found") {
1691
+ if (payload.status === "awaiting_approval" || payload.status === "completed" || payload.status === "failed" || payload.status === "expired" || payload.status === "not_found") {
1545
1692
  return payload;
1546
1693
  }
1547
1694
  await sleep(intervalMs, options.signal);
@@ -1688,6 +1835,8 @@ function directRunBody(input, prefix) {
1688
1835
  eventId,
1689
1836
  conversationKey: input.conversationKey ?? eventId,
1690
1837
  events: resolveRunEvents(input),
1838
+ ...input.mode !== undefined ? { mode: input.mode } : {},
1839
+ ...input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {},
1691
1840
  ...input.system !== undefined ? { system: input.system } : {},
1692
1841
  ...input.model !== undefined ? { model: input.model } : {}
1693
1842
  };
@@ -1703,11 +1852,16 @@ function normalizeAsyncAccepted(payload, requestBody) {
1703
1852
  const status = parseStatusUrl(statusUrl);
1704
1853
  if (!status.statusId)
1705
1854
  throw new Error("Async response statusUrl missing status id");
1855
+ const eventId = typeof payload.eventId === "string" ? payload.eventId : status.statusId;
1856
+ const acceptedStatus = payload.status;
1857
+ const requestedMode = payload.requestedMode;
1706
1858
  return {
1707
1859
  statusUrl,
1708
1860
  statusId: status.statusId,
1709
- eventId: status.statusId,
1710
- agentId: status.agentId ?? requestBody.agentId
1861
+ eventId,
1862
+ agentId: status.agentId ?? requestBody.agentId,
1863
+ ...acceptedStatus === "accepted" || acceptedStatus === "queued" || acceptedStatus === "applied" || acceptedStatus === "processing" ? { status: acceptedStatus } : {},
1864
+ ...requestedMode === "reject" || requestedMode === "followup" || requestedMode === "collect" || requestedMode === "steer" ? { requestedMode } : {}
1711
1865
  };
1712
1866
  }
1713
1867
  function parseStatusUrl(statusUrl) {
@@ -1841,6 +1995,8 @@ class BroodsWebSocketClient {
1841
1995
  events: resolveRunEvents(input),
1842
1996
  sessionId: input.sessionId,
1843
1997
  ...input.eventId !== undefined ? { eventId: input.eventId } : {},
1998
+ ...input.mode !== undefined ? { mode: input.mode } : {},
1999
+ ...input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {},
1844
2000
  ...input.system !== undefined ? { system: input.system } : {},
1845
2001
  ...input.model !== undefined ? { model: input.model } : {}
1846
2002
  }));
@@ -1851,16 +2007,17 @@ class BroodsWebSocketClient {
1851
2007
  const payload = parseServerMessage(event.data);
1852
2008
  if (!payload)
1853
2009
  return;
1854
- handlers.onMessage?.(payload);
1855
- switch (payload.type) {
2010
+ const effective = unwrapServerMessage(payload, handlers);
2011
+ handlers.onMessage?.(effective);
2012
+ switch (effective.type) {
1856
2013
  case "meta":
1857
- handlers.onMeta?.(payload);
2014
+ handlers.onMeta?.(effective);
1858
2015
  break;
1859
2016
  case "done":
1860
2017
  finish();
1861
2018
  break;
1862
2019
  case "error":
1863
- fail(new Error(formatWireError(payload.error)));
2020
+ fail(new Error(formatWireError(effective.error)));
1864
2021
  break;
1865
2022
  }
1866
2023
  };
@@ -1876,6 +2033,57 @@ class BroodsWebSocketClient {
1876
2033
  };
1877
2034
  return {
1878
2035
  url,
2036
+ sendControl(message) {
2037
+ if (socket.readyState !== WS_OPEN)
2038
+ throw new Error("WebSocket is not open");
2039
+ socket.send(JSON.stringify({ type: "control", ...message }));
2040
+ },
2041
+ close
2042
+ };
2043
+ }
2044
+ attach(input, handlers = {}) {
2045
+ const WebSocketImpl = this.resolveWebSocket();
2046
+ const url = this.buildUrl(input);
2047
+ const socket = new WebSocketImpl(url);
2048
+ let closed = false;
2049
+ const close = (code = 1000, reason = "client closed") => {
2050
+ if (closed)
2051
+ return;
2052
+ closed = true;
2053
+ if (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING)
2054
+ socket.close(code, reason);
2055
+ };
2056
+ socket.onopen = () => socket.send(JSON.stringify({
2057
+ type: "attach",
2058
+ ...input,
2059
+ signal: undefined,
2060
+ agent: undefined
2061
+ }));
2062
+ socket.onmessage = (event) => {
2063
+ if (typeof event.data !== "string")
2064
+ return;
2065
+ const payload = parseServerMessage(event.data);
2066
+ if (!payload)
2067
+ return;
2068
+ const effective = unwrapServerMessage(payload, handlers);
2069
+ handlers.onMessage?.(effective);
2070
+ if (effective.type === "done")
2071
+ handlers.onDone?.();
2072
+ if (effective.type === "error")
2073
+ handlers.onError?.(new Error(formatWireError(effective.error)));
2074
+ };
2075
+ socket.onerror = () => handlers.onError?.(webSocketAccessError(this.baseUrl));
2076
+ socket.onclose = (event) => {
2077
+ if (!closed && event.code !== 1000)
2078
+ handlers.onError?.(new Error(event.reason || "WebSocket connection closed."));
2079
+ };
2080
+ return {
2081
+ url,
2082
+ sendControl(message) {
2083
+ if (socket.readyState !== WS_OPEN)
2084
+ throw new Error("WebSocket is not open");
2085
+ socket.send(JSON.stringify({ type: "control", ...message }));
2086
+ },
1879
2087
  close
1880
2088
  };
1881
2089
  }
@@ -1968,6 +2176,13 @@ function normalizeWebSocketServiceUrl(value) {
1968
2176
  function webSocketAccessError(baseUrl) {
1969
2177
  return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
1970
2178
  }
2179
+ function unwrapServerMessage(payload, handlers) {
2180
+ if (payload.type !== "output")
2181
+ return payload;
2182
+ const output = payload;
2183
+ handlers.onOutput?.(output);
2184
+ return output.data;
2185
+ }
1971
2186
  function parseServerMessage(data) {
1972
2187
  try {
1973
2188
  const value = JSON.parse(data);
@@ -2595,7 +2810,7 @@ function renderStreamPart(part, state) {
2595
2810
  // package.json
2596
2811
  var package_default = {
2597
2812
  name: "broods",
2598
- version: "0.2.1",
2813
+ version: "0.4.0",
2599
2814
  description: "CLI + TypeScript SDK for the broods agent platform.",
2600
2815
  type: "module",
2601
2816
  license: "MIT",
@@ -2622,6 +2837,10 @@ var package_default = {
2622
2837
  ".": {
2623
2838
  types: "./dist/index.d.ts",
2624
2839
  import: "./dist/index.js"
2840
+ },
2841
+ "./account": {
2842
+ types: "./dist/account.d.ts",
2843
+ import: "./dist/account.js"
2625
2844
  }
2626
2845
  },
2627
2846
  main: "./dist/index.js",
@@ -2641,17 +2860,17 @@ var package_default = {
2641
2860
  node: ">=22"
2642
2861
  },
2643
2862
  dependencies: {
2644
- "@chat-adapter/discord": "^4.31.0",
2645
- "@chat-adapter/github": "^4.31.0",
2646
- "@chat-adapter/slack": "^4.31.0",
2647
- "@chat-adapter/telegram": "^4.31.0",
2648
- ai: "^7.0.11"
2863
+ "@chat-adapter/discord": "^4.35.0",
2864
+ "@chat-adapter/github": "^4.35.0",
2865
+ "@chat-adapter/slack": "^4.35.0",
2866
+ "@chat-adapter/telegram": "^4.35.0",
2867
+ ai: "^7.0.37"
2649
2868
  },
2650
2869
  devDependencies: {
2651
2870
  "@types/bun": "^1.3.14",
2652
- rollup: "^4.53.3",
2871
+ rollup: "^4.62.2",
2653
2872
  typescript: "^6.0.3",
2654
- "rollup-plugin-dts": "^6.3.0"
2873
+ "rollup-plugin-dts": "^6.4.1"
2655
2874
  }
2656
2875
  };
2657
2876
 
@@ -2672,7 +2891,7 @@ Commands:
2672
2891
  init Create a broods/ project shell
2673
2892
  login Authenticate with WorkOS through the dashboard
2674
2893
  dev Watch + sync Development AND live-tail agent logs (like \`convex dev\`);
2675
- confirms before deleting; auto-pushes env.NAME values from .env.local
2894
+ confirms before deleting; auto-pushes env("NAME") values from .env.local
2676
2895
  dev --once Sync Development a single time and exit (no watch, no log stream)
2677
2896
  diff Show local desired state vs remote state
2678
2897
  deploy Sync Production once; writes BROODS_API_KEY to .env.local
@@ -2682,7 +2901,7 @@ Commands:
2682
2901
  env list List environment variable names (values stay hidden)
2683
2902
  env rm <name> Remove an environment variable
2684
2903
  stream Stream live logs for the whole project/environment (Ctrl+C to stop)
2685
- logs Backfill recent logs then live-tail; default 100 lines
2904
+ logs Backfill recent logs then live-tail; all levels, default 100 lines
2686
2905
  (--errors / --level warn filter to WARN+; -n/--limit <n> changes backfill size)
2687
2906
  agent list List the agents in the current project/environment scope
2688
2907
  agent get <name> Show an agent's resources (model, sandbox, workspaces, tools, channels)
@@ -2757,8 +2976,10 @@ async function init(args) {
2757
2976
  await writeStarter(resolve4(root, ".gitignore"), `_generated
2758
2977
  .cache
2759
2978
  `, force);
2979
+ const dashboardUrl = optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL;
2760
2980
  await writeLocalEnvDefaults({
2761
- dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
2981
+ dashboardUrl,
2982
+ baseUrl: gatewayUrlForDashboard(dashboardUrl),
2762
2983
  project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
2763
2984
  environment: optionValue(args, "--env") ?? "development",
2764
2985
  region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
@@ -2778,6 +2999,7 @@ async function login(args) {
2778
2999
  const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2779
3000
  await writeLocalEnvDefaults({
2780
3001
  dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
3002
+ baseUrl: auth.baseUrl,
2781
3003
  project,
2782
3004
  environment,
2783
3005
  region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
@@ -2812,7 +3034,10 @@ async function diff(args) {
2812
3034
  command: "dev"
2813
3035
  });
2814
3036
  const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2815
- const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3037
+ const client = new BroodsSyncClient({
3038
+ baseUrl: auth.baseUrl,
3039
+ token: auth.token
3040
+ });
2816
3041
  const remote = await client.getManifest(manifest.project, manifest.environment);
2817
3042
  printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
2818
3043
  }
@@ -2824,7 +3049,10 @@ async function deploy(args) {
2824
3049
  useRuntimeEnvironment: false
2825
3050
  });
2826
3051
  const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2827
- const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3052
+ const client = new BroodsSyncClient({
3053
+ baseUrl: auth.baseUrl,
3054
+ token: auth.token
3055
+ });
2828
3056
  const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
2829
3057
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2830
3058
  await ensureGitIgnore();
@@ -2865,7 +3093,7 @@ async function dev(args) {
2865
3093
  printReadyLine(performance.now() - start);
2866
3094
  return;
2867
3095
  }
2868
- const declinedFile = join4(tmpdir(), `broods-declined-${process.pid}.txt`);
3096
+ const declinedFile = join4(tmpdir2(), `broods-declined-${process.pid}.txt`);
2869
3097
  const childEnv = {
2870
3098
  ...process.env,
2871
3099
  BROODS_DECLINED_FILE: declinedFile,
@@ -2928,7 +3156,12 @@ async function streamDevLogs(args, signal) {
2928
3156
  }
2929
3157
  const minLevel = resolveMinLevel(args);
2930
3158
  try {
2931
- for await (const entry of subscribeObservabilityLogs({ baseUrl: creds.baseUrl, apiKey: creds.apiKey, project, environment }, { backfill: 0, minLevel, signal })) {
3159
+ for await (const entry of subscribeObservabilityLogs({
3160
+ baseUrl: creds.baseUrl,
3161
+ apiKey: creds.apiKey,
3162
+ project,
3163
+ environment
3164
+ }, { backfill: 0, minLevel, signal })) {
2932
3165
  console.log(formatObservabilityEntry(entry));
2933
3166
  }
2934
3167
  } catch (error) {
@@ -2960,6 +3193,7 @@ async function ensureLocalDevDefaults(args) {
2960
3193
  const values = parseEnv2(current);
2961
3194
  const missing = [
2962
3195
  "BROODS_DASHBOARD_URL",
3196
+ "BROODS_BASE_URL",
2963
3197
  "BROODS_PROJECT",
2964
3198
  "BROODS_ENVIRONMENT",
2965
3199
  "BROODS_REGION"
@@ -2976,7 +3210,10 @@ async function ensureLocalDevDefaults(args) {
2976
3210
  let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
2977
3211
  if (process.stdin.isTTY && needsProject) {
2978
3212
  const auth = await requireAuthOrLogin(dashboardUrl);
2979
- const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3213
+ const client = new BroodsSyncClient({
3214
+ baseUrl: auth.baseUrl,
3215
+ token: auth.token
3216
+ });
2980
3217
  const context = await getOnboardingContextOrFallback(client, auth);
2981
3218
  const selectedContext = await selectOnboardingOrg(client, context);
2982
3219
  project = await selectOnboardingProject(selectedContext, project);
@@ -2993,6 +3230,7 @@ async function ensureLocalDevDefaults(args) {
2993
3230
  }
2994
3231
  await writeLocalEnvDefaults({
2995
3232
  dashboardUrl,
3233
+ baseUrl: runtime.baseUrl ?? gatewayUrlForDashboard(dashboardUrl),
2996
3234
  project,
2997
3235
  environment,
2998
3236
  region,
@@ -3018,13 +3256,15 @@ async function getOnboardingContextOrFallback(client, auth) {
3018
3256
  printWarning("CLI onboarding endpoint is not available yet; using the org from the current login.");
3019
3257
  return {
3020
3258
  currentOrgId: auth.org.id,
3021
- orgs: [{
3022
- id: auth.org.id,
3023
- name: auth.org.name,
3024
- slug: auth.org.slug,
3025
- role: "admin",
3026
- accountStatus: "active"
3027
- }],
3259
+ orgs: [
3260
+ {
3261
+ id: auth.org.id,
3262
+ name: auth.org.name,
3263
+ slug: auth.org.slug,
3264
+ role: "admin",
3265
+ accountStatus: "active"
3266
+ }
3267
+ ],
3028
3268
  projects: []
3029
3269
  };
3030
3270
  }
@@ -3053,7 +3293,11 @@ async function selectOnboardingProject(context, inferred) {
3053
3293
  if (context.projects.length === 0) {
3054
3294
  return promptText("Project name", inferred);
3055
3295
  }
3056
- const createNew = { kind: "create", name: defaultProjectName(context, inferred), slug: "" };
3296
+ const createNew = {
3297
+ kind: "create",
3298
+ name: defaultProjectName(context, inferred),
3299
+ slug: ""
3300
+ };
3057
3301
  const choices = [
3058
3302
  ...context.projects,
3059
3303
  createNew
@@ -3064,12 +3308,12 @@ async function selectOnboardingProject(context, inferred) {
3064
3308
  }
3065
3309
  return selected.name;
3066
3310
  }
3067
- function runSyncChild(args, env3) {
3311
+ function runSyncChild(args, env2) {
3068
3312
  return new Promise((resolvePromise, reject) => {
3069
3313
  const entryPoint = process.argv[1] ?? "";
3070
3314
  const child = spawn2(process.execPath, [entryPoint, "dev", "--once", ...args], {
3071
3315
  stdio: "inherit",
3072
- env: env3
3316
+ env: env2
3073
3317
  });
3074
3318
  child.on("error", reject);
3075
3319
  child.on("exit", (code, signal) => {
@@ -3088,7 +3332,10 @@ async function syncDev(args) {
3088
3332
  command: "dev"
3089
3333
  });
3090
3334
  const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3091
- const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3335
+ const client = new BroodsSyncClient({
3336
+ baseUrl: auth.baseUrl,
3337
+ token: auth.token
3338
+ });
3092
3339
  const remote = await client.getManifest(manifest.project, manifest.environment);
3093
3340
  const diff2 = diffManifests(manifest, remote?.manifest ?? null);
3094
3341
  printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
@@ -3188,7 +3435,7 @@ async function loadEnvSyncCache() {
3188
3435
  async function saveEnvSyncCache(cache) {
3189
3436
  const path = envSyncCachePath();
3190
3437
  await mkdir3(resolve4(path, ".."), { recursive: true });
3191
- await writeFile3(path, `${JSON.stringify(cache, null, 2)}
3438
+ await writeFile4(path, `${JSON.stringify(cache, null, 2)}
3192
3439
  `, "utf8");
3193
3440
  }
3194
3441
  async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
@@ -3229,7 +3476,7 @@ async function rememberDeclinedDeletes(keys) {
3229
3476
  const path = process.env.BROODS_DECLINED_FILE;
3230
3477
  if (!path || keys.length === 0)
3231
3478
  return;
3232
- await writeFile3(path, `${keys.join(`
3479
+ await writeFile4(path, `${keys.join(`
3233
3480
  `)}
3234
3481
  `, { flag: "a" });
3235
3482
  }
@@ -3237,7 +3484,7 @@ async function clearDeclinedDeletes() {
3237
3484
  const path = process.env.BROODS_DECLINED_FILE;
3238
3485
  if (!path)
3239
3486
  return;
3240
- await writeFile3(path, "", "utf8");
3487
+ await writeFile4(path, "", "utf8");
3241
3488
  }
3242
3489
  async function envCommand(args) {
3243
3490
  const subcommand = args[0];
@@ -3255,7 +3502,10 @@ async function envCommand(args) {
3255
3502
  command: "dev"
3256
3503
  });
3257
3504
  const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3258
- const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3505
+ const client = new BroodsSyncClient({
3506
+ baseUrl: auth.baseUrl,
3507
+ token: auth.token
3508
+ });
3259
3509
  const scopeKey = envSyncScopeKey(auth.baseUrl, auth.token);
3260
3510
  const target = `${manifest.project}/${manifest.environment}`;
3261
3511
  if (isList) {
@@ -3304,7 +3554,7 @@ function resolveMinLevel(args) {
3304
3554
  return "WARN";
3305
3555
  const raw = optionValue(args, "--level");
3306
3556
  if (!raw)
3307
- return "WARN";
3557
+ return;
3308
3558
  const upper = raw.toUpperCase();
3309
3559
  if (upper === "WARN" || upper === "WARNING")
3310
3560
  return "WARN";
@@ -3398,7 +3648,10 @@ async function loadAgentsWithIds(args) {
3398
3648
  command: "dev"
3399
3649
  });
3400
3650
  const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3401
- const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3651
+ const remote = await new BroodsSyncClient({
3652
+ baseUrl: auth.baseUrl,
3653
+ token: auth.token
3654
+ }).getManifest(manifest.project, manifest.environment);
3402
3655
  const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
3403
3656
  name: resource.name,
3404
3657
  config: resource.config ?? {},
@@ -3475,18 +3728,27 @@ async function run(args) {
3475
3728
  const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
3476
3729
  if (!agent)
3477
3730
  throw new Error(`Unknown local agent: ${agentName}`);
3478
- const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3731
+ const remote = await new BroodsSyncClient({
3732
+ baseUrl: auth.baseUrl,
3733
+ token: auth.token
3734
+ }).getManifest(manifest.project, manifest.environment);
3479
3735
  const agentId = remote?.ids.agents[agentName];
3480
3736
  if (!agentId)
3481
3737
  throw new Error(`Agent ${agentName} is not synced yet. Run broods dev --once or broods deploy first.`);
3482
- const runtimeKey = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
3738
+ const runtimeKey = await new BroodsSyncClient({
3739
+ baseUrl: auth.baseUrl,
3740
+ token: auth.token
3741
+ }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
3483
3742
  if (runtimeKey?.apiKey) {
3484
3743
  await writeEnvValue("BROODS_API_KEY", runtimeKey.apiKey);
3485
3744
  }
3486
3745
  if (agent.config.publicAccess !== true) {
3487
3746
  printWarning(`\u26A0 Agent "${agentName}" does not set publicAccess: true. The public endpoint is secured by default; ` + "if the deployed agent has not enabled it, this run will be refused.");
3488
3747
  }
3489
- const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
3748
+ const client = new BroodsClient({
3749
+ baseUrl: optionValue(args, "--base-url") ?? process.env.BROODS_BASE_URL ?? process.env.BROODS_HOST ?? auth.baseUrl,
3750
+ ...runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {}
3751
+ });
3490
3752
  const state = createRenderState();
3491
3753
  try {
3492
3754
  for await (const part of client.stream({
@@ -3513,7 +3775,7 @@ async function run(args) {
3513
3775
  }
3514
3776
  async function writeStarter(path, contents, force) {
3515
3777
  try {
3516
- await writeFile3(path, contents, { flag: force ? "w" : "wx" });
3778
+ await writeFile4(path, contents, { flag: force ? "w" : "wx" });
3517
3779
  } catch (error) {
3518
3780
  if (error.code === "EEXIST")
3519
3781
  return;
@@ -3533,7 +3795,7 @@ async function ensureGitIgnore() {
3533
3795
  ` : missing.join(`
3534
3796
  `) + `
3535
3797
  `;
3536
- await writeFile3(path, body, "utf8");
3798
+ await writeFile4(path, body, "utf8");
3537
3799
  }
3538
3800
  async function writeLocalEnvDefaults(options) {
3539
3801
  const path = resolve4(process.cwd(), ".env.local");
@@ -3541,14 +3803,13 @@ async function writeLocalEnvDefaults(options) {
3541
3803
  const values = parseEnv2(current);
3542
3804
  const nextValues = {
3543
3805
  BROODS_DASHBOARD_URL: options.dashboardUrl,
3806
+ ...options.baseUrl ? { BROODS_BASE_URL: options.baseUrl } : {},
3544
3807
  BROODS_PROJECT: options.project,
3545
3808
  BROODS_ENVIRONMENT: options.environment,
3546
3809
  BROODS_REGION: options.region
3547
3810
  };
3548
3811
  const lines = current ? current.replace(/\n?$/, `
3549
- `).split(/\n/) : [
3550
- "# Local broods CLI settings. Tokens are stored outside the repo."
3551
- ];
3812
+ `).split(/\n/) : ["# Local broods CLI settings. Tokens are stored outside the repo."];
3552
3813
  let changed = false;
3553
3814
  for (const [key, value] of Object.entries(nextValues)) {
3554
3815
  if (values[key] !== undefined && !options.force)
@@ -3567,7 +3828,7 @@ async function writeLocalEnvDefaults(options) {
3567
3828
  const body = `${lines.filter((line, index, all) => !(line === "" && index === all.length - 1)).join(`
3568
3829
  `)}
3569
3830
  `;
3570
- await writeFile3(path, body, "utf8");
3831
+ await writeFile4(path, body, "utf8");
3571
3832
  }
3572
3833
  async function writeEnvValue(key, value) {
3573
3834
  const path = resolve4(process.cwd(), ".env.local");
@@ -3583,7 +3844,7 @@ async function writeEnvValue(key, value) {
3583
3844
  const body = `${lines.filter((line, i, all) => !(line === "" && i === all.length - 1)).join(`
3584
3845
  `)}
3585
3846
  `;
3586
- await writeFile3(path, body, "utf8");
3847
+ await writeFile4(path, body, "utf8");
3587
3848
  }
3588
3849
  async function readTextIfExists(path) {
3589
3850
  try {
@@ -3649,33 +3910,29 @@ function starterAgent() {
3649
3910
  ` + `// A Lambda sandbox: a fresh, ephemeral bash environment created per run.
3650
3911
  ` + `export const lambdaSandbox = defineSandbox({
3651
3912
  ` + ` name: "lambda-sandbox",
3652
- ` + ` config: {
3653
- ` + ` provider: "lambda",
3654
- ` + ` network: { mode: "deny-all" },
3655
- ` + ` permissionMode: "bypass",
3656
- ` + ` timeout: 60,
3657
- ` + ` },
3913
+ ` + ` provider: "lambda",
3914
+ ` + ` network: { mode: "deny-all" },
3915
+ ` + ` permissionMode: "bypass",
3916
+ ` + ` timeout: 60,
3658
3917
  ` + `});
3659
3918
 
3660
3919
  ` + `export const myAgent = defineAgent({
3661
3920
  ` + ` name: "my-agent",
3662
- ` + ` config: {
3663
- ` + ` provider: {
3664
- ` + ` openai: { apiKey: env.OPENAI_API_KEY },
3665
- ` + ` },
3666
- ` + ` model: {
3667
- ` + ` provider: "openai",
3668
- ` + ` modelId: "gpt-5.5",
3669
- ` + ` },
3670
- ` + ` agent: {
3671
- ` + ` system: "You are a helpful assistant.",
3672
- ` + ` },
3673
- ` + ` sandbox: lambdaSandbox,
3674
- ` + ` // Expose the public runtime endpoint (SSE/WebSocket) so the API key and
3675
- ` + ` // \`broods run\` can reach this agent. Off by default \u2014 secured: a
3676
- ` + ` // private agent is only reachable via internal endpoints or channel webhooks.
3677
- ` + ` publicAccess: true,
3921
+ ` + ` provider: {
3922
+ ` + ` openai: { apiKey: env("OPENAI_API_KEY") },
3923
+ ` + ` },
3924
+ ` + ` model: {
3925
+ ` + ` provider: "openai",
3926
+ ` + ` modelId: "gpt-5.5",
3927
+ ` + ` },
3928
+ ` + ` agent: {
3929
+ ` + ` system: "You are a helpful assistant.",
3678
3930
  ` + ` },
3931
+ ` + ` sandbox: lambdaSandbox,
3932
+ ` + ` // Expose the public runtime endpoint (SSE/WebSocket) so the API key and
3933
+ ` + ` // \`broods run\` can reach this agent. Off by default \u2014 secured: a
3934
+ ` + ` // private agent is only reachable via internal endpoints or channel webhooks.
3935
+ ` + ` publicAccess: true,
3679
3936
  ` + `});
3680
3937
  `;
3681
3938
  }