sideby 0.12.0 → 0.13.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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +4 -2
  4. package/dist/src/cli/commands.js +117 -17
  5. package/dist/src/cli/json-schemas.js +52 -1
  6. package/dist/src/cli/main.js +34 -3
  7. package/dist/src/core/brand-mark.d.ts +5 -7
  8. package/dist/src/core/brand-mark.js +18 -5
  9. package/dist/src/core/browser-profiles.d.ts +71 -0
  10. package/dist/src/core/browser-profiles.js +361 -0
  11. package/dist/src/core/config.d.ts +15 -0
  12. package/dist/src/core/config.js +13 -1
  13. package/dist/src/core/create.js +2 -1
  14. package/dist/src/core/desktop-app.d.ts +3 -1
  15. package/dist/src/core/desktop-app.js +29 -8
  16. package/dist/src/core/launch.d.ts +7 -0
  17. package/dist/src/core/launch.js +22 -2
  18. package/dist/src/core/leveldb-keys.d.ts +14 -0
  19. package/dist/src/core/leveldb-keys.js +291 -0
  20. package/dist/src/core/remote-control.d.ts +17 -6
  21. package/dist/src/core/remote-control.js +40 -17
  22. package/dist/src/core/update.d.ts +10 -3
  23. package/dist/src/core/update.js +38 -3
  24. package/dist/src/core/version.d.ts +11 -0
  25. package/dist/src/core/version.js +26 -0
  26. package/dist/src/desktop/app.d.ts +2 -0
  27. package/dist/src/desktop/app.js +39 -3
  28. package/dist/src/families/browser.d.ts +20 -0
  29. package/dist/src/families/browser.js +23 -0
  30. package/dist/src/families/claude/browser.d.ts +3 -0
  31. package/dist/src/families/claude/browser.js +14 -0
  32. package/dist/src/families/claude/desktop.js +1 -0
  33. package/dist/src/families/codex/browser.d.ts +7 -0
  34. package/dist/src/families/codex/browser.js +26 -0
  35. package/dist/src/families/desktop.d.ts +5 -1
  36. package/dist/src/families/pi/index.js +2 -0
  37. package/dist/src/panel/handler.d.ts +6 -0
  38. package/dist/src/panel/handler.js +49 -6
  39. package/dist/src/panel/i18n.d.ts +75 -4
  40. package/dist/src/panel/i18n.js +162 -20
  41. package/dist/src/panel/page-logic.d.ts +23 -0
  42. package/dist/src/panel/page-logic.js +44 -0
  43. package/dist/src/panel/page-script.js +609 -180
  44. package/dist/src/panel/page-styles.js +72 -17
  45. package/dist/src/panel/page.d.ts +5 -0
  46. package/dist/src/panel/page.js +10 -9
  47. package/dist/src/runtime.d.ts +6 -1
  48. package/dist/src/runtime.js +22 -11
  49. package/dist/src/types.d.ts +2 -0
  50. package/package.json +5 -3
  51. package/schemas/browser-open.json +47 -0
  52. package/schemas/browser-switch.json +42 -0
  53. package/schemas/browser.json +99 -0
  54. package/schemas/config.json +11 -0
  55. package/schemas/families.json +6 -0
  56. package/schemas/list.json +6 -0
  57. package/schemas/remote-control.json +4 -0
  58. package/schemas/update.json +4 -0
  59. package/skills/sideby/SKILL.md +5 -3
  60. package/skills/sideby/references/setup.md +1 -1
@@ -63,6 +63,9 @@ body { margin: 0; background: var(--bg); color: var(--text); font: var(--font-si
63
63
  [hidden] { display: none !important; }
64
64
  .top-in, .wrap { max-width: var(--content-width); margin: 0 auto; }
65
65
  .top-in { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 28px var(--gutter) 0; }
66
+ /* When the name, its version pill and the buttons do not fit one row, the buttons wrap below, on the right. */
67
+ .top-in { flex-wrap: wrap; row-gap: 10px; }
68
+ .top-in > .top-actions { margin-left: auto; flex-wrap: wrap; justify-content: flex-end; }
66
69
  .wrap { padding: 20px var(--gutter) 40px; }
67
70
  .header-bar .top { background: var(--surface); border-bottom: 1px solid var(--border); }
68
71
  .header-bar .top-in { min-height: 84px; padding: 14px var(--gutter); }
@@ -82,6 +85,19 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
82
85
  .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
83
86
  .brand h1 { font-size: var(--title-size); font-weight: var(--title-weight); line-height: 1.3; letter-spacing: -0.02em; }
84
87
  .tagline { color: var(--muted); font-size: 13px; }
88
+ /* When the account data was read; the dot says whether it still refreshes on its own (green) or stopped (amber). */
89
+ #updated { display: inline-flex; align-items: center; gap: 6px; }
90
+ #updated.is-live::before, #updated.is-stale::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok-bar); }
91
+ #updated.is-stale::before { background: var(--warn-bar); }
92
+ /* The running version, a quiet pill after the name; with a newer version it takes the accent and a dot (spec §3.23). */
93
+ .title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
94
+ .ver-pill { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--muted); font: 500 11.5px/18px var(--mono); cursor: pointer; white-space: nowrap; }
95
+ .ver-pill:hover:not(:disabled), .ver-pill[aria-expanded="true"] { color: var(--on-primary); border-color: var(--primary-hover-border); background: var(--primary-hover); }
96
+ .ver-pill.has-update:hover:not(:disabled), .ver-pill.has-update[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); background: var(--active); }
97
+ .ver-pill:disabled { cursor: default; }
98
+ .ver-pill:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
99
+ .ver-pill.has-update { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
100
+ .ver-pill.has-update::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
85
101
  .logo { display: inline-flex; flex: none; width: 40px; height: 40px; border-radius: 10px; background: var(--logo-bg); box-shadow: inset 0 0 0 1px var(--logo-line); }
86
102
  /* desktop/icon.ts draws the same two half discs for the app icon. */
87
103
  .logo svg { display: block; width: 100%; height: 100%; fill: var(--logo-fg); }
@@ -132,7 +148,7 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
132
148
  .tune-acct-head { display: flex; align-items: center; gap: 8px; }
133
149
  .seg-btn[aria-pressed=true] { background: var(--active); color: var(--text); font-weight: var(--strong-weight); }
134
150
  .seg-btn:focus-visible { outline: var(--focus-outline); outline-offset: 1px; box-shadow: var(--focus-ring); }
135
- .acct-head { margin-bottom: 10px; }
151
+ .acct-head { margin-bottom: 10px; justify-content: flex-start; column-gap: 14px; }
136
152
  .acct-head h2 { display: flex; align-items: baseline; gap: 10px; }
137
153
  #acct-count { font-weight: 400; }
138
154
 
@@ -151,26 +167,38 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
151
167
  .group-meta { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
152
168
  .group-quota { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
153
169
 
154
- /* List rows: identity, one column per quota window, usage, last used, health, menu. Every column but the first has a
155
- fixed width, so the rows of all groups line up under the same column titles. --qn is the number of quota
156
- columns; the page sets it on #accounts. */
157
- #accounts { --qn: 2; --q-w: 148px; --u-w: 168px; --l-w: 82px; --h-w: 112px; --m-w: 28px; --col-gap: 20px; }
158
- .row, .col-head { display: grid; grid-template-columns: minmax(0, 1fr) calc(var(--qn) * var(--q-w) + (var(--qn) - 1) * var(--col-gap)) var(--u-w) var(--l-w) var(--h-w) var(--m-w); column-gap: var(--col-gap); align-items: center; min-width: 0; }
159
- .row { grid-template-areas: "id quota usage last health more" "notes notes notes notes notes notes"; padding: 11px 10px 11px 16px; border-top: 1px solid var(--border); transition: background-color .12s; }
170
+ /* List rows: identity, one column per quota window, last used, menu; a click opens the details (model, tokens,
171
+ Health) under them. Every column but the first has a fixed width, so the rows of all groups line up under the
172
+ same column titles. --qn is the number of quota columns; the page sets it on #accounts. */
173
+ #accounts { --qn: 2; --q-w: 176px; --l-w: 92px; --m-w: 28px; --col-gap: 28px; }
174
+ .row, .col-head { display: grid; grid-template-columns: minmax(0, 1fr) calc(var(--qn) * var(--q-w) + (var(--qn) - 1) * var(--col-gap)) var(--l-w) var(--m-w); column-gap: var(--col-gap); align-items: center; min-width: 0; }
175
+ .row { grid-template-areas: "id quota last more" "detail detail detail detail" "notes notes notes notes"; padding: 11px 10px 11px 16px; border-top: 1px solid var(--border); transition: background-color .12s; }
160
176
  .rows .row:hover { background: color-mix(in srgb, var(--hover) 55%, transparent); }
161
177
  .row:first-child { border-top: 0; }
162
178
  .row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
163
179
  .row-error .ident-name .name { color: var(--fail); }
164
- /* One account folded to its name line, opened again with the arrow at its start. */
180
+ /* A login group (spec §3.21): the head carries the email and the one quota; the directories sit indented under it. */
181
+ .login { border-top: 1px solid var(--border); }
182
+ .login:first-child { border-top: 0; }
183
+ .rows .login > .login-head { border-top: 0; padding-bottom: 4px; }
184
+ .rows .login > .login-head:hover { background: transparent; }
185
+ .login-rows > .row, .login-rows > .row:first-child { border-top: 1px dashed var(--border); padding-left: 40px; }
186
+ .login-rows > .row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
187
+ /* The arrow at a row's start opens and closes its details; so does a click anywhere else in the row. */
165
188
  .row-toggle { width: 28px; height: 28px; margin: -4px -6px -4px -8px; }
166
189
  .fold-hit { cursor: pointer; }
167
- .row > .ident.fold-hit { align-self: stretch; margin: -11px 0 -11px -16px; padding: 11px 0 11px 16px; }
168
190
  .fold-hit:hover .row-toggle { background: var(--hover); color: var(--text); }
169
191
  .row-toggle[aria-expanded=true] .chev { transform: rotate(90deg); }
170
192
  .row-toggle[aria-expanded=true]:not(:hover) { background: transparent; color: var(--muted); }
171
193
  .fold-hit:hover .row-toggle[aria-expanded=true] { background: var(--hover); color: var(--text); }
172
- .row-folded { grid-template-areas: "id id id id id more"; padding-top: 7px; padding-bottom: 7px; }
173
- .card-folded .card-head { align-items: center; }
194
+ /* The details: ref and model, tokens, Health, side by side under the name. */
195
+ .row-detail { grid-area: detail; display: flex; flex-wrap: wrap; gap: 10px 36px; margin: 10px 0 2px 64px; padding-top: 10px; border-top: 1px dashed var(--border); cursor: default; }
196
+ .rd-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
197
+ .rd-label { color: var(--faint); font-size: 11.5px; font-weight: var(--strong-weight); }
198
+ .rd-note { color: var(--muted); font-size: 12px; }
199
+ .rows .row-detail .usage { justify-content: flex-start; gap: 16px; }
200
+ .row-detail .ident-sub { margin-top: 0; }
201
+ .att-dot { flex: none; }
174
202
  .col-head { position: sticky; top: 0; z-index: 2; padding: 7px 10px 7px 16px; border-top: 1px solid var(--border); background: var(--surface); color: var(--faint); font-size: 11.5px; font-weight: var(--strong-weight); line-height: 1.3; white-space: nowrap; }
175
203
  .col-head > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
176
204
  .col-head .quota > span { overflow: hidden; text-overflow: ellipsis; }
@@ -184,18 +212,27 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
184
212
  .model-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
185
213
  .model-set { color: var(--accent); }
186
214
  .md-now { display: flex; flex-direction: column; gap: 4px; }
215
+ .br-list { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
216
+ .br-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--ctl-radius); }
187
217
  .email-btn:hover { color: var(--text); text-decoration: underline dotted; }
188
218
  .email-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
219
+ .ident-email:has(.br-btn) { display: flex; align-items: center; }
220
+ .ident-email:has(.br-btn) > * { flex: none; }
221
+ .ident-email:has(.br-btn) > .email-btn:not(.br-btn) { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
222
+ .br-btn { display: inline-flex; align-items: center; gap: 3px; vertical-align: bottom; }
223
+ .br-ico { width: 12px; height: 12px; flex: none; }
189
224
  .ident-email + .ident-sub { color: var(--faint); margin-top: 0; }
190
225
  .tools .tb-icon { width: var(--ctl-h); height: var(--ctl-h); border-color: var(--border-strong); background: var(--surface); }
191
226
  .tools .tb-icon:hover { background: var(--hover); }
192
227
  .tools .tb-icon[aria-pressed=true] { background: var(--active); color: var(--text); }
228
+ .display-panel { display: flex; flex-direction: column; gap: 10px; min-width: 240px; }
229
+ .dp-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
230
+ .dp-label { color: var(--muted); font-size: var(--small); }
231
+ .dp-check { justify-content: flex-start; gap: 8px; cursor: pointer; font-size: var(--small); }
193
232
  .ident-sub { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
194
233
  .plan { text-transform: capitalize; }
195
234
  .row > .quota { grid-area: quota; }
196
- .row > .usage { grid-area: usage; }
197
235
  .row > .last { grid-area: last; }
198
- .cell-health { grid-area: health; min-width: 0; }
199
236
  .row > .more { grid-area: more; }
200
237
  .row-notes { grid-area: notes; display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
201
238
 
@@ -339,7 +376,6 @@ a.btn:hover { text-decoration: none; }
339
376
  .icon-btn:disabled { color: var(--faint); cursor: not-allowed; }
340
377
  .top-actions .icon-btn { width: var(--ctl-h); height: var(--ctl-h); border-color: var(--border-strong); background: var(--surface); }
341
378
  .top-actions .icon-btn:hover { background: var(--hover); }
342
- .lang-btn { font-size: 12.5px; font-weight: var(--button-weight); color: var(--btn-text); width: auto !important; padding: 0 10px; }
343
379
  .btn:focus-visible, .icon-btn:focus-visible, input:focus-visible, select:focus-visible, a:focus-visible, .stat-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; box-shadow: var(--focus-ring); }
344
380
  .btn:focus-visible { border-color: var(--focus); }
345
381
  .note a, .banner a { color: inherit; text-decoration: underline; text-underline-offset: 2px; font-weight: var(--strong-weight); }
@@ -349,6 +385,9 @@ a.btn:hover { text-decoration: none; }
349
385
  .note b { color: inherit; }
350
386
  .note-fail { background: var(--fail-soft); color: var(--fail); }
351
387
  .note-warn { background: var(--warn-soft); color: var(--warn); }
388
+ /* A note in two lines: what is wrong, then the command to run (with its copy button and the install page). */
389
+ .note-line { flex-basis: 100%; min-width: 0; overflow-wrap: anywhere; }
390
+ .note-cmd { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
352
391
  .error-note { justify-content: space-between; gap: 12px; }
353
392
  .error-note > span { min-width: 0; overflow-wrap: anywhere; }
354
393
  .error-note > .note-sub { flex-basis: 100%; color: var(--text); }
@@ -358,6 +397,9 @@ a.btn:hover { text-decoration: none; }
358
397
  .banner-fail { background: var(--fail-soft); border-color: transparent; color: var(--fail); }
359
398
  .banner-warn { background: var(--warn-soft); border-color: transparent; color: var(--warn); }
360
399
  .banner ul { margin: 6px 0 0; padding-left: 18px; }
400
+ .rc-list { list-style: none; margin: 10px 0; padding: 0; display: grid; gap: 8px; }
401
+ .rc-row { padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; display: grid; gap: 2px; }
402
+ .rc-row code { word-break: break-all; }
361
403
  .banner-actions { display: flex; gap: 8px; flex: none; }
362
404
  .hp-how { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 4px; }
363
405
  .hp-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
@@ -490,6 +532,18 @@ select:is(:hover, :focus) { --chevron: var(--text); }
490
532
  /* The hint gives way first: it shrinks to an ellipsis so a long label never pushes it past the menu's edge. */
491
533
  .menu-hint { min-width: 0; margin-left: auto; padding-left: 12px; color: var(--faint); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
492
534
  .menu-sep { height: 1px; margin: 4px 6px; background: var(--border); }
535
+ /* Popover: the menu's floating box with free content (text, a few buttons), lighter than a dialog. */
536
+ .popover { width: 280px; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
537
+ .popover p { margin: 0; }
538
+ .popover-actions { display: flex; gap: 8px; flex-wrap: wrap; }
539
+ .popover-foot { padding-top: 8px; border-top: 1px solid var(--border); }
540
+ /* The header's settings popover: two segmented choices, then links to Remote Control and Auto Handoff. */
541
+ .set-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
542
+ .set-label { color: var(--muted); font-size: 13px; white-space: nowrap; }
543
+ .set-row .seg { height: var(--ctl-h-sm); }
544
+ .set-link { margin: 0 -8px; width: calc(100% + 16px); }
545
+ .set-link .chip { margin-left: auto; }
546
+ .set-dot { background: var(--warn-bar); }
493
547
  .tip { display: inline-flex; position: relative; vertical-align: middle; }
494
548
  .tip-btn { display: inline-grid; place-items: center; width: 16px; height: 16px; padding: 0; border: 1px solid var(--border-strong); border-radius: 50%; background: var(--surface); color: var(--muted); font: 700 10px/1 var(--font); cursor: help; }
495
549
  .tip-btn:hover, .tip.is-open .tip-btn { color: var(--text); border-color: var(--muted); }
@@ -543,8 +597,9 @@ select:is(:hover, :focus) { --chevron: var(--text); }
543
597
  .col-head { display: none; }
544
598
  .app-row { flex-wrap: wrap; }
545
599
  .app-actions { width: 100%; justify-content: flex-start; }
546
- .row { grid-template-columns: minmax(0, 1fr) auto var(--m-w); grid-template-areas: "id health more" "quota quota quota" "usage usage last" "notes notes notes"; row-gap: 10px; }
547
- .row-folded { grid-template-areas: "id id more"; }
600
+ .row { grid-template-columns: minmax(0, 1fr) auto var(--m-w); grid-template-areas: "id last more" "quota quota quota" "detail detail detail" "notes notes notes"; row-gap: 10px; }
601
+ .row-detail { margin-left: 0; }
602
+ .login-rows > .row, .login-rows > .row:first-child { padding-left: 28px; }
548
603
  .row > .quota { grid-template-columns: repeat(var(--qn), minmax(0, 1fr)); max-width: 560px; }
549
604
  .rows .m-label, .rows .m-unit, .rows .u-unit { display: inline; }
550
605
  .rows .usage { justify-content: flex-start; }
@@ -559,12 +614,12 @@ select:is(:hover, :focus) { --chevron: var(--text); }
559
614
  :root { --gutter: 14px; }
560
615
  .top-in { padding: 16px var(--gutter) 0; }
561
616
  .header-bar .top-in { min-height: 0; padding: 12px var(--gutter); }
617
+ .update-btn span { display: none; }
562
618
  .wrap { padding: 14px var(--gutter) 32px; }
563
619
  .hide-sm { display: none; }
564
620
  .tagline { display: none; }
565
621
  .brand .logo { width: 34px; height: 34px; }
566
622
  .brand h1 { font-size: 19px; }
567
- #updated { display: none; }
568
623
  .banner { flex-direction: column; }
569
624
  .stat { padding: 11px 13px; align-items: flex-start; flex-direction: column; gap: 6px; }
570
625
  .stat-value { font-size: 19px; }
@@ -1,5 +1,8 @@
1
+ import type { LocalBuild } from '../core/version.ts';
1
2
  import { pageStyles } from './page-styles.ts';
2
3
  import { type ResolvedTheme } from './theme.ts';
4
+ /** Browsers keep a favicon past `no-cache`; a link that changes with the mark makes them fetch the new one. */
5
+ export declare const FAVICON_VERSION: string;
3
6
  export { pageStyles };
4
7
  export interface PageOptions {
5
8
  basePath: string;
@@ -9,6 +12,8 @@ export interface PageOptions {
9
12
  version?: string;
10
13
  /** buildId() of the serving process; the page reloads when /api/state reports another one. */
11
14
  build?: string;
15
+ /** localBuild() of the serving process (ADR-0024): the version pill names the commit. */
16
+ local?: LocalBuild | null;
12
17
  readOnly?: boolean;
13
18
  /** Host theme from resolveTheme(); the default look when absent. */
14
19
  theme?: ResolvedTheme;
@@ -1,13 +1,16 @@
1
1
  // The Panel page: one self-contained HTML document with inline CSS and JS, no external resources. The parts live in
2
2
  // sibling modules: page-styles.ts (tokens and CSS), page-script.ts (behaviour, with page-logic.ts helpers) and
3
3
  // i18n.ts (English and Chinese text); this module assembles them under the CSP nonce.
4
- import { MARK_SVG } from '../core/brand-mark.js';
4
+ import { createHash } from 'node:crypto';
5
+ import { FAVICON_SVG, MARK_SVG } from '../core/brand-mark.js';
5
6
  import { ALIAS_NAME, RESERVED_ALIASES } from '../core/config.js';
6
7
  import { QUOTA_FAIL_PERCENT, QUOTA_WARN_PERCENT, USAGE_DAYS } from '../core/quota-levels.js';
7
8
  import { MESSAGES } from './i18n.js';
8
9
  import { PAGE_SCRIPT } from './page-script.js';
9
10
  import { pageStyles } from './page-styles.js';
10
11
  import { resolveTheme } from './theme.js';
12
+ /** Browsers keep a favicon past `no-cache`; a link that changes with the mark makes them fetch the new one. */
13
+ export const FAVICON_VERSION = createHash('sha256').update(FAVICON_SVG).digest('hex').slice(0, 10);
11
14
  export { pageStyles };
12
15
  /** JSON that is safe inside a `<script>` element: no `</script>`, `<!--` or line separators survive. */
13
16
  export function jsonForScript(value) {
@@ -33,6 +36,7 @@ export function renderPage(opts) {
33
36
  token: opts.token,
34
37
  version: opts.version ?? '',
35
38
  build: opts.build ?? '',
39
+ local: opts.local ?? null,
36
40
  readOnly: Boolean(opts.readOnly),
37
41
  updates: opts.updates !== false,
38
42
  colorScheme: theme.colorScheme,
@@ -52,7 +56,7 @@ export function renderPage(opts) {
52
56
  <meta name="color-scheme" content="${scheme}">
53
57
  <meta name="referrer" content="no-referrer">
54
58
  <title>sideby</title>
55
- <link rel="icon" type="image/svg+xml" href="${attr(opts.basePath)}/favicon.svg">
59
+ <link rel="icon" type="image/svg+xml" href="${attr(opts.basePath)}/favicon.svg?v=${FAVICON_VERSION}">
56
60
  <style${nonce}>${pageStyles(theme)}</style>
57
61
  </head>
58
62
  <body${theme.header === 'bar' ? ' class="header-bar"' : ''}>
@@ -61,16 +65,13 @@ export function renderPage(opts) {
61
65
  <div class="brand">
62
66
  <span class="logo" aria-hidden="true">${MARK_SVG}</span>
63
67
  <div>
64
- <h1>sideby</h1>
68
+ <div class="title-row"><h1>sideby</h1><button id="ver-top" class="ver-pill" type="button" aria-haspopup="dialog" aria-expanded="false" hidden></button></div>
65
69
  <p class="tagline" data-i18n="top.tagline">Every AI coding account, side by side.</p>
66
70
  </div>
67
71
  </div>
68
72
  <div class="top-actions">
69
- <span id="updated" class="muted small">Loading…</span>
70
- <button id="lang" class="icon-btn lang-btn" type="button"${lang === 'auto' ? '' : ' hidden'}>${lang === 'zh' ? 'EN' : '中文'}</button>
71
- <button id="theme" class="icon-btn" type="button" hidden></button>
72
73
  <button id="update" class="btn update-btn" type="button" hidden></button>
73
- <button id="handoff" class="btn" type="button" hidden></button>
74
+ <button id="settings" class="btn" type="button" aria-haspopup="dialog" aria-expanded="false"${lang === 'auto' ? '' : ' hidden'}><span data-i18n="set.button">Settings</span></button>
74
75
  <button id="refresh" class="btn" type="button" aria-label="Refresh"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 4v7h-7"/></svg><span class="hide-sm" data-i18n="top.refresh">Refresh</span></button>
75
76
  </div>
76
77
  </div>
@@ -82,7 +83,7 @@ export function renderPage(opts) {
82
83
  <section id="handoff-page" class="block" aria-labelledby="h-handoff" hidden></section>
83
84
  <section id="tuning-page" class="block" aria-labelledby="h-tuning" hidden></section>
84
85
  <section id="accounts-page" class="block" aria-labelledby="h-accounts">
85
- <div class="section-head acct-head"><h2 id="h-accounts"><span data-i18n="acct.title">Accounts</span><span id="acct-count" class="muted small"></span></h2></div>
86
+ <div class="section-head acct-head"><h2 id="h-accounts"><span data-i18n="acct.title">Accounts</span><span id="acct-count" class="muted small"></span></h2><span id="updated" class="muted small" role="status">Loading…</span></div>
86
87
  <div id="toolbar" class="toolbar" hidden></div>
87
88
  <div id="accounts" aria-busy="true"><div class="group group-list">${SKELETON_ROW.repeat(4)}</div></div>
88
89
  </section>
@@ -90,7 +91,7 @@ export function renderPage(opts) {
90
91
  <div class="section-head"><h2 id="h-checkup" data-i18n="health.title">Health check</h2></div>
91
92
  <span class="sk sk-line w70"></span><span class="sk sk-line w50"></span>
92
93
  </section>
93
- <footer class="foot muted small"><button id="ver" class="link-btn" type="button">sideby</button> · <span id="foot-text" data-i18n="foot.text">runs on this machine; nothing leaves it.</span></footer>
94
+ <footer class="foot muted small"><button id="ver" class="link-btn" type="button" aria-haspopup="dialog" aria-expanded="false">sideby</button> · <span id="foot-text" data-i18n="foot.text">runs on this machine; nothing leaves it.</span></footer>
94
95
  </main>
95
96
  <div id="conn" class="conn" role="status" hidden></div>
96
97
  <div id="toasts" class="toasts" aria-live="polite" popover="manual"></div>
@@ -59,7 +59,10 @@ export interface FamilyInfo {
59
59
  title: string;
60
60
  bin: string;
61
61
  installUrl: string;
62
+ installCommand?: string;
62
63
  installed: boolean;
64
+ /** A `bin` on PATH that exists but cannot run (an interrupted upgrade); set only when `installed` is false. */
65
+ brokenBin?: string;
63
66
  /** Plugin that provides the Family. */
64
67
  plugin: string;
65
68
  /** The Family can estimate a Compaction Threshold (spec §3.18). */
@@ -232,7 +235,9 @@ export interface Runtime {
232
235
  remoteControl(): Promise<RemoteControlGroup[]>;
233
236
  /** Sets `remoteControlAtStartup` to false wherever it is on or left to the default, remembering how to undo it. */
234
237
  remoteControlOff(): Promise<RemoteControlChange>;
235
- /** Puts back what `remoteControlOff` replaced, only where the key still holds false. */
238
+ /** Sets `remoteControlAtStartup` to true wherever it is off or left to the default, remembering how to undo it. */
239
+ remoteControlOn(): Promise<RemoteControlChange>;
240
+ /** Puts back what `remoteControlOff` or `remoteControlOn` replaced, only where the key still holds what it wrote. */
236
241
  remoteControlUndo(): Promise<RemoteControlChange>;
237
242
  /**
238
243
  * Which Account of a Family to move on to (spec §3.13). Throws a UserError with code `no-quota-source`
@@ -10,10 +10,10 @@ 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 { prepareLaunch, which } from './core/launch.js';
13
+ import { brokenOnPath, prepareLaunch, which } from './core/launch.js';
14
14
  import { memoStore } from './core/memo-store.js';
15
15
  import { resolvePaths } from './core/paths.js';
16
- import { remoteControlGroups, turnRemoteControlOff, undoRemoteControlOff, } from './core/remote-control.js';
16
+ import { remoteControlGroups, turnRemoteControlOff, turnRemoteControlOn, undoRemoteControlOff, } from './core/remote-control.js';
17
17
  import { findResumeRoute } from './core/resume.js';
18
18
  import { SAME_LOGIN_FAMILIES, sameLoginGroups, sharedQuota } from './core/same-login.js';
19
19
  import { shareMeaning } from './core/share-meaning.js';
@@ -205,15 +205,23 @@ export async function createRuntime(opts = {}) {
205
205
  plugins: loaded.plugins,
206
206
  pluginErrors: loaded.errors,
207
207
  async familyInfo() {
208
- return Promise.all([...families.values()].map(async (f) => ({
209
- id: f.id,
210
- title: f.title,
211
- bin: f.bin,
212
- installUrl: f.installUrl,
213
- installed: (await which(f.bin, env)) !== null,
214
- plugin: loaded.familyOwner.get(f.id) ?? 'unknown',
215
- tuning: Boolean(f.readTuning),
216
- })));
208
+ return Promise.all([...families.values()].map(async (f) => {
209
+ const installed = (await which(f.bin, env)) !== null;
210
+ const broken = installed ? null : await brokenOnPath(f.bin, env);
211
+ return {
212
+ id: f.id,
213
+ title: f.title,
214
+ bin: f.bin,
215
+ installUrl: f.installUrl,
216
+ ...(typeof f.installCommand === 'string' && f.installCommand.trim()
217
+ ? { installCommand: f.installCommand.trim() }
218
+ : {}),
219
+ installed,
220
+ ...(broken ? { brokenBin: broken } : {}),
221
+ plugin: loaded.familyOwner.get(f.id) ?? 'unknown',
222
+ tuning: Boolean(f.readTuning),
223
+ };
224
+ }));
217
225
  },
218
226
  async familyDetails() {
219
227
  const info = new Map((await rt.familyInfo()).map((f) => [f.id, f]));
@@ -581,6 +589,9 @@ export async function createRuntime(opts = {}) {
581
589
  async remoteControlOff() {
582
590
  return turnRemoteControlOff(await accounts(), readContext());
583
591
  },
592
+ async remoteControlOn() {
593
+ return turnRemoteControlOn(await accounts(), readContext());
594
+ },
584
595
  async remoteControlUndo() {
585
596
  return undoRemoteControlOff(await accounts(), readContext());
586
597
  },
@@ -42,6 +42,8 @@ export interface FamilyDef {
42
42
  /** Host executable looked up on PATH. */
43
43
  bin: string;
44
44
  installUrl: string;
45
+ /** The Host's official one-line install command, shown before `installUrl`; sideby never runs it. */
46
+ installCommand?: string;
45
47
  /** Environment variable that selects the Account directory. Not set for the Main Account. */
46
48
  selectVar: string;
47
49
  layout: FamilyLayout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.12.0",
3
+ "version": "0.13.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)",
@@ -55,13 +55,15 @@
55
55
  ],
56
56
  "scripts": {
57
57
  "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/src/cli/main.js && node scripts/write-build-id.ts",
58
+ "local": "node scripts/local.ts",
58
59
  "typecheck": "tsc --noEmit",
59
60
  "lint": "biome check .",
60
61
  "format": "biome check --write .",
61
- "test": "node --test --test-concurrency=4 \"src/**/*.test.ts\" \"testing/**/*.test.ts\"",
62
+ "test": "node --test --test-concurrency=4 \"src/**/*.test.ts\" \"testing/**/*.test.ts\" \"scripts/**/*.test.ts\"",
62
63
  "schemas": "node src/cli/schemas.ts",
63
64
  "leak-check": "sh scripts/leak-check.sh",
64
65
  "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm leak-check",
66
+ "preversion": "node scripts/readme-assets.ts",
65
67
  "version": "node scripts/sync-skill-version.ts && git add skills/sideby/SKILL.md",
66
68
  "prepublishOnly": "node scripts/pin-english-npm-readme.ts",
67
69
  "postpublish": "node scripts/pin-english-npm-readme.ts --restore"
@@ -77,5 +79,5 @@
77
79
  },
78
80
  "packageManager": "pnpm@10.15.0",
79
81
  "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## 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| Stop Claude sessions from connecting to claude.ai by themselves (Remote Control), for every account at once | `sideby remote-control off` (or the panel's banner) | [Remote Control](docs/guide/usage.md#remote-control) |\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` | 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 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"
82
+ "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| 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` | 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 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"
81
83
  }
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/browser-open.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "ref",
8
+ "url",
9
+ "profile",
10
+ "command"
11
+ ],
12
+ "properties": {
13
+ "schemaVersion": {
14
+ "type": "number",
15
+ "const": 1
16
+ },
17
+ "ref": {
18
+ "type": "string"
19
+ },
20
+ "url": {
21
+ "type": "string"
22
+ },
23
+ "profile": {
24
+ "type": "object",
25
+ "required": [
26
+ "dir",
27
+ "name"
28
+ ],
29
+ "properties": {
30
+ "dir": {
31
+ "type": "string"
32
+ },
33
+ "name": {
34
+ "type": "string"
35
+ }
36
+ }
37
+ },
38
+ "command": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "string"
42
+ },
43
+ "description": "The `open` command sideby ran."
44
+ }
45
+ },
46
+ "title": "sideby browser open --json"
47
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/browser-switch.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "enabled",
8
+ "changed",
9
+ "applied",
10
+ "file",
11
+ "reads"
12
+ ],
13
+ "properties": {
14
+ "schemaVersion": {
15
+ "type": "number",
16
+ "const": 1
17
+ },
18
+ "enabled": {
19
+ "type": "boolean",
20
+ "description": "The value asked for."
21
+ },
22
+ "changed": {
23
+ "type": "boolean"
24
+ },
25
+ "applied": {
26
+ "type": "boolean",
27
+ "description": "False when only the change was shown; run again with --yes (exit 10)."
28
+ },
29
+ "file": {
30
+ "type": "string",
31
+ "description": "The config file."
32
+ },
33
+ "reads": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "string"
37
+ },
38
+ "description": "What sideby reads while it is on, all on this computer."
39
+ }
40
+ },
41
+ "title": "sideby browser --enable|--disable --json"
42
+ }