index.mjs
2,150 lines 79.0 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 18 hours ago
1 #!/usr/bin/env node
2 import '../lib/load-env.mjs';
3
4 /**
5 * Knowtation CLI — single entry point for search, get-note, list-notes, index, etc.
6 */
7
8 import fs from 'fs';
9 import path from 'path';
10 import { spawn } from 'child_process';
11 import { execSync } from 'child_process';
12 import { fileURLToPath } from 'url';
13 import yaml from 'js-yaml';
14
15 const __filename = fileURLToPath(import.meta.url);
16 import { loadConfig } from '../lib/config.mjs';
17 import { readNote, resolveVaultRelativePath, noteFileExistsInVault, normalizeMetadataFacets } from '../lib/vault.mjs';
18 import { buildNoteOutline } from '../lib/note-outline.mjs';
19 import { buildDocumentTree } from '../lib/document-tree.mjs';
20 import { readSectionSource } from '../lib/section-source-note.mjs';
21 import { noteStateIdFromHubNoteJson, absentNoteStateId } from '../lib/note-state-id.mjs';
22 import { runListNotes as runListNotesOp } from '../lib/list-notes.mjs';
23 import { exitWithError } from '../lib/errors.mjs';
24 import { IMPORT_SOURCE_TYPES, IMPORT_SOURCE_TYPES_HELP } from '../lib/import-source-types.mjs';
25
26 const args = process.argv.slice(2);
27 const subcommand = args[0];
28 const useJson = args.includes('--json');
29
30 const help = `
31 knowtation — personal knowledge and content system (know + notation)
32
33 Usage:
34 knowtation <command> [options]
35
36 Commands:
37 search <query> Semantic search over vault (default), or --keyword for literal text. Use --project, --tag, --folder, --limit. --json for machine output.
38 get-note <path> Return full content of one note by path. Use --body-only, --frontmatter-only, --json.
39 get-note-outline <path> Return a derived Markdown heading outline for one note. Requires --json.
40 get-document-tree <path> Return a derived Markdown heading tree for one note. Requires --json.
41 get-metadata-facets <path> Return bounded body-free metadata facets for one note. Requires --json.
42 get-section-source <path> Return body-free SectionSource metadata for one note. Requires --json.
43 list-notes List notes. Use --folder, --project, --tag, --limit, --offset, --fields, --count-only, --json.
44 index Re-run indexer: vault → chunk → embed → vector store (Qdrant or sqlite-vec).
45 write <path> Create or overwrite a note. Use --stdin for body, --frontmatter k=v, --append.
46 export <path|query> <output> Export note(s) to dir/file. Use --format, --project. Provenance and AIR per spec.
47 import <source-type> <input> Ingest from ChatGPT, Claude, Mem0, etc. See docs/IMPORT-SOURCES.md.
48 memory <action> Memory layer commands: query, list, store, search, clear, export, stats. Requires memory.enabled.
49 hub status Check Hub reachability (use --hub <url>). Requires Hub API.
50 doctor Local vault + optional Hub API checks (token discipline per docs/TOKEN-SAVINGS.md). Options: --json, --hub <url>.
51 propose <path> Create a proposal from local vault note (body/frontmatter) on the Hub. Options: --hub, --intent, --vault (X-Vault-Id), --external-ref, --labels a,b, --source agent|human|import, --base-state-id, --no-fetch-base.
52 vault sync Commit and push vault to Git (when vault.git.enabled and remote set). See config.
53 mcp Start MCP server (stdio transport). For Cursor/Claude Desktop.
54 daemon <action> Background consolidation daemon: start [--background], stop, status, log.
55
56 Options (global):
57 --help, -h Show this help or command-specific help.
58 --json Output JSON for piping to other tools.
59
60 Config: config/local.yaml or env (KNOWTATION_VAULT_PATH). Full spec: docs/SPEC.md.
61 `;
62
63 function getOpt(name, type = 'string') {
64 const i = args.indexOf('--' + name);
65 if (i === -1 || !args[i + 1]) return null;
66 const v = args[i + 1];
67 return type === 'number' ? parseInt(v, 10) : v;
68 }
69
70 function hasOpt(name) {
71 return args.includes('--' + name);
72 }
73
74 function runGetNote() {
75 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
76 if (!pathArg) {
77 exitWithError('knowtation get-note: provide a note path.', 1, useJson);
78 }
79 const bodyOnly = hasOpt('body-only');
80 const frontmatterOnly = hasOpt('frontmatter-only');
81 if (bodyOnly && frontmatterOnly) {
82 exitWithError('knowtation get-note: use only one of --body-only or --frontmatter-only.', 1, useJson);
83 }
84
85 let config;
86 try {
87 config = loadConfig();
88 } catch (e) {
89 exitWithError(e.message, 2, useJson);
90 }
91
92 try {
93 resolveVaultRelativePath(config.vault_path, pathArg);
94 } catch (e) {
95 exitWithError(e.message, 2, useJson);
96 }
97
98 let note;
99 try {
100 note = readNote(config.vault_path, pathArg);
101 } catch (e) {
102 exitWithError(e.message, 2, useJson);
103 }
104
105 if (useJson) {
106 if (bodyOnly) {
107 console.log(JSON.stringify({ path: note.path, body: note.body }));
108 } else if (frontmatterOnly) {
109 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter }));
110 } else {
111 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter, body: note.body }));
112 }
113 } else {
114 if (bodyOnly) {
115 process.stdout.write(note.body + (note.body ? '\n' : ''));
116 } else if (frontmatterOnly) {
117 console.log(JSON.stringify(note.frontmatter, null, 2));
118 } else {
119 console.log('---');
120 console.log(yaml.dump(note.frontmatter).trimEnd());
121 console.log('---');
122 if (note.body) console.log(note.body);
123 }
124 }
125 process.exit(0);
126 }
127
128 function runGetNoteOutline() {
129 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
130 if (!pathArg) {
131 exitWithError('knowtation get-note-outline: provide a note path.', 1, useJson);
132 }
133 if (!useJson) {
134 exitWithError('knowtation get-note-outline: --json is required in this MVP.', 1, useJson);
135 }
136
137 let config;
138 try {
139 config = loadConfig();
140 } catch (e) {
141 exitWithError(e.message, 2, useJson);
142 }
143
144 try {
145 resolveVaultRelativePath(config.vault_path, pathArg);
146 } catch (e) {
147 exitWithError(e.message, 2, useJson);
148 }
149
150 let note;
151 try {
152 note = readNote(config.vault_path, pathArg);
153 } catch (e) {
154 exitWithError(e.message, 2, useJson);
155 }
156
157 try {
158 console.log(JSON.stringify(buildNoteOutline(note)));
159 } catch (e) {
160 exitWithError(e.message, 2, useJson);
161 }
162 process.exit(0);
163 }
164
165 function runGetDocumentTree() {
166 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
167 if (!pathArg) {
168 exitWithError('knowtation get-document-tree: provide a note path.', 1, useJson);
169 }
170 if (!useJson) {
171 exitWithError('knowtation get-document-tree: --json is required in this MVP.', 1, useJson);
172 }
173
174 let config;
175 try {
176 config = loadConfig();
177 } catch (e) {
178 exitWithError(e.message, 2, useJson);
179 }
180
181 try {
182 resolveVaultRelativePath(config.vault_path, pathArg);
183 } catch (e) {
184 exitWithError(e.message, 2, useJson);
185 }
186
187 let note;
188 try {
189 note = readNote(config.vault_path, pathArg);
190 } catch (e) {
191 exitWithError(e.message, 2, useJson);
192 }
193
194 try {
195 console.log(JSON.stringify(buildDocumentTree(note)));
196 } catch (e) {
197 exitWithError(e.message, 2, useJson);
198 }
199 process.exit(0);
200 }
201
202 function runGetMetadataFacets() {
203 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
204 if (!pathArg) {
205 exitWithError('knowtation get-metadata-facets: provide a note path.', 1, useJson);
206 }
207 if (!useJson) {
208 exitWithError('knowtation get-metadata-facets: --json is required.', 1, useJson);
209 }
210
211 let config;
212 try {
213 config = loadConfig();
214 } catch (e) {
215 exitWithError(e.message, 2, useJson);
216 }
217
218 try {
219 resolveVaultRelativePath(config.vault_path, pathArg);
220 } catch (e) {
221 exitWithError(e.message, 2, useJson);
222 }
223
224 let note;
225 try {
226 note = readNote(config.vault_path, pathArg);
227 } catch (e) {
228 exitWithError(e.message, 2, useJson);
229 }
230
231 try {
232 console.log(JSON.stringify(normalizeMetadataFacets(note.path, note.frontmatter)));
233 } catch (e) {
234 exitWithError(e.message, 2, useJson);
235 }
236 process.exit(0);
237 }
238
239 function runGetSectionSource() {
240 const pathArgs = args.filter((a, i) => i >= 1 && !a.startsWith('--'));
241 const pathArg = pathArgs[0];
242 if (!pathArg) {
243 exitWithError('knowtation get-section-source: provide a note path.', 1, useJson);
244 }
245 if (pathArgs.length > 1) {
246 exitWithError('knowtation get-section-source: provide exactly one note path.', 1, useJson);
247 }
248 if (!useJson) {
249 exitWithError('knowtation get-section-source: --json is required.', 1, useJson);
250 }
251
252 let config;
253 try {
254 config = loadConfig();
255 } catch (e) {
256 exitWithError(e.message, 2, useJson);
257 }
258
259 try {
260 console.log(JSON.stringify(readSectionSource(config.vault_path, pathArg)));
261 } catch (e) {
262 exitWithError(e.message, 2, useJson);
263 }
264 process.exit(0);
265 }
266
267 function runListNotes() {
268 const folder = getOpt('folder');
269 const project = getOpt('project');
270 const tag = getOpt('tag');
271 const since = getOpt('since');
272 const until = getOpt('until');
273 const chain = getOpt('chain');
274 const entity = getOpt('entity');
275 const episode = getOpt('episode');
276 let limit = getOpt('limit', 'number') ?? 20;
277 let offset = getOpt('offset', 'number') ?? 0;
278 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
279 exitWithError('knowtation list-notes: --limit must be between 0 and 100.', 1, useJson);
280 }
281 if (typeof offset === 'number' && offset < 0) {
282 exitWithError('knowtation list-notes: --offset must be non-negative.', 1, useJson);
283 }
284 limit = Math.min(100, Math.max(0, limit ?? 20));
285 offset = Math.max(0, offset ?? 0);
286 const order = getOpt('order') || 'date';
287 const fields = getOpt('fields') || 'path+metadata';
288 const countOnly = hasOpt('count-only');
289
290 let config;
291 try {
292 config = loadConfig();
293 } catch (e) {
294 exitWithError(e.message, 2, useJson);
295 }
296
297 const out = runListNotesOp(config, {
298 folder: folder ?? undefined,
299 project: project ?? undefined,
300 tag: tag ?? undefined,
301 since: since ?? undefined,
302 until: until ?? undefined,
303 chain: chain ?? undefined,
304 entity: entity ?? undefined,
305 episode: episode ?? undefined,
306 limit,
307 offset,
308 order,
309 fields,
310 countOnly,
311 });
312
313 if (countOnly) {
314 if (useJson) {
315 console.log(JSON.stringify({ total: out.total }));
316 } else {
317 console.log(out.total);
318 }
319 process.exit(0);
320 }
321
322 if (useJson) {
323 console.log(JSON.stringify({ notes: out.notes, total: out.total }));
324 } else {
325 for (const n of out.notes) {
326 const meta = [n.project, n.tags?.join?.(', ') ?? (n.tags || []).join(', '), n.date].filter(Boolean).join(' | ');
327 console.log(n.path + (meta ? ` ${meta}` : ''));
328 }
329 }
330 process.exit(0);
331 }
332
333 async function main() {
334 if (!subcommand || subcommand === '--help' || subcommand === '-h') {
335 console.log(help.trim());
336 process.exit(0);
337 }
338
339 if (subcommand === 'get-note') {
340 if (hasOpt('help') || hasOpt('h')) {
341 console.log('knowtation get-note <path>\n Options: --json, --body-only, --frontmatter-only');
342 process.exit(0);
343 }
344 runGetNote();
345 }
346
347 if (subcommand === 'get-note-outline') {
348 if (hasOpt('help') || hasOpt('h')) {
349 console.log('knowtation get-note-outline <path>\n Options: --json (required)');
350 process.exit(0);
351 }
352 runGetNoteOutline();
353 }
354
355 if (subcommand === 'get-document-tree') {
356 if (hasOpt('help') || hasOpt('h')) {
357 console.log('knowtation get-document-tree <path>\n Options: --json (required)');
358 process.exit(0);
359 }
360 runGetDocumentTree();
361 }
362
363 if (subcommand === 'get-metadata-facets') {
364 if (hasOpt('help') || hasOpt('h')) {
365 console.log('knowtation get-metadata-facets <path>\n Options: --json (required)');
366 process.exit(0);
367 }
368 runGetMetadataFacets();
369 }
370
371 if (subcommand === 'get-section-source') {
372 if (hasOpt('help') || hasOpt('h')) {
373 console.log('knowtation get-section-source <path>\n Options: --json (required)');
374 process.exit(0);
375 }
376 runGetSectionSource();
377 }
378
379 if (subcommand === 'list-notes') {
380 if (hasOpt('help') || hasOpt('h')) {
381 console.log('knowtation list-notes\n Options: --folder, --project, --tag, --since, --until, --chain, --entity, --episode, --limit, --offset, --order date|date-asc, --fields path|path+metadata|full, --count-only, --json');
382 process.exit(0);
383 }
384 runListNotes();
385 }
386
387 if (subcommand === 'search') {
388 if (hasOpt('help') || hasOpt('h')) {
389 console.log('knowtation search <query>\n Options: --keyword (substring/token search), --match phrase|all-terms (with --keyword), --folder, --project, --tag, --since, --until, --chain, --entity, --episode, --content-scope all|notes|approval_logs, --order date|date-asc, --limit, --fields path|path+snippet|full, --snippet-chars <n>, --count-only, --json');
390 process.exit(0);
391 }
392 const query = args.slice(1).filter((a) => !a.startsWith('--')).join(' ').trim();
393 if (!query) {
394 exitWithError('knowtation search: provide a query string.', 1, useJson);
395 }
396 const folder = getOpt('folder');
397 const project = getOpt('project');
398 const tag = getOpt('tag');
399 const since = getOpt('since');
400 const until = getOpt('until');
401 const chain = getOpt('chain');
402 const entity = getOpt('entity');
403 const episode = getOpt('episode');
404 const order = getOpt('order');
405 let limit = getOpt('limit', 'number') ?? 10;
406 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
407 exitWithError('knowtation search: --limit must be between 0 and 100.', 1, useJson);
408 }
409 limit = Math.min(100, Math.max(0, limit ?? 10));
410 const fields = getOpt('fields') || 'path+snippet';
411 const snippetChars = getOpt('snippet-chars', 'number');
412 const countOnly = hasOpt('count-only');
413 const useKeyword = hasOpt('keyword');
414 const matchRaw = getOpt('match');
415 const contentScope = getOpt('content-scope');
416 const validFields = ['path', 'path+snippet', 'full'];
417 if (fields && !validFields.includes(fields)) {
418 exitWithError(`knowtation search: --fields must be one of ${validFields.join(', ')}.`, 1, useJson);
419 }
420 if (matchRaw && !useKeyword) {
421 exitWithError('knowtation search: --match is only valid with --keyword.', 1, useJson);
422 }
423 let match = 'phrase';
424 if (matchRaw) {
425 if (matchRaw === 'all-terms' || matchRaw === 'all_terms') match = 'all_terms';
426 else if (matchRaw === 'phrase') match = 'phrase';
427 else exitWithError('knowtation search: --match must be phrase or all-terms.', 1, useJson);
428 }
429 const validScopes = ['all', 'notes', 'approval_logs'];
430 if (contentScope && !validScopes.includes(contentScope)) {
431 exitWithError(`knowtation search: --content-scope must be one of ${validScopes.join(', ')}.`, 1, useJson);
432 }
433 (async () => {
434 try {
435 const config = loadConfig();
436 const baseOpts = {
437 folder: folder ?? undefined,
438 project: project ?? undefined,
439 tag: tag ?? undefined,
440 since: since ?? undefined,
441 until: until ?? undefined,
442 chain: chain ?? undefined,
443 entity: entity ?? undefined,
444 episode: episode ?? undefined,
445 order: order ?? undefined,
446 limit,
447 fields: fields || 'path+snippet',
448 snippetChars: snippetChars ?? 300,
449 countOnly,
450 content_scope: contentScope === 'all' ? undefined : contentScope ?? undefined,
451 };
452 let out;
453 if (useKeyword) {
454 const { runKeywordSearch } = await import('../lib/keyword-search.mjs');
455 out = await runKeywordSearch(query, { ...baseOpts, match }, config);
456 } else {
457 const { runSearch } = await import('../lib/search.mjs');
458 out = await runSearch(query, baseOpts, config);
459 }
460 if (config.memory?.enabled) {
461 try {
462 const { createMemoryManager } = await import('../lib/memory.mjs');
463 const mm = createMemoryManager(config);
464 if (mm.shouldCapture('search')) {
465 mm.store('search', {
466 query: out.query,
467 mode: useKeyword ? 'keyword' : 'semantic',
468 paths: (out.results || []).map((r) => r.path),
469 count: out.count ?? (out.results || []).length,
470 });
471 }
472 } catch (_) {}
473 }
474 if (useJson) {
475 console.log(JSON.stringify(out));
476 } else {
477 if (out.count !== undefined) {
478 console.log(out.count);
479 } else {
480 const list = out.results || [];
481 for (const r of list) {
482 const meta = [r.project, r.tags?.join(', ')].filter(Boolean).join(' | ');
483 const line = r.snippet != null ? `${r.path}\t${r.snippet}` : r.path;
484 console.log(line + (meta ? ` ${meta}` : ''));
485 }
486 }
487 }
488 process.exit(0);
489 } catch (e) {
490 exitWithError(e.message || String(e), 2, useJson);
491 }
492 })();
493 return;
494 }
495
496 if (subcommand === 'index') {
497 if (hasOpt('help') || hasOpt('h')) {
498 console.log('knowtation index\n Re-run indexer: vault → chunk → embed → vector store. Reads config; exit 0 on success, 2 on failure.');
499 process.exit(0);
500 }
501 const { runIndex } = await import('../lib/indexer.mjs');
502 try {
503 const t0 = Date.now();
504 const result = await runIndex();
505 const config = loadConfig();
506 if (config.memory?.enabled) {
507 try {
508 const { createMemoryManager } = await import('../lib/memory.mjs');
509 const mm = createMemoryManager(config);
510 if (mm.shouldCapture('index')) {
511 mm.store('index', {
512 notes_processed: result.notesProcessed,
513 chunks_indexed: result.chunksIndexed,
514 duration_ms: Date.now() - t0,
515 });
516 }
517 } catch (_) {}
518 }
519 if (useJson) {
520 console.log(JSON.stringify({ ok: true, notesProcessed: result.notesProcessed, chunksIndexed: result.chunksIndexed }));
521 }
522 process.exit(0);
523 } catch (e) {
524 exitWithError(e.message, 2, useJson);
525 }
526 }
527
528 if (subcommand === 'write') {
529 if (hasOpt('help') || hasOpt('h')) {
530 console.log('knowtation write <path> [content]\n Options: --stdin (body from stdin), --frontmatter k=v [k2=v2 ...], --append, --json');
531 process.exit(0);
532 }
533 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
534 if (!pathArg) {
535 exitWithError('knowtation write: provide a note path.', 1, useJson);
536 }
537 const stdin = hasOpt('stdin');
538 const append = hasOpt('append');
539 const frontmatterPairs = [];
540 for (let i = 0; i < args.length; i++) {
541 if (args[i] === '--frontmatter' && args[i + 1]) {
542 let j = i + 1;
543 while (j < args.length && !args[j].startsWith('--') && args[j].includes('=')) {
544 frontmatterPairs.push(args[j]);
545 j++;
546 }
547 break;
548 }
549 }
550 const frontmatterOverrides = {};
551 for (const p of frontmatterPairs) {
552 const eq = p.indexOf('=');
553 if (eq > 0) {
554 frontmatterOverrides[p.slice(0, eq).trim()] = p.slice(eq + 1).trim();
555 }
556 }
557 let body;
558 if (stdin) {
559 body = fs.readFileSync(0, 'utf8');
560 } else {
561 const contentArg = args[args.indexOf(pathArg) + 1];
562 body = contentArg && !contentArg.startsWith('--') ? contentArg : undefined;
563 }
564 let config;
565 try {
566 config = loadConfig();
567 } catch (e) {
568 exitWithError(e.message, 2, useJson);
569 }
570 (async () => {
571 try {
572 const { writeNote } = await import('../lib/write.mjs');
573 const result = await writeNote(config.vault_path, pathArg, {
574 body,
575 frontmatter: Object.keys(frontmatterOverrides).length ? frontmatterOverrides : undefined,
576 append,
577 config,
578 });
579 try {
580 const { maybeAutoSync } = await import('../lib/vault-git-sync.mjs');
581 maybeAutoSync(config);
582 } catch (_) {}
583 if (config.memory?.enabled) {
584 try {
585 const { createMemoryManager } = await import('../lib/memory.mjs');
586 const mm = createMemoryManager(config);
587 if (mm.shouldCapture('write')) {
588 mm.store('write', {
589 path: result.path,
590 action: append ? 'append' : 'create',
591 air_id: result.air_id || undefined,
592 });
593 }
594 } catch (_) {}
595 }
596 if (useJson) {
597 console.log(JSON.stringify(result));
598 } else {
599 console.log(`Written: ${result.path}`);
600 }
601 process.exit(0);
602 } catch (e) {
603 exitWithError(e.message, 2, useJson);
604 }
605 })();
606 return;
607 }
608
609 if (subcommand === 'export') {
610 if (hasOpt('help') || hasOpt('h')) {
611 console.log('knowtation export <path-or-query> <output-dir-or-file>\n Options: --format md|html, --project <slug>, --json');
612 process.exit(0);
613 }
614 const pathOrQuery = args[1];
615 const output = args[2];
616 if (!pathOrQuery || !output) {
617 exitWithError('knowtation export: provide <path-or-query> and <output-dir-or-file>.', 1, useJson);
618 }
619 const format = getOpt('format') || 'md';
620 const project = getOpt('project');
621 if (format && !['md', 'html'].includes(format)) {
622 exitWithError('knowtation export: --format must be md or html.', 1, useJson);
623 }
624 let config;
625 try {
626 config = loadConfig();
627 } catch (e) {
628 exitWithError(e.message, 2, useJson);
629 }
630 (async () => {
631 try {
632 const { exportNotes } = await import('../lib/export.mjs');
633 const { attestBeforeExport } = await import('../lib/air.mjs');
634 let paths = [];
635 const looksLikePath = !pathOrQuery.includes(' ') && (pathOrQuery.endsWith('.md') || pathOrQuery.includes('/'));
636 if (looksLikePath) {
637 try {
638 resolveVaultRelativePath(config.vault_path, pathOrQuery);
639 paths = [pathOrQuery];
640 } catch (_) {
641 // Fall through: treat as query
642 }
643 }
644 if (paths.length === 0) {
645 const { runSearch } = await import('../lib/search.mjs');
646 const result = await runSearch(pathOrQuery, {
647 limit: 50,
648 project: project ?? undefined,
649 fields: 'path',
650 });
651 paths = (result.results || []).map((r) => r.path).filter(Boolean);
652 }
653 if (!paths.length) {
654 exitWithError('knowtation export: no notes found for path or query.', 2, useJson);
655 }
656 if (config.air?.enabled) {
657 await attestBeforeExport(config, paths);
658 }
659 const result = exportNotes(config.vault_path, paths, output, { format });
660 if (config.memory?.enabled) {
661 try {
662 const { createMemoryManager } = await import('../lib/memory.mjs');
663 const mm = createMemoryManager(config);
664 if (mm.shouldCapture('export')) {
665 mm.store('export', { provenance: result.provenance, exported: result.exported, format });
666 }
667 } catch (_) {}
668 }
669 if (useJson) {
670 console.log(JSON.stringify({ exported: result.exported, provenance: result.provenance }));
671 } else {
672 for (const e of result.exported) {
673 console.log(`${e.path} → ${e.output}`);
674 }
675 if (result.provenance) console.log(result.provenance);
676 }
677 process.exit(0);
678 } catch (e) {
679 exitWithError(e.message, 2, useJson);
680 }
681 })();
682 return;
683 }
684
685 if (subcommand === 'import') {
686 if (hasOpt('help') || hasOpt('h')) {
687 console.log(
688 `knowtation import <source-type> <input>\n Options: --project, --output-dir, --tags t1,t2, --dry-run, --json, --sheets-range 'A1 range' (google-sheets only), --url-mode auto|bookmark|extract (url only)\n Source types: ${IMPORT_SOURCE_TYPES_HELP}`
689 );
690 process.exit(0);
691 }
692 const sourceType = args[1];
693 const input = args[2];
694 if (!sourceType) {
695 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
696 }
697 if (sourceType !== 'google-sheets' && !input) {
698 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
699 }
700 if (sourceType === 'google-sheets' && !input) {
701 exitWithError(
702 'knowtation import google-sheets: provide the spreadsheet id as <input> (the id from the Google Sheets URL).',
703 1,
704 useJson,
705 );
706 }
707 if (!IMPORT_SOURCE_TYPES.includes(sourceType)) {
708 exitWithError(`Unknown source-type "${sourceType}". Valid: ${IMPORT_SOURCE_TYPES_HELP}.`, 1, useJson);
709 }
710 (async () => {
711 try {
712 const config = loadConfig();
713 const { runImport } = await import('../lib/import.mjs');
714 const project = getOpt('project');
715 const outputDir = getOpt('output-dir');
716 const tagsOpt = getOpt('tags');
717 const tags = tagsOpt ? tagsOpt.split(',').map((t) => t.trim()).filter(Boolean) : [];
718 const dryRun = hasOpt('dry-run');
719 let memoryManager;
720 if (config.memory?.enabled && !dryRun) {
721 try {
722 const { createMemoryManager } = await import('../lib/memory.mjs');
723 memoryManager = createMemoryManager(config);
724 } catch (_) {}
725 }
726
727 const urlModeRaw = getOpt('url-mode');
728 let urlMode;
729 if (urlModeRaw) {
730 const v = String(urlModeRaw).trim().toLowerCase();
731 if (v !== 'auto' && v !== 'bookmark' && v !== 'extract') {
732 exitWithError(`Invalid --url-mode "${urlModeRaw}". Use auto, bookmark, or extract.`, 1, useJson);
733 }
734 urlMode = v;
735 }
736 if (urlModeRaw && sourceType !== 'url') {
737 exitWithError('--url-mode is only valid when source-type is url.', 1, useJson);
738 }
739 const sheetsRangeRaw = getOpt('sheets-range');
740 if (sheetsRangeRaw && sourceType !== 'google-sheets') {
741 exitWithError('--sheets-range is only valid when source-type is google-sheets.', 1, useJson);
742 }
743
744 const importOpts = {
745 project: project ?? undefined,
746 outputDir: outputDir ?? undefined,
747 tags,
748 dryRun,
749 ...(sourceType === 'url' && urlMode ? { urlMode } : {}),
750 ...(sourceType === 'google-sheets' && sheetsRangeRaw
751 ? { sheetsRange: String(sheetsRangeRaw).trim() }
752 : {}),
753 };
754 if (memoryManager && sourceType === 'mem0-export' && memoryManager.shouldCapture('capture')) {
755 importOpts.onMemoryEvent = (data) => {
756 try { memoryManager.store('capture', data); } catch (_) {}
757 };
758 }
759
760 const result = await runImport(sourceType, input, importOpts);
761 if (memoryManager) {
762 try {
763 if (memoryManager.shouldCapture('import')) {
764 memoryManager.store('import', {
765 source_type: sourceType,
766 count: result.count ?? 0,
767 paths: (result.imported || []).map((r) => r.path).slice(0, 50),
768 project: project ?? undefined,
769 });
770 }
771 } catch (_) {}
772 }
773 if (useJson) {
774 console.log(JSON.stringify({ imported: result.imported, count: result.count }));
775 } else {
776 for (const r of result.imported) {
777 console.log(r.path);
778 }
779 if (result.count === 0) {
780 console.log('No notes imported.');
781 } else {
782 console.log(`Imported ${result.count} note(s).`);
783 }
784 }
785 process.exit(0);
786 } catch (e) {
787 exitWithError(e.message, 2, useJson);
788 }
789 })();
790 return;
791 }
792
793 if (subcommand === 'mcp') {
794 if (hasOpt('help') || hasOpt('h')) {
795 console.log(
796 'knowtation mcp\n Start MCP server (default: stdio for Cursor / Claude Desktop).\n Streamable HTTP: MCP_TRANSPORT=http or KNOWTATION_MCP_TRANSPORT=http (see docs/MCP-PHASE-D.md).\n Requires config/local.yaml and KNOWTATION_VAULT_PATH.'
797 );
798 process.exit(0);
799 }
800 const serverMod = await import('../mcp/server.mjs');
801 return;
802 }
803
804 if (subcommand === 'memory') {
805 const action = args[1];
806 if (hasOpt('help') || hasOpt('h')) {
807 console.log(`knowtation memory <action>
808 Actions:
809 query <key> Read latest value for an event type (e.g. search, export, write, import, index, propose, user).
810 list List recent memory events. --type, --topic, --since, --until, --limit (default 20), --json.
811 store <key> <value> Store a user-defined memory entry. Value is JSON string or --stdin.
812 search <query> Semantic search over memory (requires vector or mem0 provider). --limit, --json.
813 clear Clear memory. --type, --before <date>, --confirm required. --json.
814 export Export memory log. --format jsonl|mif, --since, --until, --type. Output to stdout.
815 stats Show memory statistics. --json.
816 index Print lightweight pointer index (markdown). --json returns structured object.
817 consolidate Run LLM-powered memory consolidation. --dry-run, --passes consolidate,verify,discover, --lookback-hours <n>. --json.
818
819 Options: --json`);
820 process.exit(0);
821 }
822 const validActions = ['query', 'list', 'store', 'search', 'clear', 'export', 'stats', 'index', 'consolidate'];
823 if (!action || !validActions.includes(action)) {
824 exitWithError(`knowtation memory: use "memory <action>". Actions: ${validActions.join(', ')}.`, 1, useJson);
825 }
826 let config;
827 try {
828 config = loadConfig();
829 } catch (e) {
830 exitWithError(e.message, 2, useJson);
831 }
832 if (!config.memory?.enabled) {
833 exitWithError('knowtation memory: memory layer not enabled. Set memory.enabled in config.', 2, useJson);
834 }
835 (async () => {
836 try {
837 const { createMemoryManager } = await import('../lib/memory.mjs');
838 const { MEMORY_EVENT_TYPES } = await import('../lib/memory-event.mjs');
839 const scopeOpt = getOpt('scope') === 'global' ? 'global' : undefined;
840 const mm = createMemoryManager(config, 'default', scopeOpt ? { scope: scopeOpt } : {});
841
842 if (action === 'query') {
843 const keyArg = args[2];
844 if (!keyArg) {
845 exitWithError('knowtation memory query: provide a key (event type).', 1, useJson);
846 }
847 const key = keyArg.replace(/\s+/g, '_');
848 const latest = mm.getLatest(key);
849 if (!latest) {
850 if (useJson) console.log(JSON.stringify({ key, value: null }));
851 else console.log('(no value)');
852 } else {
853 const { id: _id, vault_id: _vid, ...display } = latest;
854 if (useJson) console.log(JSON.stringify({ key, value: display }));
855 else console.log(JSON.stringify(display, null, 2));
856 }
857 process.exit(0);
858 }
859
860 if (action === 'list') {
861 const type = getOpt('type');
862 const topic = getOpt('topic');
863 const since = getOpt('since');
864 const until = getOpt('until');
865 const limit = getOpt('limit', 'number') ?? 20;
866 const events = mm.list({ type: type ?? undefined, topic: topic ?? undefined, since: since ?? undefined, until: until ?? undefined, limit });
867 if (useJson) {
868 console.log(JSON.stringify({ events, count: events.length }));
869 } else {
870 if (events.length === 0) console.log('(no events)');
871 for (const e of events) {
872 const summary = JSON.stringify(e.data).slice(0, 120);
873 console.log(`${e.ts} ${e.type} ${summary}`);
874 }
875 }
876 process.exit(0);
877 }
878
879 if (action === 'store') {
880 const keyArg = args[2];
881 if (!keyArg) {
882 exitWithError('knowtation memory store: provide a key.', 1, useJson);
883 }
884 let valueRaw;
885 if (hasOpt('stdin')) {
886 valueRaw = fs.readFileSync(0, 'utf8').trim();
887 } else {
888 valueRaw = args[3];
889 }
890 if (!valueRaw) {
891 exitWithError('knowtation memory store: provide a value (JSON string) or --stdin.', 1, useJson);
892 }
893 let value;
894 try {
895 value = JSON.parse(valueRaw);
896 } catch (_) {
897 value = { text: valueRaw };
898 }
899 const result = mm.store('user', { key: keyArg, ...value });
900 if (useJson) console.log(JSON.stringify(result));
901 else console.log(`Stored: ${result.id}`);
902 process.exit(0);
903 }
904
905 if (action === 'search') {
906 const query = args.slice(2).filter((a) => !a.startsWith('--')).join(' ').trim();
907 if (!query) {
908 exitWithError('knowtation memory search: provide a query string.', 1, useJson);
909 }
910 if (!mm.supportsSearch()) {
911 exitWithError('knowtation memory search: semantic search requires memory.provider: vector or mem0.', 2, useJson);
912 }
913 const limit = getOpt('limit', 'number') ?? 10;
914 const results = mm.search(query, { limit });
915 if (useJson) {
916 console.log(JSON.stringify({ results, count: results.length }));
917 } else {
918 if (results.length === 0) console.log('(no results)');
919 for (const r of results) {
920 console.log(`${r.ts} ${r.type} ${JSON.stringify(r.data).slice(0, 120)}`);
921 }
922 }
923 process.exit(0);
924 }
925
926 if (action === 'clear') {
927 if (!hasOpt('confirm')) {
928 exitWithError('knowtation memory clear: use --confirm to confirm deletion.', 1, useJson);
929 }
930 const type = getOpt('type');
931 const before = getOpt('before');
932 const result = mm.clear({ type: type ?? undefined, before: before ?? undefined });
933 if (useJson) console.log(JSON.stringify(result));
934 else console.log(`Cleared ${result.cleared} event(s).`);
935 process.exit(0);
936 }
937
938 if (action === 'export') {
939 const format = getOpt('format') || 'jsonl';
940 if (!['jsonl', 'mif'].includes(format)) {
941 exitWithError('knowtation memory export: --format must be jsonl or mif.', 1, useJson);
942 }
943 const type = getOpt('type');
944 const since = getOpt('since');
945 const until = getOpt('until');
946 const events = mm.list({ type: type ?? undefined, since: since ?? undefined, until: until ?? undefined, limit: 10000 });
947 if (format === 'jsonl') {
948 for (const e of events) {
949 console.log(JSON.stringify(e));
950 }
951 } else {
952 for (const e of events) {
953 console.log(`---`);
954 console.log(`id: ${e.id}`);
955 console.log(`type: ${e.type}`);
956 console.log(`ts: ${e.ts}`);
957 console.log(`vault_id: ${e.vault_id}`);
958 console.log(`---`);
959 console.log(JSON.stringify(e.data, null, 2));
960 console.log('');
961 }
962 }
963 process.exit(0);
964 }
965
966 if (action === 'summarize') {
967 const since = getOpt('since') || new Date(Date.now() - 86_400_000).toISOString();
968 const maxTokens = getOpt('max-tokens', 'number') ?? 512;
969 const dryRun = hasOpt('dry-run');
970 try {
971 const { generateSessionSummary } = await import('../lib/memory-session-summary.mjs');
972 const result = await generateSessionSummary(config, { since, maxTokens, dryRun });
973 if (useJson) {
974 console.log(JSON.stringify(result));
975 } else {
976 console.log(result.summary);
977 if (result.id) console.log(`\nStored as: ${result.id}`);
978 console.log(`Events summarized: ${result.event_count}`);
979 }
980 } catch (e) {
981 exitWithError(`Session summary failed: ${e.message}`, 2, useJson);
982 }
983 process.exit(0);
984 }
985
986 if (action === 'consolidate') {
987 const dryRun = hasOpt('dry-run');
988 const passesRaw = getOpt('passes', 'string');
989 const passes = passesRaw
990 ? passesRaw.split(',').map((s) => s.trim()).filter(Boolean)
991 : undefined;
992 const lookbackHours = getOpt('lookback-hours', 'number') ?? undefined;
993 try {
994 const { consolidateMemory } = await import('../lib/memory-consolidate.mjs');
995 const result = await consolidateMemory(config, { dryRun, passes, lookbackHours });
996 if (useJson) {
997 console.log(JSON.stringify(result));
998 } else if (result.dry_run) {
999 console.log(`[dry-run] Would process ${result.total_events} events across ${result.topics.length} topics.`);
1000 for (const t of result.topics) {
1001 console.log(`[dry-run] Topic "${t.topic}": ${t.event_count} events → ${t.dry_run_estimate || 'estimated facts'}`);
1002 }
1003 if (result.verify) {
1004 console.log(`[dry-run] Verify pass: would check paths in events (no writes).`);
1005 }
1006 if (result.discover) {
1007 console.log(`[dry-run] Discover pass: would analyze ${result.discover.topic_count} topic(s) for cross-topic insights (no writes).`);
1008 }
1009 } else if (result.topics.length === 0 && !result.verify && !result.discover) {
1010 console.log('No events to consolidate.');
1011 } else {
1012 if (result.topics.length > 0) {
1013 console.log(`Consolidated ${result.total_events} events across ${result.topics.length} topics.`);
1014 for (const t of result.topics) {
1015 if (t.error) {
1016 console.log(` ${t.topic}: error — ${t.error}`);
1017 } else {
1018 console.log(` ${t.topic}: ${t.facts.length} facts written${t.id ? ` (${t.id})` : ''}`);
1019 }
1020 }
1021 console.log('Index regenerated.');
1022 }
1023 if (result.verify) {
1024 const v = result.verify;
1025 console.log(`Verify pass: checked ${v.checked_count} events — ${v.verified_paths.length} verified, ${v.stale_paths.length} stale.`);
1026 if (v.stale_paths.length > 0) {
1027 for (const p of v.stale_paths) console.log(` stale: ${p}`);
1028 }
1029 }
1030 if (result.discover) {
1031 const d = result.discover;
1032 console.log(`Discover pass: ${d.connections.length} connection(s), ${d.contradictions.length} contradiction(s), ${d.open_questions.length} open question(s) across ${d.topic_count} topic(s).`);
1033 }
1034 }
1035 } catch (e) {
1036 exitWithError(`Consolidation failed: ${e.message}`, 2, useJson);
1037 }
1038 process.exit(0);
1039 }
1040
1041 if (action === 'index') {
1042 const idx = mm.generateIndex({ force: true });
1043 if (useJson) {
1044 console.log(JSON.stringify(idx));
1045 } else {
1046 console.log(idx.markdown);
1047 }
1048 process.exit(0);
1049 }
1050
1051 if (action === 'stats') {
1052 const stats = mm.stats();
1053 if (useJson) {
1054 console.log(JSON.stringify(stats));
1055 } else {
1056 console.log(`Total events: ${stats.total}`);
1057 console.log(`Storage: ${stats.size_bytes} bytes`);
1058 if (stats.oldest) console.log(`Oldest: ${stats.oldest}`);
1059 if (stats.newest) console.log(`Newest: ${stats.newest}`);
1060 if (Object.keys(stats.counts_by_type).length > 0) {
1061 console.log('Counts by type:');
1062 for (const [t, c] of Object.entries(stats.counts_by_type)) {
1063 console.log(` ${t}: ${c}`);
1064 }
1065 }
1066 }
1067 process.exit(0);
1068 }
1069 } catch (e) {
1070 exitWithError(e.message, 2, useJson);
1071 }
1072 })();
1073 return;
1074 }
1075
1076 if (subcommand === 'doctor') {
1077 if (hasOpt('help') || hasOpt('h')) {
1078 console.log(
1079 'knowtation doctor\n Checks local vault config (disk vault) and optional Hub API (KNOWTATION_HUB_*).\n Explains vault vs terminal token discipline per docs/TOKEN-SAVINGS.md.\n Options: --json, --hub <url> (override KNOWTATION_HUB_URL for probes only).'
1080 );
1081 process.exit(0);
1082 }
1083 const hubUrlOpt = getOpt('hub');
1084 const { runDoctor } = await import('./doctor.mjs');
1085 const code = await runDoctor({ useJson, hubUrlOpt });
1086 process.exit(code);
1087 }
1088
1089 if (subcommand === 'hub') {
1090 const action = args[1];
1091 if (action !== 'status') {
1092 exitWithError('knowtation hub: use "hub status". Option: --hub <url>.', 1, useJson);
1093 }
1094 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL || 'http://localhost:3333';
1095 const base = hubUrl.replace(/\/$/, '');
1096 (async () => {
1097 try {
1098 const res = await fetch(base + '/health', { method: 'GET' });
1099 const data = await res.json().catch(() => ({}));
1100 if (useJson) {
1101 console.log(JSON.stringify({ ok: res.ok, status: res.status, url: base }));
1102 } else {
1103 console.log(res.ok ? `Hub at ${base} is up.` : `Hub at ${base} returned ${res.status}.`);
1104 }
1105 process.exit(res.ok ? 0 : 2);
1106 } catch (e) {
1107 exitWithError('Hub unreachable: ' + e.message, 2, useJson);
1108 }
1109 })();
1110 return;
1111 }
1112
1113 if (subcommand === 'vault') {
1114 const vaultSub = args[1];
1115 if (vaultSub === 'sync') {
1116 if (hasOpt('help') || hasOpt('h')) {
1117 console.log('knowtation vault sync\n Commits and pushes the vault to the configured Git remote.\n Requires config: vault.git.enabled=true and vault.git.remote=<url>.');
1118 process.exit(0);
1119 }
1120 let config;
1121 try {
1122 config = loadConfig();
1123 } catch (e) {
1124 exitWithError(e.message, 2, useJson);
1125 }
1126 (async () => {
1127 try {
1128 const { runVaultSync } = await import('../lib/vault-git-sync.mjs');
1129 const result = runVaultSync(config);
1130 if (useJson) console.log(JSON.stringify(result));
1131 else console.log(result.message === 'Synced' ? 'Vault synced to remote.' : result.message);
1132 process.exit(0);
1133 } catch (e) {
1134 exitWithError('knowtation vault sync: ' + (e.message || 'git failed'), 1, useJson);
1135 }
1136 })();
1137 return;
1138 }
1139 exitWithError('knowtation vault: unknown subcommand. Use vault sync.', 1, useJson);
1140 }
1141
1142 if (subcommand === 'propose') {
1143 const pathArg = args[1];
1144 if (!pathArg || pathArg.startsWith('--')) {
1145 exitWithError('knowtation propose: provide a vault-relative note path (e.g. inbox/note.md).', 1, useJson);
1146 }
1147 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL;
1148 if (!hubUrl) {
1149 exitWithError('knowtation propose: set --hub <url> or KNOWTATION_HUB_URL.', 1, useJson);
1150 }
1151 let config;
1152 try {
1153 config = loadConfig();
1154 } catch (e) {
1155 exitWithError(e.message, 2, useJson);
1156 }
1157 try {
1158 resolveVaultRelativePath(config.vault_path, pathArg);
1159 } catch (e) {
1160 exitWithError(e.message, 2, useJson);
1161 }
1162 const intent = getOpt('intent') || '';
1163 const token = process.env.KNOWTATION_HUB_TOKEN;
1164 if (!token) {
1165 exitWithError('knowtation propose: set KNOWTATION_HUB_TOKEN (JWT from Hub login).', 2, useJson);
1166 }
1167 const base = hubUrl.replace(/\/$/, '');
1168 const vaultHdr = getOpt('vault') || process.env.KNOWTATION_HUB_VAULT_ID;
1169 const labelsRaw = getOpt('labels');
1170 const labels = labelsRaw
1171 ? labelsRaw
1172 .split(',')
1173 .map((s) => s.trim())
1174 .filter(Boolean)
1175 : undefined;
1176 const source = getOpt('source') || undefined;
1177 const externalRef = getOpt('external-ref') || undefined;
1178 const baseStateOverride = getOpt('base-state-id');
1179 const skipFetchBase = hasOpt('no-fetch-base');
1180
1181 let bodyText = '';
1182 let frontmatter = {};
1183 if (noteFileExistsInVault(config.vault_path, pathArg)) {
1184 const n = readNote(config.vault_path, pathArg);
1185 bodyText = n.body;
1186 frontmatter = n.frontmatter;
1187 }
1188
1189 (async () => {
1190 try {
1191 const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token };
1192 if (vaultHdr) headers['X-Vault-Id'] = vaultHdr;
1193
1194 let baseStateId = baseStateOverride && String(baseStateOverride).trim() ? String(baseStateOverride).trim() : '';
1195 if (!baseStateId && !skipFetchBase) {
1196 const encPath = pathArg.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/');
1197 const gres = await fetch(`${base}/api/v1/notes/${encPath}`, { method: 'GET', headers });
1198 if (gres.status === 404) {
1199 baseStateId = absentNoteStateId();
1200 } else if (gres.ok) {
1201 const noteJson = await gres.json();
1202 baseStateId = noteStateIdFromHubNoteJson(noteJson);
1203 }
1204 }
1205
1206 const payload = {
1207 path: pathArg.replace(/\\/g, '/'),
1208 body: bodyText,
1209 frontmatter,
1210 intent: intent || undefined,
1211 external_ref: externalRef || undefined,
1212 labels,
1213 source,
1214 };
1215 if (baseStateId) payload.base_state_id = baseStateId;
1216
1217 const res = await fetch(base + '/api/v1/proposals', {
1218 method: 'POST',
1219 headers,
1220 body: JSON.stringify(payload),
1221 });
1222 const data = await res.json().catch(() => ({}));
1223 if (!res.ok) {
1224 exitWithError(data.error || res.statusText, 2, useJson);
1225 return;
1226 }
1227 try {
1228 const config2 = loadConfig();
1229 if (config2.memory?.enabled) {
1230 const { createMemoryManager } = await import('../lib/memory.mjs');
1231 const mm = createMemoryManager(config2);
1232 if (mm.shouldCapture('propose')) {
1233 mm.store('propose', {
1234 proposal_id: data.proposal_id,
1235 path: data.path || pathArg,
1236 intent: intent || undefined,
1237 base_state_id: baseStateId || undefined,
1238 });
1239 }
1240 }
1241 } catch (_) {}
1242 if (useJson) console.log(JSON.stringify(data));
1243 else console.log('Proposal created:', data.proposal_id, data.path);
1244 process.exit(0);
1245 } catch (e) {
1246 exitWithError(e.message, 2, useJson);
1247 }
1248 })();
1249 return;
1250 }
1251
1252 if (subcommand === 'flow') {
1253 const flowAction = args[1];
1254 if (hasOpt('help') || hasOpt('h') || !flowAction) {
1255 console.log(`knowtation flow <action>
1256 Actions (v0 read surface):
1257 list [--scope personal|project|org] [--tag <t>] [--limit <n>] [--json]
1258 get <flow_id> [--version <semver>] [--json]
1259 project <flow_id> --harness <harness> [--version <semver>] [--out <path>] [--check] [--json]
1260
1261 Authoring (gated by FLOW_AUTHORING_WRITES; default off):
1262 propose <bundle.json> [--intent <text>] [--base-version <semver>] [--base-state-id <flowst1_…>] [--json]
1263 import <bundle.json> [--intent <text>] [--external-ref <ref>] [--source-vault-hint <hint>] [--json]
1264
1265 External-agent grants (gated by FLOW_EXTERNAL_AGENT_ENABLED; default off):
1266 grant mint <flow_id> --flow-version <semver> --tools <id>[,<id>…] [--ttl-seconds <n>] [--actor-label <label>] [--json]
1267 grant revoke <grant_id> [--json]
1268 grant list [--flow-id <id>] [--json]
1269
1270 Run execution (gated by FLOW_RUN_WRITES_ENABLED / FLOW_AUTOMATABLE_EXECUTION_ENABLED; default off):
1271 run start <flow_id> --flow-version <semver> [--task-ref <id>] [--external-ref <ref>] [--json]
1272 run get <run_id> [--json]
1273 run list [--flow-id <id>] [--json]
1274 run advance <run_id> --step-id <id> --to-status <status> [--skip-reason <enum>] [--json]
1275 run evidence <run_id> --step-id <id> --evidence-ref <ref> --pointer-kind <kind> [--json]
1276 run execute <run_id> --step-id <id> --consent-id <id> [--model-lane <lane>] [--dry-run] [--json]
1277 run consent <run_id> --lanes <lane>[,<lane>…] --cost-cap <n> [--ttl-seconds <n>] [--json]
1278 run submit-review <run_id> --intent <text> [--json]
1279
1280 Capture flywheel (gated by FLOW_CAPTURE_DETECTION_ENABLED / FLOW_CAPTURE_WRITES_ENABLED; default off):
1281 capture observe <signals.json> [--include-low-confidence] [--json]
1282 capture list [--scope personal|project|org] [--include-low-confidence] [--limit <n>] [--json]
1283 capture propose <candidate_id> --confirmed-scope <scope> --intent <text> [--scope-widen-acknowledged] [--allow-low-confidence] [--force-new-flow] [--merge-into-flow-id <id>] [--json]
1284 capture dismiss <candidate_id> --intent <text> [--json]
1285
1286 Reserved (not wired in v0): export
1287
1288 Options: --json (exact Hub JSON)`);
1289 process.exit(0);
1290 }
1291
1292 const gatedActions = ['export'];
1293 if (gatedActions.includes(flowAction)) {
1294 exitWithError(`knowtation flow ${flowAction}: not available in v0 (gated).`, 1, useJson);
1295 }
1296
1297 let config;
1298 try {
1299 config = loadConfig();
1300 } catch (e) {
1301 exitWithError(e.message, 2, useJson);
1302 }
1303
1304 const vaultId = getOpt('vault') || 'default';
1305 const cliScopes = Array.isArray(config.flow?.visible_scopes) ? config.flow.visible_scopes : undefined;
1306
1307 const flowExitWithError = (message, codeStr, exitCode = 1) => {
1308 if (useJson) {
1309 process.stderr.write(JSON.stringify({ error: message, code: codeStr }) + '\n');
1310 } else {
1311 console.error(message);
1312 }
1313 process.exit(exitCode);
1314 };
1315
1316 if (flowAction === 'list') {
1317 const limitOpt = getOpt('limit', 'number');
1318 const { handleFlowListRequest } = await import('../lib/flow/flow-handlers.mjs');
1319 const result = handleFlowListRequest({
1320 dataDir: config.data_dir,
1321 vaultId,
1322 cliScopes,
1323 scope: getOpt('scope') ?? undefined,
1324 tag: getOpt('tag') ?? undefined,
1325 limit: limitOpt ?? undefined,
1326 });
1327 if (!result.ok) {
1328 flowExitWithError(result.error, result.code);
1329 }
1330 if (useJson) {
1331 console.log(JSON.stringify(result.payload));
1332 } else {
1333 const rows = result.payload.flows;
1334 if (rows.length === 0) {
1335 console.log('(no flows)');
1336 } else {
1337 for (const f of rows) {
1338 console.log(`${f.flow_id} ${f.version} [${f.scope}] ${f.title} (${f.step_count} steps)`);
1339 }
1340 }
1341 if (result.payload.truncated) {
1342 console.log('(truncated)');
1343 }
1344 }
1345 process.exit(0);
1346 }
1347
1348 if (flowAction === 'get') {
1349 const flowId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1350 if (!flowId) {
1351 flowExitWithError('knowtation flow get: provide a flow_id.', 'BAD_REQUEST');
1352 }
1353 const { handleFlowGetRequest } = await import('../lib/flow/flow-handlers.mjs');
1354 const result = handleFlowGetRequest({
1355 dataDir: config.data_dir,
1356 vaultId,
1357 flowId,
1358 cliScopes,
1359 version: getOpt('version') ?? undefined,
1360 });
1361 if (!result.ok) {
1362 flowExitWithError(result.error, result.code);
1363 }
1364 if (useJson) {
1365 console.log(JSON.stringify(result.payload));
1366 } else {
1367 const { flow, steps } = result.payload;
1368 console.log(`${flow.flow_id} ${flow.version} [${flow.scope}]`);
1369 console.log(flow.title);
1370 console.log(flow.summary);
1371 console.log(`Steps (${steps.length}):`);
1372 for (const s of steps) {
1373 console.log(` ${s.ordinal}. ${s.owned_job}`);
1374 }
1375 }
1376 process.exit(0);
1377 }
1378
1379 if (flowAction === 'project') {
1380 const flowId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1381 const harness = getOpt('harness');
1382 if (!flowId) {
1383 flowExitWithError('knowtation flow project: provide a flow_id.', 'BAD_REQUEST');
1384 }
1385 if (!harness) {
1386 flowExitWithError('knowtation flow project: --harness is required.', 'BAD_REQUEST');
1387 }
1388 const checkMode = hasOpt('check');
1389 const outPath = getOpt('out') ?? undefined;
1390 const { handleFlowProjectRequest } = await import('../lib/flow/flow-handlers.mjs');
1391 const { detectDrift, defaultProjectionOutPath } = await import('../lib/flow/projection-generator.mjs');
1392 const result = handleFlowProjectRequest({
1393 dataDir: config.data_dir,
1394 vaultId,
1395 flowId,
1396 harness,
1397 cliScopes,
1398 version: getOpt('version') ?? undefined,
1399 });
1400 if (!result.ok) {
1401 flowExitWithError(result.error, result.code, result.status === 403 ? 1 : 1);
1402 }
1403
1404 const artifactPath = outPath || defaultProjectionOutPath(flowId, harness);
1405 if (checkMode) {
1406 if (!artifactPath) {
1407 flowExitWithError('knowtation flow project --check: provide --out or use an active harness.', 'BAD_REQUEST');
1408 }
1409 const fs = await import('node:fs');
1410 let onDisk = '';
1411 try {
1412 onDisk = fs.readFileSync(artifactPath, 'utf8');
1413 } catch {
1414 onDisk = '';
1415 }
1416 const drift = detectDrift(onDisk, result.payload.projection.rendered);
1417 const stale = result.payload.staleness.stale === true;
1418 if (useJson) {
1419 console.log(
1420 JSON.stringify({
1421 check: true,
1422 drift,
1423 stale,
1424 staleness: result.payload.staleness,
1425 generator: result.payload.generator,
1426 }),
1427 );
1428 } else {
1429 console.log(`drift: ${drift.drift} (${drift.reason})`);
1430 console.log(`stale: ${stale}`);
1431 if (stale) {
1432 console.log(
1433 `versions: projection ${result.payload.staleness.projection_version} < latest ${result.payload.staleness.latest_version}`,
1434 );
1435 }
1436 }
1437 if (drift.drift || stale) {
1438 process.exit(1);
1439 }
1440 process.exit(0);
1441 }
1442
1443 if (outPath && artifactPath) {
1444 const fs = await import('node:fs');
1445 const pathMod = await import('node:path');
1446 const dir = pathMod.dirname(artifactPath);
1447 if (!fs.existsSync(dir)) {
1448 fs.mkdirSync(dir, { recursive: true });
1449 }
1450 fs.writeFileSync(artifactPath, result.payload.projection.rendered, 'utf8');
1451 }
1452
1453 if (useJson) {
1454 console.log(JSON.stringify(result.payload));
1455 } else {
1456 console.log(result.payload.projection.rendered);
1457 const { staleness, projection } = result.payload;
1458 console.error('');
1459 console.error(`staleness: ${staleness.stale ? 'stale' : 'fresh'} (${staleness.projection_version} vs latest ${staleness.latest_version})`);
1460 if (projection.fidelity?.dropped_fields?.length) {
1461 console.error(`dropped fields: ${projection.fidelity.dropped_fields.join(', ')}`);
1462 }
1463 if (projection.fidelity?.notes) {
1464 console.error(`fidelity: ${projection.fidelity.notes}`);
1465 }
1466 if (outPath) {
1467 console.error(`wrote: ${artifactPath}`);
1468 }
1469 }
1470 process.exit(0);
1471 }
1472
1473 if (flowAction === 'propose' || flowAction === 'import') {
1474 const bundlePath = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1475 if (!bundlePath) {
1476 flowExitWithError(`knowtation flow ${flowAction}: provide a bundle.json path.`, 'BAD_REQUEST');
1477 }
1478 const fsMod = await import('node:fs');
1479 let bundle;
1480 try {
1481 bundle = JSON.parse(fsMod.readFileSync(bundlePath, 'utf8'));
1482 } catch (e) {
1483 flowExitWithError(`knowtation flow ${flowAction}: cannot read bundle (${e.message}).`, 'BAD_REQUEST');
1484 }
1485 const intent = getOpt('intent') || (bundle && typeof bundle === 'object' ? bundle.intent : undefined);
1486 const { handleFlowProposeRequest } = await import('../lib/flow/flow-authoring.mjs');
1487 const { createProposal } = await import('../hub/proposals-store.mjs');
1488
1489 let result;
1490 if (flowAction === 'import') {
1491 result = handleFlowProposeRequest({
1492 dataDir: config.data_dir,
1493 vaultId,
1494 cliScopes,
1495 kind: 'import',
1496 bundle: { flow: bundle?.flow, steps: bundle?.steps },
1497 intent,
1498 externalRef: getOpt('external-ref') || (bundle && bundle.external_ref) || undefined,
1499 sourceVaultHint: getOpt('source-vault-hint') || (bundle && bundle.source_vault_hint) || undefined,
1500 createProposal,
1501 });
1502 } else {
1503 const baseVersion = getOpt('base-version') || (bundle && bundle.base_version) || undefined;
1504 const baseStateId = getOpt('base-state-id') || (bundle && bundle.base_state_id) || undefined;
1505 result = handleFlowProposeRequest({
1506 dataDir: config.data_dir,
1507 vaultId,
1508 cliScopes,
1509 kind: baseVersion ? 'edit' : 'new',
1510 flow: bundle?.flow,
1511 steps: bundle?.steps,
1512 intent,
1513 flowId: bundle?.flow?.flow_id,
1514 baseVersion,
1515 baseStateId,
1516 createProposal,
1517 });
1518 }
1519
1520 if (!result.ok) {
1521 flowExitWithError(result.error, result.code);
1522 }
1523 if (useJson) {
1524 console.log(JSON.stringify(result.payload));
1525 } else {
1526 const p = result.payload;
1527 console.log(`proposed ${p.flow_id} → ${p.proposal_id} [${p.scope}] (status: ${p.status})`);
1528 console.log(`review queue: ${p.review_queue} auto_approvable: ${p.auto_approvable}`);
1529 }
1530 process.exit(0);
1531 }
1532
1533 if (flowAction === 'run') {
1534 const runSub = args[2];
1535 if (!runSub || hasOpt('help') || hasOpt('h')) {
1536 console.log('knowtation flow run start|get|list|advance|evidence|execute|consent|submit-review — see knowtation flow --help');
1537 process.exit(0);
1538 }
1539
1540 const {
1541 handleFlowRunStartRequest,
1542 handleFlowRunGetRequest,
1543 handleFlowRunListRequest,
1544 handleFlowRunAdvanceRequest,
1545 handleFlowRunEvidenceRequest,
1546 handleFlowRunExecuteAutomatableRequest,
1547 handleFlowRunSubmitReviewRequest,
1548 handleFlowExecutionConsentMintRequest,
1549 } = await import('../lib/flow/flow-execution.mjs');
1550 const { createProposal } = await import('../hub/proposals-store.mjs');
1551
1552 if (runSub === 'start') {
1553 const flowId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1554 const flowVersion = getOpt('flow-version');
1555 if (!flowId || !flowVersion) {
1556 flowExitWithError('knowtation flow run start: provide flow_id and --flow-version.', 'BAD_REQUEST');
1557 }
1558 const result = handleFlowRunStartRequest({
1559 dataDir: config.data_dir,
1560 vaultId,
1561 cliScopes,
1562 flowId,
1563 flowVersion,
1564 taskRef: getOpt('task-ref') ?? undefined,
1565 externalRef: getOpt('external-ref') ?? undefined,
1566 harness: 'cli',
1567 });
1568 if (!result.ok) {
1569 flowExitWithError(result.error, result.code);
1570 }
1571 if (useJson) {
1572 console.log(JSON.stringify(result.payload));
1573 } else {
1574 console.log(`started run ${result.payload.run.run_id} for ${flowId}@${flowVersion}`);
1575 }
1576 process.exit(0);
1577 }
1578
1579 if (runSub === 'get') {
1580 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1581 if (!runId) {
1582 flowExitWithError('knowtation flow run get: provide run_id.', 'BAD_REQUEST');
1583 }
1584 const result = handleFlowRunGetRequest({
1585 dataDir: config.data_dir,
1586 vaultId,
1587 cliScopes,
1588 runId,
1589 });
1590 if (!result.ok) {
1591 flowExitWithError(result.error, result.code);
1592 }
1593 if (useJson) {
1594 console.log(JSON.stringify(result.payload));
1595 } else {
1596 console.log(JSON.stringify(result.payload.run, null, 2));
1597 }
1598 process.exit(0);
1599 }
1600
1601 if (runSub === 'list') {
1602 const result = handleFlowRunListRequest({
1603 dataDir: config.data_dir,
1604 vaultId,
1605 cliScopes,
1606 flowId: getOpt('flow-id') ?? undefined,
1607 });
1608 if (!result.ok) {
1609 flowExitWithError(result.error, result.code);
1610 }
1611 if (useJson) {
1612 console.log(JSON.stringify(result.payload));
1613 } else {
1614 for (const run of result.payload.runs) {
1615 console.log(`${run.run_id} ${run.flow_id}@${run.flow_version} [${run.status}]`);
1616 }
1617 }
1618 process.exit(0);
1619 }
1620
1621 if (runSub === 'advance') {
1622 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1623 const stepId = getOpt('step-id');
1624 const toStatus = getOpt('to-status');
1625 if (!runId || !stepId || !toStatus) {
1626 flowExitWithError(
1627 'knowtation flow run advance: provide run_id, --step-id, and --to-status.',
1628 'BAD_REQUEST',
1629 );
1630 }
1631 const result = handleFlowRunAdvanceRequest({
1632 dataDir: config.data_dir,
1633 vaultId,
1634 cliScopes,
1635 runId,
1636 stepId,
1637 toStatus,
1638 skipReason: getOpt('skip-reason') ?? undefined,
1639 });
1640 if (!result.ok) {
1641 flowExitWithError(result.error, result.code);
1642 }
1643 if (useJson) {
1644 console.log(JSON.stringify(result.payload));
1645 } else {
1646 console.log(`advanced ${stepId} → ${toStatus}`);
1647 }
1648 process.exit(0);
1649 }
1650
1651 if (runSub === 'evidence') {
1652 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1653 const stepId = getOpt('step-id');
1654 const evidenceRef = getOpt('evidence-ref');
1655 const pointerKind = getOpt('pointer-kind');
1656 if (!runId || !stepId || !evidenceRef || !pointerKind) {
1657 flowExitWithError(
1658 'knowtation flow run evidence: provide run_id, --step-id, --evidence-ref, --pointer-kind.',
1659 'BAD_REQUEST',
1660 );
1661 }
1662 const result = handleFlowRunEvidenceRequest({
1663 dataDir: config.data_dir,
1664 vaultId,
1665 cliScopes,
1666 runId,
1667 stepId,
1668 evidenceRef,
1669 pointerKind,
1670 });
1671 if (!result.ok) {
1672 flowExitWithError(result.error, result.code);
1673 }
1674 if (useJson) {
1675 console.log(JSON.stringify(result.payload));
1676 } else {
1677 console.log(`evidence recorded on ${stepId}`);
1678 }
1679 process.exit(0);
1680 }
1681
1682 if (runSub === 'execute') {
1683 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1684 const stepId = getOpt('step-id');
1685 const consentId = getOpt('consent-id');
1686 if (!runId || !stepId || !consentId) {
1687 flowExitWithError(
1688 'knowtation flow run execute: provide run_id, --step-id, and --consent-id.',
1689 'BAD_REQUEST',
1690 );
1691 }
1692 const result = handleFlowRunExecuteAutomatableRequest({
1693 dataDir: config.data_dir,
1694 vaultId,
1695 cliScopes,
1696 runId,
1697 stepId,
1698 consentId,
1699 modelLane: getOpt('model-lane') ?? undefined,
1700 dryRun: hasOpt('dry-run'),
1701 });
1702 if (!result.ok) {
1703 flowExitWithError(result.error, result.code);
1704 }
1705 if (useJson) {
1706 console.log(JSON.stringify(result.payload));
1707 } else {
1708 console.log(`execution ${result.payload.execution.execution_id} → ${result.payload.execution.status}`);
1709 }
1710 process.exit(0);
1711 }
1712
1713 if (runSub === 'consent') {
1714 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1715 const lanesRaw = getOpt('lanes');
1716 const costCap = getOpt('cost-cap', 'number');
1717 if (!runId || !lanesRaw || costCap === undefined) {
1718 flowExitWithError(
1719 'knowtation flow run consent: provide run_id, --lanes, and --cost-cap.',
1720 'BAD_REQUEST',
1721 );
1722 }
1723 const allowedLanes = lanesRaw.split(',').map((l) => l.trim()).filter(Boolean);
1724 const ttlRaw = getOpt('ttl-seconds', 'number');
1725 const result = handleFlowExecutionConsentMintRequest({
1726 dataDir: config.data_dir,
1727 vaultId,
1728 cliScopes,
1729 runId,
1730 allowedLanes,
1731 costCapUnits: costCap,
1732 ttlSeconds: ttlRaw ?? undefined,
1733 });
1734 if (!result.ok) {
1735 flowExitWithError(result.error, result.code);
1736 }
1737 if (useJson) {
1738 console.log(JSON.stringify(result.payload));
1739 } else {
1740 console.log(`consent ${result.payload.consent.consent_id} expires ${result.payload.consent.expires_at}`);
1741 }
1742 process.exit(0);
1743 }
1744
1745 if (runSub === 'submit-review') {
1746 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1747 const intent = getOpt('intent');
1748 if (!runId || !intent) {
1749 flowExitWithError('knowtation flow run submit-review: provide run_id and --intent.', 'BAD_REQUEST');
1750 }
1751 const result = handleFlowRunSubmitReviewRequest({
1752 dataDir: config.data_dir,
1753 vaultId,
1754 cliScopes,
1755 runId,
1756 intent,
1757 createProposal,
1758 });
1759 if (!result.ok) {
1760 flowExitWithError(result.error, result.code);
1761 }
1762 if (useJson) {
1763 console.log(JSON.stringify(result.payload));
1764 } else {
1765 console.log(`submitted ${runId} → proposal ${result.payload.proposal_id}`);
1766 }
1767 process.exit(0);
1768 }
1769
1770 flowExitWithError(`knowtation flow run: unknown subcommand ${runSub}`, 'BAD_REQUEST');
1771 }
1772
1773 if (flowAction === 'grant') {
1774 const grantAction = args[2];
1775 if (!grantAction || hasOpt('help') || hasOpt('h')) {
1776 console.log('knowtation flow grant mint|revoke|list — see knowtation flow --help');
1777 process.exit(0);
1778 }
1779
1780 const {
1781 handleFlowExternalGrantMintRequest,
1782 handleFlowExternalGrantRevokeRequest,
1783 handleFlowExternalGrantListRequest,
1784 } = await import('../lib/flow/external-agent.mjs');
1785
1786 if (grantAction === 'mint') {
1787 const flowId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1788 const flowVersion = getOpt('flow-version');
1789 const toolsRaw = getOpt('tools');
1790 if (!flowId || !flowVersion || !toolsRaw) {
1791 flowExitWithError(
1792 'knowtation flow grant mint: provide flow_id, --flow-version, and --tools.',
1793 'BAD_REQUEST',
1794 );
1795 }
1796 const requestedTools = toolsRaw.split(',').map((t) => t.trim()).filter(Boolean);
1797 const ttlRaw = getOpt('ttl-seconds', 'number');
1798 const result = handleFlowExternalGrantMintRequest({
1799 dataDir: config.data_dir,
1800 vaultId,
1801 cliScopes,
1802 flowId,
1803 flowVersion,
1804 requestedTools,
1805 ttlSeconds: ttlRaw ?? undefined,
1806 actorLabel: getOpt('actor-label') ?? undefined,
1807 });
1808 if (!result.ok) {
1809 flowExitWithError(result.error, result.code);
1810 }
1811 if (useJson) {
1812 console.log(JSON.stringify(result.payload));
1813 } else {
1814 console.log(`grant ${result.payload.grant.grant_id} expires ${result.payload.expires_at}`);
1815 console.log(`bearer (one-time): ${result.payload.bearer}`);
1816 }
1817 process.exit(0);
1818 }
1819
1820 if (grantAction === 'revoke') {
1821 const grantId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1822 if (!grantId) {
1823 flowExitWithError('knowtation flow grant revoke: provide grant_id.', 'BAD_REQUEST');
1824 }
1825 const result = handleFlowExternalGrantRevokeRequest({
1826 dataDir: config.data_dir,
1827 vaultId,
1828 grantId,
1829 });
1830 if (!result.ok) {
1831 flowExitWithError(result.error, result.code);
1832 }
1833 if (useJson) {
1834 console.log(JSON.stringify(result.payload));
1835 } else {
1836 console.log(`revoked ${grantId} at ${result.payload.revoked_at}`);
1837 }
1838 process.exit(0);
1839 }
1840
1841 if (grantAction === 'list') {
1842 const result = handleFlowExternalGrantListRequest({
1843 dataDir: config.data_dir,
1844 vaultId,
1845 flowId: getOpt('flow-id') ?? undefined,
1846 });
1847 if (!result.ok) {
1848 flowExitWithError(result.error, result.code);
1849 }
1850 if (useJson) {
1851 console.log(JSON.stringify(result.payload));
1852 } else {
1853 for (const g of result.payload.grants) {
1854 console.log(`${g.grant_id} ${g.flow_id}@${g.flow_version} tools=${g.allowed_tools.join(',')}`);
1855 }
1856 }
1857 process.exit(0);
1858 }
1859
1860 flowExitWithError(`knowtation flow grant: unknown action "${grantAction}".`, 'BAD_REQUEST');
1861 }
1862
1863 if (flowAction === 'capture') {
1864 const captureAction = args[2];
1865 if (!captureAction || hasOpt('help') || hasOpt('h')) {
1866 console.log('knowtation flow capture observe|list|propose|dismiss — see knowtation flow --help');
1867 process.exit(0);
1868 }
1869
1870 const {
1871 handleFlowCaptureObserveRequest,
1872 handleFlowCaptureListRequest,
1873 handleFlowCaptureProposeRequest,
1874 handleFlowCaptureDismissRequest,
1875 } = await import('../lib/flow/flow-capture.mjs');
1876 const { createProposal } = await import('../hub/proposals-store.mjs');
1877
1878 if (captureAction === 'observe') {
1879 const signalsPath = args[3];
1880 if (!signalsPath) {
1881 flowExitWithError('knowtation flow capture observe: provide a signals.json path.', 'BAD_REQUEST');
1882 }
1883 const fsMod = await import('node:fs');
1884 let sessionMeta;
1885 try {
1886 sessionMeta = JSON.parse(fsMod.readFileSync(signalsPath, 'utf8'));
1887 } catch (e) {
1888 flowExitWithError(`knowtation flow capture observe: cannot read signals (${e.message}).`, 'BAD_REQUEST');
1889 }
1890 const result = handleFlowCaptureObserveRequest({
1891 dataDir: config.data_dir,
1892 vaultId,
1893 cliScopes,
1894 sessionMeta,
1895 includeLowConfidence: hasOpt('include-low-confidence'),
1896 harness: 'cli',
1897 config,
1898 });
1899 if (!result.ok) flowExitWithError(result.error, result.code);
1900 if (useJson) console.log(JSON.stringify(result.payload));
1901 else console.log(JSON.stringify(result.payload, null, 2));
1902 process.exit(0);
1903 }
1904
1905 if (captureAction === 'list') {
1906 const limitOpt = getOpt('limit', 'number');
1907 const result = handleFlowCaptureListRequest({
1908 dataDir: config.data_dir,
1909 vaultId,
1910 cliScopes,
1911 scope: getOpt('scope') ?? undefined,
1912 includeLowConfidence: hasOpt('include-low-confidence'),
1913 limit: limitOpt ?? undefined,
1914 config,
1915 });
1916 if (!result.ok) flowExitWithError(result.error, result.code);
1917 if (useJson) console.log(JSON.stringify(result.payload));
1918 else console.log(JSON.stringify(result.payload, null, 2));
1919 process.exit(0);
1920 }
1921
1922 if (captureAction === 'propose') {
1923 const candidateId = args[3];
1924 const intent = getOpt('intent');
1925 const confirmedScope = getOpt('confirmed-scope');
1926 if (!candidateId || !intent || !confirmedScope) {
1927 flowExitWithError(
1928 'knowtation flow capture propose: provide candidate_id, --intent, and --confirmed-scope.',
1929 'BAD_REQUEST',
1930 );
1931 }
1932 const result = handleFlowCaptureProposeRequest({
1933 dataDir: config.data_dir,
1934 vaultId,
1935 cliScopes,
1936 candidateId,
1937 confirmedScope,
1938 scopeWidenAcknowledged: hasOpt('scope-widen-acknowledged'),
1939 allowLowConfidence: hasOpt('allow-low-confidence'),
1940 forceNewFlow: hasOpt('force-new-flow'),
1941 mergeIntoFlowId: getOpt('merge-into-flow-id') ?? undefined,
1942 intent,
1943 createProposal,
1944 config,
1945 });
1946 if (!result.ok) flowExitWithError(result.error, result.code);
1947 if (useJson) console.log(JSON.stringify(result.payload));
1948 else console.log(JSON.stringify(result.payload, null, 2));
1949 process.exit(0);
1950 }
1951
1952 if (captureAction === 'dismiss') {
1953 const candidateId = args[3];
1954 const intent = getOpt('intent');
1955 if (!candidateId || !intent) {
1956 flowExitWithError('knowtation flow capture dismiss: provide candidate_id and --intent.', 'BAD_REQUEST');
1957 }
1958 const result = handleFlowCaptureDismissRequest({
1959 dataDir: config.data_dir,
1960 vaultId,
1961 cliScopes,
1962 candidateId,
1963 intent,
1964 createProposal,
1965 });
1966 if (!result.ok) flowExitWithError(result.error, result.code);
1967 if (useJson) console.log(JSON.stringify(result.payload));
1968 else console.log(JSON.stringify(result.payload, null, 2));
1969 process.exit(0);
1970 }
1971
1972 flowExitWithError(`knowtation flow capture: unknown action "${captureAction}".`, 'BAD_REQUEST');
1973 }
1974
1975 exitWithError(`knowtation flow: unknown action "${flowAction}". Use list, get, project, propose, import, grant, capture, or run.`, 1, useJson);
1976 return;
1977 }
1978
1979 if (subcommand === 'daemon') {
1980 const daemonAction = args[1];
1981
1982 if (!daemonAction || hasOpt('help') || hasOpt('h')) {
1983 console.log(`knowtation daemon <action>
1984 Actions:
1985 start [--background] Start the daemon. --background runs it detached (writes PID).
1986 stop Stop a running daemon (SIGTERM → SIGKILL after 10 s).
1987 status Show running state, PID, last pass, next scheduled pass.
1988 log [--tail <n>] Print daemon log entries (JSONL). --tail limits to last N.
1989
1990 Notes:
1991 - Daemon requires daemon.enabled in config and a reachable LLM.
1992 - Foreground mode: Ctrl+C to stop (SIGINT).
1993 - Background mode writes PID to {data_dir}/daemon.pid, log to {data_dir}/daemon.log.`);
1994 process.exit(0);
1995 }
1996
1997 let config;
1998 try {
1999 config = loadConfig();
2000 } catch (e) {
2001 exitWithError(e.message, 2, useJson);
2002 }
2003
2004 // ── daemon start ───────────────────────────────────────────────────────
2005 if (daemonAction === 'start') {
2006 const background = hasOpt('background');
2007
2008 if (background) {
2009 // Spawn a detached child that runs `knowtation daemon start` (foreground).
2010 // Use env var to prevent the child from re-entering background-spawn logic.
2011 const child = spawn(process.execPath, [__filename, 'daemon', 'start'], {
2012 detached: true,
2013 stdio: 'ignore',
2014 env: { ...process.env, KNOWTATION_DAEMON_BACKGROUND: '0' },
2015 });
2016 child.unref();
2017
2018 const pidPath = path.join(config.data_dir, 'daemon.pid');
2019 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
2020
2021 if (useJson) {
2022 console.log(JSON.stringify({ ok: true, pid: child.pid, pid_path: pidPath, log_path: logPath }));
2023 } else {
2024 const llmProvider = config.daemon?.llm?.provider || 'auto-detect';
2025 const llmModel = config.daemon?.llm?.model || 'default';
2026 console.log(`Daemon started in background (PID ${child.pid}). Consolidation every ${config.daemon?.interval_minutes ?? 120} min when idle.`);
2027 console.log(`LLM: ${llmProvider} ${llmModel}.`);
2028 console.log(`Log: ${logPath}`);
2029 }
2030 process.exit(0);
2031 return;
2032 }
2033
2034 // Foreground mode
2035 if (!config.daemon?.enabled && process.env.KNOWTATION_DAEMON_BACKGROUND !== '0') {
2036 console.warn('Warning: daemon.enabled is false in config. Starting anyway (foreground mode).');
2037 }
2038
2039 (async () => {
2040 try {
2041 const { startDaemon } = await import('../lib/daemon.mjs');
2042 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
2043 const intervalMin = config.daemon?.interval_minutes ?? 120;
2044 console.log(`Daemon starting (PID ${process.pid}). Consolidation every ${intervalMin} min when idle.`);
2045 console.log(`Log: ${logPath}. Press Ctrl+C to stop.`);
2046 await startDaemon(config);
2047 console.log('Daemon stopped.');
2048 process.exit(0);
2049 } catch (e) {
2050 exitWithError(`Daemon start failed: ${e.message}`, 2, useJson);
2051 }
2052 })();
2053 return;
2054 }
2055
2056 // ── daemon stop ────────────────────────────────────────────────────────
2057 if (daemonAction === 'stop') {
2058 (async () => {
2059 try {
2060 const { stopDaemon } = await import('../lib/daemon.mjs');
2061 const result = await stopDaemon(config);
2062 if (useJson) {
2063 console.log(JSON.stringify(result));
2064 } else if (result.stopped) {
2065 console.log(`Daemon stopped (PID ${result.pid}, signal ${result.signal}).`);
2066 } else {
2067 console.log(`Daemon was not running: ${result.reason}`);
2068 }
2069 process.exit(0);
2070 } catch (e) {
2071 exitWithError(`Daemon stop failed: ${e.message}`, 2, useJson);
2072 }
2073 })();
2074 return;
2075 }
2076
2077 // ── daemon status ──────────────────────────────────────────────────────
2078 if (daemonAction === 'status') {
2079 try {
2080 const { getDaemonStatus } = await import('../lib/daemon.mjs');
2081 const status = getDaemonStatus(config);
2082 if (useJson) {
2083 console.log(JSON.stringify(status));
2084 } else if (!status.running) {
2085 console.log('Status: not running');
2086 if (status.last_pass) {
2087 console.log(`Last pass: ${status.last_pass.ts} (${status.last_pass.events_processed} events, ${status.last_pass.topics} topics)`);
2088 }
2089 console.log(`Log: ${status.log_path}`);
2090 } else {
2091 const uptimeSec = Math.round((status.uptime_ms ?? 0) / 1000);
2092 const uptimeStr = uptimeSec < 60
2093 ? `${uptimeSec}s`
2094 : uptimeSec < 3600
2095 ? `${Math.round(uptimeSec / 60)}m ${uptimeSec % 60}s`
2096 : `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
2097 console.log(`Status: running (PID ${status.pid}, uptime ${uptimeStr})`);
2098 if (status.last_pass) {
2099 const lp = status.last_pass;
2100 console.log(`Last pass: ${lp.ts} (processed ${lp.events_processed} events, ${lp.topics} topics)`);
2101 } else {
2102 console.log('Last pass: none yet');
2103 }
2104 if (status.next_pass_at) {
2105 console.log(`Next pass: ~${status.next_pass_at} (if idle)`);
2106 }
2107 }
2108 process.exit(0);
2109 } catch (e) {
2110 exitWithError(`Daemon status failed: ${e.message}`, 2, useJson);
2111 }
2112 return;
2113 }
2114
2115 // ── daemon log ─────────────────────────────────────────────────────────
2116 if (daemonAction === 'log') {
2117 const tail = getOpt('tail', 'number') ?? null;
2118 try {
2119 const { getLogPath, readDaemonLog } = await import('../lib/daemon.mjs');
2120 const logPath = getLogPath(config);
2121 const entries = readDaemonLog(logPath, { tail: tail ?? undefined });
2122 if (useJson) {
2123 console.log(JSON.stringify({ entries, count: entries.length, log_path: logPath }));
2124 } else if (entries.length === 0) {
2125 console.log(`(no log entries — log: ${logPath})`);
2126 } else {
2127 for (const e of entries) {
2128 const { ts, event, ...rest } = e;
2129 const detail = Object.keys(rest).length ? ' ' + JSON.stringify(rest) : '';
2130 console.log(`${ts} ${event ?? '?'}${detail}`);
2131 }
2132 }
2133 process.exit(0);
2134 } catch (e) {
2135 exitWithError(`Daemon log failed: ${e.message}`, 2, useJson);
2136 }
2137 return;
2138 }
2139
2140 exitWithError(`knowtation daemon: unknown action "${daemonAction}". Use start, stop, status, or log.`, 1, useJson);
2141 return;
2142 }
2143
2144 exitWithError(`Unknown command: ${subcommand}`, 1, useJson);
2145 }
2146
2147 main().catch((e) => {
2148 console.error(e.message || e);
2149 process.exit(2);
2150 });
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 18 hours ago