claude-rpc 1.3.0 → 1.3.1
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/SECURITY.md +6 -0
- package/package.json +1 -1
- package/src/claude-proc.js +97 -0
- package/src/daemon.js +87 -8
- package/src/default-config.js +6 -0
- package/src/format.js +33 -4
- package/src/install.js +10 -2
- package/src/presence.js +7 -4
- package/src/version.js +1 -1
package/SECURITY.md
CHANGED
|
@@ -304,6 +304,12 @@ untrusted or remote input into a shell:
|
|
|
304
304
|
(`src/git.js`).
|
|
305
305
|
- `gh repo view --json isPrivate` — auto-hide GitHub-private repos from the card
|
|
306
306
|
(`src/privacy.js`); 1.5s timeout, silent skip if `gh` is absent.
|
|
307
|
+
- `ps -eo comm=,args=` (macOS/Linux) / `powershell.exe Get-CimInstance
|
|
308
|
+
Win32_Process` filtered to `claude|node|bun|deno` (Windows) — the daemon's
|
|
309
|
+
60-second "is Claude Code still running?" liveness poll (`src/claude-proc.js`),
|
|
310
|
+
so an open-but-quiet session shows *idle* instead of vanishing. Read-only, the
|
|
311
|
+
result is one boolean, nothing is logged or sent anywhere; disable with
|
|
312
|
+
`processDetection: false`.
|
|
307
313
|
- `gh gist` / `gh --version` — gist badge publishing, only under 3b
|
|
308
314
|
(`src/gist.js`).
|
|
309
315
|
- `npm root -g` / `npm install -g` — resolve / promote the global install during
|
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// "Is Claude Code actually running?" — process-level liveness detection.
|
|
2
|
+
//
|
|
3
|
+
// The daemon's only passive signals for "Claude Code is open" were hook
|
|
4
|
+
// freshness (state.lastActivity) and transcript mtimes (findLiveSessions).
|
|
5
|
+
// Both go quiet the moment the user stops typing: a session left open at the
|
|
6
|
+
// prompt writes NOTHING to disk, so after staleSessionMin the daemon concluded
|
|
7
|
+
// "Claude Code not running" and cleared the card — even with idleWhenOpen:true,
|
|
8
|
+
// whose staleMs backstop exists precisely because transcript silence was
|
|
9
|
+
// previously indistinguishable from a closed terminal. Asking the OS whether a
|
|
10
|
+
// Claude Code process exists removes that ambiguity: alive → the card stays up
|
|
11
|
+
// as 'idle' indefinitely; gone → the existing stale paths clear it.
|
|
12
|
+
//
|
|
13
|
+
// Detection is deliberately conservative about what counts as Claude Code:
|
|
14
|
+
// - a process literally named `claude` / `claude.exe` (native installer)
|
|
15
|
+
// - a runtime (node/bun/deno) whose command line points into the
|
|
16
|
+
// `@anthropic-ai/claude-code` package (npm/npx installs)
|
|
17
|
+
// - a binary run from the native installer's version store
|
|
18
|
+
// (~/.local/share/claude/versions/<v>)
|
|
19
|
+
// It must NOT match claude-rpc itself (this daemon is `node .../claude-rpc/
|
|
20
|
+
// src/daemon.js` — a bare /claude/ substring test would make the daemon see
|
|
21
|
+
// itself as Claude Code and the card would never clear). Every pattern below
|
|
22
|
+
// therefore requires a word boundary right after "claude".
|
|
23
|
+
//
|
|
24
|
+
// Adopted from the Teksya fork's groundwork (fork commit 46b62a5).
|
|
25
|
+
import { execFile } from 'node:child_process';
|
|
26
|
+
|
|
27
|
+
// One process-table query per call is the whole cost model; the daemon calls
|
|
28
|
+
// on a slow timer (see CLAUDE_PROC_POLL_MS in daemon.js), so no caching here.
|
|
29
|
+
const EXEC_TIMEOUT_MS = 15_000;
|
|
30
|
+
|
|
31
|
+
// Does this (name, command line) pair look like a Claude Code process?
|
|
32
|
+
// Exported for tests. `name` is the executable name where the platform gives
|
|
33
|
+
// us one (Win32_Process.Name / ps comm); `cmdline` is the full command line.
|
|
34
|
+
export function looksLikeClaudeCode(name, cmdline) {
|
|
35
|
+
const n = String(name || '').toLowerCase();
|
|
36
|
+
const c = String(cmdline || '').replace(/\\/g, '/').toLowerCase();
|
|
37
|
+
// The Claude DESKTOP app's binary is ALSO named claude(.exe) — Windows
|
|
38
|
+
// installs under …/AnthropicClaude/, macOS ships as Claude.app. It is not
|
|
39
|
+
// Claude Code; matching it would pin the card up whenever the chat app is
|
|
40
|
+
// open. Path check comes first so nothing below can re-admit it.
|
|
41
|
+
if (c.includes('anthropicclaude') || c.includes('claude.app/')) return false;
|
|
42
|
+
// npm / npx install: node …/node_modules/@anthropic-ai/claude-code/cli.js
|
|
43
|
+
if (c.includes('@anthropic-ai/claude-code/')) return true;
|
|
44
|
+
// Native-install version store: …/.local/share/claude/versions/<v>
|
|
45
|
+
if (c.includes('/share/claude/versions/')) return true;
|
|
46
|
+
// A command-line token that IS `claude` / `claude.exe` — bare (as typed in a
|
|
47
|
+
// shell, argv[0] preserved) or as the tail of a path. The boundary required
|
|
48
|
+
// right after "claude" is what keeps claude-rpc from matching.
|
|
49
|
+
if (/(^|[/"'])claude(\.exe)?["']?(\s|$)/.test(c)) return true;
|
|
50
|
+
// No command line to inspect (platform hid it) — fall back to the process
|
|
51
|
+
// name alone. Only safe here because the desktop-app exclusion above could
|
|
52
|
+
// not run; a bare-name match is still far likelier Code than the chat app
|
|
53
|
+
// being unreadable.
|
|
54
|
+
if (!c && (n === 'claude' || n === 'claude.exe')) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function execText(file, args) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile(file, args, { timeout: EXEC_TIMEOUT_MS, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
|
|
61
|
+
(err, stdout) => resolve(err ? null : String(stdout || '')));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Resolve to true/false when the platform answered, null when the query itself
|
|
66
|
+
// failed (missing tool, timeout, unsupported platform) — null means "unknown"
|
|
67
|
+
// and callers must fall back to the old transcript-only behavior. Never throws.
|
|
68
|
+
export async function detectClaudeProcess() {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
// Only the names that can host Claude Code — a full process dump with
|
|
71
|
+
// command lines is much slower than this filtered CIM query.
|
|
72
|
+
const script = "Get-CimInstance Win32_Process -Filter \"Name='claude.exe' OR Name='node.exe' OR Name='bun.exe' OR Name='deno.exe'\" | ForEach-Object { $_.Name + [char]9 + $_.CommandLine }";
|
|
73
|
+
const out = await execText('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
74
|
+
if (out === null) return null;
|
|
75
|
+
for (const line of out.split('\n')) {
|
|
76
|
+
const tab = line.indexOf('\t');
|
|
77
|
+
const name = tab === -1 ? line.trim() : line.slice(0, tab).trim();
|
|
78
|
+
const cmd = tab === -1 ? '' : line.slice(tab + 1);
|
|
79
|
+
if (looksLikeClaudeCode(name, cmd)) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
// macOS / Linux. comm is the executable name (first token); args follows.
|
|
84
|
+
const out = await execText('ps', ['-eo', 'comm=,args=']);
|
|
85
|
+
if (out === null) return null;
|
|
86
|
+
for (const line of out.split('\n')) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed) continue;
|
|
89
|
+
const sp = trimmed.indexOf(' ');
|
|
90
|
+
const comm = sp === -1 ? trimmed : trimmed.slice(0, sp);
|
|
91
|
+
const args = sp === -1 ? '' : trimmed.slice(sp + 1);
|
|
92
|
+
// comm may be a path on some ps builds — compare its basename.
|
|
93
|
+
const base = comm.slice(comm.lastIndexOf('/') + 1);
|
|
94
|
+
if (looksLikeClaudeCode(base, args)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
package/src/daemon.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readState, sweepStaleStateTmp, listSessionStates, sweepStaleSessionStat
|
|
|
7
7
|
import { makeRotationCursor, pickFrames, selectFrame, resolveLargeImageKey, shouldShowGithubButton, pickActiveSession, throttleDecision } from './presence.js';
|
|
8
8
|
import { buildVars, fillTemplate, framePasses, applyIdle, applyShipped, applyTrigger } from './format.js';
|
|
9
9
|
import { scan, readAggregate, findLiveSessions, readSessionTokens, readSessionModel } from './scanner.js';
|
|
10
|
+
import { detectClaudeProcess } from './claude-proc.js';
|
|
10
11
|
import { detectGithubUrl } from './git.js';
|
|
11
12
|
import { applyPrivacy } from './privacy.js';
|
|
12
13
|
import { pauseUntil } from './pause.js';
|
|
@@ -100,6 +101,12 @@ try {
|
|
|
100
101
|
let config = loadConfigWithLog();
|
|
101
102
|
let aggregate = readAggregate() || null;
|
|
102
103
|
let liveSessions = [];
|
|
104
|
+
// OS-level "a Claude Code process exists" flag (see claude-proc.js). Injected
|
|
105
|
+
// into every resolved state so applyIdle can tell "open but quiet" (→ idle)
|
|
106
|
+
// from "actually closed" (→ stale) — transcript mtimes alone can't, which is
|
|
107
|
+
// why an untouched-but-open session used to clear the card after staleSessionMin.
|
|
108
|
+
// null = unknown (never checked / detection unavailable) → old behavior.
|
|
109
|
+
let claudeProcAlive = null;
|
|
103
110
|
let client = null;
|
|
104
111
|
let connected = false;
|
|
105
112
|
let connecting = false; // login() in flight — see the watchdog note in connect()
|
|
@@ -147,6 +154,10 @@ const rotationCursor = makeRotationCursor();
|
|
|
147
154
|
// Which concurrent session's card we're currently showing. pickActiveSession
|
|
148
155
|
// keeps it sticky — see resolvePresence.
|
|
149
156
|
let displayedSessionId = null;
|
|
157
|
+
// sessionId → transcript path, remembered from the last tick the session was
|
|
158
|
+
// inside the live window. Lets token enrichment keep reading the (mtime-
|
|
159
|
+
// cached) transcript after it goes quiet instead of zeroing the card's count.
|
|
160
|
+
const sessionPathMemo = new Map();
|
|
150
161
|
// Stabilizes Discord's elapsed timer: applyIdle can synthesize a sessionStart
|
|
151
162
|
// from a moving transcript mtime, and missing-hook scenarios leave it null —
|
|
152
163
|
// either case would make startTimestamp jump on every rotation.
|
|
@@ -216,6 +227,10 @@ function resolvePresence(opts = {}) {
|
|
|
216
227
|
// Attach live sessions BEFORE applyIdle so the stale/idle decision can
|
|
217
228
|
// see ongoing transcript activity, not just this daemon's hook state.
|
|
218
229
|
state.liveSessions = opts.liveSessions || liveSessions;
|
|
230
|
+
// Same injection pattern for OS-level process liveness — standalone callers
|
|
231
|
+
// (preview/api) that pass an explicit state simply leave it undefined and
|
|
232
|
+
// get the historical transcript-only behavior.
|
|
233
|
+
if (state.claudeProcessAlive === undefined) state.claudeProcessAlive = claudeProcAlive;
|
|
219
234
|
state = applyIdle(state, config);
|
|
220
235
|
// Shipped overlay sits on top of idle/working/thinking — but never over
|
|
221
236
|
// stale (we don't celebrate when Claude isn't running).
|
|
@@ -228,18 +243,43 @@ function resolvePresence(opts = {}) {
|
|
|
228
243
|
// events is always {0,0,0,0}. The transcript is the only running source
|
|
229
244
|
// of truth — readSessionTokens is mtime-cached, so this is cheap unless
|
|
230
245
|
// the session is actively writing.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
// Matching is by SESSION ID first: the transcript file is named
|
|
247
|
+
// <sessionId>.jsonl, so this is exact. The old cwd-only match silently
|
|
248
|
+
// broke whenever the two cwd notions diverged — state.cwd follows the
|
|
249
|
+
// hooks (UserPromptSubmit re-stamps it after every in-session `cd`) while
|
|
250
|
+
// the live-session cwd is read from the transcript HEAD (fixed at session
|
|
251
|
+
// start) — leaving the card's token count stuck at 0. The id→path memo
|
|
252
|
+
// keeps enrichment alive after the transcript ages out of the live window
|
|
253
|
+
// (>90s quiet), so an idle card keeps its final token count instead of
|
|
254
|
+
// dropping to zero. cwd remains the fallback for borrowed states (whose
|
|
255
|
+
// sessionId belongs to the QUIET session, not the transcript we adopted)
|
|
256
|
+
// and legacy id-less states.
|
|
257
|
+
if (state.status !== 'stale') {
|
|
258
|
+
const live = state.liveSessions || [];
|
|
259
|
+
let tpath = null;
|
|
260
|
+
if (state.sessionId && !state.borrowed) {
|
|
261
|
+
const bySid = live.find((s) => basename(s.path || '', '.jsonl') === state.sessionId);
|
|
262
|
+
if (bySid) {
|
|
263
|
+
tpath = bySid.path;
|
|
264
|
+
if (!sessionPathMemo.has(state.sessionId) && sessionPathMemo.size >= 64) {
|
|
265
|
+
sessionPathMemo.delete(sessionPathMemo.keys().next().value);
|
|
266
|
+
}
|
|
267
|
+
sessionPathMemo.set(state.sessionId, tpath);
|
|
268
|
+
} else {
|
|
269
|
+
tpath = sessionPathMemo.get(state.sessionId) || null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!tpath && state.cwd) {
|
|
273
|
+
const cwdLower = state.cwd.toLowerCase();
|
|
274
|
+
tpath = live.find((s) => (s.cwd || '').toLowerCase() === cwdLower)?.path || null;
|
|
275
|
+
}
|
|
276
|
+
if (tpath) {
|
|
277
|
+
const t = readSessionTokens(tpath);
|
|
238
278
|
if (t) state.tokens = t;
|
|
239
279
|
// The hook only learns the model at SessionStart; a mid-session /model
|
|
240
280
|
// switch is visible only in the transcript. Render-time override — the
|
|
241
281
|
// state file stays hook-owned.
|
|
242
|
-
const liveModel = readSessionModel(
|
|
282
|
+
const liveModel = readSessionModel(tpath);
|
|
243
283
|
if (liveModel) state.model = liveModel;
|
|
244
284
|
}
|
|
245
285
|
}
|
|
@@ -769,6 +809,45 @@ function refreshLiveSessions() {
|
|
|
769
809
|
refreshLiveSessions();
|
|
770
810
|
setInterval(refreshLiveSessions, 30_000);
|
|
771
811
|
|
|
812
|
+
// OS-level Claude Code liveness poll (see claude-proc.js). This is what lets
|
|
813
|
+
// an open-but-untouched session stay 'idle' forever instead of being declared
|
|
814
|
+
// stale after staleSessionMin — hook state and transcript mtimes both go
|
|
815
|
+
// silent when the user merely stops typing, but the process table doesn't lie.
|
|
816
|
+
// 60s cadence: one filtered process-table query per minute is negligible, and
|
|
817
|
+
// a card lingering up to a minute after Claude truly exits is fine (the same
|
|
818
|
+
// order as the transcript-based paths). Kill switch: processDetection:false.
|
|
819
|
+
// A single failed query keeps the last known answer (a transient PowerShell/ps
|
|
820
|
+
// hiccup must not flash the card off); two consecutive failures degrade to
|
|
821
|
+
// null = unknown, which restores the old transcript-only behavior.
|
|
822
|
+
const CLAUDE_PROC_POLL_MS = 60_000;
|
|
823
|
+
let claudeProcFailStreak = 0;
|
|
824
|
+
async function refreshClaudeProc() {
|
|
825
|
+
if (config.processDetection === false) { claudeProcAlive = null; return; }
|
|
826
|
+
try {
|
|
827
|
+
const alive = await detectClaudeProcess();
|
|
828
|
+
if (alive === null) {
|
|
829
|
+
claudeProcFailStreak += 1;
|
|
830
|
+
if (claudeProcFailStreak >= 2 && claudeProcAlive !== null) {
|
|
831
|
+
log('claude-proc: detection unavailable — falling back to transcript-only liveness');
|
|
832
|
+
claudeProcAlive = null;
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
claudeProcFailStreak = 0;
|
|
837
|
+
if (alive !== claudeProcAlive) {
|
|
838
|
+
log(`claude-proc: Claude Code process ${alive ? 'detected' : 'gone'}`);
|
|
839
|
+
claudeProcAlive = alive;
|
|
840
|
+
resetTransmit();
|
|
841
|
+
pushPresence();
|
|
842
|
+
}
|
|
843
|
+
} catch (e) {
|
|
844
|
+
log('claude-proc poll failed:', e.message);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
refreshClaudeProc();
|
|
848
|
+
const claudeProcTimer = setInterval(refreshClaudeProc, CLAUDE_PROC_POLL_MS);
|
|
849
|
+
if (claudeProcTimer.unref) claudeProcTimer.unref();
|
|
850
|
+
|
|
772
851
|
// ── Connection watchdog (auto-heal) ──────────────────────────────────────────
|
|
773
852
|
// The reconnect path is event-driven (the 'disconnected' handler + login
|
|
774
853
|
// failures + setActivity errors). But a connection can rot in ways that emit
|
package/src/default-config.js
CHANGED
|
@@ -54,6 +54,12 @@ export const DEFAULT_CONFIG = {
|
|
|
54
54
|
// up through short pauses, at the cost of a closed terminal showing idle for
|
|
55
55
|
// up to staleSessionMin minutes before clearing).
|
|
56
56
|
idleWhenOpen: false,
|
|
57
|
+
// OS-level "is a Claude Code process running?" check (src/claude-proc.js,
|
|
58
|
+
// polled every 60s). When it affirmatively finds one, a quiet-but-open
|
|
59
|
+
// session stays 'idle' instead of going stale — the transcript-silence
|
|
60
|
+
// guesswork above only applies when detection is off, unavailable, or the
|
|
61
|
+
// process is confirmed gone. Set false to disable the process-table query.
|
|
62
|
+
processDetection: true,
|
|
57
63
|
// When true, the daemon CLEARS Discord activity entirely once the state
|
|
58
64
|
// goes stale — your profile shows nothing instead of an "Away" frame.
|
|
59
65
|
hideWhenStale: true,
|
package/src/format.js
CHANGED
|
@@ -885,6 +885,15 @@ export function applyIdle(state, cfg = {}) {
|
|
|
885
885
|
const idleMs = (cfg.idleThresholdSec || 60) * 1000;
|
|
886
886
|
const staleMs = Math.max(60_000, (cfg.staleSessionMin || 5) * 60 * 1000);
|
|
887
887
|
const notificationMs = (cfg.notificationWindowSec || 8) * 1000;
|
|
888
|
+
// OS-level liveness (set by the daemon from claude-proc.detectClaudeProcess,
|
|
889
|
+
// like liveSessions): true = a Claude Code process definitely exists right
|
|
890
|
+
// now. Transcripts and hooks both go silent when the user just stops typing,
|
|
891
|
+
// so without this every quiet-but-open session eventually read as "closed"
|
|
892
|
+
// and the card was cleared mid-use. Only an affirmative true changes
|
|
893
|
+
// behavior — false/null/absent (detection failed, unsupported platform,
|
|
894
|
+
// standalone preview/API callers) keeps the historical transcript-only
|
|
895
|
+
// logic, so this can only ever KEEP a card up, never strand one.
|
|
896
|
+
const procAlive = state.claudeProcessAlive === true;
|
|
888
897
|
// Closing the terminal kills Claude Code without firing SessionEnd, so the
|
|
889
898
|
// only passive "is it gone?" signal is "no transcript is being written".
|
|
890
899
|
// DEFAULT (false): clear the card within ~90-120s of the transcript going
|
|
@@ -927,7 +936,24 @@ export function applyIdle(state, cfg = {}) {
|
|
|
927
936
|
}
|
|
928
937
|
|
|
929
938
|
// Truly dormant: no live transcripts AND local state is old → stale.
|
|
930
|
-
|
|
939
|
+
// Unless the OS says a Claude Code process is still running — then the user
|
|
940
|
+
// simply walked away from an open session, which is 'idle', not 'stale'.
|
|
941
|
+
// The current-activity slots are wiped like the idle transition below so a
|
|
942
|
+
// long-dormant card can't keep showing an hours-old tool/file.
|
|
943
|
+
if (ageMs > staleMs && liveAgeMs > staleMs) {
|
|
944
|
+
if (procAlive) {
|
|
945
|
+
return {
|
|
946
|
+
...state,
|
|
947
|
+
status: 'idle',
|
|
948
|
+
currentTool: null,
|
|
949
|
+
currentFile: null,
|
|
950
|
+
filesOpened: [],
|
|
951
|
+
filesEdited: [],
|
|
952
|
+
filesRead: [],
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return staleWipe(state);
|
|
956
|
+
}
|
|
931
957
|
|
|
932
958
|
// Local state is stale but a live transcript exists somewhere on disk.
|
|
933
959
|
// Borrow the most-recent live session as our "active" context, since the
|
|
@@ -944,7 +970,9 @@ export function applyIdle(state, cfg = {}) {
|
|
|
944
970
|
// clears the card within ~90-120s of the last write. Opt in with
|
|
945
971
|
// idleWhenOpen:true to keep showing 'idle' through short pauses; the
|
|
946
972
|
// staleMs dormancy backstop above still clears it if Claude is truly gone.
|
|
947
|
-
|
|
973
|
+
// A confirmed-alive Claude Code process overrides the heuristic entirely —
|
|
974
|
+
// the process being up IS the answer the transcript silence was guessing at.
|
|
975
|
+
if (liveSessions.length === 0 && !idleWhenOpen && !procAlive) return staleWipe(state);
|
|
948
976
|
return state;
|
|
949
977
|
}
|
|
950
978
|
if (ageMs > idleMs) {
|
|
@@ -954,8 +982,9 @@ export function applyIdle(state, cfg = {}) {
|
|
|
954
982
|
// Hooks quiet AND no live transcripts. By default (idleWhenOpen=false)
|
|
955
983
|
// treat Claude as gone and go stale now. With idleWhenOpen:true the
|
|
956
984
|
// session is treated as open-but-paused and drops to idle; the staleMs
|
|
957
|
-
// backstop above clears it later if Claude actually exited.
|
|
958
|
-
|
|
985
|
+
// backstop above clears it later if Claude actually exited. A confirmed
|
|
986
|
+
// Claude Code process short-circuits the guess — drop to idle instead.
|
|
987
|
+
if (liveSessions.length === 0 && !idleWhenOpen && !procAlive) return staleWipe(state);
|
|
959
988
|
// Going idle — wipe "current activity" indicators so rotation frames
|
|
960
989
|
// gated on filesEdited / currentFile / currentTool stop showing stale
|
|
961
990
|
// active-session data. Keep the session counters (messages/tools/tokens)
|
package/src/install.js
CHANGED
|
@@ -284,10 +284,18 @@ export async function addStartupEntry(exePath) {
|
|
|
284
284
|
// daemon. Launch through a tiny .vbs shim via wscript (window style 0) so the
|
|
285
285
|
// unattended startup path is windowless, like every other launch path. We
|
|
286
286
|
// avoid schtasks deliberately — SECURITY.md advertises "no scheduled task".
|
|
287
|
-
|
|
287
|
+
// The command comes from daemonLaunch like the mac/linux branches — the old
|
|
288
|
+
// hardcoded `"<exe>" daemon` form assumed packaged mode, so npm installs got
|
|
289
|
+
// a Run entry of `"node.exe" daemon` (no script path) and login-autostart
|
|
290
|
+
// silently launched nothing.
|
|
291
|
+
const launch = daemonLaunch(exePath);
|
|
292
|
+
let runCmd = [launch.exe, ...launch.args].map((p) => `"${p}"`).join(' ');
|
|
288
293
|
try {
|
|
289
294
|
mkdirSync(CANONICAL_INSTALL_DIR, { recursive: true });
|
|
290
|
-
|
|
295
|
+
// In a VBS string literal a doubled quote is an escaped quote, so each
|
|
296
|
+
// `""x""` below reaches the shell as `"x"`.
|
|
297
|
+
const vbsCmd = [launch.exe, ...launch.args].map((p) => `""${p}""`).join(' ');
|
|
298
|
+
writeFileSync(STARTUP_VBS, `CreateObject("WScript.Shell").Run "${vbsCmd}", 0, False\r\n`);
|
|
291
299
|
runCmd = `wscript.exe "${STARTUP_VBS}"`;
|
|
292
300
|
} catch { /* couldn't write the shim — fall back to the direct (windowed) entry */ }
|
|
293
301
|
await regCommand([
|
package/src/presence.js
CHANGED
|
@@ -76,10 +76,13 @@ export function pickActiveSession(states, displayedId, now, idleMs) {
|
|
|
76
76
|
if (current && isLive(current)) {
|
|
77
77
|
return { state: current, sessionId: current.sessionId, liveCount };
|
|
78
78
|
}
|
|
79
|
-
// Otherwise show the most-recently-active live session — or, if none are
|
|
80
|
-
// the most-recent
|
|
81
|
-
//
|
|
82
|
-
|
|
79
|
+
// Otherwise show the most-recently-active live session — or, if none are
|
|
80
|
+
// live, the most-recent NOT-closed session (a window sitting idle beats one
|
|
81
|
+
// that fired SessionEnd: the closed session's fresher lastActivity would
|
|
82
|
+
// otherwise drag the card stale while a sibling is still open), and only
|
|
83
|
+
// then the most-recent overall so the card follows the last session into
|
|
84
|
+
// idle/stale rather than blanking.
|
|
85
|
+
const chosen = byRecent.find(isLive) || byRecent.find((s) => !s.claudeClosed) || byRecent[0];
|
|
83
86
|
return { state: chosen, sessionId: chosen.sessionId, liveCount };
|
|
84
87
|
}
|
|
85
88
|
|