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
|
@@ -91,10 +91,11 @@ function readProjectConfigFile(directory) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
function detectProject(directory) {
|
|
94
|
-
const
|
|
94
|
+
const resolvedDirectory = resolve(directory);
|
|
95
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
95
96
|
if (remoteUrl) {
|
|
96
97
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
97
|
-
const repoRoot = getGitTopLevel(
|
|
98
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
98
99
|
return {
|
|
99
100
|
canonical_id: canonicalId,
|
|
100
101
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -102,21 +103,22 @@ function detectProject(directory) {
|
|
|
102
103
|
local_path: repoRoot
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
|
-
const configFile = readProjectConfigFile(
|
|
106
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
106
107
|
if (configFile) {
|
|
107
108
|
return {
|
|
108
109
|
canonical_id: configFile.project_id,
|
|
109
110
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
110
111
|
remote_url: null,
|
|
111
|
-
local_path:
|
|
112
|
+
local_path: resolvedDirectory
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
|
-
const dirName = basename(
|
|
115
|
+
const dirName = basename(resolvedDirectory);
|
|
116
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
115
117
|
return {
|
|
116
|
-
canonical_id: `local/${
|
|
117
|
-
name:
|
|
118
|
+
canonical_id: `local/${safeDirName}`,
|
|
119
|
+
name: safeDirName,
|
|
118
120
|
remote_url: null,
|
|
119
|
-
local_path:
|
|
121
|
+
local_path: resolvedDirectory
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -230,7 +232,8 @@ function createDefaultConfig() {
|
|
|
230
232
|
project_name: "shared-experience",
|
|
231
233
|
namespace: "",
|
|
232
234
|
api_key: ""
|
|
233
|
-
}
|
|
235
|
+
},
|
|
236
|
+
tool_profile: "full"
|
|
234
237
|
};
|
|
235
238
|
}
|
|
236
239
|
function loadConfig() {
|
|
@@ -301,7 +304,8 @@ function loadConfig() {
|
|
|
301
304
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
302
305
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
303
306
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
304
|
-
}
|
|
307
|
+
},
|
|
308
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
305
309
|
};
|
|
306
310
|
}
|
|
307
311
|
function saveConfig(config) {
|
|
@@ -356,6 +360,11 @@ function asObserverMode(value, fallback) {
|
|
|
356
360
|
return value;
|
|
357
361
|
return fallback;
|
|
358
362
|
}
|
|
363
|
+
function asToolProfile(value, fallback) {
|
|
364
|
+
if (value === "full" || value === "memory")
|
|
365
|
+
return value;
|
|
366
|
+
return fallback;
|
|
367
|
+
}
|
|
359
368
|
function asTeams(value, fallback) {
|
|
360
369
|
if (!Array.isArray(value))
|
|
361
370
|
return fallback;
|
|
@@ -989,6 +998,7 @@ function ensureObservationTypes(db) {
|
|
|
989
998
|
DROP TABLE observations;
|
|
990
999
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
991
1000
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
1001
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
992
1002
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
993
1003
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
994
1004
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1269,6 +1279,7 @@ class MemDatabase {
|
|
|
1269
1279
|
this.db = openDatabase(dbPath);
|
|
1270
1280
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1271
1281
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1282
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1272
1283
|
this.vecAvailable = this.loadVecExtension();
|
|
1273
1284
|
runMigrations(this.db);
|
|
1274
1285
|
ensureObservationTypes(this.db);
|
|
@@ -1290,8 +1301,16 @@ class MemDatabase {
|
|
|
1290
1301
|
this.db.close();
|
|
1291
1302
|
}
|
|
1292
1303
|
upsertProject(project) {
|
|
1304
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1305
|
+
const name = project.name?.trim();
|
|
1306
|
+
if (!canonicalId) {
|
|
1307
|
+
throw new Error("Project canonical_id is required");
|
|
1308
|
+
}
|
|
1309
|
+
if (!name) {
|
|
1310
|
+
throw new Error("Project name is required");
|
|
1311
|
+
}
|
|
1293
1312
|
const now = Math.floor(Date.now() / 1000);
|
|
1294
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1313
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1295
1314
|
if (existing) {
|
|
1296
1315
|
this.db.query(`UPDATE projects SET
|
|
1297
1316
|
local_path = COALESCE(?, local_path),
|
|
@@ -1306,7 +1325,7 @@ class MemDatabase {
|
|
|
1306
1325
|
};
|
|
1307
1326
|
}
|
|
1308
1327
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1309
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1328
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1310
1329
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1311
1330
|
}
|
|
1312
1331
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2094,7 +2113,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
2094
2113
|
return null;
|
|
2095
2114
|
}
|
|
2096
2115
|
function compactLine(value) {
|
|
2097
|
-
|
|
2116
|
+
if (value === null || value === undefined)
|
|
2117
|
+
return null;
|
|
2118
|
+
let text;
|
|
2119
|
+
if (typeof value === "string") {
|
|
2120
|
+
text = value;
|
|
2121
|
+
} else {
|
|
2122
|
+
try {
|
|
2123
|
+
text = JSON.stringify(value);
|
|
2124
|
+
} catch {
|
|
2125
|
+
text = String(value);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
2098
2129
|
if (!trimmed)
|
|
2099
2130
|
return null;
|
|
2100
2131
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -3593,7 +3624,19 @@ function parseJsonArray3(value) {
|
|
|
3593
3624
|
}
|
|
3594
3625
|
}
|
|
3595
3626
|
function compactLine2(value) {
|
|
3596
|
-
|
|
3627
|
+
if (value === null || value === undefined)
|
|
3628
|
+
return null;
|
|
3629
|
+
let text;
|
|
3630
|
+
if (typeof value === "string") {
|
|
3631
|
+
text = value;
|
|
3632
|
+
} else {
|
|
3633
|
+
try {
|
|
3634
|
+
text = JSON.stringify(value);
|
|
3635
|
+
} catch {
|
|
3636
|
+
text = String(value);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
3597
3640
|
if (!trimmed)
|
|
3598
3641
|
return null;
|
|
3599
3642
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
package/dist/server.js
CHANGED
|
@@ -13643,7 +13643,8 @@ function createDefaultConfig() {
|
|
|
13643
13643
|
project_name: "shared-experience",
|
|
13644
13644
|
namespace: "",
|
|
13645
13645
|
api_key: ""
|
|
13646
|
-
}
|
|
13646
|
+
},
|
|
13647
|
+
tool_profile: "full"
|
|
13647
13648
|
};
|
|
13648
13649
|
}
|
|
13649
13650
|
function loadConfig() {
|
|
@@ -13714,7 +13715,8 @@ function loadConfig() {
|
|
|
13714
13715
|
project_name: asString(config2["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
13715
13716
|
namespace: asString(config2["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
13716
13717
|
api_key: asString(config2["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
13717
|
-
}
|
|
13718
|
+
},
|
|
13719
|
+
tool_profile: asToolProfile(config2["tool_profile"], defaults.tool_profile)
|
|
13718
13720
|
};
|
|
13719
13721
|
}
|
|
13720
13722
|
function saveConfig(config2) {
|
|
@@ -13769,6 +13771,11 @@ function asObserverMode(value, fallback) {
|
|
|
13769
13771
|
return value;
|
|
13770
13772
|
return fallback;
|
|
13771
13773
|
}
|
|
13774
|
+
function asToolProfile(value, fallback) {
|
|
13775
|
+
if (value === "full" || value === "memory")
|
|
13776
|
+
return value;
|
|
13777
|
+
return fallback;
|
|
13778
|
+
}
|
|
13772
13779
|
function asTeams(value, fallback) {
|
|
13773
13780
|
if (!Array.isArray(value))
|
|
13774
13781
|
return fallback;
|
|
@@ -14402,6 +14409,7 @@ function ensureObservationTypes(db) {
|
|
|
14402
14409
|
DROP TABLE observations;
|
|
14403
14410
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
14404
14411
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
14412
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
14405
14413
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
14406
14414
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
14407
14415
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -14682,6 +14690,7 @@ class MemDatabase {
|
|
|
14682
14690
|
this.db = openDatabase(dbPath);
|
|
14683
14691
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
14684
14692
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
14693
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
14685
14694
|
this.vecAvailable = this.loadVecExtension();
|
|
14686
14695
|
runMigrations(this.db);
|
|
14687
14696
|
ensureObservationTypes(this.db);
|
|
@@ -14703,8 +14712,16 @@ class MemDatabase {
|
|
|
14703
14712
|
this.db.close();
|
|
14704
14713
|
}
|
|
14705
14714
|
upsertProject(project) {
|
|
14715
|
+
const canonicalId = project.canonical_id?.trim();
|
|
14716
|
+
const name = project.name?.trim();
|
|
14717
|
+
if (!canonicalId) {
|
|
14718
|
+
throw new Error("Project canonical_id is required");
|
|
14719
|
+
}
|
|
14720
|
+
if (!name) {
|
|
14721
|
+
throw new Error("Project name is required");
|
|
14722
|
+
}
|
|
14706
14723
|
const now = Math.floor(Date.now() / 1000);
|
|
14707
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
14724
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
14708
14725
|
if (existing) {
|
|
14709
14726
|
this.db.query(`UPDATE projects SET
|
|
14710
14727
|
local_path = COALESCE(?, local_path),
|
|
@@ -14719,7 +14736,7 @@ class MemDatabase {
|
|
|
14719
14736
|
};
|
|
14720
14737
|
}
|
|
14721
14738
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
14722
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
14739
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
14723
14740
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
14724
14741
|
}
|
|
14725
14742
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -15815,10 +15832,11 @@ function readProjectConfigFile(directory) {
|
|
|
15815
15832
|
}
|
|
15816
15833
|
}
|
|
15817
15834
|
function detectProject(directory) {
|
|
15818
|
-
const
|
|
15835
|
+
const resolvedDirectory = resolve(directory);
|
|
15836
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
15819
15837
|
if (remoteUrl) {
|
|
15820
15838
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
15821
|
-
const repoRoot = getGitTopLevel(
|
|
15839
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
15822
15840
|
return {
|
|
15823
15841
|
canonical_id: canonicalId,
|
|
15824
15842
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -15826,21 +15844,22 @@ function detectProject(directory) {
|
|
|
15826
15844
|
local_path: repoRoot
|
|
15827
15845
|
};
|
|
15828
15846
|
}
|
|
15829
|
-
const configFile = readProjectConfigFile(
|
|
15847
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
15830
15848
|
if (configFile) {
|
|
15831
15849
|
return {
|
|
15832
15850
|
canonical_id: configFile.project_id,
|
|
15833
15851
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
15834
15852
|
remote_url: null,
|
|
15835
|
-
local_path:
|
|
15853
|
+
local_path: resolvedDirectory
|
|
15836
15854
|
};
|
|
15837
15855
|
}
|
|
15838
|
-
const dirName = basename(
|
|
15856
|
+
const dirName = basename(resolvedDirectory);
|
|
15857
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
15839
15858
|
return {
|
|
15840
|
-
canonical_id: `local/${
|
|
15841
|
-
name:
|
|
15859
|
+
canonical_id: `local/${safeDirName}`,
|
|
15860
|
+
name: safeDirName,
|
|
15842
15861
|
remote_url: null,
|
|
15843
|
-
local_path:
|
|
15862
|
+
local_path: resolvedDirectory
|
|
15844
15863
|
};
|
|
15845
15864
|
}
|
|
15846
15865
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -17530,7 +17549,19 @@ function formatTimestamp(nowMs) {
|
|
|
17530
17549
|
return `${yyyy}-${mm}-${dd} ${hh}:${mi}Z`;
|
|
17531
17550
|
}
|
|
17532
17551
|
function compactLine(value) {
|
|
17533
|
-
|
|
17552
|
+
if (value === null || value === undefined)
|
|
17553
|
+
return null;
|
|
17554
|
+
let text;
|
|
17555
|
+
if (typeof value === "string") {
|
|
17556
|
+
text = value;
|
|
17557
|
+
} else {
|
|
17558
|
+
try {
|
|
17559
|
+
text = JSON.stringify(value);
|
|
17560
|
+
} catch {
|
|
17561
|
+
text = String(value);
|
|
17562
|
+
}
|
|
17563
|
+
}
|
|
17564
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
17534
17565
|
if (!trimmed)
|
|
17535
17566
|
return null;
|
|
17536
17567
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -19471,6 +19502,26 @@ function getActivityFeed(db, input) {
|
|
|
19471
19502
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
19472
19503
|
import { homedir as homedir2 } from "node:os";
|
|
19473
19504
|
import { join as join3 } from "node:path";
|
|
19505
|
+
|
|
19506
|
+
// src/tool-profiles.ts
|
|
19507
|
+
var MEMORY_PROFILE_TOOLS = [
|
|
19508
|
+
"save_observation",
|
|
19509
|
+
"search_recall",
|
|
19510
|
+
"resume_thread",
|
|
19511
|
+
"list_recall_items",
|
|
19512
|
+
"load_recall_item",
|
|
19513
|
+
"recent_chat",
|
|
19514
|
+
"search_chat",
|
|
19515
|
+
"refresh_chat_recall",
|
|
19516
|
+
"repair_recall"
|
|
19517
|
+
];
|
|
19518
|
+
function getEnabledToolNames(profile) {
|
|
19519
|
+
if (!profile || profile === "full")
|
|
19520
|
+
return null;
|
|
19521
|
+
return new Set(MEMORY_PROFILE_TOOLS);
|
|
19522
|
+
}
|
|
19523
|
+
|
|
19524
|
+
// src/tools/capture-status.ts
|
|
19474
19525
|
var LEGACY_CODEX_SERVER_NAME = `candengo-${"mem"}`;
|
|
19475
19526
|
function getCaptureStatus(db, input = {}) {
|
|
19476
19527
|
const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
|
|
@@ -19574,6 +19625,8 @@ function getCaptureStatus(db, input = {}) {
|
|
|
19574
19625
|
http_enabled: Boolean(config2?.http?.enabled || process.env.ENGRM_HTTP_PORT),
|
|
19575
19626
|
http_port: config2?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
|
|
19576
19627
|
http_bearer_token_count: config2?.http?.bearer_tokens?.length ?? 0,
|
|
19628
|
+
tool_profile: config2?.tool_profile ?? "full",
|
|
19629
|
+
enabled_tool_count: config2 ? getEnabledToolNames(config2.tool_profile)?.size ?? null : null,
|
|
19577
19630
|
fleet_project_name: config2?.fleet?.project_name ?? null,
|
|
19578
19631
|
fleet_configured: Boolean(config2?.fleet?.namespace && config2?.fleet?.api_key),
|
|
19579
19632
|
claude_mcp_registered: claudeMcpRegistered,
|
|
@@ -21188,11 +21241,12 @@ function getPendingEntries(db, limit = 50) {
|
|
|
21188
21241
|
LIMIT ?`).all(now, limit);
|
|
21189
21242
|
}
|
|
21190
21243
|
function markSyncing(db, entryId) {
|
|
21191
|
-
|
|
21244
|
+
const now = Math.floor(Date.now() / 1000);
|
|
21245
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
21192
21246
|
}
|
|
21193
21247
|
function markSynced(db, entryId) {
|
|
21194
21248
|
const now = Math.floor(Date.now() / 1000);
|
|
21195
|
-
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch =
|
|
21249
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
21196
21250
|
}
|
|
21197
21251
|
function markFailed(db, entryId, error48) {
|
|
21198
21252
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -21216,6 +21270,74 @@ function getOutboxStats(db) {
|
|
|
21216
21270
|
}
|
|
21217
21271
|
return stats;
|
|
21218
21272
|
}
|
|
21273
|
+
function getOutboxFailureSummaries(db, limit = 5) {
|
|
21274
|
+
return db.db.query(`SELECT COALESCE(last_error, '') as error, COUNT(*) as count
|
|
21275
|
+
FROM sync_outbox
|
|
21276
|
+
WHERE status = 'failed'
|
|
21277
|
+
GROUP BY COALESCE(last_error, '')
|
|
21278
|
+
ORDER BY count DESC, error ASC
|
|
21279
|
+
LIMIT ?`).all(limit).filter((row) => row.error.length > 0);
|
|
21280
|
+
}
|
|
21281
|
+
function classifyOutboxFailure(error48) {
|
|
21282
|
+
const normalized = error48.toLowerCase();
|
|
21283
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
21284
|
+
return "auth";
|
|
21285
|
+
}
|
|
21286
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
21287
|
+
return "rate_limit";
|
|
21288
|
+
}
|
|
21289
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
21290
|
+
return "timeout";
|
|
21291
|
+
}
|
|
21292
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
21293
|
+
return "network";
|
|
21294
|
+
}
|
|
21295
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
21296
|
+
return "validation";
|
|
21297
|
+
}
|
|
21298
|
+
return "other";
|
|
21299
|
+
}
|
|
21300
|
+
function resetFailedEntries(db) {
|
|
21301
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
21302
|
+
SET status = 'pending',
|
|
21303
|
+
retry_count = 0,
|
|
21304
|
+
last_error = NULL,
|
|
21305
|
+
next_retry_epoch = NULL
|
|
21306
|
+
WHERE status = 'failed'`).run();
|
|
21307
|
+
return result.changes;
|
|
21308
|
+
}
|
|
21309
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
21310
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
21311
|
+
FROM sync_outbox
|
|
21312
|
+
WHERE status = 'failed'`).all();
|
|
21313
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
21314
|
+
if (matchingIds.length === 0)
|
|
21315
|
+
return 0;
|
|
21316
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
21317
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
21318
|
+
SET status = 'pending',
|
|
21319
|
+
retry_count = 0,
|
|
21320
|
+
last_error = NULL,
|
|
21321
|
+
next_retry_epoch = NULL
|
|
21322
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
21323
|
+
return result.changes;
|
|
21324
|
+
}
|
|
21325
|
+
function resetSyncingEntries(db) {
|
|
21326
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
21327
|
+
SET status = 'pending',
|
|
21328
|
+
next_retry_epoch = NULL
|
|
21329
|
+
WHERE status = 'syncing'`).run();
|
|
21330
|
+
return result.changes;
|
|
21331
|
+
}
|
|
21332
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
21333
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
21334
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
21335
|
+
SET status = 'pending',
|
|
21336
|
+
next_retry_epoch = NULL
|
|
21337
|
+
WHERE status = 'syncing'
|
|
21338
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
21339
|
+
return result.changes;
|
|
21340
|
+
}
|
|
21219
21341
|
|
|
21220
21342
|
// src/intelligence/value-signals.ts
|
|
21221
21343
|
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
@@ -21352,7 +21474,12 @@ function getMemoryStats(db) {
|
|
|
21352
21474
|
recent_completed: insights.recent_completed,
|
|
21353
21475
|
next_steps: insights.next_steps,
|
|
21354
21476
|
installed_packs: db.getInstalledPacks(),
|
|
21355
|
-
outbox: getOutboxStats(db)
|
|
21477
|
+
outbox: getOutboxStats(db),
|
|
21478
|
+
outbox_failure_summary: getOutboxFailureSummaries(db).map((row) => ({
|
|
21479
|
+
category: classifyOutboxFailure(row.error),
|
|
21480
|
+
error: row.error,
|
|
21481
|
+
count: row.count
|
|
21482
|
+
}))
|
|
21356
21483
|
};
|
|
21357
21484
|
}
|
|
21358
21485
|
|
|
@@ -21535,6 +21662,7 @@ function isDue(db, key, interval, now) {
|
|
|
21535
21662
|
}
|
|
21536
21663
|
|
|
21537
21664
|
// src/sync/auth.ts
|
|
21665
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
21538
21666
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
21539
21667
|
function normalizeBaseUrl(url2) {
|
|
21540
21668
|
const trimmed = url2.trim();
|
|
@@ -21565,6 +21693,36 @@ function getBaseUrl(config2) {
|
|
|
21565
21693
|
}
|
|
21566
21694
|
return null;
|
|
21567
21695
|
}
|
|
21696
|
+
function getAuthFingerprint(config2) {
|
|
21697
|
+
const apiKey = getApiKey(config2);
|
|
21698
|
+
const baseUrl = getBaseUrl(config2);
|
|
21699
|
+
if (!apiKey || !baseUrl)
|
|
21700
|
+
return null;
|
|
21701
|
+
return createHash4("sha256").update(`${baseUrl}
|
|
21702
|
+
${apiKey}
|
|
21703
|
+
${config2.namespace}
|
|
21704
|
+
${config2.site_id}`).digest("hex");
|
|
21705
|
+
}
|
|
21706
|
+
function recoverOutboxAfterAuthChange(db, config2) {
|
|
21707
|
+
const fingerprint = getAuthFingerprint(config2);
|
|
21708
|
+
if (!fingerprint) {
|
|
21709
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
21710
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
21711
|
+
}
|
|
21712
|
+
const key = "sync_auth_fingerprint";
|
|
21713
|
+
const previous = db.getSyncState(key);
|
|
21714
|
+
const fingerprintChanged = previous !== fingerprint;
|
|
21715
|
+
if (!fingerprintChanged) {
|
|
21716
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error48) => classifyOutboxFailure(error48) === "auth");
|
|
21717
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
21718
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
21719
|
+
}
|
|
21720
|
+
const failedReset = resetFailedEntries(db);
|
|
21721
|
+
const syncingReset = resetSyncingEntries(db);
|
|
21722
|
+
const staleSyncingReset = 0;
|
|
21723
|
+
db.setSyncState(key, fingerprint);
|
|
21724
|
+
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
21725
|
+
}
|
|
21568
21726
|
function buildSourceId(config2, localId, type = "obs") {
|
|
21569
21727
|
return `${config2.user_id}-${config2.device_id}-${type}-${localId}`;
|
|
21570
21728
|
}
|
|
@@ -21598,6 +21756,7 @@ class VectorClient {
|
|
|
21598
21756
|
apiKey;
|
|
21599
21757
|
siteId;
|
|
21600
21758
|
namespace;
|
|
21759
|
+
timeoutMs;
|
|
21601
21760
|
constructor(config2, overrides = {}) {
|
|
21602
21761
|
const baseUrl = getBaseUrl(config2);
|
|
21603
21762
|
const apiKey = overrides.apiKey ?? getApiKey(config2);
|
|
@@ -21608,6 +21767,7 @@ class VectorClient {
|
|
|
21608
21767
|
this.apiKey = apiKey;
|
|
21609
21768
|
this.siteId = overrides.siteId ?? config2.site_id;
|
|
21610
21769
|
this.namespace = overrides.namespace ?? config2.namespace;
|
|
21770
|
+
this.timeoutMs = overrides.timeoutMs ?? 1e4;
|
|
21611
21771
|
}
|
|
21612
21772
|
static isConfigured(config2) {
|
|
21613
21773
|
return getApiKey(config2) !== null && getBaseUrl(config2) !== null;
|
|
@@ -21670,6 +21830,7 @@ class VectorClient {
|
|
|
21670
21830
|
if (body && method !== "GET") {
|
|
21671
21831
|
init.body = JSON.stringify(body);
|
|
21672
21832
|
}
|
|
21833
|
+
init.signal = AbortSignal.timeout(this.timeoutMs);
|
|
21673
21834
|
const response = await fetch(url2, init);
|
|
21674
21835
|
if (!response.ok) {
|
|
21675
21836
|
const text = await response.text().catch(() => "");
|
|
@@ -21750,7 +21911,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
21750
21911
|
return null;
|
|
21751
21912
|
}
|
|
21752
21913
|
function compactLine2(value) {
|
|
21753
|
-
|
|
21914
|
+
if (value === null || value === undefined)
|
|
21915
|
+
return null;
|
|
21916
|
+
let text;
|
|
21917
|
+
if (typeof value === "string") {
|
|
21918
|
+
text = value;
|
|
21919
|
+
} else {
|
|
21920
|
+
try {
|
|
21921
|
+
text = JSON.stringify(value);
|
|
21922
|
+
} catch {
|
|
21923
|
+
text = String(value);
|
|
21924
|
+
}
|
|
21925
|
+
}
|
|
21926
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
21754
21927
|
if (!trimmed)
|
|
21755
21928
|
return null;
|
|
21756
21929
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -21919,7 +22092,7 @@ function buildSummaryVectorDocument(summary, config2, project, targetOrObservati
|
|
|
21919
22092
|
}
|
|
21920
22093
|
};
|
|
21921
22094
|
}
|
|
21922
|
-
async function pushOutbox(db, config2, batchSize = 50) {
|
|
22095
|
+
async function pushOutbox(db, config2, batchSize = 50, options = {}) {
|
|
21923
22096
|
const entries = getPendingEntries(db, batchSize);
|
|
21924
22097
|
let pushed = 0;
|
|
21925
22098
|
let failed = 0;
|
|
@@ -22030,7 +22203,8 @@ async function pushOutbox(db, config2, batchSize = 50) {
|
|
|
22030
22203
|
const client = new VectorClient(config2, {
|
|
22031
22204
|
apiKey: target.apiKey,
|
|
22032
22205
|
namespace: target.namespace,
|
|
22033
|
-
siteId: target.siteId
|
|
22206
|
+
siteId: target.siteId,
|
|
22207
|
+
timeoutMs: options.timeoutMs
|
|
22034
22208
|
});
|
|
22035
22209
|
try {
|
|
22036
22210
|
await client.batchIngest(items.map((b) => b.doc));
|
|
@@ -22342,6 +22516,9 @@ async function pullSettings(client, config2) {
|
|
|
22342
22516
|
|
|
22343
22517
|
// src/sync/engine.ts
|
|
22344
22518
|
var DEFAULT_PULL_INTERVAL = 60000;
|
|
22519
|
+
function recordNowEpoch(db, key) {
|
|
22520
|
+
db.setSyncState(key, String(Math.floor(Date.now() / 1000)));
|
|
22521
|
+
}
|
|
22345
22522
|
|
|
22346
22523
|
class SyncEngine {
|
|
22347
22524
|
db;
|
|
@@ -22400,7 +22577,11 @@ class SyncEngine {
|
|
|
22400
22577
|
return;
|
|
22401
22578
|
this._pushing = true;
|
|
22402
22579
|
try {
|
|
22403
|
-
|
|
22580
|
+
recoverOutboxAfterAuthChange(this.db, this.config);
|
|
22581
|
+
const result = await pushOutbox(this.db, this.config, this.config.sync.batch_size);
|
|
22582
|
+
if (result.pushed > 0) {
|
|
22583
|
+
recordNowEpoch(this.db, "last_push_epoch");
|
|
22584
|
+
}
|
|
22404
22585
|
} finally {
|
|
22405
22586
|
this._pushing = false;
|
|
22406
22587
|
}
|
|
@@ -22410,11 +22591,16 @@ class SyncEngine {
|
|
|
22410
22591
|
return;
|
|
22411
22592
|
this._pulling = true;
|
|
22412
22593
|
try {
|
|
22413
|
-
await pullFromVector(this.db, this.client, this.config);
|
|
22594
|
+
const primary = await pullFromVector(this.db, this.client, this.config);
|
|
22595
|
+
let totalReceived = primary.received;
|
|
22414
22596
|
if (this.fleetClient) {
|
|
22415
|
-
await pullFromVector(this.db, this.fleetClient, this.config);
|
|
22597
|
+
const fleet = await pullFromVector(this.db, this.fleetClient, this.config);
|
|
22598
|
+
totalReceived += fleet.received;
|
|
22416
22599
|
}
|
|
22417
22600
|
await pullSettings(this.client, this.config);
|
|
22601
|
+
if (totalReceived > 0) {
|
|
22602
|
+
recordNowEpoch(this.db, "last_pull_epoch");
|
|
22603
|
+
}
|
|
22418
22604
|
} finally {
|
|
22419
22605
|
this._pulling = false;
|
|
22420
22606
|
}
|
|
@@ -23070,21 +23256,50 @@ function resolveAgentName(clientName) {
|
|
|
23070
23256
|
return clientName;
|
|
23071
23257
|
}
|
|
23072
23258
|
var syncEngine = null;
|
|
23073
|
-
|
|
23074
|
-
|
|
23075
|
-
|
|
23076
|
-
|
|
23077
|
-
|
|
23078
|
-
process.on("SIGTERM", () => {
|
|
23259
|
+
var shuttingDown = false;
|
|
23260
|
+
function shutdown(code = 0) {
|
|
23261
|
+
if (shuttingDown)
|
|
23262
|
+
return;
|
|
23263
|
+
shuttingDown = true;
|
|
23079
23264
|
syncEngine?.stop();
|
|
23080
23265
|
db.close();
|
|
23081
|
-
process.exit(
|
|
23082
|
-
}
|
|
23266
|
+
process.exit(code);
|
|
23267
|
+
}
|
|
23268
|
+
process.on("SIGINT", () => shutdown(0));
|
|
23269
|
+
process.on("SIGTERM", () => shutdown(0));
|
|
23270
|
+
function installStdioLivenessGuards() {
|
|
23271
|
+
process.stdin.on("end", () => shutdown(0));
|
|
23272
|
+
process.stdin.on("close", () => shutdown(0));
|
|
23273
|
+
process.stdin.on("error", () => shutdown(0));
|
|
23274
|
+
process.stdin.resume();
|
|
23275
|
+
const parentPid = process.ppid;
|
|
23276
|
+
if (!Number.isInteger(parentPid) || parentPid <= 1)
|
|
23277
|
+
return;
|
|
23278
|
+
const heartbeat = setInterval(() => {
|
|
23279
|
+
try {
|
|
23280
|
+
process.kill(parentPid, 0);
|
|
23281
|
+
} catch {
|
|
23282
|
+
shutdown(0);
|
|
23283
|
+
}
|
|
23284
|
+
if (process.ppid === 1) {
|
|
23285
|
+
shutdown(0);
|
|
23286
|
+
}
|
|
23287
|
+
}, 30000);
|
|
23288
|
+
heartbeat.unref();
|
|
23289
|
+
}
|
|
23083
23290
|
function buildServer() {
|
|
23084
23291
|
const server = new McpServer({
|
|
23085
23292
|
name: "engrm",
|
|
23086
|
-
version: "0.4.
|
|
23293
|
+
version: "0.4.44"
|
|
23087
23294
|
});
|
|
23295
|
+
const enabledToolNames = getEnabledToolNames(config2.tool_profile);
|
|
23296
|
+
const originalTool = server.tool.bind(server);
|
|
23297
|
+
server.tool = (name, ...args) => {
|
|
23298
|
+
if (enabledToolNames && !enabledToolNames.has(name)) {
|
|
23299
|
+
return server;
|
|
23300
|
+
}
|
|
23301
|
+
return originalTool(name, ...args);
|
|
23302
|
+
};
|
|
23088
23303
|
server.tool("save_observation", "Directly save a durable memory item now. Use this when something should be remembered on purpose instead of waiting for an end-of-session digest.", {
|
|
23089
23304
|
type: exports_external.enum([
|
|
23090
23305
|
"bugfix",
|
|
@@ -24146,6 +24361,7 @@ ${observationLines}`
|
|
|
24146
24361
|
text: `Schema: v${result.schema_version} (${result.schema_current ? "current" : "outdated"})
|
|
24147
24362
|
` + `HTTP MCP: ${result.http_enabled ? `enabled${result.http_port ? ` (:${result.http_port})` : ""}` : "disabled"}
|
|
24148
24363
|
` + `HTTP bearer tokens: ${result.http_bearer_token_count}
|
|
24364
|
+
` + `Tool profile: ${result.tool_profile}${typeof result.enabled_tool_count === "number" ? ` (${result.enabled_tool_count} tools)` : ""}
|
|
24149
24365
|
` + `Fleet project: ${result.fleet_project_name ?? "none"}
|
|
24150
24366
|
` + `Fleet sync: ${result.fleet_configured ? "configured" : "not configured"}
|
|
24151
24367
|
|
|
@@ -25085,6 +25301,7 @@ async function main() {
|
|
|
25085
25301
|
await startHttpServer();
|
|
25086
25302
|
return;
|
|
25087
25303
|
}
|
|
25304
|
+
installStdioLivenessGuards();
|
|
25088
25305
|
const transport = new StdioServerTransport;
|
|
25089
25306
|
await server.connect(transport);
|
|
25090
25307
|
}
|
package/opencode/README.md
CHANGED
|
@@ -31,12 +31,12 @@ The MCP entry written by the helper script matches:
|
|
|
31
31
|
{
|
|
32
32
|
"$schema": "https://opencode.ai/config.json",
|
|
33
33
|
"mcp": {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
"engrm": {
|
|
35
|
+
"type": "local",
|
|
36
|
+
"command": ["node", "/absolute/path/to/engrm/dist/server.js"],
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"timeout": 5000
|
|
39
|
+
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
```
|