@cmnwlth/curate 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,738 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import path4 from "path";
5
+ import { promises as fs3 } from "fs";
6
+ import { parseArgs } from "util";
7
+ import {
8
+ computeBrainHealth,
9
+ FEATURE_FLAGS,
10
+ loadBrainConfig as loadBrainConfig2,
11
+ NOTE_KINDS,
12
+ resolveBrainDir,
13
+ resolveProjectSource,
14
+ setFeature
15
+ } from "@cmnwlth/core";
16
+
17
+ // src/capture.ts
18
+ import { isFeatureEnabled as isFeatureEnabled2 } from "@cmnwlth/core";
19
+
20
+ // src/curate.ts
21
+ import {
22
+ hasSecrets,
23
+ isFeatureEnabled,
24
+ listNotes as listNotes2,
25
+ loadBrainConfig,
26
+ scanOptions
27
+ } from "@cmnwlth/core";
28
+
29
+ // src/staging.ts
30
+ import path from "path";
31
+ import { listNotes, writeNote } from "@cmnwlth/core";
32
+ var STAGING_DIR = "staging";
33
+ function stagingRoot(brainDir) {
34
+ return path.join(brainDir, STAGING_DIR);
35
+ }
36
+ function stageNote(brainDir, input) {
37
+ return writeNote(stagingRoot(brainDir), input);
38
+ }
39
+ function listStaged(brainDir) {
40
+ return listNotes(stagingRoot(brainDir));
41
+ }
42
+ function stagedAbsPath(brainDir, note) {
43
+ return path.join(stagingRoot(brainDir), note.path);
44
+ }
45
+
46
+ // src/curate.ts
47
+ var MIN_BODY_LENGTH = 15;
48
+ var DUPLICATE_THRESHOLD = 0.8;
49
+ function tokenSet(text) {
50
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").trim();
51
+ const tokens = normalized.split(/\s+/).filter((t) => t.length > 0);
52
+ return new Set(tokens);
53
+ }
54
+ function jaccard(a, b) {
55
+ if (a.size === 0 && b.size === 0) return 0;
56
+ let intersection = 0;
57
+ for (const token of a) {
58
+ if (b.has(token)) intersection += 1;
59
+ }
60
+ const union = a.size + b.size - intersection;
61
+ return union === 0 ? 0 : intersection / union;
62
+ }
63
+ function textSimilarity(a, b) {
64
+ return jaccard(tokenSet(a), tokenSet(b));
65
+ }
66
+ function candidateText(input) {
67
+ return `${input.title} ${input.body}`;
68
+ }
69
+ function candidateSecretScanText(input) {
70
+ const parts = [input.title, input.body, ...input.tags ?? []];
71
+ if (input.author) parts.push(input.author);
72
+ if (input.source) parts.push(input.source);
73
+ if (input.fields && Object.keys(input.fields).length > 0)
74
+ parts.push(JSON.stringify(input.fields));
75
+ return parts.join("\n");
76
+ }
77
+ function noteText(note) {
78
+ return `${note.frontmatter.title} ${note.body}`;
79
+ }
80
+ var defaultCurator = {
81
+ assess(candidate, existing) {
82
+ if (candidate.title.trim().length === 0 || candidate.body.trim().length < MIN_BODY_LENGTH) {
83
+ return { accept: false, reason: "too-thin" };
84
+ }
85
+ const candidateTokens = tokenSet(candidateText(candidate));
86
+ let bestScore = 0;
87
+ let bestId;
88
+ for (const note of existing) {
89
+ const score = jaccard(candidateTokens, tokenSet(noteText(note)));
90
+ if (score > bestScore) {
91
+ bestScore = score;
92
+ bestId = note.frontmatter.id;
93
+ }
94
+ }
95
+ if (bestScore >= DUPLICATE_THRESHOLD && bestId !== void 0) {
96
+ return { accept: false, reason: "duplicate", duplicateOf: bestId };
97
+ }
98
+ return { accept: true, reason: "accepted" };
99
+ }
100
+ };
101
+ async function curate(brainDir, candidates, curator = defaultCurator) {
102
+ const canon = await listNotes2(brainDir);
103
+ const staged = await listStaged(brainDir);
104
+ const existing = [...canon, ...staged];
105
+ const result = { staged: [], rejected: [] };
106
+ const autoAdr = await isFeatureEnabled(brainDir, "autoAdr");
107
+ const secretOpts = scanOptions(await loadBrainConfig(brainDir));
108
+ for (const candidate of candidates) {
109
+ if (hasSecrets(candidateSecretScanText(candidate), secretOpts)) {
110
+ result.rejected.push({ candidate, reason: "contains-secret" });
111
+ continue;
112
+ }
113
+ if (candidate.kind === "decision" && !autoAdr) {
114
+ result.rejected.push({ candidate, reason: "auto-adr-disabled" });
115
+ continue;
116
+ }
117
+ const assessment = curator.assess(candidate, existing);
118
+ if (!assessment.accept) {
119
+ result.rejected.push({
120
+ candidate,
121
+ reason: assessment.reason,
122
+ ...assessment.duplicateOf !== void 0 ? { duplicateOf: assessment.duplicateOf } : {}
123
+ });
124
+ continue;
125
+ }
126
+ try {
127
+ const note = await stageNote(brainDir, candidate);
128
+ result.staged.push(note);
129
+ existing.push(note);
130
+ } catch (err) {
131
+ result.rejected.push({
132
+ candidate,
133
+ reason: `invalid: ${err instanceof Error ? err.message : String(err)}`
134
+ });
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+
140
+ // src/review.ts
141
+ import { promises as fs } from "fs";
142
+ import path2 from "path";
143
+ import { pathForNote, resolveWithinBrain } from "@cmnwlth/core";
144
+ function listPending(brainDir) {
145
+ return listStaged(brainDir);
146
+ }
147
+ async function findStaged(brainDir, id) {
148
+ const pending = await listStaged(brainDir);
149
+ return pending.find((n) => n.frontmatter.id === id);
150
+ }
151
+ async function approve(brainDir, id) {
152
+ const note = await findStaged(brainDir, id);
153
+ if (!note) {
154
+ throw new Error(`No staged note with id "${id}" to approve`);
155
+ }
156
+ const canonRel = pathForNote(note.frontmatter.kind, id, note.frontmatter.source);
157
+ const canonAbs = resolveWithinBrain(brainDir, canonRel);
158
+ const stagedAbs = stagedAbsPath(brainDir, note);
159
+ await fs.mkdir(path2.dirname(canonAbs), { recursive: true });
160
+ const content = await fs.readFile(stagedAbs, "utf8");
161
+ const tmp = `${canonAbs}.${Date.now()}.tmp`;
162
+ await fs.writeFile(tmp, content, "utf8");
163
+ await fs.rename(tmp, canonAbs);
164
+ await fs.rm(stagedAbs);
165
+ return canonRel;
166
+ }
167
+ async function reject(brainDir, id) {
168
+ const note = await findStaged(brainDir, id);
169
+ if (!note) {
170
+ throw new Error(`No staged note with id "${id}" to reject`);
171
+ }
172
+ await fs.rm(stagedAbsPath(brainDir, note));
173
+ }
174
+ async function approveAll(brainDir) {
175
+ const pending = await listStaged(brainDir);
176
+ const paths = [];
177
+ for (const note of pending) {
178
+ paths.push(await approve(brainDir, note.frontmatter.id));
179
+ }
180
+ return paths;
181
+ }
182
+
183
+ // src/capture.ts
184
+ async function captureCandidates(brainDir, candidates, curator) {
185
+ const result = await curate(brainDir, candidates, curator);
186
+ const promoted = [];
187
+ if (result.staged.length > 0 && await isFeatureEnabled2(brainDir, "autoPromote")) {
188
+ for (const note of result.staged) {
189
+ promoted.push(await approve(brainDir, note.frontmatter.id));
190
+ }
191
+ }
192
+ return { ...result, promoted };
193
+ }
194
+
195
+ // src/consolidate.ts
196
+ import { acquireSyncLock, listNotes as listNotes3, supersedeNote } from "@cmnwlth/core";
197
+ var DEFAULT_CONSOLIDATE_THRESHOLD = 0.9;
198
+ function noteText2(n) {
199
+ return `${n.frontmatter.title} ${n.body}`;
200
+ }
201
+ function pickSurvivor(cluster) {
202
+ return [...cluster].sort((a, b) => {
203
+ const av = a.frontmatter.kind === "memory" ? a.frontmatter.verified ?? "" : "";
204
+ const bv = b.frontmatter.kind === "memory" ? b.frontmatter.verified ?? "" : "";
205
+ if (av !== bv) return av < bv ? 1 : -1;
206
+ if (a.frontmatter.created !== b.frontmatter.created) {
207
+ return a.frontmatter.created < b.frontmatter.created ? 1 : -1;
208
+ }
209
+ return a.frontmatter.id < b.frontmatter.id ? -1 : 1;
210
+ })[0];
211
+ }
212
+ function clusterBySimilarity(notes, threshold) {
213
+ const parent = notes.map((_, i) => i);
214
+ const find = (i) => {
215
+ while (parent[i] !== i) {
216
+ parent[i] = parent[parent[i]];
217
+ i = parent[i];
218
+ }
219
+ return i;
220
+ };
221
+ const union = (a, b) => {
222
+ parent[find(a)] = find(b);
223
+ };
224
+ for (let i = 0; i < notes.length; i++) {
225
+ for (let j = i + 1; j < notes.length; j++) {
226
+ if (textSimilarity(noteText2(notes[i]), noteText2(notes[j])) >= threshold) union(i, j);
227
+ }
228
+ }
229
+ const byRoot = /* @__PURE__ */ new Map();
230
+ notes.forEach((n, i) => {
231
+ const r = find(i);
232
+ (byRoot.get(r) ?? byRoot.set(r, []).get(r)).push(n);
233
+ });
234
+ return [...byRoot.values()].filter((c) => c.length > 1);
235
+ }
236
+ async function consolidateCanon(brainDir, opts = {}) {
237
+ const threshold = opts.threshold ?? DEFAULT_CONSOLIDATE_THRESHOLD;
238
+ const release = await acquireSyncLock(brainDir);
239
+ if (!release)
240
+ return { clusters: 0, superseded: [], skipped: "another writer holds the sync lock" };
241
+ try {
242
+ const notes = await listNotes3(brainDir);
243
+ const active = notes.filter(
244
+ (n) => (n.frontmatter.kind === "memory" || n.frontmatter.kind === "decision") && n.frontmatter.status !== "superseded"
245
+ );
246
+ const superseded = [];
247
+ let clusters = 0;
248
+ for (const kind of ["memory", "decision"]) {
249
+ const ofKind = active.filter((n) => n.frontmatter.kind === kind);
250
+ for (const cluster of clusterBySimilarity(ofKind, threshold)) {
251
+ clusters += 1;
252
+ const survivor = pickSurvivor(cluster);
253
+ for (const dup of cluster) {
254
+ if (dup.frontmatter.id === survivor.frontmatter.id) continue;
255
+ if (!opts.dryRun) await supersedeNote(brainDir, dup.path, survivor.frontmatter.id);
256
+ superseded.push({
257
+ id: dup.frontmatter.id,
258
+ path: dup.path,
259
+ survivor: survivor.frontmatter.id
260
+ });
261
+ }
262
+ }
263
+ }
264
+ superseded.sort((a, b) => a.id < b.id ? -1 : 1);
265
+ return { clusters, superseded };
266
+ } finally {
267
+ await release();
268
+ }
269
+ }
270
+
271
+ // src/context.ts
272
+ import "@cmnwlth/core";
273
+ var SNIPPET_MAX = 120;
274
+ function firstLine(body) {
275
+ for (const line of body.split("\n")) {
276
+ const trimmed = line.trim();
277
+ if (trimmed.length > 0) {
278
+ return trimmed.length > SNIPPET_MAX ? trimmed.slice(0, SNIPPET_MAX) : trimmed;
279
+ }
280
+ }
281
+ return "";
282
+ }
283
+ function formatContext(notes) {
284
+ if (notes.length === 0) return "";
285
+ const lines = [
286
+ `## Team brain \u2014 ${notes.length} relevant note(s)`,
287
+ '_Cite any note you use inline, e.g. "\u{1F4D6} from the team brain: TITLE"._',
288
+ ""
289
+ ];
290
+ for (const note of notes) {
291
+ const { title, kind } = note.frontmatter;
292
+ const snippet = firstLine(note.body);
293
+ lines.push(snippet ? `- **${title}** (${kind}) \u2014 ${snippet}` : `- **${title}** (${kind})`);
294
+ }
295
+ return lines.join("\n");
296
+ }
297
+
298
+ // src/relevance.ts
299
+ import { listNotes as listNotes4, readNote, search } from "@cmnwlth/core";
300
+ var DEFAULT_LIMIT = 10;
301
+ function byId(a, b) {
302
+ return a.frontmatter.id < b.frontmatter.id ? -1 : a.frontmatter.id > b.frontmatter.id ? 1 : 0;
303
+ }
304
+ function byCreatedDesc(a, b) {
305
+ if (a.frontmatter.created !== b.frontmatter.created) {
306
+ return a.frontmatter.created < b.frontmatter.created ? 1 : -1;
307
+ }
308
+ return byId(a, b);
309
+ }
310
+ async function selectRelevant(brainDir, q = {}) {
311
+ const limit = q.limit ?? DEFAULT_LIMIT;
312
+ if (q.query !== void 0 && q.query.trim().length > 0) {
313
+ const hits = await search(brainDir, q.query, { limit });
314
+ const notes = [];
315
+ for (const hit of hits) {
316
+ notes.push(await readNote(brainDir, hit.path));
317
+ }
318
+ return notes;
319
+ }
320
+ const workStates = await listNotes4(brainDir, "work-state");
321
+ const active = workStates.filter((n) => n.frontmatter.kind === "work-state" && n.frontmatter.status !== "done").sort(byId);
322
+ const decisions = (await listNotes4(brainDir, "decision")).sort(byCreatedDesc);
323
+ return [...active, ...decisions].slice(0, limit);
324
+ }
325
+
326
+ // src/scope.ts
327
+ import { promises as fs2 } from "fs";
328
+ import os from "os";
329
+ import path3 from "path";
330
+ function defaultConfigPath() {
331
+ return process.env.COMMONWEALTH_CONFIG ?? path3.join(os.homedir(), ".commonwealth", "config.json");
332
+ }
333
+ async function loadUserConfig(configPath = defaultConfigPath()) {
334
+ let raw;
335
+ try {
336
+ raw = await fs2.readFile(configPath, "utf8");
337
+ } catch {
338
+ return { allow: [], deny: [] };
339
+ }
340
+ let parsed;
341
+ try {
342
+ parsed = JSON.parse(raw);
343
+ } catch {
344
+ return { allow: [], deny: [] };
345
+ }
346
+ const obj = typeof parsed === "object" && parsed !== null ? parsed : {};
347
+ return {
348
+ allow: Array.isArray(obj.allow) ? obj.allow : [],
349
+ deny: Array.isArray(obj.deny) ? obj.deny : []
350
+ };
351
+ }
352
+ async function saveUserConfig(config, configPath = defaultConfigPath()) {
353
+ await fs2.mkdir(path3.dirname(configPath), { recursive: true });
354
+ await fs2.writeFile(configPath, `${JSON.stringify(config, null, 2)}
355
+ `, "utf8");
356
+ }
357
+ function expand(entry) {
358
+ const home = os.homedir();
359
+ if (entry === "~") return path3.resolve(home);
360
+ if (entry.startsWith("~/")) return path3.resolve(home, entry.slice(2));
361
+ return path3.resolve(entry);
362
+ }
363
+ function isUnder(child, parent) {
364
+ if (parent === path3.sep) return true;
365
+ return child === parent || child.startsWith(parent + path3.sep);
366
+ }
367
+ function underAny(p, list) {
368
+ return list.some((entry) => isUnder(p, expand(entry)));
369
+ }
370
+ function isInScope(cwd, config) {
371
+ const target = expand(cwd);
372
+ const allowed = config.allow.length === 0 || underAny(target, config.allow);
373
+ return allowed && !underAny(target, config.deny);
374
+ }
375
+ async function addAllow(pathArg, configPath = defaultConfigPath()) {
376
+ const config = await loadUserConfig(configPath);
377
+ const resolved = expand(pathArg);
378
+ if (!config.allow.includes(resolved)) {
379
+ config.allow.push(resolved);
380
+ await saveUserConfig(config, configPath);
381
+ }
382
+ }
383
+ async function addDeny(pathArg, configPath = defaultConfigPath()) {
384
+ const config = await loadUserConfig(configPath);
385
+ const resolved = expand(pathArg);
386
+ if (!config.deny.includes(resolved)) {
387
+ config.deny.push(resolved);
388
+ await saveUserConfig(config, configPath);
389
+ }
390
+ }
391
+
392
+ // src/index.ts
393
+ async function resolveDir(explicit, cwd = process.cwd()) {
394
+ if (explicit && explicit.length > 0) return path4.resolve(explicit);
395
+ const env = process.env.COMMONWEALTH_BRAIN_DIR;
396
+ if (env && env.length > 0) return path4.resolve(env);
397
+ return resolveBrainDir(cwd);
398
+ }
399
+ function noBrain(cwd) {
400
+ console.error(
401
+ `[commonwealth-curate] no Commonwealth brain configured for ${cwd} \u2014 run \`commonwealth init\` here, add a prefix \u2192 brain mapping to ~/.commonwealth/registry.json, or pass --dir <brain>.`
402
+ );
403
+ process.exit(1);
404
+ }
405
+ function usage() {
406
+ console.error(
407
+ [
408
+ "commonwealth-curate \u2014 curation + in-repo review queue",
409
+ "",
410
+ "Usage:",
411
+ " commonwealth-curate list [--dir <brain>]",
412
+ " commonwealth-curate approve <id...> [--dir <brain>]",
413
+ " commonwealth-curate reject <id...> [--dir <brain>]",
414
+ " commonwealth-curate approve-all [--dir <brain>]",
415
+ " commonwealth-curate stage --kind <kind> --title <t> --body <b> [--tags a,b] [--dir <brain>]",
416
+ " commonwealth-curate context [--dir <brain>] [--cwd <dir>] [--query <q>] [--limit <n>]",
417
+ " commonwealth-curate capture [--dir <brain>] [--cwd <dir>] [--from <json-file>]",
418
+ " commonwealth-curate scope show",
419
+ " commonwealth-curate scope check [--cwd <dir>]",
420
+ " commonwealth-curate scope allow <path>",
421
+ " commonwealth-curate scope deny <path>",
422
+ " commonwealth-curate health [--dir <brain>]",
423
+ " commonwealth-curate consolidate [--dry-run] [--dir <brain>]",
424
+ " commonwealth-curate feature list [--dir <brain>]",
425
+ " commonwealth-curate feature enable <name> [--dir <brain>]",
426
+ " commonwealth-curate feature disable <name> [--dir <brain>]",
427
+ "",
428
+ `Kinds: ${NOTE_KINDS.join(", ")}`
429
+ ].join("\n")
430
+ );
431
+ }
432
+ function isNoteKind(value) {
433
+ return NOTE_KINDS.includes(value);
434
+ }
435
+ async function cmdList(dir) {
436
+ const pending = await listPending(dir);
437
+ for (const note of pending) {
438
+ console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);
439
+ }
440
+ }
441
+ async function cmdApprove(dir, ids) {
442
+ if (ids.length === 0) throw new Error("approve requires at least one <id>");
443
+ for (const id of ids) {
444
+ const canonPath = await approve(dir, id);
445
+ console.log(canonPath);
446
+ }
447
+ }
448
+ async function cmdReject(dir, ids) {
449
+ if (ids.length === 0) throw new Error("reject requires at least one <id>");
450
+ for (const id of ids) {
451
+ await reject(dir, id);
452
+ console.error(`[commonwealth-curate] rejected ${id}`);
453
+ }
454
+ }
455
+ async function cmdApproveAll(dir) {
456
+ const paths = await approveAll(dir);
457
+ for (const p of paths) console.log(p);
458
+ console.error(`[commonwealth-curate] approved ${paths.length} note(s)`);
459
+ }
460
+ async function cmdStage(dir, args) {
461
+ const { values } = parseArgs({
462
+ args,
463
+ options: {
464
+ kind: { type: "string" },
465
+ title: { type: "string" },
466
+ body: { type: "string" },
467
+ tags: { type: "string" },
468
+ // Tolerated here (handled by the top-level parser) so `--dir` doesn't error.
469
+ dir: { type: "string" }
470
+ },
471
+ allowPositionals: false
472
+ });
473
+ const { kind, title, body, tags } = values;
474
+ if (!kind || !title || !body) {
475
+ throw new Error("stage requires --kind, --title and --body");
476
+ }
477
+ if (!isNoteKind(kind)) {
478
+ throw new Error(`invalid --kind "${kind}"; expected one of: ${NOTE_KINDS.join(", ")}`);
479
+ }
480
+ const candidate = {
481
+ kind,
482
+ title,
483
+ body,
484
+ ...tags ? {
485
+ tags: tags.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
486
+ } : {}
487
+ };
488
+ const result = await curate(dir, [candidate]);
489
+ for (const note of result.staged) {
490
+ console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);
491
+ }
492
+ for (const r of result.rejected) {
493
+ const extra = r.duplicateOf ? ` (of ${r.duplicateOf})` : "";
494
+ console.error(`[commonwealth-curate] rejected: ${r.reason}${extra}`);
495
+ }
496
+ }
497
+ async function cmdContext(explicitDir, args) {
498
+ const { values } = parseArgs({
499
+ args,
500
+ options: {
501
+ dir: { type: "string" },
502
+ cwd: { type: "string" },
503
+ query: { type: "string" },
504
+ limit: { type: "string" }
505
+ },
506
+ allowPositionals: false
507
+ });
508
+ const cwd = typeof values.cwd === "string" ? values.cwd : process.cwd();
509
+ const config = await loadUserConfig();
510
+ if (!isInScope(cwd, config)) {
511
+ console.error(`[commonwealth-curate] ${cwd} is out of scope; injecting nothing`);
512
+ return;
513
+ }
514
+ const dir = await resolveDir(explicitDir ?? values.dir, cwd);
515
+ if (dir === null) {
516
+ console.error(`[commonwealth-curate] no brain for ${cwd}; injecting nothing`);
517
+ return;
518
+ }
519
+ const limit = values.limit !== void 0 ? Number.parseInt(values.limit, 10) : void 0;
520
+ const notes = await selectRelevant(dir, {
521
+ ...typeof values.query === "string" ? { query: values.query } : {},
522
+ ...limit !== void 0 && Number.isFinite(limit) ? { limit } : {}
523
+ });
524
+ const rendered = formatContext(notes);
525
+ if (rendered.length > 0) console.log(rendered);
526
+ }
527
+ function parseCandidates(raw) {
528
+ const parsed = JSON.parse(raw);
529
+ if (!Array.isArray(parsed)) {
530
+ throw new Error("capture expects a JSON array of candidate notes");
531
+ }
532
+ return parsed;
533
+ }
534
+ async function readStdin() {
535
+ const chunks = [];
536
+ for await (const chunk of process.stdin) {
537
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
538
+ }
539
+ return Buffer.concat(chunks).toString("utf8");
540
+ }
541
+ async function cmdCapture(explicitDir, args) {
542
+ const { values } = parseArgs({
543
+ args,
544
+ options: {
545
+ dir: { type: "string" },
546
+ cwd: { type: "string" },
547
+ from: { type: "string" },
548
+ // Explicit imports (e.g. seeding a chosen repo) bypass the per-session scope gate,
549
+ // which exists to filter out-of-scope *sessions*, not deliberate imports.
550
+ force: { type: "boolean" }
551
+ },
552
+ allowPositionals: false
553
+ });
554
+ const cwd = typeof values.cwd === "string" ? values.cwd : process.cwd();
555
+ if (!values.force) {
556
+ const config = await loadUserConfig();
557
+ if (!isInScope(cwd, config)) {
558
+ console.error(`[commonwealth-curate] ${cwd} is out of scope; capturing nothing`);
559
+ return;
560
+ }
561
+ }
562
+ const dir = await resolveDir(explicitDir ?? values.dir, cwd);
563
+ if (dir === null) {
564
+ console.error(`[commonwealth-curate] no brain for ${cwd}; capturing nothing`);
565
+ return;
566
+ }
567
+ const raw = typeof values.from === "string" ? await fs3.readFile(values.from, "utf8") : await readStdin();
568
+ const candidates = parseCandidates(raw);
569
+ const source = await resolveProjectSource(cwd) ?? void 0;
570
+ const stamped = candidates.map((c) => c.source ? c : { ...c, source });
571
+ const result = await captureCandidates(dir, stamped);
572
+ if (result.promoted.length > 0) {
573
+ result.staged.forEach((note, i) => {
574
+ const canonPath = result.promoted[i] ?? `${note.frontmatter.kind}/${note.frontmatter.id}.md`;
575
+ console.log(`promoted ${canonPath} [${note.frontmatter.kind}] ${note.frontmatter.title}`);
576
+ });
577
+ } else {
578
+ for (const note of result.staged) {
579
+ console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);
580
+ }
581
+ }
582
+ for (const r of result.rejected) {
583
+ const extra = r.duplicateOf ? ` (of ${r.duplicateOf})` : "";
584
+ console.error(`[commonwealth-curate] rejected: ${r.reason}${extra}`);
585
+ }
586
+ }
587
+ async function cmdScope(args) {
588
+ const [sub, ...rest] = args;
589
+ switch (sub) {
590
+ case "show": {
591
+ const config = await loadUserConfig();
592
+ console.log(JSON.stringify(config, null, 2));
593
+ return;
594
+ }
595
+ case "check": {
596
+ const { values } = parseArgs({
597
+ args: rest,
598
+ options: { cwd: { type: "string" } },
599
+ allowPositionals: false
600
+ });
601
+ const cwd = typeof values.cwd === "string" ? values.cwd : process.cwd();
602
+ const config = await loadUserConfig();
603
+ console.log(isInScope(cwd, config) ? "in-scope" : "out-of-scope");
604
+ return;
605
+ }
606
+ case "allow": {
607
+ const target = rest[0];
608
+ if (!target) throw new Error("scope allow requires a <path>");
609
+ await addAllow(target);
610
+ console.error(`[commonwealth-curate] allow += ${target}`);
611
+ return;
612
+ }
613
+ case "deny": {
614
+ const target = rest[0];
615
+ if (!target) throw new Error("scope deny requires a <path>");
616
+ await addDeny(target);
617
+ console.error(`[commonwealth-curate] deny += ${target}`);
618
+ return;
619
+ }
620
+ default:
621
+ throw new Error(`unknown scope subcommand "${sub ?? ""}"; expected show|check|allow|deny`);
622
+ }
623
+ }
624
+ async function cmdFeature(dir, args) {
625
+ const { positionals } = parseArgs({
626
+ args,
627
+ options: { dir: { type: "string" } },
628
+ allowPositionals: true,
629
+ strict: false
630
+ });
631
+ const [sub, ...rest] = positionals;
632
+ switch (sub) {
633
+ case "list": {
634
+ const config = await loadBrainConfig2(dir);
635
+ for (const flag of FEATURE_FLAGS) {
636
+ const state = config.features[flag.name] ? "on" : "off";
637
+ console.log(`${flag.name} [${state}] \u2014 ${flag.description}`);
638
+ }
639
+ return;
640
+ }
641
+ case "enable":
642
+ case "disable": {
643
+ const name = rest[0];
644
+ if (!name) throw new Error(`feature ${sub} requires a <name>`);
645
+ if (!FEATURE_FLAGS.some((f) => f.name === name)) {
646
+ const known = FEATURE_FLAGS.map((f) => f.name).join(", ");
647
+ throw new Error(`unknown feature "${name}"; expected one of: ${known}`);
648
+ }
649
+ const on = sub === "enable";
650
+ await setFeature(dir, name, on);
651
+ console.error(`[commonwealth-curate] feature ${name} ${on ? "enabled" : "disabled"}`);
652
+ return;
653
+ }
654
+ default:
655
+ throw new Error(`unknown feature subcommand "${sub ?? ""}"; expected list|enable|disable`);
656
+ }
657
+ }
658
+ async function cmdHealth(dir) {
659
+ const h = await computeBrainHealth(dir);
660
+ console.log(`Brain health: ${h.score}/100 (${h.total} note${h.total === 1 ? "" : "s"})`);
661
+ console.log(` stale: ${h.stale.count}`);
662
+ console.log(` unverified: ${h.unverified.count}`);
663
+ console.log(` contradicted: ${h.contradicted.count}`);
664
+ console.log(` orphaned: ${h.orphaned.count}`);
665
+ }
666
+ async function cmdConsolidate(dir, args) {
667
+ const { values } = parseArgs({
668
+ args,
669
+ options: { "dry-run": { type: "boolean" }, dir: { type: "string" } },
670
+ allowPositionals: false
671
+ });
672
+ const result = await consolidateCanon(dir, { dryRun: values["dry-run"] === true });
673
+ if (result.skipped) {
674
+ console.error(`[commonwealth-curate] consolidate skipped: ${result.skipped}`);
675
+ return;
676
+ }
677
+ const verb = values["dry-run"] ? "would supersede" : "superseded";
678
+ console.error(
679
+ `[commonwealth-curate] ${result.clusters} duplicate cluster(s); ${verb} ${result.superseded.length} note(s)`
680
+ );
681
+ for (const s of result.superseded) console.log(`${s.id} -> ${s.survivor}`);
682
+ }
683
+ async function main() {
684
+ const argv = process.argv.slice(2);
685
+ const [command, ...rest] = argv;
686
+ const { values, positionals } = parseArgs({
687
+ args: rest,
688
+ options: { dir: { type: "string" } },
689
+ allowPositionals: true,
690
+ // Leave unknown options (e.g. stage's --kind) for the subcommand parser.
691
+ strict: false
692
+ });
693
+ const explicitDir = typeof values.dir === "string" ? values.dir : void 0;
694
+ const requireBrain = async () => await resolveDir(explicitDir) ?? noBrain(process.cwd());
695
+ switch (command) {
696
+ case "list":
697
+ await cmdList(await requireBrain());
698
+ break;
699
+ case "approve":
700
+ await cmdApprove(await requireBrain(), positionals);
701
+ break;
702
+ case "reject":
703
+ await cmdReject(await requireBrain(), positionals);
704
+ break;
705
+ case "approve-all":
706
+ await cmdApproveAll(await requireBrain());
707
+ break;
708
+ case "stage":
709
+ await cmdStage(await requireBrain(), rest);
710
+ break;
711
+ case "context":
712
+ await cmdContext(explicitDir, rest);
713
+ break;
714
+ case "capture":
715
+ await cmdCapture(explicitDir, rest);
716
+ break;
717
+ case "scope":
718
+ await cmdScope(rest);
719
+ break;
720
+ case "feature":
721
+ await cmdFeature(await requireBrain(), rest);
722
+ break;
723
+ case "health":
724
+ await cmdHealth(await requireBrain());
725
+ break;
726
+ case "consolidate":
727
+ await cmdConsolidate(await requireBrain(), rest);
728
+ break;
729
+ default:
730
+ usage();
731
+ process.exitCode = command ? 1 : 0;
732
+ }
733
+ }
734
+ main().catch((err) => {
735
+ console.error("[commonwealth-curate] error:", err instanceof Error ? err.message : err);
736
+ process.exit(1);
737
+ });
738
+ //# sourceMappingURL=index.js.map