index.mjs
1,423 lines 52.9 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 #!/usr/bin/env node
2 import '../lib/load-env.mjs';
3
4 /**
5 * Knowtation CLI — single entry point for search, get-note, list-notes, index, etc.
6 */
7
8 import fs from 'fs';
9 import path from 'path';
10 import { spawn } from 'child_process';
11 import { execSync } from 'child_process';
12 import { fileURLToPath } from 'url';
13 import yaml from 'js-yaml';
14
15 const __filename = fileURLToPath(import.meta.url);
16 import { loadConfig } from '../lib/config.mjs';
17 import { readNote, resolveVaultRelativePath, noteFileExistsInVault, normalizeMetadataFacets } from '../lib/vault.mjs';
18 import { buildNoteOutline } from '../lib/note-outline.mjs';
19 import { buildDocumentTree } from '../lib/document-tree.mjs';
20 import { readSectionSource } from '../lib/section-source-note.mjs';
21 import { noteStateIdFromHubNoteJson, absentNoteStateId } from '../lib/note-state-id.mjs';
22 import { runListNotes as runListNotesOp } from '../lib/list-notes.mjs';
23 import { exitWithError } from '../lib/errors.mjs';
24 import { IMPORT_SOURCE_TYPES, IMPORT_SOURCE_TYPES_HELP } from '../lib/import-source-types.mjs';
25
26 const args = process.argv.slice(2);
27 const subcommand = args[0];
28 const useJson = args.includes('--json');
29
30 const help = `
31 knowtation — personal knowledge and content system (know + notation)
32
33 Usage:
34 knowtation <command> [options]
35
36 Commands:
37 search <query> Semantic search over vault (default), or --keyword for literal text. Use --project, --tag, --folder, --limit. --json for machine output.
38 get-note <path> Return full content of one note by path. Use --body-only, --frontmatter-only, --json.
39 get-note-outline <path> Return a derived Markdown heading outline for one note. Requires --json.
40 get-document-tree <path> Return a derived Markdown heading tree for one note. Requires --json.
41 get-metadata-facets <path> Return bounded body-free metadata facets for one note. Requires --json.
42 get-section-source <path> Return body-free SectionSource metadata for one note. Requires --json.
43 list-notes List notes. Use --folder, --project, --tag, --limit, --offset, --fields, --count-only, --json.
44 index Re-run indexer: vault → chunk → embed → vector store (Qdrant or sqlite-vec).
45 write <path> Create or overwrite a note. Use --stdin for body, --frontmatter k=v, --append.
46 export <path|query> <output> Export note(s) to dir/file. Use --format, --project. Provenance and AIR per spec.
47 import <source-type> <input> Ingest from ChatGPT, Claude, Mem0, etc. See docs/IMPORT-SOURCES.md.
48 memory <action> Memory layer commands: query, list, store, search, clear, export, stats. Requires memory.enabled.
49 hub status Check Hub reachability (use --hub <url>). Requires Hub API.
50 doctor Local vault + optional Hub API checks (token discipline per docs/TOKEN-SAVINGS.md). Options: --json, --hub <url>.
51 propose <path> Create a proposal from local vault note (body/frontmatter) on the Hub. Options: --hub, --intent, --vault (X-Vault-Id), --external-ref, --labels a,b, --source agent|human|import, --base-state-id, --no-fetch-base.
52 vault sync Commit and push vault to Git (when vault.git.enabled and remote set). See config.
53 mcp Start MCP server (stdio transport). For Cursor/Claude Desktop.
54 daemon <action> Background consolidation daemon: start [--background], stop, status, log.
55
56 Options (global):
57 --help, -h Show this help or command-specific help.
58 --json Output JSON for piping to other tools.
59
60 Config: config/local.yaml or env (KNOWTATION_VAULT_PATH). Full spec: docs/SPEC.md.
61 `;
62
63 function getOpt(name, type = 'string') {
64 const i = args.indexOf('--' + name);
65 if (i === -1 || !args[i + 1]) return null;
66 const v = args[i + 1];
67 return type === 'number' ? parseInt(v, 10) : v;
68 }
69
70 function hasOpt(name) {
71 return args.includes('--' + name);
72 }
73
74 function runGetNote() {
75 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
76 if (!pathArg) {
77 exitWithError('knowtation get-note: provide a note path.', 1, useJson);
78 }
79 const bodyOnly = hasOpt('body-only');
80 const frontmatterOnly = hasOpt('frontmatter-only');
81 if (bodyOnly && frontmatterOnly) {
82 exitWithError('knowtation get-note: use only one of --body-only or --frontmatter-only.', 1, useJson);
83 }
84
85 let config;
86 try {
87 config = loadConfig();
88 } catch (e) {
89 exitWithError(e.message, 2, useJson);
90 }
91
92 try {
93 resolveVaultRelativePath(config.vault_path, pathArg);
94 } catch (e) {
95 exitWithError(e.message, 2, useJson);
96 }
97
98 let note;
99 try {
100 note = readNote(config.vault_path, pathArg);
101 } catch (e) {
102 exitWithError(e.message, 2, useJson);
103 }
104
105 if (useJson) {
106 if (bodyOnly) {
107 console.log(JSON.stringify({ path: note.path, body: note.body }));
108 } else if (frontmatterOnly) {
109 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter }));
110 } else {
111 console.log(JSON.stringify({ path: note.path, frontmatter: note.frontmatter, body: note.body }));
112 }
113 } else {
114 if (bodyOnly) {
115 process.stdout.write(note.body + (note.body ? '\n' : ''));
116 } else if (frontmatterOnly) {
117 console.log(JSON.stringify(note.frontmatter, null, 2));
118 } else {
119 console.log('---');
120 console.log(yaml.dump(note.frontmatter).trimEnd());
121 console.log('---');
122 if (note.body) console.log(note.body);
123 }
124 }
125 process.exit(0);
126 }
127
128 function runGetNoteOutline() {
129 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
130 if (!pathArg) {
131 exitWithError('knowtation get-note-outline: provide a note path.', 1, useJson);
132 }
133 if (!useJson) {
134 exitWithError('knowtation get-note-outline: --json is required in this MVP.', 1, useJson);
135 }
136
137 let config;
138 try {
139 config = loadConfig();
140 } catch (e) {
141 exitWithError(e.message, 2, useJson);
142 }
143
144 try {
145 resolveVaultRelativePath(config.vault_path, pathArg);
146 } catch (e) {
147 exitWithError(e.message, 2, useJson);
148 }
149
150 let note;
151 try {
152 note = readNote(config.vault_path, pathArg);
153 } catch (e) {
154 exitWithError(e.message, 2, useJson);
155 }
156
157 try {
158 console.log(JSON.stringify(buildNoteOutline(note)));
159 } catch (e) {
160 exitWithError(e.message, 2, useJson);
161 }
162 process.exit(0);
163 }
164
165 function runGetDocumentTree() {
166 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
167 if (!pathArg) {
168 exitWithError('knowtation get-document-tree: provide a note path.', 1, useJson);
169 }
170 if (!useJson) {
171 exitWithError('knowtation get-document-tree: --json is required in this MVP.', 1, useJson);
172 }
173
174 let config;
175 try {
176 config = loadConfig();
177 } catch (e) {
178 exitWithError(e.message, 2, useJson);
179 }
180
181 try {
182 resolveVaultRelativePath(config.vault_path, pathArg);
183 } catch (e) {
184 exitWithError(e.message, 2, useJson);
185 }
186
187 let note;
188 try {
189 note = readNote(config.vault_path, pathArg);
190 } catch (e) {
191 exitWithError(e.message, 2, useJson);
192 }
193
194 try {
195 console.log(JSON.stringify(buildDocumentTree(note)));
196 } catch (e) {
197 exitWithError(e.message, 2, useJson);
198 }
199 process.exit(0);
200 }
201
202 function runGetMetadataFacets() {
203 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
204 if (!pathArg) {
205 exitWithError('knowtation get-metadata-facets: provide a note path.', 1, useJson);
206 }
207 if (!useJson) {
208 exitWithError('knowtation get-metadata-facets: --json is required.', 1, useJson);
209 }
210
211 let config;
212 try {
213 config = loadConfig();
214 } catch (e) {
215 exitWithError(e.message, 2, useJson);
216 }
217
218 try {
219 resolveVaultRelativePath(config.vault_path, pathArg);
220 } catch (e) {
221 exitWithError(e.message, 2, useJson);
222 }
223
224 let note;
225 try {
226 note = readNote(config.vault_path, pathArg);
227 } catch (e) {
228 exitWithError(e.message, 2, useJson);
229 }
230
231 try {
232 console.log(JSON.stringify(normalizeMetadataFacets(note.path, note.frontmatter)));
233 } catch (e) {
234 exitWithError(e.message, 2, useJson);
235 }
236 process.exit(0);
237 }
238
239 function runGetSectionSource() {
240 const pathArgs = args.filter((a, i) => i >= 1 && !a.startsWith('--'));
241 const pathArg = pathArgs[0];
242 if (!pathArg) {
243 exitWithError('knowtation get-section-source: provide a note path.', 1, useJson);
244 }
245 if (pathArgs.length > 1) {
246 exitWithError('knowtation get-section-source: provide exactly one note path.', 1, useJson);
247 }
248 if (!useJson) {
249 exitWithError('knowtation get-section-source: --json is required.', 1, useJson);
250 }
251
252 let config;
253 try {
254 config = loadConfig();
255 } catch (e) {
256 exitWithError(e.message, 2, useJson);
257 }
258
259 try {
260 console.log(JSON.stringify(readSectionSource(config.vault_path, pathArg)));
261 } catch (e) {
262 exitWithError(e.message, 2, useJson);
263 }
264 process.exit(0);
265 }
266
267 function runListNotes() {
268 const folder = getOpt('folder');
269 const project = getOpt('project');
270 const tag = getOpt('tag');
271 const since = getOpt('since');
272 const until = getOpt('until');
273 const chain = getOpt('chain');
274 const entity = getOpt('entity');
275 const episode = getOpt('episode');
276 let limit = getOpt('limit', 'number') ?? 20;
277 let offset = getOpt('offset', 'number') ?? 0;
278 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
279 exitWithError('knowtation list-notes: --limit must be between 0 and 100.', 1, useJson);
280 }
281 if (typeof offset === 'number' && offset < 0) {
282 exitWithError('knowtation list-notes: --offset must be non-negative.', 1, useJson);
283 }
284 limit = Math.min(100, Math.max(0, limit ?? 20));
285 offset = Math.max(0, offset ?? 0);
286 const order = getOpt('order') || 'date';
287 const fields = getOpt('fields') || 'path+metadata';
288 const countOnly = hasOpt('count-only');
289
290 let config;
291 try {
292 config = loadConfig();
293 } catch (e) {
294 exitWithError(e.message, 2, useJson);
295 }
296
297 const out = runListNotesOp(config, {
298 folder: folder ?? undefined,
299 project: project ?? undefined,
300 tag: tag ?? undefined,
301 since: since ?? undefined,
302 until: until ?? undefined,
303 chain: chain ?? undefined,
304 entity: entity ?? undefined,
305 episode: episode ?? undefined,
306 limit,
307 offset,
308 order,
309 fields,
310 countOnly,
311 });
312
313 if (countOnly) {
314 if (useJson) {
315 console.log(JSON.stringify({ total: out.total }));
316 } else {
317 console.log(out.total);
318 }
319 process.exit(0);
320 }
321
322 if (useJson) {
323 console.log(JSON.stringify({ notes: out.notes, total: out.total }));
324 } else {
325 for (const n of out.notes) {
326 const meta = [n.project, n.tags?.join?.(', ') ?? (n.tags || []).join(', '), n.date].filter(Boolean).join(' | ');
327 console.log(n.path + (meta ? ` ${meta}` : ''));
328 }
329 }
330 process.exit(0);
331 }
332
333 async function main() {
334 if (!subcommand || subcommand === '--help' || subcommand === '-h') {
335 console.log(help.trim());
336 process.exit(0);
337 }
338
339 if (subcommand === 'get-note') {
340 if (hasOpt('help') || hasOpt('h')) {
341 console.log('knowtation get-note <path>\n Options: --json, --body-only, --frontmatter-only');
342 process.exit(0);
343 }
344 runGetNote();
345 }
346
347 if (subcommand === 'get-note-outline') {
348 if (hasOpt('help') || hasOpt('h')) {
349 console.log('knowtation get-note-outline <path>\n Options: --json (required)');
350 process.exit(0);
351 }
352 runGetNoteOutline();
353 }
354
355 if (subcommand === 'get-document-tree') {
356 if (hasOpt('help') || hasOpt('h')) {
357 console.log('knowtation get-document-tree <path>\n Options: --json (required)');
358 process.exit(0);
359 }
360 runGetDocumentTree();
361 }
362
363 if (subcommand === 'get-metadata-facets') {
364 if (hasOpt('help') || hasOpt('h')) {
365 console.log('knowtation get-metadata-facets <path>\n Options: --json (required)');
366 process.exit(0);
367 }
368 runGetMetadataFacets();
369 }
370
371 if (subcommand === 'get-section-source') {
372 if (hasOpt('help') || hasOpt('h')) {
373 console.log('knowtation get-section-source <path>\n Options: --json (required)');
374 process.exit(0);
375 }
376 runGetSectionSource();
377 }
378
379 if (subcommand === 'list-notes') {
380 if (hasOpt('help') || hasOpt('h')) {
381 console.log('knowtation list-notes\n Options: --folder, --project, --tag, --since, --until, --chain, --entity, --episode, --limit, --offset, --order date|date-asc, --fields path|path+metadata|full, --count-only, --json');
382 process.exit(0);
383 }
384 runListNotes();
385 }
386
387 if (subcommand === 'search') {
388 if (hasOpt('help') || hasOpt('h')) {
389 console.log('knowtation search <query>\n Options: --keyword (substring/token search), --match phrase|all-terms (with --keyword), --folder, --project, --tag, --since, --until, --chain, --entity, --episode, --content-scope all|notes|approval_logs, --order date|date-asc, --limit, --fields path|path+snippet|full, --snippet-chars <n>, --count-only, --json');
390 process.exit(0);
391 }
392 const query = args.slice(1).filter((a) => !a.startsWith('--')).join(' ').trim();
393 if (!query) {
394 exitWithError('knowtation search: provide a query string.', 1, useJson);
395 }
396 const folder = getOpt('folder');
397 const project = getOpt('project');
398 const tag = getOpt('tag');
399 const since = getOpt('since');
400 const until = getOpt('until');
401 const chain = getOpt('chain');
402 const entity = getOpt('entity');
403 const episode = getOpt('episode');
404 const order = getOpt('order');
405 let limit = getOpt('limit', 'number') ?? 10;
406 if (typeof limit === 'number' && (limit < 0 || limit > 100)) {
407 exitWithError('knowtation search: --limit must be between 0 and 100.', 1, useJson);
408 }
409 limit = Math.min(100, Math.max(0, limit ?? 10));
410 const fields = getOpt('fields') || 'path+snippet';
411 const snippetChars = getOpt('snippet-chars', 'number');
412 const countOnly = hasOpt('count-only');
413 const useKeyword = hasOpt('keyword');
414 const matchRaw = getOpt('match');
415 const contentScope = getOpt('content-scope');
416 const validFields = ['path', 'path+snippet', 'full'];
417 if (fields && !validFields.includes(fields)) {
418 exitWithError(`knowtation search: --fields must be one of ${validFields.join(', ')}.`, 1, useJson);
419 }
420 if (matchRaw && !useKeyword) {
421 exitWithError('knowtation search: --match is only valid with --keyword.', 1, useJson);
422 }
423 let match = 'phrase';
424 if (matchRaw) {
425 if (matchRaw === 'all-terms' || matchRaw === 'all_terms') match = 'all_terms';
426 else if (matchRaw === 'phrase') match = 'phrase';
427 else exitWithError('knowtation search: --match must be phrase or all-terms.', 1, useJson);
428 }
429 const validScopes = ['all', 'notes', 'approval_logs'];
430 if (contentScope && !validScopes.includes(contentScope)) {
431 exitWithError(`knowtation search: --content-scope must be one of ${validScopes.join(', ')}.`, 1, useJson);
432 }
433 (async () => {
434 try {
435 const config = loadConfig();
436 const baseOpts = {
437 folder: folder ?? undefined,
438 project: project ?? undefined,
439 tag: tag ?? undefined,
440 since: since ?? undefined,
441 until: until ?? undefined,
442 chain: chain ?? undefined,
443 entity: entity ?? undefined,
444 episode: episode ?? undefined,
445 order: order ?? undefined,
446 limit,
447 fields: fields || 'path+snippet',
448 snippetChars: snippetChars ?? 300,
449 countOnly,
450 content_scope: contentScope === 'all' ? undefined : contentScope ?? undefined,
451 };
452 let out;
453 if (useKeyword) {
454 const { runKeywordSearch } = await import('../lib/keyword-search.mjs');
455 out = await runKeywordSearch(query, { ...baseOpts, match }, config);
456 } else {
457 const { runSearch } = await import('../lib/search.mjs');
458 out = await runSearch(query, baseOpts, config);
459 }
460 if (config.memory?.enabled) {
461 try {
462 const { createMemoryManager } = await import('../lib/memory.mjs');
463 const mm = createMemoryManager(config);
464 if (mm.shouldCapture('search')) {
465 mm.store('search', {
466 query: out.query,
467 mode: useKeyword ? 'keyword' : 'semantic',
468 paths: (out.results || []).map((r) => r.path),
469 count: out.count ?? (out.results || []).length,
470 });
471 }
472 } catch (_) {}
473 }
474 if (useJson) {
475 console.log(JSON.stringify(out));
476 } else {
477 if (out.count !== undefined) {
478 console.log(out.count);
479 } else {
480 const list = out.results || [];
481 for (const r of list) {
482 const meta = [r.project, r.tags?.join(', ')].filter(Boolean).join(' | ');
483 const line = r.snippet != null ? `${r.path}\t${r.snippet}` : r.path;
484 console.log(line + (meta ? ` ${meta}` : ''));
485 }
486 }
487 }
488 process.exit(0);
489 } catch (e) {
490 exitWithError(e.message || String(e), 2, useJson);
491 }
492 })();
493 return;
494 }
495
496 if (subcommand === 'index') {
497 if (hasOpt('help') || hasOpt('h')) {
498 console.log('knowtation index\n Re-run indexer: vault → chunk → embed → vector store. Reads config; exit 0 on success, 2 on failure.');
499 process.exit(0);
500 }
501 const { runIndex } = await import('../lib/indexer.mjs');
502 try {
503 const t0 = Date.now();
504 const result = await runIndex();
505 const config = loadConfig();
506 if (config.memory?.enabled) {
507 try {
508 const { createMemoryManager } = await import('../lib/memory.mjs');
509 const mm = createMemoryManager(config);
510 if (mm.shouldCapture('index')) {
511 mm.store('index', {
512 notes_processed: result.notesProcessed,
513 chunks_indexed: result.chunksIndexed,
514 duration_ms: Date.now() - t0,
515 });
516 }
517 } catch (_) {}
518 }
519 if (useJson) {
520 console.log(JSON.stringify({ ok: true, notesProcessed: result.notesProcessed, chunksIndexed: result.chunksIndexed }));
521 }
522 process.exit(0);
523 } catch (e) {
524 exitWithError(e.message, 2, useJson);
525 }
526 }
527
528 if (subcommand === 'write') {
529 if (hasOpt('help') || hasOpt('h')) {
530 console.log('knowtation write <path> [content]\n Options: --stdin (body from stdin), --frontmatter k=v [k2=v2 ...], --append, --json');
531 process.exit(0);
532 }
533 const pathArg = args.find((a, i) => i >= 1 && !a.startsWith('--'));
534 if (!pathArg) {
535 exitWithError('knowtation write: provide a note path.', 1, useJson);
536 }
537 const stdin = hasOpt('stdin');
538 const append = hasOpt('append');
539 const frontmatterPairs = [];
540 for (let i = 0; i < args.length; i++) {
541 if (args[i] === '--frontmatter' && args[i + 1]) {
542 let j = i + 1;
543 while (j < args.length && !args[j].startsWith('--') && args[j].includes('=')) {
544 frontmatterPairs.push(args[j]);
545 j++;
546 }
547 break;
548 }
549 }
550 const frontmatterOverrides = {};
551 for (const p of frontmatterPairs) {
552 const eq = p.indexOf('=');
553 if (eq > 0) {
554 frontmatterOverrides[p.slice(0, eq).trim()] = p.slice(eq + 1).trim();
555 }
556 }
557 let body;
558 if (stdin) {
559 body = fs.readFileSync(0, 'utf8');
560 } else {
561 const contentArg = args[args.indexOf(pathArg) + 1];
562 body = contentArg && !contentArg.startsWith('--') ? contentArg : undefined;
563 }
564 let config;
565 try {
566 config = loadConfig();
567 } catch (e) {
568 exitWithError(e.message, 2, useJson);
569 }
570 (async () => {
571 try {
572 const { writeNote } = await import('../lib/write.mjs');
573 const result = await writeNote(config.vault_path, pathArg, {
574 body,
575 frontmatter: Object.keys(frontmatterOverrides).length ? frontmatterOverrides : undefined,
576 append,
577 config,
578 });
579 try {
580 const { maybeAutoSync } = await import('../lib/vault-git-sync.mjs');
581 maybeAutoSync(config);
582 } catch (_) {}
583 if (config.memory?.enabled) {
584 try {
585 const { createMemoryManager } = await import('../lib/memory.mjs');
586 const mm = createMemoryManager(config);
587 if (mm.shouldCapture('write')) {
588 mm.store('write', {
589 path: result.path,
590 action: append ? 'append' : 'create',
591 air_id: result.air_id || undefined,
592 });
593 }
594 } catch (_) {}
595 }
596 if (useJson) {
597 console.log(JSON.stringify(result));
598 } else {
599 console.log(`Written: ${result.path}`);
600 }
601 process.exit(0);
602 } catch (e) {
603 exitWithError(e.message, 2, useJson);
604 }
605 })();
606 return;
607 }
608
609 if (subcommand === 'export') {
610 if (hasOpt('help') || hasOpt('h')) {
611 console.log('knowtation export <path-or-query> <output-dir-or-file>\n Options: --format md|html, --project <slug>, --json');
612 process.exit(0);
613 }
614 const pathOrQuery = args[1];
615 const output = args[2];
616 if (!pathOrQuery || !output) {
617 exitWithError('knowtation export: provide <path-or-query> and <output-dir-or-file>.', 1, useJson);
618 }
619 const format = getOpt('format') || 'md';
620 const project = getOpt('project');
621 if (format && !['md', 'html'].includes(format)) {
622 exitWithError('knowtation export: --format must be md or html.', 1, useJson);
623 }
624 let config;
625 try {
626 config = loadConfig();
627 } catch (e) {
628 exitWithError(e.message, 2, useJson);
629 }
630 (async () => {
631 try {
632 const { exportNotes } = await import('../lib/export.mjs');
633 const { attestBeforeExport } = await import('../lib/air.mjs');
634 let paths = [];
635 const looksLikePath = !pathOrQuery.includes(' ') && (pathOrQuery.endsWith('.md') || pathOrQuery.includes('/'));
636 if (looksLikePath) {
637 try {
638 resolveVaultRelativePath(config.vault_path, pathOrQuery);
639 paths = [pathOrQuery];
640 } catch (_) {
641 // Fall through: treat as query
642 }
643 }
644 if (paths.length === 0) {
645 const { runSearch } = await import('../lib/search.mjs');
646 const result = await runSearch(pathOrQuery, {
647 limit: 50,
648 project: project ?? undefined,
649 fields: 'path',
650 });
651 paths = (result.results || []).map((r) => r.path).filter(Boolean);
652 }
653 if (!paths.length) {
654 exitWithError('knowtation export: no notes found for path or query.', 2, useJson);
655 }
656 if (config.air?.enabled) {
657 await attestBeforeExport(config, paths);
658 }
659 const result = exportNotes(config.vault_path, paths, output, { format });
660 if (config.memory?.enabled) {
661 try {
662 const { createMemoryManager } = await import('../lib/memory.mjs');
663 const mm = createMemoryManager(config);
664 if (mm.shouldCapture('export')) {
665 mm.store('export', { provenance: result.provenance, exported: result.exported, format });
666 }
667 } catch (_) {}
668 }
669 if (useJson) {
670 console.log(JSON.stringify({ exported: result.exported, provenance: result.provenance }));
671 } else {
672 for (const e of result.exported) {
673 console.log(`${e.path} → ${e.output}`);
674 }
675 if (result.provenance) console.log(result.provenance);
676 }
677 process.exit(0);
678 } catch (e) {
679 exitWithError(e.message, 2, useJson);
680 }
681 })();
682 return;
683 }
684
685 if (subcommand === 'import') {
686 if (hasOpt('help') || hasOpt('h')) {
687 console.log(
688 `knowtation import <source-type> <input>\n Options: --project, --output-dir, --tags t1,t2, --dry-run, --json, --sheets-range 'A1 range' (google-sheets only), --url-mode auto|bookmark|extract (url only)\n Source types: ${IMPORT_SOURCE_TYPES_HELP}`
689 );
690 process.exit(0);
691 }
692 const sourceType = args[1];
693 const input = args[2];
694 if (!sourceType) {
695 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
696 }
697 if (sourceType !== 'google-sheets' && !input) {
698 exitWithError('knowtation import: provide <source-type> and <input>. See docs/IMPORT-SOURCES.md.', 1, useJson);
699 }
700 if (sourceType === 'google-sheets' && !input) {
701 exitWithError(
702 'knowtation import google-sheets: provide the spreadsheet id as <input> (the id from the Google Sheets URL).',
703 1,
704 useJson,
705 );
706 }
707 if (!IMPORT_SOURCE_TYPES.includes(sourceType)) {
708 exitWithError(`Unknown source-type "${sourceType}". Valid: ${IMPORT_SOURCE_TYPES_HELP}.`, 1, useJson);
709 }
710 (async () => {
711 try {
712 const config = loadConfig();
713 const { runImport } = await import('../lib/import.mjs');
714 const project = getOpt('project');
715 const outputDir = getOpt('output-dir');
716 const tagsOpt = getOpt('tags');
717 const tags = tagsOpt ? tagsOpt.split(',').map((t) => t.trim()).filter(Boolean) : [];
718 const dryRun = hasOpt('dry-run');
719 let memoryManager;
720 if (config.memory?.enabled && !dryRun) {
721 try {
722 const { createMemoryManager } = await import('../lib/memory.mjs');
723 memoryManager = createMemoryManager(config);
724 } catch (_) {}
725 }
726
727 const urlModeRaw = getOpt('url-mode');
728 let urlMode;
729 if (urlModeRaw) {
730 const v = String(urlModeRaw).trim().toLowerCase();
731 if (v !== 'auto' && v !== 'bookmark' && v !== 'extract') {
732 exitWithError(`Invalid --url-mode "${urlModeRaw}". Use auto, bookmark, or extract.`, 1, useJson);
733 }
734 urlMode = v;
735 }
736 if (urlModeRaw && sourceType !== 'url') {
737 exitWithError('--url-mode is only valid when source-type is url.', 1, useJson);
738 }
739 const sheetsRangeRaw = getOpt('sheets-range');
740 if (sheetsRangeRaw && sourceType !== 'google-sheets') {
741 exitWithError('--sheets-range is only valid when source-type is google-sheets.', 1, useJson);
742 }
743
744 const importOpts = {
745 project: project ?? undefined,
746 outputDir: outputDir ?? undefined,
747 tags,
748 dryRun,
749 ...(sourceType === 'url' && urlMode ? { urlMode } : {}),
750 ...(sourceType === 'google-sheets' && sheetsRangeRaw
751 ? { sheetsRange: String(sheetsRangeRaw).trim() }
752 : {}),
753 };
754 if (memoryManager && sourceType === 'mem0-export' && memoryManager.shouldCapture('capture')) {
755 importOpts.onMemoryEvent = (data) => {
756 try { memoryManager.store('capture', data); } catch (_) {}
757 };
758 }
759
760 const result = await runImport(sourceType, input, importOpts);
761 if (memoryManager) {
762 try {
763 if (memoryManager.shouldCapture('import')) {
764 memoryManager.store('import', {
765 source_type: sourceType,
766 count: result.count ?? 0,
767 paths: (result.imported || []).map((r) => r.path).slice(0, 50),
768 project: project ?? undefined,
769 });
770 }
771 } catch (_) {}
772 }
773 if (useJson) {
774 console.log(JSON.stringify({ imported: result.imported, count: result.count }));
775 } else {
776 for (const r of result.imported) {
777 console.log(r.path);
778 }
779 if (result.count === 0) {
780 console.log('No notes imported.');
781 } else {
782 console.log(`Imported ${result.count} note(s).`);
783 }
784 }
785 process.exit(0);
786 } catch (e) {
787 exitWithError(e.message, 2, useJson);
788 }
789 })();
790 return;
791 }
792
793 if (subcommand === 'mcp') {
794 if (hasOpt('help') || hasOpt('h')) {
795 console.log(
796 'knowtation mcp\n Start MCP server (default: stdio for Cursor / Claude Desktop).\n Streamable HTTP: MCP_TRANSPORT=http or KNOWTATION_MCP_TRANSPORT=http (see docs/MCP-PHASE-D.md).\n Requires config/local.yaml and KNOWTATION_VAULT_PATH.'
797 );
798 process.exit(0);
799 }
800 const serverMod = await import('../mcp/server.mjs');
801 return;
802 }
803
804 if (subcommand === 'memory') {
805 const action = args[1];
806 if (hasOpt('help') || hasOpt('h')) {
807 console.log(`knowtation memory <action>
808 Actions:
809 query <key> Read latest value for an event type (e.g. search, export, write, import, index, propose, user).
810 list List recent memory events. --type, --topic, --since, --until, --limit (default 20), --json.
811 store <key> <value> Store a user-defined memory entry. Value is JSON string or --stdin.
812 search <query> Semantic search over memory (requires vector or mem0 provider). --limit, --json.
813 clear Clear memory. --type, --before <date>, --confirm required. --json.
814 export Export memory log. --format jsonl|mif, --since, --until, --type. Output to stdout.
815 stats Show memory statistics. --json.
816 index Print lightweight pointer index (markdown). --json returns structured object.
817 consolidate Run LLM-powered memory consolidation. --dry-run, --passes consolidate,verify,discover, --lookback-hours <n>. --json.
818
819 Options: --json`);
820 process.exit(0);
821 }
822 const validActions = ['query', 'list', 'store', 'search', 'clear', 'export', 'stats', 'index', 'consolidate'];
823 if (!action || !validActions.includes(action)) {
824 exitWithError(`knowtation memory: use "memory <action>". Actions: ${validActions.join(', ')}.`, 1, useJson);
825 }
826 let config;
827 try {
828 config = loadConfig();
829 } catch (e) {
830 exitWithError(e.message, 2, useJson);
831 }
832 if (!config.memory?.enabled) {
833 exitWithError('knowtation memory: memory layer not enabled. Set memory.enabled in config.', 2, useJson);
834 }
835 (async () => {
836 try {
837 const { createMemoryManager } = await import('../lib/memory.mjs');
838 const { MEMORY_EVENT_TYPES } = await import('../lib/memory-event.mjs');
839 const scopeOpt = getOpt('scope') === 'global' ? 'global' : undefined;
840 const mm = createMemoryManager(config, 'default', scopeOpt ? { scope: scopeOpt } : {});
841
842 if (action === 'query') {
843 const keyArg = args[2];
844 if (!keyArg) {
845 exitWithError('knowtation memory query: provide a key (event type).', 1, useJson);
846 }
847 const key = keyArg.replace(/\s+/g, '_');
848 const latest = mm.getLatest(key);
849 if (!latest) {
850 if (useJson) console.log(JSON.stringify({ key, value: null }));
851 else console.log('(no value)');
852 } else {
853 const { id: _id, vault_id: _vid, ...display } = latest;
854 if (useJson) console.log(JSON.stringify({ key, value: display }));
855 else console.log(JSON.stringify(display, null, 2));
856 }
857 process.exit(0);
858 }
859
860 if (action === 'list') {
861 const type = getOpt('type');
862 const topic = getOpt('topic');
863 const since = getOpt('since');
864 const until = getOpt('until');
865 const limit = getOpt('limit', 'number') ?? 20;
866 const events = mm.list({ type: type ?? undefined, topic: topic ?? undefined, since: since ?? undefined, until: until ?? undefined, limit });
867 if (useJson) {
868 console.log(JSON.stringify({ events, count: events.length }));
869 } else {
870 if (events.length === 0) console.log('(no events)');
871 for (const e of events) {
872 const summary = JSON.stringify(e.data).slice(0, 120);
873 console.log(`${e.ts} ${e.type} ${summary}`);
874 }
875 }
876 process.exit(0);
877 }
878
879 if (action === 'store') {
880 const keyArg = args[2];
881 if (!keyArg) {
882 exitWithError('knowtation memory store: provide a key.', 1, useJson);
883 }
884 let valueRaw;
885 if (hasOpt('stdin')) {
886 valueRaw = fs.readFileSync(0, 'utf8').trim();
887 } else {
888 valueRaw = args[3];
889 }
890 if (!valueRaw) {
891 exitWithError('knowtation memory store: provide a value (JSON string) or --stdin.', 1, useJson);
892 }
893 let value;
894 try {
895 value = JSON.parse(valueRaw);
896 } catch (_) {
897 value = { text: valueRaw };
898 }
899 const result = mm.store('user', { key: keyArg, ...value });
900 if (useJson) console.log(JSON.stringify(result));
901 else console.log(`Stored: ${result.id}`);
902 process.exit(0);
903 }
904
905 if (action === 'search') {
906 const query = args.slice(2).filter((a) => !a.startsWith('--')).join(' ').trim();
907 if (!query) {
908 exitWithError('knowtation memory search: provide a query string.', 1, useJson);
909 }
910 if (!mm.supportsSearch()) {
911 exitWithError('knowtation memory search: semantic search requires memory.provider: vector or mem0.', 2, useJson);
912 }
913 const limit = getOpt('limit', 'number') ?? 10;
914 const results = mm.search(query, { limit });
915 if (useJson) {
916 console.log(JSON.stringify({ results, count: results.length }));
917 } else {
918 if (results.length === 0) console.log('(no results)');
919 for (const r of results) {
920 console.log(`${r.ts} ${r.type} ${JSON.stringify(r.data).slice(0, 120)}`);
921 }
922 }
923 process.exit(0);
924 }
925
926 if (action === 'clear') {
927 if (!hasOpt('confirm')) {
928 exitWithError('knowtation memory clear: use --confirm to confirm deletion.', 1, useJson);
929 }
930 const type = getOpt('type');
931 const before = getOpt('before');
932 const result = mm.clear({ type: type ?? undefined, before: before ?? undefined });
933 if (useJson) console.log(JSON.stringify(result));
934 else console.log(`Cleared ${result.cleared} event(s).`);
935 process.exit(0);
936 }
937
938 if (action === 'export') {
939 const format = getOpt('format') || 'jsonl';
940 if (!['jsonl', 'mif'].includes(format)) {
941 exitWithError('knowtation memory export: --format must be jsonl or mif.', 1, useJson);
942 }
943 const type = getOpt('type');
944 const since = getOpt('since');
945 const until = getOpt('until');
946 const events = mm.list({ type: type ?? undefined, since: since ?? undefined, until: until ?? undefined, limit: 10000 });
947 if (format === 'jsonl') {
948 for (const e of events) {
949 console.log(JSON.stringify(e));
950 }
951 } else {
952 for (const e of events) {
953 console.log(`---`);
954 console.log(`id: ${e.id}`);
955 console.log(`type: ${e.type}`);
956 console.log(`ts: ${e.ts}`);
957 console.log(`vault_id: ${e.vault_id}`);
958 console.log(`---`);
959 console.log(JSON.stringify(e.data, null, 2));
960 console.log('');
961 }
962 }
963 process.exit(0);
964 }
965
966 if (action === 'summarize') {
967 const since = getOpt('since') || new Date(Date.now() - 86_400_000).toISOString();
968 const maxTokens = getOpt('max-tokens', 'number') ?? 512;
969 const dryRun = hasOpt('dry-run');
970 try {
971 const { generateSessionSummary } = await import('../lib/memory-session-summary.mjs');
972 const result = await generateSessionSummary(config, { since, maxTokens, dryRun });
973 if (useJson) {
974 console.log(JSON.stringify(result));
975 } else {
976 console.log(result.summary);
977 if (result.id) console.log(`\nStored as: ${result.id}`);
978 console.log(`Events summarized: ${result.event_count}`);
979 }
980 } catch (e) {
981 exitWithError(`Session summary failed: ${e.message}`, 2, useJson);
982 }
983 process.exit(0);
984 }
985
986 if (action === 'consolidate') {
987 const dryRun = hasOpt('dry-run');
988 const passesRaw = getOpt('passes', 'string');
989 const passes = passesRaw
990 ? passesRaw.split(',').map((s) => s.trim()).filter(Boolean)
991 : undefined;
992 const lookbackHours = getOpt('lookback-hours', 'number') ?? undefined;
993 try {
994 const { consolidateMemory } = await import('../lib/memory-consolidate.mjs');
995 const result = await consolidateMemory(config, { dryRun, passes, lookbackHours });
996 if (useJson) {
997 console.log(JSON.stringify(result));
998 } else if (result.dry_run) {
999 console.log(`[dry-run] Would process ${result.total_events} events across ${result.topics.length} topics.`);
1000 for (const t of result.topics) {
1001 console.log(`[dry-run] Topic "${t.topic}": ${t.event_count} events → ${t.dry_run_estimate || 'estimated facts'}`);
1002 }
1003 if (result.verify) {
1004 console.log(`[dry-run] Verify pass: would check paths in events (no writes).`);
1005 }
1006 if (result.discover) {
1007 console.log(`[dry-run] Discover pass: would analyze ${result.discover.topic_count} topic(s) for cross-topic insights (no writes).`);
1008 }
1009 } else if (result.topics.length === 0 && !result.verify && !result.discover) {
1010 console.log('No events to consolidate.');
1011 } else {
1012 if (result.topics.length > 0) {
1013 console.log(`Consolidated ${result.total_events} events across ${result.topics.length} topics.`);
1014 for (const t of result.topics) {
1015 if (t.error) {
1016 console.log(` ${t.topic}: error — ${t.error}`);
1017 } else {
1018 console.log(` ${t.topic}: ${t.facts.length} facts written${t.id ? ` (${t.id})` : ''}`);
1019 }
1020 }
1021 console.log('Index regenerated.');
1022 }
1023 if (result.verify) {
1024 const v = result.verify;
1025 console.log(`Verify pass: checked ${v.checked_count} events — ${v.verified_paths.length} verified, ${v.stale_paths.length} stale.`);
1026 if (v.stale_paths.length > 0) {
1027 for (const p of v.stale_paths) console.log(` stale: ${p}`);
1028 }
1029 }
1030 if (result.discover) {
1031 const d = result.discover;
1032 console.log(`Discover pass: ${d.connections.length} connection(s), ${d.contradictions.length} contradiction(s), ${d.open_questions.length} open question(s) across ${d.topic_count} topic(s).`);
1033 }
1034 }
1035 } catch (e) {
1036 exitWithError(`Consolidation failed: ${e.message}`, 2, useJson);
1037 }
1038 process.exit(0);
1039 }
1040
1041 if (action === 'index') {
1042 const idx = mm.generateIndex({ force: true });
1043 if (useJson) {
1044 console.log(JSON.stringify(idx));
1045 } else {
1046 console.log(idx.markdown);
1047 }
1048 process.exit(0);
1049 }
1050
1051 if (action === 'stats') {
1052 const stats = mm.stats();
1053 if (useJson) {
1054 console.log(JSON.stringify(stats));
1055 } else {
1056 console.log(`Total events: ${stats.total}`);
1057 console.log(`Storage: ${stats.size_bytes} bytes`);
1058 if (stats.oldest) console.log(`Oldest: ${stats.oldest}`);
1059 if (stats.newest) console.log(`Newest: ${stats.newest}`);
1060 if (Object.keys(stats.counts_by_type).length > 0) {
1061 console.log('Counts by type:');
1062 for (const [t, c] of Object.entries(stats.counts_by_type)) {
1063 console.log(` ${t}: ${c}`);
1064 }
1065 }
1066 }
1067 process.exit(0);
1068 }
1069 } catch (e) {
1070 exitWithError(e.message, 2, useJson);
1071 }
1072 })();
1073 return;
1074 }
1075
1076 if (subcommand === 'doctor') {
1077 if (hasOpt('help') || hasOpt('h')) {
1078 console.log(
1079 'knowtation doctor\n Checks local vault config (disk vault) and optional Hub API (KNOWTATION_HUB_*).\n Explains vault vs terminal token discipline per docs/TOKEN-SAVINGS.md.\n Options: --json, --hub <url> (override KNOWTATION_HUB_URL for probes only).'
1080 );
1081 process.exit(0);
1082 }
1083 const hubUrlOpt = getOpt('hub');
1084 const { runDoctor } = await import('./doctor.mjs');
1085 const code = await runDoctor({ useJson, hubUrlOpt });
1086 process.exit(code);
1087 }
1088
1089 if (subcommand === 'hub') {
1090 const action = args[1];
1091 if (action !== 'status') {
1092 exitWithError('knowtation hub: use "hub status". Option: --hub <url>.', 1, useJson);
1093 }
1094 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL || 'http://localhost:3333';
1095 const base = hubUrl.replace(/\/$/, '');
1096 (async () => {
1097 try {
1098 const res = await fetch(base + '/health', { method: 'GET' });
1099 const data = await res.json().catch(() => ({}));
1100 if (useJson) {
1101 console.log(JSON.stringify({ ok: res.ok, status: res.status, url: base }));
1102 } else {
1103 console.log(res.ok ? `Hub at ${base} is up.` : `Hub at ${base} returned ${res.status}.`);
1104 }
1105 process.exit(res.ok ? 0 : 2);
1106 } catch (e) {
1107 exitWithError('Hub unreachable: ' + e.message, 2, useJson);
1108 }
1109 })();
1110 return;
1111 }
1112
1113 if (subcommand === 'vault') {
1114 const vaultSub = args[1];
1115 if (vaultSub === 'sync') {
1116 if (hasOpt('help') || hasOpt('h')) {
1117 console.log('knowtation vault sync\n Commits and pushes the vault to the configured Git remote.\n Requires config: vault.git.enabled=true and vault.git.remote=<url>.');
1118 process.exit(0);
1119 }
1120 let config;
1121 try {
1122 config = loadConfig();
1123 } catch (e) {
1124 exitWithError(e.message, 2, useJson);
1125 }
1126 (async () => {
1127 try {
1128 const { runVaultSync } = await import('../lib/vault-git-sync.mjs');
1129 const result = runVaultSync(config);
1130 if (useJson) console.log(JSON.stringify(result));
1131 else console.log(result.message === 'Synced' ? 'Vault synced to remote.' : result.message);
1132 process.exit(0);
1133 } catch (e) {
1134 exitWithError('knowtation vault sync: ' + (e.message || 'git failed'), 1, useJson);
1135 }
1136 })();
1137 return;
1138 }
1139 exitWithError('knowtation vault: unknown subcommand. Use vault sync.', 1, useJson);
1140 }
1141
1142 if (subcommand === 'propose') {
1143 const pathArg = args[1];
1144 if (!pathArg || pathArg.startsWith('--')) {
1145 exitWithError('knowtation propose: provide a vault-relative note path (e.g. inbox/note.md).', 1, useJson);
1146 }
1147 const hubUrl = getOpt('hub') || process.env.KNOWTATION_HUB_URL;
1148 if (!hubUrl) {
1149 exitWithError('knowtation propose: set --hub <url> or KNOWTATION_HUB_URL.', 1, useJson);
1150 }
1151 let config;
1152 try {
1153 config = loadConfig();
1154 } catch (e) {
1155 exitWithError(e.message, 2, useJson);
1156 }
1157 try {
1158 resolveVaultRelativePath(config.vault_path, pathArg);
1159 } catch (e) {
1160 exitWithError(e.message, 2, useJson);
1161 }
1162 const intent = getOpt('intent') || '';
1163 const token = process.env.KNOWTATION_HUB_TOKEN;
1164 if (!token) {
1165 exitWithError('knowtation propose: set KNOWTATION_HUB_TOKEN (JWT from Hub login).', 2, useJson);
1166 }
1167 const base = hubUrl.replace(/\/$/, '');
1168 const vaultHdr = getOpt('vault') || process.env.KNOWTATION_HUB_VAULT_ID;
1169 const labelsRaw = getOpt('labels');
1170 const labels = labelsRaw
1171 ? labelsRaw
1172 .split(',')
1173 .map((s) => s.trim())
1174 .filter(Boolean)
1175 : undefined;
1176 const source = getOpt('source') || undefined;
1177 const externalRef = getOpt('external-ref') || undefined;
1178 const baseStateOverride = getOpt('base-state-id');
1179 const skipFetchBase = hasOpt('no-fetch-base');
1180
1181 let bodyText = '';
1182 let frontmatter = {};
1183 if (noteFileExistsInVault(config.vault_path, pathArg)) {
1184 const n = readNote(config.vault_path, pathArg);
1185 bodyText = n.body;
1186 frontmatter = n.frontmatter;
1187 }
1188
1189 (async () => {
1190 try {
1191 const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token };
1192 if (vaultHdr) headers['X-Vault-Id'] = vaultHdr;
1193
1194 let baseStateId = baseStateOverride && String(baseStateOverride).trim() ? String(baseStateOverride).trim() : '';
1195 if (!baseStateId && !skipFetchBase) {
1196 const encPath = pathArg.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/');
1197 const gres = await fetch(`${base}/api/v1/notes/${encPath}`, { method: 'GET', headers });
1198 if (gres.status === 404) {
1199 baseStateId = absentNoteStateId();
1200 } else if (gres.ok) {
1201 const noteJson = await gres.json();
1202 baseStateId = noteStateIdFromHubNoteJson(noteJson);
1203 }
1204 }
1205
1206 const payload = {
1207 path: pathArg.replace(/\\/g, '/'),
1208 body: bodyText,
1209 frontmatter,
1210 intent: intent || undefined,
1211 external_ref: externalRef || undefined,
1212 labels,
1213 source,
1214 };
1215 if (baseStateId) payload.base_state_id = baseStateId;
1216
1217 const res = await fetch(base + '/api/v1/proposals', {
1218 method: 'POST',
1219 headers,
1220 body: JSON.stringify(payload),
1221 });
1222 const data = await res.json().catch(() => ({}));
1223 if (!res.ok) {
1224 exitWithError(data.error || res.statusText, 2, useJson);
1225 return;
1226 }
1227 try {
1228 const config2 = loadConfig();
1229 if (config2.memory?.enabled) {
1230 const { createMemoryManager } = await import('../lib/memory.mjs');
1231 const mm = createMemoryManager(config2);
1232 if (mm.shouldCapture('propose')) {
1233 mm.store('propose', {
1234 proposal_id: data.proposal_id,
1235 path: data.path || pathArg,
1236 intent: intent || undefined,
1237 base_state_id: baseStateId || undefined,
1238 });
1239 }
1240 }
1241 } catch (_) {}
1242 if (useJson) console.log(JSON.stringify(data));
1243 else console.log('Proposal created:', data.proposal_id, data.path);
1244 process.exit(0);
1245 } catch (e) {
1246 exitWithError(e.message, 2, useJson);
1247 }
1248 })();
1249 return;
1250 }
1251
1252 if (subcommand === 'daemon') {
1253 const daemonAction = args[1];
1254
1255 if (!daemonAction || hasOpt('help') || hasOpt('h')) {
1256 console.log(`knowtation daemon <action>
1257 Actions:
1258 start [--background] Start the daemon. --background runs it detached (writes PID).
1259 stop Stop a running daemon (SIGTERM → SIGKILL after 10 s).
1260 status Show running state, PID, last pass, next scheduled pass.
1261 log [--tail <n>] Print daemon log entries (JSONL). --tail limits to last N.
1262
1263 Notes:
1264 - Daemon requires daemon.enabled in config and a reachable LLM.
1265 - Foreground mode: Ctrl+C to stop (SIGINT).
1266 - Background mode writes PID to {data_dir}/daemon.pid, log to {data_dir}/daemon.log.`);
1267 process.exit(0);
1268 }
1269
1270 let config;
1271 try {
1272 config = loadConfig();
1273 } catch (e) {
1274 exitWithError(e.message, 2, useJson);
1275 }
1276
1277 // ── daemon start ───────────────────────────────────────────────────────
1278 if (daemonAction === 'start') {
1279 const background = hasOpt('background');
1280
1281 if (background) {
1282 // Spawn a detached child that runs `knowtation daemon start` (foreground).
1283 // Use env var to prevent the child from re-entering background-spawn logic.
1284 const child = spawn(process.execPath, [__filename, 'daemon', 'start'], {
1285 detached: true,
1286 stdio: 'ignore',
1287 env: { ...process.env, KNOWTATION_DAEMON_BACKGROUND: '0' },
1288 });
1289 child.unref();
1290
1291 const pidPath = path.join(config.data_dir, 'daemon.pid');
1292 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
1293
1294 if (useJson) {
1295 console.log(JSON.stringify({ ok: true, pid: child.pid, pid_path: pidPath, log_path: logPath }));
1296 } else {
1297 const llmProvider = config.daemon?.llm?.provider || 'auto-detect';
1298 const llmModel = config.daemon?.llm?.model || 'default';
1299 console.log(`Daemon started in background (PID ${child.pid}). Consolidation every ${config.daemon?.interval_minutes ?? 120} min when idle.`);
1300 console.log(`LLM: ${llmProvider} ${llmModel}.`);
1301 console.log(`Log: ${logPath}`);
1302 }
1303 process.exit(0);
1304 return;
1305 }
1306
1307 // Foreground mode
1308 if (!config.daemon?.enabled && process.env.KNOWTATION_DAEMON_BACKGROUND !== '0') {
1309 console.warn('Warning: daemon.enabled is false in config. Starting anyway (foreground mode).');
1310 }
1311
1312 (async () => {
1313 try {
1314 const { startDaemon } = await import('../lib/daemon.mjs');
1315 const logPath = config.daemon?.log_file || path.join(config.data_dir, 'daemon.log');
1316 const intervalMin = config.daemon?.interval_minutes ?? 120;
1317 console.log(`Daemon starting (PID ${process.pid}). Consolidation every ${intervalMin} min when idle.`);
1318 console.log(`Log: ${logPath}. Press Ctrl+C to stop.`);
1319 await startDaemon(config);
1320 console.log('Daemon stopped.');
1321 process.exit(0);
1322 } catch (e) {
1323 exitWithError(`Daemon start failed: ${e.message}`, 2, useJson);
1324 }
1325 })();
1326 return;
1327 }
1328
1329 // ── daemon stop ────────────────────────────────────────────────────────
1330 if (daemonAction === 'stop') {
1331 (async () => {
1332 try {
1333 const { stopDaemon } = await import('../lib/daemon.mjs');
1334 const result = await stopDaemon(config);
1335 if (useJson) {
1336 console.log(JSON.stringify(result));
1337 } else if (result.stopped) {
1338 console.log(`Daemon stopped (PID ${result.pid}, signal ${result.signal}).`);
1339 } else {
1340 console.log(`Daemon was not running: ${result.reason}`);
1341 }
1342 process.exit(0);
1343 } catch (e) {
1344 exitWithError(`Daemon stop failed: ${e.message}`, 2, useJson);
1345 }
1346 })();
1347 return;
1348 }
1349
1350 // ── daemon status ──────────────────────────────────────────────────────
1351 if (daemonAction === 'status') {
1352 try {
1353 const { getDaemonStatus } = await import('../lib/daemon.mjs');
1354 const status = getDaemonStatus(config);
1355 if (useJson) {
1356 console.log(JSON.stringify(status));
1357 } else if (!status.running) {
1358 console.log('Status: not running');
1359 if (status.last_pass) {
1360 console.log(`Last pass: ${status.last_pass.ts} (${status.last_pass.events_processed} events, ${status.last_pass.topics} topics)`);
1361 }
1362 console.log(`Log: ${status.log_path}`);
1363 } else {
1364 const uptimeSec = Math.round((status.uptime_ms ?? 0) / 1000);
1365 const uptimeStr = uptimeSec < 60
1366 ? `${uptimeSec}s`
1367 : uptimeSec < 3600
1368 ? `${Math.round(uptimeSec / 60)}m ${uptimeSec % 60}s`
1369 : `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
1370 console.log(`Status: running (PID ${status.pid}, uptime ${uptimeStr})`);
1371 if (status.last_pass) {
1372 const lp = status.last_pass;
1373 console.log(`Last pass: ${lp.ts} (processed ${lp.events_processed} events, ${lp.topics} topics)`);
1374 } else {
1375 console.log('Last pass: none yet');
1376 }
1377 if (status.next_pass_at) {
1378 console.log(`Next pass: ~${status.next_pass_at} (if idle)`);
1379 }
1380 }
1381 process.exit(0);
1382 } catch (e) {
1383 exitWithError(`Daemon status failed: ${e.message}`, 2, useJson);
1384 }
1385 return;
1386 }
1387
1388 // ── daemon log ─────────────────────────────────────────────────────────
1389 if (daemonAction === 'log') {
1390 const tail = getOpt('tail', 'number') ?? null;
1391 try {
1392 const { getLogPath, readDaemonLog } = await import('../lib/daemon.mjs');
1393 const logPath = getLogPath(config);
1394 const entries = readDaemonLog(logPath, { tail: tail ?? undefined });
1395 if (useJson) {
1396 console.log(JSON.stringify({ entries, count: entries.length, log_path: logPath }));
1397 } else if (entries.length === 0) {
1398 console.log(`(no log entries — log: ${logPath})`);
1399 } else {
1400 for (const e of entries) {
1401 const { ts, event, ...rest } = e;
1402 const detail = Object.keys(rest).length ? ' ' + JSON.stringify(rest) : '';
1403 console.log(`${ts} ${event ?? '?'}${detail}`);
1404 }
1405 }
1406 process.exit(0);
1407 } catch (e) {
1408 exitWithError(`Daemon log failed: ${e.message}`, 2, useJson);
1409 }
1410 return;
1411 }
1412
1413 exitWithError(`knowtation daemon: unknown action "${daemonAction}". Use start, stop, status, or log.`, 1, useJson);
1414 return;
1415 }
1416
1417 exitWithError(`Unknown command: ${subcommand}`, 1, useJson);
1418 }
1419
1420 main().catch((e) => {
1421 console.error(e.message || e);
1422 process.exit(2);
1423 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago