@bitkyc08/opencodex 2.49.0 → 2.51.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/errors.ts +8 -0
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/src/cli/hub.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { OcxConfig, OcxConnectedClientId } from "../types";
|
|
2
|
+
import { canonicalGuiBrowserOrigin, canonicalHttpOrigin } from "../lib/gui-pair-capability";
|
|
3
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
4
|
+
import {
|
|
5
|
+
requestBoundGuiPairingGrant,
|
|
6
|
+
type GuiPairClientDeps,
|
|
7
|
+
type GuiPairRequestResult,
|
|
8
|
+
} from "./gui-pair-client";
|
|
9
|
+
import type { RuntimeApiDeps } from "./runtime-api";
|
|
10
|
+
|
|
11
|
+
export const HUB_USAGE =
|
|
12
|
+
"ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]";
|
|
13
|
+
|
|
14
|
+
const PAIRING_WARNING = "Pairing codes are secret, single-use, and expire quickly. Do not save them.";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The default browser origin a connecting machine presents.
|
|
18
|
+
*
|
|
19
|
+
* `ocx connect --pairing-code-stdin` exchanges the code with `Origin:` set by `localGuiOrigin()`
|
|
20
|
+
* in `src/client/connect.ts` — `http://localhost:<that machine's configured port>`, which on a
|
|
21
|
+
* fresh client is the default 10100. The hub cannot observe the other machine's port, so the
|
|
22
|
+
* grant is bound to this origin unless `corsAllowOrigins` names a different loopback one.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_CLIENT_BROWSER_ORIGIN = "http://localhost:10100";
|
|
25
|
+
|
|
26
|
+
export interface HubCommandDeps extends RuntimeApiDeps {
|
|
27
|
+
loadConfig: () => OcxConfig;
|
|
28
|
+
findLiveProxy?: () => Promise<LiveProxy | null>;
|
|
29
|
+
requestPairingGrant?: (
|
|
30
|
+
target: LiveProxy,
|
|
31
|
+
browserOrigin: string,
|
|
32
|
+
deps?: GuiPairClientDeps,
|
|
33
|
+
) => Promise<GuiPairRequestResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HubInviteOptions {
|
|
37
|
+
json: boolean;
|
|
38
|
+
dataUrl?: string;
|
|
39
|
+
managementUrl?: string;
|
|
40
|
+
clients?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface HubInvitePayload {
|
|
44
|
+
code: string;
|
|
45
|
+
expiresAt: string;
|
|
46
|
+
dataUrl: string;
|
|
47
|
+
managementUrl: string;
|
|
48
|
+
command: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isLoopbackOrigin(origin: string): boolean {
|
|
52
|
+
try {
|
|
53
|
+
const host = new URL(origin).hostname.toLowerCase();
|
|
54
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Loopback or HTTPS, the rule `consumeGuiPairingGrant` enforces on the hub side. */
|
|
61
|
+
export function pairingOriginUsable(origin: string): boolean {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(origin).protocol === "https:" || isLoopbackOrigin(origin);
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseHubInviteArgs(args: string[]): HubInviteOptions | null {
|
|
70
|
+
const options: HubInviteOptions = { json: false };
|
|
71
|
+
for (let index = 0; index < args.length; index++) {
|
|
72
|
+
const arg = args[index]!;
|
|
73
|
+
if (arg === "--json" && !options.json) {
|
|
74
|
+
options.json = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--data-url" || arg === "--management-url" || arg === "--clients") {
|
|
78
|
+
const value = args[++index];
|
|
79
|
+
if (!value || value.startsWith("--")) return null;
|
|
80
|
+
const key = arg === "--data-url" ? "dataUrl" : arg === "--management-url" ? "managementUrl" : "clients";
|
|
81
|
+
if (options[key] !== undefined) return null;
|
|
82
|
+
options[key] = value;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return options;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseInviteClients(raw: string | undefined): OcxConnectedClientId[] | null {
|
|
91
|
+
if (raw === undefined) return [];
|
|
92
|
+
const values = raw.split(",").map(value => value.trim()).filter(Boolean);
|
|
93
|
+
if (values.length < 1 || values.some(value => value !== "codex" && value !== "claude")) return null;
|
|
94
|
+
return values as OcxConnectedClientId[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The browser origin the grant is bound to, or null when the hub admits none.
|
|
99
|
+
*
|
|
100
|
+
* `createGuiPairingGrant` accepts only `hub.managementPublicOrigin` itself or an entry of
|
|
101
|
+
* `corsAllowOrigins`, so this picks from exactly that set rather than guessing: the default
|
|
102
|
+
* client origin when it is admitted, otherwise the first admitted loopback origin. A hub whose
|
|
103
|
+
* allow-list names no loopback origin cannot pair a remote `ocx connect` at all, and saying so
|
|
104
|
+
* here is better than minting a code the exchange will reject.
|
|
105
|
+
*/
|
|
106
|
+
export function selectInviteBrowserOrigin(config: OcxConfig): string | null {
|
|
107
|
+
const allowed = [
|
|
108
|
+
canonicalGuiBrowserOrigin(config.hub?.managementPublicOrigin ?? ""),
|
|
109
|
+
...(config.corsAllowOrigins ?? []).map(value => canonicalGuiBrowserOrigin(value)),
|
|
110
|
+
].filter((value): value is string => Boolean(value));
|
|
111
|
+
if (allowed.includes(DEFAULT_CLIENT_BROWSER_ORIGIN)) return DEFAULT_CLIENT_BROWSER_ORIGIN;
|
|
112
|
+
return allowed.find(origin => isLoopbackOrigin(origin)) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `http://<bind address>:<port>` — right for a plain tailnet/LAN bind with no TLS frontend. */
|
|
116
|
+
export function derivedHubDataOrigin(hostname: string | undefined, port: number): string {
|
|
117
|
+
const host = probeHostname(hostname);
|
|
118
|
+
return `http://${host === "127.0.0.1" ? "localhost" : host}:${port}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The `ocx config set` lines that will actually work on THIS config.
|
|
123
|
+
*
|
|
124
|
+
* `setPath` in `src/cli/config-command.ts` walks only parents that already exist, so
|
|
125
|
+
* `ocx config set hub.<field> …` exits with `config parent path not found: hub` on a config
|
|
126
|
+
* that has no `hub` object yet — which is exactly the config that needs the advice. Create the
|
|
127
|
+
* parent first, the way `guides/remote-hub.md` does, and only when it is actually missing, so
|
|
128
|
+
* the operator can paste the lines verbatim either way.
|
|
129
|
+
*/
|
|
130
|
+
export function configSetHubLines(
|
|
131
|
+
config: Pick<OcxConfig, "hub">,
|
|
132
|
+
field: "managementPublicOrigin" | "dataPublicOrigin",
|
|
133
|
+
example: string,
|
|
134
|
+
): string[] {
|
|
135
|
+
const set = `ocx config set hub.${field} '${JSON.stringify(example)}'`;
|
|
136
|
+
return config.hub ? [set] : ["ocx config set hub '{}'", set];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A `corsAllowOrigins` line that ADDS an origin instead of replacing the list.
|
|
141
|
+
*
|
|
142
|
+
* `ocx config set corsAllowOrigins '[…]'` overwrites the array, so printing a one-element
|
|
143
|
+
* literal tells an operator with an existing allow-list to delete it. The already-configured
|
|
144
|
+
* entries are known here, so the suggested value carries them.
|
|
145
|
+
*/
|
|
146
|
+
export function appendCorsAllowOriginsCommand(
|
|
147
|
+
config: Pick<OcxConfig, "corsAllowOrigins">,
|
|
148
|
+
origin: string,
|
|
149
|
+
): string {
|
|
150
|
+
const current = config.corsAllowOrigins ?? [];
|
|
151
|
+
const next = current.includes(origin) ? current : [...current, origin];
|
|
152
|
+
return `ocx config set corsAllowOrigins '${JSON.stringify(next)}'`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type HubDataOriginResolution =
|
|
156
|
+
| { kind: "usable"; dataUrl: string; source: "flag" | "config" | "derived" }
|
|
157
|
+
/** The bind address can only be spelled as loopback, so there is nothing to advertise. */
|
|
158
|
+
| { kind: "loopback-derived"; dataUrl: string; bindHostname: string };
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The data origin to advertise, or a refusal.
|
|
162
|
+
*
|
|
163
|
+
* `derivedHubDataOrigin` maps a loopback bind AND every wildcard spelling to
|
|
164
|
+
* `http://localhost:<port>` (via `probeHostname`), which on the other machine means "dial
|
|
165
|
+
* yourself". Printing it burns the single-use code on a connect that cannot succeed, so a
|
|
166
|
+
* derived loopback origin is a refusal rather than a value. A wildcard bind is refused the same
|
|
167
|
+
* way on purpose: nothing in this repo derives a tailnet or LAN address, and guessing one from
|
|
168
|
+
* `os.networkInterfaces()` would advertise an interface the operator never chose.
|
|
169
|
+
*
|
|
170
|
+
* An explicit `--data-url` or `hub.dataPublicOrigin` is never second-guessed — a loopback data
|
|
171
|
+
* origin is legitimate when the "other machine" is reached through an SSH tunnel.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveHubDataOrigin(
|
|
174
|
+
override: string | null,
|
|
175
|
+
configured: string | undefined,
|
|
176
|
+
bindHostname: string | undefined,
|
|
177
|
+
port: number,
|
|
178
|
+
): HubDataOriginResolution {
|
|
179
|
+
if (override) return { kind: "usable", dataUrl: override, source: "flag" };
|
|
180
|
+
const fromConfig = canonicalHttpOrigin(configured);
|
|
181
|
+
if (fromConfig) return { kind: "usable", dataUrl: fromConfig, source: "config" };
|
|
182
|
+
const derived = derivedHubDataOrigin(bindHostname, port);
|
|
183
|
+
if (!isLoopbackOrigin(derived)) return { kind: "usable", dataUrl: derived, source: "derived" };
|
|
184
|
+
const trimmed = (bindHostname ?? "").trim();
|
|
185
|
+
return { kind: "loopback-derived", dataUrl: derived, bindHostname: trimmed || "127.0.0.1" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Wildcards and loopback fail for different reasons; say which one this hub has. */
|
|
189
|
+
function bindAddressPhrase(bindHostname: string): string {
|
|
190
|
+
return bindHostname === "0.0.0.0" || bindHostname === "::" || bindHostname === "[::]"
|
|
191
|
+
? `the bind address ${bindHostname} is a wildcard, which names no address another machine can dial`
|
|
192
|
+
: `the bind address ${bindHostname} is loopback-only`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* What an operator has to know about the origin the grant actually got bound to.
|
|
197
|
+
*
|
|
198
|
+
* `selectInviteBrowserOrigin` falls back to the first admitted loopback origin when
|
|
199
|
+
* `http://localhost:10100` is not admitted, and a remote `ocx connect` sends
|
|
200
|
+
* `Origin: http://localhost:<its own configured port>` — so a grant bound to anything else is
|
|
201
|
+
* refused at the exchange and the single-use code is spent with nothing printed to explain it.
|
|
202
|
+
* Always stating the bound origin, and naming the port the client needs when it differs, is the
|
|
203
|
+
* difference between a fixable failure and a mystery.
|
|
204
|
+
*/
|
|
205
|
+
export function inviteBoundOriginNotes(
|
|
206
|
+
browserOrigin: string,
|
|
207
|
+
config: Pick<OcxConfig, "corsAllowOrigins">,
|
|
208
|
+
): string[] {
|
|
209
|
+
const notes = [`Bound browser origin: ${browserOrigin} — the connecting machine must present exactly this.`];
|
|
210
|
+
if (browserOrigin === DEFAULT_CLIENT_BROWSER_ORIGIN) return notes;
|
|
211
|
+
const parsed = new URL(browserOrigin);
|
|
212
|
+
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
213
|
+
notes.push(
|
|
214
|
+
`That is NOT ${DEFAULT_CLIENT_BROWSER_ORIGIN}, which is what an unconfigured client sends: the other machine `
|
|
215
|
+
+ `must already be running on port ${port} ('ocx config set port ${port}' there) before it runs the line `
|
|
216
|
+
+ "below, or the hub refuses the exchange and the code is spent.",
|
|
217
|
+
);
|
|
218
|
+
notes.push(
|
|
219
|
+
"To accept a default client instead, admit its origin on this hub: "
|
|
220
|
+
+ appendCorsAllowOriginsCommand(config, DEFAULT_CLIENT_BROWSER_ORIGIN),
|
|
221
|
+
);
|
|
222
|
+
return notes;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function hubInviteCommand(
|
|
226
|
+
code: string,
|
|
227
|
+
dataUrl: string,
|
|
228
|
+
managementUrl: string,
|
|
229
|
+
clients: OcxConnectedClientId[],
|
|
230
|
+
): string {
|
|
231
|
+
const clientsFlag = clients.length > 0 ? ` --clients ${clients.join(",")}` : "";
|
|
232
|
+
return `echo '${code}' | ocx connect ${dataUrl} --management-url ${managementUrl}${clientsFlag} --pairing-code-stdin`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runInvite(args: string[], deps: HubCommandDeps): Promise<number> {
|
|
236
|
+
const options = parseHubInviteArgs(args);
|
|
237
|
+
if (!options) {
|
|
238
|
+
console.error(`Usage: ${HUB_USAGE}`);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const clients = parseInviteClients(options.clients);
|
|
242
|
+
if (!clients) {
|
|
243
|
+
console.error("--clients must contain codex and/or claude.");
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
const config = deps.loadConfig();
|
|
247
|
+
if (config.runtimeRole !== "hub") {
|
|
248
|
+
console.error(
|
|
249
|
+
`ocx hub invite runs on a hub; this machine's runtimeRole is "${config.runtimeRole ?? "standalone"}". `
|
|
250
|
+
+ "A client machine runs 'ocx connect' with the code its hub printed.",
|
|
251
|
+
);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
// The grant's server origin IS hub.managementPublicOrigin (createGuiPairingGrant reads it,
|
|
255
|
+
// and the exchange compares the request's management origin against it), so an invite that
|
|
256
|
+
// advertised anything else would hand out a code the hub then refuses.
|
|
257
|
+
const managementPublic = canonicalHttpOrigin(config.hub?.managementPublicOrigin);
|
|
258
|
+
if (!managementPublic) {
|
|
259
|
+
console.error(
|
|
260
|
+
"hub.managementPublicOrigin is not set, so there is no origin to pair against. Set the exact "
|
|
261
|
+
+ "browser-visible HTTPS origin:",
|
|
262
|
+
);
|
|
263
|
+
for (const line of configSetHubLines(config, "managementPublicOrigin", "https://hub.tailnet.ts.net")) {
|
|
264
|
+
console.error(` ${line}`);
|
|
265
|
+
}
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
const managementOverride = options.managementUrl === undefined ? null : canonicalHttpOrigin(options.managementUrl);
|
|
269
|
+
if (options.managementUrl !== undefined && !managementOverride) {
|
|
270
|
+
console.error("--management-url must be a bare http(s) origin with no path, query, or credentials.");
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
if (managementOverride && managementOverride !== managementPublic) {
|
|
274
|
+
console.error(
|
|
275
|
+
`--management-url ${managementOverride} does not match hub.managementPublicOrigin ${managementPublic}. `
|
|
276
|
+
+ "The pairing code is bound to the configured origin, so the other machine would be refused.",
|
|
277
|
+
);
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
if (!pairingOriginUsable(managementPublic)) {
|
|
281
|
+
console.error(
|
|
282
|
+
`hub.managementPublicOrigin ${managementPublic} is non-loopback plain HTTP, which cannot carry a `
|
|
283
|
+
+ "pairing code. Put management behind an HTTPS frontend (Tailscale Serve) and set that origin.",
|
|
284
|
+
);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
const dataOverride = options.dataUrl === undefined ? null : canonicalHttpOrigin(options.dataUrl);
|
|
288
|
+
if (options.dataUrl !== undefined && !dataOverride) {
|
|
289
|
+
console.error("--data-url must be a bare http(s) origin with no path, query, or credentials.");
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
const browserOrigin = selectInviteBrowserOrigin(config);
|
|
293
|
+
if (!browserOrigin) {
|
|
294
|
+
// `ocx config set corsAllowOrigins` REPLACES the array, so the suggested value carries the
|
|
295
|
+
// entries this hub already has -- a one-element literal would tell the operator to drop them.
|
|
296
|
+
console.error(
|
|
297
|
+
"No loopback browser origin is admitted for pairing. Add the connecting machine's local origin "
|
|
298
|
+
+ "(this keeps the entries already configured; 'ocx config get corsAllowOrigins' shows them): "
|
|
299
|
+
+ appendCorsAllowOriginsCommand(config, DEFAULT_CLIENT_BROWSER_ORIGIN),
|
|
300
|
+
);
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
const target = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
304
|
+
if (!target) {
|
|
305
|
+
console.error("No running attested OpenCodex hub was found. Check 'ocx service status', then 'ocx service repair'.");
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const resolved = resolveHubDataOrigin(
|
|
309
|
+
dataOverride,
|
|
310
|
+
config.hub?.dataPublicOrigin,
|
|
311
|
+
target.hostname ?? config.hostname,
|
|
312
|
+
target.port,
|
|
313
|
+
);
|
|
314
|
+
if (resolved.kind === "loopback-derived") {
|
|
315
|
+
console.error(
|
|
316
|
+
`The advertised data origin would be ${resolved.dataUrl} — this machine's own loopback — because `
|
|
317
|
+
+ `${bindAddressPhrase(resolved.bindHostname)}, and nothing here guesses a tailnet or LAN address. `
|
|
318
|
+
+ "The other machine would dial itself and the single-use code would be spent for nothing. Name the "
|
|
319
|
+
+ "origin remote machines reach this hub's data plane on:",
|
|
320
|
+
);
|
|
321
|
+
for (const line of configSetHubLines(config, "dataPublicOrigin", "https://hub.tailnet.ts.net:8443")) {
|
|
322
|
+
console.error(` ${line}`);
|
|
323
|
+
}
|
|
324
|
+
console.error(" ...or, for this invite only: ocx hub invite --data-url https://hub.tailnet.ts.net:8443");
|
|
325
|
+
return 1;
|
|
326
|
+
}
|
|
327
|
+
const dataUrl = resolved.dataUrl;
|
|
328
|
+
const result = await (deps.requestPairingGrant ?? requestBoundGuiPairingGrant)(target, browserOrigin, {
|
|
329
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
330
|
+
});
|
|
331
|
+
if (result.kind !== "created") {
|
|
332
|
+
console.error(`Minting a pairing code failed (${result.reason}).`);
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
const payload: HubInvitePayload = {
|
|
336
|
+
code: result.grant,
|
|
337
|
+
expiresAt: new Date(result.expiresAt).toISOString(),
|
|
338
|
+
dataUrl,
|
|
339
|
+
managementUrl: managementPublic,
|
|
340
|
+
command: hubInviteCommand(result.grant, dataUrl, managementPublic, clients),
|
|
341
|
+
};
|
|
342
|
+
// Always, in both modes: the grant is bound to ONE browser origin and the operator cannot
|
|
343
|
+
// see which from the printed command (#4236 review).
|
|
344
|
+
for (const note of inviteBoundOriginNotes(browserOrigin, config)) console.error(note);
|
|
345
|
+
if (options.json) {
|
|
346
|
+
console.log(JSON.stringify(payload));
|
|
347
|
+
console.error(PAIRING_WARNING);
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
// Remaining time, not the constant TTL: the number an operator reads has to be the one
|
|
351
|
+
// they actually have left by the time the line is printed.
|
|
352
|
+
const ttlSeconds = Math.max(0, Math.round((result.expiresAt - Date.now()) / 1000));
|
|
353
|
+
console.log(`Pairing code for one machine — single-use, expires in ${ttlSeconds}s (${payload.expiresAt}).`);
|
|
354
|
+
console.log("");
|
|
355
|
+
console.log("# Run on the other machine:");
|
|
356
|
+
console.log(payload.command);
|
|
357
|
+
console.error(PAIRING_WARNING);
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function runHubCommand(args: string[], deps: HubCommandDeps): Promise<number> {
|
|
362
|
+
if (args[0] !== "invite") {
|
|
363
|
+
console.error(`Usage: ${HUB_USAGE}`);
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
return runInvite(args.slice(1), deps);
|
|
367
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
pendingTeardownPathFor,
|
|
49
49
|
quarantinePendingTeardown,
|
|
50
50
|
} from "../config/pending-teardown";
|
|
51
|
-
import { collectStatus, unusedProxyWarningLines } from "./status";
|
|
51
|
+
import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
|
|
52
52
|
import { endpointsToProve, everyEndpointProvenDown, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
53
53
|
import { takeFlag } from "./runtime-api";
|
|
54
54
|
|
|
@@ -66,10 +66,11 @@ import { dispatchCommand , decideStartWithLiveOwner } from "./dispatch";
|
|
|
66
66
|
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
67
67
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
68
68
|
import { createReadinessGate } from "../server/readiness";
|
|
69
|
+
import { isApiAuthRequired } from "../server/auth-cors";
|
|
69
70
|
import { runReady, type ReadyArgs } from "./ready";
|
|
70
71
|
import { runCli } from "./root";
|
|
71
|
-
import { isProcessAlive, ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
|
|
72
|
-
import {
|
|
72
|
+
import { isProcessAlive, ProxyOwnershipRefusedError, refusalNextStep, stopProxy } from "../lib/process-control";
|
|
73
|
+
import { startupDataPlaneToken } from "../lib/service-secrets";
|
|
73
74
|
import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxyStillLiveAfterStop, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalledDetailed, uninstallServiceIfInstalled, uninstallServiceDetailed } from "../service";
|
|
74
75
|
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
75
76
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
@@ -81,8 +82,11 @@ import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
|
81
82
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
82
83
|
import { syncModelsToCodex } from "../codex/sync";
|
|
83
84
|
import {
|
|
85
|
+
HUB_GATED_SKIP_MESSAGE,
|
|
86
|
+
localClientSkipReason,
|
|
84
87
|
shouldSyncGrokOnStart,
|
|
85
88
|
syncCodexOnStartIfEnabled,
|
|
89
|
+
type LocalClientSkipReason,
|
|
86
90
|
} from "../codex/desired-state";
|
|
87
91
|
import {
|
|
88
92
|
reconcileClientStartupBeforeReady,
|
|
@@ -174,6 +178,19 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
|
174
178
|
return null;
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The one startup line for "nothing was written to Codex".
|
|
183
|
+
*
|
|
184
|
+
* Two very different facts reached it: the user's own OFF switch, and a hub declining to
|
|
185
|
+
* rewrite its own local clients. Printing the toggle's wording for the gate is what made
|
|
186
|
+
* operators hunt for a switch they never set (#4236).
|
|
187
|
+
*/
|
|
188
|
+
function startupLeftCodexNativeLine(reason: LocalClientSkipReason): string {
|
|
189
|
+
return reason === "hub-gated"
|
|
190
|
+
? ` ${HUB_GATED_SKIP_MESSAGE} Startup left Codex native.`
|
|
191
|
+
: " Codex integration OFF; startup left Codex native.";
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
178
195
|
function startArgv(port?: number): string[] {
|
|
179
196
|
const args = ["start"];
|
|
@@ -190,6 +207,10 @@ async function chooseListenPort(
|
|
|
190
207
|
const config = loadConfig();
|
|
191
208
|
const preferred = requestedPort ?? config.port ?? 10100;
|
|
192
209
|
const hardPin = requestedPort !== undefined && requestedPort > 0;
|
|
210
|
+
// Only an EXPLICITLY ported listener reserves a port. The companion form (`{enabled:true}`
|
|
211
|
+
// with no port) deliberately shares the public port on 127.0.0.1, so treating it as a
|
|
212
|
+
// reservation — via `effectiveLoopbackListenerPort` — would refuse every start on a
|
|
213
|
+
// one-port hub and hop the public listener onto an ephemeral port (#4236).
|
|
193
214
|
const reservedLoopbackPort = config.unauthenticatedLoopbackListener?.enabled
|
|
194
215
|
? config.unauthenticatedLoopbackListener.port
|
|
195
216
|
: undefined;
|
|
@@ -271,10 +292,16 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
271
292
|
}
|
|
272
293
|
|
|
273
294
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
274
|
-
// Native (WinSW) service mode has no batch wrapper to read the service token file
|
|
275
|
-
//
|
|
295
|
+
// Native (WinSW) service mode has no batch wrapper to read the service token file into
|
|
296
|
+
// the environment, and a FOREGROUND `ocx start` has no wrapper at all — so the app loads
|
|
297
|
+
// the token here, before the server binds, with the same precedence the launchd plist and
|
|
298
|
+
// the systemd unit use when they cat the file into the environment. Without the second
|
|
299
|
+
// source, `ocx start` refused to bind a non-loopback hostname (assertServerAuthConfig)
|
|
300
|
+
// that the installed service on the same machine was serving happily (#4236). The server
|
|
276
301
|
// auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
|
|
277
|
-
const serviceToken =
|
|
302
|
+
const serviceToken = startupDataPlaneToken(process.env, {
|
|
303
|
+
authRequired: isApiAuthRequired(loadConfig()),
|
|
304
|
+
});
|
|
278
305
|
if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
|
|
279
306
|
// The service wrapper (and WinSW via OCX_API_TOKEN_FILE) can still export a colliding
|
|
280
307
|
// token that install now refuses to write. Refuse it here too, before bind, so an
|
|
@@ -513,7 +540,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
513
540
|
}
|
|
514
541
|
},
|
|
515
542
|
);
|
|
516
|
-
if (!startupSync.ran) console.log(
|
|
543
|
+
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config)));
|
|
517
544
|
await refreshOwnedRaycastCatalog(config, port);
|
|
518
545
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
519
546
|
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
@@ -579,7 +606,9 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
579
606
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
580
607
|
return null;
|
|
581
608
|
});
|
|
582
|
-
if (synced?.status === "skipped")
|
|
609
|
+
if (synced?.status === "skipped") {
|
|
610
|
+
console.log(startupLeftCodexNativeLine(synced.skippedReason ?? "desired_disabled"));
|
|
611
|
+
}
|
|
583
612
|
// Do not refresh Raycast from saved config here: live bind/admission and
|
|
584
613
|
// secondary-listener settings may differ. Explicit sync or server startup
|
|
585
614
|
// owns catalog refresh; ensure must not overwrite a working destination.
|
|
@@ -626,7 +655,9 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
626
655
|
console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
|
|
627
656
|
return null;
|
|
628
657
|
});
|
|
629
|
-
if (synced?.status === "skipped")
|
|
658
|
+
if (synced?.status === "skipped") {
|
|
659
|
+
console.log(startupLeftCodexNativeLine(synced.skippedReason ?? "desired_disabled"));
|
|
660
|
+
}
|
|
630
661
|
// The child performs Raycast refresh with its actual startup config. The
|
|
631
662
|
// parent's pre-spawn snapshot is not authoritative for a client-file write.
|
|
632
663
|
// The child opens /healthz before its best-effort roster reconcile. Await the same idempotent
|
|
@@ -952,7 +983,12 @@ async function handleStop() {
|
|
|
952
983
|
if (detail) console.error(` ${detail}`);
|
|
953
984
|
if (err instanceof ProxyOwnershipRefusedError) {
|
|
954
985
|
ownershipBlocked = true;
|
|
955
|
-
|
|
986
|
+
// Every refusal `POST /api/stop` produces is written for an API client, so it
|
|
987
|
+
// recommends `ocx stop` — the command printing it. Following that advice returns
|
|
988
|
+
// the operator to this exact message, which is the loop #4169 was filed for. The
|
|
989
|
+
// service manager was already asked to stop above, so name what is actually left.
|
|
990
|
+
console.error(` ${refusalNextStep(err.code)}`);
|
|
991
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the refusing proxy is still running.");
|
|
956
992
|
}
|
|
957
993
|
}
|
|
958
994
|
} else {
|
|
@@ -979,7 +1015,9 @@ async function handleStop() {
|
|
|
979
1015
|
if (detail) console.error(` ${detail}`);
|
|
980
1016
|
if (err instanceof ProxyOwnershipRefusedError) {
|
|
981
1017
|
ownershipBlocked = true;
|
|
982
|
-
|
|
1018
|
+
// Same loop as the tracked-pid path above: the refusal recommends this command.
|
|
1019
|
+
console.error(` ${refusalNextStep(err.code)}`);
|
|
1020
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the refusing proxy is still running.");
|
|
983
1021
|
}
|
|
984
1022
|
}
|
|
985
1023
|
} else if (live) {
|
|
@@ -1345,12 +1383,22 @@ async function handleStatus() {
|
|
|
1345
1383
|
return;
|
|
1346
1384
|
}
|
|
1347
1385
|
|
|
1386
|
+
// First line of the report, above the proxy line, deliberately (#4236): on a connected client
|
|
1387
|
+
// the provider/login/model lines describe the HUB, and the lines that describe this machine
|
|
1388
|
+
// are tagged `(local)`. A reader who sees neither draws the wrong conclusion from a correct
|
|
1389
|
+
// report — an agent read `xai ✗ not logged in` off a client and decided the hub could not
|
|
1390
|
+
// serve grok.
|
|
1391
|
+
const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub);
|
|
1392
|
+
if (remoteHubBanner) console.log(remoteHubBanner);
|
|
1393
|
+
// `(local)` only while connected: on a standalone install every line is local, and tagging
|
|
1394
|
+
// them all would be noise that trains the reader to skip the tag.
|
|
1395
|
+
const local = status.json.remoteHub.connected ? " (local)" : "";
|
|
1348
1396
|
if (status.json.proxy.pid || status.json.proxy.health.ok) {
|
|
1349
|
-
console.log(`✅ Proxy: ${status.proxyLabel}`);
|
|
1397
|
+
console.log(`✅ Proxy: ${status.proxyLabel}${local}`);
|
|
1350
1398
|
} else {
|
|
1351
|
-
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
1399
|
+
console.log(`❌ Proxy: ${status.proxyLabel}${local}`);
|
|
1352
1400
|
}
|
|
1353
|
-
console.log(` Health: ${status.healthLabel}`);
|
|
1401
|
+
console.log(` Health: ${status.healthLabel}${local}`);
|
|
1354
1402
|
if (status.json.claudeDesktop.desiredEnabled && !status.json.claudeDesktop.policy.ok) {
|
|
1355
1403
|
console.log(` ⚠️ Claude Desktop 3P health: ${status.json.claudeDesktop.policy.status}`);
|
|
1356
1404
|
console.log(` ${status.json.claudeDesktop.policy.message}`);
|
|
@@ -1390,25 +1438,31 @@ async function handleStatus() {
|
|
|
1390
1438
|
? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
1391
1439
|
: " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
|
1392
1440
|
}
|
|
1393
|
-
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
1394
|
-
console.log(` Config: ${status.json.paths.config}`);
|
|
1395
|
-
console.log(` PID file: ${status.json.paths.pid}`);
|
|
1396
|
-
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
1397
|
-
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
1398
|
-
|
|
1441
|
+
console.log(` Dashboard: ${status.json.dashboard.url}${local}`);
|
|
1442
|
+
console.log(` Config: ${status.json.paths.config}${local}`);
|
|
1443
|
+
console.log(` PID file: ${status.json.paths.pid}${local}`);
|
|
1444
|
+
console.log(` Runtime: ${status.json.paths.runtime}${local}`);
|
|
1445
|
+
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}${local}`);
|
|
1446
|
+
// On a client this is the local default, which routing does not use — the hub applies its own.
|
|
1447
|
+
console.log(` Default provider: ${status.json.defaultProvider}${local}`);
|
|
1448
|
+
// One block rather than six scattered lines, and only on a hub: `hubStatusLines` owns the
|
|
1449
|
+
// sentences so they are testable without spawning the CLI. It prints no token value.
|
|
1450
|
+
if (status.json.hub) {
|
|
1451
|
+
for (const line of hubStatusLines(status.json.hub)) console.log(` ${line}`);
|
|
1452
|
+
}
|
|
1399
1453
|
console.log(` Remote hub: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
|
|
1400
1454
|
if (status.json.connection.state === "invalid" || status.json.connection.state === "mismatched") {
|
|
1401
1455
|
console.log(` ⚠️ ${status.json.connection.reason}`);
|
|
1402
1456
|
}
|
|
1403
|
-
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
1404
|
-
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
1405
|
-
console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
|
|
1406
|
-
console.log(` Service: ${status.json.service.summary}`);
|
|
1407
|
-
console.log(` ${status.json.codexShim.summary}`);
|
|
1408
|
-
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
|
1409
|
-
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
|
|
1410
|
-
console.log(` Codex source: ${status.json.codexRuntime.source}`);
|
|
1411
|
-
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`);
|
|
1457
|
+
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}${local}`);
|
|
1458
|
+
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}${local}`);
|
|
1459
|
+
console.log(` ${formatStartupRoutingDetail(status.json.startup)}${local}`);
|
|
1460
|
+
console.log(` Service: ${status.json.service.summary}${local}`);
|
|
1461
|
+
console.log(` ${status.json.codexShim.summary}${local}`);
|
|
1462
|
+
console.log(` Codex runtime: ${status.json.codexRuntime.path}${local}`);
|
|
1463
|
+
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}${local}`);
|
|
1464
|
+
console.log(` Codex source: ${status.json.codexRuntime.source}${local}`);
|
|
1465
|
+
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}${local}`);
|
|
1412
1466
|
if (status.json.codexHome.warning) {
|
|
1413
1467
|
console.log(` ⚠️ ${status.json.codexHome.warning}`);
|
|
1414
1468
|
console.log(` Action: ${status.json.codexHome.action}`);
|
|
@@ -1428,9 +1482,23 @@ async function handleStatus() {
|
|
|
1428
1482
|
}
|
|
1429
1483
|
}
|
|
1430
1484
|
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
1485
|
+
const { emailMaskingEnabled } = await import("../lib/privacy");
|
|
1431
1486
|
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
1432
|
-
|
|
1433
|
-
|
|
1487
|
+
// On a connected client the HUB's providers and logins come first, because they are the ones
|
|
1488
|
+
// that decide what a request can route to. The local block still prints — an operator debugging
|
|
1489
|
+
// a half-migrated machine needs to see it — but under a heading that says it is not in use, and
|
|
1490
|
+
// below the hub's, so the hub's is what a reader (or an agent) encounters first (#4236).
|
|
1491
|
+
for (const line of remoteHubStatusLines(status.json.remoteHub)) console.log(` ${line}`);
|
|
1492
|
+
if (status.json.remoteHub.connected) {
|
|
1493
|
+
console.log(status.json.remoteHub.stateSource === "unavailable"
|
|
1494
|
+
? " Local-only credential state (this is NOT the hub's; the hub's state could not be read):"
|
|
1495
|
+
: " Local-only (not used for routing while connected):");
|
|
1496
|
+
}
|
|
1497
|
+
console.log(` OAuth logins${local}:`);
|
|
1498
|
+
// The operator's own `privacy.maskEmails` decision applies to the CLI too: `ocx status` is not
|
|
1499
|
+
// the dashboard, but it reads the same stored addresses, and a flag that only moved one of the
|
|
1500
|
+
// two would leave the operator unable to tell which surface they had configured.
|
|
1501
|
+
for (const e of oauthLoginSummary(emailMaskingEnabled(loadConfig()))) {
|
|
1434
1502
|
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
1435
1503
|
}
|
|
1436
1504
|
const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trusted facts captured by the plain-Node
|
|
2
|
+
* Trusted facts captured by the plain-Node package launcher before Bun auto-loads
|
|
3
3
|
* project dotenv files. The random proof travels in argv while the context
|
|
4
4
|
* travels in the environment, so a project `.env` cannot forge the pair during
|
|
5
5
|
* an ordinary `ocx ...` invocation.
|
|
@@ -21,7 +21,7 @@ import { MAX_COST4_RATE } from "../usage/expected-prices";
|
|
|
21
21
|
import { isValidCost4Rate } from "../usage/user-cost-overlays";
|
|
22
22
|
|
|
23
23
|
const USAGE = `Usage:
|
|
24
|
-
ocx models live [--provider <name>] [--json]
|
|
24
|
+
ocx models live [--provider <name>] [--free-only] [--json]
|
|
25
25
|
ocx models price <provider/model> [--json]
|
|
26
26
|
ocx models set-price <provider/model> --input N --output N [--cache-read N] [--cache-write N] [--json]
|
|
27
27
|
ocx models set-price <provider/model> --auto [--json]
|
|
@@ -52,17 +52,22 @@ type ModelRow = {
|
|
|
52
52
|
custom?: boolean;
|
|
53
53
|
customId?: string;
|
|
54
54
|
displayName?: string;
|
|
55
|
+
pricingStatus?: "free" | "paid";
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
async function live(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
58
59
|
const args = [...argv];
|
|
59
60
|
const wantsJson = takeFlag(args, "--json");
|
|
60
61
|
const provider = takeOption(args, "--provider");
|
|
62
|
+
// Absent pricingStatus means the provider published no usable per-token pair, so it is
|
|
63
|
+
// excluded here for the same fail-closed reason the classifier omits it (#3666).
|
|
64
|
+
const freeOnly = takeFlag(args, "--free-only");
|
|
61
65
|
rejectArgs(args, USAGE);
|
|
62
66
|
const rows = await runtimeRequest<ModelRow[]>("/api/models", {}, deps);
|
|
63
|
-
const
|
|
67
|
+
const byProvider = provider ? rows.filter(row => row.provider === provider) : rows;
|
|
68
|
+
const filtered = freeOnly ? byProvider.filter(row => row.pricingStatus === "free") : byProvider;
|
|
64
69
|
printData(filtered, wantsJson, filtered.map(row => {
|
|
65
|
-
const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.initialSelectionPending ? "initial discovery pending" : row.disabled ? "disabled" : "enabled"].filter(Boolean);
|
|
70
|
+
const flags = [row.native ? "native" : "routed", row.custom ? "custom" : "", row.pricingStatus === "free" ? "free" : "", row.initialSelectionPending ? "initial discovery pending" : row.disabled ? "disabled" : "enabled"].filter(Boolean);
|
|
66
71
|
return `${row.namespaced ?? `${row.provider}/${row.id}`} [${flags.join(", ")}]`;
|
|
67
72
|
}));
|
|
68
73
|
}
|