@bitkyc08/opencodex 2.77.0 → 2.78.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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ import { homedir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
|
|
10
10
|
import { packageVersion } from "../lib/package-version";
|
|
11
|
+
import { admitUpdateRestartChild } from "./update-restart-child";
|
|
12
|
+
import { UpdateRestartRequired } from "./update-restart-candidate";
|
|
13
|
+
import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
|
|
11
14
|
import { findGuiDist } from "../server/gui-static";
|
|
12
15
|
import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
|
|
13
16
|
|
|
@@ -85,9 +88,9 @@ import {
|
|
|
85
88
|
runProxyRestart,
|
|
86
89
|
runTrayProxyStart,
|
|
87
90
|
type ProxyRestartLive,
|
|
88
|
-
type ProxyRestartResult,
|
|
89
91
|
type ProxyRestartStartOutcome,
|
|
90
92
|
} from "./tray-proxy";
|
|
93
|
+
import { reportRestartFailure } from "./restart-failure";
|
|
91
94
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
92
95
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
93
96
|
import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
|
|
@@ -142,6 +145,7 @@ import { honorSiblingMarker, markSiblingStart, siblingOfLivePort, siblingRuntime
|
|
|
142
145
|
import { consumeSiblingHandoff } from "../codex/sibling-handoff";
|
|
143
146
|
import {
|
|
144
147
|
reconcileClientStartupBeforeReady,
|
|
148
|
+
syncCodexBeforeCatalogObservation,
|
|
145
149
|
syncClaudeAgentDefsAtProxyStartup,
|
|
146
150
|
} from "./claude-agent-startup-sync";
|
|
147
151
|
import {
|
|
@@ -200,6 +204,7 @@ async function refreshOwnedRaycastCatalog(
|
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
const updateRestartChild = admitUpdateRestartChild(process.argv.slice(2));
|
|
203
208
|
initializeNodeLauncherContext();
|
|
204
209
|
// The compiled executable is also the capture-only MCP server's launcher.
|
|
205
210
|
// Handle this private entrypoint before CLI preflight or command dispatch.
|
|
@@ -371,6 +376,7 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
379
|
+
updateRestartChild?.check();
|
|
374
380
|
// A supervised service child defers to a foreign recorded owner before doing
|
|
375
381
|
// anything else. 'ocx service start' refuses this activation path already, but
|
|
376
382
|
// the process managers below it — the Windows boot wrapper's restart loop and
|
|
@@ -466,6 +472,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
466
472
|
},
|
|
467
473
|
});
|
|
468
474
|
|
|
475
|
+
updateRestartChild?.check();
|
|
469
476
|
const clientState = readClientConnectionState();
|
|
470
477
|
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
471
478
|
throw new Error(`client startup refused: ${clientState.reason}`);
|
|
@@ -550,7 +557,17 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
550
557
|
let server: ReturnType<typeof serverModule.startServer>;
|
|
551
558
|
for (let attempt = 0; ; attempt++) {
|
|
552
559
|
try {
|
|
560
|
+
if (updateRestartChild && siblingStart) throw new Error("update_restart_sibling_refused");
|
|
561
|
+
updateRestartChild?.check(port, loadConfig().hostname ?? "");
|
|
553
562
|
server = serverModule.startServer(port, { localAttestationSecret, readinessGate });
|
|
563
|
+
if (updateRestartChild) {
|
|
564
|
+
try { updateRestartChild.check(port, server.hostname); }
|
|
565
|
+
catch (error) {
|
|
566
|
+
try { await server.stop(true); }
|
|
567
|
+
catch (rollback) { throw new StartOwnershipRollbackUncertainError([error, rollback]); }
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
554
571
|
break;
|
|
555
572
|
} catch (err) {
|
|
556
573
|
try { await serverModule.waitForFailedStartRollback(err); }
|
|
@@ -578,14 +595,14 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
578
595
|
}
|
|
579
596
|
return { server, serverModule, port, readinessGate, localAttestationSecret, config };
|
|
580
597
|
},
|
|
581
|
-
writePid: () => writePid(process.pid),
|
|
582
|
-
writeRuntime: bound => writeRuntimePort({
|
|
598
|
+
writePid: () => { updateRestartChild?.check(); writePid(process.pid); },
|
|
599
|
+
writeRuntime: bound => { updateRestartChild?.check(bound.port, bound.config.hostname ?? ""); writeRuntimePort({
|
|
583
600
|
pid: process.pid,
|
|
584
601
|
port: bound.port,
|
|
585
602
|
hostname: bound.config.hostname,
|
|
586
603
|
attestationSecret: bound.localAttestationSecret,
|
|
587
604
|
...siblingRuntimeField(),
|
|
588
|
-
}),
|
|
605
|
+
}); updateRestartChild?.complete(); },
|
|
589
606
|
stopBound: bound => bound.server.stop(true),
|
|
590
607
|
removeRuntime: () => removeRuntimePortIfPidIs(process.pid),
|
|
591
608
|
removePid: () => removePidIfValueIs(process.pid),
|
|
@@ -615,12 +632,14 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
615
632
|
// Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
|
|
616
633
|
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
617
634
|
let routingHealer: { stop(): void } | undefined; // routing-healer.ts; stopped first in syncCleanup
|
|
635
|
+
let catalogHealer: { stop(): void } | undefined;
|
|
618
636
|
|
|
619
637
|
let cleaned = false;
|
|
620
638
|
let cleanupSucceeded = true;
|
|
621
639
|
const syncCleanup = () => {
|
|
622
640
|
if (cleaned) return cleanupSucceeded;
|
|
623
641
|
cleaned = true;
|
|
642
|
+
try { catalogHealer?.stop(); } catch { /* best-effort */ }
|
|
624
643
|
try { routingHealer?.stop(); } catch { /* best-effort */ }
|
|
625
644
|
try { guardian.stop(); } catch { /* best-effort */ }
|
|
626
645
|
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
@@ -703,9 +722,14 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
703
722
|
// deferred until the best-effort Claude roster and Desktop registry settle. This
|
|
704
723
|
// keeps /readyz closed across startup initialization without making an optional
|
|
705
724
|
// Claude integration failure prevent the proxy from starting.
|
|
725
|
+
const catalogHealerModule = siblingStart ? null : await import("../codex/catalog-self-heal");
|
|
706
726
|
const startupSync = await reconcileClientStartupBeforeReady(
|
|
707
727
|
readinessGate,
|
|
708
|
-
gate =>
|
|
728
|
+
gate => syncCodexBeforeCatalogObservation(gate,
|
|
729
|
+
forwarding => syncCodexOnStartIfEnabled(port, config, undefined, forwarding),
|
|
730
|
+
() => {
|
|
731
|
+
if (catalogHealerModule && !siblingStart && !cleaned) catalogHealer = catalogHealerModule.startCodexCatalogSelfHeal({ port });
|
|
732
|
+
}),
|
|
709
733
|
() => systemEnv.injected
|
|
710
734
|
? Promise.resolve(null)
|
|
711
735
|
: syncClaudeAgentDefsAtProxyStartup(config, port),
|
|
@@ -895,28 +919,6 @@ const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIM
|
|
|
895
919
|
|
|
896
920
|
/** Reserve confirmation time within the shared restart deadline. */
|
|
897
921
|
const RESTART_REOBSERVE_RESERVE_MS = 10_000;
|
|
898
|
-
function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
|
|
899
|
-
if (result.phase === "identity") {
|
|
900
|
-
console.error("❌ Refusing to restart because the running proxy identity could not be attested.");
|
|
901
|
-
} else if (result.phase === "request") {
|
|
902
|
-
const code = result.error instanceof Error ? result.error.message : "";
|
|
903
|
-
if (code === "restart_capability_unsupported") {
|
|
904
|
-
console.error("❌ The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
|
|
905
|
-
console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
|
|
906
|
-
} else if (code === "restart_version_skew") {
|
|
907
|
-
console.error("❌ The running proxy reports a different OpenCodex version than this CLI; restarting in place would respawn the old installation.");
|
|
908
|
-
console.error(" Run `ocx stop` and then `ocx start` from this installation instead.");
|
|
909
|
-
} else if (code === "restart_package_tree_unsettled") {
|
|
910
|
-
console.error("❌ The proxy's package files are still being replaced; wait for the install to finish, then run `ocx restart` again.");
|
|
911
|
-
} else {
|
|
912
|
-
console.error("❌ Proxy restart request could not be confirmed; no fallback stop/start was attempted.");
|
|
913
|
-
}
|
|
914
|
-
} else if (result.phase === "replacement") {
|
|
915
|
-
console.error("❌ Proxy restart was accepted, but no identity-verified replacement became healthy in time.");
|
|
916
|
-
} else {
|
|
917
|
-
console.error("❌ Proxy was not running and the fallback start did not become healthy.");
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
922
|
async function handleProxyRestart(
|
|
921
923
|
startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
|
|
922
924
|
): Promise<boolean> {
|
|
@@ -939,6 +941,15 @@ async function handleProxyRestart(
|
|
|
939
941
|
findLive: () => findLiveProxy({ deadlineAt: end, attempts: 2, acceptPackageTreeFenced: true }), expired: () => Date.now() >= end,
|
|
940
942
|
})),
|
|
941
943
|
});
|
|
944
|
+
if (!result.ok && result.phase === "request" && result.error instanceof UpdateRestartRequired) {
|
|
945
|
+
const candidate = result.error.candidate();
|
|
946
|
+
console.log(`🔄 Running proxy ${candidate.target.version} is older than this CLI (${candidate.cliVersion}); restarting it from the current installation...`);
|
|
947
|
+
const update = await restartFromCurrentInstallation(candidate, deadlineAt, detachedStartEnvironment());
|
|
948
|
+
if (update.ok) console.log(`✅ Proxy updated to ${update.live.version} (PID ${update.live.pid}).`);
|
|
949
|
+
else console.error(`❌ ${describeUpdateRestartFailure(update.code, update.reason)} (${update.code})`);
|
|
950
|
+
process.exitCode = update.ok ? 0 : 1;
|
|
951
|
+
return update.ok;
|
|
952
|
+
}
|
|
942
953
|
if (!result.ok) reportRestartFailure(result);
|
|
943
954
|
process.exitCode = result.ok ? 0 : 1;
|
|
944
955
|
return result.ok;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { printData } from "./runtime-api";
|
|
2
|
+
import { ObservationError, withObserveStream, type ObserveStreamDeps } from "./observe-stream";
|
|
3
|
+
import type { DebugLogEntry } from "../lib/debug-log-buffer";
|
|
4
|
+
|
|
5
|
+
function parseRows(body: unknown, after: number, limit: number): DebugLogEntry[] {
|
|
6
|
+
if (!Array.isArray(body) || body.length > limit) throw new ObservationError("Invalid injection log window. Restart follow after checking the runtime.");
|
|
7
|
+
let previous = after;
|
|
8
|
+
return body.map(value => {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ObservationError("Invalid injection log row.");
|
|
10
|
+
const { seq, at, line } = value as Record<string, unknown>;
|
|
11
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq <= previous || seq <= 0
|
|
12
|
+
|| typeof at !== "number" || !Number.isFinite(at) || typeof line !== "string") {
|
|
13
|
+
throw new ObservationError("Invalid injection log order or row. Restart follow after checking the runtime.");
|
|
14
|
+
}
|
|
15
|
+
previous = seq;
|
|
16
|
+
return { seq, at, line };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function followInjection(limit: number, jsonl: boolean, deps: ObserveStreamDeps): Promise<number> {
|
|
21
|
+
return withObserveStream(deps, async stream => {
|
|
22
|
+
let after = 0;
|
|
23
|
+
while (true) {
|
|
24
|
+
const query = new URLSearchParams({ limit: String(limit), after: String(after) });
|
|
25
|
+
const rows = parseRows(await stream.get("/api/debug/injection-logs", query), after, limit);
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
stream.signal.throwIfAborted();
|
|
28
|
+
if (jsonl) console.log(JSON.stringify(row));
|
|
29
|
+
else printData(row, false, [`${row.seq} ${row.at} ${row.line}`]);
|
|
30
|
+
after = row.seq;
|
|
31
|
+
}
|
|
32
|
+
await stream.wait();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/cli/inspect.ts
CHANGED
|
@@ -35,6 +35,7 @@ const USAGE = `Usage:
|
|
|
35
35
|
ocx inspect windows-tray [--json]
|
|
36
36
|
ocx integration native [list] [--json]
|
|
37
37
|
ocx integration native <claude|claude-desktop|codex|grok> <on|off> [--json]
|
|
38
|
+
ocx integration native cursor [status|local-installer] [--json]
|
|
38
39
|
ocx agent request-user-input [on|off] [--json]`;
|
|
39
40
|
|
|
40
41
|
/** A read that takes no arguments beyond `--json`. */
|
|
@@ -220,6 +221,10 @@ export async function handleInspectCommand(argv: string[], deps: RuntimeApiDeps
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
export async function handleIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
224
|
+
if (argv[0] === "native" && argv[1] === "cursor") {
|
|
225
|
+
const { handleCursorIntegrationCommand } = await import("./integration-cursor");
|
|
226
|
+
return handleCursorIntegrationCommand(argv.slice(2), deps);
|
|
227
|
+
}
|
|
223
228
|
return runCliAction(async () => {
|
|
224
229
|
const [sub, ...rest] = argv;
|
|
225
230
|
if (sub === "native" || sub === undefined) await nativeIntegration(sub === undefined ? [] : rest, deps);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { redactUserPath } from "../lib/redact";
|
|
3
|
+
import type { RefusalReason } from "../integrations/mutation-plan";
|
|
4
|
+
import { refreshAsideProfilesThroughServer } from "./aside-profiles";
|
|
5
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
6
|
+
import { CliUsageError, RuntimeApiError, printData, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
|
|
7
|
+
|
|
8
|
+
const USAGE = "Usage: ocx integration client sync --client aside [--json]";
|
|
9
|
+
const refusalText = {
|
|
10
|
+
not_installed: "Aside is not installed for this profile.",
|
|
11
|
+
conflict: "Profile configuration conflicts with the managed integration.",
|
|
12
|
+
unsafe: "Profile configuration cannot be changed safely.",
|
|
13
|
+
non_loopback: "The integration requires a loopback destination.",
|
|
14
|
+
superseded_store: "The client now uses a different configuration store.",
|
|
15
|
+
drift_requires_confirm: "Profile configuration changed; inspect it before an explicit restore.",
|
|
16
|
+
snapshot_expired: "The recovery snapshot has expired.",
|
|
17
|
+
write_failed: "The profile write failed. Inspect its state before retrying.",
|
|
18
|
+
} satisfies Record<RefusalReason, string>;
|
|
19
|
+
const profileId = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
20
|
+
const text = z.string().max(32768);
|
|
21
|
+
const resultSchema = z.discriminatedUnion("ok", [
|
|
22
|
+
z.object({ client: z.literal("aside"), profileId, ok: z.literal(true), changed: z.boolean(),
|
|
23
|
+
reason: text.optional().transform(value => value === undefined ? undefined
|
|
24
|
+
: value === "managed block is absent; refresh did not reconnect it"
|
|
25
|
+
? "Managed block remains absent; synchronization did not reconnect it."
|
|
26
|
+
: "Profile synchronization returned a notice; inspect integration status."),
|
|
27
|
+
}),
|
|
28
|
+
z.object({ client: z.literal("aside"), profileId, ok: z.literal(false),
|
|
29
|
+
state: z.enum(["absent", "current", "stale", "conflict", "unsafe"]),
|
|
30
|
+
refusalReason: z.enum(["not_installed", "conflict", "unsafe", "non_loopback", "superseded_store", "drift_requires_confirm", "snapshot_expired", "write_failed"]),
|
|
31
|
+
reason: text.optional(), residual: z.boolean().optional(),
|
|
32
|
+
snapshotPath: text.min(1).transform(redactUserPath).optional(),
|
|
33
|
+
}).transform(row => ({ ...row, reason: refusalText[row.refusalReason] })),
|
|
34
|
+
]);
|
|
35
|
+
const resultsSchema = z.array(resultSchema).refine(rows => new Set(rows.map(row => row.profileId)).size === rows.length);
|
|
36
|
+
|
|
37
|
+
/** Narrow owner seam: production always uses the established attested exchange. */
|
|
38
|
+
export interface AsideSyncCliDeps extends RuntimeApiDeps {
|
|
39
|
+
refreshAsideProfilesImpl?: typeof refreshAsideProfilesThroughServer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** argv begins after sync. No local writer or base-URL pre-resolution. */
|
|
43
|
+
export function handleIntegrationAsideSync(argv: string[], deps: AsideSyncCliDeps = {}): Promise<number> {
|
|
44
|
+
return runCatalogAction(async () => {
|
|
45
|
+
const args = [...argv];
|
|
46
|
+
const wantsJson = takeFlag(args, "--json");
|
|
47
|
+
const client = takeOption(args, "--client");
|
|
48
|
+
if (args.length || client !== "aside") throw new CliUsageError("Sync requires --client aside and accepts only --json", USAGE);
|
|
49
|
+
// Sync is an aggregate operation: every owner/transport failure is exit 1,
|
|
50
|
+
// not the record-not-found/conflict exits used by addressed journal writes.
|
|
51
|
+
const outcomes = await (deps.refreshAsideProfilesImpl ?? refreshAsideProfilesThroughServer)(deps)
|
|
52
|
+
.catch(error => {
|
|
53
|
+
if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
|
|
54
|
+
throw new Error("Aside synchronization did not complete");
|
|
55
|
+
});
|
|
56
|
+
const parsed = resultsSchema.safeParse(outcomes);
|
|
57
|
+
if (!parsed.success) throw new Error("Invalid Aside synchronization outcome");
|
|
58
|
+
const results = parsed.data;
|
|
59
|
+
const lines = results.length === 0 ? ["No eligible Aside profiles to synchronize. Check integration status and profile sync preferences."]
|
|
60
|
+
: results.flatMap(row => [
|
|
61
|
+
`Aside profile ${row.profileId}: ${row.ok ? row.changed ? "updated" : "unchanged" : "failed"}.`,
|
|
62
|
+
...(row.reason ? [row.reason] : []),
|
|
63
|
+
...(!row.ok && row.residual ? ["Recovery is incomplete; inspect the profile before retrying."] : []),
|
|
64
|
+
...(!row.ok && row.snapshotPath ? [`Backup (redacted path): ${row.snapshotPath}`] : []),
|
|
65
|
+
]);
|
|
66
|
+
printData({ results }, wantsJson, lines);
|
|
67
|
+
return results.some(row => !row.ok) ? 1 : 0;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Read-only Cursor integration observations; never downloads or configures Cursor. */
|
|
2
|
+
import type { CursorIntegrationStatus } from "../server/management/cursor-integration-routes";
|
|
3
|
+
import type { CursorLocalInstallerHint } from "../integrations/cursor-local-installer";
|
|
4
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
5
|
+
import { CliUsageError, printData, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
|
|
6
|
+
|
|
7
|
+
const USAGE = "Usage: ocx integration native cursor [status|local-installer] [--json]";
|
|
8
|
+
const GUIDE_URL = "https://lidge-jun.github.io/opencodex/guides/cursor-private-inference/";
|
|
9
|
+
const PLACEHOLDER = "opencodex-loopback";
|
|
10
|
+
|
|
11
|
+
function record(value: unknown): Record<string, unknown> {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Cursor response");
|
|
13
|
+
return value as Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
function text(value: unknown): string {
|
|
16
|
+
if (typeof value !== "string" || !value.length) throw new Error("Invalid Cursor text");
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function nullableText(value: unknown): string | null { return value === null ? null : text(value); }
|
|
20
|
+
function boolean(value: unknown): boolean {
|
|
21
|
+
if (typeof value !== "boolean") throw new Error("Invalid Cursor flag");
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function integer(value: unknown): number {
|
|
25
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid Cursor number");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function strings(value: unknown): string[] {
|
|
29
|
+
if (!Array.isArray(value)) throw new Error("Invalid Cursor list");
|
|
30
|
+
return value.map(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function statusDto(raw: unknown): CursorIntegrationStatus {
|
|
34
|
+
const value = record(raw), privateInference = record(value.privateInference), regular = record(value.regularCursor);
|
|
35
|
+
const gateway = record(value.gateway), table = record(value.effortTable);
|
|
36
|
+
if (!Array.isArray(value.models) || (gateway.apiKeyMode !== "credential" && gateway.apiKeyMode !== "placeholder")
|
|
37
|
+
|| gateway.placeholder !== PLACEHOLDER || (table.source !== "bundle" && table.source !== "static")
|
|
38
|
+
|| value.guideUrl !== GUIDE_URL) throw new Error("Invalid Cursor status");
|
|
39
|
+
const baseUrl = text(gateway.baseUrl), parsedUrl = new URL(baseUrl);
|
|
40
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password
|
|
41
|
+
|| parsedUrl.search || parsedUrl.hash || parsedUrl.pathname !== "/v1") throw new Error("Invalid Cursor gateway");
|
|
42
|
+
const last = value.lastSeen === null ? null : record(value.lastSeen);
|
|
43
|
+
return {
|
|
44
|
+
privateInference: { installed: boolean(privateInference.installed), path: nullableText(privateInference.path), version: nullableText(privateInference.version) },
|
|
45
|
+
regularCursor: { installed: boolean(regular.installed), path: nullableText(regular.path) },
|
|
46
|
+
gateway: { baseUrl, apiKeyMode: gateway.apiKeyMode, placeholder: PLACEHOLDER },
|
|
47
|
+
lastSeen: last === null ? null : { at: integer(last.at), userAgent: text(last.userAgent) },
|
|
48
|
+
effortTable: { source: table.source, version: nullableText(table.version), families: table.families === null ? null : integer(table.families) },
|
|
49
|
+
models: value.models.map(rawModel => {
|
|
50
|
+
const model = record(rawModel), context = model.context === null ? null : record(model.context);
|
|
51
|
+
return { id: text(model.id), reasoning: model.reasoning === null ? null : strings(model.reasoning), family: nullableText(model.family),
|
|
52
|
+
tableLess: boolean(model.tableLess), effortRows: strings(model.effortRows),
|
|
53
|
+
context: context === null ? null : { defaultWindow: integer(context.defaultWindow), longWindow: integer(context.longWindow) } };
|
|
54
|
+
}),
|
|
55
|
+
guideUrl: GUIDE_URL,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function installerDto(raw: unknown): CursorLocalInstallerHint {
|
|
60
|
+
const value = record(raw), available = boolean(value.available);
|
|
61
|
+
if (value.reason !== null && (typeof value.reason !== "string" || !["no-regular-install", "unsupported-platform", "unreachable", "unusable-response"].includes(value.reason))) {
|
|
62
|
+
throw new Error("Invalid Cursor installer reason");
|
|
63
|
+
}
|
|
64
|
+
const url = nullableText(value.url), version = nullableText(value.version);
|
|
65
|
+
if (available) {
|
|
66
|
+
if (url === null || version === null || value.reason !== null || url.length > 4096 || version.length > 256) throw new Error("Invalid Cursor installer");
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "downloads.cursor.com" || parsed.port || parsed.username || parsed.password
|
|
69
|
+
|| !parsed.pathname.startsWith("/local-mode/")) throw new Error("Invalid Cursor installer URL");
|
|
70
|
+
} else if (url !== null || version !== null) throw new Error("Invalid unavailable installer");
|
|
71
|
+
return { available, url, version, reason: value.reason as CursorLocalInstallerHint["reason"] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Arguments start immediately after `integration native cursor`. */
|
|
75
|
+
export async function handleCursorIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
76
|
+
return runCatalogAction(async () => {
|
|
77
|
+
const args = [...argv], wantsJson = takeFlag(args, "--json"), action = args.shift() ?? "status";
|
|
78
|
+
if (args.length || (action !== "status" && action !== "local-installer")) {
|
|
79
|
+
throw new CliUsageError("Cursor supports only status and local-installer reads.", USAGE);
|
|
80
|
+
}
|
|
81
|
+
const raw = await runtimeRequest(`/api/native-integrations/cursor${action === "local-installer" ? "/local-installer" : ""}`,
|
|
82
|
+
{ method: "GET", redirect: "error" }, deps);
|
|
83
|
+
if (action === "local-installer") {
|
|
84
|
+
const result = installerDto(raw);
|
|
85
|
+
printData(result, wantsJson, result.available
|
|
86
|
+
? [`Cursor installer available: ${result.version}`, `URL: ${result.url}`, "Nothing was downloaded or installed."]
|
|
87
|
+
: [`No installer advertised. Reason: ${result.reason ?? "none (Private Inference is already installed)"}.`, "Nothing was downloaded or installed."]);
|
|
88
|
+
} else {
|
|
89
|
+
const result = statusDto(raw);
|
|
90
|
+
printData(result, wantsJson, [
|
|
91
|
+
`Cursor Private Inference: ${result.privateInference.installed ? "installed" : "not found"}`,
|
|
92
|
+
`Regular Cursor: ${result.regularCursor.installed ? "installed" : "not found"}`,
|
|
93
|
+
`Gateway: ${result.gateway.baseUrl}`,
|
|
94
|
+
result.gateway.apiKeyMode === "credential"
|
|
95
|
+
? "API key: an existing proxy credential is required. The placeholder is not an access credential."
|
|
96
|
+
: `API key placeholder: ${PLACEHOLDER} (not a secret).`,
|
|
97
|
+
`Last seen: ${result.lastSeen ? `${result.lastSeen.userAgent} at ${result.lastSeen.at}` : "no Cursor request observed since proxy startup"}`,
|
|
98
|
+
`Models: ${result.models.length}`,
|
|
99
|
+
...result.models.map(model => `${model.id}: reasoning ${model.reasoning?.join(", ") ?? "not advertised"}`),
|
|
100
|
+
`Guide: ${result.guideUrl}`,
|
|
101
|
+
...(!result.privateInference.installed ? ["Next: ocx integration native cursor local-installer"] : []),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Input shared by integration preview and journal commands; no writer imports. */
|
|
2
|
+
import { CliUsageError, takeOptionWithSyntax } from "./runtime-api";
|
|
3
|
+
|
|
4
|
+
export function validateAsideProfile(profile: string | undefined, client: string | undefined, usage: string): void {
|
|
5
|
+
if (profile === undefined) return;
|
|
6
|
+
if (client !== "aside") throw new CliUsageError("--profile requires --client aside", usage);
|
|
7
|
+
if (!/^(0|[1-9][0-9]*)$/.test(profile) || !Number.isSafeInteger(Number(profile))) {
|
|
8
|
+
throw new CliUsageError("--profile must be a nonnegative integer account ID", usage);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function clientIntegrationPath(client: string, profile?: string): string {
|
|
13
|
+
const base = `/api/client-integrations/${encodeURIComponent(client)}`;
|
|
14
|
+
return client === "aside" ? `${base}/profiles${profile === undefined ? "" : `/${encodeURIComponent(profile)}`}` : base;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function integrationOption(args: string[], flag: string): string | undefined {
|
|
18
|
+
return takeOptionWithSyntax(args, flag)?.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function integrationFlag(args: string[], flag: string): boolean {
|
|
22
|
+
const occurrences = args.filter(arg => arg === flag || arg.startsWith(`${flag}=`));
|
|
23
|
+
if (occurrences.length > 1) throw new CliUsageError(`${flag} was given more than once`);
|
|
24
|
+
if (occurrences.length === 0) return false;
|
|
25
|
+
if (occurrences[0] !== flag) throw new CliUsageError(`${flag} does not take a value`);
|
|
26
|
+
args.splice(args.indexOf(flag), 1);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function integrationFingerprint(args: string[]): string | undefined {
|
|
31
|
+
const value = integrationOption(args, "--plan-fingerprint");
|
|
32
|
+
if (value !== undefined && !/^p[0-9]+:[0-9a-f]{32}$/.test(value)) {
|
|
33
|
+
throw new CliUsageError("--plan-fingerprint requires a bound preview token");
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Entries replace the map; omission preserves it; explicit clear sends {}. */
|
|
39
|
+
export function integrationDroidDefaults(args: string[]): Record<string, string> | undefined {
|
|
40
|
+
const clear = integrationFlag(args, "--clear-reasoning-defaults");
|
|
41
|
+
const entries: Array<[string, string]> = [];
|
|
42
|
+
while (true) {
|
|
43
|
+
const index = args.findIndex(arg => arg === "--reasoning-default" || arg.startsWith("--reasoning-default="));
|
|
44
|
+
if (index < 0) break;
|
|
45
|
+
const token = args[index]!;
|
|
46
|
+
const inline = token.startsWith("--reasoning-default=");
|
|
47
|
+
const raw = inline ? token.slice("--reasoning-default=".length) : args[index + 1];
|
|
48
|
+
if (!raw || (!inline && raw.startsWith("--"))) throw new CliUsageError("--reasoning-default requires MODEL=EFFORT");
|
|
49
|
+
args.splice(index, inline ? 1 : 2);
|
|
50
|
+
const separator = raw.lastIndexOf("=");
|
|
51
|
+
if (separator < 1 || separator === raw.length - 1 || !raw.slice(0, separator).trim() || !raw.slice(separator + 1).trim()) {
|
|
52
|
+
throw new CliUsageError("--reasoning-default requires nonempty MODEL=EFFORT");
|
|
53
|
+
}
|
|
54
|
+
const model = raw.slice(0, separator);
|
|
55
|
+
if (entries.some(([key]) => key === model)) throw new CliUsageError("--reasoning-default contains a duplicate model");
|
|
56
|
+
entries.push([model, raw.slice(separator + 1)]);
|
|
57
|
+
}
|
|
58
|
+
if (clear && entries.length) throw new CliUsageError("--clear-reasoning-defaults cannot be combined with --reasoning-default");
|
|
59
|
+
return clear || entries.length ? Object.fromEntries(entries) : undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isExportClientId } from "../clients/config-export";
|
|
3
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
4
|
+
import { clientIntegrationPath, validateAsideProfile } from "./integration-input";
|
|
5
|
+
import { CliUsageError, printData, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
|
|
6
|
+
|
|
7
|
+
const USAGE = "Usage: ocx integration client history remove --op <opId> --yes [--client aside [--profile <id>]] [--json]";
|
|
8
|
+
const receiptSchema = z.object({
|
|
9
|
+
ok: z.literal(true), opId: z.string(),
|
|
10
|
+
clientId: z.string().refine(isExportClientId),
|
|
11
|
+
profileId: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
12
|
+
snapshotRemoved: z.boolean(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** argv begins after history remove (also used by the journal alias). */
|
|
16
|
+
export function handleIntegrationJournalRemove(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
17
|
+
return runCatalogAction(async () => {
|
|
18
|
+
const args = [...argv];
|
|
19
|
+
const wantsJson = takeFlag(args, "--json");
|
|
20
|
+
const confirmed = takeFlag(args, "--yes");
|
|
21
|
+
const opId = takeOption(args, "--op");
|
|
22
|
+
const client = takeOption(args, "--client");
|
|
23
|
+
const profile = takeOption(args, "--profile");
|
|
24
|
+
if (args.length) throw new CliUsageError("Unknown or repeated journal removal arguments", USAGE);
|
|
25
|
+
if (!opId?.trim() || opId !== opId.trim() || opId.length > 512 || /[\u0000-\u001f\u007f-\u009f]/u.test(opId)) {
|
|
26
|
+
throw new CliUsageError("--op requires a nonempty operation ID without control characters", USAGE);
|
|
27
|
+
}
|
|
28
|
+
if (!confirmed) throw new CliUsageError("Journal removal retires a recovery record; pass --yes to confirm", USAGE);
|
|
29
|
+
if (client !== undefined && client !== "aside") throw new CliUsageError("Journal removal accepts only --client aside or no client selector", USAGE);
|
|
30
|
+
validateAsideProfile(profile, client, USAGE);
|
|
31
|
+
const path = client === "aside" ? `${clientIntegrationPath(client, profile)}/journal` : "/api/client-integrations/journal";
|
|
32
|
+
const parsed = receiptSchema.safeParse(await runtimeRequest(`${path}?opId=${encodeURIComponent(opId)}`, {
|
|
33
|
+
method: "DELETE", redirect: "error",
|
|
34
|
+
}, deps));
|
|
35
|
+
if (!parsed.success) throw new Error("Invalid journal removal receipt");
|
|
36
|
+
const receipt = parsed.data;
|
|
37
|
+
if (receipt.opId !== opId || (client !== undefined && receipt.clientId !== client)
|
|
38
|
+
|| (profile !== undefined && receipt.profileId !== Number(profile))
|
|
39
|
+
|| (receipt.clientId !== "aside" && receipt.profileId !== undefined)) throw new Error("Journal removal identity mismatch");
|
|
40
|
+
printData(receipt, wantsJson, [
|
|
41
|
+
`Journal operation ${receipt.opId} retired (${receipt.clientId}${receipt.profileId === undefined ? "" : ` profile ${receipt.profileId}`}).`,
|
|
42
|
+
receipt.snapshotRemoved ? "Snapshot cleanup completed." : "Snapshot cleanup is incomplete. The record is already retired; inspect integration status before further cleanup.",
|
|
43
|
+
]);
|
|
44
|
+
return receipt.snapshotRemoved ? 0 : 1;
|
|
45
|
+
}, {
|
|
46
|
+
integration_journal_newest_protected: "The newest recovery record cannot be removed. Keep it available for restore.",
|
|
47
|
+
integration_operation_not_found: "The operation was not found in the selected journal. Read integration history before retrying.",
|
|
48
|
+
});
|
|
49
|
+
}
|