engrm 0.3.2 → 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.
@@ -5,9 +5,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
 
6
6
  // src/config.ts
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { homedir, hostname } from "node:os";
8
+ import { homedir, hostname, networkInterfaces } from "node:os";
9
9
  import { join } from "node:path";
10
- import { randomBytes } from "node:crypto";
10
+ import { createHash } from "node:crypto";
11
11
  var CONFIG_DIR = join(homedir(), ".engrm");
12
12
  var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
13
13
  var DB_PATH = join(CONFIG_DIR, "engrm.db");
@@ -16,7 +16,22 @@ function getDbPath() {
16
16
  }
17
17
  function generateDeviceId() {
18
18
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
19
- const suffix = randomBytes(4).toString("hex");
19
+ let mac = "";
20
+ const ifaces = networkInterfaces();
21
+ for (const entries of Object.values(ifaces)) {
22
+ if (!entries)
23
+ continue;
24
+ for (const entry of entries) {
25
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
26
+ mac = entry.mac;
27
+ break;
28
+ }
29
+ }
30
+ if (mac)
31
+ break;
32
+ }
33
+ const material = `${host}:${mac || "no-mac"}`;
34
+ const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
20
35
  return `${host}-${suffix}`;
21
36
  }
22
37
  function createDefaultConfig() {
@@ -58,7 +73,10 @@ function createDefaultConfig() {
58
73
  observer: {
59
74
  enabled: true,
60
75
  mode: "per_event",
61
- model: "haiku"
76
+ model: "sonnet"
77
+ },
78
+ transcript_analysis: {
79
+ enabled: false
62
80
  }
63
81
  };
64
82
  }
@@ -117,6 +135,9 @@ function loadConfig() {
117
135
  enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
118
136
  mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
119
137
  model: asString(config["observer"]?.["model"], defaults.observer.model)
138
+ },
139
+ transcript_analysis: {
140
+ enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
120
141
  }
121
142
  };
122
143
  }
@@ -515,6 +536,56 @@ function runMigrations(db) {
515
536
  }
516
537
  }
517
538
  }
539
+ function ensureObservationTypes(db) {
540
+ try {
541
+ db.exec("INSERT INTO observations (session_id, project_id, type, title, user_id, device_id, agent, created_at, created_at_epoch) " + "VALUES ('_typecheck', 1, 'message', '_test', '_test', '_test', '_test', '2000-01-01', 0)");
542
+ db.exec("DELETE FROM observations WHERE session_id = '_typecheck'");
543
+ } catch {
544
+ db.exec("BEGIN TRANSACTION");
545
+ try {
546
+ db.exec(`
547
+ CREATE TABLE observations_repair (
548
+ id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT,
549
+ project_id INTEGER NOT NULL REFERENCES projects(id),
550
+ type TEXT NOT NULL CHECK (type IN (
551
+ 'bugfix','discovery','decision','pattern','change','feature',
552
+ 'refactor','digest','standard','message')),
553
+ title TEXT NOT NULL, narrative TEXT, facts TEXT, concepts TEXT,
554
+ files_read TEXT, files_modified TEXT,
555
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
556
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN ('active','aging','archived','purged','pinned')),
557
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN ('shared','personal','secret')),
558
+ user_id TEXT NOT NULL, device_id TEXT NOT NULL, agent TEXT DEFAULT 'claude-code',
559
+ created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
560
+ archived_at_epoch INTEGER,
561
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
562
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
563
+ remote_source_id TEXT
564
+ );
565
+ INSERT INTO observations_repair SELECT * FROM observations;
566
+ DROP TABLE observations;
567
+ ALTER TABLE observations_repair RENAME TO observations;
568
+ CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
569
+ CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
570
+ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
571
+ CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
572
+ CREATE INDEX IF NOT EXISTS idx_observations_lifecycle ON observations(lifecycle);
573
+ CREATE INDEX IF NOT EXISTS idx_observations_quality ON observations(quality);
574
+ CREATE INDEX IF NOT EXISTS idx_observations_user ON observations(user_id);
575
+ CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by);
576
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
577
+ DROP TABLE IF EXISTS observations_fts;
578
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
579
+ title, narrative, facts, concepts, content=observations, content_rowid=id
580
+ );
581
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
582
+ `);
583
+ db.exec("COMMIT");
584
+ } catch (err) {
585
+ db.exec("ROLLBACK");
586
+ }
587
+ }
588
+ }
518
589
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
519
590
 
520
591
  // src/storage/sqlite.ts
@@ -581,6 +652,7 @@ class MemDatabase {
581
652
  this.db.exec("PRAGMA foreign_keys = ON");
582
653
  this.vecAvailable = this.loadVecExtension();
583
654
  runMigrations(this.db);
655
+ ensureObservationTypes(this.db);
584
656
  }
585
657
  loadVecExtension() {
586
658
  try {
@@ -1058,7 +1130,7 @@ function estimateTokens(text) {
1058
1130
  }
1059
1131
  function buildSessionContext(db, cwd, options = {}) {
1060
1132
  const opts = typeof options === "number" ? { maxCount: options } : options;
1061
- const tokenBudget = opts.tokenBudget ?? 800;
1133
+ const tokenBudget = opts.tokenBudget ?? 3000;
1062
1134
  const maxCount = opts.maxCount;
1063
1135
  const detected = detectProject(cwd);
1064
1136
  const project = db.getProjectByCanonicalId(detected.canonical_id);
@@ -1080,6 +1152,12 @@ function buildSessionContext(db, cwd, options = {}) {
1080
1152
  AND superseded_by IS NULL
1081
1153
  ORDER BY quality DESC, created_at_epoch DESC
1082
1154
  LIMIT ?`).all(project.id, MAX_PINNED);
1155
+ const MAX_RECENT = 5;
1156
+ const recent = db.db.query(`SELECT * FROM observations
1157
+ WHERE project_id = ? AND lifecycle IN ('active', 'aging')
1158
+ AND superseded_by IS NULL
1159
+ ORDER BY created_at_epoch DESC
1160
+ LIMIT ?`).all(project.id, MAX_RECENT);
1083
1161
  const candidateLimit = maxCount ?? 50;
1084
1162
  const candidates = db.db.query(`SELECT * FROM observations
1085
1163
  WHERE project_id = ? AND lifecycle IN ('active', 'aging')
@@ -1107,6 +1185,12 @@ function buildSessionContext(db, cwd, options = {}) {
1107
1185
  });
1108
1186
  }
1109
1187
  const seenIds = new Set(pinned.map((o) => o.id));
1188
+ const dedupedRecent = recent.filter((o) => {
1189
+ if (seenIds.has(o.id))
1190
+ return false;
1191
+ seenIds.add(o.id);
1192
+ return true;
1193
+ });
1110
1194
  const deduped = candidates.filter((o) => !seenIds.has(o.id));
1111
1195
  for (const obs of crossProjectCandidates) {
1112
1196
  if (!seenIds.has(obs.id)) {
@@ -1123,8 +1207,8 @@ function buildSessionContext(db, cwd, options = {}) {
1123
1207
  return scoreB - scoreA;
1124
1208
  });
1125
1209
  if (maxCount !== undefined) {
1126
- const remaining = Math.max(0, maxCount - pinned.length);
1127
- const all = [...pinned, ...sorted.slice(0, remaining)];
1210
+ const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
1211
+ const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
1128
1212
  return {
1129
1213
  project_name: project.name,
1130
1214
  canonical_id: project.canonical_id,
@@ -1140,6 +1224,11 @@ function buildSessionContext(db, cwd, options = {}) {
1140
1224
  remainingBudget -= cost;
1141
1225
  selected.push(obs);
1142
1226
  }
1227
+ for (const obs of dedupedRecent) {
1228
+ const cost = estimateObservationTokens(obs, selected.length);
1229
+ remainingBudget -= cost;
1230
+ selected.push(obs);
1231
+ }
1143
1232
  for (const obs of sorted) {
1144
1233
  const cost = estimateObservationTokens(obs, selected.length);
1145
1234
  if (remainingBudget - cost < 0 && selected.length > 0)
@@ -1147,7 +1236,7 @@ function buildSessionContext(db, cwd, options = {}) {
1147
1236
  remainingBudget -= cost;
1148
1237
  selected.push(obs);
1149
1238
  }
1150
- const summaries = db.getRecentSummaries(project.id, 2);
1239
+ const summaries = db.getRecentSummaries(project.id, 5);
1151
1240
  let securityFindings = [];
1152
1241
  try {
1153
1242
  const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
@@ -1167,7 +1256,7 @@ function buildSessionContext(db, cwd, options = {}) {
1167
1256
  };
1168
1257
  }
1169
1258
  function estimateObservationTokens(obs, index) {
1170
- const DETAILED_THRESHOLD = 3;
1259
+ const DETAILED_THRESHOLD = 5;
1171
1260
  const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
1172
1261
  if (index >= DETAILED_THRESHOLD) {
1173
1262
  return titleCost;
@@ -1179,7 +1268,7 @@ function formatContextForInjection(context) {
1179
1268
  if (context.observations.length === 0) {
1180
1269
  return `Project: ${context.project_name} (no prior observations)`;
1181
1270
  }
1182
- const DETAILED_COUNT = 3;
1271
+ const DETAILED_COUNT = 5;
1183
1272
  const lines = [
1184
1273
  `## Project Memory: ${context.project_name}`,
1185
1274
  `${context.session_count} relevant observation(s) from prior sessions:`,
@@ -1466,6 +1555,202 @@ function recommendPacks(stacks, installedPacks) {
1466
1555
  return recommendations;
1467
1556
  }
1468
1557
 
1558
+ // src/config.ts
1559
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
1560
+ import { homedir as homedir2, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
1561
+ import { join as join5 } from "node:path";
1562
+ import { createHash as createHash2 } from "node:crypto";
1563
+ var CONFIG_DIR2 = join5(homedir2(), ".engrm");
1564
+ var SETTINGS_PATH2 = join5(CONFIG_DIR2, "settings.json");
1565
+ var DB_PATH2 = join5(CONFIG_DIR2, "engrm.db");
1566
+ function getDbPath2() {
1567
+ return DB_PATH2;
1568
+ }
1569
+ function generateDeviceId2() {
1570
+ const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
1571
+ let mac = "";
1572
+ const ifaces = networkInterfaces2();
1573
+ for (const entries of Object.values(ifaces)) {
1574
+ if (!entries)
1575
+ continue;
1576
+ for (const entry of entries) {
1577
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
1578
+ mac = entry.mac;
1579
+ break;
1580
+ }
1581
+ }
1582
+ if (mac)
1583
+ break;
1584
+ }
1585
+ const material = `${host}:${mac || "no-mac"}`;
1586
+ const suffix = createHash2("sha256").update(material).digest("hex").slice(0, 8);
1587
+ return `${host}-${suffix}`;
1588
+ }
1589
+ function createDefaultConfig2() {
1590
+ return {
1591
+ candengo_url: "",
1592
+ candengo_api_key: "",
1593
+ site_id: "",
1594
+ namespace: "",
1595
+ user_id: "",
1596
+ user_email: "",
1597
+ device_id: generateDeviceId2(),
1598
+ teams: [],
1599
+ sync: {
1600
+ enabled: true,
1601
+ interval_seconds: 30,
1602
+ batch_size: 50
1603
+ },
1604
+ search: {
1605
+ default_limit: 10,
1606
+ local_boost: 1.2,
1607
+ scope: "all"
1608
+ },
1609
+ scrubbing: {
1610
+ enabled: true,
1611
+ custom_patterns: [],
1612
+ default_sensitivity: "shared"
1613
+ },
1614
+ sentinel: {
1615
+ enabled: false,
1616
+ mode: "advisory",
1617
+ provider: "openai",
1618
+ model: "gpt-4o-mini",
1619
+ api_key: "",
1620
+ base_url: "",
1621
+ skip_patterns: [],
1622
+ daily_limit: 100,
1623
+ tier: "free"
1624
+ },
1625
+ observer: {
1626
+ enabled: true,
1627
+ mode: "per_event",
1628
+ model: "sonnet"
1629
+ },
1630
+ transcript_analysis: {
1631
+ enabled: false
1632
+ }
1633
+ };
1634
+ }
1635
+ function loadConfig2() {
1636
+ if (!existsSync5(SETTINGS_PATH2)) {
1637
+ throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
1638
+ }
1639
+ const raw = readFileSync4(SETTINGS_PATH2, "utf-8");
1640
+ let parsed;
1641
+ try {
1642
+ parsed = JSON.parse(raw);
1643
+ } catch {
1644
+ throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
1645
+ }
1646
+ if (typeof parsed !== "object" || parsed === null) {
1647
+ throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
1648
+ }
1649
+ const config = parsed;
1650
+ const defaults = createDefaultConfig2();
1651
+ return {
1652
+ candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
1653
+ candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
1654
+ site_id: asString2(config["site_id"], defaults.site_id),
1655
+ namespace: asString2(config["namespace"], defaults.namespace),
1656
+ user_id: asString2(config["user_id"], defaults.user_id),
1657
+ user_email: asString2(config["user_email"], defaults.user_email),
1658
+ device_id: asString2(config["device_id"], defaults.device_id),
1659
+ teams: asTeams2(config["teams"], defaults.teams),
1660
+ sync: {
1661
+ enabled: asBool2(config["sync"]?.["enabled"], defaults.sync.enabled),
1662
+ interval_seconds: asNumber2(config["sync"]?.["interval_seconds"], defaults.sync.interval_seconds),
1663
+ batch_size: asNumber2(config["sync"]?.["batch_size"], defaults.sync.batch_size)
1664
+ },
1665
+ search: {
1666
+ default_limit: asNumber2(config["search"]?.["default_limit"], defaults.search.default_limit),
1667
+ local_boost: asNumber2(config["search"]?.["local_boost"], defaults.search.local_boost),
1668
+ scope: asScope2(config["search"]?.["scope"], defaults.search.scope)
1669
+ },
1670
+ scrubbing: {
1671
+ enabled: asBool2(config["scrubbing"]?.["enabled"], defaults.scrubbing.enabled),
1672
+ custom_patterns: asStringArray2(config["scrubbing"]?.["custom_patterns"], defaults.scrubbing.custom_patterns),
1673
+ default_sensitivity: asSensitivity2(config["scrubbing"]?.["default_sensitivity"], defaults.scrubbing.default_sensitivity)
1674
+ },
1675
+ sentinel: {
1676
+ enabled: asBool2(config["sentinel"]?.["enabled"], defaults.sentinel.enabled),
1677
+ mode: asSentinelMode2(config["sentinel"]?.["mode"], defaults.sentinel.mode),
1678
+ provider: asLlmProvider2(config["sentinel"]?.["provider"], defaults.sentinel.provider),
1679
+ model: asString2(config["sentinel"]?.["model"], defaults.sentinel.model),
1680
+ api_key: asString2(config["sentinel"]?.["api_key"], defaults.sentinel.api_key),
1681
+ base_url: asString2(config["sentinel"]?.["base_url"], defaults.sentinel.base_url),
1682
+ skip_patterns: asStringArray2(config["sentinel"]?.["skip_patterns"], defaults.sentinel.skip_patterns),
1683
+ daily_limit: asNumber2(config["sentinel"]?.["daily_limit"], defaults.sentinel.daily_limit),
1684
+ tier: asTier2(config["sentinel"]?.["tier"], defaults.sentinel.tier)
1685
+ },
1686
+ observer: {
1687
+ enabled: asBool2(config["observer"]?.["enabled"], defaults.observer.enabled),
1688
+ mode: asObserverMode2(config["observer"]?.["mode"], defaults.observer.mode),
1689
+ model: asString2(config["observer"]?.["model"], defaults.observer.model)
1690
+ },
1691
+ transcript_analysis: {
1692
+ enabled: asBool2(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
1693
+ }
1694
+ };
1695
+ }
1696
+ function saveConfig(config) {
1697
+ if (!existsSync5(CONFIG_DIR2)) {
1698
+ mkdirSync2(CONFIG_DIR2, { recursive: true });
1699
+ }
1700
+ writeFileSync2(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
1701
+ `, "utf-8");
1702
+ }
1703
+ function configExists2() {
1704
+ return existsSync5(SETTINGS_PATH2);
1705
+ }
1706
+ function asString2(value, fallback) {
1707
+ return typeof value === "string" ? value : fallback;
1708
+ }
1709
+ function asNumber2(value, fallback) {
1710
+ return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
1711
+ }
1712
+ function asBool2(value, fallback) {
1713
+ return typeof value === "boolean" ? value : fallback;
1714
+ }
1715
+ function asStringArray2(value, fallback) {
1716
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : fallback;
1717
+ }
1718
+ function asScope2(value, fallback) {
1719
+ if (value === "personal" || value === "team" || value === "all")
1720
+ return value;
1721
+ return fallback;
1722
+ }
1723
+ function asSensitivity2(value, fallback) {
1724
+ if (value === "shared" || value === "personal" || value === "secret")
1725
+ return value;
1726
+ return fallback;
1727
+ }
1728
+ function asSentinelMode2(value, fallback) {
1729
+ if (value === "advisory" || value === "blocking")
1730
+ return value;
1731
+ return fallback;
1732
+ }
1733
+ function asLlmProvider2(value, fallback) {
1734
+ if (value === "openai" || value === "anthropic" || value === "ollama" || value === "custom")
1735
+ return value;
1736
+ return fallback;
1737
+ }
1738
+ function asTier2(value, fallback) {
1739
+ if (value === "free" || value === "vibe" || value === "solo" || value === "pro" || value === "team" || value === "enterprise")
1740
+ return value;
1741
+ return fallback;
1742
+ }
1743
+ function asObserverMode2(value, fallback) {
1744
+ if (value === "per_event" || value === "per_session")
1745
+ return value;
1746
+ return fallback;
1747
+ }
1748
+ function asTeams2(value, fallback) {
1749
+ if (!Array.isArray(value))
1750
+ return fallback;
1751
+ return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
1752
+ }
1753
+
1469
1754
  // src/sync/auth.ts
1470
1755
  function getApiKey(config) {
1471
1756
  const envKey = process.env.ENGRM_TOKEN;
@@ -1651,6 +1936,44 @@ function extractNarrative(content) {
1651
1936
  `).trim();
1652
1937
  return narrative.length > 0 ? narrative : null;
1653
1938
  }
1939
+ async function pullSettings(client, config) {
1940
+ try {
1941
+ const settings = await client.fetchSettings();
1942
+ if (!settings)
1943
+ return false;
1944
+ let changed = false;
1945
+ if (settings.transcript_analysis !== undefined) {
1946
+ const ta = settings.transcript_analysis;
1947
+ if (typeof ta === "object" && ta !== null) {
1948
+ const taObj = ta;
1949
+ if (taObj.enabled !== undefined && taObj.enabled !== config.transcript_analysis.enabled) {
1950
+ config.transcript_analysis.enabled = !!taObj.enabled;
1951
+ changed = true;
1952
+ }
1953
+ }
1954
+ }
1955
+ if (settings.observer !== undefined) {
1956
+ const obs = settings.observer;
1957
+ if (typeof obs === "object" && obs !== null) {
1958
+ const obsObj = obs;
1959
+ if (obsObj.enabled !== undefined && obsObj.enabled !== config.observer.enabled) {
1960
+ config.observer.enabled = !!obsObj.enabled;
1961
+ changed = true;
1962
+ }
1963
+ if (obsObj.model !== undefined && typeof obsObj.model === "string" && obsObj.model !== config.observer.model) {
1964
+ config.observer.model = obsObj.model;
1965
+ changed = true;
1966
+ }
1967
+ }
1968
+ }
1969
+ if (changed) {
1970
+ saveConfig(config);
1971
+ }
1972
+ return changed;
1973
+ } catch {
1974
+ return false;
1975
+ }
1976
+ }
1654
1977
 
1655
1978
  // src/sync/client.ts
1656
1979
  class VectorClient {
@@ -1705,6 +2028,13 @@ class VectorClient {
1705
2028
  async sendTelemetry(beacon) {
1706
2029
  await this.request("POST", "/v1/mem/telemetry", beacon);
1707
2030
  }
2031
+ async fetchSettings() {
2032
+ try {
2033
+ return await this.request("GET", "/v1/mem/user-settings");
2034
+ } catch {
2035
+ return null;
2036
+ }
2037
+ }
1708
2038
  async health() {
1709
2039
  try {
1710
2040
  await this.request("GET", "/health");
@@ -1776,11 +2106,12 @@ async function main() {
1776
2106
  try {
1777
2107
  if (config.sync.enabled && config.candengo_api_key) {
1778
2108
  try {
1779
- const client = new VectorClient(config.candengo_url, config.candengo_api_key, config.site_id, config.namespace);
2109
+ const client = new VectorClient(config);
1780
2110
  const pullResult = await pullFromVector(db, client, config, 50);
1781
2111
  if (pullResult.merged > 0) {
1782
2112
  console.error(`Engrm: synced ${pullResult.merged} observation(s) from server`);
1783
2113
  }
2114
+ await pullSettings(client, config);
1784
2115
  } catch {}
1785
2116
  }
1786
2117
  const context = buildSessionContext(db, event.cwd, {