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
|
@@ -97,10 +97,11 @@ function readProjectConfigFile(directory) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
function detectProject(directory) {
|
|
100
|
-
const
|
|
100
|
+
const resolvedDirectory = resolve(directory);
|
|
101
|
+
const remoteUrl = getGitRemoteUrl(resolvedDirectory);
|
|
101
102
|
if (remoteUrl) {
|
|
102
103
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
103
|
-
const repoRoot = getGitTopLevel(
|
|
104
|
+
const repoRoot = getGitTopLevel(resolvedDirectory) ?? resolvedDirectory;
|
|
104
105
|
return {
|
|
105
106
|
canonical_id: canonicalId,
|
|
106
107
|
name: projectNameFromCanonicalId(canonicalId),
|
|
@@ -108,21 +109,22 @@ function detectProject(directory) {
|
|
|
108
109
|
local_path: repoRoot
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
|
-
const configFile = readProjectConfigFile(
|
|
112
|
+
const configFile = readProjectConfigFile(resolvedDirectory);
|
|
112
113
|
if (configFile) {
|
|
113
114
|
return {
|
|
114
115
|
canonical_id: configFile.project_id,
|
|
115
116
|
name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
|
|
116
117
|
remote_url: null,
|
|
117
|
-
local_path:
|
|
118
|
+
local_path: resolvedDirectory
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
|
-
const dirName = basename(
|
|
121
|
+
const dirName = basename(resolvedDirectory);
|
|
122
|
+
const safeDirName = !dirName || dirName === "/" || dirName === "." ? "root" : dirName;
|
|
121
123
|
return {
|
|
122
|
-
canonical_id: `local/${
|
|
123
|
-
name:
|
|
124
|
+
canonical_id: `local/${safeDirName}`,
|
|
125
|
+
name: safeDirName,
|
|
124
126
|
remote_url: null,
|
|
125
|
-
local_path:
|
|
127
|
+
local_path: resolvedDirectory
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
130
|
function detectProjectForPath(filePath, fallbackCwd) {
|
|
@@ -1625,7 +1627,19 @@ function parseJsonArray2(value) {
|
|
|
1625
1627
|
}
|
|
1626
1628
|
}
|
|
1627
1629
|
function compactLine(value) {
|
|
1628
|
-
|
|
1630
|
+
if (value === null || value === undefined)
|
|
1631
|
+
return null;
|
|
1632
|
+
let text;
|
|
1633
|
+
if (typeof value === "string") {
|
|
1634
|
+
text = value;
|
|
1635
|
+
} else {
|
|
1636
|
+
try {
|
|
1637
|
+
text = JSON.stringify(value);
|
|
1638
|
+
} catch {
|
|
1639
|
+
text = String(value);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
1629
1643
|
if (!trimmed)
|
|
1630
1644
|
return null;
|
|
1631
1645
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
@@ -3266,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
3266
3280
|
import { join as join3 } from "node:path";
|
|
3267
3281
|
import { homedir } from "node:os";
|
|
3268
3282
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
3269
|
-
var CLIENT_VERSION = "0.4.
|
|
3283
|
+
var CLIENT_VERSION = "0.4.44";
|
|
3270
3284
|
function hashFile(filePath) {
|
|
3271
3285
|
try {
|
|
3272
3286
|
if (!existsSync3(filePath))
|
|
@@ -3480,7 +3494,8 @@ function createDefaultConfig() {
|
|
|
3480
3494
|
project_name: "shared-experience",
|
|
3481
3495
|
namespace: "",
|
|
3482
3496
|
api_key: ""
|
|
3483
|
-
}
|
|
3497
|
+
},
|
|
3498
|
+
tool_profile: "full"
|
|
3484
3499
|
};
|
|
3485
3500
|
}
|
|
3486
3501
|
function loadConfig() {
|
|
@@ -3551,7 +3566,8 @@ function loadConfig() {
|
|
|
3551
3566
|
project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
|
|
3552
3567
|
namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
|
|
3553
3568
|
api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
|
|
3554
|
-
}
|
|
3569
|
+
},
|
|
3570
|
+
tool_profile: asToolProfile(config["tool_profile"], defaults.tool_profile)
|
|
3555
3571
|
};
|
|
3556
3572
|
}
|
|
3557
3573
|
function saveConfig(config) {
|
|
@@ -3606,12 +3622,107 @@ function asObserverMode(value, fallback) {
|
|
|
3606
3622
|
return value;
|
|
3607
3623
|
return fallback;
|
|
3608
3624
|
}
|
|
3625
|
+
function asToolProfile(value, fallback) {
|
|
3626
|
+
if (value === "full" || value === "memory")
|
|
3627
|
+
return value;
|
|
3628
|
+
return fallback;
|
|
3629
|
+
}
|
|
3609
3630
|
function asTeams(value, fallback) {
|
|
3610
3631
|
if (!Array.isArray(value))
|
|
3611
3632
|
return fallback;
|
|
3612
3633
|
return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
|
|
3613
3634
|
}
|
|
3614
3635
|
|
|
3636
|
+
// src/sync/auth.ts
|
|
3637
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3638
|
+
|
|
3639
|
+
// src/storage/outbox.ts
|
|
3640
|
+
function getPendingEntries(db, limit = 50) {
|
|
3641
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3642
|
+
return db.db.query(`SELECT * FROM sync_outbox
|
|
3643
|
+
WHERE (status = 'pending')
|
|
3644
|
+
OR (status = 'failed' AND retry_count < max_retries AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?))
|
|
3645
|
+
ORDER BY created_at_epoch ASC
|
|
3646
|
+
LIMIT ?`).all(now, limit);
|
|
3647
|
+
}
|
|
3648
|
+
function markSyncing(db, entryId) {
|
|
3649
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3650
|
+
db.db.query("UPDATE sync_outbox SET status = 'syncing', next_retry_epoch = ? WHERE id = ?").run(now, entryId);
|
|
3651
|
+
}
|
|
3652
|
+
function markSynced(db, entryId) {
|
|
3653
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3654
|
+
db.db.query("UPDATE sync_outbox SET status = 'synced', synced_at_epoch = ?, next_retry_epoch = NULL, last_error = NULL WHERE id = ?").run(now, entryId);
|
|
3655
|
+
}
|
|
3656
|
+
function markFailed(db, entryId, error) {
|
|
3657
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3658
|
+
db.db.query(`UPDATE sync_outbox SET
|
|
3659
|
+
status = 'failed',
|
|
3660
|
+
retry_count = retry_count + 1,
|
|
3661
|
+
last_error = ?,
|
|
3662
|
+
next_retry_epoch = ? + MIN(30 * (1 << retry_count), 3600)
|
|
3663
|
+
WHERE id = ?`).run(error, now, entryId);
|
|
3664
|
+
}
|
|
3665
|
+
function classifyOutboxFailure(error) {
|
|
3666
|
+
const normalized = error.toLowerCase();
|
|
3667
|
+
if (normalized.includes("401") || normalized.includes("invalid or missing credentials")) {
|
|
3668
|
+
return "auth";
|
|
3669
|
+
}
|
|
3670
|
+
if (normalized.includes("429") || normalized.includes("rate limit")) {
|
|
3671
|
+
return "rate_limit";
|
|
3672
|
+
}
|
|
3673
|
+
if (normalized.includes("timeout") || normalized.includes("abort")) {
|
|
3674
|
+
return "timeout";
|
|
3675
|
+
}
|
|
3676
|
+
if (normalized.includes("network") || normalized.includes("fetch") || normalized.includes("econn")) {
|
|
3677
|
+
return "network";
|
|
3678
|
+
}
|
|
3679
|
+
if (normalized.includes("400") || normalized.includes("422") || normalized.includes("validation")) {
|
|
3680
|
+
return "validation";
|
|
3681
|
+
}
|
|
3682
|
+
return "other";
|
|
3683
|
+
}
|
|
3684
|
+
function resetFailedEntries(db) {
|
|
3685
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3686
|
+
SET status = 'pending',
|
|
3687
|
+
retry_count = 0,
|
|
3688
|
+
last_error = NULL,
|
|
3689
|
+
next_retry_epoch = NULL
|
|
3690
|
+
WHERE status = 'failed'`).run();
|
|
3691
|
+
return result.changes;
|
|
3692
|
+
}
|
|
3693
|
+
function resetFailedEntriesMatching(db, predicate) {
|
|
3694
|
+
const rows = db.db.query(`SELECT id, last_error
|
|
3695
|
+
FROM sync_outbox
|
|
3696
|
+
WHERE status = 'failed'`).all();
|
|
3697
|
+
const matchingIds = rows.filter((row) => row.last_error && predicate(row.last_error)).map((row) => row.id);
|
|
3698
|
+
if (matchingIds.length === 0)
|
|
3699
|
+
return 0;
|
|
3700
|
+
const placeholders = matchingIds.map(() => "?").join(", ");
|
|
3701
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3702
|
+
SET status = 'pending',
|
|
3703
|
+
retry_count = 0,
|
|
3704
|
+
last_error = NULL,
|
|
3705
|
+
next_retry_epoch = NULL
|
|
3706
|
+
WHERE id IN (${placeholders})`).run(...matchingIds);
|
|
3707
|
+
return result.changes;
|
|
3708
|
+
}
|
|
3709
|
+
function resetSyncingEntries(db) {
|
|
3710
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3711
|
+
SET status = 'pending',
|
|
3712
|
+
next_retry_epoch = NULL
|
|
3713
|
+
WHERE status = 'syncing'`).run();
|
|
3714
|
+
return result.changes;
|
|
3715
|
+
}
|
|
3716
|
+
function resetStaleSyncingEntries(db, maxAgeSeconds = 300) {
|
|
3717
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
|
3718
|
+
const result = db.db.query(`UPDATE sync_outbox
|
|
3719
|
+
SET status = 'pending',
|
|
3720
|
+
next_retry_epoch = NULL
|
|
3721
|
+
WHERE status = 'syncing'
|
|
3722
|
+
AND (next_retry_epoch IS NULL OR next_retry_epoch <= ?)`).run(cutoff);
|
|
3723
|
+
return result.changes;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3615
3726
|
// src/sync/auth.ts
|
|
3616
3727
|
var LEGACY_PUBLIC_HOSTS = new Set(["www.candengo.com", "candengo.com"]);
|
|
3617
3728
|
function normalizeBaseUrl(url) {
|
|
@@ -3643,6 +3754,36 @@ function getBaseUrl(config) {
|
|
|
3643
3754
|
}
|
|
3644
3755
|
return null;
|
|
3645
3756
|
}
|
|
3757
|
+
function getAuthFingerprint(config) {
|
|
3758
|
+
const apiKey = getApiKey(config);
|
|
3759
|
+
const baseUrl = getBaseUrl(config);
|
|
3760
|
+
if (!apiKey || !baseUrl)
|
|
3761
|
+
return null;
|
|
3762
|
+
return createHash3("sha256").update(`${baseUrl}
|
|
3763
|
+
${apiKey}
|
|
3764
|
+
${config.namespace}
|
|
3765
|
+
${config.site_id}`).digest("hex");
|
|
3766
|
+
}
|
|
3767
|
+
function recoverOutboxAfterAuthChange(db, config) {
|
|
3768
|
+
const fingerprint = getAuthFingerprint(config);
|
|
3769
|
+
if (!fingerprint) {
|
|
3770
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
3771
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset: 0, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
3772
|
+
}
|
|
3773
|
+
const key = "sync_auth_fingerprint";
|
|
3774
|
+
const previous = db.getSyncState(key);
|
|
3775
|
+
const fingerprintChanged = previous !== fingerprint;
|
|
3776
|
+
if (!fingerprintChanged) {
|
|
3777
|
+
const authFailedReset = resetFailedEntriesMatching(db, (error) => classifyOutboxFailure(error) === "auth");
|
|
3778
|
+
const staleSyncingReset2 = resetStaleSyncingEntries(db);
|
|
3779
|
+
return { fingerprintChanged: false, failedReset: 0, authFailedReset, syncingReset: 0, staleSyncingReset: staleSyncingReset2 };
|
|
3780
|
+
}
|
|
3781
|
+
const failedReset = resetFailedEntries(db);
|
|
3782
|
+
const syncingReset = resetSyncingEntries(db);
|
|
3783
|
+
const staleSyncingReset = 0;
|
|
3784
|
+
db.setSyncState(key, fingerprint);
|
|
3785
|
+
return { fingerprintChanged: true, failedReset, authFailedReset: 0, syncingReset, staleSyncingReset };
|
|
3786
|
+
}
|
|
3646
3787
|
function buildSourceId(config, localId, type = "obs") {
|
|
3647
3788
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
3648
3789
|
}
|
|
@@ -3905,6 +4046,7 @@ class VectorClient {
|
|
|
3905
4046
|
apiKey;
|
|
3906
4047
|
siteId;
|
|
3907
4048
|
namespace;
|
|
4049
|
+
timeoutMs;
|
|
3908
4050
|
constructor(config, overrides = {}) {
|
|
3909
4051
|
const baseUrl = getBaseUrl(config);
|
|
3910
4052
|
const apiKey = overrides.apiKey ?? getApiKey(config);
|
|
@@ -3915,6 +4057,7 @@ class VectorClient {
|
|
|
3915
4057
|
this.apiKey = apiKey;
|
|
3916
4058
|
this.siteId = overrides.siteId ?? config.site_id;
|
|
3917
4059
|
this.namespace = overrides.namespace ?? config.namespace;
|
|
4060
|
+
this.timeoutMs = overrides.timeoutMs ?? 1e4;
|
|
3918
4061
|
}
|
|
3919
4062
|
static isConfigured(config) {
|
|
3920
4063
|
return getApiKey(config) !== null && getBaseUrl(config) !== null;
|
|
@@ -3977,6 +4120,7 @@ class VectorClient {
|
|
|
3977
4120
|
if (body && method !== "GET") {
|
|
3978
4121
|
init.body = JSON.stringify(body);
|
|
3979
4122
|
}
|
|
4123
|
+
init.signal = AbortSignal.timeout(this.timeoutMs);
|
|
3980
4124
|
const response = await fetch(url, init);
|
|
3981
4125
|
if (!response.ok) {
|
|
3982
4126
|
const text = await response.text().catch(() => "");
|
|
@@ -4629,6 +4773,7 @@ function ensureObservationTypes(db) {
|
|
|
4629
4773
|
DROP TABLE observations;
|
|
4630
4774
|
ALTER TABLE observations_repair RENAME TO observations;
|
|
4631
4775
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
4776
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project_lifecycle ON observations(project_id, lifecycle);
|
|
4632
4777
|
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
4633
4778
|
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
4634
4779
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
@@ -4761,7 +4906,7 @@ function getSchemaVersion(db) {
|
|
|
4761
4906
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
4762
4907
|
|
|
4763
4908
|
// src/storage/sqlite.ts
|
|
4764
|
-
import { createHash as
|
|
4909
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
4765
4910
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
4766
4911
|
function openDatabase(dbPath) {
|
|
4767
4912
|
if (IS_BUN) {
|
|
@@ -4829,6 +4974,7 @@ class MemDatabase {
|
|
|
4829
4974
|
this.db = openDatabase(dbPath);
|
|
4830
4975
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
4831
4976
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
4977
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
4832
4978
|
this.vecAvailable = this.loadVecExtension();
|
|
4833
4979
|
runMigrations(this.db);
|
|
4834
4980
|
ensureObservationTypes(this.db);
|
|
@@ -4850,8 +4996,16 @@ class MemDatabase {
|
|
|
4850
4996
|
this.db.close();
|
|
4851
4997
|
}
|
|
4852
4998
|
upsertProject(project) {
|
|
4999
|
+
const canonicalId = project.canonical_id?.trim();
|
|
5000
|
+
const name = project.name?.trim();
|
|
5001
|
+
if (!canonicalId) {
|
|
5002
|
+
throw new Error("Project canonical_id is required");
|
|
5003
|
+
}
|
|
5004
|
+
if (!name) {
|
|
5005
|
+
throw new Error("Project name is required");
|
|
5006
|
+
}
|
|
4853
5007
|
const now = Math.floor(Date.now() / 1000);
|
|
4854
|
-
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(
|
|
5008
|
+
const existing = this.db.query("SELECT * FROM projects WHERE canonical_id = ?").get(canonicalId);
|
|
4855
5009
|
if (existing) {
|
|
4856
5010
|
this.db.query(`UPDATE projects SET
|
|
4857
5011
|
local_path = COALESCE(?, local_path),
|
|
@@ -4866,7 +5020,7 @@ class MemDatabase {
|
|
|
4866
5020
|
};
|
|
4867
5021
|
}
|
|
4868
5022
|
const result = this.db.query(`INSERT INTO projects (canonical_id, name, local_path, remote_url, first_seen_epoch, last_active_epoch)
|
|
4869
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
5023
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(canonicalId, name, project.local_path ?? null, project.remote_url ?? null, now, now);
|
|
4870
5024
|
return this.db.query("SELECT * FROM projects WHERE id = ?").get(Number(result.lastInsertRowid));
|
|
4871
5025
|
}
|
|
4872
5026
|
getProjectByCanonicalId(canonicalId) {
|
|
@@ -5542,7 +5696,7 @@ class MemDatabase {
|
|
|
5542
5696
|
}
|
|
5543
5697
|
}
|
|
5544
5698
|
function hashPrompt(prompt) {
|
|
5545
|
-
return
|
|
5699
|
+
return createHash4("sha256").update(prompt).digest("hex");
|
|
5546
5700
|
}
|
|
5547
5701
|
|
|
5548
5702
|
// src/hooks/common.ts
|