linear.mjs
104 lines 3.7 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 2 days ago
1 /**
2 * Linear CSV export importer. Parses CSV from Linear export (Exporting Data).
3 * One note per issue; frontmatter: source: linear, source_id: issue id; body: title + description.
4 */
5
6 import fs from 'fs';
7 import path from 'path';
8 import { writeNote } from '../write.mjs';
9 import { normalizeSlug } from '../vault.mjs';
10 import { buildRowObjectForJson } from './tabular-import.mjs';
11
12 function parseCSVLine(line) {
13 const out = [];
14 let i = 0;
15 while (i < line.length) {
16 if (line[i] === '"') {
17 i++;
18 let field = '';
19 while (i < line.length) {
20 if (line[i] === '"') {
21 i++;
22 if (line[i] === '"') {
23 field += '"';
24 i++;
25 } else break;
26 } else {
27 field += line[i++];
28 }
29 }
30 out.push(field);
31 } else {
32 let field = '';
33 while (i < line.length && line[i] !== ',') {
34 field += line[i++];
35 }
36 out.push(field.trim());
37 if (line[i] === ',') i++;
38 }
39 }
40 return out;
41 }
42
43 /**
44 * @param {string} input - Path to Linear CSV file
45 * @param {{ vaultPath: string, outputBase: string, project?: string, tags: string[], dryRun: boolean }} ctx
46 * @returns {Promise<{ imported: { path: string, source_id?: string }[], count: number }>}
47 */
48 export async function importLinear(input, ctx) {
49 const { vaultPath, outputBase, project, tags, dryRun } = ctx;
50 const absInput = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
51 if (!fs.existsSync(absInput) || !fs.statSync(absInput).isFile()) {
52 throw new Error(`Linear import expects a CSV file. Export from Linear: Command menu → Export data → CSV.`);
53 }
54 if (!absInput.endsWith('.csv')) {
55 throw new Error('Input must be a .csv file.');
56 }
57
58 const raw = fs.readFileSync(absInput, 'utf8');
59 const lines = raw.split(/\r?\n/).filter((l) => l.trim());
60 if (lines.length < 2) return { imported: [], count: 0 };
61
62 const headerRaw = parseCSVLine(lines[0]);
63 const header = headerRaw.map((h) => h.trim().toLowerCase());
64 const idIdx = header.findIndex((h) => h === 'id' || h === 'identifier');
65 const titleIdx = header.findIndex((h) => h === 'title' || h === 'name');
66 const descIdx = header.findIndex((h) => h === 'description' || h === 'body');
67 const nCols = headerRaw.length;
68
69 const imported = [];
70 const now = new Date().toISOString().slice(0, 10);
71
72 for (let rowNum = 1; rowNum < lines.length; rowNum++) {
73 const row = parseCSVLine(lines[rowNum]);
74 const id = idIdx >= 0 ? (row[idIdx] || '').trim() : `linear-${rowNum}`;
75 if (!id) continue;
76 const title = titleIdx >= 0 ? (row[titleIdx] || '').trim() : '';
77 const description = descIdx >= 0 ? (row[descIdx] || '').trim() : '';
78 const labelHeaders = headerRaw.map((h, i) => h.trim() || `column_${i}`);
79 const rowCells = Array.from({ length: nCols }, (_, i) => (i < row.length ? row[i] : ''));
80 const fullRowJson = buildRowObjectForJson(labelHeaders, rowCells);
81 const jsonStr = JSON.stringify(fullRowJson, null, 2);
82 let body = title ? `# ${title}\n\n${description}` : description || '(no content)';
83 body += '\n\n## All CSV fields (JSON)\n\n```json\n' + jsonStr + '\n```\n';
84 const safeName = id.replace(/[^a-zA-Z0-9-_]/g, '_').slice(0, 60) + '.md';
85 const outputRel = path.join(outputBase, safeName).replace(/\\/g, '/');
86
87 const frontmatter = {
88 source: 'linear',
89 source_id: id,
90 date: now,
91 import_column_headers: JSON.stringify(labelHeaders),
92 ...(title && { title }),
93 ...(project && { project: normalizeSlug(project) }),
94 ...(tags.length && { tags }),
95 };
96
97 if (!dryRun) {
98 writeNote(vaultPath, outputRel, { body, frontmatter });
99 }
100 imported.push({ path: outputRel, source_id: id });
101 }
102
103 return { imported, count: imported.length };
104 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 2 days ago