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