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 +396 -118
- package/dist/hooks/elicitation-result.js +81 -15
- package/dist/hooks/post-tool-use.js +250 -23
- package/dist/hooks/pre-compact.js +249 -23
- package/dist/hooks/sentinel.js +81 -15
- package/dist/hooks/session-start.js +105 -17
- package/dist/hooks/stop.js +311 -27
- package/dist/hooks/user-prompt-submit.js +81 -1521
- package/dist/server.js +193 -34
- package/package.json +1 -1
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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(
|
|
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 ${
|
|
113
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
103
114
|
}
|
|
104
115
|
if (typeof parsed !== "object" || parsed === null) {
|
|
105
|
-
throw new Error(`Config at ${
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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(
|
|
187
|
+
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
|
|
188
|
+
`, "utf-8");
|
|
189
|
+
if (!looksLikePlaceholderAuth(config)) {
|
|
190
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
|
|
170
191
|
`, "utf-8");
|
|
192
|
+
}
|
|
171
193
|
}
|
|
172
194
|
function configExists() {
|
|
173
|
-
return existsSync(
|
|
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 = [
|
|
@@ -4114,14 +4180,18 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
4114
4180
|
|
|
4115
4181
|
// src/capture/transcript.ts
|
|
4116
4182
|
import { createHash as createHash3 } from "node:crypto";
|
|
4117
|
-
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
4183
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, statSync, readdirSync } from "node:fs";
|
|
4118
4184
|
import { join as join3 } from "node:path";
|
|
4119
4185
|
import { homedir as homedir2 } from "node:os";
|
|
4120
4186
|
function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
|
|
4121
4187
|
if (transcriptPath)
|
|
4122
4188
|
return transcriptPath;
|
|
4123
4189
|
const encodedCwd = cwd.replace(/\//g, "-");
|
|
4124
|
-
|
|
4190
|
+
const directPath = join3(homedir2(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
4191
|
+
if (existsSync3(directPath))
|
|
4192
|
+
return directPath;
|
|
4193
|
+
const discovered = findTranscriptPathBySessionId(sessionId);
|
|
4194
|
+
return discovered ?? directPath;
|
|
4125
4195
|
}
|
|
4126
4196
|
function readTranscript(sessionId, cwd, transcriptPath) {
|
|
4127
4197
|
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
@@ -4144,10 +4214,10 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4144
4214
|
} catch {
|
|
4145
4215
|
continue;
|
|
4146
4216
|
}
|
|
4147
|
-
const role = entry
|
|
4217
|
+
const role = getTranscriptRole(entry);
|
|
4148
4218
|
if (role !== "user" && role !== "assistant")
|
|
4149
4219
|
continue;
|
|
4150
|
-
const content = entry
|
|
4220
|
+
const content = getTranscriptContent(entry);
|
|
4151
4221
|
if (typeof content === "string") {
|
|
4152
4222
|
messages.push({ role, text: content });
|
|
4153
4223
|
continue;
|
|
@@ -4167,6 +4237,66 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
4167
4237
|
}
|
|
4168
4238
|
return messages;
|
|
4169
4239
|
}
|
|
4240
|
+
function readTranscriptToolEvents(sessionId, cwd, transcriptPath) {
|
|
4241
|
+
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4242
|
+
if (!existsSync3(path))
|
|
4243
|
+
return [];
|
|
4244
|
+
let raw;
|
|
4245
|
+
try {
|
|
4246
|
+
raw = readFileSync3(path, "utf-8");
|
|
4247
|
+
} catch {
|
|
4248
|
+
return [];
|
|
4249
|
+
}
|
|
4250
|
+
const toolEvents = [];
|
|
4251
|
+
const toolEventIndexes = new Map;
|
|
4252
|
+
for (const line of raw.split(`
|
|
4253
|
+
`)) {
|
|
4254
|
+
if (!line.trim())
|
|
4255
|
+
continue;
|
|
4256
|
+
let entry;
|
|
4257
|
+
try {
|
|
4258
|
+
entry = JSON.parse(line);
|
|
4259
|
+
} catch {
|
|
4260
|
+
continue;
|
|
4261
|
+
}
|
|
4262
|
+
const createdAtEpoch = parseTranscriptTimestamp(entry);
|
|
4263
|
+
const content = getTranscriptContent(entry);
|
|
4264
|
+
if (!Array.isArray(content))
|
|
4265
|
+
continue;
|
|
4266
|
+
for (const block of content) {
|
|
4267
|
+
if (!block || typeof block !== "object")
|
|
4268
|
+
continue;
|
|
4269
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
4270
|
+
const preview = extractToolResultPreview(block.content);
|
|
4271
|
+
const index = toolEventIndexes.get(block.tool_use_id);
|
|
4272
|
+
if (preview && index !== undefined) {
|
|
4273
|
+
toolEvents[index] = {
|
|
4274
|
+
...toolEvents[index],
|
|
4275
|
+
tool_response_preview: preview
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
continue;
|
|
4279
|
+
}
|
|
4280
|
+
if (block.type !== "tool_use")
|
|
4281
|
+
continue;
|
|
4282
|
+
const input = block.input && typeof block.input === "object" ? block.input : {};
|
|
4283
|
+
const toolUseId = typeof block.id === "string" ? block.id : null;
|
|
4284
|
+
const nextEvent = {
|
|
4285
|
+
tool_name: typeof block.name === "string" ? block.name : "Unknown",
|
|
4286
|
+
tool_input_json: JSON.stringify(input),
|
|
4287
|
+
tool_response_preview: null,
|
|
4288
|
+
file_path: extractToolFilePath(input),
|
|
4289
|
+
command: typeof input.command === "string" ? input.command : null,
|
|
4290
|
+
created_at_epoch: createdAtEpoch
|
|
4291
|
+
};
|
|
4292
|
+
toolEvents.push(nextEvent);
|
|
4293
|
+
if (toolUseId) {
|
|
4294
|
+
toolEventIndexes.set(toolUseId, toolEvents.length - 1);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
return toolEvents;
|
|
4299
|
+
}
|
|
4170
4300
|
function resolveHistoryPath(historyPath) {
|
|
4171
4301
|
if (historyPath)
|
|
4172
4302
|
return historyPath;
|
|
@@ -4232,9 +4362,22 @@ function readHistoryFallback(sessionId, cwd, opts) {
|
|
|
4232
4362
|
createdAtEpoch: entry.timestamp
|
|
4233
4363
|
})));
|
|
4234
4364
|
}
|
|
4235
|
-
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
4365
|
+
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath, options = {}) {
|
|
4366
|
+
const embed = options.embed ?? true;
|
|
4236
4367
|
const session = db.getSessionById(sessionId);
|
|
4237
|
-
const
|
|
4368
|
+
const resolvedTranscriptPath = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
4369
|
+
const syncCursorKey = `transcript_sync_cursor:${sessionId}`;
|
|
4370
|
+
if (existsSync3(resolvedTranscriptPath)) {
|
|
4371
|
+
try {
|
|
4372
|
+
const stat = statSync(resolvedTranscriptPath);
|
|
4373
|
+
const cursor = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
|
|
4374
|
+
if (db.getSyncState(syncCursorKey) === cursor) {
|
|
4375
|
+
return { imported: 0, total: 0 };
|
|
4376
|
+
}
|
|
4377
|
+
db.setSyncState(syncCursorKey, cursor);
|
|
4378
|
+
} catch {}
|
|
4379
|
+
}
|
|
4380
|
+
const transcriptMessages = readTranscript(sessionId, cwd, resolvedTranscriptPath).map((message) => ({
|
|
4238
4381
|
...message,
|
|
4239
4382
|
text: message.text.trim()
|
|
4240
4383
|
})).filter((message) => message.text.length > 0);
|
|
@@ -4296,7 +4439,7 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4296
4439
|
created_at_epoch: createdAtEpoch
|
|
4297
4440
|
});
|
|
4298
4441
|
}
|
|
4299
|
-
if (db.vecAvailable) {
|
|
4442
|
+
if (embed && db.vecAvailable) {
|
|
4300
4443
|
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
4301
4444
|
if (embedding) {
|
|
4302
4445
|
db.vecChatInsert(row.id, embedding);
|
|
@@ -4306,6 +4449,35 @@ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
4306
4449
|
}
|
|
4307
4450
|
return { imported, total: messages.length };
|
|
4308
4451
|
}
|
|
4452
|
+
function syncTranscriptToolEvents(db, config, sessionId, cwd, transcriptPath) {
|
|
4453
|
+
const session = db.getSessionById(sessionId);
|
|
4454
|
+
if (!session)
|
|
4455
|
+
return { imported: 0, total: 0 };
|
|
4456
|
+
if (db.getSessionToolEvents(sessionId, 1).length > 0) {
|
|
4457
|
+
return { imported: 0, total: 0 };
|
|
4458
|
+
}
|
|
4459
|
+
const toolEvents = readTranscriptToolEvents(sessionId, cwd, transcriptPath);
|
|
4460
|
+
if (toolEvents.length === 0)
|
|
4461
|
+
return { imported: 0, total: 0 };
|
|
4462
|
+
let imported = 0;
|
|
4463
|
+
for (const event of toolEvents) {
|
|
4464
|
+
db.insertToolEvent({
|
|
4465
|
+
session_id: sessionId,
|
|
4466
|
+
project_id: session.project_id,
|
|
4467
|
+
tool_name: event.tool_name,
|
|
4468
|
+
tool_input_json: event.tool_input_json,
|
|
4469
|
+
tool_response_preview: event.tool_response_preview,
|
|
4470
|
+
file_path: event.file_path,
|
|
4471
|
+
command: event.command,
|
|
4472
|
+
user_id: config.user_id,
|
|
4473
|
+
device_id: config.device_id,
|
|
4474
|
+
agent: "claude-code",
|
|
4475
|
+
created_at_epoch: event.created_at_epoch ?? undefined
|
|
4476
|
+
});
|
|
4477
|
+
imported++;
|
|
4478
|
+
}
|
|
4479
|
+
return { imported, total: toolEvents.length };
|
|
4480
|
+
}
|
|
4309
4481
|
function dedupeHistoryMessages(messages) {
|
|
4310
4482
|
const deduped = [];
|
|
4311
4483
|
for (const message of messages) {
|
|
@@ -4323,6 +4495,59 @@ function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
|
4323
4495
|
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
4324
4496
|
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
4325
4497
|
}
|
|
4498
|
+
function getTranscriptRole(entry) {
|
|
4499
|
+
return entry.role ?? entry.message?.role ?? entry.type ?? entry.message?.type;
|
|
4500
|
+
}
|
|
4501
|
+
function getTranscriptContent(entry) {
|
|
4502
|
+
return entry.content ?? entry.message?.content;
|
|
4503
|
+
}
|
|
4504
|
+
function parseTranscriptTimestamp(entry) {
|
|
4505
|
+
const raw = entry.timestamp ?? entry.message?.timestamp;
|
|
4506
|
+
if (typeof raw !== "string")
|
|
4507
|
+
return null;
|
|
4508
|
+
const epoch = Date.parse(raw);
|
|
4509
|
+
return Number.isFinite(epoch) ? Math.floor(epoch / 1000) : null;
|
|
4510
|
+
}
|
|
4511
|
+
function extractToolResultPreview(content) {
|
|
4512
|
+
if (typeof content === "string")
|
|
4513
|
+
return content.slice(0, 4000);
|
|
4514
|
+
if (Array.isArray(content)) {
|
|
4515
|
+
const text = content.map((item) => {
|
|
4516
|
+
if (typeof item === "string")
|
|
4517
|
+
return item;
|
|
4518
|
+
if (item && typeof item === "object" && typeof item.text === "string")
|
|
4519
|
+
return item.text;
|
|
4520
|
+
return "";
|
|
4521
|
+
}).filter(Boolean).join(`
|
|
4522
|
+
`);
|
|
4523
|
+
return text ? text.slice(0, 4000) : null;
|
|
4524
|
+
}
|
|
4525
|
+
return null;
|
|
4526
|
+
}
|
|
4527
|
+
function extractToolFilePath(input) {
|
|
4528
|
+
for (const key of ["file_path", "path", "target_file"]) {
|
|
4529
|
+
if (typeof input[key] === "string")
|
|
4530
|
+
return input[key];
|
|
4531
|
+
}
|
|
4532
|
+
return null;
|
|
4533
|
+
}
|
|
4534
|
+
function findTranscriptPathBySessionId(sessionId) {
|
|
4535
|
+
const projectsDir = join3(homedir2(), ".claude", "projects");
|
|
4536
|
+
if (!existsSync3(projectsDir))
|
|
4537
|
+
return null;
|
|
4538
|
+
try {
|
|
4539
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
4540
|
+
if (!entry.isDirectory())
|
|
4541
|
+
continue;
|
|
4542
|
+
const candidate = join3(projectsDir, entry.name, `${sessionId}.jsonl`);
|
|
4543
|
+
if (existsSync3(candidate))
|
|
4544
|
+
return candidate;
|
|
4545
|
+
}
|
|
4546
|
+
} catch {
|
|
4547
|
+
return null;
|
|
4548
|
+
}
|
|
4549
|
+
return null;
|
|
4550
|
+
}
|
|
4326
4551
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
4327
4552
|
const lines = [];
|
|
4328
4553
|
for (const msg of messages) {
|
|
@@ -4452,7 +4677,8 @@ async function main() {
|
|
|
4452
4677
|
try {
|
|
4453
4678
|
let importedChat = 0;
|
|
4454
4679
|
if (event.session_id) {
|
|
4455
|
-
|
|
4680
|
+
syncTranscriptToolEvents(db, config, event.session_id, event.cwd);
|
|
4681
|
+
const chatSync = await syncTranscriptChat(db, config, event.session_id, event.cwd, undefined, { embed: false });
|
|
4456
4682
|
importedChat = chatSync.imported;
|
|
4457
4683
|
await upsertRollingHandoff(db, config, {
|
|
4458
4684
|
session_id: event.session_id,
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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(
|
|
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 ${
|
|
189
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
179
190
|
}
|
|
180
191
|
if (typeof parsed !== "object" || parsed === null) {
|
|
181
|
-
throw new Error(`Config at ${
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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(
|
|
263
|
+
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + `
|
|
264
|
+
`, "utf-8");
|
|
265
|
+
if (!looksLikePlaceholderAuth(config)) {
|
|
266
|
+
writeFileSync(authBackupPath, JSON.stringify(extractAuthBackup(config), null, 2) + `
|
|
246
267
|
`, "utf-8");
|
|
268
|
+
}
|
|
247
269
|
}
|
|
248
270
|
function configExists() {
|
|
249
|
-
return existsSync(
|
|
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 = [
|