@event4u/agent-switch 1.6.1 → 2.0.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/README.md +5 -1
- package/dist/api.js +5 -4
- package/dist/args.js +1 -0
- package/dist/codex-reset.js +2 -2
- package/dist/config-transfer.js +184 -0
- package/dist/credentials.js +6 -4
- package/dist/daemon.js +68 -34
- package/dist/doctor.js +7 -0
- package/dist/gui-launch.js +45 -3
- package/dist/history.js +23 -0
- package/dist/hooks.js +5 -2
- package/dist/index.js +300 -50
- package/dist/keychain.js +14 -0
- package/dist/notifications.js +7 -2
- package/dist/profiles.js +26 -14
- package/dist/rebind-dialog.js +65 -0
- package/dist/rebind.js +715 -0
- package/dist/token-freshen.js +154 -0
- package/dist/tooling.js +324 -0
- package/dist/usage.js +8 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,20 +13,27 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import * as readline from "node:readline";
|
|
16
17
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
-
import { HOME, browserDir, configDir, die, ensureRoot, identity, listAllProfiles, listProfiles, migrateLegacyLayout, profileDir, profileExists, readJson, requireProfile, activeFor, setActive, labelFor, setLabel, clearLabel, renameProfile, isProfileLabel, PROFILE_LABELS, readAutoSwitch, readAutoSwitchAll, setAutoSwitch,
|
|
18
|
+
import { HOME, browserDir, configDir, die, ensureRoot, identity, listAllProfiles, listProfiles, migrateLegacyLayout, profileDir, profileExists, readJson, requireProfile, activeFor, setActive, labelFor, setLabel, clearLabel, renameProfile, isProfileLabel, PROFILE_LABELS, readAutoSwitch, readAutoSwitchAll, setAutoSwitch, readOsNotifications, setOsNotifications, readProviders, setProviderSurface, readBinaryPath, readBinaryPaths, setBinaryPath, ROOT, resetRebindKillSwitch, } from "./profiles.js";
|
|
18
19
|
import { PROVIDER_IDS, provider, isProviderInstalled, resolveBinary } from "./providers.js";
|
|
20
|
+
import { rebind, restoreRebind } from "./rebind.js";
|
|
21
|
+
import { buildRebindDialog, resolveRebindChoice, selectableRows } from "./rebind-dialog.js";
|
|
19
22
|
import { parseArgs, parseRun, resolveProviderValue } from "./args.js";
|
|
20
23
|
import { extractBrief, writeBrief, sweepBriefs, seedPrompt } from "./handoff.js";
|
|
21
24
|
import { credentialStore } from "./credentials.js";
|
|
22
25
|
import { withProperLock } from "./locks.js";
|
|
23
26
|
import { applySharing, removeSharing, syncSharing, sharedLinkHealth } from "./share.js";
|
|
27
|
+
import { assertNotFull, exportConfig, importConfig } from "./config-transfer.js";
|
|
24
28
|
import { detectShell, shellenvScript } from "./shellenv.js";
|
|
25
29
|
import { runDoctor } from "./doctor.js";
|
|
30
|
+
import { TOOL_IDS, checkTooling, formatToolingLines, runToolAction } from "./tooling.js";
|
|
31
|
+
import { readHistory, recordHistorySample } from "./history.js";
|
|
26
32
|
import { launchGui } from "./gui-launch.js";
|
|
27
33
|
import { checkForUpdate, selfUpdate } from "./updates.js";
|
|
28
34
|
import { mappingRows, pruneMappings, removeMapping, resolveMapping, setMapping, } from "./mappings.js";
|
|
29
|
-
import {
|
|
35
|
+
import { checkAuth, fetchProfile, fetchUsage, liveSessionPids, } from "./api.js";
|
|
36
|
+
import { defaultFreshenDeps, freshAccessToken } from "./token-freshen.js";
|
|
30
37
|
import { formatSnapshot, parseUsage } from "./usage.js";
|
|
31
38
|
import { readCodexUsage } from "./codex-usage.js";
|
|
32
39
|
import { redeemResetCredit } from "./codex-reset.js";
|
|
@@ -270,8 +277,7 @@ async function claudeSnapshot(name) {
|
|
|
270
277
|
if (cached)
|
|
271
278
|
return cached;
|
|
272
279
|
}
|
|
273
|
-
const
|
|
274
|
-
const token = creds ? accessTokenOf(creds) : null;
|
|
280
|
+
const token = freshAccessToken(configDir("claude", name), defaultFreshenDeps(readBinaryPath("claude")));
|
|
275
281
|
if (!token)
|
|
276
282
|
return null;
|
|
277
283
|
const raw = await fetchUsage(token);
|
|
@@ -293,6 +299,12 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
293
299
|
: pid === "codex"
|
|
294
300
|
? await readCodexUsage(configDir("codex", active))
|
|
295
301
|
: null;
|
|
302
|
+
// Feed the 30-day usage history from the GUI's own fetches: the GUI polls
|
|
303
|
+
// every displayed profile on each refresh, so recording the fresh snapshot
|
|
304
|
+
// here (throttled to ~hourly) fills the chart for all profiles without
|
|
305
|
+
// depending on the background daemon or an "active" profile being set.
|
|
306
|
+
if (usage)
|
|
307
|
+
recordHistorySample(path.join(profileDir(pid, active), "usage-history.json"), usage);
|
|
296
308
|
const wc = worstLiveContext(pid, active);
|
|
297
309
|
const context = wc
|
|
298
310
|
? { sessionId: wc.sessionId, pct: wc.pct, contextTokens: wc.contextTokens, windowTokens: wc.windowTokens, model: wc.model, confidence: wc.confidence }
|
|
@@ -326,8 +338,7 @@ async function cmdStatus(providerId, name, json = false) {
|
|
|
326
338
|
console.log(" (no usage readout for this provider — shows own usage only where available)");
|
|
327
339
|
continue;
|
|
328
340
|
}
|
|
329
|
-
const
|
|
330
|
-
const token = creds ? accessTokenOf(creds) : null;
|
|
341
|
+
const token = freshAccessToken(configDir("claude", n), defaultFreshenDeps(readBinaryPath("claude")));
|
|
331
342
|
if (!token) {
|
|
332
343
|
console.log(" (credential not readable — profile may not have run yet)");
|
|
333
344
|
continue;
|
|
@@ -449,6 +460,55 @@ function cmdShare(mode, flags = []) {
|
|
|
449
460
|
die("usage: agent-switch share on|sync|off|status [--history] [--source <profile|default>]");
|
|
450
461
|
}
|
|
451
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* `agent-switch config export|import <profile> [--full]` — move a Claude
|
|
465
|
+
* profile's SHARED config (settings.json, keybindings.json, CLAUDE.md, skills/,
|
|
466
|
+
* commands/, agents/) between machines as a JSON bundle. Config-ONLY — it never
|
|
467
|
+
* touches credentials. Claude-scoped (config lives under the Claude layout).
|
|
468
|
+
*
|
|
469
|
+
* export → the bundle goes to STDOUT (pipe it: `| gpg -c > backup.gpg`), all
|
|
470
|
+
* status on STDERR so stdout stays a clean bundle.
|
|
471
|
+
* import → reads a bundle from STDIN into the profile's config dir.
|
|
472
|
+
* --full → REFUSED (would bundle live OAuth tokens; account-takeover vector).
|
|
473
|
+
*/
|
|
474
|
+
function cmdConfig(sub, name, flags = {}) {
|
|
475
|
+
try {
|
|
476
|
+
assertNotFull(!!flags.full);
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
die(e.message);
|
|
480
|
+
}
|
|
481
|
+
if (sub === "export") {
|
|
482
|
+
const n = requireProfile("claude", name, "config export");
|
|
483
|
+
const bundle = exportConfig(configDir("claude", n));
|
|
484
|
+
// Status on stderr — stdout is the bundle (so `... | gpg -c > backup.gpg` works).
|
|
485
|
+
console.error(`Exported config-only bundle for claude/${n} (SHARED_ITEMS allowlist; no credentials).`);
|
|
486
|
+
process.stdout.write(bundle + "\n");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (sub === "import") {
|
|
490
|
+
const n = requireProfile("claude", name, "config import");
|
|
491
|
+
let input;
|
|
492
|
+
try {
|
|
493
|
+
input = fs.readFileSync(0, "utf8"); // stdin
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
die("config import reads a bundle from stdin — pipe one in (e.g. `gpg -d backup.gpg | agent-switch config import <profile>`).");
|
|
497
|
+
}
|
|
498
|
+
let written;
|
|
499
|
+
try {
|
|
500
|
+
written = importConfig(configDir("claude", n), input);
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
die(e.message);
|
|
504
|
+
}
|
|
505
|
+
console.error(`Imported ${written.length} config file(s) into claude/${n} (config-only; no credentials):`);
|
|
506
|
+
for (const w of written)
|
|
507
|
+
console.error(` ${w}`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
die("usage: agent-switch config export|import <profile> (config-only; pipe export | gpg -c > backup.gpg)");
|
|
511
|
+
}
|
|
452
512
|
// ---------- session inventory + takeover (Claude; Codex lands per G0.3) ------
|
|
453
513
|
/** Relative age like "3m" / "2h" / "5d" for the sessions table. */
|
|
454
514
|
function ageOf(mtimeMs) {
|
|
@@ -658,6 +718,16 @@ function cmdHookEvent() {
|
|
|
658
718
|
at: new Date().toISOString(),
|
|
659
719
|
};
|
|
660
720
|
appendEvent(eventFile(ROOT, who.provider, who.profile), rec);
|
|
721
|
+
// Session awareness: surface "Claude needs you / finished" as a notification
|
|
722
|
+
// the GUI routes to the pet. dedupe=false — each turn is a distinct moment,
|
|
723
|
+
// so a solo session's repeated turns are never collapsed. Best-effort; a hook
|
|
724
|
+
// must never throw.
|
|
725
|
+
if (rec.event === "Notification") {
|
|
726
|
+
appendNotification({ kind: "warning", title: "Claude is waiting", message: `${who.provider}/${who.profile} needs your input.` }, undefined, undefined, false);
|
|
727
|
+
}
|
|
728
|
+
else if (rec.event === "Stop") {
|
|
729
|
+
appendNotification({ kind: "info", title: "Claude finished", message: `${who.provider}/${who.profile} finished its turn.` }, undefined, undefined, false);
|
|
730
|
+
}
|
|
661
731
|
}
|
|
662
732
|
catch {
|
|
663
733
|
// never throw from a hook
|
|
@@ -1210,7 +1280,8 @@ function cmdLabel(providerId, name, label) {
|
|
|
1210
1280
|
setLabel(providerId, name, label);
|
|
1211
1281
|
console.log(`Labeled ${providerId}/${name} as ${label}.`);
|
|
1212
1282
|
}
|
|
1213
|
-
/** Enable/disable opt-in
|
|
1283
|
+
/** Enable/disable opt-in near-limit notifications (notify + suggest, never an
|
|
1284
|
+
* automatic switch), or show the current setting. */
|
|
1214
1285
|
/** Rename a profile (move its config dir + carry active/label/mappings). */
|
|
1215
1286
|
function cmdRename(providerId, from, to) {
|
|
1216
1287
|
const n = requireProfile(providerId, from, "rename");
|
|
@@ -1254,23 +1325,7 @@ async function cmdReset(providerId, name) {
|
|
|
1254
1325
|
die(`reset failed: ${r.reason ?? "unknown"}`);
|
|
1255
1326
|
}
|
|
1256
1327
|
}
|
|
1257
|
-
function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}
|
|
1258
|
-
// `autoswitch strategy [reset-first|rotation-first]` — global switch behaviour.
|
|
1259
|
-
if (mode === "strategy") {
|
|
1260
|
-
if (value === "reset-first" || value === "rotation-first") {
|
|
1261
|
-
setSwitchStrategy(value);
|
|
1262
|
-
}
|
|
1263
|
-
else if (value !== undefined) {
|
|
1264
|
-
die("usage: agent-switch autoswitch strategy [reset-first|rotation-first]");
|
|
1265
|
-
}
|
|
1266
|
-
if (flags.json) {
|
|
1267
|
-
console.log(JSON.stringify({ strategy: readSwitchStrategy() }));
|
|
1268
|
-
}
|
|
1269
|
-
else {
|
|
1270
|
-
console.log(`Auto-switch strategy: ${readSwitchStrategy()}.`);
|
|
1271
|
-
}
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1328
|
+
function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}) {
|
|
1274
1329
|
const thresholdFlag = flags.threshold;
|
|
1275
1330
|
const threshold = typeof thresholdFlag === "string" ? Number(thresholdFlag) : undefined;
|
|
1276
1331
|
if (threshold !== undefined && (!Number.isFinite(threshold) || threshold < 1 || threshold > 100)) {
|
|
@@ -1299,22 +1354,12 @@ function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}, value) {
|
|
|
1299
1354
|
...(tag !== undefined ? { tag } : {}),
|
|
1300
1355
|
});
|
|
1301
1356
|
const tagNote = cfg.tag === "all" ? "" : `, tag ${cfg.tag}`;
|
|
1302
|
-
console.log(`
|
|
1357
|
+
console.log(`Near-limit notifications for ${providerId} ${cfg.enabled ? "ON" : "OFF"} (threshold ${cfg.threshold}%${tagNote}).`);
|
|
1303
1358
|
if (cfg.enabled) {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
console.warn("⚠️ Usage-policy warning: quota-triggered rotation across accounts can violate\n" +
|
|
1309
|
-
" the providers' usage policies — it pools separate subscriptions to route around\n" +
|
|
1310
|
-
" per-account rate limits. An internal review unanimously recommended removing it;\n" +
|
|
1311
|
-
" it ships off-by-default anyway. See the README before enabling. (Per-context\n" +
|
|
1312
|
-
" profile switching — private / work / client — is unaffected; this applies only\n" +
|
|
1313
|
-
" to automated quota-driven rotation.)");
|
|
1314
|
-
const strat = readSwitchStrategy();
|
|
1315
|
-
console.log(`Strategy: ${strat}${strat === "reset-first" ? " (redeem a banked reset before switching, Codex)" : ""}.\n` +
|
|
1316
|
-
`The daemon moves the active ${providerId} profile to the account with the most headroom once the\n` +
|
|
1317
|
-
"active one hits the threshold. Run `agent-switch service start` so the daemon is watching.");
|
|
1359
|
+
console.log(`Once the active ${providerId} profile hits the threshold, the daemon NOTIFIES you and SUGGESTS the\n` +
|
|
1360
|
+
"account with the most headroom — it never switches automatically; switching stays a manual action.\n" +
|
|
1361
|
+
"On Codex it redeems a banked reset first (and stays on the account) before suggesting a switch.\n" +
|
|
1362
|
+
"Run `agent-switch service start` so the daemon is watching.");
|
|
1318
1363
|
}
|
|
1319
1364
|
return;
|
|
1320
1365
|
}
|
|
@@ -1324,15 +1369,14 @@ function cmdAutoswitch(providerId, providerExplicit, mode, flags = {}, value) {
|
|
|
1324
1369
|
console.log(JSON.stringify(readAutoSwitchAll()));
|
|
1325
1370
|
return;
|
|
1326
1371
|
}
|
|
1327
|
-
console.log(`strategy: ${readSwitchStrategy()}`);
|
|
1328
1372
|
for (const p of providerExplicit ? [providerId] : PROVIDER_IDS) {
|
|
1329
1373
|
const cfg = readAutoSwitch(p);
|
|
1330
1374
|
const tagNote = cfg.tag === "all" ? "" : `, tag ${cfg.tag}`;
|
|
1331
|
-
console.log(`${p}:
|
|
1375
|
+
console.log(`${p}: near-limit notifications ${cfg.enabled ? "ON" : "OFF"} (threshold ${cfg.threshold}%${tagNote}).`);
|
|
1332
1376
|
}
|
|
1333
1377
|
return;
|
|
1334
1378
|
}
|
|
1335
|
-
die("usage: agent-switch autoswitch on|off|status
|
|
1379
|
+
die("usage: agent-switch autoswitch on|off|status [--provider P] [--threshold <1-100>] [--tag all|work|personal|other] [--json]");
|
|
1336
1380
|
}
|
|
1337
1381
|
/**
|
|
1338
1382
|
* Enable/disable a provider's surfaces (cli / ui), or show the current setting.
|
|
@@ -1549,8 +1593,8 @@ async function cmdGui() {
|
|
|
1549
1593
|
// Launch the tray/menubar GUI — downloads its prebuilt artifact from the
|
|
1550
1594
|
// matching GitHub Release on first use, then caches it (see gui-launch.ts).
|
|
1551
1595
|
try {
|
|
1552
|
-
await launchGui();
|
|
1553
|
-
console.log("Launched the agent-switch GUI.");
|
|
1596
|
+
const result = await launchGui();
|
|
1597
|
+
console.log(result === "already-running" ? "The agent-switch GUI is already running." : "Launched the agent-switch GUI.");
|
|
1554
1598
|
}
|
|
1555
1599
|
catch (err) {
|
|
1556
1600
|
die(String(err?.message ?? err));
|
|
@@ -1623,10 +1667,65 @@ function cmdOpen(appId, name) {
|
|
|
1623
1667
|
function cmdShellenv(shellArg) {
|
|
1624
1668
|
console.log(shellenvScript(detectShell(shellArg)));
|
|
1625
1669
|
}
|
|
1670
|
+
/** `agent-switch tooling` — ecosystem tool readout (agent-config, rtk, provider
|
|
1671
|
+
* CLIs). `--json` is the GUI IPC contract: the GUI renders this readout and
|
|
1672
|
+
* never shells out to detect on its own. */
|
|
1673
|
+
function cmdTooling(json = false) {
|
|
1674
|
+
const tools = checkTooling();
|
|
1675
|
+
if (json) {
|
|
1676
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
console.log(`agent-switch tooling — platform ${process.platform}\n`);
|
|
1680
|
+
for (const l of formatToolingLines(tools))
|
|
1681
|
+
console.log(l);
|
|
1682
|
+
}
|
|
1683
|
+
/** `agent-switch tooling install|upgrade <id>` — run the tool's per-platform
|
|
1684
|
+
* install/upgrade command as a visible child process (the GUI runs this inside
|
|
1685
|
+
* its embedded terminal). Exit code = the child's exit code. */
|
|
1686
|
+
function cmdToolingAction(action, id) {
|
|
1687
|
+
if (!id || !TOOL_IDS.includes(id)) {
|
|
1688
|
+
die(`usage: agent-switch tooling ${action} <${TOOL_IDS.join("|")}>`);
|
|
1689
|
+
}
|
|
1690
|
+
process.exit(runToolAction(id, action));
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* `agent-switch usage history [--provider claude] [--profile <name>] [--json]`
|
|
1694
|
+
* — the daemon-recorded usage-history ring per profile (same file the daemon
|
|
1695
|
+
* appends: `<profileDir>/usage-history.json`). `--json` is the GUI contract:
|
|
1696
|
+
* `[{ profile, samples: [{ at, windows }] }]`. Own-profile data only.
|
|
1697
|
+
*/
|
|
1698
|
+
function cmdUsageHistory(providerId, sub, flags = {}) {
|
|
1699
|
+
if (sub !== "history")
|
|
1700
|
+
die("usage: agent-switch usage history [--provider P] [--profile <name>] [--json]");
|
|
1701
|
+
const profiles = typeof flags.profile === "string"
|
|
1702
|
+
? [requireProfile(providerId, flags.profile, "usage history --profile")]
|
|
1703
|
+
: listProfiles(providerId);
|
|
1704
|
+
if (profiles.length === 0)
|
|
1705
|
+
die(`no ${providerId} profiles`);
|
|
1706
|
+
const rows = profiles.map((p) => ({
|
|
1707
|
+
profile: p,
|
|
1708
|
+
samples: readHistory(path.join(profileDir(providerId, p), "usage-history.json")),
|
|
1709
|
+
}));
|
|
1710
|
+
if (flags.json) {
|
|
1711
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
for (const r of rows) {
|
|
1715
|
+
if (r.samples.length === 0) {
|
|
1716
|
+
console.log(`${providerId}/${r.profile}: no usage history yet (the daemon records samples — agent-switch service start)`);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const first = r.samples[0].at;
|
|
1720
|
+
const last = r.samples[r.samples.length - 1].at;
|
|
1721
|
+
console.log(`${providerId}/${r.profile}: ${r.samples.length} sample${r.samples.length === 1 ? "" : "s"} (${first} → ${last})`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1626
1724
|
function usage() {
|
|
1627
1725
|
console.log(`agent-switch — switch accounts for Claude Code, Codex, and Antigravity (macOS · Linux · Windows)
|
|
1628
1726
|
|
|
1629
1727
|
Provider defaults to claude; pass --provider codex|antigravity for the others.
|
|
1728
|
+
Run with no command to launch the tray/menubar GUI (single-instance).
|
|
1630
1729
|
|
|
1631
1730
|
agent-switch add [--provider P] <name> create a profile and log it in
|
|
1632
1731
|
agent-switch import [--provider P] <name> migrate the default install (no re-login)
|
|
@@ -1642,31 +1741,165 @@ Provider defaults to claude; pass --provider codex|antigravity for the others.
|
|
|
1642
1741
|
agent-switch unmap [--provider P] [dir] remove a directory mapping
|
|
1643
1742
|
agent-switch mappings list directory mappings
|
|
1644
1743
|
agent-switch share on|sync|off|status [--history] [--source <profile|default>] [--json] (Claude)
|
|
1744
|
+
agent-switch config export|import <profile> config-only cross-machine bundle to stdout / from stdin (settings, keybindings, CLAUDE.md, skills/, commands/, agents/ — NEVER credentials; pipe export | gpg -c)
|
|
1645
1745
|
agent-switch sessions [profile] [--recent N] [--json] recent + live Claude sessions per profile (with context %)
|
|
1646
1746
|
agent-switch hooks install|uninstall|status [profile] manage lifecycle push hooks in Claude settings.json
|
|
1647
1747
|
agent-switch alerts on|off|status [--threshold 80,95] record context/usage crossings to the notification log (off by default)
|
|
1648
1748
|
agent-switch compact <profile> [--clear] [--dry-run] [--force] type /compact (or /clear) into the profile's managed tmux pane
|
|
1649
1749
|
agent-switch takeover <id> --to <profile> [--from <profile>] [--keep-source] [--in-place] [--print-only] [--force] move a session to another profile and resume it
|
|
1750
|
+
agent-switch rebind [<account>] [--profile <p>] | rebind --restore [--profile <p>] | rebind --reset switch a RUNNING profile's account (macOS; no <account> = interactive picker; --restore reverses; --reset re-enables after a circuit-breaker trip)
|
|
1650
1751
|
agent-switch web <name> claude.ai in a persistent browser (Claude)
|
|
1651
1752
|
agent-switch remove [--provider P] <name> [--force] delete a profile
|
|
1652
1753
|
agent-switch label [--provider P] <name> [Work|Personal|Other|none] tag a profile
|
|
1653
|
-
agent-switch autoswitch on|off|status [--provider P] [--threshold <1-100>]
|
|
1754
|
+
agent-switch autoswitch on|off|status [--provider P] [--threshold <1-100>] notify + suggest near a usage limit; never switches automatically (default OFF)
|
|
1654
1755
|
agent-switch reset <profile> --provider codex redeem one banked Codex rate-limit reset
|
|
1655
1756
|
agent-switch rename <old> <new> [--provider P] rename a profile (name & keep its tag)
|
|
1656
1757
|
agent-switch providers enable|disable|status [--provider P] [--surface cli|ui] enable/disable a provider (default: Claude + Codex)
|
|
1657
|
-
agent-switch gui
|
|
1758
|
+
agent-switch [gui] launch the tray/menubar GUI (the default with no command; won't start a second if one is running)
|
|
1658
1759
|
agent-switch check-update [--json] check GitHub Releases for a newer version
|
|
1659
1760
|
agent-switch update self-update to the latest version (via npm)
|
|
1660
1761
|
agent-switch apps list launchable GUI apps (macOS)
|
|
1661
1762
|
agent-switch open <app> [profile] launch a GUI app on a profile, isolated (macOS)
|
|
1662
1763
|
agent-switch shellenv [--shell zsh|bash|fish|powershell] shell integration
|
|
1663
1764
|
agent-switch service run|start|stop|status|install|uninstall background usage daemon
|
|
1664
|
-
agent-switch notifications [clear] [--json] recent notifications (
|
|
1765
|
+
agent-switch notifications [clear] [--json] recent notifications (usage-limit suggestions, fetch failures)
|
|
1665
1766
|
agent-switch notify --kind K --title T --message M [--json] record a notification event
|
|
1666
1767
|
agent-switch os-notify [on|off|status] [--json] daemon-side OS desktop notifications (default off)
|
|
1667
1768
|
agent-switch uninstall [--force] remove all agent-switch data + daemon
|
|
1769
|
+
agent-switch usage history [--provider P] [--profile <name>] [--json] daemon-recorded usage samples per profile
|
|
1770
|
+
agent-switch tooling [--json] ecosystem tool readout (agent-config, rtk, provider CLIs)
|
|
1771
|
+
agent-switch tooling install|upgrade <tool> install/upgrade an ecosystem tool (agent-config, rtk, claude, codex)
|
|
1668
1772
|
agent-switch doctor per-OS, per-provider self-check`);
|
|
1669
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* The interactive limit dialog (Phase 4) — `agent-switch rebind` with no account
|
|
1776
|
+
* and no `--restore`. Gathers the OTHER claude profiles + their usage, pre-selects
|
|
1777
|
+
* the best-headroom profile as a convenience, and prompts the user to pick. Only
|
|
1778
|
+
* an explicit selection switches (via `rebind`); cancel stays put. The user
|
|
1779
|
+
* interaction IS the compliance line — there is NO automatic switch anywhere in
|
|
1780
|
+
* here. macOS-only for the actual switch; a non-TTY stdin prints the suggested
|
|
1781
|
+
* command instead of hanging on a prompt no one can answer.
|
|
1782
|
+
*/
|
|
1783
|
+
async function rebindDialog(running) {
|
|
1784
|
+
const others = listProfiles("claude").filter((n) => n !== running);
|
|
1785
|
+
const candidates = [];
|
|
1786
|
+
for (const n of others) {
|
|
1787
|
+
// Same usage read path `status` uses (daemon cache when fresh, else the
|
|
1788
|
+
// OAuth /usage endpoint); null when unreadable — the row shows no %.
|
|
1789
|
+
candidates.push({ name: n, snapshot: await claudeSnapshot(n), label: labelFor("claude", n) });
|
|
1790
|
+
}
|
|
1791
|
+
const dialog = buildRebindDialog(candidates, running);
|
|
1792
|
+
const rows = selectableRows(dialog);
|
|
1793
|
+
if (rows.length === 0) {
|
|
1794
|
+
die(`no other claude profile to rebind "${running}" to — add one with \`agent-switch add <name>\`.`);
|
|
1795
|
+
}
|
|
1796
|
+
// Non-interactive stdin: never hang. Print the pre-selected profile + the exact
|
|
1797
|
+
// command that performs the switch, then return (no switch — the user has to run it).
|
|
1798
|
+
if (!process.stdin.isTTY) {
|
|
1799
|
+
const sug = dialog.suggestedProfile;
|
|
1800
|
+
if (sug) {
|
|
1801
|
+
console.error(`Suggested profile (most headroom): ${sug}`);
|
|
1802
|
+
console.error(`Switch "${running}" to it with: agent-switch rebind ${sug} --profile ${running}`);
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
console.error(`No usage data to pre-select a profile. Switch with: agent-switch rebind <account> --profile ${running}`);
|
|
1806
|
+
}
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
// The actual switch is macOS-only (the Linux/Windows credential backend is
|
|
1810
|
+
// unproven — spike R0.1). Refuse before prompting rather than after a pick.
|
|
1811
|
+
if (process.platform !== "darwin") {
|
|
1812
|
+
die("rebind is macOS-only for now (the Linux/Windows credential backend is unproven — spike R0.1).");
|
|
1813
|
+
}
|
|
1814
|
+
console.log(`Rebind "${running}" to which account? A running Claude session adopts it on its next message.`);
|
|
1815
|
+
rows.forEach((r, i) => {
|
|
1816
|
+
const marker = r.profile === dialog.suggestedProfile ? " ← suggested (most headroom)" : "";
|
|
1817
|
+
const label = r.label ? ` [${r.label}]` : "";
|
|
1818
|
+
const util = r.maxUtil === null ? "usage n/a" : `${r.maxUtil}% used`;
|
|
1819
|
+
console.log(` ${i + 1}) ${(r.profile + label).padEnd(24)} ${util}${marker}`);
|
|
1820
|
+
});
|
|
1821
|
+
const enterHint = dialog.suggestedProfile ?? "(no suggestion)";
|
|
1822
|
+
console.log(` [enter] = ${enterHint} · q = cancel (stay on "${running}")`);
|
|
1823
|
+
const SIGINT = "\u0003"; // sentinel distinct from empty input (empty = accept the suggestion)
|
|
1824
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1825
|
+
const answer = await new Promise((resolve) => {
|
|
1826
|
+
rl.on("SIGINT", () => resolve(SIGINT)); // Ctrl-C -> cancel, no switch
|
|
1827
|
+
rl.question("> ", (a) => resolve(a));
|
|
1828
|
+
});
|
|
1829
|
+
rl.close();
|
|
1830
|
+
if (answer === SIGINT) {
|
|
1831
|
+
console.log(`\nStayed on "${running}" — no switch.`);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const choice = resolveRebindChoice(answer, dialog);
|
|
1835
|
+
if (choice.kind === "cancel") {
|
|
1836
|
+
console.log(`Stayed on "${running}" — no switch.`);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (choice.kind === "invalid") {
|
|
1840
|
+
console.log(`No valid selection — stayed on "${running}".`);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
try {
|
|
1844
|
+
const r = await rebind({ account: choice.profile, profile: running });
|
|
1845
|
+
console.log(r.uxNote);
|
|
1846
|
+
}
|
|
1847
|
+
catch (e) {
|
|
1848
|
+
die(e.message);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* `agent-switch rebind <account> [--profile <p>]` — point a RUNNING profile's
|
|
1853
|
+
* credential store at another account, so a live Claude session adopts it on its
|
|
1854
|
+
* next message (macOS; the user-interaction switch, ADR-003). `--restore` reverses
|
|
1855
|
+
* it. With NO account and NO `--restore`, opens the interactive limit dialog
|
|
1856
|
+
* ({@link rebindDialog}). Claude only; the target + running profiles must be logged in.
|
|
1857
|
+
*/
|
|
1858
|
+
async function cmdRebind(providerId, providerExplicit, account, flags) {
|
|
1859
|
+
if (providerExplicit && providerId !== "claude")
|
|
1860
|
+
die(`rebind supports claude only (not ${providerId})`);
|
|
1861
|
+
const profile = typeof flags.profile === "string" ? flags.profile : activeFor("claude") ?? undefined;
|
|
1862
|
+
if (flags.reset) {
|
|
1863
|
+
// Kill-switch reset (ADR-003): clear `disabled` + the failure counter so a
|
|
1864
|
+
// circuit-breaker trip (or a manual disable) no longer refuses rebind.
|
|
1865
|
+
resetRebindKillSwitch();
|
|
1866
|
+
console.log("rebind re-enabled — circuit-breaker cleared (disabled=false, consecutiveFailures=0).");
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (flags.restore) {
|
|
1870
|
+
if (!profile)
|
|
1871
|
+
die("usage: agent-switch rebind --restore [--profile <p>] — no active claude profile to restore");
|
|
1872
|
+
requireProfile("claude", profile, "rebind --restore --profile");
|
|
1873
|
+
try {
|
|
1874
|
+
const r = await restoreRebind({ profile });
|
|
1875
|
+
console.log(`Restored "${r.restoredProfile}" to its own account (was rebound to "${r.wasBoundTo}").`);
|
|
1876
|
+
}
|
|
1877
|
+
catch (e) {
|
|
1878
|
+
die(e.message);
|
|
1879
|
+
}
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (!account) {
|
|
1883
|
+
// No account + no --restore → the interactive limit dialog (Phase 4). The
|
|
1884
|
+
// switch is user-interaction-gated: the picker pre-selects the best-headroom
|
|
1885
|
+
// profile as a convenience, but only an explicit pick switches.
|
|
1886
|
+
if (!profile)
|
|
1887
|
+
die("no running claude profile — pass --profile <name> or set one active with `asw <name>`.");
|
|
1888
|
+
requireProfile("claude", profile, "rebind --profile");
|
|
1889
|
+
return rebindDialog(profile);
|
|
1890
|
+
}
|
|
1891
|
+
if (!profile)
|
|
1892
|
+
die("no running claude profile — pass --profile <name> or set one active with `asw <name>`.");
|
|
1893
|
+
requireProfile("claude", account, "rebind");
|
|
1894
|
+
requireProfile("claude", profile, "rebind --profile");
|
|
1895
|
+
try {
|
|
1896
|
+
const r = await rebind({ account, profile });
|
|
1897
|
+
console.log(r.uxNote);
|
|
1898
|
+
}
|
|
1899
|
+
catch (e) {
|
|
1900
|
+
die(e.message);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1670
1903
|
// ---------- main -------------------------------------------------------------
|
|
1671
1904
|
async function main() {
|
|
1672
1905
|
const argv = process.argv.slice(2);
|
|
@@ -1681,7 +1914,9 @@ async function main() {
|
|
|
1681
1914
|
// profiles) skip it.
|
|
1682
1915
|
if (cmd === "__hook-event")
|
|
1683
1916
|
return cmdHookEvent(); // internal, silent, no migration
|
|
1684
|
-
|
|
1917
|
+
// A bare `agent-switch` (no command) launches the GUI, which reads profiles —
|
|
1918
|
+
// so it runs the migration too. Only pure help / shellenv skip it.
|
|
1919
|
+
if (cmd === undefined || !["help", "--help", "-h", "shellenv"].includes(cmd)) {
|
|
1685
1920
|
const moved = migrateLegacyLayout();
|
|
1686
1921
|
if (moved.length > 0) {
|
|
1687
1922
|
// stderr, not stdout: `dir` is machine-consumed by the shell wrapper
|
|
@@ -1696,7 +1931,7 @@ async function main() {
|
|
|
1696
1931
|
case "use": return cmdUse(providerId, positional[0]);
|
|
1697
1932
|
case "deactivate": return cmdDeactivate(providerId);
|
|
1698
1933
|
case "label": return cmdLabel(providerId, positional[0], positional[1]);
|
|
1699
|
-
case "autoswitch": return cmdAutoswitch(providerId, providerExplicit, positional[0], flags
|
|
1934
|
+
case "autoswitch": return cmdAutoswitch(providerId, providerExplicit, positional[0], flags);
|
|
1700
1935
|
case "reset": return cmdReset(providerId, positional[0]);
|
|
1701
1936
|
case "rename": return cmdRename(providerId, positional[0], positional[1]);
|
|
1702
1937
|
case "providers": return cmdProviders(providerId, providerExplicit, positional[0], flags);
|
|
@@ -1726,6 +1961,7 @@ async function main() {
|
|
|
1726
1961
|
case "unmap": return cmdUnmap(providerExplicit ? providerId : undefined, positional[0]);
|
|
1727
1962
|
case "mappings": return cmdMappings();
|
|
1728
1963
|
case "share": return cmdShare(positional[0], rest.slice(1));
|
|
1964
|
+
case "config": return cmdConfig(positional[0], positional[1], flags);
|
|
1729
1965
|
case "sessions":
|
|
1730
1966
|
if (positional[0] === "rm")
|
|
1731
1967
|
return cmdSessionsRm(providerId, providerExplicit, positional[1], flags);
|
|
@@ -1738,18 +1974,32 @@ async function main() {
|
|
|
1738
1974
|
case "alerts": return cmdAlerts(positional[0], flags);
|
|
1739
1975
|
case "compact": return cmdCompact(providerId, positional[0], flags);
|
|
1740
1976
|
case "takeover": return cmdTakeover(providerId, providerExplicit, positional[0], flags);
|
|
1977
|
+
case "rebind": return cmdRebind(providerId, providerExplicit, positional[0], flags);
|
|
1741
1978
|
case "web": return cmdWeb(positional[0]);
|
|
1742
1979
|
case "remove":
|
|
1743
1980
|
case "rm": return cmdRemove(providerId, positional[0], !!flags.force);
|
|
1744
1981
|
case "shellenv": return cmdShellenv(flags.shell ?? positional[0]);
|
|
1745
1982
|
case "service": return cmdService(positional[0]);
|
|
1983
|
+
case "usage": return cmdUsageHistory(providerId, positional[0], flags);
|
|
1984
|
+
case "tooling":
|
|
1985
|
+
if (positional[0] === "install" || positional[0] === "upgrade") {
|
|
1986
|
+
return cmdToolingAction(positional[0], positional[1]);
|
|
1987
|
+
}
|
|
1988
|
+
if (positional[0] !== undefined) {
|
|
1989
|
+
die(`usage: agent-switch tooling [install|upgrade <${TOOL_IDS.join("|")}>] [--json]`);
|
|
1990
|
+
}
|
|
1991
|
+
return cmdTooling(!!flags.json);
|
|
1746
1992
|
case "doctor": return process.exit(await runDoctor());
|
|
1747
1993
|
case "help":
|
|
1748
1994
|
case "--help":
|
|
1749
1995
|
case "-h": return usage();
|
|
1996
|
+
// A bare `agent-switch` (no command) launches the GUI; an unknown command
|
|
1997
|
+
// still prints usage and exits non-zero.
|
|
1750
1998
|
default:
|
|
1999
|
+
if (cmd === undefined)
|
|
2000
|
+
return cmdGui();
|
|
1751
2001
|
usage();
|
|
1752
|
-
process.exit(
|
|
2002
|
+
process.exit(1);
|
|
1753
2003
|
}
|
|
1754
2004
|
}
|
|
1755
2005
|
main().catch((err) => die(String(err?.message ?? err)));
|
package/dist/keychain.js
CHANGED
|
@@ -48,6 +48,20 @@ export function deletePassword(service) {
|
|
|
48
48
|
return false;
|
|
49
49
|
return (security(["delete-generic-password", "-a", os.userInfo().username, "-s", service]) !== null);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Upsert a generic password entry (`-U` overwrites an existing value). Returns
|
|
53
|
+
* true on success. This is the ONE credential-store WRITE in agent-switch — used
|
|
54
|
+
* ONLY by `src/rebind.ts` under Claude Code's lock (ADR-003). Elsewhere the
|
|
55
|
+
* credential store stays read-only.
|
|
56
|
+
*
|
|
57
|
+
* Note: the value is passed on argv (as `security` requires with `-w`), so it is
|
|
58
|
+
* briefly visible to `ps` — the same trade-off Claude Code's own tooling makes.
|
|
59
|
+
*/
|
|
60
|
+
export function addPassword(service, value) {
|
|
61
|
+
if (!isMac())
|
|
62
|
+
return false;
|
|
63
|
+
return security(["add-generic-password", "-U", "-a", os.userInfo().username, "-s", service, "-w", value]) !== null;
|
|
64
|
+
}
|
|
51
65
|
/** The live credential of the default ~/.claude install (keychain, macOS). */
|
|
52
66
|
export function readDefaultCredential() {
|
|
53
67
|
return getPassword(DEFAULT_SERVICE);
|
package/dist/notifications.js
CHANGED
|
@@ -33,11 +33,16 @@ function writeNotifications(list, file) {
|
|
|
33
33
|
* Append an event (kept newest-last). Deduplicates a byte-identical
|
|
34
34
|
* kind+title+message against the most recent entry within DEDUP_WINDOW_MS.
|
|
35
35
|
* Returns the created notification, or `null` when it was deduplicated.
|
|
36
|
+
*
|
|
37
|
+
* `dedupe: false` skips that collapse — for session-awareness events (Claude
|
|
38
|
+
* finished / is waiting) where each occurrence is a distinct moment the user
|
|
39
|
+
* wants surfaced, so a solo session's repeated turns are never swallowed.
|
|
36
40
|
*/
|
|
37
|
-
export function appendNotification(event, now = Date.now(), file = NOTIFICATIONS_FILE) {
|
|
41
|
+
export function appendNotification(event, now = Date.now(), file = NOTIFICATIONS_FILE, dedupe = true) {
|
|
38
42
|
const list = readNotifications(file);
|
|
39
43
|
const last = list[list.length - 1];
|
|
40
|
-
if (
|
|
44
|
+
if (dedupe &&
|
|
45
|
+
last &&
|
|
41
46
|
last.kind === event.kind &&
|
|
42
47
|
last.title === event.title &&
|
|
43
48
|
last.message === event.message &&
|
package/dist/profiles.js
CHANGED
|
@@ -13,7 +13,6 @@ export function isProfileLabel(v) {
|
|
|
13
13
|
return typeof v === "string" && PROFILE_LABELS.includes(v);
|
|
14
14
|
}
|
|
15
15
|
export const DEFAULT_AUTOSWITCH = { enabled: false, threshold: 95, tag: "all" };
|
|
16
|
-
export const DEFAULT_SWITCH_STRATEGY = "reset-first";
|
|
17
16
|
/**
|
|
18
17
|
* Providers enabled out of the box. Everything else is available but off by
|
|
19
18
|
* default — the user opts it in from the Providers settings tab. A provider that
|
|
@@ -21,6 +20,7 @@ export const DEFAULT_SWITCH_STRATEGY = "reset-first";
|
|
|
21
20
|
* upgrading never hides an account the user already set up.
|
|
22
21
|
*/
|
|
23
22
|
export const PROVIDERS_ON_BY_DEFAULT = ["claude", "codex"];
|
|
23
|
+
export const DEFAULT_REBIND_STATE = { disabled: false, consecutiveFailures: 0 };
|
|
24
24
|
function emptyActive() {
|
|
25
25
|
return { claude: null, codex: null, antigravity: null };
|
|
26
26
|
}
|
|
@@ -155,6 +155,11 @@ function normalizeBinaryPaths(raw) {
|
|
|
155
155
|
out[p] = r[p];
|
|
156
156
|
return out;
|
|
157
157
|
}
|
|
158
|
+
function normalizeRebindState(raw) {
|
|
159
|
+
const r = (raw ?? {});
|
|
160
|
+
const consecutiveFailures = typeof r.consecutiveFailures === "number" && r.consecutiveFailures >= 0 ? Math.floor(r.consecutiveFailures) : 0;
|
|
161
|
+
return { disabled: r.disabled === true, consecutiveFailures };
|
|
162
|
+
}
|
|
158
163
|
export function readState() {
|
|
159
164
|
try {
|
|
160
165
|
const raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
@@ -162,29 +167,21 @@ export function readState() {
|
|
|
162
167
|
const autoSwitch = normalizeAutoSwitchMap(raw?.autoSwitch);
|
|
163
168
|
const providers = normalizeProvidersMap(raw?.providers);
|
|
164
169
|
const binaryPaths = normalizeBinaryPaths(raw?.binaryPaths);
|
|
165
|
-
const switchStrategy = raw?.switchStrategy === "rotation-first" ? "rotation-first" : DEFAULT_SWITCH_STRATEGY;
|
|
166
170
|
const osNotifications = raw?.osNotifications === true;
|
|
171
|
+
const rebind = normalizeRebindState(raw?.rebind);
|
|
167
172
|
// v1: { active: "<name>" } — a single Claude profile.
|
|
168
173
|
if (typeof raw?.active === "string") {
|
|
169
|
-
return { active: { ...emptyActive(), claude: raw.active }, labels, autoSwitch, providers, binaryPaths,
|
|
174
|
+
return { active: { ...emptyActive(), claude: raw.active }, labels, autoSwitch, providers, binaryPaths, osNotifications, rebind };
|
|
170
175
|
}
|
|
171
176
|
if (raw?.active && typeof raw.active === "object") {
|
|
172
|
-
return { active: { ...emptyActive(), ...raw.active }, labels, autoSwitch, providers, binaryPaths,
|
|
177
|
+
return { active: { ...emptyActive(), ...raw.active }, labels, autoSwitch, providers, binaryPaths, osNotifications, rebind };
|
|
173
178
|
}
|
|
174
|
-
return { active: emptyActive(), labels, autoSwitch, providers, binaryPaths,
|
|
179
|
+
return { active: emptyActive(), labels, autoSwitch, providers, binaryPaths, osNotifications, rebind };
|
|
175
180
|
}
|
|
176
181
|
catch {
|
|
177
182
|
/* absent / unparsable → default */
|
|
178
183
|
}
|
|
179
|
-
return { active: emptyActive(), labels: {}, autoSwitch: emptyAutoSwitch(), providers: emptyProviders(), binaryPaths: {},
|
|
180
|
-
}
|
|
181
|
-
export function readSwitchStrategy() {
|
|
182
|
-
return readState().switchStrategy;
|
|
183
|
-
}
|
|
184
|
-
export function setSwitchStrategy(strategy) {
|
|
185
|
-
const state = readState();
|
|
186
|
-
state.switchStrategy = strategy;
|
|
187
|
-
writeState(state);
|
|
184
|
+
return { active: emptyActive(), labels: {}, autoSwitch: emptyAutoSwitch(), providers: emptyProviders(), binaryPaths: {}, osNotifications: false, rebind: { ...DEFAULT_REBIND_STATE } };
|
|
188
185
|
}
|
|
189
186
|
export function readOsNotifications() {
|
|
190
187
|
return readState().osNotifications;
|
|
@@ -194,6 +191,21 @@ export function setOsNotifications(on) {
|
|
|
194
191
|
state.osNotifications = on;
|
|
195
192
|
writeState(state);
|
|
196
193
|
}
|
|
194
|
+
// ---------- rebind kill-switch / circuit-breaker (ADR-003) ----------
|
|
195
|
+
/** Current rollback/circuit-breaker state for the `rebind` write path. */
|
|
196
|
+
export function readRebindKillSwitch() {
|
|
197
|
+
return readState().rebind;
|
|
198
|
+
}
|
|
199
|
+
/** Persist the rebind kill-switch state (breaker counter + disabled flag). */
|
|
200
|
+
export function writeRebindKillSwitch(next) {
|
|
201
|
+
const state = readState();
|
|
202
|
+
state.rebind = normalizeRebindState(next);
|
|
203
|
+
writeState(state);
|
|
204
|
+
}
|
|
205
|
+
/** Re-enable `rebind`: clear `disabled` + reset the failure counter (`--reset`). */
|
|
206
|
+
export function resetRebindKillSwitch() {
|
|
207
|
+
writeRebindKillSwitch({ ...DEFAULT_REBIND_STATE });
|
|
208
|
+
}
|
|
197
209
|
export function writeState(state) {
|
|
198
210
|
ensureRoot();
|
|
199
211
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|