broods 0.1.1 → 0.2.1

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,211 @@ async function normalizeConfig(resource, projectRoot) {
564
686
  }
565
687
  return rewriteValues(resource.config);
566
688
  }
689
+ var KNOWN_PROVIDER_NAMES = ["google", "openai", "anthropic", "bedrock", "gateway", "minimax", "custom"];
690
+ var KNOWN_PROVIDER_SETTING_KEYS = new Set([
691
+ "apiKey",
692
+ "base_url",
693
+ "baseURL",
694
+ "headers",
695
+ "organization",
696
+ "project",
697
+ "name",
698
+ "region",
699
+ "accessKeyId",
700
+ "secretAccessKey",
701
+ "sessionToken"
702
+ ]);
703
+ function suggestProviderKey(key) {
704
+ const canonical = key.replace(/[^a-z0-9]/gi, "").toLowerCase();
705
+ if (canonical === "baseurl")
706
+ return `"base_url" or "baseURL"`;
707
+ if (canonical === "apikey")
708
+ return `"apiKey"`;
709
+ return "";
710
+ }
711
+ function validateProviderConfig(agentName, provider) {
712
+ if (provider === undefined || provider === null)
713
+ return;
714
+ if (typeof provider !== "object" || Array.isArray(provider)) {
715
+ throw new Error(`Agent "${agentName}" config.provider must be an object`);
716
+ }
717
+ for (const [providerName, settings] of Object.entries(provider)) {
718
+ if (!KNOWN_PROVIDER_NAMES.includes(providerName)) {
719
+ throw new Error(`Agent "${agentName}" config.provider.${providerName} is not a supported provider (expected one of: ${KNOWN_PROVIDER_NAMES.join(", ")})`);
720
+ }
721
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
722
+ throw new Error(`Agent "${agentName}" config.provider.${providerName} must be an object`);
723
+ }
724
+ const record = settings;
725
+ for (const key of Object.keys(record)) {
726
+ if (KNOWN_PROVIDER_SETTING_KEYS.has(key))
727
+ continue;
728
+ const suggestion = suggestProviderKey(key);
729
+ throw new Error(`Agent "${agentName}" config.provider.${providerName} has unknown option "${key}"${suggestion ? ` \u2014 did you mean ${suggestion}?` : ""}`);
730
+ }
731
+ if (providerName === "custom" && record.base_url === undefined && record.baseURL === undefined) {
732
+ throw new Error(`Agent "${agentName}" config.provider.custom.base_url is required (use "base_url" or "baseURL")`);
733
+ }
734
+ }
735
+ }
736
+ function normalizeAgentConfig(resource, _projectRoot) {
737
+ const config = { ...resource.config };
738
+ validateProviderConfig(resource.name, config.provider);
739
+ const inlineHooks = normalizeInlineAgentHooks(resource.name, config.hooks);
740
+ if (inlineHooks) {
741
+ config.hooks = inlineHooks.agentHooksConfig;
742
+ } else if (config.hooks !== undefined) {
743
+ config.hooks = stripInlineHookKeys(config.hooks, resource.name);
744
+ }
745
+ if (config.channels !== undefined) {
746
+ if (!Array.isArray(config.channels)) {
747
+ throw new Error(`Agent "${resource.name}" config.channels must be an array of channel definitions`);
748
+ }
749
+ config.channels = Object.fromEntries(config.channels.map((channel) => {
750
+ if (!isChannelDefinition(channel)) {
751
+ throw new Error(`Agent "${resource.name}" config.channels must contain channel definitions`);
752
+ }
753
+ const channelId = `${resource.name}${capitalize(channel.type)}Channel`;
754
+ return [channel.type, { id: channelId, ...channel.config }];
755
+ }));
756
+ }
757
+ if (isResource(config.sandbox)) {
758
+ config.sandbox = config.sandbox.name;
759
+ }
760
+ if (Array.isArray(config.workspaces)) {
761
+ config.workspaces = config.workspaces.map((workspace) => normalizeWorkspaceRef(workspace, resource.name));
762
+ }
763
+ if (config.policy !== undefined) {
764
+ const policy = normalizeAgentPolicyConfig(config.policy, resource.name);
765
+ if (policy)
766
+ config.policy = policy;
767
+ else
768
+ delete config.policy;
769
+ }
770
+ return {
771
+ config: rewriteValues(config),
772
+ ...inlineHooks?.hookResource ? { hookResource: inlineHooks.hookResource } : {}
773
+ };
774
+ }
775
+ function normalizeInlineAgentHooks(agentName, hooks) {
776
+ if (hooks === undefined)
777
+ return;
778
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
779
+ throw new Error(`Agent "${agentName}" config.hooks must be an object`);
780
+ }
781
+ const hookConfig = hooks;
782
+ const entries = Object.keys(INLINE_AGENT_HOOK_EVENTS).flatMap((name) => {
783
+ const handler = hookConfig[name];
784
+ if (handler === undefined)
785
+ return [];
786
+ if (typeof handler !== "function") {
787
+ throw new Error(`Agent "${agentName}" config.hooks.${name} must be a function`);
788
+ }
789
+ return [{
790
+ name,
791
+ event: INLINE_AGENT_HOOK_EVENTS[name],
792
+ source: toHookFunctionExpression(handler.toString())
793
+ }];
794
+ });
795
+ if (entries.length === 0)
796
+ return;
797
+ const hookName = `${agentName}-hooks`;
798
+ const bundle = transpileInlineHookBundle(agentName, entries);
799
+ const bundleSize = Buffer.byteLength(bundle);
800
+ if (bundleSize > MAX_BUNDLE_FILE_BYTES) {
801
+ throw new Error(`Agent "${agentName}" hook bundle is too large (${bundleSize} bytes, max ${MAX_BUNDLE_FILE_BYTES})`);
802
+ }
803
+ const events = entries.map((entry) => entry.event);
804
+ const agentHooksConfig = stripInlineHookKeys(hookConfig, agentName);
805
+ agentHooksConfig.code = [{ hookId: hookName }];
806
+ return {
807
+ agentHooksConfig,
808
+ hookResource: {
809
+ kind: "hook",
810
+ name: hookName,
811
+ description: `Inline hooks for agent ${agentName}`,
812
+ config: {
813
+ events,
814
+ bundle,
815
+ sha256: sha256Hex(bundle)
816
+ }
817
+ }
818
+ };
819
+ }
820
+ function stripInlineHookKeys(hooks, agentName) {
821
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
822
+ throw new Error(`Agent "${agentName}" config.hooks must be an object`);
823
+ }
824
+ const entries = Object.entries(hooks);
825
+ for (const [key] of entries) {
826
+ if (key === "webhooks" || key in INLINE_AGENT_HOOK_EVENTS)
827
+ continue;
828
+ throw new Error(`Agent "${agentName}" config.hooks.${key} is not supported. Use inline hook callbacks or hooks.webhooks.`);
829
+ }
830
+ const result = Object.fromEntries(entries.filter(([key]) => key === "webhooks"));
831
+ return result;
832
+ }
833
+ function toHookFunctionExpression(source) {
834
+ const trimmed = source.trim();
835
+ if (/^(async\s+)?function\b/.test(trimmed) || /^(async\s*)?\(/.test(trimmed) || /^(async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed)) {
836
+ return trimmed;
837
+ }
838
+ if (/^(async\s+)?[A-Za-z_$][\w$]*\s*\(/.test(trimmed)) {
839
+ return trimmed.replace(/^(async\s+)?/, "$1function ");
840
+ }
841
+ return trimmed;
842
+ }
843
+ function transpileInlineHookBundle(agentName, entries) {
844
+ const moduleSource = `export default {
845
+ ${entries.map((entry) => ` ${JSON.stringify(entry.event)}: ${entry.source},`).join(`
846
+ `)}
847
+ };
848
+ `;
849
+ try {
850
+ return new Bun.Transpiler({ loader: "ts" }).transformSync(moduleSource);
851
+ } catch (error) {
852
+ const message = error instanceof Error ? error.message : String(error);
853
+ throw new Error(`Agent "${agentName}" inline hooks failed to transpile: ${message}`);
854
+ }
855
+ }
856
+ function normalizeAgentPolicyConfig(config, agentName) {
857
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
858
+ throw new Error(`Agent "${agentName}" config.policy must be an object`);
859
+ }
860
+ const input = config;
861
+ for (const key of Object.keys(input)) {
862
+ if (key !== "enabled" && key !== "policyIds" && key !== "policies" && key !== "mode") {
863
+ throw new Error(`Agent "${agentName}" config.policy.${key} is not supported`);
864
+ }
865
+ }
866
+ const policyIds = input.policyIds;
867
+ const policies = input.policies;
868
+ if (policyIds !== undefined && policies !== undefined) {
869
+ throw new Error(`Agent "${agentName}" config.policy cannot set both policyIds and policies`);
870
+ }
871
+ const refs = policies ?? policyIds;
872
+ if (refs !== undefined && !Array.isArray(refs)) {
873
+ throw new Error(`Agent "${agentName}" config.policy.policies must be an array`);
874
+ }
875
+ const normalized = {
876
+ ...input,
877
+ ...Array.isArray(refs) ? {
878
+ policyIds: refs.map((entry) => {
879
+ if (isResource(entry) && entry.kind === "policy")
880
+ return entry.name;
881
+ if (typeof entry === "string")
882
+ return entry;
883
+ throw new Error(`Agent "${agentName}" config.policy.policies must contain definePolicy(...) resources or strings`);
884
+ })
885
+ } : {}
886
+ };
887
+ delete normalized.policies;
888
+ delete normalized.enabled;
889
+ if (Array.isArray(normalized.policyIds) && normalized.policyIds.length === 0) {
890
+ delete normalized.policyIds;
891
+ }
892
+ return Array.isArray(normalized.policyIds) && normalized.policyIds.length > 0 ? normalized : undefined;
893
+ }
567
894
  async function normalizeSkillConfig(config, projectRoot) {
568
895
  const skillRoot = resolveContainedResourcePath(projectRoot, config.path, "Skill");
569
896
  const manifestPath = relative(projectRoot, skillRoot).split("\\").join("/");
@@ -605,6 +932,7 @@ async function normalizeToolConfig(config, projectRoot) {
605
932
  path: manifestPath,
606
933
  description: config.description,
607
934
  inputSchema: config.inputSchema,
935
+ ...config.runtime !== undefined ? { runtime: config.runtime } : {},
608
936
  ...config.defaultConfig !== undefined ? { defaultConfig: config.defaultConfig } : {},
609
937
  bundle,
610
938
  sha256: sha256Hex(bundle)
@@ -743,7 +1071,8 @@ function idsForManifest(ids, manifest) {
743
1071
  sandboxes: namesForKind(manifest, "sandbox"),
744
1072
  crons: namesForKind(manifest, "cron"),
745
1073
  skills: namesForKind(manifest, "skill"),
746
- tools: namesForKind(manifest, "tool")
1074
+ tools: namesForKind(manifest, "tool"),
1075
+ policies: namesForKind(manifest, "policy")
747
1076
  };
748
1077
  return {
749
1078
  agents: pickIds(ids.agents, namesByKind.agents),
@@ -751,14 +1080,16 @@ function idsForManifest(ids, manifest) {
751
1080
  sandboxes: pickIds(ids.sandboxes, namesByKind.sandboxes),
752
1081
  crons: pickIds(ids.crons, namesByKind.crons),
753
1082
  skills: pickIds(ids.skills, namesByKind.skills),
754
- tools: pickIds(ids.tools, namesByKind.tools)
1083
+ tools: pickIds(ids.tools, namesByKind.tools),
1084
+ hooks: {},
1085
+ policies: pickIds(ids.policies, namesByKind.policies)
755
1086
  };
756
1087
  }
757
1088
  function namesForKind(manifest, kind) {
758
1089
  return new Set(manifest.resources.filter((resource) => resource.kind === kind).map((resource) => resource.name));
759
1090
  }
760
1091
  function pickIds(values, names) {
761
- return Object.fromEntries(Object.entries(values).filter(([name]) => names.has(name)));
1092
+ return Object.fromEntries(Object.entries(values ?? {}).filter(([name]) => names.has(name)));
762
1093
  }
763
1094
  function idsFile(ids) {
764
1095
  return `// Generated by broods. Do not edit by hand.
@@ -769,6 +1100,7 @@ function idsFile(ids) {
769
1100
  ` + `export const crons = ids.crons;
770
1101
  ` + `export const skills = ids.skills;
771
1102
  ` + `export const tools = ids.tools;
1103
+ ` + `export const policies = ids.policies;
772
1104
  `;
773
1105
  }
774
1106
  function resourcesFile(manifest) {
@@ -803,6 +1135,7 @@ ${channelEntries}
803
1135
  ` + ` crons: ${aliasedIdMap("crons", ids.crons, aliases.cron)},
804
1136
  ` + ` skills: ${aliasedIdMap("skills", ids.skills, aliases.skill)},
805
1137
  ` + ` tools: ${aliasedIdMap("tools", ids.tools, aliases.tool)},
1138
+ ` + ` policies: ${aliasedIdMap("policies", ids.policies, aliases.policy)},
806
1139
  ` + `} as const satisfies ResourceApi;
807
1140
  `;
808
1141
  }
@@ -827,11 +1160,11 @@ function dataModelFile() {
827
1160
 
828
1161
  // src/sync.ts
829
1162
  class BroodsSyncClient {
830
- dashboardUrl;
1163
+ baseUrl;
831
1164
  token;
832
1165
  fetchImpl;
833
1166
  constructor(options) {
834
- this.dashboardUrl = stripTrailingSlash(options.dashboardUrl);
1167
+ this.baseUrl = stripTrailingSlash(options.baseUrl);
835
1168
  this.token = options.token;
836
1169
  this.fetchImpl = options.fetch ?? fetch;
837
1170
  }
@@ -859,7 +1192,13 @@ class BroodsSyncClient {
859
1192
  const payload = await response.json();
860
1193
  if (!payload.apiKey)
861
1194
  throw new Error("Fetch runtime key failed: response omitted apiKey");
862
- return { apiKey: payload.apiKey, keyHint: payload.keyHint ?? "" };
1195
+ return {
1196
+ apiKey: payload.apiKey,
1197
+ keyHint: payload.keyHint ?? "",
1198
+ ...payload.endpointId ? { endpointId: payload.endpointId } : {},
1199
+ ...payload.projectSlug ? { projectSlug: payload.projectSlug } : {},
1200
+ ...payload.environmentSlug ? { environmentSlug: payload.environmentSlug } : {}
1201
+ };
863
1202
  }
864
1203
  async setEnv(project, environment, name, value) {
865
1204
  const response = await this.request(project, environment, `/env/${encodeURIComponent(name)}`, {
@@ -890,7 +1229,7 @@ class BroodsSyncClient {
890
1229
  await assertOk(response, "Remove environment variable failed");
891
1230
  }
892
1231
  async getOnboarding() {
893
- const response = await this.fetchImpl(`${this.dashboardUrl}/api/cli/onboarding`, {
1232
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
894
1233
  method: "GET",
895
1234
  headers: {
896
1235
  Authorization: `Bearer ${this.token}`
@@ -900,7 +1239,7 @@ class BroodsSyncClient {
900
1239
  return await response.json();
901
1240
  }
902
1241
  async selectOnboardingOrg(orgId) {
903
- const response = await this.fetchImpl(`${this.dashboardUrl}/api/cli/onboarding`, {
1242
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
904
1243
  method: "POST",
905
1244
  headers: {
906
1245
  Authorization: `Bearer ${this.token}`,
@@ -911,8 +1250,20 @@ class BroodsSyncClient {
911
1250
  await assertOk(response, "Select CLI org failed");
912
1251
  return await response.json();
913
1252
  }
1253
+ async createOnboardingOrg(name) {
1254
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
1255
+ method: "POST",
1256
+ headers: {
1257
+ Authorization: `Bearer ${this.token}`,
1258
+ "Content-Type": "application/json"
1259
+ },
1260
+ body: JSON.stringify({ createOrgName: name })
1261
+ });
1262
+ await assertOk(response, "Create CLI org failed");
1263
+ return await response.json();
1264
+ }
914
1265
  async request(project, environment, suffix, init) {
915
- const url = `${this.dashboardUrl}/api/cli/projects/${encodeURIComponent(project)}` + `/environments/${encodeURIComponent(environment)}${suffix}`;
1266
+ const url = `${this.baseUrl}/v1/account/projects/${encodeURIComponent(project)}` + `/environments/${encodeURIComponent(environment)}${suffix}`;
916
1267
  return await this.fetchImpl(url, {
917
1268
  ...init,
918
1269
  headers: {
@@ -970,7 +1321,7 @@ function diffManifests(local, remote) {
970
1321
  }
971
1322
  function snapshotResource(resource) {
972
1323
  const normalized = normalizeEnvRefs(resource);
973
- if (resource.kind !== "skill" && resource.kind !== "tool")
1324
+ if (resource.kind !== "skill" && resource.kind !== "tool" && resource.kind !== "hook")
974
1325
  return normalized;
975
1326
  return {
976
1327
  ...normalized,
@@ -983,7 +1334,7 @@ function renameSnapshot(resource) {
983
1334
  return rest;
984
1335
  }
985
1336
  function isRenamableKind(kind) {
986
- return kind === "agent" || kind === "workspace" || kind === "sandbox";
1337
+ return kind === "agent" || kind === "workspace" || kind === "sandbox" || kind === "policy";
987
1338
  }
988
1339
  function diffSortKey(entry) {
989
1340
  const rank = {
@@ -1093,7 +1444,7 @@ class BroodsClient {
1093
1444
  const name = refOrName;
1094
1445
  const id = agentId ?? "";
1095
1446
  if (!id)
1096
- throw new Error(`Agent ${name} is missing a generated id. Run broods deploy first.`);
1447
+ throw new Error(`Agent ${name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
1097
1448
  return {
1098
1449
  id,
1099
1450
  run: (input) => this.run({ ...input, agentName: name, agentId: id }),
@@ -1103,7 +1454,7 @@ class BroodsClient {
1103
1454
  }
1104
1455
  const ref = refOrName;
1105
1456
  if (!ref.id)
1106
- throw new Error(`Agent ${ref.name} is missing a generated id. Run broods deploy first.`);
1457
+ throw new Error(`Agent ${ref.name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
1107
1458
  return {
1108
1459
  id: ref.id,
1109
1460
  run: (input) => this.run(ref, input),
@@ -1154,7 +1505,8 @@ class BroodsClient {
1154
1505
  agentName: refOrInput.name
1155
1506
  } : refOrInput;
1156
1507
  const body = directRunBody(input, "async");
1157
- const response = await this.fetchJson(`${this.baseUrl}/async`, {
1508
+ const targetUrl = maybeInput ? this.scopedUrl(refOrInput, "/async") : `${this.baseUrl}/async`;
1509
+ const response = await this.fetchJson(targetUrl, {
1158
1510
  method: "POST",
1159
1511
  headers: this.apiKeyHeaders(),
1160
1512
  body: JSON.stringify(body)
@@ -1197,7 +1549,7 @@ class BroodsClient {
1197
1549
  throw new Error("Polling timeout");
1198
1550
  }
1199
1551
  async createCron(input) {
1200
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
1552
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
1201
1553
  method: "POST",
1202
1554
  headers: this.apiKeyHeaders(),
1203
1555
  body: JSON.stringify(resolveCronInput(input))
@@ -1207,7 +1559,7 @@ class BroodsClient {
1207
1559
  return await response.json();
1208
1560
  }
1209
1561
  async listCrons() {
1210
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
1562
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
1211
1563
  method: "GET",
1212
1564
  headers: this.apiKeyHeaders()
1213
1565
  });
@@ -1217,7 +1569,7 @@ class BroodsClient {
1217
1569
  return payload.crons;
1218
1570
  }
1219
1571
  async getCron(cronId) {
1220
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1572
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1221
1573
  method: "GET",
1222
1574
  headers: this.apiKeyHeaders()
1223
1575
  });
@@ -1232,7 +1584,7 @@ class BroodsClient {
1232
1584
  if (options.limit !== undefined)
1233
1585
  params.set("limit", String(options.limit));
1234
1586
  const suffix = params.size > 0 ? `?${params}` : "";
1235
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
1587
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
1236
1588
  method: "GET",
1237
1589
  headers: this.apiKeyHeaders()
1238
1590
  });
@@ -1242,7 +1594,7 @@ class BroodsClient {
1242
1594
  return payload.runs;
1243
1595
  }
1244
1596
  async updateCron(cronId, patch) {
1245
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1597
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1246
1598
  method: "PATCH",
1247
1599
  headers: this.apiKeyHeaders(),
1248
1600
  body: JSON.stringify(patch)
@@ -1252,7 +1604,7 @@ class BroodsClient {
1252
1604
  return await response.json();
1253
1605
  }
1254
1606
  async deleteCron(cronId) {
1255
- const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
1607
+ const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
1256
1608
  method: "DELETE",
1257
1609
  headers: this.apiKeyHeaders()
1258
1610
  });
@@ -1263,11 +1615,11 @@ class BroodsClient {
1263
1615
  const payload = await response.json();
1264
1616
  return payload.deleted;
1265
1617
  }
1266
- scopedUrl(ref) {
1618
+ scopedUrl(ref, suffix = "") {
1267
1619
  if (ref.projectSlug && ref.environmentSlug && ref.endpointId) {
1268
- return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}`;
1620
+ return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}${suffix}`;
1269
1621
  }
1270
- return this.baseUrl;
1622
+ return `${this.baseUrl}${suffix}`;
1271
1623
  }
1272
1624
  async openStream(body, targetUrl) {
1273
1625
  if (this.apiKey) {
@@ -1424,7 +1776,6 @@ function formatStreamError(error) {
1424
1776
  var DEFAULT_CONNECT_TIMEOUT_MS = 2000;
1425
1777
  var WS_CONNECTING = 0;
1426
1778
  var WS_OPEN = 1;
1427
- var LAMBDA_FUNCTION_URL_HOST_RE = /\.lambda-url\.[a-z0-9-]+\.on\.aws$/i;
1428
1779
 
1429
1780
  class BroodsWebSocketClient {
1430
1781
  baseUrl;
@@ -1433,7 +1784,7 @@ class BroodsWebSocketClient {
1433
1784
  connectTimeoutMs;
1434
1785
  constructor(options = {}) {
1435
1786
  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);
1787
+ this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
1437
1788
  this.apiKey = options.apiKey || process.env.BROODS_API_KEY || "";
1438
1789
  if (!this.apiKey)
1439
1790
  throw new Error("BroodsWebSocketClient requires apiKey or BROODS_API_KEY.");
@@ -1615,10 +1966,6 @@ function normalizeWebSocketServiceUrl(value) {
1615
1966
  return normalizeHttpServiceUrl(trimmed);
1616
1967
  }
1617
1968
  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
1969
  return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
1623
1970
  }
1624
1971
  function parseServerMessage(data) {
@@ -1690,6 +2037,7 @@ async function* subscribeObservabilityLogs(options, subscribeOptions = {}) {
1690
2037
  async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
1691
2038
  const { baseUrl, apiKey, project, environment } = options;
1692
2039
  const { backfill = 0, minLevel, signal } = subscribeOptions;
2040
+ const liveOnly = subscribeOptions.liveOnly ?? backfill <= 0;
1693
2041
  if (signal?.aborted)
1694
2042
  return;
1695
2043
  const url = buildObservabilityUrl(baseUrl, project, environment, apiKey);
@@ -1725,6 +2073,7 @@ async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
1725
2073
  type: "subscribe",
1726
2074
  stream: "logs",
1727
2075
  ...backfill > 0 ? { backfill } : {},
2076
+ ...liveOnly ? { liveOnly: true } : {},
1728
2077
  ...minLevel !== undefined ? { minLevel } : {}
1729
2078
  };
1730
2079
  socket.send(JSON.stringify(msg));
@@ -1814,13 +2163,19 @@ function optionValue(args, name) {
1814
2163
  function hasFlag(args, name) {
1815
2164
  return args.includes(name);
1816
2165
  }
1817
- async function requireAuth(dashboardUrl) {
2166
+ function isPlainObject(value) {
2167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2168
+ }
2169
+ async function requireAuth(baseUrl) {
1818
2170
  loadBroodsRuntimeConfig();
1819
2171
  const auth = await readStoredAuth();
1820
2172
  if (!auth) {
1821
- throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_DASHBOARD_URL.");
2173
+ throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_BASE_URL.");
1822
2174
  }
1823
- return dashboardUrl ? { ...auth, dashboardUrl } : auth;
2175
+ return {
2176
+ ...auth,
2177
+ ...baseUrl ? { baseUrl: stripTrailingSlash(baseUrl) } : {}
2178
+ };
1824
2179
  }
1825
2180
  async function promptSecret(label) {
1826
2181
  const rl = createInterface({ input, output });
@@ -1893,17 +2248,18 @@ async function loginWithBrowser(dashboardUrl) {
1893
2248
  await assertCliAuthRouteExists(startUrl);
1894
2249
  openBrowser(startUrl);
1895
2250
  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`, {
2251
+ const login = await waitWithTimeout(code.promise, LOGIN_TIMEOUT_MS);
2252
+ const response = await fetch(`${login.baseUrl}/v1/account/auth/exchange`, {
1898
2253
  method: "POST",
1899
2254
  headers: { "Content-Type": "application/json" },
1900
- body: JSON.stringify({ code: loginCode })
2255
+ body: JSON.stringify({ code: login.code })
1901
2256
  });
1902
2257
  if (!response.ok) {
1903
2258
  throw new Error(`Login exchange failed: ${response.status} ${await response.text()}`);
1904
2259
  }
1905
2260
  const payload = await response.json();
1906
2261
  const auth = {
2262
+ baseUrl: login.baseUrl,
1907
2263
  dashboardUrl: stripTrailingSlash(dashboardUrl),
1908
2264
  token: payload.token,
1909
2265
  createdAt: new Date().toISOString(),
@@ -1937,12 +2293,21 @@ function waitForCallback(expectedState) {
1937
2293
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1938
2294
  const state = url.searchParams.get("state");
1939
2295
  const code = url.searchParams.get("code");
2296
+ const baseUrl = url.searchParams.get("base_url");
1940
2297
  if (state !== expectedState || !code) {
1941
2298
  res.writeHead(400).end("Invalid broods login callback.");
1942
2299
  return;
1943
2300
  }
2301
+ if (!baseUrl) {
2302
+ res.writeHead(400).end("Login callback omitted base_url; update the dashboard deployment.");
2303
+ 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."));
2304
+ return;
2305
+ }
1944
2306
  res.writeHead(200, { "Content-Type": "text/plain" }).end("broods CLI login complete. You can close this tab.");
1945
- callbackResolve(code);
2307
+ callbackResolve({
2308
+ code,
2309
+ baseUrl: stripTrailingSlash(baseUrl)
2310
+ });
1946
2311
  } catch (error) {
1947
2312
  callbackReject(error);
1948
2313
  }
@@ -2230,7 +2595,7 @@ function renderStreamPart(part, state) {
2230
2595
  // package.json
2231
2596
  var package_default = {
2232
2597
  name: "broods",
2233
- version: "0.1.1",
2598
+ version: "0.2.1",
2234
2599
  description: "CLI + TypeScript SDK for the broods agent platform.",
2235
2600
  type: "module",
2236
2601
  license: "MIT",
@@ -2272,12 +2637,20 @@ var package_default = {
2272
2637
  "pack:check": "bun run build && npm pack --dry-run",
2273
2638
  test: "bun test"
2274
2639
  },
2640
+ engines: {
2641
+ node: ">=22"
2642
+ },
2275
2643
  dependencies: {
2276
- ai: "^6.0.195"
2644
+ "@chat-adapter/discord": "^4.31.0",
2645
+ "@chat-adapter/github": "^4.31.0",
2646
+ "@chat-adapter/slack": "^4.31.0",
2647
+ "@chat-adapter/telegram": "^4.31.0",
2648
+ ai: "^7.0.11"
2277
2649
  },
2278
2650
  devDependencies: {
2279
2651
  "@types/bun": "^1.3.14",
2280
2652
  rollup: "^4.53.3",
2653
+ typescript: "^6.0.3",
2281
2654
  "rollup-plugin-dts": "^6.3.0"
2282
2655
  }
2283
2656
  };
@@ -2285,6 +2658,12 @@ var package_default = {
2285
2658
  // src/cli/index.ts
2286
2659
  var VERSION = package_default.version;
2287
2660
  var DEFAULT_DASHBOARD_URL = "https://dashboard.broods.app";
2661
+ var DEFAULT_SERVICE_REGION = "eu-west-1";
2662
+ var SERVICE_REGIONS = [
2663
+ { region: "eu-west-1", label: "eu-west-1 (Ireland)" },
2664
+ { region: "us-east-1", label: "us-east-1 (US East)" },
2665
+ { region: "ap-southeast-1", label: "ap-southeast-1 (Singapore)" }
2666
+ ];
2288
2667
  var HELP = `broods v${VERSION}
2289
2668
 
2290
2669
  Usage: broods <command>
@@ -2310,13 +2689,15 @@ Commands:
2310
2689
  run <agent> <prompt> Run an agent once and pretty-stream the result (thinking, tool calls, text)
2311
2690
 
2312
2691
  Options:
2313
- --dashboard-url <url> Dashboard base URL (default: ${DEFAULT_DASHBOARD_URL})
2692
+ --dashboard-url <url> Dashboard base URL for login and deep links (default: ${DEFAULT_DASHBOARD_URL})
2693
+ --base-url <url> broods API base URL for sync/env calls (default: discovered at login)
2314
2694
  --project <name> Project name override (default: package name or folder)
2315
2695
  --env <name> Target environment override
2696
+ --region <region> Broods service region preference (default: ${DEFAULT_SERVICE_REGION})
2316
2697
  --prune Allow deploy to delete undeclared remote resources
2317
2698
  --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\`)
2699
+ --errors Show WARN/ERROR only (default for \`dev\`, \`stream\`, and \`logs\`)
2700
+ --level <lvl> Minimum log level INFO|WARN|ERROR (default WARN)
2320
2701
  -n, --limit <n> Backfill line count (with \`logs\`, default 100)
2321
2702
  --json Print logs as raw JSON (with \`logs\`, applies to backfill output)
2322
2703
  --force Allow init to overwrite starter files`;
@@ -2380,6 +2761,7 @@ async function init(args) {
2380
2761
  dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
2381
2762
  project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
2382
2763
  environment: optionValue(args, "--env") ?? "development",
2764
+ region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
2383
2765
  force
2384
2766
  });
2385
2767
  console.log(`Created ${PROJECT_DIR}/`);
@@ -2395,9 +2777,10 @@ async function login(args) {
2395
2777
  const project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2396
2778
  const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2397
2779
  await writeLocalEnvDefaults({
2398
- dashboardUrl: auth.dashboardUrl,
2780
+ dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
2399
2781
  project,
2400
2782
  environment,
2783
+ region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
2401
2784
  force: false
2402
2785
  });
2403
2786
  const user = auth.user?.email || auth.user?.name || auth.user?.authId;
@@ -2410,11 +2793,11 @@ async function login(args) {
2410
2793
  console.log(`Org: ${org}`);
2411
2794
  if (account)
2412
2795
  console.log(`Account: ${account}`);
2413
- await writeRuntimeKeyForLogin(auth.dashboardUrl, auth.token, project, environment);
2796
+ await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
2414
2797
  }
2415
- async function writeRuntimeKeyForLogin(dashboardUrl, token, project, environment) {
2798
+ async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
2416
2799
  try {
2417
- const client = new BroodsSyncClient({ dashboardUrl, token });
2800
+ const client = new BroodsSyncClient({ baseUrl, token });
2418
2801
  const key = await client.getRuntimeKey(project, environment);
2419
2802
  if (key?.apiKey) {
2420
2803
  await writeEnvValue("BROODS_API_KEY", key.apiKey);
@@ -2428,8 +2811,8 @@ async function diff(args) {
2428
2811
  environment: optionValue(args, "--env"),
2429
2812
  command: "dev"
2430
2813
  });
2431
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2432
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2814
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2815
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2433
2816
  const remote = await client.getManifest(manifest.project, manifest.environment);
2434
2817
  printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
2435
2818
  }
@@ -2440,8 +2823,8 @@ async function deploy(args) {
2440
2823
  command: "deploy",
2441
2824
  useRuntimeEnvironment: false
2442
2825
  });
2443
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2444
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2826
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2827
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2445
2828
  const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
2446
2829
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2447
2830
  await ensureGitIgnore();
@@ -2461,11 +2844,15 @@ async function applyDeploymentKey(deployment) {
2461
2844
  }
2462
2845
  function printSyncWarnings(result) {
2463
2846
  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}`);
2847
+ if (missing.length > 0) {
2848
+ printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
2849
+ for (const name of missing)
2850
+ console.log(` broods env set ${name}`);
2851
+ }
2852
+ const missingPolicies = result.warnings?.missingPolicies ?? [];
2853
+ if (missingPolicies.length > 0) {
2854
+ 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(", ")}`);
2855
+ }
2469
2856
  }
2470
2857
  async function dev(args) {
2471
2858
  await ensureDevOnboarding(args);
@@ -2482,7 +2869,8 @@ async function dev(args) {
2482
2869
  const childEnv = {
2483
2870
  ...process.env,
2484
2871
  BROODS_DECLINED_FILE: declinedFile,
2485
- BROODS_SUPPRESS_DEV_TARGET: "1"
2872
+ BROODS_SUPPRESS_DEV_TARGET: "1",
2873
+ BROODS_RELOAD_ENV: "1"
2486
2874
  };
2487
2875
  await printDevTarget(args);
2488
2876
  await runSyncChild(args, childEnv);
@@ -2528,7 +2916,7 @@ async function streamDevLogs(args, signal) {
2528
2916
  try {
2529
2917
  creds = resolveObservabilityCredentials();
2530
2918
  } catch {
2531
- console.log("\xB7 live logs off \u2014 set BROODS_API_KEY (run `deploy` first) to stream agent logs here");
2919
+ 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
2920
  return;
2533
2921
  }
2534
2922
  let project;
@@ -2573,27 +2961,25 @@ async function ensureLocalDevDefaults(args) {
2573
2961
  const missing = [
2574
2962
  "BROODS_DASHBOARD_URL",
2575
2963
  "BROODS_PROJECT",
2576
- "BROODS_ENVIRONMENT"
2964
+ "BROODS_ENVIRONMENT",
2965
+ "BROODS_REGION"
2577
2966
  ].filter((key) => values[key] === undefined);
2578
2967
  if (missing.length === 0)
2579
2968
  return;
2580
2969
  const needsProject = values.BROODS_PROJECT === undefined;
2581
2970
  const needsEnvironment = values.BROODS_ENVIRONMENT === undefined;
2971
+ const needsRegion = values.BROODS_REGION === undefined;
2582
2972
  const runtime = loadBroodsRuntimeConfig();
2583
2973
  const dashboardUrl = optionValue(args, "--dashboard-url") ?? runtime.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
2584
2974
  let project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
2585
2975
  let environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
2976
+ let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
2586
2977
  if (process.stdin.isTTY && needsProject) {
2587
2978
  const auth = await requireAuthOrLogin(dashboardUrl);
2588
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
2979
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2589
2980
  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));
2981
+ const selectedContext = await selectOnboardingOrg(client, context);
2982
+ project = await selectOnboardingProject(selectedContext, project);
2597
2983
  if (!project.trim())
2598
2984
  throw new Error("Project name is required.");
2599
2985
  }
@@ -2602,16 +2988,20 @@ async function ensureLocalDevDefaults(args) {
2602
2988
  if (!environment.trim())
2603
2989
  throw new Error("Environment is required.");
2604
2990
  }
2991
+ if (process.stdin.isTTY && needsRegion) {
2992
+ region = await promptSelect("Select service region", [...SERVICE_REGIONS], (entry) => entry.label).then((entry) => entry.region);
2993
+ }
2605
2994
  await writeLocalEnvDefaults({
2606
2995
  dashboardUrl,
2607
2996
  project,
2608
2997
  environment,
2998
+ region,
2609
2999
  force: false
2610
3000
  });
2611
3001
  }
2612
3002
  async function requireAuthOrLogin(dashboardUrl) {
2613
3003
  try {
2614
- return await requireAuth(dashboardUrl);
3004
+ return await requireAuth();
2615
3005
  } catch (error) {
2616
3006
  if (!process.stdin.isTTY)
2617
3007
  throw error;
@@ -2643,10 +3033,37 @@ function formatOrgChoice(org) {
2643
3033
  const suffix = org.role === "owner" || org.role === "admin" ? org.role : "member";
2644
3034
  return `${org.name} (${org.slug}, ${suffix})`;
2645
3035
  }
3036
+ async function selectOnboardingOrg(client, context) {
3037
+ const activeOrgs = context.orgs.filter((org) => org.accountStatus === "active");
3038
+ const createNew = { kind: "create", name: "New organization" };
3039
+ const selected = await promptSelect("Select organization", [...activeOrgs, createNew], (entry) => ("kind" in entry) ? "Create new organization" : formatOrgChoice(entry));
3040
+ if ("kind" in selected) {
3041
+ const name = await promptText("Organization name", inferProjectName(process.cwd()));
3042
+ if (!name.trim())
3043
+ throw new Error("Organization name is required.");
3044
+ return await client.createOnboardingOrg(name);
3045
+ }
3046
+ return selected.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selected.id);
3047
+ }
2646
3048
  function defaultProjectName(context, inferred) {
2647
3049
  const exact = context.projects.find((project) => project.name === inferred || project.slug === inferred);
2648
3050
  return exact?.name ?? inferred;
2649
3051
  }
3052
+ async function selectOnboardingProject(context, inferred) {
3053
+ if (context.projects.length === 0) {
3054
+ return promptText("Project name", inferred);
3055
+ }
3056
+ const createNew = { kind: "create", name: defaultProjectName(context, inferred), slug: "" };
3057
+ const choices = [
3058
+ ...context.projects,
3059
+ createNew
3060
+ ];
3061
+ const selected = await promptSelect("Select project", choices, (entry) => ("kind" in entry) && entry.kind === "create" ? `Create new project (${entry.name})` : `${entry.name} (${entry.slug})`);
3062
+ if ("kind" in selected && selected.kind === "create") {
3063
+ return promptText("New project name", selected.name);
3064
+ }
3065
+ return selected.name;
3066
+ }
2650
3067
  function runSyncChild(args, env3) {
2651
3068
  return new Promise((resolvePromise, reject) => {
2652
3069
  const entryPoint = process.argv[1] ?? "";
@@ -2670,12 +3087,12 @@ async function syncDev(args) {
2670
3087
  environment: optionValue(args, "--env"),
2671
3088
  command: "dev"
2672
3089
  });
2673
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2674
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
3090
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3091
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
2675
3092
  const remote = await client.getManifest(manifest.project, manifest.environment);
2676
3093
  const diff2 = diffManifests(manifest, remote?.manifest ?? null);
2677
3094
  printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
2678
- await syncLocalEnvVars(client, manifest);
3095
+ await syncLocalEnvVars(client, manifest, envSyncScopeKey(auth.baseUrl, auth.token));
2679
3096
  let result = await client.putManifest(manifest, false);
2680
3097
  await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
2681
3098
  await ensureGitIgnore();
@@ -2725,7 +3142,7 @@ function printChannelEndpoints(channels, result) {
2725
3142
  console.log(`Channel ${channel.alias} (${channel.type}): ${url}${channel.type === "pancake" ? "?secret=<PANCAKE_WEBHOOK_SECRET>" : ""}`);
2726
3143
  }
2727
3144
  }
2728
- async function syncLocalEnvVars(client, manifest) {
3145
+ async function syncLocalEnvVars(client, manifest, scopeKey) {
2729
3146
  const present = collectEnvRefNames(manifest).filter((name) => {
2730
3147
  const value = process.env[name];
2731
3148
  return !name.startsWith("BROODS_") && value !== undefined && value !== "";
@@ -2733,19 +3150,23 @@ async function syncLocalEnvVars(client, manifest) {
2733
3150
  if (present.length === 0)
2734
3151
  return;
2735
3152
  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]));
3153
+ const known = cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] ?? {};
3154
+ const remoteNames = new Set((await client.listEnv(manifest.project, manifest.environment)).map((entry) => entry.name));
3155
+ const changed = present.filter((name) => !remoteNames.has(name) || known[name] !== hashEnvValue(process.env[name]));
2738
3156
  if (changed.length === 0)
2739
3157
  return;
2740
3158
  await Promise.all(changed.map((name) => client.setEnv(manifest.project, manifest.environment, name, process.env[name])));
2741
3159
  for (const name of changed)
2742
3160
  known[name] = hashEnvValue(process.env[name]);
2743
- cache[envCacheKey(manifest.project, manifest.environment)] = known;
3161
+ cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] = known;
2744
3162
  await saveEnvSyncCache(cache);
2745
3163
  printEnvSync(changed);
2746
3164
  }
2747
- function envCacheKey(project, environment) {
2748
- return `${project}:${environment}`;
3165
+ function envSyncScopeKey(baseUrl, token) {
3166
+ return `${baseUrl}:${createHash2("sha256").update(token).digest("hex").slice(0, 16)}`;
3167
+ }
3168
+ function envCacheKey(scopeKey, project, environment) {
3169
+ return `${scopeKey}:${project}:${environment}`;
2749
3170
  }
2750
3171
  function envSyncCachePath() {
2751
3172
  return resolve4(dirname2(USER_CONFIG_PATH), "env-sync.json");
@@ -2770,15 +3191,15 @@ async function saveEnvSyncCache(cache) {
2770
3191
  await writeFile3(path, `${JSON.stringify(cache, null, 2)}
2771
3192
  `, "utf8");
2772
3193
  }
2773
- async function rememberEnvSyncValue(project, environment, name, value) {
3194
+ async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
2774
3195
  const cache = await loadEnvSyncCache();
2775
- const key = envCacheKey(project, environment);
3196
+ const key = envCacheKey(scopeKey, project, environment);
2776
3197
  cache[key] = { ...cache[key] ?? {}, [name]: hashEnvValue(value) };
2777
3198
  await saveEnvSyncCache(cache);
2778
3199
  }
2779
- async function forgetEnvSyncValue(project, environment, name) {
3200
+ async function forgetEnvSyncValue(scopeKey, project, environment, name) {
2780
3201
  const cache = await loadEnvSyncCache();
2781
- const key = envCacheKey(project, environment);
3202
+ const key = envCacheKey(scopeKey, project, environment);
2782
3203
  if (!cache[key] || !(name in cache[key]))
2783
3204
  return;
2784
3205
  delete cache[key][name];
@@ -2790,11 +3211,11 @@ async function printDevTarget(args) {
2790
3211
  environment: optionValue(args, "--env"),
2791
3212
  command: "dev"
2792
3213
  });
2793
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3214
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
2794
3215
  printDeploymentTarget({
2795
3216
  project: manifest.project,
2796
3217
  environment: manifest.environment,
2797
- dashboardUrl: auth.dashboardUrl
3218
+ dashboardUrl: auth.dashboardUrl ?? DEFAULT_DASHBOARD_URL
2798
3219
  });
2799
3220
  }
2800
3221
  async function loadDeclinedDeletes() {
@@ -2833,8 +3254,9 @@ async function envCommand(args) {
2833
3254
  environment: optionValue(args, "--env"),
2834
3255
  command: "dev"
2835
3256
  });
2836
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
2837
- const client = new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token });
3257
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3258
+ const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
3259
+ const scopeKey = envSyncScopeKey(auth.baseUrl, auth.token);
2838
3260
  const target = `${manifest.project}/${manifest.environment}`;
2839
3261
  if (isList) {
2840
3262
  const variables = await client.listEnv(manifest.project, manifest.environment);
@@ -2859,13 +3281,13 @@ async function envCommand(args) {
2859
3281
  }
2860
3282
  if (isRemove) {
2861
3283
  await client.removeEnv(manifest.project, manifest.environment, name);
2862
- await forgetEnvSyncValue(manifest.project, manifest.environment, name);
3284
+ await forgetEnvSyncValue(scopeKey, manifest.project, manifest.environment, name);
2863
3285
  console.log(`Removed ${name} from ${target}`);
2864
3286
  return;
2865
3287
  }
2866
3288
  const value = await promptSecret(name);
2867
3289
  await client.setEnv(manifest.project, manifest.environment, name, value);
2868
- await rememberEnvSyncValue(manifest.project, manifest.environment, name, value);
3290
+ await rememberEnvSyncValue(scopeKey, manifest.project, manifest.environment, name, value);
2869
3291
  console.log(`Stored ${name} for ${target}`);
2870
3292
  }
2871
3293
  function resolveObservabilityCredentials() {
@@ -2882,7 +3304,7 @@ function resolveMinLevel(args) {
2882
3304
  return "WARN";
2883
3305
  const raw = optionValue(args, "--level");
2884
3306
  if (!raw)
2885
- return;
3307
+ return "WARN";
2886
3308
  const upper = raw.toUpperCase();
2887
3309
  if (upper === "WARN" || upper === "WARNING")
2888
3310
  return "WARN";
@@ -2975,8 +3397,8 @@ async function loadAgentsWithIds(args) {
2975
3397
  environment: optionValue(args, "--env"),
2976
3398
  command: "dev"
2977
3399
  });
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);
3400
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3401
+ const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
2980
3402
  const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
2981
3403
  name: resource.name,
2982
3404
  config: resource.config ?? {},
@@ -3008,10 +3430,10 @@ async function agentGet(name, args) {
3008
3430
  const config = agent.config;
3009
3431
  const sandbox = typeof config.sandbox === "string" ? config.sandbox : undefined;
3010
3432
  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) : [];
3433
+ const tools = isPlainObject(config.tools) ? Object.keys(config.tools) : [];
3434
+ const channels = isPlainObject(config.channels) ? Object.keys(config.channels) : [];
3435
+ const subagents = isPlainObject(config.subagent) && Array.isArray(config.subagent.allowed) ? config.subagent.allowed.map((entry) => typeof entry === "string" ? entry : entry.name).filter(Boolean) : [];
3436
+ const webhooks = isPlainObject(config.hooks) && Array.isArray(config.hooks.webhooks) ? config.hooks.webhooks.filter(isPlainObject) : [];
3015
3437
  console.log(`Agent: ${agent.name}`);
3016
3438
  console.log(` Project/Env: ${manifest.project}/${manifest.environment}`);
3017
3439
  console.log(` Deployed id: ${agent.agentId ?? "not deployed"}`);
@@ -3032,16 +3454,13 @@ async function agentGet(name, args) {
3032
3454
  }
3033
3455
  }
3034
3456
  function agentModelLabel(config) {
3035
- const model = isRecord(config.model) ? config.model : {};
3457
+ const model = isPlainObject(config.model) ? config.model : {};
3036
3458
  const provider = typeof model.provider === "string" ? model.provider : undefined;
3037
3459
  const modelId = typeof model.modelId === "string" ? model.modelId : undefined;
3038
3460
  if (provider && modelId)
3039
3461
  return `${provider}/${modelId}`;
3040
3462
  return modelId ?? provider ?? "unconfigured model";
3041
3463
  }
3042
- function isRecord(value) {
3043
- return typeof value === "object" && value !== null && !Array.isArray(value);
3044
- }
3045
3464
  async function run(args) {
3046
3465
  const [agentName, ...promptParts] = args.filter((arg) => !arg.startsWith("--"));
3047
3466
  if (!agentName || promptParts.length === 0) {
@@ -3052,18 +3471,22 @@ async function run(args) {
3052
3471
  environment: optionValue(args, "--env"),
3053
3472
  command: "dev"
3054
3473
  });
3055
- const auth = await requireAuth(optionValue(args, "--dashboard-url") ?? config.dashboardUrl);
3474
+ const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
3056
3475
  const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
3057
3476
  if (!agent)
3058
3477
  throw new Error(`Unknown local agent: ${agentName}`);
3059
- const remote = await new BroodsSyncClient({ dashboardUrl: auth.dashboardUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3478
+ const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
3060
3479
  const agentId = remote?.ids.agents[agentName];
3061
3480
  if (!agentId)
3062
- throw new Error(`Agent ${agentName} is not deployed. Run broods deploy first.`);
3481
+ throw new Error(`Agent ${agentName} is not synced yet. Run broods dev --once or broods deploy first.`);
3482
+ const runtimeKey = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
3483
+ if (runtimeKey?.apiKey) {
3484
+ await writeEnvValue("BROODS_API_KEY", runtimeKey.apiKey);
3485
+ }
3063
3486
  if (agent.config.publicAccess !== true) {
3064
3487
  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
3488
  }
3066
- const client = new BroodsClient;
3489
+ const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
3067
3490
  const state = createRenderState();
3068
3491
  try {
3069
3492
  for await (const part of client.stream({
@@ -3071,7 +3494,10 @@ async function run(args) {
3071
3494
  name: agentName,
3072
3495
  id: agentId,
3073
3496
  project: manifest.project,
3074
- environment: manifest.environment
3497
+ environment: manifest.environment,
3498
+ ...runtimeKey?.endpointId ? { endpointId: runtimeKey.endpointId } : {},
3499
+ ...runtimeKey?.projectSlug ? { projectSlug: runtimeKey.projectSlug } : {},
3500
+ ...runtimeKey?.environmentSlug ? { environmentSlug: runtimeKey.environmentSlug } : {}
3075
3501
  }, { input: promptParts.join(" ") })) {
3076
3502
  renderStreamPart(part, state);
3077
3503
  }
@@ -3116,7 +3542,8 @@ async function writeLocalEnvDefaults(options) {
3116
3542
  const nextValues = {
3117
3543
  BROODS_DASHBOARD_URL: options.dashboardUrl,
3118
3544
  BROODS_PROJECT: options.project,
3119
- BROODS_ENVIRONMENT: options.environment
3545
+ BROODS_ENVIRONMENT: options.environment,
3546
+ BROODS_REGION: options.region
3120
3547
  };
3121
3548
  const lines = current ? current.replace(/\n?$/, `
3122
3549
  `).split(/\n/) : [
@@ -3209,7 +3636,7 @@ async function collectSourceFiles(dir, files) {
3209
3636
  await collectSourceFiles(full, files);
3210
3637
  continue;
3211
3638
  }
3212
- if (entry.isFile() && entry.name.endsWith(".ts"))
3639
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".md")))
3213
3640
  files.push(full);
3214
3641
  }
3215
3642
  }