engrm 0.4.42 → 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/README.md +4 -1
- package/dist/cli.js +300 -28
- package/dist/hooks/elicitation-result.js +31 -12
- package/dist/hooks/post-tool-use.js +108 -38
- package/dist/hooks/pre-compact.js +44 -13
- package/dist/hooks/sentinel.js +31 -12
- package/dist/hooks/session-start.js +170 -16
- package/dist/hooks/stop.js +258 -152
- package/dist/hooks/user-prompt-submit.js +57 -14
- package/dist/server.js +248 -31
- 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/README.md
CHANGED
|
@@ -110,6 +110,7 @@ Add this to `~/.engrm/settings.json`:
|
|
|
110
110
|
"port": 3767,
|
|
111
111
|
"bearer_tokens": ["replace-with-a-long-random-token"]
|
|
112
112
|
},
|
|
113
|
+
"tool_profile": "memory",
|
|
113
114
|
"fleet": {
|
|
114
115
|
"project_name": "shared-experience",
|
|
115
116
|
"namespace": "ns_fleet_shared",
|
|
@@ -154,6 +155,8 @@ Fleet writes:
|
|
|
154
155
|
- sync to the dedicated fleet namespace/key instead of the normal org namespace
|
|
155
156
|
- get an extra outbound scrub pass that redacts hostnames, IPs, and MACs before upload
|
|
156
157
|
|
|
158
|
+
For Hermes-style shared learning deployments, set `"tool_profile": "memory"` to expose a reduced Engrm tool set focused on durable memory, recall, and thread resumption instead of the full developer-oriented surface.
|
|
159
|
+
|
|
157
160
|
---
|
|
158
161
|
|
|
159
162
|
## How It Works
|
|
@@ -664,7 +667,7 @@ Engrm auto-registers in:
|
|
|
664
667
|
- **Local storage:** SQLite via `better-sqlite3`, FTS5 full-text search, `sqlite-vec` for embeddings
|
|
665
668
|
- **Embeddings:** all-MiniLM-L6-v2 via `@xenova/transformers` (384 dims, ~23MB)
|
|
666
669
|
- **Remote backend:** Candengo Vector (BGE-M3, Qdrant, hybrid dense+sparse search)
|
|
667
|
-
- **MCP:** `@modelcontextprotocol/sdk` (stdio
|
|
670
|
+
- **MCP:** `@modelcontextprotocol/sdk` (stdio for local agents, Streamable HTTP + SSE compatibility for Hermes-style remote clients)
|
|
668
671
|
- **AI extraction:** `@anthropic-ai/claude-agent-sdk` (optional, for richer observations)
|
|
669
672
|
|
|
670
673
|
---
|
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";
|
|
@@ -114,7 +114,8 @@ function createDefaultConfig() {
|
|
|
114
114
|
project_name: "shared-experience",
|
|
115
115
|
namespace: "",
|
|
116
116
|
api_key: ""
|
|
117
|
-
}
|
|
117
|
+
},
|
|
118
|
+
tool_profile: "full"
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
121
|
function loadConfig() {
|
|
@@ -185,7 +186,8 @@ function loadConfig() {
|
|
|
185
186
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
186
187
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
187
188
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
188
|
-
}
|
|
189
|
+
},
|
|
190
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
189
191
|
};
|
|
190
192
|
}
|
|
191
193
|
function saveConfig(config) {
|
|
@@ -240,6 +242,11 @@ function asObserverMode(value, fallback) {
|
|
|
240
242
|
return value;
|
|
241
243
|
return fallback;
|
|
242
244
|
}
|
|
245
|
+
function asToolProfile(value, fallback) {
|
|
246
|
+
if (value === "full" || value === "memory")
|
|
247
|
+
return value;
|
|
248
|
+
return fallback;
|
|
249
|
+
}
|
|
243
250
|
function asTeams(value, fallback) {
|
|
244
251
|
if (!Array.isArray(value))
|
|
245
252
|
return fallback;
|
|
@@ -873,6 +880,7 @@ function ensureObservationTypes(db) {
|
|
|
873
880
|
DROP TABLE observations;
|
|
874
881
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
875
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);
|
|
876
884
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
877
885
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
878
886
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1153,6 +1161,7 @@ class MemDatabase {
|
|
|
1153
1161
|
this.db = openDatabase(dbPath);
|
|
1154
1162
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1155
1163
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1164
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1156
1165
|
this.vecAvailable = this.loadVecExtension();
|
|
1157
1166
|
runMigrations(this.db);
|
|
1158
1167
|
ensureObservationTypes(this.db);
|
|
@@ -1174,8 +1183,16 @@ class MemDatabase {
|
|
|
1174
1183
|
this.db.close();
|
|
1175
1184
|
}
|
|
1176
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
|
+
}
|
|
1177
1194
|
const now = Math.floor(Date.now() / 1000);
|
|
1178
|
-
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);
|
|
1179
1196
|
if (existing) {
|
|
1180
1197
|
this.db.query(`UPDATE projects SET
|
|
1181
1198
|
local_path = COALESCE(?, local_path),
|
|
@@ -1190,7 +1207,7 @@ class MemDatabase {
|
|
|
1190
1207
|
};
|
|
1191
1208
|
}
|
|
1192
1209
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1193
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1210
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1194
1211
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1195
1212
|
}
|
|
1196
1213
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -1883,6 +1900,33 @@ function getOutboxStats(db) {
|
|
|
1883
1900
|
}
|
|
1884
1901
|
return stats;
|
|
1885
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
|
+
}
|
|
1886
1930
|
|
|
1887
1931
|
// src/intelligence/value-signals.ts
|
|
1888
1932
|
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
@@ -2468,7 +2512,7 @@ async function provision(baseUrl, request) {
|
|
|
2468
2512
|
import { randomBytes } from "node:crypto";
|
|
2469
2513
|
import { createServer } from "node:http";
|
|
2470
2514
|
import { execFile } from "node:child_process";
|
|
2471
|
-
var CALLBACK_TIMEOUT_MS =
|
|
2515
|
+
var CALLBACK_TIMEOUT_MS = 600000;
|
|
2472
2516
|
async function runBrowserAuth(candengoUrl) {
|
|
2473
2517
|
const state = randomBytes(16).toString("hex");
|
|
2474
2518
|
const { port, waitForCallback, stop } = await startCallbackServer(state);
|
|
@@ -2803,13 +2847,16 @@ function registerCodexHooks() {
|
|
|
2803
2847
|
return { path: CODEX_HOOKS, added: true };
|
|
2804
2848
|
}
|
|
2805
2849
|
function registerOpenCode() {
|
|
2850
|
+
const runtime = findRuntime();
|
|
2806
2851
|
const root = findPackageRoot();
|
|
2852
|
+
const dist = isBuiltDist();
|
|
2807
2853
|
const pluginSource = join2(root, "opencode", "plugin", "engrm-opencode.js");
|
|
2854
|
+
const serverPath = dist ? join2(root, "dist", "server.js") : join2(root, "src", "server.ts");
|
|
2808
2855
|
const config = readJsonFile(OPENCODE_CONFIG);
|
|
2809
2856
|
const mcp = config["mcp"] ?? {};
|
|
2810
2857
|
mcp["engrm"] = {
|
|
2811
2858
|
type: "local",
|
|
2812
|
-
command: ["
|
|
2859
|
+
command: dist ? [runtime, serverPath] : [runtime, "run", serverPath],
|
|
2813
2860
|
enabled: true,
|
|
2814
2861
|
timeout: 5000
|
|
2815
2862
|
};
|
|
@@ -3319,10 +3366,11 @@ function readProjectConfigFile(directory) {
|
|
|
3319
3366
|
}
|
|
3320
3367
|
}
|
|
3321
3368
|
function detectProject(directory) {
|
|
3322
|
-
const
|
|
3369
|
+
const resolvedDirectory = resolve(directory);
|
|
3370
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
3323
3371
|
if (remoteUrl) {
|
|
3324
3372
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
3325
|
-
const repoRoot = getGitTopLevel(
|
|
3373
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
3326
3374
|
return {
|
|
3327
3375
|
canonical_id: canonicalId,
|
|
3328
3376
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -3330,21 +3378,22 @@ function detectProject(directory) {
|
|
|
3330
3378
|
local_path: repoRoot
|
|
3331
3379
|
};
|
|
3332
3380
|
}
|
|
3333
|
-
const configFile = readProjectConfigFile(
|
|
3381
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
3334
3382
|
if (configFile) {
|
|
3335
3383
|
return {
|
|
3336
3384
|
canonical_id: configFile.project_id,
|
|
3337
3385
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
3338
3386
|
remote_url: null,
|
|
3339
|
-
local_path:
|
|
3387
|
+
local_path: resolvedDirectory
|
|
3340
3388
|
};
|
|
3341
3389
|
}
|
|
3342
|
-
const dirName = basename(
|
|
3390
|
+
const dirName = basename(resolvedDirectory);
|
|
3391
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
3343
3392
|
return {
|
|
3344
|
-
canonical_id: `local/${
|
|
3345
|
-
name:
|
|
3393
|
+
canonical_id: `local/${safeDirName}`,
|
|
3394
|
+
name: safeDirName,
|
|
3346
3395
|
remote_url: null,
|
|
3347
|
-
local_path:
|
|
3396
|
+
local_path: resolvedDirectory
|
|
3348
3397
|
};
|
|
3349
3398
|
}
|
|
3350
3399
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -4006,7 +4055,8 @@ function createDefaultConfig2() {
|
|
|
4006
4055
|
project_name: "shared-experience",
|
|
4007
4056
|
namespace: "",
|
|
4008
4057
|
api_key: ""
|
|
4009
|
-
}
|
|
4058
|
+
},
|
|
4059
|
+
tool_profile: "full"
|
|
4010
4060
|
};
|
|
4011
4061
|
}
|
|
4012
4062
|
function loadConfig2() {
|
|
@@ -4077,7 +4127,8 @@ function loadConfig2() {
|
|
|
4077
4127
|
project_name: asString2(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
4078
4128
|
namespace: asString2(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
4079
4129
|
api_key: asString2(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
4080
|
-
}
|
|
4130
|
+
},
|
|
4131
|
+
tool_profile: asToolProfile2(config["tool_profile"], defaults.tool_profile)
|
|
4081
4132
|
};
|
|
4082
4133
|
}
|
|
4083
4134
|
function saveConfig2(config) {
|
|
@@ -4132,12 +4183,35 @@ function asObserverMode2(value, fallback) {
|
|
|
4132
4183
|
return value;
|
|
4133
4184
|
return fallback;
|
|
4134
4185
|
}
|
|
4186
|
+
function asToolProfile2(value, fallback) {
|
|
4187
|
+
if (value === "full" || value === "memory")
|
|
4188
|
+
return value;
|
|
4189
|
+
return fallback;
|
|
4190
|
+
}
|
|
4135
4191
|
function asTeams2(value, fallback) {
|
|
4136
4192
|
if (!Array.isArray(value))
|
|
4137
4193
|
return fallback;
|
|
4138
4194
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
4139
4195
|
}
|
|
4140
4196
|
|
|
4197
|
+
// src/tool-profiles.ts
|
|
4198
|
+
var MEMORY_PROFILE_TOOLS = [
|
|
4199
|
+
"save_observation",
|
|
4200
|
+
"search_recall",
|
|
4201
|
+
"resume_thread",
|
|
4202
|
+
"list_recall_items",
|
|
4203
|
+
"load_recall_item",
|
|
4204
|
+
"recent_chat",
|
|
4205
|
+
"search_chat",
|
|
4206
|
+
"refresh_chat_recall",
|
|
4207
|
+
"repair_recall"
|
|
4208
|
+
];
|
|
4209
|
+
function getEnabledToolNames(profile) {
|
|
4210
|
+
if (!profile || profile === "full")
|
|
4211
|
+
return null;
|
|
4212
|
+
return new Set(MEMORY_PROFILE_TOOLS);
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4141
4215
|
// src/tools/capture-status.ts
|
|
4142
4216
|
var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
|
|
4143
4217
|
function getCaptureStatus(db, input = {}) {
|
|
@@ -4242,6 +4316,8 @@ function getCaptureStatus(db, input = {}) {
|
|
|
4242
4316
|
http_enabled: Boolean(config?.http?.enabled || process.env.ENGRM_HTTP_PORT),
|
|
4243
4317
|
http_port: config?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
|
|
4244
4318
|
http_bearer_token_count: config?.http?.bearer_tokens?.length ?? 0,
|
|
4319
|
+
tool_profile: config?.tool_profile ?? "full",
|
|
4320
|
+
enabled_tool_count: config ? getEnabledToolNames(config.tool_profile)?.size ?? null : null,
|
|
4245
4321
|
fleet_project_name: config?.fleet?.project_name ?? null,
|
|
4246
4322
|
fleet_configured: Boolean(config?.fleet?.namespace && config?.fleet?.api_key),
|
|
4247
4323
|
claude_mcp_registered: claudeMcpRegistered,
|
|
@@ -4289,6 +4365,117 @@ function hasOpenClawMcpRegistration(content) {
|
|
|
4289
4365
|
}
|
|
4290
4366
|
}
|
|
4291
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
|
+
|
|
4292
4479
|
// src/sync/auth.ts
|
|
4293
4480
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
4294
4481
|
function normalizeBaseUrl(url) {
|
|
@@ -4305,6 +4492,46 @@ function normalizeBaseUrl(url) {
|
|
|
4305
4492
|
return trimmed.replace(/\/$/, "");
|
|
4306
4493
|
}
|
|
4307
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
|
+
}
|
|
4308
4535
|
|
|
4309
4536
|
// src/cli.ts
|
|
4310
4537
|
var LEGACY_CODEX_SERVER_NAME3 = `candengo-${"mem"}`;
|
|
@@ -4316,6 +4543,9 @@ switch (command) {
|
|
|
4316
4543
|
case "init":
|
|
4317
4544
|
await handleInit(args.slice(1));
|
|
4318
4545
|
break;
|
|
4546
|
+
case "serve":
|
|
4547
|
+
await handleServe();
|
|
4548
|
+
break;
|
|
4319
4549
|
case "status":
|
|
4320
4550
|
handleStatus();
|
|
4321
4551
|
break;
|
|
@@ -4381,6 +4611,12 @@ async function handleInit(flags) {
|
|
|
4381
4611
|
await initWithBrowser(url);
|
|
4382
4612
|
await maybeInstallPack(flags);
|
|
4383
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
|
+
}
|
|
4384
4620
|
async function maybeInstallPack(flags) {
|
|
4385
4621
|
const packFlag = flags.find((f) => f.startsWith("--pack"));
|
|
4386
4622
|
if (!packFlag)
|
|
@@ -4476,11 +4712,27 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4476
4712
|
ensureConfigDir();
|
|
4477
4713
|
let existingDeviceId;
|
|
4478
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;
|
|
4479
4723
|
if (configExists()) {
|
|
4480
4724
|
try {
|
|
4481
4725
|
const existing = loadConfig();
|
|
4482
4726
|
existingDeviceId = existing.device_id;
|
|
4483
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;
|
|
4484
4736
|
} catch {}
|
|
4485
4737
|
}
|
|
4486
4738
|
const config = {
|
|
@@ -4492,17 +4744,17 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4492
4744
|
user_email: result.user_email,
|
|
4493
4745
|
device_id: existingDeviceId || generateDeviceId3(),
|
|
4494
4746
|
teams: result.teams ?? [],
|
|
4495
|
-
sync: {
|
|
4747
|
+
sync: existingSync ?? {
|
|
4496
4748
|
enabled: true,
|
|
4497
4749
|
interval_seconds: 30,
|
|
4498
4750
|
batch_size: 50
|
|
4499
4751
|
},
|
|
4500
|
-
search: {
|
|
4752
|
+
search: existingSearch ?? {
|
|
4501
4753
|
default_limit: 10,
|
|
4502
4754
|
local_boost: 1.2,
|
|
4503
4755
|
scope: "all"
|
|
4504
4756
|
},
|
|
4505
|
-
scrubbing: {
|
|
4757
|
+
scrubbing: existingScrubbing ?? {
|
|
4506
4758
|
enabled: true,
|
|
4507
4759
|
custom_patterns: [],
|
|
4508
4760
|
default_sensitivity: "shared"
|
|
@@ -4518,17 +4770,29 @@ function writeConfigFromProvision(baseUrl, result) {
|
|
|
4518
4770
|
daily_limit: 100,
|
|
4519
4771
|
tier: "free"
|
|
4520
4772
|
},
|
|
4521
|
-
observer: {
|
|
4773
|
+
observer: existingObserver ?? {
|
|
4522
4774
|
enabled: true,
|
|
4523
4775
|
mode: "per_event",
|
|
4524
4776
|
model: "haiku"
|
|
4525
4777
|
},
|
|
4526
|
-
transcript_analysis: {
|
|
4778
|
+
transcript_analysis: existingTranscriptAnalysis ?? {
|
|
4527
4779
|
enabled: false
|
|
4528
|
-
}
|
|
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"
|
|
4529
4792
|
};
|
|
4530
4793
|
saveConfig(config);
|
|
4531
4794
|
const db = new MemDatabase(getDbPath());
|
|
4795
|
+
recoverOutboxAfterSuccessfulAuth(db, config);
|
|
4532
4796
|
db.close();
|
|
4533
4797
|
console.log(`Configuration saved to ${getSettingsPath()}`);
|
|
4534
4798
|
console.log(`Database initialised at ${getDbPath()}`);
|
|
@@ -4724,6 +4988,7 @@ function handleStatus() {
|
|
|
4724
4988
|
console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
|
|
4725
4989
|
console.log(` HTTP MCP: ${config.http.enabled ? `enabled (:${config.http.port})` : "disabled"}`);
|
|
4726
4990
|
console.log(` HTTP tokens: ${config.http.bearer_tokens.length}`);
|
|
4991
|
+
console.log(` Tool profile: ${config.tool_profile ?? "full"}`);
|
|
4727
4992
|
console.log(` Fleet project: ${config.fleet.project_name || "(not set)"}`);
|
|
4728
4993
|
console.log(` Fleet sync: ${config.fleet.namespace && config.fleet.api_key ? "configured" : "not configured"}`);
|
|
4729
4994
|
const claudeJson = join8(homedir5(), ".claude.json");
|
|
@@ -4843,7 +5108,7 @@ function handleStatus() {
|
|
|
4843
5108
|
}
|
|
4844
5109
|
console.log(` Value: ${signalParts.join(", ")}`);
|
|
4845
5110
|
if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
|
|
4846
|
-
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"}`);
|
|
4847
5112
|
}
|
|
4848
5113
|
} catch {}
|
|
4849
5114
|
try {
|
|
@@ -4864,6 +5129,11 @@ function handleStatus() {
|
|
|
4864
5129
|
console.log(`
|
|
4865
5130
|
Sync`);
|
|
4866
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
|
+
}
|
|
4867
5137
|
try {
|
|
4868
5138
|
const lastPush = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_push_epoch");
|
|
4869
5139
|
const lastPull = db.db.query("SELECT value FROM sync_state WHERE key = ?").get("last_pull_epoch");
|
|
@@ -4944,7 +5214,7 @@ function generateDeviceId3() {
|
|
|
4944
5214
|
break;
|
|
4945
5215
|
}
|
|
4946
5216
|
const material = `${host}:${mac || "no-mac"}`;
|
|
4947
|
-
const suffix =
|
|
5217
|
+
const suffix = createHash5("sha256").update(material).digest("hex").slice(0, 8);
|
|
4948
5218
|
return `${host}-${suffix}`;
|
|
4949
5219
|
}
|
|
4950
5220
|
async function handleInstallPack(flags) {
|
|
@@ -5083,6 +5353,7 @@ async function handleDoctor() {
|
|
|
5083
5353
|
} else {
|
|
5084
5354
|
info("HTTP MCP disabled");
|
|
5085
5355
|
}
|
|
5356
|
+
info(`Tool profile: ${config.tool_profile ?? "full"}`);
|
|
5086
5357
|
if (config.fleet.project_name) {
|
|
5087
5358
|
if (config.fleet.namespace && config.fleet.api_key) {
|
|
5088
5359
|
pass(`Fleet project '${config.fleet.project_name}' is configured`);
|
|
@@ -5357,7 +5628,7 @@ async function handleDoctor() {
|
|
|
5357
5628
|
const dbPath = getDbPath();
|
|
5358
5629
|
if (existsSync8(dbPath)) {
|
|
5359
5630
|
const stats = statSync(dbPath);
|
|
5360
|
-
const sizeMB = stats.size /
|
|
5631
|
+
const sizeMB = stats.size / 1048576;
|
|
5361
5632
|
const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
|
|
5362
5633
|
info(`Database size: ${sizeStr}`);
|
|
5363
5634
|
}
|
|
@@ -5493,7 +5764,7 @@ And add to ~/.config/opencode/opencode.json:`);
|
|
|
5493
5764
|
"mcp": {
|
|
5494
5765
|
"engrm": {
|
|
5495
5766
|
"type": "local",
|
|
5496
|
-
"command": ["
|
|
5767
|
+
"command": ${JSON.stringify(IS_BUILT_DIST ? [process.execPath, join8(packageRoot, "dist", "server.js")] : ["bun", "run", join8(packageRoot, "src", "server.ts")])},
|
|
5497
5768
|
"enabled": true,
|
|
5498
5769
|
"timeout": 5000
|
|
5499
5770
|
}
|
|
@@ -5522,6 +5793,7 @@ function printUsage() {
|
|
|
5522
5793
|
console.log(`Engrm \u2014 Memory layer for AI coding agents
|
|
5523
5794
|
`);
|
|
5524
5795
|
console.log("Usage:");
|
|
5796
|
+
console.log(" engrm serve Run the MCP server over stdio");
|
|
5525
5797
|
console.log(" engrm init Setup via browser (recommended)");
|
|
5526
5798
|
console.log(" engrm init --token=cmt_xxx Setup from provisioning token");
|
|
5527
5799
|
console.log(" engrm init --pack=<name> Setup + install a starter pack");
|