sideby 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +4 -2
- package/dist/src/cli/commands.js +117 -17
- package/dist/src/cli/json-schemas.js +52 -1
- package/dist/src/cli/main.js +34 -3
- package/dist/src/core/brand-mark.d.ts +5 -7
- package/dist/src/core/brand-mark.js +18 -5
- package/dist/src/core/browser-profiles.d.ts +71 -0
- package/dist/src/core/browser-profiles.js +361 -0
- package/dist/src/core/config.d.ts +15 -0
- package/dist/src/core/config.js +13 -1
- package/dist/src/core/create.js +2 -1
- package/dist/src/core/desktop-app.d.ts +3 -1
- package/dist/src/core/desktop-app.js +29 -8
- package/dist/src/core/launch.d.ts +7 -0
- package/dist/src/core/launch.js +22 -2
- package/dist/src/core/leveldb-keys.d.ts +14 -0
- package/dist/src/core/leveldb-keys.js +291 -0
- package/dist/src/core/remote-control.d.ts +17 -6
- package/dist/src/core/remote-control.js +40 -17
- package/dist/src/core/update.d.ts +10 -3
- package/dist/src/core/update.js +38 -3
- package/dist/src/core/version.d.ts +11 -0
- package/dist/src/core/version.js +26 -0
- package/dist/src/desktop/app.d.ts +2 -0
- package/dist/src/desktop/app.js +39 -3
- package/dist/src/families/browser.d.ts +20 -0
- package/dist/src/families/browser.js +23 -0
- package/dist/src/families/claude/browser.d.ts +3 -0
- package/dist/src/families/claude/browser.js +14 -0
- package/dist/src/families/claude/desktop.js +1 -0
- package/dist/src/families/codex/browser.d.ts +7 -0
- package/dist/src/families/codex/browser.js +26 -0
- package/dist/src/families/desktop.d.ts +5 -1
- package/dist/src/families/pi/index.js +2 -0
- package/dist/src/panel/handler.d.ts +6 -0
- package/dist/src/panel/handler.js +49 -6
- package/dist/src/panel/i18n.d.ts +75 -4
- package/dist/src/panel/i18n.js +162 -20
- package/dist/src/panel/page-logic.d.ts +23 -0
- package/dist/src/panel/page-logic.js +44 -0
- package/dist/src/panel/page-script.js +609 -180
- package/dist/src/panel/page-styles.js +72 -17
- package/dist/src/panel/page.d.ts +5 -0
- package/dist/src/panel/page.js +10 -9
- package/dist/src/runtime.d.ts +6 -1
- package/dist/src/runtime.js +22 -11
- package/dist/src/types.d.ts +2 -0
- package/package.json +5 -3
- package/schemas/browser-open.json +47 -0
- package/schemas/browser-switch.json +42 -0
- package/schemas/browser.json +99 -0
- package/schemas/config.json +11 -0
- package/schemas/families.json +6 -0
- package/schemas/list.json +6 -0
- package/schemas/remote-control.json +4 -0
- package/schemas/update.json +4 -0
- package/skills/sideby/SKILL.md +5 -3
- package/skills/sideby/references/setup.md +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ cc-switch's docs describe that switch: it writes the live settings file ([README
|
|
|
77
77
|
| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |
|
|
78
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) |
|
|
79
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
|
-
|
|
|
80
|
+
| 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) |
|
|
81
81
|
| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |
|
|
82
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) |
|
|
83
83
|
| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |
|
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261016T155413-627722"}
|
|
@@ -44,8 +44,8 @@ export declare function cmdResume(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
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
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).
|
|
47
|
+
* `sideby remote-control [on|off|undo] [--yes] [--json]` (spec §3.22, ADR-0017): whether Claude sessions connect to
|
|
48
|
+
* claude.ai by themselves, and turning that on or off for every Account (a preview with exit 10 until --yes).
|
|
49
49
|
*/
|
|
50
50
|
export declare function cmdRemoteControl(a: ParsedArgs, io: Io): Promise<number>;
|
|
51
51
|
/**
|
|
@@ -74,6 +74,8 @@ export declare function cmdApp(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
74
74
|
* a Grok Bot window, new or open, and with `--remove` its removal (ADR-0019).
|
|
75
75
|
*/
|
|
76
76
|
export declare function cmdDesktop(a: ParsedArgs, io: Io): Promise<number>;
|
|
77
|
+
/** `sideby browser [--enable|--disable] [--yes]` and `sideby browser open <account> [--profile <p>]` (spec §3.24). */
|
|
78
|
+
export declare function cmdBrowser(a: ParsedArgs, io: Io): Promise<number>;
|
|
77
79
|
export declare function cmdAlias(a: ParsedArgs, io: Io): Promise<number>;
|
|
78
80
|
/** `sideby families [family]`: each Family's layout, sign-in and Shared Items, from its FamilyDef (spec §3.16). */
|
|
79
81
|
export declare function cmdFamilies(a: ParsedArgs, io: Io): Promise<number>;
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { aliasesByAccount } from '../core/accounts.js';
|
|
3
3
|
import { listAppProfiles, openAppProfile, parseProfileRef, removeAppProfile } from '../core/app-profiles.js';
|
|
4
|
+
import { browserReport, openInBrowser, setBrowserSignIns } from '../core/browser-profiles.js';
|
|
4
5
|
import { aliasRefs } from '../core/config.js';
|
|
5
6
|
import { openDesktopApp, withDesktopApps } from '../core/desktop-app.js';
|
|
6
7
|
import { UserError } from '../core/errors.js';
|
|
7
8
|
import { requestHandoff } from '../core/handoff-chain.js';
|
|
8
9
|
import { explain, readyDecision, runWithHandoff } from '../core/handoff-run.js';
|
|
9
10
|
import { configureHandoff, handoffHint, handoffStatus } from '../core/handoff-status.js';
|
|
10
|
-
import { runHost } from '../core/launch.js';
|
|
11
|
+
import { installHint, runHost } from '../core/launch.js';
|
|
11
12
|
import { accountModel, setAccountModel } from '../core/model-choice.js';
|
|
12
13
|
import { resolvePaths, tildify } from '../core/paths.js';
|
|
13
14
|
import { USAGE_DAYS } from '../core/quota-levels.js';
|
|
14
|
-
import { groupsToTurnOff, groupsToUndo, } from '../core/remote-control.js';
|
|
15
|
+
import { groupsToTurnOff, groupsToTurnOn, groupsToUndo, } from '../core/remote-control.js';
|
|
15
16
|
import { SHELLS } from '../core/shell-init.js';
|
|
16
17
|
import { suggestAlias, suggestName } from '../panel/page-logic.js';
|
|
17
18
|
import { createRuntime, UnknownFamilyError } from '../runtime.js';
|
|
@@ -85,12 +86,13 @@ export async function cmdList(a, io) {
|
|
|
85
86
|
io.out('No accounts found yet.');
|
|
86
87
|
const installed = families.filter((f) => f.installed);
|
|
87
88
|
if (installed.length === 0)
|
|
88
|
-
io.out(`Install a host first: ${families.map((f) => `${f.title} (${f
|
|
89
|
+
io.out(`Install a host first: ${families.map((f) => `${f.title} (${installHint(f)})`).join(', ')}`);
|
|
89
90
|
else
|
|
90
91
|
io.out(`Run ${installed.map((f) => f.bin).join(' or ')} once, then \`sideby new <family> <name>\`.`);
|
|
91
92
|
}
|
|
92
93
|
else {
|
|
93
94
|
// `= main`: the other Accounts of its Same Login, which share its Quota (spec §3.21).
|
|
95
|
+
const brokenOf = (id) => families.find((f) => f.id === id)?.brokenBin;
|
|
94
96
|
const sameMark = (refs) => refs?.length ? ` ${c.dim(`= ${refs.map((r) => r.slice(r.indexOf(':') + 1)).join(', ')}`)}` : '';
|
|
95
97
|
const rows = statuses.map((s) => [
|
|
96
98
|
`${c.bold(s.ref)}${sameMark(s.sameLogin)}`,
|
|
@@ -104,10 +106,17 @@ export async function cmdList(a, io) {
|
|
|
104
106
|
: c.dim('?'),
|
|
105
107
|
s.identity?.email ?? c.dim('—'),
|
|
106
108
|
s.model ?? c.dim('—'),
|
|
107
|
-
s.hostInstalled
|
|
109
|
+
s.hostInstalled
|
|
110
|
+
? tildify(s.dir, home)
|
|
111
|
+
: `${tildify(s.dir, home)} ${c.yellow(brokenOf(s.family) ? '(host install broken)' : '(host not installed)')}`,
|
|
108
112
|
...(hasDesktop ? [s.desktopApp?.running ? c.green('open') : c.dim('—')] : []),
|
|
109
113
|
]);
|
|
110
114
|
io.out(table(rows, ['ACCOUNT', 'TYPE', 'LOGIN', 'EMAIL', 'MODEL', 'DIR', ...(hasDesktop ? ['DESKTOP'] : [])]));
|
|
115
|
+
// One line per Host that cannot run, with what to do: the rows only say which accounts it affects.
|
|
116
|
+
for (const f of families.filter((x) => !x.installed && statuses.some((s) => s.family === x.id)))
|
|
117
|
+
io.out(c.yellow(f.brokenBin
|
|
118
|
+
? `\n${f.title}'s install is broken (${tildify(f.brokenBin, home)} cannot run, often after an interrupted upgrade); reinstall it: ${installHint(f)}`
|
|
119
|
+
: `\n${f.title} is not installed; install it: ${installHint(f)}`));
|
|
111
120
|
if (statuses.some((s) => s.sameLogin?.length))
|
|
112
121
|
io.out(c.dim('\n= <account>: signed in as the same login as that account, so they share one quota.'));
|
|
113
122
|
io.out(c.dim('\nsideby run <account> · sideby new <family> <name> · sideby quota · sideby ui'));
|
|
@@ -471,13 +480,14 @@ function rcGroupLine(g, home) {
|
|
|
471
480
|
return ` ${paint(state)} ${c.bold(g.accounts.join(', '))} ${c.dim(where)}`;
|
|
472
481
|
}
|
|
473
482
|
/**
|
|
474
|
-
* `sideby remote-control [off|undo] [--yes] [--json]` (spec §3.22, ADR-0017): whether Claude sessions connect to
|
|
475
|
-
* claude.ai by themselves, and turning that off for every Account (a preview with exit 10 until --yes).
|
|
483
|
+
* `sideby remote-control [on|off|undo] [--yes] [--json]` (spec §3.22, ADR-0017): whether Claude sessions connect to
|
|
484
|
+
* claude.ai by themselves, and turning that on or off for every Account (a preview with exit 10 until --yes).
|
|
476
485
|
*/
|
|
477
486
|
export async function cmdRemoteControl(a, io) {
|
|
478
487
|
const [action, extra] = a.positionals;
|
|
479
|
-
if (extra !== undefined ||
|
|
480
|
-
|
|
488
|
+
if (extra !== undefined ||
|
|
489
|
+
(action !== undefined && action !== 'on' && action !== 'off' && action !== 'undo'))
|
|
490
|
+
throw new UsageError('usage: sideby remote-control [on|off|undo] [--yes] [--json]');
|
|
481
491
|
const wantJson = a.flags.has('json');
|
|
482
492
|
const rt = await createRuntime();
|
|
483
493
|
const home = rt.paths.home;
|
|
@@ -501,9 +511,15 @@ export async function cmdRemoteControl(a, io) {
|
|
|
501
511
|
io.out(c.cyan('\nRun `sideby remote-control off` so no session connects by itself (it shows the change first).'));
|
|
502
512
|
return 0;
|
|
503
513
|
}
|
|
504
|
-
const
|
|
514
|
+
const apply = () => action === 'off' ? rt.remoteControlOff() : action === 'on' ? rt.remoteControlOn() : rt.remoteControlUndo();
|
|
515
|
+
const todo = action === 'off'
|
|
516
|
+
? groupsToTurnOff(groups)
|
|
517
|
+
: action === 'on'
|
|
518
|
+
? groupsToTurnOn(groups)
|
|
519
|
+
: groupsToUndo(groups);
|
|
520
|
+
const verb = action === 'off' ? 'turn it off' : action === 'on' ? 'turn it on' : 'undo';
|
|
505
521
|
if (todo.length === 0) {
|
|
506
|
-
const r =
|
|
522
|
+
const r = await apply();
|
|
507
523
|
if (wantJson)
|
|
508
524
|
out({ applied: false, groups: show(groups), message: r.message });
|
|
509
525
|
else
|
|
@@ -515,26 +531,30 @@ export async function cmdRemoteControl(a, io) {
|
|
|
515
531
|
out({
|
|
516
532
|
applied: false,
|
|
517
533
|
groups: show(todo),
|
|
518
|
-
message: `Run again with --yes to ${
|
|
534
|
+
message: `Run again with --yes to ${verb}.`,
|
|
519
535
|
});
|
|
520
536
|
else {
|
|
521
537
|
io.out(action === 'off'
|
|
522
538
|
? 'Turn off Remote Control at startup?'
|
|
523
|
-
:
|
|
539
|
+
: action === 'on'
|
|
540
|
+
? 'Turn on Remote Control at startup?'
|
|
541
|
+
: 'Put back the Remote Control setting sideby replaced?');
|
|
524
542
|
for (const g of todo) {
|
|
525
|
-
const to = action === 'off' ? 'false' : 'what it was before';
|
|
543
|
+
const to = action === 'off' ? 'false' : action === 'on' ? 'true' : 'what it was before';
|
|
526
544
|
const from = g.state === 'default' ? 'not set' : g.state === 'on' ? 'true' : 'false';
|
|
527
545
|
io.out(` ${tildify(g.file, home)}: remoteControlAtStartup ${from} -> ${to}`);
|
|
528
546
|
io.out(c.dim(` used by ${g.accounts.join(', ')}`));
|
|
529
547
|
}
|
|
530
548
|
io.out(action === 'off'
|
|
531
549
|
? '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`.'
|
|
532
|
-
:
|
|
533
|
-
|
|
550
|
+
: action === 'on'
|
|
551
|
+
? 'Every new session then connects to claude.ai and the Claude app, where every device signed in to that account can read it. Undo this with `sideby remote-control undo`.'
|
|
552
|
+
: 'New sessions go back to the previous setting.');
|
|
553
|
+
io.out(`Run again with --yes to ${verb}.`);
|
|
534
554
|
}
|
|
535
555
|
return 10;
|
|
536
556
|
}
|
|
537
|
-
const r =
|
|
557
|
+
const r = await apply();
|
|
538
558
|
const results = r.results.map((x) => ({ ...x, file: tildify(x.file, home) }));
|
|
539
559
|
if (wantJson)
|
|
540
560
|
out({ applied: r.ok, results, message: r.message });
|
|
@@ -1069,6 +1089,7 @@ export async function cmdUpdate(a, io) {
|
|
|
1069
1089
|
updated: r.ok,
|
|
1070
1090
|
message: r.message,
|
|
1071
1091
|
restartedPanel: r.restartedPanel,
|
|
1092
|
+
refreshedApp: r.refreshedApp,
|
|
1072
1093
|
skill: SKILL_INSTALL,
|
|
1073
1094
|
...(r.ok ? {} : { output: r.output }),
|
|
1074
1095
|
});
|
|
@@ -1081,6 +1102,8 @@ export async function cmdUpdate(a, io) {
|
|
|
1081
1102
|
io.out(`${c.green('✓')} ${r.message}.`);
|
|
1082
1103
|
if (r.restartedPanel)
|
|
1083
1104
|
io.out(' The background panel is restarting with the new version; open pages reload by themselves.');
|
|
1105
|
+
if (r.refreshedApp)
|
|
1106
|
+
io.out(' The desktop app now starts the new version and shows its icon.');
|
|
1084
1107
|
io.out(' A `sideby ui` running in a terminal and sessions already running keep the old version until you restart them.');
|
|
1085
1108
|
io.out(` If you use the sideby skill, update it too: ${SKILL_INSTALL}`);
|
|
1086
1109
|
return 0;
|
|
@@ -1180,6 +1203,83 @@ export async function cmdDesktop(a, io) {
|
|
|
1180
1203
|
io.err(c.cyan(n));
|
|
1181
1204
|
return 0;
|
|
1182
1205
|
}
|
|
1206
|
+
/** `sideby browser [--enable|--disable] [--yes]` and `sideby browser open <account> [--profile <p>]` (spec §3.24). */
|
|
1207
|
+
export async function cmdBrowser(a, io) {
|
|
1208
|
+
const [sub, ref, ...extra] = a.positionals;
|
|
1209
|
+
const rt = await createRuntime();
|
|
1210
|
+
const home = rt.paths.home;
|
|
1211
|
+
if (sub === 'open') {
|
|
1212
|
+
if (!ref || extra.length || a.rest.length)
|
|
1213
|
+
throw new UsageError('usage: sideby browser open <account> [--profile <folder|name>]');
|
|
1214
|
+
const profile = a.flags.get('profile');
|
|
1215
|
+
const r = await openInBrowser(rt, await rt.resolve(ref), typeof profile === 'string' ? profile : undefined);
|
|
1216
|
+
if (a.flags.has('json'))
|
|
1217
|
+
json(io, r);
|
|
1218
|
+
else
|
|
1219
|
+
io.out(`${c.green('✓')} opened ${r.url} in Chrome as ${c.bold(r.profile.name)} for ${c.bold(r.ref)}`);
|
|
1220
|
+
return 0;
|
|
1221
|
+
}
|
|
1222
|
+
if (sub !== undefined || a.rest.length)
|
|
1223
|
+
throw new UsageError('usage: sideby browser [--enable|--disable] [--yes] | sideby browser open <account>');
|
|
1224
|
+
const enable = a.flags.has('enable');
|
|
1225
|
+
if (enable || a.flags.has('disable')) {
|
|
1226
|
+
if (enable && a.flags.has('disable'))
|
|
1227
|
+
throw new UsageError('pass --enable or --disable, not both');
|
|
1228
|
+
const r = await setBrowserSignIns(rt, enable, { apply: a.flags.has('yes') });
|
|
1229
|
+
if (a.flags.has('json'))
|
|
1230
|
+
json(io, r);
|
|
1231
|
+
else if (!r.changed)
|
|
1232
|
+
io.out(`Finding browser sign-ins is already ${enable ? 'on' : 'off'}; nothing to change.`);
|
|
1233
|
+
else if (!r.applied) {
|
|
1234
|
+
if (enable) {
|
|
1235
|
+
io.out('sideby will read, on this computer only (nothing is sent anywhere):');
|
|
1236
|
+
for (const x of r.reads)
|
|
1237
|
+
io.out(` - ${x}`);
|
|
1238
|
+
io.out('It never opens cookies, saved passwords or the Keychain, and never changes Chrome.');
|
|
1239
|
+
}
|
|
1240
|
+
io.out(`\nChange to ${tildify(r.file, home)}: browser.signIns ${enable ? 'off → on' : 'on → off'}`);
|
|
1241
|
+
io.out(c.cyan(`Apply it with \`sideby browser --${enable ? 'enable' : 'disable'} --yes\`.`));
|
|
1242
|
+
}
|
|
1243
|
+
else
|
|
1244
|
+
io.out(`${c.green('✓')} finding browser sign-ins is ${enable ? 'on' : 'off'}.`);
|
|
1245
|
+
return r.changed && !r.applied ? 10 : 0;
|
|
1246
|
+
}
|
|
1247
|
+
const accounts = await rt.accounts();
|
|
1248
|
+
const r = await browserReport(rt, accounts);
|
|
1249
|
+
if (a.flags.has('json')) {
|
|
1250
|
+
json(io, r);
|
|
1251
|
+
return 0;
|
|
1252
|
+
}
|
|
1253
|
+
for (const p of r.problems)
|
|
1254
|
+
io.err(c.yellow(p));
|
|
1255
|
+
if (!r.enabled) {
|
|
1256
|
+
io.out('Finding which Chrome browser user signed in to each account is off.');
|
|
1257
|
+
io.out(c.cyan('Turn it on with `sideby browser --enable` (it shows what it reads first).'));
|
|
1258
|
+
io.out(c.dim('Without it, open a website in a browser user you name: sideby browser open <account> --profile <name>'));
|
|
1259
|
+
return 0;
|
|
1260
|
+
}
|
|
1261
|
+
if (!r.found) {
|
|
1262
|
+
io.out('Google Chrome has no data on this computer yet; open it once, then try again.');
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
const signedIn = new Set(r.profiles.flatMap((p) => p.signIns));
|
|
1266
|
+
const now = new Date();
|
|
1267
|
+
io.out(table(r.profiles.map((p) => [
|
|
1268
|
+
p.name,
|
|
1269
|
+
p.email ?? c.dim('-'),
|
|
1270
|
+
p.dir,
|
|
1271
|
+
p.signIns.join(', ') || c.dim('-'),
|
|
1272
|
+
p.changedAt ? clock(p.changedAt, now) : '-',
|
|
1273
|
+
]), ['Browser user', 'Google account', 'Folder', 'Signed in to', 'Changed']));
|
|
1274
|
+
const missing = accounts.filter((x) => r.families.matched.includes(x.family) && x.kind !== 'api' && !signedIn.has(x.ref));
|
|
1275
|
+
if (missing.length)
|
|
1276
|
+
io.out(c.dim(`\nNo browser user found for: ${missing.map((x) => x.ref).join(', ')}`));
|
|
1277
|
+
if (r.families.openOnly.length)
|
|
1278
|
+
io.out(c.dim(`${r.families.openOnly.join(', ')}: the website keeps no id to match; open it with --profile.`));
|
|
1279
|
+
io.out(c.dim('\nRead from Chrome on this computer only; a browser user that signed out may still show.'));
|
|
1280
|
+
io.out(c.dim('Open one: sideby browser open <account> [--profile <name>]'));
|
|
1281
|
+
return 0;
|
|
1282
|
+
}
|
|
1183
1283
|
export async function cmdAlias(a, io) {
|
|
1184
1284
|
const [sub, alias, target, ...more] = a.positionals;
|
|
1185
1285
|
const usage = 'usage: sideby alias add <short-command> <account> [-- host args] | sideby alias rm <short-command>';
|
|
@@ -1221,7 +1321,7 @@ export async function cmdFamilies(a, io) {
|
|
|
1221
1321
|
families.forEach((f, i) => {
|
|
1222
1322
|
if (i)
|
|
1223
1323
|
io.out('');
|
|
1224
|
-
io.out(`${c.bold(f.title)} (${f.id}) ${f.installed ? c.green('installed') : c.yellow(`not installed: ${f
|
|
1324
|
+
io.out(`${c.bold(f.title)} (${f.id}) ${f.installed ? c.green('installed') : c.yellow(f.brokenBin ? `install broken (${tildify(f.brokenBin, rt.paths.home)} cannot run); reinstall: ${installHint(f)}` : `not installed: ${installHint(f)}`)}`);
|
|
1225
1325
|
io.out(` Accounts ~/${f.layout.main} (main), ~/${f.layout.account}; selected with ${f.selectVar}`);
|
|
1226
1326
|
io.out(` Sign in sideby login ${f.id}:<name> ${c.dim(f.login.args ? `(runs ${[f.bin, ...f.login.args].join(' ')})` : `(${f.login.hint})`)}`);
|
|
1227
1327
|
const yes = (b) => (b ? c.green('yes') : c.dim('no public source'));
|
|
@@ -50,7 +50,9 @@ const FamilyInfo = Type.Object({
|
|
|
50
50
|
title: Str,
|
|
51
51
|
bin: Str,
|
|
52
52
|
installUrl: Str,
|
|
53
|
+
installCommand: Type.Optional(Str),
|
|
53
54
|
installed: Type.Boolean(),
|
|
55
|
+
brokenBin: Type.Optional(Str),
|
|
54
56
|
plugin: Str,
|
|
55
57
|
tuning: Type.Boolean(),
|
|
56
58
|
});
|
|
@@ -256,7 +258,12 @@ export const OUTPUT_SCHEMAS = {
|
|
|
256
258
|
}, { title: 'sideby tune apply|undo --json' }),
|
|
257
259
|
'remote-control': Type.Object({
|
|
258
260
|
schemaVersion: Version,
|
|
259
|
-
action: Type.Union([
|
|
261
|
+
action: Type.Union([
|
|
262
|
+
Type.Literal('status'),
|
|
263
|
+
Type.Literal('on'),
|
|
264
|
+
Type.Literal('off'),
|
|
265
|
+
Type.Literal('undo'),
|
|
266
|
+
]),
|
|
260
267
|
applied: Type.Optional(Type.Boolean({ description: 'True once the change was written (or undone).' })),
|
|
261
268
|
groups: Type.Optional(Type.Array(Type.Object({
|
|
262
269
|
file: Str,
|
|
@@ -491,6 +498,47 @@ export const OUTPUT_SCHEMAS = {
|
|
|
491
498
|
description: 'False when only the plan was shown; run again with --yes (exit 10).',
|
|
492
499
|
}),
|
|
493
500
|
}, { title: 'sideby desktop grok-bot:<name> --remove --json' }),
|
|
501
|
+
browser: Type.Object({
|
|
502
|
+
schemaVersion: Version,
|
|
503
|
+
enabled: Type.Boolean({
|
|
504
|
+
description: 'config `browser.signIns`. When false nothing of the browser was read and `profiles` is empty.',
|
|
505
|
+
}),
|
|
506
|
+
browser: Type.String({ description: 'The browser read, `Google Chrome`.' }),
|
|
507
|
+
found: Type.Boolean({ description: "Chrome's user data folder exists on this Mac." }),
|
|
508
|
+
profiles: Type.Array(Type.Object({
|
|
509
|
+
dir: Type.String({ description: "The browser user's folder, such as `Default` or `Profile 7`." }),
|
|
510
|
+
name: Type.String({ description: 'The name Chrome shows; it may be an email.' }),
|
|
511
|
+
email: Type.Optional(Type.String({ description: 'The Google account this browser user is signed in to Chrome with.' })),
|
|
512
|
+
signIns: Type.Array(Str, {
|
|
513
|
+
description: "Refs of the accounts this browser user signed in to on the vendor's website, as far as its local data shows; it may be left over from before a sign-out.",
|
|
514
|
+
}),
|
|
515
|
+
changedAt: Type.Optional(Type.String({ description: 'When its Local Storage last changed (ISO 8601).' })),
|
|
516
|
+
}), { description: 'Most recently changed first.' }),
|
|
517
|
+
families: Type.Object({
|
|
518
|
+
matched: Type.Array(Str, { description: 'Families whose accounts can be matched (claude, codex).' }),
|
|
519
|
+
openOnly: Type.Array(Str, {
|
|
520
|
+
description: 'Families that can only be opened in a browser user given with --profile (grok).',
|
|
521
|
+
}),
|
|
522
|
+
}),
|
|
523
|
+
problems: Type.Array(Str),
|
|
524
|
+
}, { title: 'sideby browser --json' }),
|
|
525
|
+
'browser-switch': Type.Object({
|
|
526
|
+
schemaVersion: Version,
|
|
527
|
+
enabled: Type.Boolean({ description: 'The value asked for.' }),
|
|
528
|
+
changed: Type.Boolean(),
|
|
529
|
+
applied: Type.Boolean({
|
|
530
|
+
description: 'False when only the change was shown; run again with --yes (exit 10).',
|
|
531
|
+
}),
|
|
532
|
+
file: Type.String({ description: 'The config file.' }),
|
|
533
|
+
reads: Type.Array(Str, { description: 'What sideby reads while it is on, all on this computer.' }),
|
|
534
|
+
}, { title: 'sideby browser --enable|--disable --json' }),
|
|
535
|
+
'browser-open': Type.Object({
|
|
536
|
+
schemaVersion: Version,
|
|
537
|
+
ref: Str,
|
|
538
|
+
url: Str,
|
|
539
|
+
profile: Type.Object({ dir: Str, name: Str }),
|
|
540
|
+
command: Type.Array(Str, { description: 'The `open` command sideby ran.' }),
|
|
541
|
+
}, { title: 'sideby browser open --json' }),
|
|
494
542
|
update: Type.Object({
|
|
495
543
|
schemaVersion: Version,
|
|
496
544
|
current: Type.String({ description: 'The running sideby version.' }),
|
|
@@ -513,6 +561,9 @@ export const OUTPUT_SCHEMAS = {
|
|
|
513
561
|
restartedPanel: Type.Optional(Type.Boolean({
|
|
514
562
|
description: 'A background Panel was running and starts again with the new version.',
|
|
515
563
|
})),
|
|
564
|
+
refreshedApp: Type.Optional(Type.Boolean({
|
|
565
|
+
description: 'The desktop app (`sideby app install`) was installed and was written again by the new version.',
|
|
566
|
+
})),
|
|
516
567
|
skill: Type.Optional(Type.String({ description: 'The command that updates the sideby skill; sideby never runs it.' })),
|
|
517
568
|
output: Type.Optional(Type.String({ description: 'What the failed install command printed.' })),
|
|
518
569
|
}, { title: 'sideby update --json' }),
|
package/dist/src/cli/main.js
CHANGED
|
@@ -34,7 +34,7 @@ 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]
|
|
37
|
+
sideby remote-control [on|off|undo] [--yes]
|
|
38
38
|
whether Claude sessions connect to claude.ai by themselves;
|
|
39
39
|
off stops that for every account (shows the change first)
|
|
40
40
|
sideby handoff status what Auto Handoff still needs on this machine
|
|
@@ -52,6 +52,10 @@ Usage:
|
|
|
52
52
|
sideby desktop <account> open the desktop app (Claude Desktop, the Codex app) as this
|
|
53
53
|
account, next to the others (macOS)
|
|
54
54
|
sideby desktop grok-bot:<name> open a Grok Bot window, a new one for a new name (macOS)
|
|
55
|
+
sideby browser [--enable [--yes]] which Chrome browser user signed in to each account's website
|
|
56
|
+
(read on this computer only; off until you enable it)
|
|
57
|
+
sideby browser open <account> [--profile <name>]
|
|
58
|
+
open claude.ai, chatgpt.com or grok.com in that browser user
|
|
55
59
|
sideby app install [--url <url>] add a sideby app that opens the panel (macOS, Linux)
|
|
56
60
|
sideby app uninstall remove that app
|
|
57
61
|
sideby shell-init zsh|bash print shell functions, e.g. eval "$(sideby shell-init zsh)"
|
|
@@ -62,8 +66,8 @@ Usage:
|
|
|
62
66
|
|
|
63
67
|
Options:
|
|
64
68
|
--json machine-readable output (list, new, next, alias, doctor, quota, tune, model, remote-control, handoff,
|
|
65
|
-
families, plugins, app, desktop, update)
|
|
66
|
-
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop
|
|
69
|
+
families, plugins, app, desktop, browser, update)
|
|
70
|
+
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop, browser
|
|
67
71
|
-v, --version show the version
|
|
68
72
|
|
|
69
73
|
An <account> is <family>:<name> (claude:work) or just <name> when it is unique.
|
|
@@ -164,6 +168,30 @@ Examples:
|
|
|
164
168
|
sideby desktop grok-bot:002 --remove --yes
|
|
165
169
|
Exit: 0 opened, brought to the front or removed; 1 not macOS, no desktop app for that host, API account, app not
|
|
166
170
|
installed, a bad window name, or the app did not open; 10 --remove without --yes; 2 malformed command line.`,
|
|
171
|
+
browser: `sideby browser [--enable|--disable] [--yes] [--json]
|
|
172
|
+
sideby browser open <account> [--profile <folder|name>] [--json]
|
|
173
|
+
|
|
174
|
+
Find which Google Chrome browser user signed in to each account's website (claude.ai for claude accounts,
|
|
175
|
+
chatgpt.com for codex accounts), and open the website in that browser user. macOS only for now.
|
|
176
|
+
|
|
177
|
+
Everything is read on this computer and sent nowhere. sideby reads each browser user's name, the key names (never
|
|
178
|
+
the values) claude.ai and chatgpt.com keep in that browser user's Local Storage, and the account id in each Claude and
|
|
179
|
+
Codex login file, only to compare. It never opens cookies, saved passwords or the Keychain, and never changes Chrome.
|
|
180
|
+
It is off until you turn it on: --enable shows what it reads and changes config browser.signIns after --yes.
|
|
181
|
+
|
|
182
|
+
"Signed in to" means that browser user's data shows a sign-in as that account; it may be left over from before a
|
|
183
|
+
sign-out. grok.com keeps no id to match, so grok accounts can only be opened with --profile.
|
|
184
|
+
|
|
185
|
+
open uses the one browser user that signed in to the account, or the one --profile names (folder such as
|
|
186
|
+
"Profile 7", or the name Chrome shows). It runs: open -na "Google Chrome" --args --profile-directory=<folder> <url>.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
sideby browser --enable --yes
|
|
190
|
+
sideby browser
|
|
191
|
+
sideby browser open cc008
|
|
192
|
+
sideby browser open grok:main --profile Work
|
|
193
|
+
Exit: 0 shown, changed or opened; 1 not macOS, Chrome not installed, no or several browser users to choose from, or
|
|
194
|
+
Chrome did not open; 10 --enable/--disable without --yes; 2 malformed command line.`,
|
|
167
195
|
families: `sideby families [family] [--json]
|
|
168
196
|
|
|
169
197
|
For each host (built in or from a plugin): where its accounts live, the variable that selects one, how to sign
|
|
@@ -204,6 +232,7 @@ const COMMANDS = {
|
|
|
204
232
|
ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
|
|
205
233
|
app: { flags: ['json'], valued: ['url'] },
|
|
206
234
|
desktop: { flags: ['json', 'remove', 'yes'] },
|
|
235
|
+
browser: { flags: ['json', 'enable', 'disable', 'yes'], valued: ['profile'] },
|
|
207
236
|
'shell-init': { flags: ['write'] },
|
|
208
237
|
plugins: { flags: ['json'] },
|
|
209
238
|
families: { flags: ['json'] },
|
|
@@ -379,6 +408,8 @@ async function dispatch(cmd, parsed, io, cmds) {
|
|
|
379
408
|
return await cmds.cmdApp(parsed, io);
|
|
380
409
|
case 'desktop':
|
|
381
410
|
return await cmds.cmdDesktop(parsed, io);
|
|
411
|
+
case 'browser':
|
|
412
|
+
return await cmds.cmdBrowser(parsed, io);
|
|
382
413
|
case 'shell-init':
|
|
383
414
|
return await cmds.cmdShellInit(parsed, io);
|
|
384
415
|
case 'plugins':
|
|
@@ -64,12 +64,6 @@ export declare const MARK_VIEWBOX: {
|
|
|
64
64
|
readonly y: 3;
|
|
65
65
|
readonly size: 69;
|
|
66
66
|
};
|
|
67
|
-
/** The square around one buddy at full size, for the small cut (favicon). */
|
|
68
|
-
export declare const BUDDY_VIEWBOX: {
|
|
69
|
-
readonly x: 3;
|
|
70
|
-
readonly y: 10.5;
|
|
71
|
-
readonly size: 58;
|
|
72
|
-
};
|
|
73
67
|
/**
|
|
74
68
|
* The mark as SVG children (no outer <svg>). With `front` omitted the blue buddy's body has no fill of its own, so it
|
|
75
69
|
* takes the surrounding `fill` (the Panel's `--logo-fg`, which Portals set with `logoColor`); everything else is fixed.
|
|
@@ -79,5 +73,9 @@ export declare function markSvgBody(front?: string): string;
|
|
|
79
73
|
export declare const MARK_SVG: string;
|
|
80
74
|
/** `<basePath>/icon.svg`: the whole mark on a transparent square, for a Portal's tab or menu (ADR-0006 amendment). */
|
|
81
75
|
export declare const ICON_SVG: string;
|
|
82
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* `<basePath>/favicon.svg`: the small cut for sizes below about 24 px, redrawn for a 16 pt browser tab (ADR-0022):
|
|
78
|
+
* the blue buddy alone on a 32 grid, its body filling the width, short feet and no arms, `>` or mouth, bigger eyes,
|
|
79
|
+
* and a 2-unit outline (about 6% of the canvas). The same rules as the Local tools Portal's tab icons.
|
|
80
|
+
*/
|
|
83
81
|
export declare const FAVICON_SVG: string;
|
|
@@ -46,8 +46,6 @@ export const MARK = {
|
|
|
46
46
|
};
|
|
47
47
|
/** The square the pair is drawn in, in mark units (the Panel's 40 px tile and the app icon tile). */
|
|
48
48
|
export const MARK_VIEWBOX = { x: -2.5, y: 3, size: 69 };
|
|
49
|
-
/** The square around one buddy at full size, for the small cut (favicon). */
|
|
50
|
-
export const BUDDY_VIEWBOX = { x: 3, y: 10.5, size: 58 };
|
|
51
49
|
function buddySvg(b, line, bodyFill) {
|
|
52
50
|
const color = (fill) => {
|
|
53
51
|
if (fill === 'none')
|
|
@@ -83,6 +81,21 @@ export const MARK_SVG = `<svg viewBox="${viewBox(MARK_VIEWBOX)}">${markSvgBody()
|
|
|
83
81
|
/** `<basePath>/icon.svg`: the whole mark on a transparent square, for a Portal's tab or menu (ADR-0006 amendment). */
|
|
84
82
|
export const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox(MARK_VIEWBOX)}" role="img" aria-label="sideby">` +
|
|
85
83
|
`${markSvgBody(MARK_COLORS.blue.body)}</svg>`;
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
/**
|
|
85
|
+
* `<basePath>/favicon.svg`: the small cut for sizes below about 24 px, redrawn for a 16 pt browser tab (ADR-0022):
|
|
86
|
+
* the blue buddy alone on a 32 grid, its body filling the width, short feet and no arms, `>` or mouth, bigger eyes,
|
|
87
|
+
* and a 2-unit outline (about 6% of the canvas). The same rules as the Local tools Portal's tab icons.
|
|
88
|
+
*/
|
|
89
|
+
export const FAVICON_SVG = (() => {
|
|
90
|
+
const blue = MARK_COLORS.blue;
|
|
91
|
+
const ink = MARK_COLORS.ink;
|
|
92
|
+
const light = MARK_COLORS.light;
|
|
93
|
+
const body = BUDDY.paths[2].d;
|
|
94
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="sideby">` +
|
|
95
|
+
`<g stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -1.3)">` +
|
|
96
|
+
`<path d="M8.5 25 Q7.6 31 11.5 30.6 Q14 30.4 14 26.6 M18 26.6 Q18 30.4 20.5 30.6 Q24.4 31 23.5 25" fill="${blue.shade}" stroke="${ink}" stroke-width="2"/>` +
|
|
97
|
+
`<g transform="translate(-7.2 -12.675) scale(0.725)"><path d="${body}" fill="${blue.body}" stroke="${ink}" stroke-width="2.759"/>` +
|
|
98
|
+
`<ellipse cx="22.5" cy="38" rx="5.5" ry="7" fill="${ink}"/><ellipse cx="41.5" cy="38" rx="5.5" ry="7" fill="${ink}"/>` +
|
|
99
|
+
`<ellipse cx="20.6" cy="35.4" rx="2.1" ry="2.6" fill="${light}"/><ellipse cx="39.6" cy="35.4" rx="2.1" ry="2.6" fill="${light}"/>` +
|
|
100
|
+
`</g></g></svg>`);
|
|
101
|
+
})();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Runtime } from '../runtime.ts';
|
|
2
|
+
import type { Account } from '../types.ts';
|
|
3
|
+
export declare const CHROME: {
|
|
4
|
+
title: string;
|
|
5
|
+
bundleId: string;
|
|
6
|
+
bundleNames: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare const CHROME_DOWNLOAD = "https://www.google.com/chrome/";
|
|
9
|
+
/** Chrome's user data folder on macOS. */
|
|
10
|
+
export declare function chromeUserData(home: string): string;
|
|
11
|
+
export interface BrowserProfile {
|
|
12
|
+
/** The profile's folder inside Chrome's user data, such as `Default` or `Profile 7`. */
|
|
13
|
+
dir: string;
|
|
14
|
+
/** The name Chrome shows for it. May be an email; the Panel masks it with identities. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** The Google account this browser user is signed in to Chrome with, if any. */
|
|
17
|
+
email?: string;
|
|
18
|
+
/** Refs of the Accounts this profile signed in to, as far as its local data shows. */
|
|
19
|
+
signIns: string[];
|
|
20
|
+
/** When its Local Storage last changed (ISO 8601), if known. */
|
|
21
|
+
changedAt?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface BrowserReport {
|
|
24
|
+
/** config `browser.signIns`; when false nothing of the browser was read. */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
browser: string;
|
|
27
|
+
/** Chrome's user data folder exists (macOS, and Windows' Chrome on WSL). */
|
|
28
|
+
found: boolean;
|
|
29
|
+
/** Newest change first. */
|
|
30
|
+
profiles: BrowserProfile[];
|
|
31
|
+
/** Families whose Accounts can be matched, and those that can only be opened in a profile the user picks. */
|
|
32
|
+
families: {
|
|
33
|
+
matched: string[];
|
|
34
|
+
openOnly: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Problems met while reading, without file contents. */
|
|
37
|
+
problems: string[];
|
|
38
|
+
}
|
|
39
|
+
/** `id` appears in `key` as a whole segment: not glued to another letter, digit or underscore. */
|
|
40
|
+
export declare function keyHasId(key: string, id: string): boolean;
|
|
41
|
+
/** The Browser Report for `sideby browser` and the Panel. Reads nothing of the browser while it is off. */
|
|
42
|
+
export declare function browserReport(rt: Runtime, accounts: readonly Account[]): Promise<BrowserReport>;
|
|
43
|
+
export interface BrowserOpenResult {
|
|
44
|
+
ref: string;
|
|
45
|
+
url: string;
|
|
46
|
+
profile: {
|
|
47
|
+
dir: string;
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
command: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Opens an Account's vendor website in one Browser Profile: the one given (folder or name), else the only profile
|
|
54
|
+
* that signed in to it. Several or none: refuses with the choices, so the caller can ask. `profile` must be a folder
|
|
55
|
+
* Chrome lists in `Local State`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function openInBrowser(rt: Runtime, account: Account, profile?: string): Promise<BrowserOpenResult>;
|
|
58
|
+
/** What turning Browser Sign-ins on reads, for the confirmation in the CLI and the Panel. */
|
|
59
|
+
export declare const BROWSER_READS: readonly ["the name of each Google Chrome browser user and the Google account email it uses in Chrome (from Chrome's Local State)", 'the key names claude.ai and chatgpt.com keep in each browser user’s Local Storage, never their values', 'the account id in each Claude and Codex login file, only to compare; it is never shown or saved'];
|
|
60
|
+
export interface BrowserSwitch {
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
changed: boolean;
|
|
63
|
+
applied: boolean;
|
|
64
|
+
file: string;
|
|
65
|
+
/** What is read while it is on. */
|
|
66
|
+
reads: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
/** Turns Browser Sign-ins on or off in config `browser.signIns`; without `apply`, shows the change only. */
|
|
69
|
+
export declare function setBrowserSignIns(rt: Runtime, on: boolean, opts: {
|
|
70
|
+
apply: boolean;
|
|
71
|
+
}): Promise<BrowserSwitch>;
|