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,24 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * Returns true when Codex running inside a container has a plausible auth source:
7
+ * - explicit OpenAI auth env vars, or
8
+ * - known auth files under ~/.codex that can be copied into the container.
9
+ */
10
+ export function hasContainerCodexAuth(envVars?: Record<string, string>): boolean {
11
+ if (
12
+ !!envVars?.OPENAI_API_KEY
13
+ || !!envVars?.CODEX_API_KEY
14
+ ) {
15
+ return true;
16
+ }
17
+
18
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
19
+ const candidates = [
20
+ join(home, ".codex", "auth.json"),
21
+ ];
22
+
23
+ return candidates.some((p) => existsSync(p));
24
+ }
@@ -0,0 +1,27 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import { HEYHANK_HOME } from "./paths.js";
4
+
5
+ export const DEFAULT_HEYHANK_CODEX_HOME = join(
6
+ HEYHANK_HOME,
7
+ "codex-home",
8
+ );
9
+
10
+ export function getLegacyCodexHome(): string {
11
+ return join(homedir(), ".codex");
12
+ }
13
+
14
+ export function resolveHeyHankCodexHome(explicitCodexHome?: string): string {
15
+ // Intentionally do NOT fall back to process.env.CODEX_HOME here.
16
+ // That env var points to the user's global Codex home (~/.codex), which
17
+ // would break per-session isolation by nesting session dirs inside it.
18
+ return resolve(explicitCodexHome || DEFAULT_HEYHANK_CODEX_HOME);
19
+ }
20
+
21
+ export function resolveHeyHankCodexSessionHome(
22
+ sessionId: string,
23
+ explicitCodexHome?: string,
24
+ ): string {
25
+ return join(resolveHeyHankCodexHome(explicitCodexHome), sessionId);
26
+ }
27
+
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Bridges newline-delimited JSON on stdin/stdout to WebSocket text frames for
5
+ // Codex app-server. Runs in real Node (not Bun) so the `ws` package handles the
6
+ // Codex Rust server handshake correctly with perMessageDeflate disabled.
7
+
8
+ const readline = require("node:readline");
9
+ const WebSocket = require("ws");
10
+
11
+ const url = process.argv[2];
12
+ const timeoutMs = Number(process.argv[3] || "30000");
13
+ const pongTimeoutArg = process.argv[4];
14
+
15
+ if (!url) {
16
+ process.stderr.write("[codex-ws-proxy] Missing URL argument\n");
17
+ process.exit(2);
18
+ }
19
+
20
+ let ws = null;
21
+ let opened = false;
22
+ let closed = false;
23
+ let exiting = false;
24
+ let queue = [];
25
+ let connectAttempt = 0;
26
+ const startedAt = Date.now();
27
+
28
+ // Reconnection state — after a successful initial connection, transient
29
+ // WebSocket drops are retried with exponential backoff before giving up.
30
+ const MAX_RECONNECT_ATTEMPTS = 10;
31
+ const RECONNECT_BASE_MS = 200;
32
+ const RECONNECT_MAX_MS = 5000;
33
+ let reconnecting = false;
34
+ let reconnectAttempt = 0;
35
+
36
+ // Heartbeat — detect zombie WebSocket connections where the TCP socket is open
37
+ // but the remote Codex process has stopped responding.
38
+ const PING_INTERVAL_MS = 30000;
39
+ const PONG_TIMEOUT_MS = pongTimeoutArg ? Number(pongTimeoutArg) : 30000;
40
+ let pingTimer = null;
41
+ let pongTimer = null;
42
+
43
+ function log(msg) {
44
+ process.stderr.write(`[codex-ws-proxy] ${msg}\n`);
45
+ }
46
+
47
+ function startHeartbeat() {
48
+ stopHeartbeat();
49
+ pingTimer = setInterval(() => {
50
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
51
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
52
+ ws.ping();
53
+ pongTimer = setTimeout(() => {
54
+ log("Pong timeout — connection appears dead");
55
+ try { ws.terminate(); } catch {}
56
+ // terminate() fires the close event which triggers scheduleReconnect
57
+ }, PONG_TIMEOUT_MS);
58
+ }, PING_INTERVAL_MS);
59
+ }
60
+
61
+ function stopHeartbeat() {
62
+ if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
63
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
64
+ }
65
+
66
+ function decodeMessageData(data) {
67
+ if (typeof data === "string") return data;
68
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
69
+ if (Array.isArray(data)) return Buffer.concat(data.map((x) => Buffer.from(x))).toString("utf8");
70
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
71
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
72
+ return String(data);
73
+ }
74
+
75
+ function flushQueue() {
76
+ if (!ws || ws.readyState !== WebSocket.OPEN || queue.length === 0) return;
77
+ for (const line of queue) {
78
+ ws.send(line);
79
+ }
80
+ queue = [];
81
+ }
82
+
83
+ function failAndExit(message, code = 1) {
84
+ if (exiting) return;
85
+ exiting = true;
86
+ stopHeartbeat();
87
+ log(message);
88
+ try { if (ws) ws.close(); } catch {}
89
+ process.exit(code);
90
+ }
91
+
92
+ /**
93
+ * Attempt to reconnect after a post-open WebSocket drop.
94
+ * Uses exponential backoff up to MAX_RECONNECT_ATTEMPTS before giving up.
95
+ */
96
+ function scheduleReconnect(reason) {
97
+ if (closed || exiting) return;
98
+ stopHeartbeat();
99
+ reconnectAttempt++;
100
+ if (reconnectAttempt > MAX_RECONNECT_ATTEMPTS) {
101
+ failAndExit(`WebSocket reconnection failed after ${MAX_RECONNECT_ATTEMPTS} attempts (last: ${reason})`);
102
+ return;
103
+ }
104
+
105
+ reconnecting = true;
106
+ const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt - 1), RECONNECT_MAX_MS);
107
+ log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempt}/${MAX_RECONNECT_ATTEMPTS}) — ${reason}`);
108
+ setTimeout(connect, delay);
109
+ }
110
+
111
+ function connect() {
112
+ if (closed || exiting) return;
113
+
114
+ // During initial connection (before first successful open), enforce timeout.
115
+ if (!opened) {
116
+ connectAttempt += 1;
117
+ const elapsed = Date.now() - startedAt;
118
+ if (elapsed > timeoutMs) {
119
+ failAndExit(`Failed to connect within ${timeoutMs}ms`);
120
+ return;
121
+ }
122
+ }
123
+
124
+ ws = new WebSocket(url, { perMessageDeflate: false });
125
+
126
+ ws.once("open", () => {
127
+ if (!opened) {
128
+ opened = true;
129
+ }
130
+ const wasReconnect = reconnecting;
131
+ if (reconnecting) {
132
+ log(`Reconnected successfully (attempt ${reconnectAttempt})`);
133
+ reconnecting = false;
134
+ reconnectAttempt = 0;
135
+ }
136
+ startHeartbeat();
137
+ flushQueue();
138
+ // Notify the adapter AFTER flushing any buffered messages so stale Codex
139
+ // responses from the pre-drop session are delivered before the adapter
140
+ // rejects all pending calls and cleans up.
141
+ if (wasReconnect) {
142
+ const reconnectNotification = JSON.stringify({
143
+ method: "companion/wsReconnected",
144
+ params: {},
145
+ });
146
+ process.stdout.write(reconnectNotification + "\n");
147
+ }
148
+ });
149
+
150
+ ws.on("message", (data) => {
151
+ const raw = decodeMessageData(data);
152
+ // stdout is protocol channel: ONLY write payload lines
153
+ process.stdout.write(raw + "\n");
154
+ });
155
+
156
+ ws.on("pong", () => {
157
+ // Heartbeat response received — connection is alive
158
+ if (pongTimer) { clearTimeout(pongTimer); pongTimer = null; }
159
+ });
160
+
161
+ ws.once("close", (code, reason) => {
162
+ stopHeartbeat();
163
+ if (closed || exiting) return;
164
+ const why = reason ? ` reason=${reason}` : "";
165
+ // If connection closes before we ever opened, keep retrying until timeout.
166
+ if (!opened) {
167
+ setTimeout(connect, Math.min(100 * connectAttempt, 500));
168
+ return;
169
+ }
170
+ // Post-open close — attempt reconnection with backoff
171
+ scheduleReconnect(`WebSocket closed (code=${code}${why})`);
172
+ });
173
+
174
+ ws.once("error", (err) => {
175
+ if (closed || exiting) return;
176
+ // Retry during startup; after a successful connection, use reconnect logic.
177
+ if (!opened) {
178
+ setTimeout(connect, Math.min(100 * connectAttempt, 500));
179
+ return;
180
+ }
181
+ // Post-open error — attempt reconnection with backoff
182
+ scheduleReconnect(`WebSocket error: ${err && err.message ? err.message : String(err)}`);
183
+ });
184
+ }
185
+
186
+ const rl = readline.createInterface({
187
+ input: process.stdin,
188
+ crlfDelay: Infinity,
189
+ });
190
+
191
+ rl.on("line", (line) => {
192
+ if (closed || exiting) return;
193
+ if (!line) return;
194
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
195
+ queue.push(line);
196
+ return;
197
+ }
198
+ ws.send(line);
199
+ });
200
+
201
+ rl.on("close", () => {
202
+ closed = true;
203
+ stopHeartbeat();
204
+ try {
205
+ if (ws && ws.readyState === WebSocket.OPEN) {
206
+ ws.close();
207
+ }
208
+ } catch {}
209
+ process.exit(0);
210
+ });
211
+
212
+ process.on("SIGINT", () => {
213
+ closed = true;
214
+ stopHeartbeat();
215
+ try { if (ws) ws.close(); } catch {}
216
+ process.exit(0);
217
+ });
218
+
219
+ process.on("SIGTERM", () => {
220
+ closed = true;
221
+ stopHeartbeat();
222
+ try { if (ws) ws.close(); } catch {}
223
+ process.exit(0);
224
+ });
225
+
226
+ connect();
@@ -0,0 +1,81 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * Discover slash commands and skills from the filesystem.
8
+ * Used to pre-populate session state before system.init arrives from the CLI.
9
+ *
10
+ * Commands come from:
11
+ * - ~/.claude/commands/*.md (user-level)
12
+ * - {cwd}/.claude/commands/*.md (project-level)
13
+ *
14
+ * Skills come from:
15
+ * - ~/.claude/skills/`*`/SKILL.md (user-level, directory name = slug)
16
+ *
17
+ * Returns sorted, deduplicated arrays. Never throws.
18
+ */
19
+ export async function discoverCommandsAndSkills(cwd?: string): Promise<{
20
+ slash_commands: string[];
21
+ skills: string[];
22
+ }> {
23
+ const commandSet = new Set<string>();
24
+ const skills: string[] = [];
25
+
26
+ const home = homedir();
27
+
28
+ // User-level commands: ~/.claude/commands/*.md
29
+ try {
30
+ const userCommandsDir = join(home, ".claude", "commands");
31
+ if (existsSync(userCommandsDir)) {
32
+ const entries = await readdir(userCommandsDir, { withFileTypes: true });
33
+ for (const e of entries) {
34
+ if (e.isFile() && e.name.endsWith(".md")) {
35
+ commandSet.add(e.name.replace(/\.md$/, ""));
36
+ }
37
+ }
38
+ }
39
+ } catch {
40
+ /* gracefully ignore */
41
+ }
42
+
43
+ // Project-level commands: {cwd}/.claude/commands/*.md
44
+ if (cwd) {
45
+ try {
46
+ const projectCommandsDir = join(cwd, ".claude", "commands");
47
+ if (existsSync(projectCommandsDir)) {
48
+ const entries = await readdir(projectCommandsDir, { withFileTypes: true });
49
+ for (const e of entries) {
50
+ if (e.isFile() && e.name.endsWith(".md")) {
51
+ commandSet.add(e.name.replace(/\.md$/, ""));
52
+ }
53
+ }
54
+ }
55
+ } catch {
56
+ /* gracefully ignore */
57
+ }
58
+ }
59
+
60
+ // User-level skills: ~/.claude/skills/*/SKILL.md
61
+ try {
62
+ const skillsDir = join(home, ".claude", "skills");
63
+ if (existsSync(skillsDir)) {
64
+ const entries = await readdir(skillsDir, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ if (!entry.isDirectory()) continue;
67
+ const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
68
+ if (existsSync(skillMdPath)) {
69
+ skills.push(entry.name);
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ /* gracefully ignore */
75
+ }
76
+
77
+ const slash_commands = Array.from(commandSet).sort();
78
+ skills.sort();
79
+
80
+ return { slash_commands, skills };
81
+ }
@@ -0,0 +1,7 @@
1
+ export const DEFAULT_PORT_DEV = 3457;
2
+ export const DEFAULT_PORT_PROD = 3456;
3
+
4
+ // Container port constants — shared between routes.ts and session-creation-service.ts
5
+ export const VSCODE_EDITOR_CONTAINER_PORT = 13337;
6
+ export const CODEX_APP_SERVER_CONTAINER_PORT = Number(process.env.HEYHANK_CODEX_CONTAINER_WS_PORT || process.env.COMPANION_CODEX_CONTAINER_WS_PORT || "4502");
7
+ export const NOVNC_CONTAINER_PORT = 6080;