broods 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +530 -152
- package/dist/index.d.ts +417 -88
- package/dist/index.js +54 -38
- package/package.json +9 -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,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
|
}
|
|
@@ -2230,7 +2547,7 @@ function renderStreamPart(part, state) {
|
|
|
2230
2547
|
// package.json
|
|
2231
2548
|
var package_default = {
|
|
2232
2549
|
name: "broods",
|
|
2233
|
-
version: "0.
|
|
2550
|
+
version: "0.2.0",
|
|
2234
2551
|
description: "CLI + TypeScript SDK for the broods agent platform.",
|
|
2235
2552
|
type: "module",
|
|
2236
2553
|
license: "MIT",
|
|
@@ -2272,8 +2589,15 @@ var package_default = {
|
|
|
2272
2589
|
"pack:check": "bun run build && npm pack --dry-run",
|
|
2273
2590
|
test: "bun test"
|
|
2274
2591
|
},
|
|
2592
|
+
engines: {
|
|
2593
|
+
node: ">=22"
|
|
2594
|
+
},
|
|
2275
2595
|
dependencies: {
|
|
2276
|
-
|
|
2596
|
+
"@chat-adapter/discord": "^4.31.0",
|
|
2597
|
+
"@chat-adapter/github": "^4.31.0",
|
|
2598
|
+
"@chat-adapter/slack": "^4.31.0",
|
|
2599
|
+
"@chat-adapter/telegram": "^4.31.0",
|
|
2600
|
+
ai: "^7.0.11"
|
|
2277
2601
|
},
|
|
2278
2602
|
devDependencies: {
|
|
2279
2603
|
"@types/bun": "^1.3.14",
|
|
@@ -2285,6 +2609,12 @@ var package_default = {
|
|
|
2285
2609
|
// src/cli/index.ts
|
|
2286
2610
|
var VERSION = package_default.version;
|
|
2287
2611
|
var DEFAULT_DASHBOARD_URL = "https://dashboard.broods.app";
|
|
2612
|
+
var DEFAULT_SERVICE_REGION = "eu-west-1";
|
|
2613
|
+
var SERVICE_REGIONS = [
|
|
2614
|
+
{ region: "eu-west-1", label: "eu-west-1 (Ireland)" },
|
|
2615
|
+
{ region: "us-east-1", label: "us-east-1 (US East)" },
|
|
2616
|
+
{ region: "ap-southeast-1", label: "ap-southeast-1 (Singapore)" }
|
|
2617
|
+
];
|
|
2288
2618
|
var HELP = `broods v${VERSION}
|
|
2289
2619
|
|
|
2290
2620
|
Usage: broods <command>
|
|
@@ -2310,13 +2640,15 @@ Commands:
|
|
|
2310
2640
|
run <agent> <prompt> Run an agent once and pretty-stream the result (thinking, tool calls, text)
|
|
2311
2641
|
|
|
2312
2642
|
Options:
|
|
2313
|
-
--dashboard-url <url> Dashboard base URL (default: ${DEFAULT_DASHBOARD_URL})
|
|
2643
|
+
--dashboard-url <url> Dashboard base URL for login and deep links (default: ${DEFAULT_DASHBOARD_URL})
|
|
2644
|
+
--base-url <url> broods API base URL for sync/env calls (default: discovered at login)
|
|
2314
2645
|
--project <name> Project name override (default: package name or folder)
|
|
2315
2646
|
--env <name> Target environment override
|
|
2647
|
+
--region <region> Broods service region preference (default: ${DEFAULT_SERVICE_REGION})
|
|
2316
2648
|
--prune Allow deploy to delete undeclared remote resources
|
|
2317
2649
|
--rotate-key Mint a fresh runtime API key on deploy and write it to .env.local
|
|
2318
|
-
--errors Show WARN/ERROR only (
|
|
2319
|
-
--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)
|
|
2320
2652
|
-n, --limit <n> Backfill line count (with \`logs\`, default 100)
|
|
2321
2653
|
--json Print logs as raw JSON (with \`logs\`, applies to backfill output)
|
|
2322
2654
|
--force Allow init to overwrite starter files`;
|
|
@@ -2380,6 +2712,7 @@ async function init(args) {
|
|
|
2380
2712
|
dashboardUrl: optionValue(args, "--dashboard-url") ?? DEFAULT_DASHBOARD_URL,
|
|
2381
2713
|
project: optionValue(args, "--project") ?? inferProjectName(process.cwd()),
|
|
2382
2714
|
environment: optionValue(args, "--env") ?? "development",
|
|
2715
|
+
region: optionValue(args, "--region") ?? DEFAULT_SERVICE_REGION,
|
|
2383
2716
|
force
|
|
2384
2717
|
});
|
|
2385
2718
|
console.log(`Created ${PROJECT_DIR}/`);
|
|
@@ -2395,9 +2728,10 @@ async function login(args) {
|
|
|
2395
2728
|
const project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
|
|
2396
2729
|
const environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
|
|
2397
2730
|
await writeLocalEnvDefaults({
|
|
2398
|
-
dashboardUrl: auth.dashboardUrl,
|
|
2731
|
+
dashboardUrl: auth.dashboardUrl ?? dashboardUrl,
|
|
2399
2732
|
project,
|
|
2400
2733
|
environment,
|
|
2734
|
+
region: optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION,
|
|
2401
2735
|
force: false
|
|
2402
2736
|
});
|
|
2403
2737
|
const user = auth.user?.email || auth.user?.name || auth.user?.authId;
|
|
@@ -2410,11 +2744,11 @@ async function login(args) {
|
|
|
2410
2744
|
console.log(`Org: ${org}`);
|
|
2411
2745
|
if (account)
|
|
2412
2746
|
console.log(`Account: ${account}`);
|
|
2413
|
-
await writeRuntimeKeyForLogin(auth.
|
|
2747
|
+
await writeRuntimeKeyForLogin(auth.baseUrl, auth.token, project, environment);
|
|
2414
2748
|
}
|
|
2415
|
-
async function writeRuntimeKeyForLogin(
|
|
2749
|
+
async function writeRuntimeKeyForLogin(baseUrl, token, project, environment) {
|
|
2416
2750
|
try {
|
|
2417
|
-
const client = new BroodsSyncClient({
|
|
2751
|
+
const client = new BroodsSyncClient({ baseUrl, token });
|
|
2418
2752
|
const key = await client.getRuntimeKey(project, environment);
|
|
2419
2753
|
if (key?.apiKey) {
|
|
2420
2754
|
await writeEnvValue("BROODS_API_KEY", key.apiKey);
|
|
@@ -2428,8 +2762,8 @@ async function diff(args) {
|
|
|
2428
2762
|
environment: optionValue(args, "--env"),
|
|
2429
2763
|
command: "dev"
|
|
2430
2764
|
});
|
|
2431
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2432
|
-
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 });
|
|
2433
2767
|
const remote = await client.getManifest(manifest.project, manifest.environment);
|
|
2434
2768
|
printDiffEntries(diffManifests(manifest, remote?.manifest ?? null));
|
|
2435
2769
|
}
|
|
@@ -2440,8 +2774,8 @@ async function deploy(args) {
|
|
|
2440
2774
|
command: "deploy",
|
|
2441
2775
|
useRuntimeEnvironment: false
|
|
2442
2776
|
});
|
|
2443
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2444
|
-
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 });
|
|
2445
2779
|
const result = await client.putManifest(manifest, hasFlag(args, "--prune"), hasFlag(args, "--rotate-key"));
|
|
2446
2780
|
await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
|
|
2447
2781
|
await ensureGitIgnore();
|
|
@@ -2461,11 +2795,15 @@ async function applyDeploymentKey(deployment) {
|
|
|
2461
2795
|
}
|
|
2462
2796
|
function printSyncWarnings(result) {
|
|
2463
2797
|
const missing = result.warnings?.missingEnv ?? [];
|
|
2464
|
-
if (missing.length
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2798
|
+
if (missing.length > 0) {
|
|
2799
|
+
printWarning(`\u26A0 ${missing.length} env var(s) referenced in agent config but not set: ${missing.join(", ")}`);
|
|
2800
|
+
for (const name of missing)
|
|
2801
|
+
console.log(` broods env set ${name}`);
|
|
2802
|
+
}
|
|
2803
|
+
const missingPolicies = result.warnings?.missingPolicies ?? [];
|
|
2804
|
+
if (missingPolicies.length > 0) {
|
|
2805
|
+
printWarning(`\u26A0 ${missingPolicies.length} policy ref(s) in agent config match no policy resource ` + `in this deploy and will be ignored at runtime: ${missingPolicies.join(", ")}`);
|
|
2806
|
+
}
|
|
2469
2807
|
}
|
|
2470
2808
|
async function dev(args) {
|
|
2471
2809
|
await ensureDevOnboarding(args);
|
|
@@ -2482,7 +2820,8 @@ async function dev(args) {
|
|
|
2482
2820
|
const childEnv = {
|
|
2483
2821
|
...process.env,
|
|
2484
2822
|
BROODS_DECLINED_FILE: declinedFile,
|
|
2485
|
-
BROODS_SUPPRESS_DEV_TARGET: "1"
|
|
2823
|
+
BROODS_SUPPRESS_DEV_TARGET: "1",
|
|
2824
|
+
BROODS_RELOAD_ENV: "1"
|
|
2486
2825
|
};
|
|
2487
2826
|
await printDevTarget(args);
|
|
2488
2827
|
await runSyncChild(args, childEnv);
|
|
@@ -2528,7 +2867,7 @@ async function streamDevLogs(args, signal) {
|
|
|
2528
2867
|
try {
|
|
2529
2868
|
creds = resolveObservabilityCredentials();
|
|
2530
2869
|
} catch {
|
|
2531
|
-
console.log("\xB7 live logs off \u2014
|
|
2870
|
+
console.log("\xB7 live logs off \u2014 no runtime key found for this environment yet. Run `broods dev --once` after login to create or reconnect it.");
|
|
2532
2871
|
return;
|
|
2533
2872
|
}
|
|
2534
2873
|
let project;
|
|
@@ -2573,27 +2912,25 @@ async function ensureLocalDevDefaults(args) {
|
|
|
2573
2912
|
const missing = [
|
|
2574
2913
|
"BROODS_DASHBOARD_URL",
|
|
2575
2914
|
"BROODS_PROJECT",
|
|
2576
|
-
"BROODS_ENVIRONMENT"
|
|
2915
|
+
"BROODS_ENVIRONMENT",
|
|
2916
|
+
"BROODS_REGION"
|
|
2577
2917
|
].filter((key) => values[key] === undefined);
|
|
2578
2918
|
if (missing.length === 0)
|
|
2579
2919
|
return;
|
|
2580
2920
|
const needsProject = values.BROODS_PROJECT === undefined;
|
|
2581
2921
|
const needsEnvironment = values.BROODS_ENVIRONMENT === undefined;
|
|
2922
|
+
const needsRegion = values.BROODS_REGION === undefined;
|
|
2582
2923
|
const runtime = loadBroodsRuntimeConfig();
|
|
2583
2924
|
const dashboardUrl = optionValue(args, "--dashboard-url") ?? runtime.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
|
|
2584
2925
|
let project = optionValue(args, "--project") ?? process.env.BROODS_PROJECT ?? inferProjectName(process.cwd());
|
|
2585
2926
|
let environment = optionValue(args, "--env") ?? process.env.BROODS_ENVIRONMENT ?? "development";
|
|
2927
|
+
let region = optionValue(args, "--region") ?? process.env.BROODS_REGION ?? DEFAULT_SERVICE_REGION;
|
|
2586
2928
|
if (process.stdin.isTTY && needsProject) {
|
|
2587
2929
|
const auth = await requireAuthOrLogin(dashboardUrl);
|
|
2588
|
-
const client = new BroodsSyncClient({
|
|
2930
|
+
const client = new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token });
|
|
2589
2931
|
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));
|
|
2932
|
+
const selectedContext = await selectOnboardingOrg(client, context);
|
|
2933
|
+
project = await selectOnboardingProject(selectedContext, project);
|
|
2597
2934
|
if (!project.trim())
|
|
2598
2935
|
throw new Error("Project name is required.");
|
|
2599
2936
|
}
|
|
@@ -2602,16 +2939,20 @@ async function ensureLocalDevDefaults(args) {
|
|
|
2602
2939
|
if (!environment.trim())
|
|
2603
2940
|
throw new Error("Environment is required.");
|
|
2604
2941
|
}
|
|
2942
|
+
if (process.stdin.isTTY && needsRegion) {
|
|
2943
|
+
region = await promptSelect("Select service region", [...SERVICE_REGIONS], (entry) => entry.label).then((entry) => entry.region);
|
|
2944
|
+
}
|
|
2605
2945
|
await writeLocalEnvDefaults({
|
|
2606
2946
|
dashboardUrl,
|
|
2607
2947
|
project,
|
|
2608
2948
|
environment,
|
|
2949
|
+
region,
|
|
2609
2950
|
force: false
|
|
2610
2951
|
});
|
|
2611
2952
|
}
|
|
2612
2953
|
async function requireAuthOrLogin(dashboardUrl) {
|
|
2613
2954
|
try {
|
|
2614
|
-
return await requireAuth(
|
|
2955
|
+
return await requireAuth();
|
|
2615
2956
|
} catch (error) {
|
|
2616
2957
|
if (!process.stdin.isTTY)
|
|
2617
2958
|
throw error;
|
|
@@ -2643,10 +2984,37 @@ function formatOrgChoice(org) {
|
|
|
2643
2984
|
const suffix = org.role === "owner" || org.role === "admin" ? org.role : "member";
|
|
2644
2985
|
return `${org.name} (${org.slug}, ${suffix})`;
|
|
2645
2986
|
}
|
|
2987
|
+
async function selectOnboardingOrg(client, context) {
|
|
2988
|
+
const activeOrgs = context.orgs.filter((org) => org.accountStatus === "active");
|
|
2989
|
+
const createNew = { kind: "create", name: "New organization" };
|
|
2990
|
+
const selected = await promptSelect("Select organization", [...activeOrgs, createNew], (entry) => ("kind" in entry) ? "Create new organization" : formatOrgChoice(entry));
|
|
2991
|
+
if ("kind" in selected) {
|
|
2992
|
+
const name = await promptText("Organization name", inferProjectName(process.cwd()));
|
|
2993
|
+
if (!name.trim())
|
|
2994
|
+
throw new Error("Organization name is required.");
|
|
2995
|
+
return await client.createOnboardingOrg(name);
|
|
2996
|
+
}
|
|
2997
|
+
return selected.id === context.currentOrgId ? context : await client.selectOnboardingOrg(selected.id);
|
|
2998
|
+
}
|
|
2646
2999
|
function defaultProjectName(context, inferred) {
|
|
2647
3000
|
const exact = context.projects.find((project) => project.name === inferred || project.slug === inferred);
|
|
2648
3001
|
return exact?.name ?? inferred;
|
|
2649
3002
|
}
|
|
3003
|
+
async function selectOnboardingProject(context, inferred) {
|
|
3004
|
+
if (context.projects.length === 0) {
|
|
3005
|
+
return promptText("Project name", inferred);
|
|
3006
|
+
}
|
|
3007
|
+
const createNew = { kind: "create", name: defaultProjectName(context, inferred), slug: "" };
|
|
3008
|
+
const choices = [
|
|
3009
|
+
...context.projects,
|
|
3010
|
+
createNew
|
|
3011
|
+
];
|
|
3012
|
+
const selected = await promptSelect("Select project", choices, (entry) => ("kind" in entry) && entry.kind === "create" ? `Create new project (${entry.name})` : `${entry.name} (${entry.slug})`);
|
|
3013
|
+
if ("kind" in selected && selected.kind === "create") {
|
|
3014
|
+
return promptText("New project name", selected.name);
|
|
3015
|
+
}
|
|
3016
|
+
return selected.name;
|
|
3017
|
+
}
|
|
2650
3018
|
function runSyncChild(args, env3) {
|
|
2651
3019
|
return new Promise((resolvePromise, reject) => {
|
|
2652
3020
|
const entryPoint = process.argv[1] ?? "";
|
|
@@ -2670,12 +3038,12 @@ async function syncDev(args) {
|
|
|
2670
3038
|
environment: optionValue(args, "--env"),
|
|
2671
3039
|
command: "dev"
|
|
2672
3040
|
});
|
|
2673
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2674
|
-
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 });
|
|
2675
3043
|
const remote = await client.getManifest(manifest.project, manifest.environment);
|
|
2676
3044
|
const diff2 = diffManifests(manifest, remote?.manifest ?? null);
|
|
2677
3045
|
printDiffEntries(diff2.filter((entry) => entry.operation !== "delete"));
|
|
2678
|
-
await syncLocalEnvVars(client, manifest);
|
|
3046
|
+
await syncLocalEnvVars(client, manifest, envSyncScopeKey(auth.baseUrl, auth.token));
|
|
2679
3047
|
let result = await client.putManifest(manifest, false);
|
|
2680
3048
|
await writeGeneratedFiles(manifest, result.ids, process.cwd(), resourceAliases, result.deployment, channels);
|
|
2681
3049
|
await ensureGitIgnore();
|
|
@@ -2725,7 +3093,7 @@ function printChannelEndpoints(channels, result) {
|
|
|
2725
3093
|
console.log(`Channel ${channel.alias} (${channel.type}): ${url}${channel.type === "pancake" ? "?secret=<PANCAKE_WEBHOOK_SECRET>" : ""}`);
|
|
2726
3094
|
}
|
|
2727
3095
|
}
|
|
2728
|
-
async function syncLocalEnvVars(client, manifest) {
|
|
3096
|
+
async function syncLocalEnvVars(client, manifest, scopeKey) {
|
|
2729
3097
|
const present = collectEnvRefNames(manifest).filter((name) => {
|
|
2730
3098
|
const value = process.env[name];
|
|
2731
3099
|
return !name.startsWith("BROODS_") && value !== undefined && value !== "";
|
|
@@ -2733,19 +3101,23 @@ async function syncLocalEnvVars(client, manifest) {
|
|
|
2733
3101
|
if (present.length === 0)
|
|
2734
3102
|
return;
|
|
2735
3103
|
const cache = await loadEnvSyncCache();
|
|
2736
|
-
const known = cache[envCacheKey(manifest.project, manifest.environment)] ?? {};
|
|
2737
|
-
const
|
|
3104
|
+
const known = cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] ?? {};
|
|
3105
|
+
const remoteNames = new Set((await client.listEnv(manifest.project, manifest.environment)).map((entry) => entry.name));
|
|
3106
|
+
const changed = present.filter((name) => !remoteNames.has(name) || known[name] !== hashEnvValue(process.env[name]));
|
|
2738
3107
|
if (changed.length === 0)
|
|
2739
3108
|
return;
|
|
2740
3109
|
await Promise.all(changed.map((name) => client.setEnv(manifest.project, manifest.environment, name, process.env[name])));
|
|
2741
3110
|
for (const name of changed)
|
|
2742
3111
|
known[name] = hashEnvValue(process.env[name]);
|
|
2743
|
-
cache[envCacheKey(manifest.project, manifest.environment)] = known;
|
|
3112
|
+
cache[envCacheKey(scopeKey, manifest.project, manifest.environment)] = known;
|
|
2744
3113
|
await saveEnvSyncCache(cache);
|
|
2745
3114
|
printEnvSync(changed);
|
|
2746
3115
|
}
|
|
2747
|
-
function
|
|
2748
|
-
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}`;
|
|
2749
3121
|
}
|
|
2750
3122
|
function envSyncCachePath() {
|
|
2751
3123
|
return resolve4(dirname2(USER_CONFIG_PATH), "env-sync.json");
|
|
@@ -2770,15 +3142,15 @@ async function saveEnvSyncCache(cache) {
|
|
|
2770
3142
|
await writeFile3(path, `${JSON.stringify(cache, null, 2)}
|
|
2771
3143
|
`, "utf8");
|
|
2772
3144
|
}
|
|
2773
|
-
async function rememberEnvSyncValue(project, environment, name, value) {
|
|
3145
|
+
async function rememberEnvSyncValue(scopeKey, project, environment, name, value) {
|
|
2774
3146
|
const cache = await loadEnvSyncCache();
|
|
2775
|
-
const key = envCacheKey(project, environment);
|
|
3147
|
+
const key = envCacheKey(scopeKey, project, environment);
|
|
2776
3148
|
cache[key] = { ...cache[key] ?? {}, [name]: hashEnvValue(value) };
|
|
2777
3149
|
await saveEnvSyncCache(cache);
|
|
2778
3150
|
}
|
|
2779
|
-
async function forgetEnvSyncValue(project, environment, name) {
|
|
3151
|
+
async function forgetEnvSyncValue(scopeKey, project, environment, name) {
|
|
2780
3152
|
const cache = await loadEnvSyncCache();
|
|
2781
|
-
const key = envCacheKey(project, environment);
|
|
3153
|
+
const key = envCacheKey(scopeKey, project, environment);
|
|
2782
3154
|
if (!cache[key] || !(name in cache[key]))
|
|
2783
3155
|
return;
|
|
2784
3156
|
delete cache[key][name];
|
|
@@ -2790,11 +3162,11 @@ async function printDevTarget(args) {
|
|
|
2790
3162
|
environment: optionValue(args, "--env"),
|
|
2791
3163
|
command: "dev"
|
|
2792
3164
|
});
|
|
2793
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
3165
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
2794
3166
|
printDeploymentTarget({
|
|
2795
3167
|
project: manifest.project,
|
|
2796
3168
|
environment: manifest.environment,
|
|
2797
|
-
dashboardUrl: auth.dashboardUrl
|
|
3169
|
+
dashboardUrl: auth.dashboardUrl ?? DEFAULT_DASHBOARD_URL
|
|
2798
3170
|
});
|
|
2799
3171
|
}
|
|
2800
3172
|
async function loadDeclinedDeletes() {
|
|
@@ -2833,8 +3205,9 @@ async function envCommand(args) {
|
|
|
2833
3205
|
environment: optionValue(args, "--env"),
|
|
2834
3206
|
command: "dev"
|
|
2835
3207
|
});
|
|
2836
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2837
|
-
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);
|
|
2838
3211
|
const target = `${manifest.project}/${manifest.environment}`;
|
|
2839
3212
|
if (isList) {
|
|
2840
3213
|
const variables = await client.listEnv(manifest.project, manifest.environment);
|
|
@@ -2859,13 +3232,13 @@ async function envCommand(args) {
|
|
|
2859
3232
|
}
|
|
2860
3233
|
if (isRemove) {
|
|
2861
3234
|
await client.removeEnv(manifest.project, manifest.environment, name);
|
|
2862
|
-
await forgetEnvSyncValue(manifest.project, manifest.environment, name);
|
|
3235
|
+
await forgetEnvSyncValue(scopeKey, manifest.project, manifest.environment, name);
|
|
2863
3236
|
console.log(`Removed ${name} from ${target}`);
|
|
2864
3237
|
return;
|
|
2865
3238
|
}
|
|
2866
3239
|
const value = await promptSecret(name);
|
|
2867
3240
|
await client.setEnv(manifest.project, manifest.environment, name, value);
|
|
2868
|
-
await rememberEnvSyncValue(manifest.project, manifest.environment, name, value);
|
|
3241
|
+
await rememberEnvSyncValue(scopeKey, manifest.project, manifest.environment, name, value);
|
|
2869
3242
|
console.log(`Stored ${name} for ${target}`);
|
|
2870
3243
|
}
|
|
2871
3244
|
function resolveObservabilityCredentials() {
|
|
@@ -2882,7 +3255,7 @@ function resolveMinLevel(args) {
|
|
|
2882
3255
|
return "WARN";
|
|
2883
3256
|
const raw = optionValue(args, "--level");
|
|
2884
3257
|
if (!raw)
|
|
2885
|
-
return;
|
|
3258
|
+
return "WARN";
|
|
2886
3259
|
const upper = raw.toUpperCase();
|
|
2887
3260
|
if (upper === "WARN" || upper === "WARNING")
|
|
2888
3261
|
return "WARN";
|
|
@@ -2975,8 +3348,8 @@ async function loadAgentsWithIds(args) {
|
|
|
2975
3348
|
environment: optionValue(args, "--env"),
|
|
2976
3349
|
command: "dev"
|
|
2977
3350
|
});
|
|
2978
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
2979
|
-
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);
|
|
2980
3353
|
const agents = manifest.resources.filter((resource) => resource.kind === "agent").map((resource) => ({
|
|
2981
3354
|
name: resource.name,
|
|
2982
3355
|
config: resource.config ?? {},
|
|
@@ -3008,10 +3381,10 @@ async function agentGet(name, args) {
|
|
|
3008
3381
|
const config = agent.config;
|
|
3009
3382
|
const sandbox = typeof config.sandbox === "string" ? config.sandbox : undefined;
|
|
3010
3383
|
const workspaces = Array.isArray(config.workspaces) ? config.workspaces.map((ref) => typeof ref === "string" ? ref : ref.name).filter(Boolean) : [];
|
|
3011
|
-
const tools =
|
|
3012
|
-
const channels =
|
|
3013
|
-
const subagents =
|
|
3014
|
-
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) : [];
|
|
3015
3388
|
console.log(`Agent: ${agent.name}`);
|
|
3016
3389
|
console.log(` Project/Env: ${manifest.project}/${manifest.environment}`);
|
|
3017
3390
|
console.log(` Deployed id: ${agent.agentId ?? "not deployed"}`);
|
|
@@ -3032,16 +3405,13 @@ async function agentGet(name, args) {
|
|
|
3032
3405
|
}
|
|
3033
3406
|
}
|
|
3034
3407
|
function agentModelLabel(config) {
|
|
3035
|
-
const model =
|
|
3408
|
+
const model = isPlainObject(config.model) ? config.model : {};
|
|
3036
3409
|
const provider = typeof model.provider === "string" ? model.provider : undefined;
|
|
3037
3410
|
const modelId = typeof model.modelId === "string" ? model.modelId : undefined;
|
|
3038
3411
|
if (provider && modelId)
|
|
3039
3412
|
return `${provider}/${modelId}`;
|
|
3040
3413
|
return modelId ?? provider ?? "unconfigured model";
|
|
3041
3414
|
}
|
|
3042
|
-
function isRecord(value) {
|
|
3043
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3044
|
-
}
|
|
3045
3415
|
async function run(args) {
|
|
3046
3416
|
const [agentName, ...promptParts] = args.filter((arg) => !arg.startsWith("--"));
|
|
3047
3417
|
if (!agentName || promptParts.length === 0) {
|
|
@@ -3052,18 +3422,22 @@ async function run(args) {
|
|
|
3052
3422
|
environment: optionValue(args, "--env"),
|
|
3053
3423
|
command: "dev"
|
|
3054
3424
|
});
|
|
3055
|
-
const auth = await requireAuth(optionValue(args, "--
|
|
3425
|
+
const auth = await requireAuth(optionValue(args, "--base-url") ?? config.baseUrl);
|
|
3056
3426
|
const agent = manifest.resources.find((resource) => resource.kind === "agent" && resource.name === agentName);
|
|
3057
3427
|
if (!agent)
|
|
3058
3428
|
throw new Error(`Unknown local agent: ${agentName}`);
|
|
3059
|
-
const remote = await new BroodsSyncClient({
|
|
3429
|
+
const remote = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getManifest(manifest.project, manifest.environment);
|
|
3060
3430
|
const agentId = remote?.ids.agents[agentName];
|
|
3061
3431
|
if (!agentId)
|
|
3062
|
-
throw new Error(`Agent ${agentName} is not
|
|
3432
|
+
throw new Error(`Agent ${agentName} is not synced yet. Run broods dev --once or broods deploy first.`);
|
|
3433
|
+
const runtimeKey = await new BroodsSyncClient({ baseUrl: auth.baseUrl, token: auth.token }).getRuntimeKey(manifest.project, manifest.environment).catch(() => null);
|
|
3434
|
+
if (runtimeKey?.apiKey) {
|
|
3435
|
+
await writeEnvValue("BROODS_API_KEY", runtimeKey.apiKey);
|
|
3436
|
+
}
|
|
3063
3437
|
if (agent.config.publicAccess !== true) {
|
|
3064
3438
|
printWarning(`\u26A0 Agent "${agentName}" does not set publicAccess: true. The public endpoint is secured by default; ` + "if the deployed agent has not enabled it, this run will be refused.");
|
|
3065
3439
|
}
|
|
3066
|
-
const client = new BroodsClient;
|
|
3440
|
+
const client = new BroodsClient(runtimeKey?.apiKey ? { apiKey: runtimeKey.apiKey } : {});
|
|
3067
3441
|
const state = createRenderState();
|
|
3068
3442
|
try {
|
|
3069
3443
|
for await (const part of client.stream({
|
|
@@ -3071,7 +3445,10 @@ async function run(args) {
|
|
|
3071
3445
|
name: agentName,
|
|
3072
3446
|
id: agentId,
|
|
3073
3447
|
project: manifest.project,
|
|
3074
|
-
environment: manifest.environment
|
|
3448
|
+
environment: manifest.environment,
|
|
3449
|
+
...runtimeKey?.endpointId ? { endpointId: runtimeKey.endpointId } : {},
|
|
3450
|
+
...runtimeKey?.projectSlug ? { projectSlug: runtimeKey.projectSlug } : {},
|
|
3451
|
+
...runtimeKey?.environmentSlug ? { environmentSlug: runtimeKey.environmentSlug } : {}
|
|
3075
3452
|
}, { input: promptParts.join(" ") })) {
|
|
3076
3453
|
renderStreamPart(part, state);
|
|
3077
3454
|
}
|
|
@@ -3116,7 +3493,8 @@ async function writeLocalEnvDefaults(options) {
|
|
|
3116
3493
|
const nextValues = {
|
|
3117
3494
|
BROODS_DASHBOARD_URL: options.dashboardUrl,
|
|
3118
3495
|
BROODS_PROJECT: options.project,
|
|
3119
|
-
BROODS_ENVIRONMENT: options.environment
|
|
3496
|
+
BROODS_ENVIRONMENT: options.environment,
|
|
3497
|
+
BROODS_REGION: options.region
|
|
3120
3498
|
};
|
|
3121
3499
|
const lines = current ? current.replace(/\n?$/, `
|
|
3122
3500
|
`).split(/\n/) : [
|
|
@@ -3209,7 +3587,7 @@ async function collectSourceFiles(dir, files) {
|
|
|
3209
3587
|
await collectSourceFiles(full, files);
|
|
3210
3588
|
continue;
|
|
3211
3589
|
}
|
|
3212
|
-
if (entry.isFile() && entry.name.endsWith(".ts"))
|
|
3590
|
+
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".md")))
|
|
3213
3591
|
files.push(full);
|
|
3214
3592
|
}
|
|
3215
3593
|
}
|