get-claudia 1.55.21 → 1.57.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/bin/index.js +213 -5
  3. package/bin/manifest-lib.js +245 -0
  4. package/memory-daemon/claudia_memory/daemon/health.py +1 -1
  5. package/memory-daemon/claudia_memory/daemon/scheduler.py +1 -1
  6. package/memory-daemon/claudia_memory/mcp/server.py +132 -123
  7. package/memory-daemon/claudia_memory/services/consolidate.py +1 -1
  8. package/memory-daemon/claudia_memory/services/remember.py +1 -1
  9. package/package.json +6 -2
  10. package/template-v2/.claude/hooks/__pycache__/post-tool-capture.cpython-313.pyc +0 -0
  11. package/template-v2/.claude/hooks/__pycache__/session-health-check.cpython-313.pyc +0 -0
  12. package/template-v2/.claude/hooks/__pycache__/user-prompt-capture.cpython-313.pyc +0 -0
  13. package/template-v2/.claude/hooks/hooks.json +11 -11
  14. package/template-v2/.claude/hooks/post-tool-capture.py +110 -10
  15. package/template-v2/.claude/hooks/pre-compact.py +4 -4
  16. package/template-v2/.claude/hooks/pre-compact.sh +1 -1
  17. package/template-v2/.claude/hooks/session-health-check.py +52 -4
  18. package/template-v2/.claude/hooks/session-summary.py +399 -0
  19. package/template-v2/.claude/hooks/user-prompt-capture.py +123 -0
  20. package/template-v2/.claude/manifest.json +73 -0
  21. package/template-v2/.claude/rules/claudia-principles.md +2 -2
  22. package/template-v2/.claude/rules/memory-availability.md +3 -3
  23. package/template-v2/.claude/rules/memory-commitment.md +92 -0
  24. package/template-v2/.claude/settings.local.json +26 -0
  25. package/template-v2/.claude/skills/capture-meeting/SKILL.md +6 -6
  26. package/template-v2/.claude/skills/capture-meeting/evals/basic.yaml +1 -1
  27. package/template-v2/.claude/skills/deep-context/SKILL.md +7 -7
  28. package/template-v2/.claude/skills/meditate/SKILL.md +10 -10
  29. package/template-v2/.claude/skills/meditate/evals/basic.yaml +1 -1
  30. package/template-v2/.claude/skills/meeting-prep/SKILL.md +3 -3
  31. package/template-v2/.claude/skills/memory-health/SKILL.md +1 -1
  32. package/template-v2/.claude/skills/memory-manager.md +85 -85
  33. package/template-v2/.claude/skills/morning-brief/SKILL.md +10 -10
  34. package/template-v2/.claude/skills/research/SKILL.md +2 -2
  35. package/template-v2/.claude/skills/skill-index.json +1 -1
  36. package/template-v2/CLAUDE.md +6 -6
  37. package/template-v2/.claude/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
  38. package/template-v2/gitignore +0 -35
package/CHANGELOG.md CHANGED
@@ -2,6 +2,85 @@
2
2
 
3
3
  All notable changes to Claudia will be documented in this file.
4
4
 
5
+ ## 1.57.0 (2026-05-13)
6
+
7
+ ### The Curated Memory Release
8
+
9
+ Five PRs that complete one thesis: **curated, judgment-driven memory capture, enforced at prompt time and persisted across sessions.** Claudia now catches the user's intent when it matters, persists canonical facts as they emerge, and writes a daily session summary so context survives across days.
10
+
11
+ #### Fixed
12
+ - **PostToolUse hook actually runs (#38)** -- The hook was reading `os.environ.get("CLAUDE_TOOL_NAME")`, which Claude Code never sets. Every install since the hook landed had been silently no-op'ing, so `~/.claudia/observations.jsonl` was never written. The hook now reads its payload from stdin per the documented hook contract. Includes a sibling fix to the legacy `claudia/.claude/hooks/post-tool-capture.py` for codebase consistency.
13
+
14
+ #### Added
15
+ - **Memory-commitment rule (#39)** -- A new always-active rule (`template-v2/.claude/rules/memory-commitment.md`) codifies when to save canonical facts immediately via `memory_remember` / `memory_batch` rather than batching to end-of-session reflection. Trigger phrases include "lock this in," "remember this," "this is canonical." Substantive-artifact discipline: at the end of producing a multi-file artifact, do a memory commitment pass and save the canonical facts as one bundled `memory_batch` call.
16
+ - **UserPromptSubmit hook with intent detection (#42)** -- A new hook (`template-v2/.claude/hooks/user-prompt-capture.py`) inspects the user's prompt at submit time and injects reminder context for two trigger classes. Class 1: canonical-fact phrases ("lock this in," "remember this," etc.) tell the agent to save immediately rather than wait for `/meditate`. Class 2: destructive command patterns (`rm -rf`, `git push --force`, `DROP TABLE`, etc.) trigger a "verify before acting" reminder per the safety-first principle. Destructive patterns are surfaced to the model as human-readable labels (`rm -rf (recursive delete)`), not raw regex, so the agent can reason about them clearly.
17
+ - **Daily session summary system (#40)** -- A new SessionEnd hook (`template-v2/.claude/hooks/session-summary.py`) writes a per-session markdown summary to `~/.claudia/sessions/YYYY-MM-DD/NN-slug.md` covering opening prompt, files touched, external actions, and find-this-again references. SessionStart now surfaces a 3-day digest of recent sessions via the existing health-check hook, so future-Claudia knows what past-Claudia worked on. PostToolUse hook gained `file_path` extraction for Write/Edit/MultiEdit/NotebookEdit and `external_action` labels for git push, gh repo create, vercel/netlify deploy, supabase db push, and direct MCP sends.
18
+ - **Explicit upgrade messaging (#50)** -- The installer now names `~/.claudia/` explicitly after an upgrade and lists what is preserved (entities, relationships, reflections, embeddings) instead of the generic "data preserved" phrasing. Users care about their accumulated memory graph; the previous wording did not signal that the database is safe.
19
+
20
+ #### Changed
21
+ - **External-action detection uses word-boundary regex (#40)** -- Previously a substring match, so `echo "git push for testing"` falsely fired the `external_action` flag. The new patterns anchor on command separators (line start, `;`, `&&`, `|`, `(`) and skip transparent prefixes (`sudo`, `nohup`, `time`, `env`). False positives on echoed/quoted strings are eliminated; real commands still fire.
22
+ - **PostToolUse output truncation 200 -> 300 chars (#40)** -- Room for the richer output context that includes `file_path` and `external_action` labels alongside the truncated stdout/stderr.
23
+
24
+ #### Stats
25
+ - 41 new hook tests in `tests/hooks/` (stdlib `unittest`, zero new dependencies), all passing in ~1.5s
26
+ - TDD sensitivity proofs for every behavior change: tests fail on the un-modified hook, pass after the fix
27
+ - 5 PRs merged, 0 regressions
28
+
29
+ #### Notes
30
+ - The brief that drove this chain emphasized one principle: **trust the existing user-file preservation policy (commit `efce9f2`)** rather than inventing a new upgrade framework. The installer's behavior didn't change; only the messaging did.
31
+ - The four hook PRs each landed with their own automated tests and TDD sensitivity proofs. The legacy `claudia/` subdirectory was kept in sync with the canonical `template-v2/` to avoid maintenance drift.
32
+
33
+ ---
34
+
35
+ ## 1.56.1 (2026-04-11)
36
+
37
+ ### Preserve User-Modified Skills on Upgrade
38
+
39
+ 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.
40
+
41
+ #### Added
42
+ - **`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.
43
+ - **`bin/manifest-lib.js`** -- Pure-function library: `hashFile`, `generateManifest`, `detectConflicts`, `resolveBakPath`, `applyResolution`, `loadManifest`. No runtime dependencies.
44
+ - **`scripts/generate-manifest.js`** -- Standalone CLI wrapper: `npm run generate-manifest` rebuilds the shipped manifest from the current `template-v2/` tree.
45
+ - **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`.
46
+ - **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.
47
+ - **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.
48
+
49
+ #### Changed
50
+ - **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.
51
+ - **Non-TTY and `--yes` mode** defaults to keep-all for conflicts, printing what was preserved. Safe for CI.
52
+
53
+ #### Notes
54
+ - Manifest scope is deliberately narrow: `.claude/skills/**`, `.claude/rules/**`, and `CLAUDE.md`. Files under `hooks/`, `agents/`, `commands/`, `workspaces/`, and `settings.local.json` are excluded.
55
+ - Missing or corrupt user manifest falls back to direct hash comparison against the new template -- the upgrade does not crash.
56
+ - 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.
57
+
58
+ #### Rollback
59
+ Single atomic commit. `git revert <sha>` undoes everything. Pre-push `origin/main` was `2d65baa`.
60
+
61
+ ---
62
+
63
+ ## 1.56.0 (2026-04-01)
64
+
65
+ ### Claude Desktop Compatibility
66
+
67
+ 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.
68
+
69
+ #### Fixed
70
+ - **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)
71
+ - **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
72
+ - **template-v2/ mirrors synced** -- New installations get underscore names from day one
73
+
74
+ #### Added
75
+ - **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
76
+ - **test_tool_name_compat.py** -- 6 new tests proving aliases are registered, `list_tools()` only advertises underscore names, and `cognitive.ingest` is unaffected
77
+
78
+ #### Stats
79
+ - 762 tests pass, 0 regressions (+6 new tests)
80
+ - 30 files changed across 3 directory trees (root, claudia/, template-v2/)
81
+
82
+ ---
83
+
5
84
  ## 1.55.21 (2026-03-19)
6
85
 
7
86
  ### 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
- cpSync(src, dest, { force: true });
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
 
@@ -672,7 +877,10 @@ async function main() {
672
877
  }
673
878
 
674
879
  console.log('');
675
- console.log(` ${colors.cyan}✓${colors.reset} Framework updated (data preserved)`);
880
+ console.log(` ${colors.cyan}✓${colors.reset} Framework updated`);
881
+ console.log(` • Your memory at ${colors.bold}~/.claudia/${colors.reset} is preserved (entities, relationships, reflections, embeddings).`);
882
+ console.log(` • Skills and hooks refreshed; any modifications you chose to keep were respected.`);
883
+ console.log(` • Restart Claude Code for changes to take effect.`);
676
884
  }
677
885
 
678
886
  // Self-heal: strip CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from settings (#24)
@@ -1932,7 +2140,7 @@ ${contextual.map(s => `- **/${s.name}** - ${s.description}`).join('\n')}
1932
2140
  ${explicit.map(s => `- **/${s.name}** - ${s.description}`).join('\n')}
1933
2141
 
1934
2142
  ## Memory System
1935
- Memory operations use MCP tools from the claudia-memory daemon (memory.recall, memory.remember, memory.about, etc.).
2143
+ Memory operations use MCP tools from the claudia-memory daemon (memory_recall, memory_remember, memory_about, etc.).
1936
2144
  The daemon provides ~33 tools for semantic search, pattern detection, and relationship tracking.
1937
2145
  See the memory-manager skill for the full tool reference.`;
1938
2146
  } 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 memory.briefing MCP tool).
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
- "memory.file", "memory.remember",
34
+ "memory_file", "memory_remember",
35
35
  }
36
36
 
37
37
  COMMITMENT_RE = re.compile(