memoir-cli 3.11.3 → 3.14.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.
Files changed (76) hide show
  1. package/README.md +129 -124
  2. package/bin/memoir-work.js +9 -0
  3. package/bin/memoir.js +72 -8
  4. package/docs/AUDIT-REMEDIATION.md +55 -0
  5. package/docs/CASE_TAPE_AMNESIA.md +39 -0
  6. package/docs/HANDOFF-SECURITY-AUDIT.md +106 -0
  7. package/docs/LOCAL-HANDOFF-VALIDATION.md +129 -0
  8. package/docs/MCP-V2-MIGRATION.md +17 -0
  9. package/docs/PROJECT-HANDOFF.md +255 -0
  10. package/docs/PROJECT-VIEW-DEBUG.md +66 -0
  11. package/docs/PROJECT-VIEW-VALIDATION.md +136 -0
  12. package/docs/RELEASE-3.14-VALIDATION.md +36 -0
  13. package/docs/RELIABILITY-ROLLOUT.md +57 -0
  14. package/docs/RETRIEVAL-INDEX.md +45 -0
  15. package/docs/RETRIEVAL-RESULTS.md +26 -0
  16. package/docs/SPEC.md +684 -0
  17. package/evals/CONTINUITY-PROTOCOL.md +45 -0
  18. package/evals/cases.json +200 -0
  19. package/evals/results/retrieval-2026-09-05.json +5333 -0
  20. package/evals/retrieval-performance.mjs +99 -0
  21. package/evals/run.mjs +87 -0
  22. package/package.json +13 -5
  23. package/src/adapters/index.js +13 -6
  24. package/src/adapters/restore.js +83 -36
  25. package/src/cloud/auth.js +12 -15
  26. package/src/cloud/constants.js +6 -2
  27. package/src/cloud/storage.js +130 -93
  28. package/src/commands/activate.js +43 -9
  29. package/src/commands/cloud.js +56 -5
  30. package/src/commands/consolidate.js +49 -10
  31. package/src/commands/diff.js +2 -2
  32. package/src/commands/doctor.js +3 -3
  33. package/src/commands/forget.js +100 -0
  34. package/src/commands/push.js +164 -161
  35. package/src/commands/recall.js +42 -0
  36. package/src/commands/restore.js +32 -44
  37. package/src/commands/resume.js +15 -164
  38. package/src/commands/session.js +51 -9
  39. package/src/commands/snapshot.js +6 -7
  40. package/src/commands/status.js +23 -1
  41. package/src/commands/upgrade.js +13 -11
  42. package/src/commands/validate.js +16 -0
  43. package/src/commands/view.js +2 -2
  44. package/src/commands/why.js +4 -3
  45. package/src/config.js +9 -40
  46. package/src/context/capture.js +135 -33
  47. package/src/context/handoffs.js +72 -0
  48. package/src/events/summary.js +122 -0
  49. package/src/integrations/setup.js +88 -0
  50. package/src/mcp.js +151 -283
  51. package/src/memory/lexical-index.js +65 -0
  52. package/src/memory/repository.js +16 -0
  53. package/src/memory/scope.js +65 -0
  54. package/src/memory/search.js +598 -0
  55. package/src/memory/store.js +141 -0
  56. package/src/providers/index.js +182 -51
  57. package/src/providers/restore.js +5 -1
  58. package/src/security/encryption.js +34 -60
  59. package/src/security/files.js +155 -0
  60. package/src/session/brief.js +47 -0
  61. package/src/session/inject.js +12 -6
  62. package/src/session/lock.js +39 -118
  63. package/src/session/migrations.js +6 -0
  64. package/src/session/render.js +34 -4
  65. package/src/session/state.js +305 -34
  66. package/src/work/cli.js +64 -0
  67. package/src/work/errors.js +8 -0
  68. package/src/work/server.js +28 -0
  69. package/src/work/setup.js +96 -0
  70. package/src/work/store.js +340 -0
  71. package/src/work/ui/app.js +205 -0
  72. package/src/work/ui/index.html +30 -0
  73. package/src/work/ui/style.css +3 -0
  74. package/src/work/view.js +93 -0
  75. package/src/workspace/tracker.js +84 -332
  76. package/supabase/migrations/202609050001_backup_versions.sql +50 -0
@@ -0,0 +1,65 @@
1
+ // Rebuildable, process-local postings. Canonical files remain authoritative;
2
+ // nothing is persisted or trusted across a filesystem refresh or scope change.
3
+ export class LexicalIndex {
4
+ constructor() { this.clear(); }
5
+
6
+ clear() {
7
+ this.documents = new Set();
8
+ this.postings = new Map();
9
+ this.tokensByDocument = new Map();
10
+ this.vocabulary = null;
11
+ }
12
+
13
+ sync(documents) {
14
+ const current = new Set(documents);
15
+ for (const doc of this.documents) {
16
+ if (current.has(doc)) continue;
17
+ for (const token of this.tokensByDocument.get(doc)) {
18
+ const posting = this.postings.get(token);
19
+ posting.delete(doc);
20
+ if (!posting.size) { this.postings.delete(token); this.vocabulary = null; }
21
+ }
22
+ this.tokensByDocument.delete(doc);
23
+ }
24
+ for (const doc of current) {
25
+ if (this.documents.has(doc)) continue;
26
+ const tokens = new Set(Object.values(doc.tf).flatMap(field => [...field.keys()]));
27
+ this.tokensByDocument.set(doc, tokens);
28
+ for (const token of tokens) {
29
+ if (!this.postings.has(token)) { this.postings.set(token, new Set()); this.vocabulary = null; }
30
+ this.postings.get(token).add(doc);
31
+ }
32
+ }
33
+ this.documents = current;
34
+ }
35
+
36
+ lookup(terms) {
37
+ if (!this.vocabulary) this.vocabulary = [...this.postings.keys()].sort();
38
+ const documents = new Set();
39
+ const matches = new Map();
40
+ for (const term of terms) {
41
+ const tokens = new Map();
42
+ if (this.postings.has(term)) tokens.set(term, 1);
43
+ if (term.length >= 4) {
44
+ // Binary seek to query-prefix matches; no full vocabulary scan.
45
+ let lo = 0, hi = this.vocabulary.length;
46
+ while (lo < hi) {
47
+ const mid = (lo + hi) >>> 1;
48
+ if (this.vocabulary[mid] < term) lo = mid + 1; else hi = mid;
49
+ }
50
+ for (let i = lo; i < this.vocabulary.length && this.vocabulary[i].startsWith(term); i++) {
51
+ const token = this.vocabulary[i];
52
+ if (token !== term) tokens.set(token, .6);
53
+ }
54
+ // The reference scorer also allows a document token to prefix the query.
55
+ for (let n = 4; n < term.length; n++) {
56
+ const token = term.slice(0, n);
57
+ if (this.postings.has(token)) tokens.set(token, .6);
58
+ }
59
+ }
60
+ matches.set(term, tokens);
61
+ for (const token of tokens.keys()) for (const doc of this.postings.get(token)) documents.add(doc);
62
+ }
63
+ return { documents, matches };
64
+ }
65
+ }
@@ -0,0 +1,16 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ export function repositoryState(project) {
4
+ const run = args => execFileSync('git', args, { cwd: project, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }).trim();
5
+ try {
6
+ return {
7
+ root: run(['rev-parse', '--show-toplevel']),
8
+ head: run(['rev-parse', 'HEAD']),
9
+ branch: run(['branch', '--show-current']) || '(detached)',
10
+ // git status can execute fsmonitor hooks and clean filters from project
11
+ // configuration. Reading a memory record must not run that code. File
12
+ // hashes provide scoped check freshness; report overall dirtiness unknown.
13
+ dirty: null,
14
+ };
15
+ } catch { return { root: project, head: null, branch: null, dirty: null }; }
16
+ }
@@ -0,0 +1,65 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import { execFileSync } from 'child_process';
6
+
7
+ const cache = new Map();
8
+ export function projectIdentity(project = process.env.MEMOIR_PROJECT_ROOT || process.cwd()) {
9
+ if (project === 'shared') return 'shared';
10
+ if (/^(git|local):[a-f0-9]{32}$/.test(project)) return project;
11
+ let absolute = path.resolve(project.replace(/^~/, os.homedir()));
12
+ try { absolute = fs.realpathSync(absolute); } catch {}
13
+ const old = cache.get(absolute);
14
+ if (old && Date.now() - old.at < 60_000) return old.id;
15
+ let home = os.homedir();
16
+ try { home = fs.realpathSync(home); } catch {}
17
+ let key = path.relative(home, absolute).replace(/\\/g, '/');
18
+ let kind = 'local';
19
+ try {
20
+ const remote = execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: absolute, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
21
+ if (remote) {
22
+ // Normalize common SSH/HTTPS spellings; never persist embedded credentials.
23
+ key = remote.replace(/^git@([^:]+):/, '$1/').replace(/^https?:\/\/(?:[^/@]+@)?/, '').replace(/^ssh:\/\/git@/, '').replace(/\.git\/?$/, '').replace(/\/$/, '');
24
+ kind = 'git';
25
+ }
26
+ } catch {}
27
+ const id = kind + ':' + crypto.createHash('sha256').update(key).digest('hex').slice(0, 32);
28
+ cache.set(absolute, { id, at: Date.now() });
29
+ return id;
30
+ }
31
+
32
+ // Resolve project paths once per query, not once for every stored record.
33
+ // A new predicate is created on each query so time-based validity is current.
34
+ export function memoryVisibility({ project = process.env.MEMOIR_PROJECT_ROOT || process.cwd(), allProjects = false, now = Date.now() } = {}) {
35
+ let activeId, currentKey, sharedKey;
36
+ const identities = new Map();
37
+ return item => {
38
+ if (!item || item.hidden === true || item.hidden === 'true' || item.deleted === true || ['deleted', 'hidden', 'superseded'].includes(item.status) || item.superseded_by) return false;
39
+ if (item.valid_from && Date.parse(item.valid_from) > now) return false;
40
+ if (item.valid_until && Date.parse(item.valid_until) <= now) return false;
41
+ if (!allProjects && item.claudeProjectKey) {
42
+ currentKey ??= path.resolve(project.replace(/^~/, os.homedir())).replace(/[\\/:]/g, '-');
43
+ sharedKey ??= os.homedir().replace(/[\\/:]/g, '-');
44
+ if (item.claudeProjectKey !== currentKey && item.claudeProjectKey !== sharedKey) return false;
45
+ }
46
+ if (allProjects || !item.project || item.project === 'shared') return true;
47
+ activeId ??= projectIdentity(project);
48
+ const key = String(item.project);
49
+ if (!identities.has(key)) identities.set(key, projectIdentity(key));
50
+ return identities.get(key) === activeId;
51
+ };
52
+ }
53
+
54
+ export function visibleMemory(item, options = {}) {
55
+ return memoryVisibility(options)(item);
56
+ }
57
+
58
+ export function sessionView(state, options = {}) {
59
+ const current = { ...(state?.current || {}) };
60
+ for (const key of ['goals', 'next_actions', 'parked_actions', 'open_questions', 'decisions']) {
61
+ const archived = { goals: 'archived_goals', decisions: 'archived_decisions', open_questions: 'archived_questions' }[key];
62
+ current[key] = [...(current[key] || []), ...(archived ? current[archived] || [] : [])].filter(item => visibleMemory(item, options));
63
+ }
64
+ return { ...state, current, history: (state?.history || []).filter(item => visibleMemory(item, options)) };
65
+ }