@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 CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "ok",
3
- "prepared_at": "2026-06-06T16:01:10.933885+00:00",
3
+ "prepared_at": "2026-06-08T01:10:30.489829+00:00",
4
4
  "managed_entries": [
5
5
  {
6
6
  "id": "dm-context",
@@ -87,7 +87,7 @@ describe("registerCommands", () => {
87
87
  );
88
88
  });
89
89
 
90
- it("returns sync-only autocomplete and hides account identifiers", () => {
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(["sync"]);
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).toBeNull();
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 sync <secret>.",
151
+ "/ultradex requires a subcommand in non-interactive mode. Use /ultradex help.",
144
152
  "warning",
145
153
  );
146
154
  });
147
155
 
148
- it("hides old subcommands even when typed directly", async () => {
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("Usage: /ultradex sync [secret]", "info");
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
- "Sync DuckMind managed accounts",
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 sync <secret>.",
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
- if (!isSubcommand(parsed.subcommand)) {
1067
- ctx.ui.notify(`Unknown subcommand: ${parsed.subcommand}`, "warning");
1068
- runHelpSubcommand(ctx);
1069
- return;
1070
- }
1071
- if (parsed.subcommand !== "sync") {
1072
- ctx.ui.notify(HELP_TEXT, "info");
1073
- return;
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
- await runSubcommand(
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.1",
3609
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
3610
- "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
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 { readdir, readFile } from "node:fs/promises";
14
- import { join } from "node:path";
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 getSessionsDir(): string {
224
+ function getAgentDir(): string {
199
225
  // Respect DM_CODING_AGENT_DIR, keeping PI_CODING_AGENT_DIR only as a compatibility fallback
200
- const agentDir = process.env.DM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".dm", "agent");
201
- return join(agentDir, "sessions");
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: "Show usage statistics dashboard",
1165
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-darwin-x64",
3
- "version": "0.35.8",
3
+ "version": "0.36.0",
4
4
  "description": "DuckMind (dm) binary payload for darwin x64",
5
5
  "license": "MIT",
6
6
  "os": [