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.
- package/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- 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;
|