tagteam 0.1.0 → 0.2.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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { execSync as execSync2 } from "child_process";
5
6
  import chalk from "chalk";
6
7
 
7
8
  // src/config.ts
@@ -10,17 +11,27 @@ import { join } from "path";
10
11
  import { homedir } from "os";
11
12
  import { parse, stringify } from "smol-toml";
12
13
  var DEFAULT_CONFIG = {
14
+ agents: ["claude", "codex"],
13
15
  claude: {
14
16
  model: "sonnet"
15
17
  },
16
18
  codex: {
17
19
  model: "gpt-5.3-codex"
18
20
  },
21
+ gemini: {
22
+ model: "gemini-2.5-pro"
23
+ },
19
24
  discussion: {
20
25
  max_rounds: 10
21
26
  }
22
27
  };
23
28
  function getConfigDir() {
29
+ if (process.platform === "win32") {
30
+ return join(
31
+ process.env.APPDATA || join(homedir(), "AppData", "Roaming"),
32
+ "tagteam"
33
+ );
34
+ }
24
35
  return join(homedir(), ".tagteam");
25
36
  }
26
37
  function getConfigPath() {
@@ -41,8 +52,10 @@ function loadConfig() {
41
52
  const raw = readFileSync(configPath, "utf-8");
42
53
  const parsed = parse(raw);
43
54
  return {
55
+ agents: Array.isArray(parsed.agents) && parsed.agents.length === 2 ? parsed.agents : [...DEFAULT_CONFIG.agents],
44
56
  claude: { ...DEFAULT_CONFIG.claude, ...parsed.claude },
45
57
  codex: { ...DEFAULT_CONFIG.codex, ...parsed.codex },
58
+ gemini: { ...DEFAULT_CONFIG.gemini, ...parsed.gemini },
46
59
  discussion: { ...DEFAULT_CONFIG.discussion, ...parsed.discussion }
47
60
  };
48
61
  } catch {
@@ -59,12 +72,18 @@ function setConfigValue(key, value) {
59
72
  const parts = key.split(".");
60
73
  if (parts.length === 1) {
61
74
  switch (parts[0]) {
75
+ case "agents":
76
+ config.agents = value.split(",").map((s) => s.trim());
77
+ break;
62
78
  case "claude_model":
63
79
  config.claude.model = value;
64
80
  break;
65
81
  case "codex_model":
66
82
  config.codex.model = value;
67
83
  break;
84
+ case "gemini_model":
85
+ config.gemini.model = value;
86
+ break;
68
87
  case "discussion_max_rounds":
69
88
  config.discussion.max_rounds = Number(value);
70
89
  break;
@@ -77,6 +96,8 @@ function setConfigValue(key, value) {
77
96
  config.claude.model = value;
78
97
  } else if (section === "codex" && field === "model") {
79
98
  config.codex.model = value;
99
+ } else if (section === "gemini" && field === "model") {
100
+ config.gemini.model = value;
80
101
  } else if (section === "discussion" && field === "max_rounds") {
81
102
  config.discussion.max_rounds = Number(value);
82
103
  } else {
@@ -338,6 +359,150 @@ async function runCodex(options) {
338
359
  };
339
360
  }
340
361
 
362
+ // src/agents/gemini.ts
363
+ import { spawn as spawn3 } from "child_process";
364
+ async function* streamGemini(options) {
365
+ const args = ["-p"];
366
+ let fullPrompt = options.prompt;
367
+ if (options.systemPrompt) {
368
+ fullPrompt = `${options.systemPrompt}
369
+
370
+ ---
371
+
372
+ ${options.prompt}`;
373
+ }
374
+ args.push(fullPrompt, "--output-format", "json", "--yolo");
375
+ if (options.model) {
376
+ args.push("-m", options.model);
377
+ }
378
+ const proc = spawn3("gemini", args, {
379
+ cwd: options.cwd,
380
+ stdio: ["ignore", "pipe", "inherit"],
381
+ env: process.env
382
+ });
383
+ let spawnErrorMsg = "";
384
+ proc.on("error", (err) => {
385
+ spawnErrorMsg = err.message;
386
+ });
387
+ if (options.signal) {
388
+ if (options.signal.aborted) {
389
+ proc.kill();
390
+ } else {
391
+ options.signal.addEventListener("abort", () => proc.kill(), { once: true });
392
+ }
393
+ }
394
+ const chunks = [];
395
+ proc.stdout.on("data", (chunk) => {
396
+ chunks.push(chunk);
397
+ });
398
+ await new Promise((resolve) => {
399
+ proc.on("close", resolve);
400
+ });
401
+ if (spawnErrorMsg) {
402
+ yield {
403
+ type: "error",
404
+ agent: "gemini",
405
+ content: `Failed to spawn gemini: ${spawnErrorMsg}`
406
+ };
407
+ yield { type: "done", agent: "gemini" };
408
+ return;
409
+ }
410
+ const stdout = Buffer.concat(chunks).toString("utf-8").trim();
411
+ if (!stdout) {
412
+ yield { type: "error", agent: "gemini", content: "No output from gemini" };
413
+ yield { type: "done", agent: "gemini" };
414
+ return;
415
+ }
416
+ try {
417
+ const data = JSON.parse(stdout);
418
+ if (data.error) {
419
+ yield { type: "error", agent: "gemini", content: data.error };
420
+ } else if (data.response) {
421
+ yield { type: "text", agent: "gemini", content: data.response };
422
+ } else {
423
+ yield { type: "error", agent: "gemini", content: "Unexpected response format" };
424
+ }
425
+ } catch {
426
+ yield { type: "text", agent: "gemini", content: stdout };
427
+ }
428
+ yield { type: "done", agent: "gemini" };
429
+ }
430
+ async function runGemini(options) {
431
+ const start = Date.now();
432
+ const events = [];
433
+ const textParts = [];
434
+ const errors = [];
435
+ for await (const event of streamGemini(options)) {
436
+ events.push(event);
437
+ if (event.type === "text" && event.content) {
438
+ textParts.push(event.content);
439
+ } else if (event.type === "error" && event.content) {
440
+ errors.push(event.content);
441
+ }
442
+ }
443
+ return {
444
+ agent: "gemini",
445
+ text: textParts.join(""),
446
+ events,
447
+ durationMs: Date.now() - start,
448
+ error: errors.length > 0 ? errors.join("\n") : void 0
449
+ };
450
+ }
451
+
452
+ // src/agents/registry.ts
453
+ var AGENTS = {
454
+ claude: {
455
+ name: "claude",
456
+ displayName: "Claude",
457
+ color: "magenta",
458
+ cliBinary: "claude",
459
+ installUrl: "https://docs.anthropic.com/en/docs/claude-code",
460
+ org: "Anthropic",
461
+ run: runClaude
462
+ },
463
+ codex: {
464
+ name: "codex",
465
+ displayName: "Codex",
466
+ color: "green",
467
+ cliBinary: "codex",
468
+ installUrl: "https://github.com/openai/codex",
469
+ org: "OpenAI",
470
+ run: runCodex
471
+ },
472
+ gemini: {
473
+ name: "gemini",
474
+ displayName: "Gemini",
475
+ color: "blue",
476
+ cliBinary: "gemini",
477
+ installUrl: "https://github.com/google-gemini/gemini-cli",
478
+ org: "Google",
479
+ run: runGemini
480
+ }
481
+ };
482
+ function getAgent(name) {
483
+ return AGENTS[name];
484
+ }
485
+ function getAllAgentNames() {
486
+ return Object.keys(AGENTS);
487
+ }
488
+ function isValidAgentName(name) {
489
+ return name in AGENTS;
490
+ }
491
+ function validateAgentPair(pair) {
492
+ if (pair.length !== 2) {
493
+ throw new Error("Agent pair must contain exactly 2 agents");
494
+ }
495
+ if (pair[0] === pair[1]) {
496
+ throw new Error("Agent pair must contain 2 distinct agents");
497
+ }
498
+ for (const name of pair) {
499
+ if (!isValidAgentName(name)) {
500
+ throw new Error(`Unknown agent: "${name}". Valid agents: ${getAllAgentNames().join(", ")}`);
501
+ }
502
+ }
503
+ return pair;
504
+ }
505
+
341
506
  // src/format.ts
342
507
  function formatAsMarkdown(messages) {
343
508
  const parts = [];
@@ -345,8 +510,8 @@ function formatAsMarkdown(messages) {
345
510
  if (msg.role === "system") continue;
346
511
  if (msg.role === "user") {
347
512
  parts.push(`**You:** ${msg.content}`);
348
- } else {
349
- const name = msg.role === "claude" ? "Claude" : "Codex";
513
+ } else if (isValidAgentName(msg.role)) {
514
+ const name = getAgent(msg.role).displayName;
350
515
  if (msg.error) {
351
516
  parts.push(`**${name}:** *(error)*
352
517
 
@@ -371,14 +536,37 @@ function copyToClipboard(text) {
371
536
  } else if (platform === "win32") {
372
537
  cmd = "clip";
373
538
  } else {
539
+ let hasXclip = false;
540
+ let hasXsel = false;
374
541
  try {
375
542
  execSync("which xclip", { stdio: "ignore" });
376
- cmd = "xclip -selection clipboard";
543
+ hasXclip = true;
377
544
  } catch {
545
+ }
546
+ if (!hasXclip) {
547
+ try {
548
+ execSync("which xsel", { stdio: "ignore" });
549
+ hasXsel = true;
550
+ } catch {
551
+ }
552
+ }
553
+ if (hasXclip) {
554
+ cmd = "xclip -selection clipboard";
555
+ } else if (hasXsel) {
378
556
  cmd = "xsel --clipboard --input";
557
+ } else {
558
+ throw new Error(
559
+ "Clipboard requires xclip or xsel. Install one with: sudo apt install xclip"
560
+ );
379
561
  }
380
562
  }
381
- execSync(cmd, { input: text, stdio: ["pipe", "ignore", "ignore"] });
563
+ try {
564
+ execSync(cmd, { input: text, stdio: ["pipe", "ignore", "ignore"] });
565
+ } catch {
566
+ throw new Error(
567
+ `Failed to copy to clipboard using "${cmd.split(" ")[0]}". Check that it is installed and working.`
568
+ );
569
+ }
382
570
  }
383
571
 
384
572
  // src/db/index.ts
@@ -496,15 +684,16 @@ function deleteMessagesFromRound(sessionId, fromRound) {
496
684
  }
497
685
 
498
686
  // 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.`;
687
+ function otherAgent(agent, pair) {
688
+ const other = pair[0] === agent ? pair[1] : pair[0];
689
+ const desc = getAgent(other);
690
+ return `${desc.displayName} (${desc.org})`;
691
+ }
692
+ function collaborationPrompt(agent, pair) {
693
+ return `You are in a collaborative session with ${otherAgent(agent, pair)}. 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
694
  }
506
- function discussionPrompt(agent, conversationHistory) {
507
- return `${collaborationPrompt(agent)}
695
+ function discussionPrompt(agent, conversationHistory, pair) {
696
+ return `${collaborationPrompt(agent, pair)}
508
697
 
509
698
  Here is the conversation so far:
510
699
 
@@ -512,29 +701,40 @@ ${conversationHistory}
512
701
 
513
702
  Now provide your response for this discussion round. Build on what was said, highlight agreements, and address any disagreements constructively. Be concise.`;
514
703
  }
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.
704
+ function debatePrompt(agent, pair) {
705
+ const other = otherAgent(agent, pair);
706
+ return `You are in a structured debate with ${other}. You'll both respond to the user's prompt, then see each other's responses and discuss.
517
707
 
518
708
  Your goal is to reach consensus through constructive discussion. In each round:
519
709
  - Address specific points of agreement and disagreement
520
- - Refine your position based on valid arguments from ${OTHER[agent]}
710
+ - Refine your position based on valid arguments from ${other}
521
711
  - Be concise \u2014 don't repeat points already established
522
712
 
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.`;
713
+ When you believe you and ${other} 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
714
  }
525
- function debateRoundPrompt(agent, conversationHistory) {
526
- return `${debatePrompt(agent)}
715
+ function debateRoundPrompt(agent, conversationHistory, pair) {
716
+ const other = otherAgent(agent, pair);
717
+ return `${debatePrompt(agent, pair)}
527
718
 
528
719
  Here is the conversation so far:
529
720
 
530
721
  ${conversationHistory}
531
722
 
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.`;
723
+ Respond to the latest round. If you agree with ${other}'s position on all key points, end with [CONSENSUS]. Otherwise, continue the discussion.`;
533
724
  }
534
725
  var CONSENSUS_MARKER = "[CONSENSUS]";
535
726
  function formatConversationHistory(messages) {
536
727
  return messages.map((m) => {
537
- const label = m.role === "user" ? "User" : m.agent === "claude" ? "Claude" : "Codex";
728
+ let label;
729
+ if (m.role === "user") {
730
+ label = "User";
731
+ } else if (isValidAgentName(m.role)) {
732
+ label = getAgent(m.role).displayName;
733
+ } else if (m.agent && isValidAgentName(m.agent)) {
734
+ label = getAgent(m.agent).displayName;
735
+ } else {
736
+ label = m.role;
737
+ }
538
738
  return `[${label}]: ${m.content}`;
539
739
  }).join("\n\n");
540
740
  }
@@ -545,6 +745,21 @@ import { render, Box, Text, useApp, useInput } from "ink";
545
745
  import TextInput from "ink-text-input";
546
746
  import { jsx, jsxs } from "react/jsx-runtime";
547
747
  var CONFIG_FIELDS = [
748
+ {
749
+ key: "agents",
750
+ label: "Agent pair",
751
+ get: (c) => c.agents.join(", "),
752
+ type: "string",
753
+ validate: (value) => {
754
+ try {
755
+ const names = value.split(",").map((s) => s.trim());
756
+ validateAgentPair(names);
757
+ return null;
758
+ } catch (e) {
759
+ return e.message;
760
+ }
761
+ }
762
+ },
548
763
  {
549
764
  key: "claude.model",
550
765
  label: "Claude model",
@@ -557,6 +772,12 @@ var CONFIG_FIELDS = [
557
772
  get: (c) => c.codex.model,
558
773
  type: "string"
559
774
  },
775
+ {
776
+ key: "gemini.model",
777
+ label: "Gemini model",
778
+ get: (c) => c.gemini.model,
779
+ type: "string"
780
+ },
560
781
  {
561
782
  key: "discussion.max_rounds",
562
783
  label: "Discussion max rounds",
@@ -608,6 +829,14 @@ function InlineConfigEditor({ isActive, onClose }) {
608
829
  return;
609
830
  }
610
831
  }
832
+ if (field.validate) {
833
+ const error = field.validate(value);
834
+ if (error) {
835
+ setSavedMessage(`Error: ${error}`);
836
+ setMode("select");
837
+ return;
838
+ }
839
+ }
611
840
  try {
612
841
  const updated = setConfigValue(field.key, value);
613
842
  setConfig(updated);
@@ -658,16 +887,15 @@ function startConfigEditor() {
658
887
  // src/ui.tsx
659
888
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
660
889
  marked.use(markedTerminal());
661
- function parseInput(input) {
890
+ function parseInput(input, pair) {
662
891
  const lower = input.toLowerCase();
663
892
  if (lower.startsWith("discuss ")) {
664
893
  return { target: "both", prompt: input.slice(8).trim(), discuss: true };
665
894
  }
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 };
895
+ for (const agent of pair) {
896
+ if (lower.startsWith(`${agent} `) || lower.startsWith(`${agent}, `)) {
897
+ return { target: agent, prompt: input.slice(input.indexOf(" ") + 1).trim(), discuss: false };
898
+ }
671
899
  }
672
900
  return { target: "both", prompt: input, discuss: false };
673
901
  }
@@ -680,19 +908,19 @@ function AgentResponseBlock({
680
908
  content,
681
909
  error
682
910
  }) {
683
- const color = agent === "claude" ? "magenta" : "green";
911
+ const descriptor = getAgent(agent);
684
912
  if (error) {
685
913
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
686
914
  /* @__PURE__ */ jsxs2(Text2, { color: "red", bold: true, children: [
687
- agent === "claude" ? "Claude" : "Codex",
915
+ descriptor.displayName,
688
916
  " error:"
689
917
  ] }),
690
918
  /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "red", children: content }) })
691
919
  ] });
692
920
  }
693
921
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
694
- /* @__PURE__ */ jsxs2(Text2, { color, bold: true, children: [
695
- agent === "claude" ? "Claude" : "Codex",
922
+ /* @__PURE__ */ jsxs2(Text2, { color: descriptor.color, bold: true, children: [
923
+ descriptor.displayName,
696
924
  ":"
697
925
  ] }),
698
926
  /* @__PURE__ */ jsx2(Box2, { marginLeft: 1, children: /* @__PURE__ */ jsx2(RenderedMarkdown, { text: content }) })
@@ -717,13 +945,12 @@ function UserMessage({ content }) {
717
945
  ] });
718
946
  }
719
947
  function ThinkingIndicator({ agent }) {
720
- const color = agent === "claude" ? "magenta" : "green";
721
- const label = agent === "claude" ? "Claude" : "Codex";
948
+ const descriptor = getAgent(agent);
722
949
  return /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, children: [
723
- /* @__PURE__ */ jsx2(Text2, { color, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
724
- /* @__PURE__ */ jsxs2(Text2, { color, children: [
950
+ /* @__PURE__ */ jsx2(Text2, { color: descriptor.color, children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }),
951
+ /* @__PURE__ */ jsxs2(Text2, { color: descriptor.color, children: [
725
952
  " ",
726
- label,
953
+ descriptor.displayName,
727
954
  " is thinking..."
728
955
  ] })
729
956
  ] });
@@ -768,9 +995,9 @@ function PromptInput({
768
995
  function App({
769
996
  initialPrompt,
770
997
  sessionId: existingSessionId,
771
- claudeModel,
772
- codexModel,
773
- config,
998
+ agents: initialPair,
999
+ agentModels: initialAgentModels,
1000
+ config: initialConfig,
774
1001
  showTranscript: showTranscript2,
775
1002
  discuss: initialDiscuss
776
1003
  }) {
@@ -780,6 +1007,9 @@ function App({
780
1007
  const [state, setState] = useState2(
781
1008
  initialPrompt ? "running" : "input"
782
1009
  );
1010
+ const [pair, setPair] = useState2(initialPair);
1011
+ const [agentModels, setAgentModels] = useState2(initialAgentModels);
1012
+ const [config, setConfig] = useState2(initialConfig);
783
1013
  const [thinkingAgents, setThinkingAgents] = useState2([]);
784
1014
  const [discussionRound, setDiscussionRound] = useState2(0);
785
1015
  const [consensusReached, setConsensusReached] = useState2(false);
@@ -828,22 +1058,18 @@ function App({
828
1058
  }
829
1059
  });
830
1060
  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");
1061
+ const activeAgents = target === "both" ? [...pair] : [target];
836
1062
  setThinkingAgents(activeAgents);
837
1063
  const history = formatConversationHistory(
838
1064
  currentMessages.map((m) => ({
839
1065
  role: m.role,
840
- agent: m.role === "claude" || m.role === "codex" ? m.role : void 0,
1066
+ agent: isValidAgentName(m.role) ? m.role : void 0,
841
1067
  content: m.content
842
1068
  }))
843
1069
  );
844
1070
  const cwd = process.cwd();
845
1071
  const isFirstRound = round === 0 && !existingSessionId;
846
- const agentPrompt = (agent) => {
1072
+ const agentPrompt = (_agent) => {
847
1073
  if (promptOverride) return promptOverride;
848
1074
  if (isFirstRound && target === "both") {
849
1075
  return currentMessages[currentMessages.length - 1]?.content || "";
@@ -852,48 +1078,31 @@ function App({
852
1078
  };
853
1079
  const agentSystemPrompt = (agent) => {
854
1080
  if (isDebate) {
855
- if (isFirstRound) return debatePrompt(agent);
856
- return debateRoundPrompt(agent, history);
1081
+ if (isFirstRound) return debatePrompt(agent, pair);
1082
+ return debateRoundPrompt(agent, history, pair);
857
1083
  }
858
- if (isFirstRound && target === "both") return collaborationPrompt(agent);
859
- return discussionPrompt(agent, history);
1084
+ if (isFirstRound && target === "both") return collaborationPrompt(agent, pair);
1085
+ return discussionPrompt(agent, history, pair);
860
1086
  };
861
1087
  const ac = new AbortController();
862
1088
  abortRef.current = ac;
863
1089
  const results = [];
864
1090
  const promises = [];
865
- if (runCl) {
1091
+ for (const agent of activeAgents) {
1092
+ const descriptor = getAgent(agent);
866
1093
  promises.push(
867
- runClaude({
868
- prompt: agentPrompt("claude"),
869
- systemPrompt: agentSystemPrompt("claude"),
870
- model: claudeModel,
1094
+ descriptor.run({
1095
+ prompt: agentPrompt(agent),
1096
+ systemPrompt: agentSystemPrompt(agent),
1097
+ model: agentModels[agent],
871
1098
  cwd,
872
1099
  signal: ac.signal
873
1100
  }).then(
874
1101
  (value) => {
875
- results.push({ agent: "claude", result: { status: "fulfilled", value } });
1102
+ results.push({ agent, result: { status: "fulfilled", value } });
876
1103
  },
877
1104
  (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 } });
1105
+ results.push({ agent, result: { status: "rejected", reason } });
897
1106
  }
898
1107
  )
899
1108
  );
@@ -940,7 +1149,7 @@ function App({
940
1149
  return newMessages;
941
1150
  };
942
1151
  const runRound = async (rawInput) => {
943
- const { target, prompt, discuss } = parseInput(rawInput);
1152
+ const { target, prompt, discuss } = parseInput(rawInput, pair);
944
1153
  if (discuss) {
945
1154
  return runDiscussion(prompt);
946
1155
  }
@@ -1005,11 +1214,11 @@ function App({
1005
1214
  setMessages((prev) => [...prev, ...newMessages]);
1006
1215
  allMessages = [...allMessages, ...newMessages];
1007
1216
  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) {
1217
+ const consensusFlags = pair.map((agent) => {
1218
+ const msg = newMessages.find((m) => m.role === agent && !m.error);
1219
+ return msg?.content.includes(CONSENSUS_MARKER) ?? false;
1220
+ });
1221
+ if (consensusFlags.every(Boolean)) {
1013
1222
  setConsensusReached(true);
1014
1223
  break;
1015
1224
  }
@@ -1076,7 +1285,7 @@ function App({
1076
1285
  if (msg.role === "user") {
1077
1286
  return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1078
1287
  }
1079
- if (msg.role === "claude" || msg.role === "codex") {
1288
+ if (isValidAgentName(msg.role)) {
1080
1289
  return /* @__PURE__ */ jsx2(
1081
1290
  AgentResponseBlock,
1082
1291
  {
@@ -1097,7 +1306,21 @@ function App({
1097
1306
  InlineConfigEditor,
1098
1307
  {
1099
1308
  isActive: state === "config",
1100
- onClose: () => setState("input")
1309
+ onClose: () => {
1310
+ const updated = loadConfig();
1311
+ setConfig(updated);
1312
+ try {
1313
+ setPair(validateAgentPair(updated.agents));
1314
+ } catch {
1315
+ }
1316
+ setAgentModels((prev) => ({
1317
+ ...prev,
1318
+ claude: updated.claude.model,
1319
+ codex: updated.codex.model,
1320
+ gemini: updated.gemini.model
1321
+ }));
1322
+ setState("input");
1323
+ }
1101
1324
  }
1102
1325
  ),
1103
1326
  state === "input" && /* @__PURE__ */ jsx2(PromptInput, { onSubmit: handleSubmit })
@@ -1129,7 +1352,7 @@ function showTranscript(sessionId) {
1129
1352
  if (msg.role === "user") {
1130
1353
  return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, i);
1131
1354
  }
1132
- if (msg.role === "claude" || msg.role === "codex") {
1355
+ if (isValidAgentName(msg.role)) {
1133
1356
  return /* @__PURE__ */ jsx2(AgentResponseBlock, { agent: msg.role, content: msg.content }, i);
1134
1357
  }
1135
1358
  return null;
@@ -1155,25 +1378,75 @@ function showSessionList(sessions) {
1155
1378
  }
1156
1379
 
1157
1380
  // src/index.ts
1381
+ process.on("SIGTERM", () => {
1382
+ closeDb();
1383
+ process.exit(0);
1384
+ });
1385
+ process.on("SIGINT", () => {
1386
+ closeDb();
1387
+ process.exit(0);
1388
+ });
1389
+ function checkCli(name, installUrl) {
1390
+ try {
1391
+ execSync2(`${name} --version`, { stdio: "ignore" });
1392
+ return true;
1393
+ } catch {
1394
+ console.error(
1395
+ chalk.red(`"${name}" not found. Install it from: ${installUrl}`)
1396
+ );
1397
+ return false;
1398
+ }
1399
+ }
1400
+ function preflight(pair) {
1401
+ let allFound = true;
1402
+ for (const agent of pair) {
1403
+ const descriptor = getAgent(agent);
1404
+ if (!checkCli(descriptor.cliBinary, descriptor.installUrl)) {
1405
+ allFound = false;
1406
+ }
1407
+ }
1408
+ if (!allFound) {
1409
+ process.exit(1);
1410
+ }
1411
+ }
1412
+ function resolveAgentPair(opts, config) {
1413
+ if (opts.agents) {
1414
+ const names = opts.agents.split(",").map((s) => s.trim());
1415
+ return validateAgentPair(names);
1416
+ }
1417
+ return validateAgentPair(config.agents);
1418
+ }
1419
+ function resolveAgentModels(pair, opts, config) {
1420
+ return {
1421
+ claude: opts.claudeModel ?? config.claude.model,
1422
+ codex: opts.codexModel ?? config.codex.model,
1423
+ gemini: opts.geminiModel ?? config.gemini.model
1424
+ };
1425
+ }
1158
1426
  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) => {
1427
+ program.name("tagteam").description("Tag Team - Orchestrate AI agents collaboratively").version("0.2.0").option("--agents <pair>", "Agent pair to use (comma-separated, e.g. claude,gemini)").option("--claude-model <model>", "Claude model to use").option("--codex-model <model>", "Codex model to use").option("--gemini-model <model>", "Gemini model to use").argument("[prompt...]", "Prompt to send to both agents").action(async (promptParts, opts) => {
1160
1428
  const config = loadConfig();
1429
+ const pair = resolveAgentPair(opts, config);
1430
+ preflight(pair);
1161
1431
  const prompt = promptParts.join(" ") || void 0;
1162
1432
  const instance = startApp({
1163
1433
  initialPrompt: prompt,
1164
- claudeModel: opts.claudeModel ?? config.claude.model,
1165
- codexModel: opts.codexModel ?? config.codex.model,
1434
+ agents: pair,
1435
+ agentModels: resolveAgentModels(pair, opts, config),
1166
1436
  config
1167
1437
  });
1168
1438
  await instance.waitUntilExit();
1169
1439
  });
1170
- program.command("discuss").description("Have Claude and Codex discuss a topic until they reach consensus").argument("<prompt...>", "Topic to discuss").action(async (promptParts) => {
1440
+ program.command("discuss").description("Have agents discuss a topic until they reach consensus").argument("<prompt...>", "Topic to discuss").action(async (promptParts) => {
1171
1441
  const config = loadConfig();
1442
+ const parentOpts = program.opts();
1443
+ const pair = resolveAgentPair(parentOpts, config);
1444
+ preflight(pair);
1172
1445
  const prompt = promptParts.join(" ");
1173
1446
  const instance = startApp({
1174
1447
  initialPrompt: prompt,
1175
- claudeModel: config.claude.model,
1176
- codexModel: config.codex.model,
1448
+ agents: pair,
1449
+ agentModels: resolveAgentModels(pair, parentOpts, config),
1177
1450
  config,
1178
1451
  discuss: true
1179
1452
  });
@@ -1181,6 +1454,9 @@ program.command("discuss").description("Have Claude and Codex discuss a topic un
1181
1454
  });
1182
1455
  program.command("continue").description("Resume the most recent session").action(async () => {
1183
1456
  const config = loadConfig();
1457
+ const parentOpts = program.opts();
1458
+ const pair = resolveAgentPair(parentOpts, config);
1459
+ preflight(pair);
1184
1460
  const session = getMostRecentSession();
1185
1461
  if (!session) {
1186
1462
  console.log(chalk.red(" No active sessions found."));
@@ -1194,8 +1470,8 @@ program.command("continue").description("Resume the most recent session").action
1194
1470
  }));
1195
1471
  const instance = startApp({
1196
1472
  sessionId: session.id,
1197
- claudeModel: config.claude.model,
1198
- codexModel: config.codex.model,
1473
+ agents: pair,
1474
+ agentModels: resolveAgentModels(pair, parentOpts, config),
1199
1475
  config,
1200
1476
  showTranscript: transcript
1201
1477
  });
@@ -1203,6 +1479,9 @@ program.command("continue").description("Resume the most recent session").action
1203
1479
  });
1204
1480
  program.command("resume [id]").description("Resume a session by ID, or pick interactively").action(async (id) => {
1205
1481
  const config = loadConfig();
1482
+ const parentOpts = program.opts();
1483
+ const pair = resolveAgentPair(parentOpts, config);
1484
+ preflight(pair);
1206
1485
  if (!id) {
1207
1486
  const sessions = listSessions(20);
1208
1487
  if (sessions.length === 0) {
@@ -1241,8 +1520,8 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1241
1520
  }));
1242
1521
  const instance2 = startApp({
1243
1522
  sessionId: session2.id,
1244
- claudeModel: config.claude.model,
1245
- codexModel: config.codex.model,
1523
+ agents: pair,
1524
+ agentModels: resolveAgentModels(pair, parentOpts, config),
1246
1525
  config,
1247
1526
  showTranscript: transcript2
1248
1527
  });
@@ -1264,8 +1543,8 @@ program.command("resume [id]").description("Resume a session by ID, or pick inte
1264
1543
  }));
1265
1544
  const instance = startApp({
1266
1545
  sessionId: session.id,
1267
- claudeModel: config.claude.model,
1268
- codexModel: config.codex.model,
1546
+ agents: pair,
1547
+ agentModels: resolveAgentModels(pair, parentOpts, config),
1269
1548
  config,
1270
1549
  showTranscript: transcript
1271
1550
  });
@@ -1303,10 +1582,16 @@ configCmd.command("show").description("Show current configuration").action(() =>
1303
1582
  const config = loadConfig();
1304
1583
  console.log(chalk.bold("\n Configuration:\n"));
1305
1584
  console.log(
1306
- chalk.dim(" claude.model = ") + chalk.white(config.claude.model)
1585
+ chalk.dim(" agents = ") + chalk.white(config.agents.join(", "))
1586
+ );
1587
+ console.log(
1588
+ chalk.dim(" claude.model = ") + chalk.white(config.claude.model)
1589
+ );
1590
+ console.log(
1591
+ chalk.dim(" codex.model = ") + chalk.white(config.codex.model)
1307
1592
  );
1308
1593
  console.log(
1309
- chalk.dim(" codex.model = ") + chalk.white(config.codex.model)
1594
+ chalk.dim(" gemini.model = ") + chalk.white(config.gemini.model)
1310
1595
  );
1311
1596
  console.log(
1312
1597
  chalk.dim(" discussion.max_rounds = ") + chalk.white(String(config.discussion.max_rounds))