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
|
@@ -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) {
|
|
@@ -986,7 +988,8 @@ function createDefaultConfig() {
|
|
|
986
988
|
project_name: "shared-experience",
|
|
987
989
|
namespace: "",
|
|
988
990
|
api_key: ""
|
|
989
|
-
}
|
|
991
|
+
},
|
|
992
|
+
tool_profile: "full"
|
|
990
993
|
};
|
|
991
994
|
}
|
|
992
995
|
function loadConfig() {
|
|
@@ -1057,7 +1060,8 @@ function loadConfig() {
|
|
|
1057
1060
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
1058
1061
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
1059
1062
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
1060
|
-
}
|
|
1063
|
+
},
|
|
1064
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
1061
1065
|
};
|
|
1062
1066
|
}
|
|
1063
1067
|
function saveConfig(config) {
|
|
@@ -1112,6 +1116,11 @@ function asObserverMode(value, fallback) {
|
|
|
1112
1116
|
return value;
|
|
1113
1117
|
return fallback;
|
|
1114
1118
|
}
|
|
1119
|
+
function asToolProfile(value, fallback) {
|
|
1120
|
+
if (value === "full" || value === "memory")
|
|
1121
|
+
return value;
|
|
1122
|
+
return fallback;
|
|
1123
|
+
}
|
|
1115
1124
|
function asTeams(value, fallback) {
|
|
1116
1125
|
if (!Array.isArray(value))
|
|
1117
1126
|
return fallback;
|
|
@@ -1745,6 +1754,7 @@ function ensureObservationTypes(db) {
|
|
|
1745
1754
|
DROP TABLE observations;
|
|
1746
1755
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1747
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);
|
|
1748
1758
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1749
1759
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1750
1760
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -2025,6 +2035,7 @@ class MemDatabase {
|
|
|
2025
2035
|
this.db = openDatabase(dbPath);
|
|
2026
2036
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2027
2037
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
2038
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
2028
2039
|
this.vecAvailable = this.loadVecExtension();
|
|
2029
2040
|
runMigrations(this.db);
|
|
2030
2041
|
ensureObservationTypes(this.db);
|
|
@@ -2046,8 +2057,16 @@ class MemDatabase {
|
|
|
2046
2057
|
this.db.close();
|
|
2047
2058
|
}
|
|
2048
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
|
+
}
|
|
2049
2068
|
const now = Math.floor(Date.now() / 1000);
|
|
2050
|
-
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);
|
|
2051
2070
|
if (existing) {
|
|
2052
2071
|
this.db.query(`UPDATE projects SET
|
|
2053
2072
|
local_path = COALESCE(?, local_path),
|
|
@@ -2062,7 +2081,7 @@ class MemDatabase {
|
|
|
2062
2081
|
};
|
|
2063
2082
|
}
|
|
2064
2083
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
2065
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
2084
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
2066
2085
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
2067
2086
|
}
|
|
2068
2087
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -292,7 +292,8 @@ function createDefaultConfig() {
|
|
|
292
292
|
project_name: "shared-experience",
|
|
293
293
|
namespace: "",
|
|
294
294
|
api_key: ""
|
|
295
|
-
}
|
|
295
|
+
},
|
|
296
|
+
tool_profile: "full"
|
|
296
297
|
};
|
|
297
298
|
}
|
|
298
299
|
function loadConfig() {
|
|
@@ -363,7 +364,8 @@ function loadConfig() {
|
|
|
363
364
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
364
365
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
365
366
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
366
|
-
}
|
|
367
|
+
},
|
|
368
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
367
369
|
};
|
|
368
370
|
}
|
|
369
371
|
function saveConfig(config) {
|
|
@@ -418,6 +420,11 @@ function asObserverMode(value, fallback) {
|
|
|
418
420
|
return value;
|
|
419
421
|
return fallback;
|
|
420
422
|
}
|
|
423
|
+
function asToolProfile(value, fallback) {
|
|
424
|
+
if (value === "full" || value === "memory")
|
|
425
|
+
return value;
|
|
426
|
+
return fallback;
|
|
427
|
+
}
|
|
421
428
|
function asTeams(value, fallback) {
|
|
422
429
|
if (!Array.isArray(value))
|
|
423
430
|
return fallback;
|
|
@@ -1051,6 +1058,7 @@ function ensureObservationTypes(db) {
|
|
|
1051
1058
|
DROP TABLE observations;
|
|
1052
1059
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1053
1060
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
1061
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
1054
1062
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1055
1063
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1056
1064
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1331,6 +1339,7 @@ class MemDatabase {
|
|
|
1331
1339
|
this.db = openDatabase(dbPath);
|
|
1332
1340
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1333
1341
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1342
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1334
1343
|
this.vecAvailable = this.loadVecExtension();
|
|
1335
1344
|
runMigrations(this.db);
|
|
1336
1345
|
ensureObservationTypes(this.db);
|
|
@@ -1352,8 +1361,16 @@ class MemDatabase {
|
|
|
1352
1361
|
this.db.close();
|
|
1353
1362
|
}
|
|
1354
1363
|
upsertProject(project) {
|
|
1364
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1365
|
+
const name = project.name?.trim();
|
|
1366
|
+
if (!canonicalId) {
|
|
1367
|
+
throw new Error("Project canonical_id is required");
|
|
1368
|
+
}
|
|
1369
|
+
if (!name) {
|
|
1370
|
+
throw new Error("Project name is required");
|
|
1371
|
+
}
|
|
1355
1372
|
const now = Math.floor(Date.now() / 1000);
|
|
1356
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1373
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1357
1374
|
if (existing) {
|
|
1358
1375
|
this.db.query(`UPDATE projects SET
|
|
1359
1376
|
local_path = COALESCE(?, local_path),
|
|
@@ -1368,7 +1385,7 @@ class MemDatabase {
|
|
|
1368
1385
|
};
|
|
1369
1386
|
}
|
|
1370
1387
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1371
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1388
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1372
1389
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1373
1390
|
}
|
|
1374
1391
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2518,10 +2535,11 @@ function readProjectConfigFile(directory) {
|
|
|
2518
2535
|
}
|
|
2519
2536
|
}
|
|
2520
2537
|
function detectProject(directory) {
|
|
2521
|
-
const
|
|
2538
|
+
const resolvedDirectory = resolve(directory);
|
|
2539
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
2522
2540
|
if (remoteUrl) {
|
|
2523
2541
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2524
|
-
const repoRoot = getGitTopLevel(
|
|
2542
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
2525
2543
|
return {
|
|
2526
2544
|
canonical_id: canonicalId,
|
|
2527
2545
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -2529,21 +2547,22 @@ function detectProject(directory) {
|
|
|
2529
2547
|
local_path: repoRoot
|
|
2530
2548
|
};
|
|
2531
2549
|
}
|
|
2532
|
-
const configFile = readProjectConfigFile(
|
|
2550
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
2533
2551
|
if (configFile) {
|
|
2534
2552
|
return {
|
|
2535
2553
|
canonical_id: configFile.project_id,
|
|
2536
2554
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
2537
2555
|
remote_url: null,
|
|
2538
|
-
local_path:
|
|
2556
|
+
local_path: resolvedDirectory
|
|
2539
2557
|
};
|
|
2540
2558
|
}
|
|
2541
|
-
const dirName = basename(
|
|
2559
|
+
const dirName = basename(resolvedDirectory);
|
|
2560
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
2542
2561
|
return {
|
|
2543
|
-
canonical_id: `local/${
|
|
2544
|
-
name:
|
|
2562
|
+
canonical_id: `local/${safeDirName}`,
|
|
2563
|
+
name: safeDirName,
|
|
2545
2564
|
remote_url: null,
|
|
2546
|
-
local_path:
|
|
2565
|
+
local_path: resolvedDirectory
|
|
2547
2566
|
};
|
|
2548
2567
|
}
|
|
2549
2568
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3427,9 +3446,21 @@ function incrementObserverSaveCount(sessionId) {
|
|
|
3427
3446
|
// src/capture/recall.ts
|
|
3428
3447
|
var VEC_DISTANCE_THRESHOLD = 0.25;
|
|
3429
3448
|
function extractErrorSignature(output) {
|
|
3430
|
-
if (
|
|
3449
|
+
if (output === null || output === undefined)
|
|
3450
|
+
return null;
|
|
3451
|
+
let text;
|
|
3452
|
+
if (typeof output === "string") {
|
|
3453
|
+
text = output;
|
|
3454
|
+
} else {
|
|
3455
|
+
try {
|
|
3456
|
+
text = JSON.stringify(output);
|
|
3457
|
+
} catch {
|
|
3458
|
+
text = String(output);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (!text || text.length < 10)
|
|
3431
3462
|
return null;
|
|
3432
|
-
const lines =
|
|
3463
|
+
const lines = text.split(`
|
|
3433
3464
|
`);
|
|
3434
3465
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
3435
3466
|
const line = lines[i].trim();
|
|
@@ -3759,7 +3790,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
3759
3790
|
return null;
|
|
3760
3791
|
}
|
|
3761
3792
|
function compactLine(value) {
|
|
3762
|
-
|
|
3793
|
+
if (value === null || value === undefined)
|
|
3794
|
+
return null;
|
|
3795
|
+
let text;
|
|
3796
|
+
if (typeof value === "string") {
|
|
3797
|
+
text = value;
|
|
3798
|
+
} else {
|
|
3799
|
+
try {
|
|
3800
|
+
text = JSON.stringify(value);
|
|
3801
|
+
} catch {
|
|
3802
|
+
text = String(value);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
3763
3806
|
if (!trimmed)
|
|
3764
3807
|
return null;
|
|
3765
3808
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -4500,7 +4543,19 @@ function parseJsonArray3(value) {
|
|
|
4500
4543
|
}
|
|
4501
4544
|
}
|
|
4502
4545
|
function compactLine2(value) {
|
|
4503
|
-
|
|
4546
|
+
if (value === null || value === undefined)
|
|
4547
|
+
return null;
|
|
4548
|
+
let text;
|
|
4549
|
+
if (typeof value === "string") {
|
|
4550
|
+
text = value;
|
|
4551
|
+
} else {
|
|
4552
|
+
try {
|
|
4553
|
+
text = JSON.stringify(value);
|
|
4554
|
+
} catch {
|
|
4555
|
+
text = String(value);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
4504
4559
|
if (!trimmed)
|
|
4505
4560
|
return null;
|
|
4506
4561
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -4532,7 +4587,7 @@ async function main() {
|
|
|
4532
4587
|
db.setSyncState("hook_post_tool_last_payload_preview", truncatePreview(raw, 400) ?? "parsed");
|
|
4533
4588
|
try {
|
|
4534
4589
|
if (event.session_id) {
|
|
4535
|
-
persistRawToolChronology(event, config.user_id, config.device_id);
|
|
4590
|
+
persistRawToolChronology(db, event, config.user_id, config.device_id);
|
|
4536
4591
|
await syncTranscriptChat(db, config, event.session_id, event.cwd);
|
|
4537
4592
|
}
|
|
4538
4593
|
const textToScan = extractScanText(event);
|
|
@@ -4638,24 +4693,22 @@ async function main() {
|
|
|
4638
4693
|
db.close();
|
|
4639
4694
|
}
|
|
4640
4695
|
}
|
|
4641
|
-
function persistRawToolChronology(event, userId, deviceId) {
|
|
4642
|
-
const
|
|
4696
|
+
function persistRawToolChronology(rawDb, event, userId, deviceId) {
|
|
4697
|
+
const detected = detectProjectForEvent(event);
|
|
4698
|
+
const project = rawDb.upsertProject({
|
|
4699
|
+
canonical_id: detected.canonical_id,
|
|
4700
|
+
name: detected.name,
|
|
4701
|
+
local_path: detected.local_path,
|
|
4702
|
+
remote_url: detected.remote_url ?? null
|
|
4703
|
+
});
|
|
4704
|
+
rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
|
|
4705
|
+
const metricsIncrement = {
|
|
4706
|
+
toolCalls: 1
|
|
4707
|
+
};
|
|
4708
|
+
if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
|
|
4709
|
+
metricsIncrement.files = 1;
|
|
4710
|
+
}
|
|
4643
4711
|
try {
|
|
4644
|
-
const detected = detectProjectForEvent(event);
|
|
4645
|
-
const project = rawDb.upsertProject({
|
|
4646
|
-
canonical_id: detected.canonical_id,
|
|
4647
|
-
name: detected.name,
|
|
4648
|
-
local_path: detected.local_path,
|
|
4649
|
-
remote_url: detected.remote_url ?? null
|
|
4650
|
-
});
|
|
4651
|
-
rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
|
|
4652
|
-
const metricsIncrement = {
|
|
4653
|
-
toolCalls: 1
|
|
4654
|
-
};
|
|
4655
|
-
if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
|
|
4656
|
-
metricsIncrement.files = 1;
|
|
4657
|
-
}
|
|
4658
|
-
rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
|
|
4659
4712
|
rawDb.insertToolEvent({
|
|
4660
4713
|
session_id: event.session_id,
|
|
4661
4714
|
project_id: project.id,
|
|
@@ -4668,8 +4721,13 @@ function persistRawToolChronology(event, userId, deviceId) {
|
|
|
4668
4721
|
device_id: deviceId,
|
|
4669
4722
|
agent: "claude-code"
|
|
4670
4723
|
});
|
|
4671
|
-
|
|
4672
|
-
rawDb.
|
|
4724
|
+
rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
|
|
4725
|
+
rawDb.setSyncState("hook_post_tool_last_store_status", "stored");
|
|
4726
|
+
rawDb.setSyncState("hook_post_tool_last_store_error", "");
|
|
4727
|
+
} catch (error) {
|
|
4728
|
+
rawDb.setSyncState("hook_post_tool_last_store_status", "store-error");
|
|
4729
|
+
rawDb.setSyncState("hook_post_tool_last_store_error", truncatePreview(error instanceof Error ? error.message : String(error), 400) ?? "unknown");
|
|
4730
|
+
throw error;
|
|
4673
4731
|
}
|
|
4674
4732
|
}
|
|
4675
4733
|
async function withTimeout(promise, timeoutMs) {
|
|
@@ -4754,9 +4812,21 @@ function safeSerializeToolInput(toolInput) {
|
|
|
4754
4812
|
}
|
|
4755
4813
|
}
|
|
4756
4814
|
function truncatePreview(value, maxLen) {
|
|
4757
|
-
if (
|
|
4815
|
+
if (value === null || value === undefined)
|
|
4816
|
+
return null;
|
|
4817
|
+
let text;
|
|
4818
|
+
if (typeof value === "string") {
|
|
4819
|
+
text = value;
|
|
4820
|
+
} else {
|
|
4821
|
+
try {
|
|
4822
|
+
text = JSON.stringify(value);
|
|
4823
|
+
} catch {
|
|
4824
|
+
text = String(value);
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
if (!text)
|
|
4758
4828
|
return null;
|
|
4759
|
-
const normalized =
|
|
4829
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
4760
4830
|
if (!normalized)
|
|
4761
4831
|
return null;
|
|
4762
4832
|
if (normalized.length <= maxLen)
|
|
@@ -86,7 +86,8 @@ function createDefaultConfig() {
|
|
|
86
86
|
project_name: "shared-experience",
|
|
87
87
|
namespace: "",
|
|
88
88
|
api_key: ""
|
|
89
|
-
}
|
|
89
|
+
},
|
|
90
|
+
tool_profile: "full"
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
function loadConfig() {
|
|
@@ -157,7 +158,8 @@ function loadConfig() {
|
|
|
157
158
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
158
159
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
159
160
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
160
|
-
}
|
|
161
|
+
},
|
|
162
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
161
163
|
};
|
|
162
164
|
}
|
|
163
165
|
function saveConfig(config) {
|
|
@@ -212,6 +214,11 @@ function asObserverMode(value, fallback) {
|
|
|
212
214
|
return value;
|
|
213
215
|
return fallback;
|
|
214
216
|
}
|
|
217
|
+
function asToolProfile(value, fallback) {
|
|
218
|
+
if (value === "full" || value === "memory")
|
|
219
|
+
return value;
|
|
220
|
+
return fallback;
|
|
221
|
+
}
|
|
215
222
|
function asTeams(value, fallback) {
|
|
216
223
|
if (!Array.isArray(value))
|
|
217
224
|
return fallback;
|
|
@@ -845,6 +852,7 @@ function ensureObservationTypes(db) {
|
|
|
845
852
|
DROP TABLE observations;
|
|
846
853
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
847
854
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
855
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
848
856
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
849
857
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
850
858
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1125,6 +1133,7 @@ class MemDatabase {
|
|
|
1125
1133
|
this.db = openDatabase(dbPath);
|
|
1126
1134
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1127
1135
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1136
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1128
1137
|
this.vecAvailable = this.loadVecExtension();
|
|
1129
1138
|
runMigrations(this.db);
|
|
1130
1139
|
ensureObservationTypes(this.db);
|
|
@@ -1146,8 +1155,16 @@ class MemDatabase {
|
|
|
1146
1155
|
this.db.close();
|
|
1147
1156
|
}
|
|
1148
1157
|
upsertProject(project) {
|
|
1158
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1159
|
+
const name = project.name?.trim();
|
|
1160
|
+
if (!canonicalId) {
|
|
1161
|
+
throw new Error("Project canonical_id is required");
|
|
1162
|
+
}
|
|
1163
|
+
if (!name) {
|
|
1164
|
+
throw new Error("Project name is required");
|
|
1165
|
+
}
|
|
1149
1166
|
const now = Math.floor(Date.now() / 1000);
|
|
1150
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1167
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1151
1168
|
if (existing) {
|
|
1152
1169
|
this.db.query(`UPDATE projects SET
|
|
1153
1170
|
local_path = COALESCE(?, local_path),
|
|
@@ -1162,7 +1179,7 @@ class MemDatabase {
|
|
|
1162
1179
|
};
|
|
1163
1180
|
}
|
|
1164
1181
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1165
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1182
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1166
1183
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1167
1184
|
}
|
|
1168
1185
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -1930,10 +1947,11 @@ function readProjectConfigFile(directory) {
|
|
|
1930
1947
|
}
|
|
1931
1948
|
}
|
|
1932
1949
|
function detectProject(directory) {
|
|
1933
|
-
const
|
|
1950
|
+
const resolvedDirectory = resolve(directory);
|
|
1951
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
1934
1952
|
if (remoteUrl) {
|
|
1935
1953
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
1936
|
-
const repoRoot = getGitTopLevel(
|
|
1954
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
1937
1955
|
return {
|
|
1938
1956
|
canonical_id: canonicalId,
|
|
1939
1957
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -1941,21 +1959,22 @@ function detectProject(directory) {
|
|
|
1941
1959
|
local_path: repoRoot
|
|
1942
1960
|
};
|
|
1943
1961
|
}
|
|
1944
|
-
const configFile = readProjectConfigFile(
|
|
1962
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
1945
1963
|
if (configFile) {
|
|
1946
1964
|
return {
|
|
1947
1965
|
canonical_id: configFile.project_id,
|
|
1948
1966
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
1949
1967
|
remote_url: null,
|
|
1950
|
-
local_path:
|
|
1968
|
+
local_path: resolvedDirectory
|
|
1951
1969
|
};
|
|
1952
1970
|
}
|
|
1953
|
-
const dirName = basename(
|
|
1971
|
+
const dirName = basename(resolvedDirectory);
|
|
1972
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
1954
1973
|
return {
|
|
1955
|
-
canonical_id: `local/${
|
|
1956
|
-
name:
|
|
1974
|
+
canonical_id: `local/${safeDirName}`,
|
|
1975
|
+
name: safeDirName,
|
|
1957
1976
|
remote_url: null,
|
|
1958
|
-
local_path:
|
|
1977
|
+
local_path: resolvedDirectory
|
|
1959
1978
|
};
|
|
1960
1979
|
}
|
|
1961
1980
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3380,7 +3399,19 @@ function parseJsonArray2(value) {
|
|
|
3380
3399
|
}
|
|
3381
3400
|
}
|
|
3382
3401
|
function compactLine(value) {
|
|
3383
|
-
|
|
3402
|
+
if (value === null || value === undefined)
|
|
3403
|
+
return null;
|
|
3404
|
+
let text;
|
|
3405
|
+
if (typeof value === "string") {
|
|
3406
|
+
text = value;
|
|
3407
|
+
} else {
|
|
3408
|
+
try {
|
|
3409
|
+
text = JSON.stringify(value);
|
|
3410
|
+
} catch {
|
|
3411
|
+
text = String(value);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
3384
3415
|
if (!trimmed)
|
|
3385
3416
|
return null;
|
|
3386
3417
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -162,7 +162,8 @@ function createDefaultConfig() {
|
|
|
162
162
|
project_name: "shared-experience",
|
|
163
163
|
namespace: "",
|
|
164
164
|
api_key: ""
|
|
165
|
-
}
|
|
165
|
+
},
|
|
166
|
+
tool_profile: "full"
|
|
166
167
|
};
|
|
167
168
|
}
|
|
168
169
|
function loadConfig() {
|
|
@@ -233,7 +234,8 @@ function loadConfig() {
|
|
|
233
234
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
234
235
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
235
236
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
236
|
-
}
|
|
237
|
+
},
|
|
238
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
237
239
|
};
|
|
238
240
|
}
|
|
239
241
|
function saveConfig(config) {
|
|
@@ -288,6 +290,11 @@ function asObserverMode(value, fallback) {
|
|
|
288
290
|
return value;
|
|
289
291
|
return fallback;
|
|
290
292
|
}
|
|
293
|
+
function asToolProfile(value, fallback) {
|
|
294
|
+
if (value === "full" || value === "memory")
|
|
295
|
+
return value;
|
|
296
|
+
return fallback;
|
|
297
|
+
}
|
|
291
298
|
function asTeams(value, fallback) {
|
|
292
299
|
if (!Array.isArray(value))
|
|
293
300
|
return fallback;
|
|
@@ -921,6 +928,7 @@ function ensureObservationTypes(db) {
|
|
|
921
928
|
DROP TABLE observations;
|
|
922
929
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
923
930
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
931
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
924
932
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
925
933
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
926
934
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1201,6 +1209,7 @@ class MemDatabase {
|
|
|
1201
1209
|
this.db = openDatabase(dbPath);
|
|
1202
1210
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1203
1211
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1212
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1204
1213
|
this.vecAvailable = this.loadVecExtension();
|
|
1205
1214
|
runMigrations(this.db);
|
|
1206
1215
|
ensureObservationTypes(this.db);
|
|
@@ -1222,8 +1231,16 @@ class MemDatabase {
|
|
|
1222
1231
|
this.db.close();
|
|
1223
1232
|
}
|
|
1224
1233
|
upsertProject(project) {
|
|
1234
|
+
const canonicalId = project.canonical_id?.trim();
|
|
1235
|
+
const name = project.name?.trim();
|
|
1236
|
+
if (!canonicalId) {
|
|
1237
|
+
throw new Error("Project canonical_id is required");
|
|
1238
|
+
}
|
|
1239
|
+
if (!name) {
|
|
1240
|
+
throw new Error("Project name is required");
|
|
1241
|
+
}
|
|
1225
1242
|
const now = Math.floor(Date.now() / 1000);
|
|
1226
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
1243
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
1227
1244
|
if (existing) {
|
|
1228
1245
|
this.db.query(`UPDATE projects SET
|
|
1229
1246
|
local_path = COALESCE(?, local_path),
|
|
@@ -1238,7 +1255,7 @@ class MemDatabase {
|
|
|
1238
1255
|
};
|
|
1239
1256
|
}
|
|
1240
1257
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1241
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1258
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1242
1259
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1243
1260
|
}
|
|
1244
1261
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2060,10 +2077,11 @@ function readProjectConfigFile(directory) {
|
|
|
2060
2077
|
}
|
|
2061
2078
|
}
|
|
2062
2079
|
function detectProject(directory) {
|
|
2063
|
-
const
|
|
2080
|
+
const resolvedDirectory = resolve(directory);
|
|
2081
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
2064
2082
|
if (remoteUrl) {
|
|
2065
2083
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2066
|
-
const repoRoot = getGitTopLevel(
|
|
2084
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
2067
2085
|
return {
|
|
2068
2086
|
canonical_id: canonicalId,
|
|
2069
2087
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -2071,21 +2089,22 @@ function detectProject(directory) {
|
|
|
2071
2089
|
local_path: repoRoot
|
|
2072
2090
|
};
|
|
2073
2091
|
}
|
|
2074
|
-
const configFile = readProjectConfigFile(
|
|
2092
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
2075
2093
|
if (configFile) {
|
|
2076
2094
|
return {
|
|
2077
2095
|
canonical_id: configFile.project_id,
|
|
2078
2096
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
2079
2097
|
remote_url: null,
|
|
2080
|
-
local_path:
|
|
2098
|
+
local_path: resolvedDirectory
|
|
2081
2099
|
};
|
|
2082
2100
|
}
|
|
2083
|
-
const dirName = basename(
|
|
2101
|
+
const dirName = basename(resolvedDirectory);
|
|
2102
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
2084
2103
|
return {
|
|
2085
|
-
canonical_id: `local/${
|
|
2086
|
-
name:
|
|
2104
|
+
canonical_id: `local/${safeDirName}`,
|
|
2105
|
+
name: safeDirName,
|
|
2087
2106
|
remote_url: null,
|
|
2088
|
-
local_path:
|
|
2107
|
+
local_path: resolvedDirectory
|
|
2089
2108
|
};
|
|
2090
2109
|
}
|
|
2091
2110
|
function detectProjectForPath(filePath, fallbackCwd) {
|