bareloop 0.31.0 → 0.32.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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @param {string} [home] test seam; production never passes it
3
+ * @returns {string} `<home>/config.json`
4
+ */
5
+ export function configPath(home?: string): string;
6
+ /**
7
+ * @typedef {object} ConfigRead
8
+ * @property {Record<string, any>} config the parsed document ({} when missing/unreadable)
9
+ * @property {boolean} exists
10
+ * @property {string|null} problem plain words when the file is there but unreadable
11
+ */
12
+ /**
13
+ * @param {{ home?: string }} [opts]
14
+ * @returns {ConfigRead}
15
+ */
16
+ export function readConfig(opts?: {
17
+ home?: string;
18
+ }): ConfigRead;
19
+ /**
20
+ * Apply a patch and write. `null` deletes a field. Top-level fields shallow-merge;
21
+ * `keys` merges per key variable name, then per field. Validated before anything is
22
+ * written; nothing is written on a refusal.
23
+ *
24
+ * Patch fields understood: `monthlyLimitUsd` (finite > 0), `anthropicBalanceNote`
25
+ * (finite >= 0), `keys.<ENV NAME>.{name,shape,baseUrl}` (Name = a model id, shape = a provider table name, baseUrl = blank or http(s)). Any other
26
+ * field is passed through untouched (kept), but the whole document is swept for
27
+ * secret-shaped strings first.
28
+ * @param {Record<string, any>} patch
29
+ * @param {{ home?: string }} [opts]
30
+ * @returns {Record<string, any>} the document as written
31
+ */
32
+ export function updateConfig(patch: Record<string, any>, opts?: {
33
+ home?: string;
34
+ }): Record<string, any>;
35
+ /** A plain-words config failure (bad value, secret-shaped string, unreadable file). */
36
+ export class ConfigError extends Error {
37
+ }
38
+ export type ConfigRead = {
39
+ /**
40
+ * the parsed document ({} when missing/unreadable)
41
+ */
42
+ config: Record<string, any>;
43
+ exists: boolean;
44
+ /**
45
+ * plain words when the file is there but unreadable
46
+ */
47
+ problem: string | null;
48
+ };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @typedef {object} Deps
3
3
  * @property {Record<string,string|undefined>} [env]
4
+ * @property {string} [keysHome] P4a — test seam: load this home's `.env` keys file even when `env` is injected.
4
5
  * @property {NodeJS.ReadStream} [stdin]
5
6
  * @property {NodeJS.WritableStream} [stdout]
6
7
  * @property {NodeJS.WritableStream} [stderr]
@@ -25,6 +26,10 @@
25
26
  export function main(argv: string[], deps?: Deps): Promise<number>;
26
27
  export type Deps = {
27
28
  env?: Record<string, string | undefined> | undefined;
29
+ /**
30
+ * P4a — test seam: load this home's `.env` keys file even when `env` is injected.
31
+ */
32
+ keysHome?: string | undefined;
28
33
  stdin?: NodeJS.ReadStream | undefined;
29
34
  stdout?: NodeJS.WritableStream | undefined;
30
35
  stderr?: NodeJS.WritableStream | undefined;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `~/.config/bareloop` (or the injected override) — same home every other
3
+ * per-person file (runs.jsonl, config.json) lives in.
4
+ * @param {string} [home] test seam; production never passes it
5
+ * @returns {string}
6
+ */
7
+ export function keysHome(home?: string): string;
8
+ /**
9
+ * @param {string} [home]
10
+ * @returns {string} `<home>/.env`
11
+ */
12
+ export function keysFilePath(home?: string): string;
13
+ /**
14
+ * Parse the file's text. Lines that are not `NAME=value` are skipped silently
15
+ * (never echoed — a malformed line may hold a secret).
16
+ * @param {string} text
17
+ * @returns {Record<string,string>}
18
+ */
19
+ export function parseKeysText(text: string): Record<string, string>;
20
+ /**
21
+ * The NAMES in the file that carry a non-empty value, in file order — the Providers rows
22
+ * (an empty preset line is not a row). Names only; a missing/unreadable file = none.
23
+ * @param {string} [home]
24
+ * @returns {string[]}
25
+ */
26
+ export function filledKeyNames(home?: string): string[];
27
+ /**
28
+ * Create the keys file with the five empty preset lines when — and only when — it does not
29
+ * exist (mode 600). An existing file is never touched.
30
+ * @param {readonly string[]} presetNames the names to seed, in order
31
+ * @param {string} [home]
32
+ * @returns {boolean} true if it was created
33
+ */
34
+ export function ensureKeysFile(presetNames: readonly string[], home?: string): boolean;
35
+ /**
36
+ * @typedef {object} KeysLoad
37
+ * @property {boolean} exists the file is there
38
+ * @property {string[]} names the NAMES the file defines (sorted; never values)
39
+ * @property {string|null} warning a plain-words warning (file readable by others, or
40
+ * unreadable), else null — a warning, never a refusal
41
+ * @property {Record<string,string|undefined>} env shell env with the file filling the
42
+ * names the shell does not set. THE ONLY place values leave this module.
43
+ */
44
+ /**
45
+ * Load the keys file over an env. Missing file is fine (no names, env unchanged).
46
+ * @param {{ env?: Record<string,string|undefined>, home?: string }} [opts]
47
+ * @returns {KeysLoad}
48
+ */
49
+ export function loadKeysEnv(opts?: {
50
+ env?: Record<string, string | undefined>;
51
+ home?: string;
52
+ }): KeysLoad;
53
+ /**
54
+ * The door helper the CLI and the panel call: load the file only when the caller is
55
+ * on the REAL environment (no injected env) or names a keys home explicitly — so a
56
+ * test that injects `env` alone never reads the real `~/.config/bareloop/.env`.
57
+ * @param {{ env?: Record<string,string|undefined>, keysHome?: string }} deps
58
+ * @returns {KeysLoad & { env: Record<string,string|undefined>, home: string|undefined }}
59
+ * `home` = the home the file was loaded from (the real `~/.config/bareloop` when none was
60
+ * injected) — or `undefined` when loading was skipped (injected env, no keysHome). The
61
+ * door passes it on so config.json (src/config.js) is read from the SAME place, and never
62
+ * from the real home in a test.
63
+ */
64
+ export function keysForDoor(deps: {
65
+ env?: Record<string, string | undefined>;
66
+ keysHome?: string;
67
+ }): KeysLoad & {
68
+ env: Record<string, string | undefined>;
69
+ home: string | undefined;
70
+ };
71
+ export type KeysLoad = {
72
+ /**
73
+ * the file is there
74
+ */
75
+ exists: boolean;
76
+ /**
77
+ * the NAMES the file defines (sorted; never values)
78
+ */
79
+ names: string[];
80
+ /**
81
+ * a plain-words warning (file readable by others, or
82
+ * unreadable), else null — a warning, never a refusal
83
+ */
84
+ warning: string | null;
85
+ /**
86
+ * shell env with the file filling the
87
+ * names the shell does not set. THE ONLY place values leave this module.
88
+ */
89
+ env: Record<string, string | undefined>;
90
+ };
package/types/ledger.d.ts CHANGED
@@ -81,6 +81,20 @@ export function spendProvenance(events: any[]): {
81
81
  unpriced: ProvenanceBucket;
82
82
  unknown: ProvenanceBucket;
83
83
  };
84
+ /**
85
+ * The priced-rounds spend floor + first-record→last-record wall floor,
86
+ * derived straight off the spine's raw records — the same derivation a
87
+ * died run's "at least …" figures use, factored out so build item 6
88
+ * (2026-09-28) can hand a genuinely-still-running spine the identical
89
+ * floor instead of "unknown" on the Run tab, without a second copy of this
90
+ * math.
91
+ * @param {any[]} records raw parsed spine records
92
+ * @returns {{spendFloorUsd: number|null, wallFloorMs: number|null}}
93
+ */
94
+ export function floorsFromRecords(records: any[]): {
95
+ spendFloorUsd: number | null;
96
+ wallFloorMs: number | null;
97
+ };
84
98
  /** severity order, worst-first (the fold's display order; frequency ranks within) */
85
99
  export const LEDGER_CLASSES: readonly string[];
86
100
  /**
@@ -0,0 +1,192 @@
1
+ /**
2
+ * One run's own spend, off its spine records: this LEG's figure (never the chain fold),
3
+ * plus the drafting spend the run was signed with (counted once — on the first leg only).
4
+ * @param {any[]} records
5
+ * @returns {{ usd: number, complete: boolean }}
6
+ */
7
+ export function legSpend(records: any[]): {
8
+ usd: number;
9
+ complete: boolean;
10
+ };
11
+ /**
12
+ * One run's wall time off its spine: job-start -> job-end when the run ended (exact), else the
13
+ * first -> last record floor (the SAME floor a died / still-running run's "at least" wall uses,
14
+ * `floorsFromRecords`). `ms` null = unknown — never 0.
15
+ * @param {any[]} records
16
+ * @returns {{ ms: number|null, complete: boolean }}
17
+ */
18
+ export function legWall(records: any[]): {
19
+ ms: number | null;
20
+ complete: boolean;
21
+ };
22
+ /**
23
+ * Tokens a spine's rounds processed (input + output + cache read + cache write — all four,
24
+ * so a cache-heavy run is not under-reported), off the real `usage` field names.
25
+ * @param {any[]} records
26
+ * @returns {number}
27
+ */
28
+ export function legTokens(records: any[]): number;
29
+ /**
30
+ * Every listed run as one {@link Leg}. The ONE reader the month total, the all-time total
31
+ * and the per-provider figures share.
32
+ * @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `now` = the clock for the in-flight test; `resumingSpine` = the spine a resume continues: that leg reserves its spend only, never its unspent cap (the resume's own leg cap is that remainder)
33
+ * @returns {Leg[]}
34
+ */
35
+ export function readLegs(opts?: {
36
+ home?: string;
37
+ now?: () => number;
38
+ resumingSpine?: string | null;
39
+ }): Leg[];
40
+ /**
41
+ * @param {{ home?: string, now?: () => number, resumingSpine?: string|null }} [opts] `resumingSpine`: see {@link readLegs}
42
+ * @returns {{ usd: number, atLeast: boolean, runs: number, reservedUsd: number }} `usd` = real spend (the Money tab's figure); `reservedUsd` = the same with in-flight runs at their cap (the refusal check's)
43
+ */
44
+ export function monthSpend(opts?: {
45
+ home?: string;
46
+ now?: () => number;
47
+ resumingSpine?: string | null;
48
+ }): {
49
+ usd: number;
50
+ atLeast: boolean;
51
+ runs: number;
52
+ reservedUsd: number;
53
+ };
54
+ /**
55
+ * The Money tab's figures: all-time and this-month spend, and per-provider breakdown
56
+ * (rows from `rowIdFor`; a run on no known row lands under its own provider name, never
57
+ * pooled into another row). Any leg with unknown spend makes the figure it belongs to
58
+ * an "at least".
59
+ * @param {{ home?: string, now?: () => number, rows?: readonly import('./providerrows.js').KeyRow[] }} [opts] `rows` = the Providers rows: when given, `tokensByRow` sums each row's tokens
60
+ * @returns {{ tokensByRow: Record<string, number>, total: {usd: number, atLeast: boolean, tokens: number}, month: {usd: number, atLeast: boolean, tokens: number},
61
+ * byProvider: Record<string, {label: string, monthUsd: number, monthAtLeast: boolean, totalUsd: number, totalAtLeast: boolean, tokens: number, vouchedRounds: number, otherRounds: number, monthWallMs: number|null, monthWallAtLeast: boolean, totalWallMs: number|null, totalWallAtLeast: boolean}> }}
62
+ */
63
+ export function spendSummary(opts?: {
64
+ home?: string;
65
+ now?: () => number;
66
+ rows?: readonly import("./providerrows.js").KeyRow[];
67
+ }): {
68
+ tokensByRow: Record<string, number>;
69
+ total: {
70
+ usd: number;
71
+ atLeast: boolean;
72
+ tokens: number;
73
+ };
74
+ month: {
75
+ usd: number;
76
+ atLeast: boolean;
77
+ tokens: number;
78
+ };
79
+ byProvider: Record<string, {
80
+ label: string;
81
+ monthUsd: number;
82
+ monthAtLeast: boolean;
83
+ totalUsd: number;
84
+ totalAtLeast: boolean;
85
+ tokens: number;
86
+ vouchedRounds: number;
87
+ otherRounds: number;
88
+ monthWallMs: number | null;
89
+ monthWallAtLeast: boolean;
90
+ totalWallMs: number | null;
91
+ totalWallAtLeast: boolean;
92
+ }>;
93
+ };
94
+ /**
95
+ * @typedef {object} MonthlyRoom
96
+ * @property {boolean} ok
97
+ * @property {number|null} leftUsd what is left this month (null = no limit set)
98
+ * @property {number|null} limitUsd the person's monthly limit (null = none)
99
+ * @property {boolean} atLeast the month's spend is a floor, so `leftUsd` is a ceiling
100
+ */
101
+ /**
102
+ * The person's monthly limit off a parsed config.json: key absent (or null) = no limit (null);
103
+ * a number above 0 = the limit; ANY other value present is a broken setting and throws
104
+ * `ConfigError` — a limit the file names but this code cannot read never reads as "no limit".
105
+ * @param {Record<string, any>} config
106
+ * @param {string} [home]
107
+ * @returns {number|null}
108
+ */
109
+ export function monthlyLimitOf(config: Record<string, any>, home?: string): number | null;
110
+ /**
111
+ * Does a run with this $ cap fit in what is left this month (running jobs counted at their full cap, except the spine a resume continues — `resumingSpine` — which counts its real spend)? No limit set (key absent or null) = ok, no check; a limit that is present but not a
112
+ * number above 0 throws `ConfigError`.
113
+ * Compared in whole cents. Also throws `ConfigError` when config.json is unreadable — a gate
114
+ * whose own instrument is broken refuses; it never silently runs with no limit.
115
+ * @param {{ capUsd: number, home?: string, now?: () => number, resumingSpine?: string|null }} args
116
+ * @returns {MonthlyRoom}
117
+ */
118
+ export function checkMonthlyRoom({ capUsd, home, now, resumingSpine }: {
119
+ capUsd: number;
120
+ home?: string;
121
+ now?: () => number;
122
+ resumingSpine?: string | null;
123
+ }): MonthlyRoom;
124
+ /**
125
+ * The ONE refusal text, everywhere (CLI, panel cap note, panel Sign refusal).
126
+ * @param {MonthlyRoom} room
127
+ * @returns {string|null} null when the run fits
128
+ */
129
+ export function monthlyRefusalText(room: MonthlyRoom): string | null;
130
+ export type Leg = {
131
+ /**
132
+ * when the run was listed
133
+ */
134
+ at: Date;
135
+ /**
136
+ * job-start's provider (null = an older spine that carried none)
137
+ */
138
+ provider: string | null;
139
+ baseUrl: string | null;
140
+ /**
141
+ * this leg's spend (a floor when `!complete`)
142
+ */
143
+ usd: number;
144
+ /**
145
+ * every figure behind `usd` is known
146
+ */
147
+ complete: boolean;
148
+ tokens: number;
149
+ /**
150
+ * job-start's model (older spines carry a model but no provider)
151
+ */
152
+ model: string | null;
153
+ /**
154
+ * this leg's wall time (null = unknown; a floor when `!wallComplete`)
155
+ */
156
+ wallMs: number | null;
157
+ /**
158
+ * the wall is the exact job-start -> job-end span
159
+ */
160
+ wallComplete: boolean;
161
+ /**
162
+ * rounds priced by a rate somebody vouched for (`provider`/`caller`)
163
+ */
164
+ vouchedRounds: number;
165
+ /**
166
+ * every other round: a built-in guess, unpriced, or no provenance on record
167
+ */
168
+ otherRounds: number;
169
+ /**
170
+ * what the refusal check counts: `usd`, or the leg's cap when the run is in flight
171
+ */
172
+ reservedUsd: number;
173
+ /**
174
+ * the spine was missing/unreadable or the listed date was bad (spend unknown)
175
+ */
176
+ unreadable: boolean;
177
+ };
178
+ export type MonthlyRoom = {
179
+ ok: boolean;
180
+ /**
181
+ * what is left this month (null = no limit set)
182
+ */
183
+ leftUsd: number | null;
184
+ /**
185
+ * the person's monthly limit (null = none)
186
+ */
187
+ limitUsd: number | null;
188
+ /**
189
+ * the month's spend is a floor, so `leftUsd` is a ceiling
190
+ */
191
+ atLeast: boolean;
192
+ };
@@ -25,7 +25,7 @@ export function checkHumanGuard(req: import("node:http").IncomingMessage, { toke
25
25
  * deliberately has none of).
26
26
  * @param {{ port: number, token: string, env?: Record<string,string|undefined>,
27
27
  * sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
28
- * jobsDir?: string, fetchImpl?: typeof fetch }} opts
28
+ * jobsDir?: string, fetchImpl?: typeof fetch, home?: string }} opts
29
29
  */
30
30
  export function createAuthorRoutes(opts: {
31
31
  port: number;
@@ -36,6 +36,7 @@ export function createAuthorRoutes(opts: {
36
36
  bareloopBin?: string;
37
37
  jobsDir?: string;
38
38
  fetchImpl?: typeof fetch;
39
+ home?: string;
39
40
  }): {
40
41
  handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
41
42
  sessions: Map<string, any>;
@@ -48,7 +49,7 @@ export function createAuthorRoutes(opts: {
48
49
  * it to sign"), and this is where it is checked, not trusted.
49
50
  * @param {ReturnType<typeof createSession>} session
50
51
  * @param {string} claimedHash
51
- * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string}} o
52
+ * @param {{env: any, spawnFn: typeof realSpawn, bareloopBin: string, sessionsRoot?: string, home?: string}} o
52
53
  * @returns {{ok: boolean, error?: string, job?: string}}
53
54
  */
54
55
  export function signRun(session: ReturnType<typeof createSession>, claimedHash: string, o: {
@@ -56,6 +57,7 @@ export function signRun(session: ReturnType<typeof createSession>, claimedHash:
56
57
  spawnFn: typeof realSpawn;
57
58
  bareloopBin: string;
58
59
  sessionsRoot?: string;
60
+ home?: string;
59
61
  }): {
60
62
  ok: boolean;
61
63
  error?: string;
@@ -16,9 +16,13 @@ export function jobNameTaken(name: string, opts?: {
16
16
  * creates a session or spends anything — never inside the async pipeline,
17
17
  * so a bad card refuses synchronously, in the same HTTP response.
18
18
  * @param {any} card
19
+ * @param {{jobsDir?: string, rows?: readonly import('../providerrows.js').KeyRow[]}} [opts] `rows` = the Settings rows (the Model menu); jobsDir is a server-side test seam — NEVER read off the client card
19
20
  * @returns {{ok: true}|{ok: false, error: string}}
20
21
  */
21
- export function validateJobCard(card: any): {
22
+ export function validateJobCard(card: any, opts?: {
23
+ jobsDir?: string;
24
+ rows?: readonly import("../providerrows.js").KeyRow[];
25
+ }): {
22
26
  ok: true;
23
27
  } | {
24
28
  ok: false;
@@ -30,7 +34,7 @@ export function validateJobCard(card: any): {
30
34
  * is the ONE thing a person answers next; `answer()`/`send()`/`revise()`/
31
35
  * `signPrepare()` are the only ways in.
32
36
  * @param {any} card the validated job card (see {@link validateJobCard})
33
- * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, timeoutMs?: number,
37
+ * @param {{env?: Record<string,string|undefined>, sessionsRoot?: string, home?: string, timeoutMs?: number,
34
38
  * scout?: any, generate?: Function, confirmGenerate?: Function, authorFn?: Function,
35
39
  * prepareSigningFn?: Function}} [deps] the last five are TEST SEAMS ONLY — see the note
36
40
  * just above where each is read, below.
@@ -39,6 +43,7 @@ export function validateJobCard(card: any): {
39
43
  export function createSession(card: any, deps?: {
40
44
  env?: Record<string, string | undefined>;
41
45
  sessionsRoot?: string;
46
+ home?: string;
42
47
  timeoutMs?: number;
43
48
  scout?: any;
44
49
  generate?: Function;
@@ -46,16 +51,3 @@ export function createSession(card: any, deps?: {
46
51
  authorFn?: Function;
47
52
  prepareSigningFn?: Function;
48
53
  }): any;
49
- /**
50
- * The job card's "Model" select — a fixed, small, source-grounded mapping
51
- * from a display id to the `{provider, baseUrl?}` pair `src/interviewrun.js`
52
- * itself documents (that script's own `--provider openai-api --base-url
53
- * https://api.deepseek.com/v1` usage line for DeepSeek): the panel builds no
54
- * provider table of its own, this is a UI convenience over the SAME two
55
- * provider identities the CLI already resolves through `resolveProvider`.
56
- * @type {Readonly<Record<string, {provider: string, baseUrl?: string}>>}
57
- */
58
- export const MODEL_OPTIONS: Readonly<Record<string, {
59
- provider: string;
60
- baseUrl?: string;
61
- }>>;
@@ -211,13 +211,14 @@ export function getRunJob(runid: string, opts?: {
211
211
  * only `createPanelServer` below always supplies one.
212
212
  * @param {import('node:http').IncomingMessage} req
213
213
  * @param {import('node:http').ServerResponse} res
214
- * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes> }} opts
214
+ * @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes> }} opts
215
215
  */
216
216
  export function handleRequest(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, opts: {
217
217
  home?: string;
218
218
  port: number;
219
219
  token?: string;
220
220
  authorRoutes?: ReturnType<typeof createAuthorRoutes>;
221
+ settingsRoutes?: ReturnType<typeof createSettingsRoutes>;
221
222
  }): void;
222
223
  /**
223
224
  * Start the panel server. Binds `127.0.0.1` ONLY. A taken port is a LOUD,
@@ -259,13 +260,17 @@ export function createPanelServer(opts?: {
259
260
  * Never picks a different port on collision (see file header): prints a
260
261
  * loud, named error to stderr and returns 1.
261
262
  * @param {string[]} argv
262
- * @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string }} ctx
263
+ * @param {{ out: (s: string) => void, err: (s: string) => void, runlistHome?: string, env?: Record<string,string|undefined> }} ctx
264
+ * `env` is the RAW shell env (never the start-time merge with the keys file) — the routes
265
+ * re-merge `~/.config/bareloop/.env` onto it per request so Reload keys is real;
266
+ * undefined = the process env.
263
267
  * @returns {Promise<number>}
264
268
  */
265
269
  export function panelMain(argv: string[], ctx: {
266
270
  out: (s: string) => void;
267
271
  err: (s: string) => void;
268
272
  runlistHome?: string;
273
+ env?: Record<string, string | undefined>;
269
274
  }): Promise<number>;
270
275
  /**
271
276
  * A runid, as it appears in a URL path segment — never a raw filesystem
@@ -286,10 +291,12 @@ export const DEFAULT_PORT: 4700;
286
291
  * 2026-09-25).
287
292
  */
288
293
  export const SOFTGREEN_ADMITTED_ISO: "2026-08-18T00:00:00.000Z";
289
- export const DIED_MTIME_MS: number;
294
+ export { DIED_MTIME_MS };
290
295
  /** The default/max page size for {@link getRunRounds} — tighten-only,
291
296
  * named so a future change is a deliberate, visible edit (item 5,
292
297
  * 2026-09-25). */
293
298
  export const ROUNDS_PAGE_DEFAULT: 50;
294
299
  export const ROUNDS_PAGE_MAX: 200;
295
300
  import { createAuthorRoutes } from './authorroutes.js';
301
+ import { createSettingsRoutes } from './settingsroutes.js';
302
+ import { DIED_MTIME_MS } from '../runlist.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @param {{ port: number, token: string, home?: string, env?: Record<string,string|undefined>,
3
+ * fetchImpl?: typeof fetch, now?: () => number }} opts
4
+ */
5
+ export function createSettingsRoutes(opts: {
6
+ port: number;
7
+ token: string;
8
+ home?: string;
9
+ env?: Record<string, string | undefined>;
10
+ fetchImpl?: typeof fetch;
11
+ now?: () => number;
12
+ }): {
13
+ handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
14
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The key value to hand a provider: the `null` marker becomes the placeholder, anything else
3
+ * passes through unchanged.
4
+ * @template {string|undefined} T
5
+ * @param {T} raw
6
+ * @returns {T|string}
7
+ */
8
+ export function usableKey<T extends string | undefined>(raw: T): T | string;
9
+ /**
10
+ * What a key with no saved entry shows. Any name not listed is OpenAI-compatible with a
11
+ * blank URL and a blank Name.
12
+ * @param {string} envName
13
+ * @returns {{ name: string, shape: string, baseUrl: string }}
14
+ */
15
+ export function defaultsFor(envName: string): {
16
+ name: string;
17
+ shape: string;
18
+ baseUrl: string;
19
+ };
20
+ /**
21
+ * @typedef {object} KeyRow
22
+ * @property {string} envName the key variable NAME (read-only label; never a value)
23
+ * @property {string} name the model id Chat uses ('' = not offered in Chat)
24
+ * @property {string} provider the `src/providers.js` table entry (the API shape's id)
25
+ * @property {string} baseUrl the saved Base URL ('' = the shape's default host)
26
+ */
27
+ /**
28
+ * The rows: one per name in `filled`, saved entry over defaults.
29
+ * @param {{ filled: readonly string[], config: Record<string, any> }} src
30
+ * @returns {KeyRow[]}
31
+ */
32
+ export function keyRows({ filled, config }: {
33
+ filled: readonly string[];
34
+ config: Record<string, any>;
35
+ }): KeyRow[];
36
+ /**
37
+ * The rows for a keys home. `undefined` home (a door that skipped the keys file) = no
38
+ * rows, so nothing resolves through the file and the provider's built-in variable stands.
39
+ * @param {string|undefined} home
40
+ * @returns {KeyRow[]}
41
+ */
42
+ export function rowsForHome(home: string | undefined): KeyRow[];
43
+ /**
44
+ * @param {string} provider
45
+ * @returns {string} the shape's default host ('' = the vendor's own)
46
+ */
47
+ export function defaultUrlOf(provider: string): string;
48
+ /**
49
+ * An endpoint in comparable form: blank = the shape's default host; scheme + host lower-cased,
50
+ * trailing slashes dropped.
51
+ * @param {string} provider
52
+ * @param {string|null|undefined} baseUrl
53
+ * @returns {string}
54
+ */
55
+ export function endpointOf(provider: string, baseUrl: string | null | undefined): string;
56
+ /**
57
+ * The URL a row talks to, for display ('' = the vendor's own host, which has no URL to show).
58
+ * @param {KeyRow} row
59
+ * @returns {string}
60
+ */
61
+ export function shownUrl(row: KeyRow): string;
62
+ /**
63
+ * Which row a `(provider, baseUrl[, model])` identity belongs to: the rows on that shape and
64
+ * endpoint; when several share it, the one whose Name is the model, else the first.
65
+ * @param {readonly KeyRow[]} rows
66
+ * @param {{ provider?: string|null, baseUrl?: string|null, model?: string|null }} id
67
+ * @returns {KeyRow|null}
68
+ */
69
+ export function findRow(rows: readonly KeyRow[], { provider, baseUrl, model }: {
70
+ provider?: string | null;
71
+ baseUrl?: string | null;
72
+ model?: string | null;
73
+ }): KeyRow | null;
74
+ /**
75
+ * What Chat's Model menu offers and how a pick resolves: the row whose Name is the model
76
+ * (a blank Name is never offered). `baseUrl` is absent when blank, exactly as a spec omits it.
77
+ * @param {readonly KeyRow[]} rows
78
+ * @param {string} model
79
+ * @returns {{ provider: string, baseUrl?: string, envName: string, name: string }|null}
80
+ */
81
+ export function modelChoiceFor(rows: readonly KeyRow[], model: string): {
82
+ provider: string;
83
+ baseUrl?: string;
84
+ envName: string;
85
+ name: string;
86
+ } | null;
87
+ /**
88
+ * The models Chat offers, in keys-file order.
89
+ * @param {readonly KeyRow[]} rows
90
+ * @returns {{ id: string, envName: string, shape: string }[]}
91
+ */
92
+ export function chatModels(rows: readonly KeyRow[]): {
93
+ id: string;
94
+ envName: string;
95
+ shape: string;
96
+ }[];
97
+ /**
98
+ * The key variable NAME a job on `(provider, baseUrl[, model])` reads: the matching row's
99
+ * variable, else the provider's built-in one (byte-identical to before for anything no row
100
+ * claims).
101
+ * @param {string|null|undefined} provider
102
+ * @param {string|null|undefined} baseUrl
103
+ * @param {readonly KeyRow[]} rows
104
+ * @param {string|null|undefined} [model]
105
+ * @returns {{ name: string, builtIn: string, chosen: boolean }}
106
+ */
107
+ export function keyNameFor(provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): {
108
+ name: string;
109
+ builtIn: string;
110
+ chosen: boolean;
111
+ };
112
+ /**
113
+ * The env a door hands to provider construction, with the matching row's key variable
114
+ * standing in for the provider's built-in one: `env[builtIn] = env[row.envName]` (the `null`
115
+ * no-key marker becomes {@link NO_KEY_PLACEHOLDER}). A copy — never mutates the input.
116
+ * No matching row and no marker = the env unchanged.
117
+ * @param {Record<string,string|undefined>} env
118
+ * @param {string|null|undefined} provider
119
+ * @param {string|null|undefined} baseUrl
120
+ * @param {readonly KeyRow[]} rows
121
+ * @param {string|null|undefined} [model]
122
+ * @returns {Record<string,string|undefined>}
123
+ */
124
+ export function applyConfiguredKey(env: Record<string, string | undefined>, provider: string | null | undefined, baseUrl: string | null | undefined, rows: readonly KeyRow[], model?: string | null | undefined): Record<string, string | undefined>;
125
+ /** DeepSeek's OpenAI-shaped endpoint (the same string the panel's Model menu used). */
126
+ export const DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1";
127
+ /**
128
+ * The API shapes the dropdown offers. `id` is the `src/providers.js` table name;
129
+ * `defaultUrl` is the host a blank Base URL means ('' = the vendor's own, unshown).
130
+ * @type {readonly {id: string, label: string, defaultUrl: string}[]}
131
+ */
132
+ export const SHAPES: readonly {
133
+ id: string;
134
+ label: string;
135
+ defaultUrl: string;
136
+ }[];
137
+ /** The five empty lines a fresh keys file is created with, in this order. */
138
+ export const PRESET_KEY_NAMES: readonly string[];
139
+ /** A local server's default endpoint: Ollama's OpenAI-compatible host (LM Studio / llama.cpp: change the URL). */
140
+ export const LOCAL_BASE_URL: "http://127.0.0.1:11434/v1";
141
+ /**
142
+ * A key line whose value is the word `null` is SET and means "no real key": the row exists and
143
+ * the provider is handed this harmless stand-in (local servers ignore auth). It is not a
144
+ * secret and is never shown or logged as a key.
145
+ */
146
+ export const NO_KEY_PLACEHOLDER: "no-key-needed";
147
+ export type KeyRow = {
148
+ /**
149
+ * the key variable NAME (read-only label; never a value)
150
+ */
151
+ envName: string;
152
+ /**
153
+ * the model id Chat uses ('' = not offered in Chat)
154
+ */
155
+ name: string;
156
+ /**
157
+ * the `src/providers.js` table entry (the API shape's id)
158
+ */
159
+ provider: string;
160
+ /**
161
+ * the saved Base URL ('' = the shape's default host)
162
+ */
163
+ baseUrl: string;
164
+ };