@bitkyc08/opencodex 2.8.0 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx export --client <opencode|pi>` — print a client config for the live proxy.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers, one payload (devlog 260731_client_config_export/020):
|
|
5
|
+
*
|
|
6
|
+
* - **Agent** (`--json`): stdout is exactly the client config JSON and nothing else, so
|
|
7
|
+
* `ocx export --client pi --json > models.json` is safe to pipe. Every diagnostic —
|
|
8
|
+
* including the `--out` write note — goes to stderr.
|
|
9
|
+
* - **Human** (no flag): the JSON leads, then the destination path, the merge warning,
|
|
10
|
+
* the env export line, and the model/degraded counts.
|
|
11
|
+
*
|
|
12
|
+
* The command never writes the user's real config path. `--out` is an explicit target and
|
|
13
|
+
* refuses to clobber an existing file without `--force`, because the common mistake
|
|
14
|
+
* (`--out ~/.config/opencode/opencode.json`) would silently destroy other providers.
|
|
15
|
+
*
|
|
16
|
+
* Serialization itself belongs to src/clients/config-export.ts; this module only resolves
|
|
17
|
+
* the base URL, filters the catalog, and renders. No secret is ever serialized: the config
|
|
18
|
+
* carries the client's documented env reference and the real key stays in the environment.
|
|
19
|
+
*/
|
|
20
|
+
import { writeFileSync } from "node:fs";
|
|
21
|
+
import { loadConfig } from "../config";
|
|
22
|
+
import {
|
|
23
|
+
EXPORT_CLIENTS,
|
|
24
|
+
EXPORT_CLIENT_IDS,
|
|
25
|
+
buildClientConfig,
|
|
26
|
+
isExportClientId,
|
|
27
|
+
opencodeProxyBaseUrl,
|
|
28
|
+
type ExportClientId,
|
|
29
|
+
type ExportModel,
|
|
30
|
+
} from "../clients/config-export";
|
|
31
|
+
import { opencodeCatalogFromProxyRows, type OpencodeProxyModelRow } from "./opencode";
|
|
32
|
+
import type { OcxConfig } from "../types";
|
|
33
|
+
import {
|
|
34
|
+
CliUsageError,
|
|
35
|
+
RuntimeApiError,
|
|
36
|
+
printData,
|
|
37
|
+
rejectArgs,
|
|
38
|
+
runCliAction,
|
|
39
|
+
runtimeBaseUrl,
|
|
40
|
+
runtimeRequest,
|
|
41
|
+
takeFlag,
|
|
42
|
+
takeOption,
|
|
43
|
+
type RuntimeApiDeps,
|
|
44
|
+
} from "./runtime-api";
|
|
45
|
+
|
|
46
|
+
const USAGE = `Usage:
|
|
47
|
+
ocx export --client <${EXPORT_CLIENT_IDS.join("|")}> [--json] [--out <path>] [--force]`;
|
|
48
|
+
|
|
49
|
+
export interface ExportCommandDeps extends RuntimeApiDeps {
|
|
50
|
+
/** Live config seam; tests inject a fixture instead of reading the user's config. */
|
|
51
|
+
configImpl?: () => OcxConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `/api/models` row plus the modality list Pi consumes. The launcher's row type predates
|
|
56
|
+
* the Pi exporter and stops at the fields OpenCode needs.
|
|
57
|
+
*/
|
|
58
|
+
type ExportProxyModelRow = OpencodeProxyModelRow & { inputModalities?: string[] };
|
|
59
|
+
|
|
60
|
+
/** Same authoritativeness rule the serializers apply, for the degraded-count line. */
|
|
61
|
+
function hasContextLimit(model: ExportModel): boolean {
|
|
62
|
+
return typeof model.contextWindow === "number"
|
|
63
|
+
&& Number.isFinite(model.contextWindow)
|
|
64
|
+
&& model.contextWindow > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Export rows from proxy `/api/models` rows.
|
|
69
|
+
*
|
|
70
|
+
* `opencodeCatalogFromProxyRows` owns the visibility rules (drop `disabled`, drop dupes,
|
|
71
|
+
* drop native under Codex Direct) — the export core does none of that, so a row filtered
|
|
72
|
+
* here is the only thing keeping a disabled model out of a client's picker. Modalities are
|
|
73
|
+
* re-joined by `namespaced` because the launcher's catalog type does not carry them.
|
|
74
|
+
*/
|
|
75
|
+
export function exportModelsFromProxyRows(
|
|
76
|
+
rows: readonly ExportProxyModelRow[],
|
|
77
|
+
config: OcxConfig,
|
|
78
|
+
): ExportModel[] {
|
|
79
|
+
const modalities = new Map<string, string[]>();
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
const namespaced = row.namespaced?.trim();
|
|
82
|
+
if (namespaced && Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
|
|
83
|
+
if (!modalities.has(namespaced)) modalities.set(namespaced, [...row.inputModalities]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return opencodeCatalogFromProxyRows(rows, config).map(entry => {
|
|
87
|
+
const model: ExportModel = {
|
|
88
|
+
namespaced: entry.namespaced,
|
|
89
|
+
provider: entry.provider ?? (entry.native ? "openai" : "routed"),
|
|
90
|
+
id: entry.id ?? entry.namespaced,
|
|
91
|
+
};
|
|
92
|
+
if (entry.native) model.native = true;
|
|
93
|
+
if (entry.displayName) model.displayName = entry.displayName;
|
|
94
|
+
if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
|
|
95
|
+
const input = modalities.get(entry.namespaced);
|
|
96
|
+
if (input) model.inputModalities = input;
|
|
97
|
+
return model;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `http://host:port/v1` for the proxy that is actually listening.
|
|
103
|
+
*
|
|
104
|
+
* `runtimeBaseUrl` is the identity-checked `findLiveProxy` probe the launcher uses, and it
|
|
105
|
+
* already throws the "Start it with: ocx start" error when nothing answers — so a config
|
|
106
|
+
* with an empty models block can never be emitted.
|
|
107
|
+
*
|
|
108
|
+
* Resolved ONCE and handed back to `runtimeRequest` as `baseUrl`, so the catalog and the
|
|
109
|
+
* exported endpoint can never come from two different probes.
|
|
110
|
+
*/
|
|
111
|
+
function proxyV1BaseUrl(root: string): string {
|
|
112
|
+
const url = new URL(root);
|
|
113
|
+
const port = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
|
|
114
|
+
return opencodeProxyBaseUrl(port, url.hostname);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseClient(args: string[]): ExportClientId {
|
|
118
|
+
const raw = takeOption(args, "--client");
|
|
119
|
+
if (raw === undefined) {
|
|
120
|
+
throw new CliUsageError(`--client is required (${EXPORT_CLIENT_IDS.join(", ")})`, USAGE);
|
|
121
|
+
}
|
|
122
|
+
const client = raw.trim().toLowerCase();
|
|
123
|
+
if (!isExportClientId(client)) {
|
|
124
|
+
throw new CliUsageError(`--client must be one of: ${EXPORT_CLIENT_IDS.join(", ")}`, USAGE);
|
|
125
|
+
}
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write the config, refusing to replace an existing file without `--force`.
|
|
131
|
+
*
|
|
132
|
+
* The `wx` flag does the refusal in the kernel rather than after an `existsSync` check, so
|
|
133
|
+
* a file created between the two can never be truncated. Nothing is printed before this
|
|
134
|
+
* runs: a refusal leaves both the target bytes and stdout untouched.
|
|
135
|
+
*/
|
|
136
|
+
function writeExport(path: string, text: string, force: boolean): void {
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(path, text, force ? { encoding: "utf8" } : { encoding: "utf8", flag: "wx" });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
141
|
+
throw new CliUsageError(
|
|
142
|
+
`${path} already exists. Re-run with --force to replace it, or print the config and merge it yourself.`,
|
|
143
|
+
USAGE,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function handleExportCommand(argv: string[], deps: ExportCommandDeps = {}): Promise<number> {
|
|
151
|
+
return runCliAction(async () => {
|
|
152
|
+
const args = [...argv];
|
|
153
|
+
const client = parseClient(args);
|
|
154
|
+
const wantsJson = takeFlag(args, "--json");
|
|
155
|
+
const force = takeFlag(args, "--force");
|
|
156
|
+
const out = takeOption(args, "--out");
|
|
157
|
+
rejectArgs(args, USAGE);
|
|
158
|
+
|
|
159
|
+
const spec = EXPORT_CLIENTS[client];
|
|
160
|
+
const config = (deps.configImpl ?? loadConfig)();
|
|
161
|
+
const root = await runtimeBaseUrl(deps);
|
|
162
|
+
const rows = await runtimeRequest<ExportProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
|
|
163
|
+
if (!Array.isArray(rows)) {
|
|
164
|
+
throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
|
|
165
|
+
}
|
|
166
|
+
const models = exportModelsFromProxyRows(rows, config);
|
|
167
|
+
const clientConfig = buildClientConfig(client, { baseUrl: proxyV1BaseUrl(root), models, config });
|
|
168
|
+
const text = JSON.stringify(clientConfig, null, 2);
|
|
169
|
+
|
|
170
|
+
if (out !== undefined) writeExport(out, `${text}\n`, force);
|
|
171
|
+
// stderr, so `--json` stdout stays byte-exact for a redirect.
|
|
172
|
+
if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);
|
|
173
|
+
|
|
174
|
+
const degraded = models.filter(model => !hasContextLimit(model)).length;
|
|
175
|
+
printData(clientConfig, wantsJson, [
|
|
176
|
+
text,
|
|
177
|
+
"",
|
|
178
|
+
...(out !== undefined ? [`Wrote ${out}`] : []),
|
|
179
|
+
`Destination: ${spec.destination(process.env)}`,
|
|
180
|
+
"Merge this provider block into that file; do not replace it.",
|
|
181
|
+
`Before launching: ${spec.exportHint}`,
|
|
182
|
+
`${models.length} model${models.length === 1 ? "" : "s"}; ${degraded} omit context limits (the client applies its own defaults).`,
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const EXPORT_USAGE = USAGE;
|
package/src/cli/help.ts
CHANGED
|
@@ -169,6 +169,16 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
169
169
|
summary: "Manage OpenCodex admission API keys and inspect external endpoints.",
|
|
170
170
|
},
|
|
171
171
|
"api-key": { usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
172
|
+
export: {
|
|
173
|
+
usage: "ocx export --client <opencode|pi> [--json] [--out <path>] [--force]",
|
|
174
|
+
summary: "Print a client config (opencode, Pi) wired to the running proxy.",
|
|
175
|
+
details: [
|
|
176
|
+
"--json prints only the config JSON on stdout, so it is safe to redirect to a file.",
|
|
177
|
+
"--out <path> writes the config there and refuses to replace an existing file without --force.",
|
|
178
|
+
"The config never contains a key; it references the client's env var, which you export before launching.",
|
|
179
|
+
"The destination path is printed for merging by hand — ocx never writes your real client config.",
|
|
180
|
+
],
|
|
181
|
+
},
|
|
172
182
|
grok: { usage: "ocx grok <status|exclude|include|set|clear|apply> ...", summary: "Manage and apply the Grok Build model fence." },
|
|
173
183
|
integration: { usage: "ocx integration <claude|grok> ...", summary: "Manage supported client integrations." },
|
|
174
184
|
system: {
|
|
@@ -286,6 +296,7 @@ Usage:
|
|
|
286
296
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
287
297
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
288
298
|
ocx access <sub> External API keys and endpoint information
|
|
299
|
+
ocx export --client <id> Print an opencode/Pi config wired to the running proxy
|
|
289
300
|
ocx grok <sub> Grok Build model selection and apply
|
|
290
301
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
|
291
302
|
ocx config <sub> Validated configuration show/get/set/import/export
|
package/src/cli/index.ts
CHANGED
|
@@ -129,16 +129,21 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
129
129
|
if (hardPin && preferred > 0) {
|
|
130
130
|
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
131
131
|
await reclaimListenPort(preferred, config.hostname ?? "127.0.0.1", {
|
|
132
|
-
|
|
132
|
+
// Ghost LISTEN rows with a dead PID can outlive the process for a while.
|
|
133
|
+
// SetTcpEntry(DELETE_TCB) needs elevation (often returns 317), so the only
|
|
134
|
+
// reliable non-admin recovery is to wait for the OS to release the TCB.
|
|
135
|
+
timeoutMs: 60_000,
|
|
133
136
|
intervalMs: 100,
|
|
134
137
|
scanIntervalMs: 500,
|
|
135
138
|
killOcxHolders: false,
|
|
136
|
-
dropTcpRows:
|
|
139
|
+
dropTcpRows: true,
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
try {
|
|
140
143
|
const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1", {
|
|
141
|
-
|
|
144
|
+
// After reclaim, keep probing briefly — ghost rows sometimes clear between
|
|
145
|
+
// the reclaim deadline and the final listen. Still never hop off `--port`.
|
|
146
|
+
preferRetryMs: hardPin ? 5_000 : 750,
|
|
142
147
|
preferRetryIntervalMs: 50,
|
|
143
148
|
allowEphemeralFallback: !hardPin,
|
|
144
149
|
});
|
|
@@ -1031,6 +1036,11 @@ switch (command) {
|
|
|
1031
1036
|
process.exitCode = await handleAccessCommand(["key", ...args.slice(1)]);
|
|
1032
1037
|
break;
|
|
1033
1038
|
}
|
|
1039
|
+
case "export": {
|
|
1040
|
+
const { handleExportCommand } = await import("./export-command");
|
|
1041
|
+
process.exitCode = await handleExportCommand(args.slice(1));
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1034
1044
|
case "grok": {
|
|
1035
1045
|
const { handleGrokCommand } = await import("./integrations");
|
|
1036
1046
|
process.exitCode = await handleGrokCommand(args.slice(1));
|
package/src/cli/init.ts
CHANGED
|
@@ -8,11 +8,28 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
|
8
8
|
|
|
9
9
|
function createPrompt(): { ask(question: string): Promise<string>; close(): void } {
|
|
10
10
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
let closed = false;
|
|
12
|
+
rl.on("close", () => { closed = true; });
|
|
11
13
|
return {
|
|
12
14
|
ask(question: string): Promise<string> {
|
|
13
|
-
return new Promise(resolve =>
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (closed) {
|
|
17
|
+
reject(new Error("stdin closed before the prompt could be answered"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const onClose = () => {
|
|
21
|
+
reject(new Error("stdin reached EOF while waiting for input"));
|
|
22
|
+
};
|
|
23
|
+
rl.once("close", onClose);
|
|
24
|
+
rl.question(question, answer => {
|
|
25
|
+
rl.off("close", onClose);
|
|
26
|
+
resolve(answer);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
close() {
|
|
31
|
+
if (!closed) rl.close();
|
|
14
32
|
},
|
|
15
|
-
close() { rl.close(); },
|
|
16
33
|
};
|
|
17
34
|
}
|
|
18
35
|
|
|
@@ -83,115 +100,125 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
|
|
|
83
100
|
|
|
84
101
|
export async function runInit(): Promise<void> {
|
|
85
102
|
const prompt = createPrompt();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
try {
|
|
104
|
+
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
105
|
+
|
|
106
|
+
const providers = buildInitProviders();
|
|
107
|
+
printMenu(providers);
|
|
108
|
+
|
|
109
|
+
const choice = await prompt.ask("\nSelect default provider (number): ");
|
|
110
|
+
const idx = parseInt(choice, 10) - 1;
|
|
111
|
+
|
|
112
|
+
let providerName: string;
|
|
113
|
+
let providerConfig: OcxProviderConfig;
|
|
114
|
+
let oauthHint = false;
|
|
115
|
+
|
|
116
|
+
if (idx >= 0 && idx < providers.length) {
|
|
117
|
+
const p = providers[idx];
|
|
118
|
+
providerName = p.id;
|
|
119
|
+
console.log(`\n📡 ${p.label}`);
|
|
120
|
+
console.log(` Base URL: ${p.baseUrl}`);
|
|
121
|
+
|
|
122
|
+
if (p.kind === "forward") {
|
|
123
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "forward" };
|
|
124
|
+
console.log(" No API key needed — forwards your existing `codex login`.");
|
|
125
|
+
} else if (p.kind === "oauth") {
|
|
126
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "oauth", ...(p.defaultModel ? { defaultModel: p.defaultModel } : {}) };
|
|
127
|
+
oauthHint = true;
|
|
128
|
+
} else {
|
|
129
|
+
// key + local: collect a key (local usually blank).
|
|
130
|
+
if (p.dashboardUrl) console.log(` 🔑 Get your key: ${p.dashboardUrl}`);
|
|
131
|
+
// Template URL with placeholders (e.g. Cloudflare's {account_id}) needs a resolved value.
|
|
132
|
+
let baseUrl = p.baseUrl;
|
|
133
|
+
if (/\{[^}]*\}/.test(baseUrl)) {
|
|
134
|
+
const resolved = (await prompt.ask(` Your endpoint URL (${baseUrl}): `)).trim();
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
console.error(" A resolved URL is required — replace the {placeholder} with your actual value.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
baseUrl = resolved;
|
|
120
140
|
}
|
|
121
|
-
|
|
141
|
+
const env = envKeyFor(p.id);
|
|
142
|
+
const hint = p.kind === "local" ? "API key (usually blank — press Enter): " : `API key (paste, or env var $${env}): `;
|
|
143
|
+
const apiKey = (await prompt.ask(`\n${hint}`)).trim();
|
|
144
|
+
const modelChoice = (await prompt.ask(`Default model${p.defaultModel ? ` [${p.defaultModel}]` : " (optional)"}: `)).trim();
|
|
145
|
+
const defaultModel = modelChoice || p.defaultModel;
|
|
146
|
+
providerConfig = {
|
|
147
|
+
adapter: p.adapter,
|
|
148
|
+
baseUrl,
|
|
149
|
+
...(p.kind === "key" ? { apiKey: apiKey || `\${${env}}` } : apiKey ? { apiKey } : {}),
|
|
150
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
151
|
+
};
|
|
152
|
+
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
153
|
+
enrichProviderFromCatalog(p.id, providerConfig);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
157
|
+
if (!isValidProviderName(providerName)) {
|
|
158
|
+
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
159
|
+
process.exit(1);
|
|
122
160
|
}
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const apiKey =
|
|
126
|
-
const
|
|
127
|
-
const defaultModel = modelChoice || p.defaultModel;
|
|
161
|
+
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
162
|
+
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
163
|
+
const apiKey = await prompt.ask("API key (optional): ");
|
|
164
|
+
const defaultModel = await prompt.ask("Default model: ");
|
|
128
165
|
providerConfig = {
|
|
129
|
-
adapter:
|
|
130
|
-
baseUrl,
|
|
131
|
-
...(
|
|
132
|
-
...(defaultModel ? { defaultModel } : {}),
|
|
166
|
+
adapter: adapter.trim(),
|
|
167
|
+
baseUrl: baseUrl.trim(),
|
|
168
|
+
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
169
|
+
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
133
170
|
};
|
|
134
|
-
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
135
|
-
enrichProviderFromCatalog(p.id, providerConfig);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
139
|
-
if (!isValidProviderName(providerName)) {
|
|
140
|
-
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
141
|
-
prompt.close();
|
|
142
|
-
process.exit(1);
|
|
143
171
|
}
|
|
144
|
-
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
145
|
-
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
146
|
-
const apiKey = await prompt.ask("API key (optional): ");
|
|
147
|
-
const defaultModel = await prompt.ask("Default model: ");
|
|
148
|
-
providerConfig = {
|
|
149
|
-
adapter: adapter.trim(),
|
|
150
|
-
baseUrl: baseUrl.trim(),
|
|
151
|
-
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
152
|
-
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
172
|
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
const portStr = await prompt.ask("\nProxy port [10100]: ");
|
|
174
|
+
const port = parseInt(portStr, 10) || 10100;
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const config: OcxConfig = {
|
|
177
|
+
...getDefaultConfig(),
|
|
178
|
+
port,
|
|
179
|
+
providers: { [providerName]: providerConfig },
|
|
180
|
+
defaultProvider: providerName,
|
|
181
|
+
};
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
saveConfig(config);
|
|
184
|
+
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
185
|
+
// installation would make the next `ocx start` crash on a stale-backup
|
|
186
|
+
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
187
|
+
// post-migration v2 snapshot) may be deleted; a backup that still parses as a
|
|
188
|
+
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
189
|
+
// preserved by renaming it out of the collision path (sol review 260722).
|
|
190
|
+
cleanupOpenAiTierBackupAfterInit();
|
|
191
|
+
console.log(`\n✅ Config saved to ~/.opencodex/config.json`);
|
|
192
|
+
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
193
|
+
|
|
194
|
+
const injectAnswer = await prompt.ask("Inject into Codex config.toml? [Y/n]: ");
|
|
195
|
+
if (injectAnswer.trim().toLowerCase() !== "n") {
|
|
196
|
+
console.log("Fetching available models from provider...");
|
|
197
|
+
const result = await injectCodexConfig(port, config);
|
|
198
|
+
console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
199
|
+
}
|
|
183
200
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
202
|
+
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
203
|
+
try {
|
|
204
|
+
const { installCodexShim } = await import("../codex/shim");
|
|
205
|
+
const result = installCodexShim();
|
|
206
|
+
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
209
|
+
}
|
|
192
210
|
}
|
|
193
|
-
}
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
if (/stdin (closed|reached EOF)/i.test(message)) {
|
|
216
|
+
console.error(`\n❌ ${message}. Re-run \`ocx init\` in an interactive terminal.`);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
} finally {
|
|
222
|
+
prompt.close();
|
|
223
|
+
}
|
|
197
224
|
}
|