@bitkyc08/opencodex 2.10.0 → 2.10.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_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/cli/doctor.ts
CHANGED
|
@@ -13,10 +13,15 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
|
|
14
14
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
15
15
|
import { gracefulStopHost } from "../lib/process-control";
|
|
16
|
+
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
17
|
+
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
16
18
|
import { maskAccountId } from "../lib/privacy";
|
|
17
19
|
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
|
|
18
20
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
19
21
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
22
|
+
import { withNativeMainSharedClaim } from "../codex/native-main-claim";
|
|
23
|
+
import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "../codex/native-profile-store";
|
|
24
|
+
import { NativeProfileError } from "../codex/native-profile-types";
|
|
20
25
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
21
26
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
22
27
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
@@ -479,36 +484,75 @@ export type WhamProbeResult = {
|
|
|
479
484
|
authenticated: boolean;
|
|
480
485
|
};
|
|
481
486
|
|
|
487
|
+
type NativeMainDoctorClaim = <T>(operation: () => Promise<T>) => Promise<T>;
|
|
488
|
+
|
|
489
|
+
export interface WhamProbeDeps {
|
|
490
|
+
withNativeMainClaim?: NativeMainDoctorClaim;
|
|
491
|
+
probeNativeMainRecoveryState?: typeof probeNativeProfileRecoveryState;
|
|
492
|
+
}
|
|
493
|
+
|
|
482
494
|
/**
|
|
483
495
|
* Replicate the runtime WHAM fetch shape (same URL, 8s timeout, main-token
|
|
484
496
|
* headers when present) so the probe fails exactly where the real path fails.
|
|
485
497
|
* `fetchImpl` is injectable for testing.
|
|
486
498
|
*/
|
|
487
|
-
export async function probeWham(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
headers.Authorization = `Bearer ${tokens.access_token}`;
|
|
492
|
-
headers["ChatGPT-Account-Id"] = tokens.account_id;
|
|
493
|
-
}
|
|
499
|
+
export async function probeWham(
|
|
500
|
+
fetchImpl: typeof fetch = fetch,
|
|
501
|
+
deps: WhamProbeDeps = {},
|
|
502
|
+
): Promise<WhamProbeResult> {
|
|
494
503
|
const start = performance.now();
|
|
504
|
+
let authenticated = false;
|
|
495
505
|
try {
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
const context = resolveNativeProfileContext();
|
|
507
|
+
const withClaim = deps.withNativeMainClaim
|
|
508
|
+
?? (<T>(operation: () => Promise<T>) => withNativeMainSharedClaim(context, operation));
|
|
509
|
+
return await withClaim(async () => {
|
|
510
|
+
const recoveryState = (deps.probeNativeMainRecoveryState ?? probeNativeProfileRecoveryState)(context);
|
|
511
|
+
if (recoveryState !== "none") {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
status: null,
|
|
515
|
+
durationMs: Math.round(performance.now() - start),
|
|
516
|
+
classification: `native_main_recovery_${recoveryState}`,
|
|
517
|
+
authenticated: false,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const tokens = readCodexTokens();
|
|
521
|
+
const headers: Record<string, string> = {};
|
|
522
|
+
if (tokens) {
|
|
523
|
+
headers.Authorization = `Bearer ${tokens.access_token}`;
|
|
524
|
+
headers["ChatGPT-Account-Id"] = tokens.account_id;
|
|
525
|
+
}
|
|
526
|
+
authenticated = !!tokens;
|
|
527
|
+
const resp = await fetchImpl(WHAM_USAGE_URL, { headers, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
528
|
+
const durationMs = Math.round(performance.now() - start);
|
|
529
|
+
return {
|
|
530
|
+
ok: resp.ok,
|
|
531
|
+
status: resp.status,
|
|
532
|
+
durationMs,
|
|
533
|
+
classification: resp.ok ? "ok" : `http_${resp.status}`,
|
|
534
|
+
authenticated,
|
|
535
|
+
};
|
|
536
|
+
});
|
|
505
537
|
} catch (err) {
|
|
506
538
|
const durationMs = Math.round(performance.now() - start);
|
|
539
|
+
if (
|
|
540
|
+
err instanceof NativeProfileError
|
|
541
|
+
&& (err.code === "NATIVE_MAIN_CLAIM_BUSY" || err.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE")
|
|
542
|
+
) {
|
|
543
|
+
return {
|
|
544
|
+
ok: false,
|
|
545
|
+
status: null,
|
|
546
|
+
durationMs,
|
|
547
|
+
classification: err.code.toLowerCase(),
|
|
548
|
+
authenticated: false,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
507
551
|
const name = err instanceof Error ? err.name : String(err);
|
|
508
552
|
const classification = name === "TimeoutError" || name === "AbortError"
|
|
509
553
|
? "timeout"
|
|
510
554
|
: "connect_error";
|
|
511
|
-
return { ok: false, status: null, durationMs, classification, authenticated
|
|
555
|
+
return { ok: false, status: null, durationMs, classification, authenticated };
|
|
512
556
|
}
|
|
513
557
|
}
|
|
514
558
|
|
|
@@ -523,6 +567,8 @@ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamPr
|
|
|
523
567
|
export type ServiceMemoryData = {
|
|
524
568
|
pid: number;
|
|
525
569
|
bunVersion: string;
|
|
570
|
+
/** Launch-time provenance; absent for services installed before the marker existed. */
|
|
571
|
+
bunRuntimeSource?: BunRuntimeSource;
|
|
526
572
|
platform: string;
|
|
527
573
|
rss: number;
|
|
528
574
|
heapUsed: number;
|
|
@@ -579,6 +625,9 @@ export async function fetchServiceMemory(
|
|
|
579
625
|
data: {
|
|
580
626
|
pid: body.pid,
|
|
581
627
|
bunVersion: body.bunVersion,
|
|
628
|
+
// Allowlisted independently of the server: an unrecognized wire value is
|
|
629
|
+
// treated as absent rather than echoed into user-facing guidance.
|
|
630
|
+
bunRuntimeSource: BUN_RUNTIME_SOURCES.find(source => source === body.bunRuntimeSource),
|
|
582
631
|
platform: typeof body.platform === "string" ? body.platform : "unknown",
|
|
583
632
|
rss: body.rss,
|
|
584
633
|
heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
|
|
@@ -652,12 +701,22 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
|
|
|
652
701
|
} else {
|
|
653
702
|
lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
|
|
654
703
|
}
|
|
655
|
-
// Version-claiming (never binary-claiming): the endpoint cannot distinguish
|
|
656
|
-
// the bundled binary from an OPENCODEX_BUN_PATH override of the same version.
|
|
657
704
|
if (d.platform === "win32" && d.eagerRelay?.reason === "auto-known-bad") {
|
|
658
705
|
lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
|
|
659
|
-
|
|
660
|
-
|
|
706
|
+
// The remediation depends on how the SERVICE was launched, which only the
|
|
707
|
+
// launch-time marker can answer. Telling someone to set OPENCODEX_BUN_PATH
|
|
708
|
+
// when it is already set is the bug this branch exists to avoid (#848).
|
|
709
|
+
if (d.bunRuntimeSource === "override") {
|
|
710
|
+
lines.push(` OPENCODEX_BUN_PATH is already active for this service — the override runtime is itself an affected version (unvalidated — own risk).`);
|
|
711
|
+
lines.push(" Options: point the override at a different runtime, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
|
|
712
|
+
} else if (d.bunRuntimeSource === undefined) {
|
|
713
|
+
lines.push(" this service records no runtime origin (installed before provenance tracking), so OpenCodex cannot tell whether an override is already active.");
|
|
714
|
+
lines.push(" Reinstall the service to record it, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
|
|
715
|
+
} else {
|
|
716
|
+
const origin = d.bunRuntimeSource === "process" ? "the runtime that launched it" : "the bundled runtime";
|
|
717
|
+
lines.push(` the service is using ${origin}. Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),`);
|
|
718
|
+
lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
|
|
719
|
+
}
|
|
661
720
|
}
|
|
662
721
|
return lines;
|
|
663
722
|
}
|
|
@@ -671,11 +730,21 @@ export function proxyDownRestartHint(input: {
|
|
|
671
730
|
proxyRunning: boolean;
|
|
672
731
|
port: number;
|
|
673
732
|
serviceViable: boolean;
|
|
733
|
+
/** Absent means "unknown"; the hint then keeps its pre-repair wording. */
|
|
734
|
+
serviceInstalled?: boolean;
|
|
735
|
+
serviceConflict?: boolean;
|
|
674
736
|
}): string | null {
|
|
675
737
|
if (input.proxyRunning) return null;
|
|
738
|
+
// `serviceViable` alone conflates "no service at all" with "registered but stale or
|
|
739
|
+
// stopped". Only the first wants `install`: re-registering an existing service costs a
|
|
740
|
+
// UAC prompt on Windows and can switch a WinSW backend to Task Scheduler. A conflict
|
|
741
|
+
// still needs uninstall-then-install, which repairService() refuses outright.
|
|
742
|
+
const installedButBroken = input.serviceInstalled === true && input.serviceConflict !== true;
|
|
676
743
|
const restart = input.serviceViable
|
|
677
744
|
? "Restart it with 'ocx service start' (service installed) or 'ocx start'."
|
|
678
|
-
:
|
|
745
|
+
: installedButBroken
|
|
746
|
+
? "Restart it with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
747
|
+
: "Restart it with 'ocx start', or install the persistent service: 'ocx service install'.";
|
|
679
748
|
return `The ocx proxy is not running. Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
|
|
680
749
|
}
|
|
681
750
|
|
|
@@ -891,6 +960,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
891
960
|
proxyRunning: Boolean(live),
|
|
892
961
|
port: live?.port ?? doctorConfig.port ?? 10100,
|
|
893
962
|
serviceViable: startup.serviceViable,
|
|
963
|
+
serviceInstalled: startup.serviceInstalled,
|
|
964
|
+
serviceConflict: startup.serviceConflict,
|
|
894
965
|
});
|
|
895
966
|
if (proxyDown) hints.push(proxyDown);
|
|
896
967
|
for (const row of providerApiKeys) {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ocx export --client <
|
|
2
|
+
* `ocx export --client <id>` — print a client config for the live proxy.
|
|
3
|
+
*
|
|
4
|
+
* Six clients, four formats: opencode and Pi are JSON, Hermes and Gajae YAML,
|
|
5
|
+
* OpenClaw JSON5, Kimi TOML.
|
|
3
6
|
*
|
|
4
7
|
* Two consumers, one payload (devlog 260731_client_config_export/020):
|
|
5
8
|
*
|
|
6
|
-
* - **Agent** (`--json`): stdout is exactly the client config JSON and nothing else,
|
|
7
|
-
* `ocx export --client pi --json > models.json` is safe to pipe.
|
|
8
|
-
* including the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* - **Agent** (`--json`): stdout is exactly the client config as JSON and nothing else,
|
|
10
|
+
* so `ocx export --client pi --json > models.json` is safe to pipe. This is JSON for
|
|
11
|
+
* every client, including the YAML/JSON5/TOML ones — the flag is about machine
|
|
12
|
+
* readability, not the client's native format. Every diagnostic — including the
|
|
13
|
+
* `--out` write note — goes to stderr.
|
|
14
|
+
* - **Human** (no flag) and `--out`: the client's NATIVE serialization leads, then the
|
|
15
|
+
* destination path, the merge warning, the env export line, and the model/degraded
|
|
16
|
+
* counts.
|
|
11
17
|
*
|
|
12
18
|
* The command never writes the user's real config path. `--out` is an explicit target and
|
|
13
19
|
* refuses to clobber an existing file without `--force`, because the common mistake
|
|
@@ -15,14 +21,15 @@
|
|
|
15
21
|
*
|
|
16
22
|
* Serialization itself belongs to src/clients/config-export.ts; this module only resolves
|
|
17
23
|
* the base URL, filters the catalog, and renders. No secret is ever serialized: the config
|
|
18
|
-
* carries the client's documented env reference
|
|
24
|
+
* carries the client's documented env reference — or, for Kimi, which cannot hold one, a
|
|
25
|
+
* loopback placeholder — and the real key stays in the environment.
|
|
19
26
|
*/
|
|
20
27
|
import { writeFileSync } from "node:fs";
|
|
21
28
|
import { loadConfig } from "../config";
|
|
22
29
|
import {
|
|
23
30
|
EXPORT_CLIENTS,
|
|
24
31
|
EXPORT_CLIENT_IDS,
|
|
25
|
-
|
|
32
|
+
buildClientConfigText,
|
|
26
33
|
isExportClientId,
|
|
27
34
|
opencodeProxyBaseUrl,
|
|
28
35
|
type ExportClientId,
|
|
@@ -164,16 +171,23 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
164
171
|
throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
|
|
165
172
|
}
|
|
166
173
|
const models = exportModelsFromProxyRows(rows, config);
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
// The text is the client's OWN format — YAML, TOML and JSON5 clients would
|
|
175
|
+
// otherwise receive a JSON rendering their parser reads differently.
|
|
176
|
+
const built = buildClientConfigText(client, { baseUrl: proxyV1BaseUrl(root), models, config });
|
|
177
|
+
const clientConfig = built.document;
|
|
178
|
+
const text = built.text;
|
|
169
179
|
|
|
170
|
-
|
|
180
|
+
// Every serializer already ends with exactly one newline.
|
|
181
|
+
if (out !== undefined) writeExport(out, text, force);
|
|
171
182
|
// stderr, so `--json` stdout stays byte-exact for a redirect.
|
|
172
183
|
if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);
|
|
173
184
|
|
|
174
185
|
const degraded = models.filter(model => !hasContextLimit(model)).length;
|
|
186
|
+
// `--json` keeps emitting the DOCUMENT at the top level: a script that
|
|
187
|
+
// pipes it into a config file must not have to unwrap an envelope we added
|
|
188
|
+
// for our own convenience. Format metadata rides in the human lines below.
|
|
175
189
|
printData(clientConfig, wantsJson, [
|
|
176
|
-
text,
|
|
190
|
+
text.trimEnd(),
|
|
177
191
|
"",
|
|
178
192
|
...(out !== undefined ? [`Wrote ${out}`] : []),
|
|
179
193
|
`Destination: ${spec.destination(process.env)}`,
|
package/src/cli/help.ts
CHANGED
|
@@ -110,7 +110,7 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
110
110
|
],
|
|
111
111
|
},
|
|
112
112
|
account: {
|
|
113
|
-
usage: "ocx account <list|current|use|refresh|auto-switch|login|reauth|code|cancel|remove|add-key|reset-credits> ...",
|
|
113
|
+
usage: "ocx account <list|current|use|refresh|auto-switch|login|reauth|code|cancel|remove|add-key|reset-credits|main> ...",
|
|
114
114
|
summary: "List and switch provider accounts and API-key pools (GUI parity).",
|
|
115
115
|
details: [
|
|
116
116
|
"list [provider] Codex account pool, OAuth accounts and API keys (identifiers shown masked as the API returns them).",
|
|
@@ -122,6 +122,7 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
122
122
|
"add-key <provider> [--label <label>] Add a key read only from piped stdin.",
|
|
123
123
|
"login/reauth/code/cancel Run browser or manual-code auth from a headless shell.",
|
|
124
124
|
"reset-credits <id|main> [--consume --yes] Inspect or consume Codex reset credits.",
|
|
125
|
+
"main <subcommand> Manage the physical native Codex login separately from Pool routing.",
|
|
125
126
|
"Codex pool selection applies to the next request after clearing existing affinity; in-flight requests keep their captured account.",
|
|
126
127
|
],
|
|
127
128
|
},
|
|
@@ -170,17 +171,17 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
170
171
|
},
|
|
171
172
|
"api-key": { usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
172
173
|
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.",
|
|
174
|
+
usage: "ocx export --client <opencode|pi|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
|
|
175
|
+
summary: "Print a client config (opencode, Pi, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.",
|
|
175
176
|
details: [
|
|
176
177
|
"--json prints only the config JSON on stdout, so it is safe to redirect to a file.",
|
|
177
178
|
"--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 config never contains a key; it references the client's env var, which you export before launching. Kimi cannot hold an env reference, so it carries a loopback placeholder instead.",
|
|
179
180
|
"The destination path is printed for merging by hand — ocx never writes your real client config.",
|
|
180
181
|
],
|
|
181
182
|
},
|
|
182
183
|
grok: { usage: "ocx grok <status|exclude|include|set|clear|apply> ...", summary: "Manage and apply the Grok Build model fence." },
|
|
183
|
-
integration: { usage: "ocx integration <claude|grok> ...", summary: "Manage supported client integrations." },
|
|
184
|
+
integration: { usage: "ocx integration <claude|grok|client> ...", summary: "Manage supported client integrations." },
|
|
184
185
|
system: {
|
|
185
186
|
usage: "ocx system <status|settings|startup|diagnostics|sync|update> ...",
|
|
186
187
|
summary: "Manage headless runtime settings, startup, sync, diagnostics, and updates.",
|
|
@@ -296,7 +297,8 @@ Usage:
|
|
|
296
297
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
297
298
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
298
299
|
ocx access <sub> External API keys and endpoint information
|
|
299
|
-
ocx export --client <id> Print
|
|
300
|
+
ocx export --client <id> Print a client config wired to the running proxy (6 clients)
|
|
301
|
+
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
300
302
|
ocx grok <sub> Grok Build model selection and apply
|
|
301
303
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
|
302
304
|
ocx config <sub> Validated configuration show/get/set/import/export
|
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
3
|
+
import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
4
4
|
import { stripGrokConfig } from "../grok/inject";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveCodexHistoryJobTarget, runCodexHistoryJob } from "../codex/history-job";
|
|
6
6
|
import { reconcileJournal } from "../codex/journal";
|
|
7
7
|
import {
|
|
8
8
|
codexAutoStartEnabled,
|
|
@@ -41,9 +41,11 @@ import { maybeShowStarPrompt } from "./star-prompt";
|
|
|
41
41
|
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
42
42
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
43
43
|
import { syncModelsToCodex } from "../codex/sync";
|
|
44
|
+
import { shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
|
|
44
45
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
45
46
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
46
47
|
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
48
|
+
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
47
49
|
|
|
48
50
|
const args = process.argv.slice(2);
|
|
49
51
|
const command = args[0];
|
|
@@ -315,7 +317,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
315
317
|
installShellHook();
|
|
316
318
|
|
|
317
319
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
318
|
-
await
|
|
320
|
+
await syncCodexOnStartIfEnabled(port, config);
|
|
319
321
|
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
320
322
|
historyGuardian = startHistoryMigrationGuardian();
|
|
321
323
|
}
|
|
@@ -335,7 +337,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
335
337
|
// absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
|
|
336
338
|
// Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
|
|
337
339
|
// failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
|
|
338
|
-
|
|
340
|
+
//
|
|
341
|
+
// Gated on the persisted switch: without this, turning Grok off lasted exactly
|
|
342
|
+
// one restart, because the toggle removed the fence and start wrote it back.
|
|
343
|
+
if (shouldSyncGrokOnStart(config)) try {
|
|
339
344
|
const { syncGrokConfig } = await import("../grok/sync");
|
|
340
345
|
const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
341
346
|
if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
@@ -385,7 +390,7 @@ async function handleEnsure() {
|
|
|
385
390
|
detached: true,
|
|
386
391
|
stdio: "ignore",
|
|
387
392
|
windowsHide: true,
|
|
388
|
-
env: { ...process.env, OCX_SERVICE: "1" },
|
|
393
|
+
env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
|
|
389
394
|
});
|
|
390
395
|
child.unref();
|
|
391
396
|
|
|
@@ -427,7 +432,7 @@ async function handleTrayProxyStart(): Promise<void> {
|
|
|
427
432
|
detached: true,
|
|
428
433
|
stdio: "ignore",
|
|
429
434
|
windowsHide: true,
|
|
430
|
-
env: { ...process.env, OCX_SERVICE: "1" },
|
|
435
|
+
env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
|
|
431
436
|
});
|
|
432
437
|
child.unref();
|
|
433
438
|
},
|
|
@@ -524,7 +529,7 @@ async function handleStop() {
|
|
|
524
529
|
}
|
|
525
530
|
}
|
|
526
531
|
if (!ownershipBlocked) {
|
|
527
|
-
const r =
|
|
532
|
+
const r = await restoreNativeCodexAsync();
|
|
528
533
|
if (r.success) console.log(`↩️ ${r.message}`);
|
|
529
534
|
else {
|
|
530
535
|
stopFailed = true;
|
|
@@ -586,8 +591,8 @@ async function handleUninstall() {
|
|
|
586
591
|
});
|
|
587
592
|
}
|
|
588
593
|
|
|
589
|
-
await runStep("native Codex restored", () => {
|
|
590
|
-
const r =
|
|
594
|
+
await runStep("native Codex restored", async () => {
|
|
595
|
+
const r = await restoreNativeCodexAsync();
|
|
591
596
|
if (!r.success) throw new Error(r.message);
|
|
592
597
|
});
|
|
593
598
|
|
|
@@ -659,7 +664,14 @@ async function handleStatus() {
|
|
|
659
664
|
console.log(` Health: ${status.healthLabel}`);
|
|
660
665
|
if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
|
|
661
666
|
console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
|
|
662
|
-
|
|
667
|
+
// The service summary a few lines below already tells a registered-but-not-serving
|
|
668
|
+
// user to repair. Printing "install the persistent service" unconditionally
|
|
669
|
+
// contradicted it in the same report, and install re-registers: UAC on Windows and a
|
|
670
|
+
// possible WinSW-to-scheduler switch for someone who already has a service.
|
|
671
|
+
const installed = status.json.startup.serviceInstalled && !status.json.startup.serviceConflict;
|
|
672
|
+
console.log(installed
|
|
673
|
+
? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
674
|
+
: " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
|
663
675
|
}
|
|
664
676
|
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
665
677
|
console.log(` Config: ${status.json.paths.config}`);
|
|
@@ -707,13 +719,22 @@ async function handleStatus() {
|
|
|
707
719
|
}
|
|
708
720
|
}
|
|
709
721
|
|
|
710
|
-
function handleRecoverHistory() {
|
|
722
|
+
async function handleRecoverHistory() {
|
|
711
723
|
if (args[1] !== "--legacy-openai") {
|
|
712
724
|
console.error("Usage: ocx recover-history --legacy-openai");
|
|
713
725
|
console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
|
|
714
726
|
process.exit(1);
|
|
715
727
|
}
|
|
716
|
-
|
|
728
|
+
// Manifest-independent legacy ejection, serialized like every other history
|
|
729
|
+
// mutation. It is a separate operation from generic restore precisely because
|
|
730
|
+
// it must not read, consume or replace the backup manifest.
|
|
731
|
+
const outcome = await runCodexHistoryJob({
|
|
732
|
+
...resolveCodexHistoryJobTarget(),
|
|
733
|
+
operation: "recover-legacy-openai",
|
|
734
|
+
});
|
|
735
|
+
const r = outcome.kind === "converged"
|
|
736
|
+
? { rows: outcome.rows, files: outcome.files, failed: undefined }
|
|
737
|
+
: { rows: 0, files: 0, failed: true as const };
|
|
717
738
|
if (r.failed) {
|
|
718
739
|
console.error(
|
|
719
740
|
"⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
|
|
@@ -764,7 +785,7 @@ switch (command) {
|
|
|
764
785
|
}
|
|
765
786
|
let r: { success: boolean; message: string };
|
|
766
787
|
try {
|
|
767
|
-
r =
|
|
788
|
+
r = await restoreNativeCodexAsync();
|
|
768
789
|
} catch (err) {
|
|
769
790
|
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
770
791
|
}
|
|
@@ -789,7 +810,7 @@ switch (command) {
|
|
|
789
810
|
break;
|
|
790
811
|
}
|
|
791
812
|
case "recover-history":
|
|
792
|
-
handleRecoverHistory();
|
|
813
|
+
await handleRecoverHistory();
|
|
793
814
|
break;
|
|
794
815
|
case "uninstall":
|
|
795
816
|
case "remove":
|
|
@@ -847,9 +868,14 @@ switch (command) {
|
|
|
847
868
|
}
|
|
848
869
|
case "sync-cache": {
|
|
849
870
|
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
850
|
-
const {
|
|
871
|
+
const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
|
|
872
|
+
const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
|
|
873
|
+
const { getCodexHome } = await import("../codex/paths");
|
|
874
|
+
const owningCodexHome = getCodexHome();
|
|
875
|
+
const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
876
|
+
invalidateCodexModelsCacheWithPermit(permit, owningCodexHome));
|
|
851
877
|
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
852
|
-
if (
|
|
878
|
+
if (invalidated.kind === "completed" && invalidated.value) {
|
|
853
879
|
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
854
880
|
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
855
881
|
}
|
|
@@ -867,7 +893,7 @@ switch (command) {
|
|
|
867
893
|
detached: true,
|
|
868
894
|
stdio: "ignore",
|
|
869
895
|
windowsHide: true,
|
|
870
|
-
env: process.env,
|
|
896
|
+
env: withProcessRuntimeProvenance(process.env),
|
|
871
897
|
});
|
|
872
898
|
child.unref();
|
|
873
899
|
live = await waitForProxy();
|
|
@@ -999,13 +1025,18 @@ switch (command) {
|
|
|
999
1025
|
break;
|
|
1000
1026
|
}
|
|
1001
1027
|
case "route": {
|
|
1002
|
-
if (args[1] !== "combo") {
|
|
1003
|
-
console.error("Usage: ocx route combo <subcommand>");
|
|
1028
|
+
if (args[1] !== "combo" && args[1] !== "policy") {
|
|
1029
|
+
console.error("Usage: ocx route <combo|policy> <subcommand>");
|
|
1004
1030
|
process.exitCode = 2;
|
|
1005
1031
|
break;
|
|
1006
1032
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1033
|
+
if (args[1] === "combo") {
|
|
1034
|
+
const { handleComboCommand } = await import("./combo");
|
|
1035
|
+
process.exitCode = await handleComboCommand(args.slice(2));
|
|
1036
|
+
} else {
|
|
1037
|
+
const { handleRoutePolicyCommand } = await import("./route-policy");
|
|
1038
|
+
process.exitCode = await handleRoutePolicyCommand(args.slice(2));
|
|
1039
|
+
}
|
|
1009
1040
|
break;
|
|
1010
1041
|
}
|
|
1011
1042
|
case "agent": {
|
|
@@ -1054,8 +1085,11 @@ switch (command) {
|
|
|
1054
1085
|
} else if (integration === "claude") {
|
|
1055
1086
|
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
1056
1087
|
process.exitCode = await handleClaudeConfigCommand(args.slice(2));
|
|
1088
|
+
} else if (integration === "client") {
|
|
1089
|
+
const { handleClientIntegrationCommand } = await import("./integrations");
|
|
1090
|
+
process.exitCode = await handleClientIntegrationCommand(args.slice(2));
|
|
1057
1091
|
} else {
|
|
1058
|
-
console.error("Usage: ocx integration <claude|grok> <subcommand>");
|
|
1092
|
+
console.error("Usage: ocx integration <claude|grok|client> <subcommand>");
|
|
1059
1093
|
process.exitCode = 2;
|
|
1060
1094
|
}
|
|
1061
1095
|
break;
|
package/src/cli/integrations.ts
CHANGED
|
@@ -27,6 +27,12 @@ const GROK_USAGE = `Usage:
|
|
|
27
27
|
ocx grok clear [--json]
|
|
28
28
|
ocx grok apply [--json]`;
|
|
29
29
|
|
|
30
|
+
const CLIENT_USAGE = `Usage:
|
|
31
|
+
ocx integration client [status] [--client <id>] [--json]
|
|
32
|
+
ocx integration client <enable|disable> --client <id> [--json]
|
|
33
|
+
ocx integration client history [--client <id>] [--json]
|
|
34
|
+
ocx integration client restore --op <opId> [--confirm-drift] [--json]`;
|
|
35
|
+
|
|
30
36
|
function parseMap(raw: string): Record<string, string> {
|
|
31
37
|
if (raw === "-") return {};
|
|
32
38
|
const map: Record<string, string> = {};
|
|
@@ -139,4 +145,81 @@ export async function handleGrokCommand(argv: string[], deps: RuntimeApiDeps = {
|
|
|
139
145
|
});
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
|
|
148
|
+
/**
|
|
149
|
+
* The headless half of the client-integration toggle.
|
|
150
|
+
*
|
|
151
|
+
* Every safety property lives behind the management API — ownership, the
|
|
152
|
+
* pre-write snapshot, the journal, the drift refusal — so this command is a
|
|
153
|
+
* thin caller and deliberately re-implements none of it. That is also why
|
|
154
|
+
* `restore` surfaces the drift refusal as an error telling the user to pass
|
|
155
|
+
* `--confirm-drift` rather than retrying on their behalf: replacing edits a
|
|
156
|
+
* user made after the snapshot is exactly the decision they have to make.
|
|
157
|
+
*/
|
|
158
|
+
export async function handleClientIntegrationCommand(
|
|
159
|
+
argv: string[],
|
|
160
|
+
deps: RuntimeApiDeps = {},
|
|
161
|
+
): Promise<number> {
|
|
162
|
+
return runCliAction(async () => {
|
|
163
|
+
const args = [...argv];
|
|
164
|
+
const action = (args.shift() ?? "status").toLowerCase();
|
|
165
|
+
const wantsJson = takeFlag(args, "--json");
|
|
166
|
+
|
|
167
|
+
if (action === "status" || action === "show" || action === "list") {
|
|
168
|
+
const client = takeOption(args, "--client");
|
|
169
|
+
rejectArgs(args, CLIENT_USAGE);
|
|
170
|
+
const path = client
|
|
171
|
+
? `/api/client-integrations/${encodeURIComponent(client)}`
|
|
172
|
+
: "/api/client-integrations";
|
|
173
|
+
const result = await runtimeRequest(path, {}, deps);
|
|
174
|
+
const rows = (result as { clients?: Array<Record<string, unknown>> }).clients;
|
|
175
|
+
printData(result, wantsJson, rows
|
|
176
|
+
? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
|
|
177
|
+
: summaryLines(result));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (action === "history" || action === "journal") {
|
|
182
|
+
const client = takeOption(args, "--client");
|
|
183
|
+
rejectArgs(args, CLIENT_USAGE);
|
|
184
|
+
const query = client ? `?client=${encodeURIComponent(client)}` : "";
|
|
185
|
+
const result = await runtimeRequest(`/api/client-integrations/journal${query}`, {}, deps);
|
|
186
|
+
const operations = (result as { operations?: Array<Record<string, unknown>> }).operations ?? [];
|
|
187
|
+
printData(result, wantsJson, operations.length === 0
|
|
188
|
+
? ["No integration operations recorded yet."]
|
|
189
|
+
: operations.map(row => {
|
|
190
|
+
// `snapshot` is resolved against the disk by the route, so "expired"
|
|
191
|
+
// here means the bytes are genuinely gone, not merely old.
|
|
192
|
+
const backup = row.snapshot === "expired" ? "backup expired" : `op ${String(row.opId)}`;
|
|
193
|
+
return `${String(row.at)} ${String(row.clientId)} ${String(row.kind)} (${backup})`;
|
|
194
|
+
}));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (action === "restore") {
|
|
199
|
+
const opId = takeOption(args, "--op") ?? takeOption(args, "--op-id");
|
|
200
|
+
const confirmDrift = takeFlag(args, "--confirm-drift");
|
|
201
|
+
rejectArgs(args, CLIENT_USAGE);
|
|
202
|
+
if (!opId) throw new CliUsageError("--op <opId> is required", CLIENT_USAGE);
|
|
203
|
+
const result = await runtimeRequest("/api/client-integrations/restore", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: JSON.stringify({ opId, confirmDrift }),
|
|
206
|
+
}, deps);
|
|
207
|
+
printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? "Restored.")]);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (action !== "enable" && action !== "disable") {
|
|
212
|
+
throw new CliUsageError(`unknown client integration command ${action}`, CLIENT_USAGE);
|
|
213
|
+
}
|
|
214
|
+
const client = takeOption(args, "--client");
|
|
215
|
+
rejectArgs(args, CLIENT_USAGE);
|
|
216
|
+
if (!client) throw new CliUsageError("--client <id> is required", CLIENT_USAGE);
|
|
217
|
+
const result = await runtimeRequest(`/api/client-integrations/${encodeURIComponent(client)}`, {
|
|
218
|
+
method: "PUT",
|
|
219
|
+
body: JSON.stringify({ enabled: action === "enable" }),
|
|
220
|
+
}, deps);
|
|
221
|
+
printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? `${client} ${action}d.`)]);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const INTEGRATION_USAGE = { claude: CLAUDE_USAGE, grok: GROK_USAGE, client: CLIENT_USAGE };
|