claude-code-discord-presence 1.0.0 → 1.0.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/README.md CHANGED
@@ -85,6 +85,9 @@ The command ensures hooks are installed before registering startup. Remove it wi
85
85
  claude-code-presence autostart:remove
86
86
  ```
87
87
 
88
+ `Ctrl+C` stops only the foreground instance in the current terminal. The removal command also
89
+ gracefully stops an active background instance so Discord activity is cleared before exit.
90
+
88
91
  ## Optional configuration
89
92
 
90
93
  Defaults work without configuration. Pass options directly:
@@ -36,6 +36,7 @@ export class RpcClient {
36
36
  reconnectTimer;
37
37
  reconnectAttempt = 0;
38
38
  connectionGeneration = 0;
39
+ activeSend;
39
40
  constructor(applicationId, transport) {
40
41
  if (typeof transport === "function") {
41
42
  this.factory = transport;
@@ -68,6 +69,9 @@ export class RpcClient {
68
69
  this.flushTimer = undefined;
69
70
  this.reconnectTimer = undefined;
70
71
  this.refreshTimer = undefined;
72
+ const activeSend = this.activeSend;
73
+ if (activeSend)
74
+ await activeSend.catch(() => undefined);
71
75
  const client = this.client;
72
76
  if (!client)
73
77
  return;
@@ -102,6 +106,8 @@ export class RpcClient {
102
106
  return;
103
107
  this.connectionGeneration++;
104
108
  this.ready = false;
109
+ if (this.stopped)
110
+ return;
105
111
  log.warn("disconnected from Discord");
106
112
  this.scheduleReconnect();
107
113
  });
@@ -185,12 +191,23 @@ export class RpcClient {
185
191
  if (desired === null) {
186
192
  if (this.lastSent === null && this.lastSentPid === pid)
187
193
  return;
188
- void this.send(desired, pid);
194
+ this.beginSend(desired, pid);
189
195
  return;
190
196
  }
191
197
  if (this.lastSentPid === pid && this.lastSent !== null && activityEquals(desired, this.lastSent))
192
198
  return;
193
- void this.send(desired, pid);
199
+ this.beginSend(desired, pid);
200
+ }
201
+ beginSend(activity, pid) {
202
+ const pending = this.send(activity, pid);
203
+ this.activeSend = pending;
204
+ void pending.then(() => {
205
+ if (this.activeSend === pending)
206
+ this.activeSend = undefined;
207
+ }, () => {
208
+ if (this.activeSend === pending)
209
+ this.activeSend = undefined;
210
+ });
194
211
  }
195
212
  async send(activity, pid) {
196
213
  const client = this.client;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { loadConfig } from "./config.js";
3
3
  import { startServer } from "./server/http-server.js";
4
4
  import { SessionStore } from "./claude/session-store.js";
@@ -134,6 +134,8 @@ monthlyUsage.start();
134
134
  remoteTunnels.start();
135
135
  void refreshPlan();
136
136
  const planTimer = setInterval(() => void refreshPlan(), 30 * 60 * 1000);
137
+ const stopFile = process.env.PRESENCE_STOP_FILE?.trim();
138
+ let stopFileTimer;
137
139
  let shuttingDown = false;
138
140
  async function shutdown(signal) {
139
141
  if (shuttingDown)
@@ -141,6 +143,8 @@ async function shutdown(signal) {
141
143
  shuttingDown = true;
142
144
  log.info(`shutting down (${signal})`);
143
145
  clearInterval(planTimer);
146
+ if (stopFileTimer)
147
+ clearInterval(stopFileTimer);
144
148
  themeWatcher.stop();
145
149
  poller.stop();
146
150
  desktopFocus?.stop();
@@ -151,10 +155,19 @@ async function shutdown(signal) {
151
155
  monthlyUsage.stop();
152
156
  remoteTunnels.stop();
153
157
  store.dispose();
154
- await new Promise((resolve) => server.close(() => resolve()));
155
158
  await rpc.stop();
159
+ await new Promise((resolve) => {
160
+ server.close(() => resolve());
161
+ server.closeAllConnections();
162
+ });
156
163
  process.exit(0);
157
164
  }
158
165
  process.on("SIGINT", () => void shutdown("SIGINT"));
159
166
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
167
+ if (stopFile) {
168
+ stopFileTimer = setInterval(() => {
169
+ if (existsSync(stopFile))
170
+ void shutdown("autostart removal");
171
+ }, 500);
172
+ }
160
173
  log.info("Claude Code Discord Presence started");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-discord-presence",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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",
@@ -1,7 +1,42 @@
1
1
  #Requires -Version 5.1
2
2
  $ErrorActionPreference = 'Stop'
3
3
 
4
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
5
+ $runScript = Join-Path $scriptDir 'run-service.ps1'
6
+ $stopFile = Join-Path $env:LOCALAPPDATA 'Claude Code Discord Presence\stop.request'
4
7
  $name = 'ClaudeCodeDiscordPresence'
5
8
  $runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
6
9
  Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue
10
+
11
+ $escapedRunScript = [WildcardPattern]::Escape($runScript)
12
+ function Get-PresenceSupervisor {
13
+ @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {
14
+ $_.Name -match '^(?:powershell|pwsh)\.exe$' -and
15
+ $_.CommandLine -like "*$escapedRunScript*"
16
+ })
17
+ }
18
+
19
+ $supervisors = @(Get-PresenceSupervisor)
20
+ if ($supervisors.Count -gt 0) {
21
+ $stopDir = Split-Path -Parent $stopFile
22
+ if (-not (Test-Path -LiteralPath $stopDir)) {
23
+ New-Item -ItemType Directory -Path $stopDir -Force | Out-Null
24
+ }
25
+ [IO.File]::WriteAllText(
26
+ $stopFile,
27
+ [DateTimeOffset]::Now.ToString('O'),
28
+ [Text.UTF8Encoding]::new($false)
29
+ )
30
+ $deadline = [DateTimeOffset]::Now.AddSeconds(15)
31
+ do {
32
+ Start-Sleep -Milliseconds 250
33
+ $supervisors = @(Get-PresenceSupervisor)
34
+ } while ($supervisors.Count -gt 0 -and [DateTimeOffset]::Now -lt $deadline)
35
+ if ($supervisors.Count -gt 0) {
36
+ throw 'The background service did not stop within 15 seconds.'
37
+ }
38
+ } else {
39
+ Remove-Item -LiteralPath $stopFile -Force -ErrorAction SilentlyContinue
40
+ }
41
+
7
42
  Write-Host "Removed '$name' from $runKey."
@@ -3,7 +3,9 @@ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
3
3
  $projectDir = Split-Path -Parent $scriptDir
4
4
  $entryPoint = Join-Path $projectDir 'dist\index.js'
5
5
  $logFile = Join-Path $env:LOCALAPPDATA 'Claude Code Discord Presence\supervisor.log'
6
+ $stopFile = Join-Path $env:LOCALAPPDATA 'Claude Code Discord Presence\stop.request'
6
7
  $maxLogBytes = 1MB
8
+ $env:PRESENCE_STOP_FILE = $stopFile
7
9
 
8
10
  $created = $false
9
11
  $mutex = New-Object System.Threading.Mutex($true, 'Local\ClaudeCodeDiscordPresenceSupervisor', [ref]$created)
@@ -32,13 +34,27 @@ function Write-BoundedLog([string]$message) {
32
34
  Rotate-Log
33
35
  $fastExits = 0
34
36
  while ($true) {
37
+ if (Test-Path -LiteralPath $stopFile) {
38
+ Remove-Item -LiteralPath $stopFile -Force -ErrorAction SilentlyContinue
39
+ exit 0
40
+ }
35
41
  $startedAt = Get-Date
36
42
  & node --disable-warning=ExperimentalWarning --env-file-if-exists=.env --enable-source-maps $entryPoint 2>&1 |
37
43
  ForEach-Object { Write-BoundedLog ([string]$_) }
38
44
  $exitCode = $LASTEXITCODE
39
45
  $uptimeSeconds = [int]((Get-Date) - $startedAt).TotalSeconds
40
46
  Write-BoundedLog "[$(Get-Date -Format o)] supervisor: service exited code=$exitCode uptime=${uptimeSeconds}s"
47
+ if (Test-Path -LiteralPath $stopFile) {
48
+ Remove-Item -LiteralPath $stopFile -Force -ErrorAction SilentlyContinue
49
+ exit 0
50
+ }
41
51
  if ($uptimeSeconds -lt 60) { $fastExits++ } else { $fastExits = 0 }
42
52
  $delay = [int][Math]::Min(60, 5 * [Math]::Pow(2, [Math]::Min($fastExits, 4)))
43
- Start-Sleep -Seconds $delay
53
+ for ($i = 0; $i -lt $delay * 2; $i++) {
54
+ if (Test-Path -LiteralPath $stopFile) {
55
+ Remove-Item -LiteralPath $stopFile -Force -ErrorAction SilentlyContinue
56
+ exit 0
57
+ }
58
+ Start-Sleep -Milliseconds 500
59
+ }
44
60
  }
@@ -4,6 +4,7 @@ $ErrorActionPreference = 'Stop'
4
4
  $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
5
5
  $projectDir = Split-Path -Parent $scriptDir
6
6
  $launcher = Join-Path $scriptDir 'run-hidden.vbs'
7
+ $stopFile = Join-Path $env:LOCALAPPDATA 'Claude Code Discord Presence\stop.request'
7
8
  $entryPoint = Join-Path $projectDir 'dist\index.js'
8
9
  $name = 'ClaudeCodeDiscordPresence'
9
10
  $runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
@@ -20,6 +21,7 @@ if (-not (Test-Path -LiteralPath $entryPoint)) {
20
21
 
21
22
  $command = "wscript.exe `"$launcher`""
22
23
  Set-ItemProperty -Path $runKey -Name $name -Value $command
24
+ Remove-Item -LiteralPath $stopFile -Force -ErrorAction SilentlyContinue
23
25
 
24
26
  $escapedEntryPoint = [WildcardPattern]::Escape($entryPoint)
25
27
  $existing = Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -ErrorAction SilentlyContinue |