tagteam 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1326 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+
7
+ // src/config.ts
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import { parse, stringify } from "smol-toml";
12
+ var DEFAULT_CONFIG = {
13
+ claude: {
14
+ model: "sonnet"
15
+ },
16
+ codex: {
17
+ model: "gpt-5.3-codex"
18
+ },
19
+ discussion: {
20
+ max_rounds: 10
21
+ }
22
+ };
23
+ function getConfigDir() {
24
+ return join(homedir(), ".tagteam");
25
+ }
26
+ function getConfigPath() {
27
+ return join(getConfigDir(), "config.toml");
28
+ }
29
+ function ensureConfigDir() {
30
+ const dir = getConfigDir();
31
+ if (!existsSync(dir)) {
32
+ mkdirSync(dir, { recursive: true });
33
+ }
34
+ }
35
+ function loadConfig() {
36
+ const configPath = getConfigPath();
37
+ if (!existsSync(configPath)) {
38
+ return { ...DEFAULT_CONFIG };
39
+ }
40
+ try {
41
+ const raw = readFileSync(configPath, "utf-8");
42
+ const parsed = parse(raw);
43
+ return {
44
+ claude: { ...DEFAULT_CONFIG.claude, ...parsed.claude },
45
+ codex: { ...DEFAULT_CONFIG.codex, ...parsed.codex },
46
+ discussion: { ...DEFAULT_CONFIG.discussion, ...parsed.discussion }
47
+ };
48
+ } catch {
49
+ return { ...DEFAULT_CONFIG };
50
+ }
51
+ }
52
+ function saveConfig(config) {
53
+ ensureConfigDir();
54
+ const configPath = getConfigPath();
55
+ writeFileSync(configPath, stringify(config), "utf-8");
56
+ }
57
+ function setConfigValue(key, value) {
58
+ const config = loadConfig();
59
+ const parts = key.split(".");
60
+ if (parts.length === 1) {
61
+ switch (parts[0]) {
62
+ case "claude_model":
63
+ config.claude.model = value;
64
+ break;
65
+ case "codex_model":
66
+ config.codex.model = value;
67
+ break;
68
+ case "discussion_max_rounds":
69
+ config.discussion.max_rounds = Number(value);
70
+ break;
71
+ default:
72
+ throw new Error(`Unknown config key: ${key}`);
73
+ }
74
+ } else if (parts.length === 2) {
75
+ const [section, field] = parts;
76
+ if (section === "claude" && field === "model") {
77
+ config.claude.model = value;
78
+ } else if (section === "codex" && field === "model") {
79
+ config.codex.model = value;
80
+ } else if (section === "discussion" && field === "max_rounds") {
81
+ config.discussion.max_rounds = Number(value);
82
+ } else {
83
+ throw new Error(`Unknown config key: ${key}`);
84
+ }
85
+ } else {
86
+ throw new Error(`Invalid config key format: ${key}`);
87
+ }
88
+ saveConfig(config);
89
+ return config;
90
+ }
91
+
92
+ // src/ui.tsx
93
+ import { useState as useState2, useEffect, useCallback, useRef } from "react";
94
+ import { render as render2, Box as Box2, Text as Text2, useApp as useApp2, useInput as useInput2 } from "ink";
95
+ import TextInput2 from "ink-text-input";
96
+ import Spinner from "ink-spinner";
97
+ import { marked } from "marked";
98
+ import { markedTerminal } from "marked-terminal";
99
+ import { nanoid } from "nanoid";
100
+
101
+ // src/agents/claude.ts
102
+ import { spawn } from "child_process";
103
+ import { createInterface } from "readline";
104
+ async function* streamClaude(options) {
105
+ const args = [
106
+ "-p",
107
+ "--verbose",
108
+ "--output-format",
109
+ "stream-json",
110
+ "--no-session-persistence"
111
+ ];
112
+ if (options.systemPrompt) {
113
+ args.push("--system-prompt", options.systemPrompt);
114
+ }
115
+ if (options.model) {
116
+ args.push("--model", options.model);
117
+ }
118
+ args.push(options.prompt);
119
+ const proc = spawn("claude", args, {
120
+ cwd: options.cwd,
121
+ stdio: ["ignore", "pipe", "inherit"],
122
+ env: { ...process.env, CLAUDECODE: "" }
123
+ });
124
+ let spawnErrorMsg = "";
125
+ proc.on("error", (err) => {
126
+ spawnErrorMsg = err.message;
127
+ });
128
+ if (options.signal) {
129
+ if (options.signal.aborted) {
130
+ proc.kill();
131
+ } else {
132
+ options.signal.addEventListener("abort", () => proc.kill(), { once: true });
133
+ }
134
+ }
135
+ const rl = createInterface({ input: proc.stdout });
136
+ let gotResultText = false;
137
+ for await (const line of rl) {
138
+ if (!line.trim()) continue;
139
+ let data;
140
+ try {
141
+ data = JSON.parse(line);
142
+ } catch {
143
+ continue;
144
+ }
145
+ if (data.type === "assistant" && data.message?.content) {
146
+ for (const block of data.message.content) {
147
+ if (block.type === "text" && block.text) {
148
+ yield { type: "text", agent: "claude", content: block.text };
149
+ gotResultText = true;
150
+ } else if (block.type === "tool_use") {
151
+ yield {
152
+ type: "tool_call",
153
+ agent: "claude",
154
+ toolName: block.name,
155
+ toolInput: typeof block.input === "string" ? block.input : JSON.stringify(block.input)
156
+ };
157
+ }
158
+ }
159
+ } else if (data.type === "user" && data.message?.content) {
160
+ for (const block of data.message.content) {
161
+ if (block.type === "tool_result") {
162
+ const text = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.filter((c) => c.type === "text").map((c) => c.text).join("\n") : "";
163
+ if (text) {
164
+ yield {
165
+ type: "tool_result",
166
+ agent: "claude",
167
+ toolOutput: text
168
+ };
169
+ }
170
+ }
171
+ }
172
+ } else if (data.type === "result") {
173
+ if (data.is_error) {
174
+ yield {
175
+ type: "error",
176
+ agent: "claude",
177
+ content: data.error || data.result || "Unknown error"
178
+ };
179
+ } else if (data.result && !gotResultText) {
180
+ yield { type: "text", agent: "claude", content: data.result };
181
+ }
182
+ }
183
+ }
184
+ await new Promise((resolve) => {
185
+ proc.on("close", resolve);
186
+ });
187
+ if (spawnErrorMsg) {
188
+ yield {
189
+ type: "error",
190
+ agent: "claude",
191
+ content: `Failed to spawn claude: ${spawnErrorMsg}`
192
+ };
193
+ }
194
+ yield { type: "done", agent: "claude" };
195
+ }
196
+ async function runClaude(options) {
197
+ const start = Date.now();
198
+ const events = [];
199
+ const textParts = [];
200
+ const errors = [];
201
+ for await (const event of streamClaude(options)) {
202
+ events.push(event);
203
+ if (event.type === "text" && event.content) {
204
+ textParts.push(event.content);
205
+ } else if (event.type === "error" && event.content) {
206
+ errors.push(event.content);
207
+ }
208
+ }
209
+ return {
210
+ agent: "claude",
211
+ text: textParts.join(""),
212
+ events,
213
+ durationMs: Date.now() - start,
214
+ error: errors.length > 0 ? errors.join("\n") : void 0
215
+ };
216
+ }
217
+
218
+ // src/agents/codex.ts
219
+ import { spawn as spawn2 } from "child_process";
220
+ import { createInterface as createInterface2 } from "readline";
221
+ async function* streamCodex(options) {
222
+ const args = ["exec", "--json", "--full-auto", "--ephemeral"];
223
+ if (options.model) {
224
+ args.push("-m", options.model);
225
+ }
226
+ let fullPrompt = options.prompt;
227
+ if (options.systemPrompt) {
228
+ fullPrompt = `${options.systemPrompt}
229
+
230
+ ---
231
+
232
+ ${options.prompt}`;
233
+ }
234
+ args.push(fullPrompt);
235
+ const proc = spawn2("codex", args, {
236
+ cwd: options.cwd,
237
+ stdio: ["ignore", "pipe", "inherit"],
238
+ env: process.env
239
+ });
240
+ let spawnErrorMsg = "";
241
+ proc.on("error", (err) => {
242
+ spawnErrorMsg = err.message;
243
+ });
244
+ if (options.signal) {
245
+ if (options.signal.aborted) {
246
+ proc.kill();
247
+ } else {
248
+ options.signal.addEventListener("abort", () => proc.kill(), { once: true });
249
+ }
250
+ }
251
+ const rl = createInterface2({ input: proc.stdout });
252
+ for await (const line of rl) {
253
+ if (!line.trim()) continue;
254
+ let data;
255
+ try {
256
+ data = JSON.parse(line);
257
+ } catch {
258
+ continue;
259
+ }
260
+ if (data.type === "item.completed" && data.item) {
261
+ const item = data.item;
262
+ if (item.type === "agent_message" && item.text) {
263
+ yield { type: "text", agent: "codex", content: item.text };
264
+ } else if (item.type === "command_execution") {
265
+ yield {
266
+ type: "tool_call",
267
+ agent: "codex",
268
+ toolName: "command",
269
+ toolInput: item.command
270
+ };
271
+ if (item.aggregated_output) {
272
+ yield {
273
+ type: "tool_result",
274
+ agent: "codex",
275
+ toolOutput: item.aggregated_output
276
+ };
277
+ }
278
+ } else if (item.type === "file_change" && item.changes) {
279
+ const desc = item.changes.map((c) => `${c.kind}: ${c.path}`).join(", ");
280
+ yield {
281
+ type: "tool_result",
282
+ agent: "codex",
283
+ toolName: "file_change",
284
+ toolOutput: desc
285
+ };
286
+ } else if (item.type === "error") {
287
+ yield {
288
+ type: "error",
289
+ agent: "codex",
290
+ content: item.message || "Unknown error"
291
+ };
292
+ }
293
+ } else if (data.type === "turn.failed") {
294
+ yield {
295
+ type: "error",
296
+ agent: "codex",
297
+ content: data.error?.message || "Turn failed"
298
+ };
299
+ } else if (data.type === "error") {
300
+ yield {
301
+ type: "error",
302
+ agent: "codex",
303
+ content: data.message || "Stream error"
304
+ };
305
+ }
306
+ }
307
+ await new Promise((resolve) => {
308
+ proc.on("close", resolve);
309
+ });
310
+ if (spawnErrorMsg) {
311
+ yield {
312
+ type: "error",
313
+ agent: "codex",
314
+ content: `Failed to spawn codex: ${spawnErrorMsg}`
315
+ };
316
+ }
317
+ yield { type: "done", agent: "codex" };
318
+ }
319
+ async function runCodex(options) {
320
+ const start = Date.now();
321
+ const events = [];
322
+ const textParts = [];
323
+ const errors = [];
324
+ for await (const event of streamCodex(options)) {
325
+ events.push(event);
326
+ if (event.type === "text" && event.content) {
327
+ textParts.push(event.content);
328
+ } else if (event.type === "error" && event.content) {
329
+ errors.push(event.content);
330
+ }
331
+ }
332
+ return {
333
+ agent: "codex",
334
+ text: textParts.join(""),
335
+ events,
336
+ durationMs: Date.now() - start,
337
+ error: errors.length > 0 ? errors.join("\n") : void 0
338
+ };
339
+ }
340
+
341
+ // src/format.ts
342
+ function formatAsMarkdown(messages) {
343
+ const parts = [];
344
+ for (const msg of messages) {
345
+ if (msg.role === "system") continue;
346
+ if (msg.role === "user") {
347
+ parts.push(`**You:** ${msg.content}`);
348
+ } else {
349
+ const name = msg.role === "claude" ? "Claude" : "Codex";
350
+ if (msg.error) {
351
+ parts.push(`**${name}:** *(error)*
352
+
353
+ ${msg.content}`);
354
+ } else {
355
+ parts.push(`**${name}:**
356
+
357
+ ${msg.content}`);
358
+ }
359
+ }
360
+ }
361
+ return parts.join("\n\n---\n\n") + "\n";
362
+ }
363
+
364
+ // src/clipboard.ts
365
+ import { execSync } from "child_process";
366
+ function copyToClipboard(text) {
367
+ const platform = process.platform;
368
+ let cmd;
369
+ if (platform === "darwin") {
370
+ cmd = "pbcopy";
371
+ } else if (platform === "win32") {
372
+ cmd = "clip";
373
+ } else {
374
+ try {
375
+ execSync("which xclip", { stdio: "ignore" });
376
+ cmd = "xclip -selection clipboard";
377
+ } catch {
378
+ cmd = "xsel --clipboard --input";
379
+ }
380
+ }
381
+ execSync(cmd, { input: text, stdio: ["pipe", "ignore", "ignore"] });
382
+ }
383
+
384
+ // src/db/index.ts
385
+ import Database from "better-sqlite3";
386
+ import { join as join2 } from "path";
387
+ var db = null;
388
+ function getDbPath() {
389
+ return join2(getConfigDir(), "tagteam.db");
390
+ }
391
+ function getDb() {
392
+ if (db) return db;
393
+ ensureConfigDir();
394
+ db = new Database(getDbPath());
395
+ db.pragma("journal_mode = WAL");
396
+ db.pragma("foreign_keys = ON");
397
+ initSchema(db);
398
+ return db;
399
+ }
400
+ function initSchema(db2) {
401
+ db2.exec(`
402
+ CREATE TABLE IF NOT EXISTS sessions (
403
+ id TEXT PRIMARY KEY,
404
+ title TEXT,
405
+ working_dir TEXT NOT NULL,
406
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
407
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
408
+ status TEXT NOT NULL DEFAULT 'active'
409
+ );
410
+
411
+ CREATE TABLE IF NOT EXISTS messages (
412
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
413
+ session_id TEXT NOT NULL REFERENCES sessions(id),
414
+ role TEXT NOT NULL,
415
+ content TEXT NOT NULL,
416
+ round INTEGER NOT NULL DEFAULT 0,
417
+ duration_ms INTEGER,
418
+ metadata TEXT,
419
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
420
+ );
421
+
422
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, id);
423
+ `);
424
+ }
425
+ function closeDb() {
426
+ if (db) {
427
+ db.close();
428
+ db = null;
429
+ }
430
+ }
431
+
432
+ // src/db/sessions.ts
433
+ function createSession(id, workingDir) {
434
+ const db2 = getDb();
435
+ db2.prepare(
436
+ `INSERT INTO sessions (id, working_dir) VALUES (?, ?)`
437
+ ).run(id, workingDir);
438
+ return getSession(id);
439
+ }
440
+ function getSession(id) {
441
+ const db2 = getDb();
442
+ return db2.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id);
443
+ }
444
+ function getSessionByPrefix(prefix) {
445
+ const db2 = getDb();
446
+ return db2.prepare(`SELECT * FROM sessions WHERE id LIKE ? ORDER BY updated_at DESC LIMIT 1`).get(`${prefix}%`);
447
+ }
448
+ function getMostRecentSession() {
449
+ const db2 = getDb();
450
+ return db2.prepare(
451
+ `SELECT * FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1`
452
+ ).get();
453
+ }
454
+ function listSessions(limit = 20) {
455
+ const db2 = getDb();
456
+ return db2.prepare(`SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(limit);
457
+ }
458
+ function updateSessionTitle(id, title) {
459
+ const db2 = getDb();
460
+ db2.prepare(
461
+ `UPDATE sessions SET title = ?, updated_at = datetime('now') WHERE id = ?`
462
+ ).run(title, id);
463
+ }
464
+ function touchSession(id) {
465
+ const db2 = getDb();
466
+ db2.prepare(`UPDATE sessions SET updated_at = datetime('now') WHERE id = ?`).run(
467
+ id
468
+ );
469
+ }
470
+
471
+ // src/db/messages.ts
472
+ function insertMessage(params) {
473
+ const db2 = getDb();
474
+ const result = db2.prepare(
475
+ `INSERT INTO messages (session_id, role, content, round, duration_ms, metadata)
476
+ VALUES (?, ?, ?, ?, ?, ?)`
477
+ ).run(
478
+ params.sessionId,
479
+ params.role,
480
+ params.content,
481
+ params.round,
482
+ params.durationMs ?? null,
483
+ params.metadata ? JSON.stringify(params.metadata) : null
484
+ );
485
+ return db2.prepare(`SELECT * FROM messages WHERE id = ?`).get(result.lastInsertRowid);
486
+ }
487
+ function getMessages(sessionId) {
488
+ const db2 = getDb();
489
+ return db2.prepare(`SELECT * FROM messages WHERE session_id = ? ORDER BY id`).all(sessionId);
490
+ }
491
+ function deleteMessagesFromRound(sessionId, fromRound) {
492
+ const db2 = getDb();
493
+ db2.prepare(
494
+ `DELETE FROM messages WHERE session_id = ? AND round >= ?`
495
+ ).run(sessionId, fromRound);
496
+ }
497
+
498
+ // src/prompts.ts
499
+ var OTHER = {
500
+ claude: "Codex (OpenAI)",
501
+ codex: "Claude (Anthropic)"
502
+ };
503
+ function collaborationPrompt(agent) {
504
+ return `You are in a collaborative session with ${OTHER[agent]}. You'll both respond to the user's prompt independently, then see each other's responses. In discussion rounds: highlight where you agree, constructively address disagreements, and build on each other's ideas. Be concise - avoid repeating what was already said.`;
505
+ }
506
+ function discussionPrompt(agent, conversationHistory) {
507
+ return `${collaborationPrompt(agent)}
508
+
509
+ Here is the conversation so far:
510
+
511
+ ${conversationHistory}
512
+
513
+ Now provide your response for this discussion round. Build on what was said, highlight agreements, and address any disagreements constructively. Be concise.`;
514
+ }
515
+ function debatePrompt(agent) {
516
+ return `You are in a structured debate with ${OTHER[agent]}. You'll both respond to the user's prompt, then see each other's responses and discuss.
517
+
518
+ Your goal is to reach consensus through constructive discussion. In each round:
519
+ - Address specific points of agreement and disagreement
520
+ - Refine your position based on valid arguments from ${OTHER[agent]}
521
+ - Be concise \u2014 don't repeat points already established
522
+
523
+ When you believe you and ${OTHER[agent]} have reached substantial agreement on the key points, end your response with [CONSENSUS] on its own line. Only do this when you genuinely agree \u2014 don't force premature consensus.`;
524
+ }
525
+ function debateRoundPrompt(agent, conversationHistory) {
526
+ return `${debatePrompt(agent)}
527
+
528
+ Here is the conversation so far:
529
+
530
+ ${conversationHistory}
531
+
532
+ Respond to the latest round. If you agree with ${OTHER[agent]}'s position on all key points, end with [CONSENSUS]. Otherwise, continue the discussion.`;
533
+ }
534
+ var CONSENSUS_MARKER = "[CONSENSUS]";
535
+ function formatConversationHistory(messages) {
536
+ return messages.map((m) => {
537
+ const label = m.role === "user" ? "User" : m.agent === "claude" ? "Claude" : "Codex";
538
+ return `[${label}]: ${m.content}`;
539
+ }).join("\n\n");
540
+ }
541
+
542
+ // src/config-editor.tsx
543
+ import { useState } from "react";
544
+ import { render, Box, Text, useApp, useInput } from "ink";
545
+ import TextInput from "ink-text-input";
546
+ import { jsx, jsxs } from "react/jsx-runtime";
547
+ var CONFIG_FIELDS = [
548
+ {
549
+ key: "claude.model",
550
+ label: "Claude model",
551
+ get: (c) => c.claude.model,
552
+ type: "string"
553
+ },
554
+ {
555
+ key: "codex.model",
556
+ label: "Codex model",
557
+ get: (c) => c.codex.model,
558
+ type: "string"
559
+ },
560
+ {
561
+ key: "discussion.max_rounds",
562
+ label: "Discussion max rounds",
563
+ get: (c) => String(c.discussion.max_rounds),
564
+ type: "number"
565
+ }
566
+ ];
567
+ var LABEL_WIDTH = Math.max(...CONFIG_FIELDS.map((f) => f.label.length));
568
+ function InlineConfigEditor({ isActive, onClose }) {
569
+ const [config, setConfig] = useState(() => loadConfig());
570
+ const [selectedIndex, setSelectedIndex] = useState(0);
571
+ const [mode, setMode] = useState("select");
572
+ const [editValue, setEditValue] = useState("");
573
+ const [savedMessage, setSavedMessage] = useState(null);
574
+ useInput(
575
+ (input, key) => {
576
+ if (key.upArrow) {
577
+ setSavedMessage(null);
578
+ setSelectedIndex((i) => i > 0 ? i - 1 : CONFIG_FIELDS.length - 1);
579
+ } else if (key.downArrow) {
580
+ setSavedMessage(null);
581
+ setSelectedIndex((i) => i < CONFIG_FIELDS.length - 1 ? i + 1 : 0);
582
+ } else if (key.return) {
583
+ const field = CONFIG_FIELDS[selectedIndex];
584
+ setEditValue(field.get(config));
585
+ setSavedMessage(null);
586
+ setMode("edit");
587
+ } else if (key.escape || input === "q") {
588
+ onClose();
589
+ }
590
+ },
591
+ { isActive: isActive && mode === "select" }
592
+ );
593
+ useInput(
594
+ (_input, key) => {
595
+ if (key.escape) {
596
+ setMode("select");
597
+ }
598
+ },
599
+ { isActive: isActive && mode === "edit" }
600
+ );
601
+ const handleEditSubmit = (value) => {
602
+ const field = CONFIG_FIELDS[selectedIndex];
603
+ if (field.type === "number") {
604
+ const n = Number(value);
605
+ if (!Number.isInteger(n) || n < 1) {
606
+ setSavedMessage("Error: must be a positive integer");
607
+ setMode("select");
608
+ return;
609
+ }
610
+ }
611
+ try {
612
+ const updated = setConfigValue(field.key, value);
613
+ setConfig(updated);
614
+ setSavedMessage(`Saved ${field.key} = ${value}`);
615
+ } catch (e) {
616
+ setSavedMessage(`Error: ${e.message}`);
617
+ }
618
+ setMode("select");
619
+ };
620
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
621
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
622
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Configuration" }),
623
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2500\u2500 edit values inline" })
624
+ ] }),
625
+ CONFIG_FIELDS.map((field, i) => {
626
+ const selected = i === selectedIndex;
627
+ const pointer = selected ? "> " : " ";
628
+ const currentValue = field.get(config);
629
+ return /* @__PURE__ */ jsxs(Box, { marginLeft: 1, children: [
630
+ /* @__PURE__ */ jsxs(Text, { color: selected ? "cyan" : void 0, bold: selected, children: [
631
+ pointer,
632
+ field.label.padEnd(LABEL_WIDTH)
633
+ ] }),
634
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " = " }),
635
+ mode === "edit" && selected ? /* @__PURE__ */ jsx(
636
+ TextInput,
637
+ {
638
+ value: editValue,
639
+ onChange: setEditValue,
640
+ onSubmit: handleEditSubmit,
641
+ showCursor: true
642
+ }
643
+ ) : /* @__PURE__ */ jsx(Text, { color: "white", children: currentValue })
644
+ ] }, field.key);
645
+ }),
646
+ savedMessage && /* @__PURE__ */ jsx(Box, { marginTop: 1, marginLeft: 1, children: /* @__PURE__ */ jsx(Text, { color: savedMessage.startsWith("Error") ? "red" : "green", children: savedMessage }) }),
647
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, marginLeft: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: mode === "edit" ? "Enter save Esc cancel" : "\u2191\u2193 navigate Enter edit Esc/q quit" }) })
648
+ ] });
649
+ }
650
+ function StandaloneConfigEditor() {
651
+ const { exit } = useApp();
652
+ return /* @__PURE__ */ jsx(InlineConfigEditor, { isActive: true, onClose: exit });
653
+ }
654
+ function startConfigEditor() {
655
+ return render(/* @__PURE__ */ jsx(StandaloneConfigEditor, {}));
656
+ }
657
+
658
+ // src/ui.tsx
659
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
660
+ marked.use(markedTerminal());
661
+ function parseInput(input) {
662
+ const lower = input.toLowerCase();
663
+ if (lower.startsWith("discuss ")) {
664
+ return { target: "both", prompt: input.slice(8).trim(), discuss: true };
665
+ }
666
+ if (lower.startsWith("claude ") || lower.startsWith("claude, ")) {
667
+ return { target: "claude", prompt: input.slice(input.indexOf(" ") + 1).trim(), discuss: false };
668
+ }
669
+ if (lower.startsWith("codex ") || lower.startsWith("codex, ")) {
670
+ return { target: "codex", prompt: input.slice(input.indexOf(" ") + 1).trim(), discuss: false };
671
+ }
672
+ return { target: "both", prompt: input, discuss: false };
673
+ }
674
+ function RenderedMarkdown({ text }) {
675
+ const rendered = marked.parse(text).trimEnd();
676
+ return /* @__PURE__ */ jsx2(Text2, { children: rendered });
677
+ }
678
+ function AgentResponseBlock({
679
+ agent,
680
+ content,
681
+ error
682
+ }) {
683
+ const color = agent === "claude" ? "magenta" : "green";
684
+ if (error) {
685
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
686
+ /* @__PURE__ */ jsxs2(Text2, { color: "red", bold: true, children: [
687
+ agent === "claude" ? "Claude" : "Codex",
688
+ " error:"
689
+ ] }),
690
+ /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "red", children: content }) })
691
+ ] });
692
+ }
693
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
694
+ /* @__PURE__ */ jsxs2(Text2, { color, bold: true, children: [
695
+ agent === "claude" ? "Claude" : "Codex",
696
+ ":"
697
+ ] }),
698
+ /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(RenderedMarkdown, { text: content }) })
699
+ ] });
700
+ }
701
+ function Header({ sessionId }) {
702
+ return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
703
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500\u2500 " }),
704
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Tag Team" }),
705
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \u2500\u2500 session " }),
706
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: sessionId.slice(0, 7) }),
707
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " + "\u2500".repeat(35) })
708
+ ] });
709
+ }
710
+ function UserMessage({ content }) {
711
+ return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, marginBottom: 1, children: [
712
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "white", children: [
713
+ "You:",
714
+ " "
715
+ ] }),
716
+ /* @__PURE__ */ jsx2(Text2, { children: content })
717
+ ] });
718
+ }
719
+ function ThinkingIndicator({ agent }) {
720
+ const color = agent === "claude" ? "magenta" : "green";
721
+ const label = agent === "claude" ? "Claude" : "Codex";
722
+ return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
723
+ /* @__PURE__ */ jsx2(Text2, { color, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
724
+ /* @__PURE__ */ jsxs2(Text2, { color, children: [
725
+ " ",
726
+ label,
727
+ " is thinking..."
728
+ ] })
729
+ ] });
730
+ }
731
+ function DiscussionStatus({ round, maxRounds }) {
732
+ return /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
733
+ "Discussion round ",
734
+ round,
735
+ "/",
736
+ maxRounds
737
+ ] }) });
738
+ }
739
+ function ConsensusReached() {
740
+ return /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "green", bold: true, children: "Consensus reached." }) });
741
+ }
742
+ function PromptInput({
743
+ onSubmit
744
+ }) {
745
+ const [value, setValue] = useState2("");
746
+ const handleSubmit = useCallback(
747
+ (submitted) => {
748
+ if (submitted.trim()) {
749
+ onSubmit(submitted.trim());
750
+ setValue("");
751
+ }
752
+ },
753
+ [onSubmit]
754
+ );
755
+ return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
756
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "white", children: "> " }),
757
+ /* @__PURE__ */ jsx2(
758
+ TextInput2,
759
+ {
760
+ value,
761
+ onChange: setValue,
762
+ onSubmit: handleSubmit,
763
+ showCursor: true
764
+ }
765
+ )
766
+ ] });
767
+ }
768
+ function App({
769
+ initialPrompt,
770
+ sessionId: existingSessionId,
771
+ claudeModel,
772
+ codexModel,
773
+ config,
774
+ showTranscript: showTranscript2,
775
+ discuss: initialDiscuss
776
+ }) {
777
+ const { exit } = useApp2();
778
+ const [sessionId, setSessionId] = useState2(() => existingSessionId ?? nanoid(12));
779
+ const [messages, setMessages] = useState2(showTranscript2 ?? []);
780
+ const [state, setState] = useState2(
781
+ initialPrompt ? "running" : "input"
782
+ );
783
+ const [thinkingAgents, setThinkingAgents] = useState2([]);
784
+ const [discussionRound, setDiscussionRound] = useState2(0);
785
+ const [consensusReached, setConsensusReached] = useState2(false);
786
+ const [statusMessage, setStatusMessage] = useState2(null);
787
+ const [roundNum, setRoundNum] = useState2(() => {
788
+ if (showTranscript2 && showTranscript2.length > 0) {
789
+ return Math.max(...showTranscript2.map((m) => m.round)) + 1;
790
+ }
791
+ return 0;
792
+ });
793
+ useEffect(() => {
794
+ if (!existingSessionId) {
795
+ createSession(sessionId, process.cwd());
796
+ }
797
+ }, []);
798
+ useEffect(() => {
799
+ if (initialPrompt && state === "running") {
800
+ if (initialDiscuss) {
801
+ runDiscussion(initialPrompt);
802
+ } else {
803
+ runRound(initialPrompt);
804
+ }
805
+ }
806
+ }, []);
807
+ const abortRef = useRef(null);
808
+ const runningRoundRef = useRef(null);
809
+ useInput2((input, key) => {
810
+ if (key.ctrl && input === "c") {
811
+ abortRef.current?.abort();
812
+ closeDb();
813
+ exit();
814
+ }
815
+ if (key.escape && state === "running") {
816
+ abortRef.current?.abort();
817
+ abortRef.current = null;
818
+ if (runningRoundRef.current !== null) {
819
+ deleteMessagesFromRound(sessionId, runningRoundRef.current);
820
+ const fromRound = runningRoundRef.current;
821
+ setMessages((prev) => prev.filter((m) => m.round < fromRound));
822
+ runningRoundRef.current = null;
823
+ }
824
+ setThinkingAgents([]);
825
+ setDiscussionRound(0);
826
+ setStatusMessage("Interrupted.");
827
+ setState("input");
828
+ }
829
+ });
830
+ const runAgents = async (currentMessages, round, target, promptOverride, isDebate = false) => {
831
+ const runCl = target === "both" || target === "claude";
832
+ const runCx = target === "both" || target === "codex";
833
+ const activeAgents = [];
834
+ if (runCl) activeAgents.push("claude");
835
+ if (runCx) activeAgents.push("codex");
836
+ setThinkingAgents(activeAgents);
837
+ const history = formatConversationHistory(
838
+ currentMessages.map((m) => ({
839
+ role: m.role,
840
+ agent: m.role === "claude" || m.role === "codex" ? m.role : void 0,
841
+ content: m.content
842
+ }))
843
+ );
844
+ const cwd = process.cwd();
845
+ const isFirstRound = round === 0 && !existingSessionId;
846
+ const agentPrompt = (agent) => {
847
+ if (promptOverride) return promptOverride;
848
+ if (isFirstRound && target === "both") {
849
+ return currentMessages[currentMessages.length - 1]?.content || "";
850
+ }
851
+ return "Provide your response for this round.";
852
+ };
853
+ const agentSystemPrompt = (agent) => {
854
+ if (isDebate) {
855
+ if (isFirstRound) return debatePrompt(agent);
856
+ return debateRoundPrompt(agent, history);
857
+ }
858
+ if (isFirstRound && target === "both") return collaborationPrompt(agent);
859
+ return discussionPrompt(agent, history);
860
+ };
861
+ const ac = new AbortController();
862
+ abortRef.current = ac;
863
+ const results = [];
864
+ const promises = [];
865
+ if (runCl) {
866
+ promises.push(
867
+ runClaude({
868
+ prompt: agentPrompt("claude"),
869
+ systemPrompt: agentSystemPrompt("claude"),
870
+ model: claudeModel,
871
+ cwd,
872
+ signal: ac.signal
873
+ }).then(
874
+ (value) => {
875
+ results.push({ agent: "claude", result: { status: "fulfilled", value } });
876
+ },
877
+ (reason) => {
878
+ results.push({ agent: "claude", result: { status: "rejected", reason } });
879
+ }
880
+ )
881
+ );
882
+ }
883
+ if (runCx) {
884
+ promises.push(
885
+ runCodex({
886
+ prompt: agentPrompt("codex"),
887
+ systemPrompt: agentSystemPrompt("codex"),
888
+ model: codexModel,
889
+ cwd,
890
+ signal: ac.signal
891
+ }).then(
892
+ (value) => {
893
+ results.push({ agent: "codex", result: { status: "fulfilled", value } });
894
+ },
895
+ (reason) => {
896
+ results.push({ agent: "codex", result: { status: "rejected", reason } });
897
+ }
898
+ )
899
+ );
900
+ }
901
+ await Promise.all(promises);
902
+ abortRef.current = null;
903
+ if (ac.signal.aborted) return [];
904
+ setThinkingAgents([]);
905
+ const newMessages = [];
906
+ for (const { agent, result } of results) {
907
+ if (result.status === "fulfilled") {
908
+ const resp = result.value;
909
+ const msg = {
910
+ role: agent,
911
+ content: resp.error || resp.text,
912
+ round,
913
+ error: !!resp.error
914
+ };
915
+ newMessages.push(msg);
916
+ insertMessage({
917
+ sessionId,
918
+ role: agent,
919
+ content: resp.error || resp.text,
920
+ round,
921
+ durationMs: resp.durationMs
922
+ });
923
+ } else {
924
+ const errorMsg = result.reason?.message || "Failed to run";
925
+ const msg = {
926
+ role: agent,
927
+ content: errorMsg,
928
+ round,
929
+ error: true
930
+ };
931
+ newMessages.push(msg);
932
+ insertMessage({
933
+ sessionId,
934
+ role: agent,
935
+ content: `[Error: ${errorMsg}]`,
936
+ round
937
+ });
938
+ }
939
+ }
940
+ return newMessages;
941
+ };
942
+ const runRound = async (rawInput) => {
943
+ const { target, prompt, discuss } = parseInput(rawInput);
944
+ if (discuss) {
945
+ return runDiscussion(prompt);
946
+ }
947
+ const currentRound = roundNum;
948
+ runningRoundRef.current = currentRound;
949
+ const userMsg = {
950
+ role: "user",
951
+ content: rawInput,
952
+ round: currentRound
953
+ };
954
+ setMessages((prev) => [...prev, userMsg]);
955
+ insertMessage({
956
+ sessionId,
957
+ role: "user",
958
+ content: rawInput,
959
+ round: currentRound
960
+ });
961
+ const allMessages = [...messages, userMsg];
962
+ const newMessages = await runAgents(allMessages, currentRound, target);
963
+ if (newMessages.length === 0) return;
964
+ setMessages((prev) => [...prev, ...newMessages]);
965
+ setRoundNum(currentRound + 1);
966
+ runningRoundRef.current = null;
967
+ if (currentRound === 0 && !existingSessionId) {
968
+ const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
969
+ updateSessionTitle(sessionId, title);
970
+ }
971
+ touchSession(sessionId);
972
+ setState("input");
973
+ };
974
+ const runDiscussion = async (prompt) => {
975
+ setConsensusReached(false);
976
+ let currentRound = roundNum;
977
+ runningRoundRef.current = currentRound;
978
+ const userMsg = {
979
+ role: "user",
980
+ content: `discuss ${prompt}`,
981
+ round: currentRound
982
+ };
983
+ setMessages((prev) => [...prev, userMsg]);
984
+ insertMessage({
985
+ sessionId,
986
+ role: "user",
987
+ content: `discuss ${prompt}`,
988
+ round: currentRound
989
+ });
990
+ let allMessages = [...messages, userMsg];
991
+ if (currentRound === 0 && !existingSessionId) {
992
+ const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
993
+ updateSessionTitle(sessionId, title);
994
+ }
995
+ for (let disc = 1; disc <= config.discussion.max_rounds; disc++) {
996
+ setDiscussionRound(disc);
997
+ const newMessages = await runAgents(
998
+ allMessages,
999
+ currentRound,
1000
+ "both",
1001
+ disc === 1 ? prompt : "Provide your response for this round.",
1002
+ true
1003
+ );
1004
+ if (newMessages.length === 0) break;
1005
+ setMessages((prev) => [...prev, ...newMessages]);
1006
+ allMessages = [...allMessages, ...newMessages];
1007
+ currentRound++;
1008
+ const claudeMsg = newMessages.find((m) => m.role === "claude" && !m.error);
1009
+ const codexMsg = newMessages.find((m) => m.role === "codex" && !m.error);
1010
+ const claudeConsensus = claudeMsg?.content.includes(CONSENSUS_MARKER) ?? false;
1011
+ const codexConsensus = codexMsg?.content.includes(CONSENSUS_MARKER) ?? false;
1012
+ if (claudeConsensus && codexConsensus) {
1013
+ setConsensusReached(true);
1014
+ break;
1015
+ }
1016
+ }
1017
+ setRoundNum(currentRound);
1018
+ setDiscussionRound(0);
1019
+ runningRoundRef.current = null;
1020
+ touchSession(sessionId);
1021
+ setState("input");
1022
+ };
1023
+ const handleSubmit = (value) => {
1024
+ setStatusMessage(null);
1025
+ if (value === "/exit") {
1026
+ closeDb();
1027
+ exit();
1028
+ return;
1029
+ }
1030
+ if (value === "/help") {
1031
+ setStatusMessage(
1032
+ [
1033
+ "/help Show this help",
1034
+ "/config Edit configuration",
1035
+ "/new Start a new session",
1036
+ "/copy Copy conversation to clipboard",
1037
+ "/exit Exit the app",
1038
+ "",
1039
+ "Esc Interrupt running agents"
1040
+ ].join("\n")
1041
+ );
1042
+ return;
1043
+ }
1044
+ if (value === "/config") {
1045
+ setState("config");
1046
+ return;
1047
+ }
1048
+ if (value === "/new") {
1049
+ const newId = nanoid(12);
1050
+ createSession(newId, process.cwd());
1051
+ setSessionId(newId);
1052
+ setMessages([]);
1053
+ setRoundNum(0);
1054
+ setConsensusReached(false);
1055
+ setDiscussionRound(0);
1056
+ setStatusMessage("Started new session.");
1057
+ return;
1058
+ }
1059
+ if (value === "/copy") {
1060
+ try {
1061
+ const md = formatAsMarkdown(messages);
1062
+ copyToClipboard(md);
1063
+ setStatusMessage("Copied conversation to clipboard.");
1064
+ } catch {
1065
+ setStatusMessage("Failed to copy to clipboard.");
1066
+ }
1067
+ return;
1068
+ }
1069
+ setConsensusReached(false);
1070
+ setState("running");
1071
+ runRound(value);
1072
+ };
1073
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1074
+ /* @__PURE__ */ jsx2(Header, { sessionId }),
1075
+ messages.map((msg, i) => {
1076
+ if (msg.role === "user") {
1077
+ return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1078
+ }
1079
+ if (msg.role === "claude" || msg.role === "codex") {
1080
+ return /* @__PURE__ */ jsx2(
1081
+ AgentResponseBlock,
1082
+ {
1083
+ agent: msg.role,
1084
+ content: msg.content,
1085
+ error: msg.error
1086
+ },
1087
+ i
1088
+ );
1089
+ }
1090
+ return null;
1091
+ }),
1092
+ discussionRound > 0 && thinkingAgents.length > 0 && /* @__PURE__ */ jsx2(DiscussionStatus, { round: discussionRound, maxRounds: config.discussion.max_rounds }),
1093
+ thinkingAgents.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginBottom: 1, children: thinkingAgents.map((agent) => /* @__PURE__ */ jsx2(ThinkingIndicator, { agent }, agent)) }),
1094
+ consensusReached && /* @__PURE__ */ jsx2(ConsensusReached, {}),
1095
+ statusMessage && /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, italic: true, children: statusMessage }) }),
1096
+ state === "config" && /* @__PURE__ */ jsx2(
1097
+ InlineConfigEditor,
1098
+ {
1099
+ isActive: state === "config",
1100
+ onClose: () => setState("input")
1101
+ }
1102
+ ),
1103
+ state === "input" && /* @__PURE__ */ jsx2(PromptInput, { onSubmit: handleSubmit })
1104
+ ] });
1105
+ }
1106
+ function startApp(props) {
1107
+ return render2(/* @__PURE__ */ jsx2(App, { ...props }));
1108
+ }
1109
+ function showTranscriptMarkdown(sessionId) {
1110
+ const dbMessages = getMessages(sessionId);
1111
+ const messages = dbMessages.map((m) => ({
1112
+ role: m.role,
1113
+ content: m.content,
1114
+ round: m.round
1115
+ }));
1116
+ return formatAsMarkdown(messages);
1117
+ }
1118
+ function showTranscript(sessionId) {
1119
+ const dbMessages = getMessages(sessionId);
1120
+ const messages = dbMessages.map((m) => ({
1121
+ role: m.role,
1122
+ content: m.content,
1123
+ round: m.round
1124
+ }));
1125
+ const { unmount } = render2(
1126
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1127
+ /* @__PURE__ */ jsx2(Header, { sessionId }),
1128
+ messages.map((msg, i) => {
1129
+ if (msg.role === "user") {
1130
+ return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1131
+ }
1132
+ if (msg.role === "claude" || msg.role === "codex") {
1133
+ return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, i);
1134
+ }
1135
+ return null;
1136
+ }),
1137
+ /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(60) }) })
1138
+ ] })
1139
+ );
1140
+ unmount();
1141
+ }
1142
+ function showSessionList(sessions) {
1143
+ const { unmount } = render2(
1144
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: sessions.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " No sessions found." }) : sessions.map((s) => /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
1145
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: s.id.slice(0, 7) }),
1146
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1147
+ /* @__PURE__ */ jsx2(Text2, { children: s.title || "(untitled)" }),
1148
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1149
+ /* @__PURE__ */ jsx2(Text2, { color: s.status === "active" ? "green" : void 0, dimColor: s.status !== "active", children: s.status }),
1150
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
1151
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: s.updated_at })
1152
+ ] }, s.id)) })
1153
+ );
1154
+ unmount();
1155
+ }
1156
+
1157
+ // src/index.ts
1158
+ var program = new Command();
1159
+ program.name("tagteam").description("Tag Team - Orchestrate Claude and Codex collaboratively").version("0.1.0").option("--claude-model <model>", "Claude model to use").option("--codex-model <model>", "Codex model to use").argument("[prompt...]", "Prompt to send to both agents").action(async (promptParts, opts) => {
1160
+ const config = loadConfig();
1161
+ const prompt = promptParts.join(" ") || void 0;
1162
+ const instance = startApp({
1163
+ initialPrompt: prompt,
1164
+ claudeModel: opts.claudeModel ?? config.claude.model,
1165
+ codexModel: opts.codexModel ?? config.codex.model,
1166
+ config
1167
+ });
1168
+ await instance.waitUntilExit();
1169
+ });
1170
+ program.command("discuss").description("Have Claude and Codex discuss a topic until they reach consensus").argument("<prompt...>", "Topic to discuss").action(async (promptParts) => {
1171
+ const config = loadConfig();
1172
+ const prompt = promptParts.join(" ");
1173
+ const instance = startApp({
1174
+ initialPrompt: prompt,
1175
+ claudeModel: config.claude.model,
1176
+ codexModel: config.codex.model,
1177
+ config,
1178
+ discuss: true
1179
+ });
1180
+ await instance.waitUntilExit();
1181
+ });
1182
+ program.command("continue").description("Resume the most recent session").action(async () => {
1183
+ const config = loadConfig();
1184
+ const session = getMostRecentSession();
1185
+ if (!session) {
1186
+ console.log(chalk.red(" No active sessions found."));
1187
+ process.exit(1);
1188
+ }
1189
+ const dbMessages = getMessages(session.id);
1190
+ const transcript = dbMessages.map((m) => ({
1191
+ role: m.role,
1192
+ content: m.content,
1193
+ round: m.round
1194
+ }));
1195
+ const instance = startApp({
1196
+ sessionId: session.id,
1197
+ claudeModel: config.claude.model,
1198
+ codexModel: config.codex.model,
1199
+ config,
1200
+ showTranscript: transcript
1201
+ });
1202
+ await instance.waitUntilExit();
1203
+ });
1204
+ program.command("resume [id]").description("Resume a session by ID, or pick interactively").action(async (id) => {
1205
+ const config = loadConfig();
1206
+ if (!id) {
1207
+ const sessions = listSessions(20);
1208
+ if (sessions.length === 0) {
1209
+ console.log(chalk.red(" No sessions found."));
1210
+ process.exit(1);
1211
+ }
1212
+ console.log(chalk.bold("\n Recent sessions:\n"));
1213
+ sessions.forEach((s, i) => {
1214
+ const sid = chalk.cyan(s.id.slice(0, 7));
1215
+ const title = s.title || chalk.dim("(untitled)");
1216
+ const date = chalk.dim(s.updated_at);
1217
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${sid} ${title} ${date}`);
1218
+ });
1219
+ console.log();
1220
+ const readline = await import("readline");
1221
+ const rl = readline.createInterface({
1222
+ input: process.stdin,
1223
+ output: process.stdout
1224
+ });
1225
+ return new Promise((resolve) => {
1226
+ rl.question(" Select session number: ", async (input) => {
1227
+ rl.close();
1228
+ const num = parseInt(input.trim(), 10);
1229
+ if (isNaN(num) || num < 1 || num > sessions.length) {
1230
+ console.log(chalk.red(" Invalid selection."));
1231
+ closeDb();
1232
+ resolve();
1233
+ return;
1234
+ }
1235
+ const session2 = sessions[num - 1];
1236
+ const dbMessages2 = getMessages(session2.id);
1237
+ const transcript2 = dbMessages2.map((m) => ({
1238
+ role: m.role,
1239
+ content: m.content,
1240
+ round: m.round
1241
+ }));
1242
+ const instance2 = startApp({
1243
+ sessionId: session2.id,
1244
+ claudeModel: config.claude.model,
1245
+ codexModel: config.codex.model,
1246
+ config,
1247
+ showTranscript: transcript2
1248
+ });
1249
+ await instance2.waitUntilExit();
1250
+ resolve();
1251
+ });
1252
+ });
1253
+ }
1254
+ const session = getSession(id) || getSessionByPrefix(id);
1255
+ if (!session) {
1256
+ console.log(chalk.red(` Session not found: ${id}`));
1257
+ process.exit(1);
1258
+ }
1259
+ const dbMessages = getMessages(session.id);
1260
+ const transcript = dbMessages.map((m) => ({
1261
+ role: m.role,
1262
+ content: m.content,
1263
+ round: m.round
1264
+ }));
1265
+ const instance = startApp({
1266
+ sessionId: session.id,
1267
+ claudeModel: config.claude.model,
1268
+ codexModel: config.codex.model,
1269
+ config,
1270
+ showTranscript: transcript
1271
+ });
1272
+ await instance.waitUntilExit();
1273
+ });
1274
+ program.command("history").description("List recent sessions").option("-n, --limit <n>", "Number of sessions to show", parseInt, 20).action((opts) => {
1275
+ const sessions = listSessions(opts.limit);
1276
+ console.log(chalk.bold("\n Recent sessions:\n"));
1277
+ showSessionList(sessions);
1278
+ console.log();
1279
+ closeDb();
1280
+ });
1281
+ program.command("show <id>").description("Print full transcript for a session").option("-m, --markdown", "Output as GitHub-compatible markdown").action((id, opts) => {
1282
+ const session = getSession(id) || getSessionByPrefix(id);
1283
+ if (!session) {
1284
+ console.log(chalk.red(` Session not found: ${id}`));
1285
+ process.exit(1);
1286
+ }
1287
+ if (opts.markdown) {
1288
+ process.stdout.write(showTranscriptMarkdown(session.id));
1289
+ } else {
1290
+ showTranscript(session.id);
1291
+ }
1292
+ closeDb();
1293
+ });
1294
+ var configCmd = program.command("config").description("Manage configuration").action(async () => {
1295
+ const instance = startConfigEditor();
1296
+ await instance.waitUntilExit();
1297
+ });
1298
+ configCmd.command("edit").description("Interactively edit configuration").action(async () => {
1299
+ const instance = startConfigEditor();
1300
+ await instance.waitUntilExit();
1301
+ });
1302
+ configCmd.command("show").description("Show current configuration").action(() => {
1303
+ const config = loadConfig();
1304
+ console.log(chalk.bold("\n Configuration:\n"));
1305
+ console.log(
1306
+ chalk.dim(" claude.model = ") + chalk.white(config.claude.model)
1307
+ );
1308
+ console.log(
1309
+ chalk.dim(" codex.model = ") + chalk.white(config.codex.model)
1310
+ );
1311
+ console.log(
1312
+ chalk.dim(" discussion.max_rounds = ") + chalk.white(String(config.discussion.max_rounds))
1313
+ );
1314
+ console.log();
1315
+ });
1316
+ configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
1317
+ try {
1318
+ setConfigValue(key, value);
1319
+ console.log(chalk.green(` Set ${key} = ${value}`));
1320
+ } catch (e) {
1321
+ console.log(chalk.red(` ${e.message}`));
1322
+ process.exit(1);
1323
+ }
1324
+ });
1325
+ program.parseAsync();
1326
+ //# sourceMappingURL=index.js.map