egregore-artifacts 0.10.1 → 0.10.2

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/lib/comments.js DELETED
@@ -1,248 +0,0 @@
1
- // Comment thread + composer — renders inline at the bottom of any
2
- // `/view`-rendered artifact. Reads markdown files from
3
- // `memory/comments/{kind}/{id}/*.md`. No graph access — filesystem only,
4
- // so it works in both connected and local mode.
5
- import React from 'react';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { execSync } from 'node:child_process';
9
- import { renderMarkdown } from './markdown.js';
10
-
11
- const h = React.createElement;
12
-
13
- // ── Path resolution ─────────────────────────────────────────────
14
-
15
- function findGitRoot(startPath) {
16
- try {
17
- return execSync('git rev-parse --show-toplevel 2>/dev/null', {
18
- encoding: 'utf-8',
19
- cwd: startPath || process.cwd(),
20
- }).trim();
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- // Derive (parentKind, parentId) for a rendered artifact.
27
- // Mirrors the indexer's parent_kind enum: quest|handoff|card|decision|document.
28
- export function derivedParent(type, filePath) {
29
- if (!filePath || typeof filePath !== 'string') return null;
30
-
31
- const base = path.basename(filePath, '.md');
32
-
33
- if (type === 'quest') {
34
- return { kind: 'quest', id: base };
35
- }
36
- if (type === 'handoff') {
37
- // sessionId can be either YYYY-MM-DD-author-topic (flat) or DD-author-topic
38
- // inside a YYYY-MM/ subdir. Mirror bin/index-handoff.sh's id derivation.
39
- const dir = path.basename(path.dirname(filePath));
40
- if (/^\d{4}-\d{2}$/.test(dir)) {
41
- return { kind: 'handoff', id: `${dir}-${base}` };
42
- }
43
- return { kind: 'handoff', id: base };
44
- }
45
- if (type === 'document') {
46
- if (filePath.includes('/knowledge/decisions/')) {
47
- return { kind: 'decision', id: base };
48
- }
49
- return { kind: 'document', id: base };
50
- }
51
- return null;
52
- }
53
-
54
- // ── Loader ──────────────────────────────────────────────────────
55
-
56
- export function loadComments(parent, gitRootHint) {
57
- if (!parent || !parent.kind || !parent.id) return [];
58
-
59
- const gitRoot = gitRootHint || findGitRoot();
60
- if (!gitRoot) return [];
61
-
62
- const dir = path.join(gitRoot, 'memory', 'comments', parent.kind, parent.id);
63
- if (!fs.existsSync(dir)) return [];
64
-
65
- const files = fs.readdirSync(dir)
66
- .filter(f => f.endsWith('.md'))
67
- .sort();
68
-
69
- return files.map(f => parseCommentFile(path.join(dir, f))).filter(Boolean);
70
- }
71
-
72
- function parseCommentFile(filePath) {
73
- const raw = fs.readFileSync(filePath, 'utf-8');
74
- const id = path.basename(filePath, '.md');
75
-
76
- const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
77
- if (!fmMatch) return null;
78
-
79
- const fm = parseSimpleYaml(fmMatch[1]);
80
- const body = fmMatch[2].trim();
81
-
82
- return {
83
- id,
84
- parentKind: fm.parent_kind,
85
- parentId: fm.parent_id,
86
- author: fm.author,
87
- to: fm.to || [],
88
- replyTo: fm.reply_to || null,
89
- createdAt: fm.created || null,
90
- body,
91
- };
92
- }
93
-
94
- function parseSimpleYaml(text) {
95
- const result = {};
96
- for (const line of text.split('\n')) {
97
- const m = line.match(/^([\w_-]+):\s*(.*)$/);
98
- if (!m) continue;
99
- const [, key, rawVal] = m;
100
- let val = rawVal.trim();
101
- if (val.startsWith('[') && val.endsWith(']')) {
102
- val = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
103
- } else if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
104
- val = val.slice(1, -1);
105
- } else if (val === 'null' || val === '') {
106
- val = null;
107
- }
108
- result[key] = val;
109
- }
110
- return result;
111
- }
112
-
113
- // ── Threading ───────────────────────────────────────────────────
114
-
115
- // Build a tree from a flat list using replyTo. Top-level items keep their order;
116
- // each child renders inline under its parent. Orphaned replies (parent missing)
117
- // are promoted to top-level so nothing disappears.
118
- function buildThreads(comments) {
119
- const byId = new Map(comments.map(c => [c.id, { ...c, children: [] }]));
120
- const roots = [];
121
- for (const c of byId.values()) {
122
- if (c.replyTo && byId.has(c.replyTo)) {
123
- byId.get(c.replyTo).children.push(c);
124
- } else {
125
- roots.push(c);
126
- }
127
- }
128
- return roots;
129
- }
130
-
131
- function formatDate(iso) {
132
- if (!iso) return '';
133
- try {
134
- return new Date(iso).toLocaleDateString('en-US', {
135
- month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
136
- });
137
- } catch {
138
- return iso;
139
- }
140
- }
141
-
142
- // ── Components ──────────────────────────────────────────────────
143
-
144
- function Comment({ comment, depth }) {
145
- const indent = depth > 0 ? { marginLeft: `${Math.min(depth, 3) * 1.5}rem` } : {};
146
- const recipients = (comment.to || []).filter(Boolean);
147
-
148
- return h('div', { className: 'eg-comment', style: indent, 'data-comment-id': comment.id },
149
- h('div', { className: 'eg-comment-meta' },
150
- h('span', { className: 'eg-comment-avatar' },
151
- (comment.author || '?')[0].toUpperCase()
152
- ),
153
- h('span', { className: 'eg-comment-author' }, comment.author || 'unknown'),
154
- comment.createdAt && h('span', { className: 'eg-comment-date' }, formatDate(comment.createdAt)),
155
- recipients.length > 0 && h('span', { className: 'eg-comment-to' },
156
- '→ ', recipients.join(', ')
157
- ),
158
- ),
159
- h('div', { className: 'eg-comment-body' }, renderMarkdown(comment.body)),
160
- h('div', { className: 'eg-comment-actions' },
161
- h('button', {
162
- type: 'button',
163
- className: 'eg-comment-reply',
164
- 'data-reply-to': comment.id,
165
- 'data-reply-author': comment.author || '',
166
- }, '↩ reply'),
167
- ),
168
- ...(comment.children || []).map(child =>
169
- h(Comment, { key: child.id, comment: child, depth: depth + 1 })
170
- ),
171
- );
172
- }
173
-
174
- function CommentThread({ comments }) {
175
- if (!comments || comments.length === 0) {
176
- return h('div', { className: 'eg-comments-empty' },
177
- 'No comments yet. Be the first.'
178
- );
179
- }
180
- const roots = buildThreads(comments);
181
- return h('div', { className: 'eg-comments-thread' },
182
- ...roots.map(c => h(Comment, { key: c.id, comment: c, depth: 0 }))
183
- );
184
- }
185
-
186
- function CommentComposer({ parent }) {
187
- return h('div', {
188
- className: 'eg-comments-composer',
189
- 'data-parent-kind': parent.kind,
190
- 'data-parent-id': parent.id,
191
- },
192
- h('div', { className: 'eg-card-title' },
193
- h('span', { className: 'eg-live-only' }, 'Add a comment to '),
194
- h('span', { className: 'eg-paste-only' }, 'Compose a comment on '),
195
- h('code', { className: 'eg-comment-target' }, `${parent.kind}/${parent.id}`),
196
- ),
197
- h('textarea', {
198
- className: 'eg-comment-textarea',
199
- placeholder: 'Type your comment. @handle to ping someone. Markdown is supported.',
200
- rows: 4,
201
- }),
202
-
203
- h('div', { className: 'eg-comment-preview-label' },
204
- h('span', { className: 'eg-live-only' }, 'Preview:'),
205
- h('span', { className: 'eg-paste-only' }, 'Paste this in your Claude terminal:'),
206
- ),
207
- h('div', { className: 'eg-comment-preview-frame' },
208
- h('pre', { className: 'eg-comment-preview' },
209
- h('code', { className: 'eg-comment-preview-code' }, ''),
210
- ),
211
- h('button', {
212
- type: 'button',
213
- className: 'eg-comment-copy-btn',
214
- 'aria-label': 'Copy snippet',
215
- }, 'Copy'),
216
- ),
217
- h('div', { className: 'eg-comment-status', 'aria-live': 'polite' }),
218
-
219
- // Primary action — only visible when the loopback server is reachable.
220
- h('button', {
221
- type: 'button',
222
- className: 'eg-comment-post-btn eg-live-only',
223
- }, 'Post comment'),
224
-
225
- h('div', { className: 'eg-comment-help' },
226
- h('span', { className: 'eg-live-only' },
227
- 'Posts directly via the local /view server. Writes the file, indexes the graph, fires Telegram for any @mentions, and commits to memory.',
228
- ),
229
- h('span', { className: 'eg-paste-only' },
230
- 'The browser can’t write to your filesystem or run git, so the snippet is the handoff. Claude recognizes the marker and runs the publish flow.',
231
- ),
232
- ),
233
- );
234
- }
235
-
236
- export function CommentSection({ parent, comments }) {
237
- if (!parent) return null;
238
- return h('section', { className: 'eg-comments-section' },
239
- h('div', { className: 'eg-comments-heading' },
240
- h('span', null, 'Comments'),
241
- comments && comments.length > 0 && h('span', { className: 'eg-comments-count' },
242
- ` (${comments.length})`
243
- ),
244
- ),
245
- h(CommentThread, { comments }),
246
- h(CommentComposer, { parent }),
247
- );
248
- }