HagiCode.Libs is a lightweight .NET 10 library workspace for reusable HagiCode CLI integrations and repository exploration.
src/HagiCode.Libs.Core- transport, process management, executable discovery, and runtime environment resolution.src/HagiCode.Libs.Prompts- file-backed Handlebars prompt catalog loading, override merging, locale fallback, diagnostics, and rendering.src/HagiCode.Libs.Providers- provider abstractions, the Claude Code/Copilot/Codex/DeepAgents/CodeBuddy/Gemini/Hermes/Kimi/Kiro/OpenCode/Pi/QoderCLI/Reasonix providers, and optional DI registration.src/HagiCode.Libs.Skills- skills-oriented infrastructure. Its first shipped capability is a typed online API client for search, well-known discovery, audit, telemetry, and GitHub metadata/tree requests.src/HagiCode.Libs.Imaging- in-process AVIF conversion (PNG, JPEG, WebP and GIF first frame to AVIF) with bundled libvips native libraries. Noffmpeg,avifencor other tool is used or looked up. Tests:tests/HagiCode.Libs.Imaging.Tests. Backend integration guide:docs/avif-backend-integration-guide.md.src/HagiCode.Libs.Exploration- Git repository discovery and state inspection.tests/*- NUnit coverage for each project. Shared test settings, the NUnit and Shouldly packages and the global usings live intests/Directory.Build.props; each test project lists only its own references.
cd repos/Hagicode.Libs
dotnet build HagiCode.Libs.slnx
dotnet test HagiCode.Libs.slnx
dotnet test tests/HagiCode.Libs.Imaging.Tests/HagiCode.Libs.Imaging.Tests.csproj # AVIF suite; native tests fail on supported platforms when the runtime cannot loadrepos/Hagicode.Libs/.github/workflows/nuget-publish.yml now supports two publish modes for src/HagiCode.Libs.Core, src/HagiCode.Libs.Prompts, src/HagiCode.Libs.Providers, src/HagiCode.Libs.Skills, and src/HagiCode.Libs.Imaging (AVIF conversion with bundled native libraries; the package step also checks its native layout with scripts/imaging/verify_native_layout.py):
- Push a
v*.*.*tag to publish a stable package version that matches the tag name without the leadingvand publish the matching GitHub Release. - Push to
mainto publish a dev prerelease package automatically without creating a GitHub Release.
The main prerelease version format is:
<next-patch>-dev.<github.run_number>.<github.run_attempt>
- If the latest stable tag is
v1.2.3, the nextmainpublish becomes1.2.4-dev.<run_number>.<run_attempt>. - If the repository has no stable
v*.*.*tags yet, the workflow falls back to0.1.0-dev.<run_number>.<run_attempt>. - Rerunning the same workflow increments
github.run_attempt, so each rerun still produces a unique package version.
Because the -dev.* suffix marks these builds as NuGet prereleases, consumers must explicitly allow prerelease packages to install or upgrade to them. Stable consumers that do not opt into prerelease packages continue to resolve only stable versions.
Before using that workflow, configure GitHub and nuget.org for Trusted Publishing:
- Set the GitHub Actions secret
NUGET_USERto the nuget.org account name that owns the packages. - In nuget.org Trusted Publishing, add a policy for the
newbe36524/Hagicode.Libsrepository and set the workflow file name tonuget-publish.yml. - Do not keep a long-lived
NUGET_API_KEYsecret for this workflow; both stable and dev publishes expectNuGet/login@v1to mint a temporary key through GitHub OIDC.
HagiCode.Libs.Skills is intentionally broader than the initial HTTP wrapper so future non-HTTP skills capabilities can ship in the same package without a rename. For now, the OnlineApi module is the delivered surface.
Register the package through DI:
using HagiCode.Libs.Skills;
using HagiCode.Libs.Skills.OnlineApi;
using HagiCode.Libs.Skills.OnlineApi.Models;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddHagiCodeSkills(options =>
{
options.DisableTelemetry = true;
options.GitHubToken = "<token>";
});
await using var provider = services.BuildServiceProvider();
var onlineApiClient = provider.GetRequiredService<IOnlineApiClient>();Search and well-known discovery use typed request/response models:
var searchResponse = await onlineApiClient.SearchAsync(new SearchSkillsRequest
{
Query = "codex",
Limit = 10,
});
var discoveryResponse = await onlineApiClient.DiscoverWellKnownAsync(new WellKnownDiscoveryRequest
{
SourceUrl = "https://example.com/docs",
});The endpoint profile is provider-driven, so consumers can replace IOnlineApiEndpointProvider or override base URIs without changing the public client contract.
src/HagiCode.Libs.ClaudeCode.Console, src/HagiCode.Libs.Copilot.Console, src/HagiCode.Libs.Codex.Console, src/HagiCode.Libs.DeepAgents.Console, src/HagiCode.Libs.Dsh.Console, src/HagiCode.Libs.Codebuddy.Console, src/HagiCode.Libs.Gemini.Console, src/HagiCode.Libs.Junie.Console, src/HagiCode.Libs.Hermes.Console, src/HagiCode.Libs.Kimi.Console, src/HagiCode.Libs.Kiro.Console, src/HagiCode.Libs.OpenCode.Console, src/HagiCode.Libs.Pi.Console, src/HagiCode.Libs.QoderCli.Console, and src/HagiCode.Libs.Reasonix.Console are dedicated provider consoles built on the shared HagiCode.Libs.ConsoleTesting harness.
src/HagiCode.Libs.Providers/OpenCode now owns the canonical OpenCode typed runtime/session surface and the reusable OpenCodeFixtureServer test fixture. hagicode-core consumes that boundary through adapter-level tests and no longer ships a second OpenCode console or runtime project.
From repos/Hagicode.Libs, you can use:
dotnet run --project src/HagiCode.Libs.ClaudeCode.Console -- --help
dotnet run --project src/HagiCode.Libs.ClaudeCode.Console
dotnet run --project src/HagiCode.Libs.ClaudeCode.Console -- --test-provider
dotnet run --project src/HagiCode.Libs.ClaudeCode.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.ClaudeCode.Console -- --test-all claude
dotnet run --project src/HagiCode.Libs.Copilot.Console -- --help
dotnet run --project src/HagiCode.Libs.Copilot.Console
dotnet run --project src/HagiCode.Libs.Copilot.Console -- --test-provider github-copilot
dotnet run --project src/HagiCode.Libs.Copilot.Console -- --test-provider-full --model claude-sonnet-4.5 --config-dir .copilot --repo .
dotnet run --project src/HagiCode.Libs.Copilot.Console -- --test-all copilot
dotnet run --project src/HagiCode.Libs.Codex.Console -- --help
dotnet run --project src/HagiCode.Libs.Codex.Console
dotnet run --project src/HagiCode.Libs.Codex.Console -- --test-provider codex-cli
dotnet run --project src/HagiCode.Libs.Codex.Console -- --test-provider-full --sandbox workspace-write --repo .
dotnet run --project src/HagiCode.Libs.Codex.Console -- --test-all codex
uv tool install deepagents-cli
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --help
dotnet run --project src/HagiCode.Libs.DeepAgents.Console
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-provider deepagents-acp
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-provider-full --model glm-5.1 --workspace . --skill ./skills --arg --debug
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-provider-full --toolcall --verbose
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-provider-full --toolcall-case mixed
dotnet run --project src/HagiCode.Libs.DeepAgents.Console -- --test-all deepagents
dotnet run --project src/HagiCode.Libs.Codebuddy.Console -- --help
dotnet run --project src/HagiCode.Libs.Codebuddy.Console
dotnet run --project src/HagiCode.Libs.Codebuddy.Console -- --test-provider codebuddy-cli
dotnet run --project src/HagiCode.Libs.Codebuddy.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Codebuddy.Console -- --test-all codebuddy
dotnet run --project src/HagiCode.Libs.Hermes.Console -- --help
dotnet run --project src/HagiCode.Libs.Hermes.Console
dotnet run --project src/HagiCode.Libs.Hermes.Console -- --test-provider hermes-cli
dotnet run --project src/HagiCode.Libs.Hermes.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Hermes.Console -- --test-provider-full --arguments "acp --profile smoke"
dotnet run --project src/HagiCode.Libs.Hermes.Console -- --test-all hermes
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --help
dotnet run --project src/HagiCode.Libs.Gemini.Console
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --test-provider gemini-cli
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --test-provider-full --model gemini-2.5-pro --arg --profile=smoke
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --test-all gemini
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --help
dotnet run --project src/HagiCode.Libs.Dsh.Console
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --test-provider dsh-cli
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --test-provider-full --workspace .
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --test-provider-full --provider omniroute --model claude/claude-haiku-5-5 --dsh-home /path/to/dsh-home --verbose
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --test-all dsh
dotnet run --project src/HagiCode.Libs.Junie.Console -- --help
dotnet run --project src/HagiCode.Libs.Junie.Console
dotnet run --project src/HagiCode.Libs.Junie.Console -- --test-provider junie-cli
dotnet run --project src/HagiCode.Libs.Junie.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Junie.Console -- --test-provider-full --model sonnet --arg --brave
dotnet run --project src/HagiCode.Libs.Junie.Console -- --test-all junie
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --help
dotnet run --project src/HagiCode.Libs.Kimi.Console
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --test-provider kimi-cli
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --test-provider-full --model kimi-k2.5 --arg --profile=smoke
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --test-all kimi
dotnet run --project src/HagiCode.Libs.Kiro.Console -- --help
dotnet run --project src/HagiCode.Libs.Kiro.Console
dotnet run --project src/HagiCode.Libs.Kiro.Console -- --test-provider kiro-cli
dotnet run --project src/HagiCode.Libs.Kiro.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Kiro.Console -- --test-provider-full --model kiro-default --auth-method token --auth-token <token> --arg --profile
dotnet run --project src/HagiCode.Libs.Kiro.Console -- --test-all kiro
dotnet run --project src/HagiCode.Libs.Pi.Console -- --help
dotnet run --project src/HagiCode.Libs.Pi.Console
dotnet run --project src/HagiCode.Libs.Pi.Console -- --test-provider pi-cli
dotnet run --project src/HagiCode.Libs.Pi.Console -- --test-provider-full --provider omniroute --model glm/glm-4.7 --repo .
dotnet run --project src/HagiCode.Libs.Pi.Console -- --test-all pi
dotnet run --project src/HagiCode.Libs.OpenCode.Console -- --help
dotnet run --project src/HagiCode.Libs.OpenCode.Console
dotnet run --project src/HagiCode.Libs.OpenCode.Console -- --test-provider open-code
dotnet run --project src/HagiCode.Libs.OpenCode.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.OpenCode.Console -- --test-provider-full --model anthropic/claude-sonnet-4 --base-url http://127.0.0.1:4096
dotnet run --project src/HagiCode.Libs.OpenCode.Console -- --test-all opencode
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --help
dotnet run --project src/HagiCode.Libs.QoderCli.Console
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --test-provider qodercli
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --test-provider-full --model qoder-max
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --test-all qodercli
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --help
dotnet run --project src/HagiCode.Libs.Reasonix.Console
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider reasonix
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider-full --repo .
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider-full --model <provider>/<model>
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-all reasonix- No arguments run the default Claude suite.
- 默认套件当前包含
Ping、Simple Prompt、Complex Prompt、Session Restore (Session Id)和Session Restore (Resume Id)。Session Restore (Session Id)用--session-id <id>开启会话,再用--resume <id>恢复,并要求 CLI 回报的session_id与请求的一致。Session Restore (Resume Id)不指定 id,读取首轮 stream 里回报的session_id,再用--resume <id>恢复。- 两个 Session Restore 场景都用运行时生成的随机 secret 校验,只有真正取回上一轮内容才算通过。
- Claude provider 只按显式 session id 恢复会话,从不发出
--continue;ExtraArgs里的continue会被拒绝。
--test-providerruns the provider ping flow for the Claude console only.--test-provider-fulland--test-allrun the full provider-scoped suite.--repo <path>adds the repository analysis scenario to the suite.--subagentadds theBackground Subagentscenario (about 20-30 seconds, uses extra tokens). It asks Claude to launch a background subagent and passes only when the finalresultcarries a token the subagent generated at run time, i.e. the stream was tracked past the launch turn's ownresult. It reports "inconclusive" if Claude does not launch a background subagent; run it again in that case.--api-key <key>and--model <model>override Claude execution options for scenario runs.--effort <level>overrides the ClaudeEffortoption and is forwarded to the CLI as--effort. Supported levels arelow,medium,high,xhigh, andmax(case-insensitive).- No arguments also run the default Copilot suite.
- Copilot 默认套件当前包含
Ping、Simple Prompt和Complex Prompt。 - Copilot accepts
--model <model>,--executable <path>,--auth-source <mode>,--github-token <token>,--config-dir <path>, and compatible filtered startup overrides such as--log-level <level>. - Copilot repository analysis remains opt-in via
--repo <path>, and the provider filters unsupported startup flags before SDK launch with deterministic diagnostics. - No arguments also run the default Codex suite.
- Codex 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Codex accepts
--model <model>,--sandbox <mode>,--approval-policy <mode>,--api-key <key>, and--base-url <url>overrides. - Codex repository analysis remains opt-in via
--repo <path>and reuses the same shared report formatter. - No arguments also run the default DeepAgents suite.
- DeepAgents 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - DeepAgents accepts the provider aliases
deepagentsanddeepagents-acp. - DeepAgents accepts
--model <model>,--workspace <path>,--name <name>,--description <text>, repeated--skill <path>, repeated--memory <path>,--executable <path>, repeated--arg <value>,--toolcall, and--toolcall-case <parsing|failure|mixed>overrides. - DeepAgents repository summary remains opt-in via
--repo <path>, and the runtime now prefers an explicit executable, thendeepagents --acp, thenuvx --from deepagents-cli deepagents --acp. --toolcallappends the dedicated DeepAgents toolcall diagnostics on top of the baseline suite, while--toolcall-case <name>narrows execution to one toolcall scenario without droppingPing,Simple Prompt,Complex Prompt, orSession Resume.- Toolcall verbose runs print
RawMessageTypes,LifecycleTrace, and per-tool metadata such asTool[1]: stage=tool.call, name=bash, id=tool-parse-1, which makes the first parser divergence easy to spot in CI logs. - DeepAgents is now treated as local-only validation metadata in
CliInstallRegistry; install thedeepagentsCLI separately before running the dedicated console commands above. - No arguments also run the default CodeBuddy suite.
- CodeBuddy 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - CodeBuddy accepts
--model <model>and defaults toglm-4.7when no explicit model override is supplied. - CodeBuddy repository summary remains opt-in via
--repo <path>. - No arguments also run the default Hermes suite.
- Hermes 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Memory Reuse。 - Hermes accepts
--model <model>,--executable <path>, and--arguments <value>overrides. - Hermes repository summary remains opt-in via
--repo <path>. - No arguments also run the default Gemini suite.
- Gemini 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Gemini accepts
--model <model>,--executable <path>, repeated--arg <value>overrides, and optional--auth-method <id>/--auth-token <token>bootstrap hints. - Gemini repository summary remains opt-in via
--repo <path>, andgemini-cliremains a dedicated-console alias for the canonicalgeminiprovider name. - No arguments also run the default Dsh suite.
- Dsh 默认套件当前包含
Ping、Simple Prompt、Complex Prompt、Session Resume和Tool Call。 - Dsh accepts
--provider <id>with--model <model>(for example--provider omniroute --model claude/claude-haiku-5-5). Libs sends dsh's advertised["<provider>","<model>"]value throughsession/set_config_optionand forwards the model without splitting it.--modelwithout--providerfails before the ACP session starts. Also accepted:--executable <path>, repeated--arg <value>overrides,--dsh-home <path>(exported asDSH_HOMEto the spawned process), and--verbose(adds provider, model, and workspace detail lines to the output). - Dsh runs in the directory given by
--workspace <path>; it has no--repooption.dsh-cliremains a dedicated-console alias for the canonicaldshprovider name. - No arguments also run the default Junie suite.
- Junie 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Junie accepts
--model <model>,--executable <path>, repeated--arg <value>overrides, and optional--auth-method <id>/--auth-token <token>bootstrap hints (--auth-tokenmaps toJUNIE_API_KEY). - Junie repository summary remains opt-in via
--repo <path>, andjunie-cliremains a dedicated-console alias for the canonicaljunieprovider name. - No arguments also run the default Kimi suite.
- Kimi 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Kimi accepts
--model <model>,--executable <path>, repeated--arg <value>overrides, and optional--auth-method <id>/--auth-token <token>bootstrap hints. - Kimi repository summary remains opt-in via
--repo <path>, andkimi-cliremains a dedicated-console alias for the canonicalkimiprovider name. - No arguments also run the default Kiro suite.
- Kiro 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Kiro accepts
--model <model>,--executable <path>, repeated--arg <value>overrides, and optional--auth-method <id>/--auth-token <token>/--bootstrap-method <name>bootstrap hints. - Kiro repository summary remains opt-in via
--repo <path>, andkiro-cliremains a dedicated-console alias for the canonicalkiroprovider name. - No arguments also run the default Pi suite.
- Pi 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Pi accepts
--provider <name>,--model <model>,--session-dir <path>,--thinking <level>,--executable <path>, and repeated--arg <value>overrides. - Pi repository summary remains opt-in via
--repo <path>, andpi-cliremains a dedicated-console alias for the canonicalpiprovider name. - No arguments also run the default OpenCode suite.
- OpenCode 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - OpenCode accepts
--model <model>,--executable <path>,--base-url <url>,--workspace <id>, and repeated--arg <value>. - OpenCode repository summary remains opt-in via
--repo <path>, andopen-code/opencode-cliboth normalize to the canonicalopencodeprovider name. - No arguments also run the default QoderCLI suite.
- QoderCLI 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - No arguments also run the default Reasonix suite.
- Reasonix 默认套件当前包含
Ping、Simple Prompt、Complex Prompt和Session Resume。 - Reasonix accepts
--model <provider>/<model>(a ref defined in your Reasonix configuration),--executable <path>, repeated--arg <value>overrides, and keeps repository summary opt-in via--repo <path>. The console targets the Reasonix 2.x ACP line (verified withv2.41.0). - QoderCLI accepts
--model <model>for explicit model forwarding only; no default model is imposed because supported qodercli model identifiers have not been confirmed yet. - QoderCLI repository summary remains opt-in via
--repo <path>, and the provider now forces ACP sessions intoyolomode for unattended runs.
The DI registration path now exposes all built-in providers:
using HagiCode.Libs.Providers;
using HagiCode.Libs.Providers.ClaudeCode;
using HagiCode.Libs.Providers.Codebuddy;
using HagiCode.Libs.Providers.Copilot;
using HagiCode.Libs.Providers.Codex;
using HagiCode.Libs.Providers.DeepAgents;
using HagiCode.Libs.Providers.Gemini;
using HagiCode.Libs.Providers.Hermes;
using HagiCode.Libs.Providers.Kimi;
using HagiCode.Libs.Providers.Kiro;
using HagiCode.Libs.Providers.OpenCode;
using HagiCode.Libs.Providers.QoderCli;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddHagiCodeLibs();
await using var provider = services.BuildServiceProvider();
var claude = provider.GetRequiredService<ICliProvider<ClaudeCodeOptions>>();
var codebuddy = provider.GetRequiredService<ICliProvider<CodebuddyOptions>>();
var copilot = provider.GetRequiredService<ICliProvider<CopilotOptions>>();
var codex = provider.GetRequiredService<ICodexProvider>();
var deepAgents = provider.GetRequiredService<ICliProvider<DeepAgentsOptions>>();
var gemini = provider.GetRequiredService<ICliProvider<GeminiOptions>>();
var hermes = provider.GetRequiredService<ICliProvider<HermesOptions>>();
var kimi = provider.GetRequiredService<ICliProvider<KimiOptions>>();
var kiro = provider.GetRequiredService<ICliProvider<KiroOptions>>();
var opencode = provider.GetRequiredService<ICliProvider<OpenCodeOptions>>();
var qoderCli = provider.GetRequiredService<ICliProvider<QoderCliOptions>>();OpenCode uses an HTTP runtime/session model instead of ACP. Consumers can attach to an existing runtime or launch a local opencode serve:
await foreach (var message in opencode.ExecuteAsync(
new OpenCodeOptions
{
WorkingDirectory = "/path/to/repo",
Model = "anthropic/claude-sonnet-4",
},
"Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}hagicode-core 里的 ClaudeCodeCliProvider、CodebuddyCliProvider、HermesCliProvider 现已全部降为薄适配层,直接复用这里的 ICliProvider<TOptions> 实现。结论是:同一组 Claude raw-stream / resume、CodeBuddy ACP tool update、Hermes 会话复用与 fallback 语义,现在在 libs 与 core 间保持一致。
ProviderRegistry 也同步暴露了新的兼容别名,便于 console、测试和上层工厂共用同一命名面:
claude-code->claude,claudecode,anthropic-claudecodebuddy->codebuddy-clihermes->hermes-cliopencode->open-code,opencode-cli
CliInstallRegistry now tracks OpenCode as a publicly installable CLI descriptor:
- npm package:
opencode-ai@1.3.3 - executable candidates:
opencode - public installability:
true
The descriptor is aligned with the official OpenCode install guidance (npm i -g opencode-ai@latest) plus the pinned npm version snapshot resolved for this repository on 2026-03-28. Real CLI validation still stays behind the HAGICODE_REAL_CLI_TESTS gate, so the default automated suite remains deterministic even when OpenCode is not installed locally.
ClaudeCodeOptions.ThinkingDisplay is forwarded to the CLI as --thinking-display <mode>. Supported modes are summarized, omitted, and highlights (case-insensitive, surrounding whitespace ignored); any other value throws ArgumentException before a process starts. When unset or empty no flag is sent, so the CLI and model default applies. Current Claude models then stream thinking blocks with empty text, and summarized is what makes the text come back. Display only changes visibility; summaries cost the same tokens as omitted thinking.
--thinking-display is hidden from claude --help. If a CLI exits at launch with an unknown-option error for exactly that flag, before any stream message, the provider starts one replacement process with the same arguments minus the flag, and remembers the executable as unsupported until the host process restarts. Any other failure, and any failure after the first message, is surfaced once with no retry. The --settings argument is never inspected or rewritten: showThinkingSummaries is ignored by the CLI in --print / stream-json mode.
Verified with Claude Code 2.1.292 and 2.1.295: --thinking-display summarized returns thinking text and omitting the flag returns empty thinking text. Older builds are covered only by the fake-CLI fallback tests.
PiProvider runs pi --mode json --print and normalizes its JSONL stream into the shared CliMessage types. A request that sets PiOptions.UserInputHandler runs in Pi's RPC mode instead, so the agent can ask the user questions (see Interactive questions below).
- Package and runtime: the npm package is
@earendil-works/pi-coding-agent, which requires Node>=22.19.0. TheCliInstallRegistryentry records the package name but stays local-only (IsPubliclyInstallable = false, no pinned version); the Desktop manifest owns the Pi version. - Thinking levels:
PiOptions.Thinkingacceptsoff,minimal,low,medium,high,xhighandmax(case-insensitive) and forwards the value as--thinking. Any other value throwsArgumentExceptionbefore a process starts, and the message lists the valid values. - A provider needs a model: setting
PiOptions.ProviderwithoutPiOptions.Model(empty or whitespace counts as none) is a configuration error for every provider name,omnirouteincluded. No process is started and the stream carries a singleterminal.failedthat names the provider, because Pi 1.x would exit with--provider requires --model. - Streaming formats: Pi 0.84 and later emit
message_updateevents that carry only the new fragment; earlier releases (0.78.x) carry a fullmessage/partialsnapshot. The provider recognizes the format per event, with no version probing. Incrementalassistantandassistant.thoughtmessages carrydelta: trueand contain only the new fragment, so a consumer appends them as they are: a whitespace-only fragment, or one equal to earlier text such asThe, is real content. Messages from the snapshot format carry nodeltamarker and are still reconciled against earlier text. Tool input arrives astool.callwhen Pi announces the call and astool.updatewith the complete arguments; partial argument JSON is never forwarded. - New or resumed session: the
sessionheader'stimestampis compared with the time the process was started. A header created by this run givessession.startedwithresumeMode: "started", an older one givessession.resumed, and an id different from the requested one givesrestarted. When the timestamp is missing or unreadable the provider falls back to comparing the returned id with the requested one. - Environment variables: nothing is set by default. To avoid network activity at startup pass
PI_OFFLINE=1,PI_SKIP_VERSION_CHECK=1andPI_TELEMETRY=0throughPiOptions.EnvironmentVariables. - Interactive questions (
PiOptions.UserInputHandler): the handler isnullby default and then nothing changes: the command line, the closed standard input, the completion on process exit and the absence of any extension are exactly what they were. JSON print mode has no UI, so a Pi that can ask must run in RPC mode. When a handler is set (andDisableAllToolsis off; with every tool disabled the handler is ignored and the print path runs) the provider startspi --mode rpcwith the same model, prompt, session, thinking and tool options, adds the bundledask_user_questionextension with-e(and adds the tool to a non-emptyAllowedToolsallowlist), and sends the prompt as an RPCpromptcommand instead of an argument. Because RPC output has no session header, the session id is read withget_stateand reported as the samesession.started/session.resumedmessage the print path produces (RPC cannot tell a session it just created under a requested id from an existing one, so a requested id that Pi reports back issession.resumed); the run ends on Pi'sagent_settledevent rather than on process exit, after which standard input is closed and the process is given 5 seconds to exit. Every other Pi event goes through the samePiJsonEventMapper, so text, thinking, tool and usage messages are identical on both paths. A prompt that Pi rejects (promptreply withsuccess: false) or an exit beforeagent_settledends the stream with aterminal.failedthat keeps Pi's error text, exit code and standard error. The extension is an embedded resource ofHagiCode.Libs.Providers, written once to<temp>/hagicode-libs/pi-extensions/<content hash>/ask-user-question.ts(temporary file, then move; an intact file is reused, so concurrent runs and upgrades agree) and needs no installation. It registersask_user_questionwith the input schema of Claude'sAskUserQuestion(questions[]withquestion,header,multiSelectandoptions[]oflabel/description). Pi's own dialogs cannot carry a header, option descriptions or a multi-select flag (selecttakes plain strings,ctx.ui.custom()is TUI only), so the whole question travels in oneinputdialog: the title is the wire markerhagicode:ask_user_question:v1, a newline and the JSON{ "toolCallId": "...", "questions": [ ... ] }; the answer is the JSON{ "answers": { "<question text>": "<answer>" } }in the dialog value, where a multi-select answer is one string with the values joined by a comma and a space and free text is passed through unchanged. A dialog with another marker version is not a question. When the dialog is cancelled, or the run is aborted (the extension hands Pi's abort signal to the dialog), the tool tells the agent that the user declined. The handler receives aPiUserInputRequest(the dialogRequestId, the agent'sToolCallIdand the questions with header,MultiSelectand option labels and descriptions) and returns aPiUserInputResponsewith the chosen labels or free text per question text. It runs in a background task, so the stream keeps flowing while a person thinks. Returningnull, throwing, orPiOptions.QuestionTimeout(default 30 minutes,Timeout.InfiniteTimeSpanfor no limit; any other non-positive value throwsArgumentOutOfRangeExceptionbefore a process starts) passing answers the dialog as cancelled, the agent is told the user declined and the run goes on to its normal result. Every dialog gets one response. Dialogs of other extensions (select,confirm,editor, aninputwithout the marker) are answered cancelled and never reach the handler; fire-and-forget UI requests (notify,setStatus,setWidget,setTitle,set_editor_text) are ignored. Pi has no tool timeout: an answer after more than 60 seconds worked end to end. The question is also yielded to the caller as anextension_ui_requeststream message, in the position it arrived, andPiUserInputRequest.TryFromCliMessagereads it back; the call and result of the question tool are ordinarytool.call/tool.update/tool.completedmessages, so a consumer that shows the question another way hides them (PiUserInputRequest.QuestionToolName). Callers that persist or parse every message of the stream must recognize theextension_ui_requesttype. Cancelling the call dismisses every open dialog, sends Pi anabort, closes standard input and, when Pi does not exit within the bound, stops the process; the call ends withOperationCanceledExceptionand no Pi process outlives it. Pi refuses to start (exit code 1, no output) when a user extension registersask_user_questionas well; the provider recognizes that, logs a warning through its optionalILogger<PiProvider>and runs the request in JSON print mode without questions. The opt-in real-CLI tests (HAGICODE_LIBS_REAL_CLI_PI_USER_QUESTION=1, optionally..._MODELand..._EXECUTABLE) ask real questions through a real model; the default suite covers the same paths with a scripted process and needs no Pi.
OmpProvider runs omp --mode json --print and normalizes its JSONL stream into the shared CliMessage types. A request that sets OmpOptions.UserInputHandler runs in OMP's rpc-ui mode instead, so the agent can ask the user questions through OMP's built-in ask tool. Verified against omp 18.10.4.
- Interactive questions (
OmpOptions.UserInputHandler): the handler isnullby default and then nothing changes: the command line, the closed standard input and the completion on process exit are exactly what they were. When a handler is set (andDisableAllToolsis off; with every tool disabled the handler is ignored and the print path runs) the provider startsomp --mode rpc-uiwith the same model (omniroute selector handling included), system prompts, thinking,--resume/--no-session, tool and extra options, addsaskto a non-emptyAllowedToolsallowlist, and never passes--no-ui,--print, an extension or a positional prompt. Plain--mode rpchas noasktool. Before the prompt it runs a sequential handshake:{"type":"set_ask_dialog","enabled":true}(OMP's structured ask dialog is off by default for every process), thenget_state, whosedata.sessionIdis reported as the samesession.started/session.resumedmessage the print path produces, and only then the RPCpromptcommand. OMP answers a run withprompt_resultand ends it withsession_settled; background work (an asyncbashjob) can wake the agent after aprompt_resultwhosesessionSettledis false, so the provider keeps streaming untilsession_settled, then closes standard input and gives the process 5 seconds to exit.prompt_result.statusdecides the terminal message:completedgives the print path's completion,erroraterminal.failedwitherror.message(andprovider,model,http_status,retryable),abortedis described below. A prompt that OMP rejects, an exit beforesession_settledand output that is not JSON end the stream with aterminal.failedthat keeps the diagnostic text. Every other event goes through the sameOmpJsonEventMapper.rpc-uiattaches a message snapshot (message,assistantMessageEvent.partial) tomessage_updateevents that--mode jsondoes not send, serialized after later deltas already reached it, so fed as it is the mapper would repeat text; the provider removes it from text and thinking events (they reach the mapper exactly as on the print path) and keeps only thepartialof tool-call events, which gives the call its real id and tool name from its firsttool.call(the print path's delta-only stream starts with atool_callplaceholder untiltoolcall_end).ready,available_commands_updateand widget frames are ignored. Anaskrequest (extension_ui_requestwithmethod: "ask") becomes anOmpUserInputRequest:RequestIdis the frame id (unique peraskcall, stable for the run), and every question carries its OMPId, text,Header,MultiSelectand option labels and descriptions; OMP'spreview,recommendedand the frame'stimeoutare not passed on. The handler returns anOmpUserInputResponsewith the chosen labels or free text per question text, and the provider answers with one{id, selectedOptions, customInput?}per question, in request order. OMP rejects more than one option on a single-select question, an unknown label and a label selected twice (theasktool then fails), so a single-select answer selects a label only when the text equals an offered label (case-sensitive), a multi-select answer only when the text splits on", "into distinct offered labels (the longest label is tried first, so a label may contain a comma), and anything else is sent ascustomInputwith no selected option. The handler runs in a background task, so the stream keeps flowing while a person thinks. Returningnull, leaving a question without an answer, throwing, orOmpOptions.QuestionTimeout(default 30 minutes,Timeout.InfiniteTimeSpanfor no limit; any other non-positive value throwsArgumentOutOfRangeExceptionbefore a process starts) passing answers the request{"cancelled":true}. Unlike Claude and Pi, OMP then ends the run: the tool reportsAsk tool was cancelled by the userandprompt_result.statusisaborted, which the provider reports as a normalterminal.completedwithstop_reason: "aborted", not as a failure; an aborted run the host did not cause (neither a cancelled answer nor a caller cancellation) is aterminal.failedsaying OMP aborted the run. ThetimedOutmarker of OMP's schema is never sent, because OMP answers a timed-outaskwith the recommended (or first) options and lets the agent continue as if the user had chosen them. OMP has no ask timeout of its own (ask.timeoutdefaults to 0): an answer after more than 60 seconds worked end to end. Dialogs of extensions (select,confirm,input,editor) are answered cancelled and never reach the handler (they resolve toundefined/falseand the run goes on); fire-and-forget requests (notify,setStatus,setWidget,setTitle,set_editor_text,open_url) are ignored. Every dialog gets at most one response. The question is also yielded to the caller as anextension_ui_requeststream message, in the position it arrived, andOmpUserInputRequest.TryFromCliMessagereads it back; the call and result of theasktool are ordinarytool.call/tool.update/tool.completedmessages, so a consumer that shows the question another way hides them (OmpUserInputRequest.QuestionToolName). Cancelling the call sends OMPabortand nothing for an open question: OMP closes it with its own{"method":"cancel","targetId":...}frame (the provider cancels the handler's token and writes nothing), ends the runabortedand settles; the provider then closes standard input and stops the process when it does not exit within the bound, and the call ends withOperationCanceledException, so no OMP process outlives it. A build that answersset_ask_dialogwith a failure (one without the command) gets no prompt: the provider logs a warning with theomp --versionoutput through its optionalILogger<OmpProvider>, stops that process and runs the request in JSON print mode without questions. The opt-in real-CLI tests (HAGICODE_LIBS_REAL_CLI_OMP_USER_QUESTION=1, optionally..._MODELand..._EXECUTABLE) ask real questions through a real model in an isolated home; the default suite covers the same paths with frames captured from omp 18.10.4 (tests/HagiCode.Libs.Providers.Tests/Fixtures/Omp) and needs no OMP.
ReasonixProvider drives reasonix acp over the shared ACP session layer (AcpSessionClient). Verified against Reasonix v2.41.0.
- Interactive questions (
ReasonixOptions.UserInputHandler): Reasonix's built-inasktool is always available, and each of its questions arrives as its own ACPsession/request_permissionrequest: tool call idask-<hex>-<turn>-qN, optionsqN:1,qN:2, … plusqN:cancel(reject_once), with the question text, the options (Label/Description, read case-insensitively) and themultiflag intoolCall.rawInputand the header intoolCall.content. The next question of the sameaskcall is sent only after the previous one is answered. The handler isnullby default and then nothing changes: a question request is answered with its first option like any other permission request, no question message is produced, and a warning that names the question is logged through the optionalILogger<ReasonixProvider>. When a handler is set, the provider givesAcpSessionClienta permission hook (AcpPermissionRequestHandler, shared ACP core; the default answer applies when it returnsnull) that routes question requests, and only those, to the handler as aReasonixUserInputRequest(RequestId= theask-…-qNtool call id, oneQuestionwithId, text,Header,MultiSelectas Reasonix reported it, and the options withoutqN:cancel); tool approvals (gate-*) are answered exactly as before. The handler returns aReasonixUserInputResponsewith the chosen label per question text. ACP carries one option id per answer, so the questions are single-choice with no free text: exactly one offered label (matched exactly and as a whole, so a label may contain a comma; with duplicate labels the first option wins) is sent asselected qN:<i>. Anullresponse, an empty answer, text that is not an offered label, more than one value, a throwing handler andReasonixOptions.QuestionTimeout(default 30 minutes,Timeout.InfiniteTimeSpanfor no limit; any other non-positive value throwsArgumentOutOfRangeExceptionbefore a process starts when a handler is set) passing decline the question withselected qN:cancel, or thecancelledoutcome when the request offers no cancel option. Reasonix 2.41.0 does not continue after either: it fails theaskcall withask: context canceledand ends the turn with stop reasonerror. The provider records its own decline and reports such a turn (stop reasonerrororcancelled) asterminal.completedwithquestion_declined: trueand the originalstop_reason; a turn that ends witherrorand no declined question is stillterminal.failed, and a turn that continues after a decline ends with its own result. Reasonix accepted an answer after 120 seconds. Cancelling the call cancels the handler token, answers a pending questioncancelledwhile the transport is still open, records no decline and ends the Reasonix process; the call ends withOperationCanceledException. Each routed question is also yielded to the caller as aquestion.requeststream message (ReasonixUserInputRequest.MessageType) after thetool.callof theaskcall and in the position it arrived;ReasonixUserInputRequest.TryFromCliMessagereads it back from itsparams. Theaskcall itself stays an ordinarytool.call/tool.updatepair whosetoolu_…id differs from the question'sRequestId, so a consumer that shows the question another way hides it by the tool titleask(ReasonixUserInputRequest.QuestionToolName). The sharedAcpJsonRpcClientnow serializes its transport writes, so a delayed answer cannot interleave with another request, and a hook that is still waiting when the client is disposed is cancelled without an error reply. The default suite replays transcripts recorded from Reasonix 2.41.0 (tests/HagiCode.Libs.Providers.Tests/Fixtures/Reasonix/v2.41.0/ask-*.jsonl) and needs no Reasonix;ReasonixProviderRealCliUserQuestionTestsasks real questions whenHAGICODE_REAL_CLI_TESTS=1(optionallyHAGICODE_REAL_CLI_REASONIX_MODEL).
Built-in providers now participate in a shared pooling architecture:
- ACP providers (
CodeBuddy,Gemini,Hermes,Kimi,Kiro,QoderCLI) lease warm ACP sessions from the sharedCliProviderPoolCoordinator. Claude Codekeeps nativeSessionId/Resumecontinuity but always starts a fresh local CLI transport for each invocation and disposes it after the terminal message. Aresultis terminal only once no background subagent is still running and every started turn has produced itsresult; the turn that launches a background subagent ends with its ownresult, and the subagent's output plus the follow-up turn keep streaming afterwards. A subagent can also finish its turn while a background command or monitor it started (a task with aparent_task_id) is still running; the CLI resumes that subagent when the task ends, so the run stays open for it, bounded byClaudeCodeOptions.SubagentBackgroundWorkTimeout(default 20 minutes,Timeout.InfiniteTimeSpanfor no limit). Background work the main agent armed with a declared timeout is awaited too: aMonitor(timeout_ms) or aBashcommand run in the background with an explicittimeout. The agent ends its turn to wait for their events or exit, and closing the stream would kill them, so the run stays open until the task's own timeout plusClaudeCodeOptions.BackgroundWorkGracePeriod(default 1 minute) has passed, which the CLI enforces by ending the task. Both appear as plainlocal_bashtasks and are recognised by their tool call. A backgroundBashcommand with notimeoutis awaited as well, because an agent that starts a build this way and ends its turn to wait for it would otherwise lose the build and its notification when the stream closes; the CLI has no limit for such a command, so the bound is the host's,ClaudeCodeOptions.UntimedBackgroundCommandTimeout(default 30 minutes, counted from the command's start, without the grace period;Timeout.InfiniteTimeSpanfor no limit,TimeSpan.Zeroto never await it). A command meant to run for good, such as a dev server, therefore keeps the run open until that limit passes or the run is stopped. A subagent or background command that finishes while the main agent is still writing its final answer is reported mid-turn, but the CLI can only hand it to the agent by starting another turn right after theresult(it attaches notifications to the request after a tool result, and there is none left); the run therefore waits for that follow-up turn to start, bounded byClaudeCodeOptions.NotificationTurnTimeout(default 10 seconds), instead of closing the stream first. A notification that was already handed over (a tool result of the main agent follows it, or the CLI replayed it withorigin.kindtask-notification) needs no follow-up turn. While a run waits,ClaudeCodeProvideralso yields syntheticagent_work_snapshotmessages (HagiCode.Libs.Core.AgentWork.AgentWorkSnapshot, read back withAgentWorkSnapshot.TryFromCliMessage) that tell the caller what the host is waiting on. Each snapshot is the complete item list: asubagentfor everylocal_agenttask, and amonitorfor aMonitor, a timed backgroundBashcommand, or any task a subagent owns (nested under its subagent throughparentId). An item carries its title, a truncated command, its start anddeadlineAt, the subagent's final summary, andblocksCompletion, which is true exactly while the completion tracker keeps the run open for it. A snapshot is placed right after the CLI message that changed it, at most one per message and only on change; it holds no raw CLI JSON and never alters when the run completes. Status events for a task that never started in the run (a resumed session replays stopped notifications of its old tasks) create no item. Callers that persist or parse every message of the stream must recognize the type and skip it.ClaudeCodeOptions.UserInputHandlerlets the agent ask the user questions during a run. It isnullby default and then nothing changes. When it is set, the provider adds--permission-prompt-tool stdioto the CLI arguments, which makes the CLI registerAskUserQuestionand send its permission prompts to the host ascontrol_requestmessages. AnAskUserQuestionrequest (ClaudeUserInputRequest: request and tool-use ids, questions with header, options andmultiSelect, and the original input) is passed to the handler in a background task, so the stream keeps flowing while the question is pending. The handler returns aClaudeUserInputResponsewith the chosen option labels or free text for each question text; the provider writescontrol_responseallowwithupdatedInputequal to the original input plus ananswersobject, and joins the values of a multi-select question with a comma and a space. Returningnull, a handler that throws,ClaudeCodeOptions.QuestionTimeout(default 30 minutes,Timeout.InfiniteTimeSpanfor no limit) passing, or a caller cancellation answerdenywith a message, so the CLI reports the call underpermission_denialsand the run still reaches its normal result (a cancellation ends the stream withOperationCanceledExceptionas before, after a best-effort deny). While a question is pending the other wait bounds (PromptStartTimeout,SubagentBackgroundWorkTimeout, the monitor deadline plusBackgroundWorkGracePeriod) do not end the run; they apply again once it is resolved. Every othercan_use_toolrequest is denied at once with a fixed message and never reaches the handler, which is what the CLI does today for a tool its permission mode does not approve. The CLI echoes the host's owncontrol_responseon stdout; the provider drops that echo. The request is identified by the tool name because CLI 2.1.159 does not sendrequires_user_interaction, which 2.1.292 does. The interactive request is also yielded to the caller as acontrol_requeststream message, in the position it arrived and after the output that preceded it, andClaudeUserInputRequest.TryFromCliMessagereads it back. Likeagent_work_snapshot, callers that persist or parse every message of the stream must recognize the type and skip or handle it; requests the provider answers itself are not passed on.Codexruns each request in a fresh local CLI process. Resume continuity depends on an explicitThreadId; there is no Codex-specific runtime pooling.Copilotdoes not participate in the shared runtime pool. Each invocation creates a fresh SDK runtime, and continuity depends only on provider-nativeSessionIdresume semantics.
Providers that actually participate in pooling expose PoolSettings so callers can disable pooling or tune provider-level behavior. ClaudeCodeOptions intentionally does not expose provider-level pool controls anymore:
var options = new CodebuddyOptions
{
SessionId = "demo-session",
WorkingDirectory = "/path/to/repo",
PoolSettings = new CliPoolSettings
{
Enabled = true,
IdleTimeout = TimeSpan.FromMinutes(15),
MaxActiveSessions = 50,
KeepAnonymousSessions = false
}
};Operational notes:
- Warm reuse only occurs when the logical session key and compatibility fingerprint still match.
- Each pooled entry executes one prompt at a time through an execution lock.
- Idle entries are reaped before capacity failures are reported; if a provider is still full after TTL cleanup, the oldest idle entry is evicted next.
- Faulted transports and broken ACP sessions are removed immediately instead of being returned to the pool.
CliAcpSessionPool.GetDiagnosticsSnapshot()now reports global plus provider-scoped hit/miss/evict/fault counters, along with the latest eviction/fault reason; the pool also emits structured logs andSystem.Diagnostics.Metricscounters for monitoring hooks.- Default ACP idle TTL baselines remain provider-specific:
codebuddy/deepagents/gemini/kimi/kiro/qodercli=10mandhermes=24h.claude-code,codex,copilot, andopencodedo not register shared local-runtime TTL entries. - Hosts can enable
CliProcessOwnershipOptionsto persist managed subprocess PID ownership.hagicode-corenow defaults this toDataDir/cli-owned-processes.jsonand reaps matching orphaned CLI processes during startup recovery.
CodeBuddy execution options cover the ACP-specific runtime settings without forcing raw command lines:
var options = new CodebuddyOptions
{
Model = "glm-4.7",
WorkingDirectory = "/path/to/repo",
SessionId = "codebuddy-session-123",
ModeId = "plan",
EnvironmentVariables = new Dictionary<string, string?>
{
["CODEBUDDY_TOKEN"] = "<token>"
}
};
await foreach (var message in codebuddy.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}SessionId 会命中共享 ACP 池中的兼容会话;ModeId 会在 session/new 或 warm reuse 后重新下发,确保权限/执行模式与业务层请求保持一致。
Copilot execution options cover SDK-managed session startup without exposing raw prompt-mode wiring. Unsupported startup flags are filtered before launch, while compatible flags remain available through AdditionalArgs:
var options = new CopilotOptions
{
Model = "claude-sonnet-4.5",
WorkingDirectory = "/path/to/repo",
SessionId = "copilot-session-123",
Permissions = new CopilotPermissionOptions
{
AllowAllTools = true,
AllowedPaths = ["/path/to/repo"]
},
AdditionalArgs = ["--config-dir", "/path/to/.copilot"]
};
await foreach (var message in copilot.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}Set SessionId when you want provider-native Copilot resume semantics. The provider first attempts SDK resume for that id, falls back to creating a new session pinned to the requested id when nothing persisted yet, and emits session.started or session.resumed messages accordingly. Requests without SessionId stay anonymous and still create a fresh SDK runtime on every attempt. Legacy PoolSettings values are ignored for compatibility.
Codex execution options cover the common CLI settings without forcing raw command lines:
var options = new CodexSessionOptions
{
ThreadOptions = new ThreadOptions
{
Model = "gpt-5-codex",
SandboxMode = SandboxMode.WorkspaceWrite,
ApprovalPolicy = ApprovalMode.Never,
WorkingDirectory = "/path/to/repo",
AdditionalDirectories = ["/path/to/repo"],
SkipGitRepoCheck = true,
}
};
await using var codexSession = await codex.CreateSessionAsync(options);
var result = await codexSession.Thread.RunAsync("Reply with exactly the word 'pong'");
Console.WriteLine(result.FinalResponse);For Codex sessions, pass a stable CodexSessionOptions.ThreadId when you want to resume a previous conversation. Requests without ThreadId stay anonymous and always start a fresh local CLI process.
Hermes execution options cover the managed hermes acp bootstrap path without forcing raw process wiring. SessionId is treated as an in-memory conversation key for the current provider instance, rather than a cross-process resume token:
var options = new HermesOptions
{
Model = "hermes/default",
WorkingDirectory = "/path/to/repo",
SessionId = "hermes-session-123",
ModeId = "bypassPermissions",
Arguments = ["acp"],
EnvironmentVariables = new Dictionary<string, string?>
{
["HERMES_TOKEN"] = "<token>"
}
};
await foreach (var message in hermes.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}Hermes 的 SessionId 仍表示当前 provider 实例内的会话复用键;ModeId 现在会进入共享池指纹,并在新建/复用 ACP 会话后重新应用。
Gemini execution options cover ACP bootstrap, optional authentication, and session/load resume without forcing consumers to handcraft JSON-RPC calls:
var options = new GeminiOptions
{
WorkingDirectory = "/path/to/repo",
Model = "gemini-2.5-pro",
AuthenticationMethod = "token",
AuthenticationToken = "<token>",
AuthenticationInfo = new Dictionary<string, string?>
{
["region"] = "global"
},
ExtraArguments = ["--profile", "smoke"]
};
await foreach (var message in gemini.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}Kimi execution options cover ACP bootstrap, optional authentication, and session/load resume without forcing consumers to handcraft JSON-RPC calls:
var options = new KimiOptions
{
WorkingDirectory = "/path/to/repo",
Model = "kimi-k2.5",
AuthenticationMethod = "token",
AuthenticationToken = "<token>",
AuthenticationInfo = new Dictionary<string, string?>
{
["region"] = "cn"
},
ExtraArguments = ["--profile", "smoke"]
};
await foreach (var message in kimi.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}Kiro execution options cover the shared ACP bootstrap path with optional authentication and custom bootstrap overrides:
var options = new KiroOptions
{
Model = "kiro-default",
WorkingDirectory = "/path/to/repo",
AuthenticationMethod = "token",
AuthenticationToken = "<token>",
ExtraArguments = ["--profile", "smoke"]
};
await foreach (var message in kiro.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}If initialize advertises auth methods, the provider performs authenticate before creating the session. PingAsync() intentionally reports that state as a local-validation requirement instead of assuming credentials are available in public CI.
QoderCLI execution options mirror the ACP stdio providers while keeping the model override optional. SessionId is forwarded to the session/new or session/resume ACP call when present, and ExtraArguments are appended after the managed --acp bootstrap switch:
var options = new QoderCliOptions
{
WorkingDirectory = "/path/to/repo",
Model = "qoder-max",
EnvironmentVariables = new Dictionary<string, string?>
{
["QODERCLI_TOKEN"] = "<token>"
},
ExtraArguments = ["--profile", "smoke"]
};
await foreach (var message in qoderCli.ExecuteAsync(options, "Reply with exactly the word 'pong'"))
{
Console.WriteLine($"{message.Type}: {message.Content}");
}If you omit Model, the provider forwards no model override so qodercli can use its own default. This keeps the integration aligned with local qodercli installations whose supported model names may vary by environment. The provider also forces each ACP session into yolo mode after session/new or session/load, so unattended prompts can use tool and file-system callbacks without interactive permission gates.
repos/Hagicode.Libs/.github/workflows/cli-discovery-cross-platform.yml runs real CLI discovery on ubuntu-latest, macos-latest, and windows-latest. The workflow uses CiSetup.Console to install all publicly available CLI tools and verify their availability on PATH, then runs provider-specific discovery tests for the installed CLIs.
All CLI metadata is centralized in HagiCode.Libs.Core.Discovery.CliInstallRegistry. Each CliInstallDescriptor records the provider name, npm package, pinned version, executable candidates, and whether the CLI is publicly installable. This registry is the single source of truth — updating it automatically affects CI install/verify behavior.
| Provider | npm Package | Pinned Version | CI-Covered |
|---|---|---|---|
| Claude Code | @anthropic-ai/claude-code |
2.1.79 | Yes |
| Copilot | @github/copilot |
1.0.10 | Yes |
| Codex | @openai/codex |
0.115.0 | Yes |
| CodeBuddy | (private) | — | No |
| Gemini | (local / explicit validation) | — | No |
| Hermes | (private) | — | No |
| Kimi | (local / explicit validation) | — | No |
| Pi | @earendil-works/pi-coding-agent (local / explicit validation, Node >=22.19.0) |
— | No |
| QoderCLI | (private) | — | No |
CLIs with IsPubliclyInstallable = true are automatically installed and tested in CI. Today that public set is Claude Code, Copilot, and Codex. CLIs marked as not publicly installable (CodeBuddy, Gemini, Hermes, Kimi, Pi, QoderCLI) are skipped during CI setup and require local installation for validation.
To reproduce the same real-CLI validation locally from the repos/Hagicode.Libs directory:
# Install and verify all publicly available CLIs
dotnet run --project src/HagiCode.Libs.CiSetup.Console -- --install --verify
# Run CI-targeted discovery tests (Claude Code + Copilot + Codex)
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~ClaudeCodeProviderTests|FullyQualifiedName~CopilotProviderTests|FullyQualifiedName~CodexProviderTests" --logger "console;verbosity=normal"The real-CLI path only checks executable discovery and the auth-free version ping behavior through the provider and dedicated console flows. It does not attempt interactive login or prompt execution, so the default test suite remains usable on machines without the external CLI installed.
HagiCode.Libs.Skills also includes an opt-in suite that calls the live search, well-known discovery, audit, telemetry, and GitHub metadata endpoints. The tests stay disabled by default and only run when HAGICODE_REAL_ONLINE_API_TESTS is enabled.
From repos/Hagicode.Libs, you can run:
HAGICODE_REAL_ONLINE_API_TESTS=1 dotnet test tests/HagiCode.Libs.Skills.Tests/HagiCode.Libs.Skills.Tests.csproj --filter "FullyQualifiedName~RealOnlineApiIntegrationTests" --logger "console;verbosity=normal"GitHub Actions mirrors the same opt-in path in repos/Hagicode.Libs/.github/workflows/online-api-integration.yml.
Each provider can be validated individually when its CLI is available on PATH:
Claude Code:
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~ClaudeCodeProviderTests"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Claude"Codex:
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~CodexProviderTests"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Codex"Copilot:
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~CopilotProviderTests"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Copilot"
dotnet run --project src/HagiCode.Libs.Copilot.Console -- --test-provider-full --model claude-sonnet-4.5 --config-dir .copilot --repo .CodeBuddy (requires local installation):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Codebuddy"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Codebuddy"Hermes (requires local installation):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Hermes"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Hermes"Gemini (requires local installation and any required local auth bootstrap):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Gemini"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Gemini"
dotnet run --project src/HagiCode.Libs.Gemini.Console -- --test-provider-full --repo .Dsh (requires an installed and authenticated dsh whose DSH_HOME can serve Haiku 5.5):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Dsh"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Dsh"
dotnet run --project src/HagiCode.Libs.Dsh.Console -- --test-provider-full --provider omniroute --model claude/claude-haiku-5-5 --workspace .The real-run tests deliberately use Haiku 5.5 to keep token use low. claude/claude-haiku-5-5 is the shared test model, and omniroute is the dsh provider that owns it in the local OmniRoute setup. Confirm both values appear in your authenticated DSH_HOME model list before running these commands; the provider and model are sent as dsh's ["provider","model"] selector, so a mismatch is rejected by dsh. The real-run gate is opt-in and never runs by default.
Junie (requires local installation and any required local auth bootstrap):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Junie"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Junie"
dotnet run --project src/HagiCode.Libs.Junie.Console -- --test-provider-full --repo .Kimi (requires local installation and any required local auth bootstrap):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Kimi"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Kimi"
dotnet run --project src/HagiCode.Libs.Kimi.Console -- --test-provider-full --repo .Pi (requires local installation):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~PiProviderTests"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Pi"
HAGICODE_LIBS_REAL_CLI_PI_USER_QUESTION=1 HAGICODE_LIBS_REAL_CLI_PI_USER_QUESTION_MODEL=<cheap model selector> dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~PiProviderRealCliUserQuestionTests"
dotnet run --project src/HagiCode.Libs.Pi.Console -- --test-provider pi-cli
dotnet run --project src/HagiCode.Libs.Pi.Console -- --test-provider-full --provider omniroute --model glm/glm-4.7 --repo .QoderCLI (requires local installation):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~QoderCli"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~QoderCli"
dotnet run --project src/HagiCode.Libs.QoderCli.Console -- --test-provider-full --repo .Reasonix (requires local installation; targets the 2.x ACP line, verified with v2.41.0; ReasonixConsoleIntegrationTests and the resume, unknown-model and enableYolo real-CLI tests need HAGICODE_REAL_CLI_TESTS=1 and HAGICODE_REAL_CLI_REASONIX_MODEL=<provider>/<model>; ReasonixProviderRealCliUserQuestionTests needs only HAGICODE_REAL_CLI_TESTS=1 and uses the model ref when it is set):
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.Providers.Tests/HagiCode.Libs.Providers.Tests.csproj --filter "FullyQualifiedName~Reasonix"
HAGICODE_REAL_CLI_TESTS=1 dotnet test tests/HagiCode.Libs.ConsoleTesting.Tests/HagiCode.Libs.ConsoleTesting.Tests.csproj --filter "FullyQualifiedName~Reasonix"
dotnet run --project src/HagiCode.Libs.Reasonix.Console -- --test-provider-full --repo .- Zero heavy framework dependencies in the reusable libraries.
- Cross-platform CLI process management for Windows, macOS, and Linux.
- Stream-oriented provider integrations that can power higher-level HagiCode tools.
- Simple provider registration for DI and non-DI consumers.