@byokit/accounts 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.0 (2026-10-01)
6
+
7
+ - Dependency update: pins @byokit/usage 0.4.0.
8
+
9
+ - SECURITY: Add a Node-only managed CLI account boundary for subscription sign-in: only app-owned folders and explicitly passed absolute binaries, no default login access, no credential-file reads, no token output, and launch environment credential shedding.
10
+ - Add managed CLI account creation, marker-gated sign-in, status, rename, cancellation, history links and removal, with legacy roster and terms compatibility.
11
+ - Share the portable chooser's AccountLike type and accept normalized subscription usage with millisecond reset times.
12
+ - Bound native status deadlines even when a passed CLI ignores termination; stdout is capped and only the owned child is terminated.
13
+ - Offer every subscription catalogue row by default on supported platforms, adding Kimi, Meta, Qwen and MiniMax labels; preserve Claude Pro/Max sign-in and remove terms and visibility gates. API key (billed per use) rows remain opt-in. Simplify the ChatGPT plan-use error words.
14
+
5
15
  ## 0.13.0 (2026-10-01)
6
16
 
7
17
 
package/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
  </p>
9
9
 
10
10
  <p align="center"><strong>Sign in with the AI plan you already pay for, inside your own app.</strong><br/>
11
- ChatGPT and Claude Pro/Max on every platform (Claude needs Web Crypto); OpenRouter on computers when an app offers it (API billing, never by default); Grok and
12
- GitHub Copilot hidden by default. Anthropic uses an app-passed API key (billed per use), explicitly opted in. Sign-ins go into your app's own store: on a computer (Node, Electron), in a browser
11
+ ChatGPT and Claude Pro/Max on every platform (Claude needs Web Crypto); Grok, GitHub Copilot, Kimi and Meta on computers.
12
+ OpenRouter and Anthropic API keys are billed per use and require explicit app opt-in. Sign-ins go into your app's own store: on a computer (Node, Electron), in a browser
13
13
  (a PWA, Electron's renderer) and on a phone (React Native and Expo, iOS and Android). One import; your bundler picks
14
14
  the platform's side (`package.json`'s `react-native` and `browser` conditions).</p>
15
15
 
@@ -75,16 +75,18 @@ On a computer it uses Pi's [`@earendil-works/pi-ai`](https://www.npmjs.com/packa
75
75
  flows, pinned exactly:
76
76
 
77
77
  ```ts
78
+ import type { SafeStorageLike } from '@byokit/accounts';
78
79
  import { isolate } from '@byokit/accounts/isolate'; // first, before any Pi import
79
80
  isolate('/path/to/app/engine'); // scrub inherited Pi settings and provider keys
80
- const { Accounts, fileStore } = await import('@byokit/accounts');
81
- const { app, safeStorage } = await import('electron');
82
- await app.whenReady();
83
-
84
- const accounts = new Accounts({ store: (member) => fileStore(`/path/to/app/people/${member}/auth.json`, safeStorage) });
85
- const shown = await accounts.login(1, 'chatgpt', { via: 'code' }); // { state: 'waiting', code, url }
86
- // show shown.code and shown.url; the sign-in finishes by itself
87
- (await accounts.status(1, 'chatgpt')).words; // "ChatGPT is connected."
81
+
82
+ // Your Electron main process waits for app.whenReady(), then passes its safeStorage here.
83
+ export async function connect(safeStorage: SafeStorageLike) {
84
+ const { Accounts, fileStore } = await import('@byokit/accounts');
85
+ const accounts = new Accounts({ store: (member) => fileStore(`/path/to/app/people/${member}/auth.json`, safeStorage) });
86
+ const shown = await accounts.login(1, 'chatgpt', { via: 'code' }); // { state: 'waiting', code, url }
87
+ // show shown.code and shown.url; the sign-in finishes by itself
88
+ return { shown, status: await accounts.status(1, 'chatgpt') };
89
+ }
88
90
  ```
89
91
 
90
92
  ### On a phone or in a browser
@@ -119,7 +121,7 @@ and [`examples/pwa`](../../examples/pwa) (browser sign-in).
119
121
  | `Accounts` | Sign-in, status, sign-out, asking and limits for each member: `login`, `finished`, `status`, `plan`, `logout`, `respond`, `failed`, `ladder`, `keepFresh` |
120
122
  | `portable`, `computer`, `loopback` | The platform `Accounts` runs on: device code with `fetch` alone, or (Node entry only) Pi's flows and the loopback listener |
121
123
  | `memoryStore`, `fileStore`, `secureStore`, `browserStore`, `recordStore` | One store per person: in memory, a sealed 0600 file (Node entry only), Keychain/Keystore, IndexedDB, or your own load and save |
122
- | `offered`, `provider`, `PROVIDERS` | The catalogue: each provider's billing, terms status, reason and source |
124
+ | `offered`, `provider`, `PROVIDERS` | The catalogue: each provider's billing, models and source |
123
125
  | `billingWords`, `say`, `WORDS`, `signInError`, `failure`, `clock`, `callbackPage` | The plain sentences every app shows the same way (`words.json`), a time in words, and the page a browser sees after a sign-in |
124
126
  | `respond`, `ResponseError`, `IncompleteError`, `sseReader`, `limitResponse`, `isFunctionCall` | Ask ChatGPT's answers endpoint with a sign-in, with tools, pictures, thinking effort and an answer shape; the error with the words to show and the kind acted on |
125
127
  | `classifyFailure`, `classify`, `REST_MS` | An error's kind (limit, overload, plan without this use, lapsed sign-in, network) and default rest times |
@@ -138,7 +140,7 @@ and [`examples/pwa`](../../examples/pwa) (browser sign-in).
138
140
  | Claude Pro/Max (subscription) | Provider page, paste its code back | Same PKCE flow; token endpoint CORS required | Same PKCE flow; app supplies Web Crypto |
139
141
  | Anthropic (API key, billed per use) | App passes its own key, explicitly | Same fetch-only Messages provider | Same fetch-only Messages provider |
140
142
  | OpenRouter (API billing) | Its own page, back to this computer (Pi's flow), when an app offers it (never by default) | Not yet | Not yet |
141
- | Grok, Copilot (hidden) | Pi's flows | No | No |
143
+ | Grok, Copilot, Kimi, Meta | Pi's flows | No | No |
142
144
  | Where sign-ins are kept | `fileStore(path, safeStorage)`, sealing required | `browserStore(name)` (IndexedDB) | `secureStore(SecureStore, name)` (Keychain, Keystore) |
143
145
 
144
146
  Device code works everywhere: OpenAI's sign-in endpoints answer any web page. The page-straight-back sign-in needs a
@@ -148,13 +150,16 @@ doesn't answer other web pages), so a PWA's model calls go through the app's own
148
150
 
149
151
  ## Catalogue and billing
150
152
 
151
- `catalogue.json` holds each provider with its billing (`subscription`, `api`) and terms status (`allowed`, `grey`,
152
- `partner`), a one-line reason and a source. The kit labels; your app decides what to offer
153
- (`new Accounts({ offer: ['chatgpt'] })`). Without an explicit `offer`, only subscription sign-ins supported on this
154
- platform are shown: OpenRouter is API-billed and never offered by default. An explicit list is not platform-filtered,
155
- so choose from the table above. Show `billingWords(p)` next to every provider you list.
153
+ `catalogue.json` holds each provider's billing (`subscription`, `api`), models and source.
154
+ All subscription rows are offered by default on platforms that support their sign-in. API-billed rows
155
+ are offered only when the app names them. An explicit `offer` list is not platform-filtered.
156
+ The `Provider` shape no longer has `terms`, `hidden` or `why`, and `Terms` is no longer exported.
157
+ Qwen and MiniMax have subscription catalogue rows; their paste and portal sign-in flows follow in later work packages.
158
+ Each provider's own terms apply to how you use your plan.
159
+ Anthropic Messages uses an app-passed API key (billed per use); authentication is separate from the Messages request.
156
160
 
157
- Anthropic Messages uses an app-passed API key (billed per use), with explicit opt-in; its authentication is separate from the Messages request.
161
+ Native Claude CLI sign-in uses the managed-folder entry.
162
+ Approved exception: the `./cli` entry reads and runs only app-managed per-account folders under `stateDir` and the absolute CLI binaries the app passes; it never touches the person's default login; tokens never leave the device and are never logged.
158
163
 
159
164
  ```ts
160
165
  import { Accounts, billingWords, offered } from '@byokit/accounts';
@@ -164,7 +169,7 @@ for (const p of offered(['chatgpt', 'openrouter'])) console.log(`${p.name}: ${bi
164
169
  ```
165
170
 
166
171
  ```text
167
- [ 'chatgpt', 'claude' ]
172
+ [ 'chatgpt', 'grok', 'copilot', 'claude', 'kimi', 'meta', 'qwen', 'minimax' ]
168
173
  ChatGPT: Uses your ChatGPT plan.
169
174
  OpenRouter: Charged per use to your OpenRouter account, not a plan.
170
175
  ```
@@ -438,8 +443,6 @@ fetch such as Expo's. `betas` explicitly opts into native beta headers.
438
443
 
439
444
  ## Claude Pro/Max subscription
440
445
 
441
- Anthropic's [developer guidance](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use) prohibits third-party Claude.ai login without approval; BYOKit has no approval, and this route may stop working or lead to account restrictions (the separately billed API-key route is the documented alternative).
442
-
443
446
  Claude is available by default. Open its page, then paste the returned `code#state` into the app:
444
447
 
445
448
  ```ts
@@ -551,3 +554,38 @@ Identity and re-authentication stay with the host's canonical device store or en
551
554
  scoped by member/provider proves identity; names and emails do not. The TypeScript identity fixture records
552
555
  wrong-account, duplicate identity, changed-email, absent-identity, removal/refresh and extension-field boundaries
553
556
  for runtime integration; the chooser consumes host-validated state and never adopts credentials itself.
557
+
558
+ ## Managed CLI accounts (Node only)
559
+
560
+ `@byokit/accounts/cli` exports `cliAccounts`, `CliAccountError`, `CliProvider`, `CliAccount`, `CliOptions` and `SignInCommand`. Accounts use subscription billing. The portable entries and the `Accounts` class retain their existing sign-in flows.
561
+
562
+ `CliAccount` extends the portable chooser's `AccountLike`. Pass normalized usage through `@byokit/usage`'s `roomOf(reading, nowMs)` when selecting an account, preserving millisecond reset times and the original measurement time.
563
+
564
+ ```ts
565
+ import { cliAccounts } from '@byokit/accounts/cli';
566
+ const accounts = cliAccounts({
567
+ stateDir: '/app/state/plans',
568
+ bins: { claude: '/app/bin/claude', codex: '/app/bin/codex' },
569
+ env: { HOME: '/app/home', PATH: '/app/bin:/usr/bin:/bin' },
570
+ historyFrom: { claude: '/app/history/projects', codex: '/app/history/sessions' },
571
+ prepare: async (folder, provider) => { /* app-owned setup, such as installing hooks */ },
572
+ });
573
+ const { account, signIn } = await accounts.add('claude');
574
+ // Run signIn.shell in the app's sign-in tab, or run signIn.argv with signIn.env
575
+ // and create signIn.completion privately only after that command succeeds.
576
+ const current = await accounts.status(account.id);
577
+ const { set, unset } = accounts.launchEnv(account.id);
578
+ // Apply unset to the launch environment, then apply set, before starting the agent.
579
+ ```
580
+
581
+ `stateDir` must be an absolute app-owned directory with an existing parent. The kit creates it at 0700 and creates private `<provider>/<hex>` account folders below it. `bins` are absolute paths; PATH is never used to find the CLI. Status and login use an environment built from nothing plus the app's `env`, after removing provider credential overrides and adding the managed-folder variable. Pass proxy or temporary-directory settings explicitly if needed. `launchEnv` returns the folder variable in `set` and the provider overrides in `unset`; hosts must apply both so a stray API key (billed per use) cannot override the chosen subscription.
582
+
583
+ `add` returns a `signing` account and the native login command. `status` stays `signing` until the completion marker exists, including after a host restart, and does not run a second native client during pending sign-in. The shell uses a private per-folder lock and marks completion only after successful login. Stop the app's sign-in tab before `cancel` or `remove`; the kit does not supervise that tab. `signInAgain` reuses a pending command or starts a new completion cycle; if an account operation is already in flight it throws `prepare-failed`, so await that operation before retrying. Native CLIs own their refresh transactions; the kit neither copies credentials nor refreshes grants. A crashed sign-in shell can leave its lock; the host should stop that process and remove only that managed lock before retrying.
584
+
585
+ `list` probes managed folders concurrently; `status`, `rename`, `remove` and `cancel` serialize operations per account. Only signed-in state (`ready` or `signed_out`), email and plan come from native status output; no credential file is opened and raw CLI errors and output are discarded. `rename` accepts a trimmed name of 1–64 characters. `cancel` removes only folders added by this instance; for existing accounts it ends the pending completion cycle without deleting the account. `remove` deletes only a validated managed folder. `historyFrom` creates a history symlink inside that folder; it never creates or writes the target, even when it is missing. A throwing `prepare` rolls back the new folder.
586
+
587
+ Native status reads resolve within 15 seconds. Claude stdout is capped at 256 KB and identity JSON at 64 KB; the shared Codex client caps stdout at 64 KB. Timeout or excess output terminates only that owned child, with SIGTERM followed by SIGKILL after one second if it is still alive.
588
+
589
+ The existing `accounts-v1.json` `{version:1,accounts:[{id,provider,name,folder,found}]}` and `auto-terms-v1.json` `{acknowledged:true}` encodings remain unchanged, with 0600 files and atomic replacement. The kit preserves but excludes `found-*` and `found:true` rows, which belong to the host's default-login adapter. Legacy managed rows without kit completion sidecars retain their native signed-in status; new or re-signing rows require the completion marker. Symlinked account folders and records outside the provider/hex layout are refused.
590
+
591
+ `usageSource(id)` returns a Codex Source for `@byokit/usage`; Claude returns `undefined`, and its usage Source is `{provider:'claude', folder:set.CLAUDE_CONFIG_DIR, headers}` in a usage reader whose `stateDir` is the same managed root. `kinds` serves only the matching native agent (`claude` or `codex`); Pi is excluded until its folder mapping is verified. `resumeArgs` accepts an `id` conversation reference for these kinds. `termsAcknowledged` and `acknowledgeTerms` keep the host's existing terms bit; they do not gate sign-in. `suggestName` uses the first part of an email, falling back to the provider's name.
package/SECURITY.md CHANGED
@@ -66,7 +66,7 @@ Deleting local files alone does not revoke tokens, and deletion cannot erase old
66
66
 
67
67
  Tests use fake providers, temporary homes, filesystem canaries and Node permissions;
68
68
  `npm test` blocks outbound networking and checks the owner's existing setup byte for byte.
69
- The kit never invokes the owner's installed tools or borrows environment API keys.
69
+ The kit never borrows environment API keys. Its Node-only `./cli` exception invokes only app-passed absolute CLI binaries against app-managed folders; it never opens credential files or the person's default login. Native CLI credentials remain on the device under the CLI's own storage policy; they do not pass through `fileStore` sealing.
70
70
 
71
71
  ## Review record
72
72
 
@@ -55,7 +55,7 @@ export type AnthropicAccountAsk = AnthropicAsk & {
55
55
  key: string;
56
56
  };
57
57
  export type AccountsOptions<M extends Member = Member> = {
58
- /** The accounts this app offers, in order. Default: supported subscription providers not hidden. */
58
+ /** The accounts this app offers, in order. Default: every subscription provider supported on this platform. */
59
59
  offer?: readonly string[];
60
60
  /** Each member's own store. Default: in memory. */
61
61
  store?: (member: M) => CredentialStore;
@@ -112,7 +112,7 @@ export declare class Accounts<R extends AuthHost = AuthHost, M extends Member =
112
112
  onExpired?: (member: M, key: string) => void;
113
113
  onSignOutError?: (member: M, key: string, error: Error) => void;
114
114
  private platform;
115
- /** Offered: the providers named in `offer`, else every provider not hidden that this platform can sign in to. */
115
+ /** Offered: the providers named in `offer`, else every subscription provider that this platform can sign in to. */
116
116
  constructor(opts?: AccountsOptions<M>, platform?: Platform);
117
117
  /** A member's own store. */
118
118
  protected store(member: M): EndingStore;
package/dist/accounts.js CHANGED
@@ -51,7 +51,7 @@ export class Accounts {
51
51
  onExpired;
52
52
  onSignOutError;
53
53
  platform;
54
- /** Offered: the providers named in `offer`, else every provider not hidden that this platform can sign in to. */
54
+ /** Offered: the providers named in `offer`, else every subscription provider that this platform can sign in to. */
55
55
  constructor(opts = {}, platform = portable) {
56
56
  this.opts = opts;
57
57
  this.platform = platform;
@@ -1,9 +1,8 @@
1
- export type Terms = 'allowed' | 'grey' | 'partner' | 'forbidden';
2
1
  /** How the person pays: their plan (`subscription`), or per-use charges to their account (`api`, never offered by default). */
3
2
  export type Billing = 'subscription' | 'api';
4
3
  /** Terms assessment for choosing between several accounts; descriptive, never an eligibility gate. */
5
4
  export type MultiAccountTerms = {
6
- terms: Terms;
5
+ terms: 'allowed' | 'grey' | 'partner' | 'forbidden';
7
6
  why: string;
8
7
  source: string;
9
8
  };
@@ -25,14 +24,11 @@ export type Provider = {
25
24
  auth?: 'api-key' | 'oauth';
26
25
  label?: string;
27
26
  offer?: boolean;
28
- terms: Terms;
29
- hidden: boolean;
30
- why: string;
31
27
  source: string;
32
28
  multiAccount: MultiAccountTerms;
33
29
  };
34
30
  export declare const PROVIDERS: Record<string, Provider>;
35
31
  export declare function provider(key: string): Provider;
36
- /** What an app offers: the keys it names, in its order, or every subscription provider not hidden by default.
32
+ /** What an app offers: the keys it names, in its order, or every subscription provider by default.
37
33
  * API-billed rows are never in the default: an app offers them only by naming them. */
38
34
  export declare const offered: (keys?: readonly string[]) => Provider[];
package/dist/catalogue.js CHANGED
@@ -1,7 +1,3 @@
1
- // The AI accounts a person can bring, by the name they know, with each provider's billing and terms status as data
2
- // (catalogue.json, readable from Kotlin too). The kit labels; the app decides what to offer. API-billed rows are never
3
- // offered by default; an app names them explicitly. Anthropic here is the app-passed API-key route; authentication
4
- // is separate from Messages, allowing additional opt-in auth routes to be implemented independently.
5
1
  import CATALOGUE from './catalogue.json' with { type: 'json' };
6
2
  export const PROVIDERS = Object.fromEntries(Object.entries(CATALOGUE).map(([key, p]) => [key, { key, ...p }]));
7
3
  export function provider(key) {
@@ -10,6 +6,6 @@ export function provider(key) {
10
6
  throw Object.assign(new Error('no such AI account'), { status: 404 });
11
7
  return p;
12
8
  }
13
- /** What an app offers: the keys it names, in its order, or every subscription provider not hidden by default.
9
+ /** What an app offers: the keys it names, in its order, or every subscription provider by default.
14
10
  * API-billed rows are never in the default: an app offers them only by naming them. */
15
- export const offered = (keys) => keys ? keys.map(provider) : Object.values(PROVIDERS).filter((p) => !p.hidden && p.offer !== false && p.billing === 'subscription');
11
+ export const offered = (keys) => keys ? keys.map(provider) : Object.values(PROVIDERS).filter((p) => p.billing === 'subscription');
@@ -11,9 +11,6 @@
11
11
  "clientId": "app_EMoamEEZ73f0CkXaXp7hrann",
12
12
  "revoke": "https://auth.openai.com/oauth/revoke",
13
13
  "billing": "subscription",
14
- "terms": "grey",
15
- "hidden": false,
16
- "why": "Signs in through Codex's own sign-in. OpenAI documents it for Codex, not for other apps, and has endorsed one other app using it.",
17
14
  "source": "https://developers.openai.com/codex/auth",
18
15
  "multiAccount": {
19
16
  "terms": "grey",
@@ -29,9 +26,6 @@
29
26
  "strong": "moonshotai/kimi-k2.6"
30
27
  },
31
28
  "billing": "api",
32
- "terms": "allowed",
33
- "hidden": false,
34
- "why": "Documented sign-in for any app, no registration. Pay-as-you-go credits, not a subscription.",
35
29
  "source": "https://openrouter.ai/docs/guides/overview/auth/oauth",
36
30
  "multiAccount": {
37
31
  "terms": "grey",
@@ -47,9 +41,6 @@
47
41
  "strong": "grok-4.7"
48
42
  },
49
43
  "billing": "subscription",
50
- "terms": "partner",
51
- "hidden": true,
52
- "why": "xAI allows plan sign-in only in apps it has partnered with.",
53
44
  "source": "https://x.ai/news/grok-opencode",
54
45
  "multiAccount": {
55
46
  "terms": "grey",
@@ -65,9 +56,6 @@
65
56
  "strong": "gpt-5.4"
66
57
  },
67
58
  "billing": "subscription",
68
- "terms": "partner",
69
- "hidden": true,
70
- "why": "GitHub allows Copilot sign-in only in apps it has partnered with; this uses VS Code's client.",
71
59
  "source": "https://github.blog/changelog/2026-01-16-github-copilot-now-supports-opencode",
72
60
  "multiAccount": {
73
61
  "terms": "grey",
@@ -83,12 +71,9 @@
83
71
  "strong": "claude-opus-5-5"
84
72
  },
85
73
  "billing": "api",
86
- "terms": "allowed",
87
- "hidden": false,
88
74
  "auth": "api-key",
89
75
  "label": "API key (billed per use)",
90
76
  "offer": false,
91
- "why": "The app passes an Anthropic API key explicitly. API key (billed per use), never a default or subscription fallback.",
92
77
  "source": "https://platform.claude.com/docs/en/api/overview",
93
78
  "multiAccount": {
94
79
  "terms": "grey",
@@ -107,14 +92,73 @@
107
92
  "clientId": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
108
93
  "billing": "subscription",
109
94
  "auth": "oauth",
110
- "terms": "grey",
111
- "hidden": false,
112
- "why": "Anthropic prohibits third-party Claude.ai sign-in without approval; this route may stop working or lead to account restrictions.",
113
95
  "source": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use",
114
96
  "multiAccount": {
115
97
  "terms": "grey",
116
98
  "why": "This source does not explicitly document choosing between several accounts of this service in another app.",
117
99
  "source": "https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use"
118
100
  }
101
+ },
102
+ "kimi": {
103
+ "pi": "kimi-coding",
104
+ "name": "Kimi",
105
+ "company": "Moonshot AI",
106
+ "models": {
107
+ "strong": "k3",
108
+ "fast": "kimi-for-coding-highspeed"
109
+ },
110
+ "billing": "subscription",
111
+ "source": "https://www.kimi.com",
112
+ "multiAccount": {
113
+ "terms": "grey",
114
+ "why": "This source does not explicitly document choosing between several accounts of this service in another app.",
115
+ "source": "https://www.kimi.com"
116
+ }
117
+ },
118
+ "meta": {
119
+ "pi": "meta",
120
+ "name": "Meta",
121
+ "company": "Meta",
122
+ "models": {
123
+ "strong": "muse-spark-1.3"
124
+ },
125
+ "billing": "subscription",
126
+ "source": "https://www.meta.ai",
127
+ "multiAccount": {
128
+ "terms": "grey",
129
+ "why": "This source does not explicitly document choosing between several accounts of this service in another app.",
130
+ "source": "https://www.meta.ai"
131
+ }
132
+ },
133
+ "qwen": {
134
+ "pi": "qwen-portal",
135
+ "name": "Qwen",
136
+ "company": "Alibaba",
137
+ "models": {
138
+ "strong": "qwen3-coder-plus"
139
+ },
140
+ "billing": "subscription",
141
+ "source": "https://chat.qwen.ai",
142
+ "multiAccount": {
143
+ "terms": "grey",
144
+ "why": "This source does not explicitly document choosing between several accounts of this service in another app.",
145
+ "source": "https://chat.qwen.ai"
146
+ }
147
+ },
148
+ "minimax": {
149
+ "pi": "minimax",
150
+ "name": "MiniMax",
151
+ "company": "MiniMax",
152
+ "models": {
153
+ "strong": "MiniMax-M3",
154
+ "fast": "MiniMax-M2.7-highspeed"
155
+ },
156
+ "billing": "subscription",
157
+ "source": "https://www.minimax.io",
158
+ "multiAccount": {
159
+ "terms": "grey",
160
+ "why": "This source does not explicitly document choosing between several accounts of this service in another app.",
161
+ "source": "https://www.minimax.io"
162
+ }
119
163
  }
120
164
  }
@@ -12,7 +12,7 @@ export function chatgptPlan(o) {
12
12
  const session = await o.session(signal);
13
13
  if (!session || !Array.isArray(session.scopes) || !session.scopes.includes('chatgpt.tokens.use.direct') ||
14
14
  !session.scopes.includes('resource.invoke') || typeof session.accessToken !== 'string' || !session.accessToken.trim()) {
15
- throw new UnsupportedAccountError('ChatGPT plan usage needs a consented token-sharing session.');
15
+ throw new UnsupportedAccountError('This needs a ChatGPT sign-in that allows plan use.');
16
16
  }
17
17
  return session.accessToken;
18
18
  },
package/dist/cli.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { AccountLike } from './multi.ts';
2
+ export type CliProvider = 'claude' | 'codex';
3
+ /** Shared selection surface; the CLI entry remains Node-only. */
4
+ export type CliAccount = AccountLike & {
5
+ provider: CliProvider;
6
+ billing: 'subscription';
7
+ email?: string;
8
+ plan?: string;
9
+ };
10
+ export type CliOptions = {
11
+ stateDir: string;
12
+ bins: Partial<Record<CliProvider, string>>;
13
+ env: {
14
+ PATH: string;
15
+ HOME: string;
16
+ } & Record<string, string>;
17
+ historyFrom?: Partial<Record<CliProvider, string>>;
18
+ prepare?: (folder: string, provider: CliProvider) => Promise<void>;
19
+ };
20
+ export type SignInCommand = {
21
+ argv: string[];
22
+ env: Record<string, string>;
23
+ completion: string;
24
+ shell: string;
25
+ };
26
+ export declare class CliAccountError extends Error {
27
+ name: string;
28
+ readonly code: 'unknown-account' | 'invalid-name' | 'kind-mismatch' | 'prepare-failed' | 'bad-option';
29
+ constructor(code: CliAccountError['code']);
30
+ }
31
+ /** Only app-managed folders and explicitly supplied absolute CLI binaries. */
32
+ export declare function cliAccounts(options: CliOptions): {
33
+ list: () => Promise<CliAccount[]>;
34
+ add(provider: CliProvider): Promise<{
35
+ account: CliAccount;
36
+ signIn: SignInCommand;
37
+ }>;
38
+ signInAgain: (id: string) => SignInCommand;
39
+ status: (id: string) => Promise<CliAccount>;
40
+ cancel(id: string): Promise<{
41
+ removed: boolean;
42
+ }>;
43
+ rename(id: string, name: string): Promise<CliAccount>;
44
+ remove: (id: string) => Promise<void>;
45
+ launchEnv: (id: string) => {
46
+ set: {
47
+ [x: string]: string;
48
+ };
49
+ unset: string[];
50
+ };
51
+ kinds: (provider: CliProvider) => string[];
52
+ resumeArgs(kind: string, ref: {
53
+ kind: "id" | "path";
54
+ value: string;
55
+ }): string[];
56
+ usageSource(id: string): {
57
+ provider: "codex";
58
+ bin: string;
59
+ home: string;
60
+ env: Record<string, string>;
61
+ } | undefined;
62
+ termsAcknowledged: () => boolean;
63
+ acknowledgeTerms: () => void;
64
+ suggestName: (email: string | undefined, provider: CliProvider) => string;
65
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,337 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { identity } from '@byokit/usage';
6
+ import { say } from "./words.js";
7
+ export class CliAccountError extends Error {
8
+ name = 'CliAccountError';
9
+ code;
10
+ constructor(code) {
11
+ super(say({ 'unknown-account': 'cli.unknownAccount', 'invalid-name': 'cli.invalidName', 'kind-mismatch': 'cli.kindMismatch', 'prepare-failed': 'cli.prepareFailed', 'bad-option': 'cli.badOption' }[code]));
12
+ this.code = code;
13
+ }
14
+ }
15
+ const record = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
16
+ const text = (v) => typeof v === 'string' && !v.includes('\0');
17
+ const providers = ['claude', 'codex'];
18
+ const shed = {
19
+ claude: ['CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_ANTHROPIC_AWS', 'CLAUDE_CODE_USE_MANTLE', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_PROFILE', 'ANTHROPIC_FEDERATION_RULE_ID'],
20
+ codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
21
+ };
22
+ const folderVar = (p) => p === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME';
23
+ const quote = (v) => `'${v.replaceAll("'", "'\\''")}'`;
24
+ const complete = (r) => join(r.folder, '.byokit-signin-complete');
25
+ const pending = (r) => join(r.folder, '.byokit-signin-pending');
26
+ function marker(file) {
27
+ try {
28
+ const stat = lstatSync(file);
29
+ if (!stat.isFile() || stat.isSymbolicLink())
30
+ throw new CliAccountError('bad-option');
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT')
35
+ return false;
36
+ throw error;
37
+ }
38
+ }
39
+ function json(file) {
40
+ let fd;
41
+ try {
42
+ fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
43
+ const stat = fstatSync(fd);
44
+ if (!stat.isFile() || stat.size > 256 * 1024)
45
+ return undefined;
46
+ const data = Buffer.alloc(256 * 1024 + 1);
47
+ let n = 0;
48
+ while (n < data.length) {
49
+ const count = readSync(fd, data, n, data.length - n, null);
50
+ if (!count)
51
+ break;
52
+ n += count;
53
+ }
54
+ return n < data.length ? JSON.parse(data.subarray(0, n).toString('utf8')) : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ finally {
60
+ if (fd !== undefined)
61
+ closeSync(fd);
62
+ }
63
+ }
64
+ function directory(path, create = false) {
65
+ try {
66
+ const s = lstatSync(path);
67
+ return s.isDirectory() && !s.isSymbolicLink();
68
+ }
69
+ catch (error) {
70
+ if (create && error.code === 'ENOENT') {
71
+ mkdirSync(path, { mode: 0o700 });
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ }
77
+ /** Only app-managed folders and explicitly supplied absolute CLI binaries. */
78
+ export function cliAccounts(options) {
79
+ if (!record(options) || !text(options.stateDir) || !isAbsolute(options.stateDir) || !record(options.bins) || !record(options.env) || !text(options.env.PATH) || !text(options.env.HOME) || !isAbsolute(options.env.HOME))
80
+ throw new CliAccountError('bad-option');
81
+ if (Object.entries(options.env).some(([k, v]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(k) || !text(v)))
82
+ throw new CliAccountError('bad-option');
83
+ if (Object.entries(options.bins).some(([p, bin]) => !providers.includes(p) || !text(bin) || !isAbsolute(bin)))
84
+ throw new CliAccountError('bad-option');
85
+ if (options.historyFrom && (!record(options.historyFrom) || Object.entries(options.historyFrom).some(([p, path]) => !providers.includes(p) || !text(path) || !isAbsolute(path))))
86
+ throw new CliAccountError('bad-option');
87
+ const stateDir = resolve(options.stateDir);
88
+ const env = { ...options.env };
89
+ const bins = { ...options.bins };
90
+ const history = { ...options.historyFrom };
91
+ for (const path of ['.claude', '.codex', '.pi']) {
92
+ const own = join(resolve(env.HOME), path);
93
+ if (stateDir === own || stateDir.startsWith(own + '/'))
94
+ throw new CliAccountError('bad-option');
95
+ }
96
+ // Parents must already be app-owned. Never follow a state/provider/folder symlink.
97
+ try {
98
+ if (realpathSync(dirname(stateDir)) !== dirname(stateDir))
99
+ throw new CliAccountError('bad-option');
100
+ }
101
+ catch {
102
+ throw new CliAccountError('bad-option');
103
+ }
104
+ if (!directory(stateDir, true))
105
+ throw new CliAccountError('bad-option');
106
+ chmodSync(stateDir, 0o700);
107
+ const file = join(stateDir, 'accounts-v1.json');
108
+ const created = new Set();
109
+ const operations = new Map();
110
+ function safe(r) {
111
+ const parent = join(stateDir, r.provider);
112
+ return resolve(r.folder) === r.folder && r.folder.startsWith(parent + '/') && /^[a-f0-9]+$/.test(r.folder.slice(parent.length + 1)) && directory(stateDir) && realpathSync(stateDir) === stateDir && directory(parent) && directory(r.folder);
113
+ }
114
+ function load() {
115
+ if (!directory(stateDir) || realpathSync(stateDir) !== stateDir)
116
+ throw new CliAccountError('bad-option');
117
+ const saved = json(file);
118
+ if (!record(saved) || !Array.isArray(saved.accounts))
119
+ return [];
120
+ const seen = new Set();
121
+ const rows = [];
122
+ for (const candidate of saved.accounts) {
123
+ if (!record(candidate) || !text(candidate.id) || !candidate.id || candidate.id.length > 128 || seen.has(candidate.id) || !providers.includes(candidate.provider) || !text(candidate.name) || candidate.name.length > 64 || !text(candidate.folder) || typeof candidate.found !== 'boolean')
124
+ continue;
125
+ const r = { id: candidate.id, provider: candidate.provider, name: candidate.name, folder: candidate.folder, found: candidate.found };
126
+ // Host-owned rows remain byte-compatible in the roster, without touching their folder.
127
+ if (!r.found && !r.id.startsWith('found-') && !safe(r))
128
+ continue;
129
+ seen.add(r.id);
130
+ rows.push(r);
131
+ }
132
+ return rows;
133
+ }
134
+ function atomic(path, value) {
135
+ if (!directory(stateDir) || realpathSync(stateDir) !== stateDir)
136
+ throw new CliAccountError('bad-option');
137
+ const tmp = `${path}.${randomUUID()}.tmp`;
138
+ try {
139
+ writeFileSync(tmp, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
140
+ renameSync(tmp, path);
141
+ }
142
+ catch {
143
+ throw new CliAccountError('prepare-failed');
144
+ }
145
+ finally {
146
+ rmSync(tmp, { force: true });
147
+ }
148
+ }
149
+ const save = (rows) => atomic(file, { version: 1, accounts: rows });
150
+ function row(id) {
151
+ const r = load().find((r) => r.id === id && !r.found && !r.id.startsWith('found-'));
152
+ if (!r)
153
+ throw new CliAccountError('unknown-account');
154
+ return r;
155
+ }
156
+ function binary(p) { const bin = bins[p]; if (!bin)
157
+ throw new CliAccountError('bad-option'); return bin; }
158
+ function launch(r) { return { set: { [folderVar(r.provider)]: r.folder }, unset: [...shed[r.provider]] }; }
159
+ function spawnEnv(r) {
160
+ const out = { ...env };
161
+ for (const key of shed[r.provider])
162
+ delete out[key];
163
+ delete out.CLAUDE_CONFIG_DIR;
164
+ delete out.CODEX_HOME;
165
+ return { ...out, ...launch(r).set };
166
+ }
167
+ function command(r) {
168
+ const argv = r.provider === 'claude' ? [binary(r.provider), 'auth', 'login', '--claudeai'] : [binary(r.provider), 'login', '--device-auth'];
169
+ const passed = spawnEnv(r);
170
+ const completion = complete(r);
171
+ const lock = join(r.folder, '.byokit-signin-lock');
172
+ const login = ['/usr/bin/env', '-i', ...Object.entries(passed).map(([k, v]) => `${k}=${v}`), ...argv].map(quote).join(' ');
173
+ // Cross-process guard: a native CLI owns any refresh transaction; the kit never copies or refreshes its grants.
174
+ const shell = `(umask 077; /bin/mkdir ${quote(lock)} || exit 1; trap ${quote(`/bin/rmdir ${quote(lock)}`)} EXIT; ${login} && (set -C; printf complete > ${quote(completion)}))`;
175
+ return { argv, env: passed, completion, shell };
176
+ }
177
+ function begin(r) {
178
+ const result = command(r);
179
+ // Refuse overlapping sign-in starts; reuse the pending command until completion or cancel.
180
+ if (!marker(pending(r)) || marker(complete(r))) {
181
+ rmSync(complete(r), { force: true });
182
+ if (!marker(pending(r)))
183
+ writeFileSync(pending(r), '', { mode: 0o600, flag: 'wx' });
184
+ }
185
+ return result;
186
+ }
187
+ function serial(id, action) {
188
+ const task = (operations.get(id) ?? Promise.resolve()).then(action, action);
189
+ operations.set(id, task);
190
+ return task.finally(() => { if (operations.get(id) === task)
191
+ operations.delete(id); });
192
+ }
193
+ async function readIdentity(r) {
194
+ if (r.provider === 'codex')
195
+ return identity({ provider: 'codex', bin: binary('codex'), home: r.folder, env: spawnEnv(r) });
196
+ return new Promise((accept) => {
197
+ const child = spawn(binary('claude'), ['auth', 'status'], { env: spawnEnv(r), stdio: ['ignore', 'pipe', 'ignore'] });
198
+ let stdout = '';
199
+ let bytes = 0;
200
+ let settled = false;
201
+ let escalation;
202
+ const finish = (answer) => {
203
+ if (settled)
204
+ return;
205
+ settled = true;
206
+ clearTimeout(timer);
207
+ if (child.exitCode === null && child.signalCode === null) {
208
+ child.kill('SIGTERM');
209
+ escalation = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
210
+ child.kill('SIGKILL'); }, 1000);
211
+ }
212
+ accept(answer);
213
+ };
214
+ const timer = setTimeout(() => finish({ signedIn: false }), 15_000);
215
+ child.once('error', () => finish({ signedIn: false }));
216
+ child.stdout.setEncoding('utf8');
217
+ child.stdout.on('data', (chunk) => {
218
+ if (settled)
219
+ return;
220
+ bytes += Buffer.byteLength(chunk);
221
+ if (bytes > 256 * 1024) {
222
+ finish({ signedIn: false });
223
+ return;
224
+ }
225
+ stdout += chunk;
226
+ });
227
+ child.once('close', () => {
228
+ clearTimeout(escalation);
229
+ if (settled)
230
+ return;
231
+ if (bytes > 64 * 1024) {
232
+ finish({ signedIn: false });
233
+ return;
234
+ }
235
+ let raw;
236
+ try {
237
+ raw = JSON.parse(stdout);
238
+ }
239
+ catch {
240
+ finish({ signedIn: false });
241
+ return;
242
+ }
243
+ if (!record(raw) || raw.loggedIn !== true) {
244
+ finish({ signedIn: false });
245
+ return;
246
+ }
247
+ const email = typeof raw.email === 'string' && raw.email.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw.email) ? raw.email : undefined;
248
+ const plan = [raw.subscriptionType, raw.plan, raw.planName, raw.tier].find((v) => typeof v === 'string' && /^[a-zA-Z][a-zA-Z0-9 _+-]{0,63}$/.test(v));
249
+ finish({ signedIn: true, ...(email ? { email } : {}), ...(typeof plan === 'string' ? { plan } : {}) });
250
+ });
251
+ });
252
+ }
253
+ function suggestName(email, provider) {
254
+ const first = email?.split('@')[0]?.split(/[._-]+/).find(Boolean);
255
+ return first ? (first[0].toUpperCase() + first.slice(1).toLowerCase()).slice(0, 64) : provider === 'claude' ? 'Claude' : 'Codex';
256
+ }
257
+ async function status(id) {
258
+ return serial(id, async () => {
259
+ const r = row(id);
260
+ const signing = marker(pending(r)) && !marker(complete(r));
261
+ if (signing)
262
+ return { id: r.id, provider: r.provider, name: r.name.trim() || suggestName(undefined, r.provider), billing: 'subscription', state: 'signing' };
263
+ const info = await readIdentity(r);
264
+ if (marker(pending(r)))
265
+ rmSync(pending(r), { force: true });
266
+ // Pre-existing roster rows have no kit marker; their native status remains authoritative.
267
+ return { id: r.id, provider: r.provider, name: r.name.trim() || suggestName(info.email, r.provider), billing: 'subscription', state: info.signedIn ? 'ready' : 'signed_out', ...(info.email ? { email: info.email } : {}), ...(info.plan ? { plan: info.plan } : {}) };
268
+ });
269
+ }
270
+ async function remove(id) {
271
+ return serial(id, async () => { const r = row(id); rmSync(r.folder, { recursive: true, force: true }); save(load().filter((v) => v.id !== id)); created.delete(id); });
272
+ }
273
+ return {
274
+ list: async () => Promise.all(load().filter((r) => !r.found && !r.id.startsWith('found-')).map((r) => status(r.id))),
275
+ async add(provider) {
276
+ if (!providers.includes(provider))
277
+ throw new CliAccountError('bad-option');
278
+ binary(provider);
279
+ const parent = join(stateDir, provider);
280
+ if (!directory(stateDir) || !directory(parent, true))
281
+ throw new CliAccountError('bad-option');
282
+ chmodSync(parent, 0o700);
283
+ const r = { id: `pa_${randomBytes(9).toString('hex')}`, provider, name: '', folder: join(parent, randomBytes(8).toString('hex')), found: false };
284
+ mkdirSync(r.folder, { mode: 0o700 });
285
+ try {
286
+ if (history[provider])
287
+ symlinkSync(history[provider], join(r.folder, provider === 'claude' ? 'projects' : 'sessions'), 'dir');
288
+ const signIn = begin(r);
289
+ await options.prepare?.(r.folder, provider);
290
+ if (!safe(r))
291
+ throw new CliAccountError('prepare-failed');
292
+ save([...load(), r]);
293
+ created.add(r.id);
294
+ return { account: { id: r.id, provider, name: suggestName(undefined, provider), billing: 'subscription', state: 'signing' }, signIn };
295
+ }
296
+ catch {
297
+ rmSync(r.folder, { recursive: true, force: true });
298
+ throw new CliAccountError('prepare-failed');
299
+ }
300
+ },
301
+ signInAgain: (id) => {
302
+ if (operations.has(id))
303
+ throw new CliAccountError('prepare-failed');
304
+ return begin(row(id));
305
+ },
306
+ status,
307
+ async cancel(id) {
308
+ if (created.has(id)) {
309
+ await remove(id);
310
+ return { removed: true };
311
+ }
312
+ return serial(id, async () => { const r = row(id); rmSync(pending(r), { force: true }); return { removed: false }; });
313
+ },
314
+ async rename(id, name) {
315
+ if (!text(name) || !name.trim() || name.trim().length > 64 || /[\x00-\x1f\x7f]/.test(name))
316
+ throw new CliAccountError('invalid-name');
317
+ await serial(id, async () => { row(id); const rows = load(); rows.find((r) => r.id === id).name = name.trim(); save(rows); });
318
+ return status(id);
319
+ },
320
+ remove,
321
+ launchEnv: (id) => launch(row(id)),
322
+ kinds: (provider) => providers.includes(provider) ? [provider] : [],
323
+ resumeArgs(kind, ref) {
324
+ if (!text(ref.value) || !ref.value || (kind !== 'claude' && kind !== 'codex') || (kind === 'claude' && ref.kind !== 'id') || (kind === 'codex' && ref.kind !== 'id'))
325
+ throw new CliAccountError('kind-mismatch');
326
+ return kind === 'claude' ? ['--resume', ref.value] : ['resume', ref.value];
327
+ },
328
+ usageSource(id) {
329
+ const r = row(id);
330
+ return r.provider === 'codex' ? { provider: 'codex', bin: binary('codex'), home: r.folder, env: spawnEnv(r) } : undefined;
331
+ },
332
+ termsAcknowledged: () => { if (!directory(stateDir))
333
+ throw new CliAccountError('bad-option'); const saved = json(join(stateDir, 'auto-terms-v1.json')); return record(saved) && saved.acknowledged === true; },
334
+ acknowledgeTerms: () => atomic(join(stateDir, 'auto-terms-v1.json'), { acknowledged: true }),
335
+ suggestName,
336
+ };
337
+ }
@@ -1,5 +1,5 @@
1
1
  export { Accounts, planOf, portable, type AccountsOptions, type ClaudePlanAsk, type AnthropicAccountAsk, type AuthHost, type Loopback, type Member, type Platform, type SignIn, type Status } from './accounts.ts';
2
- export { PROVIDERS, offered, provider, type Billing, type MultiAccountTerms, type Provider, type Terms } from './catalogue.ts';
2
+ export { PROVIDERS, offered, provider, type Billing, type MultiAccountTerms, type Provider } from './catalogue.ts';
3
3
  export { PORTABLE, claims, credentialOf, devicePoll, deviceStart, portableEngine, type EngineOptions, type Poll } from './engine.ts';
4
4
  export { REST_MS, classify, classifyFailure, type Failure, type Kind } from './limits.ts';
5
5
  export { IncompleteError, ResponseError, isFunctionCall, limitResponse, respond, sseReader, type Ask, type ResponseFunctionCall, type ResponseInputItem, type ResponseOutputItem, type ResponseOutputMessage, type ResponseReasoning, type ResponseResult, type ResponseStreamEvent, type ResponseText, type ResponseTextFormat, type ResponseTool, type ResponseToolChoice } from './responses.ts';
package/dist/words.json CHANGED
@@ -57,5 +57,10 @@
57
57
  "pick.age": "Read {ago} ago",
58
58
  "pick.ageUnknown": "Reading time unknown",
59
59
  "ago.minutes": "{n} minutes",
60
- "ago.hours": "{n} hours"
60
+ "ago.hours": "{n} hours",
61
+ "cli.unknownAccount": "Choose an account from the list.",
62
+ "cli.invalidName": "Give the account a name up to 64 characters.",
63
+ "cli.kindMismatch": "That agent cannot use this account.",
64
+ "cli.prepareFailed": "Could not prepare the account. Try again.",
65
+ "cli.badOption": "The app supplied invalid account options."
61
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/accounts",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Sign in with the AI plan you already pay for, into your app's own store.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,6 +29,10 @@
29
29
  "types": "./dist/isolate.d.ts",
30
30
  "default": "./dist/isolate.js"
31
31
  },
32
+ "./cli": {
33
+ "types": "./dist/cli.d.ts",
34
+ "default": "./dist/cli.js"
35
+ },
32
36
  "./testing": {
33
37
  "types": "./dist/testing/index.d.ts",
34
38
  "default": "./dist/testing/index.js"
@@ -48,6 +52,7 @@
48
52
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
49
53
  },
50
54
  "dependencies": {
55
+ "@byokit/usage": "0.4.0",
51
56
  "@earendil-works/pi-ai": "0.87.1"
52
57
  },
53
58
  "publishConfig": {