index.mjs
2,910 lines 106.7 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days 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 auth <subcommand> Offline-locked auth: generate-setup-token, bootstrap-admin, token.
51 doctor Local vault + optional Hub API checks (token discipline per docs/TOKEN-SAVINGS.md). Options: --json, --hub <url>.
52 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.
53 vault sync Commit and push vault to Git (when vault.git.enabled and remote set). See config.
54 mcp Start MCP server (stdio transport). For Cursor/Claude Desktop.
55 daemon <action> Background consolidation daemon: start [--background], stop, status, log.
56
57 Options (global):
58 --help, -h Show this help or command-specific help.
59 --json Output JSON for piping to other tools.
60
61 Config: config/local.yaml or env (KNOWTATION_VAULT_PATH). Full spec: docs/SPEC.md.
62 `;
63
64 function getOpt(name, type = 'string') {
65 const i = args.indexOf('--' + name);
66 if (i === -1 || !args[i + 1]) return null;
67 const v = args[i + 1];
68 return type === 'number' ? parseInt(v, 10) : v;
69 }
70
71 function hasOpt(name) {
72 return args.includes('--' + name);
73 }
74
75 function runGetNote() {
76 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
77 if (!pathArg) {
78 exitWithError('knowtation get-note: provide a note path.', 1, useJson);
79 }
80 const bodyOnly = hasOpt('body-only');
81 const frontmatterOnly = hasOpt('frontmatter-only');
82 if (bodyOnly && frontmatterOnly) {
83 exitWithError('knowtation get-note: use only one of --body-only or --frontmatter-only.', 1, useJson);
84 }
85
86 let config;
87 try {
88 config = loadConfig();
89 } catch (e) {
90 exitWithError(e.message, 2, useJson);
91 }
92
93 try {
94 resolveVaultRelativePath(config.vault_path, pathArg);
95 } catch (e) {
96 exitWithError(e.message, 2, useJson);
97 }
98
99 let note;
100 try {
101 note = readNote(config.vault_path, pathArg);
102 } catch (e) {
103 exitWithError(e.message, 2, useJson);
104 }
105
106 if (useJson) {
107 if (bodyOnly) {
108 console.log(JSON.stringify({ path: note.path, body: note.body }));
109 } else if (frontmatterOnly) {
110 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter }));
111 } else {
112 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter, body: note.body }));
113 }
114 } else {
115 if (bodyOnly) {
116 process.stdout.write(note.body + (note.body ? '\n' : ''));
117 } else if (frontmatterOnly) {
118 console.log(JSON.stringify(note.frontmatter, null, 2));
119 } else {
120 console.log('---');
121 console.log(yaml.dump(note.frontmatter).trimEnd());
122 console.log('---');
123 if (note.body) console.log(note.body);
124 }
125 }
126 process.exit(0);
127 }
128
129 function runGetNoteOutline() {
130 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
131 if (!pathArg) {
132 exitWithError('knowtation get-note-outline: provide a note path.', 1, useJson);
133 }
134 if (!useJson) {
135 exitWithError('knowtation get-note-outline: --json is required in this MVP.', 1, useJson);
136 }
137
138 let config;
139 try {
140 config = loadConfig();
141 } catch (e) {
142 exitWithError(e.message, 2, useJson);
143 }
144
145 try {
146 resolveVaultRelativePath(config.vault_path, pathArg);
147 } catch (e) {
148 exitWithError(e.message, 2, useJson);
149 }
150
151 let note;
152 try {
153 note = readNote(config.vault_path, pathArg);
154 } catch (e) {
155 exitWithError(e.message, 2, useJson);
156 }
157
158 try {
159 console.log(JSON.stringify(buildNoteOutline(note)));
160 } catch (e) {
161 exitWithError(e.message, 2, useJson);
162 }
163 process.exit(0);
164 }
165
166 function runGetDocumentTree() {
167 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
168 if (!pathArg) {
169 exitWithError('knowtation get-document-tree: provide a note path.', 1, useJson);
170 }
171 if (!useJson) {
172 exitWithError('knowtation get-document-tree: --json is required in this MVP.', 1, useJson);
173 }
174
175 let config;
176 try {
177 config = loadConfig();
178 } catch (e) {
179 exitWithError(e.message, 2, useJson);
180 }
181
182 try {
183 resolveVaultRelativePath(config.vault_path, pathArg);
184 } catch (e) {
185 exitWithError(e.message, 2, useJson);
186 }
187
188 let note;
189 try {
190 note = readNote(config.vault_path, pathArg);
191 } catch (e) {
192 exitWithError(e.message, 2, useJson);
193 }
194
195 try {
196 console.log(JSON.stringify(buildDocumentTree(note)));
197 } catch (e) {
198 exitWithError(e.message, 2, useJson);
199 }
200 process.exit(0);
201 }
202
203 function runGetMetadataFacets() {
204 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
205 if (!pathArg) {
206 exitWithError('knowtation get-metadata-facets: provide a note path.', 1, useJson);
207 }
208 if (!useJson) {
209 exitWithError('knowtation get-metadata-facets: --json is required.', 1, useJson);
210 }
211
212 let config;
213 try {
214 config = loadConfig();
215 } catch (e) {
216 exitWithError(e.message, 2, useJson);
217 }
218
219 try {
220 resolveVaultRelativePath(config.vault_path, pathArg);
221 } catch (e) {
222 exitWithError(e.message, 2, useJson);
223 }
224
225 let note;
226 try {
227 note = readNote(config.vault_path, pathArg);
228 } catch (e) {
229 exitWithError(e.message, 2, useJson);
230 }
231
232 try {
233 console.log(JSON.stringify(normalizeMetadataFacets(note.path, note.frontmatter)));
234 } catch (e) {
235 exitWithError(e.message, 2, useJson);
236 }
237 process.exit(0);
238 }
239
240 function runGetSectionSource() {
241 const pathArgs = args.filter((a, i) => i >= 1 && !a.startsWith('--'));
242 const pathArg = pathArgs[0];
243 if (!pathArg) {
244 exitWithError('knowtation get-section-source: provide a note path.', 1, useJson);
245 }
246 if (pathArgs.length > 1) {
247 exitWithError('knowtation get-section-source: provide exactly one note path.', 1, useJson);
248 }
249 if (!useJson) {
250 exitWithError('knowtation get-section-source: --json is required.', 1, useJson);
251 }
252
253 let config;
254 try {
255 config = loadConfig();
256 } catch (e) {
257 exitWithError(e.message, 2, useJson);
258 }
259
260 try {
261 console.log(JSON.stringify(readSectionSource(config.vault_path, pathArg)));
262 } catch (e) {
263 exitWithError(e.message, 2, useJson);
264 }
265 process.exit(0);
266 }
267
268 function runListNotes() {
269 const folder = getOpt('folder');
270 const project = getOpt('project');
271 const tag = getOpt('tag');
272 const since = getOpt('since');
273 const until = getOpt('until');
274 const chain = getOpt('chain');
275 const entity = getOpt('entity');
276 const episode = getOpt('episode');
277 let limit = getOpt('limit', 'number') ?? 20;
278 let offset = getOpt('offset', 'number') ?? 0;
279 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
280 exitWithError('knowtation list-notes: --limit must be between 0 and 100.', 1, useJson);
281 }
282 if (typeof offset === 'number' && offset < 0) {
283 exitWithError('knowtation list-notes: --offset must be non-negative.', 1, useJson);
284 }
285 limit = Math.min(100, Math.max(0, limit ?? 20));
286 offset = Math.max(0, offset ?? 0);
287 const order = getOpt('order') || 'date';
288 const fields = getOpt('fields') || 'path+metadata';
289 const countOnly = hasOpt('count-only');
290
291 let config;
292 try {
293 config = loadConfig();
294 } catch (e) {
295 exitWithError(e.message, 2, useJson);
296 }
297
298 const out = runListNotesOp(config, {
299 folder: folder ?? undefined,
300 project: project ?? undefined,
301 tag: tag ?? undefined,
302 since: since ?? undefined,
303 until: until ?? undefined,
304 chain: chain ?? undefined,
305 entity: entity ?? undefined,
306 episode: episode ?? undefined,
307 limit,
308 offset,
309 order,
310 fields,
311 countOnly,
312 });
313
314 if (countOnly) {
315 if (useJson) {
316 console.log(JSON.stringify({ total: out.total }));
317 } else {
318 console.log(out.total);
319 }
320 process.exit(0);
321 }
322
323 if (useJson) {
324 console.log(JSON.stringify({ notes: out.notes, total: out.total }));
325 } else {
326 for (const n of out.notes) {
327 const meta = [n.project, n.tags?.join?.(', ') ?? (n.tags || []).join(', '), n.date].filter(Boolean).join(' | ');
328 console.log(n.path + (meta ? ` ${meta}` : ''));
329 }
330 }
331 process.exit(0);
332 }
333
334 async function main() {
335 if (!subcommand || subcommand === '--help' || subcommand === '-h') {
336 console.log(help.trim());
337 process.exit(0);
338 }
339
340 if (subcommand === 'get-note') {
341 if (hasOpt('help') || hasOpt('h')) {
342 console.log('knowtation get-note <path>\n Options: --json, --body-only, --frontmatter-only');
343 process.exit(0);
344 }
345 runGetNote();
346 }
347
348 if (subcommand === 'get-note-outline') {
349 if (hasOpt('help') || hasOpt('h')) {
350 console.log('knowtation get-note-outline <path>\n Options: --json (required)');
351 process.exit(0);
352 }
353 runGetNoteOutline();
354 }
355
356 if (subcommand === 'get-document-tree') {
357 if (hasOpt('help') || hasOpt('h')) {
358 console.log('knowtation get-document-tree <path>\n Options: --json (required)');
359 process.exit(0);
360 }
361 runGetDocumentTree();
362 }
363
364 if (subcommand === 'get-metadata-facets') {
365 if (hasOpt('help') || hasOpt('h')) {
366 console.log('knowtation get-metadata-facets <path>\n Options: --json (required)');
367 process.exit(0);
368 }
369 runGetMetadataFacets();
370 }
371
372 if (subcommand === 'get-section-source') {
373 if (hasOpt('help') || hasOpt('h')) {
374 console.log('knowtation get-section-source <path>\n Options: --json (required)');
375 process.exit(0);
376 }
377 runGetSectionSource();
378 }
379
380 if (subcommand === 'list-notes') {
381 if (hasOpt('help') || hasOpt('h')) {
382 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');
383 process.exit(0);
384 }
385 runListNotes();
386 }
387
388 if (subcommand === 'search') {
389 if (hasOpt('help') || hasOpt('h')) {
390 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');
391 process.exit(0);
392 }
393 const query = args.slice(1).filter((a) => !a.startsWith('--')).join(' ').trim();
394 if (!query) {
395 exitWithError('knowtation search: provide a query string.', 1, useJson);
396 }
397 const folder = getOpt('folder');
398 const project = getOpt('project');
399 const tag = getOpt('tag');
400 const since = getOpt('since');
401 const until = getOpt('until');
402 const chain = getOpt('chain');
403 const entity = getOpt('entity');
404 const episode = getOpt('episode');
405 const order = getOpt('order');
406 let limit = getOpt('limit', 'number') ?? 10;
407 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
408 exitWithError('knowtation search: --limit must be between 0 and 100.', 1, useJson);
409 }
410 limit = Math.min(100, Math.max(0, limit ?? 10));
411 const fields = getOpt('fields') || 'path+snippet';
412 const snippetChars = getOpt('snippet-chars', 'number');
413 const countOnly = hasOpt('count-only');
414 const useKeyword = hasOpt('keyword');
415 const matchRaw = getOpt('match');
416 const contentScope = getOpt('content-scope');
417 const validFields = ['path', 'path+snippet', 'full'];
418 if (fields && !validFields.includes(fields)) {
419 exitWithError(`knowtation search: --fields must be one of ${validFields.join(', ')}.`, 1, useJson);
420 }
421 if (matchRaw && !useKeyword) {
422 exitWithError('knowtation search: --match is only valid with --keyword.', 1, useJson);
423 }
424 let match = 'phrase';
425 if (matchRaw) {
426 if (matchRaw === 'all-terms' || matchRaw === 'all_terms') match = 'all_terms';
427 else if (matchRaw === 'phrase') match = 'phrase';
428 else exitWithError('knowtation search: --match must be phrase or all-terms.', 1, useJson);
429 }
430 const validScopes = ['all', 'notes', 'approval_logs'];
431 if (contentScope && !validScopes.includes(contentScope)) {
432 exitWithError(`knowtation search: --content-scope must be one of ${validScopes.join(', ')}.`, 1, useJson);
433 }
434 (async () => {
435 try {
436 const config = loadConfig();
437 const baseOpts = {
438 folder: folder ?? undefined,
439 project: project ?? undefined,
440 tag: tag ?? undefined,
441 since: since ?? undefined,
442 until: until ?? undefined,
443 chain: chain ?? undefined,
444 entity: entity ?? undefined,
445 episode: episode ?? undefined,
446 order: order ?? undefined,
447 limit,
448 fields: fields || 'path+snippet',
449 snippetChars: snippetChars ?? 300,
450 countOnly,
451 content_scope: contentScope === 'all' ? undefined : contentScope ?? undefined,
452 };
453 let out;
454 if (useKeyword) {
455 const { runKeywordSearch } = await import('../lib/keyword-search.mjs');
456 out = await runKeywordSearch(query, { ...baseOpts, match }, config);
457 } else {
458 const { runSearch } = await import('../lib/search.mjs');
459 out = await runSearch(query, baseOpts, config);
460 }
461 if (config.memory?.enabled) {
462 try {
463 const { createMemoryManager } = await import('../lib/memory.mjs');
464 const mm = createMemoryManager(config);
465 if (mm.shouldCapture('search')) {
466 mm.store('search', {
467 query: out.query,
468 mode: useKeyword ? 'keyword' : 'semantic',
469 paths: (out.results || []).map((r) => r.path),
470 count: out.count ?? (out.results || []).length,
471 });
472 }
473 } catch (_) {}
474 }
475 if (useJson) {
476 console.log(JSON.stringify(out));
477 } else {
478 if (out.count !== undefined) {
479 console.log(out.count);
480 } else {
481 const list = out.results || [];
482 for (const r of list) {
483 const meta = [r.project, r.tags?.join(', ')].filter(Boolean).join(' | ');
484 const line = r.snippet != null ? `${r.path}\t${r.snippet}` : r.path;
485 console.log(line + (meta ? ` ${meta}` : ''));
486 }
487 }
488 }
489 process.exit(0);
490 } catch (e) {
491 exitWithError(e.message || String(e), 2, useJson);
492 }
493 })();
494 return;
495 }
496
497 if (subcommand === 'index') {
498 if (hasOpt('help') || hasOpt('h')) {
499 console.log('knowtation index\n Re-run indexer: vault → chunk → embed → vector store. Reads config; exit 0 on success, 2 on failure.');
500 process.exit(0);
501 }
502 const { runIndex } = await import('../lib/indexer.mjs');
503 try {
504 const t0 = Date.now();
505 const result = await runIndex();
506 const config = loadConfig();
507 if (config.memory?.enabled) {
508 try {
509 const { createMemoryManager } = await import('../lib/memory.mjs');
510 const mm = createMemoryManager(config);
511 if (mm.shouldCapture('index')) {
512 mm.store('index', {
513 notes_processed: result.notesProcessed,
514 chunks_indexed: result.chunksIndexed,
515 duration_ms: Date.now() - t0,
516 });
517 }
518 } catch (_) {}
519 }
520 if (useJson) {
521 console.log(JSON.stringify({ ok: true, notesProcessed: result.notesProcessed, chunksIndexed: result.chunksIndexed }));
522 }
523 process.exit(0);
524 } catch (e) {
525 exitWithError(e.message, 2, useJson);
526 }
527 }
528
529 if (subcommand === 'write') {
530 if (hasOpt('help') || hasOpt('h')) {
531 console.log('knowtation write <path> [content]\n Options: --stdin (body from stdin), --frontmatter k=v [k2=v2 ...], --append, --json');
532 process.exit(0);
533 }
534 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
535 if (!pathArg) {
536 exitWithError('knowtation write: provide a note path.', 1, useJson);
537 }
538 const stdin = hasOpt('stdin');
539 const append = hasOpt('append');
540 const frontmatterPairs = [];
541 for (let i = 0; i < args.length; i++) {
542 if (args[i] === '--frontmatter' && args[i + 1]) {
543 let j = i + 1;
544 while (j < args.length && !args[j].startsWith('--') && args[j].includes('=')) {
545 frontmatterPairs.push(args[j]);
546 j++;
547 }
548 break;
549 }
550 }
551 const frontmatterOverrides = {};
552 for (const p of frontmatterPairs) {
553 const eq = p.indexOf('=');
554 if (eq > 0) {
555 frontmatterOverrides[p.slice(0, eq).trim()] = p.slice(eq + 1).trim();
556 }
557 }
558 let body;
559 if (stdin) {
560 body = fs.readFileSync(0, 'utf8');
561 } else {
562 const contentArg = args[args.indexOf(pathArg) + 1];
563 body = contentArg && !contentArg.startsWith('--') ? contentArg : undefined;
564 }
565 let config;
566 try {
567 config = loadConfig();
568 } catch (e) {
569 exitWithError(e.message, 2, useJson);
570 }
571 (async () => {
572 try {
573 const { writeNote } = await import('../lib/write.mjs');
574 const result = await writeNote(config.vault_path, pathArg, {
575 body,
576 frontmatter: Object.keys(frontmatterOverrides).length ? frontmatterOverrides : undefined,
577 append,
578 config,
579 });
580 try {
581 const { maybeAutoSync } = await import('../lib/vault-git-sync.mjs');
582 maybeAutoSync(config);
583 } catch (_) {}
584 if (config.memory?.enabled) {
585 try {
586 const { createMemoryManager } = await import('../lib/memory.mjs');
587 const mm = createMemoryManager(config);
588 if (mm.shouldCapture('write')) {
589 mm.store('write', {
590 path: result.path,
591 action: append ? 'append' : 'create',
592 air_id: result.air_id || undefined,
593 });
594 }
595 } catch (_) {}
596 }
597 if (useJson) {
598 console.log(JSON.stringify(result));
599 } else {
600 console.log(`Written: ${result.path}`);
601 }
602 process.exit(0);
603 } catch (e) {
604 exitWithError(e.message, 2, useJson);
605 }
606 })();
607 return;
608 }
609
610 if (subcommand === 'export') {
611 if (hasOpt('help') || hasOpt('h')) {
612 console.log('knowtation export <path-or-query> <output-dir-or-file>\n Options: --format md|html, --project <slug>, --json');
613 process.exit(0);
614 }
615 const pathOrQuery = args[1];
616 const output = args[2];
617 if (!pathOrQuery || !output) {
618 exitWithError('knowtation export: provide <path-or-query> and <output-dir-or-file>.', 1, useJson);
619 }
620 const format = getOpt('format') || 'md';
621 const project = getOpt('project');
622 if (format && !['md', 'html'].includes(format)) {
623 exitWithError('knowtation export: --format must be md or html.', 1, useJson);
624 }
625 let config;
626 try {
627 config = loadConfig();
628 } catch (e) {
629 exitWithError(e.message, 2, useJson);
630 }
631 (async () => {
632 try {
633 const { exportNotes } = await import('../lib/export.mjs');
634 const { attestBeforeExport } = await import('../lib/air.mjs');
635 let paths = [];
636 const looksLikePath = !pathOrQuery.includes(' ') && (pathOrQuery.endsWith('.md') || pathOrQuery.includes('/'));
637 if (looksLikePath) {
638 try {
639 resolveVaultRelativePath(config.vault_path, pathOrQuery);
640 paths = [pathOrQuery];
641 } catch (_) {
642 // Fall through: treat as query
643 }
644 }
645 if (paths.length === 0) {
646 const { runSearch } = await import('../lib/search.mjs');
647 const result = await runSearch(pathOrQuery, {
648 limit: 50,
649 project: project ?? undefined,
650 fields: 'path',
651 });
652 paths = (result.results || []).map((r) => r.path).filter(Boolean);
653 }
654 if (!paths.length) {
655 exitWithError('knowtation export: no notes found for path or query.', 2, useJson);
656 }
657 if (config.air?.enabled) {
658 await attestBeforeExport(config, paths);
659 }
660 const result = exportNotes(config.vault_path, paths, output, { format });
661 if (config.memory?.enabled) {
662 try {
663 const { createMemoryManager } = await import('../lib/memory.mjs');
664 const mm = createMemoryManager(config);
665 if (mm.shouldCapture('export')) {
666 mm.store('export', { provenance: result.provenance, exported: result.exported, format });
667 }
668 } catch (_) {}
669 }
670 if (useJson) {
671 console.log(JSON.stringify({ exported: result.exported, provenance: result.provenance }));
672 } else {
673 for (const e of result.exported) {
674 console.log(`${e.path} → ${e.output}`);
675 }
676 if (result.provenance) console.log(result.provenance);
677 }
678 process.exit(0);
679 } catch (e) {
680 exitWithError(e.message, 2, useJson);
681 }
682 })();
683 return;
684 }
685
686 if (subcommand === 'import') {
687 if (hasOpt('help') || hasOpt('h')) {
688 console.log(
689 `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}`
690 );
691 process.exit(0);
692 }
693 const sourceType = args[1];
694 const input = args[2];
695 if (!sourceType) {
696 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
697 }
698 if (sourceType !== 'google-sheets' && !input) {
699 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
700 }
701 if (sourceType === 'google-sheets' && !input) {
702 exitWithError(
703 'knowtation import google-sheets: provide the spreadsheet id as <input> (the id from the Google Sheets URL).',
704 1,
705 useJson,
706 );
707 }
708 if (!IMPORT_SOURCE_TYPES.includes(sourceType)) {
709 exitWithError(`Unknown source-type "${sourceType}". Valid: ${IMPORT_SOURCE_TYPES_HELP}.`, 1, useJson);
710 }
711 (async () => {
712 try {
713 const config = loadConfig();
714 const { runImport } = await import('../lib/import.mjs');
715 const project = getOpt('project');
716 const outputDir = getOpt('output-dir');
717 const tagsOpt = getOpt('tags');
718 const tags = tagsOpt ? tagsOpt.split(',').map((t) => t.trim()).filter(Boolean) : [];
719 const dryRun = hasOpt('dry-run');
720 let memoryManager;
721 if (config.memory?.enabled && !dryRun) {
722 try {
723 const { createMemoryManager } = await import('../lib/memory.mjs');
724 memoryManager = createMemoryManager(config);
725 } catch (_) {}
726 }
727
728 const urlModeRaw = getOpt('url-mode');
729 let urlMode;
730 if (urlModeRaw) {
731 const v = String(urlModeRaw).trim().toLowerCase();
732 if (v !== 'auto' && v !== 'bookmark' && v !== 'extract') {
733 exitWithError(`Invalid --url-mode "${urlModeRaw}". Use auto, bookmark, or extract.`, 1, useJson);
734 }
735 urlMode = v;
736 }
737 if (urlModeRaw && sourceType !== 'url') {
738 exitWithError('--url-mode is only valid when source-type is url.', 1, useJson);
739 }
740 const sheetsRangeRaw = getOpt('sheets-range');
741 if (sheetsRangeRaw && sourceType !== 'google-sheets') {
742 exitWithError('--sheets-range is only valid when source-type is google-sheets.', 1, useJson);
743 }
744
745 const importOpts = {
746 project: project ?? undefined,
747 outputDir: outputDir ?? undefined,
748 tags,
749 dryRun,
750 ...(sourceType === 'url' && urlMode ? { urlMode } : {}),
751 ...(sourceType === 'google-sheets' && sheetsRangeRaw
752 ? { sheetsRange: String(sheetsRangeRaw).trim() }
753 : {}),
754 };
755 if (memoryManager && sourceType === 'mem0-export' && memoryManager.shouldCapture('capture')) {
756 importOpts.onMemoryEvent = (data) => {
757 try { memoryManager.store('capture', data); } catch (_) {}
758 };
759 }
760
761 const result = await runImport(sourceType, input, importOpts);
762 if (memoryManager) {
763 try {
764 if (memoryManager.shouldCapture('import')) {
765 memoryManager.store('import', {
766 source_type: sourceType,
767 count: result.count ?? 0,
768 paths: (result.imported || []).map((r) => r.path).slice(0, 50),
769 project: project ?? undefined,
770 });
771 }
772 } catch (_) {}
773 }
774 if (useJson) {
775 console.log(JSON.stringify({ imported: result.imported, count: result.count }));
776 } else {
777 for (const r of result.imported) {
778 console.log(r.path);
779 }
780 if (result.count === 0) {
781 console.log('No notes imported.');
782 } else {
783 console.log(`Imported ${result.count} note(s).`);
784 }
785 }
786 process.exit(0);
787 } catch (e) {
788 exitWithError(e.message, 2, useJson);
789 }
790 })();
791 return;
792 }
793
794 if (subcommand === 'mcp') {
795 if (hasOpt('help') || hasOpt('h')) {
796 console.log(
797 '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.'
798 );
799 process.exit(0);
800 }
801 const serverMod = await import('../mcp/server.mjs');
802 return;
803 }
804
805 if (subcommand === 'memory') {
806 const action = args[1];
807 if (hasOpt('help') || hasOpt('h')) {
808 console.log(`knowtation memory <action>
809 Actions:
810 query <key> Read latest value for an event type (e.g. search, export, write, import, index, propose, user).
811 list List recent memory events. --type, --topic, --since, --until, --limit (default 20), --json.
812 store <key> <value> Store a user-defined memory entry. Value is JSON string or --stdin.
813 search <query> Semantic search over memory (requires vector or mem0 provider). --limit, --json.
814 clear Clear memory. --type, --before <date>, --confirm required. --json.
815 export Export memory log. --format jsonl|mif, --since, --until, --type. Output to stdout.
816 stats Show memory statistics. --json.
817 index Print lightweight pointer index (markdown). --json returns structured object.
818 consolidate Run LLM-powered memory consolidation. --dry-run, --passes consolidate,verify,discover, --lookback-hours <n>. --json.
819
820 Options: --json`);
821 process.exit(0);
822 }
823 const validActions = ['query', 'list', 'store', 'search', 'clear', 'export', 'stats', 'index', 'consolidate'];
824 if (!action || !validActions.includes(action)) {
825 exitWithError(`knowtation memory: use "memory <action>". Actions: ${validActions.join(', ')}.`, 1, useJson);
826 }
827 let config;
828 try {
829 config = loadConfig();
830 } catch (e) {
831 exitWithError(e.message, 2, useJson);
832 }
833 if (!config.memory?.enabled) {
834 exitWithError('knowtation memory: memory layer not enabled. Set memory.enabled in config.', 2, useJson);
835 }
836 (async () => {
837 try {
838 const { createMemoryManager } = await import('../lib/memory.mjs');
839 const { MEMORY_EVENT_TYPES } = await import('../lib/memory-event.mjs');
840 const scopeOpt = getOpt('scope') === 'global' ? 'global' : undefined;
841 const mm = createMemoryManager(config, 'default', scopeOpt ? { scope: scopeOpt } : {});
842
843 if (action === 'query') {
844 const keyArg = args[2];
845 if (!keyArg) {
846 exitWithError('knowtation memory query: provide a key (event type).', 1, useJson);
847 }
848 const key = keyArg.replace(/\s+/g, '_');
849 const latest = mm.getLatest(key);
850 if (!latest) {
851 if (useJson) console.log(JSON.stringify({ key, value: null }));
852 else console.log('(no value)');
853 } else {
854 const { id: _id, vault_id: _vid, ...display } = latest;
855 if (useJson) console.log(JSON.stringify({ key, value: display }));
856 else console.log(JSON.stringify(display, null, 2));
857 }
858 process.exit(0);
859 }
860
861 if (action === 'list') {
862 const type = getOpt('type');
863 const topic = getOpt('topic');
864 const since = getOpt('since');
865 const until = getOpt('until');
866 const limit = getOpt('limit', 'number') ?? 20;
867 const events = mm.list({ type: type ?? undefined, topic: topic ?? undefined, since: since ?? undefined, until: until ?? undefined, limit });
868 if (useJson) {
869 console.log(JSON.stringify({ events, count: events.length }));
870 } else {
871 if (events.length === 0) console.log('(no events)');
872 for (const e of events) {
873 const summary = JSON.stringify(e.data).slice(0, 120);
874 console.log(`${e.ts} ${e.type} ${summary}`);
875 }
876 }
877 process.exit(0);
878 }
879
880 if (action === 'store') {
881 const keyArg = args[2];
882 if (!keyArg) {
883 exitWithError('knowtation memory store: provide a key.', 1, useJson);
884 }
885 let valueRaw;
886 if (hasOpt('stdin')) {
887 valueRaw = fs.readFileSync(0, 'utf8').trim();
888 } else {
889 valueRaw = args[3];
890 }
891 if (!valueRaw) {
892 exitWithError('knowtation memory store: provide a value (JSON string) or --stdin.', 1, useJson);
893 }
894 let value;
895 try {
896 value = JSON.parse(valueRaw);
897 } catch (_) {
898 value = { text: valueRaw };
899 }
900 const result = mm.store('user', { key: keyArg, ...value });
901 if (useJson) console.log(JSON.stringify(result));
902 else console.log(`Stored: ${result.id}`);
903 process.exit(0);
904 }
905
906 if (action === 'search') {
907 const query = args.slice(2).filter((a) => !a.startsWith('--')).join(' ').trim();
908 if (!query) {
909 exitWithError('knowtation memory search: provide a query string.', 1, useJson);
910 }
911 if (!mm.supportsSearch()) {
912 exitWithError('knowtation memory search: semantic search requires memory.provider: vector or mem0.', 2, useJson);
913 }
914 const limit = getOpt('limit', 'number') ?? 10;
915 const results = mm.search(query, { limit });
916 if (useJson) {
917 console.log(JSON.stringify({ results, count: results.length }));
918 } else {
919 if (results.length === 0) console.log('(no results)');
920 for (const r of results) {
921 console.log(`${r.ts} ${r.type} ${JSON.stringify(r.data).slice(0, 120)}`);
922 }
923 }
924 process.exit(0);
925 }
926
927 if (action === 'clear') {
928 if (!hasOpt('confirm')) {
929 exitWithError('knowtation memory clear: use --confirm to confirm deletion.', 1, useJson);
930 }
931 const type = getOpt('type');
932 const before = getOpt('before');
933 const result = mm.clear({ type: type ?? undefined, before: before ?? undefined });
934 if (useJson) console.log(JSON.stringify(result));
935 else console.log(`Cleared ${result.cleared} event(s).`);
936 process.exit(0);
937 }
938
939 if (action === 'export') {
940 const format = getOpt('format') || 'jsonl';
941 if (!['jsonl', 'mif'].includes(format)) {
942 exitWithError('knowtation memory export: --format must be jsonl or mif.', 1, useJson);
943 }
944 const type = getOpt('type');
945 const since = getOpt('since');
946 const until = getOpt('until');
947 const events = mm.list({ type: type ?? undefined, since: since ?? undefined, until: until ?? undefined, limit: 10000 });
948 if (format === 'jsonl') {
949 for (const e of events) {
950 console.log(JSON.stringify(e));
951 }
952 } else {
953 for (const e of events) {
954 console.log(`---`);
955 console.log(`id: ${e.id}`);
956 console.log(`type: ${e.type}`);
957 console.log(`ts: ${e.ts}`);
958 console.log(`vault_id: ${e.vault_id}`);
959 console.log(`---`);
960 console.log(JSON.stringify(e.data, null, 2));
961 console.log('');
962 }
963 }
964 process.exit(0);
965 }
966
967 if (action === 'summarize') {
968 const since = getOpt('since') || new Date(Date.now() - 86_400_000).toISOString();
969 const maxTokens = getOpt('max-tokens', 'number') ?? 512;
970 const dryRun = hasOpt('dry-run');
971 try {
972 const { generateSessionSummary } = await import('../lib/memory-session-summary.mjs');
973 const result = await generateSessionSummary(config, { since, maxTokens, dryRun });
974 if (useJson) {
975 console.log(JSON.stringify(result));
976 } else {
977 console.log(result.summary);
978 if (result.id) console.log(`\nStored as: ${result.id}`);
979 console.log(`Events summarized: ${result.event_count}`);
980 }
981 } catch (e) {
982 exitWithError(`Session summary failed: ${e.message}`, 2, useJson);
983 }
984 process.exit(0);
985 }
986
987 if (action === 'consolidate') {
988 const dryRun = hasOpt('dry-run');
989 const passesRaw = getOpt('passes', 'string');
990 const passes = passesRaw
991 ? passesRaw.split(',').map((s) => s.trim()).filter(Boolean)
992 : undefined;
993 const lookbackHours = getOpt('lookback-hours', 'number') ?? undefined;
994 try {
995 const { consolidateMemory } = await import('../lib/memory-consolidate.mjs');
996 const result = await consolidateMemory(config, { dryRun, passes, lookbackHours });
997 if (useJson) {
998 console.log(JSON.stringify(result));
999 } else if (result.dry_run) {
1000 console.log(`[dry-run] Would process ${result.total_events} events across ${result.topics.length} topics.`);
1001 for (const t of result.topics) {
1002 console.log(`[dry-run] Topic "${t.topic}": ${t.event_count} events → ${t.dry_run_estimate || 'estimated facts'}`);
1003 }
1004 if (result.verify) {
1005 console.log(`[dry-run] Verify pass: would check paths in events (no writes).`);
1006 }
1007 if (result.discover) {
1008 console.log(`[dry-run] Discover pass: would analyze ${result.discover.topic_count} topic(s) for cross-topic insights (no writes).`);
1009 }
1010 } else if (result.topics.length === 0 && !result.verify && !result.discover) {
1011 console.log('No events to consolidate.');
1012 } else {
1013 if (result.topics.length > 0) {
1014 console.log(`Consolidated ${result.total_events} events across ${result.topics.length} topics.`);
1015 for (const t of result.topics) {
1016 if (t.error) {
1017 console.log(` ${t.topic}: error — ${t.error}`);
1018 } else {
1019 console.log(` ${t.topic}: ${t.facts.length} facts written${t.id ? ` (${t.id})` : ''}`);
1020 }
1021 }
1022 console.log('Index regenerated.');
1023 }
1024 if (result.verify) {
1025 const v = result.verify;
1026 console.log(`Verify pass: checked ${v.checked_count} events — ${v.verified_paths.length} verified, ${v.stale_paths.length} stale.`);
1027 if (v.stale_paths.length > 0) {
1028 for (const p of v.stale_paths) console.log(` stale: ${p}`);
1029 }
1030 }
1031 if (result.discover) {
1032 const d = result.discover;
1033 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).`);
1034 }
1035 }
1036 } catch (e) {
1037 exitWithError(`Consolidation failed: ${e.message}`, 2, useJson);
1038 }
1039 process.exit(0);
1040 }
1041
1042 if (action === 'index') {
1043 const idx = mm.generateIndex({ force: true });
1044 if (useJson) {
1045 console.log(JSON.stringify(idx));
1046 } else {
1047 console.log(idx.markdown);
1048 }
1049 process.exit(0);
1050 }
1051
1052 if (action === 'stats') {
1053 const stats = mm.stats();
1054 if (useJson) {
1055 console.log(JSON.stringify(stats));
1056 } else {
1057 console.log(`Total events: ${stats.total}`);
1058 console.log(`Storage: ${stats.size_bytes} bytes`);
1059 if (stats.oldest) console.log(`Oldest: ${stats.oldest}`);
1060 if (stats.newest) console.log(`Newest: ${stats.newest}`);
1061 if (Object.keys(stats.counts_by_type).length > 0) {
1062 console.log('Counts by type:');
1063 for (const [t, c] of Object.entries(stats.counts_by_type)) {
1064 console.log(` ${t}: ${c}`);
1065 }
1066 }
1067 }
1068 process.exit(0);
1069 }
1070 } catch (e) {
1071 exitWithError(e.message, 2, useJson);
1072 }
1073 })();
1074 return;
1075 }
1076
1077 if (subcommand === 'doctor') {
1078 if (hasOpt('help') || hasOpt('h')) {
1079 console.log(
1080 '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).'
1081 );
1082 process.exit(0);
1083 }
1084 const hubUrlOpt = getOpt('hub');
1085 const { runDoctor } = await import('./doctor.mjs');
1086 const code = await runDoctor({ useJson, hubUrlOpt });
1087 process.exit(code);
1088 }
1089
1090 if (subcommand === 'hub') {
1091 const action = args[1];
1092 if (action !== 'status') {
1093 exitWithError('knowtation hub: use "hub status". Option: --hub <url>.', 1, useJson);
1094 }
1095 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL || 'http://localhost:3333';
1096 const base = hubUrl.replace(/\/$/, '');
1097 (async () => {
1098 try {
1099 const res = await fetch(base + '/health', { method: 'GET' });
1100 const data = await res.json().catch(() => ({}));
1101 if (useJson) {
1102 console.log(JSON.stringify({ ok: res.ok, status: res.status, url: base }));
1103 } else {
1104 console.log(res.ok ? `Hub at ${base} is up.` : `Hub at ${base} returned ${res.status}.`);
1105 }
1106 process.exit(res.ok ? 0 : 2);
1107 } catch (e) {
1108 exitWithError('Hub unreachable: ' + e.message, 2, useJson);
1109 }
1110 })();
1111 return;
1112 }
1113
1114 if (subcommand === 'auth') {
1115 const authArgs = args.slice(1);
1116 (async () => {
1117 try {
1118 const { runAuthCli } = await import('../hub/lib/local-auth-cli.mjs');
1119 const code = await runAuthCli(authArgs);
1120 process.exit(code);
1121 } catch (e) {
1122 exitWithError('knowtation auth: ' + (e.message || String(e)), 1, useJson);
1123 }
1124 })();
1125 return;
1126 }
1127
1128 if (subcommand === 'vault') {
1129 const vaultSub = args[1];
1130 if (vaultSub === 'sync') {
1131 if (hasOpt('help') || hasOpt('h')) {
1132 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>.');
1133 process.exit(0);
1134 }
1135 let config;
1136 try {
1137 config = loadConfig();
1138 } catch (e) {
1139 exitWithError(e.message, 2, useJson);
1140 }
1141 (async () => {
1142 try {
1143 const { runVaultSync } = await import('../lib/vault-git-sync.mjs');
1144 const result = runVaultSync(config);
1145 if (useJson) console.log(JSON.stringify(result));
1146 else console.log(result.message === 'Synced' ? 'Vault synced to remote.' : result.message);
1147 process.exit(0);
1148 } catch (e) {
1149 exitWithError('knowtation vault sync: ' + (e.message || 'git failed'), 1, useJson);
1150 }
1151 })();
1152 return;
1153 }
1154 exitWithError('knowtation vault: unknown subcommand. Use vault sync.', 1, useJson);
1155 }
1156
1157 if (subcommand === 'propose') {
1158 const pathArg = args[1];
1159 if (!pathArg || pathArg.startsWith('--')) {
1160 exitWithError('knowtation propose: provide a vault-relative note path (e.g. inbox/note.md).', 1, useJson);
1161 }
1162 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL;
1163 if (!hubUrl) {
1164 exitWithError('knowtation propose: set --hub <url> or KNOWTATION_HUB_URL.', 1, useJson);
1165 }
1166 let config;
1167 try {
1168 config = loadConfig();
1169 } catch (e) {
1170 exitWithError(e.message, 2, useJson);
1171 }
1172 try {
1173 resolveVaultRelativePath(config.vault_path, pathArg);
1174 } catch (e) {
1175 exitWithError(e.message, 2, useJson);
1176 }
1177 const intent = getOpt('intent') || '';
1178 const token = process.env.KNOWTATION_HUB_TOKEN;
1179 if (!token) {
1180 exitWithError('knowtation propose: set KNOWTATION_HUB_TOKEN (JWT from Hub login).', 2, useJson);
1181 }
1182 const base = hubUrl.replace(/\/$/, '');
1183 const vaultHdr = getOpt('vault') || process.env.KNOWTATION_HUB_VAULT_ID;
1184 const labelsRaw = getOpt('labels');
1185 const labels = labelsRaw
1186 ? labelsRaw
1187 .split(',')
1188 .map((s) => s.trim())
1189 .filter(Boolean)
1190 : undefined;
1191 const source = getOpt('source') || undefined;
1192 const externalRef = getOpt('external-ref') || undefined;
1193 const baseStateOverride = getOpt('base-state-id');
1194 const skipFetchBase = hasOpt('no-fetch-base');
1195
1196 let bodyText = '';
1197 let frontmatter = {};
1198 if (noteFileExistsInVault(config.vault_path, pathArg)) {
1199 const n = readNote(config.vault_path, pathArg);
1200 bodyText = n.body;
1201 frontmatter = n.frontmatter;
1202 }
1203
1204 (async () => {
1205 try {
1206 const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token };
1207 if (vaultHdr) headers['X-Vault-Id'] = vaultHdr;
1208
1209 let baseStateId = baseStateOverride && String(baseStateOverride).trim() ? String(baseStateOverride).trim() : '';
1210 if (!baseStateId && !skipFetchBase) {
1211 const encPath = pathArg.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/');
1212 const gres = await fetch(`${base}/api/v1/notes/${encPath}`, { method: 'GET', headers });
1213 if (gres.status === 404) {
1214 baseStateId = absentNoteStateId();
1215 } else if (gres.ok) {
1216 const noteJson = await gres.json();
1217 baseStateId = noteStateIdFromHubNoteJson(noteJson);
1218 }
1219 }
1220
1221 const payload = {
1222 path: pathArg.replace(/\\/g, '/'),
1223 body: bodyText,
1224 frontmatter,
1225 intent: intent || undefined,
1226 external_ref: externalRef || undefined,
1227 labels,
1228 source,
1229 };
1230 if (baseStateId) payload.base_state_id = baseStateId;
1231
1232 const res = await fetch(base + '/api/v1/proposals', {
1233 method: 'POST',
1234 headers,
1235 body: JSON.stringify(payload),
1236 });
1237 const data = await res.json().catch(() => ({}));
1238 if (!res.ok) {
1239 exitWithError(data.error || res.statusText, 2, useJson);
1240 return;
1241 }
1242 try {
1243 const config2 = loadConfig();
1244 if (config2.memory?.enabled) {
1245 const { createMemoryManager } = await import('../lib/memory.mjs');
1246 const mm = createMemoryManager(config2);
1247 if (mm.shouldCapture('propose')) {
1248 mm.store('propose', {
1249 proposal_id: data.proposal_id,
1250 path: data.path || pathArg,
1251 intent: intent || undefined,
1252 base_state_id: baseStateId || undefined,
1253 });
1254 }
1255 }
1256 } catch (_) {}
1257 if (useJson) console.log(JSON.stringify(data));
1258 else console.log('Proposal created:', data.proposal_id, data.path);
1259 process.exit(0);
1260 } catch (e) {
1261 exitWithError(e.message, 2, useJson);
1262 }
1263 })();
1264 return;
1265 }
1266
1267 if (subcommand === 'flow') {
1268 const flowAction = args[1];
1269 if (hasOpt('help') || hasOpt('h') || !flowAction) {
1270 console.log(`knowtation flow <action>
1271 Actions (v0 read surface):
1272 list [--scope personal|project|org] [--tag <t>] [--limit <n>] [--json]
1273 get <flow_id> [--version <semver>] [--json]
1274 project <flow_id> --harness <harness> [--version <semver>] [--out <path>] [--check] [--json]
1275
1276 Authoring (gated by FLOW_AUTHORING_WRITES; default off):
1277 propose <bundle.json> [--intent <text>] [--base-version <semver>] [--base-state-id <flowst1_…>] [--json]
1278 import <bundle.json> [--intent <text>] [--external-ref <ref>] [--source-vault-hint <hint>] [--json]
1279
1280 External-agent grants (gated by FLOW_EXTERNAL_AGENT_ENABLED; default off):
1281 grant mint <flow_id> --flow-version <semver> --tools <id>[,<id>…] [--ttl-seconds <n>] [--actor-label <label>] [--json]
1282 grant revoke <grant_id> [--json]
1283 grant list [--flow-id <id>] [--json]
1284
1285 Run execution (gated by FLOW_RUN_WRITES_ENABLED / FLOW_AUTOMATABLE_EXECUTION_ENABLED; default off):
1286 run start <flow_id> --flow-version <semver> [--task-ref <id>] [--external-ref <ref>] [--json]
1287 run get <run_id> [--json]
1288 run list [--flow-id <id>] [--json]
1289 run advance <run_id> --step-id <id> --to-status <status> [--skip-reason <enum>] [--json]
1290 run evidence <run_id> --step-id <id> --evidence-ref <ref> --pointer-kind <kind> [--json]
1291 run execute <run_id> --step-id <id> --consent-id <id> [--model-lane <lane>] [--dry-run] [--json]
1292 run consent <run_id> --lanes <lane>[,<lane>…] --cost-cap <n> [--ttl-seconds <n>] [--json]
1293 run submit-review <run_id> --intent <text> [--json]
1294
1295 Capture flywheel (gated by FLOW_CAPTURE_DETECTION_ENABLED / FLOW_CAPTURE_WRITES_ENABLED; default off):
1296 capture observe <signals.json> [--include-low-confidence] [--json]
1297 capture list [--scope personal|project|org] [--include-low-confidence] [--limit <n>] [--json]
1298 capture propose <candidate_id> --confirmed-scope <scope> --intent <text> [--scope-widen-acknowledged] [--allow-low-confidence] [--force-new-flow] [--merge-into-flow-id <id>] [--json]
1299 capture dismiss <candidate_id> --intent <text> [--json]
1300
1301 Reserved (not wired in v0): export
1302
1303 Options: --json (exact Hub JSON)`);
1304 process.exit(0);
1305 }
1306
1307 const gatedActions = ['export'];
1308 if (gatedActions.includes(flowAction)) {
1309 exitWithError(`knowtation flow ${flowAction}: not available in v0 (gated).`, 1, useJson);
1310 }
1311
1312 let config;
1313 try {
1314 config = loadConfig();
1315 } catch (e) {
1316 exitWithError(e.message, 2, useJson);
1317 }
1318
1319 const vaultId = getOpt('vault') || 'default';
1320 const cliScopes = Array.isArray(config.flow?.visible_scopes) ? config.flow.visible_scopes : undefined;
1321
1322 const flowExitWithError = (message, codeStr, exitCode = 1) => {
1323 if (useJson) {
1324 process.stderr.write(JSON.stringify({ error: message, code: codeStr }) + '\n');
1325 } else {
1326 console.error(message);
1327 }
1328 process.exit(exitCode);
1329 };
1330
1331 if (flowAction === 'list') {
1332 const limitOpt = getOpt('limit', 'number');
1333 const { handleFlowListRequest } = await import('../lib/flow/flow-handlers.mjs');
1334 const result = handleFlowListRequest({
1335 dataDir: config.data_dir,
1336 vaultId,
1337 cliScopes,
1338 scope: getOpt('scope') ?? undefined,
1339 tag: getOpt('tag') ?? undefined,
1340 limit: limitOpt ?? undefined,
1341 });
1342 if (!result.ok) {
1343 flowExitWithError(result.error, result.code);
1344 }
1345 if (useJson) {
1346 console.log(JSON.stringify(result.payload));
1347 } else {
1348 const rows = result.payload.flows;
1349 if (rows.length === 0) {
1350 console.log('(no flows)');
1351 } else {
1352 for (const f of rows) {
1353 console.log(`${f.flow_id} ${f.version} [${f.scope}] ${f.title} (${f.step_count} steps)`);
1354 }
1355 }
1356 if (result.payload.truncated) {
1357 console.log('(truncated)');
1358 }
1359 }
1360 process.exit(0);
1361 }
1362
1363 if (flowAction === 'get') {
1364 const flowId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1365 if (!flowId) {
1366 flowExitWithError('knowtation flow get: provide a flow_id.', 'BAD_REQUEST');
1367 }
1368 const { handleFlowGetRequest } = await import('../lib/flow/flow-handlers.mjs');
1369 const result = handleFlowGetRequest({
1370 dataDir: config.data_dir,
1371 vaultId,
1372 flowId,
1373 cliScopes,
1374 version: getOpt('version') ?? undefined,
1375 });
1376 if (!result.ok) {
1377 flowExitWithError(result.error, result.code);
1378 }
1379 if (useJson) {
1380 console.log(JSON.stringify(result.payload));
1381 } else {
1382 const { flow, steps } = result.payload;
1383 console.log(`${flow.flow_id} ${flow.version} [${flow.scope}]`);
1384 console.log(flow.title);
1385 console.log(flow.summary);
1386 console.log(`Steps (${steps.length}):`);
1387 for (const s of steps) {
1388 console.log(` ${s.ordinal}. ${s.owned_job}`);
1389 }
1390 }
1391 process.exit(0);
1392 }
1393
1394 if (flowAction === 'project') {
1395 const flowId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1396 const harness = getOpt('harness');
1397 if (!flowId) {
1398 flowExitWithError('knowtation flow project: provide a flow_id.', 'BAD_REQUEST');
1399 }
1400 if (!harness) {
1401 flowExitWithError('knowtation flow project: --harness is required.', 'BAD_REQUEST');
1402 }
1403 const checkMode = hasOpt('check');
1404 const outPath = getOpt('out') ?? undefined;
1405 const { handleFlowProjectRequest } = await import('../lib/flow/flow-handlers.mjs');
1406 const { detectDrift, defaultProjectionOutPath } = await import('../lib/flow/projection-generator.mjs');
1407 const result = handleFlowProjectRequest({
1408 dataDir: config.data_dir,
1409 vaultId,
1410 flowId,
1411 harness,
1412 cliScopes,
1413 version: getOpt('version') ?? undefined,
1414 });
1415 if (!result.ok) {
1416 flowExitWithError(result.error, result.code, result.status === 403 ? 1 : 1);
1417 }
1418
1419 const artifactPath = outPath || defaultProjectionOutPath(flowId, harness);
1420 if (checkMode) {
1421 if (!artifactPath) {
1422 flowExitWithError('knowtation flow project --check: provide --out or use an active harness.', 'BAD_REQUEST');
1423 }
1424 const fs = await import('node:fs');
1425 let onDisk = '';
1426 try {
1427 onDisk = fs.readFileSync(artifactPath, 'utf8');
1428 } catch {
1429 onDisk = '';
1430 }
1431 const drift = detectDrift(onDisk, result.payload.projection.rendered);
1432 const stale = result.payload.staleness.stale === true;
1433 if (useJson) {
1434 console.log(
1435 JSON.stringify({
1436 check: true,
1437 drift,
1438 stale,
1439 staleness: result.payload.staleness,
1440 generator: result.payload.generator,
1441 }),
1442 );
1443 } else {
1444 console.log(`drift: ${drift.drift} (${drift.reason})`);
1445 console.log(`stale: ${stale}`);
1446 if (stale) {
1447 console.log(
1448 `versions: projection ${result.payload.staleness.projection_version} < latest ${result.payload.staleness.latest_version}`,
1449 );
1450 }
1451 }
1452 if (drift.drift || stale) {
1453 process.exit(1);
1454 }
1455 process.exit(0);
1456 }
1457
1458 if (outPath && artifactPath) {
1459 const fs = await import('node:fs');
1460 const pathMod = await import('node:path');
1461 const dir = pathMod.dirname(artifactPath);
1462 if (!fs.existsSync(dir)) {
1463 fs.mkdirSync(dir, { recursive: true });
1464 }
1465 fs.writeFileSync(artifactPath, result.payload.projection.rendered, 'utf8');
1466 }
1467
1468 if (useJson) {
1469 console.log(JSON.stringify(result.payload));
1470 } else {
1471 console.log(result.payload.projection.rendered);
1472 const { staleness, projection } = result.payload;
1473 console.error('');
1474 console.error(`staleness: ${staleness.stale ? 'stale' : 'fresh'} (${staleness.projection_version} vs latest ${staleness.latest_version})`);
1475 if (projection.fidelity?.dropped_fields?.length) {
1476 console.error(`dropped fields: ${projection.fidelity.dropped_fields.join(', ')}`);
1477 }
1478 if (projection.fidelity?.notes) {
1479 console.error(`fidelity: ${projection.fidelity.notes}`);
1480 }
1481 if (outPath) {
1482 console.error(`wrote: ${artifactPath}`);
1483 }
1484 }
1485 process.exit(0);
1486 }
1487
1488 if (flowAction === 'propose' || flowAction === 'import') {
1489 const bundlePath = args.find((a, i) => i >= 2 && !a.startsWith('--'));
1490 if (!bundlePath) {
1491 flowExitWithError(`knowtation flow ${flowAction}: provide a bundle.json path.`, 'BAD_REQUEST');
1492 }
1493 const fsMod = await import('node:fs');
1494 let bundle;
1495 try {
1496 bundle = JSON.parse(fsMod.readFileSync(bundlePath, 'utf8'));
1497 } catch (e) {
1498 flowExitWithError(`knowtation flow ${flowAction}: cannot read bundle (${e.message}).`, 'BAD_REQUEST');
1499 }
1500 const intent = getOpt('intent') || (bundle && typeof bundle === 'object' ? bundle.intent : undefined);
1501 const { handleFlowProposeRequest } = await import('../lib/flow/flow-authoring.mjs');
1502 const { createProposal } = await import('../hub/proposals-store.mjs');
1503
1504 let result;
1505 if (flowAction === 'import') {
1506 result = handleFlowProposeRequest({
1507 dataDir: config.data_dir,
1508 vaultId,
1509 cliScopes,
1510 kind: 'import',
1511 bundle: { flow: bundle?.flow, steps: bundle?.steps },
1512 intent,
1513 externalRef: getOpt('external-ref') || (bundle && bundle.external_ref) || undefined,
1514 sourceVaultHint: getOpt('source-vault-hint') || (bundle && bundle.source_vault_hint) || undefined,
1515 createProposal,
1516 });
1517 } else {
1518 const baseVersion = getOpt('base-version') || (bundle && bundle.base_version) || undefined;
1519 const baseStateId = getOpt('base-state-id') || (bundle && bundle.base_state_id) || undefined;
1520 result = handleFlowProposeRequest({
1521 dataDir: config.data_dir,
1522 vaultId,
1523 cliScopes,
1524 kind: baseVersion ? 'edit' : 'new',
1525 flow: bundle?.flow,
1526 steps: bundle?.steps,
1527 intent,
1528 flowId: bundle?.flow?.flow_id,
1529 baseVersion,
1530 baseStateId,
1531 createProposal,
1532 });
1533 }
1534
1535 if (!result.ok) {
1536 flowExitWithError(result.error, result.code);
1537 }
1538 if (useJson) {
1539 console.log(JSON.stringify(result.payload));
1540 } else {
1541 const p = result.payload;
1542 console.log(`proposed ${p.flow_id} → ${p.proposal_id} [${p.scope}] (status: ${p.status})`);
1543 console.log(`review queue: ${p.review_queue} auto_approvable: ${p.auto_approvable}`);
1544 }
1545 process.exit(0);
1546 }
1547
1548 if (flowAction === 'run') {
1549 const runSub = args[2];
1550 if (!runSub || hasOpt('help') || hasOpt('h')) {
1551 console.log('knowtation flow run start|get|list|advance|evidence|execute|consent|submit-review — see knowtation flow --help');
1552 process.exit(0);
1553 }
1554
1555 const {
1556 handleFlowRunStartRequest,
1557 handleFlowRunGetRequest,
1558 handleFlowRunListRequest,
1559 handleFlowRunAdvanceRequest,
1560 handleFlowRunEvidenceRequest,
1561 handleFlowRunExecuteAutomatableRequest,
1562 handleFlowRunSubmitReviewRequest,
1563 handleFlowExecutionConsentMintRequest,
1564 } = await import('../lib/flow/flow-execution.mjs');
1565 const { createProposal } = await import('../hub/proposals-store.mjs');
1566
1567 if (runSub === 'start') {
1568 const flowId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1569 const flowVersion = getOpt('flow-version');
1570 if (!flowId || !flowVersion) {
1571 flowExitWithError('knowtation flow run start: provide flow_id and --flow-version.', 'BAD_REQUEST');
1572 }
1573 const result = handleFlowRunStartRequest({
1574 dataDir: config.data_dir,
1575 vaultId,
1576 cliScopes,
1577 flowId,
1578 flowVersion,
1579 taskRef: getOpt('task-ref') ?? undefined,
1580 externalRef: getOpt('external-ref') ?? undefined,
1581 harness: 'cli',
1582 });
1583 if (!result.ok) {
1584 flowExitWithError(result.error, result.code);
1585 }
1586 if (useJson) {
1587 console.log(JSON.stringify(result.payload));
1588 } else {
1589 console.log(`started run ${result.payload.run.run_id} for ${flowId}@${flowVersion}`);
1590 }
1591 process.exit(0);
1592 }
1593
1594 if (runSub === 'get') {
1595 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1596 if (!runId) {
1597 flowExitWithError('knowtation flow run get: provide run_id.', 'BAD_REQUEST');
1598 }
1599 const result = handleFlowRunGetRequest({
1600 dataDir: config.data_dir,
1601 vaultId,
1602 cliScopes,
1603 runId,
1604 });
1605 if (!result.ok) {
1606 flowExitWithError(result.error, result.code);
1607 }
1608 if (useJson) {
1609 console.log(JSON.stringify(result.payload));
1610 } else {
1611 console.log(JSON.stringify(result.payload.run, null, 2));
1612 }
1613 process.exit(0);
1614 }
1615
1616 if (runSub === 'list') {
1617 const result = handleFlowRunListRequest({
1618 dataDir: config.data_dir,
1619 vaultId,
1620 cliScopes,
1621 flowId: getOpt('flow-id') ?? undefined,
1622 });
1623 if (!result.ok) {
1624 flowExitWithError(result.error, result.code);
1625 }
1626 if (useJson) {
1627 console.log(JSON.stringify(result.payload));
1628 } else {
1629 for (const run of result.payload.runs) {
1630 console.log(`${run.run_id} ${run.flow_id}@${run.flow_version} [${run.status}]`);
1631 }
1632 }
1633 process.exit(0);
1634 }
1635
1636 if (runSub === 'advance') {
1637 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1638 const stepId = getOpt('step-id');
1639 const toStatus = getOpt('to-status');
1640 if (!runId || !stepId || !toStatus) {
1641 flowExitWithError(
1642 'knowtation flow run advance: provide run_id, --step-id, and --to-status.',
1643 'BAD_REQUEST',
1644 );
1645 }
1646 const result = handleFlowRunAdvanceRequest({
1647 dataDir: config.data_dir,
1648 vaultId,
1649 cliScopes,
1650 runId,
1651 stepId,
1652 toStatus,
1653 skipReason: getOpt('skip-reason') ?? undefined,
1654 });
1655 if (!result.ok) {
1656 flowExitWithError(result.error, result.code);
1657 }
1658 if (useJson) {
1659 console.log(JSON.stringify(result.payload));
1660 } else {
1661 console.log(`advanced ${stepId} → ${toStatus}`);
1662 }
1663 process.exit(0);
1664 }
1665
1666 if (runSub === 'evidence') {
1667 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1668 const stepId = getOpt('step-id');
1669 const evidenceRef = getOpt('evidence-ref');
1670 const pointerKind = getOpt('pointer-kind');
1671 if (!runId || !stepId || !evidenceRef || !pointerKind) {
1672 flowExitWithError(
1673 'knowtation flow run evidence: provide run_id, --step-id, --evidence-ref, --pointer-kind.',
1674 'BAD_REQUEST',
1675 );
1676 }
1677 const result = handleFlowRunEvidenceRequest({
1678 dataDir: config.data_dir,
1679 vaultId,
1680 cliScopes,
1681 runId,
1682 stepId,
1683 evidenceRef,
1684 pointerKind,
1685 });
1686 if (!result.ok) {
1687 flowExitWithError(result.error, result.code);
1688 }
1689 if (useJson) {
1690 console.log(JSON.stringify(result.payload));
1691 } else {
1692 console.log(`evidence recorded on ${stepId}`);
1693 }
1694 process.exit(0);
1695 }
1696
1697 if (runSub === 'execute') {
1698 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1699 const stepId = getOpt('step-id');
1700 const consentId = getOpt('consent-id');
1701 if (!runId || !stepId || !consentId) {
1702 flowExitWithError(
1703 'knowtation flow run execute: provide run_id, --step-id, and --consent-id.',
1704 'BAD_REQUEST',
1705 );
1706 }
1707 const result = handleFlowRunExecuteAutomatableRequest({
1708 dataDir: config.data_dir,
1709 vaultId,
1710 cliScopes,
1711 runId,
1712 stepId,
1713 consentId,
1714 modelLane: getOpt('model-lane') ?? undefined,
1715 dryRun: hasOpt('dry-run'),
1716 });
1717 if (!result.ok) {
1718 flowExitWithError(result.error, result.code);
1719 }
1720 if (useJson) {
1721 console.log(JSON.stringify(result.payload));
1722 } else {
1723 console.log(`execution ${result.payload.execution.execution_id} → ${result.payload.execution.status}`);
1724 }
1725 process.exit(0);
1726 }
1727
1728 if (runSub === 'consent') {
1729 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1730 const lanesRaw = getOpt('lanes');
1731 const costCap = getOpt('cost-cap', 'number');
1732 if (!runId || !lanesRaw || costCap === undefined) {
1733 flowExitWithError(
1734 'knowtation flow run consent: provide run_id, --lanes, and --cost-cap.',
1735 'BAD_REQUEST',
1736 );
1737 }
1738 const allowedLanes = lanesRaw.split(',').map((l) => l.trim()).filter(Boolean);
1739 const ttlRaw = getOpt('ttl-seconds', 'number');
1740 const result = handleFlowExecutionConsentMintRequest({
1741 dataDir: config.data_dir,
1742 vaultId,
1743 cliScopes,
1744 runId,
1745 allowedLanes,
1746 costCapUnits: costCap,
1747 ttlSeconds: ttlRaw ?? undefined,
1748 });
1749 if (!result.ok) {
1750 flowExitWithError(result.error, result.code);
1751 }
1752 if (useJson) {
1753 console.log(JSON.stringify(result.payload));
1754 } else {
1755 console.log(`consent ${result.payload.consent.consent_id} expires ${result.payload.consent.expires_at}`);
1756 }
1757 process.exit(0);
1758 }
1759
1760 if (runSub === 'submit-review') {
1761 const runId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1762 const intent = getOpt('intent');
1763 if (!runId || !intent) {
1764 flowExitWithError('knowtation flow run submit-review: provide run_id and --intent.', 'BAD_REQUEST');
1765 }
1766 const result = handleFlowRunSubmitReviewRequest({
1767 dataDir: config.data_dir,
1768 vaultId,
1769 cliScopes,
1770 runId,
1771 intent,
1772 createProposal,
1773 });
1774 if (!result.ok) {
1775 flowExitWithError(result.error, result.code);
1776 }
1777 if (useJson) {
1778 console.log(JSON.stringify(result.payload));
1779 } else {
1780 console.log(`submitted ${runId} → proposal ${result.payload.proposal_id}`);
1781 }
1782 process.exit(0);
1783 }
1784
1785 flowExitWithError(`knowtation flow run: unknown subcommand ${runSub}`, 'BAD_REQUEST');
1786 }
1787
1788 if (flowAction === 'grant') {
1789 const grantAction = args[2];
1790 if (!grantAction || hasOpt('help') || hasOpt('h')) {
1791 console.log('knowtation flow grant mint|revoke|list — see knowtation flow --help');
1792 process.exit(0);
1793 }
1794
1795 const {
1796 handleFlowExternalGrantMintRequest,
1797 handleFlowExternalGrantRevokeRequest,
1798 handleFlowExternalGrantListRequest,
1799 } = await import('../lib/flow/external-agent.mjs');
1800
1801 if (grantAction === 'mint') {
1802 const flowId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1803 const flowVersion = getOpt('flow-version');
1804 const toolsRaw = getOpt('tools');
1805 if (!flowId || !flowVersion || !toolsRaw) {
1806 flowExitWithError(
1807 'knowtation flow grant mint: provide flow_id, --flow-version, and --tools.',
1808 'BAD_REQUEST',
1809 );
1810 }
1811 const requestedTools = toolsRaw.split(',').map((t) => t.trim()).filter(Boolean);
1812 const ttlRaw = getOpt('ttl-seconds', 'number');
1813 const result = handleFlowExternalGrantMintRequest({
1814 dataDir: config.data_dir,
1815 vaultId,
1816 cliScopes,
1817 flowId,
1818 flowVersion,
1819 requestedTools,
1820 ttlSeconds: ttlRaw ?? undefined,
1821 actorLabel: getOpt('actor-label') ?? undefined,
1822 });
1823 if (!result.ok) {
1824 flowExitWithError(result.error, result.code);
1825 }
1826 if (useJson) {
1827 console.log(JSON.stringify(result.payload));
1828 } else {
1829 console.log(`grant ${result.payload.grant.grant_id} expires ${result.payload.expires_at}`);
1830 console.log(`bearer (one-time): ${result.payload.bearer}`);
1831 }
1832 process.exit(0);
1833 }
1834
1835 if (grantAction === 'revoke') {
1836 const grantId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
1837 if (!grantId) {
1838 flowExitWithError('knowtation flow grant revoke: provide grant_id.', 'BAD_REQUEST');
1839 }
1840 const result = handleFlowExternalGrantRevokeRequest({
1841 dataDir: config.data_dir,
1842 vaultId,
1843 grantId,
1844 });
1845 if (!result.ok) {
1846 flowExitWithError(result.error, result.code);
1847 }
1848 if (useJson) {
1849 console.log(JSON.stringify(result.payload));
1850 } else {
1851 console.log(`revoked ${grantId} at ${result.payload.revoked_at}`);
1852 }
1853 process.exit(0);
1854 }
1855
1856 if (grantAction === 'list') {
1857 const result = handleFlowExternalGrantListRequest({
1858 dataDir: config.data_dir,
1859 vaultId,
1860 flowId: getOpt('flow-id') ?? undefined,
1861 });
1862 if (!result.ok) {
1863 flowExitWithError(result.error, result.code);
1864 }
1865 if (useJson) {
1866 console.log(JSON.stringify(result.payload));
1867 } else {
1868 for (const g of result.payload.grants) {
1869 console.log(`${g.grant_id} ${g.flow_id}@${g.flow_version} tools=${g.allowed_tools.join(',')}`);
1870 }
1871 }
1872 process.exit(0);
1873 }
1874
1875 flowExitWithError(`knowtation flow grant: unknown action "${grantAction}".`, 'BAD_REQUEST');
1876 }
1877
1878 if (flowAction === 'capture') {
1879 const captureAction = args[2];
1880 if (!captureAction || hasOpt('help') || hasOpt('h')) {
1881 console.log('knowtation flow capture observe|list|propose|dismiss — see knowtation flow --help');
1882 process.exit(0);
1883 }
1884
1885 const {
1886 handleFlowCaptureObserveRequest,
1887 handleFlowCaptureListRequest,
1888 handleFlowCaptureProposeRequest,
1889 handleFlowCaptureDismissRequest,
1890 } = await import('../lib/flow/flow-capture.mjs');
1891 const { createProposal } = await import('../hub/proposals-store.mjs');
1892
1893 if (captureAction === 'observe') {
1894 const signalsPath = args[3];
1895 if (!signalsPath) {
1896 flowExitWithError('knowtation flow capture observe: provide a signals.json path.', 'BAD_REQUEST');
1897 }
1898 const fsMod = await import('node:fs');
1899 let sessionMeta;
1900 try {
1901 sessionMeta = JSON.parse(fsMod.readFileSync(signalsPath, 'utf8'));
1902 } catch (e) {
1903 flowExitWithError(`knowtation flow capture observe: cannot read signals (${e.message}).`, 'BAD_REQUEST');
1904 }
1905 const result = handleFlowCaptureObserveRequest({
1906 dataDir: config.data_dir,
1907 vaultId,
1908 cliScopes,
1909 sessionMeta,
1910 includeLowConfidence: hasOpt('include-low-confidence'),
1911 harness: 'cli',
1912 config,
1913 });
1914 if (!result.ok) flowExitWithError(result.error, result.code);
1915 if (useJson) console.log(JSON.stringify(result.payload));
1916 else console.log(JSON.stringify(result.payload, null, 2));
1917 process.exit(0);
1918 }
1919
1920 if (captureAction === 'list') {
1921 const limitOpt = getOpt('limit', 'number');
1922 const result = handleFlowCaptureListRequest({
1923 dataDir: config.data_dir,
1924 vaultId,
1925 cliScopes,
1926 scope: getOpt('scope') ?? undefined,
1927 includeLowConfidence: hasOpt('include-low-confidence'),
1928 limit: limitOpt ?? undefined,
1929 config,
1930 });
1931 if (!result.ok) flowExitWithError(result.error, result.code);
1932 if (useJson) console.log(JSON.stringify(result.payload));
1933 else console.log(JSON.stringify(result.payload, null, 2));
1934 process.exit(0);
1935 }
1936
1937 if (captureAction === 'propose') {
1938 const candidateId = args[3];
1939 const intent = getOpt('intent');
1940 const confirmedScope = getOpt('confirmed-scope');
1941 if (!candidateId || !intent || !confirmedScope) {
1942 flowExitWithError(
1943 'knowtation flow capture propose: provide candidate_id, --intent, and --confirmed-scope.',
1944 'BAD_REQUEST',
1945 );
1946 }
1947 const result = handleFlowCaptureProposeRequest({
1948 dataDir: config.data_dir,
1949 vaultId,
1950 cliScopes,
1951 candidateId,
1952 confirmedScope,
1953 scopeWidenAcknowledged: hasOpt('scope-widen-acknowledged'),
1954 allowLowConfidence: hasOpt('allow-low-confidence'),
1955 forceNewFlow: hasOpt('force-new-flow'),
1956 mergeIntoFlowId: getOpt('merge-into-flow-id') ?? undefined,
1957 intent,
1958 createProposal,
1959 config,
1960 });
1961 if (!result.ok) flowExitWithError(result.error, result.code);
1962 if (useJson) console.log(JSON.stringify(result.payload));
1963 else console.log(JSON.stringify(result.payload, null, 2));
1964 process.exit(0);
1965 }
1966
1967 if (captureAction === 'dismiss') {
1968 const candidateId = args[3];
1969 const intent = getOpt('intent');
1970 if (!candidateId || !intent) {
1971 flowExitWithError('knowtation flow capture dismiss: provide candidate_id and --intent.', 'BAD_REQUEST');
1972 }
1973 const result = handleFlowCaptureDismissRequest({
1974 dataDir: config.data_dir,
1975 vaultId,
1976 cliScopes,
1977 candidateId,
1978 intent,
1979 createProposal,
1980 });
1981 if (!result.ok) flowExitWithError(result.error, result.code);
1982 if (useJson) console.log(JSON.stringify(result.payload));
1983 else console.log(JSON.stringify(result.payload, null, 2));
1984 process.exit(0);
1985 }
1986
1987 flowExitWithError(`knowtation flow capture: unknown action "${captureAction}".`, 'BAD_REQUEST');
1988 }
1989
1990 exitWithError(`knowtation flow: unknown action "${flowAction}". Use list, get, project, propose, import, grant, capture, or run.`, 1, useJson);
1991 return;
1992 }
1993
1994 if (subcommand === 'task') {
1995 const taskAction = args[1];
1996 if (hasOpt('help') || hasOpt('h') || !taskAction) {
1997 console.log(`knowtation task <action>
1998 Actions:
1999 list [--scope personal|project|org] [--workspace-id <id>] [--status <s>] [--kind <k>] [--limit <n>] [--json]
2000 get <task_id> [--json]
2001 propose <payload.json> [--intent <text>] [--json]
2002
2003 Options: --json (exact Hub JSON)`);
2004 process.exit(0);
2005 }
2006
2007 let config;
2008 try {
2009 config = loadConfig();
2010 } catch (e) {
2011 exitWithError(e.message, 2, useJson);
2012 }
2013
2014 const vaultId = getOpt('vault') || 'default';
2015 const cliScopes = Array.isArray(config.flow?.visible_scopes) ? config.flow.visible_scopes : undefined;
2016
2017 const taskExitWithError = (message, codeStr, exitCode = 1) => {
2018 if (useJson) {
2019 process.stderr.write(JSON.stringify({ error: message, code: codeStr }) + '\n');
2020 } else {
2021 console.error(message);
2022 }
2023 process.exit(exitCode);
2024 };
2025
2026 if (taskAction === 'list') {
2027 const limitOpt = getOpt('limit', 'number');
2028 const { handleTaskListRequest } = await import('../lib/task/task-handlers.mjs');
2029 const result = handleTaskListRequest({
2030 dataDir: config.data_dir,
2031 vaultId,
2032 cliScopes,
2033 scope: getOpt('scope') ?? undefined,
2034 workspaceId: getOpt('workspace-id') ?? undefined,
2035 status: getOpt('status') ?? undefined,
2036 kind: getOpt('kind') ?? undefined,
2037 limit: limitOpt ?? undefined,
2038 });
2039 if (!result.ok) {
2040 taskExitWithError(result.error, result.code);
2041 }
2042 if (useJson) {
2043 console.log(JSON.stringify(result.payload));
2044 } else {
2045 const rows = result.payload.tasks;
2046 if (rows.length === 0) {
2047 console.log('(no tasks)');
2048 } else {
2049 for (const t of rows) {
2050 console.log(`${t.task_id} [${t.scope}] ${t.status} ${t.title}`);
2051 }
2052 }
2053 if (result.payload.truncated) {
2054 console.log('(truncated)');
2055 }
2056 }
2057 process.exit(0);
2058 }
2059
2060 if (taskAction === 'get') {
2061 const taskId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
2062 if (!taskId) {
2063 taskExitWithError('knowtation task get: provide a task_id.', 'BAD_REQUEST');
2064 }
2065 const { handleTaskGetRequest } = await import('../lib/task/task-handlers.mjs');
2066 const result = handleTaskGetRequest({
2067 dataDir: config.data_dir,
2068 vaultId,
2069 taskId,
2070 cliScopes,
2071 });
2072 if (!result.ok) {
2073 taskExitWithError(result.error, result.code);
2074 }
2075 if (useJson) {
2076 console.log(JSON.stringify(result.payload));
2077 } else {
2078 const { task } = result.payload;
2079 console.log(`${task.task_id} [${task.scope}] ${task.status}`);
2080 console.log(task.title);
2081 console.log(`workspace: ${task.workspace_id} due: ${task.due_at ?? '—'}`);
2082 if (task.run_ref) console.log(`run_ref: ${task.run_ref}`);
2083 if (task.artifact_links.length) {
2084 console.log(`artifact_links (${task.artifact_links.length}):`);
2085 for (const link of task.artifact_links) {
2086 console.log(` ${link.kind}: ${link.ref}`);
2087 }
2088 }
2089 }
2090 process.exit(0);
2091 }
2092
2093 if (taskAction === 'propose') {
2094 const payloadPath = args.find((a, i) => i >= 2 && !a.startsWith('--'));
2095 if (!payloadPath) {
2096 taskExitWithError('knowtation task propose: provide a payload.json path.', 'BAD_REQUEST');
2097 }
2098 const fsMod = await import('node:fs');
2099 let payload;
2100 try {
2101 payload = JSON.parse(fsMod.readFileSync(payloadPath, 'utf8'));
2102 } catch (e) {
2103 taskExitWithError(`knowtation task propose: cannot read payload (${e.message}).`, 'BAD_REQUEST');
2104 }
2105 const intent = getOpt('intent') || (payload && typeof payload === 'object' ? payload.intent : undefined);
2106 const proposalKind =
2107 payload && typeof payload.proposal_kind === 'string' ? payload.proposal_kind : 'task_create';
2108 const { handleTaskProposeRequest } = await import('../lib/task/task-write.mjs');
2109 const { createProposal } = await import('../hub/proposals-store.mjs');
2110 const result = await handleTaskProposeRequest({
2111 dataDir: config.data_dir,
2112 vaultId,
2113 cliScopes,
2114 proposalKind,
2115 body: payload,
2116 intent,
2117 createProposal,
2118 });
2119 if (!result.ok) {
2120 taskExitWithError(result.error, result.code);
2121 }
2122 if (useJson) {
2123 console.log(JSON.stringify(result.payload));
2124 } else {
2125 console.log(
2126 `proposed ${result.payload.proposal_kind} → ${result.payload.proposal_id} task=${result.payload.task_id ?? '—'}`,
2127 );
2128 }
2129 process.exit(0);
2130 }
2131
2132 exitWithError(`knowtation task: unknown action "${taskAction}". Use list, get, or propose.`, 1, useJson);
2133 return;
2134 }
2135
2136 if (subcommand === 'attachment') {
2137 const attachmentAction = args[1];
2138 if (hasOpt('help') || hasOpt('h') || !attachmentAction) {
2139 console.log(`knowtation attachment <action>
2140 Actions:
2141 list [--scope personal|project|org] [--note-ref <note:path>] [--source vault_file|mist_blob|embedded_url|connector_ref] [--mime-class <c>] [--storage-kind <k>] [--agent-visible] [--limit <n>] [--json]
2142 get <attachment_id> [--json]
2143 link-propose <payload.json> [--intent <text>] [--json]
2144 attach-propose <payload.json> [--intent <text>] [--json]
2145 import-consent grant|list|revoke [--json] (grant/revoke: payload.json or flags)
2146
2147 Options: --json (exact Hub JSON)`);
2148 process.exit(0);
2149 }
2150
2151 let config;
2152 try {
2153 config = loadConfig();
2154 } catch (e) {
2155 exitWithError(e.message, 2, useJson);
2156 }
2157
2158 const vaultId = getOpt('vault') || 'default';
2159 const cliScopes = Array.isArray(config.flow?.visible_scopes) ? config.flow.visible_scopes : undefined;
2160
2161 const attachmentExitWithError = (message, codeStr, exitCode = 1) => {
2162 if (useJson) {
2163 process.stderr.write(JSON.stringify({ error: message, code: codeStr }) + '\n');
2164 } else {
2165 console.error(message);
2166 }
2167 process.exit(exitCode);
2168 };
2169
2170 if (attachmentAction === 'list') {
2171 const limitOpt = getOpt('limit', 'number');
2172 const { handleAttachmentListRequest } = await import('../lib/attachments/attachment-handlers.mjs');
2173 const result = handleAttachmentListRequest({
2174 dataDir: config.data_dir,
2175 vaultPath: config.vault_path,
2176 vaultId,
2177 cliScopes,
2178 scope: getOpt('scope') ?? undefined,
2179 noteRef: getOpt('note-ref') ?? undefined,
2180 source: getOpt('source') ?? undefined,
2181 mimeClass: getOpt('mime-class') ?? undefined,
2182 storageKind: getOpt('storage-kind') ?? undefined,
2183 agentVisible: hasOpt('agent-visible'),
2184 limit: limitOpt ?? undefined,
2185 vaultConfig: { ignore: config.ignore },
2186 });
2187 if (!result.ok) {
2188 attachmentExitWithError(result.error, result.code);
2189 }
2190 if (useJson) {
2191 console.log(JSON.stringify(result.payload));
2192 } else {
2193 const rows = result.payload.attachments;
2194 if (rows.length === 0) {
2195 console.log('(no attachments)');
2196 } else {
2197 for (const a of rows) {
2198 console.log(`${a.attachment_id} [${a.scope}] ${a.mime_class} ${a.display_label}`);
2199 }
2200 }
2201 if (result.payload.truncated) {
2202 console.log('(truncated)');
2203 }
2204 }
2205 process.exit(0);
2206 }
2207
2208 if (attachmentAction === 'get') {
2209 const attachmentId = args.find((a, i) => i >= 2 && !a.startsWith('--'));
2210 if (!attachmentId) {
2211 attachmentExitWithError('knowtation attachment get: provide an attachment_id.', 'BAD_REQUEST');
2212 }
2213 const { handleAttachmentGetRequest } = await import('../lib/attachments/attachment-handlers.mjs');
2214 const result = handleAttachmentGetRequest({
2215 dataDir: config.data_dir,
2216 vaultPath: config.vault_path,
2217 vaultId,
2218 attachmentId,
2219 cliScopes,
2220 vaultConfig: { ignore: config.ignore },
2221 });
2222 if (!result.ok) {
2223 attachmentExitWithError(result.error, result.code);
2224 }
2225 if (useJson) {
2226 console.log(JSON.stringify(result.payload));
2227 } else {
2228 const { attachment } = result.payload;
2229 console.log(`${attachment.attachment_id} [${attachment.scope}] ${attachment.mime_class}`);
2230 console.log(JSON.stringify(attachment, null, 2));
2231 }
2232 process.exit(0);
2233 }
2234
2235 if (attachmentAction === 'link-propose' || attachmentAction === 'attach-propose') {
2236 const payloadPath = args.find((a, i) => i >= 2 && !a.startsWith('--'));
2237 if (!payloadPath) {
2238 attachmentExitWithError(
2239 `knowtation attachment ${attachmentAction}: provide a payload.json path.`,
2240 'BAD_REQUEST',
2241 );
2242 }
2243 const fsMod = await import('node:fs');
2244 let payload;
2245 try {
2246 payload = JSON.parse(fsMod.readFileSync(payloadPath, 'utf8'));
2247 } catch (e) {
2248 attachmentExitWithError(`cannot read payload (${e.message}).`, 'BAD_REQUEST');
2249 }
2250 const intent = getOpt('intent') || (payload && typeof payload === 'object' ? payload.intent : undefined);
2251 const { createProposal } = await import('../hub/proposals-store.mjs');
2252 const {
2253 handleMediaLinkProposeRequest,
2254 handleMediaAttachProposeRequest,
2255 } = await import('../lib/attachments/attachment-write.mjs');
2256 const handler =
2257 attachmentAction === 'link-propose'
2258 ? handleMediaLinkProposeRequest
2259 : handleMediaAttachProposeRequest;
2260 const result = await handler({
2261 dataDir: config.data_dir,
2262 vaultPath: config.vault_path,
2263 vaultId,
2264 cliScopes,
2265 body: payload,
2266 intent,
2267 createProposal,
2268 vaultConfig: { ignore: config.ignore },
2269 });
2270 if (!result.ok) {
2271 attachmentExitWithError(result.error, result.code);
2272 }
2273 if (useJson) {
2274 console.log(JSON.stringify(result.payload));
2275 } else {
2276 console.log(
2277 `proposed ${result.payload.proposal_kind} → ${result.payload.proposal_id} attachment=${result.payload.attachment_id}`,
2278 );
2279 }
2280 process.exit(0);
2281 }
2282
2283 if (attachmentAction === 'import-consent') {
2284 const consentAction = args[2];
2285 const {
2286 handleMediaImportConsentGrantRequest,
2287 handleMediaImportConsentListRequest,
2288 handleMediaImportConsentRevokeRequest,
2289 } = await import('../lib/attachments/attachment-write.mjs');
2290
2291 if (consentAction === 'list') {
2292 const result = handleMediaImportConsentListRequest({
2293 dataDir: config.data_dir,
2294 vaultId,
2295 cliScopes,
2296 scope: getOpt('scope') ?? undefined,
2297 });
2298 if (!result.ok) {
2299 attachmentExitWithError(result.error, result.code);
2300 }
2301 if (useJson) {
2302 console.log(JSON.stringify(result.payload));
2303 } else {
2304 for (const c of result.payload.consents) {
2305 console.log(`${c.consent_id} ${c.connector_id} [${c.scope}] ${c.status}`);
2306 }
2307 }
2308 process.exit(0);
2309 }
2310
2311 const payloadPath = args.find((a, i) => i >= 3 && !a.startsWith('--'));
2312 const fsMod = await import('node:fs');
2313 let payload = {};
2314 if (payloadPath) {
2315 try {
2316 payload = JSON.parse(fsMod.readFileSync(payloadPath, 'utf8'));
2317 } catch (e) {
2318 attachmentExitWithError(`cannot read payload (${e.message}).`, 'BAD_REQUEST');
2319 }
2320 }
2321
2322 if (consentAction === 'grant') {
2323 const result = handleMediaImportConsentGrantRequest({
2324 dataDir: config.data_dir,
2325 vaultId,
2326 userId: 'cli-user',
2327 cliScopes,
2328 body: payload,
2329 });
2330 if (!result.ok) {
2331 attachmentExitWithError(result.error, result.code);
2332 }
2333 if (useJson) {
2334 console.log(JSON.stringify(result.payload));
2335 } else {
2336 console.log(`granted consent ${result.payload.consent_id}`);
2337 }
2338 process.exit(0);
2339 }
2340
2341 if (consentAction === 'revoke') {
2342 const consentId =
2343 payload.consent_id || getOpt('consent-id') || args.find((a, i) => i >= 3 && !a.startsWith('--'));
2344 const result = handleMediaImportConsentRevokeRequest({
2345 dataDir: config.data_dir,
2346 vaultId,
2347 cliScopes,
2348 consentId,
2349 body: payload,
2350 });
2351 if (!result.ok) {
2352 attachmentExitWithError(result.error, result.code);
2353 }
2354 if (useJson) {
2355 console.log(JSON.stringify(result.payload));
2356 } else {
2357 console.log(`revoked consent ${result.payload.consent_id}`);
2358 }
2359 process.exit(0);
2360 }
2361
2362 attachmentExitWithError(
2363 'knowtation attachment import-consent: use grant, list, or revoke.',
2364 'BAD_REQUEST',
2365 );
2366 }
2367
2368 exitWithError(
2369 `knowtation attachment: unknown action "${attachmentAction}". Use list, get, link-propose, attach-propose, or import-consent.`,
2370 1,
2371 useJson,
2372 );
2373 return;
2374 }
2375
2376 if (subcommand === 'task-loop') {
2377 const loopAction = args[1];
2378 if (hasOpt('help') || hasOpt('h') || !loopAction) {
2379 console.log(`knowtation task-loop <action>
2380 Actions:
2381 propose <payload.json> [--intent <text>] [--json]
2382 materialize --loop-id <loop_id> [--occurrence-key <key>] [--occurrence-at <iso>] [--due-at <iso>] [--intent <text>] [--json]
2383
2384 Options: --json (exact Hub JSON)`);
2385 process.exit(0);
2386 }
2387
2388 let config;
2389 try {
2390 config = loadConfig();
2391 } catch (e) {
2392 exitWithError(e.message, 2, useJson);
2393 }
2394
2395 const vaultId = getOpt('vault') || 'default';
2396 const cliScopes = Array.isArray(config.flow?.visible_scopes) ? config.flow.visible_scopes : undefined;
2397
2398 const loopExitWithError = (message, codeStr, exitCode = 1) => {
2399 if (useJson) {
2400 process.stderr.write(JSON.stringify({ error: message, code: codeStr }) + '\n');
2401 } else {
2402 console.error(message);
2403 }
2404 process.exit(exitCode);
2405 };
2406
2407 if (loopAction === 'propose') {
2408 const payloadPath = args.find((a, i) => i >= 2 && !a.startsWith('--'));
2409 if (!payloadPath) {
2410 loopExitWithError('knowtation task-loop propose: provide a payload.json path.', 'BAD_REQUEST');
2411 }
2412 const fsMod = await import('node:fs');
2413 let payload;
2414 try {
2415 payload = JSON.parse(fsMod.readFileSync(payloadPath, 'utf8'));
2416 } catch (e) {
2417 loopExitWithError(`knowtation task-loop propose: cannot read payload (${e.message}).`, 'BAD_REQUEST');
2418 }
2419 const intent = getOpt('intent') || (payload && typeof payload === 'object' ? payload.intent : undefined);
2420 const proposalKind =
2421 payload && typeof payload.proposal_kind === 'string' ? payload.proposal_kind : 'task_loop_create';
2422 const { handleTaskLoopProposeRequest } = await import('../lib/task/task-write.mjs');
2423 const { createProposal } = await import('../hub/proposals-store.mjs');
2424 const result = await handleTaskLoopProposeRequest({
2425 dataDir: config.data_dir,
2426 vaultId,
2427 cliScopes,
2428 proposalKind,
2429 body: payload,
2430 intent,
2431 createProposal,
2432 });
2433 if (!result.ok) {
2434 loopExitWithError(result.error, result.code);
2435 }
2436 if (useJson) {
2437 console.log(JSON.stringify(result.payload));
2438 } else {
2439 console.log(
2440 `proposed ${result.payload.proposal_kind} → ${result.payload.proposal_id} loop=${result.payload.loop_id ?? '—'}`,
2441 );
2442 }
2443 process.exit(0);
2444 }
2445
2446 if (loopAction === 'materialize') {
2447 const loopId = getOpt('loop-id');
2448 if (!loopId) {
2449 loopExitWithError('knowtation task-loop materialize: --loop-id required.', 'BAD_REQUEST');
2450 }
2451 const intent = getOpt('intent') || 'materialize occurrence';
2452 const { handleTaskInstanceMaterializeRequest } = await import('../lib/task/task-write.mjs');
2453 const { createProposal } = await import('../hub/proposals-store.mjs');
2454 const result = await handleTaskInstanceMaterializeRequest({
2455 dataDir: config.data_dir,
2456 vaultId,
2457 cliScopes,
2458 loopId,
2459 body: {
2460 loop_id: loopId,
2461 occurrence_key: getOpt('occurrence-key') ?? undefined,
2462 occurrence_at: getOpt('occurrence-at') ?? undefined,
2463 due_at: getOpt('due-at') ?? undefined,
2464 title_override: getOpt('title-override') ?? undefined,
2465 base_state_id: getOpt('base-state-id') ?? undefined,
2466 },
2467 intent,
2468 createProposal,
2469 });
2470 if (!result.ok) {
2471 loopExitWithError(result.error, result.code);
2472 }
2473 if (useJson) {
2474 console.log(JSON.stringify(result.payload));
2475 } else {
2476 console.log(
2477 `materialize proposal ${result.payload.proposal_id} task=${result.payload.task_id} occurrence=${result.payload.occurrence_key}`,
2478 );
2479 }
2480 process.exit(0);
2481 }
2482
2483 exitWithError(`knowtation task-loop: unknown action "${loopAction}". Use propose or materialize.`, 1, useJson);
2484 return;
2485 }
2486
2487 if (subcommand === 'agent') {
2488 const agentAction = args[1];
2489 if (!agentAction || hasOpt('help') || hasOpt('h')) {
2490 console.log(`knowtation agent identity register|list — gated by DELEGATION_ENABLED (default off)
2491 register --kind user_owned|org_owned|delegate [--agent-id <id>] [--label <text>] [--scope-ceiling personal|project|org]
2492 list [--kind <kind>] [--status active|suspended|revoked]`);
2493 process.exit(0);
2494 }
2495
2496 let config;
2497 try {
2498 config = loadConfig();
2499 } catch (e) {
2500 exitWithError(e.message, 2, useJson);
2501 }
2502 const vaultId = config.default_vault_id || 'default';
2503 const {
2504 handleAgentIdentityRegisterProposeRequest,
2505 handleAgentIdentityListRequest,
2506 } = await import('../lib/agent/delegation.mjs');
2507 const { createProposal } = await import('../hub/proposals-store.mjs');
2508
2509 if (agentAction === 'identity') {
2510 const idAction = args[2];
2511 if (idAction === 'register') {
2512 const kind = getOpt('kind');
2513 if (!kind) {
2514 exitWithError('knowtation agent identity register: --kind required.', 'BAD_REQUEST', useJson);
2515 }
2516 const result = await handleAgentIdentityRegisterProposeRequest({
2517 dataDir: config.data_dir,
2518 vaultId,
2519 userId: config.user_id ?? 'cli-user',
2520 kind,
2521 agentId: getOpt('agent-id') ?? undefined,
2522 label: getOpt('label') ?? undefined,
2523 scopeCeiling: getOpt('scope-ceiling') ?? undefined,
2524 createProposal,
2525 });
2526 if (!result.ok) exitWithError(result.error, result.code, useJson);
2527 if (useJson) console.log(JSON.stringify(result.payload));
2528 else console.log(`proposed agent ${result.payload.agent_id} → proposal ${result.payload.proposal_id}`);
2529 process.exit(0);
2530 }
2531 if (idAction === 'list') {
2532 const result = handleAgentIdentityListRequest({
2533 dataDir: config.data_dir,
2534 vaultId,
2535 kind: getOpt('kind') ?? undefined,
2536 status: getOpt('status') ?? undefined,
2537 });
2538 if (!result.ok) exitWithError(result.error, result.code, useJson);
2539 if (useJson) console.log(JSON.stringify(result.payload));
2540 else {
2541 for (const i of result.payload.identities) {
2542 console.log(`${i.agent_id} ${i.kind} ${i.status}`);
2543 }
2544 }
2545 process.exit(0);
2546 }
2547 exitWithError(`knowtation agent identity: unknown action "${idAction}".`, 'BAD_REQUEST', useJson);
2548 }
2549 exitWithError(`knowtation agent: unknown action "${agentAction}".`, 'BAD_REQUEST', useJson);
2550 }
2551
2552 if (subcommand === 'delegation') {
2553 const delAction = args[1];
2554 if (!delAction || hasOpt('help') || hasOpt('h')) {
2555 console.log(`knowtation delegation consent propose|revoke — grant mint|revoke|list — audit append
2556 consent propose --delegate-agent-id <id> --scope personal|project|org [--workspace-id <ws>] [--allowed-task-ids a,b]
2557 consent revoke <consent_id>
2558 grant mint --consent-id <id> --actor-agent-id <id> [--task-ref <id>] [--flow-id <id>] [--flow-version <semver>]
2559 grant revoke <grant_id>
2560 grant list [--actor-agent-id <id>]
2561 audit append --grant-id <id> --actor-agent-id <id> --action advance_step|... --evidence-refs a,b`);
2562 process.exit(0);
2563 }
2564
2565 let config;
2566 try {
2567 config = loadConfig();
2568 } catch (e) {
2569 exitWithError(e.message, 2, useJson);
2570 }
2571 const vaultId = config.default_vault_id || 'default';
2572 const {
2573 handleDelegationConsentProposeRequest,
2574 handleDelegationConsentRevokeRequest,
2575 handleDelegationGrantMintRequest,
2576 handleDelegationGrantRevokeRequest,
2577 handleDelegationGrantListRequest,
2578 handleDelegationAuditAppendRequest,
2579 hashPrincipalRef,
2580 } = await import('../lib/agent/delegation.mjs');
2581 const { createProposal } = await import('../hub/proposals-store.mjs');
2582
2583 if (delAction === 'consent') {
2584 const consentAction = args[2];
2585 if (consentAction === 'propose') {
2586 const delegateAgentId = getOpt('delegate-agent-id');
2587 const scope = getOpt('scope');
2588 if (!delegateAgentId || !scope) {
2589 exitWithError(
2590 'knowtation delegation consent propose: --delegate-agent-id and --scope required.',
2591 'BAD_REQUEST',
2592 useJson,
2593 );
2594 }
2595 const taskIdsRaw = getOpt('allowed-task-ids');
2596 const flowIdsRaw = getOpt('allowed-flow-ids');
2597 const result = await handleDelegationConsentProposeRequest({
2598 dataDir: config.data_dir,
2599 vaultId,
2600 userId: config.user_id ?? 'cli-user',
2601 delegateAgentId,
2602 scope,
2603 workspaceId: getOpt('workspace-id') ?? undefined,
2604 allowedFlowIds: flowIdsRaw ? flowIdsRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
2605 allowedTaskIds: taskIdsRaw ? taskIdsRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined,
2606 expiresAt: getOpt('expires-at') ?? undefined,
2607 createProposal,
2608 });
2609 if (!result.ok) exitWithError(result.error, result.code, useJson);
2610 if (useJson) console.log(JSON.stringify(result.payload));
2611 else console.log(`proposed consent ${result.payload.consent_id} → proposal ${result.payload.proposal_id}`);
2612 process.exit(0);
2613 }
2614 if (consentAction === 'revoke') {
2615 const consentId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
2616 if (!consentId) {
2617 exitWithError('knowtation delegation consent revoke: provide consent_id.', 'BAD_REQUEST', useJson);
2618 }
2619 const result = handleDelegationConsentRevokeRequest({
2620 dataDir: config.data_dir,
2621 vaultId,
2622 consentId,
2623 userId: config.user_id ?? 'cli-user',
2624 });
2625 if (!result.ok) exitWithError(result.error, result.code, useJson);
2626 if (useJson) console.log(JSON.stringify(result.payload));
2627 else console.log(`revoked ${consentId} at ${result.payload.revoked_at}`);
2628 process.exit(0);
2629 }
2630 exitWithError(`knowtation delegation consent: unknown action "${consentAction}".`, 'BAD_REQUEST', useJson);
2631 }
2632
2633 if (delAction === 'grant') {
2634 const grantAction = args[2];
2635 if (grantAction === 'mint') {
2636 const consentId = getOpt('consent-id');
2637 const actorAgentId = getOpt('actor-agent-id');
2638 if (!consentId || !actorAgentId) {
2639 exitWithError(
2640 'knowtation delegation grant mint: --consent-id and --actor-agent-id required.',
2641 'BAD_REQUEST',
2642 useJson,
2643 );
2644 }
2645 const ttlRaw = getOpt('ttl-seconds', 'number');
2646 const result = handleDelegationGrantMintRequest({
2647 dataDir: config.data_dir,
2648 vaultId,
2649 consentId,
2650 actorAgentId,
2651 taskRef: getOpt('task-ref') ?? undefined,
2652 runRef: getOpt('run-ref') ?? undefined,
2653 flowId: getOpt('flow-id') ?? undefined,
2654 flowVersion: getOpt('flow-version') ?? undefined,
2655 ttlSeconds: ttlRaw ?? undefined,
2656 });
2657 if (!result.ok) exitWithError(result.error, result.code, useJson);
2658 if (useJson) console.log(JSON.stringify(result.payload));
2659 else {
2660 console.log(`grant ${result.payload.grant.grant_id} expires ${result.payload.expires_at}`);
2661 console.log(`bearer (one-time): ${result.payload.bearer}`);
2662 }
2663 process.exit(0);
2664 }
2665 if (grantAction === 'revoke') {
2666 const grantId = args.find((a, i) => i >= 3 && !a.startsWith('--'));
2667 if (!grantId) {
2668 exitWithError('knowtation delegation grant revoke: provide grant_id.', 'BAD_REQUEST', useJson);
2669 }
2670 const result = handleDelegationGrantRevokeRequest({
2671 dataDir: config.data_dir,
2672 vaultId,
2673 grantId,
2674 });
2675 if (!result.ok) exitWithError(result.error, result.code, useJson);
2676 if (useJson) console.log(JSON.stringify(result.payload));
2677 else console.log(`revoked ${grantId} at ${result.payload.revoked_at}`);
2678 process.exit(0);
2679 }
2680 if (grantAction === 'list') {
2681 const result = handleDelegationGrantListRequest({
2682 dataDir: config.data_dir,
2683 vaultId,
2684 actorAgentId: getOpt('actor-agent-id') ?? undefined,
2685 });
2686 if (!result.ok) exitWithError(result.error, result.code, useJson);
2687 if (useJson) console.log(JSON.stringify(result.payload));
2688 else {
2689 for (const g of result.payload.grants) {
2690 console.log(`${g.grant_id} actor=${g.actor_agent_id} scope=${g.scope}`);
2691 }
2692 }
2693 process.exit(0);
2694 }
2695 exitWithError(`knowtation delegation grant: unknown action "${grantAction}".`, 'BAD_REQUEST', useJson);
2696 }
2697
2698 if (delAction === 'audit') {
2699 const auditAction = args[2];
2700 if (auditAction === 'append') {
2701 const grantId = getOpt('grant-id');
2702 const actorAgentId = getOpt('actor-agent-id');
2703 const action = getOpt('action');
2704 const evidenceRaw = getOpt('evidence-refs');
2705 if (!grantId || !actorAgentId || !action || !evidenceRaw) {
2706 exitWithError(
2707 'knowtation delegation audit append: --grant-id, --actor-agent-id, --action, --evidence-refs required.',
2708 'BAD_REQUEST',
2709 useJson,
2710 );
2711 }
2712 const principalRef = hashPrincipalRef(config.user_id ?? 'cli-user');
2713 const result = handleDelegationAuditAppendRequest({
2714 dataDir: config.data_dir,
2715 vaultId,
2716 grantId,
2717 actorAgentId,
2718 principalRef,
2719 action,
2720 evidenceRefs: evidenceRaw.split(',').map((s) => s.trim()).filter(Boolean),
2721 taskRef: getOpt('task-ref') ?? undefined,
2722 runRef: getOpt('run-ref') ?? undefined,
2723 flowId: getOpt('flow-id') ?? undefined,
2724 flowVersion: getOpt('flow-version') ?? undefined,
2725 stepId: getOpt('step-id') ?? undefined,
2726 executionLocation: getOpt('execution-location') ?? undefined,
2727 });
2728 if (!result.ok) exitWithError(result.error, result.code, useJson);
2729 if (useJson) console.log(JSON.stringify(result.payload));
2730 else console.log(`audit ${result.payload.audit_id} action=${result.payload.action}`);
2731 process.exit(0);
2732 }
2733 exitWithError(`knowtation delegation audit: unknown action "${auditAction}".`, 'BAD_REQUEST', useJson);
2734 }
2735
2736 exitWithError(`knowtation delegation: unknown action "${delAction}".`, 'BAD_REQUEST', useJson);
2737 }
2738
2739 if (subcommand === 'daemon') {
2740 const daemonAction = args[1];
2741
2742 if (!daemonAction || hasOpt('help') || hasOpt('h')) {
2743 console.log(`knowtation daemon <action>
2744 Actions:
2745 start [--background] Start the daemon. --background runs it detached (writes PID).
2746 stop Stop a running daemon (SIGTERM → SIGKILL after 10 s).
2747 status Show running state, PID, last pass, next scheduled pass.
2748 log [--tail <n>] Print daemon log entries (JSONL). --tail limits to last N.
2749
2750 Notes:
2751 - Daemon requires daemon.enabled in config and a reachable LLM.
2752 - Foreground mode: Ctrl+C to stop (SIGINT).
2753 - Background mode writes PID to {data_dir}/daemon.pid, log to {data_dir}/daemon.log.`);
2754 process.exit(0);
2755 }
2756
2757 let config;
2758 try {
2759 config = loadConfig();
2760 } catch (e) {
2761 exitWithError(e.message, 2, useJson);
2762 }
2763
2764 // ── daemon start ───────────────────────────────────────────────────────
2765 if (daemonAction === 'start') {
2766 const background = hasOpt('background');
2767
2768 if (background) {
2769 // Spawn a detached child that runs `knowtation daemon start` (foreground).
2770 // Use env var to prevent the child from re-entering background-spawn logic.
2771 const child = spawn(process.execPath, [__filename, 'daemon', 'start'], {
2772 detached: true,
2773 stdio: 'ignore',
2774 env: { ...process.env, KNOWTATION_DAEMON_BACKGROUND: '0' },
2775 });
2776 child.unref();
2777
2778 const pidPath = path.join(config.data_dir, 'daemon.pid');
2779 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
2780
2781 if (useJson) {
2782 console.log(JSON.stringify({ ok: true, pid: child.pid, pid_path: pidPath, log_path: logPath }));
2783 } else {
2784 const llmProvider = config.daemon?.llm?.provider || 'auto-detect';
2785 const llmModel = config.daemon?.llm?.model || 'default';
2786 console.log(`Daemon started in background (PID ${child.pid}). Consolidation every ${config.daemon?.interval_minutes ?? 120} min when idle.`);
2787 console.log(`LLM: ${llmProvider} ${llmModel}.`);
2788 console.log(`Log: ${logPath}`);
2789 }
2790 process.exit(0);
2791 return;
2792 }
2793
2794 // Foreground mode
2795 if (!config.daemon?.enabled && process.env.KNOWTATION_DAEMON_BACKGROUND !== '0') {
2796 console.warn('Warning: daemon.enabled is false in config. Starting anyway (foreground mode).');
2797 }
2798
2799 (async () => {
2800 try {
2801 const { startDaemon } = await import('../lib/daemon.mjs');
2802 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
2803 const intervalMin = config.daemon?.interval_minutes ?? 120;
2804 console.log(`Daemon starting (PID ${process.pid}). Consolidation every ${intervalMin} min when idle.`);
2805 console.log(`Log: ${logPath}. Press Ctrl+C to stop.`);
2806 await startDaemon(config);
2807 console.log('Daemon stopped.');
2808 process.exit(0);
2809 } catch (e) {
2810 exitWithError(`Daemon start failed: ${e.message}`, 2, useJson);
2811 }
2812 })();
2813 return;
2814 }
2815
2816 // ── daemon stop ────────────────────────────────────────────────────────
2817 if (daemonAction === 'stop') {
2818 (async () => {
2819 try {
2820 const { stopDaemon } = await import('../lib/daemon.mjs');
2821 const result = await stopDaemon(config);
2822 if (useJson) {
2823 console.log(JSON.stringify(result));
2824 } else if (result.stopped) {
2825 console.log(`Daemon stopped (PID ${result.pid}, signal ${result.signal}).`);
2826 } else {
2827 console.log(`Daemon was not running: ${result.reason}`);
2828 }
2829 process.exit(0);
2830 } catch (e) {
2831 exitWithError(`Daemon stop failed: ${e.message}`, 2, useJson);
2832 }
2833 })();
2834 return;
2835 }
2836
2837 // ── daemon status ──────────────────────────────────────────────────────
2838 if (daemonAction === 'status') {
2839 try {
2840 const { getDaemonStatus } = await import('../lib/daemon.mjs');
2841 const status = getDaemonStatus(config);
2842 if (useJson) {
2843 console.log(JSON.stringify(status));
2844 } else if (!status.running) {
2845 console.log('Status: not running');
2846 if (status.last_pass) {
2847 console.log(`Last pass: ${status.last_pass.ts} (${status.last_pass.events_processed} events, ${status.last_pass.topics} topics)`);
2848 }
2849 console.log(`Log: ${status.log_path}`);
2850 } else {
2851 const uptimeSec = Math.round((status.uptime_ms ?? 0) / 1000);
2852 const uptimeStr = uptimeSec < 60
2853 ? `${uptimeSec}s`
2854 : uptimeSec < 3600
2855 ? `${Math.round(uptimeSec / 60)}m ${uptimeSec % 60}s`
2856 : `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
2857 console.log(`Status: running (PID ${status.pid}, uptime ${uptimeStr})`);
2858 if (status.last_pass) {
2859 const lp = status.last_pass;
2860 console.log(`Last pass: ${lp.ts} (processed ${lp.events_processed} events, ${lp.topics} topics)`);
2861 } else {
2862 console.log('Last pass: none yet');
2863 }
2864 if (status.next_pass_at) {
2865 console.log(`Next pass: ~${status.next_pass_at} (if idle)`);
2866 }
2867 }
2868 process.exit(0);
2869 } catch (e) {
2870 exitWithError(`Daemon status failed: ${e.message}`, 2, useJson);
2871 }
2872 return;
2873 }
2874
2875 // ── daemon log ─────────────────────────────────────────────────────────
2876 if (daemonAction === 'log') {
2877 const tail = getOpt('tail', 'number') ?? null;
2878 try {
2879 const { getLogPath, readDaemonLog } = await import('../lib/daemon.mjs');
2880 const logPath = getLogPath(config);
2881 const entries = readDaemonLog(logPath, { tail: tail ?? undefined });
2882 if (useJson) {
2883 console.log(JSON.stringify({ entries, count: entries.length, log_path: logPath }));
2884 } else if (entries.length === 0) {
2885 console.log(`(no log entries — log: ${logPath})`);
2886 } else {
2887 for (const e of entries) {
2888 const { ts, event, ...rest } = e;
2889 const detail = Object.keys(rest).length ? ' ' + JSON.stringify(rest) : '';
2890 console.log(`${ts} ${event ?? '?'}${detail}`);
2891 }
2892 }
2893 process.exit(0);
2894 } catch (e) {
2895 exitWithError(`Daemon log failed: ${e.message}`, 2, useJson);
2896 }
2897 return;
2898 }
2899
2900 exitWithError(`knowtation daemon: unknown action "${daemonAction}". Use start, stop, status, or log.`, 1, useJson);
2901 return;
2902 }
2903
2904 exitWithError(`Unknown command: ${subcommand}`, 1, useJson);
2905 }
2906
2907 main().catch((e) => {
2908 console.error(e.message || e);
2909 process.exit(2);
2910 });
File History 5 commits
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b fix(7C-L1b): route hosted delegation proposals through cani… Human minor 30 days ago
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 42 days ago
sha256:2827ba9e7632a4b141c50caf1e8f7d77abbc3515be20e7465f2bccb0ac4edf91 fix: repair endpoint now sets has_active_subscription when … Human minor 49 days ago