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 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 [,, type, filePath] = process.argv;
7
+ const KNOWN_TYPES = ['quest', 'handoff', 'activity', 'document', 'board', 'network'];
8
+ const args = process.argv.slice(2);
8
9
 
9
- if (!type) {
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 (runs bin/activity-data.sh live)');
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
- if (!filePath && type !== 'activity') {
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
- const outputPath = await openArtifact(html, `${type}-${slug}`);
53
- console.log(`✓ Artifact generated: ${outputPath}`);
54
- console.log(' Opening in browser...');
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: colors.muted, fontSize: '13px', fontFamily: fonts.mono } }, `(${a.author})`),
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: colors.muted, fontSize: '12px' } }, c.role),
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: colors.black,
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: colors.black,
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: colors.dark,
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: colors.terracotta,
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: colors.terracotta,
132
- color: colors.cream,
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: colors.terminalBg,
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: colors.dark },
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', null, ...elements);
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
- style: { fontFamily: fonts.mono, fontSize: '0.88em', background: 'rgba(59, 45, 33, 0.06)', padding: '2px 5px', borderRadius: '3px' },
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: colors.terracotta, textDecoration: 'underline' },
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: `2px solid ${colors.border}`,
325
+ borderBottom: '2px solid var(--border)',
289
326
  fontFamily: fonts.mono,
290
327
  fontSize: '12px',
291
328
  fontWeight: 500,
292
- color: colors.muted,
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: `1px solid rgba(224, 216, 204, 0.5)`,
310
- color: colors.dark,
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: colors.black } }, line.slice(3)));
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: colors.dark } }, line));
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
+ }