pig-claude-bridge 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/UPSTREAM.md +2 -2
- package/docs/compatibility.md +17 -4
- package/extensions/pig-claude-bridge/extension.go +38 -0
- package/extensions/pig-claude-bridge/go.mod +1 -1
- package/extensions/pig-claude-bridge/go.sum +2 -2
- package/extensions/pig-claude-bridge/internal/bridge/askclaude.go +1 -0
- package/extensions/pig-claude-bridge/internal/bridge/hooks.go +1 -0
- package/extensions/pig-claude-bridge/internal/bridge/host.go +12 -0
- package/extensions/pig-claude-bridge/internal/bridge/isolated.go +3 -2
- package/extensions/pig-claude-bridge/internal/bridge/notices.go +55 -0
- package/extensions/pig-claude-bridge/internal/bridge/provider.go +5 -1
- package/extensions/pig-claude-bridge/internal/bridge/ratelimit.go +45 -15
- package/extensions/pig-claude-bridge/internal/ccproto/args.go +55 -6
- package/extensions/pig-claude-bridge/internal/ccproto/exec.go +27 -0
- package/extensions/pig-claude-bridge/internal/config/config.go +4 -1
- package/extensions/pig-claude-bridge/internal/models/models.go +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,19 @@ Behind the scenes, PiG's tools are bridged to Claude Code but everything works l
|
|
|
32
32
|
|
|
33
33
|
The model list is a static table generated from the Anthropic catalog of `@earendil-works/pi-ai` 1.2.0 (`scripts/dev/gen-catalog.mjs`) and registered when the extension loads; it does not follow later catalog updates. Dated snapshot ids are not shown. See [docs/compatibility.md](docs/compatibility.md) (D5).
|
|
34
34
|
|
|
35
|
-
**1M context:** Fable 5/5.1, Opus 5.5/5/4.8/4.7, and Sonnet 5.5/5 get 1M context. Opus 4.6 gets 1M
|
|
35
|
+
**1M context:** Fable 5/5.1, Opus 5.5/5/4.8/4.7, and Sonnet 5.5/5 get 1M context. Opus 4.6 gets 1M on Max, Ultra, Team (Standard and Premium seats) and Enterprise plans or with Extra Usage, and Sonnet 4.6 only with Extra Usage ([Claude Code docs](https://code.claude.com/docs/en/model-config#extended-context)): the extension detects the plan by itself (see `provider.plan` in [Configuration](#configuration)); set `provider.longContextExtraUsage` for Extra Usage.
|
|
36
|
+
|
|
37
|
+
**Fast mode:** `/claude-fast` turns Claude Code's fast mode on or off for the rest of the session (`/claude-fast on`, `/claude-fast off`, or no argument to toggle); set `provider.fastMode` to `true` to start every session with it on. It takes effect from the next turn, on models that have fast mode (Opus 5.5, for example); other models ignore it. On subscription plans fast mode is billed to usage credits, not to the plan's limits, and on Team and Enterprise an Owner has to enable it for the organization first ([Claude Code docs](https://code.claude.com/docs/en/fast-mode)). While it is on, the footer shows `fast` (see [Footer status](#footer-status)).
|
|
38
|
+
|
|
39
|
+
### Footer status
|
|
40
|
+
|
|
41
|
+
The bridge keeps one entry in PiG's footer status bar, for example `Claude Max · fast · 5h 6% ↻3h · 7d 76% ↻8h · Fable 99% ↻8h`:
|
|
42
|
+
|
|
43
|
+
- the plan (`Pro`, `Max`, `Ultra`, `Team`, `Enterprise`), when `provider.plan` is set or the plan was detected; nothing when it is unknown.
|
|
44
|
+
- `fast` while fast mode is on; nothing when it is off.
|
|
45
|
+
- the plan's usage limits, once Claude Code reports them (see [Plan usage for footers and quota extensions](#plan-usage-for-footers-and-quota-extensions)).
|
|
46
|
+
|
|
47
|
+
The plan shows from session start, fast mode as soon as `/claude-fast` changes it, and the usage after the first turn.
|
|
36
48
|
|
|
37
49
|
## AskClaude Tool
|
|
38
50
|
|
|
@@ -57,6 +69,7 @@ The extension asks PiG for both directories; nothing is hard-coded. A missing or
|
|
|
57
69
|
},
|
|
58
70
|
"provider": {
|
|
59
71
|
"plan": "max",
|
|
72
|
+
"fastMode": false,
|
|
60
73
|
"longContextExtraUsage": false,
|
|
61
74
|
"strictMcpConfig": true,
|
|
62
75
|
"pathToClaudeCodeExecutable": "/home/you/.nix-profile/bin/claude"
|
|
@@ -75,14 +88,15 @@ The extension asks PiG for both directories; nothing is hard-coded. A missing or
|
|
|
75
88
|
- `appendSkills`: forward PiG's skills block into the system prompt (default `true`)
|
|
76
89
|
|
|
77
90
|
`provider`:
|
|
78
|
-
- `plan`
|
|
91
|
+
- `plan`: `"pro"`, `"max"`, `"ultra"`, `"team"` or `"enterprise"`. Every plan except `"pro"` enables Opus 4.6 with 1M context. Unset, the extension asks `claude auth status --json` once at load and uses the subscription it reports; when that fails (no `claude` on `PATH`, not logged in, or an API key login) it falls back to `"pro"`.
|
|
92
|
+
- `fastMode` (default `false`): start every session with Claude Code's fast mode on. `/claude-fast` changes it for the current session.
|
|
79
93
|
- `longContextExtraUsage`: set to `true` to enable 1M context models even if they cost money through Extra Usage on your plan. It enables Sonnet 4.6 with 1M on every plan and Opus 4.6 with 1M on Pro.
|
|
80
94
|
- `forceTwoHundredK`: array of model ids to pin to 200K context (bare id, no `[1m]` suffix). Use if the catalog declares a model at 1M but Claude Code won't serve it on your plan.
|
|
81
95
|
- `strictMcpConfig`: block MCP servers from `~/.claude.json` / `.mcp.json` (default `true`). Cloud MCP (Gmail/Drive via claude.ai OAuth) is always blocked.
|
|
82
96
|
- `autoMemoryEnabled`: enable Claude Code's auto-memory system (default `false`)
|
|
83
97
|
- `pathToClaudeCodeExecutable`: path to the `claude` binary, used instead of the one on `PATH`. A path ending in `.js`, `.mjs` or `.ts` is run through `node`, as the Agent SDK does.
|
|
84
98
|
|
|
85
|
-
**Notices:** the startup notice, shown at the first bridge turn in the TUI only (it stays pending in RPC, JSON and print mode), points out when `provider.plan` is unset and records `startupNoticeShown` (a date) in the global config. A deprecation notice on first use of AskClaude records its display date as `askClaudeDeprecationShown`. The extension rewrites the global file only to add these keys and leaves every other key as it was; an unparseable global file is left alone.
|
|
99
|
+
**Notices:** the startup notice, shown at the first bridge turn in the TUI only (it stays pending in RPC, JSON and print mode), points out when `provider.plan` is unset and was not detected, and records `startupNoticeShown` (a date) in the global config. A deprecation notice on first use of AskClaude records its display date as `askClaudeDeprecationShown`. The extension rewrites the global file only to add these keys and leaves every other key as it was; an unparseable global file is left alone.
|
|
86
100
|
|
|
87
101
|
## Why Go
|
|
88
102
|
|
|
@@ -166,7 +180,7 @@ PiG runs compaction and branch summaries natively. The bridge routes their `cach
|
|
|
166
180
|
|
|
167
181
|
### Plan usage for footers and quota extensions
|
|
168
182
|
|
|
169
|
-
Claude Code sends a `rate_limit_event` when a usage window changes. The TypeScript bridge passes it to Pi's `after_provider_response` event as `anthropic-ratelimit-unified-*` headers. PiG's host does not emit that event, so this port writes the headers to the debug log only; the user-visible rate-limit notices stay. The bridge also shows the plan's limits in PiG's footer status bar, for example `Claude 5h 6% ↻3h · 7d 76% ↻8h · Fable 99% ↻8h · reset available`. On each event it asks Claude Code for its `/usage` rows (`get_usage`), which name per-model limits; when that fails it shows the event's own windows. This is difference D1 in [docs/compatibility.md](docs/compatibility.md).
|
|
183
|
+
Claude Code sends a `rate_limit_event` when a usage window changes. The TypeScript bridge passes it to Pi's `after_provider_response` event as `anthropic-ratelimit-unified-*` headers. PiG's host does not emit that event, so this port writes the headers to the debug log only; the user-visible rate-limit notices stay. The bridge also shows the plan's limits in PiG's footer status bar, after the plan and fast mode (see [Footer status](#footer-status)), for example `Claude Max · 5h 6% ↻3h · 7d 76% ↻8h · Fable 99% ↻8h · reset available`. On each event it asks Claude Code for its `/usage` rows (`get_usage`), which name per-model limits; when that fails it shows the event's own windows. This is difference D1 in [docs/compatibility.md](docs/compatibility.md).
|
|
170
184
|
|
|
171
185
|
## Known issues
|
|
172
186
|
|
|
@@ -176,6 +190,8 @@ Claude Code sends a `rate_limit_event` when a usage window changes. The TypeScri
|
|
|
176
190
|
|
|
177
191
|
**System prompt changes mid-session may not reach the model.** Project context (AGENTS.md/CLAUDE.md), skills, and extension-written instructions are captured on the first request and reused on resume. This keeps the cached prefix stable, but later changes may not take effect until a rebuild or compaction. Start a new session if updated instructions must take effect immediately.
|
|
178
192
|
|
|
193
|
+
**Claude Code's feature flags and experiments are off.** The bridge starts Claude Code with `CLAUDE_CODE_DISABLE_FEATURE_FLAGS=1` (and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, which implies it), so the GrowthBook flags and A/B experiments Claude Code evaluates at startup (`tengu_*`, cached in `~/.claude.json`) all take their built-in defaults in bridge sessions. That keeps a client-side experiment (a prompt or tool tweak, a limit, a feature toggle) from changing a session unannounced; a change made on the server side of a model is outside what any client can switch off.
|
|
194
|
+
|
|
179
195
|
**Exported Anthropic environment variables override the Claude Code child (pi-claude-bridge issue #107).** An exported `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, or `ANTHROPIC_AUTH_TOKEN` redirects Claude Code to that gateway and every turn fails with its auth error. Unset them for the PiG process.
|
|
180
196
|
|
|
181
197
|
## More
|
package/UPSTREAM.md
CHANGED
|
@@ -6,11 +6,11 @@ This extension is a Go port of [pi-claude-bridge](https://github.com/elidickinso
|
|
|
6
6
|
|
|
7
7
|
| what | pin |
|
|
8
8
|
|---|---|
|
|
9
|
-
| pi-claude-bridge | `6aa276a1498e5376363f5c1f8cf04bfcaee38c93
|
|
9
|
+
| pi-claude-bridge | `6aa276a1498e5376363f5c1f8cf04bfcaee38c93`, plus these later commits ported on their own: f502f5d (silent-turn reminder, MCP description limit), 2edde93 (interrupted-turn resume), 5c4a268 (default output style), e93d51d (parent-session env markers), 0b096a8 (thinking off). The 0.10.0 session-sync rework (#222) and the AskClaude split (0.11.0) are not ported |
|
|
10
10
|
| cc-session-io | 0.4.0 (MIT), the subset the bridge imports |
|
|
11
11
|
| `@anthropic-ai/claude-agent-sdk` | 0.3.310 (Claude Code 2.1.310). The protocol client was written from the SDK's public type definitions and live probes ([protocol reference](docs/reference/agent-sdk-protocol-0.3.310.md)); the SDK itself is not vendored or depended on |
|
|
12
12
|
| Claude Code | 2.1.310 (`ccproto.MinVersion`) |
|
|
13
|
-
| PiG SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.
|
|
13
|
+
| PiG SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.7.0 (PiG 0.7.0); the [signatures](docs/reference/pig-sdk-signatures-v0.5.3.md) recorded at v0.5.3 still match every symbol the bridge uses |
|
|
14
14
|
| `@modelcontextprotocol/sdk` | 1.29.0, only as the reference for the JSON-RPC responses the in-process MCP server must produce |
|
|
15
15
|
| `@earendil-works/pi-ai` | 1.2.0, source of the generated model catalog |
|
|
16
16
|
| `golang.org/x/text` | v0.43.0, for NFC normalisation of project paths |
|
package/docs/compatibility.md
CHANGED
|
@@ -6,11 +6,11 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
6
6
|
|
|
7
7
|
| what | pin |
|
|
8
8
|
|---|---|
|
|
9
|
-
| PiG | 0.
|
|
10
|
-
| PiG Go SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.
|
|
11
|
-
| Claude Code | 2.1.310 (the port warns when the binary is older) |
|
|
9
|
+
| PiG | 0.7.0 (`pig 0.7.0+1.5.0`) |
|
|
10
|
+
| PiG Go SDK | `github.com/MichaelKinsy/PiG/extensions/sdk` v0.7.0, Go 1.26 floor |
|
|
11
|
+
| Claude Code | 2.1.310 minimum (the port warns when the binary is older); verified against 2.1.319 |
|
|
12
12
|
| Agent SDK protocol | `@anthropic-ai/claude-agent-sdk` 0.3.310 (Claude Code 2.1.310); [reference](reference/agent-sdk-protocol-0.3.310.md). 0.3.308 to 0.3.310 changes nothing on the wire that the bridge uses; the one new control subtype, survey feedback, is never sent |
|
|
13
|
-
| pi-claude-bridge | `6aa276a`, https://github.com/elidickinson/pi-claude-bridge |
|
|
13
|
+
| pi-claude-bridge | `6aa276a`, https://github.com/elidickinson/pi-claude-bridge, plus the later commits listed in [../UPSTREAM.md](../UPSTREAM.md) (see "Ported after 6aa276a" below) |
|
|
14
14
|
| cc-session-io | 0.4.0 (MIT) |
|
|
15
15
|
| Pi compatibility surface | pi 1.4.0 (the Pi that PiG 0.5.3 tracks) |
|
|
16
16
|
| `@modelcontextprotocol/sdk` | 1.31.0 (the TypeScript declares `^1.29.0`), only as the reference for the JSON-RPC responses the in-process MCP server produces |
|
|
@@ -127,6 +127,19 @@ What this port is pinned to, and every place where it differs from the TypeScrip
|
|
|
127
127
|
- The alternative provider for the AskClaude, image and session-resume tests must come from `CLAUDE_BRIDGE_TESTING_ALT_PROVIDER`, `CLAUDE_BRIDGE_TESTING_ALT_MODEL` and that provider's API-key variable, because every live run uses a fresh `PIG_HOME`/`PIG_CODING_AGENT_DIR` (the TypeScript suite used the real agent dir). Without them those checks skip.
|
|
128
128
|
- The deadlock-check log pattern, the shutdown test (a third case for the stdin close) and the subagent test (our own agent definition, run through `@tintinweb/pi-subagents`) are adapted as listed in [verification.md](verification.md).
|
|
129
129
|
|
|
130
|
+
## Ported after 6aa276a
|
|
131
|
+
|
|
132
|
+
Upstream pi-claude-bridge moved on to 0.11.0 (acbe45b, 100 commits) after the pin. These were ported on their own, each with the TypeScript commit; the rest is listed below.
|
|
133
|
+
|
|
134
|
+
- **Child environment (f502f5d, 2edde93).** `CLAUDE_CODE_SILENT_TURN_REMINDER=0`, `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH=16384` and `CLAUDE_CODE_RESUME_INTERRUPTED_TURN_MAX_AGE_MS=1` join `CCChildEnv`. The argv snapshot predates them, so `TestArgvMatchesNodeSdkSnapshot` sets them aside and `TestBuildEnv` pins their values.
|
|
135
|
+
- **Parent-session markers (e93d51d, #198).** When `CLAUDECODE` is set in PiG's environment, the parent Claude Code session's markers are dropped from the child env. The TypeScript also moved `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` off its own `process.env`; the port never set it there.
|
|
136
|
+
- **Default output style (5c4a268, #220).** The provider's `--settings` carries `outputStyle: "default"`. Pinned live by `TestOutputStyleDefaultOverridesAUserLevelOutputStyle`.
|
|
137
|
+
- **Thinking off (0b096a8, #152).** No reasoning level means `--thinking=disabled` on the provider and isolated paths, for reasoning models whose `thinkingLevelMap` does not mark `off` as `null`.
|
|
138
|
+
|
|
139
|
+
Port-only, with no TypeScript counterpart: **`CLAUDE_CODE_DISABLE_FEATURE_FLAGS=1`** in `CCChildEnv`. In Claude Code 2.1.319 feature-flag evaluation (`Ph()`) requires both no disable variable and telemetry on, and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` counts as telemetry off, so the TypeScript already runs with flags off; the explicit variable also keeps the disk cache closed should `CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF` be exported, and `TestDisableFeatureFlagsTurnsFlagEvaluationOff` pins the behaviour.
|
|
140
|
+
|
|
141
|
+
Not ported from 0.10.0 and 0.11.0: the content-digest session sync (#222: forks and trims instead of rebuilds, atomic session state, `/fork` and clone continuing the parent's Claude Code session), `CLAUDE_BRIDGE_TRACE_DIR`, provider `env` from `auth.json`, the Extra Usage billing notice, the empty-tool-result placeholder (#227), Claude Code's normalised tool args (#242), the `settingSources` change that stops loading a repo's `.claude/settings*.json` (#198), the AskClaude split into its own package, and the removal of the `provider.plan` notice (the port detects the plan instead). D1 to D7 above still hold.
|
|
142
|
+
|
|
130
143
|
## Pi-only tests that are not ported
|
|
131
144
|
|
|
132
145
|
The TypeScript suite has 45 `tests/unit-*.mjs` files. Those that test Pi-only wiring with no PiG counterpart are not ported:
|
|
@@ -4,6 +4,7 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"fmt"
|
|
6
6
|
"os"
|
|
7
|
+
"strings"
|
|
7
8
|
"time"
|
|
8
9
|
|
|
9
10
|
sdk "github.com/MichaelKinsy/PiG/extensions/sdk"
|
|
@@ -32,6 +33,12 @@ func (h sdkHost) SetStatus(key, text string) {
|
|
|
32
33
|
h.ctx.SetStatus(key, text)
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// ClearStatus drops the call if the context is stale, as SetStatus does.
|
|
37
|
+
func (h sdkHost) ClearStatus(key string) {
|
|
38
|
+
defer func() { _ = recover() }()
|
|
39
|
+
h.ctx.ClearStatus(key)
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
func (h sdkHost) SessionID() (string, error) { return h.ctx.SessionManager().GetSessionID() }
|
|
36
43
|
func (h sdkHost) Cwd() string { return h.ctx.Cwd() }
|
|
37
44
|
|
|
@@ -124,6 +131,17 @@ func Extension() *sdk.Extension {
|
|
|
124
131
|
configDirName, _ := sdk.ConfigDirName()
|
|
125
132
|
cwd, _ := os.Getwd()
|
|
126
133
|
cfg := config.Load(agentDir, cwd, configDirName)
|
|
134
|
+
// Detected before the provider registers, since the plan decides which models get 1M.
|
|
135
|
+
// Skipped outside a host, so validate-only installs and tests never spawn claude.
|
|
136
|
+
if agentDir != "" && cfg.Provider.Plan == nil {
|
|
137
|
+
plan, err := bridge.DetectPlan(context.Background(), cfg.Provider)
|
|
138
|
+
if err != nil {
|
|
139
|
+
cfg.Problems = append(cfg.Problems, fmt.Sprintf("claude-bridge: plan detection failed: %v", err))
|
|
140
|
+
}
|
|
141
|
+
if plan != "" {
|
|
142
|
+
cfg.Provider.Plan = &plan
|
|
143
|
+
}
|
|
144
|
+
}
|
|
127
145
|
b := bridge.New(bridge.Deps{
|
|
128
146
|
Config: cfg,
|
|
129
147
|
AgentDir: agentDir,
|
|
@@ -150,6 +168,26 @@ func Extension() *sdk.Extension {
|
|
|
150
168
|
"streamSimple": sdk.ProviderStreamSimpleFunc(streamSimple),
|
|
151
169
|
})
|
|
152
170
|
|
|
171
|
+
ext.Command("claude-fast", "Toggle Claude Code fast mode for this session (on|off)", func(ctx sdk.Context, args string) error {
|
|
172
|
+
host := sdkHost{ctx}
|
|
173
|
+
switch strings.TrimSpace(args) {
|
|
174
|
+
case "":
|
|
175
|
+
b.SetFastMode(host, !b.FastMode())
|
|
176
|
+
case "on":
|
|
177
|
+
b.SetFastMode(host, true)
|
|
178
|
+
case "off":
|
|
179
|
+
b.SetFastMode(host, false)
|
|
180
|
+
default:
|
|
181
|
+
return fmt.Errorf("usage: /claude-fast [on|off]")
|
|
182
|
+
}
|
|
183
|
+
state := "off"
|
|
184
|
+
if b.FastMode() {
|
|
185
|
+
state = "on (from the next turn, on models that support it)"
|
|
186
|
+
}
|
|
187
|
+
host.Notify("Claude fast mode "+state, "info")
|
|
188
|
+
return nil
|
|
189
|
+
})
|
|
190
|
+
|
|
153
191
|
if a := cfg.AskClaude; a != nil && a.Enabled != nil && *a.Enabled {
|
|
154
192
|
ext.RegisterTool(askclaude.Definition(b, cfg, agentDir,
|
|
155
193
|
func(ctx sdk.Context) bridge.Host { return sdkHost{ctx} }, requestContext))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
github.com/MichaelKinsy/PiG/extensions/sdk v0.
|
|
2
|
-
github.com/MichaelKinsy/PiG/extensions/sdk v0.
|
|
1
|
+
github.com/MichaelKinsy/PiG/extensions/sdk v0.7.0 h1:5esSYPrwspz6Xo6XMAVJu05qyrp/7wijF5ZqgG0x6kw=
|
|
2
|
+
github.com/MichaelKinsy/PiG/extensions/sdk v0.7.0/go.mod h1:4j0XhCW0EPOBWvqYE7XKjTvIAahI7K1VyyW3KpjTKj8=
|
|
3
3
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
|
4
4
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
|
@@ -117,6 +117,7 @@ func (b *Bridge) PromptAndWait(ctx context.Context, host Host, r AskRequest) (te
|
|
|
117
117
|
// the skills block, the only channel. The preset prompt is unconditional: without it
|
|
118
118
|
// the child lacks the tool and permission guidance the bridge relies on.
|
|
119
119
|
settings := config.ClaudeCodeSettings(b.deps.Config.Provider)
|
|
120
|
+
b.applyFastMode(settings)
|
|
120
121
|
settings["claudeMdExcludes"] = claudeMdExcludes
|
|
121
122
|
debugFile, onStderr := b.cliDebugOptions("askclaude")
|
|
122
123
|
opts := ccproto.Options{
|
|
@@ -121,6 +121,7 @@ func (b *Bridge) hostSessionID(host Host, event string) string {
|
|
|
121
121
|
// session is a fresh conversation.
|
|
122
122
|
func (b *Bridge) OnSessionStart(host Host, reason string) {
|
|
123
123
|
defer b.recoverHandler("session_start")
|
|
124
|
+
b.showStatus(host)
|
|
124
125
|
b.rememberSessionCwd(b.hostSessionID(host, "session_start"), host.Cwd())
|
|
125
126
|
switch reason {
|
|
126
127
|
case "new", "resume", "fork":
|
|
@@ -71,6 +71,12 @@ type Bridge struct {
|
|
|
71
71
|
// captures is the prompt-capture registry, guarded by its own mutex.
|
|
72
72
|
captures *promptcapture.Captures
|
|
73
73
|
longContext models.LongContextSettings
|
|
74
|
+
// fastMode adds fastMode to Claude Code's --settings; /claude-fast flips it mid-session.
|
|
75
|
+
fastMode atomic.Bool
|
|
76
|
+
// plan is provider.plan as configured or detected, "" when unknown; the footer shows it.
|
|
77
|
+
plan string
|
|
78
|
+
// lastUsage is the usage windows the footer last showed, guarded by mu.
|
|
79
|
+
lastUsage string
|
|
74
80
|
|
|
75
81
|
// queries tracks the goroutines a provider call starts, so a shutdown or a test
|
|
76
82
|
// can wait for them.
|
|
@@ -95,6 +101,12 @@ func New(d Deps) *Bridge {
|
|
|
95
101
|
cwds: map[string]string{},
|
|
96
102
|
longContext: LongContextSettings(d.Config.Provider),
|
|
97
103
|
}
|
|
104
|
+
if p := d.Config.Provider; p != nil && p.FastMode != nil {
|
|
105
|
+
b.fastMode.Store(*p.FastMode)
|
|
106
|
+
}
|
|
107
|
+
if p := d.Config.Provider; p != nil && p.Plan != nil {
|
|
108
|
+
b.plan = *p.Plan
|
|
109
|
+
}
|
|
98
110
|
b.captures = promptcapture.NewCaptures(0, b.debugCaptureMiss)
|
|
99
111
|
b.queueStartupNotices()
|
|
100
112
|
for _, p := range d.Config.Problems {
|
|
@@ -100,14 +100,15 @@ func (b *Bridge) runIsolatedSummary(ctx context.Context, host Host, model, piCon
|
|
|
100
100
|
cwd := b.sessionCwdFor(owner)
|
|
101
101
|
cliModel := models.ClaudeCodeModelID(str(model["id"]), b.longContext)
|
|
102
102
|
effort := effortForReasoning(model, options["reasoning"])
|
|
103
|
-
|
|
103
|
+
thinkingOff := thinkingDisabledForReasoning(model, options["reasoning"])
|
|
104
|
+
b.debugf("compact summary: spawn model=%s registeredModel=%v effort=%s thinkingDisabled=%t promptLen=%d", cliModel, model["id"], orDefault(effort), thinkingOff, len([]rune(promptText)))
|
|
104
105
|
|
|
105
106
|
if tools == nil {
|
|
106
107
|
tools = []string{}
|
|
107
108
|
}
|
|
108
109
|
debugFile, onStderr := b.cliDebugOptions(tag)
|
|
109
110
|
opts := ccproto.Options{
|
|
110
|
-
Executable: exe, Cwd: cwd, Model: cliModel, Effort: effort, MaxTurns: 1,
|
|
111
|
+
Executable: exe, Cwd: cwd, Model: cliModel, Effort: effort, ThinkingDisabled: thinkingOff, MaxTurns: 1,
|
|
111
112
|
Tools: tools, ToolsSet: true, AllowedTools: allowedTools,
|
|
112
113
|
SettingSourcesSet: true, Skills: []string{},
|
|
113
114
|
SystemPrompt: str(piContext["systemPrompt"]), SystemPromptSet: true,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
package bridge
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
4
5
|
"fmt"
|
|
5
6
|
"strings"
|
|
6
7
|
|
|
8
|
+
"github.com/balcsida/pig-claude-bridge/extensions/pig-claude-bridge/internal/ccproto"
|
|
7
9
|
"github.com/balcsida/pig-claude-bridge/extensions/pig-claude-bridge/internal/config"
|
|
8
10
|
"github.com/balcsida/pig-claude-bridge/extensions/pig-claude-bridge/internal/models"
|
|
9
11
|
"github.com/balcsida/pig-claude-bridge/extensions/pig-claude-bridge/internal/promptcapture"
|
|
@@ -102,6 +104,44 @@ func LongContextSettings(p *config.ProviderConfig) models.LongContextSettings {
|
|
|
102
104
|
return s
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
// DetectPlan asks Claude Code for the subscription behind its login, for when provider.plan
|
|
108
|
+
// is unset. A subscription the 1M rules don't know is not returned, so the notice still asks.
|
|
109
|
+
func DetectPlan(ctx context.Context, p *config.ProviderConfig) (string, error) {
|
|
110
|
+
configured := ""
|
|
111
|
+
if p != nil && p.PathToClaudeCodeExecutable != nil {
|
|
112
|
+
configured = *p.PathToClaudeCodeExecutable
|
|
113
|
+
}
|
|
114
|
+
exe, err := ccproto.ResolveExecutable(configured)
|
|
115
|
+
if err != nil {
|
|
116
|
+
return "", err
|
|
117
|
+
}
|
|
118
|
+
sub, err := ccproto.SubscriptionType(ctx, exe)
|
|
119
|
+
switch sub {
|
|
120
|
+
case "pro", "max", "ultra", "team", "enterprise":
|
|
121
|
+
return sub, err
|
|
122
|
+
}
|
|
123
|
+
return "", err
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// SetFastMode turns Claude Code's fast mode on or off from the next turn on and updates
|
|
127
|
+
// host's footer; each turn spawns a fresh Claude Code, so a live query keeps the mode it
|
|
128
|
+
// started with.
|
|
129
|
+
func (b *Bridge) SetFastMode(host Host, on bool) {
|
|
130
|
+
b.fastMode.Store(on)
|
|
131
|
+
b.showStatus(host)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// FastMode reports whether the next turn asks Claude Code for fast mode.
|
|
135
|
+
func (b *Bridge) FastMode() bool { return b.fastMode.Load() }
|
|
136
|
+
|
|
137
|
+
// applyFastMode opts settings into fast mode. Claude Code under the SDK keeps fast mode off
|
|
138
|
+
// (sdk_opt_in_required) unless --settings carries fastMode; a model without fast mode ignores it.
|
|
139
|
+
func (b *Bridge) applyFastMode(settings map[string]any) {
|
|
140
|
+
if b.fastMode.Load() {
|
|
141
|
+
settings["fastMode"] = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
105
145
|
// reasoningToEffort maps pi reasoning levels to Claude Code effort levels.
|
|
106
146
|
var reasoningToEffort = map[string]string{
|
|
107
147
|
"minimal": "low", "low": "low", "medium": "medium", "high": "high", "xhigh": "max", "max": "max",
|
|
@@ -115,6 +155,21 @@ var validEfforts = map[string]bool{"low": true, "medium": true, "high": true, "x
|
|
|
115
155
|
// the level is unsupported on that model: no effort is sent, so Claude Code's own
|
|
116
156
|
// default applies. A map value is trusted only when it names a level Claude Code
|
|
117
157
|
// accepts. "" means no effort.
|
|
158
|
+
// thinkingDisabledForReasoning reports whether Claude Code should be told thinking is off. PiG
|
|
159
|
+
// sends no reasoning level when thinking is off, and Claude Code then applies its own default
|
|
160
|
+
// (thinking on). As pi-ai's Anthropic provider: only for reasoning models, and not when the
|
|
161
|
+
// model's thinkingLevelMap marks "off" unsupported (null), as on models that always think.
|
|
162
|
+
func thinkingDisabledForReasoning(model map[string]any, reasoning any) bool {
|
|
163
|
+
if level, _ := reasoning.(string); level != "" {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
if on, _ := model["reasoning"].(bool); !on {
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
off, present := obj(model["thinkingLevelMap"])["off"]
|
|
170
|
+
return !(present && off == nil)
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
func effortForReasoning(model map[string]any, reasoning any) string {
|
|
119
174
|
level, _ := reasoning.(string)
|
|
120
175
|
if level == "" {
|
|
@@ -347,10 +347,14 @@ func (b *Bridge) freshQuery(ctx context.Context, host Host, stream *sdk.ModelEve
|
|
|
347
347
|
settings := config.ClaudeCodeSettings(providerSettings)
|
|
348
348
|
settings["claudeMdExcludes"] = claudeMdExcludes
|
|
349
349
|
settings["includeGitInstructions"] = false
|
|
350
|
+
// A user's output style is persona text written for standalone Claude Code; PiG owns the persona here.
|
|
351
|
+
settings["outputStyle"] = "default"
|
|
352
|
+
b.applyFastMode(settings)
|
|
350
353
|
debugFile, onStderr := b.cliDebugOptions("provider")
|
|
351
354
|
opts := ccproto.Options{
|
|
352
355
|
Executable: exe, Cwd: cwd, Effort: effort, Resume: syncResult.SessionID,
|
|
353
|
-
|
|
356
|
+
ThinkingDisabled: thinkingDisabledForReasoning(model, options["reasoning"]),
|
|
357
|
+
Tools: []string{}, ToolsSet: true, PermissionMode: "bypassPermissions", IncludePartialMessages: true,
|
|
354
358
|
Settings: settings, DebugFile: debugFile, AppendSystemPrompt: systemPromptAppend, ExtraArgs: extraArgs,
|
|
355
359
|
}
|
|
356
360
|
handlers := ccproto.Handlers{OnStderr: onStderr}
|
|
@@ -212,15 +212,18 @@ func (b *Bridge) handleRateLimitEvent(host Host, q *QueryContext, info RateLimit
|
|
|
212
212
|
|
|
213
213
|
// showUsage puts the plan's usage in the footer status. The event's windows lack the
|
|
214
214
|
// per-model limits' names, so it asks the live CLI for its /usage rows and falls back
|
|
215
|
-
// to the windows when there is no client or the CLI does not answer.
|
|
215
|
+
// to the windows when there is no client or the CLI does not answer. An empty usage
|
|
216
|
+
// keeps the last one shown.
|
|
216
217
|
func (b *Bridge) showUsage(host Host, client ccproto.Client, windows map[string]*UsageWindow) {
|
|
217
|
-
|
|
218
|
-
if !ok {
|
|
218
|
+
if _, ok := host.(StatusSetter); !ok {
|
|
219
219
|
return
|
|
220
220
|
}
|
|
221
|
-
set := func(
|
|
222
|
-
if
|
|
223
|
-
|
|
221
|
+
set := func(usage string) {
|
|
222
|
+
if usage != "" {
|
|
223
|
+
b.mu.Lock()
|
|
224
|
+
b.lastUsage = usage
|
|
225
|
+
b.mu.Unlock()
|
|
226
|
+
b.showStatus(host)
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
229
|
if client == nil {
|
|
@@ -242,13 +245,43 @@ func (b *Bridge) showUsage(host Host, client ccproto.Client, windows map[string]
|
|
|
242
245
|
}()
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
// UsageStatusKey is the footer status entry that shows the plan
|
|
248
|
+
// UsageStatusKey is the footer status entry that shows the plan, fast mode and usage windows.
|
|
246
249
|
const UsageStatusKey = "claude-usage"
|
|
247
250
|
|
|
248
251
|
// StatusSetter is a Host that can show a footer status; hosts without one skip it.
|
|
249
|
-
type StatusSetter interface{
|
|
252
|
+
type StatusSetter interface {
|
|
253
|
+
SetStatus(key, text string)
|
|
254
|
+
ClearStatus(key string)
|
|
255
|
+
}
|
|
250
256
|
|
|
251
|
-
//
|
|
257
|
+
// showStatus renders the footer entry as "Claude Max · fast · 5h 6% ↻3h · 7d 76% ↻8h": the
|
|
258
|
+
// plan when configured or detected, "fast" while fast mode is on, then the last usage
|
|
259
|
+
// windows. With none of the three the entry is removed.
|
|
260
|
+
func (b *Bridge) showStatus(host any) {
|
|
261
|
+
s, ok := host.(StatusSetter)
|
|
262
|
+
if !ok {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
var parts []string
|
|
266
|
+
if b.plan != "" {
|
|
267
|
+
parts = append(parts, strings.ToUpper(b.plan[:1])+b.plan[1:])
|
|
268
|
+
}
|
|
269
|
+
if b.fastMode.Load() {
|
|
270
|
+
parts = append(parts, "fast")
|
|
271
|
+
}
|
|
272
|
+
b.mu.Lock()
|
|
273
|
+
if b.lastUsage != "" {
|
|
274
|
+
parts = append(parts, b.lastUsage)
|
|
275
|
+
}
|
|
276
|
+
b.mu.Unlock()
|
|
277
|
+
if len(parts) == 0 {
|
|
278
|
+
s.ClearStatus(UsageStatusKey)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
s.SetStatus(UsageStatusKey, "Claude "+strings.Join(parts, " · "))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// usageStatus renders the unified windows as "5h 14% ↻3h · 7d 1% ↻2d", or ""
|
|
252
285
|
// when no window carries a utilization.
|
|
253
286
|
func usageStatus(windows map[string]*UsageWindow, now time.Time) string {
|
|
254
287
|
var parts []string
|
|
@@ -267,10 +300,7 @@ func usageStatus(windows map[string]*UsageWindow, now time.Time) string {
|
|
|
267
300
|
}
|
|
268
301
|
parts = append(parts, fmt.Sprintf("%s %d%%", label, int(math.Floor(*w.Utilization*100+0.5)))+resetsIn(resetsAt, now))
|
|
269
302
|
}
|
|
270
|
-
|
|
271
|
-
return ""
|
|
272
|
-
}
|
|
273
|
-
return "Claude " + strings.Join(parts, " · ")
|
|
303
|
+
return strings.Join(parts, " · ")
|
|
274
304
|
}
|
|
275
305
|
|
|
276
306
|
// resetsIn is " ↻3h": the time to a window's reset, floored to its largest unit as the
|
|
@@ -307,7 +337,7 @@ type usageReply struct {
|
|
|
307
337
|
}
|
|
308
338
|
|
|
309
339
|
// planUsageStatus renders a get_usage reply as
|
|
310
|
-
// "
|
|
340
|
+
// "5h 6% ↻3h · 7d 76% ↻8h · Fable 99% ↻8h", plus "reset available" when the
|
|
311
341
|
// account holds an unused rate-limit reset. "" when the reply has no windows.
|
|
312
342
|
func planUsageStatus(raw json.RawMessage, now time.Time) string {
|
|
313
343
|
var r usageReply
|
|
@@ -331,7 +361,7 @@ func planUsageStatus(raw json.RawMessage, now time.Time) string {
|
|
|
331
361
|
if holdsUnusedReset(r.RateLimits.CedarEmber) {
|
|
332
362
|
parts = append(parts, "reset available")
|
|
333
363
|
}
|
|
334
|
-
return
|
|
364
|
+
return strings.Join(parts, " · ")
|
|
335
365
|
}
|
|
336
366
|
|
|
337
367
|
// holdsUnusedReset reports whether the usage reply's cedar_ember block (the reset
|
|
@@ -12,11 +12,44 @@ import (
|
|
|
12
12
|
// sdkVersion is the Node Agent SDK version whose behaviour is mirrored.
|
|
13
13
|
const sdkVersion = "0.3.310"
|
|
14
14
|
|
|
15
|
-
// CCChildEnv is applied to every Claude Code child (index.ts CC_CHILD_ENV
|
|
15
|
+
// CCChildEnv is applied to every Claude Code child (index.ts CC_CHILD_ENV, plus the entries
|
|
16
|
+
// pi-claude-bridge added in f502f5d and 2edde93, and CLAUDE_CODE_DISABLE_FEATURE_FLAGS, which is this port's own).
|
|
16
17
|
var CCChildEnv = map[string]string{
|
|
17
18
|
"ENABLE_CLAUDEAI_MCP_SERVERS": "0",
|
|
18
19
|
"DISABLE_AUTO_COMPACT": "1",
|
|
19
20
|
"CLAUDE_CODE_ELEGANT_MEADOW": "0",
|
|
21
|
+
// On Opus 5.5, Fable 5.1, Mythos 5.1 and Sonnet 5.5, Claude Code adds "The user hasn't heard from
|
|
22
|
+
// you in a while..." after 5 assistant messages with no visible text. PiG decides what the model is
|
|
23
|
+
// told; the message is not in PiG's history and the user never sees it.
|
|
24
|
+
"CLAUDE_CODE_SILENT_TURN_REMINDER": "0",
|
|
25
|
+
// Claude Code cuts MCP tool descriptions mid-sentence at 4096 chars; 16384 is its own larger limit.
|
|
26
|
+
"CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH": "16384",
|
|
27
|
+
// A resumed session that ends at a tool result reads to Claude Code as an interrupted turn, and behind
|
|
28
|
+
// a server flag it inserts a "Continue from where you left off." user message ahead of PiG's prompt.
|
|
29
|
+
// PiG supplies every prompt, so every turn counts as too old to resume.
|
|
30
|
+
"CLAUDE_CODE_RESUME_INTERRUPTED_TURN_MAX_AGE_MS": "1",
|
|
31
|
+
// No GrowthBook feature flags or experiments (tengu_*): every flag takes its built-in default, and the
|
|
32
|
+
// cached assignments in ~/.claude.json are not read either. CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
|
|
33
|
+
// already implies this in 2.1.319 (flag evaluation is off whenever telemetry is); the explicit
|
|
34
|
+
// variable pins it. Pinned by TestDisableFeatureFlagsTurnsFlagEvaluationOff.
|
|
35
|
+
"CLAUDE_CODE_DISABLE_FEATURE_FLAGS": "1",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// EnvAddedAfterSnapshot lists the CCChildEnv keys that testdata/sdk-argv-snapshot.json (recorded at
|
|
39
|
+
// pi-claude-bridge 6aa276a) does not have: added upstream later (f502f5d, 2edde93) or by this port
|
|
40
|
+
// (CLAUDE_CODE_DISABLE_FEATURE_FLAGS). Snapshot tests set them aside; TestBuildEnv pins their values.
|
|
41
|
+
var EnvAddedAfterSnapshot = []string{"CLAUDE_CODE_SILENT_TURN_REMINDER", "CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH",
|
|
42
|
+
"CLAUDE_CODE_RESUME_INTERRUPTED_TURN_MAX_AGE_MS", "CLAUDE_CODE_DISABLE_FEATURE_FLAGS"}
|
|
43
|
+
|
|
44
|
+
// parentSessionVars are set by Claude Code on its own child processes, or exported by that parent. When PiG
|
|
45
|
+
// runs inside a Claude Code session they describe that session, not ours: the SDK stamps its own entrypoint
|
|
46
|
+
// and version only when they are unset, and Claude Code's Bash tool runs CLAUDE_ENV_FILE's script.
|
|
47
|
+
// AI_AGENT is left alone. Dropped only when CLAUDECODE says PiG was started from inside Claude Code (index.ts e93d51d).
|
|
48
|
+
var parentSessionVars = []string{
|
|
49
|
+
"CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_SESSION_ATTENDED",
|
|
50
|
+
"CLAUDE_PID", "CLAUDE_EFFORT", "TRACEPARENT", "CLAUDE_CODE_INVOKED_SKILLS", "CLAUDE_ENV_FILE",
|
|
51
|
+
"CLAUDE_CODE_ENTRYPOINT", "CLAUDE_AGENT_SDK_VERSION", "CLAUDE_CODE_EXECPATH",
|
|
52
|
+
"CLAUDE_CODE_SSE_PORT", "CLAUDE_CODE_MESSAGING_SOCKET", "CLAUDE_CODE_MESSAGING_TOKEN",
|
|
20
53
|
}
|
|
21
54
|
|
|
22
55
|
// ExtraArg is one extraArgs entry; a nil Value emits the bare flag.
|
|
@@ -32,7 +65,10 @@ type Options struct {
|
|
|
32
65
|
// Model is the SDK's model option (--model=<id>). The provider and AskClaude pass their
|
|
33
66
|
// model through ExtraArgs instead (two tokens, as the TypeScript's extraArgs.model); only
|
|
34
67
|
// the isolated query sets Model.
|
|
35
|
-
Model, Effort, Resume
|
|
68
|
+
Model, Effort, Resume string
|
|
69
|
+
// ThinkingDisabled is the SDK's thinking:{type:"disabled"} (--thinking=disabled): PiG sends no
|
|
70
|
+
// reasoning level when thinking is off, and Claude Code would otherwise apply its own default.
|
|
71
|
+
ThinkingDisabled bool
|
|
36
72
|
MaxTurns int
|
|
37
73
|
StrictMcpConfig, NoSessionPersistence bool
|
|
38
74
|
IncludePartialMessages bool
|
|
@@ -65,6 +101,9 @@ func sdkOption(key, value string) []string {
|
|
|
65
101
|
// last as two-token pairs (extraArgs first, settings after). SDK MCP servers and the system prompt never appear on argv.
|
|
66
102
|
func BuildArgs(o Options) []string {
|
|
67
103
|
args := []string{"--output-format", "stream-json", "--verbose", "--input-format", "stream-json"}
|
|
104
|
+
if o.ThinkingDisabled {
|
|
105
|
+
args = append(args, "--thinking=disabled")
|
|
106
|
+
}
|
|
68
107
|
if o.Effort != "" {
|
|
69
108
|
args = append(args, "--effort="+o.Effort)
|
|
70
109
|
}
|
|
@@ -127,13 +166,23 @@ func settingsJSON(settings map[string]any) string {
|
|
|
127
166
|
}
|
|
128
167
|
|
|
129
168
|
// BuildEnv returns the child environment: base plus o.Env, minus NODE_OPTIONS, DEBUG, TRACEPARENT and
|
|
130
|
-
// TRACESTATE, with the SDK identity variables added when
|
|
131
|
-
// CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 (
|
|
169
|
+
// TRACESTATE (and parentSessionVars when CLAUDECODE is set), with the SDK identity variables added when
|
|
170
|
+
// unset, then CCChildEnv, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 (the TypeScript sets it for the child
|
|
171
|
+
// only since e93d51d; before, on process.env at load) and PI_CODING_AGENT=true.
|
|
132
172
|
// Later entries win over earlier duplicates in the returned slice (execve semantics for the consumer).
|
|
133
173
|
func BuildEnv(base []string, o Options) []string {
|
|
134
174
|
strip := map[string]bool{"NODE_OPTIONS": true, "DEBUG": true, "TRACEPARENT": true, "TRACESTATE": true}
|
|
135
|
-
|
|
136
|
-
|
|
175
|
+
inherited := append(append([]string{}, base...), o.Env...)
|
|
176
|
+
if slices.ContainsFunc(inherited, func(kv string) bool {
|
|
177
|
+
k, v, _ := strings.Cut(kv, "=")
|
|
178
|
+
return k == "CLAUDECODE" && v != ""
|
|
179
|
+
}) {
|
|
180
|
+
for _, k := range parentSessionVars {
|
|
181
|
+
strip[k] = true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
env := make([]string, 0, len(inherited)+8)
|
|
185
|
+
for _, kv := range inherited {
|
|
137
186
|
if !strip[strings.SplitN(kv, "=", 2)[0]] {
|
|
138
187
|
env = append(env, kv)
|
|
139
188
|
}
|
|
@@ -4,6 +4,8 @@ package ccproto
|
|
|
4
4
|
|
|
5
5
|
import (
|
|
6
6
|
"context"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"errors"
|
|
7
9
|
"fmt"
|
|
8
10
|
"os/exec"
|
|
9
11
|
"path/filepath"
|
|
@@ -75,6 +77,31 @@ func Version(ctx context.Context, exe string) (string, error) {
|
|
|
75
77
|
return m[1], nil
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// AuthStatusTimeout bounds `claude auth status`: it runs while the extension loads, which must not hang.
|
|
81
|
+
var AuthStatusTimeout = 5 * time.Second
|
|
82
|
+
|
|
83
|
+
// SubscriptionType runs `<exe> auth status --json` and returns its subscriptionType ("pro", "max",
|
|
84
|
+
// "team", "enterprise"); "" when the login is not a Claude subscription.
|
|
85
|
+
func SubscriptionType(ctx context.Context, exe string) (string, error) {
|
|
86
|
+
probe, cancel := context.WithTimeout(ctx, AuthStatusTimeout)
|
|
87
|
+
defer cancel()
|
|
88
|
+
name, args, err := commandLine(exe, "auth", "status", "--json")
|
|
89
|
+
if err != nil {
|
|
90
|
+
return "", err
|
|
91
|
+
}
|
|
92
|
+
cmd := exec.CommandContext(probe, name, args...)
|
|
93
|
+
cmd.WaitDelay = time.Second
|
|
94
|
+
// Logged out exits non-zero but still prints the JSON, so the output decides.
|
|
95
|
+
out, runErr := cmd.Output()
|
|
96
|
+
var status struct {
|
|
97
|
+
SubscriptionType string `json:"subscriptionType"`
|
|
98
|
+
}
|
|
99
|
+
if err := json.Unmarshal(out, &status); err != nil {
|
|
100
|
+
return "", fmt.Errorf("run %s auth status --json: %v (output %q)", exe, errors.Join(runErr, err), strings.TrimSpace(string(out)))
|
|
101
|
+
}
|
|
102
|
+
return status.SubscriptionType, nil
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
// OlderThan reports whether version is numerically older than minimum; unparsable input is not older.
|
|
79
106
|
func OlderThan(version, minimum string) bool {
|
|
80
107
|
a, okA := parseSemver(version)
|
|
@@ -43,8 +43,11 @@ type ProviderConfig struct {
|
|
|
43
43
|
StrictMcpConfig *bool `json:"strictMcpConfig,omitempty"`
|
|
44
44
|
AutoMemoryEnabled *bool `json:"autoMemoryEnabled,omitempty"`
|
|
45
45
|
PathToClaudeCodeExecutable *string `json:"pathToClaudeCodeExecutable,omitempty"`
|
|
46
|
-
// Plan is the subscription tier ("pro" | "max"
|
|
46
|
+
// Plan is the subscription tier ("pro" | "max" | "ultra" | "team" | "enterprise"). Every plan
|
|
47
|
+
// but "pro" enables Opus 4.6 at 1M context. Unset, the extension asks `claude auth status` at load.
|
|
47
48
|
Plan *string `json:"plan,omitempty"`
|
|
49
|
+
// FastMode starts every session with Claude Code's fast mode on; /claude-fast toggles it.
|
|
50
|
+
FastMode *bool `json:"fastMode,omitempty"`
|
|
48
51
|
// LongContextExtraUsage opts into metered 1M context usage. Enables Sonnet 4.6 [1m]
|
|
49
52
|
// on every plan and Opus 4.6 [1m] on Pro.
|
|
50
53
|
LongContextExtraUsage *bool `json:"longContextExtraUsage,omitempty"`
|
|
@@ -18,7 +18,7 @@ const (
|
|
|
18
18
|
|
|
19
19
|
// LongContextSettings carries the plan facts that decide whether a [1m] id is served.
|
|
20
20
|
type LongContextSettings struct {
|
|
21
|
-
Plan string // "pro" or "
|
|
21
|
+
Plan string // "pro", "max", "ultra", "team" or "enterprise"
|
|
22
22
|
LongContextExtraUsage bool
|
|
23
23
|
// ForceTwoHundredK lists model ids whose declared 1M context Claude Code
|
|
24
24
|
// turned out not to serve; they stay on the bare id at 200K.
|
|
@@ -58,8 +58,11 @@ var measuredOneM = map[string]bool{
|
|
|
58
58
|
// planGatedOneM holds the measured exceptions: pi-ai declares 1M and the [1m]
|
|
59
59
|
// id works, but only when the plan allows it.
|
|
60
60
|
var planGatedOneM = map[string]func(LongContextSettings) bool{
|
|
61
|
-
// [1m] measured 1M on Max plan / extra usage; 429 on Pro without it.
|
|
62
|
-
|
|
61
|
+
// [1m] measured 1M on Max plan / extra usage; 429 on Pro without it. Max, Ultra, Team (Standard
|
|
62
|
+
// and Premium) and Enterprise include it, per code.claude.com/docs/en/model-config#extended-context.
|
|
63
|
+
"claude-opus-4-6": func(s LongContextSettings) bool {
|
|
64
|
+
return slices.Contains([]string{"max", "ultra", "team", "enterprise"}, s.Plan) || s.LongContextExtraUsage
|
|
65
|
+
},
|
|
63
66
|
// [1m] measured 1M with extra usage only.
|
|
64
67
|
"claude-sonnet-4-6": func(s LongContextSettings) bool { return s.LongContextExtraUsage },
|
|
65
68
|
}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"pig-claude-bridge","version":"0.
|
|
1
|
+
{"name":"pig-claude-bridge","version":"0.4.0","description":"Claude Code in PiG: Claude models as a provider and an AskClaude tool. A native Go port of pi-claude-bridge.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/balcsida/pig-claude-bridge.git"},"files":["extensions","!**/*_test.go","!**/testdata","docs/compatibility.md","UPSTREAM.md","THIRD_PARTY_NOTICES.md","SECURITY.md"],"keywords":["pig-package"],"pi":{"extensions":["extensions/pig-claude-bridge"]}}
|