engrm 0.4.45 → 0.4.47

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.
@@ -153,11 +153,20 @@ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, wr
153
153
  import { homedir, hostname, networkInterfaces } from "node:os";
154
154
  import { join as join2 } from "node:path";
155
155
  import { createHash } from "node:crypto";
156
- var CONFIG_DIR = join2(homedir(), ".engrm");
157
- var SETTINGS_PATH = join2(CONFIG_DIR, "settings.json");
158
- var DB_PATH = join2(CONFIG_DIR, "engrm.db");
156
+ function resolveConfigDir() {
157
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join2(homedir(), ".engrm");
158
+ }
159
+ function resolveSettingsPath() {
160
+ return join2(resolveConfigDir(), "settings.json");
161
+ }
162
+ function resolveDbPath() {
163
+ return join2(resolveConfigDir(), "engrm.db");
164
+ }
165
+ function resolveAuthBackupPath() {
166
+ return join2(resolveConfigDir(), "auth-backup.json");
167
+ }
159
168
  function getDbPath() {
160
- return DB_PATH;
169
+ return resolveDbPath();
161
170
  }
162
171
  function generateDeviceId() {
163
172
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -180,7 +189,7 @@ function generateDeviceId() {
180
189
  return `${host}-${suffix}`;
181
190
  }
182
191
  function createDefaultConfig() {
183
- return {
192
+ const merged = {
184
193
  candengo_url: "",
185
194
  candengo_api_key: "",
186
195
  site_id: "",
@@ -235,24 +244,26 @@ function createDefaultConfig() {
235
244
  },
236
245
  tool_profile: "full"
237
246
  };
247
+ return merged;
238
248
  }
239
249
  function loadConfig() {
240
- if (!existsSync2(SETTINGS_PATH)) {
241
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
250
+ const settingsPath = resolveSettingsPath();
251
+ if (!existsSync2(settingsPath)) {
252
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
242
253
  }
243
- const raw = readFileSync2(SETTINGS_PATH, "utf-8");
254
+ const raw = readFileSync2(settingsPath, "utf-8");
244
255
  let parsed;
245
256
  try {
246
257
  parsed = JSON.parse(raw);
247
258
  } catch {
248
- throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
259
+ throw new Error(`Invalid JSON in ${settingsPath}`);
249
260
  }
250
261
  if (typeof parsed !== "object" || parsed === null) {
251
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
262
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
252
263
  }
253
264
  const config = parsed;
254
265
  const defaults = createDefaultConfig();
255
- return {
266
+ const merged = {
256
267
  candengo_url: asString(config["candengo_url"], defaults.candengo_url),
257
268
  candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
258
269
  site_id: asString(config["site_id"], defaults.site_id),
@@ -307,16 +318,27 @@ function loadConfig() {
307
318
  },
308
319
  tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
309
320
  };
321
+ if (looksLikePlaceholderAuth(merged)) {
322
+ return restoreAuthBackup(merged) ?? merged;
323
+ }
324
+ return merged;
310
325
  }
311
326
  function saveConfig(config) {
312
- if (!existsSync2(CONFIG_DIR)) {
313
- mkdirSync(CONFIG_DIR, { recursive: true });
327
+ const configDir = resolveConfigDir();
328
+ const settingsPath = resolveSettingsPath();
329
+ const authBackupPath = resolveAuthBackupPath();
330
+ if (!existsSync2(configDir)) {
331
+ mkdirSync(configDir, { recursive: true });
314
332
  }
315
- writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
333
+ writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
334
+ `, "utf-8");
335
+ if (!looksLikePlaceholderAuth(config)) {
336
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
316
337
  `, "utf-8");
338
+ }
317
339
  }
318
340
  function configExists() {
319
- return existsSync2(SETTINGS_PATH);
341
+ return existsSync2(resolveSettingsPath());
320
342
  }
321
343
  function asString(value, fallback) {
322
344
  return typeof value === "string" ? value : fallback;
@@ -370,6 +392,50 @@ function asTeams(value, fallback) {
370
392
  return fallback;
371
393
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
372
394
  }
395
+ function looksLikePlaceholderAuth(config) {
396
+ const apiKey = config.candengo_api_key.trim();
397
+ const siteId = config.site_id.trim();
398
+ const namespace = config.namespace.trim();
399
+ const email = config.user_email.trim().toLowerCase();
400
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
401
+ return true;
402
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
403
+ return true;
404
+ return false;
405
+ }
406
+ function extractAuthBackup(config) {
407
+ return {
408
+ candengo_url: config.candengo_url,
409
+ candengo_api_key: config.candengo_api_key,
410
+ site_id: config.site_id,
411
+ namespace: config.namespace,
412
+ user_id: config.user_id,
413
+ user_email: config.user_email,
414
+ teams: config.teams
415
+ };
416
+ }
417
+ function restoreAuthBackup(config) {
418
+ const authBackupPath = resolveAuthBackupPath();
419
+ if (!existsSync2(authBackupPath))
420
+ return null;
421
+ try {
422
+ const raw = readFileSync2(authBackupPath, "utf-8");
423
+ const parsed = JSON.parse(raw);
424
+ const restored = {
425
+ ...config,
426
+ candengo_url: asString(parsed["candengo_url"], config.candengo_url),
427
+ candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
428
+ site_id: asString(parsed["site_id"], config.site_id),
429
+ namespace: asString(parsed["namespace"], config.namespace),
430
+ user_id: asString(parsed["user_id"], config.user_id),
431
+ user_email: asString(parsed["user_email"], config.user_email),
432
+ teams: asTeams(parsed["teams"], config.teams)
433
+ };
434
+ return looksLikePlaceholderAuth(restored) ? null : restored;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
373
439
 
374
440
  // src/storage/migrations.ts
375
441
  var MIGRATIONS = [
@@ -1057,6 +1123,20 @@ function ensureChatMessageColumns(db) {
1057
1123
  db.exec("PRAGMA user_version = 17");
1058
1124
  }
1059
1125
  }
1126
+ function ensureObservationVectorTable(db) {
1127
+ if (!isVecExtensionLoaded(db))
1128
+ return;
1129
+ db.exec(`
1130
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1131
+ observation_id INTEGER PRIMARY KEY,
1132
+ embedding FLOAT[384]
1133
+ );
1134
+ `);
1135
+ const current = getSchemaVersion(db);
1136
+ if (current < 4) {
1137
+ db.exec("PRAGMA user_version = 4");
1138
+ }
1139
+ }
1060
1140
  function ensureChatVectorTable(db) {
1061
1141
  if (!isVecExtensionLoaded(db))
1062
1142
  return;
@@ -1285,6 +1365,7 @@ class MemDatabase {
1285
1365
  ensureObservationTypes(this.db);
1286
1366
  ensureSessionSummaryColumns(this.db);
1287
1367
  ensureChatMessageColumns(this.db);
1368
+ ensureObservationVectorTable(this.db);
1288
1369
  ensureChatVectorTable(this.db);
1289
1370
  ensureSyncOutboxSupportsChatMessages(this.db);
1290
1371
  }
package/dist/server.js CHANGED
@@ -13564,11 +13564,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13564
13564
  import { homedir, hostname as hostname3, networkInterfaces } from "node:os";
13565
13565
  import { join } from "node:path";
13566
13566
  import { createHash } from "node:crypto";
13567
- var CONFIG_DIR = join(homedir(), ".engrm");
13568
- var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
13569
- var DB_PATH = join(CONFIG_DIR, "engrm.db");
13567
+ function resolveConfigDir() {
13568
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
13569
+ }
13570
+ function resolveSettingsPath() {
13571
+ return join(resolveConfigDir(), "settings.json");
13572
+ }
13573
+ function resolveDbPath() {
13574
+ return join(resolveConfigDir(), "engrm.db");
13575
+ }
13576
+ function resolveAuthBackupPath() {
13577
+ return join(resolveConfigDir(), "auth-backup.json");
13578
+ }
13570
13579
  function getDbPath() {
13571
- return DB_PATH;
13580
+ return resolveDbPath();
13572
13581
  }
13573
13582
  function generateDeviceId() {
13574
13583
  const host = hostname3().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -13591,7 +13600,7 @@ function generateDeviceId() {
13591
13600
  return `${host}-${suffix}`;
13592
13601
  }
13593
13602
  function createDefaultConfig() {
13594
- return {
13603
+ const merged = {
13595
13604
  candengo_url: "",
13596
13605
  candengo_api_key: "",
13597
13606
  site_id: "",
@@ -13646,24 +13655,26 @@ function createDefaultConfig() {
13646
13655
  },
13647
13656
  tool_profile: "full"
13648
13657
  };
13658
+ return merged;
13649
13659
  }
13650
13660
  function loadConfig() {
13651
- if (!existsSync(SETTINGS_PATH)) {
13652
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
13661
+ const settingsPath = resolveSettingsPath();
13662
+ if (!existsSync(settingsPath)) {
13663
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
13653
13664
  }
13654
- const raw = readFileSync(SETTINGS_PATH, "utf-8");
13665
+ const raw = readFileSync(settingsPath, "utf-8");
13655
13666
  let parsed;
13656
13667
  try {
13657
13668
  parsed = JSON.parse(raw);
13658
13669
  } catch {
13659
- throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
13670
+ throw new Error(`Invalid JSON in ${settingsPath}`);
13660
13671
  }
13661
13672
  if (typeof parsed !== "object" || parsed === null) {
13662
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
13673
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
13663
13674
  }
13664
13675
  const config2 = parsed;
13665
13676
  const defaults = createDefaultConfig();
13666
- return {
13677
+ const merged = {
13667
13678
  candengo_url: asString(config2["candengo_url"], defaults.candengo_url),
13668
13679
  candengo_api_key: asString(config2["candengo_api_key"], defaults.candengo_api_key),
13669
13680
  site_id: asString(config2["site_id"], defaults.site_id),
@@ -13718,16 +13729,27 @@ function loadConfig() {
13718
13729
  },
13719
13730
  tool_profile: asToolProfile(config2["tool_profile"], defaults.tool_profile)
13720
13731
  };
13732
+ if (looksLikePlaceholderAuth(merged)) {
13733
+ return restoreAuthBackup(merged) ?? merged;
13734
+ }
13735
+ return merged;
13721
13736
  }
13722
13737
  function saveConfig(config2) {
13723
- if (!existsSync(CONFIG_DIR)) {
13724
- mkdirSync(CONFIG_DIR, { recursive: true });
13738
+ const configDir = resolveConfigDir();
13739
+ const settingsPath = resolveSettingsPath();
13740
+ const authBackupPath = resolveAuthBackupPath();
13741
+ if (!existsSync(configDir)) {
13742
+ mkdirSync(configDir, { recursive: true });
13725
13743
  }
13726
- writeFileSync(SETTINGS_PATH, JSON.stringify(config2, null, 2) + `
13744
+ writeFileSync(settingsPath, JSON.stringify(config2, null, 2) + `
13727
13745
  `, "utf-8");
13746
+ if (!looksLikePlaceholderAuth(config2)) {
13747
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config2), null, 2) + `
13748
+ `, "utf-8");
13749
+ }
13728
13750
  }
13729
13751
  function configExists() {
13730
- return existsSync(SETTINGS_PATH);
13752
+ return existsSync(resolveSettingsPath());
13731
13753
  }
13732
13754
  function asString(value, fallback) {
13733
13755
  return typeof value === "string" ? value : fallback;
@@ -13781,6 +13803,50 @@ function asTeams(value, fallback) {
13781
13803
  return fallback;
13782
13804
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
13783
13805
  }
13806
+ function looksLikePlaceholderAuth(config2) {
13807
+ const apiKey = config2.candengo_api_key.trim();
13808
+ const siteId = config2.site_id.trim();
13809
+ const namespace = config2.namespace.trim();
13810
+ const email3 = config2.user_email.trim().toLowerCase();
13811
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
13812
+ return true;
13813
+ if (siteId === "site-1" && namespace === "org-ns" && email3.endsWith("@example.com"))
13814
+ return true;
13815
+ return false;
13816
+ }
13817
+ function extractAuthBackup(config2) {
13818
+ return {
13819
+ candengo_url: config2.candengo_url,
13820
+ candengo_api_key: config2.candengo_api_key,
13821
+ site_id: config2.site_id,
13822
+ namespace: config2.namespace,
13823
+ user_id: config2.user_id,
13824
+ user_email: config2.user_email,
13825
+ teams: config2.teams
13826
+ };
13827
+ }
13828
+ function restoreAuthBackup(config2) {
13829
+ const authBackupPath = resolveAuthBackupPath();
13830
+ if (!existsSync(authBackupPath))
13831
+ return null;
13832
+ try {
13833
+ const raw = readFileSync(authBackupPath, "utf-8");
13834
+ const parsed = JSON.parse(raw);
13835
+ const restored = {
13836
+ ...config2,
13837
+ candengo_url: asString(parsed["candengo_url"], config2.candengo_url),
13838
+ candengo_api_key: asString(parsed["candengo_api_key"], config2.candengo_api_key),
13839
+ site_id: asString(parsed["site_id"], config2.site_id),
13840
+ namespace: asString(parsed["namespace"], config2.namespace),
13841
+ user_id: asString(parsed["user_id"], config2.user_id),
13842
+ user_email: asString(parsed["user_email"], config2.user_email),
13843
+ teams: asTeams(parsed["teams"], config2.teams)
13844
+ };
13845
+ return looksLikePlaceholderAuth(restored) ? null : restored;
13846
+ } catch {
13847
+ return null;
13848
+ }
13849
+ }
13784
13850
 
13785
13851
  // src/storage/migrations.ts
13786
13852
  var MIGRATIONS = [
@@ -14468,6 +14534,20 @@ function ensureChatMessageColumns(db) {
14468
14534
  db.exec("PRAGMA user_version = 17");
14469
14535
  }
14470
14536
  }
14537
+ function ensureObservationVectorTable(db) {
14538
+ if (!isVecExtensionLoaded(db))
14539
+ return;
14540
+ db.exec(`
14541
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
14542
+ observation_id INTEGER PRIMARY KEY,
14543
+ embedding FLOAT[384]
14544
+ );
14545
+ `);
14546
+ const current = getSchemaVersion(db);
14547
+ if (current < 4) {
14548
+ db.exec("PRAGMA user_version = 4");
14549
+ }
14550
+ }
14471
14551
  function ensureChatVectorTable(db) {
14472
14552
  if (!isVecExtensionLoaded(db))
14473
14553
  return;
@@ -14696,6 +14776,7 @@ class MemDatabase {
14696
14776
  ensureObservationTypes(this.db);
14697
14777
  ensureSessionSummaryColumns(this.db);
14698
14778
  ensureChatMessageColumns(this.db);
14779
+ ensureObservationVectorTable(this.db);
14699
14780
  ensureChatVectorTable(this.db);
14700
14781
  ensureSyncOutboxSupportsChatMessages(this.db);
14701
14782
  }
@@ -19600,13 +19681,8 @@ function getCaptureStatus(db, input = {}) {
19600
19681
  FROM sessions s
19601
19682
  WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
19602
19683
  ${input.user_id ? "AND s.user_id = ?" : ""}
19603
- AND (
19604
- (s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
19605
- OR (
19606
- EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
19607
- AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
19608
- )
19609
- )`).get(...params)?.count ?? 0;
19684
+ AND s.tool_calls_count > 0
19685
+ AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)`).get(...params)?.count ?? 0;
19610
19686
  const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
19611
19687
  WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
19612
19688
  ORDER BY created_at_epoch DESC, prompt_number DESC
@@ -20648,14 +20724,18 @@ function captureRepoScan(input = {}) {
20648
20724
 
20649
20725
  // src/capture/transcript.ts
20650
20726
  import { createHash as createHash3 } from "node:crypto";
20651
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "node:fs";
20727
+ import { readFileSync as readFileSync4, existsSync as existsSync6, statSync, readdirSync } from "node:fs";
20652
20728
  import { join as join4 } from "node:path";
20653
20729
  import { homedir as homedir3 } from "node:os";
20654
20730
  function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
20655
20731
  if (transcriptPath)
20656
20732
  return transcriptPath;
20657
20733
  const encodedCwd = cwd.replace(/\//g, "-");
20658
- return join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
20734
+ const directPath = join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
20735
+ if (existsSync6(directPath))
20736
+ return directPath;
20737
+ const discovered = findTranscriptPathBySessionId(sessionId);
20738
+ return discovered ?? directPath;
20659
20739
  }
20660
20740
  function readTranscript(sessionId, cwd, transcriptPath) {
20661
20741
  const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
@@ -20678,10 +20758,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
20678
20758
  } catch {
20679
20759
  continue;
20680
20760
  }
20681
- const role = entry.role;
20761
+ const role = getTranscriptRole(entry);
20682
20762
  if (role !== "user" && role !== "assistant")
20683
20763
  continue;
20684
- const content = entry.content;
20764
+ const content = getTranscriptContent(entry);
20685
20765
  if (typeof content === "string") {
20686
20766
  messages.push({ role, text: content });
20687
20767
  continue;
@@ -20766,9 +20846,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
20766
20846
  createdAtEpoch: entry.timestamp
20767
20847
  })));
20768
20848
  }
20769
- async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
20849
+ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath, options = {}) {
20850
+ const embed = options.embed ?? true;
20770
20851
  const session = db.getSessionById(sessionId);
20771
- const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
20852
+ const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
20853
+ const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
20854
+ if (existsSync6(resolvedTranscriptPath)) {
20855
+ try {
20856
+ const stat = statSync(resolvedTranscriptPath);
20857
+ const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
20858
+ if (db.getSyncState(syncCursorKey) === cursor) {
20859
+ return { imported: 0, total: 0 };
20860
+ }
20861
+ db.setSyncState(syncCursorKey, cursor);
20862
+ } catch {}
20863
+ }
20864
+ const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
20772
20865
  ...message,
20773
20866
  text: message.text.trim()
20774
20867
  })).filter((message) => message.text.length > 0);
@@ -20830,7 +20923,7 @@ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
20830
20923
  created_at_epoch: createdAtEpoch
20831
20924
  });
20832
20925
  }
20833
- if (db.vecAvailable) {
20926
+ if (embed && db.vecAvailable) {
20834
20927
  const embedding = await embedText(composeChatEmbeddingText(message.text));
20835
20928
  if (embedding) {
20836
20929
  db.vecChatInsert(row.id, embedding);
@@ -20857,6 +20950,29 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
20857
20950
  const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
20858
20951
  return `history:${sessionId}:${createdAtEpoch}:${digest}`;
20859
20952
  }
20953
+ function getTranscriptRole(entry) {
20954
+ return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
20955
+ }
20956
+ function getTranscriptContent(entry) {
20957
+ return entry.content ?? entry.message?.content;
20958
+ }
20959
+ function findTranscriptPathBySessionId(sessionId) {
20960
+ const projectsDir = join4(homedir3(), ".claude", "projects");
20961
+ if (!existsSync6(projectsDir))
20962
+ return null;
20963
+ try {
20964
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
20965
+ if (!entry.isDirectory())
20966
+ continue;
20967
+ const candidate = join4(projectsDir, entry.name, `${sessionId}.jsonl`);
20968
+ if (existsSync6(candidate))
20969
+ return candidate;
20970
+ }
20971
+ } catch {
20972
+ return null;
20973
+ }
20974
+ return null;
20975
+ }
20860
20976
 
20861
20977
  // src/tools/repair-recall.ts
20862
20978
  async function repairRecall(db, config2, input = {}) {
@@ -21664,6 +21780,10 @@ function isDue(db, key, interval, now) {
21664
21780
  // src/sync/auth.ts
21665
21781
  import { createHash as createHash4 } from "node:crypto";
21666
21782
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
21783
+ var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
21784
+ var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
21785
+ var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
21786
+ var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
21667
21787
  function normalizeBaseUrl(url2) {
21668
21788
  const trimmed = url2.trim();
21669
21789
  if (!trimmed)
@@ -21682,7 +21802,7 @@ function getApiKey(config2) {
21682
21802
  const envKey = process.env.ENGRM_TOKEN;
21683
21803
  if (envKey && envKey.startsWith("cvk_"))
21684
21804
  return envKey;
21685
- if (config2.candengo_api_key && config2.candengo_api_key.length > 0) {
21805
+ if (config2.candengo_api_key && config2.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config2)) {
21686
21806
  return config2.candengo_api_key;
21687
21807
  }
21688
21808
  return null;
@@ -21703,6 +21823,23 @@ ${apiKey}
21703
21823
  ${config2.namespace}
21704
21824
  ${config2.site_id}`).digest("hex");
21705
21825
  }
21826
+ function looksLikePlaceholderConfig(config2) {
21827
+ const apiKey = config2.candengo_api_key?.trim() ?? "";
21828
+ const siteId = config2.site_id?.trim() ?? "";
21829
+ const namespace = config2.namespace?.trim() ?? "";
21830
+ const email3 = config2.user_email?.trim().toLowerCase() ?? "";
21831
+ if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
21832
+ return true;
21833
+ }
21834
+ if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email3.endsWith(suffix))) {
21835
+ return true;
21836
+ }
21837
+ return false;
21838
+ }
21839
+ function clearSyncPushBlock(db) {
21840
+ db.setSyncState("sync_push_blocked_until", "0");
21841
+ db.setSyncState("sync_push_block_reason", "");
21842
+ }
21706
21843
  function recoverOutboxAfterAuthChange(db, config2) {
21707
21844
  const fingerprint = getAuthFingerprint(config2);
21708
21845
  if (!fingerprint) {
@@ -21721,6 +21858,7 @@ function recoverOutboxAfterAuthChange(db, config2) {
21721
21858
  const syncingReset = resetSyncingEntries(db);
21722
21859
  const staleSyncingReset = 0;
21723
21860
  db.setSyncState(key, fingerprint);
21861
+ clearSyncPushBlock(db);
21724
21862
  return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
21725
21863
  }
21726
21864
  function buildSourceId(config2, localId, type = "obs") {
@@ -22093,10 +22231,16 @@ function buildSummaryVectorDocument(summary, config2, project, targetOrObservati
22093
22231
  };
22094
22232
  }
22095
22233
  async function pushOutbox(db, config2, batchSize = 50, options = {}) {
22234
+ resetStaleSyncingEntries(db);
22235
+ if (isPushBlocked(db)) {
22236
+ return { pushed: 0, failed: 0, skipped: 0, blocked: true };
22237
+ }
22096
22238
  const entries = getPendingEntries(db, batchSize);
22097
22239
  let pushed = 0;
22098
22240
  let failed = 0;
22099
22241
  let skipped = 0;
22242
+ let authFailures = 0;
22243
+ let rateLimitFailures = 0;
22100
22244
  const batch = [];
22101
22245
  for (const entry of entries) {
22102
22246
  if (entry.record_type === "summary") {
@@ -22231,14 +22375,44 @@ async function pushOutbox(db, config2, batchSize = 50, options = {}) {
22231
22375
  markSynced(db, entryId);
22232
22376
  pushed++;
22233
22377
  } catch (err) {
22234
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
22378
+ const message = err instanceof Error ? err.message : String(err);
22379
+ const kind = classifyOutboxFailure(message);
22380
+ if (kind === "auth")
22381
+ authFailures++;
22382
+ if (kind === "rate_limit")
22383
+ rateLimitFailures++;
22384
+ markFailed(db, entryId, message);
22235
22385
  failed++;
22236
22386
  }
22237
22387
  }
22238
22388
  }
22239
22389
  }
22390
+ updatePushCooldown(db, { pushed, authFailures, rateLimitFailures });
22240
22391
  return { pushed, failed, skipped };
22241
22392
  }
22393
+ var PUSH_BLOCK_UNTIL_KEY = "sync_push_blocked_until";
22394
+ var PUSH_BLOCK_REASON_KEY = "sync_push_block_reason";
22395
+ function isPushBlocked(db) {
22396
+ const blockedUntil = parseInt(db.getSyncState(PUSH_BLOCK_UNTIL_KEY) ?? "0", 10);
22397
+ return Number.isFinite(blockedUntil) && blockedUntil > Math.floor(Date.now() / 1000);
22398
+ }
22399
+ function updatePushCooldown(db, result) {
22400
+ const now = Math.floor(Date.now() / 1000);
22401
+ if (result.authFailures > 0) {
22402
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 365 * 24 * 60 * 60));
22403
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "auth");
22404
+ return;
22405
+ }
22406
+ if (result.rateLimitFailures > 0) {
22407
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 2 * 60));
22408
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "rate_limit");
22409
+ return;
22410
+ }
22411
+ if (result.pushed > 0) {
22412
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, "0");
22413
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "");
22414
+ }
22415
+ }
22242
22416
  function maybeScrubFleetDocument(doc2, target) {
22243
22417
  if (!target.isFleet)
22244
22418
  return doc2;
@@ -22637,7 +22811,7 @@ async function backfillEmbeddings(db, batchSize = 50) {
22637
22811
  }
22638
22812
 
22639
22813
  // src/packs/recommender.ts
22640
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "node:fs";
22814
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
22641
22815
  import { join as join5, basename as basename2, dirname as dirname2 } from "node:path";
22642
22816
  import { fileURLToPath } from "node:url";
22643
22817
  function getPacksDir() {
@@ -23290,7 +23464,7 @@ function installStdioLivenessGuards() {
23290
23464
  function buildServer() {
23291
23465
  const server = new McpServer({
23292
23466
  name: "engrm",
23293
- version: "0.4.45"
23467
+ version: "0.4.47"
23294
23468
  });
23295
23469
  const enabledToolNames = getEnabledToolNames(config2.tool_profile);
23296
23470
  const originalTool = server.tool.bind(server);
@@ -25306,7 +25480,7 @@ async function main() {
25306
25480
  await server.connect(transport);
25307
25481
  }
25308
25482
  function shouldStartHttpMode() {
25309
- return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT) || config2.http.enabled;
25483
+ return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT);
25310
25484
  }
25311
25485
  function resolveHttpPort() {
25312
25486
  const raw = process.env.ENGRM_HTTP_PORT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
4
4
  "description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture, live continuity, and Hermes-ready remote MCP support",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",