sideby 0.16.0 → 0.17.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 (77) hide show
  1. package/README.md +5 -3
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +16 -0
  4. package/dist/src/cli/commands.js +460 -5
  5. package/dist/src/cli/json-schemas.js +115 -0
  6. package/dist/src/cli/main.js +93 -3
  7. package/dist/src/core/ask.d.ts +150 -0
  8. package/dist/src/core/ask.js +365 -0
  9. package/dist/src/core/attachments.d.ts +50 -0
  10. package/dist/src/core/attachments.js +442 -0
  11. package/dist/src/core/autostart.d.ts +88 -0
  12. package/dist/src/core/autostart.js +256 -0
  13. package/dist/src/core/config.d.ts +97 -0
  14. package/dist/src/core/config.js +278 -10
  15. package/dist/src/core/launch.d.ts +15 -4
  16. package/dist/src/core/launch.js +27 -23
  17. package/dist/src/core/local-build.d.ts +17 -0
  18. package/dist/src/core/local-build.js +39 -3
  19. package/dist/src/core/probe.d.ts +78 -0
  20. package/dist/src/core/probe.js +127 -0
  21. package/dist/src/core/same-login.d.ts +9 -4
  22. package/dist/src/core/same-login.js +9 -4
  23. package/dist/src/core/secret-file.d.ts +26 -0
  24. package/dist/src/core/secret-file.js +126 -7
  25. package/dist/src/core/terminal.d.ts +93 -0
  26. package/dist/src/core/terminal.js +371 -0
  27. package/dist/src/core/update.d.ts +17 -0
  28. package/dist/src/core/update.js +63 -1
  29. package/dist/src/families/claude/index.js +3 -0
  30. package/dist/src/families/claude/quota-cache.d.ts +24 -7
  31. package/dist/src/families/claude/quota-cache.js +72 -21
  32. package/dist/src/families/codex/index.js +7 -0
  33. package/dist/src/families/codex/login-key.d.ts +8 -0
  34. package/dist/src/families/codex/login-key.js +33 -0
  35. package/dist/src/families/grok/index.js +3 -0
  36. package/dist/src/families/login-keys.d.ts +6 -0
  37. package/dist/src/families/login-keys.js +6 -0
  38. package/dist/src/families/pi/index.js +4 -0
  39. package/dist/src/panel/background.js +3 -2
  40. package/dist/src/panel/handler.d.ts +29 -2
  41. package/dist/src/panel/handler.js +396 -9
  42. package/dist/src/panel/i18n.d.ts +175 -2
  43. package/dist/src/panel/i18n.js +381 -35
  44. package/dist/src/panel/page-logic.d.ts +214 -0
  45. package/dist/src/panel/page-logic.js +394 -0
  46. package/dist/src/panel/page-script.js +984 -134
  47. package/dist/src/panel/page-styles.js +69 -19
  48. package/dist/src/panel/page.d.ts +5 -0
  49. package/dist/src/panel/page.js +2 -0
  50. package/dist/src/panel/server.d.ts +16 -2
  51. package/dist/src/panel/server.js +41 -13
  52. package/dist/src/plugins/check.d.ts +47 -0
  53. package/dist/src/plugins/check.js +544 -0
  54. package/dist/src/plugins/scaffold.d.ts +21 -0
  55. package/dist/src/plugins/scaffold.js +123 -0
  56. package/dist/src/quota/statusline-tap.js +1 -1
  57. package/dist/src/runtime.d.ts +10 -0
  58. package/dist/src/runtime.js +47 -16
  59. package/dist/src/types.d.ts +19 -0
  60. package/package.json +2 -2
  61. package/schemas/ask.json +180 -0
  62. package/schemas/autostart.json +165 -0
  63. package/schemas/config-get.json +132 -0
  64. package/schemas/config-set.json +107 -0
  65. package/schemas/config.json +73 -6
  66. package/schemas/key.json +93 -0
  67. package/schemas/local.json +4 -0
  68. package/schemas/plugins-check.json +113 -0
  69. package/schemas/plugins-new.json +87 -0
  70. package/schemas/terminal.json +152 -0
  71. package/schemas/try.json +109 -0
  72. package/schemas/update.json +4 -0
  73. package/skills/sideby/SKILL.md +12 -6
  74. package/skills/sideby/references/accounts.md +1 -1
  75. package/skills/sideby/references/ai-tasks.md +116 -0
  76. package/skills/sideby/references/panel.md +14 -0
  77. package/skills/sideby/references/update.md +1 -1
@@ -0,0 +1,123 @@
1
+ // `sideby plugins new` (spec §3.28 "加一个新宿主"): writes a Family Plugin skeleton the loader accepts as it is, with a
2
+ // TODO on every fact the author has to look up in the Host's own docs.
3
+ import { chmod, mkdir, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { UserError } from '../core/errors.js';
6
+ /** Same rule the loader applies to a Family id: it becomes part of account refs and shell function names. */
7
+ export const FAMILY_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
8
+ /** Marks each line of the skeleton the author still has to fill in; `checkPlugin` reports any that are left. */
9
+ export const TODO_MARK = 'TODO(sideby)';
10
+ /** Why `id` cannot name a Family Plugin, or null when it can. */
11
+ export function pluginIdProblem(id) {
12
+ return FAMILY_ID_RE.test(id)
13
+ ? null
14
+ : `"${id}" cannot name a plugin: use lowercase letters, digits or "-", starting with a letter, at most 32 characters (for example "mytool")`;
15
+ }
16
+ /**
17
+ * Creates `<dir>/<id>/` with `plugin.json`, `index.ts` (an `api.family(def)` skeleton that only imports types from
18
+ * sideby) and `index.test.ts` (a starter on `sideby/testing`). The directory is mode 700 and the files 600, owned by
19
+ * the current user, so the loader's trust checks pass. Refuses a bad id or an existing directory before writing.
20
+ */
21
+ export async function scaffoldPlugin(opts) {
22
+ const problem = pluginIdProblem(opts.id);
23
+ if (problem)
24
+ throw new UserError(problem, { code: 'plugin-id-invalid' });
25
+ await mkdir(opts.dir, { recursive: true, mode: 0o700 });
26
+ const dir = join(opts.dir, opts.id);
27
+ try {
28
+ await mkdir(dir, { mode: 0o700 });
29
+ }
30
+ catch (err) {
31
+ if (err.code === 'EEXIST')
32
+ throw new UserError(`${dir} already exists; pick another id, or check the plugin there with \`sideby plugins check ${opts.id}\``, { code: 'plugin-exists' });
33
+ throw err;
34
+ }
35
+ // mkdir's mode is narrowed by the umask; set it exactly.
36
+ await chmod(dir, 0o700);
37
+ const files = [];
38
+ for (const [name, text] of [
39
+ ['plugin.json', manifestText(opts.id)],
40
+ ['index.ts', indexText(opts.id)],
41
+ ['index.test.ts', testText(opts.id)],
42
+ ]) {
43
+ const p = join(dir, name);
44
+ await writeFile(p, text, { mode: 0o600, flag: 'wx' });
45
+ await chmod(p, 0o600);
46
+ files.push(p);
47
+ }
48
+ return { dir, files };
49
+ }
50
+ function manifestText(id) {
51
+ return `${JSON.stringify({ name: id, version: '0.1.0', description: `${id} family for sideby`, main: 'index.ts' }, null, 2)}\n`;
52
+ }
53
+ function envPrefix(id) {
54
+ return id.toUpperCase().replace(/-/g, '_');
55
+ }
56
+ function indexText(id) {
57
+ const v = envPrefix(id);
58
+ return `// Family Plugin for ${id}. This file runs with your full permissions, like any local code.
59
+ // Fill in every ${TODO_MARK} with a fact from the Host's own docs or \`--help\`, and write down where it came from as
60
+ // "<tool> <version>, <date>", for example "${id} 1.2.0 --help, 2026-10-17". Then run \`sideby plugins check ${id}\`.
61
+ // Only \`import type\` from sideby: the plugin gets everything else through \`api\`.
62
+ import type { FamilyDef, Plugin } from 'sideby'
63
+
64
+ const family: FamilyDef = {
65
+ id: '${id}',
66
+ // ${TODO_MARK}: the Host's name as its docs write it.
67
+ title: '${id}',
68
+ // ${TODO_MARK}: the executable the Host installs on PATH (its docs or install script).
69
+ bin: '${id}',
70
+ // ${TODO_MARK}: the Host's official install page.
71
+ installUrl: 'https://example.com/${id}',
72
+ // ${TODO_MARK}: the environment variable that moves the Host's whole config directory (its docs or \`--help\`).
73
+ // sideby sets it for every Account but the Main Account.
74
+ selectVar: '${v}_HOME',
75
+ // ${TODO_MARK}: where the Host keeps its config, relative to HOME; the other Accounts put <name> in the first segment.
76
+ layout: { main: '.${id}', account: '.${id}-<name>' },
77
+ // ${TODO_MARK}: variables that would send every Account to the same login or endpoint (API keys, base URLs);
78
+ // sideby clears them before each launch.
79
+ hijackVars: ['${v}_API_KEY'],
80
+ // ${TODO_MARK}: variables an API Account sets in its proxy.env.
81
+ apiVars: ['${v}_API_KEY'],
82
+ // ${TODO_MARK}: each file or folder the Accounts share, with its Share Mode (see \`sideby families claude\` for examples).
83
+ // Paths are relative to the Account directory. Mark a file that holds a login \`credential: true\`; sideby never reads it.
84
+ sharedItems: [{ path: 'settings.json', mode: 'link-or-copy' }],
85
+ // ${TODO_MARK}: the Host's sign-in arguments, or null when it signs in inside the app.
86
+ login: { args: ['login'], hint: 'run ${id} login' },
87
+ // Optional: loginState(account) tells signed in from signed out by whether a file exists, never by reading it.
88
+ // Optional: logo, one 24x24 SVG path with its source and license in a comment.
89
+ }
90
+
91
+ const plugin: Plugin = {
92
+ name: '${id}',
93
+ register(api) {
94
+ api.family(family)
95
+ },
96
+ }
97
+
98
+ export default plugin
99
+ `;
100
+ }
101
+ function testText(id) {
102
+ return `// Starter test for the ${id} Family Plugin. Run it with \`node --test index.test.ts\` where sideby is installed
103
+ // (\`npm i -D sideby\`). It only uses a throwaway HOME from sideby/testing, never your real one.
104
+ import assert from 'node:assert/strict'
105
+ import { it } from 'node:test'
106
+ import type { FamilyDef, PluginApi } from 'sideby'
107
+ import { withFakeHome } from 'sideby/testing'
108
+ import plugin from './index.ts'
109
+
110
+ it('registers the ${id} family', async () => {
111
+ await withFakeHome(async (h) => {
112
+ const families: FamilyDef[] = []
113
+ await plugin.register({ family: (def: FamilyDef) => families.push(def) } as unknown as PluginApi)
114
+ assert.equal(families.length, 1)
115
+ const def = families[0]!
116
+ assert.equal(def.id, '${id}')
117
+ assert.ok(def.selectVar)
118
+ // ${TODO_MARK}: write the files your Host keeps in a signed-in Account and assert what loginState reads.
119
+ await h.mkdir(def.layout.main)
120
+ })
121
+ })
122
+ `;
123
+ }
@@ -91,7 +91,7 @@ export async function runStatuslineTap(argv, env, stdin, stdout, stderr = proces
91
91
  // Several sessions of one Account share this cache; an idle one must not put back older numbers.
92
92
  const stateDir = cacheStateDir(env);
93
93
  const prev = await readQuotaCache(stateDir, name).catch(() => null);
94
- const merged = prev?.status === 'ok' ? mergeQuotaCache(prev.windows, cache) : cache;
94
+ const merged = mergeQuotaCache(prev?.status === 'ok' ? prev : null, cache);
95
95
  if (!merged) {
96
96
  // Older numbers: write nothing, and show what the cache already knows.
97
97
  if (prev?.status === 'ok')
@@ -203,6 +203,16 @@ export interface Runtime {
203
203
  /** Rewrites the configured `shellInitFile` of each shell (all configured ones by default). */
204
204
  writeShellInitFiles(shells?: readonly Shell[]): Promise<ShellInitWrite[]>;
205
205
  prepareLaunch(ref: string, userArgs: readonly string[], command: 'run' | 'login'): Promise<PreparedLaunch>;
206
+ /**
207
+ * The Host and arguments `prepareLaunch` starts from, without reading the Secret File or running `launch.before`
208
+ * hooks: what a preview may show before the user agrees to start anything.
209
+ */
210
+ launchCommand(ref: string, userArgs: readonly string[], command: 'run' | 'login'): Promise<{
211
+ account: Account;
212
+ family: FamilyDef;
213
+ bin: string;
214
+ args: string[];
215
+ }>;
206
216
  /**
207
217
  * Starts the non-main Account that holds the session the Host arguments resume, or else the Host unchanged
208
218
  * (spec §3.15). Throws UnknownFamilyError, or a UserError when the Family cannot route sessions.
@@ -10,7 +10,7 @@ import { handoffFindings } from './core/handoff-doctor.js';
10
10
  import { selectNext } from './core/handoff-select.js';
11
11
  import { identityOf } from './core/identity.js';
12
12
  import { mergeDoctorHistory, readDoctorHistory, writeDoctorHistory, } from './core/last-doctor.js';
13
- import { accountEnv, brokenOnPath, prepareLaunch, which, } from './core/launch.js';
13
+ import { accountEnv, brokenOnPath, launchCommand, prepareLaunch, which, } from './core/launch.js';
14
14
  import { memoStore } from './core/memo-store.js';
15
15
  import { resolvePaths } from './core/paths.js';
16
16
  import { fetchedQuota, quotaCheckOff, shownQuota } from './core/quota-fetch.js';
@@ -23,6 +23,7 @@ import { groupSuggestion, MAX_COMPACT_WINDOW, MIN_COMPACT_WINDOW, } from './core
23
23
  import { applyTune, planTuneApply, readTuneSettings, settingsFileOf, tuneUndoInfo, undoTune, } from './core/tuning-apply.js';
24
24
  import { packageVersion } from './core/version.js';
25
25
  import { BUILTIN_PLUGINS } from './families/index.js';
26
+ import { LOGIN_KEY_READERS } from './families/login-keys.js';
26
27
  import { HookBus } from './plugins/bus.js';
27
28
  import { loadPlugins } from './plugins/loader.js';
28
29
  export class UnknownFamilyError extends UserError {
@@ -42,6 +43,21 @@ async function readIdentity(f, account) {
42
43
  return undefined;
43
44
  }
44
45
  }
46
+ /**
47
+ * An Account's Login Key (ADR-0028), only for grouping a Same Login: it stays inside `groupsOf` and never reaches an
48
+ * output, an error, a log or a cache. Undefined for a Family without a reader, or on any problem.
49
+ */
50
+ async function readLoginKey(account) {
51
+ const read = LOGIN_KEY_READERS.get(account.family);
52
+ if (!read || account.kind === 'api')
53
+ return undefined;
54
+ try {
55
+ return await read(account);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
45
61
  export async function createRuntime(opts = {}) {
46
62
  const env = opts.env ?? process.env;
47
63
  const paths = resolvePaths(env);
@@ -92,8 +108,17 @@ export async function createRuntime(opts = {}) {
92
108
  const members = await Promise.all(list
93
109
  .filter((a) => a.kind === 'subscription' && SAME_LOGIN_FAMILIES.has(a.family) && families.has(a.family))
94
110
  .map(async (a) => {
95
- const identity = await readIdentity(familyOf(a.family), a);
96
- return { ref: a.ref, family: a.family, kind: a.kind, ...(identity ? { identity } : {}) };
111
+ const [identity, loginKey] = await Promise.all([
112
+ readIdentity(familyOf(a.family), a),
113
+ readLoginKey(a),
114
+ ]);
115
+ return {
116
+ ref: a.ref,
117
+ family: a.family,
118
+ kind: a.kind,
119
+ ...(identity ? { identity } : {}),
120
+ ...(loginKey ? { loginKey } : {}),
121
+ };
97
122
  }));
98
123
  return sameLoginGroups(members);
99
124
  };
@@ -198,6 +223,19 @@ export async function createRuntime(opts = {}) {
198
223
  const ownAlias = (name) => config.aliases && Object.hasOwn(config.aliases, name) ? config.aliases[name] : undefined;
199
224
  /** The config value of `ref` when it names an alias (no colon), as `resolveRef` reads it. */
200
225
  const aliasValue = (ref) => (ref.includes(':') ? undefined : ownAlias(ref));
226
+ /** What `prepareLaunch` and `launchCommand` both start from, so a preview cannot differ from the Launch. */
227
+ const launchInputs = (ref, all, userArgs, command) => {
228
+ const account = resolveRef(ref, all, aliasRefs(config.aliases));
229
+ return {
230
+ title: config.accountTitle === true,
231
+ account,
232
+ family: familyOf(account.family),
233
+ accountArgs: config.accounts?.[account.ref]?.args ?? [],
234
+ // An alias's arguments come before the user's; the Family sees both as the user's choice (ADR-0005).
235
+ userArgs: command === 'run' ? [...aliasArgs(aliasValue(ref)), ...userArgs] : userArgs,
236
+ command,
237
+ };
238
+ };
201
239
  const aliasEdit = async (alias, status, value) => {
202
240
  const args = aliasArgs(value);
203
241
  const files = await rt.writeShellInitFiles();
@@ -471,19 +509,12 @@ export async function createRuntime(opts = {}) {
471
509
  },
472
510
  async prepareLaunch(ref, userArgs, command) {
473
511
  const all = await accounts();
474
- const account = resolveRef(ref, all, aliasRefs(config.aliases));
475
- return prepareLaunch({
476
- label: launchLabel(ref, account, all),
477
- title: config.accountTitle === true,
478
- account,
479
- family: familyOf(account.family),
480
- baseEnv: env,
481
- accountArgs: config.accounts?.[account.ref]?.args ?? [],
482
- // An alias's arguments come before the user's; the Family sees both as the user's choice (ADR-0005).
483
- userArgs: command === 'run' ? [...aliasArgs(aliasValue(ref)), ...userArgs] : userArgs,
484
- command,
485
- bus,
486
- });
512
+ const inputs = launchInputs(ref, all, userArgs, command);
513
+ return prepareLaunch({ ...inputs, label: launchLabel(ref, inputs.account, all), baseEnv: env, bus });
514
+ },
515
+ async launchCommand(ref, userArgs, command) {
516
+ const inputs = launchInputs(ref, await accounts(), userArgs, command);
517
+ return { account: inputs.account, family: inputs.family, ...launchCommand(inputs) };
487
518
  },
488
519
  async resumeLaunch(familyId, userArgs) {
489
520
  const family = familyOf(familyId);
@@ -61,6 +61,25 @@ export interface FamilyDef {
61
61
  hijackVars: string[];
62
62
  /** Variables written, commented out, into a new API Account's Secret File. */
63
63
  apiVars: string[];
64
+ /**
65
+ * Secret File variable `sideby key` writes when the user does not name one (spec §3.28).
66
+ * The API key, not a base URL or a model. Public contract, additive only.
67
+ */
68
+ secretVar?: string;
69
+ /**
70
+ * Arguments for one short non-interactive check of an Account (`sideby try`, spec §3.28).
71
+ * sideby runs `bin` with these arguments and no others. Public contract, additive only:
72
+ * leaving it out means this tool cannot be tried.
73
+ */
74
+ probe?: {
75
+ args: string[];
76
+ };
77
+ /**
78
+ * The Host option that attaches an image to the first prompt, such as Codex's `--image` (spec §3.28 "附件").
79
+ * An AI Task with attached images adds `<imageArg>=<path>` once per image before the prompt; Families without
80
+ * it read the images from the paths the prompt lists. Public contract, additive only.
81
+ */
82
+ imageArg?: string;
64
83
  sharedItems: SharedItemDef[];
65
84
  /** Arguments for the Host's sign-in command, or null when sign-in happens inside the Host. */
66
85
  login: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.16.0",
3
+ "version": "0.17.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)",
@@ -80,5 +80,5 @@
80
80
  },
81
81
  "packageManager": "pnpm@10.15.0",
82
82
  "readmeFilename": "README.md",
83
- "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## 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 quota setup claude --yes # Claude quota in the panel; undo: sideby quota teardown claude\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`sideby update` installs a newer sideby when there is one; sideby says so after a command and in the panel. `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\nThe 25-second story: one account buried in tasks, then accounts side by side sharing one setup, and a task moving on when one runs out.\n\nhttps://github.com/user-attachments/assets/3a470c03-9009-4f1d-b7f6-8b4625d74958\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## 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## 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| Choose whether Claude sessions connect to claude.ai by themselves (Remote Control), for every account at once | `sideby remote-control off` or `on` (or the panel's Settings) | [Remote Control](docs/guide/usage.md#remote-control) |\n| Pin the accounts you use most and put tools and accounts in your own order | `sideby pin claude:work`, `sideby order` (or the panel's Settings) | [Your own order](docs/guide/usage.md#your-own-order) |\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| Keep several Grok Bot windows open, each signed in to its own account (macOS) | `sideby desktop grok-bot:002` (or the panel) | [Grok Bot windows](docs/guide/usage.md#grok-bot-windows) |\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` | weekly, asked from the `grok` CLI | 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 about your accounts leaves your machine: the only request sideby makes on its own is a daily check of the npm registry for its newest version (off with `NO_UPDATE_NOTIFIER=1`; see [Updates](docs/guide/configuration.md#updates)).\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"
83
+ "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## 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 quota setup claude --yes # Claude quota in the panel; undo: sideby quota teardown claude\nsideby ui --background # every account's quota at http://127.0.0.1:17420, still there after you close the terminal\n```\n\n`sideby app install` adds a Dock icon that opens the panel, and `sideby autostart on` has it ready each time you log in. `sideby update` installs a newer sideby when there is one; sideby says so after a command and in the panel. `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\nThe 25-second story: one account buried in tasks, then accounts side by side sharing one setup, and a task moving on when one runs out.\n\nhttps://github.com/user-attachments/assets/fd6138ae-499d-4055-b94f-25ef9ce74035\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## 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## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Hand a task to your own AI from the panel: set up an account, fix a problem, support a new tool | **Let AI do it** in the panel, or `sideby ask` | [Let AI do it](docs/guide/panel.md#let-ai-do-it) |\n| Have the panel ready in the background whenever you log in | **Settings › Startup** in the panel, or `sideby autostart on` | [Start at login](docs/guide/usage.md#start-at-login) |\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| Choose whether Claude sessions connect to claude.ai by themselves (Remote Control), for every account at once | `sideby remote-control off` or `on` (or the panel's Settings) | [Remote Control](docs/guide/usage.md#remote-control) |\n| Pin the accounts you use most and put tools and accounts in your own order | `sideby pin claude:work`, `sideby order` (or the panel's Settings) | [Your own order](docs/guide/usage.md#your-own-order) |\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| Keep several Grok Bot windows open, each signed in to its own account (macOS) | `sideby desktop grok-bot:002` (or the panel) | [Grok Bot windows](docs/guide/usage.md#grok-bot-windows) |\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` | weekly, asked from the `grok` CLI | 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 about your accounts leaves your machine: the only request sideby makes on its own is a daily check of the npm registry for its newest version (off with `NO_UPDATE_NOTIFIER=1`; see [Updates](docs/guide/configuration.md#updates)).\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"
84
84
  }
@@ -0,0 +1,180 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/ask.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "task",
8
+ "account",
9
+ "terminal",
10
+ "opened",
11
+ "command",
12
+ "removedSecrets",
13
+ "attachments"
14
+ ],
15
+ "properties": {
16
+ "schemaVersion": {
17
+ "type": "number",
18
+ "const": 1
19
+ },
20
+ "task": {
21
+ "anyOf": [
22
+ {
23
+ "type": "string"
24
+ },
25
+ {
26
+ "type": "null"
27
+ }
28
+ ]
29
+ },
30
+ "account": {
31
+ "anyOf": [
32
+ {
33
+ "type": "string"
34
+ },
35
+ {
36
+ "type": "null"
37
+ }
38
+ ]
39
+ },
40
+ "terminal": {
41
+ "anyOf": [
42
+ {
43
+ "type": "string"
44
+ },
45
+ {
46
+ "type": "null"
47
+ }
48
+ ]
49
+ },
50
+ "opened": {
51
+ "type": "boolean"
52
+ },
53
+ "command": {
54
+ "anyOf": [
55
+ {
56
+ "type": "string"
57
+ },
58
+ {
59
+ "type": "null"
60
+ }
61
+ ]
62
+ },
63
+ "removedSecrets": {
64
+ "type": "boolean"
65
+ },
66
+ "reason": {
67
+ "type": "string",
68
+ "enum": [
69
+ "no-accounts",
70
+ "none-signed-in",
71
+ "all-full",
72
+ "not-installed",
73
+ "automation-denied",
74
+ "npx-cache",
75
+ "unsafe-path",
76
+ "failed",
77
+ "unsupported-platform"
78
+ ],
79
+ "description": "Why nothing opened (only when `opened` is false); `detail` says what to do next."
80
+ },
81
+ "detail": {
82
+ "type": "string",
83
+ "description": "What to do next, for a person."
84
+ },
85
+ "prompt": {
86
+ "type": "string"
87
+ },
88
+ "attachments": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "object",
92
+ "required": [
93
+ "path",
94
+ "kind",
95
+ "bytes"
96
+ ],
97
+ "properties": {
98
+ "path": {
99
+ "type": "string",
100
+ "description": "Absolute path of the copy saved for the task."
101
+ },
102
+ "kind": {
103
+ "type": "string",
104
+ "enum": [
105
+ "image",
106
+ "video"
107
+ ]
108
+ },
109
+ "bytes": {
110
+ "type": "integer",
111
+ "minimum": 0
112
+ }
113
+ }
114
+ },
115
+ "description": "Files given with `--attach`, in order; empty without any."
116
+ },
117
+ "_notice": {
118
+ "type": "object",
119
+ "properties": {
120
+ "update": {
121
+ "type": "object",
122
+ "required": [
123
+ "current",
124
+ "latest",
125
+ "command",
126
+ "message"
127
+ ],
128
+ "properties": {
129
+ "current": {
130
+ "type": "string"
131
+ },
132
+ "latest": {
133
+ "type": "string"
134
+ },
135
+ "command": {
136
+ "type": "string",
137
+ "description": "Run it after the user agrees: it shows the plan and exits 10; then --yes."
138
+ },
139
+ "message": {
140
+ "type": "string"
141
+ }
142
+ }
143
+ },
144
+ "skill": {
145
+ "type": "object",
146
+ "required": [
147
+ "installed",
148
+ "current",
149
+ "command",
150
+ "message"
151
+ ],
152
+ "properties": {
153
+ "installed": {
154
+ "anyOf": [
155
+ {
156
+ "type": "string"
157
+ },
158
+ {
159
+ "type": "null"
160
+ }
161
+ ],
162
+ "description": "The skill's version; null when it has none."
163
+ },
164
+ "current": {
165
+ "type": "string"
166
+ },
167
+ "command": {
168
+ "type": "string",
169
+ "description": "`sideby update`, which updates the skill with sideby: run it after the user agrees; it shows the plan and exits 10, then --yes."
170
+ },
171
+ "message": {
172
+ "type": "string"
173
+ }
174
+ }
175
+ }
176
+ },
177
+ "description": "Not the answer to the task: mention it briefly afterwards and offer to update."
178
+ }
179
+ }
180
+ }