@jameslovespancakes/pi-plus 1.0.14 → 1.0.16

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 CHANGED
@@ -17,6 +17,8 @@
17
17
 
18
18
  ## Install
19
19
 
20
+ Requires **pi 0.87.0 or newer**.
21
+
20
22
  ```sh
21
23
  pi install npm:@jameslovespancakes/pi-plus # npm
22
24
  pi install git:github.com/jameslovespancakes/pi-plus # git
@@ -48,13 +50,13 @@ Nothing else is required, every feature configures itself from within pi.
48
50
 
49
51
  ### Pool every subscription
50
52
 
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.
53
+ Add multiple Claude, ChatGPT/Codex, Gemini, Kimi Code, or xAI/Grok accounts.
54
+ pi-plus keeps credentials separate, refreshes them safely, and supports
55
+ sequential or quota-aware routing.
54
56
 
55
57
  ```
56
58
  /accounts add anthropic work
57
- /accounts add kimi-coding personal
59
+ /accounts add gemini personal
58
60
  /routing quota-aware
59
61
  ```
60
62
 
@@ -75,6 +77,31 @@ Account and routing commands are provider-agnostic. Sequential routing uses
75
77
  account order; quota-aware routing uses reported capacity and fairly probes
76
78
  accounts whose provider does not publish quota headers.
77
79
 
80
+ ### Gemini on a Google account
81
+
82
+ pi keeps only the metered `google/*` API. pi-plus adds `gemini/*`, served by
83
+ Google's Antigravity backend and pooled like every other subscription. The
84
+ provider is ported from [`pi-antigravity`](https://github.com/Rahularya01/pi-antigravity).
85
+ `/login` → **Gemini** signs in through the browser (callback on port 51121; on
86
+ a headless machine, paste the callback URL when asked).
87
+
88
+ ```
89
+ /login gemini
90
+ /accounts add gemini personal
91
+ ```
92
+
93
+ Models include Gemini 3.x Flash and 3.1 Pro, plus the Claude and GPT-OSS
94
+ models the backend also serves; each thinking level routes to the backend's own
95
+ runtime model. The list refreshes from your account, so newly enabled models
96
+ appear without an update, and `/models-refresh` forces it.
97
+
98
+ A quota-walled account is held out of routing until it resets, so the next
99
+ request goes to another pooled account. `PI_GEMINI_PROJECT_ID` pins a Cloud
100
+ project; most accounts need none.
101
+
102
+ `gemini/*` is auto-approved because the subscription has already paid for it;
103
+ the metered `google/*` still asks.
104
+
78
105
  ### Pick models on evidence
79
106
 
80
107
  `list_models` puts the full **Artificial Analysis** benchmark set in front of the
@@ -257,10 +284,15 @@ pi-plus is a thin layer over other people's work.
257
284
  | [`pi`](https://pi.dev/) | the host agent and the entire extension API | MIT |
258
285
  | [`xxhash-wasm`](https://github.com/jungomi/xxhash-wasm) | vendored into `src/core/anthropic/vendor/` for the billing checksum | MIT |
259
286
  | [`pi-workflow-engine`](https://github.com/timbrinded/pi-workflow-engine) | embedded workflow runtime and built-in workflows | MIT |
287
+ | [`pi-antigravity`](https://github.com/Rahularya01/pi-antigravity) | reference for the `gemini` provider: Antigravity OAuth, wire format, model routing and catalogue discovery | MIT |
260
288
 
261
289
  The workflow engine keeps its upstream license in
262
290
  [`src/domains/workflows/LICENSE.md`](src/domains/workflows/LICENSE.md).
263
291
 
292
+ The `gemini` provider in `src/core/gemini/` is ported from `pi-antigravity`
293
+ 0.8.0 and adapted to pi-plus's pooled accounts and pi's own Google adapter; its
294
+ upstream license is kept in [`src/core/gemini/LICENSE.md`](src/core/gemini/LICENSE.md).
295
+
264
296
  The Anthropic provider, OAuth, quota and routing were originally adopted from
265
297
  [`@cortexkit/pi-anthropic-auth`](https://github.com/cortexkit/anthropic-auth)
266
298
  (MIT) and have since been reimplemented in this repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jameslovespancakes/pi-plus",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "description": "pi and more",
6
6
  "license": "MIT",
@@ -49,10 +49,10 @@
49
49
  "ws": "^8.18.3"
50
50
  },
51
51
  "peerDependencies": {
52
- "@earendil-works/pi-agent-core": "^0.86.1",
53
- "@earendil-works/pi-ai": "^0.86.1",
54
- "@earendil-works/pi-coding-agent": "^0.86.1",
55
- "@earendil-works/pi-tui": "^0.86.1",
52
+ "@earendil-works/pi-agent-core": "^0.87.0",
53
+ "@earendil-works/pi-ai": "^0.87.0",
54
+ "@earendil-works/pi-coding-agent": "^0.87.0",
55
+ "@earendil-works/pi-tui": "^0.87.0",
56
56
  "typebox": "*"
57
57
  },
58
58
  "scripts": {
@@ -40,7 +40,6 @@ interface OAuthPoolFile {
40
40
  providers: Record<string, ProviderOAuthPool>;
41
41
  }
42
42
 
43
- const EMPTY_FILE: OAuthPoolFile = { version: 1, providers: {} };
44
43
  const DEFAULT_PATH = "pi-plus-oauth-accounts.json";
45
44
  const fileCache = new Map<string, OAuthPoolFile>();
46
45
 
@@ -52,7 +51,10 @@ function loadFile(path = oauthPoolPath()): OAuthPoolFile {
52
51
  const cached = fileCache.get(path);
53
52
  if (cached) return cached;
54
53
 
55
- const raw = readJson<Partial<OAuthPoolFile>>(path, EMPTY_FILE);
54
+ // The fallback is built per call: a shared constant would be aliased into
55
+ // `file.providers` whenever the file is missing, and every later write would
56
+ // accumulate in it — outliving both the cache and the file itself.
57
+ const raw = readJson<Partial<OAuthPoolFile>>(path, { version: 1, providers: {} });
56
58
  const file: OAuthPoolFile = {
57
59
  version: 1,
58
60
  providers: raw.providers && typeof raw.providers === "object" ? raw.providers : {},
@@ -16,9 +16,14 @@ export interface ManagedAccount {
16
16
  /** Fixed account order or provider quota-aware selection. */
17
17
  export type RoutingMode = "sequential" | "quota-aware";
18
18
 
19
+ /** Dismisses a dialog programmatically, e.g. a paste prompt once the browser callback wins. */
20
+ export interface AccountDialogOptions {
21
+ signal?: AbortSignal;
22
+ }
23
+
19
24
  export interface AccountUi {
20
- input(title: string, placeholder?: string): Promise<string | undefined>;
21
- select(title: string, options: string[]): Promise<string | undefined>;
25
+ input(title: string, placeholder?: string, options?: AccountDialogOptions): Promise<string | undefined>;
26
+ select(title: string, options: string[], dialog?: AccountDialogOptions): Promise<string | undefined>;
22
27
  confirm(title: string, message: string): Promise<boolean>;
23
28
  notify(message: string, type?: "info" | "warning" | "error"): void;
24
29
  }
@@ -0,0 +1,139 @@
1
+ import { agentPath, readJson, writeJson } from "../store.ts";
2
+
3
+ /**
4
+ * Live Anthropic model list.
5
+ *
6
+ * pi's bundled catalogue is generated at build time, so a model Anthropic
7
+ * ships afterwards is invisible to pi until pi is upgraded — `claude-opus-5-5`
8
+ * was usable for days while the picker denied it existed. Claude Code does not
9
+ * have that problem because it asks the API.
10
+ *
11
+ * `GET /v1/models` returns identity only (`id`, `display_name`), never limits
12
+ * or pricing, so a discovered model still needs those filled in. They are
13
+ * inherited from the newest model of the same family in pi's catalogue rather
14
+ * than invented here — see `models.ts`.
15
+ *
16
+ * The result is cached on disk so the picker is correct on the very first
17
+ * render, before any network call has finished.
18
+ */
19
+
20
+ const CACHE_FILE = "anthropic-models.json";
21
+ /** Re-ask once a day; a new model is news, not an emergency. */
22
+ export const CATALOG_TTL_MS = 24 * 60 * 60 * 1000;
23
+
24
+ export interface LiveModel {
25
+ id: string;
26
+ displayName?: string;
27
+ }
28
+
29
+ interface CacheFile {
30
+ version: 1;
31
+ fetchedAt: number;
32
+ models: LiveModel[];
33
+ }
34
+
35
+ const EMPTY: CacheFile = { version: 1, fetchedAt: 0, models: [] };
36
+
37
+ export function catalogPath(): string {
38
+ return process.env.PI_PLUS_ANTHROPIC_MODELS_FILE ?? agentPath(CACHE_FILE);
39
+ }
40
+
41
+ function load(): CacheFile {
42
+ const raw = readJson<Partial<CacheFile>>(catalogPath(), { ...EMPTY });
43
+ return {
44
+ version: 1,
45
+ fetchedAt: typeof raw.fetchedAt === "number" ? raw.fetchedAt : 0,
46
+ models: Array.isArray(raw.models)
47
+ ? raw.models.filter((model): model is LiveModel => typeof model?.id === "string" && model.id.length > 0)
48
+ : [],
49
+ };
50
+ }
51
+
52
+ /** Last known live list. Empty before the first successful fetch. */
53
+ export function cachedAnthropicModels(): LiveModel[] {
54
+ return load().models;
55
+ }
56
+
57
+ export function catalogIsStale(now = Date.now()): boolean {
58
+ return now - load().fetchedAt > CATALOG_TTL_MS;
59
+ }
60
+
61
+ /** Test seam and reset path. */
62
+ export function writeAnthropicCatalog(models: LiveModel[], fetchedAt = Date.now()): void {
63
+ writeJson(catalogPath(), { version: 1, fetchedAt, models } satisfies CacheFile, true);
64
+ }
65
+
66
+ interface ModelsResponse {
67
+ data?: { id?: string; display_name?: string }[];
68
+ }
69
+
70
+ /**
71
+ * Asks Anthropic what this credential can actually use.
72
+ *
73
+ * `auth` comes from pi's resolved provider auth, so an OAuth subscription and
74
+ * a plain API key both work and the token is already refreshed.
75
+ */
76
+ export interface ResolvedAnthropicAuth {
77
+ apiKey?: string;
78
+ headers?: Record<string, string | null>;
79
+ baseUrl?: string;
80
+ /** pi reports "OAuth" for a subscription credential. */
81
+ source?: string;
82
+ }
83
+
84
+ /**
85
+ * Only a real API key goes in `x-api-key`; everything else is a bearer token.
86
+ *
87
+ * A prefix test alone is a trap: an Anthropic *OAuth* token is `sk-ant-oat…`
88
+ * and an API key is `sk-ant-api…`, so checking for `sk-` routes subscription
89
+ * tokens into the wrong header and the endpoint answers 401.
90
+ */
91
+ function usesApiKeyHeader(auth: ResolvedAnthropicAuth): boolean {
92
+ if (auth.source === "OAuth") return false;
93
+ return (auth.apiKey ?? "").startsWith("sk-ant-api");
94
+ }
95
+
96
+ export async function fetchAnthropicModels(
97
+ auth: ResolvedAnthropicAuth,
98
+ signal?: AbortSignal,
99
+ ): Promise<LiveModel[]> {
100
+ const headers: Record<string, string> = { "anthropic-version": "2023-06-01" };
101
+ for (const [key, value] of Object.entries(auth.headers ?? {})) {
102
+ if (value !== null) headers[key] = value;
103
+ }
104
+
105
+ if (auth.apiKey) {
106
+ if (usesApiKeyHeader(auth)) headers["x-api-key"] = auth.apiKey;
107
+ else headers.Authorization = `Bearer ${auth.apiKey}`;
108
+ }
109
+ // Exactly this one beta. The model list rejects the request when the
110
+ // Claude Code betas meant for /v1/messages are carried over.
111
+ headers["anthropic-beta"] = "oauth-2025-04-20";
112
+
113
+ const base = (auth.baseUrl || "https://api.anthropic.com").replace(/\/+$/, "");
114
+ const response = await fetch(`${base}/v1/models?limit=200`, { headers, signal });
115
+ if (!response.ok) {
116
+ throw new Error(`Anthropic model list failed: ${response.status} ${response.statusText}`);
117
+ }
118
+
119
+ const body = (await response.json()) as ModelsResponse;
120
+ return (body.data ?? [])
121
+ .filter((model) => typeof model?.id === "string" && model.id.length > 0)
122
+ .map((model) => ({ id: model.id!, ...(model.display_name && { displayName: model.display_name }) }));
123
+ }
124
+
125
+ /**
126
+ * Refreshes the cache. Returns the ids that were not already known, so a
127
+ * caller can decide whether re-registering the provider is worth it.
128
+ */
129
+ export async function refreshAnthropicCatalog(
130
+ auth: ResolvedAnthropicAuth,
131
+ signal?: AbortSignal,
132
+ ): Promise<string[]> {
133
+ const before = new Set(cachedAnthropicModels().map((model) => model.id));
134
+ const models = await fetchAnthropicModels(auth, signal);
135
+ if (models.length === 0) return [];
136
+
137
+ writeAnthropicCatalog(models);
138
+ return models.map((model) => model.id).filter((id) => !before.has(id));
139
+ }
@@ -8,7 +8,7 @@ import { xxhash64 } from "./xxhash64.ts";
8
8
  */
9
9
 
10
10
  /** Pinned to the Claude Code release being imitated. */
11
- export const CLAUDE_CODE_VERSION = "2.1.258";
11
+ export const CLAUDE_CODE_VERSION = "2.1.280";
12
12
 
13
13
  /** Claude Code checksum constants. They can change between CLI releases. */
14
14
  const CCH_SEED = 0x4d659218e32a3268n;
@@ -163,12 +163,31 @@ export function selectBetas(body: unknown, extra: string[] = []): string {
163
163
  }
164
164
 
165
165
  /** Headers presenting this client as Claude Code. */
166
- export function clientIdentityHeaders(body?: unknown, existingBetas?: string): Record<string, string> {
167
- const incoming = (existingBetas ?? "").split(",").map((b) => b.trim()).filter(Boolean);
166
+ /**
167
+ * Betas this identity adds, on top of whatever pi computed for the model.
168
+ *
169
+ * These must be *merged*, never substituted. pi enables model-specific betas
170
+ * that authorise fields it also emits — `mid-conversation-output-config-…`
171
+ * covers the `output_config` system messages it inserts for adaptive-effort
172
+ * models. Replacing the list leaves those messages in the body with nothing
173
+ * permitting them, and Anthropic rejects the request:
174
+ *
175
+ * messages.1.output_config: Extra inputs are not permitted
176
+ *
177
+ * which is a 400 on every Opus 5 request and reads like a pi bug.
178
+ */
179
+ export function identityBetas(body?: unknown, existing: readonly string[] = []): string[] {
180
+ return [...new Set([...existing.map((beta) => beta.trim()).filter(Boolean), ...selectBetas(body).split(",")])];
181
+ }
182
+
183
+ /**
184
+ * Deliberately omits `anthropic-beta`. pi copies that header verbatim into the
185
+ * request body's `betas`, so setting it here would discard pi's own list; the
186
+ * merge happens per request through {@link identityBetas} instead.
187
+ */
188
+ export function clientIdentityHeaders(): Record<string, string> {
168
189
  return {
169
190
  "user-agent": USER_AGENT,
170
- // Pi copies this header into the request body's betas field.
171
- "anthropic-beta": selectBetas(body, incoming),
172
191
  "anthropic-version": "2023-06-01",
173
192
  "anthropic-dangerous-direct-browser-access": "true",
174
193
  "x-app": "cli",
@@ -1,9 +1,23 @@
1
+ import type { Model } from "@earendil-works/pi-ai";
2
+ import { ANTHROPIC_MODELS as PI_ANTHROPIC_MODELS } from "@earendil-works/pi-ai/providers/anthropic.models";
3
+ import { cachedAnthropicModels, type LiveModel } from "./catalog.ts";
4
+
1
5
  /**
2
6
  * Anthropic model catalogue.
3
7
  *
4
- * These are the models pi's built-in catalogue does not carry (or carries with
5
- * stale pricing). Extracted so the provider definition lives here rather than
6
- * in a dependency.
8
+ * Three sources, in increasing authority:
9
+ *
10
+ * 1. pi's bundled catalogue — complete metadata, but generated at build time
11
+ * and therefore always a little behind Anthropic.
12
+ * 2. the live `/v1/models` list cached by `catalog.ts` — authoritative about
13
+ * which models exist, silent about their limits.
14
+ * 3. local corrections and additions, for the handful of things pi has
15
+ * wrong or has never carried.
16
+ *
17
+ * The merge has to be a superset of pi's list.
18
+ * `registerProvider("anthropic", { models })` *substitutes* the catalogue
19
+ * rather than extending it, so anything omitted here vanishes from the picker
20
+ * with no error at all.
7
21
  */
8
22
 
9
23
  export const FABLE_CONTEXT_WINDOW = 1_000_000;
@@ -31,39 +45,107 @@ const fable = (id: string, name: string, pricing = FABLE_PRICING): ModelSpec =>
31
45
  contextWindow: FABLE_CONTEXT_WINDOW, maxTokens: FABLE_MAX_OUTPUT,
32
46
  });
33
47
 
34
- export const ANTHROPIC_MODELS: ModelSpec[] = [
48
+ /** Models neither pi nor the live list carries. */
49
+ const ADDITIONS: ModelSpec[] = [
35
50
  fable("claude-fable-5", "Claude Fable 5"),
36
51
  fable("claude-mythos-5", "Claude Mythos 5"),
37
52
  fable("claude-fable-5-1", "Claude Fable 5.1", FABLE_5_1_PRICING),
38
53
  fable("claude-mythos-5-1", "Claude Mythos 5.1", FABLE_5_1_PRICING),
39
- {
40
- id: "claude-opus-5", name: "Claude Opus 5", reasoning: true, input: textImage,
54
+ ];
55
+
56
+ /**
57
+ * Applied on top of pi's entry for the same id. Only the listed fields change;
58
+ * everything else pi knows (thinking levels, input limits, cache tiers) stays.
59
+ */
60
+ const CORRECTIONS: Record<string, Partial<ModelSpec>> = {
61
+ "claude-opus-5": {
41
62
  cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
42
63
  contextWindow: 1_000_000, maxTokens: 128_000,
43
64
  },
44
- {
45
- id: "claude-opus-4-8", name: "Claude Opus 4.8", reasoning: true, input: textImage,
65
+ "claude-opus-4-8": {
46
66
  cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
47
67
  contextWindow: 1_000_000, maxTokens: 128_000,
48
68
  },
49
- {
50
- id: "claude-opus-4-5", name: "Claude Opus 4.5", reasoning: true, input: textImage,
51
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
52
- contextWindow: 200_000, maxTokens: 64_000,
53
- },
54
- {
55
- id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, input: textImage,
69
+ "claude-sonnet-5": {
56
70
  cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
57
71
  contextWindow: 1_000_000, maxTokens: 128_000,
58
72
  },
59
- {
60
- id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, input: textImage,
61
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
62
- contextWindow: 200_000, maxTokens: 64_000,
63
- },
64
- {
65
- id: "claude-haiku-4-5", name: "Claude Haiku 4.5", reasoning: false, input: textImage,
66
- cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
67
- contextWindow: 200_000, maxTokens: 64_000,
68
- },
69
- ];
73
+ };
74
+
75
+ type AnthropicModel = Model<"anthropic-messages">;
76
+
77
+ /** `claude-opus-5-5` -> `opus`; undated ids only, so aliases do not win below. */
78
+ function familyOf(id: string): string | undefined {
79
+ return /^claude-([a-z]+)-\d/.exec(id)?.[1];
80
+ }
81
+
82
+ /** `claude-opus-5-5` -> [5, 5]. A dated alias sorts below its undated form. */
83
+ function versionOf(id: string): number[] {
84
+ const tail = /^claude-[a-z]+-(.+)$/.exec(id)?.[1] ?? "";
85
+ // A date suffix (20251101) is a release stamp, not a version component.
86
+ return tail.split("-").filter((part) => /^\d+$/.test(part) && part.length < 5).map(Number);
87
+ }
88
+
89
+ function isNewer(candidate: string, incumbent: string): boolean {
90
+ const left = versionOf(candidate);
91
+ const right = versionOf(incumbent);
92
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
93
+ const a = left[i] ?? -1;
94
+ const b = right[i] ?? -1;
95
+ if (a !== b) return a > b;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /** Title-case a display name when Anthropic did not send one. */
101
+ function nameFor(model: LiveModel): string {
102
+ if (model.displayName) return model.displayName;
103
+ const family = familyOf(model.id) ?? "";
104
+ const version = versionOf(model.id).join(".");
105
+ const pretty = family ? family[0].toUpperCase() + family.slice(1) : model.id;
106
+ return version ? `Claude ${pretty} ${version}` : `Claude ${pretty}`;
107
+ }
108
+
109
+ const base = Object.values(PI_ANTHROPIC_MODELS as unknown as Record<string, AnthropicModel>);
110
+
111
+ /**
112
+ * Newest model per family, used as the template for a model the live list
113
+ * reports but pi has never seen. Inheriting beats guessing: a new Opus gets
114
+ * the current Opus's context window, output cap, thinking levels and pricing,
115
+ * and those track pi upgrades automatically.
116
+ */
117
+ function templates(models: AnthropicModel[]): Map<string, AnthropicModel> {
118
+ const newest = new Map<string, AnthropicModel>();
119
+ for (const model of models) {
120
+ const family = familyOf(model.id);
121
+ if (!family) continue;
122
+ const incumbent = newest.get(family);
123
+ if (!incumbent || isNewer(model.id, incumbent.id)) newest.set(family, model);
124
+ }
125
+ return newest;
126
+ }
127
+
128
+ /** pi's catalogue, corrected, plus anything the live list knows about that pi does not. */
129
+ export function buildAnthropicModels(live: LiveModel[] = cachedAnthropicModels()): ModelSpec[] {
130
+ const corrected = base.map((model) => ({ ...model, ...CORRECTIONS[model.id] })) as AnthropicModel[];
131
+ const known = new Set(corrected.map((model) => model.id));
132
+ const byFamily = templates(corrected);
133
+
134
+ const discovered: AnthropicModel[] = [];
135
+ for (const model of live) {
136
+ if (known.has(model.id)) continue;
137
+ const template = byFamily.get(familyOf(model.id) ?? "");
138
+ if (!template) continue; // An unrecognised family has nothing safe to inherit.
139
+ known.add(model.id);
140
+ discovered.push({ ...template, id: model.id, name: nameFor(model) });
141
+ }
142
+
143
+ return [
144
+ ...corrected,
145
+ ...discovered,
146
+ ...ADDITIONS.filter((model) => !known.has(model.id)),
147
+ ] as unknown as ModelSpec[];
148
+ }
149
+
150
+ /** Snapshot taken at load; `refreshAnthropicModels()` re-registers on change. */
151
+ export const ANTHROPIC_MODELS: ModelSpec[] = buildAnthropicModels();
@@ -28,7 +28,7 @@ export interface PiPlusConfig {
28
28
  const DEFAULTS: PiPlusConfig = {
29
29
  env: {},
30
30
  policy: {
31
- autoApprove: ["anthropic/*", "openai-codex/*", "kimi-coding/*", "xai/*"],
31
+ autoApprove: ["anthropic/*", "openai-codex/*", "gemini/*", "kimi-coding/*", "xai/*"],
32
32
  requireApproval: ["openrouter/*", "google/*", "openai/*"],
33
33
  deny: [],
34
34
  },
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rahul Arya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.