botinabox 0.6.0 → 1.1.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.
@@ -39,8 +39,23 @@ declare function createSlackAdapter(client?: BoltClient): SlackAdapter;
39
39
  * Story 4.5
40
40
  */
41
41
 
42
+ interface SlackFile {
43
+ id?: string;
44
+ filetype?: string;
45
+ subtype?: string;
46
+ url_private?: string;
47
+ preview?: string;
48
+ transcription?: {
49
+ status?: string;
50
+ preview?: {
51
+ content?: string;
52
+ };
53
+ };
54
+ [key: string]: unknown;
55
+ }
42
56
  interface SlackEvent {
43
57
  type: string;
58
+ subtype?: string;
44
59
  client_msg_id?: string;
45
60
  ts?: string;
46
61
  event_ts?: string;
@@ -48,10 +63,21 @@ interface SlackEvent {
48
63
  user?: string;
49
64
  text?: string;
50
65
  thread_ts?: string;
66
+ files?: SlackFile[];
51
67
  [key: string]: unknown;
52
68
  }
69
+ /**
70
+ * Extract the text content from a voice message file.
71
+ * Prefers the transcription preview; falls back to the file preview text.
72
+ * Returns null if the file is not a voice message or has no transcript.
73
+ */
74
+ declare function extractVoiceTranscript(file: SlackFile): string | null;
53
75
  /**
54
76
  * Parse a Slack event into an InboundMessage.
77
+ *
78
+ * Handles standard text messages and voice messages (file_share subtype
79
+ * with audio files). Voice message transcripts are extracted and prefixed
80
+ * with `[Voice message]`.
55
81
  */
56
82
  declare function parseSlackEvent(event: SlackEvent): InboundMessage;
57
83
 
@@ -78,4 +104,4 @@ interface SlackConfig {
78
104
  signingSecret?: string;
79
105
  }
80
106
 
81
- export { type BoltClient, SlackAdapter, type SlackConfig, type SlackEvent, createSlackAdapter as default, formatForSlack, parseSlackEvent };
107
+ export { type BoltClient, SlackAdapter, type SlackConfig, type SlackEvent, type SlackFile, createSlackAdapter as default, extractVoiceTranscript, formatForSlack, parseSlackEvent };
@@ -1,6 +1,7 @@
1
1
  import {
2
+ extractVoiceTranscript,
2
3
  parseSlackEvent
3
- } from "../../chunk-QLA6YOFN.js";
4
+ } from "../../chunk-2LGXQPEA.js";
4
5
 
5
6
  // src/channels/slack/outbound.ts
6
7
  function formatForSlack(text) {
@@ -63,7 +64,7 @@ var SlackAdapter = class {
63
64
  /** Simulate receiving an inbound message (for testing/webhooks). */
64
65
  async receive(event) {
65
66
  if (this.onMessage) {
66
- const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-AFOHYNUY.js");
67
+ const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-CGIXRXGC.js");
67
68
  const msg = parseSlackEvent2(event);
68
69
  await this.onMessage(msg);
69
70
  }
@@ -75,6 +76,7 @@ function createSlackAdapter(client) {
75
76
  export {
76
77
  SlackAdapter,
77
78
  createSlackAdapter as default,
79
+ extractVoiceTranscript,
78
80
  formatForSlack,
79
81
  parseSlackEvent
80
82
  };
@@ -0,0 +1,41 @@
1
+ // src/channels/slack/inbound.ts
2
+ var AUDIO_TYPES = /* @__PURE__ */ new Set(["aac", "mp4", "m4a", "ogg", "webm", "mp3", "wav"]);
3
+ function extractVoiceTranscript(file) {
4
+ const isAudio = file.subtype === "slack_audio" || AUDIO_TYPES.has(file.filetype ?? "");
5
+ if (!isAudio) return null;
6
+ const transcript = file.transcription?.preview?.content ?? (typeof file.preview === "string" ? file.preview : null);
7
+ return transcript ?? null;
8
+ }
9
+ function parseSlackEvent(event) {
10
+ const id = event.client_msg_id ?? event.ts ?? event.event_ts ?? `slack-${Date.now()}`;
11
+ const channel = event.channel ?? "unknown";
12
+ const from = event.user ?? "unknown";
13
+ const threadId = event.thread_ts !== void 0 ? event.thread_ts : void 0;
14
+ const receivedAt = event.ts ? new Date(parseFloat(event.ts) * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
15
+ let body = event.text ?? "";
16
+ if (event.subtype === "file_share" && event.files?.length) {
17
+ for (const file of event.files) {
18
+ const transcript = extractVoiceTranscript(file);
19
+ if (transcript) {
20
+ body = body ? `${body}
21
+
22
+ [Voice message] ${transcript}` : `[Voice message] ${transcript}`;
23
+ break;
24
+ }
25
+ }
26
+ }
27
+ return {
28
+ id,
29
+ channel,
30
+ from,
31
+ body,
32
+ threadId,
33
+ receivedAt,
34
+ raw: event
35
+ };
36
+ }
37
+
38
+ export {
39
+ extractVoiceTranscript,
40
+ parseSlackEvent
41
+ };
@@ -0,0 +1,8 @@
1
+ import {
2
+ extractVoiceTranscript,
3
+ parseSlackEvent
4
+ } from "./chunk-2LGXQPEA.js";
5
+ export {
6
+ extractVoiceTranscript,
7
+ parseSlackEvent
8
+ };
package/dist/index.d.ts CHANGED
@@ -324,6 +324,13 @@ declare const AGENT_STATUSES: readonly ["idle", "running", "paused", "terminated
324
324
  /** Run status values */
325
325
  declare const RUN_STATUSES: readonly ["queued", "running", "succeeded", "failed", "cancelled"];
326
326
 
327
+ /** Shared utility functions. */
328
+ /**
329
+ * Truncate text at a word boundary, appending "..." if truncated.
330
+ * Returns the original text if it's shorter than maxLen.
331
+ */
332
+ declare function truncateAtWord(text: string, maxLen: number): string;
333
+
327
334
  type HookHandler = (context: Record<string, unknown>) => Promise<void> | void;
328
335
  type Unsubscribe = () => void;
329
336
  interface HookOptions {
@@ -1012,6 +1019,8 @@ interface DomainSchemaOptions {
1012
1019
  rules?: boolean;
1013
1020
  /** Include event audit log (default: true) */
1014
1021
  events?: boolean;
1022
+ /** Include cross-domain junction tables (default: true) */
1023
+ junctions?: boolean;
1015
1024
  }
1016
1025
  /**
1017
1026
  * Define standard domain tables that most multi-agent apps need.
@@ -1514,6 +1523,15 @@ declare class SecretStore {
1514
1523
  list(): Promise<SecretMeta[]>;
1515
1524
  rotate(name: string, newValue: string, environment?: string): Promise<void>;
1516
1525
  delete(name: string, environment?: string): Promise<void>;
1526
+ /**
1527
+ * Load a sync cursor by key. Returns undefined if not found.
1528
+ * Cursors are stored as secrets with type='sync_cursor'.
1529
+ */
1530
+ loadCursor(key: string): Promise<string | undefined>;
1531
+ /**
1532
+ * Persist a sync cursor by key. Creates or updates the secret.
1533
+ */
1534
+ saveCursor(key: string, value: string): Promise<void>;
1517
1535
  private _toMeta;
1518
1536
  }
1519
1537
 
@@ -1549,4 +1567,4 @@ declare function isLoginRequired(stdout: string): boolean;
1549
1567
  /** Rewrite local image paths to prevent CLI auto-embedding as vision content. */
1550
1568
  declare function deactivateLocalImagePaths(prompt: string): string;
1551
1569
 
1552
- export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatSessionManager, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, ConnectorConfig, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, type DomainEntityContextOptions, type DomainSchemaOptions, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type Filter, HealthStatus, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, LLMProvider, MAX_CHAIN_DEPTH, MessagePipeline, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PkLookup, ProviderRegistry, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type Schedule, type ScheduleDef, Scheduler, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SqliteAdapter, type StepRef, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, validateConfig };
1570
+ export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatSessionManager, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, ConnectorConfig, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, type DomainEntityContextOptions, type DomainSchemaOptions, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type Filter, HealthStatus, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, LLMProvider, MAX_CHAIN_DEPTH, MessagePipeline, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PkLookup, ProviderRegistry, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type Schedule, type ScheduleDef, Scheduler, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SqliteAdapter, type StepRef, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, truncateAtWord, validateConfig };
package/dist/index.js CHANGED
@@ -75,6 +75,15 @@ var RUN_STATUSES = [
75
75
  "cancelled"
76
76
  ];
77
77
 
78
+ // src/shared/utils.ts
79
+ function truncateAtWord(text, maxLen) {
80
+ if (text.length <= maxLen) return text;
81
+ const truncated = text.slice(0, maxLen);
82
+ const lastSpace = truncated.lastIndexOf(" ");
83
+ const cutPoint = lastSpace > maxLen * 0.5 ? lastSpace : maxLen;
84
+ return truncated.slice(0, cutPoint) + "...";
85
+ }
86
+
78
87
  // src/core/hooks/hook-bus.ts
79
88
  var HookBus = class {
80
89
  registrations = /* @__PURE__ */ new Map();
@@ -1763,7 +1772,7 @@ ${s.definition}` : null,
1763
1772
  const dir = r.direction === "outbound" ? "\u2192" : "\u2190";
1764
1773
  const who = r.from_agent ?? r.from_user ?? "unknown";
1765
1774
  const time = (r.created_at ?? "").slice(0, 16);
1766
- const preview = (r.body ?? "").slice(0, 80);
1775
+ const preview = truncateAtWord(r.body ?? "", 80);
1767
1776
  return `- ${dir} **${who}** (${time}): ${preview}`;
1768
1777
  });
1769
1778
  return `# Messages
@@ -1810,6 +1819,7 @@ function defineDomainTables(db, options = {}) {
1810
1819
  channels: true,
1811
1820
  rules: true,
1812
1821
  events: true,
1822
+ junctions: true,
1813
1823
  ...options
1814
1824
  };
1815
1825
  db.define("org", {
@@ -1837,6 +1847,8 @@ function defineDomainTables(db, options = {}) {
1837
1847
  deploy_target: "TEXT",
1838
1848
  production_url: "TEXT",
1839
1849
  branch_strategy: "TEXT",
1850
+ repo_path: "TEXT",
1851
+ codename: "TEXT",
1840
1852
  notes: "TEXT",
1841
1853
  created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1842
1854
  updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
@@ -1877,6 +1889,7 @@ function defineDomainTables(db, options = {}) {
1877
1889
  contact_name: "TEXT",
1878
1890
  contact_email: "TEXT",
1879
1891
  phone: "TEXT",
1892
+ address: "TEXT",
1880
1893
  status: "TEXT NOT NULL DEFAULT 'active'",
1881
1894
  notes: "TEXT",
1882
1895
  created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
@@ -1954,6 +1967,7 @@ function defineDomainTables(db, options = {}) {
1954
1967
  project_id: "TEXT",
1955
1968
  access_level: "TEXT NOT NULL DEFAULT 'org'",
1956
1969
  description: "TEXT",
1970
+ tags: "TEXT",
1957
1971
  notes: "TEXT",
1958
1972
  created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1959
1973
  updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
@@ -1987,6 +2001,8 @@ function defineDomainTables(db, options = {}) {
1987
2001
  scope: "TEXT NOT NULL DEFAULT 'org'",
1988
2002
  category: "TEXT NOT NULL DEFAULT 'process'",
1989
2003
  priority: "INTEGER NOT NULL DEFAULT 50",
2004
+ rationale: "TEXT",
2005
+ enforcement: "TEXT DEFAULT 'advisory'",
1990
2006
  created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1991
2007
  updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
1992
2008
  deleted_at: "TEXT"
@@ -2030,6 +2046,7 @@ function defineDomainTables(db, options = {}) {
2030
2046
  actor_user_id: "TEXT",
2031
2047
  project_id: "TEXT",
2032
2048
  channel_id: "TEXT",
2049
+ source: "TEXT",
2033
2050
  created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
2034
2051
  deleted_at: "TEXT"
2035
2052
  },
@@ -2039,6 +2056,58 @@ function defineDomainTables(db, options = {}) {
2039
2056
  ]
2040
2057
  });
2041
2058
  }
2059
+ if (opts.junctions) {
2060
+ db.define("secret_client", {
2061
+ columns: {
2062
+ secret_id: "TEXT NOT NULL",
2063
+ client_id: "TEXT NOT NULL",
2064
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2065
+ },
2066
+ primaryKey: ["secret_id", "client_id"]
2067
+ });
2068
+ db.define("secret_user", {
2069
+ columns: {
2070
+ secret_id: "TEXT NOT NULL",
2071
+ user_id: "TEXT NOT NULL",
2072
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2073
+ },
2074
+ primaryKey: ["secret_id", "user_id"]
2075
+ });
2076
+ db.define("secret_repository", {
2077
+ columns: {
2078
+ secret_id: "TEXT NOT NULL",
2079
+ repository_id: "TEXT NOT NULL",
2080
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2081
+ },
2082
+ primaryKey: ["secret_id", "repository_id"]
2083
+ });
2084
+ db.define("file_agent", {
2085
+ columns: {
2086
+ file_id: "TEXT NOT NULL",
2087
+ agent_id: "TEXT NOT NULL",
2088
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2089
+ },
2090
+ primaryKey: ["file_id", "agent_id"]
2091
+ });
2092
+ db.define("user_channel", {
2093
+ columns: {
2094
+ user_id: "TEXT NOT NULL",
2095
+ channel_id: "TEXT NOT NULL",
2096
+ role: "TEXT",
2097
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2098
+ },
2099
+ primaryKey: ["user_id", "channel_id"]
2100
+ });
2101
+ db.define("user_project", {
2102
+ columns: {
2103
+ user_id: "TEXT NOT NULL",
2104
+ project_id: "TEXT NOT NULL",
2105
+ role: "TEXT",
2106
+ created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
2107
+ },
2108
+ primaryKey: ["user_id", "project_id"]
2109
+ });
2110
+ }
2042
2111
  }
2043
2112
 
2044
2113
  // src/core/data/domain-entity-contexts.ts
@@ -2151,7 +2220,32 @@ ${lines.join("\n\n")}
2151
2220
  },
2152
2221
  omitIfEmpty: true
2153
2222
  }
2154
- } : {}
2223
+ } : {},
2224
+ "MESSAGES.md": {
2225
+ source: {
2226
+ type: "hasMany",
2227
+ table: "messages",
2228
+ foreignKey: "project_id",
2229
+ orderBy: "created_at",
2230
+ limit: 100
2231
+ },
2232
+ render: (rows) => {
2233
+ if (!rows.length) return "# Messages\n\nNo messages.\n";
2234
+ const lines = rows.map((r) => {
2235
+ const dir = r.direction === "inbound" ? "\u2192" : "\u2190";
2236
+ const ts = (r.created_at ?? "").slice(0, 16);
2237
+ const agent = r.from_agent ? ` [${r.from_agent}]` : "";
2238
+ const body = r.body ?? "";
2239
+ const preview = truncateAtWord(body, 150);
2240
+ return `- ${dir} **${ts}**${agent} ${preview}`;
2241
+ });
2242
+ return `# Messages
2243
+
2244
+ ${lines.join("\n")}
2245
+ `;
2246
+ },
2247
+ omitIfEmpty: false
2248
+ }
2155
2249
  }
2156
2250
  });
2157
2251
  if (opts.clients) {
@@ -4039,6 +4133,44 @@ var SecretStore = class {
4039
4133
  });
4040
4134
  await this.hooks.emit("secret.deleted", { name, environment });
4041
4135
  }
4136
+ // ── Cursor persistence helpers ──────────────────────────────────
4137
+ /**
4138
+ * Load a sync cursor by key. Returns undefined if not found.
4139
+ * Cursors are stored as secrets with type='sync_cursor'.
4140
+ */
4141
+ async loadCursor(key) {
4142
+ const name = `sync-cursor:${key}`;
4143
+ const rows = await this.db.query("secrets", {
4144
+ where: { name },
4145
+ filters: [{ col: "deleted_at", op: "isNull" }],
4146
+ limit: 1
4147
+ });
4148
+ return rows[0]?.value ?? void 0;
4149
+ }
4150
+ /**
4151
+ * Persist a sync cursor by key. Creates or updates the secret.
4152
+ */
4153
+ async saveCursor(key, value) {
4154
+ const name = `sync-cursor:${key}`;
4155
+ const rows = await this.db.query("secrets", {
4156
+ where: { name },
4157
+ filters: [{ col: "deleted_at", op: "isNull" }],
4158
+ limit: 1
4159
+ });
4160
+ if (rows.length > 0) {
4161
+ await this.db.update("secrets", rows[0].id, {
4162
+ value,
4163
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
4164
+ });
4165
+ } else {
4166
+ await this.db.insert("secrets", {
4167
+ id: uuidv42(),
4168
+ name,
4169
+ type: "sync_cursor",
4170
+ value
4171
+ });
4172
+ }
4173
+ }
4042
4174
  _toMeta(row) {
4043
4175
  return {
4044
4176
  id: row.id,
@@ -4213,5 +4345,6 @@ export {
4213
4345
  runPackageMigrations,
4214
4346
  sanitize,
4215
4347
  topologicalSort,
4348
+ truncateAtWord,
4216
4349
  validateConfig
4217
4350
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "0.6.0",
3
+ "version": "1.1.0",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -59,7 +59,7 @@
59
59
  "@types/uuid": "^10.0.0",
60
60
  "ajv": "^8.17.1",
61
61
  "cron-parser": "^4.9.0",
62
- "latticesql": "^0.18.0",
62
+ "latticesql": "^1.0.0",
63
63
  "uuid": "^13.0.0",
64
64
  "yaml": "^2.7.0"
65
65
  },