@jameslovespancakes/pi-plus 1.0.10 → 1.0.12
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 +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
**Everything [pi](https://pi.dev/) is missing, in one install.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@jameslovespancakes/pi-plus)
|
|
8
|
-
[](https://github.com/jameslovespancakes/pi-plus/actions/workflows/ci.yml)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://pi.dev/packages)
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ on any row starts that setup.
|
|
|
29
29
|
```
|
|
30
30
|
──────────────────────────────────────────────────────────
|
|
31
31
|
pi-plus
|
|
32
|
-
› ● Subscriptions 2 accounts ·
|
|
32
|
+
› ● Subscriptions 2 accounts · quota-aware
|
|
33
33
|
● Model Information catalogue cached
|
|
34
34
|
● Providers 2 allowed · 4 need approval
|
|
35
35
|
● Agent Board not configured
|
|
@@ -48,13 +48,14 @@ Nothing else is required, every feature configures itself from within pi.
|
|
|
48
48
|
|
|
49
49
|
### Pool every subscription
|
|
50
50
|
|
|
51
|
-
Add
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
Add multiple Claude, ChatGPT/Codex, Kimi Code, or xAI/Grok accounts. pi-plus
|
|
52
|
+
keeps credentials separate, refreshes them safely, and supports sequential or
|
|
53
|
+
quota-aware routing.
|
|
54
54
|
|
|
55
55
|
```
|
|
56
|
-
/
|
|
57
|
-
/
|
|
56
|
+
/accounts add anthropic work
|
|
57
|
+
/accounts add kimi-coding personal
|
|
58
|
+
/routing quota-aware
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
Your live quota, always in the footer:
|
|
@@ -70,9 +71,9 @@ Your live quota, always in the footer:
|
|
|
70
71
|
With more than two accounts only the two most recently used are listed, so the
|
|
71
72
|
footer stays a fixed height however many you pool.
|
|
72
73
|
|
|
73
|
-
Account and routing commands are provider-agnostic
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
Account and routing commands are provider-agnostic. Sequential routing uses
|
|
75
|
+
account order; quota-aware routing uses reported capacity and fairly probes
|
|
76
|
+
accounts whose provider does not publish quota headers.
|
|
76
77
|
|
|
77
78
|
### Pick models on evidence
|
|
78
79
|
|
|
@@ -153,6 +154,23 @@ across every running pi agent. `/board` opens the messaging view:
|
|
|
153
154
|
any Mac or Linux host over SSH, where launchd or systemd brings it back after a
|
|
154
155
|
reboot.
|
|
155
156
|
|
|
157
|
+
### Orchestrate repeatable workflows
|
|
158
|
+
|
|
159
|
+
The built-in workflow engine runs named or inline multi-agent workflows with
|
|
160
|
+
optional concurrency limits, replay, background runs, worktree isolation, progress,
|
|
161
|
+
and usage accounting. `code-review`, `diagnose`, `perf-review`,
|
|
162
|
+
`refactor-scout`, and `research` ship in this package—no external workflow
|
|
163
|
+
package is installed.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
/workflow code-review HEAD~3
|
|
167
|
+
/workflow research "Compare the current provider implementations"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The `workflow` tool exposes the same engine to the model. Runs have no agent,
|
|
171
|
+
timeout, submission, or token-budget limit by default; set limits explicitly
|
|
172
|
+
with workflow options when a task needs them.
|
|
173
|
+
|
|
156
174
|
---
|
|
157
175
|
|
|
158
176
|
## Commands
|
|
@@ -161,10 +179,10 @@ reboot.
|
|
|
161
179
|
| --- | --- |
|
|
162
180
|
| `/pi-plus` | status modal for every feature |
|
|
163
181
|
| `/pi-plus help` | model explains the pack and what is missing |
|
|
164
|
-
| `/
|
|
165
|
-
| `/
|
|
166
|
-
| `/
|
|
167
|
-
| `/routing
|
|
182
|
+
| `/accounts` | account hub: toggle, add, reauth, switch routing |
|
|
183
|
+
| `/accounts add <provider> [label]` | add a subscription |
|
|
184
|
+
| `/accounts reauth <provider> [label]` | reauthorize one |
|
|
185
|
+
| `/routing sequential \| quota-aware` | account order, or remaining capacity |
|
|
168
186
|
| `/usage [on\|off\|text]` | quota bars |
|
|
169
187
|
| `/models [sort]` | ranked catalog |
|
|
170
188
|
| `/model-info <id>` | every benchmark for one model |
|
|
@@ -176,9 +194,11 @@ reboot.
|
|
|
176
194
|
| `/remote add \| rename \| remove` | jump to one step |
|
|
177
195
|
| `/board` | live agent board UI |
|
|
178
196
|
| `/board setup \| restart \| clear \| status` | manage the board server |
|
|
197
|
+
| `/workflow` | open the running workflow agent board |
|
|
198
|
+
| `/workflow <name> [args]` | run a bundled workflow |
|
|
179
199
|
|
|
180
|
-
**Tools available to the agent:** `
|
|
181
|
-
`remote_test`.
|
|
200
|
+
**Tools available to the agent:** `workflow`, `list_models`, `agent_board`,
|
|
201
|
+
`remote_status`, `remote_test`.
|
|
182
202
|
|
|
183
203
|
---
|
|
184
204
|
|
|
@@ -213,13 +233,13 @@ src/
|
|
|
213
233
|
usage-service, the single quota poller
|
|
214
234
|
ui/ dumb render primitives: format · usage-bars
|
|
215
235
|
domains/ one pi extension entry each
|
|
216
|
-
setup · subscriptions · models · agents · remote
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
setup · subscriptions · models · workflows · agents · remote
|
|
237
|
+
server/ agent board server
|
|
238
|
+
config/ example settings and bundled skills
|
|
219
239
|
```
|
|
220
240
|
|
|
221
241
|
```sh
|
|
222
|
-
npm install && npm run verify #
|
|
242
|
+
npm install && npm run verify # lint + type check + tests
|
|
223
243
|
```
|
|
224
244
|
|
|
225
245
|
---
|
|
@@ -232,6 +252,10 @@ pi-plus is a thin layer over other people's work.
|
|
|
232
252
|
| --- | --- | --- |
|
|
233
253
|
| [`pi`](https://pi.dev/) | the host agent and the entire extension API | MIT |
|
|
234
254
|
| [`xxhash-wasm`](https://github.com/jungomi/xxhash-wasm) | vendored into `src/core/anthropic/vendor/` for the billing checksum | MIT |
|
|
255
|
+
| [`pi-workflow-engine`](https://github.com/timbrinded/pi-workflow-engine) | embedded workflow runtime and built-in workflows | MIT |
|
|
256
|
+
|
|
257
|
+
The workflow engine keeps its upstream license in
|
|
258
|
+
[`src/domains/workflows/LICENSE.md`](src/domains/workflows/LICENSE.md).
|
|
235
259
|
|
|
236
260
|
The Anthropic provider, OAuth, quota and routing were originally adopted from
|
|
237
261
|
[`@cortexkit/pi-anthropic-auth`](https://github.com/cortexkit/anthropic-auth)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-code-review-actions
|
|
3
|
+
description: "Act on selected workflow code-review findings: make minimal fixes or post GitHub PR inline comments using gh, GitHub MCP/tools, or project-specific tools."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use this skill when the parent agent receives selected findings from the workflow code review.
|
|
7
|
+
|
|
8
|
+
## Inputs
|
|
9
|
+
|
|
10
|
+
The prompt will include compact JSON with:
|
|
11
|
+
|
|
12
|
+
- `context`: workflow name, target, diff command, changed files, and optional summary.
|
|
13
|
+
- `issues`: selected findings with `id`, `summary`, `category`, `severity`, `confidence`, `location`, `impact`, `evidence`, and `recommendation`.
|
|
14
|
+
|
|
15
|
+
## Fix mode
|
|
16
|
+
|
|
17
|
+
When mode is `fix selected code-review findings`:
|
|
18
|
+
|
|
19
|
+
1. Inspect the selected issue JSON before editing.
|
|
20
|
+
2. Make minimal edits that address only the selected findings.
|
|
21
|
+
3. Preserve unrelated user changes and avoid broad refactors.
|
|
22
|
+
4. Run focused validation if available for the touched files or behavior.
|
|
23
|
+
5. Summarize changed files and validation results.
|
|
24
|
+
6. Do not post GitHub PR comments in fix mode.
|
|
25
|
+
|
|
26
|
+
## Comment mode
|
|
27
|
+
|
|
28
|
+
When mode is `post inline GitHub PR comments`:
|
|
29
|
+
|
|
30
|
+
1. Do not edit files or make code changes.
|
|
31
|
+
2. Prefer installed GitHub MCP/tools if visible in the active tool list.
|
|
32
|
+
3. If no GitHub MCP/tools are available, use `gh`:
|
|
33
|
+
- Resolve the PR with `gh pr view --json number,headRefOid,url,headRepositoryOwner,headRepository`.
|
|
34
|
+
- Resolve the repository with `gh repo view --json nameWithOwner` when owner/name is missing.
|
|
35
|
+
- Post each inline comment with `gh api repos/{owner}/{repo}/pulls/{number}/comments` and include `commit_id`, `path`, `line`, and `side=RIGHT`.
|
|
36
|
+
4. Keep each comment concise: summary, severity/confidence/category, impact, evidence, and recommendation.
|
|
37
|
+
5. Report posted, skipped, and failed counts.
|
|
38
|
+
|
|
39
|
+
## Safety rules
|
|
40
|
+
|
|
41
|
+
- Do not post duplicate comments.
|
|
42
|
+
- Do not comment line-less findings inline.
|
|
43
|
+
- Ask the user if the upstream PR cannot be identified.
|
|
44
|
+
- Keep actions scoped to the selected issue IDs only.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jameslovespancakes/pi-plus",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "pi and more",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "james",
|
|
8
8
|
"keywords": [
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"anthropic",
|
|
13
13
|
"claude",
|
|
14
14
|
"quota",
|
|
15
|
-
"models"
|
|
15
|
+
"models",
|
|
16
|
+
"workflows",
|
|
17
|
+
"multi-agent"
|
|
16
18
|
],
|
|
17
19
|
"repository": {
|
|
18
20
|
"type": "git",
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
"./src/domains/setup/index.ts",
|
|
36
38
|
"./src/domains/subscriptions/index.ts",
|
|
37
39
|
"./src/domains/models/index.ts",
|
|
40
|
+
"./src/domains/workflows/index.ts",
|
|
38
41
|
"./src/domains/agents/index.ts",
|
|
39
42
|
"./src/domains/remote/index.ts"
|
|
40
43
|
],
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
2
|
+
import { agentPath, readJson, writeJson } from "../store.ts";
|
|
3
|
+
import { normalizeRoutingMode, type AccountQuotaState, type AccountRoutingMode } from "./routing.ts";
|
|
4
|
+
|
|
5
|
+
export type OAuthPoolMode = AccountRoutingMode;
|
|
6
|
+
|
|
7
|
+
export interface PooledOAuthAccount extends OAuthCredential {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
identity?: string;
|
|
12
|
+
addedAt: number;
|
|
13
|
+
lastUsed?: number;
|
|
14
|
+
quota?: AccountQuotaState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderOAuthPool {
|
|
18
|
+
accounts: PooledOAuthAccount[];
|
|
19
|
+
mode: OAuthPoolMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OAuthPoolFile {
|
|
23
|
+
version: 1;
|
|
24
|
+
providers: Record<string, ProviderOAuthPool>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EMPTY_FILE: OAuthPoolFile = { version: 1, providers: {} };
|
|
28
|
+
const DEFAULT_PATH = "pi-plus-oauth-accounts.json";
|
|
29
|
+
const fileCache = new Map<string, OAuthPoolFile>();
|
|
30
|
+
|
|
31
|
+
export function oauthPoolPath(): string {
|
|
32
|
+
return process.env.PI_PLUS_OAUTH_ACCOUNTS_FILE ?? agentPath(DEFAULT_PATH);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadFile(path = oauthPoolPath()): OAuthPoolFile {
|
|
36
|
+
const cached = fileCache.get(path);
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
|
|
39
|
+
const raw = readJson<Partial<OAuthPoolFile>>(path, EMPTY_FILE);
|
|
40
|
+
const file: OAuthPoolFile = {
|
|
41
|
+
version: 1,
|
|
42
|
+
providers: raw.providers && typeof raw.providers === "object" ? raw.providers : {},
|
|
43
|
+
};
|
|
44
|
+
fileCache.set(path, file);
|
|
45
|
+
return file;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function saveFile(file: OAuthPoolFile, path = oauthPoolPath()): void {
|
|
49
|
+
fileCache.set(path, file);
|
|
50
|
+
if (!writeJson(path, file, true, 0o600)) throw new Error(`Could not write OAuth accounts to ${path}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadOAuthPool(providerId: string, path = oauthPoolPath()): ProviderOAuthPool {
|
|
54
|
+
const pool = loadFile(path).providers[providerId];
|
|
55
|
+
return {
|
|
56
|
+
accounts: Array.isArray(pool?.accounts) ? pool.accounts : [],
|
|
57
|
+
mode: normalizeRoutingMode(pool?.mode as string | undefined),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function saveOAuthPool(providerId: string, pool: ProviderOAuthPool, path = oauthPoolPath()): void {
|
|
62
|
+
const file = loadFile(path);
|
|
63
|
+
file.providers[providerId] = pool;
|
|
64
|
+
saveFile(file, path);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function saveOAuthAccount(providerId: string, account: PooledOAuthAccount, path = oauthPoolPath()): void {
|
|
68
|
+
const pool = loadOAuthPool(providerId, path);
|
|
69
|
+
const index = pool.accounts.findIndex((candidate) => candidate.id === account.id);
|
|
70
|
+
if (index === -1) pool.accounts.push(account);
|
|
71
|
+
else pool.accounts[index] = { ...pool.accounts[index], ...account };
|
|
72
|
+
saveOAuthPool(providerId, pool, path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function removeOAuthAccount(providerId: string, accountId: string, path = oauthPoolPath()): boolean {
|
|
76
|
+
const pool = loadOAuthPool(providerId, path);
|
|
77
|
+
const accounts = pool.accounts.filter((account) => account.id !== accountId);
|
|
78
|
+
if (accounts.length === pool.accounts.length) return false;
|
|
79
|
+
saveOAuthPool(providerId, { ...pool, accounts }, path);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function setOAuthPoolMode(providerId: string, mode: OAuthPoolMode, path = oauthPoolPath()): void {
|
|
84
|
+
saveOAuthPool(providerId, { ...loadOAuthPool(providerId, path), mode }, path);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function oauthIdentity(access: string): string | undefined {
|
|
88
|
+
try {
|
|
89
|
+
const payload = JSON.parse(Buffer.from(access.split(".")[1] ?? "", "base64url").toString()) as Record<string, unknown>;
|
|
90
|
+
for (const key of ["sub", "email", "account_id", "user_id", "uid"]) {
|
|
91
|
+
const value = payload[key];
|
|
92
|
+
if (typeof value === "string" && value) return `${key}:${value}`;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Opaque access tokens have no local identity.
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resetOAuthPoolCache(): void {
|
|
101
|
+
fileCache.clear();
|
|
102
|
+
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider-agnostic subscription accounts.
|
|
3
|
-
*
|
|
4
|
-
* Today only Anthropic implements this, through
|
|
5
|
-
* `domains/subscriptions/providers/anthropic.ts`. The point of the indirection
|
|
6
|
-
* is that `/account` and `/routing` contain no provider-specific logic, so a
|
|
7
|
-
* second provider is a new adapter rather than a new command surface.
|
|
8
|
-
*
|
|
9
|
-
* Structural types only; nothing here imports pi.
|
|
10
|
-
*/
|
|
1
|
+
/** Provider-agnostic subscription account adapters. */
|
|
11
2
|
|
|
12
3
|
export interface ManagedAccount {
|
|
13
4
|
id: string;
|
|
@@ -20,15 +11,12 @@ export interface ManagedAccount {
|
|
|
20
11
|
primary?: boolean;
|
|
21
12
|
}
|
|
22
13
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* exhausted. `optimal` balances across accounts by remaining quota and time to
|
|
26
|
-
* reset, keeping session caches sticky.
|
|
27
|
-
*/
|
|
28
|
-
export type RoutingMode = "standard" | "optimal";
|
|
14
|
+
/** Fixed account order or provider quota-aware selection. */
|
|
15
|
+
export type RoutingMode = "sequential" | "quota-aware";
|
|
29
16
|
|
|
30
17
|
export interface AccountUi {
|
|
31
18
|
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
19
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
32
20
|
confirm(title: string, message: string): Promise<boolean>;
|
|
33
21
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
34
22
|
}
|
|
@@ -36,7 +24,8 @@ export interface AccountUi {
|
|
|
36
24
|
export interface AccountContext {
|
|
37
25
|
ui: AccountUi;
|
|
38
26
|
hasUI: boolean;
|
|
39
|
-
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/** Opens an OAuth URL. */
|
|
40
29
|
openBrowser(url: string): Promise<void>;
|
|
41
30
|
}
|
|
42
31
|
|
|
@@ -56,15 +45,9 @@ export interface AccountProvider {
|
|
|
56
45
|
add(ctx: AccountContext, label: string): Promise<string | undefined>;
|
|
57
46
|
/** Returns the label of the account that was reauthorized. */
|
|
58
47
|
reauth(ctx: AccountContext, accountId: string): Promise<string | undefined>;
|
|
59
|
-
/**
|
|
60
|
-
* Enables or disables one account without removing its credentials.
|
|
61
|
-
* Optional: a provider that cannot suspend accounts simply omits it.
|
|
62
|
-
*/
|
|
48
|
+
/** Enables or disables an account without removing credentials. */
|
|
63
49
|
setEnabled?(accountId: string, enabled: boolean): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Changes an account's display label. Credentials are untouched, so this is
|
|
66
|
-
* purely cosmetic and never needs a re-authorization.
|
|
67
|
-
*/
|
|
50
|
+
/** Changes only the display label. */
|
|
68
51
|
rename?(accountId: string, label: string): Promise<void>;
|
|
69
52
|
routing?: RoutingSupport;
|
|
70
53
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface AccountQuotaState {
|
|
2
|
+
remainingPercent?: number;
|
|
3
|
+
resetAt?: number;
|
|
4
|
+
checkedAt: number;
|
|
5
|
+
blockedUntil?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AccountRoutingCandidate<T> {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly order: number;
|
|
11
|
+
readonly lastUsed: number;
|
|
12
|
+
readonly quota?: AccountQuotaState;
|
|
13
|
+
readonly value: T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AccountRoutingMode = "sequential" | "quota-aware";
|
|
17
|
+
|
|
18
|
+
/** Maps stored legacy names onto the two routing modes. */
|
|
19
|
+
export function normalizeRoutingMode(value: string | undefined): AccountRoutingMode {
|
|
20
|
+
return value === "quota-aware" || value === "optimal" || value === "sticky-balanced"
|
|
21
|
+
? "quota-aware"
|
|
22
|
+
: "sequential";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Sequential keeps account order; quota-aware prefers unmeasured then highest remaining capacity. */
|
|
26
|
+
export function selectRoutingCandidate<T>(
|
|
27
|
+
candidates: readonly AccountRoutingCandidate<T>[],
|
|
28
|
+
mode: AccountRoutingMode,
|
|
29
|
+
now = Date.now(),
|
|
30
|
+
): AccountRoutingCandidate<T> | undefined {
|
|
31
|
+
if (candidates.length === 0) return undefined;
|
|
32
|
+
const viable = candidates.filter((candidate) => isCandidateViable(candidate.quota, now));
|
|
33
|
+
if (viable.length === 0) return undefined;
|
|
34
|
+
|
|
35
|
+
if (mode === "sequential") {
|
|
36
|
+
return [...viable].sort((left, right) => left.order - right.order)[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const unmeasured = viable.filter((candidate) => candidate.quota?.remainingPercent === undefined);
|
|
40
|
+
if (unmeasured.length > 0) return leastRecentlyUsed(unmeasured);
|
|
41
|
+
|
|
42
|
+
return [...viable].sort((left, right) =>
|
|
43
|
+
(right.quota?.remainingPercent ?? 0) - (left.quota?.remainingPercent ?? 0)
|
|
44
|
+
|| left.lastUsed - right.lastUsed
|
|
45
|
+
|| left.order - right.order
|
|
46
|
+
|| left.id.localeCompare(right.id))[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function quotaStateFromHeaders(
|
|
50
|
+
status: number,
|
|
51
|
+
headers: Record<string, string>,
|
|
52
|
+
previous?: AccountQuotaState,
|
|
53
|
+
now = Date.now(),
|
|
54
|
+
): AccountQuotaState | undefined {
|
|
55
|
+
const normalized = new Map(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
56
|
+
const remaining = firstNumber(normalized, [
|
|
57
|
+
"x-ratelimit-remaining-requests",
|
|
58
|
+
"ratelimit-remaining-requests",
|
|
59
|
+
"x-ratelimit-remaining",
|
|
60
|
+
"ratelimit-remaining",
|
|
61
|
+
]);
|
|
62
|
+
const limit = firstNumber(normalized, [
|
|
63
|
+
"x-ratelimit-limit-requests",
|
|
64
|
+
"ratelimit-limit-requests",
|
|
65
|
+
"x-ratelimit-limit",
|
|
66
|
+
"ratelimit-limit",
|
|
67
|
+
]);
|
|
68
|
+
const resetAt = parseResetAt(normalized, now);
|
|
69
|
+
|
|
70
|
+
if (status === 429) {
|
|
71
|
+
return {
|
|
72
|
+
remainingPercent: 0,
|
|
73
|
+
resetAt,
|
|
74
|
+
checkedAt: now,
|
|
75
|
+
blockedUntil: resetAt ?? now + 60_000,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (remaining === undefined) return previous;
|
|
79
|
+
|
|
80
|
+
const remainingPercent = limit && limit > 0
|
|
81
|
+
? Math.max(0, Math.min(100, remaining / limit * 100))
|
|
82
|
+
: Math.max(0, Math.min(100, remaining));
|
|
83
|
+
return { remainingPercent, resetAt, checkedAt: now };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isCandidateViable(quota: AccountQuotaState | undefined, now: number): boolean {
|
|
87
|
+
if (!quota) return true;
|
|
88
|
+
if (quota.blockedUntil !== undefined && quota.blockedUntil > now) return false;
|
|
89
|
+
if (quota.remainingPercent !== 0) return true;
|
|
90
|
+
return quota.resetAt !== undefined && quota.resetAt <= now;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function leastRecentlyUsed<T>(candidates: readonly AccountRoutingCandidate<T>[]): AccountRoutingCandidate<T> {
|
|
94
|
+
return [...candidates].sort((left, right) =>
|
|
95
|
+
left.lastUsed - right.lastUsed
|
|
96
|
+
|| left.order - right.order
|
|
97
|
+
|| left.id.localeCompare(right.id))[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstNumber(headers: ReadonlyMap<string, string>, names: readonly string[]): number | undefined {
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
const value = Number(headers.get(name));
|
|
103
|
+
if (Number.isFinite(value)) return value;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseResetAt(headers: ReadonlyMap<string, string>, now: number): number | undefined {
|
|
109
|
+
const retryAfter = Number(headers.get("retry-after"));
|
|
110
|
+
if (Number.isFinite(retryAfter) && retryAfter >= 0) return now + retryAfter * 1_000;
|
|
111
|
+
|
|
112
|
+
const raw = firstNumber(headers, [
|
|
113
|
+
"x-ratelimit-reset-requests",
|
|
114
|
+
"ratelimit-reset-requests",
|
|
115
|
+
"x-ratelimit-reset",
|
|
116
|
+
"ratelimit-reset",
|
|
117
|
+
]);
|
|
118
|
+
if (raw === undefined) return undefined;
|
|
119
|
+
if (raw > 1_000_000_000_000) return raw;
|
|
120
|
+
if (raw > 1_000_000_000) return raw * 1_000;
|
|
121
|
+
return now + raw * 1_000;
|
|
122
|
+
}
|
|
@@ -2,43 +2,15 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { xxhash64 } from "./xxhash64.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Claude Code
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* READ THIS BEFORE CHANGING ANYTHING HERE
|
|
9
|
-
*
|
|
10
|
-
* These headers make a request indistinguishable from Anthropic's official
|
|
11
|
-
* Claude Code CLI. Without them Anthropic classifies the caller as a
|
|
12
|
-
* third-party app and answers:
|
|
13
|
-
*
|
|
14
|
-
* "Third-party apps now draw from your extra usage, not your plan limits."
|
|
15
|
-
*
|
|
16
|
-
* So the purpose of this file is to bill against plan limits rather than extra
|
|
17
|
-
* usage. That is plausibly contrary to Anthropic's intent as stated in that
|
|
18
|
-
* message, and the exposure falls on the account owner.
|
|
19
|
-
*
|
|
20
|
-
* It is isolated in one file, and referenced from exactly one place, so it can
|
|
21
|
-
* be deleted or replaced without touching the rest of the provider. Removing it
|
|
22
|
-
* does not break anything: requests keep working and bill to extra usage.
|
|
23
|
-
*
|
|
24
|
-
* Ported from @cortexkit/anthropic-auth-core during extraction.
|
|
25
|
-
* ---------------------------------------------------------------------------
|
|
5
|
+
* Emulates Claude Code so OAuth requests use plan limits instead of extra usage.
|
|
6
|
+
* This may conflict with Anthropic's stated intent; remove this identity from
|
|
7
|
+
* the provider to opt out. Adapted from @cortexkit/anthropic-auth-core.
|
|
26
8
|
*/
|
|
27
9
|
|
|
28
10
|
/** Pinned to the Claude Code release being imitated. */
|
|
29
11
|
export const CLAUDE_CODE_VERSION = "2.1.258";
|
|
30
12
|
|
|
31
|
-
/**
|
|
32
|
-
* Anti-tamper checksum, replicated from the official client.
|
|
33
|
-
*
|
|
34
|
-
* The real CLI signs the serialised request body with xxHash64 under a fixed
|
|
35
|
-
* seed and writes the low 20 bits into a `cch=` placeholder. Anthropic added
|
|
36
|
-
* this so a client cannot simply assert it is Claude Code in a header: it has
|
|
37
|
-
* to prove it by producing a value derived from the request itself.
|
|
38
|
-
*
|
|
39
|
-
* The salt and sample positions are constants in the official bundle, not
|
|
40
|
-
* derivable from anything, and they will change when the CLI is updated.
|
|
41
|
-
*/
|
|
13
|
+
/** Claude Code checksum constants. They can change between CLI releases. */
|
|
42
14
|
const CCH_SEED = 0x4d659218e32a3268n;
|
|
43
15
|
const CCH_SALT = "59cf53e54c78";
|
|
44
16
|
const CCH_POSITIONS = [4, 7, 20];
|
|
@@ -147,15 +119,7 @@ function hasFullAgentShape(body: any): boolean {
|
|
|
147
119
|
&& !!body?.thinking && typeof body.thinking === "object";
|
|
148
120
|
}
|
|
149
121
|
|
|
150
|
-
/**
|
|
151
|
-
* Paragraphs containing this anchor cannot sit in the top-level `system` array.
|
|
152
|
-
*
|
|
153
|
-
* Two lines of pi's documentation paragraph are each independently sufficient to
|
|
154
|
-
* make Anthropic answer 400 "Third-party apps now draw from your extra usage".
|
|
155
|
-
* The same text is accepted inside `messages`, so it is moved there. Confirmed
|
|
156
|
-
* by bisection: the identical payload returns 200 with the paragraph removed and
|
|
157
|
-
* 400 with it present, and entry count and payload size are not the factors.
|
|
158
|
-
*/
|
|
122
|
+
/** Documentation paragraphs must move from `system` into `messages`. */
|
|
159
123
|
const DOCS_ANCHOR = "Pi documentation";
|
|
160
124
|
|
|
161
125
|
/** Lone surrogates are invalid UTF-8 and are rejected outright. */
|
|
@@ -163,14 +127,7 @@ function sanitizePrompt(text: string): string {
|
|
|
163
127
|
return text.replace(/[\uD800-\uDFFF]/gu, "\uFFFD");
|
|
164
128
|
}
|
|
165
129
|
|
|
166
|
-
/**
|
|
167
|
-
* Splits pi's system prompt into what may stay in `system` and what must move
|
|
168
|
-
* into the first user message.
|
|
169
|
-
*
|
|
170
|
-
* An unrecognised prompt shape (no docs paragraph found) is moved whole, the
|
|
171
|
-
* same conservative fallback the vendor uses: better to shift the entire prompt
|
|
172
|
-
* into messages than to guess and trigger the 400.
|
|
173
|
-
*/
|
|
130
|
+
/** Splits the system prompt, moving unknown shapes conservatively. */
|
|
174
131
|
export function splitSystemPrompt(prompt: string): { systemText?: string; messageText: string } {
|
|
175
132
|
const paragraphs = sanitizePrompt(prompt).split(/\n\n+/);
|
|
176
133
|
const docs = paragraphs.filter((p) => p.includes(DOCS_ANCHOR));
|
|
@@ -183,15 +140,7 @@ export function splitSystemPrompt(prompt: string): { systemText?: string; messag
|
|
|
183
140
|
};
|
|
184
141
|
}
|
|
185
142
|
|
|
186
|
-
/**
|
|
187
|
-
* Inserts text as its own cache-controlled block ahead of the first user
|
|
188
|
-
* message.
|
|
189
|
-
*
|
|
190
|
-
* A separate block rather than merged text, so the cache prefix ends before the
|
|
191
|
-
* user's own words and a new conversation with a different first message still
|
|
192
|
-
* reads this from cache. `cache_control` is explicit because the message-level
|
|
193
|
-
* breakpoint elsewhere only covers the last user message.
|
|
194
|
-
*/
|
|
143
|
+
/** Prepends a cache-controlled block to the first user message. */
|
|
195
144
|
export function prependPromptBlock(messages: any[], text: string): void {
|
|
196
145
|
const firstUser = (messages ?? []).find((m) => m?.role === "user");
|
|
197
146
|
if (!firstUser || !text) return;
|
|
@@ -213,16 +162,12 @@ export function selectBetas(body: unknown, extra: string[] = []): string {
|
|
|
213
162
|
return [...new Set(selected)].join(",");
|
|
214
163
|
}
|
|
215
164
|
|
|
216
|
-
/**
|
|
217
|
-
* Headers presenting this client as Claude Code.
|
|
218
|
-
* Merged over whatever pi already set.
|
|
219
|
-
*/
|
|
165
|
+
/** Headers presenting this client as Claude Code. */
|
|
220
166
|
export function clientIdentityHeaders(body?: unknown, existingBetas?: string): Record<string, string> {
|
|
221
167
|
const incoming = (existingBetas ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
222
168
|
return {
|
|
223
169
|
"user-agent": USER_AGENT,
|
|
224
|
-
//
|
|
225
|
-
// to send, then puts them in the body's `betas` field.
|
|
170
|
+
// Pi copies this header into the request body's betas field.
|
|
226
171
|
"anthropic-beta": selectBetas(body, incoming),
|
|
227
172
|
"anthropic-version": "2023-06-01",
|
|
228
173
|
"anthropic-dangerous-direct-browser-access": "true",
|