broods 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -105
- package/README.md +5 -0
- package/dist/cli/index.js +583 -151
- package/dist/index.d.ts +417 -88
- package/dist/index.js +54 -38
- package/package.json +10 -3
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);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
81
|
+
const cached = envFileCache.get(path);
|
|
82
|
+
if (cached && cached.mtimeMs === mtimeMs)
|
|
74
83
|
continue;
|
|
84
|
+
envFileCache.set(path, { mtimeMs });
|
|
75
85
|
for (const [key, value] of Object.entries(parseEnv(readFileSync(path, "utf8")))) {
|
|
76
|
-
if (!
|
|
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,163 @@ async function normalizeConfig(resource, projectRoot) {
|
|
|
564
686
|
}
|
|
565
687
|
return rewriteValues(resource.config);
|
|
566
688
|
}
|
|
689
|
+
function normalizeAgentConfig(resource, _projectRoot) {
|
|
690
|
+
const config = { ...resource.config };
|
|
691
|
+
const inlineHooks = normalizeInlineAgentHooks(resource.name, config.hooks);
|
|
692
|
+
if (inlineHooks) {
|
|
693
|
+
config.hooks = inlineHooks.agentHooksConfig;
|
|
694
|
+
} else if (config.hooks !== undefined) {
|
|
695
|
+
config.hooks = stripInlineHookKeys(config.hooks, resource.name);
|
|
696
|
+
}
|
|
697
|
+
if (config.channels !== undefined) {
|
|
698
|
+
if (!Array.isArray(config.channels)) {
|
|
699
|
+
throw new Error(`Agent "${resource.name}" config.channels must be an array of channel definitions`);
|
|
700
|
+
}
|
|
701
|
+
config.channels = Object.fromEntries(config.channels.map((channel) => {
|
|
702
|
+
if (!isChannelDefinition(channel)) {
|
|
703
|
+
throw new Error(`Agent "${resource.name}" config.channels must contain channel definitions`);
|
|
704
|
+
}
|
|
705
|
+
const channelId = `${resource.name}${capitalize(channel.type)}Channel`;
|
|
706
|
+
return [channel.type, { id: channelId, ...channel.config }];
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
if (isResource(config.sandbox)) {
|
|
710
|
+
config.sandbox = config.sandbox.name;
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(config.workspaces)) {
|
|
713
|
+
config.workspaces = config.workspaces.map((workspace) => normalizeWorkspaceRef(workspace, resource.name));
|
|
714
|
+
}
|
|
715
|
+
if (config.policy !== undefined) {
|
|
716
|
+
const policy = normalizeAgentPolicyConfig(config.policy, resource.name);
|
|
717
|
+
if (policy)
|
|
718
|
+
config.policy = policy;
|
|
719
|
+
else
|
|
720
|
+
delete config.policy;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
config: rewriteValues(config),
|
|
724
|
+
...inlineHooks?.hookResource ? { hookResource: inlineHooks.hookResource } : {}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function normalizeInlineAgentHooks(agentName, hooks) {
|
|
728
|
+
if (hooks === undefined)
|
|
729
|
+
return;
|
|
730
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
731
|
+
throw new Error(`Agent "${agentName}" config.hooks must be an object`);
|
|
732
|
+
}
|
|
733
|
+
const hookConfig = hooks;
|
|
734
|
+
const entries = Object.keys(INLINE_AGENT_HOOK_EVENTS).flatMap((name) => {
|
|
735
|
+
const handler = hookConfig[name];
|
|
736
|
+
if (handler === undefined)
|
|
737
|
+
return [];
|
|
738
|
+
if (typeof handler !== "function") {
|
|
739
|
+
throw new Error(`Agent "${agentName}" config.hooks.${name} must be a function`);
|
|
740
|
+
}
|
|
741
|
+
return [{
|
|
742
|
+
name,
|
|
743
|
+
event: INLINE_AGENT_HOOK_EVENTS[name],
|
|
744
|
+
source: toHookFunctionExpression(handler.toString())
|
|
745
|
+
}];
|
|
746
|
+
});
|
|
747
|
+
if (entries.length === 0)
|
|
748
|
+
return;
|
|
749
|
+
const hookName = `${agentName}-hooks`;
|
|
750
|
+
const bundle = transpileInlineHookBundle(agentName, entries);
|
|
751
|
+
const bundleSize = Buffer.byteLength(bundle);
|
|
752
|
+
if (bundleSize > MAX_BUNDLE_FILE_BYTES) {
|
|
753
|
+
throw new Error(`Agent "${agentName}" hook bundle is too large (${bundleSize} bytes, max ${MAX_BUNDLE_FILE_BYTES})`);
|
|
754
|
+
}
|
|
755
|
+
const events = entries.map((entry) => entry.event);
|
|
756
|
+
const agentHooksConfig = stripInlineHookKeys(hookConfig, agentName);
|
|
757
|
+
agentHooksConfig.code = [{ hookId: hookName }];
|
|
758
|
+
return {
|
|
759
|
+
agentHooksConfig,
|
|
760
|
+
hookResource: {
|
|
761
|
+
kind: "hook",
|
|
762
|
+
name: hookName,
|
|
763
|
+
description: `Inline hooks for agent ${agentName}`,
|
|
764
|
+
config: {
|
|
765
|
+
events,
|
|
766
|
+
bundle,
|
|
767
|
+
sha256: sha256Hex(bundle)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function stripInlineHookKeys(hooks, agentName) {
|
|
773
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
774
|
+
throw new Error(`Agent "${agentName}" config.hooks must be an object`);
|
|
775
|
+
}
|
|
776
|
+
const entries = Object.entries(hooks);
|
|
777
|
+
for (const [key] of entries) {
|
|
778
|
+
if (key === "webhooks" || key in INLINE_AGENT_HOOK_EVENTS)
|
|
779
|
+
continue;
|
|
780
|
+
throw new Error(`Agent "${agentName}" config.hooks.${key} is not supported. Use inline hook callbacks or hooks.webhooks.`);
|
|
781
|
+
}
|
|
782
|
+
const result = Object.fromEntries(entries.filter(([key]) => key === "webhooks"));
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
function toHookFunctionExpression(source) {
|
|
786
|
+
const trimmed = source.trim();
|
|
787
|
+
if (/^(async\s+)?function\b/.test(trimmed) || /^(async\s*)?\(/.test(trimmed) || /^(async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed)) {
|
|
788
|
+
return trimmed;
|
|
789
|
+
}
|
|
790
|
+
if (/^(async\s+)?[A-Za-z_$][\w$]*\s*\(/.test(trimmed)) {
|
|
791
|
+
return trimmed.replace(/^(async\s+)?/, "$1function ");
|
|
792
|
+
}
|
|
793
|
+
return trimmed;
|
|
794
|
+
}
|
|
795
|
+
function transpileInlineHookBundle(agentName, entries) {
|
|
796
|
+
const moduleSource = `export default {
|
|
797
|
+
${entries.map((entry) => ` ${JSON.stringify(entry.event)}: ${entry.source},`).join(`
|
|
798
|
+
`)}
|
|
799
|
+
};
|
|
800
|
+
`;
|
|
801
|
+
try {
|
|
802
|
+
return new Bun.Transpiler({ loader: "ts" }).transformSync(moduleSource);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
805
|
+
throw new Error(`Agent "${agentName}" inline hooks failed to transpile: ${message}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function normalizeAgentPolicyConfig(config, agentName) {
|
|
809
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
810
|
+
throw new Error(`Agent "${agentName}" config.policy must be an object`);
|
|
811
|
+
}
|
|
812
|
+
const input = config;
|
|
813
|
+
for (const key of Object.keys(input)) {
|
|
814
|
+
if (key !== "enabled" && key !== "policyIds" && key !== "policies" && key !== "mode") {
|
|
815
|
+
throw new Error(`Agent "${agentName}" config.policy.${key} is not supported`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const policyIds = input.policyIds;
|
|
819
|
+
const policies = input.policies;
|
|
820
|
+
if (policyIds !== undefined && policies !== undefined) {
|
|
821
|
+
throw new Error(`Agent "${agentName}" config.policy cannot set both policyIds and policies`);
|
|
822
|
+
}
|
|
823
|
+
const refs = policies ?? policyIds;
|
|
824
|
+
if (refs !== undefined && !Array.isArray(refs)) {
|
|
825
|
+
throw new Error(`Agent "${agentName}" config.policy.policies must be an array`);
|
|
826
|
+
}
|
|
827
|
+
const normalized = {
|
|
828
|
+
...input,
|
|
829
|
+
...Array.isArray(refs) ? {
|
|
830
|
+
policyIds: refs.map((entry) => {
|
|
831
|
+
if (isResource(entry) && entry.kind === "policy")
|
|
832
|
+
return entry.name;
|
|
833
|
+
if (typeof entry === "string")
|
|
834
|
+
return entry;
|
|
835
|
+
throw new Error(`Agent "${agentName}" config.policy.policies must contain definePolicy(...) resources or strings`);
|
|
836
|
+
})
|
|
837
|
+
} : {}
|
|
838
|
+
};
|
|
839
|
+
delete normalized.policies;
|
|
840
|
+
delete normalized.enabled;
|
|
841
|
+
if (Array.isArray(normalized.policyIds) && normalized.policyIds.length === 0) {
|
|
842
|
+
delete normalized.policyIds;
|
|
843
|
+
}
|
|
844
|
+
return Array.isArray(normalized.policyIds) && normalized.policyIds.length > 0 ? normalized : undefined;
|
|
845
|
+
}
|
|
567
846
|
async function normalizeSkillConfig(config, projectRoot) {
|
|
568
847
|
const skillRoot = resolveContainedResourcePath(projectRoot, config.path, "Skill");
|
|
569
848
|
const manifestPath = relative(projectRoot, skillRoot).split("\\").join("/");
|
|
@@ -605,6 +884,7 @@ async function normalizeToolConfig(config, projectRoot) {
|
|
|
605
884
|
path: manifestPath,
|
|
606
885
|
description: config.description,
|
|
607
886
|
inputSchema: config.inputSchema,
|
|
887
|
+
...config.runtime !== undefined ? { runtime: config.runtime } : {},
|
|
608
888
|
...config.defaultConfig !== undefined ? { defaultConfig: config.defaultConfig } : {},
|
|
609
889
|
bundle,
|
|
610
890
|
sha256: sha256Hex(bundle)
|
|
@@ -743,7 +1023,8 @@ function idsForManifest(ids, manifest) {
|
|
|
743
1023
|
sandboxes: namesForKind(manifest, "sandbox"),
|
|
744
1024
|
crons: namesForKind(manifest, "cron"),
|
|
745
1025
|
skills: namesForKind(manifest, "skill"),
|
|
746
|
-
tools: namesForKind(manifest, "tool")
|
|
1026
|
+
tools: namesForKind(manifest, "tool"),
|
|
1027
|
+
policies: namesForKind(manifest, "policy")
|
|
747
1028
|
};
|
|
748
1029
|
return {
|
|
749
1030
|
agents: pickIds(ids.agents, namesByKind.agents),
|
|
@@ -751,14 +1032,16 @@ function idsForManifest(ids, manifest) {
|
|
|
751
1032
|
sandboxes: pickIds(ids.sandboxes, namesByKind.sandboxes),
|
|
752
1033
|
crons: pickIds(ids.crons, namesByKind.crons),
|
|
753
1034
|
skills: pickIds(ids.skills, namesByKind.skills),
|
|
754
|
-
tools: pickIds(ids.tools, namesByKind.tools)
|
|
1035
|
+
tools: pickIds(ids.tools, namesByKind.tools),
|
|
1036
|
+
hooks: {},
|
|
1037
|
+
policies: pickIds(ids.policies, namesByKind.policies)
|
|
755
1038
|
};
|
|
756
1039
|
}
|
|
757
1040
|
function namesForKind(manifest, kind) {
|
|
758
1041
|
return new Set(manifest.resources.filter((resource) => resource.kind === kind).map((resource) => resource.name));
|
|
759
1042
|
}
|
|
760
1043
|
function pickIds(values, names) {
|
|
761
|
-
return Object.fromEntries(Object.entries(values).filter(([name]) => names.has(name)));
|
|
1044
|
+
return Object.fromEntries(Object.entries(values ?? {}).filter(([name]) => names.has(name)));
|
|
762
1045
|
}
|
|
763
1046
|
function idsFile(ids) {
|
|
764
1047
|
return `// Generated by broods. Do not edit by hand.
|
|
@@ -769,6 +1052,7 @@ function idsFile(ids) {
|
|
|
769
1052
|
` + `export const crons = ids.crons;
|
|
770
1053
|
` + `export const skills = ids.skills;
|
|
771
1054
|
` + `export const tools = ids.tools;
|
|
1055
|
+
` + `export const policies = ids.policies;
|
|
772
1056
|
`;
|
|
773
1057
|
}
|
|
774
1058
|
function resourcesFile(manifest) {
|
|
@@ -803,6 +1087,7 @@ ${channelEntries}
|
|
|
803
1087
|
` + ` crons: ${aliasedIdMap("crons", ids.crons, aliases.cron)},
|
|
804
1088
|
` + ` skills: ${aliasedIdMap("skills", ids.skills, aliases.skill)},
|
|
805
1089
|
` + ` tools: ${aliasedIdMap("tools", ids.tools, aliases.tool)},
|
|
1090
|
+
` + ` policies: ${aliasedIdMap("policies", ids.policies, aliases.policy)},
|
|
806
1091
|
` + `} as const satisfies ResourceApi;
|
|
807
1092
|
`;
|
|
808
1093
|
}
|
|
@@ -827,11 +1112,11 @@ function dataModelFile() {
|
|
|
827
1112
|
|
|
828
1113
|
// src/sync.ts
|
|
829
1114
|
class BroodsSyncClient {
|
|
830
|
-
|
|
1115
|
+
baseUrl;
|
|
831
1116
|
token;
|
|
832
1117
|
fetchImpl;
|
|
833
1118
|
constructor(options) {
|
|
834
|
-
this.
|
|
1119
|
+
this.baseUrl = stripTrailingSlash(options.baseUrl);
|
|
835
1120
|
this.token = options.token;
|
|
836
1121
|
this.fetchImpl = options.fetch ?? fetch;
|
|
837
1122
|
}
|
|
@@ -859,7 +1144,13 @@ class BroodsSyncClient {
|
|
|
859
1144
|
const payload = await response.json();
|
|
860
1145
|
if (!payload.apiKey)
|
|
861
1146
|
throw new Error("Fetch runtime key failed: response omitted apiKey");
|
|
862
|
-
return {
|
|
1147
|
+
return {
|
|
1148
|
+
apiKey: payload.apiKey,
|
|
1149
|
+
keyHint: payload.keyHint ?? "",
|
|
1150
|
+
...payload.endpointId ? { endpointId: payload.endpointId } : {},
|
|
1151
|
+
...payload.projectSlug ? { projectSlug: payload.projectSlug } : {},
|
|
1152
|
+
...payload.environmentSlug ? { environmentSlug: payload.environmentSlug } : {}
|
|
1153
|
+
};
|
|
863
1154
|
}
|
|
864
1155
|
async setEnv(project, environment, name, value) {
|
|
865
1156
|
const response = await this.request(project, environment, `/env/${encodeURIComponent(name)}`, {
|
|
@@ -890,7 +1181,7 @@ class BroodsSyncClient {
|
|
|
890
1181
|
await assertOk(response, "Remove environment variable failed");
|
|
891
1182
|
}
|
|
892
1183
|
async getOnboarding() {
|
|
893
|
-
const response = await this.fetchImpl(`${this.
|
|
1184
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
|
|
894
1185
|
method: "GET",
|
|
895
1186
|
headers: {
|
|
896
1187
|
Authorization: `Bearer ${this.token}`
|
|
@@ -900,7 +1191,7 @@ class BroodsSyncClient {
|
|
|
900
1191
|
return await response.json();
|
|
901
1192
|
}
|
|
902
1193
|
async selectOnboardingOrg(orgId) {
|
|
903
|
-
const response = await this.fetchImpl(`${this.
|
|
1194
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
|
|
904
1195
|
method: "POST",
|
|
905
1196
|
headers: {
|
|
906
1197
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -911,8 +1202,20 @@ class BroodsSyncClient {
|
|
|
911
1202
|
await assertOk(response, "Select CLI org failed");
|
|
912
1203
|
return await response.json();
|
|
913
1204
|
}
|
|
1205
|
+
async createOnboardingOrg(name) {
|
|
1206
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/account/onboarding`, {
|
|
1207
|
+
method: "POST",
|
|
1208
|
+
headers: {
|
|
1209
|
+
Authorization: `Bearer ${this.token}`,
|
|
1210
|
+
"Content-Type": "application/json"
|
|
1211
|
+
},
|
|
1212
|
+
body: JSON.stringify({ createOrgName: name })
|
|
1213
|
+
});
|
|
1214
|
+
await assertOk(response, "Create CLI org failed");
|
|
1215
|
+
return await response.json();
|
|
1216
|
+
}
|
|
914
1217
|
async request(project, environment, suffix, init) {
|
|
915
|
-
const url = `${this.
|
|
1218
|
+
const url = `${this.baseUrl}/v1/account/projects/${encodeURIComponent(project)}` + `/environments/${encodeURIComponent(environment)}${suffix}`;
|
|
916
1219
|
return await this.fetchImpl(url, {
|
|
917
1220
|
...init,
|
|
918
1221
|
headers: {
|
|
@@ -970,7 +1273,7 @@ function diffManifests(local, remote) {
|
|
|
970
1273
|
}
|
|
971
1274
|
function snapshotResource(resource) {
|
|
972
1275
|
const normalized = normalizeEnvRefs(resource);
|
|
973
|
-
if (resource.kind !== "skill" && resource.kind !== "tool")
|
|
1276
|
+
if (resource.kind !== "skill" && resource.kind !== "tool" && resource.kind !== "hook")
|
|
974
1277
|
return normalized;
|
|
975
1278
|
return {
|
|
976
1279
|
...normalized,
|
|
@@ -983,7 +1286,7 @@ function renameSnapshot(resource) {
|
|
|
983
1286
|
return rest;
|
|
984
1287
|
}
|
|
985
1288
|
function isRenamableKind(kind) {
|
|
986
|
-
return kind === "agent" || kind === "workspace" || kind === "sandbox";
|
|
1289
|
+
return kind === "agent" || kind === "workspace" || kind === "sandbox" || kind === "policy";
|
|
987
1290
|
}
|
|
988
1291
|
function diffSortKey(entry) {
|
|
989
1292
|
const rank = {
|
|
@@ -1093,7 +1396,7 @@ class BroodsClient {
|
|
|
1093
1396
|
const name = refOrName;
|
|
1094
1397
|
const id = agentId ?? "";
|
|
1095
1398
|
if (!id)
|
|
1096
|
-
throw new Error(`Agent ${name} is missing a generated id. Run broods deploy first.`);
|
|
1399
|
+
throw new Error(`Agent ${name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
|
|
1097
1400
|
return {
|
|
1098
1401
|
id,
|
|
1099
1402
|
run: (input) => this.run({ ...input, agentName: name, agentId: id }),
|
|
@@ -1103,7 +1406,7 @@ class BroodsClient {
|
|
|
1103
1406
|
}
|
|
1104
1407
|
const ref = refOrName;
|
|
1105
1408
|
if (!ref.id)
|
|
1106
|
-
throw new Error(`Agent ${ref.name} is missing a generated id. Run broods deploy first.`);
|
|
1409
|
+
throw new Error(`Agent ${ref.name} is missing a generated id. Run broods dev --once or broods deploy to sync it first.`);
|
|
1107
1410
|
return {
|
|
1108
1411
|
id: ref.id,
|
|
1109
1412
|
run: (input) => this.run(ref, input),
|
|
@@ -1154,7 +1457,8 @@ class BroodsClient {
|
|
|
1154
1457
|
agentName: refOrInput.name
|
|
1155
1458
|
} : refOrInput;
|
|
1156
1459
|
const body = directRunBody(input, "async");
|
|
1157
|
-
const
|
|
1460
|
+
const targetUrl = maybeInput ? this.scopedUrl(refOrInput, "/async") : `${this.baseUrl}/async`;
|
|
1461
|
+
const response = await this.fetchJson(targetUrl, {
|
|
1158
1462
|
method: "POST",
|
|
1159
1463
|
headers: this.apiKeyHeaders(),
|
|
1160
1464
|
body: JSON.stringify(body)
|
|
@@ -1197,7 +1501,7 @@ class BroodsClient {
|
|
|
1197
1501
|
throw new Error("Polling timeout");
|
|
1198
1502
|
}
|
|
1199
1503
|
async createCron(input) {
|
|
1200
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1504
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
|
|
1201
1505
|
method: "POST",
|
|
1202
1506
|
headers: this.apiKeyHeaders(),
|
|
1203
1507
|
body: JSON.stringify(resolveCronInput(input))
|
|
@@ -1207,7 +1511,7 @@ class BroodsClient {
|
|
|
1207
1511
|
return await response.json();
|
|
1208
1512
|
}
|
|
1209
1513
|
async listCrons() {
|
|
1210
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1514
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons`, {
|
|
1211
1515
|
method: "GET",
|
|
1212
1516
|
headers: this.apiKeyHeaders()
|
|
1213
1517
|
});
|
|
@@ -1217,7 +1521,7 @@ class BroodsClient {
|
|
|
1217
1521
|
return payload.crons;
|
|
1218
1522
|
}
|
|
1219
1523
|
async getCron(cronId) {
|
|
1220
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1524
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
|
|
1221
1525
|
method: "GET",
|
|
1222
1526
|
headers: this.apiKeyHeaders()
|
|
1223
1527
|
});
|
|
@@ -1232,7 +1536,7 @@ class BroodsClient {
|
|
|
1232
1536
|
if (options.limit !== undefined)
|
|
1233
1537
|
params.set("limit", String(options.limit));
|
|
1234
1538
|
const suffix = params.size > 0 ? `?${params}` : "";
|
|
1235
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1539
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
|
|
1236
1540
|
method: "GET",
|
|
1237
1541
|
headers: this.apiKeyHeaders()
|
|
1238
1542
|
});
|
|
@@ -1242,7 +1546,7 @@ class BroodsClient {
|
|
|
1242
1546
|
return payload.runs;
|
|
1243
1547
|
}
|
|
1244
1548
|
async updateCron(cronId, patch) {
|
|
1245
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1549
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
|
|
1246
1550
|
method: "PATCH",
|
|
1247
1551
|
headers: this.apiKeyHeaders(),
|
|
1248
1552
|
body: JSON.stringify(patch)
|
|
@@ -1252,7 +1556,7 @@ class BroodsClient {
|
|
|
1252
1556
|
return await response.json();
|
|
1253
1557
|
}
|
|
1254
1558
|
async deleteCron(cronId) {
|
|
1255
|
-
const response = await this.fetchJson(`${this.baseUrl}/
|
|
1559
|
+
const response = await this.fetchJson(`${this.baseUrl}/v1/crons/${encodeURIComponent(cronId)}`, {
|
|
1256
1560
|
method: "DELETE",
|
|
1257
1561
|
headers: this.apiKeyHeaders()
|
|
1258
1562
|
});
|
|
@@ -1263,11 +1567,11 @@ class BroodsClient {
|
|
|
1263
1567
|
const payload = await response.json();
|
|
1264
1568
|
return payload.deleted;
|
|
1265
1569
|
}
|
|
1266
|
-
scopedUrl(ref) {
|
|
1570
|
+
scopedUrl(ref, suffix = "") {
|
|
1267
1571
|
if (ref.projectSlug && ref.environmentSlug && ref.endpointId) {
|
|
1268
|
-
return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}`;
|
|
1572
|
+
return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}${suffix}`;
|
|
1269
1573
|
}
|
|
1270
|
-
return this.baseUrl
|
|
1574
|
+
return `${this.baseUrl}${suffix}`;
|
|
1271
1575
|
}
|
|
1272
1576
|
async openStream(body, targetUrl) {
|
|
1273
1577
|
if (this.apiKey) {
|
|
@@ -1424,7 +1728,6 @@ function formatStreamError(error) {
|
|
|
1424
1728
|
var DEFAULT_CONNECT_TIMEOUT_MS = 2000;
|
|
1425
1729
|
var WS_CONNECTING = 0;
|
|
1426
1730
|
var WS_OPEN = 1;
|
|
1427
|
-
var LAMBDA_FUNCTION_URL_HOST_RE = /\.lambda-url\.[a-z0-9-]+\.on\.aws$/i;
|
|
1428
1731
|
|
|
1429
1732
|
class BroodsWebSocketClient {
|
|
1430
1733
|
baseUrl;
|
|
@@ -1433,7 +1736,7 @@ class BroodsWebSocketClient {
|
|
|
1433
1736
|
connectTimeoutMs;
|
|
1434
1737
|
constructor(options = {}) {
|
|
1435
1738
|
loadBroodsRuntimeConfig();
|
|
1436
|
-
this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl ||
|
|
1739
|
+
this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
|
|
1437
1740
|
this.apiKey = options.apiKey || process.env.BROODS_API_KEY || "";
|
|
1438
1741
|
if (!this.apiKey)
|
|
1439
1742
|
throw new Error("BroodsWebSocketClient requires apiKey or BROODS_API_KEY.");
|
|
@@ -1615,10 +1918,6 @@ function normalizeWebSocketServiceUrl(value) {
|
|
|
1615
1918
|
return normalizeHttpServiceUrl(trimmed);
|
|
1616
1919
|
}
|
|
1617
1920
|
function webSocketAccessError(baseUrl) {
|
|
1618
|
-
const host = new URL(baseUrl).hostname;
|
|
1619
|
-
if (LAMBDA_FUNCTION_URL_HOST_RE.test(host)) {
|
|
1620
|
-
return new Error(`Cannot access the WebSocket service at ${baseUrl}. AWS Lambda Function URLs do not support WebSocket upgrades; ` + "set BROODS_WEBSOCKET_URL or BROODS_HOST to the deployed WebSocket gateway URL instead.");
|
|
1621
|
-
}
|
|
1622
1921
|
return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
|
|
1623
1922
|
}
|
|
1624
1923
|
function parseServerMessage(data) {
|
|
@@ -1690,6 +1989,7 @@ async function* subscribeObservabilityLogs(options, subscribeOptions = {}) {
|
|
|
1690
1989
|
async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
|
|
1691
1990
|
const { baseUrl, apiKey, project, environment } = options;
|
|
1692
1991
|
const { backfill = 0, minLevel, signal } = subscribeOptions;
|
|
1992
|
+
const liveOnly = subscribeOptions.liveOnly ?? backfill <= 0;
|
|
1693
1993
|
if (signal?.aborted)
|
|
1694
1994
|
return;
|
|
1695
1995
|
const url = buildObservabilityUrl(baseUrl, project, environment, apiKey);
|
|
@@ -1725,6 +2025,7 @@ async function* subscribeObservabilityLogsOnce(options, subscribeOptions) {
|
|
|
1725
2025
|
type: "subscribe",
|
|
1726
2026
|
stream: "logs",
|
|
1727
2027
|
...backfill > 0 ? { backfill } : {},
|
|
2028
|
+
...liveOnly ? { liveOnly: true } : {},
|
|
1728
2029
|
...minLevel !== undefined ? { minLevel } : {}
|
|
1729
2030
|
};
|
|
1730
2031
|
socket.send(JSON.stringify(msg));
|
|
@@ -1814,13 +2115,19 @@ function optionValue(args, name) {
|
|
|
1814
2115
|
function hasFlag(args, name) {
|
|
1815
2116
|
return args.includes(name);
|
|
1816
2117
|
}
|
|
1817
|
-
|
|
2118
|
+
function isPlainObject(value) {
|
|
2119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2120
|
+
}
|
|
2121
|
+
async function requireAuth(baseUrl) {
|
|
1818
2122
|
loadBroodsRuntimeConfig();
|
|
1819
2123
|
const auth = await readStoredAuth();
|
|
1820
2124
|
if (!auth) {
|
|
1821
|
-
throw new Error("Run `broods login` first, or set BROODS_TOKEN and
|
|
2125
|
+
throw new Error("Run `broods login` first, or set BROODS_TOKEN and BROODS_BASE_URL.");
|
|
1822
2126
|
}
|
|
1823
|
-
return
|
|
2127
|
+
return {
|
|
2128
|
+
...auth,
|
|
2129
|
+
...baseUrl ? { baseUrl: stripTrailingSlash(baseUrl) } : {}
|
|
2130
|
+
};
|
|
1824
2131
|
}
|
|
1825
2132
|
async function promptSecret(label) {
|
|
1826
2133
|
const rl = createInterface({ input, output });
|
|
@@ -1893,17 +2200,18 @@ async function loginWithBrowser(dashboardUrl) {
|
|
|
1893
2200
|
await assertCliAuthRouteExists(startUrl);
|
|
1894
2201
|
openBrowser(startUrl);
|
|
1895
2202
|
console.log(`Opening ${startUrl}`);
|
|
1896
|
-
const
|
|
1897
|
-
const response = await fetch(`${
|
|
2203
|
+
const login = await waitWithTimeout(code.promise, LOGIN_TIMEOUT_MS);
|
|
2204
|
+
const response = await fetch(`${login.baseUrl}/v1/account/auth/exchange`, {
|
|
1898
2205
|
method: "POST",
|
|
1899
2206
|
headers: { "Content-Type": "application/json" },
|
|
1900
|
-
body: JSON.stringify({ code:
|
|
2207
|
+
body: JSON.stringify({ code: login.code })
|
|
1901
2208
|
});
|
|
1902
2209
|
if (!response.ok) {
|
|
1903
2210
|
throw new Error(`Login exchange failed: ${response.status} ${await response.text()}`);
|
|
1904
2211
|
}
|
|
1905
2212
|
const payload = await response.json();
|
|
1906
2213
|
const auth = {
|
|
2214
|
+
baseUrl: login.baseUrl,
|
|
1907
2215
|
dashboardUrl: stripTrailingSlash(dashboardUrl),
|
|
1908
2216
|
token: payload.token,
|
|
1909
2217
|
createdAt: new Date().toISOString(),
|
|
@@ -1937,12 +2245,21 @@ function waitForCallback(expectedState) {
|
|
|
1937
2245
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1938
2246
|
const state = url.searchParams.get("state");
|
|
1939
2247
|
const code = url.searchParams.get("code");
|
|
2248
|
+
const baseUrl = url.searchParams.get("base_url");
|
|
1940
2249
|
if (state !== expectedState || !code) {
|
|
1941
2250
|
res.writeHead(400).end("Invalid broods login callback.");
|
|
1942
2251
|
return;
|
|
1943
2252
|
}
|
|
2253
|
+
if (!baseUrl) {
|
|
2254
|
+
res.writeHead(400).end("Login callback omitted base_url; update the dashboard deployment.");
|
|
2255
|
+
callbackReject(new Error("Login callback did not advertise the API base URL (base_url). " + "Deploy a dashboard build that includes the Convex-direct CLI auth flow."));
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
1944
2258
|
res.writeHead(200, { "Content-Type": "text/plain" }).end("broods CLI login complete. You can close this tab.");
|
|
1945
|
-
callbackResolve(
|
|
2259
|
+
callbackResolve({
|
|
2260
|
+
code,
|
|
2261
|
+
baseUrl: stripTrailingSlash(baseUrl)
|
|
2262
|
+
});
|
|
1946
2263
|
} catch (error) {
|
|
1947
2264
|
callbackReject(error);
|
|
1948
2265
|
}
|
|
@@ -2227,10 +2544,77 @@ function renderStreamPart(part, state) {
|
|
|
2227
2544
|
break;
|
|
2228
2545
|
}
|
|
2229
2546
|
}
|
|
2547
|
+
// package.json
|
|
2548
|
+
var package_default = {
|
|
2549
|
+
name: "broods",
|
|
2550
|
+
version: "0.2.0",
|
|
2551
|
+
description: "CLI + TypeScript SDK for the broods agent platform.",
|
|
2552
|
+
type: "module",
|
|
2553
|
+
license: "MIT",
|
|
2554
|
+
homepage: "https://github.com/beeblastco/broods#readme",
|
|
2555
|
+
repository: {
|
|
2556
|
+
type: "git",
|
|
2557
|
+
url: "git+https://github.com/beeblastco/broods.git",
|
|
2558
|
+
directory: "packages/broods"
|
|
2559
|
+
},
|
|
2560
|
+
bugs: {
|
|
2561
|
+
url: "https://github.com/beeblastco/broods/issues"
|
|
2562
|
+
},
|
|
2563
|
+
keywords: [
|
|
2564
|
+
"agents",
|
|
2565
|
+
"ai",
|
|
2566
|
+
"sdk",
|
|
2567
|
+
"cli",
|
|
2568
|
+
"broods"
|
|
2569
|
+
],
|
|
2570
|
+
bin: {
|
|
2571
|
+
broods: "dist/cli/index.js"
|
|
2572
|
+
},
|
|
2573
|
+
exports: {
|
|
2574
|
+
".": {
|
|
2575
|
+
types: "./dist/index.d.ts",
|
|
2576
|
+
import: "./dist/index.js"
|
|
2577
|
+
}
|
|
2578
|
+
},
|
|
2579
|
+
main: "./dist/index.js",
|
|
2580
|
+
types: "./dist/index.d.ts",
|
|
2581
|
+
files: [
|
|
2582
|
+
"dist",
|
|
2583
|
+
"README.md",
|
|
2584
|
+
"LICENSE.md"
|
|
2585
|
+
],
|
|
2586
|
+
scripts: {
|
|
2587
|
+
build: "bun scripts/build.ts && rollup -c rollup.dts.config.mjs && bun scripts/clean-dts-side-effects.ts && rm -rf .dts-temp",
|
|
2588
|
+
check: "tsc --noEmit",
|
|
2589
|
+
"pack:check": "bun run build && npm pack --dry-run",
|
|
2590
|
+
test: "bun test"
|
|
2591
|
+
},
|
|
2592
|
+
engines: {
|
|
2593
|
+
node: ">=22"
|
|
2594
|
+
},
|
|
2595
|
+
dependencies: {
|
|
2596
|
+
"@chat-adapter/discord": "^4.31.0",
|
|
2597
|
+
"@chat-adapter/github": "^4.31.0",
|
|
2598
|
+
"@chat-adapter/slack": "^4.31.0",
|
|
2599
|
+
"@chat-adapter/telegram": "^4.31.0",
|
|
2600
|
+
ai: "^7.0.11"
|
|
2601
|
+
},
|
|
2602
|
+
devDependencies: {
|
|
2603
|
+
"@types/bun": "^1.3.14",
|
|
2604
|
+
rollup: "^4.53.3",
|
|
2605
|
+
"rollup-plugin-dts": "^6.3.0"
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2230
2608
|
|
|
2231
2609
|
// src/cli/index.ts
|
|
2232
|
-
var VERSION =
|
|
2610
|
+
var VERSION = package_default.version;
|
|
2233
2611
|
var DEFAULT_DASHBOARD_URL = "https://dashboard.broods.app";
|
|
2612
|
+
var DEFAULT_SERVICE_REGION = "eu-west-1";
|
|
2613
|
+
var SERVICE_REGIONS = [
|
|
2614
|
+
{ region: "eu-west-1", label: "eu-west-1 (Ireland)" },
|
|
2615
|
+
{ region: "us-east-1", label: "us-east-1 (US East)" },
|
|
2616
|
+
{ region: "ap-southeast-1", label: "ap-southeast-1 (Singapore)" }
|
|
2617
|
+
];
|
|
2234
2618
|
var HELP = `broods v${VERSION}
|
|
2235
2619
|
|
|
2236
2620
|
Usage: broods <command>
|
|
@@ -2256,13 +2640,15 @@ Commands:
|
|
|
2256
2640
|
run <agent> <prompt> Run an agent once and pretty-stream the result (thinking, tool calls, text)
|
|
2257
2641
|
|
|
2258
2642
|
Options:
|
|
2259
|
-
--dashboard-url <url> Dashboard base URL (default: ${DEFAULT_DASHBOARD_URL})
|
|
2643
|
+
--dashboard-url <url> Dashboard base URL for login and deep links (default: ${DEFAULT_DASHBOARD_URL})
|
|
2644
|
+
--base-url <url> broods API base URL for sync/env calls (default: discovered at login)
|
|
2260
2645
|
--project <name> Project name override (default: package name or folder)
|
|
2261
2646
|
--env <name> Target environment override
|
|
2647
|
+
--region <region> Broods service region preference (default: ${DEFAULT_SERVICE_REGION})
|
|
2262
2648
|
--prune Allow deploy to delete undeclared remote resources
|
|
2263
2649
|
--rotate-key Mint a fresh runtime API key on deploy and write it to .env.local
|
|
2264
|
-
--errors Show WARN/ERROR only (
|
|
2265
|
-
--level <lvl> Minimum log level INFO|WARN|ERROR (
|
|
2650
|
+
--errors Show WARN/ERROR only (default for \`dev\`, \`stream\`, and \`logs\`)
|
|
2651
|
+
--level <lvl> Minimum log level INFO|WARN|ERROR (default WARN)
|
|
2266
2652
|
-n, --limit <n> Backfill line count (with \`logs\`, default 100)
|
|
2267
2653
|
--json Print logs as raw JSON (with \`logs\`, applies to backfill output)
|
|
2268
2654
|
--force Allow init to overwrite starter files`;
|
|
@@ -2326,6 +2712,7 @@ async function init(args) {
|
|
|
2326
2712
|
dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
|
|
2327
2713
|
project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
|
|
2328
2714
|
environment: optionValue(args, "--env") ?? "development",
|
|
2715
|
+
region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
|
|
2329
2716
|
force
|
|
2330
2717
|
});
|
|
2331
2718
|
console.log(`Created ${PROJECT_DIR}/`);
|
|
@@ -2341,9 +2728,10 @@ async function login(args) {
|
|
|
2341
2728
|
const project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
|
|
2342
2729
|
const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
|
|
2343
2730
|
await writeLocalEnvDefaults({
|
|
2344
|
-
dashboardUrl: auth.dashboardUrl,
|
|
2731
|
+
dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
|
|
2345
2732
|
project,
|
|
2346
2733
|
environment,
|
|
2734
|
+
region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
|
|
2347
2735
|
force: false
|
|
2348
2736
|
});
|
|
2349
2737
|
const user = auth.user?.email || auth.user?.name || auth.user?.authId;
|
|
@@ -2356,11 +2744,11 @@ async function login(args) {
|
|
|
2356
2744
|
console.log(`Org: ${org}`);
|
|
2357
2745
|
if (account)
|
|
2358
2746
|
console.log(`Account: ${account}`);
|
|
2359
|
-
await writeRuntimeKeyForLogin(auth.
|
|
2747
|
+
await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
|
|
2360
2748
|
}
|
|
2361
|
-
async function writeRuntimeKeyForLogin(
|
|
2749
|
+
async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
|
|
2362
2750
|
try {
|
|
2363
|
-
const client = new BroodsSyncClient({
|
|
2751
|
+
const client = new BroodsSyncClient({ baseUrl, token });
|
|
2364
2752
|
const key = await client.getRuntimeKey(project, environment);
|
|
2365
2753
|
if (key?.apiKey) {
|
|
2366
2754
|
await writeEnvValue("BROODS_API_KEY", key.apiKey);
|
|
@@ -2374,8 +2762,8 @@ async function diff(args) {
|
|
|
2374
2762
|
environment: optionValue(args, "--env"),
|
|
2375
2763
|
command: "dev"
|
|
2376
2764
|
});
|
|
2377
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2378
|
-
const client = new BroodsSyncClient({
|
|
2765
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
2766
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2379
2767
|
const remote = await client.getManifest(manifest.project, manifest.environment);
|
|
2380
2768
|
printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
|
|
2381
2769
|
}
|
|
@@ -2386,8 +2774,8 @@ async function deploy(args) {
|
|
|
2386
2774
|
command: "deploy",
|
|
2387
2775
|
useRuntimeEnvironment: false
|
|
2388
2776
|
});
|
|
2389
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2390
|
-
const client = new BroodsSyncClient({
|
|
2777
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
2778
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2391
2779
|
const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
|
|
2392
2780
|
await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
|
|
2393
2781
|
await ensureGitIgnore();
|
|
@@ -2407,11 +2795,15 @@ async function applyDeploymentKey(deployment) {
|
|
|
2407
2795
|
}
|
|
2408
2796
|
function printSyncWarnings(result) {
|
|
2409
2797
|
const missing = result.warnings?.missingEnv ?? [];
|
|
2410
|
-
if (missing.length
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2798
|
+
if (missing.length > 0) {
|
|
2799
|
+
printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
|
|
2800
|
+
for (const name of missing)
|
|
2801
|
+
console.log(` broods env set ${name}`);
|
|
2802
|
+
}
|
|
2803
|
+
const missingPolicies = result.warnings?.missingPolicies ?? [];
|
|
2804
|
+
if (missingPolicies.length > 0) {
|
|
2805
|
+
printWarning(`\u26A0 ${missingPolicies.length} policy ref(s) in agent config match no policy resource ` + `in this deploy and will be ignored at runtime: ${missingPolicies.join(", ")}`);
|
|
2806
|
+
}
|
|
2415
2807
|
}
|
|
2416
2808
|
async function dev(args) {
|
|
2417
2809
|
await ensureDevOnboarding(args);
|
|
@@ -2428,7 +2820,8 @@ async function dev(args) {
|
|
|
2428
2820
|
const childEnv = {
|
|
2429
2821
|
...process.env,
|
|
2430
2822
|
BROODS_DECLINED_FILE: declinedFile,
|
|
2431
|
-
BROODS_SUPPRESS_DEV_TARGET: "1"
|
|
2823
|
+
BROODS_SUPPRESS_DEV_TARGET: "1",
|
|
2824
|
+
BROODS_RELOAD_ENV: "1"
|
|
2432
2825
|
};
|
|
2433
2826
|
await printDevTarget(args);
|
|
2434
2827
|
await runSyncChild(args, childEnv);
|
|
@@ -2474,7 +2867,7 @@ async function streamDevLogs(args, signal) {
|
|
|
2474
2867
|
try {
|
|
2475
2868
|
creds = resolveObservabilityCredentials();
|
|
2476
2869
|
} catch {
|
|
2477
|
-
console.log("\xB7 live logs off \u2014
|
|
2870
|
+
console.log("\xB7 live logs off \u2014 no runtime key found for this environment yet. Run `broods dev --once` after login to create or reconnect it.");
|
|
2478
2871
|
return;
|
|
2479
2872
|
}
|
|
2480
2873
|
let project;
|
|
@@ -2519,27 +2912,25 @@ async function ensureLocalDevDefaults(args) {
|
|
|
2519
2912
|
const missing = [
|
|
2520
2913
|
"BROODS_DASHBOARD_URL",
|
|
2521
2914
|
"BROODS_PROJECT",
|
|
2522
|
-
"BROODS_ENVIRONMENT"
|
|
2915
|
+
"BROODS_ENVIRONMENT",
|
|
2916
|
+
"BROODS_REGION"
|
|
2523
2917
|
].filter((key) => values[key] === undefined);
|
|
2524
2918
|
if (missing.length === 0)
|
|
2525
2919
|
return;
|
|
2526
2920
|
const needsProject = values.BROODS_PROJECT === undefined;
|
|
2527
2921
|
const needsEnvironment = values.BROODS_ENVIRONMENT === undefined;
|
|
2922
|
+
const needsRegion = values.BROODS_REGION === undefined;
|
|
2528
2923
|
const runtime = loadBroodsRuntimeConfig();
|
|
2529
2924
|
const dashboardUrl = optionValue(args, "--dashboard-url") ?? runtime.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
|
|
2530
2925
|
let project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
|
|
2531
2926
|
let environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
|
|
2927
|
+
let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
|
|
2532
2928
|
if (process.stdin.isTTY && needsProject) {
|
|
2533
2929
|
const auth = await requireAuthOrLogin(dashboardUrl);
|
|
2534
|
-
const client = new BroodsSyncClient({
|
|
2930
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2535
2931
|
const context = await getOnboardingContextOrFallback(client, auth);
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
throw new Error("No selectable org has an active API account. Open Settings -> API Access in the dashboard first.");
|
|
2539
|
-
}
|
|
2540
|
-
const selectedOrg = await promptSelect("Select organization", selectableOrgs, formatOrgChoice);
|
|
2541
|
-
const selectedContext = selectedOrg.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selectedOrg.id);
|
|
2542
|
-
project = await promptText("Project name", defaultProjectName(selectedContext, project));
|
|
2932
|
+
const selectedContext = await selectOnboardingOrg(client, context);
|
|
2933
|
+
project = await selectOnboardingProject(selectedContext, project);
|
|
2543
2934
|
if (!project.trim())
|
|
2544
2935
|
throw new Error("Project name is required.");
|
|
2545
2936
|
}
|
|
@@ -2548,16 +2939,20 @@ async function ensureLocalDevDefaults(args) {
|
|
|
2548
2939
|
if (!environment.trim())
|
|
2549
2940
|
throw new Error("Environment is required.");
|
|
2550
2941
|
}
|
|
2942
|
+
if (process.stdin.isTTY && needsRegion) {
|
|
2943
|
+
region = await promptSelect("Select service region", [...SERVICE_REGIONS], (entry) => entry.label).then((entry) => entry.region);
|
|
2944
|
+
}
|
|
2551
2945
|
await writeLocalEnvDefaults({
|
|
2552
2946
|
dashboardUrl,
|
|
2553
2947
|
project,
|
|
2554
2948
|
environment,
|
|
2949
|
+
region,
|
|
2555
2950
|
force: false
|
|
2556
2951
|
});
|
|
2557
2952
|
}
|
|
2558
2953
|
async function requireAuthOrLogin(dashboardUrl) {
|
|
2559
2954
|
try {
|
|
2560
|
-
return await requireAuth(
|
|
2955
|
+
return await requireAuth();
|
|
2561
2956
|
} catch (error) {
|
|
2562
2957
|
if (!process.stdin.isTTY)
|
|
2563
2958
|
throw error;
|
|
@@ -2589,10 +2984,37 @@ function formatOrgChoice(org) {
|
|
|
2589
2984
|
const suffix = org.role === "owner" || org.role === "admin" ? org.role : "member";
|
|
2590
2985
|
return `${org.name} (${org.slug}, ${suffix})`;
|
|
2591
2986
|
}
|
|
2987
|
+
async function selectOnboardingOrg(client, context) {
|
|
2988
|
+
const activeOrgs = context.orgs.filter((org) => org.accountStatus === "active");
|
|
2989
|
+
const createNew = { kind: "create", name: "New organization" };
|
|
2990
|
+
const selected = await promptSelect("Select organization", [...activeOrgs, createNew], (entry) => ("kind" in entry) ? "Create new organization" : formatOrgChoice(entry));
|
|
2991
|
+
if ("kind" in selected) {
|
|
2992
|
+
const name = await promptText("Organization name", inferProjectName(process.cwd()));
|
|
2993
|
+
if (!name.trim())
|
|
2994
|
+
throw new Error("Organization name is required.");
|
|
2995
|
+
return await client.createOnboardingOrg(name);
|
|
2996
|
+
}
|
|
2997
|
+
return selected.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selected.id);
|
|
2998
|
+
}
|
|
2592
2999
|
function defaultProjectName(context, inferred) {
|
|
2593
3000
|
const exact = context.projects.find((project) => project.name === inferred || project.slug === inferred);
|
|
2594
3001
|
return exact?.name ?? inferred;
|
|
2595
3002
|
}
|
|
3003
|
+
async function selectOnboardingProject(context, inferred) {
|
|
3004
|
+
if (context.projects.length === 0) {
|
|
3005
|
+
return promptText("Project name", inferred);
|
|
3006
|
+
}
|
|
3007
|
+
const createNew = { kind: "create", name: defaultProjectName(context, inferred), slug: "" };
|
|
3008
|
+
const choices = [
|
|
3009
|
+
...context.projects,
|
|
3010
|
+
createNew
|
|
3011
|
+
];
|
|
3012
|
+
const selected = await promptSelect("Select project", choices, (entry) => ("kind" in entry) && entry.kind === "create" ? `Create new project (${entry.name})` : `${entry.name} (${entry.slug})`);
|
|
3013
|
+
if ("kind" in selected && selected.kind === "create") {
|
|
3014
|
+
return promptText("New project name", selected.name);
|
|
3015
|
+
}
|
|
3016
|
+
return selected.name;
|
|
3017
|
+
}
|
|
2596
3018
|
function runSyncChild(args, env3) {
|
|
2597
3019
|
return new Promise((resolvePromise, reject) => {
|
|
2598
3020
|
const entryPoint = process.argv[1] ?? "";
|
|
@@ -2616,12 +3038,12 @@ async function syncDev(args) {
|
|
|
2616
3038
|
environment: optionValue(args, "--env"),
|
|
2617
3039
|
command: "dev"
|
|
2618
3040
|
});
|
|
2619
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2620
|
-
const client = new BroodsSyncClient({
|
|
3041
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
3042
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2621
3043
|
const remote = await client.getManifest(manifest.project, manifest.environment);
|
|
2622
3044
|
const diff2 = diffManifests(manifest, remote?.manifest ?? null);
|
|
2623
3045
|
printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
|
|
2624
|
-
await syncLocalEnvVars(client, manifest);
|
|
3046
|
+
await syncLocalEnvVars(client, manifest, envSyncScopeKey(auth.baseUrl, auth.token));
|
|
2625
3047
|
let result = await client.putManifest(manifest, false);
|
|
2626
3048
|
await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
|
|
2627
3049
|
await ensureGitIgnore();
|
|
@@ -2671,7 +3093,7 @@ function printChannelEndpoints(channels, result) {
|
|
|
2671
3093
|
console.log(`Channel ${channel.alias} (${channel.type}): ${url}${channel.type === "pancake" ? "?secret=<PANCAKE_WEBHOOK_SECRET>" : ""}`);
|
|
2672
3094
|
}
|
|
2673
3095
|
}
|
|
2674
|
-
async function syncLocalEnvVars(client, manifest) {
|
|
3096
|
+
async function syncLocalEnvVars(client, manifest, scopeKey) {
|
|
2675
3097
|
const present = collectEnvRefNames(manifest).filter((name) => {
|
|
2676
3098
|
const value = process.env[name];
|
|
2677
3099
|
return !name.startsWith("BROODS_") && value !== undefined && value !== "";
|
|
@@ -2679,19 +3101,23 @@ async function syncLocalEnvVars(client, manifest) {
|
|
|
2679
3101
|
if (present.length === 0)
|
|
2680
3102
|
return;
|
|
2681
3103
|
const cache = await loadEnvSyncCache();
|
|
2682
|
-
const known = cache[envCacheKey(manifest.project, manifest.environment)] ?? {};
|
|
2683
|
-
const
|
|
3104
|
+
const known = cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] ?? {};
|
|
3105
|
+
const remoteNames = new Set((await client.listEnv(manifest.project, manifest.environment)).map((entry) => entry.name));
|
|
3106
|
+
const changed = present.filter((name) => !remoteNames.has(name) || known[name] !== hashEnvValue(process.env[name]));
|
|
2684
3107
|
if (changed.length === 0)
|
|
2685
3108
|
return;
|
|
2686
3109
|
await Promise.all(changed.map((name) => client.setEnv(manifest.project, manifest.environment, name, process.env[name])));
|
|
2687
3110
|
for (const name of changed)
|
|
2688
3111
|
known[name] = hashEnvValue(process.env[name]);
|
|
2689
|
-
cache[envCacheKey(manifest.project, manifest.environment)] = known;
|
|
3112
|
+
cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] = known;
|
|
2690
3113
|
await saveEnvSyncCache(cache);
|
|
2691
3114
|
printEnvSync(changed);
|
|
2692
3115
|
}
|
|
2693
|
-
function
|
|
2694
|
-
return `${
|
|
3116
|
+
function envSyncScopeKey(baseUrl, token) {
|
|
3117
|
+
return `${baseUrl}:${createHash2("sha256").update(token).digest("hex").slice(0, 16)}`;
|
|
3118
|
+
}
|
|
3119
|
+
function envCacheKey(scopeKey, project, environment) {
|
|
3120
|
+
return `${scopeKey}:${project}:${environment}`;
|
|
2695
3121
|
}
|
|
2696
3122
|
function envSyncCachePath() {
|
|
2697
3123
|
return resolve4(dirname2(USER_CONFIG_PATH), "env-sync.json");
|
|
@@ -2716,15 +3142,15 @@ async function saveEnvSyncCache(cache) {
|
|
|
2716
3142
|
await writeFile3(path, `${JSON.stringify(cache, null, 2)}
|
|
2717
3143
|
`, "utf8");
|
|
2718
3144
|
}
|
|
2719
|
-
async function rememberEnvSyncValue(project, environment, name, value) {
|
|
3145
|
+
async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
|
|
2720
3146
|
const cache = await loadEnvSyncCache();
|
|
2721
|
-
const key = envCacheKey(project, environment);
|
|
3147
|
+
const key = envCacheKey(scopeKey, project, environment);
|
|
2722
3148
|
cache[key] = { ...cache[key] ?? {}, [name]: hashEnvValue(value) };
|
|
2723
3149
|
await saveEnvSyncCache(cache);
|
|
2724
3150
|
}
|
|
2725
|
-
async function forgetEnvSyncValue(project, environment, name) {
|
|
3151
|
+
async function forgetEnvSyncValue(scopeKey, project, environment, name) {
|
|
2726
3152
|
const cache = await loadEnvSyncCache();
|
|
2727
|
-
const key = envCacheKey(project, environment);
|
|
3153
|
+
const key = envCacheKey(scopeKey, project, environment);
|
|
2728
3154
|
if (!cache[key] || !(name in cache[key]))
|
|
2729
3155
|
return;
|
|
2730
3156
|
delete cache[key][name];
|
|
@@ -2736,11 +3162,11 @@ async function printDevTarget(args) {
|
|
|
2736
3162
|
environment: optionValue(args, "--env"),
|
|
2737
3163
|
command: "dev"
|
|
2738
3164
|
});
|
|
2739
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
3165
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
2740
3166
|
printDeploymentTarget({
|
|
2741
3167
|
project: manifest.project,
|
|
2742
3168
|
environment: manifest.environment,
|
|
2743
|
-
dashboardUrl: auth.dashboardUrl
|
|
3169
|
+
dashboardUrl: auth.dashboardUrl ?? DEFAULT_DASHBOARD_URL
|
|
2744
3170
|
});
|
|
2745
3171
|
}
|
|
2746
3172
|
async function loadDeclinedDeletes() {
|
|
@@ -2779,8 +3205,9 @@ async function envCommand(args) {
|
|
|
2779
3205
|
environment: optionValue(args, "--env"),
|
|
2780
3206
|
command: "dev"
|
|
2781
3207
|
});
|
|
2782
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2783
|
-
const client = new BroodsSyncClient({
|
|
3208
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
3209
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
3210
|
+
const scopeKey = envSyncScopeKey(auth.baseUrl, auth.token);
|
|
2784
3211
|
const target = `${manifest.project}/${manifest.environment}`;
|
|
2785
3212
|
if (isList) {
|
|
2786
3213
|
const variables = await client.listEnv(manifest.project, manifest.environment);
|
|
@@ -2805,13 +3232,13 @@ async function envCommand(args) {
|
|
|
2805
3232
|
}
|
|
2806
3233
|
if (isRemove) {
|
|
2807
3234
|
await client.removeEnv(manifest.project, manifest.environment, name);
|
|
2808
|
-
await forgetEnvSyncValue(manifest.project, manifest.environment, name);
|
|
3235
|
+
await forgetEnvSyncValue(scopeKey, manifest.project, manifest.environment, name);
|
|
2809
3236
|
console.log(`Removed ${name} from ${target}`);
|
|
2810
3237
|
return;
|
|
2811
3238
|
}
|
|
2812
3239
|
const value = await promptSecret(name);
|
|
2813
3240
|
await client.setEnv(manifest.project, manifest.environment, name, value);
|
|
2814
|
-
await rememberEnvSyncValue(manifest.project, manifest.environment, name, value);
|
|
3241
|
+
await rememberEnvSyncValue(scopeKey, manifest.project, manifest.environment, name, value);
|
|
2815
3242
|
console.log(`Stored ${name} for ${target}`);
|
|
2816
3243
|
}
|
|
2817
3244
|
function resolveObservabilityCredentials() {
|
|
@@ -2828,7 +3255,7 @@ function resolveMinLevel(args) {
|
|
|
2828
3255
|
return "WARN";
|
|
2829
3256
|
const raw = optionValue(args, "--level");
|
|
2830
3257
|
if (!raw)
|
|
2831
|
-
return;
|
|
3258
|
+
return "WARN";
|
|
2832
3259
|
const upper = raw.toUpperCase();
|
|
2833
3260
|
if (upper === "WARN" || upper === "WARNING")
|
|
2834
3261
|
return "WARN";
|
|
@@ -2921,8 +3348,8 @@ async function loadAgentsWithIds(args) {
|
|
|
2921
3348
|
environment: optionValue(args, "--env"),
|
|
2922
3349
|
command: "dev"
|
|
2923
3350
|
});
|
|
2924
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2925
|
-
const remote = await new BroodsSyncClient({
|
|
3351
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
3352
|
+
const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
|
|
2926
3353
|
const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
|
|
2927
3354
|
name: resource.name,
|
|
2928
3355
|
config: resource.config ?? {},
|
|
@@ -2954,10 +3381,10 @@ async function agentGet(name, args) {
|
|
|
2954
3381
|
const config = agent.config;
|
|
2955
3382
|
const sandbox = typeof config.sandbox === "string" ? config.sandbox : undefined;
|
|
2956
3383
|
const workspaces = Array.isArray(config.workspaces) ? config.workspaces.map((ref) => typeof ref === "string" ? ref : ref.name).filter(Boolean) : [];
|
|
2957
|
-
const tools =
|
|
2958
|
-
const channels =
|
|
2959
|
-
const subagents =
|
|
2960
|
-
const webhooks =
|
|
3384
|
+
const tools = isPlainObject(config.tools) ? Object.keys(config.tools) : [];
|
|
3385
|
+
const channels = isPlainObject(config.channels) ? Object.keys(config.channels) : [];
|
|
3386
|
+
const subagents = isPlainObject(config.subagent) && Array.isArray(config.subagent.allowed) ? config.subagent.allowed.map((entry) => typeof entry === "string" ? entry : entry.name).filter(Boolean) : [];
|
|
3387
|
+
const webhooks = isPlainObject(config.hooks) && Array.isArray(config.hooks.webhooks) ? config.hooks.webhooks.filter(isPlainObject) : [];
|
|
2961
3388
|
console.log(`Agent: ${agent.name}`);
|
|
2962
3389
|
console.log(` Project/Env: ${manifest.project}/${manifest.environment}`);
|
|
2963
3390
|
console.log(` Deployed id: ${agent.agentId ?? "not deployed"}`);
|
|
@@ -2978,16 +3405,13 @@ async function agentGet(name, args) {
|
|
|
2978
3405
|
}
|
|
2979
3406
|
}
|
|
2980
3407
|
function agentModelLabel(config) {
|
|
2981
|
-
const model =
|
|
3408
|
+
const model = isPlainObject(config.model) ? config.model : {};
|
|
2982
3409
|
const provider = typeof model.provider === "string" ? model.provider : undefined;
|
|
2983
3410
|
const modelId = typeof model.modelId === "string" ? model.modelId : undefined;
|
|
2984
3411
|
if (provider && modelId)
|
|
2985
3412
|
return `${provider}/${modelId}`;
|
|
2986
3413
|
return modelId ?? provider ?? "unconfigured model";
|
|
2987
3414
|
}
|
|
2988
|
-
function isRecord(value) {
|
|
2989
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2990
|
-
}
|
|
2991
3415
|
async function run(args) {
|
|
2992
3416
|
const [agentName, ...promptParts] = args.filter((arg) => !arg.startsWith("--"));
|
|
2993
3417
|
if (!agentName || promptParts.length === 0) {
|
|
@@ -2998,18 +3422,22 @@ async function run(args) {
|
|
|
2998
3422
|
environment: optionValue(args, "--env"),
|
|
2999
3423
|
command: "dev"
|
|
3000
3424
|
});
|
|
3001
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
3425
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
3002
3426
|
const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
|
|
3003
3427
|
if (!agent)
|
|
3004
3428
|
throw new Error(`Unknown local agent: ${agentName}`);
|
|
3005
|
-
const remote = await new BroodsSyncClient({
|
|
3429
|
+
const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
|
|
3006
3430
|
const agentId = remote?.ids.agents[agentName];
|
|
3007
3431
|
if (!agentId)
|
|
3008
|
-
throw new Error(`Agent ${agentName} is not
|
|
3432
|
+
throw new Error(`Agent ${agentName} is not synced yet. Run broods dev --once or broods deploy first.`);
|
|
3433
|
+
const runtimeKey = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
|
|
3434
|
+
if (runtimeKey?.apiKey) {
|
|
3435
|
+
await writeEnvValue("BROODS_API_KEY", runtimeKey.apiKey);
|
|
3436
|
+
}
|
|
3009
3437
|
if (agent.config.publicAccess !== true) {
|
|
3010
3438
|
printWarning(`\u26A0 Agent "${agentName}" does not set publicAccess: true. The public endpoint is secured by default; ` + "if the deployed agent has not enabled it, this run will be refused.");
|
|
3011
3439
|
}
|
|
3012
|
-
const client = new BroodsClient;
|
|
3440
|
+
const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
|
|
3013
3441
|
const state = createRenderState();
|
|
3014
3442
|
try {
|
|
3015
3443
|
for await (const part of client.stream({
|
|
@@ -3017,7 +3445,10 @@ async function run(args) {
|
|
|
3017
3445
|
name: agentName,
|
|
3018
3446
|
id: agentId,
|
|
3019
3447
|
project: manifest.project,
|
|
3020
|
-
environment: manifest.environment
|
|
3448
|
+
environment: manifest.environment,
|
|
3449
|
+
...runtimeKey?.endpointId ? { endpointId: runtimeKey.endpointId } : {},
|
|
3450
|
+
...runtimeKey?.projectSlug ? { projectSlug: runtimeKey.projectSlug } : {},
|
|
3451
|
+
...runtimeKey?.environmentSlug ? { environmentSlug: runtimeKey.environmentSlug } : {}
|
|
3021
3452
|
}, { input: promptParts.join(" ") })) {
|
|
3022
3453
|
renderStreamPart(part, state);
|
|
3023
3454
|
}
|
|
@@ -3062,7 +3493,8 @@ async function writeLocalEnvDefaults(options) {
|
|
|
3062
3493
|
const nextValues = {
|
|
3063
3494
|
BROODS_DASHBOARD_URL: options.dashboardUrl,
|
|
3064
3495
|
BROODS_PROJECT: options.project,
|
|
3065
|
-
BROODS_ENVIRONMENT: options.environment
|
|
3496
|
+
BROODS_ENVIRONMENT: options.environment,
|
|
3497
|
+
BROODS_REGION: options.region
|
|
3066
3498
|
};
|
|
3067
3499
|
const lines = current ? current.replace(/\n?$/, `
|
|
3068
3500
|
`).split(/\n/) : [
|
|
@@ -3155,7 +3587,7 @@ async function collectSourceFiles(dir, files) {
|
|
|
3155
3587
|
await collectSourceFiles(full, files);
|
|
3156
3588
|
continue;
|
|
3157
3589
|
}
|
|
3158
|
-
if (entry.isFile() && entry.name.endsWith(".ts"))
|
|
3590
|
+
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".md")))
|
|
3159
3591
|
files.push(full);
|
|
3160
3592
|
}
|
|
3161
3593
|
}
|