wyrm-mcp 7.2.0 → 7.2.2
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/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/constellation.js
CHANGED
|
@@ -1,193 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
* wyrm_constellation — cross-project memory query (spec 018 phase 3).
|
|
3
|
-
*
|
|
4
|
-
* Asks one FTS5 question across *every* registered Wyrm project — not just
|
|
5
|
-
* the current one. The killer founder-grade query: "have I solved this
|
|
6
|
-
* before in any project?" Returns candidates grouped by project; the
|
|
7
|
-
* calling AI does semantic ranking on its side (per spec 018 decision #2 —
|
|
8
|
-
* no embedding dependency).
|
|
9
|
-
*
|
|
10
|
-
* Privacy: respects the `cross_project_visibility` flag (added by
|
|
11
|
-
* migration 14, default `'within'`). Only artifacts explicitly flagged
|
|
12
|
-
* `'org'` or `'public'` leak across the project boundary.
|
|
13
|
-
*
|
|
14
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
15
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
16
|
-
*/
|
|
17
|
-
const SAFE_KINDS = new Set(['truth', 'artifact', 'quest', 'decision', 'reference']);
|
|
18
|
-
const SAFE_VIS = new Set(['within', 'org', 'public']);
|
|
19
|
-
function sanitizeFtsQuery(q) {
|
|
20
|
-
// FTS5 syntax allows `"`, `*`, `AND`, `OR`, `NOT`. We strip anything that
|
|
21
|
-
// could close out into raw SQL — only allow word chars, spaces, quotes,
|
|
22
|
-
// asterisk, +/-. This is defense-in-depth; we already use parameterized
|
|
23
|
-
// bind, but the query also drives string-concat in some paths.
|
|
24
|
-
return q.replace(/[^\w\s"\*\-\+]/g, ' ').trim().slice(0, 200);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Query a single project's FTS5 indexes for matching artifacts.
|
|
28
|
-
* Pre-filtered to honour visibility; returns up to `limit` hits.
|
|
29
|
-
*/
|
|
30
|
-
function queryProject(db, project, query, kinds, visibility, limit) {
|
|
31
|
-
const hits = [];
|
|
32
|
-
const visList = `(${[...visibility].map((v) => `'${v}'`).join(',')})`;
|
|
33
|
-
const safeQ = sanitizeFtsQuery(query);
|
|
34
|
-
if (!safeQ)
|
|
35
|
-
return [];
|
|
36
|
-
// truths
|
|
37
|
-
if (kinds.has('truth')) {
|
|
38
|
-
try {
|
|
39
|
-
const rows = db.prepare(`
|
|
1
|
+
const p=new Set(["truth","artifact","quest","decision","reference"]),u=new Set(["within","org","public"]);function f(s){return s.replace(/[^\w\s"\*\-\+]/g," ").trim().slice(0,200)}function _(s,i,l,c,n,a){const r=[],d=`(${[...n].map(o=>`'${o}'`).join(",")})`,e=f(l);if(!e)return[];if(c.has("truth"))try{const o=s.prepare(`
|
|
40
2
|
SELECT id, key, value, cross_project_visibility AS visibility, created_at
|
|
41
3
|
FROM ground_truths
|
|
42
4
|
WHERE project_id = ?
|
|
43
|
-
AND cross_project_visibility IN ${
|
|
5
|
+
AND cross_project_visibility IN ${d}
|
|
44
6
|
AND (key LIKE '%' || ? || '%' OR value LIKE '%' || ? || '%')
|
|
45
7
|
ORDER BY created_at DESC
|
|
46
8
|
LIMIT ?
|
|
47
|
-
`).all(
|
|
48
|
-
for (const r of rows) {
|
|
49
|
-
hits.push({
|
|
50
|
-
projectId: project.id,
|
|
51
|
-
projectName: project.name,
|
|
52
|
-
kind: 'truth',
|
|
53
|
-
id: r.id,
|
|
54
|
-
title: r.key,
|
|
55
|
-
snippet: r.value.slice(0, 200),
|
|
56
|
-
visibility: r.visibility,
|
|
57
|
-
capturedAt: r.created_at,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch { /* table or column may not exist on older schema */ }
|
|
62
|
-
}
|
|
63
|
-
// memory artifacts
|
|
64
|
-
if (kinds.has('artifact')) {
|
|
65
|
-
try {
|
|
66
|
-
const rows = db.prepare(`
|
|
9
|
+
`).all(i.id,e,e,a);for(const t of o)r.push({projectId:i.id,projectName:i.name,kind:"truth",id:t.id,title:t.key,snippet:t.value.slice(0,200),visibility:t.visibility,capturedAt:t.created_at})}catch{}if(c.has("artifact"))try{const o=s.prepare(`
|
|
67
10
|
SELECT ma.id, ma.problem AS title, ma.validated_fix AS snippet,
|
|
68
11
|
ma.cross_project_visibility AS visibility, ma.created_at
|
|
69
12
|
FROM memory_artifacts ma
|
|
70
13
|
WHERE ma.project_id = ?
|
|
71
|
-
AND ma.cross_project_visibility IN ${
|
|
14
|
+
AND ma.cross_project_visibility IN ${d}
|
|
72
15
|
AND (ma.problem LIKE '%' || ? || '%' OR ma.validated_fix LIKE '%' || ? || '%')
|
|
73
16
|
ORDER BY ma.created_at DESC
|
|
74
17
|
LIMIT ?
|
|
75
|
-
`).all(
|
|
76
|
-
for (const r of rows) {
|
|
77
|
-
hits.push({
|
|
78
|
-
projectId: project.id,
|
|
79
|
-
projectName: project.name,
|
|
80
|
-
kind: 'artifact',
|
|
81
|
-
id: r.id,
|
|
82
|
-
title: r.title.slice(0, 100),
|
|
83
|
-
snippet: (r.snippet || '').slice(0, 200),
|
|
84
|
-
visibility: r.visibility,
|
|
85
|
-
capturedAt: r.created_at,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch { /* tolerant */ }
|
|
90
|
-
}
|
|
91
|
-
// quests
|
|
92
|
-
if (kinds.has('quest')) {
|
|
93
|
-
try {
|
|
94
|
-
const rows = db.prepare(`
|
|
18
|
+
`).all(i.id,e,e,a);for(const t of o)r.push({projectId:i.id,projectName:i.name,kind:"artifact",id:t.id,title:t.title.slice(0,100),snippet:(t.snippet||"").slice(0,200),visibility:t.visibility,capturedAt:t.created_at})}catch{}if(c.has("quest"))try{const o=s.prepare(`
|
|
95
19
|
SELECT id, title, description, cross_project_visibility AS visibility, created_at
|
|
96
20
|
FROM quests
|
|
97
21
|
WHERE project_id = ?
|
|
98
|
-
AND cross_project_visibility IN ${
|
|
22
|
+
AND cross_project_visibility IN ${d}
|
|
99
23
|
AND (title LIKE '%' || ? || '%' OR description LIKE '%' || ? || '%')
|
|
100
24
|
ORDER BY created_at DESC
|
|
101
25
|
LIMIT ?
|
|
102
|
-
`).all(
|
|
103
|
-
for (const r of rows) {
|
|
104
|
-
hits.push({
|
|
105
|
-
projectId: project.id,
|
|
106
|
-
projectName: project.name,
|
|
107
|
-
kind: 'quest',
|
|
108
|
-
id: r.id,
|
|
109
|
-
title: r.title,
|
|
110
|
-
snippet: (r.description || '').slice(0, 200),
|
|
111
|
-
visibility: r.visibility,
|
|
112
|
-
capturedAt: r.created_at,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch { /* tolerant */ }
|
|
117
|
-
}
|
|
118
|
-
// design references (using existing FTS5 index)
|
|
119
|
-
if (kinds.has('reference')) {
|
|
120
|
-
try {
|
|
121
|
-
const rows = db.prepare(`
|
|
26
|
+
`).all(i.id,e,e,a);for(const t of o)r.push({projectId:i.id,projectName:i.name,kind:"quest",id:t.id,title:t.title,snippet:(t.description||"").slice(0,200),visibility:t.visibility,capturedAt:t.created_at})}catch{}if(c.has("reference"))try{const o=s.prepare(`
|
|
122
27
|
SELECT dr.id, dr.title, dr.notes, dr.tags, dr.cross_project_visibility AS visibility, dr.captured_at
|
|
123
28
|
FROM design_references dr
|
|
124
29
|
WHERE dr.project_id = ?
|
|
125
|
-
AND dr.cross_project_visibility IN ${
|
|
30
|
+
AND dr.cross_project_visibility IN ${d}
|
|
126
31
|
AND (dr.title LIKE '%' || ? || '%' OR dr.notes LIKE '%' || ? || '%' OR dr.tags LIKE '%' || ? || '%')
|
|
127
32
|
ORDER BY dr.captured_at DESC
|
|
128
33
|
LIMIT ?
|
|
129
|
-
`).all(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
projectName: project.name,
|
|
134
|
-
kind: 'reference',
|
|
135
|
-
id: r.id,
|
|
136
|
-
title: r.title || `(reference #${r.id})`,
|
|
137
|
-
snippet: [r.notes, r.tags].filter(Boolean).join(' · ').slice(0, 200),
|
|
138
|
-
visibility: r.visibility,
|
|
139
|
-
capturedAt: r.captured_at,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch { /* tolerant */ }
|
|
144
|
-
}
|
|
145
|
-
return hits;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Run a constellation query across every project.
|
|
149
|
-
* Tolerant of older schemas (any table or column missing simply skips).
|
|
150
|
-
*/
|
|
151
|
-
export function constellationQuery(db, opts) {
|
|
152
|
-
const kinds = new Set((opts.kinds ?? ['truth', 'artifact', 'quest', 'reference']).filter((k) => SAFE_KINDS.has(k)));
|
|
153
|
-
const visibility = new Set((opts.visibility ?? ['org', 'public']).filter((v) => SAFE_VIS.has(v)));
|
|
154
|
-
// If the caller wants their OWN project's data too, allow 'within'.
|
|
155
|
-
// Otherwise we strictly enforce the cross-project gate.
|
|
156
|
-
const includeOwn = visibility.has('within');
|
|
157
|
-
const perProject = Math.min(Math.max(1, opts.perProjectLimit ?? 5), 50);
|
|
158
|
-
const projects = db.prepare(`SELECT id, name FROM projects ORDER BY id`).all();
|
|
159
|
-
const allHits = [];
|
|
160
|
-
for (const p of projects) {
|
|
161
|
-
if (!includeOwn && opts.callerProjectId && p.id === opts.callerProjectId)
|
|
162
|
-
continue;
|
|
163
|
-
const hits = queryProject(db, p, opts.query, kinds, visibility, perProject);
|
|
164
|
-
allHits.push(...hits);
|
|
165
|
-
}
|
|
166
|
-
// Sort: most recent first overall.
|
|
167
|
-
allHits.sort((a, b) => b.capturedAt.localeCompare(a.capturedAt));
|
|
168
|
-
return allHits;
|
|
169
|
-
}
|
|
170
|
-
export function renderConstellation(hits, query) {
|
|
171
|
-
if (hits.length === 0) {
|
|
172
|
-
return ` No constellation matches for *${query}* across registered projects.\n\n_Set \`cross_project_visibility\` to \`'org'\` or \`'public'\` on the items you want surfaced here. Default is \`'within'\` — within-project only._`;
|
|
173
|
-
}
|
|
174
|
-
// Group by project
|
|
175
|
-
const byProject = new Map();
|
|
176
|
-
for (const h of hits) {
|
|
177
|
-
const arr = byProject.get(h.projectName) ?? [];
|
|
178
|
-
arr.push(h);
|
|
179
|
-
byProject.set(h.projectName, arr);
|
|
180
|
-
}
|
|
181
|
-
const lines = [` **Constellation** — ${hits.length} match(es) for *${query}* across ${byProject.size} project(s)`];
|
|
182
|
-
for (const [name, items] of byProject) {
|
|
183
|
-
lines.push('');
|
|
184
|
-
lines.push(`### ${name}`);
|
|
185
|
-
for (const h of items) {
|
|
186
|
-
lines.push(`- \`${h.kind}:${h.id}\` **${h.title}** (${h.visibility})`);
|
|
187
|
-
if (h.snippet)
|
|
188
|
-
lines.push(` ${h.snippet}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return lines.join('\n');
|
|
192
|
-
}
|
|
193
|
-
//# sourceMappingURL=constellation.js.map
|
|
34
|
+
`).all(i.id,e,e,e,a);for(const t of o)r.push({projectId:i.id,projectName:i.name,kind:"reference",id:t.id,title:t.title||`(reference #${t.id})`,snippet:[t.notes,t.tags].filter(Boolean).join(" \xB7 ").slice(0,200),visibility:t.visibility,capturedAt:t.captured_at})}catch{}return r}function h(s,i){const l=new Set((i.kinds??["truth","artifact","quest","reference"]).filter(e=>p.has(e))),c=new Set((i.visibility??["org","public"]).filter(e=>u.has(e))),n=c.has("within"),a=Math.min(Math.max(1,i.perProjectLimit??5),50),r=s.prepare("SELECT id, name FROM projects ORDER BY id").all(),d=[];for(const e of r){if(!n&&i.callerProjectId&&e.id===i.callerProjectId)continue;const o=_(s,e,i.query,l,c,a);d.push(...o)}return d.sort((e,o)=>o.capturedAt.localeCompare(e.capturedAt)),d}function y(s,i){if(s.length===0)return`\u{F115D} No constellation matches for *${i}* across registered projects.
|
|
35
|
+
|
|
36
|
+
_Set \`cross_project_visibility\` to \`'org'\` or \`'public'\` on the items you want surfaced here. Default is \`'within'\` \u2014 within-project only._`;const l=new Map;for(const n of s){const a=l.get(n.projectName)??[];a.push(n),l.set(n.projectName,a)}const c=[`\u{F115D} **Constellation** \u2014 ${s.length} match(es) for *${i}* across ${l.size} project(s)`];for(const[n,a]of l){c.push(""),c.push(`### ${n}`);for(const r of a)c.push(`- \`${r.kind}:${r.id}\` **${r.title}** (${r.visibility})`),r.snippet&&c.push(` ${r.snippet}`)}return c.join(`
|
|
37
|
+
`)}export{h as constellationQuery,y as renderConstellation};
|
|
@@ -1,144 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* cutoff elide to one-line stubs the agent can re-`wyrm_recall`. Lifted VERBATIM
|
|
6
|
-
* behind a factory that captures the same index.ts singletons + budgeting
|
|
7
|
-
* helpers it always closed over (deterministic; no LLM, Article III).
|
|
8
|
-
*
|
|
9
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
10
|
-
* @license AGPL-3.0-or-later
|
|
11
|
-
*/
|
|
12
|
-
import { applyBudget, resolveBudget } from "./token-budget.js";
|
|
13
|
-
import { score as rankScore } from "./context-ranking.js";
|
|
14
|
-
export function makeRunBudgetedContextBuild(deps) {
|
|
15
|
-
const { groundTruths, scaffoldLib, memory, toolAnalytics, sessionSeen, tokenEstimator, rankWeights } = deps;
|
|
16
|
-
return function runBudgetedContextBuild(opts) {
|
|
17
|
-
// Resolve effective budget per spec 014 D1 precedence: call > env > fallback.
|
|
18
|
-
const budget = resolveBudget({
|
|
19
|
-
callArg: opts.maxTokens,
|
|
20
|
-
envValue: process.env.WYRM_CONTEXT_TOKEN_BUDGET,
|
|
21
|
-
clientName: null, // clientInfo wiring is a follow-up; env override is the lever today
|
|
22
|
-
});
|
|
23
|
-
const seen = opts.sessionId ? sessionSeen().getSeen(opts.sessionId) : new Set();
|
|
24
|
-
// --- Stable preamble (ground truths) ---
|
|
25
|
-
const truthsText = groundTruths.formatForContext(opts.project.id);
|
|
26
|
-
const truthsTokens = tokenEstimator.count(truthsText ?? '');
|
|
27
|
-
// Strict budget mode (D3) elides truths if even the preamble overflows;
|
|
28
|
-
// default behavior is to warn-and-overflow per spec 014.
|
|
29
|
-
let preambleText = truthsText ?? '';
|
|
30
|
-
let preambleEmitted = truthsTokens;
|
|
31
|
-
if (truthsTokens > budget * 0.5 && !opts.strictBudget) {
|
|
32
|
-
process.stderr.write(`[wyrm_context_build] ground truths consume ${truthsTokens} of ${budget}-token budget — body will be heavily elided. Pass strict_budget:true to elide truths too.\n`);
|
|
33
|
-
}
|
|
34
|
-
if (opts.strictBudget && truthsTokens > budget * 0.5) {
|
|
35
|
-
// Strict mode: keep truths brief
|
|
36
|
-
preambleText = (truthsText ?? '').slice(0, budget * 2); // 2 chars/token rough cap on chars
|
|
37
|
-
preambleEmitted = tokenEstimator.count(preambleText);
|
|
38
|
-
}
|
|
39
|
-
// --- Build candidate body items: best scaffold + memory artifacts ---
|
|
40
|
-
const items = [];
|
|
41
|
-
// Scaffold (treated as a single candidate)
|
|
42
|
-
const scaffoldMatch = scaffoldLib.findBest(opts.task, opts.project.id);
|
|
43
|
-
if (scaffoldMatch) {
|
|
44
|
-
const sText = scaffoldLib.formatForContext(scaffoldMatch);
|
|
45
|
-
const sId = scaffoldMatch.scaffold.id;
|
|
46
|
-
items.push({
|
|
47
|
-
item: { kind: 'scaffold', id: sId, title: scaffoldMatch.scaffold.problem_type, body: sText },
|
|
48
|
-
key: `scaffold:${sId}`,
|
|
49
|
-
inlineCost: tokenEstimator.count(sText),
|
|
50
|
-
stubCost: 20,
|
|
51
|
-
// Scaffold is high-relevance by definition; assign generous score.
|
|
52
|
-
score: rankScore({ confidence: 0.9, relevance: 0.9, updatedAt: new Date().toISOString() }, rankWeights),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Memory artifacts via recall (which returns id-level results with relevance scores)
|
|
56
|
-
const recalled = memory.recall(opts.project.id, opts.task, {
|
|
57
|
-
limit: 20,
|
|
58
|
-
minConfidence: opts.minConfidence ?? 0.2,
|
|
59
|
-
});
|
|
60
|
-
for (const r of recalled) {
|
|
61
|
-
const a = r.artifact;
|
|
62
|
-
// Skip kind filter mismatches if caller specified one
|
|
63
|
-
if (opts.kinds && opts.kinds.length > 0 && !opts.kinds.includes(a.kind))
|
|
64
|
-
continue;
|
|
65
|
-
const body = [
|
|
66
|
-
`**${a.kind.toUpperCase()}** · ${a.problem}`,
|
|
67
|
-
a.constraints ? `Constraints: ${a.constraints}` : '',
|
|
68
|
-
a.validated_fix ? `Fix: ${a.validated_fix}` : '',
|
|
69
|
-
a.why_it_worked ? `Why: ${a.why_it_worked}` : '',
|
|
70
|
-
].filter(Boolean).join('\n');
|
|
71
|
-
items.push({
|
|
72
|
-
item: { kind: 'memory', id: a.id, title: a.problem.slice(0, 60), body },
|
|
73
|
-
key: `memory:${a.id}`,
|
|
74
|
-
inlineCost: tokenEstimator.count(body),
|
|
75
|
-
stubCost: 25,
|
|
76
|
-
score: rankScore({
|
|
77
|
-
confidence: a.confidence,
|
|
78
|
-
relevance: r.relevance_score,
|
|
79
|
-
updatedAt: a.updated_at,
|
|
80
|
-
}, rankWeights),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// --- Apply budget ---
|
|
84
|
-
const result = applyBudget(items, {
|
|
85
|
-
budget,
|
|
86
|
-
alreadySeen: seen,
|
|
87
|
-
reserved: opts.strictBudget ? 0 : preambleEmitted,
|
|
88
|
-
});
|
|
89
|
-
// --- Mark inlined items as seen for this session ---
|
|
90
|
-
if (opts.sessionId) {
|
|
91
|
-
sessionSeen().markBulk(opts.sessionId, result.inline.map((it) => ({ id: it.id, kind: it.kind, mode: 'inline' })));
|
|
92
|
-
sessionSeen().markBulk(opts.sessionId, result.elided.map((e) => ({ id: e.item.id, kind: e.item.kind, mode: 'stub' })));
|
|
93
|
-
}
|
|
94
|
-
// --- Render output ---
|
|
95
|
-
const lines = [];
|
|
96
|
-
lines.push(` **Context Brief** — "${opts.task}"`);
|
|
97
|
-
lines.push('');
|
|
98
|
-
lines.push('## Ground truths (stable preamble)');
|
|
99
|
-
lines.push('');
|
|
100
|
-
lines.push(preambleText || '_no ground truths set for this project yet_');
|
|
101
|
-
lines.push('');
|
|
102
|
-
lines.push('## Task-relevant memory');
|
|
103
|
-
lines.push('');
|
|
104
|
-
for (const it of result.inline) {
|
|
105
|
-
lines.push(`### [${it.kind}:${it.id}] ${it.title}`);
|
|
106
|
-
lines.push(it.body);
|
|
107
|
-
lines.push('');
|
|
108
|
-
}
|
|
109
|
-
if (result.elided.length > 0) {
|
|
110
|
-
lines.push('## Elided to stubs');
|
|
111
|
-
lines.push('');
|
|
112
|
-
lines.push('_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_');
|
|
113
|
-
lines.push('');
|
|
114
|
-
for (const e of result.elided) {
|
|
115
|
-
const tag = e.reason === 'seen' ? 'shown earlier in session' : `~${e.stubCost} tokens`;
|
|
116
|
-
lines.push(`- [${e.item.kind}:${e.item.id}] ${e.item.title} · ${tag}`);
|
|
117
|
-
}
|
|
118
|
-
lines.push('');
|
|
119
|
-
}
|
|
120
|
-
lines.push('---');
|
|
121
|
-
lines.push(`_Budget: ${result.tokensInline}/${budget} tokens inline · ${result.elided.length} stub${result.elided.length === 1 ? '' : 's'} · estimator: ${tokenEstimator.source}_`);
|
|
122
|
-
const text = lines.join('\n');
|
|
123
|
-
// Telemetry — fire-and-forget via the existing tool_call_log surface.
|
|
124
|
-
try {
|
|
125
|
-
toolAnalytics.log({
|
|
126
|
-
tool_name: 'wyrm_context_build',
|
|
127
|
-
project_id: opts.project.id,
|
|
128
|
-
args: {
|
|
129
|
-
budget,
|
|
130
|
-
items_total: items.length,
|
|
131
|
-
items_inline: result.inline.length,
|
|
132
|
-
items_elided: result.elided.length,
|
|
133
|
-
tokens_inline: result.tokensInline,
|
|
134
|
-
tokens_stubs: result.tokensStubs,
|
|
135
|
-
},
|
|
136
|
-
success: true,
|
|
137
|
-
latency_ms: 0,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
catch { /* never fail context_build on telemetry */ }
|
|
141
|
-
return { content: [{ type: 'text', text }] };
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
//# sourceMappingURL=context-build-budgeted.js.map
|
|
1
|
+
import{applyBudget as w,resolveBudget as v}from"./token-budget.js";import{score as p}from"./context-ranking.js";function S(g){const{groundTruths:b,scaffoldLib:h,memory:y,toolAnalytics:$,sessionSeen:u,tokenEstimator:d,rankWeights:f}=g;return function(t){const o=v({callArg:t.maxTokens,envValue:process.env.WYRM_CONTEXT_TOKEN_BUDGET,clientName:null}),x=t.sessionId?u().getSeen(t.sessionId):new Set,a=b.formatForContext(t.project.id),r=d.count(a??"");let m=a??"",k=r;r>o*.5&&!t.strictBudget&&process.stderr.write(`[wyrm_context_build] ground truths consume ${r} of ${o}-token budget \u2014 body will be heavily elided. Pass strict_budget:true to elide truths too.
|
|
2
|
+
`),t.strictBudget&&r>o*.5&&(m=(a??"").slice(0,o*2),k=d.count(m));const l=[],c=h.findBest(t.task,t.project.id);if(c){const e=h.formatForContext(c),i=c.scaffold.id;l.push({item:{kind:"scaffold",id:i,title:c.scaffold.problem_type,body:e},key:`scaffold:${i}`,inlineCost:d.count(e),stubCost:20,score:p({confidence:.9,relevance:.9,updatedAt:new Date().toISOString()},f)})}const B=y.recall(t.project.id,t.task,{limit:20,minConfidence:t.minConfidence??.2});for(const e of B){const i=e.artifact;if(t.kinds&&t.kinds.length>0&&!t.kinds.includes(i.kind))continue;const _=[`**${i.kind.toUpperCase()}** \xB7 ${i.problem}`,i.constraints?`Constraints: ${i.constraints}`:"",i.validated_fix?`Fix: ${i.validated_fix}`:"",i.why_it_worked?`Why: ${i.why_it_worked}`:""].filter(Boolean).join(`
|
|
3
|
+
`);l.push({item:{kind:"memory",id:i.id,title:i.problem.slice(0,60),body:_},key:`memory:${i.id}`,inlineCost:d.count(_),stubCost:25,score:p({confidence:i.confidence,relevance:e.relevance_score,updatedAt:i.updated_at},f)})}const s=w(l,{budget:o,alreadySeen:x,reserved:t.strictBudget?0:k});t.sessionId&&(u().markBulk(t.sessionId,s.inline.map(e=>({id:e.id,kind:e.kind,mode:"inline"}))),u().markBulk(t.sessionId,s.elided.map(e=>({id:e.item.id,kind:e.item.kind,mode:"stub"}))));const n=[];n.push(`\u{F115D} **Context Brief** \u2014 "${t.task}"`),n.push(""),n.push("## Ground truths (stable preamble)"),n.push(""),n.push(m||"_no ground truths set for this project yet_"),n.push(""),n.push("## Task-relevant memory"),n.push("");for(const e of s.inline)n.push(`### [${e.kind}:${e.id}] ${e.title}`),n.push(e.body),n.push("");if(s.elided.length>0){n.push("## Elided to stubs"),n.push(""),n.push("_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_"),n.push("");for(const e of s.elided){const i=e.reason==="seen"?"shown earlier in session":`~${e.stubCost} tokens`;n.push(`- [${e.item.kind}:${e.item.id}] ${e.item.title} \xB7 ${i}`)}n.push("")}n.push("---"),n.push(`_Budget: ${s.tokensInline}/${o} tokens inline \xB7 ${s.elided.length} stub${s.elided.length===1?"":"s"} \xB7 estimator: ${d.source}_`);const C=n.join(`
|
|
4
|
+
`);try{$.log({tool_name:"wyrm_context_build",project_id:t.project.id,args:{budget:o,items_total:l.length,items_inline:s.inline.length,items_elided:s.elided.length,tokens_inline:s.tokensInline,tokens_stubs:s.tokensStubs},success:!0,latency_ms:0})}catch{}return{content:[{type:"text",text:C}]}}}export{S as makeRunBudgetedContextBuild};
|
package/dist/context-ranking.js
CHANGED
|
@@ -1,69 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Combined-score computation for context-build candidates (spec 014).
|
|
3
|
-
*
|
|
4
|
-
* Combines four signals into a single score in [0, 1]:
|
|
5
|
-
*
|
|
6
|
-
* confidence × wC + recency × wR + relevance × wU + usefulness × wU
|
|
7
|
-
*
|
|
8
|
-
* Weights load from WYRM_RANK_WEIGHTS env (JSON) with sane defaults.
|
|
9
|
-
*
|
|
10
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
11
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
12
|
-
*/
|
|
13
|
-
import { logger } from './logger.js';
|
|
14
|
-
export const DEFAULT_WEIGHTS = {
|
|
15
|
-
confidence: 0.4,
|
|
16
|
-
recency: 0.2,
|
|
17
|
-
relevance: 0.3,
|
|
18
|
-
usefulness: 0.1,
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Load weights from WYRM_RANK_WEIGHTS env var. Falls back to defaults
|
|
22
|
-
* on any parse/validation error. Logs the active weights at startup.
|
|
23
|
-
*/
|
|
24
|
-
export function loadWeightsFromEnv(env = process.env) {
|
|
25
|
-
const raw = env.WYRM_RANK_WEIGHTS;
|
|
26
|
-
if (!raw)
|
|
27
|
-
return { ...DEFAULT_WEIGHTS };
|
|
28
|
-
try {
|
|
29
|
-
const parsed = JSON.parse(raw);
|
|
30
|
-
const w = {
|
|
31
|
-
confidence: clamp01(parsed.confidence ?? DEFAULT_WEIGHTS.confidence),
|
|
32
|
-
recency: clamp01(parsed.recency ?? DEFAULT_WEIGHTS.recency),
|
|
33
|
-
relevance: clamp01(parsed.relevance ?? DEFAULT_WEIGHTS.relevance),
|
|
34
|
-
usefulness: clamp01(parsed.usefulness ?? DEFAULT_WEIGHTS.usefulness),
|
|
35
|
-
};
|
|
36
|
-
return w;
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
logger.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${e.message}`);
|
|
40
|
-
return { ...DEFAULT_WEIGHTS };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function clamp01(x) {
|
|
44
|
-
if (!Number.isFinite(x))
|
|
45
|
-
return 0;
|
|
46
|
-
return Math.max(0, Math.min(1, x));
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Compute the combined score in [0, 1].
|
|
50
|
-
*
|
|
51
|
-
* Recency uses exponential decay over 30 days from updatedAt to now.
|
|
52
|
-
* Missing signals default to neutral (0.5 confidence, 0.5 relevance, 0.5
|
|
53
|
-
* usefulness; recency to 0 if no updatedAt).
|
|
54
|
-
*/
|
|
55
|
-
export function score(item, weights, now = new Date()) {
|
|
56
|
-
const c = clamp01(item.confidence ?? 0.5);
|
|
57
|
-
const u = clamp01(item.usefulness ?? 0.5);
|
|
58
|
-
const r = clamp01(item.relevance ?? 0.5);
|
|
59
|
-
const ageDays = item.updatedAt
|
|
60
|
-
? Math.max(0, (now.getTime() - new Date(item.updatedAt).getTime()) / (24 * 60 * 60 * 1000))
|
|
61
|
-
: Infinity;
|
|
62
|
-
const recency = Number.isFinite(ageDays) ? Math.exp(-ageDays / 30) : 0;
|
|
63
|
-
const total = c * weights.confidence +
|
|
64
|
-
recency * weights.recency +
|
|
65
|
-
r * weights.relevance +
|
|
66
|
-
u * weights.usefulness;
|
|
67
|
-
return clamp01(total);
|
|
68
|
-
}
|
|
69
|
-
//# sourceMappingURL=context-ranking.js.map
|
|
1
|
+
import{logger as l}from"./logger.js";const s={confidence:.4,recency:.2,relevance:.3,usefulness:.1};function p(e=process.env){const r=e.WYRM_RANK_WEIGHTS;if(!r)return{...s};try{const n=JSON.parse(r);return{confidence:c(n.confidence??s.confidence),recency:c(n.recency??s.recency),relevance:c(n.relevance??s.relevance),usefulness:c(n.usefulness??s.usefulness)}}catch(n){return l.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${n.message}`),{...s}}}function c(e){return Number.isFinite(e)?Math.max(0,Math.min(1,e)):0}function v(e,r,n=new Date){const t=c(e.confidence??.5),a=c(e.usefulness??.5),u=c(e.relevance??.5),o=e.updatedAt?Math.max(0,(n.getTime()-new Date(e.updatedAt).getTime())/(1440*60*1e3)):1/0,f=Number.isFinite(o)?Math.exp(-o/30):0,i=t*r.confidence+f*r.recency+u*r.relevance+a*r.usefulness;return c(i)}export{s as DEFAULT_WEIGHTS,p as loadWeightsFromEnv,v as score};
|
package/dist/crypto.js
CHANGED
|
@@ -1,179 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Wyrm Encryption Module
|
|
3
|
-
* AES-256-GCM encryption for sensitive memory data
|
|
4
|
-
*
|
|
5
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
6
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
7
|
-
* @module crypto
|
|
8
|
-
* @version 3.0.0
|
|
9
|
-
*/
|
|
10
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from 'crypto';
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import { homedir } from 'os';
|
|
14
|
-
const WYRM_DIR = join(homedir(), '.wyrm');
|
|
15
|
-
const MASTER_SALT_FILE = join(WYRM_DIR, 'crypto.salt');
|
|
16
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
17
|
-
const IV_LENGTH = 16;
|
|
18
|
-
const SALT_LENGTH = 32;
|
|
19
|
-
const TAG_LENGTH = 16;
|
|
20
|
-
const KEY_LENGTH = 32;
|
|
21
|
-
/**
|
|
22
|
-
* Wyrm Crypto - Handles encryption/decryption of sensitive data
|
|
23
|
-
*/
|
|
24
|
-
export class WyrmCrypto {
|
|
25
|
-
masterKey = null;
|
|
26
|
-
config;
|
|
27
|
-
constructor(config) {
|
|
28
|
-
this.config = {
|
|
29
|
-
enabled: config?.enabled ?? false,
|
|
30
|
-
keyDerivation: config?.keyDerivation ?? 'scrypt',
|
|
31
|
-
iterations: config?.iterations ?? 100000,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Initialize crypto with a master password
|
|
36
|
-
*/
|
|
37
|
-
initialize(password) {
|
|
38
|
-
if (!password || password.length < 8) {
|
|
39
|
-
throw new Error('Password must be at least 8 characters');
|
|
40
|
-
}
|
|
41
|
-
// Use a per-installation random salt (persisted, never derived from constant)
|
|
42
|
-
if (!existsSync(WYRM_DIR)) {
|
|
43
|
-
mkdirSync(WYRM_DIR, { recursive: true, mode: 0o700 });
|
|
44
|
-
}
|
|
45
|
-
let salt;
|
|
46
|
-
if (existsSync(MASTER_SALT_FILE)) {
|
|
47
|
-
salt = Buffer.from(readFileSync(MASTER_SALT_FILE, 'utf8').trim(), 'hex');
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
salt = randomBytes(32);
|
|
51
|
-
writeFileSync(MASTER_SALT_FILE, salt.toString('hex'), { mode: 0o600 });
|
|
52
|
-
}
|
|
53
|
-
this.masterKey = scryptSync(password, salt, KEY_LENGTH);
|
|
54
|
-
this.config.enabled = true;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if encryption is enabled and configured
|
|
58
|
-
*/
|
|
59
|
-
isEnabled() {
|
|
60
|
-
return this.config.enabled && this.masterKey !== null;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Derive a unique key for each piece of data
|
|
64
|
-
*/
|
|
65
|
-
deriveKey(salt) {
|
|
66
|
-
if (!this.masterKey) {
|
|
67
|
-
throw new Error('Crypto not initialized. Call initialize() with a password first.');
|
|
68
|
-
}
|
|
69
|
-
return scryptSync(this.masterKey, salt, KEY_LENGTH);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Encrypt a string value
|
|
73
|
-
*/
|
|
74
|
-
encrypt(plaintext) {
|
|
75
|
-
if (!this.isEnabled()) {
|
|
76
|
-
throw new Error('Encryption not enabled');
|
|
77
|
-
}
|
|
78
|
-
const salt = randomBytes(SALT_LENGTH);
|
|
79
|
-
const iv = randomBytes(IV_LENGTH);
|
|
80
|
-
const key = this.deriveKey(salt);
|
|
81
|
-
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
82
|
-
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
83
|
-
encrypted += cipher.final('hex');
|
|
84
|
-
const tag = cipher.getAuthTag();
|
|
85
|
-
return {
|
|
86
|
-
iv: iv.toString('hex'),
|
|
87
|
-
salt: salt.toString('hex'),
|
|
88
|
-
tag: tag.toString('hex'),
|
|
89
|
-
data: encrypted,
|
|
90
|
-
version: 1,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Decrypt an encrypted value
|
|
95
|
-
*/
|
|
96
|
-
decrypt(encrypted) {
|
|
97
|
-
if (!this.isEnabled()) {
|
|
98
|
-
throw new Error('Encryption not enabled');
|
|
99
|
-
}
|
|
100
|
-
const salt = Buffer.from(encrypted.salt, 'hex');
|
|
101
|
-
const iv = Buffer.from(encrypted.iv, 'hex');
|
|
102
|
-
const tag = Buffer.from(encrypted.tag, 'hex');
|
|
103
|
-
const key = this.deriveKey(salt);
|
|
104
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
105
|
-
decipher.setAuthTag(tag);
|
|
106
|
-
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
|
|
107
|
-
decrypted += decipher.final('utf8');
|
|
108
|
-
return decrypted;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Encrypt if enabled, otherwise return plaintext
|
|
112
|
-
*/
|
|
113
|
-
maybeEncrypt(value) {
|
|
114
|
-
if (!this.isEnabled()) {
|
|
115
|
-
return value;
|
|
116
|
-
}
|
|
117
|
-
const encrypted = this.encrypt(value);
|
|
118
|
-
return `ENC:${JSON.stringify(encrypted)}`;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Decrypt if encrypted, otherwise return as-is
|
|
122
|
-
*/
|
|
123
|
-
maybeDecrypt(value) {
|
|
124
|
-
if (!value.startsWith('ENC:')) {
|
|
125
|
-
return value;
|
|
126
|
-
}
|
|
127
|
-
if (!this.isEnabled()) {
|
|
128
|
-
// Can't decrypt without key
|
|
129
|
-
return '[ENCRYPTED - KEY REQUIRED]';
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
const encrypted = JSON.parse(value.slice(4));
|
|
133
|
-
return this.decrypt(encrypted);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return '[DECRYPT FAILED]';
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Generate a secure random key for API tokens, etc.
|
|
141
|
-
*/
|
|
142
|
-
static generateSecureToken(length = 32) {
|
|
143
|
-
return randomBytes(length).toString('hex');
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Hash a value for comparison (one-way)
|
|
147
|
-
*/
|
|
148
|
-
static hash(value, algorithm = 'sha256') {
|
|
149
|
-
return createHash(algorithm).update(value).digest('hex');
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Verify a hash
|
|
153
|
-
*/
|
|
154
|
-
static verifyHash(value, hash, algorithm = 'sha256') {
|
|
155
|
-
const computed = WyrmCrypto.hash(value, algorithm);
|
|
156
|
-
// Constant-time comparison
|
|
157
|
-
if (computed.length !== hash.length)
|
|
158
|
-
return false;
|
|
159
|
-
let result = 0;
|
|
160
|
-
for (let i = 0; i < computed.length; i++) {
|
|
161
|
-
result |= computed.charCodeAt(i) ^ hash.charCodeAt(i);
|
|
162
|
-
}
|
|
163
|
-
return result === 0;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Singleton instance
|
|
167
|
-
let cryptoInstance = null;
|
|
168
|
-
export function getCrypto() {
|
|
169
|
-
if (!cryptoInstance) {
|
|
170
|
-
cryptoInstance = new WyrmCrypto();
|
|
171
|
-
}
|
|
172
|
-
return cryptoInstance;
|
|
173
|
-
}
|
|
174
|
-
export function initializeCrypto(password) {
|
|
175
|
-
cryptoInstance = new WyrmCrypto({ enabled: true });
|
|
176
|
-
cryptoInstance.initialize(password);
|
|
177
|
-
return cryptoInstance;
|
|
178
|
-
}
|
|
179
|
-
//# sourceMappingURL=crypto.js.map
|
|
1
|
+
import{createCipheriv as g,createDecipheriv as x,randomBytes as c,scryptSync as u,createHash as S}from"crypto";import{existsSync as y,readFileSync as b,writeFileSync as T,mkdirSync as w}from"fs";import{join as d}from"path";import{homedir as v}from"os";const l=d(v(),".wyrm"),f=d(l,"crypto.salt"),p="aes-256-gcm",C=16,A=32,R=16,m=32;class h{masterKey=null;config;constructor(t){this.config={enabled:t?.enabled??!1,keyDerivation:t?.keyDerivation??"scrypt",iterations:t?.iterations??1e5}}initialize(t){if(!t||t.length<8)throw new Error("Password must be at least 8 characters");y(l)||w(l,{recursive:!0,mode:448});let e;y(f)?e=Buffer.from(b(f,"utf8").trim(),"hex"):(e=c(32),T(f,e.toString("hex"),{mode:384})),this.masterKey=u(t,e,m),this.config.enabled=!0}isEnabled(){return this.config.enabled&&this.masterKey!==null}deriveKey(t){if(!this.masterKey)throw new Error("Crypto not initialized. Call initialize() with a password first.");return u(this.masterKey,t,m)}encrypt(t){if(!this.isEnabled())throw new Error("Encryption not enabled");const e=c(A),s=c(C),n=this.deriveKey(e),i=g(p,n,s);let r=i.update(t,"utf8","hex");r+=i.final("hex");const a=i.getAuthTag();return{iv:s.toString("hex"),salt:e.toString("hex"),tag:a.toString("hex"),data:r,version:1}}decrypt(t){if(!this.isEnabled())throw new Error("Encryption not enabled");const e=Buffer.from(t.salt,"hex"),s=Buffer.from(t.iv,"hex"),n=Buffer.from(t.tag,"hex"),i=this.deriveKey(e),r=x(p,i,s);r.setAuthTag(n);let a=r.update(t.data,"hex","utf8");return a+=r.final("utf8"),a}maybeEncrypt(t){if(!this.isEnabled())return t;const e=this.encrypt(t);return`ENC:${JSON.stringify(e)}`}maybeDecrypt(t){if(!t.startsWith("ENC:"))return t;if(!this.isEnabled())return"[ENCRYPTED - KEY REQUIRED]";try{const e=JSON.parse(t.slice(4));return this.decrypt(e)}catch{return"[DECRYPT FAILED]"}}static generateSecureToken(t=32){return c(t).toString("hex")}static hash(t,e="sha256"){return S(e).update(t).digest("hex")}static verifyHash(t,e,s="sha256"){const n=h.hash(t,s);if(n.length!==e.length)return!1;let i=0;for(let r=0;r<n.length;r++)i|=n.charCodeAt(r)^e.charCodeAt(r);return i===0}}let o=null;function H(){return o||(o=new h),o}function I(E){return o=new h({enabled:!0}),o.initialize(E),o}export{h as WyrmCrypto,H as getCrypto,I as initializeCrypto};
|