memoir-cli 3.16.2 → 3.16.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.16.2",
3
+ "version": "3.16.3",
4
4
  "mcpName": "io.github.camgitt/memoir",
5
5
  "description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
6
6
  "main": "src/index.js",
package/src/mcp.js CHANGED
@@ -483,15 +483,28 @@ server.tool(
483
483
 
484
484
  server.tool(
485
485
  'memoir_session',
486
- 'Show the current session state — goals, next actions, open questions, recent decisions, recent sessions across machines. Use this to catch up at the start of a session, or when you need to orient yourself on what was decided.',
487
- {},
488
- async () => {
489
- const state = sessionView(await readSession());
486
+ 'Show the current session state — goals, next actions, open questions, recent decisions, recent sessions across machines. Use this to catch up at the start of a session, or when you need to orient yourself on what was decided. Pass project to narrow it to one project, or tag to narrow it to items whose text carries a [tag] prefix.',
487
+ {
488
+ project: z.string().optional().describe('Project directory or identity. Defaults to the current working project.'),
489
+ tag: z.string().optional().describe('Show only items whose text starts with a bracketed tag, e.g. "gathered" matches "[gathered — CAM] …". Case-insensitive.'),
490
+ },
491
+ async ({ project, tag }) => {
492
+ const state = sessionView(await readSession(), project ? { project } : {});
490
493
  const machine = await getMachineId();
491
- const goals = state.current.goals.map(g => `- ${g.text}`).join('\n') || '(none)';
492
- const nexts = state.current.next_actions.map(n => `- [ ] ${n.text}`).join('\n') || '(none)';
493
- const questions = state.current.open_questions.map(q => `- ${q.text}`).join('\n') || '(none)';
494
- const decisions = state.current.decisions.filter(d => visibleMemory(d)).slice(0, 5).map(d => {
494
+ // Untagged items are visible in every project by design; a [tag] prefix in
495
+ // the text is the only project signal many sessions actually carry.
496
+ const tagged = items => {
497
+ if (!tag) return items;
498
+ const want = tag.toLowerCase().replace(/^\[|\]$/g, '');
499
+ return items.filter(i => {
500
+ const m = String(i?.text || '').match(/^\s*\[([^\]]+)\]/);
501
+ return m && m[1].toLowerCase().includes(want);
502
+ });
503
+ };
504
+ const goals = tagged(state.current.goals).map(g => `- ${g.text}`).join('\n') || '(none)';
505
+ const nexts = tagged(state.current.next_actions).map(n => `- [ ] ${n.text}`).join('\n') || '(none)';
506
+ const questions = tagged(state.current.open_questions).map(q => `- ${q.text}`).join('\n') || '(none)';
507
+ const decisions = tagged(state.current.decisions.filter(d => visibleMemory(d, project ? { project } : {}))).slice(0, 5).map(d => {
495
508
  let line = `- ${d.text}`;
496
509
  if (d.why) line += ` — *${d.why}*`;
497
510
  return line;
@@ -5,6 +5,14 @@ import crypto from 'crypto';
5
5
  import { execFileSync } from 'child_process';
6
6
 
7
7
  const cache = new Map();
8
+
9
+ // Records written before the home directory got an explicit key carry the hash
10
+ // of the empty string. Treat that as an alias for home so they stay visible.
11
+ const LEGACY_HOME_ID = 'local:' + crypto.createHash('sha256').update('').digest('hex').slice(0, 32);
12
+ export function canonicalIdentity(id) {
13
+ return id === LEGACY_HOME_ID ? projectIdentity(os.homedir()) : id;
14
+ }
15
+
8
16
  export function projectIdentity(project = process.env.MEMOIR_PROJECT_ROOT || process.cwd()) {
9
17
  if (project === 'shared') return 'shared';
10
18
  if (/^(git|local):[a-f0-9]{32}$/.test(project)) return project;
@@ -14,7 +22,11 @@ export function projectIdentity(project = process.env.MEMOIR_PROJECT_ROOT || pro
14
22
  if (old && Date.now() - old.at < 60_000) return old.id;
15
23
  let home = os.homedir();
16
24
  try { home = fs.realpathSync(home); } catch {}
17
- let key = path.relative(home, absolute).replace(/\\/g, '/');
25
+ // The home directory itself is a legitimate working root for people who run
26
+ // their agent from ~. path.relative(home, home) is '', and hashing '' gives
27
+ // every such user the same degenerate id (sha256 of the empty string), so
28
+ // name it explicitly instead.
29
+ let key = path.relative(home, absolute).replace(/\\/g, '/') || '~';
18
30
  let kind = 'local';
19
31
  try {
20
32
  const remote = execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: absolute, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
@@ -44,9 +56,9 @@ export function memoryVisibility({ project = process.env.MEMOIR_PROJECT_ROOT ||
44
56
  if (item.claudeProjectKey !== currentKey && item.claudeProjectKey !== sharedKey) return false;
45
57
  }
46
58
  if (allProjects || !item.project || item.project === 'shared') return true;
47
- activeId ??= projectIdentity(project);
59
+ activeId ??= canonicalIdentity(projectIdentity(project));
48
60
  const key = String(item.project);
49
- if (!identities.has(key)) identities.set(key, projectIdentity(key));
61
+ if (!identities.has(key)) identities.set(key, canonicalIdentity(projectIdentity(key)));
50
62
  return identities.get(key) === activeId;
51
63
  };
52
64
  }