sideby 0.2.2 → 0.3.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/dist/build.json CHANGED
@@ -1 +1 @@
1
- {"id":"20261008T095241-216534"}
1
+ {"id":"20261008T112709-0a27b0"}
@@ -34,6 +34,7 @@ export declare const ConfigSchema: Type.TObject<{
34
34
  noSymlink: Type.TOptional<Type.TBoolean>;
35
35
  credential: Type.TOptional<Type.TBoolean>;
36
36
  }>>>>;
37
+ accountTitle: Type.TOptional<Type.TBoolean>;
37
38
  resumeRouting: Type.TOptional<Type.TBoolean>;
38
39
  shellInitFile: Type.TOptional<Type.TObject<{
39
40
  zsh: Type.TOptional<Type.TString>;
@@ -57,6 +57,9 @@ export const ConfigSchema = Type.Object({
57
57
  description: 'Per-Plugin settings; `enabled: false` turns a Plugin off.',
58
58
  })),
59
59
  extraSharedItems: Type.Optional(Type.Record(Type.String(), Type.Array(SharedItemSchema))),
60
+ accountTitle: Type.Optional(Type.Boolean({
61
+ description: 'Set the terminal title to `[<short command or account>]` when sideby starts an Account, and keep Hosts that allow it (Codex) from replacing it. Off by default.',
62
+ })),
60
63
  resumeRouting: Type.Optional(Type.Boolean({
61
64
  description: 'Make `sideby shell-init` also define Host-named functions (`claude`, `codex`, `grok`) that resume a session by id in the Account that holds it. Off by default.',
62
65
  })),
@@ -8,9 +8,11 @@ export interface PreparedLaunch {
8
8
  env: Env;
9
9
  /** Printed before launching when sign-in happens inside the Host. */
10
10
  notice?: string;
11
+ /** Terminal title set before the Host starts (config `accountTitle`), for example `[codex002]`. */
12
+ title?: string;
11
13
  }
12
14
  /** What `runHost` needs: a prepared Account launch, or the Host run unchanged (`sideby resume`). */
13
- export type HostLaunch = Pick<PreparedLaunch, 'family' | 'bin' | 'args' | 'env'>;
15
+ export type HostLaunch = Pick<PreparedLaunch, 'family' | 'bin' | 'args' | 'env' | 'title'>;
14
16
  /** Finds an executable on PATH; returns null when the Host is not installed. */
15
17
  export declare function which(bin: string, env: Env): Promise<string | null>;
16
18
  /**
@@ -26,6 +28,10 @@ export declare function prepareLaunch(opts: {
26
28
  userArgs: readonly string[];
27
29
  command: 'run' | 'login';
28
30
  bus: HookBus;
31
+ /** What the Host may show as the Account (`SIDEBY_LABEL`): the short command, else the ref. */
32
+ label?: string;
33
+ /** Config `accountTitle`: set the terminal title to `[<label>]` and keep the Host from replacing it. */
34
+ title?: boolean;
29
35
  }): Promise<PreparedLaunch>;
30
36
  /**
31
37
  * Runs the Host with the terminal attached and returns sideby's exit code (spec §3.5):
@@ -34,6 +34,10 @@ export async function prepareLaunch(opts) {
34
34
  Object.assign(env, await readSecretFile(account.secretFile, opts.baseEnv));
35
35
  if (!account.isMain)
36
36
  env[family.selectVar] = account.dir;
37
+ // Which Account this is, for a status line, prompt or hook to show (spec §3.5); set for every Launch, so a value
38
+ // inherited from another sideby Launch never leaks through.
39
+ env.SIDEBY_ACCOUNT = account.ref;
40
+ env.SIDEBY_LABEL = opts.label || account.ref;
37
41
  let args;
38
42
  let notice;
39
43
  if (opts.command === 'login') {
@@ -47,7 +51,15 @@ export async function prepareLaunch(opts) {
47
51
  }
48
52
  }
49
53
  else {
50
- args = [...(family.defaultArgs?.(account, opts.userArgs) ?? []), ...opts.accountArgs, ...opts.userArgs];
54
+ const keep = opts.title && family.keepTitle && !opts.userArgs.some((a) => a.includes(family.keepTitle.key))
55
+ ? family.keepTitle.args
56
+ : [];
57
+ args = [
58
+ ...(family.defaultArgs?.(account, opts.userArgs) ?? []),
59
+ ...keep,
60
+ ...opts.accountArgs,
61
+ ...opts.userArgs,
62
+ ];
51
63
  }
52
64
  await opts.bus.run('launch.before', family.id, {
53
65
  account,
@@ -57,7 +69,19 @@ export async function prepareLaunch(opts) {
57
69
  config: {},
58
70
  command: opts.command,
59
71
  });
60
- return { account, family, bin: family.bin, args, env, ...(notice ? { notice } : {}) };
72
+ // Control characters would end the escape sequence early; labels are shell-safe names, so this is a guard only.
73
+ const title = opts.title
74
+ ? [...`[${env.SIDEBY_LABEL ?? account.ref}]`].filter((ch) => ch >= ' ' && ch !== '\x7f').join('')
75
+ : undefined;
76
+ return {
77
+ account,
78
+ family,
79
+ bin: family.bin,
80
+ args,
81
+ env,
82
+ ...(notice ? { notice } : {}),
83
+ ...(title ? { title } : {}),
84
+ };
61
85
  }
62
86
  /**
63
87
  * Runs the Host with the terminal attached and returns sideby's exit code (spec §3.5):
@@ -67,6 +91,9 @@ export async function prepareLaunch(opts) {
67
91
  * default action so Ctrl-Z suspends sideby together with the Host and `fg` resumes both.
68
92
  */
69
93
  export function runHost(p) {
94
+ // OSC 0 sets the terminal's tab and window title; only on a terminal, never into a pipe.
95
+ if (p.title && process.stdout.isTTY)
96
+ process.stdout.write(`\x1b]0;${p.title}\x07`);
70
97
  return new Promise((resolve) => {
71
98
  const child = spawn(p.bin, p.args, { stdio: 'inherit', env: p.env });
72
99
  const ignore = () => { };
@@ -47,6 +47,8 @@ export const codexFamily = {
47
47
  return [];
48
48
  return ['-c', `${FILE_STORE_KEY}="file"`];
49
49
  },
50
+ // `tui.terminal_title` takes only built-in items; an empty list leaves the title sideby set alone.
51
+ keepTitle: { key: 'tui.terminal_title', args: ['-c', 'tui.terminal_title=[]'] },
50
52
  async loginState(account) {
51
53
  try {
52
54
  return (await stat(join(account.dir, 'auth.json'))).isFile() ? 'logged-in' : 'unknown';
@@ -1,5 +1,5 @@
1
1
  // The one service layer the CLI and the Panel share. Frozen contract for v0.1 (plan: 审核修订).
2
- import { discoverAccounts, resolveRef } from './core/accounts.js';
2
+ import { aliasesByAccount, discoverAccounts, resolveRef } from './core/accounts.js';
3
3
  import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, loadConfig, removeConfigAlias, } from './core/config.js';
4
4
  import { createAccount } from './core/create.js';
5
5
  import { applyFixes, runDoctor } from './core/doctor.js';
@@ -60,6 +60,17 @@ export async function createRuntime(opts = {}) {
60
60
  if (f)
61
61
  families.set(id, { ...f, sharedItems: [...f.sharedItems, ...extra] });
62
62
  }
63
+ /**
64
+ * `SIDEBY_LABEL` for a Launch: the alias it was started through; else the Account's aliases, those without Host
65
+ * arguments first, then by name; else the ref.
66
+ */
67
+ const launchLabel = (ref, account, all) => {
68
+ if (!ref.includes(':') && ownAlias(ref) !== undefined)
69
+ return ref;
70
+ const names = aliasesByAccount(aliasRefs(config.aliases), all).get(account.ref) ?? [];
71
+ const plain = names.filter((n) => aliasArgs(ownAlias(n)).length === 0);
72
+ return plain[0] ?? names[0] ?? account.ref;
73
+ };
63
74
  const accounts = async () => {
64
75
  const out = [];
65
76
  for (const f of families.values())
@@ -338,8 +349,11 @@ export async function createRuntime(opts = {}) {
338
349
  return out;
339
350
  },
340
351
  async prepareLaunch(ref, userArgs, command) {
341
- const account = resolveRef(ref, await accounts(), aliasRefs(config.aliases));
352
+ const all = await accounts();
353
+ const account = resolveRef(ref, all, aliasRefs(config.aliases));
342
354
  return prepareLaunch({
355
+ label: launchLabel(ref, account, all),
356
+ title: config.accountTitle === true,
343
357
  account,
344
358
  family: familyOf(account.family),
345
359
  baseEnv: env,
@@ -67,6 +67,14 @@ export interface FamilyDef {
67
67
  };
68
68
  /** Arguments sideby adds before the Account's and the user's arguments. */
69
69
  defaultArgs?(account: Account, userArgs: readonly string[]): string[];
70
+ /**
71
+ * Host arguments that stop the Host from setting the terminal title for this run, so the title sideby sets with
72
+ * config `accountTitle` (`[<SIDEBY_LABEL>]`) stays. Added after `defaultArgs`, unless a user argument contains `key`.
73
+ */
74
+ keepTitle?: {
75
+ args: string[];
76
+ key: string;
77
+ };
70
78
  loginState?(account: Account): Promise<LoginState>;
71
79
  model?(account: Account): Promise<string | undefined>;
72
80
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Run every AI coding account side by side: Claude Code, Codex, Grok and pi, with one shared config and a quota panel.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -145,6 +145,10 @@
145
145
  }
146
146
  }
147
147
  },
148
+ "accountTitle": {
149
+ "type": "boolean",
150
+ "description": "Set the terminal title to `[<short command or account>]` when sideby starts an Account, and keep Hosts that allow it (Codex) from replacing it. Off by default."
151
+ },
148
152
  "resumeRouting": {
149
153
  "type": "boolean",
150
154
  "description": "Make `sideby shell-init` also define Host-named functions (`claude`, `codex`, `grok`) that resume a session by id in the Account that holds it. Off by default."
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: sideby
3
- description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
3
+ description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
4
4
  metadata:
5
- version: "0.2.2"
5
+ version: "0.3.0"
6
6
  requires:
7
7
  bins: [sideby, node]
8
8
  cliHelp: sideby --help
@@ -37,6 +37,7 @@ Account refs are `<family>:<name>`; a bare `<name>` works only when it is unique
37
37
  | The user wants to | Run | Then read |
38
38
  |---|---|---|
39
39
  | Install sideby, or set up a new account ("add cc008") | `sideby --version`, `sideby list --json`, `sideby new <family> --next --json` | `references/setup.md` |
40
+ | Show which account a session is (`[cc001]` in the status line or tab title) | `sideby list --json` for the labels | `references/badge.md` |
40
41
  | Know what accounts share and what stays separate | `sideby families <family> --json` | `references/sharing.md` |
41
42
  | See accounts, health, quota and usage | `sideby list --json`, `sideby doctor --json`, `sideby quota --json` | `references/diagnose.md` |
42
43
  | Repair drift or permissions | `sideby doctor [ref] --json`, then `--fix` after they agree | `references/diagnose.md` |
@@ -0,0 +1,31 @@
1
+ # Show which account a session is
2
+
3
+ For "show the account in the status line", "加上 [cc001] 这个标识". sideby sets `SIDEBY_ACCOUNT` (`claude:001`) and `SIDEBY_LABEL` (`cc001`, the short command, else the ref) for every account it starts. Configure each Host the user runs; tell them what you change. Always check the family prefix of `SIDEBY_ACCOUNT`, because a Host started inside another inherits both variables.
4
+
5
+ ## Claude Code: status line
6
+
7
+ 1. Read `statusLine` in `~/.claude/settings.json` (linked, so one edit covers every account). Do not touch the env section or anything else in it.
8
+ 2. If `command` is `sideby statusline-tap --orig-b64 <base64>`, the real script is the base64-decoded original: edit that script, never the wrapper.
9
+ 3. If there is a script, add before its first output: `case "${SIDEBY_ACCOUNT-}" in claude:*) printf '[%s] ' "$SIDEBY_LABEL" ;; esac`. Keep the rest.
10
+ 4. If there is no status line: write `~/.claude/statusline.sh` (badge, then `jq -r '.model.display_name // empty'` from stdin), `chmod +x` it, and set `"statusLine": { "type": "command", "command": "~/.claude/statusline.sh" }`. When quota setup was already on with no original command, run `sideby quota teardown claude` first and `sideby quota setup claude` again after (it exits 10; get the user's yes before `--yes`).
11
+
12
+ ## Grok Build: status line
13
+
14
+ 1. Write `~/.grok/statusline.sh`, `chmod +x`: badge from `SIDEBY_LABEL` when `SIDEBY_ACCOUNT` is `grok:*`, else the `GROK_HOME` directory name; then `jq -r` of `.workspace.current_dir`, `.model.display_name` and `.context_window.used_percentage` from stdin, because `type = "command"` replaces the built-in row.
15
+ 2. In `~/.grok/config.toml`, after the `[ui]` keys, add:
16
+
17
+ ```toml
18
+ [ui.status_line]
19
+ type = "command"
20
+ command = "~/.grok/statusline.sh"
21
+ ```
22
+
23
+ Never add a second `[ui]` table; if `[ui.status_line]` exists, change it.
24
+ 3. `sideby doctor grok --fix` copies `config.toml` to subscription accounts (it must be a real copy there). API accounts (`kind: "api"`) keep their own `config.toml`: add the same table to each.
25
+ 4. `GROK_HOME=<dir> grok inspect` must not report the new keys as unrecognized. It takes effect in a new Grok session.
26
+
27
+ ## Codex: terminal tab title
28
+
29
+ Codex's status line and title accept only built-in items, so the badge goes in the tab title. Set `"accountTitle": true` in `~/.config/sideby/config.json`, keeping every other key. sideby then sets the title to `[codex002]` at launch and starts Codex with `-c tui.terminal_title=[]` so Codex keeps it; the title no longer shows Codex's own spinner. Check `sideby --version` first: it must be 0.3.0 or later. An older sideby rejects the unknown key and then every sideby command and short command fails; upgrade with `npm i -g sideby` before adding it.
30
+
31
+ Finish by telling the user to start a new session (`cc001`, `grok001`, `codex002`) to see it.
@@ -15,8 +15,9 @@ For "install sideby", "set up another Claude account", "add cc008", "配置一
15
15
  - Subscription account: `sideby login <ref>`. It opens the Host's sign-in in their browser. For pi it starts pi; they type `/login`.
16
16
  - API account: they open the `proxy.env` path that `new` printed and fill in the values. Never read or write it, and never ask for the key in chat.
17
17
  7. **Short command in new terminals.** If `~/.config/sideby/config.json` has no `shellInitFile`, and `~/.zshrc` (bash: `~/.bashrc`) has no line containing `sideby shell-init`, say so and append `eval "$(sideby shell-init zsh)"` (or `bash`). It takes effect in new terminals. Skip this when the user wanted no short command.
18
- 8. **Verify.** `sideby doctor <ref> --json` should report no `fail`; fix only the new account with `sideby doctor <ref> --fix`. After the user signed in, `sideby list --json` shows `login: "logged-in"` for it.
19
- 9. **Optional, Claude Code:** for quota in `sideby quota` and the panel, follow `quota.md` (it needs the user's yes).
18
+ 8. **Account badge.** Offer to show which account each session is (`[cc008]` in the status line or tab title); if they want it, follow `badge.md`.
19
+ 9. **Verify.** `sideby doctor <ref> --json` should report no `fail`; fix only the new account with `sideby doctor <ref> --fix`. After the user signed in, `sideby list --json` shows `login: "logged-in"` for it.
20
+ 10. **Optional, Claude Code:** for quota in `sideby quota` and the panel, follow `quota.md` (it needs the user's yes).
20
21
 
21
22
  Finish with how to start it: the short command in a new terminal (`cc008`), or `sideby run <ref>`; and `sideby ui` to see every account's quota.
22
23