sideby 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +3 -2
- package/dist/src/cli/commands.js +69 -8
- package/dist/src/cli/format.js +7 -3
- package/dist/src/cli/json-schemas.js +62 -2
- package/dist/src/cli/main.js +42 -4
- package/dist/src/core/config.d.ts +27 -0
- package/dist/src/core/config.js +48 -1
- package/dist/src/core/handoff-enable.js +10 -5
- package/dist/src/core/launch.d.ts +5 -0
- package/dist/src/core/launch.js +14 -3
- package/dist/src/core/quota-fetch.d.ts +22 -0
- package/dist/src/core/quota-fetch.js +98 -0
- package/dist/src/core/quota-levels.d.ts +3 -2
- package/dist/src/core/quota-levels.js +8 -4
- package/dist/src/core/quota-store.d.ts +19 -0
- package/dist/src/core/quota-store.js +62 -0
- package/dist/src/core/skill-check.d.ts +15 -0
- package/dist/src/core/skill-check.js +49 -3
- package/dist/src/core/update.d.ts +45 -0
- package/dist/src/core/update.js +72 -1
- package/dist/src/families/grok/handoff-hook.d.ts +6 -0
- package/dist/src/families/grok/handoff-hook.js +45 -2
- package/dist/src/families/grok/handoff.d.ts +6 -1
- package/dist/src/families/grok/handoff.js +30 -13
- package/dist/src/families/grok/index.js +5 -0
- package/dist/src/families/grok/quota-log.d.ts +30 -0
- package/dist/src/families/grok/quota-log.js +140 -0
- package/dist/src/families/grok/quota.d.ts +5 -0
- package/dist/src/families/grok/quota.js +112 -0
- package/dist/src/panel/handler.d.ts +9 -3
- package/dist/src/panel/handler.js +65 -3
- package/dist/src/panel/i18n.d.ts +25 -5
- package/dist/src/panel/i18n.js +52 -12
- package/dist/src/panel/page-logic.d.ts +18 -2
- package/dist/src/panel/page-logic.js +53 -6
- package/dist/src/panel/page-script.js +286 -92
- package/dist/src/panel/page-styles.js +30 -10
- package/dist/src/runtime.d.ts +30 -2
- package/dist/src/runtime.js +64 -21
- package/dist/src/types.d.ts +49 -1
- package/package.json +2 -2
- package/schemas/alias.json +62 -0
- package/schemas/app-install.json +62 -0
- package/schemas/app-uninstall.json +62 -0
- package/schemas/browser-open.json +62 -0
- package/schemas/browser-switch.json +62 -0
- package/schemas/browser.json +62 -0
- package/schemas/config.json +46 -0
- package/schemas/desktop-remove.json +62 -0
- package/schemas/desktop.json +62 -0
- package/schemas/doctor.json +62 -0
- package/schemas/error.json +62 -0
- package/schemas/families.json +62 -0
- package/schemas/handoff-disable.json +62 -0
- package/schemas/handoff-enable.json +62 -0
- package/schemas/handoff-ready.json +62 -0
- package/schemas/handoff-setup.json +62 -0
- package/schemas/handoff-status.json +62 -0
- package/schemas/handoff-teardown.json +62 -0
- package/schemas/list.json +62 -0
- package/schemas/model.json +62 -0
- package/schemas/new.json +62 -0
- package/schemas/next.json +62 -0
- package/schemas/plugins.json +62 -0
- package/schemas/quota-setup.json +62 -0
- package/schemas/quota-teardown.json +62 -0
- package/schemas/quota.json +76 -1
- package/schemas/remote-control.json +62 -0
- package/schemas/tune-change.json +62 -0
- package/schemas/tune.json +62 -0
- package/schemas/update.json +107 -1
- package/skills/sideby/SKILL.md +4 -2
- package/skills/sideby/references/diagnose.md +3 -2
- package/skills/sideby/references/handoff.md +1 -1
- package/skills/sideby/references/quota.md +5 -1
- package/skills/sideby/references/update.md +16 -0
|
@@ -177,13 +177,29 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
177
177
|
.row:first-child { border-top: 0; }
|
|
178
178
|
.row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
|
|
179
179
|
.row-error .ident-name .name { color: var(--fail); }
|
|
180
|
-
/* A login group (spec §3.21):
|
|
181
|
-
.login { border-top: 1px solid var(--border); }
|
|
180
|
+
/* A login group (spec §3.21): ordinary rows that share one quota, tied together by a rail on the left. */
|
|
181
|
+
.login { position: relative; border-top: 1px solid var(--border); }
|
|
182
182
|
.login:first-child { border-top: 0; }
|
|
183
|
-
.
|
|
184
|
-
.rows
|
|
185
|
-
.login-rows > .row
|
|
183
|
+
.login::before { content: ""; position: absolute; left: 5px; top: 12px; bottom: 12px; width: 3px; border-radius: 3px; background: var(--border-strong); }
|
|
184
|
+
.login-rows > .row:first-child { border-top: 0; }
|
|
185
|
+
.login-rows > .row + .row { border-top: 1px dashed var(--border); }
|
|
186
186
|
.login-rows > .row:last-child { border-radius: 0 0 var(--radius) var(--radius); }
|
|
187
|
+
.q-shared { gap: 6px; color: var(--faint); }
|
|
188
|
+
.q-shared .ic { width: 13px; height: 13px; }
|
|
189
|
+
/* Display Order (spec §3.25): a grip at the start of a group head or row, shown on hover or focus, always on touch
|
|
190
|
+
screens. A row's grip sits in its left padding, so the row's layout is the same with or without it. */
|
|
191
|
+
.row { position: relative; }
|
|
192
|
+
.drag-handle { width: 16px; height: 28px; color: var(--faint); cursor: grab; opacity: 0; transition: opacity .12s; }
|
|
193
|
+
.drag-handle .ic { width: 14px; height: 14px; }
|
|
194
|
+
.drag-handle:active { cursor: grabbing; }
|
|
195
|
+
.row > .ident > .drag-handle { position: absolute; left: 0; top: 13px; }
|
|
196
|
+
.group-head > .drag-handle { margin-right: -8px; }
|
|
197
|
+
.group-head:hover > .drag-handle, .row:hover > .ident > .drag-handle, .drag-handle:focus-visible { opacity: 1; }
|
|
198
|
+
@media (hover: none) { .drag-handle { opacity: 1; } }
|
|
199
|
+
.is-dragging { opacity: .45; }
|
|
200
|
+
.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
|
|
201
|
+
.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
|
|
202
|
+
.tb-reset[hidden] { display: none; }
|
|
187
203
|
/* The arrow at a row's start opens and closes its details; so does a click anywhere else in the row. */
|
|
188
204
|
.row-toggle { width: 28px; height: 28px; margin: -4px -6px -4px -8px; }
|
|
189
205
|
.fold-hit { cursor: pointer; }
|
|
@@ -245,7 +261,9 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
245
261
|
.m-pct { font-weight: var(--strong-weight); font-size: 13px; font-variant-numeric: tabular-nums; }
|
|
246
262
|
.m-unit { color: var(--faint); font-weight: 400; font-size: 11.5px; }
|
|
247
263
|
.m-reset { margin-left: auto; color: var(--faint); font-size: 11.5px; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
|
|
248
|
-
.m-passed {
|
|
264
|
+
.m-passed { color: var(--muted); }
|
|
265
|
+
.m-asof { display: inline-flex; margin-left: 4px; color: var(--faint); vertical-align: -1px; }
|
|
266
|
+
.m-asof .ic { width: 12px; height: 12px; }
|
|
249
267
|
.m-none { color: var(--faint); }
|
|
250
268
|
.bar { height: 6px; border-radius: 999px; background: var(--track); overflow: hidden; margin-top: 5px; }
|
|
251
269
|
.bar > span { display: block; height: 100%; min-width: 0; border-radius: inherit; background: var(--ok-bar); transition: width .4s ease; }
|
|
@@ -253,9 +271,9 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
253
271
|
.lvl-fail .bar > span { background: var(--fail-bar); } .lvl-fail .m-pct { color: var(--fail); }
|
|
254
272
|
.lvl-reset .m-pct { color: var(--muted); }
|
|
255
273
|
.lvl-stale.lvl-ok .m-pct { color: var(--muted); }
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
.
|
|
274
|
+
/* Old numbers keep their level's color, paler, so the bar and the number agree. */
|
|
275
|
+
.lvl-stale .bar > span { opacity: .45; }
|
|
276
|
+
.lvl-stale.lvl-ok .bar > span { background: var(--faint); }
|
|
259
277
|
.q-note { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: var(--muted); font-size: var(--small); }
|
|
260
278
|
.link-btn { padding: 0; border: 0; background: none; color: var(--accent); font: inherit; font-weight: var(--strong-weight); cursor: pointer; }
|
|
261
279
|
.link-btn:hover:not(:disabled) { text-decoration: underline; }
|
|
@@ -541,6 +559,9 @@ select:is(:hover, :focus) { --chevron: var(--text); }
|
|
|
541
559
|
.set-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
542
560
|
.set-label { color: var(--muted); font-size: 13px; white-space: nowrap; }
|
|
543
561
|
.set-row .seg { height: var(--ctl-h-sm); }
|
|
562
|
+
.set-check { align-items: flex-start; }
|
|
563
|
+
.set-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
564
|
+
.set-hint { color: var(--faint); font-size: 12px; max-width: 260px; }
|
|
544
565
|
.set-link { margin: 0 -8px; width: calc(100% + 16px); }
|
|
545
566
|
.set-link .chip { margin-left: auto; }
|
|
546
567
|
.set-dot { background: var(--warn-bar); }
|
|
@@ -599,7 +620,6 @@ select:is(:hover, :focus) { --chevron: var(--text); }
|
|
|
599
620
|
.app-actions { width: 100%; justify-content: flex-start; }
|
|
600
621
|
.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
622
|
.row-detail { margin-left: 0; }
|
|
602
|
-
.login-rows > .row, .login-rows > .row:first-child { padding-left: 28px; }
|
|
603
623
|
.row > .quota { grid-template-columns: repeat(var(--qn), minmax(0, 1fr)); max-width: 560px; }
|
|
604
624
|
.rows .m-label, .rows .m-unit, .rows .u-unit { display: inline; }
|
|
605
625
|
.rows .usage { justify-content: flex-start; }
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AccountRefError } from './core/accounts.ts';
|
|
2
|
-
import { type Config, type HandoffSettings } from './core/config.ts';
|
|
2
|
+
import { type Config, type DisplayConfig, type HandoffSettings } from './core/config.ts';
|
|
3
3
|
import { type CreateResult } from './core/create.ts';
|
|
4
4
|
import { type DesktopDeps } from './core/desktop-app.ts';
|
|
5
5
|
import { type DoctorReport } from './core/doctor.ts';
|
|
@@ -9,6 +9,7 @@ import { type HandoffDecision } from './core/handoff-select.ts';
|
|
|
9
9
|
import { type DoctorHistory } from './core/last-doctor.ts';
|
|
10
10
|
import { type HostLaunch, type PreparedLaunch } from './core/launch.ts';
|
|
11
11
|
import { type Paths } from './core/paths.ts';
|
|
12
|
+
import { type QuotaFetchMode } from './core/quota-fetch.ts';
|
|
12
13
|
import { type RemoteControlChange, type RemoteControlGroup } from './core/remote-control.ts';
|
|
13
14
|
import { type Shell, type ShellInitWrite } from './core/shell-init.ts';
|
|
14
15
|
import { type TuneEntry, type TuneGroup } from './core/tuning.ts';
|
|
@@ -209,7 +210,24 @@ export interface Runtime {
|
|
|
209
210
|
* The Account's Quota. For an Account of a Same Login (spec §3.21) it is the group's freshest reading, with `from`
|
|
210
211
|
* naming the Account it was read from when that is another one.
|
|
211
212
|
*/
|
|
212
|
-
quota(account: Account
|
|
213
|
+
quota(account: Account, opts?: {
|
|
214
|
+
fetch?: QuotaFetchMode;
|
|
215
|
+
}): Promise<QuotaResult>;
|
|
216
|
+
/**
|
|
217
|
+
* Asks every Family that fetches Quota (spec §3.26) again now, for `only` or every signed-in subscription Account,
|
|
218
|
+
* and waits for the answers. Fetching that is turned off stays off.
|
|
219
|
+
*/
|
|
220
|
+
refreshQuota(only?: readonly Account[]): Promise<void>;
|
|
221
|
+
/** Families that fetch Quota, with their switch (config `quota.check.<family>`) and why it is off, if it is. */
|
|
222
|
+
quotaChecks(): QuotaCheckInfo[];
|
|
223
|
+
/** Turns a Family's Quota fetching on (removes the key) or off (`quota.check.<family>: false`). */
|
|
224
|
+
setQuotaCheck(family: string, on: boolean): Promise<{
|
|
225
|
+
changed: boolean;
|
|
226
|
+
}>;
|
|
227
|
+
/** The config `display` (spec §3.25), or an empty object. */
|
|
228
|
+
displayOrder(): DisplayConfig;
|
|
229
|
+
/** Replaces the config `display`; undefined or empty removes it. Returns what the file now holds. */
|
|
230
|
+
setDisplayOrder(display: DisplayConfig | undefined): Promise<DisplayConfig>;
|
|
213
231
|
/** The other Accounts signed in as the same login as this one (spec §3.21), in discovery order; empty when none. */
|
|
214
232
|
sameLogin(account: Account): Promise<string[]>;
|
|
215
233
|
usage(account: Account): Promise<UsageResult>;
|
|
@@ -263,6 +281,16 @@ export interface Runtime {
|
|
|
263
281
|
}
|
|
264
282
|
export declare class UnknownFamilyError extends UserError {
|
|
265
283
|
}
|
|
284
|
+
export interface QuotaCheckInfo {
|
|
285
|
+
family: string;
|
|
286
|
+
title: string;
|
|
287
|
+
summary: string;
|
|
288
|
+
everyMinutes: number;
|
|
289
|
+
/** False when config `quota.check.<family>` is false. */
|
|
290
|
+
on: boolean;
|
|
291
|
+
/** Why no fetch happens even though it is on (CI), or undefined. */
|
|
292
|
+
blocked?: string;
|
|
293
|
+
}
|
|
266
294
|
export declare function createRuntime(opts?: {
|
|
267
295
|
env?: Env;
|
|
268
296
|
builtins?: BuiltinPlugin[];
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The one service layer the CLI and the Panel share. Frozen contract for v0.1 (plan: 审核修订).
|
|
2
2
|
import { aliasesByAccount, discoverAccounts, resolveRef } from './core/accounts.js';
|
|
3
|
-
import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, handoffSettings, loadConfig, removeConfigAlias, } from './core/config.js';
|
|
3
|
+
import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, editConfigDisplay, editConfigQuotaCheck, handoffSettings, loadConfig, removeConfigAlias, } from './core/config.js';
|
|
4
4
|
import { createAccount } from './core/create.js';
|
|
5
5
|
import { bindDesktopDeps, checkAppData } from './core/desktop-app.js';
|
|
6
6
|
import { applyFixes, runDoctor } from './core/doctor.js';
|
|
@@ -10,9 +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 { brokenOnPath, prepareLaunch, which } from './core/launch.js';
|
|
13
|
+
import { accountEnv, 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 { fetchedQuota, quotaCheckOff, shownQuota } from './core/quota-fetch.js';
|
|
16
17
|
import { remoteControlGroups, turnRemoteControlOff, turnRemoteControlOn, undoRemoteControlOff, } from './core/remote-control.js';
|
|
17
18
|
import { findResumeRoute } from './core/resume.js';
|
|
18
19
|
import { SAME_LOGIN_FAMILIES, sameLoginGroups, sharedQuota } from './core/same-login.js';
|
|
@@ -106,31 +107,43 @@ export async function createRuntime(opts = {}) {
|
|
|
106
107
|
return list.filter((a) => peers.includes(a.ref));
|
|
107
108
|
};
|
|
108
109
|
/** Every Account's Quota as shown (shared within a Same Login), reading each Account's own cache once. */
|
|
109
|
-
const quotasOf = async (list) => {
|
|
110
|
+
const quotasOf = async (list, mode = 'wait') => {
|
|
110
111
|
const [own, groups] = await Promise.all([
|
|
111
|
-
Promise.all(list.map(async (a) => [a.ref, await ownQuota(a)])).then((e) => new Map(e)),
|
|
112
|
+
Promise.all(list.map(async (a) => [a.ref, await ownQuota(a, mode)])).then((e) => new Map(e)),
|
|
112
113
|
groupsOf(list),
|
|
113
114
|
]);
|
|
114
115
|
return new Map(list.map((a) => [a.ref, sharedQuota(a.ref, own, groups.get(a.ref) ?? [])]));
|
|
115
116
|
};
|
|
116
|
-
|
|
117
|
+
/** A reader's own `from`, `fetched` and `fetchError` are dropped: only sideby sets them (spec §3.21, §3.26). */
|
|
118
|
+
const readerOnly = (q) => {
|
|
119
|
+
if (q.status !== 'ok')
|
|
120
|
+
return q;
|
|
121
|
+
const { from: _from, fetched: _fetched, fetchError: _error, ...own } = q;
|
|
122
|
+
return own;
|
|
123
|
+
};
|
|
124
|
+
const ownQuota = async (account, mode = 'wait') => {
|
|
117
125
|
if (account.kind === 'api')
|
|
118
126
|
return { status: 'unavailable', reason: 'api-account' };
|
|
119
127
|
const f = familyOf(account.family);
|
|
120
|
-
if (!f.readQuota)
|
|
128
|
+
if (!f.readQuota && !f.fetchQuota)
|
|
121
129
|
return { status: 'unavailable', reason: 'no-source' };
|
|
122
130
|
await memos.ready();
|
|
123
131
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
let local = null;
|
|
133
|
+
if (f.readQuota)
|
|
134
|
+
try {
|
|
135
|
+
local = readerOnly(await f.readQuota(account, readContext()));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
local = { status: 'unavailable', reason: 'unrecognized', detail: err.message };
|
|
139
|
+
}
|
|
140
|
+
if (!f.fetchQuota)
|
|
141
|
+
return local;
|
|
142
|
+
const off = quotaCheckOff(f.id, config, env);
|
|
143
|
+
// A signed-out Account has nothing to ask about; its kept answer still shows.
|
|
144
|
+
const ask = off || mode === 'never' || (await loginOf(account)) === 'logged-out' ? 'never' : mode;
|
|
145
|
+
const kept = await fetchedQuota(f.fetchQuota, account, readContext(), accountEnv(f, account, env), ask);
|
|
146
|
+
return shownQuota(local, kept, off);
|
|
134
147
|
}
|
|
135
148
|
finally {
|
|
136
149
|
memos.saveSoon();
|
|
@@ -233,7 +246,7 @@ export async function createRuntime(opts = {}) {
|
|
|
233
246
|
layout: { main: f.layout.main, account: f.layout.account },
|
|
234
247
|
selectVar: f.selectVar,
|
|
235
248
|
login: { args: f.login.args ? [...f.login.args] : null, hint: f.login.hint },
|
|
236
|
-
quota: Boolean(f.readQuota),
|
|
249
|
+
quota: Boolean(f.readQuota || f.fetchQuota),
|
|
237
250
|
usage: Boolean(f.readUsage),
|
|
238
251
|
quotaSetup: Boolean(f.quotaSetup),
|
|
239
252
|
shared: f.sharedItems.map((item) => ({
|
|
@@ -497,11 +510,41 @@ export async function createRuntime(opts = {}) {
|
|
|
497
510
|
account: route.account,
|
|
498
511
|
};
|
|
499
512
|
},
|
|
500
|
-
async quota(account) {
|
|
513
|
+
async quota(account, o = {}) {
|
|
501
514
|
const peers = await peersOf(account);
|
|
502
515
|
if (!peers.length)
|
|
503
|
-
return ownQuota(account);
|
|
504
|
-
return (await quotasOf([account, ...peers])).get(account.ref);
|
|
516
|
+
return ownQuota(account, o.fetch);
|
|
517
|
+
return (await quotasOf([account, ...peers], o.fetch)).get(account.ref);
|
|
518
|
+
},
|
|
519
|
+
async refreshQuota(only) {
|
|
520
|
+
const list = (only ?? (await accounts())).filter((a) => a.kind === 'subscription' && families.get(a.family)?.fetchQuota);
|
|
521
|
+
await Promise.all(list.map((a) => ownQuota(a, 'force')));
|
|
522
|
+
},
|
|
523
|
+
quotaChecks() {
|
|
524
|
+
const ci = quotaCheckOff('', {}, env);
|
|
525
|
+
return [...families.values()]
|
|
526
|
+
.filter((f) => f.fetchQuota)
|
|
527
|
+
.map((f) => ({
|
|
528
|
+
family: f.id,
|
|
529
|
+
title: f.title,
|
|
530
|
+
summary: f.fetchQuota.summary,
|
|
531
|
+
everyMinutes: f.fetchQuota.everyMinutes,
|
|
532
|
+
on: config.quota?.check?.[f.id] !== false,
|
|
533
|
+
...(ci ? { blocked: ci } : {}),
|
|
534
|
+
}));
|
|
535
|
+
},
|
|
536
|
+
async setQuotaCheck(family, on) {
|
|
537
|
+
if (!families.get(family)?.fetchQuota)
|
|
538
|
+
throw new UserError(`${family} does not ask for quota, so there is nothing to turn on or off`);
|
|
539
|
+
const r = await editConfigQuotaCheck(paths.configFile, family, on);
|
|
540
|
+
config.quota = r.after;
|
|
541
|
+
return { changed: r.changed };
|
|
542
|
+
},
|
|
543
|
+
displayOrder: () => config.display ?? {},
|
|
544
|
+
async setDisplayOrder(display) {
|
|
545
|
+
const r = await editConfigDisplay(paths.configFile, display);
|
|
546
|
+
config.display = r.after;
|
|
547
|
+
return r.after ?? {};
|
|
505
548
|
},
|
|
506
549
|
async sameLogin(account) {
|
|
507
550
|
return (await peersOf(account)).map((a) => a.ref);
|
|
@@ -616,7 +659,7 @@ export async function createRuntime(opts = {}) {
|
|
|
616
659
|
async handoff(familyId, o = {}) {
|
|
617
660
|
const f = familyOf(familyId);
|
|
618
661
|
const list = (await accounts()).filter((a) => a.family === f.id);
|
|
619
|
-
if (!f.readQuota)
|
|
662
|
+
if (!f.readQuota && !f.fetchQuota)
|
|
620
663
|
throw new UserError(`${f.title} has no public quota source, so sideby cannot tell which account has room; pick one yourself: ${list.map((a) => `sideby run ${a.ref}`).join(', ') || `sideby new ${f.id} <name>`}`, { code: 'no-quota-source' });
|
|
621
664
|
if (list.length === 0)
|
|
622
665
|
throw new UserError(`no ${f.title} accounts yet; create one with \`sideby new ${f.id} <name>\``, {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -87,7 +87,13 @@ export interface FamilyDef {
|
|
|
87
87
|
* error is treated the same way and never shown as a problem.
|
|
88
88
|
*/
|
|
89
89
|
identity?(account: Account): Promise<AccountIdentity | undefined>;
|
|
90
|
+
/** Quota from local files only; read every time sideby shows Quota. */
|
|
90
91
|
readQuota?(account: Account, ctx: ReadContext): Promise<QuotaResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Quota that costs a request, such as asking the Host's own CLI (spec §3.26, ADR-0025). sideby decides when to call
|
|
94
|
+
* it and keeps its last answer; the newer of that answer and `readQuota`'s is shown.
|
|
95
|
+
*/
|
|
96
|
+
fetchQuota?: QuotaFetch;
|
|
91
97
|
readUsage?(account: Account, ctx: ReadContext): Promise<UsageResult>;
|
|
92
98
|
/**
|
|
93
99
|
* Estimates the best Compaction Threshold per model from the Account's recent local session records
|
|
@@ -185,6 +191,12 @@ export interface QuotaSetup {
|
|
|
185
191
|
message: string;
|
|
186
192
|
diff?: string;
|
|
187
193
|
}>;
|
|
194
|
+
/**
|
|
195
|
+
* For a setup that adds a whole file: earlier versions of that file, byte for byte, which an older sideby wrote.
|
|
196
|
+
* Setup replaces such a file after a preview, teardown removes it, and the Panel's copies into other Accounts
|
|
197
|
+
* replace it; a file with any other content is never touched.
|
|
198
|
+
*/
|
|
199
|
+
earlierTexts?: readonly string[];
|
|
188
200
|
}
|
|
189
201
|
export interface Account {
|
|
190
202
|
family: string;
|
|
@@ -206,15 +218,47 @@ export interface ReadContext {
|
|
|
206
218
|
/** sideby's own environment (PATH lookups); never the Host's. */
|
|
207
219
|
env: Env;
|
|
208
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* How sideby asks for a Family's Quota when that costs a request (spec §3.26). sideby keeps each Account's last
|
|
223
|
+
* answer in its state directory and calls `fetch` at most every `everyMinutes`, or when the user refreshes; never
|
|
224
|
+
* from the status line or the handoff hook, for an API Account or a signed-out one, in CI, or when the user turned it
|
|
225
|
+
* off (config `quota.check.<family>: false`).
|
|
226
|
+
*/
|
|
227
|
+
export interface QuotaFetch {
|
|
228
|
+
/** At most one `fetch` per Account in this many minutes, unless the user refreshes. */
|
|
229
|
+
everyMinutes: number;
|
|
230
|
+
/** What a fetch does, in one plain sentence, shown next to the setting that turns it off. */
|
|
231
|
+
summary: string;
|
|
232
|
+
/**
|
|
233
|
+
* The Account's Quota now. An error, or an `unavailable` result, keeps the last good answer on show. Stop when
|
|
234
|
+
* `ctx.signal` aborts (after 20 seconds).
|
|
235
|
+
*/
|
|
236
|
+
fetch(account: Account, ctx: QuotaFetchContext): Promise<QuotaResult>;
|
|
237
|
+
}
|
|
238
|
+
export interface QuotaFetchContext extends ReadContext {
|
|
239
|
+
signal: AbortSignal;
|
|
240
|
+
/**
|
|
241
|
+
* The environment the Host gets at Launch, before the Secret File and Plugin hooks: sideby's own, Hijack Variables
|
|
242
|
+
* cleared, the Family's `selectVar` set for every Account but the Main Account. Run the Host with this one.
|
|
243
|
+
*/
|
|
244
|
+
hostEnv: Env;
|
|
245
|
+
}
|
|
209
246
|
export interface QuotaWindow {
|
|
210
247
|
/** `5h`, `7d`, or `<n>m` for unknown window lengths. */
|
|
211
248
|
label: string;
|
|
249
|
+
/**
|
|
250
|
+
* The group of models this limit covers, such as Cursor's "Other models", when it covers only some of them. Such a
|
|
251
|
+
* window is shown under this name but never counts toward Quota Pressure: the Account can still use other models.
|
|
252
|
+
*/
|
|
253
|
+
name?: string;
|
|
212
254
|
windowMinutes: number;
|
|
213
255
|
usedPercent: number;
|
|
214
256
|
/** ISO timestamp. */
|
|
215
257
|
resetsAt: string;
|
|
216
258
|
}
|
|
217
|
-
export type QuotaUnavailableReason = 'not-enabled' | 'no-session' | 'unrecognized' | 'no-source' | 'api-account'
|
|
259
|
+
export type QuotaUnavailableReason = 'not-enabled' | 'no-session' | 'unrecognized' | 'no-source' | 'api-account'
|
|
260
|
+
/** A `fetchQuota` answer failed; `detail` says why and what to do. */
|
|
261
|
+
| 'fetch-failed';
|
|
218
262
|
export type QuotaResult = {
|
|
219
263
|
status: 'ok';
|
|
220
264
|
windows: QuotaWindow[];
|
|
@@ -226,6 +270,10 @@ export type QuotaResult = {
|
|
|
226
270
|
* Account signed in as the same login (spec §3.21). `observedAt` is that Account's time.
|
|
227
271
|
*/
|
|
228
272
|
from?: string;
|
|
273
|
+
/** Set by sideby: this reading is the kept answer of the Family's `fetchQuota`, not of its local reader. */
|
|
274
|
+
fetched?: boolean;
|
|
275
|
+
/** Set by sideby: why the latest `fetchQuota` attempt failed, while this older reading stays on show. */
|
|
276
|
+
fetchError?: string;
|
|
229
277
|
} | {
|
|
230
278
|
status: 'unavailable';
|
|
231
279
|
reason: QuotaUnavailableReason;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sideby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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)",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
},
|
|
80
80
|
"packageManager": "pnpm@10.15.0",
|
|
81
81
|
"readmeFilename": "README.md",
|
|
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"
|
|
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` | 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
83
|
}
|
package/schemas/alias.json
CHANGED
|
@@ -96,6 +96,68 @@
|
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"description": "One entry per config `shellInitFile`, rewritten after the change."
|
|
99
|
+
},
|
|
100
|
+
"_notice": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"update": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": [
|
|
106
|
+
"current",
|
|
107
|
+
"latest",
|
|
108
|
+
"command",
|
|
109
|
+
"message"
|
|
110
|
+
],
|
|
111
|
+
"properties": {
|
|
112
|
+
"current": {
|
|
113
|
+
"type": "string"
|
|
114
|
+
},
|
|
115
|
+
"latest": {
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
"command": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Run it after the user agrees: it shows the plan and exits 10; then --yes."
|
|
121
|
+
},
|
|
122
|
+
"message": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"skill": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"required": [
|
|
130
|
+
"installed",
|
|
131
|
+
"current",
|
|
132
|
+
"command",
|
|
133
|
+
"message"
|
|
134
|
+
],
|
|
135
|
+
"properties": {
|
|
136
|
+
"installed": {
|
|
137
|
+
"anyOf": [
|
|
138
|
+
{
|
|
139
|
+
"type": "string"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"type": "null"
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"description": "The skill's version; null when it has none."
|
|
146
|
+
},
|
|
147
|
+
"current": {
|
|
148
|
+
"type": "string"
|
|
149
|
+
},
|
|
150
|
+
"command": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"description": "Updates the installed sideby skill (run it after the user agrees)."
|
|
153
|
+
},
|
|
154
|
+
"message": {
|
|
155
|
+
"type": "string"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"description": "Not the answer to the task: mention it briefly afterwards and offer to update."
|
|
99
161
|
}
|
|
100
162
|
},
|
|
101
163
|
"title": "sideby alias add|rm --json"
|
package/schemas/app-install.json
CHANGED
|
@@ -68,6 +68,68 @@
|
|
|
68
68
|
"items": {
|
|
69
69
|
"type": "string"
|
|
70
70
|
}
|
|
71
|
+
},
|
|
72
|
+
"_notice": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"update": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": [
|
|
78
|
+
"current",
|
|
79
|
+
"latest",
|
|
80
|
+
"command",
|
|
81
|
+
"message"
|
|
82
|
+
],
|
|
83
|
+
"properties": {
|
|
84
|
+
"current": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
"latest": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
},
|
|
90
|
+
"command": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Run it after the user agrees: it shows the plan and exits 10; then --yes."
|
|
93
|
+
},
|
|
94
|
+
"message": {
|
|
95
|
+
"type": "string"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"skill": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"required": [
|
|
102
|
+
"installed",
|
|
103
|
+
"current",
|
|
104
|
+
"command",
|
|
105
|
+
"message"
|
|
106
|
+
],
|
|
107
|
+
"properties": {
|
|
108
|
+
"installed": {
|
|
109
|
+
"anyOf": [
|
|
110
|
+
{
|
|
111
|
+
"type": "string"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "null"
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"description": "The skill's version; null when it has none."
|
|
118
|
+
},
|
|
119
|
+
"current": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"command": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Updates the installed sideby skill (run it after the user agrees)."
|
|
125
|
+
},
|
|
126
|
+
"message": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"description": "Not the answer to the task: mention it briefly afterwards and offer to update."
|
|
71
133
|
}
|
|
72
134
|
},
|
|
73
135
|
"title": "sideby app install --json"
|
|
@@ -48,6 +48,68 @@
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
},
|
|
52
|
+
"_notice": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"update": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"required": [
|
|
58
|
+
"current",
|
|
59
|
+
"latest",
|
|
60
|
+
"command",
|
|
61
|
+
"message"
|
|
62
|
+
],
|
|
63
|
+
"properties": {
|
|
64
|
+
"current": {
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"latest": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
},
|
|
70
|
+
"command": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Run it after the user agrees: it shows the plan and exits 10; then --yes."
|
|
73
|
+
},
|
|
74
|
+
"message": {
|
|
75
|
+
"type": "string"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"skill": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"required": [
|
|
82
|
+
"installed",
|
|
83
|
+
"current",
|
|
84
|
+
"command",
|
|
85
|
+
"message"
|
|
86
|
+
],
|
|
87
|
+
"properties": {
|
|
88
|
+
"installed": {
|
|
89
|
+
"anyOf": [
|
|
90
|
+
{
|
|
91
|
+
"type": "string"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "null"
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"description": "The skill's version; null when it has none."
|
|
98
|
+
},
|
|
99
|
+
"current": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"command": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "Updates the installed sideby skill (run it after the user agrees)."
|
|
105
|
+
},
|
|
106
|
+
"message": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"description": "Not the answer to the task: mention it briefly afterwards and offer to update."
|
|
51
113
|
}
|
|
52
114
|
},
|
|
53
115
|
"title": "sideby app uninstall --json"
|