sideby 0.6.0 → 0.8.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 (43) hide show
  1. package/README.md +3 -2
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +2 -0
  4. package/dist/src/cli/commands.js +44 -5
  5. package/dist/src/cli/json-schemas.js +37 -0
  6. package/dist/src/cli/main.js +26 -2
  7. package/dist/src/core/desktop-app.d.ts +96 -0
  8. package/dist/src/core/desktop-app.js +382 -0
  9. package/dist/src/core/doctor.d.ts +6 -1
  10. package/dist/src/core/doctor.js +19 -0
  11. package/dist/src/core/handoff-run.d.ts +2 -0
  12. package/dist/src/core/handoff-run.js +10 -4
  13. package/dist/src/core/handoff-select.d.ts +9 -0
  14. package/dist/src/core/handoff-select.js +10 -2
  15. package/dist/src/core/handoff-status.js +12 -0
  16. package/dist/src/core/handoff.d.ts +12 -0
  17. package/dist/src/core/handoff.js +13 -4
  18. package/dist/src/core/same-login.d.ts +24 -0
  19. package/dist/src/core/same-login.js +52 -0
  20. package/dist/src/core/tuning-apply.js +1 -1
  21. package/dist/src/families/claude/desktop.d.ts +6 -0
  22. package/dist/src/families/claude/desktop.js +28 -0
  23. package/dist/src/families/codex/desktop.d.ts +7 -0
  24. package/dist/src/families/codex/desktop.js +25 -0
  25. package/dist/src/families/desktop.d.ts +27 -0
  26. package/dist/src/families/desktop.js +9 -0
  27. package/dist/src/panel/handler.js +28 -6
  28. package/dist/src/panel/i18n.d.ts +11 -0
  29. package/dist/src/panel/i18n.js +22 -0
  30. package/dist/src/panel/page-logic.d.ts +6 -1
  31. package/dist/src/panel/page-logic.js +5 -4
  32. package/dist/src/panel/page-script.js +37 -3
  33. package/dist/src/runtime.d.ts +26 -0
  34. package/dist/src/runtime.js +89 -23
  35. package/dist/src/types.d.ts +5 -0
  36. package/package.json +2 -2
  37. package/schemas/desktop.json +80 -0
  38. package/schemas/list.json +27 -0
  39. package/schemas/next.json +12 -0
  40. package/schemas/quota.json +4 -0
  41. package/skills/sideby/SKILL.md +4 -2
  42. package/skills/sideby/references/handoff.md +1 -1
  43. package/skills/sideby/references/quota.md +2 -0
@@ -2,6 +2,7 @@
2
2
  import { aliasesByAccount, discoverAccounts, resolveRef } from './core/accounts.js';
3
3
  import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, handoffSettings, loadConfig, removeConfigAlias, } from './core/config.js';
4
4
  import { createAccount } from './core/create.js';
5
+ import { bindDesktopDeps, checkAppData } from './core/desktop-app.js';
5
6
  import { applyFixes, runDoctor } from './core/doctor.js';
6
7
  import { UserError } from './core/errors.js';
7
8
  import { planHandoff } from './core/handoff.js';
@@ -13,6 +14,7 @@ import { prepareLaunch, which } from './core/launch.js';
13
14
  import { memoStore } from './core/memo-store.js';
14
15
  import { resolvePaths } from './core/paths.js';
15
16
  import { findResumeRoute } from './core/resume.js';
17
+ import { SAME_LOGIN_FAMILIES, sameLoginGroups, sharedQuota } from './core/same-login.js';
16
18
  import { shareMeaning } from './core/share-meaning.js';
17
19
  import { SHELLS, shellInitScript, writeShellInitFile, } from './core/shell-init.js';
18
20
  import { groupSuggestion, MAX_COMPACT_WINDOW, MIN_COMPACT_WINDOW, } from './core/tuning.js';
@@ -83,6 +85,56 @@ export async function createRuntime(opts = {}) {
83
85
  out.push(...(await discoverAccounts(f, paths.home, ignore)));
84
86
  return out;
85
87
  };
88
+ /** Same Login groups (spec §3.21) among `list`: ref → the other refs. Only Families that group take part. */
89
+ const groupsOf = async (list) => {
90
+ const members = await Promise.all(list
91
+ .filter((a) => a.kind === 'subscription' && SAME_LOGIN_FAMILIES.has(a.family) && families.has(a.family))
92
+ .map(async (a) => {
93
+ const identity = await readIdentity(familyOf(a.family), a);
94
+ return { ref: a.ref, family: a.family, kind: a.kind, ...(identity ? { identity } : {}) };
95
+ }));
96
+ return sameLoginGroups(members);
97
+ };
98
+ /** The Accounts of `account`'s Same Login, other than itself; reads only its own Family. */
99
+ const peersOf = async (account) => {
100
+ const f = families.get(account.family);
101
+ if (!f || account.kind === 'api' || !SAME_LOGIN_FAMILIES.has(f.id))
102
+ return [];
103
+ const list = await discoverAccounts(f, paths.home, ignore);
104
+ const peers = (await groupsOf(list)).get(account.ref) ?? [];
105
+ return list.filter((a) => peers.includes(a.ref));
106
+ };
107
+ /** Every Account's Quota as shown (shared within a Same Login), reading each Account's own cache once. */
108
+ const quotasOf = async (list) => {
109
+ const [own, groups] = await Promise.all([
110
+ Promise.all(list.map(async (a) => [a.ref, await ownQuota(a)])).then((e) => new Map(e)),
111
+ groupsOf(list),
112
+ ]);
113
+ return new Map(list.map((a) => [a.ref, sharedQuota(a.ref, own, groups.get(a.ref) ?? [])]));
114
+ };
115
+ const ownQuota = async (account) => {
116
+ if (account.kind === 'api')
117
+ return { status: 'unavailable', reason: 'api-account' };
118
+ const f = familyOf(account.family);
119
+ if (!f.readQuota)
120
+ return { status: 'unavailable', reason: 'no-source' };
121
+ await memos.ready();
122
+ try {
123
+ const q = await f.readQuota(account, readContext());
124
+ // Only sideby marks a reading as borrowed from another Account (spec §3.21); a reader's own `from` is dropped.
125
+ if (q.status === 'ok' && q.from !== undefined) {
126
+ const { from: _from, ...own } = q;
127
+ return own;
128
+ }
129
+ return q;
130
+ }
131
+ catch (err) {
132
+ return { status: 'unavailable', reason: 'unrecognized', detail: err.message };
133
+ }
134
+ finally {
135
+ memos.saveSoon();
136
+ }
137
+ };
86
138
  const loginOf = async (a) => {
87
139
  if (a.kind === 'api')
88
140
  return 'not-needed';
@@ -207,13 +259,14 @@ export async function createRuntime(opts = {}) {
207
259
  return undefined;
208
260
  }
209
261
  };
210
- const [installed, login, model, identity] = await Promise.all([
262
+ const [installed, login, model, identity, peers] = await Promise.all([
211
263
  which(f.bin, env),
212
264
  account.kind === 'api'
213
265
  ? undefined
214
266
  : safe('login state', f.loginState && (() => f.loginState(account))),
215
267
  safe('model', f.model && (() => f.model(account))),
216
268
  readIdentity(f, account),
269
+ peersOf(account).catch(() => []),
217
270
  ]);
218
271
  return {
219
272
  ...account,
@@ -222,6 +275,7 @@ export async function createRuntime(opts = {}) {
222
275
  login: account.kind === 'api' ? 'not-needed' : (login ?? 'unknown'),
223
276
  ...(model ? { model } : {}),
224
277
  ...(identity ? { identity } : {}),
278
+ ...(peers.length ? { sameLogin: peers.map((p) => p.ref) } : {}),
225
279
  ...(problems.length ? { problems } : {}),
226
280
  };
227
281
  },
@@ -254,6 +308,8 @@ export async function createRuntime(opts = {}) {
254
308
  // Checked for every Family in the run, also when Doctor is aimed at one Account, so the saved result
255
309
  // of that Family keeps its skill warning.
256
310
  skillVersion: packageVersion(),
311
+ // Desktop App data folders sideby made for an Account (spec §3.20).
312
+ extraChecks: (a) => checkAppData(loaded.plugins, paths.home, a),
257
313
  };
258
314
  // Auto Handoff warnings (spec §3.17) join the general Findings; they never fail Doctor.
259
315
  const all = await accounts();
@@ -433,21 +489,13 @@ export async function createRuntime(opts = {}) {
433
489
  };
434
490
  },
435
491
  async quota(account) {
436
- if (account.kind === 'api')
437
- return { status: 'unavailable', reason: 'api-account' };
438
- const f = familyOf(account.family);
439
- if (!f.readQuota)
440
- return { status: 'unavailable', reason: 'no-source' };
441
- await memos.ready();
442
- try {
443
- return await f.readQuota(account, readContext());
444
- }
445
- catch (err) {
446
- return { status: 'unavailable', reason: 'unrecognized', detail: err.message };
447
- }
448
- finally {
449
- memos.saveSoon();
450
- }
492
+ const peers = await peersOf(account);
493
+ if (!peers.length)
494
+ return ownQuota(account);
495
+ return (await quotasOf([account, ...peers])).get(account.ref);
496
+ },
497
+ async sameLogin(account) {
498
+ return (await peersOf(account)).map((a) => a.ref);
451
499
  },
452
500
  async usage(account) {
453
501
  const f = familyOf(account.family);
@@ -553,21 +601,36 @@ export async function createRuntime(opts = {}) {
553
601
  throw new UserError(`no ${f.title} accounts yet; create one with \`sideby new ${f.id} <name>\``, {
554
602
  code: 'no-accounts',
555
603
  });
556
- // Only the login state and Quota: no identity, model or Host lookups are needed to rank.
604
+ // Login state, Quota and Same Login (spec §3.21): no model or Host lookups are needed to rank.
605
+ const [quotas, groups] = await Promise.all([quotasOf(list), groupsOf(list)]);
557
606
  const candidates = await Promise.all(list.map(async (a) => {
558
- const [login, quota] = await Promise.all([loginOf(a), rt.quota(a)]);
559
- return { ref: a.ref, name: a.name, isMain: a.isMain, kind: a.kind, login, quota };
607
+ const peers = groups.get(a.ref);
608
+ return {
609
+ ref: a.ref,
610
+ name: a.name,
611
+ isMain: a.isMain,
612
+ kind: a.kind,
613
+ login: await loginOf(a),
614
+ quota: quotas.get(a.ref),
615
+ ...(peers ? { sameLogin: peers } : {}),
616
+ };
560
617
  }));
561
- return planHandoff(f.id, candidates, readContext().now, o);
618
+ // The current Account: the one sideby started this process from, when it belongs to this Family.
619
+ const current = env.SIDEBY_ACCOUNT;
620
+ const from = current && list.some((a) => a.ref === current) ? current : undefined;
621
+ return planHandoff(f.id, candidates, readContext().now, { ...o, ...(from ? { from } : {}) });
562
622
  },
563
623
  handoffSettings: () => handoffSettings(config),
564
624
  async handoffDecision(origin, used, opts = {}) {
565
- const candidates = await Promise.all((await accounts()).map(async (a) => {
625
+ const all = await accounts();
626
+ const [quotas, groups] = await Promise.all([quotasOf(all), groupsOf(all)]);
627
+ const candidates = await Promise.all(all.map(async (a) => {
566
628
  const f = familyOf(a.family);
567
- const [installed, login, quota, identity] = await Promise.all([
629
+ const quota = quotas.get(a.ref);
630
+ const peers = groups.get(a.ref);
631
+ const [installed, login, identity] = await Promise.all([
568
632
  which(f.bin, env),
569
633
  loginOf(a),
570
- rt.quota(a),
571
634
  readIdentity(f, a),
572
635
  ]);
573
636
  let receive = false;
@@ -587,6 +650,7 @@ export async function createRuntime(opts = {}) {
587
650
  login,
588
651
  quota,
589
652
  ...(identity ? { identity } : {}),
653
+ ...(peers ? { sameLogin: peers } : {}),
590
654
  installed: installed !== null,
591
655
  receive,
592
656
  };
@@ -606,5 +670,7 @@ export async function createRuntime(opts = {}) {
606
670
  },
607
671
  readContext,
608
672
  };
673
+ if (opts.desktop)
674
+ bindDesktopDeps(rt, opts.desktop);
609
675
  return rt;
610
676
  }
@@ -214,6 +214,11 @@ export type QuotaResult = {
214
214
  observedAt: string;
215
215
  source: string;
216
216
  plan?: string;
217
+ /**
218
+ * Set by sideby, never by a Family's reader: the Account this reading was taken from, when it is another
219
+ * Account signed in as the same login (spec §3.21). `observedAt` is that Account's time.
220
+ */
221
+ from?: string;
217
222
  } | {
218
223
  status: 'unavailable';
219
224
  reason: QuotaUnavailableReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
5
5
  "license": "MIT",
6
6
  "author": "Adonis0123 (https://github.com/Adonis0123)",
@@ -77,5 +77,5 @@
77
77
  },
78
78
  "packageManager": "pnpm@10.15.0",
79
79
  "readmeFilename": "README.md",
80
- "readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Why not just an alias?\n\n`CLAUDE_CONFIG_DIR` plus a shell alias already starts a second account. It does not keep skills and settings in one place, and it does not show quota for every account. [Does this break a provider's terms?](docs/guide/faq.md#terms)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `CLAUDE_CONFIG_DIR` and an alias | Yes. Each terminal exports the variable. | Each directory is its own copy. They drift unless you symlink by hand. | That session's status line | One alias per account |\n| [cc-switch](https://github.com/farion1231/cc-switch) | Enabling a provider writes the host's live config, so open sessions share that switch. | Plugin settings are copied across providers as a separate step. | The active provider, from its quota endpoint | Install the desktop app |\n| sideby | Yes. `sideby run` selects the directory for that process and leaves your shell alone. | Linked to the main account. `sideby doctor --fix` repairs copies. Sign-in and sessions stay separate. | Every account, from local files. Claude needs `quota setup`. | `npm i -g sideby`, then `sideby new` and sign in |\n\ncc-switch's docs describe that switch: it writes the live settings file ([README](https://github.com/farion1231/cc-switch#readme)). [#1105](https://github.com/farion1231/cc-switch/issues/1105) and [#2908](https://github.com/farion1231/cc-switch/issues/2908) ask for several sessions at once. [#1106](https://github.com/farion1231/cc-switch/issues/1106) is closed; the maintainer says Claude Code's \"open terminal\" can start one provider without changing the global config.\n\n## Quick start\n\nRequires Node 22.18+ and at least one Host CLI (Claude Code, Codex, Grok Build or pi) installed. AI agents: follow the setup steps in [`llms.txt`](llms.txt) (\"Set up sideby for a user\") instead.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\n<img alt=\"Two terminals: the personal account and the work account running at the same time, both linked to the main account's skills and settings, then the local quota panel, and at 95% Auto Handoff moving work's session to the next account\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif\" width=\"1280\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <img alt=\"sideby panel: every account's 5h and 7d quota, 7-day token usage and health in one page\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\" width=\"1280\">\n</picture>\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| Find the cheapest auto-compact size from your own sessions, and use it in one click | `sideby tune` (or the panel's Context tuning tab) | [Context tuning](docs/guide/usage.md#context-tuning) |\n| See each account's model and effort, and set them per account | `sideby model <account>` (or click the model in the panel) | [Model and effort](docs/guide/usage.md#model-and-effort) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| Let a running task move to the next account by itself when quota runs low (off by default; see the risk) | `sideby handoff enable` (or the panel) | [Auto Handoff](docs/guide/usage.md#auto-handoff) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| Reopen a session from Orca and the like in the account that holds it | `\"resumeRouting\": true` in config | [Resuming from other tools](docs/guide/configuration.md#resuming-from-other-tools) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Desktop app](docs/guide/panel.md#desktop-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby calls no remote service.\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, desktop app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
80
+ "readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Why not just an alias?\n\n`CLAUDE_CONFIG_DIR` plus a shell alias already starts a second account. It does not keep skills and settings in one place, and it does not show quota for every account. [Does this break a provider's terms?](docs/guide/faq.md#terms)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `CLAUDE_CONFIG_DIR` and an alias | Yes. Each terminal exports the variable. | Each directory is its own copy. They drift unless you symlink by hand. | That session's status line | One alias per account |\n| [cc-switch](https://github.com/farion1231/cc-switch) | Enabling a provider writes the host's live config, so open sessions share that switch. | Plugin settings are copied across providers as a separate step. | The active provider, from its quota endpoint | Install the desktop app |\n| sideby | Yes. `sideby run` selects the directory for that process and leaves your shell alone. | Linked to the main account. `sideby doctor --fix` repairs copies. Sign-in and sessions stay separate. | Every account, from local files. Claude needs `quota setup`. | `npm i -g sideby`, then `sideby new` and sign in |\n\ncc-switch's docs describe that switch: it writes the live settings file ([README](https://github.com/farion1231/cc-switch#readme)). [#1105](https://github.com/farion1231/cc-switch/issues/1105) and [#2908](https://github.com/farion1231/cc-switch/issues/2908) ask for several sessions at once. [#1106](https://github.com/farion1231/cc-switch/issues/1106) is closed; the maintainer says Claude Code's \"open terminal\" can start one provider without changing the global config.\n\n## Quick start\n\nRequires Node 22.18+ and at least one Host CLI (Claude Code, Codex, Grok Build or pi) installed. AI agents: follow the setup steps in [`llms.txt`](llms.txt) (\"Set up sideby for a user\") instead.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\n<img alt=\"Two terminals: the personal account and the work account running at the same time, both linked to the main account's skills and settings, then the local quota panel, and at 95% Auto Handoff moving work's session to the next account\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif\" width=\"1280\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <img alt=\"sideby panel: every account's 5h and 7d quota, 7-day token usage and health in one page\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\" width=\"1280\">\n</picture>\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| Find the cheapest auto-compact size from your own sessions, and use it in one click | `sideby tune` (or the panel's Context tuning tab) | [Context tuning](docs/guide/usage.md#context-tuning) |\n| See each account's model and effort, and set them per account | `sideby model <account>` (or click the model in the panel) | [Model and effort](docs/guide/usage.md#model-and-effort) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| Let a running task move to the next account by itself when quota runs low (off by default; see the risk) | `sideby handoff enable` (or the panel) | [Auto Handoff](docs/guide/usage.md#auto-handoff) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| Reopen a session from Orca and the like in the account that holds it | `\"resumeRouting\": true` in config | [Resuming from other tools](docs/guide/configuration.md#resuming-from-other-tools) |\n| Open Claude Desktop or the Codex app as one account, several side by side (macOS) | `sideby desktop claude:work` (or the panel) | [Desktop apps](docs/guide/usage.md#desktop-apps) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby calls no remote service.\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, panel app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
81
81
  }
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/desktop.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "ref",
8
+ "family",
9
+ "app",
10
+ "appPath",
11
+ "action",
12
+ "createdAppData",
13
+ "linked",
14
+ "notices"
15
+ ],
16
+ "properties": {
17
+ "schemaVersion": {
18
+ "type": "number",
19
+ "const": 1
20
+ },
21
+ "ref": {
22
+ "type": "string"
23
+ },
24
+ "family": {
25
+ "type": "string"
26
+ },
27
+ "app": {
28
+ "type": "string",
29
+ "description": "The desktop app, for example `Claude Desktop`."
30
+ },
31
+ "appPath": {
32
+ "type": "string"
33
+ },
34
+ "action": {
35
+ "anyOf": [
36
+ {
37
+ "type": "string",
38
+ "const": "started"
39
+ },
40
+ {
41
+ "type": "string",
42
+ "const": "focused"
43
+ },
44
+ {
45
+ "type": "string",
46
+ "const": "already-running"
47
+ }
48
+ ],
49
+ "description": "`started`: a new window for this account; `focused`: its running window was brought to the front; `already-running`: it runs but could not be brought to the front."
50
+ },
51
+ "appData": {
52
+ "type": "string",
53
+ "description": "This account's app data folder; absent for the main account, which uses the app's default."
54
+ },
55
+ "createdAppData": {
56
+ "type": "boolean"
57
+ },
58
+ "linked": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "string"
62
+ },
63
+ "description": "Files linked into a new app data folder from the main account's (claude_desktop_config.json)."
64
+ },
65
+ "command": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ },
70
+ "description": "The `open` command sideby ran."
71
+ },
72
+ "notices": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string"
76
+ }
77
+ }
78
+ },
79
+ "title": "sideby desktop --json"
80
+ }
package/schemas/list.json CHANGED
@@ -147,11 +147,38 @@
147
147
  },
148
148
  "description": "Who the account is signed in as, from the host's login file: identity fields only, never a token (ADR-0003)."
149
149
  },
150
+ "sameLogin": {
151
+ "type": "array",
152
+ "items": {
153
+ "type": "string"
154
+ },
155
+ "description": "The other accounts signed in as the same login (same family, email and organization), which share one quota (spec §3.21). Absent when there are none."
156
+ },
150
157
  "problems": {
151
158
  "type": "array",
152
159
  "items": {
153
160
  "type": "string"
154
161
  }
162
+ },
163
+ "desktopApp": {
164
+ "type": "object",
165
+ "required": [
166
+ "app",
167
+ "installed",
168
+ "running"
169
+ ],
170
+ "properties": {
171
+ "app": {
172
+ "type": "string"
173
+ },
174
+ "installed": {
175
+ "type": "boolean"
176
+ },
177
+ "running": {
178
+ "type": "boolean"
179
+ }
180
+ },
181
+ "description": "The family's desktop app for this account (macOS, subscription accounts of claude and codex): whether it is installed and whether this account's own window runs now. Open it with `sideby desktop <account>`."
155
182
  }
156
183
  }
157
184
  }
package/schemas/next.json CHANGED
@@ -17,6 +17,10 @@
17
17
  "family": {
18
18
  "type": "string"
19
19
  },
20
+ "from": {
21
+ "type": "string",
22
+ "description": "The account `sideby next` runs in (SIDEBY_ACCOUNT, set by sideby for the sessions it starts), when it belongs to this family."
23
+ },
20
24
  "pick": {
21
25
  "anyOf": [
22
26
  {
@@ -97,6 +101,14 @@
97
101
  "observedAt": {
98
102
  "type": "string",
99
103
  "description": "When the account's quota was recorded (its last session)."
104
+ },
105
+ "quotaFrom": {
106
+ "type": "string",
107
+ "description": "The account the quota was read from, when it is another account of the same login."
108
+ },
109
+ "sameLoginAs": {
110
+ "type": "string",
111
+ "description": "The current account (`from`), when this one is signed in as the same login: it shares that quota and is never picked."
100
112
  }
101
113
  }
102
114
  },
@@ -86,6 +86,10 @@
86
86
  },
87
87
  "plan": {
88
88
  "type": "string"
89
+ },
90
+ "from": {
91
+ "type": "string",
92
+ "description": "The account this reading was taken from, when it is another account of the same login (spec §3.21); observedAt is its time."
89
93
  }
90
94
  }
91
95
  },
@@ -2,7 +2,7 @@
2
2
  name: sideby
3
3
  description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
4
4
  metadata:
5
- version: "0.6.0"
5
+ version: "0.8.0"
6
6
  requires:
7
7
  bins: [sideby, node]
8
8
  cliHelp: sideby --help
@@ -58,7 +58,9 @@ Read only the reference the task needs.
58
58
 
59
59
  When a terminal manager such as Orca reopens a pane with a bare `claude --resume <id>` (or `codex resume <id>`, `grok --resume <id>`) and the Host says `No conversation found`, the session lives in another account. Suggest config `"resumeRouting": true` followed by `sideby shell-init --write` (or a new shell with `eval`): `shell-init` then defines Host-named functions that call `sideby resume <family>`, which starts the account holding that session. For a one-off, find it with `ls -d ~/.claude*/projects/*/<id>.jsonl` and run `sideby run <ref> -- --resume <id>`. When no account has that file, the session was opened but never got a message, so there is nothing to resume: `ls -d ~/.claude*/session-env/<id>` names the account that started it, and with routing on `sideby resume` starts a new session there by itself.
60
60
 
61
- For someone who would rather double-click than use a terminal, `sideby app install` adds a desktop app (macOS `~/Applications/sideby.app`, Linux app menu) that runs `sideby ui --background`; `sideby ui --stop` stops it and `sideby app uninstall` removes the app. Ask before `app install`: it writes outside sideby's own directories.
61
+ To open Claude Desktop or the Codex app as one account (macOS, subscription accounts), run `sideby desktop <ref> --json` yourself: it opens a new window for that account, or brings its open window to the front, and never closes anything. Relay its `notices`: the first Claude Desktop sign-in should happen while other Claude Desktop windows are closed, and a sign-in screen that suggests the main account ("Welcome back" in the Codex app) must not be used for another account.
62
+
63
+ For someone who would rather double-click than use a terminal, `sideby app install` adds a panel app (macOS `~/Applications/sideby.app`, Linux app menu) that runs `sideby ui --background`; `sideby ui --stop` stops it and `sideby app uninstall` removes the app. Ask before `app install`: it writes outside sideby's own directories.
62
64
 
63
65
  ## Keeping this skill current
64
66
 
@@ -15,7 +15,7 @@ Use this when the user says an account hit its 5-hour or weekly limit ("usage li
15
15
  | `full` | A live window is at 85% or more; `resetsAt` is when it is usable again | No |
16
16
  | `logged-out` | Needs `sideby login <ref>` | No |
17
17
 
18
- Each entry's `observedAt` says when its quota was recorded. If `hasQuota` is `false`, no account has quota data: the pick is only a guess, so say so and suggest turning quota on (see the routing table in SKILL.md) or letting the user choose.
18
+ Each entry's `observedAt` says when its quota was recorded (`quotaFrom` when it was read from another account of the same login). An entry with `sameLoginAs` is signed in as the same login as the account you run in: it shares that quota and is never picked. If `hasQuota` is `false`, no account has quota data: the pick is only a guess, so say so and suggest turning quota on (see the routing table in SKILL.md) or letting the user choose.
19
19
  3. Give the user the command to run in their own terminal: `sideby next <family>` (starts the pick) or `sideby run <pick>`. Host arguments go after `--`. Do not run it yourself.
20
20
  4. Offer a handoff note: the new session starts empty, because each account keeps its own sessions. Write a short note the user can paste into it: the goal, what is done, what is in progress, the files involved, and the next step.
21
21
 
@@ -11,4 +11,6 @@ Accounts with their own (not linked) `settings.json` show quota `not-enabled` un
11
11
 
12
12
  After setup, each account shows quota once it has had one session with a reply. Codex quota needs no setup: sideby reads it from Codex's own session files.
13
13
 
14
+ Two Claude accounts signed in as the same login (same email and organization; `sameLogin` in `sideby list --json`) share one quota: both show the freshest reading, and `quota.from` names the account it was read from. Explain that to the user rather than calling the numbers wrong; usage stays per account, and sideby never merges or deletes either directory.
15
+
14
16
  Usage (7-day tokens) is read by sideby itself from Claude Code and Codex local records; no ccusage is needed. Grok and pi have no quota or usage source.