botinabox 2.16.16 → 2.16.18

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.
@@ -117,6 +117,27 @@ export interface ChatPipelineV2Config {
117
117
  messageText: string;
118
118
  channel: string;
119
119
  }) => Promise<ContextFile[]> | ContextFile[];
120
+ /**
121
+ * Optional per-turn tool-context resolver. Called once per inbound message
122
+ * with the resolved conversation coordinates (the same shape passed to
123
+ * `resolveContextFiles`). The returned fields are merged into the
124
+ * `ToolContext` handed to every tool handler for that turn — letting an app
125
+ * thread per-turn identity (e.g. which user the primary agent is acting on
126
+ * behalf of) into tool execution, which the static config cannot express.
127
+ *
128
+ * The base `ToolContext` fields (`taskId`, `agentId`, `hooks`, `db`,
129
+ * `resolveFilePath`) are applied first and cannot be overridden by the
130
+ * resolver — returned keys that collide with them are ignored. If the
131
+ * resolver throws, the error propagates to the turn's try/catch and fails
132
+ * loudly; there is no silent fallback.
133
+ */
134
+ resolveToolContext?: (ctx: {
135
+ channelId: string;
136
+ threadId: string;
137
+ userId?: string;
138
+ messageText: string;
139
+ channel: string;
140
+ }) => Promise<Record<string, unknown>> | Record<string, unknown>;
120
141
  }
121
142
  export declare class ChatPipelineV2 {
122
143
  private db;
@@ -41,6 +41,17 @@ export declare class SecretStore {
41
41
  set(input: SecretInput): Promise<SecretMeta>;
42
42
  get(name: string, environment?: string): Promise<string | null>;
43
43
  getMeta(name: string, environment?: string): Promise<SecretMeta | null>;
44
+ /**
45
+ * Fetch the most recently created live row for (name, environment).
46
+ *
47
+ * `set()` keeps this to a single row in steady state, but we still pick the
48
+ * newest in JS rather than rely on a SQL `LIMIT 1` (nondeterministic without
49
+ * an ORDER BY) or a dialect/version-specific `orderBy` form: legacy data
50
+ * written before the upsert fix can still have duplicate live rows, and JS
51
+ * sorting resolves them deterministically across SQLite and Postgres. (Same
52
+ * cross-dialect-sort rationale as the chat memory resolver.)
53
+ */
54
+ private _latestRow;
44
55
  list(): Promise<SecretMeta[]>;
45
56
  rotate(name: string, newValue: string, environment?: string): Promise<void>;
46
57
  delete(name: string, environment?: string): Promise<void>;
package/dist/index.js CHANGED
@@ -2345,13 +2345,24 @@ ${ctx2}`;
2345
2345
 
2346
2346
  ${contextFilesBlock}`;
2347
2347
  }
2348
+ let toolContextExtra = {};
2349
+ if (this.config.resolveToolContext) {
2350
+ toolContextExtra = await this.config.resolveToolContext({
2351
+ channelId,
2352
+ threadId: threadTs,
2353
+ userId: msg.from,
2354
+ messageText: msg.body,
2355
+ channel: this.channel
2356
+ }) ?? {};
2357
+ }
2348
2358
  const { text, tasksDispatched } = await this.think(
2349
2359
  systemPrompt,
2350
2360
  history,
2351
2361
  msg.body,
2352
2362
  threadTs,
2353
2363
  channelId,
2354
- msg.attachmentBlocks
2364
+ msg.attachmentBlocks,
2365
+ toolContextExtra
2355
2366
  );
2356
2367
  await this.hooks.emit("typing.stop", { channel: this.channel, threadId: threadTs });
2357
2368
  if (text) {
@@ -2399,7 +2410,7 @@ ${contextFilesBlock}`;
2399
2410
  /**
2400
2411
  * Primary agent tool loop — adapted from ExecutionEngine pattern.
2401
2412
  */
2402
- async think(systemPrompt, history, currentMessage, threadTs, channelId, attachmentBlocks) {
2413
+ async think(systemPrompt, history, currentMessage, threadTs, channelId, attachmentBlocks, toolContextExtra) {
2403
2414
  const model = this.config.model ?? "claude-sonnet-4-6";
2404
2415
  const maxIterations = this.config.maxIterations ?? DEFAULT_MAX_ITERATIONS;
2405
2416
  const maxTokens = this.config.maxTokens ?? DEFAULT_MAX_TOKENS;
@@ -2432,6 +2443,9 @@ ${contextFilesBlock}`;
2432
2443
  if (handler) {
2433
2444
  try {
2434
2445
  const toolCtx = {
2446
+ // Per-turn extras first; base fields applied last so the
2447
+ // resolver can never override taskId/agentId/hooks/db.
2448
+ ...toolContextExtra,
2435
2449
  taskId: "",
2436
2450
  agentId: "primary",
2437
2451
  hooks: this.hooks,
@@ -7510,35 +7524,65 @@ var SecretStore = class {
7510
7524
  this.encKey = encryptionKey ? deriveEncKey(encryptionKey) : null;
7511
7525
  }
7512
7526
  async set(input) {
7513
- const id = uuidv42();
7514
- const data = { ...input, id };
7515
- if (this.encKey && data.value) {
7516
- data.value = encryptValue(data.value, this.encKey);
7527
+ const environment = input.environment ?? "production";
7528
+ const value = this.encKey && input.value ? encryptValue(input.value, this.encKey) : input.value;
7529
+ const existing = await this.db.query("secrets", {
7530
+ where: { name: input.name, environment },
7531
+ filters: [{ col: "deleted_at", op: "isNull" }],
7532
+ limit: 1
7533
+ });
7534
+ if (existing.length > 0) {
7535
+ const id2 = existing[0].id;
7536
+ const changes = {
7537
+ value,
7538
+ environment,
7539
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
7540
+ };
7541
+ if (input.type !== void 0) changes.type = input.type;
7542
+ if (input.description !== void 0) changes.description = input.description;
7543
+ await this.db.update("secrets", id2, changes);
7544
+ await this.hooks.emit("secret.updated", { name: input.name });
7545
+ const updated = await this.db.get("secrets", id2);
7546
+ return this._toMeta(updated);
7517
7547
  }
7518
- await this.db.insert("secrets", data);
7548
+ const id = uuidv42();
7549
+ await this.db.insert("secrets", { ...input, id, value, environment });
7519
7550
  await this.hooks.emit("secret.created", { name: input.name });
7520
7551
  const inserted = await this.db.get("secrets", id);
7521
7552
  return this._toMeta(inserted);
7522
7553
  }
7523
7554
  async get(name, environment = "production") {
7524
- const rows = await this.db.query("secrets", {
7525
- where: { name, environment },
7526
- filters: [{ col: "deleted_at", op: "isNull" }],
7527
- limit: 1
7528
- });
7529
- if (rows.length === 0) return null;
7555
+ const row = await this._latestRow(name, environment);
7556
+ if (!row) return null;
7530
7557
  await this.hooks.emit("secret.accessed", { name, environment });
7531
- const raw = rows[0].value ?? null;
7558
+ const raw = row.value ?? null;
7532
7559
  if (raw && this.encKey) return decryptValue(raw, this.encKey);
7533
7560
  return raw;
7534
7561
  }
7535
7562
  async getMeta(name, environment = "production") {
7563
+ const row = await this._latestRow(name, environment);
7564
+ return row ? this._toMeta(row) : null;
7565
+ }
7566
+ /**
7567
+ * Fetch the most recently created live row for (name, environment).
7568
+ *
7569
+ * `set()` keeps this to a single row in steady state, but we still pick the
7570
+ * newest in JS rather than rely on a SQL `LIMIT 1` (nondeterministic without
7571
+ * an ORDER BY) or a dialect/version-specific `orderBy` form: legacy data
7572
+ * written before the upsert fix can still have duplicate live rows, and JS
7573
+ * sorting resolves them deterministically across SQLite and Postgres. (Same
7574
+ * cross-dialect-sort rationale as the chat memory resolver.)
7575
+ */
7576
+ async _latestRow(name, environment) {
7536
7577
  const rows = await this.db.query("secrets", {
7537
7578
  where: { name, environment },
7538
- filters: [{ col: "deleted_at", op: "isNull" }],
7539
- limit: 1
7579
+ filters: [{ col: "deleted_at", op: "isNull" }]
7540
7580
  });
7541
- return rows.length > 0 ? this._toMeta(rows[0]) : null;
7581
+ if (rows.length === 0) return void 0;
7582
+ rows.sort(
7583
+ (a, b) => String(b.created_at ?? "").localeCompare(String(a.created_at ?? ""))
7584
+ );
7585
+ return rows[0];
7542
7586
  }
7543
7587
  async list() {
7544
7588
  const rows = await this.db.query("secrets", {
package/package.json CHANGED
@@ -1,100 +1,100 @@
1
- {
2
- "name": "botinabox",
3
- "version": "2.16.16",
4
- "description": "Bot in a Box — framework for building multi-agent bots",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
12
- },
13
- "./anthropic": {
14
- "import": "./dist/providers/anthropic/index.js",
15
- "types": "./dist/providers/anthropic/index.d.ts"
16
- },
17
- "./openai": {
18
- "import": "./dist/providers/openai/index.js",
19
- "types": "./dist/providers/openai/index.d.ts"
20
- },
21
- "./ollama": {
22
- "import": "./dist/providers/ollama/index.js",
23
- "types": "./dist/providers/ollama/index.d.ts"
24
- },
25
- "./slack": {
26
- "import": "./dist/channels/slack/index.js",
27
- "types": "./dist/channels/slack/index.d.ts"
28
- },
29
- "./discord": {
30
- "import": "./dist/channels/discord/index.js",
31
- "types": "./dist/channels/discord/index.d.ts"
32
- },
33
- "./webhook": {
34
- "import": "./dist/channels/webhook/index.js",
35
- "types": "./dist/channels/webhook/index.d.ts"
36
- },
37
- "./google": {
38
- "import": "./dist/connectors/google/index.js",
39
- "types": "./dist/connectors/google/index.d.ts"
40
- }
41
- },
42
- "bin": {
43
- "botinabox": "./bin/botinabox.mjs"
44
- },
45
- "files": [
46
- "dist",
47
- "bin"
48
- ],
49
- "engines": {
50
- "node": ">=18"
51
- },
52
- "scripts": {
53
- "build": "tsup && tsc --emitDeclarationOnly",
54
- "test": "vitest run",
55
- "typecheck": "tsc --noEmit",
56
- "check-docs": "echo 'Documentation check passed'",
57
- "prepublishOnly": "npm run build && npm run typecheck && npm test"
58
- },
59
- "dependencies": {
60
- "@types/uuid": "^10.0.0",
61
- "ajv": "^8.17.1",
62
- "cron-parser": "^4.9.0",
63
- "latticesql": "^1.16.4",
64
- "uuid": "^13.0.0",
65
- "yaml": "^2.7.0"
66
- },
67
- "optionalDependencies": {
68
- "whisper-node": "^1.1.1"
69
- },
70
- "peerDependencies": {
71
- "@anthropic-ai/sdk": "^0.52.0",
72
- "googleapis": ">=140.0.0 <200.0.0",
73
- "openai": "^4.104.0"
74
- },
75
- "peerDependenciesMeta": {
76
- "@anthropic-ai/sdk": {
77
- "optional": true
78
- },
79
- "openai": {
80
- "optional": true
81
- },
82
- "googleapis": {
83
- "optional": true
84
- }
85
- },
86
- "repository": {
87
- "type": "git",
88
- "url": "https://github.com/automated-industries/botinabox.git"
89
- },
90
- "devDependencies": {
91
- "@anthropic-ai/sdk": "^0.52.0",
92
- "@types/better-sqlite3": "^7.6.12",
93
- "@types/node": "^22.10.0",
94
- "googleapis": "^171.4.0",
95
- "openai": "^4.104.0",
96
- "tsup": "^8.3.5",
97
- "typescript": "^5.7.2",
98
- "vitest": "^3.0.0"
99
- }
100
- }
1
+ {
2
+ "name": "botinabox",
3
+ "version": "2.16.18",
4
+ "description": "Bot in a Box — framework for building multi-agent bots",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./anthropic": {
14
+ "import": "./dist/providers/anthropic/index.js",
15
+ "types": "./dist/providers/anthropic/index.d.ts"
16
+ },
17
+ "./openai": {
18
+ "import": "./dist/providers/openai/index.js",
19
+ "types": "./dist/providers/openai/index.d.ts"
20
+ },
21
+ "./ollama": {
22
+ "import": "./dist/providers/ollama/index.js",
23
+ "types": "./dist/providers/ollama/index.d.ts"
24
+ },
25
+ "./slack": {
26
+ "import": "./dist/channels/slack/index.js",
27
+ "types": "./dist/channels/slack/index.d.ts"
28
+ },
29
+ "./discord": {
30
+ "import": "./dist/channels/discord/index.js",
31
+ "types": "./dist/channels/discord/index.d.ts"
32
+ },
33
+ "./webhook": {
34
+ "import": "./dist/channels/webhook/index.js",
35
+ "types": "./dist/channels/webhook/index.d.ts"
36
+ },
37
+ "./google": {
38
+ "import": "./dist/connectors/google/index.js",
39
+ "types": "./dist/connectors/google/index.d.ts"
40
+ }
41
+ },
42
+ "bin": {
43
+ "botinabox": "./bin/botinabox.mjs"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "bin"
48
+ ],
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup && tsc --emitDeclarationOnly",
54
+ "test": "vitest run",
55
+ "typecheck": "tsc --noEmit",
56
+ "check-docs": "echo 'Documentation check passed'",
57
+ "prepublishOnly": "npm run build && npm run typecheck && npm test"
58
+ },
59
+ "dependencies": {
60
+ "@types/uuid": "^10.0.0",
61
+ "ajv": "^8.17.1",
62
+ "cron-parser": "^4.9.0",
63
+ "latticesql": "^1.16.4",
64
+ "uuid": "^13.0.0",
65
+ "yaml": "^2.7.0"
66
+ },
67
+ "optionalDependencies": {
68
+ "whisper-node": "^1.1.1"
69
+ },
70
+ "peerDependencies": {
71
+ "@anthropic-ai/sdk": "^0.52.0",
72
+ "googleapis": ">=140.0.0 <200.0.0",
73
+ "openai": "^4.104.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@anthropic-ai/sdk": {
77
+ "optional": true
78
+ },
79
+ "openai": {
80
+ "optional": true
81
+ },
82
+ "googleapis": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "repository": {
87
+ "type": "git",
88
+ "url": "https://github.com/automated-industries/botinabox.git"
89
+ },
90
+ "devDependencies": {
91
+ "@anthropic-ai/sdk": "^0.52.0",
92
+ "@types/better-sqlite3": "^7.6.12",
93
+ "@types/node": "^22.10.0",
94
+ "googleapis": "^171.4.0",
95
+ "openai": "^4.104.0",
96
+ "tsup": "^8.3.5",
97
+ "typescript": "^5.7.2",
98
+ "vitest": "^3.0.0"
99
+ }
100
+ }