broods 0.1.0 → 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);
78
+ continue;
79
+ }
80
+ const mtimeMs = statSync(path).mtimeMs;
81
+ const cached = envFileCache.get(path);
82
+ if (cached && cached.mtimeMs === mtimeMs)
74
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
  }
@@ -2227,10 +2544,77 @@ function renderStreamPart(part, state) {
2227
2544
  break;
2228
2545
  }
2229
2546
  }
2547
+ // package.json
2548
+ var package_default = {
2549
+ name: "broods",
2550
+ version: "0.2.0",
2551
+ description: "CLI + TypeScript SDK for the broods agent platform.",
2552
+ type: "module",
2553
+ license: "MIT",
2554
+ homepage: "https://github.com/beeblastco/broods#readme",
2555
+ repository: {
2556
+ type: "git",
2557
+ url: "git+https://github.com/beeblastco/broods.git",
2558
+ directory: "packages/broods"
2559
+ },
2560
+ bugs: {
2561
+ url: "https://github.com/beeblastco/broods/issues"
2562
+ },
2563
+ keywords: [
2564
+ "agents",
2565
+ "ai",
2566
+ "sdk",
2567
+ "cli",
2568
+ "broods"
2569
+ ],
2570
+ bin: {
2571
+ broods: "dist/cli/index.js"
2572
+ },
2573
+ exports: {
2574
+ ".": {
2575
+ types: "./dist/index.d.ts",
2576
+ import: "./dist/index.js"
2577
+ }
2578
+ },
2579
+ main: "./dist/index.js",
2580
+ types: "./dist/index.d.ts",
2581
+ files: [
2582
+ "dist",
2583
+ "README.md",
2584
+ "LICENSE.md"
2585
+ ],
2586
+ scripts: {
2587
+ build: "bun scripts/build.ts && rollup -c rollup.dts.config.mjs && bun scripts/clean-dts-side-effects.ts && rm -rf .dts-temp",
2588
+ check: "tsc --noEmit",
2589
+ "pack:check": "bun run build && npm pack --dry-run",
2590
+ test: "bun test"
2591
+ },
2592
+ engines: {
2593
+ node: ">=22"
2594
+ },
2595
+ dependencies: {
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"
2601
+ },
2602
+ devDependencies: {
2603
+ "@types/bun": "^1.3.14",
2604
+ rollup: "^4.53.3",
2605
+ "rollup-plugin-dts": "^6.3.0"
2606
+ }
2607
+ };
2230
2608
 
2231
2609
  // src/cli/index.ts
2232
- var VERSION = "0.1.0";
2610
+ var VERSION = package_default.version;
2233
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
+ ];
2234
2618
  var HELP = `broods v${VERSION}
2235
2619
 
2236
2620
  Usage: broods <command>
@@ -2256,13 +2640,15 @@ Commands:
2256
2640
  run <agent> <prompt> Run an agent once and pretty-stream the result (thinking, tool calls, text)
2257
2641
 
2258
2642
  Options:
2259
- --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)
2260
2645
  --project <name> Project name override (default: package name or folder)
2261
2646
  --env <name> Target environment override
2647
+ --region <region> Broods service region preference (default: ${DEFAULT_SERVICE_REGION})
2262
2648
  --prune Allow deploy to delete undeclared remote resources
2263
2649
  --rotate-key Mint a fresh runtime API key on deploy and write it to .env.local
2264
- --errors Show WARN/ERROR only (with \`stream\` and \`logs\`)
2265
- --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)
2266
2652
  -n, --limit <n> Backfill line count (with \`logs\`, default 100)
2267
2653
  --json Print logs as raw JSON (with \`logs\`, applies to backfill output)
2268
2654
  --force Allow init to overwrite starter files`;
@@ -2326,6 +2712,7 @@ async function init(args) {
2326
2712
  dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
2327
2713
  project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
2328
2714
  environment: optionValue(args, "--env") ?? "development",
2715
+ region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
2329
2716
  force
2330
2717
  });
2331
2718
  console.log(`Created ${PROJECT_DIR}/`);
@@ -2341,9 +2728,10 @@ async function login(args) {
2341
2728
  const project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2342
2729
  const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2343
2730
  await writeLocalEnvDefaults({
2344
- dashboardUrl: auth.dashboardUrl,
2731
+ dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
2345
2732
  project,
2346
2733
  environment,
2734
+ region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
2347
2735
  force: false
2348
2736
  });
2349
2737
  const user = auth.user?.email || auth.user?.name || auth.user?.authId;
@@ -2356,11 +2744,11 @@ async function login(args) {
2356
2744
  console.log(`Org: ${org}`);
2357
2745
  if (account)
2358
2746
  console.log(`Account: ${account}`);
2359
- await writeRuntimeKeyForLogin(auth.dashboardUrl, auth.token, project, environment);
2747
+ await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
2360
2748
  }
2361
- async function writeRuntimeKeyForLogin(dashboardUrl, token, project, environment) {
2749
+ async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
2362
2750
  try {
2363
- const client = new BroodsSyncClient({ dashboardUrl, token });
2751
+ const client = new BroodsSyncClient({ baseUrl, token });
2364
2752
  const key = await client.getRuntimeKey(project, environment);
2365
2753
  if (key?.apiKey) {
2366
2754
  await writeEnvValue("BROODS_API_KEY", key.apiKey);
@@ -2374,8 +2762,8 @@ async function diff(args) {
2374
2762
  environment: optionValue(args, "--env"),
2375
2763
  command: "dev"
2376
2764
  });
2377
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2378
- 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 });
2379
2767
  const remote = await client.getManifest(manifest.project, manifest.environment);
2380
2768
  printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
2381
2769
  }
@@ -2386,8 +2774,8 @@ async function deploy(args) {
2386
2774
  command: "deploy",
2387
2775
  useRuntimeEnvironment: false
2388
2776
  });
2389
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2390
- 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 });
2391
2779
  const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
2392
2780
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2393
2781
  await ensureGitIgnore();
@@ -2407,11 +2795,15 @@ async function applyDeploymentKey(deployment) {
2407
2795
  }
2408
2796
  function printSyncWarnings(result) {
2409
2797
  const missing = result.warnings?.missingEnv ?? [];
2410
- if (missing.length === 0)
2411
- return;
2412
- printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
2413
- for (const name of missing)
2414
- 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
+ }
2415
2807
  }
2416
2808
  async function dev(args) {
2417
2809
  await ensureDevOnboarding(args);
@@ -2428,7 +2820,8 @@ async function dev(args) {
2428
2820
  const childEnv = {
2429
2821
  ...process.env,
2430
2822
  BROODS_DECLINED_FILE: declinedFile,
2431
- BROODS_SUPPRESS_DEV_TARGET: "1"
2823
+ BROODS_SUPPRESS_DEV_TARGET: "1",
2824
+ BROODS_RELOAD_ENV: "1"
2432
2825
  };
2433
2826
  await printDevTarget(args);
2434
2827
  await runSyncChild(args, childEnv);
@@ -2474,7 +2867,7 @@ async function streamDevLogs(args, signal) {
2474
2867
  try {
2475
2868
  creds = resolveObservabilityCredentials();
2476
2869
  } catch {
2477
- 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.");
2478
2871
  return;
2479
2872
  }
2480
2873
  let project;
@@ -2519,27 +2912,25 @@ async function ensureLocalDevDefaults(args) {
2519
2912
  const missing = [
2520
2913
  "BROODS_DASHBOARD_URL",
2521
2914
  "BROODS_PROJECT",
2522
- "BROODS_ENVIRONMENT"
2915
+ "BROODS_ENVIRONMENT",
2916
+ "BROODS_REGION"
2523
2917
  ].filter((key) => values[key] === undefined);
2524
2918
  if (missing.length === 0)
2525
2919
  return;
2526
2920
  const needsProject = values.BROODS_PROJECT === undefined;
2527
2921
  const needsEnvironment = values.BROODS_ENVIRONMENT === undefined;
2922
+ const needsRegion = values.BROODS_REGION === undefined;
2528
2923
  const runtime = loadBroodsRuntimeConfig();
2529
2924
  const dashboardUrl = optionValue(args, "--dashboard-url") ?? runtime.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
2530
2925
  let project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2531
2926
  let environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2927
+ let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
2532
2928
  if (process.stdin.isTTY && needsProject) {
2533
2929
  const auth = await requireAuthOrLogin(dashboardUrl);
2534
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2930
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2535
2931
  const context = await getOnboardingContextOrFallback(client, auth);
2536
- const selectableOrgs = context.orgs.filter((org) => org.accountStatus === "active");
2537
- if (selectableOrgs.length === 0) {
2538
- throw new Error("No selectable org has an active API account. Open Settings -> API Access in the dashboard first.");
2539
- }
2540
- const selectedOrg = await promptSelect("Select organization", selectableOrgs, formatOrgChoice);
2541
- const selectedContext = selectedOrg.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selectedOrg.id);
2542
- project = await promptText("Project name", defaultProjectName(selectedContext, project));
2932
+ const selectedContext = await selectOnboardingOrg(client, context);
2933
+ project = await selectOnboardingProject(selectedContext, project);
2543
2934
  if (!project.trim())
2544
2935
  throw new Error("Project name is required.");
2545
2936
  }
@@ -2548,16 +2939,20 @@ async function ensureLocalDevDefaults(args) {
2548
2939
  if (!environment.trim())
2549
2940
  throw new Error("Environment is required.");
2550
2941
  }
2942
+ if (process.stdin.isTTY && needsRegion) {
2943
+ region = await promptSelect("Select service region", [...SERVICE_REGIONS], (entry) => entry.label).then((entry) => entry.region);
2944
+ }
2551
2945
  await writeLocalEnvDefaults({
2552
2946
  dashboardUrl,
2553
2947
  project,
2554
2948
  environment,
2949
+ region,
2555
2950
  force: false
2556
2951
  });
2557
2952
  }
2558
2953
  async function requireAuthOrLogin(dashboardUrl) {
2559
2954
  try {
2560
- return await requireAuth(dashboardUrl);
2955
+ return await requireAuth();
2561
2956
  } catch (error) {
2562
2957
  if (!process.stdin.isTTY)
2563
2958
  throw error;
@@ -2589,10 +2984,37 @@ function formatOrgChoice(org) {
2589
2984
  const suffix = org.role === "owner" || org.role === "admin" ? org.role : "member";
2590
2985
  return `${org.name} (${org.slug}, ${suffix})`;
2591
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
+ }
2592
2999
  function defaultProjectName(context, inferred) {
2593
3000
  const exact = context.projects.find((project) => project.name === inferred || project.slug === inferred);
2594
3001
  return exact?.name ?? inferred;
2595
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
+ }
2596
3018
  function runSyncChild(args, env3) {
2597
3019
  return new Promise((resolvePromise, reject) => {
2598
3020
  const entryPoint = process.argv[1] ?? "";
@@ -2616,12 +3038,12 @@ async function syncDev(args) {
2616
3038
  environment: optionValue(args, "--env"),
2617
3039
  command: "dev"
2618
3040
  });
2619
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2620
- 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 });
2621
3043
  const remote = await client.getManifest(manifest.project, manifest.environment);
2622
3044
  const diff2 = diffManifests(manifest, remote?.manifest ?? null);
2623
3045
  printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
2624
- await syncLocalEnvVars(client, manifest);
3046
+ await syncLocalEnvVars(client, manifest, envSyncScopeKey(auth.baseUrl, auth.token));
2625
3047
  let result = await client.putManifest(manifest, false);
2626
3048
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2627
3049
  await ensureGitIgnore();
@@ -2671,7 +3093,7 @@ function printChannelEndpoints(channels, result) {
2671
3093
  console.log(`Channel ${channel.alias} (${channel.type}): ${url}${channel.type === "pancake" ? "?secret=<PANCAKE_WEBHOOK_SECRET>" : ""}`);
2672
3094
  }
2673
3095
  }
2674
- async function syncLocalEnvVars(client, manifest) {
3096
+ async function syncLocalEnvVars(client, manifest, scopeKey) {
2675
3097
  const present = collectEnvRefNames(manifest).filter((name) => {
2676
3098
  const value = process.env[name];
2677
3099
  return !name.startsWith("BROODS_") && value !== undefined && value !== "";
@@ -2679,19 +3101,23 @@ async function syncLocalEnvVars(client, manifest) {
2679
3101
  if (present.length === 0)
2680
3102
  return;
2681
3103
  const cache = await loadEnvSyncCache();
2682
- const known = cache[envCacheKey(manifest.project, manifest.environment)] ?? {};
2683
- 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]));
2684
3107
  if (changed.length === 0)
2685
3108
  return;
2686
3109
  await Promise.all(changed.map((name) => client.setEnv(manifest.project, manifest.environment, name, process.env[name])));
2687
3110
  for (const name of changed)
2688
3111
  known[name] = hashEnvValue(process.env[name]);
2689
- cache[envCacheKey(manifest.project, manifest.environment)] = known;
3112
+ cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] = known;
2690
3113
  await saveEnvSyncCache(cache);
2691
3114
  printEnvSync(changed);
2692
3115
  }
2693
- function envCacheKey(project, environment) {
2694
- 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}`;
2695
3121
  }
2696
3122
  function envSyncCachePath() {
2697
3123
  return resolve4(dirname2(USER_CONFIG_PATH), "env-sync.json");
@@ -2716,15 +3142,15 @@ async function saveEnvSyncCache(cache) {
2716
3142
  await writeFile3(path, `${JSON.stringify(cache, null, 2)}
2717
3143
  `, "utf8");
2718
3144
  }
2719
- async function rememberEnvSyncValue(project, environment, name, value) {
3145
+ async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
2720
3146
  const cache = await loadEnvSyncCache();
2721
- const key = envCacheKey(project, environment);
3147
+ const key = envCacheKey(scopeKey, project, environment);
2722
3148
  cache[key] = { ...cache[key] ?? {}, [name]: hashEnvValue(value) };
2723
3149
  await saveEnvSyncCache(cache);
2724
3150
  }
2725
- async function forgetEnvSyncValue(project, environment, name) {
3151
+ async function forgetEnvSyncValue(scopeKey, project, environment, name) {
2726
3152
  const cache = await loadEnvSyncCache();
2727
- const key = envCacheKey(project, environment);
3153
+ const key = envCacheKey(scopeKey, project, environment);
2728
3154
  if (!cache[key] || !(name in cache[key]))
2729
3155
  return;
2730
3156
  delete cache[key][name];
@@ -2736,11 +3162,11 @@ async function printDevTarget(args) {
2736
3162
  environment: optionValue(args, "--env"),
2737
3163
  command: "dev"
2738
3164
  });
2739
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3165
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2740
3166
  printDeploymentTarget({
2741
3167
  project: manifest.project,
2742
3168
  environment: manifest.environment,
2743
- dashboardUrl: auth.dashboardUrl
3169
+ dashboardUrl: auth.dashboardUrl ?? DEFAULT_DASHBOARD_URL
2744
3170
  });
2745
3171
  }
2746
3172
  async function loadDeclinedDeletes() {
@@ -2779,8 +3205,9 @@ async function envCommand(args) {
2779
3205
  environment: optionValue(args, "--env"),
2780
3206
  command: "dev"
2781
3207
  });
2782
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2783
- 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);
2784
3211
  const target = `${manifest.project}/${manifest.environment}`;
2785
3212
  if (isList) {
2786
3213
  const variables = await client.listEnv(manifest.project, manifest.environment);
@@ -2805,13 +3232,13 @@ async function envCommand(args) {
2805
3232
  }
2806
3233
  if (isRemove) {
2807
3234
  await client.removeEnv(manifest.project, manifest.environment, name);
2808
- await forgetEnvSyncValue(manifest.project, manifest.environment, name);
3235
+ await forgetEnvSyncValue(scopeKey, manifest.project, manifest.environment, name);
2809
3236
  console.log(`Removed ${name} from ${target}`);
2810
3237
  return;
2811
3238
  }
2812
3239
  const value = await promptSecret(name);
2813
3240
  await client.setEnv(manifest.project, manifest.environment, name, value);
2814
- await rememberEnvSyncValue(manifest.project, manifest.environment, name, value);
3241
+ await rememberEnvSyncValue(scopeKey, manifest.project, manifest.environment, name, value);
2815
3242
  console.log(`Stored ${name} for ${target}`);
2816
3243
  }
2817
3244
  function resolveObservabilityCredentials() {
@@ -2828,7 +3255,7 @@ function resolveMinLevel(args) {
2828
3255
  return "WARN";
2829
3256
  const raw = optionValue(args, "--level");
2830
3257
  if (!raw)
2831
- return;
3258
+ return "WARN";
2832
3259
  const upper = raw.toUpperCase();
2833
3260
  if (upper === "WARN" || upper === "WARNING")
2834
3261
  return "WARN";
@@ -2921,8 +3348,8 @@ async function loadAgentsWithIds(args) {
2921
3348
  environment: optionValue(args, "--env"),
2922
3349
  command: "dev"
2923
3350
  });
2924
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2925
- 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);
2926
3353
  const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
2927
3354
  name: resource.name,
2928
3355
  config: resource.config ?? {},
@@ -2954,10 +3381,10 @@ async function agentGet(name, args) {
2954
3381
  const config = agent.config;
2955
3382
  const sandbox = typeof config.sandbox === "string" ? config.sandbox : undefined;
2956
3383
  const workspaces = Array.isArray(config.workspaces) ? config.workspaces.map((ref) => typeof ref === "string" ? ref : ref.name).filter(Boolean) : [];
2957
- const tools = isRecord(config.tools) ? Object.keys(config.tools) : [];
2958
- const channels = isRecord(config.channels) ? Object.keys(config.channels) : [];
2959
- const subagents = isRecord(config.subagent) && Array.isArray(config.subagent.allowed) ? config.subagent.allowed.map((entry) => typeof entry === "string" ? entry : entry.name).filter(Boolean) : [];
2960
- 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) : [];
2961
3388
  console.log(`Agent: ${agent.name}`);
2962
3389
  console.log(` Project/Env: ${manifest.project}/${manifest.environment}`);
2963
3390
  console.log(` Deployed id: ${agent.agentId ?? "not deployed"}`);
@@ -2978,16 +3405,13 @@ async function agentGet(name, args) {
2978
3405
  }
2979
3406
  }
2980
3407
  function agentModelLabel(config) {
2981
- const model = isRecord(config.model) ? config.model : {};
3408
+ const model = isPlainObject(config.model) ? config.model : {};
2982
3409
  const provider = typeof model.provider === "string" ? model.provider : undefined;
2983
3410
  const modelId = typeof model.modelId === "string" ? model.modelId : undefined;
2984
3411
  if (provider && modelId)
2985
3412
  return `${provider}/${modelId}`;
2986
3413
  return modelId ?? provider ?? "unconfigured model";
2987
3414
  }
2988
- function isRecord(value) {
2989
- return typeof value === "object" && value !== null && !Array.isArray(value);
2990
- }
2991
3415
  async function run(args) {
2992
3416
  const [agentName, ...promptParts] = args.filter((arg) => !arg.startsWith("--"));
2993
3417
  if (!agentName || promptParts.length === 0) {
@@ -2998,18 +3422,22 @@ async function run(args) {
2998
3422
  environment: optionValue(args, "--env"),
2999
3423
  command: "dev"
3000
3424
  });
3001
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3425
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3002
3426
  const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
3003
3427
  if (!agent)
3004
3428
  throw new Error(`Unknown local agent: ${agentName}`);
3005
- 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);
3006
3430
  const agentId = remote?.ids.agents[agentName];
3007
3431
  if (!agentId)
3008
- 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
+ }
3009
3437
  if (agent.config.publicAccess !== true) {
3010
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.");
3011
3439
  }
3012
- const client = new BroodsClient;
3440
+ const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
3013
3441
  const state = createRenderState();
3014
3442
  try {
3015
3443
  for await (const part of client.stream({
@@ -3017,7 +3445,10 @@ async function run(args) {
3017
3445
  name: agentName,
3018
3446
  id: agentId,
3019
3447
  project: manifest.project,
3020
- 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 } : {}
3021
3452
  }, { input: promptParts.join(" ") })) {
3022
3453
  renderStreamPart(part, state);
3023
3454
  }
@@ -3062,7 +3493,8 @@ async function writeLocalEnvDefaults(options) {
3062
3493
  const nextValues = {
3063
3494
  BROODS_DASHBOARD_URL: options.dashboardUrl,
3064
3495
  BROODS_PROJECT: options.project,
3065
- BROODS_ENVIRONMENT: options.environment
3496
+ BROODS_ENVIRONMENT: options.environment,
3497
+ BROODS_REGION: options.region
3066
3498
  };
3067
3499
  const lines = current ? current.replace(/\n?$/, `
3068
3500
  `).split(/\n/) : [
@@ -3155,7 +3587,7 @@ async function collectSourceFiles(dir, files) {
3155
3587
  await collectSourceFiles(full, files);
3156
3588
  continue;
3157
3589
  }
3158
- if (entry.isFile() && entry.name.endsWith(".ts"))
3590
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".md")))
3159
3591
  files.push(full);
3160
3592
  }
3161
3593
  }