sideby 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +453 -2
- package/README.zh-CN.md +455 -0
- package/dist/build.json +1 -0
- package/dist/src/cli/args.d.ts +13 -0
- package/dist/src/cli/args.js +38 -0
- package/dist/src/cli/commands.d.ts +37 -0
- package/dist/src/cli/commands.js +487 -0
- package/dist/src/cli/format.d.ts +16 -0
- package/dist/src/cli/format.js +80 -0
- package/dist/src/cli/json-schemas.d.ts +2 -0
- package/dist/src/cli/json-schemas.js +224 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +186 -0
- package/dist/src/cli/schemas.d.ts +1 -0
- package/dist/src/cli/schemas.js +16 -0
- package/dist/src/core/account-name.d.ts +2 -0
- package/dist/src/core/account-name.js +2 -0
- package/dist/src/core/accounts.d.ts +18 -0
- package/dist/src/core/accounts.js +116 -0
- package/dist/src/core/config.d.ts +60 -0
- package/dist/src/core/config.js +181 -0
- package/dist/src/core/create.d.ts +30 -0
- package/dist/src/core/create.js +126 -0
- package/dist/src/core/doctor.d.ts +41 -0
- package/dist/src/core/doctor.js +171 -0
- package/dist/src/core/errors.d.ts +12 -0
- package/dist/src/core/errors.js +14 -0
- package/dist/src/core/file-memo.d.ts +24 -0
- package/dist/src/core/file-memo.js +41 -0
- package/dist/src/core/fs-safe.d.ts +38 -0
- package/dist/src/core/fs-safe.js +205 -0
- package/dist/src/core/handoff.d.ts +41 -0
- package/dist/src/core/handoff.js +54 -0
- package/dist/src/core/identity.d.ts +23 -0
- package/dist/src/core/identity.js +79 -0
- package/dist/src/core/last-doctor.d.ts +36 -0
- package/dist/src/core/last-doctor.js +69 -0
- package/dist/src/core/launch.d.ts +35 -0
- package/dist/src/core/launch.js +108 -0
- package/dist/src/core/login-path.d.ts +23 -0
- package/dist/src/core/login-path.js +96 -0
- package/dist/src/core/paths.d.ts +13 -0
- package/dist/src/core/paths.js +32 -0
- package/dist/src/core/quota-levels.d.ts +19 -0
- package/dist/src/core/quota-levels.js +34 -0
- package/dist/src/core/secret-file.d.ts +30 -0
- package/dist/src/core/secret-file.js +116 -0
- package/dist/src/core/share-mode-names.d.ts +1 -0
- package/dist/src/core/share-mode-names.js +10 -0
- package/dist/src/core/share-modes.d.ts +24 -0
- package/dist/src/core/share-modes.js +405 -0
- package/dist/src/core/shell-init.d.ts +25 -0
- package/dist/src/core/shell-init.js +61 -0
- package/dist/src/core/skill-check.d.ts +11 -0
- package/dist/src/core/skill-check.js +57 -0
- package/dist/src/core/usage-days.d.ts +15 -0
- package/dist/src/core/usage-days.js +33 -0
- package/dist/src/core/version.d.ts +9 -0
- package/dist/src/core/version.js +46 -0
- package/dist/src/desktop/app.d.ts +60 -0
- package/dist/src/desktop/app.js +316 -0
- package/dist/src/desktop/icon.d.ts +16 -0
- package/dist/src/desktop/icon.js +174 -0
- package/dist/src/families/claude/index.d.ts +10 -0
- package/dist/src/families/claude/index.js +124 -0
- package/dist/src/families/claude/layout.d.ts +6 -0
- package/dist/src/families/claude/layout.js +5 -0
- package/dist/src/families/claude/quota-cache.d.ts +31 -0
- package/dist/src/families/claude/quota-cache.js +111 -0
- package/dist/src/families/claude/quota.d.ts +3 -0
- package/dist/src/families/claude/quota.js +53 -0
- package/dist/src/families/claude/usage.d.ts +22 -0
- package/dist/src/families/claude/usage.js +146 -0
- package/dist/src/families/codex/index.d.ts +5 -0
- package/dist/src/families/codex/index.js +67 -0
- package/dist/src/families/codex/rollout.d.ts +67 -0
- package/dist/src/families/codex/rollout.js +298 -0
- package/dist/src/families/grok/index.d.ts +8 -0
- package/dist/src/families/grok/index.js +78 -0
- package/dist/src/families/index.d.ts +3 -0
- package/dist/src/families/index.js +40 -0
- package/dist/src/families/logos.d.ts +21 -0
- package/dist/src/families/logos.js +26 -0
- package/dist/src/families/pi/index.d.ts +3 -0
- package/dist/src/families/pi/index.js +54 -0
- package/dist/src/families/shared/toml.d.ts +5 -0
- package/dist/src/families/shared/toml.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/panel/background.d.ts +62 -0
- package/dist/src/panel/background.js +274 -0
- package/dist/src/panel/handler.d.ts +93 -0
- package/dist/src/panel/handler.js +411 -0
- package/dist/src/panel/i18n.d.ts +287 -0
- package/dist/src/panel/i18n.js +570 -0
- package/dist/src/panel/page-logic.d.ts +154 -0
- package/dist/src/panel/page-logic.js +326 -0
- package/dist/src/panel/page-script.d.ts +2 -0
- package/dist/src/panel/page-script.js +1633 -0
- package/dist/src/panel/page-styles.d.ts +7 -0
- package/dist/src/panel/page-styles.js +514 -0
- package/dist/src/panel/page.d.ts +20 -0
- package/dist/src/panel/page.js +96 -0
- package/dist/src/panel/server.d.ts +32 -0
- package/dist/src/panel/server.js +133 -0
- package/dist/src/panel/theme.d.ts +116 -0
- package/dist/src/panel/theme.js +132 -0
- package/dist/src/plugins/account-script.d.ts +4 -0
- package/dist/src/plugins/account-script.js +66 -0
- package/dist/src/plugins/bus.d.ts +33 -0
- package/dist/src/plugins/bus.js +84 -0
- package/dist/src/plugins/loader.d.ts +58 -0
- package/dist/src/plugins/loader.js +250 -0
- package/dist/src/quota/claude-statusline.d.ts +28 -0
- package/dist/src/quota/claude-statusline.js +301 -0
- package/dist/src/quota/statusline-tap.d.ts +11 -0
- package/dist/src/quota/statusline-tap.js +104 -0
- package/dist/src/runtime.d.ts +149 -0
- package/dist/src/runtime.js +371 -0
- package/dist/src/types.d.ts +278 -0
- package/dist/src/types.js +3 -0
- package/dist/testing/demo.d.ts +8 -0
- package/dist/testing/demo.js +50 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.js +117 -0
- package/package.json +66 -4
- package/schemas/alias.json +102 -0
- package/schemas/app-install.json +74 -0
- package/schemas/app-uninstall.json +54 -0
- package/schemas/config.json +166 -0
- package/schemas/doctor.json +229 -0
- package/schemas/error.json +25 -0
- package/schemas/list.json +177 -0
- package/schemas/new.json +181 -0
- package/schemas/next.json +127 -0
- package/schemas/plugins.json +74 -0
- package/schemas/quota-setup.json +60 -0
- package/schemas/quota-teardown.json +30 -0
- package/schemas/quota.json +203 -0
- package/skills/sideby/SKILL.md +52 -0
- package/skills/sideby/references/accounts.md +31 -0
- package/skills/sideby/references/diagnose.md +47 -0
- package/skills/sideby/references/handoff.md +41 -0
- package/skills/sideby/references/quota.md +14 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Reads Codex Quota and Usage from the Account's local rollout files (spec §3.7, ADR-0003).
|
|
2
|
+
// No network, no credentials: only `token_count` events the Host writes into `sessions/**/rollout-*.jsonl`.
|
|
3
|
+
// Each file's parse is remembered until the file changes (core/file-memo.ts), so a refresh reads only new rollouts.
|
|
4
|
+
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
6
|
+
import { join, relative } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { FileMemo } from '../../core/file-memo.js';
|
|
9
|
+
import { USAGE_DAYS } from '../../core/quota-levels.js';
|
|
10
|
+
import { dailyBuckets } from '../../core/usage-days.js';
|
|
11
|
+
export const MAX_QUOTA_FILES = 20;
|
|
12
|
+
const ROLLOUT = /^rollout-.*\.jsonl$/;
|
|
13
|
+
const MAX_RESETS_AT_SECONDS = 1e11;
|
|
14
|
+
function isObj(v) {
|
|
15
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
function isNum(v) {
|
|
18
|
+
return typeof v === 'number' && Number.isFinite(v);
|
|
19
|
+
}
|
|
20
|
+
/** Every rollout file under `<dir>/sessions`, newest mtime first. */
|
|
21
|
+
export async function listRollouts(dir) {
|
|
22
|
+
const root = join(dir, 'sessions');
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (!e.isFile() || !ROLLOUT.test(e.name))
|
|
33
|
+
continue;
|
|
34
|
+
const path = join(e.parentPath, e.name);
|
|
35
|
+
try {
|
|
36
|
+
const st = await stat(path);
|
|
37
|
+
out.push({ path, mtimeMs: st.mtimeMs, stats: st });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// removed while scanning
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Yields parsed `token_count` payloads with their event time; bad and truncated lines are skipped. A read error
|
|
47
|
+
* ends the file early and sets `read.failed`, so a caller does not remember a partial parse.
|
|
48
|
+
*/
|
|
49
|
+
async function* tokenCounts(file, read = { failed: false }) {
|
|
50
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Number.POSITIVE_INFINITY });
|
|
51
|
+
try {
|
|
52
|
+
for await (const line of lines) {
|
|
53
|
+
if (!line.includes('token_count'))
|
|
54
|
+
continue;
|
|
55
|
+
let o;
|
|
56
|
+
try {
|
|
57
|
+
o = JSON.parse(line);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isObj(o) || !isObj(o.payload) || o.payload.type !== 'token_count')
|
|
63
|
+
continue;
|
|
64
|
+
const at = typeof o.timestamp === 'string' ? Date.parse(o.timestamp) : Number.NaN;
|
|
65
|
+
if (!Number.isFinite(at))
|
|
66
|
+
continue;
|
|
67
|
+
yield { at, payload: o.payload };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// unreadable file: keep what was read
|
|
72
|
+
read.failed = true;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
lines.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function windowLabel(minutes) {
|
|
79
|
+
if (minutes === 300)
|
|
80
|
+
return '5h';
|
|
81
|
+
if (minutes === 10080)
|
|
82
|
+
return '7d';
|
|
83
|
+
return `${minutes}m`;
|
|
84
|
+
}
|
|
85
|
+
function quotaWindow(raw) {
|
|
86
|
+
if (!isObj(raw))
|
|
87
|
+
return null;
|
|
88
|
+
const { used_percent: used, window_minutes: minutes, resets_at: resets } = raw;
|
|
89
|
+
if (!isNum(used) || !isNum(minutes) || minutes <= 0)
|
|
90
|
+
return null;
|
|
91
|
+
// Seconds since the epoch; a millisecond value would land tens of thousands of years ahead.
|
|
92
|
+
if (!isNum(resets) || resets <= 0 || resets >= MAX_RESETS_AT_SECONDS)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
label: windowLabel(minutes),
|
|
96
|
+
windowMinutes: minutes,
|
|
97
|
+
usedPercent: used,
|
|
98
|
+
resetsAt: new Date(resets * 1000).toISOString(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function rateLimits(payload) {
|
|
102
|
+
const rl = payload.rate_limits;
|
|
103
|
+
if (!isObj(rl))
|
|
104
|
+
return null;
|
|
105
|
+
const primary = quotaWindow(rl.primary);
|
|
106
|
+
if (!primary)
|
|
107
|
+
return null;
|
|
108
|
+
const windows = [primary];
|
|
109
|
+
if (rl.secondary !== null && rl.secondary !== undefined) {
|
|
110
|
+
const secondary = quotaWindow(rl.secondary);
|
|
111
|
+
if (!secondary)
|
|
112
|
+
return null;
|
|
113
|
+
windows.push(secondary);
|
|
114
|
+
}
|
|
115
|
+
return { windows, ...(typeof rl.plan_type === 'string' ? { plan: rl.plan_type } : {}) };
|
|
116
|
+
}
|
|
117
|
+
async function fileLimits(file) {
|
|
118
|
+
const read = { failed: false };
|
|
119
|
+
let best = null;
|
|
120
|
+
for await (const { at, payload } of tokenCounts(file, read)) {
|
|
121
|
+
if (best && at < best.at)
|
|
122
|
+
continue;
|
|
123
|
+
const rl = rateLimits(payload);
|
|
124
|
+
if (rl)
|
|
125
|
+
best = { at, ...rl };
|
|
126
|
+
}
|
|
127
|
+
return { best, failed: read.failed };
|
|
128
|
+
}
|
|
129
|
+
/** Parsed `rate_limits` per Account directory; a rollout is read again only after it changed. */
|
|
130
|
+
export const codexQuotaMemo = new FileMemo();
|
|
131
|
+
/** Newest well-formed `rate_limits` across the newest rollout files, by event timestamp. */
|
|
132
|
+
export async function readCodexQuota(dir) {
|
|
133
|
+
const files = (await listRollouts(dir)).slice(0, MAX_QUOTA_FILES);
|
|
134
|
+
if (files.length === 0)
|
|
135
|
+
return { status: 'unavailable', reason: 'no-session', detail: 'no Codex session in this account yet' };
|
|
136
|
+
let best = null;
|
|
137
|
+
const memo = codexQuotaMemo.pass(dir);
|
|
138
|
+
for (const f of files) {
|
|
139
|
+
// Same pick as reading every line in this order: the newest event wins, a later one on a tie.
|
|
140
|
+
const fb = (await memo.get(f.path, f.stats, () => fileLimits(f.path), (v) => !v.failed)).best;
|
|
141
|
+
if (fb && (!best || fb.at >= best.at))
|
|
142
|
+
best = { ...fb, file: f.path };
|
|
143
|
+
}
|
|
144
|
+
memo.done();
|
|
145
|
+
if (!best)
|
|
146
|
+
return {
|
|
147
|
+
status: 'unavailable',
|
|
148
|
+
reason: 'unrecognized',
|
|
149
|
+
detail: `no rate_limits event in the newest ${files.length} rollout file(s)`,
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
status: 'ok',
|
|
153
|
+
windows: best.windows,
|
|
154
|
+
observedAt: new Date(best.at).toISOString(),
|
|
155
|
+
source: relative(dir, best.file),
|
|
156
|
+
...(best.plan ? { plan: best.plan } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function parseTotals(t) {
|
|
160
|
+
if (!isObj(t) || !isNum(t.input_tokens) || !isNum(t.output_tokens))
|
|
161
|
+
return null;
|
|
162
|
+
return {
|
|
163
|
+
input: t.input_tokens,
|
|
164
|
+
cached: isNum(t.cached_input_tokens) ? t.cached_input_tokens : 0,
|
|
165
|
+
output: t.output_tokens,
|
|
166
|
+
cacheWrite: isNum(t.cache_write_input_tokens) ? t.cache_write_input_tokens : 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Cumulative totals so far in the session, plus this turn's own usage when the event carries it. */
|
|
170
|
+
function totals(payload) {
|
|
171
|
+
const info = payload.info;
|
|
172
|
+
if (!isObj(info))
|
|
173
|
+
return null;
|
|
174
|
+
const total = parseTotals(info.total_token_usage);
|
|
175
|
+
return total ? { total, turn: parseTotals(info.last_token_usage) } : null;
|
|
176
|
+
}
|
|
177
|
+
const ZERO = { input: 0, cached: 0, output: 0, cacheWrite: 0 };
|
|
178
|
+
const FIELDS = ['input', 'cached', 'output', 'cacheWrite'];
|
|
179
|
+
const size = (t) => t.input + t.output + t.cacheWrite;
|
|
180
|
+
const plus = (a, b) => ({
|
|
181
|
+
input: a.input + b.input,
|
|
182
|
+
cached: a.cached + b.cached,
|
|
183
|
+
output: a.output + b.output,
|
|
184
|
+
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
185
|
+
});
|
|
186
|
+
const minus = (a, b) => ({
|
|
187
|
+
input: Math.max(0, a.input - b.input),
|
|
188
|
+
cached: Math.max(0, a.cached - b.cached),
|
|
189
|
+
output: Math.max(0, a.output - b.output),
|
|
190
|
+
cacheWrite: Math.max(0, a.cacheWrite - b.cacheWrite),
|
|
191
|
+
});
|
|
192
|
+
/** True when any counter went down: each one only grows within a segment. */
|
|
193
|
+
const dropped = (now, prev) => FIELDS.some((k) => now[k] < prev[k]);
|
|
194
|
+
/**
|
|
195
|
+
* What each event adds, by index. `total_token_usage` is cumulative per segment: every field, cached input
|
|
196
|
+
* included, only grows until the Host starts a new segment, where they all start again from zero. A subagent's
|
|
197
|
+
* file starts from its parent's running total. So the first event adds its total minus the starting baseline
|
|
198
|
+
* (that total minus the first turn's own usage, zero for an ordinary session), an event where any field went
|
|
199
|
+
* down starts a new segment and adds its whole total, and any other event adds its growth. Every field of every
|
|
200
|
+
* increment is therefore zero or more, and any run of events sums to exactly what it added.
|
|
201
|
+
*/
|
|
202
|
+
export function fileDeltas(events) {
|
|
203
|
+
if (events.length === 0)
|
|
204
|
+
return [];
|
|
205
|
+
const first = events[0];
|
|
206
|
+
const baseline = first.turn ? minus(first.total, first.turn) : ZERO;
|
|
207
|
+
const out = [minus(first.total, baseline)];
|
|
208
|
+
let prev = first.total;
|
|
209
|
+
for (const e of events.slice(1)) {
|
|
210
|
+
out.push(dropped(e.total, prev) ? minus(e.total, ZERO) : minus(e.total, prev));
|
|
211
|
+
prev = e.total;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Tokens one whole rollout file adds: the sum of its fileDeltas. */
|
|
216
|
+
export function fileTokens(events) {
|
|
217
|
+
if (events.length === 0)
|
|
218
|
+
return null;
|
|
219
|
+
return fileDeltas(events).reduce(plus, ZERO);
|
|
220
|
+
}
|
|
221
|
+
async function fileUsage(file) {
|
|
222
|
+
const read = { failed: false };
|
|
223
|
+
const times = [];
|
|
224
|
+
const events = [];
|
|
225
|
+
for await (const { at, payload } of tokenCounts(file, read)) {
|
|
226
|
+
times.push(at);
|
|
227
|
+
const t = totals(payload);
|
|
228
|
+
if (t)
|
|
229
|
+
events.push({ at, ...t });
|
|
230
|
+
}
|
|
231
|
+
return { times, events, deltas: fileDeltas(events), failed: read.failed };
|
|
232
|
+
}
|
|
233
|
+
/** Parsed token totals per Account directory; a rollout is read again only after it changed. */
|
|
234
|
+
export const codexUsageMemo = new FileMemo();
|
|
235
|
+
/**
|
|
236
|
+
* Token totals for events in the last `days` x 24 h, from rollout files modified in that window (mtime only
|
|
237
|
+
* picks which files to read). Each event counts its fileDeltas increment, so a long session resumed today
|
|
238
|
+
* adds only its recent turns. `daily` spreads the same increments over local days.
|
|
239
|
+
*/
|
|
240
|
+
export async function readCodexUsage(dir, now, days = USAGE_DAYS) {
|
|
241
|
+
const end = now.getTime();
|
|
242
|
+
const since = end - days * 24 * 60 * 60 * 1000;
|
|
243
|
+
const files = (await listRollouts(dir)).filter((f) => f.mtimeMs >= since);
|
|
244
|
+
if (files.length === 0)
|
|
245
|
+
return {
|
|
246
|
+
status: 'unavailable',
|
|
247
|
+
reason: 'no-session',
|
|
248
|
+
detail: `no Codex session in the last ${days} days`,
|
|
249
|
+
};
|
|
250
|
+
let sessions = 0;
|
|
251
|
+
let sawTokenCount = false;
|
|
252
|
+
let sawTotals = false;
|
|
253
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
254
|
+
const daily = dailyBuckets(now, days);
|
|
255
|
+
const sum = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
256
|
+
const memo = codexUsageMemo.pass(dir);
|
|
257
|
+
for (const f of files) {
|
|
258
|
+
const { times, events, deltas } = await memo.get(f.path, f.stats, () => fileUsage(f.path), (v) => !v.failed);
|
|
259
|
+
if (times.length)
|
|
260
|
+
sawTokenCount = true;
|
|
261
|
+
for (const at of times)
|
|
262
|
+
if (at <= end && at > last)
|
|
263
|
+
last = at;
|
|
264
|
+
if (events.length)
|
|
265
|
+
sawTotals = true;
|
|
266
|
+
let used = ZERO;
|
|
267
|
+
let inWindow = false;
|
|
268
|
+
for (const [i, e] of events.entries()) {
|
|
269
|
+
if (e.at < since || e.at > end)
|
|
270
|
+
continue;
|
|
271
|
+
inWindow = true;
|
|
272
|
+
used = plus(used, deltas[i]);
|
|
273
|
+
daily.add(e.at, size(deltas[i]));
|
|
274
|
+
}
|
|
275
|
+
if (!inWindow)
|
|
276
|
+
continue;
|
|
277
|
+
sessions++;
|
|
278
|
+
const cached = Math.min(used.cached, used.input);
|
|
279
|
+
sum.inputTokens += used.input - cached;
|
|
280
|
+
sum.cacheReadTokens += cached;
|
|
281
|
+
sum.outputTokens += used.output;
|
|
282
|
+
sum.cacheWriteTokens += used.cacheWrite;
|
|
283
|
+
}
|
|
284
|
+
memo.done();
|
|
285
|
+
if (sessions === 0)
|
|
286
|
+
return sawTokenCount && !sawTotals
|
|
287
|
+
? { status: 'unavailable', reason: 'unrecognized', detail: 'token_count events have no usable totals' }
|
|
288
|
+
: { status: 'unavailable', reason: 'no-session', detail: `no Codex turn in the last ${days} days` };
|
|
289
|
+
return {
|
|
290
|
+
status: 'ok',
|
|
291
|
+
days,
|
|
292
|
+
sessions,
|
|
293
|
+
...sum,
|
|
294
|
+
totalTokens: sum.inputTokens + sum.outputTokens + sum.cacheReadTokens + sum.cacheWriteTokens,
|
|
295
|
+
daily: daily.daily,
|
|
296
|
+
...(Number.isFinite(last) ? { lastActivityAt: new Date(last).toISOString() } : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Account, FamilyDef, Plugin } from '../../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The `email` of the single sign-in entry in auth.json (keyed by the auth server, such as `https://auth.x.ai::<id>`).
|
|
4
|
+
* Only that field is read; tokens and every other field are dropped. Several entries give no answer.
|
|
5
|
+
*/
|
|
6
|
+
export declare function grokIdentity(account: Account): Promise<import("../../types.ts").AccountIdentity | undefined>;
|
|
7
|
+
export declare const grokFamily: FamilyDef;
|
|
8
|
+
export declare const grokPlugin: Plugin;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { identityFromJsonFile, identityOf } from '../../core/identity.js';
|
|
4
|
+
import { BUILTIN_LOGOS } from '../logos.js';
|
|
5
|
+
import { topLevelTomlString } from '../shared/toml.js';
|
|
6
|
+
// Grok's sandbox rejects these paths when they are symlinks; doctor quotes the Host's own words.
|
|
7
|
+
const HOOKS_DIR_MESSAGE = 'Grok hooks directory has wrong type (expected real directory)';
|
|
8
|
+
const REAL_FILE_MESSAGE = 'Grok hooks-paths registry has wrong type (expected real file)';
|
|
9
|
+
const AUTH_MESSAGE = 'the policy lock file under ~/.grok-00x is not accessible';
|
|
10
|
+
/** `model = "..."` before the first `[table]` of config.toml. */
|
|
11
|
+
async function readModel(account) {
|
|
12
|
+
let text;
|
|
13
|
+
try {
|
|
14
|
+
text = await readFile(join(account.dir, 'config.toml'), 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return topLevelTomlString(text, 'model');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The `email` of the single sign-in entry in auth.json (keyed by the auth server, such as `https://auth.x.ai::<id>`).
|
|
23
|
+
* Only that field is read; tokens and every other field are dropped. Several entries give no answer.
|
|
24
|
+
*/
|
|
25
|
+
export function grokIdentity(account) {
|
|
26
|
+
return identityFromJsonFile(join(account.dir, 'auth.json'), (data) => {
|
|
27
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data))
|
|
28
|
+
return undefined;
|
|
29
|
+
const entries = Object.values(data);
|
|
30
|
+
if (entries.length !== 1)
|
|
31
|
+
return undefined;
|
|
32
|
+
const entry = entries[0];
|
|
33
|
+
return typeof entry === 'object' && entry !== null
|
|
34
|
+
? identityOf(entry.email)
|
|
35
|
+
: undefined;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export const grokFamily = {
|
|
39
|
+
id: 'grok',
|
|
40
|
+
title: 'Grok Build',
|
|
41
|
+
logo: BUILTIN_LOGOS.grok,
|
|
42
|
+
bin: 'grok',
|
|
43
|
+
installUrl: 'https://docs.x.ai/build/overview',
|
|
44
|
+
selectVar: 'GROK_HOME',
|
|
45
|
+
layout: { main: '.grok', account: '.grok-<name>' },
|
|
46
|
+
hijackVars: ['XAI_API_KEY', 'GROK_MODELS_BASE_URL'],
|
|
47
|
+
apiVars: ['GROK_MODELS_BASE_URL', 'XAI_API_KEY'],
|
|
48
|
+
sharedItems: [
|
|
49
|
+
{ path: 'skills', mode: 'link' },
|
|
50
|
+
{ path: 'installed-plugins', mode: 'link' },
|
|
51
|
+
{ path: 'hooks', mode: 'copy', hostMessage: HOOKS_DIR_MESSAGE },
|
|
52
|
+
{ path: 'hooks-paths', mode: 'copy', hostMessage: REAL_FILE_MESSAGE },
|
|
53
|
+
{ path: 'trusted_folders.toml', mode: 'local', hostMessage: REAL_FILE_MESSAGE },
|
|
54
|
+
{ path: 'config.toml', mode: 'local-if-api', noSymlink: true, hostMessage: REAL_FILE_MESSAGE },
|
|
55
|
+
{ path: 'auth.json', mode: 'info', noSymlink: true, credential: true, hostMessage: AUTH_MESSAGE },
|
|
56
|
+
],
|
|
57
|
+
login: {
|
|
58
|
+
args: ['login'],
|
|
59
|
+
hint: "Sign in to your xAI account in the browser; the login is saved to this account's auth.json",
|
|
60
|
+
},
|
|
61
|
+
async loginState(account) {
|
|
62
|
+
try {
|
|
63
|
+
const st = await lstat(join(account.dir, 'auth.json'));
|
|
64
|
+
return st.isFile() ? 'logged-in' : 'unknown';
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return err.code === 'ENOENT' ? 'logged-out' : 'unknown';
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
model: readModel,
|
|
71
|
+
identity: grokIdentity,
|
|
72
|
+
};
|
|
73
|
+
export const grokPlugin = {
|
|
74
|
+
name: 'grok',
|
|
75
|
+
register(api) {
|
|
76
|
+
api.family(grokFamily);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { packageVersion } from '../core/version.js';
|
|
2
|
+
import { accountScript } from '../plugins/account-script.js';
|
|
3
|
+
import { claudePlugin } from './claude/index.js';
|
|
4
|
+
import { codexPlugin } from './codex/index.js';
|
|
5
|
+
import { grokPlugin } from './grok/index.js';
|
|
6
|
+
import { piPlugin } from './pi/index.js';
|
|
7
|
+
const VERSION = packageVersion();
|
|
8
|
+
/** Built-in Plugins in load order. Families register here; `account-script` is off unless enabled. */
|
|
9
|
+
export const BUILTIN_PLUGINS = [
|
|
10
|
+
{
|
|
11
|
+
plugin: claudePlugin,
|
|
12
|
+
version: VERSION,
|
|
13
|
+
description: 'Claude Code family (~/.claude, CLAUDE_CONFIG_DIR)',
|
|
14
|
+
defaultEnabled: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
plugin: codexPlugin,
|
|
18
|
+
version: VERSION,
|
|
19
|
+
description: 'Codex family (~/.codex, CODEX_HOME)',
|
|
20
|
+
defaultEnabled: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
plugin: grokPlugin,
|
|
24
|
+
version: VERSION,
|
|
25
|
+
description: 'Grok Build family (~/.grok, GROK_HOME)',
|
|
26
|
+
defaultEnabled: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
plugin: piPlugin,
|
|
30
|
+
version: VERSION,
|
|
31
|
+
description: 'pi family (~/.pi/agent, PI_CODING_AGENT_DIR)',
|
|
32
|
+
defaultEnabled: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
plugin: accountScript,
|
|
36
|
+
version: VERSION,
|
|
37
|
+
description: 'Run <account>/sideby-before-launch before each launch',
|
|
38
|
+
defaultEnabled: false,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Logos of the built-in Families, by Family id; each Family sets its own as `logo`. */
|
|
2
|
+
export declare const BUILTIN_LOGOS: {
|
|
3
|
+
claude: {
|
|
4
|
+
title: string;
|
|
5
|
+
color: string;
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
codex: {
|
|
9
|
+
title: string;
|
|
10
|
+
path: string;
|
|
11
|
+
};
|
|
12
|
+
grok: {
|
|
13
|
+
title: string;
|
|
14
|
+
fillRule: "evenodd";
|
|
15
|
+
path: string;
|
|
16
|
+
};
|
|
17
|
+
pi: {
|
|
18
|
+
title: string;
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Built-in Family marks for the Panel (FamilyDef.logo): 24x24 single-path SVG icons, drawn inline because the page CSP
|
|
2
|
+
// allows no external images.
|
|
3
|
+
// Sources: Claude, OpenAI and pi from Simple Icons (CC0 1.0, https://simpleicons.org; OpenAI from v15.0.0, the last release
|
|
4
|
+
// that ships it; pi is the mark from https://pi.dev). Grok from LobeHub Icons (MIT, Copyright (c) 2023 LobeHub,
|
|
5
|
+
// https://github.com/lobehub/lobe-icons). The marks are trademarks of their owners and only identify each Host.
|
|
6
|
+
/** Logos of the built-in Families, by Family id; each Family sets its own as `logo`. */
|
|
7
|
+
export const BUILTIN_LOGOS = {
|
|
8
|
+
claude: {
|
|
9
|
+
title: 'Claude',
|
|
10
|
+
color: '#D97757',
|
|
11
|
+
path: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z',
|
|
12
|
+
},
|
|
13
|
+
codex: {
|
|
14
|
+
title: 'OpenAI',
|
|
15
|
+
path: 'M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z',
|
|
16
|
+
},
|
|
17
|
+
grok: {
|
|
18
|
+
title: 'Grok',
|
|
19
|
+
fillRule: 'evenodd',
|
|
20
|
+
path: 'M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815',
|
|
21
|
+
},
|
|
22
|
+
pi: {
|
|
23
|
+
title: 'pi',
|
|
24
|
+
path: 'M0 0v24h6v-6h6v-6H6V6h6v6h6V0Zm18 12v12h6V12Z',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Built-in Family Plugin for pi (spec §3.4, checked against pi 1.0.2). pi keeps each Account one level
|
|
2
|
+
// deeper (`~/.pi-<name>/agent`) and signs in inside the app, so there is no login command.
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { lstatOrNull } from '../../core/fs-safe.js';
|
|
6
|
+
import { BUILTIN_LOGOS } from '../logos.js';
|
|
7
|
+
/** `auth.json` exists after `/login`; its contents are never read. */
|
|
8
|
+
async function loginState(account) {
|
|
9
|
+
return (await lstatOrNull(join(account.dir, 'auth.json'))) ? 'logged-in' : 'logged-out';
|
|
10
|
+
}
|
|
11
|
+
async function model(account) {
|
|
12
|
+
try {
|
|
13
|
+
const s = JSON.parse(await readFile(join(account.dir, 'settings.json'), 'utf8'));
|
|
14
|
+
const provider = typeof s?.defaultProvider === 'string' ? s.defaultProvider : '';
|
|
15
|
+
const id = typeof s?.defaultModel === 'string' ? s.defaultModel : '';
|
|
16
|
+
if (!id)
|
|
17
|
+
return undefined;
|
|
18
|
+
return provider ? `${provider}/${id}` : id;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const piFamily = {
|
|
25
|
+
id: 'pi',
|
|
26
|
+
title: 'pi',
|
|
27
|
+
logo: BUILTIN_LOGOS.pi,
|
|
28
|
+
bin: 'pi',
|
|
29
|
+
installUrl: 'https://github.com/earendil-works/pi',
|
|
30
|
+
selectVar: 'PI_CODING_AGENT_DIR',
|
|
31
|
+
secretFileMeansApi: false,
|
|
32
|
+
layout: { main: '.pi/agent', account: '.pi-<name>/agent' },
|
|
33
|
+
hijackVars: [
|
|
34
|
+
'ANTHROPIC_API_KEY',
|
|
35
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
36
|
+
'ANTHROPIC_BASE_URL',
|
|
37
|
+
'OPENAI_API_KEY',
|
|
38
|
+
'OPENAI_BASE_URL',
|
|
39
|
+
],
|
|
40
|
+
apiVars: ['ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL'],
|
|
41
|
+
sharedItems: [
|
|
42
|
+
{ path: 'AGENTS.md', mode: 'link' },
|
|
43
|
+
{ path: 'auth.json', mode: 'info', credential: true },
|
|
44
|
+
],
|
|
45
|
+
login: { args: null, hint: 'pi signs in inside the app: start it and type /login' },
|
|
46
|
+
loginState,
|
|
47
|
+
model,
|
|
48
|
+
};
|
|
49
|
+
export const piPlugin = {
|
|
50
|
+
name: 'pi',
|
|
51
|
+
register(api) {
|
|
52
|
+
api.family(piFamily);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `key = "value"` among the top-level keys of a TOML file, i.e. before the first `[table]` header.
|
|
3
|
+
* Only a whole-line header ends the top level, so a multi-line array line such as ` ["a"],` does not.
|
|
4
|
+
*/
|
|
5
|
+
export declare function topLevelTomlString(text: string, key: string): string | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const TABLE_HEADER = /^\s*\[\[?[^\]\s][^\]]*\]\]?\s*(#.*)?$/;
|
|
2
|
+
/**
|
|
3
|
+
* `key = "value"` among the top-level keys of a TOML file, i.e. before the first `[table]` header.
|
|
4
|
+
* Only a whole-line header ends the top level, so a multi-line array line such as ` ["a"],` does not.
|
|
5
|
+
*/
|
|
6
|
+
export function topLevelTomlString(text, key) {
|
|
7
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`);
|
|
8
|
+
for (const line of text.split(/\r?\n/)) {
|
|
9
|
+
if (TABLE_HEADER.test(line) && !/^\s*\[\s*["'\d]/.test(line))
|
|
10
|
+
return undefined;
|
|
11
|
+
const m = pattern.exec(line);
|
|
12
|
+
if (m)
|
|
13
|
+
return m[1] ?? m[2];
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createPanelHandler, type PanelFamily, type PanelHandler, type PanelHandlerOptions, type PanelState, } from './panel/handler.ts';
|
|
2
|
+
export { startPanelServer } from './panel/server.ts';
|
|
3
|
+
export type { PanelTheme, PanelThemeTokens } from './panel/theme.ts';
|
|
4
|
+
export { type AccountStatus, createRuntime, type DoctorOptions, type FamilyInfo, type Runtime, } from './runtime.ts';
|
|
5
|
+
export type * from './types.ts';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Paths } from '../core/paths.ts';
|
|
2
|
+
import type { Env } from '../types.ts';
|
|
3
|
+
export declare const DETACHED_FLAG = "serve-detached";
|
|
4
|
+
export interface PanelPidFile {
|
|
5
|
+
pid: number;
|
|
6
|
+
port: number;
|
|
7
|
+
url: string;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
/** Random id the Panel also returns on `/api/health`. */
|
|
10
|
+
instance: string;
|
|
11
|
+
/** `ps -o lstart=` of the process, when `ps` gave one; a reused pid has another. */
|
|
12
|
+
processStart?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const pidFilePath: (p: Paths) => string;
|
|
15
|
+
export declare const logFilePath: (p: Paths) => string;
|
|
16
|
+
export declare function readPidFile(p: Paths): Promise<PanelPidFile | undefined>;
|
|
17
|
+
/** When `pid` started, as `ps` prints it; with the pid it names one process, which a reused pid does not match. */
|
|
18
|
+
export declare const processStartTime: (pid: number) => Promise<string | undefined>;
|
|
19
|
+
/** URL of a running background Panel, or of any sideby Panel on `port`; removes a stale pid file. */
|
|
20
|
+
export declare function findRunningPanel(p: Paths, port: number): Promise<string | undefined>;
|
|
21
|
+
export interface StartBackgroundOptions {
|
|
22
|
+
paths: Paths;
|
|
23
|
+
env: Env;
|
|
24
|
+
/** Absolute path of the sideby CLI entry the detached process runs. */
|
|
25
|
+
entry: string;
|
|
26
|
+
port?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Makes sure one background Panel runs and returns its URL. Reuses a running one; otherwise starts
|
|
30
|
+
* `sideby ui --serve-detached` in its own session, logging to the state directory, and waits until it listens.
|
|
31
|
+
*/
|
|
32
|
+
export declare function startBackgroundPanel(opts: StartBackgroundOptions): Promise<{
|
|
33
|
+
url: string;
|
|
34
|
+
reused: boolean;
|
|
35
|
+
pid?: number;
|
|
36
|
+
replaced?: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Body of the detached process: serve the Panel with Hosts found through the login shell's PATH, tell the
|
|
40
|
+
* launching process the URL over IPC, keep the pid file while running, and remove it on SIGTERM or SIGINT.
|
|
41
|
+
*/
|
|
42
|
+
export declare function serveDetached(opts: {
|
|
43
|
+
paths: Paths;
|
|
44
|
+
env: Env;
|
|
45
|
+
port?: number;
|
|
46
|
+
runtimeFactory: (env: Env) => Promise<import('../runtime.ts').Runtime>;
|
|
47
|
+
log: (s: string) => void;
|
|
48
|
+
}): Promise<number>;
|
|
49
|
+
export type StopResult = {
|
|
50
|
+
status: 'stopped';
|
|
51
|
+
pid: number;
|
|
52
|
+
url: string;
|
|
53
|
+
} | {
|
|
54
|
+
status: 'not-running';
|
|
55
|
+
detail?: string;
|
|
56
|
+
} | {
|
|
57
|
+
status: 'failed';
|
|
58
|
+
pid: number;
|
|
59
|
+
detail: string;
|
|
60
|
+
};
|
|
61
|
+
/** Stops the background Panel named in the pid file, signalling it only when checkPanel confirms it. */
|
|
62
|
+
export declare function stopBackgroundPanel(paths: Paths): Promise<StopResult>;
|