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 +579 -152
- package/dist/index.d.ts +464 -89
- package/dist/index.js +54 -38
- package/package.json +10 -2
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
|
|
29
|
-
if (envToken &&
|
|
28
|
+
const envConvexUrl = process.env.BROODS_BASE_URL;
|
|
29
|
+
if (envToken && envConvexUrl) {
|
|
30
30
|
return {
|
|
31
|
-
|
|
31
|
+
baseUrl: stripTrailingSlash(envConvexUrl),
|
|
32
32
|
token: envToken,
|
|
33
33
|
createdAt: new Date().toISOString()
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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.
|
|
124
|
+
if (typeof value.baseUrl !== "string" || typeof value.token !== "string")
|
|
113
125
|
return null;
|
|
114
126
|
return {
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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"
|
|
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({
|
|
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
|
|
518
|
-
|
|
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
|
-
|
|
1163
|
+
baseUrl;
|
|
831
1164
|
token;
|
|
832
1165
|
fetchImpl;
|
|
833
1166
|
constructor(options) {
|
|
834
|
-
this.
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
2173
|
+
throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_BASE_URL.");
|
|
1822
2174
|
}
|
|
1823
|
-
return
|
|
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
|
|
1897
|
-
const response = await fetch(`${
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
2319
|
-
--level <lvl> Minimum log level INFO|WARN|ERROR (
|
|
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.
|
|
2796
|
+
await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
|
|
2414
2797
|
}
|
|
2415
|
-
async function writeRuntimeKeyForLogin(
|
|
2798
|
+
async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
|
|
2416
2799
|
try {
|
|
2417
|
-
const client = new BroodsSyncClient({
|
|
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, "--
|
|
2432
|
-
const client = new BroodsSyncClient({
|
|
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, "--
|
|
2444
|
-
const client = new BroodsSyncClient({
|
|
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
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
|
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({
|
|
2979
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2589
2980
|
const context = await getOnboardingContextOrFallback(client, auth);
|
|
2590
|
-
const
|
|
2591
|
-
|
|
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(
|
|
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, "--
|
|
2674
|
-
const client = new BroodsSyncClient({
|
|
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
|
|
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
|
|
2748
|
-
return `${
|
|
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, "--
|
|
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, "--
|
|
2837
|
-
const client = new BroodsSyncClient({
|
|
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, "--
|
|
2979
|
-
const remote = await new BroodsSyncClient({
|
|
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 =
|
|
3012
|
-
const channels =
|
|
3013
|
-
const subagents =
|
|
3014
|
-
const webhooks =
|
|
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 =
|
|
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, "--
|
|
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({
|
|
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
|
|
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
|
}
|