@cortexkit/common-auth 0.2.5 → 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.
@@ -0,0 +1,71 @@
1
+ import type { AddInput, PoolLockSpec, PoolRow, PoolStore, RemoveOptions } from '../store/index.js';
2
+ import type { CommandInvocation, KnobValues, MenuChoice, MenuKnob } from './model.js';
3
+ import type { ResolvedSection } from './seam.js';
4
+ /**
5
+ * What a plugin's login hands back. `ready` adds the account now. `pending`
6
+ * means the user must finish somewhere else (a browser, a device code):
7
+ * `message` tells them how, and the account is added when `completion`
8
+ * settles, with the result sent to the invocation that started it.
9
+ */
10
+ export type LoginOutcome = {
11
+ status: 'ready';
12
+ account: AddInput;
13
+ } | {
14
+ status: 'pending';
15
+ message: string;
16
+ /** Resolves with the account to add, or undefined when the user gave up. */
17
+ completion: Promise<AddInput | undefined>;
18
+ } | {
19
+ status: 'cancelled';
20
+ message?: string;
21
+ };
22
+ export interface AccountsSectionOptions {
23
+ /** What to show as an account's identity; defaults to its recorded identity. */
24
+ describeIdentity?(row: PoolRow): string | undefined;
25
+ /** The plugin's login; without it the section offers no add action. */
26
+ login?: {
27
+ label?: string;
28
+ knobs?: MenuKnob[];
29
+ run(values: KnobValues, invocation: CommandInvocation): Promise<LoginOutcome>;
30
+ };
31
+ /** Passed to `store.remove`: a reason refuses removing that id. */
32
+ protect?: RemoveOptions['protect'];
33
+ }
34
+ export interface QuotaSectionOptions {
35
+ /** The quota scope to show (`all` or a model family); defaults to `all`. */
36
+ scope?: string;
37
+ /**
38
+ * Checks quota now for the named rows. Without it, the menu asks the store
39
+ * for a reading of each row and waits for the pulls to settle.
40
+ */
41
+ check?(ids: readonly string[], invocation: CommandInvocation): Promise<void>;
42
+ }
43
+ export interface RoutingSectionOptions {
44
+ /**
45
+ * Further `routing.mode` values the plugin routes as `ordered` with the
46
+ * former main row moved (`main-first`, `fallback-first`), offered between
47
+ * `ordered` and `sticky-balanced`.
48
+ */
49
+ orderedVariants?: MenuChoice[];
50
+ formerMainId?: string;
51
+ }
52
+ export interface LimitsSectionOptions {
53
+ /**
54
+ * The quota window labels a floor can be set for. Defaults to every label
55
+ * the pool's quota readings carry, or `primary` when there are none.
56
+ */
57
+ labels?: readonly string[];
58
+ }
59
+ export interface BuiltinOptions {
60
+ store: PoolStore;
61
+ extraLocks?: readonly PoolLockSpec[];
62
+ now: () => number;
63
+ accounts?: AccountsSectionOptions;
64
+ quota?: QuotaSectionOptions;
65
+ routing?: RoutingSectionOptions;
66
+ limits?: LimitsSectionOptions;
67
+ }
68
+ /** `disabledReason` recorded when the user disables an account from the menu. */
69
+ export declare const MENU_DISABLED_REASON = "disabled from the command menu";
70
+ /** The four built-in sections, in their fixed order, from one read of the store. */
71
+ export declare function builtinSections(options: BuiltinOptions): Promise<ResolvedSection[]>;
@@ -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
+ }
@@ -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';
@@ -1,2 +1,5 @@
1
- // Placeholder so the export resolves until this subpath is built.
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;