mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 10:43:10 +08:00
release: 2.0.0 — the agent harness operating system
Graduate 2.0.0-rc.1 to stable. Bump version across package, plugin, marketplace, OpenCode, agent metadata, VERSION, and all localized docs. Add 2.0.0 release notes + README sections (en/zh/pt-BR/tr), CHANGELOG entry, and the ECC community Discord bot (dependency-free gateway client + guild command registrar). Update copilot-support and release-surface tests for the sponsored-review migration and the 2.0.0 surface.
This commit is contained in:
222
scripts/discord/ecc-bot.mjs
Normal file
222
scripts/discord/ecc-bot.mjs
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
// ECC community Discord bot — dependency-free (Node 22+ native WebSocket).
|
||||
// Slash commands: /ecc /help /skill /docs /release
|
||||
//
|
||||
// Env: DISCORD_BOT_TOKEN (required), DISCORD_APP_ID (required),
|
||||
// ECC_REPO (path to local clone, default ~/GitHub/ECC/everything-claude-code),
|
||||
// DISCORD_INVITE (optional, shown in /ecc)
|
||||
//
|
||||
// Crash-only design: any gateway close, error, or missed heartbeat ack exits
|
||||
// the process; the launchd/pm2 supervisor restarts it with a fresh identify.
|
||||
// Register commands first: node scripts/discord/register-commands.mjs
|
||||
'use strict';
|
||||
|
||||
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const TOKEN = process.env.DISCORD_BOT_TOKEN;
|
||||
const APP_ID = process.env.DISCORD_APP_ID;
|
||||
if (!TOKEN || !APP_ID) {
|
||||
console.error('missing DISCORD_BOT_TOKEN / DISCORD_APP_ID');
|
||||
process.exit(1);
|
||||
}
|
||||
const REPO = process.env.ECC_REPO || join(homedir(), 'GitHub/ECC/everything-claude-code');
|
||||
const REPO_URL = 'https://github.com/affaan-m/ECC';
|
||||
const INVITE = process.env.DISCORD_INVITE || '';
|
||||
const API = 'https://discord.com/api/v10';
|
||||
|
||||
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
||||
|
||||
// ---------- skill + docs lookup (local clone as the data source) ----------
|
||||
|
||||
function parseFrontmatter(text) {
|
||||
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!m) return {};
|
||||
const out = {};
|
||||
for (const line of m[1].split('\n')) {
|
||||
const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
||||
if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, '');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loadSkills() {
|
||||
const dir = join(REPO, 'skills');
|
||||
if (!existsSync(dir)) return [];
|
||||
const skills = [];
|
||||
for (const name of readdirSync(dir)) {
|
||||
const md = join(dir, name, 'SKILL.md');
|
||||
if (!existsSync(md)) continue;
|
||||
try {
|
||||
const fm = parseFrontmatter(readFileSync(md, 'utf8'));
|
||||
skills.push({ name, description: fm.description || '(no description)' });
|
||||
} catch { /* unreadable skill dirs are skipped, not fatal */ }
|
||||
}
|
||||
return skills;
|
||||
}
|
||||
|
||||
function findSkill(query) {
|
||||
const q = query.toLowerCase().trim().replace(/\s+/g, '-');
|
||||
const skills = loadSkills();
|
||||
const exact = skills.find(s => s.name === q);
|
||||
const ranked = exact
|
||||
? [exact, ...skills.filter(s => s !== exact && s.name.includes(q))]
|
||||
: skills.filter(s => s.name.includes(q) || s.description.toLowerCase().includes(query.toLowerCase()));
|
||||
return ranked.slice(0, 5);
|
||||
}
|
||||
|
||||
function searchDocs(query) {
|
||||
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const hits = [];
|
||||
const roots = ['docs', 'README.md'];
|
||||
const walk = rel => {
|
||||
const abs = join(REPO, rel);
|
||||
if (!existsSync(abs)) return;
|
||||
if (statSync(abs).isDirectory()) {
|
||||
for (const f of readdirSync(abs)) walk(join(rel, f));
|
||||
return;
|
||||
}
|
||||
if (!rel.endsWith('.md')) return;
|
||||
const nameScore = terms.filter(t => rel.toLowerCase().includes(t)).length;
|
||||
let score = nameScore * 3;
|
||||
if (nameScore < terms.length) {
|
||||
try {
|
||||
const head = readFileSync(abs, 'utf8').slice(0, 4000).toLowerCase();
|
||||
score += terms.filter(t => head.includes(t)).length;
|
||||
} catch { /* skip unreadable */ }
|
||||
}
|
||||
if (score > 0) hits.push({ rel, score });
|
||||
};
|
||||
for (const r of roots) walk(r);
|
||||
return hits.sort((a, b) => b.score - a.score).slice(0, 5);
|
||||
}
|
||||
|
||||
// ---------- command handlers ----------
|
||||
|
||||
const HELP = [
|
||||
'**ECC bot commands**',
|
||||
'- `/ecc` — what ECC is + all the links',
|
||||
'- `/skill name:<query>` — look up an ECC skill',
|
||||
'- `/docs query:<terms>` — search the ECC docs',
|
||||
'- `/release` — latest ECC release',
|
||||
'- `/help` — this message',
|
||||
].join('\n');
|
||||
|
||||
const handlers = {
|
||||
ecc: () => [
|
||||
'**Everything Claude Code (ECC)** — the agent harness performance system.',
|
||||
'Skills, agents, rules, hooks, MCP conventions, and operator workflows that move across Claude Code, Codex, OpenCode, Cursor, Gemini, and Zed.',
|
||||
'',
|
||||
`- repo: ${REPO_URL}`,
|
||||
'- site: https://ecc.tools',
|
||||
`- install: \`/plugin marketplace add affaan-m/everything-claude-code\` then \`/plugin install ecc\``,
|
||||
INVITE ? `- invite a friend: ${INVITE}` : '',
|
||||
].filter(Boolean).join('\n'),
|
||||
|
||||
help: () => HELP,
|
||||
|
||||
skill: (options) => {
|
||||
const query = options.find(o => o.name === 'name')?.value || '';
|
||||
const found = findSkill(query);
|
||||
if (!found.length) return `no skill matching \`${query}\` — browse all: ${REPO_URL}/tree/main/skills`;
|
||||
const [top, ...rest] = found;
|
||||
return [
|
||||
`**${top.name}** — ${top.description}`,
|
||||
`${REPO_URL}/tree/main/skills/${top.name}`,
|
||||
rest.length ? `\nalso close: ${rest.map(s => `\`${s.name}\``).join(', ')}` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
},
|
||||
|
||||
docs: (options) => {
|
||||
const query = options.find(o => o.name === 'query')?.value || '';
|
||||
const hits = searchDocs(query);
|
||||
if (!hits.length) return `nothing found for \`${query}\` — try ${REPO_URL}/tree/main/docs`;
|
||||
return [`**docs matching \`${query}\`:**`, ...hits.map(h => `- ${REPO_URL}/blob/main/${h.rel.replace(/\\/g, '/')}`)].join('\n');
|
||||
},
|
||||
|
||||
release: async () => {
|
||||
const res = await fetch('https://api.github.com/repos/affaan-m/ECC/releases/latest', {
|
||||
headers: { 'User-Agent': 'ecc-discord-bot' },
|
||||
});
|
||||
if (!res.ok) return `couldn't reach GitHub (${res.status}) — ${REPO_URL}/releases`;
|
||||
const r = await res.json();
|
||||
return `**${r.name || r.tag_name}**\n${r.html_url}`;
|
||||
},
|
||||
};
|
||||
|
||||
async function respond(interaction) {
|
||||
const name = interaction.data?.name;
|
||||
const handler = handlers[name];
|
||||
const url = `${API}/interactions/${interaction.id}/${interaction.token}/callback`;
|
||||
if (!handler) {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 4, data: { content: `unknown command \`${name}\`` } }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = await handler(interaction.data?.options || []);
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 4, data: { content: String(content).slice(0, 1990) } }),
|
||||
});
|
||||
log('handled', `/${name}`);
|
||||
} catch (err) {
|
||||
log('handler error', name, err.message);
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 4, data: { content: 'something broke handling that — try again in a minute' } }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- gateway (crash-only: exit on any failure, supervisor restarts) ----------
|
||||
|
||||
let seq = null;
|
||||
let acked = true;
|
||||
|
||||
async function main() {
|
||||
const gw = await fetch(`${API}/gateway/bot`, { headers: { Authorization: `Bot ${TOKEN}` } }).then(r => r.json());
|
||||
if (!gw.url) { console.error('gateway discovery failed:', JSON.stringify(gw).slice(0, 200)); process.exit(1); }
|
||||
const ws = new WebSocket(`${gw.url}?v=10&encoding=json`);
|
||||
const send = payload => ws.send(JSON.stringify(payload));
|
||||
const die = reason => { log('exiting:', reason); process.exit(1); };
|
||||
|
||||
ws.onmessage = ev => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.s) seq = msg.s;
|
||||
switch (msg.op) {
|
||||
case 10: { // HELLO
|
||||
const interval = msg.d.heartbeat_interval;
|
||||
setTimeout(() => {
|
||||
send({ op: 1, d: seq });
|
||||
setInterval(() => {
|
||||
if (!acked) die('missed heartbeat ack');
|
||||
acked = false;
|
||||
send({ op: 1, d: seq });
|
||||
}, interval);
|
||||
}, interval * Math.random());
|
||||
send({ op: 2, d: { token: TOKEN, intents: 1, properties: { os: 'darwin', browser: 'ecc-bot', device: 'ecc-bot' } } });
|
||||
break;
|
||||
}
|
||||
case 11: acked = true; break; // HEARTBEAT_ACK
|
||||
case 1: send({ op: 1, d: seq }); break; // server-requested heartbeat
|
||||
case 7: die('server requested reconnect'); break;
|
||||
case 9: die('invalid session'); break;
|
||||
case 0:
|
||||
if (msg.t === 'READY') log(`READY as ${msg.d.user.username}#${msg.d.user.discriminator}`);
|
||||
if (msg.t === 'INTERACTION_CREATE' && msg.d.type === 2) respond(msg.d);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
};
|
||||
ws.onclose = ev => die(`gateway closed (${ev.code})`);
|
||||
ws.onerror = () => die('gateway error');
|
||||
}
|
||||
|
||||
main().catch(err => { console.error('fatal:', err.message); process.exit(1); });
|
||||
38
scripts/discord/register-commands.mjs
Normal file
38
scripts/discord/register-commands.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
// Registers the ECC bot's guild slash commands (bulk overwrite, instant).
|
||||
// Env: DISCORD_BOT_TOKEN, DISCORD_APP_ID, DISCORD_GUILD_ID
|
||||
'use strict';
|
||||
|
||||
const { DISCORD_BOT_TOKEN: TOKEN, DISCORD_APP_ID: APP_ID, DISCORD_GUILD_ID: GUILD } = process.env;
|
||||
if (!TOKEN || !APP_ID || !GUILD) {
|
||||
console.error('missing DISCORD_BOT_TOKEN / DISCORD_APP_ID / DISCORD_GUILD_ID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const COMMANDS = [
|
||||
{ name: 'ecc', description: 'What ECC is + all the links' },
|
||||
{ name: 'help', description: 'List ECC bot commands' },
|
||||
{
|
||||
name: 'skill',
|
||||
description: 'Look up an ECC skill by name',
|
||||
options: [{ type: 3, name: 'name', description: 'skill name or keyword', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
description: 'Search the ECC docs',
|
||||
options: [{ type: 3, name: 'query', description: 'search terms', required: true }],
|
||||
},
|
||||
{ name: 'release', description: 'Latest ECC release' },
|
||||
];
|
||||
|
||||
const res = await fetch(`https://discord.com/api/v10/applications/${APP_ID}/guilds/${GUILD}/commands`, {
|
||||
method: 'PUT',
|
||||
headers: { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(COMMANDS),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error('registration failed:', res.status, (await res.text()).slice(0, 300));
|
||||
process.exit(1);
|
||||
}
|
||||
const registered = await res.json();
|
||||
console.log('registered:', registered.map(c => `/${c.name}`).join(' '));
|
||||
Reference in New Issue
Block a user