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/wyrm-cli.js
CHANGED
|
@@ -1,1724 +1,115 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* Wyrm CLI — Terminal interface for browsing and capturing AI memory
|
|
4
|
-
*
|
|
5
|
-
* Usage: wyrm <command> [options]
|
|
6
|
-
*
|
|
7
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
8
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
9
|
-
*/
|
|
10
|
-
import { join, dirname } from 'path';
|
|
11
|
-
import { homedir } from 'os';
|
|
12
|
-
import { existsSync, readFileSync } from 'fs';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { spawnSync } from 'child_process';
|
|
15
|
-
import { createInterface } from 'readline';
|
|
16
|
-
import { WyrmDB } from './database.js';
|
|
17
|
-
import { MemoryArtifacts } from './memory-artifacts.js';
|
|
18
|
-
import { GroundTruths } from './intelligence.js';
|
|
19
|
-
import { Rehydration } from './rehydration.js';
|
|
20
|
-
import { makeRenderDeps, buildRenderPlan, renderToDisk, renderForClient, } from './render-target.js';
|
|
21
|
-
import { classifyCapture } from './capture.js';
|
|
22
|
-
import { c, colors, formatTable, printSection, printSuccess, printError, icons } from './cli.js';
|
|
23
|
-
import { readActor } from './attribution.js';
|
|
24
|
-
// ==================== VERSION ====================
|
|
25
|
-
/**
|
|
26
|
-
* Read name+version from the package.json shipped alongside dist/, so the CLI
|
|
27
|
-
* banner and `--version` always match what npm published — no hard-coded
|
|
28
|
-
* constant to drift on each release. Falls back gracefully if unreadable.
|
|
29
|
-
*/
|
|
30
|
-
function readPkg() {
|
|
31
|
-
try {
|
|
32
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
-
// dist/ sits one level below the package root.
|
|
34
|
-
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8'));
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// ==================== FLAG HELPERS ====================
|
|
41
|
-
/** Parse an int flag, falling back to a default for missing/invalid values
|
|
42
|
-
* (a bare `--limit` with no number would otherwise coerce to NaN). */
|
|
43
|
-
function intFlag(v, def) {
|
|
44
|
-
const n = typeof v === 'string' ? parseInt(v, 10) : NaN;
|
|
45
|
-
return Number.isFinite(n) ? n : def;
|
|
46
|
-
}
|
|
47
|
-
/** Parse a float flag, falling back to a default for missing/invalid values. */
|
|
48
|
-
function floatFlag(v, def) {
|
|
49
|
-
const n = typeof v === 'string' ? parseFloat(v) : NaN;
|
|
50
|
-
return Number.isFinite(n) ? n : def;
|
|
51
|
-
}
|
|
52
|
-
// ==================== DB INITIALIZATION ====================
|
|
53
|
-
function getDbPath() {
|
|
54
|
-
return process.env.WYRM_DB_PATH ?? join(homedir(), '.wyrm', 'wyrm.db');
|
|
55
|
-
}
|
|
56
|
-
function openDb() {
|
|
57
|
-
return new WyrmDB(getDbPath());
|
|
58
|
-
}
|
|
59
|
-
function parseArgs(args) {
|
|
60
|
-
const positional = [];
|
|
61
|
-
const flags = {};
|
|
62
|
-
let i = 0;
|
|
63
|
-
while (i < args.length) {
|
|
64
|
-
const arg = args[i];
|
|
65
|
-
if (arg.startsWith('--')) {
|
|
66
|
-
const key = arg.slice(2);
|
|
67
|
-
const next = args[i + 1];
|
|
68
|
-
if (next && !next.startsWith('--')) {
|
|
69
|
-
flags[key] = next;
|
|
70
|
-
i += 2;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
flags[key] = true;
|
|
74
|
-
i++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
positional.push(arg);
|
|
79
|
-
i++;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return { positional, flags };
|
|
83
|
-
}
|
|
84
|
-
// ==================== PROJECT RESOLUTION ====================
|
|
85
|
-
function resolveProject(db, projectName) {
|
|
86
|
-
if (!projectName)
|
|
87
|
-
return null;
|
|
88
|
-
const rawDb = db.getDatabase();
|
|
89
|
-
const proj = rawDb.prepare('SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1').get(`%${projectName}%`);
|
|
90
|
-
return proj ?? null;
|
|
91
|
-
}
|
|
92
|
-
// ==================== COMMANDS ====================
|
|
93
|
-
async function cmdSearch(args) {
|
|
94
|
-
const { positional, flags } = parseArgs(args);
|
|
95
|
-
const query = positional[0];
|
|
96
|
-
if (!query) {
|
|
97
|
-
printError('Usage: wyrm search <query> [--project <name>] [--type all|memories|truths|quests|data]');
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
const db = openDb();
|
|
101
|
-
const rawDb = db.getDatabase();
|
|
102
|
-
const searchType = flags['type'] ?? 'all';
|
|
103
|
-
const projectName = flags['project'];
|
|
104
|
-
const proj = projectName ? resolveProject(db, projectName) : null;
|
|
105
|
-
const projectId = proj?.id;
|
|
106
|
-
printSection(`Search: "${query}"`);
|
|
107
|
-
const rows = [];
|
|
108
|
-
// FTS search memories
|
|
109
|
-
if (searchType === 'all' || searchType === 'memories') {
|
|
110
|
-
try {
|
|
111
|
-
const projectClause = projectId ? `AND m.project_id = ${projectId}` : '';
|
|
112
|
-
const mems = rawDb.prepare(`
|
|
2
|
+
import{join as oe,dirname as ae}from"path";import{homedir as Ee}from"os";import{existsSync as ce,readFileSync as V}from"fs";import{fileURLToPath as le}from"url";import{spawnSync as B}from"child_process";import{createInterface as K}from"readline";import{WyrmDB as xe}from"./database.js";import{MemoryArtifacts as G}from"./memory-artifacts.js";import{GroundTruths as de}from"./intelligence.js";import{Rehydration as Ce}from"./rehydration.js";import{makeRenderDeps as pe,buildRenderPlan as se,renderToDisk as Re,renderForClient as ue}from"./render-target.js";import{classifyCapture as Pe}from"./capture.js";import{c as d,colors as J,formatTable as M,printSection as j,printSuccess as v,printError as p,icons as L}from"./cli.js";import{readActor as Te}from"./attribution.js";function O(){try{const l=ae(le(import.meta.url));return JSON.parse(V(oe(l,"..","package.json"),"utf-8"))}catch{return{}}}function R(l,i){const e=typeof l=="string"?parseInt(l,10):NaN;return Number.isFinite(e)?e:i}function z(l,i){const e=typeof l=="string"?parseFloat(l):NaN;return Number.isFinite(e)?e:i}function De(){return process.env.WYRM_DB_PATH??oe(Ee(),".wyrm","wyrm.db")}function E(){return new xe(De())}function x(l){const i=[],e={};let s=0;for(;s<l.length;){const r=l[s];if(r.startsWith("--")){const t=r.slice(2),n=l[s+1];n&&!n.startsWith("--")?(e[t]=n,s+=2):(e[t]=!0,s++)}else i.push(r),s++}return{positional:i,flags:e}}function P(l,i){return i?l.getDatabase().prepare("SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1").get(`%${i}%`)??null:null}async function Me(l){const{positional:i,flags:e}=x(l),s=i[0];s||(p("Usage: wyrm search <query> [--project <name>] [--type all|memories|truths|quests|data]"),process.exit(1));const r=E(),t=r.getDatabase(),n=e.type??"all",a=e.project,u=(a?P(r,a):null)?.id;j(`Search: "${s}"`);const c=[];if(n==="all"||n==="memories")try{const m=u?`AND m.project_id = ${u}`:"",y=t.prepare(`
|
|
113
3
|
SELECT m.id, m.kind, m.problem, m.project_id FROM memory_artifacts m
|
|
114
4
|
JOIN memory_artifacts_fts fts ON m.id = fts.rowid
|
|
115
|
-
WHERE memory_artifacts_fts MATCH ? ${
|
|
5
|
+
WHERE memory_artifacts_fts MATCH ? ${m}
|
|
116
6
|
LIMIT 20
|
|
117
|
-
`).all(
|
|
118
|
-
for (const m of mems) {
|
|
119
|
-
rows.push([`mem:${m.id}`, 'memory', m.kind, m.problem.slice(0, 80)]);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch { /* FTS not available or empty */ }
|
|
123
|
-
}
|
|
124
|
-
// FTS search sessions
|
|
125
|
-
if (searchType === 'all' || searchType === 'sessions') {
|
|
126
|
-
try {
|
|
127
|
-
const projectClause = projectId ? `AND s.project_id = ${projectId}` : '';
|
|
128
|
-
const sessions = rawDb.prepare(`
|
|
7
|
+
`).all(s);for(const f of y)c.push([`mem:${f.id}`,"memory",f.kind,f.problem.slice(0,80)])}catch{}if(n==="all"||n==="sessions")try{const m=u?`AND s.project_id = ${u}`:"",y=t.prepare(`
|
|
129
8
|
SELECT s.id, s.objectives, s.project_id FROM sessions s
|
|
130
9
|
JOIN sessions_fts fts ON s.id = fts.rowid
|
|
131
|
-
WHERE sessions_fts MATCH ? ${
|
|
10
|
+
WHERE sessions_fts MATCH ? ${m}
|
|
132
11
|
LIMIT 10
|
|
133
|
-
`).all(
|
|
134
|
-
for (const s of sessions) {
|
|
135
|
-
rows.push([`session:${s.id}`, 'session', '', s.objectives.slice(0, 80)]);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch { /* ignore */ }
|
|
139
|
-
}
|
|
140
|
-
// FTS search quests
|
|
141
|
-
if (searchType === 'all' || searchType === 'quests') {
|
|
142
|
-
try {
|
|
143
|
-
const projectClause = projectId ? `AND q.project_id = ${projectId}` : '';
|
|
144
|
-
const quests = rawDb.prepare(`
|
|
12
|
+
`).all(s);for(const f of y)c.push([`session:${f.id}`,"session","",f.objectives.slice(0,80)])}catch{}if(n==="all"||n==="quests")try{const m=u?`AND q.project_id = ${u}`:"",y=t.prepare(`
|
|
145
13
|
SELECT q.id, q.title, q.priority, q.project_id FROM quests q
|
|
146
14
|
JOIN quests_fts fts ON q.id = fts.rowid
|
|
147
|
-
WHERE quests_fts MATCH ? ${
|
|
15
|
+
WHERE quests_fts MATCH ? ${m}
|
|
148
16
|
LIMIT 10
|
|
149
|
-
`).all(
|
|
150
|
-
for (const q of quests) {
|
|
151
|
-
rows.push([`quest:${q.id}`, 'quest', q.priority, q.title.slice(0, 80)]);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch { /* ignore */ }
|
|
155
|
-
}
|
|
156
|
-
// FTS search data lake
|
|
157
|
-
if (searchType === 'all' || searchType === 'data') {
|
|
158
|
-
try {
|
|
159
|
-
const projectClause = projectId ? `AND d.project_id = ${projectId}` : '';
|
|
160
|
-
const data = rawDb.prepare(`
|
|
17
|
+
`).all(s);for(const f of y)c.push([`quest:${f.id}`,"quest",f.priority,f.title.slice(0,80)])}catch{}if(n==="all"||n==="data")try{const m=u?`AND d.project_id = ${u}`:"",y=t.prepare(`
|
|
161
18
|
SELECT d.id, d.category, d.key, d.value FROM data_lake d
|
|
162
19
|
JOIN data_lake_fts fts ON d.id = fts.rowid
|
|
163
|
-
WHERE data_lake_fts MATCH ? ${
|
|
20
|
+
WHERE data_lake_fts MATCH ? ${m}
|
|
164
21
|
LIMIT 10
|
|
165
|
-
`).all(
|
|
166
|
-
|
|
167
|
-
rows.push([`data:${d.id}`, 'data', d.category, d.value.slice(0, 80)]);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch { /* ignore */ }
|
|
171
|
-
}
|
|
172
|
-
db.close();
|
|
173
|
-
if (rows.length === 0) {
|
|
174
|
-
console.log(c.dim(` No results found for "${query}"`));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
console.log(formatTable(['ID', 'Type', 'Subtype', 'Preview'], rows));
|
|
178
|
-
console.log(c.dim(`\n ${rows.length} result${rows.length !== 1 ? 's' : ''}`));
|
|
179
|
-
}
|
|
180
|
-
async function cmdLs(args) {
|
|
181
|
-
const { flags } = parseArgs(args);
|
|
182
|
-
const type = flags['type'] ?? 'all';
|
|
183
|
-
const limit = intFlag(flags['limit'], 20);
|
|
184
|
-
const projectName = flags['project'];
|
|
185
|
-
const db = openDb();
|
|
186
|
-
const rawDb = db.getDatabase();
|
|
187
|
-
const proj = projectName ? resolveProject(db, projectName) : null;
|
|
188
|
-
const projectId = proj?.id;
|
|
189
|
-
const projectClause = projectId ? `WHERE project_id = ${projectId}` : '';
|
|
190
|
-
const projectClauseAnd = projectId ? `AND project_id = ${projectId}` : '';
|
|
191
|
-
printSection('Wyrm Memory');
|
|
192
|
-
if (type === 'all' || type === 'memories') {
|
|
193
|
-
printSection('Memories');
|
|
194
|
-
const mems = rawDb.prepare(`
|
|
22
|
+
`).all(s);for(const f of y)c.push([`data:${f.id}`,"data",f.category,f.value.slice(0,80)])}catch{}if(r.close(),c.length===0){console.log(d.dim(` No results found for "${s}"`));return}console.log(M(["ID","Type","Subtype","Preview"],c)),console.log(d.dim(`
|
|
23
|
+
${c.length} result${c.length!==1?"s":""}`))}async function Ie(l){const{flags:i}=x(l),e=i.type??"all",s=R(i.limit,20),r=i.project,t=E(),n=t.getDatabase(),o=(r?P(t,r):null)?.id,u=o?`WHERE project_id = ${o}`:"",c=o?`AND project_id = ${o}`:"";if(j("Wyrm Memory"),e==="all"||e==="memories"){j("Memories");const m=n.prepare(`
|
|
195
24
|
SELECT id, kind, confidence, problem, tags FROM memory_artifacts
|
|
196
|
-
${
|
|
25
|
+
${u}
|
|
197
26
|
ORDER BY created_at DESC LIMIT ?
|
|
198
|
-
`).all(
|
|
199
|
-
if (mems.length > 0) {
|
|
200
|
-
const rows = mems.map(m => [
|
|
201
|
-
`mem:${m.id}`,
|
|
202
|
-
m.kind,
|
|
203
|
-
`${Math.round(m.confidence * 100)}%`,
|
|
204
|
-
m.problem.slice(0, 60),
|
|
205
|
-
m.tags?.slice(0, 30) ?? '',
|
|
206
|
-
]);
|
|
207
|
-
console.log(formatTable(['ID', 'Kind', 'Conf', 'Preview', 'Tags'], rows));
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
console.log(c.dim(' No memories yet.'));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (type === 'all' || type === 'truths') {
|
|
214
|
-
printSection('Ground Truths');
|
|
215
|
-
const truths = rawDb.prepare(`
|
|
27
|
+
`).all(s);if(m.length>0){const y=m.map(f=>[`mem:${f.id}`,f.kind,`${Math.round(f.confidence*100)}%`,f.problem.slice(0,60),f.tags?.slice(0,30)??""]);console.log(M(["ID","Kind","Conf","Preview","Tags"],y))}else console.log(d.dim(" No memories yet."))}if(e==="all"||e==="truths"){j("Ground Truths");const m=n.prepare(`
|
|
216
28
|
SELECT id, category, key, value FROM ground_truths
|
|
217
|
-
WHERE is_current = 1 ${
|
|
29
|
+
WHERE is_current = 1 ${c}
|
|
218
30
|
ORDER BY created_at DESC LIMIT ?
|
|
219
|
-
`).all(
|
|
220
|
-
if (truths.length > 0) {
|
|
221
|
-
const rows = truths.map(t => [
|
|
222
|
-
`truth:${t.id}`,
|
|
223
|
-
t.category,
|
|
224
|
-
t.key.slice(0, 30),
|
|
225
|
-
t.value.slice(0, 60),
|
|
226
|
-
]);
|
|
227
|
-
console.log(formatTable(['ID', 'Category', 'Key', 'Value'], rows));
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
console.log(c.dim(' No ground truths yet.'));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (type === 'all' || type === 'quests') {
|
|
234
|
-
printSection('Quests');
|
|
235
|
-
const quests = rawDb.prepare(`
|
|
31
|
+
`).all(s);if(m.length>0){const y=m.map(f=>[`truth:${f.id}`,f.category,f.key.slice(0,30),f.value.slice(0,60)]);console.log(M(["ID","Category","Key","Value"],y))}else console.log(d.dim(" No ground truths yet."))}if(e==="all"||e==="quests"){j("Quests");const m=n.prepare(`
|
|
236
32
|
SELECT id, priority, title, status FROM quests
|
|
237
|
-
${
|
|
33
|
+
${u}
|
|
238
34
|
ORDER BY created_at DESC LIMIT ?
|
|
239
|
-
`).all(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
async function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!typedId) {
|
|
259
|
-
printError('Usage: wyrm show <typed-id> (e.g. mem:41, quest:12, truth:7, data:5, session:3)');
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
const [typePrefix, idStr] = typedId.split(':');
|
|
263
|
-
const id = parseInt(idStr ?? '', 10);
|
|
264
|
-
if (!typePrefix || isNaN(id)) {
|
|
265
|
-
printError(`Invalid typed ID: ${typedId}. Format: type:number (e.g. mem:41)`);
|
|
266
|
-
process.exit(1);
|
|
267
|
-
}
|
|
268
|
-
const db = openDb();
|
|
269
|
-
const rawDb = db.getDatabase();
|
|
270
|
-
printSection(`${typedId}`);
|
|
271
|
-
switch (typePrefix) {
|
|
272
|
-
case 'mem': {
|
|
273
|
-
const row = rawDb.prepare('SELECT * FROM memory_artifacts WHERE id = ?').get(id);
|
|
274
|
-
if (!row) {
|
|
275
|
-
printError(`Memory artifact ${id} not found`);
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
console.log(c.bold('Kind: ') + row['kind']);
|
|
279
|
-
console.log(c.bold('Confidence:') + ` ${Math.round(row['confidence'] * 100)}%`);
|
|
280
|
-
console.log(c.bold('Problem: ') + '\n' + row['problem']);
|
|
281
|
-
if (row['validated_fix'])
|
|
282
|
-
console.log(c.bold('Solution: ') + '\n' + row['validated_fix']);
|
|
283
|
-
if (row['why_it_worked'])
|
|
284
|
-
console.log(c.bold('Why: ') + '\n' + row['why_it_worked']);
|
|
285
|
-
if (row['tags'])
|
|
286
|
-
console.log(c.bold('Tags: ') + row['tags']);
|
|
287
|
-
console.log(c.bold('Created: ') + row['created_at']);
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
case 'quest': {
|
|
291
|
-
const row = rawDb.prepare('SELECT * FROM quests WHERE id = ?').get(id);
|
|
292
|
-
if (!row) {
|
|
293
|
-
printError(`Quest ${id} not found`);
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
console.log(c.bold('Title: ') + row['title']);
|
|
297
|
-
console.log(c.bold('Priority: ') + row['priority']);
|
|
298
|
-
console.log(c.bold('Status: ') + row['status']);
|
|
299
|
-
if (row['description'])
|
|
300
|
-
console.log(c.bold('Desc: ') + '\n' + row['description']);
|
|
301
|
-
if (row['tags'])
|
|
302
|
-
console.log(c.bold('Tags: ') + row['tags']);
|
|
303
|
-
console.log(c.bold('Created: ') + row['created_at']);
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
case 'truth': {
|
|
307
|
-
const row = rawDb.prepare('SELECT * FROM ground_truths WHERE id = ?').get(id);
|
|
308
|
-
if (!row) {
|
|
309
|
-
printError(`Ground truth ${id} not found`);
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
console.log(c.bold('Category: ') + row['category']);
|
|
313
|
-
console.log(c.bold('Key: ') + row['key']);
|
|
314
|
-
console.log(c.bold('Value: ') + '\n' + row['value']);
|
|
315
|
-
if (row['rationale'])
|
|
316
|
-
console.log(c.bold('Rationale:') + '\n' + row['rationale']);
|
|
317
|
-
console.log(c.bold('Active: ') + (row['is_current'] ? 'Yes' : 'No (superseded)'));
|
|
318
|
-
console.log(c.bold('Created: ') + row['created_at']);
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
case 'data': {
|
|
322
|
-
const row = rawDb.prepare('SELECT * FROM data_lake WHERE id = ?').get(id);
|
|
323
|
-
if (!row) {
|
|
324
|
-
printError(`Data point ${id} not found`);
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
console.log(c.bold('Category: ') + row['category']);
|
|
328
|
-
console.log(c.bold('Key: ') + row['key']);
|
|
329
|
-
console.log(c.bold('Value: ') + '\n' + String(row['value']).slice(0, 500));
|
|
330
|
-
console.log(c.bold('Created: ') + row['created_at']);
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
case 'session': {
|
|
334
|
-
const row = rawDb.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
335
|
-
if (!row) {
|
|
336
|
-
printError(`Session ${id} not found`);
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
console.log(c.bold('Date: ') + row['date']);
|
|
340
|
-
console.log(c.bold('Objectives: ') + '\n' + row['objectives']);
|
|
341
|
-
if (row['completed'])
|
|
342
|
-
console.log(c.bold('Completed: ') + '\n' + row['completed']);
|
|
343
|
-
if (row['notes'])
|
|
344
|
-
console.log(c.bold('Notes: ') + '\n' + row['notes']);
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
default:
|
|
348
|
-
printError(`Unknown type prefix: ${typePrefix}. Use mem|quest|truth|data|session`);
|
|
349
|
-
}
|
|
350
|
-
db.close();
|
|
351
|
-
}
|
|
352
|
-
async function cmdCapture(args) {
|
|
353
|
-
const { positional, flags } = parseArgs(args);
|
|
354
|
-
const content = positional[0];
|
|
355
|
-
if (!content) {
|
|
356
|
-
printError('Usage: wyrm capture "<content>" [--project <name>] [--mode auto|quest|truth|memory]');
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
const projectName = flags['project'];
|
|
360
|
-
const mode = flags['mode'];
|
|
361
|
-
const db = openDb();
|
|
362
|
-
const rawDb = db.getDatabase();
|
|
363
|
-
let projectId = null;
|
|
364
|
-
if (projectName) {
|
|
365
|
-
const proj = resolveProject(db, projectName);
|
|
366
|
-
if (!proj) {
|
|
367
|
-
printError(`Project not found: ${projectName}`);
|
|
368
|
-
db.close();
|
|
369
|
-
process.exit(1);
|
|
370
|
-
}
|
|
371
|
-
projectId = proj.id;
|
|
372
|
-
}
|
|
373
|
-
// Classify
|
|
374
|
-
let classified = classifyCapture(content);
|
|
375
|
-
if (mode && mode !== 'auto') {
|
|
376
|
-
const subtypeMap = { quest: 'quest', truth: 'decision', memory: 'pattern' };
|
|
377
|
-
classified = {
|
|
378
|
-
type: mode,
|
|
379
|
-
subtype: subtypeMap[mode] ?? mode,
|
|
380
|
-
confidence: 100,
|
|
381
|
-
reasoning: `Mode override: ${mode}`,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
const { type, subtype, confidence, reasoning } = classified;
|
|
385
|
-
if ((type === 'quest' || type === 'truth' || type === 'memory') && projectId === null) {
|
|
386
|
-
printError('A --project is required to capture. Use: wyrm capture "<text>" --project <name>');
|
|
387
|
-
db.close();
|
|
388
|
-
process.exit(1);
|
|
389
|
-
}
|
|
390
|
-
let storedId = 0;
|
|
391
|
-
let typeShort = '';
|
|
392
|
-
let needsReview = false;
|
|
393
|
-
const memory = new MemoryArtifacts(rawDb);
|
|
394
|
-
const groundTruths = new GroundTruths(rawDb);
|
|
395
|
-
if (type === 'quest') {
|
|
396
|
-
const quest = db.addQuest(projectId, content.slice(0, 200), '', 'medium');
|
|
397
|
-
storedId = quest.id;
|
|
398
|
-
typeShort = 'quest';
|
|
399
|
-
}
|
|
400
|
-
else if (type === 'truth') {
|
|
401
|
-
if (mode !== 'truth' && confidence < 100) {
|
|
402
|
-
const artifact = memory.add(projectId, { kind: 'pattern', problem: content, confidence: confidence / 100, needsReview: 1 });
|
|
403
|
-
storedId = artifact.id;
|
|
404
|
-
typeShort = 'mem';
|
|
405
|
-
needsReview = true;
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const truth = groundTruths.set(projectId, { category: 'decision', key: content.slice(0, 60), value: content });
|
|
409
|
-
storedId = truth.id;
|
|
410
|
-
typeShort = 'truth';
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
const shouldAutoCreate = confidence >= 75;
|
|
415
|
-
const artifact = memory.add(projectId, {
|
|
416
|
-
kind: subtype,
|
|
417
|
-
problem: content, confidence: confidence / 100, needsReview: shouldAutoCreate ? 0 : 1,
|
|
418
|
-
});
|
|
419
|
-
storedId = artifact.id;
|
|
420
|
-
typeShort = 'mem';
|
|
421
|
-
if (!shouldAutoCreate)
|
|
422
|
-
needsReview = true;
|
|
423
|
-
}
|
|
424
|
-
db.close();
|
|
425
|
-
printSuccess(`Captured as ${type}: ${subtype}`);
|
|
426
|
-
console.log(`${c.dim('Confidence:')} ${confidence}% | ${c.dim(reasoning)}`);
|
|
427
|
-
console.log(`${c.dim('ID:')} ${typeShort}:${storedId}`);
|
|
428
|
-
if (needsReview)
|
|
429
|
-
console.log(`${icons.warning} Stored for review — run ${c.cyan('wyrm review')} to activate`);
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Print a session rehydration brief: the cross-session continuity briefing
|
|
433
|
-
* (objectives, completed work, ground truths, open quests, validated patterns,
|
|
434
|
-
* unresolved failures) drawn from a project's most recent session.
|
|
435
|
-
*
|
|
436
|
-
* Resolution order: --session <id> → --path <dir> (exact project path) →
|
|
437
|
-
* --project <name> → the current working directory. This is the command the
|
|
438
|
-
* SessionStart hook shells to, so a fresh agent inherits prior state without a
|
|
439
|
-
* manual tool call. It is READ-ONLY and never exits non-zero (a load hook must
|
|
440
|
-
* not break a session): on any miss it writes a short note to stderr and exits 0.
|
|
441
|
-
*/
|
|
442
|
-
async function cmdRehydrate(args) {
|
|
443
|
-
const { flags } = parseArgs(args);
|
|
444
|
-
const explicitSession = intFlag(flags['session'], 0);
|
|
445
|
-
const pathFlag = flags['path'];
|
|
446
|
-
const projectName = flags['project'];
|
|
447
|
-
const maxChars = intFlag(flags['max-chars'], 6000);
|
|
448
|
-
const quiet = flags['quiet'] === true; // suppress the "nothing found" stderr note (for hooks)
|
|
449
|
-
// Silence the DB layer's own info logging (migrations / WAL checkpoint / close)
|
|
450
|
-
// so this command's stdout is a clean, machine-consumable brief: it is piped
|
|
451
|
-
// straight into the SessionStart hook. The brief itself and the not-found
|
|
452
|
-
// notes go through process.stdout/stderr directly, so they are unaffected.
|
|
453
|
-
const realConsoleLog = console.log;
|
|
454
|
-
console.log = () => { };
|
|
455
|
-
const db = openDb();
|
|
456
|
-
try {
|
|
457
|
-
const rehydration = new Rehydration(db.getDatabase());
|
|
458
|
-
// 1. Resolve which session to rehydrate.
|
|
459
|
-
let sessionId = explicitSession;
|
|
460
|
-
if (sessionId <= 0) {
|
|
461
|
-
// Resolve the project: explicit --path (exact), then --project (name match), then cwd.
|
|
462
|
-
let project = pathFlag ? db.getProject(pathFlag) : undefined;
|
|
463
|
-
if (!project && projectName)
|
|
464
|
-
project = resolveProject(db, projectName) ?? undefined;
|
|
465
|
-
if (!project)
|
|
466
|
-
project = db.getProject(process.cwd());
|
|
467
|
-
if (!project) {
|
|
468
|
-
if (!quiet)
|
|
469
|
-
process.stderr.write('wyrm rehydrate: no Wyrm project for this directory, nothing to restore.\n');
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const recent = db.getRecentSessions(project.id, 1);
|
|
473
|
-
if (recent.length === 0) {
|
|
474
|
-
if (!quiet)
|
|
475
|
-
process.stderr.write(`wyrm rehydrate: project "${project.name}" has no prior sessions yet.\n`);
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
sessionId = recent[0].id;
|
|
479
|
-
}
|
|
480
|
-
// 2. Build the brief and print it (truncating very long briefs so a
|
|
481
|
-
// SessionStart injection never bloats a fresh context window).
|
|
482
|
-
const brief = rehydration.rehydrate(sessionId);
|
|
483
|
-
if (!brief) {
|
|
484
|
-
if (!quiet)
|
|
485
|
-
process.stderr.write(`wyrm rehydrate: session ${sessionId} not found.\n`);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
let md = brief.briefing_markdown;
|
|
489
|
-
if (maxChars > 0 && md.length > maxChars) {
|
|
490
|
-
md = md.slice(0, maxChars) +
|
|
491
|
-
`\n\n_... brief truncated at ${maxChars} chars, run \`wyrm show session:${brief.session_id}\` for the full record._`;
|
|
492
|
-
}
|
|
493
|
-
process.stdout.write(md + '\n');
|
|
494
|
-
}
|
|
495
|
-
finally {
|
|
496
|
-
db.close();
|
|
497
|
-
console.log = realConsoleLog;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* `wyrm render` — compile the project's memory to MEMORY.md + topic files (+
|
|
502
|
-
* optional client adapters). The zero-MCP-token path for casual sessions: a
|
|
503
|
-
* deterministic digest of ground truths / failures / quests / patterns written
|
|
504
|
-
* straight into the harness-native memory slot. Safe (never escapes the target
|
|
505
|
-
* dir, never clobbers operator prose outside the wyrm:render markers).
|
|
506
|
-
*
|
|
507
|
-
* Flags:
|
|
508
|
-
* --path <dir> project to render (defaults to cwd's project)
|
|
509
|
-
* --project <name> project by name
|
|
510
|
-
* --out <dir> where to write (defaults to the project path)
|
|
511
|
-
* --client <list> comma-separated: claude,cursor,copilot,agents
|
|
512
|
-
* --brief print the SessionStart brief to stdout instead of writing files
|
|
513
|
-
* --force append into non-Wyrm-managed files (never overwrites prose)
|
|
514
|
-
* --quiet suppress the per-file write log
|
|
515
|
-
*/
|
|
516
|
-
async function cmdRender(args) {
|
|
517
|
-
const { flags } = parseArgs(args);
|
|
518
|
-
const pathFlag = flags['path'];
|
|
519
|
-
const projectName = flags['project'];
|
|
520
|
-
const outDir = flags['out'];
|
|
521
|
-
const briefOnly = flags['brief'] === true;
|
|
522
|
-
const force = flags['force'] === true;
|
|
523
|
-
const quiet = flags['quiet'] === true;
|
|
524
|
-
const clientList = (typeof flags['client'] === 'string' ? flags['client'] : '')
|
|
525
|
-
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
526
|
-
const VALID_CLIENTS = ['claude', 'cursor', 'copilot', 'agents'];
|
|
527
|
-
const clients = clientList.filter((c) => VALID_CLIENTS.includes(c));
|
|
528
|
-
const badClients = clientList.filter((c) => !VALID_CLIENTS.includes(c));
|
|
529
|
-
if (badClients.length > 0) {
|
|
530
|
-
printError(`Unknown --client value(s): ${badClients.join(', ')} (valid: ${VALID_CLIENTS.join('|')})`);
|
|
531
|
-
process.exit(1);
|
|
532
|
-
}
|
|
533
|
-
// Silence the DB layer's own info logging so --brief stdout stays clean.
|
|
534
|
-
const realConsoleLog = console.log;
|
|
535
|
-
if (briefOnly)
|
|
536
|
-
console.log = () => { };
|
|
537
|
-
const db = openDb();
|
|
538
|
-
try {
|
|
539
|
-
let project = pathFlag ? db.getProject(pathFlag) : undefined;
|
|
540
|
-
if (!project && projectName)
|
|
541
|
-
project = resolveProject(db, projectName) ?? undefined;
|
|
542
|
-
if (!project)
|
|
543
|
-
project = db.getProject(process.cwd());
|
|
544
|
-
if (!project) {
|
|
545
|
-
console.log = realConsoleLog;
|
|
546
|
-
printError('wyrm render: no Wyrm project for this directory (use --path or --project).');
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
const deps = makeRenderDeps(db.getDatabase());
|
|
550
|
-
const stamp = { wyrm_version: readPkg().version ?? 'unknown', compiled_at: new Date().toISOString() };
|
|
551
|
-
if (briefOnly) {
|
|
552
|
-
const plan = buildRenderPlan(deps, project, stamp);
|
|
553
|
-
console.log = realConsoleLog;
|
|
554
|
-
process.stdout.write(plan.sessionBrief + '\n');
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const root = outDir ?? project.path;
|
|
558
|
-
// T039 silent-overwrite guard (security pass #2, critical): an explicit
|
|
559
|
-
// `wyrm render` OVERWRITES the Wyrm-managed region, so it MUST harvest any
|
|
560
|
-
// human edit to that region into the review queue FIRST — UNCONDITIONALLY.
|
|
561
|
-
// The opt-in WYRM_REVERSE_BRIDGE/WYRM_RENDER_WATCH env gates the CONTINUOUS
|
|
562
|
-
// WATCHER (the daemon loop), NOT this one-shot data-loss guard on a direct
|
|
563
|
-
// overwrite. Gating the sweep behind reverseBridgeEnabled() (the default
|
|
564
|
-
// OFF) meant an operator who hand-edited a truth inside the wyrm:render
|
|
565
|
-
// region and re-ran `wyrm render` lost that edit with no review candidate —
|
|
566
|
-
// the cardinal sin the provenance header (render-target.ts) promises against.
|
|
567
|
-
// The edit is never lost: it becomes a review candidate; the render then
|
|
568
|
-
// proceeds (the operator vets the candidate). Fail-safe: a sweep error never
|
|
569
|
-
// blocks the render, but the sweep always RUNS.
|
|
570
|
-
{
|
|
571
|
-
const bridge = await import('./reverse-bridge.js');
|
|
572
|
-
const preBrief = buildRenderPlan(deps, project, stamp);
|
|
573
|
-
const lastBlocks = { 'MEMORY.md': preBrief.memoryMd };
|
|
574
|
-
for (const client of clients) {
|
|
575
|
-
const t = renderForClient(client, preBrief.model, stamp);
|
|
576
|
-
lastBlocks[t.relPath] = t.block;
|
|
577
|
-
}
|
|
578
|
-
try {
|
|
579
|
-
const bridgeDeps = bridge.makeBridgeDeps(db.getDatabase());
|
|
580
|
-
const swept = await bridge.sweepProject(bridgeDeps, { id: project.id, path: project.path }, lastBlocks, { rootDir: root });
|
|
581
|
-
if (swept.added > 0 && !quiet) {
|
|
582
|
-
console.log(` ${icons.warning} harvested ${swept.added} human edit(s) → review queue before overwrite`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
catch { /* fail-safe: guard never blocks a render */ }
|
|
586
|
-
}
|
|
587
|
-
const { plan, writes } = renderToDisk(deps, project, stamp, { rootDir: root, clients, force });
|
|
588
|
-
if (!quiet) {
|
|
589
|
-
printSuccess(`Rendered ${project.name} memory ` +
|
|
590
|
-
`(${plan.model.truths.length} truths, ${plan.model.failures.length} failures, ` +
|
|
591
|
-
`${plan.model.quests.length} quests, ${plan.model.artifacts.length} patterns) to ${root}`);
|
|
592
|
-
for (const w of writes) {
|
|
593
|
-
const mark = w.action === 'created' ? icons.success
|
|
594
|
-
: w.action === 'updated' ? icons.info : icons.warning;
|
|
595
|
-
console.log(` ${mark} ${w.action.padEnd(7)} ${w.path}${w.reason ? ` (${w.reason})` : ''}`);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
finally {
|
|
600
|
-
db.close();
|
|
601
|
-
console.log = realConsoleLog;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* `wyrm reverse-bridge` (v7 F4 — T039) — watch native memory files for human/
|
|
606
|
-
* agent edits, diff them against what `wyrm render` last wrote, and queue the
|
|
607
|
-
* edits as review candidates. Read-only on the watched files; never silently
|
|
608
|
-
* ingests (everything lands needs_review=1) and never overwrites an edit. The
|
|
609
|
-
* last-rendered blocks are reconstructed deterministically from the DB (the same
|
|
610
|
-
* blocks `wyrm render` would emit), so the diff has no separate state to drift.
|
|
611
|
-
*/
|
|
612
|
-
async function cmdReverseBridge(args) {
|
|
613
|
-
const { flags } = parseArgs(args);
|
|
614
|
-
const pathFlag = flags['path'];
|
|
615
|
-
const projectName = flags['project'];
|
|
616
|
-
const rootDir = flags['root'];
|
|
617
|
-
const dryRun = flags['dry-run'] === true || flags.dry === true;
|
|
618
|
-
const clientList = (typeof flags['client'] === 'string' ? flags['client'] : '')
|
|
619
|
-
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
620
|
-
const VALID_CLIENTS = ['claude', 'cursor', 'copilot', 'agents'];
|
|
621
|
-
const clients = clientList.filter((c) => VALID_CLIENTS.includes(c));
|
|
622
|
-
const bridge = await import('./reverse-bridge.js');
|
|
623
|
-
const db = openDb();
|
|
624
|
-
try {
|
|
625
|
-
let project = pathFlag ? db.getProject(pathFlag) : undefined;
|
|
626
|
-
if (!project && projectName)
|
|
627
|
-
project = resolveProject(db, projectName) ?? undefined;
|
|
628
|
-
if (!project)
|
|
629
|
-
project = db.getProject(process.cwd());
|
|
630
|
-
if (!project) {
|
|
631
|
-
printError('wyrm reverse-bridge: no Wyrm project for this directory (use --path or --project).');
|
|
632
|
-
process.exitCode = 1;
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
// Reconstruct the blocks Wyrm last rendered into each watched file, so the
|
|
636
|
-
// diff knows what is Wyrm-owned (overwritable) vs a human edit.
|
|
637
|
-
const deps = makeRenderDeps(db.getDatabase());
|
|
638
|
-
const stamp = { wyrm_version: readPkg().version ?? 'unknown', compiled_at: new Date().toISOString() };
|
|
639
|
-
const plan = buildRenderPlan(deps, project, stamp);
|
|
640
|
-
const lastBlocks = {
|
|
641
|
-
'MEMORY.md': plan.memoryMd,
|
|
642
|
-
};
|
|
643
|
-
for (const client of clients) {
|
|
644
|
-
const t = renderForClient(client, plan.model, stamp);
|
|
645
|
-
lastBlocks[t.relPath] = t.block;
|
|
646
|
-
}
|
|
647
|
-
// For any watched file we did NOT render this run, last block is null → the
|
|
648
|
-
// bridge treats its region as operator-edited (harvest before any overwrite).
|
|
649
|
-
const bridgeDeps = bridge.makeBridgeDeps(db.getDatabase());
|
|
650
|
-
const report = await bridge.sweepProject(bridgeDeps, { id: project.id, path: project.path }, lastBlocks, { dryRun, rootDir });
|
|
651
|
-
printSection(`Reverse bridge ${dryRun ? '(dry run) ' : ''}— ${report.added} candidate(s) queued, ` +
|
|
652
|
-
`${report.skipped} already present (${report.filesWithEdits}/${report.filesScanned} file(s) with edits)`);
|
|
653
|
-
for (const s of report.sample)
|
|
654
|
-
console.log(` ${icons.bullet} ${s}`);
|
|
655
|
-
if (!dryRun && report.added > 0)
|
|
656
|
-
printSuccess('Review with: wyrm review');
|
|
657
|
-
}
|
|
658
|
-
finally {
|
|
659
|
-
db.close();
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
async function cmdImport(args) {
|
|
663
|
-
const { positional, flags } = parseArgs(args.slice(1));
|
|
664
|
-
const subCmd = args[0];
|
|
665
|
-
const projectName = flags['project'];
|
|
666
|
-
if (subCmd === 'git') {
|
|
667
|
-
const n = intFlag(flags['last'], 20);
|
|
668
|
-
const db = openDb();
|
|
669
|
-
const rawDb = db.getDatabase();
|
|
670
|
-
let projectId = null;
|
|
671
|
-
if (projectName) {
|
|
672
|
-
const proj = resolveProject(db, projectName);
|
|
673
|
-
if (!proj) {
|
|
674
|
-
printError(`Project not found: ${projectName}`);
|
|
675
|
-
db.close();
|
|
676
|
-
process.exit(1);
|
|
677
|
-
}
|
|
678
|
-
projectId = proj.id;
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
// Use most recent project
|
|
682
|
-
const recent = rawDb.prepare('SELECT p.* FROM projects p JOIN sessions s ON s.project_id = p.id ORDER BY s.created_at DESC LIMIT 1').get();
|
|
683
|
-
if (recent)
|
|
684
|
-
projectId = recent.id;
|
|
685
|
-
}
|
|
686
|
-
if (!projectId) {
|
|
687
|
-
printError('No project found. Use --project <name>');
|
|
688
|
-
db.close();
|
|
689
|
-
process.exit(1);
|
|
690
|
-
}
|
|
691
|
-
// Run git log
|
|
692
|
-
// Use the unit-separator control byte (%x1f) as the field delimiter — commit
|
|
693
|
-
// subjects/authors can contain '|'. Drop %b (body): it's unused here and its
|
|
694
|
-
// embedded newlines would break the per-line parse below.
|
|
695
|
-
const gitResult = spawnSync('git', ['log', `--pretty=format:%H%x1f%s%x1f%an%x1f%ai`, `-${n}`], {
|
|
696
|
-
cwd: process.cwd(), encoding: 'utf-8', timeout: 10000, shell: false,
|
|
697
|
-
});
|
|
698
|
-
if (gitResult.error || gitResult.status !== 0) {
|
|
699
|
-
printError('git log failed. Make sure you are in a git repository.');
|
|
700
|
-
db.close();
|
|
701
|
-
process.exit(1);
|
|
702
|
-
}
|
|
703
|
-
const lines = gitResult.stdout.split('\n').filter((l) => l.trim());
|
|
704
|
-
const memory = new MemoryArtifacts(rawDb);
|
|
705
|
-
let captured = 0, skipped = 0;
|
|
706
|
-
for (const line of lines) {
|
|
707
|
-
const [, message, author, date] = line.split('\x1f');
|
|
708
|
-
const msg = message ?? '';
|
|
709
|
-
if (/^Merge /i.test(msg) || /^(chore|bump|release|version)/i.test(msg)) {
|
|
710
|
-
skipped++;
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
let kind = 'pattern';
|
|
714
|
-
if (/^fix(\(.+\))?:/i.test(msg))
|
|
715
|
-
kind = 'lesson';
|
|
716
|
-
else if (/^refactor(\(.+\))?:/i.test(msg))
|
|
717
|
-
kind = 'heuristic';
|
|
718
|
-
const typePrefix = msg.split(':')[0] ?? 'commit';
|
|
719
|
-
memory.add(projectId, {
|
|
720
|
-
kind, problem: msg,
|
|
721
|
-
whyItWorked: `Committed by ${author ?? 'unknown'} on ${date ?? 'unknown'}`,
|
|
722
|
-
tags: ['git', 'commit', typePrefix.toLowerCase()],
|
|
723
|
-
confidence: 0.6, needsReview: 1,
|
|
724
|
-
});
|
|
725
|
-
captured++;
|
|
726
|
-
}
|
|
727
|
-
db.close();
|
|
728
|
-
printSuccess(`Imported ${captured} commits (${skipped} skipped). Run ${c.cyan('wyrm review')} to activate.`);
|
|
729
|
-
}
|
|
730
|
-
else if (subCmd === 'rules') {
|
|
731
|
-
const filePath = positional[0];
|
|
732
|
-
const fmt = flags['format'] ?? 'plain';
|
|
733
|
-
if (!filePath) {
|
|
734
|
-
printError('Usage: wyrm import rules <path> [--project <name>] [--format cursorrules|copilot|plain]');
|
|
735
|
-
process.exit(1);
|
|
736
|
-
}
|
|
737
|
-
if (!existsSync(filePath)) {
|
|
738
|
-
printError(`File not found: ${filePath}`);
|
|
739
|
-
process.exit(1);
|
|
740
|
-
}
|
|
741
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
742
|
-
const db = openDb();
|
|
743
|
-
const rawDb = db.getDatabase();
|
|
744
|
-
let projectId = null;
|
|
745
|
-
if (projectName) {
|
|
746
|
-
const proj = resolveProject(db, projectName);
|
|
747
|
-
if (!proj) {
|
|
748
|
-
printError(`Project not found: ${projectName}`);
|
|
749
|
-
db.close();
|
|
750
|
-
process.exit(1);
|
|
751
|
-
}
|
|
752
|
-
projectId = proj.id;
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
const recent = rawDb.prepare('SELECT p.* FROM projects p JOIN sessions s ON s.project_id = p.id ORDER BY s.created_at DESC LIMIT 1').get();
|
|
756
|
-
if (recent)
|
|
757
|
-
projectId = recent.id;
|
|
758
|
-
}
|
|
759
|
-
if (!projectId) {
|
|
760
|
-
printError('No project found. Use --project <name>');
|
|
761
|
-
db.close();
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
const src = filePath.split('/').pop() ?? 'rules';
|
|
765
|
-
const baseTags = ['imported', fmt, src];
|
|
766
|
-
const byHeadings = content.split(/\n(?=#)/);
|
|
767
|
-
const byParagraphs = content.split(/\n\n+/);
|
|
768
|
-
const chunks = (byHeadings.length >= byParagraphs.length ? byHeadings : byParagraphs)
|
|
769
|
-
.map(ch => ch.trim()).filter(ch => ch.length >= 15);
|
|
770
|
-
const memory = new MemoryArtifacts(rawDb);
|
|
771
|
-
const groundTruths = new GroundTruths(rawDb);
|
|
772
|
-
let truthsCreated = 0, artifactsCreated = 0;
|
|
773
|
-
for (const chunk of chunks) {
|
|
774
|
-
if (/\b(always|never|must|use|don't|avoid|prefer)\b/i.test(chunk)) {
|
|
775
|
-
groundTruths.set(projectId, { category: 'constraint', key: chunk.slice(0, 50).replace(/\n/g, ' '), value: chunk, source: src });
|
|
776
|
-
truthsCreated++;
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
memory.add(projectId, { kind: 'heuristic', problem: chunk, tags: baseTags, confidence: 0.7, needsReview: 1 });
|
|
780
|
-
artifactsCreated++;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
db.close();
|
|
784
|
-
printSuccess(`Imported ${truthsCreated} ground truths + ${artifactsCreated} artifacts (pending review).`);
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
printError('Usage: wyrm import git [--project <name>] [--last N]');
|
|
788
|
-
printError(' wyrm import rules <path> [--project <name>] [--format cursorrules|copilot|plain]');
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
async function cmdStats(args) {
|
|
793
|
-
const { flags } = parseArgs(args);
|
|
794
|
-
const projectName = flags['project'];
|
|
795
|
-
const db = openDb();
|
|
796
|
-
const rawDb = db.getDatabase();
|
|
797
|
-
printSection('Wyrm Statistics');
|
|
798
|
-
if (projectName) {
|
|
799
|
-
const proj = resolveProject(db, projectName);
|
|
800
|
-
if (!proj) {
|
|
801
|
-
printError(`Project not found: ${projectName}`);
|
|
802
|
-
db.close();
|
|
803
|
-
process.exit(1);
|
|
804
|
-
}
|
|
805
|
-
const stats = db.getProjectStats(proj.id);
|
|
806
|
-
const rows = [
|
|
807
|
-
['Sessions', String(stats.sessions)],
|
|
808
|
-
['Quests (pending)', String(stats.quests.pending)],
|
|
809
|
-
['Quests (completed)', String(stats.quests.completed)],
|
|
810
|
-
['Data Points', String(stats.dataPoints)],
|
|
811
|
-
];
|
|
812
|
-
console.log(formatTable(['Metric', 'Value'], rows));
|
|
813
|
-
}
|
|
814
|
-
else {
|
|
815
|
-
const stats = db.getStats();
|
|
816
|
-
const rows = [
|
|
817
|
-
['Projects', String(stats.projects)],
|
|
818
|
-
['Sessions', String(stats.sessions)],
|
|
819
|
-
['Quests', String(stats.quests)],
|
|
820
|
-
['Data Points', String(stats.dataPoints)],
|
|
821
|
-
['DB Size', stats.dbSize],
|
|
822
|
-
];
|
|
823
|
-
// Also show memory/truths counts
|
|
824
|
-
const memCount = rawDb.prepare('SELECT COUNT(*) as n FROM memory_artifacts').get().n;
|
|
825
|
-
const truthCount = rawDb.prepare('SELECT COUNT(*) as n FROM ground_truths WHERE is_current = 1').get().n;
|
|
826
|
-
rows.push(['Memories', String(memCount)], ['Ground Truths', String(truthCount)]);
|
|
827
|
-
console.log(formatTable(['Metric', 'Value'], rows));
|
|
828
|
-
}
|
|
829
|
-
db.close();
|
|
830
|
-
}
|
|
831
|
-
async function cmdReview(args) {
|
|
832
|
-
const { flags } = parseArgs(args);
|
|
833
|
-
const projectName = flags['project'];
|
|
834
|
-
const db = openDb();
|
|
835
|
-
const rawDb = db.getDatabase();
|
|
836
|
-
let projectId = null;
|
|
837
|
-
if (projectName) {
|
|
838
|
-
const proj = resolveProject(db, projectName);
|
|
839
|
-
if (!proj) {
|
|
840
|
-
printError(`Project not found: ${projectName}`);
|
|
841
|
-
db.close();
|
|
842
|
-
process.exit(1);
|
|
843
|
-
}
|
|
844
|
-
projectId = proj.id;
|
|
845
|
-
}
|
|
846
|
-
const projectClause = projectId ? `AND project_id = ${projectId}` : '';
|
|
847
|
-
const pending = rawDb.prepare(`
|
|
35
|
+
`).all(s);if(m.length>0){const y=m.map(f=>[`quest:${f.id}`,f.priority,f.title.slice(0,60),f.status]);console.log(M(["ID","Priority","Title","Status"],y))}else console.log(d.dim(" No quests yet."))}t.close()}async function Ae(l){const{positional:i}=x(l),e=i[0];e||(p("Usage: wyrm show <typed-id> (e.g. mem:41, quest:12, truth:7, data:5, session:3)"),process.exit(1));const[s,r]=e.split(":"),t=parseInt(r??"",10);(!s||isNaN(t))&&(p(`Invalid typed ID: ${e}. Format: type:number (e.g. mem:41)`),process.exit(1));const n=E(),a=n.getDatabase();switch(j(`${e}`),s){case"mem":{const o=a.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t);if(!o){p(`Memory artifact ${t} not found`);break}console.log(d.bold("Kind: ")+o.kind),console.log(d.bold("Confidence:")+` ${Math.round(o.confidence*100)}%`),console.log(d.bold("Problem: ")+`
|
|
36
|
+
`+o.problem),o.validated_fix&&console.log(d.bold("Solution: ")+`
|
|
37
|
+
`+o.validated_fix),o.why_it_worked&&console.log(d.bold("Why: ")+`
|
|
38
|
+
`+o.why_it_worked),o.tags&&console.log(d.bold("Tags: ")+o.tags),console.log(d.bold("Created: ")+o.created_at);break}case"quest":{const o=a.prepare("SELECT * FROM quests WHERE id = ?").get(t);if(!o){p(`Quest ${t} not found`);break}console.log(d.bold("Title: ")+o.title),console.log(d.bold("Priority: ")+o.priority),console.log(d.bold("Status: ")+o.status),o.description&&console.log(d.bold("Desc: ")+`
|
|
39
|
+
`+o.description),o.tags&&console.log(d.bold("Tags: ")+o.tags),console.log(d.bold("Created: ")+o.created_at);break}case"truth":{const o=a.prepare("SELECT * FROM ground_truths WHERE id = ?").get(t);if(!o){p(`Ground truth ${t} not found`);break}console.log(d.bold("Category: ")+o.category),console.log(d.bold("Key: ")+o.key),console.log(d.bold("Value: ")+`
|
|
40
|
+
`+o.value),o.rationale&&console.log(d.bold("Rationale:")+`
|
|
41
|
+
`+o.rationale),console.log(d.bold("Active: ")+(o.is_current?"Yes":"No (superseded)")),console.log(d.bold("Created: ")+o.created_at);break}case"data":{const o=a.prepare("SELECT * FROM data_lake WHERE id = ?").get(t);if(!o){p(`Data point ${t} not found`);break}console.log(d.bold("Category: ")+o.category),console.log(d.bold("Key: ")+o.key),console.log(d.bold("Value: ")+`
|
|
42
|
+
`+String(o.value).slice(0,500)),console.log(d.bold("Created: ")+o.created_at);break}case"session":{const o=a.prepare("SELECT * FROM sessions WHERE id = ?").get(t);if(!o){p(`Session ${t} not found`);break}console.log(d.bold("Date: ")+o.date),console.log(d.bold("Objectives: ")+`
|
|
43
|
+
`+o.objectives),o.completed&&console.log(d.bold("Completed: ")+`
|
|
44
|
+
`+o.completed),o.notes&&console.log(d.bold("Notes: ")+`
|
|
45
|
+
`+o.notes);break}default:p(`Unknown type prefix: ${s}. Use mem|quest|truth|data|session`)}n.close()}async function Ne(l){const{positional:i,flags:e}=x(l),s=i[0];s||(p('Usage: wyrm capture "<content>" [--project <name>] [--mode auto|quest|truth|memory]'),process.exit(1));const r=e.project,t=e.mode,n=E(),a=n.getDatabase();let o=null;if(r){const S=P(n,r);S||(p(`Project not found: ${r}`),n.close(),process.exit(1)),o=S.id}let u=Pe(s);t&&t!=="auto"&&(u={type:t,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[t]??t,confidence:100,reasoning:`Mode override: ${t}`});const{type:c,subtype:m,confidence:y,reasoning:f}=u;(c==="quest"||c==="truth"||c==="memory")&&o===null&&(p('A --project is required to capture. Use: wyrm capture "<text>" --project <name>'),n.close(),process.exit(1));let g=0,w="",h=!1;const b=new G(a),_=new de(a);if(c==="quest")g=n.addQuest(o,s.slice(0,200),"","medium").id,w="quest";else if(c==="truth")t!=="truth"&&y<100?(g=b.add(o,{kind:"pattern",problem:s,confidence:y/100,needsReview:1}).id,w="mem",h=!0):(g=_.set(o,{category:"decision",key:s.slice(0,60),value:s}).id,w="truth");else{const S=y>=75;g=b.add(o,{kind:m,problem:s,confidence:y/100,needsReview:S?0:1}).id,w="mem",S||(h=!0)}n.close(),v(`Captured as ${c}: ${m}`),console.log(`${d.dim("Confidence:")} ${y}% | ${d.dim(f)}`),console.log(`${d.dim("ID:")} ${w}:${g}`),h&&console.log(`${L.warning} Stored for review \u2014 run ${d.cyan("wyrm review")} to activate`)}async function Le(l){const{flags:i}=x(l),e=R(i.session,0),s=i.path,r=i.project,t=R(i["max-chars"],6e3),n=i.quiet===!0,a=console.log;console.log=()=>{};const o=E();try{const u=new Ce(o.getDatabase());let c=e;if(c<=0){let f=s?o.getProject(s):void 0;if(!f&&r&&(f=P(o,r)??void 0),f||(f=o.getProject(process.cwd())),!f){n||process.stderr.write(`wyrm rehydrate: no Wyrm project for this directory, nothing to restore.
|
|
46
|
+
`);return}const g=o.getRecentSessions(f.id,1);if(g.length===0){n||process.stderr.write(`wyrm rehydrate: project "${f.name}" has no prior sessions yet.
|
|
47
|
+
`);return}c=g[0].id}const m=u.rehydrate(c);if(!m){n||process.stderr.write(`wyrm rehydrate: session ${c} not found.
|
|
48
|
+
`);return}let y=m.briefing_markdown;t>0&&y.length>t&&(y=y.slice(0,t)+`
|
|
49
|
+
|
|
50
|
+
_... brief truncated at ${t} chars, run \`wyrm show session:${m.session_id}\` for the full record._`),process.stdout.write(y+`
|
|
51
|
+
`)}finally{o.close(),console.log=a}}async function Oe(l){const{flags:i}=x(l),e=i.path,s=i.project,r=i.out,t=i.brief===!0,n=i.force===!0,a=i.quiet===!0,o=(typeof i.client=="string"?i.client:"").split(",").map(g=>g.trim().toLowerCase()).filter(Boolean),u=["claude","cursor","copilot","agents"],c=o.filter(g=>u.includes(g)),m=o.filter(g=>!u.includes(g));m.length>0&&(p(`Unknown --client value(s): ${m.join(", ")} (valid: ${u.join("|")})`),process.exit(1));const y=console.log;t&&(console.log=()=>{});const f=E();try{let g=e?f.getProject(e):void 0;!g&&s&&(g=P(f,s)??void 0),g||(g=f.getProject(process.cwd())),g||(console.log=y,p("wyrm render: no Wyrm project for this directory (use --path or --project)."),process.exit(1));const w=pe(f.getDatabase()),h={wyrm_version:O().version??"unknown",compiled_at:new Date().toISOString()};if(t){const $=se(w,g,h);console.log=y,process.stdout.write($.sessionBrief+`
|
|
52
|
+
`);return}const b=r??g.path;{const $=await import("./reverse-bridge.js"),T=se(w,g,h),Y={"MEMORY.md":T.memoryMd};for(const q of c){const A=ue(q,T.model,h);Y[A.relPath]=A.block}try{const q=$.makeBridgeDeps(f.getDatabase()),A=await $.sweepProject(q,{id:g.id,path:g.path},Y,{rootDir:b});A.added>0&&!a&&console.log(` ${L.warning} harvested ${A.added} human edit(s) \u2192 review queue before overwrite`)}catch{}}const{plan:_,writes:S}=Re(w,g,h,{rootDir:b,clients:c,force:n});if(!a){v(`Rendered ${g.name} memory (${_.model.truths.length} truths, ${_.model.failures.length} failures, ${_.model.quests.length} quests, ${_.model.artifacts.length} patterns) to ${b}`);for(const $ of S){const T=$.action==="created"?L.success:$.action==="updated"?L.info:L.warning;console.log(` ${T} ${$.action.padEnd(7)} ${$.path}${$.reason?` (${$.reason})`:""}`)}}}finally{f.close(),console.log=y}}async function We(l){const{flags:i}=x(l),e=i.path,s=i.project,r=i.root,t=i["dry-run"]===!0||i.dry===!0,n=(typeof i.client=="string"?i.client:"").split(",").map(m=>m.trim().toLowerCase()).filter(Boolean),a=["claude","cursor","copilot","agents"],o=n.filter(m=>a.includes(m)),u=await import("./reverse-bridge.js"),c=E();try{let m=e?c.getProject(e):void 0;if(!m&&s&&(m=P(c,s)??void 0),m||(m=c.getProject(process.cwd())),!m){p("wyrm reverse-bridge: no Wyrm project for this directory (use --path or --project)."),process.exitCode=1;return}const y=pe(c.getDatabase()),f={wyrm_version:O().version??"unknown",compiled_at:new Date().toISOString()},g=se(y,m,f),w={"MEMORY.md":g.memoryMd};for(const _ of o){const S=ue(_,g.model,f);w[S.relPath]=S.block}const h=u.makeBridgeDeps(c.getDatabase()),b=await u.sweepProject(h,{id:m.id,path:m.path},w,{dryRun:t,rootDir:r});j(`Reverse bridge ${t?"(dry run) ":""}\u2014 ${b.added} candidate(s) queued, ${b.skipped} already present (${b.filesWithEdits}/${b.filesScanned} file(s) with edits)`);for(const _ of b.sample)console.log(` ${L.bullet} ${_}`);!t&&b.added>0&&v("Review with: wyrm review")}finally{c.close()}}async function Ye(l){const{positional:i,flags:e}=x(l.slice(1)),s=l[0],r=e.project;if(s==="git"){const t=R(e.last,20),n=E(),a=n.getDatabase();let o=null;if(r){const g=P(n,r);g||(p(`Project not found: ${r}`),n.close(),process.exit(1)),o=g.id}else{const g=a.prepare("SELECT p.* FROM projects p JOIN sessions s ON s.project_id = p.id ORDER BY s.created_at DESC LIMIT 1").get();g&&(o=g.id)}o||(p("No project found. Use --project <name>"),n.close(),process.exit(1));const u=B("git",["log","--pretty=format:%H%x1f%s%x1f%an%x1f%ai",`-${t}`],{cwd:process.cwd(),encoding:"utf-8",timeout:1e4,shell:!1});(u.error||u.status!==0)&&(p("git log failed. Make sure you are in a git repository."),n.close(),process.exit(1));const c=u.stdout.split(`
|
|
53
|
+
`).filter(g=>g.trim()),m=new G(a);let y=0,f=0;for(const g of c){const[,w,h,b]=g.split(""),_=w??"";if(/^Merge /i.test(_)||/^(chore|bump|release|version)/i.test(_)){f++;continue}let S="pattern";/^fix(\(.+\))?:/i.test(_)?S="lesson":/^refactor(\(.+\))?:/i.test(_)&&(S="heuristic");const $=_.split(":")[0]??"commit";m.add(o,{kind:S,problem:_,whyItWorked:`Committed by ${h??"unknown"} on ${b??"unknown"}`,tags:["git","commit",$.toLowerCase()],confidence:.6,needsReview:1}),y++}n.close(),v(`Imported ${y} commits (${f} skipped). Run ${d.cyan("wyrm review")} to activate.`)}else if(s==="rules"){const t=i[0],n=e.format??"plain";t||(p("Usage: wyrm import rules <path> [--project <name>] [--format cursorrules|copilot|plain]"),process.exit(1)),ce(t)||(p(`File not found: ${t}`),process.exit(1));const a=V(t,"utf-8"),o=E(),u=o.getDatabase();let c=null;if(r){const $=P(o,r);$||(p(`Project not found: ${r}`),o.close(),process.exit(1)),c=$.id}else{const $=u.prepare("SELECT p.* FROM projects p JOIN sessions s ON s.project_id = p.id ORDER BY s.created_at DESC LIMIT 1").get();$&&(c=$.id)}c||(p("No project found. Use --project <name>"),o.close(),process.exit(1));const m=t.split("/").pop()??"rules",y=["imported",n,m],f=a.split(/\n(?=#)/),g=a.split(/\n\n+/),w=(f.length>=g.length?f:g).map($=>$.trim()).filter($=>$.length>=15),h=new G(u),b=new de(u);let _=0,S=0;for(const $ of w)/\b(always|never|must|use|don't|avoid|prefer)\b/i.test($)?(b.set(c,{category:"constraint",key:$.slice(0,50).replace(/\n/g," "),value:$,source:m}),_++):(h.add(c,{kind:"heuristic",problem:$,tags:y,confidence:.7,needsReview:1}),S++);o.close(),v(`Imported ${_} ground truths + ${S} artifacts (pending review).`)}else p("Usage: wyrm import git [--project <name>] [--last N]"),p(" wyrm import rules <path> [--project <name>] [--format cursorrules|copilot|plain]"),process.exit(1)}async function qe(l){const{flags:i}=x(l),e=i.project,s=E(),r=s.getDatabase();if(j("Wyrm Statistics"),e){const t=P(s,e);t||(p(`Project not found: ${e}`),s.close(),process.exit(1));const n=s.getProjectStats(t.id),a=[["Sessions",String(n.sessions)],["Quests (pending)",String(n.quests.pending)],["Quests (completed)",String(n.quests.completed)],["Data Points",String(n.dataPoints)]];console.log(M(["Metric","Value"],a))}else{const t=s.getStats(),n=[["Projects",String(t.projects)],["Sessions",String(t.sessions)],["Quests",String(t.quests)],["Data Points",String(t.dataPoints)],["DB Size",t.dbSize]],a=r.prepare("SELECT COUNT(*) as n FROM memory_artifacts").get().n,o=r.prepare("SELECT COUNT(*) as n FROM ground_truths WHERE is_current = 1").get().n;n.push(["Memories",String(a)],["Ground Truths",String(o)]),console.log(M(["Metric","Value"],n))}s.close()}async function Fe(l){const{flags:i}=x(l),e=i.project,s=E(),r=s.getDatabase();let t=null;if(e){const c=P(s,e);c||(p(`Project not found: ${e}`),s.close(),process.exit(1)),t=c.id}const n=t?`AND project_id = ${t}`:"",a=r.prepare(`
|
|
848
54
|
SELECT id, kind, problem FROM memory_artifacts
|
|
849
|
-
WHERE needs_review = 1 ${
|
|
55
|
+
WHERE needs_review = 1 ${n}
|
|
850
56
|
ORDER BY created_at ASC
|
|
851
|
-
`).all();
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
else if (choice === 'r') {
|
|
872
|
-
rawDb.prepare('DELETE FROM memory_artifacts WHERE id = ?').run(item.id);
|
|
873
|
-
console.log(`${icons.cross} Rejected #${item.id}`);
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
console.log(c.dim(` Skipped #${item.id}`));
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
rl.close();
|
|
880
|
-
db.close();
|
|
881
|
-
console.log('\nReview complete.');
|
|
882
|
-
}
|
|
883
|
-
async function cmdSync(args) {
|
|
884
|
-
const { positional, flags } = parseArgs(args);
|
|
885
|
-
const subcommand = positional[0]; // 'export' | 'import' | 'preview'
|
|
886
|
-
if (!subcommand || !['export', 'import', 'preview'].includes(subcommand)) {
|
|
887
|
-
printError('Usage: wyrm sync export --out <path> | wyrm sync import --from <path> | wyrm sync preview --from <path>');
|
|
888
|
-
process.exit(1);
|
|
889
|
-
}
|
|
890
|
-
const { randomBytes, pbkdf2Sync, createCipheriv, createDecipheriv } = await import('crypto');
|
|
891
|
-
const { readFileSync, writeFileSync, copyFileSync, unlinkSync, existsSync, chmodSync } = await import('fs');
|
|
892
|
-
const { homedir: homedirCli } = await import('os');
|
|
893
|
-
const { join: joinCli } = await import('path');
|
|
894
|
-
const BetterSQLite = (await import('better-sqlite3')).default;
|
|
895
|
-
// Get passphrase from env or prompt interactively
|
|
896
|
-
let passphrase = process.env.WYRM_SYNC_PASSPHRASE ?? '';
|
|
897
|
-
if (!passphrase) {
|
|
898
|
-
const rlSync = createInterface({ input: process.stdin, output: process.stdout });
|
|
899
|
-
passphrase = await new Promise(resolve => {
|
|
900
|
-
process.stdout.write('Passphrase: ');
|
|
901
|
-
// Hide input
|
|
902
|
-
if (process.stdin.isTTY)
|
|
903
|
-
process.stdin.setRawMode?.(true);
|
|
904
|
-
rlSync.question('', ans => {
|
|
905
|
-
if (process.stdin.isTTY)
|
|
906
|
-
process.stdin.setRawMode?.(false);
|
|
907
|
-
console.log('');
|
|
908
|
-
rlSync.close();
|
|
909
|
-
resolve(ans);
|
|
910
|
-
});
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
if (!passphrase) {
|
|
914
|
-
printError('Passphrase is required. Set WYRM_SYNC_PASSPHRASE or enter interactively.');
|
|
915
|
-
process.exit(1);
|
|
916
|
-
}
|
|
917
|
-
const wyrmDirCli = joinCli(homedirCli(), '.wyrm');
|
|
918
|
-
const db = openDb();
|
|
919
|
-
if (subcommand === 'export') {
|
|
920
|
-
const outputPath = flags['out'];
|
|
921
|
-
if (!outputPath) {
|
|
922
|
-
printError('--out <path> is required');
|
|
923
|
-
process.exit(1);
|
|
924
|
-
}
|
|
925
|
-
const tempPath = joinCli(wyrmDirCli, 'wyrm_cli_export_temp.db');
|
|
926
|
-
try {
|
|
927
|
-
const rawDb = db.getDatabase();
|
|
928
|
-
if (existsSync(tempPath))
|
|
929
|
-
unlinkSync(tempPath);
|
|
930
|
-
rawDb.prepare('VACUUM INTO ?').run(tempPath);
|
|
931
|
-
const plaintext = readFileSync(tempPath);
|
|
932
|
-
const salt = randomBytes(32);
|
|
933
|
-
const iv = randomBytes(16);
|
|
934
|
-
const key = pbkdf2Sync(passphrase, salt, 600000, 32, 'sha256');
|
|
935
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
936
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
937
|
-
const authTag = cipher.getAuthTag();
|
|
938
|
-
const magic = Buffer.from('WYRM');
|
|
939
|
-
const version = Buffer.alloc(1);
|
|
940
|
-
version.writeUInt8(1, 0);
|
|
941
|
-
const output = Buffer.concat([magic, version, salt, iv, authTag, encrypted]);
|
|
942
|
-
writeFileSync(outputPath, output);
|
|
943
|
-
try {
|
|
944
|
-
chmodSync(outputPath, 0o600);
|
|
945
|
-
}
|
|
946
|
-
catch { /* non-fatal */ }
|
|
947
|
-
try {
|
|
948
|
-
unlinkSync(tempPath);
|
|
949
|
-
}
|
|
950
|
-
catch { /* non-fatal */ }
|
|
951
|
-
const sizeMb = (output.length / (1024 * 1024)).toFixed(2);
|
|
952
|
-
printSuccess(`Exported to ${outputPath} (${sizeMb} MB)`);
|
|
953
|
-
}
|
|
954
|
-
catch (err) {
|
|
955
|
-
try {
|
|
956
|
-
if (existsSync(tempPath))
|
|
957
|
-
unlinkSync(tempPath);
|
|
958
|
-
}
|
|
959
|
-
catch { /* clean up */ }
|
|
960
|
-
printError(`Export failed: ${err}`);
|
|
961
|
-
}
|
|
962
|
-
db.close();
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
const inputPath = flags['from'];
|
|
966
|
-
if (!inputPath) {
|
|
967
|
-
printError('--from <path> is required');
|
|
968
|
-
process.exit(1);
|
|
969
|
-
}
|
|
970
|
-
const fileData = readFileSync(inputPath);
|
|
971
|
-
if (fileData.subarray(0, 4).toString('ascii') !== 'WYRM') {
|
|
972
|
-
printError('Invalid Wyrm snapshot file.');
|
|
973
|
-
process.exit(1);
|
|
974
|
-
}
|
|
975
|
-
const version = fileData.readUInt8(4);
|
|
976
|
-
if (version !== 1) {
|
|
977
|
-
printError(`Unsupported snapshot version: ${version}`);
|
|
978
|
-
process.exit(1);
|
|
979
|
-
}
|
|
980
|
-
const salt = fileData.subarray(5, 37);
|
|
981
|
-
const iv = fileData.subarray(37, 53);
|
|
982
|
-
const authTag = fileData.subarray(53, 69);
|
|
983
|
-
const encrypted = fileData.subarray(69);
|
|
984
|
-
const key = pbkdf2Sync(passphrase, salt, 600000, 32, 'sha256');
|
|
985
|
-
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
986
|
-
decipher.setAuthTag(authTag);
|
|
987
|
-
let decrypted;
|
|
988
|
-
try {
|
|
989
|
-
decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
990
|
-
}
|
|
991
|
-
catch {
|
|
992
|
-
printError('Decryption failed — wrong passphrase or corrupted file.');
|
|
993
|
-
process.exit(1);
|
|
994
|
-
}
|
|
995
|
-
if (subcommand === 'preview') {
|
|
996
|
-
const tempPreview = joinCli(wyrmDirCli, 'wyrm_cli_preview_temp.db');
|
|
997
|
-
if (existsSync(tempPreview))
|
|
998
|
-
unlinkSync(tempPreview);
|
|
999
|
-
writeFileSync(tempPreview, decrypted);
|
|
1000
|
-
try {
|
|
1001
|
-
const previewDb = new BetterSQLite(tempPreview, { readonly: true });
|
|
1002
|
-
const tables = ['projects', 'sessions', 'ground_truths', 'memory_artifacts', 'quests'];
|
|
1003
|
-
printSection('Snapshot Preview');
|
|
1004
|
-
const rows = [];
|
|
1005
|
-
for (const t of tables) {
|
|
1006
|
-
try {
|
|
1007
|
-
const row = previewDb.prepare(`SELECT COUNT(*) as n FROM ${t}`).get();
|
|
1008
|
-
rows.push([t, String(row.n)]);
|
|
1009
|
-
}
|
|
1010
|
-
catch {
|
|
1011
|
-
rows.push([t, '?']);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
console.log(formatTable(['Table', 'Count'], rows));
|
|
1015
|
-
previewDb.close();
|
|
1016
|
-
}
|
|
1017
|
-
catch (err) {
|
|
1018
|
-
printError(`Preview failed: ${err}`);
|
|
1019
|
-
}
|
|
1020
|
-
try {
|
|
1021
|
-
if (existsSync(tempPreview))
|
|
1022
|
-
unlinkSync(tempPreview);
|
|
1023
|
-
}
|
|
1024
|
-
catch { /* clean up */ }
|
|
1025
|
-
db.close();
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
// restore
|
|
1029
|
-
const dbPath = db.getDatabasePath();
|
|
1030
|
-
const rlConfirm = createInterface({ input: process.stdin, output: process.stdout });
|
|
1031
|
-
const answer = await new Promise(resolve => {
|
|
1032
|
-
rlConfirm.question('This will REPLACE your current database. Type CONFIRM to proceed: ', resolve);
|
|
1033
|
-
});
|
|
1034
|
-
rlConfirm.close();
|
|
1035
|
-
if (answer.trim() !== 'CONFIRM') {
|
|
1036
|
-
console.log(c.dim('Aborted.'));
|
|
1037
|
-
db.close();
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1041
|
-
const backupPath = `${dbPath}.backup.${timestamp}`;
|
|
1042
|
-
copyFileSync(dbPath, backupPath);
|
|
1043
|
-
printSuccess(`Backed up to ${backupPath}`);
|
|
1044
|
-
const tempRestore = joinCli(wyrmDirCli, 'wyrm_cli_restore_temp.db');
|
|
1045
|
-
writeFileSync(tempRestore, decrypted);
|
|
1046
|
-
db.getDatabase().close();
|
|
1047
|
-
copyFileSync(tempRestore, dbPath);
|
|
1048
|
-
// Drop stale WAL/SHM sidecars from the OLD database — leaving them lets
|
|
1049
|
-
// SQLite replay an unrelated WAL over the freshly restored file and corrupt
|
|
1050
|
-
// it (Wyrm runs in WAL mode).
|
|
1051
|
-
for (const suffix of ['-wal', '-shm']) {
|
|
1052
|
-
try {
|
|
1053
|
-
if (existsSync(dbPath + suffix))
|
|
1054
|
-
unlinkSync(dbPath + suffix);
|
|
1055
|
-
}
|
|
1056
|
-
catch { /* non-fatal */ }
|
|
1057
|
-
}
|
|
1058
|
-
try {
|
|
1059
|
-
unlinkSync(tempRestore);
|
|
1060
|
-
}
|
|
1061
|
-
catch { /* clean up */ }
|
|
1062
|
-
printSuccess(`Restored from ${inputPath}. Backup at ${backupPath}`);
|
|
1063
|
-
}
|
|
1064
|
-
async function cmdPrune(args) {
|
|
1065
|
-
const { flags } = parseArgs(args);
|
|
1066
|
-
const projectName = flags['project'];
|
|
1067
|
-
const pathFlag = flags['path'];
|
|
1068
|
-
const minConf = floatFlag(flags['min-confidence'], 0.3);
|
|
1069
|
-
const olderThanDays = intFlag(flags['older-than'], 90);
|
|
1070
|
-
const noDryRun = flags['no-dry-run'] === true;
|
|
1071
|
-
const assumeYes = flags['yes'] === true; // skip the interactive CONFIRM (for the SessionEnd auto-prune hook)
|
|
1072
|
-
const db = openDb();
|
|
1073
|
-
const rawDb = db.getDatabase();
|
|
1074
|
-
// Scope resolution: exact --path first (what the auto-prune hook passes), then
|
|
1075
|
-
// --project name. Exact-path scoping prevents a loose name LIKE match from
|
|
1076
|
-
// deleting a *different* project's artifacts. No flag → global prune.
|
|
1077
|
-
let projectId = null;
|
|
1078
|
-
if (pathFlag || projectName) {
|
|
1079
|
-
const proj = pathFlag ? db.getProject(pathFlag) : resolveProject(db, projectName);
|
|
1080
|
-
if (!proj) {
|
|
1081
|
-
printError(`Project not found: ${pathFlag ?? projectName}`);
|
|
1082
|
-
db.close();
|
|
1083
|
-
process.exit(1);
|
|
1084
|
-
}
|
|
1085
|
-
projectId = proj.id;
|
|
1086
|
-
}
|
|
1087
|
-
// Candidate selection + deletion live in MemoryArtifacts.pruneStale (unit
|
|
1088
|
-
// tested for exact project scoping). The CLI keeps the table render + confirm.
|
|
1089
|
-
const memory = new MemoryArtifacts(rawDb);
|
|
1090
|
-
const { candidates } = memory.pruneStale({ projectId, minConfidence: minConf, olderThanDays, dryRun: true });
|
|
1091
|
-
printSection(`Prune Candidates${noDryRun ? ' (LIVE DELETE)' : ' (dry-run)'}`);
|
|
1092
|
-
if (candidates.length === 0) {
|
|
1093
|
-
console.log(c.dim(' No artifacts match prune criteria.'));
|
|
1094
|
-
db.close();
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
const rows = candidates.map(r => [
|
|
1098
|
-
String(r.id), r.kind, r.problem.slice(0, 60),
|
|
1099
|
-
(r.confidence * 100).toFixed(0) + '%',
|
|
1100
|
-
r.last_accessed_at ?? 'never',
|
|
1101
|
-
]);
|
|
1102
|
-
console.log(formatTable(['ID', 'Kind', 'Problem', 'Conf', 'Last Accessed'], rows));
|
|
1103
|
-
console.log(`\nTotal: ${candidates.length} candidate(s)`);
|
|
1104
|
-
if (!noDryRun) {
|
|
1105
|
-
console.log(c.dim('\nThis is a dry run. Use --no-dry-run to delete (confirm each ID).'));
|
|
1106
|
-
db.close();
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
// --yes bypasses the prompt for non-interactive callers (the SessionEnd
|
|
1110
|
-
// auto-prune hook). Still bounded by the same conservative filters above:
|
|
1111
|
-
// low confidence, stale, already-reviewed, non-superseding artifacts only.
|
|
1112
|
-
if (!assumeYes) {
|
|
1113
|
-
const rlPrune = createInterface({ input: process.stdin, output: process.stdout });
|
|
1114
|
-
const answer = await new Promise(resolve => {
|
|
1115
|
-
rlPrune.question(`\nDelete these ${candidates.length} artifact(s)? Type CONFIRM to proceed: `, resolve);
|
|
1116
|
-
});
|
|
1117
|
-
rlPrune.close();
|
|
1118
|
-
if (answer.trim() !== 'CONFIRM') {
|
|
1119
|
-
console.log(c.dim('Aborted.'));
|
|
1120
|
-
db.close();
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
// Delete exactly the candidate set we showed (and confirmed), not a fresh
|
|
1125
|
-
// re-select, so the deleted set always matches the displayed set.
|
|
1126
|
-
const deleted = memory.deleteArtifacts(candidates.map(c => c.id));
|
|
1127
|
-
printSuccess(`Deleted ${deleted} artifact(s).`);
|
|
1128
|
-
db.close();
|
|
1129
|
-
}
|
|
1130
|
-
// ==================== v7 F3 (T023) — CLI-EXILE SUBCOMMANDS ====================
|
|
1131
|
-
// The 22 CLI-exiled MCP names (src/deprecations.ts CLI_EXILE_TOOLS) redirect
|
|
1132
|
-
// to `wyrm` subcommands in 7.0. `setup|embed|sync|cloud|vault|harvest|review|
|
|
1133
|
-
// grove` already existed; the commands below ship the genuinely missing ones.
|
|
1134
|
-
// Every command WRAPS the existing src module the MCP case ran — never a
|
|
1135
|
-
// reimplementation (license.ts, hours.ts, agent-daemon.ts, version-check.ts,
|
|
1136
|
-
// autoconfig.ts, migrate-prompt.ts, maintenance.ts, reindex.ts, vectors.ts).
|
|
1137
|
-
/** Read all of stdin (for `wyrm activate` / piped license JSON). */
|
|
1138
|
-
function readStdin() {
|
|
1139
|
-
return readFileSync(0, 'utf-8').trim();
|
|
1140
|
-
}
|
|
1141
|
-
/** `wyrm license` — show the current license status (wraps license.ts). */
|
|
1142
|
-
async function cmdLicense() {
|
|
1143
|
-
const { initializeLicense, getLicenseInfo, getTier } = await import('./license.js');
|
|
1144
|
-
initializeLicense();
|
|
1145
|
-
const info = getLicenseInfo();
|
|
1146
|
-
printSection('Wyrm License');
|
|
1147
|
-
const rows = [
|
|
1148
|
-
['Tier', getTier()],
|
|
1149
|
-
['Status', info.valid ? 'valid' : 'free tier (no license key)'],
|
|
1150
|
-
];
|
|
1151
|
-
if (info.valid && info.key) {
|
|
1152
|
-
rows.push(['Key', info.key]);
|
|
1153
|
-
rows.push(['Issued to', info.issuedTo ?? 'unknown']);
|
|
1154
|
-
rows.push(['Expires', info.expiresAt ? new Date(info.expiresAt).toLocaleDateString() : 'never']);
|
|
1155
|
-
}
|
|
1156
|
-
rows.push(['Features', info.features.join(', ') || '(free)']);
|
|
1157
|
-
console.log(formatTable(['Field', 'Value'], rows));
|
|
1158
|
-
if (!info.valid) {
|
|
1159
|
-
console.log(c.dim('\n Activate with: wyrm activate <license.json | key> · buy at https://ghosts.lk/wyrm'));
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
/** `wyrm activate <key|path>` — activate a license (wraps license.ts). */
|
|
1163
|
-
async function cmdLogin() {
|
|
1164
|
-
// Device-authorization flow against the identity hub → a free, account-bound,
|
|
1165
|
-
// short-lived signed license saved to ~/.wyrm/license.json. This is what
|
|
1166
|
-
// satisfies the activation gate on account-required (official) builds.
|
|
1167
|
-
const base = (process.env.WYRM_ACCOUNT_URL ?? 'https://account.ghosts.lk').replace(/\/$/, '');
|
|
1168
|
-
let start;
|
|
1169
|
-
try {
|
|
1170
|
-
const r = await fetch(`${base}/api/v1/cli/auth/start`, { method: 'POST' });
|
|
1171
|
-
if (!r.ok)
|
|
1172
|
-
throw new Error(`HTTP ${r.status}`);
|
|
1173
|
-
start = await r.json();
|
|
1174
|
-
}
|
|
1175
|
-
catch (e) {
|
|
1176
|
-
printError(`Couldn't reach ${base} (${e instanceof Error ? e.message : 'network error'}).`);
|
|
1177
|
-
process.exitCode = 1;
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
const url = start.verification_uri_complete || start.verification_uri || `${base}/cli`;
|
|
1181
|
-
console.log(`\n ${c.cyan('Sign in to activate Wyrm (free):')}`);
|
|
1182
|
-
console.log(` 1. Open ${c.cyan(url)}`);
|
|
1183
|
-
console.log(` 2. Approve the code ${c.cyan(start.user_code)}`);
|
|
1184
|
-
console.log(c.dim('\n Waiting for approval… (Ctrl-C to cancel)'));
|
|
1185
|
-
const interval = (start.interval ?? 3) * 1000;
|
|
1186
|
-
const deadline = Date.now() + (start.expires_in ?? 600) * 1000;
|
|
1187
|
-
let token = '';
|
|
1188
|
-
while (Date.now() < deadline) {
|
|
1189
|
-
await new Promise((res) => setTimeout(res, interval));
|
|
1190
|
-
try {
|
|
1191
|
-
const pr = await fetch(`${base}/api/v1/cli/auth/poll`, {
|
|
1192
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1193
|
-
body: JSON.stringify({ device_code: start.device_code }),
|
|
1194
|
-
});
|
|
1195
|
-
const pj = await pr.json();
|
|
1196
|
-
if (pj.status === 'approved' && pj.token) {
|
|
1197
|
-
token = pj.token;
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
if (pj.status === 'denied' || pj.error === 'expired') {
|
|
1201
|
-
printError('Login was denied or the code expired. Run `wyrm login` again.');
|
|
1202
|
-
process.exitCode = 1;
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
catch { /* transient — keep polling */ }
|
|
1207
|
-
}
|
|
1208
|
-
if (!token) {
|
|
1209
|
-
printError('Login timed out. Run `wyrm login` again.');
|
|
1210
|
-
process.exitCode = 1;
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
let signedStr;
|
|
1214
|
-
try {
|
|
1215
|
-
const lr = await fetch(`${base}/api/v1/license/free`, { method: 'POST', headers: { authorization: `Bearer ${token}` } });
|
|
1216
|
-
if (!lr.ok) {
|
|
1217
|
-
const ej = await lr.json().catch(() => ({}));
|
|
1218
|
-
printError(`Activation failed (${lr.status}): ${ej.hint || ej.error || 'unknown error'}`);
|
|
1219
|
-
process.exitCode = 1;
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
signedStr = JSON.stringify(await lr.json());
|
|
1223
|
-
}
|
|
1224
|
-
catch (e) {
|
|
1225
|
-
printError(`Activation request failed (${e instanceof Error ? e.message : 'network error'}).`);
|
|
1226
|
-
process.exitCode = 1;
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
const { activateLicense } = await import('./license.js');
|
|
1230
|
-
const result = activateLicense(signedStr);
|
|
1231
|
-
if (result.valid) {
|
|
1232
|
-
printSuccess(`Signed in & activated — ${result.tier} tier (expires ${result.expiresAt ?? 'never'}).`);
|
|
1233
|
-
console.log(c.dim(' Restart the Wyrm MCP server / daemon to apply.'));
|
|
1234
|
-
}
|
|
1235
|
-
else {
|
|
1236
|
-
printError(`Activation failed: ${result.error ?? 'unknown error'}`);
|
|
1237
|
-
process.exitCode = 1;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
async function cmdActivate(args) {
|
|
1241
|
-
const { positional } = parseArgs(args);
|
|
1242
|
-
const input = positional[0];
|
|
1243
|
-
let licenseStr;
|
|
1244
|
-
if (input && existsSync(input)) {
|
|
1245
|
-
licenseStr = readFileSync(input, 'utf-8');
|
|
1246
|
-
}
|
|
1247
|
-
else if (input) {
|
|
1248
|
-
licenseStr = input;
|
|
1249
|
-
}
|
|
1250
|
-
else if (!process.stdin.isTTY) {
|
|
1251
|
-
licenseStr = readStdin();
|
|
1252
|
-
}
|
|
1253
|
-
else {
|
|
1254
|
-
printError('Usage: wyrm activate <license.json path | license JSON> (or pipe the JSON on stdin)');
|
|
1255
|
-
process.exit(1);
|
|
1256
|
-
}
|
|
1257
|
-
const { activateLicense } = await import('./license.js');
|
|
1258
|
-
try {
|
|
1259
|
-
const result = activateLicense(licenseStr);
|
|
1260
|
-
if (result.valid) {
|
|
1261
|
-
printSuccess(`License activated — ${result.tier} tier (${result.features.join(', ')})`);
|
|
1262
|
-
console.log(c.dim(' Restart Wyrm (MCP server / daemon) to apply all features.'));
|
|
1263
|
-
}
|
|
1264
|
-
else {
|
|
1265
|
-
printError(`License activation failed: ${result.error ?? 'unknown error'}`);
|
|
1266
|
-
process.exitCode = 1;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
catch {
|
|
1270
|
-
printError('Invalid license format. Please verify your license key.');
|
|
1271
|
-
process.exitCode = 1;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
/** `wyrm maintenance [--vacuum] [--archive-days N]` — wraps maintenance.ts
|
|
1275
|
-
* (the SAME runMaintenance the wyrm_maintenance MCP tool runs). */
|
|
1276
|
-
async function cmdMaintenance(args) {
|
|
1277
|
-
const { flags } = parseArgs(args);
|
|
1278
|
-
const { runMaintenance } = await import('./maintenance.js');
|
|
1279
|
-
const { FailurePatterns } = await import('./failure-patterns.js');
|
|
1280
|
-
const { SessionSeen } = await import('./session-seen.js');
|
|
1281
|
-
const { AgentPresence } = await import('./presence.js');
|
|
1282
|
-
const db = openDb();
|
|
1283
|
-
try {
|
|
1284
|
-
const raw = db.getDatabase();
|
|
1285
|
-
const archiveDays = intFlag(flags['archive-days'], 0);
|
|
1286
|
-
const report = runMaintenance(
|
|
1287
|
-
// v7 F3 (T029): + presence — stale-claim eviction rides maintenance.
|
|
1288
|
-
{ db, sessionSeen: new SessionSeen(raw), failures: new FailurePatterns(raw), presence: new AgentPresence(raw) }, { vacuum: flags.vacuum === true, archiveDays: archiveDays > 0 ? archiveDays : undefined });
|
|
1289
|
-
printSection('Maintenance complete');
|
|
1290
|
-
for (const line of report.lines)
|
|
1291
|
-
console.log(` - ${line}`);
|
|
1292
|
-
console.log(c.dim(`\n Database size: ${report.dbSize}`));
|
|
1293
|
-
}
|
|
1294
|
-
finally {
|
|
1295
|
-
db.close();
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
/** `wyrm index <setup|rebuild|status>` — vector search ops (wraps vectors.ts +
|
|
1299
|
-
* reindex.ts — the SAME loop the wyrm_reindex MCP tool runs). */
|
|
1300
|
-
async function cmdIndexVectors(args) {
|
|
1301
|
-
const { positional, flags } = parseArgs(args);
|
|
1302
|
-
const sub = positional[0] ?? 'status';
|
|
1303
|
-
const { createVectorStore } = await import('./vectors.js');
|
|
1304
|
-
const providerName = flags.provider
|
|
1305
|
-
?? process.env.WYRM_VECTOR_PROVIDER ?? 'auto';
|
|
1306
|
-
const providerConfig = {
|
|
1307
|
-
provider: providerName,
|
|
1308
|
-
model: flags.model,
|
|
1309
|
-
apiKey: flags['api-key'] ?? process.env.OPENAI_API_KEY,
|
|
1310
|
-
ollamaUrl: flags['ollama-url'],
|
|
1311
|
-
};
|
|
1312
|
-
if (sub === 'setup') {
|
|
1313
|
-
const { createProvider } = await import('./providers/embedding-provider.js');
|
|
1314
|
-
const provider = createProvider(providerConfig);
|
|
1315
|
-
const ready = await provider.isReady();
|
|
1316
|
-
if (!ready && providerConfig.provider !== 'none') {
|
|
1317
|
-
printError(`Provider not ready: ${provider.name}. Check the configuration and try again.`);
|
|
1318
|
-
process.exitCode = 1;
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
printSuccess(`Vector provider verified: ${provider.name} (model ${provider.model}, ${provider.dimensions}d)`);
|
|
1322
|
-
console.log(c.dim(' The MCP server reads WYRM_VECTOR_PROVIDER (and provider-specific env) at boot —'));
|
|
1323
|
-
console.log(c.dim(` set WYRM_VECTOR_PROVIDER=${provider.name === 'none' ? 'none' : providerConfig.provider} in the server env, then: wyrm index rebuild`));
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
const db = openDb();
|
|
1327
|
-
try {
|
|
1328
|
-
const raw = db.getDatabase();
|
|
1329
|
-
const store = createVectorStore(providerConfig, raw);
|
|
1330
|
-
if (sub === 'status') {
|
|
1331
|
-
const stats = store.getStats();
|
|
1332
|
-
printSection('Vector index');
|
|
1333
|
-
const rows = [
|
|
1334
|
-
['Provider', stats.provider],
|
|
1335
|
-
['Model', stats.model],
|
|
1336
|
-
['Vectors', String(stats.total)],
|
|
1337
|
-
...Object.entries(stats.byType).map(([t, n]) => [` ${t}`, String(n)]),
|
|
1338
|
-
];
|
|
1339
|
-
console.log(formatTable(['Field', 'Value'], rows));
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (sub === 'rebuild') {
|
|
1343
|
-
const { reindexProjects } = await import('./reindex.js');
|
|
1344
|
-
const dryRun = flags['dry-run'] === true;
|
|
1345
|
-
const projRef = flags.project;
|
|
1346
|
-
let projectIds;
|
|
1347
|
-
if (projRef) {
|
|
1348
|
-
const proj = db.getProject(projRef) ?? resolveProject(db, projRef);
|
|
1349
|
-
if (!proj) {
|
|
1350
|
-
printError(`Project not found: ${projRef}`);
|
|
1351
|
-
process.exitCode = 1;
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
projectIds = [proj.id];
|
|
1355
|
-
}
|
|
1356
|
-
else {
|
|
1357
|
-
projectIds = db.getAllProjects(1000).map((p) => p.id);
|
|
1358
|
-
}
|
|
1359
|
-
const { indexed, skipped } = await reindexProjects(raw, store, projectIds, {
|
|
1360
|
-
dryRun,
|
|
1361
|
-
onError: (message, context) => printError(`${message}: ${JSON.stringify(context)}`),
|
|
1362
|
-
});
|
|
1363
|
-
printSuccess(`Reindex ${dryRun ? '(dry run) ' : ''}— ${projectIds.length} project(s), ${indexed} indexed, ${skipped} skipped`);
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
printError('Usage: wyrm index <setup|rebuild|status> [--provider auto|local|ollama|openai|none] [--model M] [--project P] [--dry-run]');
|
|
1367
|
-
process.exitCode = 1;
|
|
1368
|
-
}
|
|
1369
|
-
finally {
|
|
1370
|
-
db.close();
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
/** `wyrm update [--check] [--force]` — wraps version-check.ts; the update
|
|
1374
|
-
* itself is the same `npm install -g wyrm-mcp@latest` wyrm_self_update ran. */
|
|
1375
|
-
async function cmdUpdate(args) {
|
|
1376
|
-
const { flags } = parseArgs(args);
|
|
1377
|
-
const { getUpdateStatus } = await import('./version-check.js');
|
|
1378
|
-
const current = readPkg().version ?? '0.0.0';
|
|
1379
|
-
const db = openDb();
|
|
1380
|
-
let status;
|
|
1381
|
-
try {
|
|
1382
|
-
status = await getUpdateStatus(db.getDatabase(), current, { force: flags.force === true || flags.check === true });
|
|
1383
|
-
}
|
|
1384
|
-
finally {
|
|
1385
|
-
db.close();
|
|
1386
|
-
}
|
|
1387
|
-
printSection('Wyrm update');
|
|
1388
|
-
console.log(formatTable(['Field', 'Value'], [
|
|
1389
|
-
['Current', status.current],
|
|
1390
|
-
['Latest', status.latest ?? 'unknown (offline?)'],
|
|
1391
|
-
['Update available', status.updateAvailable ? 'yes' : 'no'],
|
|
1392
|
-
['Checked', `${status.checkedAt} (${status.source})`],
|
|
1393
|
-
]));
|
|
1394
|
-
if (flags.check === true)
|
|
1395
|
-
return;
|
|
1396
|
-
if (!status.updateAvailable && flags.force !== true) {
|
|
1397
|
-
console.log(c.dim('\n Already up to date. (Use --force to reinstall anyway.)'));
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
console.log(c.dim('\n Running: npm install -g wyrm-mcp@latest\n'));
|
|
1401
|
-
const r = spawnSync('npm', ['install', '-g', 'wyrm-mcp@latest'], { stdio: 'inherit', shell: false });
|
|
1402
|
-
if (r.status === 0)
|
|
1403
|
-
printSuccess('Updated. Restart your MCP clients to pick up the new binary.');
|
|
1404
|
-
else {
|
|
1405
|
-
printError(`npm install exited with ${r.status ?? 'unknown'}`);
|
|
1406
|
-
process.exitCode = r.status ?? 1;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
/** `wyrm prompt <inject|migrate>` — wraps autoconfig.ts / migrate-prompt.ts. */
|
|
1410
|
-
async function cmdPrompt(args) {
|
|
1411
|
-
const { positional, flags } = parseArgs(args);
|
|
1412
|
-
const sub = positional[0];
|
|
1413
|
-
const projectPath = flags.project ?? process.cwd();
|
|
1414
|
-
if (sub === 'inject') {
|
|
1415
|
-
const { injectSystemPrompt } = await import('./autoconfig.js');
|
|
1416
|
-
const clients = typeof flags.clients === 'string' ? flags.clients.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
1417
|
-
const result = injectSystemPrompt(projectPath, clients);
|
|
1418
|
-
printSection('System prompt injection');
|
|
1419
|
-
for (const f of result.injected)
|
|
1420
|
-
console.log(` + ${f}`);
|
|
1421
|
-
for (const s of result.skipped)
|
|
1422
|
-
console.log(c.dim(` o skipped (unknown client): ${s}`));
|
|
1423
|
-
for (const e of result.errors)
|
|
1424
|
-
printError(e);
|
|
1425
|
-
if (result.injected.length > 0) {
|
|
1426
|
-
printSuccess('AI clients in this project will now call wyrm_session_prime at conversation start.');
|
|
1427
|
-
}
|
|
1428
|
-
if (result.errors.length > 0)
|
|
1429
|
-
process.exitCode = 1;
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
if (sub === 'migrate') {
|
|
1433
|
-
const { migrateProject, renderMigrationReport } = await import('./migrate-prompt.js');
|
|
1434
|
-
const { WYRM_INJECT_BLOCK } = await import('./autoconfig.js');
|
|
1435
|
-
const apply = flags.apply === true;
|
|
1436
|
-
const results = migrateProject({ projectPath, newBlock: WYRM_INJECT_BLOCK, apply });
|
|
1437
|
-
console.log(renderMigrationReport(results, apply));
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
printError('Usage: wyrm prompt inject [--project <path>] [--clients copilot,cursor] | wyrm prompt migrate [--project <path>] [--apply]');
|
|
1441
|
-
process.exitCode = 1;
|
|
1442
|
-
}
|
|
1443
|
-
/** `wyrm hours report --from <date> --to <date>` — wraps hours.ts HourLedger. */
|
|
1444
|
-
async function cmdHours(args) {
|
|
1445
|
-
const { positional, flags } = parseArgs(args);
|
|
1446
|
-
const sub = positional[0] ?? 'report';
|
|
1447
|
-
if (sub !== 'report') {
|
|
1448
|
-
printError('Usage: wyrm hours report --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--session-hours H] [--json]');
|
|
1449
|
-
process.exit(1);
|
|
1450
|
-
}
|
|
1451
|
-
const from = flags.from;
|
|
1452
|
-
const to = flags.to;
|
|
1453
|
-
if (!from || !to) {
|
|
1454
|
-
printError('--from and --to are required (YYYY-MM-DD)');
|
|
1455
|
-
process.exit(1);
|
|
1456
|
-
}
|
|
1457
|
-
const { HourLedger } = await import('./hours.js');
|
|
1458
|
-
const db = openDb();
|
|
1459
|
-
try {
|
|
1460
|
-
const projectName = flags.project;
|
|
1461
|
-
const proj = projectName ? resolveProject(db, projectName) : null;
|
|
1462
|
-
if (projectName && !proj) {
|
|
1463
|
-
printError(`Project not found: ${projectName}`);
|
|
1464
|
-
process.exitCode = 1;
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
const report = new HourLedger(db.getDatabase()).report({
|
|
1468
|
-
range_start: from,
|
|
1469
|
-
range_end: to,
|
|
1470
|
-
project_id: proj?.id,
|
|
1471
|
-
default_session_hours: floatFlag(flags['session-hours'], 1.0),
|
|
1472
|
-
});
|
|
1473
|
-
if (flags.json === true) {
|
|
1474
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
printSection(`Hours ${report.range.start} → ${report.range.end}`);
|
|
1478
|
-
if (report.by_project.length === 0) {
|
|
1479
|
-
console.log(c.dim(' No sessions in range.'));
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
console.log(formatTable(['Project', 'Sessions', 'Hours'], report.by_project.map((p) => [p.project_name, String(p.session_count), p.hours.toFixed(2)])));
|
|
1483
|
-
console.log(`\n Total: ${report.total_hours.toFixed(2)}h across ${report.entries.length} session(s)` +
|
|
1484
|
-
(report.estimated_sessions > 0 ? c.dim(` (${report.estimated_sessions} estimated)`) : ''));
|
|
1485
|
-
}
|
|
1486
|
-
finally {
|
|
1487
|
-
db.close();
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
/** `wyrm invoice generate --client <name> --rate <usd/h> --from --to` —
|
|
1491
|
-
* wraps hours.ts HourLedger.invoice (markdown to stdout or --out file). */
|
|
1492
|
-
async function cmdInvoice(args) {
|
|
1493
|
-
const { positional, flags } = parseArgs(args);
|
|
1494
|
-
const sub = positional[0] ?? 'generate';
|
|
1495
|
-
const client = flags.client;
|
|
1496
|
-
const rate = floatFlag(flags.rate, NaN);
|
|
1497
|
-
const from = flags.from;
|
|
1498
|
-
const to = flags.to;
|
|
1499
|
-
if (sub !== 'generate' || !client || !Number.isFinite(rate) || !from || !to) {
|
|
1500
|
-
printError('Usage: wyrm invoice generate --client <name> --rate <usd/hour> --from YYYY-MM-DD --to YYYY-MM-DD' +
|
|
1501
|
-
' [--project <name>] [--number INV-X] [--currency USD] [--notes "…"] [--out <path>]');
|
|
1502
|
-
process.exit(1);
|
|
1503
|
-
}
|
|
1504
|
-
const { HourLedger } = await import('./hours.js');
|
|
1505
|
-
const db = openDb();
|
|
1506
|
-
try {
|
|
1507
|
-
const projectName = flags.project;
|
|
1508
|
-
const proj = projectName ? resolveProject(db, projectName) : null;
|
|
1509
|
-
if (projectName && !proj) {
|
|
1510
|
-
printError(`Project not found: ${projectName}`);
|
|
1511
|
-
process.exitCode = 1;
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
const md = new HourLedger(db.getDatabase()).invoice({
|
|
1515
|
-
client_name: client,
|
|
1516
|
-
hourly_rate_usd: rate,
|
|
1517
|
-
range_start: from,
|
|
1518
|
-
range_end: to,
|
|
1519
|
-
project_id: proj?.id,
|
|
1520
|
-
invoice_number: flags.number,
|
|
1521
|
-
currency: flags.currency,
|
|
1522
|
-
notes: flags.notes,
|
|
1523
|
-
business_name: flags['business-name'],
|
|
1524
|
-
business_address: flags['business-address'],
|
|
1525
|
-
business_contact: flags['business-contact'],
|
|
1526
|
-
client_address: flags['client-address'],
|
|
1527
|
-
default_session_hours: floatFlag(flags['session-hours'], 1.0),
|
|
1528
|
-
});
|
|
1529
|
-
const out = flags.out;
|
|
1530
|
-
if (out) {
|
|
1531
|
-
const { writeFileSync } = await import('node:fs');
|
|
1532
|
-
writeFileSync(out, md + '\n', 'utf-8');
|
|
1533
|
-
printSuccess(`Invoice written to ${out}`);
|
|
1534
|
-
}
|
|
1535
|
-
else {
|
|
1536
|
-
process.stdout.write(md + '\n');
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
finally {
|
|
1540
|
-
db.close();
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
/** `wyrm agent <init|status|stop|restart>` — wraps agent-daemon.ts. */
|
|
1544
|
-
async function cmdAgent(args) {
|
|
1545
|
-
const { positional, flags } = parseArgs(args);
|
|
1546
|
-
const sub = positional[0] ?? 'status';
|
|
1547
|
-
const { AgentDaemon } = await import('./agent-daemon.js');
|
|
1548
|
-
const db = openDb();
|
|
1549
|
-
try {
|
|
1550
|
-
const daemon = new AgentDaemon(db.getDatabase());
|
|
1551
|
-
switch (sub) {
|
|
1552
|
-
case 'init':
|
|
1553
|
-
case 'start': {
|
|
1554
|
-
const interval = Math.max(10, Math.min(intFlag(flags.interval, 600), 86400));
|
|
1555
|
-
const r = daemon.start({
|
|
1556
|
-
interval_seconds: interval,
|
|
1557
|
-
max_steps: flags['max-steps'] !== undefined ? intFlag(flags['max-steps'], 0) || undefined : undefined,
|
|
1558
|
-
project_path: flags.project,
|
|
1559
|
-
verbose: flags.verbose === true,
|
|
1560
|
-
});
|
|
1561
|
-
if (!r.ok) {
|
|
1562
|
-
printError(`Agent init failed: ${r.error}`);
|
|
1563
|
-
process.exitCode = 1;
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
const s = r.status;
|
|
1567
|
-
printSuccess(`Agent ${s.pid != null && s.pid !== r.pid ? 'already running' : 'started'} — pid ${s.pid}`);
|
|
1568
|
-
console.log(` Interval: ${interval}s · Active goals: ${s.active_goals} · Total iterations: ${s.total_iterations}`);
|
|
1569
|
-
console.log(c.dim(` Log: ${s.log_file}`));
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
case 'status': {
|
|
1573
|
-
const s = daemon.status();
|
|
1574
|
-
printSection('Wyrm agent');
|
|
1575
|
-
console.log(s.running
|
|
1576
|
-
? ` RUNNING — pid ${s.pid}${s.started_at ? ` (since ${s.started_at})` : ''}`
|
|
1577
|
-
: ' NOT RUNNING. Start it with: wyrm agent init');
|
|
1578
|
-
console.log(` Active goals: ${s.active_goals} · Total iterations: ${s.total_iterations}`);
|
|
1579
|
-
if (s.last_action) {
|
|
1580
|
-
console.log(` Last action (${s.last_action.ran_at}): ${s.last_action.summary} [${s.last_action.result_status ?? '?'}]`);
|
|
1581
|
-
}
|
|
1582
|
-
if (flags.log === true) {
|
|
1583
|
-
console.log('\n=== Recent log ===');
|
|
1584
|
-
console.log(daemon.recentLog(40));
|
|
1585
|
-
}
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
case 'stop': {
|
|
1589
|
-
const r = await daemon.stop({ grace_ms: flags.grace !== undefined ? intFlag(flags.grace, 3000) : undefined });
|
|
1590
|
-
if (!r.ok) {
|
|
1591
|
-
printError(`Stop failed: ${r.error}`);
|
|
1592
|
-
process.exitCode = 1;
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
printSuccess(r.was_running
|
|
1596
|
-
? `Agent stopped (was pid ${r.pid}). Goals remain in DB — wyrm agent init resumes.`
|
|
1597
|
-
: 'Agent was not running. (No-op.)');
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
case 'restart': {
|
|
1601
|
-
const r = await daemon.restart({
|
|
1602
|
-
interval_seconds: flags.interval !== undefined ? intFlag(flags.interval, 600) : undefined,
|
|
1603
|
-
max_steps: flags['max-steps'] !== undefined ? intFlag(flags['max-steps'], 0) || undefined : undefined,
|
|
1604
|
-
project_path: flags.project,
|
|
1605
|
-
verbose: flags.verbose === true,
|
|
1606
|
-
});
|
|
1607
|
-
if (!r.ok) {
|
|
1608
|
-
printError(`Restart failed: ${r.error}`);
|
|
1609
|
-
process.exitCode = 1;
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
printSuccess(`Agent restarted — pid ${r.status.pid}. Active goals: ${r.status.active_goals}.`);
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
default:
|
|
1616
|
-
printError('Usage: wyrm agent <init|status|stop|restart> [--interval N] [--max-steps N] [--project <path>] [--verbose] [--log]');
|
|
1617
|
-
process.exitCode = 1;
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
finally {
|
|
1621
|
-
db.close();
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
/** `wyrm setup [...]` — delegate to the existing wyrm-setup bin (dist/setup.js
|
|
1625
|
-
* parses argv itself); `--encrypt` wraps crypto.ts (the wyrm_encrypt_setup
|
|
1626
|
-
* exile target: `wyrm setup --encrypt [--enable|--test]`). */
|
|
1627
|
-
async function cmdSetup(args) {
|
|
1628
|
-
if (args.includes('--encrypt')) {
|
|
1629
|
-
const { initializeLicense, hasFeature } = await import('./license.js');
|
|
1630
|
-
initializeLicense();
|
|
1631
|
-
if (!hasFeature('encryption')) {
|
|
1632
|
-
printError('Encryption setup requires a Pro license or higher. See: wyrm license');
|
|
1633
|
-
process.exitCode = 1;
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
const { getCrypto, initializeCrypto } = await import('./crypto.js');
|
|
1637
|
-
const wantEnable = args.includes('--enable');
|
|
1638
|
-
const wantTest = args.includes('--test');
|
|
1639
|
-
if (wantEnable) {
|
|
1640
|
-
// password from env or stdin pipe — never argv (shell-history safety,
|
|
1641
|
-
// same posture as `wyrm vault set`).
|
|
1642
|
-
const password = process.env.WYRM_ENCRYPTION_KEY ?? (!process.stdin.isTTY ? readStdin() : '');
|
|
1643
|
-
if (!password || password.length < 8) {
|
|
1644
|
-
printError('Password must be at least 8 characters. Set WYRM_ENCRYPTION_KEY or pipe it: printf %s "$PW" | wyrm setup --encrypt --enable');
|
|
1645
|
-
process.exitCode = 1;
|
|
1646
|
-
return;
|
|
1647
|
-
}
|
|
1648
|
-
initializeCrypto(password);
|
|
1649
|
-
printSuccess('Encryption enabled (AES-256-GCM, key derived via PBKDF2). Store your password safely — it cannot be recovered.');
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
const crypto = getCrypto();
|
|
1653
|
-
if (wantTest) {
|
|
1654
|
-
if (!crypto.isEnabled()) {
|
|
1655
|
-
printError('Encryption not enabled. Run: wyrm setup --encrypt --enable');
|
|
1656
|
-
process.exitCode = 1;
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
const testData = 'Wyrm encryption test ' + Date.now();
|
|
1660
|
-
const ok = crypto.decrypt(crypto.encrypt(testData)) === testData;
|
|
1661
|
-
if (ok)
|
|
1662
|
-
printSuccess('Encryption test PASSED (encrypt → decrypt roundtrip).');
|
|
1663
|
-
else {
|
|
1664
|
-
printError('Encryption test FAILED.');
|
|
1665
|
-
process.exitCode = 1;
|
|
1666
|
-
}
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
printSection('Encryption status');
|
|
1670
|
-
console.log(` Enabled: ${crypto.isEnabled() ? 'yes — new data is encrypted at rest' : 'no'}`);
|
|
1671
|
-
console.log(' Algorithm: AES-256-GCM');
|
|
1672
|
-
if (!crypto.isEnabled())
|
|
1673
|
-
console.log(c.dim(' Enable with: wyrm setup --encrypt --enable (password via WYRM_ENCRYPTION_KEY or stdin)'));
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
// Plain `wyrm setup [...flags]` == the wyrm-setup bin: spawn the sibling
|
|
1677
|
-
// script so its own argv parsing (argv.slice(2)) sees the flags directly.
|
|
1678
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
1679
|
-
const r = spawnSync(process.execPath, [join(here, 'setup.js'), ...args], { stdio: 'inherit', shell: false });
|
|
1680
|
-
process.exitCode = r.status ?? 0;
|
|
1681
|
-
}
|
|
1682
|
-
function printHelp() {
|
|
1683
|
-
console.log(`
|
|
1684
|
-
${colors.brightMagenta} Wyrm CLI v${readPkg().version ?? 'unknown'}${colors.reset}
|
|
1685
|
-
${colors.dim}Persistent AI Memory System${colors.reset}
|
|
57
|
+
`).all();if(a.length===0){console.log(d.dim(` No artifacts pending review${e?` for ${e}`:""}.`)),s.close();return}j(`Review Queue (${a.length} items)`);const o=K({input:process.stdin,output:process.stdout}),u=c=>new Promise(m=>{o.question(c,m)});for(const c of a){console.log(`
|
|
58
|
+
${d.bold(`[${c.kind}] #${c.id}`)}`),console.log(d.dim("\u2500".repeat(60))),console.log(c.problem.slice(0,300)),console.log(d.dim("\u2500".repeat(60)));const y=(await u(`${d.cyan("[a]")}pprove / ${d.red("[r]")}eject / ${d.yellow("[s]")}kip? `)).trim().toLowerCase();y==="a"?(r.prepare("UPDATE memory_artifacts SET needs_review = 0, updated_at = datetime('now') WHERE id = ?").run(c.id),v(`Approved #${c.id}`)):y==="r"?(r.prepare("DELETE FROM memory_artifacts WHERE id = ?").run(c.id),console.log(`${L.cross} Rejected #${c.id}`)):console.log(d.dim(` Skipped #${c.id}`))}o.close(),s.close(),console.log(`
|
|
59
|
+
Review complete.`)}async function Ue(l){const{positional:i,flags:e}=x(l),s=i[0];(!s||!["export","import","preview"].includes(s))&&(p("Usage: wyrm sync export --out <path> | wyrm sync import --from <path> | wyrm sync preview --from <path>"),process.exit(1));const{randomBytes:r,pbkdf2Sync:t,createCipheriv:n,createDecipheriv:a}=await import("crypto"),{readFileSync:o,writeFileSync:u,copyFileSync:c,unlinkSync:m,existsSync:y,chmodSync:f}=await import("fs"),{homedir:g}=await import("os"),{join:w}=await import("path"),h=(await import("better-sqlite3")).default;let b=process.env.WYRM_SYNC_PASSPHRASE??"";if(!b){const C=K({input:process.stdin,output:process.stdout});b=await new Promise(D=>{process.stdout.write("Passphrase: "),process.stdin.isTTY&&process.stdin.setRawMode?.(!0),C.question("",N=>{process.stdin.isTTY&&process.stdin.setRawMode?.(!1),console.log(""),C.close(),D(N)})})}b||(p("Passphrase is required. Set WYRM_SYNC_PASSPHRASE or enter interactively."),process.exit(1));const _=w(g(),".wyrm"),S=E();if(s==="export"){const C=e.out;C||(p("--out <path> is required"),process.exit(1));const D=w(_,"wyrm_cli_export_temp.db");try{const N=S.getDatabase();y(D)&&m(D),N.prepare("VACUUM INTO ?").run(D);const U=o(D),W=r(32),H=r(16),$e=t(b,W,6e5,32,"sha256"),te=n("aes-256-gcm",$e,H),ke=Buffer.concat([te.update(U),te.final()]),Se=te.getAuthTag(),je=Buffer.from("WYRM"),ne=Buffer.alloc(1);ne.writeUInt8(1,0);const ie=Buffer.concat([je,ne,W,H,Se,ke]);u(C,ie);try{f(C,384)}catch{}try{m(D)}catch{}const _e=(ie.length/(1024*1024)).toFixed(2);v(`Exported to ${C} (${_e} MB)`)}catch(N){try{y(D)&&m(D)}catch{}p(`Export failed: ${N}`)}S.close();return}const $=e.from;$||(p("--from <path> is required"),process.exit(1));const T=o($);T.subarray(0,4).toString("ascii")!=="WYRM"&&(p("Invalid Wyrm snapshot file."),process.exit(1));const Y=T.readUInt8(4);Y!==1&&(p(`Unsupported snapshot version: ${Y}`),process.exit(1));const q=T.subarray(5,37),A=T.subarray(37,53),ge=T.subarray(53,69),he=T.subarray(69),we=t(b,q,6e5,32,"sha256"),Q=a("aes-256-gcm",we,A);Q.setAuthTag(ge);let X;try{X=Buffer.concat([Q.update(he),Q.final()])}catch{p("Decryption failed \u2014 wrong passphrase or corrupted file."),process.exit(1)}if(s==="preview"){const C=w(_,"wyrm_cli_preview_temp.db");y(C)&&m(C),u(C,X);try{const D=new h(C,{readonly:!0}),N=["projects","sessions","ground_truths","memory_artifacts","quests"];j("Snapshot Preview");const U=[];for(const W of N)try{const H=D.prepare(`SELECT COUNT(*) as n FROM ${W}`).get();U.push([W,String(H.n)])}catch{U.push([W,"?"])}console.log(M(["Table","Count"],U)),D.close()}catch(D){p(`Preview failed: ${D}`)}try{y(C)&&m(C)}catch{}S.close();return}const F=S.getDatabasePath(),re=K({input:process.stdin,output:process.stdout}),be=await new Promise(C=>{re.question("This will REPLACE your current database. Type CONFIRM to proceed: ",C)});if(re.close(),be.trim()!=="CONFIRM"){console.log(d.dim("Aborted.")),S.close();return}const ve=new Date().toISOString().replace(/[:.]/g,"-"),Z=`${F}.backup.${ve}`;c(F,Z),v(`Backed up to ${Z}`);const ee=w(_,"wyrm_cli_restore_temp.db");u(ee,X),S.getDatabase().close(),c(ee,F);for(const C of["-wal","-shm"])try{y(F+C)&&m(F+C)}catch{}try{m(ee)}catch{}v(`Restored from ${$}. Backup at ${Z}`)}async function He(l){const{flags:i}=x(l),e=i.project,s=i.path,r=z(i["min-confidence"],.3),t=R(i["older-than"],90),n=i["no-dry-run"]===!0,a=i.yes===!0,o=E(),u=o.getDatabase();let c=null;if(s||e){const w=s?o.getProject(s):P(o,e);w||(p(`Project not found: ${s??e}`),o.close(),process.exit(1)),c=w.id}const m=new G(u),{candidates:y}=m.pruneStale({projectId:c,minConfidence:r,olderThanDays:t,dryRun:!0});if(j(`Prune Candidates${n?" (LIVE DELETE)":" (dry-run)"}`),y.length===0){console.log(d.dim(" No artifacts match prune criteria.")),o.close();return}const f=y.map(w=>[String(w.id),w.kind,w.problem.slice(0,60),(w.confidence*100).toFixed(0)+"%",w.last_accessed_at??"never"]);if(console.log(M(["ID","Kind","Problem","Conf","Last Accessed"],f)),console.log(`
|
|
60
|
+
Total: ${y.length} candidate(s)`),!n){console.log(d.dim(`
|
|
61
|
+
This is a dry run. Use --no-dry-run to delete (confirm each ID).`)),o.close();return}if(!a){const w=K({input:process.stdin,output:process.stdout}),h=await new Promise(b=>{w.question(`
|
|
62
|
+
Delete these ${y.length} artifact(s)? Type CONFIRM to proceed: `,b)});if(w.close(),h.trim()!=="CONFIRM"){console.log(d.dim("Aborted.")),o.close();return}}const g=m.deleteArtifacts(y.map(w=>w.id));v(`Deleted ${g} artifact(s).`),o.close()}function me(){return V(0,"utf-8").trim()}async function Ve(){const{initializeLicense:l,getLicenseInfo:i,getTier:e}=await import("./license.js");l();const s=i();j("Wyrm License");const r=[["Tier",e()],["Status",s.valid?"valid":"free tier (no license key)"]];s.valid&&s.key&&(r.push(["Key",s.key]),r.push(["Issued to",s.issuedTo??"unknown"]),r.push(["Expires",s.expiresAt?new Date(s.expiresAt).toLocaleDateString():"never"])),r.push(["Features",s.features.join(", ")||"(free)"]),console.log(M(["Field","Value"],r)),s.valid||console.log(d.dim(`
|
|
63
|
+
Activate with: wyrm login (free) \xB7 or: wyrm activate <license.json | key>`)),console.log(d.dim(`
|
|
64
|
+
Source (AGPL-3.0-or-later): https://github.com/Ghosts-Protocol-Pvt-Ltd/Wyrm`)),console.log(d.dim(" Per AGPL \xA713, operators of a MODIFIED Wyrm network service must offer their modified source to users."))}async function Be(){const l=(process.env.WYRM_ACCOUNT_URL??"https://account.ghosts.lk").replace(/\/$/,""),i=(()=>{try{return O().version??"unknown"}catch{return"unknown"}})();let e;try{const c=await fetch(`${l}/api/v1/cli/auth/start`,{method:"POST",headers:{"x-wyrm-version":i}});if(!c.ok)throw new Error(`HTTP ${c.status}`);e=await c.json()}catch(c){p(`Couldn't reach ${l} (${c instanceof Error?c.message:"network error"}).`),process.exitCode=1;return}const s=e.verification_uri_complete||e.verification_uri||`${l}/cli`;console.log(`
|
|
65
|
+
${d.cyan("Sign in to activate Wyrm (free):")}`),console.log(` 1. Open ${d.cyan(s)}`),console.log(` 2. Approve the code ${d.cyan(e.user_code)}`),console.log(d.dim(`
|
|
66
|
+
Waiting for approval\u2026 (Ctrl-C to cancel)`));const r=(e.interval??3)*1e3,t=Date.now()+(e.expires_in??600)*1e3;let n="";for(;Date.now()<t;){await new Promise(c=>setTimeout(c,r));try{const m=await(await fetch(`${l}/api/v1/cli/auth/poll`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({device_code:e.device_code})})).json();if(m.status==="approved"&&m.token){n=m.token;break}if(m.status==="denied"||m.error==="expired"){p("Login was denied or the code expired. Run `wyrm login` again."),process.exitCode=1;return}}catch{}}if(!n){p("Login timed out. Run `wyrm login` again."),process.exitCode=1;return}let a;try{const c=await fetch(`${l}/api/v1/license/free`,{method:"POST",headers:{authorization:`Bearer ${n}`,"x-wyrm-version":i}});if(!c.ok){const m=await c.json().catch(()=>({}));p(`Activation failed (${c.status}): ${m.hint||m.error||"unknown error"}`),process.exitCode=1;return}a=JSON.stringify(await c.json())}catch(c){p(`Activation request failed (${c instanceof Error?c.message:"network error"}).`),process.exitCode=1;return}const{activateLicense:o}=await import("./license.js"),u=o(a);u.valid?(v(`Signed in & activated \u2014 ${u.tier} tier (expires ${u.expiresAt??"never"}).`),console.log(d.dim(" Restart the Wyrm MCP server / daemon to apply."))):(p(`Activation failed: ${u.error??"unknown error"}`),process.exitCode=1)}async function Ke(l){const{positional:i}=x(l),e=i[0];let s;e&&ce(e)?s=V(e,"utf-8"):e?s=e:process.stdin.isTTY?(p("Usage: wyrm activate <license.json path | license JSON> (or pipe the JSON on stdin)"),process.exit(1)):s=me();const{activateLicense:r}=await import("./license.js");try{const t=r(s);t.valid?(v(`License activated \u2014 ${t.tier} tier (${t.features.join(", ")})`),console.log(d.dim(" Restart Wyrm (MCP server / daemon) to apply all features."))):(p(`License activation failed: ${t.error??"unknown error"}`),process.exitCode=1)}catch{p("Invalid license format. Please verify your license key."),process.exitCode=1}}async function Ge(l){const{flags:i}=x(l),{runMaintenance:e}=await import("./maintenance.js"),{FailurePatterns:s}=await import("./failure-patterns.js"),{SessionSeen:r}=await import("./session-seen.js"),{AgentPresence:t}=await import("./presence.js"),n=E();try{const a=n.getDatabase(),o=R(i["archive-days"],0),u=e({db:n,sessionSeen:new r(a),failures:new s(a),presence:new t(a)},{vacuum:i.vacuum===!0,archiveDays:o>0?o:void 0});j("Maintenance complete");for(const c of u.lines)console.log(` - ${c}`);console.log(d.dim(`
|
|
67
|
+
Database size: ${u.dbSize}`))}finally{n.close()}}async function Je(l){const{positional:i,flags:e}=x(l),s=i[0]??"status",{createVectorStore:r}=await import("./vectors.js"),n={provider:e.provider??process.env.WYRM_VECTOR_PROVIDER??"auto",model:e.model,apiKey:e["api-key"]??process.env.OPENAI_API_KEY,ollamaUrl:e["ollama-url"]};if(s==="setup"){const{createProvider:o}=await import("./providers/embedding-provider.js"),u=o(n);if(!await u.isReady()&&n.provider!=="none"){p(`Provider not ready: ${u.name}. Check the configuration and try again.`),process.exitCode=1;return}v(`Vector provider verified: ${u.name} (model ${u.model}, ${u.dimensions}d)`),console.log(d.dim(" The MCP server reads WYRM_VECTOR_PROVIDER (and provider-specific env) at boot \u2014")),console.log(d.dim(` set WYRM_VECTOR_PROVIDER=${u.name==="none"?"none":n.provider} in the server env, then: wyrm index rebuild`));return}const a=E();try{const o=a.getDatabase(),u=r(n,o);if(s==="status"){const c=u.getStats();j("Vector index");const m=[["Provider",c.provider],["Model",c.model],["Vectors",String(c.total)],...Object.entries(c.byType).map(([y,f])=>[` ${y}`,String(f)])];console.log(M(["Field","Value"],m));return}if(s==="rebuild"){const{reindexProjects:c}=await import("./reindex.js"),m=e["dry-run"]===!0,y=e.project;let f;if(y){const h=a.getProject(y)??P(a,y);if(!h){p(`Project not found: ${y}`),process.exitCode=1;return}f=[h.id]}else f=a.getAllProjects(1e3).map(h=>h.id);const{indexed:g,skipped:w}=await c(o,u,f,{dryRun:m,onError:(h,b)=>p(`${h}: ${JSON.stringify(b)}`)});v(`Reindex ${m?"(dry run) ":""}\u2014 ${f.length} project(s), ${g} indexed, ${w} skipped`);return}p("Usage: wyrm index <setup|rebuild|status> [--provider auto|local|ollama|openai|none] [--model M] [--project P] [--dry-run]"),process.exitCode=1}finally{a.close()}}async function ze(l){const{flags:i}=x(l),{getUpdateStatus:e}=await import("./version-check.js"),s=O().version??"0.0.0",r=E();let t;try{t=await e(r.getDatabase(),s,{force:i.force===!0||i.check===!0})}finally{r.close()}if(j("Wyrm update"),console.log(M(["Field","Value"],[["Current",t.current],["Latest",t.latest??"unknown (offline?)"],["Update available",t.updateAvailable?"yes":"no"],["Checked",`${t.checkedAt} (${t.source})`]])),i.check===!0)return;if(!t.updateAvailable&&i.force!==!0){console.log(d.dim(`
|
|
68
|
+
Already up to date. (Use --force to reinstall anyway.)`));return}console.log(d.dim(`
|
|
69
|
+
Running: npm install -g wyrm-mcp@latest
|
|
70
|
+
`));const n=B("npm",["install","-g","wyrm-mcp@latest"],{stdio:"inherit",shell:!1});n.status===0?v("Updated. Restart your MCP clients to pick up the new binary."):(p(`npm install exited with ${n.status??"unknown"}`),process.exitCode=n.status??1)}async function Qe(l){const{positional:i,flags:e}=x(l),s=i[0],r=e.project??process.cwd();if(s==="inject"){const{injectSystemPrompt:t}=await import("./autoconfig.js"),n=typeof e.clients=="string"?e.clients.split(",").map(o=>o.trim()).filter(Boolean):[],a=t(r,n);j("System prompt injection");for(const o of a.injected)console.log(` + ${o}`);for(const o of a.skipped)console.log(d.dim(` o skipped (unknown client): ${o}`));for(const o of a.errors)p(o);a.injected.length>0&&v("AI clients in this project will now call wyrm_session_prime at conversation start."),a.errors.length>0&&(process.exitCode=1);return}if(s==="migrate"){const{migrateProject:t,renderMigrationReport:n}=await import("./migrate-prompt.js"),{WYRM_INJECT_BLOCK:a}=await import("./autoconfig.js"),o=e.apply===!0,u=t({projectPath:r,newBlock:a,apply:o});console.log(n(u,o));return}p("Usage: wyrm prompt inject [--project <path>] [--clients copilot,cursor] | wyrm prompt migrate [--project <path>] [--apply]"),process.exitCode=1}async function Xe(l){const{positional:i,flags:e}=x(l);(i[0]??"report")!=="report"&&(p("Usage: wyrm hours report --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--session-hours H] [--json]"),process.exit(1));const r=e.from,t=e.to;(!r||!t)&&(p("--from and --to are required (YYYY-MM-DD)"),process.exit(1));const{HourLedger:n}=await import("./hours.js"),a=E();try{const o=e.project,u=o?P(a,o):null;if(o&&!u){p(`Project not found: ${o}`),process.exitCode=1;return}const c=new n(a.getDatabase()).report({range_start:r,range_end:t,project_id:u?.id,default_session_hours:z(e["session-hours"],1)});if(e.json===!0){console.log(JSON.stringify(c,null,2));return}if(j(`Hours ${c.range.start} \u2192 ${c.range.end}`),c.by_project.length===0){console.log(d.dim(" No sessions in range."));return}console.log(M(["Project","Sessions","Hours"],c.by_project.map(m=>[m.project_name,String(m.session_count),m.hours.toFixed(2)]))),console.log(`
|
|
71
|
+
Total: ${c.total_hours.toFixed(2)}h across ${c.entries.length} session(s)`+(c.estimated_sessions>0?d.dim(` (${c.estimated_sessions} estimated)`):""))}finally{a.close()}}async function Ze(l){const{positional:i,flags:e}=x(l),s=i[0]??"generate",r=e.client,t=z(e.rate,NaN),n=e.from,a=e.to;(s!=="generate"||!r||!Number.isFinite(t)||!n||!a)&&(p('Usage: wyrm invoice generate --client <name> --rate <usd/hour> --from YYYY-MM-DD --to YYYY-MM-DD [--project <name>] [--number INV-X] [--currency USD] [--notes "\u2026"] [--out <path>]'),process.exit(1));const{HourLedger:o}=await import("./hours.js"),u=E();try{const c=e.project,m=c?P(u,c):null;if(c&&!m){p(`Project not found: ${c}`),process.exitCode=1;return}const y=new o(u.getDatabase()).invoice({client_name:r,hourly_rate_usd:t,range_start:n,range_end:a,project_id:m?.id,invoice_number:e.number,currency:e.currency,notes:e.notes,business_name:e["business-name"],business_address:e["business-address"],business_contact:e["business-contact"],client_address:e["client-address"],default_session_hours:z(e["session-hours"],1)}),f=e.out;if(f){const{writeFileSync:g}=await import("node:fs");g(f,y+`
|
|
72
|
+
`,"utf-8"),v(`Invoice written to ${f}`)}else process.stdout.write(y+`
|
|
73
|
+
`)}finally{u.close()}}async function et(l){const{positional:i,flags:e}=x(l),s=i[0]??"status",{AgentDaemon:r}=await import("./agent-daemon.js"),t=E();try{const n=new r(t.getDatabase());switch(s){case"init":case"start":{const a=Math.max(10,Math.min(R(e.interval,600),86400)),o=n.start({interval_seconds:a,max_steps:e["max-steps"]!==void 0&&R(e["max-steps"],0)||void 0,project_path:e.project,verbose:e.verbose===!0});if(!o.ok){p(`Agent init failed: ${o.error}`),process.exitCode=1;return}const u=o.status;v(`Agent ${u.pid!=null&&u.pid!==o.pid?"already running":"started"} \u2014 pid ${u.pid}`),console.log(` Interval: ${a}s \xB7 Active goals: ${u.active_goals} \xB7 Total iterations: ${u.total_iterations}`),console.log(d.dim(` Log: ${u.log_file}`));return}case"status":{const a=n.status();j("Wyrm agent"),console.log(a.running?` RUNNING \u2014 pid ${a.pid}${a.started_at?` (since ${a.started_at})`:""}`:" NOT RUNNING. Start it with: wyrm agent init"),console.log(` Active goals: ${a.active_goals} \xB7 Total iterations: ${a.total_iterations}`),a.last_action&&console.log(` Last action (${a.last_action.ran_at}): ${a.last_action.summary} [${a.last_action.result_status??"?"}]`),e.log===!0&&(console.log(`
|
|
74
|
+
=== Recent log ===`),console.log(n.recentLog(40)));return}case"stop":{const a=await n.stop({grace_ms:e.grace!==void 0?R(e.grace,3e3):void 0});if(!a.ok){p(`Stop failed: ${a.error}`),process.exitCode=1;return}v(a.was_running?`Agent stopped (was pid ${a.pid}). Goals remain in DB \u2014 wyrm agent init resumes.`:"Agent was not running. (No-op.)");return}case"restart":{const a=await n.restart({interval_seconds:e.interval!==void 0?R(e.interval,600):void 0,max_steps:e["max-steps"]!==void 0&&R(e["max-steps"],0)||void 0,project_path:e.project,verbose:e.verbose===!0});if(!a.ok){p(`Restart failed: ${a.error}`),process.exitCode=1;return}v(`Agent restarted \u2014 pid ${a.status.pid}. Active goals: ${a.status.active_goals}.`);return}default:p("Usage: wyrm agent <init|status|stop|restart> [--interval N] [--max-steps N] [--project <path>] [--verbose] [--log]"),process.exitCode=1}}finally{t.close()}}async function tt(l){if(l.includes("--encrypt")){const{initializeLicense:s,hasFeature:r}=await import("./license.js");if(s(),!r("encryption")){p("Encryption setup requires a Pro license or higher. See: wyrm license"),process.exitCode=1;return}const{getCrypto:t,initializeCrypto:n}=await import("./crypto.js"),a=l.includes("--enable"),o=l.includes("--test");if(a){const c=process.env.WYRM_ENCRYPTION_KEY??(process.stdin.isTTY?"":me());if(!c||c.length<8){p('Password must be at least 8 characters. Set WYRM_ENCRYPTION_KEY or pipe it: printf %s "$PW" | wyrm setup --encrypt --enable'),process.exitCode=1;return}n(c),v("Encryption enabled (AES-256-GCM, key derived via PBKDF2). Store your password safely \u2014 it cannot be recovered.");return}const u=t();if(o){if(!u.isEnabled()){p("Encryption not enabled. Run: wyrm setup --encrypt --enable"),process.exitCode=1;return}const c="Wyrm encryption test "+Date.now();u.decrypt(u.encrypt(c))===c?v("Encryption test PASSED (encrypt \u2192 decrypt roundtrip)."):(p("Encryption test FAILED."),process.exitCode=1);return}j("Encryption status"),console.log(` Enabled: ${u.isEnabled()?"yes \u2014 new data is encrypted at rest":"no"}`),console.log(" Algorithm: AES-256-GCM"),u.isEnabled()||console.log(d.dim(" Enable with: wyrm setup --encrypt --enable (password via WYRM_ENCRYPTION_KEY or stdin)"));return}const i=ae(le(import.meta.url)),e=B(process.execPath,[oe(i,"setup.js"),...l],{stdio:"inherit",shell:!1});process.exitCode=e.status??0}function fe(){console.log(`
|
|
75
|
+
${J.brightMagenta}\u{F115D} Wyrm CLI v${O().version??"unknown"}${J.reset}
|
|
76
|
+
${J.dim}Persistent AI Memory System${J.reset}
|
|
1686
77
|
|
|
1687
|
-
${
|
|
78
|
+
${d.bold("Usage:")} wyrm <command> [options]
|
|
1688
79
|
|
|
1689
|
-
${
|
|
1690
|
-
${
|
|
1691
|
-
${
|
|
1692
|
-
${
|
|
1693
|
-
${
|
|
1694
|
-
${
|
|
1695
|
-
${
|
|
1696
|
-
${
|
|
1697
|
-
${
|
|
1698
|
-
${
|
|
1699
|
-
${
|
|
1700
|
-
${
|
|
1701
|
-
${
|
|
1702
|
-
${
|
|
1703
|
-
${
|
|
1704
|
-
${
|
|
1705
|
-
${
|
|
1706
|
-
${
|
|
1707
|
-
${
|
|
1708
|
-
${
|
|
1709
|
-
${
|
|
1710
|
-
${
|
|
1711
|
-
${
|
|
1712
|
-
${
|
|
1713
|
-
${
|
|
1714
|
-
${
|
|
1715
|
-
${
|
|
1716
|
-
${
|
|
1717
|
-
${
|
|
1718
|
-
${
|
|
1719
|
-
${
|
|
80
|
+
${d.bold("Commands:")}
|
|
81
|
+
${d.cyan("search")} <query> Search across memories, truths, quests, and data
|
|
82
|
+
${d.cyan("ls")} List stored items
|
|
83
|
+
${d.cyan("show")} <typed-id> Show full detail (mem:N, quest:N, truth:N, data:N, session:N)
|
|
84
|
+
${d.cyan("capture")} "<content>" Classify and capture a thought, task, or lesson
|
|
85
|
+
${d.cyan("rehydrate")} [--project <name>] Print the latest session's continuity brief (for SessionStart)
|
|
86
|
+
${d.cyan("render")} [--client <list>] [--brief] Compile DB \u2192 MEMORY.md + topic files (zero-MCP-token memory)
|
|
87
|
+
${d.cyan("import git")} Import git log history to review queue
|
|
88
|
+
${d.cyan("import rules")} <path> Import .cursorrules / copilot-instructions file
|
|
89
|
+
${d.cyan("stats")} Show database statistics
|
|
90
|
+
${d.cyan("review")} Interactively review pending artifacts
|
|
91
|
+
${d.cyan("sync export")} --out <path> Export encrypted DB snapshot
|
|
92
|
+
${d.cyan("sync import")} --from <path> Restore from encrypted snapshot (backs up first)
|
|
93
|
+
${d.cyan("sync preview")} --from <path> Preview snapshot stats without restoring
|
|
94
|
+
${d.cyan("prune")} [--project <name>] Prune stale low-confidence artifacts (dry-run by default)
|
|
95
|
+
${d.cyan("cloud")} <login|sync|export|\u2026> Cloud backup & multi-device sync (Pro license)
|
|
96
|
+
${d.cyan("grove")} <status|policy> Per-project sync lanes (private|cloud|team) + leak audit
|
|
97
|
+
${d.cyan("skill")} <backfill-content|export|share> Portable skills: store SKILL.md content + sync/export
|
|
98
|
+
${d.cyan("serve")} [--ui] Start the Wyrm HTTP server (optionally open the dashboard)
|
|
99
|
+
${d.cyan("setup")} [--check|--encrypt|\u2026] Auto-configure AI clients (wyrm-setup); --encrypt for at-rest crypto
|
|
100
|
+
${d.cyan("intro")} What Wyrm is and how to use it
|
|
101
|
+
${d.cyan("license")} Show license status & features
|
|
102
|
+
${d.cyan("login")} Sign in (free account) and activate \u2014 required on official builds
|
|
103
|
+
${d.cyan("activate")} <key|path> Activate a license (JSON arg, file path, or stdin)
|
|
104
|
+
${d.cyan("maintenance")} [--vacuum] Archive/prune/sweep/checkpoint the database
|
|
105
|
+
${d.cyan("index")} <setup|rebuild|status> Vector search: verify provider, reindex, stats
|
|
106
|
+
${d.cyan("update")} [--check] Check for / install the latest wyrm-mcp
|
|
107
|
+
${d.cyan("prompt")} <inject|migrate> Inject/refresh the session-prime block in client files
|
|
108
|
+
${d.cyan("hours")} report --from --to Hours report derived from session history
|
|
109
|
+
${d.cyan("invoice")} generate --client \u2026 Markdown invoice from the hours ledger
|
|
110
|
+
${d.cyan("agent")} <init|status|stop|restart> Background agent daemon (goal loop)
|
|
1720
111
|
|
|
1721
|
-
${
|
|
112
|
+
${d.bold("Options:")}
|
|
1722
113
|
--project <name> Filter or associate with a specific project
|
|
1723
114
|
--type <type> Filter type (memories|truths|quests|data|all)
|
|
1724
115
|
--limit <N> Limit number of results (default: 20)
|
|
@@ -1735,766 +126,22 @@ ${c.bold('Options:')}
|
|
|
1735
126
|
--help, -h Show this help
|
|
1736
127
|
--version, -v Print the installed version
|
|
1737
128
|
|
|
1738
|
-
${
|
|
129
|
+
${d.bold("Environment:")}
|
|
1739
130
|
WYRM_DB_PATH Override database path (default: ~/.wyrm/wyrm.db)
|
|
1740
131
|
WYRM_SYNC_PASSPHRASE Passphrase for encrypted sync (or prompted interactively)
|
|
1741
132
|
|
|
1742
|
-
${
|
|
133
|
+
${d.bold("Examples:")}
|
|
1743
134
|
wyrm search "authentication bug"
|
|
1744
135
|
wyrm ls --type memories --project MyApp
|
|
1745
136
|
wyrm capture "TODO: refactor the auth module" --project MyApp
|
|
1746
137
|
wyrm sync export --out ~/wyrm-backup.wyrm
|
|
1747
138
|
wyrm sync preview --from ~/wyrm-backup.wyrm
|
|
1748
139
|
wyrm prune --project MyApp --min-confidence 0.2 --older-than 30
|
|
1749
|
-
`);
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
const [, ,
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
return `#${e.cursor} ${e.kind.padEnd(15)} ${ref.padEnd(16)} ${readActor(e.actor)} ${when}`;
|
|
1758
|
-
}
|
|
1759
|
-
/** `wyrm events publish <kind> --project <p>` and `wyrm events since --project <p>`. */
|
|
1760
|
-
async function cmdEvents(args) {
|
|
1761
|
-
const { positional, flags } = parseArgs(args);
|
|
1762
|
-
const sub = positional[0] || 'since';
|
|
1763
|
-
const db = openDb();
|
|
1764
|
-
try {
|
|
1765
|
-
if (!db.liveMemoryEnabled()) {
|
|
1766
|
-
printError('Live Memory is disabled (set WYRM_LIVE_MEMORY=1).');
|
|
1767
|
-
process.exitCode = 1;
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
const projRef = (typeof flags.project === 'string' ? flags.project : '') || process.cwd();
|
|
1771
|
-
// Exact resolution only (path then name) — matches the HTTP surface; a fuzzy
|
|
1772
|
-
// LIKE match could silently attach the stream to the WRONG project.
|
|
1773
|
-
const project = db.getProject(projRef) ?? db.getProjectByName(projRef);
|
|
1774
|
-
if (!project) {
|
|
1775
|
-
printError(`Project not found: ${projRef}`);
|
|
1776
|
-
process.exitCode = 1;
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
if (sub === 'publish') {
|
|
1780
|
-
const kind = positional[1] || (typeof flags.kind === 'string' ? flags.kind : '');
|
|
1781
|
-
if (!kind) {
|
|
1782
|
-
printError('usage: wyrm events publish <kind> --project <p> [--actor A] [--ref-table T --ref-id ID]');
|
|
1783
|
-
process.exitCode = 1;
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
db.publishEvent({
|
|
1787
|
-
projectId: project.id,
|
|
1788
|
-
kind: kind,
|
|
1789
|
-
refTable: typeof flags['ref-table'] === 'string' ? flags['ref-table'] : undefined,
|
|
1790
|
-
refId: typeof flags['ref-id'] === 'string' ? flags['ref-id'] : undefined,
|
|
1791
|
-
actor: typeof flags.actor === 'string' ? flags.actor : undefined,
|
|
1792
|
-
});
|
|
1793
|
-
printSuccess(`Event published (${kind}) to ${project.name}`);
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
if (sub === 'since') {
|
|
1797
|
-
const cursor = intFlag(flags.cursor, 0);
|
|
1798
|
-
const limit = intFlag(flags.limit, 50);
|
|
1799
|
-
const events = db.eventsSince(project.id, cursor, limit);
|
|
1800
|
-
for (const e of events)
|
|
1801
|
-
console.log(fmtEvent(e));
|
|
1802
|
-
printSection(`${events.length} event(s) for '${project.name}' since cursor ${cursor}`);
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
printError(`usage: wyrm events <publish|since> ...`);
|
|
1806
|
-
process.exitCode = 1;
|
|
1807
|
-
}
|
|
1808
|
-
finally {
|
|
1809
|
-
db.close();
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
/** `wyrm watch [--project <p>] [--interval ms]` — tail a project's event stream to stdout. */
|
|
1813
|
-
async function cmdWatch(args) {
|
|
1814
|
-
const { flags } = parseArgs(args);
|
|
1815
|
-
const db = openDb();
|
|
1816
|
-
if (!db.liveMemoryEnabled()) {
|
|
1817
|
-
printError('Live Memory is disabled (set WYRM_LIVE_MEMORY=1).');
|
|
1818
|
-
db.close();
|
|
1819
|
-
process.exitCode = 1;
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
const projRef = (typeof flags.project === 'string' ? flags.project : '') || process.cwd();
|
|
1823
|
-
const project = db.getProject(projRef) ?? resolveProject(db, projRef);
|
|
1824
|
-
if (!project) {
|
|
1825
|
-
printError(`Project not found: ${projRef}`);
|
|
1826
|
-
db.close();
|
|
1827
|
-
process.exitCode = 1;
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
const intervalMs = Math.max(250, intFlag(flags.interval, 1000));
|
|
1831
|
-
// Default: tail only NEW events (start at current head). `--since N` to replay.
|
|
1832
|
-
let cursor = intFlag(flags.since, db.subscribeEvents(project.id, 1).cursor);
|
|
1833
|
-
printSection(`Watching '${project.name}' (cursor ${cursor}, every ${intervalMs}ms) — Ctrl-C to stop`);
|
|
1834
|
-
const tick = () => {
|
|
1835
|
-
try {
|
|
1836
|
-
for (const e of db.eventsSince(project.id, cursor, 200)) {
|
|
1837
|
-
cursor = e.cursor;
|
|
1838
|
-
console.log(fmtEvent(e));
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
catch { /* transient — keep watching */ }
|
|
1842
|
-
};
|
|
1843
|
-
tick();
|
|
1844
|
-
const timer = setInterval(tick, intervalMs);
|
|
1845
|
-
const stop = () => { clearInterval(timer); try {
|
|
1846
|
-
db.close();
|
|
1847
|
-
}
|
|
1848
|
-
catch { /* */ } process.exit(0); };
|
|
1849
|
-
process.on('SIGINT', stop);
|
|
1850
|
-
process.on('SIGTERM', stop);
|
|
1851
|
-
}
|
|
1852
|
-
/** `wyrm embed [--remove|--status] [--project <dir>] [--all]` — make Wyrm first-priority memory. */
|
|
1853
|
-
async function cmdEmbed(args) {
|
|
1854
|
-
const { flags } = parseArgs(args);
|
|
1855
|
-
const { embedAll, removeAll, statusAll } = await import('./priority-embed.js');
|
|
1856
|
-
const opts = {
|
|
1857
|
-
projectDir: typeof flags.project === 'string' ? flags.project : undefined,
|
|
1858
|
-
allClients: flags.all === true,
|
|
1859
|
-
};
|
|
1860
|
-
const line = (r) => console.log(` ${String(r.result ?? r.status).padEnd(9)} [${r.scope}] ${r.file}`);
|
|
1861
|
-
if (flags.status) {
|
|
1862
|
-
printSection('Wyrm priority embedding — status');
|
|
1863
|
-
statusAll(opts).forEach(line);
|
|
1864
|
-
return;
|
|
1865
|
-
}
|
|
1866
|
-
if (flags.remove) {
|
|
1867
|
-
printSection('Wyrm priority embedding — removed');
|
|
1868
|
-
removeAll(opts).forEach(line);
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
printSection('Wyrm is now FIRST-PRIORITY memory');
|
|
1872
|
-
embedAll(opts).forEach(line);
|
|
1873
|
-
// Install the proactive hooks + persistent buddy statusline too (best-effort).
|
|
1874
|
-
try {
|
|
1875
|
-
const { installClaudeCodeHooks, installClaudeStatusline } = await import('./autoconfig.js');
|
|
1876
|
-
const hooks = installClaudeCodeHooks();
|
|
1877
|
-
if (hooks)
|
|
1878
|
-
printSuccess('Proactive hooks installed (SessionStart rehydrate + capture + tool-trace).');
|
|
1879
|
-
else
|
|
1880
|
-
console.log(' (Claude Code not detected — skipped hook install.)');
|
|
1881
|
-
const sl = installClaudeStatusline();
|
|
1882
|
-
if (sl)
|
|
1883
|
-
printSuccess(`Buddy statusline: ${sl.message}`);
|
|
1884
|
-
}
|
|
1885
|
-
catch { /* best-effort */ }
|
|
1886
|
-
printSuccess('Wyrm will now be read first, primed proactively, and shown in the TUI at all times.');
|
|
1887
|
-
}
|
|
1888
|
-
/** `wyrm harvest [--project <p>] [--dry-run] [--limit N]` — auto-populate memory from docs + git into the review queue. */
|
|
1889
|
-
async function cmdHarvest(args) {
|
|
1890
|
-
const { flags } = parseArgs(args);
|
|
1891
|
-
const { harvestProjects } = await import('./harvest.js');
|
|
1892
|
-
const { MemoryArtifacts } = await import('./memory-artifacts.js');
|
|
1893
|
-
const { escapeLikePattern } = await import('./auto-capture.js');
|
|
1894
|
-
const db = openDb();
|
|
1895
|
-
try {
|
|
1896
|
-
const raw = db.getDatabase();
|
|
1897
|
-
const memory = new MemoryArtifacts(raw);
|
|
1898
|
-
const deps = {
|
|
1899
|
-
// Security pass #1: harvest sigs are doc/git-text-derived — %/_ must be
|
|
1900
|
-
// LITERAL in the dedup probe (same escapeLikePattern rule as the MCP
|
|
1901
|
-
// harvest/auto_capture/debrief probes).
|
|
1902
|
-
existsBySig: (pid, sig) => !!raw.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(pid, '%' + escapeLikePattern(sig) + '%'),
|
|
1903
|
-
addCandidate: (pid, item) => memory.add(pid, { kind: item.kind, problem: item.text, tags: [...item.tags, item.sig], confidence: item.confidence, needsReview: 1, createdBy: 'harvest' }),
|
|
1904
|
-
};
|
|
1905
|
-
const projRef = typeof flags.project === 'string' ? flags.project : undefined;
|
|
1906
|
-
let projects;
|
|
1907
|
-
if (projRef) {
|
|
1908
|
-
const p = db.getProject(projRef) ?? db.getProjectByName(projRef);
|
|
1909
|
-
if (!p) {
|
|
1910
|
-
printError(`Project not found: ${projRef}`);
|
|
1911
|
-
process.exitCode = 1;
|
|
1912
|
-
return;
|
|
1913
|
-
}
|
|
1914
|
-
projects = [{ id: p.id, name: p.name, path: p.path }];
|
|
1915
|
-
}
|
|
1916
|
-
else {
|
|
1917
|
-
projects = db.getAllProjects(500).map((p) => ({ id: p.id, name: p.name, path: p.path }));
|
|
1918
|
-
}
|
|
1919
|
-
const dryRun = flags['dry-run'] === true || flags.dry === true;
|
|
1920
|
-
const includeCode = flags.code === true || flags['include-code'] === true;
|
|
1921
|
-
const { reports, totalAdded, totalSkipped } = harvestProjects(deps, projects, { dryRun, gitLimit: intFlag(flags.limit, 30), includeCode });
|
|
1922
|
-
printSection(`Harvest ${dryRun ? '(dry run) ' : ''}— ${totalAdded} candidate(s), ${totalSkipped} already present (${projects.length} project(s))`);
|
|
1923
|
-
for (const r of reports.filter((r) => r.added > 0).sort((a, b) => b.added - a.added).slice(0, 25)) {
|
|
1924
|
-
console.log(` +${String(r.added).padStart(3)} (skip ${r.skipped}) ${r.project}`);
|
|
1925
|
-
}
|
|
1926
|
-
if (includeCode && !dryRun) {
|
|
1927
|
-
const { SymbolGraph } = await import('./symbols.js');
|
|
1928
|
-
const sg = new SymbolGraph(raw);
|
|
1929
|
-
let syms = 0, files = 0;
|
|
1930
|
-
for (const p of projects) {
|
|
1931
|
-
try {
|
|
1932
|
-
const r = sg.indexProject(p.id, p.path);
|
|
1933
|
-
syms += r.symbols;
|
|
1934
|
-
files += r.files;
|
|
1935
|
-
}
|
|
1936
|
-
catch { /* skip */ }
|
|
1937
|
-
}
|
|
1938
|
-
console.log(` 📐 Indexed ${syms} code symbols (${files} files) → searchable via 'wyrm search'`);
|
|
1939
|
-
}
|
|
1940
|
-
if (!dryRun && totalAdded > 0)
|
|
1941
|
-
printSuccess('Review with: wyrm review');
|
|
1942
|
-
}
|
|
1943
|
-
finally {
|
|
1944
|
-
db.close();
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
/** `wyrm vault <set|get|list|rm|exec|import-npm|info>` — local encrypted secret store. */
|
|
1948
|
-
async function cmdVault(args) {
|
|
1949
|
-
const v = await import('./vault.js');
|
|
1950
|
-
const [sub, ...rest] = args;
|
|
1951
|
-
try {
|
|
1952
|
-
switch (sub) {
|
|
1953
|
-
case 'set': {
|
|
1954
|
-
const name = rest[0];
|
|
1955
|
-
if (!name) {
|
|
1956
|
-
printError('usage: wyrm vault set <name> (the secret is read from STDIN, never argv)');
|
|
1957
|
-
process.exitCode = 1;
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
if (process.stdin.isTTY) {
|
|
1961
|
-
printError(`pipe the secret in, e.g.: printf %s "$TOKEN" | wyrm vault set ${name}`);
|
|
1962
|
-
process.exitCode = 1;
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const fs = await import('node:fs');
|
|
1966
|
-
const value = fs.readFileSync(0, 'utf8').replace(/\r?\n$/, '');
|
|
1967
|
-
if (!value) {
|
|
1968
|
-
printError('empty secret on stdin');
|
|
1969
|
-
process.exitCode = 1;
|
|
1970
|
-
return;
|
|
1971
|
-
}
|
|
1972
|
-
v.vaultSet(name, value);
|
|
1973
|
-
printSuccess(`Stored "${name}" (AES-256-GCM). Use it without exposing it: wyrm vault exec ${name} -- <command>`);
|
|
1974
|
-
break;
|
|
1975
|
-
}
|
|
1976
|
-
case 'get': {
|
|
1977
|
-
const name = rest[0];
|
|
1978
|
-
if (!name) {
|
|
1979
|
-
printError('usage: wyrm vault get <name>');
|
|
1980
|
-
process.exitCode = 1;
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
const val = v.vaultGet(name);
|
|
1984
|
-
if (val === undefined) {
|
|
1985
|
-
printError(`no secret named "${name}"`);
|
|
1986
|
-
process.exitCode = 1;
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
process.stdout.write(val); // no trailing newline — clean for $(...) / pipes
|
|
1990
|
-
break;
|
|
1991
|
-
}
|
|
1992
|
-
case 'list':
|
|
1993
|
-
case 'ls': {
|
|
1994
|
-
const names = v.vaultList();
|
|
1995
|
-
if (!names.length) {
|
|
1996
|
-
console.log('(vault is empty)');
|
|
1997
|
-
break;
|
|
1998
|
-
}
|
|
1999
|
-
printSection(`Vault — ${names.length} secret(s)`);
|
|
2000
|
-
for (const n of names)
|
|
2001
|
-
console.log(' • ' + n);
|
|
2002
|
-
break;
|
|
2003
|
-
}
|
|
2004
|
-
case 'rm':
|
|
2005
|
-
case 'remove':
|
|
2006
|
-
case 'delete': {
|
|
2007
|
-
const name = rest[0];
|
|
2008
|
-
if (!name) {
|
|
2009
|
-
printError('usage: wyrm vault rm <name>');
|
|
2010
|
-
process.exitCode = 1;
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
printSuccess(v.vaultRemove(name) ? `Removed "${name}"` : `(no secret named "${name}")`);
|
|
2014
|
-
break;
|
|
2015
|
-
}
|
|
2016
|
-
case 'exec': {
|
|
2017
|
-
// wyrm vault exec <name> [--as ENVVAR] -- <command...>
|
|
2018
|
-
const name = rest[0];
|
|
2019
|
-
const sep = rest.indexOf('--');
|
|
2020
|
-
if (!name || sep === -1 || sep + 1 >= rest.length) {
|
|
2021
|
-
printError('usage: wyrm vault exec <name> [--as ENVVAR] -- <command...>');
|
|
2022
|
-
process.exitCode = 1;
|
|
2023
|
-
return;
|
|
2024
|
-
}
|
|
2025
|
-
const opts = rest.slice(1, sep);
|
|
2026
|
-
const asIdx = opts.indexOf('--as');
|
|
2027
|
-
const envVar = asIdx >= 0 ? opts[asIdx + 1] : name.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
2028
|
-
const cmd = rest.slice(sep + 1);
|
|
2029
|
-
const secret = v.vaultGet(name);
|
|
2030
|
-
if (secret === undefined) {
|
|
2031
|
-
printError(`no secret named "${name}"`);
|
|
2032
|
-
process.exitCode = 1;
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
const r = spawnSync(cmd[0], cmd.slice(1), { stdio: 'inherit', env: { ...process.env, [envVar]: secret } });
|
|
2036
|
-
process.exitCode = r.status ?? 1;
|
|
2037
|
-
break;
|
|
2038
|
-
}
|
|
2039
|
-
case 'import-npm': {
|
|
2040
|
-
const fs = await import('node:fs');
|
|
2041
|
-
const os = await import('node:os');
|
|
2042
|
-
const path = await import('node:path');
|
|
2043
|
-
const npmrc = path.join(os.homedir(), '.npmrc');
|
|
2044
|
-
if (!fs.existsSync(npmrc)) {
|
|
2045
|
-
printError('~/.npmrc not found');
|
|
2046
|
-
process.exitCode = 1;
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
const m = fs.readFileSync(npmrc, 'utf8').match(/\/\/registry\.npmjs\.org\/:_authToken=(.+)/);
|
|
2050
|
-
if (!m) {
|
|
2051
|
-
printError('no npm authToken found in ~/.npmrc');
|
|
2052
|
-
process.exitCode = 1;
|
|
2053
|
-
return;
|
|
2054
|
-
}
|
|
2055
|
-
v.vaultSet('npm-token', m[1].trim());
|
|
2056
|
-
printSuccess('Imported npm token → vault as "npm-token". You can now scrub the plaintext from ~/.npmrc and use: wyrm vault exec npm-token --as NODE_AUTH_TOKEN -- npm publish');
|
|
2057
|
-
break;
|
|
2058
|
-
}
|
|
2059
|
-
case 'setup': {
|
|
2060
|
-
// Idempotent "set it up professionally" entry point — the action a client AI
|
|
2061
|
-
// runs after seeing the session-prime advisory. Secures an insecure vault using
|
|
2062
|
-
// the best available backend, then teaches the safe store/use workflow.
|
|
2063
|
-
const p = v.vaultPaths();
|
|
2064
|
-
if (p.secure) {
|
|
2065
|
-
printSuccess(`Vault is already secure (backend: ${p.backend}, ${p.count} secret(s)).`);
|
|
2066
|
-
}
|
|
2067
|
-
else {
|
|
2068
|
-
const backend = p.keychainAvailable ? 'keychain' : 'passphrase';
|
|
2069
|
-
if (backend === 'passphrase' && !process.env.WYRM_VAULT_PASSPHRASE) {
|
|
2070
|
-
printError('No OS keychain on this host. Set WYRM_VAULT_PASSPHRASE, then re-run: wyrm vault setup');
|
|
2071
|
-
process.exitCode = 1;
|
|
2072
|
-
return;
|
|
2073
|
-
}
|
|
2074
|
-
const r = v.vaultSecure({ backend });
|
|
2075
|
-
printSuccess(`Vault secured: ${r.from} → ${r.to}${r.rotated ? ' (key rotated)' : ''}.`);
|
|
2076
|
-
if (r.keyfileShredded)
|
|
2077
|
-
console.log(' plaintext vault.key: shredded ✔');
|
|
2078
|
-
if (r.backup)
|
|
2079
|
-
console.log(` ciphertext backup: ${r.backup} (delete once confirmed)`);
|
|
2080
|
-
}
|
|
2081
|
-
printSection('Store & use credentials safely');
|
|
2082
|
-
console.log(' store: printf %s "$TOKEN" | wyrm vault set <name> # reads STDIN — never argv/shell history');
|
|
2083
|
-
console.log(' use: wyrm vault exec <name> --as ENV_VAR -- <cmd> # injected as env var, never printed');
|
|
2084
|
-
console.log(' list: wyrm vault list inspect: wyrm vault info');
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
|
-
case 'secure': {
|
|
2088
|
-
// wyrm vault secure [--backend keychain|passphrase] [--no-rotate]
|
|
2089
|
-
const bIdx = rest.indexOf('--backend');
|
|
2090
|
-
const backend = (bIdx >= 0 ? rest[bIdx + 1] : 'keychain');
|
|
2091
|
-
if (backend !== 'keychain' && backend !== 'passphrase') {
|
|
2092
|
-
printError('usage: wyrm vault secure [--backend keychain|passphrase] [--no-rotate]');
|
|
2093
|
-
process.exitCode = 1;
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
if (backend === 'passphrase' && !process.env.WYRM_VAULT_PASSPHRASE) {
|
|
2097
|
-
printError('set WYRM_VAULT_PASSPHRASE before: wyrm vault secure --backend passphrase');
|
|
2098
|
-
process.exitCode = 1;
|
|
2099
|
-
return;
|
|
2100
|
-
}
|
|
2101
|
-
const rotate = !rest.includes('--no-rotate');
|
|
2102
|
-
const r = v.vaultSecure({ backend, rotate });
|
|
2103
|
-
printSuccess(`Vault secured: ${r.from} → ${r.to}${r.rotated ? ' (key rotated)' : ''}.`);
|
|
2104
|
-
console.log(` secrets re-encrypted: ${r.secrets}`);
|
|
2105
|
-
if (r.keyfileShredded)
|
|
2106
|
-
console.log(' plaintext vault.key: shredded ✔');
|
|
2107
|
-
if (r.backup)
|
|
2108
|
-
console.log(` ciphertext backup: ${r.backup} (delete once you've confirmed)`);
|
|
2109
|
-
console.log(backend === 'keychain'
|
|
2110
|
-
? ' master key now lives in the OS keychain — not on disk.'
|
|
2111
|
-
: ' master key now derived from WYRM_VAULT_PASSPHRASE — keep that set for future use.');
|
|
2112
|
-
break;
|
|
2113
|
-
}
|
|
2114
|
-
case 'info': {
|
|
2115
|
-
const p = v.vaultPaths();
|
|
2116
|
-
printSection('Vault');
|
|
2117
|
-
console.log(` backend: ${p.backend}`);
|
|
2118
|
-
console.log(` secrets: ${p.count}`);
|
|
2119
|
-
console.log(` store: ${p.vault} (0600)`);
|
|
2120
|
-
console.log(` key: ${p.backend === 'keyfile'
|
|
2121
|
-
? p.key + ' (0600)'
|
|
2122
|
-
: p.backend === 'keychain' ? '(OS keychain — no key on disk)' : '(derived from WYRM_VAULT_PASSPHRASE — no key on disk)'}`);
|
|
2123
|
-
console.log(` secure: ${p.secure ? 'yes — key is not a plaintext file beside the ciphertext' : 'NO — key sits beside ciphertext'}`);
|
|
2124
|
-
if (!p.secure) {
|
|
2125
|
-
console.log(p.keychainAvailable
|
|
2126
|
-
? ' ⚠ run `wyrm vault secure` to move the key into the OS keychain.'
|
|
2127
|
-
: ' ⚠ no OS keychain found — set WYRM_VAULT_PASSPHRASE and run `wyrm vault secure --backend passphrase`.');
|
|
2128
|
-
}
|
|
2129
|
-
break;
|
|
2130
|
-
}
|
|
2131
|
-
default:
|
|
2132
|
-
printError('usage: wyrm vault <setup|set|get|list|rm|exec|import-npm|secure|info>');
|
|
2133
|
-
process.exitCode = 1;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
catch (e) {
|
|
2137
|
-
printError(`vault: ${e.message}`);
|
|
2138
|
-
process.exitCode = 1;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
// ==================== GROVE (sync policy lanes) ====================
|
|
2142
|
-
/**
|
|
2143
|
-
* `wyrm skill`: portability ops for the skill registry (migration 25).
|
|
2144
|
-
* wyrm skill backfill-content read every skill's SKILL.md into the registry (idempotent)
|
|
2145
|
-
* wyrm skill export <targetDir> [--all] materialize SKILL.md files from stored content
|
|
2146
|
-
* wyrm skill share <name|--all|--tier T> [--public|--private] [--include-inactive] set cloud-sync visibility (single or bulk)
|
|
2147
|
-
*
|
|
2148
|
-
* Storing the SKILL.md body in the registry makes skills portable across
|
|
2149
|
-
* machines: backfill on the source box, sync the DB, then `export` on the
|
|
2150
|
-
* target to reconstitute the files. Egress is private-by-default — a skill
|
|
2151
|
-
* only leaves the machine once `share`d (cross_project_visibility org/public).
|
|
2152
|
-
*/
|
|
2153
|
-
async function cmdSkill(args) {
|
|
2154
|
-
const sub = args[0];
|
|
2155
|
-
if (!sub || sub === 'help' || sub === '--help') {
|
|
2156
|
-
console.log('Usage:\n'
|
|
2157
|
-
+ ' wyrm skill backfill-content read every skill\'s SKILL.md into the registry (idempotent)\n'
|
|
2158
|
-
+ ' wyrm skill export <targetDir> [--all] materialize SKILL.md files from stored content\n'
|
|
2159
|
-
+ ' wyrm skill share <name|--all|--tier T> [--public|--private] [--include-inactive] set cloud-sync visibility (single or bulk)');
|
|
2160
|
-
return;
|
|
2161
|
-
}
|
|
2162
|
-
const db = openDb();
|
|
2163
|
-
try {
|
|
2164
|
-
if (sub === 'backfill-content' || sub === 'backfill') {
|
|
2165
|
-
printSection('Backfill SKILL.md content into the registry');
|
|
2166
|
-
const r = db.backfillSkillContent();
|
|
2167
|
-
printSuccess(`${r.filled} filled . ${r.unchanged} unchanged . ${r.missing} missing-file (of ${r.total} registered)`);
|
|
2168
|
-
if (r.missing > 0)
|
|
2169
|
-
console.log(c.yellow(` ${r.missing} skill(s) had no readable SKILL.md — re-run after restoring their files.`));
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
if (sub === 'export') {
|
|
2173
|
-
const targetDir = args[1];
|
|
2174
|
-
if (!targetDir) {
|
|
2175
|
-
printError('Usage: wyrm skill export <targetDir> [--all]');
|
|
2176
|
-
db.close();
|
|
2177
|
-
process.exit(1);
|
|
2178
|
-
}
|
|
2179
|
-
const includeInactive = args.includes('--all');
|
|
2180
|
-
printSection(`Export skills → ${targetDir}`);
|
|
2181
|
-
const r = db.exportSkillContent(targetDir, { includeInactive });
|
|
2182
|
-
printSuccess(`${r.written} SKILL.md written . ${r.skipped_no_content} skipped (no stored content) (of ${r.total} ${includeInactive ? 'total' : 'active'})`);
|
|
2183
|
-
if (r.skipped_no_content > 0)
|
|
2184
|
-
console.log(c.yellow(' Run `wyrm skill backfill-content` on the source machine to populate content first.'));
|
|
2185
|
-
if (r.collisions > 0)
|
|
2186
|
-
console.log(c.yellow(` ${r.collisions} slug collision(s) disambiguated with a name-hash suffix (distinct skills, same slug) — no content lost.`));
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
if (sub === 'share') {
|
|
2190
|
-
const vis = args.includes('--private') ? 'within' : args.includes('--public') ? 'public' : 'org';
|
|
2191
|
-
const includeInactive = args.includes('--include-inactive');
|
|
2192
|
-
const tierIdx = args.indexOf('--tier');
|
|
2193
|
-
const tier = tierIdx >= 0 ? args[tierIdx + 1] : undefined;
|
|
2194
|
-
if (tierIdx >= 0 && (!tier || tier.startsWith('--'))) {
|
|
2195
|
-
printError('Usage: wyrm skill share --tier <god|mega|atomic> [--public|--private] [--include-inactive]');
|
|
2196
|
-
db.close();
|
|
2197
|
-
process.exit(1);
|
|
2198
|
-
}
|
|
2199
|
-
const bulk = args.includes('--all') || !!tier;
|
|
2200
|
-
if (bulk) {
|
|
2201
|
-
const n = db.setAllSkillsVisibility(vis, { tier, includeInactive });
|
|
2202
|
-
const scope = tier ? `tier '${tier}'` : includeInactive ? 'all skills' : 'all active skills';
|
|
2203
|
-
printSuccess(`${n} skill(s) (${scope}) visibility → '${vis}'.`);
|
|
2204
|
-
if (vis === 'within')
|
|
2205
|
-
console.log(' These skills will NOT egress on cloud sync (private).');
|
|
2206
|
-
else
|
|
2207
|
-
console.log(` ${n} skills are now cloud-sync-eligible (they leave on the next \`wyrm cloud sync\`).`);
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
const name = args[1];
|
|
2211
|
-
if (!name) {
|
|
2212
|
-
printError('Usage: wyrm skill share <name|--all|--tier <tier>> [--public|--private] [--include-inactive]');
|
|
2213
|
-
db.close();
|
|
2214
|
-
process.exit(1);
|
|
2215
|
-
}
|
|
2216
|
-
const row = db.setSkillVisibility(name, vis);
|
|
2217
|
-
if (!row) {
|
|
2218
|
-
printError(`Skill not found: ${name}`);
|
|
2219
|
-
db.close();
|
|
2220
|
-
process.exit(1);
|
|
2221
|
-
}
|
|
2222
|
-
printSuccess(`Skill "${name}" visibility → '${vis}'.`);
|
|
2223
|
-
if (vis === 'within')
|
|
2224
|
-
console.log(' This skill will NOT egress on cloud sync (private).');
|
|
2225
|
-
else
|
|
2226
|
-
console.log(' This skill is now cloud-sync-eligible (it leaves on the next `wyrm cloud sync`).');
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
printError(`Unknown skill subcommand: ${sub}`);
|
|
2230
|
-
process.exit(1);
|
|
2231
|
-
}
|
|
2232
|
-
finally {
|
|
2233
|
-
db.close();
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
/**
|
|
2237
|
-
* `wyrm grove`: manage and audit per-project replication lanes (sync_policy).
|
|
2238
|
-
* wyrm grove status list groves + leak audit
|
|
2239
|
-
* wyrm grove policy <project|id> <private|cloud|team> set a grove's lane
|
|
2240
|
-
*
|
|
2241
|
-
* private = never replicates. cloud = the operator's own cloud backup.
|
|
2242
|
-
* team = also federates to a team Wyrm. Per-row visibility / is_shared flags
|
|
2243
|
-
* still apply on top; the grove is the outer gate.
|
|
2244
|
-
*/
|
|
2245
|
-
async function cmdGrove(args) {
|
|
2246
|
-
const sub = args[0] ?? 'status';
|
|
2247
|
-
const db = openDb();
|
|
2248
|
-
const raw = db.getDatabase();
|
|
2249
|
-
const hasPolicy = raw.prepare('PRAGMA table_info(projects)').all()
|
|
2250
|
-
.some((col) => col.name === 'sync_policy');
|
|
2251
|
-
if (!hasPolicy) {
|
|
2252
|
-
printError('Grove sync policy is not on this database yet (upgrade Wyrm so migrations apply).');
|
|
2253
|
-
db.close();
|
|
2254
|
-
process.exit(1);
|
|
2255
|
-
}
|
|
2256
|
-
if (sub === 'status' || sub === 'ls' || sub === 'list') {
|
|
2257
|
-
printSection('Grove sync policy + leak audit');
|
|
2258
|
-
const projects = raw.prepare('SELECT id, name, sync_policy FROM projects ORDER BY id').all();
|
|
2259
|
-
const visTables = ['ground_truths', 'memory_artifacts', 'quests', 'design_tokens', 'design_references'];
|
|
2260
|
-
const sharedTables = ['ground_truths', 'memory_artifacts', 'quests', 'sessions', 'decision_edges'];
|
|
2261
|
-
const countWhere = (tables, pid, where) => {
|
|
2262
|
-
let n = 0;
|
|
2263
|
-
for (const t of tables) {
|
|
2264
|
-
try {
|
|
2265
|
-
n += raw.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE project_id = ? AND ${where}`).get(pid).n;
|
|
2266
|
-
}
|
|
2267
|
-
catch { /* table/col may be absent */ }
|
|
2268
|
-
}
|
|
2269
|
-
return n;
|
|
2270
|
-
};
|
|
2271
|
-
const rows = [];
|
|
2272
|
-
let leaks = 0;
|
|
2273
|
-
for (const p of projects) {
|
|
2274
|
-
const promoted = countWhere(visTables, p.id, "cross_project_visibility IN ('org','public')");
|
|
2275
|
-
const shared = countWhere(sharedTables, p.id, 'is_shared = 1');
|
|
2276
|
-
const isLeak = p.sync_policy === 'private' && (promoted + shared) > 0;
|
|
2277
|
-
if (isLeak)
|
|
2278
|
-
leaks++;
|
|
2279
|
-
const audit = isLeak
|
|
2280
|
-
? c.red(`LEAK: ${promoted} promoted + ${shared} shared in a PRIVATE grove`)
|
|
2281
|
-
: `${promoted} promoted / ${shared} shared`;
|
|
2282
|
-
rows.push([String(p.id), p.name, p.sync_policy, audit]);
|
|
2283
|
-
}
|
|
2284
|
-
console.log(formatTable(['#', 'Grove', 'sync_policy', 'rows eligible to leave'], rows));
|
|
2285
|
-
console.log('\nprivate = never replicates . cloud = your own cloud backup . team = federates to a team Wyrm');
|
|
2286
|
-
if (leaks > 0)
|
|
2287
|
-
console.log(c.red(`\n! ${leaks} private grove(s) hold rows marked to leave. Re-private those rows or change the grove lane.`));
|
|
2288
|
-
db.close();
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
if (sub === 'policy' || sub === 'set') {
|
|
2292
|
-
const ref = args[1];
|
|
2293
|
-
const policy = args[2];
|
|
2294
|
-
if (!ref || !['private', 'cloud', 'team'].includes(policy)) {
|
|
2295
|
-
printError('Usage: wyrm grove policy <project|id> <private|cloud|team>');
|
|
2296
|
-
db.close();
|
|
2297
|
-
process.exit(1);
|
|
2298
|
-
}
|
|
2299
|
-
let proj = resolveProject(db, ref);
|
|
2300
|
-
if (!proj && /^\d+$/.test(ref)) {
|
|
2301
|
-
proj = raw.prepare('SELECT id, name FROM projects WHERE id = ?').get(Number(ref));
|
|
2302
|
-
}
|
|
2303
|
-
if (!proj) {
|
|
2304
|
-
printError(`Grove not found: ${ref}`);
|
|
2305
|
-
db.close();
|
|
2306
|
-
process.exit(1);
|
|
2307
|
-
}
|
|
2308
|
-
raw.prepare('UPDATE projects SET sync_policy = ? WHERE id = ?').run(policy, proj.id);
|
|
2309
|
-
printSuccess(`Grove "${proj.name}" set to '${policy}'.`);
|
|
2310
|
-
if (policy !== 'private') {
|
|
2311
|
-
console.log(` Rows still only leave when also marked ${policy === 'team' ? 'is_shared (team)' : 'org/public (cloud)'}. The grove is the outer gate.`);
|
|
2312
|
-
}
|
|
2313
|
-
db.close();
|
|
2314
|
-
return;
|
|
2315
|
-
}
|
|
2316
|
-
printError(`Unknown grove subcommand: ${sub}`);
|
|
2317
|
-
console.log('Usage:\n wyrm grove status\n wyrm grove policy <project|id> <private|cloud|team>');
|
|
2318
|
-
db.close();
|
|
2319
|
-
process.exit(1);
|
|
2320
|
-
}
|
|
2321
|
-
if (command === '--version' || command === '-v' || command === 'version') {
|
|
2322
|
-
// Single source of truth (package.json), shared with the help banner.
|
|
2323
|
-
const pkg = readPkg();
|
|
2324
|
-
console.log(`${pkg.name ?? 'wyrm-mcp'} v${pkg.version ?? 'unknown'}`);
|
|
2325
|
-
}
|
|
2326
|
-
else if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
2327
|
-
printHelp();
|
|
2328
|
-
}
|
|
2329
|
-
else {
|
|
2330
|
-
(async () => {
|
|
2331
|
-
try {
|
|
2332
|
-
switch (command) {
|
|
2333
|
-
case 'search':
|
|
2334
|
-
await cmdSearch(restArgs);
|
|
2335
|
-
break;
|
|
2336
|
-
case 'ls':
|
|
2337
|
-
await cmdLs(restArgs);
|
|
2338
|
-
break;
|
|
2339
|
-
case 'show':
|
|
2340
|
-
await cmdShow(restArgs);
|
|
2341
|
-
break;
|
|
2342
|
-
case 'capture':
|
|
2343
|
-
await cmdCapture(restArgs);
|
|
2344
|
-
break;
|
|
2345
|
-
case 'rehydrate':
|
|
2346
|
-
await cmdRehydrate(restArgs);
|
|
2347
|
-
break;
|
|
2348
|
-
case 'render':
|
|
2349
|
-
await cmdRender(restArgs);
|
|
2350
|
-
break;
|
|
2351
|
-
case 'reverse-bridge':
|
|
2352
|
-
await cmdReverseBridge(restArgs);
|
|
2353
|
-
break;
|
|
2354
|
-
case 'import':
|
|
2355
|
-
await cmdImport(restArgs);
|
|
2356
|
-
break;
|
|
2357
|
-
case 'stats':
|
|
2358
|
-
await cmdStats(restArgs);
|
|
2359
|
-
break;
|
|
2360
|
-
case 'review':
|
|
2361
|
-
await cmdReview(restArgs);
|
|
2362
|
-
break;
|
|
2363
|
-
case 'sync':
|
|
2364
|
-
await cmdSync(restArgs);
|
|
2365
|
-
break;
|
|
2366
|
-
case 'cloud': {
|
|
2367
|
-
const { cmdCloud } = await import('./cloud/cli.js');
|
|
2368
|
-
await cmdCloud(restArgs);
|
|
2369
|
-
break;
|
|
2370
|
-
}
|
|
2371
|
-
case 'grove':
|
|
2372
|
-
await cmdGrove(restArgs);
|
|
2373
|
-
break;
|
|
2374
|
-
case 'skill':
|
|
2375
|
-
await cmdSkill(restArgs);
|
|
2376
|
-
break;
|
|
2377
|
-
case 'prune':
|
|
2378
|
-
await cmdPrune(restArgs);
|
|
2379
|
-
break;
|
|
2380
|
-
// v7 F3 (T023): CLI-exile subcommands — the `wyrm` replacements the
|
|
2381
|
-
// 22 exiled MCP names redirect to (src/deprecations.ts CLI_EXILE_TOOLS).
|
|
2382
|
-
case 'license':
|
|
2383
|
-
await cmdLicense();
|
|
2384
|
-
break;
|
|
2385
|
-
case 'login':
|
|
2386
|
-
await cmdLogin();
|
|
2387
|
-
break;
|
|
2388
|
-
case 'activate':
|
|
2389
|
-
await cmdActivate(restArgs);
|
|
2390
|
-
break;
|
|
2391
|
-
case 'maintenance':
|
|
2392
|
-
await cmdMaintenance(restArgs);
|
|
2393
|
-
break;
|
|
2394
|
-
case 'index':
|
|
2395
|
-
await cmdIndexVectors(restArgs);
|
|
2396
|
-
break;
|
|
2397
|
-
case 'update':
|
|
2398
|
-
await cmdUpdate(restArgs);
|
|
2399
|
-
break;
|
|
2400
|
-
case 'prompt':
|
|
2401
|
-
await cmdPrompt(restArgs);
|
|
2402
|
-
break;
|
|
2403
|
-
case 'hours':
|
|
2404
|
-
await cmdHours(restArgs);
|
|
2405
|
-
break;
|
|
2406
|
-
case 'invoice':
|
|
2407
|
-
await cmdInvoice(restArgs);
|
|
2408
|
-
break;
|
|
2409
|
-
case 'agent':
|
|
2410
|
-
await cmdAgent(restArgs);
|
|
2411
|
-
break;
|
|
2412
|
-
case 'setup':
|
|
2413
|
-
await cmdSetup(restArgs);
|
|
2414
|
-
break;
|
|
2415
|
-
case 'intro': {
|
|
2416
|
-
const { renderIntro } = await import('./visibility.js');
|
|
2417
|
-
console.log(renderIntro(readPkg().version ?? 'unknown'));
|
|
2418
|
-
break;
|
|
2419
|
-
}
|
|
2420
|
-
case 'events':
|
|
2421
|
-
await cmdEvents(restArgs);
|
|
2422
|
-
break;
|
|
2423
|
-
case 'watch':
|
|
2424
|
-
await cmdWatch(restArgs);
|
|
2425
|
-
break;
|
|
2426
|
-
case 'embed':
|
|
2427
|
-
await cmdEmbed(restArgs);
|
|
2428
|
-
break;
|
|
2429
|
-
case 'harvest':
|
|
2430
|
-
await cmdHarvest(restArgs);
|
|
2431
|
-
break;
|
|
2432
|
-
case 'vault':
|
|
2433
|
-
await cmdVault(restArgs);
|
|
2434
|
-
break;
|
|
2435
|
-
case 'statusline': {
|
|
2436
|
-
const { installClaudeStatusline, removeClaudeStatusline } = await import('./autoconfig.js');
|
|
2437
|
-
const res = restArgs.includes('--remove') ? removeClaudeStatusline() : installClaudeStatusline();
|
|
2438
|
-
if (!res)
|
|
2439
|
-
printError('Claude Code not detected (~/.claude missing).');
|
|
2440
|
-
else
|
|
2441
|
-
printSuccess(res.message);
|
|
2442
|
-
break;
|
|
2443
|
-
}
|
|
2444
|
-
// Easy dashboard access: `wyrm ui` / `wyrm dashboard` == `wyrm serve --ui`
|
|
2445
|
-
// (starts the HTTP server and opens the /ui dashboard in the browser).
|
|
2446
|
-
case 'ui':
|
|
2447
|
-
case 'dashboard':
|
|
2448
|
-
if (!restArgs.includes('--ui'))
|
|
2449
|
-
restArgs.push('--ui');
|
|
2450
|
-
// falls through to `serve`
|
|
2451
|
-
case 'serve': {
|
|
2452
|
-
const hasUiFlag = restArgs.includes('--ui');
|
|
2453
|
-
if (hasUiFlag) {
|
|
2454
|
-
const { enableDevMode } = await import('./http-auth.js');
|
|
2455
|
-
enableDevMode();
|
|
2456
|
-
}
|
|
2457
|
-
// Start HTTP server inline
|
|
2458
|
-
const { server } = await import('./http-fast.js');
|
|
2459
|
-
const port = parseInt(process.env.WYRM_PORT ?? process.env.PORT ?? '3333', 10);
|
|
2460
|
-
const bindHost = process.env.WYRM_BIND_HOST || '127.0.0.1';
|
|
2461
|
-
server.listen(port, bindHost, () => {
|
|
2462
|
-
printSuccess(`Wyrm HTTP server running on ${bindHost}:${port}`);
|
|
2463
|
-
if (process.env.WYRM_UI_READONLY === '1') {
|
|
2464
|
-
console.log('🔒 READ-ONLY mode: writes + off-box egress are blocked; safe to expose.');
|
|
2465
|
-
}
|
|
2466
|
-
if (hasUiFlag) {
|
|
2467
|
-
const url = `http://localhost:${port}/ui`;
|
|
2468
|
-
console.log(`🖥️ Dashboard: ${url}`);
|
|
2469
|
-
// Best-effort browser launch; never crash the server if the
|
|
2470
|
-
// OS opener isn't available (CI, headless box, missing PATH).
|
|
2471
|
-
import('child_process').then(({ spawn }) => {
|
|
2472
|
-
const plt = process.platform;
|
|
2473
|
-
try {
|
|
2474
|
-
const child = plt === 'darwin'
|
|
2475
|
-
? spawn('open', [url], { stdio: 'ignore', detached: true })
|
|
2476
|
-
: plt === 'win32'
|
|
2477
|
-
? spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true })
|
|
2478
|
-
: spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
2479
|
-
child.on('error', () => { });
|
|
2480
|
-
child.unref();
|
|
2481
|
-
}
|
|
2482
|
-
catch { /* spawn threw synchronously — also fine */ }
|
|
2483
|
-
}).catch(() => { });
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2486
|
-
break;
|
|
2487
|
-
}
|
|
2488
|
-
default:
|
|
2489
|
-
printError(`Unknown command: ${command}`);
|
|
2490
|
-
printHelp();
|
|
2491
|
-
process.exit(1);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
catch (err) {
|
|
2495
|
-
printError(String(err));
|
|
2496
|
-
process.exit(1);
|
|
2497
|
-
}
|
|
2498
|
-
})();
|
|
2499
|
-
}
|
|
2500
|
-
//# sourceMappingURL=wyrm-cli.js.map
|
|
140
|
+
`)}const[,,I,...k]=process.argv;function ye(l){const i=l.ref_table?`${l.ref_table}${l.ref_id?"#"+l.ref_id:""}`:"",e=new Date(l.created_at).toLocaleTimeString();return`#${l.cursor} ${l.kind.padEnd(15)} ${i.padEnd(16)} ${Te(l.actor)} ${e}`}async function ot(l){const{positional:i,flags:e}=x(l),s=i[0]||"since",r=E();try{if(!r.liveMemoryEnabled()){p("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),process.exitCode=1;return}const t=(typeof e.project=="string"?e.project:"")||process.cwd(),n=r.getProject(t)??r.getProjectByName(t);if(!n){p(`Project not found: ${t}`),process.exitCode=1;return}if(s==="publish"){const a=i[1]||(typeof e.kind=="string"?e.kind:"");if(!a){p("usage: wyrm events publish <kind> --project <p> [--actor A] [--ref-table T --ref-id ID]"),process.exitCode=1;return}r.publishEvent({projectId:n.id,kind:a,refTable:typeof e["ref-table"]=="string"?e["ref-table"]:void 0,refId:typeof e["ref-id"]=="string"?e["ref-id"]:void 0,actor:typeof e.actor=="string"?e.actor:void 0}),v(`Event published (${a}) to ${n.name}`);return}if(s==="since"){const a=R(e.cursor,0),o=R(e.limit,50),u=r.eventsSince(n.id,a,o);for(const c of u)console.log(ye(c));j(`${u.length} event(s) for '${n.name}' since cursor ${a}`);return}p("usage: wyrm events <publish|since> ..."),process.exitCode=1}finally{r.close()}}async function st(l){const{flags:i}=x(l),e=E();if(!e.liveMemoryEnabled()){p("Live Memory is disabled (set WYRM_LIVE_MEMORY=1)."),e.close(),process.exitCode=1;return}const s=(typeof i.project=="string"?i.project:"")||process.cwd(),r=e.getProject(s)??P(e,s);if(!r){p(`Project not found: ${s}`),e.close(),process.exitCode=1;return}const t=Math.max(250,R(i.interval,1e3));let n=R(i.since,e.subscribeEvents(r.id,1).cursor);j(`Watching '${r.name}' (cursor ${n}, every ${t}ms) \u2014 Ctrl-C to stop`);const a=()=>{try{for(const c of e.eventsSince(r.id,n,200))n=c.cursor,console.log(ye(c))}catch{}};a();const o=setInterval(a,t),u=()=>{clearInterval(o);try{e.close()}catch{}process.exit(0)};process.on("SIGINT",u),process.on("SIGTERM",u)}async function rt(l){const{flags:i}=x(l),{embedAll:e,removeAll:s,statusAll:r}=await import("./priority-embed.js"),t={projectDir:typeof i.project=="string"?i.project:void 0,allClients:i.all===!0},n=a=>console.log(` ${String(a.result??a.status).padEnd(9)} [${a.scope}] ${a.file}`);if(i.status){j("Wyrm priority embedding \u2014 status"),r(t).forEach(n);return}if(i.remove){j("Wyrm priority embedding \u2014 removed"),s(t).forEach(n);return}j("Wyrm is now FIRST-PRIORITY memory"),e(t).forEach(n);try{const{installClaudeCodeHooks:a,installClaudeStatusline:o}=await import("./autoconfig.js");a()?v("Proactive hooks installed (SessionStart rehydrate + capture + tool-trace)."):console.log(" (Claude Code not detected \u2014 skipped hook install.)");const c=o();c&&v(`Buddy statusline: ${c.message}`)}catch{}v("Wyrm will now be read first, primed proactively, and shown in the TUI at all times.")}async function nt(l){const{flags:i}=x(l),{harvestProjects:e}=await import("./harvest.js"),{MemoryArtifacts:s}=await import("./memory-artifacts.js"),{escapeLikePattern:r}=await import("./auto-capture.js"),t=E();try{const n=t.getDatabase(),a=new s(n),o={existsBySig:(h,b)=>!!n.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(h,"%"+r(b)+"%"),addCandidate:(h,b)=>a.add(h,{kind:b.kind,problem:b.text,tags:[...b.tags,b.sig],confidence:b.confidence,needsReview:1,createdBy:"harvest"})},u=typeof i.project=="string"?i.project:void 0;let c;if(u){const h=t.getProject(u)??t.getProjectByName(u);if(!h){p(`Project not found: ${u}`),process.exitCode=1;return}c=[{id:h.id,name:h.name,path:h.path}]}else c=t.getAllProjects(500).map(h=>({id:h.id,name:h.name,path:h.path}));const m=i["dry-run"]===!0||i.dry===!0,y=i.code===!0||i["include-code"]===!0,{reports:f,totalAdded:g,totalSkipped:w}=e(o,c,{dryRun:m,gitLimit:R(i.limit,30),includeCode:y});j(`Harvest ${m?"(dry run) ":""}\u2014 ${g} candidate(s), ${w} already present (${c.length} project(s))`);for(const h of f.filter(b=>b.added>0).sort((b,_)=>_.added-b.added).slice(0,25))console.log(` +${String(h.added).padStart(3)} (skip ${h.skipped}) ${h.project}`);if(y&&!m){const{SymbolGraph:h}=await import("./symbols.js"),b=new h(n);let _=0,S=0;for(const $ of c)try{const T=b.indexProject($.id,$.path);_+=T.symbols,S+=T.files}catch{}console.log(` \u{1F4D0} Indexed ${_} code symbols (${S} files) \u2192 searchable via 'wyrm search'`)}!m&&g>0&&v("Review with: wyrm review")}finally{t.close()}}async function it(l){const i=await import("./vault.js"),[e,...s]=l;try{switch(e){case"set":{const r=s[0];if(!r){p("usage: wyrm vault set <name> (the secret is read from STDIN, never argv)"),process.exitCode=1;return}if(process.stdin.isTTY){p(`pipe the secret in, e.g.: printf %s "$TOKEN" | wyrm vault set ${r}`),process.exitCode=1;return}const n=(await import("node:fs")).readFileSync(0,"utf8").replace(/\r?\n$/,"");if(!n){p("empty secret on stdin"),process.exitCode=1;return}i.vaultSet(r,n),v(`Stored "${r}" (AES-256-GCM). Use it without exposing it: wyrm vault exec ${r} -- <command>`);break}case"get":{const r=s[0];if(!r){p("usage: wyrm vault get <name>"),process.exitCode=1;return}const t=i.vaultGet(r);if(t===void 0){p(`no secret named "${r}"`),process.exitCode=1;return}process.stdout.write(t);break}case"list":case"ls":{const r=i.vaultList();if(!r.length){console.log("(vault is empty)");break}j(`Vault \u2014 ${r.length} secret(s)`);for(const t of r)console.log(" \u2022 "+t);break}case"rm":case"remove":case"delete":{const r=s[0];if(!r){p("usage: wyrm vault rm <name>"),process.exitCode=1;return}v(i.vaultRemove(r)?`Removed "${r}"`:`(no secret named "${r}")`);break}case"exec":{const r=s[0],t=s.indexOf("--");if(!r||t===-1||t+1>=s.length){p("usage: wyrm vault exec <name> [--as ENVVAR] -- <command...>"),process.exitCode=1;return}const n=s.slice(1,t),a=n.indexOf("--as"),o=a>=0?n[a+1]:r.toUpperCase().replace(/[^A-Z0-9]+/g,"_"),u=s.slice(t+1),c=i.vaultGet(r);if(c===void 0){p(`no secret named "${r}"`),process.exitCode=1;return}const m=B(u[0],u.slice(1),{stdio:"inherit",env:{...process.env,[o]:c}});process.exitCode=m.status??1;break}case"import-npm":{const r=await import("node:fs"),t=await import("node:os"),a=(await import("node:path")).join(t.homedir(),".npmrc");if(!r.existsSync(a)){p("~/.npmrc not found"),process.exitCode=1;return}const o=r.readFileSync(a,"utf8").match(/\/\/registry\.npmjs\.org\/:_authToken=(.+)/);if(!o){p("no npm authToken found in ~/.npmrc"),process.exitCode=1;return}i.vaultSet("npm-token",o[1].trim()),v('Imported npm token \u2192 vault as "npm-token". You can now scrub the plaintext from ~/.npmrc and use: wyrm vault exec npm-token --as NODE_AUTH_TOKEN -- npm publish');break}case"setup":{const r=i.vaultPaths();if(r.secure)v(`Vault is already secure (backend: ${r.backend}, ${r.count} secret(s)).`);else{const t=r.keychainAvailable?"keychain":"passphrase";if(t==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){p("No OS keychain on this host. Set WYRM_VAULT_PASSPHRASE, then re-run: wyrm vault setup"),process.exitCode=1;return}const n=i.vaultSecure({backend:t});v(`Vault secured: ${n.from} \u2192 ${n.to}${n.rotated?" (key rotated)":""}.`),n.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),n.backup&&console.log(` ciphertext backup: ${n.backup} (delete once confirmed)`)}j("Store & use credentials safely"),console.log(' store: printf %s "$TOKEN" | wyrm vault set <name> # reads STDIN \u2014 never argv/shell history'),console.log(" use: wyrm vault exec <name> --as ENV_VAR -- <cmd> # injected as env var, never printed"),console.log(" list: wyrm vault list inspect: wyrm vault info");break}case"secure":{const r=s.indexOf("--backend"),t=r>=0?s[r+1]:"keychain";if(t!=="keychain"&&t!=="passphrase"){p("usage: wyrm vault secure [--backend keychain|passphrase] [--no-rotate]"),process.exitCode=1;return}if(t==="passphrase"&&!process.env.WYRM_VAULT_PASSPHRASE){p("set WYRM_VAULT_PASSPHRASE before: wyrm vault secure --backend passphrase"),process.exitCode=1;return}const n=!s.includes("--no-rotate"),a=i.vaultSecure({backend:t,rotate:n});v(`Vault secured: ${a.from} \u2192 ${a.to}${a.rotated?" (key rotated)":""}.`),console.log(` secrets re-encrypted: ${a.secrets}`),a.keyfileShredded&&console.log(" plaintext vault.key: shredded \u2714"),a.backup&&console.log(` ciphertext backup: ${a.backup} (delete once you've confirmed)`),console.log(t==="keychain"?" master key now lives in the OS keychain \u2014 not on disk.":" master key now derived from WYRM_VAULT_PASSPHRASE \u2014 keep that set for future use.");break}case"info":{const r=i.vaultPaths();j("Vault"),console.log(` backend: ${r.backend}`),console.log(` secrets: ${r.count}`),console.log(` store: ${r.vault} (0600)`),console.log(` key: ${r.backend==="keyfile"?r.key+" (0600)":r.backend==="keychain"?"(OS keychain \u2014 no key on disk)":"(derived from WYRM_VAULT_PASSPHRASE \u2014 no key on disk)"}`),console.log(` secure: ${r.secure?"yes \u2014 key is not a plaintext file beside the ciphertext":"NO \u2014 key sits beside ciphertext"}`),r.secure||console.log(r.keychainAvailable?" \u26A0 run `wyrm vault secure` to move the key into the OS keychain.":" \u26A0 no OS keychain found \u2014 set WYRM_VAULT_PASSPHRASE and run `wyrm vault secure --backend passphrase`.");break}default:p("usage: wyrm vault <setup|set|get|list|rm|exec|import-npm|secure|info>"),process.exitCode=1}}catch(r){p(`vault: ${r.message}`),process.exitCode=1}}async function at(l){const i=l[0];if(!i||i==="help"||i==="--help"){console.log(`Usage:
|
|
141
|
+
wyrm skill backfill-content read every skill's SKILL.md into the registry (idempotent)
|
|
142
|
+
wyrm skill export <targetDir> [--all] materialize SKILL.md files from stored content
|
|
143
|
+
wyrm skill share <name|--all|--tier T> [--public|--private] [--include-inactive] set cloud-sync visibility (single or bulk)`);return}const e=E();try{if(i==="backfill-content"||i==="backfill"){j("Backfill SKILL.md content into the registry");const s=e.backfillSkillContent();v(`${s.filled} filled . ${s.unchanged} unchanged . ${s.missing} missing-file (of ${s.total} registered)`),s.missing>0&&console.log(d.yellow(` ${s.missing} skill(s) had no readable SKILL.md \u2014 re-run after restoring their files.`));return}if(i==="export"){const s=l[1];s||(p("Usage: wyrm skill export <targetDir> [--all]"),e.close(),process.exit(1));const r=l.includes("--all");j(`Export skills \u2192 ${s}`);const t=e.exportSkillContent(s,{includeInactive:r});v(`${t.written} SKILL.md written . ${t.skipped_no_content} skipped (no stored content) (of ${t.total} ${r?"total":"active"})`),t.skipped_no_content>0&&console.log(d.yellow(" Run `wyrm skill backfill-content` on the source machine to populate content first.")),t.collisions>0&&console.log(d.yellow(` ${t.collisions} slug collision(s) disambiguated with a name-hash suffix (distinct skills, same slug) \u2014 no content lost.`));return}if(i==="share"){const s=l.includes("--private")?"within":l.includes("--public")?"public":"org",r=l.includes("--include-inactive"),t=l.indexOf("--tier"),n=t>=0?l[t+1]:void 0;if(t>=0&&(!n||n.startsWith("--"))&&(p("Usage: wyrm skill share --tier <god|mega|atomic> [--public|--private] [--include-inactive]"),e.close(),process.exit(1)),l.includes("--all")||!!n){const c=e.setAllSkillsVisibility(s,{tier:n,includeInactive:r}),m=n?`tier '${n}'`:r?"all skills":"all active skills";v(`${c} skill(s) (${m}) visibility \u2192 '${s}'.`),console.log(s==="within"?" These skills will NOT egress on cloud sync (private).":` ${c} skills are now cloud-sync-eligible (they leave on the next \`wyrm cloud sync\`).`);return}const o=l[1];o||(p("Usage: wyrm skill share <name|--all|--tier <tier>> [--public|--private] [--include-inactive]"),e.close(),process.exit(1)),e.setSkillVisibility(o,s)||(p(`Skill not found: ${o}`),e.close(),process.exit(1)),v(`Skill "${o}" visibility \u2192 '${s}'.`),console.log(s==="within"?" This skill will NOT egress on cloud sync (private).":" This skill is now cloud-sync-eligible (it leaves on the next `wyrm cloud sync`).");return}p(`Unknown skill subcommand: ${i}`),process.exit(1)}finally{e.close()}}async function ct(l){const i=l[0]??"status",e=E(),s=e.getDatabase();if(s.prepare("PRAGMA table_info(projects)").all().some(t=>t.name==="sync_policy")||(p("Grove sync policy is not on this database yet (upgrade Wyrm so migrations apply)."),e.close(),process.exit(1)),i==="status"||i==="ls"||i==="list"){j("Grove sync policy + leak audit");const t=s.prepare("SELECT id, name, sync_policy FROM projects ORDER BY id").all(),n=["ground_truths","memory_artifacts","quests","design_tokens","design_references"],a=["ground_truths","memory_artifacts","quests","sessions","decision_edges"],o=(m,y,f)=>{let g=0;for(const w of m)try{g+=s.prepare(`SELECT COUNT(*) AS n FROM ${w} WHERE project_id = ? AND ${f}`).get(y).n}catch{}return g},u=[];let c=0;for(const m of t){const y=o(n,m.id,"cross_project_visibility IN ('org','public')"),f=o(a,m.id,"is_shared = 1"),g=m.sync_policy==="private"&&y+f>0;g&&c++;const w=g?d.red(`LEAK: ${y} promoted + ${f} shared in a PRIVATE grove`):`${y} promoted / ${f} shared`;u.push([String(m.id),m.name,m.sync_policy,w])}console.log(M(["#","Grove","sync_policy","rows eligible to leave"],u)),console.log(`
|
|
144
|
+
private = never replicates . cloud = your own cloud backup . team = federates to a team Wyrm`),c>0&&console.log(d.red(`
|
|
145
|
+
! ${c} private grove(s) hold rows marked to leave. Re-private those rows or change the grove lane.`)),e.close();return}if(i==="policy"||i==="set"){const t=l[1],n=l[2];(!t||!["private","cloud","team"].includes(n))&&(p("Usage: wyrm grove policy <project|id> <private|cloud|team>"),e.close(),process.exit(1));let a=P(e,t);!a&&/^\d+$/.test(t)&&(a=s.prepare("SELECT id, name FROM projects WHERE id = ?").get(Number(t))),a||(p(`Grove not found: ${t}`),e.close(),process.exit(1)),s.prepare("UPDATE projects SET sync_policy = ? WHERE id = ?").run(n,a.id),v(`Grove "${a.name}" set to '${n}'.`),n!=="private"&&console.log(` Rows still only leave when also marked ${n==="team"?"is_shared (team)":"org/public (cloud)"}. The grove is the outer gate.`),e.close();return}p(`Unknown grove subcommand: ${i}`),console.log(`Usage:
|
|
146
|
+
wyrm grove status
|
|
147
|
+
wyrm grove policy <project|id> <private|cloud|team>`),e.close(),process.exit(1)}if(I==="--version"||I==="-v"||I==="version"){const l=O();console.log(`${l.name??"wyrm-mcp"} v${l.version??"unknown"}`)}else!I||I==="--help"||I==="-h"||I==="help"?fe():(async()=>{try{switch(I){case"search":await Me(k);break;case"ls":await Ie(k);break;case"show":await Ae(k);break;case"capture":await Ne(k);break;case"rehydrate":await Le(k);break;case"render":await Oe(k);break;case"reverse-bridge":await We(k);break;case"import":await Ye(k);break;case"stats":await qe(k);break;case"review":await Fe(k);break;case"sync":await Ue(k);break;case"cloud":{const{cmdCloud:l}=await import("./cloud/cli.js");await l(k);break}case"grove":await ct(k);break;case"skill":await at(k);break;case"prune":await He(k);break;case"license":await Ve();break;case"login":await Be();break;case"activate":await Ke(k);break;case"maintenance":await Ge(k);break;case"index":await Je(k);break;case"update":await ze(k);break;case"prompt":await Qe(k);break;case"hours":await Xe(k);break;case"invoice":await Ze(k);break;case"agent":await et(k);break;case"setup":await tt(k);break;case"intro":{const{renderIntro:l}=await import("./visibility.js");console.log(l(O().version??"unknown"));break}case"events":await ot(k);break;case"watch":await st(k);break;case"embed":await rt(k);break;case"harvest":await nt(k);break;case"vault":await it(k);break;case"statusline":{const{installClaudeStatusline:l,removeClaudeStatusline:i}=await import("./autoconfig.js"),e=k.includes("--remove")?i():l();e?v(e.message):p("Claude Code not detected (~/.claude missing).");break}case"ui":case"dashboard":k.includes("--ui")||k.push("--ui");case"serve":{const l=k.includes("--ui");if(l){const{enableDevMode:r}=await import("./http-auth.js");r()}const{server:i}=await import("./http-fast.js"),e=parseInt(process.env.WYRM_PORT??process.env.PORT??"3333",10),s=process.env.WYRM_BIND_HOST||"127.0.0.1";i.listen(e,s,()=>{if(v(`Wyrm HTTP server running on ${s}:${e}`),process.env.WYRM_UI_READONLY==="1"&&console.log("\u{1F512} READ-ONLY mode: writes + off-box egress are blocked; safe to expose."),l){const r=`http://localhost:${e}/ui`;console.log(`\u{1F5A5}\uFE0F Dashboard: ${r}`),import("child_process").then(({spawn:t})=>{const n=process.platform;try{const a=n==="darwin"?t("open",[r],{stdio:"ignore",detached:!0}):n==="win32"?t("cmd",["/c","start","",r],{stdio:"ignore",detached:!0}):t("xdg-open",[r],{stdio:"ignore",detached:!0});a.on("error",()=>{}),a.unref()}catch{}}).catch(()=>{})}});break}default:p(`Unknown command: ${I}`),fe(),process.exit(1)}}catch(l){p(String(l)),process.exit(1)}})();
|