sideby 0.9.3 → 0.11.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 +23 -16
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +9 -1
- package/dist/src/cli/commands.js +145 -5
- package/dist/src/cli/json-schemas.js +55 -1
- package/dist/src/cli/main.js +23 -7
- package/dist/src/core/app-profiles.d.ts +50 -0
- package/dist/src/core/app-profiles.js +206 -0
- package/dist/src/core/brand-mark.d.ts +29 -0
- package/dist/src/core/brand-mark.js +42 -0
- package/dist/src/core/claude-settings-file.d.ts +17 -0
- package/dist/src/core/claude-settings-file.js +53 -0
- package/dist/src/core/desktop-app.d.ts +18 -1
- package/dist/src/core/desktop-app.js +22 -12
- package/dist/src/core/handoff-chain.d.ts +16 -2
- package/dist/src/core/handoff-chain.js +19 -6
- package/dist/src/core/handoff-run.js +43 -18
- package/dist/src/core/handoff-select.d.ts +4 -0
- package/dist/src/core/handoff-select.js +3 -2
- package/dist/src/core/remote-control.d.ts +45 -0
- package/dist/src/core/remote-control.js +157 -0
- package/dist/src/core/tuning-apply.js +2 -49
- package/dist/src/desktop/icon.js +33 -33
- package/dist/src/families/claude/handoff.js +10 -0
- package/dist/src/families/desktop.d.ts +25 -0
- package/dist/src/families/desktop.js +8 -2
- package/dist/src/families/grok/bot.d.ts +8 -0
- package/dist/src/families/grok/bot.js +18 -0
- package/dist/src/handoff/hook.js +21 -4
- package/dist/src/handoff/messages.d.ts +4 -0
- package/dist/src/handoff/messages.js +12 -0
- package/dist/src/panel/handler.d.ts +9 -0
- package/dist/src/panel/handler.js +61 -2
- package/dist/src/panel/i18n.d.ts +46 -0
- package/dist/src/panel/i18n.js +92 -0
- package/dist/src/panel/page-script.js +205 -5
- package/dist/src/panel/page-styles.js +15 -0
- package/dist/src/panel/page.js +2 -1
- package/dist/src/runtime.d.ts +10 -1
- package/dist/src/runtime.js +17 -0
- package/dist/src/types.d.ts +5 -0
- package/package.json +2 -2
- package/schemas/desktop-remove.json +33 -0
- package/schemas/desktop.json +2 -1
- package/schemas/handoff-ready.json +4 -0
- package/schemas/list.json +47 -0
- package/schemas/remote-control.json +125 -0
- package/skills/sideby/SKILL.md +4 -1
- package/skills/sideby/references/handoff.md +1 -1
package/README.md
CHANGED
|
@@ -16,28 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
<p align="center">English | <a href="README.zh-CN.md">中文</a></p>
|
|
18
18
|
|
|
19
|
-
## Why not just an alias?
|
|
20
|
-
|
|
21
|
-
`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)
|
|
22
|
-
|
|
23
|
-
| | At the same time | Skills, hooks, settings | Quota and usage | Setup |
|
|
24
|
-
|---|---|---|---|---|
|
|
25
|
-
| `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 |
|
|
26
|
-
| [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 |
|
|
27
|
-
| 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 |
|
|
28
|
-
|
|
29
|
-
cc-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.
|
|
30
|
-
|
|
31
19
|
## Quick start
|
|
32
20
|
|
|
33
21
|
Requires 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.
|
|
34
22
|
|
|
35
23
|
```sh
|
|
36
24
|
npm i -g sideby
|
|
37
|
-
sideby new claude work
|
|
38
|
-
sideby login claude:work
|
|
39
|
-
sideby run work
|
|
40
|
-
sideby
|
|
25
|
+
sideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks
|
|
26
|
+
sideby login claude:work # sign in once
|
|
27
|
+
sideby run work # Claude Code on the work account
|
|
28
|
+
sideby quota setup claude --yes # Claude quota in the panel; undo: sideby quota teardown claude
|
|
29
|
+
sideby ui # every account's quota at http://127.0.0.1:17420
|
|
41
30
|
```
|
|
42
31
|
|
|
43
32
|
`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`.
|
|
@@ -55,6 +44,10 @@ The agent checks Node and your Host CLIs, installs sideby and the skill, creates
|
|
|
55
44
|
|
|
56
45
|
## What it looks like
|
|
57
46
|
|
|
47
|
+
The 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.
|
|
48
|
+
|
|
49
|
+
https://github.com/user-attachments/assets/3a470c03-9009-4f1d-b7f6-8b4625d74958
|
|
50
|
+
|
|
58
51
|
<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
52
|
|
|
60
53
|
<picture>
|
|
@@ -63,6 +56,18 @@ The agent checks Node and your Host CLIs, installs sideby and the skill, creates
|
|
|
63
56
|
<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">
|
|
64
57
|
</picture>
|
|
65
58
|
|
|
59
|
+
## Why not just an alias?
|
|
60
|
+
|
|
61
|
+
`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)
|
|
62
|
+
|
|
63
|
+
| | At the same time | Skills, hooks, settings | Quota and usage | Setup |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| `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 |
|
|
66
|
+
| [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 |
|
|
67
|
+
| 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 |
|
|
68
|
+
|
|
69
|
+
cc-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.
|
|
70
|
+
|
|
66
71
|
## What else it does
|
|
67
72
|
|
|
68
73
|
| You want to | Run | Guide |
|
|
@@ -72,11 +77,13 @@ The agent checks Node and your Host CLIs, installs sideby and the skill, creates
|
|
|
72
77
|
| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |
|
|
73
78
|
| 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
79
|
| 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) |
|
|
80
|
+
| 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) |
|
|
75
81
|
| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |
|
|
76
82
|
| 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) |
|
|
77
83
|
| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |
|
|
78
84
|
| 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) |
|
|
79
85
|
| 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) |
|
|
86
|
+
| 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) |
|
|
80
87
|
| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |
|
|
81
88
|
| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |
|
|
82
89
|
| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |
|
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261016T040419-834a8d"}
|
|
@@ -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 remote-control [off|undo] [--yes] [--json]` (spec §3.22, ADR-0017): whether Claude sessions connect to
|
|
48
|
+
* claude.ai by themselves, and turning that off for every Account (a preview with exit 10 until --yes).
|
|
49
|
+
*/
|
|
50
|
+
export declare function cmdRemoteControl(a: ParsedArgs, io: Io): Promise<number>;
|
|
46
51
|
/**
|
|
47
52
|
* `sideby model <account> [<model>|default] [--effort <level>|default] [--yes]` (spec §3.19, ADR-0014): shows the
|
|
48
53
|
* Account's model and effort; with a change, shows it (exit 10) and writes it with --yes.
|
|
@@ -57,7 +62,10 @@ export declare function cmdPlugins(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
57
62
|
export declare function cmdShellInit(a: ParsedArgs, io: Io): Promise<number>;
|
|
58
63
|
export declare function cmdUi(a: ParsedArgs, io: Io): Promise<number>;
|
|
59
64
|
export declare function cmdApp(a: ParsedArgs, io: Io): Promise<number>;
|
|
60
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* `sideby desktop <account>`: the Family's Desktop App as that Account (spec §3.20); `sideby desktop grok-bot:<name>`:
|
|
67
|
+
* a Grok Bot window, new or open, and with `--remove` its removal (ADR-0019).
|
|
68
|
+
*/
|
|
61
69
|
export declare function cmdDesktop(a: ParsedArgs, io: Io): Promise<number>;
|
|
62
70
|
export declare function cmdAlias(a: ParsedArgs, io: Io): Promise<number>;
|
|
63
71
|
/** `sideby families [family]`: each Family's layout, sign-in and Shared Items, from its FamilyDef (spec §3.16). */
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { aliasesByAccount } from '../core/accounts.js';
|
|
3
|
+
import { listAppProfiles, openAppProfile, parseProfileRef, removeAppProfile } from '../core/app-profiles.js';
|
|
3
4
|
import { aliasRefs } from '../core/config.js';
|
|
4
5
|
import { openDesktopApp, withDesktopApps } from '../core/desktop-app.js';
|
|
5
6
|
import { UserError } from '../core/errors.js';
|
|
@@ -10,6 +11,7 @@ import { runHost } from '../core/launch.js';
|
|
|
10
11
|
import { accountModel, setAccountModel } from '../core/model-choice.js';
|
|
11
12
|
import { resolvePaths, tildify } from '../core/paths.js';
|
|
12
13
|
import { USAGE_DAYS } from '../core/quota-levels.js';
|
|
14
|
+
import { groupsToTurnOff, groupsToUndo, } from '../core/remote-control.js';
|
|
13
15
|
import { SHELLS } from '../core/shell-init.js';
|
|
14
16
|
import { suggestAlias, suggestName } from '../panel/page-logic.js';
|
|
15
17
|
import { createRuntime, UnknownFamilyError } from '../runtime.js';
|
|
@@ -54,11 +56,14 @@ function tilde(text, home) {
|
|
|
54
56
|
export async function cmdList(a, io) {
|
|
55
57
|
const rt = await createRuntime();
|
|
56
58
|
const accounts = await rt.accounts();
|
|
57
|
-
const statuses = await
|
|
59
|
+
const [statuses, appProfiles] = await Promise.all([
|
|
60
|
+
withDesktopApps(rt, accounts, Promise.all(accounts.map((x) => rt.status(x)))),
|
|
61
|
+
listAppProfiles(rt),
|
|
62
|
+
]);
|
|
58
63
|
const hasDesktop = statuses.some((s) => s.desktopApp);
|
|
59
64
|
const families = await rt.familyInfo();
|
|
60
65
|
if (a.flags.has('json')) {
|
|
61
|
-
json(io, { families, accounts: statuses, pluginErrors: [...rt.pluginErrors] });
|
|
66
|
+
json(io, { families, accounts: statuses, appProfiles, pluginErrors: [...rt.pluginErrors] });
|
|
62
67
|
return 0;
|
|
63
68
|
}
|
|
64
69
|
const home = rt.paths.home;
|
|
@@ -93,6 +98,25 @@ export async function cmdList(a, io) {
|
|
|
93
98
|
io.out(c.dim('\n= <account>: signed in as the same login as that account, so they share one quota.'));
|
|
94
99
|
io.out(c.dim('\nsideby run <account> · sideby new <family> <name> · sideby quota · sideby ui'));
|
|
95
100
|
}
|
|
101
|
+
// App Profiles (ADR-0019): windows of an app that signs in apart from the Accounts above.
|
|
102
|
+
for (const app of new Set(appProfiles.map((p) => p.app))) {
|
|
103
|
+
const list = appProfiles.filter((p) => p.app === app);
|
|
104
|
+
io.out(`\n${c.bold(app)} ${c.dim('(signs in on its own, not with the accounts above)')}`);
|
|
105
|
+
io.out(table(list.map((p) => [
|
|
106
|
+
c.bold(p.ref),
|
|
107
|
+
p.running ? c.green('open') : c.dim('—'),
|
|
108
|
+
p.signedIn === true
|
|
109
|
+
? c.green('signed in')
|
|
110
|
+
: p.signedIn === false
|
|
111
|
+
? c.yellow('not signed in')
|
|
112
|
+
: c.dim('?'),
|
|
113
|
+
p.appData ? tildify(p.appData, home) : c.dim('default'),
|
|
114
|
+
]), ['WINDOW', 'STATE', 'SIGN-IN', 'DATA']));
|
|
115
|
+
if (!list[0]?.installed)
|
|
116
|
+
io.out(c.yellow(`${app} is not installed; these windows open again once it is`));
|
|
117
|
+
const id = list[0]?.ref.slice(0, list[0].ref.indexOf(':'));
|
|
118
|
+
io.out(c.dim(`sideby desktop ${id}:<name> opens one (a new name makes a new window)`));
|
|
119
|
+
}
|
|
96
120
|
for (const s of statuses)
|
|
97
121
|
for (const p of s.problems ?? [])
|
|
98
122
|
io.err(c.yellow(`${s.ref}: ${p}`));
|
|
@@ -413,6 +437,100 @@ async function cmdTuneChange(a, io, action) {
|
|
|
413
437
|
io.out(r.message);
|
|
414
438
|
return r.ok ? 0 : 1;
|
|
415
439
|
}
|
|
440
|
+
const RC_STATE = {
|
|
441
|
+
on: 'on',
|
|
442
|
+
default: 'default',
|
|
443
|
+
off: 'off',
|
|
444
|
+
missing: 'no file',
|
|
445
|
+
blocked: 'cannot change',
|
|
446
|
+
};
|
|
447
|
+
function rcGroupLine(g, home) {
|
|
448
|
+
const state = RC_STATE[g.state].padEnd(13);
|
|
449
|
+
const paint = g.state === 'on'
|
|
450
|
+
? c.yellow
|
|
451
|
+
: g.state === 'off'
|
|
452
|
+
? c.green
|
|
453
|
+
: g.state === 'default'
|
|
454
|
+
? (x) => x
|
|
455
|
+
: c.dim;
|
|
456
|
+
const where = g.reason ? tilde(g.reason, home) : tildify(g.file, home);
|
|
457
|
+
return ` ${paint(state)} ${c.bold(g.accounts.join(', '))} ${c.dim(where)}`;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* `sideby remote-control [off|undo] [--yes] [--json]` (spec §3.22, ADR-0017): whether Claude sessions connect to
|
|
461
|
+
* claude.ai by themselves, and turning that off for every Account (a preview with exit 10 until --yes).
|
|
462
|
+
*/
|
|
463
|
+
export async function cmdRemoteControl(a, io) {
|
|
464
|
+
const [action, extra] = a.positionals;
|
|
465
|
+
if (extra !== undefined || (action !== undefined && action !== 'off' && action !== 'undo'))
|
|
466
|
+
throw new UsageError('usage: sideby remote-control [off|undo] [--yes] [--json]');
|
|
467
|
+
const wantJson = a.flags.has('json');
|
|
468
|
+
const rt = await createRuntime();
|
|
469
|
+
const home = rt.paths.home;
|
|
470
|
+
const show = (gs) => gs.map((g) => ({ ...g, file: tildify(g.file, home) }));
|
|
471
|
+
const out = (data) => json(io, { action: action ?? 'status', ...data });
|
|
472
|
+
const groups = await rt.remoteControl();
|
|
473
|
+
if (!action) {
|
|
474
|
+
if (wantJson) {
|
|
475
|
+
out({ groups: show(groups) });
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
if (groups.length === 0) {
|
|
479
|
+
io.out('No Claude accounts found.');
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
io.out('Remote Control: whether Claude sessions connect to claude.ai/code and the Claude app by themselves');
|
|
483
|
+
for (const g of groups)
|
|
484
|
+
io.out(rcGroupLine(g, home));
|
|
485
|
+
io.out(c.dim(' on: every session connects; default: Claude Code or your organization decides; off: only when you run /remote-control'));
|
|
486
|
+
if (groupsToTurnOff(groups).length)
|
|
487
|
+
io.out(c.cyan('\nRun `sideby remote-control off` so no session connects by itself (it shows the change first).'));
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
const todo = action === 'off' ? groupsToTurnOff(groups) : groupsToUndo(groups);
|
|
491
|
+
if (todo.length === 0) {
|
|
492
|
+
const r = action === 'off' ? await rt.remoteControlOff() : await rt.remoteControlUndo();
|
|
493
|
+
if (wantJson)
|
|
494
|
+
out({ applied: false, groups: show(groups), message: r.message });
|
|
495
|
+
else
|
|
496
|
+
io.out(tilde(r.message, home));
|
|
497
|
+
return r.ok ? 0 : 1;
|
|
498
|
+
}
|
|
499
|
+
if (!a.flags.has('yes')) {
|
|
500
|
+
if (wantJson)
|
|
501
|
+
out({
|
|
502
|
+
applied: false,
|
|
503
|
+
groups: show(todo),
|
|
504
|
+
message: `Run again with --yes to ${action === 'off' ? 'turn it off' : 'undo'}.`,
|
|
505
|
+
});
|
|
506
|
+
else {
|
|
507
|
+
io.out(action === 'off'
|
|
508
|
+
? 'Turn off Remote Control at startup?'
|
|
509
|
+
: 'Put back the Remote Control setting sideby replaced?');
|
|
510
|
+
for (const g of todo) {
|
|
511
|
+
const to = action === 'off' ? 'false' : 'what it was before';
|
|
512
|
+
const from = g.state === 'default' ? 'not set' : g.state === 'on' ? 'true' : 'false';
|
|
513
|
+
io.out(` ${tildify(g.file, home)}: remoteControlAtStartup ${from} -> ${to}`);
|
|
514
|
+
io.out(c.dim(` used by ${g.accounts.join(', ')}`));
|
|
515
|
+
}
|
|
516
|
+
io.out(action === 'off'
|
|
517
|
+
? 'Sessions already running stay connected until you restart them. You can still start it in a session with /remote-control, and undo this with `sideby remote-control undo`.'
|
|
518
|
+
: 'New sessions go back to the previous setting.');
|
|
519
|
+
io.out(`Run again with --yes to ${action === 'off' ? 'turn it off' : 'undo'}.`);
|
|
520
|
+
}
|
|
521
|
+
return 10;
|
|
522
|
+
}
|
|
523
|
+
const r = action === 'off' ? await rt.remoteControlOff() : await rt.remoteControlUndo();
|
|
524
|
+
const results = r.results.map((x) => ({ ...x, file: tildify(x.file, home) }));
|
|
525
|
+
if (wantJson)
|
|
526
|
+
out({ applied: r.ok, results, message: r.message });
|
|
527
|
+
else {
|
|
528
|
+
for (const x of results)
|
|
529
|
+
io.out(` ${x.ok ? c.green('ok') : c.red('failed')} ${x.file}: ${x.message}`);
|
|
530
|
+
io.out(r.message);
|
|
531
|
+
}
|
|
532
|
+
return r.ok ? 0 : 1;
|
|
533
|
+
}
|
|
416
534
|
const FROM = {
|
|
417
535
|
sideby: 'set by sideby',
|
|
418
536
|
host: 'from the host config',
|
|
@@ -765,7 +883,10 @@ export async function cmdHandoff(a, io) {
|
|
|
765
883
|
requested: true,
|
|
766
884
|
...(next ? { next } : {}),
|
|
767
885
|
...(request.brief ? { brief: request.brief } : {}),
|
|
886
|
+
...(request.now ? { now: true } : {}),
|
|
768
887
|
});
|
|
888
|
+
else if (request.now)
|
|
889
|
+
io.out(`This session was waiting for its quota; ${next ?? 'the next account'} takes over now.`);
|
|
769
890
|
else
|
|
770
891
|
io.out(`${next ? `${next} takes over` : 'The next account starts'} when this turn ends; finish the turn now.`);
|
|
771
892
|
return 0;
|
|
@@ -924,13 +1045,32 @@ export async function cmdApp(a, io) {
|
|
|
924
1045
|
}
|
|
925
1046
|
return r.skipped.length ? 1 : 0;
|
|
926
1047
|
}
|
|
927
|
-
/**
|
|
1048
|
+
/**
|
|
1049
|
+
* `sideby desktop <account>`: the Family's Desktop App as that Account (spec §3.20); `sideby desktop grok-bot:<name>`:
|
|
1050
|
+
* a Grok Bot window, new or open, and with `--remove` its removal (ADR-0019).
|
|
1051
|
+
*/
|
|
928
1052
|
export async function cmdDesktop(a, io) {
|
|
929
1053
|
const [ref, ...extra] = a.positionals;
|
|
930
1054
|
if (!ref || extra.length || a.rest.length)
|
|
931
|
-
throw new UsageError('usage: sideby desktop <account>');
|
|
1055
|
+
throw new UsageError('usage: sideby desktop <account> | sideby desktop grok-bot:<name> [--remove [--yes]]');
|
|
932
1056
|
const rt = await createRuntime();
|
|
933
|
-
const
|
|
1057
|
+
const profile = parseProfileRef(ref) !== undefined;
|
|
1058
|
+
if (a.flags.has('remove')) {
|
|
1059
|
+
if (!profile)
|
|
1060
|
+
throw new UsageError('--remove works on Grok Bot windows only: sideby desktop grok-bot:<name> --remove');
|
|
1061
|
+
const r = await removeAppProfile(rt, ref, { yes: a.flags.has('yes') });
|
|
1062
|
+
if (a.flags.has('json'))
|
|
1063
|
+
json(io, r);
|
|
1064
|
+
else if (r.removed)
|
|
1065
|
+
io.out(`${c.green('✓')} removed the ${r.app} window ${c.bold(r.ref)} and its data`);
|
|
1066
|
+
else {
|
|
1067
|
+
io.out(`Remove the ${r.app} window ${c.bold(r.ref)}?`);
|
|
1068
|
+
io.out(` deletes ${tildify(r.appData, rt.paths.home)}: its sign-in, bots and local data on this Mac`);
|
|
1069
|
+
io.out('Run again with --yes to remove it.');
|
|
1070
|
+
}
|
|
1071
|
+
return r.removed ? 0 : 10;
|
|
1072
|
+
}
|
|
1073
|
+
const r = profile ? await openAppProfile(rt, ref) : await openDesktopApp(rt, ref);
|
|
934
1074
|
if (a.flags.has('json')) {
|
|
935
1075
|
json(io, r);
|
|
936
1076
|
return 0;
|
|
@@ -194,11 +194,30 @@ const ShellInitFiles = Type.Array(Type.Object({
|
|
|
194
194
|
action: Type.Optional(Type.Union([Type.Literal('written'), Type.Literal('unchanged')])),
|
|
195
195
|
message: Type.Optional(Str),
|
|
196
196
|
}), { description: 'One entry per config `shellInitFile`, rewritten after the change.' });
|
|
197
|
+
const AppProfile = Type.Object({
|
|
198
|
+
ref: Type.String({ description: "`grok-bot:<name>`; `grok-bot:main` is the app's own default data." }),
|
|
199
|
+
app: Type.String({ description: 'The app, for example `Grok Bot`.' }),
|
|
200
|
+
name: Str,
|
|
201
|
+
isMain: Type.Boolean(),
|
|
202
|
+
appData: Type.Optional(Type.String({
|
|
203
|
+
description: "This window's data folder; absent for the main one, which uses the app's default.",
|
|
204
|
+
})),
|
|
205
|
+
installed: Type.Boolean(),
|
|
206
|
+
running: Type.Boolean({ description: "This window's own instance runs now." }),
|
|
207
|
+
signedIn: Type.Optional(Type.Boolean({
|
|
208
|
+
description: 'What the app last wrote about its sign-in (it keeps the last value after the app quits); absent when unknown. Which account is signed in is not known.',
|
|
209
|
+
})),
|
|
210
|
+
}, {
|
|
211
|
+
description: 'A window of a desktop app that signs in apart from every account (Grok Bot, ADR-0019). Open or create one with `sideby desktop grok-bot:<name>`.',
|
|
212
|
+
});
|
|
197
213
|
export const OUTPUT_SCHEMAS = {
|
|
198
214
|
list: Type.Object({
|
|
199
215
|
schemaVersion: Version,
|
|
200
216
|
families: Type.Array(FamilyInfo),
|
|
201
217
|
accounts: Type.Array(AccountStatus),
|
|
218
|
+
appProfiles: Type.Optional(Type.Array(AppProfile, {
|
|
219
|
+
description: 'Grok Bot windows (macOS, when Grok Bot is installed or has windows), the main one first. Not accounts.',
|
|
220
|
+
})),
|
|
202
221
|
pluginErrors: Type.Array(PluginError),
|
|
203
222
|
}, { title: 'sideby list --json' }),
|
|
204
223
|
doctor: Type.Object({
|
|
@@ -235,6 +254,30 @@ export const OUTPUT_SCHEMAS = {
|
|
|
235
254
|
})),
|
|
236
255
|
message: Type.Optional(Str),
|
|
237
256
|
}, { title: 'sideby tune apply|undo --json' }),
|
|
257
|
+
'remote-control': Type.Object({
|
|
258
|
+
schemaVersion: Version,
|
|
259
|
+
action: Type.Union([Type.Literal('status'), Type.Literal('off'), Type.Literal('undo')]),
|
|
260
|
+
applied: Type.Optional(Type.Boolean({ description: 'True once the change was written (or undone).' })),
|
|
261
|
+
groups: Type.Optional(Type.Array(Type.Object({
|
|
262
|
+
file: Str,
|
|
263
|
+
accounts: Type.Array(Str, { description: 'Claude accounts whose settings.json is this file.' }),
|
|
264
|
+
state: Type.Union([
|
|
265
|
+
Type.Literal('on'),
|
|
266
|
+
Type.Literal('default'),
|
|
267
|
+
Type.Literal('off'),
|
|
268
|
+
Type.Literal('missing'),
|
|
269
|
+
Type.Literal('blocked'),
|
|
270
|
+
], {
|
|
271
|
+
description: 'on: remoteControlAtStartup is true; default: not set; off: false; missing: no settings.json; blocked: cannot be changed safely (see reason).',
|
|
272
|
+
}),
|
|
273
|
+
reason: Type.Optional(Str),
|
|
274
|
+
undo: Type.Boolean({
|
|
275
|
+
description: 'sideby turned it off here and can put the earlier value back.',
|
|
276
|
+
}),
|
|
277
|
+
}), { description: 'For status, every file; for a preview, the files that would change.' })),
|
|
278
|
+
results: Type.Optional(Type.Array(Type.Object({ file: Str, accounts: Type.Array(Str), ok: Type.Boolean(), message: Str }))),
|
|
279
|
+
message: Type.Optional(Str),
|
|
280
|
+
}, { title: 'sideby remote-control [off|undo] --json' }),
|
|
238
281
|
model: Type.Object({
|
|
239
282
|
schemaVersion: Version,
|
|
240
283
|
account: Str,
|
|
@@ -356,6 +399,8 @@ export const OUTPUT_SCHEMAS = {
|
|
|
356
399
|
requested: Type.Literal(true),
|
|
357
400
|
next: Type.Optional(Str),
|
|
358
401
|
brief: Type.Optional(Str),
|
|
402
|
+
/** The session was waiting for its quota, so the handoff starts now instead of after a turn (ADR-0018). */
|
|
403
|
+
now: Type.Optional(Type.Literal(true)),
|
|
359
404
|
}, { title: 'sideby handoff ready --json' }),
|
|
360
405
|
'handoff-setup': Type.Object({ schemaVersion: Version, family: Str, applied: Type.Boolean(), plan: QuotaSetupPlan }, { title: 'sideby handoff setup --json' }),
|
|
361
406
|
'handoff-teardown': Type.Object({ schemaVersion: Version, family: Str, ok: Type.Boolean(), message: Str, diff: Type.Optional(Str) }, { title: 'sideby handoff teardown --json' }),
|
|
@@ -421,7 +466,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
421
466
|
desktop: Type.Object({
|
|
422
467
|
schemaVersion: Version,
|
|
423
468
|
ref: Str,
|
|
424
|
-
family:
|
|
469
|
+
family: Type.String({ description: 'The family, or `grok-bot` for a Grok Bot window.' }),
|
|
425
470
|
app: Type.String({ description: 'The desktop app, for example `Claude Desktop`.' }),
|
|
426
471
|
appPath: Str,
|
|
427
472
|
action: Type.Union([Type.Literal('started'), Type.Literal('focused'), Type.Literal('already-running')], {
|
|
@@ -437,6 +482,15 @@ export const OUTPUT_SCHEMAS = {
|
|
|
437
482
|
command: Type.Optional(Type.Array(Str, { description: 'The `open` command sideby ran.' })),
|
|
438
483
|
notices: Type.Array(Str),
|
|
439
484
|
}, { title: 'sideby desktop --json' }),
|
|
485
|
+
'desktop-remove': Type.Object({
|
|
486
|
+
schemaVersion: Version,
|
|
487
|
+
ref: Str,
|
|
488
|
+
app: Str,
|
|
489
|
+
appData: Type.String({ description: 'The folder that is (or would be) deleted.' }),
|
|
490
|
+
removed: Type.Boolean({
|
|
491
|
+
description: 'False when only the plan was shown; run again with --yes (exit 10).',
|
|
492
|
+
}),
|
|
493
|
+
}, { title: 'sideby desktop grok-bot:<name> --remove --json' }),
|
|
440
494
|
error: Type.Object({
|
|
441
495
|
schemaVersion: Version,
|
|
442
496
|
error: Type.String({ description: 'What went wrong and what to do next.' }),
|
package/dist/src/cli/main.js
CHANGED
|
@@ -34,6 +34,9 @@ Usage:
|
|
|
34
34
|
from your last 7 days of sessions (read-only)
|
|
35
35
|
sideby model <account> [<model>|default] [--effort <level>|default] [--yes]
|
|
36
36
|
show or set the model and effort sideby starts this account with
|
|
37
|
+
sideby remote-control [off|undo] [--yes]
|
|
38
|
+
whether Claude sessions connect to claude.ai by themselves;
|
|
39
|
+
off stops that for every account (shows the change first)
|
|
37
40
|
sideby handoff status what Auto Handoff still needs on this machine
|
|
38
41
|
sideby handoff enable [--yes] turn it on (shows the config change first); --same-family,
|
|
39
42
|
--order claude=codex001,cc002, --threshold n, --prepare-at n
|
|
@@ -48,6 +51,7 @@ Usage:
|
|
|
48
51
|
sideby ui --stop stop the background panel
|
|
49
52
|
sideby desktop <account> open the desktop app (Claude Desktop, the Codex app) as this
|
|
50
53
|
account, next to the others (macOS)
|
|
54
|
+
sideby desktop grok-bot:<name> open a Grok Bot window, a new one for a new name (macOS)
|
|
51
55
|
sideby app install [--url <url>] add a sideby app that opens the panel (macOS, Linux)
|
|
52
56
|
sideby app uninstall remove that app
|
|
53
57
|
sideby shell-init zsh|bash print shell functions, e.g. eval "$(sideby shell-init zsh)"
|
|
@@ -56,8 +60,8 @@ Usage:
|
|
|
56
60
|
sideby plugins list loaded plugins and load errors
|
|
57
61
|
|
|
58
62
|
Options:
|
|
59
|
-
--json machine-readable output (list, new, next, alias, doctor, quota, tune,
|
|
60
|
-
desktop)
|
|
63
|
+
--json machine-readable output (list, new, next, alias, doctor, quota, tune, model, remote-control, handoff,
|
|
64
|
+
families, plugins, app, desktop)
|
|
61
65
|
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop
|
|
62
66
|
-v, --version show the version
|
|
63
67
|
|
|
@@ -130,6 +134,7 @@ Examples:
|
|
|
130
134
|
sideby doctor claude:008 --fix
|
|
131
135
|
Exit: 0 when nothing fails (warnings allowed); 1 when a check fails.`,
|
|
132
136
|
desktop: `sideby desktop <account> [--json]
|
|
137
|
+
sideby desktop grok-bot:<name> [--remove [--yes]] [--json]
|
|
133
138
|
|
|
134
139
|
Open the host's desktop app as one account: Claude Desktop for claude accounts, the Codex app (ChatGPT.app) for codex
|
|
135
140
|
accounts. Each account gets its own window, side by side with the others; macOS only for now.
|
|
@@ -139,14 +144,22 @@ directory (CLAUDE_CONFIG_DIR, CODEX_HOME) and its own app data folder,
|
|
|
139
144
|
~/Library/Application Support/sideby/<family>/<name>/. When that account's window is already open, sideby brings it
|
|
140
145
|
to the front instead of starting another one. API accounts are refused: the apps sign in with a subscription only.
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
Grok Bot signs in with a Cursor account, apart from every account above, so its windows have names of their own:
|
|
148
|
+
grok-bot:<name> opens that window, and makes it first when the name is new (a new window, signed out). grok-bot or
|
|
149
|
+
grok-bot:main is Grok Bot's own default window. --remove deletes a window you made, which signs it out and deletes
|
|
150
|
+
its data on this Mac; it asks for --yes first (exit 10) and refuses while that window is open. \`sideby list\` shows
|
|
151
|
+
the Grok Bot windows.
|
|
152
|
+
|
|
153
|
+
sideby never copies or changes the app, and never closes a window. The first time, sign in inside the new window;
|
|
154
|
+
close the app's other windows while you do, because the sign-in may come back to one of them.
|
|
144
155
|
|
|
145
156
|
Examples:
|
|
146
157
|
sideby desktop claude:work
|
|
147
158
|
sideby desktop cc008 --json
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
sideby desktop grok-bot:002
|
|
160
|
+
sideby desktop grok-bot:002 --remove --yes
|
|
161
|
+
Exit: 0 opened, brought to the front or removed; 1 not macOS, no desktop app for that host, API account, app not
|
|
162
|
+
installed, a bad window name, or the app did not open; 10 --remove without --yes; 2 malformed command line.`,
|
|
150
163
|
families: `sideby families [family] [--json]
|
|
151
164
|
|
|
152
165
|
For each host (built in or from a plugin): where its accounts live, the variable that selects one, how to sign
|
|
@@ -179,13 +192,14 @@ const COMMANDS = {
|
|
|
179
192
|
quota: { flags: ['json', 'yes'] },
|
|
180
193
|
tune: { flags: ['json', 'yes'] },
|
|
181
194
|
model: { flags: ['json', 'yes'], valued: ['effort'] },
|
|
195
|
+
'remote-control': { flags: ['json', 'yes'] },
|
|
182
196
|
handoff: {
|
|
183
197
|
flags: ['json', 'yes', 'same-family', 'no-same-family'],
|
|
184
198
|
valued: ['brief', 'to', 'order', 'prepare-at', 'threshold', 'wait-minutes', 'countdown'],
|
|
185
199
|
},
|
|
186
200
|
ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
|
|
187
201
|
app: { flags: ['json'], valued: ['url'] },
|
|
188
|
-
desktop: { flags: ['json'] },
|
|
202
|
+
desktop: { flags: ['json', 'remove', 'yes'] },
|
|
189
203
|
'shell-init': { flags: ['write'] },
|
|
190
204
|
plugins: { flags: ['json'] },
|
|
191
205
|
families: { flags: ['json'] },
|
|
@@ -283,6 +297,8 @@ export async function main(argv) {
|
|
|
283
297
|
return await cmds.cmdTune(parsed, io);
|
|
284
298
|
case 'model':
|
|
285
299
|
return await cmds.cmdModel(parsed, io);
|
|
300
|
+
case 'remote-control':
|
|
301
|
+
return await cmds.cmdRemoteControl(parsed, io);
|
|
286
302
|
case 'handoff':
|
|
287
303
|
return await cmds.cmdHandoff(parsed, io);
|
|
288
304
|
case 'ui':
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ProfileAppDef } from '../families/desktop.ts';
|
|
2
|
+
import type { Runtime } from '../runtime.ts';
|
|
3
|
+
import { type DesktopOpenResult } from './desktop-app.ts';
|
|
4
|
+
export interface AppProfileStatus {
|
|
5
|
+
/** `grok-bot:<name>`; the Main Profile is `grok-bot:main`. */
|
|
6
|
+
ref: string;
|
|
7
|
+
/** The app as messages name it, for example `Grok Bot`. */
|
|
8
|
+
app: string;
|
|
9
|
+
name: string;
|
|
10
|
+
isMain: boolean;
|
|
11
|
+
/** The Profile's app data folder; absent for the Main Profile, which uses the app's default. */
|
|
12
|
+
appData?: string;
|
|
13
|
+
installed: boolean;
|
|
14
|
+
/** That Profile's own window runs now. */
|
|
15
|
+
running: boolean;
|
|
16
|
+
/** What the app last wrote about its sign-in; absent when unknown. */
|
|
17
|
+
signedIn?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface AppProfileRemoveResult {
|
|
20
|
+
ref: string;
|
|
21
|
+
app: string;
|
|
22
|
+
appData: string;
|
|
23
|
+
/** False when only the plan was shown (no `--yes`). */
|
|
24
|
+
removed: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The app and Profile name a ref names, or undefined when it is not an App Profile ref. */
|
|
27
|
+
export declare function parseProfileRef(ref: string): {
|
|
28
|
+
def: ProfileAppDef;
|
|
29
|
+
name: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
/** Where the App Profiles of an app live: `~/Library/Application Support/sideby/<app>`. */
|
|
32
|
+
export declare function profilesRoot(home: string, def: ProfileAppDef): string;
|
|
33
|
+
/**
|
|
34
|
+
* Every App Profile with its state, for `sideby list` and the Panel: only on macOS, and per app only when it is
|
|
35
|
+
* installed or has Profiles. The Main Profile first. Never throws.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listAppProfiles(rt: Runtime): Promise<AppProfileStatus[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Opens an App Profile's window, creating its folder first when it is new, or brings its running window to the front.
|
|
40
|
+
* Never starts a second window for the same Profile and never ends a process.
|
|
41
|
+
*/
|
|
42
|
+
export declare function openAppProfile(rt: Runtime, ref: string): Promise<DesktopOpenResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Removes a non-main App Profile's folder, which signs that window out and deletes its local data. Without `yes` it
|
|
45
|
+
* only returns what would go. Refuses the Main Profile, a running window, a missing folder and anything that is not a
|
|
46
|
+
* real folder inside sideby's own; never follows a link.
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeAppProfile(rt: Runtime, ref: string, opts: {
|
|
49
|
+
yes: boolean;
|
|
50
|
+
}): Promise<AppProfileRemoveResult>;
|