@duckmind/dm-darwin-x64 0.35.8 → 0.36.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/dm +0 -0
- package/extensions/.dm-extensions.json +1 -1
- package/extensions/dm-ultradex/commands.test.ts +71 -13
- package/extensions/dm-ultradex/commands.ts +51 -14
- package/extensions/dm-ultradex/package-lock.json +3 -3
- package/extensions/dm-usage/README.md +8 -0
- package/extensions/dm-usage/index.ts +504 -8
- package/package.json +1 -1
package/dm
CHANGED
|
Binary file
|
|
@@ -87,7 +87,7 @@ describe("registerCommands", () => {
|
|
|
87
87
|
);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
it("returns
|
|
90
|
+
it("returns restored autocomplete while hiding repair-only account identifiers", () => {
|
|
91
91
|
const registerCommand = vi.fn();
|
|
92
92
|
registerCommands(
|
|
93
93
|
{ registerCommand } as never,
|
|
@@ -102,21 +102,29 @@ describe("registerCommands", () => {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
const subcommands = commandOptions.getArgumentCompletions("");
|
|
105
|
-
expect(subcommands?.map((item) => item.value)).toEqual([
|
|
105
|
+
expect(subcommands?.map((item) => item.value)).toEqual([
|
|
106
|
+
"sync",
|
|
107
|
+
"use",
|
|
108
|
+
"show",
|
|
109
|
+
"rotation",
|
|
110
|
+
"verify",
|
|
111
|
+
"path",
|
|
112
|
+
"reset",
|
|
113
|
+
"help",
|
|
114
|
+
]);
|
|
106
115
|
expect(subcommands?.map((item) => item.value)).not.toContain("accounts");
|
|
107
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("show");
|
|
108
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("use");
|
|
109
116
|
expect(subcommands?.map((item) => item.value)).not.toContain("refresh");
|
|
110
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("rotation");
|
|
111
117
|
expect(subcommands?.map((item) => item.value)).not.toContain("reauth");
|
|
112
118
|
expect(subcommands?.map((item) => item.value)).not.toContain("footer");
|
|
113
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("verify");
|
|
114
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("path");
|
|
115
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("reset");
|
|
116
|
-
expect(subcommands?.map((item) => item.value)).not.toContain("help");
|
|
117
119
|
|
|
118
120
|
const useAccounts = commandOptions.getArgumentCompletions("use a");
|
|
119
|
-
expect(useAccounts).
|
|
121
|
+
expect(useAccounts).toEqual([
|
|
122
|
+
{ value: "use alpha@example.com", label: "alpha@example.com" },
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
expect(commandOptions.getArgumentCompletions("reset m")).toEqual([
|
|
126
|
+
{ value: "reset manual", label: "manual" },
|
|
127
|
+
]);
|
|
120
128
|
|
|
121
129
|
const refreshAccounts = commandOptions.getArgumentCompletions("refresh a");
|
|
122
130
|
expect(refreshAccounts).toBeNull();
|
|
@@ -140,12 +148,12 @@ describe("registerCommands", () => {
|
|
|
140
148
|
});
|
|
141
149
|
|
|
142
150
|
expect(notify).toHaveBeenCalledWith(
|
|
143
|
-
"/ultradex requires a subcommand in non-interactive mode. Use /ultradex
|
|
151
|
+
"/ultradex requires a subcommand in non-interactive mode. Use /ultradex help.",
|
|
144
152
|
"warning",
|
|
145
153
|
);
|
|
146
154
|
});
|
|
147
155
|
|
|
148
|
-
it("hides
|
|
156
|
+
it("hides repair-only subcommands even when typed directly", async () => {
|
|
149
157
|
const registerCommand = vi.fn();
|
|
150
158
|
registerCommands(
|
|
151
159
|
{ registerCommand } as never,
|
|
@@ -162,7 +170,57 @@ describe("registerCommands", () => {
|
|
|
162
170
|
ui: { notify },
|
|
163
171
|
});
|
|
164
172
|
|
|
165
|
-
expect(notify).toHaveBeenCalledWith(
|
|
173
|
+
expect(notify).toHaveBeenCalledWith(
|
|
174
|
+
expect.stringContaining(
|
|
175
|
+
"use: select, activate, or remove managed account",
|
|
176
|
+
),
|
|
177
|
+
"info",
|
|
178
|
+
);
|
|
179
|
+
const helpText = String(notify.mock.calls.at(-1)?.[0] ?? "");
|
|
180
|
+
for (const label of [
|
|
181
|
+
"sync: download and decrypt DuckMind managed accounts",
|
|
182
|
+
"use: select, activate, or remove managed account",
|
|
183
|
+
"show: managed account and usage summary",
|
|
184
|
+
"rotation: current rotation behavior",
|
|
185
|
+
"verify: runtime health checks",
|
|
186
|
+
"path: storage and settings locations",
|
|
187
|
+
"reset: clear manual or quota state",
|
|
188
|
+
"help: command usage",
|
|
189
|
+
]) {
|
|
190
|
+
expect(helpText).toContain(label);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("runs restored state subcommands instead of falling back to sync-only help", async () => {
|
|
195
|
+
const registerCommand = vi.fn();
|
|
196
|
+
const accountManager = {
|
|
197
|
+
getAccounts: () => [],
|
|
198
|
+
hasManualAccount: vi.fn(() => true),
|
|
199
|
+
clearManualAccount: vi.fn(),
|
|
200
|
+
clearAllQuotaExhaustion: vi.fn(() => 0),
|
|
201
|
+
} as unknown as AccountManager;
|
|
202
|
+
const statusController = createStatusControllerMock();
|
|
203
|
+
registerCommands(
|
|
204
|
+
{ registerCommand } as never,
|
|
205
|
+
accountManager,
|
|
206
|
+
statusController,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const commandOptions = registerCommand.mock.calls[0]?.[1] as {
|
|
210
|
+
handler: (args: string, ctx: unknown) => Promise<void>;
|
|
211
|
+
};
|
|
212
|
+
const notify = vi.fn();
|
|
213
|
+
await commandOptions.handler("reset manual", {
|
|
214
|
+
hasUI: false,
|
|
215
|
+
ui: { notify },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(accountManager.clearManualAccount).toHaveBeenCalledOnce();
|
|
219
|
+
expect(mocks.syncManagedAccountsFromDuckMind).not.toHaveBeenCalled();
|
|
220
|
+
expect(notify).toHaveBeenCalledWith(
|
|
221
|
+
expect.stringContaining("reset: target=manual"),
|
|
222
|
+
"info",
|
|
223
|
+
);
|
|
166
224
|
});
|
|
167
225
|
|
|
168
226
|
it("uses the explicit sync secret without remembering it after a successful sync", async () => {
|
|
@@ -33,8 +33,17 @@ import { formatResetAt, isUsageUntouched } from "./usage";
|
|
|
33
33
|
const SETTINGS_FILE = getAgentSettingsPath();
|
|
34
34
|
const NO_ACCOUNTS_MESSAGE =
|
|
35
35
|
"No managed accounts found. Run /ultradex sync <secret> to import DuckMind managed accounts.";
|
|
36
|
-
const HELP_TEXT =
|
|
37
|
-
"Usage: /ultradex sync [secret]"
|
|
36
|
+
const HELP_TEXT = [
|
|
37
|
+
"Usage: /ultradex [sync [secret]|use [identifier]|show|rotation|verify|path|reset [manual|quota|all]|help]",
|
|
38
|
+
"sync: download and decrypt DuckMind managed accounts",
|
|
39
|
+
"use: select, activate, or remove managed account",
|
|
40
|
+
"show: managed account and usage summary",
|
|
41
|
+
"rotation: current rotation behavior",
|
|
42
|
+
"verify: runtime health checks",
|
|
43
|
+
"path: storage and settings locations",
|
|
44
|
+
"reset: clear manual or quota state",
|
|
45
|
+
"help: command usage",
|
|
46
|
+
].join("\n");
|
|
38
47
|
const SUBCOMMANDS = [
|
|
39
48
|
"accounts",
|
|
40
49
|
"use",
|
|
@@ -52,6 +61,13 @@ const SUBCOMMANDS = [
|
|
|
52
61
|
const RESET_TARGETS = ["manual", "quota", "all"] as const;
|
|
53
62
|
const VISIBLE_SUBCOMMANDS = [
|
|
54
63
|
"sync",
|
|
64
|
+
"use",
|
|
65
|
+
"show",
|
|
66
|
+
"rotation",
|
|
67
|
+
"verify",
|
|
68
|
+
"path",
|
|
69
|
+
"reset",
|
|
70
|
+
"help",
|
|
55
71
|
] as const;
|
|
56
72
|
|
|
57
73
|
type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
@@ -156,6 +172,10 @@ function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
|
|
|
156
172
|
return matches.length > 0 ? toAutocompleteItems(matches) : null;
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
function isVisibleSubcommand(value: Subcommand): boolean {
|
|
176
|
+
return VISIBLE_SUBCOMMANDS.some((subcommand) => subcommand === value);
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
function getAccountCompletions(
|
|
160
180
|
subcommand: "accounts" | "use" | "reauth",
|
|
161
181
|
prefix: string,
|
|
@@ -208,6 +228,16 @@ function getCommandCompletions(
|
|
|
208
228
|
return getSubcommandCompletions(trimmedStart.toLowerCase());
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
|
|
232
|
+
const rest = trimmedStart.slice(firstSpaceIndex + 1).trimStart();
|
|
233
|
+
|
|
234
|
+
if (subcommand === "use") {
|
|
235
|
+
return getAccountCompletions("use", rest, accountManager);
|
|
236
|
+
}
|
|
237
|
+
if (subcommand === "reset") {
|
|
238
|
+
return getResetCompletions(rest);
|
|
239
|
+
}
|
|
240
|
+
|
|
211
241
|
return null;
|
|
212
242
|
}
|
|
213
243
|
|
|
@@ -1016,6 +1046,13 @@ async function openMainPanel(
|
|
|
1016
1046
|
): Promise<void> {
|
|
1017
1047
|
const actions = [
|
|
1018
1048
|
"sync: download and decrypt DuckMind managed accounts",
|
|
1049
|
+
"use: select, activate, or remove managed account",
|
|
1050
|
+
"show: managed account and usage summary",
|
|
1051
|
+
"rotation: current rotation behavior",
|
|
1052
|
+
"verify: runtime health checks",
|
|
1053
|
+
"path: storage and settings locations",
|
|
1054
|
+
"reset: clear manual or quota state",
|
|
1055
|
+
"help: command usage",
|
|
1019
1056
|
];
|
|
1020
1057
|
|
|
1021
1058
|
const selected = await ctx.ui.select("Ultradex", actions);
|
|
@@ -1043,7 +1080,7 @@ export function registerCommands(
|
|
|
1043
1080
|
): void {
|
|
1044
1081
|
pi.registerCommand("ultradex", {
|
|
1045
1082
|
description:
|
|
1046
|
-
"
|
|
1083
|
+
"Manage Ultradex sync, account selection, status, rotation, and health",
|
|
1047
1084
|
getArgumentCompletions: (argumentPrefix: string) =>
|
|
1048
1085
|
getCommandCompletions(argumentPrefix, accountManager),
|
|
1049
1086
|
handler: async (
|
|
@@ -1054,7 +1091,7 @@ export function registerCommands(
|
|
|
1054
1091
|
if (!parsed.subcommand) {
|
|
1055
1092
|
if (!ctx.hasUI) {
|
|
1056
1093
|
ctx.ui.notify(
|
|
1057
|
-
"/ultradex requires a subcommand in non-interactive mode. Use /ultradex
|
|
1094
|
+
"/ultradex requires a subcommand in non-interactive mode. Use /ultradex help.",
|
|
1058
1095
|
"warning",
|
|
1059
1096
|
);
|
|
1060
1097
|
return;
|
|
@@ -1063,17 +1100,17 @@ export function registerCommands(
|
|
|
1063
1100
|
return;
|
|
1064
1101
|
}
|
|
1065
1102
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1103
|
+
if (!isSubcommand(parsed.subcommand)) {
|
|
1104
|
+
ctx.ui.notify(`Unknown subcommand: ${parsed.subcommand}`, "warning");
|
|
1105
|
+
runHelpSubcommand(ctx);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (!isVisibleSubcommand(parsed.subcommand)) {
|
|
1109
|
+
runHelpSubcommand(ctx);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1075
1112
|
|
|
1076
|
-
|
|
1113
|
+
await runSubcommand(
|
|
1077
1114
|
parsed.subcommand,
|
|
1078
1115
|
parsed.rest,
|
|
1079
1116
|
pi,
|
|
@@ -3605,9 +3605,9 @@
|
|
|
3605
3605
|
"license": "MIT"
|
|
3606
3606
|
},
|
|
3607
3607
|
"node_modules/cosmiconfig": {
|
|
3608
|
-
"version": "9.0.
|
|
3609
|
-
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.
|
|
3610
|
-
"integrity": "sha512-
|
|
3608
|
+
"version": "9.0.2",
|
|
3609
|
+
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
|
|
3610
|
+
"integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
|
|
3611
3611
|
"dev": true,
|
|
3612
3612
|
"license": "MIT",
|
|
3613
3613
|
"dependencies": {
|
|
@@ -7,12 +7,20 @@ Bundled DM extension that adds `/usage`, an interactive dashboard for local sess
|
|
|
7
7
|
## Included command surface
|
|
8
8
|
|
|
9
9
|
- `/usage`
|
|
10
|
+
- `/usage use [provider-id|clear|remove <provider-id>]`
|
|
11
|
+
- `/usage show`
|
|
12
|
+
- `/usage rotation`
|
|
13
|
+
- `/usage verify`
|
|
14
|
+
- `/usage path`
|
|
15
|
+
- `/usage reset [manual|quota|all]`
|
|
16
|
+
- `/usage help`
|
|
10
17
|
|
|
11
18
|
## Runtime notes
|
|
12
19
|
|
|
13
20
|
- Reads session JSONL files recursively from `~/.dm/agent/sessions/`.
|
|
14
21
|
- Respects `DM_CODING_AGENT_DIR`; `PI_CODING_AGENT_DIR` is accepted only as a compatibility fallback.
|
|
15
22
|
- Displays raw provider `openai-codex` as `duckmind-ultra` and `openrouter` as `duckmind-standard` without changing recorded session data.
|
|
23
|
+
- Restored account-tool subcommands keep local managed-account metadata in `~/.dm/agent/usage/state.json`; they do not store API secrets or route provider requests.
|
|
16
24
|
- `Tab` / `←` / `→` switch periods, `v` toggles table/insights view, and `q` / `Esc` closes the dashboard.
|
|
17
25
|
- Cost data comes from recorded assistant usage payloads, so accuracy depends on providers reporting `usage.cost.total`.
|
|
18
26
|
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
12
|
-
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi, type AutocompleteItem } from "@mariozechner/pi-tui";
|
|
13
|
+
import { constants as fsConstants } from "node:fs";
|
|
14
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
|
|
17
18
|
// =============================================================================
|
|
@@ -90,6 +91,31 @@ interface UsageData {
|
|
|
90
91
|
|
|
91
92
|
type TabName = "today" | "thisWeek" | "lastWeek" | "allTime";
|
|
92
93
|
type ViewMode = "table" | "insights";
|
|
94
|
+
type UsageSubcommand = "use" | "show" | "rotation" | "verify" | "path" | "reset" | "help";
|
|
95
|
+
type UsageResetTarget = "manual" | "quota" | "all";
|
|
96
|
+
|
|
97
|
+
interface UsageManagedAccount {
|
|
98
|
+
id: string;
|
|
99
|
+
label: string;
|
|
100
|
+
provider: string;
|
|
101
|
+
source: "session" | "manual";
|
|
102
|
+
updatedAt: string;
|
|
103
|
+
quotaExhaustedUntil?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface UsageManagedState {
|
|
107
|
+
version: 1;
|
|
108
|
+
manualAccountId?: string;
|
|
109
|
+
removedAccountIds: string[];
|
|
110
|
+
accounts: UsageManagedAccount[];
|
|
111
|
+
updatedAt: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface UsageManagedSummary {
|
|
115
|
+
data: UsageData;
|
|
116
|
+
state: UsageManagedState;
|
|
117
|
+
accounts: UsageManagedAccount[];
|
|
118
|
+
}
|
|
93
119
|
|
|
94
120
|
// =============================================================================
|
|
95
121
|
// Column Configuration
|
|
@@ -195,10 +221,25 @@ const TABLE_LAYOUTS: TableLayoutCandidate[] = [
|
|
|
195
221
|
// Data Collection
|
|
196
222
|
// =============================================================================
|
|
197
223
|
|
|
198
|
-
function
|
|
224
|
+
function getAgentDir(): string {
|
|
199
225
|
// Respect DM_CODING_AGENT_DIR, keeping PI_CODING_AGENT_DIR only as a compatibility fallback
|
|
200
|
-
|
|
201
|
-
|
|
226
|
+
return process.env.DM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".dm", "agent");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getSessionsDir(): string {
|
|
230
|
+
return join(getAgentDir(), "sessions");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getUsageStateDir(): string {
|
|
234
|
+
return join(getAgentDir(), "usage");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getUsageStatePath(): string {
|
|
238
|
+
return join(getUsageStateDir(), "state.json");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getAgentSettingsPath(): string {
|
|
242
|
+
return join(getAgentDir(), "settings.json");
|
|
202
243
|
}
|
|
203
244
|
|
|
204
245
|
async function collectSessionFilesRecursively(dir: string, files: string[], signal?: AbortSignal): Promise<void> {
|
|
@@ -513,6 +554,449 @@ async function collectUsageData(signal?: AbortSignal): Promise<UsageData | null>
|
|
|
513
554
|
return data;
|
|
514
555
|
}
|
|
515
556
|
|
|
557
|
+
// =============================================================================
|
|
558
|
+
// Managed Account Command Helpers
|
|
559
|
+
// =============================================================================
|
|
560
|
+
|
|
561
|
+
const USAGE_SUBCOMMANDS = ["use", "show", "rotation", "verify", "path", "reset", "help"] as const;
|
|
562
|
+
const USAGE_RESET_TARGETS = ["manual", "quota", "all"] as const;
|
|
563
|
+
const USAGE_HELP_TEXT = [
|
|
564
|
+
"Usage: /usage [use|show|rotation|verify|path|reset|help]",
|
|
565
|
+
"use: select, activate, or remove managed account",
|
|
566
|
+
"show: managed account and usage summary",
|
|
567
|
+
"rotation: current rotation behavior",
|
|
568
|
+
"verify: runtime health checks",
|
|
569
|
+
"path: storage and settings locations",
|
|
570
|
+
"reset: clear manual or quota state",
|
|
571
|
+
].join("\n");
|
|
572
|
+
|
|
573
|
+
function parseUsageCommandArgs(args: string): { subcommand?: string; rest: string } {
|
|
574
|
+
const trimmed = args.trim();
|
|
575
|
+
if (!trimmed) return { rest: "" };
|
|
576
|
+
const firstSpaceIndex = trimmed.indexOf(" ");
|
|
577
|
+
if (firstSpaceIndex < 0) return { subcommand: trimmed.toLowerCase(), rest: "" };
|
|
578
|
+
return {
|
|
579
|
+
subcommand: trimmed.slice(0, firstSpaceIndex).toLowerCase(),
|
|
580
|
+
rest: trimmed.slice(firstSpaceIndex + 1).trim(),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function isUsageSubcommand(value: string): value is UsageSubcommand {
|
|
585
|
+
return USAGE_SUBCOMMANDS.some((subcommand) => subcommand === value);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function parseUsageResetTarget(value: string): UsageResetTarget | undefined {
|
|
589
|
+
if (value === "manual" || value === "quota" || value === "all") return value;
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function toAutocompleteItems(values: readonly string[]): AutocompleteItem[] {
|
|
594
|
+
return values.map((value) => ({ value, label: value }));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getUsageCommandCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
598
|
+
const trimmedStart = argumentPrefix.trimStart();
|
|
599
|
+
if (!trimmedStart) return toAutocompleteItems(USAGE_SUBCOMMANDS);
|
|
600
|
+
|
|
601
|
+
const firstSpaceIndex = trimmedStart.indexOf(" ");
|
|
602
|
+
if (firstSpaceIndex < 0) {
|
|
603
|
+
const matches = USAGE_SUBCOMMANDS.filter((value) => value.startsWith(trimmedStart.toLowerCase()));
|
|
604
|
+
return matches.length > 0 ? toAutocompleteItems(matches) : null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
|
|
608
|
+
const rest = trimmedStart.slice(firstSpaceIndex + 1).toLowerCase();
|
|
609
|
+
if (subcommand === "reset") {
|
|
610
|
+
const matches = USAGE_RESET_TARGETS.filter((value) => value.startsWith(rest));
|
|
611
|
+
return matches.length > 0 ? matches.map((value) => ({ value: `reset ${value}`, label: value })) : null;
|
|
612
|
+
}
|
|
613
|
+
if (subcommand === "use") {
|
|
614
|
+
const matches = ["clear", "remove"].filter((value) => value.startsWith(rest));
|
|
615
|
+
return matches.length > 0 ? matches.map((value) => ({ value: `use ${value}`, label: value })) : null;
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function emptyUsageManagedState(): UsageManagedState {
|
|
621
|
+
return { version: 1, removedAccountIds: [], accounts: [], updatedAt: new Date(0).toISOString() };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function asString(value: unknown): string | undefined {
|
|
625
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function normalizeUsageManagedAccount(value: unknown): UsageManagedAccount | undefined {
|
|
629
|
+
if (!value || typeof value !== "object") return undefined;
|
|
630
|
+
const record = value as Record<string, unknown>;
|
|
631
|
+
const id = asString(record.id);
|
|
632
|
+
const provider = asString(record.provider) ?? id;
|
|
633
|
+
if (!id || !provider) return undefined;
|
|
634
|
+
const label = asString(record.label) ?? formatProviderName(provider);
|
|
635
|
+
const source = record.source === "manual" ? "manual" : "session";
|
|
636
|
+
const updatedAt = asString(record.updatedAt) ?? new Date(0).toISOString();
|
|
637
|
+
const quota =
|
|
638
|
+
typeof record.quotaExhaustedUntil === "number" && Number.isFinite(record.quotaExhaustedUntil)
|
|
639
|
+
? record.quotaExhaustedUntil
|
|
640
|
+
: undefined;
|
|
641
|
+
return { id, label, provider, source, updatedAt, quotaExhaustedUntil: quota };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function normalizeUsageManagedState(value: unknown): UsageManagedState {
|
|
645
|
+
const empty = emptyUsageManagedState();
|
|
646
|
+
if (!value || typeof value !== "object") return empty;
|
|
647
|
+
const record = value as Record<string, unknown>;
|
|
648
|
+
const accounts = Array.isArray(record.accounts)
|
|
649
|
+
? record.accounts.map(normalizeUsageManagedAccount).filter((account): account is UsageManagedAccount => Boolean(account))
|
|
650
|
+
: [];
|
|
651
|
+
const removedAccountIds = Array.isArray(record.removedAccountIds)
|
|
652
|
+
? record.removedAccountIds.map(asString).filter((id): id is string => Boolean(id))
|
|
653
|
+
: [];
|
|
654
|
+
return {
|
|
655
|
+
version: 1,
|
|
656
|
+
manualAccountId: asString(record.manualAccountId),
|
|
657
|
+
removedAccountIds,
|
|
658
|
+
accounts,
|
|
659
|
+
updatedAt: asString(record.updatedAt) ?? empty.updatedAt,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
664
|
+
return error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function readUsageManagedState(): Promise<{ state: UsageManagedState; error?: string }> {
|
|
668
|
+
try {
|
|
669
|
+
const raw = await readFile(getUsageStatePath(), "utf8");
|
|
670
|
+
return { state: normalizeUsageManagedState(JSON.parse(raw)) };
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (getErrorCode(error) === "ENOENT") return { state: emptyUsageManagedState() };
|
|
673
|
+
return { state: emptyUsageManagedState(), error: error instanceof Error ? error.message : String(error) };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function saveUsageManagedState(state: UsageManagedState): Promise<void> {
|
|
678
|
+
state.version = 1;
|
|
679
|
+
state.updatedAt = new Date().toISOString();
|
|
680
|
+
state.removedAccountIds = [...new Set(state.removedAccountIds)].sort();
|
|
681
|
+
state.accounts = state.accounts
|
|
682
|
+
.filter((account) => !state.removedAccountIds.includes(account.id))
|
|
683
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
684
|
+
await mkdir(dirname(getUsageStatePath()), { recursive: true });
|
|
685
|
+
await writeFile(getUsageStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isQuotaActive(account: UsageManagedAccount): boolean {
|
|
689
|
+
return typeof account.quotaExhaustedUntil === "number" && account.quotaExhaustedUntil > Date.now();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function getAccountStats(data: UsageData, account: UsageManagedAccount): ProviderStats | undefined {
|
|
693
|
+
return data.allTime.providers.get(account.provider);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function getManagedAccounts(data: UsageData, state: UsageManagedState): UsageManagedAccount[] {
|
|
697
|
+
const now = new Date().toISOString();
|
|
698
|
+
const accounts = new Map<string, UsageManagedAccount>();
|
|
699
|
+
for (const account of state.accounts) {
|
|
700
|
+
if (!state.removedAccountIds.includes(account.id)) accounts.set(account.id, account);
|
|
701
|
+
}
|
|
702
|
+
for (const provider of data.allTime.providers.keys()) {
|
|
703
|
+
if (state.removedAccountIds.includes(provider) || accounts.has(provider)) continue;
|
|
704
|
+
accounts.set(provider, {
|
|
705
|
+
id: provider,
|
|
706
|
+
label: formatProviderName(provider),
|
|
707
|
+
provider,
|
|
708
|
+
source: "session",
|
|
709
|
+
updatedAt: now,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return Array.from(accounts.values()).sort((a, b) => {
|
|
714
|
+
const manualId = state.manualAccountId;
|
|
715
|
+
if (a.id === manualId && b.id !== manualId) return -1;
|
|
716
|
+
if (b.id === manualId && a.id !== manualId) return 1;
|
|
717
|
+
if (isQuotaActive(a) !== isQuotaActive(b)) return isQuotaActive(a) ? 1 : -1;
|
|
718
|
+
const costDelta = (getAccountStats(data, b)?.cost ?? 0) - (getAccountStats(data, a)?.cost ?? 0);
|
|
719
|
+
if (costDelta !== 0) return costDelta;
|
|
720
|
+
return a.label.localeCompare(b.label);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function getActiveManagedAccount(accounts: UsageManagedAccount[], state: UsageManagedState): UsageManagedAccount | undefined {
|
|
725
|
+
const manual = accounts.find((account) => account.id === state.manualAccountId && !isQuotaActive(account));
|
|
726
|
+
return manual ?? accounts.find((account) => !isQuotaActive(account));
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function findManagedAccount(accounts: UsageManagedAccount[], query: string): UsageManagedAccount | undefined {
|
|
730
|
+
const normalized = query.trim().toLowerCase();
|
|
731
|
+
return accounts.find(
|
|
732
|
+
(account) =>
|
|
733
|
+
account.id.toLowerCase() === normalized ||
|
|
734
|
+
account.provider.toLowerCase() === normalized ||
|
|
735
|
+
account.label.toLowerCase() === normalized
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function formatManagedAccountLine(account: UsageManagedAccount, data: UsageData, state: UsageManagedState): string {
|
|
740
|
+
const stats = getAccountStats(data, account);
|
|
741
|
+
const tags = [
|
|
742
|
+
state.manualAccountId === account.id ? "manual" : undefined,
|
|
743
|
+
isQuotaActive(account) ? "quota" : undefined,
|
|
744
|
+
account.source === "manual" ? "manual-entry" : undefined,
|
|
745
|
+
]
|
|
746
|
+
.filter(Boolean)
|
|
747
|
+
.join(", ");
|
|
748
|
+
const suffix = tags ? ` (${tags})` : "";
|
|
749
|
+
const cost = stats ? formatCost(stats.cost) : formatCost(0);
|
|
750
|
+
const messages = stats ? formatNumber(stats.messages) : "0";
|
|
751
|
+
const sessions = stats ? formatNumber(stats.sessions.size) : "0";
|
|
752
|
+
return `${account.label}${suffix} · id=${account.id} · spend=${cost} · messages=${messages} · sessions=${sessions}`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function loadUsageManagedSummary(ctx: ExtensionCommandContext): Promise<UsageManagedSummary | null> {
|
|
756
|
+
const data = await collectUsageData(ctx.signal);
|
|
757
|
+
if (!data) {
|
|
758
|
+
ctx.ui.notify("Usage data unavailable or cancelled.", "warning");
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
const { state, error } = await readUsageManagedState();
|
|
762
|
+
if (error) {
|
|
763
|
+
ctx.ui.notify(`dm-usage state unreadable; using empty state: ${error}`, "warning");
|
|
764
|
+
}
|
|
765
|
+
return { data, state, accounts: getManagedAccounts(data, state) };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function managedAccountNoDataMessage(): string {
|
|
769
|
+
return "No managed accounts found. Run /usage after at least one provider response, or use /usage use <provider-id> to create a local manual entry.";
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function activateUsageAccount(ctx: ExtensionCommandContext, summary: UsageManagedSummary, account: UsageManagedAccount): Promise<void> {
|
|
773
|
+
summary.state.manualAccountId = account.id;
|
|
774
|
+
summary.state.removedAccountIds = summary.state.removedAccountIds.filter((id) => id !== account.id);
|
|
775
|
+
summary.state.accounts = summary.accounts.map((item) => (item.id === account.id ? { ...item, updatedAt: new Date().toISOString() } : item));
|
|
776
|
+
await saveUsageManagedState(summary.state);
|
|
777
|
+
ctx.ui.notify(`dm-usage: now using ${account.label} (${account.id})`, "info");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function clearUsageManualAccount(ctx: ExtensionCommandContext, state: UsageManagedState): Promise<void> {
|
|
781
|
+
const hadManual = Boolean(state.manualAccountId);
|
|
782
|
+
state.manualAccountId = undefined;
|
|
783
|
+
await saveUsageManagedState(state);
|
|
784
|
+
ctx.ui.notify(`dm-usage: manual account ${hadManual ? "cleared" : "was not set"}`, "info");
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function removeUsageAccountState(ctx: ExtensionCommandContext, summary: UsageManagedSummary, account: UsageManagedAccount): Promise<void> {
|
|
788
|
+
summary.state.removedAccountIds = [...summary.state.removedAccountIds, account.id];
|
|
789
|
+
if (summary.state.manualAccountId === account.id) summary.state.manualAccountId = undefined;
|
|
790
|
+
summary.state.accounts = summary.accounts.filter((item) => item.id !== account.id);
|
|
791
|
+
await saveUsageManagedState(summary.state);
|
|
792
|
+
ctx.ui.notify(`dm-usage: removed local managed-account state for ${account.label}; session data was not changed.`, "info");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function runUsageUseSubcommand(ctx: ExtensionCommandContext, rest: string): Promise<void> {
|
|
796
|
+
const summary = await loadUsageManagedSummary(ctx);
|
|
797
|
+
if (!summary) return;
|
|
798
|
+
|
|
799
|
+
if (rest === "clear") {
|
|
800
|
+
await clearUsageManualAccount(ctx, summary.state);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (rest.startsWith("remove ")) {
|
|
805
|
+
const account = findManagedAccount(summary.accounts, rest.slice("remove ".length));
|
|
806
|
+
if (!account) {
|
|
807
|
+
ctx.ui.notify(`Unknown managed account: ${rest.slice("remove ".length)}`, "warning");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
await removeUsageAccountState(ctx, summary, account);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (rest) {
|
|
815
|
+
const account =
|
|
816
|
+
findManagedAccount(summary.accounts, rest) ??
|
|
817
|
+
({ id: rest, label: formatProviderName(rest), provider: rest, source: "manual", updatedAt: new Date().toISOString() } satisfies UsageManagedAccount);
|
|
818
|
+
if (!summary.accounts.some((item) => item.id === account.id)) summary.accounts.push(account);
|
|
819
|
+
await activateUsageAccount(ctx, summary, account);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (!ctx.hasUI) {
|
|
824
|
+
ctx.ui.notify(summary.accounts.length ? summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state)).join("\n") : managedAccountNoDataMessage(), summary.accounts.length ? "info" : "warning");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const options = [
|
|
829
|
+
...summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state)),
|
|
830
|
+
"add manual account",
|
|
831
|
+
"clear manual account",
|
|
832
|
+
];
|
|
833
|
+
const selected = await ctx.ui.select("DuckMind Usage Accounts", options);
|
|
834
|
+
if (!selected) return;
|
|
835
|
+
if (selected === "add manual account") {
|
|
836
|
+
const id = (await ctx.ui.input("Managed account id (provider key, no secret)"))?.trim();
|
|
837
|
+
if (!id) return;
|
|
838
|
+
await runUsageUseSubcommand(ctx, id);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (selected === "clear manual account") {
|
|
842
|
+
await clearUsageManualAccount(ctx, summary.state);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const account = summary.accounts[options.indexOf(selected)];
|
|
847
|
+
if (!account) return;
|
|
848
|
+
const action = await ctx.ui.select("Account Action", ["activate", "remove local state", "cancel"]);
|
|
849
|
+
if (action === "activate") await activateUsageAccount(ctx, summary, account);
|
|
850
|
+
if (action === "remove local state") await removeUsageAccountState(ctx, summary, account);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function runUsageShowSubcommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
854
|
+
const summary = await loadUsageManagedSummary(ctx);
|
|
855
|
+
if (!summary) return;
|
|
856
|
+
const active = getActiveManagedAccount(summary.accounts, summary.state);
|
|
857
|
+
const hiddenCount = summary.state.removedAccountIds.length;
|
|
858
|
+
const lines = [
|
|
859
|
+
"DuckMind Usage managed account summary",
|
|
860
|
+
`active: ${active ? `${active.label} (${active.id})` : "none"}`,
|
|
861
|
+
`manual: ${summary.state.manualAccountId ?? "none"}`,
|
|
862
|
+
`accounts: ${summary.accounts.length}${hiddenCount ? ` · hidden=${hiddenCount}` : ""}`,
|
|
863
|
+
`all-time: spend=${formatCost(summary.data.allTime.totals.cost)} · messages=${formatNumber(summary.data.allTime.totals.messages)} · sessions=${formatNumber(summary.data.allTime.totals.sessions)} · tokens=${formatTokens(summary.data.allTime.totals.tokens.total)}`,
|
|
864
|
+
"",
|
|
865
|
+
...(summary.accounts.length
|
|
866
|
+
? summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state))
|
|
867
|
+
: [managedAccountNoDataMessage()]),
|
|
868
|
+
];
|
|
869
|
+
if (ctx.hasUI) {
|
|
870
|
+
await ctx.ui.select("DuckMind Usage Summary", lines);
|
|
871
|
+
} else {
|
|
872
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function runUsageRotationSubcommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
877
|
+
const lines = [
|
|
878
|
+
"Current policy: manual account first, then accounts without quota cooldown, then highest all-time local spend, then alphabetical fallback.",
|
|
879
|
+
"dm-usage is observational: this state changes /usage account summaries only and does not route provider requests or store API secrets.",
|
|
880
|
+
"Use /usage reset manual to clear manual selection, or /usage reset quota to clear quota cooldown markers.",
|
|
881
|
+
];
|
|
882
|
+
if (ctx.hasUI) {
|
|
883
|
+
await ctx.ui.select("DuckMind Usage Rotation", lines);
|
|
884
|
+
} else {
|
|
885
|
+
ctx.ui.notify(lines.join(" "), "info");
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function isDirectoryReadable(path: string): Promise<boolean> {
|
|
890
|
+
try {
|
|
891
|
+
await access(path, fsConstants.R_OK);
|
|
892
|
+
return true;
|
|
893
|
+
} catch {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function isWritableDirectoryFor(filePath: string): Promise<boolean> {
|
|
899
|
+
try {
|
|
900
|
+
const directory = dirname(filePath);
|
|
901
|
+
await mkdir(directory, { recursive: true });
|
|
902
|
+
await access(directory, fsConstants.R_OK | fsConstants.W_OK);
|
|
903
|
+
return true;
|
|
904
|
+
} catch {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function runUsageVerifySubcommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
910
|
+
const sessionsReadable = await isDirectoryReadable(getSessionsDir());
|
|
911
|
+
const stateWritable = await isWritableDirectoryFor(getUsageStatePath());
|
|
912
|
+
const settingsWritable = await isWritableDirectoryFor(getAgentSettingsPath());
|
|
913
|
+
const stateRead = await readUsageManagedState();
|
|
914
|
+
const summary = await loadUsageManagedSummary(ctx);
|
|
915
|
+
const accounts = summary?.accounts.length ?? 0;
|
|
916
|
+
const active = summary ? getActiveManagedAccount(summary.accounts, summary.state)?.id ?? "none" : "none";
|
|
917
|
+
const ok = sessionsReadable && stateWritable && settingsWritable && !stateRead.error;
|
|
918
|
+
const lines = [
|
|
919
|
+
`verify: ${ok ? "PASS" : "WARN"}`,
|
|
920
|
+
`sessions readable: ${sessionsReadable ? "yes" : "no"}`,
|
|
921
|
+
`state directory writable: ${stateWritable ? "yes" : "no"}`,
|
|
922
|
+
`settings directory writable: ${settingsWritable ? "yes" : "no"}`,
|
|
923
|
+
`state json: ${stateRead.error ? `invalid (${stateRead.error})` : "ok"}`,
|
|
924
|
+
`managed accounts: ${accounts}`,
|
|
925
|
+
`active account: ${active}`,
|
|
926
|
+
];
|
|
927
|
+
if (ctx.hasUI) {
|
|
928
|
+
await ctx.ui.select(`DuckMind Usage Verify (${ok ? "PASS" : "WARN"})`, lines);
|
|
929
|
+
} else {
|
|
930
|
+
ctx.ui.notify(lines.join("\n"), ok ? "info" : "warning");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function runUsagePathSubcommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
935
|
+
const lines = [
|
|
936
|
+
`Session storage: ${getSessionsDir()}`,
|
|
937
|
+
`Managed account state: ${getUsageStatePath()}`,
|
|
938
|
+
`Agent settings: ${getAgentSettingsPath()}`,
|
|
939
|
+
];
|
|
940
|
+
if (ctx.hasUI) {
|
|
941
|
+
await ctx.ui.select("DuckMind Usage Paths", lines);
|
|
942
|
+
} else {
|
|
943
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function chooseUsageResetTarget(ctx: ExtensionCommandContext, argument: string): Promise<UsageResetTarget | undefined> {
|
|
948
|
+
const explicitTarget = parseUsageResetTarget(argument.toLowerCase());
|
|
949
|
+
if (explicitTarget) return explicitTarget;
|
|
950
|
+
if (argument) {
|
|
951
|
+
ctx.ui.notify("Unknown reset target. Use: /usage reset [manual|quota|all]", "warning");
|
|
952
|
+
return undefined;
|
|
953
|
+
}
|
|
954
|
+
if (!ctx.hasUI) return "all";
|
|
955
|
+
const selected = await ctx.ui.select("Reset DuckMind Usage State", [
|
|
956
|
+
"manual - clear manual account override",
|
|
957
|
+
"quota - clear quota cooldown markers",
|
|
958
|
+
"all - clear manual override, quota cooldown markers, and hidden account state",
|
|
959
|
+
]);
|
|
960
|
+
if (!selected) return undefined;
|
|
961
|
+
if (selected.startsWith("manual")) return "manual";
|
|
962
|
+
if (selected.startsWith("quota")) return "quota";
|
|
963
|
+
return "all";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function runUsageResetSubcommand(ctx: ExtensionCommandContext, rest: string): Promise<void> {
|
|
967
|
+
const target = await chooseUsageResetTarget(ctx, rest);
|
|
968
|
+
if (!target) return;
|
|
969
|
+
const { state } = await readUsageManagedState();
|
|
970
|
+
const hadManual = Boolean(state.manualAccountId);
|
|
971
|
+
if (target === "manual" || target === "all") state.manualAccountId = undefined;
|
|
972
|
+
let quotaCleared = 0;
|
|
973
|
+
if (target === "quota" || target === "all") {
|
|
974
|
+
state.accounts = state.accounts.map((account) => {
|
|
975
|
+
if (account.quotaExhaustedUntil === undefined) return account;
|
|
976
|
+
quotaCleared++;
|
|
977
|
+
const { quotaExhaustedUntil: _quotaExhaustedUntil, ...restAccount } = account;
|
|
978
|
+
return restAccount;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const hiddenCleared = target === "all" ? state.removedAccountIds.length : 0;
|
|
982
|
+
if (target === "all") state.removedAccountIds = [];
|
|
983
|
+
await saveUsageManagedState(state);
|
|
984
|
+
ctx.ui.notify(
|
|
985
|
+
`reset: target=${target} manualCleared=${hadManual && !state.manualAccountId ? "yes" : "no"} quotaCleared=${quotaCleared} hiddenCleared=${hiddenCleared}`,
|
|
986
|
+
"info"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function runUsageSubcommand(subcommand: UsageSubcommand, rest: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
991
|
+
if (subcommand === "use") return runUsageUseSubcommand(ctx, rest);
|
|
992
|
+
if (subcommand === "show") return runUsageShowSubcommand(ctx);
|
|
993
|
+
if (subcommand === "rotation") return runUsageRotationSubcommand(ctx);
|
|
994
|
+
if (subcommand === "verify") return runUsageVerifySubcommand(ctx);
|
|
995
|
+
if (subcommand === "path") return runUsagePathSubcommand(ctx);
|
|
996
|
+
if (subcommand === "reset") return runUsageResetSubcommand(ctx, rest);
|
|
997
|
+
ctx.ui.notify(USAGE_HELP_TEXT, "info");
|
|
998
|
+
}
|
|
999
|
+
|
|
516
1000
|
// =============================================================================
|
|
517
1001
|
// Insights
|
|
518
1002
|
// =============================================================================
|
|
@@ -1161,9 +1645,21 @@ class UsageComponent {
|
|
|
1161
1645
|
|
|
1162
1646
|
export default function (pi: ExtensionAPI) {
|
|
1163
1647
|
pi.registerCommand("usage", {
|
|
1164
|
-
description:
|
|
1165
|
-
|
|
1648
|
+
description: `Show usage statistics dashboard and account tools: ${USAGE_SUBCOMMANDS.join(", ")}`,
|
|
1649
|
+
getArgumentCompletions: getUsageCommandCompletions,
|
|
1650
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
1651
|
+
const parsed = parseUsageCommandArgs(args);
|
|
1652
|
+
if (parsed.subcommand) {
|
|
1653
|
+
if (!isUsageSubcommand(parsed.subcommand)) {
|
|
1654
|
+
ctx.ui.notify(`Unknown /usage subcommand: ${parsed.subcommand}\n${USAGE_HELP_TEXT}`, "warning");
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
await runUsageSubcommand(parsed.subcommand, parsed.rest, ctx);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1166
1661
|
if (!ctx.hasUI) {
|
|
1662
|
+
await runUsageShowSubcommand(ctx);
|
|
1167
1663
|
return;
|
|
1168
1664
|
}
|
|
1169
1665
|
|