engrm 0.3.2 → 0.4.0
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 +684 -12
- package/dist/hooks/elicitation-result.js +85 -5
- package/dist/hooks/post-tool-use.js +340 -5
- package/dist/hooks/pre-compact.js +106 -10
- package/dist/hooks/sentinel.js +90 -4
- package/dist/hooks/session-start.js +342 -11
- package/dist/hooks/stop.js +903 -4
- package/dist/server.js +155 -11
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -13554,9 +13554,9 @@ function date4(params) {
|
|
|
13554
13554
|
config(en_default());
|
|
13555
13555
|
// src/config.ts
|
|
13556
13556
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13557
|
-
import { homedir, hostname as hostname3 } from "node:os";
|
|
13557
|
+
import { homedir, hostname as hostname3, networkInterfaces } from "node:os";
|
|
13558
13558
|
import { join } from "node:path";
|
|
13559
|
-
import {
|
|
13559
|
+
import { createHash } from "node:crypto";
|
|
13560
13560
|
var CONFIG_DIR = join(homedir(), ".engrm");
|
|
13561
13561
|
var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
|
|
13562
13562
|
var DB_PATH = join(CONFIG_DIR, "engrm.db");
|
|
@@ -13565,7 +13565,22 @@ function getDbPath() {
|
|
|
13565
13565
|
}
|
|
13566
13566
|
function generateDeviceId() {
|
|
13567
13567
|
const host = hostname3().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
13568
|
-
|
|
13568
|
+
let mac3 = "";
|
|
13569
|
+
const ifaces = networkInterfaces();
|
|
13570
|
+
for (const entries of Object.values(ifaces)) {
|
|
13571
|
+
if (!entries)
|
|
13572
|
+
continue;
|
|
13573
|
+
for (const entry of entries) {
|
|
13574
|
+
if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
|
|
13575
|
+
mac3 = entry.mac;
|
|
13576
|
+
break;
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
13579
|
+
if (mac3)
|
|
13580
|
+
break;
|
|
13581
|
+
}
|
|
13582
|
+
const material = `${host}:${mac3 || "no-mac"}`;
|
|
13583
|
+
const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
|
|
13569
13584
|
return `${host}-${suffix}`;
|
|
13570
13585
|
}
|
|
13571
13586
|
function createDefaultConfig() {
|
|
@@ -13607,7 +13622,10 @@ function createDefaultConfig() {
|
|
|
13607
13622
|
observer: {
|
|
13608
13623
|
enabled: true,
|
|
13609
13624
|
mode: "per_event",
|
|
13610
|
-
model: "
|
|
13625
|
+
model: "sonnet"
|
|
13626
|
+
},
|
|
13627
|
+
transcript_analysis: {
|
|
13628
|
+
enabled: false
|
|
13611
13629
|
}
|
|
13612
13630
|
};
|
|
13613
13631
|
}
|
|
@@ -13666,9 +13684,19 @@ function loadConfig() {
|
|
|
13666
13684
|
enabled: asBool(config2["observer"]?.["enabled"], defaults.observer.enabled),
|
|
13667
13685
|
mode: asObserverMode(config2["observer"]?.["mode"], defaults.observer.mode),
|
|
13668
13686
|
model: asString(config2["observer"]?.["model"], defaults.observer.model)
|
|
13687
|
+
},
|
|
13688
|
+
transcript_analysis: {
|
|
13689
|
+
enabled: asBool(config2["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
|
|
13669
13690
|
}
|
|
13670
13691
|
};
|
|
13671
13692
|
}
|
|
13693
|
+
function saveConfig(config2) {
|
|
13694
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
13695
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
13696
|
+
}
|
|
13697
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(config2, null, 2) + `
|
|
13698
|
+
`, "utf-8");
|
|
13699
|
+
}
|
|
13672
13700
|
function configExists() {
|
|
13673
13701
|
return existsSync(SETTINGS_PATH);
|
|
13674
13702
|
}
|
|
@@ -14064,6 +14092,56 @@ function runMigrations(db) {
|
|
|
14064
14092
|
}
|
|
14065
14093
|
}
|
|
14066
14094
|
}
|
|
14095
|
+
function ensureObservationTypes(db) {
|
|
14096
|
+
try {
|
|
14097
|
+
db.exec("INSERT INTO observations (session_id, project_id, type, title, user_id, device_id, agent, created_at, created_at_epoch) " + "VALUES ('_typecheck', 1, 'message', '_test', '_test', '_test', '_test', '2000-01-01', 0)");
|
|
14098
|
+
db.exec("DELETE FROM observations WHERE session_id = '_typecheck'");
|
|
14099
|
+
} catch {
|
|
14100
|
+
db.exec("BEGIN TRANSACTION");
|
|
14101
|
+
try {
|
|
14102
|
+
db.exec(`
|
|
14103
|
+
CREATE TABLE observations_repair (
|
|
14104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT,
|
|
14105
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
14106
|
+
type TEXT NOT NULL CHECK (type IN (
|
|
14107
|
+
'bugfix','discovery','decision','pattern','change','feature',
|
|
14108
|
+
'refactor','digest','standard','message')),
|
|
14109
|
+
title TEXT NOT NULL, narrative TEXT, facts TEXT, concepts TEXT,
|
|
14110
|
+
files_read TEXT, files_modified TEXT,
|
|
14111
|
+
quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
|
|
14112
|
+
lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN ('active','aging','archived','purged','pinned')),
|
|
14113
|
+
sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN ('shared','personal','secret')),
|
|
14114
|
+
user_id TEXT NOT NULL, device_id TEXT NOT NULL, agent TEXT DEFAULT 'claude-code',
|
|
14115
|
+
created_at TEXT NOT NULL, created_at_epoch INTEGER NOT NULL,
|
|
14116
|
+
archived_at_epoch INTEGER,
|
|
14117
|
+
compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
14118
|
+
superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
14119
|
+
remote_source_id TEXT
|
|
14120
|
+
);
|
|
14121
|
+
INSERT INTO observations_repair SELECT * FROM observations;
|
|
14122
|
+
DROP TABLE observations;
|
|
14123
|
+
ALTER TABLE observations_repair RENAME TO observations;
|
|
14124
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project_id);
|
|
14125
|
+
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
14126
|
+
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch);
|
|
14127
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
14128
|
+
CREATE INDEX IF NOT EXISTS idx_observations_lifecycle ON observations(lifecycle);
|
|
14129
|
+
CREATE INDEX IF NOT EXISTS idx_observations_quality ON observations(quality);
|
|
14130
|
+
CREATE INDEX IF NOT EXISTS idx_observations_user ON observations(user_id);
|
|
14131
|
+
CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by);
|
|
14132
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
|
|
14133
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
14134
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
14135
|
+
title, narrative, facts, concepts, content=observations, content_rowid=id
|
|
14136
|
+
);
|
|
14137
|
+
INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
|
|
14138
|
+
`);
|
|
14139
|
+
db.exec("COMMIT");
|
|
14140
|
+
} catch (err) {
|
|
14141
|
+
db.exec("ROLLBACK");
|
|
14142
|
+
}
|
|
14143
|
+
}
|
|
14144
|
+
}
|
|
14067
14145
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
14068
14146
|
|
|
14069
14147
|
// src/storage/sqlite.ts
|
|
@@ -14130,6 +14208,7 @@ class MemDatabase {
|
|
|
14130
14208
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
14131
14209
|
this.vecAvailable = this.loadVecExtension();
|
|
14132
14210
|
runMigrations(this.db);
|
|
14211
|
+
ensureObservationTypes(this.db);
|
|
14133
14212
|
}
|
|
14134
14213
|
loadVecExtension() {
|
|
14135
14214
|
try {
|
|
@@ -15098,7 +15177,8 @@ var VALID_TYPES = [
|
|
|
15098
15177
|
"feature",
|
|
15099
15178
|
"refactor",
|
|
15100
15179
|
"digest",
|
|
15101
|
-
"standard"
|
|
15180
|
+
"standard",
|
|
15181
|
+
"message"
|
|
15102
15182
|
];
|
|
15103
15183
|
async function saveObservation(db, config2, input) {
|
|
15104
15184
|
if (!VALID_TYPES.includes(input.type)) {
|
|
@@ -15391,7 +15471,7 @@ function estimateTokens(text) {
|
|
|
15391
15471
|
}
|
|
15392
15472
|
function buildSessionContext(db, cwd, options = {}) {
|
|
15393
15473
|
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
15394
|
-
const tokenBudget = opts.tokenBudget ??
|
|
15474
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
15395
15475
|
const maxCount = opts.maxCount;
|
|
15396
15476
|
const detected = detectProject(cwd);
|
|
15397
15477
|
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
@@ -15413,6 +15493,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15413
15493
|
AND superseded_by IS NULL
|
|
15414
15494
|
ORDER BY quality DESC, created_at_epoch DESC
|
|
15415
15495
|
LIMIT ?`).all(project.id, MAX_PINNED);
|
|
15496
|
+
const MAX_RECENT = 5;
|
|
15497
|
+
const recent = db.db.query(`SELECT * FROM observations
|
|
15498
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
15499
|
+
AND superseded_by IS NULL
|
|
15500
|
+
ORDER BY created_at_epoch DESC
|
|
15501
|
+
LIMIT ?`).all(project.id, MAX_RECENT);
|
|
15416
15502
|
const candidateLimit = maxCount ?? 50;
|
|
15417
15503
|
const candidates = db.db.query(`SELECT * FROM observations
|
|
15418
15504
|
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
@@ -15440,6 +15526,12 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15440
15526
|
});
|
|
15441
15527
|
}
|
|
15442
15528
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
15529
|
+
const dedupedRecent = recent.filter((o) => {
|
|
15530
|
+
if (seenIds.has(o.id))
|
|
15531
|
+
return false;
|
|
15532
|
+
seenIds.add(o.id);
|
|
15533
|
+
return true;
|
|
15534
|
+
});
|
|
15443
15535
|
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
15444
15536
|
for (const obs of crossProjectCandidates) {
|
|
15445
15537
|
if (!seenIds.has(obs.id)) {
|
|
@@ -15456,8 +15548,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15456
15548
|
return scoreB - scoreA;
|
|
15457
15549
|
});
|
|
15458
15550
|
if (maxCount !== undefined) {
|
|
15459
|
-
const remaining = Math.max(0, maxCount - pinned.length);
|
|
15460
|
-
const all = [...pinned, ...sorted.slice(0, remaining)];
|
|
15551
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
15552
|
+
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
15461
15553
|
return {
|
|
15462
15554
|
project_name: project.name,
|
|
15463
15555
|
canonical_id: project.canonical_id,
|
|
@@ -15473,6 +15565,11 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15473
15565
|
remainingBudget -= cost;
|
|
15474
15566
|
selected.push(obs);
|
|
15475
15567
|
}
|
|
15568
|
+
for (const obs of dedupedRecent) {
|
|
15569
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
15570
|
+
remainingBudget -= cost;
|
|
15571
|
+
selected.push(obs);
|
|
15572
|
+
}
|
|
15476
15573
|
for (const obs of sorted) {
|
|
15477
15574
|
const cost = estimateObservationTokens(obs, selected.length);
|
|
15478
15575
|
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
@@ -15480,7 +15577,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15480
15577
|
remainingBudget -= cost;
|
|
15481
15578
|
selected.push(obs);
|
|
15482
15579
|
}
|
|
15483
|
-
const summaries = db.getRecentSummaries(project.id,
|
|
15580
|
+
const summaries = db.getRecentSummaries(project.id, 5);
|
|
15484
15581
|
let securityFindings = [];
|
|
15485
15582
|
try {
|
|
15486
15583
|
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
@@ -15500,7 +15597,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
15500
15597
|
};
|
|
15501
15598
|
}
|
|
15502
15599
|
function estimateObservationTokens(obs, index) {
|
|
15503
|
-
const DETAILED_THRESHOLD =
|
|
15600
|
+
const DETAILED_THRESHOLD = 5;
|
|
15504
15601
|
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
15505
15602
|
if (index >= DETAILED_THRESHOLD) {
|
|
15506
15603
|
return titleCost;
|
|
@@ -15512,7 +15609,7 @@ function formatContextForInjection(context) {
|
|
|
15512
15609
|
if (context.observations.length === 0) {
|
|
15513
15610
|
return `Project: ${context.project_name} (no prior observations)`;
|
|
15514
15611
|
}
|
|
15515
|
-
const DETAILED_COUNT =
|
|
15612
|
+
const DETAILED_COUNT = 5;
|
|
15516
15613
|
const lines = [
|
|
15517
15614
|
`## Project Memory: ${context.project_name}`,
|
|
15518
15615
|
`${context.session_count} relevant observation(s) from prior sessions:`,
|
|
@@ -15892,6 +15989,13 @@ class VectorClient {
|
|
|
15892
15989
|
async sendTelemetry(beacon) {
|
|
15893
15990
|
await this.request("POST", "/v1/mem/telemetry", beacon);
|
|
15894
15991
|
}
|
|
15992
|
+
async fetchSettings() {
|
|
15993
|
+
try {
|
|
15994
|
+
return await this.request("GET", "/v1/mem/user-settings");
|
|
15995
|
+
} catch {
|
|
15996
|
+
return null;
|
|
15997
|
+
}
|
|
15998
|
+
}
|
|
15895
15999
|
async health() {
|
|
15896
16000
|
try {
|
|
15897
16001
|
await this.request("GET", "/health");
|
|
@@ -15991,6 +16095,7 @@ function buildVectorDocument(obs, config2, project) {
|
|
|
15991
16095
|
project_name: project.name,
|
|
15992
16096
|
user_id: obs.user_id,
|
|
15993
16097
|
device_id: obs.device_id,
|
|
16098
|
+
device_name: __require("node:os").hostname(),
|
|
15994
16099
|
agent: obs.agent,
|
|
15995
16100
|
title: obs.title,
|
|
15996
16101
|
narrative: obs.narrative,
|
|
@@ -16207,6 +16312,44 @@ function extractNarrative(content) {
|
|
|
16207
16312
|
`).trim();
|
|
16208
16313
|
return narrative.length > 0 ? narrative : null;
|
|
16209
16314
|
}
|
|
16315
|
+
async function pullSettings(client, config2) {
|
|
16316
|
+
try {
|
|
16317
|
+
const settings = await client.fetchSettings();
|
|
16318
|
+
if (!settings)
|
|
16319
|
+
return false;
|
|
16320
|
+
let changed = false;
|
|
16321
|
+
if (settings.transcript_analysis !== undefined) {
|
|
16322
|
+
const ta = settings.transcript_analysis;
|
|
16323
|
+
if (typeof ta === "object" && ta !== null) {
|
|
16324
|
+
const taObj = ta;
|
|
16325
|
+
if (taObj.enabled !== undefined && taObj.enabled !== config2.transcript_analysis.enabled) {
|
|
16326
|
+
config2.transcript_analysis.enabled = !!taObj.enabled;
|
|
16327
|
+
changed = true;
|
|
16328
|
+
}
|
|
16329
|
+
}
|
|
16330
|
+
}
|
|
16331
|
+
if (settings.observer !== undefined) {
|
|
16332
|
+
const obs = settings.observer;
|
|
16333
|
+
if (typeof obs === "object" && obs !== null) {
|
|
16334
|
+
const obsObj = obs;
|
|
16335
|
+
if (obsObj.enabled !== undefined && obsObj.enabled !== config2.observer.enabled) {
|
|
16336
|
+
config2.observer.enabled = !!obsObj.enabled;
|
|
16337
|
+
changed = true;
|
|
16338
|
+
}
|
|
16339
|
+
if (obsObj.model !== undefined && typeof obsObj.model === "string" && obsObj.model !== config2.observer.model) {
|
|
16340
|
+
config2.observer.model = obsObj.model;
|
|
16341
|
+
changed = true;
|
|
16342
|
+
}
|
|
16343
|
+
}
|
|
16344
|
+
}
|
|
16345
|
+
if (changed) {
|
|
16346
|
+
saveConfig(config2);
|
|
16347
|
+
}
|
|
16348
|
+
return changed;
|
|
16349
|
+
} catch {
|
|
16350
|
+
return false;
|
|
16351
|
+
}
|
|
16352
|
+
}
|
|
16210
16353
|
|
|
16211
16354
|
// src/sync/engine.ts
|
|
16212
16355
|
var DEFAULT_PULL_INTERVAL = 60000;
|
|
@@ -16271,6 +16414,7 @@ class SyncEngine {
|
|
|
16271
16414
|
this._pulling = true;
|
|
16272
16415
|
try {
|
|
16273
16416
|
await pullFromVector(this.db, this.client, this.config);
|
|
16417
|
+
await pullSettings(this.client, this.config);
|
|
16274
16418
|
} finally {
|
|
16275
16419
|
this._pulling = false;
|
|
16276
16420
|
}
|