claude-code-session-manager 0.113.0 → 0.114.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/assets/{AgentLibrary-wvrcLGGS.js → AgentLibrary-DZFM7Lto.js} +1 -1
- package/dist/assets/{Button-B6h0F0XT.js → Button-BJVFKdGH.js} +1 -1
- package/dist/assets/{DataModel-C5pW9E3u.js → DataModel-CebvcP_y.js} +1 -1
- package/dist/assets/{History-BV4e8YOM.js → History-BwISEUhR.js} +1 -1
- package/dist/assets/{Hooks-CrFSv4x1.js → Hooks-Dac3smYP.js} +1 -1
- package/dist/assets/{Library-D-GIt3EY.js → Library-DTjsUr9W.js} +1 -1
- package/dist/assets/{ListDetail-Bco_wfs6.js → ListDetail-B1L5TbjS.js} +1 -1
- package/dist/assets/{MarkdownEditor-C-yHSXNV.js → MarkdownEditor-BFRhWZD4.js} +1 -1
- package/dist/assets/{McpServers-CfDekadG.js → McpServers-CBjGCMg0.js} +1 -1
- package/dist/assets/Memory-DdLi5fqe.js +8 -0
- package/dist/assets/{Panel-DGMEuYYG.js → Panel-BQkkkjex.js} +1 -1
- package/dist/assets/{Permissions-Cx2zjtYZ.js → Permissions-CaACwY0B.js} +1 -1
- package/dist/assets/{Plugins-Bc_W3IyR.js → Plugins-DJeBwVoh.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DiKAUR1u.js → ProvenanceBadge--N5j8xiK.js} +1 -1
- package/dist/assets/{RemoteMachines-8_77-WPD.js → RemoteMachines-WjWaDHEA.js} +1 -1
- package/dist/assets/{SaveBar-Blpzdq1R.js → SaveBar-DFmOjb4_.js} +1 -1
- package/dist/assets/{Scheduler-Bd70AM4X.js → Scheduler-Miap6_aY.js} +1 -1
- package/dist/assets/{ScopeSwitcher-wxN0h4YW.js → ScopeSwitcher-CiEujp4L.js} +1 -1
- package/dist/assets/{Settings-CQ7DA3zW.js → Settings-UGpW4P0j.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DH1NLOiS.js → SkillReferenceGraph-BgyAnxGW.js} +1 -1
- package/dist/assets/{Skills-CmyRyu4_.js → Skills-BQUpnNfx.js} +1 -1
- package/dist/assets/{SystemPrompt-KAc3UMCd.js → SystemPrompt-CQjOgHGT.js} +1 -1
- package/dist/assets/{TagLibrary-C8EvHnpg.js → TagLibrary-CTVpoLl0.js} +1 -1
- package/dist/assets/{TiptapBody-d423mH6D.js → TiptapBody-BtOcrdRR.js} +1 -1
- package/dist/assets/{Toggle-D1wsGE9S.js → Toggle-Chri4G19.js} +1 -1
- package/dist/assets/{ViewTabs-CjHw3kGY.js → ViewTabs-BIenDGVv.js} +1 -1
- package/dist/assets/{historyTotals-DFHzqLt7.js → historyTotals-CX8mUxJY.js} +1 -1
- package/dist/assets/{index-Ci1u018F.js → index-AVrhGmUO.js} +86 -86
- package/dist/assets/{index-Dutcsly-.css → index-HpEbvDiE.css} +1 -1
- package/dist/assets/{settingsSchema-Ctf3pZVI.js → settingsSchema-uqwlznbH.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-B1NBmOAe.js → useAgentsDirWatch-BaezRtWw.js} +1 -1
- package/dist/assets/{useRemotePeers-BHjSJs9D.js → useRemotePeers-BuFbBqDt.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/build-info.json +4 -4
- package/src/main/index.cjs +0 -2
- package/src/main/ipcSchemas.cjs +24 -34
- package/src/main/lib/memoryDirResolve.cjs +139 -0
- package/src/main/lib/memoryIndex.cjs +98 -0
- package/src/main/memoryAggregate.cjs +15 -8
- package/src/main/memoryTool.cjs +151 -22
- package/src/preload/api.d.ts +41 -52
- package/src/preload/index.cjs +19 -21
- package/dist/assets/Memory-CGGLexkR.js +0 -8
- package/src/main/agentMemory.cjs +0 -246
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoryDirResolve.cjs — where Claude Code itself keeps memory.
|
|
3
|
+
*
|
|
4
|
+
* Pure resolver (all IO injected) so the Memory tab reads and writes exactly the native locations:
|
|
5
|
+
* - workspace memory: ~/.claude/projects/<encoded git-repo root>/memory/ (all worktrees of a
|
|
6
|
+
* repo share it), unless the `autoMemoryDirectory` setting overrides it.
|
|
7
|
+
* - subagent memory: enabled by a `memory:` frontmatter field on the agent.
|
|
8
|
+
*
|
|
9
|
+
* Top-level in lib/ because it spans settings, git and agent files (no single domain folder fits).
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { encodeCwd } = require('./util/encodeCwd.cjs');
|
|
15
|
+
|
|
16
|
+
const SCOPES = ['user', 'project', 'local'];
|
|
17
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
18
|
+
|
|
19
|
+
/** Highest-precedence value (local > project > user) of `key` passing `pred`. */
|
|
20
|
+
function pick(settings, key, pred) {
|
|
21
|
+
for (const layer of ['local', 'project', 'user']) {
|
|
22
|
+
const s = settings && settings[layer];
|
|
23
|
+
if (s && typeof s === 'object' && pred(s[key])) return s[key];
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expandDir(value, home, cwd) {
|
|
29
|
+
if (value === '~') return home;
|
|
30
|
+
if (value.startsWith('~/')) return path.join(home, value.slice(2));
|
|
31
|
+
return path.resolve(cwd, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveWorkspaceMemoryDir({ home, cwd, settings, repoRoot }) {
|
|
35
|
+
const override = pick(settings, 'autoMemoryDirectory', (v) => typeof v === 'string' && v.trim() !== '');
|
|
36
|
+
if (override !== undefined) {
|
|
37
|
+
return { dir: expandDir(override.trim(), home, cwd), source: 'autoMemoryDirectory' };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
dir: path.join(home, '.claude', 'projects', encodeCwd(repoRoot || cwd), 'memory'),
|
|
41
|
+
source: 'default',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function autoMemoryEnabled(settings) {
|
|
46
|
+
return pick(settings, 'autoMemoryEnabled', (v) => typeof v === 'boolean') !== false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveAgentMemoryDir({ home, root, name, scope }) {
|
|
50
|
+
if (typeof name !== 'string' || !NAME_RE.test(name) || name === '.' || name === '..') {
|
|
51
|
+
throw new Error(`invalid agent name: ${JSON.stringify(name)}`);
|
|
52
|
+
}
|
|
53
|
+
if (!SCOPES.includes(scope)) throw new Error(`unknown memory scope: ${JSON.stringify(scope)}`);
|
|
54
|
+
if (scope === 'user') return path.join(home, '.claude', 'agent-memory', name);
|
|
55
|
+
if (typeof root !== 'string' || !root) throw new Error(`scope '${scope}' requires a project root`);
|
|
56
|
+
return path.join(root, '.claude', scope === 'project' ? 'agent-memory' : 'agent-memory-local', name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parse the leading `---` block into a flat key -> string map. */
|
|
60
|
+
function parseFrontmatter(text) {
|
|
61
|
+
const out = {};
|
|
62
|
+
if (typeof text !== 'string') return out;
|
|
63
|
+
const lines = text.replace(/^/, '').split(/\r?\n/);
|
|
64
|
+
if (lines[0].trim() !== '---') return out;
|
|
65
|
+
for (let i = 1; i < lines.length; i++) {
|
|
66
|
+
if (lines[i].trim() === '---') return out;
|
|
67
|
+
const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(lines[i]);
|
|
68
|
+
if (m) out[m[1]] = m[2].trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
69
|
+
}
|
|
70
|
+
return {}; // unterminated block: not frontmatter
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scanAgents(dir, origin, readDir, readText) {
|
|
74
|
+
let names;
|
|
75
|
+
try {
|
|
76
|
+
names = readDir(dir);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const file of names || []) {
|
|
82
|
+
if (typeof file !== 'string' || !file.endsWith('.md')) continue;
|
|
83
|
+
const agentFile = path.join(dir, file);
|
|
84
|
+
let text;
|
|
85
|
+
try {
|
|
86
|
+
text = readText(agentFile);
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (text == null) continue;
|
|
91
|
+
const fm = parseFrontmatter(text);
|
|
92
|
+
out.push({
|
|
93
|
+
name: fm.name || file.slice(0, -3),
|
|
94
|
+
origin,
|
|
95
|
+
memoryScope: SCOPES.includes(fm.memory) ? fm.memory : null,
|
|
96
|
+
agentFile,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function listMemoryAgents({ home, root, readDir, readText }) {
|
|
103
|
+
const byName = new Map();
|
|
104
|
+
for (const a of scanAgents(path.join(home, '.claude', 'agents'), 'user', readDir, readText)) byName.set(a.name, a);
|
|
105
|
+
if (root) {
|
|
106
|
+
for (const a of scanAgents(path.join(root, '.claude', 'agents'), 'project', readDir, readText)) byName.set(a.name, a);
|
|
107
|
+
}
|
|
108
|
+
return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rootCache = new Map();
|
|
112
|
+
|
|
113
|
+
function gitRepoRoot(cwd, execFileSync) {
|
|
114
|
+
if (rootCache.has(cwd)) return rootCache.get(cwd);
|
|
115
|
+
let root = null;
|
|
116
|
+
try {
|
|
117
|
+
const out = String(
|
|
118
|
+
execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
119
|
+
cwd,
|
|
120
|
+
timeout: 5000,
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
123
|
+
}),
|
|
124
|
+
).trim();
|
|
125
|
+
if (out.endsWith('/.git')) root = out.slice(0, -'/.git'.length) || null;
|
|
126
|
+
} catch {
|
|
127
|
+
root = null;
|
|
128
|
+
}
|
|
129
|
+
rootCache.set(cwd, root);
|
|
130
|
+
return root;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
resolveWorkspaceMemoryDir,
|
|
135
|
+
autoMemoryEnabled,
|
|
136
|
+
resolveAgentMemoryDir,
|
|
137
|
+
listMemoryAgents,
|
|
138
|
+
gitRepoRoot,
|
|
139
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure helpers for Claude Code's auto-memory index file (MEMORY.md): one line
|
|
5
|
+
* per memory, `- [Title](file.md) — hook`. Claude Code loads only the first
|
|
6
|
+
* 200 lines / 25KB of it per session, so an unlisted memory is invisible.
|
|
7
|
+
* No fs/electron access; callers pass text in and write text out.
|
|
8
|
+
*
|
|
9
|
+
* Top-level (not in a lib/ subfolder) to sit beside its sibling memoryStale.cjs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const MAX_LINES = 200;
|
|
13
|
+
const MAX_BYTES = 25600;
|
|
14
|
+
const NEAR_LINES = 180;
|
|
15
|
+
const NEAR_BYTES = 23040;
|
|
16
|
+
|
|
17
|
+
// `- [Title](name.md)` with optional ` — hook` / ` - hook`. Bare file names only
|
|
18
|
+
// (no path separators), so links into subfolders or URLs never match.
|
|
19
|
+
const LINE_RE = /^\s*[-*]\s+\[([^\]]*)\]\(([^)\s/\\]+\.md)\)(?:\s+(?:—|-)\s+(.*?))?\s*$/;
|
|
20
|
+
|
|
21
|
+
/** Split into lines; a trailing newline does not add a line. */
|
|
22
|
+
function splitLines(text) {
|
|
23
|
+
if (typeof text !== 'string' || text === '') return [];
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchLine(raw) {
|
|
30
|
+
return LINE_RE.exec(raw.replace(/\r$/, ''));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse index link lines. Complexity: O(lines).
|
|
35
|
+
* @param {string|null} text
|
|
36
|
+
* @returns {{line: string, lineNo: number, title: string, file: string, hook: string}[]}
|
|
37
|
+
*/
|
|
38
|
+
function parseIndex(text) {
|
|
39
|
+
const out = [];
|
|
40
|
+
splitLines(text).forEach((raw, i) => {
|
|
41
|
+
const m = matchLine(raw);
|
|
42
|
+
if (!m) return;
|
|
43
|
+
out.push({ line: raw.replace(/\r$/, ''), lineNo: i + 1, title: m[1], file: m[2], hook: m[3] || '' });
|
|
44
|
+
});
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Append an index line unless `file` is already linked. Complexity: O(lines).
|
|
50
|
+
* @param {string|null} text
|
|
51
|
+
* @param {{file: string, title: string, hook?: string}} entry
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function addIndexLine(text, { file, title, hook }) {
|
|
55
|
+
const base = typeof text === 'string' ? text : '';
|
|
56
|
+
if (parseIndex(base).some((e) => e.file === file)) return base;
|
|
57
|
+
const line = `- [${title}](${file})${hook ? ` — ${hook}` : ''}`;
|
|
58
|
+
const trimmed = base.replace(/(\r?\n)+$/, '');
|
|
59
|
+
return `${trimmed === '' ? '' : `${trimmed}\n`}${line}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove every line linking to `file`; other lines are untouched.
|
|
64
|
+
* Complexity: O(lines).
|
|
65
|
+
* @param {string|null} text
|
|
66
|
+
* @param {string} file
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function removeIndexLines(text, file) {
|
|
70
|
+
if (typeof text !== 'string' || text === '') return '';
|
|
71
|
+
const parts = text.split('\n');
|
|
72
|
+
const hadTrailing = parts[parts.length - 1] === '';
|
|
73
|
+
if (hadTrailing) parts.pop();
|
|
74
|
+
const kept = parts.filter((raw) => {
|
|
75
|
+
const m = matchLine(raw);
|
|
76
|
+
return !(m && m[2] === file);
|
|
77
|
+
});
|
|
78
|
+
return kept.length === 0 ? '' : kept.join('\n') + (hadTrailing ? '\n' : '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Index budget + consistency report. Complexity: O((lines + files) log n).
|
|
83
|
+
* @param {{indexText: string|null, files: string[]}} args
|
|
84
|
+
*/
|
|
85
|
+
function indexHealth({ indexText, files }) {
|
|
86
|
+
const text = typeof indexText === 'string' ? indexText : '';
|
|
87
|
+
const lines = splitLines(text).length;
|
|
88
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
89
|
+
const overBudget = lines > MAX_LINES || bytes > MAX_BYTES;
|
|
90
|
+
const nearBudget = !overBudget && (lines >= NEAR_LINES || bytes >= NEAR_BYTES);
|
|
91
|
+
const present = new Set((files || []).filter((f) => f !== 'MEMORY.md'));
|
|
92
|
+
const linked = new Set(parseIndex(text).map((e) => e.file));
|
|
93
|
+
const orphans = [...present].filter((f) => !linked.has(f)).sort();
|
|
94
|
+
const dangling = [...linked].filter((f) => f !== 'MEMORY.md' && !present.has(f)).sort();
|
|
95
|
+
return { lines, bytes, maxLines: MAX_LINES, maxBytes: MAX_BYTES, overBudget, nearBudget, orphans, dangling };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { parseIndex, addIndexLine, removeIndexLines, indexHealth };
|
|
@@ -33,8 +33,15 @@ const { MEMORY_SLUG_RE } = require('./lib/memorySlug.cjs');
|
|
|
33
33
|
const HOME = os.homedir();
|
|
34
34
|
const CACHE_WRITER = 'memory-clusters';
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Resolved through memoryTool.cjs so cwd-addressed callers read Claude Code's
|
|
37
|
+
// native dir (git root + autoMemoryDirectory); no cwd -> legacy workspace dir.
|
|
38
|
+
async function memoryDir(workspace, cwd) {
|
|
39
|
+
const { resolveDir } = require('./memoryTool.cjs');
|
|
40
|
+
try {
|
|
41
|
+
return (await resolveDir({ workspace, cwd })).dir;
|
|
42
|
+
} catch {
|
|
43
|
+
return path.join(HOME, '.claude', 'projects', workspace, 'memory');
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function clustersCachePath(cwd) {
|
|
@@ -94,15 +101,15 @@ function parseFrontmatter(text) {
|
|
|
94
101
|
return { type, description, body: m[2] };
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
async function listMemoryFiles(workspace) {
|
|
98
|
-
const dir = memoryDir(workspace);
|
|
104
|
+
async function listMemoryFiles(workspace, cwd) {
|
|
105
|
+
const dir = await memoryDir(workspace, cwd);
|
|
99
106
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
100
107
|
if (!r.ok) return [];
|
|
101
108
|
return r.entries.filter((e) => MEMORY_SLUG_RE.test(e.name)).map((e) => e.name).sort();
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
async function readMemories(workspace, names) {
|
|
105
|
-
const dir = memoryDir(workspace);
|
|
111
|
+
async function readMemories(workspace, names, cwd) {
|
|
112
|
+
const dir = await memoryDir(workspace, cwd);
|
|
106
113
|
const out = [];
|
|
107
114
|
for (const name of names) {
|
|
108
115
|
const abs = path.join(dir, name);
|
|
@@ -178,8 +185,8 @@ async function aggregate({ workspace, refresh, cwd }) {
|
|
|
178
185
|
return { workspace: ws, generatedAt: null, clusters: [], orphans: [], cached: false };
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
const names = await listMemoryFiles(ws);
|
|
182
|
-
const memories = await readMemories(ws, names);
|
|
188
|
+
const names = await listMemoryFiles(ws, cwd);
|
|
189
|
+
const memories = await readMemories(ws, names, cwd);
|
|
183
190
|
const slugs = memories.map((m) => m.slug);
|
|
184
191
|
|
|
185
192
|
let clusters = [];
|
package/src/main/memoryTool.cjs
CHANGED
|
@@ -32,6 +32,14 @@ const config = require('./config.cjs');
|
|
|
32
32
|
|
|
33
33
|
const { MEMORY_SLUG_RE: SLUG_RE } = require('./lib/memorySlug.cjs');
|
|
34
34
|
const { scoreMemories } = require('./lib/memoryStale.cjs');
|
|
35
|
+
const { addIndexLine, removeIndexLines, indexHealth } = require('./lib/memoryIndex.cjs');
|
|
36
|
+
const {
|
|
37
|
+
resolveWorkspaceMemoryDir,
|
|
38
|
+
autoMemoryEnabled,
|
|
39
|
+
resolveAgentMemoryDir,
|
|
40
|
+
listMemoryAgents,
|
|
41
|
+
gitRepoRoot,
|
|
42
|
+
} = require('./lib/memoryDirResolve.cjs');
|
|
35
43
|
|
|
36
44
|
const MAX_FILE_BYTES = 1024 * 1024; // 1 MiB
|
|
37
45
|
const MAX_ENTRIES = 1000;
|
|
@@ -52,13 +60,59 @@ function workspaceDir(workspace) {
|
|
|
52
60
|
return path.join(memoryRoot(), workspace, 'memory');
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
/** Read + parse a settings file; missing/malformed -> null. */
|
|
64
|
+
async function readSettings(abs) {
|
|
65
|
+
try {
|
|
66
|
+
const r = await config.readText(abs);
|
|
67
|
+
if (!r.exists) return null;
|
|
68
|
+
const parsed = JSON.parse(r.text);
|
|
69
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The one place a memory directory is chosen.
|
|
77
|
+
* agent -> native subagent memory dir (root = git root of cwd, or cwd)
|
|
78
|
+
* cwd -> Claude Code's native workspace dir (git root + autoMemoryDirectory)
|
|
79
|
+
* else -> legacy ~/.claude/projects/<workspace>/memory
|
|
80
|
+
* Returns { dir, autoMemoryEnabled }. Throws on an invalid cwd/agent; every path
|
|
81
|
+
* the callers derive from `dir` still goes through config.validatePath/validateWrite.
|
|
82
|
+
*/
|
|
83
|
+
async function resolveDir({ workspace, cwd, agent } = {}) {
|
|
84
|
+
const home = os.homedir();
|
|
85
|
+
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
86
|
+
if (typeof cwd !== 'string' || !cwd) {
|
|
87
|
+
if (agent) throw new Error('agent memory requires a cwd');
|
|
88
|
+
return { dir: workspaceDir(ws), autoMemoryEnabled: true };
|
|
89
|
+
}
|
|
90
|
+
const realCwd = config.validatePath(cwd);
|
|
91
|
+
const root = gitRepoRoot(realCwd, require('node:child_process').execFileSync) || realCwd;
|
|
92
|
+
const realRoot = config.validatePath(root);
|
|
93
|
+
// Project-scoped native dirs live under <root>/.claude; validateWrite only
|
|
94
|
+
// permits that subtree for registered roots.
|
|
95
|
+
config.addAllowedRoot(realRoot);
|
|
96
|
+
const settings = {
|
|
97
|
+
user: await readSettings(path.join(home, '.claude', 'settings.json')),
|
|
98
|
+
project: await readSettings(path.join(realRoot, '.claude', 'settings.json')),
|
|
99
|
+
local: await readSettings(path.join(realRoot, '.claude', 'settings.local.json')),
|
|
100
|
+
};
|
|
101
|
+
const enabled = autoMemoryEnabled(settings);
|
|
102
|
+
if (agent) {
|
|
103
|
+
return { dir: resolveAgentMemoryDir({ home, root: realRoot, name: agent.name, scope: agent.scope }), autoMemoryEnabled: enabled };
|
|
104
|
+
}
|
|
105
|
+
const r = resolveWorkspaceMemoryDir({ home, cwd: realCwd, settings, repoRoot: realRoot });
|
|
106
|
+
return { dir: r.dir, autoMemoryEnabled: enabled };
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
/**
|
|
56
110
|
* Reject NUL chars, '..' segments, absolute paths, or anything that would
|
|
57
111
|
* resolve outside the workspace dir. Returns the resolved absolute path.
|
|
58
112
|
* The downstream config.cjs helpers add a second layer of validation, but
|
|
59
113
|
* this lets us return a clean error message before we get there.
|
|
60
114
|
*/
|
|
61
|
-
function resolveEntryPath(
|
|
115
|
+
function resolveEntryPath(dir, name) {
|
|
62
116
|
if (typeof name !== 'string' || !name) {
|
|
63
117
|
throw new Error('invalid entry name');
|
|
64
118
|
}
|
|
@@ -66,7 +120,6 @@ function resolveEntryPath(workspace, name) {
|
|
|
66
120
|
if (!SLUG_RE.test(name)) {
|
|
67
121
|
throw new Error(`invalid slug (must match ${SLUG_RE.source}): ${name}`);
|
|
68
122
|
}
|
|
69
|
-
const dir = workspaceDir(workspace);
|
|
70
123
|
const full = path.resolve(dir, name);
|
|
71
124
|
// path.resolve normalizes; verify the result is still under dir to guard
|
|
72
125
|
// against bizarre name inputs.
|
|
@@ -84,12 +137,15 @@ function validWorkspaceName(name) {
|
|
|
84
137
|
return typeof name === 'string' && /^[a-zA-Z0-9-_]{1,256}$/.test(name);
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
async function list({ workspace }) {
|
|
140
|
+
async function list({ workspace, cwd, agent }) {
|
|
88
141
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
89
|
-
|
|
142
|
+
let resolved;
|
|
143
|
+
try { resolved = await resolveDir({ workspace: ws, cwd, agent }); }
|
|
144
|
+
catch (e) { return { entries: [], workspace: ws, dir: null, autoMemoryEnabled: true, error: e.message }; }
|
|
145
|
+
const { dir } = resolved;
|
|
90
146
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
91
147
|
if (!r.ok) {
|
|
92
|
-
return { entries: [], workspace: ws, error: r.error };
|
|
148
|
+
return { entries: [], workspace: ws, dir, autoMemoryEnabled: resolved.autoMemoryEnabled, error: r.error };
|
|
93
149
|
}
|
|
94
150
|
const entries = r.entries
|
|
95
151
|
.filter((e) => e.name.endsWith('.md'))
|
|
@@ -100,12 +156,14 @@ async function list({ workspace }) {
|
|
|
100
156
|
bytes: e.size,
|
|
101
157
|
}))
|
|
102
158
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
-
return { entries, workspace: ws, error: null };
|
|
159
|
+
return { entries, workspace: ws, dir, autoMemoryEnabled: resolved.autoMemoryEnabled, error: null };
|
|
104
160
|
}
|
|
105
161
|
|
|
106
|
-
async function read({ workspace, name }) {
|
|
162
|
+
async function read({ workspace, name, cwd, agent }) {
|
|
107
163
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
108
|
-
|
|
164
|
+
let abs;
|
|
165
|
+
try { abs = resolveEntryPath((await resolveDir({ workspace: ws, cwd, agent })).dir, name); }
|
|
166
|
+
catch (e) { return { content: '', exists: false, mtimeMs: 0, bytes: 0, error: e.message }; }
|
|
109
167
|
const r = await config.readText(abs);
|
|
110
168
|
if (!r.exists) {
|
|
111
169
|
return { content: '', exists: false, mtimeMs: 0, bytes: 0, error: r.error };
|
|
@@ -119,7 +177,7 @@ async function read({ workspace, name }) {
|
|
|
119
177
|
};
|
|
120
178
|
}
|
|
121
179
|
|
|
122
|
-
async function write({ workspace, name, content }) {
|
|
180
|
+
async function write({ workspace, name, content, cwd, agent }) {
|
|
123
181
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
124
182
|
if (typeof content !== 'string') {
|
|
125
183
|
return { ok: false, error: 'content must be a string' };
|
|
@@ -128,7 +186,9 @@ async function write({ workspace, name, content }) {
|
|
|
128
186
|
if (bytes > MAX_FILE_BYTES) {
|
|
129
187
|
return { ok: false, error: `file exceeds 1 MiB cap (${bytes} bytes)` };
|
|
130
188
|
}
|
|
131
|
-
|
|
189
|
+
let abs;
|
|
190
|
+
try { abs = resolveEntryPath((await resolveDir({ workspace: ws, cwd, agent })).dir, name); }
|
|
191
|
+
catch (e) { return { ok: false, error: e.message }; }
|
|
132
192
|
// Enforce per-workspace entry cap on creates (writes to existing files are
|
|
133
193
|
// always allowed regardless of count). Cheap fs.stat check rather than a
|
|
134
194
|
// full directory enumeration where possible.
|
|
@@ -136,7 +196,7 @@ async function write({ workspace, name, content }) {
|
|
|
136
196
|
try { await fsp.access(abs, fs.constants.F_OK); }
|
|
137
197
|
catch { exists = false; }
|
|
138
198
|
if (!exists) {
|
|
139
|
-
const l = await list({ workspace: ws });
|
|
199
|
+
const l = await list({ workspace: ws, cwd, agent });
|
|
140
200
|
if (l.entries.length >= MAX_ENTRIES) {
|
|
141
201
|
return { ok: false, error: `workspace at ${MAX_ENTRIES}-entry cap` };
|
|
142
202
|
}
|
|
@@ -149,9 +209,22 @@ async function write({ workspace, name, content }) {
|
|
|
149
209
|
}
|
|
150
210
|
}
|
|
151
211
|
|
|
152
|
-
|
|
212
|
+
const INDEX_NAME = 'MEMORY.md';
|
|
213
|
+
|
|
214
|
+
/** Read <dir>/MEMORY.md; missing/unreadable -> null. */
|
|
215
|
+
async function readIndexText(dir) {
|
|
216
|
+
const r = await config.readText(path.join(dir, INDEX_NAME));
|
|
217
|
+
return r.exists ? r.text : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function deleteEntry({ workspace, name, cwd, agent }) {
|
|
153
221
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
154
|
-
|
|
222
|
+
let abs;
|
|
223
|
+
let dir;
|
|
224
|
+
try {
|
|
225
|
+
dir = (await resolveDir({ workspace: ws, cwd, agent })).dir;
|
|
226
|
+
abs = resolveEntryPath(dir, name);
|
|
227
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
155
228
|
// Route through config.cjs::validatePath (home-boundary pin, symlink-safe)
|
|
156
229
|
// plus validateWrite (write-prefix pin) — same contract every other
|
|
157
230
|
// mutating call in the codebase follows. Complexity: O(1) per call.
|
|
@@ -164,16 +237,30 @@ async function deleteEntry({ workspace, name }) {
|
|
|
164
237
|
}
|
|
165
238
|
try {
|
|
166
239
|
await fsp.unlink(real);
|
|
167
|
-
return { ok: true, error: null };
|
|
168
240
|
} catch (e) {
|
|
169
|
-
if (e.code
|
|
170
|
-
|
|
241
|
+
if (e.code !== 'ENOENT') return { ok: false, error: e.message };
|
|
242
|
+
}
|
|
243
|
+
if (name === INDEX_NAME) return { ok: true, error: null };
|
|
244
|
+
try {
|
|
245
|
+
const text = await readIndexText(dir);
|
|
246
|
+
if (text !== null) {
|
|
247
|
+
const next = removeIndexLines(text, name);
|
|
248
|
+
if (next !== text) await config.writeTextAtomic(path.join(dir, INDEX_NAME), next);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { ok: true, error: null, indexError: e.message };
|
|
171
252
|
}
|
|
253
|
+
return { ok: true, error: null };
|
|
172
254
|
}
|
|
173
255
|
|
|
174
|
-
async function create({ workspace, name, description }) {
|
|
256
|
+
async function create({ workspace, name, description, cwd, agent }) {
|
|
175
257
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
176
|
-
|
|
258
|
+
let abs;
|
|
259
|
+
let dir;
|
|
260
|
+
try {
|
|
261
|
+
dir = (await resolveDir({ workspace: ws, cwd, agent })).dir;
|
|
262
|
+
abs = resolveEntryPath(dir, name);
|
|
263
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
177
264
|
// Refuse to overwrite — create is strictly "new file".
|
|
178
265
|
try {
|
|
179
266
|
await fsp.access(abs, fs.constants.F_OK);
|
|
@@ -190,7 +277,16 @@ async function create({ workspace, name, description }) {
|
|
|
190
277
|
desc || `# ${stem}`,
|
|
191
278
|
'',
|
|
192
279
|
].join('\n');
|
|
193
|
-
|
|
280
|
+
const w = await write({ workspace: ws, name, content: fm, cwd, agent });
|
|
281
|
+
if (!w.ok || name === INDEX_NAME) return w;
|
|
282
|
+
try {
|
|
283
|
+
const text = (await readIndexText(dir)) || '';
|
|
284
|
+
const next = addIndexLine(text, { file: name, title: stem, hook: desc.replace(/\n/g, ' ') });
|
|
285
|
+
if (next !== text) await config.writeTextAtomic(path.join(dir, INDEX_NAME), next);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
return { ...w, indexError: e.message };
|
|
288
|
+
}
|
|
289
|
+
return w;
|
|
194
290
|
}
|
|
195
291
|
|
|
196
292
|
/**
|
|
@@ -200,9 +296,11 @@ async function create({ workspace, name, description }) {
|
|
|
200
296
|
* the dead-ref existence check. Absent/invalid cwd disables dead-ref
|
|
201
297
|
* detection entirely rather than mass-flagging memories.
|
|
202
298
|
*/
|
|
203
|
-
async function stale({ workspace, cwd }) {
|
|
299
|
+
async function stale({ workspace, cwd, agent }) {
|
|
204
300
|
const ws = validWorkspaceName(workspace) ? workspace : 'default';
|
|
205
|
-
|
|
301
|
+
let dir;
|
|
302
|
+
try { dir = (await resolveDir({ workspace: ws, cwd, agent })).dir; }
|
|
303
|
+
catch (e) { return { entries: [], workspace: ws, error: e.message }; }
|
|
206
304
|
const r = await config.listDir(dir, { filesOnly: true });
|
|
207
305
|
if (!r.ok) {
|
|
208
306
|
return { entries: [], workspace: ws, error: r.error };
|
|
@@ -236,7 +334,30 @@ async function stale({ workspace, cwd }) {
|
|
|
236
334
|
entries.push({ name, mtimeMs: r2.mtimeMs, body: r2.text });
|
|
237
335
|
}
|
|
238
336
|
|
|
239
|
-
|
|
337
|
+
const index = indexHealth({ indexText: await readIndexText(dir), files: r.entries.map((e) => e.name).filter((n) => n.endsWith('.md')) });
|
|
338
|
+
return { entries: scoreMemories({ entries, now: Date.now(), existsPath }), workspace: ws, error: null, index };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Agents (user + project) with their `memory:` frontmatter scope. */
|
|
342
|
+
async function agents({ cwd } = {}) {
|
|
343
|
+
const home = os.homedir();
|
|
344
|
+
let root = null;
|
|
345
|
+
if (typeof cwd === 'string' && cwd) {
|
|
346
|
+
try {
|
|
347
|
+
const realCwd = config.validatePath(cwd);
|
|
348
|
+
root = config.validatePath(gitRepoRoot(realCwd, require('node:child_process').execFileSync) || realCwd);
|
|
349
|
+
} catch {
|
|
350
|
+
root = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
agents: listMemoryAgents({
|
|
355
|
+
home,
|
|
356
|
+
root,
|
|
357
|
+
readDir: (d) => fs.readdirSync(d),
|
|
358
|
+
readText: (f) => fs.readFileSync(f, 'utf8'),
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
240
361
|
}
|
|
241
362
|
|
|
242
363
|
function registerMemoryHandlers() {
|
|
@@ -247,6 +368,7 @@ function registerMemoryHandlers() {
|
|
|
247
368
|
ipcMain.handle('memory:delete', v(s.memoryDelete, deleteEntry));
|
|
248
369
|
ipcMain.handle('memory:create', v(s.memoryCreate, create));
|
|
249
370
|
ipcMain.handle('memory:stale', v(s.memoryStale, stale));
|
|
371
|
+
ipcMain.handle('memory:agents', v(s.memoryAgents, agents));
|
|
250
372
|
}
|
|
251
373
|
|
|
252
374
|
module.exports = {
|
|
@@ -255,5 +377,12 @@ module.exports = {
|
|
|
255
377
|
// exported for tests
|
|
256
378
|
memoryRoot,
|
|
257
379
|
workspaceDir,
|
|
380
|
+
resolveDir,
|
|
381
|
+
list,
|
|
382
|
+
read,
|
|
383
|
+
write,
|
|
384
|
+
create,
|
|
385
|
+
deleteEntry,
|
|
386
|
+
agents,
|
|
258
387
|
stale,
|
|
259
388
|
};
|