Compare commits

..

8 Commits

Author SHA1 Message Date
YeonGyu-Kim
a70e7fe742 test(git-master): cover full git command prefix injection 2026-03-11 17:07:43 +09:00
YeonGyu-Kim
02fec3ddb1 test(git-master): cover git_env_prefix validation 2026-03-11 17:07:38 +09:00
YeonGyu-Kim
bf9721d4ee fix(git-master): prefix git commands in injected templates 2026-03-11 17:07:33 +09:00
YeonGyu-Kim
c288ad7124 feat(git-master): validate git_env_prefix values 2026-03-11 17:07:29 +09:00
Jeon Suyeol
6366c7ef6e test(git-master): add tests for git_env_prefix injection
Add unit tests for env prefix injection (default, disabled, custom value) and update existing skill-content tests to include git_env_prefix field.
2026-03-06 11:35:59 +09:00
Jeon Suyeol
26c8d55b67 feat(git-master): add git_env_prefix config to prefix all git commands
When git-master skill is loaded, all git commands are prefixed with the configured env variable (default: GIT_MASTER=1). This enables custom git hooks to detect git-master skill usage. Set to empty string to disable.
2026-03-06 11:35:52 +09:00
YeonGyu-Kim
ee3d88af9d refactor(installer): remove dead Antigravity auth plugin code
The installer was writing Antigravity provider config and calling a no-op addAuthPlugins function. Since opencode-antigravity-auth is no longer auto-installed and OpenCode supports native Google/Gemini auth, all Antigravity-related installer code is dead. Gemini detection now checks for native google provider instead.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-06 10:59:41 +09:00
YeonGyu-Kim
89dc302403 update agent-browser skill to match upstream v0.16.3
Sync SKILL.md and inline template with vercel-labs/agent-browser v0.16.3.
Adds: native Rust daemon, diff commands, annotated screenshots, profiler,
keyboard type/inserttext, get styles, expanded locators (placeholder/alt/
title/testid/last), security options, config file support, iOS Simulator,
cloud providers (Browserbase/Browser Use/Kernel), session persistence,
CDP auto-connect, and state management commands.
2026-03-06 10:45:35 +09:00
35 changed files with 678 additions and 1433 deletions

View File

@@ -1,92 +0,0 @@
/// <reference types="bun-types" />
import { describe, it, expect } from "bun:test"
import { buildAntiDuplicationSection } from "./dynamic-agent-prompt-builder"
describe("buildAntiDuplicationSection", () => {
it("#given no arguments #when building anti-duplication section #then returns comprehensive rule section", () => {
//#given: no special configuration needed
//#when: building the anti-duplication section
const result = buildAntiDuplicationSection()
//#then: should contain the anti-duplication rule with all key concepts
expect(result).toContain("Anti-Duplication Rule")
expect(result).toContain("CRITICAL")
expect(result).toContain("DO NOT perform the same search yourself")
})
it("#given no arguments #when building #then explicitly forbids manual re-search after delegation", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should explicitly list forbidden behaviors
expect(result).toContain("FORBIDDEN")
expect(result).toContain("manually grep/search for the same information")
expect(result).toContain("Re-doing the research")
})
it("#given no arguments #when building #then allows non-overlapping work", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should explicitly allow non-overlapping work
expect(result).toContain("ALLOWED")
expect(result).toContain("non-overlapping work")
expect(result).toContain("work that doesn't depend on the delegated research")
})
it("#given no arguments #when building #then includes wait-for-results instructions", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should include instructions for waiting properly
expect(result).toContain("Wait for Results Properly")
expect(result).toContain("End your response")
expect(result).toContain("Wait for the completion notification")
expect(result).toContain("background_output")
})
it("#given no arguments #when building #then explains why this matters", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should explain the purpose
expect(result).toContain("Why This Matters")
expect(result).toContain("Wasted tokens")
expect(result).toContain("Confusion")
expect(result).toContain("Efficiency")
})
it("#given no arguments #when building #then provides code examples", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should include examples
expect(result).toContain("Example")
expect(result).toContain("WRONG")
expect(result).toContain("CORRECT")
expect(result).toContain("task(subagent_type=")
})
it("#given no arguments #when building #then uses proper markdown formatting", () => {
//#given: no special configuration
//#when: building the section
const result = buildAntiDuplicationSection()
//#then: should be wrapped in Anti_Duplication tag
expect(result).toContain("<Anti_Duplication>")
expect(result).toContain("</Anti_Duplication>")
})
})

View File

@@ -1,118 +0,0 @@
import { describe, test, expect } from "bun:test"
import { ATLAS_SYSTEM_PROMPT } from "./default"
import { ATLAS_GPT_SYSTEM_PROMPT } from "./gpt"
import { ATLAS_GEMINI_SYSTEM_PROMPT } from "./gemini"
describe("Atlas prompts auto-continue policy", () => {
test("default variant should forbid asking user for continuation confirmation", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
// when
const lowerPrompt = prompt.toLowerCase()
// then
expect(lowerPrompt).toContain("auto-continue policy")
expect(lowerPrompt).toContain("never ask the user")
expect(lowerPrompt).toContain("should i continue")
expect(lowerPrompt).toContain("proceed to next task")
expect(lowerPrompt).toContain("approval-style")
expect(lowerPrompt).toContain("auto-continue immediately")
})
test("gpt variant should forbid asking user for continuation confirmation", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
// when
const lowerPrompt = prompt.toLowerCase()
// then
expect(lowerPrompt).toContain("auto-continue policy")
expect(lowerPrompt).toContain("never ask the user")
expect(lowerPrompt).toContain("should i continue")
expect(lowerPrompt).toContain("proceed to next task")
expect(lowerPrompt).toContain("approval-style")
expect(lowerPrompt).toContain("auto-continue immediately")
})
test("gemini variant should forbid asking user for continuation confirmation", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
// when
const lowerPrompt = prompt.toLowerCase()
// then
expect(lowerPrompt).toContain("auto-continue policy")
expect(lowerPrompt).toContain("never ask the user")
expect(lowerPrompt).toContain("should i continue")
expect(lowerPrompt).toContain("proceed to next task")
expect(lowerPrompt).toContain("approval-style")
expect(lowerPrompt).toContain("auto-continue immediately")
})
test("all variants should require immediate continuation after verification passes", () => {
// given
const prompts = [ATLAS_SYSTEM_PROMPT, ATLAS_GPT_SYSTEM_PROMPT, ATLAS_GEMINI_SYSTEM_PROMPT]
// when / then
for (const prompt of prompts) {
const lowerPrompt = prompt.toLowerCase()
expect(lowerPrompt).toMatch(/auto-continue immediately after verification/)
expect(lowerPrompt).toMatch(/immediately delegate next task/)
}
})
test("all variants should define when user interaction is actually needed", () => {
// given
const prompts = [ATLAS_SYSTEM_PROMPT, ATLAS_GPT_SYSTEM_PROMPT, ATLAS_GEMINI_SYSTEM_PROMPT]
// when / then
for (const prompt of prompts) {
const lowerPrompt = prompt.toLowerCase()
expect(lowerPrompt).toMatch(/only pause.*truly blocked/)
expect(lowerPrompt).toMatch(/plan needs clarification|blocked by external/)
}
})
})
describe("Atlas prompts plan path consistency", () => {
test("default variant should use .sisyphus/plans/{plan-name}.md path", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
// when / then
expect(prompt).toContain(".sisyphus/plans/{plan-name}.md")
expect(prompt).not.toContain(".sisyphus/tasks/{plan-name}.yaml")
expect(prompt).not.toContain(".sisyphus/tasks/")
})
test("gpt variant should use .sisyphus/plans/{plan-name}.md path", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
// when / then
expect(prompt).toContain(".sisyphus/plans/{plan-name}.md")
expect(prompt).not.toContain(".sisyphus/tasks/")
})
test("gemini variant should use .sisyphus/plans/{plan-name}.md path", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
// when / then
expect(prompt).toContain(".sisyphus/plans/{plan-name}.md")
expect(prompt).not.toContain(".sisyphus/tasks/")
})
test("all variants should read plan file after verification", () => {
// given
const prompts = [ATLAS_SYSTEM_PROMPT, ATLAS_GPT_SYSTEM_PROMPT, ATLAS_GEMINI_SYSTEM_PROMPT]
// when / then
for (const prompt of prompts) {
expect(prompt).toMatch(/read[\s\S]*?\.sisyphus\/plans\//)
}
})
})

View File

@@ -99,29 +99,6 @@ Every \`task()\` prompt MUST include ALL 6 sections:
**If your prompt is under 30 lines, it's TOO SHORT.**
</delegation_system>
<auto_continue>
## AUTO-CONTINUE POLICY (STRICT)
**CRITICAL: NEVER ask the user "should I continue", "proceed to next task", or any approval-style questions between plan steps.**
**You MUST auto-continue immediately after verification passes:**
- After any delegation completes and passes verification → Immediately delegate next task
- Do NOT wait for user input, do NOT ask "should I continue"
- Only pause or ask if you are truly blocked by missing information, an external dependency, or a critical failure
**The only time you ask the user:**
- Plan needs clarification or modification before execution
- Blocked by an external dependency beyond your control
- Critical failure prevents any further progress
**Auto-continue examples:**
- Task A done → Verify → Pass → Immediately start Task B
- Task fails → Retry 3x → Still fails → Document → Move to next independent task
- NEVER: "Should I continue to the next task?"
**This is NOT optional. This is core to your role as orchestrator.**
</auto_continue>
<workflow>
## Step 0: Register Tracking
@@ -237,7 +214,7 @@ After EVERY delegation, complete ALL of these steps — no shortcuts:
After verification, READ the plan file directly — every time, no exceptions:
\`\`\`
Read(".sisyphus/plans/{plan-name}.md")
Read(".sisyphus/tasks/{plan-name}.yaml")
\`\`\`
Count remaining \`- [ ]\` tasks. This is your ground truth for what comes next.

View File

@@ -116,29 +116,6 @@ Every \`task()\` prompt MUST include ALL 6 sections:
**Minimum 30 lines per delegation prompt. Under 30 lines = the subagent WILL fail.**
</delegation_system>
<auto_continue>
## AUTO-CONTINUE POLICY (STRICT)
**CRITICAL: NEVER ask the user "should I continue", "proceed to next task", or any approval-style questions between plan steps.**
**You MUST auto-continue immediately after verification passes:**
- After any delegation completes and passes verification → Immediately delegate next task
- Do NOT wait for user input, do NOT ask "should I continue"
- Only pause or ask if you are truly blocked by missing information, an external dependency, or a critical failure
**The only time you ask the user:**
- Plan needs clarification or modification before execution
- Blocked by an external dependency beyond your control
- Critical failure prevents any further progress
**Auto-continue examples:**
- Task A done → Verify → Pass → Immediately start Task B
- Task fails → Retry 3x → Still fails → Document → Move to next independent task
- NEVER: "Should I continue to the next task?"
**This is NOT optional. This is core to your role as orchestrator.**
</auto_continue>
<workflow>
## Step 0: Register Tracking

View File

@@ -48,10 +48,9 @@ Complete ALL tasks in a work plan via \`task()\` until fully done.
</scope_and_design_constraints>
<uncertainty_and_ambiguity>
- During initial plan analysis, if a task is ambiguous or underspecified:
- If a task is ambiguous or underspecified:
- Ask 1-3 precise clarifying questions, OR
- State your interpretation explicitly and proceed with the simplest approach.
- Once execution has started, do NOT stop to ask for continuation or approval between steps.
- Never fabricate task details, file paths, or requirements.
- Prefer language like "Based on the plan..." instead of absolute claims.
- When unsure about parallelization, default to sequential execution.
@@ -135,29 +134,6 @@ Every \`task()\` prompt MUST include ALL 6 sections:
**Minimum 30 lines per delegation prompt.**
</delegation_system>
<auto_continue>
## AUTO-CONTINUE POLICY (STRICT)
**CRITICAL: NEVER ask the user "should I continue", "proceed to next task", or any approval-style questions between plan steps.**
**You MUST auto-continue immediately after verification passes:**
- After any delegation completes and passes verification → Immediately delegate next task
- Do NOT wait for user input, do NOT ask "should I continue"
- Only pause or ask if you are truly blocked by missing information, an external dependency, or a critical failure
**The only time you ask the user:**
- Plan needs clarification or modification before execution
- Blocked by an external dependency beyond your control
- Critical failure prevents any further progress
**Auto-continue examples:**
- Task A done → Verify → Pass → Immediately start Task B
- Task fails → Retry 3x → Still fails → Document → Move to next independent task
- NEVER: "Should I continue to the next task?"
**This is NOT optional. This is core to your role as orchestrator.**
</auto_continue>
<workflow>
## Step 0: Register Tracking

View File

@@ -1,88 +0,0 @@
import { describe, expect, test } from "bun:test"
import { createSisyphusAgent } from "./sisyphus"
import { createHephaestusAgent } from "./hephaestus"
import { buildSisyphusJuniorPrompt } from "./sisyphus-junior/agent"
import {
buildAntiDuplicationSection,
buildExploreSection,
type AvailableAgent,
} from "./dynamic-agent-prompt-builder"
const exploreAgent = {
name: "explore",
description: "Contextual grep specialist",
metadata: {
category: "advisor",
cost: "FREE",
promptAlias: "Explore",
triggers: [],
useWhen: ["Multiple search angles needed"],
avoidWhen: ["Single keyword search is enough"],
},
} satisfies AvailableAgent
describe("delegation trust prompt rules", () => {
test("buildAntiDuplicationSection explains overlap is forbidden", () => {
// given
const section = buildAntiDuplicationSection()
// when / then
expect(section).toContain("DO NOT perform the same search yourself")
expect(section).toContain("non-overlapping work")
expect(section).toContain("End your response")
})
test("buildExploreSection includes delegation trust rule", () => {
// given
const agents = [exploreAgent]
// when
const section = buildExploreSection(agents)
// then
expect(section).toContain("Delegation Trust Rule")
expect(section).toContain("do **not** manually perform that same search yourself")
})
test("Sisyphus prompt forbids duplicate delegated exploration", () => {
// given
const agent = createSisyphusAgent("anthropic/claude-sonnet-4-6", [exploreAgent])
// when
const prompt = agent.prompt
// then
expect(prompt).toContain("Continue only with non-overlapping work")
expect(prompt).toContain("DO NOT perform the same search yourself")
})
test("Hephaestus prompt forbids duplicate delegated exploration", () => {
// given
const agent = createHephaestusAgent("openai/gpt-5.2", [exploreAgent])
// when
const prompt = agent.prompt
// then
expect(prompt).toContain("Continue only with non-overlapping work after launching background agents")
expect(prompt).toContain("DO NOT perform the same search yourself")
})
test("Sisyphus-Junior GPT prompt forbids duplicate delegated exploration", () => {
// given
const prompt = buildSisyphusJuniorPrompt("openai/gpt-5.2", false)
// when / then
expect(prompt).toContain("continue only with non-overlapping work while they search")
expect(prompt).toContain("DO NOT perform the same search yourself")
})
test("Sisyphus-Junior Gemini prompt forbids duplicate delegated exploration", () => {
// given
const prompt = buildSisyphusJuniorPrompt("google/gemini-3.1-pro", false)
// when / then
expect(prompt).toContain("continue only with non-overlapping work while they search")
expect(prompt).toContain("DO NOT perform the same search yourself")
})
})

View File

@@ -118,8 +118,6 @@ export function buildExploreSection(agents: AvailableAgent[]): string {
Use it as a **peer tool**, not a fallback. Fire liberally.
**Delegation Trust Rule:** Once you fire an explore agent for a search, do **not** manually perform that same search yourself. Use direct tools only for non-overlapping work or when you intentionally skipped delegation.
**Use Direct Tools when:**
${avoidWhen.map((w) => `- ${w}`).join("\n")}
@@ -310,7 +308,6 @@ export function buildAntiPatternsSection(): string {
"- **Search**: Firing agents for single-line typos or obvious syntax errors",
"- **Debugging**: Shotgun debugging, random changes",
"- **Background Tasks**: Polling `background_output` on running tasks — end response and wait for notification",
"- **Delegation Duplication**: Delegating exploration to explore/librarian and then manually doing the same search yourself",
"- **Oracle**: Delivering answer without collecting Oracle results",
]
@@ -412,52 +409,3 @@ export function buildUltraworkSection(
return lines.join("\n")
}
// Anti-duplication section for agent prompts
export function buildAntiDuplicationSection(): string {
return `<Anti_Duplication>
## Anti-Duplication Rule (CRITICAL)
Once you delegate exploration to explore/librarian agents, **DO NOT perform the same search yourself**.
### What this means:
**FORBIDDEN:**
- After firing explore/librarian, manually grep/search for the same information
- Re-doing the research the agents were just tasked with
- "Just quickly checking" the same files the background agents are checking
**ALLOWED:**
- Continue with **non-overlapping work** — work that doesn't depend on the delegated research
- Work on unrelated parts of the codebase
- Preparation work (e.g., setting up files, configs) that can proceed independently
### Wait for Results Properly:
When you need the delegated results but they're not ready:
1. **End your response** — do NOT continue with work that depends on those results
2. **Wait for the completion notification** — the system will trigger your next turn
3. **Then** collect results via \`background_output(task_id="...")\`
4. **Do NOT** impatiently re-search the same topics while waiting
### Why This Matters:
- **Wasted tokens**: Duplicate exploration wastes your context budget
- **Confusion**: You might contradict the agent's findings
- **Efficiency**: The whole point of delegation is parallel throughput
### Example:
\`\`\`typescript
// WRONG: After delegating, re-doing the search
task(subagent_type="explore", run_in_background=true, ...)
// Then immediately grep for the same thing yourself — FORBIDDEN
// CORRECT: Continue non-overlapping work
task(subagent_type="explore", run_in_background=true, ...)
// Work on a different, unrelated file while they search
// End your response and wait for the notification
\`\`\`
</Anti_Duplication>`
}

View File

@@ -16,7 +16,6 @@ import {
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
buildAntiDuplicationSection,
categorizeTools,
} from "./dynamic-agent-prompt-builder";
@@ -291,13 +290,11 @@ Prompt structure for each agent:
- Fire 2-5 explore agents in parallel for any non-trivial codebase question
- Parallelize independent file reads — don't read files one at a time
- NEVER use \`run_in_background=false\` for explore/librarian
- Continue only with non-overlapping work after launching background agents
- Continue your work immediately after launching background agents
- Collect results with \`background_output(task_id="...")\` when needed
- BEFORE final answer, cancel DISPOSABLE tasks individually: \`background_cancel(taskId="bg_explore_xxx")\`, \`background_cancel(taskId="bg_librarian_xxx")\`
- **NEVER use \`background_cancel(all=true)\`** — it kills tasks whose results you haven't collected yet
${buildAntiDuplicationSection()}
### Search Stop Conditions
STOP searching when:

View File

@@ -9,7 +9,6 @@
*/
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder"
export function buildGeminiSisyphusJuniorPrompt(
useTaskSystem: boolean,
@@ -59,7 +58,7 @@ Before responding, ask yourself: What tools do I need to call? What am I assumin
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — continue only with non-overlapping work while they search
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — keep working while they search
## Scope Discipline
@@ -78,15 +77,13 @@ Before responding, ask yourself: What tools do I need to call? What am I assumin
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian via call_omo_agent = background research. Fire them and continue only with non-overlapping work
- Explore/Librarian via call_omo_agent = background research. Fire them and keep working
- After any file edit: restate what changed, where, and what validation follows
- Prefer tools over guessing whenever you need specific data (files, configs, patterns)
- ALWAYS use tools over internal knowledge for file contents, project state, and verification
- **DO NOT SKIP tool calls because you think you already know the answer. You DON'T.**
</tool_usage_rules>
${buildAntiDuplicationSection()}
${taskDiscipline}
## Progress Updates

View File

@@ -7,7 +7,6 @@
*/
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
import { buildAntiDuplicationSection } from "../dynamic-agent-prompt-builder"
export function buildGptSisyphusJuniorPrompt(
useTaskSystem: boolean,
@@ -41,7 +40,7 @@ When blocked: try a different approach → decompose the problem → challenge a
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — continue only with non-overlapping work while they search
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — keep working while they search
## Scope Discipline
@@ -59,14 +58,12 @@ When blocked: try a different approach → decompose the problem → challenge a
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian via call_omo_agent = background research. Fire them and continue only with non-overlapping work
- Explore/Librarian via call_omo_agent = background research. Fire them and keep working
- After any file edit: restate what changed, where, and what validation follows
- Prefer tools over guessing whenever you need specific data (files, configs, patterns)
- ALWAYS use tools over internal knowledge for file contents, project state, and verification
</tool_usage_rules>
${buildAntiDuplicationSection()}
${taskDiscipline}
## Progress Updates

View File

@@ -35,7 +35,6 @@ import {
buildAntiPatternsSection,
buildDeepParallelSection,
buildNonClaudePlannerSection,
buildAntiDuplicationSection,
categorizeTools,
} from "./dynamic-agent-prompt-builder";
@@ -334,7 +333,7 @@ task(subagent_type="explore", run_in_background=true, load_skills=[], descriptio
// Reference Grep (external)
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find JWT security docs", prompt="I'm implementing JWT auth and need current security best practices to choose token storage (httpOnly cookies vs localStorage) and set expiration policy. Find: OWASP auth guidelines, recommended token lifetimes, refresh token rotation strategies, common JWT vulnerabilities. Skip 'what is JWT' tutorials — production security guidance only.")
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find Express auth patterns", prompt="I'm building Express auth middleware and need production-quality patterns to structure my middleware chain. Find how established Express apps (1000+ stars) handle: middleware ordering, token refresh, role-based access control, auth error propagation. Skip basic tutorials — I need battle-tested patterns with proper error handling.")
// Continue only with non-overlapping work. System notifies on completion — collect with background_output then.
// Continue working immediately. System notifies on completion — collect with background_output then.
// WRONG: Sequential or blocking
result = task(..., run_in_background=false) // Never wait synchronously for explore/librarian
@@ -342,13 +341,11 @@ result = task(..., run_in_background=false) // Never wait synchronously for exp
### Background Result Collection:
1. Launch parallel agents \u2192 receive task_ids
2. Continue only with non-overlapping work
2. Continue immediate work
3. System sends \`<system-reminder>\` on each task completion — then call \`background_output(task_id="...")\`
4. Need results not yet ready? **End your response.** The notification will trigger your next turn.
5. Cleanup: Cancel disposable tasks individually via \`background_cancel(taskId="...")\`
${buildAntiDuplicationSection()}
### Search Stop Conditions
STOP searching when:

View File

@@ -21,19 +21,9 @@ describe("runCliInstaller", () => {
console.error = originalConsoleError
})
it("runs auth and provider setup steps when openai or copilot are enabled without gemini", async () => {
it("completes installation without auth plugin or provider config steps", async () => {
//#given
const addAuthPluginsSpy = spyOn(configManager, "addAuthPlugins").mockResolvedValue({
success: true,
configPath: "/tmp/opencode.jsonc",
})
const addProviderConfigSpy = spyOn(configManager, "addProviderConfig").mockReturnValue({
success: true,
configPath: "/tmp/opencode.jsonc",
})
const restoreSpies = [
addAuthPluginsSpy,
addProviderConfigSpy,
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
isInstalled: false,
hasClaude: false,
@@ -73,8 +63,6 @@ describe("runCliInstaller", () => {
//#then
expect(result).toBe(0)
expect(addAuthPluginsSpy).toHaveBeenCalledTimes(1)
expect(addProviderConfigSpy).toHaveBeenCalledTimes(1)
for (const spy of restoreSpies) {
spy.mockRestore()

View File

@@ -1,9 +1,7 @@
import color from "picocolors"
import type { InstallArgs } from "./types"
import {
addAuthPlugins,
addPluginToOpenCodeConfig,
addProviderConfig,
detectCurrentConfig,
getOpenCodeVersion,
isOpenCodeInstalled,
@@ -45,7 +43,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
printHeader(isUpdate)
const totalSteps = 6
const totalSteps = 4
let step = 1
printStep(step++, totalSteps, "Checking OpenCode installation...")
@@ -77,28 +75,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
)
const needsProviderSetup = config.hasGemini || config.hasOpenAI || config.hasCopilot
if (needsProviderSetup) {
printStep(step++, totalSteps, "Adding auth plugins...")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
printError(`Failed: ${authResult.error}`)
return 1
}
printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`)
printStep(step++, totalSteps, "Adding provider configurations...")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
printError(`Failed: ${providerResult.error}`)
return 1
}
printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`)
} else {
step += 2
}
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
@@ -156,7 +132,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
printBox(
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ Gemini")}\n` : "") +
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
"Authenticate Your Providers",
)

View File

@@ -1,6 +1,6 @@
import { describe, expect, test, mock, afterEach } from "bun:test"
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
import { getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
import type { InstallConfig } from "./types"
describe("getPluginNameWithVersion", () => {
@@ -169,76 +169,6 @@ describe("fetchNpmDistTags", () => {
})
})
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
test("all models include full spec (limit + modalities + Antigravity label)", () => {
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
expect(google).toBeTruthy()
const models = google.models as Record<string, any>
expect(models).toBeTruthy()
const required = [
"antigravity-gemini-3.1-pro",
"antigravity-gemini-3-flash",
"antigravity-claude-sonnet-4-6",
"antigravity-claude-sonnet-4-6-thinking",
"antigravity-claude-opus-4-5-thinking",
]
for (const key of required) {
const model = models[key]
expect(model).toBeTruthy()
expect(typeof model.name).toBe("string")
expect(model.name.includes("(Antigravity)")).toBe(true)
expect(model.limit).toBeTruthy()
expect(typeof model.limit.context).toBe("number")
expect(typeof model.limit.output).toBe("number")
expect(model.modalities).toBeTruthy()
expect(Array.isArray(model.modalities.input)).toBe(true)
expect(Array.isArray(model.modalities.output)).toBe(true)
}
})
test("Gemini models have variant definitions", () => {
// #given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Gemini Pro variants
const pro = models["antigravity-gemini-3.1-pro"]
// #then should have low and high variants
expect(pro.variants).toBeTruthy()
expect(pro.variants.low).toBeTruthy()
expect(pro.variants.high).toBeTruthy()
// #when checking Gemini Flash variants
const flash = models["antigravity-gemini-3-flash"]
// #then should have minimal, low, medium, high variants
expect(flash.variants).toBeTruthy()
expect(flash.variants.minimal).toBeTruthy()
expect(flash.variants.low).toBeTruthy()
expect(flash.variants.medium).toBeTruthy()
expect(flash.variants.high).toBeTruthy()
})
test("Claude thinking models have variant definitions", () => {
// #given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Claude thinking variants
const sonnetThinking = models["antigravity-claude-sonnet-4-6-thinking"]
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
// #then both should have low and max variants
for (const model of [sonnetThinking, opusThinking]) {
expect(model.variants).toBeTruthy()
expect(model.variants.low).toBeTruthy()
expect(model.variants.max).toBeTruthy()
}
})
})
describe("generateOmoConfig - model fallback system", () => {
test("uses github-copilot sonnet fallback when only copilot available", () => {
// #given user has only copilot (no max plan)

View File

@@ -14,9 +14,6 @@ export { writeOmoConfig } from "./config-manager/write-omo-config"
export { isOpenCodeInstalled, getOpenCodeVersion } from "./config-manager/opencode-binary"
export { fetchLatestVersion, addAuthPlugins } from "./config-manager/auth-plugins"
export { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager/antigravity-provider-configuration"
export { addProviderConfig } from "./config-manager/add-provider-config"
export { detectCurrentConfig } from "./config-manager/detect-current-config"
export type { BunInstallResult } from "./config-manager/bun-install"

View File

@@ -1,205 +0,0 @@
import { describe, expect, it } from "bun:test"
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
import { parseJsonc } from "../../shared/jsonc-parser"
describe("modifyProviderInJsonc", () => {
describe("Test 1: Basic JSONC with existing provider", () => {
it("replaces provider value, preserves comments and other keys", () => {
// given
const content = `{
// my config
"provider": { "openai": {} },
"plugin": ["foo"]
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"google"')
expect(result).toContain('"plugin": ["foo"]')
expect(result).toContain('// my config')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('plugin')
expect(parsed).toHaveProperty('provider')
})
})
describe("Test 2: Comment containing '}' inside provider block", () => {
it("must NOT corrupt file", () => {
// given
const content = `{
"provider": {
// } this brace should be ignored
"openai": {}
},
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
})
})
describe("Test 3: Comment containing '\"provider\"' before real key", () => {
it("must NOT match wrong location", () => {
// given
const content = `{
// "provider": { "example": true }
"provider": { "openai": {} },
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
expect(parsed.provider).toHaveProperty('google')
})
})
describe("Test 4: Comment containing '{' inside provider", () => {
it("must NOT mess up depth", () => {
// given
const content = `{
"provider": {
// { unmatched brace in comment
"openai": {}
},
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
})
})
describe("Test 5: No existing provider key", () => {
it("inserts provider without corrupting", () => {
// given
const content = `{
// config comment
"plugin": ["foo"]
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"provider"')
expect(result).toContain('"plugin"')
expect(result).toContain('foo')
expect(result).toContain('// config comment')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('provider')
expect(parsed).toHaveProperty('plugin')
expect(parsed.plugin).toEqual(['foo'])
})
})
describe("Test 6: String value exactly 'provider' before real key", () => {
it("must NOT match wrong location", () => {
// given
const content = `{
"note": "provider",
"provider": { "openai": {} },
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
expect(result).toContain('"note": "provider"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
expect(parsed.note).toBe('provider')
})
})
describe("Test 7: Post-write validation", () => {
it("result file must be valid JSONC for all cases", () => {
// Test Case 1
const content1 = `{
"provider": { "openai": {} },
"plugin": ["foo"]
}`
const result1 = modifyProviderInJsonc(content1, { google: {} })
expect(() => parseJsonc(result1)).not.toThrow()
// Test Case 2
const content2 = `{
"provider": {
// } comment
"openai": {}
}
}`
const result2 = modifyProviderInJsonc(content2, { google: {} })
expect(() => parseJsonc(result2)).not.toThrow()
// Test Case 3
const content3 = `{
"plugin": ["foo"]
}`
const result3 = modifyProviderInJsonc(content3, { google: {} })
expect(() => parseJsonc(result3)).not.toThrow()
})
})
describe("Test 8: Trailing commas preserved", () => {
it("file is valid JSONC with trailing commas", () => {
// given
const content = `{
"provider": { "openai": {}, },
"plugin": ["foo",],
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(() => parseJsonc(result)).not.toThrow()
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('plugin')
expect(parsed.plugin).toEqual(['foo'])
})
})
})

View File

@@ -1,82 +0,0 @@
import { readFileSync, writeFileSync, copyFileSync } from "node:fs"
import type { ConfigMergeResult, InstallConfig } from "../types"
import { getConfigDir } from "./config-context"
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
import { detectConfigFormat } from "./opencode-config-format"
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./antigravity-provider-configuration"
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
import { parseJsonc } from "../../shared/jsonc-parser"
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
try {
ensureConfigDirectoryExists()
} catch (err) {
return {
success: false,
configPath: getConfigDir(),
error: formatErrorWithSuggestion(err, "create config directory"),
}
}
const { format, path } = detectConfigFormat()
try {
let existingConfig: OpenCodeConfig | null = null
if (format !== "none") {
const parseResult = parseOpenCodeConfigFileWithError(path)
if (parseResult.error && !parseResult.config) {
return {
success: false,
configPath: path,
error: `Failed to parse config file: ${parseResult.error}`,
}
}
existingConfig = parseResult.config
}
const newConfig = { ...(existingConfig ?? {}) }
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
if (config.hasGemini) {
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
}
if (Object.keys(providers).length > 0) {
newConfig.provider = providers
}
if (format === "jsonc") {
const content = readFileSync(path, "utf-8")
// Backup original file
copyFileSync(path, `${path}.bak`)
const providerValue = (newConfig.provider ?? {}) as Record<string, unknown>
const newContent = modifyProviderInJsonc(content, providerValue)
// Post-write validation
try {
parseJsonc(newContent)
} catch (error) {
return {
success: false,
configPath: path,
error: `Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`,
}
}
writeFileSync(path, newContent)
} else {
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
}
return { success: true, configPath: path }
} catch (err) {
return {
success: false,
configPath: path,
error: formatErrorWithSuggestion(err, "add provider config"),
}
}
}

View File

@@ -1,64 +0,0 @@
/**
* Antigravity Provider Configuration
*
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
*
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
* - `antigravity-gemini-3.1-pro` with variants: low, high
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
*
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3.1-pro-high`) still work
* but variants are the recommended approach.
*
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
*/
export const ANTIGRAVITY_PROVIDER_CONFIG = {
google: {
name: "Google",
models: {
"antigravity-gemini-3.1-pro": {
name: "Gemini 3 Pro (Antigravity)",
limit: { context: 1048576, output: 65535 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingLevel: "low" },
high: { thinkingLevel: "high" },
},
},
"antigravity-gemini-3-flash": {
name: "Gemini 3 Flash (Antigravity)",
limit: { context: 1048576, output: 65536 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
minimal: { thinkingLevel: "minimal" },
low: { thinkingLevel: "low" },
medium: { thinkingLevel: "medium" },
high: { thinkingLevel: "high" },
},
},
"antigravity-claude-sonnet-4-6": {
name: "Claude Sonnet 4.6 (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"antigravity-claude-sonnet-4-6-thinking": {
name: "Claude Sonnet 4.6 Thinking (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingConfig: { thinkingBudget: 8192 } },
max: { thinkingConfig: { thinkingBudget: 32768 } },
},
},
"antigravity-claude-opus-4-5-thinking": {
name: "Claude Opus 4.5 Thinking (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingConfig: { thinkingBudget: 8192 } },
max: { thinkingConfig: { thinkingBudget: 32768 } },
},
},
},
},
}

View File

@@ -1,230 +0,0 @@
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "node:fs"
import { parseJsonc } from "../../shared/jsonc-parser"
import type { InstallConfig } from "../types"
import { resetConfigContext } from "./config-context"
let testConfigPath: string
let testConfigDir: string
let testCounter = 0
let fetchVersionSpy: unknown
beforeEach(async () => {
testCounter++
testConfigDir = join(tmpdir(), `test-opencode-${Date.now()}-${testCounter}`)
testConfigPath = join(testConfigDir, "opencode.jsonc")
mkdirSync(testConfigDir, { recursive: true })
process.env.OPENCODE_CONFIG_DIR = testConfigDir
resetConfigContext()
const module = await import("./auth-plugins")
fetchVersionSpy = spyOn(module, "fetchLatestVersion").mockResolvedValue("1.2.3")
})
afterEach(() => {
try {
rmSync(testConfigDir, { recursive: true, force: true })
} catch {}
})
const testConfig: InstallConfig = {
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: true,
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
describe("addAuthPlugins", () => {
describe("Test 1: JSONC with commented plugin line", () => {
it("preserves comment, does NOT add antigravity plugin", async () => {
const content = `{
// "plugin": ["old-plugin"]
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
expect(newContent).toContain('// "plugin": ["old-plugin"]')
expect(newContent).toContain('existing-plugin')
// antigravity plugin should NOT be auto-added anymore
expect(newContent).not.toContain('opencode-antigravity-auth')
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('existing-plugin')
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
})
})
describe("Test 2: Plugin array already contains antigravity", () => {
it("preserves existing antigravity, does not add another", async () => {
const content = `{
"plugin": ["existing-plugin", "opencode-antigravity-auth"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
const antigravityCount = plugins.filter((p) => p.startsWith('opencode-antigravity-auth')).length
expect(antigravityCount).toBe(1)
expect(plugins).toContain('existing-plugin')
})
})
describe("Test 3: Backup created before write", () => {
it("creates .bak file", async () => {
const originalContent = `{
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, originalContent, "utf-8")
readFileSync(testConfigPath, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
expect(existsSync(`${result.configPath}.bak`)).toBe(true)
const backupContent = readFileSync(`${result.configPath}.bak`, "utf-8")
expect(backupContent).toBe(originalContent)
})
})
describe("Test 4: Comment with } character", () => {
it("preserves comments with special characters", async () => {
const content = `{
// This comment has } special characters
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(newContent).toContain('// This comment has } special characters')
expect(() => parseJsonc(newContent)).not.toThrow()
})
})
describe("Test 5: Comment containing 'plugin' string", () => {
it("must NOT match comment location", async () => {
const content = `{
// "plugin": ["fake"]
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(newContent).toContain('// "plugin": ["fake"]')
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('existing-plugin')
expect(plugins).not.toContain('fake')
})
})
describe("Test 6: No existing plugin array", () => {
it("creates empty plugin array when none exists, does NOT add antigravity", async () => {
const content = `{
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
expect(parsed).toHaveProperty('plugin')
const plugins = parsed.plugin as string[]
// antigravity plugin should NOT be auto-added anymore
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
expect(plugins.length).toBe(0)
})
})
describe("Test 7: Post-write validation ensures valid JSONC", () => {
it("result file must be valid JSONC", async () => {
const content = `{
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(() => parseJsonc(newContent)).not.toThrow()
const parsed = parseJsonc<Record<string, unknown>>(newContent)
expect(parsed).toHaveProperty('plugin')
expect(parsed).toHaveProperty('provider')
})
})
describe("Test 8: Multiple plugins in array", () => {
it("preserves existing plugins, does NOT add antigravity", async () => {
const content = `{
"plugin": ["plugin-1", "plugin-2", "plugin-3"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('plugin-1')
expect(plugins).toContain('plugin-2')
expect(plugins).toContain('plugin-3')
// antigravity plugin should NOT be auto-added anymore
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
expect(plugins.length).toBe(3)
})
})
})

View File

@@ -1,140 +0,0 @@
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"
import { modify, applyEdits } from "jsonc-parser"
import type { ConfigMergeResult, InstallConfig } from "../types"
import { getConfigDir } from "./config-context"
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
import { detectConfigFormat } from "./opencode-config-format"
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
import { parseJsonc } from "../../shared/jsonc-parser"
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
try {
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`)
if (!res.ok) return null
const data = (await res.json()) as { version: string }
return data.version
} catch {
return null
}
}
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
try {
ensureConfigDirectoryExists()
} catch (err) {
return {
success: false,
configPath: getConfigDir(),
error: formatErrorWithSuggestion(err, "create config directory"),
}
}
const { format, path } = detectConfigFormat()
const backupPath = `${path}.bak`
try {
let existingConfig: OpenCodeConfig | null = null
if (format !== "none") {
const parseResult = parseOpenCodeConfigFileWithError(path)
if (parseResult.error && !parseResult.config) {
return {
success: false,
configPath: path,
error: `Failed to parse config file: ${parseResult.error}`,
}
}
existingConfig = parseResult.config
}
const rawPlugins = existingConfig?.plugin
const plugins: string[] = Array.isArray(rawPlugins) ? rawPlugins : []
// Note: opencode-antigravity-auth plugin auto-installation has been removed
// Users can manually add auth plugins if needed
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
if (format !== "none" && existsSync(path)) {
copyFileSync(path, backupPath)
}
if (format === "jsonc") {
const content = readFileSync(path, "utf-8")
const newContent = applyEdits(
content,
modify(content, ["plugin"], plugins, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
)
try {
parseJsonc(newContent)
} catch (error) {
if (existsSync(backupPath)) {
copyFileSync(backupPath, path)
}
throw new Error(`Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`)
}
try {
writeFileSync(path, newContent)
} catch (error) {
const hasBackup = existsSync(backupPath)
try {
if (hasBackup) {
copyFileSync(backupPath, path)
}
} catch (restoreError) {
return {
success: false,
configPath: path,
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
}
}
return {
success: false,
configPath: path,
error: hasBackup
? `Failed to write config file. Restored from backup: ${String(error)}`
: `Failed to write config file. No backup was available: ${String(error)}`,
}
}
} else {
const nextContent = JSON.stringify(newConfig, null, 2) + "\n"
try {
writeFileSync(path, nextContent)
} catch (error) {
const hasBackup = existsSync(backupPath)
try {
if (hasBackup) {
copyFileSync(backupPath, path)
}
} catch (restoreError) {
return {
success: false,
configPath: path,
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
}
}
return {
success: false,
configPath: path,
error: hasBackup
? `Failed to write config file. Restored from backup: ${String(error)}`
: `Failed to write config file. No backup was available: ${String(error)}`,
}
}
}
return { success: true, configPath: path }
} catch (err) {
return {
success: false,
configPath: path,
error: formatErrorWithSuggestion(err, "add auth plugins to config"),
}
}
}

View File

@@ -66,7 +66,8 @@ export function detectCurrentConfig(): DetectedConfig {
return result
}
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
const providers = openCodeConfig.provider as Record<string, unknown> | undefined
result.hasGemini = providers ? "google" in providers : false
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
result.hasOpenAI = hasOpenAI

View File

@@ -1,11 +0,0 @@
import { modify, applyEdits } from "jsonc-parser"
export function modifyProviderInJsonc(
content: string,
newProviderValue: Record<string, unknown>
): string {
const edits = modify(content, ["provider"], newProviderValue, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
return applyEdits(content, edits)
}

View File

@@ -2,9 +2,7 @@ import * as p from "@clack/prompts"
import color from "picocolors"
import type { InstallArgs } from "./types"
import {
addAuthPlugins,
addPluginToOpenCodeConfig,
addProviderConfig,
detectCurrentConfig,
getOpenCodeVersion,
isOpenCodeInstalled,
@@ -54,26 +52,6 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
}
spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
if (config.hasGemini) {
spinner.start("Adding auth plugins (fetching latest versions)")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
spinner.stop(`Failed to add auth plugins: ${authResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
spinner.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`)
spinner.start("Adding provider configurations")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
spinner.stop(`Failed to add provider config: ${providerResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
spinner.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`)
}
spinner.start("Writing oh-my-opencode configuration")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
@@ -123,7 +101,7 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
const providers: string[] = []
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
if (config.hasGemini) providers.push(`Google ${color.gray("→ Gemini")}`)
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
console.log()

View File

@@ -884,6 +884,25 @@ describe("GitMasterConfigSchema", () => {
//#then
expect(result.success).toBe(false)
})
test("accepts shell-safe git_env_prefix", () => {
const config = { git_env_prefix: "MY_HOOK=active" }
const result = GitMasterConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.git_env_prefix).toBe("MY_HOOK=active")
}
})
test("rejects git_env_prefix with shell metacharacters", () => {
const config = { git_env_prefix: "A=1; rm -rf /" }
const result = GitMasterConfigSchema.safeParse(config)
expect(result.success).toBe(false)
})
})
describe("skills schema", () => {

View File

@@ -10,6 +10,7 @@ export * from "./schema/commands"
export * from "./schema/dynamic-context-pruning"
export * from "./schema/experimental"
export * from "./schema/fallback-models"
export * from "./schema/git-env-prefix"
export * from "./schema/git-master"
export * from "./schema/hooks"
export * from "./schema/notification"

View File

@@ -0,0 +1,28 @@
import { z } from "zod"
const GIT_ENV_ASSIGNMENT_PATTERN =
/^(?:[A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)(?: [A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)*$/
export const GIT_ENV_PREFIX_VALIDATION_MESSAGE =
'git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"'
export function isValidGitEnvPrefix(value: string): boolean {
if (value === "") {
return true
}
return GIT_ENV_ASSIGNMENT_PATTERN.test(value)
}
export function assertValidGitEnvPrefix(value: string): string {
if (!isValidGitEnvPrefix(value)) {
throw new Error(GIT_ENV_PREFIX_VALIDATION_MESSAGE)
}
return value
}
export const GitEnvPrefixSchema = z
.string()
.refine(isValidGitEnvPrefix, { message: GIT_ENV_PREFIX_VALIDATION_MESSAGE })
.default("GIT_MASTER=1")

View File

@@ -1,10 +1,14 @@
import { z } from "zod"
import { GitEnvPrefixSchema } from "./git-env-prefix"
export const GitMasterConfigSchema = z.object({
/** Add "Ultraworked with Sisyphus" footer to commit messages (default: true). Can be boolean or custom string. */
commit_footer: z.union([z.boolean(), z.string()]).default(true),
/** Add "Co-authored-by: Sisyphus" trailer to commit messages (default: true) */
include_co_authored_by: z.boolean().default(true),
/** Environment variable prefix for all git commands (default: "GIT_MASTER=1"). Set to "" to disable. Allows custom git hooks to detect git-master skill usage. */
git_env_prefix: GitEnvPrefixSchema,
})
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>

View File

@@ -26,29 +26,35 @@ agent-browser close # Close browser
### Navigation
```bash
agent-browser open <url> # Navigate to URL
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
agent-browser close # Close browser (aliases: quit, exit)
```
### Snapshot (page analysis)
```bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
agent-browser snapshot -c # Compact (remove empty structural elements)
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
agent-browser snapshot -i -c -d 5 # Combine options
```
The `-C` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
### Interactions (use @refs from snapshot)
```bash
agent-browser click @e1 # Click
agent-browser click @e1 # Click (--new-tab to open in new tab)
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
@@ -57,8 +63,8 @@ agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
```
@@ -73,6 +79,7 @@ agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
agent-browser get styles @e1 # Get computed styles
```
### Check state
@@ -84,12 +91,20 @@ agent-browser is checked @e1 # Check if checked
### Screenshots & PDF
```bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot # Screenshot (saves to temp dir if no path)
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
```
Annotated screenshots overlay numbered labels `[N]` on interactive elements. Each label corresponds to ref `@eN`, so refs work for both visual and text workflows:
```bash
agent-browser screenshot --annotate ./page.png
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
agent-browser click @e2 # Click the "Home" link labeled [2]
```
### Video recording
```bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
@@ -109,10 +124,12 @@ agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
```
Load states: `load`, `domcontentloaded`, `networkidle`
### Mouse control
```bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse down left # Press button (left/right/middle)
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
```
@@ -122,10 +139,18 @@ agent-browser mouse wheel 100 # Scroll wheel
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find placeholder "Search..." fill "query"
agent-browser find alt "Logo" click
agent-browser find title "Close" click
agent-browser find testid "submit-btn" click
agent-browser find first ".item" click
agent-browser find last ".item" click
agent-browser find nth 2 "a" text
```
Actions: `click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`
Options: `--name <name>` (filter role by accessible name), `--exact` (require exact text match)
### Browser settings
```bash
agent-browser set viewport 1920 1080 # Set viewport size
@@ -142,14 +167,13 @@ agent-browser set media dark # Emulate color scheme
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
agent-browser storage session # Same for sessionStorage
```
### Network
@@ -179,13 +203,59 @@ agent-browser frame main # Back to main frame
### Dialogs
```bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
agent-browser dialog dismiss # Dismiss dialog
```
### Diff (compare snapshots, screenshots, URLs)
```bash
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
```
### JavaScript
```bash
agent-browser eval "document.title" # Run JavaScript
agent-browser eval -b "base64code" # Run base64-encoded JS
agent-browser eval --stdin # Read JS from stdin
```
### Debug & Profiling
```bash
agent-browser console # View console messages
agent-browser console --clear # Clear console
agent-browser errors # View page errors
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop profile.json # Stop and save profile
```
### State management
```bash
agent-browser state save auth.json # Save auth state
agent-browser state load auth.json # Load auth state
agent-browser state list # List saved state files
agent-browser state show <file> # Show state summary
agent-browser state rename <old> <new> # Rename state file
agent-browser state clear [name] # Clear states for session
agent-browser state clear --all # Clear all saved states
agent-browser state clean --older-than <days> # Delete old states
```
### Setup
```bash
agent-browser install # Download Chromium browser
agent-browser install --with-deps # Also install system deps (Linux)
```
## Global Options
@@ -193,19 +263,60 @@ agent-browser eval "document.title" # Run JavaScript
| Option | Description |
|--------|-------------|
| `--session <name>` | Isolated browser session (`AGENT_BROWSER_SESSION` env) |
| `--session-name <name>` | Auto-save/restore session state (`AGENT_BROWSER_SESSION_NAME` env) |
| `--profile <path>` | Persistent browser profile (`AGENT_BROWSER_PROFILE` env) |
| `--state <path>` | Load storage state from JSON file (`AGENT_BROWSER_STATE` env) |
| `--headers <json>` | HTTP headers scoped to URL's origin |
| `--executable-path <path>` | Custom browser binary (`AGENT_BROWSER_EXECUTABLE_PATH` env) |
| `--extension <path>` | Load browser extension (repeatable; `AGENT_BROWSER_EXTENSIONS` env) |
| `--args <args>` | Browser launch args (`AGENT_BROWSER_ARGS` env) |
| `--user-agent <ua>` | Custom User-Agent (`AGENT_BROWSER_USER_AGENT` env) |
| `--proxy <url>` | Proxy server (`AGENT_BROWSER_PROXY` env) |
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (`AGENT_BROWSER_PROXY_BYPASS` env) |
| `--ignore-https-errors` | Ignore HTTPS certificate errors |
| `--allow-file-access` | Allow file:// URLs to access local files |
| `-p, --provider <name>` | Cloud browser provider (`AGENT_BROWSER_PROVIDER` env) |
| `--device <name>` | iOS device name (`AGENT_BROWSER_IOS_DEVICE` env) |
| `--json` | Machine-readable JSON output |
| `--headed` | Show browser window (not headless) |
| `--full, -f` | Full page screenshot |
| `--annotate` | Annotated screenshot with numbered labels (`AGENT_BROWSER_ANNOTATE` env) |
| `--headed` | Show browser window (`AGENT_BROWSER_HEADED` env) |
| `--cdp <port\|wss://url>` | Connect via Chrome DevTools Protocol |
| `--auto-connect` | Auto-discover running Chrome (`AGENT_BROWSER_AUTO_CONNECT` env) |
| `--color-scheme <scheme>` | Color scheme: dark, light, no-preference (`AGENT_BROWSER_COLOR_SCHEME` env) |
| `--download-path <path>` | Default download directory (`AGENT_BROWSER_DOWNLOAD_PATH` env) |
| `--native` | [Experimental] Use native Rust daemon (`AGENT_BROWSER_NATIVE` env) |
| `--config <path>` | Custom config file (`AGENT_BROWSER_CONFIG` env) |
| `--debug` | Debug output |
### Security options
| Option | Description |
|--------|-------------|
| `--content-boundaries` | Wrap page output in boundary markers (`AGENT_BROWSER_CONTENT_BOUNDARIES` env) |
| `--max-output <chars>` | Truncate page output to N characters (`AGENT_BROWSER_MAX_OUTPUT` env) |
| `--allowed-domains <list>` | Comma-separated allowed domain patterns (`AGENT_BROWSER_ALLOWED_DOMAINS` env) |
| `--action-policy <path>` | Path to action policy JSON file (`AGENT_BROWSER_ACTION_POLICY` env) |
| `--confirm-actions <list>` | Action categories requiring confirmation (`AGENT_BROWSER_CONFIRM_ACTIONS` env) |
## Configuration file
Create `agent-browser.json` for persistent defaults (no need to repeat flags):
**Locations (lowest to highest priority):**
1. `~/.agent-browser/config.json` — user-level defaults
2. `./agent-browser.json` — project-level overrides
3. `AGENT_BROWSER_*` environment variables
4. CLI flags override everything
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"native": true
}
```
## Example: Form submission
```bash
@@ -247,6 +358,13 @@ agent-browser open other-site.com
agent-browser set headers '{"X-Custom-Header": "value"}'
```
### Authentication Vault
```bash
# Store credentials locally (encrypted). The LLM never sees passwords.
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth login github
```
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
@@ -256,6 +374,13 @@ agent-browser --session test2 open site-b.com
agent-browser session list
```
### Session persistence (auto-save/restore)
```bash
agent-browser --session-name twitter open twitter.com
# Login once, state persists automatically across restarts
# State files stored in ~/.agent-browser/sessions/
```
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
```bash
@@ -263,9 +388,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
@@ -275,62 +397,54 @@ agent-browser snapshot -i --json
agent-browser get text @e1 --json
```
## Debugging
## Local files
```bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
```
## CDP Mode
```bash
agent-browser connect 9222 # Local CDP port
agent-browser --cdp 9222 snapshot # Direct CDP on each command
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser --auto-connect snapshot # Auto-discover running Chrome
```
## Cloud providers
```bash
# Browserbase
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
# Browser Use
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
# Kernel
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
```
## iOS Simulator
```bash
agent-browser device list # List available simulators
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
agent-browser -p ios snapshot -i # Same commands as desktop
agent-browser -p ios tap @e1 # Tap
agent-browser -p ios swipe up # Mobile-specific
agent-browser -p ios close # Close session
```
## Native Mode (Experimental)
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
```bash
agent-browser --native open example.com
# Or: export AGENT_BROWSER_NATIVE=1
# Or: {"native": true} in agent-browser.json
```
---
## Installation
### Step 1: Install agent-browser CLI
```bash
bun add -g agent-browser
```
### Step 2: Install Playwright browsers
**IMPORTANT**: `agent-browser install` may fail on some platforms (e.g., darwin-arm64) with "No binary found" error. In that case, install Playwright browsers directly:
```bash
# Create a temp project and install playwright
cd /tmp && bun init -y && bun add playwright
# Install Chromium browser
bun playwright install chromium
```
This downloads Chrome for Testing to `~/Library/Caches/ms-playwright/`.
### Verify installation
```bash
agent-browser open https://example.com --headed
```
If the browser opens successfully, installation is complete.
### Troubleshooting
| Error | Solution |
|-------|----------|
| `No binary found for darwin-arm64` | Run `bun playwright install chromium` in a project with playwright dependency |
| `Executable doesn't exist at .../chromium-XXXX` | Re-run `bun playwright install chromium` |
| Browser doesn't open | Ensure `--headed` flag is used for visible browser |
---
Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
Install: `bun add -g agent-browser && agent-browser install`. Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser

View File

@@ -40,29 +40,35 @@ agent-browser close # Close browser
### Navigation
\`\`\`bash
agent-browser open <url> # Navigate to URL
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
agent-browser close # Close browser (aliases: quit, exit)
\`\`\`
### Snapshot (page analysis)
\`\`\`bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
agent-browser snapshot -c # Compact (remove empty structural elements)
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
agent-browser snapshot -i -c -d 5 # Combine options
\`\`\`
The \`-C\` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
### Interactions (use @refs from snapshot)
\`\`\`bash
agent-browser click @e1 # Click
agent-browser click @e1 # Click (--new-tab to open in new tab)
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
@@ -71,8 +77,8 @@ agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
\`\`\`
@@ -87,6 +93,7 @@ agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
agent-browser get styles @e1 # Get computed styles
\`\`\`
### Check state
@@ -98,12 +105,20 @@ agent-browser is checked @e1 # Check if checked
### Screenshots & PDF
\`\`\`bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot # Screenshot (saves to temp dir if no path)
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
\`\`\`
Annotated screenshots overlay numbered labels \`[N]\` on interactive elements. Each label corresponds to ref \`@eN\`, so refs work for both visual and text workflows:
\`\`\`bash
agent-browser screenshot --annotate ./page.png
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
agent-browser click @e2 # Click the "Home" link labeled [2]
\`\`\`
### Video recording
\`\`\`bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
@@ -123,10 +138,12 @@ agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
\`\`\`
Load states: \`load\`, \`domcontentloaded\`, \`networkidle\`
### Mouse control
\`\`\`bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse down left # Press button (left/right/middle)
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
\`\`\`
@@ -136,10 +153,18 @@ agent-browser mouse wheel 100 # Scroll wheel
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find placeholder "Search..." fill "query"
agent-browser find alt "Logo" click
agent-browser find title "Close" click
agent-browser find testid "submit-btn" click
agent-browser find first ".item" click
agent-browser find last ".item" click
agent-browser find nth 2 "a" text
\`\`\`
Actions: \`click\`, \`fill\`, \`type\`, \`hover\`, \`focus\`, \`check\`, \`uncheck\`, \`text\`
Options: \`--name <name>\` (filter role by accessible name), \`--exact\` (require exact text match)
### Browser settings
\`\`\`bash
agent-browser set viewport 1920 1080 # Set viewport size
@@ -156,14 +181,13 @@ agent-browser set media dark # Emulate color scheme
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
agent-browser storage session # Same for sessionStorage
\`\`\`
### Network
@@ -193,13 +217,59 @@ agent-browser frame main # Back to main frame
### Dialogs
\`\`\`bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
agent-browser dialog dismiss # Dismiss dialog
\`\`\`
### Diff (compare snapshots, screenshots, URLs)
\`\`\`bash
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
\`\`\`
### JavaScript
\`\`\`bash
agent-browser eval "document.title" # Run JavaScript
agent-browser eval -b "base64code" # Run base64-encoded JS
agent-browser eval --stdin # Read JS from stdin
\`\`\`
### Debug & Profiling
\`\`\`bash
agent-browser console # View console messages
agent-browser console --clear # Clear console
agent-browser errors # View page errors
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop profile.json # Stop and save profile
\`\`\`
### State management
\`\`\`bash
agent-browser state save auth.json # Save auth state
agent-browser state load auth.json # Load auth state
agent-browser state list # List saved state files
agent-browser state show <file> # Show state summary
agent-browser state rename <old> <new> # Rename state file
agent-browser state clear [name] # Clear states for session
agent-browser state clear --all # Clear all saved states
agent-browser state clean --older-than <days> # Delete old states
\`\`\`
### Setup
\`\`\`bash
agent-browser install # Download Chromium browser
agent-browser install --with-deps # Also install system deps (Linux)
\`\`\`
## Global Options
@@ -207,19 +277,60 @@ agent-browser eval "document.title" # Run JavaScript
| Option | Description |
|--------|-------------|
| \`--session <name>\` | Isolated browser session (\`AGENT_BROWSER_SESSION\` env) |
| \`--session-name <name>\` | Auto-save/restore session state (\`AGENT_BROWSER_SESSION_NAME\` env) |
| \`--profile <path>\` | Persistent browser profile (\`AGENT_BROWSER_PROFILE\` env) |
| \`--state <path>\` | Load storage state from JSON file (\`AGENT_BROWSER_STATE\` env) |
| \`--headers <json>\` | HTTP headers scoped to URL's origin |
| \`--executable-path <path>\` | Custom browser binary (\`AGENT_BROWSER_EXECUTABLE_PATH\` env) |
| \`--extension <path>\` | Load browser extension (repeatable; \`AGENT_BROWSER_EXTENSIONS\` env) |
| \`--args <args>\` | Browser launch args (\`AGENT_BROWSER_ARGS\` env) |
| \`--user-agent <ua>\` | Custom User-Agent (\`AGENT_BROWSER_USER_AGENT\` env) |
| \`--proxy <url>\` | Proxy server (\`AGENT_BROWSER_PROXY\` env) |
| \`--proxy-bypass <hosts>\` | Hosts to bypass proxy (\`AGENT_BROWSER_PROXY_BYPASS\` env) |
| \`--ignore-https-errors\` | Ignore HTTPS certificate errors |
| \`--allow-file-access\` | Allow file:// URLs to access local files |
| \`-p, --provider <name>\` | Cloud browser provider (\`AGENT_BROWSER_PROVIDER\` env) |
| \`--device <name>\` | iOS device name (\`AGENT_BROWSER_IOS_DEVICE\` env) |
| \`--json\` | Machine-readable JSON output |
| \`--headed\` | Show browser window (not headless) |
| \`--full, -f\` | Full page screenshot |
| \`--annotate\` | Annotated screenshot with numbered labels (\`AGENT_BROWSER_ANNOTATE\` env) |
| \`--headed\` | Show browser window (\`AGENT_BROWSER_HEADED\` env) |
| \`--cdp <port\\|wss://url>\` | Connect via Chrome DevTools Protocol |
| \`--auto-connect\` | Auto-discover running Chrome (\`AGENT_BROWSER_AUTO_CONNECT\` env) |
| \`--color-scheme <scheme>\` | Color scheme: dark, light, no-preference (\`AGENT_BROWSER_COLOR_SCHEME\` env) |
| \`--download-path <path>\` | Default download directory (\`AGENT_BROWSER_DOWNLOAD_PATH\` env) |
| \`--native\` | [Experimental] Use native Rust daemon (\`AGENT_BROWSER_NATIVE\` env) |
| \`--config <path>\` | Custom config file (\`AGENT_BROWSER_CONFIG\` env) |
| \`--debug\` | Debug output |
### Security options
| Option | Description |
|--------|-------------|
| \`--content-boundaries\` | Wrap page output in boundary markers (\`AGENT_BROWSER_CONTENT_BOUNDARIES\` env) |
| \`--max-output <chars>\` | Truncate page output to N characters (\`AGENT_BROWSER_MAX_OUTPUT\` env) |
| \`--allowed-domains <list>\` | Comma-separated allowed domain patterns (\`AGENT_BROWSER_ALLOWED_DOMAINS\` env) |
| \`--action-policy <path>\` | Path to action policy JSON file (\`AGENT_BROWSER_ACTION_POLICY\` env) |
| \`--confirm-actions <list>\` | Action categories requiring confirmation (\`AGENT_BROWSER_CONFIRM_ACTIONS\` env) |
## Configuration file
Create \`agent-browser.json\` for persistent defaults (no need to repeat flags):
**Locations (lowest to highest priority):**
1. \`~/.agent-browser/config.json\` — user-level defaults
2. \`./agent-browser.json\` — project-level overrides
3. \`AGENT_BROWSER_*\` environment variables
4. CLI flags override everything
\`\`\`json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"native": true
}
\`\`\`
## Example: Form submission
\`\`\`bash
@@ -261,6 +372,13 @@ agent-browser open other-site.com
agent-browser set headers '{"X-Custom-Header": "value"}'
\`\`\`
### Authentication Vault
\`\`\`bash
# Store credentials locally (encrypted). The LLM never sees passwords.
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth login github
\`\`\`
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
@@ -270,6 +388,13 @@ agent-browser --session test2 open site-b.com
agent-browser session list
\`\`\`
### Session persistence (auto-save/restore)
\`\`\`bash
agent-browser --session-name twitter open twitter.com
# Login once, state persists automatically across restarts
# State files stored in ~/.agent-browser/sessions/
\`\`\`
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
\`\`\`bash
@@ -277,9 +402,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
\`\`\`
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
@@ -289,21 +411,53 @@ agent-browser snapshot -i --json
agent-browser get text @e1 --json
\`\`\`
## Debugging
## Local files
\`\`\`bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
\`\`\`
## CDP Mode
\`\`\`bash
agent-browser connect 9222 # Local CDP port
agent-browser --cdp 9222 snapshot # Direct CDP on each command
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser --auto-connect snapshot # Auto-discover running Chrome
\`\`\`
## Cloud providers
\`\`\`bash
# Browserbase
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
# Browser Use
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
# Kernel
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
\`\`\`
## iOS Simulator
\`\`\`bash
agent-browser device list # List available simulators
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
agent-browser -p ios snapshot -i # Same commands as desktop
agent-browser -p ios tap @e1 # Tap
agent-browser -p ios swipe up # Mobile-specific
agent-browser -p ios close # Close session
\`\`\`
## Native Mode (Experimental)
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
\`\`\`bash
agent-browser --native open example.com
# Or: export AGENT_BROWSER_NATIVE=1
# Or: {"native": true} in agent-browser.json
\`\`\`
---

View File

@@ -0,0 +1,155 @@
/// <reference types="bun-types" />
import { describe, it, expect } from "bun:test"
import { injectGitMasterConfig } from "./git-master-template-injection"
const SAMPLE_TEMPLATE = [
"# Git Master Agent",
"",
"## MODE DETECTION (FIRST STEP)",
"",
"Analyze the request.",
"",
"```bash",
"git status",
"git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null",
"MERGE_BASE=$(git merge-base HEAD main)",
"GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash $MERGE_BASE",
"```",
"",
"```",
"</execution>",
].join("\n")
describe("#given git_env_prefix config", () => {
describe("#when default config (GIT_MASTER=1)", () => {
it("#then injects env prefix section before MODE DETECTION", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
})
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
expect(result).toContain("GIT_MASTER=1 git status")
expect(result).toContain("GIT_MASTER=1 git commit")
expect(result).toContain("GIT_MASTER=1 git push")
expect(result).toContain("EVERY git command MUST be prefixed with `GIT_MASTER=1`")
const prefixIndex = result.indexOf("## GIT COMMAND PREFIX")
const modeIndex = result.indexOf("## MODE DETECTION")
expect(prefixIndex).toBeLessThan(modeIndex)
})
})
describe("#when git_env_prefix is empty string", () => {
it("#then does NOT inject env prefix section", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "",
})
expect(result).not.toContain("## GIT COMMAND PREFIX")
expect(result).not.toContain("GIT_MASTER=1")
expect(result).not.toContain("git_env_prefix")
})
})
describe("#when git_env_prefix is custom value", () => {
it("#then injects custom prefix in section", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "MY_HOOK=active",
})
expect(result).toContain("MY_HOOK=active git status")
expect(result).toContain("MY_HOOK=active git commit")
expect(result).not.toContain("GIT_MASTER=1")
})
})
describe("#when git_env_prefix contains shell metacharacters", () => {
it("#then rejects the malicious value", () => {
expect(() =>
injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "A=1; rm -rf /",
})
).toThrow('git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"')
})
})
describe("#when no config provided", () => {
it("#then uses default GIT_MASTER=1 prefix", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE)
expect(result).toContain("GIT_MASTER=1 git status")
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
})
})
})
describe("#given git_env_prefix with commit footer", () => {
describe("#when both env prefix and footer are enabled", () => {
it("#then commit examples include the env prefix", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: true,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
})
expect(result).toContain("GIT_MASTER=1 git commit")
expect(result).toContain("Ultraworked with [Sisyphus]")
})
})
describe("#when the template already contains bare git commands in bash blocks", () => {
it("#then prefixes every git invocation in the final output", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
})
expect(result).toContain("GIT_MASTER=1 git status")
expect(result).toContain(
"GIT_MASTER=1 git merge-base HEAD main 2>/dev/null || GIT_MASTER=1 git merge-base HEAD master 2>/dev/null"
)
expect(result).toContain("MERGE_BASE=$(GIT_MASTER=1 git merge-base HEAD main)")
expect(result).toContain(
"GIT_SEQUENCE_EDITOR=: GIT_MASTER=1 git rebase -i --autosquash $MERGE_BASE"
)
})
})
describe("#when env prefix disabled but footer enabled", () => {
it("#then commit examples have no env prefix", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: true,
include_co_authored_by: false,
git_env_prefix: "",
})
expect(result).not.toContain("GIT_MASTER=1 git commit")
expect(result).toContain("git commit -m")
expect(result).toContain("Ultraworked with [Sisyphus]")
})
})
describe("#when both env prefix and co-author are enabled", () => {
it("#then commit example includes prefix, footer, and co-author", () => {
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
commit_footer: true,
include_co_authored_by: true,
git_env_prefix: "GIT_MASTER=1",
})
expect(result).toContain("GIT_MASTER=1 git commit")
expect(result).toContain("Ultraworked with [Sisyphus]")
expect(result).toContain("Co-authored-by: Sisyphus")
})
})
})

View File

@@ -1,14 +1,88 @@
import type { GitMasterConfig } from "../../config/schema"
import { assertValidGitEnvPrefix, type GitMasterConfig } from "../../config/schema"
const BASH_CODE_BLOCK_PATTERN = /```bash\r?\n([\s\S]*?)```/g
const LEADING_GIT_COMMAND_PATTERN = /^([ \t]*(?:[A-Za-z_][A-Za-z0-9_]*=[^ \t]+\s+)*)git(?=[ \t]|$)/gm
const INLINE_GIT_COMMAND_PATTERN = /([;&|()][ \t]*)git(?=[ \t]|$)/g
export function injectGitMasterConfig(template: string, config?: GitMasterConfig): string {
const commitFooter = config?.commit_footer ?? true
const includeCoAuthoredBy = config?.include_co_authored_by ?? true
const gitEnvPrefix = assertValidGitEnvPrefix(config?.git_env_prefix ?? "GIT_MASTER=1")
if (!commitFooter && !includeCoAuthoredBy) {
return template
let result = gitEnvPrefix ? injectGitEnvPrefix(template, gitEnvPrefix) : template
if (commitFooter || includeCoAuthoredBy) {
const injection = buildCommitFooterInjection(commitFooter, includeCoAuthoredBy, gitEnvPrefix)
const insertionPoint = result.indexOf("```\n</execution>")
result =
insertionPoint !== -1
? result.slice(0, insertionPoint) +
"```\n\n" +
injection +
"\n</execution>" +
result.slice(insertionPoint + "```\n</execution>".length)
: result + "\n\n" + injection
}
return gitEnvPrefix ? prefixGitCommandsInBashCodeBlocks(result, gitEnvPrefix) : result
}
function injectGitEnvPrefix(template: string, prefix: string): string {
const envPrefixSection = [
"## GIT COMMAND PREFIX (MANDATORY)",
"",
`<git_env_prefix>`,
`**EVERY git command MUST be prefixed with \`${prefix}\`.**`,
"",
"This allows custom git hooks to detect when git-master skill is active.",
"",
"```bash",
`${prefix} git status`,
`${prefix} git add <files>`,
`${prefix} git commit -m "message"`,
`${prefix} git push`,
`${prefix} git rebase ...`,
`${prefix} git log ...`,
"```",
"",
"**NO EXCEPTIONS. Every `git` invocation must include this prefix.**",
`</git_env_prefix>`,
].join("\n")
const modeDetectionMarker = "## MODE DETECTION (FIRST STEP)"
const markerIndex = template.indexOf(modeDetectionMarker)
if (markerIndex !== -1) {
return (
template.slice(0, markerIndex) +
envPrefixSection +
"\n\n---\n\n" +
template.slice(markerIndex)
)
}
return envPrefixSection + "\n\n---\n\n" + template
}
function prefixGitCommandsInBashCodeBlocks(template: string, prefix: string): string {
return template.replace(BASH_CODE_BLOCK_PATTERN, (block, codeBlock: string) => {
return block.replace(codeBlock, prefixGitCommandsInCodeBlock(codeBlock, prefix))
})
}
function prefixGitCommandsInCodeBlock(codeBlock: string, prefix: string): string {
return codeBlock
.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
}
function buildCommitFooterInjection(
commitFooter: boolean | string,
includeCoAuthoredBy: boolean,
gitEnvPrefix: string,
): string {
const sections: string[] = []
const cmdPrefix = gitEnvPrefix ? `${gitEnvPrefix} ` : ""
sections.push("### 5.5 Commit Footer & Co-Author")
sections.push("")
@@ -43,7 +117,7 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig
sections.push("**Example (both enabled):**")
sections.push("```bash")
sections.push(
`git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
)
sections.push("```")
} else if (commitFooter) {
@@ -53,29 +127,16 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig
: "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)"
sections.push("**Example:**")
sections.push("```bash")
sections.push(`git commit -m "{Commit Message}" -m "${footerText}"`)
sections.push(`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}"`)
sections.push("```")
} else if (includeCoAuthoredBy) {
sections.push("**Example:**")
sections.push("```bash")
sections.push(
"git commit -m \"{Commit Message}\" -m \"Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>\""
`${cmdPrefix}git commit -m "{Commit Message}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
)
sections.push("```")
}
const injection = sections.join("\n")
const insertionPoint = template.indexOf("```\n</execution>")
if (insertionPoint !== -1) {
return (
template.slice(0, insertionPoint) +
"```\n\n" +
injection +
"\n</execution>" +
template.slice(insertionPoint + "```\n</execution>".length)
)
}
return template + "\n\n" + injection
return sections.join("\n")
}

View File

@@ -228,6 +228,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: false,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
},
}
@@ -249,6 +250,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: true,
include_co_authored_by: true,
git_env_prefix: "GIT_MASTER=1",
},
}
@@ -269,6 +271,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: true,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
},
}
@@ -302,6 +305,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: false,
include_co_authored_by: true,
git_env_prefix: "GIT_MASTER=1",
},
}
@@ -322,6 +326,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: customFooter,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
},
}
@@ -341,6 +346,7 @@ describe("resolveMultipleSkillsAsync", () => {
gitMasterConfig: {
commit_footer: true,
include_co_authored_by: false,
git_env_prefix: "GIT_MASTER=1",
},
}

View File

@@ -87,6 +87,7 @@ export function createBackgroundOutput(manager: BackgroundOutputManager, client:
const shouldBlock = args.block === true
const timeoutMs = Math.min(args.timeout ?? 60000, 600000)
const fullSession = args.full_session ?? true
let resolvedTask = task
@@ -122,10 +123,6 @@ export function createBackgroundOutput(manager: BackgroundOutputManager, client:
}
const isActive = isTaskActiveStatus(resolvedTask.status)
const fullSessionProvided = args.full_session !== undefined
const fullSession = fullSessionProvided
? (args.full_session ?? true)
: !isActive
const includeThinking = isActive || (args.include_thinking ?? false)
const includeToolResults = isActive || (args.include_tool_results ?? false)