get-claudia 1.55.21 → 1.56.1
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/CHANGELOG.md +49 -0
- package/bin/index.js +209 -4
- package/bin/manifest-lib.js +245 -0
- package/memory-daemon/claudia_memory/daemon/health.py +1 -1
- package/memory-daemon/claudia_memory/daemon/scheduler.py +1 -1
- package/memory-daemon/claudia_memory/mcp/server.py +132 -123
- package/memory-daemon/claudia_memory/services/consolidate.py +1 -1
- package/memory-daemon/claudia_memory/services/remember.py +1 -1
- package/package.json +6 -2
- package/template-v2/.claude/hooks/hooks.json +11 -11
- package/template-v2/.claude/hooks/post-tool-capture.py +1 -1
- package/template-v2/.claude/hooks/pre-compact.py +4 -4
- package/template-v2/.claude/hooks/pre-compact.sh +1 -1
- package/template-v2/.claude/manifest.json +72 -0
- package/template-v2/.claude/rules/claudia-principles.md +2 -2
- package/template-v2/.claude/rules/memory-availability.md +3 -3
- package/template-v2/.claude/skills/capture-meeting/SKILL.md +6 -6
- package/template-v2/.claude/skills/capture-meeting/evals/basic.yaml +1 -1
- package/template-v2/.claude/skills/deep-context/SKILL.md +7 -7
- package/template-v2/.claude/skills/meditate/SKILL.md +10 -10
- package/template-v2/.claude/skills/meditate/evals/basic.yaml +1 -1
- package/template-v2/.claude/skills/meeting-prep/SKILL.md +3 -3
- package/template-v2/.claude/skills/memory-health/SKILL.md +1 -1
- package/template-v2/.claude/skills/memory-manager.md +85 -85
- package/template-v2/.claude/skills/morning-brief/SKILL.md +10 -10
- package/template-v2/.claude/skills/research/SKILL.md +2 -2
- package/template-v2/.claude/skills/skill-index.json +1 -1
- package/template-v2/CLAUDE.md +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Claudia will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.56.1 (2026-04-11)
|
|
6
|
+
|
|
7
|
+
### Preserve User-Modified Skills on Upgrade
|
|
8
|
+
|
|
9
|
+
Re-running the installer in an existing project no longer silently overwrites skills, rules, or `CLAUDE.md` that the user has customized. Three-way merge via a shipped manifest detects which tracked files the user has edited and prompts before touching them.
|
|
10
|
+
|
|
11
|
+
#### Added
|
|
12
|
+
- **`template-v2/.claude/manifest.json`** -- SHA-256 hashes of every shipped file under `.claude/skills/`, `.claude/rules/`, and `CLAUDE.md`. Regenerated on `npm publish` via `prepublishOnly`. Users get the new manifest automatically on every upgrade so the next upgrade has a clean comparison baseline.
|
|
13
|
+
- **`bin/manifest-lib.js`** -- Pure-function library: `hashFile`, `generateManifest`, `detectConflicts`, `resolveBakPath`, `applyResolution`, `loadManifest`. No runtime dependencies.
|
|
14
|
+
- **`scripts/generate-manifest.js`** -- Standalone CLI wrapper: `npm run generate-manifest` rebuilds the shipped manifest from the current `template-v2/` tree.
|
|
15
|
+
- **Batch conflict prompt** -- When an upgrade would overwrite locally-modified files, the installer prints a summary and offers `[k]eep all`, `[o]verwrite all`, `[r]eview each`, or `[c]ancel`. Review-each supports `[d]iff` (uses `git diff --no-index` when available), and `[s]kip rest`.
|
|
16
|
+
- **Automatic `.bak` backups** -- Any file the user chooses to overwrite is first copied to `<file>.bak` (with numeric suffix on collision: `.bak.1`, `.bak.2`, ...). No existing `.bak` file is ever overwritten.
|
|
17
|
+
- **25 tests** -- 22 unit tests in `test/manifest.test.js` plus 3 integration tests in `test/integration.test.js`. Run with `npm test`. Uses Node's built-in `node:test`; zero new dependencies.
|
|
18
|
+
|
|
19
|
+
#### Changed
|
|
20
|
+
- **Upgrade copy path** in `bin/index.js` now runs conflict detection before `cpSync` and passes a filter callback that skips any file the user chose to keep. The fresh-install path is unchanged.
|
|
21
|
+
- **Non-TTY and `--yes` mode** defaults to keep-all for conflicts, printing what was preserved. Safe for CI.
|
|
22
|
+
|
|
23
|
+
#### Notes
|
|
24
|
+
- Manifest scope is deliberately narrow: `.claude/skills/**`, `.claude/rules/**`, and `CLAUDE.md`. Files under `hooks/`, `agents/`, `commands/`, `workspaces/`, and `settings.local.json` are excluded.
|
|
25
|
+
- Missing or corrupt user manifest falls back to direct hash comparison against the new template -- the upgrade does not crash.
|
|
26
|
+
- On the first upgrade after this feature ships, users will see a slightly noisier prompt because there's no prior manifest to diff against. Every subsequent upgrade is clean.
|
|
27
|
+
|
|
28
|
+
#### Rollback
|
|
29
|
+
Single atomic commit. `git revert <sha>` undoes everything. Pre-push `origin/main` was `2d65baa`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1.56.0 (2026-04-01)
|
|
34
|
+
|
|
35
|
+
### Claude Desktop Compatibility
|
|
36
|
+
|
|
37
|
+
MCP tool names migrated from dot notation (`memory.recall`) to underscore notation (`memory_recall`) to comply with the MCP spec's `^[a-zA-Z0-9_-]{1,64}$` requirement. Claude Code tolerated dots; Claude Desktop rejected them at registration, blocking Desktop users entirely.
|
|
38
|
+
|
|
39
|
+
#### Fixed
|
|
40
|
+
- **Tool names comply with MCP spec** -- All 33 memory tools renamed from `memory.xxx` to `memory_xxx` in the daemon server, scheduler, and test assertions (PR #32)
|
|
41
|
+
- **Instructional references updated** -- ~200 tool name references renamed across CLAUDE.md, skills (memory-manager, morning-brief, deep-context, meditate, capture-meeting, meeting-prep, research), hooks (pre-compact, hooks.json, post-tool-capture), rules (memory-availability, claudia-principles), README, bin/index.js, and internal error messages
|
|
42
|
+
- **template-v2/ mirrors synced** -- New installations get underscore names from day one
|
|
43
|
+
|
|
44
|
+
#### Added
|
|
45
|
+
- **Backward-compatible alias layer** -- The daemon registers dot-notation aliases (`memory.recall` resolves to the same handler as `memory_recall`) so users who haven't restarted their session or updated their skills keep working during the transition
|
|
46
|
+
- **test_tool_name_compat.py** -- 6 new tests proving aliases are registered, `list_tools()` only advertises underscore names, and `cognitive.ingest` is unaffected
|
|
47
|
+
|
|
48
|
+
#### Stats
|
|
49
|
+
- 762 tests pass, 0 regressions (+6 new tests)
|
|
50
|
+
- 30 files changed across 3 directory trees (root, claudia/, template-v2/)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
5
54
|
## 1.55.21 (2026-03-19)
|
|
6
55
|
|
|
7
56
|
### The Community Release
|
package/bin/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync, statSync, renameSync, unlinkSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync, statSync, renameSync, unlinkSync, copyFileSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn, execFileSync } from 'child_process';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { createInterface } from 'readline';
|
|
9
9
|
import { setupGoogleWorkspace, detectOldGoogleMcp, extractProjectNumber, buildApiEnableUrl, TIER_APIS } from './google-setup.js';
|
|
10
|
+
import {
|
|
11
|
+
loadManifest,
|
|
12
|
+
generateManifest,
|
|
13
|
+
detectConflicts,
|
|
14
|
+
resolveBakPath,
|
|
15
|
+
applyResolution,
|
|
16
|
+
} from './manifest-lib.js';
|
|
10
17
|
|
|
11
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
19
|
const __dirname = dirname(__filename);
|
|
@@ -70,6 +77,181 @@ function confirm(question) {
|
|
|
70
77
|
});
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
// Single-keystroke prompt. Returns the lowercased first character of the
|
|
81
|
+
// user's answer, or `defaultKey` when non-TTY / --yes.
|
|
82
|
+
function promptKey(question, validKeys, defaultKey) {
|
|
83
|
+
if (!isTTY || process.argv.includes('--yes') || process.argv.includes('-y')) {
|
|
84
|
+
return Promise.resolve(defaultKey);
|
|
85
|
+
}
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
+
rl.question(question, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
const c = (answer || '').trim().toLowerCase().charAt(0);
|
|
91
|
+
if (validKeys.includes(c)) resolve(c);
|
|
92
|
+
else resolve(defaultKey);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Detect + resolve conflicts between shipped framework files and the user's
|
|
98
|
+
// locally modified versions. Returns the set of relative paths the caller
|
|
99
|
+
// must skip during cpSync. Saves .bak siblings for any file the user chose
|
|
100
|
+
// to overwrite. May exit(0) if the user cancels.
|
|
101
|
+
async function handleSkillConflicts(targetPath, templatePath) {
|
|
102
|
+
const userManifestPath = join(targetPath, '.claude', 'manifest.json');
|
|
103
|
+
const newManifestPath = join(templatePath, '.claude', 'manifest.json');
|
|
104
|
+
|
|
105
|
+
const oldManifest = loadManifest(userManifestPath);
|
|
106
|
+
let newManifest = loadManifest(newManifestPath);
|
|
107
|
+
|
|
108
|
+
// If the shipped manifest is missing (older package or dev build), fall
|
|
109
|
+
// back to generating one on the fly from the template tree. This keeps
|
|
110
|
+
// the feature working even when scripts/generate-manifest.js wasn't run.
|
|
111
|
+
if (!newManifest) {
|
|
112
|
+
try {
|
|
113
|
+
newManifest = generateManifest(templatePath, { version: getVersion() });
|
|
114
|
+
} catch {
|
|
115
|
+
return new Set(); // can't detect conflicts; preserve old behavior
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = detectConflicts({
|
|
120
|
+
userDir: targetPath,
|
|
121
|
+
templateDir: templatePath,
|
|
122
|
+
oldManifest,
|
|
123
|
+
newManifest,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.conflicts.length === 0) {
|
|
127
|
+
return new Set(); // nothing to prompt about
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Non-TTY or --yes → default to keeping user versions (safe in CI).
|
|
131
|
+
const isNonInteractive = !isTTY || process.argv.includes('--yes') || process.argv.includes('-y');
|
|
132
|
+
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(` ${colors.yellow}⚠${colors.reset} ${result.conflicts.length} file(s) have local modifications that would be overwritten:`);
|
|
135
|
+
console.log('');
|
|
136
|
+
for (const f of result.conflicts) {
|
|
137
|
+
console.log(` ${colors.dim}${f}${colors.reset}`);
|
|
138
|
+
}
|
|
139
|
+
console.log('');
|
|
140
|
+
|
|
141
|
+
if (isNonInteractive) {
|
|
142
|
+
console.log(` ${colors.cyan}i${colors.reset} Non-interactive mode — keeping your versions. Updates for these files skipped.`);
|
|
143
|
+
console.log('');
|
|
144
|
+
return new Set(result.conflicts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(' How do you want to handle these?');
|
|
148
|
+
console.log(` ${colors.bold}[k]${colors.reset} Keep all my versions (skip updates for these files)`);
|
|
149
|
+
console.log(` ${colors.bold}[o]${colors.reset} Overwrite all ${colors.dim}(saves your versions as .bak)${colors.reset}`);
|
|
150
|
+
console.log(` ${colors.bold}[r]${colors.reset} Review each one`);
|
|
151
|
+
console.log(` ${colors.bold}[c]${colors.reset} Cancel upgrade`);
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
const topChoice = await promptKey(' Choice: ', ['k', 'o', 'r', 'c'], 'k');
|
|
155
|
+
|
|
156
|
+
let resolution;
|
|
157
|
+
if (topChoice === 'k') {
|
|
158
|
+
resolution = applyResolution(result.conflicts, { choice: 'keep-all' });
|
|
159
|
+
} else if (topChoice === 'o') {
|
|
160
|
+
resolution = applyResolution(result.conflicts, { choice: 'overwrite-all' });
|
|
161
|
+
} else if (topChoice === 'c') {
|
|
162
|
+
resolution = applyResolution(result.conflicts, { choice: 'cancel' });
|
|
163
|
+
} else {
|
|
164
|
+
// review each
|
|
165
|
+
const perFile = {};
|
|
166
|
+
let skipRest = false;
|
|
167
|
+
for (const f of result.conflicts) {
|
|
168
|
+
if (skipRest) {
|
|
169
|
+
perFile[f] = 'keep';
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(` ${colors.cyan}•${colors.reset} ${f}`);
|
|
174
|
+
const k = await promptKey(
|
|
175
|
+
` ${colors.bold}[k]${colors.reset}eep / ${colors.bold}[o]${colors.reset}verwrite / ${colors.bold}[d]${colors.reset}iff / ${colors.bold}[s]${colors.reset}kip rest: `,
|
|
176
|
+
['k', 'o', 'd', 's'],
|
|
177
|
+
'k',
|
|
178
|
+
);
|
|
179
|
+
if (k === 'd') {
|
|
180
|
+
showDiff(join(targetPath, f), join(templatePath, f));
|
|
181
|
+
// Re-prompt after showing the diff
|
|
182
|
+
const k2 = await promptKey(
|
|
183
|
+
` ${colors.bold}[k]${colors.reset}eep / ${colors.bold}[o]${colors.reset}verwrite: `,
|
|
184
|
+
['k', 'o'],
|
|
185
|
+
'k',
|
|
186
|
+
);
|
|
187
|
+
perFile[f] = k2 === 'o' ? 'overwrite' : 'keep';
|
|
188
|
+
} else if (k === 's') {
|
|
189
|
+
perFile[f] = 'keep';
|
|
190
|
+
skipRest = true;
|
|
191
|
+
} else {
|
|
192
|
+
perFile[f] = k === 'o' ? 'overwrite' : 'keep';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
resolution = applyResolution(result.conflicts, { choice: 'per-file', perFile });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (resolution.cancelled) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(` ${colors.dim}Upgrade cancelled. No files changed.${colors.reset}`);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Back up user versions for any file they chose to overwrite
|
|
205
|
+
for (const relPath of resolution.overwrite) {
|
|
206
|
+
const userAbs = join(targetPath, relPath);
|
|
207
|
+
if (existsSync(userAbs)) {
|
|
208
|
+
try {
|
|
209
|
+
const bakAbs = resolveBakPath(userAbs);
|
|
210
|
+
copyFileSync(userAbs, bakAbs);
|
|
211
|
+
console.log(` ${colors.cyan}↺${colors.reset} Backed up ${colors.dim}${relPath}${colors.reset} → ${colors.dim}${bakAbs.replace(targetPath + '/', '')}${colors.reset}`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.log(` ${colors.red}!${colors.reset} Failed to back up ${relPath}: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (resolution.skip.length > 0) {
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(` ${colors.green}✓${colors.reset} Kept your versions of ${resolution.skip.length} file(s).`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new Set(resolution.skip);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Best-effort diff display. Uses `git diff --no-index` if git is on PATH;
|
|
227
|
+
// otherwise prints a plain head-of-each-file comparison.
|
|
228
|
+
function showDiff(userAbs, templateAbs) {
|
|
229
|
+
try {
|
|
230
|
+
const out = execFileSync('git', ['diff', '--no-index', '--no-color', userAbs, templateAbs], {
|
|
231
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
232
|
+
encoding: 'utf8',
|
|
233
|
+
});
|
|
234
|
+
console.log(out);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
// git diff --no-index returns exit 1 when files differ — that's normal
|
|
237
|
+
if (err.stdout) {
|
|
238
|
+
console.log(err.stdout);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// No git available — fall back to naive display
|
|
242
|
+
try {
|
|
243
|
+
const userLines = readFileSync(userAbs, 'utf8').split('\n').slice(0, 40);
|
|
244
|
+
const tmplLines = readFileSync(templateAbs, 'utf8').split('\n').slice(0, 40);
|
|
245
|
+
console.log(` ${colors.dim}--- your version (first 40 lines) ---${colors.reset}`);
|
|
246
|
+
userLines.forEach((l) => console.log(` ${l}`));
|
|
247
|
+
console.log(` ${colors.dim}--- shipped version (first 40 lines) ---${colors.reset}`);
|
|
248
|
+
tmplLines.forEach((l) => console.log(` ${l}`));
|
|
249
|
+
} catch {
|
|
250
|
+
console.log(` ${colors.dim}(diff unavailable)${colors.reset}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
73
255
|
// Compact portrait-only banner
|
|
74
256
|
function getBanner(version) {
|
|
75
257
|
if (!isTTY) {
|
|
@@ -646,6 +828,26 @@ async function main() {
|
|
|
646
828
|
// Upgrade: copy framework files, preserve user data
|
|
647
829
|
const frameworkPaths = ['.claude', 'CLAUDE.md', '.mcp.json.example', 'LICENSE', 'NOTICE', 'workspaces'];
|
|
648
830
|
|
|
831
|
+
// Detect user-modified shipped files and let the user decide what to
|
|
832
|
+
// do before we touch anything. Returns a Set of POSIX-relative paths
|
|
833
|
+
// to exclude from the copy; may exit(0) if the user cancels.
|
|
834
|
+
let skipPaths;
|
|
835
|
+
try {
|
|
836
|
+
skipPaths = await handleSkillConflicts(targetPath, templatePath);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
// Conflict detection must never break the upgrade. Fall back to the
|
|
839
|
+
// original copy-over-top behavior with a warning.
|
|
840
|
+
console.log(` ${colors.yellow}!${colors.reset} Conflict detection failed (${err.message}); falling back to overwrite.`);
|
|
841
|
+
skipPaths = new Set();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Build an absolute-path skip set for the cpSync filter callback.
|
|
845
|
+
const skipAbs = new Set();
|
|
846
|
+
for (const rel of skipPaths) {
|
|
847
|
+
skipAbs.add(join(targetPath, rel));
|
|
848
|
+
}
|
|
849
|
+
const copyFilter = (_src, dest) => !skipAbs.has(dest);
|
|
850
|
+
|
|
649
851
|
try {
|
|
650
852
|
for (const item of frameworkPaths) {
|
|
651
853
|
const src = join(templatePath, item);
|
|
@@ -654,9 +856,12 @@ async function main() {
|
|
|
654
856
|
|
|
655
857
|
const srcStat = statSync(src);
|
|
656
858
|
if (srcStat.isDirectory()) {
|
|
657
|
-
cpSync(src, dest, { recursive: true, force: true });
|
|
859
|
+
cpSync(src, dest, { recursive: true, force: true, filter: copyFilter });
|
|
658
860
|
} else {
|
|
659
|
-
|
|
861
|
+
// For top-level files (CLAUDE.md, LICENSE, etc.), check skip manually
|
|
862
|
+
if (!skipAbs.has(dest)) {
|
|
863
|
+
cpSync(src, dest, { force: true });
|
|
864
|
+
}
|
|
660
865
|
}
|
|
661
866
|
}
|
|
662
867
|
|
|
@@ -1932,7 +2137,7 @@ ${contextual.map(s => `- **/${s.name}** - ${s.description}`).join('\n')}
|
|
|
1932
2137
|
${explicit.map(s => `- **/${s.name}** - ${s.description}`).join('\n')}
|
|
1933
2138
|
|
|
1934
2139
|
## Memory System
|
|
1935
|
-
Memory operations use MCP tools from the claudia-memory daemon (
|
|
2140
|
+
Memory operations use MCP tools from the claudia-memory daemon (memory_recall, memory_remember, memory_about, etc.).
|
|
1936
2141
|
The daemon provides ~33 tools for semantic search, pattern detection, and relationship tracking.
|
|
1937
2142
|
See the memory-manager skill for the full tool reference.`;
|
|
1938
2143
|
} catch {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Manifest library: pure functions powering user-skill preservation on upgrade.
|
|
2
|
+
//
|
|
3
|
+
// Shipped with every Claudia release. bin/index.js imports from here for the
|
|
4
|
+
// upgrade flow; the unit tests in test/manifest.test.js cover every exported
|
|
5
|
+
// function.
|
|
6
|
+
//
|
|
7
|
+
// See the execution brief in the commit that introduced this file for the
|
|
8
|
+
// full design (three-way merge via shipped manifest + batch prompt UX).
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
statSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { join, relative, sep, posix } from 'node:path';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Scope rules — what the manifest tracks.
|
|
21
|
+
// Only files matching TRACK_PREFIXES (or the exact file CLAUDE.md) are
|
|
22
|
+
// included. Anything matching EXCLUDE_PATTERNS is filtered out.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const TRACK_PREFIXES = [
|
|
26
|
+
'.claude/skills/',
|
|
27
|
+
'.claude/rules/',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const TRACKED_ROOT_FILES = new Set([
|
|
31
|
+
'CLAUDE.md',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const EXCLUDE_SEGMENTS = [
|
|
35
|
+
'.claude/hooks/',
|
|
36
|
+
'.claude/agents/',
|
|
37
|
+
'.claude/commands/',
|
|
38
|
+
'workspaces/',
|
|
39
|
+
'node_modules/',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const EXCLUDE_BASENAMES = new Set([
|
|
43
|
+
'settings.local.json',
|
|
44
|
+
'manifest.json',
|
|
45
|
+
'.DS_Store',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
function isTrackedPath(relPosixPath) {
|
|
49
|
+
if (EXCLUDE_BASENAMES.has(relPosixPath.split('/').pop())) return false;
|
|
50
|
+
for (const seg of EXCLUDE_SEGMENTS) {
|
|
51
|
+
if (relPosixPath.startsWith(seg)) return false;
|
|
52
|
+
}
|
|
53
|
+
if (TRACKED_ROOT_FILES.has(relPosixPath)) return true;
|
|
54
|
+
for (const prefix of TRACK_PREFIXES) {
|
|
55
|
+
if (relPosixPath.startsWith(prefix)) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// hashFile — SHA-256 hex over raw bytes.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export function hashFile(absPath) {
|
|
65
|
+
const bytes = readFileSync(absPath);
|
|
66
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// walkFiles — recursive directory walk yielding posix-style relative paths.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function walkFiles(rootDir) {
|
|
74
|
+
const out = [];
|
|
75
|
+
function recurse(dir) {
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const full = join(dir, entry.name);
|
|
84
|
+
if (entry.isSymbolicLink()) continue;
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
recurse(full);
|
|
87
|
+
} else if (entry.isFile()) {
|
|
88
|
+
out.push(full);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
recurse(rootDir);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toPosixRel(rootDir, absPath) {
|
|
97
|
+
return relative(rootDir, absPath).split(sep).join(posix.sep);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// generateManifest — hashes every tracked file under rootDir.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export function generateManifest(rootDir, { version = 'unknown' } = {}) {
|
|
105
|
+
const files = {};
|
|
106
|
+
for (const abs of walkFiles(rootDir)) {
|
|
107
|
+
const relPosix = toPosixRel(rootDir, abs);
|
|
108
|
+
if (!isTrackedPath(relPosix)) continue;
|
|
109
|
+
files[relPosix] = hashFile(abs);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
version,
|
|
113
|
+
generated: new Date().toISOString(),
|
|
114
|
+
algorithm: 'sha256',
|
|
115
|
+
files,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// detectConflicts — three-way merge between old shipped, new shipped, and
|
|
121
|
+
// the user's current files.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function safeManifestFiles(manifest) {
|
|
125
|
+
if (!manifest || typeof manifest !== 'object') return null;
|
|
126
|
+
if (!manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return manifest.files;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function detectConflicts({ userDir, templateDir, oldManifest, newManifest }) {
|
|
133
|
+
const conflicts = [];
|
|
134
|
+
const userModifiedOnly = [];
|
|
135
|
+
const templateChangedOnly = [];
|
|
136
|
+
const unchanged = [];
|
|
137
|
+
|
|
138
|
+
const newFiles = safeManifestFiles(newManifest) || {};
|
|
139
|
+
const oldFiles = safeManifestFiles(oldManifest); // may be null
|
|
140
|
+
|
|
141
|
+
for (const relPath of Object.keys(newFiles)) {
|
|
142
|
+
const newHash = newFiles[relPath];
|
|
143
|
+
const userAbs = join(userDir, ...relPath.split('/'));
|
|
144
|
+
|
|
145
|
+
// File doesn't exist in user dir → not a conflict, the normal copy will create it
|
|
146
|
+
if (!existsSync(userAbs)) continue;
|
|
147
|
+
|
|
148
|
+
let userHash;
|
|
149
|
+
try {
|
|
150
|
+
userHash = hashFile(userAbs);
|
|
151
|
+
} catch {
|
|
152
|
+
// Unreadable file → skip, let the normal copy handle it
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (oldFiles && typeof oldFiles[relPath] === 'string') {
|
|
157
|
+
const oldHash = oldFiles[relPath];
|
|
158
|
+
const userDiffers = userHash !== oldHash;
|
|
159
|
+
const templateDiffers = newHash !== oldHash;
|
|
160
|
+
|
|
161
|
+
if (!userDiffers && !templateDiffers) {
|
|
162
|
+
unchanged.push(relPath);
|
|
163
|
+
} else if (!userDiffers && templateDiffers) {
|
|
164
|
+
templateChangedOnly.push(relPath);
|
|
165
|
+
} else if (userDiffers && !templateDiffers) {
|
|
166
|
+
userModifiedOnly.push(relPath);
|
|
167
|
+
} else {
|
|
168
|
+
// both changed
|
|
169
|
+
if (userHash === newHash) {
|
|
170
|
+
// User happened to edit to the new value — no conflict
|
|
171
|
+
unchanged.push(relPath);
|
|
172
|
+
} else {
|
|
173
|
+
conflicts.push(relPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// No prior manifest entry — fall back to direct compare against new template
|
|
178
|
+
if (userHash === newHash) {
|
|
179
|
+
unchanged.push(relPath);
|
|
180
|
+
} else {
|
|
181
|
+
conflicts.push(relPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { conflicts, userModifiedOnly, templateChangedOnly, unchanged };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// resolveBakPath — returns a .bak path that doesn't collide.
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export function resolveBakPath(absPath) {
|
|
194
|
+
const base = absPath + '.bak';
|
|
195
|
+
if (!existsSync(base)) return base;
|
|
196
|
+
let n = 1;
|
|
197
|
+
while (existsSync(base + '.' + n)) n++;
|
|
198
|
+
return base + '.' + n;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// applyResolution — turns a user choice into explicit skip/overwrite lists.
|
|
203
|
+
// Pure: no filesystem side effects. Wraps the decision for unit testing.
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export function applyResolution(conflicts, { choice, perFile } = {}) {
|
|
207
|
+
if (choice === 'cancel') {
|
|
208
|
+
return { skip: [], overwrite: [], cancelled: true };
|
|
209
|
+
}
|
|
210
|
+
if (choice === 'keep-all') {
|
|
211
|
+
return { skip: [...conflicts], overwrite: [], cancelled: false };
|
|
212
|
+
}
|
|
213
|
+
if (choice === 'overwrite-all') {
|
|
214
|
+
return { skip: [], overwrite: [...conflicts], cancelled: false };
|
|
215
|
+
}
|
|
216
|
+
if (choice === 'per-file') {
|
|
217
|
+
const skip = [];
|
|
218
|
+
const overwrite = [];
|
|
219
|
+
for (const f of conflicts) {
|
|
220
|
+
const answer = (perFile && perFile[f]) || 'keep';
|
|
221
|
+
if (answer === 'overwrite') overwrite.push(f);
|
|
222
|
+
else skip.push(f);
|
|
223
|
+
}
|
|
224
|
+
return { skip, overwrite, cancelled: false };
|
|
225
|
+
}
|
|
226
|
+
// Unknown choice → fail safe: keep user versions
|
|
227
|
+
return { skip: [...conflicts], overwrite: [], cancelled: false };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// loadManifest — safe JSON load that returns null on any failure.
|
|
232
|
+
// Used by bin/index.js to read the user's existing manifest.
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
export function loadManifest(absPath) {
|
|
236
|
+
try {
|
|
237
|
+
if (!existsSync(absPath)) return null;
|
|
238
|
+
const raw = readFileSync(absPath, 'utf8');
|
|
239
|
+
const parsed = JSON.parse(raw);
|
|
240
|
+
if (!safeManifestFiles(parsed)) return null;
|
|
241
|
+
return parsed;
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -290,7 +290,7 @@ class HealthCheckHandler(BaseHTTPRequestHandler):
|
|
|
290
290
|
self.send_error(500, str(e))
|
|
291
291
|
|
|
292
292
|
def _send_briefing_response(self):
|
|
293
|
-
"""Send compact session briefing (same data as
|
|
293
|
+
"""Send compact session briefing (same data as memory_briefing MCP tool).
|
|
294
294
|
|
|
295
295
|
Lets session hooks call briefing data over HTTP before MCP tools register,
|
|
296
296
|
providing a pre-MCP layer in the fallback chain.
|
|
@@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|
|
31
31
|
RELEVANT_TOOL_PREFIXES = {
|
|
32
32
|
"gmail", "google_workspace", "slack", "telegram",
|
|
33
33
|
"SLACK_", "GMAIL_", "NOTION_", "CALENDAR_",
|
|
34
|
-
"
|
|
34
|
+
"memory_file", "memory_remember",
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
COMMITMENT_RE = re.compile(
|