engrm 0.4.44 → 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/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 = [
@@ -3976,11 +4042,20 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as rea
3976
4042
  import { homedir as homedir3, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
3977
4043
  import { join as join6 } from "node:path";
3978
4044
  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");
4045
+ function resolveConfigDir2() {
4046
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join6(homedir3(), ".engrm");
4047
+ }
4048
+ function resolveSettingsPath2() {
4049
+ return join6(resolveConfigDir2(), "settings.json");
4050
+ }
4051
+ function resolveDbPath2() {
4052
+ return join6(resolveConfigDir2(), "engrm.db");
4053
+ }
4054
+ function resolveAuthBackupPath2() {
4055
+ return join6(resolveConfigDir2(), "auth-backup.json");
4056
+ }
3982
4057
  function getDbPath2() {
3983
- return DB_PATH2;
4058
+ return resolveDbPath2();
3984
4059
  }
3985
4060
  function generateDeviceId2() {
3986
4061
  const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -4003,7 +4078,7 @@ function generateDeviceId2() {
4003
4078
  return `${host}-${suffix}`;
4004
4079
  }
4005
4080
  function createDefaultConfig2() {
4006
- return {
4081
+ const merged = {
4007
4082
  candengo_url: "",
4008
4083
  candengo_api_key: "",
4009
4084
  site_id: "",
@@ -4058,24 +4133,26 @@ function createDefaultConfig2() {
4058
4133
  },
4059
4134
  tool_profile: "full"
4060
4135
  };
4136
+ return merged;
4061
4137
  }
4062
4138
  function loadConfig2() {
4063
- if (!existsSync6(SETTINGS_PATH2)) {
4064
- throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
4139
+ const settingsPath = resolveSettingsPath2();
4140
+ if (!existsSync6(settingsPath)) {
4141
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
4065
4142
  }
4066
- const raw = readFileSync6(SETTINGS_PATH2, "utf-8");
4143
+ const raw = readFileSync6(settingsPath, "utf-8");
4067
4144
  let parsed;
4068
4145
  try {
4069
4146
  parsed = JSON.parse(raw);
4070
4147
  } catch {
4071
- throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
4148
+ throw new Error(`Invalid JSON in ${settingsPath}`);
4072
4149
  }
4073
4150
  if (typeof parsed !== "object" || parsed === null) {
4074
- throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
4151
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
4075
4152
  }
4076
4153
  const config = parsed;
4077
4154
  const defaults = createDefaultConfig2();
4078
- return {
4155
+ const merged = {
4079
4156
  candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
4080
4157
  candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
4081
4158
  site_id: asString2(config["site_id"], defaults.site_id),
@@ -4130,16 +4207,27 @@ function loadConfig2() {
4130
4207
  },
4131
4208
  tool_profile: asToolProfile2(config["tool_profile"], defaults.tool_profile)
4132
4209
  };
4210
+ if (looksLikePlaceholderAuth2(merged)) {
4211
+ return restoreAuthBackup2(merged) ?? merged;
4212
+ }
4213
+ return merged;
4133
4214
  }
4134
4215
  function saveConfig2(config) {
4135
- if (!existsSync6(CONFIG_DIR2)) {
4136
- mkdirSync3(CONFIG_DIR2, { recursive: true });
4216
+ const configDir = resolveConfigDir2();
4217
+ const settingsPath = resolveSettingsPath2();
4218
+ const authBackupPath = resolveAuthBackupPath2();
4219
+ if (!existsSync6(configDir)) {
4220
+ mkdirSync3(configDir, { recursive: true });
4137
4221
  }
4138
- writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
4222
+ writeFileSync3(settingsPath, JSON.stringify(config, null, 2) + `
4223
+ `, "utf-8");
4224
+ if (!looksLikePlaceholderAuth2(config)) {
4225
+ writeFileSync3(authBackupPath, JSON.stringify(extractAuthBackup2(config), null, 2) + `
4139
4226
  `, "utf-8");
4227
+ }
4140
4228
  }
4141
4229
  function configExists2() {
4142
- return existsSync6(SETTINGS_PATH2);
4230
+ return existsSync6(resolveSettingsPath2());
4143
4231
  }
4144
4232
  function asString2(value, fallback) {
4145
4233
  return typeof value === "string" ? value : fallback;
@@ -4193,6 +4281,50 @@ function asTeams2(value, fallback) {
4193
4281
  return fallback;
4194
4282
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
4195
4283
  }
4284
+ function looksLikePlaceholderAuth2(config) {
4285
+ const apiKey = config.candengo_api_key.trim();
4286
+ const siteId = config.site_id.trim();
4287
+ const namespace = config.namespace.trim();
4288
+ const email = config.user_email.trim().toLowerCase();
4289
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
4290
+ return true;
4291
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
4292
+ return true;
4293
+ return false;
4294
+ }
4295
+ function extractAuthBackup2(config) {
4296
+ return {
4297
+ candengo_url: config.candengo_url,
4298
+ candengo_api_key: config.candengo_api_key,
4299
+ site_id: config.site_id,
4300
+ namespace: config.namespace,
4301
+ user_id: config.user_id,
4302
+ user_email: config.user_email,
4303
+ teams: config.teams
4304
+ };
4305
+ }
4306
+ function restoreAuthBackup2(config) {
4307
+ const authBackupPath = resolveAuthBackupPath2();
4308
+ if (!existsSync6(authBackupPath))
4309
+ return null;
4310
+ try {
4311
+ const raw = readFileSync6(authBackupPath, "utf-8");
4312
+ const parsed = JSON.parse(raw);
4313
+ const restored = {
4314
+ ...config,
4315
+ candengo_url: asString2(parsed["candengo_url"], config.candengo_url),
4316
+ candengo_api_key: asString2(parsed["candengo_api_key"], config.candengo_api_key),
4317
+ site_id: asString2(parsed["site_id"], config.site_id),
4318
+ namespace: asString2(parsed["namespace"], config.namespace),
4319
+ user_id: asString2(parsed["user_id"], config.user_id),
4320
+ user_email: asString2(parsed["user_email"], config.user_email),
4321
+ teams: asTeams2(parsed["teams"], config.teams)
4322
+ };
4323
+ return looksLikePlaceholderAuth2(restored) ? null : restored;
4324
+ } catch {
4325
+ return null;
4326
+ }
4327
+ }
4196
4328
 
4197
4329
  // src/tool-profiles.ts
4198
4330
  var MEMORY_PROFILE_TOOLS = [
@@ -4291,13 +4423,8 @@ function getCaptureStatus(db, input = {}) {
4291
4423
  FROM sessions s
4292
4424
  WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
4293
4425
  ${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;
4426
+ AND s.tool_calls_count > 0
4427
+ AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)`).get(...params)?.count ?? 0;
4301
4428
  const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
4302
4429
  WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
4303
4430
  ORDER BY created_at_epoch DESC, prompt_number DESC
@@ -4478,6 +4605,10 @@ function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
4478
4605
 
4479
4606
  // src/sync/auth.ts
4480
4607
  var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
4608
+ var PLACEHOLDER_API_KEYS = new Set(["cvk_org"]);
4609
+ var PLACEHOLDER_SITE_IDS = new Set(["site-1"]);
4610
+ var PLACEHOLDER_NAMESPACES = new Set(["org-ns", "fleet-ns"]);
4611
+ var PLACEHOLDER_EMAIL_SUFFIXES = ["@example.com"];
4481
4612
  function normalizeBaseUrl(url) {
4482
4613
  const trimmed = url.trim();
4483
4614
  if (!trimmed)
@@ -4496,7 +4627,7 @@ function getApiKey(config) {
4496
4627
  const envKey = process.env.ENGRM_TOKEN;
4497
4628
  if (envKey && envKey.startsWith("cvk_"))
4498
4629
  return envKey;
4499
- if (config.candengo_api_key && config.candengo_api_key.length > 0) {
4630
+ if (config.candengo_api_key && config.candengo_api_key.length > 0 && !looksLikePlaceholderConfig(config)) {
4500
4631
  return config.candengo_api_key;
4501
4632
  }
4502
4633
  return null;
@@ -4517,18 +4648,37 @@ ${apiKey}
4517
4648
  ${config.namespace}
4518
4649
  ${config.site_id}`).digest("hex");
4519
4650
  }
4520
- function recoverOutboxAfterSuccessfulAuth(db, config) {
4651
+ function looksLikePlaceholderConfig(config) {
4652
+ const apiKey = config.candengo_api_key?.trim() ?? "";
4653
+ const siteId = config.site_id?.trim() ?? "";
4654
+ const namespace = config.namespace?.trim() ?? "";
4655
+ const email = config.user_email?.trim().toLowerCase() ?? "";
4656
+ if (PLACEHOLDER_API_KEYS.has(apiKey) && PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace)) {
4657
+ return true;
4658
+ }
4659
+ if (PLACEHOLDER_SITE_IDS.has(siteId) && PLACEHOLDER_NAMESPACES.has(namespace) && PLACEHOLDER_EMAIL_SUFFIXES.some((suffix) => email.endsWith(suffix))) {
4660
+ return true;
4661
+ }
4662
+ return false;
4663
+ }
4664
+ function clearSyncPushBlock(db) {
4665
+ db.setSyncState("sync_push_blocked_until", "0");
4666
+ db.setSyncState("sync_push_block_reason", "");
4667
+ }
4668
+ function resumeOutboxAfterValidatedAuth(db, config) {
4521
4669
  const fingerprint = getAuthFingerprint(config);
4522
- const staleSyncingReset = resetStaleSyncingEntries(db);
4523
- const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure2(error) === "auth");
4670
+ const failedReset = resetFailedEntries(db);
4671
+ const syncingReset = resetSyncingEntries(db);
4672
+ const staleSyncingReset = 0;
4524
4673
  if (fingerprint) {
4525
4674
  db.setSyncState("sync_auth_fingerprint", fingerprint);
4526
4675
  }
4676
+ clearSyncPushBlock(db);
4527
4677
  return {
4528
4678
  fingerprintChanged: false,
4529
- failedReset: 0,
4530
- authFailedReset,
4531
- syncingReset: 0,
4679
+ failedReset,
4680
+ authFailedReset: 0,
4681
+ syncingReset,
4532
4682
  staleSyncingReset
4533
4683
  };
4534
4684
  }
@@ -4552,6 +4702,9 @@ switch (command) {
4552
4702
  case "update":
4553
4703
  handleUpdate();
4554
4704
  break;
4705
+ case "sync":
4706
+ await handleSync(args.slice(1));
4707
+ break;
4555
4708
  case "install-pack":
4556
4709
  await handleInstallPack(args.slice(1));
4557
4710
  break;
@@ -4710,31 +4863,7 @@ Authorization failed: ${error instanceof Error ? error.message : String(error)}`
4710
4863
  }
4711
4864
  function writeConfigFromProvision(baseUrl, result) {
4712
4865
  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
- }
4866
+ const preserved = loadPreservedLocalConfig();
4738
4867
  const config = {
4739
4868
  candengo_url: baseUrl,
4740
4869
  candengo_api_key: result.api_key,
@@ -4742,24 +4871,24 @@ function writeConfigFromProvision(baseUrl, result) {
4742
4871
  namespace: result.namespace,
4743
4872
  user_id: result.user_id,
4744
4873
  user_email: result.user_email,
4745
- device_id: existingDeviceId || generateDeviceId3(),
4874
+ device_id: preserved.device_id || generateDeviceId3(),
4746
4875
  teams: result.teams ?? [],
4747
- sync: existingSync ?? {
4876
+ sync: preserved.sync ?? {
4748
4877
  enabled: true,
4749
4878
  interval_seconds: 30,
4750
4879
  batch_size: 50
4751
4880
  },
4752
- search: existingSearch ?? {
4881
+ search: preserved.search ?? {
4753
4882
  default_limit: 10,
4754
4883
  local_boost: 1.2,
4755
4884
  scope: "all"
4756
4885
  },
4757
- scrubbing: existingScrubbing ?? {
4886
+ scrubbing: preserved.scrubbing ?? {
4758
4887
  enabled: true,
4759
4888
  custom_patterns: [],
4760
4889
  default_sensitivity: "shared"
4761
4890
  },
4762
- sentinel: existingSentinel ?? {
4891
+ sentinel: preserved.sentinel ?? {
4763
4892
  enabled: false,
4764
4893
  mode: "advisory",
4765
4894
  provider: "openai",
@@ -4770,33 +4899,54 @@ function writeConfigFromProvision(baseUrl, result) {
4770
4899
  daily_limit: 100,
4771
4900
  tier: "free"
4772
4901
  },
4773
- observer: existingObserver ?? {
4902
+ observer: preserved.observer ?? {
4774
4903
  enabled: true,
4775
4904
  mode: "per_event",
4776
4905
  model: "haiku"
4777
4906
  },
4778
- transcript_analysis: existingTranscriptAnalysis ?? {
4907
+ transcript_analysis: preserved.transcript_analysis ?? {
4779
4908
  enabled: false
4780
4909
  },
4781
- http: existingHttp ?? {
4910
+ http: preserved.http ?? {
4782
4911
  enabled: false,
4783
4912
  port: 3767,
4784
4913
  bearer_tokens: []
4785
4914
  },
4786
- fleet: existingFleet ?? {
4915
+ fleet: preserved.fleet ?? {
4787
4916
  project_name: "shared-experience",
4788
4917
  namespace: "",
4789
4918
  api_key: ""
4790
4919
  },
4791
- tool_profile: existingToolProfile ?? "full"
4920
+ tool_profile: preserved.tool_profile ?? "full"
4792
4921
  };
4793
4922
  saveConfig(config);
4794
4923
  const db = new MemDatabase(getDbPath());
4795
- recoverOutboxAfterSuccessfulAuth(db, config);
4924
+ resumeOutboxAfterValidatedAuth(db, config);
4796
4925
  db.close();
4797
4926
  console.log(`Configuration saved to ${getSettingsPath()}`);
4798
4927
  console.log(`Database initialised at ${getDbPath()}`);
4799
4928
  }
4929
+ function loadPreservedLocalConfig() {
4930
+ if (!configExists())
4931
+ return {};
4932
+ try {
4933
+ const existing = loadConfig();
4934
+ return {
4935
+ device_id: existing.device_id,
4936
+ sync: existing.sync,
4937
+ search: existing.search,
4938
+ scrubbing: existing.scrubbing,
4939
+ sentinel: existing.sentinel,
4940
+ observer: existing.observer,
4941
+ transcript_analysis: existing.transcript_analysis,
4942
+ http: existing.http,
4943
+ fleet: existing.fleet,
4944
+ tool_profile: existing.tool_profile
4945
+ };
4946
+ } catch {
4947
+ return {};
4948
+ }
4949
+ }
4800
4950
  function initFromFile(configPath) {
4801
4951
  if (!existsSync8(configPath)) {
4802
4952
  console.error(`Config file not found: ${configPath}`);
@@ -4829,6 +4979,7 @@ function initFromFile(configPath) {
4829
4979
  }
4830
4980
  }
4831
4981
  ensureConfigDir();
4982
+ const preserved = loadPreservedLocalConfig();
4832
4983
  const config = {
4833
4984
  candengo_url: input["candengo_url"].trim(),
4834
4985
  candengo_api_key: input["candengo_api_key"].trim(),
@@ -4836,24 +4987,24 @@ function initFromFile(configPath) {
4836
4987
  namespace: input["namespace"].trim(),
4837
4988
  user_id: input["user_id"].trim(),
4838
4989
  user_email: typeof input["user_email"] === "string" ? input["user_email"].trim() : "",
4839
- device_id: typeof input["device_id"] === "string" ? input["device_id"] : generateDeviceId3(),
4990
+ device_id: typeof input["device_id"] === "string" ? input["device_id"] : preserved.device_id ?? generateDeviceId3(),
4840
4991
  teams: [],
4841
- sync: {
4992
+ sync: preserved.sync ?? {
4842
4993
  enabled: true,
4843
4994
  interval_seconds: 30,
4844
4995
  batch_size: 50
4845
4996
  },
4846
- search: {
4997
+ search: preserved.search ?? {
4847
4998
  default_limit: 10,
4848
4999
  local_boost: 1.2,
4849
5000
  scope: "all"
4850
5001
  },
4851
- scrubbing: {
5002
+ scrubbing: preserved.scrubbing ?? {
4852
5003
  enabled: true,
4853
5004
  custom_patterns: [],
4854
5005
  default_sensitivity: "shared"
4855
5006
  },
4856
- sentinel: {
5007
+ sentinel: preserved.sentinel ?? {
4857
5008
  enabled: false,
4858
5009
  mode: "advisory",
4859
5010
  provider: "openai",
@@ -4864,14 +5015,25 @@ function initFromFile(configPath) {
4864
5015
  daily_limit: 100,
4865
5016
  tier: "free"
4866
5017
  },
4867
- observer: {
5018
+ observer: preserved.observer ?? {
4868
5019
  enabled: true,
4869
5020
  mode: "per_event",
4870
5021
  model: "haiku"
4871
5022
  },
4872
- transcript_analysis: {
5023
+ transcript_analysis: preserved.transcript_analysis ?? {
4873
5024
  enabled: false
4874
- }
5025
+ },
5026
+ http: preserved.http ?? {
5027
+ enabled: false,
5028
+ port: 3767,
5029
+ bearer_tokens: []
5030
+ },
5031
+ fleet: preserved.fleet ?? {
5032
+ project_name: "shared-experience",
5033
+ namespace: "",
5034
+ api_key: ""
5035
+ },
5036
+ tool_profile: preserved.tool_profile ?? "full"
4875
5037
  };
4876
5038
  saveConfig(config);
4877
5039
  const db = new MemDatabase(getDbPath());
@@ -4902,6 +5064,7 @@ async function initManual() {
4902
5064
  process.exit(1);
4903
5065
  }
4904
5066
  ensureConfigDir();
5067
+ const preserved = loadPreservedLocalConfig();
4905
5068
  const config = {
4906
5069
  candengo_url: candengoUrl.trim(),
4907
5070
  candengo_api_key: apiKey.trim(),
@@ -4909,24 +5072,24 @@ async function initManual() {
4909
5072
  namespace: namespace.trim(),
4910
5073
  user_id: userId.trim(),
4911
5074
  user_email: userEmail.trim(),
4912
- device_id: generateDeviceId3(),
5075
+ device_id: preserved.device_id ?? generateDeviceId3(),
4913
5076
  teams: [],
4914
- sync: {
5077
+ sync: preserved.sync ?? {
4915
5078
  enabled: true,
4916
5079
  interval_seconds: 30,
4917
5080
  batch_size: 50
4918
5081
  },
4919
- search: {
5082
+ search: preserved.search ?? {
4920
5083
  default_limit: 10,
4921
5084
  local_boost: 1.2,
4922
5085
  scope: "all"
4923
5086
  },
4924
- scrubbing: {
5087
+ scrubbing: preserved.scrubbing ?? {
4925
5088
  enabled: true,
4926
5089
  custom_patterns: [],
4927
5090
  default_sensitivity: "shared"
4928
5091
  },
4929
- sentinel: {
5092
+ sentinel: preserved.sentinel ?? {
4930
5093
  enabled: false,
4931
5094
  mode: "advisory",
4932
5095
  provider: "openai",
@@ -4937,14 +5100,25 @@ async function initManual() {
4937
5100
  daily_limit: 100,
4938
5101
  tier: "free"
4939
5102
  },
4940
- observer: {
5103
+ observer: preserved.observer ?? {
4941
5104
  enabled: true,
4942
5105
  mode: "per_event",
4943
5106
  model: "haiku"
4944
5107
  },
4945
- transcript_analysis: {
5108
+ transcript_analysis: preserved.transcript_analysis ?? {
4946
5109
  enabled: false
4947
- }
5110
+ },
5111
+ http: preserved.http ?? {
5112
+ enabled: false,
5113
+ port: 3767,
5114
+ bearer_tokens: []
5115
+ },
5116
+ fleet: preserved.fleet ?? {
5117
+ project_name: "shared-experience",
5118
+ namespace: "",
5119
+ api_key: ""
5120
+ },
5121
+ tool_profile: preserved.tool_profile ?? "full"
4948
5122
  };
4949
5123
  saveConfig(config);
4950
5124
  const db = new MemDatabase(getDbPath());
@@ -5129,6 +5303,15 @@ function handleStatus() {
5129
5303
  console.log(`
5130
5304
  Sync`);
5131
5305
  console.log(` Outbox: ${outbox["pending"] ?? 0} pending, ${outbox["failed"] ?? 0} failed, ${outbox["synced"] ?? 0} synced`);
5306
+ const syncBlock = getSyncBlockState(db);
5307
+ if (syncBlock.active) {
5308
+ console.log(` Push block: ${formatSyncBlock(syncBlock)}`);
5309
+ if (syncBlock.reason === "auth") {
5310
+ console.log(" Next step: Re-run `engrm init`, then `engrm sync resume`");
5311
+ } else if (syncBlock.reason === "rate_limit") {
5312
+ console.log(" Next step: Wait for the block to expire or retry later");
5313
+ }
5314
+ }
5132
5315
  const topFailures = getOutboxFailureSummaries(db, 2);
5133
5316
  if (topFailures.length > 0) {
5134
5317
  const failureSummary = topFailures.map((row) => `${classifyOutboxFailure(row.error)} ${row.count}`).join(", ");
@@ -5183,6 +5366,18 @@ function formatTimeAgo(epoch) {
5183
5366
  return `${Math.floor(ago / 3600)}h ago`;
5184
5367
  return `${Math.floor(ago / 86400)}d ago`;
5185
5368
  }
5369
+ function formatTimeUntil(epoch) {
5370
+ const remaining = epoch - Math.floor(Date.now() / 1000);
5371
+ if (remaining <= 0)
5372
+ return "now";
5373
+ if (remaining < 60)
5374
+ return `in ${remaining}s`;
5375
+ if (remaining < 3600)
5376
+ return `in ${Math.floor(remaining / 60)}m`;
5377
+ if (remaining < 86400)
5378
+ return `in ${Math.floor(remaining / 3600)}h`;
5379
+ return `in ${Math.floor(remaining / 86400)}d`;
5380
+ }
5186
5381
  function formatSyncTime(epochStr) {
5187
5382
  if (!epochStr)
5188
5383
  return "never";
@@ -5191,6 +5386,25 @@ function formatSyncTime(epochStr) {
5191
5386
  return "never";
5192
5387
  return formatTimeAgo(epoch);
5193
5388
  }
5389
+ function getSyncBlockState(db) {
5390
+ const untilRaw = db.getSyncState("sync_push_blocked_until");
5391
+ const reason = db.getSyncState("sync_push_block_reason");
5392
+ const untilEpoch = untilRaw ? parseInt(untilRaw, 10) : NaN;
5393
+ if (!Number.isFinite(untilEpoch) || untilEpoch <= Math.floor(Date.now() / 1000)) {
5394
+ return { active: false, untilEpoch: null, reason: null };
5395
+ }
5396
+ return {
5397
+ active: true,
5398
+ untilEpoch,
5399
+ reason: reason && reason.length > 0 ? reason : null
5400
+ };
5401
+ }
5402
+ function formatSyncBlock(block) {
5403
+ const reasonLabel = block.reason === "auth" ? "waiting for re-auth" : block.reason === "rate_limit" ? "rate limited" : block.reason ?? "paused";
5404
+ if (!block.untilEpoch)
5405
+ return reasonLabel;
5406
+ return `${reasonLabel} until ${new Date(block.untilEpoch * 1000).toISOString()} (${formatTimeUntil(block.untilEpoch)})`;
5407
+ }
5194
5408
  function ensureConfigDir() {
5195
5409
  const dir = getConfigDir();
5196
5410
  if (!existsSync8(dir)) {
@@ -5322,6 +5536,56 @@ Restart Claude Code or Codex to use the new version.`);
5322
5536
  console.error("Try manually: npm install -g engrm@<version>");
5323
5537
  }
5324
5538
  }
5539
+ async function handleSync(flags) {
5540
+ const subcommand = flags[0];
5541
+ if (subcommand === "resume") {
5542
+ if (!configExists()) {
5543
+ console.error("Engrm is not configured. Run: engrm init");
5544
+ process.exit(1);
5545
+ }
5546
+ const config = loadConfig();
5547
+ if (!config.candengo_url || !config.candengo_api_key) {
5548
+ console.error("Authentication is not configured. Run: engrm init");
5549
+ process.exit(1);
5550
+ }
5551
+ try {
5552
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
5553
+ const controller = new AbortController;
5554
+ const timeout = setTimeout(() => controller.abort(), 5000);
5555
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
5556
+ headers: { Authorization: `Bearer ${config.candengo_api_key}` },
5557
+ signal: controller.signal
5558
+ });
5559
+ clearTimeout(timeout);
5560
+ if (!res.ok) {
5561
+ if (res.status === 401 || res.status === 403) {
5562
+ console.error("Sync resume blocked: authentication is still invalid. Re-run `engrm init` first.");
5563
+ } else {
5564
+ console.error(`Sync resume blocked: authentication check returned HTTP ${res.status}.`);
5565
+ }
5566
+ process.exit(1);
5567
+ }
5568
+ const db = new MemDatabase(getDbPath());
5569
+ try {
5570
+ const recovery = resumeOutboxAfterValidatedAuth(db, config);
5571
+ const pending = getOutboxStats(db)["pending"] ?? 0;
5572
+ console.log("Sync queue resumed.");
5573
+ console.log(` Failed reset: ${recovery.failedReset}`);
5574
+ console.log(` Syncing reset: ${recovery.syncingReset}`);
5575
+ console.log(` Pending now: ${pending}`);
5576
+ } finally {
5577
+ db.close();
5578
+ }
5579
+ return;
5580
+ } catch (error) {
5581
+ console.error(`Sync resume failed: ${error instanceof Error ? error.message : String(error)}`);
5582
+ process.exit(1);
5583
+ }
5584
+ }
5585
+ console.log(`Sync commands:
5586
+ `);
5587
+ console.log(" engrm sync resume Validate auth and unblock the paused sync queue");
5588
+ }
5325
5589
  async function handleDoctor() {
5326
5590
  const results = [];
5327
5591
  const pass = (msg) => results.push({ symbol: "\u2713", message: msg, kind: "pass" });
@@ -5547,26 +5811,30 @@ async function handleDoctor() {
5547
5811
  fail("Server URL not configured");
5548
5812
  }
5549
5813
  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}`);
5814
+ if (looksLikePlaceholderConfig(config)) {
5815
+ fail("Authentication is using placeholder credentials \u2014 re-run 'engrm init' to restore real account settings");
5816
+ } else {
5817
+ try {
5818
+ const baseUrl = normalizeBaseUrl(config.candengo_url);
5819
+ const controller = new AbortController;
5820
+ const timeout = setTimeout(() => controller.abort(), 5000);
5821
+ const res = await fetch(`${baseUrl}/v1/mem/user-settings`, {
5822
+ headers: { Authorization: `Bearer ${config.candengo_api_key}` },
5823
+ signal: controller.signal
5824
+ });
5825
+ clearTimeout(timeout);
5826
+ if (res.ok) {
5827
+ const email = config.user_email ?? "configured";
5828
+ pass(`Authentication valid (${email})`);
5829
+ } else if (res.status === 401 || res.status === 403) {
5830
+ fail("Authentication failed \u2014 API key may be expired");
5831
+ } else {
5832
+ fail(`Authentication check returned HTTP ${res.status}`);
5833
+ }
5834
+ } catch (err) {
5835
+ const msg = err instanceof Error ? err.message : String(err);
5836
+ fail(`Authentication check failed: ${msg.includes("abort") ? "timeout (5s)" : msg}`);
5566
5837
  }
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
5838
  }
5571
5839
  } else {
5572
5840
  fail("Authentication not configured (missing URL or API key)");
@@ -5574,7 +5842,16 @@ async function handleDoctor() {
5574
5842
  try {
5575
5843
  const outbox = getOutboxStats(db);
5576
5844
  const failedCount = outbox["failed"] ?? 0;
5577
- if (failedCount > 10) {
5845
+ const syncBlock = getSyncBlockState(db);
5846
+ if (syncBlock.active) {
5847
+ const pending = outbox["pending"] ?? 0;
5848
+ warn(`Sync push is paused (${formatSyncBlock(syncBlock)}; ${pending} pending, ${failedCount} failed)`);
5849
+ if (syncBlock.reason === "auth") {
5850
+ info("Next step: re-run `engrm init`, then `engrm sync resume`");
5851
+ } else if (syncBlock.reason === "rate_limit") {
5852
+ info("Next step: wait for the pause window to expire, then try again");
5853
+ }
5854
+ } else if (failedCount > 10) {
5578
5855
  warn(`Sync has stuck items (${failedCount} failed in outbox)`);
5579
5856
  } else {
5580
5857
  const pending = outbox["pending"] ?? 0;
@@ -5802,6 +6079,7 @@ function printUsage() {
5802
6079
  console.log(" engrm init --config <file> Setup from JSON file");
5803
6080
  console.log(" engrm status Show status");
5804
6081
  console.log(" engrm update Update to latest version");
6082
+ console.log(" engrm sync resume Validate auth and resume paused sync queue");
5805
6083
  console.log(" engrm packs List available starter packs");
5806
6084
  console.log(" engrm install-pack <name> Install a starter pack");
5807
6085
  console.log(" engrm doctor Run diagnostic checks");