verybot 0.1.8

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.
Files changed (277) hide show
  1. package/README.md +167 -0
  2. package/dist/aliases/store.d.ts +21 -0
  3. package/dist/aliases/store.js +148 -0
  4. package/dist/aliases/types.d.ts +6 -0
  5. package/dist/aliases/types.js +1 -0
  6. package/dist/brain/agent-registry.d.ts +96 -0
  7. package/dist/brain/agent-registry.js +141 -0
  8. package/dist/brain/agent.d.ts +167 -0
  9. package/dist/brain/agent.js +932 -0
  10. package/dist/brain/channel-store.d.ts +27 -0
  11. package/dist/brain/channel-store.js +78 -0
  12. package/dist/brain/compaction.d.ts +37 -0
  13. package/dist/brain/compaction.js +214 -0
  14. package/dist/brain/context.d.ts +43 -0
  15. package/dist/brain/context.js +139 -0
  16. package/dist/brain/delegation-store.d.ts +33 -0
  17. package/dist/brain/delegation-store.js +106 -0
  18. package/dist/brain/loop.d.ts +24 -0
  19. package/dist/brain/loop.js +318 -0
  20. package/dist/brain/mcp-adapter.d.ts +43 -0
  21. package/dist/brain/mcp-adapter.js +244 -0
  22. package/dist/brain/memory-extractor.d.ts +26 -0
  23. package/dist/brain/memory-extractor.js +82 -0
  24. package/dist/brain/providers.d.ts +14 -0
  25. package/dist/brain/providers.js +85 -0
  26. package/dist/brain/queue.d.ts +18 -0
  27. package/dist/brain/queue.js +111 -0
  28. package/dist/brain/run-tools.d.ts +50 -0
  29. package/dist/brain/run-tools.js +136 -0
  30. package/dist/brain/session-key.d.ts +23 -0
  31. package/dist/brain/session-key.js +41 -0
  32. package/dist/brain/session-state.d.ts +36 -0
  33. package/dist/brain/session-state.js +51 -0
  34. package/dist/brain/session-store.d.ts +50 -0
  35. package/dist/brain/session-store.js +207 -0
  36. package/dist/brain/session.d.ts +32 -0
  37. package/dist/brain/session.js +75 -0
  38. package/dist/brain/task-subscriber.d.ts +56 -0
  39. package/dist/brain/task-subscriber.js +317 -0
  40. package/dist/brain/user-content.d.ts +16 -0
  41. package/dist/brain/user-content.js +32 -0
  42. package/dist/brain/utils.d.ts +4 -0
  43. package/dist/brain/utils.js +26 -0
  44. package/dist/brain/worker-coordinator.d.ts +25 -0
  45. package/dist/brain/worker-coordinator.js +83 -0
  46. package/dist/channels/commands.d.ts +50 -0
  47. package/dist/channels/commands.js +132 -0
  48. package/dist/channels/discord/channel.d.ts +29 -0
  49. package/dist/channels/discord/channel.js +159 -0
  50. package/dist/channels/discord/markdown.d.ts +19 -0
  51. package/dist/channels/discord/markdown.js +62 -0
  52. package/dist/channels/manager.d.ts +29 -0
  53. package/dist/channels/manager.js +100 -0
  54. package/dist/channels/slack/channel.d.ts +37 -0
  55. package/dist/channels/slack/channel.js +227 -0
  56. package/dist/channels/slack/markdown.d.ts +19 -0
  57. package/dist/channels/slack/markdown.js +62 -0
  58. package/dist/channels/specs.d.ts +32 -0
  59. package/dist/channels/specs.js +99 -0
  60. package/dist/channels/telegram/channel.d.ts +29 -0
  61. package/dist/channels/telegram/channel.js +182 -0
  62. package/dist/channels/telegram/markdown.d.ts +17 -0
  63. package/dist/channels/telegram/markdown.js +66 -0
  64. package/dist/channels/types.d.ts +26 -0
  65. package/dist/channels/types.js +1 -0
  66. package/dist/channels/whatsapp/channel.d.ts +34 -0
  67. package/dist/channels/whatsapp/channel.js +276 -0
  68. package/dist/channels/whatsapp/markdown.d.ts +20 -0
  69. package/dist/channels/whatsapp/markdown.js +51 -0
  70. package/dist/cli/claude-login.d.ts +5 -0
  71. package/dist/cli/claude-login.js +47 -0
  72. package/dist/cli/config.d.ts +5 -0
  73. package/dist/cli/config.js +78 -0
  74. package/dist/cli/index.d.ts +11 -0
  75. package/dist/cli/index.js +96 -0
  76. package/dist/computer/browser/actions.d.ts +31 -0
  77. package/dist/computer/browser/actions.js +148 -0
  78. package/dist/computer/browser/context-manager.d.ts +28 -0
  79. package/dist/computer/browser/context-manager.js +78 -0
  80. package/dist/computer/browser/manager.d.ts +91 -0
  81. package/dist/computer/browser/manager.js +344 -0
  82. package/dist/computer/browser/profile-badge.d.ts +13 -0
  83. package/dist/computer/browser/profile-badge.js +67 -0
  84. package/dist/computer/browser/screenshot.d.ts +5 -0
  85. package/dist/computer/browser/screenshot.js +21 -0
  86. package/dist/computer/browser/snapshot.d.ts +30 -0
  87. package/dist/computer/browser/snapshot.js +242 -0
  88. package/dist/computer/browser/tools.d.ts +5 -0
  89. package/dist/computer/browser/tools.js +167 -0
  90. package/dist/computer/browser/types.d.ts +26 -0
  91. package/dist/computer/browser/types.js +1 -0
  92. package/dist/computer/desktop/adapter.d.ts +25 -0
  93. package/dist/computer/desktop/adapter.js +11 -0
  94. package/dist/computer/desktop/macos.d.ts +24 -0
  95. package/dist/computer/desktop/macos.js +223 -0
  96. package/dist/computer/desktop/tools.d.ts +25 -0
  97. package/dist/computer/desktop/tools.js +114 -0
  98. package/dist/config/agent-config.d.ts +55 -0
  99. package/dist/config/agent-config.js +16 -0
  100. package/dist/config/model-catalog.d.ts +22 -0
  101. package/dist/config/model-catalog.js +112 -0
  102. package/dist/config/model-spec.d.ts +8 -0
  103. package/dist/config/model-spec.js +66 -0
  104. package/dist/config/store.d.ts +25 -0
  105. package/dist/config/store.js +143 -0
  106. package/dist/config.d.ts +110 -0
  107. package/dist/config.js +259 -0
  108. package/dist/control-ui/assets/index-Cbl7G5Sc.css +1 -0
  109. package/dist/control-ui/assets/index-Cu1P4C62.js +266 -0
  110. package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
  111. package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
  112. package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
  113. package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
  114. package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
  115. package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
  116. package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
  117. package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
  118. package/dist/control-ui/index.html +14 -0
  119. package/dist/control-ui/vite.svg +1 -0
  120. package/dist/events.d.ts +2 -0
  121. package/dist/events.js +11 -0
  122. package/dist/gateway/broadcast.d.ts +5 -0
  123. package/dist/gateway/broadcast.js +33 -0
  124. package/dist/gateway/methods/aliases.d.ts +17 -0
  125. package/dist/gateway/methods/aliases.js +22 -0
  126. package/dist/gateway/methods/chat.d.ts +33 -0
  127. package/dist/gateway/methods/chat.js +37 -0
  128. package/dist/gateway/methods/config.d.ts +14 -0
  129. package/dist/gateway/methods/config.js +24 -0
  130. package/dist/gateway/methods/models.d.ts +10 -0
  131. package/dist/gateway/methods/models.js +14 -0
  132. package/dist/gateway/methods/playbooks.d.ts +45 -0
  133. package/dist/gateway/methods/playbooks.js +488 -0
  134. package/dist/gateway/methods/prompt-templates.d.ts +27 -0
  135. package/dist/gateway/methods/prompt-templates.js +106 -0
  136. package/dist/gateway/methods/scheduler.d.ts +62 -0
  137. package/dist/gateway/methods/scheduler.js +129 -0
  138. package/dist/gateway/methods/sessions.d.ts +44 -0
  139. package/dist/gateway/methods/sessions.js +111 -0
  140. package/dist/gateway/methods/system.d.ts +12 -0
  141. package/dist/gateway/methods/system.js +39 -0
  142. package/dist/gateway/methods/tasks.d.ts +40 -0
  143. package/dist/gateway/methods/tasks.js +151 -0
  144. package/dist/gateway/methods/teams.d.ts +69 -0
  145. package/dist/gateway/methods/teams.js +376 -0
  146. package/dist/gateway/methods/tools.d.ts +6 -0
  147. package/dist/gateway/methods/tools.js +7 -0
  148. package/dist/gateway/methods/whatsapp.d.ts +19 -0
  149. package/dist/gateway/methods/whatsapp.js +35 -0
  150. package/dist/gateway/rpc.d.ts +38 -0
  151. package/dist/gateway/rpc.js +79 -0
  152. package/dist/gateway/server.d.ts +9 -0
  153. package/dist/gateway/server.js +137 -0
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +254 -0
  156. package/dist/integrations/github.d.ts +7 -0
  157. package/dist/integrations/github.js +133 -0
  158. package/dist/integrations/mcp.d.ts +7 -0
  159. package/dist/integrations/mcp.js +106 -0
  160. package/dist/integrations/registry.d.ts +47 -0
  161. package/dist/integrations/registry.js +332 -0
  162. package/dist/integrations/scanner.d.ts +10 -0
  163. package/dist/integrations/scanner.js +122 -0
  164. package/dist/integrations/twitter.d.ts +10 -0
  165. package/dist/integrations/twitter.js +120 -0
  166. package/dist/integrations/types.d.ts +72 -0
  167. package/dist/integrations/types.js +1 -0
  168. package/dist/logger.d.ts +16 -0
  169. package/dist/logger.js +104 -0
  170. package/dist/markdown/chunk.d.ts +9 -0
  171. package/dist/markdown/chunk.js +52 -0
  172. package/dist/markdown/ir.d.ts +37 -0
  173. package/dist/markdown/ir.js +529 -0
  174. package/dist/markdown/render.d.ts +22 -0
  175. package/dist/markdown/render.js +148 -0
  176. package/dist/markdown/table-render.d.ts +43 -0
  177. package/dist/markdown/table-render.js +219 -0
  178. package/dist/markdown/tables.d.ts +17 -0
  179. package/dist/markdown/tables.js +27 -0
  180. package/dist/memory/embedding.d.ts +16 -0
  181. package/dist/memory/embedding.js +66 -0
  182. package/dist/memory/explicit.d.ts +16 -0
  183. package/dist/memory/explicit.js +29 -0
  184. package/dist/memory/extractor.d.ts +13 -0
  185. package/dist/memory/extractor.js +82 -0
  186. package/dist/memory/search.d.ts +15 -0
  187. package/dist/memory/search.js +57 -0
  188. package/dist/memory/session-learning.d.ts +23 -0
  189. package/dist/memory/session-learning.js +55 -0
  190. package/dist/memory/store.d.ts +36 -0
  191. package/dist/memory/store.js +334 -0
  192. package/dist/memory/types.d.ts +9 -0
  193. package/dist/memory/types.js +2 -0
  194. package/dist/paths.d.ts +28 -0
  195. package/dist/paths.js +48 -0
  196. package/dist/prompt-templates/builtins/index.d.ts +4 -0
  197. package/dist/prompt-templates/builtins/index.js +5 -0
  198. package/dist/prompt-templates/builtins/planner.d.ts +4 -0
  199. package/dist/prompt-templates/builtins/planner.js +77 -0
  200. package/dist/prompt-templates/store.d.ts +45 -0
  201. package/dist/prompt-templates/store.js +224 -0
  202. package/dist/prompt-templates/types.d.ts +10 -0
  203. package/dist/prompt-templates/types.js +1 -0
  204. package/dist/scheduler/connected-channels.d.ts +24 -0
  205. package/dist/scheduler/connected-channels.js +57 -0
  206. package/dist/scheduler/scheduler.d.ts +22 -0
  207. package/dist/scheduler/scheduler.js +132 -0
  208. package/dist/scheduler/store.d.ts +27 -0
  209. package/dist/scheduler/store.js +205 -0
  210. package/dist/scheduler/types.d.ts +29 -0
  211. package/dist/scheduler/types.js +1 -0
  212. package/dist/security/command-validator.d.ts +22 -0
  213. package/dist/security/command-validator.js +160 -0
  214. package/dist/security/docker-sandbox.d.ts +48 -0
  215. package/dist/security/docker-sandbox.js +218 -0
  216. package/dist/security/env-filter.d.ts +8 -0
  217. package/dist/security/env-filter.js +41 -0
  218. package/dist/skills/loader.d.ts +33 -0
  219. package/dist/skills/loader.js +132 -0
  220. package/dist/skills/prompt.d.ts +6 -0
  221. package/dist/skills/prompt.js +17 -0
  222. package/dist/skills/read-tool.d.ts +7 -0
  223. package/dist/skills/read-tool.js +24 -0
  224. package/dist/skills/scanner.d.ts +6 -0
  225. package/dist/skills/scanner.js +73 -0
  226. package/dist/skills/types.d.ts +15 -0
  227. package/dist/skills/types.js +1 -0
  228. package/dist/tasks/inline-attachment-content.d.ts +9 -0
  229. package/dist/tasks/inline-attachment-content.js +64 -0
  230. package/dist/tasks/store.d.ts +112 -0
  231. package/dist/tasks/store.js +519 -0
  232. package/dist/tasks/types.d.ts +129 -0
  233. package/dist/tasks/types.js +80 -0
  234. package/dist/teams/status-config.d.ts +8 -0
  235. package/dist/teams/status-config.js +40 -0
  236. package/dist/teams/store.d.ts +111 -0
  237. package/dist/teams/store.js +671 -0
  238. package/dist/teams/types.d.ts +30 -0
  239. package/dist/teams/types.js +1 -0
  240. package/dist/tools/bash.d.ts +18 -0
  241. package/dist/tools/bash.js +64 -0
  242. package/dist/tools/channel-history.d.ts +10 -0
  243. package/dist/tools/channel-history.js +43 -0
  244. package/dist/tools/delegate.d.ts +20 -0
  245. package/dist/tools/delegate.js +299 -0
  246. package/dist/tools/fs.d.ts +4 -0
  247. package/dist/tools/fs.js +335 -0
  248. package/dist/tools/integration-toggle.d.ts +14 -0
  249. package/dist/tools/integration-toggle.js +47 -0
  250. package/dist/tools/memory.d.ts +13 -0
  251. package/dist/tools/memory.js +59 -0
  252. package/dist/tools/prompt-templates.d.ts +7 -0
  253. package/dist/tools/prompt-templates.js +133 -0
  254. package/dist/tools/registry.d.ts +6 -0
  255. package/dist/tools/registry.js +9 -0
  256. package/dist/tools/schedule.d.ts +8 -0
  257. package/dist/tools/schedule.js +219 -0
  258. package/dist/tools/speak.d.ts +10 -0
  259. package/dist/tools/speak.js +56 -0
  260. package/dist/tools/tasks.d.ts +67 -0
  261. package/dist/tools/tasks.js +288 -0
  262. package/dist/tools/teams.d.ts +22 -0
  263. package/dist/tools/teams.js +470 -0
  264. package/dist/tools/web-fetch.d.ts +3 -0
  265. package/dist/tools/web-fetch.js +22 -0
  266. package/dist/tts/edge.d.ts +10 -0
  267. package/dist/tts/edge.js +60 -0
  268. package/dist/tts/speak.d.ts +12 -0
  269. package/dist/tts/speak.js +81 -0
  270. package/dist/tts/transcribe.d.ts +5 -0
  271. package/dist/tts/transcribe.js +40 -0
  272. package/dist/utils.d.ts +5 -0
  273. package/dist/utils.js +22 -0
  274. package/dist/version.d.ts +1 -0
  275. package/dist/version.js +13 -0
  276. package/package.json +102 -0
  277. package/verybot.js +2 -0
@@ -0,0 +1,671 @@
1
+ import { mkdirSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { randomUUID } from "crypto";
4
+ import Database from "better-sqlite3";
5
+ import { logger } from "../logger.js";
6
+ import { DEFAULT_TEAM_ID, DEFAULT_WORKER_TIMEOUT_S, FALLBACK_ORCHESTRATOR } from "../config/agent-config.js";
7
+ import { LEGACY_STATUS_KEYS } from "../tasks/types.js";
8
+ import { validateStatusConfigs } from "./status-config.js";
9
+ /** Max allowed length for team/agent names. */
10
+ export const MAX_NAME_LENGTH = 128;
11
+ /** Max allowed length for agent identity strings. */
12
+ export const MAX_IDENTITY_LENGTH = 10_000;
13
+ /** Max allowed length for model strings. */
14
+ export const MAX_MODEL_LENGTH = 256;
15
+ /** Max allowed length for caller-provided ids. */
16
+ const MAX_ID_LENGTH = 128;
17
+ /** Concurrency bounds for worker agents. */
18
+ const MIN_CONCURRENCY = 1;
19
+ const MAX_CONCURRENCY = 10;
20
+ /** Hex color pattern: #RRGGBB */
21
+ export const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
22
+ const DEFAULT_TEAM_NAME = "Default";
23
+ /** Validate a caller-provided id string. */
24
+ function validateId(id) {
25
+ if (typeof id !== "string" || id.trim().length === 0) {
26
+ throw new Error("id must be a non-empty string");
27
+ }
28
+ if (id.length > MAX_ID_LENGTH) {
29
+ throw new Error(`id exceeds maximum length of ${MAX_ID_LENGTH}`);
30
+ }
31
+ }
32
+ /** Max allowed length for workspace paths. */
33
+ const MAX_WORKSPACE_LENGTH = 1024;
34
+ /** Max number of user-defined variables per team. */
35
+ const MAX_VARIABLES_COUNT = 50;
36
+ /** Max allowed length for a single variable value. */
37
+ const MAX_VARIABLE_VALUE_LENGTH = 10_000;
38
+ /** Variable key pattern: alphanumeric + underscore. */
39
+ const VARIABLE_KEY_RE = /^\w+$/;
40
+ /**
41
+ * SQLite-backed persistence for teams and agents.
42
+ * Shares the same DB file as MemoryStore, TaskStore, etc.
43
+ */
44
+ export class TeamStore {
45
+ db;
46
+ constructor(db) {
47
+ this.db = db;
48
+ }
49
+ static async create(dbPath) {
50
+ mkdirSync(dirname(dbPath), { recursive: true });
51
+ const db = new Database(dbPath);
52
+ db.pragma("journal_mode = WAL");
53
+ const store = new TeamStore(db);
54
+ store.createSchema();
55
+ return store;
56
+ }
57
+ createSchema() {
58
+ // Enable foreign key enforcement before creating tables
59
+ this.db.pragma("foreign_keys = ON");
60
+ // Ensure prompt_templates table exists (may already be created by PromptTemplateStore)
61
+ // Needed for agents.template_id FK and toTeamConfigs() JOIN.
62
+ this.db.exec(`
63
+ CREATE TABLE IF NOT EXISTS prompt_templates (
64
+ id TEXT PRIMARY KEY,
65
+ name TEXT NOT NULL UNIQUE,
66
+ description TEXT NOT NULL DEFAULT '',
67
+ role TEXT NOT NULL,
68
+ content TEXT NOT NULL DEFAULT '',
69
+ builtin INTEGER NOT NULL DEFAULT 0,
70
+ created_at INTEGER NOT NULL,
71
+ updated_at INTEGER NOT NULL
72
+ );
73
+ `);
74
+ this.db.exec(`
75
+ CREATE TABLE IF NOT EXISTS teams (
76
+ id TEXT PRIMARY KEY,
77
+ name TEXT NOT NULL UNIQUE,
78
+ created_at INTEGER NOT NULL,
79
+ updated_at INTEGER NOT NULL
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS agents (
83
+ id TEXT PRIMARY KEY,
84
+ team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
85
+ name TEXT NOT NULL,
86
+ role TEXT NOT NULL,
87
+ model TEXT NOT NULL,
88
+ context_window INTEGER DEFAULT 0,
89
+ max_steps INTEGER DEFAULT 0,
90
+ identity TEXT NOT NULL DEFAULT '',
91
+ tools TEXT DEFAULT '[]',
92
+ timeout INTEGER DEFAULT ${DEFAULT_WORKER_TIMEOUT_S},
93
+ created_at INTEGER NOT NULL,
94
+ updated_at INTEGER NOT NULL,
95
+ UNIQUE(team_id, name)
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_agents_team ON agents(team_id);
98
+ `);
99
+ // Add color column to existing tables (idempotent — SQLite errors if column exists)
100
+ try {
101
+ this.db.exec("ALTER TABLE teams ADD COLUMN color TEXT NOT NULL DEFAULT ''");
102
+ }
103
+ catch {
104
+ // Column already exists
105
+ }
106
+ // Add template_id FK column to agents (idempotent)
107
+ try {
108
+ this.db.exec("ALTER TABLE agents ADD COLUMN template_id TEXT REFERENCES prompt_templates(id) ON DELETE SET NULL");
109
+ }
110
+ catch {
111
+ // Column already exists
112
+ }
113
+ // Add workspace column to teams (idempotent)
114
+ try {
115
+ this.db.exec("ALTER TABLE teams ADD COLUMN workspace TEXT NOT NULL DEFAULT ''");
116
+ }
117
+ catch {
118
+ // Column already exists
119
+ }
120
+ // Add variables column to teams (JSON-encoded Record<string, string>)
121
+ try {
122
+ this.db.exec("ALTER TABLE teams ADD COLUMN variables TEXT NOT NULL DEFAULT '{}'");
123
+ }
124
+ catch {
125
+ // Column already exists
126
+ }
127
+ // Add concurrency column to agents (default 1)
128
+ try {
129
+ this.db.exec("ALTER TABLE agents ADD COLUMN concurrency INTEGER NOT NULL DEFAULT 1");
130
+ }
131
+ catch {
132
+ // Column already exists
133
+ }
134
+ // Add statuses column to teams (JSON-encoded TaskStatusConfig[])
135
+ try {
136
+ this.db.exec("ALTER TABLE teams ADD COLUMN statuses TEXT DEFAULT NULL");
137
+ }
138
+ catch {
139
+ // Column already exists
140
+ }
141
+ // Agent subscriptions table for pull-based task execution
142
+ this.db.exec(`
143
+ CREATE TABLE IF NOT EXISTS agent_subscriptions (
144
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
145
+ task_status TEXT NOT NULL,
146
+ PRIMARY KEY (agent_id, task_status)
147
+ );
148
+ `);
149
+ }
150
+ // --- Team CRUD ---
151
+ /** Run a function inside a SQLite transaction (all-or-nothing). */
152
+ transaction(fn) {
153
+ return this.db.transaction(fn)();
154
+ }
155
+ createTeam(input) {
156
+ const { name, color = "", workspace = "", variables = {}, statuses } = input;
157
+ if (input.id !== undefined)
158
+ validateId(input.id);
159
+ if (name.length > MAX_NAME_LENGTH)
160
+ throw new Error(`Team name exceeds maximum length of ${MAX_NAME_LENGTH}`);
161
+ if (color !== "" && !HEX_COLOR_RE.test(color))
162
+ throw new Error("color must be a valid hex color (e.g. #ef4444)");
163
+ if (workspace.length > MAX_WORKSPACE_LENGTH)
164
+ throw new Error(`workspace exceeds maximum length of ${MAX_WORKSPACE_LENGTH}`);
165
+ if (statuses !== undefined)
166
+ validateStatusConfigs(statuses);
167
+ validateVariables(variables);
168
+ const now = Date.now();
169
+ const id = input.id ?? randomUUID();
170
+ const statusesJson = statuses ? JSON.stringify(statuses) : null;
171
+ try {
172
+ this.db.prepare("INSERT INTO teams (id, name, color, workspace, variables, statuses, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(id, name, color, workspace, JSON.stringify(variables), statusesJson, now, now);
173
+ }
174
+ catch (err) {
175
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
176
+ throw new Error(`A team named "${name}" already exists`);
177
+ }
178
+ throw err;
179
+ }
180
+ return { id, name, color, workspace, statuses: statuses ?? undefined, createdAt: now, updatedAt: now };
181
+ }
182
+ updateTeam(id, input) {
183
+ const existing = this.getTeamById(id);
184
+ if (!existing)
185
+ return null;
186
+ const name = input.name ?? existing.name;
187
+ if (name.length > MAX_NAME_LENGTH)
188
+ throw new Error(`Team name exceeds maximum length of ${MAX_NAME_LENGTH}`);
189
+ const color = input.color ?? existing.color;
190
+ const workspace = input.workspace ?? existing.workspace;
191
+ if (color !== "" && !HEX_COLOR_RE.test(color))
192
+ throw new Error("color must be a valid hex color (e.g. #ef4444)");
193
+ if (workspace.length > MAX_WORKSPACE_LENGTH)
194
+ throw new Error(`workspace exceeds maximum length of ${MAX_WORKSPACE_LENGTH}`);
195
+ // Variables: replace entirely if provided, otherwise keep existing
196
+ let variables;
197
+ if (input.variables !== undefined) {
198
+ validateVariables(input.variables);
199
+ variables = input.variables;
200
+ }
201
+ const varsJson = variables !== undefined ? JSON.stringify(variables) : undefined;
202
+ // Statuses: replace entirely if provided, otherwise keep existing
203
+ if (input.statuses !== undefined)
204
+ validateStatusConfigs(input.statuses);
205
+ const statuses = input.statuses !== undefined ? input.statuses : existing.statuses;
206
+ const statusesJson = input.statuses !== undefined ? (input.statuses ? JSON.stringify(input.statuses) : null) : undefined;
207
+ const now = Date.now();
208
+ const sets = ["name = ?", "color = ?", "workspace = ?", "updated_at = ?"];
209
+ const params = [name, color, workspace, now];
210
+ if (varsJson !== undefined) {
211
+ sets.splice(3, 0, "variables = ?");
212
+ params.splice(3, 0, varsJson);
213
+ }
214
+ if (statusesJson !== undefined) {
215
+ sets.splice(sets.length - 1, 0, "statuses = ?");
216
+ params.splice(params.length - 1, 0, statusesJson);
217
+ }
218
+ params.push(id);
219
+ this.db.prepare(`UPDATE teams SET ${sets.join(", ")} WHERE id = ?`).run(...params);
220
+ return { ...existing, name, color, workspace, statuses: statuses ?? undefined, updatedAt: now };
221
+ }
222
+ deleteTeam(id) {
223
+ if (id === DEFAULT_TEAM_ID)
224
+ throw new Error("Cannot delete the default team");
225
+ const info = this.db.prepare("DELETE FROM teams WHERE id = ?").run(id);
226
+ return info.changes > 0;
227
+ }
228
+ getTeamById(id) {
229
+ const row = this.db.prepare("SELECT * FROM teams WHERE id = ?").get(id);
230
+ return row ? toTeam(row) : null;
231
+ }
232
+ getTeamByName(name) {
233
+ const row = this.db.prepare("SELECT * FROM teams WHERE name = ?").get(name);
234
+ return row ? toTeam(row) : null;
235
+ }
236
+ listTeams() {
237
+ const rows = this.db.prepare("SELECT * FROM teams ORDER BY created_at ASC").all();
238
+ return rows.map(toTeam);
239
+ }
240
+ /**
241
+ * Bootstrap helper:
242
+ * guarantees a team row with id "default" exists.
243
+ */
244
+ ensureTeamWhenEmpty() {
245
+ const existing = this.getTeamById(DEFAULT_TEAM_ID);
246
+ if (existing)
247
+ return existing;
248
+ return this.createTeam({ id: DEFAULT_TEAM_ID, name: DEFAULT_TEAM_NAME });
249
+ }
250
+ // --- Agent CRUD ---
251
+ createAgent(teamId, input) {
252
+ if (input.id !== undefined)
253
+ validateId(input.id);
254
+ if (input.name.length > MAX_NAME_LENGTH)
255
+ throw new Error(`Agent name exceeds maximum length of ${MAX_NAME_LENGTH}`);
256
+ if (input.model.length > MAX_MODEL_LENGTH)
257
+ throw new Error(`Model string exceeds maximum length of ${MAX_MODEL_LENGTH}`);
258
+ if (input.identity && input.identity.length > MAX_IDENTITY_LENGTH)
259
+ throw new Error(`Identity exceeds maximum length of ${MAX_IDENTITY_LENGTH}`);
260
+ // Verify team exists (friendly error instead of FK violation)
261
+ const team = this.getTeamById(teamId);
262
+ if (!team)
263
+ throw new Error(`Team not found: ${teamId}`);
264
+ // Prevent multiple orchestrators per team
265
+ if (input.role === "orchestrator") {
266
+ const existing = this.db.prepare("SELECT id FROM agents WHERE team_id = ? AND role = 'orchestrator'").get(teamId);
267
+ if (existing)
268
+ throw new Error("Team already has an orchestrator — update the existing one instead");
269
+ }
270
+ const concurrency = input.concurrency ?? 1;
271
+ if (concurrency < MIN_CONCURRENCY || concurrency > MAX_CONCURRENCY) {
272
+ throw new Error(`concurrency must be between ${MIN_CONCURRENCY} and ${MAX_CONCURRENCY}`);
273
+ }
274
+ const subscriptions = input.subscriptions ?? [];
275
+ // Validate against team's custom statuses if present
276
+ const teamStatusKeys = team.statuses?.map((s) => s.key);
277
+ validateSubscriptions(subscriptions, teamStatusKeys);
278
+ const now = Date.now();
279
+ const id = input.id ?? randomUUID();
280
+ try {
281
+ this.db.transaction(() => {
282
+ this.db.prepare(`INSERT INTO agents (id, team_id, name, role, model, context_window, max_steps, identity, tools, timeout, template_id, concurrency, created_at, updated_at)
283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, teamId, input.name, input.role, input.model, input.contextWindow ?? 0, input.maxSteps ?? 0, input.identity ?? "", JSON.stringify(input.tools ?? []), input.timeout ?? DEFAULT_WORKER_TIMEOUT_S, input.templateId ?? null, concurrency, now, now);
284
+ if (subscriptions.length > 0) {
285
+ const ins = this.db.prepare("INSERT INTO agent_subscriptions (agent_id, task_status) VALUES (?, ?)");
286
+ for (const s of subscriptions)
287
+ ins.run(id, s);
288
+ }
289
+ })();
290
+ }
291
+ catch (err) {
292
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
293
+ throw new Error(`Agent "${input.name}" already exists in this team`);
294
+ }
295
+ throw err;
296
+ }
297
+ return {
298
+ id, teamId, name: input.name, role: input.role, model: input.model,
299
+ contextWindow: input.contextWindow ?? 0, maxSteps: input.maxSteps ?? 0,
300
+ identity: input.identity ?? "", tools: input.tools ?? [],
301
+ timeout: input.timeout ?? DEFAULT_WORKER_TIMEOUT_S,
302
+ templateId: input.templateId ?? null,
303
+ subscriptions, concurrency,
304
+ createdAt: now, updatedAt: now,
305
+ };
306
+ }
307
+ updateAgent(id, input) {
308
+ if (input.name !== undefined && input.name.length > MAX_NAME_LENGTH)
309
+ throw new Error(`Agent name exceeds maximum length of ${MAX_NAME_LENGTH}`);
310
+ if (input.model !== undefined && input.model.length > MAX_MODEL_LENGTH)
311
+ throw new Error(`Model string exceeds maximum length of ${MAX_MODEL_LENGTH}`);
312
+ if (input.identity !== undefined && input.identity.length > MAX_IDENTITY_LENGTH)
313
+ throw new Error(`Identity exceeds maximum length of ${MAX_IDENTITY_LENGTH}`);
314
+ if (input.concurrency !== undefined && (input.concurrency < MIN_CONCURRENCY || input.concurrency > MAX_CONCURRENCY)) {
315
+ throw new Error(`concurrency must be between ${MIN_CONCURRENCY} and ${MAX_CONCURRENCY}`);
316
+ }
317
+ const existing = this.getAgentById(id);
318
+ if (!existing)
319
+ return null;
320
+ if (input.subscriptions !== undefined) {
321
+ const team = this.getTeamById(existing.teamId);
322
+ const teamStatusKeys = team?.statuses?.map((s) => s.key);
323
+ validateSubscriptions(input.subscriptions, teamStatusKeys);
324
+ }
325
+ const now = Date.now();
326
+ const updated = {
327
+ ...existing,
328
+ ...(input.name !== undefined && { name: input.name }),
329
+ ...(input.role !== undefined && { role: input.role }),
330
+ ...(input.model !== undefined && { model: input.model }),
331
+ ...(input.contextWindow !== undefined && { contextWindow: input.contextWindow }),
332
+ ...(input.maxSteps !== undefined && { maxSteps: input.maxSteps }),
333
+ ...(input.identity !== undefined && { identity: input.identity }),
334
+ ...(input.tools !== undefined && { tools: input.tools }),
335
+ ...(input.timeout !== undefined && { timeout: input.timeout }),
336
+ ...(input.templateId !== undefined && { templateId: input.templateId }),
337
+ ...(input.subscriptions !== undefined && { subscriptions: input.subscriptions }),
338
+ ...(input.concurrency !== undefined && { concurrency: input.concurrency }),
339
+ updatedAt: now,
340
+ };
341
+ this.db.transaction(() => {
342
+ this.db.prepare(`UPDATE agents SET name = ?, role = ?, model = ?, context_window = ?, max_steps = ?,
343
+ identity = ?, tools = ?, timeout = ?, template_id = ?, concurrency = ?, updated_at = ? WHERE id = ?`).run(updated.name, updated.role, updated.model, updated.contextWindow, updated.maxSteps, updated.identity, JSON.stringify(updated.tools), updated.timeout, updated.templateId ?? null, updated.concurrency, now, id);
344
+ if (input.subscriptions !== undefined) {
345
+ this.setSubscriptions(id, input.subscriptions);
346
+ }
347
+ })();
348
+ return updated;
349
+ }
350
+ deleteAgent(id) {
351
+ // Prevent deleting a team's only orchestrator
352
+ const agent = this.getAgentById(id);
353
+ if (agent?.role === "orchestrator") {
354
+ throw new Error("Cannot delete a team's orchestrator agent");
355
+ }
356
+ const info = this.db.prepare("DELETE FROM agents WHERE id = ?").run(id);
357
+ return info.changes > 0;
358
+ }
359
+ getAgentById(id) {
360
+ const row = this.db.prepare("SELECT * FROM agents WHERE id = ?").get(id);
361
+ if (!row)
362
+ return null;
363
+ return toAgentRow(row, this.getSubscriptions(id));
364
+ }
365
+ /**
366
+ * Runtime lookup for agent execution.
367
+ * Resolves template-linked identity and interpolates team variables.
368
+ */
369
+ getRuntimeAgentById(id) {
370
+ const row = this.db.prepare(`SELECT a.*,
371
+ t.name AS tname,
372
+ t.workspace AS tworkspace,
373
+ t.variables AS tvariables,
374
+ pt.content AS template_content
375
+ FROM agents a
376
+ LEFT JOIN teams t ON t.id = a.team_id
377
+ LEFT JOIN prompt_templates pt ON a.template_id = pt.id
378
+ WHERE a.id = ?`).get(id);
379
+ if (!row)
380
+ return null;
381
+ const resolvedIdentity = resolveAgentIdentity({
382
+ templateId: row.template_id ?? null,
383
+ templateContent: row.template_content ?? null,
384
+ rawIdentity: row.identity ?? "",
385
+ teamName: row.tname ?? "",
386
+ workspace: row.tworkspace ?? "",
387
+ teamVariables: parseTeamVariables(row.tvariables),
388
+ });
389
+ return toAgentRow(row, this.getSubscriptions(id), resolvedIdentity);
390
+ }
391
+ listAgentsByTeam(teamId) {
392
+ const rows = this.db.prepare("SELECT * FROM agents WHERE team_id = ? ORDER BY role ASC, created_at ASC").all(teamId);
393
+ if (rows.length === 0)
394
+ return [];
395
+ // Batch-load subscriptions to avoid N+1 queries
396
+ const agentIds = rows.map((r) => r.id);
397
+ const placeholders = agentIds.map(() => "?").join(", ");
398
+ const subRows = this.db.prepare(`SELECT agent_id, task_status FROM agent_subscriptions WHERE agent_id IN (${placeholders})`).all(...agentIds);
399
+ const subsMap = new Map();
400
+ for (const s of subRows) {
401
+ if (!subsMap.has(s.agent_id))
402
+ subsMap.set(s.agent_id, []);
403
+ subsMap.get(s.agent_id).push(s.task_status);
404
+ }
405
+ return rows.map((row) => toAgentRow(row, subsMap.get(row.id)));
406
+ }
407
+ getAgentByName(teamId, name) {
408
+ const row = this.db.prepare("SELECT * FROM agents WHERE team_id = ? AND name = ?").get(teamId, name);
409
+ if (!row)
410
+ return null;
411
+ return toAgentRow(row, this.getSubscriptions(row.id));
412
+ }
413
+ // --- Subscription CRUD ---
414
+ /** Replace an agent's subscriptions (transactional delete + reinsert). */
415
+ setSubscriptions(agentId, statuses) {
416
+ this.db.transaction(() => {
417
+ this.db.prepare("DELETE FROM agent_subscriptions WHERE agent_id = ?").run(agentId);
418
+ const insert = this.db.prepare("INSERT INTO agent_subscriptions (agent_id, task_status) VALUES (?, ?)");
419
+ for (const status of statuses) {
420
+ insert.run(agentId, status);
421
+ }
422
+ })();
423
+ }
424
+ /** Get an agent's subscribed task statuses. */
425
+ getSubscriptions(agentId) {
426
+ const rows = this.db.prepare("SELECT task_status FROM agent_subscriptions WHERE agent_id = ?").all(agentId);
427
+ return rows.map((r) => r.task_status);
428
+ }
429
+ /**
430
+ * Find claimable tasks by joining tasks, agent_subscriptions, and agents tables.
431
+ * Used by TaskSubscriberManager to find task/agent matches without raw DB access.
432
+ * Enforces concurrency at query time from live claim counts.
433
+ */
434
+ findClaimableTasks(limit) {
435
+ const rows = this.db.prepare(`
436
+ SELECT t.id AS task_id, s.agent_id, a.team_id, a.name AS agent_name, a.model, a.concurrency
437
+ FROM tasks t
438
+ JOIN agent_subscriptions s ON s.task_status = t.status
439
+ JOIN agents a ON a.id = s.agent_id
440
+ WHERE t.claimed_by IS NULL
441
+ AND t.needs_human_review = 0
442
+ AND t.team_id = a.team_id
443
+ AND (t.assignee IS NULL OR t.assignee = s.agent_id)
444
+ AND (
445
+ t.last_processed_by IS NULL
446
+ OR t.last_processed_for_updated_at IS NULL
447
+ OR t.last_processed_by != s.agent_id
448
+ OR t.last_processed_for_updated_at != t.updated_at
449
+ )
450
+ AND (
451
+ SELECT COUNT(*)
452
+ FROM tasks claimed
453
+ WHERE claimed.claimed_by = s.agent_id
454
+ ) < a.concurrency
455
+ ORDER BY
456
+ CASE WHEN t.assignee = s.agent_id THEN 0 ELSE 1 END,
457
+ t.created_at ASC
458
+ LIMIT ?
459
+ `).all(limit);
460
+ return rows.map((r) => ({
461
+ taskId: r.task_id,
462
+ agentId: r.agent_id,
463
+ teamId: r.team_id,
464
+ agentName: r.agent_name,
465
+ model: r.model,
466
+ concurrency: r.concurrency,
467
+ }));
468
+ }
469
+ /**
470
+ * Convert DB rows to the existing TeamConfig[] format so TeamRegistry
471
+ * constructor is unchanged. Uses a single JOIN query instead of N+1.
472
+ */
473
+ toTeamConfigs() {
474
+ const rows = this.db.prepare(`SELECT t.id AS tid, t.name AS tname, t.color AS tcolor,
475
+ t.workspace AS tworkspace, t.variables AS tvariables,
476
+ t.statuses AS tstatuses,
477
+ a.id, a.name, a.role, a.model, a.context_window, a.max_steps,
478
+ a.identity, a.tools, a.timeout, a.template_id, a.concurrency,
479
+ pt.content AS template_content
480
+ FROM teams t LEFT JOIN agents a ON a.team_id = t.id
481
+ LEFT JOIN prompt_templates pt ON a.template_id = pt.id
482
+ WHERE t.id != ?
483
+ ORDER BY t.created_at ASC, a.role ASC, a.created_at ASC`).all(DEFAULT_TEAM_ID);
484
+ // Batch-load all subscriptions into a map
485
+ const allSubs = this.db.prepare("SELECT agent_id, task_status FROM agent_subscriptions").all();
486
+ const subsMap = new Map();
487
+ for (const s of allSubs) {
488
+ if (!subsMap.has(s.agent_id))
489
+ subsMap.set(s.agent_id, []);
490
+ subsMap.get(s.agent_id).push(s.task_status);
491
+ }
492
+ const teamMap = new Map();
493
+ for (const row of rows) {
494
+ const tid = row.tid;
495
+ if (!teamMap.has(tid)) {
496
+ let variables = {};
497
+ try {
498
+ variables = JSON.parse(row.tvariables || "{}");
499
+ }
500
+ catch { /* ignore */ }
501
+ let statuses;
502
+ if (typeof row.tstatuses === "string" && row.tstatuses) {
503
+ try {
504
+ statuses = JSON.parse(row.tstatuses);
505
+ }
506
+ catch { /* ignore */ }
507
+ }
508
+ teamMap.set(tid, {
509
+ id: tid,
510
+ name: row.tname,
511
+ color: row.tcolor ?? "",
512
+ workspace: row.tworkspace ?? "",
513
+ variables,
514
+ statuses,
515
+ orchestrator: { ...FALLBACK_ORCHESTRATOR },
516
+ workers: [],
517
+ });
518
+ }
519
+ // LEFT JOIN may produce a row with no agent columns
520
+ if (!row.id)
521
+ continue;
522
+ const team = teamMap.get(tid);
523
+ const agentId = row.id;
524
+ const agent = toAgentConfig(row, team.variables, subsMap.get(agentId));
525
+ if (row.role === "orchestrator") {
526
+ team.orchestrator = agent;
527
+ }
528
+ else {
529
+ team.workers.push(agent);
530
+ }
531
+ }
532
+ return [...teamMap.values()];
533
+ }
534
+ close() {
535
+ this.db.close();
536
+ logger.info("Team store closed");
537
+ }
538
+ }
539
+ function toTeam(row) {
540
+ let statuses;
541
+ if (typeof row.statuses === "string" && row.statuses) {
542
+ try {
543
+ statuses = JSON.parse(row.statuses);
544
+ }
545
+ catch { /* ignore */ }
546
+ }
547
+ return {
548
+ id: row.id,
549
+ name: row.name,
550
+ color: row.color ?? "",
551
+ workspace: row.workspace ?? "",
552
+ statuses,
553
+ createdAt: row.created_at,
554
+ updatedAt: row.updated_at,
555
+ };
556
+ }
557
+ function toAgentConfig(row, teamVariables, subscriptions) {
558
+ let tools = [];
559
+ try {
560
+ tools = JSON.parse(row.tools || "[]");
561
+ }
562
+ catch {
563
+ tools = [];
564
+ }
565
+ const templateId = row.template_id ?? null;
566
+ const identity = resolveAgentIdentity({
567
+ templateId,
568
+ templateContent: row.template_content ?? null,
569
+ rawIdentity: row.identity ?? "",
570
+ teamName: row.tname ?? "",
571
+ workspace: row.tworkspace ?? "",
572
+ teamVariables,
573
+ });
574
+ return {
575
+ id: row.id,
576
+ name: row.name,
577
+ model: row.model,
578
+ contextWindow: row.context_window ?? 0,
579
+ maxSteps: row.max_steps ?? 0,
580
+ identity,
581
+ tools,
582
+ timeout: row.timeout ?? DEFAULT_WORKER_TIMEOUT_S,
583
+ templateId,
584
+ subscriptions: subscriptions ?? [],
585
+ concurrency: row.concurrency ?? 1,
586
+ };
587
+ }
588
+ function validateVariables(variables) {
589
+ const keys = Object.keys(variables);
590
+ if (keys.length > MAX_VARIABLES_COUNT) {
591
+ throw new Error(`Too many variables (max ${MAX_VARIABLES_COUNT})`);
592
+ }
593
+ for (const key of keys) {
594
+ if (!VARIABLE_KEY_RE.test(key)) {
595
+ throw new Error(`Invalid variable key "${key}": only letters, numbers, and underscores allowed`);
596
+ }
597
+ if (typeof variables[key] !== "string") {
598
+ throw new Error(`Variable "${key}" must have a string value`);
599
+ }
600
+ if (variables[key].length > MAX_VARIABLE_VALUE_LENGTH) {
601
+ throw new Error(`Variable "${key}" value exceeds maximum length of ${MAX_VARIABLE_VALUE_LENGTH}`);
602
+ }
603
+ }
604
+ }
605
+ /** Legacy fallback for teams without custom statuses. */
606
+ const LEGACY_VALID_STATUSES = new Set(LEGACY_STATUS_KEYS);
607
+ function validateSubscriptions(statuses, validStatuses) {
608
+ const allowed = validStatuses ? new Set(validStatuses) : LEGACY_VALID_STATUSES;
609
+ for (const s of statuses) {
610
+ if (!allowed.has(s)) {
611
+ const validList = validStatuses ? validStatuses.join(", ") : LEGACY_STATUS_KEYS.join(", ");
612
+ throw new Error(`Invalid subscription status "${s}". Valid: ${validList}`);
613
+ }
614
+ }
615
+ }
616
+ function toAgentRow(row, subscriptions, identityOverride) {
617
+ let tools = [];
618
+ try {
619
+ tools = JSON.parse(row.tools || "[]");
620
+ }
621
+ catch {
622
+ tools = [];
623
+ }
624
+ return {
625
+ id: row.id,
626
+ teamId: row.team_id,
627
+ name: row.name,
628
+ role: row.role,
629
+ model: row.model,
630
+ contextWindow: row.context_window ?? 0,
631
+ maxSteps: row.max_steps ?? 0,
632
+ identity: identityOverride ?? (row.identity ?? ""),
633
+ tools,
634
+ timeout: row.timeout ?? DEFAULT_WORKER_TIMEOUT_S,
635
+ templateId: row.template_id ?? null,
636
+ subscriptions: subscriptions ?? [],
637
+ concurrency: row.concurrency ?? 1,
638
+ createdAt: row.created_at,
639
+ updatedAt: row.updated_at,
640
+ };
641
+ }
642
+ function parseTeamVariables(raw) {
643
+ if (typeof raw !== "string" || raw.length === 0)
644
+ return {};
645
+ try {
646
+ const parsed = JSON.parse(raw);
647
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
648
+ return {};
649
+ const out = {};
650
+ for (const [key, value] of Object.entries(parsed)) {
651
+ if (typeof value === "string")
652
+ out[key] = value;
653
+ }
654
+ return out;
655
+ }
656
+ catch {
657
+ return {};
658
+ }
659
+ }
660
+ function resolveAgentIdentity(input) {
661
+ const { templateId, templateContent, rawIdentity, teamName, workspace, teamVariables, } = input;
662
+ const baseIdentity = templateId && templateContent != null ? templateContent : rawIdentity;
663
+ if (!baseIdentity)
664
+ return "";
665
+ const variables = {
666
+ teamName,
667
+ workspace,
668
+ ...(teamVariables ?? {}),
669
+ };
670
+ return baseIdentity.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? `{{${key}}}`);
671
+ }