engrm 0.3.4 → 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.
@@ -4,9 +4,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/config.ts
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { homedir, hostname } from "node:os";
7
+ import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { join } from "node:path";
9
- import { randomBytes } from "node:crypto";
9
+ import { createHash } from "node:crypto";
10
10
  var CONFIG_DIR = join(homedir(), ".engrm");
11
11
  var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
12
12
  var DB_PATH = join(CONFIG_DIR, "engrm.db");
@@ -15,7 +15,22 @@ function getDbPath() {
15
15
  }
16
16
  function generateDeviceId() {
17
17
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
18
- const suffix = randomBytes(4).toString("hex");
18
+ let mac = "";
19
+ const ifaces = networkInterfaces();
20
+ for (const entries of Object.values(ifaces)) {
21
+ if (!entries)
22
+ continue;
23
+ for (const entry of entries) {
24
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
25
+ mac = entry.mac;
26
+ break;
27
+ }
28
+ }
29
+ if (mac)
30
+ break;
31
+ }
32
+ const material = `${host}:${mac || "no-mac"}`;
33
+ const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
19
34
  return `${host}-${suffix}`;
20
35
  }
21
36
  function createDefaultConfig() {
@@ -57,7 +72,10 @@ function createDefaultConfig() {
57
72
  observer: {
58
73
  enabled: true,
59
74
  mode: "per_event",
60
- model: "haiku"
75
+ model: "sonnet"
76
+ },
77
+ transcript_analysis: {
78
+ enabled: false
61
79
  }
62
80
  };
63
81
  }
@@ -116,9 +134,19 @@ function loadConfig() {
116
134
  enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
117
135
  mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
118
136
  model: asString(config["observer"]?.["model"], defaults.observer.model)
137
+ },
138
+ transcript_analysis: {
139
+ enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
119
140
  }
120
141
  };
121
142
  }
143
+ function saveConfig(config) {
144
+ if (!existsSync(CONFIG_DIR)) {
145
+ mkdirSync(CONFIG_DIR, { recursive: true });
146
+ }
147
+ writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
148
+ `, "utf-8");
149
+ }
122
150
  function configExists() {
123
151
  return existsSync(SETTINGS_PATH);
124
152
  }
@@ -4,9 +4,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/config.ts
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { homedir, hostname } from "node:os";
7
+ import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { join } from "node:path";
9
- import { randomBytes } from "node:crypto";
9
+ import { createHash } from "node:crypto";
10
10
  var CONFIG_DIR = join(homedir(), ".engrm");
11
11
  var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
12
12
  var DB_PATH = join(CONFIG_DIR, "engrm.db");
@@ -15,7 +15,22 @@ function getDbPath() {
15
15
  }
16
16
  function generateDeviceId() {
17
17
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
18
- const suffix = randomBytes(4).toString("hex");
18
+ let mac = "";
19
+ const ifaces = networkInterfaces();
20
+ for (const entries of Object.values(ifaces)) {
21
+ if (!entries)
22
+ continue;
23
+ for (const entry of entries) {
24
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
25
+ mac = entry.mac;
26
+ break;
27
+ }
28
+ }
29
+ if (mac)
30
+ break;
31
+ }
32
+ const material = `${host}:${mac || "no-mac"}`;
33
+ const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
19
34
  return `${host}-${suffix}`;
20
35
  }
21
36
  function createDefaultConfig() {
@@ -57,7 +72,10 @@ function createDefaultConfig() {
57
72
  observer: {
58
73
  enabled: true,
59
74
  mode: "per_event",
60
- model: "haiku"
75
+ model: "sonnet"
76
+ },
77
+ transcript_analysis: {
78
+ enabled: false
61
79
  }
62
80
  };
63
81
  }
@@ -116,9 +134,19 @@ function loadConfig() {
116
134
  enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
117
135
  mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
118
136
  model: asString(config["observer"]?.["model"], defaults.observer.model)
137
+ },
138
+ transcript_analysis: {
139
+ enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
119
140
  }
120
141
  };
121
142
  }
143
+ function saveConfig(config) {
144
+ if (!existsSync(CONFIG_DIR)) {
145
+ mkdirSync(CONFIG_DIR, { recursive: true });
146
+ }
147
+ writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
148
+ `, "utf-8");
149
+ }
122
150
  function configExists() {
123
151
  return existsSync(SETTINGS_PATH);
124
152
  }
@@ -2330,6 +2358,229 @@ function extractFilesFromEvent(event) {
2330
2358
  return { files_modified: [filePath] };
2331
2359
  }
2332
2360
 
2361
+ // src/capture/recall.ts
2362
+ var VEC_DISTANCE_THRESHOLD = 0.25;
2363
+ function extractErrorSignature(output) {
2364
+ if (!output || output.length < 10)
2365
+ return null;
2366
+ const lines = output.split(`
2367
+ `);
2368
+ for (let i = lines.length - 1;i >= 0; i--) {
2369
+ const line = lines[i].trim();
2370
+ if (/^[A-Z]\w*(Error|Exception):\s/.test(line)) {
2371
+ return line.slice(0, 200);
2372
+ }
2373
+ }
2374
+ for (const line of lines) {
2375
+ const trimmed = line.trim();
2376
+ if (/^(TypeError|ReferenceError|SyntaxError|RangeError|Error):\s/.test(trimmed)) {
2377
+ return trimmed.slice(0, 200);
2378
+ }
2379
+ }
2380
+ for (const line of lines) {
2381
+ const match = line.match(/panicked at '(.+?)'/);
2382
+ if (match)
2383
+ return `panic: ${match[1].slice(0, 180)}`;
2384
+ }
2385
+ for (const line of lines) {
2386
+ const trimmed = line.trim();
2387
+ if (trimmed.startsWith("panic:"))
2388
+ return trimmed.slice(0, 200);
2389
+ }
2390
+ for (const line of lines) {
2391
+ const trimmed = line.trim();
2392
+ if (/^(error|Error|ERROR)\b[:\[]/.test(trimmed) && trimmed.length > 10) {
2393
+ return trimmed.slice(0, 200);
2394
+ }
2395
+ }
2396
+ for (const line of lines) {
2397
+ const match = line.match(/(E[A-Z]{2,}): (.+)/);
2398
+ if (match && /^E[A-Z]+$/.test(match[1])) {
2399
+ return `${match[1]}: ${match[2].slice(0, 180)}`;
2400
+ }
2401
+ }
2402
+ for (const line of lines) {
2403
+ const trimmed = line.trim();
2404
+ if (/^fatal:\s/.test(trimmed))
2405
+ return trimmed.slice(0, 200);
2406
+ }
2407
+ return null;
2408
+ }
2409
+ async function recallPastFix(db, errorSignature, projectId) {
2410
+ if (db.vecAvailable) {
2411
+ const embedding = await embedText(errorSignature);
2412
+ if (embedding) {
2413
+ const vecResults = db.searchVec(embedding, null, ["active", "aging", "pinned"], 10);
2414
+ for (const match of vecResults) {
2415
+ if (match.distance > VEC_DISTANCE_THRESHOLD)
2416
+ continue;
2417
+ const obs = db.getObservationById(match.observation_id);
2418
+ if (!obs)
2419
+ continue;
2420
+ if (obs.type !== "bugfix")
2421
+ continue;
2422
+ let projectName;
2423
+ if (projectId != null && obs.project_id !== projectId) {
2424
+ const proj = db.getProjectById(obs.project_id);
2425
+ if (proj)
2426
+ projectName = proj.name;
2427
+ }
2428
+ return {
2429
+ found: true,
2430
+ title: obs.title,
2431
+ narrative: truncateNarrative(obs.narrative, 200),
2432
+ observationId: obs.id,
2433
+ projectName,
2434
+ similarity: 1 - match.distance
2435
+ };
2436
+ }
2437
+ }
2438
+ }
2439
+ const ftsQuery = buildFtsQueryFromError(errorSignature);
2440
+ if (!ftsQuery)
2441
+ return { found: false };
2442
+ const ftsResults = db.searchFts(ftsQuery, null, ["active", "aging", "pinned"], 10);
2443
+ for (const match of ftsResults) {
2444
+ const obs = db.getObservationById(match.id);
2445
+ if (!obs)
2446
+ continue;
2447
+ if (obs.type !== "bugfix")
2448
+ continue;
2449
+ let projectName;
2450
+ if (projectId != null && obs.project_id !== projectId) {
2451
+ const proj = db.getProjectById(obs.project_id);
2452
+ if (proj)
2453
+ projectName = proj.name;
2454
+ }
2455
+ return {
2456
+ found: true,
2457
+ title: obs.title,
2458
+ narrative: truncateNarrative(obs.narrative, 200),
2459
+ observationId: obs.id,
2460
+ projectName
2461
+ };
2462
+ }
2463
+ return { found: false };
2464
+ }
2465
+ function buildFtsQueryFromError(error) {
2466
+ const cleaned = error.replace(/[{}()[\]^~*:'"`,.<>\/\\|]/g, " ").replace(/\b(at|in|of|the|is|to|from|for|with|a|an|and|or|not|no|on)\b/gi, " ").replace(/\b\d+\b/g, " ").replace(/\s+/g, " ").trim();
2467
+ const tokens = cleaned.split(" ").filter((t) => t.length >= 3);
2468
+ if (tokens.length === 0)
2469
+ return null;
2470
+ return tokens.slice(0, 5).join(" ");
2471
+ }
2472
+ function truncateNarrative(narrative, maxLen) {
2473
+ if (!narrative)
2474
+ return;
2475
+ if (narrative.length <= maxLen)
2476
+ return narrative;
2477
+ return narrative.slice(0, maxLen - 3) + "...";
2478
+ }
2479
+
2480
+ // src/capture/fatigue.ts
2481
+ var DEBOUNCE_MINUTES = 10;
2482
+ var DEFAULT_AVG_SESSION_MINUTES = 90;
2483
+ var DEFAULT_P90_SESSION_MINUTES = 180;
2484
+ var ERROR_ACCELERATION_THRESHOLD = 2;
2485
+ var RECENT_WINDOW_MINUTES = 30;
2486
+ function computeUserStats(db) {
2487
+ const rows = db.db.query(`SELECT (completed_at_epoch - started_at_epoch) / 60.0 AS duration
2488
+ FROM sessions
2489
+ WHERE status = 'completed'
2490
+ AND started_at_epoch IS NOT NULL
2491
+ AND completed_at_epoch IS NOT NULL
2492
+ AND completed_at_epoch > started_at_epoch
2493
+ ORDER BY duration ASC`).all();
2494
+ if (rows.length < 3) {
2495
+ return {
2496
+ avgDurationMinutes: DEFAULT_AVG_SESSION_MINUTES,
2497
+ p90DurationMinutes: DEFAULT_P90_SESSION_MINUTES
2498
+ };
2499
+ }
2500
+ const durations = rows.map((r) => r.duration);
2501
+ const sum = durations.reduce((a, b) => a + b, 0);
2502
+ const avg = sum / durations.length;
2503
+ const p90Index = Math.floor(durations.length * 0.9);
2504
+ const p90 = durations[Math.min(p90Index, durations.length - 1)];
2505
+ return {
2506
+ avgDurationMinutes: avg,
2507
+ p90DurationMinutes: p90
2508
+ };
2509
+ }
2510
+ function computeErrorAcceleration(db, sessionId, nowEpoch) {
2511
+ const recentWindowStart = nowEpoch - RECENT_WINDOW_MINUTES * 60;
2512
+ const sessionRow = db.db.query("SELECT started_at_epoch FROM sessions WHERE session_id = ?").get(sessionId);
2513
+ if (!sessionRow || !sessionRow.started_at_epoch) {
2514
+ return { ratio: 0, recentCount: 0, sessionCount: 0 };
2515
+ }
2516
+ const sessionStartEpoch = sessionRow.started_at_epoch;
2517
+ const sessionMinutes = (nowEpoch - sessionStartEpoch) / 60;
2518
+ if (sessionMinutes < 5) {
2519
+ return { ratio: 0, recentCount: 0, sessionCount: 0 };
2520
+ }
2521
+ const totalRow = db.db.query(`SELECT COUNT(*) as cnt FROM observations
2522
+ WHERE session_id = ? AND type = 'bugfix'`).get(sessionId);
2523
+ const sessionCount = totalRow?.cnt ?? 0;
2524
+ const recentRow = db.db.query(`SELECT COUNT(*) as cnt FROM observations
2525
+ WHERE session_id = ? AND type = 'bugfix' AND created_at_epoch >= ?`).get(sessionId, recentWindowStart);
2526
+ const recentCount = recentRow?.cnt ?? 0;
2527
+ if (sessionCount === 0) {
2528
+ return { ratio: 0, recentCount: 0, sessionCount: 0 };
2529
+ }
2530
+ const sessionRate = sessionCount / sessionMinutes * RECENT_WINDOW_MINUTES;
2531
+ const recentRate = recentCount;
2532
+ if (sessionRate === 0) {
2533
+ return { ratio: 0, recentCount, sessionCount };
2534
+ }
2535
+ return {
2536
+ ratio: recentRate / sessionRate,
2537
+ recentCount,
2538
+ sessionCount
2539
+ };
2540
+ }
2541
+ function checkSessionFatigue(db, sessionId) {
2542
+ const nowEpoch = Math.floor(Date.now() / 1000);
2543
+ const debounceKey = `fatigue_last_check:${sessionId}`;
2544
+ const lastCheck = db.db.query("SELECT value FROM sync_state WHERE key = ?").get(debounceKey);
2545
+ if (lastCheck) {
2546
+ const lastCheckEpoch = parseInt(lastCheck.value, 10);
2547
+ if (nowEpoch - lastCheckEpoch < DEBOUNCE_MINUTES * 60) {
2548
+ return { fatigued: false, sessionMinutes: 0, recentErrorRate: 0 };
2549
+ }
2550
+ }
2551
+ db.db.query("INSERT OR REPLACE INTO sync_state (key, value) VALUES (?, ?)").run(debounceKey, String(nowEpoch));
2552
+ const sessionRow = db.db.query("SELECT started_at_epoch FROM sessions WHERE session_id = ?").get(sessionId);
2553
+ if (!sessionRow || !sessionRow.started_at_epoch) {
2554
+ return { fatigued: false, sessionMinutes: 0, recentErrorRate: 0 };
2555
+ }
2556
+ const sessionMinutes = (nowEpoch - sessionRow.started_at_epoch) / 60;
2557
+ const acceleration = computeErrorAcceleration(db, sessionId, nowEpoch);
2558
+ const stats = computeUserStats(db);
2559
+ const reasons = [];
2560
+ if (acceleration.ratio >= ERROR_ACCELERATION_THRESHOLD && acceleration.recentCount >= 2) {
2561
+ reasons.push(`your error rate in the last 30 min is ${acceleration.ratio.toFixed(1)}x your session average`);
2562
+ }
2563
+ if (sessionMinutes > stats.p90DurationMinutes) {
2564
+ const hours = Math.floor(sessionMinutes / 60);
2565
+ const mins = Math.round(sessionMinutes % 60);
2566
+ reasons.push(`this session (${hours}h${mins}m) is longer than 90% of your past sessions`);
2567
+ }
2568
+ if (reasons.length === 0) {
2569
+ return {
2570
+ fatigued: false,
2571
+ sessionMinutes,
2572
+ recentErrorRate: acceleration.recentCount
2573
+ };
2574
+ }
2575
+ const message = `Consider taking a break — ${reasons.join(", and ")}.`;
2576
+ return {
2577
+ fatigued: true,
2578
+ message,
2579
+ sessionMinutes,
2580
+ recentErrorRate: acceleration.recentCount
2581
+ };
2582
+ }
2583
+
2333
2584
  // hooks/post-tool-use.ts
2334
2585
  async function main() {
2335
2586
  const chunks = [];
@@ -2357,6 +2608,9 @@ async function main() {
2357
2608
  }
2358
2609
  try {
2359
2610
  if (event.session_id) {
2611
+ const detected = detectProject(event.cwd);
2612
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
2613
+ db.upsertSession(event.session_id, project?.id ?? null, config.user_id, config.device_id);
2360
2614
  const metricsIncrement = {
2361
2615
  toolCalls: 1
2362
2616
  };
@@ -2402,6 +2656,35 @@ async function main() {
2402
2656
  });
2403
2657
  }
2404
2658
  }
2659
+ if (event.tool_name === "Bash" && event.tool_response) {
2660
+ const sig = extractErrorSignature(event.tool_response);
2661
+ if (sig) {
2662
+ try {
2663
+ const detected = detectProject(event.cwd);
2664
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
2665
+ const recall = await recallPastFix(db, sig, project?.id ?? null);
2666
+ if (recall.found) {
2667
+ const projectLabel = recall.projectName ? ` (from ${recall.projectName})` : "";
2668
+ console.error(`
2669
+ \uD83D\uDCA1 Engrm: You solved this before${projectLabel}: "${recall.title}"`);
2670
+ if (recall.narrative) {
2671
+ console.error(` ${recall.narrative}`);
2672
+ }
2673
+ }
2674
+ } catch {}
2675
+ }
2676
+ }
2677
+ if (event.tool_name === "Bash" && event.tool_response && event.session_id) {
2678
+ if (extractErrorSignature(event.tool_response)) {
2679
+ try {
2680
+ const fatigue = checkSessionFatigue(db, event.session_id);
2681
+ if (fatigue.fatigued && fatigue.message) {
2682
+ console.error(`
2683
+ \uD83D\uDCA1 Engrm: ${fatigue.message}`);
2684
+ }
2685
+ } catch {}
2686
+ }
2687
+ }
2405
2688
  let saved = false;
2406
2689
  if (config.observer?.enabled !== false) {
2407
2690
  try {
@@ -4,9 +4,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/config.ts
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { homedir, hostname } from "node:os";
7
+ import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { join } from "node:path";
9
- import { randomBytes } from "node:crypto";
9
+ import { createHash } from "node:crypto";
10
10
  var CONFIG_DIR = join(homedir(), ".engrm");
11
11
  var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
12
12
  var DB_PATH = join(CONFIG_DIR, "engrm.db");
@@ -15,7 +15,22 @@ function getDbPath() {
15
15
  }
16
16
  function generateDeviceId() {
17
17
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
18
- const suffix = randomBytes(4).toString("hex");
18
+ let mac = "";
19
+ const ifaces = networkInterfaces();
20
+ for (const entries of Object.values(ifaces)) {
21
+ if (!entries)
22
+ continue;
23
+ for (const entry of entries) {
24
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
25
+ mac = entry.mac;
26
+ break;
27
+ }
28
+ }
29
+ if (mac)
30
+ break;
31
+ }
32
+ const material = `${host}:${mac || "no-mac"}`;
33
+ const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
19
34
  return `${host}-${suffix}`;
20
35
  }
21
36
  function createDefaultConfig() {
@@ -57,7 +72,10 @@ function createDefaultConfig() {
57
72
  observer: {
58
73
  enabled: true,
59
74
  mode: "per_event",
60
- model: "haiku"
75
+ model: "sonnet"
76
+ },
77
+ transcript_analysis: {
78
+ enabled: false
61
79
  }
62
80
  };
63
81
  }
@@ -116,9 +134,19 @@ function loadConfig() {
116
134
  enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
117
135
  mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
118
136
  model: asString(config["observer"]?.["model"], defaults.observer.model)
137
+ },
138
+ transcript_analysis: {
139
+ enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
119
140
  }
120
141
  };
121
142
  }
143
+ function saveConfig(config) {
144
+ if (!existsSync(CONFIG_DIR)) {
145
+ mkdirSync(CONFIG_DIR, { recursive: true });
146
+ }
147
+ writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
148
+ `, "utf-8");
149
+ }
122
150
  function configExists() {
123
151
  return existsSync(SETTINGS_PATH);
124
152
  }
@@ -1108,7 +1136,7 @@ function estimateTokens(text) {
1108
1136
  }
1109
1137
  function buildSessionContext(db, cwd, options = {}) {
1110
1138
  const opts = typeof options === "number" ? { maxCount: options } : options;
1111
- const tokenBudget = opts.tokenBudget ?? 800;
1139
+ const tokenBudget = opts.tokenBudget ?? 3000;
1112
1140
  const maxCount = opts.maxCount;
1113
1141
  const detected = detectProject(cwd);
1114
1142
  const project = db.getProjectByCanonicalId(detected.canonical_id);
@@ -1130,6 +1158,12 @@ function buildSessionContext(db, cwd, options = {}) {
1130
1158
  AND superseded_by IS NULL
1131
1159
  ORDER BY quality DESC, created_at_epoch DESC
1132
1160
  LIMIT ?`).all(project.id, MAX_PINNED);
1161
+ const MAX_RECENT = 5;
1162
+ const recent = db.db.query(`SELECT * FROM observations
1163
+ WHERE project_id = ? AND lifecycle IN ('active', 'aging')
1164
+ AND superseded_by IS NULL
1165
+ ORDER BY created_at_epoch DESC
1166
+ LIMIT ?`).all(project.id, MAX_RECENT);
1133
1167
  const candidateLimit = maxCount ?? 50;
1134
1168
  const candidates = db.db.query(`SELECT * FROM observations
1135
1169
  WHERE project_id = ? AND lifecycle IN ('active', 'aging')
@@ -1157,6 +1191,12 @@ function buildSessionContext(db, cwd, options = {}) {
1157
1191
  });
1158
1192
  }
1159
1193
  const seenIds = new Set(pinned.map((o) => o.id));
1194
+ const dedupedRecent = recent.filter((o) => {
1195
+ if (seenIds.has(o.id))
1196
+ return false;
1197
+ seenIds.add(o.id);
1198
+ return true;
1199
+ });
1160
1200
  const deduped = candidates.filter((o) => !seenIds.has(o.id));
1161
1201
  for (const obs of crossProjectCandidates) {
1162
1202
  if (!seenIds.has(obs.id)) {
@@ -1173,8 +1213,8 @@ function buildSessionContext(db, cwd, options = {}) {
1173
1213
  return scoreB - scoreA;
1174
1214
  });
1175
1215
  if (maxCount !== undefined) {
1176
- const remaining = Math.max(0, maxCount - pinned.length);
1177
- const all = [...pinned, ...sorted.slice(0, remaining)];
1216
+ const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
1217
+ const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
1178
1218
  return {
1179
1219
  project_name: project.name,
1180
1220
  canonical_id: project.canonical_id,
@@ -1190,6 +1230,11 @@ function buildSessionContext(db, cwd, options = {}) {
1190
1230
  remainingBudget -= cost;
1191
1231
  selected.push(obs);
1192
1232
  }
1233
+ for (const obs of dedupedRecent) {
1234
+ const cost = estimateObservationTokens(obs, selected.length);
1235
+ remainingBudget -= cost;
1236
+ selected.push(obs);
1237
+ }
1193
1238
  for (const obs of sorted) {
1194
1239
  const cost = estimateObservationTokens(obs, selected.length);
1195
1240
  if (remainingBudget - cost < 0 && selected.length > 0)
@@ -1197,7 +1242,7 @@ function buildSessionContext(db, cwd, options = {}) {
1197
1242
  remainingBudget -= cost;
1198
1243
  selected.push(obs);
1199
1244
  }
1200
- const summaries = db.getRecentSummaries(project.id, 2);
1245
+ const summaries = db.getRecentSummaries(project.id, 5);
1201
1246
  let securityFindings = [];
1202
1247
  try {
1203
1248
  const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
@@ -1217,7 +1262,7 @@ function buildSessionContext(db, cwd, options = {}) {
1217
1262
  };
1218
1263
  }
1219
1264
  function estimateObservationTokens(obs, index) {
1220
- const DETAILED_THRESHOLD = 3;
1265
+ const DETAILED_THRESHOLD = 5;
1221
1266
  const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
1222
1267
  if (index >= DETAILED_THRESHOLD) {
1223
1268
  return titleCost;
@@ -1229,7 +1274,7 @@ function formatContextForInjection(context) {
1229
1274
  if (context.observations.length === 0) {
1230
1275
  return `Project: ${context.project_name} (no prior observations)`;
1231
1276
  }
1232
- const DETAILED_COUNT = 3;
1277
+ const DETAILED_COUNT = 5;
1233
1278
  const lines = [
1234
1279
  `## Project Memory: ${context.project_name}`,
1235
1280
  `${context.session_count} relevant observation(s) from prior sessions:`,
@@ -4,9 +4,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/config.ts
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
- import { homedir, hostname } from "node:os";
7
+ import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { join } from "node:path";
9
- import { randomBytes } from "node:crypto";
9
+ import { createHash } from "node:crypto";
10
10
  var CONFIG_DIR = join(homedir(), ".engrm");
11
11
  var SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
12
12
  var DB_PATH = join(CONFIG_DIR, "engrm.db");
@@ -15,7 +15,22 @@ function getDbPath() {
15
15
  }
16
16
  function generateDeviceId() {
17
17
  const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
18
- const suffix = randomBytes(4).toString("hex");
18
+ let mac = "";
19
+ const ifaces = networkInterfaces();
20
+ for (const entries of Object.values(ifaces)) {
21
+ if (!entries)
22
+ continue;
23
+ for (const entry of entries) {
24
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
25
+ mac = entry.mac;
26
+ break;
27
+ }
28
+ }
29
+ if (mac)
30
+ break;
31
+ }
32
+ const material = `${host}:${mac || "no-mac"}`;
33
+ const suffix = createHash("sha256").update(material).digest("hex").slice(0, 8);
19
34
  return `${host}-${suffix}`;
20
35
  }
21
36
  function createDefaultConfig() {
@@ -57,7 +72,10 @@ function createDefaultConfig() {
57
72
  observer: {
58
73
  enabled: true,
59
74
  mode: "per_event",
60
- model: "haiku"
75
+ model: "sonnet"
76
+ },
77
+ transcript_analysis: {
78
+ enabled: false
61
79
  }
62
80
  };
63
81
  }
@@ -116,9 +134,19 @@ function loadConfig() {
116
134
  enabled: asBool(config["observer"]?.["enabled"], defaults.observer.enabled),
117
135
  mode: asObserverMode(config["observer"]?.["mode"], defaults.observer.mode),
118
136
  model: asString(config["observer"]?.["model"], defaults.observer.model)
137
+ },
138
+ transcript_analysis: {
139
+ enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
119
140
  }
120
141
  };
121
142
  }
143
+ function saveConfig(config) {
144
+ if (!existsSync(CONFIG_DIR)) {
145
+ mkdirSync(CONFIG_DIR, { recursive: true });
146
+ }
147
+ writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + `
148
+ `, "utf-8");
149
+ }
122
150
  function configExists() {
123
151
  return existsSync(SETTINGS_PATH);
124
152
  }