spectoflow 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,10 +21,12 @@ You speak in plain language; the framework classifies your intent and runs the r
21
21
  ceremonial command to start.
22
22
 
23
23
  **Works with whichever coding agent you have** — Claude Code, Codex, Cursor, Gemini CLI, OpenCode,
24
- Kiro CLI, and Antigravity are auto-detected at `init`. The dashboard's topbar always shows the
25
- **active agent**, front and center, with a switcher: pick another and it's verified as genuinely
24
+ Kiro CLI, Antigravity, and Kimi CLI are auto-detected at `init`. The dashboard's topbar always shows
25
+ the **active agent**, front and center, with a switcher: pick another and it's verified as genuinely
26
26
  installed before activating (a red **"No agent found"** if none is) — it never silently activates
27
- something that isn't there.
27
+ something that isn't there. (Kimi CLI doesn't yet ship a non-interactive run mode, so spectoflow can
28
+ set it as active but won't try to spawn it — the agent pickers that launch a run grey it out rather
29
+ than hide it.)
28
30
 
29
31
  ## Install
30
32
 
package/lib/adapters.js CHANGED
@@ -68,11 +68,20 @@ Argument: \`$ARGUMENTS\`
68
68
 
69
69
  // Priority order = which agent becomes the default when several are detected.
70
70
  //
71
- // Considered and left out (researched, not re-added on a whim): Kimi Code CLI (MoonshotAI/kimi-cli)
72
- // and DeepSeek Harness (deepseek-ai/deepseek-harness) as of Sept 2026 neither ships a genuine
73
- // non-interactive one-shot mode (prompt-as-trailing-arg, stdout, exit) that runner.js's spawn model
74
- // needs; Kimi is interactive/ACP-only, DeepSeek Harness is a local web-app framework. Revisit if
75
- // either ships a headless flaga fabricated runner command would just fail silently for real users.
71
+ // `headless: false` marks an agent spectoflow can DETECT and let you set as active, but never spawn
72
+ // itself no confirmed non-interactive one-shot mode (prompt as trailing arg stdout exit) as of
73
+ // this research pass, so `runner` is null on purpose (a fabricated command would just fail silently
74
+ // for real users). The dashboard still lists it and shows why Run/Orchestrate/Summarize are disabled
75
+ // for it, rather than hiding it you can drive it yourself in its own terminal meanwhile, and
76
+ // `config.json → runners` is still yours to hand-wire a command into if one ships later.
77
+ //
78
+ // Left out of KNOWN_AGENTS entirely (not even headless:false) — researched, not re-added on a whim:
79
+ // DeepSeek Harness (deepseek-ai/deepseek-harness). It has no single installable binary to detect —
80
+ // it's invoked as `npx @deepseek-ai/dsh <subcommand>`, is explicitly a local WEB-APP framework (not a
81
+ // terminal chat/prompt tool like the others), and ships as an August-2026 "developer preview" whose
82
+ // APIs the project itself says will change. Detecting "is npx present" would not mean deepseek-harness
83
+ // is; it doesn't fit this registry's detection model. Revisit once it (or Kimi) publishes a real
84
+ // one-shot flag, or wire a custom command into config.json → runners by hand in the meantime.
76
85
  const REGISTRY = [
77
86
  {
78
87
  id: 'claude',
@@ -81,44 +90,58 @@ const REGISTRY = [
81
90
  { path: '.claude/commands/spectoflow.md', content: SLASH_CMD },
82
91
  ],
83
92
  runner: 'claude -p --permission-mode acceptEdits',
93
+ headless: true,
84
94
  detect: { bin: 'claude', dirs: ['.claude'] },
85
95
  },
86
96
  {
87
97
  id: 'codex',
88
98
  entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
89
99
  runner: 'codex exec',
100
+ headless: true,
90
101
  detect: { bin: 'codex', dirs: ['.codex'] },
91
102
  },
92
103
  {
93
104
  id: 'cursor',
94
105
  entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
95
106
  runner: 'cursor-agent -p',
107
+ headless: true,
96
108
  detect: { bin: 'cursor-agent', dirs: ['.cursor'] },
97
109
  },
98
110
  {
99
111
  id: 'gemini',
100
112
  entries: [{ path: 'GEMINI.md', content: GEMINI_MD }],
101
113
  runner: 'gemini -p',
114
+ headless: true,
102
115
  detect: { bin: 'gemini', dirs: ['.gemini'] },
103
116
  },
104
117
  {
105
118
  id: 'opencode',
106
119
  entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
107
120
  runner: 'opencode run --quiet',
121
+ headless: true,
108
122
  detect: { bin: 'opencode', dirs: ['.opencode'] },
109
123
  },
110
124
  {
111
125
  id: 'kiro',
112
126
  entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
113
127
  runner: 'kiro-cli chat --no-interactive --trust-all-tools',
128
+ headless: true,
114
129
  detect: { bin: 'kiro-cli', dirs: ['.kiro'] },
115
130
  },
116
131
  {
117
132
  id: 'antigravity',
118
133
  entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
119
134
  runner: 'agy -p',
135
+ headless: true,
120
136
  detect: { bin: 'agy' },
121
137
  },
138
+ {
139
+ id: 'kimi',
140
+ entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
141
+ runner: null, // no confirmed non-interactive one-shot mode — see the note above REGISTRY
142
+ headless: false,
143
+ detect: { bin: 'kimi', dirs: [] },
144
+ },
122
145
  ];
123
146
 
124
147
  const byId = (id) => REGISTRY.find((a) => a.id === id);
@@ -148,7 +171,7 @@ function defaultRunners(agents) {
148
171
  const out = {};
149
172
  for (const id of agents) {
150
173
  const a = byId(id);
151
- if (a) out[id] = a.runner;
174
+ if (a && a.runner) out[id] = a.runner; // headless:false agents (e.g. kimi) have no runner to seed
152
175
  }
153
176
  return out;
154
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -95,7 +95,8 @@ function setChat(open){
95
95
  $('#chat').setAttribute('aria-hidden', open?'false':'true');
96
96
  $('#chatFab').classList.toggle('is-open',open);
97
97
  try{ localStorage.setItem('spf-chat', open?'1':'0'); }catch{}
98
- if(open) setTimeout(()=>$('#runPrompt').focus(),60);
98
+ // Land where you can actually type, not wherever the log happened to be scrolled last.
99
+ if(open) setTimeout(()=>{ scrollChat($('#chatLog')); $('#runPrompt').focus(); },60);
99
100
  }
100
101
  // doRun/doOrchestrate default to the floating widget's textarea/select; the Chat tab has its own
101
102
  // #tabRunPrompt/#tabRunAgent (an id can't be shared by two elements) and passes them explicitly —
@@ -139,11 +140,10 @@ function render(){
139
140
  // in renderSettings() below (called on every render tick) and saved via saveSettings().
140
141
  // agent select — widget (#runAgent) and Chat tab (#tabRunAgent) each get their own populated
141
142
  // <select>, since an id can't be shared by two elements; same option list, same source of truth.
142
- const runners=Object.keys((c.runners)||{claude:1});
143
- [$('#runAgent'),$('#tabRunAgent')].forEach(sel=>{
144
- if(!sel) return;
145
- if(sel.options.length!==runners.length){ sel.innerHTML=''; runners.forEach(k=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); }); if(c.agent) sel.value=c.agent; }
146
- });
143
+ // Shows every known agent (not just ones already in config.json → runners — a freshly-installed
144
+ // agent works the first time, runner.js falls back to the registry default), disabling any that
145
+ // isn't genuinely installed or can't run headless (e.g. kimi).
146
+ [$('#runAgent'),$('#tabRunAgent')].forEach(sel=>fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),c.agent));
147
147
  const s=SpectoStats.stats(P);
148
148
  const meterFill=$('#globalMeterFill');
149
149
  if(meterFill) meterFill.style.width=(s.pct||0)+'%';
@@ -717,6 +717,12 @@ function setModeSelects(mode){ [$('#setMode'),$('#topMode')].forEach(s=>{ if(s)
717
717
  // the user activate an agent that isn't actually installed — options for a not-installed known agent
718
718
  // are present but disabled, and P.installedAgents itself comes from the server testing each agent's
719
719
  // real CLI (bin on PATH, or the project's own config dir), not a guess.
720
+ // ids that can actually be SPAWNED right now: installed AND headless-capable (a known agent like
721
+ // kimi can be "installed" and even the active agent, but nothing can run it non-interactively).
722
+ function runnableAgentIds(){
723
+ const installed=P.installedAgents||[];
724
+ return (P.knownAgents||[]).filter(a=>a.headless&&installed.includes(a.id)).map(a=>a.id);
725
+ }
720
726
  function fillAgentSelect(sel,known,installed,active){
721
727
  if(!sel) return;
722
728
  const empty=!installed.length;
@@ -936,9 +942,7 @@ function renderCustomize(){
936
942
  const form=el('div','cz-form');
937
943
  const ta=el('textarea','chat-ta'); ta.placeholder=t('customize.describePh');
938
944
  const sel=el('select','chat-agent');
939
- const runners=Object.keys((P.config&&P.config.runners)||{claude:1});
940
- runners.forEach((k)=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); });
941
- if(P.config&&P.config.agent) sel.value=P.config.agent;
945
+ fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),P.config&&P.config.agent);
942
946
  const actions=el('div','cz-form-actions');
943
947
  const autoBtn=el('button','btn',t('customize.auto'));
944
948
  const goBtn=el('button','btn primary',t('customize.generate'));
@@ -978,6 +982,10 @@ function navigateTab(tabId,push){
978
982
  }
979
983
  applyActiveTab();
980
984
  closeNav(); // a tab pick closes the mobile menu
985
+ // Landing on Chat: go straight to where you can read the latest and type, not wherever the log
986
+ // was scrolled to last (only on an actual switch INTO the tab — applyActiveTab() alone runs on
987
+ // every SSE render tick too, and re-scrolling/re-focusing there would fight the user's typing).
988
+ if(tabId==='chat') setTimeout(()=>{ scrollChat($('#chatTabLog')); $('#tabRunPrompt').focus(); },60);
981
989
  }
982
990
  function closeNav(){ document.body.classList.remove('nav-open'); const nt=$('#navToggle'); if(nt) nt.setAttribute('aria-expanded','false'); }
983
991
 
@@ -1297,6 +1305,8 @@ $('#chatClose').addEventListener('click',()=> setChat(false));
1297
1305
  try{ if(localStorage.getItem('spf-chat')==='1') setChat(true); }catch{}
1298
1306
  $('#runBtn').addEventListener('click',()=>doRun());
1299
1307
  $('#orchBtn').addEventListener('click',()=>doOrchestrate());
1308
+ $('#widgetSummarizeBtn').addEventListener('click',()=>summarizeChat($('#runAgent')));
1309
+ $('#widgetClearBtn').addEventListener('click',clearChat);
1300
1310
  $('#runPrompt').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')doRun(); });
1301
1311
  // Chat tab — same doRun/doOrchestrate/approve, its own textarea/select (#tabRunPrompt/#tabRunAgent)
1302
1312
  $('#tabRunBtn').addEventListener('click',()=>doRun($('#tabRunPrompt'),$('#tabRunAgent')));
@@ -201,16 +201,18 @@
201
201
  <h2 class="panel-title" data-i18n="nav.chat">Chat</h2>
202
202
  <p class="panel-sub" data-i18n="chat.tabSub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
203
203
  </div>
204
- <div class="chat-tab-head-actions">
205
- <button id="tabSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
206
- <button id="tabClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
207
- <select id="tabRunAgent" class="chat-agent" title="Agent"></select>
208
- </div>
209
204
  </div>
210
205
  <div class="chat-tab-log" id="chatTabLog">
211
206
  <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
212
207
  (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
213
208
  </div>
209
+ <!-- Toolbar right above the input, not up in the header — this is where you're looking
210
+ when you reach for Summarize/Clear or switch which agent the next message runs on. -->
211
+ <div class="chat-tab-toolbar">
212
+ <button id="tabSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
213
+ <button id="tabClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
214
+ <select id="tabRunAgent" class="chat-agent" title="Agent"></select>
215
+ </div>
214
216
  <div class="chat-tab-input">
215
217
  <textarea id="tabRunPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
216
218
  <div class="chat-tab-actions">
@@ -306,13 +308,17 @@
306
308
  <span class="chat-title" data-i18n="chat.widgetTitle">Run an agent</span>
307
309
  <span class="chat-sub" data-i18n="chat.widgetSub">Quick access · full view in the Chat tab</span>
308
310
  </div>
309
- <select id="runAgent" class="chat-agent" title="Agent"></select>
310
311
  <button class="chat-close" id="chatClose" aria-label="Close">×</button>
311
312
  </div>
312
313
  <div class="chat-log" id="chatLog">
313
314
  <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
314
315
  (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
315
316
  </div>
317
+ <div class="chat-toolbar">
318
+ <button id="widgetSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
319
+ <button id="widgetClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
320
+ <select id="runAgent" class="chat-agent" title="Agent"></select>
321
+ </div>
316
322
  <div class="chat-input">
317
323
  <textarea id="runPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
318
324
  <button id="runBtn" class="btn primary chat-send" data-i18n="action.send">Send</button>
@@ -378,7 +378,8 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
378
378
  .chat-title { font-weight:700; font-size:15px; }
379
379
  .chat-sub { font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.04em; }
380
380
  .chat-agent { margin-left:auto; flex-shrink:0; font:inherit; font-size:12px; background:var(--surface); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:5px 8px; }
381
- .chat-close { width:28px; height:28px; flex-shrink:0; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); border-radius:8px; font-size:17px; cursor:pointer; transition:border-color .15s,color .15s; }
381
+ .chat-toolbar { display:flex; align-items:center; gap:8px; padding:8px 14px; border-top:1px solid var(--line); flex-wrap:wrap; }
382
+ .chat-close { width:28px; height:28px; flex-shrink:0; margin-left:auto; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); border-radius:8px; font-size:17px; cursor:pointer; transition:border-color .15s,color .15s; }
382
383
  .chat-close:hover { border-color:color-mix(in srgb,var(--s-blocked) 45%,var(--line)); color:var(--ink); }
383
384
  .chat-log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:11px; }
384
385
  .chat-idle { color:var(--faint); font-style:italic; font-size:12.5px; }
@@ -414,10 +415,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
414
415
  .chat-tab-head { display:flex; align-items:flex-start; gap:14px; margin-bottom:14px; }
415
416
  .chat-tab-head .panel-title { margin:0; }
416
417
  .chat-tab-head .panel-sub { margin:2px 0 0; }
417
- .chat-tab-head-actions { display:flex; align-items:center; gap:8px; margin-left:auto; flex-shrink:0; flex-wrap:wrap; }
418
- .chat-tab-head .chat-agent { flex-shrink:0; }
419
418
  .chat-tab-log { flex:1; min-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:20px; box-shadow:var(--shadow); }
420
- .chat-tab-input { display:flex; gap:10px; margin-top:14px; }
419
+ .chat-tab-toolbar { display:flex; align-items:center; gap:8px; margin-top:10px; flex-wrap:wrap; }
420
+ .chat-tab-toolbar .chat-agent { margin-left:auto; flex-shrink:0; }
421
+ .chat-tab-input { display:flex; gap:10px; margin-top:10px; }
421
422
  .chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
422
423
  .chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
423
424
  .chat-tab-wrap .chat-warn { margin:8px 0 0; }
@@ -9,6 +9,18 @@
9
9
  */
10
10
  const { spawn } = require('child_process');
11
11
  const store = require('../lib/store');
12
+ const agentsRegistry = require('../lib/agents-registry');
13
+
14
+ // The command to run `which`: config.json's own runners map first (an explicit user choice always
15
+ // wins), falling back to the registry's default for a known, headless-capable, genuinely-installed
16
+ // agent — so picking one from a per-message agent list works the first time, before it's ever been
17
+ // the project's "active agent" and had a runner seeded into config.json for it.
18
+ function resolveRunnerCommand(root, cfg, which, opts) {
19
+ if (cfg.runners && cfg.runners[which]) return cfg.runners[which];
20
+ const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === which);
21
+ if (known && known.runner && agentsRegistry.isAgentInstalled(which, root, opts)) return known.runner;
22
+ return null;
23
+ }
12
24
 
13
25
  function runStart(root, run) {
14
26
  const rt = store.readRuntime(root); rt.agents = rt.agents || []; rt.agents.push(run); store.writeRuntime(root, rt);
@@ -50,7 +62,7 @@ function pushAttention(root, text, by) {
50
62
  function startRun(root, { prompt, agent, logPrompt = true }, emit) {
51
63
  const cfg = store.readConfig(root);
52
64
  const which = agent || cfg.agent || 'claude';
53
- const cmdStr = cfg.runners && cfg.runners[which];
65
+ const cmdStr = resolveRunnerCommand(root, cfg, which);
54
66
  if (!cmdStr) return { error: `No runner configured for "${which}".` };
55
67
  const parts = cmdStr.split(/\s+/).filter(Boolean);
56
68
  const runId = 'r' + Date.now().toString(36);
@@ -97,4 +109,4 @@ function startRun(root, { prompt, agent, logPrompt = true }, emit) {
97
109
  return { runId, child };
98
110
  }
99
111
 
100
- module.exports = { startRun };
112
+ module.exports = { startRun, resolveRunnerCommand };
@@ -35,7 +35,7 @@ function project(){
35
35
  // Known vs. actually-installed agents — the topbar switcher needs both: the full list to offer,
36
36
  // and which ones are real (bin on PATH, or the project already has that agent's config dir) so it
37
37
  // can refuse to activate one that isn't there.
38
- p.knownAgents = agentsRegistry.KNOWN_AGENTS.map((a) => ({ id: a.id, label: a.label }));
38
+ p.knownAgents = agentsRegistry.KNOWN_AGENTS.map((a) => ({ id: a.id, label: a.label, headless: a.headless }));
39
39
  p.installedAgents = agentsRegistry.installedAgents(ROOT);
40
40
  return p;
41
41
  }
@@ -62,8 +62,11 @@ function writeConfig(patch){
62
62
  }
63
63
  cfg.agent = id;
64
64
  // Seed a default runner if this agent was never configured (e.g. installed after init/update).
65
+ // A headless:false agent (e.g. kimi) has no runner to seed — it can still be the active agent,
66
+ // it just can't be spawned by Run/Orchestrate/Summarize (disabled client-side; runner.js and
67
+ // summarize.js also refuse server-side either way).
65
68
  const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === id);
66
- if (known) { cfg.runners = cfg.runners || {}; if (!cfg.runners[id]) cfg.runners[id] = known.runner; }
69
+ if (known && known.runner) { cfg.runners = cfg.runners || {}; if (!cfg.runners[id]) cfg.runners[id] = known.runner; }
67
70
  }
68
71
  fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
69
72
  return cfg;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  const { spawn } = require('child_process');
8
8
  const store = require('../lib/store');
9
+ const { resolveRunnerCommand } = require('./runner');
9
10
 
10
11
  const DEFAULT_LIMIT = 40;
11
12
 
@@ -20,7 +21,7 @@ function formatLog(messages, limit = DEFAULT_LIMIT) {
20
21
  function runSummarize(root, { agent } = {}, emit) {
21
22
  const cfg = store.readConfig(root);
22
23
  const which = agent || cfg.agent || 'claude';
23
- const cmdStr = cfg.runners && cfg.runners[which];
24
+ const cmdStr = resolveRunnerCommand(root, cfg, which);
24
25
  if (!cmdStr) return { error: `No runner configured for "${which}".` };
25
26
 
26
27
  const rt = store.readRuntime(root);
@@ -9,14 +9,19 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
 
12
+ // headless:false = detectable and selectable as the active agent, but spectoflow never spawns it
13
+ // itself (no confirmed non-interactive one-shot mode) — Run/Orchestrate/Summarize stay disabled for
14
+ // it client-side; runner/summarize.js also refuse server-side (defense in depth). See the longer
15
+ // rationale above lib/adapters.js's REGISTRY, the richer install-time twin of this list.
12
16
  const KNOWN_AGENTS = [
13
- { id: 'claude', label: 'Claude Code', bin: 'claude', dirs: ['.claude'], runner: 'claude -p --permission-mode acceptEdits' },
14
- { id: 'codex', label: 'Codex', bin: 'codex', dirs: ['.codex'], runner: 'codex exec' },
15
- { id: 'cursor', label: 'Cursor', bin: 'cursor-agent', dirs: ['.cursor'], runner: 'cursor-agent -p' },
16
- { id: 'gemini', label: 'Gemini CLI', bin: 'gemini', dirs: ['.gemini'], runner: 'gemini -p' },
17
- { id: 'opencode', label: 'OpenCode', bin: 'opencode', dirs: ['.opencode'], runner: 'opencode run --quiet' },
18
- { id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli', dirs: ['.kiro'], runner: 'kiro-cli chat --no-interactive --trust-all-tools' },
19
- { id: 'antigravity', label: 'Antigravity', bin: 'agy', dirs: [], runner: 'agy -p' },
17
+ { id: 'claude', label: 'Claude Code', bin: 'claude', dirs: ['.claude'], runner: 'claude -p --permission-mode acceptEdits', headless: true },
18
+ { id: 'codex', label: 'Codex', bin: 'codex', dirs: ['.codex'], runner: 'codex exec', headless: true },
19
+ { id: 'cursor', label: 'Cursor', bin: 'cursor-agent', dirs: ['.cursor'], runner: 'cursor-agent -p', headless: true },
20
+ { id: 'gemini', label: 'Gemini CLI', bin: 'gemini', dirs: ['.gemini'], runner: 'gemini -p', headless: true },
21
+ { id: 'opencode', label: 'OpenCode', bin: 'opencode', dirs: ['.opencode'], runner: 'opencode run --quiet', headless: true },
22
+ { id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli', dirs: ['.kiro'], runner: 'kiro-cli chat --no-interactive --trust-all-tools', headless: true },
23
+ { id: 'antigravity', label: 'Antigravity', bin: 'agy', dirs: [], runner: 'agy -p', headless: true },
24
+ { id: 'kimi', label: 'Kimi CLI', bin: 'kimi', dirs: [], runner: null, headless: false },
20
25
  ];
21
26
 
22
27
  // Is `bin` an executable resolvable on PATH? On win32, an extension from PATHEXT is required, so we