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.
@@ -7,11 +7,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { createHash } from "node:crypto";
10
- var CONFIG_DIR = join(homedir(), ".engrm");
11
- var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
12
- var DB_PATH = join(CONFIG_DIR, "engrm.db");
10
+ function resolveConfigDir() {
11
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
12
+ }
13
+ function resolveSettingsPath() {
14
+ return join(resolveConfigDir(), "settings.json");
15
+ }
16
+ function resolveDbPath() {
17
+ return join(resolveConfigDir(), "engrm.db");
18
+ }
19
+ function resolveAuthBackupPath() {
20
+ return join(resolveConfigDir(), "auth-backup.json");
21
+ }
13
22
  function getDbPath() {
14
- return DB_PATH;
23
+ return resolveDbPath();
15
24
  }
16
25
  function generateDeviceId() {
17
26
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -34,7 +43,7 @@ function generateDeviceId() {
34
43
  return `${host}-${suffix}`;
35
44
  }
36
45
  function createDefaultConfig() {
37
- return {
46
+ const merged = {
38
47
  candengo_url: "",
39
48
  candengo_api_key: "",
40
49
  site_id: "",
@@ -89,24 +98,26 @@ function createDefaultConfig() {
89
98
  },
90
99
  tool_profile: "full"
91
100
  };
101
+ return merged;
92
102
  }
93
103
  function loadConfig() {
94
- if (!existsSync(SETTINGS_PATH)) {
95
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
104
+ const settingsPath = resolveSettingsPath();
105
+ if (!existsSync(settingsPath)) {
106
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
96
107
  }
97
- const raw = readFileSync(SETTINGS_PATH, "utf-8");
108
+ const raw = readFileSync(settingsPath, "utf-8");
98
109
  let parsed;
99
110
  try {
100
111
  parsed = JSON.parse(raw);
101
112
  } catch {
102
- throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
113
+ throw new Error(`Invalid JSON in ${settingsPath}`);
103
114
  }
104
115
  if (typeof parsed !== "object" || parsed === null) {
105
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
116
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
106
117
  }
107
118
  const config = parsed;
108
119
  const defaults = createDefaultConfig();
109
- return {
120
+ const merged = {
110
121
  candengo_url: asString(config["candengo_url"], defaults.candengo_url),
111
122
  candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
112
123
  site_id: asString(config["site_id"], defaults.site_id),
@@ -161,16 +172,27 @@ function loadConfig() {
161
172
  },
162
173
  tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
163
174
  };
175
+ if (looksLikePlaceholderAuth(merged)) {
176
+ return restoreAuthBackup(merged) ?? merged;
177
+ }
178
+ return merged;
164
179
  }
165
180
  function saveConfig(config) {
166
- if (!existsSync(CONFIG_DIR)) {
167
- mkdirSync(CONFIG_DIR, { recursive: true });
181
+ const configDir = resolveConfigDir();
182
+ const settingsPath = resolveSettingsPath();
183
+ const authBackupPath = resolveAuthBackupPath();
184
+ if (!existsSync(configDir)) {
185
+ mkdirSync(configDir, { recursive: true });
168
186
  }
169
- writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
187
+ writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
170
188
  `, "utf-8");
189
+ if (!looksLikePlaceholderAuth(config)) {
190
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
191
+ `, "utf-8");
192
+ }
171
193
  }
172
194
  function configExists() {
173
- return existsSync(SETTINGS_PATH);
195
+ return existsSync(resolveSettingsPath());
174
196
  }
175
197
  function asString(value, fallback) {
176
198
  return typeof value === "string" ? value : fallback;
@@ -224,6 +246,50 @@ function asTeams(value, fallback) {
224
246
  return fallback;
225
247
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
226
248
  }
249
+ function looksLikePlaceholderAuth(config) {
250
+ const apiKey = config.candengo_api_key.trim();
251
+ const siteId = config.site_id.trim();
252
+ const namespace = config.namespace.trim();
253
+ const email = config.user_email.trim().toLowerCase();
254
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
255
+ return true;
256
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
257
+ return true;
258
+ return false;
259
+ }
260
+ function extractAuthBackup(config) {
261
+ return {
262
+ candengo_url: config.candengo_url,
263
+ candengo_api_key: config.candengo_api_key,
264
+ site_id: config.site_id,
265
+ namespace: config.namespace,
266
+ user_id: config.user_id,
267
+ user_email: config.user_email,
268
+ teams: config.teams
269
+ };
270
+ }
271
+ function restoreAuthBackup(config) {
272
+ const authBackupPath = resolveAuthBackupPath();
273
+ if (!existsSync(authBackupPath))
274
+ return null;
275
+ try {
276
+ const raw = readFileSync(authBackupPath, "utf-8");
277
+ const parsed = JSON.parse(raw);
278
+ const restored = {
279
+ ...config,
280
+ candengo_url: asString(parsed["candengo_url"], config.candengo_url),
281
+ candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
282
+ site_id: asString(parsed["site_id"], config.site_id),
283
+ namespace: asString(parsed["namespace"], config.namespace),
284
+ user_id: asString(parsed["user_id"], config.user_id),
285
+ user_email: asString(parsed["user_email"], config.user_email),
286
+ teams: asTeams(parsed["teams"], config.teams)
287
+ };
288
+ return looksLikePlaceholderAuth(restored) ? null : restored;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
227
293
 
228
294
  // src/storage/migrations.ts
229
295
  var MIGRATIONS = [
@@ -911,6 +977,20 @@ function ensureChatMessageColumns(db) {
911
977
  db.exec("PRAGMA user_version = 17");
912
978
  }
913
979
  }
980
+ function ensureObservationVectorTable(db) {
981
+ if (!isVecExtensionLoaded(db))
982
+ return;
983
+ db.exec(`
984
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
985
+ observation_id INTEGER PRIMARY KEY,
986
+ embedding FLOAT[384]
987
+ );
988
+ `);
989
+ const current = getSchemaVersion(db);
990
+ if (current < 4) {
991
+ db.exec("PRAGMA user_version = 4");
992
+ }
993
+ }
914
994
  function ensureChatVectorTable(db) {
915
995
  if (!isVecExtensionLoaded(db))
916
996
  return;
@@ -1139,6 +1219,7 @@ class MemDatabase {
1139
1219
  ensureObservationTypes(this.db);
1140
1220
  ensureSessionSummaryColumns(this.db);
1141
1221
  ensureChatMessageColumns(this.db);
1222
+ ensureObservationVectorTable(this.db);
1142
1223
  ensureChatVectorTable(this.db);
1143
1224
  ensureSyncOutboxSupportsChatMessages(this.db);
1144
1225
  }
@@ -4114,14 +4195,18 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
4114
4195
 
4115
4196
  // src/capture/transcript.ts
4116
4197
  import { createHash as createHash3 } from "node:crypto";
4117
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
4198
+ import { readFileSync as readFileSync3, existsSync as existsSync3, statSync, readdirSync } from "node:fs";
4118
4199
  import { join as join3 } from "node:path";
4119
4200
  import { homedir as homedir2 } from "node:os";
4120
4201
  function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
4121
4202
  if (transcriptPath)
4122
4203
  return transcriptPath;
4123
4204
  const encodedCwd = cwd.replace(/\//g, "-");
4124
- return join3(homedir2(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
4205
+ const directPath = join3(homedir2(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
4206
+ if (existsSync3(directPath))
4207
+ return directPath;
4208
+ const discovered = findTranscriptPathBySessionId(sessionId);
4209
+ return discovered ?? directPath;
4125
4210
  }
4126
4211
  function readTranscript(sessionId, cwd, transcriptPath) {
4127
4212
  const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
@@ -4144,10 +4229,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
4144
4229
  } catch {
4145
4230
  continue;
4146
4231
  }
4147
- const role = entry.role;
4232
+ const role = getTranscriptRole(entry);
4148
4233
  if (role !== "user" && role !== "assistant")
4149
4234
  continue;
4150
- const content = entry.content;
4235
+ const content = getTranscriptContent(entry);
4151
4236
  if (typeof content === "string") {
4152
4237
  messages.push({ role, text: content });
4153
4238
  continue;
@@ -4167,6 +4252,66 @@ function readTranscript(sessionId, cwd, transcriptPath) {
4167
4252
  }
4168
4253
  return messages;
4169
4254
  }
4255
+ function readTranscriptToolEvents(sessionId, cwd, transcriptPath) {
4256
+ const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
4257
+ if (!existsSync3(path))
4258
+ return [];
4259
+ let raw;
4260
+ try {
4261
+ raw = readFileSync3(path, "utf-8");
4262
+ } catch {
4263
+ return [];
4264
+ }
4265
+ const toolEvents = [];
4266
+ const toolEventIndexes = new Map;
4267
+ for (const line of raw.split(`
4268
+ `)) {
4269
+ if (!line.trim())
4270
+ continue;
4271
+ let entry;
4272
+ try {
4273
+ entry = JSON.parse(line);
4274
+ } catch {
4275
+ continue;
4276
+ }
4277
+ const createdAtEpoch = parseTranscriptTimestamp(entry);
4278
+ const content = getTranscriptContent(entry);
4279
+ if (!Array.isArray(content))
4280
+ continue;
4281
+ for (const block of content) {
4282
+ if (!block || typeof block !== "object")
4283
+ continue;
4284
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
4285
+ const preview = extractToolResultPreview(block.content);
4286
+ const index = toolEventIndexes.get(block.tool_use_id);
4287
+ if (preview && index !== undefined) {
4288
+ toolEvents[index] = {
4289
+ ...toolEvents[index],
4290
+ tool_response_preview: preview
4291
+ };
4292
+ }
4293
+ continue;
4294
+ }
4295
+ if (block.type !== "tool_use")
4296
+ continue;
4297
+ const input = block.input && typeof block.input === "object" ? block.input : {};
4298
+ const toolUseId = typeof block.id === "string" ? block.id : null;
4299
+ const nextEvent = {
4300
+ tool_name: typeof block.name === "string" ? block.name : "Unknown",
4301
+ tool_input_json: JSON.stringify(input),
4302
+ tool_response_preview: null,
4303
+ file_path: extractToolFilePath(input),
4304
+ command: typeof input.command === "string" ? input.command : null,
4305
+ created_at_epoch: createdAtEpoch
4306
+ };
4307
+ toolEvents.push(nextEvent);
4308
+ if (toolUseId) {
4309
+ toolEventIndexes.set(toolUseId, toolEvents.length - 1);
4310
+ }
4311
+ }
4312
+ }
4313
+ return toolEvents;
4314
+ }
4170
4315
  function resolveHistoryPath(historyPath) {
4171
4316
  if (historyPath)
4172
4317
  return historyPath;
@@ -4232,9 +4377,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
4232
4377
  createdAtEpoch: entry.timestamp
4233
4378
  })));
4234
4379
  }
4235
- async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4380
+ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath, options = {}) {
4381
+ const embed = options.embed ?? true;
4236
4382
  const session = db.getSessionById(sessionId);
4237
- const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
4383
+ const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
4384
+ const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
4385
+ if (existsSync3(resolvedTranscriptPath)) {
4386
+ try {
4387
+ const stat = statSync(resolvedTranscriptPath);
4388
+ const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
4389
+ if (db.getSyncState(syncCursorKey) === cursor) {
4390
+ return { imported: 0, total: 0 };
4391
+ }
4392
+ db.setSyncState(syncCursorKey, cursor);
4393
+ } catch {}
4394
+ }
4395
+ const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
4238
4396
  ...message,
4239
4397
  text: message.text.trim()
4240
4398
  })).filter((message) => message.text.length > 0);
@@ -4296,7 +4454,7 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4296
4454
  created_at_epoch: createdAtEpoch
4297
4455
  });
4298
4456
  }
4299
- if (db.vecAvailable) {
4457
+ if (embed && db.vecAvailable) {
4300
4458
  const embedding = await embedText(composeChatEmbeddingText(message.text));
4301
4459
  if (embedding) {
4302
4460
  db.vecChatInsert(row.id, embedding);
@@ -4306,6 +4464,35 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
4306
4464
  }
4307
4465
  return { imported, total: messages.length };
4308
4466
  }
4467
+ function syncTranscriptToolEvents(db, config, sessionId, cwd, transcriptPath) {
4468
+ const session = db.getSessionById(sessionId);
4469
+ if (!session)
4470
+ return { imported: 0, total: 0 };
4471
+ if (db.getSessionToolEvents(sessionId, 1).length > 0) {
4472
+ return { imported: 0, total: 0 };
4473
+ }
4474
+ const toolEvents = readTranscriptToolEvents(sessionId, cwd, transcriptPath);
4475
+ if (toolEvents.length === 0)
4476
+ return { imported: 0, total: 0 };
4477
+ let imported = 0;
4478
+ for (const event of toolEvents) {
4479
+ db.insertToolEvent({
4480
+ session_id: sessionId,
4481
+ project_id: session.project_id,
4482
+ tool_name: event.tool_name,
4483
+ tool_input_json: event.tool_input_json,
4484
+ tool_response_preview: event.tool_response_preview,
4485
+ file_path: event.file_path,
4486
+ command: event.command,
4487
+ user_id: config.user_id,
4488
+ device_id: config.device_id,
4489
+ agent: "claude-code",
4490
+ created_at_epoch: event.created_at_epoch ?? undefined
4491
+ });
4492
+ imported++;
4493
+ }
4494
+ return { imported, total: toolEvents.length };
4495
+ }
4309
4496
  function dedupeHistoryMessages(messages) {
4310
4497
  const deduped = [];
4311
4498
  for (const message of messages) {
@@ -4323,6 +4510,59 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
4323
4510
  const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
4324
4511
  return `history:${sessionId}:${createdAtEpoch}:${digest}`;
4325
4512
  }
4513
+ function getTranscriptRole(entry) {
4514
+ return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
4515
+ }
4516
+ function getTranscriptContent(entry) {
4517
+ return entry.content ?? entry.message?.content;
4518
+ }
4519
+ function parseTranscriptTimestamp(entry) {
4520
+ const raw = entry.timestamp ?? entry.message?.timestamp;
4521
+ if (typeof raw !== "string")
4522
+ return null;
4523
+ const epoch = Date.parse(raw);
4524
+ return Number.isFinite(epoch) ? Math.floor(epoch / 1000) : null;
4525
+ }
4526
+ function extractToolResultPreview(content) {
4527
+ if (typeof content === "string")
4528
+ return content.slice(0, 4000);
4529
+ if (Array.isArray(content)) {
4530
+ const text = content.map((item) => {
4531
+ if (typeof item === "string")
4532
+ return item;
4533
+ if (item && typeof item === "object" && typeof item.text === "string")
4534
+ return item.text;
4535
+ return "";
4536
+ }).filter(Boolean).join(`
4537
+ `);
4538
+ return text ? text.slice(0, 4000) : null;
4539
+ }
4540
+ return null;
4541
+ }
4542
+ function extractToolFilePath(input) {
4543
+ for (const key of ["file_path", "path", "target_file"]) {
4544
+ if (typeof input[key] === "string")
4545
+ return input[key];
4546
+ }
4547
+ return null;
4548
+ }
4549
+ function findTranscriptPathBySessionId(sessionId) {
4550
+ const projectsDir = join3(homedir2(), ".claude", "projects");
4551
+ if (!existsSync3(projectsDir))
4552
+ return null;
4553
+ try {
4554
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
4555
+ if (!entry.isDirectory())
4556
+ continue;
4557
+ const candidate = join3(projectsDir, entry.name, `${sessionId}.jsonl`);
4558
+ if (existsSync3(candidate))
4559
+ return candidate;
4560
+ }
4561
+ } catch {
4562
+ return null;
4563
+ }
4564
+ return null;
4565
+ }
4326
4566
  function truncateTranscript(messages, maxBytes = 50000) {
4327
4567
  const lines = [];
4328
4568
  for (const msg of messages) {
@@ -4452,7 +4692,8 @@ async function main() {
4452
4692
  try {
4453
4693
  let importedChat = 0;
4454
4694
  if (event.session_id) {
4455
- const chatSync = await syncTranscriptChat(db, config, event.session_id, event.cwd);
4695
+ syncTranscriptToolEvents(db, config, event.session_id, event.cwd);
4696
+ const chatSync = await syncTranscriptChat(db, config, event.session_id, event.cwd, undefined, { embed: false });
4456
4697
  importedChat = chatSync.imported;
4457
4698
  await upsertRollingHandoff(db, config, {
4458
4699
  session_id: event.session_id,
@@ -83,11 +83,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
83
83
  import { homedir, hostname, networkInterfaces } from "node:os";
84
84
  import { join } from "node:path";
85
85
  import { createHash } from "node:crypto";
86
- var CONFIG_DIR = join(homedir(), ".engrm");
87
- var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
88
- var DB_PATH = join(CONFIG_DIR, "engrm.db");
86
+ function resolveConfigDir() {
87
+ return process.env["ENGRM_CONFIG_DIR"]?.trim() || join(homedir(), ".engrm");
88
+ }
89
+ function resolveSettingsPath() {
90
+ return join(resolveConfigDir(), "settings.json");
91
+ }
92
+ function resolveDbPath() {
93
+ return join(resolveConfigDir(), "engrm.db");
94
+ }
95
+ function resolveAuthBackupPath() {
96
+ return join(resolveConfigDir(), "auth-backup.json");
97
+ }
89
98
  function getDbPath() {
90
- return DB_PATH;
99
+ return resolveDbPath();
91
100
  }
92
101
  function generateDeviceId() {
93
102
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
@@ -110,7 +119,7 @@ function generateDeviceId() {
110
119
  return `${host}-${suffix}`;
111
120
  }
112
121
  function createDefaultConfig() {
113
- return {
122
+ const merged = {
114
123
  candengo_url: "",
115
124
  candengo_api_key: "",
116
125
  site_id: "",
@@ -165,24 +174,26 @@ function createDefaultConfig() {
165
174
  },
166
175
  tool_profile: "full"
167
176
  };
177
+ return merged;
168
178
  }
169
179
  function loadConfig() {
170
- if (!existsSync(SETTINGS_PATH)) {
171
- throw new Error(`Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`);
180
+ const settingsPath = resolveSettingsPath();
181
+ if (!existsSync(settingsPath)) {
182
+ throw new Error(`Config not found at ${settingsPath}. Run 'engrm init --manual' to configure.`);
172
183
  }
173
- const raw = readFileSync(SETTINGS_PATH, "utf-8");
184
+ const raw = readFileSync(settingsPath, "utf-8");
174
185
  let parsed;
175
186
  try {
176
187
  parsed = JSON.parse(raw);
177
188
  } catch {
178
- throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
189
+ throw new Error(`Invalid JSON in ${settingsPath}`);
179
190
  }
180
191
  if (typeof parsed !== "object" || parsed === null) {
181
- throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
192
+ throw new Error(`Config at ${settingsPath} is not a JSON object`);
182
193
  }
183
194
  const config = parsed;
184
195
  const defaults = createDefaultConfig();
185
- return {
196
+ const merged = {
186
197
  candengo_url: asString(config["candengo_url"], defaults.candengo_url),
187
198
  candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
188
199
  site_id: asString(config["site_id"], defaults.site_id),
@@ -237,16 +248,27 @@ function loadConfig() {
237
248
  },
238
249
  tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
239
250
  };
251
+ if (looksLikePlaceholderAuth(merged)) {
252
+ return restoreAuthBackup(merged) ?? merged;
253
+ }
254
+ return merged;
240
255
  }
241
256
  function saveConfig(config) {
242
- if (!existsSync(CONFIG_DIR)) {
243
- mkdirSync(CONFIG_DIR, { recursive: true });
257
+ const configDir = resolveConfigDir();
258
+ const settingsPath = resolveSettingsPath();
259
+ const authBackupPath = resolveAuthBackupPath();
260
+ if (!existsSync(configDir)) {
261
+ mkdirSync(configDir, { recursive: true });
244
262
  }
245
- writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
263
+ writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
246
264
  `, "utf-8");
265
+ if (!looksLikePlaceholderAuth(config)) {
266
+ writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
267
+ `, "utf-8");
268
+ }
247
269
  }
248
270
  function configExists() {
249
- return existsSync(SETTINGS_PATH);
271
+ return existsSync(resolveSettingsPath());
250
272
  }
251
273
  function asString(value, fallback) {
252
274
  return typeof value === "string" ? value : fallback;
@@ -300,6 +322,50 @@ function asTeams(value, fallback) {
300
322
  return fallback;
301
323
  return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
302
324
  }
325
+ function looksLikePlaceholderAuth(config) {
326
+ const apiKey = config.candengo_api_key.trim();
327
+ const siteId = config.site_id.trim();
328
+ const namespace = config.namespace.trim();
329
+ const email = config.user_email.trim().toLowerCase();
330
+ if (apiKey === "cvk_org" && siteId === "site-1" && namespace === "org-ns")
331
+ return true;
332
+ if (siteId === "site-1" && namespace === "org-ns" && email.endsWith("@example.com"))
333
+ return true;
334
+ return false;
335
+ }
336
+ function extractAuthBackup(config) {
337
+ return {
338
+ candengo_url: config.candengo_url,
339
+ candengo_api_key: config.candengo_api_key,
340
+ site_id: config.site_id,
341
+ namespace: config.namespace,
342
+ user_id: config.user_id,
343
+ user_email: config.user_email,
344
+ teams: config.teams
345
+ };
346
+ }
347
+ function restoreAuthBackup(config) {
348
+ const authBackupPath = resolveAuthBackupPath();
349
+ if (!existsSync(authBackupPath))
350
+ return null;
351
+ try {
352
+ const raw = readFileSync(authBackupPath, "utf-8");
353
+ const parsed = JSON.parse(raw);
354
+ const restored = {
355
+ ...config,
356
+ candengo_url: asString(parsed["candengo_url"], config.candengo_url),
357
+ candengo_api_key: asString(parsed["candengo_api_key"], config.candengo_api_key),
358
+ site_id: asString(parsed["site_id"], config.site_id),
359
+ namespace: asString(parsed["namespace"], config.namespace),
360
+ user_id: asString(parsed["user_id"], config.user_id),
361
+ user_email: asString(parsed["user_email"], config.user_email),
362
+ teams: asTeams(parsed["teams"], config.teams)
363
+ };
364
+ return looksLikePlaceholderAuth(restored) ? null : restored;
365
+ } catch {
366
+ return null;
367
+ }
368
+ }
303
369
 
304
370
  // src/storage/migrations.ts
305
371
  var MIGRATIONS = [
@@ -987,6 +1053,20 @@ function ensureChatMessageColumns(db) {
987
1053
  db.exec("PRAGMA user_version = 17");
988
1054
  }
989
1055
  }
1056
+ function ensureObservationVectorTable(db) {
1057
+ if (!isVecExtensionLoaded(db))
1058
+ return;
1059
+ db.exec(`
1060
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1061
+ observation_id INTEGER PRIMARY KEY,
1062
+ embedding FLOAT[384]
1063
+ );
1064
+ `);
1065
+ const current = getSchemaVersion(db);
1066
+ if (current < 4) {
1067
+ db.exec("PRAGMA user_version = 4");
1068
+ }
1069
+ }
990
1070
  function ensureChatVectorTable(db) {
991
1071
  if (!isVecExtensionLoaded(db))
992
1072
  return;
@@ -1215,6 +1295,7 @@ class MemDatabase {
1215
1295
  ensureObservationTypes(this.db);
1216
1296
  ensureSessionSummaryColumns(this.db);
1217
1297
  ensureChatMessageColumns(this.db);
1298
+ ensureObservationVectorTable(this.db);
1218
1299
  ensureChatVectorTable(this.db);
1219
1300
  ensureSyncOutboxSupportsChatMessages(this.db);
1220
1301
  }