broods 0.3.0 → 0.4.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/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  // @bun
2
2
  // src/account.ts
3
3
  var DEFAULT_ACCOUNT_BASE_URL = "https://gateway.broods.app";
4
+ var ACCOUNT_ENV_VAR_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
5
+ function envPlaceholder(name) {
6
+ if (!ACCOUNT_ENV_VAR_NAME_PATTERN.test(name) || name.length > 64) {
7
+ throw new Error("envPlaceholder name must match /^[A-Z][A-Z0-9_]*$/ and be at most 64 characters.");
8
+ }
9
+ return `\${${name}}`;
10
+ }
4
11
 
5
12
  class BroodsAccountApiError extends Error {
6
13
  status;
@@ -73,6 +80,17 @@ class BroodsAccountClient {
73
80
  const result = await this.request("DELETE", `/v1/agents/${encodeURIComponent(agentId)}`);
74
81
  return result?.deleted ?? false;
75
82
  }
83
+ async listEnvVars() {
84
+ const result = await this.request("GET", "/v1/env");
85
+ return result?.env ?? [];
86
+ }
87
+ async setEnvVar(name, value) {
88
+ await this.request("PUT", `/v1/env/${encodeURIComponent(name)}`, { value });
89
+ }
90
+ async deleteEnvVar(name) {
91
+ const result = await this.request("DELETE", `/v1/env/${encodeURIComponent(name)}`);
92
+ return result?.deleted ?? false;
93
+ }
76
94
  async listCrons() {
77
95
  const result = await this.request("GET", "/v1/crons");
78
96
  return result?.crons ?? [];
@@ -337,6 +355,9 @@ import { dirname, join } from "path";
337
355
  var PROJECT_DIR = "broods";
338
356
  var GENERATED_DIR = "_generated";
339
357
  var USER_CONFIG_PATH = join(homedir(), ".broods", "config.json");
358
+ var BROODS_APEX_DOMAIN = "broods.app";
359
+ var DASHBOARD_HOST_PREFIX = "dashboard";
360
+ var GATEWAY_HOST_PREFIX = "gateway";
340
361
  async function readStoredAuth() {
341
362
  const envToken = process.env.BROODS_TOKEN;
342
363
  const envConvexUrl = process.env.BROODS_BASE_URL;
@@ -364,6 +385,20 @@ async function writeStoredAuth(config) {
364
385
  function stripTrailingSlash(value) {
365
386
  return value.replace(/\/+$/, "");
366
387
  }
388
+ function gatewayUrlForDashboard(dashboardUrl) {
389
+ try {
390
+ const url = new URL(dashboardUrl);
391
+ if (!url.hostname.startsWith(`${DASHBOARD_HOST_PREFIX}.`))
392
+ return;
393
+ const rest = url.hostname.slice(DASHBOARD_HOST_PREFIX.length + 1);
394
+ if (rest !== BROODS_APEX_DOMAIN && !rest.endsWith(`.${BROODS_APEX_DOMAIN}`))
395
+ return;
396
+ url.hostname = `${GATEWAY_HOST_PREFIX}.${rest}`;
397
+ return stripTrailingSlash(url.origin);
398
+ } catch {
399
+ return;
400
+ }
401
+ }
367
402
 
368
403
  // src/runtime-config.ts
369
404
  var envFileCache = new Map;
@@ -448,6 +483,15 @@ function readStoredAuthSync() {
448
483
  // src/client.ts
449
484
  var DEFAULT_CORE_BASE_URL = "https://gateway.broods.app";
450
485
 
486
+ class IngressAcceptedError extends Error {
487
+ accepted;
488
+ constructor(accepted) {
489
+ super(`Ingress ${accepted.eventId} was accepted asynchronously with status ${accepted.status ?? "queued"}`);
490
+ this.accepted = accepted;
491
+ this.name = "IngressAcceptedError";
492
+ }
493
+ }
494
+
451
495
  class BroodsClient {
452
496
  baseUrl;
453
497
  apiKey;
@@ -506,6 +550,9 @@ class BroodsClient {
506
550
  const response = await this.openStream(body, targetUrl);
507
551
  if (!response.ok)
508
552
  throw new Error(`Run failed: ${response.status} ${await response.text()}`);
553
+ if (response.status === 202) {
554
+ throw new IngressAcceptedError(normalizeAsyncAccepted(await response.json(), body));
555
+ }
509
556
  if (!response.body)
510
557
  throw new Error("Run response has no body");
511
558
  for await (const data of readSseStream(response.body)) {
@@ -563,7 +610,7 @@ class BroodsClient {
563
610
  if (options.signal?.aborted)
564
611
  throw new Error("Async status polling aborted.");
565
612
  const payload = await this.getAsyncStatus(status, options);
566
- if (payload.status === "awaiting_approval" || payload.status === "completed" || payload.status === "failed" || payload.status === "not_found") {
613
+ if (payload.status === "awaiting_approval" || payload.status === "completed" || payload.status === "failed" || payload.status === "expired" || payload.status === "not_found") {
567
614
  return payload;
568
615
  }
569
616
  await sleep(intervalMs, options.signal);
@@ -710,6 +757,8 @@ function directRunBody(input, prefix) {
710
757
  eventId,
711
758
  conversationKey: input.conversationKey ?? eventId,
712
759
  events: resolveRunEvents(input),
760
+ ...input.mode !== undefined ? { mode: input.mode } : {},
761
+ ...input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {},
713
762
  ...input.system !== undefined ? { system: input.system } : {},
714
763
  ...input.model !== undefined ? { model: input.model } : {}
715
764
  };
@@ -725,11 +774,16 @@ function normalizeAsyncAccepted(payload, requestBody) {
725
774
  const status = parseStatusUrl(statusUrl);
726
775
  if (!status.statusId)
727
776
  throw new Error("Async response statusUrl missing status id");
777
+ const eventId = typeof payload.eventId === "string" ? payload.eventId : status.statusId;
778
+ const acceptedStatus = payload.status;
779
+ const requestedMode = payload.requestedMode;
728
780
  return {
729
781
  statusUrl,
730
782
  statusId: status.statusId,
731
- eventId: status.statusId,
732
- agentId: status.agentId ?? requestBody.agentId
783
+ eventId,
784
+ agentId: status.agentId ?? requestBody.agentId,
785
+ ...acceptedStatus === "accepted" || acceptedStatus === "queued" || acceptedStatus === "applied" || acceptedStatus === "processing" ? { status: acceptedStatus } : {},
786
+ ...requestedMode === "reject" || requestedMode === "followup" || requestedMode === "collect" || requestedMode === "steer" ? { requestedMode } : {}
733
787
  };
734
788
  }
735
789
  function parseStatusUrl(statusUrl) {
@@ -862,6 +916,8 @@ class BroodsWebSocketClient {
862
916
  events: resolveRunEvents(input),
863
917
  sessionId: input.sessionId,
864
918
  ...input.eventId !== undefined ? { eventId: input.eventId } : {},
919
+ ...input.mode !== undefined ? { mode: input.mode } : {},
920
+ ...input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {},
865
921
  ...input.system !== undefined ? { system: input.system } : {},
866
922
  ...input.model !== undefined ? { model: input.model } : {}
867
923
  }));
@@ -872,16 +928,17 @@ class BroodsWebSocketClient {
872
928
  const payload = parseServerMessage(event.data);
873
929
  if (!payload)
874
930
  return;
875
- handlers.onMessage?.(payload);
876
- switch (payload.type) {
931
+ const effective = unwrapServerMessage(payload, handlers);
932
+ handlers.onMessage?.(effective);
933
+ switch (effective.type) {
877
934
  case "meta":
878
- handlers.onMeta?.(payload);
935
+ handlers.onMeta?.(effective);
879
936
  break;
880
937
  case "done":
881
938
  finish();
882
939
  break;
883
940
  case "error":
884
- fail(new Error(formatWireError(payload.error)));
941
+ fail(new Error(formatWireError(effective.error)));
885
942
  break;
886
943
  }
887
944
  };
@@ -897,6 +954,57 @@ class BroodsWebSocketClient {
897
954
  };
898
955
  return {
899
956
  url,
957
+ sendControl(message) {
958
+ if (socket.readyState !== WS_OPEN)
959
+ throw new Error("WebSocket is not open");
960
+ socket.send(JSON.stringify({ type: "control", ...message }));
961
+ },
962
+ close
963
+ };
964
+ }
965
+ attach(input, handlers = {}) {
966
+ const WebSocketImpl = this.resolveWebSocket();
967
+ const url = this.buildUrl(input);
968
+ const socket = new WebSocketImpl(url);
969
+ let closed = false;
970
+ const close = (code = 1000, reason = "client closed") => {
971
+ if (closed)
972
+ return;
973
+ closed = true;
974
+ if (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING)
975
+ socket.close(code, reason);
976
+ };
977
+ socket.onopen = () => socket.send(JSON.stringify({
978
+ type: "attach",
979
+ ...input,
980
+ signal: undefined,
981
+ agent: undefined
982
+ }));
983
+ socket.onmessage = (event) => {
984
+ if (typeof event.data !== "string")
985
+ return;
986
+ const payload = parseServerMessage(event.data);
987
+ if (!payload)
988
+ return;
989
+ const effective = unwrapServerMessage(payload, handlers);
990
+ handlers.onMessage?.(effective);
991
+ if (effective.type === "done")
992
+ handlers.onDone?.();
993
+ if (effective.type === "error")
994
+ handlers.onError?.(new Error(formatWireError(effective.error)));
995
+ };
996
+ socket.onerror = () => handlers.onError?.(webSocketAccessError(this.baseUrl));
997
+ socket.onclose = (event) => {
998
+ if (!closed && event.code !== 1000)
999
+ handlers.onError?.(new Error(event.reason || "WebSocket connection closed."));
1000
+ };
1001
+ return {
1002
+ url,
1003
+ sendControl(message) {
1004
+ if (socket.readyState !== WS_OPEN)
1005
+ throw new Error("WebSocket is not open");
1006
+ socket.send(JSON.stringify({ type: "control", ...message }));
1007
+ },
900
1008
  close
901
1009
  };
902
1010
  }
@@ -989,6 +1097,13 @@ function normalizeWebSocketServiceUrl(value) {
989
1097
  function webSocketAccessError(baseUrl) {
990
1098
  return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
991
1099
  }
1100
+ function unwrapServerMessage(payload, handlers) {
1101
+ if (payload.type !== "output")
1102
+ return payload;
1103
+ const output = payload;
1104
+ handlers.onOutput?.(output);
1105
+ return output.data;
1106
+ }
992
1107
  function parseServerMessage(data) {
993
1108
  try {
994
1109
  const value = JSON.parse(data);
@@ -1009,25 +1124,27 @@ function formatWireError(error) {
1009
1124
  var RESOURCE_MARKER = Symbol.for("broods.resource");
1010
1125
  var CONFIG_MARKER = Symbol.for("broods.config");
1011
1126
  var CHANNEL_MARKER = Symbol.for("broods.channel");
1012
- var env = new Proxy(function env2(name) {
1127
+ var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
1128
+ var env = new Proxy((name) => {
1129
+ if (!ENV_NAME_PATTERN.test(name)) {
1130
+ throw new Error("env name must match /^[A-Z][A-Z0-9_]*$/ and be at most 64 characters.");
1131
+ }
1013
1132
  return { __beeblastEnv: true, name };
1014
1133
  }, {
1015
1134
  get(target, property, receiver) {
1016
- if (typeof property === "string")
1017
- return { __beeblastEnv: true, name: property };
1135
+ if (typeof property === "string" && ENV_NAME_PATTERN.test(property)) {
1136
+ throw new Error(`env.${property} is not supported; use env("${property}")`);
1137
+ }
1018
1138
  return Reflect.get(target, property, receiver);
1019
1139
  }
1020
1140
  });
1021
- function defineResource(kind, input) {
1022
- if (input.config === undefined) {
1023
- throw new Error(`Resource "${input.name}" must include config`);
1024
- }
1141
+ function defineResource(kind, name, description, config) {
1025
1142
  return {
1026
1143
  [RESOURCE_MARKER]: true,
1027
1144
  kind,
1028
- name: input.name,
1029
- ...input.description ? { description: input.description } : {},
1030
- config: input.config
1145
+ name,
1146
+ ...description ? { description } : {},
1147
+ config
1031
1148
  };
1032
1149
  }
1033
1150
  function defineChannel(type, config) {
@@ -1065,25 +1182,32 @@ function defineBroods(config) {
1065
1182
  return { [CONFIG_MARKER]: true, config };
1066
1183
  }
1067
1184
  function defineAgent(input) {
1068
- return defineResource("agent", input);
1185
+ const { name, description, ...config } = input;
1186
+ return defineResource("agent", name, description, config);
1069
1187
  }
1070
1188
  function defineWorkspace(input) {
1071
- return defineResource("workspace", input);
1189
+ const { name, description, ...config } = input;
1190
+ return defineResource("workspace", name, description, config);
1072
1191
  }
1073
1192
  function defineSandbox(input) {
1074
- return defineResource("sandbox", input);
1193
+ const { name, description, ...config } = input;
1194
+ return defineResource("sandbox", name, description, config);
1075
1195
  }
1076
1196
  function defineSkill(input) {
1077
- return defineResource("skill", input);
1197
+ const { name, description, ...config } = input;
1198
+ return defineResource("skill", name, description, config);
1078
1199
  }
1079
1200
  function defineTool(input) {
1080
- return defineResource("tool", input);
1201
+ const { name, ...config } = input;
1202
+ return defineResource("tool", name, undefined, config);
1081
1203
  }
1082
1204
  function definePolicy(input) {
1083
- return defineResource("policy", input);
1205
+ const { name, description, ...config } = input;
1206
+ return defineResource("policy", name, description, config);
1084
1207
  }
1085
1208
  function defineCron(input) {
1086
- return defineResource("cron", input);
1209
+ const { name, description, ...config } = input;
1210
+ return defineResource("cron", name, description, config);
1087
1211
  }
1088
1212
  function isResource(value) {
1089
1213
  return Boolean(value && typeof value === "object" && value[RESOURCE_MARKER]);
@@ -1103,6 +1227,7 @@ export {
1103
1227
  isObservabilityClientMessage,
1104
1228
  isChannelDefinition,
1105
1229
  isBroodsConfig,
1230
+ envPlaceholder,
1106
1231
  env,
1107
1232
  defineZaloChannel,
1108
1233
  defineWorkspace,
@@ -1121,6 +1246,7 @@ export {
1121
1246
  BroodsWebSocketClient as WebsocketClient,
1122
1247
  BroodsWebSocketClient as WebSocketClient,
1123
1248
  MAX_OBSERVABILITY_BACKFILL,
1249
+ IngressAcceptedError,
1124
1250
  DEFAULT_CORE_BASE_URL,
1125
1251
  BroodsWebSocketClient,
1126
1252
  BroodsClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "broods",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI + TypeScript SDK for the broods agent platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,16 +50,16 @@
50
50
  "node": ">=22"
51
51
  },
52
52
  "dependencies": {
53
- "@chat-adapter/discord": "^4.31.0",
54
- "@chat-adapter/github": "^4.31.0",
55
- "@chat-adapter/slack": "^4.31.0",
56
- "@chat-adapter/telegram": "^4.31.0",
57
- "ai": "^7.0.11"
53
+ "@chat-adapter/discord": "^4.35.0",
54
+ "@chat-adapter/github": "^4.35.0",
55
+ "@chat-adapter/slack": "^4.35.0",
56
+ "@chat-adapter/telegram": "^4.35.0",
57
+ "ai": "^7.0.37"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.3.14",
61
- "rollup": "^4.53.3",
61
+ "rollup": "^4.62.2",
62
62
  "typescript": "^6.0.3",
63
- "rollup-plugin-dts": "^6.3.0"
63
+ "rollup-plugin-dts": "^6.4.1"
64
64
  }
65
65
  }