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.
- package/README.md +3 -2
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +2 -0
- package/dist/src/cli/commands.js +44 -5
- package/dist/src/cli/json-schemas.js +37 -0
- package/dist/src/cli/main.js +26 -2
- package/dist/src/core/desktop-app.d.ts +96 -0
- package/dist/src/core/desktop-app.js +382 -0
- package/dist/src/core/doctor.d.ts +6 -1
- package/dist/src/core/doctor.js +19 -0
- package/dist/src/core/handoff-run.d.ts +2 -0
- package/dist/src/core/handoff-run.js +10 -4
- package/dist/src/core/handoff-select.d.ts +9 -0
- package/dist/src/core/handoff-select.js +10 -2
- package/dist/src/core/handoff-status.js +12 -0
- package/dist/src/core/handoff.d.ts +12 -0
- package/dist/src/core/handoff.js +13 -4
- package/dist/src/core/same-login.d.ts +24 -0
- package/dist/src/core/same-login.js +52 -0
- package/dist/src/core/tuning-apply.js +1 -1
- package/dist/src/families/claude/desktop.d.ts +6 -0
- package/dist/src/families/claude/desktop.js +28 -0
- package/dist/src/families/codex/desktop.d.ts +7 -0
- package/dist/src/families/codex/desktop.js +25 -0
- package/dist/src/families/desktop.d.ts +27 -0
- package/dist/src/families/desktop.js +9 -0
- package/dist/src/panel/handler.js +28 -6
- package/dist/src/panel/i18n.d.ts +11 -0
- package/dist/src/panel/i18n.js +22 -0
- package/dist/src/panel/page-logic.d.ts +6 -1
- package/dist/src/panel/page-logic.js +5 -4
- package/dist/src/panel/page-script.js +37 -3
- package/dist/src/runtime.d.ts +26 -0
- package/dist/src/runtime.js +89 -23
- package/dist/src/types.d.ts +5 -0
- package/package.json +2 -2
- package/schemas/desktop.json +80 -0
- package/schemas/list.json +27 -0
- package/schemas/next.json +12 -0
- package/schemas/quota.json +4 -0
- package/skills/sideby/SKILL.md +4 -2
- package/skills/sideby/references/handoff.md +1 -1
- package/skills/sideby/references/quota.md +2 -0
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
//
|
|
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
|
|
559
|
-
return {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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.
|
|
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
|
},
|
package/schemas/quota.json
CHANGED
package/skills/sideby/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|