sideby 0.0.0-stage → 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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/README.zh-CN.md +112 -0
  4. package/dist/build.json +1 -0
  5. package/dist/src/cli/args.d.ts +13 -0
  6. package/dist/src/cli/args.js +38 -0
  7. package/dist/src/cli/commands.d.ts +50 -0
  8. package/dist/src/cli/commands.js +566 -0
  9. package/dist/src/cli/format.d.ts +16 -0
  10. package/dist/src/cli/format.js +80 -0
  11. package/dist/src/cli/json-schemas.d.ts +2 -0
  12. package/dist/src/cli/json-schemas.js +259 -0
  13. package/dist/src/cli/main.d.ts +2 -0
  14. package/dist/src/cli/main.js +276 -0
  15. package/dist/src/cli/schemas.d.ts +1 -0
  16. package/dist/src/cli/schemas.js +16 -0
  17. package/dist/src/core/account-name.d.ts +2 -0
  18. package/dist/src/core/account-name.js +2 -0
  19. package/dist/src/core/accounts.d.ts +20 -0
  20. package/dist/src/core/accounts.js +131 -0
  21. package/dist/src/core/config.d.ts +61 -0
  22. package/dist/src/core/config.js +184 -0
  23. package/dist/src/core/create.d.ts +30 -0
  24. package/dist/src/core/create.js +126 -0
  25. package/dist/src/core/doctor.d.ts +41 -0
  26. package/dist/src/core/doctor.js +171 -0
  27. package/dist/src/core/errors.d.ts +12 -0
  28. package/dist/src/core/errors.js +14 -0
  29. package/dist/src/core/file-memo.d.ts +24 -0
  30. package/dist/src/core/file-memo.js +41 -0
  31. package/dist/src/core/fs-safe.d.ts +38 -0
  32. package/dist/src/core/fs-safe.js +205 -0
  33. package/dist/src/core/handoff.d.ts +41 -0
  34. package/dist/src/core/handoff.js +54 -0
  35. package/dist/src/core/identity.d.ts +23 -0
  36. package/dist/src/core/identity.js +79 -0
  37. package/dist/src/core/last-doctor.d.ts +36 -0
  38. package/dist/src/core/last-doctor.js +69 -0
  39. package/dist/src/core/launch.d.ts +37 -0
  40. package/dist/src/core/launch.js +108 -0
  41. package/dist/src/core/login-path.d.ts +23 -0
  42. package/dist/src/core/login-path.js +96 -0
  43. package/dist/src/core/paths.d.ts +13 -0
  44. package/dist/src/core/paths.js +32 -0
  45. package/dist/src/core/quota-levels.d.ts +19 -0
  46. package/dist/src/core/quota-levels.js +34 -0
  47. package/dist/src/core/resume.d.ts +9 -0
  48. package/dist/src/core/resume.js +15 -0
  49. package/dist/src/core/secret-file.d.ts +30 -0
  50. package/dist/src/core/secret-file.js +116 -0
  51. package/dist/src/core/share-meaning.d.ts +6 -0
  52. package/dist/src/core/share-meaning.js +32 -0
  53. package/dist/src/core/share-mode-names.d.ts +1 -0
  54. package/dist/src/core/share-mode-names.js +10 -0
  55. package/dist/src/core/share-modes.d.ts +24 -0
  56. package/dist/src/core/share-modes.js +405 -0
  57. package/dist/src/core/shell-init.d.ts +31 -0
  58. package/dist/src/core/shell-init.js +84 -0
  59. package/dist/src/core/skill-check.d.ts +11 -0
  60. package/dist/src/core/skill-check.js +57 -0
  61. package/dist/src/core/usage-days.d.ts +15 -0
  62. package/dist/src/core/usage-days.js +33 -0
  63. package/dist/src/core/version.d.ts +9 -0
  64. package/dist/src/core/version.js +46 -0
  65. package/dist/src/desktop/app.d.ts +60 -0
  66. package/dist/src/desktop/app.js +316 -0
  67. package/dist/src/desktop/icon.d.ts +16 -0
  68. package/dist/src/desktop/icon.js +174 -0
  69. package/dist/src/families/claude/index.d.ts +10 -0
  70. package/dist/src/families/claude/index.js +127 -0
  71. package/dist/src/families/claude/layout.d.ts +6 -0
  72. package/dist/src/families/claude/layout.js +5 -0
  73. package/dist/src/families/claude/quota-cache.d.ts +31 -0
  74. package/dist/src/families/claude/quota-cache.js +111 -0
  75. package/dist/src/families/claude/quota.d.ts +3 -0
  76. package/dist/src/families/claude/quota.js +53 -0
  77. package/dist/src/families/claude/usage.d.ts +22 -0
  78. package/dist/src/families/claude/usage.js +146 -0
  79. package/dist/src/families/codex/index.d.ts +5 -0
  80. package/dist/src/families/codex/index.js +70 -0
  81. package/dist/src/families/codex/rollout.d.ts +67 -0
  82. package/dist/src/families/codex/rollout.js +298 -0
  83. package/dist/src/families/grok/index.d.ts +8 -0
  84. package/dist/src/families/grok/index.js +82 -0
  85. package/dist/src/families/index.d.ts +3 -0
  86. package/dist/src/families/index.js +40 -0
  87. package/dist/src/families/logos.d.ts +21 -0
  88. package/dist/src/families/logos.js +26 -0
  89. package/dist/src/families/pi/index.d.ts +3 -0
  90. package/dist/src/families/pi/index.js +54 -0
  91. package/dist/src/families/shared/sessions.d.ts +9 -0
  92. package/dist/src/families/shared/sessions.js +70 -0
  93. package/dist/src/families/shared/toml.d.ts +5 -0
  94. package/dist/src/families/shared/toml.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +4 -0
  97. package/dist/src/panel/background.d.ts +62 -0
  98. package/dist/src/panel/background.js +274 -0
  99. package/dist/src/panel/handler.d.ts +91 -0
  100. package/dist/src/panel/handler.js +396 -0
  101. package/dist/src/panel/i18n.d.ts +287 -0
  102. package/dist/src/panel/i18n.js +570 -0
  103. package/dist/src/panel/page-logic.d.ts +154 -0
  104. package/dist/src/panel/page-logic.js +326 -0
  105. package/dist/src/panel/page-script.d.ts +2 -0
  106. package/dist/src/panel/page-script.js +1633 -0
  107. package/dist/src/panel/page-styles.d.ts +7 -0
  108. package/dist/src/panel/page-styles.js +514 -0
  109. package/dist/src/panel/page.d.ts +20 -0
  110. package/dist/src/panel/page.js +96 -0
  111. package/dist/src/panel/server.d.ts +32 -0
  112. package/dist/src/panel/server.js +133 -0
  113. package/dist/src/panel/theme.d.ts +116 -0
  114. package/dist/src/panel/theme.js +132 -0
  115. package/dist/src/plugins/account-script.d.ts +4 -0
  116. package/dist/src/plugins/account-script.js +66 -0
  117. package/dist/src/plugins/bus.d.ts +33 -0
  118. package/dist/src/plugins/bus.js +84 -0
  119. package/dist/src/plugins/loader.d.ts +58 -0
  120. package/dist/src/plugins/loader.js +250 -0
  121. package/dist/src/quota/claude-statusline.d.ts +28 -0
  122. package/dist/src/quota/claude-statusline.js +301 -0
  123. package/dist/src/quota/statusline-tap.d.ts +11 -0
  124. package/dist/src/quota/statusline-tap.js +104 -0
  125. package/dist/src/runtime.d.ts +195 -0
  126. package/dist/src/runtime.js +424 -0
  127. package/dist/src/types.d.ts +285 -0
  128. package/dist/src/types.js +3 -0
  129. package/dist/testing/demo.d.ts +8 -0
  130. package/dist/testing/demo.js +50 -0
  131. package/dist/testing/index.d.ts +36 -0
  132. package/dist/testing/index.js +118 -0
  133. package/package.json +66 -4
  134. package/schemas/alias.json +102 -0
  135. package/schemas/app-install.json +74 -0
  136. package/schemas/app-uninstall.json +54 -0
  137. package/schemas/config.json +170 -0
  138. package/schemas/doctor.json +229 -0
  139. package/schemas/error.json +25 -0
  140. package/schemas/families.json +205 -0
  141. package/schemas/list.json +177 -0
  142. package/schemas/new.json +199 -0
  143. package/schemas/next.json +127 -0
  144. package/schemas/plugins.json +74 -0
  145. package/schemas/quota-setup.json +60 -0
  146. package/schemas/quota-teardown.json +30 -0
  147. package/schemas/quota.json +203 -0
  148. package/skills/sideby/SKILL.md +61 -0
  149. package/skills/sideby/references/accounts.md +31 -0
  150. package/skills/sideby/references/diagnose.md +47 -0
  151. package/skills/sideby/references/handoff.md +41 -0
  152. package/skills/sideby/references/quota.md +14 -0
  153. package/skills/sideby/references/setup.md +31 -0
  154. package/skills/sideby/references/sharing.md +17 -0
@@ -0,0 +1,174 @@
1
+ // The sideby mark drawn without dependencies: the Panel header's logo (a dark rounded tile with a light disc
2
+ // split into two halves, offset along the diagonal, the second at 55% opacity) rasterised with analytic
3
+ // antialiasing and encoded as PNG, SVG or ICNS.
4
+ import { deflateSync } from 'node:zlib';
5
+ /** Text stored in every generated file, so `sideby app uninstall` deletes only what it wrote. */
6
+ export const APP_MARKER = 'sideby-app-install';
7
+ // Geometry in the Panel's 40-unit logo box (page.ts: .logo 40×40, radius 10; halves of radius 10.5 centred at
8
+ // 18.5,18.5 facing left and 21.5,21.5 facing right, so they sit 3 apart and mirror through the centre).
9
+ const TILE = { size: 40, radius: 10, color: [0x15, 0x17, 0x1c] };
10
+ const MARK = { color: [0xf5, 0xf6, 0xf8], opacities: [1, 0.55] };
11
+ const HALVES = [
12
+ { cx: 18.5, cy: 18.5, r: 10.5, side: -1 },
13
+ { cx: 21.5, cy: 21.5, r: 10.5, side: 1 },
14
+ ];
15
+ // The tile sits on the macOS icon grid: 824 of 1024 units, centred.
16
+ const GRID = { canvas: 1024, tile: 824 };
17
+ function shapes(size) {
18
+ const tilePx = (size * GRID.tile) / GRID.canvas;
19
+ const off = (size - tilePx) / 2;
20
+ const u = tilePx / TILE.size;
21
+ return {
22
+ tile: { x: off, y: off, w: tilePx, h: tilePx, r: TILE.radius * u },
23
+ halves: HALVES.map((h) => ({ cx: off + h.cx * u, cy: off + h.cy * u, r: h.r * u, side: h.side })),
24
+ };
25
+ }
26
+ /** Signed distance from (px, py) to a rounded rectangle; negative inside. */
27
+ function roundedRectDistance(px, py, s) {
28
+ const hx = s.w / 2;
29
+ const hy = s.h / 2;
30
+ const qx = Math.abs(px - (s.x + hx)) - (hx - s.r);
31
+ const qy = Math.abs(py - (s.y + hy)) - (hy - s.r);
32
+ const outside = Math.hypot(Math.max(qx, 0), Math.max(qy, 0));
33
+ return outside + Math.min(Math.max(qx, qy), 0) - s.r;
34
+ }
35
+ /** Signed distance from (px, py) to a half disc: the disc intersected with the half-plane on its side. */
36
+ function halfDistance(px, py, h) {
37
+ return Math.max(Math.hypot(px - h.cx, py - h.cy) - h.r, (px - h.cx) * -h.side);
38
+ }
39
+ /** RGBA pixels (straight alpha) of the icon at `size`×`size`. */
40
+ export function renderIcon(size) {
41
+ const { tile, halves } = shapes(size);
42
+ const layers = [
43
+ { dist: (x, y) => roundedRectDistance(x, y, tile), color: TILE.color, opacity: 1 },
44
+ ...halves.map((h, i) => ({
45
+ dist: (x, y) => halfDistance(x, y, h),
46
+ color: MARK.color,
47
+ opacity: MARK.opacities[i],
48
+ })),
49
+ ];
50
+ const px = new Uint8Array(size * size * 4);
51
+ for (let y = 0; y < size; y++)
52
+ for (let x = 0; x < size; x++) {
53
+ // Premultiplied "over" compositing; coverage from the distance at the pixel centre.
54
+ let r = 0;
55
+ let g = 0;
56
+ let b = 0;
57
+ let a = 0;
58
+ for (const l of layers) {
59
+ const cover = Math.min(1, Math.max(0, 0.5 - l.dist(x + 0.5, y + 0.5)));
60
+ const la = cover * l.opacity;
61
+ if (la === 0)
62
+ continue;
63
+ r = (l.color[0] / 255) * la + r * (1 - la);
64
+ g = (l.color[1] / 255) * la + g * (1 - la);
65
+ b = (l.color[2] / 255) * la + b * (1 - la);
66
+ a = la + a * (1 - la);
67
+ }
68
+ const i = (y * size + x) * 4;
69
+ if (a > 0) {
70
+ px[i] = Math.round((r / a) * 255);
71
+ px[i + 1] = Math.round((g / a) * 255);
72
+ px[i + 2] = Math.round((b / a) * 255);
73
+ px[i + 3] = Math.round(a * 255);
74
+ }
75
+ }
76
+ return px;
77
+ }
78
+ const CRC_TABLE = (() => {
79
+ const t = new Uint32Array(256);
80
+ for (let n = 0; n < 256; n++) {
81
+ let c = n;
82
+ for (let k = 0; k < 8; k++)
83
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
84
+ t[n] = c >>> 0;
85
+ }
86
+ return t;
87
+ })();
88
+ function crc32(buf) {
89
+ let c = 0xffffffff;
90
+ for (const byte of buf)
91
+ c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
92
+ return (c ^ 0xffffffff) >>> 0;
93
+ }
94
+ function chunk(type, data) {
95
+ const head = Buffer.alloc(8);
96
+ head.writeUInt32BE(data.length, 0);
97
+ head.write(type, 4, 'latin1');
98
+ const crc = Buffer.alloc(4);
99
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
100
+ return Buffer.concat([head, data, crc]);
101
+ }
102
+ /** Encodes straight-alpha RGBA pixels as a PNG with a `Software` text chunk holding APP_MARKER. */
103
+ export function encodePng(width, height, rgba) {
104
+ const ihdr = Buffer.alloc(13);
105
+ ihdr.writeUInt32BE(width, 0);
106
+ ihdr.writeUInt32BE(height, 4);
107
+ ihdr.set([8, 6, 0, 0, 0], 8); // 8-bit RGBA, no interlace
108
+ const raw = Buffer.alloc((width * 4 + 1) * height);
109
+ for (let y = 0; y < height; y++) {
110
+ raw[y * (width * 4 + 1)] = 0; // filter: none
111
+ raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
112
+ }
113
+ return Buffer.concat([
114
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
115
+ chunk('IHDR', ihdr),
116
+ chunk('tEXt', Buffer.from(`Software\0${APP_MARKER}`, 'latin1')),
117
+ chunk('IDAT', deflateSync(raw, { level: 9 })),
118
+ chunk('IEND', Buffer.alloc(0)),
119
+ ]);
120
+ }
121
+ export const iconPng = (size) => encodePng(size, size, renderIcon(size));
122
+ /** The macOS `.iconset` file names and their pixel sizes, 16 to 1024. */
123
+ export const ICONSET = [16, 32, 128, 256, 512].flatMap((s) => [
124
+ { name: `icon_${s}x${s}.png`, size: s },
125
+ { name: `icon_${s}x${s}@2x.png`, size: s * 2 },
126
+ ]);
127
+ // ICNS element types for PNG data, by pixel size (`@2x` variants have their own types).
128
+ const ICNS_TYPES = {
129
+ 'icon_16x16.png': 'icp4',
130
+ 'icon_16x16@2x.png': 'ic11',
131
+ 'icon_32x32.png': 'icp5',
132
+ 'icon_32x32@2x.png': 'ic12',
133
+ 'icon_128x128.png': 'ic07',
134
+ 'icon_128x128@2x.png': 'ic13',
135
+ 'icon_256x256.png': 'ic08',
136
+ 'icon_256x256@2x.png': 'ic14',
137
+ 'icon_512x512.png': 'ic09',
138
+ 'icon_512x512@2x.png': 'ic10',
139
+ };
140
+ /** Builds an `.icns` file directly from PNGs; used when `iconutil` is not available. */
141
+ export function encodeIcns(pngs) {
142
+ const parts = [];
143
+ for (const { name } of ICONSET) {
144
+ const data = pngs.get(name);
145
+ if (!data)
146
+ continue;
147
+ const head = Buffer.alloc(8);
148
+ head.write(ICNS_TYPES[name], 0, 'latin1');
149
+ head.writeUInt32BE(data.length + 8, 4);
150
+ parts.push(head, data);
151
+ }
152
+ const body = Buffer.concat(parts);
153
+ const head = Buffer.alloc(8);
154
+ head.write('icns', 0, 'latin1');
155
+ head.writeUInt32BE(body.length + 8, 4);
156
+ return Buffer.concat([head, body]);
157
+ }
158
+ /** The same mark as an SVG (1024-unit canvas), for Linux icon themes. */
159
+ export function iconSvg() {
160
+ const { tile, halves } = shapes(GRID.canvas);
161
+ const hex = (c) => `#${c.map((v) => v.toString(16).padStart(2, '0')).join('')}`;
162
+ const opacityAttr = (opacity) => (opacity === 1 ? '' : ` fill-opacity="${opacity}"`);
163
+ const rect = (s, fill) => ` <rect x="${s.x}" y="${s.y}" width="${s.w}" height="${s.h}" rx="${s.r}" fill="${fill}"/>`;
164
+ // From the top of the cut to its bottom, bulging to the half's side.
165
+ const half = (h, fill, opacity) => ` <path d="M${h.cx} ${h.cy - h.r}A${h.r} ${h.r} 0 0 ${h.side > 0 ? 1 : 0} ${h.cx} ${h.cy + h.r}Z" fill="${fill}"${opacityAttr(opacity)}/>`;
166
+ return [
167
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${GRID.canvas} ${GRID.canvas}" width="${GRID.canvas}" height="${GRID.canvas}">`,
168
+ ` <!-- ${APP_MARKER} -->`,
169
+ rect(tile, hex(TILE.color)),
170
+ ...halves.map((h, i) => half(h, hex(MARK.color), MARK.opacities[i])),
171
+ '</svg>',
172
+ '',
173
+ ].join('\n');
174
+ }
@@ -0,0 +1,10 @@
1
+ import type { Account, FamilyDef, Plugin } from '../../types.ts';
2
+ /** The Main Account keeps `.claude.json` in HOME; other Accounts keep it inside their directory. */
3
+ export declare function claudeJsonOf(account: Account): string;
4
+ /**
5
+ * `oauthAccount.emailAddress` and `organizationName` from `.claude.json`; nothing else is kept. The organization a
6
+ * personal sign-in gets by default ("<name>'s Organization") says nothing new, so it is left out.
7
+ */
8
+ export declare function claudeIdentity(account: Account): Promise<import("../../types.ts").AccountIdentity | undefined>;
9
+ export declare const claudeFamily: FamilyDef;
10
+ export declare const claudePlugin: Plugin;
@@ -0,0 +1,127 @@
1
+ // Built-in Family Plugin for Claude Code (spec §3.4, checked against Claude Code 2.1.289).
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { identityFromJsonFile, identityOf } from '../../core/identity.js';
5
+ import { readSecretFile } from '../../core/secret-file.js';
6
+ import { claudeStatusline } from '../../quota/claude-statusline.js';
7
+ import { BUILTIN_LOGOS } from '../logos.js';
8
+ import { groupedEntryWrittenAt, resumeFlagSession } from '../shared/sessions.js';
9
+ import { CLAUDE_LAYOUT } from './layout.js';
10
+ import { readClaudeQuota } from './quota.js';
11
+ import { readClaudeUsage } from './usage.js';
12
+ const LINKED = [
13
+ 'settings.json',
14
+ 'settings.local.json',
15
+ 'skills',
16
+ 'commands',
17
+ 'plugins',
18
+ 'hooks',
19
+ 'themes',
20
+ 'CLAUDE.md',
21
+ ];
22
+ /** The Main Account keeps `.claude.json` in HOME; other Accounts keep it inside their directory. */
23
+ export function claudeJsonOf(account) {
24
+ return account.isMain ? join(dirname(account.dir), '.claude.json') : join(account.dir, '.claude.json');
25
+ }
26
+ /** Only checks whether `oauthAccount` is present; the parsed content is dropped immediately. */
27
+ async function loginState(account) {
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(await readFile(claudeJsonOf(account), 'utf8'));
31
+ }
32
+ catch {
33
+ return 'unknown';
34
+ }
35
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
36
+ return 'unknown';
37
+ return parsed.oauthAccount != null ? 'logged-in' : 'logged-out';
38
+ }
39
+ /**
40
+ * `oauthAccount.emailAddress` and `organizationName` from `.claude.json`; nothing else is kept. The organization a
41
+ * personal sign-in gets by default ("<name>'s Organization") says nothing new, so it is left out.
42
+ */
43
+ export function claudeIdentity(account) {
44
+ return identityFromJsonFile(claudeJsonOf(account), (data) => {
45
+ const o = data
46
+ ?.oauthAccount;
47
+ if (typeof o !== 'object' || o === null)
48
+ return undefined;
49
+ const org = typeof o.organizationName === 'string' && !/'s Organization$/.test(o.organizationName)
50
+ ? o.organizationName
51
+ : undefined;
52
+ return identityOf(o.emailAddress, org);
53
+ });
54
+ }
55
+ async function model(account) {
56
+ if (account.kind === 'api') {
57
+ if (!account.secretFile)
58
+ return undefined;
59
+ try {
60
+ return (await readSecretFile(account.secretFile)).ANTHROPIC_MODEL || undefined;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ try {
67
+ const s = JSON.parse(await readFile(join(account.dir, 'settings.json'), 'utf8'));
68
+ return typeof s?.model === 'string' && s.model ? s.model : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ export const claudeFamily = {
75
+ id: 'claude',
76
+ title: 'Claude Code',
77
+ logo: BUILTIN_LOGOS.claude,
78
+ bin: 'claude',
79
+ installUrl: 'https://code.claude.com/docs/en/setup',
80
+ selectVar: 'CLAUDE_CONFIG_DIR',
81
+ layout: CLAUDE_LAYOUT,
82
+ // `plugins` alone is too generic: claude-code-router keeps `~/.claude-code-router/plugins`.
83
+ markers: [
84
+ '.claude.json',
85
+ 'settings.json',
86
+ 'settings.local.json',
87
+ 'skills',
88
+ 'commands',
89
+ 'hooks',
90
+ 'themes',
91
+ 'CLAUDE.md',
92
+ 'projects',
93
+ ],
94
+ hijackVars: [
95
+ 'ANTHROPIC_API_KEY',
96
+ 'ANTHROPIC_AUTH_TOKEN',
97
+ 'ANTHROPIC_BASE_URL',
98
+ 'ANTHROPIC_MODEL',
99
+ 'ANTHROPIC_SMALL_FAST_MODEL',
100
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
101
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
102
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
103
+ ],
104
+ apiVars: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL'],
105
+ sharedItems: [
106
+ ...LINKED.map((path) => ({ path, mode: 'link' })),
107
+ { path: '.claude.json', mode: 'json-key', key: 'mcpServers', mainPath: '.claude.json', credential: true },
108
+ ],
109
+ login: {
110
+ args: ['auth', 'login'],
111
+ hint: 'Opens the browser to sign this account in to Claude (`claude auth login`).',
112
+ },
113
+ loginState,
114
+ model,
115
+ identity: claudeIdentity,
116
+ readQuota: readClaudeQuota,
117
+ readUsage: readClaudeUsage,
118
+ quotaSetup: claudeStatusline,
119
+ resumedSession: resumeFlagSession,
120
+ sessionWrittenAt: (account, id) => groupedEntryWrittenAt(join(account.dir, 'projects'), `${id}.jsonl`, 'file'),
121
+ };
122
+ export const claudePlugin = {
123
+ name: 'claude',
124
+ register(api) {
125
+ api.family(claudeFamily);
126
+ },
127
+ };
@@ -0,0 +1,6 @@
1
+ export declare const CLAUDE_MAIN_DIR = ".claude";
2
+ export declare const CLAUDE_ACCOUNT_PREFIX = ".claude-";
3
+ export declare const CLAUDE_LAYOUT: {
4
+ main: string;
5
+ account: string;
6
+ };
@@ -0,0 +1,5 @@
1
+ // Where Claude Code keeps an Account. A leaf module (no imports) shared by the Family, the quota setup and
2
+ // the status line tap, which must not import the Family's index on every refresh.
3
+ export const CLAUDE_MAIN_DIR = '.claude';
4
+ export const CLAUDE_ACCOUNT_PREFIX = '.claude-';
5
+ export const CLAUDE_LAYOUT = { main: CLAUDE_MAIN_DIR, account: `${CLAUDE_ACCOUNT_PREFIX}<name>` };
@@ -0,0 +1,31 @@
1
+ import type { Env, QuotaWindow } from '../../types.ts';
2
+ export interface ClaudeQuotaCache {
3
+ version: 1;
4
+ /** When the status line last reported these windows. */
5
+ observedAt: string;
6
+ windows: QuotaWindow[];
7
+ }
8
+ export declare function claudeQuotaCacheFile(stateDir: string, name: string): string;
9
+ /** sideby's state directory for this environment (same rule as the Runtime). */
10
+ export declare function cacheStateDir(env: Env): string;
11
+ /** Account name from CLAUDE_CONFIG_DIR: unset or `~/.claude` is `main`, `.claude-<name>` is `<name>`, else null. */
12
+ export declare function accountNameFromEnv(env: Env): string | null;
13
+ /** Extracts the 5h and 7d windows from status line JSON; null when there is nothing usable. */
14
+ export declare function parseRateLimits(input: string, observedAt: Date): ClaudeQuotaCache | null;
15
+ /** Writes the cache atomically; creates the private parent directory first (writeFileAtomic does not). */
16
+ export declare function writeQuotaCache(stateDir: string, name: string, cache: ClaudeQuotaCache): Promise<void>;
17
+ export type CacheRead = {
18
+ status: 'ok';
19
+ observedAt: string;
20
+ windows: QuotaWindow[];
21
+ } | {
22
+ status: 'missing';
23
+ } | {
24
+ status: 'unrecognized';
25
+ detail: string;
26
+ };
27
+ /**
28
+ * Reads the cache. Accepts any file with a parseable `observedAt` and at least one valid window; `version`
29
+ * is not enforced so caches written by earlier releases keep working.
30
+ */
31
+ export declare function readQuotaCache(stateDir: string, name: string): Promise<CacheRead>;
@@ -0,0 +1,111 @@
1
+ // The Claude Quota cache: one module owns where it lives, how the Account is named, and how it is written,
2
+ // read and validated. The status line tap writes it on every refresh, so this module imports only leaf
3
+ // modules and Node built-ins (never the Family index, the Runtime or core/accounts.ts).
4
+ import { mkdir, readFile } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
+ import { ACCOUNT_NAME } from '../../core/account-name.js';
8
+ import { writeFileAtomic } from '../../core/fs-safe.js';
9
+ import { resolvePaths } from '../../core/paths.js';
10
+ import { CLAUDE_ACCOUNT_PREFIX, CLAUDE_MAIN_DIR } from './layout.js';
11
+ const WINDOWS = [
12
+ { key: 'five_hour', label: '5h', minutes: 300 },
13
+ { key: 'seven_day', label: '7d', minutes: 10_080 },
14
+ ];
15
+ export function claudeQuotaCacheFile(stateDir, name) {
16
+ return join(stateDir, 'quota', 'claude', `${name}.json`);
17
+ }
18
+ /** sideby's state directory for this environment (same rule as the Runtime). */
19
+ export function cacheStateDir(env) {
20
+ return resolvePaths(env).stateDir;
21
+ }
22
+ /** Account name from CLAUDE_CONFIG_DIR: unset or `~/.claude` is `main`, `.claude-<name>` is `<name>`, else null. */
23
+ export function accountNameFromEnv(env) {
24
+ const dir = env.CLAUDE_CONFIG_DIR;
25
+ if (!dir)
26
+ return 'main';
27
+ if (resolve(dir) === join(env.HOME || homedir(), CLAUDE_MAIN_DIR))
28
+ return 'main';
29
+ const base = basename(dir);
30
+ if (!base.startsWith(CLAUDE_ACCOUNT_PREFIX))
31
+ return null;
32
+ const name = base.slice(CLAUDE_ACCOUNT_PREFIX.length);
33
+ return name !== 'main' && ACCOUNT_NAME.test(name) ? name : null;
34
+ }
35
+ /** Extracts the 5h and 7d windows from status line JSON; null when there is nothing usable. */
36
+ export function parseRateLimits(input, observedAt) {
37
+ let data;
38
+ try {
39
+ data = JSON.parse(input);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ const rl = data?.rate_limits;
45
+ if (typeof rl !== 'object' || rl === null)
46
+ return null;
47
+ const windows = [];
48
+ for (const w of WINDOWS) {
49
+ const v = rl[w.key];
50
+ if (typeof v !== 'object' || v === null)
51
+ continue;
52
+ const used = v.used_percentage;
53
+ const resets = v.resets_at;
54
+ if (typeof used !== 'number' || !Number.isFinite(used))
55
+ continue;
56
+ if (typeof resets !== 'number' || !Number.isFinite(resets) || resets <= 0)
57
+ continue;
58
+ windows.push({
59
+ label: w.label,
60
+ windowMinutes: w.minutes,
61
+ usedPercent: used,
62
+ resetsAt: new Date(resets * 1000).toISOString(),
63
+ });
64
+ }
65
+ return windows.length ? { version: 1, observedAt: observedAt.toISOString(), windows } : null;
66
+ }
67
+ /** Writes the cache atomically; creates the private parent directory first (writeFileAtomic does not). */
68
+ export async function writeQuotaCache(stateDir, name, cache) {
69
+ const file = claudeQuotaCacheFile(stateDir, name);
70
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
71
+ await writeFileAtomic(file, `${JSON.stringify(cache)}\n`, 0o600);
72
+ }
73
+ function validWindow(w) {
74
+ if (typeof w !== 'object' || w === null)
75
+ return false;
76
+ const v = w;
77
+ return (typeof v.label === 'string' &&
78
+ typeof v.windowMinutes === 'number' &&
79
+ typeof v.usedPercent === 'number' &&
80
+ typeof v.resetsAt === 'string' &&
81
+ Number.isFinite(Date.parse(v.resetsAt)));
82
+ }
83
+ /**
84
+ * Reads the cache. Accepts any file with a parseable `observedAt` and at least one valid window; `version`
85
+ * is not enforced so caches written by earlier releases keep working.
86
+ */
87
+ export async function readQuotaCache(stateDir, name) {
88
+ const file = claudeQuotaCacheFile(stateDir, name);
89
+ let raw;
90
+ try {
91
+ raw = await readFile(file, 'utf8');
92
+ }
93
+ catch (err) {
94
+ if (err.code === 'ENOENT')
95
+ return { status: 'missing' };
96
+ throw err;
97
+ }
98
+ let cache;
99
+ try {
100
+ cache = JSON.parse(raw);
101
+ }
102
+ catch {
103
+ return { status: 'unrecognized', detail: `${file} is not valid JSON` };
104
+ }
105
+ const windows = Array.isArray(cache?.windows) ? cache.windows.filter(validWindow) : [];
106
+ if (typeof cache?.observedAt !== 'string' ||
107
+ !Number.isFinite(Date.parse(cache.observedAt)) ||
108
+ !windows.length)
109
+ return { status: 'unrecognized', detail: `${file} has no usable quota windows` };
110
+ return { status: 'ok', observedAt: cache.observedAt, windows };
111
+ }
@@ -0,0 +1,3 @@
1
+ import type { Account, QuotaResult, ReadContext } from '../../types.ts';
2
+ export declare const QUOTA_SOURCE = "claude statusline";
3
+ export declare function readClaudeQuota(account: Account, ctx: ReadContext): Promise<QuotaResult>;
@@ -0,0 +1,53 @@
1
+ // Quota for a Claude subscription Account: the windows `sideby statusline-tap` last cached for it.
2
+ // Windows past `resetsAt` are returned unchanged; callers compare with the current time and show "reset".
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { realpathOrNull } from '../../core/fs-safe.js';
6
+ import { claudeStatusline, isWrapped, mainSettingsFile } from '../../quota/claude-statusline.js';
7
+ import { readQuotaCache } from './quota-cache.js';
8
+ export const QUOTA_SOURCE = 'claude statusline';
9
+ /** How the Account reaches the wrapper: the shared main settings.json, its own wrapped file, or neither. */
10
+ async function accountWrapped(account, ctx) {
11
+ if (account.isMain)
12
+ return 'shared';
13
+ const own = join(account.dir, 'settings.json');
14
+ const [a, b] = await Promise.all([realpathOrNull(own), realpathOrNull(mainSettingsFile(ctx.home))]);
15
+ if (a !== null && a === b)
16
+ return 'shared';
17
+ try {
18
+ const s = JSON.parse(await readFile(own, 'utf8'));
19
+ return isWrapped(s?.statusLine?.command) ? 'own' : 'own-unwrapped';
20
+ }
21
+ catch {
22
+ return 'own-unwrapped';
23
+ }
24
+ }
25
+ async function withoutCache(account, ctx) {
26
+ const wiring = await accountWrapped(account, ctx);
27
+ if (wiring === 'own-unwrapped')
28
+ return {
29
+ status: 'unavailable',
30
+ reason: 'not-enabled',
31
+ detail: "this account's settings.json is not shared with the main account, so its quota needs its own status line setup",
32
+ };
33
+ const planStatus = await claudeStatusline.plan(ctx).then((p) => p.status, () => 'blocked');
34
+ if (wiring === 'shared' && planStatus !== 'enabled')
35
+ return {
36
+ status: 'unavailable',
37
+ reason: 'not-enabled',
38
+ detail: 'run `sideby quota setup claude` to record Claude quota from the status line',
39
+ };
40
+ return {
41
+ status: 'unavailable',
42
+ reason: 'no-session',
43
+ detail: 'start a Claude Code session with this account; quota is recorded after its first reply',
44
+ };
45
+ }
46
+ export async function readClaudeQuota(account, ctx) {
47
+ const read = await readQuotaCache(ctx.stateDir, account.name);
48
+ if (read.status === 'missing')
49
+ return withoutCache(account, ctx);
50
+ if (read.status === 'unrecognized')
51
+ return { status: 'unavailable', reason: 'unrecognized', detail: read.detail };
52
+ return { status: 'ok', windows: read.windows, observedAt: read.observedAt, source: QUOTA_SOURCE };
53
+ }
@@ -0,0 +1,22 @@
1
+ import { FileMemo } from '../../core/file-memo.ts';
2
+ import type { Account, ReadContext, UsageResult } from '../../types.ts';
3
+ interface Entry {
4
+ timestamp: number;
5
+ session: string;
6
+ input: number;
7
+ output: number;
8
+ cacheRead: number;
9
+ cacheWrite: number;
10
+ }
11
+ /** What one session log holds: its usage rows in file order, keyed for the "last line wins" dedupe. */
12
+ interface Parsed {
13
+ lines: number;
14
+ parsed: number;
15
+ rows: [string, Entry][];
16
+ /** The read stopped early; the rows so far still count, but the result is not remembered. */
17
+ failed: boolean;
18
+ }
19
+ /** Parsed session logs per Account directory; a file is parsed again only after it changed. */
20
+ export declare const claudeUsageMemo: FileMemo<Parsed>;
21
+ export declare function readClaudeUsage(account: Account, ctx: ReadContext): Promise<UsageResult>;
22
+ export {};