@event4u/agent-switch 1.0.0 → 1.0.2

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 CHANGED
@@ -307,6 +307,24 @@ Usage readout is per-provider: Claude exposes an OAuth `/usage` endpoint;
307
307
  **Codex and Gemini have no usage readout**, so they show identity only (never a
308
308
  fabricated number).
309
309
 
310
+ ### Run it all automatically (on login)
311
+
312
+ For a hands-off setup where the daemon **and** the tray GUI start on login:
313
+
314
+ ```bash
315
+ agent-switch service install # daemon on login (launchd / systemd --user / Task Scheduler)
316
+ agent-switch gui # launch the tray GUI (installs it on first use, then runs it)
317
+ ```
318
+
319
+ The tray GUI enables **launch-at-login by default on its first run**; toggle it
320
+ any time in **Settings → General → Start at login**. The daemon is a separate,
321
+ one-time `service install` — the GUI does not manage it. Check both with
322
+ `agent-switch service status` and the GUI's General tab.
323
+
324
+ > After updating the GUI (a new npm version), re-toggle *Start at login* once so
325
+ > the login item points at the new build — the launch-at-login entry references
326
+ > the installed binary path, which changes across versions.
327
+
310
328
  ### Installing the desktop app (unsigned builds)
311
329
 
312
330
  The tray GUI ships as native installers on the project's GitHub Releases —
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-switch",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Switch between multiple Claude Code accounts on macOS, Linux, and Windows via isolated CLAUDE_CONFIG_DIR profiles",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "optionalDependencies": {
34
34
  "playwright": "^1.49.0",
35
- "@event4u/agent-switch-darwin": "1.0.0",
36
- "@event4u/agent-switch-linux-x64": "1.0.0",
37
- "@event4u/agent-switch-win32-x64": "1.0.0"
35
+ "@event4u/agent-switch-darwin": "1.0.2",
36
+ "@event4u/agent-switch-linux-x64": "1.0.2",
37
+ "@event4u/agent-switch-win32-x64": "1.0.2"
38
38
  }
39
39
  }
@@ -1,81 +0,0 @@
1
- /**
2
- * Seed a Claude profile's permission allowlist from the user's GLOBAL Claude
3
- * settings (~/.claude/settings.json).
4
- *
5
- * agent-switch runs each profile under its own CLAUDE_CONFIG_DIR, so Claude Code
6
- * reads THAT profile's settings.json and never the global one. A freshly-`add`ed
7
- * profile therefore starts with none of the globally-approved command
8
- * permissions and re-prompts for each (`import` is unaffected — it copies the
9
- * whole ~/.claude, settings.json included). This copies (unions) the global
10
- * `permissions` into a profile — at create time, and via a backfill command for
11
- * existing profiles.
12
- *
13
- * Scope: ONLY `permissions` is inherited. Hooks are managed per-profile by
14
- * hooks.ts; theme / model / statusLine are deliberately profile-own. The global
15
- * file is only ever READ here — it is never modified, so a profile can never
16
- * clobber the source allowlist (unlike a shared symlink + `share sync`).
17
- */
18
- import * as fs from "node:fs";
19
- import * as path from "node:path";
20
- import { readSettings } from "./hooks.js";
21
- /** Union two string lists, global first, de-duplicated, order-preserving. */
22
- function union(a = [], b = []) {
23
- return [...new Set([...a, ...b])];
24
- }
25
- /**
26
- * Pure merge: the profile's permissions with the global allow/deny/ask lists
27
- * unioned in. A profile's own grants are never dropped; a profile scalar
28
- * (`defaultMode`) wins over the global. Returns null when the global carries no
29
- * permissions to inherit (caller then leaves the profile untouched).
30
- */
31
- export function mergePermissions(globalPerms, profilePerms) {
32
- if (!globalPerms || typeof globalPerms !== "object")
33
- return null;
34
- // Nothing to inherit unless the global actually carries a grant list.
35
- const globalHasGrants = (globalPerms.allow?.length ?? 0) + (globalPerms.deny?.length ?? 0) + (globalPerms.ask?.length ?? 0) > 0;
36
- if (!globalHasGrants)
37
- return null;
38
- const p = profilePerms && typeof profilePerms === "object" ? profilePerms : {};
39
- const merged = { ...p };
40
- const allow = union(globalPerms.allow, p.allow);
41
- if (allow.length)
42
- merged.allow = allow;
43
- const deny = union(globalPerms.deny, p.deny);
44
- if (deny.length)
45
- merged.deny = deny;
46
- const ask = union(globalPerms.ask, p.ask);
47
- if (ask.length)
48
- merged.ask = ask;
49
- const defaultMode = p.defaultMode ?? globalPerms.defaultMode;
50
- if (defaultMode !== undefined)
51
- merged.defaultMode = defaultMode;
52
- return merged;
53
- }
54
- /** Pure: a NEW profile settings object with the global permissions inherited.
55
- * Returns the input unchanged when there is nothing to inherit. */
56
- export function withInheritedPermissions(profileSettings, globalSettings) {
57
- const merged = mergePermissions(globalSettings?.permissions, profileSettings?.permissions);
58
- if (!merged)
59
- return profileSettings;
60
- return { ...profileSettings, permissions: merged };
61
- }
62
- /**
63
- * Seed `profileConfigDir/settings.json` permissions from
64
- * `globalConfigDir/settings.json`. Reads both, writes only the profile. Degraded
65
- * mode: a missing/garbage global settings file → no-op (readSettings returns
66
- * {}). The global file is never written. Idempotent — a second run over an
67
- * already-seeded profile reports `changed: false`.
68
- */
69
- export function inheritPermissions(profileConfigDir, globalConfigDir) {
70
- const globalSettings = readSettings(globalConfigDir);
71
- const before = readSettings(profileConfigDir);
72
- const next = withInheritedPermissions(before, globalSettings);
73
- const afterAllow = (next.permissions?.allow ?? []).length;
74
- const beforeAllow = (before.permissions?.allow ?? []).length;
75
- if (JSON.stringify(next) === JSON.stringify(before)) {
76
- return { changed: false, allowCount: afterAllow, addedAllow: 0 };
77
- }
78
- fs.mkdirSync(profileConfigDir, { recursive: true, mode: 0o700 });
79
- fs.writeFileSync(path.join(profileConfigDir, "settings.json"), JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
80
- return { changed: true, allowCount: afterAllow, addedAllow: afterAllow - beforeAllow };
81
- }
package/dist/tokens.js DELETED
@@ -1,88 +0,0 @@
1
- /**
2
- * Token + cost reporting (roadmap: road-to-agent-switch-session-telemetry,
3
- * Phase 5), DELEGATED to ccusage (council D2 — verified viable in spikes/t7).
4
- * agent-switch does NOT re-implement transcript aggregation, dedup, or a
5
- * pricing engine. It contributes two things ccusage cannot: (1) pointing
6
- * ccusage at a specific profile's config dir, and (2) the cost-honesty label —
7
- * agent-switch knows a profile is a subscription/OAuth login, so its cost is
8
- * "notional" (API-equivalent), not real spend.
9
- *
10
- * ccusage is an OPTIONAL external tool (the tmux/playwright pattern): detected
11
- * on PATH, never bundled. Absent → the caller prints an install pointer.
12
- */
13
- import { spawnSync } from "node:child_process";
14
- /** The command `tokens install` runs (in the GUI's embedded terminal or a
15
- * user's shell) to install ccusage globally. Fixed literal — never bundled as
16
- * a dependency (zero-dep invariant + council D2). */
17
- export const CCUSAGE_INSTALL = ["npm", "install", "-g", "ccusage"];
18
- /** Resolve the ccusage runner. A PATH binary is preferred; the
19
- * `AGENT_SWITCH_CCUSAGE` env var overrides with an explicit argv prefix
20
- * (e.g. "npx -y ccusage@latest") for a zero-install run or tests. Null when
21
- * neither is available. */
22
- export function resolveCcusageRunner(env = process.env) {
23
- const override = env.AGENT_SWITCH_CCUSAGE?.trim();
24
- if (override)
25
- return override.split(/\s+/);
26
- const probe = process.platform === "win32" ? "where" : "which";
27
- const r = spawnSync(probe, ["ccusage"], { encoding: "utf8" });
28
- if (r.status === 0 && (r.stdout || "").trim())
29
- return ["ccusage"];
30
- return null;
31
- }
32
- /** Parse ccusage `daily --json` output ({ daily: [...], totals: {...} }) into
33
- * our shape. Pure + defensive: unknown/short shapes degrade to empty, never
34
- * throw. `costBasis` is stamped by the caller (ccusage cannot know it). */
35
- export function parseCcusageDaily(json, costBasis) {
36
- const rows = Array.isArray(json?.daily) ? json.daily : Array.isArray(json) ? json : [];
37
- const num = (v) => (Number.isFinite(v) ? v : 0);
38
- const days = rows.map((r) => ({
39
- date: typeof r?.period === "string" ? r.period : typeof r?.date === "string" ? r.date : "?",
40
- inputTokens: num(r?.inputTokens),
41
- outputTokens: num(r?.outputTokens),
42
- cacheCreationTokens: num(r?.cacheCreationTokens),
43
- cacheReadTokens: num(r?.cacheReadTokens),
44
- totalTokens: num(r?.totalTokens),
45
- cost: num(r?.totalCost),
46
- models: Array.isArray(r?.modelsUsed) ? r.modelsUsed : [],
47
- }));
48
- const t = json?.totals ?? {};
49
- const totals = {
50
- inputTokens: num(t.inputTokens) || days.reduce((s, d) => s + d.inputTokens, 0),
51
- outputTokens: num(t.outputTokens) || days.reduce((s, d) => s + d.outputTokens, 0),
52
- cacheCreationTokens: num(t.cacheCreationTokens) || days.reduce((s, d) => s + d.cacheCreationTokens, 0),
53
- cacheReadTokens: num(t.cacheReadTokens) || days.reduce((s, d) => s + d.cacheReadTokens, 0),
54
- totalTokens: num(t.totalTokens) || days.reduce((s, d) => s + d.totalTokens, 0),
55
- cost: num(t.totalCost) || days.reduce((s, d) => s + d.cost, 0),
56
- };
57
- return { days, totals, costBasis };
58
- }
59
- /** The cost basis for a profile. agent-switch profiles are OAuth/subscription
60
- * logins → their token cost is NOTIONAL (API-equivalent, not real spend). A
61
- * raw API key credential (sk-ant-…) would be `computed`. */
62
- export function costBasisFor(rawCredential) {
63
- // A raw API key → real, computed spend. Anything else (OAuth login blob, or
64
- // unknown) → notional, the safe label that never overstates real spend.
65
- if (rawCredential && /^sk-(ant|proj)-/.test(rawCredential.trim()))
66
- return "computed";
67
- return "notional";
68
- }
69
- /** Run ccusage for one profile's config dir and return the parsed report, or
70
- * null when ccusage failed / produced no JSON (caller degrades gracefully). */
71
- export function runCcusage(runner, provider, configDir, costBasis) {
72
- const envKey = provider === "codex" ? "CODEX_HOME" : "CLAUDE_CONFIG_DIR";
73
- const [cmd, ...prefix] = runner;
74
- const r = spawnSync(cmd, [...prefix, "daily", "--json"], {
75
- encoding: "utf8",
76
- timeout: 180_000,
77
- env: { ...process.env, [envKey]: configDir },
78
- });
79
- const out = (r.stdout || "").trim();
80
- if (!out)
81
- return null;
82
- try {
83
- return parseCcusageDaily(JSON.parse(out), costBasis);
84
- }
85
- catch {
86
- return null;
87
- }
88
- }