dual-brain 0.2.30 → 0.3.1
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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/precompact.mjs +3 -3
- package/hooks/session-end.mjs +3 -3
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/install.mjs +2 -2
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/think-engine.mjs
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
// think-engine.mjs — Adaptive thinking ladder: recall → triage → tier decision.
|
|
2
|
-
// Replaces fixed "always dual-brain" with knowledge preflight + heuristic classification.
|
|
3
|
-
// Zero network calls. All matching is keyword-based.
|
|
4
|
-
|
|
5
|
-
import { readFileSync, appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
const DOCS_DIR = '.dual-brain';
|
|
9
|
-
const DECISIONS_FILE = 'decisions.jsonl';
|
|
10
|
-
|
|
11
|
-
const STOP_WORDS = new Set([
|
|
12
|
-
'a','an','the','and','or','but','in','on','at','to','for','of','with',
|
|
13
|
-
'by','from','is','it','its','be','as','are','was','were','been','has',
|
|
14
|
-
'have','had','do','does','did','will','would','could','should','may',
|
|
15
|
-
'might','shall','can','this','that','these','those','i','we','you',
|
|
16
|
-
'he','she','they','my','our','your','his','her','their','what','how',
|
|
17
|
-
'when','where','why','which','who','all','any','more','most','also',
|
|
18
|
-
'not','no','so','if','then','than','into','up','out','about','just',
|
|
19
|
-
'after','before','between','through','during','each','get','use',
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
const HARD_ESCALATION_KEYWORDS = [
|
|
23
|
-
'auth','credential','secret','token','security','migration','billing',
|
|
24
|
-
'payment','deploy production','delete','drop','force push','routing logic',
|
|
25
|
-
'dispatcher','pipeline gate',
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const TIER_TOKENS = {
|
|
29
|
-
recall: 0,
|
|
30
|
-
quick: 2000,
|
|
31
|
-
standard: 8000,
|
|
32
|
-
deep: 20000,
|
|
33
|
-
ultra: 50000,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const TIER_COST = {
|
|
37
|
-
recall: 'zero',
|
|
38
|
-
quick: 'minimal',
|
|
39
|
-
standard: 'moderate',
|
|
40
|
-
deep: 'significant',
|
|
41
|
-
ultra: 'heavy',
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export function normalizeIntent(text) {
|
|
45
|
-
if (!text || typeof text !== 'string') return [];
|
|
46
|
-
return text
|
|
47
|
-
.toLowerCase()
|
|
48
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
49
|
-
.split(/\s+/)
|
|
50
|
-
.filter(w => w.length > 2 && !STOP_WORDS.has(w));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function decisionsPath(cwd) {
|
|
54
|
-
return join(cwd, DOCS_DIR, DECISIONS_FILE);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readDecisions(cwd) {
|
|
58
|
-
const path = decisionsPath(cwd);
|
|
59
|
-
if (!existsSync(path)) return [];
|
|
60
|
-
try {
|
|
61
|
-
const raw = readFileSync(path, 'utf8');
|
|
62
|
-
return raw
|
|
63
|
-
.split('\n')
|
|
64
|
-
.filter(l => l.trim())
|
|
65
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
66
|
-
.filter(Boolean);
|
|
67
|
-
} catch {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getFreshness(timestamp) {
|
|
73
|
-
if (!timestamp) return 'stale';
|
|
74
|
-
const ageMs = Date.now() - new Date(timestamp).getTime();
|
|
75
|
-
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
76
|
-
if (ageDays < 7) return 'current';
|
|
77
|
-
if (ageDays < 30) return 'aging';
|
|
78
|
-
return 'stale';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function keywordOverlap(kwA, kwB) {
|
|
82
|
-
if (!kwA.length || !kwB.length) return 0;
|
|
83
|
-
const setA = new Set(kwA);
|
|
84
|
-
const matches = kwB.filter(w => setA.has(w)).length;
|
|
85
|
-
return matches / Math.max(kwA.length, kwB.length);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getApplicability(relevance, freshness) {
|
|
89
|
-
if (relevance > 0.8 && freshness === 'current') return 'exact_reuse';
|
|
90
|
-
if (relevance > 0.8 && freshness === 'aging') return 'reuse_with_validation';
|
|
91
|
-
if (relevance > 0.8 && freshness === 'stale') return 'stale';
|
|
92
|
-
if (relevance >= 0.4) return 'related_precedent';
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function lookupDecision(intent, tags = [], cwd = process.cwd()) {
|
|
97
|
-
const queryKw = normalizeIntent(intent);
|
|
98
|
-
const queryTags = tags.map(t => t.toLowerCase());
|
|
99
|
-
const decisions = readDecisions(cwd);
|
|
100
|
-
|
|
101
|
-
const candidates = [];
|
|
102
|
-
for (const dec of decisions) {
|
|
103
|
-
const decKw = dec.normalizedIntent
|
|
104
|
-
? dec.normalizedIntent.split(' ').filter(Boolean)
|
|
105
|
-
: normalizeIntent(dec.question || dec.decision || '');
|
|
106
|
-
|
|
107
|
-
let relevance = keywordOverlap(queryKw, decKw);
|
|
108
|
-
|
|
109
|
-
const decTags = (dec.tags || []).map(t => t.toLowerCase());
|
|
110
|
-
const tagMatch = queryTags.some(t => decTags.includes(t));
|
|
111
|
-
if (tagMatch) relevance = Math.min(1, relevance + 0.15);
|
|
112
|
-
|
|
113
|
-
if (relevance < 0.4) continue;
|
|
114
|
-
|
|
115
|
-
const freshness = getFreshness(dec.timestamp);
|
|
116
|
-
const applicability = getApplicability(relevance, freshness);
|
|
117
|
-
if (!applicability) continue;
|
|
118
|
-
|
|
119
|
-
candidates.push({ decision: dec, relevance, freshness, applicability });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
candidates.sort((a, b) => b.relevance - a.relevance);
|
|
123
|
-
|
|
124
|
-
const highRelevance = candidates.filter(c => c.relevance > 0.8);
|
|
125
|
-
let recommendation = 'new_thinking_needed';
|
|
126
|
-
|
|
127
|
-
if (highRelevance.length > 1) {
|
|
128
|
-
const decisions_set = highRelevance.map(c =>
|
|
129
|
-
normalizeIntent(typeof c.decision.decision === 'string' ? c.decision.decision : JSON.stringify(c.decision.decision)).join(' ')
|
|
130
|
-
);
|
|
131
|
-
const pairOverlap = keywordOverlap(
|
|
132
|
-
normalizeIntent(decisions_set[0]),
|
|
133
|
-
normalizeIntent(decisions_set[1])
|
|
134
|
-
);
|
|
135
|
-
if (pairOverlap < 0.3) {
|
|
136
|
-
for (const c of highRelevance) c.applicability = 'conflicting';
|
|
137
|
-
recommendation = 'new_thinking_needed';
|
|
138
|
-
} else if (candidates[0]?.applicability === 'exact_reuse') {
|
|
139
|
-
recommendation = 'reuse';
|
|
140
|
-
} else {
|
|
141
|
-
recommendation = 'validate';
|
|
142
|
-
}
|
|
143
|
-
} else if (candidates[0]?.applicability === 'exact_reuse') {
|
|
144
|
-
recommendation = 'reuse';
|
|
145
|
-
} else if (candidates[0]?.applicability === 'reuse_with_validation') {
|
|
146
|
-
recommendation = 'validate';
|
|
147
|
-
} else if (candidates.length > 0) {
|
|
148
|
-
recommendation = 'new_thinking_needed';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
found: candidates.length > 0,
|
|
153
|
-
candidates: candidates.slice(0, 5),
|
|
154
|
-
recommendation,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function detectRisk(question) {
|
|
159
|
-
const q = question.toLowerCase();
|
|
160
|
-
const critical = ['auth','credential','secret','token','security','billing','payment','force push','drop table','delete production'];
|
|
161
|
-
const high = ['migration','deploy production','routing logic','dispatcher','pipeline gate','delete','drop'];
|
|
162
|
-
const low = ['readme','doc','comment','explain','list','show','what is','how does'];
|
|
163
|
-
|
|
164
|
-
if (critical.some(k => q.includes(k))) return 'critical';
|
|
165
|
-
if (high.some(k => q.includes(k))) return 'high';
|
|
166
|
-
if (low.some(k => q.includes(k))) return 'low';
|
|
167
|
-
return 'medium';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function detectComplexity(question) {
|
|
171
|
-
const wordCount = question.trim().split(/\s+/).length;
|
|
172
|
-
const hasMultiStep = /and then|then also|first.*then|step \d|multiple|several|across|all/i.test(question);
|
|
173
|
-
const hasComparison = /vs|versus|compare|difference|between|trade.?off/i.test(question);
|
|
174
|
-
|
|
175
|
-
if (wordCount > 80 || (hasMultiStep && hasComparison)) return 'complex';
|
|
176
|
-
if (wordCount > 30 || hasMultiStep || hasComparison) return 'moderate';
|
|
177
|
-
return 'simple';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function detectNovelty(preflight) {
|
|
181
|
-
if (!preflight || !preflight.found) return 'novel';
|
|
182
|
-
if (preflight.recommendation === 'reuse') return 'known';
|
|
183
|
-
if (preflight.candidates?.some(c => c.applicability === 'related_precedent' || c.applicability === 'reuse_with_validation')) {
|
|
184
|
-
return 'variation';
|
|
185
|
-
}
|
|
186
|
-
return 'novel';
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function hasHardEscalation(question) {
|
|
190
|
-
const q = question.toLowerCase();
|
|
191
|
-
return HARD_ESCALATION_KEYWORDS.some(k => q.includes(k));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function triageQuestion(question, projectBrief, preflight) {
|
|
195
|
-
const risk = detectRisk(question);
|
|
196
|
-
const complexity = detectComplexity(question);
|
|
197
|
-
const novelty = detectNovelty(preflight);
|
|
198
|
-
const hardEscalation = hasHardEscalation(question);
|
|
199
|
-
|
|
200
|
-
let recommendedTier;
|
|
201
|
-
let reason;
|
|
202
|
-
|
|
203
|
-
if (preflight?.recommendation === 'reuse') {
|
|
204
|
-
recommendedTier = 'recall';
|
|
205
|
-
reason = 'exact match found in decision log';
|
|
206
|
-
} else if (hardEscalation || risk === 'critical') {
|
|
207
|
-
recommendedTier = 'ultra';
|
|
208
|
-
reason = hardEscalation
|
|
209
|
-
? `hard escalation keyword detected`
|
|
210
|
-
: 'critical risk requires maximum deliberation';
|
|
211
|
-
} else if (preflight?.candidates?.some(c => c.applicability === 'conflicting')) {
|
|
212
|
-
recommendedTier = 'ultra';
|
|
213
|
-
reason = 'conflicting prior decisions require reconciliation';
|
|
214
|
-
} else if (risk === 'high' && (novelty === 'novel' || complexity === 'complex')) {
|
|
215
|
-
recommendedTier = 'deep';
|
|
216
|
-
reason = `high risk + ${novelty === 'novel' ? 'novel question' : 'complex scope'}`;
|
|
217
|
-
} else if (novelty === 'novel' && (risk === 'medium' || complexity === 'complex')) {
|
|
218
|
-
recommendedTier = 'standard';
|
|
219
|
-
reason = 'novel question with non-trivial risk or complexity';
|
|
220
|
-
} else if (novelty === 'variation' && risk === 'low') {
|
|
221
|
-
recommendedTier = 'quick';
|
|
222
|
-
reason = 'similar precedent found, low risk variation';
|
|
223
|
-
} else if (preflight?.candidates?.length > 0 && novelty !== 'novel') {
|
|
224
|
-
recommendedTier = 'quick';
|
|
225
|
-
reason = 'related precedent available, minor adaptation needed';
|
|
226
|
-
} else if (novelty === 'novel' && risk === 'low' && complexity === 'simple') {
|
|
227
|
-
recommendedTier = 'quick';
|
|
228
|
-
reason = 'novel but simple and low risk';
|
|
229
|
-
} else {
|
|
230
|
-
recommendedTier = 'standard';
|
|
231
|
-
reason = 'default tier for unclassified novel questions';
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const riskRank = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
235
|
-
const tierRank = { recall: 0, quick: 1, standard: 2, deep: 3, ultra: 4 };
|
|
236
|
-
const minTierForRisk = { low: 'recall', medium: 'quick', high: 'deep', critical: 'ultra' };
|
|
237
|
-
const riskFloor = minTierForRisk[risk] ?? 'quick';
|
|
238
|
-
if (tierRank[recommendedTier] < tierRank[riskFloor]) {
|
|
239
|
-
recommendedTier = riskFloor;
|
|
240
|
-
reason += ` (escalated to ${riskFloor} by risk floor)`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const confidenceBase = novelty === 'known' ? 0.9
|
|
244
|
-
: novelty === 'variation' ? 0.75
|
|
245
|
-
: 0.6;
|
|
246
|
-
const confidence = Math.max(0.3, confidenceBase - (risk === 'critical' ? 0.2 : 0));
|
|
247
|
-
|
|
248
|
-
const estimatedTokens = TIER_TOKENS[recommendedTier] ?? 0;
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
novelty,
|
|
252
|
-
risk,
|
|
253
|
-
complexity,
|
|
254
|
-
confidence,
|
|
255
|
-
recommendedTier,
|
|
256
|
-
reason,
|
|
257
|
-
estimatedTokens,
|
|
258
|
-
hardEscalation,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function think(question, options = {}, cwd = process.cwd()) {
|
|
263
|
-
const result = {
|
|
264
|
-
question,
|
|
265
|
-
startedAt: Date.now(),
|
|
266
|
-
tier: null,
|
|
267
|
-
phases: [],
|
|
268
|
-
answer: null,
|
|
269
|
-
tokensUsed: 0,
|
|
270
|
-
cost: 'minimal',
|
|
271
|
-
fromCache: false,
|
|
272
|
-
decision: null,
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
if (!options.skipRecall) {
|
|
276
|
-
const preflight = lookupDecision(question, options.tags || [], cwd);
|
|
277
|
-
result.phases.push({ phase: 'recall', ...preflight });
|
|
278
|
-
|
|
279
|
-
if (preflight.recommendation === 'reuse' && preflight.candidates[0]) {
|
|
280
|
-
result.tier = 'recall';
|
|
281
|
-
result.answer = preflight.candidates[0].decision;
|
|
282
|
-
result.fromCache = true;
|
|
283
|
-
result.cost = 'zero';
|
|
284
|
-
result.tokensUsed = 0;
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const recallPhase = result.phases[0] ?? null;
|
|
290
|
-
const triage = triageQuestion(question, options.projectBrief, recallPhase);
|
|
291
|
-
result.phases.push({ phase: 'triage', ...triage });
|
|
292
|
-
result.tier = options.forceLevel || triage.recommendedTier;
|
|
293
|
-
|
|
294
|
-
result.tokensUsed = TIER_TOKENS[result.tier] ?? triage.estimatedTokens;
|
|
295
|
-
result.cost = TIER_COST[result.tier] ?? 'moderate';
|
|
296
|
-
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function persistDecision(question, answer, tier, options = {}, cwd = process.cwd()) {
|
|
301
|
-
const dir = join(cwd, DOCS_DIR);
|
|
302
|
-
if (!existsSync(dir)) {
|
|
303
|
-
mkdirSync(dir, { recursive: true });
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const kw = normalizeIntent(question);
|
|
307
|
-
const normalizedIntent = kw.join(' ');
|
|
308
|
-
|
|
309
|
-
const answerText = typeof answer === 'string' ? answer : JSON.stringify(answer);
|
|
310
|
-
const sentences = answerText.match(/[^.!?]+[.!?]+/g) ?? [];
|
|
311
|
-
const rationale = sentences.slice(0, 3).map(s => s.trim()).filter(Boolean);
|
|
312
|
-
|
|
313
|
-
const autoTags = [];
|
|
314
|
-
const q = question.toLowerCase();
|
|
315
|
-
if (/auth|security|credential|secret|token/.test(q)) autoTags.push('security');
|
|
316
|
-
if (/migration|migrate|upgrade/.test(q)) autoTags.push('migration');
|
|
317
|
-
if (/architecture|design|structure|pattern/.test(q)) autoTags.push('architecture');
|
|
318
|
-
if (/test|spec|coverage/.test(q)) autoTags.push('testing');
|
|
319
|
-
if (/deploy|release|publish|production/.test(q)) autoTags.push('deployment');
|
|
320
|
-
if (/routing|dispatch|pipeline/.test(q)) autoTags.push('routing');
|
|
321
|
-
|
|
322
|
-
const tags = [...new Set([...(options.tags || []), ...autoTags])];
|
|
323
|
-
|
|
324
|
-
const contextSpecific = /this session|right now|current branch|today|temporary|one.?off/i.test(answerText);
|
|
325
|
-
const reusable = !contextSpecific;
|
|
326
|
-
|
|
327
|
-
const tokensUsed = options.tokensUsed ?? TIER_TOKENS[tier] ?? 0;
|
|
328
|
-
|
|
329
|
-
const now = new Date();
|
|
330
|
-
const expiresAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
331
|
-
|
|
332
|
-
const confScore = options.confidence ?? (
|
|
333
|
-
tier === 'ultra' || tier === 'deep' ? 'high'
|
|
334
|
-
: tier === 'standard' ? 'medium'
|
|
335
|
-
: 'low'
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
const entry = {
|
|
339
|
-
id: `dec_${Date.now()}`,
|
|
340
|
-
timestamp: now.toISOString(),
|
|
341
|
-
question,
|
|
342
|
-
normalizedIntent,
|
|
343
|
-
decision: answerText,
|
|
344
|
-
rationale,
|
|
345
|
-
tags,
|
|
346
|
-
confidence: typeof confScore === 'string' ? confScore : (confScore > 0.7 ? 'high' : confScore > 0.4 ? 'medium' : 'low'),
|
|
347
|
-
tier,
|
|
348
|
-
tokensUsed,
|
|
349
|
-
expiresAt,
|
|
350
|
-
reusable,
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
appendFileSync(join(dir, DECISIONS_FILE), JSON.stringify(entry) + '\n');
|
|
354
|
-
return entry;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export function getThinkingStats(cwd = process.cwd()) {
|
|
358
|
-
const decisions = readDecisions(cwd);
|
|
359
|
-
if (!decisions.length) {
|
|
360
|
-
return {
|
|
361
|
-
totalDecisions: 0,
|
|
362
|
-
cacheHits: 0,
|
|
363
|
-
cacheHitRate: 0,
|
|
364
|
-
tierDistribution: { recall: 0, quick: 0, standard: 0, deep: 0, ultra: 0 },
|
|
365
|
-
totalTokensSaved: 0,
|
|
366
|
-
avgTier: 'none',
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const tierDist = { recall: 0, quick: 0, standard: 0, deep: 0, ultra: 0 };
|
|
371
|
-
let cacheHits = 0;
|
|
372
|
-
let totalTokensSaved = 0;
|
|
373
|
-
const tierCounts = {};
|
|
374
|
-
|
|
375
|
-
for (const dec of decisions) {
|
|
376
|
-
const t = dec.tier ?? 'standard';
|
|
377
|
-
if (tierDist[t] !== undefined) tierDist[t]++;
|
|
378
|
-
tierCounts[t] = (tierCounts[t] ?? 0) + 1;
|
|
379
|
-
|
|
380
|
-
if (t === 'recall') {
|
|
381
|
-
cacheHits++;
|
|
382
|
-
totalTokensSaved += TIER_TOKENS.standard;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const cacheHitRate = decisions.length > 0 ? cacheHits / decisions.length : 0;
|
|
387
|
-
|
|
388
|
-
let maxCount = 0;
|
|
389
|
-
let avgTier = 'standard';
|
|
390
|
-
for (const [tier, count] of Object.entries(tierCounts)) {
|
|
391
|
-
if (count > maxCount) { maxCount = count; avgTier = tier; }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
totalDecisions: decisions.length,
|
|
396
|
-
cacheHits,
|
|
397
|
-
cacheHitRate: Math.round(cacheHitRate * 1000) / 1000,
|
|
398
|
-
tierDistribution: tierDist,
|
|
399
|
-
totalTokensSaved,
|
|
400
|
-
avgTier,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export function formatThinkResult(result) {
|
|
405
|
-
const { tier, phases, cost, fromCache, tokensUsed } = result;
|
|
406
|
-
|
|
407
|
-
const tierLabel = tier ? tier.charAt(0).toUpperCase() + tier.slice(1) : 'Unknown';
|
|
408
|
-
const tokenStr = tokensUsed > 0 ? `${(tokensUsed / 1000).toFixed(0)}K tokens estimated` : 'zero tokens';
|
|
409
|
-
|
|
410
|
-
const lines = [`THINKING: ${tierLabel} tier (${tokenStr})`];
|
|
411
|
-
|
|
412
|
-
for (const phase of phases ?? []) {
|
|
413
|
-
if (phase.phase === 'recall') {
|
|
414
|
-
const count = phase.candidates?.length ?? 0;
|
|
415
|
-
const found = count > 0
|
|
416
|
-
? `${count} related precedent${count === 1 ? '' : 's'} found`
|
|
417
|
-
: 'no prior decisions found';
|
|
418
|
-
lines.push(` Phase 1: Recall — ${found}`);
|
|
419
|
-
} else if (phase.phase === 'triage') {
|
|
420
|
-
lines.push(` Phase 2: Triage — ${phase.novelty ?? 'novel'} question, ${phase.risk ?? 'medium'} risk`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
lines.push(` Cost: ${cost ?? 'unknown'}`);
|
|
425
|
-
if (fromCache) lines.push(' Source: decision cache (no model call needed)');
|
|
426
|
-
|
|
427
|
-
return lines.join('\n');
|
|
428
|
-
}
|
package/src/tui.mjs
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
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
|
-
// ── Modern box rendering with rounded corners ────────────────────────────────
|
|
175
|
-
|
|
176
|
-
const ROUNDED = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', ml: '├', mr: '┤' };
|
|
177
|
-
|
|
178
|
-
export function panel(title, content, opts = {}) {
|
|
179
|
-
const { width = 70, titleColor = '\x1b[36m', borderColor = '\x1b[2m', reset = '\x1b[0m' } = opts;
|
|
180
|
-
const lines = [];
|
|
181
|
-
const innerW = width - 2;
|
|
182
|
-
|
|
183
|
-
// Top border with title
|
|
184
|
-
if (title) {
|
|
185
|
-
const titleStr = ` ${title} `;
|
|
186
|
-
const remaining = innerW - titleStr.length - 1;
|
|
187
|
-
lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h} ${titleColor}${title}${borderColor} ${ROUNDED.h.repeat(Math.max(0, remaining))}${ROUNDED.tr}${reset}`);
|
|
188
|
-
} else {
|
|
189
|
-
lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h.repeat(innerW)}${ROUNDED.tr}${reset}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Content lines
|
|
193
|
-
const contentLines = (typeof content === 'string' ? content.split('\n') : content);
|
|
194
|
-
for (const line of contentLines) {
|
|
195
|
-
const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
196
|
-
const pad = Math.max(0, innerW - stripped.length);
|
|
197
|
-
lines.push(`${borderColor}${ROUNDED.v}${reset} ${line}${' '.repeat(pad)}${borderColor}${ROUNDED.v}${reset}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Bottom border
|
|
201
|
-
lines.push(`${borderColor}${ROUNDED.bl}${ROUNDED.h.repeat(innerW)}${ROUNDED.br}${reset}`);
|
|
202
|
-
|
|
203
|
-
return lines.join('\n');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function divider(width = 70) {
|
|
207
|
-
const borderColor = '\x1b[2m';
|
|
208
|
-
const reset = '\x1b[0m';
|
|
209
|
-
return `${borderColor}${ROUNDED.ml}${ROUNDED.h.repeat(width - 2)}${ROUNDED.mr}${reset}`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function statusChip(label, healthy, opts = {}) {
|
|
213
|
-
const green = '\x1b[32m';
|
|
214
|
-
const red = '\x1b[31m';
|
|
215
|
-
const dim = '\x1b[2m';
|
|
216
|
-
const reset = '\x1b[0m';
|
|
217
|
-
const icon = healthy ? `${green}●${reset}` : `${red}●${reset}`;
|
|
218
|
-
return `${icon} ${dim}${label}${reset}`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function headerBar(left, right, width = 70) {
|
|
222
|
-
const leftStripped = left.replace(/\x1b\[[0-9;]*m/g, '');
|
|
223
|
-
const rightStripped = right.replace(/\x1b\[[0-9;]*m/g, '');
|
|
224
|
-
const gap = Math.max(1, width - leftStripped.length - rightStripped.length);
|
|
225
|
-
return `${left}${' '.repeat(gap)}${right}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function prompt(text = '> task or /help') {
|
|
229
|
-
const cyan = '\x1b[36m';
|
|
230
|
-
const dim = '\x1b[2m';
|
|
231
|
-
const reset = '\x1b[0m';
|
|
232
|
-
return `${cyan}>${reset} ${dim}${text.replace(/^>\s*/, '')}${reset}`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function signalLine(type, text, meta = '') {
|
|
236
|
-
const green = '\x1b[32m';
|
|
237
|
-
const yellow = '\x1b[33m';
|
|
238
|
-
const dim = '\x1b[2m';
|
|
239
|
-
const reset = '\x1b[0m';
|
|
240
|
-
|
|
241
|
-
let icon;
|
|
242
|
-
switch (type) {
|
|
243
|
-
case 'success': icon = `${green}✓${reset}`; break;
|
|
244
|
-
case 'warning': icon = `${yellow}!${reset}`; break;
|
|
245
|
-
case 'info': icon = `${dim}·${reset}`; break;
|
|
246
|
-
default: icon = `${dim}·${reset}`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const metaStr = meta ? `${dim}${meta}${reset}` : '';
|
|
250
|
-
return `${icon} ${text}${metaStr ? ' ' + metaStr : ''}`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ─── Self-test ────────────────────────────────────────────────────────────────
|
|
254
|
-
|
|
255
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
256
|
-
// Read version dynamically from package.json
|
|
257
|
-
let selfTestVersion = '0.0.0';
|
|
258
|
-
try {
|
|
259
|
-
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
260
|
-
selfTestVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
261
|
-
} catch { /* fallback to 0.0.0 */ }
|
|
262
|
-
|
|
263
|
-
console.log(box(`🧠 Dual-Brain v${selfTestVersion}`, [
|
|
264
|
-
'🟢 Claude ✅ 🟢 OpenAI ✅',
|
|
265
|
-
'🌀 Replit + replit-tools',
|
|
266
|
-
]));
|
|
267
|
-
console.log(bar(75, 20, { label: 'Claude' }));
|
|
268
|
-
console.log(bar(25, 20, { label: 'OpenAI' }));
|
|
269
|
-
console.log(menu([
|
|
270
|
-
{ key: 'c', label: 'Continue last session', section: 'Sessions' },
|
|
271
|
-
{ key: 'n', label: 'New session', section: 'Sessions' },
|
|
272
|
-
{ key: 'a', label: 'Auth management', section: 'Settings' },
|
|
273
|
-
{ key: 'p', label: 'Profile settings', section: 'Settings' },
|
|
274
|
-
{ key: 's', label: 'Exit to shell', section: '' },
|
|
275
|
-
]));
|
|
276
|
-
}
|