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.
Files changed (51) hide show
  1. package/.env.example +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/assets/claude-code.png +0 -0
  5. package/assets/claude-liquid-dark.png +0 -0
  6. package/assets/claude-liquid-light.png +0 -0
  7. package/assets/claude-stats-dark.png +0 -0
  8. package/assets/claude-stats-light.png +0 -0
  9. package/assets/claude-usage-stats.png +0 -0
  10. package/assets/social-preview.jpg +0 -0
  11. package/assets/usage-stats.png +0 -0
  12. package/dist/appearance/theme-assets.js +13 -0
  13. package/dist/appearance/theme-watcher.js +188 -0
  14. package/dist/claude/app-liveness.js +82 -0
  15. package/dist/claude/cost.js +54 -0
  16. package/dist/claude/default-selection.js +268 -0
  17. package/dist/claude/desktop-focus.js +191 -0
  18. package/dist/claude/limits.js +90 -0
  19. package/dist/claude/monthly-usage.js +279 -0
  20. package/dist/claude/oauth-discovery.js +82 -0
  21. package/dist/claude/paths.js +13 -0
  22. package/dist/claude/plan-info.js +102 -0
  23. package/dist/claude/session-store.js +617 -0
  24. package/dist/claude/tool-labels.js +55 -0
  25. package/dist/claude/transcript.js +150 -0
  26. package/dist/claude/usage-poller.js +243 -0
  27. package/dist/cli.js +137 -0
  28. package/dist/config.js +96 -0
  29. package/dist/discord/presence-builder.js +295 -0
  30. package/dist/discord/rpc-client.js +224 -0
  31. package/dist/index.js +160 -0
  32. package/dist/remote/tunnel-manager.js +83 -0
  33. package/dist/server/http-server.js +89 -0
  34. package/dist/types.js +1 -0
  35. package/dist/util/autostart.js +73 -0
  36. package/dist/util/logger.js +74 -0
  37. package/dist/util/process-liveness.js +231 -0
  38. package/dist/util/process-scan-watcher.js +196 -0
  39. package/package.json +73 -0
  40. package/pricing.json +47 -0
  41. package/scripts/hook.mjs +32 -0
  42. package/scripts/install-autostart.mjs +50 -0
  43. package/scripts/install-remote.mjs +53 -0
  44. package/scripts/remove-autostart.mjs +35 -0
  45. package/scripts/remove-autostart.ps1 +7 -0
  46. package/scripts/run-hidden.vbs +7 -0
  47. package/scripts/run-service.ps1 +44 -0
  48. package/scripts/setup-autostart.ps1 +32 -0
  49. package/scripts/setup-claude.mjs +132 -0
  50. package/scripts/setup-remote.mjs +99 -0
  51. package/scripts/statusline.mjs +59 -0
@@ -0,0 +1,279 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { createLogger } from "../util/logger.js";
5
+ import { costForUsageByModel } from "./cost.js";
6
+ const log = createLogger("claude-monthly");
7
+ const DEFAULT_POLL_MS = 60_000;
8
+ function object(value) {
9
+ return value && typeof value === "object" ? value : undefined;
10
+ }
11
+ function count(value) {
12
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
13
+ }
14
+ function timestamp(value) {
15
+ if (typeof value === "string" && Number.isFinite(Date.parse(value)))
16
+ return value;
17
+ return undefined;
18
+ }
19
+ function usageTotal(usage) {
20
+ return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
21
+ }
22
+ function parseLine(line) {
23
+ if (line.charCodeAt(0) !== 123 || !line.includes('"usage"'))
24
+ return undefined;
25
+ let record;
26
+ try {
27
+ record = JSON.parse(line);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ const wrapped = object(object(record.data)?.message);
33
+ const entry = wrapped && object(wrapped.message) ? wrapped : record;
34
+ const message = object(entry.message);
35
+ const raw = object(message?.usage);
36
+ const at = timestamp(entry.timestamp);
37
+ const model = message?.model;
38
+ if (!message || !raw || !at || typeof model !== "string" || model.trim() === "") {
39
+ return undefined;
40
+ }
41
+ const cacheCreation = object(raw.cache_creation);
42
+ const oneHour = count(cacheCreation?.ephemeral_1h_input_tokens);
43
+ const fiveMinute = count(cacheCreation?.ephemeral_5m_input_tokens);
44
+ const usage = {
45
+ input: count(raw.input_tokens),
46
+ output: count(raw.output_tokens),
47
+ cacheRead: count(raw.cache_read_input_tokens),
48
+ cacheWrite: cacheCreation ? oneHour + fiveMinute : count(raw.cache_creation_input_tokens),
49
+ };
50
+ if (oneHour > 0)
51
+ usage.cacheWriteOneHour = Math.min(usage.cacheWrite, oneHour);
52
+ if (usageTotal(usage) === 0)
53
+ return undefined;
54
+ return {
55
+ timestamp: at,
56
+ model: raw.speed === "fast" ? `${model}-fast` : model,
57
+ messageId: typeof message.id === "string" && message.id !== "" ? message.id : undefined,
58
+ requestId: typeof entry.requestId === "string" && entry.requestId !== "" ? entry.requestId : undefined,
59
+ sidechain: entry.isSidechain === true,
60
+ usage,
61
+ };
62
+ }
63
+ async function collectJsonl(dir) {
64
+ const files = [];
65
+ let entries;
66
+ try {
67
+ entries = await readdir(dir, { withFileTypes: true });
68
+ }
69
+ catch {
70
+ return files;
71
+ }
72
+ for (const entry of entries) {
73
+ const path = join(dir, entry.name);
74
+ if (entry.isDirectory())
75
+ files.push(...(await collectJsonl(path)));
76
+ else if (entry.isFile() && entry.name.endsWith(".jsonl"))
77
+ files.push(path);
78
+ }
79
+ return files;
80
+ }
81
+ function expandHome(path) {
82
+ if (path === "~")
83
+ return homedir();
84
+ if (path.startsWith("~/") || path.startsWith("~\\"))
85
+ return join(homedir(), path.slice(2));
86
+ return path;
87
+ }
88
+ export function claudeConfigDirs(env = process.env) {
89
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
90
+ if (configured) {
91
+ return configured
92
+ .split(",")
93
+ .map((path) => expandHome(path.trim()))
94
+ .filter((path) => path !== "")
95
+ .map((path) => (basename(path).toLowerCase() === "projects" ? dirname(path) : path));
96
+ }
97
+ const xdg = env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
98
+ return [join(xdg, "claude"), join(homedir(), ".claude")];
99
+ }
100
+ function dedupe(events) {
101
+ const deduped = [];
102
+ const exact = new Map();
103
+ const byMessage = new Map();
104
+ const anonymous = [];
105
+ for (const event of events) {
106
+ if (!event.messageId) {
107
+ anonymous.push(event);
108
+ continue;
109
+ }
110
+ const exactKey = `${event.messageId}\u0000${event.requestId ?? ""}`;
111
+ let index = exact.get(exactKey);
112
+ if (index !== undefined && deduped[index]?.requestId !== event.requestId)
113
+ index = undefined;
114
+ if (index === undefined) {
115
+ index = byMessage.get(event.messageId)?.find((candidate) => event.sidechain || deduped[candidate]?.sidechain === true);
116
+ }
117
+ if (index === undefined) {
118
+ const next = deduped.length;
119
+ deduped.push(event);
120
+ exact.set(exactKey, next);
121
+ const indexes = byMessage.get(event.messageId) ?? [];
122
+ indexes.push(next);
123
+ byMessage.set(event.messageId, indexes);
124
+ continue;
125
+ }
126
+ const existing = deduped[index];
127
+ const nextTotal = usageTotal(event.usage);
128
+ const currentTotal = usageTotal(existing.usage);
129
+ if ((existing.sidechain !== event.sidechain && !event.sidechain) ||
130
+ (existing.sidechain === event.sidechain && nextTotal > currentTotal) ||
131
+ (existing.sidechain === event.sidechain &&
132
+ nextTotal === currentTotal &&
133
+ event.requestId !== undefined &&
134
+ existing.requestId === undefined)) {
135
+ deduped[index] = event;
136
+ exact.set(exactKey, index);
137
+ }
138
+ }
139
+ return [...deduped, ...anonymous];
140
+ }
141
+ export class ClaudeUsageFileCache {
142
+ entries = new Map();
143
+ }
144
+ function parseTranscript(text) {
145
+ const events = [];
146
+ for (const line of text.split(/\r?\n/)) {
147
+ const event = parseLine(line);
148
+ if (event)
149
+ events.push(event);
150
+ }
151
+ return events;
152
+ }
153
+ export async function readClaudeMonthlyUsageRaw(configDirs = claudeConfigDirs(), now = new Date(), cache) {
154
+ const events = [];
155
+ const liveFiles = new Set();
156
+ for (const configDir of configDirs) {
157
+ for (const file of await collectJsonl(join(configDir, "projects"))) {
158
+ liveFiles.add(file);
159
+ try {
160
+ let fileEvents;
161
+ if (cache) {
162
+ const info = await stat(file);
163
+ const entry = cache.entries.get(file);
164
+ if (entry && entry.size === info.size && entry.mtimeMs === info.mtimeMs) {
165
+ fileEvents = entry.events;
166
+ }
167
+ else {
168
+ fileEvents = parseTranscript(await readFile(file, "utf8"));
169
+ cache.entries.set(file, { size: info.size, mtimeMs: info.mtimeMs, events: fileEvents });
170
+ }
171
+ }
172
+ else {
173
+ fileEvents = parseTranscript(await readFile(file, "utf8"));
174
+ }
175
+ for (const event of fileEvents)
176
+ events.push(event);
177
+ }
178
+ catch {
179
+ // A transcript can be rotated while the scan is running; the next poll retries it.
180
+ }
181
+ }
182
+ }
183
+ if (cache) {
184
+ for (const key of cache.entries.keys()) {
185
+ if (!liveFiles.has(key))
186
+ cache.entries.delete(key);
187
+ }
188
+ }
189
+ const unique = dedupe(events);
190
+ const aggregate = (selected) => {
191
+ const usageByModel = {};
192
+ let totalTokens = 0;
193
+ for (const event of selected) {
194
+ const bucket = (usageByModel[event.model] ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
195
+ bucket.input += event.usage.input;
196
+ bucket.output += event.usage.output;
197
+ bucket.cacheRead += event.usage.cacheRead;
198
+ bucket.cacheWrite += event.usage.cacheWrite;
199
+ if (event.usage.cacheWriteOneHour) {
200
+ bucket.cacheWriteOneHour = (bucket.cacheWriteOneHour ?? 0) + event.usage.cacheWriteOneHour;
201
+ }
202
+ totalTokens += usageTotal(event.usage);
203
+ }
204
+ return { totalTokens, usageByModel };
205
+ };
206
+ const startDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
207
+ const startWeek = startDay - 6 * 24 * 60 * 60 * 1000;
208
+ const startMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
209
+ const beforeTomorrow = startDay + 24 * 60 * 60 * 1000;
210
+ const within = (start) => unique.filter((event) => {
211
+ const at = Date.parse(event.timestamp);
212
+ return at >= start && at < beforeTomorrow;
213
+ });
214
+ return {
215
+ ...aggregate(within(startMonth)),
216
+ day: aggregate(within(startDay)),
217
+ week: aggregate(within(startWeek)),
218
+ allTime: aggregate(unique),
219
+ };
220
+ }
221
+ export function claudeMonthlyUsage(raw) {
222
+ const summary = (period) => ({
223
+ totalTokens: period.totalTokens,
224
+ costUsd: costForUsageByModel(period.usageByModel).total,
225
+ });
226
+ return {
227
+ ...summary(raw),
228
+ ...(raw.day ? { day: summary(raw.day) } : {}),
229
+ ...(raw.week ? { week: summary(raw.week) } : {}),
230
+ ...(raw.allTime ? { allTime: summary(raw.allTime) } : {}),
231
+ };
232
+ }
233
+ function same(a, b) {
234
+ return JSON.stringify(a) === JSON.stringify(b);
235
+ }
236
+ export class ClaudeMonthlyUsageWatcher {
237
+ onUpdate;
238
+ intervalMs;
239
+ timer;
240
+ polling = false;
241
+ stopped = false;
242
+ last;
243
+ cache = new ClaudeUsageFileCache();
244
+ constructor(onUpdate, intervalMs = DEFAULT_POLL_MS) {
245
+ this.onUpdate = onUpdate;
246
+ this.intervalMs = intervalMs;
247
+ }
248
+ start() {
249
+ if (this.timer)
250
+ return;
251
+ this.stopped = false;
252
+ void this.poll();
253
+ this.timer = setInterval(() => void this.poll(), this.intervalMs);
254
+ }
255
+ stop() {
256
+ this.stopped = true;
257
+ if (this.timer)
258
+ clearInterval(this.timer);
259
+ this.timer = undefined;
260
+ }
261
+ async poll() {
262
+ if (this.stopped || this.polling)
263
+ return;
264
+ this.polling = true;
265
+ try {
266
+ const usage = claudeMonthlyUsage(await readClaudeMonthlyUsageRaw(undefined, undefined, this.cache));
267
+ if (!same(this.last, usage)) {
268
+ this.last = usage;
269
+ this.onUpdate(usage);
270
+ }
271
+ }
272
+ catch (error) {
273
+ log.warn(`monthly usage scan failed: ${error.message}`);
274
+ }
275
+ finally {
276
+ this.polling = false;
277
+ }
278
+ }
279
+ }
@@ -0,0 +1,82 @@
1
+ import { execFile } from "node:child_process";
2
+ import { open } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { createLogger } from "../util/logger.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const log = createLogger("oauth-discovery");
7
+ export const FALLBACK_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
8
+ export const FALLBACK_TOKEN_URLS = [
9
+ "https://platform.claude.com/v1/oauth/token",
10
+ "https://console.anthropic.com/v1/oauth/token",
11
+ ];
12
+ const CLIENT_RE = /MANUAL_REDIRECT_URL:"https:\/\/[a-z0-9.\-]+\/oauth\/code\/callback",CLIENT_ID:"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/g;
13
+ const TOKEN_URL_RE = /https:\/\/[a-z0-9.\-]+\/v1\/oauth\/token/g;
14
+ const CHUNK_BYTES = 8 * 1024 * 1024;
15
+ const OVERLAP_BYTES = 1024;
16
+ function ownHost(url) {
17
+ return url.includes("claude.com") || url.includes("anthropic.com");
18
+ }
19
+ function push(list, value) {
20
+ if (!list.includes(value))
21
+ list.push(value);
22
+ }
23
+ async function findClaudeExecutable() {
24
+ try {
25
+ const command = process.platform === "win32" ? "where.exe" : "which";
26
+ const { stdout } = await execFileAsync(command, ["claude"], {
27
+ encoding: "utf8",
28
+ windowsHide: true,
29
+ timeout: 10_000,
30
+ });
31
+ return stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ export async function discoverOauthConfig() {
38
+ const clientIds = [];
39
+ const tokenUrls = [];
40
+ const envId = process.env.CLAUDE_CODE_OAUTH_CLIENT_ID?.trim();
41
+ if (envId)
42
+ push(clientIds, envId);
43
+ let discovered;
44
+ let handle;
45
+ try {
46
+ const executable = await findClaudeExecutable();
47
+ if (executable) {
48
+ handle = await open(executable, "r");
49
+ const size = (await handle.stat()).size;
50
+ const decoder = new TextDecoder();
51
+ let carry = "";
52
+ for (let offset = 0; offset < size; offset += CHUNK_BYTES) {
53
+ const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, size - offset));
54
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
55
+ const text = carry + decoder.decode(buffer.subarray(0, bytesRead));
56
+ for (const match of text.matchAll(CLIENT_RE)) {
57
+ discovered ??= match[1];
58
+ push(clientIds, match[1]);
59
+ }
60
+ for (const match of text.matchAll(TOKEN_URL_RE)) {
61
+ if (ownHost(match[0]))
62
+ push(tokenUrls, match[0]);
63
+ }
64
+ carry = text.slice(-OVERLAP_BYTES);
65
+ }
66
+ }
67
+ else {
68
+ log.debug("claude executable not found; using fallback oauth config");
69
+ }
70
+ }
71
+ catch (err) {
72
+ log.debug(`oauth discovery failed: ${err.message}; using fallback`);
73
+ }
74
+ finally {
75
+ await handle?.close().catch(() => undefined);
76
+ }
77
+ push(clientIds, FALLBACK_CLIENT_ID);
78
+ for (const url of FALLBACK_TOKEN_URLS)
79
+ push(tokenUrls, url);
80
+ log.debug(`oauth config source=${discovered ? "claude binary" : "fallback"}`);
81
+ return { clientIds, tokenUrls };
82
+ }
@@ -0,0 +1,13 @@
1
+ import { homedir } from "node:os";
2
+ import { resolve } from "node:path";
3
+ export function claudeConfigDir(env = process.env) {
4
+ const configured = env.CLAUDE_CONFIG_DIR?.split(",")[0]?.trim();
5
+ if (!configured)
6
+ return resolve(homedir(), ".claude");
7
+ if (configured === "~")
8
+ return homedir();
9
+ if (configured.startsWith("~/") || configured.startsWith("~\\")) {
10
+ return resolve(homedir(), configured.slice(2));
11
+ }
12
+ return resolve(configured);
13
+ }
@@ -0,0 +1,102 @@
1
+ import { join } from "node:path";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { createLogger } from "../util/logger.js";
4
+ import { claudeConfigDir } from "./paths.js";
5
+ const log = createLogger("plan-info");
6
+ const CLAUDE_DIR = claudeConfigDir();
7
+ export const CREDENTIALS_PATH = join(CLAUDE_DIR, ".credentials.json");
8
+ const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
9
+ const CACHE_TTL_MS = 60 * 60 * 1000;
10
+ const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
11
+ export async function getDefaultEffort() {
12
+ try {
13
+ const raw = await readFile(SETTINGS_PATH, "utf8");
14
+ const level = JSON.parse(raw).effortLevel;
15
+ return typeof level === "string" && EFFORT_LEVELS.has(level) ? level : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export function planNameFrom(subscriptionType, rateLimitTier) {
22
+ const sub = (subscriptionType ?? "").toLowerCase();
23
+ const tier = (rateLimitTier ?? "").toLowerCase();
24
+ if (sub === "pro")
25
+ return "Pro";
26
+ if (sub === "max") {
27
+ if (tier.includes("20x"))
28
+ return "Max 20X";
29
+ if (tier.includes("5x"))
30
+ return "Max 5X";
31
+ return "Max";
32
+ }
33
+ if (sub === "team" || sub === "enterprise")
34
+ return sub.charAt(0).toUpperCase() + sub.slice(1);
35
+ return "Claude";
36
+ }
37
+ async function readCredentials() {
38
+ for (let attempt = 0; attempt < 3; attempt++) {
39
+ try {
40
+ const raw = await readFile(CREDENTIALS_PATH, "utf8");
41
+ return JSON.parse(raw);
42
+ }
43
+ catch (err) {
44
+ if (attempt === 2) {
45
+ const error = err;
46
+ const message = `could not read credentials: ${error.message}`;
47
+ if (error.code === "ENOENT")
48
+ log.debug(message);
49
+ else
50
+ log.warn(message);
51
+ return undefined;
52
+ }
53
+ await new Promise((r) => setTimeout(r, 50));
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+ export async function readAccessToken() {
59
+ const creds = await readCredentials();
60
+ return creds?.claudeAiOauth?.accessToken;
61
+ }
62
+ export async function readOauthCredentials() {
63
+ const oauth = (await readCredentials())?.claudeAiOauth;
64
+ return {
65
+ accessToken: oauth?.accessToken,
66
+ refreshToken: oauth?.refreshToken,
67
+ expiresAt: typeof oauth?.expiresAt === "number" ? oauth.expiresAt : undefined,
68
+ };
69
+ }
70
+ export async function saveRefreshedCredentials(accessToken, refreshToken, expiresInS) {
71
+ try {
72
+ const raw = await readFile(CREDENTIALS_PATH, "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ const oauth = parsed.claudeAiOauth && typeof parsed.claudeAiOauth === "object"
75
+ ? parsed.claudeAiOauth
76
+ : {};
77
+ oauth.accessToken = accessToken;
78
+ if (refreshToken)
79
+ oauth.refreshToken = refreshToken;
80
+ if (typeof expiresInS === "number" && Number.isFinite(expiresInS) && expiresInS > 0) {
81
+ oauth.expiresAt = Date.now() + Math.round(expiresInS * 1000);
82
+ }
83
+ parsed.claudeAiOauth = oauth;
84
+ await writeFile(CREDENTIALS_PATH, JSON.stringify(parsed), "utf8");
85
+ return true;
86
+ }
87
+ catch (err) {
88
+ log.warn(`could not save refreshed credentials: ${err.message}`);
89
+ return false;
90
+ }
91
+ }
92
+ let cached;
93
+ export async function getPlanName() {
94
+ const now = Date.now();
95
+ if (cached && now - cached.at < CACHE_TTL_MS)
96
+ return cached.name;
97
+ const creds = await readCredentials();
98
+ const oauth = creds?.claudeAiOauth;
99
+ const name = planNameFrom(oauth?.subscriptionType, oauth?.rateLimitTier);
100
+ cached = { name, at: now };
101
+ return name;
102
+ }