context-vault 3.9.0 → 3.11.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 (49) hide show
  1. package/.claude-plugin/README.md +219 -0
  2. package/.claude-plugin/plugin.json +11 -0
  3. package/.mcp.json +9 -0
  4. package/bin/cli.js +311 -0
  5. package/commands/vault-cleanup.md +43 -0
  6. package/commands/vault-snapshot.md +43 -0
  7. package/commands/vault-status.md +35 -0
  8. package/dist/register-tools.d.ts.map +1 -1
  9. package/dist/register-tools.js +27 -3
  10. package/dist/register-tools.js.map +1 -1
  11. package/dist/remote.d.ts +52 -0
  12. package/dist/remote.d.ts.map +1 -1
  13. package/dist/remote.js +130 -0
  14. package/dist/remote.js.map +1 -1
  15. package/dist/server.js +18 -0
  16. package/dist/server.js.map +1 -1
  17. package/dist/tools/get-context.d.ts.map +1 -1
  18. package/dist/tools/get-context.js +27 -1
  19. package/dist/tools/get-context.js.map +1 -1
  20. package/dist/tools/recall.d.ts.map +1 -1
  21. package/dist/tools/recall.js +36 -1
  22. package/dist/tools/recall.js.map +1 -1
  23. package/dist/tools/session-start.d.ts.map +1 -1
  24. package/dist/tools/session-start.js +46 -2
  25. package/dist/tools/session-start.js.map +1 -1
  26. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -1
  27. package/node_modules/@context-vault/core/dist/embed.js +24 -11
  28. package/node_modules/@context-vault/core/dist/embed.js.map +1 -1
  29. package/node_modules/@context-vault/core/dist/index.d.ts +1 -0
  30. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  31. package/node_modules/@context-vault/core/dist/index.js +36 -31
  32. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  33. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  34. package/node_modules/@context-vault/core/dist/search.js +46 -0
  35. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  36. package/node_modules/@context-vault/core/package.json +1 -1
  37. package/node_modules/@context-vault/core/src/embed.ts +29 -13
  38. package/node_modules/@context-vault/core/src/index.ts +41 -37
  39. package/node_modules/@context-vault/core/src/search.ts +50 -0
  40. package/package.json +6 -2
  41. package/skills/context-assembly/SKILL.md +308 -0
  42. package/skills/knowledge-capture/SKILL.md +303 -0
  43. package/skills/memory-management/SKILL.md +237 -0
  44. package/src/register-tools.ts +39 -2
  45. package/src/remote.ts +145 -0
  46. package/src/server.ts +16 -0
  47. package/src/tools/get-context.ts +29 -1
  48. package/src/tools/recall.ts +35 -1
  49. package/src/tools/session-start.ts +47 -2
@@ -0,0 +1,219 @@
1
+ # context-vault Claude Plugin
2
+
3
+ Persistent memory and knowledge management for AI agents. Save decisions, insights, patterns, and context across sessions using semantic search and project-scoped buckets.
4
+
5
+ ## What is context-vault?
6
+
7
+ context-vault is a local-first memory system that lets Claude persistently store and retrieve knowledge. Think of it as a personal knowledge base that follows you across sessions, projects, and contexts.
8
+
9
+ ### Key Features
10
+
11
+ - **Persistent Memory** -- Save insights, decisions, patterns, and references across sessions
12
+ - **Semantic Search** -- Find entries by meaning, not just keywords
13
+ - **Project Buckets** -- Organize entries by project for easy scoping
14
+ - **Team Vaults** -- Share knowledge with your team (requires hosted API)
15
+ - **Deduplication** -- Automatic detection of similar entries
16
+ - **Encoding Context** -- Future-proof your saves with metadata for discovery months later
17
+ - **Snapshot Briefs** -- Consolidate scattered entries into a single context brief
18
+ - **Recall Tracking** -- Monitor how often your entries are actually used
19
+
20
+ ## Installation
21
+
22
+ Install via Claude Code:
23
+
24
+ ```
25
+ /plugin install context-vault
26
+ ```
27
+
28
+ For full setup with embedding model download and agent rules:
29
+
30
+ ```bash
31
+ npx context-vault setup
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Save Your First Insight
37
+
38
+ ```javascript
39
+ save_context({
40
+ kind: "insight",
41
+ title: "PostgreSQL connection pooling lesson",
42
+ body: "Connections without pooling exhausted at 100 concurrent users...",
43
+ tags: ["bucket:myproject", "database"],
44
+ encoding_context: { project: "myproject", arc: "scaling", task: "investigation" }
45
+ })
46
+ ```
47
+
48
+ ### Load Context for a Task
49
+
50
+ ```javascript
51
+ // At the start of a session, scope to your project and get an overview
52
+ clear_context({
53
+ preload_bucket: "myproject",
54
+ max_tokens: 4000
55
+ })
56
+
57
+ // Then search for specific context
58
+ get_context({
59
+ query: "how do we handle database scaling?",
60
+ buckets: ["myproject"]
61
+ })
62
+ ```
63
+
64
+ ### Create a Context Brief
65
+
66
+ ```javascript
67
+ create_snapshot({
68
+ topic: "API authentication decisions",
69
+ buckets: ["myproject"]
70
+ })
71
+ ```
72
+
73
+ ## Commands
74
+
75
+ Three slash commands for common vault workflows:
76
+
77
+ - **/vault-status** -- Check vault health (entry counts, recall ratio, warnings)
78
+ - **/vault-snapshot** -- Create a consolidated context brief for a topic
79
+ - **/vault-cleanup** -- Triage stale entries, consolidate duplicates, manage growth
80
+
81
+ ## Skills
82
+
83
+ Three skills teach Claude how to use the vault effectively:
84
+
85
+ ### 1. Memory Management
86
+ When to save, what kind to use, how to tag entries, and how to keep your vault healthy.
87
+
88
+ ### 2. Knowledge Capture
89
+ Session-end protocol: extract and save the insights and decisions you discovered.
90
+
91
+ ### 3. Context Assembly
92
+ Load relevant knowledge at task start. Assemble context efficiently and scope to your project.
93
+
94
+ ## Use Cases
95
+
96
+ ### Personal Memory
97
+ Save decisions and insights from your work, then access them months later.
98
+
99
+ ```javascript
100
+ save_context({
101
+ kind: "decision",
102
+ title: "Chose SQLite + embeddings for local RAG",
103
+ body: "Evaluated: PostgreSQL (setup), Pinecone (cost), local SQLite (simplicity). Chose SQLite + sqlite-vec for full control and no external dependencies.",
104
+ tags: ["bucket:rag-project", "architecture", "database"],
105
+ tier: "durable"
106
+ })
107
+ ```
108
+
109
+ ### Team Knowledge Sharing
110
+ Capture lessons learned that the team should know, then publish to a shared vault.
111
+
112
+ ```javascript
113
+ save_context({
114
+ kind: "pattern",
115
+ title: "Always test OAuth flow end-to-end on real device",
116
+ body: "Simulator behavior differs from real devices. Always test with physical phone before production.",
117
+ tags: ["bucket:mobile-team", "oauth", "testing"],
118
+ tier: "durable"
119
+ })
120
+
121
+ // Later, share to team vault
122
+ publish_to_team({ id: "entry-id" })
123
+ ```
124
+
125
+ ### Project Onboarding
126
+ Create a snapshot of all decisions and patterns for a new team member.
127
+
128
+ ```javascript
129
+ create_snapshot({
130
+ topic: "project architecture and key decisions",
131
+ buckets: ["project-x"],
132
+ kinds: ["decision", "pattern"]
133
+ })
134
+ ```
135
+
136
+ ## Tools Reference
137
+
138
+ The plugin provides 14 MCP tools:
139
+
140
+ ### Read-Only (no side effects)
141
+ - `list_context` -- Browse entries by kind, tags, date range
142
+ - `context_status` -- Vault health dashboard (entry counts, recall stats, warnings)
143
+ - `list_buckets` -- List all project buckets
144
+ - `session_start` -- Load project context at session start
145
+
146
+ ### Read with Analytics (update access counters as side effect)
147
+ - `get_context` -- Search vault by query, kind, bucket, or date. Updates `hit_count` and `recall_count` on returned entries.
148
+ - `recall` -- Proactive context surfacing. Takes a `signal` (text) and `signal_type` (prompt/error/file/task), returns relevant hints. Records co-retrieval pairs.
149
+
150
+ ### Write (create new entries)
151
+ - `save_context` -- Create or update an entry. Supports `encoding_context` for future discovery and `supersedes` to retire old entries.
152
+ - `create_snapshot` -- Consolidate scattered entries into a single brief. Returns a confirmation with the new entry's ID and identity_key.
153
+ - `ingest_url` -- Fetch a web page and save as a reference entry
154
+ - `ingest_project` -- Scan a project directory and save metadata
155
+ - `session_end` -- Capture session learnings (auto-save insights)
156
+ - `publish_to_team` -- Copy an entry to your team vault
157
+
158
+ ### Destructive (modify or delete existing data)
159
+ - `delete_context` -- Permanently remove an entry by ID
160
+ - `clear_context` -- Reset session scope. With `preload_bucket`, returns recent entries from that bucket in the response.
161
+
162
+ ## Storage
163
+
164
+ Data is stored in two locations:
165
+
166
+ ```
167
+ ~/.context-mcp/ # Data directory
168
+ ├── vault.db # SQLite database (entries, embeddings, FTS index)
169
+ ├── config.json # User configuration
170
+ └── telemetry.jsonl # Local usage telemetry
171
+
172
+ ~/vault/ (or configured path) # Vault directory (markdown source of truth)
173
+ ├── knowledge/ # Decisions, insights, patterns, references
174
+ ├── entity/ # People, projects, tools
175
+ └── event/ # Session logs, activity
176
+ ```
177
+
178
+ Markdown files are the source of truth. The SQLite database is a derived index, rebuildable via `context-vault reindex`.
179
+
180
+ ## Privacy
181
+
182
+ **Local-first:** All data stored locally on your machine. No network calls unless you explicitly enable hosted sync or team vaults.
183
+
184
+ **Hosted sync (optional):** When configured, entries sync to your personal cloud vault (per-user isolated database). Team vault entries are shared only when you call `publish_to_team`.
185
+
186
+ **Your vault is never accessed or indexed by third parties.** Full privacy policy: https://context-vault.com/privacy
187
+
188
+ ## Tips
189
+
190
+ - **Encoding context is key:** Entries without `encoding_context` are rarely discovered later. Always include project, arc, and task.
191
+ - **Check before saving:** Call `get_context` first to avoid duplicates. Duplicates hurt recall ratio.
192
+ - **Use tiers wisely:** `durable` for architecture decisions, `working` (default) for active context, `ephemeral` for temp notes.
193
+ - **Snapshot for scale:** If you have 20+ entries on a topic, create a snapshot instead of loading them individually.
194
+ - **Recall ratio:** 20-30% is healthy. Run `/vault-status` to check yours.
195
+
196
+ ## Troubleshooting
197
+
198
+ **"I can't find my entry"**
199
+ - Check the right bucket: `get_context({ query: "...", buckets: ["myproject"] })`
200
+ - Try a broader query: "authentication" instead of "OAuth 2.0 PKCE state handling"
201
+ - Browse by kind: `list_context({ kind: "decision", tags: ["bucket:myproject"] })`
202
+
203
+ **"Vault is growing too fast"**
204
+ - Run `/vault-cleanup` to identify stale or duplicate entries
205
+ - Create snapshots to consolidate large topics
206
+ - Check for session/debug noise mixed with knowledge entries
207
+
208
+ **"Embeddings not working"**
209
+ - Run `context-vault reindex` to regenerate the embedding index
210
+ - Run `context-vault doctor` for a full diagnostic
211
+
212
+ ## Support
213
+
214
+ - GitHub: https://github.com/fellanH/context-vault/issues
215
+ - Website: https://context-vault.com
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "context-vault",
3
+ "version": "3.10.0",
4
+ "description": "Persistent memory and knowledge management for AI agents. Save decisions, insights, patterns, and context across sessions. Semantic search, project-scoped buckets, team vaults, and recall tracking.",
5
+ "author": "Klarhimmel",
6
+ "homepage": "https://context-vault.com",
7
+ "repository": "https://github.com/fellanH/context-vault",
8
+ "license": "MIT",
9
+ "keywords": ["memory", "knowledge", "vault", "context", "mcp", "persistence", "recall"],
10
+ "categories": ["productivity", "knowledge-management", "developer-tools"]
11
+ }
package/.mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "context-vault": {
4
+ "command": "context-vault",
5
+ "args": ["serve"]
6
+ }
7
+ }
8
+ }
9
+
package/bin/cli.js CHANGED
@@ -416,6 +416,7 @@ ${bold('Commands:')}
416
416
  ${cyan('stats')} recall|co-retrieval Measure recall ratio and co-retrieval graph
417
417
  ${cyan('remote')} setup|status|sync|pull Connect to hosted vault (cloud sync)
418
418
  ${cyan('team')} join|leave|status|browse Join or manage a team vault
419
+ ${cyan('public')} create|seed|list|add Manage and consume public vaults
419
420
  ${cyan('update')} Check for and install updates
420
421
  ${cyan('uninstall')} Remove MCP configs and optionally data
421
422
  `);
@@ -7367,6 +7368,313 @@ async function runTeam() {
7367
7368
  console.log();
7368
7369
  }
7369
7370
 
7371
+ async function runPublic() {
7372
+ const subcommand = args[1];
7373
+ const { getRemoteConfig, saveRemoteConfig } = await import('@context-vault/core/config');
7374
+ const dataDir = join(HOME, '.context-mcp');
7375
+
7376
+ if (subcommand === 'create') {
7377
+ const name = args[2];
7378
+ const slug = getFlag('--slug') || (name ? name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') : null);
7379
+ const description = getFlag('--description') || '';
7380
+ const domainTags = getFlag('--domains') ? getFlag('--domains').split(',').map(s => s.trim()) : [];
7381
+ const visibility = getFlag('--visibility') || 'free';
7382
+
7383
+ if (!name || !slug) {
7384
+ console.error(`\n ${red('Usage:')} context-vault public create <name> [--slug <slug>] [--description <desc>] [--domains <tag1,tag2>] [--visibility free|pro]\n`);
7385
+ process.exit(1);
7386
+ }
7387
+
7388
+ const remote = getRemoteConfig(dataDir);
7389
+ if (!remote || !remote.enabled || !remote.apiKey) {
7390
+ console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
7391
+ process.exit(1);
7392
+ }
7393
+
7394
+ console.log(`\n Creating public vault "${bold(name)}" (${slug})...`);
7395
+ try {
7396
+ const res = await fetch(`${remote.url.replace(/\/$/, '')}/api/public/vaults`, {
7397
+ method: 'POST',
7398
+ headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
7399
+ signal: AbortSignal.timeout(15000),
7400
+ body: JSON.stringify({ name, slug, description, domain_tags: domainTags, visibility }),
7401
+ });
7402
+ if (res.ok) {
7403
+ const data = await res.json();
7404
+ console.log(` ${green('✓')} Created public vault: ${bold(data.slug || slug)}`);
7405
+ console.log(` Visibility: ${visibility}`);
7406
+ if (domainTags.length) console.log(` Domains: ${domainTags.join(', ')}`);
7407
+ } else {
7408
+ const data = await res.json().catch(() => ({}));
7409
+ console.error(` ${red('✘')} Failed: ${data.error || `HTTP ${res.status}`}`);
7410
+ process.exit(1);
7411
+ }
7412
+ } catch (e) {
7413
+ console.error(` ${red('✘')} Connection failed: ${e.message}`);
7414
+ process.exit(1);
7415
+ }
7416
+ console.log();
7417
+ return;
7418
+ }
7419
+
7420
+ if (subcommand === 'seed') {
7421
+ const vaultSlug = getFlag('--vault');
7422
+ const tagsFlag = getFlag('--tags');
7423
+ const remote = getRemoteConfig(dataDir);
7424
+
7425
+ if (!remote || !remote.enabled || !remote.apiKey) {
7426
+ console.error(`\n ${red('✘')} Remote is not configured. Run ${cyan('context-vault remote setup')} first.\n`);
7427
+ process.exit(1);
7428
+ }
7429
+
7430
+ if (!vaultSlug) {
7431
+ console.error(`\n ${red('Usage:')} context-vault public seed --vault <slug> [--tags <filter>] [--dry-run]\n`);
7432
+ process.exit(1);
7433
+ }
7434
+
7435
+ // Load local vault DB
7436
+ const { resolveConfig } = await import('@context-vault/core/config');
7437
+ const config = resolveConfig();
7438
+
7439
+ let DatabaseSync;
7440
+ try {
7441
+ DatabaseSync = (await import('node:sqlite')).DatabaseSync;
7442
+ } catch {
7443
+ console.error(`\n ${red('✘')} Node.js SQLite not available. Requires Node >= 22.5.\n`);
7444
+ process.exit(1);
7445
+ }
7446
+
7447
+ const db = new DatabaseSync(config.dbPath, { open: true });
7448
+
7449
+ let sql = 'SELECT id, kind, category, title, body, tags, meta, source, identity_key, tier FROM vault WHERE superseded_by IS NULL';
7450
+ const params = [];
7451
+ if (tagsFlag) {
7452
+ sql += ' AND tags LIKE ?';
7453
+ params.push(`%"${tagsFlag}"%`);
7454
+ }
7455
+
7456
+ const rows = db.prepare(sql).all(...params);
7457
+ db.close();
7458
+
7459
+ let publishedKnowledge = 0;
7460
+ let publishedEntities = 0;
7461
+ let skippedEvents = 0;
7462
+ let blockedPrivacy = 0;
7463
+ const blockedEntries = [];
7464
+ let errors = 0;
7465
+
7466
+ const apiUrl = remote.url.replace(/\/$/, '');
7467
+
7468
+ console.log();
7469
+ console.log(` ${bold('◇ Public Vault Seed')}`);
7470
+ console.log(` Vault: ${vaultSlug}`);
7471
+ console.log(` Filter: ${tagsFlag || dim('(all entries)')}`);
7472
+ console.log(` Entries: ${rows.length}`);
7473
+ console.log();
7474
+
7475
+ if (isDryRun) {
7476
+ for (const row of rows) {
7477
+ if (row.category === 'event') skippedEvents++;
7478
+ else if (row.category === 'entity') publishedEntities++;
7479
+ else publishedKnowledge++;
7480
+ }
7481
+ console.log(` ${dim('[dry run]')}`);
7482
+ console.log(` Would publish: ${green(publishedKnowledge)} knowledge, ${cyan(publishedEntities)} entities`);
7483
+ console.log(` Would skip: ${dim(skippedEvents)} events (blocked for public)`);
7484
+ console.log();
7485
+ return;
7486
+ }
7487
+
7488
+ for (const row of rows) {
7489
+ if (row.category === 'event') {
7490
+ skippedEvents++;
7491
+ continue;
7492
+ }
7493
+
7494
+ const tags = row.tags ? JSON.parse(row.tags) : [];
7495
+ const meta = row.meta ? JSON.parse(row.meta) : {};
7496
+
7497
+ try {
7498
+ const res = await fetch(`${apiUrl}/api/public/${vaultSlug}/entries`, {
7499
+ method: 'POST',
7500
+ headers: { 'Authorization': `Bearer ${remote.apiKey}`, 'Content-Type': 'application/json' },
7501
+ signal: AbortSignal.timeout(15000),
7502
+ body: JSON.stringify({
7503
+ kind: row.kind,
7504
+ title: row.title,
7505
+ body: row.body,
7506
+ tags,
7507
+ meta,
7508
+ source: row.source,
7509
+ identity_key: row.identity_key,
7510
+ tier: row.tier,
7511
+ category: row.category,
7512
+ }),
7513
+ });
7514
+
7515
+ if (res.ok) {
7516
+ if (row.category === 'entity') publishedEntities++;
7517
+ else publishedKnowledge++;
7518
+ } else if (res.status === 422) {
7519
+ const data = await res.json().catch(() => ({}));
7520
+ if (data.code === 'PRIVACY_SCAN_FAILED') {
7521
+ const matchTypes = Array.isArray(data.matches) ? data.matches.map(m => m.type) : [];
7522
+ const uniqueTypes = [...new Set(matchTypes)];
7523
+ console.log(` ${yellow('!')} Blocked: "${row.title || row.id}" (${uniqueTypes.join(', ') || 'sensitive content'})`);
7524
+ blockedPrivacy++;
7525
+ blockedEntries.push({ title: row.title || row.id, types: uniqueTypes });
7526
+ } else {
7527
+ errors++;
7528
+ console.error(` ${red('!')} 422 for "${row.title || row.id}": ${data.error || 'unknown'}`);
7529
+ }
7530
+ } else {
7531
+ errors++;
7532
+ const text = await res.text().catch(() => '');
7533
+ console.error(` ${red('!')} HTTP ${res.status} for "${row.title || row.id}": ${text.slice(0, 200)}`);
7534
+ }
7535
+ } catch (e) {
7536
+ errors++;
7537
+ console.error(` ${red('✘')} Failed: ${row.title || row.id} (${e.message})`);
7538
+ }
7539
+
7540
+ const processed = publishedKnowledge + publishedEntities + skippedEvents + blockedPrivacy + errors;
7541
+ if (processed % 10 === 0) {
7542
+ process.stdout.write(` ${dim(`${processed}/${rows.length}...`)}\r`);
7543
+ }
7544
+ }
7545
+
7546
+ console.log(` Seeded: ${green(publishedKnowledge)} knowledge, ${cyan(publishedEntities)} entities, ${dim(skippedEvents)} skipped (events), ${blockedPrivacy > 0 ? yellow(blockedPrivacy) : dim(blockedPrivacy)} blocked (privacy)`);
7547
+ if (errors > 0) console.log(` Errors: ${red(errors)}`);
7548
+ if (blockedEntries.length > 0) {
7549
+ console.log();
7550
+ console.log(` ${yellow('Blocked entries (review and clean before re-seeding):')}`);
7551
+ for (const entry of blockedEntries) {
7552
+ console.log(` - ${entry.title} [${entry.types.join(', ')}]`);
7553
+ }
7554
+ }
7555
+ console.log();
7556
+ return;
7557
+ }
7558
+
7559
+ if (subcommand === 'list') {
7560
+ const domain = getFlag('--domain');
7561
+ const remote = getRemoteConfig(dataDir);
7562
+
7563
+ const apiUrl = remote?.url?.replace(/\/$/, '') || 'https://api.context-vault.com';
7564
+ const headers = {};
7565
+ if (remote?.apiKey) headers['Authorization'] = `Bearer ${remote.apiKey}`;
7566
+ headers['Content-Type'] = 'application/json';
7567
+
7568
+ const query = new URLSearchParams();
7569
+ if (domain) query.set('domain', domain);
7570
+
7571
+ console.log();
7572
+ console.log(` ${bold('◇ Public Vaults')}`);
7573
+ console.log();
7574
+
7575
+ try {
7576
+ const res = await fetch(`${apiUrl}/api/public/vaults?${query.toString()}`, {
7577
+ headers,
7578
+ signal: AbortSignal.timeout(10000),
7579
+ });
7580
+ if (res.ok) {
7581
+ const data = await res.json();
7582
+ const vaults = Array.isArray(data.vaults) ? data.vaults : Array.isArray(data) ? data : [];
7583
+ if (vaults.length === 0) {
7584
+ console.log(` ${dim('No public vaults found.')}`);
7585
+ } else {
7586
+ for (const v of vaults) {
7587
+ const domains = Array.isArray(v.domain_tags) ? v.domain_tags.join(', ') : '';
7588
+ console.log(` ${bold(v.name || v.slug)} ${dim(`(${v.slug})`)}`);
7589
+ if (v.description) console.log(` ${v.description}`);
7590
+ const stats = [`${v.consumer_count ?? 0} consumers`, `${v.total_recalls ?? 0} recalls`];
7591
+ if (domains) stats.push(domains);
7592
+ console.log(` ${dim(stats.join(' · '))}`);
7593
+ console.log();
7594
+ }
7595
+ }
7596
+ } else {
7597
+ console.error(` ${red('✘')} Failed to list vaults: HTTP ${res.status}`);
7598
+ }
7599
+ } catch (e) {
7600
+ console.error(` ${red('✘')} Connection failed: ${e.message}`);
7601
+ }
7602
+ console.log();
7603
+ return;
7604
+ }
7605
+
7606
+ if (subcommand === 'add') {
7607
+ const slug = args[2];
7608
+ if (!slug) {
7609
+ console.error(`\n ${red('Usage:')} context-vault public add <slug>\n`);
7610
+ process.exit(1);
7611
+ }
7612
+
7613
+ const configPath = join(dataDir, 'config.json');
7614
+ let config = {};
7615
+ try {
7616
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
7617
+ } catch {}
7618
+
7619
+ if (!config.remote) config.remote = {};
7620
+ if (!Array.isArray(config.remote.publicVaults)) config.remote.publicVaults = [];
7621
+
7622
+ if (config.remote.publicVaults.includes(slug)) {
7623
+ console.log(`\n ${dim('Already added:')} ${slug}\n`);
7624
+ return;
7625
+ }
7626
+
7627
+ config.remote.publicVaults.push(slug);
7628
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
7629
+ console.log(`\n ${green('✓')} Added public vault: ${bold(slug)}`);
7630
+ console.log(` Your agent will now query this vault in get_context, session_start, and recall.\n`);
7631
+ return;
7632
+ }
7633
+
7634
+ if (subcommand === 'remove') {
7635
+ const slug = args[2];
7636
+ if (!slug) {
7637
+ console.error(`\n ${red('Usage:')} context-vault public remove <slug>\n`);
7638
+ process.exit(1);
7639
+ }
7640
+
7641
+ const configPath = join(dataDir, 'config.json');
7642
+ let config = {};
7643
+ try {
7644
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
7645
+ } catch {}
7646
+
7647
+ if (!config.remote?.publicVaults || !config.remote.publicVaults.includes(slug)) {
7648
+ console.log(`\n ${dim('Not found:')} ${slug}\n`);
7649
+ return;
7650
+ }
7651
+
7652
+ config.remote.publicVaults = config.remote.publicVaults.filter(s => s !== slug);
7653
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
7654
+ console.log(`\n ${green('✓')} Removed public vault: ${bold(slug)}\n`);
7655
+ return;
7656
+ }
7657
+
7658
+ // Default: show public help
7659
+ console.log();
7660
+ console.log(` ${bold('◇ context-vault public')}`);
7661
+ console.log();
7662
+ console.log(` ${cyan('create <name>')} Create a new public vault`);
7663
+ console.log(` ${dim('--slug <slug>')} URL-friendly identifier`);
7664
+ console.log(` ${dim('--description <d>')} Vault description`);
7665
+ console.log(` ${dim('--domains <tags>')} Comma-separated domain tags`);
7666
+ console.log(` ${dim('--visibility <v>')} free (default) or pro`);
7667
+ console.log(` ${cyan('seed')} Publish local entries to a public vault`);
7668
+ console.log(` ${dim('--vault <slug>')} Target public vault slug (required)`);
7669
+ console.log(` ${dim('--tags <filter>')} Filter entries by tag`);
7670
+ console.log(` ${dim('--dry-run')} Preview without publishing`);
7671
+ console.log(` ${cyan('list')} Browse available public vaults`);
7672
+ console.log(` ${dim('--domain <tag>')} Filter by domain tag`);
7673
+ console.log(` ${cyan('add <slug>')} Add a public vault to your agent config`);
7674
+ console.log(` ${cyan('remove <slug>')} Remove a public vault from your config`);
7675
+ console.log();
7676
+ }
7677
+
7370
7678
  async function runRemote() {
7371
7679
  const subcommand = args[1];
7372
7680
  const { getRemoteConfig, saveRemoteConfig } = await import('@context-vault/core/config');
@@ -8045,6 +8353,9 @@ async function main() {
8045
8353
  case 'team':
8046
8354
  await runTeam();
8047
8355
  break;
8356
+ case 'public':
8357
+ await runPublic();
8358
+ break;
8048
8359
  default:
8049
8360
  console.error(red(`Unknown command: ${command}`));
8050
8361
  console.error(`Run ${cyan('context-vault --help')} for usage.`);
@@ -0,0 +1,43 @@
1
+ # /vault-cleanup — Vault Maintenance
2
+
3
+ Identify stale, duplicate, or low-value entries and help the user clean up their vault.
4
+
5
+ ## Steps
6
+
7
+ 1. **Gather vault state.** Call `context_status()` to get entry counts, kind breakdown, recall stats, and warnings.
8
+
9
+ 2. **Identify cleanup candidates.** Use `list_context` to browse entries that may need attention:
10
+ ```
11
+ list_context({ kind: "session", limit: 20 })
12
+ list_context({ kind: "feedback", tags: ["auto-captured"], limit: 20 })
13
+ list_context({ kind: "observation", limit: 20 })
14
+ ```
15
+ Review results for entries with `recall_count: 0` (never recalled) and old `created_at` dates.
16
+
17
+ 3. **Check for duplicates.** Search for topics the user likely has many entries on:
18
+ ```
19
+ get_context({ query: "<suspected duplicate topic>", buckets: ["<project>"], limit: 15 })
20
+ ```
21
+ If multiple entries cover the same insight, suggest consolidating via `create_snapshot`.
22
+
23
+ 4. **Present a triage report.** For each category, show:
24
+ - Entry title, kind, created date, recall count
25
+ - Recommended action: **delete**, **consolidate**, **expire**, or **keep**
26
+
27
+ 5. **Execute only user-approved actions.** Wait for explicit confirmation before any deletion.
28
+
29
+ ## Available Actions
30
+
31
+ | Action | How | When |
32
+ |--------|-----|------|
33
+ | **Delete** | `delete_context({ id: "<entry-id>" })` | Duplicates, noise, irrelevant entries |
34
+ | **Consolidate** | `create_snapshot({ topic: "...", buckets: ["..."] })` | Many scattered entries on one topic |
35
+ | **Set to expire** | `save_context({ id: "<entry-id>", tier: "ephemeral" })` | Stale but not worth deleting now |
36
+ | **Supersede** | `save_context({ title: "...", body: "...", supersedes: ["<old-id>"] })` | Replace outdated with current |
37
+
38
+ ## Important
39
+
40
+ - `delete_context` is **permanent and irreversible**. Always confirm with the user.
41
+ - There is no bulk delete. Each deletion requires a separate call with a specific entry ID.
42
+ - There is no "archive" or "soft delete." To deprioritize without deleting, set `tier: "ephemeral"`.
43
+ - Do not reference tools or parameters that don't exist. Only use: `context_status`, `list_context`, `get_context`, `delete_context`, `save_context`, `create_snapshot`.
@@ -0,0 +1,43 @@
1
+ # /vault-snapshot — Create a Context Brief
2
+
3
+ Consolidate scattered vault entries on a topic into a single brief.
4
+
5
+ ## Steps
6
+
7
+ 1. Ask the user what topic they want a snapshot of. Also ask which project bucket to scope to (if not obvious from context).
8
+
9
+ 2. Call `create_snapshot` with the topic as a **natural language search query** (not a slug):
10
+ ```
11
+ create_snapshot({
12
+ topic: "API authentication decisions and patterns",
13
+ buckets: ["project-name"],
14
+ kinds: ["decision", "pattern"],
15
+ tags: ["authentication"]
16
+ })
17
+ ```
18
+ All parameters except `topic` are optional. `kinds` filters by entry kind. `tags` adds tag filters. `buckets` scopes to a project.
19
+
20
+ 3. The tool returns a short confirmation (not the brief content):
21
+ ```
22
+ ✓ Snapshot created → id: <entry-id>
23
+ title: <topic> — Context Brief
24
+ identity_key: snapshot-<slugified-topic>
25
+ synthesized from: N entries
26
+ ```
27
+ The brief is saved to the vault as a `kind: brief` entry.
28
+
29
+ 4. To show the user the brief, make a follow-up call:
30
+ ```
31
+ get_context({ kind: "brief", identity_key: "snapshot-<slugified-topic>" })
32
+ ```
33
+
34
+ 5. Present the brief and offer next steps:
35
+ - "Share this with your team" via `publish_to_team({ id: "<entry-id>" })`
36
+ - "Create another snapshot on a related topic"
37
+ - "Search for more context" via `get_context`
38
+
39
+ ## Tips
40
+
41
+ - The `topic` is used as a search query (hybrid FTS + semantic). Use descriptive natural language for best results. "API authentication decisions" works better than "api-auth".
42
+ - If "no entries found," try a broader query or different bucket.
43
+ - Snapshots are idempotent on `identity_key`. Re-running with the same topic updates the existing snapshot.