@duckmind/dm-darwin-x64 0.35.8 → 0.35.9

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.
@@ -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.35.9",
4
4
  "description": "DuckMind (dm) binary payload for darwin x64",
5
5
  "license": "MIT",
6
6
  "os": [