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/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/cli.js +2319 -0
- package/dist/cli.js.map +1 -0
- package/dist/header.txt +10 -0
- package/package.json +79 -0
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
|