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,208 @@
|
|
|
1
|
+
// ─── Social Media Routes ─────────────────────────────────────────────────────
|
|
2
|
+
// REST API for social media posting, scheduling, analytics, and comments.
|
|
3
|
+
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import * as store from "../socialmedia/store.js";
|
|
6
|
+
import * as manager from "../socialmedia/manager.js";
|
|
7
|
+
|
|
8
|
+
export function registerSocialMediaRoutes(api: Hono): void {
|
|
9
|
+
// ─── Settings ───────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Get settings (API keys masked) */
|
|
12
|
+
api.get("/socialmedia/settings", (c) => {
|
|
13
|
+
try {
|
|
14
|
+
const settings = store.getSettings();
|
|
15
|
+
// Mask API keys
|
|
16
|
+
const masked = JSON.parse(JSON.stringify(settings));
|
|
17
|
+
for (const [, cfg] of Object.entries(masked.backends || {})) {
|
|
18
|
+
const conf = cfg as { apiKey?: string };
|
|
19
|
+
if (conf.apiKey) conf.apiKey = conf.apiKey.slice(0, 4) + "••••••••";
|
|
20
|
+
}
|
|
21
|
+
return c.json(masked);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Save settings (restore masked keys from existing) */
|
|
28
|
+
api.put("/socialmedia/settings", async (c) => {
|
|
29
|
+
try {
|
|
30
|
+
const body = await c.req.json();
|
|
31
|
+
const existing = store.getSettings();
|
|
32
|
+
// Restore masked keys from existing settings
|
|
33
|
+
for (const [key, cfg] of Object.entries(body.backends || {})) {
|
|
34
|
+
const conf = cfg as { apiKey?: string };
|
|
35
|
+
if (conf.apiKey && conf.apiKey.includes("••••")) {
|
|
36
|
+
const existingKey = (existing.backends as Record<string, { apiKey?: string }>)?.[key]?.apiKey;
|
|
37
|
+
if (existingKey) conf.apiKey = existingKey;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
store.saveSettings(body);
|
|
41
|
+
return c.json({ ok: true });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return c.json({ error: e instanceof Error ? e.message : "bad request" }, 400);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Connection ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Test backend connection */
|
|
50
|
+
api.post("/socialmedia/test-connection", async (c) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await manager.testConnection();
|
|
53
|
+
return c.json(result);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return c.json({ ok: false, error: e instanceof Error ? e.message : "failed" });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Get connected profiles */
|
|
60
|
+
api.get("/socialmedia/profiles", async (c) => {
|
|
61
|
+
try {
|
|
62
|
+
const profiles = await manager.getProfiles();
|
|
63
|
+
return c.json({ profiles });
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── Posts ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Create post */
|
|
72
|
+
api.post("/socialmedia/posts", async (c) => {
|
|
73
|
+
try {
|
|
74
|
+
const body = await c.req.json();
|
|
75
|
+
const text = (body.text || "").trim();
|
|
76
|
+
if (!text) return c.json({ error: "text required" }, 400);
|
|
77
|
+
const result = await manager.createPost({
|
|
78
|
+
text,
|
|
79
|
+
platforms: body.platforms || [],
|
|
80
|
+
scheduledAt: body.scheduledAt || null,
|
|
81
|
+
mediaUrls: body.mediaUrls || [],
|
|
82
|
+
});
|
|
83
|
+
return c.json(result, 201);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** List posts */
|
|
90
|
+
api.get("/socialmedia/posts", async (c) => {
|
|
91
|
+
const opts: { status?: string; platform?: string; limit?: number } = {};
|
|
92
|
+
const status = c.req.query("status");
|
|
93
|
+
const platform = c.req.query("platform");
|
|
94
|
+
const limit = c.req.query("limit");
|
|
95
|
+
if (status) opts.status = status;
|
|
96
|
+
if (platform) opts.platform = platform;
|
|
97
|
+
if (limit) opts.limit = parseInt(limit, 10);
|
|
98
|
+
const posts = await manager.listPosts(opts);
|
|
99
|
+
return c.json({ posts });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** Get single post */
|
|
103
|
+
api.get("/socialmedia/posts/:id", async (c) => {
|
|
104
|
+
const post = await manager.getPost(c.req.param("id"));
|
|
105
|
+
if (!post) return c.json({ error: "post not found" }, 404);
|
|
106
|
+
return c.json(post);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Update/reschedule post */
|
|
110
|
+
api.patch("/socialmedia/posts/:id", async (c) => {
|
|
111
|
+
try {
|
|
112
|
+
const id = c.req.param("id");
|
|
113
|
+
const body = await c.req.json();
|
|
114
|
+
const post = store.getPost(id);
|
|
115
|
+
if (!post) return c.json({ error: "post not found" }, 404);
|
|
116
|
+
if (body.text !== undefined) post.text = body.text;
|
|
117
|
+
if (body.scheduledAt !== undefined) post.scheduledAt = body.scheduledAt;
|
|
118
|
+
if (body.platforms !== undefined) post.platforms = body.platforms;
|
|
119
|
+
if (body.scheduledAt && post.status === "published") post.status = "scheduled";
|
|
120
|
+
store.savePost(post);
|
|
121
|
+
return c.json(post);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return c.json({ error: e instanceof Error ? e.message : "bad request" }, 400);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** Publish draft */
|
|
128
|
+
api.post("/socialmedia/posts/:id/publish", async (c) => {
|
|
129
|
+
try {
|
|
130
|
+
const smManager = await import("../socialmedia/manager.js");
|
|
131
|
+
const post = await smManager.publishDraft(c.req.param("id"));
|
|
132
|
+
return c.json(post);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to publish" }, 400);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** Delete post */
|
|
139
|
+
api.delete("/socialmedia/posts/:id", async (c) => {
|
|
140
|
+
const ok = await manager.deletePost(c.req.param("id"));
|
|
141
|
+
return c.json({ ok }, ok ? 200 : 404);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/** Get post analytics */
|
|
145
|
+
api.get("/socialmedia/posts/:id/analytics", async (c) => {
|
|
146
|
+
try {
|
|
147
|
+
const analytics = await manager.getPostAnalytics(c.req.param("id"));
|
|
148
|
+
return c.json(analytics);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/** Get post comments */
|
|
155
|
+
api.get("/socialmedia/posts/:id/comments", async (c) => {
|
|
156
|
+
try {
|
|
157
|
+
const comments = await manager.getComments(c.req.param("id"));
|
|
158
|
+
return c.json({ comments });
|
|
159
|
+
} catch (e) {
|
|
160
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** Reply to comment */
|
|
165
|
+
api.post("/socialmedia/posts/:id/comments", async (c) => {
|
|
166
|
+
try {
|
|
167
|
+
const body = await c.req.json();
|
|
168
|
+
const text = (body.text || "").trim();
|
|
169
|
+
if (!text) return c.json({ error: "text required" }, 400);
|
|
170
|
+
const result = await manager.replyToComment(c.req.param("id"), body.commentId || null, text);
|
|
171
|
+
return c.json(result);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ─── Calendar View ──────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** Get posts grouped by day for a month */
|
|
180
|
+
api.get("/socialmedia/calendar", (c) => {
|
|
181
|
+
try {
|
|
182
|
+
const month = c.req.query("month") || new Date().toISOString().slice(0, 7);
|
|
183
|
+
const allPosts = store.listLocalPosts({});
|
|
184
|
+
const grouped: Record<string, typeof allPosts> = {};
|
|
185
|
+
for (const p of allPosts) {
|
|
186
|
+
const dateStr = (p.scheduledAt || p.createdAt || "").slice(0, 10);
|
|
187
|
+
if (!dateStr.startsWith(month)) continue;
|
|
188
|
+
if (!grouped[dateStr]) grouped[dateStr] = [];
|
|
189
|
+
grouped[dateStr].push(p);
|
|
190
|
+
}
|
|
191
|
+
return c.json({ month, days: grouped });
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ─── Account Analytics ──────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** Get account analytics for a profile */
|
|
200
|
+
api.get("/socialmedia/analytics/:profileId", async (c) => {
|
|
201
|
+
try {
|
|
202
|
+
const analytics = await manager.getAccountAnalytics(c.req.param("profileId"));
|
|
203
|
+
return c.json(analytics);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
return c.json({ error: e instanceof Error ? e.message : "failed" }, 500);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { CliLauncher } from "../cli-launcher.js";
|
|
3
|
+
import type { WsBridge } from "../ws-bridge.js";
|
|
4
|
+
import type { TerminalManager } from "../terminal-manager.js";
|
|
5
|
+
import { getUsageLimits } from "../usage-limits.js";
|
|
6
|
+
import {
|
|
7
|
+
getUpdateState,
|
|
8
|
+
checkForUpdate,
|
|
9
|
+
isUpdateAvailable,
|
|
10
|
+
setUpdateInProgress,
|
|
11
|
+
} from "../update-checker.js";
|
|
12
|
+
import { refreshServiceDefinition } from "../service.js";
|
|
13
|
+
import { getSettings } from "../settings-manager.js";
|
|
14
|
+
import { imagePullManager } from "../image-pull-manager.js";
|
|
15
|
+
|
|
16
|
+
export function registerSystemRoutes(
|
|
17
|
+
api: Hono,
|
|
18
|
+
deps: {
|
|
19
|
+
launcher: CliLauncher;
|
|
20
|
+
wsBridge: WsBridge;
|
|
21
|
+
terminalManager: TerminalManager;
|
|
22
|
+
updateCheckStaleMs: number;
|
|
23
|
+
},
|
|
24
|
+
): void {
|
|
25
|
+
api.get("/usage-limits", async (c) => {
|
|
26
|
+
const limits = await getUsageLimits();
|
|
27
|
+
return c.json(limits);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
api.get("/sessions/:id/usage-limits", async (c) => {
|
|
31
|
+
const sessionId = c.req.param("id");
|
|
32
|
+
const session = deps.wsBridge.getSession(sessionId);
|
|
33
|
+
const empty = { five_hour: null, seven_day: null, extra_usage: null };
|
|
34
|
+
|
|
35
|
+
if (session?.backendType === "codex") {
|
|
36
|
+
const rl = deps.wsBridge.getCodexRateLimits(sessionId);
|
|
37
|
+
if (!rl) return c.json(empty);
|
|
38
|
+
const toEpochMs = (value: number): number => (
|
|
39
|
+
value > 0 && value < 1_000_000_000_000 ? value * 1000 : value
|
|
40
|
+
);
|
|
41
|
+
const mapLimit = (l: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null) => {
|
|
42
|
+
if (!l) return null;
|
|
43
|
+
const resetsAtMs = toEpochMs(l.resetsAt);
|
|
44
|
+
return {
|
|
45
|
+
utilization: l.usedPercent,
|
|
46
|
+
resets_at: resetsAtMs ? new Date(resetsAtMs).toISOString() : null,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
return c.json({
|
|
50
|
+
five_hour: mapLimit(rl.primary),
|
|
51
|
+
seven_day: mapLimit(rl.secondary),
|
|
52
|
+
extra_usage: null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const limits = await getUsageLimits();
|
|
57
|
+
return c.json(limits);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
api.get("/update-check", async (c) => {
|
|
61
|
+
const initialState = getUpdateState();
|
|
62
|
+
const needsRefresh =
|
|
63
|
+
initialState.lastChecked === 0
|
|
64
|
+
|| Date.now() - initialState.lastChecked > deps.updateCheckStaleMs;
|
|
65
|
+
if (needsRefresh) {
|
|
66
|
+
await checkForUpdate();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const state = getUpdateState();
|
|
70
|
+
return c.json({
|
|
71
|
+
currentVersion: state.currentVersion,
|
|
72
|
+
latestVersion: state.latestVersion,
|
|
73
|
+
updateAvailable: isUpdateAvailable(),
|
|
74
|
+
isServiceMode: state.isServiceMode,
|
|
75
|
+
updateInProgress: state.updateInProgress,
|
|
76
|
+
lastChecked: state.lastChecked,
|
|
77
|
+
channel: state.channel,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
api.post("/update-check", async (c) => {
|
|
82
|
+
await checkForUpdate();
|
|
83
|
+
const state = getUpdateState();
|
|
84
|
+
return c.json({
|
|
85
|
+
currentVersion: state.currentVersion,
|
|
86
|
+
latestVersion: state.latestVersion,
|
|
87
|
+
updateAvailable: isUpdateAvailable(),
|
|
88
|
+
isServiceMode: state.isServiceMode,
|
|
89
|
+
updateInProgress: state.updateInProgress,
|
|
90
|
+
lastChecked: state.lastChecked,
|
|
91
|
+
channel: state.channel,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
api.post("/update", async (c) => {
|
|
96
|
+
const state = getUpdateState();
|
|
97
|
+
if (!state.isServiceMode) {
|
|
98
|
+
return c.json(
|
|
99
|
+
{ error: "Update & restart is only available in service mode" },
|
|
100
|
+
400,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (!isUpdateAvailable()) {
|
|
104
|
+
return c.json({ error: "No update available" }, 400);
|
|
105
|
+
}
|
|
106
|
+
if (state.updateInProgress) {
|
|
107
|
+
return c.json({ error: "Update already in progress" }, 409);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setUpdateInProgress(true);
|
|
111
|
+
|
|
112
|
+
setTimeout(async () => {
|
|
113
|
+
try {
|
|
114
|
+
console.log(
|
|
115
|
+
`[update] Updating heyhank to ${state.latestVersion}...`,
|
|
116
|
+
);
|
|
117
|
+
const proc = Bun.spawn(
|
|
118
|
+
["bun", "install", "-g", `heyhank@${state.latestVersion}`],
|
|
119
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
120
|
+
);
|
|
121
|
+
const exitCode = await proc.exited;
|
|
122
|
+
if (exitCode !== 0) {
|
|
123
|
+
const stderr = await new Response(proc.stderr).text();
|
|
124
|
+
console.error(
|
|
125
|
+
`[update] bun install failed (code ${exitCode}):`,
|
|
126
|
+
stderr,
|
|
127
|
+
);
|
|
128
|
+
setUpdateInProgress(false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Re-pull Docker image if auto-update is enabled
|
|
133
|
+
if (getSettings().dockerAutoUpdate) {
|
|
134
|
+
try {
|
|
135
|
+
console.log("[update] Re-pulling Docker image (dockerAutoUpdate enabled)...");
|
|
136
|
+
imagePullManager.pull("the-companion:latest");
|
|
137
|
+
const ready = await imagePullManager.waitForReady("the-companion:latest", 120_000);
|
|
138
|
+
if (ready) {
|
|
139
|
+
console.log("[update] Docker image re-pull complete.");
|
|
140
|
+
} else {
|
|
141
|
+
console.warn("[update] Docker image re-pull failed or timed out, continuing with restart.");
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.warn("[update] Docker image re-pull error, continuing:", err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
refreshServiceDefinition();
|
|
150
|
+
console.log("[update] Service definition refreshed.");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.warn("[update] Failed to refresh service definition:", err);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log("[update] Update successful, restarting service...");
|
|
156
|
+
|
|
157
|
+
const isLinux = process.platform === "linux";
|
|
158
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
159
|
+
const restartCmd = isLinux
|
|
160
|
+
? ["systemctl", "--user", "restart", "heyhank.service"]
|
|
161
|
+
: uid !== undefined
|
|
162
|
+
? ["launchctl", "kickstart", "-k", `gui/${uid}/sh.heyhank.app`]
|
|
163
|
+
: ["launchctl", "kickstart", "-k", "sh.heyhank.app"];
|
|
164
|
+
|
|
165
|
+
Bun.spawn(restartCmd, {
|
|
166
|
+
stdout: "ignore",
|
|
167
|
+
stderr: "ignore",
|
|
168
|
+
stdin: "ignore",
|
|
169
|
+
env: isLinux
|
|
170
|
+
? {
|
|
171
|
+
...process.env,
|
|
172
|
+
XDG_RUNTIME_DIR:
|
|
173
|
+
process.env.XDG_RUNTIME_DIR ||
|
|
174
|
+
`/run/user/${uid ?? 1000}`,
|
|
175
|
+
}
|
|
176
|
+
: undefined,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
setTimeout(() => process.exit(0), 500);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error("[update] Update failed:", err);
|
|
182
|
+
setUpdateInProgress(false);
|
|
183
|
+
}
|
|
184
|
+
}, 100);
|
|
185
|
+
|
|
186
|
+
return c.json({
|
|
187
|
+
ok: true,
|
|
188
|
+
message: "Update started. Server will restart shortly.",
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
api.get("/terminal", (c) => {
|
|
193
|
+
const terminalId = c.req.query("terminalId");
|
|
194
|
+
const info = deps.terminalManager.getInfo(terminalId || undefined);
|
|
195
|
+
if (!info) return c.json({ active: false });
|
|
196
|
+
return c.json({ active: true, terminalId: info.id, cwd: info.cwd });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
api.post("/terminal/spawn", async (c) => {
|
|
200
|
+
const body = await c.req.json<{ cwd: string; cols?: number; rows?: number; containerId?: string }>();
|
|
201
|
+
if (!body.cwd) return c.json({ error: "cwd is required" }, 400);
|
|
202
|
+
const terminalId = deps.terminalManager.spawn(body.cwd, body.cols, body.rows, {
|
|
203
|
+
containerId: body.containerId,
|
|
204
|
+
});
|
|
205
|
+
return c.json({ terminalId });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
api.post("/terminal/kill", async (c) => {
|
|
209
|
+
const body = await c.req.json<{ terminalId?: string }>().catch(() => undefined);
|
|
210
|
+
const terminalId = body?.terminalId?.trim();
|
|
211
|
+
if (!terminalId) return c.json({ error: "terminalId is required" }, 400);
|
|
212
|
+
deps.terminalManager.kill(terminalId);
|
|
213
|
+
return c.json({ ok: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
api.post("/sessions/:id/message", async (c) => {
|
|
217
|
+
const id = c.req.param("id");
|
|
218
|
+
const session = deps.launcher.getSession(id);
|
|
219
|
+
if (!session) return c.json({ error: "Session not found" }, 404);
|
|
220
|
+
if (!deps.launcher.isAlive(id)) return c.json({ error: "Session is not running" }, 400);
|
|
221
|
+
const body = await c.req.json().catch(() => ({}));
|
|
222
|
+
if (typeof body.content !== "string" || !body.content.trim()) {
|
|
223
|
+
return c.json({ error: "content is required" }, 400);
|
|
224
|
+
}
|
|
225
|
+
deps.wsBridge.injectUserMessage(id, body.content);
|
|
226
|
+
return c.json({ ok: true, sessionId: id });
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import { getTailscaleStatus, startFunnel, stopFunnel } from "../tailscale-manager.js";
|
|
3
|
+
|
|
4
|
+
export function registerTailscaleRoutes(api: Hono, port: number): void {
|
|
5
|
+
api.get("/tailscale/status", async (c) => {
|
|
6
|
+
const status = await getTailscaleStatus(port);
|
|
7
|
+
return c.json(status);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Always return 200 — the `error` and `needsOperatorMode` fields in the body
|
|
11
|
+
// signal failures. This lets the frontend receive the full structured status
|
|
12
|
+
// instead of the generic post() helper throwing and losing context.
|
|
13
|
+
api.post("/tailscale/funnel/start", async (c) => {
|
|
14
|
+
const status = await startFunnel(port);
|
|
15
|
+
return c.json(status);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
api.post("/tailscale/funnel/stop", async (c) => {
|
|
19
|
+
const status = await stopFunnel(port);
|
|
20
|
+
return c.json(status);
|
|
21
|
+
});
|
|
22
|
+
}
|