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 +59 -7
- package/lib/index.js +5 -2
- package/lib/parsers/document.js +101 -0
- package/lib/templates/document.js +52 -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'];
|
|
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
|
|
|
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
|
-
|
|
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/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
|
+
}
|