sideby 0.5.0 → 0.7.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 +5 -3
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +96 -2
- package/dist/src/cli/json-schemas.js +52 -0
- package/dist/src/cli/main.js +31 -2
- package/dist/src/core/config.d.ts +14 -0
- package/dist/src/core/config.js +9 -0
- package/dist/src/core/desktop-app.d.ts +96 -0
- package/dist/src/core/desktop-app.js +371 -0
- package/dist/src/core/doctor.d.ts +6 -1
- package/dist/src/core/doctor.js +19 -0
- package/dist/src/core/model-choice.d.ts +41 -0
- package/dist/src/core/model-choice.js +128 -0
- package/dist/src/core/tuning-apply.js +1 -1
- package/dist/src/families/claude/desktop.d.ts +6 -0
- package/dist/src/families/claude/desktop.js +28 -0
- package/dist/src/families/claude/index.js +2 -0
- package/dist/src/families/claude/model.d.ts +2 -0
- package/dist/src/families/claude/model.js +42 -0
- package/dist/src/families/claude/usage.d.ts +2 -0
- package/dist/src/families/claude/usage.js +13 -1
- package/dist/src/families/codex/desktop.d.ts +7 -0
- package/dist/src/families/codex/desktop.js +25 -0
- package/dist/src/families/codex/index.js +2 -0
- package/dist/src/families/codex/model.d.ts +2 -0
- package/dist/src/families/codex/model.js +59 -0
- package/dist/src/families/codex/rollout.d.ts +5 -0
- package/dist/src/families/codex/rollout.js +45 -3
- package/dist/src/families/desktop.d.ts +27 -0
- package/dist/src/families/desktop.js +9 -0
- package/dist/src/families/shared/model-args.d.ts +7 -0
- package/dist/src/families/shared/model-args.js +28 -0
- package/dist/src/panel/handler.d.ts +3 -0
- package/dist/src/panel/handler.js +63 -5
- package/dist/src/panel/i18n.d.ts +27 -0
- package/dist/src/panel/i18n.js +54 -0
- package/dist/src/panel/page-logic.js +2 -2
- package/dist/src/panel/page-script.js +143 -2
- package/dist/src/panel/page-styles.js +5 -1
- package/dist/src/runtime.d.ts +16 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/types.d.ts +29 -0
- package/package.json +3 -2
- package/schemas/desktop.json +80 -0
- package/schemas/list.json +20 -0
- package/schemas/model.json +169 -0
- package/schemas/quota.json +6 -0
- package/skills/sideby/SKILL.md +5 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ The agent checks Node and your Host CLIs, installs sideby and the skill, creates
|
|
|
55
55
|
|
|
56
56
|
## What it looks like
|
|
57
57
|
|
|
58
|
-
<img alt="Two terminals: the personal account and the work account running at the same time, then the local quota panel" src="https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif" width="1280">
|
|
58
|
+
<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">
|
|
59
59
|
|
|
60
60
|
<picture>
|
|
61
61
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png">
|
|
@@ -71,11 +71,13 @@ The agent checks Node and your Host CLIs, installs sideby and the skill, creates
|
|
|
71
71
|
| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |
|
|
72
72
|
| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |
|
|
73
73
|
| 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) |
|
|
74
|
+
| 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) |
|
|
74
75
|
| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |
|
|
75
76
|
| 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) |
|
|
76
77
|
| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |
|
|
77
78
|
| 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) |
|
|
78
|
-
| Open
|
|
79
|
+
| 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) |
|
|
80
|
+
| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |
|
|
79
81
|
| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |
|
|
80
82
|
| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |
|
|
81
83
|
|
|
@@ -113,7 +115,7 @@ Usage questions and ideas go in [Discussions](https://github.com/Adonis0123/side
|
|
|
113
115
|
- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes
|
|
114
116
|
- [Commands](docs/guide/commands.md): every command, exit codes and `--json`
|
|
115
117
|
- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions
|
|
116
|
-
- [Panel](docs/guide/panel.md): the panel,
|
|
118
|
+
- [Panel](docs/guide/panel.md): the panel, panel app and embedding
|
|
117
119
|
- [Plugins](docs/guide/plugins.md): write your own Family or launch hook
|
|
118
120
|
- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall
|
|
119
121
|
- [Changelog](https://github.com/Adonis0123/sideby/releases)
|
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261014T084812-64e992"}
|
|
@@ -43,6 +43,11 @@ export declare function cmdResume(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
43
43
|
/** `sideby next <family>`: recommend the Account with the most room, then start it (spec §3.13). */
|
|
44
44
|
export declare function cmdNext(a: ParsedArgs, io: Io): Promise<number>;
|
|
45
45
|
export declare function cmdDoctor(a: ParsedArgs, io: Io): Promise<number>;
|
|
46
|
+
/**
|
|
47
|
+
* `sideby model <account> [<model>|default] [--effort <level>|default] [--yes]` (spec §3.19, ADR-0014): shows the
|
|
48
|
+
* Account's model and effort; with a change, shows it (exit 10) and writes it with --yes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function cmdModel(a: ParsedArgs, io: Io): Promise<number>;
|
|
46
51
|
/** `sideby tune [account|family]`: estimated Compaction Threshold per model (spec §3.18). */
|
|
47
52
|
export declare function cmdTune(a: ParsedArgs, io: Io): Promise<number>;
|
|
48
53
|
export declare function cmdQuota(a: ParsedArgs, io: Io): Promise<number>;
|
|
@@ -52,6 +57,8 @@ export declare function cmdPlugins(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
52
57
|
export declare function cmdShellInit(a: ParsedArgs, io: Io): Promise<number>;
|
|
53
58
|
export declare function cmdUi(a: ParsedArgs, io: Io): Promise<number>;
|
|
54
59
|
export declare function cmdApp(a: ParsedArgs, io: Io): Promise<number>;
|
|
60
|
+
/** `sideby desktop <account>`: the Family's Desktop App as that Account (spec §3.20). */
|
|
61
|
+
export declare function cmdDesktop(a: ParsedArgs, io: Io): Promise<number>;
|
|
55
62
|
export declare function cmdAlias(a: ParsedArgs, io: Io): Promise<number>;
|
|
56
63
|
/** `sideby families [family]`: each Family's layout, sign-in and Shared Items, from its FamilyDef (spec §3.16). */
|
|
57
64
|
export declare function cmdFamilies(a: ParsedArgs, io: Io): Promise<number>;
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { aliasesByAccount } from '../core/accounts.js';
|
|
3
3
|
import { aliasRefs } from '../core/config.js';
|
|
4
|
+
import { openDesktopApp, withDesktopApps } from '../core/desktop-app.js';
|
|
4
5
|
import { UserError } from '../core/errors.js';
|
|
5
6
|
import { requestHandoff } from '../core/handoff-chain.js';
|
|
6
7
|
import { explain, readyDecision, runWithHandoff } from '../core/handoff-run.js';
|
|
7
8
|
import { configureHandoff, handoffHint, handoffStatus } from '../core/handoff-status.js';
|
|
8
9
|
import { runHost } from '../core/launch.js';
|
|
10
|
+
import { accountModel, setAccountModel } from '../core/model-choice.js';
|
|
9
11
|
import { resolvePaths, tildify } from '../core/paths.js';
|
|
10
12
|
import { USAGE_DAYS } from '../core/quota-levels.js';
|
|
11
13
|
import { SHELLS } from '../core/shell-init.js';
|
|
@@ -52,7 +54,8 @@ function tilde(text, home) {
|
|
|
52
54
|
export async function cmdList(a, io) {
|
|
53
55
|
const rt = await createRuntime();
|
|
54
56
|
const accounts = await rt.accounts();
|
|
55
|
-
const statuses = await Promise.all(accounts.map((x) => rt.status(x)));
|
|
57
|
+
const statuses = await withDesktopApps(rt, accounts, Promise.all(accounts.map((x) => rt.status(x))));
|
|
58
|
+
const hasDesktop = statuses.some((s) => s.desktopApp);
|
|
56
59
|
const families = await rt.familyInfo();
|
|
57
60
|
if (a.flags.has('json')) {
|
|
58
61
|
json(io, { families, accounts: statuses, pluginErrors: [...rt.pluginErrors] });
|
|
@@ -81,8 +84,9 @@ export async function cmdList(a, io) {
|
|
|
81
84
|
s.identity?.email ?? c.dim('—'),
|
|
82
85
|
s.model ?? c.dim('—'),
|
|
83
86
|
s.hostInstalled ? tildify(s.dir, home) : `${tildify(s.dir, home)} ${c.yellow('(host not installed)')}`,
|
|
87
|
+
...(hasDesktop ? [s.desktopApp?.running ? c.green('open') : c.dim('—')] : []),
|
|
84
88
|
]);
|
|
85
|
-
io.out(table(rows, ['ACCOUNT', 'TYPE', 'LOGIN', 'EMAIL', 'MODEL', 'DIR']));
|
|
89
|
+
io.out(table(rows, ['ACCOUNT', 'TYPE', 'LOGIN', 'EMAIL', 'MODEL', 'DIR', ...(hasDesktop ? ['DESKTOP'] : [])]));
|
|
86
90
|
io.out(c.dim('\nsideby run <account> · sideby new <family> <name> · sideby quota · sideby ui'));
|
|
87
91
|
}
|
|
88
92
|
for (const s of statuses)
|
|
@@ -401,6 +405,72 @@ async function cmdTuneChange(a, io, action) {
|
|
|
401
405
|
io.out(r.message);
|
|
402
406
|
return r.ok ? 0 : 1;
|
|
403
407
|
}
|
|
408
|
+
const FROM = {
|
|
409
|
+
sideby: 'set by sideby',
|
|
410
|
+
host: 'from the host config',
|
|
411
|
+
last: 'last session',
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* `sideby model <account> [<model>|default] [--effort <level>|default] [--yes]` (spec §3.19, ADR-0014): shows the
|
|
415
|
+
* Account's model and effort; with a change, shows it (exit 10) and writes it with --yes.
|
|
416
|
+
*/
|
|
417
|
+
export async function cmdModel(a, io) {
|
|
418
|
+
const [ref, model] = a.positionals;
|
|
419
|
+
if (!ref || a.positionals.length > 2 || a.flags.get('effort') === true)
|
|
420
|
+
throw new UsageError('usage: sideby model <account> [<model>|default] [--effort <level>|default] [--yes] [--json]');
|
|
421
|
+
const wantJson = a.flags.has('json');
|
|
422
|
+
const rt = await createRuntime();
|
|
423
|
+
const account = await rt.resolve(ref);
|
|
424
|
+
const effort = a.flags.get('effort');
|
|
425
|
+
const view = await accountModel(rt, account);
|
|
426
|
+
const shown = {
|
|
427
|
+
account: account.ref,
|
|
428
|
+
family: account.family,
|
|
429
|
+
settable: view.settable,
|
|
430
|
+
...(view.reason ? { reason: view.reason } : {}),
|
|
431
|
+
efforts: view.efforts,
|
|
432
|
+
models: view.models,
|
|
433
|
+
sideby: view.sideby,
|
|
434
|
+
host: view.host,
|
|
435
|
+
last: view.last,
|
|
436
|
+
...(view.model ? { model: view.model } : {}),
|
|
437
|
+
...(view.effort ? { effort: view.effort } : {}),
|
|
438
|
+
};
|
|
439
|
+
if (model === undefined && effort === undefined) {
|
|
440
|
+
if (a.flags.has('yes'))
|
|
441
|
+
throw new UsageError('--yes needs a change: sideby model <account> <model> or --effort <level>');
|
|
442
|
+
if (wantJson)
|
|
443
|
+
json(io, shown);
|
|
444
|
+
else {
|
|
445
|
+
io.out(account.ref);
|
|
446
|
+
io.out(` model ${view.model ? `${view.model.value} (${FROM[view.model.from]})` : 'the host default'}`);
|
|
447
|
+
io.out(` effort ${view.effort ? `${view.effort.value} (${FROM[view.effort.from]})` : 'the host default'}`);
|
|
448
|
+
if (view.settable)
|
|
449
|
+
io.out(c.dim(`Change it: sideby model ${account.ref} <model|default> --effort <${view.efforts.join('|')}|default>`));
|
|
450
|
+
else if (view.reason)
|
|
451
|
+
io.out(c.dim(view.reason));
|
|
452
|
+
}
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
const asChange = (v) => (v === undefined ? undefined : v === 'default' ? null : v);
|
|
456
|
+
const change = {
|
|
457
|
+
...(model !== undefined ? { model: asChange(model) } : {}),
|
|
458
|
+
...(typeof effort === 'string' ? { effort: asChange(effort) } : {}),
|
|
459
|
+
};
|
|
460
|
+
const yes = a.flags.has('yes');
|
|
461
|
+
const r = await setAccountModel(rt, account, change, { apply: yes });
|
|
462
|
+
const home = rt.paths.home;
|
|
463
|
+
const result = { ...r, file: tilde(r.file, home) };
|
|
464
|
+
if (wantJson)
|
|
465
|
+
json(io, { ...shown, change: result });
|
|
466
|
+
else if (!r.changed)
|
|
467
|
+
io.out('Nothing to change.');
|
|
468
|
+
else if (!yes)
|
|
469
|
+
io.out(`${r.diff}Run again with --yes to apply. It affects sessions started with sideby from now on.`);
|
|
470
|
+
else
|
|
471
|
+
io.out(`Saved. New sessions of ${account.ref} started with sideby use: ${r.args.join(' ') || 'the host defaults'}`);
|
|
472
|
+
return r.changed && !yes ? 10 : 0;
|
|
473
|
+
}
|
|
404
474
|
/** `sideby tune [account|family]`: estimated Compaction Threshold per model (spec §3.18). */
|
|
405
475
|
export async function cmdTune(a, io) {
|
|
406
476
|
if (a.positionals[0] === 'apply' || a.positionals[0] === 'undo')
|
|
@@ -830,6 +900,30 @@ export async function cmdApp(a, io) {
|
|
|
830
900
|
}
|
|
831
901
|
return r.skipped.length ? 1 : 0;
|
|
832
902
|
}
|
|
903
|
+
/** `sideby desktop <account>`: the Family's Desktop App as that Account (spec §3.20). */
|
|
904
|
+
export async function cmdDesktop(a, io) {
|
|
905
|
+
const [ref, ...extra] = a.positionals;
|
|
906
|
+
if (!ref || extra.length || a.rest.length)
|
|
907
|
+
throw new UsageError('usage: sideby desktop <account>');
|
|
908
|
+
const rt = await createRuntime();
|
|
909
|
+
const r = await openDesktopApp(rt, ref);
|
|
910
|
+
if (a.flags.has('json')) {
|
|
911
|
+
json(io, r);
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
const home = rt.paths.home;
|
|
915
|
+
if (r.action === 'started')
|
|
916
|
+
io.out(`${c.green('✓')} opened ${r.app} as ${c.bold(r.ref)}`);
|
|
917
|
+
else if (r.action === 'focused')
|
|
918
|
+
io.out(`${c.green('✓')} ${r.app} is already open as ${c.bold(r.ref)}; brought it to the front`);
|
|
919
|
+
if (r.appData)
|
|
920
|
+
io.out(c.dim(` its data: ${tildify(r.appData, home)}`));
|
|
921
|
+
for (const f of r.linked)
|
|
922
|
+
io.out(c.dim(` linked ${f} to the main account's`));
|
|
923
|
+
for (const n of r.notices)
|
|
924
|
+
io.err(c.cyan(n));
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
833
927
|
export async function cmdAlias(a, io) {
|
|
834
928
|
const [sub, alias, target, ...more] = a.positionals;
|
|
835
929
|
const usage = 'usage: sideby alias add <short-command> <account> [-- host args] | sideby alias rm <short-command>';
|
|
@@ -6,6 +6,11 @@ const Str = Type.String();
|
|
|
6
6
|
const Level = Type.Union([Type.Literal('ok'), Type.Literal('warn'), Type.Literal('fail')]);
|
|
7
7
|
const Kind = Type.Union([Type.Literal('subscription'), Type.Literal('api')]);
|
|
8
8
|
const Version = Type.Literal(1);
|
|
9
|
+
const Choice = Type.Object({ model: Type.Optional(Str), effort: Type.Optional(Str) });
|
|
10
|
+
const Shown = Type.Object({
|
|
11
|
+
value: Str,
|
|
12
|
+
from: Type.Union([Type.Literal('sideby'), Type.Literal('host'), Type.Literal('last')]),
|
|
13
|
+
});
|
|
9
14
|
const AppPlatform = Type.Union([Type.Literal('darwin'), Type.Literal('linux')]);
|
|
10
15
|
const Account = Type.Object({
|
|
11
16
|
family: Str,
|
|
@@ -32,6 +37,9 @@ const AccountStatus = Type.Intersect([
|
|
|
32
37
|
description: "Who the account is signed in as, from the host's login file: identity fields only, never a token (ADR-0003).",
|
|
33
38
|
})),
|
|
34
39
|
problems: Type.Optional(Type.Array(Str)),
|
|
40
|
+
desktopApp: Type.Optional(Type.Object({ app: Str, installed: Type.Boolean(), running: Type.Boolean() }, {
|
|
41
|
+
description: "The family's desktop app for this account (macOS, subscription accounts of claude and codex): whether it is installed and whether this account's own window runs now. Open it with `sideby desktop <account>`.",
|
|
42
|
+
})),
|
|
35
43
|
}),
|
|
36
44
|
]);
|
|
37
45
|
const FamilyInfo = Type.Object({
|
|
@@ -162,6 +170,8 @@ const Usage = Type.Union([
|
|
|
162
170
|
description: 'Tokens per local day (YYYY-MM-DD), oldest first, days without records as 0.',
|
|
163
171
|
})),
|
|
164
172
|
lastActivityAt: Type.Optional(Str),
|
|
173
|
+
lastModel: Type.Optional(Str),
|
|
174
|
+
lastEffort: Type.Optional(Str),
|
|
165
175
|
}),
|
|
166
176
|
Type.Object({ status: Type.Literal('unavailable'), reason: Str, detail: Type.Optional(Str) }),
|
|
167
177
|
]);
|
|
@@ -219,6 +229,29 @@ export const OUTPUT_SCHEMAS = {
|
|
|
219
229
|
})),
|
|
220
230
|
message: Type.Optional(Str),
|
|
221
231
|
}, { title: 'sideby tune apply|undo --json' }),
|
|
232
|
+
model: Type.Object({
|
|
233
|
+
schemaVersion: Version,
|
|
234
|
+
account: Str,
|
|
235
|
+
family: Str,
|
|
236
|
+
settable: Type.Boolean({
|
|
237
|
+
description: 'Whether sideby can set the model and effort for this account.',
|
|
238
|
+
}),
|
|
239
|
+
reason: Type.Optional(Str),
|
|
240
|
+
efforts: Type.Array(Str),
|
|
241
|
+
models: Type.Array(Str, { description: 'Model names to offer; any other name may be given.' }),
|
|
242
|
+
sideby: Choice,
|
|
243
|
+
host: Choice,
|
|
244
|
+
last: Choice,
|
|
245
|
+
model: Type.Optional(Shown),
|
|
246
|
+
effort: Type.Optional(Shown),
|
|
247
|
+
change: Type.Optional(Type.Object({
|
|
248
|
+
changed: Type.Boolean(),
|
|
249
|
+
applied: Type.Boolean(),
|
|
250
|
+
file: Str,
|
|
251
|
+
diff: Str,
|
|
252
|
+
args: Type.Array(Str, { description: 'The account arguments in the config after the change.' }),
|
|
253
|
+
})),
|
|
254
|
+
}, { title: 'sideby model --json' }),
|
|
222
255
|
tune: Type.Object({
|
|
223
256
|
schemaVersion: Version,
|
|
224
257
|
accounts: Type.Array(Type.Object({ ref: Str, family: Str, tuning: Tuning })),
|
|
@@ -365,6 +398,25 @@ export const OUTPUT_SCHEMAS = {
|
|
|
365
398
|
removed: Type.Array(Str),
|
|
366
399
|
skipped: Type.Array(Type.Object({ path: Str, reason: Str })),
|
|
367
400
|
}, { title: 'sideby app uninstall --json' }),
|
|
401
|
+
desktop: Type.Object({
|
|
402
|
+
schemaVersion: Version,
|
|
403
|
+
ref: Str,
|
|
404
|
+
family: Str,
|
|
405
|
+
app: Type.String({ description: 'The desktop app, for example `Claude Desktop`.' }),
|
|
406
|
+
appPath: Str,
|
|
407
|
+
action: Type.Union([Type.Literal('started'), Type.Literal('focused'), Type.Literal('already-running')], {
|
|
408
|
+
description: '`started`: a new window for this account; `focused`: its running window was brought to the front; `already-running`: it runs but could not be brought to the front.',
|
|
409
|
+
}),
|
|
410
|
+
appData: Type.Optional(Type.String({
|
|
411
|
+
description: "This account's app data folder; absent for the main account, which uses the app's default.",
|
|
412
|
+
})),
|
|
413
|
+
createdAppData: Type.Boolean(),
|
|
414
|
+
linked: Type.Array(Str, {
|
|
415
|
+
description: "Files linked into a new app data folder from the main account's (claude_desktop_config.json).",
|
|
416
|
+
}),
|
|
417
|
+
command: Type.Optional(Type.Array(Str, { description: 'The `open` command sideby ran.' })),
|
|
418
|
+
notices: Type.Array(Str),
|
|
419
|
+
}, { title: 'sideby desktop --json' }),
|
|
368
420
|
error: Type.Object({
|
|
369
421
|
schemaVersion: Version,
|
|
370
422
|
error: Type.String({ description: 'What went wrong and what to do next.' }),
|
package/dist/src/cli/main.js
CHANGED
|
@@ -32,6 +32,8 @@ Usage:
|
|
|
32
32
|
sideby quota teardown claude undo it, restoring the original file
|
|
33
33
|
sideby tune [account|family] estimate the cheapest auto-compact threshold per model
|
|
34
34
|
from your last 7 days of sessions (read-only)
|
|
35
|
+
sideby model <account> [<model>|default] [--effort <level>|default] [--yes]
|
|
36
|
+
show or set the model and effort sideby starts this account with
|
|
35
37
|
sideby handoff status what Auto Handoff still needs on this machine
|
|
36
38
|
sideby handoff enable [--yes] turn it on (shows the config change first); --same-family,
|
|
37
39
|
--order claude=codex001,cc002, --threshold n, --prepare-at n
|
|
@@ -43,6 +45,8 @@ Usage:
|
|
|
43
45
|
sideby ui [--port n] [--no-open] open the local panel
|
|
44
46
|
sideby ui --background same, without a terminal: keeps running after this command exits
|
|
45
47
|
sideby ui --stop stop the background panel
|
|
48
|
+
sideby desktop <account> open the desktop app (Claude Desktop, the Codex app) as this
|
|
49
|
+
account, next to the others (macOS)
|
|
46
50
|
sideby app install [--url <url>] add a sideby app that opens the panel (macOS, Linux)
|
|
47
51
|
sideby app uninstall remove that app
|
|
48
52
|
sideby shell-init zsh|bash print shell functions, e.g. eval "$(sideby shell-init zsh)"
|
|
@@ -51,8 +55,9 @@ Usage:
|
|
|
51
55
|
sideby plugins list loaded plugins and load errors
|
|
52
56
|
|
|
53
57
|
Options:
|
|
54
|
-
--json machine-readable output (list, new, next, alias, doctor, quota, tune, handoff, families, plugins, app
|
|
55
|
-
|
|
58
|
+
--json machine-readable output (list, new, next, alias, doctor, quota, tune, handoff, families, plugins, app,
|
|
59
|
+
desktop)
|
|
60
|
+
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop
|
|
56
61
|
-v, --version show the version
|
|
57
62
|
|
|
58
63
|
An <account> is <family>:<name> (claude:work) or just <name> when it is unique.
|
|
@@ -123,6 +128,24 @@ Examples:
|
|
|
123
128
|
sideby doctor --json
|
|
124
129
|
sideby doctor claude:008 --fix
|
|
125
130
|
Exit: 0 when nothing fails (warnings allowed); 1 when a check fails.`,
|
|
131
|
+
desktop: `sideby desktop <account> [--json]
|
|
132
|
+
|
|
133
|
+
Open the host's desktop app as one account: Claude Desktop for claude accounts, the Codex app (ChatGPT.app) for codex
|
|
134
|
+
accounts. Each account gets its own window, side by side with the others; macOS only for now.
|
|
135
|
+
|
|
136
|
+
The main account opens the app as usual. Any other subscription account opens a new window with that account's
|
|
137
|
+
directory (CLAUDE_CONFIG_DIR, CODEX_HOME) and its own app data folder,
|
|
138
|
+
~/Library/Application Support/sideby/<family>/<name>/. When that account's window is already open, sideby brings it
|
|
139
|
+
to the front instead of starting another one. API accounts are refused: the apps sign in with a subscription only.
|
|
140
|
+
|
|
141
|
+
sideby never copies or changes the app, and never closes a window. The first time, sign in inside Claude Desktop;
|
|
142
|
+
close other Claude Desktop windows while you do, because the sign-in may come back to one of them.
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
sideby desktop claude:work
|
|
146
|
+
sideby desktop cc008 --json
|
|
147
|
+
Exit: 0 opened or brought to the front; 1 not macOS, no desktop app for that host, API account, app not installed,
|
|
148
|
+
or the app did not open; 2 malformed command line.`,
|
|
126
149
|
families: `sideby families [family] [--json]
|
|
127
150
|
|
|
128
151
|
For each host (built in or from a plugin): where its accounts live, the variable that selects one, how to sign
|
|
@@ -154,12 +177,14 @@ const COMMANDS = {
|
|
|
154
177
|
doctor: { flags: ['fix', 'force', 'json'] },
|
|
155
178
|
quota: { flags: ['json', 'yes'] },
|
|
156
179
|
tune: { flags: ['json', 'yes'] },
|
|
180
|
+
model: { flags: ['json', 'yes'], valued: ['effort'] },
|
|
157
181
|
handoff: {
|
|
158
182
|
flags: ['json', 'yes', 'same-family', 'no-same-family'],
|
|
159
183
|
valued: ['brief', 'order', 'prepare-at', 'threshold', 'wait-minutes', 'countdown'],
|
|
160
184
|
},
|
|
161
185
|
ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
|
|
162
186
|
app: { flags: ['json'], valued: ['url'] },
|
|
187
|
+
desktop: { flags: ['json'] },
|
|
163
188
|
'shell-init': { flags: ['write'] },
|
|
164
189
|
plugins: { flags: ['json'] },
|
|
165
190
|
families: { flags: ['json'] },
|
|
@@ -255,12 +280,16 @@ export async function main(argv) {
|
|
|
255
280
|
return await cmds.cmdQuota(parsed, io);
|
|
256
281
|
case 'tune':
|
|
257
282
|
return await cmds.cmdTune(parsed, io);
|
|
283
|
+
case 'model':
|
|
284
|
+
return await cmds.cmdModel(parsed, io);
|
|
258
285
|
case 'handoff':
|
|
259
286
|
return await cmds.cmdHandoff(parsed, io);
|
|
260
287
|
case 'ui':
|
|
261
288
|
return await cmds.cmdUi(parsed, io);
|
|
262
289
|
case 'app':
|
|
263
290
|
return await cmds.cmdApp(parsed, io);
|
|
291
|
+
case 'desktop':
|
|
292
|
+
return await cmds.cmdDesktop(parsed, io);
|
|
264
293
|
case 'shell-init':
|
|
265
294
|
return await cmds.cmdShellInit(parsed, io);
|
|
266
295
|
case 'plugins':
|
|
@@ -85,6 +85,20 @@ export declare function editConfigHandoff(file: string, edit: (current: HandoffC
|
|
|
85
85
|
before: HandoffConfig | undefined;
|
|
86
86
|
after: HandoffConfig | undefined;
|
|
87
87
|
}>;
|
|
88
|
+
export type AccountsConfig = NonNullable<Config['accounts']>;
|
|
89
|
+
/**
|
|
90
|
+
* `sideby model` (spec §3.19): changes the config `accounts` key with `edit` (see `editConfigKey` for how the file is
|
|
91
|
+
* written). With `dryRun`, shows the change without writing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function editConfigAccounts(file: string,
|
|
94
|
+
/** Returns null to leave the file as it is. */
|
|
95
|
+
edit: (current: AccountsConfig) => AccountsConfig | null, opts?: {
|
|
96
|
+
dryRun?: boolean;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
changed: boolean;
|
|
99
|
+
before: AccountsConfig | undefined;
|
|
100
|
+
after: AccountsConfig | undefined;
|
|
101
|
+
}>;
|
|
88
102
|
/**
|
|
89
103
|
* Adds `alias → ref` to the config file's `aliases`, as `{ account, args }` when `args` is not empty (see
|
|
90
104
|
* `editConfigAliases` for how the file is written). Refuses an invalid alias, or one that already starts another
|
package/dist/src/core/config.js
CHANGED
|
@@ -224,6 +224,15 @@ async function editConfigAliases(file, retry, edit) {
|
|
|
224
224
|
export function editConfigHandoff(file, edit, opts = {}) {
|
|
225
225
|
return editConfigKey(file, 'handoff', 'run the command', (current) => edit(current ?? {}), opts);
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* `sideby model` (spec §3.19): changes the config `accounts` key with `edit` (see `editConfigKey` for how the file is
|
|
229
|
+
* written). With `dryRun`, shows the change without writing.
|
|
230
|
+
*/
|
|
231
|
+
export function editConfigAccounts(file,
|
|
232
|
+
/** Returns null to leave the file as it is. */
|
|
233
|
+
edit, opts = {}) {
|
|
234
|
+
return editConfigKey(file, 'accounts', 'run the command', (current) => edit(current ?? {}), opts);
|
|
235
|
+
}
|
|
227
236
|
/**
|
|
228
237
|
* Adds `alias → ref` to the config file's `aliases`, as `{ account, args }` when `args` is not empty (see
|
|
229
238
|
* `editConfigAliases` for how the file is written). Refuses an invalid alias, or one that already starts another
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type DesktopAppDef } from '../families/desktop.ts';
|
|
2
|
+
import type { LoadedPlugin } from '../plugins/loader.ts';
|
|
3
|
+
import type { Runtime } from '../runtime.ts';
|
|
4
|
+
import type { Account, CheckResult, Env } from '../types.ts';
|
|
5
|
+
export interface ExecResult {
|
|
6
|
+
code: number;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
/** What opening a Desktop App needs from the machine; tests replace all of it. */
|
|
11
|
+
export interface DesktopDeps {
|
|
12
|
+
platform: NodeJS.Platform;
|
|
13
|
+
/** Folders searched for the app, in order (normally /Applications and ~/Applications). */
|
|
14
|
+
appDirs: readonly string[];
|
|
15
|
+
/** Runs a program and resolves with its exit code and output; never rejects. */
|
|
16
|
+
exec(cmd: string, args: readonly string[], env?: Env): Promise<ExecResult>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Internal: makes every Desktop App call made with this Runtime use `deps` instead of the machine. Only
|
|
20
|
+
* `createRuntime` calls it, from its `desktop` option, which is the one way to replace the machine (tests).
|
|
21
|
+
*/
|
|
22
|
+
export declare function bindDesktopDeps(rt: Runtime, deps: Partial<DesktopDeps>): void;
|
|
23
|
+
/** The Desktop App of a Family, only when a built-in Plugin provides that Family (a plugin may not claim one). */
|
|
24
|
+
export declare function desktopAppOf(plugins: readonly LoadedPlugin[], familyId: string): DesktopAppDef | undefined;
|
|
25
|
+
/** Where a non-main Account's App Data lives: `~/Library/Application Support/sideby/<family>/<name>`. */
|
|
26
|
+
export declare function appDataDir(home: string, account: Pick<Account, 'family' | 'name'>): string;
|
|
27
|
+
export interface FoundApp {
|
|
28
|
+
path: string;
|
|
29
|
+
/** `Contents/MacOS/<executable>`: the app's main process runs this. */
|
|
30
|
+
executable: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Finds the app by bundle id: the usual names in the app folders first, then (with `spotlight`) Spotlight. Listing
|
|
34
|
+
* Accounts leaves Spotlight out to stay fast; opening one asks it too.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findApp(def: DesktopAppDef, deps: DesktopDeps, opts?: {
|
|
37
|
+
spotlight?: boolean;
|
|
38
|
+
}): Promise<FoundApp | null>;
|
|
39
|
+
export interface Proc {
|
|
40
|
+
pid: number;
|
|
41
|
+
command: string;
|
|
42
|
+
}
|
|
43
|
+
/** The running instance of this Account: the one with its App Data, or for the Main Account the one without any. */
|
|
44
|
+
export declare function instanceOf(app: FoundApp, appData: string | null, procs: readonly Proc[]): Proc | undefined;
|
|
45
|
+
export interface DesktopOpenResult {
|
|
46
|
+
ref: string;
|
|
47
|
+
family: string;
|
|
48
|
+
/** The app as messages name it, for example `Claude Desktop`. */
|
|
49
|
+
app: string;
|
|
50
|
+
appPath: string;
|
|
51
|
+
/** `started`: a new instance; `focused`: the running one is in front; `already-running`: it runs but stayed behind. */
|
|
52
|
+
action: 'started' | 'focused' | 'already-running';
|
|
53
|
+
/** The Account's App Data; absent for the Main Account, which uses the app's default. */
|
|
54
|
+
appData?: string;
|
|
55
|
+
/** True when this call created the App Data folder. */
|
|
56
|
+
createdAppData: boolean;
|
|
57
|
+
/** Files linked into a new App Data from the Main Account's default one. */
|
|
58
|
+
linked: string[];
|
|
59
|
+
/** The `open` arguments, when sideby started the app. */
|
|
60
|
+
command?: string[];
|
|
61
|
+
/** What the user should know: sign in first, close other windows while signing in. */
|
|
62
|
+
notices: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Opens the Account's Desktop App, or brings its running instance to the front. Never starts a second instance of the
|
|
66
|
+
* same Account, never ends a process, never touches the app bundle or the Host account directory.
|
|
67
|
+
*/
|
|
68
|
+
export declare function openDesktopApp(rt: Runtime, ref: string): Promise<DesktopOpenResult>;
|
|
69
|
+
export interface DesktopAppStatus {
|
|
70
|
+
/** The app as messages name it. */
|
|
71
|
+
app: string;
|
|
72
|
+
installed: boolean;
|
|
73
|
+
/** That Account's own instance runs now. */
|
|
74
|
+
running: boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Desktop App state per Account ref for `sideby list` and the Panel: only on macOS, only Subscription Accounts of a
|
|
78
|
+
* Family that has one. One process listing for all of them; never throws.
|
|
79
|
+
*/
|
|
80
|
+
export declare function desktopStatuses(rt: Runtime, accounts: readonly Account[]): Promise<Map<string, DesktopAppStatus>>;
|
|
81
|
+
/**
|
|
82
|
+
* Adds `desktopApp` to each status whose Account has one, for `sideby list` and the Panel. The process listing runs
|
|
83
|
+
* while `statuses` is still being built; the order of the statuses is kept.
|
|
84
|
+
*/
|
|
85
|
+
export declare function withDesktopApps<T extends {
|
|
86
|
+
ref: string;
|
|
87
|
+
}>(rt: Runtime, accounts: readonly Account[], statuses: Promise<readonly T[]>): Promise<(T & {
|
|
88
|
+
desktopApp?: DesktopAppStatus;
|
|
89
|
+
})[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Doctor's extra checks for an Account's App Data (spec §3.20): the Desktop App's shared files under `link` rules,
|
|
92
|
+
* only when that Account's App Data folder exists, never for the Main Account (it uses the app's default) or an API
|
|
93
|
+
* Account. Results are named `app-data/<path>` and are not counted in `shared`. The App Data is the repair boundary:
|
|
94
|
+
* a fix may only add a missing link inside it, never replace a real file.
|
|
95
|
+
*/
|
|
96
|
+
export declare function checkAppData(plugins: readonly LoadedPlugin[], home: string, account: Account): Promise<CheckResult[]>;
|