engrm 0.4.43 → 0.4.44
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 +260 -24
- package/dist/hooks/elicitation-result.js +22 -10
- package/dist/hooks/post-tool-use.js +99 -36
- package/dist/hooks/pre-compact.js +35 -11
- package/dist/hooks/sentinel.js +22 -10
- package/dist/hooks/session-start.js +158 -14
- package/dist/hooks/stop.js +178 -41
- package/dist/hooks/user-prompt-submit.js +48 -12
- package/dist/server.js +202 -27
- package/opencode/README.md +6 -6
- package/opencode/install-or-update-opencode-plugin.sh +7 -1
- package/opencode/opencode.example.json +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -21,8 +21,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
21
21
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync } from "fs";
|
|
22
22
|
import { hostname as hostname3, homedir as homedir5, networkInterfaces as networkInterfaces3 } from "os";
|
|
23
23
|
import { dirname as dirname5, join as join8 } from "path";
|
|
24
|
-
import { createHash as
|
|
25
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
24
|
+
import { createHash as createHash5 } from "crypto";
|
|
25
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
|
|
26
26
|
|
|
27
27
|
// src/config.ts
|
|
28
28
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -880,6 +880,7 @@ function ensureObservationTypes(db) {
|
|
|
880
880
|
DROP TABLE observations;
|
|
881
881
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
882
882
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
883
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
883
884
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
884
885
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
885
886
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1160,6 +1161,7 @@ class MemDatabase {
|
|
|
1160
1161
|
this.db = openDatabase(dbPath);
|
|
1161
1162
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1162
1163
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1164
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1163
1165
|
this.vecAvailable = this.loadVecExtension();
|
|
1164
1166
|
runMigrations(this.db);
|
|
1165
1167
|
ensureObservationTypes(this.db);
|
|
@@ -1181,8 +1183,16 @@ class MemDatabase {
|
|
|
1181
1183
|
this.db.close();
|
|
1182
1184
|
}
|
|
1183
1185
|
upsertProject(project) {
|
|
1186
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1187
|
+
const name = project.name?.trim();
|
|
1188
|
+
if (!canonicalId) {
|
|
1189
|
+
throw new Error("Project canonical_id is required");
|
|
1190
|
+
}
|
|
1191
|
+
if (!name) {
|
|
1192
|
+
throw new Error("Project name is required");
|
|
1193
|
+
}
|
|
1184
1194
|
const now = Math.floor(Date.now() / 1000);
|
|
1185
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1195
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1186
1196
|
if (existing) {
|
|
1187
1197
|
this.db.query(`UPDATE projects SET
|
|
1188
1198
|
local_path = COALESCE(?, local_path),
|
|
@@ -1197,7 +1207,7 @@ class MemDatabase {
|
|
|
1197
1207
|
};
|
|
1198
1208
|
}
|
|
1199
1209
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1200
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1210
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1201
1211
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1202
1212
|
}
|
|
1203
1213
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -1890,6 +1900,33 @@ function getOutboxStats(db) {
|
|
|
1890
1900
|
}
|
|
1891
1901
|
return stats;
|
|
1892
1902
|
}
|
|
1903
|
+
function getOutboxFailureSummaries(db, limit = 5) {
|
|
1904
|
+
return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
|
|
1905
|
+
FROM sync_outbox
|
|
1906
|
+
WHERE status = 'failed'
|
|
1907
|
+
GROUP BY COALESCE(last_error, '')
|
|
1908
|
+
ORDER BY count DESC, error ASC
|
|
1909
|
+
LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
|
|
1910
|
+
}
|
|
1911
|
+
function classifyOutboxFailure(error) {
|
|
1912
|
+
const normalized = error.toLowerCase();
|
|
1913
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
1914
|
+
return "auth";
|
|
1915
|
+
}
|
|
1916
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
1917
|
+
return "rate_limit";
|
|
1918
|
+
}
|
|
1919
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
1920
|
+
return "timeout";
|
|
1921
|
+
}
|
|
1922
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
1923
|
+
return "network";
|
|
1924
|
+
}
|
|
1925
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
1926
|
+
return "validation";
|
|
1927
|
+
}
|
|
1928
|
+
return "other";
|
|
1929
|
+
}
|
|
1893
1930
|
|
|
1894
1931
|
// src/intelligence/value-signals.ts
|
|
1895
1932
|
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
@@ -2475,7 +2512,7 @@ async function provision(baseUrl, request) {
|
|
|
2475
2512
|
import { randomBytes } from "node:crypto";
|
|
2476
2513
|
import { createServer } from "node:http";
|
|
2477
2514
|
import { execFile } from "node:child_process";
|
|
2478
|
-
var CALLBACK_TIMEOUT_MS =
|
|
2515
|
+
var CALLBACK_TIMEOUT_MS = 600000;
|
|
2479
2516
|
async function runBrowserAuth(candengoUrl) {
|
|
2480
2517
|
const state = randomBytes(16).toString("hex");
|
|
2481
2518
|
const { port, waitForCallback, stop } = await startCallbackServer(state);
|
|
@@ -2810,13 +2847,16 @@ function registerCodexHooks() {
|
|
|
2810
2847
|
return { path: CODEX_HOOKS, added: true };
|
|
2811
2848
|
}
|
|
2812
2849
|
function registerOpenCode() {
|
|
2850
|
+
const runtime = findRuntime();
|
|
2813
2851
|
const root = findPackageRoot();
|
|
2852
|
+
const dist = isBuiltDist();
|
|
2814
2853
|
const pluginSource = join2(root, "opencode", "plugin", "engrm-opencode.js");
|
|
2854
|
+
const serverPath = dist ? join2(root, "dist", "server.js") : join2(root, "src", "server.ts");
|
|
2815
2855
|
const config = readJsonFile(OPENCODE_CONFIG);
|
|
2816
2856
|
const mcp = config["mcp"] ?? {};
|
|
2817
2857
|
mcp["engrm"] = {
|
|
2818
2858
|
type: "local",
|
|
2819
|
-
command: ["
|
|
2859
|
+
command: dist ? [runtime, serverPath] : [runtime, "run", serverPath],
|
|
2820
2860
|
enabled: true,
|
|
2821
2861
|
timeout: 5000
|
|
2822
2862
|
};
|
|
@@ -3326,10 +3366,11 @@ function readProjectConfigFile(directory) {
|
|
|
3326
3366
|
}
|
|
3327
3367
|
}
|
|
3328
3368
|
function detectProject(directory) {
|
|
3329
|
-
const
|
|
3369
|
+
const resolvedDirectory = resolve(directory);
|
|
3370
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
3330
3371
|
if (remoteUrl) {
|
|
3331
3372
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
3332
|
-
const repoRoot = getGitTopLevel(
|
|
3373
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
3333
3374
|
return {
|
|
3334
3375
|
canonical_id: canonicalId,
|
|
3335
3376
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -3337,21 +3378,22 @@ function detectProject(directory) {
|
|
|
3337
3378
|
local_path: repoRoot
|
|
3338
3379
|
};
|
|
3339
3380
|
}
|
|
3340
|
-
const configFile = readProjectConfigFile(
|
|
3381
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
3341
3382
|
if (configFile) {
|
|
3342
3383
|
return {
|
|
3343
3384
|
canonical_id: configFile.project_id,
|
|
3344
3385
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
3345
3386
|
remote_url: null,
|
|
3346
|
-
local_path:
|
|
3387
|
+
local_path: resolvedDirectory
|
|
3347
3388
|
};
|
|
3348
3389
|
}
|
|
3349
|
-
const dirName = basename(
|
|
3390
|
+
const dirName = basename(resolvedDirectory);
|
|
3391
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
3350
3392
|
return {
|
|
3351
|
-
canonical_id: `local/${
|
|
3352
|
-
name:
|
|
3393
|
+
canonical_id: `local/${safeDirName}`,
|
|
3394
|
+
name: safeDirName,
|
|
3353
3395
|
remote_url: null,
|
|
3354
|
-
local_path:
|
|
3396
|
+
local_path: resolvedDirectory
|
|
3355
3397
|
};
|
|
3356
3398
|
}
|
|
3357
3399
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -4323,6 +4365,117 @@ function hasOpenClawMcpRegistration(content) {
|
|
|
4323
4365
|
}
|
|
4324
4366
|
}
|
|
4325
4367
|
|
|
4368
|
+
// src/sync/auth.ts
|
|
4369
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4370
|
+
|
|
4371
|
+
// src/storage/outbox.ts
|
|
4372
|
+
function getPendingEntries(db, limit = 50) {
|
|
4373
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4374
|
+
return db.db.query(`SELECT * FROM sync_outbox
|
|
4375
|
+
WHERE (status = 'pending')
|
|
4376
|
+
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
4377
|
+
ORDER BY created_at_epoch ASC
|
|
4378
|
+
LIMIT ?`).all(now, limit);
|
|
4379
|
+
}
|
|
4380
|
+
function markSyncing(db, entryId) {
|
|
4381
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4382
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
4383
|
+
}
|
|
4384
|
+
function markSynced(db, entryId) {
|
|
4385
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4386
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
4387
|
+
}
|
|
4388
|
+
function markFailed(db, entryId, error) {
|
|
4389
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4390
|
+
db.db.query(`UPDATE sync_outbox SET
|
|
4391
|
+
status = 'failed',
|
|
4392
|
+
retry_count = retry_count + 1,
|
|
4393
|
+
last_error = ?,
|
|
4394
|
+
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
4395
|
+
WHERE id = ?`).run(error, now, entryId);
|
|
4396
|
+
}
|
|
4397
|
+
function getOutboxStats2(db) {
|
|
4398
|
+
const rows = db.db.query("SELECT status, COUNT(*) as count FROM sync_outbox GROUP BY status").all();
|
|
4399
|
+
const stats = {
|
|
4400
|
+
pending: 0,
|
|
4401
|
+
syncing: 0,
|
|
4402
|
+
synced: 0,
|
|
4403
|
+
failed: 0
|
|
4404
|
+
};
|
|
4405
|
+
for (const row of rows) {
|
|
4406
|
+
stats[row.status] = row.count;
|
|
4407
|
+
}
|
|
4408
|
+
return stats;
|
|
4409
|
+
}
|
|
4410
|
+
function getOutboxFailureSummaries2(db, limit = 5) {
|
|
4411
|
+
return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
|
|
4412
|
+
FROM sync_outbox
|
|
4413
|
+
WHERE status = 'failed'
|
|
4414
|
+
GROUP BY COALESCE(last_error, '')
|
|
4415
|
+
ORDER BY count DESC, error ASC
|
|
4416
|
+
LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
|
|
4417
|
+
}
|
|
4418
|
+
function classifyOutboxFailure2(error) {
|
|
4419
|
+
const normalized = error.toLowerCase();
|
|
4420
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
4421
|
+
return "auth";
|
|
4422
|
+
}
|
|
4423
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
4424
|
+
return "rate_limit";
|
|
4425
|
+
}
|
|
4426
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
4427
|
+
return "timeout";
|
|
4428
|
+
}
|
|
4429
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
4430
|
+
return "network";
|
|
4431
|
+
}
|
|
4432
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
4433
|
+
return "validation";
|
|
4434
|
+
}
|
|
4435
|
+
return "other";
|
|
4436
|
+
}
|
|
4437
|
+
function resetFailedEntries(db) {
|
|
4438
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
4439
|
+
SET status = 'pending',
|
|
4440
|
+
retry_count = 0,
|
|
4441
|
+
last_error = NULL,
|
|
4442
|
+
next_retry_epoch = NULL
|
|
4443
|
+
WHERE status = 'failed'`).run();
|
|
4444
|
+
return result.changes;
|
|
4445
|
+
}
|
|
4446
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
4447
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
4448
|
+
FROM sync_outbox
|
|
4449
|
+
WHERE status = 'failed'`).all();
|
|
4450
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
4451
|
+
if (matchingIds.length === 0)
|
|
4452
|
+
return 0;
|
|
4453
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
4454
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
4455
|
+
SET status = 'pending',
|
|
4456
|
+
retry_count = 0,
|
|
4457
|
+
last_error = NULL,
|
|
4458
|
+
next_retry_epoch = NULL
|
|
4459
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
4460
|
+
return result.changes;
|
|
4461
|
+
}
|
|
4462
|
+
function resetSyncingEntries(db) {
|
|
4463
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
4464
|
+
SET status = 'pending',
|
|
4465
|
+
next_retry_epoch = NULL
|
|
4466
|
+
WHERE status = 'syncing'`).run();
|
|
4467
|
+
return result.changes;
|
|
4468
|
+
}
|
|
4469
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
4470
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
4471
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
4472
|
+
SET status = 'pending',
|
|
4473
|
+
next_retry_epoch = NULL
|
|
4474
|
+
WHERE status = 'syncing'
|
|
4475
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
4476
|
+
return result.changes;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4326
4479
|
// src/sync/auth.ts
|
|
4327
4480
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
4328
4481
|
function normalizeBaseUrl(url) {
|
|
@@ -4339,6 +4492,46 @@ function normalizeBaseUrl(url) {
|
|
|
4339
4492
|
return trimmed.replace(/\/$/, "");
|
|
4340
4493
|
}
|
|
4341
4494
|
}
|
|
4495
|
+
function getApiKey(config) {
|
|
4496
|
+
const envKey = process.env.ENGRM_TOKEN;
|
|
4497
|
+
if (envKey && envKey.startsWith("cvk_"))
|
|
4498
|
+
return envKey;
|
|
4499
|
+
if (config.candengo_api_key && config.candengo_api_key.length > 0) {
|
|
4500
|
+
return config.candengo_api_key;
|
|
4501
|
+
}
|
|
4502
|
+
return null;
|
|
4503
|
+
}
|
|
4504
|
+
function getBaseUrl(config) {
|
|
4505
|
+
if (config.candengo_url && config.candengo_url.length > 0) {
|
|
4506
|
+
return normalizeBaseUrl(config.candengo_url);
|
|
4507
|
+
}
|
|
4508
|
+
return null;
|
|
4509
|
+
}
|
|
4510
|
+
function getAuthFingerprint(config) {
|
|
4511
|
+
const apiKey = getApiKey(config);
|
|
4512
|
+
const baseUrl = getBaseUrl(config);
|
|
4513
|
+
if (!apiKey || !baseUrl)
|
|
4514
|
+
return null;
|
|
4515
|
+
return createHash4("sha256").update(`${baseUrl}
|
|
4516
|
+
${apiKey}
|
|
4517
|
+
${config.namespace}
|
|
4518
|
+
${config.site_id}`).digest("hex");
|
|
4519
|
+
}
|
|
4520
|
+
function recoverOutboxAfterSuccessfulAuth(db, config) {
|
|
4521
|
+
const fingerprint = getAuthFingerprint(config);
|
|
4522
|
+
const staleSyncingReset = resetStaleSyncingEntries(db);
|
|
4523
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure2(error) === "auth");
|
|
4524
|
+
if (fingerprint) {
|
|
4525
|
+
db.setSyncState("sync_auth_fingerprint", fingerprint);
|
|
4526
|
+
}
|
|
4527
|
+
return {
|
|
4528
|
+
fingerprintChanged: false,
|
|
4529
|
+
failedReset: 0,
|
|
4530
|
+
authFailedReset,
|
|
4531
|
+
syncingReset: 0,
|
|
4532
|
+
staleSyncingReset
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4342
4535
|
|
|
4343
4536
|
// src/cli.ts
|
|
4344
4537
|
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
@@ -4350,6 +4543,9 @@ switch (command) {
|
|
|
4350
4543
|
case "init":
|
|
4351
4544
|
await handleInit(args.slice(1));
|
|
4352
4545
|
break;
|
|
4546
|
+
case "serve":
|
|
4547
|
+
await handleServe();
|
|
4548
|
+
break;
|
|
4353
4549
|
case "status":
|
|
4354
4550
|
handleStatus();
|
|
4355
4551
|
break;
|
|
@@ -4415,6 +4611,12 @@ async function handleInit(flags) {
|
|
|
4415
4611
|
await initWithBrowser(url);
|
|
4416
4612
|
await maybeInstallPack(flags);
|
|
4417
4613
|
}
|
|
4614
|
+
async function handleServe() {
|
|
4615
|
+
const packageRoot = join8(THIS_DIR, "..");
|
|
4616
|
+
const serverPath = IS_BUILT_DIST ? join8(packageRoot, "dist", "server.js") : join8(packageRoot, "src", "server.ts");
|
|
4617
|
+
await import(pathToFileURL(serverPath).href);
|
|
4618
|
+
await new Promise(() => {});
|
|
4619
|
+
}
|
|
4418
4620
|
async function maybeInstallPack(flags) {
|
|
4419
4621
|
const packFlag = flags.find((f) => f.startsWith("--pack"));
|
|
4420
4622
|
if (!packFlag)
|
|
@@ -4510,11 +4712,27 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4510
4712
|
ensureConfigDir();
|
|
4511
4713
|
let existingDeviceId;
|
|
4512
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;
|
|
4513
4723
|
if (configExists()) {
|
|
4514
4724
|
try {
|
|
4515
4725
|
const existing = loadConfig();
|
|
4516
4726
|
existingDeviceId = existing.device_id;
|
|
4517
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;
|
|
4518
4736
|
} catch {}
|
|
4519
4737
|
}
|
|
4520
4738
|
const config = {
|
|
@@ -4526,17 +4744,17 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4526
4744
|
user_email: result.user_email,
|
|
4527
4745
|
device_id: existingDeviceId || generateDeviceId3(),
|
|
4528
4746
|
teams: result.teams ?? [],
|
|
4529
|
-
sync: {
|
|
4747
|
+
sync: existingSync ?? {
|
|
4530
4748
|
enabled: true,
|
|
4531
4749
|
interval_seconds: 30,
|
|
4532
4750
|
batch_size: 50
|
|
4533
4751
|
},
|
|
4534
|
-
search: {
|
|
4752
|
+
search: existingSearch ?? {
|
|
4535
4753
|
default_limit: 10,
|
|
4536
4754
|
local_boost: 1.2,
|
|
4537
4755
|
scope: "all"
|
|
4538
4756
|
},
|
|
4539
|
-
scrubbing: {
|
|
4757
|
+
scrubbing: existingScrubbing ?? {
|
|
4540
4758
|
enabled: true,
|
|
4541
4759
|
custom_patterns: [],
|
|
4542
4760
|
default_sensitivity: "shared"
|
|
@@ -4552,17 +4770,29 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4552
4770
|
daily_limit: 100,
|
|
4553
4771
|
tier: "free"
|
|
4554
4772
|
},
|
|
4555
|
-
observer: {
|
|
4773
|
+
observer: existingObserver ?? {
|
|
4556
4774
|
enabled: true,
|
|
4557
4775
|
mode: "per_event",
|
|
4558
4776
|
model: "haiku"
|
|
4559
4777
|
},
|
|
4560
|
-
transcript_analysis: {
|
|
4778
|
+
transcript_analysis: existingTranscriptAnalysis ?? {
|
|
4561
4779
|
enabled: false
|
|
4562
|
-
}
|
|
4780
|
+
},
|
|
4781
|
+
http: existingHttp ?? {
|
|
4782
|
+
enabled: false,
|
|
4783
|
+
port: 3767,
|
|
4784
|
+
bearer_tokens: []
|
|
4785
|
+
},
|
|
4786
|
+
fleet: existingFleet ?? {
|
|
4787
|
+
project_name: "shared-experience",
|
|
4788
|
+
namespace: "",
|
|
4789
|
+
api_key: ""
|
|
4790
|
+
},
|
|
4791
|
+
tool_profile: existingToolProfile ?? "full"
|
|
4563
4792
|
};
|
|
4564
4793
|
saveConfig(config);
|
|
4565
4794
|
const db = new MemDatabase(getDbPath());
|
|
4795
|
+
recoverOutboxAfterSuccessfulAuth(db, config);
|
|
4566
4796
|
db.close();
|
|
4567
4797
|
console.log(`Configuration saved to ${getSettingsPath()}`);
|
|
4568
4798
|
console.log(`Database initialised at ${getDbPath()}`);
|
|
@@ -4878,7 +5108,7 @@ function handleStatus() {
|
|
|
4878
5108
|
}
|
|
4879
5109
|
console.log(` Value: ${signalParts.join(", ")}`);
|
|
4880
5110
|
if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
|
|
4881
|
-
console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"},
|
|
5111
|
+
console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
|
|
4882
5112
|
}
|
|
4883
5113
|
} catch {}
|
|
4884
5114
|
try {
|
|
@@ -4899,6 +5129,11 @@ function handleStatus() {
|
|
|
4899
5129
|
console.log(`
|
|
4900
5130
|
Sync`);
|
|
4901
5131
|
console.log(` Outbox: ${outbox["pending"] ?? 0} pending, ${outbox["failed"] ?? 0} failed, ${outbox["synced"] ?? 0} synced`);
|
|
5132
|
+
const topFailures = getOutboxFailureSummaries(db, 2);
|
|
5133
|
+
if (topFailures.length > 0) {
|
|
5134
|
+
const failureSummary = topFailures.map((row) => `${classifyOutboxFailure(row.error)} ${row.count}`).join(", ");
|
|
5135
|
+
console.log(` Failures: ${failureSummary}`);
|
|
5136
|
+
}
|
|
4902
5137
|
try {
|
|
4903
5138
|
const lastPush = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_push_epoch");
|
|
4904
5139
|
const lastPull = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_pull_epoch");
|
|
@@ -4979,7 +5214,7 @@ function generateDeviceId3() {
|
|
|
4979
5214
|
break;
|
|
4980
5215
|
}
|
|
4981
5216
|
const material = `${host}:${mac || "no-mac"}`;
|
|
4982
|
-
const suffix =
|
|
5217
|
+
const suffix = createHash5("sha256").update(material).digest("hex").slice(0, 8);
|
|
4983
5218
|
return `${host}-${suffix}`;
|
|
4984
5219
|
}
|
|
4985
5220
|
async function handleInstallPack(flags) {
|
|
@@ -5393,7 +5628,7 @@ async function handleDoctor() {
|
|
|
5393
5628
|
const dbPath = getDbPath();
|
|
5394
5629
|
if (existsSync8(dbPath)) {
|
|
5395
5630
|
const stats = statSync(dbPath);
|
|
5396
|
-
const sizeMB = stats.size /
|
|
5631
|
+
const sizeMB = stats.size / 1048576;
|
|
5397
5632
|
const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
|
|
5398
5633
|
info(`Database size: ${sizeStr}`);
|
|
5399
5634
|
}
|
|
@@ -5529,7 +5764,7 @@ And add to ~/.config/opencode/opencode.json:`);
|
|
|
5529
5764
|
"mcp": {
|
|
5530
5765
|
"engrm": {
|
|
5531
5766
|
"type": "local",
|
|
5532
|
-
"command": ["
|
|
5767
|
+
"command": ${JSON.stringify(IS_BUILT_DIST ? [process.execPath, join8(packageRoot, "dist", "server.js")] : ["bun", "run", join8(packageRoot, "src", "server.ts")])},
|
|
5533
5768
|
"enabled": true,
|
|
5534
5769
|
"timeout": 5000
|
|
5535
5770
|
}
|
|
@@ -5558,6 +5793,7 @@ function printUsage() {
|
|
|
5558
5793
|
console.log(`Engrm \u2014 Memory layer for AI coding agents
|
|
5559
5794
|
`);
|
|
5560
5795
|
console.log("Usage:");
|
|
5796
|
+
console.log(" engrm serve Run the MCP server over stdio");
|
|
5561
5797
|
console.log(" engrm init Setup via browser (recommended)");
|
|
5562
5798
|
console.log(" engrm init --token=cmt_xxx Setup from provisioning token");
|
|
5563
5799
|
console.log(" engrm init --pack=<name> Setup + install a starter pack");
|
|
@@ -419,10 +419,11 @@ function readProjectConfigFile(directory) {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
function detectProject(directory) {
|
|
422
|
-
const
|
|
422
|
+
const resolvedDirectory = resolve(directory);
|
|
423
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
423
424
|
if (remoteUrl) {
|
|
424
425
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
425
|
-
const repoRoot = getGitTopLevel(
|
|
426
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
426
427
|
return {
|
|
427
428
|
canonical_id: canonicalId,
|
|
428
429
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -430,21 +431,22 @@ function detectProject(directory) {
|
|
|
430
431
|
local_path: repoRoot
|
|
431
432
|
};
|
|
432
433
|
}
|
|
433
|
-
const configFile = readProjectConfigFile(
|
|
434
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
434
435
|
if (configFile) {
|
|
435
436
|
return {
|
|
436
437
|
canonical_id: configFile.project_id,
|
|
437
438
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
438
439
|
remote_url: null,
|
|
439
|
-
local_path:
|
|
440
|
+
local_path: resolvedDirectory
|
|
440
441
|
};
|
|
441
442
|
}
|
|
442
|
-
const dirName = basename(
|
|
443
|
+
const dirName = basename(resolvedDirectory);
|
|
444
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
443
445
|
return {
|
|
444
|
-
canonical_id: `local/${
|
|
445
|
-
name:
|
|
446
|
+
canonical_id: `local/${safeDirName}`,
|
|
447
|
+
name: safeDirName,
|
|
446
448
|
remote_url: null,
|
|
447
|
-
local_path:
|
|
449
|
+
local_path: resolvedDirectory
|
|
448
450
|
};
|
|
449
451
|
}
|
|
450
452
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -1752,6 +1754,7 @@ function ensureObservationTypes(db) {
|
|
|
1752
1754
|
DROP TABLE observations;
|
|
1753
1755
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1754
1756
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
1757
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1755
1758
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1756
1759
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1757
1760
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -2032,6 +2035,7 @@ class MemDatabase {
|
|
|
2032
2035
|
this.db = openDatabase(dbPath);
|
|
2033
2036
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2034
2037
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
2038
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
2035
2039
|
this.vecAvailable = this.loadVecExtension();
|
|
2036
2040
|
runMigrations(this.db);
|
|
2037
2041
|
ensureObservationTypes(this.db);
|
|
@@ -2053,8 +2057,16 @@ class MemDatabase {
|
|
|
2053
2057
|
this.db.close();
|
|
2054
2058
|
}
|
|
2055
2059
|
upsertProject(project) {
|
|
2060
|
+
const canonicalId = project.canonical_id?.trim();
|
|
2061
|
+
const name = project.name?.trim();
|
|
2062
|
+
if (!canonicalId) {
|
|
2063
|
+
throw new Error("Project canonical_id is required");
|
|
2064
|
+
}
|
|
2065
|
+
if (!name) {
|
|
2066
|
+
throw new Error("Project name is required");
|
|
2067
|
+
}
|
|
2056
2068
|
const now = Math.floor(Date.now() / 1000);
|
|
2057
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
2069
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
2058
2070
|
if (existing) {
|
|
2059
2071
|
this.db.query(`UPDATE projects SET
|
|
2060
2072
|
local_path = COALESCE(?, local_path),
|
|
@@ -2069,7 +2081,7 @@ class MemDatabase {
|
|
|
2069
2081
|
};
|
|
2070
2082
|
}
|
|
2071
2083
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
2072
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
2084
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
2073
2085
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
2074
2086
|
}
|
|
2075
2087
|
getProjectByCanonicalId(canonicalId) {
|