erne-universal 0.2.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/README.md +92 -26
- package/agents/feature-builder.md +88 -0
- package/agents/senior-developer.md +77 -0
- package/bin/cli.js +4 -2
- package/dashboard/package.json +10 -0
- package/dashboard/public/agents.js +329 -0
- package/dashboard/public/canvas.js +275 -0
- package/dashboard/public/index.html +113 -0
- package/dashboard/public/sidebar.js +107 -0
- package/dashboard/public/ws-client.js +69 -0
- package/dashboard/server.js +191 -0
- package/docs/assets/dashboard-preview.png +0 -0
- package/docs/superpowers/plans/2026-03-11-agent-dashboard.md +1537 -0
- package/docs/superpowers/specs/2026-03-11-agent-dashboard-design.md +275 -0
- package/hooks/hooks.json +14 -0
- package/lib/dashboard.js +156 -0
- package/lib/init.js +294 -0
- package/lib/start.js +26 -0
- package/lib/update.js +60 -0
- package/package.json +3 -1
- package/scripts/daily-news/scan-ai-agents.js +222 -0
- package/scripts/daily-news/scan-rn-expo.js +233 -0
- package/scripts/hooks/dashboard-event.js +89 -0
- package/scripts/sync/issue-to-clickup.js +108 -0
- package/scripts/validate-all.js +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Daily React Native & Expo News Scanner
|
|
5
|
+
*
|
|
6
|
+
* Scans multiple sources for RN/Expo updates and creates ClickUp tasks.
|
|
7
|
+
* Uses keyword-based relevance filtering (no API credits needed).
|
|
8
|
+
*
|
|
9
|
+
* Sources:
|
|
10
|
+
* - GitHub releases (react-native, expo, key libraries)
|
|
11
|
+
* - React Native blog RSS
|
|
12
|
+
* - Expo blog RSS
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SOURCES = {
|
|
16
|
+
github_releases: [
|
|
17
|
+
'facebook/react-native',
|
|
18
|
+
'expo/expo',
|
|
19
|
+
'expo/router',
|
|
20
|
+
'software-mansion/react-native-reanimated',
|
|
21
|
+
'software-mansion/react-native-screens',
|
|
22
|
+
'Shopify/flash-list',
|
|
23
|
+
'callstack/react-native-paper',
|
|
24
|
+
'react-native-community/cli',
|
|
25
|
+
'mrousavy/react-native-vision-camera',
|
|
26
|
+
'margelo/react-native-nitro-modules',
|
|
27
|
+
],
|
|
28
|
+
rss_feeds: [
|
|
29
|
+
'https://reactnative.dev/blog/rss.xml',
|
|
30
|
+
'https://blog.expo.dev/feed',
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CLICKUP_API = 'https://api.clickup.com/api/v2';
|
|
35
|
+
|
|
36
|
+
// Keywords for prioritization
|
|
37
|
+
const HIGH_PRIORITY_KEYWORDS = [
|
|
38
|
+
'breaking', 'deprecat', 'removed', 'migration', 'upgrade required',
|
|
39
|
+
'security', 'critical', 'major',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const ERNE_KEYWORDS = [
|
|
43
|
+
'expo-router', 'file-based', 'typescript', 'hermes',
|
|
44
|
+
'new architecture', 'fabric', 'turbo', 'bridgeless',
|
|
45
|
+
'flashlist', 'reanimated', 'gesture',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
async function fetchGitHubReleases(repo) {
|
|
49
|
+
const res = await fetch(
|
|
50
|
+
`https://api.github.com/repos/${repo}/releases?per_page=3`,
|
|
51
|
+
{
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
|
54
|
+
Accept: 'application/vnd.github.v3+json',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
if (!res.ok) return [];
|
|
59
|
+
const releases = await res.json();
|
|
60
|
+
|
|
61
|
+
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
62
|
+
return releases
|
|
63
|
+
.filter((r) => new Date(r.published_at).getTime() > oneDayAgo)
|
|
64
|
+
.map((r) => ({
|
|
65
|
+
type: 'release',
|
|
66
|
+
source: repo,
|
|
67
|
+
title: `${repo} ${r.tag_name}`,
|
|
68
|
+
body: r.body?.slice(0, 1000) || '',
|
|
69
|
+
url: r.html_url,
|
|
70
|
+
date: r.published_at,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchRSS(url) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(url);
|
|
77
|
+
if (!res.ok) return [];
|
|
78
|
+
const xml = await res.text();
|
|
79
|
+
|
|
80
|
+
const items = [];
|
|
81
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
82
|
+
let match;
|
|
83
|
+
while ((match = itemRegex.exec(xml)) !== null) {
|
|
84
|
+
const item = match[1];
|
|
85
|
+
const title = item.match(/<title><!\[CDATA\[(.*?)\]\]>|<title>(.*?)<\/title>/);
|
|
86
|
+
const link = item.match(/<link>(.*?)<\/link>/);
|
|
87
|
+
const pubDate = item.match(/<pubDate>(.*?)<\/pubDate>/);
|
|
88
|
+
const desc = item.match(/<description><!\[CDATA\[(.*?)\]\]>|<description>(.*?)<\/description>/);
|
|
89
|
+
|
|
90
|
+
if (title && pubDate) {
|
|
91
|
+
const date = new Date(pubDate[1]);
|
|
92
|
+
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
93
|
+
if (date.getTime() > oneDayAgo) {
|
|
94
|
+
items.push({
|
|
95
|
+
type: 'blog',
|
|
96
|
+
source: new URL(url).hostname,
|
|
97
|
+
title: title[1] || title[2],
|
|
98
|
+
body: (desc?.[1] || desc?.[2] || '').slice(0, 500),
|
|
99
|
+
url: link?.[1] || '',
|
|
100
|
+
date: date.toISOString(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return items;
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function analyzeRelevance(items) {
|
|
112
|
+
if (items.length === 0) return [];
|
|
113
|
+
|
|
114
|
+
return items.map((item) => {
|
|
115
|
+
const text = `${item.title} ${item.body}`.toLowerCase();
|
|
116
|
+
|
|
117
|
+
const isHighPriority = HIGH_PRIORITY_KEYWORDS.some((kw) => text.includes(kw));
|
|
118
|
+
const isErneRelevant = ERNE_KEYWORDS.some((kw) => text.includes(kw));
|
|
119
|
+
const isRelease = item.type === 'release';
|
|
120
|
+
const isBlog = item.type === 'blog';
|
|
121
|
+
|
|
122
|
+
// All releases and blog posts are relevant
|
|
123
|
+
if (!isRelease && !isBlog && !isHighPriority && !isErneRelevant) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const priority = isHighPriority ? 'high' : isErneRelevant ? 'normal' : 'low';
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
title: `[${item.type.toUpperCase()}] ${item.title}`,
|
|
131
|
+
description: item.body.slice(0, 300) || 'New update detected',
|
|
132
|
+
action: isHighPriority
|
|
133
|
+
? 'Urgent: Check for breaking changes affecting ERNE'
|
|
134
|
+
: 'Review for potential ERNE updates',
|
|
135
|
+
priority,
|
|
136
|
+
source_url: item.url,
|
|
137
|
+
};
|
|
138
|
+
}).filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function createClickUpTask(task) {
|
|
142
|
+
const listId = process.env.CLICKUP_LIST_ID;
|
|
143
|
+
if (!listId) {
|
|
144
|
+
console.log('CLICKUP_LIST_ID not set, skipping task creation');
|
|
145
|
+
console.log('Task:', task.title);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const priorityMap = { urgent: 1, high: 2, normal: 3, low: 4 };
|
|
150
|
+
|
|
151
|
+
const res = await fetch(`${CLICKUP_API}/list/${listId}/task`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
Authorization: process.env.CLICKUP_API_TOKEN,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
name: task.title,
|
|
159
|
+
markdown_description: [
|
|
160
|
+
`## Summary`,
|
|
161
|
+
task.description,
|
|
162
|
+
'',
|
|
163
|
+
`## Action Required`,
|
|
164
|
+
task.action,
|
|
165
|
+
'',
|
|
166
|
+
`**Source:** [${task.source_url}](${task.source_url})`,
|
|
167
|
+
'',
|
|
168
|
+
`---`,
|
|
169
|
+
`*Auto-generated by ERNE Daily Scanner*`,
|
|
170
|
+
].join('\n'),
|
|
171
|
+
priority: priorityMap[task.priority] || 3,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (res.ok) {
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
console.log(`Created task: ${task.title} (${data.id})`);
|
|
178
|
+
} else {
|
|
179
|
+
console.error(`Failed to create task: ${task.title}`, res.status);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function main() {
|
|
184
|
+
console.log('=== ERNE Daily RN/Expo News Scanner ===');
|
|
185
|
+
console.log(`Date: ${new Date().toISOString()}\n`);
|
|
186
|
+
|
|
187
|
+
const allItems = [];
|
|
188
|
+
|
|
189
|
+
// GitHub releases
|
|
190
|
+
console.log('Fetching GitHub releases...');
|
|
191
|
+
for (const repo of SOURCES.github_releases) {
|
|
192
|
+
const releases = await fetchGitHubReleases(repo);
|
|
193
|
+
allItems.push(...releases);
|
|
194
|
+
if (releases.length > 0) {
|
|
195
|
+
console.log(` ${repo}: ${releases.length} new release(s)`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// RSS feeds
|
|
200
|
+
console.log('Fetching RSS feeds...');
|
|
201
|
+
for (const feed of SOURCES.rss_feeds) {
|
|
202
|
+
const posts = await fetchRSS(feed);
|
|
203
|
+
allItems.push(...posts);
|
|
204
|
+
if (posts.length > 0) {
|
|
205
|
+
console.log(` ${new URL(feed).hostname}: ${posts.length} new post(s)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`\nTotal items found: ${allItems.length}`);
|
|
210
|
+
|
|
211
|
+
if (allItems.length === 0) {
|
|
212
|
+
console.log('No new items today. Exiting.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log('\nAnalyzing relevance...');
|
|
217
|
+
const relevant = analyzeRelevance(allItems);
|
|
218
|
+
console.log(`Relevant items: ${relevant.length}`);
|
|
219
|
+
|
|
220
|
+
if (relevant.length > 0) {
|
|
221
|
+
console.log('\nCreating ClickUp tasks...');
|
|
222
|
+
for (const task of relevant) {
|
|
223
|
+
await createClickUpTask(task);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log('\nDone!');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch((err) => {
|
|
231
|
+
console.error('Scanner failed:', err);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
// Read stdin
|
|
8
|
+
let stdinData = '';
|
|
9
|
+
try {
|
|
10
|
+
stdinData = fs.readFileSync(0, 'utf8');
|
|
11
|
+
} catch {
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let hookContext;
|
|
16
|
+
try {
|
|
17
|
+
hookContext = JSON.parse(stdinData);
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DASHBOARD_PORT = parseInt(process.env.ERNE_DASHBOARD_PORT || '3333', 10);
|
|
23
|
+
|
|
24
|
+
const AGENT_KEYWORDS = [
|
|
25
|
+
'architect', 'code-reviewer', 'tdd-guide', 'performance-profiler',
|
|
26
|
+
'native-bridge-builder', 'expo-config-resolver', 'ui-designer', 'upgrade-assistant',
|
|
27
|
+
'senior-developer', 'feature-builder',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function detectAgent(text) {
|
|
31
|
+
if (!text) return null;
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
for (const keyword of AGENT_KEYWORDS) {
|
|
34
|
+
if (lower.includes(keyword)) return keyword;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractTaskDescription(text) {
|
|
40
|
+
if (!text) return '';
|
|
41
|
+
return text.slice(0, 100).split('\n')[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const event = hookContext.event || '';
|
|
45
|
+
const toolName = hookContext.tool_name || hookContext.toolName || '';
|
|
46
|
+
const toolInput = hookContext.tool_input || hookContext.toolInput || {};
|
|
47
|
+
|
|
48
|
+
if (toolName !== 'Agent' && toolName !== 'agent') {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prompt = toolInput.prompt || toolInput.description || '';
|
|
53
|
+
const agentName = detectAgent(prompt);
|
|
54
|
+
|
|
55
|
+
if (!agentName) {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let eventType;
|
|
60
|
+
if (event.toLowerCase().includes('pre')) {
|
|
61
|
+
eventType = 'agent:start';
|
|
62
|
+
} else if (event.toLowerCase().includes('post')) {
|
|
63
|
+
eventType = 'agent:complete';
|
|
64
|
+
} else {
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const payload = JSON.stringify({
|
|
69
|
+
type: eventType,
|
|
70
|
+
agent: agentName,
|
|
71
|
+
task: extractTaskDescription(prompt),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const req = http.request(
|
|
75
|
+
{
|
|
76
|
+
hostname: '127.0.0.1',
|
|
77
|
+
port: DASHBOARD_PORT,
|
|
78
|
+
path: '/api/events',
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
timeout: 2000,
|
|
82
|
+
},
|
|
83
|
+
() => { process.exit(0); }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
req.on('error', () => { process.exit(0); });
|
|
87
|
+
req.on('timeout', () => { req.destroy(); process.exit(0); });
|
|
88
|
+
req.write(payload);
|
|
89
|
+
req.end();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Issue → ClickUp Task Sync
|
|
5
|
+
*
|
|
6
|
+
* Routes issues to the correct ClickUp list based on labels:
|
|
7
|
+
* - bug → Dev Tasks
|
|
8
|
+
* - enhancement, feature → Backlog
|
|
9
|
+
* - partnership → Discussion
|
|
10
|
+
* - skill-proposal → Backlog
|
|
11
|
+
* - discussion, question → Discussion
|
|
12
|
+
* - Default → Backlog
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const CLICKUP_API = 'https://api.clickup.com/api/v2';
|
|
16
|
+
|
|
17
|
+
const LABEL_TO_LIST = {
|
|
18
|
+
bug: process.env.CLICKUP_DEV_LIST_ID,
|
|
19
|
+
enhancement: process.env.CLICKUP_BACKLOG_LIST_ID,
|
|
20
|
+
'skill-proposal': process.env.CLICKUP_BACKLOG_LIST_ID,
|
|
21
|
+
partnership: process.env.CLICKUP_DISCUSSION_LIST_ID,
|
|
22
|
+
discussion: process.env.CLICKUP_DISCUSSION_LIST_ID,
|
|
23
|
+
question: process.env.CLICKUP_DISCUSSION_LIST_ID,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const LABEL_TO_PRIORITY = {
|
|
27
|
+
bug: 2, // high
|
|
28
|
+
enhancement: 3, // normal
|
|
29
|
+
'skill-proposal': 3,
|
|
30
|
+
partnership: 3,
|
|
31
|
+
discussion: 4, // low
|
|
32
|
+
question: 4,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getListId(labels) {
|
|
36
|
+
const labelList = labels.split(',').map((l) => l.trim().toLowerCase());
|
|
37
|
+
for (const label of labelList) {
|
|
38
|
+
if (LABEL_TO_LIST[label]) return LABEL_TO_LIST[label];
|
|
39
|
+
}
|
|
40
|
+
return process.env.CLICKUP_BACKLOG_LIST_ID;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getPriority(labels) {
|
|
44
|
+
const labelList = labels.split(',').map((l) => l.trim().toLowerCase());
|
|
45
|
+
for (const label of labelList) {
|
|
46
|
+
if (LABEL_TO_PRIORITY[label]) return LABEL_TO_PRIORITY[label];
|
|
47
|
+
}
|
|
48
|
+
return 3; // normal
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function createClickUpTask() {
|
|
52
|
+
const { ISSUE_TITLE, ISSUE_BODY, ISSUE_URL, ISSUE_LABELS, ISSUE_NUMBER, ISSUE_AUTHOR } =
|
|
53
|
+
process.env;
|
|
54
|
+
|
|
55
|
+
const listId = getListId(ISSUE_LABELS || '');
|
|
56
|
+
if (!listId) {
|
|
57
|
+
console.error('No ClickUp list ID configured for labels:', ISSUE_LABELS);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const priority = getPriority(ISSUE_LABELS || '');
|
|
62
|
+
const labels = (ISSUE_LABELS || '').split(',').filter(Boolean);
|
|
63
|
+
|
|
64
|
+
const description = [
|
|
65
|
+
`## GitHub Issue #${ISSUE_NUMBER}`,
|
|
66
|
+
'',
|
|
67
|
+
`**Author:** @${ISSUE_AUTHOR}`,
|
|
68
|
+
`**Labels:** ${labels.length > 0 ? labels.join(', ') : 'none'}`,
|
|
69
|
+
`**Link:** [#${ISSUE_NUMBER} on GitHub](${ISSUE_URL})`,
|
|
70
|
+
'',
|
|
71
|
+
'---',
|
|
72
|
+
'',
|
|
73
|
+
ISSUE_BODY || '*No description provided*',
|
|
74
|
+
'',
|
|
75
|
+
'---',
|
|
76
|
+
`*Synced from GitHub by ERNE Issue Sync*`,
|
|
77
|
+
].join('\n');
|
|
78
|
+
|
|
79
|
+
const res = await fetch(`${CLICKUP_API}/list/${listId}/task`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
Authorization: process.env.CLICKUP_API_TOKEN,
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
name: `[GH-${ISSUE_NUMBER}] ${ISSUE_TITLE}`,
|
|
87
|
+
markdown_description: description,
|
|
88
|
+
priority,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (res.ok) {
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
console.log(`Created ClickUp task: ${data.id} for GitHub issue #${ISSUE_NUMBER}`);
|
|
95
|
+
console.log(` List: ${listId}`);
|
|
96
|
+
console.log(` Priority: ${priority}`);
|
|
97
|
+
console.log(` Labels: ${labels.join(', ')}`);
|
|
98
|
+
} else {
|
|
99
|
+
const err = await res.text();
|
|
100
|
+
console.error(`Failed to create ClickUp task:`, res.status, err);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
createClickUpTask().catch((err) => {
|
|
106
|
+
console.error('Sync failed:', err);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
package/scripts/validate-all.js
CHANGED
|
@@ -60,7 +60,7 @@ console.log('\n ERNE Content Validation\n');
|
|
|
60
60
|
|
|
61
61
|
// Agents
|
|
62
62
|
console.log(' Agents:');
|
|
63
|
-
validateCount('agents', '.md',
|
|
63
|
+
validateCount('agents', '.md', 10, 'agents/');
|
|
64
64
|
const agentFiles = fs.readdirSync('agents').filter(f => f.endsWith('.md'));
|
|
65
65
|
for (const f of agentFiles) {
|
|
66
66
|
validateFrontmatter(path.join('agents', f), ['name', 'description']);
|