heyhank 0.1.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 (199) hide show
  1. package/README.md +40 -0
  2. package/bin/cli.ts +168 -0
  3. package/bin/ctl.ts +528 -0
  4. package/bin/generate-token.ts +28 -0
  5. package/dist/apple-touch-icon.png +0 -0
  6. package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
  7. package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
  8. package/dist/assets/CronManager-DDbz-yiT.js +1 -0
  9. package/dist/assets/HelpPage-DMfkzERp.js +1 -0
  10. package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
  11. package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
  12. package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
  13. package/dist/assets/Playground-Fc5cdc5p.js +109 -0
  14. package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
  15. package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
  16. package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
  17. package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
  18. package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
  19. package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
  20. package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
  21. package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
  22. package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
  23. package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
  24. package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
  25. package/dist/assets/index-C8M_PUmX.css +32 -0
  26. package/dist/assets/index-CEqZnThB.js +204 -0
  27. package/dist/assets/sw-register-LSSpj6RU.js +1 -0
  28. package/dist/assets/time-ago-B6r_l9u1.js +1 -0
  29. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  30. package/dist/favicon-32-original.png +0 -0
  31. package/dist/favicon-32.png +0 -0
  32. package/dist/favicon.ico +0 -0
  33. package/dist/favicon.svg +8 -0
  34. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  35. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  36. package/dist/heyhank-mascot-poster.png +0 -0
  37. package/dist/heyhank-mascot.mp4 +0 -0
  38. package/dist/heyhank-mascot.webm +0 -0
  39. package/dist/icon-192-original.png +0 -0
  40. package/dist/icon-192.png +0 -0
  41. package/dist/icon-512-original.png +0 -0
  42. package/dist/icon-512.png +0 -0
  43. package/dist/index.html +21 -0
  44. package/dist/logo-192.png +0 -0
  45. package/dist/logo-512.png +0 -0
  46. package/dist/logo-codex.svg +14 -0
  47. package/dist/logo-docker.svg +4 -0
  48. package/dist/logo-original.png +0 -0
  49. package/dist/logo.png +0 -0
  50. package/dist/logo.svg +14 -0
  51. package/dist/manifest.json +24 -0
  52. package/dist/push-sw.js +34 -0
  53. package/dist/sw.js +1 -0
  54. package/dist/workbox-d2a0910a.js +1 -0
  55. package/package.json +109 -0
  56. package/server/agent-cron-migrator.ts +85 -0
  57. package/server/agent-executor.ts +357 -0
  58. package/server/agent-store.ts +185 -0
  59. package/server/agent-timeout.ts +107 -0
  60. package/server/agent-types.ts +122 -0
  61. package/server/ai-validation-settings.ts +37 -0
  62. package/server/ai-validator.ts +181 -0
  63. package/server/anthropic-provider-migration.ts +48 -0
  64. package/server/assistant-store.ts +272 -0
  65. package/server/auth-manager.ts +150 -0
  66. package/server/auto-approve.ts +153 -0
  67. package/server/auto-namer.ts +36 -0
  68. package/server/backend-adapter.ts +54 -0
  69. package/server/cache-headers.ts +61 -0
  70. package/server/calendar-service.ts +434 -0
  71. package/server/claude-adapter.ts +889 -0
  72. package/server/claude-container-auth.ts +30 -0
  73. package/server/claude-session-discovery.ts +157 -0
  74. package/server/claude-session-history.ts +410 -0
  75. package/server/cli-launcher.ts +1303 -0
  76. package/server/codex-adapter.ts +3027 -0
  77. package/server/codex-container-auth.ts +24 -0
  78. package/server/codex-home.ts +27 -0
  79. package/server/codex-ws-proxy.cjs +226 -0
  80. package/server/commands-discovery.ts +81 -0
  81. package/server/constants.ts +7 -0
  82. package/server/container-manager.ts +1053 -0
  83. package/server/cost-tracker.ts +222 -0
  84. package/server/cron-scheduler.ts +243 -0
  85. package/server/cron-store.ts +148 -0
  86. package/server/cron-types.ts +63 -0
  87. package/server/email-service.ts +354 -0
  88. package/server/env-manager.ts +161 -0
  89. package/server/event-bus-types.ts +75 -0
  90. package/server/event-bus.ts +124 -0
  91. package/server/execution-store.ts +170 -0
  92. package/server/federation/node-connection.ts +190 -0
  93. package/server/federation/node-manager.ts +366 -0
  94. package/server/federation/node-store.ts +86 -0
  95. package/server/federation/node-types.ts +121 -0
  96. package/server/fs-utils.ts +15 -0
  97. package/server/git-utils.ts +421 -0
  98. package/server/github-pr.ts +379 -0
  99. package/server/google-media.ts +342 -0
  100. package/server/image-pull-manager.ts +279 -0
  101. package/server/index.ts +491 -0
  102. package/server/internal-ai.ts +237 -0
  103. package/server/kill-switch.ts +99 -0
  104. package/server/llm-providers.ts +342 -0
  105. package/server/logger.ts +259 -0
  106. package/server/mcp-registry.ts +401 -0
  107. package/server/message-bus.ts +271 -0
  108. package/server/message-delivery.ts +128 -0
  109. package/server/metrics-collector.ts +350 -0
  110. package/server/metrics-types.ts +108 -0
  111. package/server/middleware/managed-auth.ts +195 -0
  112. package/server/novnc-proxy.ts +99 -0
  113. package/server/path-resolver.ts +186 -0
  114. package/server/paths.ts +13 -0
  115. package/server/pr-poller.ts +162 -0
  116. package/server/prompt-manager.ts +211 -0
  117. package/server/protocol/claude-upstream/README.md +19 -0
  118. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  119. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  120. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  121. package/server/protocol/codex-upstream/README.md +18 -0
  122. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  123. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  124. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  125. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  126. package/server/protocol-monitor.ts +50 -0
  127. package/server/provider-manager.ts +111 -0
  128. package/server/provider-registry.ts +393 -0
  129. package/server/push-notifications.ts +221 -0
  130. package/server/recorder.ts +374 -0
  131. package/server/recording-hub/compat-validator.ts +284 -0
  132. package/server/recording-hub/diagnostics.ts +299 -0
  133. package/server/recording-hub/hub-config.ts +19 -0
  134. package/server/recording-hub/hub-routes.ts +236 -0
  135. package/server/recording-hub/hub-store.ts +265 -0
  136. package/server/recording-hub/replay-adapter.ts +207 -0
  137. package/server/relay-client.ts +320 -0
  138. package/server/reminder-scheduler.ts +38 -0
  139. package/server/replay.ts +78 -0
  140. package/server/routes/agent-routes.ts +264 -0
  141. package/server/routes/assistant-routes.ts +90 -0
  142. package/server/routes/cron-routes.ts +103 -0
  143. package/server/routes/env-routes.ts +95 -0
  144. package/server/routes/federation-routes.ts +76 -0
  145. package/server/routes/fs-routes.ts +622 -0
  146. package/server/routes/git-routes.ts +97 -0
  147. package/server/routes/llm-routes.ts +166 -0
  148. package/server/routes/media-routes.ts +135 -0
  149. package/server/routes/metrics-routes.ts +13 -0
  150. package/server/routes/platform-routes.ts +1379 -0
  151. package/server/routes/prompt-routes.ts +67 -0
  152. package/server/routes/provider-routes.ts +109 -0
  153. package/server/routes/sandbox-routes.ts +127 -0
  154. package/server/routes/settings-routes.ts +285 -0
  155. package/server/routes/skills-routes.ts +100 -0
  156. package/server/routes/socialmedia-routes.ts +208 -0
  157. package/server/routes/system-routes.ts +228 -0
  158. package/server/routes/tailscale-routes.ts +22 -0
  159. package/server/routes/telephony-routes.ts +259 -0
  160. package/server/routes.ts +1379 -0
  161. package/server/sandbox-manager.ts +168 -0
  162. package/server/service.ts +718 -0
  163. package/server/session-creation-service.ts +457 -0
  164. package/server/session-git-info.ts +104 -0
  165. package/server/session-names.ts +67 -0
  166. package/server/session-orchestrator.ts +824 -0
  167. package/server/session-state-machine.ts +207 -0
  168. package/server/session-store.ts +146 -0
  169. package/server/session-types.ts +511 -0
  170. package/server/settings-manager.ts +149 -0
  171. package/server/shared-context.ts +157 -0
  172. package/server/socialmedia/adapter.ts +15 -0
  173. package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
  174. package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
  175. package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
  176. package/server/socialmedia/manager.ts +227 -0
  177. package/server/socialmedia/store.ts +98 -0
  178. package/server/socialmedia/types.ts +89 -0
  179. package/server/tailscale-manager.ts +451 -0
  180. package/server/telephony/audio-bridge.ts +331 -0
  181. package/server/telephony/call-manager.ts +457 -0
  182. package/server/telephony/call-types.ts +108 -0
  183. package/server/telephony/telephony-store.ts +119 -0
  184. package/server/terminal-manager.ts +240 -0
  185. package/server/update-checker.ts +192 -0
  186. package/server/usage-limits.ts +225 -0
  187. package/server/web-push.d.ts +51 -0
  188. package/server/worktree-tracker.ts +84 -0
  189. package/server/ws-auth.ts +41 -0
  190. package/server/ws-bridge-browser-ingest.ts +72 -0
  191. package/server/ws-bridge-browser.ts +112 -0
  192. package/server/ws-bridge-cli-ingest.ts +81 -0
  193. package/server/ws-bridge-codex.ts +266 -0
  194. package/server/ws-bridge-controls.ts +20 -0
  195. package/server/ws-bridge-persist.ts +66 -0
  196. package/server/ws-bridge-publish.ts +79 -0
  197. package/server/ws-bridge-replay.ts +61 -0
  198. package/server/ws-bridge-types.ts +121 -0
  199. package/server/ws-bridge.ts +1240 -0
@@ -0,0 +1,240 @@
1
+ import type { ServerWebSocket } from "bun";
2
+ import { existsSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import type { SocketData } from "./ws-bridge.js";
5
+
6
+ /** Bun's PTY terminal handle exposed on proc when spawned with `terminal` option */
7
+ interface BunTerminalHandle {
8
+ write(data: string): void;
9
+ resize(cols: number, rows: number): void;
10
+ close(): void;
11
+ }
12
+
13
+ interface TerminalInstance {
14
+ id: string;
15
+ cwd: string;
16
+ containerId?: string;
17
+ proc: ReturnType<typeof Bun.spawn>;
18
+ terminal: BunTerminalHandle;
19
+ browserSockets: Set<ServerWebSocket<SocketData>>;
20
+ cols: number;
21
+ rows: number;
22
+ orphanTimer: ReturnType<typeof setTimeout> | null;
23
+ }
24
+
25
+ function resolveShell(): string {
26
+ if (process.env.SHELL && existsSync(process.env.SHELL)) return process.env.SHELL;
27
+ if (existsSync("/bin/bash")) return "/bin/bash";
28
+ return "/bin/sh";
29
+ }
30
+
31
+ export class TerminalManager {
32
+ private instances = new Map<string, TerminalInstance>();
33
+
34
+ /** Spawn a terminal in the given directory (host or container). */
35
+ spawn(cwd: string, cols = 80, rows = 24, options?: { containerId?: string }): string {
36
+ const id = randomUUID();
37
+ const containerId = options?.containerId?.trim() || undefined;
38
+ const sockets = new Set<ServerWebSocket<SocketData>>();
39
+ const shell = resolveShell();
40
+ const cmd = containerId
41
+ ? [
42
+ "docker",
43
+ "exec",
44
+ "-i",
45
+ "-t",
46
+ "-w",
47
+ cwd,
48
+ containerId,
49
+ "sh",
50
+ "-lc",
51
+ "if command -v bash >/dev/null 2>&1; then exec bash -l; else exec sh -l; fi",
52
+ ]
53
+ : [shell, "-l"];
54
+
55
+ const proc = Bun.spawn(cmd, {
56
+ cwd: containerId ? undefined : cwd,
57
+ env: { ...process.env, TERM: "xterm-256color", CLAUDECODE: undefined },
58
+ terminal: {
59
+ cols,
60
+ rows,
61
+ data: (_terminal, data) => {
62
+ // Broadcast raw PTY output as binary to all connected browsers
63
+ for (const ws of sockets) {
64
+ try {
65
+ ws.sendBinary(data);
66
+ } catch {
67
+ // socket may have closed
68
+ }
69
+ }
70
+ },
71
+ exit: () => {
72
+ // PTY stream closed — get exit code from proc
73
+ const inst = this.instances.get(id);
74
+ if (inst) {
75
+ const exitMsg = JSON.stringify({ type: "exit", exitCode: proc.exitCode ?? 0 });
76
+ for (const ws of inst.browserSockets) {
77
+ try {
78
+ ws.send(exitMsg);
79
+ } catch {
80
+ // socket may have closed
81
+ }
82
+ }
83
+ }
84
+ },
85
+ },
86
+ });
87
+
88
+ // Extract the terminal handle from the proc — Bun attaches it when spawned with `terminal` option
89
+ const terminal = (proc as any).terminal as BunTerminalHandle;
90
+ this.instances.set(id, {
91
+ id,
92
+ cwd,
93
+ containerId,
94
+ proc,
95
+ terminal,
96
+ browserSockets: sockets,
97
+ cols,
98
+ rows,
99
+ orphanTimer: null,
100
+ });
101
+ console.log(
102
+ `[terminal] Spawned terminal ${id} in ${cwd}${containerId ? ` (container ${containerId.slice(0, 12)})` : ""} (${containerId ? "docker-shell" : shell}, ${cols}x${rows})`,
103
+ );
104
+
105
+ // Handle process exit
106
+ proc.exited.then((exitCode) => {
107
+ const inst = this.instances.get(id);
108
+ if (!inst) return;
109
+ console.log(`[terminal] Terminal ${id} exited with code ${exitCode}`);
110
+ this.cleanupInstance(id);
111
+ });
112
+
113
+ return id;
114
+ }
115
+
116
+ private getTerminalIdFromSocket(ws: ServerWebSocket<SocketData>): string | null {
117
+ const data = ws.data;
118
+ if (data.kind !== "terminal") return null;
119
+ return data.terminalId;
120
+ }
121
+
122
+ private cleanupInstance(terminalId: string): void {
123
+ const inst = this.instances.get(terminalId);
124
+ if (!inst) return;
125
+ if (inst.orphanTimer) clearTimeout(inst.orphanTimer);
126
+ this.instances.delete(terminalId);
127
+ }
128
+
129
+ /** Handle a message from a browser WebSocket */
130
+ handleBrowserMessage(ws: ServerWebSocket<SocketData>, msg: string | Buffer): void {
131
+ const terminalId = this.getTerminalIdFromSocket(ws);
132
+ if (!terminalId) return;
133
+ const inst = this.instances.get(terminalId);
134
+ if (!inst) return;
135
+ try {
136
+ const str = typeof msg === "string" ? msg : msg.toString();
137
+ const parsed = JSON.parse(str);
138
+ if (parsed.type === "input" && typeof parsed.data === "string") {
139
+ inst.terminal.write(parsed.data);
140
+ } else if (parsed.type === "resize" && typeof parsed.cols === "number" && typeof parsed.rows === "number") {
141
+ this.resize(terminalId, parsed.cols, parsed.rows);
142
+ }
143
+ } catch {
144
+ // Malformed message, ignore
145
+ }
146
+ }
147
+
148
+ /** Resize the PTY */
149
+ resize(terminalId: string, cols: number, rows: number): void {
150
+ const inst = this.instances.get(terminalId);
151
+ if (!inst) return;
152
+ inst.cols = cols;
153
+ inst.rows = rows;
154
+ try {
155
+ inst.terminal.resize(cols, rows);
156
+ } catch {
157
+ // resize not available or failed
158
+ }
159
+ }
160
+
161
+ private killInstance(inst: TerminalInstance): void {
162
+ if (inst.orphanTimer) clearTimeout(inst.orphanTimer);
163
+ this.instances.delete(inst.id);
164
+
165
+ try {
166
+ inst.proc.kill();
167
+ } catch {
168
+ // process may have already exited
169
+ }
170
+
171
+ // SIGKILL fallback if SIGTERM doesn't work within 2 seconds
172
+ const pid = inst.proc.pid;
173
+ setTimeout(() => {
174
+ try {
175
+ process.kill(pid, 0); // check if still alive
176
+ inst.proc.kill(9); // SIGKILL
177
+ } catch {
178
+ // already dead, good
179
+ }
180
+ }, 2_000);
181
+
182
+ console.log(`[terminal] Killed terminal ${inst.id}`);
183
+ }
184
+
185
+ /** Kill one terminal instance. */
186
+ kill(terminalId: string): void {
187
+ const inst = this.instances.get(terminalId);
188
+ if (!inst) return;
189
+ this.killInstance(inst);
190
+ }
191
+
192
+ /** Get current terminal info */
193
+ getInfo(terminalId?: string): { id: string; cwd: string; containerId?: string } | null {
194
+ if (terminalId) {
195
+ const inst = this.instances.get(terminalId);
196
+ if (!inst) return null;
197
+ return { id: inst.id, cwd: inst.cwd, containerId: inst.containerId };
198
+ }
199
+ const first = this.instances.values().next().value as TerminalInstance | undefined;
200
+ if (!first) return null;
201
+ return { id: first.id, cwd: first.cwd, containerId: first.containerId };
202
+ }
203
+
204
+ /** Attach a browser WebSocket to the terminal */
205
+ addBrowserSocket(ws: ServerWebSocket<SocketData>): void {
206
+ const terminalId = this.getTerminalIdFromSocket(ws);
207
+ if (!terminalId) return;
208
+ const inst = this.instances.get(terminalId);
209
+ if (!inst) return;
210
+
211
+ // Cancel orphan kill timer if any
212
+ if (inst.orphanTimer) {
213
+ clearTimeout(inst.orphanTimer);
214
+ inst.orphanTimer = null;
215
+ }
216
+
217
+ inst.browserSockets.add(ws);
218
+ }
219
+
220
+ /** Remove a browser WebSocket from the terminal */
221
+ removeBrowserSocket(ws: ServerWebSocket<SocketData>): void {
222
+ const terminalId = this.getTerminalIdFromSocket(ws);
223
+ if (!terminalId) return;
224
+ const inst = this.instances.get(terminalId);
225
+ if (!inst) return;
226
+ inst.browserSockets.delete(ws);
227
+
228
+ // If no browsers remain, start a grace timer to kill the orphaned terminal
229
+ if (inst.browserSockets.size === 0) {
230
+ const id = inst.id;
231
+ inst.orphanTimer = setTimeout(() => {
232
+ const alive = this.instances.get(id);
233
+ if (alive && alive.browserSockets.size === 0) {
234
+ console.log(`[terminal] No browsers connected, killing orphaned terminal ${id}`);
235
+ this.kill(id);
236
+ }
237
+ }, 5_000);
238
+ }
239
+ }
240
+ }
@@ -0,0 +1,192 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getSettings, type UpdateChannel } from "./settings-manager.js";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ // Read current version from package.json
9
+ const packageJsonPath = resolve(__dirname, "..", "package.json");
10
+ const currentVersion: string = JSON.parse(
11
+ readFileSync(packageJsonPath, "utf-8"),
12
+ ).version;
13
+
14
+ const NPM_REGISTRY_BASE = "https://registry.npmjs.org/heyhank";
15
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
16
+ const INITIAL_DELAY_MS = 10_000; // 10 seconds after boot
17
+
18
+ interface UpdateState {
19
+ currentVersion: string;
20
+ latestVersion: string | null;
21
+ lastChecked: number;
22
+ isServiceMode: boolean;
23
+ checking: boolean;
24
+ updateInProgress: boolean;
25
+ channel: UpdateChannel;
26
+ }
27
+
28
+ const state: UpdateState = {
29
+ currentVersion,
30
+ latestVersion: null,
31
+ lastChecked: 0,
32
+ isServiceMode: false,
33
+ checking: false,
34
+ updateInProgress: false,
35
+ channel: "stable",
36
+ };
37
+
38
+ export function getUpdateState(): Readonly<UpdateState> {
39
+ return { ...state };
40
+ }
41
+
42
+ export function getCurrentVersion(): string {
43
+ return currentVersion;
44
+ }
45
+
46
+ /** Returns the npm registry URL for the given dist-tag. */
47
+ function getRegistryUrl(channel: UpdateChannel): string {
48
+ const distTag = channel === "prerelease" ? "next" : "latest";
49
+ return `${NPM_REGISTRY_BASE}/${distTag}`;
50
+ }
51
+
52
+ export async function checkForUpdate(): Promise<void> {
53
+ if (state.checking) return;
54
+ state.checking = true;
55
+ try {
56
+ // Read channel from settings on each check so switching is immediate
57
+ const channel = getSettings().updateChannel;
58
+ if (channel !== state.channel) {
59
+ state.latestVersion = null; // avoid cross-channel stale comparison
60
+ }
61
+ state.channel = channel;
62
+ const url = getRegistryUrl(channel);
63
+
64
+ const res = await fetch(url, {
65
+ headers: { Accept: "application/json" },
66
+ signal: AbortSignal.timeout(10_000),
67
+ });
68
+ if (res.ok) {
69
+ const data = (await res.json()) as { version: string };
70
+ state.latestVersion = data.version;
71
+ state.lastChecked = Date.now();
72
+ if (isUpdateAvailable()) {
73
+ console.log(
74
+ `[update-checker] Update available (${channel}): ${currentVersion} -> ${state.latestVersion}`,
75
+ );
76
+ }
77
+ }
78
+ } catch (err) {
79
+ console.warn(
80
+ "[update-checker] Failed to check for updates:",
81
+ err instanceof Error ? err.message : String(err),
82
+ );
83
+ } finally {
84
+ state.checking = false;
85
+ }
86
+ }
87
+
88
+ export function setServiceMode(isService: boolean): void {
89
+ state.isServiceMode = isService;
90
+ }
91
+
92
+ export function setUpdateInProgress(inProgress: boolean): void {
93
+ state.updateInProgress = inProgress;
94
+ }
95
+
96
+ export function isUpdateAvailable(): boolean {
97
+ if (!state.latestVersion || !state.currentVersion) return false;
98
+ return isNewerVersion(state.latestVersion, state.currentVersion);
99
+ }
100
+
101
+ /**
102
+ * Parse a semver string into its components.
103
+ * Handles versions like "1.2.3", "1.2.3-preview.20260228120000.abc1234"
104
+ */
105
+ function parseSemver(v: string): { major: number; minor: number; patch: number; prerelease: string[] } {
106
+ const [corePart, ...prereleaseParts] = v.split("-");
107
+ const prerelease = prereleaseParts.length > 0 ? prereleaseParts.join("-").split(".") : [];
108
+ const parts = corePart.split(".").map(Number);
109
+ return {
110
+ major: parts[0] || 0,
111
+ minor: parts[1] || 0,
112
+ patch: parts[2] || 0,
113
+ prerelease,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Compare two semver prerelease identifier arrays.
119
+ * Returns -1 if a < b, 0 if a == b, 1 if a > b.
120
+ * A version with no prerelease identifiers has higher precedence than one with.
121
+ */
122
+ function comparePrereleaseArrays(a: string[], b: string[]): number {
123
+ // No prerelease on both = equal
124
+ if (a.length === 0 && b.length === 0) return 0;
125
+ // No prerelease > has prerelease (stable is newer than prerelease of same core version)
126
+ if (a.length === 0) return 1;
127
+ if (b.length === 0) return -1;
128
+
129
+ const maxLen = Math.max(a.length, b.length);
130
+ for (let i = 0; i < maxLen; i++) {
131
+ // Fewer fields = lower precedence
132
+ if (i >= a.length) return -1;
133
+ if (i >= b.length) return 1;
134
+
135
+ const aNum = Number(a[i]);
136
+ const bNum = Number(b[i]);
137
+ const aIsNum = !isNaN(aNum);
138
+ const bIsNum = !isNaN(bNum);
139
+
140
+ if (aIsNum && bIsNum) {
141
+ if (aNum > bNum) return 1;
142
+ if (aNum < bNum) return -1;
143
+ } else if (aIsNum) {
144
+ // Numeric identifiers have lower precedence than alphanumeric
145
+ return -1;
146
+ } else if (bIsNum) {
147
+ return 1;
148
+ } else {
149
+ // Both alphanumeric: compare lexically
150
+ if (a[i] > b[i]) return 1;
151
+ if (a[i] < b[i]) return -1;
152
+ }
153
+ }
154
+ return 0;
155
+ }
156
+
157
+ /**
158
+ * Prerelease-aware semver comparison: returns true if a > b.
159
+ * Handles both stable versions (1.2.3) and prerelease versions
160
+ * (1.2.3-preview.20260228120000.abc1234).
161
+ */
162
+ export function isNewerVersion(a: string, b: string): boolean {
163
+ const pa = parseSemver(a);
164
+ const pb = parseSemver(b);
165
+
166
+ // Compare major.minor.patch
167
+ if (pa.major !== pb.major) return pa.major > pb.major;
168
+ if (pa.minor !== pb.minor) return pa.minor > pb.minor;
169
+ if (pa.patch !== pb.patch) return pa.patch > pb.patch;
170
+
171
+ // Core versions are equal — compare prerelease
172
+ return comparePrereleaseArrays(pa.prerelease, pb.prerelease) > 0;
173
+ }
174
+
175
+ let intervalId: ReturnType<typeof setInterval> | null = null;
176
+
177
+ export function startPeriodicCheck(): void {
178
+ if (intervalId) return;
179
+ setTimeout(() => {
180
+ checkForUpdate();
181
+ }, INITIAL_DELAY_MS);
182
+ intervalId = setInterval(() => {
183
+ checkForUpdate();
184
+ }, CHECK_INTERVAL_MS);
185
+ }
186
+
187
+ export function stopPeriodicCheck(): void {
188
+ if (intervalId) {
189
+ clearInterval(intervalId);
190
+ intervalId = null;
191
+ }
192
+ }
@@ -0,0 +1,225 @@
1
+ import { execSync, execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ export interface UsageLimits {
7
+ five_hour: { utilization: number; resets_at: string | null } | null;
8
+ seven_day: { utilization: number; resets_at: string | null } | null;
9
+ extra_usage: {
10
+ is_enabled: boolean;
11
+ monthly_limit: number;
12
+ used_credits: number;
13
+ utilization: number | null;
14
+ } | null;
15
+ }
16
+
17
+ const OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
18
+ const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
19
+
20
+ // In-memory cache (60s TTL)
21
+ const CACHE_DURATION_MS = 60 * 1000;
22
+ let cache: { data: UsageLimits; timestamp: number } | null = null;
23
+
24
+ interface OAuthCredentials {
25
+ accessToken: string;
26
+ refreshToken: string;
27
+ expiresAt: number;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ interface RawCredentials {
32
+ raw: string;
33
+ parsed: Record<string, unknown>;
34
+ oauth: OAuthCredentials;
35
+ sourcePath?: string;
36
+ }
37
+
38
+ // Credential file candidates - matches claude-container-auth.ts
39
+ const CREDENTIAL_FILE_NAMES = [
40
+ ".credentials.json",
41
+ "auth.json",
42
+ ".auth.json",
43
+ "credentials.json",
44
+ ];
45
+
46
+ function readCredentialsFromFile(): RawCredentials | null {
47
+ const home =
48
+ process.env.USERPROFILE || process.env.HOME || homedir() || "";
49
+ const claudeDir = join(home, ".claude");
50
+
51
+ for (const fileName of CREDENTIAL_FILE_NAMES) {
52
+ const credPath = join(claudeDir, fileName);
53
+ if (!existsSync(credPath)) continue;
54
+ try {
55
+ const raw = readFileSync(credPath, "utf-8");
56
+ const parsed = JSON.parse(raw);
57
+ if (!parsed?.claudeAiOauth?.accessToken) continue;
58
+ return { raw, parsed, oauth: parsed.claudeAiOauth, sourcePath: credPath };
59
+ } catch {
60
+ continue;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function readRawCredentials(): RawCredentials | null {
67
+ try {
68
+ // macOS: use Keychain via security command
69
+ if (process.platform === "darwin") {
70
+ const raw = execSync(
71
+ 'security find-generic-password -s "Claude Code-credentials" -w',
72
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
73
+ ).trim();
74
+
75
+ const decoded = raw.startsWith("{")
76
+ ? raw
77
+ : Buffer.from(raw, "hex").toString("utf-8");
78
+
79
+ const parsed = JSON.parse(decoded);
80
+ if (!parsed?.claudeAiOauth?.accessToken) return null;
81
+ return { raw: decoded, parsed, oauth: parsed.claudeAiOauth };
82
+ }
83
+
84
+ // Windows, Linux, Docker, and other platforms: read from credential files
85
+ return readCredentialsFromFile();
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function writeCredentials(creds: Record<string, unknown>, sourcePath?: string): void {
92
+ try {
93
+ const json = JSON.stringify(creds);
94
+ if (process.platform === "darwin") {
95
+ execFileSync(
96
+ "security",
97
+ ["add-generic-password", "-U", "-s", "Claude Code-credentials", "-a", "Claude Code", "-w", json],
98
+ { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
99
+ );
100
+ } else {
101
+ // Write back to the same file that was read, or default to .credentials.json
102
+ const credPath = sourcePath ?? join(
103
+ process.env.USERPROFILE || process.env.HOME || homedir() || "",
104
+ ".claude",
105
+ ".credentials.json",
106
+ );
107
+ writeFileSync(credPath, json, "utf-8");
108
+ }
109
+ } catch {
110
+ // best-effort
111
+ }
112
+ }
113
+
114
+ async function refreshAccessToken(refreshToken: string): Promise<{
115
+ accessToken: string;
116
+ refreshToken: string;
117
+ expiresIn: number;
118
+ } | null> {
119
+ try {
120
+ const res = await fetch(OAUTH_TOKEN_URL, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
123
+ body: new URLSearchParams({
124
+ grant_type: "refresh_token",
125
+ refresh_token: refreshToken,
126
+ client_id: OAUTH_CLIENT_ID,
127
+ }),
128
+ });
129
+ if (!res.ok) return null;
130
+ const data = await res.json();
131
+ return {
132
+ accessToken: data.access_token,
133
+ refreshToken: data.refresh_token,
134
+ expiresIn: data.expires_in,
135
+ };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ export function getCredentials(): string | null {
142
+ const creds = readRawCredentials();
143
+ return creds?.oauth.accessToken ?? null;
144
+ }
145
+
146
+ async function getValidAccessToken(): Promise<string | null> {
147
+ const creds = readRawCredentials();
148
+ if (!creds) return null;
149
+
150
+ const { oauth } = creds;
151
+
152
+ // Token still valid (with 5min buffer)
153
+ if (oauth.expiresAt && Date.now() < oauth.expiresAt - 5 * 60 * 1000) {
154
+ return oauth.accessToken;
155
+ }
156
+
157
+ // Token expired - try to refresh
158
+ if (!oauth.refreshToken) return null;
159
+
160
+ const refreshed = await refreshAccessToken(oauth.refreshToken);
161
+ if (!refreshed) return null;
162
+
163
+ // Update stored credentials
164
+ creds.parsed.claudeAiOauth = {
165
+ ...oauth,
166
+ accessToken: refreshed.accessToken,
167
+ refreshToken: refreshed.refreshToken,
168
+ expiresAt: Date.now() + refreshed.expiresIn * 1000,
169
+ };
170
+ writeCredentials(creds.parsed, creds.sourcePath);
171
+
172
+ return refreshed.accessToken;
173
+ }
174
+
175
+ export async function fetchUsageLimits(
176
+ token: string,
177
+ ): Promise<UsageLimits | null> {
178
+ try {
179
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
180
+ method: "GET",
181
+ headers: {
182
+ Accept: "application/json",
183
+ "Content-Type": "application/json",
184
+ "User-Agent": "claude-code/2.1.39",
185
+ Authorization: `Bearer ${token}`,
186
+ "anthropic-beta": "oauth-2025-04-20",
187
+ },
188
+ });
189
+
190
+ if (!response.ok) return null;
191
+
192
+ const data = await response.json();
193
+ return {
194
+ five_hour: data.five_hour || null,
195
+ seven_day: data.seven_day || null,
196
+ extra_usage: data.extra_usage || null,
197
+ };
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
203
+ export async function getUsageLimits(): Promise<UsageLimits> {
204
+ const empty: UsageLimits = {
205
+ five_hour: null,
206
+ seven_day: null,
207
+ extra_usage: null,
208
+ };
209
+ try {
210
+ if (cache && Date.now() - cache.timestamp < CACHE_DURATION_MS) {
211
+ return cache.data;
212
+ }
213
+
214
+ const token = await getValidAccessToken();
215
+ if (!token) return empty;
216
+
217
+ const limits = await fetchUsageLimits(token);
218
+ if (!limits) return empty;
219
+
220
+ cache = { data: limits, timestamp: Date.now() };
221
+ return limits;
222
+ } catch {
223
+ return empty;
224
+ }
225
+ }
@@ -0,0 +1,51 @@
1
+ declare module "web-push" {
2
+ interface VapidKeys {
3
+ publicKey: string;
4
+ privateKey: string;
5
+ }
6
+
7
+ interface PushSubscription {
8
+ endpoint: string;
9
+ keys: {
10
+ p256dh: string;
11
+ auth: string;
12
+ };
13
+ }
14
+
15
+ interface SendOptions {
16
+ TTL?: number;
17
+ headers?: Record<string, string>;
18
+ vapidDetails?: {
19
+ subject: string;
20
+ publicKey: string;
21
+ privateKey: string;
22
+ };
23
+ }
24
+
25
+ interface SendResult {
26
+ statusCode: number;
27
+ body: string;
28
+ headers: Record<string, string>;
29
+ }
30
+
31
+ function generateVAPIDKeys(): VapidKeys;
32
+ function setVapidDetails(
33
+ subject: string,
34
+ publicKey: string,
35
+ privateKey: string,
36
+ ): void;
37
+ function sendNotification(
38
+ subscription: PushSubscription,
39
+ payload: string | Buffer | null,
40
+ options?: SendOptions,
41
+ ): Promise<SendResult>;
42
+
43
+ const webpush: {
44
+ generateVAPIDKeys: typeof generateVAPIDKeys;
45
+ setVapidDetails: typeof setVapidDetails;
46
+ sendNotification: typeof sendNotification;
47
+ };
48
+
49
+ export default webpush;
50
+ export { generateVAPIDKeys, setVapidDetails, sendNotification };
51
+ }