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/render-target.js
CHANGED
|
@@ -1,72 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* Wyrm Render Target (v7 F4 — T038) — the deterministic DB→memory-file compiler.
|
|
3
|
-
*
|
|
4
|
-
* `wyrm render` is the ZERO-MCP-TOKEN path for casual sessions: instead of an
|
|
5
|
-
* agent spending tool calls to load context, Wyrm compiles the project's
|
|
6
|
-
* authoritative state (ground truths, open quests, validated patterns,
|
|
7
|
-
* unresolved failures) straight into the harness-native memory slot —
|
|
8
|
-
* `MEMORY.md` (hard 200-line budget) + per-topic files + a lean SessionStart
|
|
9
|
-
* brief — plus per-client adapters (Claude Code / Cursor / Copilot / AGENTS.md).
|
|
10
|
-
*
|
|
11
|
-
* Design law (this module is the NET-NEW, TEMPLATE-ISOLATED writer per the spec):
|
|
12
|
-
* - DETERMINISTIC / BYTE-STABLE: there is NO Date.now()/Math.random() in the
|
|
13
|
-
* OUTPUT. Every volatile value (the Wyrm version, the compiled-at stamp, the
|
|
14
|
-
* artifact count) is PASSED IN via {@link RenderStamp}. Same model + same
|
|
15
|
-
* stamp ⇒ byte-identical bytes, forever (golden-replayable).
|
|
16
|
-
* - PROVENANCE-STAMPED: every emitted file carries a header that says it was
|
|
17
|
-
* compiled by Wyrm and that edits are HARVESTED, not lost (the reverse bridge,
|
|
18
|
-
* T039, picks them up). Wyrm-managed regions are MARKER-BOUNDED so a writer
|
|
19
|
-
* NEVER clobbers operator prose outside the markers (the autoconfig.ts
|
|
20
|
-
* idempotent-block discipline, reused here).
|
|
21
|
-
* - SAFE (Article VII): {@link writeRenderTarget} refuses to escape its target
|
|
22
|
-
* directory (no `..`, no absolute re-roots) and refuses to overwrite a file
|
|
23
|
-
* that is NOT Wyrm-managed unless explicitly forced — a human's hand-written
|
|
24
|
-
* MEMORY.md is never silently destroyed.
|
|
25
|
-
* - OFFLINE (Article III): pure DB read + string building. No network, no LLM,
|
|
26
|
-
* no clock of its own.
|
|
27
|
-
*
|
|
28
|
-
* The DB read is injected ({@link RenderDeps}) so the COMPILER is unit-testable
|
|
29
|
-
* against an in-memory fixture, exactly like harvest.ts.
|
|
30
|
-
*
|
|
31
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
32
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
33
|
-
*/
|
|
34
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'fs';
|
|
35
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
36
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
37
|
-
// Budgets & markers
|
|
38
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
39
|
-
/** MEMORY.md hard line budget (spec FR-6). The compiler trims to fit. */
|
|
40
|
-
export const MEMORY_MD_LINE_BUDGET = 200;
|
|
41
|
-
/** Marker delimiting the Wyrm-managed region inside any rendered file. */
|
|
42
|
-
export const RENDER_MARKER_START = '<!-- wyrm:render:start -->';
|
|
43
|
-
export const RENDER_MARKER_END = '<!-- wyrm:render:end -->';
|
|
44
|
-
/**
|
|
45
|
-
* Default DB-backed deps. Pure SQL, bound params, deterministic ORDER BY on a
|
|
46
|
-
* TOTAL order (every query appends an id/key tiebreak so two rows never tie on
|
|
47
|
-
* the wire — the bare-sort-key trap). All listings are hard-capped.
|
|
48
|
-
*/
|
|
49
|
-
export function makeRenderDeps(db) {
|
|
50
|
-
return {
|
|
51
|
-
truths(projectId) {
|
|
52
|
-
const rows = db.prepare(`
|
|
1
|
+
import{existsSync as E,lstatSync as T,mkdirSync as k,readFileSync as D,realpathSync as N,writeFileSync as O}from"fs";import{dirname as _,isAbsolute as $,join as f,relative as w,resolve as R,sep as S}from"path";const W=200,m="<!-- wyrm:render:start -->",h="<!-- wyrm:render:end -->";function P(e){return{truths(r){return e.prepare(`
|
|
53
2
|
SELECT category, key, value, rationale, confidence, ttl_days,
|
|
54
3
|
CAST((julianday('now') - julianday(last_verified_at)) AS REAL) AS age_days
|
|
55
4
|
FROM ground_truths
|
|
56
5
|
WHERE project_id = ? AND is_current = 1
|
|
57
6
|
ORDER BY category, key
|
|
58
|
-
`).all(
|
|
59
|
-
return rows.map((r) => ({
|
|
60
|
-
category: r.category,
|
|
61
|
-
key: r.key,
|
|
62
|
-
value: r.value,
|
|
63
|
-
rationale: r.rationale,
|
|
64
|
-
confidence: r.confidence,
|
|
65
|
-
stale: r.ttl_days != null && r.age_days > r.ttl_days,
|
|
66
|
-
}));
|
|
67
|
-
},
|
|
68
|
-
quests(projectId) {
|
|
69
|
-
return db.prepare(`
|
|
7
|
+
`).all(r).map(t=>({category:t.category,key:t.key,value:t.value,rationale:t.rationale,confidence:t.confidence,stale:t.ttl_days!=null&&t.age_days>t.ttl_days}))},quests(r){return e.prepare(`
|
|
70
8
|
SELECT id, title, description, priority
|
|
71
9
|
FROM quests
|
|
72
10
|
WHERE project_id = ? AND status IN ('pending','in_progress')
|
|
@@ -75,461 +13,30 @@ export function makeRenderDeps(db) {
|
|
|
75
13
|
WHEN 'medium' THEN 2 ELSE 3 END,
|
|
76
14
|
id ASC
|
|
77
15
|
LIMIT 100
|
|
78
|
-
`).all(
|
|
79
|
-
},
|
|
80
|
-
artifacts(projectId) {
|
|
81
|
-
try {
|
|
82
|
-
return db.prepare(`
|
|
16
|
+
`).all(r)},artifacts(r){try{return e.prepare(`
|
|
83
17
|
SELECT id, problem, validated_fix
|
|
84
18
|
FROM memory_artifacts
|
|
85
19
|
WHERE project_id = ? AND (needs_review = 0 OR needs_review IS NULL)
|
|
86
20
|
ORDER BY id DESC LIMIT 50
|
|
87
|
-
`).all(
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
failures(projectId) {
|
|
94
|
-
try {
|
|
95
|
-
return db.prepare(`
|
|
21
|
+
`).all(r)}catch{return[]}},failures(r){try{return e.prepare(`
|
|
96
22
|
SELECT id, scope, target, description, severity, occurrences
|
|
97
23
|
FROM failure_patterns
|
|
98
24
|
WHERE (project_id = ? OR project_id IS NULL) AND resolved = 0
|
|
99
25
|
ORDER BY last_seen DESC, id DESC LIMIT 50
|
|
100
|
-
`).all(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
failures: deps.failures(project.id),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// Provenance
|
|
122
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
123
|
-
function artifactCount(m) {
|
|
124
|
-
return m.truths.length + m.quests.length + m.artifacts.length + m.failures.length;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* The provenance lines. Deterministic given (model, stamp). States plainly that
|
|
128
|
-
* edits are HARVESTED, not lost (the reverse bridge picks up changes between the
|
|
129
|
-
* markers and routes them to the review queue, T039).
|
|
130
|
-
*/
|
|
131
|
-
export function provenanceHeader(m, stamp) {
|
|
132
|
-
const n = artifactCount(m);
|
|
133
|
-
return [
|
|
134
|
-
`<!-- Compiled by Wyrm v${stamp.wyrm_version} from ${n} memory artifact${n === 1 ? '' : 's'}.`,
|
|
135
|
-
` Project: ${m.project_name}. Compiled at ${stamp.compiled_at}.`,
|
|
136
|
-
` Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost —`,
|
|
137
|
-
` but they are OVERWRITTEN on the next \`wyrm render\`; capture durable notes with \`wyrm capture\`. -->`,
|
|
138
|
-
];
|
|
139
|
-
}
|
|
140
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
141
|
-
// MEMORY.md — the 200-line-budgeted master file.
|
|
142
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
143
|
-
/** Sanitise a one-line cell: collapse newlines so a value never spends >1 line. */
|
|
144
|
-
function oneLine(s) {
|
|
145
|
-
return s.replace(/\s*\n\s*/g, ' ').trim();
|
|
146
|
-
}
|
|
147
|
-
/** Truncate to a max length with an ellipsis (deterministic). */
|
|
148
|
-
function clip(s, max) {
|
|
149
|
-
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Render MEMORY.md body (the lines BETWEEN the markers, provenance included).
|
|
153
|
-
* Hard 200-line budget: sections are emitted in priority order and the LAST
|
|
154
|
-
* section to overflow is truncated with an explicit "N more" note, so the file
|
|
155
|
-
* never exceeds the budget no matter how large the corpus.
|
|
156
|
-
*
|
|
157
|
-
* Returns the full marker-bounded block (markers + provenance + body), ready to
|
|
158
|
-
* splice into a file by {@link spliceWyrmRegion}.
|
|
159
|
-
*/
|
|
160
|
-
export function renderMemoryMd(m, stamp) {
|
|
161
|
-
const header = provenanceHeader(m, stamp);
|
|
162
|
-
// The marker + provenance lines count against NOTHING extra here; the budget
|
|
163
|
-
// governs the human-readable body. We reserve a few lines for the title and
|
|
164
|
-
// the footer so the total stays at/under the budget.
|
|
165
|
-
const body = [];
|
|
166
|
-
body.push(`# ${m.project_name} — Project Memory`);
|
|
167
|
-
body.push('');
|
|
168
|
-
// Footer is always 2 lines; title+blank is 2 lines. Keep the content within
|
|
169
|
-
// the remaining budget.
|
|
170
|
-
const reserved = 4;
|
|
171
|
-
const contentBudget = MEMORY_MD_LINE_BUDGET - reserved;
|
|
172
|
-
const sections = [];
|
|
173
|
-
if (m.truths.length > 0) {
|
|
174
|
-
sections.push({
|
|
175
|
-
heading: `## Ground truths (${m.truths.length})`,
|
|
176
|
-
refTool: 'wyrm_truth_get',
|
|
177
|
-
lines: m.truths.map((t) => {
|
|
178
|
-
const marker = t.stale ? ' [STALE]' : '';
|
|
179
|
-
const conf = t.confidence < 1 ? ` (conf ${t.confidence})` : '';
|
|
180
|
-
return `- **${t.category}.${t.key}**${marker}: ${clip(oneLine(t.value), 160)}${conf}`;
|
|
181
|
-
}),
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
if (m.failures.length > 0) {
|
|
185
|
-
sections.push({
|
|
186
|
-
heading: `## Failures — DO NOT repeat (${m.failures.length})`,
|
|
187
|
-
refTool: 'wyrm_failure_check',
|
|
188
|
-
lines: m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences} — ${clip(oneLine(f.description), 140)}`),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
if (m.quests.length > 0) {
|
|
192
|
-
sections.push({
|
|
193
|
-
heading: `## Open quests (${m.quests.length})`,
|
|
194
|
-
refTool: 'wyrm_quest',
|
|
195
|
-
lines: m.quests.map((q) => `- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 140)}`),
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
if (m.artifacts.length > 0) {
|
|
199
|
-
sections.push({
|
|
200
|
-
heading: `## Validated patterns (${m.artifacts.length})`,
|
|
201
|
-
refTool: 'wyrm_recall',
|
|
202
|
-
lines: m.artifacts.map((a) => `- ${clip(oneLine(a.problem), 120)}${a.validated_fix ? ` → ${clip(oneLine(a.validated_fix), 120)}` : ''}`),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
// Greedily emit sections within the budget. Each section costs: heading + a
|
|
206
|
-
// blank line + N item lines + a trailing blank. When a section would overflow,
|
|
207
|
-
// emit as many items as fit and a single "N more — query <tool>" line.
|
|
208
|
-
let used = 0;
|
|
209
|
-
for (const s of sections) {
|
|
210
|
-
// Minimum cost to open a section: heading + blank + at least the "more" note
|
|
211
|
-
// + trailing blank = 4 lines. If we can't even do that, stop.
|
|
212
|
-
if (used + 4 > contentBudget)
|
|
213
|
-
break;
|
|
214
|
-
body.push(s.heading);
|
|
215
|
-
body.push('');
|
|
216
|
-
used += 2;
|
|
217
|
-
let emitted = 0;
|
|
218
|
-
for (const line of s.lines) {
|
|
219
|
-
// Reserve 1 line for a potential "more" note + 1 trailing blank.
|
|
220
|
-
if (used + 1 + (emitted < s.lines.length ? 1 : 0) + 1 > contentBudget)
|
|
221
|
-
break;
|
|
222
|
-
body.push(line);
|
|
223
|
-
used += 1;
|
|
224
|
-
emitted += 1;
|
|
225
|
-
}
|
|
226
|
-
if (emitted < s.lines.length) {
|
|
227
|
-
body.push(`- _… ${s.lines.length - emitted} more — query \`${s.refTool}\`._`);
|
|
228
|
-
used += 1;
|
|
229
|
-
}
|
|
230
|
-
body.push('');
|
|
231
|
-
used += 1;
|
|
232
|
-
}
|
|
233
|
-
body.push('---');
|
|
234
|
-
body.push(`*Full memory: \`wyrm recall\` / \`wyrm session prime\`. This file is a deterministic digest.*`);
|
|
235
|
-
return [...header, ...body].join('\n');
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Render the per-topic files (the full, un-budgeted corpus). Each is its own
|
|
239
|
-
* marker-bounded, provenance-stamped file. Deterministic given (model, stamp).
|
|
240
|
-
*/
|
|
241
|
-
export function renderTopicFiles(m, stamp) {
|
|
242
|
-
const header = provenanceHeader(m, stamp);
|
|
243
|
-
const out = [];
|
|
244
|
-
const mk = (filename, title, lines) => {
|
|
245
|
-
if (lines.length === 0)
|
|
246
|
-
return;
|
|
247
|
-
out.push({
|
|
248
|
-
filename,
|
|
249
|
-
content: [...header, `# ${m.project_name} — ${title}`, '', ...lines].join('\n'),
|
|
250
|
-
});
|
|
251
|
-
};
|
|
252
|
-
mk('truths.md', 'Ground truths', m.truths.map((t) => {
|
|
253
|
-
const marker = t.stale ? ' [STALE]' : '';
|
|
254
|
-
return `- **${t.category}.${t.key}**${marker}: ${oneLine(t.value)}` +
|
|
255
|
-
(t.rationale ? `\n - rationale: ${oneLine(t.rationale)}` : '') +
|
|
256
|
-
(t.confidence < 1 ? `\n - confidence: ${t.confidence}` : '');
|
|
257
|
-
}));
|
|
258
|
-
mk('quests.md', 'Open quests', m.quests.map((q) => `- #${q.id} [${q.priority}] ${oneLine(q.title)}` +
|
|
259
|
-
(q.description ? `\n - ${oneLine(q.description)}` : '')));
|
|
260
|
-
mk('patterns.md', 'Validated patterns', m.artifacts.map((a) => `- ${oneLine(a.problem)}` + (a.validated_fix ? `\n - fix: ${oneLine(a.validated_fix)}` : '')));
|
|
261
|
-
mk('failures.md', 'Failures — DO NOT repeat', m.failures.map((f) => `- [${f.severity}] ${f.scope}:${f.target} ×${f.occurrences}\n - ${oneLine(f.description)}`));
|
|
262
|
-
return out;
|
|
263
|
-
}
|
|
264
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
265
|
-
// SessionStart brief — the lean, byte-stable injection payload.
|
|
266
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
267
|
-
/**
|
|
268
|
-
* The SessionStart brief: a compact, deterministic summary a harness can inject
|
|
269
|
-
* at session start with near-zero cost. Caps each section so the brief stays
|
|
270
|
-
* lean (it is NOT the full corpus — that's the topic files). Byte-stable.
|
|
271
|
-
*/
|
|
272
|
-
export function renderSessionBrief(m, stamp) {
|
|
273
|
-
const lines = [];
|
|
274
|
-
lines.push(`Wyrm memory for ${m.project_name} (compiled by Wyrm v${stamp.wyrm_version}):`);
|
|
275
|
-
if (m.failures.length > 0) {
|
|
276
|
-
lines.push('');
|
|
277
|
-
lines.push(`Known failures (do not repeat):`);
|
|
278
|
-
for (const f of m.failures.slice(0, 5)) {
|
|
279
|
-
lines.push(`- ${f.scope}:${f.target} — ${clip(oneLine(f.description), 120)}`);
|
|
280
|
-
}
|
|
281
|
-
if (m.failures.length > 5)
|
|
282
|
-
lines.push(`- (+${m.failures.length - 5} more — wyrm_failure_check)`);
|
|
283
|
-
}
|
|
284
|
-
if (m.truths.length > 0) {
|
|
285
|
-
lines.push('');
|
|
286
|
-
lines.push(`Ground truths:`);
|
|
287
|
-
for (const t of m.truths.slice(0, 8)) {
|
|
288
|
-
lines.push(`- ${t.category}.${t.key}: ${clip(oneLine(t.value), 120)}`);
|
|
289
|
-
}
|
|
290
|
-
if (m.truths.length > 8)
|
|
291
|
-
lines.push(`- (+${m.truths.length - 8} more — wyrm_truth_get)`);
|
|
292
|
-
}
|
|
293
|
-
if (m.quests.length > 0) {
|
|
294
|
-
lines.push('');
|
|
295
|
-
lines.push(`Open quests:`);
|
|
296
|
-
for (const q of m.quests.slice(0, 5)) {
|
|
297
|
-
lines.push(`- #${q.id} [${q.priority}] ${clip(oneLine(q.title), 100)}`);
|
|
298
|
-
}
|
|
299
|
-
if (m.quests.length > 5)
|
|
300
|
-
lines.push(`- (+${m.quests.length - 5} more — wyrm_quest)`);
|
|
301
|
-
}
|
|
302
|
-
lines.push('');
|
|
303
|
-
lines.push(`Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)`);
|
|
304
|
-
return lines.join('\n');
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Per-client memory-file layout. Each adapter targets that client's native
|
|
308
|
-
* memory slot with its native filename — no Anthropic-only behaviour leaks into
|
|
309
|
-
* the others. The BLOCK content is identical (the MEMORY.md digest); only the
|
|
310
|
-
* destination differs, so the digest is one source of truth.
|
|
311
|
-
*/
|
|
312
|
-
export function renderForClient(client, m, stamp) {
|
|
313
|
-
const block = renderMemoryMd(m, stamp);
|
|
314
|
-
switch (client) {
|
|
315
|
-
case 'claude':
|
|
316
|
-
// Claude Code reads a project-root CLAUDE.md (or MEMORY.md). We target
|
|
317
|
-
// CLAUDE.md as the canonical Claude Code project memory slot.
|
|
318
|
-
return { client, relPath: 'CLAUDE.md', block };
|
|
319
|
-
case 'cursor':
|
|
320
|
-
return { client, relPath: join('.cursor', 'rules', 'wyrm-memory.md'), block };
|
|
321
|
-
case 'copilot':
|
|
322
|
-
return { client, relPath: join('.github', 'copilot-instructions.md'), block };
|
|
323
|
-
case 'agents':
|
|
324
|
-
return { client, relPath: 'AGENTS.md', block };
|
|
325
|
-
default: {
|
|
326
|
-
const never = client;
|
|
327
|
-
throw new Error(`unknown render client: ${String(never)}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
332
|
-
// Marker splicing + safe writing.
|
|
333
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
334
|
-
/**
|
|
335
|
-
* Splice a marker-bounded Wyrm block into existing file content WITHOUT touching
|
|
336
|
-
* operator prose outside the markers. If a marker region already exists it is
|
|
337
|
-
* replaced; otherwise the block is appended. Returns the new file content.
|
|
338
|
-
*
|
|
339
|
-
* The returned content wraps `block` in the start/end markers exactly once.
|
|
340
|
-
*/
|
|
341
|
-
export function spliceWyrmRegion(existing, block) {
|
|
342
|
-
const region = `${RENDER_MARKER_START}\n${block}\n${RENDER_MARKER_END}`;
|
|
343
|
-
if (!existing)
|
|
344
|
-
return `${region}\n`;
|
|
345
|
-
const startIdx = existing.indexOf(RENDER_MARKER_START);
|
|
346
|
-
const endIdx = existing.indexOf(RENDER_MARKER_END);
|
|
347
|
-
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
348
|
-
const before = existing.slice(0, startIdx);
|
|
349
|
-
const after = existing.slice(endIdx + RENDER_MARKER_END.length);
|
|
350
|
-
return `${before}${region}${after}`;
|
|
351
|
-
}
|
|
352
|
-
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
353
|
-
return `${existing}${sep}${region}\n`;
|
|
354
|
-
}
|
|
355
|
-
/** Does this file content already carry a Wyrm-managed region? */
|
|
356
|
-
export function hasWyrmRegion(content) {
|
|
357
|
-
const s = content.indexOf(RENDER_MARKER_START);
|
|
358
|
-
const e = content.indexOf(RENDER_MARKER_END);
|
|
359
|
-
return s !== -1 && e !== -1 && e > s;
|
|
360
|
-
}
|
|
361
|
-
export class RenderPathError extends Error {
|
|
362
|
-
}
|
|
363
|
-
const NODE_STAT_FS = { existsSync, lstatSync, realpathSync };
|
|
364
|
-
/**
|
|
365
|
-
* Resolve `relPath` strictly inside `rootDir`. Rejects absolute paths and any
|
|
366
|
-
* traversal that escapes the root (Article VII: a writer/watcher must never
|
|
367
|
-
* escape its target dir). Returns the absolute, validated path.
|
|
368
|
-
*
|
|
369
|
-
* The lexical `resolve`/`relative` gate blocks `..` traversal, but it does NOT
|
|
370
|
-
* stop a SYMLINK that lives inside the root and points outside it — `writeFile`/
|
|
371
|
-
* `readFile` follow symlinks, so a target like `MEMORY.md` symlinked to
|
|
372
|
-
* `~/.ssh/config` would be written/read THROUGH the link, escaping the dir. So
|
|
373
|
-
* we additionally:
|
|
374
|
-
* 1. refuse when the existing target is itself a symlink (lstat isSymbolicLink),
|
|
375
|
-
* 2. re-assert the REAL path of the target (or its nearest existing ancestor,
|
|
376
|
-
* for a not-yet-created target) is inside the REAL path of the root —
|
|
377
|
-
* defeating an in-root symlinked ancestor directory.
|
|
378
|
-
* The stat surface is injected so the symlink lens is unit-testable without a
|
|
379
|
-
* live filesystem.
|
|
380
|
-
*/
|
|
381
|
-
export function resolveInsideRoot(rootDir, relPath, statFs = NODE_STAT_FS) {
|
|
382
|
-
if (isAbsolute(relPath)) {
|
|
383
|
-
throw new RenderPathError(`render target must be relative, got absolute: ${relPath}`);
|
|
384
|
-
}
|
|
385
|
-
const root = resolve(rootDir);
|
|
386
|
-
const target = resolve(root, relPath);
|
|
387
|
-
const rel = relative(root, target);
|
|
388
|
-
if (rel === '' || rel.startsWith('..') || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
389
|
-
throw new RenderPathError(`render target escapes root: ${relPath}`);
|
|
390
|
-
}
|
|
391
|
-
// Symlink defence (Article VII). The lexical check above passed, so now guard
|
|
392
|
-
// against in-root symlinks that resolve outside the root.
|
|
393
|
-
let realRoot;
|
|
394
|
-
try {
|
|
395
|
-
realRoot = statFs.realpathSync(root);
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
// Root itself does not resolve (does not exist / broken) — fall back to the
|
|
399
|
-
// lexically validated path; nothing on disk to escape through yet.
|
|
400
|
-
return target;
|
|
401
|
-
}
|
|
402
|
-
// 1. The target, if it already exists, must not BE a symlink.
|
|
403
|
-
if (statFs.existsSync(target)) {
|
|
404
|
-
let isLink = false;
|
|
405
|
-
try {
|
|
406
|
-
isLink = statFs.lstatSync(target).isSymbolicLink();
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
isLink = false;
|
|
410
|
-
}
|
|
411
|
-
if (isLink) {
|
|
412
|
-
throw new RenderPathError(`render target is a symlink (refusing to follow): ${relPath}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// 2. The REAL path of the target's nearest existing ancestor must stay inside
|
|
416
|
-
// the real root — catches a symlinked intermediate directory.
|
|
417
|
-
let probe = target;
|
|
418
|
-
// Walk up to the nearest ancestor that exists on disk.
|
|
419
|
-
while (!statFs.existsSync(probe)) {
|
|
420
|
-
const parent = dirname(probe);
|
|
421
|
-
if (parent === probe)
|
|
422
|
-
break; // reached filesystem root
|
|
423
|
-
probe = parent;
|
|
424
|
-
}
|
|
425
|
-
let realProbe;
|
|
426
|
-
try {
|
|
427
|
-
realProbe = statFs.realpathSync(probe);
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
return target; // ancestor vanished mid-check — degrade to the lexical path
|
|
431
|
-
}
|
|
432
|
-
const realRel = relative(realRoot, realProbe);
|
|
433
|
-
if (realRel === '' && probe === root) {
|
|
434
|
-
return target; // the existing ancestor IS the root — fine
|
|
435
|
-
}
|
|
436
|
-
if (realRel.startsWith('..') || realRel.startsWith(`..${sep}`) || isAbsolute(realRel)) {
|
|
437
|
-
throw new RenderPathError(`render target escapes root via symlink: ${relPath}`);
|
|
438
|
-
}
|
|
439
|
-
return target;
|
|
440
|
-
}
|
|
441
|
-
const NODE_FS = { existsSync, readFileSync, writeFileSync, mkdirSync };
|
|
442
|
-
/**
|
|
443
|
-
* Write a marker-bounded block into `rootDir/relPath` safely:
|
|
444
|
-
* - the path is validated to stay inside rootDir (no escape),
|
|
445
|
-
* - an existing Wyrm region is replaced; operator prose outside is untouched,
|
|
446
|
-
* - a file that exists but is NOT Wyrm-managed is SKIPPED unless `force` (then
|
|
447
|
-
* the block is APPENDED, never overwriting the human content),
|
|
448
|
-
* - the write is idempotent: identical content ⇒ 'skipped' (no churn, lets the
|
|
449
|
-
* daemon re-render cheaply).
|
|
450
|
-
*/
|
|
451
|
-
export function writeRenderTarget(rootDir, relPath, block, opts = {}) {
|
|
452
|
-
const fs = opts.fs ?? NODE_FS;
|
|
453
|
-
const abs = resolveInsideRoot(rootDir, relPath);
|
|
454
|
-
const exists = fs.existsSync(abs);
|
|
455
|
-
const existing = exists ? fs.readFileSync(abs, 'utf-8') : null;
|
|
456
|
-
if (exists && existing != null && !hasWyrmRegion(existing) && !opts.force) {
|
|
457
|
-
return { path: abs, action: 'skipped', reason: 'file exists and is not Wyrm-managed (pass force to append)' };
|
|
458
|
-
}
|
|
459
|
-
const next = spliceWyrmRegion(existing, block);
|
|
460
|
-
if (existing != null && next === existing) {
|
|
461
|
-
return { path: abs, action: 'skipped', reason: 'unchanged' };
|
|
462
|
-
}
|
|
463
|
-
fs.mkdirSync(dirname(abs), { recursive: true });
|
|
464
|
-
fs.writeFileSync(abs, next, 'utf-8');
|
|
465
|
-
return { path: abs, action: exists ? 'updated' : 'created' };
|
|
466
|
-
}
|
|
467
|
-
/** Build the full deterministic render plan for a project (no I/O). */
|
|
468
|
-
export function buildRenderPlan(deps, project, stamp) {
|
|
469
|
-
const model = compileRenderModel(deps, project);
|
|
470
|
-
return {
|
|
471
|
-
model,
|
|
472
|
-
memoryMd: renderMemoryMd(model, stamp),
|
|
473
|
-
topics: renderTopicFiles(model, stamp),
|
|
474
|
-
sessionBrief: renderSessionBrief(model, stamp),
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Render a project's memory to disk: MEMORY.md + topic files (+ optional client
|
|
479
|
-
* adapter files). All writes are safe (no escape, no clobber of human prose).
|
|
480
|
-
*/
|
|
481
|
-
export function renderToDisk(deps, project, stamp, opts = {}) {
|
|
482
|
-
const root = opts.rootDir ?? project.path;
|
|
483
|
-
const topicsDir = opts.topicsDir ?? join('.wyrm', 'memory');
|
|
484
|
-
const plan = buildRenderPlan(deps, project, stamp);
|
|
485
|
-
const writes = [];
|
|
486
|
-
writes.push(writeRenderTarget(root, 'MEMORY.md', plan.memoryMd, opts));
|
|
487
|
-
for (const t of plan.topics) {
|
|
488
|
-
writes.push(writeRenderTarget(root, join(topicsDir, t.filename), t.content, { ...opts, force: true }));
|
|
489
|
-
}
|
|
490
|
-
for (const client of opts.clients ?? []) {
|
|
491
|
-
const target = renderForClient(client, plan.model, stamp);
|
|
492
|
-
writes.push(writeRenderTarget(root, target.relPath, target.block, opts));
|
|
493
|
-
}
|
|
494
|
-
return { plan, writes };
|
|
495
|
-
}
|
|
496
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
497
|
-
// Daemon re-render on Live Memory events (opt-in).
|
|
498
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
499
|
-
/**
|
|
500
|
-
* Whether daemon re-render is enabled. Off by default (Article VII) — the
|
|
501
|
-
* operator opts in with WYRM_RENDER_WATCH=1. Kept as a function so tests and the
|
|
502
|
-
* daemon read a fresh value, never a cached one.
|
|
503
|
-
*/
|
|
504
|
-
export function renderWatchEnabled(env = process.env) {
|
|
505
|
-
const v = (env.WYRM_RENDER_WATCH ?? '').toLowerCase();
|
|
506
|
-
return v === '1' || v === 'true' || v === 'yes';
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Tracks a project's Live Memory cursor and decides whether a re-render is due.
|
|
510
|
-
* Pure given the cursor inputs — the daemon supplies "the latest cursor I saw"
|
|
511
|
-
* and this returns whether it ADVANCED past the last render. No clock, no I/O.
|
|
512
|
-
*
|
|
513
|
-
* The daemon pattern: keep a RenderCursorTracker per project; on each tick poll
|
|
514
|
-
* eventsSince(db, projectId, tracker.last) for the head cursor; call
|
|
515
|
-
* tracker.advance(headCursor) — if it returns true, re-render and the tracker
|
|
516
|
-
* records the new head as rendered.
|
|
517
|
-
*/
|
|
518
|
-
export class RenderCursorTracker {
|
|
519
|
-
rendered;
|
|
520
|
-
constructor(initialCursor = 0) { this.rendered = initialCursor; }
|
|
521
|
-
/** The cursor value last rendered. */
|
|
522
|
-
get last() { return this.rendered; }
|
|
523
|
-
/**
|
|
524
|
-
* Given the current head cursor, returns true (and records it) iff it is
|
|
525
|
-
* strictly greater than the last rendered cursor — i.e. new events arrived
|
|
526
|
-
* since the last render. Idempotent: a stale/equal cursor never re-renders.
|
|
527
|
-
*/
|
|
528
|
-
advance(headCursor) {
|
|
529
|
-
if (!Number.isFinite(headCursor) || headCursor <= this.rendered)
|
|
530
|
-
return false;
|
|
531
|
-
this.rendered = headCursor;
|
|
532
|
-
return true;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
//# sourceMappingURL=render-target.js.map
|
|
26
|
+
`).all(r)}catch{return[]}}}}function b(e,r){return{project_id:r.id,project_name:r.name,project_path:r.path,truths:e.truths(r.id),quests:e.quests(r.id),artifacts:e.artifacts(r.id),failures:e.failures(r.id)}}function L(e){return e.truths.length+e.quests.length+e.artifacts.length+e.failures.length}function v(e,r){const n=L(e);return[`<!-- Compiled by Wyrm v${r.wyrm_version} from ${n} memory artifact${n===1?"":"s"}.`,` Project: ${e.project_name}. Compiled at ${r.compiled_at}.`," Edits inside the wyrm:render markers are HARVESTED to the review queue, not lost \u2014"," but they are OVERWRITTEN on the next `wyrm render`; capture durable notes with `wyrm capture`. -->"]}function u(e){return e.replace(/\s*\n\s*/g," ").trim()}function p(e,r){return e.length>r?`${e.slice(0,r-1)}\u2026`:e}function x(e,r){const n=v(e,r),t=[];t.push(`# ${e.project_name} \u2014 Project Memory`),t.push("");const s=W-4,c=[];e.truths.length>0&&c.push({heading:`## Ground truths (${e.truths.length})`,refTool:"wyrm_truth_get",lines:e.truths.map(i=>{const l=i.stale?" [STALE]":"",d=i.confidence<1?` (conf ${i.confidence})`:"";return`- **${i.category}.${i.key}**${l}: ${p(u(i.value),160)}${d}`})}),e.failures.length>0&&c.push({heading:`## Failures \u2014 DO NOT repeat (${e.failures.length})`,refTool:"wyrm_failure_check",lines:e.failures.map(i=>`- [${i.severity}] ${i.scope}:${i.target} \xD7${i.occurrences} \u2014 ${p(u(i.description),140)}`)}),e.quests.length>0&&c.push({heading:`## Open quests (${e.quests.length})`,refTool:"wyrm_quest",lines:e.quests.map(i=>`- #${i.id} [${i.priority}] ${p(u(i.title),140)}`)}),e.artifacts.length>0&&c.push({heading:`## Validated patterns (${e.artifacts.length})`,refTool:"wyrm_recall",lines:e.artifacts.map(i=>`- ${p(u(i.problem),120)}${i.validated_fix?` \u2192 ${p(u(i.validated_fix),120)}`:""}`)});let a=0;for(const i of c){if(a+4>s)break;t.push(i.heading),t.push(""),a+=2;let l=0;for(const d of i.lines){if(a+1+(l<i.lines.length?1:0)+1>s)break;t.push(d),a+=1,l+=1}l<i.lines.length&&(t.push(`- _\u2026 ${i.lines.length-l} more \u2014 query \`${i.refTool}\`._`),a+=1),t.push(""),a+=1}return t.push("---"),t.push("*Full memory: `wyrm recall` / `wyrm session prime`. This file is a deterministic digest.*"),[...n,...t].join(`
|
|
27
|
+
`)}function A(e,r){const n=v(e,r),t=[],o=(s,c,a)=>{a.length!==0&&t.push({filename:s,content:[...n,`# ${e.project_name} \u2014 ${c}`,"",...a].join(`
|
|
28
|
+
`)})};return o("truths.md","Ground truths",e.truths.map(s=>{const c=s.stale?" [STALE]":"";return`- **${s.category}.${s.key}**${c}: ${u(s.value)}`+(s.rationale?`
|
|
29
|
+
- rationale: ${u(s.rationale)}`:"")+(s.confidence<1?`
|
|
30
|
+
- confidence: ${s.confidence}`:"")})),o("quests.md","Open quests",e.quests.map(s=>`- #${s.id} [${s.priority}] ${u(s.title)}`+(s.description?`
|
|
31
|
+
- ${u(s.description)}`:""))),o("patterns.md","Validated patterns",e.artifacts.map(s=>`- ${u(s.problem)}`+(s.validated_fix?`
|
|
32
|
+
- fix: ${u(s.validated_fix)}`:""))),o("failures.md","Failures \u2014 DO NOT repeat",e.failures.map(s=>`- [${s.severity}] ${s.scope}:${s.target} \xD7${s.occurrences}
|
|
33
|
+
- ${u(s.description)}`)),t}function M(e,r){const n=[];if(n.push(`Wyrm memory for ${e.project_name} (compiled by Wyrm v${r.wyrm_version}):`),e.failures.length>0){n.push(""),n.push("Known failures (do not repeat):");for(const t of e.failures.slice(0,5))n.push(`- ${t.scope}:${t.target} \u2014 ${p(u(t.description),120)}`);e.failures.length>5&&n.push(`- (+${e.failures.length-5} more \u2014 wyrm_failure_check)`)}if(e.truths.length>0){n.push(""),n.push("Ground truths:");for(const t of e.truths.slice(0,8))n.push(`- ${t.category}.${t.key}: ${p(u(t.value),120)}`);e.truths.length>8&&n.push(`- (+${e.truths.length-8} more \u2014 wyrm_truth_get)`)}if(e.quests.length>0){n.push(""),n.push("Open quests:");for(const t of e.quests.slice(0,5))n.push(`- #${t.id} [${t.priority}] ${p(u(t.title),100)}`);e.quests.length>5&&n.push(`- (+${e.quests.length-5} more \u2014 wyrm_quest)`)}return n.push(""),n.push("Load full context with wyrm_session_prime. (Wyrm is this session's memory; consult it first.)"),n.join(`
|
|
34
|
+
`)}function q(e,r,n){const t=x(r,n);switch(e){case"claude":return{client:e,relPath:"CLAUDE.md",block:t};case"cursor":return{client:e,relPath:f(".cursor","rules","wyrm-memory.md"),block:t};case"copilot":return{client:e,relPath:f(".github","copilot-instructions.md"),block:t};case"agents":return{client:e,relPath:"AGENTS.md",block:t};default:{const o=e;throw new Error(`unknown render client: ${String(o)}`)}}}function C(e,r){const n=`${m}
|
|
35
|
+
${r}
|
|
36
|
+
${h}`;if(!e)return`${n}
|
|
37
|
+
`;const t=e.indexOf(m),o=e.indexOf(h);if(t!==-1&&o!==-1&&o>t){const c=e.slice(0,t),a=e.slice(o+h.length);return`${c}${n}${a}`}const s=e.endsWith(`
|
|
38
|
+
`)?`
|
|
39
|
+
`:`
|
|
40
|
+
|
|
41
|
+
`;return`${e}${s}${n}
|
|
42
|
+
`}function I(e){const r=e.indexOf(m),n=e.indexOf(h);return r!==-1&&n!==-1&&n>r}class y extends Error{}const j={existsSync:E,lstatSync:T,realpathSync:N};function H(e,r,n=j){if($(r))throw new y(`render target must be relative, got absolute: ${r}`);const t=R(e),o=R(t,r),s=w(t,o);if(s===""||s.startsWith("..")||s.startsWith(`..${S}`)||$(s))throw new y(`render target escapes root: ${r}`);let c;try{c=n.realpathSync(t)}catch{return o}if(n.existsSync(o)){let d=!1;try{d=n.lstatSync(o).isSymbolicLink()}catch{d=!1}if(d)throw new y(`render target is a symlink (refusing to follow): ${r}`)}let a=o;for(;!n.existsSync(a);){const d=_(a);if(d===a)break;a=d}let i;try{i=n.realpathSync(a)}catch{return o}const l=w(c,i);if(l===""&&a===t)return o;if(l.startsWith("..")||l.startsWith(`..${S}`)||$(l))throw new y(`render target escapes root via symlink: ${r}`);return o}const F={existsSync:E,readFileSync:D,writeFileSync:O,mkdirSync:k};function g(e,r,n,t={}){const o=t.fs??F,s=H(e,r),c=o.existsSync(s),a=c?o.readFileSync(s,"utf-8"):null;if(c&&a!=null&&!I(a)&&!t.force)return{path:s,action:"skipped",reason:"file exists and is not Wyrm-managed (pass force to append)"};const i=C(a,n);return a!=null&&i===a?{path:s,action:"skipped",reason:"unchanged"}:(o.mkdirSync(_(s),{recursive:!0}),o.writeFileSync(s,i,"utf-8"),{path:s,action:c?"updated":"created"})}function B(e,r,n){const t=b(e,r);return{model:t,memoryMd:x(t,n),topics:A(t,n),sessionBrief:M(t,n)}}function U(e,r,n,t={}){const o=t.rootDir??r.path,s=t.topicsDir??f(".wyrm","memory"),c=B(e,r,n),a=[];a.push(g(o,"MEMORY.md",c.memoryMd,t));for(const i of c.topics)a.push(g(o,f(s,i.filename),i.content,{...t,force:!0}));for(const i of t.clients??[]){const l=q(i,c.model,n);a.push(g(o,l.relPath,l.block,t))}return{plan:c,writes:a}}function V(e=process.env){const r=(e.WYRM_RENDER_WATCH??"").toLowerCase();return r==="1"||r==="true"||r==="yes"}class K{rendered;constructor(r=0){this.rendered=r}get last(){return this.rendered}advance(r){return!Number.isFinite(r)||r<=this.rendered?!1:(this.rendered=r,!0)}}export{W as MEMORY_MD_LINE_BUDGET,h as RENDER_MARKER_END,m as RENDER_MARKER_START,K as RenderCursorTracker,y as RenderPathError,B as buildRenderPlan,b as compileRenderModel,I as hasWyrmRegion,P as makeRenderDeps,v as provenanceHeader,q as renderForClient,x as renderMemoryMd,M as renderSessionBrief,U as renderToDisk,A as renderTopicFiles,V as renderWatchEnabled,H as resolveInsideRoot,C as spliceWyrmRegion,g as writeRenderTarget};
|