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.
package/dist/cli.js CHANGED
@@ -29,17 +29,26 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
29
29
  import { homedir, hostname, networkInterfaces } from "node:os";
30
30
  import { join } from "node:path";
31
31
  import { createHash } from "node:crypto";
32
- var CONFIG_DIR = join(homedir(), ".engrm");
33
- var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
34
- var DB_PATH = join(CONFIG_DIR, "engrm.db");
32
+ function resolveConfigDir() {
33
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
34
+ }
35
+ function resolveSettingsPath() {
36
+ return join(resolveConfigDir(), "settings.json");
37
+ }
38
+ function resolveDbPath() {
39
+ return join(resolveConfigDir(), "engrm.db");
40
+ }
41
+ function resolveAuthBackupPath() {
42
+ return join(resolveConfigDir(), "auth-backup.json");
43
+ }
35
44
  function getConfigDir() {
36
- return CONFIG_DIR;
45
+ return resolveConfigDir();
37
46
  }
38
47
  function getSettingsPath() {
39
- return SETTINGS_PATH;
48
+ return resolveSettingsPath();
40
49
  }
41
50
  function getDbPath() {
42
- return DB_PATH;
51
+ return resolveDbPath();
43
52
  }
44
53
  function generateDeviceId() {
45
54
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -62,7 +71,7 @@ function generateDeviceId() {
62
71
  return `${host}-${suffix}`;
63
72
  }
64
73
  function createDefaultConfig() {
65
- return {
74
+ const merged = {
66
75
  candengo_url: "",
67
76
  candengo_api_key: "",
68
77
  site_id: "",
@@ -117,24 +126,26 @@ function createDefaultConfig() {
117
126
  },
118
127
  tool_profile: "full"
119
128
  };
129
+ return merged;
120
130
  }
121
131
  function loadConfig() {
122
- if (!existsSync(SETTINGS_PATH)) {
123
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
132
+ const settingsPath = resolveSettingsPath();
133
+ if (!existsSync(settingsPath)) {
134
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
124
135
  }
125
- const raw = readFileSync(SETTINGS_PATH, "utf-8");
136
+ const raw = readFileSync(settingsPath, "utf-8");
126
137
  let parsed;
127
138
  try {
128
139
  parsed = JSON.parse(raw);
129
140
  } catch {
130
- throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
141
+ throw new Error(`Invalid JSON in ${settingsPath}`);
131
142
  }
132
143
  if (typeof parsed !== "object" || parsed === null) {
133
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
144
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
134
145
  }
135
146
  const config = parsed;
136
147
  const defaults = createDefaultConfig();
137
- return {
148
+ const merged = {
138
149
  candengo_url: asString(config["candengo_url"], defaults.candengo_url),
139
150
  candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
140
151
  site_id: asString(config["site_id"], defaults.site_id),
@@ -189,16 +200,27 @@ function loadConfig() {
189
200
  },
190
201
  tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
191
202
  };
203
+ if (looksLikePlaceholderAuth(merged)) {
204
+ return restoreAuthBackup(merged) ?? merged;
205
+ }
206
+ return merged;
192
207
  }
193
208
  function saveConfig(config) {
194
- if (!existsSync(CONFIG_DIR)) {
195
- mkdirSync(CONFIG_DIR, { recursive: true });
209
+ const configDir = resolveConfigDir();
210
+ const settingsPath = resolveSettingsPath();
211
+ const authBackupPath = resolveAuthBackupPath();
212
+ if (!existsSync(configDir)) {
213
+ mkdirSync(configDir, { recursive: true });
196
214
  }
197
- writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
215
+ writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
216
+ `, "utf-8");
217
+ if (!looksLikePlaceholderAuth(config)) {
218
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
198
219
  `, "utf-8");
220
+ }
199
221
  }
200
222
  function configExists() {
201
- return existsSync(SETTINGS_PATH);
223
+ return existsSync(resolveSettingsPath());
202
224
  }
203
225
  function asString(value, fallback) {
204
226
  return typeof value === "string" ? value : fallback;
@@ -252,6 +274,50 @@ function asTeams(value, fallback) {
252
274
  return fallback;
253
275
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
254
276
  }
277
+ function looksLikePlaceholderAuth(config) {
278
+ const apiKey = config.candengo_api_key.trim();
279
+ const siteId = config.site_id.trim();
280
+ const namespace = config.namespace.trim();
281
+ const email = config.user_email.trim().toLowerCase();
282
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
283
+ return true;
284
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
285
+ return true;
286
+ return false;
287
+ }
288
+ function extractAuthBackup(config) {
289
+ return {
290
+ candengo_url: config.candengo_url,
291
+ candengo_api_key: config.candengo_api_key,
292
+ site_id: config.site_id,
293
+ namespace: config.namespace,
294
+ user_id: config.user_id,
295
+ user_email: config.user_email,
296
+ teams: config.teams
297
+ };
298
+ }
299
+ function restoreAuthBackup(config) {
300
+ const authBackupPath = resolveAuthBackupPath();
301
+ if (!existsSync(authBackupPath))
302
+ return null;
303
+ try {
304
+ const raw = readFileSync(authBackupPath, "utf-8");
305
+ const parsed = JSON.parse(raw);
306
+ const restored = {
307
+ ...config,
308
+ candengo_url: asString(parsed["candengo_url"], config.candengo_url),
309
+ candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
310
+ site_id: asString(parsed["site_id"], config.site_id),
311
+ namespace: asString(parsed["namespace"], config.namespace),
312
+ user_id: asString(parsed["user_id"], config.user_id),
313
+ user_email: asString(parsed["user_email"], config.user_email),
314
+ teams: asTeams(parsed["teams"], config.teams)
315
+ };
316
+ return looksLikePlaceholderAuth(restored) ? null : restored;
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
255
321
 
256
322
  // src/storage/migrations.ts
257
323
  var MIGRATIONS = [
@@ -939,6 +1005,20 @@ function ensureChatMessageColumns(db) {
939
1005
  db.exec("PRAGMA user_version = 17");
940
1006
  }
941
1007
  }
1008
+ function ensureObservationVectorTable(db) {
1009
+ if (!isVecExtensionLoaded(db))
1010
+ return;
1011
+ db.exec(`
1012
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1013
+ observation_id INTEGER PRIMARY KEY,
1014
+ embedding FLOAT[384]
1015
+ );
1016
+ `);
1017
+ const current = getSchemaVersion(db);
1018
+ if (current < 4) {
1019
+ db.exec("PRAGMA user_version = 4");
1020
+ }
1021
+ }
942
1022
  function ensureChatVectorTable(db) {
943
1023
  if (!isVecExtensionLoaded(db))
944
1024
  return;
@@ -1167,6 +1247,7 @@ class MemDatabase {
1167
1247
  ensureObservationTypes(this.db);
1168
1248
  ensureSessionSummaryColumns(this.db);
1169
1249
  ensureChatMessageColumns(this.db);
1250
+ ensureObservationVectorTable(this.db);
1170
1251
  ensureChatVectorTable(this.db);
1171
1252
  ensureSyncOutboxSupportsChatMessages(this.db);
1172
1253
  }
@@ -3976,11 +4057,20 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as rea
3976
4057
  import { homedir as homedir3, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
3977
4058
  import { join as join6 } from "node:path";
3978
4059
  import { createHash as createHash3 } from "node:crypto";
3979
- var CONFIG_DIR2 = join6(homedir3(), ".engrm");
3980
- var SETTINGS_PATH2 = join6(CONFIG_DIR2, "settings.json");
3981
- var DB_PATH2 = join6(CONFIG_DIR2, "engrm.db");
4060
+ function resolveConfigDir2() {
4061
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join6(homedir3(), ".engrm");
4062
+ }
4063
+ function resolveSettingsPath2() {
4064
+ return join6(resolveConfigDir2(), "settings.json");
4065
+ }
4066
+ function resolveDbPath2() {
4067
+ return join6(resolveConfigDir2(), "engrm.db");
4068
+ }
4069
+ function resolveAuthBackupPath2() {
4070
+ return join6(resolveConfigDir2(), "auth-backup.json");
4071
+ }
3982
4072
  function getDbPath2() {
3983
- return DB_PATH2;
4073
+ return resolveDbPath2();
3984
4074
  }
3985
4075
  function generateDeviceId2() {
3986
4076
  const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -4003,7 +4093,7 @@ function generateDeviceId2() {
4003
4093
  return `${host}-${suffix}`;
4004
4094
  }
4005
4095
  function createDefaultConfig2() {
4006
- return {
4096
+ const merged = {
4007
4097
  candengo_url: "",
4008
4098
  candengo_api_key: "",
4009
4099
  site_id: "",
@@ -4058,24 +4148,26 @@ function createDefaultConfig2() {
4058
4148
  },
4059
4149
  tool_profile: "full"
4060
4150
  };
4151
+ return merged;
4061
4152
  }
4062
4153
  function loadConfig2() {
4063
- if (!existsSync6(SETTINGS_PATH2)) {
4064
- throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
4154
+ const settingsPath = resolveSettingsPath2();
4155
+ if (!existsSync6(settingsPath)) {
4156
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
4065
4157
  }
4066
- const raw = readFileSync6(SETTINGS_PATH2, "utf-8");
4158
+ const raw = readFileSync6(settingsPath, "utf-8");
4067
4159
  let parsed;
4068
4160
  try {
4069
4161
  parsed = JSON.parse(raw);
4070
4162
  } catch {
4071
- throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
4163
+ throw new Error(`Invalid JSON in ${settingsPath}`);
4072
4164
  }
4073
4165
  if (typeof parsed !== "object" || parsed === null) {
4074
- throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
4166
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
4075
4167
  }
4076
4168
  const config = parsed;
4077
4169
  const defaults = createDefaultConfig2();
4078
- return {
4170
+ const merged = {
4079
4171
  candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
4080
4172
  candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
4081
4173
  site_id: asString2(config["site_id"], defaults.site_id),
@@ -4130,16 +4222,27 @@ function loadConfig2() {
4130
4222
  },
4131
4223
  tool_profile: asToolProfile2(config["tool_profile"], defaults.tool_profile)
4132
4224
  };
4225
+ if (looksLikePlaceholderAuth2(merged)) {
4226
+ return restoreAuthBackup2(merged) ?? merged;
4227
+ }
4228
+ return merged;
4133
4229
  }
4134
4230
  function saveConfig2(config) {
4135
- if (!existsSync6(CONFIG_DIR2)) {
4136
- mkdirSync3(CONFIG_DIR2, { recursive: true });
4231
+ const configDir = resolveConfigDir2();
4232
+ const settingsPath = resolveSettingsPath2();
4233
+ const authBackupPath = resolveAuthBackupPath2();
4234
+ if (!existsSync6(configDir)) {
4235
+ mkdirSync3(configDir, { recursive: true });
4137
4236
  }
4138
- writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
4237
+ writeFileSync3(settingsPath, JSON.stringify(config, null, 2) + `
4139
4238
  `, "utf-8");
4239
+ if (!looksLikePlaceholderAuth2(config)) {
4240
+ writeFileSync3(authBackupPath, JSON.stringify(extractAuthBackup2(config), null, 2) + `
4241
+ `, "utf-8");
4242
+ }
4140
4243
  }
4141
4244
  function configExists2() {
4142
- return existsSync6(SETTINGS_PATH2);
4245
+ return existsSync6(resolveSettingsPath2());
4143
4246
  }
4144
4247
  function asString2(value, fallback) {
4145
4248
  return typeof value === "string" ? value : fallback;
@@ -4193,6 +4296,50 @@ function asTeams2(value, fallback) {
4193
4296
  return fallback;
4194
4297
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
4195
4298
  }
4299
+ function looksLikePlaceholderAuth2(config) {
4300
+ const apiKey = config.candengo_api_key.trim();
4301
+ const siteId = config.site_id.trim();
4302
+ const namespace = config.namespace.trim();
4303
+ const email = config.user_email.trim().toLowerCase();
4304
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
4305
+ return true;
4306
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
4307
+ return true;
4308
+ return false;
4309
+ }
4310
+ function extractAuthBackup2(config) {
4311
+ return {
4312
+ candengo_url: config.candengo_url,
4313
+ candengo_api_key: config.candengo_api_key,
4314
+ site_id: config.site_id,
4315
+ namespace: config.namespace,
4316
+ user_id: config.user_id,
4317
+ user_email: config.user_email,
4318
+ teams: config.teams
4319
+ };
4320
+ }
4321
+ function restoreAuthBackup2(config) {
4322
+ const authBackupPath = resolveAuthBackupPath2();
4323
+ if (!existsSync6(authBackupPath))
4324
+ return null;
4325
+ try {
4326
+ const raw = readFileSync6(authBackupPath, "utf-8");
4327
+ const parsed = JSON.parse(raw);
4328
+ const restored = {
4329
+ ...config,
4330
+ candengo_url: asString2(parsed["candengo_url"], config.candengo_url),
4331
+ candengo_api_key: asString2(parsed["candengo_api_key"], config.candengo_api_key),
4332
+ site_id: asString2(parsed["site_id"], config.site_id),
4333
+ namespace: asString2(parsed["namespace"], config.namespace),
4334
+ user_id: asString2(parsed["user_id"], config.user_id),
4335
+ user_email: asString2(parsed["user_email"], config.user_email),
4336
+ teams: asTeams2(parsed["teams"], config.teams)
4337
+ };
4338
+ return looksLikePlaceholderAuth2(restored) ? null : restored;
4339
+ } catch {
4340
+ return null;
4341
+ }
4342
+ }
4196
4343
 
4197
4344
  // src/tool-profiles.ts
4198
4345
  var MEMORY_PROFILE_TOOLS = [
@@ -4291,13 +4438,8 @@ function getCaptureStatus(db, input = {}) {
4291
4438
  FROM sessions s
4292
4439
  WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
4293
4440
  ${input.user_id ? "AND s.user_id = ?" : ""}
4294
- AND (
4295
- (s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
4296
- OR (
4297
- EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
4298
- AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
4299
- )
4300
- )`).get(...params)?.count ?? 0;
4441
+ AND s.tool_calls_count > 0
4442
+ AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)`).get(...params)?.count ?? 0;
4301
4443
  const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
4302
4444
  WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
4303
4445
  ORDER BY created_at_epoch DESC, prompt_number DESC
@@ -4478,6 +4620,10 @@ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
4478
4620
 
4479
4621
  // src/sync/auth.ts
4480
4622
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
4623
+ var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
4624
+ var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
4625
+ var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
4626
+ var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
4481
4627
  function normalizeBaseUrl(url) {
4482
4628
  const trimmed = url.trim();
4483
4629
  if (!trimmed)
@@ -4496,7 +4642,7 @@ function getApiKey(config) {
4496
4642
  const envKey = process.env.ENGRM_TOKEN;
4497
4643
  if (envKey && envKey.startsWith("cvk_"))
4498
4644
  return envKey;
4499
- if (config.candengo_api_key && config.candengo_api_key.length > 0) {
4645
+ if (config.candengo_api_key && config.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config)) {
4500
4646
  return config.candengo_api_key;
4501
4647
  }
4502
4648
  return null;
@@ -4517,18 +4663,37 @@ ${apiKey}
4517
4663
  ${config.namespace}
4518
4664
  ${config.site_id}`).digest("hex");
4519
4665
  }
4520
- function recoverOutboxAfterSuccessfulAuth(db, config) {
4666
+ function looksLikePlaceholderConfig(config) {
4667
+ const apiKey = config.candengo_api_key?.trim() ?? "";
4668
+ const siteId = config.site_id?.trim() ?? "";
4669
+ const namespace = config.namespace?.trim() ?? "";
4670
+ const email = config.user_email?.trim().toLowerCase() ?? "";
4671
+ if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
4672
+ return true;
4673
+ }
4674
+ if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email.endsWith(suffix))) {
4675
+ return true;
4676
+ }
4677
+ return false;
4678
+ }
4679
+ function clearSyncPushBlock(db) {
4680
+ db.setSyncState("sync_push_blocked_until", "0");
4681
+ db.setSyncState("sync_push_block_reason", "");
4682
+ }
4683
+ function resumeOutboxAfterValidatedAuth(db, config) {
4521
4684
  const fingerprint = getAuthFingerprint(config);
4522
- const staleSyncingReset = resetStaleSyncingEntries(db);
4523
- const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure2(error) === "auth");
4685
+ const failedReset = resetFailedEntries(db);
4686
+ const syncingReset = resetSyncingEntries(db);
4687
+ const staleSyncingReset = 0;
4524
4688
  if (fingerprint) {
4525
4689
  db.setSyncState("sync_auth_fingerprint", fingerprint);
4526
4690
  }
4691
+ clearSyncPushBlock(db);
4527
4692
  return {
4528
4693
  fingerprintChanged: false,
4529
- failedReset: 0,
4530
- authFailedReset,
4531
- syncingReset: 0,
4694
+ failedReset,
4695
+ authFailedReset: 0,
4696
+ syncingReset,
4532
4697
  staleSyncingReset
4533
4698
  };
4534
4699
  }
@@ -4552,6 +4717,9 @@ switch (command) {
4552
4717
  case "update":
4553
4718
  handleUpdate();
4554
4719
  break;
4720
+ case "sync":
4721
+ await handleSync(args.slice(1));
4722
+ break;
4555
4723
  case "install-pack":
4556
4724
  await handleInstallPack(args.slice(1));
4557
4725
  break;
@@ -4710,31 +4878,7 @@ Authorization failed: ${error instanceof Error ? error.message : String(error)}`
4710
4878
  }
4711
4879
  function writeConfigFromProvision(baseUrl, result) {
4712
4880
  ensureConfigDir();
4713
- let existingDeviceId;
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;
4723
- if (configExists()) {
4724
- try {
4725
- const existing = loadConfig();
4726
- existingDeviceId = existing.device_id;
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;
4736
- } catch {}
4737
- }
4881
+ const preserved = loadPreservedLocalConfig();
4738
4882
  const config = {
4739
4883
  candengo_url: baseUrl,
4740
4884
  candengo_api_key: result.api_key,
@@ -4742,24 +4886,24 @@ function writeConfigFromProvision(baseUrl, result) {
4742
4886
  namespace: result.namespace,
4743
4887
  user_id: result.user_id,
4744
4888
  user_email: result.user_email,
4745
- device_id: existingDeviceId || generateDeviceId3(),
4889
+ device_id: preserved.device_id || generateDeviceId3(),
4746
4890
  teams: result.teams ?? [],
4747
- sync: existingSync ?? {
4891
+ sync: preserved.sync ?? {
4748
4892
  enabled: true,
4749
4893
  interval_seconds: 30,
4750
4894
  batch_size: 50
4751
4895
  },
4752
- search: existingSearch ?? {
4896
+ search: preserved.search ?? {
4753
4897
  default_limit: 10,
4754
4898
  local_boost: 1.2,
4755
4899
  scope: "all"
4756
4900
  },
4757
- scrubbing: existingScrubbing ?? {
4901
+ scrubbing: preserved.scrubbing ?? {
4758
4902
  enabled: true,
4759
4903
  custom_patterns: [],
4760
4904
  default_sensitivity: "shared"
4761
4905
  },
4762
- sentinel: existingSentinel ?? {
4906
+ sentinel: preserved.sentinel ?? {
4763
4907
  enabled: false,
4764
4908
  mode: "advisory",
4765
4909
  provider: "openai",
@@ -4770,33 +4914,54 @@ function writeConfigFromProvision(baseUrl, result) {
4770
4914
  daily_limit: 100,
4771
4915
  tier: "free"
4772
4916
  },
4773
- observer: existingObserver ?? {
4917
+ observer: preserved.observer ?? {
4774
4918
  enabled: true,
4775
4919
  mode: "per_event",
4776
4920
  model: "haiku"
4777
4921
  },
4778
- transcript_analysis: existingTranscriptAnalysis ?? {
4922
+ transcript_analysis: preserved.transcript_analysis ?? {
4779
4923
  enabled: false
4780
4924
  },
4781
- http: existingHttp ?? {
4925
+ http: preserved.http ?? {
4782
4926
  enabled: false,
4783
4927
  port: 3767,
4784
4928
  bearer_tokens: []
4785
4929
  },
4786
- fleet: existingFleet ?? {
4930
+ fleet: preserved.fleet ?? {
4787
4931
  project_name: "shared-experience",
4788
4932
  namespace: "",
4789
4933
  api_key: ""
4790
4934
  },
4791
- tool_profile: existingToolProfile ?? "full"
4935
+ tool_profile: preserved.tool_profile ?? "full"
4792
4936
  };
4793
4937
  saveConfig(config);
4794
4938
  const db = new MemDatabase(getDbPath());
4795
- recoverOutboxAfterSuccessfulAuth(db, config);
4939
+ resumeOutboxAfterValidatedAuth(db, config);
4796
4940
  db.close();
4797
4941
  console.log(`Configuration saved to ${getSettingsPath()}`);
4798
4942
  console.log(`Database initialised at ${getDbPath()}`);
4799
4943
  }
4944
+ function loadPreservedLocalConfig() {
4945
+ if (!configExists())
4946
+ return {};
4947
+ try {
4948
+ const existing = loadConfig();
4949
+ return {
4950
+ device_id: existing.device_id,
4951
+ sync: existing.sync,
4952
+ search: existing.search,
4953
+ scrubbing: existing.scrubbing,
4954
+ sentinel: existing.sentinel,
4955
+ observer: existing.observer,
4956
+ transcript_analysis: existing.transcript_analysis,
4957
+ http: existing.http,
4958
+ fleet: existing.fleet,
4959
+ tool_profile: existing.tool_profile
4960
+ };
4961
+ } catch {
4962
+ return {};
4963
+ }
4964
+ }
4800
4965
  function initFromFile(configPath) {
4801
4966
  if (!existsSync8(configPath)) {
4802
4967
  console.error(`Config file not found: ${configPath}`);
@@ -4829,6 +4994,7 @@ function initFromFile(configPath) {
4829
4994
  }
4830
4995
  }
4831
4996
  ensureConfigDir();
4997
+ const preserved = loadPreservedLocalConfig();
4832
4998
  const config = {
4833
4999
  candengo_url: input["candengo_url"].trim(),
4834
5000
  candengo_api_key: input["candengo_api_key"].trim(),
@@ -4836,24 +5002,24 @@ function initFromFile(configPath) {
4836
5002
  namespace: input["namespace"].trim(),
4837
5003
  user_id: input["user_id"].trim(),
4838
5004
  user_email: typeof input["user_email"] === "string" ? input["user_email"].trim() : "",
4839
- device_id: typeof input["device_id"] === "string" ? input["device_id"] : generateDeviceId3(),
5005
+ device_id: typeof input["device_id"] === "string" ? input["device_id"] : preserved.device_id ?? generateDeviceId3(),
4840
5006
  teams: [],
4841
- sync: {
5007
+ sync: preserved.sync ?? {
4842
5008
  enabled: true,
4843
5009
  interval_seconds: 30,
4844
5010
  batch_size: 50
4845
5011
  },
4846
- search: {
5012
+ search: preserved.search ?? {
4847
5013
  default_limit: 10,
4848
5014
  local_boost: 1.2,
4849
5015
  scope: "all"
4850
5016
  },
4851
- scrubbing: {
5017
+ scrubbing: preserved.scrubbing ?? {
4852
5018
  enabled: true,
4853
5019
  custom_patterns: [],
4854
5020
  default_sensitivity: "shared"
4855
5021
  },
4856
- sentinel: {
5022
+ sentinel: preserved.sentinel ?? {
4857
5023
  enabled: false,
4858
5024
  mode: "advisory",
4859
5025
  provider: "openai",
@@ -4864,14 +5030,25 @@ function initFromFile(configPath) {
4864
5030
  daily_limit: 100,
4865
5031
  tier: "free"
4866
5032
  },
4867
- observer: {
5033
+ observer: preserved.observer ?? {
4868
5034
  enabled: true,
4869
5035
  mode: "per_event",
4870
5036
  model: "haiku"
4871
5037
  },
4872
- transcript_analysis: {
5038
+ transcript_analysis: preserved.transcript_analysis ?? {
4873
5039
  enabled: false
4874
- }
5040
+ },
5041
+ http: preserved.http ?? {
5042
+ enabled: false,
5043
+ port: 3767,
5044
+ bearer_tokens: []
5045
+ },
5046
+ fleet: preserved.fleet ?? {
5047
+ project_name: "shared-experience",
5048
+ namespace: "",
5049
+ api_key: ""
5050
+ },
5051
+ tool_profile: preserved.tool_profile ?? "full"
4875
5052
  };
4876
5053
  saveConfig(config);
4877
5054
  const db = new MemDatabase(getDbPath());
@@ -4902,6 +5079,7 @@ async function initManual() {
4902
5079
  process.exit(1);
4903
5080
  }
4904
5081
  ensureConfigDir();
5082
+ const preserved = loadPreservedLocalConfig();
4905
5083
  const config = {
4906
5084
  candengo_url: candengoUrl.trim(),
4907
5085
  candengo_api_key: apiKey.trim(),
@@ -4909,24 +5087,24 @@ async function initManual() {
4909
5087
  namespace: namespace.trim(),
4910
5088
  user_id: userId.trim(),
4911
5089
  user_email: userEmail.trim(),
4912
- device_id: generateDeviceId3(),
5090
+ device_id: preserved.device_id ?? generateDeviceId3(),
4913
5091
  teams: [],
4914
- sync: {
5092
+ sync: preserved.sync ?? {
4915
5093
  enabled: true,
4916
5094
  interval_seconds: 30,
4917
5095
  batch_size: 50
4918
5096
  },
4919
- search: {
5097
+ search: preserved.search ?? {
4920
5098
  default_limit: 10,
4921
5099
  local_boost: 1.2,
4922
5100
  scope: "all"
4923
5101
  },
4924
- scrubbing: {
5102
+ scrubbing: preserved.scrubbing ?? {
4925
5103
  enabled: true,
4926
5104
  custom_patterns: [],
4927
5105
  default_sensitivity: "shared"
4928
5106
  },
4929
- sentinel: {
5107
+ sentinel: preserved.sentinel ?? {
4930
5108
  enabled: false,
4931
5109
  mode: "advisory",
4932
5110
  provider: "openai",
@@ -4937,14 +5115,25 @@ async function initManual() {
4937
5115
  daily_limit: 100,
4938
5116
  tier: "free"
4939
5117
  },
4940
- observer: {
5118
+ observer: preserved.observer ?? {
4941
5119
  enabled: true,
4942
5120
  mode: "per_event",
4943
5121
  model: "haiku"
4944
5122
  },
4945
- transcript_analysis: {
5123
+ transcript_analysis: preserved.transcript_analysis ?? {
4946
5124
  enabled: false
4947
- }
5125
+ },
5126
+ http: preserved.http ?? {
5127
+ enabled: false,
5128
+ port: 3767,
5129
+ bearer_tokens: []
5130
+ },
5131
+ fleet: preserved.fleet ?? {
5132
+ project_name: "shared-experience",
5133
+ namespace: "",
5134
+ api_key: ""
5135
+ },
5136
+ tool_profile: preserved.tool_profile ?? "full"
4948
5137
  };
4949
5138
  saveConfig(config);
4950
5139
  const db = new MemDatabase(getDbPath());
@@ -5129,6 +5318,15 @@ function handleStatus() {
5129
5318
  console.log(`
5130
5319
  Sync`);
5131
5320
  console.log(` Outbox: ${outbox["pending"] ?? 0} pending, ${outbox["failed"] ?? 0} failed, ${outbox["synced"] ?? 0} synced`);
5321
+ const syncBlock = getSyncBlockState(db);
5322
+ if (syncBlock.active) {
5323
+ console.log(` Push block: ${formatSyncBlock(syncBlock)}`);
5324
+ if (syncBlock.reason === "auth") {
5325
+ console.log(" Next step: Re-run `engrm init`, then `engrm sync resume`");
5326
+ } else if (syncBlock.reason === "rate_limit") {
5327
+ console.log(" Next step: Wait for the block to expire or retry later");
5328
+ }
5329
+ }
5132
5330
  const topFailures = getOutboxFailureSummaries(db, 2);
5133
5331
  if (topFailures.length > 0) {
5134
5332
  const failureSummary = topFailures.map((row) => `${classifyOutboxFailure(row.error)} ${row.count}`).join(", ");
@@ -5183,6 +5381,18 @@ function formatTimeAgo(epoch) {
5183
5381
  return `${Math.floor(ago / 3600)}h ago`;
5184
5382
  return `${Math.floor(ago / 86400)}d ago`;
5185
5383
  }
5384
+ function formatTimeUntil(epoch) {
5385
+ const remaining = epoch - Math.floor(Date.now() / 1000);
5386
+ if (remaining <= 0)
5387
+ return "now";
5388
+ if (remaining < 60)
5389
+ return `in ${remaining}s`;
5390
+ if (remaining < 3600)
5391
+ return `in ${Math.floor(remaining / 60)}m`;
5392
+ if (remaining < 86400)
5393
+ return `in ${Math.floor(remaining / 3600)}h`;
5394
+ return `in ${Math.floor(remaining / 86400)}d`;
5395
+ }
5186
5396
  function formatSyncTime(epochStr) {
5187
5397
  if (!epochStr)
5188
5398
  return "never";
@@ -5191,6 +5401,25 @@ function formatSyncTime(epochStr) {
5191
5401
  return "never";
5192
5402
  return formatTimeAgo(epoch);
5193
5403
  }
5404
+ function getSyncBlockState(db) {
5405
+ const untilRaw = db.getSyncState("sync_push_blocked_until");
5406
+ const reason = db.getSyncState("sync_push_block_reason");
5407
+ const untilEpoch = untilRaw ? parseInt(untilRaw, 10) : NaN;
5408
+ if (!Number.isFinite(untilEpoch) || untilEpoch <= Math.floor(Date.now() / 1000)) {
5409
+ return { active: false, untilEpoch: null, reason: null };
5410
+ }
5411
+ return {
5412
+ active: true,
5413
+ untilEpoch,
5414
+ reason: reason && reason.length > 0 ? reason : null
5415
+ };
5416
+ }
5417
+ function formatSyncBlock(block) {
5418
+ const reasonLabel = block.reason === "auth" ? "waiting for re-auth" : block.reason === "rate_limit" ? "rate limited" : block.reason ?? "paused";
5419
+ if (!block.untilEpoch)
5420
+ return reasonLabel;
5421
+ return `${reasonLabel} until ${new Date(block.untilEpoch * 1000).toISOString()} (${formatTimeUntil(block.untilEpoch)})`;
5422
+ }
5194
5423
  function ensureConfigDir() {
5195
5424
  const dir = getConfigDir();
5196
5425
  if (!existsSync8(dir)) {
@@ -5322,6 +5551,56 @@ Restart Claude Code or Codex to use the new version.`);
5322
5551
  console.error("Try manually: npm install -g engrm@<version>");
5323
5552
  }
5324
5553
  }
5554
+ async function handleSync(flags) {
5555
+ const subcommand = flags[0];
5556
+ if (subcommand === "resume") {
5557
+ if (!configExists()) {
5558
+ console.error("Engrm is not configured. Run: engrm init");
5559
+ process.exit(1);
5560
+ }
5561
+ const config = loadConfig();
5562
+ if (!config.candengo_url || !config.candengo_api_key) {
5563
+ console.error("Authentication is not configured. Run: engrm init");
5564
+ process.exit(1);
5565
+ }
5566
+ try {
5567
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
5568
+ const controller = new AbortController;
5569
+ const timeout = setTimeout(() => controller.abort(), 5000);
5570
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
5571
+ headers: { Authorization: `Bearer ${config.candengo_api_key}` },
5572
+ signal: controller.signal
5573
+ });
5574
+ clearTimeout(timeout);
5575
+ if (!res.ok) {
5576
+ if (res.status === 401 || res.status === 403) {
5577
+ console.error("Sync resume blocked: authentication is still invalid. Re-run `engrm init` first.");
5578
+ } else {
5579
+ console.error(`Sync resume blocked: authentication check returned HTTP ${res.status}.`);
5580
+ }
5581
+ process.exit(1);
5582
+ }
5583
+ const db = new MemDatabase(getDbPath());
5584
+ try {
5585
+ const recovery = resumeOutboxAfterValidatedAuth(db, config);
5586
+ const pending = getOutboxStats(db)["pending"] ?? 0;
5587
+ console.log("Sync queue resumed.");
5588
+ console.log(` Failed reset: ${recovery.failedReset}`);
5589
+ console.log(` Syncing reset: ${recovery.syncingReset}`);
5590
+ console.log(` Pending now: ${pending}`);
5591
+ } finally {
5592
+ db.close();
5593
+ }
5594
+ return;
5595
+ } catch (error) {
5596
+ console.error(`Sync resume failed: ${error instanceof Error ? error.message : String(error)}`);
5597
+ process.exit(1);
5598
+ }
5599
+ }
5600
+ console.log(`Sync commands:
5601
+ `);
5602
+ console.log(" engrm sync resume Validate auth and unblock the paused sync queue");
5603
+ }
5325
5604
  async function handleDoctor() {
5326
5605
  const results = [];
5327
5606
  const pass = (msg) => results.push({ symbol: "\u2713", message: msg, kind: "pass" });
@@ -5547,26 +5826,30 @@ async function handleDoctor() {
5547
5826
  fail("Server URL not configured");
5548
5827
  }
5549
5828
  if (config.candengo_url && config.candengo_api_key) {
5550
- try {
5551
- const baseUrl = normalizeBaseUrl(config.candengo_url);
5552
- const controller = new AbortController;
5553
- const timeout = setTimeout(() => controller.abort(), 5000);
5554
- const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
5555
- headers: { Authorization: `Bearer ${config.candengo_api_key}` },
5556
- signal: controller.signal
5557
- });
5558
- clearTimeout(timeout);
5559
- if (res.ok) {
5560
- const email = config.user_email ?? "configured";
5561
- pass(`Authentication valid (${email})`);
5562
- } else if (res.status === 401 || res.status === 403) {
5563
- fail("Authentication failed \u2014 API key may be expired");
5564
- } else {
5565
- fail(`Authentication check returned HTTP ${res.status}`);
5829
+ if (looksLikePlaceholderConfig(config)) {
5830
+ fail("Authentication is using placeholder credentials \u2014 re-run 'engrm init' to restore real account settings");
5831
+ } else {
5832
+ try {
5833
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
5834
+ const controller = new AbortController;
5835
+ const timeout = setTimeout(() => controller.abort(), 5000);
5836
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
5837
+ headers: { Authorization: `Bearer ${config.candengo_api_key}` },
5838
+ signal: controller.signal
5839
+ });
5840
+ clearTimeout(timeout);
5841
+ if (res.ok) {
5842
+ const email = config.user_email ?? "configured";
5843
+ pass(`Authentication valid (${email})`);
5844
+ } else if (res.status === 401 || res.status === 403) {
5845
+ fail("Authentication failed \u2014 API key may be expired");
5846
+ } else {
5847
+ fail(`Authentication check returned HTTP ${res.status}`);
5848
+ }
5849
+ } catch (err) {
5850
+ const msg = err instanceof Error ? err.message : String(err);
5851
+ fail(`Authentication check failed: ${msg.includes("abort") ? "timeout (5s)" : msg}`);
5566
5852
  }
5567
- } catch (err) {
5568
- const msg = err instanceof Error ? err.message : String(err);
5569
- fail(`Authentication check failed: ${msg.includes("abort") ? "timeout (5s)" : msg}`);
5570
5853
  }
5571
5854
  } else {
5572
5855
  fail("Authentication not configured (missing URL or API key)");
@@ -5574,7 +5857,16 @@ async function handleDoctor() {
5574
5857
  try {
5575
5858
  const outbox = getOutboxStats(db);
5576
5859
  const failedCount = outbox["failed"] ?? 0;
5577
- if (failedCount > 10) {
5860
+ const syncBlock = getSyncBlockState(db);
5861
+ if (syncBlock.active) {
5862
+ const pending = outbox["pending"] ?? 0;
5863
+ warn(`Sync push is paused (${formatSyncBlock(syncBlock)}; ${pending} pending, ${failedCount} failed)`);
5864
+ if (syncBlock.reason === "auth") {
5865
+ info("Next step: re-run `engrm init`, then `engrm sync resume`");
5866
+ } else if (syncBlock.reason === "rate_limit") {
5867
+ info("Next step: wait for the pause window to expire, then try again");
5868
+ }
5869
+ } else if (failedCount > 10) {
5578
5870
  warn(`Sync has stuck items (${failedCount} failed in outbox)`);
5579
5871
  } else {
5580
5872
  const pending = outbox["pending"] ?? 0;
@@ -5802,6 +6094,7 @@ function printUsage() {
5802
6094
  console.log(" engrm init --config <file> Setup from JSON file");
5803
6095
  console.log(" engrm status Show status");
5804
6096
  console.log(" engrm update Update to latest version");
6097
+ console.log(" engrm sync resume Validate auth and resume paused sync queue");
5805
6098
  console.log(" engrm packs List available starter packs");
5806
6099
  console.log(" engrm install-pack <name> Install a starter pack");
5807
6100
  console.log(" engrm doctor Run diagnostic checks");