@prplkane/roundtable 0.0.0-stage → 0.6.4

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +529 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -3
  4. package/SECURITY.md +299 -0
  5. package/docs/workflow.md +1344 -0
  6. package/package.json +52 -6
  7. package/scripts/agent.mjs +40 -0
  8. package/scripts/export.mjs +20 -0
  9. package/scripts/mcp.mjs +175 -0
  10. package/scripts/run-debate.mjs +70 -0
  11. package/scripts/server.mjs +28 -0
  12. package/scripts/setup.mjs +182 -0
  13. package/skills/roundtable/SKILL.md +57 -0
  14. package/src/adapters/agy.js +7 -0
  15. package/src/adapters/catalogue.js +49 -0
  16. package/src/adapters/claude.js +6 -0
  17. package/src/adapters/codex.js +6 -0
  18. package/src/adapters/common.js +304 -0
  19. package/src/adapters/config.js +432 -0
  20. package/src/adapters/endpoint.js +178 -0
  21. package/src/adapters/errors.js +69 -0
  22. package/src/adapters/index.js +11 -0
  23. package/src/adapters/models.js +106 -0
  24. package/src/adapters/quota.js +305 -0
  25. package/src/adapters/spawn.js +178 -0
  26. package/src/agent-client.js +108 -0
  27. package/src/cli.js +44 -0
  28. package/src/contract.js +256 -0
  29. package/src/diagnostics.js +133 -0
  30. package/src/doctor.js +101 -0
  31. package/src/engine.js +902 -0
  32. package/src/estimate.js +331 -0
  33. package/src/export.js +307 -0
  34. package/src/fake.js +75 -0
  35. package/src/folder-picker.js +46 -0
  36. package/src/history.js +220 -0
  37. package/src/local-http.js +39 -0
  38. package/src/local-server.js +110 -0
  39. package/src/notes.js +16 -0
  40. package/src/personas.js +49 -0
  41. package/src/preflight.js +126 -0
  42. package/src/progress.js +30 -0
  43. package/src/prompts.js +278 -0
  44. package/src/research.js +113 -0
  45. package/src/review-view.js +157 -0
  46. package/src/seats.js +272 -0
  47. package/src/server.js +928 -0
  48. package/src/settings.js +27 -0
  49. package/src/setup-toml.js +124 -0
  50. package/src/snapshot.js +93 -0
  51. package/src/store.js +303 -0
  52. package/src/terminal.js +77 -0
  53. package/src/terms.js +52 -0
  54. package/src/ui/app.js +1967 -0
  55. package/src/ui/avatars.js +132 -0
  56. package/src/ui/ceremony.js +87 -0
  57. package/src/ui/design/app.css +714 -0
  58. package/src/ui/design/preset-dusk.css +91 -0
  59. package/src/ui/design/tokens.css +129 -0
  60. package/src/ui/diagnostics.js +61 -0
  61. package/src/ui/help.js +122 -0
  62. package/src/ui/icons.js +64 -0
  63. package/src/ui/index.html +272 -0
  64. package/src/ui/loader.js +73 -0
  65. package/src/ui/logo.svg +4 -0
  66. package/src/ui/scan.js +57 -0
  67. package/src/ui/scribe.js +64 -0
  68. package/src/ui/sound.js +80 -0
  69. package/src/validate.js +112 -0
  70. package/src/version.js +5 -0
@@ -0,0 +1,133 @@
1
+ // Local support data uses an allowlist, never error messages, request bodies or CLI output.
2
+ import { mkdirSync, appendFileSync, readFileSync, renameSync, lstatSync, existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { safeProgress } from './progress.js';
6
+
7
+ const CODES = new Set(['request-failed', 'provider-failed', 'call-cancelled', 'reply-invalid', 'engine-failed', 'export-failed',
8
+ 'transcript-warning', 'status-failed', 'storage-failed', 'server-crash', 'server-start',
9
+ 'browser-network', 'browser-timeout', 'browser-response', 'browser-error', 'browser-rejection', 'browser-stream', 'browser-validation']);
10
+ export const BROWSER_CODES = new Set([...CODES].filter((c) => c.startsWith('browser-')));
11
+ const ACTIONS = new Set(['setup', 'pick-folder', 'history', 'delete', 'clear', 'clear-finished', 'preflight', 'preview', 'create', 'events', 'state', 'continue', 'finish', 'stop', 'hold',
12
+ 'resume', 'export', 'synthesize', 'accept', 'pools', 'terms', 'diagnostics', 'catalogue', 'quota', 'calibration', 'startup', 'shutdown', 'health', 'unknown']);
13
+ const KINDS = new Set(['quota', 'throttled', 'auth', 'missing', 'network', 'timeout', 'killed', 'format', 'budget', 'policy', 'compatibility', 'other']);
14
+ const SYSTEM_CODES = new Set(['STALL', 'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'EIO', 'EISDIR', 'EROFS', 'EMFILE', 'ENFILE',
15
+ 'TOOL_POLICY', 'OUTPUT_LIMIT', 'PROVIDER_PROTOCOL', 'CLI_EXIT', 'CLI_FAILURE', 'ISOLATION_REQUIRED', 'TERMS_ACK_REQUIRED', 'RESEARCH_LIMIT', 'RESEARCH_INVALID',
16
+ 'EADDRINUSE', 'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'TIMEOUT', 'KILLED', 'TRANSCRIPT_CORRUPT', 'TRANSCRIPT_TAIL',
17
+ 'SEAT_LENS_REQUIRED', 'SEAT_LENS_DUPLICATE', 'SEAT_MOOD_REQUIRED']);
18
+ const FILES = new Set(['server.js', 'store.js', 'history.js', 'engine.js', 'export.js', 'validate.js', 'estimate.js',
19
+ 'seats.js', 'preflight.js', 'diagnostics.js', 'snapshot.js', 'review-view.js', 'ui/app.js', 'ui/help.js', 'adapters/common.js', 'adapters/spawn.js',
20
+ 'adapters/endpoint.js', 'adapters/claude.js', 'adapters/codex.js', 'adapters/agy.js']);
21
+ const ID = /^RT-[a-f0-9]{16}$/;
22
+ const integer = (n, max) => Number.isInteger(n) && n >= 0 && n <= max;
23
+ export const diagnosticId = () => `RT-${randomBytes(8).toString('hex')}`;
24
+
25
+ export function diagnosticAction(pathname) {
26
+ if (pathname === '/api/seats') return 'setup';
27
+ if (pathname === '/api/health') return 'health';
28
+ if (pathname === '/api/shutdown') return 'shutdown';
29
+ if (pathname === '/api/debates') return 'history';
30
+ if (pathname === '/api/preflight') return 'preflight';
31
+ if (pathname === '/api/preview') return 'preview';
32
+ if (/^\/api\/pools\/[a-z]+$/.test(pathname)) return 'pools';
33
+ if (/^\/api\/terms\/[a-z]+$/.test(pathname)) return 'terms';
34
+ if (pathname.startsWith('/api/diagnostics')) return 'diagnostics';
35
+ const match = /^\/api\/debates\/[^/]+\/([a-z]+)$/.exec(pathname);
36
+ return match && ACTIONS.has(match[1]) ? match[1] : 'unknown';
37
+ }
38
+
39
+ // Project only fixed categories and bounded numbers. Even an endpoint echoing a secret cannot
40
+ // put it in this log. Source locations omit function names and absolute filesystem paths.
41
+ function fields(input) {
42
+ const out = { code: CODES.has(input?.code) ? input.code : 'request-failed',
43
+ action: ACTIONS.has(input?.action) ? input.action : 'unknown' };
44
+ if (['claude', 'codex', 'agy', 'api'].includes(input?.vendor)) out.vendor = input.vendor;
45
+ if (KINDS.has(input?.kind)) out.kind = input.kind;
46
+ const progress = safeProgress(input?.progress);
47
+ if (Object.keys(progress).length) out.progress = progress;
48
+ if (SYSTEM_CODES.has(input?.systemCode)) out.systemCode = input.systemCode;
49
+ if (['lens', 'moodText'].includes(input?.field)) out.field = input.field;
50
+ if (['human', 'wall-clock'].includes(input?.cancelReason)) out.cancelReason = input.cancelReason;
51
+ for (const [key, max] of Object.entries({ seatIndex: 7, status: 599, round: 100, line: 1_000_000, column: 1_000_000 })) {
52
+ if (integer(input?.[key], max)) out[key] = input[key];
53
+ }
54
+ if (Array.isArray(input?.frames)) out.frames = input.frames.filter((f) => FILES.has(f?.file) && integer(f.line, 1_000_000) && integer(f.column, 1_000_000))
55
+ .slice(0, 5).map(({ file, line, column }) => ({ file, line, column }));
56
+ return out;
57
+ }
58
+
59
+ export function diagnosticError(error) {
60
+ const frames = [];
61
+ for (const m of String(error?.stack || '').matchAll(/[\\/]src[\\/]([\w./\\-]+):(\d+):(\d+)/g)) {
62
+ const file = m[1].replaceAll('\\', '/');
63
+ if (FILES.has(file)) frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
64
+ }
65
+ return { systemCode: error?.code, frames: frames.slice(0, 5) };
66
+ }
67
+
68
+ export function validateBrowserDiagnostics(body) {
69
+ if (!Array.isArray(body?.events) || body.events.length > 20) throw new Error('Send at most 20 diagnostic events.');
70
+ return body.events.map((e) => {
71
+ if (!e || !ID.test(e.id) || !BROWSER_CODES.has(e.code) || !ACTIONS.has(e.action)) throw new Error('Invalid diagnostic event.');
72
+ // Browser callers cannot impersonate a server/provider event or inject stack frames.
73
+ return { id: e.id, ...fields({ code: e.code, action: e.action, status: e.status, line: e.line, column: e.column, seatIndex: e.seatIndex, field: e.field }) };
74
+ });
75
+ }
76
+
77
+ export function createDiagnostics(root, { mode = 'live', version = 'unknown', maxBytes = 256 * 1024,
78
+ maxPerMinute = 120, now = Date.now, warn = (s) => console.warn(s) } = {}) {
79
+ const dir = path.join(root, '_diagnostics');
80
+ const current = path.join(dir, 'events.jsonl'), previous = path.join(dir, 'previous.jsonl');
81
+ const environment = { mode: mode === 'demo' ? 'demo' : 'live',
82
+ version: /^\d+\.\d+\.\d+$/.test(version) ? version : 'unknown', platform: process.platform, nodeMajor: Number(process.versions.node.split('.')[0]) };
83
+ let available = true, dropped = 0, windowAt = now(), count = 0;
84
+ const safeFile = (file) => {
85
+ let stat;
86
+ try { stat = lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return 0; throw e; }
87
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Invalid diagnostic file.');
88
+ return stat.size;
89
+ };
90
+ function fail() {
91
+ if (available) warn('[diagnostics] Could not save the local error log. Check free disk space and data-folder permissions.');
92
+ available = false;
93
+ }
94
+ function write(input, error) {
95
+ const id = ID.test(input?.id) ? input.id : diagnosticId();
96
+ if (now() - windowAt >= 60_000) { windowAt = now(); count = 0; }
97
+ if (++count > maxPerMinute) { dropped++; return { id, saved: false }; }
98
+ const entry = { id, at: new Date(now()).toISOString(), ...environment, ...fields({ ...input, ...diagnosticError(error) }) };
99
+ try {
100
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
101
+ if (lstatSync(dir).isSymbolicLink()) throw new Error('Invalid diagnostic directory.');
102
+ const line = JSON.stringify(entry) + '\n';
103
+ const size = safeFile(current);
104
+ if (size > maxBytes || Buffer.byteLength(line) > maxBytes) throw new Error('Oversized diagnostic file.');
105
+ if (size + Buffer.byteLength(line) > maxBytes && existsSync(current)) { safeFile(previous); renameSync(current, previous); }
106
+ appendFileSync(current, line, { mode: 0o600 });
107
+ available = true;
108
+ return { id, saved: true };
109
+ } catch { fail(); return { id, saved: false }; }
110
+ }
111
+ function read() {
112
+ const entries = [];
113
+ try {
114
+ if (existsSync(dir) && lstatSync(dir).isSymbolicLink()) throw new Error('Invalid diagnostic directory.');
115
+ for (const file of [previous, current]) {
116
+ if (!existsSync(file)) continue;
117
+ if (safeFile(file) > maxBytes) throw new Error('Oversized diagnostic file.');
118
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
119
+ try {
120
+ const e = JSON.parse(line);
121
+ if (!ID.test(e.id) || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(e.at) || !CODES.has(e.code)) continue;
122
+ entries.push({ id: e.id, at: e.at, ...fields(e), mode: e.mode === 'demo' ? 'demo' : 'live',
123
+ version: /^\d+\.\d+\.\d+$/.test(e.version) ? e.version : 'unknown',
124
+ platform: ['win32', 'darwin', 'linux'].includes(e.platform) ? e.platform : 'other',
125
+ ...(integer(e.nodeMajor, 1000) ? { nodeMajor: e.nodeMajor } : {}) });
126
+ } catch { /* an interrupted diagnostic line is not a debate record */ }
127
+ }
128
+ }
129
+ } catch { fail(); }
130
+ return { format: 'roundtable-diagnostics-v1', generatedAt: new Date(now()).toISOString(), ...environment, available, dropped, entries };
131
+ }
132
+ return { write, read, status: () => ({ available, dropped }) };
133
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,101 @@
1
+ // node src/doctor.js [--live] [--profile <name>] [--vendor <name>] [--allow-limited-isolation] [--acknowledge-terms-risk]
2
+ //
3
+ // What this machine can seat. Per vendor: the binary and where it was found, its version, the
4
+ // models and effort levels the setup screen will offer, the live pool read where one exists
5
+ // (codex, agy), and, with --live, one trivial model call through the adapter: logged in or not,
6
+ // what it answered, whether it reached for tools (the guard must see false on a plain reply),
7
+ // tokens and the quota it reported (claude). --profile <name> prints how to log that profile
8
+ // in for each vendor. A vendor with a terms notice (config.js terms: Antigravity) is sent nothing,
9
+ // not its model list or pool read either, without --acknowledge-terms-risk; only its CLI's
10
+ // --version still runs.
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { ADAPTERS } from './adapters/config.js';
14
+ import { resolveBinary, runProcess, profileHome, dataDir } from './adapters/spawn.js';
15
+ import { dataDirectory } from './history.js';
16
+ import { paintVendor } from './terminal.js';
17
+
18
+ // Shell lines that log a profile in, per vendor, for the platform the user is on. Pure.
19
+ export function loginInstructions(vendor, profile, where = {}) {
20
+ const a = ADAPTERS[vendor];
21
+ if (!a?.home) return [`${a?.label || vendor}: the CLI documents no state-directory variable, so a profile cannot be separated from the terminal's login.`];
22
+ const dir = profileHome(vendor, profile, where);
23
+ const login = { claude: 'claude /login', codex: 'codex login' }[vendor] || `${a.binary} login`;
24
+ const platform = where.platform || process.platform;
25
+ const set = platform === 'win32' ? [`set ${a.home.env}=${dir}`, login] : [`${a.home.env}="${dir}" ${login}`];
26
+ return [`${a.label} profile "${profile}" lives in ${dir}`, ...set.map((l) => ` ${l}`), ' (PowerShell: $env:' + a.home.env + ' = "' + dir + '"; ' + login + ')'];
27
+ }
28
+
29
+ async function version(bin, vendor) {
30
+ try {
31
+ const r = await runProcess(bin, ['--version'], { timeoutMs: 20_000 });
32
+ const line = (r.stdout + r.stderr).trim().split('\n').find((l) => /\d+\.\d+/.test(l)) || (r.stdout + r.stderr).trim().split('\n')[0];
33
+ return line ? line.trim().slice(0, 80) : `exit ${r.code}`;
34
+ } catch (e) { return `(${e.message})`; }
35
+ }
36
+
37
+ const flag = (name) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? (process.argv[i + 1] || true) : null; };
38
+
39
+ async function main() {
40
+ const live = process.argv.includes('--live');
41
+ const allowLimitedIsolation = process.argv.includes('--allow-limited-isolation');
42
+ const acknowledgeTermsRisk = process.argv.includes('--acknowledge-terms-risk');
43
+ const profile = flag('profile');
44
+ const only = flag('vendor');
45
+ const { vendorCatalogue } = await import('./adapters/catalogue.js');
46
+ const { adapters, quotaReaders } = await import('./adapters/index.js');
47
+ const catalogue = await vendorCatalogue(ADAPTERS.agy.terms && !acknowledgeTermsRisk ? { agyModels: async () => [] } : {});
48
+ console.log(`roundtable doctor (${process.platform}, node ${process.version}, data dir ${dataDir()})`);
49
+ console.log(live ? 'live: one trivial model call per vendor' : 'static only; add --live for one trivial model call per vendor');
50
+ const missing = []; // CLIs not on PATH: one line at the end, a full block only when asked for by --vendor
51
+ for (const [vendor, a] of Object.entries(ADAPTERS)) {
52
+ if (only && only !== vendor) continue;
53
+ let bin = null, lookup = null;
54
+ if (a.kind === 'cli') try { bin = resolveBinary(a.binary, a.npmBin); } catch (e) { lookup = e.message; }
55
+ if (a.kind === 'cli' && !bin && only !== vendor) { missing.push(`${a.label} (${a.binary})`); continue; }
56
+ console.log(`\n== ${paintVendor(a)} (${vendor}) pool: ${a.pool}`);
57
+ if (a.kind === 'api') { // no binary: everything about an endpoint seat is on its card
58
+ console.log(' seat it on the board: base URL (…/v1), model id, and for a hosted API the NAME of the key variable');
59
+ console.log(` efforts ${a.effortLevels.join(', ')} (reasoning_effort, sent only when set)`);
60
+ console.log(` read-only ${a.readOnly}`);
61
+ console.log(` isolation ${a.isolation}`);
62
+ continue;
63
+ }
64
+ const held = !!a.terms && !acknowledgeTermsRisk;
65
+ const heldLine = `held: nothing is sent to ${a.binary} without --acknowledge-terms-risk`;
66
+ console.log(` binary ${bin ? bin.prefix.length ? `${bin.prefix[0]} (via node)` : bin.file : `NOT FOUND: ${lookup}`}`);
67
+ if (bin) console.log(` version ${await version(bin, vendor)}`);
68
+ const c = catalogue[vendor];
69
+ if (held) console.log(` models ${heldLine}`);
70
+ else console.log(` models ${c.models.length ? c.models.map((m) => m.id).join(', ') : '(free text; the CLI lists none)'}${c.defaultModel ? ` default: ${c.defaultModel}` : ''}`);
71
+ console.log(` efforts ${c.efforts.join(', ')}${c.defaultEffort ? ` default: ${c.defaultEffort}` : ''}${c.effortInModel ? ' (a model id may carry the level)' : ''}`);
72
+ console.log(` read-only ${a.readOnly}`);
73
+ console.log(` isolation ${a.isolation}`);
74
+ if (a.terms) console.log(` terms ${a.terms.notice} (${a.terms.url}, checked ${a.terms.read})`);
75
+ if (held && quotaReaders[vendor]) console.log(` pool read ${heldLine}`);
76
+ else if (quotaReaders[vendor]) {
77
+ const q = bin ? await quotaReaders[vendor]() : null;
78
+ console.log(` pool read ${q ? q.windows.map((w) => `${w.label} ${(w.used * 100).toFixed(0)}% used`).join(', ') + (q.plan ? ` (plan ${q.plan})` : '') : 'no figure (not logged in, or the read failed)'}`);
79
+ } else console.log(` pool read ${a.quota === 'per-call' ? 'rides on each reply (shown with --live)' : `none headless; usage page ${a.usageUrl}`}`);
80
+ if (live && bin && held) console.log(` live call skipped: ${heldLine}, and then only if your plan allows it`);
81
+ else if (live && bin && a.limitedIsolation && !allowLimitedIsolation) console.log(' live call skipped: Antigravity has limited isolation. Add --allow-limited-isolation to consent, or use another provider.');
82
+ if (live && bin && !held && (!a.limitedIsolation || allowLimitedIsolation)) {
83
+ const t0 = Date.now();
84
+ try {
85
+ const r = await adapters[vendor]('Reply with exactly the word pong.', null, { timeoutMs: 180_000, cwd: path.join(dataDirectory(), '_doctor', vendor), allowLimitedIsolation, acknowledgeTermsRisk });
86
+ console.log(` logged in yes (${Date.now() - t0} ms) reply: ${JSON.stringify(r.text.slice(0, 40))} session: ${r.sessionId}`);
87
+ console.log(` usedTools ${r.usedTools}`);
88
+ const cost = r.usage?.cost ?? r.cost;
89
+ console.log(` usage ${r.usage ? `in ${r.usage.input} cache ${r.usage.cacheRead + r.usage.cacheWrite} out ${r.usage.output} think ${r.usage.thinking}` : 'tokens unknown'}${cost != null ? ` $${cost.toFixed(2)}` : ''}`);
90
+ if (r.quota) console.log(` windows ${r.quota.windows.map((w) => `${w.label} ${(w.used * 100).toFixed(0)}% used`).join(', ')}`);
91
+ } catch (e) { console.log(` live call failed (${e.kind || 'other'}, ${Date.now() - t0} ms): ${e.message.slice(0, 200)}`); }
92
+ }
93
+ if (profile) for (const l of loginInstructions(vendor, profile)) console.log(` profile ${l}`);
94
+ }
95
+ if (missing.length) console.log(`\nnot installed: ${missing.join(', ')}; install and sign in to seat them, or add --vendor <name> for that block`);
96
+ if (!profile) console.log('\nadd --profile <name> to see how to log a named account profile in for each vendor');
97
+ }
98
+
99
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
100
+ main().catch((e) => { console.error(e); process.exit(1); });
101
+ }