@b3labs/b3iq-cli 0.0.0-stage → 0.0.1
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/AGENTS.md +319 -0
- package/ERROR_PATHS.md +63 -0
- package/LICENSE +202 -0
- package/README.md +207 -2
- package/dist/commands/claude-desktop.js +323 -0
- package/dist/commands/codex-desktop.js +118 -0
- package/dist/commands/commands.js +39 -0
- package/dist/commands/logs.js +41 -0
- package/dist/commands/machines.js +104 -0
- package/dist/commands/media-mcp.js +19 -0
- package/dist/commands/misc.js +257 -0
- package/dist/commands/models.js +141 -0
- package/dist/commands/native-agents.js +291 -0
- package/dist/commands/opencode.js +161 -0
- package/dist/commands/plugins.js +67 -0
- package/dist/commands/runtimes.js +76 -0
- package/dist/commands/ssh.js +156 -0
- package/dist/index.js +194 -0
- package/dist/lib/api.js +149 -0
- package/dist/lib/args.js +35 -0
- package/dist/lib/commands-wait.js +41 -0
- package/dist/lib/config.js +85 -0
- package/dist/lib/desktop-server-lifecycle.js +19 -0
- package/dist/lib/diagnosis.js +678 -0
- package/dist/lib/inference.js +272 -0
- package/dist/lib/machines.js +120 -0
- package/dist/lib/management-probe.js +36 -0
- package/dist/lib/media-download.js +50 -0
- package/dist/lib/output.js +151 -0
- package/dist/lib/ssh-session.js +280 -0
- package/dist/lib/ssh-target.js +28 -0
- package/dist/lib/stdin.js +16 -0
- package/dist/lib/version.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { configPath, resolveSettings } from "./config.js";
|
|
2
|
+
import { CliError } from "./output.js";
|
|
3
|
+
const CONTEXT_MINIMUM = { codex: 32_768, opencode: 32_768, claude: 100_000 };
|
|
4
|
+
export function shellArg(value) {
|
|
5
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6
|
+
}
|
|
7
|
+
export function b3iqLine(parts) {
|
|
8
|
+
return parts.map((part) => (typeof part === "string" ? part : shellArg(part.arg))).join(" ");
|
|
9
|
+
}
|
|
10
|
+
export function withDeadline(work, ms) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(() => reject(Object.assign(new Error("diagnostic lookup timed out"), { name: "DiagnosticTimeout" })), ms);
|
|
13
|
+
work.then((value) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
resolve(value);
|
|
16
|
+
}, (error) => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
reject(error);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function readAccountBestEffort(load) {
|
|
23
|
+
if (!resolveSettings().token)
|
|
24
|
+
return { lookup: "no_token", account: null };
|
|
25
|
+
try {
|
|
26
|
+
const { user } = await load();
|
|
27
|
+
const account = [user.email, user.displayName, user.id].find((value) => typeof value === "string" && value.trim())?.trim() ?? null;
|
|
28
|
+
return { lookup: "ok", account: account ? account.slice(0, 200) : null };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { lookup: "failed", account: null };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function observed(entries) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [key, value] of Object.entries(entries))
|
|
37
|
+
if (value !== undefined)
|
|
38
|
+
out[key] = value;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function diagnosed(message, code, diagnosis, exitCode, requestId) {
|
|
42
|
+
return new CliError(message, code, exitCode, requestId, diagnosis);
|
|
43
|
+
}
|
|
44
|
+
export function explainNotLoggedIn() {
|
|
45
|
+
return diagnosed("Not logged in. Run `b3iq login` or set B3IQ_TOKEN.", "auth", {
|
|
46
|
+
diagnostic: "auth_missing",
|
|
47
|
+
cause: `No token was present in B3IQ_TOKEN or ${configPath()}.`,
|
|
48
|
+
observed: { token_present: false },
|
|
49
|
+
recovery: ["b3iq login", "Set B3IQ_TOKEN to a Machines & SSH API key from the dashboard (Keys, API keys)."],
|
|
50
|
+
limitation: "An inference-only API key does not log this CLI in for machine-management commands.",
|
|
51
|
+
}, 3);
|
|
52
|
+
}
|
|
53
|
+
export function explainNetworkFailure(error, target) {
|
|
54
|
+
const name = error instanceof Error && error.name ? error.name : "Error";
|
|
55
|
+
const timedOut = name === "TimeoutError" || name === "AbortError" || name === "DiagnosticTimeout";
|
|
56
|
+
return diagnosed(timedOut ? `The ${target} request ended before a response (${name}).` : `The ${target} request did not complete (${name}).`, "api", {
|
|
57
|
+
diagnostic: timedOut ? "request_timeout" : "network_failed",
|
|
58
|
+
cause: timedOut
|
|
59
|
+
? "No HTTP status was received. This timeout is the client stopping, not a report that remote work failed."
|
|
60
|
+
: `No HTTP response was received (${name}). This was not an API refusal.`,
|
|
61
|
+
observed: { error_name: name, target },
|
|
62
|
+
recovery: target === "inference gateway"
|
|
63
|
+
? ["Retry the command. A rejected API key returns HTTP 401 or 403, which this failure did not."]
|
|
64
|
+
: ["b3iq doctor", "Retry the command. Check B3IQ_API_URL only if you set one."],
|
|
65
|
+
limitation: "The transport error text is omitted so a URL or credential inside it is not printed.",
|
|
66
|
+
}, 2);
|
|
67
|
+
}
|
|
68
|
+
function safeDetail(detail) {
|
|
69
|
+
if (!detail)
|
|
70
|
+
return undefined;
|
|
71
|
+
const trimmed = detail.replace(/\s+/g, " ").trim();
|
|
72
|
+
if (!trimmed || trimmed.length > 300)
|
|
73
|
+
return undefined;
|
|
74
|
+
return trimmed;
|
|
75
|
+
}
|
|
76
|
+
export function explainAuthFailure(detail, status, requestId, orgId, savedExpiresAt) {
|
|
77
|
+
const text = safeDetail(detail);
|
|
78
|
+
const personalScope = Boolean(text && /personal-scope/i.test(text));
|
|
79
|
+
const shownOrgId = orgId && /^[A-Za-z0-9_-]{1,80}$/.test(orgId) ? orgId : orgId ? "set" : null;
|
|
80
|
+
if (personalScope) {
|
|
81
|
+
return diagnosed("The control plane refused organization scope for this personal key (HTTP 403).", "auth", {
|
|
82
|
+
diagnostic: "org_scope_unsupported",
|
|
83
|
+
cause: "The response said a personal key is personal-scope. B3IQ_ORG_ID or org_id in the CLI config was sent as x-org-id.",
|
|
84
|
+
observed: observed({ http_status: status, org_id_set: Boolean(orgId), org_id: shownOrgId }),
|
|
85
|
+
recovery: [
|
|
86
|
+
`Unset B3IQ_ORG_ID and remove org_id from ${configPath()} if it is set, then retry the personal-account command.`,
|
|
87
|
+
"b3iq whoami",
|
|
88
|
+
"b3iq machines list",
|
|
89
|
+
"For an organization's machines: b3iq login, and choose the organization in the browser",
|
|
90
|
+
],
|
|
91
|
+
limitation: "A personal key cannot take organization scope, and setting an organization id on it will not list organization machines. An organization's machines need a token bound to that organization: run `b3iq login` and choose it on the approval page.",
|
|
92
|
+
}, 3, requestId);
|
|
93
|
+
}
|
|
94
|
+
if (text && /bound to a different organization/i.test(text)) {
|
|
95
|
+
return diagnosed("This CLI's API key is bound to another organization (HTTP 403).", "auth", {
|
|
96
|
+
diagnostic: "org_token_mismatch",
|
|
97
|
+
cause: "The login was approved for one organization and its API key reaches only that one. B3IQ_ORG_ID or org_id in the CLI config named a different one.",
|
|
98
|
+
observed: observed({ http_status: status, org_id_set: Boolean(orgId), org_id: shownOrgId }),
|
|
99
|
+
recovery: ["Unset B3IQ_ORG_ID, then retry.", "b3iq whoami", "b3iq login, and choose the other organization in the browser"],
|
|
100
|
+
limitation: "One login reaches one organization. Sign in again and choose the other one.",
|
|
101
|
+
}, 3, requestId);
|
|
102
|
+
}
|
|
103
|
+
if (status === 401 && text && /CLI tokens were retired/i.test(text)) {
|
|
104
|
+
return diagnosed("This CLI token was retired. Run `b3iq login` to sign in again.", "auth", {
|
|
105
|
+
diagnostic: "token_retired",
|
|
106
|
+
cause: "HTTP 401. CLI tokens (b3iq_pat_) were retired; a login now saves an API key.",
|
|
107
|
+
observed: { http_status: 401 },
|
|
108
|
+
recovery: ["b3iq login", "Set B3IQ_TOKEN to a Machines & SSH API key from the dashboard (Keys, API keys)."],
|
|
109
|
+
limitation: "No CLI token can be renewed. The new login replaces the saved token.",
|
|
110
|
+
}, 3, requestId);
|
|
111
|
+
}
|
|
112
|
+
if (status === 401 && savedExpiresAt && Date.parse(savedExpiresAt) <= Date.now()) {
|
|
113
|
+
return diagnosed(`This login expired on ${savedExpiresAt.slice(0, 10)}. Run \`b3iq login\` to sign in again.`, "auth", {
|
|
114
|
+
diagnostic: "login_expired",
|
|
115
|
+
cause: `HTTP 401. The saved login's API key expired at ${savedExpiresAt}.`,
|
|
116
|
+
observed: { http_status: 401, expired: true },
|
|
117
|
+
recovery: ["b3iq login"],
|
|
118
|
+
limitation: "A login's key lasts 90 days. Logging in again saves a new one.",
|
|
119
|
+
}, 3, requestId);
|
|
120
|
+
}
|
|
121
|
+
if (status === 401) {
|
|
122
|
+
return diagnosed("The control plane rejected this credential (HTTP 401).", "auth", {
|
|
123
|
+
diagnostic: "auth_rejected",
|
|
124
|
+
cause: text
|
|
125
|
+
? `HTTP 401. The control plane detail was: ${text}`
|
|
126
|
+
: "HTTP 401. The token was not accepted. No response body is included.",
|
|
127
|
+
observed: { http_status: 401, detail_included: Boolean(text) },
|
|
128
|
+
recovery: orgId
|
|
129
|
+
? ["b3iq login, and choose the organization again in the browser"]
|
|
130
|
+
: ["b3iq login", "Set B3IQ_TOKEN to a Machines & SSH API key from the dashboard (Keys, API keys)."],
|
|
131
|
+
limitation: orgId
|
|
132
|
+
? "An organization login's API key stops working when it expires (90 days), is revoked, or its creator leaves the organization. An inference-only API key does not authenticate machine-management commands. The response body is not printed."
|
|
133
|
+
: "A login's API key stops working when it expires (90 days) or is revoked. An inference-only API key does not authenticate machine-management commands. The response body is not printed.",
|
|
134
|
+
}, 3, requestId);
|
|
135
|
+
}
|
|
136
|
+
return diagnosed(`The control plane refused this request (HTTP ${status}).`, "auth", {
|
|
137
|
+
diagnostic: "auth_forbidden",
|
|
138
|
+
cause: text
|
|
139
|
+
? `HTTP ${status}. The control plane detail was: ${text}`
|
|
140
|
+
: `HTTP ${status}. No response body is included. This was an authorization refusal, not a usage error.`,
|
|
141
|
+
observed: observed({ http_status: status, org_id_set: Boolean(orgId), detail_included: Boolean(text) }),
|
|
142
|
+
recovery: ["b3iq whoami"],
|
|
143
|
+
limitation: orgId
|
|
144
|
+
? "An organization id is set, but this 403 did not include the personal-scope refusal, so it is not classified as that. Do not retry with a different organization id as a guess."
|
|
145
|
+
: "Do not retry with B3IQ_ORG_ID as a guess. A personal key cannot take organization scope; run `b3iq login` and choose the organization in the browser.",
|
|
146
|
+
}, 3, requestId);
|
|
147
|
+
}
|
|
148
|
+
export function explainApiStatus(detail, status, requestId) {
|
|
149
|
+
const text = safeDetail(detail);
|
|
150
|
+
if (status === 429) {
|
|
151
|
+
return diagnosed("The control plane rate-limited this request (HTTP 429).", "ratelimited", {
|
|
152
|
+
diagnostic: "rate_limited",
|
|
153
|
+
cause: "HTTP 429 after the one automatic retry. No further retry was made.",
|
|
154
|
+
observed: { http_status: 429 },
|
|
155
|
+
recovery: ["Wait, then retry the same command once."],
|
|
156
|
+
limitation: "A 429 does not say the machine, model, or token is wrong.",
|
|
157
|
+
}, 4, requestId);
|
|
158
|
+
}
|
|
159
|
+
return diagnosed(text ? `The control plane returned HTTP ${status}: ${text}` : `The control plane returned HTTP ${status}.`, "api", {
|
|
160
|
+
diagnostic: "api_rejected",
|
|
161
|
+
cause: text
|
|
162
|
+
? `HTTP ${status}. The control plane detail was: ${text}`
|
|
163
|
+
: `HTTP ${status}. The body was not JSON with a detail field, so it is not included.`,
|
|
164
|
+
observed: { http_status: status, detail_included: Boolean(text) },
|
|
165
|
+
recovery: ["Retry the command. If it keeps failing, include request_id when you contact support."],
|
|
166
|
+
limitation: "The raw response body is not printed.",
|
|
167
|
+
}, 2, requestId);
|
|
168
|
+
}
|
|
169
|
+
export function explainGatewayStatus(status, machine, requestId) {
|
|
170
|
+
const auth = status === 401 || status === 403;
|
|
171
|
+
const limited = status === 429;
|
|
172
|
+
if (auth) {
|
|
173
|
+
return diagnosed(`B3IQ inference returned HTTP ${status}.`, auth ? "auth" : "api", {
|
|
174
|
+
diagnostic: "gateway_auth_rejected",
|
|
175
|
+
cause: "The gateway rejected the credential. The response body is not included.",
|
|
176
|
+
observed: observed({ http_status: status, machine: machine ?? null }),
|
|
177
|
+
recovery: [
|
|
178
|
+
"Set B3IQ_GATEWAY_API_KEY, or save a dashboard key with b3iq inference key-save (stdin).",
|
|
179
|
+
"The management token (B3IQ_TOKEN or a `b3iq login`) does not authenticate inference.",
|
|
180
|
+
],
|
|
181
|
+
}, undefined, requestId);
|
|
182
|
+
}
|
|
183
|
+
if (limited) {
|
|
184
|
+
return diagnosed("B3IQ inference returned HTTP 429.", "ratelimited", {
|
|
185
|
+
diagnostic: "gateway_rate_limited",
|
|
186
|
+
cause: "The gateway rate-limited this request. This gateway call was not retried.",
|
|
187
|
+
observed: observed({ http_status: 429, machine: machine ?? null }),
|
|
188
|
+
recovery: ["Wait and retry the same command. A 429 does not say the model or machine is wrong."],
|
|
189
|
+
}, undefined, requestId);
|
|
190
|
+
}
|
|
191
|
+
if (status === 404 && machine) {
|
|
192
|
+
return diagnosed("B3IQ inference returned HTTP 404 for the selected machine.", "api", {
|
|
193
|
+
diagnostic: "gateway_machine_not_found",
|
|
194
|
+
cause: `The gateway returned HTTP 404 for node_id ${JSON.stringify(machine)}. This status does not establish whether the machine exists or whether the key's scope excludes it.`,
|
|
195
|
+
observed: { http_status: 404, machine },
|
|
196
|
+
recovery: ["b3iq inference models", "b3iq machines list"],
|
|
197
|
+
limitation: "b3iq machines list uses the management token, not the inference API key. A machine can answer inference and still be absent from that list.",
|
|
198
|
+
}, undefined, requestId);
|
|
199
|
+
}
|
|
200
|
+
return diagnosed(`B3IQ inference returned HTTP ${status}.`, "api", {
|
|
201
|
+
diagnostic: "gateway_http_error",
|
|
202
|
+
cause: "The gateway returned a non-success status. The response body is not included.",
|
|
203
|
+
observed: observed({ http_status: status, machine: machine ?? null }),
|
|
204
|
+
recovery: ["b3iq inference models"],
|
|
205
|
+
limitation: "This status is not a measured serving-window or tool-calling result.",
|
|
206
|
+
}, undefined, requestId);
|
|
207
|
+
}
|
|
208
|
+
export function explainMachineNotVisible(input) {
|
|
209
|
+
const shown = JSON.stringify(input.selector);
|
|
210
|
+
const checkedRentals = input.rentalsListed !== undefined;
|
|
211
|
+
const accountBit = input.account.lookup === "ok" && input.account.account
|
|
212
|
+
? ` Settings returned account ${input.account.account}.`
|
|
213
|
+
: input.account.lookup === "failed"
|
|
214
|
+
? " The account lookup failed, so this message does not name the account."
|
|
215
|
+
: " No management token was configured, so no account was read.";
|
|
216
|
+
const org = input.orgContext ?? null;
|
|
217
|
+
const listName = org ? `The machine list for organization ${org}` : "The personal machine list";
|
|
218
|
+
const listBit = checkedRentals
|
|
219
|
+
? `${listName} returned ${input.machinesListed} machines and rented guests returned ${input.rentalsListed}. None matched ${shown}.`
|
|
220
|
+
: `${listName} returned ${input.machinesListed} machines and none matched ${shown}. This command does not search rented guests.`;
|
|
221
|
+
return diagnosed(`No machine visible to this CLI account matches ${shown}.`, "usage", {
|
|
222
|
+
diagnostic: "machine_not_visible",
|
|
223
|
+
cause: `${listBit}${accountBit} That result does not establish whether the machine exists.`,
|
|
224
|
+
observed: observed({
|
|
225
|
+
selector: input.selector,
|
|
226
|
+
machines_listed: input.machinesListed,
|
|
227
|
+
rentals_listed: input.rentalsListed,
|
|
228
|
+
account_lookup: input.account.lookup,
|
|
229
|
+
account: input.account.account,
|
|
230
|
+
org_id_set: input.orgIdSet,
|
|
231
|
+
org_context: org,
|
|
232
|
+
}),
|
|
233
|
+
recovery: ["b3iq whoami", "b3iq machines list"],
|
|
234
|
+
limitation: org ? [
|
|
235
|
+
"This CLI's API key is bound to one organization and lists only that organization's machines.",
|
|
236
|
+
"A machine owned by you personally or by another organization is not in it: run `b3iq login` and choose your personal account or that organization in the browser.",
|
|
237
|
+
"A missing match does not establish whether the machine exists.",
|
|
238
|
+
].join(" ") : [
|
|
239
|
+
"If another account manages the machine, ask B3IQ support (hello@b3iq.org) to grant this account owner access.",
|
|
240
|
+
"A missing match does not prove an owner grant is what is missing: the selector may be wrong, or the machine may belong to another account.",
|
|
241
|
+
"Organization ownership alone does not make a machine visible to a personal key. For an organization's machines, run `b3iq login` and choose the organization in the browser.",
|
|
242
|
+
input.orgIdSet
|
|
243
|
+
? `B3IQ_ORG_ID or org_id in ${configPath()} is set. Unset it. A personal key cannot use that selector, and unsetting it does not grant the machine.`
|
|
244
|
+
: "Do not set B3IQ_ORG_ID. The control plane refuses that header on a personal key, and it is not a way to see organization machines.",
|
|
245
|
+
].join(" "),
|
|
246
|
+
}, 1);
|
|
247
|
+
}
|
|
248
|
+
export function explainAmbiguousMachine(selector, matches) {
|
|
249
|
+
const labels = matches.map((match) => `${match.label} (${match.node_id})`).join(", ");
|
|
250
|
+
const recovery = matches.length <= 5
|
|
251
|
+
? ["Pass one full node id.", ...matches.map((match) => b3iqLine(["b3iq machines get", { arg: match.node_id }]))]
|
|
252
|
+
: ["b3iq machines list", "Pass one full node id."];
|
|
253
|
+
return diagnosed(`${JSON.stringify(selector)} matches ${matches.length} machines in this account.`, "usage", {
|
|
254
|
+
diagnostic: "machine_ambiguous",
|
|
255
|
+
cause: `The personal machine list matched ${matches.length} machines: ${labels}.`,
|
|
256
|
+
observed: { selector, match_count: matches.length },
|
|
257
|
+
recovery,
|
|
258
|
+
limitation: "No single machine was selected, so no command was sent.",
|
|
259
|
+
}, 1);
|
|
260
|
+
}
|
|
261
|
+
export function explainMissingMachineSelector() {
|
|
262
|
+
return diagnosed("No machine given and no default set.", "usage", {
|
|
263
|
+
diagnostic: "machine_selector_missing",
|
|
264
|
+
cause: `The command had no machine argument and ${configPath()} has no default_machine.`,
|
|
265
|
+
observed: { selector_present: false, default_machine_present: false },
|
|
266
|
+
recovery: ["b3iq machines list", "b3iq config default-machine <id>"],
|
|
267
|
+
}, 1);
|
|
268
|
+
}
|
|
269
|
+
function nativeFacts(model, client) {
|
|
270
|
+
const profile = model.b3iq;
|
|
271
|
+
const raw = profile?.context_window_tokens;
|
|
272
|
+
let contextKind = "absent";
|
|
273
|
+
if (profile && Object.prototype.hasOwnProperty.call(profile, "context_window_tokens")) {
|
|
274
|
+
if (raw === null)
|
|
275
|
+
contextKind = "null";
|
|
276
|
+
else if (typeof raw === "number" && Number.isSafeInteger(raw))
|
|
277
|
+
contextKind = "integer";
|
|
278
|
+
else
|
|
279
|
+
contextKind = "not_integer";
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
modality: typeof profile?.modality === "string" ? profile.modality : null,
|
|
283
|
+
modalityReported: typeof profile?.modality === "string",
|
|
284
|
+
context: contextKind === "integer" && typeof raw === "number" ? raw : null,
|
|
285
|
+
contextKind,
|
|
286
|
+
toolCalling: profile?.capabilities?.includes("tool_calling") === true,
|
|
287
|
+
capabilitiesReported: Array.isArray(profile?.capabilities),
|
|
288
|
+
inputModalities: Array.isArray(profile?.input_modalities) ? profile.input_modalities.join(",") : null,
|
|
289
|
+
required: CONTEXT_MINIMUM[client],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function baseObserved(model, client, machine, desktop, facts) {
|
|
293
|
+
return observed({
|
|
294
|
+
model_id: model.id,
|
|
295
|
+
client,
|
|
296
|
+
machine: machine ?? null,
|
|
297
|
+
desktop: desktop ?? null,
|
|
298
|
+
modality: facts.modality,
|
|
299
|
+
modality_reported: facts.modalityReported,
|
|
300
|
+
context_window_tokens: facts.context,
|
|
301
|
+
context_kind: facts.contextKind,
|
|
302
|
+
required_context_window_tokens: facts.required,
|
|
303
|
+
tool_calling: facts.toolCalling,
|
|
304
|
+
capabilities_reported: facts.capabilitiesReported,
|
|
305
|
+
input_modalities: facts.inputModalities,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function toolNote(facts) {
|
|
309
|
+
if (!facts.capabilitiesReported)
|
|
310
|
+
return "capabilities was absent, so tool_calling was not advertised.";
|
|
311
|
+
return facts.toolCalling ? "capabilities included tool_calling." : "capabilities was present and did not include tool_calling.";
|
|
312
|
+
}
|
|
313
|
+
function imageNote(facts) {
|
|
314
|
+
if (!facts.inputModalities?.split(",").includes("image"))
|
|
315
|
+
return "";
|
|
316
|
+
return " input_modalities included image. That field is not the modality check.";
|
|
317
|
+
}
|
|
318
|
+
export function assessNativeContext(model, options = {}) {
|
|
319
|
+
const client = options.client ?? "codex";
|
|
320
|
+
const facts = nativeFacts(model, client);
|
|
321
|
+
const fields = baseObserved(model, client, options.machine, options.desktop, facts);
|
|
322
|
+
const shown = JSON.stringify(model.id);
|
|
323
|
+
if (!facts.modalityReported) {
|
|
324
|
+
return { ok: false, error: diagnosed(`Native setup refused ${shown} for ${client}: modality was absent from gateway discovery.`, "usage", {
|
|
325
|
+
diagnostic: "modality_metadata_missing",
|
|
326
|
+
cause: `The served profile had no modality. The ${facts.required}-token window check and the tool_calling check were not applied.${imageNote(facts)}`,
|
|
327
|
+
observed: fields,
|
|
328
|
+
recovery: [inferenceModels(options.machine)],
|
|
329
|
+
limitation: "Native setup does not fill modality from the model name. This is missing metadata, not a measured window.",
|
|
330
|
+
}, 1) };
|
|
331
|
+
}
|
|
332
|
+
if (facts.modality !== "text") {
|
|
333
|
+
return { ok: false, error: diagnosed(`Native setup refused ${shown} for ${client}: modality was ${JSON.stringify(facts.modality)}, not text.`, "usage", {
|
|
334
|
+
diagnostic: "modality_unsupported",
|
|
335
|
+
cause: `Gateway discovery reported modality ${JSON.stringify(facts.modality)}. Native setup accepts text only.${imageNote(facts)} ${toolNote(facts)}`,
|
|
336
|
+
observed: fields,
|
|
337
|
+
recovery: [inferenceModels(options.machine)],
|
|
338
|
+
limitation: "b3iq models context does not change modality. This refusal is the modality check, not a serving-window check.",
|
|
339
|
+
}, 1) };
|
|
340
|
+
}
|
|
341
|
+
if (facts.contextKind !== "integer") {
|
|
342
|
+
const why = facts.contextKind === "absent"
|
|
343
|
+
? "context_window_tokens was absent"
|
|
344
|
+
: facts.contextKind === "null"
|
|
345
|
+
? "context_window_tokens was null"
|
|
346
|
+
: "context_window_tokens was not a safe integer";
|
|
347
|
+
return { ok: false, error: diagnosed(`Native setup refused ${shown} for ${client}: ${why}. This is missing metadata, not a window under ${facts.required}.`, "usage", {
|
|
348
|
+
diagnostic: "context_metadata_missing",
|
|
349
|
+
cause: `Gateway discovery reported modality text. ${why}. ${toolNote(facts)}${imageNote(facts)} The ${facts.required}-token minimum was not applied because there is no integer window to compare.`,
|
|
350
|
+
observed: fields,
|
|
351
|
+
recovery: [inferenceModels(options.machine)],
|
|
352
|
+
limitation: "Native setup does not substitute a model-card maximum. Discovery can lag a context command that already finished; this reading is only the current gateway report, and it is not labeled stale.",
|
|
353
|
+
}, 1) };
|
|
354
|
+
}
|
|
355
|
+
if (facts.context !== null && facts.context < facts.required) {
|
|
356
|
+
const also = facts.toolCalling ? "" : ` ${toolNote(facts)}`;
|
|
357
|
+
return { ok: false, error: diagnosed(`Native setup refused ${shown} for ${client}: the verified profile window is ${facts.context} tokens, below the ${facts.required} tokens ${client} requires.`, "usage", {
|
|
358
|
+
diagnostic: "context_window_insufficient",
|
|
359
|
+
cause: `Gateway discovery reported modality text, context_window_tokens ${facts.context}, and ${facts.toolCalling ? "tool_calling" : "no tool_calling"}. ${client} requires at least ${facts.required}.${imageNote(facts)}${also} Native setup does not change the serving window.`,
|
|
360
|
+
observed: fields,
|
|
361
|
+
recovery: [inferenceModels(options.machine)],
|
|
362
|
+
limitation: "The reported integer is below the minimum. It is not missing metadata and it is not labeled stale. Discovery can still lag a context command that already finished; compare b3iq inference models before treating this number as the saved window.",
|
|
363
|
+
}, 1) };
|
|
364
|
+
}
|
|
365
|
+
if (!facts.toolCalling) {
|
|
366
|
+
return { ok: false, error: diagnosed(`Native setup refused ${shown} for ${client}: the live profile did not advertise tool_calling.`, "usage", {
|
|
367
|
+
diagnostic: "tool_calling_missing",
|
|
368
|
+
cause: `Gateway discovery reported modality text and context_window_tokens ${facts.context}, which meets ${facts.required}. ${toolNote(facts)}${imageNote(facts)} A text window alone does not establish coding-tool support.`,
|
|
369
|
+
observed: fields,
|
|
370
|
+
recovery: [inferenceModels(options.machine)],
|
|
371
|
+
limitation: "b3iq models context changes the serving window. It does not add tool_calling. Native setup does not install another model.",
|
|
372
|
+
}, 1) };
|
|
373
|
+
}
|
|
374
|
+
return { ok: true, context: facts.context };
|
|
375
|
+
}
|
|
376
|
+
function inferenceModels(machine) {
|
|
377
|
+
return machine ? b3iqLine(["b3iq inference models --machine", { arg: machine }]) : "b3iq inference models";
|
|
378
|
+
}
|
|
379
|
+
function setupLine(client, model, machine, desktop) {
|
|
380
|
+
const parts = ["b3iq inference setup", client];
|
|
381
|
+
if (desktop)
|
|
382
|
+
parts.push("--desktop");
|
|
383
|
+
parts.push("--model", { arg: model });
|
|
384
|
+
if (machine)
|
|
385
|
+
parts.push("--machine", { arg: machine });
|
|
386
|
+
return b3iqLine(parts);
|
|
387
|
+
}
|
|
388
|
+
function contextCommand(model, machine) {
|
|
389
|
+
return b3iqLine(["b3iq models context", { arg: model }, "--machine", { arg: machine }]);
|
|
390
|
+
}
|
|
391
|
+
export function withManagementGuidance(error, model, options, management) {
|
|
392
|
+
const diagnostic = error.diagnosis?.diagnostic;
|
|
393
|
+
if (diagnostic !== "context_window_insufficient" && diagnostic !== "context_metadata_missing")
|
|
394
|
+
return error;
|
|
395
|
+
const client = options.client ?? "codex";
|
|
396
|
+
const machine = options.machine;
|
|
397
|
+
const account = management.account_lookup === "ok" && management.account ? ` Settings returned account ${management.account}.` : "";
|
|
398
|
+
const later = `After the window is in discovery, rerun ${setupLine(client, model.id, machine, options.desktop)}. This check did not record --install or --activate; add those only if that was the command you ran.`;
|
|
399
|
+
let recovery;
|
|
400
|
+
let limitation;
|
|
401
|
+
if (!management.checked && management.reason === "no_management_token") {
|
|
402
|
+
recovery = ["b3iq login", "b3iq whoami", "b3iq machines list"];
|
|
403
|
+
limitation = `No management token was configured, so machine visibility was not listed.${account} b3iq models context needs that token and a machine this account can manage. The API key used for setup cannot run it. Do not set B3IQ_ORG_ID on a personal token. ${later}`;
|
|
404
|
+
}
|
|
405
|
+
else if (!management.checked && management.reason === "org_scope_unsupported") {
|
|
406
|
+
recovery = [`Unset B3IQ_ORG_ID and org_id in ${configPath()}, then retry.`, "b3iq whoami", "b3iq machines list"];
|
|
407
|
+
limitation = `The management lookup was refused because a personal key cannot take an organization id.${account} Unsetting it restores personal scope. It does not grant organization machines; run \`b3iq login\` and choose the organization in the browser. ${later}`;
|
|
408
|
+
}
|
|
409
|
+
else if (!management.checked) {
|
|
410
|
+
recovery = ["b3iq whoami", "b3iq machines list"];
|
|
411
|
+
limitation = `The management lookup did not complete, so this message does not say whether the machine is visible and does not recommend b3iq models context yet. The gateway result above is unchanged.${account}`;
|
|
412
|
+
}
|
|
413
|
+
else if (!machine) {
|
|
414
|
+
recovery = ["b3iq machines list", "b3iq inference models"];
|
|
415
|
+
limitation = `No --machine was passed, so a specific machine was not looked up.${account} b3iq models context needs a machine from that list. Native setup will not run it. ${later}`;
|
|
416
|
+
}
|
|
417
|
+
else if (management.machine_ambiguous) {
|
|
418
|
+
recovery = ["b3iq machines list"];
|
|
419
|
+
limitation = `The selector matched more than one machine in this account (${management.machines_listed ?? "unknown"} listed).${account} Pass a full node id before b3iq models context. No context command was sent.`;
|
|
420
|
+
}
|
|
421
|
+
else if (management.machine_visible) {
|
|
422
|
+
recovery = [contextCommand(model.id, machine), inferenceModels(machine)];
|
|
423
|
+
limitation = `The personal machine list included exactly one match. The context command below is how to set the window. Native setup does not run it. It uses the management token, not the API key. Gateway discovery can lag after that command completes; rerun inference models before setup. ${later}`;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
recovery = ["b3iq whoami", "b3iq machines list"];
|
|
427
|
+
limitation = [
|
|
428
|
+
`The personal machine list (${management.machines_listed ?? 0} machines) did not include ${JSON.stringify(machine)}, so this message does not recommend b3iq models context as the next command.${account}`,
|
|
429
|
+
"That does not establish whether the machine exists.",
|
|
430
|
+
"If another account manages it, ask B3IQ support (hello@b3iq.org) to grant this account owner access.",
|
|
431
|
+
"A missing match does not prove an owner grant is what is missing.",
|
|
432
|
+
"Organization ownership alone is not enough for a personal token. Do not set B3IQ_ORG_ID: run `b3iq login` and choose the organization in the browser.",
|
|
433
|
+
later,
|
|
434
|
+
].join(" ");
|
|
435
|
+
}
|
|
436
|
+
const prior = error.diagnosis;
|
|
437
|
+
return new CliError(error.message, error.code, error.exitCode, error.requestId, {
|
|
438
|
+
diagnostic: prior?.diagnostic ?? "context_window_insufficient",
|
|
439
|
+
cause: prior?.cause ?? error.message,
|
|
440
|
+
observed: observed({
|
|
441
|
+
...prior?.observed,
|
|
442
|
+
management_checked: management.checked,
|
|
443
|
+
management_reason: management.reason ?? (management.checked ? "listed" : null),
|
|
444
|
+
org_id_set: management.org_id_set,
|
|
445
|
+
account_lookup: management.account_lookup,
|
|
446
|
+
account: management.account,
|
|
447
|
+
machines_listed: management.machines_listed,
|
|
448
|
+
machine_visible: management.machine_visible,
|
|
449
|
+
machine_ambiguous: management.machine_ambiguous,
|
|
450
|
+
}),
|
|
451
|
+
recovery,
|
|
452
|
+
limitation,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
export function requireContext(model, options = {}) {
|
|
456
|
+
const result = assessNativeContext(model, options);
|
|
457
|
+
if (!result.ok) {
|
|
458
|
+
if (options.management && (result.error.diagnosis?.diagnostic === "context_window_insufficient" || result.error.diagnosis?.diagnostic === "context_metadata_missing")) {
|
|
459
|
+
throw withManagementGuidance(result.error, model, options, options.management);
|
|
460
|
+
}
|
|
461
|
+
throw result.error;
|
|
462
|
+
}
|
|
463
|
+
return result.context;
|
|
464
|
+
}
|
|
465
|
+
export function explainModelNotServed(modelId, discovered, machine) {
|
|
466
|
+
return diagnosed(`That exact model ID is not in this gateway discovery response (${discovered} model(s) returned).`, "usage", {
|
|
467
|
+
diagnostic: "model_not_served",
|
|
468
|
+
cause: `${JSON.stringify(modelId)} was not among the ${discovered} id(s) returned for this API key${machine ? ` with node_id ${JSON.stringify(machine)}` : ""}. This does not establish whether the model is installed on a machine.`,
|
|
469
|
+
observed: observed({ model_id: modelId, models_returned: discovered, machine: machine ?? null }),
|
|
470
|
+
recovery: [inferenceModels(machine)],
|
|
471
|
+
limitation: "Discovery uses the API key. b3iq models list uses the management token and only sees machines that account can manage. This check did not call that list.",
|
|
472
|
+
}, 1);
|
|
473
|
+
}
|
|
474
|
+
export function explainProfileRefusal(input) {
|
|
475
|
+
const offline = input.liveness === "offline";
|
|
476
|
+
const offlineBit = offline ? ` The machine list reported liveness offline for ${input.nodeId}.` : "";
|
|
477
|
+
if (input.matches.length === 0) {
|
|
478
|
+
return diagnosed(`No model matches ${JSON.stringify(input.selector)} on ${input.nodeId}.`, "usage", {
|
|
479
|
+
diagnostic: offline && input.catalogSize === 0 ? "model_catalog_offline" : "model_not_on_machine",
|
|
480
|
+
cause: input.catalogSize === 0
|
|
481
|
+
? `The machine catalog returned no profiles.${offlineBit} This does not establish that the model id is wrong.`
|
|
482
|
+
: `The catalog had ${input.catalogSize} profile(s) and none matched ${JSON.stringify(input.selector)}.${offlineBit}`,
|
|
483
|
+
observed: observed({
|
|
484
|
+
selector: input.selector,
|
|
485
|
+
catalog_size: input.catalogSize,
|
|
486
|
+
node_id: input.nodeId,
|
|
487
|
+
liveness: input.liveness ?? null,
|
|
488
|
+
}),
|
|
489
|
+
recovery: [b3iqLine(["b3iq models list --machine", { arg: input.nodeId }]), b3iqLine(["b3iq machines get", { arg: input.nodeId }])],
|
|
490
|
+
limitation: offline
|
|
491
|
+
? "No install, removal, or context command was sent. An offline catalog is not a failed install. Do not submit a duplicate until machines get shows the machine online and models list shows the profile."
|
|
492
|
+
: "No command was sent. The catalog match is the only check this error used.",
|
|
493
|
+
}, 1);
|
|
494
|
+
}
|
|
495
|
+
const eligible = input.matches.filter((profile) => profile.status === input.wantStatus);
|
|
496
|
+
if (eligible.length > 1) {
|
|
497
|
+
return diagnosed(`${JSON.stringify(input.selector)} matches ${eligible.length} ${input.wantStatus} profiles.`, "usage", {
|
|
498
|
+
diagnostic: "model_ambiguous",
|
|
499
|
+
cause: `Eligible profile ids: ${eligible.map((profile) => profile.id ?? profile.model ?? "unknown").join(", ")}.`,
|
|
500
|
+
observed: { selector: input.selector, match_count: eligible.length, node_id: input.nodeId },
|
|
501
|
+
recovery: ["Pass the full profile id."],
|
|
502
|
+
limitation: "No command was sent.",
|
|
503
|
+
}, 1);
|
|
504
|
+
}
|
|
505
|
+
const statuses = [...new Set(input.matches.map((profile) => profile.status ?? "unknown"))];
|
|
506
|
+
const status = statuses[0] ?? "unknown";
|
|
507
|
+
const recovery = profileRecovery(status, input.wantStatus, input.selector, input.nodeId);
|
|
508
|
+
return diagnosed(`${JSON.stringify(input.selector)} is ${statuses.join(", ")} on ${input.nodeId}, so it cannot ${input.verb}.`, "usage", {
|
|
509
|
+
diagnostic: profileDiagnostic(status, input.wantStatus),
|
|
510
|
+
cause: `The catalog status was ${statuses.join(", ")}. This command accepts ${input.wantStatus}.${offlineBit}`,
|
|
511
|
+
observed: observed({
|
|
512
|
+
selector: input.selector,
|
|
513
|
+
status: statuses.join(","),
|
|
514
|
+
want_status: input.wantStatus,
|
|
515
|
+
node_id: input.nodeId,
|
|
516
|
+
liveness: input.liveness ?? null,
|
|
517
|
+
match_count: input.matches.length,
|
|
518
|
+
}),
|
|
519
|
+
recovery,
|
|
520
|
+
limitation: offline
|
|
521
|
+
? "No command was sent. The machine was reported offline as well as the catalog status above."
|
|
522
|
+
: statusLimitation(status, input.wantStatus),
|
|
523
|
+
}, 1);
|
|
524
|
+
}
|
|
525
|
+
function profileDiagnostic(status, want) {
|
|
526
|
+
if (status === "runtime_missing")
|
|
527
|
+
return "runtime_missing";
|
|
528
|
+
if (status === "hardware_unsupported")
|
|
529
|
+
return "hardware_unsupported";
|
|
530
|
+
if (status === "modality_unsupported")
|
|
531
|
+
return "modality_unsupported";
|
|
532
|
+
if (want === "installed" && status === "ready_to_install")
|
|
533
|
+
return "model_not_installed";
|
|
534
|
+
if (want === "ready_to_install" && status === "installed")
|
|
535
|
+
return "model_already_installed";
|
|
536
|
+
return "model_not_ready";
|
|
537
|
+
}
|
|
538
|
+
function profileRecovery(status, want, selector, nodeId) {
|
|
539
|
+
const list = b3iqLine(["b3iq models list --machine", { arg: nodeId }]);
|
|
540
|
+
if (want === "installed" && status === "ready_to_install") {
|
|
541
|
+
return [b3iqLine(["b3iq models install", { arg: selector }, "--machine", { arg: nodeId }]), list];
|
|
542
|
+
}
|
|
543
|
+
if (status === "runtime_missing")
|
|
544
|
+
return [b3iqLine(["b3iq runtimes list --machine", { arg: nodeId }]), list];
|
|
545
|
+
if (status === "installed")
|
|
546
|
+
return [list, "The profile is already installed, so install is the wrong command."];
|
|
547
|
+
return [list, b3iqLine(["b3iq machines get", { arg: nodeId }])];
|
|
548
|
+
}
|
|
549
|
+
function statusLimitation(status, want) {
|
|
550
|
+
if (status === "hardware_unsupported")
|
|
551
|
+
return "No command in this CLI changes the machine's hardware. The catalog status is hardware_unsupported.";
|
|
552
|
+
if (status === "modality_unsupported")
|
|
553
|
+
return "The catalog status is modality_unsupported. This CLI did not choose a different model.";
|
|
554
|
+
if (status === "runtime_missing")
|
|
555
|
+
return "The catalog status is runtime_missing. This CLI did not install a runtime as a side effect.";
|
|
556
|
+
if (want === "installed" && status !== "installed")
|
|
557
|
+
return "Context and removal accept an installed profile only. Nothing was queued.";
|
|
558
|
+
return "Nothing was queued. The catalog status is the only readiness fact this error used.";
|
|
559
|
+
}
|
|
560
|
+
export function explainRuntimeMissing(selector, nodeId, label) {
|
|
561
|
+
return diagnosed(`No runtime ${JSON.stringify(selector)} on ${label}.`, "usage", {
|
|
562
|
+
diagnostic: "runtime_not_on_machine",
|
|
563
|
+
cause: `The machine's runtime list had no id or kind equal to ${JSON.stringify(selector)}.`,
|
|
564
|
+
observed: { selector, node_id: nodeId },
|
|
565
|
+
recovery: [b3iqLine(["b3iq runtimes list --machine", { arg: nodeId }])],
|
|
566
|
+
limitation: "No restart was sent.",
|
|
567
|
+
}, 1);
|
|
568
|
+
}
|
|
569
|
+
export function explainCommandTimeout(input) {
|
|
570
|
+
const waited = input.timeoutMs >= 60_000 ? `${Math.round(input.timeoutMs / 60_000)} min` : `${Math.round(input.timeoutMs / 1000)} s`;
|
|
571
|
+
const saw = input.lastStatus !== undefined;
|
|
572
|
+
const lookup = b3iqLine(["b3iq commands get", { arg: input.commandId }, "--machine", { arg: input.nodeId }]);
|
|
573
|
+
return diagnosed(saw
|
|
574
|
+
? `${input.describe} was still ${input.lastStatus} when this CLI stopped waiting (${waited}).`
|
|
575
|
+
: `${input.describe} produced no command status before this CLI stopped waiting (${waited}).`, "api", {
|
|
576
|
+
diagnostic: "command_timeout",
|
|
577
|
+
command_id: input.commandId,
|
|
578
|
+
cause: saw
|
|
579
|
+
? `The last observed status was ${JSON.stringify(input.lastStatus)}. It was not completed, failed, or cancelled.`
|
|
580
|
+
: "No row for this command id was observed before the deadline. The wait ending is not a report that the work failed.",
|
|
581
|
+
observed: {
|
|
582
|
+
command_id: input.commandId,
|
|
583
|
+
node_id: input.nodeId,
|
|
584
|
+
last_status: input.lastStatus ?? null,
|
|
585
|
+
timeout_ms: input.timeoutMs,
|
|
586
|
+
status_observed: saw,
|
|
587
|
+
},
|
|
588
|
+
recovery: [lookup],
|
|
589
|
+
limitation: "The work may still be queued or running. Do not submit the same install, upgrade, or context change again until that status is completed, failed, or cancelled.",
|
|
590
|
+
}, 2);
|
|
591
|
+
}
|
|
592
|
+
export function explainCommandEnded(describe, status, serverError, commandId, nodeId) {
|
|
593
|
+
const detail = serverError?.trim() ? serverError.trim().replace(/\s+/g, " ").slice(0, 300) : "";
|
|
594
|
+
const lookup = b3iqLine(["b3iq commands get", { arg: commandId }, "--machine", { arg: nodeId }]);
|
|
595
|
+
const headline = status === "cancelled"
|
|
596
|
+
? `${describe} was cancelled${detail ? `: ${detail}` : ""}`
|
|
597
|
+
: `${describe} failed${detail ? `: ${detail}` : ""}`;
|
|
598
|
+
return diagnosed(headline, "api", {
|
|
599
|
+
diagnostic: status === "failed" ? "command_failed" : "command_cancelled",
|
|
600
|
+
command_id: commandId,
|
|
601
|
+
cause: detail
|
|
602
|
+
? `The command status was ${status}. The control plane included this error text: ${detail}`
|
|
603
|
+
: `The command status was ${status}. No error text was included.`,
|
|
604
|
+
observed: { command_id: commandId, node_id: nodeId, status },
|
|
605
|
+
recovery: [lookup],
|
|
606
|
+
limitation: status === "failed"
|
|
607
|
+
? "Failed means this command did not complete. Do not start a duplicate until you have read that status. This CLI did not add a cause beyond the status and error text it received."
|
|
608
|
+
: "Cancelled means the command was cancelled. Read it before submitting another.",
|
|
609
|
+
}, 2);
|
|
610
|
+
}
|
|
611
|
+
export function explainSshLane(nodeId, enabled, hostnamePresent) {
|
|
612
|
+
return diagnosed(`SSH is not enabled for ${nodeId}.`, "usage", {
|
|
613
|
+
diagnostic: "ssh_lane_disabled",
|
|
614
|
+
cause: `Machine detail reported ssh_lane.enabled=${enabled} and hostname ${hostnamePresent ? "present" : "absent"}.`,
|
|
615
|
+
observed: { node_id: nodeId, ssh_lane_enabled: enabled, ssh_hostname_present: hostnamePresent },
|
|
616
|
+
recovery: ["Enable the SSH lane on the machine's dashboard page, then retry b3iq ssh."],
|
|
617
|
+
limitation: "This CLI does not enable the lane. The hostname value is not printed.",
|
|
618
|
+
}, 1);
|
|
619
|
+
}
|
|
620
|
+
export function explainDirectSshNotEnabled(feature) {
|
|
621
|
+
return diagnosed("Direct SSH is not turned on for this account.", "usage", {
|
|
622
|
+
diagnostic: "ssh_direct_not_enabled",
|
|
623
|
+
cause: `--direct was given and /v1/public/features did not include ${feature}. It answers a b3iq login's API key as signed out, so a per-account grant does not reach the CLI yet.`,
|
|
624
|
+
observed: { feature, feature_on: false },
|
|
625
|
+
recovery: ["b3iq ssh <machine>", "b3iq ssh <machine> --tunnel"],
|
|
626
|
+
limitation: "No ssh process was started.",
|
|
627
|
+
}, 1);
|
|
628
|
+
}
|
|
629
|
+
export function explainNoDirectAddress(nodeId, guest, addressesTried = 0) {
|
|
630
|
+
const message = guest
|
|
631
|
+
? "A rented guest is only reachable over its host's Cloudflare Tunnel."
|
|
632
|
+
: addressesTried > 0
|
|
633
|
+
? `None of the ${addressesTried} direct address${addressesTried === 1 ? "" : "es"} ${nodeId} reported answered with sshd.`
|
|
634
|
+
: `${nodeId} has not reported an address you can SSH to directly.`;
|
|
635
|
+
return diagnosed(message, "usage", {
|
|
636
|
+
diagnostic: guest ? "ssh_direct_not_for_guests" : addressesTried > 0 ? "ssh_direct_unreachable" : "ssh_direct_unavailable",
|
|
637
|
+
cause: guest
|
|
638
|
+
? "--direct was given for a rented guest."
|
|
639
|
+
: addressesTried > 0
|
|
640
|
+
? "--direct was given and no reported address completed a TCP connect with an SSH banner."
|
|
641
|
+
: "--direct was given and machine detail had no ssh_direct addresses.",
|
|
642
|
+
observed: { node_id: nodeId, direct_addresses_tried: addressesTried },
|
|
643
|
+
recovery: [`b3iq ssh ${shellArg(nodeId)}`],
|
|
644
|
+
limitation: "No ssh process was started. Without --direct, b3iq ssh uses the Cloudflare Tunnel.",
|
|
645
|
+
}, 1);
|
|
646
|
+
}
|
|
647
|
+
export function explainSshMissing() {
|
|
648
|
+
return diagnosed("ssh is required to reach this machine, and it was not found on PATH.", "usage", {
|
|
649
|
+
diagnostic: "ssh_missing",
|
|
650
|
+
cause: "Starting `ssh` failed with ENOENT.",
|
|
651
|
+
observed: { ssh_on_path: false },
|
|
652
|
+
recovery: ["Install an OpenSSH client (it ships with macOS and most Linux distributions)", "b3iq doctor"],
|
|
653
|
+
limitation: "No ssh process was started.",
|
|
654
|
+
}, 1);
|
|
655
|
+
}
|
|
656
|
+
export function explainCloudflaredMissing() {
|
|
657
|
+
return diagnosed("cloudflared is required to reach this machine, and it was not found on PATH.", "usage", {
|
|
658
|
+
diagnostic: "cloudflared_missing",
|
|
659
|
+
cause: "`cloudflared --version` did not exit 0.",
|
|
660
|
+
observed: { cloudflared_on_path: false },
|
|
661
|
+
recovery: ["brew install cloudflared", "b3iq doctor"],
|
|
662
|
+
limitation: "No ssh process was started.",
|
|
663
|
+
}, 1);
|
|
664
|
+
}
|
|
665
|
+
export function contextSavedLines(model, nodeId, label, result) {
|
|
666
|
+
const record = result && typeof result === "object" ? result : {};
|
|
667
|
+
const verified = record.verified === true;
|
|
668
|
+
const tokens = typeof record.context_window_tokens === "number" ? record.context_window_tokens : null;
|
|
669
|
+
const lines = [
|
|
670
|
+
verified
|
|
671
|
+
? `Context verified and saved for ${model} on ${label}${tokens !== null ? ` (${tokens} tokens)` : ""}.`
|
|
672
|
+
: `The context command completed for ${model} on ${label}, but the result did not report verified: true. Read the result before setup.`,
|
|
673
|
+
"Gateway discovery can lag this save.",
|
|
674
|
+
`Check: ${b3iqLine(["b3iq inference models --machine", { arg: nodeId }])}`,
|
|
675
|
+
"Native setup does not apply the window. Rerun your setup command after discovery shows the verified window.",
|
|
676
|
+
];
|
|
677
|
+
return lines;
|
|
678
|
+
}
|