engrm 0.2.2 → 0.3.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 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) {
@@ -1075,21 +1132,44 @@ async function openBrowser(url) {
1075
1132
  return false;
1076
1133
  }
1077
1134
  }
1135
+ var PAGE_STYLE = `
1136
+ *{margin:0;padding:0;box-sizing:border-box}
1137
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#06060e;color:#fff;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
1138
+ body::before{content:'';position:fixed;inset:0;z-index:-1;background:radial-gradient(ellipse at 30% 20%,rgba(0,212,255,0.06) 0%,transparent 50%),radial-gradient(ellipse at 70% 80%,rgba(123,44,191,0.06) 0%,transparent 50%)}
1139
+ .card{width:100%;max-width:440px;padding:48px 40px;border-radius:16px;border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.02);backdrop-filter:blur(20px);text-align:center}
1140
+ .logo{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:28px}
1141
+ .logo span{font-size:1.3rem;font-weight:700}
1142
+ .icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
1143
+ .icon.success{background:rgba(16,185,129,0.12);border:2px solid rgba(16,185,129,0.3)}
1144
+ .icon.error{background:rgba(239,68,68,0.12);border:2px solid rgba(239,68,68,0.3)}
1145
+ h1{font-size:1.4rem;font-weight:700;margin-bottom:8px}
1146
+ p{color:rgba(255,255,255,0.6);font-size:0.9rem;line-height:1.5}
1147
+ .hint{margin-top:24px;padding:12px 16px;font-size:0.82rem;color:rgba(255,255,255,0.4);background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:8px}
1148
+ a{color:#00d4ff;text-decoration:none}
1149
+ `;
1150
+ var LOGO_SVG = `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:32px;height:32px"><rect width="40" height="40" rx="10" fill="#0c0c1e"/><rect x="0.5" y="0.5" width="39" height="39" rx="9.5" stroke="rgba(255,255,255,0.08)"/><path d="M12 12h10M12 20h8M12 28h10M12 12v16" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><circle cx="13" cy="7" r="3" fill="#00d4ff"/><circle cx="33" cy="33" r="3" fill="#7b2cbf"/></svg>`;
1078
1151
  function successPage() {
1079
1152
  return `<!DOCTYPE html>
1080
- <html><head><title>Engrm</title>
1081
- <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f8f9fa}
1082
- .card{text-align:center;padding:3rem;border-radius:12px;background:white;box-shadow:0 2px 12px rgba(0,0,0,0.1)}
1083
- h1{color:#10b981;margin-bottom:0.5rem}p{color:#6b7280}</style></head>
1084
- <body><div class="card"><h1>Connected!</h1><p>You can close this tab and return to the terminal.</p></div></body></html>`;
1153
+ <html><head><title>Engrm — Connected</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect rx='6' width='32' height='32' fill='%230c0c1e'/><text x='7' y='24' font-family='system-ui' font-weight='700' font-size='22' fill='white'>E</text><circle cx='10' cy='6' r='3' fill='%2300d4ff'/><circle cx='26' cy='26' r='3' fill='%237b2cbf'/></svg>"><style>${PAGE_STYLE}</style></head>
1154
+ <body><div class="card">
1155
+ <div class="logo">${LOGO_SVG}<span>Engrm</span></div>
1156
+ <div class="icon success"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg></div>
1157
+ <h1>Connected!</h1>
1158
+ <p>Your device is now linked to your Engrm account. Memory will sync automatically across all your devices.</p>
1159
+ <div class="hint">You can close this tab and return to the terminal. Your next Claude Code session will have memory.</div>
1160
+ </div></body></html>`;
1085
1161
  }
1086
1162
  function errorPage(message) {
1163
+ const safeMessage = message.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
1087
1164
  return `<!DOCTYPE html>
1088
- <html><head><title>Engrm</title>
1089
- <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f8f9fa}
1090
- .card{text-align:center;padding:3rem;border-radius:12px;background:white;box-shadow:0 2px 12px rgba(0,0,0,0.1)}
1091
- h1{color:#ef4444;margin-bottom:0.5rem}p{color:#6b7280}</style></head>
1092
- <body><div class="card"><h1>Authorization Failed</h1><p>${message}</p></div></body></html>`;
1165
+ <html><head><title>Engrm — Error</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect rx='6' width='32' height='32' fill='%230c0c1e'/><text x='7' y='24' font-family='system-ui' font-weight='700' font-size='22' fill='white'>E</text><circle cx='10' cy='6' r='3' fill='%2300d4ff'/><circle cx='26' cy='26' r='3' fill='%237b2cbf'/></svg>"><style>${PAGE_STYLE}</style></head>
1166
+ <body><div class="card">
1167
+ <div class="logo">${LOGO_SVG}<span>Engrm</span></div>
1168
+ <div class="icon error"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></div>
1169
+ <h1>Authorization Failed</h1>
1170
+ <p>${safeMessage}</p>
1171
+ <div class="hint">Try running <code style="color:#00d4ff">engrm init</code> again, or use <code style="color:#00d4ff">engrm init --token=cmt_xxx</code> with a provisioning token from <a href="https://engrm.dev">engrm.dev</a>.</div>
1172
+ </div></body></html>`;
1093
1173
  }
1094
1174
 
1095
1175
  // src/register.ts
@@ -2070,6 +2150,9 @@ switch (command) {
2070
2150
  case "status":
2071
2151
  handleStatus();
2072
2152
  break;
2153
+ case "update":
2154
+ handleUpdate();
2155
+ break;
2073
2156
  case "install-pack":
2074
2157
  await handleInstallPack(args.slice(1));
2075
2158
  break;
@@ -2657,6 +2740,23 @@ function handleListPacks() {
2657
2740
  console.log(`
2658
2741
  Install with: engrm install-pack <name>`);
2659
2742
  }
2743
+ function handleUpdate() {
2744
+ const { execSync: execSync2 } = __require("child_process");
2745
+ console.log(`Updating Engrm to latest version...
2746
+ `);
2747
+ try {
2748
+ execSync2("npm install -g engrm@latest", { stdio: "inherit" });
2749
+ console.log(`
2750
+ Update complete. Re-registering hooks...`);
2751
+ const result = registerAll();
2752
+ console.log(` MCP server registered \u2192 ${result.mcp.path}`);
2753
+ console.log(` Hooks registered \u2192 ${result.hooks.path}`);
2754
+ console.log(`
2755
+ Restart Claude Code to use the new version.`);
2756
+ } catch (error) {
2757
+ console.error("Update failed. Try manually: npm install -g engrm@latest");
2758
+ }
2759
+ }
2660
2760
  function printPostInit() {
2661
2761
  console.log(`
2662
2762
  Registering with Claude Code...`);
@@ -2695,6 +2795,7 @@ function printUsage() {
2695
2795
  console.log(" engrm init --manual Manual setup (enter all values)");
2696
2796
  console.log(" engrm init --config <file> Setup from JSON file");
2697
2797
  console.log(" engrm status Show status");
2798
+ console.log(" engrm update Update to latest version");
2698
2799
  console.log(" engrm packs List available starter packs");
2699
2800
  console.log(" engrm install-pack <name> Install a starter pack");
2700
2801
  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.2",
3
+ "version": "0.3.0",
4
4
  "description": "Cross-device, team-shared memory layer for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",