@vybestack/llxprt-code 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7
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/package.json +13 -13
- package/src/commands/mcp/add.ts +8 -4
- package/src/config/cliArgParser.ts +2 -0
- package/src/config/config.ts +2 -0
- package/src/config/configBuilder.ts +3 -0
- package/src/config/configError.ts +23 -0
- package/src/config/extensions/extensionEnablement.ts +62 -1
- package/src/config/interactiveContext.ts +2 -1
- package/src/config/intermediateConfig.ts +4 -0
- package/src/config/keyBindings.ts +29 -3
- package/src/config/policy.ts +7 -1
- package/src/config/postConfigRuntime.ts +20 -14
- package/src/config/settings-schema/schema-ui.ts +9 -0
- package/src/config/settingsLoader.ts +21 -17
- package/src/config/settingsSchema.ts +2 -2
- package/src/config/trustedFolders.ts +275 -58
- package/src/config/yargsOptions.ts +7 -0
- package/src/generated/git-commit.json +1 -1
- package/src/nonInteractiveCli.ts +1 -0
- package/src/nonInteractiveCliSupport.ts +171 -51
- package/src/runtime/interactiveToolScheduler.ts +6 -6
- package/src/services/BuiltinCommandLoader.ts +33 -25
- package/src/services/FileCommandLoader.ts +34 -7
- package/src/test-utils/async.ts +20 -0
- package/src/test-utils/render.tsx +10 -1
- package/src/ui/App.tsx +17 -10
- package/src/ui/AppContainerRuntime.tsx +7 -9
- package/src/ui/cliUiRuntime.ts +28 -0
- package/src/ui/commands/authCommand.ts +240 -3
- package/src/ui/commands/compressCommand.ts +9 -1
- package/src/ui/commands/directoryCommand.tsx +1 -1
- package/src/ui/commands/logoutCommand.ts +2 -2
- package/src/ui/commands/permissionsCommand.ts +77 -20
- package/src/ui/commands/policiesCommand.ts +6 -1
- package/src/ui/commands/quotaCommand.ts +501 -0
- package/src/ui/commands/statsCommand.ts +2 -2
- package/src/ui/commands/statsQuota.ts +1 -1
- package/src/ui/commands/subagentCommand.ts +59 -27
- package/src/ui/components/AuthDialog.tsx +3 -3
- package/src/ui/components/Composer.tsx +1 -0
- package/src/ui/components/DialogManager.tsx +9 -2
- package/src/ui/components/FolderTrustDialog.tsx +71 -60
- package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
- package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
- package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
- package/src/ui/components/inputPromptHooks.ts +2 -0
- package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
- package/src/ui/components/inputPromptTypes.ts +6 -0
- package/src/ui/components/messages/AiMessage.tsx +24 -12
- package/src/ui/components/messages/ToolMessage.tsx +26 -40
- package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
- package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
- package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
- package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
- package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
- package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
- package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
- package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
- package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
- package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
- package/src/ui/contexts/KeypressContext.tsx +13 -2
- package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
- package/src/ui/contexts/TodoContext.tsx +0 -13
- package/src/ui/contexts/TodoProvider.tsx +1 -34
- package/src/ui/contexts/UIActionsContext.tsx +2 -1
- package/src/ui/contexts/UIStateContext.tsx +3 -2
- package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
- package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
- package/src/ui/hooks/agentStream/types.ts +1 -0
- package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
- package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
- package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
- package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
- package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
- package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
- package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
- package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
- package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
- package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
- package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
- package/src/ui/hooks/useFolderTrust.ts +118 -81
- package/src/ui/hooks/useGitBranchName.ts +79 -33
- package/src/ui/hooks/useIdeTrustListener.ts +5 -19
- package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
- package/src/ui/hooks/useReactToolScheduler.ts +13 -17
- package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
- package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
- package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
- package/src/ui/layouts/InlineContent.tsx +198 -0
- package/src/ui/trustDialogHelpers.ts +190 -0
- package/src/utils/cleanup-state.ts +115 -0
- package/src/utils/cleanup.ts +32 -34
- package/src/utils/sandbox-containers.ts +183 -53
- package/src/utils/sandbox-exec.ts +9 -3
- package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
- package/src/zed-integration/acp-types.ts +35 -0
- package/src/zed-integration/fileSystemService.ts +1 -1
- package/src/zed-integration/runZedIntegration.ts +142 -0
- package/src/zed-integration/zed-agent-event-handler.ts +179 -0
- package/src/zed-integration/zed-agent-setup.ts +43 -0
- package/src/zed-integration/zed-command-registry.ts +167 -0
- package/src/zed-integration/zed-config-options.ts +280 -0
- package/src/zed-integration/zed-content-utils.ts +2 -2
- package/src/zed-integration/zed-helpers.ts +132 -0
- package/src/zed-integration/zed-initialize.ts +64 -0
- package/src/zed-integration/zed-plan-update.ts +20 -0
- package/src/zed-integration/zed-prompt-command.ts +77 -0
- package/src/zed-integration/zed-session-config.ts +119 -0
- package/src/zed-integration/zed-session-errors.ts +337 -0
- package/src/zed-integration/zed-session-events.ts +216 -0
- package/src/zed-integration/zed-session-info.ts +255 -0
- package/src/zed-integration/zed-session-lifecycle.ts +186 -0
- package/src/zed-integration/zed-session-listing.ts +153 -0
- package/src/zed-integration/zed-session-loader.ts +193 -0
- package/src/zed-integration/zed-session-pagination.ts +240 -0
- package/src/zed-integration/zed-session-replay.ts +564 -0
- package/src/zed-integration/zed-stream-batcher.ts +236 -0
- package/src/zed-integration/zed-terminal-manager.ts +416 -0
- package/src/zed-integration/zed-terminal-setup.ts +72 -0
- package/src/zed-integration/zed-test-helpers.ts +349 -4
- package/src/zed-integration/zed-tool-handler.ts +47 -5
- package/src/zed-integration/zedIntegration.ts +538 -587
- package/src/services/todo-continuation/todoContinuationService.ts +0 -610
- package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
- package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
- package/src/ui/hooks/useTodoContinuation.ts +0 -299
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import * as process from 'node:process';
|
|
8
9
|
import type {
|
|
9
10
|
OpenDialogActionReturn,
|
|
10
11
|
SlashCommand,
|
|
11
12
|
MessageActionReturn,
|
|
12
13
|
} from './types.js';
|
|
13
14
|
import { CommandKind } from './types.js';
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
loadTrustedFolders,
|
|
17
|
+
resolveLocalWorkspaceTrust,
|
|
18
|
+
TrustLevel,
|
|
19
|
+
} from '../../config/trustedFolders.js';
|
|
20
|
+
import {
|
|
21
|
+
combineTrustUpdateFailure,
|
|
22
|
+
getTrustCommitErrorMessage,
|
|
23
|
+
} from '../trustDialogHelpers.js';
|
|
15
24
|
|
|
16
25
|
const VALID_TRUST_LEVELS = [
|
|
17
26
|
TrustLevel.TRUST_FOLDER,
|
|
@@ -25,6 +34,7 @@ const VALID_TRUST_LEVELS = [
|
|
|
25
34
|
*/
|
|
26
35
|
function parsePermissionsArgs(
|
|
27
36
|
args: string,
|
|
37
|
+
workingDirectory: string,
|
|
28
38
|
): { trustLevel: TrustLevel; targetPath: string } | { error: string } {
|
|
29
39
|
const trimmed = args.trim();
|
|
30
40
|
|
|
@@ -68,7 +78,7 @@ function parsePermissionsArgs(
|
|
|
68
78
|
if (path.isAbsolute(targetPath)) {
|
|
69
79
|
normalizedPath = path.normalize(targetPath);
|
|
70
80
|
} else {
|
|
71
|
-
normalizedPath = path.resolve(
|
|
81
|
+
normalizedPath = path.resolve(workingDirectory, targetPath);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
return { trustLevel, targetPath: normalizedPath };
|
|
@@ -78,8 +88,11 @@ export const permissionsCommand: SlashCommand = {
|
|
|
78
88
|
name: 'permissions',
|
|
79
89
|
description: 'manage folder trust settings',
|
|
80
90
|
kind: CommandKind.BUILT_IN,
|
|
81
|
-
action: (
|
|
82
|
-
const parsed = parsePermissionsArgs(
|
|
91
|
+
action: (context, args) => {
|
|
92
|
+
const parsed = parsePermissionsArgs(
|
|
93
|
+
args,
|
|
94
|
+
context.services.config?.getWorkingDir() ?? process.cwd(),
|
|
95
|
+
);
|
|
83
96
|
|
|
84
97
|
// If error is empty string, open dialog (no args case)
|
|
85
98
|
if ('error' in parsed) {
|
|
@@ -100,21 +113,65 @@ export const permissionsCommand: SlashCommand = {
|
|
|
100
113
|
// We have valid trust level and path, modify trust
|
|
101
114
|
const { trustLevel, targetPath } = parsed;
|
|
102
115
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
return (async (): Promise<OpenDialogActionReturn | MessageActionReturn> => {
|
|
117
|
+
let trustedFolders: ReturnType<typeof loadTrustedFolders> | undefined;
|
|
118
|
+
let savedSnapshot:
|
|
119
|
+
| ReturnType<ReturnType<typeof loadTrustedFolders>['snapshotValue']>
|
|
120
|
+
| undefined;
|
|
121
|
+
const config = context.services.config;
|
|
122
|
+
const previousLiveTrust = config?.isTrustedFolder() ?? false;
|
|
123
|
+
let failedPhase: 'persistence' | 'live' = 'persistence';
|
|
124
|
+
try {
|
|
125
|
+
trustedFolders = loadTrustedFolders();
|
|
126
|
+
savedSnapshot = trustedFolders.snapshotValue(targetPath);
|
|
127
|
+
trustedFolders.setValue(targetPath, trustLevel);
|
|
128
|
+
if (config) {
|
|
129
|
+
failedPhase = 'live';
|
|
130
|
+
const cwd = config.getWorkingDir();
|
|
131
|
+
await config.setTrustedFolderLive(
|
|
132
|
+
resolveLocalWorkspaceTrust(
|
|
133
|
+
{ folderTrust: config.getFolderTrust() },
|
|
134
|
+
trustedFolders,
|
|
135
|
+
cwd,
|
|
136
|
+
) ?? false,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
type: 'message',
|
|
141
|
+
messageType: 'info',
|
|
142
|
+
content: `Trust level set to ${trustLevel} for: ${targetPath}`,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const rollbackFailures: unknown[] = [];
|
|
146
|
+
if (trustedFolders !== undefined && savedSnapshot !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
trustedFolders.restoreSnapshot(savedSnapshot);
|
|
149
|
+
} catch (rollbackError) {
|
|
150
|
+
rollbackFailures.push(rollbackError);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (config && failedPhase === 'live') {
|
|
154
|
+
try {
|
|
155
|
+
await config.setTrustedFolderLive(previousLiveTrust);
|
|
156
|
+
} catch (rollbackError) {
|
|
157
|
+
rollbackFailures.push(rollbackError);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const failure = combineTrustUpdateFailure(
|
|
161
|
+
error,
|
|
162
|
+
rollbackFailures,
|
|
163
|
+
'Trust update and rollback failed',
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
type: 'message',
|
|
167
|
+
messageType: 'error',
|
|
168
|
+
content: getTrustCommitErrorMessage(
|
|
169
|
+
failedPhase,
|
|
170
|
+
failure.error,
|
|
171
|
+
failure.rollbackSucceeded,
|
|
172
|
+
),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
119
176
|
},
|
|
120
177
|
};
|
|
@@ -37,6 +37,7 @@ function getTierBand(priority: number): string {
|
|
|
37
37
|
interface PolicyRuleDisplay {
|
|
38
38
|
priority?: number;
|
|
39
39
|
toolName?: string;
|
|
40
|
+
toolNamePrefix?: string;
|
|
40
41
|
decision: PolicyDecision;
|
|
41
42
|
argsPatternSource?: string;
|
|
42
43
|
source?: string;
|
|
@@ -45,6 +46,7 @@ interface PolicyRuleDisplay {
|
|
|
45
46
|
interface PolicyRuleBase {
|
|
46
47
|
priority?: number;
|
|
47
48
|
toolName?: string;
|
|
49
|
+
toolNamePrefix?: string;
|
|
48
50
|
decision: PolicyDecision;
|
|
49
51
|
source?: string;
|
|
50
52
|
}
|
|
@@ -56,6 +58,7 @@ function toPolicyRuleDisplay<T extends PolicyRuleBase>(
|
|
|
56
58
|
return {
|
|
57
59
|
priority: rule.priority,
|
|
58
60
|
toolName: rule.toolName,
|
|
61
|
+
toolNamePrefix: rule.toolNamePrefix,
|
|
59
62
|
decision: rule.decision,
|
|
60
63
|
argsPatternSource: extractPattern(rule),
|
|
61
64
|
source: rule.source,
|
|
@@ -107,7 +110,9 @@ function formatPolicyOutput(
|
|
|
107
110
|
lines.push(`${tier}:`);
|
|
108
111
|
|
|
109
112
|
for (const rule of tierRules) {
|
|
110
|
-
const toolName =
|
|
113
|
+
const toolName =
|
|
114
|
+
rule.toolName ??
|
|
115
|
+
(rule.toolNamePrefix !== undefined ? `${rule.toolNamePrefix}*` : '*');
|
|
111
116
|
const decision = formatDecision(rule.decision);
|
|
112
117
|
const priority = rule.priority ?? 0;
|
|
113
118
|
const argsPattern = rule.argsPatternSource
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Vybestack LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CommandKind,
|
|
9
|
+
type SlashCommand,
|
|
10
|
+
type CommandContext,
|
|
11
|
+
type SlashCommandActionReturn,
|
|
12
|
+
type ConfirmActionReturn,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import { MessageType } from '../types.js';
|
|
15
|
+
import { getRuntimeApi } from '../contexts/RuntimeContext.js';
|
|
16
|
+
import { fetchAllQuotaInfo } from './statsQuota.js';
|
|
17
|
+
import {
|
|
18
|
+
CodexRateLimitResetCreditsResponseSchema,
|
|
19
|
+
consumeCodexRateLimitResetCredit,
|
|
20
|
+
formatCodexResetCredits,
|
|
21
|
+
} from '@vybestack/llxprt-code-providers';
|
|
22
|
+
import type { CodexRateLimitResetCreditsResponse } from '@vybestack/llxprt-code-providers';
|
|
23
|
+
import type { OAuthManager } from '@vybestack/llxprt-code-providers/auth.js';
|
|
24
|
+
import { CodexOAuthTokenSchema } from '@vybestack/llxprt-code-auth';
|
|
25
|
+
import type { CommandArgumentSchema } from './schema/types.js';
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
|
|
28
|
+
// Non-interactive mode (`llxprt-code --command "/quota reset"`) does not load
|
|
29
|
+
// BuiltinCommandLoader (only FileCommandLoader) and createNonInteractiveUI is
|
|
30
|
+
// a no-op sink, so NO built-in slash command works non-interactively today.
|
|
31
|
+
// Making /quota reset work there requires a cross-cutting change to load
|
|
32
|
+
// built-ins + provide a real non-interactive UI sink, which affects every
|
|
33
|
+
// built-in and is out of scope for this issue.
|
|
34
|
+
|
|
35
|
+
const NO_RESET_CREDITS_MSG =
|
|
36
|
+
'No Codex reset credits available. Reset credits are earned via referrals or purchased.';
|
|
37
|
+
const NOT_AUTHED_CODEX_MSG =
|
|
38
|
+
'Not authenticated with Codex. Run /auth codex to login.';
|
|
39
|
+
const NO_REDEEMABLE_CREDITS_MSG =
|
|
40
|
+
'No reset credits available to redeem. Reset credits are earned via referrals or purchased.';
|
|
41
|
+
|
|
42
|
+
interface BucketCredits {
|
|
43
|
+
readonly bucket: string;
|
|
44
|
+
readonly availableCount: number;
|
|
45
|
+
readonly firstCreditId: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A bucket + credit guaranteed to be redeemable (non-null credit id).
|
|
50
|
+
* Returned by findRedeemableCredit so callers need no null-narrowing guard.
|
|
51
|
+
*/
|
|
52
|
+
interface RedeemableCredit {
|
|
53
|
+
readonly bucket: string;
|
|
54
|
+
readonly firstCreditId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface RedeemableCreditWithToken extends RedeemableCredit {
|
|
58
|
+
readonly accessToken: string;
|
|
59
|
+
readonly accountId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface BucketTokenInfo {
|
|
63
|
+
readonly accessToken: string | null;
|
|
64
|
+
readonly accountId: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a raw reset-credits payload via the Zod schema once.
|
|
69
|
+
* Returns the validated response or null when the shape does not validate.
|
|
70
|
+
*/
|
|
71
|
+
function safeParseResetCredits(
|
|
72
|
+
raw: Record<string, unknown>,
|
|
73
|
+
): CodexRateLimitResetCreditsResponse | null {
|
|
74
|
+
const parsed = CodexRateLimitResetCreditsResponseSchema.safeParse(raw);
|
|
75
|
+
return parsed.success ? parsed.data : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the OAuthManager via the runtime API, returning null when the
|
|
80
|
+
* runtime infrastructure is not registered (mirrors authCommand.getOAuthManager
|
|
81
|
+
* but degrades to null instead of throwing).
|
|
82
|
+
*/
|
|
83
|
+
function resolveOAuthManager(): OAuthManager | null {
|
|
84
|
+
try {
|
|
85
|
+
return getRuntimeApi().getCliOAuthManager();
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the trimmed base-url ephemeral setting, or undefined when blank.
|
|
93
|
+
*/
|
|
94
|
+
function resolveBaseUrl(): string | undefined {
|
|
95
|
+
const value = getRuntimeApi().getEphemeralSetting('base-url');
|
|
96
|
+
if (typeof value !== 'string') {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
return trimmed === '' ? undefined : trimmed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addInfo(context: CommandContext, text: string): void {
|
|
104
|
+
context.ui.addItem({ type: MessageType.INFO, text }, Date.now());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addError(context: CommandContext, text: string): void {
|
|
108
|
+
context.ui.addItem({ type: MessageType.ERROR, text }, Date.now());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Status (and default) action: show quota/rate-limit info for all providers.
|
|
113
|
+
*/
|
|
114
|
+
async function statusAction(context: CommandContext): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
const runtimeApi = getRuntimeApi();
|
|
117
|
+
const quotaLines = await fetchAllQuotaInfo(runtimeApi);
|
|
118
|
+
|
|
119
|
+
if (quotaLines.length === 0) {
|
|
120
|
+
addInfo(
|
|
121
|
+
context,
|
|
122
|
+
'No quota information available. Supported providers: Claude Code (OAuth), Codex (OAuth), Z.ai, Synthetic, Chutes, Kimi.',
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
addInfo(context, quotaLines.join('\n'));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
130
|
+
addError(context, `Failed to retrieve quota information: ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a single bucket's reset-credits payload via the Zod schema.
|
|
136
|
+
* Returns null when the shape does not validate.
|
|
137
|
+
*/
|
|
138
|
+
function parseBucketCredits(
|
|
139
|
+
bucket: string,
|
|
140
|
+
raw: Record<string, unknown>,
|
|
141
|
+
): BucketCredits | null {
|
|
142
|
+
const parsed = safeParseResetCredits(raw);
|
|
143
|
+
if (parsed === null) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const credits = parsed.rate_limit_reset_credits;
|
|
147
|
+
const availableCount = credits?.available_count ?? 0;
|
|
148
|
+
const creditList = credits?.credits ?? [];
|
|
149
|
+
const firstCreditId =
|
|
150
|
+
creditList.length > 0 && creditList[0]?.id ? creditList[0].id : null;
|
|
151
|
+
return { bucket, availableCount, firstCreditId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format a single bucket's reset credits into display lines (with optional
|
|
156
|
+
* bucket header), or null when there is nothing to show.
|
|
157
|
+
*/
|
|
158
|
+
function formatOneBucket(
|
|
159
|
+
bucket: string,
|
|
160
|
+
parsed: CodexRateLimitResetCreditsResponse,
|
|
161
|
+
multiBucket: boolean,
|
|
162
|
+
): string[] | null {
|
|
163
|
+
const lines = formatCodexResetCredits(parsed);
|
|
164
|
+
if (lines.length === 0) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const result: string[] = [];
|
|
168
|
+
if (multiBucket) {
|
|
169
|
+
result.push(`### Bucket: ${bucket}\n`);
|
|
170
|
+
}
|
|
171
|
+
result.push(...lines);
|
|
172
|
+
result.push('');
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Format all bucket reset-credits into display lines with bucket headers.
|
|
178
|
+
* Each bucket is parsed once via the schema; buckets that fail to validate
|
|
179
|
+
* are skipped.
|
|
180
|
+
*/
|
|
181
|
+
function formatAllResetCreditsLines(
|
|
182
|
+
creditsMap: Map<string, Record<string, unknown>>,
|
|
183
|
+
): string[] {
|
|
184
|
+
const output: string[] = [];
|
|
185
|
+
const multiBucket = creditsMap.size > 1;
|
|
186
|
+
|
|
187
|
+
for (const [bucket, raw] of creditsMap.entries()) {
|
|
188
|
+
const parsed = safeParseResetCredits(raw);
|
|
189
|
+
if (parsed === null) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const bucketLines = formatOneBucket(bucket, parsed, multiBucket);
|
|
193
|
+
if (bucketLines !== null) {
|
|
194
|
+
output.push(...bucketLines);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (output[output.length - 1] === '') {
|
|
199
|
+
output.pop();
|
|
200
|
+
}
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Credits subcommand action: show available Codex reset credits.
|
|
206
|
+
*/
|
|
207
|
+
async function creditsAction(context: CommandContext): Promise<void> {
|
|
208
|
+
const oauthManager = resolveOAuthManager();
|
|
209
|
+
if (!oauthManager) {
|
|
210
|
+
addInfo(context, NO_RESET_CREDITS_MSG);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const creditsMap = await oauthManager.getAllCodexRateLimitResetCredits();
|
|
216
|
+
|
|
217
|
+
if (creditsMap.size === 0) {
|
|
218
|
+
addInfo(context, NO_RESET_CREDITS_MSG);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lines = formatAllResetCreditsLines(creditsMap);
|
|
223
|
+
if (lines.length === 0) {
|
|
224
|
+
addInfo(context, NO_RESET_CREDITS_MSG);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
addInfo(context, lines.join('\n'));
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
231
|
+
addError(context, `Failed to retrieve reset credits: ${msg}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Determine whether the user is authenticated with Codex.
|
|
237
|
+
*/
|
|
238
|
+
async function isCodexAuthed(oauthManager: OAuthManager): Promise<boolean> {
|
|
239
|
+
const token = await oauthManager.getToken('codex');
|
|
240
|
+
if (token !== null) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const buckets = await oauthManager.listBuckets('codex');
|
|
244
|
+
return buckets.length > 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find all buckets with a credit that can be redeemed.
|
|
249
|
+
*/
|
|
250
|
+
async function findRedeemableCredits(
|
|
251
|
+
oauthManager: OAuthManager,
|
|
252
|
+
): Promise<readonly RedeemableCredit[]> {
|
|
253
|
+
const creditsMap = await oauthManager.getAllCodexRateLimitResetCredits();
|
|
254
|
+
const redeemableCredits: RedeemableCredit[] = [];
|
|
255
|
+
|
|
256
|
+
for (const [bucket, raw] of creditsMap.entries()) {
|
|
257
|
+
const parsed = parseBucketCredits(bucket, raw);
|
|
258
|
+
if (
|
|
259
|
+
parsed !== null &&
|
|
260
|
+
parsed.availableCount > 0 &&
|
|
261
|
+
parsed.firstCreditId !== null
|
|
262
|
+
) {
|
|
263
|
+
redeemableCredits.push({
|
|
264
|
+
bucket: parsed.bucket,
|
|
265
|
+
firstCreditId: parsed.firstCreditId,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return redeemableCredits;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve the access token + account_id for a Codex bucket.
|
|
274
|
+
*/
|
|
275
|
+
async function resolveBucketToken(
|
|
276
|
+
oauthManager: OAuthManager,
|
|
277
|
+
bucket: string,
|
|
278
|
+
): Promise<BucketTokenInfo> {
|
|
279
|
+
const tokenStore = oauthManager.getTokenStore();
|
|
280
|
+
const token = await tokenStore.getToken('codex', bucket);
|
|
281
|
+
if (token === null) {
|
|
282
|
+
return { accessToken: null, accountId: null };
|
|
283
|
+
}
|
|
284
|
+
const parsed = CodexOAuthTokenSchema.safeParse(token);
|
|
285
|
+
if (!parsed.success) {
|
|
286
|
+
return { accessToken: null, accountId: null };
|
|
287
|
+
}
|
|
288
|
+
// Guard against a token that expired between the credit-listing call and
|
|
289
|
+
// this resolve — sending an expired token to consume would yield a 401 and
|
|
290
|
+
// a misleading generic error.
|
|
291
|
+
if (parsed.data.expiry <= Math.floor(Date.now() / 1000)) {
|
|
292
|
+
return { accessToken: null, accountId: null };
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
accessToken: parsed.data.access_token,
|
|
296
|
+
accountId: parsed.data.account_id,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function findRedeemableCreditWithToken(
|
|
301
|
+
oauthManager: OAuthManager,
|
|
302
|
+
redeemableCredits: readonly RedeemableCredit[],
|
|
303
|
+
): Promise<RedeemableCreditWithToken | null> {
|
|
304
|
+
for (const redeemable of redeemableCredits) {
|
|
305
|
+
const tokenInfo = await resolveBucketToken(oauthManager, redeemable.bucket);
|
|
306
|
+
if (tokenInfo.accessToken !== null && tokenInfo.accountId !== null) {
|
|
307
|
+
return {
|
|
308
|
+
...redeemable,
|
|
309
|
+
accessToken: tokenInfo.accessToken,
|
|
310
|
+
accountId: tokenInfo.accountId,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const RESET_CONFIRMATION_PROMPT =
|
|
318
|
+
'Redeem a Codex rate-limit-reset credit to reset your rate-limit window? This consumes one earned/purchased reset credit.';
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Build the confirm_action return value for /quota reset, deriving the
|
|
322
|
+
* original raw invocation (falling back to a canonical form when the
|
|
323
|
+
* invocation context is absent or blank).
|
|
324
|
+
*/
|
|
325
|
+
function buildResetConfirmation(context: CommandContext): ConfirmActionReturn {
|
|
326
|
+
const rawInvocation =
|
|
327
|
+
context.invocation?.raw && context.invocation.raw.trim() !== ''
|
|
328
|
+
? context.invocation.raw
|
|
329
|
+
: '/quota reset codex';
|
|
330
|
+
return {
|
|
331
|
+
type: 'confirm_action',
|
|
332
|
+
prompt: RESET_CONFIRMATION_PROMPT,
|
|
333
|
+
originalInvocation: { raw: rawInvocation },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Consume the redeemable credit and render the result (success / already
|
|
339
|
+
* redeemed / failure) plus a refreshed quota snapshot. Returns void — all
|
|
340
|
+
* user feedback flows through addInfo/addError.
|
|
341
|
+
*/
|
|
342
|
+
async function redeemResetCredit(
|
|
343
|
+
context: CommandContext,
|
|
344
|
+
accessToken: string,
|
|
345
|
+
accountId: string,
|
|
346
|
+
creditId: string,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
// Both the credit listing (getAllCodexRateLimitResetCredits) and this
|
|
349
|
+
// consume call resolve base-url from the same runtime settings source, so
|
|
350
|
+
// list and consume always target the same backend host.
|
|
351
|
+
const baseUrl = resolveBaseUrl();
|
|
352
|
+
const redeemRequestId = randomUUID();
|
|
353
|
+
const consumeResult = await consumeCodexRateLimitResetCredit(
|
|
354
|
+
accessToken,
|
|
355
|
+
accountId,
|
|
356
|
+
creditId,
|
|
357
|
+
redeemRequestId,
|
|
358
|
+
baseUrl,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (consumeResult === null) {
|
|
362
|
+
addError(
|
|
363
|
+
context,
|
|
364
|
+
'Failed to reset rate-limit window. Please try again later.',
|
|
365
|
+
);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (consumeResult.code === 'reset') {
|
|
370
|
+
addInfo(context, 'Rate-limit window reset successfully.');
|
|
371
|
+
} else {
|
|
372
|
+
addInfo(context, 'Credit already redeemed.');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// fetchAllQuotaInfo has its own internal error handling and returns [] on
|
|
376
|
+
// failure, so it never throws — no extra guard is needed here.
|
|
377
|
+
const runtimeApi = getRuntimeApi();
|
|
378
|
+
const quotaLines = await fetchAllQuotaInfo(runtimeApi);
|
|
379
|
+
if (quotaLines.length > 0) {
|
|
380
|
+
addInfo(context, quotaLines.join('\n'));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Reset subcommand action: redeem a Codex rate-limit-reset credit.
|
|
386
|
+
*/
|
|
387
|
+
async function resetAction(
|
|
388
|
+
context: CommandContext,
|
|
389
|
+
args: string,
|
|
390
|
+
): Promise<SlashCommandActionReturn | void> {
|
|
391
|
+
const tokens = args
|
|
392
|
+
.trim()
|
|
393
|
+
.split(/\s+/)
|
|
394
|
+
.filter((t) => t !== '');
|
|
395
|
+
const provider = tokens[0] ?? '';
|
|
396
|
+
|
|
397
|
+
if (provider !== '' && provider !== 'codex') {
|
|
398
|
+
addError(context, "Only 'codex' supports reset.");
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
if (tokens.length > 1) {
|
|
402
|
+
addError(context, 'Too many arguments. Usage: /quota reset [codex]');
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const oauthManager = resolveOAuthManager();
|
|
407
|
+
if (!oauthManager) {
|
|
408
|
+
addInfo(context, NOT_AUTHED_CODEX_MSG);
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const authed = await isCodexAuthed(oauthManager);
|
|
414
|
+
if (!authed) {
|
|
415
|
+
addInfo(context, NOT_AUTHED_CODEX_MSG);
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const redeemableCredits = await findRedeemableCredits(oauthManager);
|
|
420
|
+
if (redeemableCredits.length === 0) {
|
|
421
|
+
addInfo(context, NO_REDEEMABLE_CREDITS_MSG);
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const redeemable = await findRedeemableCreditWithToken(
|
|
426
|
+
oauthManager,
|
|
427
|
+
redeemableCredits,
|
|
428
|
+
);
|
|
429
|
+
if (redeemable === null) {
|
|
430
|
+
addError(
|
|
431
|
+
context,
|
|
432
|
+
'Codex credentials for all credit-bearing buckets are unavailable or expired. Run /auth codex to re-authenticate.',
|
|
433
|
+
);
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Confirmation gate: redeeming consumes a paid/referral-earned resource,
|
|
438
|
+
// so — like other mutating commands — prompt before performing the
|
|
439
|
+
// mutation. The confirmed re-invocation arrives with
|
|
440
|
+
// overwriteConfirmed === true and skips this gate to actually consume.
|
|
441
|
+
if (context.overwriteConfirmed !== true) {
|
|
442
|
+
return buildResetConfirmation(context);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await redeemResetCredit(
|
|
446
|
+
context,
|
|
447
|
+
redeemable.accessToken,
|
|
448
|
+
redeemable.accountId,
|
|
449
|
+
redeemable.firstCreditId,
|
|
450
|
+
);
|
|
451
|
+
return undefined;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
454
|
+
addError(context, `Failed to reset rate-limit window: ${msg}`);
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Schema for /quota reset autocomplete: offers 'codex' as the provider.
|
|
461
|
+
*/
|
|
462
|
+
const resetSchema: CommandArgumentSchema = [
|
|
463
|
+
{
|
|
464
|
+
kind: 'value',
|
|
465
|
+
name: 'provider',
|
|
466
|
+
description: 'Provider to reset',
|
|
467
|
+
options: [{ value: 'codex', description: 'Codex (ChatGPT)' }],
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
export const quotaCommand: SlashCommand = {
|
|
472
|
+
name: 'quota',
|
|
473
|
+
description:
|
|
474
|
+
'Manage quota and rate-limit reset. Usage: /quota [status|credits|reset]',
|
|
475
|
+
kind: CommandKind.BUILT_IN,
|
|
476
|
+
action: statusAction,
|
|
477
|
+
subCommands: [
|
|
478
|
+
{
|
|
479
|
+
name: 'status',
|
|
480
|
+
description: 'Show quota/rate-limit status for all providers.',
|
|
481
|
+
kind: CommandKind.BUILT_IN,
|
|
482
|
+
autoExecute: true,
|
|
483
|
+
action: statusAction,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: 'credits',
|
|
487
|
+
description: 'Show available Codex rate-limit-reset credits.',
|
|
488
|
+
kind: CommandKind.BUILT_IN,
|
|
489
|
+
autoExecute: true,
|
|
490
|
+
action: creditsAction,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'reset',
|
|
494
|
+
description:
|
|
495
|
+
'Redeem a Codex rate-limit-reset credit to reset the rate-limit window.',
|
|
496
|
+
kind: CommandKind.BUILT_IN,
|
|
497
|
+
schema: resetSchema,
|
|
498
|
+
action: resetAction,
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
};
|
|
@@ -74,7 +74,7 @@ async function quotaSubcommandAction(context: CommandContext): Promise<void> {
|
|
|
74
74
|
context.ui.addItem(
|
|
75
75
|
{
|
|
76
76
|
type: MessageType.INFO,
|
|
77
|
-
text: 'No quota information available. Supported providers:
|
|
77
|
+
text: 'No quota information available. Supported providers: Claude Code (OAuth), Codex (OAuth), Z.ai, Synthetic, Chutes, Kimi.',
|
|
78
78
|
},
|
|
79
79
|
Date.now(),
|
|
80
80
|
);
|
|
@@ -278,7 +278,7 @@ export const statsCommand: SlashCommand = {
|
|
|
278
278
|
{
|
|
279
279
|
name: 'quota',
|
|
280
280
|
description:
|
|
281
|
-
'Show quota/usage information for OAuth providers (
|
|
281
|
+
'Show quota/usage information for OAuth providers (Claude Code, Codex) and API-key providers (Z.ai, Synthetic, Chutes, Kimi).',
|
|
282
282
|
kind: CommandKind.BUILT_IN,
|
|
283
283
|
action: quotaSubcommandAction,
|
|
284
284
|
},
|
|
@@ -305,7 +305,7 @@ async function fetchOAuthQuotaLines(
|
|
|
305
305
|
|
|
306
306
|
const anthropicLines = formatAnthropicLines(anthropicUsageInfo);
|
|
307
307
|
if (anthropicLines.length > 0) {
|
|
308
|
-
output.push('##
|
|
308
|
+
output.push('## Claude Code Quota Information\n');
|
|
309
309
|
output.push(...anthropicLines);
|
|
310
310
|
}
|
|
311
311
|
|