claude-code-discord-presence 1.0.2 → 1.0.3
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 +7 -0
- package/dist/claude/plan-info.js +49 -2
- package/dist/claude/session-store.js +4 -2
- package/dist/claude/usage-poller.js +2 -0
- package/dist/cli.js +1 -1
- package/dist/discord/presence-builder.js +36 -13
- package/package.json +1 -1
- package/scripts/setup-claude.mjs +2 -1
- package/scripts/setup-remote.mjs +2 -2
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ required. Existing unrelated Claude settings and hooks are preserved.
|
|
|
42
42
|
|
|
43
43
|
The account usage endpoint is preferred over placeholder statusline zeroes. This prevents the false
|
|
44
44
|
`100% left` display that some Claude clients emit while keeping statusline data as an offline fallback.
|
|
45
|
+
On macOS, the service reads the same OAuth metadata from the encrypted macOS Keychain that Claude
|
|
46
|
+
Code uses; Linux and Windows use Claude's protected `.credentials.json` file.
|
|
45
47
|
|
|
46
48
|
## Hooks and statusline
|
|
47
49
|
|
|
@@ -57,6 +59,11 @@ user-level `settings.json` used by Claude Code CLI and Desktop.
|
|
|
57
59
|
- Existing settings are backed up under `~/.claude/backups/claude-code-presence/`; only five backups
|
|
58
60
|
are retained.
|
|
59
61
|
|
|
62
|
+
If `settings.json` already contains a statusline that is not managed by this package, the installer
|
|
63
|
+
leaves it unchanged. Claude Code supports one `statusLine` command, so the presence statusline is not
|
|
64
|
+
installed in that case: hooks and basic activity still work, but statusline-only fields may be
|
|
65
|
+
unavailable until the user chooses which statusline to keep or combines the commands manually.
|
|
66
|
+
|
|
60
67
|
Run the installer explicitly at any time with `claude-code-presence setup`.
|
|
61
68
|
|
|
62
69
|
## Desktop, CLI, and platforms
|
package/dist/claude/plan-info.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
3
5
|
import { createLogger } from "../util/logger.js";
|
|
4
6
|
import { claudeConfigDir } from "./paths.js";
|
|
5
7
|
const log = createLogger("plan-info");
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
6
9
|
const CLAUDE_DIR = claudeConfigDir();
|
|
7
10
|
export const CREDENTIALS_PATH = join(CLAUDE_DIR, ".credentials.json");
|
|
11
|
+
const MACOS_KEYCHAIN_SERVICES = ["Claude Code-credentials", "Claude Code"];
|
|
8
12
|
const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
9
13
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
10
14
|
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
@@ -34,11 +38,50 @@ export function planNameFrom(subscriptionType, rateLimitTier) {
|
|
|
34
38
|
return sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
35
39
|
return "Claude";
|
|
36
40
|
}
|
|
41
|
+
const runSecurity = async (file, args, options) => execFileAsync(file, args, options);
|
|
42
|
+
function parseCredentials(raw) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function readMacosKeychainCredentials(run = runSecurity) {
|
|
52
|
+
for (const service of MACOS_KEYCHAIN_SERVICES) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await run("/usr/bin/security", ["find-generic-password", "-s", service, "-w"], { encoding: "utf8", timeout: 10_000, maxBuffer: 512 * 1024 });
|
|
55
|
+
if (stdout.trim())
|
|
56
|
+
return stdout.trim();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Try the legacy service name before falling back to the credentials file.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
export async function readCredentialsForPlatform(platform, readCredentialsFile, readKeychain) {
|
|
65
|
+
if (platform === "darwin") {
|
|
66
|
+
const keychainRaw = await readKeychain();
|
|
67
|
+
const keychainCredentials = keychainRaw ? parseCredentials(keychainRaw) : undefined;
|
|
68
|
+
if (keychainCredentials)
|
|
69
|
+
return keychainCredentials;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return parseCredentials(await readCredentialsFile(CREDENTIALS_PATH, "utf8"));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
37
78
|
async function readCredentials() {
|
|
38
79
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
39
80
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
81
|
+
const credentials = await readCredentialsForPlatform(process.platform, readFile, readMacosKeychainCredentials);
|
|
82
|
+
if (credentials)
|
|
83
|
+
return credentials;
|
|
84
|
+
throw Object.assign(new Error("Claude Code credentials were not found"), { code: "ENOENT" });
|
|
42
85
|
}
|
|
43
86
|
catch (err) {
|
|
44
87
|
if (attempt === 2) {
|
|
@@ -68,6 +111,10 @@ export async function readOauthCredentials() {
|
|
|
68
111
|
};
|
|
69
112
|
}
|
|
70
113
|
export async function saveRefreshedCredentials(accessToken, refreshToken, expiresInS) {
|
|
114
|
+
if (process.platform === "darwin") {
|
|
115
|
+
log.warn("token refresh cannot update macOS Keychain; Claude Code will refresh its credentials");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
71
118
|
try {
|
|
72
119
|
const raw = await readFile(CREDENTIALS_PATH, "utf8");
|
|
73
120
|
const parsed = JSON.parse(raw);
|
|
@@ -290,6 +290,7 @@ export class SessionStore {
|
|
|
290
290
|
case "PostToolUse":
|
|
291
291
|
break;
|
|
292
292
|
case "Stop":
|
|
293
|
+
case "StopFailure":
|
|
293
294
|
session.status = "idle";
|
|
294
295
|
session.action = "Idle";
|
|
295
296
|
break;
|
|
@@ -329,9 +330,10 @@ export class SessionStore {
|
|
|
329
330
|
else if (session.transcriptPath) {
|
|
330
331
|
if (!this.effectiveModel(session) || turnBoundary)
|
|
331
332
|
void this.refreshModelFromTranscript(session);
|
|
332
|
-
const statsEvent = event === "Stop" || event === "
|
|
333
|
+
const statsEvent = event === "Stop" || event === "StopFailure" || event === "UserPromptSubmit" ||
|
|
334
|
+
event === "PostToolUse" || event === "SessionStart";
|
|
333
335
|
if (statsEvent && !fromAgent) {
|
|
334
|
-
void this.refreshStatsFromTranscript(session, event === "Stop" || event === "SessionStart");
|
|
336
|
+
void this.refreshStatsFromTranscript(session, event === "Stop" || event === "StopFailure" || event === "SessionStart");
|
|
335
337
|
}
|
|
336
338
|
}
|
|
337
339
|
this.onChange();
|
|
@@ -59,6 +59,8 @@ export class UsagePoller {
|
|
|
59
59
|
return base + Math.floor((0.5 - Math.abs((Date.now() % 1000) / 1000 - 0.5)) * spread);
|
|
60
60
|
}
|
|
61
61
|
watchCredentials() {
|
|
62
|
+
if (process.platform === "darwin")
|
|
63
|
+
return;
|
|
62
64
|
try {
|
|
63
65
|
this.watcher = watch(CREDENTIALS_PATH, () => {
|
|
64
66
|
if (this.watchDebounce)
|
package/dist/cli.js
CHANGED
|
@@ -96,7 +96,7 @@ function setupInstalled() {
|
|
|
96
96
|
try {
|
|
97
97
|
const config = JSON.parse(readFileSync(join(claudeDir(), "discord-presence", "config.json"), "utf8"));
|
|
98
98
|
const settings = readFileSync(join(claudeDir(), "settings.json"), "utf8");
|
|
99
|
-
return config.installerVersion ===
|
|
99
|
+
return config.installerVersion === 2 && /discord-presence[\\/]hook\.mjs/i.test(settings) &&
|
|
100
100
|
/discord-presence[\\/]statusline\.mjs/i.test(settings);
|
|
101
101
|
}
|
|
102
102
|
catch {
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
const MIN = 2;
|
|
2
2
|
const MAX = 128;
|
|
3
3
|
const SEP = " • ";
|
|
4
|
-
const MODEL_NAMES = {
|
|
5
|
-
"claude-fable-5": "Fable 5",
|
|
6
|
-
"claude-opus-4-8": "Opus 4.8",
|
|
7
|
-
"claude-opus-4-7": "Opus 4.7",
|
|
8
|
-
"claude-sonnet-5": "Sonnet 5",
|
|
9
|
-
"claude-haiku-4-5": "Haiku 4.5",
|
|
10
|
-
};
|
|
11
4
|
const EFFORT_LABELS = {
|
|
12
5
|
minimal: "Minimal",
|
|
13
6
|
low: "Light",
|
|
@@ -21,12 +14,42 @@ export function effortLabel(level) {
|
|
|
21
14
|
return EFFORT_LABELS[level];
|
|
22
15
|
}
|
|
23
16
|
export function modelDisplayName(id, fallback) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
17
|
+
const fallbackName = fallback?.trim();
|
|
18
|
+
if (fallbackName && !/[-_[\]]/.test(fallbackName))
|
|
19
|
+
return fallbackName;
|
|
20
|
+
const raw = fallbackName || id?.trim();
|
|
21
|
+
if (raw) {
|
|
22
|
+
const normalized = raw
|
|
23
|
+
.replace(/^claude[-_\s]+/i, "")
|
|
24
|
+
.replace(/\s*\[[^\]]*\]\s*$/g, "")
|
|
25
|
+
.replace(/[_\s]+/g, "-")
|
|
26
|
+
.replace(/-(?:latest|\d+[km]|\d{8})$/i, "")
|
|
27
|
+
.replace(/-+/g, "-")
|
|
28
|
+
.replace(/^-|-$/g, "");
|
|
29
|
+
const tokens = normalized.split("-").filter(Boolean);
|
|
30
|
+
const title = (value) => value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
31
|
+
if (/^\d/.test(tokens[0] ?? "")) {
|
|
32
|
+
const version = [];
|
|
33
|
+
while (tokens.length > 0 && /^\d+(?:\.\d+)?$/.test(tokens[0]))
|
|
34
|
+
version.push(tokens.shift());
|
|
35
|
+
const family = tokens.shift();
|
|
36
|
+
if (family)
|
|
37
|
+
return `${title(family)} ${version.join(".")}`.trim();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const family = [];
|
|
41
|
+
while (tokens.length > 0 && !/^\d+(?:\.\d+)?$/.test(tokens[0]))
|
|
42
|
+
family.push(tokens.shift());
|
|
43
|
+
const version = [];
|
|
44
|
+
while (tokens.length > 0 && /^\d+(?:\.\d+)?$/.test(tokens[0]))
|
|
45
|
+
version.push(tokens.shift());
|
|
46
|
+
if (family.length > 0) {
|
|
47
|
+
return `${family.map(title).join(" ")}${version.length > 0 ? ` ${version.join(".")}` : ""}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (normalized)
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
30
53
|
return "Claude";
|
|
31
54
|
}
|
|
32
55
|
function pctLeft(usedPercentage) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-discord-presence",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Cross-platform Discord Rich Presence for Claude Code Desktop and CLI with live models, limits, costs, tools, agents, statusline, and secure SSH remotes.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
package/scripts/setup-claude.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const events = [
|
|
|
23
23
|
"PostToolUse",
|
|
24
24
|
"PostToolUseFailure",
|
|
25
25
|
"Stop",
|
|
26
|
+
"StopFailure",
|
|
26
27
|
"SubagentStart",
|
|
27
28
|
"SubagentStop",
|
|
28
29
|
"Notification",
|
|
@@ -82,7 +83,7 @@ await copyFile(join(scriptsDir, "hook.mjs"), hookTarget);
|
|
|
82
83
|
await copyFile(join(scriptsDir, "statusline.mjs"), statuslineTarget);
|
|
83
84
|
await writeFile(
|
|
84
85
|
join(installDir, "config.json"),
|
|
85
|
-
`${JSON.stringify({ port, remote: false, installerVersion:
|
|
86
|
+
`${JSON.stringify({ port, remote: false, installerVersion: 2 }, null, 2)}\n`,
|
|
86
87
|
{ mode: 0o600 },
|
|
87
88
|
);
|
|
88
89
|
|
package/scripts/setup-remote.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const hookTarget = join(installDir, "hook.mjs");
|
|
|
21
21
|
const statuslineTarget = join(installDir, "statusline.mjs");
|
|
22
22
|
const events = [
|
|
23
23
|
"SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
24
|
-
"Stop", "SubagentStart", "SubagentStop", "Notification", "SessionEnd",
|
|
24
|
+
"Stop", "StopFailure", "SubagentStart", "SubagentStop", "Notification", "SessionEnd",
|
|
25
25
|
];
|
|
26
26
|
const matcherEvents = new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
|
|
27
27
|
const quote = (value) => `"${value.replaceAll("\\", "/").replaceAll('"', '\\"')}"`;
|
|
@@ -60,7 +60,7 @@ await writeFile(hookTarget, hookSource, { mode: 0o700 });
|
|
|
60
60
|
await writeFile(statuslineTarget, statuslineSource, { mode: 0o700 });
|
|
61
61
|
await writeFile(
|
|
62
62
|
join(installDir, "config.json"),
|
|
63
|
-
`${JSON.stringify({ port, remote: true, installerVersion:
|
|
63
|
+
`${JSON.stringify({ port, remote: true, installerVersion: 2 }, null, 2)}\n`,
|
|
64
64
|
{ mode: 0o600 },
|
|
65
65
|
);
|
|
66
66
|
|