tokenmaxxing 0.19.0 → 0.21.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.
Files changed (59) hide show
  1. package/DESIGN.md +34 -23
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +1 -0
  5. package/src/cli/auth.ts +25 -14
  6. package/src/cli/check.ts +3 -2
  7. package/src/cli/codexadd.ts +44 -40
  8. package/src/cli/codexinit.ts +59 -12
  9. package/src/cli/codexswitch.ts +15 -1
  10. package/src/cli/config.ts +10 -1
  11. package/src/cli/doctor.ts +3 -3
  12. package/src/cli/init.ts +54 -32
  13. package/src/cli/onboard.ts +62 -45
  14. package/src/cli/render.ts +0 -16
  15. package/src/cli/rm.ts +40 -2
  16. package/src/cli/serve.ts +650 -78
  17. package/src/cli/status.ts +69 -23
  18. package/src/cli/switch.ts +54 -19
  19. package/src/entries/codexstophook.ts +123 -4
  20. package/src/entries/codexsupervisor.ts +87 -13
  21. package/src/entries/sessionstart.ts +1 -1
  22. package/src/entries/statusline.ts +56 -20
  23. package/src/entries/stophook.ts +23 -9
  24. package/src/entries/supervisor.ts +134 -18
  25. package/src/lib/atomic.ts +28 -6
  26. package/src/lib/claudebin.ts +2 -2
  27. package/src/lib/claudejson.ts +5 -5
  28. package/src/lib/claudelock.ts +112 -37
  29. package/src/lib/codexauth.ts +10 -2
  30. package/src/lib/codexbin.ts +1 -1
  31. package/src/lib/codexdecide.ts +149 -19
  32. package/src/lib/codexpick.ts +17 -6
  33. package/src/lib/codexpresence.ts +59 -21
  34. package/src/lib/codexsample.ts +17 -8
  35. package/src/lib/codexswap.ts +10 -1
  36. package/src/lib/credstore.ts +6 -2
  37. package/src/lib/decide.ts +114 -42
  38. package/src/lib/install.ts +125 -17
  39. package/src/lib/keychain.ts +41 -15
  40. package/src/lib/lock.ts +57 -35
  41. package/src/lib/log.ts +36 -7
  42. package/src/lib/oauth.ts +18 -11
  43. package/src/lib/paths.ts +17 -11
  44. package/src/lib/picker.ts +11 -3
  45. package/src/lib/proc.ts +37 -0
  46. package/src/lib/sample.ts +91 -31
  47. package/src/lib/sessions.ts +23 -1
  48. package/src/lib/settings.ts +59 -18
  49. package/src/lib/slackbridge.ts +583 -76
  50. package/src/lib/slackstate.ts +159 -12
  51. package/src/lib/slackstream.ts +127 -21
  52. package/src/lib/state.ts +131 -35
  53. package/src/lib/swap.ts +109 -47
  54. package/src/lib/types.ts +79 -37
  55. package/src/lib/usage.ts +114 -16
  56. package/src/main.ts +61 -7
  57. package/src/serve-plugin/.claude-plugin/plugin.json +4 -0
  58. package/src/serve-plugin/skills/ask-the-user/SKILL.md +36 -0
  59. package/src/serve-plugin/skills/serve-session/SKILL.md +50 -0
package/src/cli/doctor.ts CHANGED
@@ -1,4 +1,4 @@
1
- // `tokenmaxxing doctor` - verify the supervisor + three settings entries survived
1
+ // `tokenmaxxing doctor` - verify the supervisor + four settings entries survived
2
2
  // and the pool is healthy.
3
3
 
4
4
  import { existsSync, readFileSync } from "node:fs";
@@ -55,7 +55,7 @@ export async function cmdDoctor(): Promise<number> {
55
55
  if (org) check(org.organization_uuid === active.organizationUuid, `live credential identity matches active (${active.email})`, `token belongs to ${org.organization_name} - run \`tokenmaxxing switch\``);
56
56
  else console.log(c.dim(` - live credential identity unverifiable (access token expired)`));
57
57
  } catch (e) {
58
- check(false, `live credential identity matches active (${active.email})`, String((e as Error).message ?? e).slice(0, 100));
58
+ check(false, `live credential identity matches active (${active.email})`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
59
59
  }
60
60
  }
61
61
 
@@ -68,7 +68,7 @@ export async function cmdDoctor(): Promise<number> {
68
68
  if (org) check(org.organization_uuid === a.organizationUuid, `parked credential identity matches ${a.email}`, `token belongs to ${org.organization_name} - run \`tokenmaxxing auth ${a.label}\``);
69
69
  else console.log(c.dim(` - ${a.email} identity unverifiable (access token expired)`));
70
70
  } catch (e) {
71
- check(false, `parked credential identity matches ${a.email}`, String((e as Error).message ?? e).slice(0, 100));
71
+ check(false, `parked credential identity matches ${a.email}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
72
72
  }
73
73
  }
74
74
  if (a.needsReauth) check(false, `${a.email} needs re-auth`, `run \`tokenmaxxing auth ${a.label}\` to re-login`);
package/src/cli/init.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // `tokenmaxxing init` - import the account you're already on (no prompts), then
2
- // install the supervisor + the three settings entries.
2
+ // install the supervisor + the four settings entries.
3
3
 
4
4
  import { mkdirSync } from "node:fs";
5
5
  import { isApiKeyMode, readOAuthAccount } from "../lib/claudejson.ts";
6
6
  import { readItem, writeItem, liveTarget, parkedTarget, mergeIntoLive } from "../lib/credstore.ts";
7
7
  import { refreshCredential, isAccessTokenExpiring, fetchTokenOrg } from "../lib/oauth.ts";
8
- import { loadAccounts, saveAccounts, loadConfig, saveConfig } from "../lib/state.ts";
8
+ import { withClaudeRefreshLock } from "../lib/claudelock.ts";
9
+ import { loadAccounts, saveAccounts, loadConfig, pinBinOverride } from "../lib/state.ts";
10
+ import { withLock } from "../lib/lock.ts";
9
11
  import { installSupervisor, shellRcPath, ensurePathInRc, timerActivationHint, type InstallOutcome } from "../lib/install.ts";
10
12
  import { resolveVerifiedClaude } from "../lib/claudebin.ts";
11
13
  import { credItemFor, paths } from "../lib/paths.ts";
@@ -46,6 +48,13 @@ export function printUsage(): void {
46
48
 
47
49
  export async function cmdInit(): Promise<number> {
48
50
  mkdirSync(paths.home, { recursive: true });
51
+ // Fail fast on a broken merged config BEFORE installing or claiming a
52
+ // successful repair: pinBinOverride writes sparsely without validating, so
53
+ // without this gate a re-init printed success while hooks, switching, and
54
+ // the statusline kept throwing on every loadConfig until the file was
55
+ // hand-repaired (bugbot review catch, PR #33). The throw carries the
56
+ // fix-or-remove recovery message.
57
+ loadConfig();
49
58
 
50
59
  // Already initialized → repair install ONLY. Never re-import: ~/.claude.json's
51
60
  // oauthAccount can drift from the live keychain cred (after swaps / concurrent
@@ -56,10 +65,9 @@ export async function cmdInit(): Promise<number> {
56
65
  // repair the claudeBin pin too - hooks run with claude's PATH and must
57
66
  // never have to guess which binary is the real claude. Verified pinning:
58
67
  // a pin that fails --version (or loops back into the wrapper) is replaced
59
- // by a fresh PATH scan instead of being re-saved.
60
- const cfg = loadConfig();
61
- cfg.claudeBin = resolveVerifiedClaude();
62
- saveConfig(cfg);
68
+ // by a fresh PATH scan instead of being re-saved. Sparse write: only the
69
+ // pin lands in the file, never the merged config.
70
+ pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() });
63
71
  const active = existingIdx.accounts.find((a) => a.accountUuid === existingIdx.activeAccountUuid);
64
72
  console.log(`${c.green("✓")} re-installed supervisor + hooks (pool already has ${existingIdx.accounts.length} account${existingIdx.accounts.length === 1 ? "" : "s"} - not re-importing)`);
65
73
  reportTimer(out);
@@ -96,8 +104,16 @@ export async function cmdInit(): Promise<number> {
96
104
  // keychain credential, and importing on drifted state parks a mislabeled blob.
97
105
  let creds = blob.claudeAiOauth;
98
106
  if (isAccessTokenExpiring(creds)) {
99
- creds = await refreshCredential(creds);
100
- await writeItem(liveTarget(), mergeIntoLive(liveRaw, creds));
107
+ await withClaudeRefreshLock(async (lock) => {
108
+ // re-read inside the lock: a running claude may have rotated it already.
109
+ const raw2 = await readItem(liveTarget());
110
+ if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
111
+ const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
112
+ creds = isAccessTokenExpiring(current) ? await refreshCredential(current) : current;
113
+ if (creds === current) return;
114
+ if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
115
+ await writeItem(liveTarget(), mergeIntoLive(raw2, creds));
116
+ });
101
117
  }
102
118
  const org = await fetchTokenOrg(creds.accessToken);
103
119
  if (org.organization_uuid !== oauthAccount.organizationUuid) {
@@ -108,30 +124,35 @@ export async function cmdInit(): Promise<number> {
108
124
 
109
125
  const uuid = oauthAccount.accountUuid;
110
126
  const keychainItem = credItemFor(uuid);
111
- await writeItem(parkedTarget(keychainItem), JSON.stringify({ claudeAiOauth: creds })); // park a small backup
127
+ // Park + index update under the tokenmaxxing flock, like every sibling
128
+ // index writer (add/auth/rm/status/rename): unlocked, a concurrent `xx add`
129
+ // completing between this path's load and save had its just-registered
130
+ // account silently clobbered out of the index (closing-review catch).
131
+ const account = await withLock(paths.lockFile, async () => {
132
+ await writeItem(parkedTarget(keychainItem), JSON.stringify({ claudeAiOauth: creds })); // park a small backup
133
+ const idx = loadAccounts();
134
+ const existing = idx.accounts.find((a) => a.accountUuid === uuid);
135
+ const imported: Account = {
136
+ accountUuid: uuid,
137
+ email: oauthAccount.emailAddress,
138
+ organizationUuid: oauthAccount.organizationUuid,
139
+ label: existing?.label ?? oauthAccount.emailAddress,
140
+ keychainItem,
141
+ oauthAccount,
142
+ addedAt: existing?.addedAt ?? new Date().toISOString(),
143
+ subscriptionType: blob.claudeAiOauth.subscriptionType,
144
+ rateLimitTier: blob.claudeAiOauth.rateLimitTier,
145
+ needsReauth: false,
146
+ };
147
+ if (existing) Object.assign(existing, imported);
148
+ else idx.accounts.push(imported);
149
+ idx.activeAccountUuid = uuid;
150
+ saveAccounts(idx);
151
+ return imported;
152
+ });
112
153
 
113
- const idx = loadAccounts();
114
- const existing = idx.accounts.find((a) => a.accountUuid === uuid);
115
- const account: Account = {
116
- accountUuid: uuid,
117
- email: oauthAccount.emailAddress,
118
- organizationUuid: oauthAccount.organizationUuid,
119
- label: existing?.label ?? oauthAccount.emailAddress,
120
- keychainItem,
121
- oauthAccount,
122
- addedAt: existing?.addedAt ?? new Date().toISOString(),
123
- subscriptionType: blob.claudeAiOauth.subscriptionType,
124
- rateLimitTier: blob.claudeAiOauth.rateLimitTier,
125
- needsReauth: false,
126
- };
127
- if (existing) Object.assign(existing, account);
128
- else idx.accounts.push(account);
129
- idx.activeAccountUuid = uuid;
130
- saveAccounts(idx);
131
-
132
- const cfg = loadConfig();
133
- cfg.claudeBin = resolveVerifiedClaude();
134
- saveConfig(cfg);
154
+ // Sparse write: only the pin lands in the file, never the merged config.
155
+ pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() });
135
156
 
136
157
  const out = installSupervisor();
137
158
 
@@ -142,8 +163,9 @@ export async function cmdInit(): Promise<number> {
142
163
  console.log();
143
164
  ensurePathAhead();
144
165
  }
166
+ const poolSize = loadAccounts().accounts.length;
145
167
  console.log();
146
- console.log(` pool ready (${idx.accounts.length} account${idx.accounts.length === 1 ? "" : "s"})`);
168
+ console.log(` pool ready (${poolSize} account${poolSize === 1 ? "" : "s"})`);
147
169
  printUsage();
148
170
  return 0;
149
171
  }
@@ -10,13 +10,13 @@ import { join } from "node:path";
10
10
  import { z } from "zod";
11
11
  import { readItem, deleteItem, isolatedTarget } from "../lib/credstore.ts";
12
12
  import { resolveRealClaude } from "../lib/claudebin.ts";
13
- import { probeUsage, FullUsageSchema } from "../lib/usage.ts";
13
+ import { CRED_ENV_OVERRIDES, probeUsage, FullUsageSchema } from "../lib/usage.ts";
14
14
  import { saveTermios, restoreTermios } from "../lib/tty.ts";
15
15
  import { paths } from "../lib/paths.ts";
16
16
  import { CredentialBlobSchema, OAuthAccountSchema } from "../lib/types.ts";
17
17
  import { c } from "./render.ts";
18
18
 
19
- export const HarvestedLoginSchema = z.object({
19
+ const HarvestedLoginSchema = z.object({
20
20
  /** the raw isolated credential blob (park it via claudeAiOauthOnly). */
21
21
  blobRaw: z.string(),
22
22
  blob: CredentialBlobSchema,
@@ -46,6 +46,15 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
46
46
  rmSync(onboardDir, { recursive: true, force: true });
47
47
  mkdirSync(onboardDir, { recursive: true });
48
48
  const iso = isolatedTarget(onboardDir);
49
+ // Reap a PREVIOUS run's stranded isolated credential too: on macOS the
50
+ // isolated store is a namespaced KEYCHAIN item, not a file in onboardDir,
51
+ // so the rmSync above never touched it - a signal-killed harvest (Ctrl-C
52
+ // after /login, before the ~400ms poll) left a live credential in the
53
+ // keychain that the next run's spawned claude would silently reuse as an
54
+ // already-authenticated session (closing-review catch). deleteItem on a
55
+ // missing item is a no-op; on Linux iso is a file inside onboardDir and the
56
+ // rmSync already covered it.
57
+ await deleteItem(iso);
49
58
  const cjPath = join(onboardDir, ".claude.json");
50
59
  const real = resolveRealClaude();
51
60
 
@@ -54,10 +63,7 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
54
63
  // keychain lookup (verified 2.1.205) - the onboard session must authenticate
55
64
  // only via the /login the user performs inside it.
56
65
  const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
57
- delete env.ANTHROPIC_API_KEY;
58
- delete env.ANTHROPIC_AUTH_TOKEN;
59
- delete env.CLAUDE_CODE_OAUTH_TOKEN;
60
- delete env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
66
+ for (const key of CRED_ENV_OVERRIDES) delete env[key];
61
67
  const p = Bun.spawn([real], {
62
68
  stdin: "inherit",
63
69
  stdout: "inherit",
@@ -65,48 +71,59 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
65
71
  env,
66
72
  });
67
73
 
68
- // Auto-exit (#17): watch for a completed login - identity written AND the
69
- // isolated credential present - then SIGTERM claude. No manual /exit.
70
- let exited = false;
71
- const onExit = p.exited.then(() => { exited = true; });
72
- while (!exited) {
73
- await Bun.sleep(400);
74
- if (identityReady(cjPath) && (await readItem(iso))) {
75
- p.kill();
76
- break;
74
+ // The finally makes the "always destroyed before returning" header true for
75
+ // every non-signal exit - INCLUDING a failure while the login session is
76
+ // still being polled (review catch, PR #31): an exception must not strand a
77
+ // plaintext credential on disk or leave the spawned claude running. (An
78
+ // interactive Ctrl-C is reaped by the next run's cleanup-first, which
79
+ // deletes the isolated keychain item as well as the dir.)
80
+ try {
81
+ // Auto-exit (#17): watch for a completed login - identity written AND the
82
+ // isolated credential present - then SIGTERM claude. No manual /exit.
83
+ let exited = false;
84
+ const onExit = p.exited.then(() => { exited = true; });
85
+ while (!exited) {
86
+ await Bun.sleep(400);
87
+ if (identityReady(cjPath) && (await readItem(iso))) {
88
+ p.kill();
89
+ break;
90
+ }
77
91
  }
78
- }
79
- await p.exited;
80
- await onExit;
81
- restoreTermios(savedTermios);
92
+ await p.exited;
93
+ await onExit;
94
+ // restore promptly so the harvest's own output renders on a sane terminal;
95
+ // the finally's second restore is an idempotent stty and covers the
96
+ // thrown-mid-poll path.
97
+ restoreTermios(savedTermios);
82
98
 
83
- const cleanup = async () => {
84
- await deleteItem(iso);
85
- rmSync(onboardDir, { recursive: true, force: true });
86
- };
87
-
88
- const blobRaw = await readItem(iso);
89
- if (!blobRaw || !identityReady(cjPath)) {
90
- console.error(c.red("no login detected in the isolated session - nothing changed."));
91
- await cleanup();
92
- return null;
93
- }
99
+ const blobRaw = await readItem(iso);
100
+ if (!blobRaw || !identityReady(cjPath)) {
101
+ console.error(c.red("no login detected in the isolated session - nothing changed."));
102
+ return null;
103
+ }
94
104
 
95
- let blob, oauthAccount;
96
- try {
97
- blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
98
- oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
99
- } catch {
100
- console.error(c.red("could not parse the onboarded account's credential/identity."));
101
- await cleanup();
102
- return null;
103
- }
105
+ let blob, oauthAccount;
106
+ try {
107
+ blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
108
+ oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
109
+ } catch {
110
+ console.error(c.red("could not parse the onboarded account's credential/identity."));
111
+ return null;
112
+ }
104
113
 
105
- // Sample usage now (#16) so the account isn't "not sampled yet" in status/ls.
106
- console.log(c.dim("sampling usage..."));
107
- const sampled = await probeUsage(onboardDir);
108
- if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
114
+ // Sample usage now (#16) so the account isn't "not sampled yet" in status/ls.
115
+ console.log(c.dim("sampling usage..."));
116
+ const sampled = await probeUsage(onboardDir);
117
+ if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
109
118
 
110
- await cleanup();
111
- return { blobRaw, blob, oauthAccount, sampled };
119
+ return { blobRaw, blob, oauthAccount, sampled };
120
+ } finally {
121
+ if (p.exitCode === null) {
122
+ p.kill();
123
+ await p.exited;
124
+ }
125
+ restoreTermios(savedTermios);
126
+ await deleteItem(iso);
127
+ rmSync(onboardDir, { recursive: true, force: true });
128
+ }
112
129
  }
package/src/cli/render.ts CHANGED
@@ -98,19 +98,3 @@ export function fmtAgo(epochMs: number, now = Date.now()): string {
98
98
  return `${m}m ago`;
99
99
  }
100
100
 
101
- /** Compact time-until-reset for the statusLine: the largest unit only ("6d",
102
- * "2h", "45m"), floored to "1m" so a live window never reads as zero, and ""
103
- * once the reset has passed (the window is simply empty again). Non-empty
104
- * output always ends in a unit letter, so the digit-leading used-percent glued
105
- * after it stays parseable. */
106
- export function fmtResetShort(epochMs: number | null | undefined, now = Date.now()): string {
107
- if (epochMs == null) return "";
108
- const dsec = Math.round((epochMs - now) / 1000);
109
- if (dsec <= 0) return "";
110
- const d = Math.floor(dsec / 86400);
111
- const h = Math.floor((dsec % 86400) / 3600);
112
- const m = Math.floor((dsec % 3600) / 60);
113
- if (d > 0) return `${d}d`;
114
- if (h > 0) return `${h}h`;
115
- return `${Math.max(m, 1)}m`;
116
- }
package/src/cli/rm.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  // `tokenmaxxing rm <selector>` - remove a pooled account (not the active one).
2
2
 
3
- import { deleteItem, parkedTarget } from "../lib/credstore.ts";
3
+ import { rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { deleteItem, isolatedTarget, liveTarget, parkedTarget, readItem } from "../lib/credstore.ts";
6
+ import { fetchTokenOrg } from "../lib/oauth.ts";
4
7
  import { withLock } from "../lib/lock.ts";
5
- import { paths } from "../lib/paths.ts";
8
+ import { credItemFor, paths } from "../lib/paths.ts";
6
9
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
10
+ import { CredentialBlobSchema } from "../lib/types.ts";
7
11
  import { findAccount } from "./rename.ts";
8
12
  import { c } from "./render.ts";
9
13
 
@@ -24,7 +28,41 @@ export async function cmdRm(selector?: string): Promise<number> {
24
28
  console.error(c.red(`${a.email} is the ACTIVE account - switch away before removing it.`));
25
29
  return 1;
26
30
  }
31
+ // The label drifts (manual /login); the token cannot lie. Removing the
32
+ // account whose credential is actually LIVE would destroy its only backup
33
+ // and leave the next swap refusing over an unpooled credential. Fail
34
+ // CLOSED (review catch, PR #31): when the live owner cannot be verified -
35
+ // unparsable blob, expired token, roles outage - refuse rather than trust
36
+ // the stale label; rm is destructive and can wait. The check is read-only:
37
+ // an expired bearer simply fails the roles call, nothing is ever rotated.
38
+ const live = await readItem(liveTarget());
39
+ if (live != null) {
40
+ let liveOrg: string;
41
+ try {
42
+ const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
43
+ liveOrg = (await fetchTokenOrg(liveCreds.accessToken)).organization_uuid;
44
+ } catch (e) {
45
+ console.error(c.red(`cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`));
46
+ return 1;
47
+ }
48
+ if (liveOrg === a.organizationUuid) {
49
+ console.error(c.red(`${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.`));
50
+ return 1;
51
+ }
52
+ }
27
53
  await deleteItem(parkedTarget(a.keychainItem));
54
+ // Sweep sample-probe residue too: a probe killed mid-run strands the
55
+ // account's isolated credential (macOS: a namespaced keychain item), and
56
+ // once the account leaves the pool nothing would ever probe-and-heal it
57
+ // again (closing-review critic gap). deleteItem on a missing item is a
58
+ // no-op. Hard delete, not trash, on purpose: the sample dir is a
59
+ // throwaway CLAUDE_CONFIG_DIR that can hold PLAINTEXT credential material
60
+ // on Linux - the credential-dir cleanup exception (owner 2026-07-16, same
61
+ // rule sample.ts and onboard.ts follow); trashing would move credentials
62
+ // into the Trash folder.
63
+ const sampleDir = join(paths.sampleDir, credItemFor(a.accountUuid));
64
+ await deleteItem(isolatedTarget(sampleDir));
65
+ rmSync(sampleDir, { recursive: true, force: true });
28
66
  idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
29
67
  saveAccounts(idx);
30
68
  console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);