lucid-mem 0.4.0 → 0.5.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/README.md CHANGED
@@ -34,7 +34,7 @@ Memory lives in `~/agent-memory` — a real git repo you can read, diff, and poi
34
34
 
35
35
  ## Multi-repo memory
36
36
 
37
- One memory repo serves every project. `projects/<name>-<hash>.md` files key off the repo's origin remote, so clones share memory and same-named repos don't collide. `amr link` attaches related projects (transitively); `private` policy keeps a project out of transitive auto-attach; `noglobal` skips global memory for a repo.
37
+ One memory repo serves every project. `projects/<name>-<hash>.md` files key off the repo's origin remote, so clones share memory and same-named repos don't collide. Inside a known repo, `amr context` is project-scoped — the project's latest 25 plus linked siblings (up to 8 projects). Outside one, it shows global bullets plus the latest 25 pooled across all projects. `private` policy keeps a project out of transitive auto-attach and the pooled digest; `noglobal` keeps it out of the pooled digest only.
38
38
 
39
39
  ## MCP
40
40
 
package/lib/cli.mjs CHANGED
@@ -26,7 +26,7 @@ const USAGE = `amr: agent memory CLI (lucid-mem)
26
26
  usage: amr <command> [args] [flags]
27
27
 
28
28
  ensure create/verify the memory repo ($AMR_HOME, default ~/agent-memory)
29
- context [dir] print session memory digest (global + current project, capped; --full for all)
29
+ context [dir] session digest: project-scoped inside a known repo; global + pooled projects outside
30
30
  add "text" save a memory entry
31
31
  update "match" "text" replace an existing entry in place
32
32
  search "term" grep all memory files
package/lib/commands.mjs CHANGED
@@ -77,14 +77,20 @@ export function cmdContext(flags) {
77
77
  ];
78
78
  const cfg = loadConfig(home);
79
79
  const entry = proj ? projectEntry(cfg, proj) : null;
80
- // noglobal: this repo's sessions omit the global section.
81
- const globalLines = entry?.noglobal
82
- ? []
83
- : cap(bullets(idxSplit[0] || ''), 25);
80
+ // Two modes: inside a repo that has a memory file, the digest is
81
+ // project-scoped — this project plus its linked siblings only, no
82
+ // global bullets. Outside any known project (or a repo with no
83
+ // memory yet), it is global bullets plus a pooled digest of the
84
+ // latest entries across all projects.
85
+ const projHasFile =
86
+ !!proj &&
87
+ fs.existsSync(path.join(home, 'projects', `${proj.slug}.md`));
88
+ const inProject = projHasFile;
89
+ const globalLines = inProject ? [] : cap(bullets(idxSplit[0] || ''), 25);
84
90
  const indexLinks = cap(bullets(idxSplit[1] || ''), 12);
85
91
 
86
92
  // Attach list: the current project, then its config links
87
- // (transitive, deduped, max 5 total) and any --also names.
93
+ // (transitive, deduped, max 8 total) and any --also names.
88
94
  // private blocks reachability from repos that never linked it:
89
95
  // direct links (siblings) and explicit --also attach fine; it only
90
96
  // refuses when reached transitively through another project's links.
@@ -92,7 +98,7 @@ export function cmdContext(flags) {
92
98
  const attach = [];
93
99
  const seen = new Set();
94
100
  const push = (slug, linked) => {
95
- if (seen.has(slug) || attach.length >= 5) return;
101
+ if (seen.has(slug) || attach.length >= 8) return;
96
102
  seen.add(slug);
97
103
  attach.push({ slug, name: bySlug.get(slug)?.name || slug, linked });
98
104
  };
@@ -106,7 +112,7 @@ export function cmdContext(flags) {
106
112
  );
107
113
  const refused = [];
108
114
  const unknown = [];
109
- while (queue.length && attach.length < 5) {
115
+ while (queue.length && attach.length < 8) {
110
116
  const { k: raw, via } = queue.shift();
111
117
  const key = String(raw).trim().toLowerCase();
112
118
  if (!key) continue;
@@ -131,13 +137,33 @@ export function cmdContext(flags) {
131
137
  for (const a of attach) {
132
138
  const pf = path.join(home, 'projects', `${a.slug}.md`);
133
139
  if (!fs.existsSync(pf)) continue;
134
- const body = cap(
135
- bullets(fs.readFileSync(pf, 'utf8')),
136
- a.linked ? 12 : 25
137
- ).join('\n');
140
+ const body = cap(bullets(fs.readFileSync(pf, 'utf8')), 25).join('\n');
138
141
  lines.push('', `## Project: ${a.name}${a.linked ? ' (linked)' : ''}`);
139
142
  if (body) lines.push(body);
140
143
  }
144
+ // Outside-mode digest: the latest 25 project entries pooled across
145
+ // every project file, newest first by added: date. private projects
146
+ // never contribute (must not leak into foreign sessions); noglobal
147
+ // projects opt out of the pooled view the same way.
148
+ if (!inProject) {
149
+ const pool = [];
150
+ for (const p of listProjects(home)) {
151
+ const e = projectEntry(cfg, p);
152
+ if (e?.private || e?.noglobal) continue;
153
+ const pf = path.join(home, 'projects', `${p.slug}.md`);
154
+ for (const b of bullets(fs.readFileSync(pf, 'utf8'))) {
155
+ const d = (b.match(/added: (\d{4}-\d{2}-\d{2})/) || [])[1] || '';
156
+ pool.push({
157
+ d,
158
+ b: /project:/.test(b) ? b : b.replace(/^- /, `- (${p.name}) `),
159
+ });
160
+ }
161
+ }
162
+ pool.sort((a, b) => b.d.localeCompare(a.d));
163
+ const recent = cap(pool.map((x) => x.b), 25);
164
+ if (recent.length)
165
+ lines.push('', '## Recent project memory', recent.join('\n'));
166
+ }
141
167
  if (indexLinks.length) lines.push('', '## Index', indexLinks.join('\n'));
142
168
  if (refused.length)
143
169
  lines.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucid-mem",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "lucid-mem (amr) - autonomous git-backed agent memory (Agent Memory Repo spec)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,16 +82,38 @@ test('unlink removes the auto-attach', () => {
82
82
  assert.ok(!run(['context'], { amrHome: home, cwd: repoB }).stdout.includes('x1 fact'));
83
83
  });
84
84
 
85
- test('noglobal policy suppresses global memory for that repo', () => {
85
+ test('context in a known project is project-scoped (no global section)', () => {
86
86
  const home = tmpdir();
87
87
  const repo = initRepo(path.join(tmpdir(), 'iso'), 'https://github.com/acme/iso.git');
88
88
  run(['ensure'], { amrHome: home });
89
89
  run(['add', 'global pref'], { amrHome: home });
90
90
  seed(home, repo, 'iso fact');
91
- const before = run(['context'], { amrHome: home, cwd: repo });
92
- assert.match(before.stdout, /global pref/);
93
- run(['policy', 'iso', 'noglobal', 'true'], { amrHome: home, cwd: repo });
94
- const after = run(['context'], { amrHome: home, cwd: repo });
95
- assert.match(after.stdout, /iso fact/);
96
- assert.ok(!after.stdout.includes('global pref'));
91
+ const r = run(['context'], { amrHome: home, cwd: repo });
92
+ assert.match(r.stdout, /iso fact/);
93
+ assert.ok(!r.stdout.includes('global pref'));
94
+ assert.ok(!r.stdout.includes('## Global'));
95
+ assert.ok(!r.stdout.includes('## Recent project memory'));
96
+ });
97
+
98
+ test('outside context pools latest project entries; private and noglobal excluded', () => {
99
+ const home = tmpdir();
100
+ const base = tmpdir();
101
+ const repoA = initRepo(path.join(base, 'pa'), 'https://github.com/acme/pa.git');
102
+ const repoB = initRepo(path.join(base, 'pb'), 'https://github.com/acme/pb.git');
103
+ const repoC = initRepo(path.join(base, 'pc'), 'https://github.com/acme/pc.git');
104
+ const repoD = initRepo(path.join(base, 'pd'), 'https://github.com/acme/pd.git');
105
+ run(['ensure'], { amrHome: home });
106
+ seed(home, repoA, 'pa pooled fact');
107
+ seed(home, repoB, 'pb pooled fact');
108
+ seed(home, repoC, 'pc secret fact');
109
+ seed(home, repoD, 'pd quiet fact');
110
+ run(['policy', 'pc', 'private', 'true'], { amrHome: home, cwd: repoC });
111
+ run(['policy', 'pd', 'noglobal', 'true'], { amrHome: home, cwd: repoD });
112
+ const r = run(['context'], { amrHome: home, cwd: tmpdir() });
113
+ assert.equal(r.status, 0, r.stderr);
114
+ assert.match(r.stdout, /## Recent project memory/);
115
+ assert.match(r.stdout, /pa pooled fact/);
116
+ assert.match(r.stdout, /pb pooled fact/);
117
+ assert.ok(!r.stdout.includes('pc secret fact'));
118
+ assert.ok(!r.stdout.includes('pd quiet fact'));
97
119
  });