dual-brain 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.
Files changed (68) hide show
  1. package/AGENTS.md +97 -0
  2. package/CLAUDE.md +147 -0
  3. package/LICENSE +21 -0
  4. package/README.md +197 -0
  5. package/agents/implementer.md +22 -0
  6. package/agents/researcher.md +25 -0
  7. package/agents/verifier.md +30 -0
  8. package/bin/dual-brain.mjs +2868 -0
  9. package/hooks/auto-update-wrapper.mjs +102 -0
  10. package/hooks/auto-update.sh +67 -0
  11. package/hooks/budget-balancer.mjs +679 -0
  12. package/hooks/control-panel.mjs +1195 -0
  13. package/hooks/cost-logger.mjs +286 -0
  14. package/hooks/cost-report.mjs +351 -0
  15. package/hooks/decision-ledger.mjs +299 -0
  16. package/hooks/dual-brain-review.mjs +404 -0
  17. package/hooks/dual-brain-think.mjs +393 -0
  18. package/hooks/enforce-tier.mjs +469 -0
  19. package/hooks/failure-detector.mjs +138 -0
  20. package/hooks/gpt-work-dispatcher.mjs +512 -0
  21. package/hooks/head-guard.mjs +105 -0
  22. package/hooks/health-check.mjs +444 -0
  23. package/hooks/install-git-hooks.mjs +106 -0
  24. package/hooks/model-registry.mjs +859 -0
  25. package/hooks/plan-generator.mjs +544 -0
  26. package/hooks/profiles.mjs +254 -0
  27. package/hooks/quality-gate.mjs +355 -0
  28. package/hooks/risk-classifier.mjs +41 -0
  29. package/hooks/session-report.mjs +514 -0
  30. package/hooks/setup-wizard.mjs +130 -0
  31. package/hooks/summary-checkpoint.mjs +432 -0
  32. package/hooks/task-classifier.mjs +328 -0
  33. package/hooks/test-orchestrator.mjs +1077 -0
  34. package/hooks/vibe-memory.mjs +463 -0
  35. package/hooks/vibe-router.mjs +387 -0
  36. package/hooks/wave-orchestrator.mjs +1397 -0
  37. package/install.mjs +1541 -0
  38. package/mcp-server/README.md +81 -0
  39. package/mcp-server/index.mjs +388 -0
  40. package/orchestrator.json +215 -0
  41. package/package.json +108 -0
  42. package/playbooks/debug.json +49 -0
  43. package/playbooks/refactor.json +57 -0
  44. package/playbooks/security-audit.json +57 -0
  45. package/playbooks/security.json +38 -0
  46. package/playbooks/test-gen.json +48 -0
  47. package/plugin.json +22 -0
  48. package/review-rules.md +17 -0
  49. package/shell-hook.sh +26 -0
  50. package/skills/go.md +22 -0
  51. package/skills/review.md +19 -0
  52. package/skills/status.md +13 -0
  53. package/skills/think.md +22 -0
  54. package/src/brief.mjs +266 -0
  55. package/src/decide.mjs +635 -0
  56. package/src/decompose.mjs +331 -0
  57. package/src/detect.mjs +345 -0
  58. package/src/dispatch.mjs +942 -0
  59. package/src/health.mjs +253 -0
  60. package/src/index.mjs +44 -0
  61. package/src/install-hooks.mjs +100 -0
  62. package/src/playbook.mjs +257 -0
  63. package/src/profile.mjs +990 -0
  64. package/src/redact.mjs +192 -0
  65. package/src/repo.mjs +292 -0
  66. package/src/session.mjs +1036 -0
  67. package/src/tui.mjs +197 -0
  68. package/src/update-check.mjs +35 -0
package/src/tui.mjs ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * tui.mjs — Zero-dependency terminal UI renderer for the dual-brain CLI.
3
+ * All functions return strings; callers use console.log to print.
4
+ */
5
+
6
+ import { fileURLToPath } from 'node:url';
7
+ import { readFileSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+
10
+ // ─── Unicode / ASCII mode ─────────────────────────────────────────────────────
11
+
12
+ export const useUnicode =
13
+ process.env.DUALBRAIN_ASCII !== '1' && process.stdout.isTTY !== false;
14
+
15
+ const CH = useUnicode
16
+ ? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║', ts: '╠', te: '╣', fill: '█', empty: '░' }
17
+ : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', ts: '+', te: '+', fill: '#', empty: '.' };
18
+
19
+ // ─── ANSI / emoji helpers ─────────────────────────────────────────────────────
20
+
21
+ /** Strip ANSI escape codes from a string. */
22
+ export function stripAnsi(str) {
23
+ // eslint-disable-next-line no-control-regex
24
+ return String(str).replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
25
+ }
26
+
27
+ /**
28
+ * Visible display length of a string.
29
+ * Strips ANSI codes and counts each emoji as 2 columns wide.
30
+ */
31
+ export function visibleLength(str) {
32
+ const plain = stripAnsi(String(str));
33
+ let len = 0;
34
+ for (const ch of plain) {
35
+ const cp = ch.codePointAt(0);
36
+ // Emoji / wide symbol ranges (covers most common emoji)
37
+ if (
38
+ (cp >= 0x1f300 && cp <= 0x1faff) || // Misc symbols, emoji
39
+ (cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols
40
+ (cp >= 0xfe00 && cp <= 0xfe0f) || // Variation selectors
41
+ (cp >= 0x1f1e0 && cp <= 0x1f1ff) || // Flags
42
+ cp === 0x20e3 // Combining enclosing keycap
43
+ ) {
44
+ len += 2;
45
+ } else {
46
+ len += 1;
47
+ }
48
+ }
49
+ return len;
50
+ }
51
+
52
+ /**
53
+ * Right-pad `str` with spaces so that its visible width equals `width`.
54
+ * Accounts for emoji (2-wide) and ANSI codes.
55
+ */
56
+ export function pad(str, width) {
57
+ const vl = visibleLength(str);
58
+ const spaces = Math.max(0, width - vl);
59
+ return String(str) + ' '.repeat(spaces);
60
+ }
61
+
62
+ // ─── box ─────────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Renders a Unicode (or ASCII) box with a title bar.
66
+ * @param {string} title
67
+ * @param {string[]} lines
68
+ * @param {{ width?: number }} opts
69
+ * @returns {string}
70
+ */
71
+ export function box(title, lines = [], opts = {}) {
72
+ const inner = opts.width ?? 56;
73
+ const total = inner + 2; // 2 spaces padding on each side counted inside border
74
+
75
+ const top = CH.tl + CH.h.repeat(total) + CH.tr;
76
+ const divider = CH.ts + CH.h.repeat(total) + CH.te;
77
+ const bottom = CH.bl + CH.h.repeat(total) + CH.br;
78
+
79
+ // Title row: 2-space left pad
80
+ const titleContent = ' ' + title;
81
+ const titleRow = CH.v + pad(titleContent, total) + CH.v;
82
+
83
+ const bodyRows = lines.map(line => {
84
+ const content = ' ' + line;
85
+ return CH.v + pad(content, total) + CH.v;
86
+ });
87
+
88
+ return [top, titleRow, divider, ...bodyRows, bottom].join('\n');
89
+ }
90
+
91
+ // ─── bar ─────────────────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Renders a percentage bar.
95
+ * @param {number} percent 0–100
96
+ * @param {number} width bar width in chars (default 20)
97
+ * @param {{ label?: string }} opts
98
+ * @returns {string}
99
+ */
100
+ export function bar(percent, width = 20, opts = {}) {
101
+ const pct = Math.max(0, Math.min(100, percent));
102
+ const filled = Math.round((pct / 100) * width);
103
+ const empty = width - filled;
104
+
105
+ const track = CH.fill.repeat(filled) + CH.empty.repeat(empty);
106
+ const pctStr = String(Math.round(pct)).padStart(3) + '%';
107
+ const label = opts.label ? ` ${opts.label}` : '';
108
+
109
+ return `${track} ${pctStr}${label}`;
110
+ }
111
+
112
+ // ─── badge ────────────────────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Returns a status badge emoji/symbol.
116
+ * @param {string} status
117
+ * @returns {string}
118
+ */
119
+ export function badge(status) {
120
+ const map = {
121
+ healthy: '🟢',
122
+ degraded: '🟡',
123
+ hot: '🔴',
124
+ probing: '🟠',
125
+ connected: '✅',
126
+ missing: '❌',
127
+ warning: '⚠️',
128
+ };
129
+ return map[status] ?? '❓';
130
+ }
131
+
132
+ // ─── separator ───────────────────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Returns a section separator line.
136
+ * @param {string} label
137
+ * @returns {string}
138
+ */
139
+ export function separator(label = '') {
140
+ const dash = useUnicode ? '─' : '-';
141
+ return label
142
+ ? ` ${dash}${dash}${dash} ${label}`
143
+ : ` ${dash}${dash}${dash}`;
144
+ }
145
+
146
+ // ─── menu ────────────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Renders a numbered/lettered menu grouped by section.
150
+ * @param {{ key: string, label: string, section?: string }[]} options
151
+ * @param {object} opts (reserved)
152
+ * @returns {string}
153
+ */
154
+ export function menu(options, opts = {}) {
155
+ const rows = [];
156
+ let lastSection = Symbol('none');
157
+
158
+ for (const opt of options) {
159
+ const section = opt.section ?? '';
160
+ if (section !== lastSection) {
161
+ if (section) {
162
+ rows.push(separator(section));
163
+ } else {
164
+ rows.push(separator());
165
+ }
166
+ lastSection = section;
167
+ }
168
+ rows.push(` [${opt.key}] ${opt.label}`);
169
+ }
170
+
171
+ return rows.join('\n');
172
+ }
173
+
174
+ // ─── Self-test ────────────────────────────────────────────────────────────────
175
+
176
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
177
+ // Read version dynamically from package.json
178
+ let selfTestVersion = '0.0.0';
179
+ try {
180
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
181
+ selfTestVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
182
+ } catch { /* fallback to 0.0.0 */ }
183
+
184
+ console.log(box(`🧠 Dual-Brain v${selfTestVersion}`, [
185
+ '🟢 Claude ✅ 🟢 OpenAI ✅',
186
+ '🌀 Replit + replit-tools',
187
+ ]));
188
+ console.log(bar(75, 20, { label: 'Claude' }));
189
+ console.log(bar(25, 20, { label: 'OpenAI' }));
190
+ console.log(menu([
191
+ { key: 'c', label: 'Continue last session', section: 'Sessions' },
192
+ { key: 'n', label: 'New session', section: 'Sessions' },
193
+ { key: 'a', label: 'Auth management', section: 'Settings' },
194
+ { key: 'p', label: 'Profile settings', section: 'Settings' },
195
+ { key: 's', label: 'Exit to shell', section: '' },
196
+ ]));
197
+ }
@@ -0,0 +1,35 @@
1
+ import { execSync } from 'child_process';
2
+ import { readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export function getLocalVersion() {
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
11
+ return pkg.version;
12
+ } catch { return null; }
13
+ }
14
+
15
+ export function getLatestVersion() {
16
+ try {
17
+ const result = execSync('npm view dual-brain version 2>/dev/null', { encoding: 'utf8', timeout: 5000 });
18
+ return result.trim();
19
+ } catch { return null; }
20
+ }
21
+
22
+ export function checkForUpdate() {
23
+ const local = getLocalVersion();
24
+ const latest = getLatestVersion();
25
+ if (!local || !latest) return { updateAvailable: false, local, latest };
26
+
27
+ const localParts = local.split('.').map(Number);
28
+ const latestParts = latest.split('.').map(Number);
29
+
30
+ const updateAvailable = latestParts[0] > localParts[0]
31
+ || (latestParts[0] === localParts[0] && latestParts[1] > localParts[1])
32
+ || (latestParts[0] === localParts[0] && latestParts[1] === localParts[1] && latestParts[2] > localParts[2]);
33
+
34
+ return { updateAvailable, local, latest };
35
+ }