cc-brain 0.1.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/src/archive.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Archive Management
5
+ * List, prune, and manage T3 archive entries
6
+ *
7
+ * Usage:
8
+ * bun src/archive.js list # List archive entries
9
+ * bun src/archive.js stats # Show archive statistics
10
+ * bun src/archive.js prune --keep 20 # Keep last 20 entries
11
+ * bun src/archive.js prune --older-than 90d # Delete entries older than 90 days
12
+ * bun src/archive.js auto-prune # Auto-prune (90 days), returns deleted list
13
+ */
14
+
15
+ import { existsSync, readdirSync, statSync, unlinkSync, readFileSync } from 'fs';
16
+ import { join } from 'path';
17
+ import { homedir } from 'os';
18
+ import { getProjectId } from './project-id.js';
19
+
20
+ const BRAIN_DIR = process.env.CC_BRAIN_DIR || join(homedir(), '.claude', 'brain');
21
+ const PROJECT_ID = getProjectId();
22
+ const ARCHIVE_DIR = join(BRAIN_DIR, 'projects', PROJECT_ID, 'archive');
23
+
24
+ function getArchiveEntries() {
25
+ if (!existsSync(ARCHIVE_DIR)) {
26
+ return [];
27
+ }
28
+
29
+ return readdirSync(ARCHIVE_DIR)
30
+ .filter(f => f.endsWith('.md'))
31
+ .map(f => {
32
+ const path = join(ARCHIVE_DIR, f);
33
+ const stat = statSync(path);
34
+ return {
35
+ name: f,
36
+ path,
37
+ date: stat.mtime,
38
+ size: stat.size
39
+ };
40
+ })
41
+ .sort((a, b) => b.date - a.date); // Newest first
42
+ }
43
+
44
+ function listArchive() {
45
+ const entries = getArchiveEntries();
46
+
47
+ if (entries.length === 0) {
48
+ console.log('Archive is empty.');
49
+ return;
50
+ }
51
+
52
+ console.log(`Archive: ${ARCHIVE_DIR}\n`);
53
+ for (const entry of entries) {
54
+ const date = entry.date.toISOString().split('T')[0];
55
+ const size = (entry.size / 1024).toFixed(1) + 'kb';
56
+ console.log(` ${date} ${size.padStart(8)} ${entry.name}`);
57
+ }
58
+ console.log(`\nTotal: ${entries.length} entries`);
59
+ }
60
+
61
+ function showStats() {
62
+ const entries = getArchiveEntries();
63
+
64
+ if (entries.length === 0) {
65
+ console.log('Archive is empty.');
66
+ return;
67
+ }
68
+
69
+ const totalSize = entries.reduce((sum, e) => sum + e.size, 0);
70
+ const oldest = entries[entries.length - 1];
71
+ const newest = entries[0];
72
+
73
+ console.log(`Archive Statistics`);
74
+ console.log(`──────────────────`);
75
+ console.log(`Location: ${ARCHIVE_DIR}`);
76
+ console.log(`Entries: ${entries.length}`);
77
+ console.log(`Total: ${(totalSize / 1024).toFixed(1)}kb`);
78
+ console.log(`Oldest: ${oldest.date.toISOString().split('T')[0]} (${oldest.name})`);
79
+ console.log(`Newest: ${newest.date.toISOString().split('T')[0]} (${newest.name})`);
80
+ }
81
+
82
+ function pruneByCount(keep) {
83
+ const entries = getArchiveEntries();
84
+ const toDelete = entries.slice(keep);
85
+
86
+ if (toDelete.length === 0) {
87
+ console.log(`Nothing to prune (${entries.length} entries, keeping ${keep})`);
88
+ return [];
89
+ }
90
+
91
+ for (const entry of toDelete) {
92
+ unlinkSync(entry.path);
93
+ console.log(`Deleted: ${entry.name}`);
94
+ }
95
+
96
+ console.log(`\nPruned ${toDelete.length} entries, kept ${keep}`);
97
+ return toDelete.map(e => e.name);
98
+ }
99
+
100
+ function pruneByAge(days) {
101
+ const entries = getArchiveEntries();
102
+ const cutoff = new Date();
103
+ cutoff.setDate(cutoff.getDate() - days);
104
+
105
+ const toDelete = entries.filter(e => e.date < cutoff);
106
+
107
+ if (toDelete.length === 0) {
108
+ console.log(`Nothing to prune (no entries older than ${days} days)`);
109
+ return [];
110
+ }
111
+
112
+ for (const entry of toDelete) {
113
+ unlinkSync(entry.path);
114
+ console.log(`Deleted: ${entry.name}`);
115
+ }
116
+
117
+ console.log(`\nPruned ${toDelete.length} entries older than ${days} days`);
118
+ return toDelete.map(e => e.name);
119
+ }
120
+
121
+ function autoPrune(days = 90, silent = false) {
122
+ const entries = getArchiveEntries();
123
+ const cutoff = new Date();
124
+ cutoff.setDate(cutoff.getDate() - days);
125
+
126
+ const toDelete = entries.filter(e => e.date < cutoff);
127
+
128
+ if (toDelete.length === 0) {
129
+ return [];
130
+ }
131
+
132
+ for (const entry of toDelete) {
133
+ unlinkSync(entry.path);
134
+ }
135
+
136
+ if (!silent) {
137
+ console.log(`Auto-pruned ${toDelete.length} archive entries older than ${days} days:`);
138
+ for (const entry of toDelete) {
139
+ console.log(` - ${entry.name}`);
140
+ }
141
+ }
142
+
143
+ return toDelete.map(e => e.name);
144
+ }
145
+
146
+ // CLI
147
+ const args = process.argv.slice(2);
148
+ const command = args[0];
149
+
150
+ if (command === 'list') {
151
+ listArchive();
152
+ } else if (command === 'stats') {
153
+ showStats();
154
+ } else if (command === 'prune') {
155
+ const keepIdx = args.indexOf('--keep');
156
+ const olderIdx = args.indexOf('--older-than');
157
+
158
+ if (keepIdx !== -1) {
159
+ const keep = parseInt(args[keepIdx + 1], 10);
160
+ if (isNaN(keep)) {
161
+ console.error('Error: --keep requires a number');
162
+ process.exit(1);
163
+ }
164
+ pruneByCount(keep);
165
+ } else if (olderIdx !== -1) {
166
+ const ageStr = args[olderIdx + 1];
167
+ const days = parseInt(ageStr, 10);
168
+ if (isNaN(days)) {
169
+ console.error('Error: --older-than requires a number (e.g., 90d)');
170
+ process.exit(1);
171
+ }
172
+ pruneByAge(days);
173
+ } else {
174
+ console.error('Error: prune requires --keep <n> or --older-than <days>');
175
+ process.exit(1);
176
+ }
177
+ } else if (command === 'auto-prune') {
178
+ const days = parseInt(args[1], 10) || 90;
179
+ autoPrune(days);
180
+ } else if (command === '--help' || command === '-h' || !command) {
181
+ console.log(`Usage: bun src/archive.js <command> [options]
182
+
183
+ Commands:
184
+ list List all archive entries
185
+ stats Show archive statistics
186
+ prune --keep <n> Keep only the last n entries
187
+ prune --older-than <n>d Delete entries older than n days
188
+ auto-prune [days] Auto-prune entries older than days (default: 90)
189
+
190
+ Examples:
191
+ bun src/archive.js list
192
+ bun src/archive.js prune --keep 20
193
+ bun src/archive.js prune --older-than 90d`);
194
+ } else {
195
+ console.error(`Unknown command: ${command}`);
196
+ process.exit(1);
197
+ }
198
+
199
+ // Export for use as module
200
+ export { getArchiveEntries, autoPrune, ARCHIVE_DIR };
package/src/loader.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Brain Loader
5
+ * Runs on SessionStart - injects persistent memory into context
6
+ *
7
+ * TIERS:
8
+ * T1 (always): user.md, preferences.md ~40 lines each
9
+ * T2 (project): projects/{id}/context.md ~120 lines
10
+ * T3 (on-demand): projects/{id}/archive/ never auto-loaded
11
+ *
12
+ * Features:
13
+ * - Uses .brain-id for stable project identity
14
+ * - Auto-prunes archive entries older than 90 days (with warning)
15
+ */
16
+
17
+ import { readFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { homedir } from 'os';
20
+ import { getProjectId, getProjectBrainPath } from './project-id.js';
21
+
22
+ const BRAIN_DIR = process.env.CC_BRAIN_DIR || join(homedir(), '.claude', 'brain');
23
+ const PROJECT_ID = getProjectId();
24
+ const PROJECT_PATH = getProjectBrainPath();
25
+
26
+ // Size limits (lines)
27
+ const LIMITS = {
28
+ user: 40,
29
+ preferences: 40,
30
+ context: 120
31
+ };
32
+
33
+ // Auto-prune settings
34
+ const AUTO_PRUNE_DAYS = 90;
35
+
36
+ function readIfExists(path, limit = null) {
37
+ if (!existsSync(path)) return null;
38
+
39
+ let content = readFileSync(path, 'utf-8');
40
+
41
+ if (limit) {
42
+ const lines = content.split('\n');
43
+ if (lines.length > limit) {
44
+ content = lines.slice(0, limit).join('\n') + '\n... (truncated)';
45
+ }
46
+ }
47
+
48
+ return content;
49
+ }
50
+
51
+ function ensureProjectDir() {
52
+ const archiveDir = join(PROJECT_PATH, 'archive');
53
+
54
+ if (!existsSync(PROJECT_PATH)) {
55
+ mkdirSync(PROJECT_PATH, { recursive: true });
56
+ }
57
+ if (!existsSync(archiveDir)) {
58
+ mkdirSync(archiveDir, { recursive: true });
59
+ }
60
+ }
61
+
62
+ function autoPruneArchive() {
63
+ const archiveDir = join(PROJECT_PATH, 'archive');
64
+ if (!existsSync(archiveDir)) return [];
65
+
66
+ const cutoff = new Date();
67
+ cutoff.setDate(cutoff.getDate() - AUTO_PRUNE_DAYS);
68
+
69
+ const deleted = [];
70
+
71
+ try {
72
+ const files = readdirSync(archiveDir).filter(f => f.endsWith('.md'));
73
+
74
+ for (const file of files) {
75
+ const path = join(archiveDir, file);
76
+ const stat = statSync(path);
77
+
78
+ if (stat.mtime < cutoff) {
79
+ unlinkSync(path);
80
+ deleted.push(file);
81
+ }
82
+ }
83
+ } catch (e) {
84
+ // Ignore errors during auto-prune
85
+ }
86
+
87
+ return deleted;
88
+ }
89
+
90
+ function loadBrain() {
91
+ ensureProjectDir();
92
+
93
+ // Auto-prune old archive entries
94
+ const pruned = autoPruneArchive();
95
+
96
+ const parts = [];
97
+
98
+ parts.push('<brain>');
99
+
100
+ // Show pruned files warning
101
+ if (pruned.length > 0) {
102
+ parts.push(`[Auto-pruned ${pruned.length} archive entries older than ${AUTO_PRUNE_DAYS} days: ${pruned.join(', ')}]\n`);
103
+ }
104
+
105
+ // ═══════════════════════════════════════════
106
+ // TIER 1: Always loaded (core understanding)
107
+ // ═══════════════════════════════════════════
108
+
109
+ const user = readIfExists(join(BRAIN_DIR, 'user.md'), LIMITS.user);
110
+ if (user && user.trim()) {
111
+ parts.push(user);
112
+ }
113
+
114
+ const prefs = readIfExists(join(BRAIN_DIR, 'preferences.md'), LIMITS.preferences);
115
+ if (prefs && prefs.trim()) {
116
+ parts.push(prefs);
117
+ }
118
+
119
+ // ═══════════════════════════════════════════
120
+ // TIER 2: Project context (current project)
121
+ // ═══════════════════════════════════════════
122
+
123
+ const projectContext = join(PROJECT_PATH, 'context.md');
124
+ const context = readIfExists(projectContext, LIMITS.context);
125
+ if (context && context.trim()) {
126
+ parts.push(`## Project: ${PROJECT_ID}\n`);
127
+ parts.push(context);
128
+ }
129
+
130
+ // ═══════════════════════════════════════════
131
+ // TIER 3: Archive (NOT loaded, just noted)
132
+ // ═══════════════════════════════════════════
133
+
134
+ const archiveDir = join(PROJECT_PATH, 'archive');
135
+ if (existsSync(archiveDir)) {
136
+ const archiveFiles = readdirSync(archiveDir).filter(f => f.endsWith('.md'));
137
+ if (archiveFiles.length > 0) {
138
+ parts.push(`\n[Archive: ${archiveFiles.length} entries. Use /recall to search.]`);
139
+ }
140
+ }
141
+
142
+ parts.push('</brain>');
143
+
144
+ return parts.join('\n');
145
+ }
146
+
147
+ const brain = loadBrain();
148
+
149
+ // Only output if there's actual content
150
+ if (brain.replace(/<\/?brain>/g, '').replace(/\[.*?\]/g, '').trim()) {
151
+ console.log(brain);
152
+ console.log('\n---');
153
+ console.log('Above is your persistent memory. Use /save to update, /recall to search archive.');
154
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Project Identity
5
+ * Provides stable project ID that survives directory renames
6
+ *
7
+ * Priority:
8
+ * 1. .brain-id file in project root (UUID)
9
+ * 2. Fall back to directory name
10
+ *
11
+ * Usage:
12
+ * bun src/project-id.js # Output current project ID
13
+ * bun src/project-id.js --init # Create .brain-id if not exists
14
+ * bun src/project-id.js --path # Output project brain path
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { randomUUID } from 'crypto';
20
+
21
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
22
+ const BRAIN_ID_FILE = join(PROJECT_DIR, '.brain-id');
23
+
24
+ function getProjectId() {
25
+ // Check for .brain-id file
26
+ if (existsSync(BRAIN_ID_FILE)) {
27
+ return readFileSync(BRAIN_ID_FILE, 'utf-8').trim();
28
+ }
29
+
30
+ // Fall back to directory name
31
+ return PROJECT_DIR.split(/[/\\]/).pop();
32
+ }
33
+
34
+ function initBrainId() {
35
+ if (existsSync(BRAIN_ID_FILE)) {
36
+ const existing = readFileSync(BRAIN_ID_FILE, 'utf-8').trim();
37
+ console.log(`Already exists: ${BRAIN_ID_FILE}`);
38
+ console.log(`ID: ${existing}`);
39
+ return existing;
40
+ }
41
+
42
+ const id = randomUUID();
43
+ writeFileSync(BRAIN_ID_FILE, id + '\n');
44
+ console.log(`Created: ${BRAIN_ID_FILE}`);
45
+ console.log(`ID: ${id}`);
46
+ return id;
47
+ }
48
+
49
+ function getProjectBrainPath() {
50
+ const brainDir = process.env.CC_BRAIN_DIR || join(process.env.HOME || process.env.USERPROFILE, '.claude', 'brain');
51
+ return join(brainDir, 'projects', getProjectId());
52
+ }
53
+
54
+ // CLI (only when run directly)
55
+ if (import.meta.main) {
56
+ const args = process.argv.slice(2);
57
+
58
+ if (args.includes('--init')) {
59
+ initBrainId();
60
+ } else if (args.includes('--path')) {
61
+ console.log(getProjectBrainPath());
62
+ } else if (args.includes('--help') || args.includes('-h')) {
63
+ console.log(`Usage: bun src/project-id.js [options]
64
+
65
+ Options:
66
+ (none) Output current project ID
67
+ --init Create .brain-id file if not exists
68
+ --path Output full path to project brain directory
69
+ --help Show this help`);
70
+ } else {
71
+ console.log(getProjectId());
72
+ }
73
+ }
74
+
75
+ // Export for use as module
76
+ export { getProjectId, initBrainId, getProjectBrainPath, BRAIN_ID_FILE };
package/src/recall.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Archive Search (Recall)
5
+ * Grep-based search through T3 archive
6
+ *
7
+ * Usage:
8
+ * bun src/recall.js "search term" # Search archive for term
9
+ * bun src/recall.js "regex.*pattern" # Regex search
10
+ * bun src/recall.js "term" --context # Show surrounding context
11
+ */
12
+
13
+ import { existsSync, readdirSync, readFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { homedir } from 'os';
16
+ import { getProjectId } from './project-id.js';
17
+
18
+ const BRAIN_DIR = process.env.CC_BRAIN_DIR || join(homedir(), '.claude', 'brain');
19
+ const PROJECT_ID = getProjectId();
20
+ const ARCHIVE_DIR = join(BRAIN_DIR, 'projects', PROJECT_ID, 'archive');
21
+ const CONTEXT_DIR = join(BRAIN_DIR, 'projects', PROJECT_ID);
22
+
23
+ function searchArchive(query, options = {}) {
24
+ const results = [];
25
+ const regex = new RegExp(query, 'gi');
26
+
27
+ // Search archive files
28
+ if (existsSync(ARCHIVE_DIR)) {
29
+ const files = readdirSync(ARCHIVE_DIR).filter(f => f.endsWith('.md'));
30
+
31
+ for (const file of files) {
32
+ const path = join(ARCHIVE_DIR, file);
33
+ const content = readFileSync(path, 'utf-8');
34
+ const lines = content.split('\n');
35
+
36
+ const matches = [];
37
+ for (let i = 0; i < lines.length; i++) {
38
+ if (regex.test(lines[i])) {
39
+ const match = {
40
+ line: i + 1,
41
+ text: lines[i].trim(),
42
+ context: []
43
+ };
44
+
45
+ if (options.context) {
46
+ // Add surrounding lines
47
+ const start = Math.max(0, i - 2);
48
+ const end = Math.min(lines.length - 1, i + 2);
49
+ for (let j = start; j <= end; j++) {
50
+ if (j !== i) {
51
+ match.context.push({ line: j + 1, text: lines[j].trim() });
52
+ }
53
+ }
54
+ }
55
+
56
+ matches.push(match);
57
+ }
58
+ }
59
+
60
+ if (matches.length > 0) {
61
+ // Extract date from filename (e.g., 2025-01-31.md)
62
+ const dateMatch = file.match(/(\d{4}-\d{2}-\d{2})/);
63
+ const date = dateMatch ? dateMatch[1] : 'unknown';
64
+
65
+ results.push({
66
+ file,
67
+ date,
68
+ path,
69
+ matches,
70
+ matchCount: matches.length
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ // Also search current context.md
77
+ const contextPath = join(CONTEXT_DIR, 'context.md');
78
+ if (existsSync(contextPath)) {
79
+ const content = readFileSync(contextPath, 'utf-8');
80
+ const lines = content.split('\n');
81
+
82
+ const matches = [];
83
+ for (let i = 0; i < lines.length; i++) {
84
+ if (regex.test(lines[i])) {
85
+ matches.push({
86
+ line: i + 1,
87
+ text: lines[i].trim()
88
+ });
89
+ }
90
+ }
91
+
92
+ if (matches.length > 0) {
93
+ results.unshift({
94
+ file: 'context.md',
95
+ date: 'current',
96
+ path: contextPath,
97
+ matches,
98
+ matchCount: matches.length
99
+ });
100
+ }
101
+ }
102
+
103
+ // Sort by match count (most relevant first), then by date
104
+ results.sort((a, b) => {
105
+ if (a.date === 'current') return -1;
106
+ if (b.date === 'current') return 1;
107
+ if (b.matchCount !== a.matchCount) return b.matchCount - a.matchCount;
108
+ return b.date.localeCompare(a.date);
109
+ });
110
+
111
+ return results;
112
+ }
113
+
114
+ function formatResults(results, query) {
115
+ if (results.length === 0) {
116
+ console.log(`No results found for: "${query}"`);
117
+ console.log(`\nArchive location: ${ARCHIVE_DIR}`);
118
+ return;
119
+ }
120
+
121
+ console.log(`Found ${results.reduce((sum, r) => sum + r.matchCount, 0)} matches in ${results.length} files for: "${query}"\n`);
122
+
123
+ for (const result of results) {
124
+ console.log(`── ${result.file} (${result.date}) ──`);
125
+
126
+ for (const match of result.matches) {
127
+ // Highlight the match
128
+ const highlighted = match.text.replace(
129
+ new RegExp(`(${query})`, 'gi'),
130
+ '\x1b[33m$1\x1b[0m'
131
+ );
132
+ console.log(` L${match.line}: ${highlighted}`);
133
+
134
+ if (match.context && match.context.length > 0) {
135
+ for (const ctx of match.context) {
136
+ console.log(` ${ctx.line}: ${ctx.text}`);
137
+ }
138
+ }
139
+ }
140
+ console.log('');
141
+ }
142
+ }
143
+
144
+ // CLI
145
+ if (import.meta.main) {
146
+ const args = process.argv.slice(2);
147
+
148
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
149
+ console.log(`Usage: bun src/recall.js <query> [options]
150
+
151
+ Search the brain archive for past context and decisions.
152
+
153
+ Arguments:
154
+ query Search term or regex pattern
155
+
156
+ Options:
157
+ --context Show surrounding lines for each match
158
+ --json Output results as JSON
159
+ --help Show this help
160
+
161
+ Examples:
162
+ bun src/recall.js "authentication"
163
+ bun src/recall.js "why.*choose" --context
164
+ bun src/recall.js "API" --json`);
165
+ process.exit(0);
166
+ }
167
+
168
+ const query = args.find(a => !a.startsWith('--'));
169
+ const showContext = args.includes('--context');
170
+ const jsonOutput = args.includes('--json');
171
+
172
+ if (!query) {
173
+ console.error('Error: search query required');
174
+ process.exit(1);
175
+ }
176
+
177
+ const results = searchArchive(query, { context: showContext });
178
+
179
+ if (jsonOutput) {
180
+ console.log(JSON.stringify(results, null, 2));
181
+ } else {
182
+ formatResults(results, query);
183
+ }
184
+ }
185
+
186
+ // Export for use as module
187
+ export { searchArchive, ARCHIVE_DIR };
@@ -0,0 +1,81 @@
1
+ # Brain Saver
2
+
3
+ Save session context using the structured saver tool.
4
+
5
+ ## Memory Tiers
6
+
7
+ ```
8
+ ~/.claude/brain/
9
+
10
+ ├── user.md ← T1: Who they are (40 lines max)
11
+ ├── preferences.md ← T1: How they code (40 lines max)
12
+
13
+ └── projects/{id}/
14
+ ├── context.md ← T2: Current state (120 lines max)
15
+ └── archive/ ← T3: History (unlimited)
16
+ └── {date}.md
17
+ ```
18
+
19
+ ## What to Save
20
+
21
+ **T1 (rare)** - Only genuine new insights:
22
+ - Communication style discoveries
23
+ - Thinking patterns observed
24
+ - Strong preferences revealed
25
+ - Pet peeves encountered
26
+
27
+ **T2 (common)** - Project state:
28
+ - What the project is
29
+ - Current focus/active work
30
+ - Recent decisions + rationale
31
+ - Key files and purposes
32
+ - Blockers
33
+
34
+ **T3 (optional)** - Significant sessions:
35
+ - Major features implemented
36
+ - Important decisions made
37
+ - Problems solved
38
+
39
+ ## How to Save
40
+
41
+ Build a JSON payload with only the tiers that have new information:
42
+
43
+ ```json
44
+ {
45
+ "t1_user": {
46
+ "Section Name": ["item1", "item2"]
47
+ },
48
+ "t1_prefs": {
49
+ "Section Name": ["preference1"]
50
+ },
51
+ "t2": {
52
+ "what": "One-line description",
53
+ "focus": ["task1", "task2"],
54
+ "decisions": {
55
+ "Decision": "rationale"
56
+ },
57
+ "files": {
58
+ "path/file.js": "purpose"
59
+ }
60
+ },
61
+ "t3": "Summary of significant work."
62
+ }
63
+ ```
64
+
65
+ Preview with dry-run:
66
+ ```bash
67
+ bun src/saver.js --dry-run --json '<payload>'
68
+ ```
69
+
70
+ Apply changes:
71
+ ```bash
72
+ bun src/saver.js --json '<payload>'
73
+ ```
74
+
75
+ ## Rules
76
+
77
+ - REPLACE outdated info, don't append endlessly
78
+ - Decisions need rationale (the "why")
79
+ - Keep within size limits
80
+ - Only save what helps future sessions
81
+ - T1 updates should be rare and meaningful