engrm 0.4.43 → 0.4.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +260 -24
- package/dist/hooks/elicitation-result.js +22 -10
- package/dist/hooks/post-tool-use.js +99 -36
- package/dist/hooks/pre-compact.js +35 -11
- package/dist/hooks/sentinel.js +22 -10
- package/dist/hooks/session-start.js +158 -14
- package/dist/hooks/stop.js +178 -41
- package/dist/hooks/user-prompt-submit.js +35 -1505
- package/dist/server.js +202 -27
- package/opencode/README.md +6 -6
- package/opencode/install-or-update-opencode-plugin.sh +7 -1
- package/opencode/opencode.example.json +2 -2
- package/package.json +1 -1
|
@@ -1058,6 +1058,7 @@ function ensureObservationTypes(db) {
|
|
|
1058
1058
|
DROP TABLE observations;
|
|
1059
1059
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
1060
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);
|
|
1061
1062
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
1062
1063
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
1063
1064
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1338,6 +1339,7 @@ class MemDatabase {
|
|
|
1338
1339
|
this.db = openDatabase(dbPath);
|
|
1339
1340
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1340
1341
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1342
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1341
1343
|
this.vecAvailable = this.loadVecExtension();
|
|
1342
1344
|
runMigrations(this.db);
|
|
1343
1345
|
ensureObservationTypes(this.db);
|
|
@@ -1359,8 +1361,16 @@ class MemDatabase {
|
|
|
1359
1361
|
this.db.close();
|
|
1360
1362
|
}
|
|
1361
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
|
+
}
|
|
1362
1372
|
const now = Math.floor(Date.now() / 1000);
|
|
1363
|
-
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);
|
|
1364
1374
|
if (existing) {
|
|
1365
1375
|
this.db.query(`UPDATE projects SET
|
|
1366
1376
|
local_path = COALESCE(?, local_path),
|
|
@@ -1375,7 +1385,7 @@ class MemDatabase {
|
|
|
1375
1385
|
};
|
|
1376
1386
|
}
|
|
1377
1387
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1378
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1388
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1379
1389
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1380
1390
|
}
|
|
1381
1391
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2525,10 +2535,11 @@ function readProjectConfigFile(directory) {
|
|
|
2525
2535
|
}
|
|
2526
2536
|
}
|
|
2527
2537
|
function detectProject(directory) {
|
|
2528
|
-
const
|
|
2538
|
+
const resolvedDirectory = resolve(directory);
|
|
2539
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
2529
2540
|
if (remoteUrl) {
|
|
2530
2541
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2531
|
-
const repoRoot = getGitTopLevel(
|
|
2542
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
2532
2543
|
return {
|
|
2533
2544
|
canonical_id: canonicalId,
|
|
2534
2545
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -2536,21 +2547,22 @@ function detectProject(directory) {
|
|
|
2536
2547
|
local_path: repoRoot
|
|
2537
2548
|
};
|
|
2538
2549
|
}
|
|
2539
|
-
const configFile = readProjectConfigFile(
|
|
2550
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
2540
2551
|
if (configFile) {
|
|
2541
2552
|
return {
|
|
2542
2553
|
canonical_id: configFile.project_id,
|
|
2543
2554
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
2544
2555
|
remote_url: null,
|
|
2545
|
-
local_path:
|
|
2556
|
+
local_path: resolvedDirectory
|
|
2546
2557
|
};
|
|
2547
2558
|
}
|
|
2548
|
-
const dirName = basename(
|
|
2559
|
+
const dirName = basename(resolvedDirectory);
|
|
2560
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
2549
2561
|
return {
|
|
2550
|
-
canonical_id: `local/${
|
|
2551
|
-
name:
|
|
2562
|
+
canonical_id: `local/${safeDirName}`,
|
|
2563
|
+
name: safeDirName,
|
|
2552
2564
|
remote_url: null,
|
|
2553
|
-
local_path:
|
|
2565
|
+
local_path: resolvedDirectory
|
|
2554
2566
|
};
|
|
2555
2567
|
}
|
|
2556
2568
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3434,9 +3446,21 @@ function incrementObserverSaveCount(sessionId) {
|
|
|
3434
3446
|
// src/capture/recall.ts
|
|
3435
3447
|
var VEC_DISTANCE_THRESHOLD = 0.25;
|
|
3436
3448
|
function extractErrorSignature(output) {
|
|
3437
|
-
if (
|
|
3449
|
+
if (output === null || output === undefined)
|
|
3438
3450
|
return null;
|
|
3439
|
-
|
|
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)
|
|
3462
|
+
return null;
|
|
3463
|
+
const lines = text.split(`
|
|
3440
3464
|
`);
|
|
3441
3465
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
3442
3466
|
const line = lines[i].trim();
|
|
@@ -3766,7 +3790,19 @@ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolN
|
|
|
3766
3790
|
return null;
|
|
3767
3791
|
}
|
|
3768
3792
|
function compactLine(value) {
|
|
3769
|
-
|
|
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();
|
|
3770
3806
|
if (!trimmed)
|
|
3771
3807
|
return null;
|
|
3772
3808
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -4507,7 +4543,19 @@ function parseJsonArray3(value) {
|
|
|
4507
4543
|
}
|
|
4508
4544
|
}
|
|
4509
4545
|
function compactLine2(value) {
|
|
4510
|
-
|
|
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();
|
|
4511
4559
|
if (!trimmed)
|
|
4512
4560
|
return null;
|
|
4513
4561
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -4539,7 +4587,7 @@ async function main() {
|
|
|
4539
4587
|
db.setSyncState("hook_post_tool_last_payload_preview", truncatePreview(raw, 400) ?? "parsed");
|
|
4540
4588
|
try {
|
|
4541
4589
|
if (event.session_id) {
|
|
4542
|
-
persistRawToolChronology(event, config.user_id, config.device_id);
|
|
4590
|
+
persistRawToolChronology(db, event, config.user_id, config.device_id);
|
|
4543
4591
|
await syncTranscriptChat(db, config, event.session_id, event.cwd);
|
|
4544
4592
|
}
|
|
4545
4593
|
const textToScan = extractScanText(event);
|
|
@@ -4645,24 +4693,22 @@ async function main() {
|
|
|
4645
4693
|
db.close();
|
|
4646
4694
|
}
|
|
4647
4695
|
}
|
|
4648
|
-
function persistRawToolChronology(event, userId, deviceId) {
|
|
4649
|
-
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
|
+
}
|
|
4650
4711
|
try {
|
|
4651
|
-
const detected = detectProjectForEvent(event);
|
|
4652
|
-
const project = rawDb.upsertProject({
|
|
4653
|
-
canonical_id: detected.canonical_id,
|
|
4654
|
-
name: detected.name,
|
|
4655
|
-
local_path: detected.local_path,
|
|
4656
|
-
remote_url: detected.remote_url ?? null
|
|
4657
|
-
});
|
|
4658
|
-
rawDb.upsertSession(event.session_id, project.id, userId, deviceId, "claude-code");
|
|
4659
|
-
const metricsIncrement = {
|
|
4660
|
-
toolCalls: 1
|
|
4661
|
-
};
|
|
4662
|
-
if ((event.tool_name === "Edit" || event.tool_name === "Write") && event.tool_input["file_path"]) {
|
|
4663
|
-
metricsIncrement.files = 1;
|
|
4664
|
-
}
|
|
4665
|
-
rawDb.incrementSessionMetrics(event.session_id, metricsIncrement);
|
|
4666
4712
|
rawDb.insertToolEvent({
|
|
4667
4713
|
session_id: event.session_id,
|
|
4668
4714
|
project_id: project.id,
|
|
@@ -4675,8 +4721,13 @@ function persistRawToolChronology(event, userId, deviceId) {
|
|
|
4675
4721
|
device_id: deviceId,
|
|
4676
4722
|
agent: "claude-code"
|
|
4677
4723
|
});
|
|
4678
|
-
|
|
4679
|
-
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;
|
|
4680
4731
|
}
|
|
4681
4732
|
}
|
|
4682
4733
|
async function withTimeout(promise, timeoutMs) {
|
|
@@ -4761,9 +4812,21 @@ function safeSerializeToolInput(toolInput) {
|
|
|
4761
4812
|
}
|
|
4762
4813
|
}
|
|
4763
4814
|
function truncatePreview(value, maxLen) {
|
|
4764
|
-
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)
|
|
4765
4828
|
return null;
|
|
4766
|
-
const normalized =
|
|
4829
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
4767
4830
|
if (!normalized)
|
|
4768
4831
|
return null;
|
|
4769
4832
|
if (normalized.length <= maxLen)
|
|
@@ -852,6 +852,7 @@ function ensureObservationTypes(db) {
|
|
|
852
852
|
DROP TABLE observations;
|
|
853
853
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
854
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);
|
|
855
856
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
856
857
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
857
858
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1132,6 +1133,7 @@ class MemDatabase {
|
|
|
1132
1133
|
this.db = openDatabase(dbPath);
|
|
1133
1134
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1134
1135
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1136
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1135
1137
|
this.vecAvailable = this.loadVecExtension();
|
|
1136
1138
|
runMigrations(this.db);
|
|
1137
1139
|
ensureObservationTypes(this.db);
|
|
@@ -1153,8 +1155,16 @@ class MemDatabase {
|
|
|
1153
1155
|
this.db.close();
|
|
1154
1156
|
}
|
|
1155
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
|
+
}
|
|
1156
1166
|
const now = Math.floor(Date.now() / 1000);
|
|
1157
|
-
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);
|
|
1158
1168
|
if (existing) {
|
|
1159
1169
|
this.db.query(`UPDATE projects SET
|
|
1160
1170
|
local_path = COALESCE(?, local_path),
|
|
@@ -1169,7 +1179,7 @@ class MemDatabase {
|
|
|
1169
1179
|
};
|
|
1170
1180
|
}
|
|
1171
1181
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1172
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1182
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1173
1183
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1174
1184
|
}
|
|
1175
1185
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -1937,10 +1947,11 @@ function readProjectConfigFile(directory) {
|
|
|
1937
1947
|
}
|
|
1938
1948
|
}
|
|
1939
1949
|
function detectProject(directory) {
|
|
1940
|
-
const
|
|
1950
|
+
const resolvedDirectory = resolve(directory);
|
|
1951
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
1941
1952
|
if (remoteUrl) {
|
|
1942
1953
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
1943
|
-
const repoRoot = getGitTopLevel(
|
|
1954
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
1944
1955
|
return {
|
|
1945
1956
|
canonical_id: canonicalId,
|
|
1946
1957
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -1948,21 +1959,22 @@ function detectProject(directory) {
|
|
|
1948
1959
|
local_path: repoRoot
|
|
1949
1960
|
};
|
|
1950
1961
|
}
|
|
1951
|
-
const configFile = readProjectConfigFile(
|
|
1962
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
1952
1963
|
if (configFile) {
|
|
1953
1964
|
return {
|
|
1954
1965
|
canonical_id: configFile.project_id,
|
|
1955
1966
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
1956
1967
|
remote_url: null,
|
|
1957
|
-
local_path:
|
|
1968
|
+
local_path: resolvedDirectory
|
|
1958
1969
|
};
|
|
1959
1970
|
}
|
|
1960
|
-
const dirName = basename(
|
|
1971
|
+
const dirName = basename(resolvedDirectory);
|
|
1972
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
1961
1973
|
return {
|
|
1962
|
-
canonical_id: `local/${
|
|
1963
|
-
name:
|
|
1974
|
+
canonical_id: `local/${safeDirName}`,
|
|
1975
|
+
name: safeDirName,
|
|
1964
1976
|
remote_url: null,
|
|
1965
|
-
local_path:
|
|
1977
|
+
local_path: resolvedDirectory
|
|
1966
1978
|
};
|
|
1967
1979
|
}
|
|
1968
1980
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -3387,7 +3399,19 @@ function parseJsonArray2(value) {
|
|
|
3387
3399
|
}
|
|
3388
3400
|
}
|
|
3389
3401
|
function compactLine(value) {
|
|
3390
|
-
|
|
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();
|
|
3391
3415
|
if (!trimmed)
|
|
3392
3416
|
return null;
|
|
3393
3417
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -928,6 +928,7 @@ function ensureObservationTypes(db) {
|
|
|
928
928
|
DROP TABLE observations;
|
|
929
929
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
930
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);
|
|
931
932
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
932
933
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
933
934
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -1208,6 +1209,7 @@ class MemDatabase {
|
|
|
1208
1209
|
this.db = openDatabase(dbPath);
|
|
1209
1210
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1210
1211
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1212
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
1211
1213
|
this.vecAvailable = this.loadVecExtension();
|
|
1212
1214
|
runMigrations(this.db);
|
|
1213
1215
|
ensureObservationTypes(this.db);
|
|
@@ -1229,8 +1231,16 @@ class MemDatabase {
|
|
|
1229
1231
|
this.db.close();
|
|
1230
1232
|
}
|
|
1231
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
|
+
}
|
|
1232
1242
|
const now = Math.floor(Date.now() / 1000);
|
|
1233
|
-
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);
|
|
1234
1244
|
if (existing) {
|
|
1235
1245
|
this.db.query(`UPDATE projects SET
|
|
1236
1246
|
local_path = COALESCE(?, local_path),
|
|
@@ -1245,7 +1255,7 @@ class MemDatabase {
|
|
|
1245
1255
|
};
|
|
1246
1256
|
}
|
|
1247
1257
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
1248
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
1258
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
1249
1259
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
1250
1260
|
}
|
|
1251
1261
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -2067,10 +2077,11 @@ function readProjectConfigFile(directory) {
|
|
|
2067
2077
|
}
|
|
2068
2078
|
}
|
|
2069
2079
|
function detectProject(directory) {
|
|
2070
|
-
const
|
|
2080
|
+
const resolvedDirectory = resolve(directory);
|
|
2081
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
2071
2082
|
if (remoteUrl) {
|
|
2072
2083
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
2073
|
-
const repoRoot = getGitTopLevel(
|
|
2084
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
2074
2085
|
return {
|
|
2075
2086
|
canonical_id: canonicalId,
|
|
2076
2087
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -2078,21 +2089,22 @@ function detectProject(directory) {
|
|
|
2078
2089
|
local_path: repoRoot
|
|
2079
2090
|
};
|
|
2080
2091
|
}
|
|
2081
|
-
const configFile = readProjectConfigFile(
|
|
2092
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
2082
2093
|
if (configFile) {
|
|
2083
2094
|
return {
|
|
2084
2095
|
canonical_id: configFile.project_id,
|
|
2085
2096
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
2086
2097
|
remote_url: null,
|
|
2087
|
-
local_path:
|
|
2098
|
+
local_path: resolvedDirectory
|
|
2088
2099
|
};
|
|
2089
2100
|
}
|
|
2090
|
-
const dirName = basename(
|
|
2101
|
+
const dirName = basename(resolvedDirectory);
|
|
2102
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
2091
2103
|
return {
|
|
2092
|
-
canonical_id: `local/${
|
|
2093
|
-
name:
|
|
2104
|
+
canonical_id: `local/${safeDirName}`,
|
|
2105
|
+
name: safeDirName,
|
|
2094
2106
|
remote_url: null,
|
|
2095
|
-
local_path:
|
|
2107
|
+
local_path: resolvedDirectory
|
|
2096
2108
|
};
|
|
2097
2109
|
}
|
|
2098
2110
|
function detectProjectForPath(filePath, fallbackCwd) {
|