egregore-artifacts 0.1.0 → 0.2.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'];
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
 
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
+
21
50
  if (!filePath && type !== 'activity') {
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;
@@ -49,9 +85,25 @@ try {
49
85
  const input = type === 'activity' ? (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/index.js CHANGED
@@ -6,13 +6,15 @@ 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';
9
10
  import { questTemplate } from './templates/quest.js';
10
11
  import { handoffTemplate } from './templates/handoff.js';
11
12
  import { activityTemplate } from './templates/activity.js';
13
+ import { documentTemplate } from './templates/document.js';
12
14
  import { openInBrowser } from './open.js';
13
15
 
14
- const PARSERS = { quest: parseQuest, handoff: parseHandoff, activity: parseActivity };
15
- const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, activity: activityTemplate };
16
+ const PARSERS = { quest: parseQuest, handoff: parseHandoff, activity: parseActivity, document: parseDocument };
17
+ const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, activity: activityTemplate, document: documentTemplate };
16
18
 
17
19
  export async function generateArtifact(type, input) {
18
20
  const template = TEMPLATES[type];
@@ -45,6 +47,7 @@ export async function openArtifact(html, title = 'Egregore Artifact') {
45
47
 
46
48
  export { parseQuest } from './parsers/quest.js';
47
49
  export { parseHandoff } from './parsers/handoff.js';
50
+ export { parseDocument } from './parsers/document.js';
48
51
  export { renderSpecToHtml } from './render.js';
49
52
  export { catalog, getCatalogPrompt } from './catalog.js';
50
53
  export { registry } from './registry.js';
@@ -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,52 @@
1
+ // Generic document → React element tree
2
+ import React from 'react';
3
+ import { ArtifactHeader, SectionCard, TextBlock, ArtifactFooter } from '../components.js';
4
+
5
+ const h = React.createElement;
6
+
7
+ export function documentTemplate(doc) {
8
+ const sections = [];
9
+
10
+ // Header
11
+ sections.push(
12
+ h(ArtifactHeader, {
13
+ key: 'header',
14
+ title: doc.title,
15
+ type: 'document',
16
+ date: doc.date,
17
+ author: doc.author,
18
+ status: doc.status,
19
+ priority: 0,
20
+ projects: doc.frontmatter?.projects || [],
21
+ })
22
+ );
23
+
24
+ // Preamble (text before first ## heading)
25
+ if (doc.preamble) {
26
+ sections.push(
27
+ h(SectionCard, { key: 'preamble', label: null },
28
+ h(TextBlock, { text: doc.preamble, variant: 'lead' })
29
+ )
30
+ );
31
+ }
32
+
33
+ // Body sections
34
+ for (const section of doc.sections) {
35
+ sections.push(
36
+ h(SectionCard, { key: `s-${section.heading}`, label: section.heading },
37
+ h(TextBlock, { text: section.body })
38
+ )
39
+ );
40
+ }
41
+
42
+ // Footer
43
+ sections.push(
44
+ h(ArtifactFooter, {
45
+ key: 'footer',
46
+ generatedAt: new Date().toISOString(),
47
+ source: doc.source,
48
+ })
49
+ );
50
+
51
+ return h('div', null, ...sections);
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "egregore-artifacts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generate branded HTML artifacts from Egregore data",
5
5
  "type": "module",
6
6
  "license": "MIT",