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 +411 -118
- package/dist/hooks/elicitation-result.js +96 -15
- package/dist/hooks/post-tool-use.js +265 -23
- package/dist/hooks/pre-compact.js +264 -23
- package/dist/hooks/sentinel.js +96 -15
- package/dist/hooks/session-start.js +120 -17
- package/dist/hooks/stop.js +326 -27
- package/dist/hooks/user-prompt-submit.js +96 -15
- package/dist/server.js +208 -34
- package/package.json +1 -1
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
45
|
+
return resolveConfigDir();
|
|
37
46
|
}
|
|
38
47
|
function getSettingsPath() {
|
|
39
|
-
return
|
|
48
|
+
return resolveSettingsPath();
|
|
40
49
|
}
|
|
41
50
|
function getDbPath() {
|
|
42
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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(
|
|
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 ${
|
|
141
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
131
142
|
}
|
|
132
143
|
if (typeof parsed !== "object" || parsed === null) {
|
|
133
|
-
throw new Error(`Config at ${
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4064
|
-
|
|
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(
|
|
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 ${
|
|
4163
|
+
throw new Error(`Invalid JSON in ${settingsPath}`);
|
|
4072
4164
|
}
|
|
4073
4165
|
if (typeof parsed !== "object" || parsed === null) {
|
|
4074
|
-
throw new Error(`Config at ${
|
|
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
|
-
|
|
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
|
-
|
|
4136
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
4523
|
-
const
|
|
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
|
|
4530
|
-
authFailedReset,
|
|
4531
|
-
syncingReset
|
|
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
|
-
|
|
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:
|
|
4889
|
+
device_id: preserved.device_id || generateDeviceId3(),
|
|
4746
4890
|
teams: result.teams ?? [],
|
|
4747
|
-
sync:
|
|
4891
|
+
sync: preserved.sync ?? {
|
|
4748
4892
|
enabled: true,
|
|
4749
4893
|
interval_seconds: 30,
|
|
4750
4894
|
batch_size: 50
|
|
4751
4895
|
},
|
|
4752
|
-
search:
|
|
4896
|
+
search: preserved.search ?? {
|
|
4753
4897
|
default_limit: 10,
|
|
4754
4898
|
local_boost: 1.2,
|
|
4755
4899
|
scope: "all"
|
|
4756
4900
|
},
|
|
4757
|
-
scrubbing:
|
|
4901
|
+
scrubbing: preserved.scrubbing ?? {
|
|
4758
4902
|
enabled: true,
|
|
4759
4903
|
custom_patterns: [],
|
|
4760
4904
|
default_sensitivity: "shared"
|
|
4761
4905
|
},
|
|
4762
|
-
sentinel:
|
|
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:
|
|
4917
|
+
observer: preserved.observer ?? {
|
|
4774
4918
|
enabled: true,
|
|
4775
4919
|
mode: "per_event",
|
|
4776
4920
|
model: "haiku"
|
|
4777
4921
|
},
|
|
4778
|
-
transcript_analysis:
|
|
4922
|
+
transcript_analysis: preserved.transcript_analysis ?? {
|
|
4779
4923
|
enabled: false
|
|
4780
4924
|
},
|
|
4781
|
-
http:
|
|
4925
|
+
http: preserved.http ?? {
|
|
4782
4926
|
enabled: false,
|
|
4783
4927
|
port: 3767,
|
|
4784
4928
|
bearer_tokens: []
|
|
4785
4929
|
},
|
|
4786
|
-
fleet:
|
|
4930
|
+
fleet: preserved.fleet ?? {
|
|
4787
4931
|
project_name: "shared-experience",
|
|
4788
4932
|
namespace: "",
|
|
4789
4933
|
api_key: ""
|
|
4790
4934
|
},
|
|
4791
|
-
tool_profile:
|
|
4935
|
+
tool_profile: preserved.tool_profile ?? "full"
|
|
4792
4936
|
};
|
|
4793
4937
|
saveConfig(config);
|
|
4794
4938
|
const db = new MemDatabase(getDbPath());
|
|
4795
|
-
|
|
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
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
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
|
-
|
|
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");
|