memoir-cli 3.16.2 → 3.16.4
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 +1 -1
- package/src/adapters/index.js +3 -2
- package/src/commands/snapshot.js +6 -1
- package/src/context/capture.js +3 -2
- package/src/context/transcripts.js +42 -0
- package/src/mcp.js +21 -8
- package/src/memory/scope.js +15 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.16.
|
|
3
|
+
"version": "3.16.4",
|
|
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/adapters/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import os from 'os';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { shouldIgnoreProject } from '../context/capture.js';
|
|
10
10
|
import { vscodeUserDir, vscodeGlobalStorage, xdgConfigDir } from '../utils/platform.js';
|
|
11
|
+
import { isSideCarDir } from '../context/transcripts.js';
|
|
11
12
|
|
|
12
13
|
const home = os.homedir();
|
|
13
14
|
|
|
@@ -57,8 +58,8 @@ export const adapters = [
|
|
|
57
58
|
// Allow directory traversal but skip dirs that only contain session data
|
|
58
59
|
try {
|
|
59
60
|
if (nodeFs.statSync(src).isDirectory()) {
|
|
60
|
-
// Skip
|
|
61
|
-
if (basename
|
|
61
|
+
// Skip agent side-car and UUID-named session dirs (contain large .jsonl files, no .md)
|
|
62
|
+
if (isSideCarDir(basename)) return false;
|
|
62
63
|
// UUID pattern: 8-4-4-4-12 hex chars
|
|
63
64
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(basename)) return false;
|
|
64
65
|
return true;
|
package/src/commands/snapshot.js
CHANGED
|
@@ -8,6 +8,7 @@ import gradient from 'gradient-string';
|
|
|
8
8
|
import { getConfig, getGeminiApiKey } from '../config.js';
|
|
9
9
|
import { syncToLocal, syncToGit } from '../providers/index.js';
|
|
10
10
|
import { saveHandoff } from '../context/handoffs.js';
|
|
11
|
+
import { isSideCarDir, isUserTranscript } from '../context/transcripts.js';
|
|
11
12
|
|
|
12
13
|
const home = os.homedir();
|
|
13
14
|
|
|
@@ -22,8 +23,12 @@ function findClaudeSessions() {
|
|
|
22
23
|
for (const entry of entries) {
|
|
23
24
|
const full = path.join(dir, entry.name);
|
|
24
25
|
if (entry.isDirectory()) {
|
|
26
|
+
// Same side-car directories capture.js refuses. Without this, every
|
|
27
|
+
// agent-<id>.jsonl under subagents/ is collected and its orchestrator
|
|
28
|
+
// prompt is sent to the summariser as "what they asked for".
|
|
29
|
+
if (isSideCarDir(entry.name)) continue;
|
|
25
30
|
scanDir(full);
|
|
26
|
-
} else if (
|
|
31
|
+
} else if (isUserTranscript(entry.name)) {
|
|
27
32
|
const stat = fs.statSync(full);
|
|
28
33
|
sessions.push({ path: full, mtime: stat.mtimeMs, size: stat.size });
|
|
29
34
|
}
|
package/src/context/capture.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { scanForSecrets, redactSecrets } from '../security/scanner.js';
|
|
6
|
+
import { isSideCarDir, isUserTranscript } from './transcripts.js';
|
|
6
7
|
|
|
7
8
|
const home = os.homedir();
|
|
8
9
|
|
|
@@ -47,9 +48,9 @@ export function findClaudeSessions() {
|
|
|
47
48
|
// agent-<id>.jsonl, not *subagent*), so USER_NOTE_RE minted
|
|
48
49
|
// decisions out of system prompts. Live proof: three of the
|
|
49
50
|
// author's own pinned decisions were subagent-prompt fragments.
|
|
50
|
-
if (entry.name
|
|
51
|
+
if (isSideCarDir(entry.name)) continue;
|
|
51
52
|
scanDir(full);
|
|
52
|
-
} else if (
|
|
53
|
+
} else if (isUserTranscript(entry.name)) {
|
|
53
54
|
try {
|
|
54
55
|
const stat = fs.statSync(full);
|
|
55
56
|
// Skip files older than 7 days for performance
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// One definition of "this .jsonl is an agent transcript, not the user talking."
|
|
2
|
+
//
|
|
3
|
+
// Claude Code writes subagent transcripts as `agent-<id>.jsonl` inside a
|
|
4
|
+
// `subagents/` directory. The FIRST user message in one of those files is the
|
|
5
|
+
// orchestrator's prompt ("You are a software architect. Note that ..."), not
|
|
6
|
+
// anything a person typed. Treating them as user speech has already produced
|
|
7
|
+
// two distinct failures in this codebase:
|
|
8
|
+
//
|
|
9
|
+
// 1. src/context/capture.js minted pinned "decisions" out of system prompts —
|
|
10
|
+
// three of the author's own were subagent-prompt fragments.
|
|
11
|
+
// 2. src/commands/snapshot.js collected them into a handoff summary whose
|
|
12
|
+
// prompt is labelled "User messages (what they asked for)" and POSTed to
|
|
13
|
+
// generativelanguage.googleapis.com, so orchestrator prompts left the
|
|
14
|
+
// machine entirely.
|
|
15
|
+
//
|
|
16
|
+
// Both were written as `!name.includes('subagent')`, which never matches
|
|
17
|
+
// `agent-<id>.jsonl` — the substring is "agent-", not "subagent". capture.js
|
|
18
|
+
// was fixed in place; snapshot.js kept the defeated test for three more weeks
|
|
19
|
+
// because the predicate was spelled four different ways across three files.
|
|
20
|
+
// It is spelled once here now.
|
|
21
|
+
|
|
22
|
+
/** Per-session side directories that never contain user speech. */
|
|
23
|
+
const SIDE_CAR_DIRS = new Set(['subagents', 'workflows', 'tool-results']);
|
|
24
|
+
|
|
25
|
+
/** True for a directory that holds agent-generated transcripts, not user turns. */
|
|
26
|
+
export function isSideCarDir(name) {
|
|
27
|
+
return SIDE_CAR_DIRS.has(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True for a .jsonl file that is an agent transcript rather than a user
|
|
32
|
+
* session. Covers both the real filename (`agent-<id>.jsonl`) and the older
|
|
33
|
+
* `*subagent*` spelling, so a directory walk that reaches one anyway is safe.
|
|
34
|
+
*/
|
|
35
|
+
export function isAgentTranscript(name) {
|
|
36
|
+
return name.startsWith('agent-') || name.includes('subagent');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True for a .jsonl file safe to read as a record of what the user asked for. */
|
|
40
|
+
export function isUserTranscript(name) {
|
|
41
|
+
return name.endsWith('.jsonl') && !isAgentTranscript(name);
|
|
42
|
+
}
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
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;
|
package/src/memory/scope.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|