mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat(grok): add Grok Build (xAI) as a seventh CLI run mode
SessionMode gains 'grok', a first-class backend alongside Claude Code,
shell, OpenCode, Codex, Gemini, Antigravity and Pi: its own PTY, tmux
session, charcoal tab identity ('gk' badge), welcome button, run-mode
entry, cron agentType, Docker and remote-SSH command defaults, and
clone-repo Brain option. Flag surface verified live against grok 1.0.5.
Grok mixes two existing shapes and the wiring follows from that:
- Codex-shaped on permissions: the bypass switch is GrokConfig.alwaysApprove
(--always-approve, grok's bypassPermissions mode; config-level deny rules
still apply on top). The Run button sends it true, like runAntigravity(),
and clampExternalCliBypassForOwner() puts grok in the only-if-sent branch:
a bare grok spawn is grok's own ask-mode default, which is already safe,
so only a sent config needs the flag forced off. Cron needs nothing for
the same reason.
- OpenCode-shaped on rendering: grok is a fullscreen alternate-screen TUI
with mouse support, so it stays OUT of isAltScreenStripMode() and lands
on the narrow tmux-attach strip and the 'buffer' local-echo fallthrough
(unmeasured against an authenticated composer; documented fallback is the
'off' branch).
- Pi-shaped on resolution: 'grok' has npm squatters (@vibe-kit/grok-cli
also installs a grok bin), so grok-cli-resolver.ts version-probes every
candidate (grok --version, killSignal SIGKILL, VITEST-gated) and
GET /api/grok/status surfaces path AND version; GROK_VERSION_REGEX is
shared with the dependency registry so doctor and run mode cannot drift.
Env allowlist gains GROK_* plus the XAI_* vendor namespace (XAI_API_KEY is
grok's documented headless auth var), the same narrow-vendor reasoning as
GOOGLE_* for gemini. Resume is id-regexed on purpose: grok's own --resume
also matches session titles, which are arbitrary user strings that must
never reach the bash -c spawn line.
Docker: grok is not on npm, so the agent image installs it in its own step
(xAI's installer has no --dir override; the binary is copied to
/usr/local/bin and root's ~/.grok dropped in the same layer), and
credentials are seeded per-file (auth.json, config.toml, pager.toml; the
dir also holds sessions/, memory/ and the ~160MB binary). Remote SSH routes
through the login-shell wrapper like the other agent CLIs.
Verified end to end on an isolated CODEMAN_INSTANCE with grok 1.0.5
installed: /api/grok/status resolves and reports the probed version,
quick-start spawns a pane whose command line ends in 'grok
--always-approve', the real TUI renders (OAuth device screen on an
unauthenticated box), and grokConfig round-trips through state.json.
Docs: docs/grok-integration.md (user guide) + docs/grok-integration-plan.md
(decisions, verification record, follow-ups).
Tests: test/grok-mode.test.ts, test/grok-cli-resolver.test.ts, plus
extended clamp/system-routes/render-index-html/run-mode-ui/mobile-overview/
local-echo-gating coverage. npm test (the CI gate) green: 5910 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2243,9 +2243,11 @@ class CodemanApp {
|
||||
? 'Antigravity'
|
||||
: mode === 'pi'
|
||||
? 'Pi'
|
||||
: mode === 'opencode'
|
||||
? 'OpenCode'
|
||||
: 'Claude';
|
||||
: mode === 'grok'
|
||||
? 'Grok'
|
||||
: mode === 'opencode'
|
||||
? 'OpenCode'
|
||||
: 'Claude';
|
||||
}
|
||||
|
||||
async toggleResponseViewer() {
|
||||
@@ -4725,7 +4727,7 @@ class CodemanApp {
|
||||
<span class="tab-status ${status}" aria-hidden="true"></span>
|
||||
<span class="tab-info">
|
||||
<span class="tab-name-row">
|
||||
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : mode === 'gemini' ? '<span class="tab-mode gemini" aria-hidden="true">gm</span>' : mode === 'antigravity' ? '<span class="tab-mode antigravity" aria-hidden="true">ag</span>' : mode === 'pi' ? '<span class="tab-mode pi" aria-hidden="true">pi</span>' : ''}
|
||||
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : mode === 'opencode' ? '<span class="tab-mode opencode" aria-hidden="true">oc</span>' : mode === 'codex' ? '<span class="tab-mode codex" aria-hidden="true">cx</span>' : mode === 'gemini' ? '<span class="tab-mode gemini" aria-hidden="true">gm</span>' : mode === 'antigravity' ? '<span class="tab-mode antigravity" aria-hidden="true">ag</span>' : mode === 'pi' ? '<span class="tab-mode pi" aria-hidden="true">pi</span>' : mode === 'grok' ? '<span class="tab-mode grok" aria-hidden="true">gk</span>' : ''}
|
||||
<span class="tab-name" data-session-id="${id}">${escapeHtml(tabLabel)}</span>
|
||||
<span class="tab-detached-badge" aria-hidden="true">detached</span>
|
||||
</span>
|
||||
@@ -6154,7 +6156,9 @@ class CodemanApp {
|
||||
? 'Kill Tmux & Antigravity'
|
||||
: session.mode === 'pi'
|
||||
? 'Kill Tmux & Pi'
|
||||
: 'Kill Tmux & Claude Code';
|
||||
: session.mode === 'grok'
|
||||
? 'Kill Tmux & Grok'
|
||||
: 'Kill Tmux & Claude Code';
|
||||
}
|
||||
|
||||
document.getElementById('closeConfirmModal').classList.add('active');
|
||||
|
||||
@@ -78,6 +78,7 @@ const HOME_SESSIONS_MODE_BADGE = {
|
||||
gemini: 'gm',
|
||||
antigravity: 'ag',
|
||||
pi: 'pi',
|
||||
grok: 'gk',
|
||||
};
|
||||
|
||||
Object.assign(CodemanApp.prototype, {
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
'Run Gemini': '运行 Gemini',
|
||||
'Run Antigravity': '运行 Antigravity',
|
||||
'Run Pi': '运行 Pi',
|
||||
'Run Grok': '运行 Grok',
|
||||
'Run Shell': '运行 Shell',
|
||||
'Select AI backend': '选择 AI 后端',
|
||||
'Create New Case': '新建案例',
|
||||
|
||||
@@ -430,6 +430,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
Run Pi
|
||||
</button>
|
||||
<button class="welcome-btn welcome-btn-grok" id="welcomeGrokBtn" style="display: none;" onclick="app.setRunMode('grok'); app.runGrok()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
Run Grok
|
||||
</button>
|
||||
</div>
|
||||
<div class="welcome-qr" id="welcomeQr" onclick="app.toggleWelcomeQrSize()">
|
||||
<div class="welcome-qr-inner" id="welcomeQrInner"></div>
|
||||
@@ -607,6 +611,9 @@
|
||||
<button class="run-mode-option" data-mode="pi" onclick="app.setRunMode('pi')">
|
||||
<span class="run-mode-dot pi"></span>Pi
|
||||
</button>
|
||||
<button class="run-mode-option" data-mode="grok" onclick="app.setRunMode('grok')">
|
||||
<span class="run-mode-dot grok"></span>Grok
|
||||
</button>
|
||||
<div class="run-mode-sep"></div>
|
||||
<button class="run-mode-option" data-mode="shell" onclick="app.setRunMode('shell')">
|
||||
<span class="run-mode-dot shell"></span>Terminal / Shell
|
||||
@@ -884,6 +891,7 @@
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="antigravity">Antigravity</option>
|
||||
<option value="pi">Pi</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row"><label>Working Directory</label><input type="text" id="schWorkingDir" placeholder="/absolute/path"></div>
|
||||
@@ -2616,6 +2624,7 @@
|
||||
<option value="opencode" data-cli="opencode">OpenCode</option>
|
||||
<option value="antigravity" data-cli="antigravity">Antigravity</option>
|
||||
<option value="pi" data-cli="pi">Pi</option>
|
||||
<option value="grok" data-cli="grok">Grok</option>
|
||||
<option value="shell">Shell (no agent)</option>
|
||||
</select>
|
||||
<span class="form-hint">Which CLI to point the Run button at once the clone finishes. Changeable any time from the Run dropdown.</span>
|
||||
@@ -2756,7 +2765,7 @@
|
||||
<div class="form-row">
|
||||
<label>Image</label>
|
||||
<input type="text" id="dockerImage" placeholder="codeman/agent:base" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/codex/gemini/opencode/agy/pi + tmux.</span>
|
||||
<span class="form-hint">Build it once with <code>node scripts/build-agent-image.mjs</code>. Contains node + claude/codex/gemini/opencode/agy/pi/grok + tmux.</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Network</label>
|
||||
|
||||
@@ -54,6 +54,7 @@ const MOBILE_OVERVIEW_RUN_MODES = [
|
||||
{ mode: 'gemini', label: 'Gemini', short: 'Gemini' },
|
||||
{ mode: 'antigravity', label: 'Antigravity', short: 'Antigravity' },
|
||||
{ mode: 'pi', label: 'Pi', short: 'Pi' },
|
||||
{ mode: 'grok', label: 'Grok', short: 'Grok' },
|
||||
{ mode: 'shell', label: 'Terminal / Shell', short: 'Shell' },
|
||||
];
|
||||
|
||||
|
||||
@@ -972,6 +972,20 @@ html.mobile-init .file-browser-panel {
|
||||
border-color: rgba(244, 114, 182, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Grok mode colors on mobile. Same `!important` rationale as the pi block above. */
|
||||
.btn-toolbar.btn-run.mode-grok,
|
||||
.btn-toolbar.btn-run-gear.mode-grok {
|
||||
background: #1c1c1f !important;
|
||||
border-color: rgba(212, 212, 216, 0.3) !important;
|
||||
color: #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.btn-toolbar.btn-run.mode-grok:active,
|
||||
.btn-toolbar.btn-run-gear.mode-grok:active {
|
||||
background: #3f3f46 !important;
|
||||
border-color: rgba(212, 212, 216, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Run mode dropdown menu — positioned above toolbar on mobile */
|
||||
.run-mode-menu {
|
||||
bottom: 100%;
|
||||
@@ -3055,6 +3069,12 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-toolbar.btn-run.mode-grok, .btn-toolbar.btn-run-gear.mode-grok) {
|
||||
background: linear-gradient(135deg, #27272a, #52525b);
|
||||
border-color: #18181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) .btn-toolbar.btn-run-gear {
|
||||
border-left-color: var(--control-border-hover) !important;
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
_buildCommandPaletteNewSessionItem(query = '') {
|
||||
const mode = this.runMode || this._runMode || 'claude';
|
||||
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi' };
|
||||
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok' };
|
||||
const caseName = this._findCommandPaletteCaseMatch(query) || document.getElementById('quickStartCase')?.value || 'testcase';
|
||||
return {
|
||||
id: 'new-session',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @fileoverview Quick start (case loading, session spawning for Claude/Shell/OpenCode/Codex/Gemini/Antigravity/Pi),
|
||||
* @fileoverview Quick start (case loading, session spawning for Claude/Shell/OpenCode/Codex/Gemini/Antigravity/Pi/Grok),
|
||||
* session options modal (per-session settings, color picker, rename),
|
||||
* session options tabs (Ralph config tab), case settings (CRUD, links),
|
||||
* create case modal, and mobile case picker.
|
||||
@@ -403,6 +403,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (mode === 'pi') {
|
||||
return await this.runPi();
|
||||
}
|
||||
if (mode === 'grok') {
|
||||
return await this.runGrok();
|
||||
}
|
||||
if (mode === 'shell') {
|
||||
return await this.runShell();
|
||||
}
|
||||
@@ -468,7 +471,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
* run modes like the rest, and neither `agy` nor `pi` is likely to be installed.
|
||||
*/
|
||||
_refreshRunModeAvailability(menu) {
|
||||
for (const mode of ['claude', 'opencode', 'codex', 'gemini', 'antigravity', 'pi']) {
|
||||
for (const mode of ['claude', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok']) {
|
||||
const btn = menu.querySelector(`.run-mode-option[data-mode="${mode}"]`);
|
||||
if (btn) btn.style.display = this.isCliAvailable(mode) ? 'flex' : 'none';
|
||||
}
|
||||
@@ -565,7 +568,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
gearBtn.className = `btn-toolbar btn-run-gear mode-${mode}`;
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : mode === 'antigravity' ? 'Run AG' : mode === 'pi' ? 'Run PI' : mode === 'shell' ? 'Run SH' : 'Run';
|
||||
label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : mode === 'antigravity' ? 'Run AG' : mode === 'pi' ? 'Run PI' : mode === 'grok' ? 'Run GK' : mode === 'shell' ? 'Run SH' : 'Run';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1278,6 +1281,66 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Launch a Grok Build (xAI `grok`) session.
|
||||
*
|
||||
* Sends `grokConfig: { alwaysApprove: true }` the way runAntigravity() sends
|
||||
* `dangerouslySkipPermissions: true`: Codeman sessions exist for autonomous
|
||||
* work, so the Run button opts into grok's bypassPermissions mode
|
||||
* (`--always-approve`; config-level deny rules still apply on top). The
|
||||
* multi-user clamp forces it back off for non-granted owners server-side.
|
||||
*/
|
||||
async runGrok() {
|
||||
const caseName = document.getElementById('quickStartCase').value || 'testcase';
|
||||
// Remote/docker cases run grok on the OTHER side: skip the local status probe and the
|
||||
// local-only config/env below (quick-start rejects them for remote cases).
|
||||
const _runLoc = (this.cases || []).find(c => c.name === caseName)?.location;
|
||||
const isRemote = _runLoc === 'remote' || _runLoc === 'docker';
|
||||
|
||||
const ownsLaunchTerminal = this._beginSessionLaunchStatus(`Starting Grok session in ${caseName}...`);
|
||||
this.terminal.focus();
|
||||
|
||||
try {
|
||||
if (!isRemote) {
|
||||
const statusRes = await fetch('/api/grok/status');
|
||||
const status = (await statusRes.json()).data;
|
||||
if (!status.available) {
|
||||
this._reportSessionLaunchError(
|
||||
ownsLaunchTerminal,
|
||||
'Grok CLI not found. Install with: curl -fsSL https://x.ai/cli/install.sh | bash'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
|
||||
const res = await fetch('/api/quick-start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
caseName,
|
||||
mode: 'grok',
|
||||
sessionName: `w${this._nextCaseSessionStartNumber(caseName)}-${caseName}`,
|
||||
...(isRemote ? {} : {
|
||||
grokConfig: { alwaysApprove: true },
|
||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||
}),
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.error || 'Failed to start Grok');
|
||||
await this._ensureCreatedSessionVisible(data.data.sessionId, data.data.session);
|
||||
|
||||
if (data.data.sessionId) {
|
||||
await this.selectSession(data.data.sessionId);
|
||||
}
|
||||
|
||||
this.terminal.focus();
|
||||
} catch (err) {
|
||||
this._reportSessionLaunchError(ownsLaunchTerminal, err.message);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Session Options Modal
|
||||
@@ -1343,7 +1406,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId);
|
||||
|
||||
// Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only)
|
||||
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi';
|
||||
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok';
|
||||
this.switchOptionsTab(isAltMode ? 'summary' : 'respawn');
|
||||
|
||||
// Update respawn status display and buttons
|
||||
@@ -1373,7 +1436,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
// Hide Claude-specific options for external CLI sessions
|
||||
const isExternalCli = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi';
|
||||
const isExternalCli = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi' || session.mode === 'grok';
|
||||
const claudeOnlyEls = document.querySelectorAll('[data-claude-only]');
|
||||
claudeOnlyEls.forEach(el => { el.style.display = isExternalCli ? 'none' : ''; });
|
||||
|
||||
@@ -3114,7 +3177,7 @@ Object.defineProperty(CodemanApp.prototype, 'runMode', {
|
||||
},
|
||||
set(mode) {
|
||||
this._runMode =
|
||||
mode === 'opencode' || mode === 'codex' || mode === 'gemini' || mode === 'antigravity' || mode === 'pi' || mode === 'claude'
|
||||
mode === 'opencode' || mode === 'codex' || mode === 'gemini' || mode === 'antigravity' || mode === 'pi' || mode === 'grok' || mode === 'claude'
|
||||
? mode
|
||||
: 'claude';
|
||||
},
|
||||
|
||||
@@ -1199,6 +1199,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
['welcomeAntigravityBtn', 'antigravity'],
|
||||
['welcomeGeminiBtn', 'gemini'],
|
||||
['welcomePiBtn', 'pi'],
|
||||
['welcomeGrokBtn', 'grok'],
|
||||
// Not a run mode, same reasoning: offering a Cloudflare Tunnel on a box
|
||||
// without cloudflared can only ever produce "cloudflared not found".
|
||||
['welcomeTunnelBtn', 'cloudflared'],
|
||||
|
||||
@@ -347,7 +347,8 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
.history-view-all-btn,
|
||||
.session-tab .tab-mode.gemini,
|
||||
.session-tab .tab-mode.antigravity,
|
||||
.session-tab .tab-mode.pi
|
||||
.session-tab .tab-mode.pi,
|
||||
.session-tab .tab-mode.grok
|
||||
) {
|
||||
color: var(--accent-d);
|
||||
}
|
||||
@@ -2246,6 +2247,11 @@ body.solo-mode .btn-lifecycle-log {
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
.session-tab .tab-mode.grok {
|
||||
background: rgba(212, 212, 216, 0.18);
|
||||
color: #d4d4d8;
|
||||
}
|
||||
|
||||
/* Timer Banner - Compact */
|
||||
.timer-banner {
|
||||
display: flex;
|
||||
@@ -3610,6 +3616,23 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Grok (xAI): monochrome charcoal identity, matching .btn-toolbar.btn-run.mode-grok
|
||||
and .run-mode-dot.grok so the welcome action reads as the same backend. */
|
||||
.welcome-btn-grok {
|
||||
background: linear-gradient(135deg, #131316 0%, #27272a 55%, #3f3f46 100%);
|
||||
border-color: rgba(212, 212, 216, 0.4);
|
||||
color: #f4f4f5;
|
||||
box-shadow: 0 2px 8px rgba(212, 212, 216, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.welcome-btn-grok:hover {
|
||||
background: linear-gradient(135deg, #1f1f23 0%, #3f3f46 55%, #52525b 100%);
|
||||
box-shadow: 0 4px 20px rgba(212, 212, 216, 0.22), 0 0 40px rgba(161, 161, 170, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(228, 228, 231, 0.5);
|
||||
color: #fafafa;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.welcome-btn-gemini {
|
||||
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
|
||||
border-color: rgba(96, 165, 250, 0.4);
|
||||
@@ -4684,6 +4707,25 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
color: #fff1f7;
|
||||
}
|
||||
|
||||
/* Grok mode colors. Same cascade note as pi above: this base-sheet pair only
|
||||
renders on the `og` skin — the nested `html:not([data-skin="og"])` block
|
||||
re-declares `.btn-toolbar.btn-run` at a HIGHER specificity, so grok also
|
||||
carries a rule inside that block (search `.btn-toolbar.btn-run.mode-grok`). */
|
||||
.btn-toolbar.btn-run.mode-grok,
|
||||
.btn-toolbar.btn-run-gear.mode-grok {
|
||||
background: linear-gradient(135deg, #131316 0%, #27272a 55%, #3f3f46 100%);
|
||||
border-color: rgba(212, 212, 216, 0.5);
|
||||
color: #f4f4f5;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.btn-toolbar.btn-run.mode-grok:hover,
|
||||
.btn-toolbar.btn-run-gear.mode-grok:hover {
|
||||
background: linear-gradient(135deg, #1f1f23 0%, #3f3f46 55%, #52525b 100%);
|
||||
box-shadow: 0 0 12px rgba(212, 212, 216, 0.28), 0 2px 8px rgba(63, 63, 70, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(228, 228, 231, 0.6);
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
/* Dropdown menu */
|
||||
.run-mode-menu {
|
||||
display: none;
|
||||
@@ -4767,6 +4809,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
||||
.run-mode-dot.gemini { background: #8ab4f8; }
|
||||
.run-mode-dot.antigravity { background: #22d3ee; }
|
||||
.run-mode-dot.pi { background: #f472b6; }
|
||||
.run-mode-dot.grok { background: #a1a1aa; }
|
||||
.run-mode-dot.shell { background: #94a3b8; }
|
||||
|
||||
/* Phone-only Enter button (see index.html). Hidden by default at every width;
|
||||
@@ -14102,6 +14145,15 @@ html:not([data-skin="og"]) {
|
||||
color: #fff1f7;
|
||||
}
|
||||
.btn-toolbar.btn-run.mode-pi:hover { box-shadow: 0 0 14px -2px rgba(244, 114, 182, 0.45); }
|
||||
/* Grok keeps its charcoal identity on the non-og skins. Same specificity trap
|
||||
as pi above: without this rule the generic `.btn-toolbar.btn-run` in this
|
||||
nested block wins and grok renders as generic claude blue. */
|
||||
.btn-toolbar.btn-run.mode-grok {
|
||||
background: linear-gradient(135deg, #27272a, #52525b);
|
||||
border-color: #18181b;
|
||||
color: #fafafa;
|
||||
}
|
||||
.btn-toolbar.btn-run.mode-grok:hover { box-shadow: 0 0 14px -2px rgba(161, 161, 170, 0.5); }
|
||||
.btn-toolbar.btn-run-gear {
|
||||
background: var(--accent-d);
|
||||
border-color: var(--accent);
|
||||
|
||||
@@ -2213,7 +2213,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
titleSpan.appendChild(document.createTextNode(this._historyRowLabel(s, shortDir)));
|
||||
|
||||
// Badge row: mode (claude/codex/opencode/gemini/antigravity/pi/shell) + a LIVE pill.
|
||||
// Badge row: mode (claude/codex/opencode/gemini/antigravity/pi/grok/shell) + a LIVE pill.
|
||||
const badgeRow = document.createElement('div');
|
||||
badgeRow.className = 'history-item-badges';
|
||||
if (s.mode) {
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface ResponseViewerTranscriptBlock {
|
||||
// Keep in lockstep with isExternalCliMode() in src/session.ts. Importing it here
|
||||
// would drag node-pty and the whole session layer into this pure module, so the
|
||||
// list is duplicated and test/response-viewer-transcript.test.ts pins the parity.
|
||||
const EXTERNAL_CLI_MODES = new Set(['codex', 'gemini', 'opencode', 'antigravity', 'pi']);
|
||||
const EXTERNAL_CLI_MODES = new Set(['codex', 'gemini', 'opencode', 'antigravity', 'pi', 'grok']);
|
||||
|
||||
function isPromptLine(line: string): boolean {
|
||||
return /^\s*›\s*/.test(line);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type GeminiConfig,
|
||||
type AntigravityConfig,
|
||||
type PiConfig,
|
||||
type GrokConfig,
|
||||
} from '../../types.js';
|
||||
import { Session, isAltScreenStripMode, isMuxAltScreenOnlyStripMode } from '../../session.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
@@ -331,21 +332,27 @@ export function _resetPasteRateBuckets(): void {
|
||||
* session user could simply answer "yes" to in the terminal, so merely omitting
|
||||
* `--approve` is not a clamp. Forcing `approveProjectTrust: false` makes
|
||||
* buildPiCommand emit `--no-approve`, and the prompt never appears.
|
||||
*
|
||||
* Grok is like Codex/Antigravity: the bypass switch is `alwaysApprove`
|
||||
* (`--always-approve`), and an ABSENT config already spawns in grok's own
|
||||
* ask-mode default, so only a sent config needs the flag forced off.
|
||||
*/
|
||||
async function clampExternalCliBypassForOwner(
|
||||
owner: string | undefined,
|
||||
codexConfig: CodexConfig | undefined,
|
||||
geminiConfig: GeminiConfig | undefined,
|
||||
antigravityConfig: AntigravityConfig | undefined,
|
||||
piConfig: PiConfig | undefined
|
||||
piConfig: PiConfig | undefined,
|
||||
grokConfig: GrokConfig | undefined
|
||||
): Promise<{
|
||||
codexConfig: CodexConfig | undefined;
|
||||
geminiConfig: GeminiConfig | undefined;
|
||||
antigravityConfig: AntigravityConfig | undefined;
|
||||
piConfig: PiConfig | undefined;
|
||||
grokConfig: GrokConfig | undefined;
|
||||
}> {
|
||||
const granted = await canUsernameRunPrivilegedCommands(owner);
|
||||
if (granted) return { codexConfig, geminiConfig, antigravityConfig, piConfig };
|
||||
if (granted) return { codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig };
|
||||
// Non-granted: force codex/antigravity bypass off (only meaningful when a config was
|
||||
// sent) and materialize gemini to auto_edit (clamps an explicit 'yolo' and the yolo default)
|
||||
// and pi to --no-approve (clamps an explicit true AND pi's own "ask" default).
|
||||
@@ -355,11 +362,13 @@ async function clampExternalCliBypassForOwner(
|
||||
? { ...antigravityConfig, dangerouslySkipPermissions: false }
|
||||
: antigravityConfig;
|
||||
const clampedPi: PiConfig = { ...(piConfig ?? {}), approveProjectTrust: false };
|
||||
const clampedGrok = grokConfig ? { ...grokConfig, alwaysApprove: false } : grokConfig;
|
||||
return {
|
||||
codexConfig: clampedCodex,
|
||||
geminiConfig: clampedGemini,
|
||||
antigravityConfig: clampedAntigravity,
|
||||
piConfig: clampedPi,
|
||||
grokConfig: clampedGrok,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -752,6 +761,7 @@ export function registerSessionRoutes(
|
||||
body.mode !== 'gemini' &&
|
||||
body.mode !== 'antigravity' &&
|
||||
body.mode !== 'pi' &&
|
||||
body.mode !== 'grok' &&
|
||||
body.envOverrides &&
|
||||
Object.keys(body.envOverrides).length > 0 &&
|
||||
(workingDir.startsWith(CASES_DIR + '/') || workingDir.startsWith(managedCasesBase + '/'));
|
||||
@@ -841,6 +851,12 @@ export function registerSessionRoutes(
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getPiNotFoundMessage());
|
||||
}
|
||||
}
|
||||
if (body.mode === 'grok') {
|
||||
const { isGrokAvailable, getGrokNotFoundMessage } = await import('../../utils/grok-cli-resolver.js');
|
||||
if (!isGrokAvailable()) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getGrokNotFoundMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-validate resumeSessionId: check that the conversation file actually exists
|
||||
// in Claude's projects directory. If not, skip resume to avoid confusing
|
||||
@@ -886,9 +902,11 @@ export function registerSessionRoutes(
|
||||
? body.antigravityConfig?.model
|
||||
: mode === 'pi'
|
||||
? body.piConfig?.model
|
||||
: mode !== 'shell'
|
||||
? modelConfig?.defaultModel || undefined
|
||||
: undefined;
|
||||
: mode === 'grok'
|
||||
? body.grokConfig?.model
|
||||
: mode !== 'shell'
|
||||
? modelConfig?.defaultModel || undefined
|
||||
: undefined;
|
||||
const claudeModeConfig = await ctx.getClaudeModeConfig();
|
||||
// Section 6.3: force non-granted users to a classifier-guarded mode.
|
||||
const effectiveClaudeMode = await resolveClaudeModeForUsername(claudeModeConfig.claudeMode, owner);
|
||||
@@ -898,12 +916,14 @@ export function registerSessionRoutes(
|
||||
geminiConfig: gatedGeminiConfig,
|
||||
antigravityConfig: gatedAntigravityConfig,
|
||||
piConfig: gatedPiConfig,
|
||||
grokConfig: gatedGrokConfig,
|
||||
} = await clampExternalCliBypassForOwner(
|
||||
owner,
|
||||
body.codexConfig,
|
||||
body.geminiConfig,
|
||||
body.antigravityConfig,
|
||||
body.piConfig
|
||||
body.piConfig,
|
||||
body.grokConfig
|
||||
);
|
||||
const terminalHistoryConfig = await ctx.getTerminalHistoryConfig();
|
||||
const session = new Session({
|
||||
@@ -921,6 +941,7 @@ export function registerSessionRoutes(
|
||||
geminiConfig: mode === 'gemini' ? gatedGeminiConfig : undefined,
|
||||
antigravityConfig: mode === 'antigravity' ? gatedAntigravityConfig : undefined,
|
||||
piConfig: mode === 'pi' ? gatedPiConfig : undefined,
|
||||
grokConfig: mode === 'grok' ? gatedGrokConfig : undefined,
|
||||
resumeSessionId: validatedResumeId,
|
||||
envOverrides: body.envOverrides,
|
||||
effort: body.effort,
|
||||
@@ -1152,6 +1173,7 @@ export function registerSessionRoutes(
|
||||
session.mode !== 'gemini' &&
|
||||
session.mode !== 'antigravity' &&
|
||||
session.mode !== 'pi' &&
|
||||
session.mode !== 'grok' &&
|
||||
ctx.store.getConfig().ralphEnabled &&
|
||||
!session.ralphTracker.autoEnableDisabled
|
||||
) {
|
||||
@@ -2686,6 +2708,7 @@ export function registerSessionRoutes(
|
||||
geminiConfig,
|
||||
antigravityConfig,
|
||||
piConfig,
|
||||
grokConfig,
|
||||
envOverrides,
|
||||
effort,
|
||||
parentSessionId,
|
||||
@@ -2734,6 +2757,7 @@ export function registerSessionRoutes(
|
||||
geminiConfig ||
|
||||
antigravityConfig ||
|
||||
piConfig ||
|
||||
grokConfig ||
|
||||
openCodeConfig
|
||||
) {
|
||||
return createErrorResponse(
|
||||
@@ -2766,6 +2790,7 @@ export function registerSessionRoutes(
|
||||
geminiConfig ||
|
||||
antigravityConfig ||
|
||||
piConfig ||
|
||||
grokConfig ||
|
||||
openCodeConfig
|
||||
) {
|
||||
return createErrorResponse(
|
||||
@@ -2868,6 +2893,14 @@ export function registerSessionRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// Check Grok availability if requested
|
||||
if (mode === 'grok') {
|
||||
const { isGrokAvailable, getGrokNotFoundMessage } = await import('../../utils/grok-cli-resolver.js');
|
||||
if (!isGrokAvailable()) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getGrokNotFoundMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve case path: check linked-cases registry first, then fall back to CASES_DIR.
|
||||
// This mirrors the behaviour of resolveCasePath() in case-routes so that linked
|
||||
// external project directories are honoured by quick-start just like regular case routes.
|
||||
@@ -2914,8 +2947,15 @@ export function registerSessionRoutes(
|
||||
writeFileSync(join(resolvedCasePath, 'CLAUDE.md'), claudeMd);
|
||||
|
||||
// Write .claude/settings.local.json with hooks for desktop notifications
|
||||
// (Claude-specific — OpenCode, Codex, Gemini, and Antigravity use their own systems)
|
||||
if (mode !== 'opencode' && mode !== 'codex' && mode !== 'gemini' && mode !== 'antigravity' && mode !== 'pi') {
|
||||
// (Claude-specific — OpenCode, Codex, Gemini, Antigravity, Pi and Grok use their own systems)
|
||||
if (
|
||||
mode !== 'opencode' &&
|
||||
mode !== 'codex' &&
|
||||
mode !== 'gemini' &&
|
||||
mode !== 'antigravity' &&
|
||||
mode !== 'pi' &&
|
||||
mode !== 'grok'
|
||||
) {
|
||||
await writeHooksConfig(resolvedCasePath);
|
||||
}
|
||||
|
||||
@@ -2987,6 +3027,7 @@ export function registerSessionRoutes(
|
||||
mode !== 'gemini' &&
|
||||
mode !== 'antigravity' &&
|
||||
mode !== 'pi' &&
|
||||
mode !== 'grok' &&
|
||||
!remote &&
|
||||
envOverrides &&
|
||||
Object.keys(envOverrides).length > 0
|
||||
@@ -3009,9 +3050,11 @@ export function registerSessionRoutes(
|
||||
? antigravityConfig?.model
|
||||
: mode === 'pi'
|
||||
? piConfig?.model
|
||||
: mode !== 'shell'
|
||||
? qsModelConfig?.defaultModel || undefined
|
||||
: undefined;
|
||||
: mode === 'grok'
|
||||
? grokConfig?.model
|
||||
: mode !== 'shell'
|
||||
? qsModelConfig?.defaultModel || undefined
|
||||
: undefined;
|
||||
const qsClaudeModeConfig = await ctx.getClaudeModeConfig();
|
||||
const qsEffectiveClaudeMode = await resolveClaudeModeForUsername(qsClaudeModeConfig.claudeMode, owner);
|
||||
// Section 6.3: clamp Codex/Gemini/Antigravity bypass switches for a non-granted owner (no-op single-user/granted).
|
||||
@@ -3020,7 +3063,8 @@ export function registerSessionRoutes(
|
||||
geminiConfig: qsGatedGeminiConfig,
|
||||
antigravityConfig: qsGatedAntigravityConfig,
|
||||
piConfig: qsGatedPiConfig,
|
||||
} = await clampExternalCliBypassForOwner(owner, codexConfig, geminiConfig, antigravityConfig, piConfig);
|
||||
grokConfig: qsGatedGrokConfig,
|
||||
} = await clampExternalCliBypassForOwner(owner, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig);
|
||||
const qsTerminalHistoryConfig = await ctx.getTerminalHistoryConfig();
|
||||
const session = new Session({
|
||||
workingDir: resolvedCasePath,
|
||||
@@ -3038,6 +3082,7 @@ export function registerSessionRoutes(
|
||||
geminiConfig: mode === 'gemini' ? qsGatedGeminiConfig : undefined,
|
||||
antigravityConfig: mode === 'antigravity' ? qsGatedAntigravityConfig : undefined,
|
||||
piConfig: mode === 'pi' ? qsGatedPiConfig : undefined,
|
||||
grokConfig: mode === 'grok' ? qsGatedGrokConfig : undefined,
|
||||
envOverrides,
|
||||
effort,
|
||||
remote,
|
||||
@@ -3088,7 +3133,7 @@ export function registerSessionRoutes(
|
||||
});
|
||||
ctx.broadcast(SseEvent.SessionInteractive, { id: session.id, mode: 'shell' });
|
||||
} else {
|
||||
// 'claude', 'opencode', 'codex', 'gemini', and 'antigravity' modes use startInteractive()
|
||||
// every non-shell mode ('claude', the external CLIs) uses startInteractive()
|
||||
await session.startInteractive();
|
||||
getLifecycleLog().log({
|
||||
event: 'started',
|
||||
|
||||
@@ -380,7 +380,7 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// CLI Integrations (Claude, OpenCode, Codex, Gemini, Antigravity, Pi)
|
||||
// CLI Integrations (Claude, OpenCode, Codex, Gemini, Antigravity, Pi, Grok)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// ========== Claude ==========
|
||||
@@ -446,6 +446,21 @@ export function registerSystemRoutes(
|
||||
};
|
||||
});
|
||||
|
||||
// ========== Grok ==========
|
||||
|
||||
// Carries `version` on top of the sibling shape, same reason as pi: `grok` is a
|
||||
// binary name with known squatters, so the resolver version-probes candidates and
|
||||
// this endpoint is where a misresolution shows up (path + version) instead of
|
||||
// presenting as "the mode just doesn't work".
|
||||
app.get('/api/grok/status', async () => {
|
||||
const { isGrokAvailable, resolveGrokDir, getGrokCliVersion } = await import('../../utils/grok-cli-resolver.js');
|
||||
return {
|
||||
available: isGrokAvailable(),
|
||||
path: resolveGrokDir(),
|
||||
version: getGrokCliVersion(),
|
||||
};
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// State & Lifecycle (cleanup, lifecycle log, stats)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
+44
-5
@@ -122,7 +122,17 @@ export const FileWriteSchema = z
|
||||
// ========== Env Var Allowlist ==========
|
||||
|
||||
/** Allowlisted env var key prefixes */
|
||||
const ALLOWED_ENV_PREFIXES = ['CLAUDE_CODE_', 'OPENCODE_', 'CODEX_', 'GEMINI_', 'GOOGLE_', 'ANTIGRAVITY_', 'PI_'];
|
||||
const ALLOWED_ENV_PREFIXES = [
|
||||
'CLAUDE_CODE_',
|
||||
'OPENCODE_',
|
||||
'CODEX_',
|
||||
'GEMINI_',
|
||||
'GOOGLE_',
|
||||
'ANTIGRAVITY_',
|
||||
'PI_',
|
||||
'GROK_',
|
||||
'XAI_',
|
||||
];
|
||||
|
||||
/**
|
||||
* Allowlisted exact env var keys (checked alongside the prefixes).
|
||||
@@ -161,7 +171,7 @@ const safeEnvOverridesSchema = z
|
||||
},
|
||||
{
|
||||
message:
|
||||
'envOverrides contains blocked or disallowed env var keys. Only CLAUDE_CODE_*, OPENCODE_*, CODEX_*, GEMINI_*, GOOGLE_*, ANTIGRAVITY_*, PI_* keys and CLAUDE_CONFIG_DIR are allowed.',
|
||||
'envOverrides contains blocked or disallowed env var keys. Only CLAUDE_CODE_*, OPENCODE_*, CODEX_*, GEMINI_*, GOOGLE_*, ANTIGRAVITY_*, PI_*, GROK_*, XAI_* keys and CLAUDE_CONFIG_DIR are allowed.',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -300,6 +310,32 @@ const PiConfigSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* Schema for Grok Build CLI (xAI `grok`)-specific configuration.
|
||||
*
|
||||
* `alwaysApprove` maps to `--always-approve` (grok's bypassPermissions mode).
|
||||
* An ABSENT config spawns bare `grok` = grok's own ask-mode default, so the
|
||||
* multi-user clamp only needs the only-if-sent branch (like codex/antigravity).
|
||||
*/
|
||||
const GrokConfigSchema = z
|
||||
.object({
|
||||
model: z
|
||||
.string()
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9._\-/]+$/)
|
||||
.optional(),
|
||||
alwaysApprove: z.boolean().optional(),
|
||||
continueSession: z.boolean().optional(),
|
||||
// Ids only: grok's --resume also matches session TITLES (arbitrary user
|
||||
// strings), which this regex deliberately cannot express.
|
||||
resumeSessionId: z
|
||||
.string()
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9._-]+$/)
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
/**
|
||||
* The session that spawned the one being created — pure UI decoration, drawn as a
|
||||
* lineage line between the two tabs. Accepted here and, equivalently, as the
|
||||
@@ -313,7 +349,7 @@ const parentSessionIdSchema = z.string().max(100).optional();
|
||||
|
||||
export const CreateSessionSchema = z.object({
|
||||
workingDir: safePathSchema.optional(),
|
||||
mode: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi']).optional(),
|
||||
mode: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok']).optional(),
|
||||
name: z.string().max(100).optional(),
|
||||
/** Session that spawned this one — see parentSessionIdSchema. */
|
||||
parentSessionId: parentSessionIdSchema,
|
||||
@@ -329,6 +365,7 @@ export const CreateSessionSchema = z.object({
|
||||
geminiConfig: GeminiConfigSchema,
|
||||
antigravityConfig: AntigravityConfigSchema,
|
||||
piConfig: PiConfigSchema,
|
||||
grokConfig: GrokConfigSchema,
|
||||
/** Resume a previous Claude conversation by its session ID (used for reboot recovery) */
|
||||
resumeSessionId: z
|
||||
.string()
|
||||
@@ -464,6 +501,7 @@ const RemoteCommandOverridesSchema = z
|
||||
gemini: z.string().min(1).max(300).optional(),
|
||||
antigravity: z.string().min(1).max(300).optional(),
|
||||
pi: z.string().min(1).max(300).optional(),
|
||||
grok: z.string().min(1).max(300).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
@@ -738,12 +776,13 @@ export const QuickStartSchema = z.object({
|
||||
* a real host dir, so the settings file crosses the bind mount); rejected for
|
||||
* remote cases (the file would be written on the WRONG machine). */
|
||||
modelOverride: z.string().max(50).optional(),
|
||||
mode: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi']).optional(),
|
||||
mode: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok']).optional(),
|
||||
openCodeConfig: OpenCodeConfigSchema,
|
||||
codexConfig: CodexConfigSchema,
|
||||
geminiConfig: GeminiConfigSchema,
|
||||
antigravityConfig: AntigravityConfigSchema,
|
||||
piConfig: PiConfigSchema,
|
||||
grokConfig: GrokConfigSchema,
|
||||
envOverrides: safeEnvOverridesSchema,
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
effort: effortLevelSchema,
|
||||
@@ -1267,7 +1306,7 @@ const noNewlines = (v: string) => !/[\r\n]/.test(v);
|
||||
/** Shared field shape for creating/updating a scheduled job. */
|
||||
const CronJobBaseSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
agentType: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi']),
|
||||
agentType: z.enum(['claude', 'shell', 'opencode', 'codex', 'gemini', 'antigravity', 'pi', 'grok']),
|
||||
workingDir: safePathSchema,
|
||||
launchCommand: z.string().max(2000).refine(noNewlines, 'launchCommand must be a single line').optional(),
|
||||
promptMode: z.enum(['inline_text', 'prompt_file_path']),
|
||||
|
||||
@@ -1396,6 +1396,7 @@ export class WebServer extends EventEmitter {
|
||||
{ isGeminiAvailable },
|
||||
{ isAntigravityAvailable },
|
||||
{ isPiAvailable },
|
||||
{ isGrokAvailable },
|
||||
{ isCloudflaredAvailable },
|
||||
{ isGitAvailable },
|
||||
] = await Promise.all([
|
||||
@@ -1405,6 +1406,7 @@ export class WebServer extends EventEmitter {
|
||||
import('../utils/gemini-cli-resolver.js'),
|
||||
import('../utils/antigravity-cli-resolver.js'),
|
||||
import('../utils/pi-cli-resolver.js'),
|
||||
import('../utils/grok-cli-resolver.js'),
|
||||
import('../utils/cloudflared-resolver.js'),
|
||||
import('../git-clone.js'),
|
||||
]);
|
||||
@@ -1415,6 +1417,7 @@ export class WebServer extends EventEmitter {
|
||||
gemini: isGeminiAvailable(),
|
||||
antigravity: isAntigravityAvailable(),
|
||||
pi: isPiAvailable(),
|
||||
grok: isGrokAvailable(),
|
||||
cloudflared: isCloudflaredAvailable(),
|
||||
// Not a run mode: the Add Case → Clone tab is an offer this box cannot
|
||||
// keep without git (issue #236), same reasoning as cloudflared above.
|
||||
@@ -2683,6 +2686,7 @@ export class WebServer extends EventEmitter {
|
||||
geminiConfig: muxSession.mode === 'gemini' ? savedState?.geminiConfig : undefined,
|
||||
antigravityConfig: muxSession.mode === 'antigravity' ? savedState?.antigravityConfig : undefined,
|
||||
piConfig: muxSession.mode === 'pi' ? savedState?.piConfig : undefined,
|
||||
grokConfig: muxSession.mode === 'grok' ? savedState?.grokConfig : undefined,
|
||||
envOverrides: savedEnvOverrides,
|
||||
effort: savedState?.effort,
|
||||
attachmentHistory: savedAttachmentHistory,
|
||||
|
||||
Reference in New Issue
Block a user