@yansigit/opencodex 2.33.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
package/src/chat/inbound.ts
CHANGED
|
@@ -10,6 +10,21 @@ export class ChatCompletionsRequestError extends Error {}
|
|
|
10
10
|
type Rec = Record<string, unknown>;
|
|
11
11
|
type ChatCompletionsRoutingBody = Rec & { model: string; messages: unknown[] };
|
|
12
12
|
|
|
13
|
+
/** Session/thread headers the Chat -> Responses bridge must preserve for provider affinity. */
|
|
14
|
+
export const CHAT_RESPONSES_SESSION_HEADERS = [
|
|
15
|
+
"session_id",
|
|
16
|
+
"session-id",
|
|
17
|
+
"x-session-id",
|
|
18
|
+
"thread-id",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export function copyChatResponsesSessionHeaders(source: Headers, target: Headers): void {
|
|
22
|
+
for (const name of CHAT_RESPONSES_SESSION_HEADERS) {
|
|
23
|
+
const value = source.get(name);
|
|
24
|
+
if (value) target.set(name, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
function isRec(v: unknown): v is Rec {
|
|
14
29
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
15
30
|
}
|
|
@@ -53,6 +68,14 @@ function imageUrlFromPart(part: Rec): string | null {
|
|
|
53
68
|
return null;
|
|
54
69
|
}
|
|
55
70
|
|
|
71
|
+
function videoUrlFromPart(part: Rec): string | null {
|
|
72
|
+
if (part.type !== "video_url") return null;
|
|
73
|
+
const videoUrl = part.video_url;
|
|
74
|
+
if (typeof videoUrl === "string" && videoUrl.length > 0) return videoUrl;
|
|
75
|
+
if (isRec(videoUrl) && typeof videoUrl.url === "string" && videoUrl.url.length > 0) return videoUrl.url;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
function userContentToBlocks(content: unknown): Rec[] {
|
|
57
80
|
if (typeof content === "string") {
|
|
58
81
|
return content.length > 0 ? [{ type: "input_text", text: content }] : [];
|
|
@@ -70,7 +93,12 @@ function userContentToBlocks(content: unknown): Rec[] {
|
|
|
70
93
|
continue;
|
|
71
94
|
}
|
|
72
95
|
const imageUrl = imageUrlFromPart(raw);
|
|
73
|
-
if (imageUrl)
|
|
96
|
+
if (imageUrl) {
|
|
97
|
+
blocks.push({ type: "input_image", image_url: imageUrl });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const videoUrl = videoUrlFromPart(raw);
|
|
101
|
+
if (videoUrl) blocks.push({ type: "input_video", video_url: videoUrl });
|
|
74
102
|
}
|
|
75
103
|
return blocks;
|
|
76
104
|
}
|
package/src/chat/outbound.ts
CHANGED
|
@@ -9,7 +9,14 @@ type Rec = Record<string, unknown>;
|
|
|
9
9
|
|
|
10
10
|
import { decodeServerSentEvents, sseFieldValue } from "../lib/sse-decoder";
|
|
11
11
|
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
classifyError,
|
|
14
|
+
cyberPolicyErrorType,
|
|
15
|
+
CYBER_POLICY_ERROR_CODE,
|
|
16
|
+
isCyberPolicyCode,
|
|
17
|
+
isCyberPolicyMessage,
|
|
18
|
+
} from "../lib/errors";
|
|
19
|
+
import { redactSecretString } from "../lib/redact";
|
|
13
20
|
|
|
14
21
|
function isRec(v: unknown): v is Rec {
|
|
15
22
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
@@ -48,14 +55,14 @@ export function chatCompletionsUsage(usage: unknown): Rec {
|
|
|
48
55
|
export function chatCompletionsErrorBody(
|
|
49
56
|
status: number,
|
|
50
57
|
message: string,
|
|
51
|
-
type
|
|
58
|
+
type?: string,
|
|
52
59
|
code?: string | null,
|
|
53
60
|
): Rec {
|
|
54
61
|
if (isCyberPolicyCode(code) || isCyberPolicyMessage(message)) {
|
|
55
62
|
return {
|
|
56
63
|
error: {
|
|
57
64
|
message,
|
|
58
|
-
type:
|
|
65
|
+
type: cyberPolicyErrorType(type),
|
|
59
66
|
param: null,
|
|
60
67
|
code: CYBER_POLICY_ERROR_CODE,
|
|
61
68
|
},
|
|
@@ -64,7 +71,7 @@ export function chatCompletionsErrorBody(
|
|
|
64
71
|
return {
|
|
65
72
|
error: {
|
|
66
73
|
message,
|
|
67
|
-
type,
|
|
74
|
+
type: type ?? "invalid_request_error",
|
|
68
75
|
param: null,
|
|
69
76
|
code: code !== undefined
|
|
70
77
|
? code
|
|
@@ -311,8 +318,9 @@ export function responsesSseToChatCompletionsSse(
|
|
|
311
318
|
// Deliver the error frame then close the stream abnormally (no [DONE]).
|
|
312
319
|
// Do not controller.error() — that can drop already-enqueued bytes from consumers
|
|
313
320
|
// like response.text().
|
|
314
|
-
const
|
|
315
|
-
const
|
|
321
|
+
const safeMessage = redactSecretString(message);
|
|
322
|
+
const statusHint = details?.status ?? streamErrorStatus(safeMessage);
|
|
323
|
+
const classified = classifyError(statusHint, details?.type ?? "upstream_error", safeMessage);
|
|
316
324
|
const translatorOverflow = details?.code === "translation_buffer_limit";
|
|
317
325
|
if (translatorOverflow) {
|
|
318
326
|
upstreamAbort.abort(new Error("upstream translation buffer exceeded the safe limit"));
|
|
@@ -324,7 +332,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
324
332
|
classified.type = "upstream_error";
|
|
325
333
|
} else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
326
334
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
327
|
-
classified.type =
|
|
335
|
+
classified.type = cyberPolicyErrorType(details?.type);
|
|
328
336
|
} else if (details?.code !== undefined && details.code !== null && !classified.code) {
|
|
329
337
|
classified.code = details.code;
|
|
330
338
|
}
|
|
@@ -218,7 +218,14 @@ function isOwnedFile(path: string): boolean {
|
|
|
218
218
|
export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
|
|
219
219
|
try {
|
|
220
220
|
const dir = join(configDir, "agents");
|
|
221
|
-
|
|
221
|
+
if (defs.length === 0) {
|
|
222
|
+
try { lstatSync(dir); } catch (error) {
|
|
223
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
mkdirSync(dir, { recursive: true });
|
|
228
|
+
}
|
|
222
229
|
const keep = new Set(defs.map(d => d.file));
|
|
223
230
|
for (const existing of readdirSync(dir)) {
|
|
224
231
|
if (!existing.startsWith(OWNED_PREFIX) || !existing.endsWith(".md")) continue;
|
package/src/claude/outbound.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Claude Code outbound: internal /v1/responses output -> Anthropic Messages API shapes.
|
|
3
3
|
*
|
|
4
4
|
* Wire contract pinned in devlog/260711_claude_inbound/003_evidence.md (all Tier 2):
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
5
|
+
* - Transport-only `ping` events may appear at any point, including before
|
|
6
|
+
* message_start. Semantic framing stays message_start ->
|
|
7
|
+
* (content_block_start -> deltas -> content_block_stop)* -> message_delta -> message_stop.
|
|
7
8
|
* - thinking blocks get thinking_delta(s) then ONE synthetic signature_delta just
|
|
8
9
|
* before content_block_stop (CCR precedent: Claude Code does not verify signatures).
|
|
9
10
|
* - message_delta.usage is cumulative; message_start embeds a full message snapshot.
|
|
@@ -267,12 +268,12 @@ export function responsesSseToAnthropicSse(
|
|
|
267
268
|
emit("message_start", { type: "message_start", message: messageSnapshot(model) });
|
|
268
269
|
emit("ping", { type: "ping" });
|
|
269
270
|
};
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
271
|
+
// Keepalive pings protect remote deployments behind LB/NAT idle timeouts even
|
|
272
|
+
// before semantic output. They are transport-only and must not manufacture a
|
|
273
|
+
// message before a possible initial error.
|
|
273
274
|
if (pingIntervalMs > 0) {
|
|
274
275
|
pingTimer = setInterval(() => {
|
|
275
|
-
if (terminated ||
|
|
276
|
+
if (terminated || (controller.desiredSize ?? 0) <= 0) return;
|
|
276
277
|
try {
|
|
277
278
|
emit("ping", { type: "ping" });
|
|
278
279
|
} catch { /* controller torn down; the read loop is ending anyway */ }
|
|
@@ -352,7 +353,8 @@ export function responsesSseToAnthropicSse(
|
|
|
352
353
|
const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
|
|
353
354
|
if (!started) {
|
|
354
355
|
// An initial upstream failure is an Anthropic error stream, not a partial message.
|
|
355
|
-
// Do not manufacture message_start
|
|
356
|
+
// Do not manufacture message_start before the terminal error. Earlier transport-only
|
|
357
|
+
// pings remain valid and do not turn the failure into a partial message.
|
|
356
358
|
emit("error", anthropicErrorBody(status, message, type, code));
|
|
357
359
|
return;
|
|
358
360
|
}
|
|
@@ -366,7 +368,7 @@ export function responsesSseToAnthropicSse(
|
|
|
366
368
|
// Transport prelude only. Start Anthropic framing on semantic output or completion.
|
|
367
369
|
break;
|
|
368
370
|
case "response.heartbeat":
|
|
369
|
-
if (
|
|
371
|
+
if ((controller.desiredSize ?? 0) > 0) emit("ping", { type: "ping" });
|
|
370
372
|
break;
|
|
371
373
|
case "response.output_text.delta": {
|
|
372
374
|
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
package/src/cli/account-api.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface FamilyRows {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export interface CodexQuotaDto {
|
|
144
|
+
fiveHourPercent?: number;
|
|
145
|
+
fiveHourResetAt?: number;
|
|
144
146
|
weeklyPercent?: number;
|
|
145
147
|
monthlyPercent?: number;
|
|
146
148
|
weeklyResetAt?: number;
|
|
@@ -158,8 +160,6 @@ export interface ProviderQuotaWindowDto {
|
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
export interface ProviderQuotaDto extends CodexQuotaDto {
|
|
161
|
-
fiveHourPercent?: number;
|
|
162
|
-
fiveHourResetAt?: number;
|
|
163
163
|
customWindows?: ProviderQuotaWindowDto[];
|
|
164
164
|
updatedAt?: number;
|
|
165
165
|
}
|
|
@@ -187,7 +187,7 @@ interface CodexAccountDto {
|
|
|
187
187
|
function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
|
|
188
188
|
if (!quota) return null;
|
|
189
189
|
const projected: CodexQuotaDto = {};
|
|
190
|
-
for (const key of ["weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
|
|
190
|
+
for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
|
|
191
191
|
if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
|
|
192
192
|
}
|
|
193
193
|
return projected;
|
|
@@ -240,10 +240,22 @@ interface OAuthAccountDto {
|
|
|
240
240
|
email?: string;
|
|
241
241
|
active?: boolean;
|
|
242
242
|
needsReauth?: boolean;
|
|
243
|
+
quota?: CodexQuotaDto | null;
|
|
244
|
+
quotaUnavailable?: boolean;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
async function fetchOAuthRows(
|
|
246
|
-
|
|
247
|
+
async function fetchOAuthRows(
|
|
248
|
+
deps: AccountDeps,
|
|
249
|
+
baseUrl: string,
|
|
250
|
+
name: string,
|
|
251
|
+
quota?: { refresh?: boolean },
|
|
252
|
+
): Promise<FamilyRows> {
|
|
253
|
+
// Quota is opt-in: the server probes the upstream once per stored credential when `quota=1`
|
|
254
|
+
// is present, so the default listing must stay a cheap local read (#2566).
|
|
255
|
+
const query = quota
|
|
256
|
+
? `?provider=${encodeURIComponent(name)}"a=1${quota.refresh ? "&refresh=1" : ""}`
|
|
257
|
+
: `?provider=${encodeURIComponent(name)}`;
|
|
258
|
+
const res = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts${query}`);
|
|
247
259
|
if (res.status === 0) return { rows: [], activeId: null, status: 0, networkDown: true };
|
|
248
260
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
249
261
|
const activeId = typeof res.json.activeAccountId === "string" ? res.json.activeAccountId : null;
|
|
@@ -256,6 +268,8 @@ async function fetchOAuthRows(deps: AccountDeps, baseUrl: string, name: string):
|
|
|
256
268
|
email: a.email,
|
|
257
269
|
active: a.active ?? a.id === activeId,
|
|
258
270
|
needsReauth: a.needsReauth,
|
|
271
|
+
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
272
|
+
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
259
273
|
}));
|
|
260
274
|
return { rows, activeId, status: 200 };
|
|
261
275
|
}
|
|
@@ -284,9 +298,15 @@ async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): P
|
|
|
284
298
|
return { rows, activeId, status: 200 };
|
|
285
299
|
}
|
|
286
300
|
|
|
287
|
-
export function fetchRows(
|
|
301
|
+
export function fetchRows(
|
|
302
|
+
deps: AccountDeps,
|
|
303
|
+
baseUrl: string,
|
|
304
|
+
name: string,
|
|
305
|
+
type: AccountType,
|
|
306
|
+
quota?: { refresh?: boolean },
|
|
307
|
+
): Promise<FamilyRows> {
|
|
288
308
|
if (type === "codex") return fetchCodexRows(deps, baseUrl);
|
|
289
|
-
if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name);
|
|
309
|
+
if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
|
|
290
310
|
return fetchKeyRows(deps, baseUrl, name);
|
|
291
311
|
}
|
|
292
312
|
|
|
@@ -250,9 +250,16 @@ function resetIso(value: number | undefined): string | null {
|
|
|
250
250
|
function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
251
251
|
const parts = [row.id === MAIN_ID ? "main" : row.id, row.email, row.plan];
|
|
252
252
|
const quota = row.quota;
|
|
253
|
-
|
|
253
|
+
const fiveHourPercent = quota?.fiveHourPercent ?? quota?.shortPercent;
|
|
254
|
+
const fiveHourResetAt = quota?.fiveHourResetAt ?? quota?.shortResetAt;
|
|
255
|
+
if (!quota || (quota.weeklyPercent === undefined && quota.monthlyPercent === undefined && fiveHourPercent === undefined)) {
|
|
254
256
|
parts.push("quota: unknown");
|
|
255
257
|
} else {
|
|
258
|
+
if (fiveHourPercent !== undefined) {
|
|
259
|
+
parts.push(`5h ${fiveHourPercent}%`);
|
|
260
|
+
const fiveHourReset = resetIso(fiveHourResetAt);
|
|
261
|
+
if (fiveHourReset) parts.push(`resets ${fiveHourReset}`);
|
|
262
|
+
}
|
|
256
263
|
if (quota.weeklyPercent !== undefined) parts.push(`weekly ${quota.weeklyPercent}%`);
|
|
257
264
|
const weeklyReset = resetIso(quota.weeklyResetAt);
|
|
258
265
|
if (weeklyReset) parts.push(`resets ${weeklyReset}`);
|
|
@@ -272,14 +279,14 @@ function quotaParts(quota: ProviderQuotaDto): string[] {
|
|
|
272
279
|
const reset = resetIso(resetAt);
|
|
273
280
|
if (reset) parts.push(`resets ${reset}`);
|
|
274
281
|
};
|
|
275
|
-
add("5h", quota.fiveHourPercent, quota.fiveHourResetAt);
|
|
282
|
+
add("5h", quota.fiveHourPercent ?? quota.shortPercent, quota.fiveHourResetAt ?? quota.shortResetAt);
|
|
276
283
|
add("weekly", quota.weeklyPercent, quota.weeklyResetAt);
|
|
277
284
|
add("monthly", quota.monthlyPercent, quota.monthlyResetAt);
|
|
278
285
|
for (const window of quota.customWindows ?? []) add(window.label, window.percent, window.resetAt);
|
|
279
286
|
return parts;
|
|
280
287
|
}
|
|
281
288
|
|
|
282
|
-
function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
|
|
289
|
+
export function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
|
|
283
290
|
return [name, ...quotaParts(report.quota)].join(" ");
|
|
284
291
|
}
|
|
285
292
|
|
package/src/cli/account.ts
CHANGED
|
@@ -16,7 +16,7 @@ const MAIN_CODEX_ID = "__main__";
|
|
|
16
16
|
const REPLACEMENT_STYLE_OAUTH = new Set(["kiro"]);
|
|
17
17
|
|
|
18
18
|
const ACCOUNT_USAGE = `Usage:
|
|
19
|
-
ocx account list [provider] [--json] [--all]
|
|
19
|
+
ocx account list [provider] [--json] [--all] [--quota [--refresh]]
|
|
20
20
|
ocx account current <provider> [--json]
|
|
21
21
|
ocx account use <provider> <account-or-key-id|main> [--json]
|
|
22
22
|
ocx account refresh <provider> [--json]
|
|
@@ -75,11 +75,29 @@ function priorityText(row: AccountRow): string {
|
|
|
75
75
|
return row.priority > 0 ? `+${row.priority}` : String(row.priority);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Compact per-account quota for the opt-in QUOTA column: the two windows an operator actually
|
|
80
|
+
* decides on before a long session. The full breakdown stays in `--json`.
|
|
81
|
+
*/
|
|
82
|
+
function quotaText(row: AccountRow): string {
|
|
83
|
+
if ((row as { quotaUnavailable?: boolean }).quotaUnavailable) return "unavailable";
|
|
84
|
+
const quota = row.quota;
|
|
85
|
+
if (!quota) return "-";
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
// Two spellings reach this DTO: the per-account provider probe reports `fiveHourPercent`,
|
|
88
|
+
// while the Codex pool reports the same idea as `shortPercent`.
|
|
89
|
+
const short = quota.fiveHourPercent ?? quota.shortPercent;
|
|
90
|
+
if (typeof short === "number") parts.push(`5h ${short}%`);
|
|
91
|
+
if (typeof quota.weeklyPercent === "number") parts.push(`wk ${quota.weeklyPercent}%`);
|
|
92
|
+
return parts.length > 0 ? parts.join(" ") : "-";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatAccountTable(rows: AccountRow[], withQuota = false): string {
|
|
79
96
|
const header = ["PROVIDER", "TYPE", "ID", "PLAN/LABEL", "PRIORITY", "STATUS"];
|
|
97
|
+
if (withQuota) header.push("QUOTA");
|
|
80
98
|
const data = rows.map(r => {
|
|
81
99
|
const keyLabel = r.masked && r.label !== r.masked ? `${r.masked} (${r.label})` : r.masked;
|
|
82
|
-
|
|
100
|
+
const cols = [
|
|
83
101
|
r.provider,
|
|
84
102
|
r.type,
|
|
85
103
|
displayId(r.id),
|
|
@@ -87,6 +105,8 @@ export function formatAccountTable(rows: AccountRow[]): string {
|
|
|
87
105
|
priorityText(r),
|
|
88
106
|
statusText(r),
|
|
89
107
|
];
|
|
108
|
+
if (withQuota) cols.push(quotaText(r));
|
|
109
|
+
return cols;
|
|
90
110
|
});
|
|
91
111
|
const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
|
|
92
112
|
const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
|
|
@@ -96,6 +116,10 @@ export function formatAccountTable(rows: AccountRow[]): string {
|
|
|
96
116
|
async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
|
|
97
117
|
const wantsJson = consumeFlag(rest, "--json");
|
|
98
118
|
const showAll = consumeFlag(rest, "--all");
|
|
119
|
+
// Opt-in: the server probes the upstream once per stored credential, so the default listing
|
|
120
|
+
// stays a cheap local read (#2566). --refresh bypasses the server-side TTL.
|
|
121
|
+
const wantsQuota = consumeFlag(rest, "--quota");
|
|
122
|
+
const refreshQuota = consumeFlag(rest, "--refresh");
|
|
99
123
|
const name = rest.shift();
|
|
100
124
|
const leftover = leftoverArgsError(rest);
|
|
101
125
|
if (leftover) {
|
|
@@ -139,7 +163,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
139
163
|
const rows: AccountRow[] = [];
|
|
140
164
|
const notes: string[] = [];
|
|
141
165
|
for (const t of targets) {
|
|
142
|
-
const r = await fetchRows(deps, baseUrl, t.name, t.type);
|
|
166
|
+
const r = await fetchRows(deps, baseUrl, t.name, t.type, wantsQuota ? { refresh: refreshQuota } : undefined);
|
|
143
167
|
if (r.networkDown) return proxyUnreachable();
|
|
144
168
|
if (r.errorJson) {
|
|
145
169
|
if (name) return apiError(r.errorJson, `failed to list ${t.name}`);
|
|
@@ -174,7 +198,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
174
198
|
console.log(JSON.stringify({ accounts: rows, notes }, null, 2));
|
|
175
199
|
return 0;
|
|
176
200
|
}
|
|
177
|
-
if (rows.length > 0) console.log(formatAccountTable(rows));
|
|
201
|
+
if (rows.length > 0) console.log(formatAccountTable(rows, wantsQuota));
|
|
178
202
|
for (const n of notes) console.log(n);
|
|
179
203
|
if (rows.length === 0 && notes.length === 0) console.log("No stored accounts or keys.");
|
|
180
204
|
return 0;
|
package/src/cli/alias.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CliUsageError, printData, rejectArgs, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
|
|
2
|
+
|
|
3
|
+
const USAGE = `Usage:
|
|
4
|
+
ocx alias list [--json]
|
|
5
|
+
ocx alias set <provider> <alias>
|
|
6
|
+
ocx alias set <provider>/<native-model-id> <alias>
|
|
7
|
+
ocx alias rm <provider>[/<native-model-id>]
|
|
8
|
+
ocx alias defaults <on|off> [--provider <name>]`;
|
|
9
|
+
|
|
10
|
+
function selector(value: string): { provider: string; model?: string } {
|
|
11
|
+
const slash = value.indexOf("/");
|
|
12
|
+
return slash < 0 ? { provider: value } : { provider: value.slice(0, slash), model: value.slice(slash + 1) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
16
|
+
const args = [...argv];
|
|
17
|
+
const action = (args.shift() ?? "list").toLowerCase();
|
|
18
|
+
const wantsJson = takeFlag(args, "--json");
|
|
19
|
+
if (action === "list") {
|
|
20
|
+
rejectArgs(args, USAGE);
|
|
21
|
+
const result = await runtimeRequest<Record<string, unknown>>("/api/aliases", {}, deps);
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
for (const [target, alias] of Object.entries((result.providers ?? {}) as Record<string, string>)) lines.push(`provider ${target} ${alias} user`);
|
|
24
|
+
for (const [provider, rows] of Object.entries((result.models ?? {}) as Record<string, Record<string, { alias: string; source: string }>>)) {
|
|
25
|
+
for (const [model, value] of Object.entries(rows)) lines.push(`model ${provider}/${model} ${value.alias} ${value.source}`);
|
|
26
|
+
}
|
|
27
|
+
printData(result, wantsJson, lines.length ? lines : ["No aliases configured."]);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
if (action === "defaults") {
|
|
31
|
+
const state = args.shift()?.toLowerCase();
|
|
32
|
+
const provider = takeOption(args, "--provider");
|
|
33
|
+
rejectArgs(args, USAGE);
|
|
34
|
+
if (state !== "on" && state !== "off") throw new CliUsageError("defaults requires on or off", USAGE);
|
|
35
|
+
const result = await runtimeRequest("/api/default-aliases", { method: "PUT", body: JSON.stringify({ enabled: state === "on", ...(provider ? { provider } : {}) }) }, deps);
|
|
36
|
+
printData(result, wantsJson, [`Default aliases ${state}${provider ? ` for ${provider}` : " globally"}.`]);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const target = args.shift()?.trim();
|
|
40
|
+
if (!target) throw new CliUsageError("alias target is required", USAGE);
|
|
41
|
+
const parsed = selector(target);
|
|
42
|
+
if (!parsed.provider || parsed.model === "") throw new CliUsageError("target must be provider or provider/native-model-id", USAGE);
|
|
43
|
+
if (action === "set") {
|
|
44
|
+
const alias = args.shift()?.trim();
|
|
45
|
+
rejectArgs(args, USAGE);
|
|
46
|
+
if (!alias) throw new CliUsageError("alias value is required", USAGE);
|
|
47
|
+
const path = parsed.model === undefined
|
|
48
|
+
? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
|
|
49
|
+
: `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
|
|
50
|
+
const body = parsed.model === undefined ? { alias } : { set: { [parsed.model]: alias } };
|
|
51
|
+
const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
52
|
+
printData(result, wantsJson, [`${target} → ${alias}`]);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (action === "rm") {
|
|
56
|
+
rejectArgs(args, USAGE);
|
|
57
|
+
const path = parsed.model === undefined
|
|
58
|
+
? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
|
|
59
|
+
: `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
|
|
60
|
+
const body = parsed.model === undefined ? { alias: null } : { remove: [parsed.model] };
|
|
61
|
+
const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
62
|
+
printData(result, wantsJson, [`Removed alias for ${target}.`]);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
throw new CliUsageError(`unknown alias action '${action}'`, USAGE);
|
|
66
|
+
}
|
package/src/cli/claude.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type ClaudeEnvDeps = {
|
|
|
34
34
|
authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
|
|
35
35
|
/** Test seam; production uses the authenticated Node-launcher context. */
|
|
36
36
|
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
|
|
37
|
+
/** Explicit unsafe opt-in from a root `--dangerously-skip-permissions` launch. */
|
|
38
|
+
allowRootSkipPermissions?: boolean;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
function isClaudeLoopbackHostname(hostname: string): boolean {
|
|
@@ -115,6 +117,9 @@ export function buildClaudeEnv(
|
|
|
115
117
|
if (env[name] !== undefined && env[name] !== "") return; // user wins
|
|
116
118
|
env[name] = value;
|
|
117
119
|
};
|
|
120
|
+
if (deps.allowRootSkipPermissions === true) {
|
|
121
|
+
setDefault("IS_SANDBOX", "1");
|
|
122
|
+
}
|
|
118
123
|
setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
|
|
119
124
|
const existingBaseUrl = env.ANTHROPIC_BASE_URL;
|
|
120
125
|
if (existingBaseUrl) {
|
|
@@ -301,6 +306,22 @@ export function claudeNotFoundHint(
|
|
|
301
306
|
return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
|
|
302
307
|
}
|
|
303
308
|
|
|
309
|
+
export function shouldAllowRootSkipPermissions(
|
|
310
|
+
args: readonly string[],
|
|
311
|
+
getuid: (() => number) | null | undefined = process.getuid,
|
|
312
|
+
): boolean {
|
|
313
|
+
return args.includes("--dangerously-skip-permissions")
|
|
314
|
+
&& typeof getuid === "function"
|
|
315
|
+
&& getuid() === 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function rootSkipPermissionsNotice(env: ClaudeLaunchEnv): string {
|
|
319
|
+
if (env.IS_SANDBOX === "1") {
|
|
320
|
+
return "⚠ Root --dangerously-skip-permissions requested: OpenCodex set IS_SANDBOX=1 to bypass Claude Code's root guard. OpenCodex did not create an OS sandbox; prefer running as a non-root user.";
|
|
321
|
+
}
|
|
322
|
+
return `⚠ Root --dangerously-skip-permissions requested: preserving user IS_SANDBOX=${env.IS_SANDBOX}; Claude Code's root guard remains in control.`;
|
|
323
|
+
}
|
|
324
|
+
|
|
304
325
|
export async function cmdClaude(args: string[]): Promise<number> {
|
|
305
326
|
const config = loadConfig();
|
|
306
327
|
if (config.claudeCode?.enabled === false) {
|
|
@@ -313,7 +334,11 @@ export async function cmdClaude(args: string[]): Promise<number> {
|
|
|
313
334
|
return 1;
|
|
314
335
|
}
|
|
315
336
|
const contextWindows = await fetchClaudeContextWindows(config, port);
|
|
316
|
-
const
|
|
337
|
+
const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
|
|
338
|
+
const env = buildClaudeEnv(config, port, process.env, contextWindows, { allowRootSkipPermissions });
|
|
339
|
+
if (allowRootSkipPermissions) {
|
|
340
|
+
console.error(rootSkipPermissionsNotice(env));
|
|
341
|
+
}
|
|
317
342
|
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
|
|
318
343
|
// never refreshes it, so the picker would keep showing yesterday's aliases.
|
|
319
344
|
try {
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -430,6 +430,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
430
430
|
await handleModels(deps.args.slice(1));
|
|
431
431
|
return 0;
|
|
432
432
|
},
|
|
433
|
+
alias: async deps => {
|
|
434
|
+
const { handleAliasCommand } = await import("./alias");
|
|
435
|
+
return await handleAliasCommand(deps.args.slice(1));
|
|
436
|
+
},
|
|
433
437
|
combo: async deps => {
|
|
434
438
|
const { handleComboCommand } = await import("./combo");
|
|
435
439
|
return await handleComboCommand(deps.args.slice(1));
|
|
@@ -615,6 +619,15 @@ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Pro
|
|
|
615
619
|
+ "Run 'ocx sync --restart-desktop-app' from an external terminal instead.",
|
|
616
620
|
);
|
|
617
621
|
return;
|
|
622
|
+
case "process_probe_failed":
|
|
623
|
+
// Distinct from `no_targets`: we could not look, which is not the same as looking and
|
|
624
|
+
// finding nothing. Saying "not running" here sent users away believing there was nothing
|
|
625
|
+
// to restart (#2557).
|
|
626
|
+
log.error(
|
|
627
|
+
"Could not enumerate Codex desktop processes, so the app was not restarted. "
|
|
628
|
+
+ "Quit and relaunch the desktop app manually to refresh the model picker.",
|
|
629
|
+
);
|
|
630
|
+
return;
|
|
618
631
|
case "no_targets":
|
|
619
632
|
log.log("Codex desktop app is not running; nothing to restart.");
|
|
620
633
|
return;
|
|
@@ -630,4 +643,3 @@ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Pro
|
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
645
|
}
|
|
633
|
-
|
package/src/cli/help.ts
CHANGED
|
@@ -52,6 +52,7 @@ Usage:
|
|
|
52
52
|
ocx provider <sub> Providers, connectivity, quota, and selected models
|
|
53
53
|
ocx account <sub> Accounts, login/reauth, key pools, and quota controls
|
|
54
54
|
ocx models <sub> Live/custom models, visibility, context, and shadow calls
|
|
55
|
+
ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
|
|
55
56
|
ocx combo <sub> Combo failover/round-robin routing
|
|
56
57
|
ocx agent <sub> Subagents, roles, injection, effort caps, and sidecars
|
|
57
58
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
package/src/cli/index.ts
CHANGED
|
@@ -101,6 +101,11 @@ const head = await runCli(process.argv.slice(2));
|
|
|
101
101
|
const args = head.args;
|
|
102
102
|
const command = head.command;
|
|
103
103
|
|
|
104
|
+
if (command === "telemetry") {
|
|
105
|
+
const { runTelemetryCommand } = await import("./telemetry-commands");
|
|
106
|
+
process.exit(runTelemetryCommand(args.slice(1), loadConfig()));
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
function parsePortOption(): number | undefined {
|
|
105
110
|
if (args.length === 1) return undefined;
|
|
106
111
|
if (args.length !== 3 || args[1] !== "--port") {
|
|
@@ -164,7 +169,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
164
169
|
// Ghost LISTEN rows with a dead PID can outlive the process for a while.
|
|
165
170
|
// SetTcpEntry(DELETE_TCB) needs elevation (often returns 317), so the only
|
|
166
171
|
// reliable non-admin recovery is to wait for the OS to release the TCB.
|
|
167
|
-
timeoutMs: 60_000,
|
|
172
|
+
timeoutMs: process.platform === "win32" ? 60_000 : 10_000,
|
|
168
173
|
intervalMs: 100,
|
|
169
174
|
scanIntervalMs: 500,
|
|
170
175
|
killOcxHolders: false,
|
package/src/cli/init.ts
CHANGED