botinabox 1.0.0 → 1.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/index.js CHANGED
@@ -35,8 +35,6 @@ var EVENTS = {
35
35
  var DEFAULTS = {
36
36
  TASK_POLL_INTERVAL_MS: 3e4,
37
37
  NOTIFICATION_POLL_INTERVAL_MS: 5e3,
38
- HEARTBEAT_INTERVAL_MS: 3e5,
39
- // 5 minutes
40
38
  ORPHAN_REAP_INTERVAL_MS: 3e5,
41
39
  // 5 minutes
42
40
  STALE_RUN_THRESHOLD_MS: 18e5,
@@ -77,6 +75,15 @@ var RUN_STATUSES = [
77
75
  "cancelled"
78
76
  ];
79
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
+
80
87
  // src/core/hooks/hook-bus.ts
81
88
  var HookBus = class {
82
89
  registrations = /* @__PURE__ */ new Map();
@@ -1155,15 +1162,17 @@ var DataStore = class {
1155
1162
  outputFile: def.indexFile,
1156
1163
  render: def.indexRender ?? ((rows) => {
1157
1164
  const active = rows.filter((r) => r.deleted_at == null);
1158
- const title = def.directory.charAt(0).toUpperCase() + def.directory.slice(1);
1165
+ const dir = def.directory;
1166
+ const title = dir.charAt(0).toUpperCase() + dir.slice(1);
1159
1167
  if (!active.length) return `# ${title}
1160
1168
 
1161
1169
  None.
1162
1170
  `;
1163
1171
  const lines = active.map((r) => {
1164
- const name2 = String(r.name ?? r[def.slugColumn] ?? r.id ?? "unknown");
1172
+ const slug = String(r[def.slugColumn] ?? r.name ?? r.id ?? "unknown");
1173
+ const name2 = String(r.name ?? slug);
1165
1174
  const status = r.status ? ` (${r.status})` : "";
1166
- return `- **${name2}**${status}`;
1175
+ return `- [${name2}](${dir}/${slug}/)${status}`;
1167
1176
  });
1168
1177
  return `# ${title}
1169
1178
 
@@ -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) {
@@ -2633,6 +2727,73 @@ async function runPackageMigrations(db, migrations) {
2633
2727
  );
2634
2728
  }
2635
2729
 
2730
+ // src/core/update/auto-update.ts
2731
+ import { execSync as execSync2 } from "child_process";
2732
+ import { readFileSync as readFileSync3 } from "fs";
2733
+ import { join as join5 } from "path";
2734
+ function getInstalledVersion(pkgName) {
2735
+ try {
2736
+ const pkgPath = join5(process.cwd(), "node_modules", pkgName, "package.json");
2737
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
2738
+ return pkg.version;
2739
+ } catch {
2740
+ return null;
2741
+ }
2742
+ }
2743
+ async function getLatestVersion(pkgName) {
2744
+ try {
2745
+ const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
2746
+ headers: { accept: "application/json" },
2747
+ signal: AbortSignal.timeout(5e3)
2748
+ });
2749
+ if (!res.ok) return null;
2750
+ const data = await res.json();
2751
+ return data.version;
2752
+ } catch {
2753
+ return null;
2754
+ }
2755
+ }
2756
+ function isNewer(latest, current) {
2757
+ const a = latest.split(".").map(Number);
2758
+ const b = current.split(".").map(Number);
2759
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
2760
+ if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
2761
+ if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
2762
+ }
2763
+ return false;
2764
+ }
2765
+ async function autoUpdate(packages = ["botinabox", "latticesql"], opts) {
2766
+ const log = opts?.quiet ? () => {
2767
+ } : console.log;
2768
+ const result = { updated: false, packages: [], restartRequired: false };
2769
+ const toInstall = [];
2770
+ for (const pkg of packages) {
2771
+ const installed = getInstalledVersion(pkg);
2772
+ if (!installed) continue;
2773
+ const latest = await getLatestVersion(pkg);
2774
+ if (!latest) continue;
2775
+ if (isNewer(latest, installed)) {
2776
+ toInstall.push(`${pkg}@${latest}`);
2777
+ result.packages.push({ name: pkg, from: installed, to: latest });
2778
+ }
2779
+ }
2780
+ if (toInstall.length === 0) return result;
2781
+ log(`[autoUpdate] Updating: ${toInstall.join(", ")}`);
2782
+ try {
2783
+ execSync2(`npm install ${toInstall.join(" ")}`, {
2784
+ cwd: process.cwd(),
2785
+ stdio: opts?.quiet ? "ignore" : "inherit",
2786
+ timeout: 6e4
2787
+ });
2788
+ result.updated = true;
2789
+ result.restartRequired = true;
2790
+ log(`[autoUpdate] Updated successfully. Restart required for changes to take effect.`);
2791
+ } catch (err) {
2792
+ console.error("[autoUpdate] Failed to install updates:", err);
2793
+ }
2794
+ return result;
2795
+ }
2796
+
2636
2797
  // src/core/orchestrator/config-revisions.ts
2637
2798
  async function createConfigRevision(db, agentId, before, after) {
2638
2799
  const existing = await db.query("config_revisions", {
@@ -3406,38 +3567,6 @@ var NdjsonLogger = class {
3406
3567
  }
3407
3568
  };
3408
3569
 
3409
- // src/core/orchestrator/heartbeat-scheduler.ts
3410
- var HeartbeatScheduler = class {
3411
- constructor(wakeupQueue, hooks) {
3412
- this.wakeupQueue = wakeupQueue;
3413
- this.hooks = hooks;
3414
- }
3415
- wakeupQueue;
3416
- hooks;
3417
- timers = /* @__PURE__ */ new Map();
3418
- start(agents) {
3419
- for (const agent of agents) {
3420
- let config;
3421
- try {
3422
- config = JSON.parse(agent.heartbeat_config);
3423
- } catch {
3424
- continue;
3425
- }
3426
- if (!config.enabled || !config.intervalSec) continue;
3427
- const timer = setInterval(() => {
3428
- void this.wakeupQueue.enqueue(agent.id, "heartbeat");
3429
- }, config.intervalSec * 1e3);
3430
- this.timers.set(agent.id, timer);
3431
- }
3432
- }
3433
- stop() {
3434
- for (const timer of this.timers.values()) {
3435
- clearInterval(timer);
3436
- }
3437
- this.timers.clear();
3438
- }
3439
- };
3440
-
3441
3570
  // src/core/orchestrator/scheduler.ts
3442
3571
  import cronParser from "cron-parser";
3443
3572
  import { v4 as uuid } from "uuid";
@@ -3627,7 +3756,7 @@ var Scheduler = class {
3627
3756
  };
3628
3757
 
3629
3758
  // src/core/orchestrator/adapters/api-adapter.ts
3630
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
3759
+ import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
3631
3760
 
3632
3761
  // src/core/orchestrator/adapters/tool-loop.ts
3633
3762
  async function* toolLoop(params, callLLM, executeTool) {
@@ -3714,7 +3843,7 @@ var ApiExecutionAdapter = class {
3714
3843
  const fileContents = [];
3715
3844
  for (const filePath of ctx.contextFiles) {
3716
3845
  if (existsSync2(filePath)) {
3717
- const content = readFileSync3(filePath, "utf8");
3846
+ const content = readFileSync4(filePath, "utf8");
3718
3847
  fileContents.push(`<file path="${filePath}">
3719
3848
  ${content}
3720
3849
  </file>`);
@@ -3830,7 +3959,7 @@ function killProcessGroup(pid, signal = "SIGTERM") {
3830
3959
  }
3831
3960
 
3832
3961
  // src/core/orchestrator/adapters/output-extractor.ts
3833
- import { readFileSync as readFileSync4 } from "fs";
3962
+ import { readFileSync as readFileSync5 } from "fs";
3834
3963
  var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
3835
3964
  function extractOutput(ndjsonContent) {
3836
3965
  const lines = ndjsonContent.split("\n").filter((l) => l.trim().length > 0);
@@ -4071,6 +4200,44 @@ var SecretStore = class {
4071
4200
  });
4072
4201
  await this.hooks.emit("secret.deleted", { name, environment });
4073
4202
  }
4203
+ // ── Cursor persistence helpers ──────────────────────────────────
4204
+ /**
4205
+ * Load a sync cursor by key. Returns undefined if not found.
4206
+ * Cursors are stored as secrets with type='sync_cursor'.
4207
+ */
4208
+ async loadCursor(key) {
4209
+ const name = `sync-cursor:${key}`;
4210
+ const rows = await this.db.query("secrets", {
4211
+ where: { name },
4212
+ filters: [{ col: "deleted_at", op: "isNull" }],
4213
+ limit: 1
4214
+ });
4215
+ return rows[0]?.value ?? void 0;
4216
+ }
4217
+ /**
4218
+ * Persist a sync cursor by key. Creates or updates the secret.
4219
+ */
4220
+ async saveCursor(key, value) {
4221
+ const name = `sync-cursor:${key}`;
4222
+ const rows = await this.db.query("secrets", {
4223
+ where: { name },
4224
+ filters: [{ col: "deleted_at", op: "isNull" }],
4225
+ limit: 1
4226
+ });
4227
+ if (rows.length > 0) {
4228
+ await this.db.update("secrets", rows[0].id, {
4229
+ value,
4230
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
4231
+ });
4232
+ } else {
4233
+ await this.db.insert("secrets", {
4234
+ id: uuidv42(),
4235
+ name,
4236
+ type: "sync_cursor",
4237
+ value
4238
+ });
4239
+ }
4240
+ }
4074
4241
  _toMeta(row) {
4075
4242
  return {
4076
4243
  id: row.id,
@@ -4192,7 +4359,6 @@ export {
4192
4359
  DataStore,
4193
4360
  DataStoreError,
4194
4361
  EVENTS,
4195
- HeartbeatScheduler,
4196
4362
  HookBus,
4197
4363
  MAX_CHAIN_DEPTH,
4198
4364
  MessagePipeline,
@@ -4215,6 +4381,7 @@ export {
4215
4381
  WorkflowEngine,
4216
4382
  _resetConfig,
4217
4383
  areDependenciesMet,
4384
+ autoUpdate,
4218
4385
  buildAgentBindings,
4219
4386
  buildChainOrigin,
4220
4387
  buildProcessEnv,
@@ -4246,5 +4413,6 @@ export {
4246
4413
  runPackageMigrations,
4247
4414
  sanitize,
4248
4415
  topologicalSort,
4416
+ truncateAtWord,
4249
4417
  validateConfig
4250
4418
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",