@byokit/openclaw 0.8.0 → 0.10.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/dist/pick.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Account, AccountId, AccountPick, Considered, Defaults, ModelInfo, PickWhy, Room, RunSelection } from './types.ts';
2
+ export type RoomOf = (a: Account, demand: readonly string[]) => Room;
3
+ /** Every account's `Considered` row, in list order, with the kit-only `bound` applied. */
4
+ export declare function consider(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[], bound?: readonly AccountId[]): Considered[];
5
+ /** Auto's winner only; `candidates` are the eligible accounts. Pure and portable; no timers, reads or mid-run switching. */
6
+ export declare function chooseAccount(candidates: readonly Account[], room: (a: Account) => Room, nowMs: number): {
7
+ account: Account;
8
+ why: PickWhy;
9
+ } | undefined;
10
+ /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. */
11
+ export declare function resolveSelection(accounts: readonly Account[], defaults: Defaults, sel: RunSelection, room: RoomOf, nowMs: number, models?: (a: Account) => readonly ModelInfo[], bound?: readonly AccountId[]): AccountPick;
package/dist/pick.js ADDED
@@ -0,0 +1,145 @@
1
+ import { words } from "./words.js";
2
+ const DAY_MS = 24 * 60 * 60 * 1000;
3
+ const finite = (n) => typeof n === 'number' && Number.isFinite(n);
4
+ const reset = (r) => r.resetsAt ?? Infinity;
5
+ const roomWords = (left, span) => words(`room.${span}`, { left: `${Math.round(left)}%` });
6
+ // "3:40pm", or "Fri 3:40pm" when it isn't today (matches @byokit/accounts' clock).
7
+ const clock = (t) => (new Date(t).toDateString() === new Date().toDateString() ? '' : new Date(t).toLocaleDateString('en-US', { weekday: 'short' }) + ' ') +
8
+ new Date(t).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).toLowerCase();
9
+ // One shared eligibility predicate for direct picks, Auto and every explanation row. Room is read once per row.
10
+ // `bound` (kit only) is the accounts a conversation may use; an account outside it is never a candidate, even when
11
+ // the selection names it, and its row carries `out: 'bound'`. An account the selection names is judged only on
12
+ // `bound` and the demand, so an API-key or resting account it names carries no `billing`/`resting` `out`.
13
+ function considerRows(accounts, room, nowMs, demand = [], models, provider, named, bound) {
14
+ return accounts.map((account) => {
15
+ const r = room(account);
16
+ const age = finite(r.at) ? Math.max(0, nowMs - r.at) : 'unknown';
17
+ const stale = typeof age === 'number' && age > DAY_MS;
18
+ const left = r.left === 'unknown' || !finite(r.left) || stale ? 'unknown' : Math.max(0, Math.min(100, r.left));
19
+ const row = { id: account.id, left, age,
20
+ confidence: stale ? 'stale' : left === 'unknown' || age === 'unknown' ? 'unknown' : 'known', reason: 'no_reading' };
21
+ if (r.left !== 'unknown') {
22
+ row.span = r.span;
23
+ if (finite(r.resetsAt))
24
+ row.resetsAt = r.resetsAt;
25
+ }
26
+ if (bound !== undefined && !bound.includes(account.id))
27
+ row.out = 'bound';
28
+ if (!row.out && account.id !== named) {
29
+ if (account.state === 'resting' && finite(account.until) && account.until > nowMs) {
30
+ row.out = 'resting';
31
+ row.until = account.until;
32
+ }
33
+ else if (account.state !== 'ready' && !(account.state === 'resting' && finite(account.until) && account.until <= nowMs))
34
+ row.out = 'state';
35
+ else if (account.billing !== 'subscription')
36
+ row.out = 'billing';
37
+ }
38
+ if (!row.out && demand.length && models) {
39
+ const available = models(account);
40
+ const missing = demand.find((id) => !available.some((m) => m.id === id && m.available));
41
+ if (missing !== undefined) {
42
+ row.out = 'model';
43
+ row.missing = missing;
44
+ }
45
+ }
46
+ if (!row.out && !demand.length && provider !== undefined && account.provider !== provider && account.id !== named)
47
+ row.out = 'provider';
48
+ if (row.out)
49
+ row.reason = row.out;
50
+ else
51
+ row.tier = left === 'unknown' ? 'unknown' : left > 0 || reset(row) <= nowMs ? 'room' : 'exhausted';
52
+ return { account, row };
53
+ });
54
+ }
55
+ /** Every account's `Considered` row, in list order, with the kit-only `bound` applied. */
56
+ export function consider(accounts, defaults, sel, room, nowMs, models, bound) {
57
+ const demand = [...new Set([...(sel.model ? [sel.model] : []), ...(sel.needs ?? [])])];
58
+ const defaultAccount = accounts.find((a) => a.id === defaults.account);
59
+ const named = nameOf(accounts, defaults, sel, bound);
60
+ const provider = sel.provider ?? defaultAccount?.provider ?? accounts[0]?.provider;
61
+ return considerRows(accounts, (a) => room(a, demand), nowMs, demand, models, provider, named, bound).map((c) => c.row);
62
+ }
63
+ const tierOrder = { room: 0, unknown: 1, exhausted: 2 };
64
+ function compare(a, b) {
65
+ const tier = tierOrder[a.row.tier] - tierOrder[b.row.tier];
66
+ if (tier)
67
+ return tier;
68
+ if (a.row.tier === 'unknown')
69
+ return 0;
70
+ if (a.row.tier === 'room' && a.row.left !== b.row.left)
71
+ return Number(b.row.left) - Number(a.row.left);
72
+ // Avoid Infinity - Infinity (NaN); stable sort preserves list order for ties.
73
+ return reset(a.row) === reset(b.row) ? 0 : reset(a.row) < reset(b.row) ? -1 : 1;
74
+ }
75
+ function rankingReason(winner, next) {
76
+ if (winner.row.tier === 'unknown')
77
+ return 'no_reading';
78
+ if (winner.row.tier === 'exhausted')
79
+ return 'refills_first';
80
+ if (!next || next.row.tier !== 'room' || winner.row.left !== next.row.left)
81
+ return 'most_room';
82
+ return reset(winner.row) < reset(next.row) ? 'earlier_reset' : 'list_order';
83
+ }
84
+ function rank(rows) {
85
+ const ranked = rows.filter((c) => !c.row.out).sort(compare);
86
+ const winner = ranked[0];
87
+ if (!winner)
88
+ return undefined;
89
+ const why = ranked.length === 1 ? 'only' : rankingReason(winner, ranked[1]);
90
+ for (const c of ranked)
91
+ c.row.reason = c === winner ? why : rankingReason(winner, c);
92
+ return { account: winner.account, why, row: winner.row };
93
+ }
94
+ /** Auto's winner only; `candidates` are the eligible accounts. Pure and portable; no timers, reads or mid-run switching. */
95
+ export function chooseAccount(candidates, room, nowMs) {
96
+ const pick = rank(considerRows(candidates, room, nowMs));
97
+ return pick && { account: pick.account, why: pick.why };
98
+ }
99
+ // The `'default'` id is the saved default account when it is ready, a subscription (or a non-custom API choice) and
100
+ // in `bound`; a default outside `bound` is treated as not ready (its row has `out: 'bound'`) and the pick falls through.
101
+ function nameOf(accounts, defaults, sel, bound) {
102
+ const defaultAccount = accounts.find((a) => a.id === defaults.account);
103
+ const usable = defaultAccount?.state === 'ready' &&
104
+ (defaultAccount.billing === 'subscription' || (defaultAccount.billing === 'api' && defaultAccount.provider !== 'custom')) &&
105
+ (bound === undefined || bound.includes(defaultAccount.id));
106
+ return sel.account === 'default' ? (usable ? defaultAccount.id : undefined) : sel.account === 'auto' ? undefined : sel.account;
107
+ }
108
+ /** Resolve once before starting. Without `models`, membership is host-validated and a missing model is ''. */
109
+ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, bound) {
110
+ const demand = [...new Set([...(sel.model ? [sel.model] : []), ...(sel.needs ?? [])])];
111
+ const defaultAccount = accounts.find((a) => a.id === defaults.account);
112
+ const named = nameOf(accounts, defaults, sel, bound);
113
+ const provider = sel.provider ?? defaultAccount?.provider ?? accounts[0]?.provider;
114
+ const rows = considerRows(accounts, (a) => room(a, demand), nowMs, demand, models, provider, named, bound);
115
+ const considered = rows.map((c) => c.row);
116
+ const auto = rank(rows); // also fills explanations for every eligible row, even on explicit selection
117
+ if (named !== undefined) {
118
+ const chosen = rows.find((c) => c.account.id === named);
119
+ if (!chosen)
120
+ return { ok: false, code: 'unknown_account', reason: words('auto.none', { provider: provider ?? '' }), considered };
121
+ if (chosen.row.out)
122
+ return { ok: false, code: 'not_included', reason: words(`pick.out.${chosen.row.out}`, { model: chosen.row.missing ?? '' }), considered };
123
+ return finish(chosen.account, chosen.row, sel.account === 'default' ? 'default' : 'chosen', sel.account === 'default' ? 'default' : 'chosen');
124
+ }
125
+ if (sel.account === 'default' && defaults.auto === false) {
126
+ const first = rows.find((c) => !c.row.out);
127
+ if (first)
128
+ return finish(first.account, first.row, 'default', 'first_ready');
129
+ }
130
+ else if (auto)
131
+ return finish(auto.account, auto.row, 'auto', auto.why);
132
+ return { ok: false, code: 'none', reason: words('auto.none', { provider: provider ?? '' }), considered };
133
+ function finish(account, row, how, why) {
134
+ const available = models?.(account).filter((m) => m.available);
135
+ const model = sel.model ?? (defaults.model && (!available || available.some((m) => m.id === defaults.model)) ? defaults.model : available?.[0]?.id) ?? '';
136
+ if (models && !model)
137
+ return { ok: false, code: 'not_included', reason: words('pick.out.model', { model: sel.model ?? '' }), considered };
138
+ row.reason = why;
139
+ const slots = { name: account.name, provider: account.provider };
140
+ const reason = how !== 'auto' ? words(`pick.why.${why}`, slots) : row.tier === 'unknown' ? words('auto.unknown', slots) : row.tier === 'exhausted' ?
141
+ words(finite(row.resetsAt) ? 'auto.refills' : 'auto.refillsNoTime', { ...slots, time: finite(row.resetsAt) ? clock(row.resetsAt) : '' }) :
142
+ words('auto.room', { ...slots, room: roomWords(Number(row.left), row.span ?? 'tightest') });
143
+ return { ok: true, account, model, how, why, reason, considered };
144
+ }
145
+ }
package/dist/signin.d.ts CHANGED
@@ -21,4 +21,5 @@ export declare function signIn(ctx: SignInCtx, member: Member, o: SignInOptions,
21
21
  };
22
22
  /** Usable provider profiles plus native Claude Code readiness reported by the engine. */
23
23
  export declare function providers(ctx: SignInCtx, member: Member, refresh?: boolean): Promise<string[]>;
24
+ /** Signs the member's agent out of its own profiles of `provider` only (5.7, 5.15). */
24
25
  export declare function signOut(ctx: SignInCtx, member: Member, provider: string): Promise<void>;
package/dist/signin.js CHANGED
@@ -433,7 +433,19 @@ export async function providers(ctx, member, refresh) {
433
433
  return signedInProviders(await (ctx.authStatus ? ctx.authStatus(agentId, refresh ?? false, true)
434
434
  : authStatus(ctx.request, agentId, refresh, true))) ?? [];
435
435
  }
436
+ /** Signs the member's agent out of its own profiles of `provider` only (5.7, 5.15). */
436
437
  export async function signOut(ctx, member, provider) {
437
438
  const { agentId } = await ctx.ensure(member);
438
- await ctx.request('models.authLogout', { provider, agentId }, { timeoutMs: STATUS_MS });
439
+ // Without profileIds the pin removes the provider's profiles from every owner store, another member's included.
440
+ // The inherited base is empty (5.15), so every profile this agent's status lists is its own.
441
+ const read = (refresh) => ctx.authStatus ? ctx.authStatus(agentId, refresh, false) : authStatus(ctx.request, agentId, refresh);
442
+ let status = await read(false);
443
+ if (status.unavailable)
444
+ status = await read(true); // no prepared snapshot yet: build it once
445
+ if (status.unavailable)
446
+ throw new Error(String(status.unavailable.message ?? 'account status unavailable'));
447
+ const profileIds = (status.providers ?? []).flatMap((row) => typeof row === 'string' || row.provider !== provider ? []
448
+ : (row.profiles ?? []).map((p) => p.profileId).filter((id) => typeof id === 'string' && id !== ''));
449
+ if (profileIds.length)
450
+ await ctx.request('models.authLogout', { provider, agentId, profileIds }, { timeoutMs: STATUS_MS });
439
451
  }
@@ -222,11 +222,17 @@ export function fakeGateway(script, o) {
222
222
  ensureAgent(id, workspace);
223
223
  return { id };
224
224
  },
225
+ // One profile per signed-in provider, `<provider>:default`, as the pin lists an agent's own profiles.
225
226
  'models.authStatus': (p) => ({
226
- providers: (agents.get(String(p?.agentId ?? 'main'))?.providers ?? []).map((provider) => ({ provider })),
227
+ providers: (agents.get(String(p?.agentId ?? 'main'))?.providers ?? []).map((provider) => ({ provider,
228
+ profiles: [{ profileId: `${provider}:default`, type: 'oauth', status: 'ok' }] })),
227
229
  }),
228
230
  'models.authLogout': (p) => {
229
231
  const agent = agents.get(String(p?.agentId ?? 'main'));
232
+ // The pin refuses a profileIds list naming a profile the agent does not hold.
233
+ if (p.profileIds !== undefined && (!Array.isArray(p.profileIds) || !p.profileIds.length
234
+ || p.profileIds.some((id) => id !== `${p.provider}:default` || !agent?.providers.includes(String(p.provider)))))
235
+ throw new Error('profileIds contain unavailable auth profiles');
230
236
  if (agent)
231
237
  agent.providers = agent.providers.filter((provider) => provider !== p.provider);
232
238
  if (keys.get(String(p.agentId))?.provider === p.provider)
package/dist/types.d.ts CHANGED
@@ -2,6 +2,15 @@ import type { OutputSchema } from './output.ts';
2
2
  import type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
3
3
  export type { OutputSchema, SchemaOutput } from './output.ts';
4
4
  export type Member = string;
5
+ /** The engine's learning mode: config key `skills.workshop.autonomous.mode` (5.6). */
6
+ export type LearningMode = 'off' | 'propose' | 'auto';
7
+ /** Exactly what `learning()` saw: absence is a value, not "probably default" (5.6). */
8
+ export type LearningCapture = {
9
+ present: false;
10
+ } | {
11
+ present: true;
12
+ mode: LearningMode;
13
+ };
5
14
  export interface ToolSpec {
6
15
  name: string;
7
16
  description: string;
@@ -132,8 +141,13 @@ export type Decision = {
132
141
  };
133
142
  export type KitState = {
134
143
  phase: 'stopped' | 'installing' | 'starting' | 'repairing' | 'ready' | 'restarting' | 'failed' | 'needs-update' | 'locked';
135
- why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset' | 'auth-store-unreadable';
144
+ why?: 'install' | 'handshake' | 'exited' | 'port' | 'version' | 'engine-already-running' | 'engine-patch' | 'sign-in-reset' | 'auth-store-unreadable' | 'auth-store-seal-size';
136
145
  retryAt?: number;
146
+ sealSize?: {
147
+ size: number;
148
+ cap: number;
149
+ file?: string;
150
+ };
137
151
  patchSet?: string | null;
138
152
  };
139
153
  export type Hello = {
@@ -233,3 +247,85 @@ export type KitEventName = GatewayEventName | 'byokit.browser';
233
247
  export type KitEventPayload<E extends KitEventName> = E extends 'byokit.browser' ? BrowserPing : GatewayEventPayload<Extract<E, GatewayEventName>>;
234
248
  export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
235
249
  export type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
250
+ export type AccountId = string;
251
+ export type AccountRef = AccountId;
252
+ export type SignInState = 'ready' | 'signing' | 'resting' | 'signed_out' | 'needs_again' | 'not_included';
253
+ export type Account = {
254
+ id: AccountId;
255
+ provider: string;
256
+ route: string;
257
+ name: string;
258
+ label: string;
259
+ email?: string;
260
+ plan?: string;
261
+ billing: 'subscription' | 'api';
262
+ state: SignInState;
263
+ until?: number;
264
+ addedAt: number;
265
+ };
266
+ export type RoomSpan = 'session' | 'week' | 'month' | 'tightest';
267
+ export type Room = {
268
+ left: number;
269
+ span: RoomSpan;
270
+ resetsAt?: number;
271
+ at?: number;
272
+ } | {
273
+ left: 'unknown';
274
+ at?: number;
275
+ };
276
+ export type ModelInfo = {
277
+ id: string;
278
+ name: string;
279
+ tier?: 'strong' | 'fast';
280
+ available: boolean;
281
+ why?: 'plan' | 'resting' | 'signed_out';
282
+ until?: number;
283
+ };
284
+ export type RunSelection = {
285
+ account: AccountRef | 'default' | 'auto';
286
+ model?: string;
287
+ needs?: string[];
288
+ provider?: string;
289
+ };
290
+ export type Defaults = {
291
+ account?: AccountId;
292
+ model?: string;
293
+ auto?: boolean;
294
+ };
295
+ export type Considered = {
296
+ id: AccountId;
297
+ out?: 'state' | 'resting' | 'billing' | 'model' | 'provider' | 'bound';
298
+ until?: number;
299
+ missing?: string;
300
+ tier?: 'room' | 'unknown' | 'exhausted';
301
+ left: number | 'unknown';
302
+ span?: RoomSpan;
303
+ resetsAt?: number;
304
+ age: number | 'unknown';
305
+ confidence: 'known' | 'stale' | 'unknown';
306
+ reason: PickWhy | 'state' | 'resting' | 'billing' | 'model' | 'provider' | 'bound';
307
+ };
308
+ export type PickWhy = 'chosen' | 'default' | 'first_ready' | 'only' | 'most_room' | 'earlier_reset' | 'list_order' | 'no_reading' | 'refills_first';
309
+ export type AccountPick = {
310
+ ok: true;
311
+ account: Account;
312
+ model: string;
313
+ how: 'chosen' | 'default' | 'auto';
314
+ why: PickWhy;
315
+ reason: string;
316
+ considered: Considered[];
317
+ } | {
318
+ ok: false;
319
+ code: 'none' | 'not_included' | 'unknown_account' | 'bound' | 'paid';
320
+ reason: string;
321
+ considered: Considered[];
322
+ };
323
+ export type MoveResult = {
324
+ ok: true;
325
+ session: string;
326
+ } | {
327
+ ok: false;
328
+ code: 'too_early' | 'busy' | 'unsupported' | 'env_mismatch' | 'close_failed' | 'start_failed';
329
+ message: string;
330
+ live?: string;
331
+ };
package/dist/words.js CHANGED
@@ -17,12 +17,15 @@ const PHASE_WORDS = {
17
17
  locked: 'engine.locked',
18
18
  'needs-update': 'engine.needsUpdate',
19
19
  };
20
+ const MiB = 1024 * 1024;
20
21
  export function stateWords(s) {
21
22
  // 'stopped' is never on screen (4.3 covers what a person can see), so 5.14 has no row for it.
22
23
  if (s.phase === 'failed' && s.why === 'engine-already-running')
23
24
  return words('engine.alreadyRunning');
24
25
  if (s.why === 'auth-store-unreadable')
25
26
  return words('engine.authStoreUnreadable');
27
+ if (s.why === 'auth-store-seal-size' && s.sealSize)
28
+ return words('engine.authStoreSealSize', { store: s.sealSize.file ? words('engine.store.engineData', { file: s.sealSize.file }) : words('engine.store.savedSignIn'), size: String(Math.floor(s.sealSize.size / MiB)), limit: String(s.sealSize.cap / MiB) });
26
29
  if (s.why === 'sign-in-reset')
27
30
  return words('engine.signInAgain');
28
31
  return s.phase === 'stopped' ? '' : words(PHASE_WORDS[s.phase]);
package/dist/words.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "engine.repairing": "Fixing a small problem with the setup. This takes a moment.",
5
5
  "engine.locked": "Your saved sign-in is locked. Unlock your password storage, then try again.",
6
6
  "engine.authStoreUnreadable": "Your saved sign-in couldn't be opened. It is unchanged. Restore its original key or a working backup, then retry.",
7
+ "engine.authStoreSealSize": "{store} is too large to keep safely (more than {size} MB; the limit is {limit} MB). Your sign-ins were kept.",
8
+ "engine.store.engineData": "Engine data {file}",
9
+ "engine.store.savedSignIn": "Your saved sign-in data",
7
10
  "engine.signInAgain": "Your saved sign-in couldn't be opened, so it was kept aside. Sign in again.",
8
11
  "engine.ready": "Ready.",
9
12
  "engine.restarting": "Something stopped. Starting it again by itself.",
@@ -71,5 +74,39 @@
71
74
  "key.ok": "Your key is ready. Use it only when you choose this option.",
72
75
  "key.invalid": "This key could not be saved. Check it and try again.",
73
76
  "key.notIncluded": "This option is not available here.",
74
- "key.missing": "Add an API key to use this option."
77
+ "key.missing": "Add an API key to use this option.",
78
+ "auto.room": "Right now that's {name}: {room}",
79
+ "auto.unknown": "Right now that's {name} (no recent reading)",
80
+ "auto.refills": "All {provider} accounts are out of room until {time}. {name} refills first.",
81
+ "auto.refillsNoTime": "All {provider} accounts are out of room. {name} refills first.",
82
+ "auto.terms": "Auto may use either of a provider's accounts.",
83
+ "auto.none": "No signed-in {provider} account.",
84
+ "room.unknown": "Room left unknown",
85
+ "room.session": "{left} left this session",
86
+ "room.week": "{left} left this week",
87
+ "room.month": "{left} left this month",
88
+ "room.tightest": "{left} left for now",
89
+ "pick.out.state": "Not signed in right now",
90
+ "pick.out.resting": "Taking a break until {time}",
91
+ "pick.out.billing": "Billed per use, so used only when you choose it",
92
+ "pick.out.model": "Doesn't include {model}",
93
+ "pick.out.provider": "A different service",
94
+ "pick.out.bound": "This conversation uses another account",
95
+ "pick.why.chosen": "You chose {name}.",
96
+ "pick.why.default": "{name} is your default.",
97
+ "pick.why.first_ready": "Your default isn't ready, so {name}, the first ready account.",
98
+ "pick.why.only": "{name} is the only account that can take this.",
99
+ "pick.why.most_room": "{name} has the most room left.",
100
+ "pick.why.earlier_reset": "{name} has as much room left and refills sooner.",
101
+ "pick.why.list_order": "{name} is tied for room and comes first in your list.",
102
+ "pick.why.no_reading": "No account has a recent reading, so {name}, first in your list.",
103
+ "pick.why.refills_first": "All accounts are out of room; {name} refills first.",
104
+ "pick.age": "Read {ago} ago",
105
+ "pick.ageUnknown": "Reading time unknown",
106
+ "ago.minutes": "{n} min",
107
+ "ago.hours": "{n} h",
108
+ "account.bound": "This conversation uses {name}. Move it to switch accounts.",
109
+ "account.paid": "This conversation uses {name}, which is billed per use. Choose {name} to keep going.",
110
+ "account.signOutFirst": "To add a different {provider} account, sign out of {provider} in your browser first.",
111
+ "account.legacyMember": "This person needs a new profile before adding more accounts."
75
112
  }