claude-code-discord-presence 1.0.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/.env.example +21 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/assets/claude-code.png +0 -0
- package/assets/claude-liquid-dark.png +0 -0
- package/assets/claude-liquid-light.png +0 -0
- package/assets/claude-stats-dark.png +0 -0
- package/assets/claude-stats-light.png +0 -0
- package/assets/claude-usage-stats.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/assets/usage-stats.png +0 -0
- package/dist/appearance/theme-assets.js +13 -0
- package/dist/appearance/theme-watcher.js +188 -0
- package/dist/claude/app-liveness.js +82 -0
- package/dist/claude/cost.js +54 -0
- package/dist/claude/default-selection.js +268 -0
- package/dist/claude/desktop-focus.js +191 -0
- package/dist/claude/limits.js +90 -0
- package/dist/claude/monthly-usage.js +279 -0
- package/dist/claude/oauth-discovery.js +82 -0
- package/dist/claude/paths.js +13 -0
- package/dist/claude/plan-info.js +102 -0
- package/dist/claude/session-store.js +617 -0
- package/dist/claude/tool-labels.js +55 -0
- package/dist/claude/transcript.js +150 -0
- package/dist/claude/usage-poller.js +243 -0
- package/dist/cli.js +137 -0
- package/dist/config.js +96 -0
- package/dist/discord/presence-builder.js +295 -0
- package/dist/discord/rpc-client.js +224 -0
- package/dist/index.js +160 -0
- package/dist/remote/tunnel-manager.js +83 -0
- package/dist/server/http-server.js +89 -0
- package/dist/types.js +1 -0
- package/dist/util/autostart.js +73 -0
- package/dist/util/logger.js +74 -0
- package/dist/util/process-liveness.js +231 -0
- package/dist/util/process-scan-watcher.js +196 -0
- package/package.json +73 -0
- package/pricing.json +47 -0
- package/scripts/hook.mjs +32 -0
- package/scripts/install-autostart.mjs +50 -0
- package/scripts/install-remote.mjs +53 -0
- package/scripts/remove-autostart.mjs +35 -0
- package/scripts/remove-autostart.ps1 +7 -0
- package/scripts/run-hidden.vbs +7 -0
- package/scripts/run-service.ps1 +44 -0
- package/scripts/setup-autostart.ps1 +32 -0
- package/scripts/setup-claude.mjs +132 -0
- package/scripts/setup-remote.mjs +99 -0
- package/scripts/statusline.mjs +59 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { claudeConfigDir } from "./paths.js";
|
|
5
|
+
const EFFORT_LEVELS = new Set([
|
|
6
|
+
"minimal",
|
|
7
|
+
"low",
|
|
8
|
+
"medium",
|
|
9
|
+
"high",
|
|
10
|
+
"xhigh",
|
|
11
|
+
"max",
|
|
12
|
+
"ultra",
|
|
13
|
+
]);
|
|
14
|
+
function readVarint(data, offset, limit = data.length) {
|
|
15
|
+
let value = 0;
|
|
16
|
+
let shift = 0;
|
|
17
|
+
for (let i = offset; i < limit && shift <= 49; i += 1) {
|
|
18
|
+
const byte = data[i];
|
|
19
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
20
|
+
if ((byte & 0x80) === 0)
|
|
21
|
+
return { value, next: i + 1 };
|
|
22
|
+
shift += 7;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function decodeSnappy(data) {
|
|
27
|
+
const expected = readVarint(data, 0);
|
|
28
|
+
if (!expected || expected.value > 64 * 1024 * 1024)
|
|
29
|
+
return undefined;
|
|
30
|
+
const output = new Uint8Array(expected.value);
|
|
31
|
+
let inputAt = expected.next;
|
|
32
|
+
let outputAt = 0;
|
|
33
|
+
while (inputAt < data.length && outputAt < output.length) {
|
|
34
|
+
const tag = data[inputAt++];
|
|
35
|
+
const type = tag & 3;
|
|
36
|
+
if (type === 0) {
|
|
37
|
+
let length = tag >>> 2;
|
|
38
|
+
if (length < 60) {
|
|
39
|
+
length += 1;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const bytes = length - 59;
|
|
43
|
+
length = 0;
|
|
44
|
+
if (inputAt + bytes > data.length)
|
|
45
|
+
return undefined;
|
|
46
|
+
for (let i = 0; i < bytes; i += 1)
|
|
47
|
+
length += data[inputAt++] * 2 ** (8 * i);
|
|
48
|
+
length += 1;
|
|
49
|
+
}
|
|
50
|
+
if (inputAt + length > data.length || outputAt + length > output.length)
|
|
51
|
+
return undefined;
|
|
52
|
+
output.set(data.subarray(inputAt, inputAt + length), outputAt);
|
|
53
|
+
inputAt += length;
|
|
54
|
+
outputAt += length;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const length = type === 1 ? 4 + ((tag >>> 2) & 7) : 1 + (tag >>> 2);
|
|
58
|
+
let distance;
|
|
59
|
+
if (type === 1) {
|
|
60
|
+
if (inputAt >= data.length)
|
|
61
|
+
return undefined;
|
|
62
|
+
distance = ((tag & 0xe0) << 3) | data[inputAt++];
|
|
63
|
+
}
|
|
64
|
+
else if (type === 2) {
|
|
65
|
+
if (inputAt + 2 > data.length)
|
|
66
|
+
return undefined;
|
|
67
|
+
distance = data[inputAt] | (data[inputAt + 1] << 8);
|
|
68
|
+
inputAt += 2;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (inputAt + 4 > data.length)
|
|
72
|
+
return undefined;
|
|
73
|
+
distance =
|
|
74
|
+
(data[inputAt] |
|
|
75
|
+
(data[inputAt + 1] << 8) |
|
|
76
|
+
(data[inputAt + 2] << 16) |
|
|
77
|
+
(data[inputAt + 3] << 24)) >>> 0;
|
|
78
|
+
inputAt += 4;
|
|
79
|
+
}
|
|
80
|
+
if (distance <= 0 || distance > outputAt || outputAt + length > output.length)
|
|
81
|
+
return undefined;
|
|
82
|
+
for (let i = 0; i < length; i += 1)
|
|
83
|
+
output[outputAt + i] = output[outputAt + i - distance];
|
|
84
|
+
outputAt += length;
|
|
85
|
+
}
|
|
86
|
+
return outputAt === output.length ? output : undefined;
|
|
87
|
+
}
|
|
88
|
+
function blockHandle(data, offset, limit = data.length) {
|
|
89
|
+
const start = readVarint(data, offset, limit);
|
|
90
|
+
if (!start)
|
|
91
|
+
return undefined;
|
|
92
|
+
const size = readVarint(data, start.next, limit);
|
|
93
|
+
return size ? { offset: start.value, size: size.value, next: size.next } : undefined;
|
|
94
|
+
}
|
|
95
|
+
function readBlock(file, offset, size) {
|
|
96
|
+
if (offset < 0 || size < 0 || offset + size + 5 > file.length)
|
|
97
|
+
return undefined;
|
|
98
|
+
const body = file.subarray(offset, offset + size);
|
|
99
|
+
const compression = file[offset + size];
|
|
100
|
+
if (compression === 0)
|
|
101
|
+
return body;
|
|
102
|
+
return compression === 1 ? decodeSnappy(body) : undefined;
|
|
103
|
+
}
|
|
104
|
+
function blockEntries(block) {
|
|
105
|
+
if (block.length < 4)
|
|
106
|
+
return [];
|
|
107
|
+
const view = new DataView(block.buffer, block.byteOffset, block.byteLength);
|
|
108
|
+
const restarts = view.getUint32(block.length - 4, true);
|
|
109
|
+
const limit = block.length - 4 - restarts * 4;
|
|
110
|
+
if (limit < 0 || limit > block.length - 4)
|
|
111
|
+
return [];
|
|
112
|
+
const entries = [];
|
|
113
|
+
let previous = new Uint8Array();
|
|
114
|
+
let at = 0;
|
|
115
|
+
while (at < limit) {
|
|
116
|
+
const shared = readVarint(block, at, limit);
|
|
117
|
+
if (!shared)
|
|
118
|
+
break;
|
|
119
|
+
const added = readVarint(block, shared.next, limit);
|
|
120
|
+
if (!added)
|
|
121
|
+
break;
|
|
122
|
+
const valueLength = readVarint(block, added.next, limit);
|
|
123
|
+
if (!valueLength || shared.value > previous.length)
|
|
124
|
+
break;
|
|
125
|
+
const keyAt = valueLength.next;
|
|
126
|
+
const valueAt = keyAt + added.value;
|
|
127
|
+
const next = valueAt + valueLength.value;
|
|
128
|
+
if (next > limit)
|
|
129
|
+
break;
|
|
130
|
+
const key = new Uint8Array(shared.value + added.value);
|
|
131
|
+
key.set(previous.subarray(0, shared.value));
|
|
132
|
+
key.set(block.subarray(keyAt, valueAt), shared.value);
|
|
133
|
+
const value = block.slice(valueAt, next);
|
|
134
|
+
entries.push({ key, value });
|
|
135
|
+
previous = key;
|
|
136
|
+
at = next;
|
|
137
|
+
}
|
|
138
|
+
return entries;
|
|
139
|
+
}
|
|
140
|
+
function levelDbTableEntries(file) {
|
|
141
|
+
const footerAt = file.length - 48;
|
|
142
|
+
if (footerAt < 0)
|
|
143
|
+
return [];
|
|
144
|
+
const meta = blockHandle(file, footerAt, footerAt + 40);
|
|
145
|
+
const indexHandle = meta ? blockHandle(file, meta.next, footerAt + 40) : undefined;
|
|
146
|
+
if (!indexHandle)
|
|
147
|
+
return [];
|
|
148
|
+
const index = readBlock(file, indexHandle.offset, indexHandle.size);
|
|
149
|
+
if (!index)
|
|
150
|
+
return [];
|
|
151
|
+
const entries = [];
|
|
152
|
+
for (const indexEntry of blockEntries(index)) {
|
|
153
|
+
const handle = blockHandle(indexEntry.value, 0);
|
|
154
|
+
if (!handle)
|
|
155
|
+
continue;
|
|
156
|
+
const data = readBlock(file, handle.offset, handle.size);
|
|
157
|
+
if (data)
|
|
158
|
+
entries.push(...blockEntries(data));
|
|
159
|
+
}
|
|
160
|
+
return entries;
|
|
161
|
+
}
|
|
162
|
+
function defaultClaudeDataDir() {
|
|
163
|
+
if (process.platform === "win32") {
|
|
164
|
+
return join(process.env.APPDATA?.trim() || join(homedir(), "AppData", "Roaming"), "Claude");
|
|
165
|
+
}
|
|
166
|
+
if (process.platform === "darwin") {
|
|
167
|
+
return join(homedir(), "Library", "Application Support", "Claude");
|
|
168
|
+
}
|
|
169
|
+
return join(process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "Claude");
|
|
170
|
+
}
|
|
171
|
+
export function parseClaudeDefaultModel(data) {
|
|
172
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "latin1") : data;
|
|
173
|
+
const find = (text) => {
|
|
174
|
+
const matches = [...text.matchAll(/default-model[\s\S]{0,128}?(claude-[a-z0-9][a-z0-9._-]*)/gi)];
|
|
175
|
+
return matches.at(-1)?.[1]?.toLowerCase();
|
|
176
|
+
};
|
|
177
|
+
const raw = find(Buffer.from(bytes).toString("latin1"));
|
|
178
|
+
if (raw)
|
|
179
|
+
return raw;
|
|
180
|
+
let latest;
|
|
181
|
+
for (const entry of levelDbTableEntries(bytes)) {
|
|
182
|
+
if (!Buffer.from(entry.key).includes(Buffer.from("default-model")))
|
|
183
|
+
continue;
|
|
184
|
+
latest = find(`default-model ${Buffer.from(entry.value).toString("latin1")}`) ?? latest;
|
|
185
|
+
}
|
|
186
|
+
return latest;
|
|
187
|
+
}
|
|
188
|
+
export function parseClaudeDefaultEffort(text) {
|
|
189
|
+
try {
|
|
190
|
+
const effort = JSON.parse(text).effortLevel;
|
|
191
|
+
return typeof effort === "string" && EFFORT_LEVELS.has(effort)
|
|
192
|
+
? effort
|
|
193
|
+
: undefined;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function sameSelection(a, b) {
|
|
200
|
+
return a?.model === b.model && a?.effort === b.effort;
|
|
201
|
+
}
|
|
202
|
+
export class ClaudeDefaultSelectionWatcher {
|
|
203
|
+
onChange;
|
|
204
|
+
settingsPath;
|
|
205
|
+
levelDbDir;
|
|
206
|
+
pollIntervalMs;
|
|
207
|
+
timer;
|
|
208
|
+
current;
|
|
209
|
+
constructor(options, onChange) {
|
|
210
|
+
this.onChange = onChange;
|
|
211
|
+
const dataDir = defaultClaudeDataDir();
|
|
212
|
+
this.settingsPath = options.settingsPath || join(claudeConfigDir(), "settings.json");
|
|
213
|
+
this.levelDbDir = options.levelDbDir || join(dataDir, "Local Storage", "leveldb");
|
|
214
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 2_000;
|
|
215
|
+
}
|
|
216
|
+
start() {
|
|
217
|
+
if (this.timer)
|
|
218
|
+
return;
|
|
219
|
+
this.poll();
|
|
220
|
+
this.timer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
221
|
+
}
|
|
222
|
+
stop() {
|
|
223
|
+
if (!this.timer)
|
|
224
|
+
return;
|
|
225
|
+
clearInterval(this.timer);
|
|
226
|
+
this.timer = undefined;
|
|
227
|
+
}
|
|
228
|
+
poll() {
|
|
229
|
+
const selection = {
|
|
230
|
+
model: this.readModel() ?? this.current?.model,
|
|
231
|
+
effort: this.readEffort(),
|
|
232
|
+
};
|
|
233
|
+
if (sameSelection(this.current, selection))
|
|
234
|
+
return;
|
|
235
|
+
this.current = selection;
|
|
236
|
+
this.onChange(selection);
|
|
237
|
+
}
|
|
238
|
+
readEffort() {
|
|
239
|
+
try {
|
|
240
|
+
return parseClaudeDefaultEffort(readFileSync(this.settingsPath, "utf8"));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
readModel() {
|
|
247
|
+
if (!existsSync(this.levelDbDir))
|
|
248
|
+
return undefined;
|
|
249
|
+
try {
|
|
250
|
+
const candidates = readdirSync(this.levelDbDir)
|
|
251
|
+
.filter((name) => /\.(?:ldb|log)$/i.test(name))
|
|
252
|
+
.map((name) => {
|
|
253
|
+
const path = join(this.levelDbDir, name);
|
|
254
|
+
return { path, modifiedAt: statSync(path).mtimeMs };
|
|
255
|
+
})
|
|
256
|
+
.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
257
|
+
for (const candidate of candidates) {
|
|
258
|
+
const model = parseClaudeDefaultModel(readFileSync(candidate.path));
|
|
259
|
+
if (model)
|
|
260
|
+
return model;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createLogger } from "../util/logger.js";
|
|
6
|
+
const log = createLogger("desktop-focus");
|
|
7
|
+
const POLL_MS = 2_000;
|
|
8
|
+
const RESCAN_DEBOUNCE_MS = 150;
|
|
9
|
+
const MAX_DEPTH = 4;
|
|
10
|
+
export function sameDesktopFocus(a, b) {
|
|
11
|
+
return Boolean(a &&
|
|
12
|
+
a.cliSessionId === b.cliSessionId &&
|
|
13
|
+
a.focusedAt === b.focusedAt &&
|
|
14
|
+
a.model === b.model &&
|
|
15
|
+
a.effort === b.effort &&
|
|
16
|
+
a.failed === b.failed);
|
|
17
|
+
}
|
|
18
|
+
export function defaultDesktopSessionsDir() {
|
|
19
|
+
if (process.platform === "win32") {
|
|
20
|
+
const appData = process.env.APPDATA;
|
|
21
|
+
return appData ? join(appData, "Claude", "claude-code-sessions") : undefined;
|
|
22
|
+
}
|
|
23
|
+
if (process.platform === "darwin") {
|
|
24
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude-code-sessions");
|
|
25
|
+
}
|
|
26
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
27
|
+
return join(base, "Claude", "claude-code-sessions");
|
|
28
|
+
}
|
|
29
|
+
export function parseDesktopSession(text) {
|
|
30
|
+
let obj;
|
|
31
|
+
try {
|
|
32
|
+
obj = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (obj === null || typeof obj !== "object" || obj.isArchived === true)
|
|
38
|
+
return undefined;
|
|
39
|
+
const cliSessionId = obj.cliSessionId;
|
|
40
|
+
const focusedAt = obj.lastFocusedAt;
|
|
41
|
+
if (typeof cliSessionId !== "string" || cliSessionId === "")
|
|
42
|
+
return undefined;
|
|
43
|
+
if (typeof focusedAt !== "number" || !Number.isFinite(focusedAt) || focusedAt <= 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const focus = { cliSessionId, focusedAt };
|
|
46
|
+
if (typeof obj.lastActivityAt === "number" && Number.isFinite(obj.lastActivityAt) && obj.lastActivityAt > 0) {
|
|
47
|
+
focus.lastActivityAt = obj.lastActivityAt;
|
|
48
|
+
}
|
|
49
|
+
if (typeof obj.model === "string" && obj.model !== "")
|
|
50
|
+
focus.model = obj.model;
|
|
51
|
+
if (typeof obj.effort === "string" && obj.effort !== "")
|
|
52
|
+
focus.effort = obj.effort;
|
|
53
|
+
if ((typeof obj.error === "string" && obj.error.trim() !== "") ||
|
|
54
|
+
(obj.error !== undefined && obj.error !== null && obj.error !== false && typeof obj.error !== "string")) {
|
|
55
|
+
focus.failed = true;
|
|
56
|
+
}
|
|
57
|
+
return focus;
|
|
58
|
+
}
|
|
59
|
+
export class DesktopFocusWatcher {
|
|
60
|
+
dir;
|
|
61
|
+
onFocus;
|
|
62
|
+
watcher;
|
|
63
|
+
pollTimer;
|
|
64
|
+
rescanTimer;
|
|
65
|
+
scanning = false;
|
|
66
|
+
mtimes = new Map();
|
|
67
|
+
byFile = new Map();
|
|
68
|
+
lastReported;
|
|
69
|
+
stopped = false;
|
|
70
|
+
constructor(dir, onFocus) {
|
|
71
|
+
this.dir = dir;
|
|
72
|
+
this.onFocus = onFocus;
|
|
73
|
+
}
|
|
74
|
+
async start() {
|
|
75
|
+
await this.scan();
|
|
76
|
+
try {
|
|
77
|
+
this.watcher = watch(this.dir, { recursive: true }, () => this.scheduleScan());
|
|
78
|
+
this.watcher.on("error", (err) => log.warn(`watch error: ${err.message}`));
|
|
79
|
+
log.info(`watching ${this.dir}`);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
log.warn(`fs.watch unavailable for ${this.dir}: ${err.message}; relying on poll`);
|
|
83
|
+
}
|
|
84
|
+
this.pollTimer = setInterval(() => void this.scan(), POLL_MS);
|
|
85
|
+
}
|
|
86
|
+
stop() {
|
|
87
|
+
this.stopped = true;
|
|
88
|
+
if (this.watcher)
|
|
89
|
+
this.watcher.close();
|
|
90
|
+
if (this.pollTimer)
|
|
91
|
+
clearInterval(this.pollTimer);
|
|
92
|
+
if (this.rescanTimer)
|
|
93
|
+
clearTimeout(this.rescanTimer);
|
|
94
|
+
}
|
|
95
|
+
scheduleScan() {
|
|
96
|
+
if (this.stopped || this.rescanTimer)
|
|
97
|
+
return;
|
|
98
|
+
this.rescanTimer = setTimeout(() => {
|
|
99
|
+
this.rescanTimer = undefined;
|
|
100
|
+
void this.scan();
|
|
101
|
+
}, RESCAN_DEBOUNCE_MS);
|
|
102
|
+
}
|
|
103
|
+
async scan() {
|
|
104
|
+
if (this.stopped || this.scanning)
|
|
105
|
+
return;
|
|
106
|
+
this.scanning = true;
|
|
107
|
+
try {
|
|
108
|
+
await this.scanLocked();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
log.debug(`scan failed: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.scanning = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async listSessionFiles() {
|
|
118
|
+
const out = [];
|
|
119
|
+
const walk = async (dir, depth) => {
|
|
120
|
+
let entries;
|
|
121
|
+
try {
|
|
122
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const full = join(dir, entry.name);
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
if (depth < MAX_DEPTH)
|
|
131
|
+
await walk(full, depth + 1);
|
|
132
|
+
}
|
|
133
|
+
else if (entry.name.startsWith("local_") && entry.name.endsWith(".json")) {
|
|
134
|
+
out.push(full);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
await walk(this.dir, 0);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
async scanLocked() {
|
|
142
|
+
const files = await this.listSessionFiles();
|
|
143
|
+
const seen = new Set(files);
|
|
144
|
+
for (const file of this.mtimes.keys()) {
|
|
145
|
+
if (!seen.has(file)) {
|
|
146
|
+
this.mtimes.delete(file);
|
|
147
|
+
this.byFile.delete(file);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
let mtime;
|
|
152
|
+
try {
|
|
153
|
+
mtime = (await stat(file)).mtimeMs;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (this.mtimes.get(file) === mtime)
|
|
159
|
+
continue;
|
|
160
|
+
let text;
|
|
161
|
+
try {
|
|
162
|
+
text = await readFile(file, "utf8");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const parsed = parseDesktopSession(text);
|
|
168
|
+
if (!parsed) {
|
|
169
|
+
this.mtimes.delete(file);
|
|
170
|
+
this.byFile.delete(file);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
parsed.updatedAt = mtime;
|
|
174
|
+
this.mtimes.set(file, mtime);
|
|
175
|
+
this.byFile.set(file, parsed);
|
|
176
|
+
}
|
|
177
|
+
let best;
|
|
178
|
+
for (const focus of this.byFile.values()) {
|
|
179
|
+
if (!best || focus.focusedAt > best.focusedAt)
|
|
180
|
+
best = focus;
|
|
181
|
+
}
|
|
182
|
+
if (!best)
|
|
183
|
+
return;
|
|
184
|
+
if (sameDesktopFocus(this.lastReported, best))
|
|
185
|
+
return;
|
|
186
|
+
this.lastReported = { ...best };
|
|
187
|
+
log.debug(`focused session=${best.cliSessionId.slice(0, 8)} at=${best.focusedAt} ` +
|
|
188
|
+
`model=${best.model ?? "-"} effort=${best.effort ?? "-"} failed=${best.failed ? "yes" : "no"}`);
|
|
189
|
+
this.onFocus(best);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function epochSecondsToMs(value) {
|
|
2
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
3
|
+
return undefined;
|
|
4
|
+
return value < 1e12 ? Math.round(value * 1000) : Math.round(value);
|
|
5
|
+
}
|
|
6
|
+
function isoToMs(value) {
|
|
7
|
+
if (typeof value !== "string")
|
|
8
|
+
return undefined;
|
|
9
|
+
const ms = Date.parse(value);
|
|
10
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
11
|
+
}
|
|
12
|
+
function percentage(value) {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100
|
|
14
|
+
? value
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
export function limitsFromStatusline(rateLimits, at) {
|
|
18
|
+
if (!rateLimits)
|
|
19
|
+
return undefined;
|
|
20
|
+
const limits = { updatedAt: at };
|
|
21
|
+
const five = rateLimits.five_hour;
|
|
22
|
+
const seven = rateLimits.seven_day;
|
|
23
|
+
const fiveUsed = percentage(five?.used_percentage);
|
|
24
|
+
const sevenUsed = percentage(seven?.used_percentage);
|
|
25
|
+
if (fiveUsed !== undefined) {
|
|
26
|
+
limits.fiveHour = { usedPercentage: fiveUsed, resetsAt: epochSecondsToMs(five?.resets_at) };
|
|
27
|
+
}
|
|
28
|
+
if (sevenUsed !== undefined) {
|
|
29
|
+
limits.sevenDay = { usedPercentage: sevenUsed, resetsAt: epochSecondsToMs(seven?.resets_at) };
|
|
30
|
+
}
|
|
31
|
+
if (!limits.fiveHour && !limits.sevenDay)
|
|
32
|
+
return undefined;
|
|
33
|
+
return limits;
|
|
34
|
+
}
|
|
35
|
+
export function limitsFromUsage(usage, at) {
|
|
36
|
+
const limits = { updatedAt: at };
|
|
37
|
+
const fiveUsed = percentage(usage.five_hour?.utilization);
|
|
38
|
+
const sevenUsed = percentage(usage.seven_day?.utilization);
|
|
39
|
+
if (fiveUsed !== undefined) {
|
|
40
|
+
limits.fiveHour = { usedPercentage: fiveUsed, resetsAt: isoToMs(usage.five_hour?.resets_at) };
|
|
41
|
+
}
|
|
42
|
+
if (sevenUsed !== undefined) {
|
|
43
|
+
limits.sevenDay = { usedPercentage: sevenUsed, resetsAt: isoToMs(usage.seven_day?.resets_at) };
|
|
44
|
+
}
|
|
45
|
+
const scoped = [];
|
|
46
|
+
for (const entry of usage.limits ?? []) {
|
|
47
|
+
if (entry.kind !== "weekly_scoped")
|
|
48
|
+
continue;
|
|
49
|
+
const label = entry.scope?.model?.display_name;
|
|
50
|
+
const used = percentage(entry.percent);
|
|
51
|
+
if (typeof label === "string" && label !== "" && used !== undefined) {
|
|
52
|
+
scoped.push({ label, usedPercentage: used });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (scoped.length > 0)
|
|
56
|
+
limits.sevenDayScoped = scoped;
|
|
57
|
+
if (!limits.fiveHour && !limits.sevenDay && !limits.sevenDayScoped)
|
|
58
|
+
return undefined;
|
|
59
|
+
return limits;
|
|
60
|
+
}
|
|
61
|
+
function pickWindow(a, aAt, b, bAt) {
|
|
62
|
+
if (a && b)
|
|
63
|
+
return aAt >= bAt ? a : b;
|
|
64
|
+
return a ?? b;
|
|
65
|
+
}
|
|
66
|
+
export function mergeLimits(a, b, preferB = false) {
|
|
67
|
+
if (!a)
|
|
68
|
+
return b;
|
|
69
|
+
if (!b)
|
|
70
|
+
return a;
|
|
71
|
+
const fiveHour = preferB
|
|
72
|
+
? b.fiveHour ?? a.fiveHour
|
|
73
|
+
: pickWindow(a.fiveHour, a.updatedAt, b.fiveHour, b.updatedAt);
|
|
74
|
+
const sevenDay = preferB
|
|
75
|
+
? b.sevenDay ?? a.sevenDay
|
|
76
|
+
: pickWindow(a.sevenDay, a.updatedAt, b.sevenDay, b.updatedAt);
|
|
77
|
+
const fresher = a.updatedAt >= b.updatedAt ? a : b;
|
|
78
|
+
const older = a.updatedAt >= b.updatedAt ? b : a;
|
|
79
|
+
const scoped = preferB
|
|
80
|
+
? b.sevenDayScoped ?? a.sevenDayScoped
|
|
81
|
+
: fresher.sevenDayScoped ?? older.sevenDayScoped;
|
|
82
|
+
const merged = { updatedAt: Math.max(a.updatedAt, b.updatedAt) };
|
|
83
|
+
if (fiveHour)
|
|
84
|
+
merged.fiveHour = fiveHour;
|
|
85
|
+
if (sevenDay)
|
|
86
|
+
merged.sevenDay = sevenDay;
|
|
87
|
+
if (scoped)
|
|
88
|
+
merged.sevenDayScoped = scoped;
|
|
89
|
+
return merged;
|
|
90
|
+
}
|