adversarial-mirror 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.
package/dist/cli.js ADDED
@@ -0,0 +1,2319 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/chat.ts
7
+ import { readFileSync as readFileSync2 } from "fs";
8
+ import { basename } from "path";
9
+ import React2 from "react";
10
+ import { render } from "ink";
11
+
12
+ // src/brains/anthropic.ts
13
+ import Anthropic from "@anthropic-ai/sdk";
14
+ var AnthropicAdapter = class {
15
+ id;
16
+ provider = "anthropic";
17
+ capabilities = { streaming: true };
18
+ client;
19
+ model;
20
+ constructor(id, model, apiKeyEnvVar) {
21
+ this.id = id;
22
+ this.model = model;
23
+ const apiKey = process.env[apiKeyEnvVar];
24
+ if (!apiKey) {
25
+ throw new Error(
26
+ `Missing API key. Set ${apiKeyEnvVar} or enable MOCK_BRAINS=true.`
27
+ );
28
+ }
29
+ this.client = new Anthropic({ apiKey });
30
+ }
31
+ async ping() {
32
+ const start = Date.now();
33
+ try {
34
+ await this.client.messages.create({
35
+ model: this.model,
36
+ max_tokens: 1,
37
+ messages: [{ role: "user", content: "ping" }]
38
+ });
39
+ return { ok: true, latencyMs: Date.now() - start };
40
+ } catch (err) {
41
+ return { ok: false, error: err.message };
42
+ }
43
+ }
44
+ async *chat(messages, systemPrompt, options) {
45
+ const filtered = messages.filter((m) => m.role !== "system").map((m) => ({
46
+ role: m.role === "assistant" ? "assistant" : "user",
47
+ content: m.content
48
+ }));
49
+ const stream = this.client.messages.stream(
50
+ {
51
+ model: this.model,
52
+ max_tokens: options?.maxTokens ?? 1024,
53
+ system: systemPrompt,
54
+ messages: filtered
55
+ },
56
+ { signal: options?.signal }
57
+ );
58
+ let text = "";
59
+ for await (const event of stream) {
60
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
61
+ const delta = event.delta.text;
62
+ text += delta;
63
+ yield { delta, isFinal: false };
64
+ }
65
+ }
66
+ const finalMessage = await stream.finalMessage();
67
+ const inputTokens = finalMessage.usage.input_tokens;
68
+ const outputTokens = finalMessage.usage.output_tokens;
69
+ yield { delta: "", isFinal: true, inputTokens, outputTokens };
70
+ return { text, inputTokens, outputTokens };
71
+ }
72
+ estimateTokens(messages) {
73
+ return messages.reduce((sum, msg) => sum + msg.content.length, 0);
74
+ }
75
+ async dispose() {
76
+ return;
77
+ }
78
+ };
79
+
80
+ // src/brains/gemini.ts
81
+ import { GoogleGenerativeAI } from "@google/generative-ai";
82
+ var GeminiAdapter = class {
83
+ id;
84
+ provider = "gemini";
85
+ capabilities = { streaming: true };
86
+ model;
87
+ client;
88
+ constructor(id, model, apiKeyEnvVar) {
89
+ this.id = id;
90
+ this.model = model;
91
+ const apiKey = process.env[apiKeyEnvVar];
92
+ if (!apiKey) {
93
+ throw new Error(
94
+ `Missing API key. Set ${apiKeyEnvVar} or enable MOCK_BRAINS=true.`
95
+ );
96
+ }
97
+ this.client = new GoogleGenerativeAI(apiKey);
98
+ }
99
+ async ping() {
100
+ return { ok: true };
101
+ }
102
+ async *chat(messages, systemPrompt, options) {
103
+ const model = this.client.getGenerativeModel({ model: this.model });
104
+ const contents = messages.filter((message) => message.role !== "system").map((message) => ({
105
+ role: message.role === "assistant" ? "model" : "user",
106
+ parts: [{ text: message.content }]
107
+ }));
108
+ const result = await model.generateContentStream({
109
+ contents,
110
+ systemInstruction: {
111
+ role: "system",
112
+ parts: [{ text: systemPrompt }]
113
+ },
114
+ generationConfig: {
115
+ temperature: options?.temperature,
116
+ maxOutputTokens: options?.maxTokens
117
+ }
118
+ });
119
+ let text = "";
120
+ for await (const chunk of result.stream) {
121
+ const delta = chunk.text();
122
+ if (delta) {
123
+ text += delta;
124
+ yield { delta, isFinal: false };
125
+ }
126
+ }
127
+ let inputTokens;
128
+ let outputTokens;
129
+ try {
130
+ const response2 = await result.response;
131
+ const usage = response2.usageMetadata;
132
+ if (usage) {
133
+ inputTokens = usage.promptTokenCount;
134
+ outputTokens = usage.candidatesTokenCount ?? usage.totalTokenCount ?? void 0;
135
+ }
136
+ } catch {
137
+ }
138
+ const response = { text, inputTokens, outputTokens };
139
+ yield { delta: "", isFinal: true, inputTokens, outputTokens };
140
+ return response;
141
+ }
142
+ estimateTokens(messages) {
143
+ return messages.reduce((sum, msg) => sum + msg.content.length, 0);
144
+ }
145
+ async dispose() {
146
+ return;
147
+ }
148
+ };
149
+
150
+ // src/brains/mock.ts
151
+ var MockAdapter = class {
152
+ id;
153
+ provider = "mock";
154
+ capabilities = { streaming: true };
155
+ responseText;
156
+ constructor(id, responseText = "Mock response.") {
157
+ this.id = id;
158
+ this.responseText = responseText;
159
+ }
160
+ async ping() {
161
+ return { ok: true, latencyMs: 1 };
162
+ }
163
+ async *chat(_messages, _systemPrompt, _options) {
164
+ for (const chunk of this.responseText.split(" ")) {
165
+ yield { delta: `${chunk} `, isFinal: false };
166
+ }
167
+ const response = { text: this.responseText };
168
+ yield { delta: "", isFinal: true };
169
+ return response;
170
+ }
171
+ estimateTokens(messages) {
172
+ return messages.reduce((sum, msg) => sum + msg.content.length, 0);
173
+ }
174
+ async dispose() {
175
+ return;
176
+ }
177
+ };
178
+
179
+ // src/brains/openai.ts
180
+ import OpenAI from "openai";
181
+ var OpenAIAdapter = class {
182
+ id;
183
+ provider = "openai";
184
+ capabilities = { streaming: true };
185
+ model;
186
+ client;
187
+ constructor(id, model, apiKeyEnvVar) {
188
+ this.id = id;
189
+ this.model = model;
190
+ const apiKey = process.env[apiKeyEnvVar];
191
+ if (!apiKey) {
192
+ throw new Error(
193
+ `Missing API key. Set ${apiKeyEnvVar} or enable MOCK_BRAINS=true.`
194
+ );
195
+ }
196
+ this.client = new OpenAI({ apiKey });
197
+ }
198
+ async ping() {
199
+ const start = Date.now();
200
+ try {
201
+ await this.client.models.list();
202
+ return { ok: true, latencyMs: Date.now() - start };
203
+ } catch (err) {
204
+ return { ok: false, error: err.message };
205
+ }
206
+ }
207
+ async *chat(messages, systemPrompt, options) {
208
+ const stream = await this.client.chat.completions.create(
209
+ {
210
+ model: this.model,
211
+ stream: true,
212
+ stream_options: { include_usage: true },
213
+ temperature: options?.temperature,
214
+ max_tokens: options?.maxTokens,
215
+ messages: [
216
+ { role: "system", content: systemPrompt },
217
+ ...messages.filter((message) => message.role !== "system").map((message) => ({
218
+ role: message.role === "assistant" ? "assistant" : "user",
219
+ content: message.content
220
+ }))
221
+ ]
222
+ },
223
+ { signal: options?.signal }
224
+ );
225
+ let text = "";
226
+ let inputTokens;
227
+ let outputTokens;
228
+ for await (const chunk of stream) {
229
+ const delta = chunk.choices?.[0]?.delta?.content ?? "";
230
+ if (delta) {
231
+ text += delta;
232
+ yield { delta, isFinal: false };
233
+ }
234
+ if (chunk.usage) {
235
+ inputTokens = chunk.usage.prompt_tokens;
236
+ outputTokens = chunk.usage.completion_tokens;
237
+ }
238
+ }
239
+ const response = {
240
+ text,
241
+ inputTokens,
242
+ outputTokens
243
+ };
244
+ yield { delta: "", isFinal: true, inputTokens, outputTokens };
245
+ return response;
246
+ }
247
+ estimateTokens(messages) {
248
+ return messages.reduce((sum, msg) => sum + msg.content.length, 0);
249
+ }
250
+ async dispose() {
251
+ return;
252
+ }
253
+ };
254
+
255
+ // src/brains/factory.ts
256
+ function createAdapter(config2, overrides = {}) {
257
+ const effective = { ...config2, ...overrides };
258
+ if (process.env.MOCK_BRAINS) {
259
+ return new MockAdapter(effective.id, `Mock response from ${effective.id}.`);
260
+ }
261
+ switch (effective.provider) {
262
+ case "anthropic":
263
+ return new AnthropicAdapter(
264
+ effective.id,
265
+ effective.model,
266
+ effective.apiKeyEnvVar
267
+ );
268
+ case "openai":
269
+ return new OpenAIAdapter(
270
+ effective.id,
271
+ effective.model,
272
+ effective.apiKeyEnvVar
273
+ );
274
+ case "gemini":
275
+ return new GeminiAdapter(
276
+ effective.id,
277
+ effective.model,
278
+ effective.apiKeyEnvVar
279
+ );
280
+ case "mock":
281
+ return new MockAdapter(effective.id, `Mock response from ${effective.id}.`);
282
+ default:
283
+ throw new Error(`Unsupported provider: ${effective.provider}`);
284
+ }
285
+ }
286
+
287
+ // src/config/loader.ts
288
+ import Conf from "conf";
289
+
290
+ // src/config/schema.ts
291
+ import { z } from "zod";
292
+ var brainConfigSchema = z.object({
293
+ id: z.string().min(1),
294
+ provider: z.enum(["anthropic", "openai", "gemini", "mock"]),
295
+ model: z.string().min(1),
296
+ apiKeyEnvVar: z.string().min(1)
297
+ });
298
+ var configSchema = z.object({
299
+ version: z.number().int().positive(),
300
+ session: z.object({
301
+ originalBrainId: z.string().min(1),
302
+ challengerBrainId: z.string().min(1),
303
+ defaultIntensity: z.enum(["mild", "moderate", "aggressive"]),
304
+ historyWindowSize: z.number().int().positive(),
305
+ autoClassify: z.boolean(),
306
+ judgeEnabled: z.boolean().default(true),
307
+ judgeBrainId: z.string().min(1).default("claude-sonnet-4-6"),
308
+ defaultPersona: z.string().optional()
309
+ }),
310
+ ui: z.object({
311
+ layout: z.enum(["side-by-side", "stacked"]),
312
+ showTokenCounts: z.boolean(),
313
+ showLatency: z.boolean(),
314
+ syntaxHighlighting: z.boolean()
315
+ }),
316
+ brains: z.array(brainConfigSchema).min(1),
317
+ classifier: z.object({
318
+ brainId: z.string().min(1),
319
+ model: z.string().min(1),
320
+ confidenceThreshold: z.number().min(0).max(1)
321
+ })
322
+ });
323
+
324
+ // src/config/defaults.ts
325
+ var defaultConfig = {
326
+ version: 1,
327
+ session: {
328
+ originalBrainId: "claude-sonnet-4-6",
329
+ challengerBrainId: "gpt-4o",
330
+ defaultIntensity: "moderate",
331
+ historyWindowSize: 20,
332
+ autoClassify: true,
333
+ judgeEnabled: true,
334
+ judgeBrainId: "claude-sonnet-4-6",
335
+ defaultPersona: void 0
336
+ },
337
+ ui: {
338
+ layout: "side-by-side",
339
+ showTokenCounts: false,
340
+ showLatency: true,
341
+ syntaxHighlighting: true
342
+ },
343
+ brains: [
344
+ {
345
+ id: "claude-sonnet-4-6",
346
+ provider: "anthropic",
347
+ model: "claude-sonnet-4-6",
348
+ apiKeyEnvVar: "ANTHROPIC_API_KEY"
349
+ },
350
+ {
351
+ id: "gpt-4o",
352
+ provider: "openai",
353
+ model: "gpt-4o",
354
+ apiKeyEnvVar: "OPENAI_API_KEY"
355
+ },
356
+ {
357
+ id: "o3-mini",
358
+ provider: "openai",
359
+ model: "o3-mini",
360
+ apiKeyEnvVar: "OPENAI_API_KEY"
361
+ },
362
+ {
363
+ id: "gemini-pro",
364
+ provider: "gemini",
365
+ model: "gemini-2.5-pro",
366
+ apiKeyEnvVar: "GOOGLE_API_KEY"
367
+ }
368
+ ],
369
+ classifier: {
370
+ brainId: "claude-sonnet-4-6",
371
+ model: "claude-haiku-4-5-20251001",
372
+ confidenceThreshold: 0.75
373
+ }
374
+ };
375
+
376
+ // src/config/loader.ts
377
+ var store = new Conf({
378
+ projectName: "adversarial-mirror",
379
+ configName: "config",
380
+ defaults: defaultConfig
381
+ });
382
+ function loadConfig() {
383
+ return configSchema.parse(store.store);
384
+ }
385
+ function saveConfig(next) {
386
+ store.store = configSchema.parse(next);
387
+ }
388
+ function setConfigValue(path, value) {
389
+ const current = loadConfig();
390
+ const updated = setByPath({ ...current }, path, value);
391
+ saveConfig(updated);
392
+ return updated;
393
+ }
394
+ function setByPath(config2, path, value) {
395
+ const keys = path.split(".").filter(Boolean);
396
+ if (keys.length === 0) {
397
+ return config2;
398
+ }
399
+ let cursor = config2;
400
+ for (let i = 0; i < keys.length - 1; i += 1) {
401
+ const key = keys[i];
402
+ if (typeof cursor[key] !== "object" || cursor[key] === null) {
403
+ cursor[key] = {};
404
+ }
405
+ cursor = cursor[key];
406
+ }
407
+ cursor[keys[keys.length - 1]] = value;
408
+ return config2;
409
+ }
410
+
411
+ // src/engine/intent-classifier.ts
412
+ var intentSystemPrompt = `You are an intent classifier for a CLI assistant.
413
+ Return strict JSON with keys: category, shouldMirror, confidence, reason.
414
+ Categories: factual_lookup, math_computation, code_task, conversational, opinion_advice, analysis, interpretation, prediction.
415
+ Rules:
416
+ - factual_lookup, math_computation, code_task, conversational => shouldMirror false
417
+ - opinion_advice, analysis, interpretation, prediction => shouldMirror true
418
+ Confidence is 0-1.
419
+ Return ONLY JSON.`;
420
+ var HeuristicIntentClassifier = class {
421
+ async classify(input3) {
422
+ const trimmed = input3.trim().toLowerCase();
423
+ const looksFactual = trimmed.startsWith("who ") || trimmed.startsWith("what ") || trimmed.startsWith("when ") || trimmed.startsWith("where ");
424
+ const category = looksFactual ? "factual_lookup" : "analysis";
425
+ const shouldMirror = !looksFactual;
426
+ return {
427
+ category,
428
+ shouldMirror,
429
+ confidence: looksFactual ? 0.55 : 0.45,
430
+ reason: looksFactual ? "Heuristic: question starts with who/what/when/where." : "Heuristic: default to analysis for open-ended prompts."
431
+ };
432
+ }
433
+ };
434
+ var BrainIntentClassifier = class {
435
+ adapter;
436
+ threshold;
437
+ constructor(adapter, threshold = 0.75) {
438
+ this.adapter = adapter;
439
+ this.threshold = threshold;
440
+ }
441
+ async classify(input3) {
442
+ const messages = [{ role: "user", content: input3 }];
443
+ const options = { temperature: 0 };
444
+ const stream = this.adapter.chat(messages, intentSystemPrompt, options);
445
+ let text = "";
446
+ for await (const chunk of stream) {
447
+ if (chunk.delta) {
448
+ text += chunk.delta;
449
+ }
450
+ }
451
+ const parsed = safeParseIntent(text);
452
+ const category = parsed.category;
453
+ const shouldMirror = parsed.shouldMirror;
454
+ const confidence = parsed.confidence;
455
+ if (confidence < this.threshold) {
456
+ return {
457
+ ...parsed,
458
+ shouldMirror: true,
459
+ reason: `${parsed.reason} (below confidence threshold ${this.threshold}).`
460
+ };
461
+ }
462
+ return {
463
+ category,
464
+ shouldMirror,
465
+ confidence,
466
+ reason: parsed.reason
467
+ };
468
+ }
469
+ };
470
+ function safeParseIntent(text) {
471
+ const trimmed = text.trim();
472
+ const start = trimmed.indexOf("{");
473
+ const end = trimmed.lastIndexOf("}");
474
+ if (start === -1 || end === -1 || end <= start) {
475
+ throw new Error("Classifier returned non-JSON output.");
476
+ }
477
+ const json = trimmed.slice(start, end + 1);
478
+ const parsed = JSON.parse(json);
479
+ const category = normalizeCategory(parsed.category);
480
+ const confidence = clamp(
481
+ typeof parsed.confidence === "number" ? parsed.confidence : 0
482
+ );
483
+ const shouldMirror = typeof parsed.shouldMirror === "boolean" ? parsed.shouldMirror : ["opinion_advice", "analysis", "interpretation", "prediction"].includes(
484
+ category
485
+ );
486
+ const reason = typeof parsed.reason === "string" && parsed.reason ? parsed.reason : "No reason provided.";
487
+ return { category, shouldMirror, confidence, reason };
488
+ }
489
+ function normalizeCategory(value) {
490
+ const allowed = [
491
+ "factual_lookup",
492
+ "math_computation",
493
+ "code_task",
494
+ "conversational",
495
+ "opinion_advice",
496
+ "analysis",
497
+ "interpretation",
498
+ "prediction"
499
+ ];
500
+ if (typeof value === "string" && allowed.includes(value)) {
501
+ return value;
502
+ }
503
+ return "analysis";
504
+ }
505
+ function clamp(value) {
506
+ if (value < 0) return 0;
507
+ if (value > 1) return 1;
508
+ return value;
509
+ }
510
+
511
+ // src/engine/classifier-factory.ts
512
+ function buildIntentClassifier(config2, debug = false) {
513
+ const classifierConfig = config2.classifier;
514
+ const brainConfig = config2.brains.find(
515
+ (brain) => brain.id === classifierConfig.brainId
516
+ );
517
+ if (!brainConfig) {
518
+ if (debug) {
519
+ process.stderr.write(
520
+ `[debug] Classifier brain not found: ${classifierConfig.brainId}. Using heuristic.
521
+ `
522
+ );
523
+ }
524
+ return new HeuristicIntentClassifier();
525
+ }
526
+ try {
527
+ const adapter = createAdapter(brainConfig, { model: classifierConfig.model });
528
+ return new BrainIntentClassifier(
529
+ adapter,
530
+ classifierConfig.confidenceThreshold
531
+ );
532
+ } catch (error) {
533
+ if (debug) {
534
+ process.stderr.write(
535
+ `[debug] Failed to init classifier: ${error.message}. Using heuristic.
536
+ `
537
+ );
538
+ }
539
+ return new HeuristicIntentClassifier();
540
+ }
541
+ }
542
+
543
+ // src/engine/prompt-builder.ts
544
+ var baseRule = "Every point must have a specific mechanism. Vague doubt is useless.";
545
+ var mild = `You are a gentle critic. Provide a full answer, then 1-2 real gaps and a steelman alternative. ${baseRule}`;
546
+ var moderate = `You are a devil's advocate.
547
+ 1. REFRAME the implicit assumption.
548
+ 2. CHALLENGE THE FRAME with the question the user should have asked.
549
+ 3. SURFACE HIDDEN COSTS that are under-weighted.
550
+ 4. STRONGEST COUNTERPOSITION (no straw man).
551
+ 5. VERDICT with honest synthesis.
552
+ ${baseRule}`;
553
+ var aggressive = `You are adversarial.
554
+ 1. BURIED ASSUMPTION: the most consequential unstated assumption.
555
+ 2. STRONGEST REFUTATION against the dominant view.
556
+ 3. FAILURE CASES: 2-3 concrete scenarios where standard advice fails.
557
+ 4. EXPERT DISSENT: represent serious dissenting thinkers.
558
+ 5. HONEST SYNTHESIS with calibrated confidence.
559
+ ${baseRule}`;
560
+ function buildChallengerPrompt(intensity) {
561
+ switch (intensity) {
562
+ case "mild":
563
+ return mild;
564
+ case "moderate":
565
+ return moderate;
566
+ case "aggressive":
567
+ return aggressive;
568
+ default:
569
+ return moderate;
570
+ }
571
+ }
572
+ function buildOriginalPrompt() {
573
+ return "You are the primary assistant. Provide the best direct answer.";
574
+ }
575
+ var PERSONAS = {
576
+ "vc-skeptic": {
577
+ label: "VC Skeptic",
578
+ lens: "Investor/VC scrutiny",
579
+ focusAreas: [
580
+ "Market sizing assumptions \u2014 are they realistic or aspirational?",
581
+ "Unit economics \u2014 does the math work at scale?",
582
+ "Competitive moat \u2014 what stops a well-funded competitor from copying this?",
583
+ "Defensibility \u2014 what makes this durable beyond 18 months?"
584
+ ]
585
+ },
586
+ "security-auditor": {
587
+ label: "Security Auditor",
588
+ lens: "Security and risk analysis",
589
+ focusAreas: [
590
+ "Attack surfaces \u2014 what can be exploited externally or internally?",
591
+ "Trust boundaries \u2014 where are credentials, data, or permissions crossing lines?",
592
+ "Failure modes \u2014 what happens when this breaks under adversarial conditions?",
593
+ "Blast radius \u2014 what is the worst-case scope of a breach or failure?"
594
+ ]
595
+ },
596
+ "end-user": {
597
+ label: "End User",
598
+ lens: "Real user perspective",
599
+ focusAreas: [
600
+ "Real needs vs stated needs \u2014 what does the user actually want vs what they said?",
601
+ "Adoption friction \u2014 what will cause users to abandon this in the first week?",
602
+ "Actual behavior \u2014 what do users do vs what you think they will do?",
603
+ "Comprehension gaps \u2014 what will users misunderstand or misuse?"
604
+ ]
605
+ },
606
+ "regulator": {
607
+ label: "Regulator",
608
+ lens: "Compliance and legal exposure",
609
+ focusAreas: [
610
+ "Regulatory exposure \u2014 what laws, rules, or frameworks apply and are being ignored?",
611
+ "Liability \u2014 who bears legal responsibility when this causes harm?",
612
+ "Stakeholder harm \u2014 who could be injured, defrauded, or discriminated against?",
613
+ "Unintended consequences \u2014 what second-order effects could trigger enforcement action?"
614
+ ]
615
+ },
616
+ "contrarian": {
617
+ label: "Contrarian",
618
+ lens: "Pure intellectual opposition",
619
+ focusAreas: [
620
+ "Historical failures \u2014 name similar ideas that failed and why this is the same.",
621
+ "Second-order effects \u2014 what happens after the first-order success plays out?",
622
+ "Inverted premise \u2014 what if the opposite assumption is actually correct?",
623
+ "Consensus trap \u2014 why might the conventional wisdom here be exactly wrong?"
624
+ ]
625
+ }
626
+ };
627
+ function buildPersonaChallengerPrompt(persona, intensity) {
628
+ const def = PERSONAS[persona];
629
+ if (!def) return buildChallengerPrompt(intensity);
630
+ const focusList = def.focusAreas.map((area, i) => `${i + 1}. ${area}`).join("\n");
631
+ const basePrompt = buildChallengerPrompt(intensity);
632
+ return `You are applying the lens of a ${def.label} (${def.lens}).
633
+
634
+ Your specific focus areas for this lens:
635
+ ${focusList}
636
+
637
+ Apply this lens rigorously throughout your response. Every critique must flow from this professional perspective.
638
+
639
+ ---
640
+
641
+ ${basePrompt}`;
642
+ }
643
+ function isValidPersona(name) {
644
+ return name in PERSONAS;
645
+ }
646
+
647
+ // src/engine/judge.ts
648
+ function buildJudgeSystemPrompt() {
649
+ return `You are a neutral synthesis judge evaluating two AI responses to the same question.
650
+
651
+ Your output MUST follow this exact structure:
652
+
653
+ AGREEMENT: <number>%
654
+ <One sentence explaining what drives the score \u2014 where they converge or diverge>
655
+
656
+ SYNTHESIS
657
+ <The actual synthesized recommendation \u2014 the verdict after weighing both responses. Be concrete and actionable.>
658
+
659
+ BLIND SPOT
660
+ <What both models missed or assumed without questioning. Be specific \u2014 name the assumption or gap.>
661
+
662
+ Scoring guide for AGREEMENT:
663
+ - 90\u2013100%: Substantively identical conclusions, only stylistic differences
664
+ - 70\u201389%: Same core answer, meaningful differences in emphasis or caveats
665
+ - 50\u201369%: Partial overlap, notable disagreement on key points
666
+ - 30\u201349%: Different conclusions but some shared premises
667
+ - 0\u201329%: Fundamentally opposed positions
668
+
669
+ Be direct and critical. Do not praise either response.`;
670
+ }
671
+ function buildJudgeMessages(question, originalText, challengerText) {
672
+ return [
673
+ {
674
+ role: "user",
675
+ content: `QUESTION
676
+ ${question}
677
+
678
+ ---
679
+
680
+ RESPONSE A (Original)
681
+ ${originalText}
682
+
683
+ ---
684
+
685
+ RESPONSE B (Challenger)
686
+ ${challengerText}
687
+
688
+ ---
689
+
690
+ Provide your synthesis following the required format exactly.`
691
+ }
692
+ ];
693
+ }
694
+ function extractAgreementScore(text) {
695
+ const match = /AGREEMENT:\s*(-?\d+)%/i.exec(text);
696
+ if (!match) return void 0;
697
+ const n = parseInt(match[1], 10);
698
+ if (isNaN(n)) return void 0;
699
+ return Math.max(0, Math.min(100, n));
700
+ }
701
+
702
+ // src/engine/mirror-engine.ts
703
+ var MirrorEngine = class {
704
+ original;
705
+ challenger;
706
+ intensity;
707
+ autoClassify;
708
+ classifier;
709
+ debug;
710
+ judge;
711
+ persona;
712
+ constructor(options) {
713
+ this.original = options.original;
714
+ this.challenger = options.challenger;
715
+ this.intensity = options.intensity;
716
+ this.autoClassify = options.autoClassify;
717
+ this.classifier = options.classifier;
718
+ this.debug = options.debug ?? false;
719
+ this.judge = options.judge;
720
+ this.persona = options.persona;
721
+ }
722
+ async *run(userInput, history2, options) {
723
+ try {
724
+ if (this.autoClassify) {
725
+ yield { type: "classifying" };
726
+ let result;
727
+ try {
728
+ result = await this.classifier.classify(userInput);
729
+ } catch (error) {
730
+ this.log(`Classifier error: ${error.message}`);
731
+ result = {
732
+ category: "analysis",
733
+ shouldMirror: true,
734
+ confidence: 0,
735
+ reason: "Classifier error; defaulting to mirror."
736
+ };
737
+ }
738
+ yield { type: "classified", result };
739
+ if (!result.shouldMirror || !this.challenger) {
740
+ this.log("Classifier chose direct path.");
741
+ yield* this.runSingle(userInput, history2, options);
742
+ return;
743
+ }
744
+ }
745
+ if (!this.challenger) {
746
+ yield* this.runSingle(userInput, history2, options);
747
+ return;
748
+ }
749
+ yield* this.runMirror(userInput, history2, options);
750
+ } catch (error) {
751
+ yield { type: "error", error };
752
+ }
753
+ }
754
+ async *runSingle(userInput, history2, options) {
755
+ const messages = [...history2, { role: "user", content: userInput }];
756
+ const systemPrompt = buildOriginalPrompt();
757
+ const stream = this.streamWithRetry(
758
+ this.original,
759
+ messages,
760
+ systemPrompt,
761
+ options
762
+ );
763
+ const accumulator = createAccumulator();
764
+ for await (const chunk of stream) {
765
+ accumulator.add(chunk);
766
+ yield { type: "stream_chunk", brainId: this.original.id, chunk };
767
+ }
768
+ const response = accumulator.complete();
769
+ yield { type: "brain_complete", brainId: this.original.id, response };
770
+ yield { type: "all_complete" };
771
+ }
772
+ async *runMirror(userInput, history2, options) {
773
+ const originalMessages = [...history2, { role: "user", content: userInput }];
774
+ const challengerHistory = history2.map(
775
+ (message) => message.role === "assistant" ? {
776
+ ...message,
777
+ content: `[PREVIOUS ORIGINAL RESPONSE]
778
+ ${message.content}`
779
+ } : message
780
+ );
781
+ const challengerMessages = [
782
+ ...challengerHistory,
783
+ { role: "user", content: userInput }
784
+ ];
785
+ const originalPrompt = buildOriginalPrompt();
786
+ const challengerPrompt = this.persona && isValidPersona(this.persona) ? buildPersonaChallengerPrompt(this.persona, this.intensity) : buildChallengerPrompt(this.intensity);
787
+ const originalStream = this.streamWithRetry(
788
+ this.original,
789
+ originalMessages,
790
+ originalPrompt,
791
+ options
792
+ );
793
+ const challengerStream = this.streamWithRetry(
794
+ this.challenger,
795
+ challengerMessages,
796
+ challengerPrompt,
797
+ options
798
+ );
799
+ const originalAccumulator = createAccumulator();
800
+ const challengerAccumulator = createAccumulator();
801
+ for await (const item of mergeStreams([
802
+ { brainId: this.original.id, stream: originalStream, accumulator: originalAccumulator },
803
+ {
804
+ brainId: this.challenger.id,
805
+ stream: challengerStream,
806
+ accumulator: challengerAccumulator
807
+ }
808
+ ])) {
809
+ yield item;
810
+ }
811
+ const originalResponse = originalAccumulator.complete();
812
+ const challengerResponse = challengerAccumulator.complete();
813
+ yield {
814
+ type: "brain_complete",
815
+ brainId: this.original.id,
816
+ response: originalResponse
817
+ };
818
+ yield {
819
+ type: "brain_complete",
820
+ brainId: this.challenger.id,
821
+ response: challengerResponse
822
+ };
823
+ if (this.judge) {
824
+ yield* this.runJudge(userInput, originalResponse.text, challengerResponse.text, options);
825
+ }
826
+ yield { type: "all_complete" };
827
+ }
828
+ async *runJudge(question, originalText, challengerText, options) {
829
+ yield { type: "synthesizing" };
830
+ const messages = buildJudgeMessages(question, originalText, challengerText);
831
+ const systemPrompt = buildJudgeSystemPrompt();
832
+ const stream = this.streamWithRetry(this.judge, messages, systemPrompt, options);
833
+ const accumulator = createAccumulator();
834
+ for await (const chunk of stream) {
835
+ accumulator.add(chunk);
836
+ yield { type: "synthesis_chunk", chunk };
837
+ }
838
+ const response = accumulator.complete();
839
+ const agreementScore = extractAgreementScore(response.text);
840
+ const result = {
841
+ text: response.text,
842
+ agreementScore,
843
+ inputTokens: response.inputTokens,
844
+ outputTokens: response.outputTokens
845
+ };
846
+ yield { type: "synthesis_complete", result };
847
+ }
848
+ async *streamWithRetry(adapter, messages, systemPrompt, options, retries = 1) {
849
+ let attempt = 0;
850
+ while (true) {
851
+ try {
852
+ if (attempt > 0) {
853
+ this.log(`Retrying ${adapter.id} (attempt ${attempt + 1}).`);
854
+ }
855
+ const stream = adapter.chat(messages, systemPrompt, options);
856
+ for await (const chunk of stream) {
857
+ yield chunk;
858
+ }
859
+ return { text: "" };
860
+ } catch (error) {
861
+ if (attempt >= retries) {
862
+ throw error;
863
+ }
864
+ attempt += 1;
865
+ await delay(300 * attempt);
866
+ }
867
+ }
868
+ }
869
+ log(message) {
870
+ if (!this.debug) {
871
+ return;
872
+ }
873
+ process.stderr.write(`[debug] ${message}
874
+ `);
875
+ }
876
+ };
877
+ function createAccumulator() {
878
+ let text = "";
879
+ let inputTokens;
880
+ let outputTokens;
881
+ return {
882
+ add(chunk) {
883
+ if (chunk.delta) {
884
+ text += chunk.delta;
885
+ }
886
+ if (chunk.inputTokens !== void 0) {
887
+ inputTokens = chunk.inputTokens;
888
+ }
889
+ if (chunk.outputTokens !== void 0) {
890
+ outputTokens = chunk.outputTokens;
891
+ }
892
+ },
893
+ complete() {
894
+ return { text, inputTokens, outputTokens };
895
+ }
896
+ };
897
+ }
898
+ async function* mergeStreams(entries) {
899
+ const pending = entries.map((entry) => ({
900
+ entry,
901
+ next: entry.stream.next()
902
+ }));
903
+ while (pending.length > 0) {
904
+ const race = pending.map(
905
+ (item, index2) => item.next.then((result2) => ({ index: index2, result: result2 }))
906
+ );
907
+ const { index, result } = await Promise.race(race);
908
+ const current = pending[index];
909
+ if (result.done) {
910
+ pending.splice(index, 1);
911
+ continue;
912
+ }
913
+ current.entry.accumulator.add(result.value);
914
+ yield {
915
+ type: "stream_chunk",
916
+ brainId: current.entry.brainId,
917
+ chunk: result.value
918
+ };
919
+ current.next = current.entry.stream.next();
920
+ }
921
+ }
922
+ function delay(ms) {
923
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
924
+ }
925
+
926
+ // src/engine/session.ts
927
+ var Session = class {
928
+ maxHistory;
929
+ messages = [];
930
+ constructor(maxHistory = 20) {
931
+ this.maxHistory = maxHistory;
932
+ }
933
+ addUser(content) {
934
+ this.push({ role: "user", content });
935
+ }
936
+ addAssistant(content) {
937
+ this.push({ role: "assistant", content });
938
+ }
939
+ getHistory() {
940
+ return [...this.messages];
941
+ }
942
+ clear() {
943
+ this.messages.length = 0;
944
+ }
945
+ push(message) {
946
+ this.messages.push(message);
947
+ while (this.messages.length > this.maxHistory) {
948
+ this.messages.splice(0, 2);
949
+ }
950
+ }
951
+ };
952
+
953
+ // src/ui/mirror-app.tsx
954
+ import { randomUUID } from "crypto";
955
+ import { existsSync, readFileSync } from "fs";
956
+ import { resolve } from "path";
957
+ import { fileURLToPath } from "url";
958
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
959
+ import { Box as Box2, Static, Text as Text4, useInput, useStdout } from "ink";
960
+
961
+ // src/history/store.ts
962
+ import Conf2 from "conf";
963
+ var store2 = new Conf2({
964
+ projectName: "adversarial-mirror",
965
+ configName: "history",
966
+ defaults: { entries: [] }
967
+ });
968
+ var MAX_ENTRIES = 200;
969
+ function addHistoryEntry(entry) {
970
+ const entries = store2.store.entries ?? [];
971
+ const next = [entry, ...entries];
972
+ if (next.length > MAX_ENTRIES) {
973
+ next.length = MAX_ENTRIES;
974
+ }
975
+ store2.store = { entries: next };
976
+ }
977
+ function listHistory() {
978
+ return store2.store.entries ?? [];
979
+ }
980
+ function getHistory(id) {
981
+ return (store2.store.entries ?? []).find((entry) => entry.id === id);
982
+ }
983
+
984
+ // src/ui/components/BrainPanel.tsx
985
+ import { Box, Text } from "ink";
986
+ import { jsx, jsxs } from "react/jsx-runtime";
987
+ function BrainPanel({
988
+ title,
989
+ children,
990
+ width,
991
+ marginRight,
992
+ borderColor = "cyan"
993
+ }) {
994
+ return /* @__PURE__ */ jsxs(
995
+ Box,
996
+ {
997
+ flexDirection: "column",
998
+ borderStyle: "round",
999
+ borderColor,
1000
+ padding: 1,
1001
+ flexGrow: width ? 0 : 1,
1002
+ flexShrink: 1,
1003
+ minWidth: 0,
1004
+ width,
1005
+ marginRight,
1006
+ children: [
1007
+ /* @__PURE__ */ jsx(Text, { bold: true, color: borderColor, children: title }),
1008
+ children
1009
+ ]
1010
+ }
1011
+ );
1012
+ }
1013
+
1014
+ // src/ui/components/IntentBadge.tsx
1015
+ import { Text as Text2 } from "ink";
1016
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1017
+ function IntentBadge({ category, mirrored }) {
1018
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
1019
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: mirrored ? "blue" : "blackBright", color: "white", children: ` ${mirrored ? "MIRRORING" : "DIRECT"} ` }),
1020
+ /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1021
+ " ",
1022
+ category
1023
+ ] })
1024
+ ] });
1025
+ }
1026
+
1027
+ // src/ui/components/StreamingText.tsx
1028
+ import { Text as Text3 } from "ink";
1029
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1030
+ function StreamingText({ value, dim }) {
1031
+ return /* @__PURE__ */ jsxs3(Text3, { dimColor: dim, wrap: "wrap", children: [
1032
+ value,
1033
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u258C" })
1034
+ ] });
1035
+ }
1036
+
1037
+ // src/ui/utils/highlight.ts
1038
+ import { highlight } from "cli-highlight";
1039
+ function highlightCodeBlocks(text) {
1040
+ if (!text.includes("```")) {
1041
+ return text;
1042
+ }
1043
+ const fenceRegex = /```(\w+)?\n([\s\S]*?)```/g;
1044
+ let result = "";
1045
+ let lastIndex = 0;
1046
+ let match;
1047
+ while ((match = fenceRegex.exec(text)) !== null) {
1048
+ const [block, lang, code] = match;
1049
+ result += text.slice(lastIndex, match.index);
1050
+ try {
1051
+ const highlighted = highlight(code, {
1052
+ language: lang || void 0,
1053
+ ignoreIllegals: true
1054
+ });
1055
+ result += `
1056
+ ${highlighted}
1057
+ `;
1058
+ } catch {
1059
+ result += `
1060
+ ${code}
1061
+ `;
1062
+ }
1063
+ lastIndex = match.index + block.length;
1064
+ }
1065
+ result += text.slice(lastIndex);
1066
+ return result;
1067
+ }
1068
+
1069
+ // src/ui/mirror-app.tsx
1070
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1071
+ var GRAD = ["#00D2FF", "#3A7BD5", "#7F5AF0", "#FF6EC7", "#FFB86C"];
1072
+ function hexToRgb(hex) {
1073
+ const n = hex.replace("#", "");
1074
+ const v = parseInt(n.length === 3 ? n.split("").map((c) => c + c).join("") : n, 16);
1075
+ return { r: v >> 16 & 255, g: v >> 8 & 255, b: v & 255 };
1076
+ }
1077
+ function gradColor(pos) {
1078
+ const p = Math.max(0, Math.min(1, pos));
1079
+ const steps = GRAD.length - 1;
1080
+ const scaled = p * steps;
1081
+ const i = Math.min(Math.floor(scaled), steps - 1);
1082
+ const t = scaled - i;
1083
+ const a = hexToRgb(GRAD[i]);
1084
+ const b = hexToRgb(GRAD[i + 1]);
1085
+ const r = Math.round(a.r + (b.r - a.r) * t);
1086
+ const g = Math.round(a.g + (b.g - a.g) * t);
1087
+ const bv = Math.round(a.b + (b.b - a.b) * t);
1088
+ return `#${[r, g, bv].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
1089
+ }
1090
+ function GradientLine({ line, bold }) {
1091
+ if (!line.trim()) return /* @__PURE__ */ jsx4(Text4, { children: " " });
1092
+ const chars = Array.from(line);
1093
+ const last = Math.max(chars.length - 1, 1);
1094
+ return /* @__PURE__ */ jsx4(Text4, { bold, wrap: "truncate", children: chars.map((ch, idx) => /* @__PURE__ */ jsx4(Text4, { color: gradColor(idx / last), children: ch }, idx)) });
1095
+ }
1096
+ var HeaderView = React.memo(function HeaderView2({
1097
+ lines,
1098
+ originalId,
1099
+ challengerId,
1100
+ intensity
1101
+ }) {
1102
+ return /* @__PURE__ */ jsxs4(Box2, { flexDirection: "column", marginBottom: 1, children: [
1103
+ lines.map((line, i) => /* @__PURE__ */ jsx4(GradientLine, { line, bold: true }, i)),
1104
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", dimColor: true, children: [
1105
+ " ",
1106
+ originalId,
1107
+ challengerId ? ` vs ${challengerId}` : " [direct mode]",
1108
+ " ",
1109
+ "[",
1110
+ intensity,
1111
+ "]"
1112
+ ] })
1113
+ ] });
1114
+ });
1115
+ function stripAgreementHeader(text) {
1116
+ return text.replace(/^AGREEMENT:\s*-?\d+%[^\n]*\n?\n?/i, "").trimStart();
1117
+ }
1118
+ var ExchangeView = React.memo(function ExchangeView2({
1119
+ exchange,
1120
+ originalId,
1121
+ challengerId,
1122
+ columns
1123
+ }) {
1124
+ const sideBySide = exchange.isMirrored && Boolean(exchange.challenger) && columns >= 80;
1125
+ const panelWidth = sideBySide ? Math.floor((columns - 1) / 2) : columns;
1126
+ const scoreLabel = exchange.agreementScore !== void 0 ? ` [agreement: ${exchange.agreementScore}%]` : "";
1127
+ return /* @__PURE__ */ jsxs4(Box2, { flexDirection: "column", marginBottom: 1, children: [
1128
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1129
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "You: " }),
1130
+ /* @__PURE__ */ jsx4(Text4, { children: exchange.question })
1131
+ ] }),
1132
+ exchange.intent && /* @__PURE__ */ jsxs4(Box2, { children: [
1133
+ /* @__PURE__ */ jsx4(IntentBadge, { category: exchange.intent.category, mirrored: exchange.intent.shouldMirror }),
1134
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
1135
+ " ",
1136
+ Math.round(exchange.intent.confidence * 100),
1137
+ "%"
1138
+ ] })
1139
+ ] }),
1140
+ /* @__PURE__ */ jsxs4(Box2, { marginTop: 1, flexDirection: sideBySide ? "row" : "column", children: [
1141
+ /* @__PURE__ */ jsx4(
1142
+ BrainPanel,
1143
+ {
1144
+ title: `ORIGINAL ${originalId}`,
1145
+ width: panelWidth,
1146
+ marginRight: sideBySide ? 1 : 0,
1147
+ children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: exchange.original })
1148
+ }
1149
+ ),
1150
+ exchange.isMirrored && exchange.challenger && /* @__PURE__ */ jsx4(
1151
+ BrainPanel,
1152
+ {
1153
+ title: `CHALLENGER ${challengerId}`,
1154
+ width: panelWidth,
1155
+ children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: exchange.challenger })
1156
+ }
1157
+ )
1158
+ ] }),
1159
+ exchange.synthesis && /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(
1160
+ BrainPanel,
1161
+ {
1162
+ title: `SYNTHESIS${scoreLabel}`,
1163
+ width: columns,
1164
+ borderColor: "yellow",
1165
+ children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: stripAgreementHeader(exchange.synthesis) })
1166
+ }
1167
+ ) })
1168
+ ] });
1169
+ });
1170
+ function loadRawHeaderLines() {
1171
+ const cwd = process.cwd();
1172
+ const candidates = [
1173
+ resolve(cwd, "src", "ui", "header.txt"),
1174
+ resolve(cwd, "header.txt")
1175
+ ];
1176
+ for (const f of candidates) {
1177
+ if (!existsSync(f)) continue;
1178
+ try {
1179
+ const lines = readFileSync(f, "utf8").split(/\r?\n/);
1180
+ while (lines.length > 0 && !lines[lines.length - 1].trim()) lines.pop();
1181
+ return lines;
1182
+ } catch {
1183
+ continue;
1184
+ }
1185
+ }
1186
+ try {
1187
+ const p = fileURLToPath(new URL("./header.txt", import.meta.url));
1188
+ if (existsSync(p)) {
1189
+ const lines = readFileSync(p, "utf8").split(/\r?\n/);
1190
+ while (lines.length > 0 && !lines[lines.length - 1].trim()) lines.pop();
1191
+ return lines;
1192
+ }
1193
+ } catch {
1194
+ }
1195
+ return [];
1196
+ }
1197
+ function fitLines(lines, cols) {
1198
+ if (!lines.length || cols <= 0) return [];
1199
+ const trimmed = lines.map((l) => l.replace(/\s+$/, ""));
1200
+ const nonEmpty = trimmed.filter((l) => l.trim().length > 0);
1201
+ const indent = nonEmpty.length ? Math.min(...nonEmpty.map((l) => l.match(/^\s*/)?.[0].length ?? 0)) : 0;
1202
+ const aligned = indent > 0 ? trimmed.map((l) => l.slice(indent)) : trimmed;
1203
+ return aligned.map((l) => l.length > cols ? l.slice(0, cols) : l);
1204
+ }
1205
+ function tailLines(text, maxLines) {
1206
+ if (maxLines <= 0) return "";
1207
+ const lines = text.split(/\r?\n/);
1208
+ if (lines.length <= maxLines) return text;
1209
+ return lines.slice(-maxLines).join("\n");
1210
+ }
1211
+ function formatTokens(input3, output3) {
1212
+ if (input3 === void 0 && output3 === void 0) return null;
1213
+ return `${input3 ?? 0}/${output3 ?? 0}tok`;
1214
+ }
1215
+ function MirrorApp({
1216
+ engine,
1217
+ session,
1218
+ originalId,
1219
+ challengerId,
1220
+ judgerId,
1221
+ intensity,
1222
+ showTokenCounts = false,
1223
+ showLatency = true,
1224
+ syntaxHighlighting = true
1225
+ }) {
1226
+ const { stdout } = useStdout();
1227
+ const columns = stdout?.columns ?? 120;
1228
+ const rows = stdout?.rows ?? 40;
1229
+ const headerLines = useMemo(() => {
1230
+ const raw = loadRawHeaderLines();
1231
+ return fitLines(raw, Math.max(1, columns - 1));
1232
+ }, []);
1233
+ const [staticItems, setStaticItems] = useState([
1234
+ { type: "header", id: "header" }
1235
+ ]);
1236
+ const [input3, setInput] = useState("");
1237
+ const [activeQuestion, setActiveQuestion] = useState("");
1238
+ const [currentOriginal, setCurrentOriginal] = useState("");
1239
+ const [currentChallenger, setCurrentChallenger] = useState("");
1240
+ const [currentSynthesis, setCurrentSynthesis] = useState("");
1241
+ const [isThinking, setIsThinking] = useState(false);
1242
+ const [isClassifying, setIsClassifying] = useState(false);
1243
+ const [isSynthesizing, setIsSynthesizing] = useState(false);
1244
+ const [intent, setIntent] = useState(null);
1245
+ const [error, setError] = useState(null);
1246
+ const [originalStats, setOriginalStats] = useState(null);
1247
+ const [challengerStats, setChallengerStats] = useState(null);
1248
+ const [synthesisStats, setSynthesisStats] = useState(null);
1249
+ const [turnCount, setTurnCount] = useState(0);
1250
+ const [commitTick, setCommitTick] = useState(0);
1251
+ const runningRef = useRef(false);
1252
+ const abortRef = useRef(null);
1253
+ const pendingOrigRef = useRef("");
1254
+ const pendingChalRef = useRef("");
1255
+ const pendingSynthRef = useRef("");
1256
+ const pendingExchangeRef = useRef(null);
1257
+ const startTimesRef = useRef(/* @__PURE__ */ new Map());
1258
+ const columnsRef = useRef(columns);
1259
+ useEffect(() => {
1260
+ columnsRef.current = columns;
1261
+ }, [columns]);
1262
+ useEffect(() => {
1263
+ if (isThinking) return;
1264
+ if (!pendingExchangeRef.current) return;
1265
+ setCommitTick((tick) => tick + 1);
1266
+ }, [isThinking]);
1267
+ useEffect(() => {
1268
+ if (!pendingExchangeRef.current) return;
1269
+ const item = pendingExchangeRef.current;
1270
+ pendingExchangeRef.current = null;
1271
+ setStaticItems((prev) => [...prev, item]);
1272
+ }, [commitTick]);
1273
+ useEffect(() => {
1274
+ const id = setInterval(() => {
1275
+ setCurrentOriginal(pendingOrigRef.current);
1276
+ setCurrentChallenger(pendingChalRef.current);
1277
+ setCurrentSynthesis(pendingSynthRef.current);
1278
+ }, 60);
1279
+ return () => clearInterval(id);
1280
+ }, []);
1281
+ const showChallengerPanel = Boolean(challengerId) && (intent?.shouldMirror ?? true);
1282
+ const showSideBySide = showChallengerPanel && columns >= 80;
1283
+ const panelWidth = showSideBySide ? Math.floor((columns - 1) / 2) : columns;
1284
+ const liveLineLimit = Math.max(6, Math.min(18, rows - 10));
1285
+ const formatText = useCallback(
1286
+ (text) => syntaxHighlighting ? highlightCodeBlocks(text) : text,
1287
+ [syntaxHighlighting]
1288
+ );
1289
+ const submit = useCallback(async () => {
1290
+ if (runningRef.current) return;
1291
+ const question = input3.trim();
1292
+ if (!question) return;
1293
+ runningRef.current = true;
1294
+ setInput("");
1295
+ setError(null);
1296
+ setIntent(null);
1297
+ setActiveQuestion(question);
1298
+ setIsThinking(true);
1299
+ setIsClassifying(false);
1300
+ setIsSynthesizing(false);
1301
+ setOriginalStats(null);
1302
+ setChallengerStats(null);
1303
+ setSynthesisStats(null);
1304
+ pendingOrigRef.current = "";
1305
+ pendingChalRef.current = "";
1306
+ pendingSynthRef.current = "";
1307
+ setCurrentOriginal("");
1308
+ setCurrentChallenger("");
1309
+ setCurrentSynthesis("");
1310
+ const history2 = session.getHistory();
1311
+ session.addUser(question);
1312
+ let originalBuffer = "";
1313
+ let challengerBuffer = "";
1314
+ let synthesisBuffer = "";
1315
+ let originalResult = null;
1316
+ let challengerResult;
1317
+ let synthResult;
1318
+ let intentResult;
1319
+ let isMirrored = Boolean(challengerId);
1320
+ const entryId = randomUUID();
1321
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1322
+ const controller = new AbortController();
1323
+ abortRef.current = controller;
1324
+ startTimesRef.current = new Map([
1325
+ [originalId, Date.now()],
1326
+ ...challengerId ? [[challengerId, Date.now()]] : []
1327
+ ]);
1328
+ try {
1329
+ for await (const event of engine.run(question, history2, { signal: controller.signal })) {
1330
+ if (event.type === "classifying") {
1331
+ setIsClassifying(true);
1332
+ }
1333
+ if (event.type === "classified") {
1334
+ setIsClassifying(false);
1335
+ setIntent(event.result);
1336
+ intentResult = event.result;
1337
+ isMirrored = event.result.shouldMirror && Boolean(challengerId);
1338
+ }
1339
+ if (event.type === "stream_chunk") {
1340
+ if (event.brainId === originalId) {
1341
+ originalBuffer += event.chunk.delta;
1342
+ pendingOrigRef.current = originalBuffer;
1343
+ } else if (event.brainId === challengerId) {
1344
+ challengerBuffer += event.chunk.delta;
1345
+ pendingChalRef.current = challengerBuffer;
1346
+ }
1347
+ }
1348
+ if (event.type === "brain_complete") {
1349
+ const latency = Date.now() - (startTimesRef.current.get(event.brainId) ?? Date.now());
1350
+ if (event.brainId === originalId) {
1351
+ const text = event.response.text || originalBuffer;
1352
+ originalResult = {
1353
+ brainId: originalId,
1354
+ text,
1355
+ inputTokens: event.response.inputTokens,
1356
+ outputTokens: event.response.outputTokens,
1357
+ latencyMs: latency
1358
+ };
1359
+ setOriginalStats(originalResult);
1360
+ session.addAssistant(text);
1361
+ } else if (event.brainId === challengerId) {
1362
+ const text = event.response.text || challengerBuffer;
1363
+ challengerResult = {
1364
+ brainId: challengerId,
1365
+ text,
1366
+ inputTokens: event.response.inputTokens,
1367
+ outputTokens: event.response.outputTokens,
1368
+ latencyMs: latency
1369
+ };
1370
+ setChallengerStats(challengerResult);
1371
+ }
1372
+ }
1373
+ if (event.type === "synthesizing") {
1374
+ setIsSynthesizing(true);
1375
+ }
1376
+ if (event.type === "synthesis_chunk") {
1377
+ synthesisBuffer += event.chunk.delta;
1378
+ pendingSynthRef.current = synthesisBuffer;
1379
+ }
1380
+ if (event.type === "synthesis_complete") {
1381
+ setIsSynthesizing(false);
1382
+ synthResult = event.result;
1383
+ setSynthesisStats(event.result);
1384
+ }
1385
+ if (event.type === "all_complete" && originalResult) {
1386
+ addHistoryEntry({
1387
+ id: entryId,
1388
+ createdAt,
1389
+ question,
1390
+ original: originalResult,
1391
+ challenger: challengerResult,
1392
+ intent: intentResult
1393
+ });
1394
+ const exchange = {
1395
+ id: entryId,
1396
+ question,
1397
+ intent: intentResult,
1398
+ original: formatText(originalResult.text),
1399
+ challenger: challengerResult ? formatText(challengerResult.text) : void 0,
1400
+ synthesis: synthResult ? formatText(synthResult.text) : void 0,
1401
+ agreementScore: synthResult?.agreementScore,
1402
+ isMirrored
1403
+ };
1404
+ pendingExchangeRef.current = {
1405
+ type: "exchange",
1406
+ id: entryId,
1407
+ exchange,
1408
+ originalId,
1409
+ challengerId,
1410
+ columns: columnsRef.current
1411
+ };
1412
+ setTurnCount((prev) => prev + 1);
1413
+ setActiveQuestion("");
1414
+ pendingOrigRef.current = "";
1415
+ pendingChalRef.current = "";
1416
+ pendingSynthRef.current = "";
1417
+ setCurrentOriginal("");
1418
+ setCurrentChallenger("");
1419
+ setCurrentSynthesis("");
1420
+ setIsSynthesizing(false);
1421
+ setIsClassifying(false);
1422
+ }
1423
+ if (event.type === "error") {
1424
+ setError(event.error.message);
1425
+ }
1426
+ }
1427
+ } catch (err) {
1428
+ if (err.name !== "AbortError") {
1429
+ setError(err.message ?? "Unknown error");
1430
+ }
1431
+ } finally {
1432
+ setIsThinking(false);
1433
+ setIsClassifying(false);
1434
+ setIsSynthesizing(false);
1435
+ runningRef.current = false;
1436
+ abortRef.current = null;
1437
+ }
1438
+ }, [challengerId, engine, formatText, input3, originalId, session]);
1439
+ useInput((ch, key) => {
1440
+ if (key.ctrl && ch === "c") {
1441
+ if (isThinking && abortRef.current) {
1442
+ abortRef.current.abort();
1443
+ return;
1444
+ }
1445
+ process.exit(0);
1446
+ }
1447
+ if (key.return) {
1448
+ void submit();
1449
+ return;
1450
+ }
1451
+ if (key.backspace || key.delete) {
1452
+ setInput((p) => p.slice(0, -1));
1453
+ return;
1454
+ }
1455
+ if (ch && !key.ctrl && !key.meta) setInput((p) => p + ch);
1456
+ });
1457
+ const statusParts = [];
1458
+ if (isClassifying) statusParts.push("Classifying...");
1459
+ else if (isSynthesizing) statusParts.push("Synthesizing...");
1460
+ else if (isThinking) statusParts.push("Thinking...");
1461
+ else statusParts.push("Ready");
1462
+ if (showTokenCounts) {
1463
+ const origT = formatTokens(originalStats?.inputTokens, originalStats?.outputTokens);
1464
+ if (origT) statusParts.push(`orig ${origT}`);
1465
+ if (challengerStats) {
1466
+ const chalT = formatTokens(challengerStats.inputTokens, challengerStats.outputTokens);
1467
+ if (chalT) statusParts.push(`chal ${chalT}`);
1468
+ }
1469
+ if (synthesisStats) {
1470
+ const synthT = formatTokens(synthesisStats.inputTokens, synthesisStats.outputTokens);
1471
+ if (synthT) statusParts.push(`synth ${synthT}`);
1472
+ }
1473
+ }
1474
+ if (showLatency && originalStats?.latencyMs != null) {
1475
+ statusParts.push(`orig ${(originalStats.latencyMs / 1e3).toFixed(1)}s`);
1476
+ }
1477
+ if (showLatency && challengerStats?.latencyMs != null) {
1478
+ statusParts.push(`chal ${(challengerStats.latencyMs / 1e3).toFixed(1)}s`);
1479
+ }
1480
+ if (synthesisStats?.agreementScore !== void 0) {
1481
+ statusParts.push(`agreement ${synthesisStats.agreementScore}%`);
1482
+ }
1483
+ statusParts.push(`${turnCount} turn${turnCount !== 1 ? "s" : ""}`);
1484
+ statusParts.push("Ctrl+C to exit");
1485
+ const synthScoreLabel = synthesisStats?.agreementScore !== void 0 ? ` [agreement: ${synthesisStats.agreementScore}%]` : "";
1486
+ return /* @__PURE__ */ jsxs4(Box2, { flexDirection: "column", children: [
1487
+ /* @__PURE__ */ jsx4(Static, { items: staticItems, children: (item) => /* @__PURE__ */ jsx4(React.Fragment, { children: item.type === "header" ? /* @__PURE__ */ jsx4(
1488
+ HeaderView,
1489
+ {
1490
+ lines: headerLines,
1491
+ originalId,
1492
+ challengerId,
1493
+ intensity
1494
+ }
1495
+ ) : /* @__PURE__ */ jsx4(
1496
+ ExchangeView,
1497
+ {
1498
+ exchange: item.exchange,
1499
+ originalId: item.originalId,
1500
+ challengerId: item.challengerId,
1501
+ columns: item.columns
1502
+ }
1503
+ ) }, item.id) }),
1504
+ isThinking && activeQuestion && /* @__PURE__ */ jsxs4(Box2, { flexDirection: "column", children: [
1505
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
1506
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "You: " }),
1507
+ /* @__PURE__ */ jsx4(Text4, { children: activeQuestion })
1508
+ ] }),
1509
+ intent ? /* @__PURE__ */ jsxs4(Box2, { children: [
1510
+ /* @__PURE__ */ jsx4(IntentBadge, { category: intent.category, mirrored: intent.shouldMirror }),
1511
+ /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
1512
+ " ",
1513
+ Math.round(intent.confidence * 100),
1514
+ "%"
1515
+ ] })
1516
+ ] }) : isClassifying ? /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "Classifying..." }) : null,
1517
+ /* @__PURE__ */ jsxs4(Box2, { marginTop: 1, flexDirection: showSideBySide ? "row" : "column", children: [
1518
+ /* @__PURE__ */ jsx4(
1519
+ BrainPanel,
1520
+ {
1521
+ title: `ORIGINAL ${originalId}`,
1522
+ width: panelWidth,
1523
+ marginRight: showSideBySide && showChallengerPanel ? 1 : 0,
1524
+ children: /* @__PURE__ */ jsx4(StreamingText, { value: tailLines(currentOriginal, liveLineLimit) })
1525
+ }
1526
+ ),
1527
+ showChallengerPanel && /* @__PURE__ */ jsx4(
1528
+ BrainPanel,
1529
+ {
1530
+ title: `CHALLENGER ${challengerId} [${intensity}]`,
1531
+ width: panelWidth,
1532
+ children: /* @__PURE__ */ jsx4(StreamingText, { value: tailLines(currentChallenger, liveLineLimit) })
1533
+ }
1534
+ )
1535
+ ] }),
1536
+ (isSynthesizing || currentSynthesis) && /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(
1537
+ BrainPanel,
1538
+ {
1539
+ title: `SYNTHESIS${isSynthesizing ? " synthesizing..." : synthScoreLabel} ${judgerId ?? ""}`,
1540
+ width: columns,
1541
+ borderColor: "yellow",
1542
+ children: /* @__PURE__ */ jsx4(StreamingText, { value: tailLines(stripAgreementHeader(currentSynthesis), liveLineLimit) })
1543
+ }
1544
+ ) })
1545
+ ] }),
1546
+ error && /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
1547
+ "Error: ",
1548
+ error
1549
+ ] }) }),
1550
+ /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: statusParts.join(" \xB7 ") }) }),
1551
+ /* @__PURE__ */ jsxs4(Box2, { children: [
1552
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "> " }),
1553
+ /* @__PURE__ */ jsx4(Text4, { children: input3 }),
1554
+ /* @__PURE__ */ jsx4(Text4, { color: isThinking ? "gray" : "cyan", children: "\u2588" })
1555
+ ] })
1556
+ ] });
1557
+ }
1558
+
1559
+ // src/cli/commands/chat.ts
1560
+ function runChat(_localOpts, command) {
1561
+ const parentOpts = command.parent?.opts() ?? {};
1562
+ const localOpts = command.opts();
1563
+ const opts = { ...parentOpts, ...localOpts };
1564
+ try {
1565
+ const config2 = loadConfig();
1566
+ const originalId = opts["original"] ?? config2.session.originalBrainId;
1567
+ const challengerId = opts["challenger"] ?? config2.session.challengerBrainId;
1568
+ const mirrorEnabled = opts["mirror"] !== false;
1569
+ const classifyEnabled = opts["classify"] !== false;
1570
+ const intensity = opts["intensity"] ?? config2.session.defaultIntensity;
1571
+ const judgeEnabled = opts["judge"] !== false && config2.session.judgeEnabled;
1572
+ const persona = opts["persona"] ?? config2.session.defaultPersona;
1573
+ const filePath = opts["file"];
1574
+ const brainConfig = config2.brains.find((b) => b.id === originalId);
1575
+ if (!brainConfig) {
1576
+ throw new Error(`Original brain not found: ${originalId}`);
1577
+ }
1578
+ const originalAdapter = createAdapter(brainConfig);
1579
+ const challengerConfig = config2.brains.find((b) => b.id === challengerId);
1580
+ const challengerAdapter = mirrorEnabled && challengerConfig ? createAdapter(challengerConfig) : void 0;
1581
+ let judgeAdapter = void 0;
1582
+ if (mirrorEnabled && challengerAdapter && judgeEnabled) {
1583
+ const judgeId = opts["judgeBrain"] ?? config2.session.judgeBrainId;
1584
+ const judgeConfig = config2.brains.find((b) => b.id === judgeId);
1585
+ if (judgeConfig) {
1586
+ judgeAdapter = createAdapter(judgeConfig);
1587
+ }
1588
+ }
1589
+ const session = new Session(config2.session.historyWindowSize);
1590
+ if (filePath) {
1591
+ try {
1592
+ const content = readFileSync2(filePath, "utf8");
1593
+ const name = basename(filePath);
1594
+ session.addUser(`[FILE: ${name}]
1595
+ ${content}`);
1596
+ session.addAssistant(`I have read the file "${name}". Ask me anything about it.`);
1597
+ } catch (err) {
1598
+ throw new Error(`Could not read file: ${filePath} \u2014 ${err.message}`);
1599
+ }
1600
+ }
1601
+ const classifier = buildIntentClassifier(config2, Boolean(opts["debug"]));
1602
+ const engine = new MirrorEngine({
1603
+ original: originalAdapter,
1604
+ challenger: challengerAdapter,
1605
+ intensity,
1606
+ autoClassify: mirrorEnabled && classifyEnabled && config2.session.autoClassify,
1607
+ classifier,
1608
+ debug: Boolean(opts["debug"]),
1609
+ judge: judgeAdapter,
1610
+ persona
1611
+ });
1612
+ const app = render(
1613
+ React2.createElement(MirrorApp, {
1614
+ engine,
1615
+ session,
1616
+ originalId: originalAdapter.id,
1617
+ challengerId: challengerAdapter?.id,
1618
+ judgerId: judgeAdapter?.id,
1619
+ intensity,
1620
+ layout: config2.ui.layout,
1621
+ showTokenCounts: config2.ui.showTokenCounts,
1622
+ showLatency: config2.ui.showLatency,
1623
+ syntaxHighlighting: config2.ui.syntaxHighlighting
1624
+ })
1625
+ );
1626
+ void app.waitUntilExit();
1627
+ } catch (error) {
1628
+ process.stderr.write(`Failed to start chat: ${error.message}
1629
+ `);
1630
+ process.exit(1);
1631
+ }
1632
+ }
1633
+
1634
+ // src/cli/commands/mirror.ts
1635
+ import { randomUUID as randomUUID2 } from "crypto";
1636
+ import { readFileSync as readFileSync3 } from "fs";
1637
+ import { basename as basename2 } from "path";
1638
+ function readStdin() {
1639
+ return new Promise((resolve2, reject) => {
1640
+ const chunks = [];
1641
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
1642
+ process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
1643
+ process.stdin.on("error", reject);
1644
+ });
1645
+ }
1646
+ async function runMirror(question, _localOpts, command) {
1647
+ const parentOpts = command.parent?.opts() ?? {};
1648
+ const localOpts = command.opts();
1649
+ const opts = { ...parentOpts, ...localOpts };
1650
+ try {
1651
+ const config2 = loadConfig();
1652
+ const originalId = opts["original"] ?? config2.session.originalBrainId;
1653
+ const challengerId = opts["challenger"] ?? config2.session.challengerBrainId;
1654
+ const intensity = opts["intensity"] ?? config2.session.defaultIntensity;
1655
+ const mirrorEnabled = opts["mirror"] !== false;
1656
+ const classifyEnabled = opts["classify"] !== false;
1657
+ const judgeEnabled = opts["judge"] !== false && config2.session.judgeEnabled;
1658
+ const persona = opts["persona"] ?? config2.session.defaultPersona;
1659
+ const filePath = opts["file"];
1660
+ let filePrefix = "";
1661
+ if (filePath) {
1662
+ try {
1663
+ const content = readFileSync3(filePath, "utf8");
1664
+ const name = basename2(filePath);
1665
+ filePrefix = `[FILE: ${name}]
1666
+ ${content}
1667
+
1668
+ ---
1669
+ `;
1670
+ } catch (err) {
1671
+ throw new Error(`Could not read file: ${filePath} \u2014 ${err.message}`);
1672
+ }
1673
+ } else if (!process.stdin.isTTY) {
1674
+ const content = await readStdin();
1675
+ if (content.trim()) {
1676
+ filePrefix = `[STDIN]
1677
+ ${content}
1678
+
1679
+ ---
1680
+ `;
1681
+ }
1682
+ }
1683
+ const fullQuestion = filePrefix ? `${filePrefix}${question}` : question;
1684
+ const originalConfig = config2.brains.find((b) => b.id === originalId);
1685
+ if (!originalConfig) throw new Error(`Original brain not found: ${originalId}`);
1686
+ const originalAdapter = createAdapter(originalConfig);
1687
+ const challengerConfig = config2.brains.find((b) => b.id === challengerId);
1688
+ const challengerAdapter = mirrorEnabled && challengerConfig ? createAdapter(challengerConfig) : void 0;
1689
+ let judgeAdapter = void 0;
1690
+ if (mirrorEnabled && challengerAdapter && judgeEnabled) {
1691
+ const judgeId = opts["judgeBrain"] ?? config2.session.judgeBrainId;
1692
+ const judgeConfig = config2.brains.find((b) => b.id === judgeId);
1693
+ if (judgeConfig) {
1694
+ judgeAdapter = createAdapter(judgeConfig);
1695
+ }
1696
+ }
1697
+ const classifier = buildIntentClassifier(config2, Boolean(opts["debug"]));
1698
+ const engine = new MirrorEngine({
1699
+ original: originalAdapter,
1700
+ challenger: challengerAdapter,
1701
+ intensity,
1702
+ autoClassify: mirrorEnabled && classifyEnabled,
1703
+ classifier,
1704
+ debug: Boolean(opts["debug"]),
1705
+ judge: judgeAdapter,
1706
+ persona
1707
+ });
1708
+ const session = new Session(1);
1709
+ const results = /* @__PURE__ */ new Map();
1710
+ let intentResult;
1711
+ const entryId = randomUUID2();
1712
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1713
+ const startTimes = new Map([
1714
+ [originalAdapter.id, Date.now()],
1715
+ ...challengerAdapter ? [[challengerAdapter.id, Date.now()]] : []
1716
+ ]);
1717
+ let originalHeaderPrinted = false;
1718
+ let synthBuffer = "";
1719
+ let synthScore;
1720
+ for await (const event of engine.run(fullQuestion, session.getHistory())) {
1721
+ if (event.type === "classifying") {
1722
+ process.stdout.write("Classifying...\n");
1723
+ }
1724
+ if (event.type === "classified") {
1725
+ const label = event.result.shouldMirror ? "MIRRORING" : "DIRECT";
1726
+ process.stdout.write(
1727
+ `[${label}] ${event.result.category} (${Math.round(event.result.confidence * 100)}%)
1728
+ `
1729
+ );
1730
+ intentResult = event.result;
1731
+ }
1732
+ if (event.type === "stream_chunk" && !event.chunk.isFinal) {
1733
+ if (event.brainId === originalAdapter.id) {
1734
+ if (!originalHeaderPrinted) {
1735
+ process.stdout.write(`
1736
+ ORIGINAL (${originalAdapter.id})
1737
+ `);
1738
+ originalHeaderPrinted = true;
1739
+ }
1740
+ process.stdout.write(event.chunk.delta);
1741
+ }
1742
+ }
1743
+ if (event.type === "brain_complete") {
1744
+ const latency = Date.now() - (startTimes.get(event.brainId) ?? Date.now());
1745
+ results.set(event.brainId, {
1746
+ brainId: event.brainId,
1747
+ text: event.response.text,
1748
+ inputTokens: event.response.inputTokens,
1749
+ outputTokens: event.response.outputTokens,
1750
+ latencyMs: latency
1751
+ });
1752
+ }
1753
+ if (event.type === "synthesizing") {
1754
+ if (originalHeaderPrinted) process.stdout.write("\n");
1755
+ if (challengerAdapter) {
1756
+ const challengerResult = results.get(challengerAdapter.id);
1757
+ process.stdout.write(`
1758
+ CHALLENGER (${challengerAdapter.id})
1759
+ `);
1760
+ process.stdout.write(`${challengerResult?.text ?? ""}
1761
+ `);
1762
+ }
1763
+ process.stdout.write("\nSYNTHESIS (judge)\n");
1764
+ }
1765
+ if (event.type === "synthesis_chunk" && !event.chunk.isFinal) {
1766
+ synthBuffer += event.chunk.delta;
1767
+ process.stdout.write(event.chunk.delta);
1768
+ }
1769
+ if (event.type === "synthesis_complete") {
1770
+ synthScore = event.result.agreementScore;
1771
+ process.stdout.write("\n");
1772
+ if (synthScore !== void 0) {
1773
+ process.stdout.write(`
1774
+ Agreement score: ${synthScore}%
1775
+ `);
1776
+ }
1777
+ }
1778
+ if (event.type === "all_complete") {
1779
+ if (!judgeAdapter) {
1780
+ if (originalHeaderPrinted) process.stdout.write("\n");
1781
+ if (challengerAdapter) {
1782
+ const challengerResult = results.get(challengerAdapter.id);
1783
+ process.stdout.write(`
1784
+ CHALLENGER (${challengerAdapter.id})
1785
+ `);
1786
+ process.stdout.write(`${challengerResult?.text ?? ""}
1787
+ `);
1788
+ }
1789
+ }
1790
+ }
1791
+ if (event.type === "error") throw event.error;
1792
+ }
1793
+ const originalResult = results.get(originalAdapter.id);
1794
+ if (originalResult) {
1795
+ addHistoryEntry({
1796
+ id: entryId,
1797
+ createdAt,
1798
+ question: fullQuestion,
1799
+ original: originalResult,
1800
+ challenger: challengerAdapter ? results.get(challengerAdapter.id) : void 0,
1801
+ intent: intentResult
1802
+ });
1803
+ }
1804
+ } catch (error) {
1805
+ process.stderr.write(`Failed to run mirror: ${error.message}
1806
+ `);
1807
+ process.exit(1);
1808
+ }
1809
+ }
1810
+
1811
+ // src/cli/commands/config.ts
1812
+ import { execFile } from "child_process";
1813
+ import { createInterface } from "readline/promises";
1814
+ import { stdin as input, stdout as output } from "process";
1815
+ import { promisify } from "util";
1816
+ var execFileAsync = promisify(execFile);
1817
+ function runConfigShow() {
1818
+ const config2 = loadConfig();
1819
+ process.stdout.write(JSON.stringify(config2, null, 2));
1820
+ process.stdout.write("\n");
1821
+ }
1822
+ async function runConfigInit() {
1823
+ const rl = createInterface({ input, output });
1824
+ try {
1825
+ const config2 = loadConfig();
1826
+ if (config2.brains.length === 0) {
1827
+ throw new Error("No brains configured. Run mirror brains add first.");
1828
+ }
1829
+ const intensity = await askRequired(
1830
+ rl,
1831
+ `Default intensity (mild|moderate|aggressive) [${config2.session.defaultIntensity}]: `,
1832
+ config2.session.defaultIntensity
1833
+ );
1834
+ if (!["mild", "moderate", "aggressive"].includes(intensity)) {
1835
+ throw new Error(`Invalid intensity: ${intensity}`);
1836
+ }
1837
+ const layout = await askRequired(
1838
+ rl,
1839
+ `Layout (side-by-side|stacked) [${config2.ui.layout}]: `,
1840
+ config2.ui.layout
1841
+ );
1842
+ if (!["side-by-side", "stacked"].includes(layout)) {
1843
+ throw new Error(`Invalid layout: ${layout}`);
1844
+ }
1845
+ const showTokenCounts = await askYesNo(
1846
+ rl,
1847
+ `Show token counts? (y/n) [${config2.ui.showTokenCounts ? "y" : "n"}]: `,
1848
+ config2.ui.showTokenCounts
1849
+ );
1850
+ const showLatency = await askYesNo(
1851
+ rl,
1852
+ `Show latency? (y/n) [${config2.ui.showLatency ? "y" : "n"}]: `,
1853
+ config2.ui.showLatency
1854
+ );
1855
+ const syntaxHighlighting = await askYesNo(
1856
+ rl,
1857
+ `Syntax highlighting? (y/n) [${config2.ui.syntaxHighlighting ? "y" : "n"}]: `,
1858
+ config2.ui.syntaxHighlighting
1859
+ );
1860
+ const autoClassify = await askYesNo(
1861
+ rl,
1862
+ `Auto-classify intent? (y/n) [${config2.session.autoClassify ? "y" : "n"}]: `,
1863
+ config2.session.autoClassify
1864
+ );
1865
+ const historyWindowSize = Number(
1866
+ await askRequired(
1867
+ rl,
1868
+ `History window size [${config2.session.historyWindowSize}]: `,
1869
+ String(config2.session.historyWindowSize)
1870
+ )
1871
+ );
1872
+ if (!Number.isInteger(historyWindowSize) || historyWindowSize <= 0) {
1873
+ throw new Error("History window size must be a positive integer.");
1874
+ }
1875
+ const availableBrains = config2.brains.map((brain) => brain.id).join(", ");
1876
+ const originalBrainId = await askRequired(
1877
+ rl,
1878
+ `Original brain id (${availableBrains}) [${config2.session.originalBrainId}]: `,
1879
+ config2.session.originalBrainId
1880
+ );
1881
+ const challengerBrainId = await askRequired(
1882
+ rl,
1883
+ `Challenger brain id (${availableBrains}) [${config2.session.challengerBrainId}]: `,
1884
+ config2.session.challengerBrainId
1885
+ );
1886
+ if (!config2.brains.some((brain) => brain.id === originalBrainId)) {
1887
+ throw new Error(`Unknown original brain id: ${originalBrainId}`);
1888
+ }
1889
+ if (!config2.brains.some((brain) => brain.id === challengerBrainId)) {
1890
+ throw new Error(`Unknown challenger brain id: ${challengerBrainId}`);
1891
+ }
1892
+ const judgeEnabled = await askYesNo(
1893
+ rl,
1894
+ `Enable judge synthesis pass? (y/n) [${config2.session.judgeEnabled ? "y" : "n"}]: `,
1895
+ config2.session.judgeEnabled
1896
+ );
1897
+ let judgeBrainId = config2.session.judgeBrainId;
1898
+ if (judgeEnabled) {
1899
+ judgeBrainId = await askRequired(
1900
+ rl,
1901
+ `Judge brain id (${availableBrains}) [${config2.session.judgeBrainId}]: `,
1902
+ config2.session.judgeBrainId
1903
+ );
1904
+ if (!config2.brains.some((brain) => brain.id === judgeBrainId)) {
1905
+ throw new Error(`Unknown judge brain id: ${judgeBrainId}`);
1906
+ }
1907
+ }
1908
+ const personaNames = "vc-skeptic|security-auditor|end-user|regulator|contrarian";
1909
+ const currentPersona = config2.session.defaultPersona ?? "none";
1910
+ const personaAnswer = await askOptional(
1911
+ rl,
1912
+ `Default persona (${personaNames}|none) [${currentPersona}]: `,
1913
+ currentPersona
1914
+ );
1915
+ const defaultPersona = personaAnswer === "none" || !personaAnswer ? void 0 : personaAnswer;
1916
+ const updatedKeys = await promptForApiKeys(rl, config2);
1917
+ if (Object.keys(updatedKeys).length > 0) {
1918
+ const persist = await askYesNo(
1919
+ rl,
1920
+ "Persist API keys to environment variables? (y/n) [y]: ",
1921
+ true
1922
+ );
1923
+ if (persist) {
1924
+ await persistEnvVars(updatedKeys);
1925
+ process.stdout.write(
1926
+ "Keys saved. Open a new terminal session to pick them up.\n"
1927
+ );
1928
+ }
1929
+ }
1930
+ await validateGeminiModels(rl, config2, updatedKeys);
1931
+ saveConfig({
1932
+ ...config2,
1933
+ session: {
1934
+ ...config2.session,
1935
+ originalBrainId,
1936
+ challengerBrainId,
1937
+ defaultIntensity: intensity,
1938
+ historyWindowSize,
1939
+ autoClassify,
1940
+ judgeEnabled,
1941
+ judgeBrainId,
1942
+ defaultPersona
1943
+ },
1944
+ ui: {
1945
+ ...config2.ui,
1946
+ layout,
1947
+ showTokenCounts,
1948
+ showLatency,
1949
+ syntaxHighlighting
1950
+ }
1951
+ });
1952
+ process.stdout.write("Config saved.\n");
1953
+ } catch (error) {
1954
+ process.stderr.write(
1955
+ `Failed to initialize config: ${error.message}
1956
+ `
1957
+ );
1958
+ process.exit(1);
1959
+ } finally {
1960
+ rl.close();
1961
+ }
1962
+ }
1963
+ function runConfigSet(key, value) {
1964
+ try {
1965
+ const parsed = parseValue(value);
1966
+ const updated = setConfigValue(key, parsed);
1967
+ process.stdout.write(JSON.stringify(updated, null, 2));
1968
+ process.stdout.write("\n");
1969
+ } catch (error) {
1970
+ process.stderr.write(`Failed to set config: ${error.message}
1971
+ `);
1972
+ process.exit(1);
1973
+ }
1974
+ }
1975
+ function parseValue(value) {
1976
+ const trimmed = value.trim();
1977
+ if (!trimmed) {
1978
+ return value;
1979
+ }
1980
+ if (trimmed === "true") return true;
1981
+ if (trimmed === "false") return false;
1982
+ if (!Number.isNaN(Number(trimmed))) return Number(trimmed);
1983
+ try {
1984
+ return JSON.parse(trimmed);
1985
+ } catch {
1986
+ return value;
1987
+ }
1988
+ }
1989
+ async function askRequired(rl, prompt, fallback) {
1990
+ const answer = (await rl.question(prompt)).trim();
1991
+ if (answer) {
1992
+ return answer;
1993
+ }
1994
+ if (fallback) {
1995
+ return fallback;
1996
+ }
1997
+ return askRequired(rl, prompt, fallback);
1998
+ }
1999
+ async function askOptional(rl, prompt, fallback) {
2000
+ const answer = (await rl.question(prompt)).trim();
2001
+ return answer || fallback;
2002
+ }
2003
+ async function askYesNo(rl, prompt, fallback) {
2004
+ const answer = (await rl.question(prompt)).trim().toLowerCase();
2005
+ if (!answer) {
2006
+ return fallback;
2007
+ }
2008
+ if (["y", "yes"].includes(answer)) {
2009
+ return true;
2010
+ }
2011
+ if (["n", "no"].includes(answer)) {
2012
+ return false;
2013
+ }
2014
+ return askYesNo(rl, prompt, fallback);
2015
+ }
2016
+ async function promptForApiKeys(rl, config2) {
2017
+ const updated = {};
2018
+ const uniqueEnvVars = Array.from(
2019
+ new Set(config2.brains.map((brain) => brain.apiKeyEnvVar))
2020
+ );
2021
+ process.stdout.write("\nAPI key setup (stored in environment variables):\n");
2022
+ for (const envVar of uniqueEnvVars) {
2023
+ const alreadySet = Boolean(process.env[envVar]);
2024
+ const shouldSet = await askYesNo(
2025
+ rl,
2026
+ `${envVar} ${alreadySet ? "(already set)" : "(missing)"} - set now? (y/n) [${alreadySet ? "n" : "y"}]: `,
2027
+ !alreadySet
2028
+ );
2029
+ if (!shouldSet) {
2030
+ continue;
2031
+ }
2032
+ const secret = await askSecret(rl, `Enter value for ${envVar}: `);
2033
+ if (!secret) {
2034
+ continue;
2035
+ }
2036
+ process.env[envVar] = secret;
2037
+ updated[envVar] = secret;
2038
+ }
2039
+ return updated;
2040
+ }
2041
+ async function askSecret(rl, prompt) {
2042
+ if (!input.isTTY) {
2043
+ return askRequired(rl, prompt);
2044
+ }
2045
+ output.write(prompt);
2046
+ rl.pause();
2047
+ input.setRawMode(true);
2048
+ input.resume();
2049
+ let value = "";
2050
+ return new Promise((resolve2) => {
2051
+ const onData = (chunk) => {
2052
+ const char = chunk.toString();
2053
+ if (char === "\r" || char === "\n") {
2054
+ input.setRawMode(false);
2055
+ input.pause();
2056
+ input.removeListener("data", onData);
2057
+ output.write("\n");
2058
+ rl.resume();
2059
+ resolve2(value);
2060
+ return;
2061
+ }
2062
+ if (char === "") {
2063
+ process.exit(1);
2064
+ }
2065
+ if (char === "\b" || char === "\x7F") {
2066
+ value = value.slice(0, -1);
2067
+ return;
2068
+ }
2069
+ value += char;
2070
+ };
2071
+ input.on("data", onData);
2072
+ });
2073
+ }
2074
+ async function persistEnvVars(vars) {
2075
+ if (process.platform === "win32") {
2076
+ for (const [key, value] of Object.entries(vars)) {
2077
+ await execFileAsync("setx", [key, value]);
2078
+ }
2079
+ return;
2080
+ }
2081
+ process.stdout.write(
2082
+ "Non-Windows detected. Please export your keys in your shell profile.\n"
2083
+ );
2084
+ }
2085
+ async function validateGeminiModels(rl, config2, updatedKeys) {
2086
+ const geminiBrains = config2.brains.filter((brain) => brain.provider === "gemini");
2087
+ if (geminiBrains.length === 0) {
2088
+ return;
2089
+ }
2090
+ const geminiEnvVar = geminiBrains[0].apiKeyEnvVar;
2091
+ const apiKey = updatedKeys[geminiEnvVar] ?? process.env[geminiEnvVar];
2092
+ if (!apiKey) {
2093
+ return;
2094
+ }
2095
+ const shouldCheck = await askYesNo(
2096
+ rl,
2097
+ "Check Gemini model availability now? (y/n) [y]: ",
2098
+ true
2099
+ );
2100
+ if (!shouldCheck) {
2101
+ return;
2102
+ }
2103
+ try {
2104
+ const models = await listGeminiModels(apiKey);
2105
+ const supported = models.filter(
2106
+ (model) => model.supportedGenerationMethods.some(
2107
+ (method) => ["generateContent", "streamGenerateContent"].includes(method)
2108
+ )
2109
+ );
2110
+ for (const brain of geminiBrains) {
2111
+ const exists = supported.some((model) => model.name.endsWith(`/${brain.model}`));
2112
+ if (exists) {
2113
+ continue;
2114
+ }
2115
+ process.stdout.write(
2116
+ `Gemini model not found for ${brain.id}: ${brain.model}
2117
+ `
2118
+ );
2119
+ const suggestion = supported.find(
2120
+ (model) => model.name.endsWith("/gemini-2.5-pro")
2121
+ );
2122
+ const recommended = suggestion?.name.split("/").pop() ?? "gemini-2.5-pro";
2123
+ const nextModel = await askRequired(
2124
+ rl,
2125
+ `Enter a supported Gemini model [${recommended}]: `,
2126
+ recommended
2127
+ );
2128
+ brain.model = nextModel;
2129
+ }
2130
+ } catch (error) {
2131
+ process.stderr.write(
2132
+ `Failed to validate Gemini models: ${error.message}
2133
+ `
2134
+ );
2135
+ }
2136
+ }
2137
+ async function listGeminiModels(apiKey) {
2138
+ const res = await fetch(
2139
+ `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
2140
+ );
2141
+ if (!res.ok) {
2142
+ throw new Error(`Gemini ListModels failed: ${res.status}`);
2143
+ }
2144
+ const data = await res.json();
2145
+ return data.models ?? [];
2146
+ }
2147
+
2148
+ // src/cli/commands/brains.ts
2149
+ import { createInterface as createInterface2 } from "readline/promises";
2150
+ import { stdin as input2, stdout as output2 } from "process";
2151
+ function runBrainsList() {
2152
+ const config2 = loadConfig();
2153
+ const lines = config2.brains.map(
2154
+ (brain) => `${brain.id} ${brain.provider} ${brain.model} ${brain.apiKeyEnvVar}`
2155
+ );
2156
+ if (lines.length === 0) {
2157
+ process.stdout.write("No brains configured.\n");
2158
+ return;
2159
+ }
2160
+ process.stdout.write("ID PROVIDER MODEL API_KEY_ENV\n");
2161
+ process.stdout.write(`${lines.join("\n")}
2162
+ `);
2163
+ }
2164
+ async function runBrainsTest(id) {
2165
+ try {
2166
+ const config2 = loadConfig();
2167
+ const brain = config2.brains.find((entry) => entry.id === id);
2168
+ if (!brain) {
2169
+ throw new Error(`Brain not found: ${id}`);
2170
+ }
2171
+ const adapter = createAdapter(brain);
2172
+ const result = await adapter.ping();
2173
+ if (!result.ok) {
2174
+ throw new Error(result.error ?? "Ping failed");
2175
+ }
2176
+ process.stdout.write(
2177
+ `Brain ${id} ok${result.latencyMs ? ` (${result.latencyMs}ms)` : ""}
2178
+ `
2179
+ );
2180
+ } catch (error) {
2181
+ process.stderr.write(`Brain test failed: ${error.message}
2182
+ `);
2183
+ process.exit(1);
2184
+ }
2185
+ }
2186
+ async function runBrainsAdd() {
2187
+ const rl = createInterface2({ input: input2, output: output2 });
2188
+ try {
2189
+ const config2 = loadConfig();
2190
+ const id = await askRequired2(rl, "Brain id (unique): ");
2191
+ if (config2.brains.some((brain) => brain.id === id)) {
2192
+ throw new Error(`Brain id already exists: ${id}`);
2193
+ }
2194
+ const provider = await askRequired2(
2195
+ rl,
2196
+ "Provider (anthropic|openai|gemini|mock): "
2197
+ );
2198
+ if (!["anthropic", "openai", "gemini", "mock"].includes(provider)) {
2199
+ throw new Error(`Unsupported provider: ${provider}`);
2200
+ }
2201
+ const model = await askRequired2(rl, "Model name: ");
2202
+ const suggestedEnv = defaultEnvVar(provider);
2203
+ const apiKeyEnvVar = await askRequired2(
2204
+ rl,
2205
+ `API key env var (${suggestedEnv}): `,
2206
+ suggestedEnv
2207
+ );
2208
+ const next = {
2209
+ id,
2210
+ provider,
2211
+ model,
2212
+ apiKeyEnvVar
2213
+ };
2214
+ saveConfig({
2215
+ ...config2,
2216
+ brains: [...config2.brains, next]
2217
+ });
2218
+ process.stdout.write(`Added brain ${id}.
2219
+ `);
2220
+ } catch (error) {
2221
+ process.stderr.write(`Failed to add brain: ${error.message}
2222
+ `);
2223
+ process.exit(1);
2224
+ } finally {
2225
+ rl.close();
2226
+ }
2227
+ }
2228
+ async function askRequired2(rl, prompt, fallback) {
2229
+ const answer = (await rl.question(prompt)).trim();
2230
+ if (answer) {
2231
+ return answer;
2232
+ }
2233
+ if (fallback) {
2234
+ return fallback;
2235
+ }
2236
+ return askRequired2(rl, prompt, fallback);
2237
+ }
2238
+ function defaultEnvVar(provider) {
2239
+ switch (provider) {
2240
+ case "anthropic":
2241
+ return "ANTHROPIC_API_KEY";
2242
+ case "openai":
2243
+ return "OPENAI_API_KEY";
2244
+ case "gemini":
2245
+ return "GOOGLE_API_KEY";
2246
+ case "mock":
2247
+ return "MOCK_API_KEY";
2248
+ default:
2249
+ return "API_KEY";
2250
+ }
2251
+ }
2252
+
2253
+ // src/cli/commands/history.ts
2254
+ import { writeFile } from "fs/promises";
2255
+ function runHistoryList() {
2256
+ const entries = listHistory();
2257
+ if (entries.length === 0) {
2258
+ process.stdout.write("No history yet.\n");
2259
+ return;
2260
+ }
2261
+ process.stdout.write("ID CREATED QUESTION\n");
2262
+ for (const entry of entries) {
2263
+ const question = entry.question.length > 80 ? `${entry.question.slice(0, 77)}...` : entry.question;
2264
+ process.stdout.write(`${entry.id} ${entry.createdAt} ${question}
2265
+ `);
2266
+ }
2267
+ }
2268
+ function runHistoryShow(id) {
2269
+ const entry = getHistory(id);
2270
+ if (!entry) {
2271
+ process.stderr.write(`History entry not found: ${id}
2272
+ `);
2273
+ process.exit(1);
2274
+ }
2275
+ process.stdout.write(JSON.stringify(entry, null, 2));
2276
+ process.stdout.write("\n");
2277
+ }
2278
+ async function runHistoryExport(id, file) {
2279
+ const entry = getHistory(id);
2280
+ if (!entry) {
2281
+ process.stderr.write(`History entry not found: ${id}
2282
+ `);
2283
+ process.exit(1);
2284
+ }
2285
+ const payload = JSON.stringify(entry, null, 2);
2286
+ await writeFile(file, payload);
2287
+ process.stdout.write(`Exported history ${id} to ${file}.
2288
+ `);
2289
+ }
2290
+
2291
+ // src/cli/index.ts
2292
+ var program = new Command();
2293
+ program.name("mirror").description("Adversarial Mirror CLI").version("0.1.0").option("--intensity <level>", "mild|moderate|aggressive").option("--original <brainId>", "override original brain").option("--challenger <brainId>", "override challenger brain").option("--no-mirror", "disable mirroring").option("--no-classify", "disable intent classification").option("--no-judge", "disable judge synthesis pass").option("--judge-brain <brainId>", "override judge brain id").option("--persona <name>", "persona lens: vc-skeptic|security-auditor|end-user|regulator|contrarian").option("--debug", "enable debug logging");
2294
+ program.command("chat").description("Interactive session").option("--file <path>", "load file as context before the session starts").action(runChat);
2295
+ program.command("mirror <question>").description("One-shot query").option("--file <path>", "load file as context (or pipe via stdin)").action(runMirror);
2296
+ var config = program.command("config").description("Config commands");
2297
+ config.action(runConfigShow);
2298
+ config.command("show").description("Show current config").action(runConfigShow);
2299
+ config.command("init").description("Interactive setup wizard").action(runConfigInit);
2300
+ config.command("set <key> <value>").description("Set config value by key path").action(runConfigSet);
2301
+ var brains = program.command("brains").description("Brain management commands");
2302
+ brains.action(runBrainsList);
2303
+ brains.command("list").description("List configured brains").action(runBrainsList);
2304
+ brains.command("test <id>").description("Test a brain").action(runBrainsTest);
2305
+ brains.command("add").description("Add a new brain").action(runBrainsAdd);
2306
+ var history = program.command("history").description("History commands");
2307
+ history.action(runHistoryList);
2308
+ history.command("list").description("List history").action(runHistoryList);
2309
+ history.command("show <id>").description("Show history entry").action(runHistoryShow);
2310
+ history.command("export <id> <file>").description("Export history entry to a file").action(runHistoryExport);
2311
+ var rawArgs = process.argv.slice(2);
2312
+ var knownSubcommands = /* @__PURE__ */ new Set(["chat", "mirror", "config", "brains", "history"]);
2313
+ var hasVersionOrHelp = rawArgs.some((a) => ["-V", "--version", "-h", "--help"].includes(a));
2314
+ var hasSubcommand = rawArgs.some((a) => knownSubcommands.has(a));
2315
+ if (!hasVersionOrHelp && !hasSubcommand) {
2316
+ process.argv.push("chat");
2317
+ }
2318
+ program.parse();
2319
+ //# sourceMappingURL=cli.js.map