egregore-artifacts 0.1.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 ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import { generateArtifact, openArtifact } from '../lib/index.js';
3
+ import { execSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const [,, type, filePath] = process.argv;
8
+
9
+ if (!type) {
10
+ console.error('Usage: egregore-artifacts <type> [file]');
11
+ console.error('');
12
+ console.error('Types: quest, handoff, activity');
13
+ console.error('');
14
+ console.error('Examples:');
15
+ console.error(' egregore-artifacts quest memory/quests/artifact-generation.md');
16
+ 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)');
18
+ process.exit(1);
19
+ }
20
+
21
+ if (!filePath && type !== 'activity') {
22
+ console.error(`✗ Missing file path for type "${type}"`);
23
+ process.exit(1);
24
+ }
25
+
26
+ // Resolve file path relative to git repo root (not cwd)
27
+ function resolveFile(fp) {
28
+ if (!fp) return fp;
29
+
30
+ // Already absolute and exists
31
+ if (path.isAbsolute(fp) && fs.existsSync(fp)) return fp;
32
+
33
+ // Relative to cwd
34
+ if (fs.existsSync(fp)) return path.resolve(fp);
35
+
36
+ // Relative to git repo root
37
+ try {
38
+ const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { encoding: 'utf-8' }).trim();
39
+ const fromRoot = path.join(root, fp);
40
+ if (fs.existsSync(fromRoot)) return fromRoot;
41
+ } catch {}
42
+
43
+ console.error(`✗ File not found: ${fp}`);
44
+ console.error(' Checked: cwd and git repo root');
45
+ process.exit(1);
46
+ }
47
+
48
+ try {
49
+ const input = type === 'activity' ? (filePath || 'live') : resolveFile(filePath);
50
+ const html = await generateArtifact(type, input);
51
+ 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...');
55
+ } catch (err) {
56
+ console.error(`✗ ${err.message}`);
57
+ process.exit(1);
58
+ }
package/lib/catalog.js ADDED
@@ -0,0 +1,134 @@
1
+ // Egregore component catalog for json-render
2
+ // Defines what components Claude can use when generating artifact specs
3
+ import { defineCatalog } from '@json-render/core';
4
+ import { schema } from '@json-render/react';
5
+ import { z } from 'zod';
6
+
7
+ export const catalog = defineCatalog(schema, {
8
+ components: {
9
+ // ── Layout ────────────────────────────────────────────────
10
+ Page: {
11
+ props: z.object({
12
+ title: z.string().describe('Artifact title'),
13
+ type: z.enum(['quest', 'handoff', 'activity', 'report', 'graph']).describe('Artifact type'),
14
+ date: z.string().nullable().describe('Date string'),
15
+ author: z.string().nullable().describe('Author name'),
16
+ status: z.enum(['active', 'paused', 'completed', 'blocked']).nullable(),
17
+ priority: z.number().nullable().describe('0=none, 1=low, 2=medium, 3=high'),
18
+ projects: z.array(z.string()).describe('Related project tags'),
19
+ }),
20
+ slots: { default: {} },
21
+ description: 'Top-level artifact page with branded header, metadata row, and footer. Wrap all content in this.',
22
+ },
23
+
24
+ Section: {
25
+ props: z.object({
26
+ label: z.string().describe('Section label shown as uppercase mono heading'),
27
+ }),
28
+ slots: { default: {} },
29
+ description: 'A white card container with a label. Use for grouping related content.',
30
+ },
31
+
32
+ // ── Text ─────────────────────────────────────────────────
33
+ Prose: {
34
+ props: z.object({
35
+ text: z.string().describe('Markdown text — supports **bold**, `code`, [links](url), ### headings, tables'),
36
+ variant: z.enum(['lead', 'body']).default('body').describe('lead = larger text for key questions/summaries'),
37
+ }),
38
+ description: 'Markdown-rendered text block. Use for any prose content.',
39
+ },
40
+
41
+ // ── Data Display ─────────────────────────────────────────
42
+ MetricGrid: {
43
+ props: z.object({
44
+ metrics: z.array(z.object({
45
+ label: z.string(),
46
+ value: z.string(),
47
+ })).describe('Array of label/value pairs. Max 4.'),
48
+ }),
49
+ description: 'Horizontal grid of metric cards with large serif values and mono labels.',
50
+ },
51
+
52
+ ThreadList: {
53
+ props: z.object({
54
+ threads: z.array(z.object({
55
+ text: z.string(),
56
+ done: z.boolean(),
57
+ })),
58
+ }),
59
+ description: 'Checklist of threads/tasks with branded checkboxes.',
60
+ },
61
+
62
+ ArtifactList: {
63
+ props: z.object({
64
+ artifacts: z.array(z.object({
65
+ date: z.string(),
66
+ type: z.string(),
67
+ title: z.string(),
68
+ author: z.string().nullable(),
69
+ })),
70
+ }),
71
+ description: 'Timestamped list of linked artifacts with type badges.',
72
+ },
73
+
74
+ PersonRow: {
75
+ props: z.object({
76
+ people: z.array(z.object({
77
+ name: z.string(),
78
+ role: z.string().nullable(),
79
+ })),
80
+ }),
81
+ description: 'Horizontal row of people with avatar initials.',
82
+ },
83
+
84
+ BulletList: {
85
+ props: z.object({
86
+ items: z.array(z.string()),
87
+ }),
88
+ description: 'Styled bullet list with terracotta dots.',
89
+ },
90
+
91
+ NumberedSteps: {
92
+ props: z.object({
93
+ steps: z.array(z.string()),
94
+ }),
95
+ description: 'Ordered list with terracotta numbered circles.',
96
+ },
97
+
98
+ SessionList: {
99
+ props: z.object({
100
+ sessions: z.array(z.object({
101
+ date: z.string(),
102
+ topic: z.string(),
103
+ by: z.string().nullable(),
104
+ handedTo: z.string().nullable(),
105
+ })),
106
+ }),
107
+ description: 'List of sessions with date, author badge, topic, and handoff indicator.',
108
+ },
109
+
110
+ StatusBadge: {
111
+ props: z.object({
112
+ status: z.enum(['active', 'paused', 'completed', 'blocked']),
113
+ }),
114
+ description: 'Colored status pill.',
115
+ },
116
+
117
+ Callout: {
118
+ props: z.object({
119
+ text: z.string().describe('Callout text content'),
120
+ }),
121
+ description: 'Highlighted callout box with terracotta border. Use for summaries, trends, key insights.',
122
+ },
123
+
124
+ Divider: {
125
+ props: z.object({}),
126
+ description: 'Horizontal divider line.',
127
+ },
128
+ },
129
+ });
130
+
131
+ // Generate the system prompt that constrains Claude to this catalog
132
+ export async function getCatalogPrompt() {
133
+ return catalog.prompt();
134
+ }
@@ -0,0 +1,144 @@
1
+ // Branded Egregore React components — no JSX, uses React.createElement
2
+ import React from 'react';
3
+ import { colors, fonts, typography } from './tokens.js';
4
+ import { renderMarkdown } from './markdown.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ // ── ArtifactHeader ──────────────────────────────────────────────
9
+
10
+ export function ArtifactHeader({ title, type, date, author, status, priority, projects }) {
11
+ const statusClass = `eg-badge eg-badge-${status || 'active'}`;
12
+ const priorityLabels = { 1: 'low', 2: 'medium', 3: 'high' };
13
+
14
+ return h('header', { className: 'eg-header' },
15
+ h('div', { className: 'eg-header-top' },
16
+ h('span', { className: 'eg-sigil' }, '✦'),
17
+ h('span', { className: 'eg-org-label' }, `egregore · ${type}`),
18
+ ),
19
+ h('h1', { className: 'eg-title' }, title),
20
+ h('div', { className: 'eg-meta-row' },
21
+ status && h('span', { className: statusClass }, status),
22
+ priority > 0 && h('span', { className: 'eg-badge eg-badge-priority' },
23
+ `↑ ${priorityLabels[priority] || 'p' + priority}`
24
+ ),
25
+ ...(projects || []).map((p, i) =>
26
+ h('span', { key: `proj-${i}`, className: 'eg-badge eg-badge-outline' }, p)
27
+ ),
28
+ author && h('span', null, `by ${author}`),
29
+ date && h('span', null, date),
30
+ ),
31
+ );
32
+ }
33
+
34
+ // ── SectionCard ─────────────────────────────────────────────────
35
+
36
+ export function SectionCard({ label, children }) {
37
+ return h('div', { className: 'eg-card' },
38
+ label && h('div', { className: 'eg-card-title' }, label),
39
+ children,
40
+ );
41
+ }
42
+
43
+ // ── StatusBadge ─────────────────────────────────────────────────
44
+
45
+ export function StatusBadge({ status }) {
46
+ return h('span', { className: `eg-badge eg-badge-${status}` }, status);
47
+ }
48
+
49
+ // ── ThreadList ──────────────────────────────────────────────────
50
+
51
+ export function ThreadList({ threads }) {
52
+ if (!threads || threads.length === 0) return null;
53
+
54
+ return h('ul', { className: 'eg-thread-list' },
55
+ ...threads.map((thread, i) =>
56
+ h('li', { key: i, className: 'eg-thread-item' },
57
+ h('span', {
58
+ className: thread.done ? 'eg-checkbox eg-checkbox-done' : 'eg-checkbox'
59
+ }),
60
+ h('span', { className: thread.done ? 'eg-thread-done' : undefined }, thread.text),
61
+ )
62
+ ),
63
+ );
64
+ }
65
+
66
+ // ── ArtifactList ────────────────────────────────────────────────
67
+
68
+ export function ArtifactList({ artifacts }) {
69
+ if (!artifacts || artifacts.length === 0) return null;
70
+
71
+ return h('div', null,
72
+ ...artifacts.map((a, i) =>
73
+ h('div', { key: i, className: 'eg-artifact-item' },
74
+ h('span', { className: 'eg-artifact-date' }, a.date),
75
+ h('span', { className: 'eg-artifact-type' }, a.type),
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})`),
78
+ )
79
+ ),
80
+ );
81
+ }
82
+
83
+ // ── ContributorRow ──────────────────────────────────────────────
84
+
85
+ export function ContributorRow({ contributors }) {
86
+ if (!contributors || contributors.length === 0) return null;
87
+
88
+ return h('div', { className: 'eg-contributors' },
89
+ ...contributors.map((c, i) =>
90
+ h('div', { key: i, className: 'eg-contributor' },
91
+ h('span', { className: 'eg-contributor-avatar' },
92
+ (c.name || '?')[0].toUpperCase()
93
+ ),
94
+ h('span', null, c.name),
95
+ c.role && h('span', { style: { color: colors.muted, fontSize: '12px' } }, c.role),
96
+ )
97
+ ),
98
+ );
99
+ }
100
+
101
+ // ── BulletList ──────────────────────────────────────────────────
102
+
103
+ export function BulletList({ items }) {
104
+ if (!items || items.length === 0) return null;
105
+
106
+ return h('ul', { className: 'eg-bullet-list' },
107
+ ...items.map((item, i) =>
108
+ h('li', { key: i }, item)
109
+ ),
110
+ );
111
+ }
112
+
113
+ // ── TextBlock ───────────────────────────────────────────────────
114
+
115
+ export function TextBlock({ text, variant }) {
116
+ const className = variant === 'lead' ? 'eg-lead' : 'eg-body';
117
+ return h('div', { className }, renderMarkdown(text));
118
+ }
119
+
120
+ // ── SectionDivider ──────────────────────────────────────────────
121
+
122
+ export function SectionDivider() {
123
+ return h('div', { className: 'eg-section-divider' });
124
+ }
125
+
126
+ // ── ArtifactFooter ──────────────────────────────────────────────
127
+
128
+ export function ArtifactFooter({ generatedAt, source }) {
129
+ const dateStr = new Date(generatedAt).toLocaleDateString('en-US', {
130
+ year: 'numeric', month: 'short', day: 'numeric',
131
+ hour: '2-digit', minute: '2-digit',
132
+ });
133
+
134
+ return h('footer', { className: 'eg-footer' },
135
+ h('div', { className: 'eg-footer-brand' },
136
+ h('span', { className: 'eg-footer-sigil' }, '✦'),
137
+ h('span', null, 'egregore'),
138
+ ),
139
+ h('div', null,
140
+ source && h('span', null, `${source} · `),
141
+ h('span', null, dateStr),
142
+ ),
143
+ );
144
+ }
package/lib/index.js ADDED
@@ -0,0 +1,51 @@
1
+ // Public API for egregore-artifacts
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { renderToHtml, renderSpecToHtml } from './render.js';
6
+ import { parseQuest } from './parsers/quest.js';
7
+ import { parseHandoff } from './parsers/handoff.js';
8
+ import { parseActivity } from './parsers/activity.js';
9
+ import { questTemplate } from './templates/quest.js';
10
+ import { handoffTemplate } from './templates/handoff.js';
11
+ import { activityTemplate } from './templates/activity.js';
12
+ import { openInBrowser } from './open.js';
13
+
14
+ const PARSERS = { quest: parseQuest, handoff: parseHandoff, activity: parseActivity };
15
+ const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, activity: activityTemplate };
16
+
17
+ export async function generateArtifact(type, input) {
18
+ const template = TEMPLATES[type];
19
+ if (!template) throw new Error(`Unknown artifact type: ${type}. Available: ${Object.keys(TEMPLATES).join(', ')}`);
20
+
21
+ // Input can be a file path or pre-parsed data object
22
+ let data;
23
+ if (typeof input === 'string') {
24
+ const parser = PARSERS[type];
25
+ data = parser(input);
26
+ } else {
27
+ data = input;
28
+ }
29
+
30
+ const element = template(data);
31
+ return renderToHtml(element, { title: data.title || 'Untitled', type });
32
+ }
33
+
34
+ export async function openArtifact(html, title = 'Egregore Artifact') {
35
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 50);
36
+ const tmpDir = path.join(os.tmpdir(), 'egregore-artifacts');
37
+ fs.mkdirSync(tmpDir, { recursive: true });
38
+
39
+ const filePath = path.join(tmpDir, `${slug}-${Date.now()}.html`);
40
+ fs.writeFileSync(filePath, html);
41
+ openInBrowser(filePath);
42
+
43
+ return filePath;
44
+ }
45
+
46
+ export { parseQuest } from './parsers/quest.js';
47
+ export { parseHandoff } from './parsers/handoff.js';
48
+ export { renderSpecToHtml } from './render.js';
49
+ export { catalog, getCatalogPrompt } from './catalog.js';
50
+ export { registry } from './registry.js';
51
+ export * as tokens from './tokens.js';