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.
Files changed (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. 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 ? ${projectClause}
5
+ WHERE memory_artifacts_fts MATCH ? ${m}
116
6
  LIMIT 20
117
- `).all(query);
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 ? ${projectClause}
10
+ WHERE sessions_fts MATCH ? ${m}
132
11
  LIMIT 10
133
- `).all(query);
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 ? ${projectClause}
15
+ WHERE quests_fts MATCH ? ${m}
148
16
  LIMIT 10
149
- `).all(query);
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 ? ${projectClause}
20
+ WHERE data_lake_fts MATCH ? ${m}
164
21
  LIMIT 10
165
- `).all(query);
166
- for (const d of data) {
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
- ${projectClause}
25
+ ${u}
197
26
  ORDER BY created_at DESC LIMIT ?
198
- `).all(limit);
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 ${projectClauseAnd}
29
+ WHERE is_current = 1 ${c}
218
30
  ORDER BY created_at DESC LIMIT ?
219
- `).all(limit);
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
- ${projectClause}
33
+ ${u}
238
34
  ORDER BY created_at DESC LIMIT ?
239
- `).all(limit);
240
- if (quests.length > 0) {
241
- const rows = quests.map(q => [
242
- `quest:${q.id}`,
243
- q.priority,
244
- q.title.slice(0, 60),
245
- q.status,
246
- ]);
247
- console.log(formatTable(['ID', 'Priority', 'Title', 'Status'], rows));
248
- }
249
- else {
250
- console.log(c.dim(' No quests yet.'));
251
- }
252
- }
253
- db.close();
254
- }
255
- async function cmdShow(args) {
256
- const { positional } = parseArgs(args);
257
- const typedId = positional[0];
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 ${projectClause}
55
+ WHERE needs_review = 1 ${n}
850
56
  ORDER BY created_at ASC
851
- `).all();
852
- if (pending.length === 0) {
853
- console.log(c.dim(` No artifacts pending review${projectName ? ` for ${projectName}` : ''}.`));
854
- db.close();
855
- return;
856
- }
857
- printSection(`Review Queue (${pending.length} items)`);
858
- const rl = createInterface({ input: process.stdin, output: process.stdout });
859
- const ask = (question) => new Promise(resolve => { rl.question(question, resolve); });
860
- for (const item of pending) {
861
- console.log(`\n${c.bold(`[${item.kind}] #${item.id}`)}`);
862
- console.log(c.dim('─'.repeat(60)));
863
- console.log(item.problem.slice(0, 300));
864
- console.log(c.dim('─'.repeat(60)));
865
- const answer = await ask(`${c.cyan('[a]')}pprove / ${c.red('[r]')}eject / ${c.yellow('[s]')}kip? `);
866
- const choice = answer.trim().toLowerCase();
867
- if (choice === 'a') {
868
- rawDb.prepare('UPDATE memory_artifacts SET needs_review = 0, updated_at = datetime(\'now\') WHERE id = ?').run(item.id);
869
- printSuccess(`Approved #${item.id}`);
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
- ${c.bold('Usage:')} wyrm <command> [options]
78
+ ${d.bold("Usage:")} wyrm <command> [options]
1688
79
 
1689
- ${c.bold('Commands:')}
1690
- ${c.cyan('search')} <query> Search across memories, truths, quests, and data
1691
- ${c.cyan('ls')} List stored items
1692
- ${c.cyan('show')} <typed-id> Show full detail (mem:N, quest:N, truth:N, data:N, session:N)
1693
- ${c.cyan('capture')} "<content>" Classify and capture a thought, task, or lesson
1694
- ${c.cyan('rehydrate')} [--project <name>] Print the latest session's continuity brief (for SessionStart)
1695
- ${c.cyan('render')} [--client <list>] [--brief] Compile DB MEMORY.md + topic files (zero-MCP-token memory)
1696
- ${c.cyan('import git')} Import git log history to review queue
1697
- ${c.cyan('import rules')} <path> Import .cursorrules / copilot-instructions file
1698
- ${c.cyan('stats')} Show database statistics
1699
- ${c.cyan('review')} Interactively review pending artifacts
1700
- ${c.cyan('sync export')} --out <path> Export encrypted DB snapshot
1701
- ${c.cyan('sync import')} --from <path> Restore from encrypted snapshot (backs up first)
1702
- ${c.cyan('sync preview')} --from <path> Preview snapshot stats without restoring
1703
- ${c.cyan('prune')} [--project <name>] Prune stale low-confidence artifacts (dry-run by default)
1704
- ${c.cyan('cloud')} <login|sync|export|…> Cloud backup & multi-device sync (Pro license)
1705
- ${c.cyan('grove')} <status|policy> Per-project sync lanes (private|cloud|team) + leak audit
1706
- ${c.cyan('skill')} <backfill-content|export|share> Portable skills: store SKILL.md content + sync/export
1707
- ${c.cyan('serve')} [--ui] Start the Wyrm HTTP server (optionally open the dashboard)
1708
- ${c.cyan('setup')} [--check|--encrypt|…] Auto-configure AI clients (wyrm-setup); --encrypt for at-rest crypto
1709
- ${c.cyan('intro')} What Wyrm is and how to use it
1710
- ${c.cyan('license')} Show license status & features
1711
- ${c.cyan('login')} Sign in (free account) and activate required on official builds
1712
- ${c.cyan('activate')} <key|path> Activate a license (JSON arg, file path, or stdin)
1713
- ${c.cyan('maintenance')} [--vacuum] Archive/prune/sweep/checkpoint the database
1714
- ${c.cyan('index')} <setup|rebuild|status> Vector search: verify provider, reindex, stats
1715
- ${c.cyan('update')} [--check] Check for / install the latest wyrm-mcp
1716
- ${c.cyan('prompt')} <inject|migrate> Inject/refresh the session-prime block in client files
1717
- ${c.cyan('hours')} report --from --to Hours report derived from session history
1718
- ${c.cyan('invoice')} generate --client Markdown invoice from the hours ledger
1719
- ${c.cyan('agent')} <init|status|stop|restart> Background agent daemon (goal loop)
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
- ${c.bold('Options:')}
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
- ${c.bold('Environment:')}
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
- ${c.bold('Examples:')}
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
- // ==================== MAIN ====================
1752
- const [, , command, ...restArgs] = process.argv;
1753
- function fmtEvent(e) {
1754
- const ref = e.ref_table ? `${e.ref_table}${e.ref_id ? '#' + e.ref_id : ''}` : '';
1755
- const when = new Date(e.created_at).toLocaleTimeString();
1756
- // v7 (T008): NULL attribution reads as actor='legacy' at display read sites.
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)}})();