byollm 0.1.0-alpha.63 → 0.1.0-alpha.65
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/README.md +2 -2
- package/dist/bin.js +1 -1
- package/dist/{chunk-J4GXTVXX.js → chunk-HM5G6ZUG.js} +378 -27
- package/dist/chunk-HM5G6ZUG.js.map +1 -0
- package/dist/index.d.ts +89 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-J4GXTVXX.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.65`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npx byollm@alpha`, or `npm install byollm@alpha`.
|
|
5
5
|
>
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
> packages published and `@byollm/server` did not: a Sigstore
|
|
86
86
|
> transparency-log 409 on its provenance attestation. The workflow's
|
|
87
87
|
> "already published" guard correctly refuses to resume a partial publish,
|
|
88
|
-
> so `0.1.0-alpha.
|
|
88
|
+
> so `0.1.0-alpha.65` is that release, whole.
|
|
89
89
|
>
|
|
90
90
|
> If you run the Supabase adapter, `alpha.21` needs
|
|
91
91
|
> `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
|
package/dist/bin.js
CHANGED
|
@@ -51,7 +51,7 @@ function asHttpOrigin(candidate) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// src/cli.ts
|
|
54
|
-
import { mkdir as mkdir9, readFile as
|
|
54
|
+
import { mkdir as mkdir9, readFile as readFile10, rm as rm5, stat as stat3, writeFile as writeFile10 } from "fs/promises";
|
|
55
55
|
import { hostname, userInfo } from "os";
|
|
56
56
|
import { dirname as dirname10 } from "path";
|
|
57
57
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
@@ -229,7 +229,15 @@ ${stderr}`);
|
|
|
229
229
|
finish({
|
|
230
230
|
ok: false,
|
|
231
231
|
code: authFailed ? "unauthorized" : "backend-error",
|
|
232
|
-
message: authFailed ? `${displayName} is not signed in` : stderr.trim()
|
|
232
|
+
message: authFailed ? `${displayName} is not signed in` : stderr.trim() !== "" ? `${displayName} failed: ${firstLine(stderr)}` : (
|
|
233
|
+
/* Nothing on stderr does not mean nothing was said. An
|
|
234
|
+
agentic CLI writes its errors to stdout — that is where
|
|
235
|
+
"Failed to authenticate" arrived — and reporting a bare
|
|
236
|
+
exit code while holding the explanation is the one thing a
|
|
237
|
+
diagnostic must not do. No new exposure: stdout already
|
|
238
|
+
goes to the site on success. */
|
|
239
|
+
stdout.trim() !== "" ? `${displayName} failed: ${firstLine(stdout)}` : `${displayName} exited with status ${String(code)}`
|
|
240
|
+
),
|
|
233
241
|
retryable: false,
|
|
234
242
|
durationMs
|
|
235
243
|
});
|
|
@@ -256,7 +264,31 @@ var AUTH_FAILURE = [
|
|
|
256
264
|
/\bnot authenticated\b/,
|
|
257
265
|
/\bauthentication failed\b/,
|
|
258
266
|
/\binvalid api key\b/,
|
|
259
|
-
/\b401 unauthorized\b
|
|
267
|
+
/\b401 unauthorized\b/,
|
|
268
|
+
/**
|
|
269
|
+
* The expiry family — Todd's Mac, 2026-08-31, and the reason this list grew.
|
|
270
|
+
*
|
|
271
|
+
* The CLI said "Failed to authenticate. API Error: 401 OAuth access token
|
|
272
|
+
* has expired. Re-authenticate to continue." and not one pattern above
|
|
273
|
+
* matched it: the list had "authentication failed" and the CLI wrote the
|
|
274
|
+
* same two words in the other order, and it had "401 unauthorized" against
|
|
275
|
+
* a 401 that named OAuth instead. So a signed-out backend was reported as
|
|
276
|
+
* `backend-error`, the service was not withdrawn, and the sentence that
|
|
277
|
+
* reached the person was "the claude CLI exited with status 1".
|
|
278
|
+
*
|
|
279
|
+
* An expired token is the *common* case in a subscription CLI — it is what
|
|
280
|
+
* happens to everybody eventually, on a machine that was working
|
|
281
|
+
* yesterday — and it was the one the corpus lacked.
|
|
282
|
+
*
|
|
283
|
+
* "failed to authenticate" was in this list for about a minute. The test
|
|
284
|
+
* below refused it: "the guard had failed to authenticate the visitor's
|
|
285
|
+
* papers" is a sentence a model can write, and matching it would withdraw a
|
|
286
|
+
* service that works. The two that remain are auth machinery talking about
|
|
287
|
+
* itself, which prose has little reason to imitate — and Todd's message
|
|
288
|
+
* contained both, so nothing was lost by dropping the loose one.
|
|
289
|
+
*/
|
|
290
|
+
/\baccess token has expired\b/,
|
|
291
|
+
/\bre-?authenticate\b/
|
|
260
292
|
];
|
|
261
293
|
function isAuthFailure(output) {
|
|
262
294
|
const text = output.toLowerCase();
|
|
@@ -368,6 +400,7 @@ function resolveUncached(binary, platform2, source, npmPackage) {
|
|
|
368
400
|
var ClaudeCliBackend = class {
|
|
369
401
|
id = "claude-cli";
|
|
370
402
|
class = "process";
|
|
403
|
+
signIn = "run `claude` in a terminal";
|
|
371
404
|
#binary;
|
|
372
405
|
/**
|
|
373
406
|
* @param binary - which executable to run. Defaults to `claude` and is
|
|
@@ -479,6 +512,7 @@ function codexArgv(model) {
|
|
|
479
512
|
var CodexCliBackend = class {
|
|
480
513
|
id = "codex-cli";
|
|
481
514
|
class = "process";
|
|
515
|
+
signIn = "run `codex login`";
|
|
482
516
|
#binary;
|
|
483
517
|
/**
|
|
484
518
|
* @param binary - which executable to run. Defaults to `codex` and is **not
|
|
@@ -1235,6 +1269,30 @@ var BOTH_KINDS = Object.freeze([
|
|
|
1235
1269
|
"llm.generate",
|
|
1236
1270
|
"llm.chat"
|
|
1237
1271
|
]);
|
|
1272
|
+
async function detect(id, model) {
|
|
1273
|
+
try {
|
|
1274
|
+
const backend = createBackend(id, {});
|
|
1275
|
+
const health = await backend.health();
|
|
1276
|
+
if (!health.healthy) {
|
|
1277
|
+
return {
|
|
1278
|
+
installed: false,
|
|
1279
|
+
answers: void 0,
|
|
1280
|
+
...health.detail === void 0 ? {} : { detail: health.detail }
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (backend.canary === void 0) {
|
|
1284
|
+
return { installed: true, answers: void 0 };
|
|
1285
|
+
}
|
|
1286
|
+
const proof = await backend.canary(model);
|
|
1287
|
+
return {
|
|
1288
|
+
installed: true,
|
|
1289
|
+
answers: proof.healthy,
|
|
1290
|
+
...proof.detail === void 0 ? {} : { detail: proof.detail }
|
|
1291
|
+
};
|
|
1292
|
+
} catch {
|
|
1293
|
+
return { installed: false, answers: void 0 };
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1238
1296
|
async function detectInstalled(id) {
|
|
1239
1297
|
try {
|
|
1240
1298
|
const backend = createBackend(id, {});
|
|
@@ -1249,7 +1307,7 @@ var yes = (answer, fallback) => {
|
|
|
1249
1307
|
if (text === "") return fallback;
|
|
1250
1308
|
return /^y(es)?$/.test(text);
|
|
1251
1309
|
};
|
|
1252
|
-
async function runSetup(paths, io, detector = detectInstalled, probe = () => probeLocalServers()) {
|
|
1310
|
+
async function runSetup(paths, io, detector = detectInstalled, probe = () => probeLocalServers(), verifier = detect) {
|
|
1253
1311
|
if (!io.interactive) {
|
|
1254
1312
|
io.err(
|
|
1255
1313
|
"byollm setup needs a terminal it can ask questions in.\nWrite ~/.byollm/config.json by hand instead: https://docs.byollm.cloud/guides/models\n"
|
|
@@ -1258,13 +1316,27 @@ async function runSetup(paths, io, detector = detectInstalled, probe = () => pro
|
|
|
1258
1316
|
}
|
|
1259
1317
|
const existing = await readExisting(paths.config);
|
|
1260
1318
|
if (existing !== void 0) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1319
|
+
const count = Object.keys(existing.services).length;
|
|
1320
|
+
if (count > 0) {
|
|
1321
|
+
io.out(
|
|
1322
|
+
`You already have a config at ${paths.config}.
|
|
1323
|
+
It has ${String(count)} service(s). Setup will not change it.
|
|
1264
1324
|
Run \`byollm services\` to see what it does, or edit that file.
|
|
1325
|
+
`
|
|
1326
|
+
);
|
|
1327
|
+
return { wrote: false, services: [] };
|
|
1328
|
+
}
|
|
1329
|
+
io.out(
|
|
1330
|
+
`
|
|
1331
|
+
Your config at ${paths.config} has no services in it, so nothing
|
|
1332
|
+
can run yet. That is how versions before alpha.44 left it.
|
|
1265
1333
|
`
|
|
1266
1334
|
);
|
|
1267
|
-
|
|
1335
|
+
const go = await io.ask(" Set it up now? [Y/n] ");
|
|
1336
|
+
if (!yes(go, true)) {
|
|
1337
|
+
io.out(" Left alone. Nothing was changed.\n");
|
|
1338
|
+
return { wrote: false, services: [] };
|
|
1339
|
+
}
|
|
1268
1340
|
}
|
|
1269
1341
|
io.out(`
|
|
1270
1342
|
Setting up byollm. Change any of it later in ${paths.config}.
|
|
@@ -1283,6 +1355,16 @@ Setting up byollm. Change any of it later in ${paths.config}.
|
|
|
1283
1355
|
io.out(`
|
|
1284
1356
|
Found the \`${cli.binary}\` CLI.
|
|
1285
1357
|
`);
|
|
1358
|
+
const proof = await verifier(cli.id, cli.model);
|
|
1359
|
+
if (proof.answers === false) {
|
|
1360
|
+
io.out(
|
|
1361
|
+
` It is installed but cannot answer yet \u2014 it needs signing in.
|
|
1362
|
+
Run \`${cli.binary}\` in a terminal, sign in, then rerun this.
|
|
1363
|
+
Setting it up anyway; nothing will route to it until it can
|
|
1364
|
+
answer, and \`byollm status\` will keep saying so.
|
|
1365
|
+
`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1286
1368
|
io.out(
|
|
1287
1369
|
` Uses your ${cli.plan}, for YOUR OWN jobs only \u2014 never shared with a
|
|
1288
1370
|
team, whatever the config says. Someone else's terms are not yours
|
|
@@ -2705,6 +2787,7 @@ function daemonPaths(root = defaultRoot()) {
|
|
|
2705
2787
|
ingressLog: join3(root, "ingress.log"),
|
|
2706
2788
|
budgets: join3(root, "budgets.json"),
|
|
2707
2789
|
spend: join3(root, "spend.json"),
|
|
2790
|
+
serviceStates: join3(root, "services.json"),
|
|
2708
2791
|
spentGrants: join3(root, "spent-grants.json"),
|
|
2709
2792
|
keys: join3(root, "keys.json"),
|
|
2710
2793
|
pauseFlag: join3(root, "paused"),
|
|
@@ -2717,6 +2800,39 @@ function defaultRoot() {
|
|
|
2717
2800
|
return process.env["BYOLLM_HOME"] ?? join3(homedir(), ".byollm");
|
|
2718
2801
|
}
|
|
2719
2802
|
|
|
2803
|
+
// src/site-outcome.ts
|
|
2804
|
+
var SERVICE_UNAVAILABLE = "a device service is unavailable \u2014 its owner has been told";
|
|
2805
|
+
var FOR_SITE = Object.freeze({
|
|
2806
|
+
"backend-unreachable": {
|
|
2807
|
+
code: "service_unavailable",
|
|
2808
|
+
message: SERVICE_UNAVAILABLE
|
|
2809
|
+
},
|
|
2810
|
+
"backend-error": {
|
|
2811
|
+
code: "service_unavailable",
|
|
2812
|
+
message: SERVICE_UNAVAILABLE
|
|
2813
|
+
},
|
|
2814
|
+
"model-not-found": {
|
|
2815
|
+
code: "service_unavailable",
|
|
2816
|
+
message: SERVICE_UNAVAILABLE
|
|
2817
|
+
},
|
|
2818
|
+
unauthorized: {
|
|
2819
|
+
code: "service_unavailable",
|
|
2820
|
+
message: SERVICE_UNAVAILABLE
|
|
2821
|
+
},
|
|
2822
|
+
timeout: {
|
|
2823
|
+
code: "timeout",
|
|
2824
|
+
message: "the device did not answer in time"
|
|
2825
|
+
},
|
|
2826
|
+
"output-too-large": {
|
|
2827
|
+
code: "output-too-large",
|
|
2828
|
+
message: "the answer was too large to return"
|
|
2829
|
+
},
|
|
2830
|
+
canceled: { code: "canceled", message: "the job was canceled" }
|
|
2831
|
+
});
|
|
2832
|
+
function outcomeForSite(code) {
|
|
2833
|
+
return FOR_SITE[code];
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2720
2836
|
// src/runner.ts
|
|
2721
2837
|
import {
|
|
2722
2838
|
RETIREMENT_WINDOW_MS,
|
|
@@ -2915,7 +3031,15 @@ var Runner = class {
|
|
|
2915
3031
|
* daemon then receives no work for it, which is the correct outcome and one
|
|
2916
3032
|
* the owner can see in `byollm status`.
|
|
2917
3033
|
*/
|
|
3034
|
+
/**
|
|
3035
|
+
* What the last probe learned about each service, for the owner's surfaces.
|
|
3036
|
+
*
|
|
3037
|
+
* Read by `byollm status`, by `byollm connect`'s report and by the daemon's
|
|
3038
|
+
* own output — one answer, three renderings, through `serviceLine`.
|
|
3039
|
+
*/
|
|
3040
|
+
serviceStates = /* @__PURE__ */ new Map();
|
|
2918
3041
|
async detectCapabilities(options = {}) {
|
|
3042
|
+
this.serviceStates = /* @__PURE__ */ new Map();
|
|
2919
3043
|
const capabilities = [];
|
|
2920
3044
|
const probed = /* @__PURE__ */ new Map();
|
|
2921
3045
|
for (const route of this.#options.loaded.routes) {
|
|
@@ -2925,9 +3049,33 @@ var Runner = class {
|
|
|
2925
3049
|
const backend = this.#backendFor(route);
|
|
2926
3050
|
const health = await backend.health();
|
|
2927
3051
|
usable = health.healthy && (health.models.length === 0 || modelPresent(health.models, route.model));
|
|
3052
|
+
const remedy = backend.signIn === void 0 ? {} : { signIn: backend.signIn };
|
|
3053
|
+
if (!health.healthy) {
|
|
3054
|
+
this.serviceStates.set(route.service, {
|
|
3055
|
+
state: { kind: "missing" },
|
|
3056
|
+
...remedy
|
|
3057
|
+
});
|
|
3058
|
+
} else if (options.canary !== true || backend.canary === void 0) {
|
|
3059
|
+
this.serviceStates.set(route.service, {
|
|
3060
|
+
state: { kind: "unknown", model: route.model },
|
|
3061
|
+
...remedy
|
|
3062
|
+
});
|
|
3063
|
+
} else {
|
|
3064
|
+
this.serviceStates.set(route.service, {
|
|
3065
|
+
state: { kind: "answers", model: route.model },
|
|
3066
|
+
...remedy
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
2928
3069
|
if (usable && options.canary === true && backend.canary !== void 0) {
|
|
2929
3070
|
const proof = await backend.canary(route.model);
|
|
2930
3071
|
if (!proof.healthy) {
|
|
3072
|
+
this.serviceStates.set(route.service, {
|
|
3073
|
+
state: {
|
|
3074
|
+
kind: "signed-out",
|
|
3075
|
+
...proof.detail === void 0 ? {} : { detail: proof.detail }
|
|
3076
|
+
},
|
|
3077
|
+
...remedy
|
|
3078
|
+
});
|
|
2931
3079
|
usable = false;
|
|
2932
3080
|
this.#unauthenticated.add(route.service);
|
|
2933
3081
|
this.#options.onEvent?.({
|
|
@@ -3320,12 +3468,34 @@ var Runner = class {
|
|
|
3320
3468
|
detail: result.message
|
|
3321
3469
|
});
|
|
3322
3470
|
}
|
|
3323
|
-
const outcome = result.ok ? { outcome: "ok", text: result.text } : result.code === "canceled" ? { outcome: "canceled" } :
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3471
|
+
const outcome = result.ok ? { outcome: "ok", text: result.text } : result.code === "canceled" ? { outcome: "canceled" } : (
|
|
3472
|
+
/**
|
|
3473
|
+
* The class, never the text — a8137b5.
|
|
3474
|
+
*
|
|
3475
|
+
* `result.message` is the CLI's own words and stays with the
|
|
3476
|
+
* owner: the event above, the ingress log below, `byollm status`
|
|
3477
|
+
* and Your Devices. It does not travel, for two reasons.
|
|
3478
|
+
*
|
|
3479
|
+
* It quotes the owner's machine — paths, usernames, config
|
|
3480
|
+
* locations, account emails all live in CLI errors, and a
|
|
3481
|
+
* stranger's page is not where those go.
|
|
3482
|
+
*
|
|
3483
|
+
* And it names the service. "the claude CLI is not signed in"
|
|
3484
|
+
* tells a site which model answered, which is the one thing the
|
|
3485
|
+
* disclosure fence exists to prevent — arriving through the error
|
|
3486
|
+
* path because nobody was watching the error path. Every message
|
|
3487
|
+
* named its backend, so every failure leaked what every success
|
|
3488
|
+
* is careful to hide.
|
|
3489
|
+
*
|
|
3490
|
+
* `retryable` still travels: whether to try again is the site's
|
|
3491
|
+
* decision and says nothing about whose machine it was.
|
|
3492
|
+
*/
|
|
3493
|
+
{
|
|
3494
|
+
outcome: "error",
|
|
3495
|
+
...outcomeForSite(result.code),
|
|
3496
|
+
retryable: result.retryable
|
|
3497
|
+
}
|
|
3498
|
+
);
|
|
3329
3499
|
await this.#options.ingress.recordOutcome({
|
|
3330
3500
|
at: this.#now(),
|
|
3331
3501
|
jobId: job.id,
|
|
@@ -4154,7 +4324,37 @@ WantedBy=default.target
|
|
|
4154
4324
|
deactivate: [["schtasks", "/delete", "/tn", SERVICE_LABEL, "/f"]],
|
|
4155
4325
|
query: ["schtasks", "/query", "/tn", SERVICE_LABEL],
|
|
4156
4326
|
logPath,
|
|
4157
|
-
supervisor: "Task Scheduler"
|
|
4327
|
+
supervisor: "Task Scheduler",
|
|
4328
|
+
/**
|
|
4329
|
+
* The Startup folder, for a machine that will not register a task.
|
|
4330
|
+
*
|
|
4331
|
+
* A `.cmd` here runs at logon for this user, needs no elevation and no
|
|
4332
|
+
* policy exemption, and cannot be refused. What it does not do is restart
|
|
4333
|
+
* byollm if it stops — which is the whole reason the task XML exists — so
|
|
4334
|
+
* that is said out loud rather than left for somebody to discover when a
|
|
4335
|
+
* crash goes unnoticed.
|
|
4336
|
+
*
|
|
4337
|
+
* `start ""` so the shim exits immediately and the logon does not wait on
|
|
4338
|
+
* a long-running process; the empty title is required because `start`
|
|
4339
|
+
* reads a first quoted argument as the window title.
|
|
4340
|
+
*/
|
|
4341
|
+
fallback: {
|
|
4342
|
+
unitPath: join4(
|
|
4343
|
+
process.env["APPDATA"] ?? root,
|
|
4344
|
+
"Microsoft",
|
|
4345
|
+
"Windows",
|
|
4346
|
+
"Start Menu",
|
|
4347
|
+
"Programs",
|
|
4348
|
+
"Startup",
|
|
4349
|
+
"byollm.cmd"
|
|
4350
|
+
),
|
|
4351
|
+
unitContents: `@echo off\r
|
|
4352
|
+
rem byollm \u2014 run an app's LLM jobs on your own models\r
|
|
4353
|
+
start "" /b "${execPath}" "${scriptPath}" run >> "${logPath}" 2>&1\r
|
|
4354
|
+
`,
|
|
4355
|
+
supervisor: "the Startup folder",
|
|
4356
|
+
caveat: "It starts byollm when you log in. It does not restart it if it stops \u2014 Task Scheduler would have, and this machine would not register the task."
|
|
4357
|
+
}
|
|
4158
4358
|
};
|
|
4159
4359
|
}
|
|
4160
4360
|
async function serviceIsInstalled(target) {
|
|
@@ -4201,6 +4401,11 @@ async function serviceState(plan, run2) {
|
|
|
4201
4401
|
if (/\bRunning\b/.test(result.output)) return { state: "running" };
|
|
4202
4402
|
return { state: "installed", detail: "registered, not running" };
|
|
4203
4403
|
}
|
|
4404
|
+
function refusalOf(command, result) {
|
|
4405
|
+
const said = result.output.trim();
|
|
4406
|
+
const denied = /access is denied|requires elevation|0x80070005/i.test(said);
|
|
4407
|
+
return denied ? `${command.join(" ")} \u2014 this machine will not let you register a scheduled task (no administrator rights, or IT policy).` : `${command.join(" ")} \u2014 exit ${String(result.code)}` + (said === "" ? "" : `: ${said}`);
|
|
4408
|
+
}
|
|
4204
4409
|
async function installService(target, run2) {
|
|
4205
4410
|
const plan = servicePlan(target);
|
|
4206
4411
|
const refusal = refuseToSupervise(target.scriptPath);
|
|
@@ -4213,16 +4418,46 @@ async function installService(target, run2) {
|
|
|
4213
4418
|
const result = await run2(command);
|
|
4214
4419
|
const mayFail = plan.platform === "darwin" && index === 0;
|
|
4215
4420
|
if (result.code !== 0 && !mayFail) {
|
|
4421
|
+
const fallback = plan.fallback;
|
|
4422
|
+
if (fallback !== void 0) {
|
|
4423
|
+
const fell = await mkdir8(dirname9(fallback.unitPath), {
|
|
4424
|
+
recursive: true
|
|
4425
|
+
}).then(
|
|
4426
|
+
() => writeFile8(fallback.unitPath, fallback.unitContents, "utf8")
|
|
4427
|
+
).then(
|
|
4428
|
+
() => true,
|
|
4429
|
+
() => false
|
|
4430
|
+
);
|
|
4431
|
+
if (fell) {
|
|
4432
|
+
return {
|
|
4433
|
+
ok: true,
|
|
4434
|
+
plan,
|
|
4435
|
+
lines: [
|
|
4436
|
+
`${plan.supervisor} would not register the task, so byollm is`,
|
|
4437
|
+
`set to start from ${fallback.supervisor} instead.`,
|
|
4438
|
+
"",
|
|
4439
|
+
` ${fallback.caveat}`,
|
|
4440
|
+
"",
|
|
4441
|
+
` ${refusalOf(command, result)}`,
|
|
4442
|
+
"",
|
|
4443
|
+
` startup: ${fallback.unitPath}`,
|
|
4444
|
+
` log: ${plan.logPath}`,
|
|
4445
|
+
` check: byollm status`,
|
|
4446
|
+
` remove: byollm uninstall`
|
|
4447
|
+
]
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4216
4451
|
return {
|
|
4217
4452
|
ok: false,
|
|
4218
4453
|
plan,
|
|
4219
4454
|
lines: [
|
|
4220
4455
|
`wrote ${plan.unitPath}, but ${plan.supervisor} refused it:`,
|
|
4221
4456
|
"",
|
|
4222
|
-
` ${command
|
|
4223
|
-
` exit ${String(result.code)}${result.output.trim() === "" ? "" : `: ${result.output.trim()}`}`,
|
|
4457
|
+
` ${refusalOf(command, result)}`,
|
|
4224
4458
|
"",
|
|
4225
|
-
`The daemon is not supervised. \`byollm run\` still works in a terminal
|
|
4459
|
+
`The daemon is not supervised. \`byollm run\` still works in a terminal,`,
|
|
4460
|
+
`and is the way to keep serving until this is sorted.`
|
|
4226
4461
|
]
|
|
4227
4462
|
};
|
|
4228
4463
|
}
|
|
@@ -4290,6 +4525,95 @@ var spawnCommand = async (command) => {
|
|
|
4290
4525
|
});
|
|
4291
4526
|
};
|
|
4292
4527
|
|
|
4528
|
+
// src/service-line.ts
|
|
4529
|
+
function serviceLine(input) {
|
|
4530
|
+
const { service, device, state } = input;
|
|
4531
|
+
switch (state.kind) {
|
|
4532
|
+
case "answers":
|
|
4533
|
+
return { line: `${service} \u2014 ${state.model}` };
|
|
4534
|
+
case "unknown":
|
|
4535
|
+
return { line: `${service} \u2014 ${state.model}` };
|
|
4536
|
+
case "signed-out": {
|
|
4537
|
+
const remedy = input.signIn ?? "sign it in";
|
|
4538
|
+
return {
|
|
4539
|
+
line: `${service} \u2014 needs sign-in on ${device}: ${remedy}`,
|
|
4540
|
+
...state.detail === void 0 ? {} : { detail: state.detail }
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
case "missing": {
|
|
4544
|
+
const remove = input.removeWith ?? `remove it from ~/.byollm/config.json`;
|
|
4545
|
+
return {
|
|
4546
|
+
line: `${service} \u2014 not found on ${device}: install it, or ${remove}`
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
function renderServices(states, device) {
|
|
4552
|
+
const lines = [];
|
|
4553
|
+
for (const [service, report2] of states) {
|
|
4554
|
+
const said = serviceLine({
|
|
4555
|
+
service,
|
|
4556
|
+
device,
|
|
4557
|
+
state: report2.state,
|
|
4558
|
+
...report2.signIn === void 0 ? {} : { signIn: report2.signIn }
|
|
4559
|
+
});
|
|
4560
|
+
lines.push(` ${said.line}`);
|
|
4561
|
+
if (said.detail !== void 0) lines.push(` ${said.detail}`);
|
|
4562
|
+
}
|
|
4563
|
+
return lines;
|
|
4564
|
+
}
|
|
4565
|
+
function authNote(input) {
|
|
4566
|
+
const { report: report2 } = input;
|
|
4567
|
+
if (report2 === void 0) return void 0;
|
|
4568
|
+
if (report2.state.kind === "answers") return void 0;
|
|
4569
|
+
if (report2.state.kind === "unknown") return void 0;
|
|
4570
|
+
return serviceLine({
|
|
4571
|
+
service: input.service,
|
|
4572
|
+
device: input.device,
|
|
4573
|
+
state: report2.state,
|
|
4574
|
+
...report2.signIn === void 0 ? {} : { signIn: report2.signIn }
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
// src/service-states.ts
|
|
4579
|
+
import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
|
|
4580
|
+
import { z as z7 } from "zod";
|
|
4581
|
+
var Stored = z7.record(
|
|
4582
|
+
z7.string(),
|
|
4583
|
+
z7.object({
|
|
4584
|
+
state: z7.discriminatedUnion("kind", [
|
|
4585
|
+
z7.object({ kind: z7.literal("answers"), model: z7.string() }),
|
|
4586
|
+
z7.object({
|
|
4587
|
+
kind: z7.literal("signed-out"),
|
|
4588
|
+
detail: z7.string().optional()
|
|
4589
|
+
}),
|
|
4590
|
+
z7.object({ kind: z7.literal("missing") }),
|
|
4591
|
+
z7.object({ kind: z7.literal("unknown"), model: z7.string() })
|
|
4592
|
+
]),
|
|
4593
|
+
signIn: z7.string().optional()
|
|
4594
|
+
})
|
|
4595
|
+
);
|
|
4596
|
+
async function writeServiceStates(path, states) {
|
|
4597
|
+
const out = {};
|
|
4598
|
+
for (const [service, report2] of states) out[service] = report2;
|
|
4599
|
+
try {
|
|
4600
|
+
await writeFile9(path, `${JSON.stringify(out, null, 2)}
|
|
4601
|
+
`, "utf8");
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
async function readServiceStates(path) {
|
|
4606
|
+
try {
|
|
4607
|
+
const parsed = Stored.safeParse(
|
|
4608
|
+
JSON.parse(await readFile9(path, "utf8"))
|
|
4609
|
+
);
|
|
4610
|
+
if (!parsed.success) return /* @__PURE__ */ new Map();
|
|
4611
|
+
return new Map(Object.entries(parsed.data));
|
|
4612
|
+
} catch {
|
|
4613
|
+
return /* @__PURE__ */ new Map();
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4293
4617
|
// src/cli.ts
|
|
4294
4618
|
var USAGE = `byollm \u2014 run an app's LLM jobs on your own models.
|
|
4295
4619
|
|
|
@@ -4435,7 +4759,7 @@ async function commandName(paths, args, io) {
|
|
|
4435
4759
|
return 0;
|
|
4436
4760
|
}
|
|
4437
4761
|
await mkdir9(dirname10(paths.label), { recursive: true });
|
|
4438
|
-
await
|
|
4762
|
+
await writeFile10(paths.label, `${next.slice(0, 120)}
|
|
4439
4763
|
`, { mode: 384 });
|
|
4440
4764
|
io.out(
|
|
4441
4765
|
`This device will pair as ${next.slice(0, 120)}.
|
|
@@ -4455,7 +4779,7 @@ function connectTarget(args) {
|
|
|
4455
4779
|
async function labelFor(paths, flag) {
|
|
4456
4780
|
if (flag !== void 0 && flag !== "") return flag.slice(0, 120);
|
|
4457
4781
|
try {
|
|
4458
|
-
const saved = (await
|
|
4782
|
+
const saved = (await readFile10(paths.label, "utf8")).trim();
|
|
4459
4783
|
if (saved !== "") return saved.slice(0, 120);
|
|
4460
4784
|
} catch {
|
|
4461
4785
|
}
|
|
@@ -4524,9 +4848,21 @@ ${wrap(
|
|
|
4524
4848
|
ingress
|
|
4525
4849
|
});
|
|
4526
4850
|
const capabilities = await runner.detectCapabilities({ canary: true });
|
|
4851
|
+
await writeServiceStates(paths.serviceStates, runner.serviceStates);
|
|
4852
|
+
{
|
|
4853
|
+
const lines = renderServices(
|
|
4854
|
+
runner.serviceStates,
|
|
4855
|
+
await labelFor(paths, name)
|
|
4856
|
+
);
|
|
4857
|
+
if (lines.length > 0) io.out(`
|
|
4858
|
+
services
|
|
4859
|
+
${lines.join("\n")}
|
|
4860
|
+
`);
|
|
4861
|
+
}
|
|
4862
|
+
await writeServiceStates(paths.serviceStates, runner.serviceStates);
|
|
4527
4863
|
if (capabilities.length === 0) {
|
|
4528
4864
|
io.err(
|
|
4529
|
-
"\
|
|
4865
|
+
"\nNothing will route to this device yet.\nPairing anyway \u2014 work starts arriving on its own once a service can\nanswer. `byollm status` says where each one stands.\n"
|
|
4530
4866
|
);
|
|
4531
4867
|
}
|
|
4532
4868
|
io.out(`
|
|
@@ -4634,7 +4970,7 @@ function reportSkipped(pairings, io) {
|
|
|
4634
4970
|
async function retireAllowlist(paths, io) {
|
|
4635
4971
|
let raw;
|
|
4636
4972
|
try {
|
|
4637
|
-
raw = await
|
|
4973
|
+
raw = await readFile10(paths.allowlist, "utf8");
|
|
4638
4974
|
} catch {
|
|
4639
4975
|
return;
|
|
4640
4976
|
}
|
|
@@ -4931,6 +5267,8 @@ async function commandStatus(paths, io, service) {
|
|
|
4931
5267
|
byService.set(route.service, entry);
|
|
4932
5268
|
}
|
|
4933
5269
|
io.out("\nservices\n");
|
|
5270
|
+
const recorded = await readServiceStates(paths.serviceStates);
|
|
5271
|
+
const deviceName = await labelFor(paths, void 0);
|
|
4934
5272
|
const declared = Object.entries(loaded.config.services);
|
|
4935
5273
|
if (declared.length === 0) {
|
|
4936
5274
|
io.out(" (none configured)\n");
|
|
@@ -4945,6 +5283,11 @@ async function commandStatus(paths, io, service) {
|
|
|
4945
5283
|
hasRelay
|
|
4946
5284
|
});
|
|
4947
5285
|
const scope = `${summary.scope} (${summary.audience})` + (summary.narrowedBy === void 0 ? "" : ` \u2014 ${summary.narrowedBy}`);
|
|
5286
|
+
const authLine = authNote({
|
|
5287
|
+
service: name,
|
|
5288
|
+
device: deviceName,
|
|
5289
|
+
report: recorded.get(name)
|
|
5290
|
+
});
|
|
4948
5291
|
io.out(` ${name}
|
|
4949
5292
|
`);
|
|
4950
5293
|
io.out(` ${shown}
|
|
@@ -4960,6 +5303,14 @@ async function commandStatus(paths, io, service) {
|
|
|
4960
5303
|
` ${scope} \xB7 ${says.length === 0 ? "not offered \u2014 see the problems below" : says.join(" \xB7 ")}
|
|
4961
5304
|
`
|
|
4962
5305
|
);
|
|
5306
|
+
if (authLine !== void 0) {
|
|
5307
|
+
io.out(` ${authLine.line}
|
|
5308
|
+
`);
|
|
5309
|
+
if (authLine.detail !== void 0) {
|
|
5310
|
+
io.out(` ${authLine.detail}
|
|
5311
|
+
`);
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
4963
5314
|
}
|
|
4964
5315
|
for (const held of loaded.withheld) {
|
|
4965
5316
|
io.out(
|
|
@@ -5082,7 +5433,7 @@ function indent(text) {
|
|
|
5082
5433
|
async function commandPause(paths, pause, io) {
|
|
5083
5434
|
await mkdir9(paths.root, { recursive: true });
|
|
5084
5435
|
if (pause) {
|
|
5085
|
-
await
|
|
5436
|
+
await writeFile10(paths.pauseFlag, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5086
5437
|
`);
|
|
5087
5438
|
io.out(
|
|
5088
5439
|
"paused \u2014 no new work will be claimed. `byollm resume` to start again.\n"
|
|
@@ -5178,7 +5529,7 @@ async function commandOffer(paths, args, io) {
|
|
|
5178
5529
|
}
|
|
5179
5530
|
let raw;
|
|
5180
5531
|
try {
|
|
5181
|
-
raw = await
|
|
5532
|
+
raw = await readFile10(paths.config, "utf8");
|
|
5182
5533
|
} catch {
|
|
5183
5534
|
io.err(
|
|
5184
5535
|
`no config at ${paths.config} \u2014 nothing to offer yet.
|
|
@@ -5281,7 +5632,7 @@ Offer ${serviceKey} to your team?`
|
|
|
5281
5632
|
};
|
|
5282
5633
|
}
|
|
5283
5634
|
await mkdir9(dirname10(paths.config), { recursive: true });
|
|
5284
|
-
await
|
|
5635
|
+
await writeFile10(paths.config, `${JSON.stringify(result.data, null, 2)}
|
|
5285
5636
|
`);
|
|
5286
5637
|
const written = result.data.services[serviceKey].spend?.dailyCapCents;
|
|
5287
5638
|
io.out(
|
|
@@ -5491,7 +5842,7 @@ Addresses look like https://app.example.com or localhost:8080.
|
|
|
5491
5842
|
}
|
|
5492
5843
|
|
|
5493
5844
|
// src/index.ts
|
|
5494
|
-
var DAEMON_VERSION = "0.1.0-alpha.
|
|
5845
|
+
var DAEMON_VERSION = "0.1.0-alpha.65";
|
|
5495
5846
|
function daemonVersion() {
|
|
5496
5847
|
return {
|
|
5497
5848
|
daemon: DAEMON_VERSION,
|
|
@@ -5543,4 +5894,4 @@ export {
|
|
|
5543
5894
|
runCli,
|
|
5544
5895
|
main
|
|
5545
5896
|
};
|
|
5546
|
-
//# sourceMappingURL=chunk-
|
|
5897
|
+
//# sourceMappingURL=chunk-HM5G6ZUG.js.map
|