sideby 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +5 -3
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +7 -0
  4. package/dist/src/cli/commands.js +96 -2
  5. package/dist/src/cli/json-schemas.js +52 -0
  6. package/dist/src/cli/main.js +31 -2
  7. package/dist/src/core/config.d.ts +14 -0
  8. package/dist/src/core/config.js +9 -0
  9. package/dist/src/core/desktop-app.d.ts +96 -0
  10. package/dist/src/core/desktop-app.js +371 -0
  11. package/dist/src/core/doctor.d.ts +6 -1
  12. package/dist/src/core/doctor.js +19 -0
  13. package/dist/src/core/model-choice.d.ts +41 -0
  14. package/dist/src/core/model-choice.js +128 -0
  15. package/dist/src/core/tuning-apply.js +1 -1
  16. package/dist/src/families/claude/desktop.d.ts +6 -0
  17. package/dist/src/families/claude/desktop.js +28 -0
  18. package/dist/src/families/claude/index.js +2 -0
  19. package/dist/src/families/claude/model.d.ts +2 -0
  20. package/dist/src/families/claude/model.js +42 -0
  21. package/dist/src/families/claude/usage.d.ts +2 -0
  22. package/dist/src/families/claude/usage.js +13 -1
  23. package/dist/src/families/codex/desktop.d.ts +7 -0
  24. package/dist/src/families/codex/desktop.js +25 -0
  25. package/dist/src/families/codex/index.js +2 -0
  26. package/dist/src/families/codex/model.d.ts +2 -0
  27. package/dist/src/families/codex/model.js +59 -0
  28. package/dist/src/families/codex/rollout.d.ts +5 -0
  29. package/dist/src/families/codex/rollout.js +45 -3
  30. package/dist/src/families/desktop.d.ts +27 -0
  31. package/dist/src/families/desktop.js +9 -0
  32. package/dist/src/families/shared/model-args.d.ts +7 -0
  33. package/dist/src/families/shared/model-args.js +28 -0
  34. package/dist/src/panel/handler.d.ts +3 -0
  35. package/dist/src/panel/handler.js +63 -5
  36. package/dist/src/panel/i18n.d.ts +27 -0
  37. package/dist/src/panel/i18n.js +54 -0
  38. package/dist/src/panel/page-logic.js +2 -2
  39. package/dist/src/panel/page-script.js +143 -2
  40. package/dist/src/panel/page-styles.js +5 -1
  41. package/dist/src/runtime.d.ts +16 -0
  42. package/dist/src/runtime.js +5 -0
  43. package/dist/src/types.d.ts +29 -0
  44. package/package.json +3 -2
  45. package/schemas/desktop.json +80 -0
  46. package/schemas/list.json +20 -0
  47. package/schemas/model.json +169 -0
  48. package/schemas/quota.json +6 -0
  49. package/skills/sideby/SKILL.md +5 -2
@@ -53,7 +53,7 @@ const MAIN = String.raw `
53
53
  silent: false, lastTry: 0, pending: false, fastUntil: 0, fastFamily: null,
54
54
  }
55
55
  // View state that is not saved: the search text, the attention filter, the Health check filter.
56
- const ui = { query: '', attention: false, allFindings: false, healthRef: null, checkingRef: null, highlight: null, fade: true }
56
+ const ui = { query: '', attention: false, allFindings: false, healthRef: null, checkingRef: null, desktopRef: null, highlight: null, fade: true }
57
57
  const $ = (id) => document.getElementById(id)
58
58
 
59
59
  // ---------- preferences ----------
@@ -573,10 +573,12 @@ const MAIN = String.raw `
573
573
  const items = [
574
574
  { icon: 'terminal', label: t('menu.copyRun'), hint: run, run: () => copyText(run) },
575
575
  ...(a.aliases || []).slice(0, 2).map((x) => ({ icon: 'terminal', label: t('menu.copyAlias'), hint: x, run: () => copyText(x) })),
576
+ a.desktopApp && a.desktopApp.installed && a.kind !== 'api' && !BOOT.readOnly ? { icon: 'terminal', label: t(a.desktopApp.running ? 'desk.show' : 'desk.open', { app: a.desktopApp.app }), disabled: Boolean(ui.desktopRef), run: () => openDesktop(a) } : null,
576
577
  a.kind !== 'api' ? { icon: 'key', label: t('menu.copyLogin'), hint: 'sideby login ' + a.ref, run: () => copyText('sideby login ' + a.ref) } : null,
577
578
  a.dir ? { icon: 'folder', label: t('menu.copyDir'), hint: a.dir, run: () => copyText(a.dir) } : null,
578
579
  a.identity && a.identity.email ? { icon: 'mail', label: t('menu.copyEmail'), hint: emailText(a.identity.email), run: () => copyText(a.identity.email) } : null,
579
580
  '-',
581
+ a.modelInfo && a.modelInfo.settable ? { icon: 'terminal', label: t('menu.model'), run: () => openModel(a) } : null,
580
582
  family(a.family).tuning ? { icon: 'check', label: t('menu.tune'), run: () => openTune(a) } : null,
581
583
  { icon: 'check', label: t('menu.check'), disabled: S.checking || S.fixing || Boolean(ui.checkingRef), run: () => checkAccount(a.ref) },
582
584
  BOOT.readOnly ? null : { icon: 'userPlus', label: t('menu.like'), run: () => openCreate({ family: a.family, api: a.kind === 'api' }) },
@@ -903,9 +905,36 @@ const MAIN = String.raw `
903
905
  return h('div', { class: 'ident-sub' }, dots([
904
906
  h('span', { class: 'mono' }, a.ref),
905
907
  a.kind === 'api' && a.model ? h('span', { class: 'mono', title: t('acct.model') }, a.model) : null,
908
+ a.kind !== 'api' ? modelChip(a) : null,
906
909
  a.kind !== 'api' && plan ? h('span', { class: 'plan' }, plan) : null,
910
+ desktopButton(a),
907
911
  ]))
908
912
  }
913
+ // The family's desktop app as this account (spec §3.20): only where it is installed, never for API accounts or a
914
+ // read-only panel. A running window shows as "open"; the click brings it to the front instead of a second one.
915
+ function desktopButton(a) {
916
+ const d = a.desktopApp
917
+ if (!d || !d.installed || a.kind === 'api' || BOOT.readOnly) return null
918
+ const busy = ui.desktopRef === a.ref
919
+ const label = busy ? t('desk.opening', { app: d.app }) : t(d.running ? 'desk.show' : 'desk.open', { app: d.app })
920
+ return h('button', { class: 'tag tag-btn' + (d.running ? ' tag-ok' : ''), type: 'button', disabled: busy, title: t(d.running ? 'desk.showHint' : 'desk.openHint', { app: d.app, ref: a.ref }), 'aria-label': label + ' (' + a.ref + ')', onclick: (e) => { e.stopPropagation(); openDesktop(a) } },
921
+ d.running ? h('span', { class: 'dot dot-ok', 'aria-hidden': 'true' }) : null, label)
922
+ }
923
+ async function openDesktop(a) {
924
+ if (ui.desktopRef) return
925
+ ui.desktopRef = a.ref
926
+ renderAccounts()
927
+ try {
928
+ const r = await api('/api/account/desktop', { account: a.ref })
929
+ const text = r.action === 'started' ? t('desk.started', { app: r.app, ref: r.ref }) : r.action === 'focused' ? t('desk.focused', { app: r.app, ref: r.ref }) : ''
930
+ if (text) toast(text)
931
+ // Notices say what to do next (sign in, close other windows while signing in); they stay a little longer.
932
+ for (const n of r.notices || []) toast(n, { kind: 'info', duration: 12000 })
933
+ } catch (e) { toast(t('desk.failed', { ref: a.ref, msg: e.message }), { kind: 'fail' }) }
934
+ ui.desktopRef = null
935
+ renderAccounts()
936
+ load({ silent: true })
937
+ }
909
938
  function notes(a) {
910
939
  const fam = family(a.family)
911
940
  const out = []
@@ -918,7 +947,7 @@ const MAIN = String.raw `
918
947
  }
919
948
  // What a row or card shows depends on more than the account (health, view preferences, language); the
920
949
  // fingerprint covers what its click handlers use.
921
- function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a)]) }
950
+ function rowSig(a) { return sig([a, healthOf(a.ref), isNext(a), ui.desktopRef === a.ref]) }
922
951
  // The Account 'sideby next <family>' would pick; the server ranks it with the CLI's rule (spec §3.13).
923
952
  function isNext(a) { return Boolean(S.state && S.state.handoff && S.state.handoff[a.family] === a.ref) }
924
953
  function row(a, now, cols) {
@@ -1979,6 +2008,117 @@ const MAIN = String.raw `
1979
2008
  if (ah.loading) loadHandoffStatus()
1980
2009
  }
1981
2010
 
2011
+ // ---------- model and effort (spec §3.19) ----------
2012
+ // Shown under the account name; the dialog changes what sideby starts this account with, never the Host's files.
2013
+ let md = null
2014
+ function modelFrom(a, from) { return t('md.from.' + from, { host: family(a.family).title }) }
2015
+ function modelChip(a) {
2016
+ const mi = a.modelInfo
2017
+ if (!mi || (!mi.model && !mi.effort)) return null
2018
+ const text = [mi.model && mi.model.value, mi.effort && mi.effort.value].filter(Boolean).join(' · ')
2019
+ const title = [
2020
+ mi.model ? t('md.model') + ': ' + mi.model.value + ' (' + modelFrom(a, mi.model.from) + ')' : null,
2021
+ mi.effort ? t('md.effort') + ': ' + mi.effort.value + ' (' + modelFrom(a, mi.effort.from) + ')' : null,
2022
+ mi.settable ? t('md.click') : null,
2023
+ ].filter(Boolean).join('\n')
2024
+ if (!mi.settable) return h('span', { class: 'mono', title }, text)
2025
+ return h('button', { class: 'mono model-btn' + (mi.model && mi.model.from === 'sideby' ? ' model-set' : ''), type: 'button', title, 'aria-label': t('menu.model') + ' ' + a.ref + ': ' + text, onclick: (e) => { e.stopPropagation(); openModel(a) } }, text)
2026
+ }
2027
+ function mdAccount() { return S.state && S.state.accounts.find((x) => x.ref === md.ref) }
2028
+ function openModel(a) {
2029
+ md = { ref: a.ref, family: a.family, preview: null, busy: false, error: null, form: null }
2030
+ showDialog('model')
2031
+ renderModel()
2032
+ }
2033
+ // The inputs are built once per opening, so typing keeps focus; everything else re-renders around them.
2034
+ function mdForm(a) {
2035
+ if (md.form) return md.form
2036
+ const mi = a.modelInfo
2037
+ const host = family(a.family).title
2038
+ const list = h('datalist', { id: 'md-models' }, mi.models.map((m) => h('option', { value: m })))
2039
+ const model = h('input', { type: 'text', list: 'md-models', autocomplete: 'off', spellcheck: 'false', placeholder: t('md.default') + ' (' + t('md.decides', { host }) + ')', disabled: BOOT.readOnly })
2040
+ model.value = mi.sideby.model || ''
2041
+ const effort = h('select', { disabled: BOOT.readOnly }, h('option', { value: '' }, t('md.default') + ' (' + t('md.decides', { host }) + ')'), mi.efforts.map((e) => h('option', { value: e }, e)))
2042
+ effort.value = mi.sideby.effort || ''
2043
+ const changed = () => { md.preview = null; md.error = null; renderModel() }
2044
+ model.addEventListener('input', () => { if (md.preview) changed() })
2045
+ effort.addEventListener('change', changed)
2046
+ md.form = { model, effort, list }
2047
+ return md.form
2048
+ }
2049
+ function mdChange(a, reset) {
2050
+ const mi = a.modelInfo
2051
+ if (reset) return { model: mi.sideby.model ? null : undefined, effort: mi.sideby.effort ? null : undefined }
2052
+ const f = md.form
2053
+ const m = f.model.value.trim()
2054
+ const ch = {}
2055
+ if (m !== (mi.sideby.model || '')) ch.model = m || null
2056
+ if (f.effort.value !== (mi.sideby.effort || '')) ch.effort = f.effort.value || null
2057
+ return ch
2058
+ }
2059
+ async function mdReview(reset) {
2060
+ const a = mdAccount()
2061
+ if (!a) return
2062
+ const change = mdChange(a, reset)
2063
+ for (const k of Object.keys(change)) if (change[k] === undefined) delete change[k]
2064
+ md.error = null
2065
+ if (!Object.keys(change).length) { md.preview = { nothing: true }; renderModel(); return }
2066
+ md.busy = true
2067
+ renderModel()
2068
+ try {
2069
+ const r = await api('/api/account/model', Object.assign({ account: a.ref }, change))
2070
+ md.preview = r.changed ? { change, diff: r.diff } : { nothing: true }
2071
+ } catch (e) { md.error = e }
2072
+ md.busy = false
2073
+ renderModel()
2074
+ }
2075
+ async function mdApply() {
2076
+ const a = mdAccount()
2077
+ if (!a || !md.preview || !md.preview.change) return
2078
+ md.busy = true
2079
+ dlg.busy = true
2080
+ renderModel()
2081
+ try {
2082
+ await api('/api/account/model', Object.assign({ account: a.ref, confirm: true }, md.preview.change))
2083
+ dlg.busy = false
2084
+ toast(t('md.saved', { ref: a.ref }))
2085
+ dlg.el.close()
2086
+ await load({ silent: true })
2087
+ return
2088
+ } catch (e) { md.error = e }
2089
+ md.busy = false
2090
+ dlg.busy = false
2091
+ renderModel()
2092
+ }
2093
+ function renderModel() {
2094
+ if (!md || dlg.kind !== 'model') return
2095
+ const a = mdAccount()
2096
+ if (!a || !a.modelInfo) { dlg.el.close(); return }
2097
+ const mi = a.modelInfo
2098
+ const host = family(a.family).title
2099
+ const f = mdForm(a)
2100
+ const now = (key) => mi[key] ? h('span', null, h('b', { class: 'mono' }, mi[key].value), ' ', h('span', { class: 'muted small' }, '(' + modelFrom(a, mi[key].from) + ')')) : h('span', { class: 'muted' }, t('md.none'))
2101
+ const body = [h('p', { class: 'muted small' }, t('md.intro', { ref: a.ref, host }))]
2102
+ if (BOOT.readOnly) body.push(h('p', { class: 'note' }, t('banner.readOnly')))
2103
+ body.push(h('div', { class: 'md-now' },
2104
+ h('div', null, h('span', { class: 'muted' }, t('md.model') + ': '), now('model')),
2105
+ h('div', null, h('span', { class: 'muted' }, t('md.effort') + ': '), now('effort'))))
2106
+ body.push(h('div', { class: 'ah-grid' },
2107
+ h('label', { class: 'field' }, h('span', null, t('md.model')), f.model, f.list),
2108
+ h('label', { class: 'field' }, h('span', null, t('md.effort')), f.effort)))
2109
+ if (a.family === 'claude') body.push(h('p', { class: 'muted small' }, t('md.envNote')))
2110
+ if (md.error) body.push(errorNote(md.error, null))
2111
+ if (md.preview && md.preview.nothing) body.push(h('p', { class: 'muted small' }, t('md.nothing')))
2112
+ if (md.preview && md.preview.diff) body.push(h('p', { class: 'small' }, t('md.changes')), diffView(md.preview.diff))
2113
+ const foot = [h('button', { class: 'btn', type: 'button', disabled: md.busy, onclick: () => dlg.el.close() }, t('btn.close'))]
2114
+ if (!BOOT.readOnly) {
2115
+ if (mi.sideby.model || mi.sideby.effort) foot.push(h('button', { class: 'btn', type: 'button', disabled: md.busy, onclick: () => mdReview(true) }, t('md.reset')))
2116
+ if (md.preview && md.preview.change) foot.push(h('button', { class: 'btn btn-primary', type: 'button', disabled: md.busy, 'aria-busy': md.busy ? 'true' : null, onclick: mdApply }, md.busy ? spinner() : null, t('md.apply')))
2117
+ else foot.push(h('button', { class: 'btn btn-primary', type: 'button', disabled: md.busy, onclick: () => mdReview(false) }, md.busy ? spinner() : null, t('md.review')))
2118
+ }
2119
+ setKids(dlg.el, dialogFrame(t('md.title') + ' · ' + a.ref, a.family, body, foot))
2120
+ }
2121
+
1982
2122
  // ---------- the Auto Handoff page ----------
1983
2123
  // Off: one sentence and one button. On: where each account's work would go next, the last hand-overs and what is
1984
2124
  // still missing. Turning on is one plan shown first (hp.preview) and one confirmation; no commands to copy.
@@ -2274,6 +2414,7 @@ const MAIN = String.raw `
2274
2414
  if (dlg.kind === 'create' && nf && !nf.creating) renderCreate()
2275
2415
  else if (dlg.kind === 'setup' && qs) renderSetup()
2276
2416
  else if (dlg.kind === 'tune' && tuneState) renderTune()
2417
+ else if (dlg.kind === 'model' && md) { md.form = null; renderModel() }
2277
2418
  })
2278
2419
  $('theme').addEventListener('click', () => {
2279
2420
  const order = CHOICES.theme
@@ -167,7 +167,11 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
167
167
  .ident-name { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; row-gap: 3px; }
168
168
  .ident-name .name { font-weight: var(--strong-weight); font-size: var(--font-size); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
169
169
  .ident-email { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
170
- .email-btn { all: unset; cursor: pointer; border-radius: 4px; }
170
+ .email-btn, .model-btn { all: unset; cursor: pointer; border-radius: 4px; }
171
+ .model-btn:hover { color: var(--text); text-decoration: underline dotted; }
172
+ .model-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
173
+ .model-set { color: var(--accent); }
174
+ .md-now { display: flex; flex-direction: column; gap: 4px; }
171
175
  .email-btn:hover { color: var(--text); text-decoration: underline dotted; }
172
176
  .email-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
173
177
  .ident-email + .ident-sub { color: var(--faint); margin-top: 0; }
@@ -1,6 +1,7 @@
1
1
  import { type AccountRefError } from './core/accounts.ts';
2
2
  import { type Config, type HandoffSettings } from './core/config.ts';
3
3
  import { type CreateResult } from './core/create.ts';
4
+ import { type DesktopDeps } from './core/desktop-app.ts';
4
5
  import { type DoctorReport } from './core/doctor.ts';
5
6
  import { UserError } from './core/errors.ts';
6
7
  import { type HandoffPlan } from './core/handoff.ts';
@@ -104,6 +105,15 @@ export interface AccountStatus extends Account {
104
105
  identity?: AccountIdentity;
105
106
  /** Plugin errors met while reading this Account (for example an unreadable settings file). */
106
107
  problems?: string[];
108
+ /**
109
+ * The Family's Desktop App for this Account (spec §3.20): macOS only, Subscription Accounts of claude and codex.
110
+ * Filled by `sideby list` and the Panel, which list processes once for all Accounts; `status()` leaves it out.
111
+ */
112
+ desktopApp?: {
113
+ app: string;
114
+ installed: boolean;
115
+ running: boolean;
116
+ };
107
117
  }
108
118
  export interface DoctorOptions {
109
119
  /** Family id or Account ref; all Accounts when omitted. */
@@ -232,4 +242,10 @@ export declare function createRuntime(opts?: {
232
242
  env?: Env;
233
243
  builtins?: BuiltinPlugin[];
234
244
  now?: () => Date;
245
+ /**
246
+ * Internal, for tests: replaces the machine the Desktop App calls use (platform, app folders and the programs
247
+ * `open`, `ps`, `osascript`, `plutil`, `mdfind`; spec §3.20). Omitted, the real machine is used. Not part of
248
+ * the plugin or Portal contract; it may change without notice.
249
+ */
250
+ desktop?: Partial<DesktopDeps>;
235
251
  }): Promise<Runtime>;
@@ -2,6 +2,7 @@
2
2
  import { aliasesByAccount, discoverAccounts, resolveRef } from './core/accounts.js';
3
3
  import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, handoffSettings, loadConfig, removeConfigAlias, } from './core/config.js';
4
4
  import { createAccount } from './core/create.js';
5
+ import { bindDesktopDeps, checkAppData } from './core/desktop-app.js';
5
6
  import { applyFixes, runDoctor } from './core/doctor.js';
6
7
  import { UserError } from './core/errors.js';
7
8
  import { planHandoff } from './core/handoff.js';
@@ -254,6 +255,8 @@ export async function createRuntime(opts = {}) {
254
255
  // Checked for every Family in the run, also when Doctor is aimed at one Account, so the saved result
255
256
  // of that Family keeps its skill warning.
256
257
  skillVersion: packageVersion(),
258
+ // Desktop App data folders sideby made for an Account (spec §3.20).
259
+ extraChecks: (a) => checkAppData(loaded.plugins, paths.home, a),
257
260
  };
258
261
  // Auto Handoff warnings (spec §3.17) join the general Findings; they never fail Doctor.
259
262
  const all = await accounts();
@@ -606,5 +609,7 @@ export async function createRuntime(opts = {}) {
606
609
  },
607
610
  readContext,
608
611
  };
612
+ if (opts.desktop)
613
+ bindDesktopDeps(rt, opts.desktop);
609
614
  return rt;
610
615
  }
@@ -77,6 +77,8 @@ export interface FamilyDef {
77
77
  };
78
78
  loginState?(account: Account): Promise<LoginState>;
79
79
  model?(account: Account): Promise<string | undefined>;
80
+ /** Choosing the model and effort an Account starts with, as Host arguments (spec §3.19, ADR-0014). */
81
+ modelChoice?: FamilyModelChoice;
80
82
  /**
81
83
  * Who the Account is signed in as, from the Host's own login file: identity fields such as the email and the
82
84
  * organization only, never a token or any other credential value (ADR-0003). Return undefined when unknown; an
@@ -224,6 +226,30 @@ export interface TokenTotals {
224
226
  cacheWriteTokens: number;
225
227
  totalTokens: number;
226
228
  }
229
+ /** A model and an effort level; a field left out means the Host's own default. */
230
+ export interface ModelChoice {
231
+ model?: string;
232
+ effort?: string;
233
+ }
234
+ export interface FamilyModelChoice {
235
+ /** Effort levels the Host accepts at launch, lowest first. */
236
+ efforts: readonly string[];
237
+ /** Model names to offer; any other name may be typed. */
238
+ models: readonly string[];
239
+ /**
240
+ * What the Host's own config sets for new sessions of the Account. Reads only the model and effort keys. A
241
+ * per-model effort is looked up for `model` (the model sideby starts it with), else the config's own model, else
242
+ * `lastModel` (the model of the newest session).
243
+ */
244
+ configured(account: Account, model?: string, lastModel?: string): Promise<ModelChoice>;
245
+ /** Host arguments that start a session with `choice`. */
246
+ toArgs(choice: ModelChoice): string[];
247
+ /** The choice in `args`, and the other arguments in their order. */
248
+ fromArgs(args: readonly string[]): {
249
+ choice: ModelChoice;
250
+ rest: string[];
251
+ };
252
+ }
227
253
  export type UsageResult = ({
228
254
  status: 'ok';
229
255
  days: number;
@@ -239,6 +265,9 @@ export type UsageResult = ({
239
265
  }[];
240
266
  /** ISO time of the newest record the reader saw, not later than now. */
241
267
  lastActivityAt?: string;
268
+ /** Model and effort of the newest turn the reader saw, when the logs record them (spec §3.19). */
269
+ lastModel?: string;
270
+ lastEffort?: string;
242
271
  } & TokenTotals) | {
243
272
  status: 'unavailable';
244
273
  reason: 'no-source' | 'no-session' | 'unrecognized';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
5
5
  "license": "MIT",
6
6
  "author": "Adonis0123 (https://github.com/Adonis0123)",
@@ -72,9 +72,10 @@
72
72
  "devDependencies": {
73
73
  "@biomejs/biome": "2.5.15",
74
74
  "@types/node": "^22.18.0",
75
+ "motion": "13.4.3",
75
76
  "typescript": "^7.0.2"
76
77
  },
77
78
  "packageManager": "pnpm@10.15.0",
78
79
  "readmeFilename": "README.md",
79
- "readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Why not just an alias?\n\n`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)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `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 |\n| [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 |\n| 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 |\n\ncc-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.\n\n## Quick start\n\nRequires 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.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`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`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\n<img alt=\"Two terminals: the personal account and the work account running at the same time, then the local quota panel\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif\" width=\"1280\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <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\">\n</picture>\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| 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) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| 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) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| 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) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Desktop app](docs/guide/panel.md#desktop-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby calls no remote service.\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, desktop app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
80
+ "readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Why not just an alias?\n\n`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)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `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 |\n| [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 |\n| 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 |\n\ncc-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.\n\n## Quick start\n\nRequires 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.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`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`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\n<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\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <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\">\n</picture>\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| 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) |\n| 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) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| 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) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| 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) |\n| 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) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby calls no remote service.\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, panel app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
80
81
  }
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/desktop.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "ref",
8
+ "family",
9
+ "app",
10
+ "appPath",
11
+ "action",
12
+ "createdAppData",
13
+ "linked",
14
+ "notices"
15
+ ],
16
+ "properties": {
17
+ "schemaVersion": {
18
+ "type": "number",
19
+ "const": 1
20
+ },
21
+ "ref": {
22
+ "type": "string"
23
+ },
24
+ "family": {
25
+ "type": "string"
26
+ },
27
+ "app": {
28
+ "type": "string",
29
+ "description": "The desktop app, for example `Claude Desktop`."
30
+ },
31
+ "appPath": {
32
+ "type": "string"
33
+ },
34
+ "action": {
35
+ "anyOf": [
36
+ {
37
+ "type": "string",
38
+ "const": "started"
39
+ },
40
+ {
41
+ "type": "string",
42
+ "const": "focused"
43
+ },
44
+ {
45
+ "type": "string",
46
+ "const": "already-running"
47
+ }
48
+ ],
49
+ "description": "`started`: a new window for this account; `focused`: its running window was brought to the front; `already-running`: it runs but could not be brought to the front."
50
+ },
51
+ "appData": {
52
+ "type": "string",
53
+ "description": "This account's app data folder; absent for the main account, which uses the app's default."
54
+ },
55
+ "createdAppData": {
56
+ "type": "boolean"
57
+ },
58
+ "linked": {
59
+ "type": "array",
60
+ "items": {
61
+ "type": "string"
62
+ },
63
+ "description": "Files linked into a new app data folder from the main account's (claude_desktop_config.json)."
64
+ },
65
+ "command": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ },
70
+ "description": "The `open` command sideby ran."
71
+ },
72
+ "notices": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string"
76
+ }
77
+ }
78
+ },
79
+ "title": "sideby desktop --json"
80
+ }
package/schemas/list.json CHANGED
@@ -152,6 +152,26 @@
152
152
  "items": {
153
153
  "type": "string"
154
154
  }
155
+ },
156
+ "desktopApp": {
157
+ "type": "object",
158
+ "required": [
159
+ "app",
160
+ "installed",
161
+ "running"
162
+ ],
163
+ "properties": {
164
+ "app": {
165
+ "type": "string"
166
+ },
167
+ "installed": {
168
+ "type": "boolean"
169
+ },
170
+ "running": {
171
+ "type": "boolean"
172
+ }
173
+ },
174
+ "description": "The family's desktop app for this account (macOS, subscription accounts of claude and codex): whether it is installed and whether this account's own window runs now. Open it with `sideby desktop <account>`."
155
175
  }
156
176
  }
157
177
  }
@@ -0,0 +1,169 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/model.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "account",
8
+ "family",
9
+ "settable",
10
+ "efforts",
11
+ "models",
12
+ "sideby",
13
+ "host",
14
+ "last"
15
+ ],
16
+ "properties": {
17
+ "schemaVersion": {
18
+ "type": "number",
19
+ "const": 1
20
+ },
21
+ "account": {
22
+ "type": "string"
23
+ },
24
+ "family": {
25
+ "type": "string"
26
+ },
27
+ "settable": {
28
+ "type": "boolean",
29
+ "description": "Whether sideby can set the model and effort for this account."
30
+ },
31
+ "reason": {
32
+ "type": "string"
33
+ },
34
+ "efforts": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string"
38
+ }
39
+ },
40
+ "models": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "string"
44
+ },
45
+ "description": "Model names to offer; any other name may be given."
46
+ },
47
+ "sideby": {
48
+ "type": "object",
49
+ "properties": {
50
+ "model": {
51
+ "type": "string"
52
+ },
53
+ "effort": {
54
+ "type": "string"
55
+ }
56
+ }
57
+ },
58
+ "host": {
59
+ "type": "object",
60
+ "properties": {
61
+ "model": {
62
+ "type": "string"
63
+ },
64
+ "effort": {
65
+ "type": "string"
66
+ }
67
+ }
68
+ },
69
+ "last": {
70
+ "type": "object",
71
+ "properties": {
72
+ "model": {
73
+ "type": "string"
74
+ },
75
+ "effort": {
76
+ "type": "string"
77
+ }
78
+ }
79
+ },
80
+ "model": {
81
+ "type": "object",
82
+ "required": [
83
+ "value",
84
+ "from"
85
+ ],
86
+ "properties": {
87
+ "value": {
88
+ "type": "string"
89
+ },
90
+ "from": {
91
+ "anyOf": [
92
+ {
93
+ "type": "string",
94
+ "const": "sideby"
95
+ },
96
+ {
97
+ "type": "string",
98
+ "const": "host"
99
+ },
100
+ {
101
+ "type": "string",
102
+ "const": "last"
103
+ }
104
+ ]
105
+ }
106
+ }
107
+ },
108
+ "effort": {
109
+ "type": "object",
110
+ "required": [
111
+ "value",
112
+ "from"
113
+ ],
114
+ "properties": {
115
+ "value": {
116
+ "type": "string"
117
+ },
118
+ "from": {
119
+ "anyOf": [
120
+ {
121
+ "type": "string",
122
+ "const": "sideby"
123
+ },
124
+ {
125
+ "type": "string",
126
+ "const": "host"
127
+ },
128
+ {
129
+ "type": "string",
130
+ "const": "last"
131
+ }
132
+ ]
133
+ }
134
+ }
135
+ },
136
+ "change": {
137
+ "type": "object",
138
+ "required": [
139
+ "changed",
140
+ "applied",
141
+ "file",
142
+ "diff",
143
+ "args"
144
+ ],
145
+ "properties": {
146
+ "changed": {
147
+ "type": "boolean"
148
+ },
149
+ "applied": {
150
+ "type": "boolean"
151
+ },
152
+ "file": {
153
+ "type": "string"
154
+ },
155
+ "diff": {
156
+ "type": "string"
157
+ },
158
+ "args": {
159
+ "type": "array",
160
+ "items": {
161
+ "type": "string"
162
+ },
163
+ "description": "The account arguments in the config after the change."
164
+ }
165
+ }
166
+ }
167
+ },
168
+ "title": "sideby model --json"
169
+ }