engrm 0.4.45 → 0.4.46

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/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) + `
13745
+ `, "utf-8");
13746
+ if (!looksLikePlaceholderAuth(config2)) {
13747
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config2), null, 2) + `
13727
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 = [
@@ -19600,13 +19666,8 @@ function getCaptureStatus(db, input = {}) {
19600
19666
  FROM sessions s
19601
19667
  WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
19602
19668
  ${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;
19669
+ AND s.tool_calls_count > 0
19670
+ AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)`).get(...params)?.count ?? 0;
19610
19671
  const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
19611
19672
  WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
19612
19673
  ORDER BY created_at_epoch DESC, prompt_number DESC
@@ -20648,14 +20709,18 @@ function captureRepoScan(input = {}) {
20648
20709
 
20649
20710
  // src/capture/transcript.ts
20650
20711
  import { createHash as createHash3 } from "node:crypto";
20651
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "node:fs";
20712
+ import { readFileSync as readFileSync4, existsSync as existsSync6, statSync, readdirSync } from "node:fs";
20652
20713
  import { join as join4 } from "node:path";
20653
20714
  import { homedir as homedir3 } from "node:os";
20654
20715
  function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
20655
20716
  if (transcriptPath)
20656
20717
  return transcriptPath;
20657
20718
  const encodedCwd = cwd.replace(/\//g, "-");
20658
- return join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
20719
+ const directPath = join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
20720
+ if (existsSync6(directPath))
20721
+ return directPath;
20722
+ const discovered = findTranscriptPathBySessionId(sessionId);
20723
+ return discovered ?? directPath;
20659
20724
  }
20660
20725
  function readTranscript(sessionId, cwd, transcriptPath) {
20661
20726
  const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
@@ -20678,10 +20743,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
20678
20743
  } catch {
20679
20744
  continue;
20680
20745
  }
20681
- const role = entry.role;
20746
+ const role = getTranscriptRole(entry);
20682
20747
  if (role !== "user" && role !== "assistant")
20683
20748
  continue;
20684
- const content = entry.content;
20749
+ const content = getTranscriptContent(entry);
20685
20750
  if (typeof content === "string") {
20686
20751
  messages.push({ role, text: content });
20687
20752
  continue;
@@ -20766,9 +20831,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
20766
20831
  createdAtEpoch: entry.timestamp
20767
20832
  })));
20768
20833
  }
20769
- async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
20834
+ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath, options = {}) {
20835
+ const embed = options.embed ?? true;
20770
20836
  const session = db.getSessionById(sessionId);
20771
- const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
20837
+ const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
20838
+ const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
20839
+ if (existsSync6(resolvedTranscriptPath)) {
20840
+ try {
20841
+ const stat = statSync(resolvedTranscriptPath);
20842
+ const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
20843
+ if (db.getSyncState(syncCursorKey) === cursor) {
20844
+ return { imported: 0, total: 0 };
20845
+ }
20846
+ db.setSyncState(syncCursorKey, cursor);
20847
+ } catch {}
20848
+ }
20849
+ const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
20772
20850
  ...message,
20773
20851
  text: message.text.trim()
20774
20852
  })).filter((message) => message.text.length > 0);
@@ -20830,7 +20908,7 @@ async function syncTranscriptChat(db, config2, sessionId, cwd, transcriptPath) {
20830
20908
  created_at_epoch: createdAtEpoch
20831
20909
  });
20832
20910
  }
20833
- if (db.vecAvailable) {
20911
+ if (embed && db.vecAvailable) {
20834
20912
  const embedding = await embedText(composeChatEmbeddingText(message.text));
20835
20913
  if (embedding) {
20836
20914
  db.vecChatInsert(row.id, embedding);
@@ -20857,6 +20935,29 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
20857
20935
  const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
20858
20936
  return `history:${sessionId}:${createdAtEpoch}:${digest}`;
20859
20937
  }
20938
+ function getTranscriptRole(entry) {
20939
+ return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
20940
+ }
20941
+ function getTranscriptContent(entry) {
20942
+ return entry.content ?? entry.message?.content;
20943
+ }
20944
+ function findTranscriptPathBySessionId(sessionId) {
20945
+ const projectsDir = join4(homedir3(), ".claude", "projects");
20946
+ if (!existsSync6(projectsDir))
20947
+ return null;
20948
+ try {
20949
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
20950
+ if (!entry.isDirectory())
20951
+ continue;
20952
+ const candidate = join4(projectsDir, entry.name, `${sessionId}.jsonl`);
20953
+ if (existsSync6(candidate))
20954
+ return candidate;
20955
+ }
20956
+ } catch {
20957
+ return null;
20958
+ }
20959
+ return null;
20960
+ }
20860
20961
 
20861
20962
  // src/tools/repair-recall.ts
20862
20963
  async function repairRecall(db, config2, input = {}) {
@@ -21664,6 +21765,10 @@ function isDue(db, key, interval, now) {
21664
21765
  // src/sync/auth.ts
21665
21766
  import { createHash as createHash4 } from "node:crypto";
21666
21767
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
21768
+ var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
21769
+ var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
21770
+ var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
21771
+ var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
21667
21772
  function normalizeBaseUrl(url2) {
21668
21773
  const trimmed = url2.trim();
21669
21774
  if (!trimmed)
@@ -21682,7 +21787,7 @@ function getApiKey(config2) {
21682
21787
  const envKey = process.env.ENGRM_TOKEN;
21683
21788
  if (envKey && envKey.startsWith("cvk_"))
21684
21789
  return envKey;
21685
- if (config2.candengo_api_key && config2.candengo_api_key.length > 0) {
21790
+ if (config2.candengo_api_key && config2.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config2)) {
21686
21791
  return config2.candengo_api_key;
21687
21792
  }
21688
21793
  return null;
@@ -21703,6 +21808,23 @@ ${apiKey}
21703
21808
  ${config2.namespace}
21704
21809
  ${config2.site_id}`).digest("hex");
21705
21810
  }
21811
+ function looksLikePlaceholderConfig(config2) {
21812
+ const apiKey = config2.candengo_api_key?.trim() ?? "";
21813
+ const siteId = config2.site_id?.trim() ?? "";
21814
+ const namespace = config2.namespace?.trim() ?? "";
21815
+ const email3 = config2.user_email?.trim().toLowerCase() ?? "";
21816
+ if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
21817
+ return true;
21818
+ }
21819
+ if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email3.endsWith(suffix))) {
21820
+ return true;
21821
+ }
21822
+ return false;
21823
+ }
21824
+ function clearSyncPushBlock(db) {
21825
+ db.setSyncState("sync_push_blocked_until", "0");
21826
+ db.setSyncState("sync_push_block_reason", "");
21827
+ }
21706
21828
  function recoverOutboxAfterAuthChange(db, config2) {
21707
21829
  const fingerprint = getAuthFingerprint(config2);
21708
21830
  if (!fingerprint) {
@@ -21721,6 +21843,7 @@ function recoverOutboxAfterAuthChange(db, config2) {
21721
21843
  const syncingReset = resetSyncingEntries(db);
21722
21844
  const staleSyncingReset = 0;
21723
21845
  db.setSyncState(key, fingerprint);
21846
+ clearSyncPushBlock(db);
21724
21847
  return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
21725
21848
  }
21726
21849
  function buildSourceId(config2, localId, type = "obs") {
@@ -22093,10 +22216,16 @@ function buildSummaryVectorDocument(summary, config2, project, targetOrObservati
22093
22216
  };
22094
22217
  }
22095
22218
  async function pushOutbox(db, config2, batchSize = 50, options = {}) {
22219
+ resetStaleSyncingEntries(db);
22220
+ if (isPushBlocked(db)) {
22221
+ return { pushed: 0, failed: 0, skipped: 0, blocked: true };
22222
+ }
22096
22223
  const entries = getPendingEntries(db, batchSize);
22097
22224
  let pushed = 0;
22098
22225
  let failed = 0;
22099
22226
  let skipped = 0;
22227
+ let authFailures = 0;
22228
+ let rateLimitFailures = 0;
22100
22229
  const batch = [];
22101
22230
  for (const entry of entries) {
22102
22231
  if (entry.record_type === "summary") {
@@ -22231,14 +22360,44 @@ async function pushOutbox(db, config2, batchSize = 50, options = {}) {
22231
22360
  markSynced(db, entryId);
22232
22361
  pushed++;
22233
22362
  } catch (err) {
22234
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
22363
+ const message = err instanceof Error ? err.message : String(err);
22364
+ const kind = classifyOutboxFailure(message);
22365
+ if (kind === "auth")
22366
+ authFailures++;
22367
+ if (kind === "rate_limit")
22368
+ rateLimitFailures++;
22369
+ markFailed(db, entryId, message);
22235
22370
  failed++;
22236
22371
  }
22237
22372
  }
22238
22373
  }
22239
22374
  }
22375
+ updatePushCooldown(db, { pushed, authFailures, rateLimitFailures });
22240
22376
  return { pushed, failed, skipped };
22241
22377
  }
22378
+ var PUSH_BLOCK_UNTIL_KEY = "sync_push_blocked_until";
22379
+ var PUSH_BLOCK_REASON_KEY = "sync_push_block_reason";
22380
+ function isPushBlocked(db) {
22381
+ const blockedUntil = parseInt(db.getSyncState(PUSH_BLOCK_UNTIL_KEY) ?? "0", 10);
22382
+ return Number.isFinite(blockedUntil) && blockedUntil > Math.floor(Date.now() / 1000);
22383
+ }
22384
+ function updatePushCooldown(db, result) {
22385
+ const now = Math.floor(Date.now() / 1000);
22386
+ if (result.authFailures > 0) {
22387
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 365 * 24 * 60 * 60));
22388
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "auth");
22389
+ return;
22390
+ }
22391
+ if (result.rateLimitFailures > 0) {
22392
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, String(now + 2 * 60));
22393
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "rate_limit");
22394
+ return;
22395
+ }
22396
+ if (result.pushed > 0) {
22397
+ db.setSyncState(PUSH_BLOCK_UNTIL_KEY, "0");
22398
+ db.setSyncState(PUSH_BLOCK_REASON_KEY, "");
22399
+ }
22400
+ }
22242
22401
  function maybeScrubFleetDocument(doc2, target) {
22243
22402
  if (!target.isFleet)
22244
22403
  return doc2;
@@ -22637,7 +22796,7 @@ async function backfillEmbeddings(db, batchSize = 50) {
22637
22796
  }
22638
22797
 
22639
22798
  // src/packs/recommender.ts
22640
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "node:fs";
22799
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
22641
22800
  import { join as join5, basename as basename2, dirname as dirname2 } from "node:path";
22642
22801
  import { fileURLToPath } from "node:url";
22643
22802
  function getPacksDir() {
@@ -23290,7 +23449,7 @@ function installStdioLivenessGuards() {
23290
23449
  function buildServer() {
23291
23450
  const server = new McpServer({
23292
23451
  name: "engrm",
23293
- version: "0.4.45"
23452
+ version: "0.4.46"
23294
23453
  });
23295
23454
  const enabledToolNames = getEnabledToolNames(config2.tool_profile);
23296
23455
  const originalTool = server.tool.bind(server);
@@ -25306,7 +25465,7 @@ async function main() {
25306
25465
  await server.connect(transport);
25307
25466
  }
25308
25467
  function shouldStartHttpMode() {
25309
- return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT) || config2.http.enabled;
25468
+ return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT);
25310
25469
  }
25311
25470
  function resolveHttpPort() {
25312
25471
  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.46",
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",