engrm 0.4.42 → 0.4.44

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/README.md CHANGED
@@ -110,6 +110,7 @@ Add this to `~/.engrm/settings.json`:
110
110
  "port": 3767,
111
111
  "bearer_tokens": ["replace-with-a-long-random-token"]
112
112
  },
113
+ "tool_profile": "memory",
113
114
  "fleet": {
114
115
  "project_name": "shared-experience",
115
116
  "namespace": "ns_fleet_shared",
@@ -154,6 +155,8 @@ Fleet writes:
154
155
  - sync to the dedicated fleet namespace/key instead of the normal org namespace
155
156
  - get an extra outbound scrub pass that redacts hostnames, IPs, and MACs before upload
156
157
 
158
+ For Hermes-style shared learning deployments, set `"tool_profile": "memory"` to expose a reduced Engrm tool set focused on durable memory, recall, and thread resumption instead of the full developer-oriented surface.
159
+
157
160
  ---
158
161
 
159
162
  ## How It Works
@@ -664,7 +667,7 @@ Engrm auto-registers in:
664
667
  - **Local storage:** SQLite via `better-sqlite3`, FTS5 full-text search, `sqlite-vec` for embeddings
665
668
  - **Embeddings:** all-MiniLM-L6-v2 via `@xenova/transformers` (384 dims, ~23MB)
666
669
  - **Remote backend:** Candengo Vector (BGE-M3, Qdrant, hybrid dense+sparse search)
667
- - **MCP:** `@modelcontextprotocol/sdk` (stdio transport)
670
+ - **MCP:** `@modelcontextprotocol/sdk` (stdio for local agents, Streamable HTTP + SSE compatibility for Hermes-style remote clients)
668
671
  - **AI extraction:** `@anthropic-ai/claude-agent-sdk` (optional, for richer observations)
669
672
 
670
673
  ---
package/dist/cli.js CHANGED
@@ -21,8 +21,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
21
21
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync } from "fs";
22
22
  import { hostname as hostname3, homedir as homedir5, networkInterfaces as networkInterfaces3 } from "os";
23
23
  import { dirname as dirname5, join as join8 } from "path";
24
- import { createHash as createHash4 } from "crypto";
25
- import { fileURLToPath as fileURLToPath4 } from "url";
24
+ import { createHash as createHash5 } from "crypto";
25
+ import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
26
26
 
27
27
  // src/config.ts
28
28
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -114,7 +114,8 @@ function createDefaultConfig() {
114
114
  project_name: "shared-experience",
115
115
  namespace: "",
116
116
  api_key: ""
117
- }
117
+ },
118
+ tool_profile: "full"
118
119
  };
119
120
  }
120
121
  function loadConfig() {
@@ -185,7 +186,8 @@ function loadConfig() {
185
186
  project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
186
187
  namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
187
188
  api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
188
- }
189
+ },
190
+ tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
189
191
  };
190
192
  }
191
193
  function saveConfig(config) {
@@ -240,6 +242,11 @@ function asObserverMode(value, fallback) {
240
242
  return value;
241
243
  return fallback;
242
244
  }
245
+ function asToolProfile(value, fallback) {
246
+ if (value === "full" || value === "memory")
247
+ return value;
248
+ return fallback;
249
+ }
243
250
  function asTeams(value, fallback) {
244
251
  if (!Array.isArray(value))
245
252
  return fallback;
@@ -873,6 +880,7 @@ function ensureObservationTypes(db) {
873
880
  DROP TABLE observations;
874
881
  ALTER TABLE observations_repair RENAME TO observations;
875
882
  CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
883
+ CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
876
884
  CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
877
885
  CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
878
886
  CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
@@ -1153,6 +1161,7 @@ class MemDatabase {
1153
1161
  this.db = openDatabase(dbPath);
1154
1162
  this.db.exec("PRAGMA journal_mode = WAL");
1155
1163
  this.db.exec("PRAGMA foreign_keys = ON");
1164
+ this.db.exec("PRAGMA busy_timeout = 5000");
1156
1165
  this.vecAvailable = this.loadVecExtension();
1157
1166
  runMigrations(this.db);
1158
1167
  ensureObservationTypes(this.db);
@@ -1174,8 +1183,16 @@ class MemDatabase {
1174
1183
  this.db.close();
1175
1184
  }
1176
1185
  upsertProject(project) {
1186
+ const canonicalId = project.canonical_id?.trim();
1187
+ const name = project.name?.trim();
1188
+ if (!canonicalId) {
1189
+ throw new Error("Project canonical_id is required");
1190
+ }
1191
+ if (!name) {
1192
+ throw new Error("Project name is required");
1193
+ }
1177
1194
  const now = Math.floor(Date.now() / 1000);
1178
- const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(project.canonical_id);
1195
+ const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
1179
1196
  if (existing) {
1180
1197
  this.db.query(`UPDATE projects SET
1181
1198
  local_path = COALESCE(?, local_path),
@@ -1190,7 +1207,7 @@ class MemDatabase {
1190
1207
  };
1191
1208
  }
1192
1209
  const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
1193
- VALUES (?, ?, ?, ?, ?, ?)`).run(project.canonical_id, project.name, project.local_path ?? null, project.remote_url ?? null, now, now);
1210
+ VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
1194
1211
  return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
1195
1212
  }
1196
1213
  getProjectByCanonicalId(canonicalId) {
@@ -1883,6 +1900,33 @@ function getOutboxStats(db) {
1883
1900
  }
1884
1901
  return stats;
1885
1902
  }
1903
+ function getOutboxFailureSummaries(db, limit = 5) {
1904
+ return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
1905
+ FROM sync_outbox
1906
+ WHERE status = 'failed'
1907
+ GROUP BY COALESCE(last_error, '')
1908
+ ORDER BY count DESC, error ASC
1909
+ LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
1910
+ }
1911
+ function classifyOutboxFailure(error) {
1912
+ const normalized = error.toLowerCase();
1913
+ if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
1914
+ return "auth";
1915
+ }
1916
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
1917
+ return "rate_limit";
1918
+ }
1919
+ if (normalized.includes("timeout") || normalized.includes("abort")) {
1920
+ return "timeout";
1921
+ }
1922
+ if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
1923
+ return "network";
1924
+ }
1925
+ if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
1926
+ return "validation";
1927
+ }
1928
+ return "other";
1929
+ }
1886
1930
 
1887
1931
  // src/intelligence/value-signals.ts
1888
1932
  var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
@@ -2468,7 +2512,7 @@ async function provision(baseUrl, request) {
2468
2512
  import { randomBytes } from "node:crypto";
2469
2513
  import { createServer } from "node:http";
2470
2514
  import { execFile } from "node:child_process";
2471
- var CALLBACK_TIMEOUT_MS = 120000;
2515
+ var CALLBACK_TIMEOUT_MS = 600000;
2472
2516
  async function runBrowserAuth(candengoUrl) {
2473
2517
  const state = randomBytes(16).toString("hex");
2474
2518
  const { port, waitForCallback, stop } = await startCallbackServer(state);
@@ -2803,13 +2847,16 @@ function registerCodexHooks() {
2803
2847
  return { path: CODEX_HOOKS, added: true };
2804
2848
  }
2805
2849
  function registerOpenCode() {
2850
+ const runtime = findRuntime();
2806
2851
  const root = findPackageRoot();
2852
+ const dist = isBuiltDist();
2807
2853
  const pluginSource = join2(root, "opencode", "plugin", "engrm-opencode.js");
2854
+ const serverPath = dist ? join2(root, "dist", "server.js") : join2(root, "src", "server.ts");
2808
2855
  const config = readJsonFile(OPENCODE_CONFIG);
2809
2856
  const mcp = config["mcp"] ?? {};
2810
2857
  mcp["engrm"] = {
2811
2858
  type: "local",
2812
- command: ["engrm", "serve"],
2859
+ command: dist ? [runtime, serverPath] : [runtime, "run", serverPath],
2813
2860
  enabled: true,
2814
2861
  timeout: 5000
2815
2862
  };
@@ -3319,10 +3366,11 @@ function readProjectConfigFile(directory) {
3319
3366
  }
3320
3367
  }
3321
3368
  function detectProject(directory) {
3322
- const remoteUrl = getGitRemoteUrl(directory);
3369
+ const resolvedDirectory = resolve(directory);
3370
+ const remoteUrl = getGitRemoteUrl(resolvedDirectory);
3323
3371
  if (remoteUrl) {
3324
3372
  const canonicalId = normaliseGitRemoteUrl(remoteUrl);
3325
- const repoRoot = getGitTopLevel(directory) ?? directory;
3373
+ const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
3326
3374
  return {
3327
3375
  canonical_id: canonicalId,
3328
3376
  name: projectNameFromCanonicalId(canonicalId),
@@ -3330,21 +3378,22 @@ function detectProject(directory) {
3330
3378
  local_path: repoRoot
3331
3379
  };
3332
3380
  }
3333
- const configFile = readProjectConfigFile(directory);
3381
+ const configFile = readProjectConfigFile(resolvedDirectory);
3334
3382
  if (configFile) {
3335
3383
  return {
3336
3384
  canonical_id: configFile.project_id,
3337
3385
  name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
3338
3386
  remote_url: null,
3339
- local_path: directory
3387
+ local_path: resolvedDirectory
3340
3388
  };
3341
3389
  }
3342
- const dirName = basename(directory);
3390
+ const dirName = basename(resolvedDirectory);
3391
+ const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
3343
3392
  return {
3344
- canonical_id: `local/${dirName}`,
3345
- name: dirName,
3393
+ canonical_id: `local/${safeDirName}`,
3394
+ name: safeDirName,
3346
3395
  remote_url: null,
3347
- local_path: directory
3396
+ local_path: resolvedDirectory
3348
3397
  };
3349
3398
  }
3350
3399
  function detectProjectForPath(filePath, fallbackCwd) {
@@ -4006,7 +4055,8 @@ function createDefaultConfig2() {
4006
4055
  project_name: "shared-experience",
4007
4056
  namespace: "",
4008
4057
  api_key: ""
4009
- }
4058
+ },
4059
+ tool_profile: "full"
4010
4060
  };
4011
4061
  }
4012
4062
  function loadConfig2() {
@@ -4077,7 +4127,8 @@ function loadConfig2() {
4077
4127
  project_name: asString2(config["fleet"]?.["project_name"], defaults.fleet.project_name),
4078
4128
  namespace: asString2(config["fleet"]?.["namespace"], defaults.fleet.namespace),
4079
4129
  api_key: asString2(config["fleet"]?.["api_key"], defaults.fleet.api_key)
4080
- }
4130
+ },
4131
+ tool_profile: asToolProfile2(config["tool_profile"], defaults.tool_profile)
4081
4132
  };
4082
4133
  }
4083
4134
  function saveConfig2(config) {
@@ -4132,12 +4183,35 @@ function asObserverMode2(value, fallback) {
4132
4183
  return value;
4133
4184
  return fallback;
4134
4185
  }
4186
+ function asToolProfile2(value, fallback) {
4187
+ if (value === "full" || value === "memory")
4188
+ return value;
4189
+ return fallback;
4190
+ }
4135
4191
  function asTeams2(value, fallback) {
4136
4192
  if (!Array.isArray(value))
4137
4193
  return fallback;
4138
4194
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
4139
4195
  }
4140
4196
 
4197
+ // src/tool-profiles.ts
4198
+ var MEMORY_PROFILE_TOOLS = [
4199
+ "save_observation",
4200
+ "search_recall",
4201
+ "resume_thread",
4202
+ "list_recall_items",
4203
+ "load_recall_item",
4204
+ "recent_chat",
4205
+ "search_chat",
4206
+ "refresh_chat_recall",
4207
+ "repair_recall"
4208
+ ];
4209
+ function getEnabledToolNames(profile) {
4210
+ if (!profile || profile === "full")
4211
+ return null;
4212
+ return new Set(MEMORY_PROFILE_TOOLS);
4213
+ }
4214
+
4141
4215
  // src/tools/capture-status.ts
4142
4216
  var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
4143
4217
  function getCaptureStatus(db, input = {}) {
@@ -4242,6 +4316,8 @@ function getCaptureStatus(db, input = {}) {
4242
4316
  http_enabled: Boolean(config?.http?.enabled || process.env.ENGRM_HTTP_PORT),
4243
4317
  http_port: config?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
4244
4318
  http_bearer_token_count: config?.http?.bearer_tokens?.length ?? 0,
4319
+ tool_profile: config?.tool_profile ?? "full",
4320
+ enabled_tool_count: config ? getEnabledToolNames(config.tool_profile)?.size ?? null : null,
4245
4321
  fleet_project_name: config?.fleet?.project_name ?? null,
4246
4322
  fleet_configured: Boolean(config?.fleet?.namespace && config?.fleet?.api_key),
4247
4323
  claude_mcp_registered: claudeMcpRegistered,
@@ -4289,6 +4365,117 @@ function hasOpenClawMcpRegistration(content) {
4289
4365
  }
4290
4366
  }
4291
4367
 
4368
+ // src/sync/auth.ts
4369
+ import { createHash as createHash4 } from "node:crypto";
4370
+
4371
+ // src/storage/outbox.ts
4372
+ function getPendingEntries(db, limit = 50) {
4373
+ const now = Math.floor(Date.now() / 1000);
4374
+ return db.db.query(`SELECT * FROM sync_outbox
4375
+ WHERE (status = 'pending')
4376
+ OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
4377
+ ORDER BY created_at_epoch ASC
4378
+ LIMIT ?`).all(now, limit);
4379
+ }
4380
+ function markSyncing(db, entryId) {
4381
+ const now = Math.floor(Date.now() / 1000);
4382
+ db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
4383
+ }
4384
+ function markSynced(db, entryId) {
4385
+ const now = Math.floor(Date.now() / 1000);
4386
+ db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
4387
+ }
4388
+ function markFailed(db, entryId, error) {
4389
+ const now = Math.floor(Date.now() / 1000);
4390
+ db.db.query(`UPDATE sync_outbox SET
4391
+ status = 'failed',
4392
+ retry_count = retry_count + 1,
4393
+ last_error = ?,
4394
+ next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
4395
+ WHERE id = ?`).run(error, now, entryId);
4396
+ }
4397
+ function getOutboxStats2(db) {
4398
+ const rows = db.db.query("SELECT status, COUNT(*) as count FROM sync_outbox GROUP BY status").all();
4399
+ const stats = {
4400
+ pending: 0,
4401
+ syncing: 0,
4402
+ synced: 0,
4403
+ failed: 0
4404
+ };
4405
+ for (const row of rows) {
4406
+ stats[row.status] = row.count;
4407
+ }
4408
+ return stats;
4409
+ }
4410
+ function getOutboxFailureSummaries2(db, limit = 5) {
4411
+ return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
4412
+ FROM sync_outbox
4413
+ WHERE status = 'failed'
4414
+ GROUP BY COALESCE(last_error, '')
4415
+ ORDER BY count DESC, error ASC
4416
+ LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
4417
+ }
4418
+ function classifyOutboxFailure2(error) {
4419
+ const normalized = error.toLowerCase();
4420
+ if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
4421
+ return "auth";
4422
+ }
4423
+ if (normalized.includes("429") || normalized.includes("rate limit")) {
4424
+ return "rate_limit";
4425
+ }
4426
+ if (normalized.includes("timeout") || normalized.includes("abort")) {
4427
+ return "timeout";
4428
+ }
4429
+ if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
4430
+ return "network";
4431
+ }
4432
+ if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
4433
+ return "validation";
4434
+ }
4435
+ return "other";
4436
+ }
4437
+ function resetFailedEntries(db) {
4438
+ const result = db.db.query(`UPDATE sync_outbox
4439
+ SET status = 'pending',
4440
+ retry_count = 0,
4441
+ last_error = NULL,
4442
+ next_retry_epoch = NULL
4443
+ WHERE status = 'failed'`).run();
4444
+ return result.changes;
4445
+ }
4446
+ function resetFailedEntriesMatching(db, predicate) {
4447
+ const rows = db.db.query(`SELECT id, last_error
4448
+ FROM sync_outbox
4449
+ WHERE status = 'failed'`).all();
4450
+ const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
4451
+ if (matchingIds.length === 0)
4452
+ return 0;
4453
+ const placeholders = matchingIds.map(() => "?").join(", ");
4454
+ const result = db.db.query(`UPDATE sync_outbox
4455
+ SET status = 'pending',
4456
+ retry_count = 0,
4457
+ last_error = NULL,
4458
+ next_retry_epoch = NULL
4459
+ WHERE id IN (${placeholders})`).run(...matchingIds);
4460
+ return result.changes;
4461
+ }
4462
+ function resetSyncingEntries(db) {
4463
+ const result = db.db.query(`UPDATE sync_outbox
4464
+ SET status = 'pending',
4465
+ next_retry_epoch = NULL
4466
+ WHERE status = 'syncing'`).run();
4467
+ return result.changes;
4468
+ }
4469
+ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
4470
+ const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
4471
+ const result = db.db.query(`UPDATE sync_outbox
4472
+ SET status = 'pending',
4473
+ next_retry_epoch = NULL
4474
+ WHERE status = 'syncing'
4475
+ AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
4476
+ return result.changes;
4477
+ }
4478
+
4292
4479
  // src/sync/auth.ts
4293
4480
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
4294
4481
  function normalizeBaseUrl(url) {
@@ -4305,6 +4492,46 @@ function normalizeBaseUrl(url) {
4305
4492
  return trimmed.replace(/\/$/, "");
4306
4493
  }
4307
4494
  }
4495
+ function getApiKey(config) {
4496
+ const envKey = process.env.ENGRM_TOKEN;
4497
+ if (envKey && envKey.startsWith("cvk_"))
4498
+ return envKey;
4499
+ if (config.candengo_api_key && config.candengo_api_key.length > 0) {
4500
+ return config.candengo_api_key;
4501
+ }
4502
+ return null;
4503
+ }
4504
+ function getBaseUrl(config) {
4505
+ if (config.candengo_url && config.candengo_url.length > 0) {
4506
+ return normalizeBaseUrl(config.candengo_url);
4507
+ }
4508
+ return null;
4509
+ }
4510
+ function getAuthFingerprint(config) {
4511
+ const apiKey = getApiKey(config);
4512
+ const baseUrl = getBaseUrl(config);
4513
+ if (!apiKey || !baseUrl)
4514
+ return null;
4515
+ return createHash4("sha256").update(`${baseUrl}
4516
+ ${apiKey}
4517
+ ${config.namespace}
4518
+ ${config.site_id}`).digest("hex");
4519
+ }
4520
+ function recoverOutboxAfterSuccessfulAuth(db, config) {
4521
+ const fingerprint = getAuthFingerprint(config);
4522
+ const staleSyncingReset = resetStaleSyncingEntries(db);
4523
+ const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure2(error) === "auth");
4524
+ if (fingerprint) {
4525
+ db.setSyncState("sync_auth_fingerprint", fingerprint);
4526
+ }
4527
+ return {
4528
+ fingerprintChanged: false,
4529
+ failedReset: 0,
4530
+ authFailedReset,
4531
+ syncingReset: 0,
4532
+ staleSyncingReset
4533
+ };
4534
+ }
4308
4535
 
4309
4536
  // src/cli.ts
4310
4537
  var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
@@ -4316,6 +4543,9 @@ switch (command) {
4316
4543
  case "init":
4317
4544
  await handleInit(args.slice(1));
4318
4545
  break;
4546
+ case "serve":
4547
+ await handleServe();
4548
+ break;
4319
4549
  case "status":
4320
4550
  handleStatus();
4321
4551
  break;
@@ -4381,6 +4611,12 @@ async function handleInit(flags) {
4381
4611
  await initWithBrowser(url);
4382
4612
  await maybeInstallPack(flags);
4383
4613
  }
4614
+ async function handleServe() {
4615
+ const packageRoot = join8(THIS_DIR, "..");
4616
+ const serverPath = IS_BUILT_DIST ? join8(packageRoot, "dist", "server.js") : join8(packageRoot, "src", "server.ts");
4617
+ await import(pathToFileURL(serverPath).href);
4618
+ await new Promise(() => {});
4619
+ }
4384
4620
  async function maybeInstallPack(flags) {
4385
4621
  const packFlag = flags.find((f) => f.startsWith("--pack"));
4386
4622
  if (!packFlag)
@@ -4476,11 +4712,27 @@ function writeConfigFromProvision(baseUrl, result) {
4476
4712
  ensureConfigDir();
4477
4713
  let existingDeviceId;
4478
4714
  let existingSentinel;
4715
+ let existingObserver;
4716
+ let existingTranscriptAnalysis;
4717
+ let existingHttp;
4718
+ let existingFleet;
4719
+ let existingToolProfile;
4720
+ let existingSync;
4721
+ let existingSearch;
4722
+ let existingScrubbing;
4479
4723
  if (configExists()) {
4480
4724
  try {
4481
4725
  const existing = loadConfig();
4482
4726
  existingDeviceId = existing.device_id;
4483
4727
  existingSentinel = existing.sentinel;
4728
+ existingObserver = existing.observer;
4729
+ existingTranscriptAnalysis = existing.transcript_analysis;
4730
+ existingHttp = existing.http;
4731
+ existingFleet = existing.fleet;
4732
+ existingToolProfile = existing.tool_profile;
4733
+ existingSync = existing.sync;
4734
+ existingSearch = existing.search;
4735
+ existingScrubbing = existing.scrubbing;
4484
4736
  } catch {}
4485
4737
  }
4486
4738
  const config = {
@@ -4492,17 +4744,17 @@ function writeConfigFromProvision(baseUrl, result) {
4492
4744
  user_email: result.user_email,
4493
4745
  device_id: existingDeviceId || generateDeviceId3(),
4494
4746
  teams: result.teams ?? [],
4495
- sync: {
4747
+ sync: existingSync ?? {
4496
4748
  enabled: true,
4497
4749
  interval_seconds: 30,
4498
4750
  batch_size: 50
4499
4751
  },
4500
- search: {
4752
+ search: existingSearch ?? {
4501
4753
  default_limit: 10,
4502
4754
  local_boost: 1.2,
4503
4755
  scope: "all"
4504
4756
  },
4505
- scrubbing: {
4757
+ scrubbing: existingScrubbing ?? {
4506
4758
  enabled: true,
4507
4759
  custom_patterns: [],
4508
4760
  default_sensitivity: "shared"
@@ -4518,17 +4770,29 @@ function writeConfigFromProvision(baseUrl, result) {
4518
4770
  daily_limit: 100,
4519
4771
  tier: "free"
4520
4772
  },
4521
- observer: {
4773
+ observer: existingObserver ?? {
4522
4774
  enabled: true,
4523
4775
  mode: "per_event",
4524
4776
  model: "haiku"
4525
4777
  },
4526
- transcript_analysis: {
4778
+ transcript_analysis: existingTranscriptAnalysis ?? {
4527
4779
  enabled: false
4528
- }
4780
+ },
4781
+ http: existingHttp ?? {
4782
+ enabled: false,
4783
+ port: 3767,
4784
+ bearer_tokens: []
4785
+ },
4786
+ fleet: existingFleet ?? {
4787
+ project_name: "shared-experience",
4788
+ namespace: "",
4789
+ api_key: ""
4790
+ },
4791
+ tool_profile: existingToolProfile ?? "full"
4529
4792
  };
4530
4793
  saveConfig(config);
4531
4794
  const db = new MemDatabase(getDbPath());
4795
+ recoverOutboxAfterSuccessfulAuth(db, config);
4532
4796
  db.close();
4533
4797
  console.log(`Configuration saved to ${getSettingsPath()}`);
4534
4798
  console.log(`Database initialised at ${getDbPath()}`);
@@ -4724,6 +4988,7 @@ function handleStatus() {
4724
4988
  console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
4725
4989
  console.log(` HTTP MCP: ${config.http.enabled ? `enabled (:${config.http.port})` : "disabled"}`);
4726
4990
  console.log(` HTTP tokens: ${config.http.bearer_tokens.length}`);
4991
+ console.log(` Tool profile: ${config.tool_profile ?? "full"}`);
4727
4992
  console.log(` Fleet project: ${config.fleet.project_name || "(not set)"}`);
4728
4993
  console.log(` Fleet sync: ${config.fleet.namespace && config.fleet.api_key ? "configured" : "not configured"}`);
4729
4994
  const claudeJson = join8(homedir5(), ".claude.json");
@@ -4843,7 +5108,7 @@ function handleStatus() {
4843
5108
  }
4844
5109
  console.log(` Value: ${signalParts.join(", ")}`);
4845
5110
  if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
4846
- console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
5111
+ console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
4847
5112
  }
4848
5113
  } catch {}
4849
5114
  try {
@@ -4864,6 +5129,11 @@ function handleStatus() {
4864
5129
  console.log(`
4865
5130
  Sync`);
4866
5131
  console.log(` Outbox: ${outbox["pending"] ?? 0} pending, ${outbox["failed"] ?? 0} failed, ${outbox["synced"] ?? 0} synced`);
5132
+ const topFailures = getOutboxFailureSummaries(db, 2);
5133
+ if (topFailures.length > 0) {
5134
+ const failureSummary = topFailures.map((row) => `${classifyOutboxFailure(row.error)} ${row.count}`).join(", ");
5135
+ console.log(` Failures: ${failureSummary}`);
5136
+ }
4867
5137
  try {
4868
5138
  const lastPush = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_push_epoch");
4869
5139
  const lastPull = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_pull_epoch");
@@ -4944,7 +5214,7 @@ function generateDeviceId3() {
4944
5214
  break;
4945
5215
  }
4946
5216
  const material = `${host}:${mac || "no-mac"}`;
4947
- const suffix = createHash4("sha256").update(material).digest("hex").slice(0, 8);
5217
+ const suffix = createHash5("sha256").update(material).digest("hex").slice(0, 8);
4948
5218
  return `${host}-${suffix}`;
4949
5219
  }
4950
5220
  async function handleInstallPack(flags) {
@@ -5083,6 +5353,7 @@ async function handleDoctor() {
5083
5353
  } else {
5084
5354
  info("HTTP MCP disabled");
5085
5355
  }
5356
+ info(`Tool profile: ${config.tool_profile ?? "full"}`);
5086
5357
  if (config.fleet.project_name) {
5087
5358
  if (config.fleet.namespace && config.fleet.api_key) {
5088
5359
  pass(`Fleet project '${config.fleet.project_name}' is configured`);
@@ -5357,7 +5628,7 @@ async function handleDoctor() {
5357
5628
  const dbPath = getDbPath();
5358
5629
  if (existsSync8(dbPath)) {
5359
5630
  const stats = statSync(dbPath);
5360
- const sizeMB = stats.size / (1024 * 1024);
5631
+ const sizeMB = stats.size / 1048576;
5361
5632
  const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
5362
5633
  info(`Database size: ${sizeStr}`);
5363
5634
  }
@@ -5493,7 +5764,7 @@ And add to ~/.config/opencode/opencode.json:`);
5493
5764
  "mcp": {
5494
5765
  "engrm": {
5495
5766
  "type": "local",
5496
- "command": ["engrm", "serve"],
5767
+ "command": ${JSON.stringify(IS_BUILT_DIST ? [process.execPath, join8(packageRoot, "dist", "server.js")] : ["bun", "run", join8(packageRoot, "src", "server.ts")])},
5497
5768
  "enabled": true,
5498
5769
  "timeout": 5000
5499
5770
  }
@@ -5522,6 +5793,7 @@ function printUsage() {
5522
5793
  console.log(`Engrm \u2014 Memory layer for AI coding agents
5523
5794
  `);
5524
5795
  console.log("Usage:");
5796
+ console.log(" engrm serve Run the MCP server over stdio");
5525
5797
  console.log(" engrm init Setup via browser (recommended)");
5526
5798
  console.log(" engrm init --token=cmt_xxx Setup from provisioning token");
5527
5799
  console.log(" engrm init --pack=<name> Setup + install a starter pack");