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/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 { randomBytes } from "node:crypto";
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
- const suffix = randomBytes(4).toString("hex");
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: "haiku"
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 ?? 800;
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, 2);
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 = 3;
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 = 3;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Cross-device, team-shared memory layer for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",