egregore-artifacts 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +61 -9
- package/lib/components.js +2 -2
- package/lib/index.js +9 -2
- package/lib/markdown.js +54 -17
- package/lib/parsers/board.js +109 -0
- package/lib/parsers/document.js +101 -0
- package/lib/parsers/network.js +73 -0
- package/lib/registry.js +15 -15
- package/lib/shell.js +160 -9
- package/lib/templates/activity.js +28 -28
- package/lib/templates/board.js +459 -0
- package/lib/templates/document.js +52 -0
- package/lib/templates/handoff.js +9 -9
- package/lib/templates/network.js +189 -0
- package/lib/tokens.js +12 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,25 +4,61 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
-
const [
|
|
7
|
+
const KNOWN_TYPES = ['quest', 'handoff', 'activity', 'document', 'board', 'network'];
|
|
8
|
+
const args = process.argv.slice(2);
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
let type, filePath;
|
|
11
|
+
|
|
12
|
+
if (args.length === 0) {
|
|
10
13
|
console.error('Usage: egregore-artifacts <type> [file]');
|
|
14
|
+
console.error(' egregore-artifacts <file.md> (auto-detects type)');
|
|
11
15
|
console.error('');
|
|
12
|
-
console.error('Types: quest, handoff, activity');
|
|
16
|
+
console.error('Types: quest, handoff, activity, document');
|
|
13
17
|
console.error('');
|
|
14
18
|
console.error('Examples:');
|
|
15
19
|
console.error(' egregore-artifacts quest memory/quests/artifact-generation.md');
|
|
16
20
|
console.error(' egregore-artifacts handoff memory/handoffs/2026-03/31-cem-oss-security-audit.md');
|
|
17
|
-
console.error(' egregore-artifacts activity
|
|
21
|
+
console.error(' egregore-artifacts activity');
|
|
22
|
+
console.error(' egregore-artifacts memory/knowledge/decisions/some-decision.md');
|
|
18
23
|
process.exit(1);
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
// Parse flags
|
|
27
|
+
let outputFile = null;
|
|
28
|
+
let noOpen = false;
|
|
29
|
+
const positional = [];
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] === '--output' || args[i] === '-o') {
|
|
32
|
+
outputFile = args[++i];
|
|
33
|
+
} else if (args[i] === '--no-open') {
|
|
34
|
+
noOpen = true;
|
|
35
|
+
} else {
|
|
36
|
+
positional.push(args[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If first positional is a known type, use it. Otherwise treat it as a file path.
|
|
41
|
+
if (KNOWN_TYPES.includes(positional[0])) {
|
|
42
|
+
type = positional[0];
|
|
43
|
+
filePath = positional[1];
|
|
44
|
+
} else {
|
|
45
|
+
// Auto-detect: first arg is a file path
|
|
46
|
+
filePath = positional[0];
|
|
47
|
+
type = inferType(filePath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!filePath && type !== 'activity' && type !== 'board' && type !== 'network') {
|
|
22
51
|
console.error(`✗ Missing file path for type "${type}"`);
|
|
23
52
|
process.exit(1);
|
|
24
53
|
}
|
|
25
54
|
|
|
55
|
+
function inferType(fp) {
|
|
56
|
+
if (!fp) return 'document';
|
|
57
|
+
if (fp.includes('/quests/')) return 'quest';
|
|
58
|
+
if (fp.includes('/handoffs/')) return 'handoff';
|
|
59
|
+
return 'document';
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
// Resolve file path relative to git repo root (not cwd)
|
|
27
63
|
function resolveFile(fp) {
|
|
28
64
|
if (!fp) return fp;
|
|
@@ -46,12 +82,28 @@ function resolveFile(fp) {
|
|
|
46
82
|
}
|
|
47
83
|
|
|
48
84
|
try {
|
|
49
|
-
const input = type === 'activity' ? (filePath || 'live') : resolveFile(filePath);
|
|
85
|
+
const input = (type === 'activity' || type === 'board' || type === 'network') ? (filePath || 'live') : resolveFile(filePath);
|
|
50
86
|
const html = await generateArtifact(type, input);
|
|
51
87
|
const slug = filePath ? filePath.split('/').pop().replace('.md', '') : new Date().toISOString().split('T')[0];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
|
|
89
|
+
if (outputFile) {
|
|
90
|
+
// Write to specific file, don't open browser
|
|
91
|
+
fs.mkdirSync(path.dirname(path.resolve(outputFile)), { recursive: true });
|
|
92
|
+
fs.writeFileSync(outputFile, html);
|
|
93
|
+
console.log(`✓ Artifact written: ${outputFile}`);
|
|
94
|
+
} else if (noOpen) {
|
|
95
|
+
// Write to tmp, don't open browser (for piping/automation)
|
|
96
|
+
const os = await import('node:os');
|
|
97
|
+
const tmpDir = path.join(os.default.tmpdir(), 'egregore-artifacts');
|
|
98
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
99
|
+
const outPath = path.join(tmpDir, `${type}-${slug}-${Date.now()}.html`);
|
|
100
|
+
fs.writeFileSync(outPath, html);
|
|
101
|
+
console.log(outPath);
|
|
102
|
+
} else {
|
|
103
|
+
const outputPath = await openArtifact(html, `${type}-${slug}`);
|
|
104
|
+
console.log(`✓ Artifact generated: ${outputPath}`);
|
|
105
|
+
console.log(' Opening in browser...');
|
|
106
|
+
}
|
|
55
107
|
} catch (err) {
|
|
56
108
|
console.error(`✗ ${err.message}`);
|
|
57
109
|
process.exit(1);
|
package/lib/components.js
CHANGED
|
@@ -74,7 +74,7 @@ export function ArtifactList({ artifacts }) {
|
|
|
74
74
|
h('span', { className: 'eg-artifact-date' }, a.date),
|
|
75
75
|
h('span', { className: 'eg-artifact-type' }, a.type),
|
|
76
76
|
h('span', { className: 'eg-artifact-title' }, a.title),
|
|
77
|
-
a.author && h('span', { style: { color:
|
|
77
|
+
a.author && h('span', { style: { color: 'var(--muted)', fontSize: '13px', fontFamily: fonts.mono } }, `(${a.author})`),
|
|
78
78
|
)
|
|
79
79
|
),
|
|
80
80
|
);
|
|
@@ -92,7 +92,7 @@ export function ContributorRow({ contributors }) {
|
|
|
92
92
|
(c.name || '?')[0].toUpperCase()
|
|
93
93
|
),
|
|
94
94
|
h('span', null, c.name),
|
|
95
|
-
c.role && h('span', { style: { color:
|
|
95
|
+
c.role && h('span', { style: { color: 'var(--muted)', fontSize: '12px' } }, c.role),
|
|
96
96
|
)
|
|
97
97
|
),
|
|
98
98
|
);
|
package/lib/index.js
CHANGED
|
@@ -6,13 +6,19 @@ import { renderToHtml, renderSpecToHtml } from './render.js';
|
|
|
6
6
|
import { parseQuest } from './parsers/quest.js';
|
|
7
7
|
import { parseHandoff } from './parsers/handoff.js';
|
|
8
8
|
import { parseActivity } from './parsers/activity.js';
|
|
9
|
+
import { parseDocument } from './parsers/document.js';
|
|
10
|
+
import { parseBoard } from './parsers/board.js';
|
|
11
|
+
import { parseNetwork } from './parsers/network.js';
|
|
9
12
|
import { questTemplate } from './templates/quest.js';
|
|
10
13
|
import { handoffTemplate } from './templates/handoff.js';
|
|
11
14
|
import { activityTemplate } from './templates/activity.js';
|
|
15
|
+
import { documentTemplate } from './templates/document.js';
|
|
16
|
+
import { boardTemplate } from './templates/board.js';
|
|
17
|
+
import { networkTemplate } from './templates/network.js';
|
|
12
18
|
import { openInBrowser } from './open.js';
|
|
13
19
|
|
|
14
|
-
const PARSERS = { quest: parseQuest, handoff: parseHandoff, activity: parseActivity };
|
|
15
|
-
const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, activity: activityTemplate };
|
|
20
|
+
const PARSERS = { quest: parseQuest, handoff: parseHandoff, activity: parseActivity, document: parseDocument, board: parseBoard, network: parseNetwork };
|
|
21
|
+
const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, activity: activityTemplate, document: documentTemplate, board: boardTemplate, network: networkTemplate };
|
|
16
22
|
|
|
17
23
|
export async function generateArtifact(type, input) {
|
|
18
24
|
const template = TEMPLATES[type];
|
|
@@ -45,6 +51,7 @@ export async function openArtifact(html, title = 'Egregore Artifact') {
|
|
|
45
51
|
|
|
46
52
|
export { parseQuest } from './parsers/quest.js';
|
|
47
53
|
export { parseHandoff } from './parsers/handoff.js';
|
|
54
|
+
export { parseDocument } from './parsers/document.js';
|
|
48
55
|
export { renderSpecToHtml } from './render.js';
|
|
49
56
|
export { catalog, getCatalogPrompt } from './catalog.js';
|
|
50
57
|
export { registry } from './registry.js';
|
package/lib/markdown.js
CHANGED
|
@@ -26,6 +26,40 @@ export function renderMarkdown(text) {
|
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// H1 heading (rare inside a document body; render as section-sized)
|
|
30
|
+
if (line.startsWith('# ')) {
|
|
31
|
+
elements.push(h('h2', {
|
|
32
|
+
key: elements.length,
|
|
33
|
+
style: {
|
|
34
|
+
fontFamily: fonts.serif,
|
|
35
|
+
fontSize: '28px',
|
|
36
|
+
fontWeight: 400,
|
|
37
|
+
lineHeight: 1.2,
|
|
38
|
+
margin: '1.75rem 0 0.75rem',
|
|
39
|
+
color: 'var(--black)',
|
|
40
|
+
},
|
|
41
|
+
}, inlineMarkdown(line.slice(2))));
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// H2 heading
|
|
47
|
+
if (line.startsWith('## ')) {
|
|
48
|
+
elements.push(h('h3', {
|
|
49
|
+
key: elements.length,
|
|
50
|
+
style: {
|
|
51
|
+
fontFamily: fonts.serif,
|
|
52
|
+
fontSize: '22px',
|
|
53
|
+
fontWeight: 400,
|
|
54
|
+
lineHeight: 1.25,
|
|
55
|
+
margin: '1.5rem 0 0.5rem',
|
|
56
|
+
color: 'var(--black)',
|
|
57
|
+
},
|
|
58
|
+
}, inlineMarkdown(line.slice(3))));
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
// H3 heading
|
|
30
64
|
if (line.startsWith('### ')) {
|
|
31
65
|
elements.push(h('h3', {
|
|
@@ -36,7 +70,7 @@ export function renderMarkdown(text) {
|
|
|
36
70
|
fontWeight: 600,
|
|
37
71
|
lineHeight: 1.3,
|
|
38
72
|
margin: '1.5rem 0 0.5rem',
|
|
39
|
-
color:
|
|
73
|
+
color: 'var(--black)',
|
|
40
74
|
},
|
|
41
75
|
}, inlineMarkdown(line.slice(4))));
|
|
42
76
|
i++;
|
|
@@ -52,7 +86,7 @@ export function renderMarkdown(text) {
|
|
|
52
86
|
fontSize: '15px',
|
|
53
87
|
fontWeight: 600,
|
|
54
88
|
margin: '1rem 0 0.25rem',
|
|
55
|
-
color:
|
|
89
|
+
color: 'var(--black)',
|
|
56
90
|
},
|
|
57
91
|
}, inlineMarkdown(line.slice(5))));
|
|
58
92
|
i++;
|
|
@@ -79,7 +113,7 @@ export function renderMarkdown(text) {
|
|
|
79
113
|
marginBottom: '0.35rem',
|
|
80
114
|
fontSize: '15px',
|
|
81
115
|
lineHeight: 1.55,
|
|
82
|
-
color:
|
|
116
|
+
color: 'var(--dark)',
|
|
83
117
|
},
|
|
84
118
|
},
|
|
85
119
|
h('span', {
|
|
@@ -90,7 +124,7 @@ export function renderMarkdown(text) {
|
|
|
90
124
|
width: '5px',
|
|
91
125
|
height: '5px',
|
|
92
126
|
borderRadius: '50%',
|
|
93
|
-
background:
|
|
127
|
+
background: 'var(--terracotta)',
|
|
94
128
|
},
|
|
95
129
|
}),
|
|
96
130
|
inlineMarkdown(item),
|
|
@@ -128,8 +162,8 @@ export function renderMarkdown(text) {
|
|
|
128
162
|
width: '20px',
|
|
129
163
|
height: '20px',
|
|
130
164
|
borderRadius: '50%',
|
|
131
|
-
background:
|
|
132
|
-
color:
|
|
165
|
+
background: 'var(--terracotta)',
|
|
166
|
+
color: 'var(--cream)',
|
|
133
167
|
display: 'flex',
|
|
134
168
|
alignItems: 'center',
|
|
135
169
|
justifyContent: 'center',
|
|
@@ -158,7 +192,7 @@ export function renderMarkdown(text) {
|
|
|
158
192
|
elements.push(h('pre', {
|
|
159
193
|
key: elements.length,
|
|
160
194
|
style: {
|
|
161
|
-
background:
|
|
195
|
+
background: 'var(--terminal-bg)',
|
|
162
196
|
color: 'rgba(255, 255, 255, 0.85)',
|
|
163
197
|
fontFamily: fonts.mono,
|
|
164
198
|
fontSize: '13px',
|
|
@@ -190,12 +224,15 @@ export function renderMarkdown(text) {
|
|
|
190
224
|
if (paraLines.length > 0) {
|
|
191
225
|
elements.push(h('p', {
|
|
192
226
|
key: elements.length,
|
|
193
|
-
style: { margin: '0.5rem 0', fontSize: '15px', lineHeight: 1.6, color:
|
|
227
|
+
style: { margin: '0.5rem 0', fontSize: '15px', lineHeight: 1.6, color: 'var(--dark)' },
|
|
194
228
|
}, inlineMarkdown(paraLines.join(' '))));
|
|
229
|
+
} else {
|
|
230
|
+
// Safety: nothing consumed this line. Skip it to avoid an infinite loop.
|
|
231
|
+
i++;
|
|
195
232
|
}
|
|
196
233
|
}
|
|
197
234
|
|
|
198
|
-
return h('div',
|
|
235
|
+
return h('div', { style: { color: 'var(--dark)' } }, ...elements);
|
|
199
236
|
}
|
|
200
237
|
|
|
201
238
|
// Inline markdown: **bold**, *italic*, `code`, [link](url)
|
|
@@ -210,11 +247,11 @@ function inlineMarkdown(text) {
|
|
|
210
247
|
const patterns = [
|
|
211
248
|
{ re: /`([^`]+)`/, render: (m) => h('code', {
|
|
212
249
|
key: key++,
|
|
213
|
-
|
|
250
|
+
className: 'eg-code',
|
|
214
251
|
}, m[1]) },
|
|
215
252
|
{ re: /\*\*(.+?)\*\*/, render: (m) => h('strong', { key: key++, style: { fontWeight: 600 } }, m[1]) },
|
|
216
253
|
{ re: /\[([^\]]+)\]\(([^)]+)\)/, render: (m) => h('a', {
|
|
217
|
-
key: key++, href: m[2], style: { color:
|
|
254
|
+
key: key++, href: m[2], style: { color: 'var(--terracotta)', textDecoration: 'underline' },
|
|
218
255
|
}, m[1]) },
|
|
219
256
|
];
|
|
220
257
|
|
|
@@ -285,11 +322,11 @@ function renderTable(lines, key) {
|
|
|
285
322
|
style: {
|
|
286
323
|
textAlign: 'left',
|
|
287
324
|
padding: '8px 12px',
|
|
288
|
-
borderBottom:
|
|
325
|
+
borderBottom: '2px solid var(--border)',
|
|
289
326
|
fontFamily: fonts.mono,
|
|
290
327
|
fontSize: '12px',
|
|
291
328
|
fontWeight: 500,
|
|
292
|
-
color:
|
|
329
|
+
color: 'var(--muted)',
|
|
293
330
|
textTransform: 'uppercase',
|
|
294
331
|
letterSpacing: '0.04em',
|
|
295
332
|
whiteSpace: 'nowrap',
|
|
@@ -306,8 +343,8 @@ function renderTable(lines, key) {
|
|
|
306
343
|
key: j,
|
|
307
344
|
style: {
|
|
308
345
|
padding: '6px 12px',
|
|
309
|
-
borderBottom:
|
|
310
|
-
color:
|
|
346
|
+
borderBottom: '1px solid var(--border)',
|
|
347
|
+
color: 'var(--dark)',
|
|
311
348
|
},
|
|
312
349
|
}, cell.trim())
|
|
313
350
|
),
|
|
@@ -347,7 +384,7 @@ export function renderMarkdownLite(text) {
|
|
|
347
384
|
|
|
348
385
|
// H2/H3 heading
|
|
349
386
|
if (line.startsWith('## ')) {
|
|
350
|
-
elements.push(h('h3', { key: elements.length, style: { fontFamily: fonts.serif, fontSize: '18px', fontWeight: 600, margin: '1.5rem 0 0.5rem', color:
|
|
387
|
+
elements.push(h('h3', { key: elements.length, style: { fontFamily: fonts.serif, fontSize: '18px', fontWeight: 600, margin: '1.5rem 0 0.5rem', color: 'var(--black)' } }, line.slice(3)));
|
|
351
388
|
i++;
|
|
352
389
|
continue;
|
|
353
390
|
}
|
|
@@ -369,7 +406,7 @@ export function renderMarkdownLite(text) {
|
|
|
369
406
|
if (line.trim() === '') { i++; continue; }
|
|
370
407
|
|
|
371
408
|
// Paragraph
|
|
372
|
-
elements.push(h('p', { key: elements.length, style: { margin: '0.5rem 0', fontSize: '14px', lineHeight: 1.6, color:
|
|
409
|
+
elements.push(h('p', { key: elements.length, style: { margin: '0.5rem 0', fontSize: '14px', lineHeight: 1.6, color: 'var(--dark)' } }, line));
|
|
373
410
|
i++;
|
|
374
411
|
}
|
|
375
412
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Parse board data (JSON from memory/board/board.json) into structured data
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export function parseBoard(input) {
|
|
6
|
+
let data;
|
|
7
|
+
|
|
8
|
+
if (typeof input === 'string' && fs.existsSync(input)) {
|
|
9
|
+
data = JSON.parse(fs.readFileSync(input, 'utf-8'));
|
|
10
|
+
} else if (typeof input === 'string' && input.startsWith('{')) {
|
|
11
|
+
data = JSON.parse(input);
|
|
12
|
+
} else if (typeof input === 'object') {
|
|
13
|
+
data = input;
|
|
14
|
+
} else {
|
|
15
|
+
// Try to find board.json relative to git root
|
|
16
|
+
try {
|
|
17
|
+
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
18
|
+
const boardPath = `${root}/memory/board/board.json`;
|
|
19
|
+
if (fs.existsSync(boardPath)) {
|
|
20
|
+
data = JSON.parse(fs.readFileSync(boardPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
if (!data) throw new Error('No board data found. Expected memory/board/board.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const today = new Date().toISOString().split('T')[0];
|
|
27
|
+
|
|
28
|
+
// Flatten all cards from the hierarchy for person and timeline views
|
|
29
|
+
const allCards = [];
|
|
30
|
+
for (const activity of data.activities || []) {
|
|
31
|
+
if (activity.subactivities) {
|
|
32
|
+
for (const sub of activity.subactivities) {
|
|
33
|
+
for (const card of sub.cards || []) {
|
|
34
|
+
allCards.push({ ...card, activity: activity.label, subactivity: sub.label, activityId: activity.id, subactivityId: sub.id });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const card of activity.cards || []) {
|
|
39
|
+
allCards.push({ ...card, activity: activity.label, subactivity: null, activityId: activity.id, subactivityId: null });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Group by person
|
|
44
|
+
const personMap = {};
|
|
45
|
+
for (const card of allCards) {
|
|
46
|
+
for (const owner of card.owners || []) {
|
|
47
|
+
if (!personMap[owner]) personMap[owner] = [];
|
|
48
|
+
personMap[owner].push(card);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const people = Object.entries(personMap)
|
|
52
|
+
.map(([name, cards]) => ({
|
|
53
|
+
name,
|
|
54
|
+
cards: cards.sort((a, b) => a.priority - b.priority),
|
|
55
|
+
stats: {
|
|
56
|
+
total: cards.length,
|
|
57
|
+
p0: cards.filter(c => c.priority === 0).length,
|
|
58
|
+
p1: cards.filter(c => c.priority === 1).length,
|
|
59
|
+
p2: cards.filter(c => c.priority === 2).length,
|
|
60
|
+
p3: cards.filter(c => c.priority === 3).length,
|
|
61
|
+
inProgress: cards.filter(c => c.status === 'in-progress').length,
|
|
62
|
+
todo: cards.filter(c => c.status === 'todo').length,
|
|
63
|
+
review: cards.filter(c => c.status === 'review').length,
|
|
64
|
+
done: cards.filter(c => c.status === 'done').length,
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
.sort((a, b) => b.stats.p0 - a.stats.p0 || b.stats.total - a.stats.total);
|
|
68
|
+
|
|
69
|
+
// Timeline: cards with dates, sorted by startDate
|
|
70
|
+
const timeline = allCards
|
|
71
|
+
.filter(c => c.startDate || c.dueDate)
|
|
72
|
+
.sort((a, b) => (a.startDate || a.dueDate || '').localeCompare(b.startDate || b.dueDate || ''));
|
|
73
|
+
|
|
74
|
+
// Summary stats
|
|
75
|
+
const summary = {
|
|
76
|
+
totalCards: allCards.length,
|
|
77
|
+
byPriority: { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
78
|
+
byStatus: { todo: 0, 'in-progress': 0, review: 0, done: 0 },
|
|
79
|
+
};
|
|
80
|
+
for (const card of allCards) {
|
|
81
|
+
const pKey = `P${card.priority}`;
|
|
82
|
+
if (summary.byPriority[pKey] !== undefined) summary.byPriority[pKey]++;
|
|
83
|
+
if (summary.byStatus[card.status] !== undefined) summary.byStatus[card.status]++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read org name
|
|
87
|
+
let org = 'Egregore';
|
|
88
|
+
try {
|
|
89
|
+
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
90
|
+
const configPath = `${root}/egregore.json`;
|
|
91
|
+
if (fs.existsSync(configPath)) {
|
|
92
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
93
|
+
org = config.org_name || org;
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
title: `Project Board — ${org}`,
|
|
99
|
+
date: today,
|
|
100
|
+
org,
|
|
101
|
+
updatedBy: data.updatedBy,
|
|
102
|
+
updatedAt: data.updated,
|
|
103
|
+
activities: data.activities,
|
|
104
|
+
allCards,
|
|
105
|
+
people,
|
|
106
|
+
timeline,
|
|
107
|
+
summary,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Generic markdown document parser
|
|
2
|
+
// Handles any .md file — extracts YAML frontmatter (if present) + body sections
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function parseDocument(filePath) {
|
|
7
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
8
|
+
const fileName = path.basename(filePath, '.md');
|
|
9
|
+
|
|
10
|
+
let frontmatter = {};
|
|
11
|
+
let body = raw;
|
|
12
|
+
|
|
13
|
+
// Extract YAML frontmatter if present
|
|
14
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
15
|
+
if (fmMatch) {
|
|
16
|
+
frontmatter = parseSimpleYaml(fmMatch[1]);
|
|
17
|
+
body = fmMatch[2];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Extract title: frontmatter title > first H1 > filename
|
|
21
|
+
let title = frontmatter.title;
|
|
22
|
+
if (!title) {
|
|
23
|
+
const h1Match = body.match(/^#\s+(.+)$/m);
|
|
24
|
+
if (h1Match) {
|
|
25
|
+
title = h1Match[1];
|
|
26
|
+
// Remove the H1 from body since we'll render it as the header
|
|
27
|
+
body = body.replace(/^#\s+.+\n?/, '');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!title) {
|
|
31
|
+
title = fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Extract sections (## headings split the body)
|
|
35
|
+
const sections = [];
|
|
36
|
+
const sectionRegex = /^##\s+(.+)$/gm;
|
|
37
|
+
let lastIndex = 0;
|
|
38
|
+
let match;
|
|
39
|
+
let preamble = '';
|
|
40
|
+
|
|
41
|
+
// Collect all ## heading positions
|
|
42
|
+
const headings = [];
|
|
43
|
+
while ((match = sectionRegex.exec(body)) !== null) {
|
|
44
|
+
headings.push({ title: match[1], index: match.index, fullMatch: match[0] });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (headings.length === 0) {
|
|
48
|
+
// No sections — entire body is content
|
|
49
|
+
preamble = body.trim();
|
|
50
|
+
} else {
|
|
51
|
+
// Text before first heading is preamble
|
|
52
|
+
preamble = body.slice(0, headings[0].index).trim();
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < headings.length; i++) {
|
|
55
|
+
const start = headings[i].index + headings[i].fullMatch.length;
|
|
56
|
+
const end = i + 1 < headings.length ? headings[i + 1].index : body.length;
|
|
57
|
+
sections.push({
|
|
58
|
+
heading: headings[i].title,
|
|
59
|
+
body: body.slice(start, end).trim(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
title,
|
|
66
|
+
frontmatter,
|
|
67
|
+
preamble,
|
|
68
|
+
sections,
|
|
69
|
+
source: filePath,
|
|
70
|
+
date: frontmatter.date || frontmatter.started || null,
|
|
71
|
+
author: frontmatter.author || frontmatter.started_by || frontmatter.from || null,
|
|
72
|
+
status: frontmatter.status || null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Minimal YAML parser — handles key: value, key: [array], key: "quoted"
|
|
77
|
+
function parseSimpleYaml(text) {
|
|
78
|
+
const result = {};
|
|
79
|
+
for (const line of text.split('\n')) {
|
|
80
|
+
const m = line.match(/^(\w[\w_-]*):\s*(.*)$/);
|
|
81
|
+
if (!m) continue;
|
|
82
|
+
const [, key, rawVal] = m;
|
|
83
|
+
let val = rawVal.trim();
|
|
84
|
+
|
|
85
|
+
// Array: [a, b, c]
|
|
86
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
87
|
+
val = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
88
|
+
}
|
|
89
|
+
// Quoted string
|
|
90
|
+
else if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
91
|
+
val = val.slice(1, -1);
|
|
92
|
+
}
|
|
93
|
+
// null
|
|
94
|
+
else if (val === 'null' || val === '') {
|
|
95
|
+
val = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
result[key] = val;
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Parse network/people data (JSON from memory/network/people.json)
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export function parseNetwork(input) {
|
|
6
|
+
let data;
|
|
7
|
+
|
|
8
|
+
if (typeof input === 'string' && fs.existsSync(input)) {
|
|
9
|
+
data = JSON.parse(fs.readFileSync(input, 'utf-8'));
|
|
10
|
+
} else if (typeof input === 'string' && input.startsWith('{')) {
|
|
11
|
+
data = JSON.parse(input);
|
|
12
|
+
} else if (typeof input === 'object') {
|
|
13
|
+
data = input;
|
|
14
|
+
} else {
|
|
15
|
+
try {
|
|
16
|
+
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
17
|
+
const peoplePath = `${root}/memory/network/people.json`;
|
|
18
|
+
if (fs.existsSync(peoplePath)) {
|
|
19
|
+
data = JSON.parse(fs.readFileSync(peoplePath, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
if (!data) throw new Error('No network data found. Expected memory/network/people.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const people = data.people || [];
|
|
26
|
+
|
|
27
|
+
// Collect all unique roles
|
|
28
|
+
const allRoles = [...new Set(people.flatMap(p => p.roles || []))].sort();
|
|
29
|
+
|
|
30
|
+
// Group by role
|
|
31
|
+
const byRole = {};
|
|
32
|
+
for (const role of allRoles) {
|
|
33
|
+
byRole[role] = people.filter(p => (p.roles || []).includes(role));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Group by organization
|
|
37
|
+
const byOrg = {};
|
|
38
|
+
for (const person of people) {
|
|
39
|
+
const org = person.organization || 'Independent';
|
|
40
|
+
if (!byOrg[org]) byOrg[org] = [];
|
|
41
|
+
byOrg[org].push(person);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Summary
|
|
45
|
+
const summary = {
|
|
46
|
+
totalPeople: people.length,
|
|
47
|
+
totalOrgs: Object.keys(byOrg).length,
|
|
48
|
+
byRole: Object.fromEntries(allRoles.map(r => [r, byRole[r].length])),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Read org name
|
|
52
|
+
let org = 'Egregore';
|
|
53
|
+
try {
|
|
54
|
+
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
55
|
+
const configPath = `${root}/egregore.json`;
|
|
56
|
+
if (fs.existsSync(configPath)) {
|
|
57
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
58
|
+
org = config.org_name || org;
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
title: `Network — ${org}`,
|
|
64
|
+
date: new Date().toISOString().split('T')[0],
|
|
65
|
+
org,
|
|
66
|
+
updatedBy: data.updatedBy,
|
|
67
|
+
people,
|
|
68
|
+
allRoles,
|
|
69
|
+
byRole,
|
|
70
|
+
byOrg,
|
|
71
|
+
summary,
|
|
72
|
+
};
|
|
73
|
+
}
|