claude-rpc 0.6.1 → 0.6.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/package.json +1 -1
- package/src/default-config.js +2 -2
- package/src/format.js +95 -37
- package/src/install.js +19 -0
- package/src/version.js +1 -1
package/package.json
CHANGED
package/src/default-config.js
CHANGED
|
@@ -48,12 +48,12 @@ export const DEFAULT_CONFIG = {
|
|
|
48
48
|
byStatus: {
|
|
49
49
|
working: {
|
|
50
50
|
details: "Working in {project}",
|
|
51
|
-
state: "{currentToolPretty} · {currentFilePretty} · {
|
|
51
|
+
state: "{currentToolPretty} · {currentFilePretty} · {tokensLabel}",
|
|
52
52
|
largeImageText: "Working on a {fileLang} file",
|
|
53
53
|
},
|
|
54
54
|
thinking: {
|
|
55
55
|
details: "Thinking in {project}",
|
|
56
|
-
state: "{modelPretty} · {messagesLabel} · {
|
|
56
|
+
state: "{modelPretty} · {messagesLabel} · {tokensLabel}",
|
|
57
57
|
largeImageText: "Reasoning with {modelPretty}",
|
|
58
58
|
},
|
|
59
59
|
notification: {
|
package/src/format.js
CHANGED
|
@@ -140,6 +140,37 @@ function fmtHour(h) {
|
|
|
140
140
|
return `${hh}:00`;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Detect a cwd that would leak the user's OS username if rendered on
|
|
144
|
+
// Discord. Examples:
|
|
145
|
+
// /home/lucas → basename matches $USER → leak
|
|
146
|
+
// C:\Users\lucas → equals $USERPROFILE → leak
|
|
147
|
+
// /Users/lucas/projects/x → basename "x" ≠ user → fine
|
|
148
|
+
// On a real privacy-sensitive cwd (the home dir itself, with no project
|
|
149
|
+
// scoping), buildVars falls back to `appName` so the card reads
|
|
150
|
+
// "Idle in Claude Code" instead of "Idle in lucas".
|
|
151
|
+
function looksLikeUsernameLeak(cwd) {
|
|
152
|
+
if (!cwd) return false;
|
|
153
|
+
// Check both POSIX and Windows env vars unconditionally — a test or
|
|
154
|
+
// edge case might have one without the other, and over-suppressing
|
|
155
|
+
// the leak side is the safe direction.
|
|
156
|
+
const homes = [process.env.HOME, process.env.USERPROFILE].filter(Boolean);
|
|
157
|
+
const users = [process.env.USER, process.env.USERNAME].filter(Boolean);
|
|
158
|
+
// Normalize path separators so Windows-style cwds work on POSIX
|
|
159
|
+
// basename (which doesn't split on '\').
|
|
160
|
+
const norm = (p) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
161
|
+
const cwdN = norm(cwd);
|
|
162
|
+
for (const home of homes) {
|
|
163
|
+
if (cwdN === norm(home)) return true;
|
|
164
|
+
}
|
|
165
|
+
if (users.length) {
|
|
166
|
+
const base = cwdN.split('/').pop() || '';
|
|
167
|
+
for (const u of users) {
|
|
168
|
+
if (base === u.toLowerCase()) return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
// Trim "C:\repo\src\app\page.tsx" → "src/app/page.tsx" (3 trailing segments).
|
|
144
175
|
function prettyFilePath(p) {
|
|
145
176
|
if (!p) return '';
|
|
@@ -156,7 +187,14 @@ export function buildVars(state, config, aggregate) {
|
|
|
156
187
|
// {tokens} / {tokensFmt} now means the grand total (in + out + cache).
|
|
157
188
|
const sessionTokens = sessionReal + sessionCacheRead + sessionCacheWrite;
|
|
158
189
|
const duration = state.sessionStart ? Date.now() - state.sessionStart : 0;
|
|
159
|
-
|
|
190
|
+
// Privacy: when cwd is the user's home dir (or its basename matches the
|
|
191
|
+
// OS username), don't render it. "Idle in lucas" on Discord is a username
|
|
192
|
+
// leak to anyone viewing the card. Fall back to the configured app name.
|
|
193
|
+
const cwdIsLeaky = looksLikeUsernameLeak(state.cwd);
|
|
194
|
+
const safeCwd = cwdIsLeaky ? '' : (state.cwd || '');
|
|
195
|
+
const projectPretty = cwdIsLeaky
|
|
196
|
+
? (config?.appName || 'Claude Code')
|
|
197
|
+
: (humanProject(state.cwd) || 'Claude Code');
|
|
160
198
|
const currentToolPretty = humanTool(state.currentTool);
|
|
161
199
|
const modelPretty = humanModel(state.model);
|
|
162
200
|
|
|
@@ -298,7 +336,7 @@ export function buildVars(state, config, aggregate) {
|
|
|
298
336
|
statusIcon: config?.statusIcons?.[state.status] || state.status || 'idle',
|
|
299
337
|
project: projectPretty,
|
|
300
338
|
projectPretty,
|
|
301
|
-
cwd:
|
|
339
|
+
cwd: safeCwd,
|
|
302
340
|
model: state.model || 'claude',
|
|
303
341
|
modelPretty,
|
|
304
342
|
messages,
|
|
@@ -341,7 +379,10 @@ export function buildVars(state, config, aggregate) {
|
|
|
341
379
|
// Literal single space — handy for blanking a line without `requires`.
|
|
342
380
|
empty: ' ',
|
|
343
381
|
|
|
344
|
-
//
|
|
382
|
+
// Pluralized session labels. tokensLabel is empty when zero so the
|
|
383
|
+
// `· {tokensLabel}` suffix in default templates collapses away —
|
|
384
|
+
// "Bash · · 0 tokens" is not a useful frame.
|
|
385
|
+
tokensLabel: sessionTokens > 0 ? `${fmtNum(sessionTokens)} tokens` : '',
|
|
345
386
|
messagesLabel: plural(messages, 'prompt'),
|
|
346
387
|
toolsLabel: plural(tools, 'tool call'),
|
|
347
388
|
filesEditedLabel: plural(filesEdited, 'edit'),
|
|
@@ -524,7 +565,40 @@ export function buildVars(state, config, aggregate) {
|
|
|
524
565
|
|
|
525
566
|
export function fillTemplate(tpl, vars) {
|
|
526
567
|
if (typeof tpl !== 'string') return tpl;
|
|
527
|
-
|
|
568
|
+
const filled = tpl.replace(/\{(\w+)\}/g, (_, key) => (key in vars ? String(vars[key]) : `{${key}}`));
|
|
569
|
+
return collapseSeparators(filled);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// After substitution, a template like "{currentToolPretty} · {currentFilePretty} · {tokensFmt} tokens"
|
|
573
|
+
// can resolve to "Bash · · 0 tokens" when the tool doesn't have a file path
|
|
574
|
+
// and no tokens have accumulated yet. Split on `·`, trim each segment, drop
|
|
575
|
+
// empty segments, rejoin — so empty middle vars don't leave orphan separators
|
|
576
|
+
// and trailing/leading separators disappear entirely. Templates without `·`
|
|
577
|
+
// pass through untouched.
|
|
578
|
+
function collapseSeparators(s) {
|
|
579
|
+
if (!s.includes('·')) return s;
|
|
580
|
+
return s.split('·').map((p) => p.trim()).filter(Boolean).join(' · ');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Helper used by every "go stale" branch in applyIdle. Wipes the current-
|
|
584
|
+
// activity slots so rotation frames can't render yesterday's project /
|
|
585
|
+
// file / tool names, and zeroes the session counters that are tied to
|
|
586
|
+
// the now-dead session.
|
|
587
|
+
function staleWipe(state) {
|
|
588
|
+
return {
|
|
589
|
+
...state,
|
|
590
|
+
status: 'stale',
|
|
591
|
+
currentTool: null,
|
|
592
|
+
currentFile: null,
|
|
593
|
+
sessionStart: null,
|
|
594
|
+
cwd: '',
|
|
595
|
+
messages: 0,
|
|
596
|
+
tools: 0,
|
|
597
|
+
filesOpened: [],
|
|
598
|
+
filesEdited: [],
|
|
599
|
+
filesRead: [],
|
|
600
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
601
|
+
};
|
|
528
602
|
}
|
|
529
603
|
|
|
530
604
|
// Apply idle/stale transitions based on lastActivity age. Used by both daemon
|
|
@@ -546,22 +620,7 @@ export function applyIdle(state, cfg = {}) {
|
|
|
546
620
|
// Authoritative close signal from the SessionEnd hook — trust it instead
|
|
547
621
|
// of waiting on staleSessionMin. Any other hook clears the flag, so a
|
|
548
622
|
// sibling session staying alive will reset us out of this branch.
|
|
549
|
-
if (state.claudeClosed)
|
|
550
|
-
return {
|
|
551
|
-
...state,
|
|
552
|
-
status: 'stale',
|
|
553
|
-
currentTool: null,
|
|
554
|
-
currentFile: null,
|
|
555
|
-
sessionStart: null,
|
|
556
|
-
cwd: '',
|
|
557
|
-
messages: 0,
|
|
558
|
-
tools: 0,
|
|
559
|
-
filesOpened: [],
|
|
560
|
-
filesEdited: [],
|
|
561
|
-
filesRead: [],
|
|
562
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
563
|
-
};
|
|
564
|
-
}
|
|
623
|
+
if (state.claudeClosed) return staleWipe(state);
|
|
565
624
|
|
|
566
625
|
// Notification is a brief status — hold it for ~8s after the hook fires,
|
|
567
626
|
// then fall through to normal idle/stale processing.
|
|
@@ -578,22 +637,7 @@ export function applyIdle(state, cfg = {}) {
|
|
|
578
637
|
const liveAgeMs = mostRecentLiveMs ? now - mostRecentLiveMs : Infinity;
|
|
579
638
|
|
|
580
639
|
// Truly dormant: no live transcripts AND local state is old → stale.
|
|
581
|
-
if (ageMs > staleMs && liveAgeMs > staleMs)
|
|
582
|
-
return {
|
|
583
|
-
...state,
|
|
584
|
-
status: 'stale',
|
|
585
|
-
currentTool: null,
|
|
586
|
-
currentFile: null,
|
|
587
|
-
sessionStart: null,
|
|
588
|
-
cwd: '',
|
|
589
|
-
messages: 0,
|
|
590
|
-
tools: 0,
|
|
591
|
-
filesOpened: [],
|
|
592
|
-
filesEdited: [],
|
|
593
|
-
filesRead: [],
|
|
594
|
-
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
595
|
-
};
|
|
596
|
-
}
|
|
640
|
+
if (ageMs > staleMs && liveAgeMs > staleMs) return staleWipe(state);
|
|
597
641
|
|
|
598
642
|
// Local state is stale but a live transcript exists somewhere on disk.
|
|
599
643
|
// Borrow the most-recent live session as our "active" context, since the
|
|
@@ -619,11 +663,25 @@ export function applyIdle(state, cfg = {}) {
|
|
|
619
663
|
}
|
|
620
664
|
|
|
621
665
|
// Local state is fresh.
|
|
622
|
-
if (state.status === 'idle')
|
|
666
|
+
if (state.status === 'idle') {
|
|
667
|
+
// Fast-path stale: if there are NO transcripts being written anywhere
|
|
668
|
+
// on disk, Claude Code isn't running. SessionEnd may not have fired
|
|
669
|
+
// (force-quit, OS sleep, crash). Going stale here clears Discord
|
|
670
|
+
// within ~90-120s of close instead of waiting the full staleMs (5min)
|
|
671
|
+
// — keeps the user's cwd off the card when they're away from their
|
|
672
|
+
// machine. The 5min legacy fallback below still catches the case
|
|
673
|
+
// where transcript mtime is fresh but the hook channel is silent.
|
|
674
|
+
if (liveSessions.length === 0) return staleWipe(state);
|
|
675
|
+
return state;
|
|
676
|
+
}
|
|
623
677
|
if (ageMs > idleMs) {
|
|
624
678
|
// Hook channel is quiet, but a live transcript was modified recently?
|
|
625
679
|
// Keep "working" instead of dropping to "idle".
|
|
626
680
|
if (liveAgeMs <= idleMs) return state;
|
|
681
|
+
// Hooks quiet AND no live transcripts → Claude is closed, not paused.
|
|
682
|
+
// Skip idle, go straight to stale. Same privacy reasoning as the
|
|
683
|
+
// idle-state fast-path above.
|
|
684
|
+
if (liveSessions.length === 0) return staleWipe(state);
|
|
627
685
|
// Going idle — wipe "current activity" indicators so rotation frames
|
|
628
686
|
// gated on filesEdited / currentFile / currentTool stop showing stale
|
|
629
687
|
// active-session data. Keep the session counters (messages/tools/tokens)
|
package/src/install.js
CHANGED
|
@@ -260,6 +260,25 @@ export function migrateConfig() {
|
|
|
260
260
|
added.push('presence.largeImageText');
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// v0.6.3: byStatus.working.state and .thinking.state used `{tokensFmt} tokens`
|
|
264
|
+
// which renders "0 tokens" before any session activity has accrued — combined
|
|
265
|
+
// with empty `{currentFilePretty}` for tools like Bash, that surfaced as
|
|
266
|
+
// "Bash · · 0 tokens" on the card. New default uses `{tokensLabel}` which is
|
|
267
|
+
// empty until tokens > 0, and fillTemplate now collapses adjacent separators.
|
|
268
|
+
// Migrate only the verbatim old template — leave anything the user customized.
|
|
269
|
+
const OLD_WORKING = '{currentToolPretty} · {currentFilePretty} · {tokensFmt} tokens';
|
|
270
|
+
const OLD_THINKING = '{modelPretty} · {messagesLabel} · {tokensFmt} tokens';
|
|
271
|
+
if (cfg.presence.byStatus?.working?.state === OLD_WORKING &&
|
|
272
|
+
DEFAULT_CONFIG.presence?.byStatus?.working?.state) {
|
|
273
|
+
cfg.presence.byStatus.working.state = DEFAULT_CONFIG.presence.byStatus.working.state;
|
|
274
|
+
added.push('presence.byStatus.working.state');
|
|
275
|
+
}
|
|
276
|
+
if (cfg.presence.byStatus?.thinking?.state === OLD_THINKING &&
|
|
277
|
+
DEFAULT_CONFIG.presence?.byStatus?.thinking?.state) {
|
|
278
|
+
cfg.presence.byStatus.thinking.state = DEFAULT_CONFIG.presence.byStatus.thinking.state;
|
|
279
|
+
added.push('presence.byStatus.thinking.state');
|
|
280
|
+
}
|
|
281
|
+
|
|
263
282
|
if (added.length === 0) {
|
|
264
283
|
console.log(` config up to date → ${CONFIG_PATH}`);
|
|
265
284
|
return false;
|