natureco-cli 2.23.30 → 2.23.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/natureco.js +178 -167
  2. package/package.json +1 -1
  3. package/src/commands/acp.js +39 -0
  4. package/src/commands/admin-rpc.js +83 -0
  5. package/src/commands/agent.js +214 -23
  6. package/src/commands/agents.js +114 -30
  7. package/src/commands/approvals.js +172 -11
  8. package/src/commands/ask.js +1 -1
  9. package/src/commands/browser.js +815 -0
  10. package/src/commands/capability.js +195 -22
  11. package/src/commands/channels.js +422 -267
  12. package/src/commands/chat.js +5 -8
  13. package/src/commands/clawbot.js +19 -0
  14. package/src/commands/code.js +3 -2
  15. package/src/commands/commitments.js +125 -9
  16. package/src/commands/completion.js +40 -32
  17. package/src/commands/config.js +228 -30
  18. package/src/commands/configure.js +84 -67
  19. package/src/commands/cron.js +239 -19
  20. package/src/commands/daemon.js +34 -4
  21. package/src/commands/dashboard.js +47 -374
  22. package/src/commands/devices.js +53 -26
  23. package/src/commands/directory.js +146 -14
  24. package/src/commands/dns.js +148 -10
  25. package/src/commands/docs.js +119 -26
  26. package/src/commands/doctor.js +143 -492
  27. package/src/commands/exec-policy.js +57 -48
  28. package/src/commands/gateway.js +492 -249
  29. package/src/commands/health.js +141 -11
  30. package/src/commands/help.js +24 -25
  31. package/src/commands/hooks.js +141 -87
  32. package/src/commands/infer.js +1442 -41
  33. package/src/commands/logs.js +122 -99
  34. package/src/commands/mcp.js +121 -309
  35. package/src/commands/memory.js +128 -0
  36. package/src/commands/message.js +720 -140
  37. package/src/commands/models.js +39 -1
  38. package/src/commands/node.js +77 -77
  39. package/src/commands/nodes.js +278 -22
  40. package/src/commands/onboard.js +115 -56
  41. package/src/commands/pairing.js +108 -107
  42. package/src/commands/path.js +206 -0
  43. package/src/commands/plugins.js +35 -1
  44. package/src/commands/proxy.js +159 -8
  45. package/src/commands/qr.js +55 -13
  46. package/src/commands/reset.js +101 -94
  47. package/src/commands/secrets.js +104 -21
  48. package/src/commands/sessions.js +110 -51
  49. package/src/commands/setup.js +229 -649
  50. package/src/commands/skills.js +67 -1
  51. package/src/commands/status.js +101 -127
  52. package/src/commands/tasks.js +208 -100
  53. package/src/commands/terminal.js +130 -12
  54. package/src/commands/transcripts.js +24 -1
  55. package/src/commands/tui.js +41 -0
  56. package/src/commands/uninstall.js +73 -92
  57. package/src/commands/update.js +146 -91
  58. package/src/commands/web-fetch.js +34 -0
  59. package/src/commands/webhooks.js +58 -66
  60. package/src/commands/wiki.js +783 -0
  61. package/src/utils/agents-md.js +85 -0
  62. package/src/utils/api.js +40 -41
  63. package/src/utils/format.js +144 -0
  64. package/src/utils/headless.js +2 -1
  65. package/src/utils/parallel-tools.js +106 -0
  66. package/src/utils/sub-agent.js +148 -0
  67. package/src/utils/token-budget.js +304 -0
  68. package/src/utils/tool-runner.js +7 -5
  69. package/src/utils/web-fetch.js +107 -0
@@ -0,0 +1,783 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const WIKI_DIR = path.join(os.homedir(), '.natureco', 'wiki');
7
+ const VAULT_FILE = path.join(WIKI_DIR, 'vault.json');
8
+ const DIRS = {
9
+ sources: path.join(WIKI_DIR, 'sources'),
10
+ concepts: path.join(WIKI_DIR, 'concepts'),
11
+ cache: path.join(WIKI_DIR, 'cache'),
12
+ };
13
+
14
+ function vaultExists() {
15
+ return fs.existsSync(WIKI_DIR) && fs.existsSync(VAULT_FILE);
16
+ }
17
+
18
+ function loadVault() {
19
+ if (!fs.existsSync(VAULT_FILE)) {
20
+ return { initialized: false, created: null, updated: null, pages: 0, version: 1 };
21
+ }
22
+ try {
23
+ return JSON.parse(fs.readFileSync(VAULT_FILE, 'utf8'));
24
+ } catch {
25
+ return { initialized: false, created: null, updated: null, pages: 0, version: 1 };
26
+ }
27
+ }
28
+
29
+ function saveVault(data) {
30
+ const dir = path.dirname(VAULT_FILE);
31
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
32
+ fs.writeFileSync(VAULT_FILE, JSON.stringify(data, null, 2), 'utf8');
33
+ }
34
+
35
+ function countFiles(dir, ext) {
36
+ if (!fs.existsSync(dir)) return 0;
37
+ return fs.readdirSync(dir).filter(f => !ext || f.endsWith(ext)).length;
38
+ }
39
+
40
+ function countAll(dir) {
41
+ if (!fs.existsSync(dir)) return 0;
42
+ let total = 0;
43
+ try {
44
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (entry.isFile()) total++;
47
+ else if (entry.isDirectory()) total += countAll(path.join(dir, entry.name));
48
+ }
49
+ } catch {}
50
+ return total;
51
+ }
52
+
53
+ function wiki(args) {
54
+ const [action, ...params] = args || [];
55
+
56
+ if (!action || action === 'status') return cmdStatus();
57
+ if (action === 'doctor') return cmdDoctor();
58
+ if (action === 'init') return cmdInit();
59
+ if (action === 'ingest') return cmdIngest(params.join(' '));
60
+ if (action === 'compile') return cmdCompile();
61
+ if (action === 'lint') return cmdLint();
62
+ if (action === 'search') return cmdSearch(params.join(' '));
63
+ if (action === 'get') return cmdGet(params.join(' '));
64
+ if (action === 'apply') return cmdApply(params[0], params.slice(1));
65
+ if (action === 'bridge' && params[0] === 'import') return cmdBridgeImport(params.slice(1).join(' '));
66
+ if (action === 'unsafe-local' && params[0] === 'import') return cmdUnsafeLocalImport(params.slice(1).join(' '));
67
+ if (action === 'obsidian') return cmdObsidian(params);
68
+
69
+ console.log(chalk.red(`\n Unknown wiki action: ${action}\n`));
70
+ console.log(chalk.gray(' Usage: natureco wiki <action> [params]'));
71
+ console.log(chalk.gray(' Actions: status, doctor, init, ingest, compile, lint, search, get, apply\n'));
72
+ process.exit(1);
73
+ }
74
+
75
+ // ── status ──────────────────────────────────────────────────────────────────
76
+ function cmdStatus() {
77
+ const exists = vaultExists();
78
+ const vault = loadVault();
79
+
80
+ console.log(chalk.cyan('\n Wiki Vault Status\n'));
81
+
82
+ if (!exists) {
83
+ console.log(chalk.yellow(' Not initialized. Run') + chalk.cyan(' natureco wiki init') + chalk.yellow(' to create.\n'));
84
+ return;
85
+ }
86
+
87
+ const w = process.stdout.columns || 120;
88
+ console.log(chalk.gray(' ' + '─'.repeat(Math.min(48, w - 4))));
89
+
90
+ const srcCount = countFiles(DIRS.sources, '.md');
91
+ const conCount = countFiles(DIRS.concepts, '.md');
92
+ const cacheCount = countAll(DIRS.cache);
93
+
94
+ console.log('');
95
+ console.log(chalk.white(' Sources: ') + chalk.cyan(`${srcCount}`));
96
+ console.log(chalk.white(' Concepts: ') + chalk.cyan(`${conCount}`));
97
+ console.log(chalk.white(' Cached: ') + chalk.cyan(`${cacheCount} files`));
98
+ console.log(chalk.white(' Pages: ') + chalk.cyan(`${vault.pages || 0}`));
99
+ console.log('');
100
+ console.log(chalk.gray(' Created: ') + chalk.white(vault.created ? new Date(vault.created).toLocaleString() : '—'));
101
+ console.log(chalk.gray(' Updated: ') + chalk.white(vault.updated ? new Date(vault.updated).toLocaleString() : '—'));
102
+ console.log(chalk.gray(' Version: ') + chalk.white(`v${vault.version || 1}`));
103
+ console.log('');
104
+ console.log(chalk.gray(' ' + '─'.repeat(Math.min(48, w - 4))));
105
+ console.log('');
106
+ }
107
+
108
+ // ── doctor ──────────────────────────────────────────────────────────────────
109
+ function cmdDoctor() {
110
+ console.log(chalk.cyan('\n Wiki Vault Health Check\n'));
111
+
112
+ let passed = 0;
113
+ let failed = 0;
114
+ let warnings = 0;
115
+
116
+ function check(label, condition, severity) {
117
+ if (condition) {
118
+ passed++;
119
+ console.log(chalk.green(` [PASS] ${label}`));
120
+ } else if (severity === 'warn') {
121
+ warnings++;
122
+ console.log(chalk.yellow(` [WARN] ${label}`));
123
+ } else {
124
+ failed++;
125
+ console.log(chalk.red(` [FAIL] ${label}`));
126
+ }
127
+ }
128
+
129
+ check('Vault directory exists', fs.existsSync(WIKI_DIR), 'fail');
130
+ check('sources/ directory exists', fs.existsSync(DIRS.sources), 'fail');
131
+ check('concepts/ directory exists', fs.existsSync(DIRS.concepts), 'fail');
132
+ check('cache/ directory exists', fs.existsSync(DIRS.cache), 'fail');
133
+ check('vault.json exists', fs.existsSync(VAULT_FILE), 'fail');
134
+
135
+ if (fs.existsSync(VAULT_FILE)) {
136
+ try {
137
+ const vault = JSON.parse(fs.readFileSync(VAULT_FILE, 'utf8'));
138
+ check('vault.json is valid JSON', !!vault, 'fail');
139
+ check('vault.json has version field', typeof vault.version === 'number', 'warn');
140
+ } catch {
141
+ check('vault.json is valid JSON', false, 'fail');
142
+ }
143
+ }
144
+
145
+ if (fs.existsSync(DIRS.sources)) {
146
+ const mdFiles = fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'));
147
+ check('Source files exist', mdFiles.length > 0, 'warn');
148
+ for (const file of mdFiles.slice(0, 5)) {
149
+ const content = fs.readFileSync(path.join(DIRS.sources, file), 'utf8');
150
+ check(`File "${file}" is non-empty`, content.trim().length > 0, 'warn');
151
+ }
152
+ }
153
+
154
+ const total = passed + failed + warnings;
155
+ console.log('');
156
+ console.log(chalk.gray(` ${passed} passed, ${warnings} warnings, ${failed} failed out of ${total} checks`));
157
+
158
+ if (failed > 0) {
159
+ console.log(chalk.red(`\n Some checks failed. Run ${chalk.cyan('natureco wiki init')} to fix.\n`));
160
+ } else {
161
+ console.log(chalk.green('\n Vault is healthy.\n'));
162
+ }
163
+ }
164
+
165
+ // ── init ────────────────────────────────────────────────────────────────────
166
+ function cmdInit() {
167
+ if (vaultExists()) {
168
+ console.log(chalk.yellow('\n Wiki vault already exists.\n'));
169
+ return;
170
+ }
171
+
172
+ for (const key of Object.keys(DIRS)) {
173
+ const dir = DIRS[key];
174
+ if (!fs.existsSync(dir)) {
175
+ fs.mkdirSync(dir, { recursive: true });
176
+ }
177
+ }
178
+
179
+ const vault = {
180
+ initialized: true,
181
+ version: 1,
182
+ created: new Date().toISOString(),
183
+ updated: new Date().toISOString(),
184
+ pages: 0,
185
+ };
186
+ saveVault(vault);
187
+
188
+ console.log(chalk.green('\n Wiki vault initialized at') + chalk.white(` ${WIKI_DIR}\n`));
189
+ console.log(chalk.gray(' Created:'));
190
+ console.log(chalk.gray(' sources/ — ingested markdown pages'));
191
+ console.log(chalk.gray(' concepts/ — compiled concept pages'));
192
+ console.log(chalk.gray(' cache/ — compiled digests'));
193
+ console.log(chalk.gray(' vault.json — vault metadata'));
194
+ console.log('');
195
+ console.log(chalk.cyan(' Run') + chalk.white(' natureco wiki ingest <path>') + chalk.cyan(' to add content.\n'));
196
+ }
197
+
198
+ // ── ingest ──────────────────────────────────────────────────────────────────
199
+ function cmdIngest(srcPath) {
200
+ if (!srcPath) {
201
+ console.log(chalk.red('\n Usage: natureco wiki ingest <path>\n'));
202
+ process.exit(1);
203
+ }
204
+
205
+ if (!fs.existsSync(srcPath)) {
206
+ console.log(chalk.red(`\n Path not found: ${srcPath}\n`));
207
+ process.exit(1);
208
+ }
209
+
210
+ if (!vaultExists()) {
211
+ console.log(chalk.yellow('\n Wiki vault not initialized. Run') + chalk.cyan(' natureco wiki init') + chalk.yellow(' first.\n'));
212
+ process.exit(1);
213
+ }
214
+
215
+ if (!fs.existsSync(DIRS.sources)) {
216
+ fs.mkdirSync(DIRS.sources, { recursive: true });
217
+ }
218
+
219
+ let copied = 0;
220
+ let skipped = 0;
221
+
222
+ const stat = fs.statSync(srcPath);
223
+ if (stat.isDirectory()) {
224
+ const files = fs.readdirSync(srcPath);
225
+ for (const file of files) {
226
+ if (!file.endsWith('.md')) {
227
+ skipped++;
228
+ continue;
229
+ }
230
+ const srcFile = path.join(srcPath, file);
231
+ if (fs.statSync(srcFile).isFile()) {
232
+ const dest = path.join(DIRS.sources, file);
233
+ if (fs.existsSync(dest)) {
234
+ let base = path.basename(file, '.md');
235
+ let idx = 1;
236
+ while (fs.existsSync(path.join(DIRS.sources, `${base}_${idx}.md`))) idx++;
237
+ fs.copyFileSync(srcFile, path.join(DIRS.sources, `${base}_${idx}.md`));
238
+ } else {
239
+ fs.copyFileSync(srcFile, dest);
240
+ }
241
+ copied++;
242
+ }
243
+ }
244
+ } else if (stat.isFile() && srcPath.endsWith('.md')) {
245
+ const dest = path.join(DIRS.sources, path.basename(srcPath));
246
+ if (fs.existsSync(dest)) {
247
+ const base = path.basename(srcPath, '.md');
248
+ let idx = 1;
249
+ while (fs.existsSync(path.join(DIRS.sources, `${base}_${idx}.md`))) idx++;
250
+ fs.copyFileSync(srcPath, path.join(DIRS.sources, `${base}_${idx}.md`));
251
+ } else {
252
+ fs.copyFileSync(srcPath, dest);
253
+ }
254
+ copied++;
255
+ } else {
256
+ console.log(chalk.yellow('\n No markdown files found at the given path.\n'));
257
+ return;
258
+ }
259
+
260
+ const vault = loadVault();
261
+ vault.pages = (vault.pages || 0) + copied;
262
+ vault.updated = new Date().toISOString();
263
+ saveVault(vault);
264
+
265
+ console.log(chalk.green(`\n Ingested ${copied} file(s)`));
266
+ if (skipped > 0) console.log(chalk.gray(` Skipped ${skipped} non-markdown file(s)`));
267
+ console.log('');
268
+ }
269
+
270
+ // ── compile ─────────────────────────────────────────────────────────────────
271
+ function cmdCompile() {
272
+ if (!vaultExists()) {
273
+ console.log(chalk.yellow('\n Wiki vault not initialized.\n'));
274
+ return;
275
+ }
276
+
277
+ const srcFiles = fs.existsSync(DIRS.sources)
278
+ ? fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'))
279
+ : [];
280
+
281
+ if (srcFiles.length === 0) {
282
+ console.log(chalk.yellow('\n No source files to compile. Ingest some first.\n'));
283
+ return;
284
+ }
285
+
286
+ if (!fs.existsSync(DIRS.concepts)) fs.mkdirSync(DIRS.concepts, { recursive: true });
287
+ if (!fs.existsSync(DIRS.cache)) fs.mkdirSync(DIRS.cache, { recursive: true });
288
+
289
+ const index = [];
290
+ const cacheEntries = [];
291
+
292
+ for (const file of srcFiles) {
293
+ const srcPath = path.join(DIRS.sources, file);
294
+ const content = fs.readFileSync(srcPath, 'utf8');
295
+
296
+ const title = extractTitle(content) || path.basename(file, '.md');
297
+ const firstLine = content.split('\n').find(l => l.trim()) || '';
298
+ const wordCount = content.split(/\s+/).filter(Boolean).length;
299
+
300
+ const concept = {
301
+ id: path.basename(file, '.md'),
302
+ title,
303
+ source: file,
304
+ wordCount,
305
+ imported: new Date().toISOString(),
306
+ };
307
+
308
+ const conceptFile = path.join(DIRS.concepts, `${concept.id}.json`);
309
+ fs.writeFileSync(conceptFile, JSON.stringify(concept, null, 2), 'utf8');
310
+
311
+ index.push(concept);
312
+
313
+ const digest = {
314
+ title,
315
+ source: file,
316
+ snippet: firstLine.slice(0, 200),
317
+ wordCount,
318
+ path: srcPath,
319
+ };
320
+ cacheEntries.push(digest);
321
+ }
322
+
323
+ const cacheFile = path.join(DIRS.cache, 'index.json');
324
+ fs.writeFileSync(cacheFile, JSON.stringify({ entries: cacheEntries, compiled: new Date().toISOString() }, null, 2), 'utf8');
325
+
326
+ const vault = loadVault();
327
+ vault.pages = srcFiles.length;
328
+ vault.updated = new Date().toISOString();
329
+ saveVault(vault);
330
+
331
+ console.log(chalk.green(`\n Compiled ${srcFiles.length} source files into concepts/ and cache/\n`));
332
+ }
333
+
334
+ // ── lint ────────────────────────────────────────────────────────────────────
335
+ function cmdLint() {
336
+ if (!vaultExists()) {
337
+ console.log(chalk.yellow('\n Wiki vault not initialized.\n'));
338
+ return;
339
+ }
340
+
341
+ console.log(chalk.cyan('\n Linting Wiki Vault\n'));
342
+
343
+ let issues = 0;
344
+ let filesChecked = 0;
345
+
346
+ function issue(type, msg) {
347
+ issues++;
348
+ const tag = type === 'error' ? chalk.red('[ERROR]') : type === 'warn' ? chalk.yellow('[WARN]') : chalk.gray('[INFO]');
349
+ console.log(` ${tag} ${msg}`);
350
+ }
351
+
352
+ if (!fs.existsSync(DIRS.sources)) {
353
+ issue('error', 'sources/ directory missing');
354
+ } else {
355
+ const files = fs.readdirSync(DIRS.sources);
356
+ for (const file of files) {
357
+ if (!file.endsWith('.md')) continue;
358
+ filesChecked++;
359
+ const filePath = path.join(DIRS.sources, file);
360
+ const content = fs.readFileSync(filePath, 'utf8');
361
+
362
+ if (content.trim().length === 0) {
363
+ issue('warn', `"${file}" is empty`);
364
+ continue;
365
+ }
366
+
367
+ const lines = content.split('\n');
368
+
369
+ const hasFrontMatter = lines[0] && lines[0].trim() === '---';
370
+ if (hasFrontMatter) {
371
+ const endIdx = lines.slice(1).findIndex(l => l.trim() === '---');
372
+ if (endIdx === -1) {
373
+ issue('warn', `"${file}" has unclosed front matter`);
374
+ }
375
+ }
376
+
377
+ for (let i = 0; i < lines.length; i++) {
378
+ if (lines[i].length > 2000) {
379
+ issue('info', `"${file}" has a very long line (${lines[i].length} chars) at line ${i + 1}`);
380
+ }
381
+ }
382
+
383
+ const linkRefs = content.match(/\[\[([^\]]+)\]\]/g);
384
+ if (linkRefs) {
385
+ for (const ref of linkRefs) {
386
+ const target = ref.slice(2, -2);
387
+ const targetPath = path.join(DIRS.sources, `${target}.md`);
388
+ if (!fs.existsSync(targetPath)) {
389
+ const conceptFile = path.join(DIRS.concepts, `${target}.json`);
390
+ if (!fs.existsSync(conceptFile)) {
391
+ issue('warn', `"${file}" has broken link to "${target}"`);
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ const conceptsExist = fs.existsSync(DIRS.concepts) && fs.readdirSync(DIRS.concepts).length > 0;
400
+ const cacheExists = fs.existsSync(DIRS.cache) && fs.readdirSync(DIRS.cache).length > 0;
401
+
402
+ if (!conceptsExist) issue('info', 'No compiled concepts — run compile');
403
+ if (!cacheExists) issue('info', 'No cache entries — run compile');
404
+
405
+ const summary = `${filesChecked} files checked, ${issues} issue(s) found`;
406
+ if (issues === 0) {
407
+ console.log(chalk.green(`\n ${summary}\n`));
408
+ } else {
409
+ console.log(chalk.yellow(`\n ${summary}\n`));
410
+ }
411
+ }
412
+
413
+ // ── search ──────────────────────────────────────────────────────────────────
414
+ function cmdSearch(query) {
415
+ if (!query) {
416
+ console.log(chalk.red('\n Usage: natureco wiki search <query>\n'));
417
+ process.exit(1);
418
+ }
419
+
420
+ if (!fs.existsSync(DIRS.sources)) {
421
+ console.log(chalk.yellow('\n No sources directory found.\n'));
422
+ return;
423
+ }
424
+
425
+ const files = fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'));
426
+ if (files.length === 0) {
427
+ console.log(chalk.yellow('\n No source files to search.\n'));
428
+ return;
429
+ }
430
+
431
+ const lowerQuery = query.toLowerCase();
432
+ let results = [];
433
+
434
+ for (const file of files) {
435
+ const filePath = path.join(DIRS.sources, file);
436
+ const content = fs.readFileSync(filePath, 'utf8');
437
+ const lines = content.split('\n');
438
+
439
+ for (let i = 0; i < lines.length; i++) {
440
+ const idx = lines[i].toLowerCase().indexOf(lowerQuery);
441
+ if (idx !== -1) {
442
+ const before = lines[i].slice(Math.max(0, idx - 40), idx);
443
+ const match = lines[i].slice(idx, idx + query.length);
444
+ const after = lines[i].slice(idx + query.length, idx + query.length + 40);
445
+ results.push({
446
+ file,
447
+ line: i + 1,
448
+ before,
449
+ match,
450
+ after,
451
+ });
452
+ }
453
+ }
454
+ }
455
+
456
+ if (results.length === 0) {
457
+ console.log(chalk.yellow(`\n No results for "${query}"\n`));
458
+ return;
459
+ }
460
+
461
+ console.log(chalk.cyan(`\n Found ${results.length} match(es) for "${query}"\n`));
462
+ console.log(chalk.gray(' ' + '─'.repeat(Math.min(64, (process.stdout.columns || 120) - 4))));
463
+
464
+ let lastFile = '';
465
+ for (const r of results) {
466
+ if (r.file !== lastFile) {
467
+ lastFile = r.file;
468
+ console.log(`\n ${chalk.white(r.file)}`);
469
+ }
470
+ const snippet = chalk.gray(r.before) + chalk.yellow(r.match) + chalk.gray(r.after);
471
+ console.log(chalk.gray(` ${r.line}:`) + ` ${snippet}`);
472
+ }
473
+ console.log('');
474
+ }
475
+
476
+ // ── get ─────────────────────────────────────────────────────────────────────
477
+ function cmdGet(identifier) {
478
+ if (!identifier) {
479
+ console.log(chalk.red('\n Usage: natureco wiki get <path-or-id>\n'));
480
+ process.exit(1);
481
+ }
482
+
483
+ const candidates = [];
484
+
485
+ if (fs.existsSync(DIRS.sources)) {
486
+ const exactPath = path.join(DIRS.sources, identifier);
487
+ if (fs.existsSync(exactPath)) {
488
+ candidates.push(exactPath);
489
+ }
490
+
491
+ const withMd = path.join(DIRS.sources, `${identifier}.md`);
492
+ if (fs.existsSync(withMd)) {
493
+ candidates.push(withMd);
494
+ }
495
+
496
+ const files = fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'));
497
+ for (const file of files) {
498
+ const base = path.basename(file, '.md');
499
+ if (base === identifier || base.includes(identifier)) {
500
+ const fp = path.join(DIRS.sources, file);
501
+ if (!candidates.includes(fp)) candidates.push(fp);
502
+ }
503
+ }
504
+ }
505
+
506
+ if (candidates.length === 0) {
507
+ console.log(chalk.yellow(`\n No page found for "${identifier}"\n`));
508
+ return;
509
+ }
510
+
511
+ for (const filePath of candidates) {
512
+ const content = fs.readFileSync(filePath, 'utf8');
513
+ const fileName = path.basename(filePath);
514
+ const w = process.stdout.columns || 120;
515
+
516
+ console.log(chalk.cyan(`\n ${fileName}\n`));
517
+ console.log(chalk.gray(' ' + '─'.repeat(Math.min(48, w - 4))));
518
+ console.log('');
519
+ console.log(content.trim());
520
+ console.log('');
521
+ }
522
+ }
523
+
524
+ // ── apply ──────────────────────────────────────────────────────────────────
525
+ function cmdApply(subcommand, params) {
526
+ if (!subcommand || (subcommand !== 'synthesis' && subcommand !== 'metadata')) {
527
+ console.log(chalk.red('\n Usage: natureco wiki apply synthesis|metadata [args]\n'));
528
+ process.exit(1);
529
+ }
530
+
531
+ if (subcommand === 'synthesis') {
532
+ applySynthesis(params);
533
+ } else if (subcommand === 'metadata') {
534
+ applyMetadata(params);
535
+ }
536
+ }
537
+
538
+ function applySynthesis(params) {
539
+ const vault = loadVault();
540
+ if (!vault.initialized) {
541
+ console.log(chalk.yellow('\n Wiki vault not initialized.\n'));
542
+ return;
543
+ }
544
+
545
+ const sourceFiles = fs.existsSync(DIRS.sources)
546
+ ? fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'))
547
+ : [];
548
+
549
+ if (sourceFiles.length === 0) {
550
+ console.log(chalk.yellow('\n No source files to synthesize.\n'));
551
+ return;
552
+ }
553
+
554
+ if (!fs.existsSync(DIRS.cache)) fs.mkdirSync(DIRS.cache, { recursive: true });
555
+
556
+ const digestFile = path.join(DIRS.cache, 'synthesis.json');
557
+ const synthesis = {
558
+ synthesized: new Date().toISOString(),
559
+ totalSources: sourceFiles.length,
560
+ sources: [],
561
+ };
562
+
563
+ for (const file of sourceFiles) {
564
+ const filePath = path.join(DIRS.sources, file);
565
+ const content = fs.readFileSync(filePath, 'utf8');
566
+ const lines = content.split('\n').filter(l => l.trim());
567
+
568
+ synthesis.sources.push({
569
+ file,
570
+ title: extractTitle(content) || path.basename(file, '.md'),
571
+ lineCount: lines.length,
572
+ wordCount: content.split(/\s+/).filter(Boolean).length,
573
+ headingCount: (content.match(/^#{1,6}\s+/gm) || []).length,
574
+ });
575
+ }
576
+
577
+ fs.writeFileSync(digestFile, JSON.stringify(synthesis, null, 2), 'utf8');
578
+
579
+ console.log(chalk.green(`\n Applied synthesis: ${sourceFiles.length} sources catalogued\n`));
580
+ console.log(chalk.gray(` Written to: ${digestFile}\n`));
581
+ }
582
+
583
+ function applyMetadata(params) {
584
+ if (params.length < 2) {
585
+ console.log(chalk.red('\n Usage: natureco wiki apply metadata <file|id> <key=value> [key=value...]\n'));
586
+ process.exit(1);
587
+ }
588
+
589
+ const identifier = params[0];
590
+ const kvPairs = params.slice(1);
591
+
592
+ if (!fs.existsSync(DIRS.sources)) {
593
+ console.log(chalk.yellow('\n No sources directory.\n'));
594
+ return;
595
+ }
596
+
597
+ const files = fs.readdirSync(DIRS.sources).filter(f => f.endsWith('.md'));
598
+ let targetFile = null;
599
+
600
+ for (const file of files) {
601
+ const base = path.basename(file, '.md');
602
+ if (file === identifier || file === `${identifier}.md` || base === identifier) {
603
+ targetFile = file;
604
+ break;
605
+ }
606
+ }
607
+
608
+ if (!targetFile) {
609
+ console.log(chalk.red(`\n No source file found for: ${identifier}\n`));
610
+ process.exit(1);
611
+ }
612
+
613
+ const filePath = path.join(DIRS.sources, targetFile);
614
+ let content = fs.readFileSync(filePath, 'utf8');
615
+
616
+ const lines = content.split('\n');
617
+ let hasFrontMatter = lines[0] && lines[0].trim() === '---';
618
+
619
+ if (!hasFrontMatter) {
620
+ const meta = ['---'];
621
+ for (const kv of kvPairs) {
622
+ const eqIdx = kv.indexOf('=');
623
+ if (eqIdx === -1) continue;
624
+ const key = kv.slice(0, eqIdx).trim();
625
+ const val = kv.slice(eqIdx + 1).trim();
626
+ meta.push(`${key}: ${val}`);
627
+ }
628
+ meta.push('---');
629
+ meta.push('');
630
+ content = meta.join('\n') + content;
631
+ } else {
632
+ const endIdx = lines.slice(1).findIndex(l => l.trim() === '---');
633
+ if (endIdx === -1) {
634
+ console.log(chalk.red(`\n Unclosed front matter in "${targetFile}"\n`));
635
+ process.exit(1);
636
+ }
637
+ const bodyStart = endIdx + 2;
638
+ const frontMatterLines = lines.slice(1, endIdx + 1);
639
+ const body = lines.slice(bodyStart).join('\n');
640
+
641
+ const existingMeta = {};
642
+ const updated = [];
643
+ for (const line of frontMatterLines) {
644
+ if (line.trim() === '---') continue;
645
+ const colonIdx = line.indexOf(':');
646
+ if (colonIdx === -1) {
647
+ updated.push(line);
648
+ continue;
649
+ }
650
+ const key = line.slice(0, colonIdx).trim();
651
+ existingMeta[key] = line.slice(colonIdx + 1).trim();
652
+ }
653
+
654
+ for (const kv of kvPairs) {
655
+ const eqIdx = kv.indexOf('=');
656
+ if (eqIdx === -1) continue;
657
+ const key = kv.slice(0, eqIdx).trim();
658
+ const val = kv.slice(eqIdx + 1).trim();
659
+ existingMeta[key] = val;
660
+ }
661
+
662
+ const newFront = ['---'];
663
+ for (const [key, val] of Object.entries(existingMeta)) {
664
+ newFront.push(`${key}: ${val}`);
665
+ }
666
+ newFront.push('---');
667
+ content = newFront.join('\n') + '\n' + body;
668
+ }
669
+
670
+ fs.writeFileSync(filePath, content, 'utf8');
671
+
672
+ console.log(chalk.green(`\n Applied metadata to "${targetFile}"\n`));
673
+ for (const kv of kvPairs) {
674
+ console.log(chalk.gray(` ${kv}`));
675
+ }
676
+ console.log('');
677
+ }
678
+
679
+ // ── helpers ─────────────────────────────────────────────────────────────────
680
+ function extractTitle(content) {
681
+ const lines = content.split('\n');
682
+ for (const line of lines) {
683
+ const trimmed = line.trim();
684
+ if (trimmed.startsWith('# ') || trimmed.startsWith('#\t')) {
685
+ return trimmed.replace(/^#\s+/, '');
686
+ }
687
+ }
688
+ const firstLine = lines.find(l => l.trim());
689
+ if (firstLine) {
690
+ const trimmed = firstLine.trim();
691
+ if (trimmed.startsWith('#')) {
692
+ return trimmed.replace(/^#+\s*/, '');
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+
698
+ // ── bridge import ──────────────────────────────────────────────────────
699
+ function cmdBridgeImport(url) {
700
+ if (!url) {
701
+ console.log(chalk.red('\n Usage: natureco wiki bridge import <url>\n'));
702
+ process.exit(1);
703
+ }
704
+ console.log(chalk.yellow(`\n Bridge import from ${url} not yet implemented\n`));
705
+ }
706
+
707
+ // ── unsafe-local import ──────────────────────────────────────────────
708
+ function cmdUnsafeLocalImport(filePath) {
709
+ if (!filePath) {
710
+ console.log(chalk.red('\n Usage: natureco wiki unsafe-local import <path>\n'));
711
+ process.exit(1);
712
+ }
713
+ console.log(chalk.yellow(`\n Unsafe local import from ${filePath} not yet implemented\n`));
714
+ }
715
+
716
+ // ── obsidian ─────────────────────────────────────────────────────────
717
+ function cmdObsidian(params) {
718
+ const sub = params[0];
719
+ const rest = params.slice(1).join(' ');
720
+ if (sub === 'status') return cmdObsidianStatus();
721
+ if (sub === 'search') return cmdObsidianSearch(rest);
722
+ if (sub === 'open') return cmdObsidianOpen(rest);
723
+ if (sub === 'command') return cmdObsidianCommand(rest);
724
+ if (sub === 'daily') return cmdObsidianDaily();
725
+ console.log(chalk.red(`\n Unknown obsidian action: ${sub || ''}\n`));
726
+ console.log(chalk.gray(' Usage: natureco wiki obsidian status|search|open|command|daily\n'));
727
+ process.exit(1);
728
+ }
729
+
730
+ function cmdObsidianStatus() {
731
+ const obsidianFile = path.join(WIKI_DIR, 'obsidian.json');
732
+ if (!fs.existsSync(obsidianFile)) {
733
+ console.log(chalk.gray('\n No Obsidian vault configured.\n'));
734
+ return;
735
+ }
736
+ try {
737
+ const data = JSON.parse(fs.readFileSync(obsidianFile, 'utf8'));
738
+ console.log(chalk.cyan('\n Obsidian Vault Status\n'));
739
+ console.log(chalk.white(' Vault: ') + chalk.cyan(data.vault || '—'));
740
+ console.log(chalk.white(' Path: ') + chalk.gray(data.path || '—'));
741
+ console.log(chalk.white(' Notes: ') + chalk.cyan(data.noteCount || 0));
742
+ console.log(chalk.white(' Synced: ') + chalk.gray(data.lastSync ? new Date(data.lastSync).toLocaleString() : 'Never'));
743
+ console.log('');
744
+ } catch (err) {
745
+ console.log(chalk.red('\n Error reading Obsidian config: ' + err.message + '\n'));
746
+ }
747
+ }
748
+
749
+ function cmdObsidianSearch(query) {
750
+ if (!query) {
751
+ console.log(chalk.red('\n Usage: natureco wiki obsidian search <query>\n'));
752
+ process.exit(1);
753
+ }
754
+ const obsidianFile = path.join(WIKI_DIR, 'obsidian.json');
755
+ if (!fs.existsSync(obsidianFile)) {
756
+ console.log(chalk.gray('\n No Obsidian vault configured.\n'));
757
+ return;
758
+ }
759
+ console.log(chalk.yellow(`\n Searching Obsidian vault for "${query}"...\n`));
760
+ console.log(chalk.gray(' (Obsidian search not yet fully implemented)\n'));
761
+ }
762
+
763
+ function cmdObsidianOpen(note) {
764
+ if (!note) {
765
+ console.log(chalk.red('\n Usage: natureco wiki obsidian open <note>\n'));
766
+ process.exit(1);
767
+ }
768
+ console.log(chalk.yellow(`\n Would open ${note} in Obsidian\n`));
769
+ }
770
+
771
+ function cmdObsidianCommand(cmd) {
772
+ if (!cmd) {
773
+ console.log(chalk.red('\n Usage: natureco wiki obsidian command <cmd>\n'));
774
+ process.exit(1);
775
+ }
776
+ console.log(chalk.yellow(`\n Would run Obsidian command: ${cmd}\n`));
777
+ }
778
+
779
+ function cmdObsidianDaily() {
780
+ console.log(chalk.yellow('\n Would open Obsidian daily note\n'));
781
+ }
782
+
783
+ module.exports = wiki;