sideby 0.1.0 → 0.2.1

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.
Files changed (38) hide show
  1. package/README.md +71 -413
  2. package/README.zh-CN.md +71 -414
  3. package/dist/build.json +1 -1
  4. package/dist/src/cli/commands.d.ts +13 -0
  5. package/dist/src/cli/commands.js +86 -7
  6. package/dist/src/cli/json-schemas.js +35 -0
  7. package/dist/src/cli/main.js +99 -9
  8. package/dist/src/core/accounts.d.ts +2 -0
  9. package/dist/src/core/accounts.js +15 -0
  10. package/dist/src/core/config.d.ts +1 -0
  11. package/dist/src/core/config.js +3 -0
  12. package/dist/src/core/launch.d.ts +3 -1
  13. package/dist/src/core/resume.d.ts +9 -0
  14. package/dist/src/core/resume.js +15 -0
  15. package/dist/src/core/share-meaning.d.ts +6 -0
  16. package/dist/src/core/share-meaning.js +32 -0
  17. package/dist/src/core/shell-init.d.ts +7 -1
  18. package/dist/src/core/shell-init.js +25 -2
  19. package/dist/src/families/claude/index.js +3 -0
  20. package/dist/src/families/codex/index.js +3 -0
  21. package/dist/src/families/grok/index.js +4 -0
  22. package/dist/src/families/shared/sessions.d.ts +9 -0
  23. package/dist/src/families/shared/sessions.js +70 -0
  24. package/dist/src/index.d.ts +1 -1
  25. package/dist/src/panel/handler.d.ts +0 -2
  26. package/dist/src/panel/handler.js +1 -16
  27. package/dist/src/runtime.d.ts +48 -2
  28. package/dist/src/runtime.js +54 -1
  29. package/dist/src/types.d.ts +7 -0
  30. package/dist/testing/index.js +2 -1
  31. package/package.json +1 -1
  32. package/schemas/config.json +4 -0
  33. package/schemas/families.json +205 -0
  34. package/schemas/new.json +18 -0
  35. package/skills/sideby/SKILL.md +15 -6
  36. package/skills/sideby/references/accounts.md +1 -1
  37. package/skills/sideby/references/setup.md +31 -0
  38. package/skills/sideby/references/sharing.md +17 -0
@@ -0,0 +1,70 @@
1
+ // Reading which session a Host command resumes, and whether an Account holds it (ADR-0007).
2
+ import { readdir, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ export const isSessionId = (s) => s !== undefined && SESSION_ID.test(s);
6
+ /** Arguments before the first `--`, the ones the Host reads as its own options. */
7
+ const options = (args) => {
8
+ const end = args.indexOf('--');
9
+ return end === -1 ? args : args.slice(0, end);
10
+ };
11
+ /** The id after `--resume <id>`, `--resume=<id>` or `-r <id>`; Claude Code and Grok Build share this form. */
12
+ export function resumeFlagSession(args) {
13
+ const opts = options(args);
14
+ for (let i = 0; i < opts.length; i++) {
15
+ const a = opts[i];
16
+ const id = a.startsWith('--resume=')
17
+ ? a.slice('--resume='.length)
18
+ : a === '--resume' || a === '-r'
19
+ ? opts[i + 1]
20
+ : undefined;
21
+ if (isSessionId(id))
22
+ return id;
23
+ }
24
+ return undefined;
25
+ }
26
+ /** The first UUID after the `resume` subcommand (`codex resume [options] <id>`). */
27
+ export function resumeSubcommandSession(args) {
28
+ const opts = options(args);
29
+ const at = opts.indexOf('resume');
30
+ return at === -1 ? undefined : opts.slice(at + 1).find(isSessionId);
31
+ }
32
+ /** Modification time in ms of the first of `paths` that exists with the wanted type. */
33
+ async function firstWrittenAt(paths, type) {
34
+ for (const p of paths) {
35
+ try {
36
+ const st = await stat(p);
37
+ if (type === 'file' ? st.isFile() : st.isDirectory())
38
+ return st.mtimeMs;
39
+ }
40
+ catch {
41
+ // not here
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+ async function subdirs(dir) {
47
+ try {
48
+ return (await readdir(dir, { withFileTypes: true }))
49
+ .filter((e) => e.isDirectory())
50
+ .map((e) => join(dir, e.name));
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ /** `<dir>/*` + `/<entry>`: one level of grouping directories (Claude projects, Grok working directories). */
57
+ export async function groupedEntryWrittenAt(dir, entry, type) {
58
+ return firstWrittenAt((await subdirs(dir)).map((d) => join(d, entry)), type);
59
+ }
60
+ /** A file anywhere under `dir` whose name ends with `suffix` (Codex `sessions/YYYY/MM/DD/rollout-…-<id>.jsonl`). */
61
+ export async function nestedFileWrittenAt(dir, suffix) {
62
+ let names;
63
+ try {
64
+ names = await readdir(dir, { recursive: true });
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ return firstWrittenAt(names.filter((n) => n.endsWith(suffix)).map((n) => join(dir, n)), 'file');
70
+ }
@@ -1,5 +1,5 @@
1
1
  export { createPanelHandler, type PanelFamily, type PanelHandler, type PanelHandlerOptions, type PanelState, } from './panel/handler.ts';
2
2
  export { startPanelServer } from './panel/server.ts';
3
3
  export type { PanelTheme, PanelThemeTokens } from './panel/theme.ts';
4
- export { type AccountStatus, createRuntime, type DoctorOptions, type FamilyInfo, type Runtime, } from './runtime.ts';
4
+ export { type AccountStatus, createRuntime, type DoctorOptions, type FamilyDetail, type FamilyInfo, type Runtime, type SharedItemInfo, } from './runtime.ts';
5
5
  export type * from './types.ts';
@@ -54,8 +54,6 @@ export declare function healthByAccount(history: DoctorHistory | undefined): Map
54
54
  fail: number;
55
55
  warn: number;
56
56
  }>;
57
- /** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
58
- export declare function aliasesByAccount(aliases: Readonly<Record<string, string>> | undefined, accounts: readonly Account[]): Map<string, string[]>;
59
57
  /** A Family as the page sees it: FamilyInfo plus the mark from its FamilyDef. */
60
58
  export type PanelFamily = FamilyInfo & {
61
59
  logo?: FamilyLogo;
@@ -1,6 +1,6 @@
1
1
  // HTTP handler for the local Panel. Mountable under any basePath; standalone and embedded use the same rules.
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
- import { resolveRef } from '../core/accounts.js';
3
+ import { aliasesByAccount } from '../core/accounts.js';
4
4
  import { aliasArgs, aliasRefs } from '../core/config.js';
5
5
  import { UserError } from '../core/errors.js';
6
6
  import { planHandoff } from '../core/handoff.js';
@@ -31,21 +31,6 @@ export function healthByAccount(history) {
31
31
  });
32
32
  return out;
33
33
  }
34
- /** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
35
- export function aliasesByAccount(aliases, accounts) {
36
- const out = new Map();
37
- for (const [alias, target] of Object.entries(aliases ?? {})) {
38
- let ref;
39
- try {
40
- ref = resolveRef(target, accounts).ref;
41
- }
42
- catch {
43
- continue;
44
- }
45
- out.set(ref, [...(out.get(ref) ?? []), alias].sort());
46
- }
47
- return out;
48
- }
49
34
  /** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
50
35
  export function handoffByFamily(rt, accounts, now) {
51
36
  const out = {};
@@ -5,12 +5,20 @@ import { type DoctorReport } from './core/doctor.ts';
5
5
  import { UserError } from './core/errors.ts';
6
6
  import { type HandoffPlan } from './core/handoff.ts';
7
7
  import { type DoctorHistory } from './core/last-doctor.ts';
8
- import { type PreparedLaunch } from './core/launch.ts';
8
+ import { type HostLaunch, type PreparedLaunch } from './core/launch.ts';
9
9
  import { type Paths } from './core/paths.ts';
10
10
  import { type Shell, type ShellInitWrite } from './core/shell-init.ts';
11
11
  import { type BuiltinPlugin, type LoadedPlugin, type PluginLoadError } from './plugins/loader.ts';
12
- import type { Account, AccountIdentity, Env, FamilyDef, LoginState, QuotaResult, QuotaSetupPlan, ReadContext, UsageResult } from './types.ts';
12
+ import type { Account, AccountIdentity, Env, FamilyDef, LoginState, QuotaResult, QuotaSetupPlan, ReadContext, ShareMode, UsageResult } from './types.ts';
13
13
  export type { AccountIdentity, AccountRefError, DoctorHistory, Shell, ShellInitWrite };
14
+ /** How `sideby resume` starts the Host (spec §3.15). */
15
+ export interface ResumeLaunch {
16
+ launch: HostLaunch;
17
+ /** The session the Host arguments resume by id, when they name one. */
18
+ sessionId?: string;
19
+ /** Set when the session lives in a non-main Account and `launch` starts that Account as `run` would. */
20
+ account?: Account;
21
+ }
14
22
  /** What happened to the alias asked for with a new Account. */
15
23
  export interface AliasResult {
16
24
  name: string;
@@ -45,6 +53,37 @@ export interface FamilyInfo {
45
53
  /** Plugin that provides the Family. */
46
54
  plugin: string;
47
55
  }
56
+ /** One Shared Item as `sideby families` reports it (spec §3.16). */
57
+ export interface SharedItemInfo {
58
+ path: string;
59
+ mode: ShareMode;
60
+ key?: string;
61
+ mainPath?: string;
62
+ credential?: boolean;
63
+ noSymlink?: boolean;
64
+ /** `family` from the FamilyDef, `config` from `extraSharedItems`. */
65
+ source: 'family' | 'config';
66
+ /** What the item looks like in an Account, in one fixed sentence. */
67
+ meaning: string;
68
+ }
69
+ /** A Family's facts for agents and docs: FamilyInfo plus layout, sign-in, sources and Shared Items (spec §3.16). */
70
+ export interface FamilyDetail extends FamilyInfo {
71
+ /** Relative to HOME: `{ main: '.claude', account: '.claude-<name>' }`. */
72
+ layout: {
73
+ main: string;
74
+ account: string;
75
+ };
76
+ selectVar: string;
77
+ /** `args` is null when sign-in happens inside the Host. */
78
+ login: {
79
+ args: string[] | null;
80
+ hint: string;
81
+ };
82
+ quota: boolean;
83
+ usage: boolean;
84
+ quotaSetup: boolean;
85
+ shared: SharedItemInfo[];
86
+ }
48
87
  export interface AccountStatus extends Account {
49
88
  familyTitle: string;
50
89
  hostInstalled: boolean;
@@ -84,6 +123,8 @@ export interface Runtime {
84
123
  plugins: readonly LoadedPlugin[];
85
124
  pluginErrors: readonly PluginLoadError[];
86
125
  familyInfo(): Promise<FamilyInfo[]>;
126
+ /** Every loaded Family with its layout, sign-in, quota support and Shared Items (config extras included). */
127
+ familyDetails(): Promise<FamilyDetail[]>;
87
128
  accounts(): Promise<Account[]>;
88
129
  resolve(ref: string): Promise<Account>;
89
130
  status(account: Account): Promise<AccountStatus>;
@@ -129,6 +170,11 @@ export interface Runtime {
129
170
  /** Rewrites the configured `shellInitFile` of each shell (all configured ones by default). */
130
171
  writeShellInitFiles(shells?: readonly Shell[]): Promise<ShellInitWrite[]>;
131
172
  prepareLaunch(ref: string, userArgs: readonly string[], command: 'run' | 'login'): Promise<PreparedLaunch>;
173
+ /**
174
+ * Starts the non-main Account that holds the session the Host arguments resume, or else the Host unchanged
175
+ * (spec §3.15). Throws UnknownFamilyError, or a UserError when the Family cannot route sessions.
176
+ */
177
+ resumeLaunch(familyId: string, userArgs: readonly string[]): Promise<ResumeLaunch>;
132
178
  quota(account: Account): Promise<QuotaResult>;
133
179
  usage(account: Account): Promise<UsageResult>;
134
180
  /**
@@ -9,6 +9,8 @@ import { identityOf } from './core/identity.js';
9
9
  import { mergeDoctorHistory, readDoctorHistory, writeDoctorHistory, } from './core/last-doctor.js';
10
10
  import { prepareLaunch, which } from './core/launch.js';
11
11
  import { resolvePaths } from './core/paths.js';
12
+ import { findResumeRoute } from './core/resume.js';
13
+ import { shareMeaning } from './core/share-meaning.js';
12
14
  import { SHELLS, shellInitScript, writeShellInitFile, } from './core/shell-init.js';
13
15
  import { packageVersion } from './core/version.js';
14
16
  import { BUILTIN_PLUGINS } from './families/index.js';
@@ -103,6 +105,32 @@ export async function createRuntime(opts = {}) {
103
105
  plugin: loaded.familyOwner.get(f.id) ?? 'unknown',
104
106
  })));
105
107
  },
108
+ async familyDetails() {
109
+ const info = new Map((await rt.familyInfo()).map((f) => [f.id, f]));
110
+ return [...families.values()].map((f) => {
111
+ // The merged list holds the config's own objects, so identity tells which ones came from `extraSharedItems`.
112
+ const fromConfig = new Set(config.extraSharedItems?.[f.id] ?? []);
113
+ return {
114
+ ...info.get(f.id),
115
+ layout: { main: f.layout.main, account: f.layout.account },
116
+ selectVar: f.selectVar,
117
+ login: { args: f.login.args ? [...f.login.args] : null, hint: f.login.hint },
118
+ quota: Boolean(f.readQuota),
119
+ usage: Boolean(f.readUsage),
120
+ quotaSetup: Boolean(f.quotaSetup),
121
+ shared: f.sharedItems.map((item) => ({
122
+ path: item.path,
123
+ mode: item.mode,
124
+ ...(item.key ? { key: item.key } : {}),
125
+ ...(item.mainPath ? { mainPath: item.mainPath } : {}),
126
+ ...(item.credential ? { credential: true } : {}),
127
+ ...(item.noSymlink ? { noSymlink: true } : {}),
128
+ source: fromConfig.has(item) ? 'config' : 'family',
129
+ meaning: shareMeaning(item),
130
+ })),
131
+ };
132
+ });
133
+ },
106
134
  accounts,
107
135
  async resolve(ref) {
108
136
  return resolveRef(ref, await accounts(), aliasRefs(config.aliases));
@@ -291,7 +319,14 @@ export async function createRuntime(opts = {}) {
291
319
  return null;
292
320
  },
293
321
  async shellInitScript() {
294
- return shellInitScript(await accounts(), config.aliases);
322
+ const list = await accounts();
323
+ // Routing helps only a Family that can route and has somewhere else to route to (spec §3.15).
324
+ const routes = config.resumeRouting
325
+ ? [...families.values()]
326
+ .filter((f) => f.resumedSession && f.sessionWrittenAt && list.some((a) => a.family === f.id && !a.isMain))
327
+ .map((f) => ({ family: f.id, bin: f.bin, selectVar: f.selectVar }))
328
+ : [];
329
+ return shellInitScript(list, config.aliases, routes);
295
330
  },
296
331
  async writeShellInitFiles(shells = SHELLS) {
297
332
  const out = [];
@@ -315,6 +350,24 @@ export async function createRuntime(opts = {}) {
315
350
  bus,
316
351
  });
317
352
  },
353
+ async resumeLaunch(familyId, userArgs) {
354
+ const family = familyOf(familyId);
355
+ if (!family.resumedSession || !family.sessionWrittenAt)
356
+ throw new UserError(`${family.title} sessions cannot be routed; start an account yourself with \`sideby run <account> -- …\``);
357
+ const host = { family, bin: family.bin, args: [...userArgs], env };
358
+ // A select variable already set means an Account was chosen on purpose: run the Host as it is.
359
+ if (env[family.selectVar])
360
+ return { launch: host };
361
+ const route = await findResumeRoute(family, await accounts(), userArgs);
362
+ const id = route.sessionId ? { sessionId: route.sessionId } : {};
363
+ if (!route.account || route.account.isMain)
364
+ return { launch: host, ...id };
365
+ return {
366
+ launch: await rt.prepareLaunch(route.account.ref, userArgs, 'run'),
367
+ ...id,
368
+ account: route.account,
369
+ };
370
+ },
318
371
  async quota(account) {
319
372
  if (account.kind === 'api')
320
373
  return { status: 'unavailable', reason: 'api-account' };
@@ -79,6 +79,13 @@ export interface FamilyDef {
79
79
  readUsage?(account: Account, ctx: ReadContext): Promise<UsageResult>;
80
80
  /** One-time, reversible change that lets the Host report Quota (Claude: status line wrapper). */
81
81
  quotaSetup?: QuotaSetup;
82
+ /**
83
+ * The session these Host arguments resume by id (UUID-shaped), or undefined when they start a new session or
84
+ * pick one another way. With `sessionWrittenAt` it lets `sideby resume` find the Account (ADR-0007).
85
+ */
86
+ resumedSession?(args: readonly string[]): string | undefined;
87
+ /** When the Account holds the session, the last write time of its file in ms; otherwise undefined. */
88
+ sessionWrittenAt?(account: Account, sessionId: string): Promise<number | undefined>;
82
89
  }
83
90
  /** Identity fields of a signed-in Account. */
84
91
  export interface AccountIdentity {
@@ -55,8 +55,9 @@ export async function fakeHost(h, name, opts = {}) {
55
55
  const fs = require('node:fs')
56
56
  const log = ${JSON.stringify(log)}
57
57
  const rec = (o) => fs.appendFileSync(log, JSON.stringify(o) + '\\n')
58
- rec({ argv: process.argv.slice(2), env: process.env, pid: process.pid })
58
+ // Handlers first: tests signal the Host as soon as the start record appears.
59
59
  for (const s of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) process.on(s, () => { rec({ signal: s }); process.exit(${opts.sleep ? 100 : 0}) })
60
+ rec({ argv: process.argv.slice(2), env: process.env, pid: process.pid })
60
61
  const finish = () => {
61
62
  ${opts.selfSignal ? `process.removeAllListeners(${JSON.stringify(opts.selfSignal)}); process.kill(process.pid, ${JSON.stringify(opts.selfSignal)})` : `process.exit(${opts.exitCode ?? 0})`}
62
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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
+ "resumeRouting": {
149
+ "type": "boolean",
150
+ "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."
151
+ },
148
152
  "shellInitFile": {
149
153
  "type": "object",
150
154
  "properties": {
@@ -0,0 +1,205 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/families.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "families"
8
+ ],
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "type": "number",
12
+ "const": 1
13
+ },
14
+ "families": {
15
+ "type": "array",
16
+ "items": {
17
+ "allOf": [
18
+ {
19
+ "type": "object",
20
+ "required": [
21
+ "id",
22
+ "title",
23
+ "bin",
24
+ "installUrl",
25
+ "installed",
26
+ "plugin"
27
+ ],
28
+ "properties": {
29
+ "id": {
30
+ "type": "string"
31
+ },
32
+ "title": {
33
+ "type": "string"
34
+ },
35
+ "bin": {
36
+ "type": "string"
37
+ },
38
+ "installUrl": {
39
+ "type": "string"
40
+ },
41
+ "installed": {
42
+ "type": "boolean"
43
+ },
44
+ "plugin": {
45
+ "type": "string"
46
+ }
47
+ }
48
+ },
49
+ {
50
+ "type": "object",
51
+ "required": [
52
+ "layout",
53
+ "selectVar",
54
+ "login",
55
+ "quota",
56
+ "usage",
57
+ "quotaSetup",
58
+ "shared"
59
+ ],
60
+ "properties": {
61
+ "layout": {
62
+ "type": "object",
63
+ "required": [
64
+ "main",
65
+ "account"
66
+ ],
67
+ "properties": {
68
+ "main": {
69
+ "type": "string"
70
+ },
71
+ "account": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "description": "Relative to HOME, for example `.claude` and `.claude-<name>`."
76
+ },
77
+ "selectVar": {
78
+ "type": "string",
79
+ "description": "Environment variable that selects a non-main Account directory."
80
+ },
81
+ "login": {
82
+ "type": "object",
83
+ "required": [
84
+ "args",
85
+ "hint"
86
+ ],
87
+ "properties": {
88
+ "args": {
89
+ "anyOf": [
90
+ {
91
+ "type": "array",
92
+ "items": {
93
+ "type": "string"
94
+ }
95
+ },
96
+ {
97
+ "type": "null"
98
+ }
99
+ ],
100
+ "description": "Arguments of the Host's sign-in command; null when sign-in happens inside the Host."
101
+ },
102
+ "hint": {
103
+ "type": "string"
104
+ }
105
+ }
106
+ },
107
+ "quota": {
108
+ "type": "boolean"
109
+ },
110
+ "usage": {
111
+ "type": "boolean"
112
+ },
113
+ "quotaSetup": {
114
+ "type": "boolean",
115
+ "description": "Quota needs `sideby quota setup <family>` once."
116
+ },
117
+ "shared": {
118
+ "type": "array",
119
+ "items": {
120
+ "type": "object",
121
+ "required": [
122
+ "path",
123
+ "mode",
124
+ "source",
125
+ "meaning"
126
+ ],
127
+ "properties": {
128
+ "path": {
129
+ "type": "string"
130
+ },
131
+ "mode": {
132
+ "anyOf": [
133
+ {
134
+ "type": "string",
135
+ "const": "link"
136
+ },
137
+ {
138
+ "type": "string",
139
+ "const": "copy"
140
+ },
141
+ {
142
+ "type": "string",
143
+ "const": "link-or-copy"
144
+ },
145
+ {
146
+ "type": "string",
147
+ "const": "link-or-local"
148
+ },
149
+ {
150
+ "type": "string",
151
+ "const": "local"
152
+ },
153
+ {
154
+ "type": "string",
155
+ "const": "local-if-api"
156
+ },
157
+ {
158
+ "type": "string",
159
+ "const": "info"
160
+ },
161
+ {
162
+ "type": "string",
163
+ "const": "json-key"
164
+ }
165
+ ]
166
+ },
167
+ "key": {
168
+ "type": "string"
169
+ },
170
+ "mainPath": {
171
+ "type": "string"
172
+ },
173
+ "credential": {
174
+ "type": "boolean"
175
+ },
176
+ "noSymlink": {
177
+ "type": "boolean"
178
+ },
179
+ "source": {
180
+ "anyOf": [
181
+ {
182
+ "type": "string",
183
+ "const": "family"
184
+ },
185
+ {
186
+ "type": "string",
187
+ "const": "config"
188
+ }
189
+ ]
190
+ },
191
+ "meaning": {
192
+ "type": "string",
193
+ "description": "What the item looks like in each Account, in one fixed sentence."
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ }
203
+ },
204
+ "title": "sideby families --json"
205
+ }
package/schemas/new.json CHANGED
@@ -175,6 +175,24 @@
175
175
  }
176
176
  },
177
177
  "description": "One entry per config `shellInitFile`, rewritten after the change."
178
+ },
179
+ "suggestion": {
180
+ "type": "object",
181
+ "required": [
182
+ "name"
183
+ ],
184
+ "properties": {
185
+ "name": {
186
+ "type": "string"
187
+ },
188
+ "alias": {
189
+ "type": "string"
190
+ },
191
+ "aliasProblem": {
192
+ "type": "string"
193
+ }
194
+ },
195
+ "description": "Only with `--next`: the picked name, the short command that follows the existing pattern, or why that short command was left out."
178
196
  }
179
197
  },
180
198
  "title": "sideby new --json"
@@ -1,22 +1,27 @@
1
1
  ---
2
2
  name: sideby
3
- description: Use when the user runs several AI coding accounts (Claude Code, Codex, Grok Build, pi) and wants to see them, check how much quota or usage each has left, move on to another account because one hit its 5-hour or weekly limit, create an account or API account, add a short command (alias) for one, or repair a broken or drifted account directory (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json` and leaves sign-in, credentials and starting sessions to the user.
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.
4
4
  metadata:
5
- version: "0.1.0"
5
+ version: "0.2.1"
6
+ requires:
7
+ bins: [sideby, node]
8
+ cliHelp: sideby --help
6
9
  ---
7
10
 
8
11
  # sideby
9
12
 
10
- `sideby` runs each AI coding account from its own directory (`~/.claude-work`, `~/.codex-api`, ...) and keeps skills, hooks and rules shared with the Main Account (`~/.claude`, `~/.codex`, `~/.grok`, `~/.pi/agent`). Use `npx sideby` if the command is not installed. Requires Node 22.18+.
13
+ `sideby` runs each AI coding account from its own directory (`~/.claude-work`, `~/.codex-api`, ...) and keeps skills, hooks and rules shared with the Main Account (`~/.claude`, `~/.codex`, `~/.grok`, `~/.pi/agent`). Requires Node 22.18+. If `sideby` is not installed, follow `references/setup.md`; `sideby <command> --help` shows examples for `new`, `alias`, `login`, `doctor` and `families`.
14
+
15
+ Do every step you can yourself. Only browser sign-in, secrets and interactive sessions belong to the user: hand each to them as exactly one line to run (in Claude Code they can type `! <command>`), then continue once they say it is done.
11
16
 
12
17
  ## Hard rules
13
18
 
14
19
  - **Never read credential files**: `proxy.env`, `auth.json`, `.claude.json`, or anything under an account directory that may hold a token. Do not `cat`, `grep`, `head`, open or summarize them. `sideby list --json` gives the signed-in email as `identity.email`; that is all you need.
15
- - **Never sign in for the user.** Tell them to run `sideby login <ref>` in their own terminal. OAuth needs their browser and their decision.
20
+ - **Never sign in for the user.** Give them `sideby login <ref>` as one line to run in their own terminal (Claude Code: `! sideby login <ref>`). OAuth needs their browser and their decision.
16
21
  - **Never put a secret on the command line or in chat.** For API accounts, the user edits `proxy.env` themselves.
17
22
  - **Never start a Host session from your shell.** `sideby run`, `sideby next` (without `--dry-run`) and `sideby login` open an interactive session; give the user the command instead.
18
23
  - **Never switch, rotate or proxy accounts.** sideby does not do it, and you must not work around that by editing config, copying session files or moving credentials. Choosing the next account is the user's call; `sideby next` only recommends.
19
- - **Show before you change.** Read-only commands first; changes only after the user has seen what will change.
24
+ - **Say what you change.** Read-only commands first. What the user asked for (the account they asked to create, its short command, the shell line that makes it work) you do, saying what you do. Changes they did not ask for (`doctor --fix` on other accounts, `quota setup`, `app install`) wait until they have seen the change and agreed.
20
25
  - Do not edit Host config files (`settings.json`, `config.toml`, symlinks in account directories) by hand when a `sideby` command does the job.
21
26
 
22
27
  ## Output and exit codes
@@ -31,10 +36,12 @@ Account refs are `<family>:<name>`; a bare `<name>` works only when it is unique
31
36
 
32
37
  | The user wants to | Run | Then read |
33
38
  |---|---|---|
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
+ | Know what accounts share and what stays separate | `sideby families <family> --json` | `references/sharing.md` |
34
41
  | See accounts, health, quota and usage | `sideby list --json`, `sideby doctor --json`, `sideby quota --json` | `references/diagnose.md` |
35
42
  | Repair drift or permissions | `sideby doctor [ref] --json`, then `--fix` after they agree | `references/diagnose.md` |
36
43
  | Keep working because an account hit its limit | `sideby next <family> --json` | `references/handoff.md` |
37
- | Create an account, an API account or a short command | `sideby new`, `sideby alias add` | `references/accounts.md` |
44
+ | Create an account with a given name, an API account, or a short command | `sideby new`, `sideby alias add` | `references/accounts.md` |
38
45
  | See Claude quota (it shows `not-enabled`) | `sideby quota setup claude` (exits 10) | `references/quota.md` |
39
46
  | See everything at once | `sideby ui` (local panel on `127.0.0.1:17420`; `--no-open` prints the address) | — |
40
47
  | Start an account | give them `sideby run <ref> [-- host args]` | — |
@@ -45,6 +52,8 @@ Read only the reference the task needs.
45
52
 
46
53
  `eval "$(sideby shell-init zsh)"` defines one function per account (`sideby-claude-work`) plus the config `aliases`. Users who source a file instead set config `shellInitFile` (for example `{ "zsh": "~/.config/sideby/shell-init.zsh" }`); `new` and `alias add|rm` keep it current, and `sideby shell-init zsh --write` rewrites it. New functions work in a new shell.
47
54
 
55
+ When a terminal manager such as Orca reopens a pane with a bare `claude --resume <id>` (or `codex resume <id>`, `grok --resume <id>`) and the Host says `No conversation found`, the session lives in another account. Suggest config `"resumeRouting": true` followed by `sideby shell-init --write` (or a new shell with `eval`): `shell-init` then defines Host-named functions that call `sideby resume <family>`, which starts the account holding that session. For a one-off, find it with `ls -d ~/.claude*/projects/*/<id>.jsonl` and run `sideby run <ref> -- --resume <id>`.
56
+
48
57
  For someone who would rather double-click than use a terminal, `sideby app install` adds a desktop app (macOS `~/Applications/sideby.app`, Linux app menu) that runs `sideby ui --background`; `sideby ui --stop` stops it and `sideby app uninstall` removes the app. Ask before `app install`: it writes outside sideby's own directories.
49
58
 
50
59
  ## Keeping this skill current
@@ -10,7 +10,7 @@ sideby new claude deepseek --api # API account: ~/.claude-deepseek with a pr
10
10
  sideby new claude 008 --alias cc008 # also adds the short command cc008 to config aliases
11
11
  ```
12
12
 
13
- Only pass `--alias` when the user wants a short command; follow their existing pattern (`cc001`…`cc007` for `claude:001`…`claude:007` suggests `cc008`). An invalid, reserved or taken alias is refused before anything is created (error `code: "alias-invalid"`). If `alias.added` is false in `--json`, the account exists but the alias is missing: tell the user the message, do not edit the config by hand.
13
+ Only pass `--alias` when the user wants a short command. To continue their numbering (`cc001`…`cc007` for `claude:001`…`claude:007`), use `sideby new claude --next --json` instead of a name: it picks `008` and `cc008` the same way the panel does (see `setup.md`). An invalid, reserved or taken alias is refused before anything is created (error `code: "alias-invalid"`). If `alias.added` is false in `--json`, the account exists but the alias is missing: tell the user the message, do not edit the config by hand.
14
14
 
15
15
  Then:
16
16