@sleds/cli 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/README.md +48 -0
- package/dist/sleds.mjs +2721 -0
- package/package.json +35 -0
package/dist/sleds.mjs
ADDED
|
@@ -0,0 +1,2721 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { realpathSync } from "fs";
|
|
8
|
+
import { homedir, platform as platform2 } from "os";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
// src/meter.ts
|
|
12
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
13
|
+
import { join as join3 } from "node:path";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
17
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
18
|
+
|
|
19
|
+
// src/settings.ts
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join, resolve } from "node:path";
|
|
22
|
+
var BASE_URL_KEY = "ANTHROPIC_BASE_URL";
|
|
23
|
+
var AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN";
|
|
24
|
+
var claudeDir = (home, env = process.env) => {
|
|
25
|
+
const o = env.CLAUDE_CONFIG_DIR?.trim();
|
|
26
|
+
return o ? resolve(home, o) : join(home, ".claude");
|
|
27
|
+
};
|
|
28
|
+
var hasHomeOverride = (env = process.env) => Boolean(env.SLEDS_HOME?.trim() || env.CLAUDE_CONFIG_DIR?.trim());
|
|
29
|
+
var settingsPath = (home) => join(claudeDir(home), "settings.json");
|
|
30
|
+
var backupPath = (home) => join(claudeDir(home), "settings.json.sleds-backup");
|
|
31
|
+
var tokenPath = (home) => join(home, ".sleds-token");
|
|
32
|
+
var skillDir = (home) => join(claudeDir(home), "skills", "sled");
|
|
33
|
+
var skillPath = (home) => join(skillDir(home), "SKILL.md");
|
|
34
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35
|
+
var envOf = (settings) => {
|
|
36
|
+
const env = settings.env;
|
|
37
|
+
return isObject(env) ? env : {};
|
|
38
|
+
};
|
|
39
|
+
var applyProxy = (settings, cfg) => ({
|
|
40
|
+
...settings,
|
|
41
|
+
env: { ...envOf(settings), [BASE_URL_KEY]: cfg.url, [AUTH_TOKEN_KEY]: cfg.token }
|
|
42
|
+
});
|
|
43
|
+
var removeProxy = (settings) => {
|
|
44
|
+
if (!isObject(settings.env))
|
|
45
|
+
return { ...settings };
|
|
46
|
+
const { [BASE_URL_KEY]: _url, [AUTH_TOKEN_KEY]: _token, ...rest } = settings.env;
|
|
47
|
+
return { ...settings, env: rest };
|
|
48
|
+
};
|
|
49
|
+
var readProxy = (settings) => {
|
|
50
|
+
const env = envOf(settings);
|
|
51
|
+
const url = env[BASE_URL_KEY];
|
|
52
|
+
const token = env[AUTH_TOKEN_KEY];
|
|
53
|
+
if (typeof url !== "string" || typeof token !== "string")
|
|
54
|
+
return null;
|
|
55
|
+
return { url, token };
|
|
56
|
+
};
|
|
57
|
+
var STATUS_LINE_KEY = "statusLine";
|
|
58
|
+
var statusLineEntry = (command) => ({ type: "command", command });
|
|
59
|
+
var isSledsStatusLine = (value) => isObject(value) && value.type === "command" && typeof value.command === "string" && /\bstatusline\b/.test(value.command);
|
|
60
|
+
var applyStatusLine = (settings, command) => {
|
|
61
|
+
const current = settings[STATUS_LINE_KEY];
|
|
62
|
+
const prior = current === undefined || isSledsStatusLine(current) ? undefined : current;
|
|
63
|
+
return { settings: { ...settings, [STATUS_LINE_KEY]: statusLineEntry(command) }, prior };
|
|
64
|
+
};
|
|
65
|
+
var removeStatusLine = (settings, prior) => {
|
|
66
|
+
if (!isSledsStatusLine(settings[STATUS_LINE_KEY]))
|
|
67
|
+
return { ...settings };
|
|
68
|
+
const { [STATUS_LINE_KEY]: _ours, ...rest } = settings;
|
|
69
|
+
return prior === undefined ? rest : { ...rest, [STATUS_LINE_KEY]: prior };
|
|
70
|
+
};
|
|
71
|
+
var HOOKS_KEY = "hooks";
|
|
72
|
+
var NOTIFICATION_EVENT = "Notification";
|
|
73
|
+
var SEAT_EVENT_MATCHER = "quota_auto_resume_fired|quota_auto_resume_stale|quota_auto_resume_disabled";
|
|
74
|
+
var SEAT_EVENT_TIMEOUT_S = 10;
|
|
75
|
+
var isSledsSeatHook = (entry) => {
|
|
76
|
+
if (!isObject(entry) || !Array.isArray(entry.hooks))
|
|
77
|
+
return false;
|
|
78
|
+
return entry.hooks.some((h) => isObject(h) && h.type === "command" && typeof h.command === "string" && /\bseat-event\b/.test(h.command));
|
|
79
|
+
};
|
|
80
|
+
var seatHookEntry = (command) => ({
|
|
81
|
+
matcher: SEAT_EVENT_MATCHER,
|
|
82
|
+
hooks: [{ type: "command", command, timeout: SEAT_EVENT_TIMEOUT_S }]
|
|
83
|
+
});
|
|
84
|
+
var notificationHooks = (settings) => {
|
|
85
|
+
const hooks = settings[HOOKS_KEY];
|
|
86
|
+
if (!isObject(hooks))
|
|
87
|
+
return [];
|
|
88
|
+
const list = hooks[NOTIFICATION_EVENT];
|
|
89
|
+
return Array.isArray(list) ? list : [];
|
|
90
|
+
};
|
|
91
|
+
var withNotificationHooks = (settings, list) => {
|
|
92
|
+
const hooks = isObject(settings[HOOKS_KEY]) ? settings[HOOKS_KEY] : {};
|
|
93
|
+
if (list.length === 0) {
|
|
94
|
+
const { [NOTIFICATION_EVENT]: _gone, ...restHooks } = hooks;
|
|
95
|
+
if (Object.keys(restHooks).length === 0) {
|
|
96
|
+
const { [HOOKS_KEY]: _none, ...rest } = settings;
|
|
97
|
+
return rest;
|
|
98
|
+
}
|
|
99
|
+
return { ...settings, [HOOKS_KEY]: restHooks };
|
|
100
|
+
}
|
|
101
|
+
return { ...settings, [HOOKS_KEY]: { ...hooks, [NOTIFICATION_EVENT]: list } };
|
|
102
|
+
};
|
|
103
|
+
var applySeatHook = (settings, command) => {
|
|
104
|
+
const list = notificationHooks(settings);
|
|
105
|
+
const idx = list.findIndex(isSledsSeatHook);
|
|
106
|
+
const entry = seatHookEntry(command);
|
|
107
|
+
const next = idx === -1 ? [...list, entry] : list.map((e, i) => i === idx ? entry : e);
|
|
108
|
+
return withNotificationHooks(settings, next);
|
|
109
|
+
};
|
|
110
|
+
var removeSeatHook = (settings) => {
|
|
111
|
+
const list = notificationHooks(settings);
|
|
112
|
+
if (!list.some(isSledsSeatHook))
|
|
113
|
+
return { ...settings };
|
|
114
|
+
return withNotificationHooks(settings, list.filter((e) => !isSledsSeatHook(e)));
|
|
115
|
+
};
|
|
116
|
+
var hasSeatHook = (settings) => notificationHooks(settings).some(isSledsSeatHook);
|
|
117
|
+
var POST_TOOL_EVENT = "PostToolUse";
|
|
118
|
+
var SPEC_EVENT_MATCHER = "Write|Edit|MultiEdit";
|
|
119
|
+
var SPEC_EVENT_TIMEOUT_S = 5;
|
|
120
|
+
var isSledsSpecHook = (entry) => {
|
|
121
|
+
if (!isObject(entry) || !Array.isArray(entry.hooks))
|
|
122
|
+
return false;
|
|
123
|
+
return entry.hooks.some((h) => isObject(h) && h.type === "command" && typeof h.command === "string" && /\bspec-event\b/.test(h.command));
|
|
124
|
+
};
|
|
125
|
+
var specHookEntry = (command) => ({
|
|
126
|
+
matcher: SPEC_EVENT_MATCHER,
|
|
127
|
+
hooks: [{ type: "command", command, timeout: SPEC_EVENT_TIMEOUT_S }]
|
|
128
|
+
});
|
|
129
|
+
var eventHooks = (settings, event) => {
|
|
130
|
+
const hooks = settings[HOOKS_KEY];
|
|
131
|
+
if (!isObject(hooks))
|
|
132
|
+
return [];
|
|
133
|
+
const list = hooks[event];
|
|
134
|
+
return Array.isArray(list) ? list : [];
|
|
135
|
+
};
|
|
136
|
+
var withEventHooks = (settings, event, list) => {
|
|
137
|
+
const hooks = isObject(settings[HOOKS_KEY]) ? settings[HOOKS_KEY] : {};
|
|
138
|
+
if (list.length === 0) {
|
|
139
|
+
const { [event]: _gone, ...restHooks } = hooks;
|
|
140
|
+
if (Object.keys(restHooks).length === 0) {
|
|
141
|
+
const { [HOOKS_KEY]: _none, ...rest } = settings;
|
|
142
|
+
return rest;
|
|
143
|
+
}
|
|
144
|
+
return { ...settings, [HOOKS_KEY]: restHooks };
|
|
145
|
+
}
|
|
146
|
+
return { ...settings, [HOOKS_KEY]: { ...hooks, [event]: list } };
|
|
147
|
+
};
|
|
148
|
+
var applySpecHook = (settings, command) => {
|
|
149
|
+
const list = eventHooks(settings, POST_TOOL_EVENT);
|
|
150
|
+
const idx = list.findIndex(isSledsSpecHook);
|
|
151
|
+
const entry = specHookEntry(command);
|
|
152
|
+
const next = idx === -1 ? [...list, entry] : list.map((e, i) => i === idx ? entry : e);
|
|
153
|
+
return withEventHooks(settings, POST_TOOL_EVENT, next);
|
|
154
|
+
};
|
|
155
|
+
var removeSpecHook = (settings) => {
|
|
156
|
+
const list = eventHooks(settings, POST_TOOL_EVENT);
|
|
157
|
+
if (!list.some(isSledsSpecHook))
|
|
158
|
+
return { ...settings };
|
|
159
|
+
return withEventHooks(settings, POST_TOOL_EVENT, list.filter((e) => !isSledsSpecHook(e)));
|
|
160
|
+
};
|
|
161
|
+
var hasSpecHook = (settings) => eventHooks(settings, POST_TOOL_EVENT).some(isSledsSpecHook);
|
|
162
|
+
var isSeatToken = (token) => /^sled_[A-Za-z0-9_-]+$/.test(token);
|
|
163
|
+
var maskToken = (token) => token.length <= 9 ? `${token.slice(0, 5)}…` : `${token.slice(0, 9)}…`;
|
|
164
|
+
var serialize = (settings) => `${JSON.stringify(settings, null, 2)}
|
|
165
|
+
`;
|
|
166
|
+
var hasClaudeCode = (home) => existsSync(claudeDir(home));
|
|
167
|
+
var readSettings = (home) => {
|
|
168
|
+
const p = settingsPath(home);
|
|
169
|
+
if (!existsSync(p))
|
|
170
|
+
return {};
|
|
171
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
172
|
+
if (!isObject(parsed))
|
|
173
|
+
throw new Error(`${p} is not a JSON object`);
|
|
174
|
+
return parsed;
|
|
175
|
+
};
|
|
176
|
+
var writeSettings = (home, settings) => {
|
|
177
|
+
mkdirSync(claudeDir(home), { recursive: true });
|
|
178
|
+
const target = settingsPath(home);
|
|
179
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
180
|
+
writeFileSync(tmp, serialize(settings));
|
|
181
|
+
renameSync(tmp, target);
|
|
182
|
+
};
|
|
183
|
+
var backupSettings = (home) => {
|
|
184
|
+
const src = settingsPath(home);
|
|
185
|
+
const dst = backupPath(home);
|
|
186
|
+
if (!existsSync(src) || existsSync(dst))
|
|
187
|
+
return false;
|
|
188
|
+
writeFileSync(dst, readFileSync(src));
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
var readTokenFile = (home) => {
|
|
192
|
+
const p = tokenPath(home);
|
|
193
|
+
if (!existsSync(p))
|
|
194
|
+
return null;
|
|
195
|
+
const t = readFileSync(p, "utf8").trim();
|
|
196
|
+
return t.length > 0 ? t : null;
|
|
197
|
+
};
|
|
198
|
+
var writeTokenFile = (home, token) => {
|
|
199
|
+
writeFileSync(tokenPath(home), `${token}
|
|
200
|
+
`, { mode: 384 });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/config.ts
|
|
204
|
+
var configDir = (home, env = process.env) => {
|
|
205
|
+
const o = env.SLEDS_HOME?.trim();
|
|
206
|
+
return o ? resolve2(home, o) : join2(home, ".config", "sleds");
|
|
207
|
+
};
|
|
208
|
+
var configPath = (home) => join2(configDir(home), "config.json");
|
|
209
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
210
|
+
var optString = (v) => typeof v === "string" ? v : undefined;
|
|
211
|
+
var parsePriorEnv = (v) => {
|
|
212
|
+
if (!isObject2(v))
|
|
213
|
+
return;
|
|
214
|
+
const out = {};
|
|
215
|
+
const url = optString(v[BASE_URL_KEY]);
|
|
216
|
+
const token = optString(v[AUTH_TOKEN_KEY]);
|
|
217
|
+
if (url !== undefined)
|
|
218
|
+
out[BASE_URL_KEY] = url;
|
|
219
|
+
if (token !== undefined)
|
|
220
|
+
out[AUTH_TOKEN_KEY] = token;
|
|
221
|
+
if (typeof v.hadEnv === "boolean")
|
|
222
|
+
out.hadEnv = v.hadEnv;
|
|
223
|
+
return out;
|
|
224
|
+
};
|
|
225
|
+
var parseSchedule = (v) => v === "launchd" || v === "systemd" || v === "manual" ? v : undefined;
|
|
226
|
+
var parseConfig = (raw) => {
|
|
227
|
+
if (!isObject2(raw))
|
|
228
|
+
return {};
|
|
229
|
+
const cfg = {};
|
|
230
|
+
const token = optString(raw.token);
|
|
231
|
+
const proxyUrl = optString(raw.proxyUrl);
|
|
232
|
+
const email = optString(raw.email);
|
|
233
|
+
const since = optString(raw.since);
|
|
234
|
+
const untilReset = optString(raw.untilReset);
|
|
235
|
+
const reason = optString(raw.reason);
|
|
236
|
+
const priorEnv = parsePriorEnv(raw.priorEnv);
|
|
237
|
+
const schedule = parseSchedule(raw.schedule);
|
|
238
|
+
if (token !== undefined)
|
|
239
|
+
cfg.token = token;
|
|
240
|
+
if (proxyUrl !== undefined)
|
|
241
|
+
cfg.proxyUrl = proxyUrl;
|
|
242
|
+
if (email !== undefined)
|
|
243
|
+
cfg.email = email;
|
|
244
|
+
if (raw.mode === "on" || raw.mode === "off")
|
|
245
|
+
cfg.mode = raw.mode;
|
|
246
|
+
if (since !== undefined)
|
|
247
|
+
cfg.since = since;
|
|
248
|
+
if (untilReset !== undefined)
|
|
249
|
+
cfg.untilReset = untilReset;
|
|
250
|
+
if (reason !== undefined)
|
|
251
|
+
cfg.reason = reason;
|
|
252
|
+
if (priorEnv !== undefined)
|
|
253
|
+
cfg.priorEnv = priorEnv;
|
|
254
|
+
if (schedule !== undefined)
|
|
255
|
+
cfg.schedule = schedule;
|
|
256
|
+
if (raw.priorStatusLine !== undefined)
|
|
257
|
+
cfg.priorStatusLine = raw.priorStatusLine;
|
|
258
|
+
return cfg;
|
|
259
|
+
};
|
|
260
|
+
var readConfig = (home) => {
|
|
261
|
+
const p = configPath(home);
|
|
262
|
+
if (!existsSync2(p))
|
|
263
|
+
return {};
|
|
264
|
+
return parseConfig(JSON.parse(readFileSync2(p, "utf8")));
|
|
265
|
+
};
|
|
266
|
+
var writeConfig = (home, cfg) => {
|
|
267
|
+
const dir = configDir(home);
|
|
268
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
269
|
+
const target = configPath(home);
|
|
270
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
271
|
+
writeFileSync2(tmp, `${JSON.stringify(cfg, null, 2)}
|
|
272
|
+
`, { mode: 384 });
|
|
273
|
+
renameSync2(tmp, target);
|
|
274
|
+
chmodSync(target, 384);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/meter.ts
|
|
278
|
+
var METER_CACHE_TTL_MS = 1e4;
|
|
279
|
+
var METER_TIMEOUT_MS = 1500;
|
|
280
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
281
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
282
|
+
var str = (v) => typeof v === "string" ? v : undefined;
|
|
283
|
+
var parseMeter = (raw) => {
|
|
284
|
+
if (!isObject3(raw))
|
|
285
|
+
return null;
|
|
286
|
+
const seat = isObject3(raw.seat) ? str(raw.seat.email) : undefined;
|
|
287
|
+
const today = isObject3(raw.today) ? raw.today : null;
|
|
288
|
+
const month = isObject3(raw.month) ? raw.month : null;
|
|
289
|
+
const jobs = isObject3(raw.jobs) ? raw.jobs : null;
|
|
290
|
+
const todayUsd = today ? num(today.spentUsd) : undefined;
|
|
291
|
+
const monthUsd = month ? num(month.spentUsd) : undefined;
|
|
292
|
+
if (seat === undefined || todayUsd === undefined || monthUsd === undefined || !jobs)
|
|
293
|
+
return null;
|
|
294
|
+
const band = Array.isArray(jobs.queuedEstimateUsd) ? jobs.queuedEstimateUsd : [];
|
|
295
|
+
const low = num(band[0]) ?? 0;
|
|
296
|
+
const high = num(band[1]) ?? low;
|
|
297
|
+
const window = isObject3(raw.window) ? raw.window : {};
|
|
298
|
+
const allowance = isObject3(raw.allowance) ? raw.allowance : {};
|
|
299
|
+
const out = {
|
|
300
|
+
seat: { email: seat },
|
|
301
|
+
today: { spentUsd: todayUsd, requests: (today && num(today.requests)) ?? 0 },
|
|
302
|
+
month: { spentUsd: monthUsd },
|
|
303
|
+
window: {},
|
|
304
|
+
allowance: {},
|
|
305
|
+
jobs: {
|
|
306
|
+
queued: num(jobs.queued) ?? 0,
|
|
307
|
+
queuedEstimateUsd: [low, Math.max(low, high)],
|
|
308
|
+
running: num(jobs.running) ?? 0,
|
|
309
|
+
deliveredToday: num(jobs.deliveredToday) ?? 0
|
|
310
|
+
},
|
|
311
|
+
ts: num(raw.ts) ?? 0
|
|
312
|
+
};
|
|
313
|
+
const ceiling = month ? num(month.ceilingUsd) : undefined;
|
|
314
|
+
if (ceiling !== undefined)
|
|
315
|
+
out.month.ceilingUsd = ceiling;
|
|
316
|
+
const pct = num(window.utilizationPct);
|
|
317
|
+
if (pct !== undefined)
|
|
318
|
+
out.window.utilizationPct = pct;
|
|
319
|
+
const resetAt = num(window.resetAt);
|
|
320
|
+
if (resetAt !== undefined)
|
|
321
|
+
out.window.resetAt = resetAt;
|
|
322
|
+
const remaining = num(allowance.remainingUsd);
|
|
323
|
+
if (remaining !== undefined)
|
|
324
|
+
out.allowance.remainingUsd = remaining;
|
|
325
|
+
const expiresAt = num(allowance.expiresAt);
|
|
326
|
+
if (expiresAt !== undefined)
|
|
327
|
+
out.allowance.expiresAt = expiresAt;
|
|
328
|
+
if (isObject3(raw.lastWall)) {
|
|
329
|
+
const kind = str(raw.lastWall.kind);
|
|
330
|
+
const ts = num(raw.lastWall.ts);
|
|
331
|
+
if (kind !== undefined && ts !== undefined)
|
|
332
|
+
out.lastWall = { kind, choice: str(raw.lastWall.choice) ?? null, ts };
|
|
333
|
+
}
|
|
334
|
+
const acc = num(raw.estimateAccuracyPct);
|
|
335
|
+
if (acc !== undefined)
|
|
336
|
+
out.estimateAccuracyPct = acc;
|
|
337
|
+
return out;
|
|
338
|
+
};
|
|
339
|
+
var usd = (n) => {
|
|
340
|
+
const abs = Math.abs(n);
|
|
341
|
+
if (abs >= 1000)
|
|
342
|
+
return `${n < 0 ? "-" : ""}$${Math.round(abs).toLocaleString("en-US")}`;
|
|
343
|
+
return `${n < 0 ? "-" : ""}$${abs.toFixed(2)}`;
|
|
344
|
+
};
|
|
345
|
+
var usdRange = (low, high) => {
|
|
346
|
+
const d = high >= 10 ? 0 : 2;
|
|
347
|
+
return `$${low.toFixed(d)}–${high.toFixed(d)}`;
|
|
348
|
+
};
|
|
349
|
+
var shortDuration = (ms) => {
|
|
350
|
+
const total = Math.max(0, Math.round(ms / 60000));
|
|
351
|
+
const d = Math.floor(total / (24 * 60));
|
|
352
|
+
const h = Math.floor(total % (24 * 60) / 60);
|
|
353
|
+
const m = total % 60;
|
|
354
|
+
if (d > 0)
|
|
355
|
+
return h > 0 ? `${d}d ${h}h` : `${d}d`;
|
|
356
|
+
if (h > 0)
|
|
357
|
+
return `${h}h${String(m).padStart(2, "0")}m`;
|
|
358
|
+
return `${m}m`;
|
|
359
|
+
};
|
|
360
|
+
var KIND_WORDS = {
|
|
361
|
+
rate_limit: "rate limit hit",
|
|
362
|
+
weekly_cap: "usage limit reached",
|
|
363
|
+
budget: "budget exhausted",
|
|
364
|
+
provider_down: "provider unavailable"
|
|
365
|
+
};
|
|
366
|
+
var kindInWords = (kind) => KIND_WORDS[kind] ?? kind.replace(/_/g, " ");
|
|
367
|
+
var SEP = " · ";
|
|
368
|
+
var PREFIX = "sleds ";
|
|
369
|
+
var OFFLINE_LINE = `${PREFIX}offline`;
|
|
370
|
+
var jobParts = (m) => {
|
|
371
|
+
const parts = [];
|
|
372
|
+
const [low, high] = m.jobs.queuedEstimateUsd;
|
|
373
|
+
if (m.jobs.queued > 0) {
|
|
374
|
+
const band = high > 0 ? ` ~${usdRange(low, high)}` : "";
|
|
375
|
+
parts.push(`${m.jobs.queued} queued${band}`);
|
|
376
|
+
}
|
|
377
|
+
if (m.jobs.running > 0)
|
|
378
|
+
parts.push(`${m.jobs.running} running`);
|
|
379
|
+
if (m.jobs.deliveredToday > 0)
|
|
380
|
+
parts.push(`${m.jobs.deliveredToday} delivered`);
|
|
381
|
+
return parts;
|
|
382
|
+
};
|
|
383
|
+
var formatMeterLine = (m, now, seatFragment = null) => {
|
|
384
|
+
if (!m)
|
|
385
|
+
return seatFragment ? `${OFFLINE_LINE}${SEP}${seatFragment}` : OFFLINE_LINE;
|
|
386
|
+
const parts = [`${usd(m.today.spentUsd)} today`];
|
|
387
|
+
if (seatFragment)
|
|
388
|
+
parts.push(seatFragment);
|
|
389
|
+
if (m.window.utilizationPct !== undefined) {
|
|
390
|
+
parts.push(`window ${Math.round(m.window.utilizationPct)}%`);
|
|
391
|
+
if (m.window.resetAt !== undefined && m.window.resetAt > now)
|
|
392
|
+
parts.push(`reset ${shortDuration(m.window.resetAt - now)}`);
|
|
393
|
+
}
|
|
394
|
+
if (m.allowance.remainingUsd !== undefined)
|
|
395
|
+
parts.push(`allowance ${usd(m.allowance.remainingUsd)}`);
|
|
396
|
+
parts.push(...jobParts(m));
|
|
397
|
+
return `${PREFIX}${parts.join(SEP)}`;
|
|
398
|
+
};
|
|
399
|
+
var row = (label, value) => `${label.padEnd(12)}${value}`;
|
|
400
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
401
|
+
var formatMeter = (m, now) => {
|
|
402
|
+
const lines = [row("Seat:", m.seat.email)];
|
|
403
|
+
lines.push(row("Built today:", `${usd(m.today.spentUsd)}${SEP}${plural(m.today.requests, "request")}`));
|
|
404
|
+
lines.push(row("Month:", m.month.ceilingUsd !== undefined ? `${usd(m.month.spentUsd)} of ${usd(m.month.ceilingUsd)} ceiling` : usd(m.month.spentUsd)));
|
|
405
|
+
if (m.window.utilizationPct !== undefined) {
|
|
406
|
+
const reset = m.window.resetAt !== undefined && m.window.resetAt > now ? `${SEP}resets in ${shortDuration(m.window.resetAt - now)}` : "";
|
|
407
|
+
lines.push(row("Window:", `${Math.round(m.window.utilizationPct)}% used${reset}`));
|
|
408
|
+
}
|
|
409
|
+
if (m.allowance.remainingUsd !== undefined) {
|
|
410
|
+
const exp = m.allowance.expiresAt !== undefined && m.allowance.expiresAt > now ? `${SEP}expires in ${shortDuration(m.allowance.expiresAt - now)}` : "";
|
|
411
|
+
lines.push(row("Allowance:", `${usd(m.allowance.remainingUsd)} left${exp}`));
|
|
412
|
+
}
|
|
413
|
+
const [low, high] = m.jobs.queuedEstimateUsd;
|
|
414
|
+
lines.push(row("Queued:", m.jobs.queued === 0 ? "0" : `${plural(m.jobs.queued, "job")}${high > 0 ? `${SEP}~${usdRange(low, high)}` : ""}`));
|
|
415
|
+
if (m.jobs.running > 0)
|
|
416
|
+
lines.push(row("Running:", plural(m.jobs.running, "job")));
|
|
417
|
+
lines.push(row("Delivered:", `${m.jobs.deliveredToday} today`));
|
|
418
|
+
if (m.lastWall) {
|
|
419
|
+
const choice = m.lastWall.choice ?? "open";
|
|
420
|
+
lines.push(row("Last wall:", `${kindInWords(m.lastWall.kind)} → ${choice}${SEP}${shortDuration(now - m.lastWall.ts)} ago`));
|
|
421
|
+
}
|
|
422
|
+
if (m.estimateAccuracyPct !== undefined)
|
|
423
|
+
lines.push(row("Estimates:", `${Math.round(m.estimateAccuracyPct)}% landed in band`));
|
|
424
|
+
return lines;
|
|
425
|
+
};
|
|
426
|
+
var meterCachePath = (home) => join3(configDir(home), "meter.json");
|
|
427
|
+
var readMeterCache = (home, now) => {
|
|
428
|
+
const p = meterCachePath(home);
|
|
429
|
+
if (!existsSync3(p))
|
|
430
|
+
return null;
|
|
431
|
+
try {
|
|
432
|
+
const raw = JSON.parse(readFileSync3(p, "utf8"));
|
|
433
|
+
if (!isObject3(raw))
|
|
434
|
+
return null;
|
|
435
|
+
const fetchedAt = num(raw.fetchedAt);
|
|
436
|
+
if (fetchedAt === undefined || now - fetchedAt >= METER_CACHE_TTL_MS || fetchedAt > now)
|
|
437
|
+
return null;
|
|
438
|
+
return parseMeter(raw.meter);
|
|
439
|
+
} catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var writeMeterCache = (home, meter, now) => {
|
|
444
|
+
const dir = configDir(home);
|
|
445
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
446
|
+
const target = meterCachePath(home);
|
|
447
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
448
|
+
const file = { fetchedAt: now, meter };
|
|
449
|
+
writeFileSync3(tmp, `${JSON.stringify(file)}
|
|
450
|
+
`, { mode: 384 });
|
|
451
|
+
renameSync3(tmp, target);
|
|
452
|
+
};
|
|
453
|
+
var fetchMeter = async (o) => {
|
|
454
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
455
|
+
try {
|
|
456
|
+
const res = await fetchFn(`${o.proxyUrl}/proxy/meter`, {
|
|
457
|
+
headers: { authorization: `Bearer ${o.token}` },
|
|
458
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? METER_TIMEOUT_MS)
|
|
459
|
+
});
|
|
460
|
+
if (!res.ok)
|
|
461
|
+
return null;
|
|
462
|
+
return parseMeter(await res.json());
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var meterSource = (home) => {
|
|
468
|
+
const cfg = readConfig(home);
|
|
469
|
+
const settings = existsSync3(settingsPath(home)) ? readSettings(home) : {};
|
|
470
|
+
const inSettings = readProxy(settings);
|
|
471
|
+
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
472
|
+
const token = inSettings?.token ?? cfg.token;
|
|
473
|
+
return proxyUrl && token ? { proxyUrl, token } : null;
|
|
474
|
+
};
|
|
475
|
+
var loadMeter = async (o) => {
|
|
476
|
+
const now = o.now ?? Date.now();
|
|
477
|
+
const cached = readMeterCache(o.home, now);
|
|
478
|
+
if (cached)
|
|
479
|
+
return cached;
|
|
480
|
+
const src = meterSource(o.home);
|
|
481
|
+
if (!src)
|
|
482
|
+
return null;
|
|
483
|
+
const fresh = await fetchMeter({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
484
|
+
if (fresh) {
|
|
485
|
+
try {
|
|
486
|
+
writeMeterCache(o.home, fresh, now);
|
|
487
|
+
} catch {}
|
|
488
|
+
}
|
|
489
|
+
return fresh;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/seat.ts
|
|
493
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
494
|
+
import { join as join4 } from "node:path";
|
|
495
|
+
var SEAT_POST_MIN_DELTA_PCT = 1;
|
|
496
|
+
var SEAT_POST_MIN_INTERVAL_MS = 5 * 60000;
|
|
497
|
+
var SEAT_POST_TIMEOUT_MS = 1500;
|
|
498
|
+
var STDIN_TIMEOUT_MS = 300;
|
|
499
|
+
var isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
500
|
+
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
501
|
+
var EPOCH_MS_THRESHOLD = 100000000000;
|
|
502
|
+
var parseResetsAt = (v) => {
|
|
503
|
+
const n = num2(v);
|
|
504
|
+
if (n !== undefined) {
|
|
505
|
+
if (n <= 0)
|
|
506
|
+
return;
|
|
507
|
+
return n >= EPOCH_MS_THRESHOLD ? Math.round(n) : Math.round(n * 1000);
|
|
508
|
+
}
|
|
509
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
510
|
+
const parsed = Date.parse(v);
|
|
511
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
};
|
|
515
|
+
var parseBar = (v) => {
|
|
516
|
+
if (!isObject4(v))
|
|
517
|
+
return;
|
|
518
|
+
const usedPct = num2(v.used_percentage) ?? num2(v.usedPct);
|
|
519
|
+
const resetsAt = parseResetsAt(v.resets_at ?? v.resetsAt);
|
|
520
|
+
if (usedPct === undefined || usedPct < 0 || resetsAt === undefined)
|
|
521
|
+
return;
|
|
522
|
+
return { usedPct, resetsAt };
|
|
523
|
+
};
|
|
524
|
+
var parseSeatWindow = (raw, capturedAt) => {
|
|
525
|
+
if (!isObject4(raw))
|
|
526
|
+
return null;
|
|
527
|
+
const limits = isObject4(raw.rate_limits) ? raw.rate_limits : raw;
|
|
528
|
+
const fiveHour = parseBar(limits.five_hour ?? limits.fiveHour);
|
|
529
|
+
const sevenDay = parseBar(limits.seven_day ?? limits.sevenDay);
|
|
530
|
+
const spendLimit = parseBar(limits.spend_limit ?? limits.spendLimit);
|
|
531
|
+
if (!fiveHour && !sevenDay && !spendLimit)
|
|
532
|
+
return null;
|
|
533
|
+
const out = { capturedAt };
|
|
534
|
+
if (fiveHour)
|
|
535
|
+
out.fiveHour = fiveHour;
|
|
536
|
+
if (sevenDay)
|
|
537
|
+
out.sevenDay = sevenDay;
|
|
538
|
+
if (spendLimit)
|
|
539
|
+
out.spendLimit = spendLimit;
|
|
540
|
+
return out;
|
|
541
|
+
};
|
|
542
|
+
var seatWindowFromStdin = (text, capturedAt) => {
|
|
543
|
+
const trimmed = text.trim();
|
|
544
|
+
if (!trimmed)
|
|
545
|
+
return null;
|
|
546
|
+
try {
|
|
547
|
+
return parseSeatWindow(JSON.parse(trimmed), capturedAt);
|
|
548
|
+
} catch {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var formatSeatFragment = (w) => {
|
|
553
|
+
if (!w)
|
|
554
|
+
return null;
|
|
555
|
+
const parts = [];
|
|
556
|
+
if (w.sevenDay)
|
|
557
|
+
parts.push(`wk ${Math.round(w.sevenDay.usedPct)}%`);
|
|
558
|
+
if (w.fiveHour)
|
|
559
|
+
parts.push(`5h ${Math.round(w.fiveHour.usedPct)}%`);
|
|
560
|
+
if (w.spendLimit)
|
|
561
|
+
parts.push(`spend ${Math.round(w.spendLimit.usedPct)}%`);
|
|
562
|
+
if (parts.length === 0)
|
|
563
|
+
return null;
|
|
564
|
+
return `seat ${parts.join(SEP)}`;
|
|
565
|
+
};
|
|
566
|
+
var seatStatePath = (home) => join4(configDir(home), "seat.json");
|
|
567
|
+
var parseWindowStored = (v) => {
|
|
568
|
+
if (!isObject4(v))
|
|
569
|
+
return;
|
|
570
|
+
const capturedAt = num2(v.capturedAt);
|
|
571
|
+
if (capturedAt === undefined)
|
|
572
|
+
return;
|
|
573
|
+
return parseSeatWindow(v, capturedAt) ?? undefined;
|
|
574
|
+
};
|
|
575
|
+
var parseSeatState = (raw) => {
|
|
576
|
+
if (!isObject4(raw))
|
|
577
|
+
return {};
|
|
578
|
+
const out = {};
|
|
579
|
+
const window = parseWindowStored(raw.window);
|
|
580
|
+
if (window)
|
|
581
|
+
out.window = window;
|
|
582
|
+
const lastPostedAt = num2(raw.lastPostedAt);
|
|
583
|
+
if (lastPostedAt !== undefined)
|
|
584
|
+
out.lastPostedAt = lastPostedAt;
|
|
585
|
+
if (isObject4(raw.lastPosted)) {
|
|
586
|
+
const lp = {};
|
|
587
|
+
const f = num2(raw.lastPosted.fiveHour);
|
|
588
|
+
const s = num2(raw.lastPosted.sevenDay);
|
|
589
|
+
const sp = num2(raw.lastPosted.spendLimit);
|
|
590
|
+
if (f !== undefined)
|
|
591
|
+
lp.fiveHour = f;
|
|
592
|
+
if (s !== undefined)
|
|
593
|
+
lp.sevenDay = s;
|
|
594
|
+
if (sp !== undefined)
|
|
595
|
+
lp.spendLimit = sp;
|
|
596
|
+
out.lastPosted = lp;
|
|
597
|
+
}
|
|
598
|
+
const lastWallResetsAt = num2(raw.lastWallResetsAt);
|
|
599
|
+
if (lastWallResetsAt !== undefined)
|
|
600
|
+
out.lastWallResetsAt = lastWallResetsAt;
|
|
601
|
+
return out;
|
|
602
|
+
};
|
|
603
|
+
var readSeatState = (home) => {
|
|
604
|
+
const p = seatStatePath(home);
|
|
605
|
+
if (!existsSync4(p))
|
|
606
|
+
return {};
|
|
607
|
+
try {
|
|
608
|
+
return parseSeatState(JSON.parse(readFileSync4(p, "utf8")));
|
|
609
|
+
} catch {
|
|
610
|
+
return {};
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
var writeSeatState = (home, state) => {
|
|
614
|
+
const dir = configDir(home);
|
|
615
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
616
|
+
const target = seatStatePath(home);
|
|
617
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
618
|
+
writeFileSync4(tmp, `${JSON.stringify(state)}
|
|
619
|
+
`, { mode: 384 });
|
|
620
|
+
renameSync4(tmp, target);
|
|
621
|
+
};
|
|
622
|
+
var pctOf = (b) => b?.usedPct;
|
|
623
|
+
var shouldPostSeat = (state, next, now) => {
|
|
624
|
+
if (state.lastPostedAt === undefined || !state.lastPosted)
|
|
625
|
+
return true;
|
|
626
|
+
if (now - state.lastPostedAt >= SEAT_POST_MIN_INTERVAL_MS)
|
|
627
|
+
return true;
|
|
628
|
+
const pairs = [
|
|
629
|
+
[state.lastPosted.fiveHour, pctOf(next.fiveHour)],
|
|
630
|
+
[state.lastPosted.sevenDay, pctOf(next.sevenDay)],
|
|
631
|
+
[state.lastPosted.spendLimit, pctOf(next.spendLimit)]
|
|
632
|
+
];
|
|
633
|
+
for (const [prev, cur] of pairs) {
|
|
634
|
+
if (prev === undefined && cur === undefined)
|
|
635
|
+
continue;
|
|
636
|
+
if (prev === undefined || cur === undefined)
|
|
637
|
+
return true;
|
|
638
|
+
if (Math.abs(cur - prev) >= SEAT_POST_MIN_DELTA_PCT)
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
};
|
|
643
|
+
var markPosted = (state, w, now) => {
|
|
644
|
+
const lastPosted = {};
|
|
645
|
+
if (w.fiveHour)
|
|
646
|
+
lastPosted.fiveHour = w.fiveHour.usedPct;
|
|
647
|
+
if (w.sevenDay)
|
|
648
|
+
lastPosted.sevenDay = w.sevenDay.usedPct;
|
|
649
|
+
if (w.spendLimit)
|
|
650
|
+
lastPosted.spendLimit = w.spendLimit.usedPct;
|
|
651
|
+
return { ...state, window: w, lastPostedAt: now, lastPosted };
|
|
652
|
+
};
|
|
653
|
+
var seatWallDue = (state, w) => {
|
|
654
|
+
if (!w?.sevenDay || w.sevenDay.usedPct < 100)
|
|
655
|
+
return false;
|
|
656
|
+
return state.lastWallResetsAt === undefined || state.lastWallResetsAt < w.sevenDay.resetsAt;
|
|
657
|
+
};
|
|
658
|
+
var seatWindowBody = (w) => ({
|
|
659
|
+
capturedAt: w.capturedAt,
|
|
660
|
+
...w.fiveHour ? { fiveHour: w.fiveHour } : {},
|
|
661
|
+
...w.sevenDay ? { sevenDay: w.sevenDay } : {},
|
|
662
|
+
...w.spendLimit ? { spendLimit: w.spendLimit } : {}
|
|
663
|
+
});
|
|
664
|
+
var postJson = async (o, path, body) => {
|
|
665
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
666
|
+
try {
|
|
667
|
+
const res = await fetchFn(`${o.proxyUrl}${path}`, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: { authorization: `Bearer ${o.token}`, "content-type": "application/json" },
|
|
670
|
+
body: JSON.stringify(body),
|
|
671
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? SEAT_POST_TIMEOUT_MS)
|
|
672
|
+
});
|
|
673
|
+
return res.ok;
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
|
|
679
|
+
var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", input);
|
|
680
|
+
var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the org key.";
|
|
681
|
+
var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) => {
|
|
682
|
+
if (stdin.isTTY) {
|
|
683
|
+
resolve3("");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
let data = "";
|
|
687
|
+
let settled = false;
|
|
688
|
+
const finish = () => {
|
|
689
|
+
if (settled)
|
|
690
|
+
return;
|
|
691
|
+
settled = true;
|
|
692
|
+
clearTimeout(timer);
|
|
693
|
+
resolve3(data);
|
|
694
|
+
};
|
|
695
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
696
|
+
stdin.setEncoding("utf8");
|
|
697
|
+
stdin.on("data", (chunk) => {
|
|
698
|
+
data += chunk;
|
|
699
|
+
});
|
|
700
|
+
stdin.on("end", finish);
|
|
701
|
+
stdin.on("error", finish);
|
|
702
|
+
stdin.resume();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// src/bar.ts
|
|
706
|
+
var SEAT_BAR_FRESH_MS = 6 * 60 * 60 * 1000;
|
|
707
|
+
var DASHBOARD_URL = "https://app.sleds.ai";
|
|
708
|
+
var MONO = "font=Menlo size=12";
|
|
709
|
+
var freshSeat = (w, now) => {
|
|
710
|
+
if (!w)
|
|
711
|
+
return null;
|
|
712
|
+
return now - w.capturedAt < SEAT_BAR_FRESH_MS ? w : null;
|
|
713
|
+
};
|
|
714
|
+
var barTitle = (m, seat) => {
|
|
715
|
+
const head = m ? usd(m.today.spentUsd) : "offline";
|
|
716
|
+
const wk = seat?.sevenDay ? `wk ${Math.round(seat.sevenDay.usedPct)}%` : null;
|
|
717
|
+
return [`sleds ${head}`, ...wk ? [wk] : []].join(SEP);
|
|
718
|
+
};
|
|
719
|
+
var formatBar = (m, seat, now) => {
|
|
720
|
+
const lines = [barTitle(m, seat), "---"];
|
|
721
|
+
if (m) {
|
|
722
|
+
for (const row2 of formatMeter(m, now))
|
|
723
|
+
lines.push(`${row2} | ${MONO}`);
|
|
724
|
+
} else {
|
|
725
|
+
lines.push(`offline${SEP}proxy did not answer | ${MONO}`);
|
|
726
|
+
}
|
|
727
|
+
const frag = formatSeatFragment(seat);
|
|
728
|
+
if (frag)
|
|
729
|
+
lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")} | ${MONO}`);
|
|
730
|
+
lines.push("---");
|
|
731
|
+
lines.push(`Open dashboard | href=${DASHBOARD_URL}`);
|
|
732
|
+
lines.push("Refresh | refresh=true");
|
|
733
|
+
return lines;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// src/commands/bar.ts
|
|
737
|
+
var bar = async (o) => {
|
|
738
|
+
const log = o.log ?? console.log;
|
|
739
|
+
const now = (o.now ?? Date.now)();
|
|
740
|
+
const seat = freshSeat(readSeatState(o.home).window, now);
|
|
741
|
+
if (!meterSource(o.home)) {
|
|
742
|
+
log("sleds not set up");
|
|
743
|
+
log("---");
|
|
744
|
+
log("Run `sleds init` first | font=Menlo size=12");
|
|
745
|
+
return 0;
|
|
746
|
+
}
|
|
747
|
+
const m = await loadMeter({ home: o.home, now, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
748
|
+
for (const line of formatBar(m, seat, now))
|
|
749
|
+
log(line);
|
|
750
|
+
return 0;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/commands/hooks.ts
|
|
754
|
+
var DEFAULT_SEAT_EVENT_COMMAND = "sleds seat-event";
|
|
755
|
+
var DEFAULT_SPEC_EVENT_COMMAND = "sleds spec-event";
|
|
756
|
+
var hooks = (o) => {
|
|
757
|
+
const log = o.log ?? console.log;
|
|
758
|
+
const error = o.error ?? console.error;
|
|
759
|
+
if (!hasClaudeCode(o.home)) {
|
|
760
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
761
|
+
return 1;
|
|
762
|
+
}
|
|
763
|
+
const settings = readSettings(o.home);
|
|
764
|
+
const had = hasSeatHook(settings);
|
|
765
|
+
const hadSpec = hasSpecHook(settings);
|
|
766
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? DEFAULT_SEAT_EVENT_COMMAND);
|
|
767
|
+
writeSettings(o.home, applySpecHook(withSeat, o.specEventCommand ?? DEFAULT_SPEC_EVENT_COMMAND));
|
|
768
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
769
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
770
|
+
return 0;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// src/commands/init.ts
|
|
774
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
775
|
+
import { createInterface } from "node:readline";
|
|
776
|
+
|
|
777
|
+
// src/skill.ts
|
|
778
|
+
var SKILL_MD = `---
|
|
779
|
+
name: sled
|
|
780
|
+
description: Hand work to SLEDS — continue past a rate limit or credit wall on the org's key, hold until reset, or run overnight at half price. Use when the user says /sled, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
# SLEDS
|
|
784
|
+
|
|
785
|
+
Nine to five is 40 of 168 hours. SLEDS puts the other 128 to work: it carries a session past a wall, and it runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on. Nothing changes until a wall or a \`/sled\`.
|
|
786
|
+
|
|
787
|
+
## Hit a limit on your seat
|
|
788
|
+
|
|
789
|
+
A Team or Max seat caps on Anthropic's side ("You've hit your limit…"). SLEDS is what you switch to. When the user reports a limit, a cap, or that Claude stopped for the day, tell them exactly this:
|
|
790
|
+
|
|
791
|
+
\`\`\`
|
|
792
|
+
sleds on --until-reset +5h
|
|
793
|
+
\`\`\`
|
|
794
|
+
|
|
795
|
+
then restart \`claude\`. Claude Code runs on the org key through the proxy; the seat is untouched. \`--until-reset\` takes \`+5h\`, \`+90m\`, or an ISO time; \`--reason "<text>"\` notes why. The switch reverts itself at the deadline where a scheduler exists.
|
|
796
|
+
|
|
797
|
+
## At the wall
|
|
798
|
+
|
|
799
|
+
When the proxy is active it answers a walled request with its options. The user picks one by number:
|
|
800
|
+
|
|
801
|
+
\`\`\`
|
|
802
|
+
/sled 1 continue now on the org key
|
|
803
|
+
/sled 2 hold; resume when the window resets
|
|
804
|
+
/sled 3 run overnight at half price
|
|
805
|
+
/sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
|
|
806
|
+
\`\`\`
|
|
807
|
+
|
|
808
|
+
## Window reset
|
|
809
|
+
|
|
810
|
+
When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
|
|
811
|
+
|
|
812
|
+
\`\`\`
|
|
813
|
+
sleds off
|
|
814
|
+
\`\`\`
|
|
815
|
+
|
|
816
|
+
then restart \`claude\`. Settings go back to exactly what they were.
|
|
817
|
+
|
|
818
|
+
## Check
|
|
819
|
+
|
|
820
|
+
\`sleds status\` — on/off, since, reverts at, proxy health, wall mode.
|
|
821
|
+
|
|
822
|
+
The status line's \`seat wk 52% · 5h 12%\` is the user's own Pro/Max window as Claude Code reports it (weekly, then 5-hour); \`window\` is the org key's headroom through the proxy. At \`wk 100%\` the answer is \`sleds on --until-reset\`.
|
|
823
|
+
|
|
824
|
+
## /sled — estimate the task, offer overnight
|
|
825
|
+
|
|
826
|
+
Bare \`/sled\` (or \`/sled <spec>\`) means: size the current task and offer to hand it to the night shift. Do this in order.
|
|
827
|
+
|
|
828
|
+
1. Read the task. The spec is the argument if given, otherwise the current conversation's open task (the todo list, the last user request).
|
|
829
|
+
2. Estimate with \`/sled estimate <spec>\` — the proxy answers with the calibrated band and cap, and the estimate is recorded so its actuals feed the next one. Quote that band, never a point and never your own guess.
|
|
830
|
+
3. Offer the window when any of these hold: the high end of the cost band is above the org's approval threshold, the wall-clock is over 30 minutes, or the result is not needed before end of day. The offer is one line: \`Run this overnight from 18:00? Estimated $40–120 at half price.\`
|
|
831
|
+
4. On yes, write the hand-off the Musher will work from: the spec, the acceptance test, the org conventions that apply, and every decision already made in this session as \`question -> answer\` pairs. Ask one thing before queueing: the autonomy for this job.
|
|
832
|
+
- \`ask\`: every question pages on-call.
|
|
833
|
+
- \`decide_and_flag\` (default): reversible defaults are taken and listed in the morning.
|
|
834
|
+
- \`decide\`: reversible defaults are taken quietly.
|
|
835
|
+
Irreversible questions always page, whatever the autonomy. No answer in 30 minutes parks the job and the Musher moves to the next one.
|
|
836
|
+
5. Queue it with \`/sled overnight <spec>\` (the hand-off from step 4 is the spec). The proxy runs the real estimator (its cap bounds the job's spend), schedules the job for the org's overnight window, and snapshots this session so the batch runner has the context. Confirm with the job id and the band from the reply — not your own guess.
|
|
837
|
+
|
|
838
|
+
## /sled review — the morning queue
|
|
839
|
+
|
|
840
|
+
Morning ritual. List what the night shift did, grouped:
|
|
841
|
+
|
|
842
|
+
- Delivered: jobs with a PR or a result, with actual cost against the estimate.
|
|
843
|
+
- Flagged: decisions the Musher took on a \`decide_and_flag\` job, each as \`question -> answer\` with the rung that answered it (spec, convention, or default). These need a human eye, not a redo.
|
|
844
|
+
- Parked: questions nobody answered in 30 minutes. Each shows the option set; answering resumes the job.
|
|
845
|
+
|
|
846
|
+
Read the Musher log for every delivered job before summarizing, and put each decision into the PR body with its reasoning. Then offer the next step for each: merge, kick back with a note, or \`/sled resume <id>\`. On Monday the review covers the weekend.
|
|
847
|
+
|
|
848
|
+
## /sled weekend — Friday sweep
|
|
849
|
+
|
|
850
|
+
Friday afternoon. Sweep the open task list into a weekend batch: everything the user has queued or labeled for the weekend becomes overnight jobs with the org's default autonomy, and the Monday review covers all of it. For now, explain that the sweep queues the current task list as overnight jobs; sweeping issues by GitHub label is a later ticket. Confirm the count and the total estimate band before queueing.
|
|
851
|
+
|
|
852
|
+
## /sled overnight [spec]
|
|
853
|
+
|
|
854
|
+
Queue a batch job without waiting for a wall. \`/sled overnight <spec>\` runs the spec; bare \`/sled overnight\` runs the session's original ask. The reply carries the band, the cap, when it runs, and \`/sled resume <id>\` for the morning. Half price on the batch tier.
|
|
855
|
+
|
|
856
|
+
## /sled jobs
|
|
857
|
+
|
|
858
|
+
List the org's jobs: queued, running, delivered, parked, with the estimate band, the actual so far, and the autonomy. Parked jobs show the unanswered question. Keep it to one line per job.
|
|
859
|
+
|
|
860
|
+
## /sled resume <id>
|
|
861
|
+
|
|
862
|
+
Resume a parked job. Show the unanswered question and its options, take the answer, and confirm the job is back in the queue. The answer is logged with the user's name, and it becomes a prior decision the Musher can reuse on the next question like it.
|
|
863
|
+
|
|
864
|
+
## Voice
|
|
865
|
+
|
|
866
|
+
Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
|
|
867
|
+
|
|
868
|
+
## Overnight jobs (how it runs)
|
|
869
|
+
|
|
870
|
+
At a wall the proxy prints its options; \`/sled 3\` queues the session for the org's overnight window on the batch tier at half price. The next morning:
|
|
871
|
+
|
|
872
|
+
- \`/sled jobs\` — this seat's jobs: id, status, route, cost, when.
|
|
873
|
+
- \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
|
|
874
|
+
|
|
875
|
+
A job that is \`parked\` or \`failed\` shows its reason in \`/sled jobs\`; the admin can see and cancel jobs on the dashboard's Jobs page.
|
|
876
|
+
`;
|
|
877
|
+
|
|
878
|
+
// src/commands/hooks.ts
|
|
879
|
+
var DEFAULT_SEAT_EVENT_COMMAND2 = "sleds seat-event";
|
|
880
|
+
var DEFAULT_SPEC_EVENT_COMMAND2 = "sleds spec-event";
|
|
881
|
+
var hooks2 = (o) => {
|
|
882
|
+
const log = o.log ?? console.log;
|
|
883
|
+
const error = o.error ?? console.error;
|
|
884
|
+
if (!hasClaudeCode(o.home)) {
|
|
885
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
886
|
+
return 1;
|
|
887
|
+
}
|
|
888
|
+
const settings = readSettings(o.home);
|
|
889
|
+
const had = hasSeatHook(settings);
|
|
890
|
+
const hadSpec = hasSpecHook(settings);
|
|
891
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? DEFAULT_SEAT_EVENT_COMMAND2);
|
|
892
|
+
writeSettings(o.home, applySpecHook(withSeat, o.specEventCommand ?? DEFAULT_SPEC_EVENT_COMMAND2));
|
|
893
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
894
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
895
|
+
return 0;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/commands/init.ts
|
|
899
|
+
var DEFAULT_PROXY_URL = "http://localhost:8787";
|
|
900
|
+
var DEFAULT_STATUS_LINE_COMMAND = "sleds statusline";
|
|
901
|
+
var promptStdin = (question) => new Promise((resolve3) => {
|
|
902
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
903
|
+
rl.question(question, (answer) => {
|
|
904
|
+
rl.close();
|
|
905
|
+
resolve3(answer.trim());
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
909
|
+
try {
|
|
910
|
+
const res = await fetchFn(`${proxyUrl}/signup`, {
|
|
911
|
+
method: "POST",
|
|
912
|
+
headers: { "content-type": "application/json" },
|
|
913
|
+
body: JSON.stringify({ email, apiKey: credential }),
|
|
914
|
+
signal: AbortSignal.timeout(20000)
|
|
915
|
+
});
|
|
916
|
+
const body = await res.json().catch(() => ({}));
|
|
917
|
+
if (res.ok && typeof body.token === "string")
|
|
918
|
+
return { ok: true, token: body.token, created: body.created === true };
|
|
919
|
+
return {
|
|
920
|
+
ok: false,
|
|
921
|
+
reason: typeof body.reason === "string" ? body.reason : `http_${res.status}`,
|
|
922
|
+
status: res.status
|
|
923
|
+
};
|
|
924
|
+
} catch {
|
|
925
|
+
return { ok: false, reason: "unreachable", status: 0 };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
var SIGNUP_REASONS = {
|
|
929
|
+
email: "that does not look like an email address.",
|
|
930
|
+
key: "that does not look like an Anthropic API key (sk-ant-…).",
|
|
931
|
+
key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
|
|
932
|
+
rate_limited: "too many signups from this network in the last hour. Try again later.",
|
|
933
|
+
not_configured: "the proxy is not set up for self-serve signup yet.",
|
|
934
|
+
unavailable: "the proxy could not reach its control plane. Try again in a moment.",
|
|
935
|
+
unreachable: "could not reach the proxy."
|
|
936
|
+
};
|
|
937
|
+
function signupFailureText(r) {
|
|
938
|
+
return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
|
|
939
|
+
}
|
|
940
|
+
var resolveToken = async (o) => {
|
|
941
|
+
const env = o.env ?? process.env;
|
|
942
|
+
if (o.token)
|
|
943
|
+
return { token: o.token.trim(), source: "flag" };
|
|
944
|
+
if (env.SLEDS_TOKEN)
|
|
945
|
+
return { token: env.SLEDS_TOKEN.trim(), source: "env" };
|
|
946
|
+
const fromFile = readTokenFile(o.home);
|
|
947
|
+
if (fromFile)
|
|
948
|
+
return { token: fromFile, source: "file" };
|
|
949
|
+
if (o.nonInteractive || !o.prompt)
|
|
950
|
+
return null;
|
|
951
|
+
if (!o.signup) {
|
|
952
|
+
const typed = (await o.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
|
|
953
|
+
if (typed)
|
|
954
|
+
return { token: typed, source: "prompt" };
|
|
955
|
+
}
|
|
956
|
+
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
|
|
957
|
+
const email = (await o.prompt("Your email: ")).trim();
|
|
958
|
+
if (!email.includes("@"))
|
|
959
|
+
return null;
|
|
960
|
+
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
961
|
+
if (!credential)
|
|
962
|
+
return null;
|
|
963
|
+
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
964
|
+
if (!r.ok) {
|
|
965
|
+
(o.error ?? console.error)(signupFailureText(r));
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
(o.log ?? console.log)(r.created ? `Created your individual seat (${email}). Your key is stored encrypted and is the only key this seat spends.` : `Re-issued your individual seat (${email}); the previous token is revoked.`);
|
|
969
|
+
return { token: r.token, source: "signup" };
|
|
970
|
+
};
|
|
971
|
+
var init = async (o) => {
|
|
972
|
+
const log = o.log ?? console.log;
|
|
973
|
+
const error = o.error ?? console.error;
|
|
974
|
+
const env = o.env ?? process.env;
|
|
975
|
+
if (!hasClaudeCode(o.home)) {
|
|
976
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
977
|
+
return 1;
|
|
978
|
+
}
|
|
979
|
+
const resolved = await resolveToken(o);
|
|
980
|
+
if (!resolved) {
|
|
981
|
+
error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Pass --token, set SLEDS_TOKEN, run without --non-interactive, or use --signup.");
|
|
982
|
+
return 2;
|
|
983
|
+
}
|
|
984
|
+
if (!isSeatToken(resolved.token)) {
|
|
985
|
+
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
986
|
+
return 2;
|
|
987
|
+
}
|
|
988
|
+
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
|
|
989
|
+
const wrote = [];
|
|
990
|
+
if (backupSettings(o.home))
|
|
991
|
+
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
992
|
+
const withProxy = applyProxy(readSettings(o.home), { url, token: resolved.token });
|
|
993
|
+
const statusLine = applyStatusLine(withProxy, o.statusLineCommand ?? DEFAULT_STATUS_LINE_COMMAND);
|
|
994
|
+
writeSettings(o.home, statusLine.settings);
|
|
995
|
+
wrote.push(`set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(o.home)}`);
|
|
996
|
+
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
997
|
+
hooks2({
|
|
998
|
+
home: o.home,
|
|
999
|
+
seatEventCommand: o.seatEventCommand ?? DEFAULT_SEAT_EVENT_COMMAND2,
|
|
1000
|
+
specEventCommand: o.specEventCommand ?? DEFAULT_SPEC_EVENT_COMMAND2,
|
|
1001
|
+
log: (line) => wrote.push(line),
|
|
1002
|
+
error
|
|
1003
|
+
});
|
|
1004
|
+
if (resolved.source !== "file") {
|
|
1005
|
+
writeTokenFile(o.home, resolved.token);
|
|
1006
|
+
wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
|
|
1007
|
+
}
|
|
1008
|
+
const prior = readConfig(o.home);
|
|
1009
|
+
writeConfig(o.home, {
|
|
1010
|
+
...prior,
|
|
1011
|
+
token: resolved.token,
|
|
1012
|
+
proxyUrl: url,
|
|
1013
|
+
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
1014
|
+
});
|
|
1015
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
1016
|
+
mkdirSync5(skillDir(o.home), { recursive: true });
|
|
1017
|
+
writeFileSync5(skillPath(o.home), SKILL_MD);
|
|
1018
|
+
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
1019
|
+
log("Nothing changes until a wall.");
|
|
1020
|
+
log("");
|
|
1021
|
+
for (const w of wrote)
|
|
1022
|
+
log(` ${w}`);
|
|
1023
|
+
log("");
|
|
1024
|
+
log(`Claude Code now talks to ${url}. Run \`sleds status\` to check.`);
|
|
1025
|
+
log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
|
|
1026
|
+
log("On a Team or Max seat, keep your seat and switch only at a wall: `sleds on` / `sleds off`.");
|
|
1027
|
+
return 0;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// src/mcp.ts
|
|
1031
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
1032
|
+
var SERVER_INFO = { name: "sleds", version: "0.0.1" };
|
|
1033
|
+
var MCP_TIMEOUT_MS = 5000;
|
|
1034
|
+
var specSchema = {
|
|
1035
|
+
type: "object",
|
|
1036
|
+
properties: {
|
|
1037
|
+
spec: {
|
|
1038
|
+
type: "string",
|
|
1039
|
+
description: "What the job should do. The first line becomes the title."
|
|
1040
|
+
},
|
|
1041
|
+
model: { type: "string", description: "Model to price and run with; org default when absent." }
|
|
1042
|
+
},
|
|
1043
|
+
required: ["spec"]
|
|
1044
|
+
};
|
|
1045
|
+
var TOOLS = [
|
|
1046
|
+
{
|
|
1047
|
+
name: "usage",
|
|
1048
|
+
description: "The SLEDS meter for this seat: built today, month against the ceiling, window and reset, allowance, queued/running/delivered jobs, the last wall, and the seat's own Pro/Max limit bars.",
|
|
1049
|
+
inputSchema: { type: "object", properties: {} }
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
name: "estimate",
|
|
1053
|
+
description: "Size a task before running it: the org estimator's cost and time band with confidence and a spend cap. Recorded, so its actuals calibrate the next estimate. Never a point figure.",
|
|
1054
|
+
inputSchema: specSchema
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
name: "overnight",
|
|
1058
|
+
description: "Queue a task as an overnight batch job at half price. The estimator sizes it first and its cap bounds the spend; the job runs in the org's overnight window and is pulled into a session later with /sled resume <id>.",
|
|
1059
|
+
inputSchema: specSchema
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "jobs",
|
|
1063
|
+
description: "This seat's overnight jobs, newest first: id, status, route, cost, when.",
|
|
1064
|
+
inputSchema: { type: "object", properties: {} }
|
|
1065
|
+
}
|
|
1066
|
+
];
|
|
1067
|
+
var NOT_SET_UP = {
|
|
1068
|
+
text: "SLEDS is not set up on this machine. Run `sleds init` first.",
|
|
1069
|
+
isError: true
|
|
1070
|
+
};
|
|
1071
|
+
var UNREACHABLE = {
|
|
1072
|
+
text: "Control plane unreachable. Nothing was recorded → try again in a moment.",
|
|
1073
|
+
isError: true
|
|
1074
|
+
};
|
|
1075
|
+
var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1076
|
+
var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1077
|
+
var str2 = (v) => typeof v === "string" ? v : undefined;
|
|
1078
|
+
async function proxyPost(deps, path, body) {
|
|
1079
|
+
const src = meterSource(deps.home);
|
|
1080
|
+
if (!src)
|
|
1081
|
+
return null;
|
|
1082
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1083
|
+
try {
|
|
1084
|
+
const res = await fetchFn(`${src.proxyUrl}${path}`, {
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${src.token}` },
|
|
1087
|
+
body: JSON.stringify(body),
|
|
1088
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? MCP_TIMEOUT_MS)
|
|
1089
|
+
});
|
|
1090
|
+
if (!res.ok)
|
|
1091
|
+
return null;
|
|
1092
|
+
const parsed = await res.json();
|
|
1093
|
+
return isObject5(parsed) ? parsed : null;
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function proxyGet(deps, path) {
|
|
1099
|
+
const src = meterSource(deps.home);
|
|
1100
|
+
if (!src)
|
|
1101
|
+
return null;
|
|
1102
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1103
|
+
try {
|
|
1104
|
+
const res = await fetchFn(`${src.proxyUrl}${path}`, {
|
|
1105
|
+
headers: { authorization: `Bearer ${src.token}` },
|
|
1106
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? MCP_TIMEOUT_MS)
|
|
1107
|
+
});
|
|
1108
|
+
if (!res.ok)
|
|
1109
|
+
return null;
|
|
1110
|
+
const parsed = await res.json();
|
|
1111
|
+
return isObject5(parsed) ? parsed : null;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function usageTool(deps) {
|
|
1117
|
+
if (!meterSource(deps.home))
|
|
1118
|
+
return NOT_SET_UP;
|
|
1119
|
+
const now = (deps.now ?? Date.now)();
|
|
1120
|
+
const m = await loadMeter({
|
|
1121
|
+
home: deps.home,
|
|
1122
|
+
now,
|
|
1123
|
+
fetchFn: deps.fetchFn,
|
|
1124
|
+
timeoutMs: deps.timeoutMs
|
|
1125
|
+
});
|
|
1126
|
+
const frag = formatSeatFragment(readSeatState(deps.home).window ?? null);
|
|
1127
|
+
if (!m) {
|
|
1128
|
+
return {
|
|
1129
|
+
text: frag ? `sleds offline (proxy did not answer) · ${frag}` : "sleds offline (proxy did not answer).",
|
|
1130
|
+
isError: true
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
const lines = formatMeter(m, now);
|
|
1134
|
+
if (frag)
|
|
1135
|
+
lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")}`);
|
|
1136
|
+
return { text: lines.join(`
|
|
1137
|
+
`) };
|
|
1138
|
+
}
|
|
1139
|
+
function bandLine(band) {
|
|
1140
|
+
if (!isObject5(band))
|
|
1141
|
+
return null;
|
|
1142
|
+
const low = num3(band.lowUsd);
|
|
1143
|
+
const high = num3(band.highUsd);
|
|
1144
|
+
const lowM = num3(band.lowMinutes);
|
|
1145
|
+
const highM = num3(band.highMinutes);
|
|
1146
|
+
const conf = num3(band.confidence);
|
|
1147
|
+
if (low === undefined || high === undefined)
|
|
1148
|
+
return null;
|
|
1149
|
+
const d = high >= 10 ? 0 : 2;
|
|
1150
|
+
const mins = lowM !== undefined && highM !== undefined ? ` · ${Math.max(0, Math.round(lowM))}–${Math.max(0, Math.round(highM))} min` : "";
|
|
1151
|
+
const pct = conf !== undefined ? ` · ${Math.round(conf * 100)}%` : "";
|
|
1152
|
+
return `$${low.toFixed(d)}–${high.toFixed(d)}${mins}${pct}`;
|
|
1153
|
+
}
|
|
1154
|
+
async function estimateTool(deps, args) {
|
|
1155
|
+
if (!meterSource(deps.home))
|
|
1156
|
+
return NOT_SET_UP;
|
|
1157
|
+
const spec = str2(args.spec)?.trim();
|
|
1158
|
+
if (!spec)
|
|
1159
|
+
return { text: "Nothing to estimate. Pass spec: what the job should do.", isError: true };
|
|
1160
|
+
const r = await proxyPost(deps, "/proxy/estimate", { spec, model: str2(args.model) });
|
|
1161
|
+
if (!r)
|
|
1162
|
+
return UNREACHABLE;
|
|
1163
|
+
const band = isObject5(r.band) ? r.band : {};
|
|
1164
|
+
const line = bandLine(band) ?? "band unreadable";
|
|
1165
|
+
const method = str2(band.method) ?? "plan";
|
|
1166
|
+
const cap = num3(band.capUsd);
|
|
1167
|
+
const threshold = num3(r.approvalThresholdUsd);
|
|
1168
|
+
const over = threshold !== undefined && threshold > 0 && num3(band.highUsd) !== undefined ? ` Over the ${usd(threshold)} threshold? ${num3(band.highUsd) > threshold ? "yes" : "no"}.` : "";
|
|
1169
|
+
const assumptions = Array.isArray(band.assumptions) ? band.assumptions.filter((a) => typeof a === "string") : [];
|
|
1170
|
+
const tail = assumptions.length > 0 ? `
|
|
1171
|
+
Assumes: ${assumptions.join("; ")}.` : "";
|
|
1172
|
+
const capText = cap !== undefined ? ` Cap ${usd(cap)}.` : "";
|
|
1173
|
+
const models = byModelText(r.byModel);
|
|
1174
|
+
const routing = routingText(r.routing);
|
|
1175
|
+
return { text: `Estimate: ${line} (${method}).${capText}${over}${tail}${models}${routing}` };
|
|
1176
|
+
}
|
|
1177
|
+
function byModelText(raw) {
|
|
1178
|
+
if (!Array.isArray(raw))
|
|
1179
|
+
return "";
|
|
1180
|
+
const parts = [];
|
|
1181
|
+
for (const row2 of raw) {
|
|
1182
|
+
if (!isObject5(row2) || !isObject5(row2.standard))
|
|
1183
|
+
continue;
|
|
1184
|
+
const model = str2(row2.model);
|
|
1185
|
+
const low = num3(row2.standard.lowUsd);
|
|
1186
|
+
const high = num3(row2.standard.highUsd);
|
|
1187
|
+
if (!model || low === undefined || high === undefined)
|
|
1188
|
+
continue;
|
|
1189
|
+
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
|
|
1190
|
+
const short = m?.[1] && m[2] ? `${m[1]} ${m[2]}${m[3] ? `.${m[3]}` : ""}` : model;
|
|
1191
|
+
parts.push(`${short} ${usdRange(low, high)}`);
|
|
1192
|
+
}
|
|
1193
|
+
return parts.length > 0 ? `
|
|
1194
|
+
By model: ${parts.join(" · ")} (overnight batch ½).` : "";
|
|
1195
|
+
}
|
|
1196
|
+
function routingText(raw) {
|
|
1197
|
+
if (!isObject5(raw) || !Array.isArray(raw.segments) || !isObject5(raw.cost))
|
|
1198
|
+
return "";
|
|
1199
|
+
const short = (model) => {
|
|
1200
|
+
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
|
|
1201
|
+
return m?.[1] && m[2] ? `${m[1]} ${m[2]}${m[3] ? `.${m[3]}` : ""}` : model;
|
|
1202
|
+
};
|
|
1203
|
+
const parts = [];
|
|
1204
|
+
for (const s of raw.segments) {
|
|
1205
|
+
if (!isObject5(s))
|
|
1206
|
+
continue;
|
|
1207
|
+
const from = num3(s.from);
|
|
1208
|
+
const to = num3(s.to);
|
|
1209
|
+
const model = str2(s.model);
|
|
1210
|
+
if (from === undefined || to === undefined || !model)
|
|
1211
|
+
continue;
|
|
1212
|
+
const reasons = Array.isArray(s.reasons) ? s.reasons.filter((x) => typeof x === "string") : [];
|
|
1213
|
+
parts.push(`${from === to ? from : `${from}–${to}`} ${short(model)}${reasons.length ? ` (${reasons.slice(0, 3).join(", ")})` : ""}`);
|
|
1214
|
+
}
|
|
1215
|
+
const c = raw.cost;
|
|
1216
|
+
const lo = num3(c.lowUsd);
|
|
1217
|
+
const hi = num3(c.highUsd);
|
|
1218
|
+
const slo = num3(c.singleLowUsd);
|
|
1219
|
+
const shi = num3(c.singleHighUsd);
|
|
1220
|
+
const single = str2(c.singleModel);
|
|
1221
|
+
if (parts.length === 0 || lo === undefined || hi === undefined || slo === undefined || shi === undefined || !single)
|
|
1222
|
+
return "";
|
|
1223
|
+
return `
|
|
1224
|
+
Routing: ${parts.join(" · ")} ≈ ${usdRange(lo, hi)} vs ${usdRange(slo, shi)} all ${short(single)}.`;
|
|
1225
|
+
}
|
|
1226
|
+
async function overnightTool(deps, args) {
|
|
1227
|
+
if (!meterSource(deps.home))
|
|
1228
|
+
return NOT_SET_UP;
|
|
1229
|
+
const spec = str2(args.spec)?.trim();
|
|
1230
|
+
if (!spec)
|
|
1231
|
+
return { text: "Nothing to queue. Pass spec: what the job should do.", isError: true };
|
|
1232
|
+
const r = await proxyPost(deps, "/proxy/overnight", { spec, model: str2(args.model) });
|
|
1233
|
+
if (!r)
|
|
1234
|
+
return { text: `Could not queue the job. ${UNREACHABLE.text}`, isError: true };
|
|
1235
|
+
const jobId = str2(r.jobId) ?? "unknown";
|
|
1236
|
+
const now = (deps.now ?? Date.now)();
|
|
1237
|
+
const at = num3(r.scheduledAt);
|
|
1238
|
+
const when = at !== undefined && at > now ? `Runs in ${shortDuration(at - now)}` : "Runs at the next tick";
|
|
1239
|
+
const band = bandLine(r.band);
|
|
1240
|
+
const cap = isObject5(r.band) ? num3(r.band.capUsd) : undefined;
|
|
1241
|
+
const sized = band ? ` ${band}${cap !== undefined ? ` · cap ${usd(cap)}` : ""}.` : "";
|
|
1242
|
+
return {
|
|
1243
|
+
text: `Queued overnight. Batch tier · ½ price.${sized} ${when}. Check with the jobs tool; pull the result into a session with /sled resume ${jobId}.`
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async function jobsTool(deps) {
|
|
1247
|
+
if (!meterSource(deps.home))
|
|
1248
|
+
return NOT_SET_UP;
|
|
1249
|
+
const r = await proxyGet(deps, "/proxy/jobs");
|
|
1250
|
+
if (!r)
|
|
1251
|
+
return UNREACHABLE;
|
|
1252
|
+
const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject5) : [];
|
|
1253
|
+
if (rows.length === 0)
|
|
1254
|
+
return { text: "No jobs for this seat. Queue one with the overnight tool." };
|
|
1255
|
+
const now = (deps.now ?? Date.now)();
|
|
1256
|
+
const lines = rows.map((j) => {
|
|
1257
|
+
const when = (ts) => num3(ts) !== undefined ? shortDuration(Math.abs(now - num3(ts))) : "";
|
|
1258
|
+
const status = str2(j.status) ?? "unknown";
|
|
1259
|
+
const cost = num3(j.costUsd);
|
|
1260
|
+
const tail = status === "delivered" ? `delivered ${when(j.deliveredAt)} ago` : status === "scheduled" ? num3(j.scheduledAt) !== undefined && num3(j.scheduledAt) > now ? `runs in ${when(j.scheduledAt)}` : "runs at the next tick" : str2(j.reason) ?? "";
|
|
1261
|
+
return [
|
|
1262
|
+
str2(j.jobId) ?? "?",
|
|
1263
|
+
status.padEnd(12),
|
|
1264
|
+
(str2(j.route) ?? "").padEnd(6),
|
|
1265
|
+
cost !== undefined ? usd(cost).padStart(8) : "".padStart(8),
|
|
1266
|
+
tail
|
|
1267
|
+
].join(" ").trimEnd();
|
|
1268
|
+
});
|
|
1269
|
+
lines.push("Resume a delivered job in a session with /sled resume <id>.");
|
|
1270
|
+
return { text: lines.join(`
|
|
1271
|
+
`) };
|
|
1272
|
+
}
|
|
1273
|
+
async function callTool(name, args, deps) {
|
|
1274
|
+
switch (name) {
|
|
1275
|
+
case "usage":
|
|
1276
|
+
return usageTool(deps);
|
|
1277
|
+
case "estimate":
|
|
1278
|
+
return estimateTool(deps, args);
|
|
1279
|
+
case "overnight":
|
|
1280
|
+
return overnightTool(deps, args);
|
|
1281
|
+
case "jobs":
|
|
1282
|
+
return jobsTool(deps);
|
|
1283
|
+
default:
|
|
1284
|
+
return { text: `Unknown tool: ${name}`, isError: true };
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
var response = (id, result) => JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
1288
|
+
var errorResponse = (id, code, message) => JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
1289
|
+
async function handleLine(line, deps) {
|
|
1290
|
+
const trimmed = line.trim();
|
|
1291
|
+
if (trimmed.length === 0)
|
|
1292
|
+
return null;
|
|
1293
|
+
let msg;
|
|
1294
|
+
try {
|
|
1295
|
+
msg = JSON.parse(trimmed);
|
|
1296
|
+
} catch {
|
|
1297
|
+
return errorResponse(null, -32700, "parse error");
|
|
1298
|
+
}
|
|
1299
|
+
if (!isObject5(msg))
|
|
1300
|
+
return errorResponse(null, -32600, "invalid request");
|
|
1301
|
+
const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
|
|
1302
|
+
const method = str2(msg.method);
|
|
1303
|
+
const isNotification = msg.id === undefined;
|
|
1304
|
+
if (!method)
|
|
1305
|
+
return isNotification ? null : errorResponse(id, -32600, "invalid request");
|
|
1306
|
+
if (method.startsWith("notifications/"))
|
|
1307
|
+
return null;
|
|
1308
|
+
const params = isObject5(msg.params) ? msg.params : {};
|
|
1309
|
+
switch (method) {
|
|
1310
|
+
case "initialize": {
|
|
1311
|
+
const requested = str2(params.protocolVersion);
|
|
1312
|
+
return response(id, {
|
|
1313
|
+
protocolVersion: requested ?? PROTOCOL_VERSION,
|
|
1314
|
+
capabilities: { tools: {} },
|
|
1315
|
+
serverInfo: SERVER_INFO
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
case "ping":
|
|
1319
|
+
return response(id, {});
|
|
1320
|
+
case "tools/list":
|
|
1321
|
+
return response(id, { tools: TOOLS });
|
|
1322
|
+
case "tools/call": {
|
|
1323
|
+
const name = str2(params.name) ?? "";
|
|
1324
|
+
const args = isObject5(params.arguments) ? params.arguments : {};
|
|
1325
|
+
const r = await callTool(name, args, deps);
|
|
1326
|
+
return response(id, {
|
|
1327
|
+
content: [{ type: "text", text: r.text }],
|
|
1328
|
+
...r.isError ? { isError: true } : {}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
default:
|
|
1332
|
+
return isNotification ? null : errorResponse(id, -32601, `method not found: ${method}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/commands/mcp.ts
|
|
1337
|
+
var mcp = async (o) => {
|
|
1338
|
+
const stdin = o.stdin ?? process.stdin;
|
|
1339
|
+
const write = o.write ?? ((line) => process.stdout.write(`${line}
|
|
1340
|
+
`));
|
|
1341
|
+
let buffer = "";
|
|
1342
|
+
for await (const chunk of stdin) {
|
|
1343
|
+
buffer += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
1344
|
+
let nl = buffer.indexOf(`
|
|
1345
|
+
`);
|
|
1346
|
+
while (nl !== -1) {
|
|
1347
|
+
const line = buffer.slice(0, nl);
|
|
1348
|
+
buffer = buffer.slice(nl + 1);
|
|
1349
|
+
const out = await handleLine(line, { home: o.home });
|
|
1350
|
+
if (out !== null)
|
|
1351
|
+
write(out);
|
|
1352
|
+
nl = buffer.indexOf(`
|
|
1353
|
+
`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (buffer.trim().length > 0) {
|
|
1357
|
+
const out = await handleLine(buffer, { home: o.home });
|
|
1358
|
+
if (out !== null)
|
|
1359
|
+
write(out);
|
|
1360
|
+
}
|
|
1361
|
+
return 0;
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// src/commands/meter.ts
|
|
1365
|
+
var meter = async (o) => {
|
|
1366
|
+
const log = o.log ?? console.log;
|
|
1367
|
+
const now = (o.now ?? Date.now)();
|
|
1368
|
+
const src = meterSource(o.home);
|
|
1369
|
+
if (!src) {
|
|
1370
|
+
log(`SLEDS is not set up (${configPath(o.home)}). Run \`sleds init\` first.`);
|
|
1371
|
+
return 1;
|
|
1372
|
+
}
|
|
1373
|
+
const m = await loadMeter({ home: o.home, now, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
1374
|
+
if (!m) {
|
|
1375
|
+
log(`sleds offline. ${src.proxyUrl} did not answer → \`sleds status\` for the proxy health.`);
|
|
1376
|
+
return 2;
|
|
1377
|
+
}
|
|
1378
|
+
for (const line of formatMeter(m, now))
|
|
1379
|
+
log(line);
|
|
1380
|
+
return 0;
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
// src/schedule.ts
|
|
1384
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1385
|
+
import { join as join5 } from "node:path";
|
|
1386
|
+
var REVERT_LABEL = "ai.sleds.revert";
|
|
1387
|
+
var plistPath = (home) => join5(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
|
|
1388
|
+
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1389
|
+
var buildLaunchdPlist = (sledsArgv, deadline) => {
|
|
1390
|
+
const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape(a)}</string>`).join(`
|
|
1391
|
+
`);
|
|
1392
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1393
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1394
|
+
<plist version="1.0">
|
|
1395
|
+
<dict>
|
|
1396
|
+
<key>Label</key>
|
|
1397
|
+
<string>${REVERT_LABEL}</string>
|
|
1398
|
+
<key>ProgramArguments</key>
|
|
1399
|
+
<array>
|
|
1400
|
+
${args}
|
|
1401
|
+
</array>
|
|
1402
|
+
<key>StartCalendarInterval</key>
|
|
1403
|
+
<dict>
|
|
1404
|
+
<key>Month</key>
|
|
1405
|
+
<integer>${deadline.getMonth() + 1}</integer>
|
|
1406
|
+
<key>Day</key>
|
|
1407
|
+
<integer>${deadline.getDate()}</integer>
|
|
1408
|
+
<key>Hour</key>
|
|
1409
|
+
<integer>${deadline.getHours()}</integer>
|
|
1410
|
+
<key>Minute</key>
|
|
1411
|
+
<integer>${deadline.getMinutes()}</integer>
|
|
1412
|
+
</dict>
|
|
1413
|
+
<key>RunAtLoad</key>
|
|
1414
|
+
<false/>
|
|
1415
|
+
</dict>
|
|
1416
|
+
</plist>
|
|
1417
|
+
`;
|
|
1418
|
+
};
|
|
1419
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
1420
|
+
var systemdCalendar = (deadline) => `${deadline.getFullYear()}-${pad2(deadline.getMonth() + 1)}-${pad2(deadline.getDate())} ` + `${pad2(deadline.getHours())}:${pad2(deadline.getMinutes())}:00`;
|
|
1421
|
+
var scheduleRevert = (d, deadlineMs) => {
|
|
1422
|
+
const deadline = new Date(deadlineMs);
|
|
1423
|
+
if (d.platform === "darwin") {
|
|
1424
|
+
const p = plistPath(d.home);
|
|
1425
|
+
try {
|
|
1426
|
+
mkdirSync6(join5(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
1427
|
+
d.exec("launchctl", ["unload", p]);
|
|
1428
|
+
writeFileSync6(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
1429
|
+
const r = d.exec("launchctl", ["load", p]);
|
|
1430
|
+
if (r.ok)
|
|
1431
|
+
return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
|
|
1432
|
+
return { kind: "manual", detail: `launchctl load failed: ${r.stderr.trim() || "unknown"}` };
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
return { kind: "manual", detail: err instanceof Error ? err.message : String(err) };
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (d.platform === "linux") {
|
|
1438
|
+
d.exec("systemctl", ["--user", "stop", `${REVERT_LABEL}.timer`]);
|
|
1439
|
+
const r = d.exec("systemd-run", [
|
|
1440
|
+
"--user",
|
|
1441
|
+
`--unit=${REVERT_LABEL}`,
|
|
1442
|
+
`--on-calendar=${systemdCalendar(deadline)}`,
|
|
1443
|
+
...d.sledsArgv,
|
|
1444
|
+
"off"
|
|
1445
|
+
]);
|
|
1446
|
+
if (r.ok)
|
|
1447
|
+
return { kind: "systemd", detail: `systemd user timer ${REVERT_LABEL}` };
|
|
1448
|
+
return { kind: "manual", detail: `systemd-run failed: ${r.stderr.trim() || "unavailable"}` };
|
|
1449
|
+
}
|
|
1450
|
+
return { kind: "manual", detail: `no scheduler on ${d.platform}` };
|
|
1451
|
+
};
|
|
1452
|
+
var cancelRevert = (d, kind) => {
|
|
1453
|
+
if (kind === "launchd" || d.platform === "darwin") {
|
|
1454
|
+
const p = plistPath(d.home);
|
|
1455
|
+
if (existsSync5(p)) {
|
|
1456
|
+
d.exec("launchctl", ["unload", p]);
|
|
1457
|
+
rmSync(p, { force: true });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (kind === "systemd" || d.platform === "linux") {
|
|
1461
|
+
d.exec("systemctl", ["--user", "stop", `${REVERT_LABEL}.timer`]);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// src/switch.ts
|
|
1466
|
+
var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1467
|
+
var envOf2 = (settings) => isObject6(settings.env) ? settings.env : {};
|
|
1468
|
+
var capturePriorEnv = (settings) => {
|
|
1469
|
+
const env = envOf2(settings);
|
|
1470
|
+
const prior = { hadEnv: isObject6(settings.env) };
|
|
1471
|
+
const url = env[BASE_URL_KEY];
|
|
1472
|
+
const token = env[AUTH_TOKEN_KEY];
|
|
1473
|
+
if (typeof url === "string")
|
|
1474
|
+
prior[BASE_URL_KEY] = url;
|
|
1475
|
+
if (typeof token === "string")
|
|
1476
|
+
prior[AUTH_TOKEN_KEY] = token;
|
|
1477
|
+
return prior;
|
|
1478
|
+
};
|
|
1479
|
+
var switchOn = (settings, cfg) => applyProxy(settings, cfg);
|
|
1480
|
+
var switchOff = (settings, prior) => {
|
|
1481
|
+
const hadEnv = prior.hadEnv === true;
|
|
1482
|
+
const stripped = removeProxy(settings);
|
|
1483
|
+
const env = { ...envOf2(stripped) };
|
|
1484
|
+
const url = prior[BASE_URL_KEY];
|
|
1485
|
+
const token = prior[AUTH_TOKEN_KEY];
|
|
1486
|
+
if (url !== undefined)
|
|
1487
|
+
env[BASE_URL_KEY] = url;
|
|
1488
|
+
if (token !== undefined)
|
|
1489
|
+
env[AUTH_TOKEN_KEY] = token;
|
|
1490
|
+
if (Object.keys(env).length === 0 && !hadEnv) {
|
|
1491
|
+
const { env: _env, ...rest } = stripped;
|
|
1492
|
+
return rest;
|
|
1493
|
+
}
|
|
1494
|
+
return { ...stripped, env };
|
|
1495
|
+
};
|
|
1496
|
+
var isPointedAtProxy = (settings) => {
|
|
1497
|
+
const env = envOf2(settings);
|
|
1498
|
+
return BASE_URL_KEY in env || AUTH_TOKEN_KEY in env;
|
|
1499
|
+
};
|
|
1500
|
+
var RELATIVE = /^\+(?:(\d+)h)?(?:(\d+)m)?$/;
|
|
1501
|
+
var parseUntilReset = (input, nowMs) => {
|
|
1502
|
+
const trimmed = input.trim();
|
|
1503
|
+
const rel = RELATIVE.exec(trimmed);
|
|
1504
|
+
let deadline;
|
|
1505
|
+
if (rel) {
|
|
1506
|
+
const hours = Number(rel[1] ?? 0);
|
|
1507
|
+
const minutes = Number(rel[2] ?? 0);
|
|
1508
|
+
if (hours === 0 && minutes === 0) {
|
|
1509
|
+
throw new Error(`--until-reset: "${input}" is zero; use something like +5h or +90m`);
|
|
1510
|
+
}
|
|
1511
|
+
deadline = nowMs + (hours * 60 + minutes) * 60000;
|
|
1512
|
+
} else {
|
|
1513
|
+
deadline = Date.parse(trimmed);
|
|
1514
|
+
if (Number.isNaN(deadline)) {
|
|
1515
|
+
throw new Error(`--until-reset: "${input}" is not +<n>h[<n>m] or an ISO timestamp`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (deadline <= nowMs) {
|
|
1519
|
+
throw new Error(`--until-reset: ${trimmed} is in the past`);
|
|
1520
|
+
}
|
|
1521
|
+
return deadline;
|
|
1522
|
+
};
|
|
1523
|
+
var formatLocal = (ms, tz) => new Intl.DateTimeFormat("en-US", {
|
|
1524
|
+
month: "short",
|
|
1525
|
+
day: "numeric",
|
|
1526
|
+
hour: "numeric",
|
|
1527
|
+
minute: "2-digit",
|
|
1528
|
+
timeZoneName: "short",
|
|
1529
|
+
...tz ? { timeZone: tz } : {}
|
|
1530
|
+
}).format(new Date(ms));
|
|
1531
|
+
var formatDuration = (ms) => {
|
|
1532
|
+
const total = Math.max(0, Math.round(ms / 60000));
|
|
1533
|
+
const h = Math.floor(total / 60);
|
|
1534
|
+
const m = total % 60;
|
|
1535
|
+
if (h === 0)
|
|
1536
|
+
return `${m}m`;
|
|
1537
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
// src/commands/off.ts
|
|
1541
|
+
var off = (o) => {
|
|
1542
|
+
const log = o.log ?? console.log;
|
|
1543
|
+
const cfg = readConfig(o.home);
|
|
1544
|
+
const settings = readSettings(o.home);
|
|
1545
|
+
const wasOn = cfg.mode === "on" || isPointedAtProxy(settings);
|
|
1546
|
+
if (wasOn)
|
|
1547
|
+
writeSettings(o.home, switchOff(settings, cfg.priorEnv ?? {}));
|
|
1548
|
+
if (o.scheduler)
|
|
1549
|
+
cancelRevert(o.scheduler, cfg.schedule);
|
|
1550
|
+
const next = {
|
|
1551
|
+
...cfg,
|
|
1552
|
+
mode: "off",
|
|
1553
|
+
since: undefined,
|
|
1554
|
+
untilReset: undefined,
|
|
1555
|
+
reason: undefined,
|
|
1556
|
+
priorEnv: undefined,
|
|
1557
|
+
schedule: undefined
|
|
1558
|
+
};
|
|
1559
|
+
writeConfig(o.home, next);
|
|
1560
|
+
if (!wasOn) {
|
|
1561
|
+
log("Sleds already off. Claude Code is on your seat.");
|
|
1562
|
+
return 0;
|
|
1563
|
+
}
|
|
1564
|
+
log("Sleds off. Claude Code back on your seat. Restart claude to apply.");
|
|
1565
|
+
return 0;
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/commands/init.ts
|
|
1569
|
+
var DEFAULT_PROXY_URL2 = "http://localhost:8787";
|
|
1570
|
+
|
|
1571
|
+
// src/commands/on.ts
|
|
1572
|
+
var on = (o) => {
|
|
1573
|
+
const log = o.log ?? console.log;
|
|
1574
|
+
const error = o.error ?? console.error;
|
|
1575
|
+
const env = o.env ?? process.env;
|
|
1576
|
+
const now = (o.now ?? Date.now)();
|
|
1577
|
+
if (!hasClaudeCode(o.home)) {
|
|
1578
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
1579
|
+
return 1;
|
|
1580
|
+
}
|
|
1581
|
+
const cfg = readConfig(o.home);
|
|
1582
|
+
const token = cfg.token ?? readTokenFile(o.home) ?? env.SLEDS_TOKEN?.trim();
|
|
1583
|
+
if (!token) {
|
|
1584
|
+
error(`No seat token in ${configPath(o.home)}. Run \`sleds init\` first.`);
|
|
1585
|
+
return 2;
|
|
1586
|
+
}
|
|
1587
|
+
if (!isSeatToken(token)) {
|
|
1588
|
+
error("Saved token is not a sled_ token. Run `sleds init --token <sled_…>`.");
|
|
1589
|
+
return 2;
|
|
1590
|
+
}
|
|
1591
|
+
const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
|
|
1592
|
+
let untilResetMs;
|
|
1593
|
+
if (o.untilReset !== undefined) {
|
|
1594
|
+
try {
|
|
1595
|
+
untilResetMs = parseUntilReset(o.untilReset, now);
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1598
|
+
return 2;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const settings = readSettings(o.home);
|
|
1602
|
+
const priorEnv = cfg.mode === "on" && cfg.priorEnv ? cfg.priorEnv : capturePriorEnv(settings);
|
|
1603
|
+
const backedUp = backupSettings(o.home);
|
|
1604
|
+
writeSettings(o.home, switchOn(settings, { url, token }));
|
|
1605
|
+
let schedule;
|
|
1606
|
+
let scheduleNote;
|
|
1607
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler && hasHomeOverride(env)) {
|
|
1608
|
+
error("SLEDS_HOME / CLAUDE_CONFIG_DIR is set: the auto-revert timer would act on the default seat. Re-run with --no-schedule and run `sleds off` yourself at the reset.");
|
|
1609
|
+
return 2;
|
|
1610
|
+
}
|
|
1611
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler) {
|
|
1612
|
+
const r = scheduleRevert(o.scheduler, untilResetMs);
|
|
1613
|
+
schedule = r.kind;
|
|
1614
|
+
scheduleNote = r.kind === "manual" ? r.detail : undefined;
|
|
1615
|
+
}
|
|
1616
|
+
const next = {
|
|
1617
|
+
...cfg,
|
|
1618
|
+
token,
|
|
1619
|
+
proxyUrl: url,
|
|
1620
|
+
mode: "on",
|
|
1621
|
+
since: new Date(now).toISOString(),
|
|
1622
|
+
priorEnv
|
|
1623
|
+
};
|
|
1624
|
+
if (untilResetMs !== undefined)
|
|
1625
|
+
next.untilReset = new Date(untilResetMs).toISOString();
|
|
1626
|
+
else
|
|
1627
|
+
next.untilReset = undefined;
|
|
1628
|
+
next.reason = o.reason?.trim() || undefined;
|
|
1629
|
+
next.schedule = schedule;
|
|
1630
|
+
writeConfig(o.home, next);
|
|
1631
|
+
log(`Sleds on. Claude Code now runs on the org key through ${url}. Restart claude to apply.`);
|
|
1632
|
+
if (untilResetMs !== undefined) {
|
|
1633
|
+
log(`Reverts at ${formatLocal(untilResetMs, o.tz)} — run \`sleds off\` earlier if you want.`);
|
|
1634
|
+
if (schedule === undefined || schedule === "manual") {
|
|
1635
|
+
const why = scheduleNote ? ` (${scheduleNote})` : "";
|
|
1636
|
+
log(`Auto-revert not scheduled${why}. Run \`sleds off\` after the reset.`);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (backedUp)
|
|
1640
|
+
log(`Backed up ${settingsPath(o.home)} to ${backupPath(o.home)}.`);
|
|
1641
|
+
return 0;
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// src/commands/seatEvent.ts
|
|
1645
|
+
var HOOK_STDIN_TIMEOUT_MS = 1000;
|
|
1646
|
+
var NOTIFICATION_TITLE = "sleds";
|
|
1647
|
+
var isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1648
|
+
var notificationType = (text) => {
|
|
1649
|
+
try {
|
|
1650
|
+
const raw = JSON.parse(text);
|
|
1651
|
+
if (!isObject7(raw))
|
|
1652
|
+
return;
|
|
1653
|
+
return typeof raw.notification_type === "string" ? raw.notification_type : undefined;
|
|
1654
|
+
} catch {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
var osascriptArgs = (title, body) => [
|
|
1659
|
+
"-e",
|
|
1660
|
+
"on run argv",
|
|
1661
|
+
"-e",
|
|
1662
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
1663
|
+
"-e",
|
|
1664
|
+
"end run",
|
|
1665
|
+
title,
|
|
1666
|
+
body
|
|
1667
|
+
];
|
|
1668
|
+
var seatEvent = async (o) => {
|
|
1669
|
+
const log = o.log ?? console.log;
|
|
1670
|
+
const now = (o.now ?? Date.now)();
|
|
1671
|
+
const input = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
1672
|
+
const kind = notificationType(input);
|
|
1673
|
+
try {
|
|
1674
|
+
const state = readSeatState(o.home);
|
|
1675
|
+
const week = state.window?.sevenDay;
|
|
1676
|
+
const src = meterSource(o.home);
|
|
1677
|
+
if (src) {
|
|
1678
|
+
await postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
1679
|
+
...week ? { resetsAt: week.resetsAt, usedPct: week.usedPct } : {},
|
|
1680
|
+
...kind ? { notification: kind } : {}
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
if (week && week.resetsAt > now)
|
|
1684
|
+
writeSeatState(o.home, { ...state, lastWallResetsAt: week.resetsAt });
|
|
1685
|
+
} catch {}
|
|
1686
|
+
log(SEAT_WALL_LINE);
|
|
1687
|
+
if ((o.platform ?? process.platform) === "darwin" && o.exec) {
|
|
1688
|
+
try {
|
|
1689
|
+
o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE, SEAT_WALL_LINE));
|
|
1690
|
+
} catch {}
|
|
1691
|
+
}
|
|
1692
|
+
return 0;
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
// src/commands/seatSync.ts
|
|
1696
|
+
import { execFileSync } from "node:child_process";
|
|
1697
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1698
|
+
import { platform } from "node:os";
|
|
1699
|
+
import { join as join7 } from "node:path";
|
|
1700
|
+
|
|
1701
|
+
// src/cookieDb.ts
|
|
1702
|
+
import { copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
1703
|
+
import { tmpdir } from "node:os";
|
|
1704
|
+
import { join as join6 } from "node:path";
|
|
1705
|
+
var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
|
|
1706
|
+
var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
1707
|
+
async function queryRows(dbPath) {
|
|
1708
|
+
if (isBun()) {
|
|
1709
|
+
const { Database } = await import("bun:sqlite");
|
|
1710
|
+
const conn2 = new Database(dbPath, { readonly: true });
|
|
1711
|
+
try {
|
|
1712
|
+
return conn2.query(QUERY).all();
|
|
1713
|
+
} finally {
|
|
1714
|
+
conn2.close();
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const mod = nodeSqlite();
|
|
1718
|
+
const conn = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
1719
|
+
try {
|
|
1720
|
+
return conn.prepare(QUERY).all();
|
|
1721
|
+
} finally {
|
|
1722
|
+
conn.close();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function nodeSqlite() {
|
|
1726
|
+
const get = process.getBuiltinModule;
|
|
1727
|
+
const mod = get ? get("node:sqlite") : undefined;
|
|
1728
|
+
if (!mod)
|
|
1729
|
+
throw new Error("this Node has no node:sqlite (need Node ≥ 22.5) and Bun is not running");
|
|
1730
|
+
return mod;
|
|
1731
|
+
}
|
|
1732
|
+
async function readCookieRows(dbPath) {
|
|
1733
|
+
const copy = join6(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
|
|
1734
|
+
copyFileSync(dbPath, copy);
|
|
1735
|
+
try {
|
|
1736
|
+
const rows = await queryRows(copy);
|
|
1737
|
+
return rows.map((r) => ({ name: r.name, encrypted_value: Buffer.from(r.ev, "hex") }));
|
|
1738
|
+
} finally {
|
|
1739
|
+
try {
|
|
1740
|
+
rmSync2(copy, { force: true });
|
|
1741
|
+
} catch {}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/seatSync.ts
|
|
1746
|
+
import { createDecipheriv, pbkdf2Sync } from "node:crypto";
|
|
1747
|
+
var BROWSERS = {
|
|
1748
|
+
claude: { appDir: "Claude", keychainService: "Claude Safe Storage", profiled: false },
|
|
1749
|
+
chrome: { appDir: "Google/Chrome", keychainService: "Chrome Safe Storage", profiled: true },
|
|
1750
|
+
"chrome-beta": {
|
|
1751
|
+
appDir: "Google/Chrome Beta",
|
|
1752
|
+
keychainService: "Chrome Safe Storage",
|
|
1753
|
+
profiled: true
|
|
1754
|
+
},
|
|
1755
|
+
"chrome-canary": {
|
|
1756
|
+
appDir: "Google/Chrome Canary",
|
|
1757
|
+
keychainService: "Chrome Safe Storage",
|
|
1758
|
+
profiled: true
|
|
1759
|
+
},
|
|
1760
|
+
chromium: { appDir: "Chromium", keychainService: "Chromium Safe Storage", profiled: true },
|
|
1761
|
+
brave: {
|
|
1762
|
+
appDir: "BraveSoftware/Brave-Browser",
|
|
1763
|
+
keychainService: "Brave Safe Storage",
|
|
1764
|
+
profiled: true
|
|
1765
|
+
},
|
|
1766
|
+
edge: {
|
|
1767
|
+
appDir: "Microsoft Edge",
|
|
1768
|
+
keychainService: "Microsoft Edge Safe Storage",
|
|
1769
|
+
profiled: true
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
var isBrowserId = (s) => Object.hasOwn(BROWSERS, s);
|
|
1773
|
+
var SOURCE_FLAGS = new Set(["browser", "profile", "org", "token"]);
|
|
1774
|
+
function parseSeatSyncArgs(argv) {
|
|
1775
|
+
const source = { browser: "claude", install: false, uninstall: false };
|
|
1776
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1777
|
+
const arg = argv[i];
|
|
1778
|
+
if (arg === undefined)
|
|
1779
|
+
continue;
|
|
1780
|
+
if (arg === "--install") {
|
|
1781
|
+
source.install = true;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
if (arg === "--uninstall") {
|
|
1785
|
+
source.uninstall = true;
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
if (!arg.startsWith("--"))
|
|
1789
|
+
return { error: `unexpected argument: ${arg}` };
|
|
1790
|
+
const eq = arg.indexOf("=");
|
|
1791
|
+
const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
1792
|
+
if (!SOURCE_FLAGS.has(key))
|
|
1793
|
+
return { error: `unknown flag: --${key}` };
|
|
1794
|
+
let value;
|
|
1795
|
+
if (eq !== -1)
|
|
1796
|
+
value = arg.slice(eq + 1);
|
|
1797
|
+
else {
|
|
1798
|
+
const next = argv[i + 1];
|
|
1799
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
1800
|
+
value = next;
|
|
1801
|
+
i++;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (!value)
|
|
1805
|
+
return { error: `--${key} needs a value` };
|
|
1806
|
+
if (key === "browser") {
|
|
1807
|
+
if (!isBrowserId(value))
|
|
1808
|
+
return { error: `unknown browser: ${value} (one of ${Object.keys(BROWSERS).join(", ")})` };
|
|
1809
|
+
source.browser = value;
|
|
1810
|
+
} else
|
|
1811
|
+
source[key] = value;
|
|
1812
|
+
}
|
|
1813
|
+
if (source.profile && !BROWSERS[source.browser].profiled)
|
|
1814
|
+
return { error: `--profile does not apply to --browser ${source.browser}` };
|
|
1815
|
+
return { source };
|
|
1816
|
+
}
|
|
1817
|
+
function resolveProfileDir(localStateJson, profile) {
|
|
1818
|
+
let cache = {};
|
|
1819
|
+
if (localStateJson) {
|
|
1820
|
+
try {
|
|
1821
|
+
const raw = JSON.parse(localStateJson);
|
|
1822
|
+
const p = isObject8(raw) && isObject8(raw.profile) ? raw.profile : null;
|
|
1823
|
+
if (p && isObject8(p.info_cache))
|
|
1824
|
+
cache = p.info_cache;
|
|
1825
|
+
} catch {}
|
|
1826
|
+
}
|
|
1827
|
+
if (Object.hasOwn(cache, profile))
|
|
1828
|
+
return { dir: profile };
|
|
1829
|
+
const want = profile.toLowerCase();
|
|
1830
|
+
const hits = [];
|
|
1831
|
+
for (const [dir, info] of Object.entries(cache)) {
|
|
1832
|
+
if (!isObject8(info))
|
|
1833
|
+
continue;
|
|
1834
|
+
const match = ["name", "user_name", "gaia_name"].some((k) => {
|
|
1835
|
+
const v = info[k];
|
|
1836
|
+
return typeof v === "string" && v.toLowerCase() === want;
|
|
1837
|
+
});
|
|
1838
|
+
if (match)
|
|
1839
|
+
hits.push(dir);
|
|
1840
|
+
}
|
|
1841
|
+
if (hits.length === 1)
|
|
1842
|
+
return { dir: hits[0] ?? null };
|
|
1843
|
+
if (hits.length > 1)
|
|
1844
|
+
return { dir: null, ambiguous: hits };
|
|
1845
|
+
return { dir: Object.keys(cache).length === 0 ? profile : null };
|
|
1846
|
+
}
|
|
1847
|
+
function cookieDbCandidates(profileDir) {
|
|
1848
|
+
return [`${profileDir}/Network/Cookies`, `${profileDir}/Cookies`];
|
|
1849
|
+
}
|
|
1850
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1851
|
+
function launchLabel(source) {
|
|
1852
|
+
const base = "ai.sleds.seat-sync";
|
|
1853
|
+
if (source.browser === "claude" && !source.profile)
|
|
1854
|
+
return base;
|
|
1855
|
+
const parts = [source.browser, ...source.profile ? [slug(source.profile)] : []];
|
|
1856
|
+
return `${base}.${parts.join("-")}`;
|
|
1857
|
+
}
|
|
1858
|
+
function sourceArgs(source) {
|
|
1859
|
+
const out = [];
|
|
1860
|
+
if (source.browser !== "claude")
|
|
1861
|
+
out.push("--browser", source.browser);
|
|
1862
|
+
if (source.profile)
|
|
1863
|
+
out.push("--profile", source.profile);
|
|
1864
|
+
if (source.org)
|
|
1865
|
+
out.push("--org", source.org);
|
|
1866
|
+
if (source.token)
|
|
1867
|
+
out.push("--token", source.token);
|
|
1868
|
+
return out;
|
|
1869
|
+
}
|
|
1870
|
+
var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1871
|
+
var num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1872
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
1873
|
+
function parseUsageResponse(raw, now = Date.now()) {
|
|
1874
|
+
if (!isObject8(raw))
|
|
1875
|
+
return null;
|
|
1876
|
+
const out = { capturedAt: now, models: {} };
|
|
1877
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1878
|
+
if (!isObject8(v))
|
|
1879
|
+
continue;
|
|
1880
|
+
if (k === "extra_usage") {
|
|
1881
|
+
const credits = num4(v.used_credits);
|
|
1882
|
+
if (credits !== undefined)
|
|
1883
|
+
out.usedCreditsUsd = round2(credits / 100);
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
const usedPct = num4(v.utilization);
|
|
1887
|
+
const resetsAt = parseResetsAt(v.resets_at);
|
|
1888
|
+
if (usedPct === undefined || usedPct < 0 || resetsAt === undefined)
|
|
1889
|
+
continue;
|
|
1890
|
+
const bar2 = { usedPct, resetsAt };
|
|
1891
|
+
if (k === "five_hour")
|
|
1892
|
+
out.fiveHour = bar2;
|
|
1893
|
+
else if (k === "seven_day")
|
|
1894
|
+
out.sevenDay = bar2;
|
|
1895
|
+
else
|
|
1896
|
+
out.models[k] = bar2;
|
|
1897
|
+
}
|
|
1898
|
+
const hasAny = out.fiveHour || out.sevenDay || Object.keys(out.models).length > 0 || out.usedCreditsUsd !== undefined;
|
|
1899
|
+
return hasAny ? out : null;
|
|
1900
|
+
}
|
|
1901
|
+
function seatSyncBody(u) {
|
|
1902
|
+
return {
|
|
1903
|
+
capturedAt: u.capturedAt,
|
|
1904
|
+
...u.fiveHour ? { fiveHour: u.fiveHour } : {},
|
|
1905
|
+
...u.sevenDay ? { sevenDay: u.sevenDay } : {},
|
|
1906
|
+
...Object.keys(u.models).length > 0 ? { models: u.models } : {},
|
|
1907
|
+
...u.usedCreditsUsd !== undefined ? { usedCreditsUsd: u.usedCreditsUsd } : {}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function deriveKey(safeStoragePw) {
|
|
1911
|
+
return pbkdf2Sync(safeStoragePw, "saltysalt", 1003, 16, "sha1");
|
|
1912
|
+
}
|
|
1913
|
+
function decryptCookieBytes(enc, key) {
|
|
1914
|
+
if (enc.length < 3 || enc.subarray(0, 3).toString("latin1") !== "v10")
|
|
1915
|
+
return null;
|
|
1916
|
+
const iv = Buffer.alloc(16, 32);
|
|
1917
|
+
const d = createDecipheriv("aes-128-cbc", key, iv);
|
|
1918
|
+
d.setAutoPadding(false);
|
|
1919
|
+
let out;
|
|
1920
|
+
try {
|
|
1921
|
+
out = Buffer.concat([d.update(enc.subarray(3)), d.final()]);
|
|
1922
|
+
} catch {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
if (out.length === 0)
|
|
1926
|
+
return null;
|
|
1927
|
+
const pad = out[out.length - 1] ?? 0;
|
|
1928
|
+
if (pad > 0 && pad <= 16 && pad <= out.length)
|
|
1929
|
+
out = out.subarray(0, out.length - pad);
|
|
1930
|
+
return out;
|
|
1931
|
+
}
|
|
1932
|
+
var DOMAIN_HASH_LEN = 32;
|
|
1933
|
+
function prefixLen(sessionKeyBytes) {
|
|
1934
|
+
if (sessionKeyBytes && sessionKeyBytes.subarray(0, 6).toString("latin1") === "sk-ant")
|
|
1935
|
+
return 0;
|
|
1936
|
+
return DOMAIN_HASH_LEN;
|
|
1937
|
+
}
|
|
1938
|
+
function decryptCookieJar(rows, key) {
|
|
1939
|
+
const raw = [];
|
|
1940
|
+
for (const r of rows) {
|
|
1941
|
+
const bytes = decryptCookieBytes(r.encrypted_value, key);
|
|
1942
|
+
if (bytes)
|
|
1943
|
+
raw.push({ name: r.name, bytes });
|
|
1944
|
+
}
|
|
1945
|
+
const sessionKey = raw.find((r) => r.name === "sessionKey")?.bytes ?? null;
|
|
1946
|
+
const strip = prefixLen(sessionKey);
|
|
1947
|
+
const jar = {};
|
|
1948
|
+
for (const r of raw)
|
|
1949
|
+
jar[r.name] = r.bytes.subarray(strip).toString("utf8");
|
|
1950
|
+
return jar;
|
|
1951
|
+
}
|
|
1952
|
+
var SEAT_SYNC_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
1953
|
+
function cookieHeader(jar, activeOrg) {
|
|
1954
|
+
const j = activeOrg ? { ...jar, lastActiveOrg: activeOrg } : jar;
|
|
1955
|
+
return Object.entries(j).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1956
|
+
}
|
|
1957
|
+
function usageUrl(org) {
|
|
1958
|
+
return `https://claude.ai/api/organizations/${org}/usage`;
|
|
1959
|
+
}
|
|
1960
|
+
function usageHeaders(cookie) {
|
|
1961
|
+
return {
|
|
1962
|
+
Cookie: cookie,
|
|
1963
|
+
"User-Agent": SEAT_SYNC_UA,
|
|
1964
|
+
accept: "*/*",
|
|
1965
|
+
origin: "https://claude.ai",
|
|
1966
|
+
referer: "https://claude.ai/",
|
|
1967
|
+
"anthropic-client-platform": "web_claude_ai",
|
|
1968
|
+
"sec-fetch-site": "same-origin",
|
|
1969
|
+
"sec-fetch-mode": "cors",
|
|
1970
|
+
"sec-fetch-dest": "empty"
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
var USAGE_MAX_SECONDS = 8;
|
|
1974
|
+
function usageCurlArgs(org, cookie) {
|
|
1975
|
+
const args = [
|
|
1976
|
+
"-sS",
|
|
1977
|
+
"-o",
|
|
1978
|
+
"-",
|
|
1979
|
+
"-w",
|
|
1980
|
+
`
|
|
1981
|
+
%{http_code}`,
|
|
1982
|
+
"--max-time",
|
|
1983
|
+
String(USAGE_MAX_SECONDS),
|
|
1984
|
+
usageUrl(org)
|
|
1985
|
+
];
|
|
1986
|
+
for (const [k, v] of Object.entries(usageHeaders(cookie)))
|
|
1987
|
+
args.push("-H", `${k}: ${v}`);
|
|
1988
|
+
return args;
|
|
1989
|
+
}
|
|
1990
|
+
function parseCurlOutput(out) {
|
|
1991
|
+
const nl = out.lastIndexOf(`
|
|
1992
|
+
`);
|
|
1993
|
+
if (nl === -1)
|
|
1994
|
+
return { status: 0, body: out };
|
|
1995
|
+
return { status: Number.parseInt(out.slice(nl + 1).trim(), 10) || 0, body: out.slice(0, nl) };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/commands/seatSync.ts
|
|
1999
|
+
var APP_SUPPORT = "Library/Application Support";
|
|
2000
|
+
var SYNC_INTERVAL_S = 600;
|
|
2001
|
+
var FETCH_TIMEOUT_MS = 8000;
|
|
2002
|
+
function findFile(root, name) {
|
|
2003
|
+
const stack = [root];
|
|
2004
|
+
while (stack.length > 0) {
|
|
2005
|
+
const dir = stack.pop();
|
|
2006
|
+
if (!dir || !existsSync6(dir))
|
|
2007
|
+
continue;
|
|
2008
|
+
let entries;
|
|
2009
|
+
try {
|
|
2010
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2011
|
+
} catch {
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
for (const e of entries) {
|
|
2015
|
+
const p = join7(dir, e.name);
|
|
2016
|
+
if (e.isDirectory())
|
|
2017
|
+
stack.push(p);
|
|
2018
|
+
else if (e.name === name)
|
|
2019
|
+
return p;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
function findCookieDb(home, source) {
|
|
2025
|
+
const b = BROWSERS[source.browser];
|
|
2026
|
+
const root = join7(home, APP_SUPPORT, b.appDir);
|
|
2027
|
+
if (!existsSync6(root))
|
|
2028
|
+
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
2029
|
+
if (!b.profiled) {
|
|
2030
|
+
const db2 = findFile(root, "Cookies");
|
|
2031
|
+
if (!db2)
|
|
2032
|
+
throw new Error(`no cookie store under ${root}`);
|
|
2033
|
+
return db2;
|
|
2034
|
+
}
|
|
2035
|
+
const localStatePath = join7(root, "Local State");
|
|
2036
|
+
const localState = existsSync6(localStatePath) ? readFileSync5(localStatePath, "utf8") : null;
|
|
2037
|
+
const wanted = source.profile ?? "Default";
|
|
2038
|
+
const { dir, ambiguous } = resolveProfileDir(localState, wanted);
|
|
2039
|
+
if (ambiguous)
|
|
2040
|
+
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
2041
|
+
if (!dir)
|
|
2042
|
+
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
2043
|
+
const db = cookieDbCandidates(join7(root, dir)).find((p) => existsSync6(p));
|
|
2044
|
+
if (!db)
|
|
2045
|
+
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
2046
|
+
return db;
|
|
2047
|
+
}
|
|
2048
|
+
async function readCookieRowsBun(home, source) {
|
|
2049
|
+
return readCookieRows(findCookieDb(home, source));
|
|
2050
|
+
}
|
|
2051
|
+
function keychainKey(service) {
|
|
2052
|
+
try {
|
|
2053
|
+
return execFileSync("security", ["find-generic-password", "-w", "-s", service], {
|
|
2054
|
+
encoding: "utf8"
|
|
2055
|
+
}).trim();
|
|
2056
|
+
} catch {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function decryptJar(rows, safeStoragePw) {
|
|
2061
|
+
return decryptCookieJar(rows, deriveKey(safeStoragePw));
|
|
2062
|
+
}
|
|
2063
|
+
var curlUsage = (org, cookie) => {
|
|
2064
|
+
try {
|
|
2065
|
+
const out = execFileSync("curl", usageCurlArgs(org, cookie), {
|
|
2066
|
+
encoding: "utf8",
|
|
2067
|
+
maxBuffer: 8000000
|
|
2068
|
+
});
|
|
2069
|
+
return parseCurlOutput(out);
|
|
2070
|
+
} catch {
|
|
2071
|
+
return { status: 0, body: "" };
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
function fetchSeatUsage(jar, org, now, run) {
|
|
2075
|
+
const { status, body } = run(org, cookieHeader(jar, org));
|
|
2076
|
+
if (status !== 200)
|
|
2077
|
+
return { usage: null, status };
|
|
2078
|
+
try {
|
|
2079
|
+
return { usage: parseUsageResponse(JSON.parse(body), now), status };
|
|
2080
|
+
} catch {
|
|
2081
|
+
return { usage: null, status };
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
function launchAgentPlist(bun, script, home, source) {
|
|
2085
|
+
const label = launchLabel(source);
|
|
2086
|
+
const log = join7(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
|
|
2087
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2088
|
+
const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
|
|
2089
|
+
`);
|
|
2090
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2091
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2092
|
+
<plist version="1.0">
|
|
2093
|
+
<dict>
|
|
2094
|
+
<key>Label</key><string>${label}</string>
|
|
2095
|
+
<key>ProgramArguments</key>
|
|
2096
|
+
<array>
|
|
2097
|
+
${args}
|
|
2098
|
+
</array>
|
|
2099
|
+
<key>StartInterval</key><integer>${SYNC_INTERVAL_S}</integer>
|
|
2100
|
+
<key>RunAtLoad</key><true/>
|
|
2101
|
+
<key>StandardOutPath</key><string>${esc(log)}</string>
|
|
2102
|
+
<key>StandardErrorPath</key><string>${esc(log)}</string>
|
|
2103
|
+
</dict>
|
|
2104
|
+
</plist>
|
|
2105
|
+
`;
|
|
2106
|
+
}
|
|
2107
|
+
function plistPath2(home, source) {
|
|
2108
|
+
return join7(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
|
|
2109
|
+
}
|
|
2110
|
+
function install(home, argv, source, log) {
|
|
2111
|
+
const bun = argv[0] ?? process.execPath;
|
|
2112
|
+
const script = argv[1] ?? "";
|
|
2113
|
+
if (!script) {
|
|
2114
|
+
log("cannot resolve the sleds script path to install the timer");
|
|
2115
|
+
return 1;
|
|
2116
|
+
}
|
|
2117
|
+
const dir = join7(home, "Library", "LaunchAgents");
|
|
2118
|
+
mkdirSync7(dir, { recursive: true });
|
|
2119
|
+
const path = plistPath2(home, source);
|
|
2120
|
+
writeFileSync7(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
|
|
2121
|
+
try {
|
|
2122
|
+
execFileSync("launchctl", ["unload", path], { stdio: "ignore" });
|
|
2123
|
+
} catch {}
|
|
2124
|
+
execFileSync("launchctl", ["load", "-w", path]);
|
|
2125
|
+
log(`seat-sync timer installed (every ${SYNC_INTERVAL_S / 60} min) → ${path}`);
|
|
2126
|
+
return 0;
|
|
2127
|
+
}
|
|
2128
|
+
function uninstall(home, source, log) {
|
|
2129
|
+
const path = plistPath2(home, source);
|
|
2130
|
+
try {
|
|
2131
|
+
execFileSync("launchctl", ["unload", "-w", path], { stdio: "ignore" });
|
|
2132
|
+
} catch {}
|
|
2133
|
+
log(existsSync6(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
2134
|
+
return 0;
|
|
2135
|
+
}
|
|
2136
|
+
var seatSync = async (o) => {
|
|
2137
|
+
const log = o.log ?? console.log;
|
|
2138
|
+
const now = (o.now ?? Date.now)();
|
|
2139
|
+
const parsed = parseSeatSyncArgs(o.argv ?? process.argv.slice(3));
|
|
2140
|
+
if ("error" in parsed) {
|
|
2141
|
+
log(`seat-sync: ${parsed.error}`);
|
|
2142
|
+
return 1;
|
|
2143
|
+
}
|
|
2144
|
+
const { source } = parsed;
|
|
2145
|
+
if (source.uninstall)
|
|
2146
|
+
return uninstall(o.home, source, log);
|
|
2147
|
+
if (source.install)
|
|
2148
|
+
return install(o.home, [process.execPath, process.argv[1] ?? ""], source, log);
|
|
2149
|
+
if (platform() !== "darwin" && !o.readCookieRows) {
|
|
2150
|
+
log("seat-sync is macOS-only for now.");
|
|
2151
|
+
return 2;
|
|
2152
|
+
}
|
|
2153
|
+
const saved = meterSource(o.home);
|
|
2154
|
+
const proxyUrl = saved?.proxyUrl;
|
|
2155
|
+
const token = source.token ?? saved?.token;
|
|
2156
|
+
if (!proxyUrl || !token) {
|
|
2157
|
+
log("SLEDS is not set up. Run `sleds init` first.");
|
|
2158
|
+
return 1;
|
|
2159
|
+
}
|
|
2160
|
+
let rows;
|
|
2161
|
+
try {
|
|
2162
|
+
rows = o.readCookieRows ? o.readCookieRows(o.home, source) : await readCookieRowsBun(o.home, source);
|
|
2163
|
+
} catch (e) {
|
|
2164
|
+
log(`seat-sync: ${e instanceof Error ? e.message : String(e)}`);
|
|
2165
|
+
return 1;
|
|
2166
|
+
}
|
|
2167
|
+
const service = BROWSERS[source.browser].keychainService;
|
|
2168
|
+
const pw = (o.safeStorageKey ?? keychainKey)(service);
|
|
2169
|
+
if (!pw) {
|
|
2170
|
+
log(`seat-sync: could not read the "${service}" key from your Keychain.`);
|
|
2171
|
+
return 1;
|
|
2172
|
+
}
|
|
2173
|
+
const jar = decryptJar(rows, pw);
|
|
2174
|
+
const where = source.browser === "claude" ? "the Claude app" : `this ${source.browser} profile`;
|
|
2175
|
+
if (!rows.some((r) => r.name === "sessionKey")) {
|
|
2176
|
+
log(`seat-sync: ${where} has ${rows.length} claude.ai cookies but no sessionKey — sign in to claude.ai there, or read a browser profile with --browser chrome --profile <yours>.`);
|
|
2177
|
+
return 1;
|
|
2178
|
+
}
|
|
2179
|
+
if (!jar.sessionKey?.startsWith("sk-ant")) {
|
|
2180
|
+
log(`seat-sync: could not decrypt ${where}'s session (wrong "${service}" Keychain key?).`);
|
|
2181
|
+
return 1;
|
|
2182
|
+
}
|
|
2183
|
+
const org = source.org ?? jar.lastActiveOrg;
|
|
2184
|
+
if (!org) {
|
|
2185
|
+
log("seat-sync: no active org in the session cookie (pass --org <uuid>).");
|
|
2186
|
+
return 1;
|
|
2187
|
+
}
|
|
2188
|
+
const { usage, status } = fetchSeatUsage(jar, org, now, o.runUsage ?? curlUsage);
|
|
2189
|
+
if (!usage) {
|
|
2190
|
+
const why = status === 403 ? "claude.ai refused the read (403) — session expired, or this org is not readable by this session." : status === 0 ? "could not reach claude.ai." : `unexpected HTTP ${status}.`;
|
|
2191
|
+
log(`seat-sync: usage read failed — ${why}`);
|
|
2192
|
+
return 2;
|
|
2193
|
+
}
|
|
2194
|
+
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage), o.fetchFn ?? fetch);
|
|
2195
|
+
const wk = usage.sevenDay ? `wk ${Math.round(usage.sevenDay.usedPct)}%` : "wk —";
|
|
2196
|
+
const models = Object.keys(usage.models).length;
|
|
2197
|
+
const credits = usage.usedCreditsUsd !== undefined ? ` · $${usage.usedCreditsUsd.toFixed(2)}` : "";
|
|
2198
|
+
log(posted ? `seat-sync: posted ${wk} · ${models} model bucket${models === 1 ? "" : "s"}${credits}` : `seat-sync: read ${wk} but the proxy did not accept it`);
|
|
2199
|
+
return posted ? 0 : 2;
|
|
2200
|
+
};
|
|
2201
|
+
async function postSeat(proxyUrl, token, body, fetchFn) {
|
|
2202
|
+
try {
|
|
2203
|
+
const res = await fetchFn(`${proxyUrl}/proxy/seat`, {
|
|
2204
|
+
method: "POST",
|
|
2205
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
2206
|
+
body: JSON.stringify(body),
|
|
2207
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2208
|
+
});
|
|
2209
|
+
return res.ok;
|
|
2210
|
+
} catch {
|
|
2211
|
+
return false;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/commands/specEvent.ts
|
|
2216
|
+
import { readFileSync as readFileSync6, statSync } from "node:fs";
|
|
2217
|
+
import { basename, isAbsolute, relative } from "node:path";
|
|
2218
|
+
var HOOK_STDIN_TIMEOUT_MS2 = 1000;
|
|
2219
|
+
var ESTIMATE_TIMEOUT_MS = 4000;
|
|
2220
|
+
var MIN_SPEC_CHARS = 200;
|
|
2221
|
+
var MAX_SPEC_CHARS = 12000;
|
|
2222
|
+
var SPEC_DIRS = /(^|\/)(specs?|docs?|plans?|design|rfcs?|prds?|proposals?)(\/|$)/i;
|
|
2223
|
+
var SPEC_NAMES = /(spec|plan|prd|design|rfc|proposal|brief|requirements)/i;
|
|
2224
|
+
var NOT_SPEC = /(^|\/)(readme|changelog|contributing|license|code_of_conduct|node_modules)(\.|\/|$)/i;
|
|
2225
|
+
function isSpecPath(path) {
|
|
2226
|
+
if (!/\.(md|mdx)$/i.test(path))
|
|
2227
|
+
return false;
|
|
2228
|
+
if (NOT_SPEC.test(path))
|
|
2229
|
+
return false;
|
|
2230
|
+
return SPEC_DIRS.test(path) || SPEC_NAMES.test(basename(path));
|
|
2231
|
+
}
|
|
2232
|
+
function specTitle(body, path) {
|
|
2233
|
+
const m = /^#\s+(.+?)\s*$/m.exec(body);
|
|
2234
|
+
const t = m?.[1]?.trim();
|
|
2235
|
+
return (t && t.length > 0 ? t : basename(path).replace(/\.(md|mdx)$/i, "")).slice(0, 120);
|
|
2236
|
+
}
|
|
2237
|
+
function looksLikeSpec(body) {
|
|
2238
|
+
if (body.trim().length < MIN_SPEC_CHARS)
|
|
2239
|
+
return false;
|
|
2240
|
+
return /^#{1,3}\s/m.test(body) || /^\s*(?:[-*+]|\d+[.)])\s+/m.test(body);
|
|
2241
|
+
}
|
|
2242
|
+
function parseHookInput(raw) {
|
|
2243
|
+
let parsed;
|
|
2244
|
+
try {
|
|
2245
|
+
parsed = JSON.parse(raw);
|
|
2246
|
+
} catch {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2250
|
+
return null;
|
|
2251
|
+
const p = parsed;
|
|
2252
|
+
const toolName = typeof p.tool_name === "string" ? p.tool_name : "";
|
|
2253
|
+
const input = typeof p.tool_input === "object" && p.tool_input !== null ? p.tool_input : {};
|
|
2254
|
+
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
2255
|
+
if (!filePath)
|
|
2256
|
+
return null;
|
|
2257
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : "";
|
|
2258
|
+
return { toolName, filePath, cwd };
|
|
2259
|
+
}
|
|
2260
|
+
function projectKey(filePath, cwd) {
|
|
2261
|
+
if (cwd && isAbsolute(filePath) && isAbsolute(cwd)) {
|
|
2262
|
+
const rel = relative(cwd, filePath);
|
|
2263
|
+
if (rel && !rel.startsWith(".."))
|
|
2264
|
+
return rel;
|
|
2265
|
+
}
|
|
2266
|
+
return filePath;
|
|
2267
|
+
}
|
|
2268
|
+
function readFileOrNull(path) {
|
|
2269
|
+
try {
|
|
2270
|
+
if (statSync(path).size > 2000000)
|
|
2271
|
+
return null;
|
|
2272
|
+
return readFileSync6(path, "utf8");
|
|
2273
|
+
} catch {
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
var specEvent = async (o) => {
|
|
2278
|
+
const log = o.log ?? console.log;
|
|
2279
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS2);
|
|
2280
|
+
const input = raw ? parseHookInput(raw) : null;
|
|
2281
|
+
if (!input || !isSpecPath(input.filePath))
|
|
2282
|
+
return 0;
|
|
2283
|
+
const src = meterSource(o.home);
|
|
2284
|
+
if (!src)
|
|
2285
|
+
return 0;
|
|
2286
|
+
const body = (o.readFile ?? readFileOrNull)(input.filePath);
|
|
2287
|
+
if (!body || !looksLikeSpec(body))
|
|
2288
|
+
return 0;
|
|
2289
|
+
const project = projectKey(input.filePath, input.cwd);
|
|
2290
|
+
const spec = {
|
|
2291
|
+
title: specTitle(body, input.filePath),
|
|
2292
|
+
body: body.slice(0, MAX_SPEC_CHARS),
|
|
2293
|
+
repoHint: input.cwd ? basename(input.cwd) : undefined
|
|
2294
|
+
};
|
|
2295
|
+
try {
|
|
2296
|
+
const res = await (o.fetchFn ?? fetch)(`${src.proxyUrl}/proxy/estimate`, {
|
|
2297
|
+
method: "POST",
|
|
2298
|
+
headers: { authorization: `Bearer ${src.token}`, "content-type": "application/json" },
|
|
2299
|
+
body: JSON.stringify({ spec, project }),
|
|
2300
|
+
signal: AbortSignal.timeout(ESTIMATE_TIMEOUT_MS)
|
|
2301
|
+
});
|
|
2302
|
+
if (!res.ok)
|
|
2303
|
+
return 0;
|
|
2304
|
+
const r = await res.json();
|
|
2305
|
+
const b = r.band;
|
|
2306
|
+
if (b && typeof b.lowUsd === "number" && typeof b.highUsd === "number") {
|
|
2307
|
+
const min = (n) => typeof n === "number" ? Math.round(n) : "?";
|
|
2308
|
+
log(`sleds: ${project} sized at $${b.lowUsd.toFixed(2)}–${b.highUsd.toFixed(2)} · ${min(b.lowMinutes)}–${min(b.highMinutes)} min`);
|
|
2309
|
+
}
|
|
2310
|
+
} catch {}
|
|
2311
|
+
return 0;
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
// src/commands/status.ts
|
|
2315
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2316
|
+
var checkHealth = async (url, fetchFn = fetch, timeoutMs = 1000) => {
|
|
2317
|
+
try {
|
|
2318
|
+
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2319
|
+
return res.ok ? "ok" : "unreachable";
|
|
2320
|
+
} catch {
|
|
2321
|
+
return "unreachable";
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
var fetchProxyStatus = async (url, fetchFn = fetch, timeoutMs = 1000) => {
|
|
2325
|
+
try {
|
|
2326
|
+
const res = await fetchFn(`${url}/status`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2327
|
+
if (!res.ok)
|
|
2328
|
+
return {};
|
|
2329
|
+
const body = await res.json();
|
|
2330
|
+
if (typeof body !== "object" || body === null)
|
|
2331
|
+
return {};
|
|
2332
|
+
const b = body;
|
|
2333
|
+
const out = {};
|
|
2334
|
+
if (typeof b.wallMode === "string")
|
|
2335
|
+
out.wallMode = b.wallMode;
|
|
2336
|
+
if (typeof b.version === "string")
|
|
2337
|
+
out.version = b.version;
|
|
2338
|
+
return out;
|
|
2339
|
+
} catch {
|
|
2340
|
+
return {};
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
var row2 = (label, value) => `${label.padEnd(12)}${value}`;
|
|
2344
|
+
var formatStatus = (s) => {
|
|
2345
|
+
const lines = [];
|
|
2346
|
+
const stale = s.mode === "on" && !s.pointed;
|
|
2347
|
+
lines.push(row2("Sleds:", `${s.mode}${stale ? " (settings.json no longer points at the proxy)" : ""}`));
|
|
2348
|
+
if (s.mode === "on") {
|
|
2349
|
+
if (s.sinceMs !== undefined) {
|
|
2350
|
+
lines.push(row2("Since:", `${formatLocal(s.sinceMs, s.tz)} (${formatDuration(s.nowMs - s.sinceMs)} ago)`));
|
|
2351
|
+
}
|
|
2352
|
+
if (s.untilResetMs !== undefined) {
|
|
2353
|
+
const passed = s.untilResetMs <= s.nowMs;
|
|
2354
|
+
lines.push(row2("Reverts:", passed ? `${formatLocal(s.untilResetMs, s.tz)} — reset passed. Run \`sleds off\`.` : `${formatLocal(s.untilResetMs, s.tz)} (in ${formatDuration(s.untilResetMs - s.nowMs)})`));
|
|
2355
|
+
}
|
|
2356
|
+
if (s.reason)
|
|
2357
|
+
lines.push(row2("Reason:", s.reason));
|
|
2358
|
+
}
|
|
2359
|
+
if (s.proxyUrl) {
|
|
2360
|
+
lines.push(row2("Proxy:", s.proxyUrl));
|
|
2361
|
+
if (s.tokenMasked)
|
|
2362
|
+
lines.push(row2("Token:", s.tokenMasked));
|
|
2363
|
+
if (s.health) {
|
|
2364
|
+
const extra = [];
|
|
2365
|
+
if (s.proxy?.wallMode)
|
|
2366
|
+
extra.push(`wall mode ${s.proxy.wallMode}`);
|
|
2367
|
+
if (s.proxy?.version)
|
|
2368
|
+
extra.push(`v${s.proxy.version}`);
|
|
2369
|
+
lines.push(row2("Health:", `${s.health}${extra.length ? ` (${extra.join(", ")})` : ""}`));
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (s.email)
|
|
2373
|
+
lines.push(row2("Seat:", s.email));
|
|
2374
|
+
if (s.mode === "off") {
|
|
2375
|
+
lines.push(s.pointed ? "Claude Code is pointed at the proxy by settings.json (set by `sleds init`)." : "Claude Code is on your seat. Hit a limit? `sleds on --until-reset +5h`.");
|
|
2376
|
+
}
|
|
2377
|
+
return lines;
|
|
2378
|
+
};
|
|
2379
|
+
var status = async (o) => {
|
|
2380
|
+
const log = o.log ?? console.log;
|
|
2381
|
+
const nowMs = (o.now ?? Date.now)();
|
|
2382
|
+
const cfg = readConfig(o.home);
|
|
2383
|
+
const settings = existsSync7(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
2384
|
+
const inSettings = readProxy(settings);
|
|
2385
|
+
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
2386
|
+
const token = inSettings?.token ?? cfg.token;
|
|
2387
|
+
if (!proxyUrl && !token && cfg.mode === undefined) {
|
|
2388
|
+
log(`SLEDS is not set up (${configPath(o.home)} and ${settingsPath(o.home)}).`);
|
|
2389
|
+
log("Run `sleds init` first.");
|
|
2390
|
+
return 1;
|
|
2391
|
+
}
|
|
2392
|
+
const info = {
|
|
2393
|
+
mode: cfg.mode ?? "off",
|
|
2394
|
+
pointed: inSettings !== null,
|
|
2395
|
+
nowMs,
|
|
2396
|
+
settingsFile: settingsPath(o.home),
|
|
2397
|
+
configFile: configPath(o.home)
|
|
2398
|
+
};
|
|
2399
|
+
if (proxyUrl)
|
|
2400
|
+
info.proxyUrl = proxyUrl;
|
|
2401
|
+
if (token)
|
|
2402
|
+
info.tokenMasked = maskToken(token);
|
|
2403
|
+
if (cfg.email)
|
|
2404
|
+
info.email = cfg.email;
|
|
2405
|
+
if (cfg.reason)
|
|
2406
|
+
info.reason = cfg.reason;
|
|
2407
|
+
if (o.tz)
|
|
2408
|
+
info.tz = o.tz;
|
|
2409
|
+
const since = cfg.since ? Date.parse(cfg.since) : Number.NaN;
|
|
2410
|
+
if (!Number.isNaN(since))
|
|
2411
|
+
info.sinceMs = since;
|
|
2412
|
+
const until = cfg.untilReset ? Date.parse(cfg.untilReset) : Number.NaN;
|
|
2413
|
+
if (!Number.isNaN(until))
|
|
2414
|
+
info.untilResetMs = until;
|
|
2415
|
+
if (proxyUrl) {
|
|
2416
|
+
const [health, proxy] = await Promise.all([
|
|
2417
|
+
checkHealth(proxyUrl, o.fetchFn, o.timeoutMs),
|
|
2418
|
+
fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
|
|
2419
|
+
]);
|
|
2420
|
+
info.health = health;
|
|
2421
|
+
info.proxy = proxy;
|
|
2422
|
+
}
|
|
2423
|
+
for (const line of formatStatus(info))
|
|
2424
|
+
log(line);
|
|
2425
|
+
return 0;
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
// src/commands/statusline.ts
|
|
2429
|
+
var statusline = async (o) => {
|
|
2430
|
+
const log = o.log ?? console.log;
|
|
2431
|
+
const now = (o.now ?? Date.now)();
|
|
2432
|
+
const input = o.stdin ?? await readStdin(STDIN_TIMEOUT_MS);
|
|
2433
|
+
const window = seatWindowFromStdin(input, now);
|
|
2434
|
+
let posts = [];
|
|
2435
|
+
try {
|
|
2436
|
+
if (window)
|
|
2437
|
+
posts = rememberSeat(o, window, now);
|
|
2438
|
+
} catch {}
|
|
2439
|
+
const seatFragment = formatSeatFragment(window);
|
|
2440
|
+
let line;
|
|
2441
|
+
try {
|
|
2442
|
+
const meter2 = await loadMeter({
|
|
2443
|
+
home: o.home,
|
|
2444
|
+
now,
|
|
2445
|
+
fetchFn: o.fetchFn,
|
|
2446
|
+
timeoutMs: o.timeoutMs
|
|
2447
|
+
});
|
|
2448
|
+
line = formatMeterLine(meter2, now, seatFragment);
|
|
2449
|
+
} catch {
|
|
2450
|
+
line = formatMeterLine(null, now, seatFragment);
|
|
2451
|
+
}
|
|
2452
|
+
log(line);
|
|
2453
|
+
await Promise.allSettled(posts);
|
|
2454
|
+
return 0;
|
|
2455
|
+
};
|
|
2456
|
+
var rememberSeat = (o, window, now) => {
|
|
2457
|
+
const prior = readSeatState(o.home);
|
|
2458
|
+
let state = { ...prior, window };
|
|
2459
|
+
const src = meterSource(o.home);
|
|
2460
|
+
const posts = [];
|
|
2461
|
+
if (src) {
|
|
2462
|
+
const opts = { ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS };
|
|
2463
|
+
if (shouldPostSeat(prior, window, now)) {
|
|
2464
|
+
state = markPosted(state, window, now);
|
|
2465
|
+
posts.push(postSeatWindow(opts, window));
|
|
2466
|
+
}
|
|
2467
|
+
if (seatWallDue(prior, window) && window.sevenDay) {
|
|
2468
|
+
state = { ...state, lastWallResetsAt: window.sevenDay.resetsAt };
|
|
2469
|
+
posts.push(postSeatWall(opts, {
|
|
2470
|
+
resetsAt: window.sevenDay.resetsAt,
|
|
2471
|
+
usedPct: window.sevenDay.usedPct,
|
|
2472
|
+
notification: "statusline"
|
|
2473
|
+
}));
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
writeSeatState(o.home, state);
|
|
2477
|
+
return posts;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
// src/commands/uninstall.ts
|
|
2481
|
+
import { existsSync as existsSync8, rmSync as rmSync3 } from "node:fs";
|
|
2482
|
+
var uninstall2 = (o) => {
|
|
2483
|
+
const log = o.log ?? console.log;
|
|
2484
|
+
const removed = [];
|
|
2485
|
+
if (existsSync8(settingsPath(o.home))) {
|
|
2486
|
+
let settings = readSettings(o.home);
|
|
2487
|
+
let changed = false;
|
|
2488
|
+
if (hasEitherKey(settings)) {
|
|
2489
|
+
settings = removeProxy(settings);
|
|
2490
|
+
changed = true;
|
|
2491
|
+
removed.push(`env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN from ${settingsPath(o.home)}`);
|
|
2492
|
+
}
|
|
2493
|
+
if (isSledsStatusLine(settings[STATUS_LINE_KEY])) {
|
|
2494
|
+
const cfg = readConfig(o.home);
|
|
2495
|
+
settings = removeStatusLine(settings, cfg.priorStatusLine);
|
|
2496
|
+
changed = true;
|
|
2497
|
+
removed.push(cfg.priorStatusLine === undefined ? `statusLine from ${settingsPath(o.home)}` : `statusLine from ${settingsPath(o.home)} (previous one restored)`);
|
|
2498
|
+
if (cfg.priorStatusLine !== undefined) {
|
|
2499
|
+
const { priorStatusLine: _restored, ...rest } = cfg;
|
|
2500
|
+
writeConfig(o.home, rest);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (hasSeatHook(settings)) {
|
|
2504
|
+
settings = removeSeatHook(settings);
|
|
2505
|
+
changed = true;
|
|
2506
|
+
removed.push(`hooks.Notification seat-event entry from ${settingsPath(o.home)}`);
|
|
2507
|
+
}
|
|
2508
|
+
if (hasSpecHook(settings)) {
|
|
2509
|
+
settings = removeSpecHook(settings);
|
|
2510
|
+
changed = true;
|
|
2511
|
+
removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(o.home)}`);
|
|
2512
|
+
}
|
|
2513
|
+
if (changed)
|
|
2514
|
+
writeSettings(o.home, settings);
|
|
2515
|
+
}
|
|
2516
|
+
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
2517
|
+
if (existsSync8(file)) {
|
|
2518
|
+
rmSync3(file, { force: true });
|
|
2519
|
+
removed.push(file);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const skill = skillDir(o.home);
|
|
2523
|
+
if (existsSync8(skill)) {
|
|
2524
|
+
rmSync3(skill, { recursive: true, force: true });
|
|
2525
|
+
removed.push(skill);
|
|
2526
|
+
}
|
|
2527
|
+
if (removed.length === 0) {
|
|
2528
|
+
log("Nothing to remove; SLEDS was not installed.");
|
|
2529
|
+
} else {
|
|
2530
|
+
log("Removed:");
|
|
2531
|
+
for (const r of removed)
|
|
2532
|
+
log(` ${r}`);
|
|
2533
|
+
}
|
|
2534
|
+
log(`Kept ${tokenPath(o.home)} (delete it yourself if you want the token gone).`);
|
|
2535
|
+
return 0;
|
|
2536
|
+
};
|
|
2537
|
+
var hasEitherKey = (settings) => {
|
|
2538
|
+
const env = settings.env;
|
|
2539
|
+
if (typeof env !== "object" || env === null)
|
|
2540
|
+
return false;
|
|
2541
|
+
return BASE_URL_KEY in env || AUTH_TOKEN_KEY in env;
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// src/schedule.ts
|
|
2545
|
+
import { spawnSync } from "node:child_process";
|
|
2546
|
+
var defaultExec = (cmd, args) => {
|
|
2547
|
+
const r = spawnSync(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
2548
|
+
if (r.error)
|
|
2549
|
+
return { ok: false, stderr: r.error.message };
|
|
2550
|
+
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
// src/index.ts
|
|
2554
|
+
var USAGE = `usage: sleds <command> [options]
|
|
2555
|
+
|
|
2556
|
+
commands:
|
|
2557
|
+
init Point Claude Code at a SLEDS proxy, install the /sled skill and the status line.
|
|
2558
|
+
--token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
|
|
2559
|
+
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
2560
|
+
--non-interactive never prompt; exit 2 if no token is available
|
|
2561
|
+
--signup no org? create an individual seat: your email + your own
|
|
2562
|
+
Anthropic API key (validated, stored encrypted, the only key it spends)
|
|
2563
|
+
on Hit a limit on your seat? Switch Claude Code to the org key through the proxy.
|
|
2564
|
+
--until-reset <when> revert at this time: +5h, +90m, or an ISO timestamp
|
|
2565
|
+
--reason "<text>" note shown by \`sleds status\`
|
|
2566
|
+
--no-schedule record the deadline but do not install a launchd/systemd revert
|
|
2567
|
+
off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
|
|
2568
|
+
uninstall Remove the proxy settings, skill and status line. Keeps ~/.sleds-token.
|
|
2569
|
+
status Show the switch state, deadline, proxy health and wall mode.
|
|
2570
|
+
meter Show the pipeline: built today, queued with its band, delivered overnight.
|
|
2571
|
+
bar Print the menu-bar meter in SwiftBar format (used by infra/swiftbar/sleds.1m.sh).
|
|
2572
|
+
mcp Run the MCP server on stdio: tools usage, estimate, overnight, jobs.
|
|
2573
|
+
Register with \`claude mcp add sleds -- sleds mcp\` (Cowork sees the same server).
|
|
2574
|
+
seat-sync Read the seat's subscription usage (weekly/5h limits, per-model, credits) from a
|
|
2575
|
+
claude.ai session and post it to the proxy. macOS.
|
|
2576
|
+
--browser <claude|chrome|brave|edge|chromium|\u2026> which cookie store (default claude)
|
|
2577
|
+
--profile <name> browser profile dir ("Profile 1") or its display name/email
|
|
2578
|
+
--org <uuid> claude.ai org to read (else the session's active org)
|
|
2579
|
+
--token <sled_\u2026> post as this seat (else the one \`init\` saved)
|
|
2580
|
+
--install / --uninstall a 10-min launchd timer for this source (one per source)
|
|
2581
|
+
statusline Print the one-line meter Claude Code shows (installed by init; 10s cache).
|
|
2582
|
+
hooks Register the Claude Code Notification hook for seat limits (init does this too).
|
|
2583
|
+
seat-event Claude Code hook: record a seat wall and say how to continue. Reads hook JSON on stdin.
|
|
2584
|
+
spec-event Claude Code PostToolUse hook: when a spec-shaped markdown file is written, size it
|
|
2585
|
+
(POST /proxy/estimate with the file path as the project). Silent unless it estimates.
|
|
2586
|
+
--help Show this message.
|
|
2587
|
+
|
|
2588
|
+
env:
|
|
2589
|
+
SLEDS_HOME where sleds keeps its state (default ~/.config/sleds)
|
|
2590
|
+
CLAUDE_CONFIG_DIR Claude Code's config dir (its own convention; default ~/.claude)
|
|
2591
|
+
Set both to run a second seat under one macOS login; \`on --until-reset\`
|
|
2592
|
+
then needs --no-schedule (the timer would revert the default seat).
|
|
2593
|
+
|
|
2594
|
+
Nothing changes until a wall.`;
|
|
2595
|
+
var parseArgs = (argv) => {
|
|
2596
|
+
const [cmd = "--help", ...rest] = argv;
|
|
2597
|
+
const flags = {};
|
|
2598
|
+
for (let i = 0;i < rest.length; i++) {
|
|
2599
|
+
const arg = rest[i];
|
|
2600
|
+
if (arg === undefined || !arg.startsWith("--"))
|
|
2601
|
+
continue;
|
|
2602
|
+
const eq = arg.indexOf("=");
|
|
2603
|
+
if (eq !== -1) {
|
|
2604
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const next = rest[i + 1];
|
|
2608
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
2609
|
+
flags[arg.slice(2)] = next;
|
|
2610
|
+
i++;
|
|
2611
|
+
} else {
|
|
2612
|
+
flags[arg.slice(2)] = true;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return { cmd, flags };
|
|
2616
|
+
};
|
|
2617
|
+
var stringFlag = (flags, name) => {
|
|
2618
|
+
const v = flags[name];
|
|
2619
|
+
return typeof v === "string" ? v : undefined;
|
|
2620
|
+
};
|
|
2621
|
+
var sledsArgv = () => {
|
|
2622
|
+
const script = process.argv[1];
|
|
2623
|
+
return script ? [process.execPath, script] : [process.execPath];
|
|
2624
|
+
};
|
|
2625
|
+
var shellQuote = (s) => /^[\w./-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
2626
|
+
var statusLineCommand = (argv) => [...argv, "statusline"].map(shellQuote).join(" ");
|
|
2627
|
+
var seatEventCommand = (argv) => [...argv, "seat-event"].map(shellQuote).join(" ");
|
|
2628
|
+
var specEventCommand = (argv) => [...argv, "spec-event"].map(shellQuote).join(" ");
|
|
2629
|
+
var main = async (argv) => {
|
|
2630
|
+
const { cmd, flags } = parseArgs(argv);
|
|
2631
|
+
const home = homedir();
|
|
2632
|
+
const scheduler = {
|
|
2633
|
+
platform: platform2(),
|
|
2634
|
+
home,
|
|
2635
|
+
sledsArgv: sledsArgv(),
|
|
2636
|
+
exec: defaultExec
|
|
2637
|
+
};
|
|
2638
|
+
switch (cmd) {
|
|
2639
|
+
case "init":
|
|
2640
|
+
return init({
|
|
2641
|
+
home,
|
|
2642
|
+
token: stringFlag(flags, "token"),
|
|
2643
|
+
url: stringFlag(flags, "url"),
|
|
2644
|
+
nonInteractive: flags["non-interactive"] === true,
|
|
2645
|
+
signup: flags.signup === true,
|
|
2646
|
+
prompt: promptStdin,
|
|
2647
|
+
statusLineCommand: statusLineCommand(sledsArgv()),
|
|
2648
|
+
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
2649
|
+
specEventCommand: specEventCommand(sledsArgv())
|
|
2650
|
+
});
|
|
2651
|
+
case "hooks":
|
|
2652
|
+
return hooks({
|
|
2653
|
+
home,
|
|
2654
|
+
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
2655
|
+
specEventCommand: specEventCommand(sledsArgv())
|
|
2656
|
+
});
|
|
2657
|
+
case "seat-event":
|
|
2658
|
+
return seatEvent({ home, platform: platform2(), exec: defaultExec });
|
|
2659
|
+
case "spec-event":
|
|
2660
|
+
return specEvent({ home });
|
|
2661
|
+
case "on":
|
|
2662
|
+
return on({
|
|
2663
|
+
home,
|
|
2664
|
+
untilReset: stringFlag(flags, "until-reset"),
|
|
2665
|
+
reason: stringFlag(flags, "reason"),
|
|
2666
|
+
url: stringFlag(flags, "url"),
|
|
2667
|
+
noSchedule: flags["no-schedule"] === true,
|
|
2668
|
+
scheduler
|
|
2669
|
+
});
|
|
2670
|
+
case "off":
|
|
2671
|
+
return off({ home, scheduler });
|
|
2672
|
+
case "uninstall":
|
|
2673
|
+
return uninstall2({ home });
|
|
2674
|
+
case "status":
|
|
2675
|
+
return status({ home });
|
|
2676
|
+
case "meter":
|
|
2677
|
+
return meter({ home });
|
|
2678
|
+
case "bar":
|
|
2679
|
+
return bar({ home });
|
|
2680
|
+
case "mcp":
|
|
2681
|
+
return mcp({ home });
|
|
2682
|
+
case "seat-sync":
|
|
2683
|
+
return seatSync({ home, argv: process.argv.slice(3) });
|
|
2684
|
+
case "statusline":
|
|
2685
|
+
return statusline({ home });
|
|
2686
|
+
case "help":
|
|
2687
|
+
case "--help":
|
|
2688
|
+
case "-h":
|
|
2689
|
+
console.log(USAGE);
|
|
2690
|
+
return 0;
|
|
2691
|
+
default:
|
|
2692
|
+
console.error(`unknown command: ${cmd}
|
|
2693
|
+
`);
|
|
2694
|
+
console.error(USAGE);
|
|
2695
|
+
return 1;
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
var isMain = () => {
|
|
2699
|
+
if (undefined)
|
|
2700
|
+
;
|
|
2701
|
+
const arg = process.argv[1];
|
|
2702
|
+
if (!arg)
|
|
2703
|
+
return false;
|
|
2704
|
+
try {
|
|
2705
|
+
return realpathSync(arg) === realpathSync(fileURLToPath(import.meta.url));
|
|
2706
|
+
} catch {
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
if (isMain()) {
|
|
2711
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
2712
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2713
|
+
process.exit(1);
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
export {
|
|
2717
|
+
parseArgs,
|
|
2718
|
+
seatEventCommand,
|
|
2719
|
+
specEventCommand,
|
|
2720
|
+
statusLineCommand
|
|
2721
|
+
};
|