engrm 0.2.3 → 0.3.1

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 CHANGED
@@ -460,6 +460,63 @@ var MIGRATIONS = [
460
460
  observation_count INTEGER DEFAULT 0
461
461
  );
462
462
  `
463
+ },
464
+ {
465
+ version: 8,
466
+ description: "Add message type to observations CHECK constraint",
467
+ sql: `
468
+ CREATE TABLE IF NOT EXISTS observations_v8 (
469
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
470
+ session_id TEXT,
471
+ project_id INTEGER NOT NULL REFERENCES projects(id),
472
+ type TEXT NOT NULL CHECK (type IN (
473
+ 'bugfix', 'discovery', 'decision', 'pattern',
474
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
475
+ )),
476
+ title TEXT NOT NULL,
477
+ narrative TEXT,
478
+ facts TEXT,
479
+ concepts TEXT,
480
+ files_read TEXT,
481
+ files_modified TEXT,
482
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
483
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
484
+ 'active', 'aging', 'archived', 'purged', 'pinned'
485
+ )),
486
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
487
+ 'shared', 'personal', 'secret'
488
+ )),
489
+ user_id TEXT NOT NULL,
490
+ device_id TEXT NOT NULL,
491
+ agent TEXT DEFAULT 'claude-code',
492
+ created_at TEXT NOT NULL,
493
+ created_at_epoch INTEGER NOT NULL,
494
+ archived_at_epoch INTEGER,
495
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
496
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
497
+ remote_source_id TEXT
498
+ );
499
+ INSERT INTO observations_v8 SELECT * FROM observations;
500
+ DROP TABLE observations;
501
+ ALTER TABLE observations_v8 RENAME TO observations;
502
+ CREATE INDEX idx_observations_project ON observations(project_id);
503
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
504
+ CREATE INDEX idx_observations_type ON observations(type);
505
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
506
+ CREATE INDEX idx_observations_session ON observations(session_id);
507
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
508
+ CREATE INDEX idx_observations_quality ON observations(quality);
509
+ CREATE INDEX idx_observations_user ON observations(user_id);
510
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
511
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
512
+ DROP TABLE IF EXISTS observations_fts;
513
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
514
+ title, narrative, facts, concepts,
515
+ content=observations,
516
+ content_rowid=id
517
+ );
518
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
519
+ `
463
520
  }
464
521
  ];
465
522
  function isVecExtensionLoaded(db) {
@@ -2093,6 +2150,9 @@ switch (command) {
2093
2150
  case "status":
2094
2151
  handleStatus();
2095
2152
  break;
2153
+ case "update":
2154
+ handleUpdate();
2155
+ break;
2096
2156
  case "install-pack":
2097
2157
  await handleInstallPack(args.slice(1));
2098
2158
  break;
@@ -2239,6 +2299,15 @@ Authorization failed: ${error instanceof Error ? error.message : String(error)}`
2239
2299
  }
2240
2300
  function writeConfigFromProvision(baseUrl, result) {
2241
2301
  ensureConfigDir();
2302
+ let existingDeviceId;
2303
+ let existingSentinel;
2304
+ if (configExists()) {
2305
+ try {
2306
+ const existing = loadConfig();
2307
+ existingDeviceId = existing.device_id;
2308
+ existingSentinel = existing.sentinel;
2309
+ } catch {}
2310
+ }
2242
2311
  const config = {
2243
2312
  candengo_url: baseUrl,
2244
2313
  candengo_api_key: result.api_key,
@@ -2246,7 +2315,7 @@ function writeConfigFromProvision(baseUrl, result) {
2246
2315
  namespace: result.namespace,
2247
2316
  user_id: result.user_id,
2248
2317
  user_email: result.user_email,
2249
- device_id: generateDeviceId2(),
2318
+ device_id: existingDeviceId || generateDeviceId2(),
2250
2319
  teams: result.teams ?? [],
2251
2320
  sync: {
2252
2321
  enabled: true,
@@ -2263,7 +2332,7 @@ function writeConfigFromProvision(baseUrl, result) {
2263
2332
  custom_patterns: [],
2264
2333
  default_sensitivity: "shared"
2265
2334
  },
2266
- sentinel: {
2335
+ sentinel: existingSentinel ?? {
2267
2336
  enabled: false,
2268
2337
  mode: "advisory",
2269
2338
  provider: "openai",
@@ -2680,6 +2749,23 @@ function handleListPacks() {
2680
2749
  console.log(`
2681
2750
  Install with: engrm install-pack <name>`);
2682
2751
  }
2752
+ function handleUpdate() {
2753
+ const { execSync: execSync2 } = __require("child_process");
2754
+ console.log(`Updating Engrm to latest version...
2755
+ `);
2756
+ try {
2757
+ execSync2("npm install -g engrm@latest", { stdio: "inherit" });
2758
+ console.log(`
2759
+ Update complete. Re-registering hooks...`);
2760
+ const result = registerAll();
2761
+ console.log(` MCP server registered \u2192 ${result.mcp.path}`);
2762
+ console.log(` Hooks registered \u2192 ${result.hooks.path}`);
2763
+ console.log(`
2764
+ Restart Claude Code to use the new version.`);
2765
+ } catch (error) {
2766
+ console.error("Update failed. Try manually: npm install -g engrm@latest");
2767
+ }
2768
+ }
2683
2769
  function printPostInit() {
2684
2770
  console.log(`
2685
2771
  Registering with Claude Code...`);
@@ -2718,6 +2804,7 @@ function printUsage() {
2718
2804
  console.log(" engrm init --manual Manual setup (enter all values)");
2719
2805
  console.log(" engrm init --config <file> Setup from JSON file");
2720
2806
  console.log(" engrm status Show status");
2807
+ console.log(" engrm update Update to latest version");
2721
2808
  console.log(" engrm packs List available starter packs");
2722
2809
  console.log(" engrm install-pack <name> Install a starter pack");
2723
2810
  console.log(" engrm sentinel Sentinel code audit commands");
@@ -426,6 +426,63 @@ var MIGRATIONS = [
426
426
  observation_count INTEGER DEFAULT 0
427
427
  );
428
428
  `
429
+ },
430
+ {
431
+ version: 8,
432
+ description: "Add message type to observations CHECK constraint",
433
+ sql: `
434
+ CREATE TABLE IF NOT EXISTS observations_v8 (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ session_id TEXT,
437
+ project_id INTEGER NOT NULL REFERENCES projects(id),
438
+ type TEXT NOT NULL CHECK (type IN (
439
+ 'bugfix', 'discovery', 'decision', 'pattern',
440
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
441
+ )),
442
+ title TEXT NOT NULL,
443
+ narrative TEXT,
444
+ facts TEXT,
445
+ concepts TEXT,
446
+ files_read TEXT,
447
+ files_modified TEXT,
448
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
449
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
450
+ 'active', 'aging', 'archived', 'purged', 'pinned'
451
+ )),
452
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
453
+ 'shared', 'personal', 'secret'
454
+ )),
455
+ user_id TEXT NOT NULL,
456
+ device_id TEXT NOT NULL,
457
+ agent TEXT DEFAULT 'claude-code',
458
+ created_at TEXT NOT NULL,
459
+ created_at_epoch INTEGER NOT NULL,
460
+ archived_at_epoch INTEGER,
461
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
462
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ remote_source_id TEXT
464
+ );
465
+ INSERT INTO observations_v8 SELECT * FROM observations;
466
+ DROP TABLE observations;
467
+ ALTER TABLE observations_v8 RENAME TO observations;
468
+ CREATE INDEX idx_observations_project ON observations(project_id);
469
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
470
+ CREATE INDEX idx_observations_type ON observations(type);
471
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
472
+ CREATE INDEX idx_observations_session ON observations(session_id);
473
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
474
+ CREATE INDEX idx_observations_quality ON observations(quality);
475
+ CREATE INDEX idx_observations_user ON observations(user_id);
476
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
477
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
478
+ DROP TABLE IF EXISTS observations_fts;
479
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
480
+ title, narrative, facts, concepts,
481
+ content=observations,
482
+ content_rowid=id
483
+ );
484
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
485
+ `
429
486
  }
430
487
  ];
431
488
  function isVecExtensionLoaded(db) {
@@ -426,6 +426,63 @@ var MIGRATIONS = [
426
426
  observation_count INTEGER DEFAULT 0
427
427
  );
428
428
  `
429
+ },
430
+ {
431
+ version: 8,
432
+ description: "Add message type to observations CHECK constraint",
433
+ sql: `
434
+ CREATE TABLE IF NOT EXISTS observations_v8 (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ session_id TEXT,
437
+ project_id INTEGER NOT NULL REFERENCES projects(id),
438
+ type TEXT NOT NULL CHECK (type IN (
439
+ 'bugfix', 'discovery', 'decision', 'pattern',
440
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
441
+ )),
442
+ title TEXT NOT NULL,
443
+ narrative TEXT,
444
+ facts TEXT,
445
+ concepts TEXT,
446
+ files_read TEXT,
447
+ files_modified TEXT,
448
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
449
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
450
+ 'active', 'aging', 'archived', 'purged', 'pinned'
451
+ )),
452
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
453
+ 'shared', 'personal', 'secret'
454
+ )),
455
+ user_id TEXT NOT NULL,
456
+ device_id TEXT NOT NULL,
457
+ agent TEXT DEFAULT 'claude-code',
458
+ created_at TEXT NOT NULL,
459
+ created_at_epoch INTEGER NOT NULL,
460
+ archived_at_epoch INTEGER,
461
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
462
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ remote_source_id TEXT
464
+ );
465
+ INSERT INTO observations_v8 SELECT * FROM observations;
466
+ DROP TABLE observations;
467
+ ALTER TABLE observations_v8 RENAME TO observations;
468
+ CREATE INDEX idx_observations_project ON observations(project_id);
469
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
470
+ CREATE INDEX idx_observations_type ON observations(type);
471
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
472
+ CREATE INDEX idx_observations_session ON observations(session_id);
473
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
474
+ CREATE INDEX idx_observations_quality ON observations(quality);
475
+ CREATE INDEX idx_observations_user ON observations(user_id);
476
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
477
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
478
+ DROP TABLE IF EXISTS observations_fts;
479
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
480
+ title, narrative, facts, concepts,
481
+ content=observations,
482
+ content_rowid=id
483
+ );
484
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
485
+ `
429
486
  }
430
487
  ];
431
488
  function isVecExtensionLoaded(db) {
@@ -426,6 +426,63 @@ var MIGRATIONS = [
426
426
  observation_count INTEGER DEFAULT 0
427
427
  );
428
428
  `
429
+ },
430
+ {
431
+ version: 8,
432
+ description: "Add message type to observations CHECK constraint",
433
+ sql: `
434
+ CREATE TABLE IF NOT EXISTS observations_v8 (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ session_id TEXT,
437
+ project_id INTEGER NOT NULL REFERENCES projects(id),
438
+ type TEXT NOT NULL CHECK (type IN (
439
+ 'bugfix', 'discovery', 'decision', 'pattern',
440
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
441
+ )),
442
+ title TEXT NOT NULL,
443
+ narrative TEXT,
444
+ facts TEXT,
445
+ concepts TEXT,
446
+ files_read TEXT,
447
+ files_modified TEXT,
448
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
449
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
450
+ 'active', 'aging', 'archived', 'purged', 'pinned'
451
+ )),
452
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
453
+ 'shared', 'personal', 'secret'
454
+ )),
455
+ user_id TEXT NOT NULL,
456
+ device_id TEXT NOT NULL,
457
+ agent TEXT DEFAULT 'claude-code',
458
+ created_at TEXT NOT NULL,
459
+ created_at_epoch INTEGER NOT NULL,
460
+ archived_at_epoch INTEGER,
461
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
462
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ remote_source_id TEXT
464
+ );
465
+ INSERT INTO observations_v8 SELECT * FROM observations;
466
+ DROP TABLE observations;
467
+ ALTER TABLE observations_v8 RENAME TO observations;
468
+ CREATE INDEX idx_observations_project ON observations(project_id);
469
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
470
+ CREATE INDEX idx_observations_type ON observations(type);
471
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
472
+ CREATE INDEX idx_observations_session ON observations(session_id);
473
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
474
+ CREATE INDEX idx_observations_quality ON observations(quality);
475
+ CREATE INDEX idx_observations_user ON observations(user_id);
476
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
477
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
478
+ DROP TABLE IF EXISTS observations_fts;
479
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
480
+ title, narrative, facts, concepts,
481
+ content=observations,
482
+ content_rowid=id
483
+ );
484
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
485
+ `
429
486
  }
430
487
  ];
431
488
  function isVecExtensionLoaded(db) {
@@ -426,6 +426,63 @@ var MIGRATIONS = [
426
426
  observation_count INTEGER DEFAULT 0
427
427
  );
428
428
  `
429
+ },
430
+ {
431
+ version: 8,
432
+ description: "Add message type to observations CHECK constraint",
433
+ sql: `
434
+ CREATE TABLE IF NOT EXISTS observations_v8 (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ session_id TEXT,
437
+ project_id INTEGER NOT NULL REFERENCES projects(id),
438
+ type TEXT NOT NULL CHECK (type IN (
439
+ 'bugfix', 'discovery', 'decision', 'pattern',
440
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
441
+ )),
442
+ title TEXT NOT NULL,
443
+ narrative TEXT,
444
+ facts TEXT,
445
+ concepts TEXT,
446
+ files_read TEXT,
447
+ files_modified TEXT,
448
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
449
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
450
+ 'active', 'aging', 'archived', 'purged', 'pinned'
451
+ )),
452
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
453
+ 'shared', 'personal', 'secret'
454
+ )),
455
+ user_id TEXT NOT NULL,
456
+ device_id TEXT NOT NULL,
457
+ agent TEXT DEFAULT 'claude-code',
458
+ created_at TEXT NOT NULL,
459
+ created_at_epoch INTEGER NOT NULL,
460
+ archived_at_epoch INTEGER,
461
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
462
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ remote_source_id TEXT
464
+ );
465
+ INSERT INTO observations_v8 SELECT * FROM observations;
466
+ DROP TABLE observations;
467
+ ALTER TABLE observations_v8 RENAME TO observations;
468
+ CREATE INDEX idx_observations_project ON observations(project_id);
469
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
470
+ CREATE INDEX idx_observations_type ON observations(type);
471
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
472
+ CREATE INDEX idx_observations_session ON observations(session_id);
473
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
474
+ CREATE INDEX idx_observations_quality ON observations(quality);
475
+ CREATE INDEX idx_observations_user ON observations(user_id);
476
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
477
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
478
+ DROP TABLE IF EXISTS observations_fts;
479
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
480
+ title, narrative, facts, concepts,
481
+ content=observations,
482
+ content_rowid=id
483
+ );
484
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
485
+ `
429
486
  }
430
487
  ];
431
488
  function isVecExtensionLoaded(db) {
@@ -427,6 +427,63 @@ var MIGRATIONS = [
427
427
  observation_count INTEGER DEFAULT 0
428
428
  );
429
429
  `
430
+ },
431
+ {
432
+ version: 8,
433
+ description: "Add message type to observations CHECK constraint",
434
+ sql: `
435
+ CREATE TABLE IF NOT EXISTS observations_v8 (
436
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
437
+ session_id TEXT,
438
+ project_id INTEGER NOT NULL REFERENCES projects(id),
439
+ type TEXT NOT NULL CHECK (type IN (
440
+ 'bugfix', 'discovery', 'decision', 'pattern',
441
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
442
+ )),
443
+ title TEXT NOT NULL,
444
+ narrative TEXT,
445
+ facts TEXT,
446
+ concepts TEXT,
447
+ files_read TEXT,
448
+ files_modified TEXT,
449
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
450
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
451
+ 'active', 'aging', 'archived', 'purged', 'pinned'
452
+ )),
453
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
454
+ 'shared', 'personal', 'secret'
455
+ )),
456
+ user_id TEXT NOT NULL,
457
+ device_id TEXT NOT NULL,
458
+ agent TEXT DEFAULT 'claude-code',
459
+ created_at TEXT NOT NULL,
460
+ created_at_epoch INTEGER NOT NULL,
461
+ archived_at_epoch INTEGER,
462
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
464
+ remote_source_id TEXT
465
+ );
466
+ INSERT INTO observations_v8 SELECT * FROM observations;
467
+ DROP TABLE observations;
468
+ ALTER TABLE observations_v8 RENAME TO observations;
469
+ CREATE INDEX idx_observations_project ON observations(project_id);
470
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
471
+ CREATE INDEX idx_observations_type ON observations(type);
472
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
473
+ CREATE INDEX idx_observations_session ON observations(session_id);
474
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
475
+ CREATE INDEX idx_observations_quality ON observations(quality);
476
+ CREATE INDEX idx_observations_user ON observations(user_id);
477
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
478
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
479
+ DROP TABLE IF EXISTS observations_fts;
480
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
481
+ title, narrative, facts, concepts,
482
+ content=observations,
483
+ content_rowid=id
484
+ );
485
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
486
+ `
430
487
  }
431
488
  ];
432
489
  function isVecExtensionLoaded(db) {
@@ -1741,6 +1798,14 @@ async function main() {
1741
1798
  if (remaining > 0) {
1742
1799
  parts.push(`${remaining} more searchable`);
1743
1800
  }
1801
+ try {
1802
+ const readKey = `messages_read_${config.device_id}`;
1803
+ const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
1804
+ const msgCount = db.db.query("SELECT COUNT(*) as c FROM observations WHERE type = 'message' AND id > ? AND lifecycle IN ('active', 'pinned')").get(lastReadId)?.c ?? 0;
1805
+ if (msgCount > 0) {
1806
+ parts.push(`${msgCount} unread message(s)`);
1807
+ }
1808
+ } catch {}
1744
1809
  console.error(`Engrm: ${parts.join(" \xB7 ")} \u2014 memory loaded`);
1745
1810
  }
1746
1811
  try {
@@ -426,6 +426,63 @@ var MIGRATIONS = [
426
426
  observation_count INTEGER DEFAULT 0
427
427
  );
428
428
  `
429
+ },
430
+ {
431
+ version: 8,
432
+ description: "Add message type to observations CHECK constraint",
433
+ sql: `
434
+ CREATE TABLE IF NOT EXISTS observations_v8 (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ session_id TEXT,
437
+ project_id INTEGER NOT NULL REFERENCES projects(id),
438
+ type TEXT NOT NULL CHECK (type IN (
439
+ 'bugfix', 'discovery', 'decision', 'pattern',
440
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
441
+ )),
442
+ title TEXT NOT NULL,
443
+ narrative TEXT,
444
+ facts TEXT,
445
+ concepts TEXT,
446
+ files_read TEXT,
447
+ files_modified TEXT,
448
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
449
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
450
+ 'active', 'aging', 'archived', 'purged', 'pinned'
451
+ )),
452
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
453
+ 'shared', 'personal', 'secret'
454
+ )),
455
+ user_id TEXT NOT NULL,
456
+ device_id TEXT NOT NULL,
457
+ agent TEXT DEFAULT 'claude-code',
458
+ created_at TEXT NOT NULL,
459
+ created_at_epoch INTEGER NOT NULL,
460
+ archived_at_epoch INTEGER,
461
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
462
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
463
+ remote_source_id TEXT
464
+ );
465
+ INSERT INTO observations_v8 SELECT * FROM observations;
466
+ DROP TABLE observations;
467
+ ALTER TABLE observations_v8 RENAME TO observations;
468
+ CREATE INDEX idx_observations_project ON observations(project_id);
469
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
470
+ CREATE INDEX idx_observations_type ON observations(type);
471
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
472
+ CREATE INDEX idx_observations_session ON observations(session_id);
473
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
474
+ CREATE INDEX idx_observations_quality ON observations(quality);
475
+ CREATE INDEX idx_observations_user ON observations(user_id);
476
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
477
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
478
+ DROP TABLE IF EXISTS observations_fts;
479
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
480
+ title, narrative, facts, concepts,
481
+ content=observations,
482
+ content_rowid=id
483
+ );
484
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
485
+ `
429
486
  }
430
487
  ];
431
488
  function isVecExtensionLoaded(db) {
package/dist/server.js CHANGED
@@ -13976,6 +13976,63 @@ var MIGRATIONS = [
13976
13976
  observation_count INTEGER DEFAULT 0
13977
13977
  );
13978
13978
  `
13979
+ },
13980
+ {
13981
+ version: 8,
13982
+ description: "Add message type to observations CHECK constraint",
13983
+ sql: `
13984
+ CREATE TABLE IF NOT EXISTS observations_v8 (
13985
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13986
+ session_id TEXT,
13987
+ project_id INTEGER NOT NULL REFERENCES projects(id),
13988
+ type TEXT NOT NULL CHECK (type IN (
13989
+ 'bugfix', 'discovery', 'decision', 'pattern',
13990
+ 'change', 'feature', 'refactor', 'digest', 'standard', 'message'
13991
+ )),
13992
+ title TEXT NOT NULL,
13993
+ narrative TEXT,
13994
+ facts TEXT,
13995
+ concepts TEXT,
13996
+ files_read TEXT,
13997
+ files_modified TEXT,
13998
+ quality REAL DEFAULT 0.5 CHECK (quality BETWEEN 0.0 AND 1.0),
13999
+ lifecycle TEXT DEFAULT 'active' CHECK (lifecycle IN (
14000
+ 'active', 'aging', 'archived', 'purged', 'pinned'
14001
+ )),
14002
+ sensitivity TEXT DEFAULT 'shared' CHECK (sensitivity IN (
14003
+ 'shared', 'personal', 'secret'
14004
+ )),
14005
+ user_id TEXT NOT NULL,
14006
+ device_id TEXT NOT NULL,
14007
+ agent TEXT DEFAULT 'claude-code',
14008
+ created_at TEXT NOT NULL,
14009
+ created_at_epoch INTEGER NOT NULL,
14010
+ archived_at_epoch INTEGER,
14011
+ compacted_into INTEGER REFERENCES observations(id) ON DELETE SET NULL,
14012
+ superseded_by INTEGER REFERENCES observations(id) ON DELETE SET NULL,
14013
+ remote_source_id TEXT
14014
+ );
14015
+ INSERT INTO observations_v8 SELECT * FROM observations;
14016
+ DROP TABLE observations;
14017
+ ALTER TABLE observations_v8 RENAME TO observations;
14018
+ CREATE INDEX idx_observations_project ON observations(project_id);
14019
+ CREATE INDEX idx_observations_project_lifecycle ON observations(project_id, lifecycle);
14020
+ CREATE INDEX idx_observations_type ON observations(type);
14021
+ CREATE INDEX idx_observations_created ON observations(created_at_epoch);
14022
+ CREATE INDEX idx_observations_session ON observations(session_id);
14023
+ CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
14024
+ CREATE INDEX idx_observations_quality ON observations(quality);
14025
+ CREATE INDEX idx_observations_user ON observations(user_id);
14026
+ CREATE INDEX idx_observations_superseded ON observations(superseded_by);
14027
+ CREATE UNIQUE INDEX idx_observations_remote_source ON observations(remote_source_id) WHERE remote_source_id IS NOT NULL;
14028
+ DROP TABLE IF EXISTS observations_fts;
14029
+ CREATE VIRTUAL TABLE observations_fts USING fts5(
14030
+ title, narrative, facts, concepts,
14031
+ content=observations,
14032
+ content_rowid=id
14033
+ );
14034
+ INSERT INTO observations_fts(observations_fts) VALUES('rebuild');
14035
+ `
13979
14036
  }
13980
14037
  ];
13981
14038
  function isVecExtensionLoaded(db) {
@@ -16333,7 +16390,8 @@ server.tool("save_observation", "Save an observation to memory", {
16333
16390
  "change",
16334
16391
  "feature",
16335
16392
  "refactor",
16336
- "digest"
16393
+ "digest",
16394
+ "message"
16337
16395
  ]),
16338
16396
  title: exports_external.string().describe("Brief title"),
16339
16397
  narrative: exports_external.string().optional().describe("What happened and why"),
@@ -16525,6 +16583,52 @@ server.tool("pin_observation", "Pin/unpin observation", {
16525
16583
  ]
16526
16584
  };
16527
16585
  });
16586
+ server.tool("check_messages", "Check for messages sent from other devices or sessions. Messages are cross-device notes left by you or your team.", {
16587
+ mark_read: exports_external.boolean().optional().describe("Mark messages as read after viewing (default: true)")
16588
+ }, async (params) => {
16589
+ const markRead = params.mark_read !== false;
16590
+ const readKey = `messages_read_${config2.device_id}`;
16591
+ const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
16592
+ const messages = db.db.query(`SELECT id, title, narrative, user_id, device_id, created_at FROM observations
16593
+ WHERE type = 'message' AND id > ? AND lifecycle IN ('active', 'pinned')
16594
+ ORDER BY created_at_epoch DESC LIMIT 20`).all(lastReadId);
16595
+ if (messages.length === 0) {
16596
+ return {
16597
+ content: [{ type: "text", text: "No new messages." }]
16598
+ };
16599
+ }
16600
+ if (markRead && messages.length > 0) {
16601
+ const maxId = Math.max(...messages.map((m) => m.id));
16602
+ db.setSyncState(readKey, String(maxId));
16603
+ }
16604
+ const lines = messages.map((m) => {
16605
+ const from = m.device_id === config2.device_id ? "you (this device)" : m.device_id;
16606
+ const ago = formatTimeAgo(m.created_at);
16607
+ return `[${ago}] from ${from}:
16608
+ ${m.title}${m.narrative ? `
16609
+ ` + m.narrative : ""}`;
16610
+ });
16611
+ return {
16612
+ content: [{
16613
+ type: "text",
16614
+ text: `${messages.length} message(s):
16615
+
16616
+ ${lines.join(`
16617
+
16618
+ `)}`
16619
+ }]
16620
+ };
16621
+ });
16622
+ function formatTimeAgo(isoDate) {
16623
+ const diff = Date.now() - new Date(isoDate).getTime();
16624
+ const mins = Math.floor(diff / 60000);
16625
+ if (mins < 60)
16626
+ return `${mins}m ago`;
16627
+ const hrs = Math.floor(mins / 60);
16628
+ if (hrs < 24)
16629
+ return `${hrs}h ago`;
16630
+ return `${Math.floor(hrs / 24)}d ago`;
16631
+ }
16528
16632
  server.tool("install_pack", "Install a help pack (pre-curated observations for a technology stack)", {
16529
16633
  pack_name: exports_external.string().describe("Pack name (e.g. 'typescript-patterns', 'react-gotchas')")
16530
16634
  }, async (params) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Cross-device, team-shared memory layer for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",