wyrm-mcp 3.4.0 → 3.5.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/README.md +30 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +393 -16
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +7 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +14 -3
- package/dist/intelligence.js.map +1 -1
- package/dist/memory-artifacts.d.ts +2 -0
- package/dist/memory-artifacts.d.ts.map +1 -1
- package/dist/memory-artifacts.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +19 -0
- package/dist/migrations.js.map +1 -1
- package/dist/wyrm-cli.js +266 -23
- package/dist/wyrm-cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,36 @@ AI: *checks Wyrm* "We went with JWT + refresh tokens, stored in httpOnly cookie
|
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
### npm (recommended)
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g wyrm-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Homebrew (macOS / Linux)
|
|
57
|
+
```bash
|
|
58
|
+
brew tap ghosts-lk/wyrm
|
|
59
|
+
brew install wyrm-mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Docker
|
|
63
|
+
```bash
|
|
64
|
+
docker run -d -p 3000:3000 -v wyrm-data:/data ghcr.io/ghosts-lk/wyrm-mcp:latest
|
|
65
|
+
```
|
|
66
|
+
Or with Docker Compose:
|
|
67
|
+
```bash
|
|
68
|
+
curl -O https://raw.githubusercontent.com/ghosts-lk/Wyrm/main/docker-compose.yml
|
|
69
|
+
docker compose up -d
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### npx (no install)
|
|
73
|
+
```bash
|
|
74
|
+
npx wyrm-mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
49
79
|
## Quick Start
|
|
50
80
|
|
|
51
81
|
### 1. Install
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AA4BH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,10 @@ import { WyrmSync } from "./sync.js";
|
|
|
23
23
|
import { createContextBundle } from "./summarizer.js";
|
|
24
24
|
import { autoConfigureAll, removeFromAll, getStatusSummary, findWyrmServerPath, getDefaultDbPath } from "./autoconfig.js";
|
|
25
25
|
import { cache, estimateTokens } from "./performance.js";
|
|
26
|
-
import { createHash } from "crypto";
|
|
26
|
+
import { createHash, randomBytes, pbkdf2Sync, createCipheriv, createDecipheriv } from "crypto";
|
|
27
|
+
import { readFileSync, writeFileSync, copyFileSync, chmodSync, unlinkSync, existsSync as fsExistsSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { join as pathJoin } from "path";
|
|
27
30
|
import { initializeOrchestrator, getDefaultConfig } from "./auto-orchestrator.js";
|
|
28
31
|
import { sanitizeFtsQuery, sanitizeString, validateBatchSize } from "./security.js";
|
|
29
32
|
import { getCrypto, initializeCrypto } from "./crypto.js";
|
|
@@ -31,7 +34,7 @@ import { createVectorStore } from "./vectors.js";
|
|
|
31
34
|
import { IndexingPipeline } from "./indexer.js";
|
|
32
35
|
import { KnowledgeGraph } from "./knowledge-graph.js";
|
|
33
36
|
import { MemoryArtifacts } from "./memory-artifacts.js";
|
|
34
|
-
import { GroundTruths, ReasoningScaffolds } from "./intelligence.js";
|
|
37
|
+
import { GroundTruths, ReasoningScaffolds, computeStaleness } from "./intelligence.js";
|
|
35
38
|
import { classifyCapture } from "./capture.js";
|
|
36
39
|
import { injectSystemPrompt } from "./autoconfig.js";
|
|
37
40
|
export { classifyCapture } from "./capture.js";
|
|
@@ -222,7 +225,6 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
222
225
|
"wyrm_entity_search",
|
|
223
226
|
"wyrm_entity_graph",
|
|
224
227
|
"wyrm_entity_path",
|
|
225
|
-
"wyrm_recall",
|
|
226
228
|
"wyrm_context_build",
|
|
227
229
|
"wyrm_truth_get",
|
|
228
230
|
"wyrm_scaffold_get",
|
|
@@ -251,6 +253,7 @@ const WRITE_TOOLS = new Set([
|
|
|
251
253
|
"wyrm_entity_link",
|
|
252
254
|
"wyrm_entity_merge",
|
|
253
255
|
"wyrm_remember",
|
|
256
|
+
"wyrm_recall",
|
|
254
257
|
"wyrm_feedback",
|
|
255
258
|
"wyrm_truth_set",
|
|
256
259
|
"wyrm_scaffold_save",
|
|
@@ -261,10 +264,13 @@ const WRITE_TOOLS = new Set([
|
|
|
261
264
|
"wyrm_import_pr",
|
|
262
265
|
"wyrm_import_rules",
|
|
263
266
|
"wyrm_inject_prompt",
|
|
267
|
+
"wyrm_prune",
|
|
268
|
+
"wyrm_sync_export",
|
|
269
|
+
"wyrm_sync_import",
|
|
264
270
|
]);
|
|
265
271
|
const server = new Server({
|
|
266
272
|
name: "wyrm",
|
|
267
|
-
version: "3.
|
|
273
|
+
version: "3.5.0",
|
|
268
274
|
}, {
|
|
269
275
|
capabilities: {
|
|
270
276
|
tools: {},
|
|
@@ -905,6 +911,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
905
911
|
rationale: { type: "string", description: "Why this is true / how it was determined" },
|
|
906
912
|
source: { type: "string", description: "Origin: 'user', 'derived', 'observed', 'confirmed'" },
|
|
907
913
|
confidence: { type: "number", description: "Confidence 0-1 (default: 1.0 for user-asserted truths)" },
|
|
914
|
+
ttl_days: { type: "number", description: "Time-to-live in days — truth becomes stale after this many days since last_verified_at" },
|
|
908
915
|
},
|
|
909
916
|
required: ["projectPath", "category", "key", "value"],
|
|
910
917
|
},
|
|
@@ -1020,6 +1027,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1020
1027
|
},
|
|
1021
1028
|
},
|
|
1022
1029
|
},
|
|
1030
|
+
{
|
|
1031
|
+
name: "wyrm_prune",
|
|
1032
|
+
description: "Prune low-confidence, stale memory artifacts. Dry-run by default — use dry_run:false with confirm_ids to actually delete. Never deletes artifacts in the review queue.",
|
|
1033
|
+
inputSchema: {
|
|
1034
|
+
type: "object",
|
|
1035
|
+
properties: {
|
|
1036
|
+
project_id: { type: "number", description: "Project ID (optional, all projects if omitted)" },
|
|
1037
|
+
min_confidence: { type: "number", description: "Prune artifacts below this confidence (default: 0.3)" },
|
|
1038
|
+
older_than_days: { type: "number", description: "Last accessed more than N days ago (default: 90)" },
|
|
1039
|
+
types: { type: "array", items: { type: "string" }, description: "Filter by artifact_type (optional)" },
|
|
1040
|
+
dry_run: { type: "boolean", description: "If true (default), return candidates without deleting. Set false to delete." },
|
|
1041
|
+
confirm_ids: { type: "array", items: { type: "number" }, description: "Required when dry_run:false — only IDs in this list will be deleted" },
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: "wyrm_sync_export",
|
|
1047
|
+
description: "Export an AES-256-GCM encrypted snapshot of the Wyrm database for cross-device sync. Passphrase from WYRM_SYNC_PASSPHRASE env var.",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
type: "object",
|
|
1050
|
+
properties: {
|
|
1051
|
+
output_path: { type: "string", description: "File path to write the encrypted snapshot" },
|
|
1052
|
+
description: { type: "string", description: "Optional description of this export" },
|
|
1053
|
+
},
|
|
1054
|
+
required: ["output_path"],
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "wyrm_sync_import",
|
|
1059
|
+
description: "Import an encrypted Wyrm snapshot. Use restore_mode:'preview' to inspect without touching the DB; 'restore' to replace current DB (backs up first).",
|
|
1060
|
+
inputSchema: {
|
|
1061
|
+
type: "object",
|
|
1062
|
+
properties: {
|
|
1063
|
+
input_path: { type: "string", description: "Path to the encrypted snapshot file" },
|
|
1064
|
+
restore_mode: { type: "string", enum: ["preview", "restore"], description: "preview = show stats only; restore = replace DB (backs up first)" },
|
|
1065
|
+
},
|
|
1066
|
+
required: ["input_path", "restore_mode"],
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1023
1069
|
{
|
|
1024
1070
|
name: "wyrm_import_git",
|
|
1025
1071
|
description: "Import a list of commits into the Wyrm memory review queue. Useful for onboarding a project's history or capturing recent work.",
|
|
@@ -1919,6 +1965,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1919
1965
|
limit: rcLimit ?? 10,
|
|
1920
1966
|
minConfidence: rcMinConf,
|
|
1921
1967
|
});
|
|
1968
|
+
// Track access for memory decay
|
|
1969
|
+
if (results.length > 0) {
|
|
1970
|
+
const rawDbRecall = db.getDatabase();
|
|
1971
|
+
const ids = results.map(r => r.artifact.id);
|
|
1972
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
1973
|
+
rawDbRecall.prepare(`UPDATE memory_artifacts SET last_accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${placeholders})`).run(...ids);
|
|
1974
|
+
}
|
|
1922
1975
|
if (results.length === 0) {
|
|
1923
1976
|
return { content: [{ type: "text", text: `🐉 No relevant memory found for: "${rcQuery}"\n\nUse \`wyrm_remember\` to store knowledge as you work.` }] };
|
|
1924
1977
|
}
|
|
@@ -1942,10 +1995,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1942
1995
|
text += `_Confidence: ${(a.confidence * 100).toFixed(0)}% | Used ${a.reuse_count}× | Success rate: ${a.reuse_count > 0 ? ((a.reuse_success_count / a.reuse_count) * 100).toFixed(0) + '%' : 'not yet used'}_\n\n`;
|
|
1943
1996
|
}
|
|
1944
1997
|
text += `_Use \`wyrm_feedback\` to record whether these were helpful._`;
|
|
1945
|
-
|
|
1946
|
-
if (cacheKey)
|
|
1947
|
-
cache.set(cacheKey, response, 15000);
|
|
1948
|
-
return response;
|
|
1998
|
+
return cachedResponse(text);
|
|
1949
1999
|
}
|
|
1950
2000
|
case "wyrm_feedback": {
|
|
1951
2001
|
const { artifactId, success } = args;
|
|
@@ -2004,7 +2054,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2004
2054
|
return response;
|
|
2005
2055
|
}
|
|
2006
2056
|
case "wyrm_truth_set": {
|
|
2007
|
-
const { projectPath: tPath, category: tCat, key: tKey, value: tVal, rationale: tRat, source: tSrc, confidence: tConf } = args;
|
|
2057
|
+
const { projectPath: tPath, category: tCat, key: tKey, value: tVal, rationale: tRat, source: tSrc, confidence: tConf, ttl_days: tTtl } = args;
|
|
2008
2058
|
const tProject = db.getProject(tPath);
|
|
2009
2059
|
if (!tProject)
|
|
2010
2060
|
return { content: [{ type: "text", text: `Project not found: ${tPath}` }], isError: true };
|
|
@@ -2015,10 +2065,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2015
2065
|
rationale: tRat,
|
|
2016
2066
|
source: tSrc,
|
|
2017
2067
|
confidence: tConf ?? 1.0,
|
|
2068
|
+
ttl_days: tTtl,
|
|
2018
2069
|
});
|
|
2019
2070
|
cache.invalidate('wyrm_truth_get');
|
|
2020
2071
|
cache.invalidate('wyrm_context_build');
|
|
2021
|
-
return { content: [{ type: "text", text: `🐉 **Ground Truth Set** ✅\n\n- **Category:** ${tCat}\n- **Key:** ${tKey}\n- **Value:** ${tVal}${tRat ? `\n- **Rationale:** ${tRat}` : ''}\n- **ID:** ${truth.id}\n\nThis truth will be injected at the top of every \`wyrm_context_build\` response for this project.` }] };
|
|
2072
|
+
return { content: [{ type: "text", text: `🐉 **Ground Truth Set** ✅\n\n- **Category:** ${tCat}\n- **Key:** ${tKey}\n- **Value:** ${tVal}${tRat ? `\n- **Rationale:** ${tRat}` : ''}${tTtl ? `\n- **TTL:** ${tTtl} days` : ''}\n- **ID:** ${truth.id}\n\nThis truth will be injected at the top of every \`wyrm_context_build\` response for this project.` }] };
|
|
2022
2073
|
}
|
|
2023
2074
|
case "wyrm_truth_get": {
|
|
2024
2075
|
const { projectPath: tgPath, category: tgCat } = args;
|
|
@@ -2042,11 +2093,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2042
2093
|
for (const [cat, items] of byCategory) {
|
|
2043
2094
|
text += `### ${cat}\n`;
|
|
2044
2095
|
for (const t of items) {
|
|
2045
|
-
|
|
2096
|
+
const staleness = computeStaleness(t);
|
|
2097
|
+
const stale = staleness !== null && staleness > 0.7;
|
|
2098
|
+
const stalePrefix = stale ? '[⚠️ STALE] ' : '';
|
|
2099
|
+
text += `- ${stalePrefix}**${t.key}:** ${t.value}`;
|
|
2046
2100
|
if (t.confidence < 1)
|
|
2047
2101
|
text += ` _(confidence: ${(t.confidence * 100).toFixed(0)}%)_`;
|
|
2048
2102
|
if (t.rationale)
|
|
2049
2103
|
text += `\n _${t.rationale}_`;
|
|
2104
|
+
if (staleness !== null)
|
|
2105
|
+
text += `\n _staleness: ${(staleness * 100).toFixed(0)}% (TTL: ${t.ttl_days}d)_`;
|
|
2050
2106
|
text += '\n';
|
|
2051
2107
|
}
|
|
2052
2108
|
text += '\n';
|
|
@@ -2591,6 +2647,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2591
2647
|
if (effectiveProjId === null) {
|
|
2592
2648
|
return { content: [{ type: "text", text: `🧠 Cannot capture truth without project_id. Please provide project_id.` }], isError: true };
|
|
2593
2649
|
}
|
|
2650
|
+
// Structural contradiction check: same project + same category
|
|
2651
|
+
if (capConf >= 80) {
|
|
2652
|
+
const capCategory = capSubtype ?? 'other';
|
|
2653
|
+
const existingTruths = groundTruths.getCurrent(effectiveProjId);
|
|
2654
|
+
const sameCategory = existingTruths.filter(t => t.category === capCategory);
|
|
2655
|
+
if (sameCategory.length > 0) {
|
|
2656
|
+
// Route to review queue — possible supersession
|
|
2657
|
+
const conflictArtifact = memory.add(effectiveProjId, {
|
|
2658
|
+
kind: 'pattern',
|
|
2659
|
+
problem: `Potential conflict with ${sameCategory.length} existing truth(s) in category "${capCategory}"`,
|
|
2660
|
+
validatedFix: capContent,
|
|
2661
|
+
whyItWorked: 'Pending review — possible supersession',
|
|
2662
|
+
tags: capTags ?? [],
|
|
2663
|
+
confidence: capConf / 100,
|
|
2664
|
+
needsReview: 1,
|
|
2665
|
+
});
|
|
2666
|
+
return {
|
|
2667
|
+
content: [{
|
|
2668
|
+
type: "text",
|
|
2669
|
+
text: JSON.stringify({
|
|
2670
|
+
status: 'queued_for_review',
|
|
2671
|
+
reason: 'conflict_check',
|
|
2672
|
+
artifact_id: conflictArtifact.id,
|
|
2673
|
+
conflicts_with: sameCategory.map(t => ({ id: t.id, content: t.value.slice(0, 80) })),
|
|
2674
|
+
}),
|
|
2675
|
+
}],
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2594
2679
|
if (capMode !== 'truth' && capConf < 100) {
|
|
2595
2680
|
// Store as artifact pending review
|
|
2596
2681
|
const artifact = memory.add(effectiveProjId, {
|
|
@@ -2638,6 +2723,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2638
2723
|
if (!shouldAutoCreate) {
|
|
2639
2724
|
needsReviewNote = `\n⚠️ Stored for review — run \`wyrm_review\` to activate`;
|
|
2640
2725
|
}
|
|
2726
|
+
// Advisory conflict check for heuristics (non-blocking)
|
|
2727
|
+
let advisoryConflicts;
|
|
2728
|
+
if (capSubtype === 'heuristic' && capConf >= 75 && effectiveProjId !== null) {
|
|
2729
|
+
try {
|
|
2730
|
+
const rawDbCap = db.getDatabase();
|
|
2731
|
+
const sanitizedCap = capContent.replace(/['"*]/g, ' ').trim().split(/\s+/).slice(0, 5).join(' ');
|
|
2732
|
+
const heuristicConflicts = rawDbCap.prepare(`
|
|
2733
|
+
SELECT a.id, a.problem FROM memory_artifacts a
|
|
2734
|
+
JOIN memory_artifacts_fts fts ON a.id = fts.rowid
|
|
2735
|
+
WHERE fts MATCH ? AND a.project_id = ? AND a.kind = 'heuristic' AND a.needs_review = 0
|
|
2736
|
+
LIMIT 3
|
|
2737
|
+
`).all(sanitizedCap, effectiveProjId);
|
|
2738
|
+
if (heuristicConflicts.length > 0) {
|
|
2739
|
+
advisoryConflicts = heuristicConflicts.map(h => ({ id: h.id, content: h.problem.slice(0, 80) }));
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
catch {
|
|
2743
|
+
// FTS may fail on short queries — advisory only, ignore
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (advisoryConflicts && advisoryConflicts.length > 0) {
|
|
2747
|
+
return {
|
|
2748
|
+
content: [{
|
|
2749
|
+
type: "text",
|
|
2750
|
+
text: `🧠 Captured as ${capType}: ${capSubtype}\nConfidence: ${capConf}% | ${capReasoning}\nID: ${typeShort}:${storedId}${needsReviewNote}\n\n⚠️ Advisory: ${advisoryConflicts.length} similar heuristic(s) found:\n${advisoryConflicts.map(c => ` - [${c.id}] ${c.content}`).join('\n')}`,
|
|
2751
|
+
}],
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2641
2754
|
}
|
|
2642
2755
|
return {
|
|
2643
2756
|
content: [{
|
|
@@ -2674,10 +2787,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2674
2787
|
return { content: [{ type: "text", text: `🐉 Project not found. Available projects:\n${candidates}` }] };
|
|
2675
2788
|
}
|
|
2676
2789
|
const sections = [];
|
|
2677
|
-
// Section 1 — Ground Truths (up to 1200 chars)
|
|
2678
|
-
const
|
|
2679
|
-
if (
|
|
2680
|
-
|
|
2790
|
+
// Section 1 — Ground Truths (up to 1200 chars), prefix stale truths
|
|
2791
|
+
const spCurrentTruths = groundTruths.getCurrent(spProject.id);
|
|
2792
|
+
if (spCurrentTruths.length > 0) {
|
|
2793
|
+
const CATEGORY_LABELS_SP = {
|
|
2794
|
+
tech_stack: '🛠️ Tech Stack', architecture: '🏗️ Architecture',
|
|
2795
|
+
constraint: '🚧 Constraints', decision: '📋 Decisions',
|
|
2796
|
+
contact: '👤 Contacts', other: '📌 Other Facts',
|
|
2797
|
+
};
|
|
2798
|
+
let spTruthsText = '### 📚 Project Ground Truths _(treat as authoritative — do not infer or contradict)_\n';
|
|
2799
|
+
const spByCategory = new Map();
|
|
2800
|
+
for (const t of spCurrentTruths) {
|
|
2801
|
+
if (!spByCategory.has(t.category))
|
|
2802
|
+
spByCategory.set(t.category, []);
|
|
2803
|
+
spByCategory.get(t.category).push(t);
|
|
2804
|
+
}
|
|
2805
|
+
for (const [cat, items] of spByCategory) {
|
|
2806
|
+
spTruthsText += `**${CATEGORY_LABELS_SP[cat] ?? cat}:**\n`;
|
|
2807
|
+
for (const t of items) {
|
|
2808
|
+
const staleness = computeStaleness(t);
|
|
2809
|
+
const stale = staleness !== null && staleness > 0.7;
|
|
2810
|
+
const stalePrefix = stale ? '[⚠️ STALE] ' : '';
|
|
2811
|
+
spTruthsText += `- ${stalePrefix}**${t.key}:** ${t.value}${t.rationale ? ` _(${t.rationale})_` : ''}\n`;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
sections.push(spTruthsText.slice(0, 1200));
|
|
2815
|
+
}
|
|
2681
2816
|
// Section 2 — Reasoning Scaffold (up to 1500 chars)
|
|
2682
2817
|
if (spTask) {
|
|
2683
2818
|
const scaffoldMatchSP = scaffoldLib.findBest(spTask, spProject.id, 0.3);
|
|
@@ -2869,7 +3004,249 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2869
3004
|
}
|
|
2870
3005
|
return { content: [{ type: "text", text: injText }] };
|
|
2871
3006
|
}
|
|
2872
|
-
|
|
3007
|
+
// ==================== v3.5.0 TOOLS ====================
|
|
3008
|
+
case "wyrm_prune": {
|
|
3009
|
+
const { project_id: pruneProjectId, min_confidence: pruneMinConf = 0.3, older_than_days: pruneOlderDays = 90, types: pruneTypes, dry_run: pruneDryRun = true, confirm_ids: pruneConfirmIds, } = args;
|
|
3010
|
+
const rawDbPrune = db.getDatabase();
|
|
3011
|
+
const projectClausePrune = pruneProjectId !== undefined ? `AND project_id = ${pruneProjectId}` : '';
|
|
3012
|
+
const typeClausePrune = pruneTypes && pruneTypes.length > 0
|
|
3013
|
+
? `AND kind IN (${pruneTypes.map(() => '?').join(',')})`
|
|
3014
|
+
: '';
|
|
3015
|
+
const typeParamsPrune = pruneTypes ?? [];
|
|
3016
|
+
const candidateRows = rawDbPrune.prepare(`
|
|
3017
|
+
SELECT id, project_id, kind, problem, confidence, last_accessed_at, access_count
|
|
3018
|
+
FROM memory_artifacts
|
|
3019
|
+
WHERE confidence < ?
|
|
3020
|
+
AND (last_accessed_at IS NULL OR last_accessed_at < datetime('now', ?))
|
|
3021
|
+
AND needs_review = 0
|
|
3022
|
+
AND supersedes_id IS NULL
|
|
3023
|
+
${projectClausePrune}
|
|
3024
|
+
${typeClausePrune}
|
|
3025
|
+
ORDER BY confidence ASC, last_accessed_at ASC
|
|
3026
|
+
LIMIT 500
|
|
3027
|
+
`).all(pruneMinConf, `-${pruneOlderDays} days`, ...typeParamsPrune);
|
|
3028
|
+
if (pruneDryRun) {
|
|
3029
|
+
return {
|
|
3030
|
+
content: [{
|
|
3031
|
+
type: "text",
|
|
3032
|
+
text: JSON.stringify({
|
|
3033
|
+
dry_run: true,
|
|
3034
|
+
count: candidateRows.length,
|
|
3035
|
+
candidates: candidateRows.map(r => ({
|
|
3036
|
+
id: r.id,
|
|
3037
|
+
kind: r.kind,
|
|
3038
|
+
problem: r.problem.slice(0, 100),
|
|
3039
|
+
confidence: r.confidence,
|
|
3040
|
+
last_accessed_at: r.last_accessed_at,
|
|
3041
|
+
})),
|
|
3042
|
+
}),
|
|
3043
|
+
}],
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
// dry_run: false — require confirm_ids
|
|
3047
|
+
if (!pruneConfirmIds || pruneConfirmIds.length === 0) {
|
|
3048
|
+
return {
|
|
3049
|
+
content: [{ type: "text", text: `🐉 **Prune**: dry_run:false requires confirm_ids — provide the IDs from a dry-run to confirm deletion.` }],
|
|
3050
|
+
isError: true,
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
const candidateIds = new Set(candidateRows.map(r => r.id));
|
|
3054
|
+
const toDelete = pruneConfirmIds.filter(id => candidateIds.has(id));
|
|
3055
|
+
if (toDelete.length === 0) {
|
|
3056
|
+
return {
|
|
3057
|
+
content: [{ type: "text", text: `🐉 **Prune**: No matching IDs to delete (confirm_ids not in candidate set).` }],
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
const placeholdersPrune = toDelete.map(() => '?').join(',');
|
|
3061
|
+
const deleteResult = rawDbPrune.prepare(`DELETE FROM memory_artifacts WHERE id IN (${placeholdersPrune}) AND needs_review = 0`).run(...toDelete);
|
|
3062
|
+
return {
|
|
3063
|
+
content: [{
|
|
3064
|
+
type: "text",
|
|
3065
|
+
text: JSON.stringify({ deleted: deleteResult.changes, ids: toDelete }),
|
|
3066
|
+
}],
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
case "wyrm_sync_export": {
|
|
3070
|
+
const { output_path: exportPath, description: exportDesc } = args;
|
|
3071
|
+
const passphrase = process.env.WYRM_SYNC_PASSPHRASE;
|
|
3072
|
+
if (!passphrase) {
|
|
3073
|
+
return {
|
|
3074
|
+
content: [{ type: "text", text: `🐉 **Sync Export**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted export.` }],
|
|
3075
|
+
isError: true,
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
const dbPath = db.getDatabasePath();
|
|
3079
|
+
const wyrmDir = pathJoin(homedir(), '.wyrm');
|
|
3080
|
+
const tempDbPath = pathJoin(wyrmDir, 'wyrm_sync_export_temp.db');
|
|
3081
|
+
try {
|
|
3082
|
+
// WAL-safe snapshot via VACUUM INTO
|
|
3083
|
+
const rawDbExport = db.getDatabase();
|
|
3084
|
+
if (fsExistsSync(tempDbPath))
|
|
3085
|
+
unlinkSync(tempDbPath);
|
|
3086
|
+
rawDbExport.prepare(`VACUUM INTO ?`).run(tempDbPath);
|
|
3087
|
+
// Read snapshot
|
|
3088
|
+
const plaintext = readFileSync(tempDbPath);
|
|
3089
|
+
// Derive key
|
|
3090
|
+
const salt = randomBytes(32);
|
|
3091
|
+
const iv = randomBytes(16);
|
|
3092
|
+
const key = pbkdf2Sync(passphrase, salt, 600000, 32, 'sha256');
|
|
3093
|
+
// Encrypt
|
|
3094
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
3095
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
3096
|
+
const authTag = cipher.getAuthTag();
|
|
3097
|
+
// Binary format: WYRM(4) + version(1) + salt(32) + iv(16) + authTag(16) + data
|
|
3098
|
+
const magic = Buffer.from('WYRM');
|
|
3099
|
+
const version = Buffer.alloc(1);
|
|
3100
|
+
version.writeUInt8(1, 0);
|
|
3101
|
+
const output = Buffer.concat([magic, version, salt, iv, authTag, encrypted]);
|
|
3102
|
+
writeFileSync(exportPath, output);
|
|
3103
|
+
try {
|
|
3104
|
+
chmodSync(exportPath, 0o600);
|
|
3105
|
+
}
|
|
3106
|
+
catch { /* non-fatal */ }
|
|
3107
|
+
// Clean up temp
|
|
3108
|
+
try {
|
|
3109
|
+
unlinkSync(tempDbPath);
|
|
3110
|
+
}
|
|
3111
|
+
catch { /* non-fatal */ }
|
|
3112
|
+
const sizeMb = (output.length / (1024 * 1024)).toFixed(2);
|
|
3113
|
+
return {
|
|
3114
|
+
content: [{
|
|
3115
|
+
type: "text",
|
|
3116
|
+
text: JSON.stringify({
|
|
3117
|
+
success: true,
|
|
3118
|
+
path: exportPath,
|
|
3119
|
+
size_mb: parseFloat(sizeMb),
|
|
3120
|
+
exported_at: new Date().toISOString(),
|
|
3121
|
+
description: exportDesc ?? null,
|
|
3122
|
+
}),
|
|
3123
|
+
}],
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
catch (err) {
|
|
3127
|
+
try {
|
|
3128
|
+
if (fsExistsSync(tempDbPath))
|
|
3129
|
+
unlinkSync(tempDbPath);
|
|
3130
|
+
}
|
|
3131
|
+
catch { /* clean up */ }
|
|
3132
|
+
return {
|
|
3133
|
+
content: [{ type: "text", text: `🐉 **Sync Export** failed: ${err}` }],
|
|
3134
|
+
isError: true,
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
case "wyrm_sync_import":
|
|
3139
|
+
{
|
|
3140
|
+
const { input_path: importPath, restore_mode: restoreMode } = args;
|
|
3141
|
+
const passphrase = process.env.WYRM_SYNC_PASSPHRASE;
|
|
3142
|
+
if (!passphrase) {
|
|
3143
|
+
return {
|
|
3144
|
+
content: [{ type: "text", text: `🐉 **Sync Import**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted import.` }],
|
|
3145
|
+
isError: true,
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
const wyrmDirImport = pathJoin(homedir(), '.wyrm');
|
|
3149
|
+
const tempImportPath = pathJoin(wyrmDirImport, 'wyrm_sync_import_temp.db');
|
|
3150
|
+
try {
|
|
3151
|
+
const fileData = readFileSync(importPath);
|
|
3152
|
+
// Validate magic bytes
|
|
3153
|
+
const magic = fileData.subarray(0, 4).toString('ascii');
|
|
3154
|
+
if (magic !== 'WYRM') {
|
|
3155
|
+
return { content: [{ type: "text", text: `🐉 Invalid Wyrm snapshot file (bad magic bytes).` }], isError: true };
|
|
3156
|
+
}
|
|
3157
|
+
const version = fileData.readUInt8(4);
|
|
3158
|
+
if (version !== 1) {
|
|
3159
|
+
return { content: [{ type: "text", text: `🐉 Unsupported snapshot version: ${version}` }], isError: true };
|
|
3160
|
+
}
|
|
3161
|
+
const salt = fileData.subarray(5, 37);
|
|
3162
|
+
const iv = fileData.subarray(37, 53);
|
|
3163
|
+
const authTag = fileData.subarray(53, 69);
|
|
3164
|
+
const encrypted = fileData.subarray(69);
|
|
3165
|
+
// Derive key & decrypt
|
|
3166
|
+
const key = pbkdf2Sync(passphrase, salt, 600000, 32, 'sha256');
|
|
3167
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
3168
|
+
decipher.setAuthTag(authTag);
|
|
3169
|
+
let decrypted;
|
|
3170
|
+
try {
|
|
3171
|
+
decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
3172
|
+
}
|
|
3173
|
+
catch {
|
|
3174
|
+
return { content: [{ type: "text", text: `🐉 Decryption failed — wrong passphrase or corrupted snapshot.` }], isError: true };
|
|
3175
|
+
}
|
|
3176
|
+
if (restoreMode === 'preview') {
|
|
3177
|
+
// Open temp DB and return stats
|
|
3178
|
+
if (fsExistsSync(tempImportPath))
|
|
3179
|
+
unlinkSync(tempImportPath);
|
|
3180
|
+
writeFileSync(tempImportPath, decrypted);
|
|
3181
|
+
let previewStats = {};
|
|
3182
|
+
try {
|
|
3183
|
+
const BetterSQLite = (await import('better-sqlite3')).default;
|
|
3184
|
+
const previewDb = new BetterSQLite(tempImportPath, { readonly: true });
|
|
3185
|
+
const tables = ['projects', 'sessions', 'ground_truths', 'memory_artifacts', 'quests'];
|
|
3186
|
+
for (const t of tables) {
|
|
3187
|
+
try {
|
|
3188
|
+
const row = previewDb.prepare(`SELECT COUNT(*) as n FROM ${t}`).get();
|
|
3189
|
+
previewStats[t] = row.n;
|
|
3190
|
+
}
|
|
3191
|
+
catch {
|
|
3192
|
+
previewStats[t] = 0;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
previewDb.close();
|
|
3196
|
+
}
|
|
3197
|
+
catch { /* stats optional */ }
|
|
3198
|
+
try {
|
|
3199
|
+
unlinkSync(tempImportPath);
|
|
3200
|
+
}
|
|
3201
|
+
catch { /* clean up */ }
|
|
3202
|
+
return {
|
|
3203
|
+
content: [{
|
|
3204
|
+
type: "text",
|
|
3205
|
+
text: JSON.stringify({ preview: true, stats: previewStats }),
|
|
3206
|
+
}],
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
// restore mode
|
|
3210
|
+
const dbPathImport = db.getDatabasePath();
|
|
3211
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3212
|
+
const backupPath = `${dbPathImport}.backup.${timestamp}`;
|
|
3213
|
+
// Backup current DB
|
|
3214
|
+
copyFileSync(dbPathImport, backupPath);
|
|
3215
|
+
// Write decrypted to temp
|
|
3216
|
+
if (fsExistsSync(tempImportPath))
|
|
3217
|
+
unlinkSync(tempImportPath);
|
|
3218
|
+
writeFileSync(tempImportPath, decrypted);
|
|
3219
|
+
// Close current DB connection, replace file, reopen
|
|
3220
|
+
const rawDbImport = db.getDatabase();
|
|
3221
|
+
rawDbImport.close();
|
|
3222
|
+
copyFileSync(tempImportPath, dbPathImport);
|
|
3223
|
+
try {
|
|
3224
|
+
unlinkSync(tempImportPath);
|
|
3225
|
+
}
|
|
3226
|
+
catch { /* clean up */ }
|
|
3227
|
+
return {
|
|
3228
|
+
content: [{
|
|
3229
|
+
type: "text",
|
|
3230
|
+
text: JSON.stringify({
|
|
3231
|
+
success: true,
|
|
3232
|
+
restored_from: importPath,
|
|
3233
|
+
backup_at: backupPath,
|
|
3234
|
+
}),
|
|
3235
|
+
}],
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
catch (err) {
|
|
3239
|
+
try {
|
|
3240
|
+
if (fsExistsSync(tempImportPath))
|
|
3241
|
+
unlinkSync(tempImportPath);
|
|
3242
|
+
}
|
|
3243
|
+
catch { /* clean up */ }
|
|
3244
|
+
return {
|
|
3245
|
+
content: [{ type: "text", text: `🐉 **Sync Import** failed: ${err}` }],
|
|
3246
|
+
isError: true,
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
2873
3250
|
return {
|
|
2874
3251
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
2875
3252
|
isError: true,
|