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.
- package/CHANGELOG.md +72 -0
- package/bareloop.context.md +87 -13
- package/bin/bareloop.mjs +0 -1
- package/package.json +1 -1
- package/src/authorrun.js +14 -4
- package/src/cli.js +12 -6
- package/src/config.js +135 -0
- package/src/interviewrun.js +10 -2
- package/src/keysfile.js +156 -0
- package/src/ledger.js +26 -0
- package/src/monthly.js +352 -0
- package/src/panel/authorroutes.js +61 -16
- package/src/panel/authorsession.js +23 -25
- package/src/panel/index.html +287 -13
- package/src/panel/server.js +58 -51
- package/src/panel/settingsroutes.js +230 -0
- package/src/providerrows.js +217 -0
- package/src/providers.js +3 -1
- package/src/runlist.js +13 -0
- package/src/userrun.js +59 -5
- package/types/authorrun.d.ts +5 -0
- package/types/cli.d.ts +2 -1
- package/types/config.d.ts +48 -0
- package/types/interviewrun.d.ts +5 -0
- package/types/keysfile.d.ts +90 -0
- package/types/ledger.d.ts +14 -0
- package/types/monthly.d.ts +192 -0
- package/types/panel/authorroutes.d.ts +4 -2
- package/types/panel/authorsession.d.ts +7 -15
- package/types/panel/server.d.ts +10 -3
- package/types/panel/settingsroutes.d.ts +14 -0
- package/types/providerrows.d.ts +164 -0
- package/types/runlist.d.ts +1 -0
- package/types/userrun.d.ts +5 -0
|
@@ -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
|
+
};
|
package/types/interviewrun.d.ts
CHANGED
|
@@ -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
|
-
}>>;
|
package/types/panel/server.d.ts
CHANGED
|
@@ -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
|
|
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
|
+
};
|