capture-file.mjs
105 lines 3.4 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 /**
3 * File-based capture plugin. Writes content to vault inbox per CAPTURE-CONTRACT.
4 * Use from cron, scripts, or piping.
5 *
6 * Usage:
7 * echo "Meeting notes" | node scripts/capture-file.mjs --source file --source-id meeting-001
8 * node scripts/capture-file.mjs --file /path/to/note.md --source file --project myproject
9 *
10 * Options:
11 * --source <id> Interface id (default: file)
12 * --source-id <id> External id for dedup; uses inbox/{source}_{id}.md (idempotent overwrite)
13 * --project <slug> Write to projects/<slug>/inbox/ instead of global inbox
14 * --tags <tags> Comma-separated tags for frontmatter
15 * --file <path> Read body from file; otherwise stdin
16 *
17 * Config: config/local.yaml or env KNOWTATION_VAULT_PATH.
18 */
19
20 import fs from 'fs';
21 import path from 'path';
22 import { fileURLToPath } from 'url';
23 import { loadConfig } from '../lib/config.mjs';
24 import { writeNote } from '../lib/write.mjs';
25 import { normalizeSlug } from '../lib/vault.mjs';
26
27 const __dirname = path.dirname(fileURLToPath(import.meta.url));
28 const projectRoot = path.resolve(__dirname, '..');
29
30 function parseArgs() {
31 const args = process.argv.slice(2);
32 const opts = { source: 'file', sourceId: null, project: null, tags: null, file: null };
33 for (let i = 0; i < args.length; i++) {
34 if (args[i] === '--source' && args[i + 1]) {
35 opts.source = args[++i];
36 } else if (args[i] === '--source-id' && args[i + 1]) {
37 opts.sourceId = args[++i];
38 } else if (args[i] === '--project' && args[i + 1]) {
39 opts.project = args[++i];
40 } else if (args[i] === '--tags' && args[i + 1]) {
41 opts.tags = args[++i];
42 } else if (args[i] === '--file' && args[i + 1]) {
43 opts.file = args[++i];
44 }
45 }
46 return opts;
47 }
48
49 /**
50 * Make source_id safe for filename: alphanumeric, hyphen, underscore only.
51 * @param {string} id
52 * @returns {string}
53 */
54 function sanitizeForFilename(id) {
55 if (typeof id !== 'string') return '';
56 return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64) || 'unknown';
57 }
58
59 function main() {
60 const opts = parseArgs();
61 let body;
62 if (opts.file) {
63 const abs = path.isAbsolute(opts.file) ? opts.file : path.resolve(process.cwd(), opts.file);
64 if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
65 console.error(`capture-file: file not found: ${opts.file}`);
66 process.exit(2);
67 }
68 body = fs.readFileSync(abs, 'utf8');
69 } else {
70 body = fs.readFileSync(0, 'utf8');
71 }
72
73 const config = loadConfig(projectRoot);
74 const now = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
75 const sourceSlug = normalizeSlug(opts.source) || 'file';
76 const filename = opts.sourceId
77 ? `${sourceSlug}_${sanitizeForFilename(opts.sourceId)}.md`
78 : `${sourceSlug}_${Date.now()}.md`;
79
80 const relativePath = opts.project
81 ? `projects/${normalizeSlug(opts.project)}/inbox/${filename}`
82 : `inbox/${filename}`;
83
84 const frontmatter = {
85 source: opts.source,
86 date: now,
87 ...(opts.sourceId && { source_id: opts.sourceId }),
88 ...(opts.project && { project: normalizeSlug(opts.project) }),
89 ...(opts.tags && { tags: opts.tags }),
90 };
91
92 try {
93 const result = writeNote(config.vault_path, relativePath, {
94 body: body.trimEnd(),
95 frontmatter,
96 });
97 console.log(`Captured: ${result.path}`);
98 process.exit(0);
99 } catch (e) {
100 console.error('capture-file:', e.message);
101 process.exit(2);
102 }
103 }
104
105 main();
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