claude.mjs
105 lines 4.4 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Claude (Anthropic) export importer. Supports folder of Markdown or JSON from third-party exporters.
3 * One note per conversation; frontmatter: source: claude, source_id, date.
4 */
5
6 import fs from 'fs';
7 import path from 'path';
8 import { writeNote } from '../write.mjs';
9 import { parseFrontmatterAndBody, normalizeSlug } from '../vault.mjs';
10
11 /**
12 * @param {string} input - Path to folder (of .md) or JSON file
13 * @param {{ vaultPath: string, outputBase: string, project?: string, tags: string[], dryRun: boolean }} ctx
14 * @returns {Promise<{ imported: { path: string, source_id?: string }[], count: number }>}
15 */
16 export async function importClaude(input, ctx) {
17 const { vaultPath, outputBase, project, tags, dryRun } = ctx;
18 const absInput = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
19 if (!fs.existsSync(absInput)) {
20 throw new Error(`Input not found: ${input}`);
21 }
22
23 const stat = fs.statSync(absInput);
24 const now = new Date().toISOString().slice(0, 10);
25 const imported = [];
26
27 if (stat.isFile() && absInput.endsWith('.json')) {
28 const raw = fs.readFileSync(absInput, 'utf8');
29 let data;
30 try {
31 data = JSON.parse(raw);
32 } catch (e) {
33 throw new Error(`Invalid JSON: ${e.message}`);
34 }
35 const convs = Array.isArray(data) ? data : (data.conversations ? Object.values(data.conversations) : data.chats || []);
36 for (let i = 0; i < convs.length; i++) {
37 const c = convs[i];
38 const body = typeof c.content === 'string' ? c.content : (c.messages ? formatMessages(c.messages) : JSON.stringify(c));
39 const title = c.title || c.name || `Claude ${i + 1}`;
40 const sourceId = c.id || c.uuid || `claude_${i}`;
41 const date = c.created_at || c.updated_at || c.date || now;
42 const d = (typeof date === 'number' ? new Date(date * 1000) : new Date(date)).toISOString().slice(0, 10);
43 const safeTitle = String(title).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 60) || `claude-${i}`;
44 const outputRel = path.join(outputBase, `${safeTitle}.md`).replace(/\\/g, '/');
45 const frontmatter = {
46 source: 'claude',
47 source_id: String(sourceId).slice(0, 128),
48 date: d,
49 title,
50 ...(project && { project: normalizeSlug(project) }),
51 ...(tags.length && { tags }),
52 };
53 if (!dryRun) writeNote(vaultPath, outputRel, { body, frontmatter });
54 imported.push({ path: outputRel, source_id: frontmatter.source_id });
55 }
56 } else if (stat.isDirectory()) {
57 const files = [];
58 walkMd(absInput, absInput, '', files);
59 for (const { fullPath, relPath } of files) {
60 const content = fs.readFileSync(fullPath, 'utf8');
61 const { frontmatter, body } = parseFrontmatterAndBody(content);
62 const baseName = path.basename(relPath, '.md');
63 const sourceId = frontmatter.source_id || frontmatter.id || `claude_${baseName}`;
64 const merged = {
65 ...frontmatter,
66 source: 'claude',
67 source_id: String(sourceId).slice(0, 128),
68 date: frontmatter.date || now,
69 ...(project && { project: normalizeSlug(project) }),
70 ...(tags.length && { tags: [...new Set([...(Array.isArray(frontmatter.tags) ? frontmatter.tags : []), ...tags])] }),
71 };
72 const outputRel = path.join(outputBase, relPath).replace(/\\/g, '/');
73 const clean = Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== undefined && v !== null && v !== ''));
74 if (!dryRun) writeNote(vaultPath, outputRel, { body, frontmatter: clean });
75 imported.push({ path: outputRel, source_id: merged.source_id });
76 }
77 } else {
78 throw new Error('Claude export must be a folder of .md files or a .json file. Use a third-party exporter if needed.');
79 }
80
81 return { imported, count: imported.length };
82 }
83
84 function formatMessages(msgs) {
85 if (!Array.isArray(msgs)) return '';
86 return msgs
87 .map((m) => {
88 const role = m.role || m.type || 'unknown';
89 const text = m.content || m.text || m.message || '';
90 return `**${role}:**\n${text}`;
91 })
92 .join('\n\n');
93 }
94
95 function walkMd(rootDir, dir, relDir, out) {
96 const entries = fs.readdirSync(dir, { withFileTypes: true });
97 for (const e of entries) {
98 const rel = relDir ? `${relDir}/${e.name}` : e.name;
99 if (e.isDirectory()) {
100 walkMd(rootDir, path.join(dir, e.name), rel, out);
101 } else if (e.name.endsWith('.md')) {
102 out.push({ fullPath: path.join(dir, e.name), relPath: rel.replace(/\\/g, '/') });
103 }
104 }
105 }
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