@cortexkit/common-auth 0.2.4 → 0.2.6
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/auth-menu/accounts.d.ts +88 -0
- package/dist/auth-menu/accounts.js +401 -0
- package/dist/auth-menu/ansi.d.ts +24 -0
- package/dist/auth-menu/ansi.js +67 -0
- package/dist/auth-menu/confirm.d.ts +7 -0
- package/dist/auth-menu/confirm.js +21 -0
- package/dist/auth-menu/doctor.d.ts +52 -0
- package/dist/auth-menu/doctor.js +81 -0
- package/dist/auth-menu/index.d.ts +17 -1
- package/dist/auth-menu/index.js +12 -2
- package/dist/auth-menu/login.d.ts +47 -0
- package/dist/auth-menu/login.js +63 -0
- package/dist/auth-menu/menu.d.ts +59 -0
- package/dist/auth-menu/menu.js +73 -0
- package/dist/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/auth-menu/opencode-v1.js +47 -0
- package/dist/auth-menu/select.d.ts +21 -0
- package/dist/auth-menu/select.js +153 -0
- package/dist/auth-menu/terminal.d.ts +43 -0
- package/dist/auth-menu/terminal.js +19 -0
- package/dist/cachekeep/index.d.ts +4 -1
- package/dist/cachekeep/index.js +2 -2
- package/dist/cachekeep/manager.d.ts +214 -0
- package/dist/cachekeep/manager.js +424 -0
- package/dist/cachekeep/window.d.ts +13 -0
- package/dist/cachekeep/window.js +33 -0
- package/dist/commands/builtins.d.ts +71 -0
- package/dist/commands/builtins.js +508 -0
- package/dist/commands/index.d.ts +10 -1
- package/dist/commands/index.js +5 -2
- package/dist/commands/menu.d.ts +39 -0
- package/dist/commands/menu.js +249 -0
- package/dist/commands/model.d.ts +188 -0
- package/dist/commands/model.js +16 -0
- package/dist/commands/pi.d.ts +22 -0
- package/dist/commands/pi.js +178 -0
- package/dist/commands/seam.d.ts +36 -0
- package/dist/commands/seam.js +185 -0
- package/dist/dump/index.d.ts +90 -1
- package/dist/dump/index.js +292 -2
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/pool.d.ts +12 -0
- package/dist/store/pool.js +3 -0
- package/dist/store/settings.d.ts +63 -0
- package/dist/store/settings.js +120 -0
- package/package.json +1 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// The four sections every plugin gets from the library: Accounts, Quota,
|
|
2
|
+
// Routing and Limits. They read the pool and the plugin's settings through
|
|
3
|
+
// the store and write only through store operations, so a menu action takes
|
|
4
|
+
// the same locks as any other writer of those files.
|
|
5
|
+
import { isQuotaMap, projectQuota, } from '../quota/index.js';
|
|
6
|
+
import { DEFAULT_FORMER_MAIN_ID, orderForPlacement, resolveRoutingMode, } from '../routing/index.js';
|
|
7
|
+
/** `disabledReason` recorded when the user disables an account from the menu. */
|
|
8
|
+
export const MENU_DISABLED_REASON = 'disabled from the command menu';
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function record(value) {
|
|
13
|
+
return isRecord(value) ? { ...value } : {};
|
|
14
|
+
}
|
|
15
|
+
/** The rows the menu can name: one per id, in roster order. */
|
|
16
|
+
function distinctRows(load) {
|
|
17
|
+
if (load.status !== 'ready')
|
|
18
|
+
return [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
return load.rows.filter((row) => {
|
|
21
|
+
if (seen.has(row.id))
|
|
22
|
+
return false;
|
|
23
|
+
seen.add(row.id);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function rowName(row, identity) {
|
|
28
|
+
return row.label ?? identity ?? row.id;
|
|
29
|
+
}
|
|
30
|
+
function projected(row, scope) {
|
|
31
|
+
return isQuotaMap(row.quota) ? projectQuota(row.quota, scope) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function formatPercent(value) {
|
|
34
|
+
return `${Math.round(value)}%`;
|
|
35
|
+
}
|
|
36
|
+
function formatReset(resetsAt, now) {
|
|
37
|
+
if (resetsAt === undefined)
|
|
38
|
+
return '';
|
|
39
|
+
const at = Date.parse(resetsAt);
|
|
40
|
+
if (!Number.isFinite(at))
|
|
41
|
+
return '';
|
|
42
|
+
const minutes = Math.max(0, Math.round((at - now) / 60_000));
|
|
43
|
+
if (minutes < 60)
|
|
44
|
+
return `, resets in ${minutes}m`;
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
if (hours < 48)
|
|
47
|
+
return `, resets in ${hours}h ${minutes % 60}m`;
|
|
48
|
+
return `, resets in ${Math.floor(hours / 24)}d`;
|
|
49
|
+
}
|
|
50
|
+
/** One line per quota window: `primary 58% left` and the credit budget. */
|
|
51
|
+
function quotaSummary(quota) {
|
|
52
|
+
if (!quota || (quota.limits.length === 0 && !quota.budget))
|
|
53
|
+
return 'no quota reading yet';
|
|
54
|
+
const parts = quota.limits.map((limit) => limit.kind === 'reading' && limit.remainingPercent !== undefined
|
|
55
|
+
? `${limit.label} ${formatPercent(limit.remainingPercent)} left`
|
|
56
|
+
: `${limit.label} ${limit.kind === 'retired' ? 'retired' : 'not reported'}`);
|
|
57
|
+
if (quota.budget)
|
|
58
|
+
parts.push(quota.budget.reached
|
|
59
|
+
? 'credits spent'
|
|
60
|
+
: quota.budget.remainingPercent !== undefined
|
|
61
|
+
? `credits ${formatPercent(quota.budget.remainingPercent)} left`
|
|
62
|
+
: 'credits available');
|
|
63
|
+
return parts.join(' · ');
|
|
64
|
+
}
|
|
65
|
+
function quotaFacts(quota, now) {
|
|
66
|
+
const facts = {};
|
|
67
|
+
for (const limit of quota?.limits ?? []) {
|
|
68
|
+
facts[limit.label] =
|
|
69
|
+
limit.kind === 'reading' && limit.usedPercent !== undefined
|
|
70
|
+
? `${formatPercent(limit.usedPercent)} used${formatReset(limit.resetsAt, now)}`
|
|
71
|
+
: limit.kind === 'retired'
|
|
72
|
+
? 'retired'
|
|
73
|
+
: 'not reported';
|
|
74
|
+
}
|
|
75
|
+
if (quota?.budget)
|
|
76
|
+
facts.credits = quota.budget.reached
|
|
77
|
+
? `spent${formatReset(quota.budget.resetsAt, now)}`
|
|
78
|
+
: quota.budget.remainingPercent !== undefined
|
|
79
|
+
? `${formatPercent(quota.budget.remainingPercent)} left`
|
|
80
|
+
: 'available';
|
|
81
|
+
return facts;
|
|
82
|
+
}
|
|
83
|
+
function loadProblem(load) {
|
|
84
|
+
if (load.status === 'pending-migration')
|
|
85
|
+
return 'The account pool has not been migrated yet.';
|
|
86
|
+
if (load.status === 'error')
|
|
87
|
+
return `The ${load.file} file cannot be read: ${load.reason}`;
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function addedText(name, result) {
|
|
91
|
+
switch (result.outcome) {
|
|
92
|
+
case 'added':
|
|
93
|
+
return `Added ${name}.`;
|
|
94
|
+
case 'added-disabled':
|
|
95
|
+
return `Added ${name}, disabled: another enabled account has the same identity.`;
|
|
96
|
+
case 'completed':
|
|
97
|
+
return `Finished adding ${name}.`;
|
|
98
|
+
case 'rotated':
|
|
99
|
+
return `${name} was already in the pool; its credential was updated.`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function failureMessage(error) {
|
|
103
|
+
return error instanceof Error ? error.message : String(error);
|
|
104
|
+
}
|
|
105
|
+
async function snapshot(store) {
|
|
106
|
+
const load = await store.read();
|
|
107
|
+
const read = await store.readSettings();
|
|
108
|
+
return {
|
|
109
|
+
load,
|
|
110
|
+
rows: distinctRows(load),
|
|
111
|
+
settings: read.status === 'error' ? {} : read.settings,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function accountView(row, options) {
|
|
115
|
+
const identity = options.accounts?.describeIdentity
|
|
116
|
+
? options.accounts.describeIdentity(row)
|
|
117
|
+
: row.identity;
|
|
118
|
+
// Copy only the fields the menu shows: the store row also carries its
|
|
119
|
+
// credential and fingerprint, which must never reach a payload.
|
|
120
|
+
const account = {
|
|
121
|
+
id: row.id,
|
|
122
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
123
|
+
enabled: row.enabled,
|
|
124
|
+
type: row.type,
|
|
125
|
+
...(identity !== undefined ? { identity } : {}),
|
|
126
|
+
};
|
|
127
|
+
return { account, name: rowName(row, identity) };
|
|
128
|
+
}
|
|
129
|
+
function accountsSection(options, snap) {
|
|
130
|
+
const { store, extraLocks } = options;
|
|
131
|
+
const locks = extraLocks ? { extraLocks } : {};
|
|
132
|
+
const scope = options.quota?.scope ?? 'all';
|
|
133
|
+
const ids = snap.rows.map((row) => row.id);
|
|
134
|
+
const problem = loadProblem(snap.load);
|
|
135
|
+
const enabledCount = snap.rows.filter((row) => row.enabled).length;
|
|
136
|
+
const items = snap.rows.map((row, index) => {
|
|
137
|
+
const { account, name } = accountView(row, options);
|
|
138
|
+
const detail = [
|
|
139
|
+
row.type === 'api' ? 'API key' : 'OAuth',
|
|
140
|
+
row.invalid
|
|
141
|
+
? 'invalid'
|
|
142
|
+
: row.enabled
|
|
143
|
+
? 'enabled'
|
|
144
|
+
: `disabled${row.disabledReason ? ` (${row.disabledReason})` : ''}`,
|
|
145
|
+
...(account.identity !== undefined ? [account.identity] : []),
|
|
146
|
+
quotaSummary(projected(row, scope)),
|
|
147
|
+
].join(' · ');
|
|
148
|
+
const actions = [];
|
|
149
|
+
if (!row.invalid && row.enabled)
|
|
150
|
+
actions.push({
|
|
151
|
+
id: 'disable',
|
|
152
|
+
label: 'Disable',
|
|
153
|
+
run: async () => {
|
|
154
|
+
await store.disable(row.id, MENU_DISABLED_REASON, locks);
|
|
155
|
+
return `Disabled ${name}.`;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (!row.invalid && !row.enabled)
|
|
159
|
+
actions.push({
|
|
160
|
+
id: 'enable',
|
|
161
|
+
label: 'Enable',
|
|
162
|
+
run: async () => {
|
|
163
|
+
await store.enable(row.id, locks);
|
|
164
|
+
return `Enabled ${name}.`;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
if (ids.length > 1)
|
|
168
|
+
actions.push({
|
|
169
|
+
id: 'move',
|
|
170
|
+
label: 'Move',
|
|
171
|
+
knobs: [
|
|
172
|
+
{
|
|
173
|
+
kind: 'choice',
|
|
174
|
+
id: 'position',
|
|
175
|
+
label: 'Position',
|
|
176
|
+
choices: ids.map((_, at) => ({
|
|
177
|
+
value: String(at + 1),
|
|
178
|
+
label: String(at + 1),
|
|
179
|
+
})),
|
|
180
|
+
value: String(index + 1),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
run: async ({ values }) => {
|
|
184
|
+
const position = Number(values.position);
|
|
185
|
+
const order = ids.filter((id) => id !== row.id);
|
|
186
|
+
order.splice(position - 1, 0, row.id);
|
|
187
|
+
const result = await store.reorder(order, locks);
|
|
188
|
+
return result.outcome === 'unchanged'
|
|
189
|
+
? `${name} is already at position ${position}.`
|
|
190
|
+
: `Moved ${name} to position ${position}.`;
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
actions.push({
|
|
194
|
+
id: 'remove',
|
|
195
|
+
label: 'Remove',
|
|
196
|
+
irreversible: true,
|
|
197
|
+
confirm: `Remove ${name}? Its stored credential is deleted.`,
|
|
198
|
+
run: async () => {
|
|
199
|
+
await store.remove(row.id, {
|
|
200
|
+
...locks,
|
|
201
|
+
...(options.accounts?.protect
|
|
202
|
+
? { protect: options.accounts.protect }
|
|
203
|
+
: {}),
|
|
204
|
+
});
|
|
205
|
+
return `Removed ${name}.`;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return { id: row.id, label: name, detail, account, actions };
|
|
209
|
+
});
|
|
210
|
+
const login = options.accounts?.login;
|
|
211
|
+
const actions = [];
|
|
212
|
+
if (login && !problem)
|
|
213
|
+
actions.push({
|
|
214
|
+
id: 'add',
|
|
215
|
+
label: login.label ?? 'Add account',
|
|
216
|
+
knobs: login.knobs ?? [],
|
|
217
|
+
run: async ({ values, invocation }) => {
|
|
218
|
+
const outcome = await login.run(values, invocation);
|
|
219
|
+
if (outcome.status === 'cancelled')
|
|
220
|
+
return { ok: false, text: outcome.message ?? 'Login cancelled.' };
|
|
221
|
+
if (outcome.status === 'ready') {
|
|
222
|
+
const result = await store.add(outcome.account, locks);
|
|
223
|
+
return addedText(outcome.account.label ?? result.id, result);
|
|
224
|
+
}
|
|
225
|
+
// The login finishes after this call returns: add the account then,
|
|
226
|
+
// and send the outcome to the invocation that started the login.
|
|
227
|
+
void outcome.completion
|
|
228
|
+
.then(async (account) => {
|
|
229
|
+
if (!account) {
|
|
230
|
+
invocation.notify('Login cancelled.', 'warning');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const result = await store.add(account, locks);
|
|
234
|
+
invocation.notify(addedText(account.label ?? result.id, result));
|
|
235
|
+
}, (error) => {
|
|
236
|
+
invocation.notify(`Adding the account failed: ${failureMessage(error)}`, 'error');
|
|
237
|
+
})
|
|
238
|
+
.catch((error) => {
|
|
239
|
+
invocation.notify(`Adding the account failed: ${failureMessage(error)}`, 'error');
|
|
240
|
+
});
|
|
241
|
+
return outcome.message;
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
id: 'accounts',
|
|
246
|
+
slot: 'accounts',
|
|
247
|
+
title: 'Accounts',
|
|
248
|
+
content: {
|
|
249
|
+
lines: problem
|
|
250
|
+
? [problem]
|
|
251
|
+
: [
|
|
252
|
+
snap.rows.length === 0
|
|
253
|
+
? 'No accounts yet.'
|
|
254
|
+
: `${snap.rows.length} account(s), ${enabledCount} enabled.`,
|
|
255
|
+
],
|
|
256
|
+
items: problem ? [] : items,
|
|
257
|
+
actions,
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function quotaSection(options, snap) {
|
|
262
|
+
const scope = options.quota?.scope ?? 'all';
|
|
263
|
+
const now = options.now();
|
|
264
|
+
const items = snap.rows.map((row) => {
|
|
265
|
+
const { account, name } = accountView(row, options);
|
|
266
|
+
const quota = projected(row, scope);
|
|
267
|
+
return {
|
|
268
|
+
id: row.id,
|
|
269
|
+
label: name,
|
|
270
|
+
detail: quotaSummary(quota),
|
|
271
|
+
account,
|
|
272
|
+
facts: quotaFacts(quota, now),
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
const candidates = snap.rows.filter((row) => row.candidate);
|
|
276
|
+
const actions = [];
|
|
277
|
+
if (candidates.length > 0)
|
|
278
|
+
actions.push({
|
|
279
|
+
id: 'check',
|
|
280
|
+
label: 'Check now',
|
|
281
|
+
knobs: [
|
|
282
|
+
{
|
|
283
|
+
kind: 'choice',
|
|
284
|
+
id: 'account',
|
|
285
|
+
label: 'Account',
|
|
286
|
+
choices: [
|
|
287
|
+
{ value: '*', label: 'All accounts' },
|
|
288
|
+
...candidates.map((row) => ({
|
|
289
|
+
value: row.id,
|
|
290
|
+
label: accountView(row, options).name,
|
|
291
|
+
})),
|
|
292
|
+
],
|
|
293
|
+
value: '*',
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
run: async ({ values, invocation }) => {
|
|
297
|
+
const ids = values.account === '*'
|
|
298
|
+
? candidates.map((row) => row.id)
|
|
299
|
+
: [String(values.account)];
|
|
300
|
+
if (options.quota?.check)
|
|
301
|
+
await options.quota.check(ids, invocation);
|
|
302
|
+
else {
|
|
303
|
+
for (const id of ids)
|
|
304
|
+
options.store.requestReading(id);
|
|
305
|
+
await options.store.pullsSettled();
|
|
306
|
+
}
|
|
307
|
+
return `Checked quota for ${ids.length} account(s).`;
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
id: 'quota',
|
|
312
|
+
slot: 'quota',
|
|
313
|
+
title: 'Quota',
|
|
314
|
+
content: {
|
|
315
|
+
lines: snap.rows.length === 0 ? ['No accounts yet.'] : [`Scope: ${scope}.`],
|
|
316
|
+
items,
|
|
317
|
+
actions,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function routingSection(options, snap) {
|
|
322
|
+
const { store, extraLocks } = options;
|
|
323
|
+
const locks = extraLocks ? { extraLocks } : {};
|
|
324
|
+
const choices = [
|
|
325
|
+
{ value: 'ordered', label: 'Ordered (roster order)' },
|
|
326
|
+
...(options.routing?.orderedVariants ?? []),
|
|
327
|
+
{ value: 'sticky-balanced', label: 'Sticky balanced' },
|
|
328
|
+
];
|
|
329
|
+
const raw = record(snap.settings.routing).mode;
|
|
330
|
+
const current = typeof raw === 'string' && choices.some((choice) => choice.value === raw)
|
|
331
|
+
? raw
|
|
332
|
+
: 'ordered';
|
|
333
|
+
const resolved = resolveRoutingMode(current);
|
|
334
|
+
const ids = snap.rows.map((row) => row.id);
|
|
335
|
+
const tried = orderForPlacement(ids, resolved.placement, options.routing?.formerMainId ?? DEFAULT_FORMER_MAIN_ID);
|
|
336
|
+
const label = choices.find((choice) => choice.value === current)?.label ?? current;
|
|
337
|
+
const lines = [`Mode: ${label}.`];
|
|
338
|
+
if (ids.length > 0)
|
|
339
|
+
lines.push(`Roster order: ${ids.join(', ')}.`);
|
|
340
|
+
if (resolved.mode === 'ordered' && tried.join() !== ids.join())
|
|
341
|
+
lines.push(`Tried in order: ${tried.join(', ')}.`);
|
|
342
|
+
const actions = [
|
|
343
|
+
{
|
|
344
|
+
id: 'mode',
|
|
345
|
+
label: 'Change mode',
|
|
346
|
+
knobs: [
|
|
347
|
+
{ kind: 'choice', id: 'mode', label: 'Mode', choices, value: current },
|
|
348
|
+
],
|
|
349
|
+
run: async ({ values }) => {
|
|
350
|
+
const mode = String(values.mode);
|
|
351
|
+
await store.updateSettings((settings) => {
|
|
352
|
+
settings.routing = { ...record(settings.routing), mode };
|
|
353
|
+
return undefined;
|
|
354
|
+
}, locks);
|
|
355
|
+
return `Routing mode set to ${choices.find((choice) => choice.value === mode)?.label ?? mode}.`;
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
if (ids.length > 1)
|
|
360
|
+
actions.push({
|
|
361
|
+
id: 'order',
|
|
362
|
+
label: 'Set order',
|
|
363
|
+
knobs: [
|
|
364
|
+
{
|
|
365
|
+
kind: 'text',
|
|
366
|
+
id: 'order',
|
|
367
|
+
label: 'Account ids, first to last',
|
|
368
|
+
value: ids.join(', '),
|
|
369
|
+
required: true,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
run: async ({ values }) => {
|
|
373
|
+
const order = String(values.order)
|
|
374
|
+
.split(/[\s,]+/)
|
|
375
|
+
.filter((id) => id.length > 0);
|
|
376
|
+
const result = await store.reorder(order, locks);
|
|
377
|
+
return result.outcome === 'unchanged'
|
|
378
|
+
? 'The order is unchanged.'
|
|
379
|
+
: `Order set to ${result.ids.join(', ')}.`;
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
id: 'routing',
|
|
384
|
+
slot: 'routing',
|
|
385
|
+
title: 'Routing',
|
|
386
|
+
content: { lines, actions },
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function floorLabels(options, snap) {
|
|
390
|
+
if (options.limits?.labels)
|
|
391
|
+
return [...options.limits.labels];
|
|
392
|
+
const scope = options.quota?.scope ?? 'all';
|
|
393
|
+
const labels = [];
|
|
394
|
+
for (const row of snap.rows)
|
|
395
|
+
for (const limit of projected(row, scope)?.limits ?? [])
|
|
396
|
+
if (!labels.includes(limit.label))
|
|
397
|
+
labels.push(limit.label);
|
|
398
|
+
return labels.length > 0 ? labels : ['primary'];
|
|
399
|
+
}
|
|
400
|
+
function limitsSection(options, snap) {
|
|
401
|
+
const { store, extraLocks } = options;
|
|
402
|
+
const locks = extraLocks ? { extraLocks } : {};
|
|
403
|
+
const killswitch = record(snap.settings.killswitch);
|
|
404
|
+
const enabled = killswitch.enabled === true;
|
|
405
|
+
const floors = record(killswitch.accounts);
|
|
406
|
+
const labels = floorLabels(options, snap);
|
|
407
|
+
const items = snap.rows.map((row) => {
|
|
408
|
+
const { account, name } = accountView(row, options);
|
|
409
|
+
const own = record(floors[row.id]);
|
|
410
|
+
const set = labels.filter((label) => typeof own[label] === 'number');
|
|
411
|
+
return {
|
|
412
|
+
id: row.id,
|
|
413
|
+
label: name,
|
|
414
|
+
detail: set.length === 0
|
|
415
|
+
? 'no floors'
|
|
416
|
+
: `floors: ${set.map((label) => `${label} ${own[label]}%`).join(', ')}`,
|
|
417
|
+
account,
|
|
418
|
+
actions: [
|
|
419
|
+
{
|
|
420
|
+
id: 'floors',
|
|
421
|
+
label: 'Set floors',
|
|
422
|
+
knobs: labels.map((label) => ({
|
|
423
|
+
kind: 'number',
|
|
424
|
+
id: label,
|
|
425
|
+
label: `Minimum % left for ${label}`,
|
|
426
|
+
min: 0,
|
|
427
|
+
max: 100,
|
|
428
|
+
...(typeof own[label] === 'number'
|
|
429
|
+
? { value: own[label] }
|
|
430
|
+
: {}),
|
|
431
|
+
})),
|
|
432
|
+
run: async ({ values }) => {
|
|
433
|
+
await store.updateSettings((settings) => {
|
|
434
|
+
const next = record(settings.killswitch);
|
|
435
|
+
const accounts = record(next.accounts);
|
|
436
|
+
const mine = record(accounts[row.id]);
|
|
437
|
+
for (const label of labels) {
|
|
438
|
+
const value = values[label];
|
|
439
|
+
if (typeof value === 'number')
|
|
440
|
+
mine[label] = value;
|
|
441
|
+
else if (value === null)
|
|
442
|
+
delete mine[label];
|
|
443
|
+
}
|
|
444
|
+
if (Object.keys(mine).length > 0)
|
|
445
|
+
accounts[row.id] = mine;
|
|
446
|
+
else
|
|
447
|
+
delete accounts[row.id];
|
|
448
|
+
if (Object.keys(accounts).length > 0)
|
|
449
|
+
next.accounts = accounts;
|
|
450
|
+
else
|
|
451
|
+
delete next.accounts;
|
|
452
|
+
settings.killswitch = next;
|
|
453
|
+
return undefined;
|
|
454
|
+
}, locks);
|
|
455
|
+
return `Floors updated for ${name}.`;
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
id: 'limits',
|
|
463
|
+
slot: 'limits',
|
|
464
|
+
title: 'Limits',
|
|
465
|
+
content: {
|
|
466
|
+
lines: [
|
|
467
|
+
`Killswitch: ${enabled ? 'on' : 'off'}.`,
|
|
468
|
+
'With the killswitch on, an account whose quota falls below one of its floors is not used.',
|
|
469
|
+
],
|
|
470
|
+
items,
|
|
471
|
+
actions: [
|
|
472
|
+
{
|
|
473
|
+
id: 'killswitch',
|
|
474
|
+
label: enabled ? 'Turn killswitch off' : 'Turn killswitch on',
|
|
475
|
+
knobs: [
|
|
476
|
+
{
|
|
477
|
+
kind: 'toggle',
|
|
478
|
+
id: 'enabled',
|
|
479
|
+
label: 'Killswitch',
|
|
480
|
+
value: !enabled,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
run: async ({ values }) => {
|
|
484
|
+
const on = values.enabled === true;
|
|
485
|
+
await store.updateSettings((settings) => {
|
|
486
|
+
settings.killswitch = {
|
|
487
|
+
...record(settings.killswitch),
|
|
488
|
+
enabled: on,
|
|
489
|
+
};
|
|
490
|
+
return undefined;
|
|
491
|
+
}, locks);
|
|
492
|
+
return `Killswitch ${on ? 'on' : 'off'}.`;
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/** The four built-in sections, in their fixed order, from one read of the store. */
|
|
500
|
+
export async function builtinSections(options) {
|
|
501
|
+
const snap = await snapshot(options.store);
|
|
502
|
+
return [
|
|
503
|
+
accountsSection(options, snap),
|
|
504
|
+
quotaSection(options, snap),
|
|
505
|
+
routingSection(options, snap),
|
|
506
|
+
limitsSection(options, snap),
|
|
507
|
+
];
|
|
508
|
+
}
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export type { AccountsSectionOptions, LimitsSectionOptions, LoginOutcome, QuotaSectionOptions, RoutingSectionOptions, } from './builtins.js';
|
|
2
|
+
export { MENU_DISABLED_REASON } from './builtins.js';
|
|
3
|
+
export type { CommandMenu, CommandMenuOptions } from './menu.js';
|
|
4
|
+
export { createCommandMenu, parseApplyRequest } from './menu.js';
|
|
5
|
+
export type { ActionDefinition, ActionInput, ActionOutcome, CommandApplyRequest, CommandApplyResult, CommandDialogPayload, CommandInvocation, CommandMenuModel, ItemDefinition, KnobValue, KnobValues, MenuAccount, MenuAction, MenuChoice, MenuConfirmation, MenuItem, MenuKnob, MenuSection, NotifyKind, PluginExtraSection, PluginSection, SectionContent, SectionSlot, } from './model.js';
|
|
6
|
+
export { SECTION_SLOTS } from './model.js';
|
|
7
|
+
export type { PiMenuOptions, PiMenuUi } from './pi.js';
|
|
8
|
+
export { runPiCommandMenu } from './pi.js';
|
|
9
|
+
export type { SeamLogger } from './seam.js';
|
|
10
|
+
export { DEFAULT_IRREVERSIBLE_CONFIRMATION } from './seam.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export {};
|
|
1
|
+
export { MENU_DISABLED_REASON } from './builtins.js';
|
|
2
|
+
export { createCommandMenu, parseApplyRequest } from './menu.js';
|
|
3
|
+
export { SECTION_SLOTS } from './model.js';
|
|
4
|
+
export { runPiCommandMenu } from './pi.js';
|
|
5
|
+
export { DEFAULT_IRREVERSIBLE_CONFIRMATION } from './seam.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PoolLockSpec, PoolStore } from '../store/index.js';
|
|
2
|
+
import { type AccountsSectionOptions, type LimitsSectionOptions, type QuotaSectionOptions, type RoutingSectionOptions } from './builtins.js';
|
|
3
|
+
import type { CommandApplyRequest, CommandApplyResult, CommandDialogPayload, CommandInvocation, PluginExtraSection, PluginSection } from './model.js';
|
|
4
|
+
import { type SeamLogger } from './seam.js';
|
|
5
|
+
export interface CommandMenuOptions {
|
|
6
|
+
/** The slash command's name without the slash (`openai`, `claude`). */
|
|
7
|
+
command: string;
|
|
8
|
+
/** The dialog title. */
|
|
9
|
+
title: string;
|
|
10
|
+
store: PoolStore;
|
|
11
|
+
/** The plugin's legacy locks, passed to every store write the menu makes. */
|
|
12
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
13
|
+
accounts?: AccountsSectionOptions;
|
|
14
|
+
quota?: QuotaSectionOptions;
|
|
15
|
+
routing?: RoutingSectionOptions;
|
|
16
|
+
limits?: LimitsSectionOptions;
|
|
17
|
+
/** The Cache section; omitted when the plugin has none. */
|
|
18
|
+
cache?: PluginSection;
|
|
19
|
+
/** The Diagnostics section (dumps, logging); omitted when the plugin has none. */
|
|
20
|
+
diagnostics?: PluginSection;
|
|
21
|
+
/** Provider extras, shown after every fixed section in this order. */
|
|
22
|
+
extras?: readonly PluginExtraSection[];
|
|
23
|
+
/** Receives the seam's warnings and failed actions; defaults to the library logger. */
|
|
24
|
+
logger?: SeamLogger;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
export interface CommandMenu {
|
|
28
|
+
readonly command: string;
|
|
29
|
+
/** The dialog payload for one invocation of the slash command. */
|
|
30
|
+
open(invocation: CommandInvocation): Promise<CommandDialogPayload>;
|
|
31
|
+
/** Applies one action and returns its message and the refreshed menu. */
|
|
32
|
+
apply(request: CommandApplyRequest, invocation: CommandInvocation): Promise<CommandApplyResult>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createCommandMenu(options: CommandMenuOptions): CommandMenu;
|
|
35
|
+
/**
|
|
36
|
+
* Checks an apply request that arrived over the loopback RPC; undefined when
|
|
37
|
+
* it is not one. Only the request's own fields are kept.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseApplyRequest(value: unknown): CommandApplyRequest | undefined;
|