broods 0.1.1 → 0.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.
package/dist/cli/index.js CHANGED
@@ -25,16 +25,19 @@ var GENERATED_DIR = "_generated";
25
25
  var USER_CONFIG_PATH = join(homedir(), ".broods", "config.json");
26
26
  async function readStoredAuth() {
27
27
  const envToken = process.env.BROODS_TOKEN;
28
- const envUrl = process.env.BROODS_DASHBOARD_URL;
29
- if (envToken && envUrl) {
28
+ const envConvexUrl = process.env.BROODS_BASE_URL;
29
+ if (envToken && envConvexUrl) {
30
30
  return {
31
- dashboardUrl: envUrl,
31
+ baseUrl: stripTrailingSlash(envConvexUrl),
32
32
  token: envToken,
33
33
  createdAt: new Date().toISOString()
34
34
  };
35
35
  }
36
36
  try {
37
- return JSON.parse(await readFile(USER_CONFIG_PATH, "utf8"));
37
+ const stored = JSON.parse(await readFile(USER_CONFIG_PATH, "utf8"));
38
+ if (typeof stored.baseUrl !== "string" || !stored.baseUrl)
39
+ return null;
40
+ return stored;
38
41
  } catch {
39
42
  return null;
40
43
  }
@@ -49,14 +52,17 @@ function stripTrailingSlash(value) {
49
52
  }
50
53
 
51
54
  // src/runtime-config.ts
52
- import { existsSync, readFileSync } from "fs";
55
+ import { existsSync, readFileSync, statSync } from "fs";
53
56
  import { join as join2, resolve } from "path";
54
- var loadedEnvForCwd = null;
57
+ var envFileCache = new Map;
58
+ var realEnvSnapshot = new Set(Object.keys(process.env));
59
+ var loadedFromFiles = new Set;
55
60
  function loadBroodsRuntimeConfig(cwd = process.cwd()) {
56
61
  loadEnvFiles(cwd);
57
62
  const stored = readStoredAuthSync();
58
63
  return {
59
64
  dashboardUrl: process.env.BROODS_DASHBOARD_URL ?? stored?.dashboardUrl,
65
+ baseUrl: process.env.BROODS_BASE_URL ?? stored?.baseUrl,
60
66
  token: process.env.BROODS_TOKEN ?? stored?.token,
61
67
  project: process.env.BROODS_PROJECT,
62
68
  environment: process.env.BROODS_ENVIRONMENT
@@ -64,17 +70,23 @@ function loadBroodsRuntimeConfig(cwd = process.cwd()) {
64
70
  }
65
71
  function loadEnvFiles(cwd) {
66
72
  const root = resolve(cwd);
67
- if (loadedEnvForCwd === root)
68
- return;
69
- loadedEnvForCwd = root;
70
- const originallySet = new Set(Object.keys(process.env));
73
+ const forceReload = process.env.BROODS_RELOAD_ENV === "1";
71
74
  for (const file of [".env", ".env.local"]) {
72
75
  const path = join2(root, file);
73
- if (!existsSync(path))
76
+ if (!existsSync(path)) {
77
+ envFileCache.delete(path);
74
78
  continue;
79
+ }
80
+ const mtimeMs = statSync(path).mtimeMs;
81
+ const cached = envFileCache.get(path);
82
+ if (cached && cached.mtimeMs === mtimeMs)
83
+ continue;
84
+ envFileCache.set(path, { mtimeMs });
75
85
  for (const [key, value] of Object.entries(parseEnv(readFileSync(path, "utf8")))) {
76
- if (!originallySet.has(key))
86
+ if (forceReload || !realEnvSnapshot.has(key) || loadedFromFiles.has(key)) {
77
87
  process.env[key] = value;
88
+ loadedFromFiles.add(key);
89
+ }
78
90
  }
79
91
  }
80
92
  }
@@ -109,10 +121,11 @@ function unquoteEnvValue(value) {
109
121
  function readStoredAuthSync() {
110
122
  try {
111
123
  const value = JSON.parse(readFileSync(USER_CONFIG_PATH, "utf8"));
112
- if (typeof value.dashboardUrl !== "string" || typeof value.token !== "string")
124
+ if (typeof value.baseUrl !== "string" || typeof value.token !== "string")
113
125
  return null;
114
126
  return {
115
- dashboardUrl: stripTrailingSlash(value.dashboardUrl),
127
+ baseUrl: stripTrailingSlash(value.baseUrl),
128
+ ...typeof value.dashboardUrl === "string" ? { dashboardUrl: stripTrailingSlash(value.dashboardUrl) } : {},
116
129
  token: value.token
117
130
  };
118
131
  } catch {
@@ -146,11 +159,16 @@ function defineResource(kind, input) {
146
159
  };
147
160
  }
148
161
  function defineChannel(type, config) {
162
+ const { workspaceScope, ...channelConfig } = config;
149
163
  return {
150
164
  [CHANNEL_MARKER]: true,
151
165
  kind: "channel",
152
166
  type,
153
- config
167
+ ...workspaceScope ? { workspaceScope } : {},
168
+ config: {
169
+ ...channelConfig,
170
+ ...workspaceScope ? { workspaceScope } : {}
171
+ }
154
172
  };
155
173
  }
156
174
  function defineTelegramChannel(config) {
@@ -166,11 +184,7 @@ function defineDiscordChannel(config) {
166
184
  return defineChannel("discord", config);
167
185
  }
168
186
  function definePancakeChannel(config) {
169
- const { ignoreTagIds, ...channelConfig } = config;
170
- return defineChannel("pancake", {
171
- ...channelConfig,
172
- ...ignoreTagIds === undefined ? {} : { options: { ignoreTagIds } }
173
- });
187
+ return defineChannel("pancake", config);
174
188
  }
175
189
  function defineZaloChannel(config) {
176
190
  return defineChannel("zalo", config);
@@ -193,6 +207,9 @@ function defineSkill(input) {
193
207
  function defineTool(input) {
194
208
  return defineResource("tool", input);
195
209
  }
210
+ function definePolicy(input) {
211
+ return defineResource("policy", input);
212
+ }
196
213
  function defineCron(input) {
197
214
  return defineResource("cron", input);
198
215
  }
@@ -216,6 +233,18 @@ var UNSAFE_BUNDLE_FILE_NAMES = [
216
233
  /^id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?$/i,
217
234
  /\.(?:pem|key|p12|pfx)$/i
218
235
  ];
236
+ var INLINE_AGENT_HOOK_EVENTS = {
237
+ onStart: "agent.started",
238
+ onStepFinish: "agent.step.finished",
239
+ onToolCall: "tool.call.started",
240
+ onToolResult: "tool.result",
241
+ onFinish: "agent.finished",
242
+ onApproval: "agent.approval.required",
243
+ onError: "agent.failed",
244
+ onSubagentFinish: "subagent.task.finished",
245
+ onMessageReceived: "channel.message.received",
246
+ onMessageSending: "channel.message.sending"
247
+ };
219
248
  async function compileProject(options = {}) {
220
249
  const cwd = options.cwd ?? process.cwd();
221
250
  loadBroodsRuntimeConfig(cwd);
@@ -231,10 +260,13 @@ async function compileProject(options = {}) {
231
260
  assertKnownConfigKeys(resource);
232
261
  for (const resource of resources)
233
262
  assertSupportedWorkspaceStorage(resource);
263
+ for (const resource of resources)
264
+ assertSupportedWorkspaceIsolationShape(resource);
265
+ assertWorkspaceIsolationConsistency(resources);
234
266
  assertSupportedWorkspaceSandboxMounts(resources);
235
267
  const resourceAliases = aliasesForResources(resourceExports);
236
268
  const environment = resolveEnvironment(config, options.environment ?? (options.useRuntimeEnvironment === false ? undefined : process.env.BROODS_ENVIRONMENT), options.command ?? "dev");
237
- const manifestResources = (await Promise.all(resources.map((resource) => toManifestResource(resource, root)))).sort((a, b) => `${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`));
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}`));
238
270
  return {
239
271
  config,
240
272
  resources,
@@ -329,11 +361,13 @@ var KNOWN_AGENT_CONFIG_KEYS = new Set([
329
361
  "workspaces",
330
362
  "subagent",
331
363
  "skills",
364
+ "policy",
332
365
  "publicAccess"
333
366
  ]);
334
367
  var AGENT_KEY_SUGGESTIONS = {
335
368
  workspace: "workspaces",
336
369
  skill: "skills",
370
+ policies: "policy",
337
371
  tool: "tools",
338
372
  channel: "channels",
339
373
  hook: "hooks",
@@ -372,6 +406,93 @@ function assertSupportedWorkspaceStorage(resource) {
372
406
  }
373
407
  throw new Error(`Workspace "${resource.name}" config.storage.provider must be one of: s3`);
374
408
  }
409
+ function assertSupportedWorkspaceIsolationShape(resource) {
410
+ if (resource.kind !== "workspace")
411
+ return;
412
+ const config = resource.config;
413
+ if (typeof config.isolation === "string") {
414
+ throw new Error(`Workspace "${resource.name}" config.isolation no longer supports string modes; use isolation: true or omit it.`);
415
+ }
416
+ if (config.isolation !== undefined && typeof config.isolation !== "boolean") {
417
+ throw new Error(`Workspace "${resource.name}" config.isolation must be a boolean`);
418
+ }
419
+ }
420
+ function assertWorkspaceIsolationConsistency(resources) {
421
+ const workspaceResources = new Map(resources.filter((resource) => resource.kind === "workspace").map((resource) => [resource.name, resource]));
422
+ for (const resource of resources) {
423
+ if (resource.kind !== "agent")
424
+ continue;
425
+ const config = resource.config;
426
+ const channels = config.channels;
427
+ if (channels !== undefined && !Array.isArray(channels)) {
428
+ throw new Error(`Agent "${resource.name}" config.channels must be an array of channel definitions`);
429
+ }
430
+ const channelDefinitions = Array.isArray(channels) ? channels.filter(isChannelDefinition) : [];
431
+ const channelIds = new Set;
432
+ for (const channel of channelDefinitions) {
433
+ const config2 = channel.config;
434
+ const channelId = typeof config2.id === "string" ? config2.id : undefined;
435
+ if (channelId) {
436
+ if (channelIds.has(channelId))
437
+ throw new Error(`Duplicate channel id: ${channelId}`);
438
+ channelIds.add(channelId);
439
+ }
440
+ if (channel.workspaceScope) {
441
+ assertWorkspaceScopeShape(channel.workspaceScope, `Agent "${resource.name}" channel "${channel.type}"`);
442
+ }
443
+ if (channel.config && typeof channel.config === "object" && "workspaceIsolationScope" in channel.config) {
444
+ throw new Error(`Agent "${resource.name}" channel "${channel.type}" uses workspaceIsolationScope, which is no longer supported; use workspaceScope.`);
445
+ }
446
+ }
447
+ const attachedWorkspaces = Array.isArray(config.workspaces) ? config.workspaces.map((entry) => resolveLocalWorkspace(entry, workspaceResources)).filter((entry) => Boolean(entry)) : [];
448
+ const isolatedWorkspaces = attachedWorkspaces.filter((workspace) => workspace.config.isolation === true);
449
+ const scopedChannels = channelDefinitions.filter((channel) => channel.workspaceScope);
450
+ if (scopedChannels.length > 0 && isolatedWorkspaces.length === 0) {
451
+ const channel = scopedChannels[0];
452
+ throw new Error(`Agent "${resource.name}" channel "${channel.type}" defines workspaceScope, but no attached workspace has isolation: true.`);
453
+ }
454
+ if (isolatedWorkspaces.length > 0) {
455
+ for (const channel of channelDefinitions) {
456
+ if (!channel.workspaceScope) {
457
+ throw new Error(`Agent "${resource.name}" attaches isolated workspace "${isolatedWorkspaces[0].name}", but channel "${channel.type}" does not define workspaceScope.`);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ function assertWorkspaceScopeShape(scope, name) {
464
+ if (!scope)
465
+ return;
466
+ if (scope.level === "channel") {
467
+ if ("alias" in scope && scope.alias !== undefined) {
468
+ throw new Error(`${name} workspaceScope.alias is only supported when workspaceScope.level is conversation`);
469
+ }
470
+ return;
471
+ }
472
+ if (scope.level !== "conversation") {
473
+ throw new Error(`${name} workspaceScope.level must be one of: channel, conversation`);
474
+ }
475
+ if (!("alias" in scope) || typeof scope.alias !== "string" || scope.alias.length === 0) {
476
+ throw new Error(`${name} workspaceScope.alias must be a non-empty string when workspaceScope.level is conversation`);
477
+ }
478
+ if (!/^[A-Za-z0-9._-]+$/.test(scope.alias)) {
479
+ throw new Error(`${name} workspaceScope.alias must use only letters, numbers, dots, underscores, or hyphens`);
480
+ }
481
+ }
482
+ function resolveLocalWorkspace(entry, workspaces) {
483
+ if (isResource(entry) && entry.kind === "workspace")
484
+ return entry;
485
+ if (typeof entry === "string")
486
+ return workspaces.get(entry);
487
+ if (entry && typeof entry === "object" && "workspace" in entry) {
488
+ const workspace = entry.workspace;
489
+ if (isResource(workspace) && workspace.kind === "workspace")
490
+ return workspace;
491
+ if (typeof workspace === "string")
492
+ return workspaces.get(workspace);
493
+ }
494
+ return;
495
+ }
375
496
  function assertSupportedWorkspaceSandboxMounts(resources) {
376
497
  const sandboxes = new Map(resources.filter((resource) => resource.kind === "sandbox").map((resource) => [resource.name, resource]));
377
498
  for (const resource of resources) {
@@ -387,7 +508,7 @@ function assertSupportedWorkspaceSandboxMounts(resources) {
387
508
  const sandbox = effectiveWorkspaceSandbox(entry, agentSandbox, sandboxes);
388
509
  if (!sandbox || supportsS3WorkspaceMount(sandbox))
389
510
  continue;
390
- throw new Error(`Agent "${resource.name}" workspace "${workspaceName}" uses sandbox "${sandbox.name}" (${sandboxProvider(sandbox)}) which does not support S3 workspace mounts. Use lambda, or daytona/kubernetes with options.mountAwsS3Buckets: true, or set this workspace ref to sandbox: null for read-only S3 access.`);
511
+ throw new Error(`Agent "${resource.name}" workspace "${workspaceName}" uses sandbox "${sandbox.name}" (${sandboxProvider(sandbox)}) which does not support S3 workspace mounts. Use lambda/sandbox, or daytona with options.mountAwsS3Buckets: true, or set this workspace ref to sandbox: null for read-only S3 access.`);
391
512
  }
392
513
  }
393
514
  }
@@ -421,9 +542,9 @@ function workspaceNameFor(entry) {
421
542
  }
422
543
  function supportsS3WorkspaceMount(sandbox) {
423
544
  const provider = sandboxProvider(sandbox);
424
- if (provider === "lambda")
545
+ if (provider === "lambda" || provider === "sandbox")
425
546
  return true;
426
- if (provider !== "daytona" && provider !== "kubernetes")
547
+ if (provider !== "daytona")
427
548
  return false;
428
549
  const options = sandbox.config.options;
429
550
  return Boolean(options && typeof options === "object" && !Array.isArray(options) && options.mountAwsS3Buckets === true);
@@ -499,7 +620,12 @@ function compileChannels(resources, exports) {
499
620
  if (aliases.has(alias))
500
621
  throw new Error(`Duplicate channel export alias: ${alias}`);
501
622
  aliases.add(alias);
502
- compiled.push({ alias, type: entry.type, agentName: resource.name });
623
+ compiled.push({
624
+ alias,
625
+ type: entry.type,
626
+ id: alias,
627
+ agentName: resource.name
628
+ });
503
629
  }
504
630
  }
505
631
  for (const [channel, alias] of exportedAliases) {
@@ -514,42 +640,38 @@ function capitalize(value) {
514
640
  function isValidIdentifier(value) {
515
641
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
516
642
  }
517
- async function toManifestResource(resource, projectRoot) {
518
- return {
643
+ async function toManifestResources(resource, projectRoot) {
644
+ if (resource.kind === "agent") {
645
+ const normalized = normalizeAgentConfig(resource, projectRoot);
646
+ return [
647
+ ...normalized.hookResource ? [normalized.hookResource] : [],
648
+ {
649
+ kind: resource.kind,
650
+ name: resource.name,
651
+ ...resource.description ? { description: resource.description } : {},
652
+ config: normalized.config
653
+ }
654
+ ];
655
+ }
656
+ return [{
519
657
  kind: resource.kind,
520
658
  name: resource.name,
521
659
  ...resource.description ? { description: resource.description } : {},
522
660
  config: await normalizeConfig(resource, projectRoot)
523
- };
661
+ }];
524
662
  }
525
663
  async function normalizeConfig(resource, projectRoot) {
526
- if (resource.kind === "agent") {
527
- const config = { ...resource.config };
528
- if (config.channels !== undefined) {
529
- if (!Array.isArray(config.channels)) {
530
- throw new Error(`Agent "${resource.name}" config.channels must be an array of channel definitions`);
531
- }
532
- config.channels = Object.fromEntries(config.channels.map((channel) => {
533
- if (!isChannelDefinition(channel)) {
534
- throw new Error(`Agent "${resource.name}" config.channels must contain channel definitions`);
535
- }
536
- return [channel.type, channel.config];
537
- }));
538
- }
539
- if (isResource(config.sandbox)) {
540
- config.sandbox = config.sandbox.name;
541
- }
542
- if (Array.isArray(config.workspaces)) {
543
- config.workspaces = config.workspaces.map((workspace) => normalizeWorkspaceRef(workspace, resource.name));
544
- }
545
- return rewriteValues(config);
546
- }
547
664
  if (resource.kind === "skill") {
548
665
  return await normalizeSkillConfig(resource.config, projectRoot);
549
666
  }
550
667
  if (resource.kind === "tool") {
551
668
  return await normalizeToolConfig(resource.config, projectRoot);
552
669
  }
670
+ if (resource.kind === "policy") {
671
+ const config = { ...resource.config };
672
+ config.version = config.version ?? 1;
673
+ return rewriteValues(config);
674
+ }
553
675
  if (resource.kind === "cron") {
554
676
  const config = { ...resource.config };
555
677
  const agent = config.agent;
@@ -564,6 +686,163 @@ async function normalizeConfig(resource, projectRoot) {
564
686
  }
565
687
  return rewriteValues(resource.config);
566
688
  }
689
+ function normalizeAgentConfig(resource, _projectRoot) {
690
+ const config = { ...resource.config };
691
+ const inlineHooks = normalizeInlineAgentHooks(resource.name, config.hooks);
692
+ if (inlineHooks) {
693
+ config.hooks = inlineHooks.agentHooksConfig;
694
+ } else if (config.hooks !== undefined) {
695
+ config.hooks = stripInlineHookKeys(config.hooks, resource.name);
696
+ }
697
+ if (config.channels !== undefined) {
698
+ if (!Array.isArray(config.channels)) {
699
+ throw new Error(`Agent "${resource.name}" config.channels must be an array of channel definitions`);
700
+ }
701
+ config.channels = Object.fromEntries(config.channels.map((channel) => {
702
+ if (!isChannelDefinition(channel)) {
703
+ throw new Error(`Agent "${resource.name}" config.channels must contain channel definitions`);
704
+ }
705
+ const channelId = `${resource.name}${capitalize(channel.type)}Channel`;
706
+ return [channel.type, { id: channelId, ...channel.config }];
707
+ }));
708
+ }
709
+ if (isResource(config.sandbox)) {
710
+ config.sandbox = config.sandbox.name;
711
+ }
712
+ if (Array.isArray(config.workspaces)) {
713
+ config.workspaces = config.workspaces.map((workspace) => normalizeWorkspaceRef(workspace, resource.name));
714
+ }
715
+ if (config.policy !== undefined) {
716
+ const policy = normalizeAgentPolicyConfig(config.policy, resource.name);
717
+ if (policy)
718
+ config.policy = policy;
719
+ else
720
+ delete config.policy;
721
+ }
722
+ return {
723
+ config: rewriteValues(config),
724
+ ...inlineHooks?.hookResource ? { hookResource: inlineHooks.hookResource } : {}
725
+ };
726
+ }
727
+ function normalizeInlineAgentHooks(agentName, hooks) {
728
+ if (hooks === undefined)
729
+ return;
730
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
731
+ throw new Error(`Agent "${agentName}" config.hooks must be an object`);
732
+ }
733
+ const hookConfig = hooks;
734
+ const entries = Object.keys(INLINE_AGENT_HOOK_EVENTS).flatMap((name) => {
735
+ const handler = hookConfig[name];
736
+ if (handler === undefined)
737
+ return [];
738
+ if (typeof handler !== "function") {
739
+ throw new Error(`Agent "${agentName}" config.hooks.${name} must be a function`);
740
+ }
741
+ return [{
742
+ name,
743
+ event: INLINE_AGENT_HOOK_EVENTS[name],
744
+ source: toHookFunctionExpression(handler.toString())
745
+ }];
746
+ });
747
+ if (entries.length === 0)
748
+ return;
749
+ const hookName = `${agentName}-hooks`;
750
+ const bundle = transpileInlineHookBundle(agentName, entries);
751
+ const bundleSize = Buffer.byteLength(bundle);
752
+ if (bundleSize > MAX_BUNDLE_FILE_BYTES) {
753
+ throw new Error(`Agent "${agentName}" hook bundle is too large (${bundleSize} bytes, max ${MAX_BUNDLE_FILE_BYTES})`);
754
+ }
755
+ const events = entries.map((entry) => entry.event);
756
+ const agentHooksConfig = stripInlineHookKeys(hookConfig, agentName);
757
+ agentHooksConfig.code = [{ hookId: hookName }];
758
+ return {
759
+ agentHooksConfig,
760
+ hookResource: {
761
+ kind: "hook",
762
+ name: hookName,
763
+ description: `Inline hooks for agent ${agentName}`,
764
+ config: {
765
+ events,
766
+ bundle,
767
+ sha256: sha256Hex(bundle)
768
+ }
769
+ }
770
+ };
771
+ }
772
+ function stripInlineHookKeys(hooks, agentName) {
773
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
774
+ throw new Error(`Agent "${agentName}" config.hooks must be an object`);
775
+ }
776
+ const entries = Object.entries(hooks);
777
+ for (const [key] of entries) {
778
+ if (key === "webhooks" || key in INLINE_AGENT_HOOK_EVENTS)
779
+ continue;
780
+ throw new Error(`Agent "${agentName}" config.hooks.${key} is not supported. Use inline hook callbacks or hooks.webhooks.`);
781
+ }
782
+ const result = Object.fromEntries(entries.filter(([key]) => key === "webhooks"));
783
+ return result;
784
+ }
785
+ function toHookFunctionExpression(source) {
786
+ const trimmed = source.trim();
787
+ if (/^(async\s+)?function\b/.test(trimmed) || /^(async\s*)?\(/.test(trimmed) || /^(async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed)) {
788
+ return trimmed;
789
+ }
790
+ if (/^(async\s+)?[A-Za-z_$][\w$]*\s*\(/.test(trimmed)) {
791
+ return trimmed.replace(/^(async\s+)?/, "$1function ");
792
+ }
793
+ return trimmed;
794
+ }
795
+ function transpileInlineHookBundle(agentName, entries) {
796
+ const moduleSource = `export default {
797
+ ${entries.map((entry) => ` ${JSON.stringify(entry.event)}: ${entry.source},`).join(`
798
+ `)}
799
+ };
800
+ `;
801
+ try {
802
+ return new Bun.Transpiler({ loader: "ts" }).transformSync(moduleSource);
803
+ } catch (error) {
804
+ const message = error instanceof Error ? error.message : String(error);
805
+ throw new Error(`Agent "${agentName}" inline hooks failed to transpile: ${message}`);
806
+ }
807
+ }
808
+ function normalizeAgentPolicyConfig(config, agentName) {
809
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
810
+ throw new Error(`Agent "${agentName}" config.policy must be an object`);
811
+ }
812
+ const input = config;
813
+ for (const key of Object.keys(input)) {
814
+ if (key !== "enabled" && key !== "policyIds" && key !== "policies" && key !== "mode") {
815
+ throw new Error(`Agent "${agentName}" config.policy.${key} is not supported`);
816
+ }
817
+ }
818
+ const policyIds = input.policyIds;
819
+ const policies = input.policies;
820
+ if (policyIds !== undefined && policies !== undefined) {
821
+ throw new Error(`Agent "${agentName}" config.policy cannot set both policyIds and policies`);
822
+ }
823
+ const refs = policies ?? policyIds;
824
+ if (refs !== undefined && !Array.isArray(refs)) {
825
+ throw new Error(`Agent "${agentName}" config.policy.policies must be an array`);
826
+ }
827
+ const normalized = {
828
+ ...input,
829
+ ...Array.isArray(refs) ? {
830
+ policyIds: refs.map((entry) => {
831
+ if (isResource(entry) && entry.kind === "policy")
832
+ return entry.name;
833
+ if (typeof entry === "string")
834
+ return entry;
835
+ throw new Error(`Agent "${agentName}" config.policy.policies must contain definePolicy(...) resources or strings`);
836
+ })
837
+ } : {}
838
+ };
839
+ delete normalized.policies;
840
+ delete normalized.enabled;
841
+ if (Array.isArray(normalized.policyIds) && normalized.policyIds.length === 0) {
842
+ delete normalized.policyIds;
843
+ }
844
+ return Array.isArray(normalized.policyIds) && normalized.policyIds.length > 0 ? normalized : undefined;
845
+ }
567
846
  async function normalizeSkillConfig(config, projectRoot) {
568
847
  const skillRoot = resolveContainedResourcePath(projectRoot, config.path, "Skill");
569
848
  const manifestPath = relative(projectRoot, skillRoot).split("\\").join("/");
@@ -605,6 +884,7 @@ async function normalizeToolConfig(config, projectRoot) {
605
884
  path: manifestPath,
606
885
  description: config.description,
607
886
  inputSchema: config.inputSchema,
887
+ ...config.runtime !== undefined ? { runtime: config.runtime } : {},
608
888
  ...config.defaultConfig !== undefined ? { defaultConfig: config.defaultConfig } : {},
609
889
  bundle,
610
890
  sha256: sha256Hex(bundle)
@@ -743,7 +1023,8 @@ function idsForManifest(ids, manifest) {
743
1023
  sandboxes: namesForKind(manifest, "sandbox"),
744
1024
  crons: namesForKind(manifest, "cron"),
745
1025
  skills: namesForKind(manifest, "skill"),
746
- tools: namesForKind(manifest, "tool")
1026
+ tools: namesForKind(manifest, "tool"),
1027
+ policies: namesForKind(manifest, "policy")
747
1028
  };
748
1029
  return {
749
1030
  agents: pickIds(ids.agents, namesByKind.agents),
@@ -751,14 +1032,16 @@ function idsForManifest(ids, manifest) {
751
1032
  sandboxes: pickIds(ids.sandboxes, namesByKind.sandboxes),
752
1033
  crons: pickIds(ids.crons, namesByKind.crons),
753
1034
  skills: pickIds(ids.skills, namesByKind.skills),
754
- tools: pickIds(ids.tools, namesByKind.tools)
1035
+ tools: pickIds(ids.tools, namesByKind.tools),
1036
+ hooks: {},
1037
+ policies: pickIds(ids.policies, namesByKind.policies)
755
1038
  };
756
1039
  }
757
1040
  function namesForKind(manifest, kind) {
758
1041
  return new Set(manifest.resources.filter((resource) => resource.kind === kind).map((resource) => resource.name));
759
1042
  }
760
1043
  function pickIds(values, names) {
761
- return Object.fromEntries(Object.entries(values).filter(([name]) => names.has(name)));
1044
+ return Object.fromEntries(Object.entries(values ?? {}).filter(([name]) => names.has(name)));
762
1045
  }
763
1046
  function idsFile(ids) {
764
1047
  return `// Generated by broods. Do not edit by hand.
@@ -769,6 +1052,7 @@ function idsFile(ids) {
769
1052
  ` + `export const crons = ids.crons;
770
1053
  ` + `export const skills = ids.skills;
771
1054
  ` + `export const tools = ids.tools;
1055
+ ` + `export const policies = ids.policies;
772
1056
  `;
773
1057
  }
774
1058
  function resourcesFile(manifest) {
@@ -803,6 +1087,7 @@ ${channelEntries}
803
1087
  ` + ` crons: ${aliasedIdMap("crons", ids.crons, aliases.cron)},
804
1088
  ` + ` skills: ${aliasedIdMap("skills", ids.skills, aliases.skill)},
805
1089
  ` + ` tools: ${aliasedIdMap("tools", ids.tools, aliases.tool)},
1090
+ ` + ` policies: ${aliasedIdMap("policies", ids.policies, aliases.policy)},
806
1091
  ` + `} as const satisfies ResourceApi;
807
1092
  `;
808
1093
  }
@@ -827,11 +1112,11 @@ function dataModelFile() {
827
1112
 
828
1113
  // src/sync.ts
829
1114
  class BroodsSyncClient {
830
- dashboardUrl;
1115
+ baseUrl;
831
1116
  token;
832
1117
  fetchImpl;
833
1118
  constructor(options) {
834
- this.dashboardUrl = stripTrailingSlash(options.dashboardUrl);
1119
+ this.baseUrl = stripTrailingSlash(options.baseUrl);
835
1120
  this.token = options.token;
836
1121
  this.fetchImpl = options.fetch ?? fetch;
837
1122
  }
@@ -859,7 +1144,13 @@ class BroodsSyncClient {
859
1144
  const payload = await response.json();
860
1145
  if (!payload.apiKey)
861
1146
  throw new Error("Fetch runtime key failed: response omitted apiKey");
862
- return { apiKey: payload.apiKey, keyHint: payload.keyHint ?? "" };
1147
+ return {
1148
+ apiKey: payload.apiKey,
1149
+ keyHint: payload.keyHint ?? "",
1150
+ ...payload.endpointId ? { endpointId: payload.endpointId } : {},
1151
+ ...payload.projectSlug ? { projectSlug: payload.projectSlug } : {},
1152
+ ...payload.environmentSlug ? { environmentSlug: payload.environmentSlug } : {}
1153
+ };
863
1154
  }
864
1155
  async setEnv(project, environment, name, value) {
865
1156
  const response = await this.request(project, environment, `/env/${encodeURIComponent(name)}`, {
@@ -890,7 +1181,7 @@ class BroodsSyncClient {
890
1181
  await assertOk(response, "Remove environment variable failed");
891
1182
  }
892
1183
  async getOnboarding() {
893
- const response = await this.fetchImpl(`${this.dashboardUrl}/api/cli/onboarding`, {
1184
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
894
1185
  method: "GET",
895
1186
  headers: {
896
1187
  Authorization: `Bearer ${this.token}`
@@ -900,7 +1191,7 @@ class BroodsSyncClient {
900
1191
  return await response.json();
901
1192
  }
902
1193
  async selectOnboardingOrg(orgId) {
903
- const response = await this.fetchImpl(`${this.dashboardUrl}/api/cli/onboarding`, {
1194
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
904
1195
  method: "POST",
905
1196
  headers: {
906
1197
  Authorization: `Bearer ${this.token}`,
@@ -911,8 +1202,20 @@ class BroodsSyncClient {
911
1202
  await assertOk(response, "Select CLI org failed");
912
1203
  return await response.json();
913
1204
  }
1205
+ async createOnboardingOrg(name) {
1206
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
1207
+ method: "POST",
1208
+ headers: {
1209
+ Authorization: `Bearer ${this.token}`,
1210
+ "Content-Type": "application/json"
1211
+ },
1212
+ body: JSON.stringify({ createOrgName: name })
1213
+ });
1214
+ await assertOk(response, "Create CLI org failed");
1215
+ return await response.json();
1216
+ }
914
1217
  async request(project, environment, suffix, init) {
915
- const url = `${this.dashboardUrl}/api/cli/projects/${encodeURIComponent(project)}` + `/environments/${encodeURIComponent(environment)}${suffix}`;
1218
+ const url = `${this.baseUrl}/v1/account/projects/${encodeURIComponent(project)}` + `/environments/${encodeURIComponent(environment)}${suffix}`;
916
1219
  return await this.fetchImpl(url, {
917
1220
  ...init,
918
1221
  headers: {
@@ -970,7 +1273,7 @@ function diffManifests(local, remote) {
970
1273
  }
971
1274
  function snapshotResource(resource) {
972
1275
  const normalized = normalizeEnvRefs(resource);
973
- if (resource.kind !== "skill" && resource.kind !== "tool")
1276
+ if (resource.kind !== "skill" && resource.kind !== "tool" && resource.kind !== "hook")
974
1277
  return normalized;
975
1278
  return {
976
1279
  ...normalized,
@@ -983,7 +1286,7 @@ function renameSnapshot(resource) {
983
1286
  return rest;
984
1287
  }
985
1288
  function isRenamableKind(kind) {
986
- return kind === "agent" || kind === "workspace" || kind === "sandbox";
1289
+ return kind === "agent" || kind === "workspace" || kind === "sandbox" || kind === "policy";
987
1290
  }
988
1291
  function diffSortKey(entry) {
989
1292
  const rank = {
@@ -1093,7 +1396,7 @@ class BroodsClient {
1093
1396
  const name = refOrName;
1094
1397
  const id = agentId ?? "";
1095
1398
  if (!id)
1096
- throw new Error(`Agent ${name} is missing a generated id. Run broods deploy first.`);
1399
+ throw new Error(`Agent ${name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
1097
1400
  return {
1098
1401
  id,
1099
1402
  run: (input) => this.run({ ...input, agentName: name, agentId: id }),
@@ -1103,7 +1406,7 @@ class BroodsClient {
1103
1406
  }
1104
1407
  const ref = refOrName;
1105
1408
  if (!ref.id)
1106
- throw new Error(`Agent ${ref.name} is missing a generated id. Run broods deploy first.`);
1409
+ throw new Error(`Agent ${ref.name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
1107
1410
  return {
1108
1411
  id: ref.id,
1109
1412
  run: (input) => this.run(ref, input),
@@ -1154,7 +1457,8 @@ class BroodsClient {
1154
1457
  agentName: refOrInput.name
1155
1458
  } : refOrInput;
1156
1459
  const body = directRunBody(input, "async");
1157
- const response = await this.fetchJson(`${this.baseUrl}/async`, {
1460
+ const targetUrl = maybeInput ? this.scopedUrl(refOrInput, "/async") : `${this.baseUrl}/async`;
1461
+ const response = await this.fetchJson(targetUrl, {
1158
1462
  method: "POST",
1159
1463
  headers: this.apiKeyHeaders(),
1160
1464
  body: JSON.stringify(body)
@@ -1197,7 +1501,7 @@ class BroodsClient {
1197
1501
  throw new Error("Polling timeout");
1198
1502
  }
1199
1503
  async createCron(input) {
1200
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
1504
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
1201
1505
  method: "POST",
1202
1506
  headers: this.apiKeyHeaders(),
1203
1507
  body: JSON.stringify(resolveCronInput(input))
@@ -1207,7 +1511,7 @@ class BroodsClient {
1207
1511
  return await response.json();
1208
1512
  }
1209
1513
  async listCrons() {
1210
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
1514
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
1211
1515
  method: "GET",
1212
1516
  headers: this.apiKeyHeaders()
1213
1517
  });
@@ -1217,7 +1521,7 @@ class BroodsClient {
1217
1521
  return payload.crons;
1218
1522
  }
1219
1523
  async getCron(cronId) {
1220
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1524
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1221
1525
  method: "GET",
1222
1526
  headers: this.apiKeyHeaders()
1223
1527
  });
@@ -1232,7 +1536,7 @@ class BroodsClient {
1232
1536
  if (options.limit !== undefined)
1233
1537
  params.set("limit", String(options.limit));
1234
1538
  const suffix = params.size > 0 ? `?${params}` : "";
1235
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
1539
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
1236
1540
  method: "GET",
1237
1541
  headers: this.apiKeyHeaders()
1238
1542
  });
@@ -1242,7 +1546,7 @@ class BroodsClient {
1242
1546
  return payload.runs;
1243
1547
  }
1244
1548
  async updateCron(cronId, patch) {
1245
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1549
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1246
1550
  method: "PATCH",
1247
1551
  headers: this.apiKeyHeaders(),
1248
1552
  body: JSON.stringify(patch)
@@ -1252,7 +1556,7 @@ class BroodsClient {
1252
1556
  return await response.json();
1253
1557
  }
1254
1558
  async deleteCron(cronId) {
1255
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1559
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1256
1560
  method: "DELETE",
1257
1561
  headers: this.apiKeyHeaders()
1258
1562
  });
@@ -1263,11 +1567,11 @@ class BroodsClient {
1263
1567
  const payload = await response.json();
1264
1568
  return payload.deleted;
1265
1569
  }
1266
- scopedUrl(ref) {
1570
+ scopedUrl(ref, suffix = "") {
1267
1571
  if (ref.projectSlug && ref.environmentSlug && ref.endpointId) {
1268
- return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}`;
1572
+ return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}${suffix}`;
1269
1573
  }
1270
- return this.baseUrl;
1574
+ return `${this.baseUrl}${suffix}`;
1271
1575
  }
1272
1576
  async openStream(body, targetUrl) {
1273
1577
  if (this.apiKey) {
@@ -1424,7 +1728,6 @@ function formatStreamError(error) {
1424
1728
  var DEFAULT_CONNECT_TIMEOUT_MS = 2000;
1425
1729
  var WS_CONNECTING = 0;
1426
1730
  var WS_OPEN = 1;
1427
- var LAMBDA_FUNCTION_URL_HOST_RE = /\.lambda-url\.[a-z0-9-]+\.on\.aws$/i;
1428
1731
 
1429
1732
  class BroodsWebSocketClient {
1430
1733
  baseUrl;
@@ -1433,7 +1736,7 @@ class BroodsWebSocketClient {
1433
1736
  connectTimeoutMs;
1434
1737
  constructor(options = {}) {
1435
1738
  loadBroodsRuntimeConfig();
1436
- this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl || process.env.BROODS_WEBSOCKET_URL || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
1739
+ this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
1437
1740
  this.apiKey = options.apiKey || process.env.BROODS_API_KEY || "";
1438
1741
  if (!this.apiKey)
1439
1742
  throw new Error("BroodsWebSocketClient requires apiKey or BROODS_API_KEY.");
@@ -1615,10 +1918,6 @@ function normalizeWebSocketServiceUrl(value) {
1615
1918
  return normalizeHttpServiceUrl(trimmed);
1616
1919
  }
1617
1920
  function webSocketAccessError(baseUrl) {
1618
- const host = new URL(baseUrl).hostname;
1619
- if (LAMBDA_FUNCTION_URL_HOST_RE.test(host)) {
1620
- return new Error(`Cannot access the WebSocket service at ${baseUrl}. AWS Lambda Function URLs do not support WebSocket upgrades; ` + "set BROODS_WEBSOCKET_URL or BROODS_HOST to the deployed WebSocket gateway URL instead.");
1621
- }
1622
1921
  return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
1623
1922
  }
1624
1923
  function parseServerMessage(data) {
@@ -1690,6 +1989,7 @@ async function* subscribeObservabilityLogs(options, subscribeOptions = {}) {
1690
1989
  async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
1691
1990
  const { baseUrl, apiKey, project, environment } = options;
1692
1991
  const { backfill = 0, minLevel, signal } = subscribeOptions;
1992
+ const liveOnly = subscribeOptions.liveOnly ?? backfill <= 0;
1693
1993
  if (signal?.aborted)
1694
1994
  return;
1695
1995
  const url = buildObservabilityUrl(baseUrl, project, environment, apiKey);
@@ -1725,6 +2025,7 @@ async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
1725
2025
  type: "subscribe",
1726
2026
  stream: "logs",
1727
2027
  ...backfill > 0 ? { backfill } : {},
2028
+ ...liveOnly ? { liveOnly: true } : {},
1728
2029
  ...minLevel !== undefined ? { minLevel } : {}
1729
2030
  };
1730
2031
  socket.send(JSON.stringify(msg));
@@ -1814,13 +2115,19 @@ function optionValue(args, name) {
1814
2115
  function hasFlag(args, name) {
1815
2116
  return args.includes(name);
1816
2117
  }
1817
- async function requireAuth(dashboardUrl) {
2118
+ function isPlainObject(value) {
2119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2120
+ }
2121
+ async function requireAuth(baseUrl) {
1818
2122
  loadBroodsRuntimeConfig();
1819
2123
  const auth = await readStoredAuth();
1820
2124
  if (!auth) {
1821
- throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_DASHBOARD_URL.");
2125
+ throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_BASE_URL.");
1822
2126
  }
1823
- return dashboardUrl ? { ...auth, dashboardUrl } : auth;
2127
+ return {
2128
+ ...auth,
2129
+ ...baseUrl ? { baseUrl: stripTrailingSlash(baseUrl) } : {}
2130
+ };
1824
2131
  }
1825
2132
  async function promptSecret(label) {
1826
2133
  const rl = createInterface({ input, output });
@@ -1893,17 +2200,18 @@ async function loginWithBrowser(dashboardUrl) {
1893
2200
  await assertCliAuthRouteExists(startUrl);
1894
2201
  openBrowser(startUrl);
1895
2202
  console.log(`Opening ${startUrl}`);
1896
- const loginCode = await waitWithTimeout(code.promise, LOGIN_TIMEOUT_MS);
1897
- const response = await fetch(`${stripTrailingSlash(dashboardUrl)}/api/cli/auth/exchange`, {
2203
+ const login = await waitWithTimeout(code.promise, LOGIN_TIMEOUT_MS);
2204
+ const response = await fetch(`${login.baseUrl}/v1/account/auth/exchange`, {
1898
2205
  method: "POST",
1899
2206
  headers: { "Content-Type": "application/json" },
1900
- body: JSON.stringify({ code: loginCode })
2207
+ body: JSON.stringify({ code: login.code })
1901
2208
  });
1902
2209
  if (!response.ok) {
1903
2210
  throw new Error(`Login exchange failed: ${response.status} ${await response.text()}`);
1904
2211
  }
1905
2212
  const payload = await response.json();
1906
2213
  const auth = {
2214
+ baseUrl: login.baseUrl,
1907
2215
  dashboardUrl: stripTrailingSlash(dashboardUrl),
1908
2216
  token: payload.token,
1909
2217
  createdAt: new Date().toISOString(),
@@ -1937,12 +2245,21 @@ function waitForCallback(expectedState) {
1937
2245
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1938
2246
  const state = url.searchParams.get("state");
1939
2247
  const code = url.searchParams.get("code");
2248
+ const baseUrl = url.searchParams.get("base_url");
1940
2249
  if (state !== expectedState || !code) {
1941
2250
  res.writeHead(400).end("Invalid broods login callback.");
1942
2251
  return;
1943
2252
  }
2253
+ if (!baseUrl) {
2254
+ res.writeHead(400).end("Login callback omitted base_url; update the dashboard deployment.");
2255
+ callbackReject(new Error("Login callback did not advertise the API base URL (base_url). " + "Deploy a dashboard build that includes the Convex-direct CLI auth flow."));
2256
+ return;
2257
+ }
1944
2258
  res.writeHead(200, { "Content-Type": "text/plain" }).end("broods CLI login complete. You can close this tab.");
1945
- callbackResolve(code);
2259
+ callbackResolve({
2260
+ code,
2261
+ baseUrl: stripTrailingSlash(baseUrl)
2262
+ });
1946
2263
  } catch (error) {
1947
2264
  callbackReject(error);
1948
2265
  }
@@ -2230,7 +2547,7 @@ function renderStreamPart(part, state) {
2230
2547
  // package.json
2231
2548
  var package_default = {
2232
2549
  name: "broods",
2233
- version: "0.1.1",
2550
+ version: "0.2.0",
2234
2551
  description: "CLI + TypeScript SDK for the broods agent platform.",
2235
2552
  type: "module",
2236
2553
  license: "MIT",
@@ -2272,8 +2589,15 @@ var package_default = {
2272
2589
  "pack:check": "bun run build && npm pack --dry-run",
2273
2590
  test: "bun test"
2274
2591
  },
2592
+ engines: {
2593
+ node: ">=22"
2594
+ },
2275
2595
  dependencies: {
2276
- ai: "^6.0.195"
2596
+ "@chat-adapter/discord": "^4.31.0",
2597
+ "@chat-adapter/github": "^4.31.0",
2598
+ "@chat-adapter/slack": "^4.31.0",
2599
+ "@chat-adapter/telegram": "^4.31.0",
2600
+ ai: "^7.0.11"
2277
2601
  },
2278
2602
  devDependencies: {
2279
2603
  "@types/bun": "^1.3.14",
@@ -2285,6 +2609,12 @@ var package_default = {
2285
2609
  // src/cli/index.ts
2286
2610
  var VERSION = package_default.version;
2287
2611
  var DEFAULT_DASHBOARD_URL = "https://dashboard.broods.app";
2612
+ var DEFAULT_SERVICE_REGION = "eu-west-1";
2613
+ var SERVICE_REGIONS = [
2614
+ { region: "eu-west-1", label: "eu-west-1 (Ireland)" },
2615
+ { region: "us-east-1", label: "us-east-1 (US East)" },
2616
+ { region: "ap-southeast-1", label: "ap-southeast-1 (Singapore)" }
2617
+ ];
2288
2618
  var HELP = `broods v${VERSION}
2289
2619
 
2290
2620
  Usage: broods <command>
@@ -2310,13 +2640,15 @@ Commands:
2310
2640
  run <agent> <prompt> Run an agent once and pretty-stream the result (thinking, tool calls, text)
2311
2641
 
2312
2642
  Options:
2313
- --dashboard-url <url> Dashboard base URL (default: ${DEFAULT_DASHBOARD_URL})
2643
+ --dashboard-url <url> Dashboard base URL for login and deep links (default: ${DEFAULT_DASHBOARD_URL})
2644
+ --base-url <url> broods API base URL for sync/env calls (default: discovered at login)
2314
2645
  --project <name> Project name override (default: package name or folder)
2315
2646
  --env <name> Target environment override
2647
+ --region <region> Broods service region preference (default: ${DEFAULT_SERVICE_REGION})
2316
2648
  --prune Allow deploy to delete undeclared remote resources
2317
2649
  --rotate-key Mint a fresh runtime API key on deploy and write it to .env.local
2318
- --errors Show WARN/ERROR only (with \`stream\` and \`logs\`)
2319
- --level <lvl> Minimum log level INFO|WARN|ERROR (with \`stream\` and \`logs\`)
2650
+ --errors Show WARN/ERROR only (default for \`dev\`, \`stream\`, and \`logs\`)
2651
+ --level <lvl> Minimum log level INFO|WARN|ERROR (default WARN)
2320
2652
  -n, --limit <n> Backfill line count (with \`logs\`, default 100)
2321
2653
  --json Print logs as raw JSON (with \`logs\`, applies to backfill output)
2322
2654
  --force Allow init to overwrite starter files`;
@@ -2380,6 +2712,7 @@ async function init(args) {
2380
2712
  dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
2381
2713
  project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
2382
2714
  environment: optionValue(args, "--env") ?? "development",
2715
+ region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
2383
2716
  force
2384
2717
  });
2385
2718
  console.log(`Created ${PROJECT_DIR}/`);
@@ -2395,9 +2728,10 @@ async function login(args) {
2395
2728
  const project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2396
2729
  const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2397
2730
  await writeLocalEnvDefaults({
2398
- dashboardUrl: auth.dashboardUrl,
2731
+ dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
2399
2732
  project,
2400
2733
  environment,
2734
+ region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
2401
2735
  force: false
2402
2736
  });
2403
2737
  const user = auth.user?.email || auth.user?.name || auth.user?.authId;
@@ -2410,11 +2744,11 @@ async function login(args) {
2410
2744
  console.log(`Org: ${org}`);
2411
2745
  if (account)
2412
2746
  console.log(`Account: ${account}`);
2413
- await writeRuntimeKeyForLogin(auth.dashboardUrl, auth.token, project, environment);
2747
+ await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
2414
2748
  }
2415
- async function writeRuntimeKeyForLogin(dashboardUrl, token, project, environment) {
2749
+ async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
2416
2750
  try {
2417
- const client = new BroodsSyncClient({ dashboardUrl, token });
2751
+ const client = new BroodsSyncClient({ baseUrl, token });
2418
2752
  const key = await client.getRuntimeKey(project, environment);
2419
2753
  if (key?.apiKey) {
2420
2754
  await writeEnvValue("BROODS_API_KEY", key.apiKey);
@@ -2428,8 +2762,8 @@ async function diff(args) {
2428
2762
  environment: optionValue(args, "--env"),
2429
2763
  command: "dev"
2430
2764
  });
2431
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2432
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2765
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2766
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2433
2767
  const remote = await client.getManifest(manifest.project, manifest.environment);
2434
2768
  printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
2435
2769
  }
@@ -2440,8 +2774,8 @@ async function deploy(args) {
2440
2774
  command: "deploy",
2441
2775
  useRuntimeEnvironment: false
2442
2776
  });
2443
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2444
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2777
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2778
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2445
2779
  const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
2446
2780
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2447
2781
  await ensureGitIgnore();
@@ -2461,11 +2795,15 @@ async function applyDeploymentKey(deployment) {
2461
2795
  }
2462
2796
  function printSyncWarnings(result) {
2463
2797
  const missing = result.warnings?.missingEnv ?? [];
2464
- if (missing.length === 0)
2465
- return;
2466
- printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
2467
- for (const name of missing)
2468
- console.log(` broods env set ${name}`);
2798
+ if (missing.length > 0) {
2799
+ printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
2800
+ for (const name of missing)
2801
+ console.log(` broods env set ${name}`);
2802
+ }
2803
+ const missingPolicies = result.warnings?.missingPolicies ?? [];
2804
+ if (missingPolicies.length > 0) {
2805
+ printWarning(`\u26A0 ${missingPolicies.length} policy ref(s) in agent config match no policy resource ` + `in this deploy and will be ignored at runtime: ${missingPolicies.join(", ")}`);
2806
+ }
2469
2807
  }
2470
2808
  async function dev(args) {
2471
2809
  await ensureDevOnboarding(args);
@@ -2482,7 +2820,8 @@ async function dev(args) {
2482
2820
  const childEnv = {
2483
2821
  ...process.env,
2484
2822
  BROODS_DECLINED_FILE: declinedFile,
2485
- BROODS_SUPPRESS_DEV_TARGET: "1"
2823
+ BROODS_SUPPRESS_DEV_TARGET: "1",
2824
+ BROODS_RELOAD_ENV: "1"
2486
2825
  };
2487
2826
  await printDevTarget(args);
2488
2827
  await runSyncChild(args, childEnv);
@@ -2528,7 +2867,7 @@ async function streamDevLogs(args, signal) {
2528
2867
  try {
2529
2868
  creds = resolveObservabilityCredentials();
2530
2869
  } catch {
2531
- console.log("\xB7 live logs off \u2014 set BROODS_API_KEY (run `deploy` first) to stream agent logs here");
2870
+ console.log("\xB7 live logs off \u2014 no runtime key found for this environment yet. Run `broods dev --once` after login to create or reconnect it.");
2532
2871
  return;
2533
2872
  }
2534
2873
  let project;
@@ -2573,27 +2912,25 @@ async function ensureLocalDevDefaults(args) {
2573
2912
  const missing = [
2574
2913
  "BROODS_DASHBOARD_URL",
2575
2914
  "BROODS_PROJECT",
2576
- "BROODS_ENVIRONMENT"
2915
+ "BROODS_ENVIRONMENT",
2916
+ "BROODS_REGION"
2577
2917
  ].filter((key) => values[key] === undefined);
2578
2918
  if (missing.length === 0)
2579
2919
  return;
2580
2920
  const needsProject = values.BROODS_PROJECT === undefined;
2581
2921
  const needsEnvironment = values.BROODS_ENVIRONMENT === undefined;
2922
+ const needsRegion = values.BROODS_REGION === undefined;
2582
2923
  const runtime = loadBroodsRuntimeConfig();
2583
2924
  const dashboardUrl = optionValue(args, "--dashboard-url") ?? runtime.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
2584
2925
  let project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2585
2926
  let environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2927
+ let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
2586
2928
  if (process.stdin.isTTY && needsProject) {
2587
2929
  const auth = await requireAuthOrLogin(dashboardUrl);
2588
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2930
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2589
2931
  const context = await getOnboardingContextOrFallback(client, auth);
2590
- const selectableOrgs = context.orgs.filter((org) => org.accountStatus === "active");
2591
- if (selectableOrgs.length === 0) {
2592
- throw new Error("No selectable org has an active API account. Open Settings -> API Access in the dashboard first.");
2593
- }
2594
- const selectedOrg = await promptSelect("Select organization", selectableOrgs, formatOrgChoice);
2595
- const selectedContext = selectedOrg.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selectedOrg.id);
2596
- project = await promptText("Project name", defaultProjectName(selectedContext, project));
2932
+ const selectedContext = await selectOnboardingOrg(client, context);
2933
+ project = await selectOnboardingProject(selectedContext, project);
2597
2934
  if (!project.trim())
2598
2935
  throw new Error("Project name is required.");
2599
2936
  }
@@ -2602,16 +2939,20 @@ async function ensureLocalDevDefaults(args) {
2602
2939
  if (!environment.trim())
2603
2940
  throw new Error("Environment is required.");
2604
2941
  }
2942
+ if (process.stdin.isTTY && needsRegion) {
2943
+ region = await promptSelect("Select service region", [...SERVICE_REGIONS], (entry) => entry.label).then((entry) => entry.region);
2944
+ }
2605
2945
  await writeLocalEnvDefaults({
2606
2946
  dashboardUrl,
2607
2947
  project,
2608
2948
  environment,
2949
+ region,
2609
2950
  force: false
2610
2951
  });
2611
2952
  }
2612
2953
  async function requireAuthOrLogin(dashboardUrl) {
2613
2954
  try {
2614
- return await requireAuth(dashboardUrl);
2955
+ return await requireAuth();
2615
2956
  } catch (error) {
2616
2957
  if (!process.stdin.isTTY)
2617
2958
  throw error;
@@ -2643,10 +2984,37 @@ function formatOrgChoice(org) {
2643
2984
  const suffix = org.role === "owner" || org.role === "admin" ? org.role : "member";
2644
2985
  return `${org.name} (${org.slug}, ${suffix})`;
2645
2986
  }
2987
+ async function selectOnboardingOrg(client, context) {
2988
+ const activeOrgs = context.orgs.filter((org) => org.accountStatus === "active");
2989
+ const createNew = { kind: "create", name: "New organization" };
2990
+ const selected = await promptSelect("Select organization", [...activeOrgs, createNew], (entry) => ("kind" in entry) ? "Create new organization" : formatOrgChoice(entry));
2991
+ if ("kind" in selected) {
2992
+ const name = await promptText("Organization name", inferProjectName(process.cwd()));
2993
+ if (!name.trim())
2994
+ throw new Error("Organization name is required.");
2995
+ return await client.createOnboardingOrg(name);
2996
+ }
2997
+ return selected.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selected.id);
2998
+ }
2646
2999
  function defaultProjectName(context, inferred) {
2647
3000
  const exact = context.projects.find((project) => project.name === inferred || project.slug === inferred);
2648
3001
  return exact?.name ?? inferred;
2649
3002
  }
3003
+ async function selectOnboardingProject(context, inferred) {
3004
+ if (context.projects.length === 0) {
3005
+ return promptText("Project name", inferred);
3006
+ }
3007
+ const createNew = { kind: "create", name: defaultProjectName(context, inferred), slug: "" };
3008
+ const choices = [
3009
+ ...context.projects,
3010
+ createNew
3011
+ ];
3012
+ const selected = await promptSelect("Select project", choices, (entry) => ("kind" in entry) && entry.kind === "create" ? `Create new project (${entry.name})` : `${entry.name} (${entry.slug})`);
3013
+ if ("kind" in selected && selected.kind === "create") {
3014
+ return promptText("New project name", selected.name);
3015
+ }
3016
+ return selected.name;
3017
+ }
2650
3018
  function runSyncChild(args, env3) {
2651
3019
  return new Promise((resolvePromise, reject) => {
2652
3020
  const entryPoint = process.argv[1] ?? "";
@@ -2670,12 +3038,12 @@ async function syncDev(args) {
2670
3038
  environment: optionValue(args, "--env"),
2671
3039
  command: "dev"
2672
3040
  });
2673
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2674
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
3041
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3042
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2675
3043
  const remote = await client.getManifest(manifest.project, manifest.environment);
2676
3044
  const diff2 = diffManifests(manifest, remote?.manifest ?? null);
2677
3045
  printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
2678
- await syncLocalEnvVars(client, manifest);
3046
+ await syncLocalEnvVars(client, manifest, envSyncScopeKey(auth.baseUrl, auth.token));
2679
3047
  let result = await client.putManifest(manifest, false);
2680
3048
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2681
3049
  await ensureGitIgnore();
@@ -2725,7 +3093,7 @@ function printChannelEndpoints(channels, result) {
2725
3093
  console.log(`Channel ${channel.alias} (${channel.type}): ${url}${channel.type === "pancake" ? "?secret=<PANCAKE_WEBHOOK_SECRET>" : ""}`);
2726
3094
  }
2727
3095
  }
2728
- async function syncLocalEnvVars(client, manifest) {
3096
+ async function syncLocalEnvVars(client, manifest, scopeKey) {
2729
3097
  const present = collectEnvRefNames(manifest).filter((name) => {
2730
3098
  const value = process.env[name];
2731
3099
  return !name.startsWith("BROODS_") && value !== undefined && value !== "";
@@ -2733,19 +3101,23 @@ async function syncLocalEnvVars(client, manifest) {
2733
3101
  if (present.length === 0)
2734
3102
  return;
2735
3103
  const cache = await loadEnvSyncCache();
2736
- const known = cache[envCacheKey(manifest.project, manifest.environment)] ?? {};
2737
- const changed = present.filter((name) => known[name] !== hashEnvValue(process.env[name]));
3104
+ const known = cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] ?? {};
3105
+ const remoteNames = new Set((await client.listEnv(manifest.project, manifest.environment)).map((entry) => entry.name));
3106
+ const changed = present.filter((name) => !remoteNames.has(name) || known[name] !== hashEnvValue(process.env[name]));
2738
3107
  if (changed.length === 0)
2739
3108
  return;
2740
3109
  await Promise.all(changed.map((name) => client.setEnv(manifest.project, manifest.environment, name, process.env[name])));
2741
3110
  for (const name of changed)
2742
3111
  known[name] = hashEnvValue(process.env[name]);
2743
- cache[envCacheKey(manifest.project, manifest.environment)] = known;
3112
+ cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] = known;
2744
3113
  await saveEnvSyncCache(cache);
2745
3114
  printEnvSync(changed);
2746
3115
  }
2747
- function envCacheKey(project, environment) {
2748
- return `${project}:${environment}`;
3116
+ function envSyncScopeKey(baseUrl, token) {
3117
+ return `${baseUrl}:${createHash2("sha256").update(token).digest("hex").slice(0, 16)}`;
3118
+ }
3119
+ function envCacheKey(scopeKey, project, environment) {
3120
+ return `${scopeKey}:${project}:${environment}`;
2749
3121
  }
2750
3122
  function envSyncCachePath() {
2751
3123
  return resolve4(dirname2(USER_CONFIG_PATH), "env-sync.json");
@@ -2770,15 +3142,15 @@ async function saveEnvSyncCache(cache) {
2770
3142
  await writeFile3(path, `${JSON.stringify(cache, null, 2)}
2771
3143
  `, "utf8");
2772
3144
  }
2773
- async function rememberEnvSyncValue(project, environment, name, value) {
3145
+ async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
2774
3146
  const cache = await loadEnvSyncCache();
2775
- const key = envCacheKey(project, environment);
3147
+ const key = envCacheKey(scopeKey, project, environment);
2776
3148
  cache[key] = { ...cache[key] ?? {}, [name]: hashEnvValue(value) };
2777
3149
  await saveEnvSyncCache(cache);
2778
3150
  }
2779
- async function forgetEnvSyncValue(project, environment, name) {
3151
+ async function forgetEnvSyncValue(scopeKey, project, environment, name) {
2780
3152
  const cache = await loadEnvSyncCache();
2781
- const key = envCacheKey(project, environment);
3153
+ const key = envCacheKey(scopeKey, project, environment);
2782
3154
  if (!cache[key] || !(name in cache[key]))
2783
3155
  return;
2784
3156
  delete cache[key][name];
@@ -2790,11 +3162,11 @@ async function printDevTarget(args) {
2790
3162
  environment: optionValue(args, "--env"),
2791
3163
  command: "dev"
2792
3164
  });
2793
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3165
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2794
3166
  printDeploymentTarget({
2795
3167
  project: manifest.project,
2796
3168
  environment: manifest.environment,
2797
- dashboardUrl: auth.dashboardUrl
3169
+ dashboardUrl: auth.dashboardUrl ?? DEFAULT_DASHBOARD_URL
2798
3170
  });
2799
3171
  }
2800
3172
  async function loadDeclinedDeletes() {
@@ -2833,8 +3205,9 @@ async function envCommand(args) {
2833
3205
  environment: optionValue(args, "--env"),
2834
3206
  command: "dev"
2835
3207
  });
2836
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2837
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
3208
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3209
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3210
+ const scopeKey = envSyncScopeKey(auth.baseUrl, auth.token);
2838
3211
  const target = `${manifest.project}/${manifest.environment}`;
2839
3212
  if (isList) {
2840
3213
  const variables = await client.listEnv(manifest.project, manifest.environment);
@@ -2859,13 +3232,13 @@ async function envCommand(args) {
2859
3232
  }
2860
3233
  if (isRemove) {
2861
3234
  await client.removeEnv(manifest.project, manifest.environment, name);
2862
- await forgetEnvSyncValue(manifest.project, manifest.environment, name);
3235
+ await forgetEnvSyncValue(scopeKey, manifest.project, manifest.environment, name);
2863
3236
  console.log(`Removed ${name} from ${target}`);
2864
3237
  return;
2865
3238
  }
2866
3239
  const value = await promptSecret(name);
2867
3240
  await client.setEnv(manifest.project, manifest.environment, name, value);
2868
- await rememberEnvSyncValue(manifest.project, manifest.environment, name, value);
3241
+ await rememberEnvSyncValue(scopeKey, manifest.project, manifest.environment, name, value);
2869
3242
  console.log(`Stored ${name} for ${target}`);
2870
3243
  }
2871
3244
  function resolveObservabilityCredentials() {
@@ -2882,7 +3255,7 @@ function resolveMinLevel(args) {
2882
3255
  return "WARN";
2883
3256
  const raw = optionValue(args, "--level");
2884
3257
  if (!raw)
2885
- return;
3258
+ return "WARN";
2886
3259
  const upper = raw.toUpperCase();
2887
3260
  if (upper === "WARN" || upper === "WARNING")
2888
3261
  return "WARN";
@@ -2975,8 +3348,8 @@ async function loadAgentsWithIds(args) {
2975
3348
  environment: optionValue(args, "--env"),
2976
3349
  command: "dev"
2977
3350
  });
2978
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2979
- const remote = await new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3351
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3352
+ const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
2980
3353
  const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
2981
3354
  name: resource.name,
2982
3355
  config: resource.config ?? {},
@@ -3008,10 +3381,10 @@ async function agentGet(name, args) {
3008
3381
  const config = agent.config;
3009
3382
  const sandbox = typeof config.sandbox === "string" ? config.sandbox : undefined;
3010
3383
  const workspaces = Array.isArray(config.workspaces) ? config.workspaces.map((ref) => typeof ref === "string" ? ref : ref.name).filter(Boolean) : [];
3011
- const tools = isRecord(config.tools) ? Object.keys(config.tools) : [];
3012
- const channels = isRecord(config.channels) ? Object.keys(config.channels) : [];
3013
- const subagents = isRecord(config.subagent) && Array.isArray(config.subagent.allowed) ? config.subagent.allowed.map((entry) => typeof entry === "string" ? entry : entry.name).filter(Boolean) : [];
3014
- const webhooks = isRecord(config.hooks) && Array.isArray(config.hooks.webhooks) ? config.hooks.webhooks.filter(isRecord) : [];
3384
+ const tools = isPlainObject(config.tools) ? Object.keys(config.tools) : [];
3385
+ const channels = isPlainObject(config.channels) ? Object.keys(config.channels) : [];
3386
+ const subagents = isPlainObject(config.subagent) && Array.isArray(config.subagent.allowed) ? config.subagent.allowed.map((entry) => typeof entry === "string" ? entry : entry.name).filter(Boolean) : [];
3387
+ const webhooks = isPlainObject(config.hooks) && Array.isArray(config.hooks.webhooks) ? config.hooks.webhooks.filter(isPlainObject) : [];
3015
3388
  console.log(`Agent: ${agent.name}`);
3016
3389
  console.log(` Project/Env: ${manifest.project}/${manifest.environment}`);
3017
3390
  console.log(` Deployed id: ${agent.agentId ?? "not deployed"}`);
@@ -3032,16 +3405,13 @@ async function agentGet(name, args) {
3032
3405
  }
3033
3406
  }
3034
3407
  function agentModelLabel(config) {
3035
- const model = isRecord(config.model) ? config.model : {};
3408
+ const model = isPlainObject(config.model) ? config.model : {};
3036
3409
  const provider = typeof model.provider === "string" ? model.provider : undefined;
3037
3410
  const modelId = typeof model.modelId === "string" ? model.modelId : undefined;
3038
3411
  if (provider && modelId)
3039
3412
  return `${provider}/${modelId}`;
3040
3413
  return modelId ?? provider ?? "unconfigured model";
3041
3414
  }
3042
- function isRecord(value) {
3043
- return typeof value === "object" && value !== null && !Array.isArray(value);
3044
- }
3045
3415
  async function run(args) {
3046
3416
  const [agentName, ...promptParts] = args.filter((arg) => !arg.startsWith("--"));
3047
3417
  if (!agentName || promptParts.length === 0) {
@@ -3052,18 +3422,22 @@ async function run(args) {
3052
3422
  environment: optionValue(args, "--env"),
3053
3423
  command: "dev"
3054
3424
  });
3055
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3425
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3056
3426
  const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
3057
3427
  if (!agent)
3058
3428
  throw new Error(`Unknown local agent: ${agentName}`);
3059
- const remote = await new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3429
+ const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3060
3430
  const agentId = remote?.ids.agents[agentName];
3061
3431
  if (!agentId)
3062
- throw new Error(`Agent ${agentName} is not deployed. Run broods deploy first.`);
3432
+ throw new Error(`Agent ${agentName} is not synced yet. Run broods dev --once or broods deploy first.`);
3433
+ const runtimeKey = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
3434
+ if (runtimeKey?.apiKey) {
3435
+ await writeEnvValue("BROODS_API_KEY", runtimeKey.apiKey);
3436
+ }
3063
3437
  if (agent.config.publicAccess !== true) {
3064
3438
  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.");
3065
3439
  }
3066
- const client = new BroodsClient;
3440
+ const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
3067
3441
  const state = createRenderState();
3068
3442
  try {
3069
3443
  for await (const part of client.stream({
@@ -3071,7 +3445,10 @@ async function run(args) {
3071
3445
  name: agentName,
3072
3446
  id: agentId,
3073
3447
  project: manifest.project,
3074
- environment: manifest.environment
3448
+ environment: manifest.environment,
3449
+ ...runtimeKey?.endpointId ? { endpointId: runtimeKey.endpointId } : {},
3450
+ ...runtimeKey?.projectSlug ? { projectSlug: runtimeKey.projectSlug } : {},
3451
+ ...runtimeKey?.environmentSlug ? { environmentSlug: runtimeKey.environmentSlug } : {}
3075
3452
  }, { input: promptParts.join(" ") })) {
3076
3453
  renderStreamPart(part, state);
3077
3454
  }
@@ -3116,7 +3493,8 @@ async function writeLocalEnvDefaults(options) {
3116
3493
  const nextValues = {
3117
3494
  BROODS_DASHBOARD_URL: options.dashboardUrl,
3118
3495
  BROODS_PROJECT: options.project,
3119
- BROODS_ENVIRONMENT: options.environment
3496
+ BROODS_ENVIRONMENT: options.environment,
3497
+ BROODS_REGION: options.region
3120
3498
  };
3121
3499
  const lines = current ? current.replace(/\n?$/, `
3122
3500
  `).split(/\n/) : [
@@ -3209,7 +3587,7 @@ async function collectSourceFiles(dir, files) {
3209
3587
  await collectSourceFiles(full, files);
3210
3588
  continue;
3211
3589
  }
3212
- if (entry.isFile() && entry.name.endsWith(".ts"))
3590
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".md")))
3213
3591
  files.push(full);
3214
3592
  }
3215
3593
  }