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,264 @@
1
+ import crypto from "node:crypto";
2
+ import type { Hono } from "hono";
3
+ import * as agentStore from "../agent-store.js";
4
+ import type { AgentExecutor } from "../agent-executor.js";
5
+ import type { AgentConfig, AgentConfigCreateInput, AgentConfigExport } from "../agent-types.js";
6
+
7
+ /** Fields the user can set when creating/updating an agent */
8
+ const EDITABLE_FIELDS = [
9
+ "name", "description", "icon", "version",
10
+ "backendType", "model", "permissionMode", "cwd",
11
+ "envSlug", "env", "allowedTools", "codexInternetAccess",
12
+ "prompt", "mcpServers", "skills",
13
+ "container", "branch", "createBranch", "useWorktree",
14
+ "triggers", "enabled",
15
+ ] as const;
16
+
17
+ function pickEditable(body: Record<string, unknown>): Partial<AgentConfig> {
18
+ const result: Record<string, unknown> = {};
19
+ for (const key of EDITABLE_FIELDS) {
20
+ if (key in body) result[key] = body[key];
21
+ }
22
+ return result as Partial<AgentConfig>;
23
+ }
24
+
25
+ function buildCreateInput(
26
+ body: Record<string, unknown>,
27
+ overrides?: Partial<Pick<AgentConfigCreateInput, "enabled" | "version">>,
28
+ ): AgentConfigCreateInput {
29
+ return {
30
+ version: overrides?.version ?? 1,
31
+ name: (body.name as string | undefined) || "",
32
+ description: (body.description as string | undefined) || "",
33
+ icon: body.icon as string | undefined,
34
+ backendType: (body.backendType as AgentConfig["backendType"] | undefined) || "claude",
35
+ model: (body.model as string | undefined) || "",
36
+ permissionMode: (body.permissionMode as string | undefined) || "bypassPermissions",
37
+ cwd: (body.cwd as string | undefined) || "",
38
+ envSlug: body.envSlug as string | undefined,
39
+ env: body.env as Record<string, string> | undefined,
40
+ allowedTools: body.allowedTools as string[] | undefined,
41
+ codexInternetAccess: body.codexInternetAccess as boolean | undefined,
42
+ prompt: (body.prompt as string | undefined) || "",
43
+ mcpServers: body.mcpServers as AgentConfig["mcpServers"] | undefined,
44
+ skills: body.skills as string[] | undefined,
45
+ container: body.container as AgentConfig["container"] | undefined,
46
+ branch: body.branch as string | undefined,
47
+ createBranch: body.createBranch as boolean | undefined,
48
+ useWorktree: body.useWorktree as boolean | undefined,
49
+ triggers: body.triggers as AgentConfig["triggers"] | undefined,
50
+ enabled: overrides?.enabled ?? ((body.enabled as boolean | undefined) ?? true),
51
+ };
52
+ }
53
+
54
+ /** Sanitize agent before sending to the browser */
55
+ function sanitizeAgent(agent: AgentConfig & { nextRunAt?: number | null }): Record<string, unknown> {
56
+ return agent as unknown as Record<string, unknown>;
57
+ }
58
+
59
+ /** Strip internal tracking fields to produce a portable export */
60
+ function toExport(agent: AgentConfig): AgentConfigExport {
61
+ const {
62
+ id: _id,
63
+ createdAt: _ca,
64
+ updatedAt: _ua,
65
+ totalRuns: _tr,
66
+ consecutiveFailures: _cf,
67
+ lastRunAt: _lr,
68
+ lastSessionId: _ls,
69
+ enabled: _en,
70
+ ...exportable
71
+ } = agent;
72
+ return exportable;
73
+ }
74
+
75
+ export function registerAgentRoutes(
76
+ api: Hono,
77
+ agentExecutor?: AgentExecutor,
78
+ ): void {
79
+ // ── CRUD ────────────────────────────────────────────────────────────────
80
+
81
+ api.get("/agents", (c) => {
82
+ const agents = agentStore.listAgents();
83
+ const enriched = agents.map((a) => sanitizeAgent({
84
+ ...a,
85
+ nextRunAt: agentExecutor?.getNextRunTime(a.id)?.getTime() ?? null,
86
+ }));
87
+ return c.json(enriched);
88
+ });
89
+
90
+ api.get("/agents/:id", (c) => {
91
+ const agent = agentStore.getAgent(c.req.param("id"));
92
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
93
+ return c.json(sanitizeAgent({
94
+ ...agent,
95
+ nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null,
96
+ }));
97
+ });
98
+
99
+ api.post("/agents", async (c) => {
100
+ const body = await c.req.json().catch(() => ({}));
101
+ try {
102
+ const agent = agentStore.createAgent(buildCreateInput(body));
103
+
104
+ if (agent.enabled && agent.triggers?.schedule?.enabled) {
105
+ agentExecutor?.scheduleAgent(agent);
106
+ }
107
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
108
+ } catch (e: unknown) {
109
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
110
+ }
111
+ });
112
+
113
+ api.put("/agents/:id", async (c) => {
114
+ const id = c.req.param("id");
115
+ const body = await c.req.json().catch(() => ({}));
116
+ try {
117
+ const allowed = pickEditable(body);
118
+ const agent = agentStore.updateAgent(id, allowed);
119
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
120
+ // Stop old timer (id may differ after a rename)
121
+ if (agent.id !== id) {
122
+ agentExecutor?.stopAgent(id);
123
+ }
124
+ // Reschedule if enabled
125
+ if (agent.enabled && agent.triggers?.schedule?.enabled) {
126
+ agentExecutor?.scheduleAgent(agent);
127
+ } else {
128
+ agentExecutor?.stopAgent(agent.id);
129
+ }
130
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
131
+ } catch (e: unknown) {
132
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
133
+ }
134
+ });
135
+
136
+ api.delete("/agents/:id", (c) => {
137
+ const id = c.req.param("id");
138
+ agentExecutor?.stopAgent(id);
139
+ const deleted = agentStore.deleteAgent(id);
140
+ if (!deleted) return c.json({ error: "Agent not found" }, 404);
141
+ return c.json({ ok: true });
142
+ });
143
+
144
+ // ── Toggle ──────────────────────────────────────────────────────────────
145
+
146
+ api.post("/agents/:id/toggle", (c) => {
147
+ const id = c.req.param("id");
148
+ const agent = agentStore.getAgent(id);
149
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
150
+ const updated = agentStore.updateAgent(id, { enabled: !agent.enabled });
151
+ if (updated?.enabled && updated.triggers?.schedule?.enabled) {
152
+ agentExecutor?.scheduleAgent(updated);
153
+ } else if (updated) {
154
+ agentExecutor?.stopAgent(updated.id);
155
+ }
156
+ return c.json(updated ? sanitizeAgent({ ...updated, nextRunAt: agentExecutor?.getNextRunTime(updated.id)?.getTime() ?? null }) : updated);
157
+ });
158
+
159
+ // ── Run (manual trigger) ───────────────────────────────────────────────
160
+
161
+ api.post("/agents/:id/run", async (c) => {
162
+ const id = c.req.param("id");
163
+ const agent = agentStore.getAgent(id);
164
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
165
+ const body = await c.req.json().catch(() => ({}));
166
+ const input = typeof body.input === "string" ? body.input : undefined;
167
+ try {
168
+ const sessionInfo = await agentExecutor?.executeAgent(id, input, { force: true, triggerType: "manual" });
169
+ return c.json({
170
+ ok: true,
171
+ message: "Agent triggered",
172
+ sessionId: sessionInfo?.sessionId || null,
173
+ agentName: agent.name,
174
+ });
175
+ } catch (err) {
176
+ return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
177
+ }
178
+ });
179
+
180
+ // ── Executions ─────────────────────────────────────────────────────────
181
+
182
+ api.get("/agents/:id/executions", (c) => {
183
+ const id = c.req.param("id");
184
+ return c.json(agentExecutor?.getExecutions(id) ?? []);
185
+ });
186
+
187
+ /** List executions across all agents with filtering and pagination (for Runs view). */
188
+ api.get("/executions", (c) => {
189
+ const agentId = c.req.query("agentId");
190
+ const triggerType = c.req.query("triggerType");
191
+ const rawStatus = c.req.query("status");
192
+ const status = (rawStatus === "running" || rawStatus === "success" || rawStatus === "error")
193
+ ? rawStatus : undefined;
194
+ const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 500);
195
+ const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
196
+ return c.json(agentExecutor?.listAllExecutions({ agentId, triggerType, status, limit, offset }) ?? { executions: [], total: 0 });
197
+ });
198
+
199
+ // ── Import / Export ────────────────────────────────────────────────────
200
+
201
+ api.post("/agents/import", async (c) => {
202
+ const body = await c.req.json().catch(() => ({}));
203
+ try {
204
+ // Accept an exported agent JSON and create a new agent from it
205
+ const agent = agentStore.createAgent(buildCreateInput(body, {
206
+ version: (body.version as AgentConfigCreateInput["version"] | undefined) || 1,
207
+ enabled: false, // Imported agents start disabled for safety
208
+ }));
209
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
210
+ } catch (e: unknown) {
211
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
212
+ }
213
+ });
214
+
215
+ api.get("/agents/:id/export", (c) => {
216
+ const agent = agentStore.getAgent(c.req.param("id"));
217
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
218
+ return c.json(toExport(agent));
219
+ });
220
+
221
+ // ── Webhook Secret ─────────────────────────────────────────────────────
222
+
223
+ api.post("/agents/:id/regenerate-secret", (c) => {
224
+ const id = c.req.param("id");
225
+ const agent = agentStore.regenerateWebhookSecret(id);
226
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
227
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
228
+ });
229
+
230
+ // ── Webhook Trigger ────────────────────────────────────────────────────
231
+
232
+ api.post("/agents/:id/webhook/:secret", async (c) => {
233
+ const id = c.req.param("id");
234
+ const secret = c.req.param("secret");
235
+
236
+ const agent = agentStore.getAgent(id);
237
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
238
+
239
+ // Validate webhook is enabled and secret matches
240
+ if (!agent.triggers?.webhook?.enabled) {
241
+ return c.json({ error: "Webhook not enabled for this agent" }, 403);
242
+ }
243
+ // Use constant-time comparison to prevent timing attacks
244
+ const expected = Buffer.from(agent.triggers.webhook.secret);
245
+ const received = Buffer.from(secret);
246
+ if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
247
+ return c.json({ error: "Invalid webhook secret" }, 401);
248
+ }
249
+
250
+ // Extract input from body — accept JSON { input: "..." } or plain text
251
+ let input: string | undefined;
252
+ const contentType = c.req.header("content-type") || "";
253
+ if (contentType.includes("application/json")) {
254
+ const body = await c.req.json().catch(() => ({}));
255
+ input = typeof body.input === "string" ? body.input : undefined;
256
+ } else {
257
+ const text = await c.req.text().catch(() => "");
258
+ if (text.trim()) input = text.trim();
259
+ }
260
+
261
+ agentExecutor?.executeAgentManually(id, input);
262
+ return c.json({ ok: true, message: "Agent triggered via webhook" });
263
+ });
264
+ }
@@ -0,0 +1,90 @@
1
+ // ─── Assistant Routes ─────────────────────────────────────────────────────────
2
+ // REST API for todos, notes, and reminders.
3
+
4
+ import type { Hono } from "hono";
5
+ import * as store from "../assistant-store.js";
6
+
7
+ export function registerAssistantRoutes(api: Hono): void {
8
+ // ─── Todos ──────────────────────────────────────────────────────────
9
+
10
+ api.get("/assistant/todos", (c) => {
11
+ const done = c.req.query("done");
12
+ const priority = c.req.query("priority");
13
+ const category = c.req.query("category");
14
+ const filter: { done?: boolean; priority?: string; category?: string } = {};
15
+ if (done !== undefined) filter.done = done === "true";
16
+ if (priority) filter.priority = priority;
17
+ if (category) filter.category = category;
18
+ return c.json({ todos: store.listTodos(filter) });
19
+ });
20
+
21
+ api.post("/assistant/todos", async (c) => {
22
+ const body = await c.req.json<{ text: string; priority?: string; category?: string }>();
23
+ if (!body.text) return c.json({ error: "text is required" }, 400);
24
+ const todo = store.addTodo(body.text, body.priority, body.category);
25
+ return c.json(todo);
26
+ });
27
+
28
+ api.patch("/assistant/todos/:id", async (c) => {
29
+ const id = c.req.param("id");
30
+ const body = await c.req.json<{ text?: string; priority?: string; category?: string; done?: boolean }>();
31
+ if (body.done === true) {
32
+ const todo = store.completeTodo(id);
33
+ if (!todo) return c.json({ error: "not found" }, 404);
34
+ return c.json(todo);
35
+ }
36
+ const todo = store.updateTodo(id, body);
37
+ if (!todo) return c.json({ error: "not found" }, 404);
38
+ return c.json(todo);
39
+ });
40
+
41
+ api.delete("/assistant/todos/:id", (c) => {
42
+ const ok = store.deleteTodo(c.req.param("id"));
43
+ return c.json({ ok });
44
+ });
45
+
46
+ // ─── Notes ──────────────────────────────────────────────────────────
47
+
48
+ api.get("/assistant/notes", (c) => {
49
+ const search = c.req.query("search");
50
+ return c.json({ notes: store.listNotes(search) });
51
+ });
52
+
53
+ api.post("/assistant/notes", async (c) => {
54
+ const body = await c.req.json<{ title: string; content: string; tags?: string[] }>();
55
+ if (!body.title) return c.json({ error: "title is required" }, 400);
56
+ const note = store.addNote(body.title, body.content || "", body.tags);
57
+ return c.json(note);
58
+ });
59
+
60
+ api.patch("/assistant/notes/:id", async (c) => {
61
+ const body = await c.req.json<{ title?: string; content?: string; tags?: string[] }>();
62
+ const note = store.updateNote(c.req.param("id"), body);
63
+ if (!note) return c.json({ error: "not found" }, 404);
64
+ return c.json(note);
65
+ });
66
+
67
+ api.delete("/assistant/notes/:id", (c) => {
68
+ const ok = store.deleteNote(c.req.param("id"));
69
+ return c.json({ ok });
70
+ });
71
+
72
+ // ─── Reminders ──────────────────────────────────────────────────────
73
+
74
+ api.get("/assistant/reminders", (c) => {
75
+ const all = c.req.query("all") === "true";
76
+ return c.json({ reminders: store.listReminders(all) });
77
+ });
78
+
79
+ api.post("/assistant/reminders", async (c) => {
80
+ const body = await c.req.json<{ text: string; triggerAt: string }>();
81
+ if (!body.text || !body.triggerAt) return c.json({ error: "text and triggerAt required" }, 400);
82
+ const reminder = store.addReminder(body.text, body.triggerAt);
83
+ return c.json(reminder);
84
+ });
85
+
86
+ api.delete("/assistant/reminders/:id", (c) => {
87
+ const ok = store.deleteReminder(c.req.param("id"));
88
+ return c.json({ ok });
89
+ });
90
+ }
@@ -0,0 +1,103 @@
1
+ import type { Hono } from "hono";
2
+ import * as cronStore from "../cron-store.js";
3
+ import type { CronScheduler } from "../cron-scheduler.js";
4
+
5
+ export function registerCronRoutes(
6
+ api: Hono,
7
+ cronScheduler?: CronScheduler,
8
+ ): void {
9
+ api.get("/cron/jobs", (c) => {
10
+ const jobs = cronStore.listJobs();
11
+ const enriched = jobs.map((j) => ({
12
+ ...j,
13
+ nextRunAt: cronScheduler?.getNextRunTime(j.id)?.getTime() ?? null,
14
+ }));
15
+ return c.json(enriched);
16
+ });
17
+
18
+ api.get("/cron/jobs/:id", (c) => {
19
+ const job = cronStore.getJob(c.req.param("id"));
20
+ if (!job) return c.json({ error: "Job not found" }, 404);
21
+ return c.json({
22
+ ...job,
23
+ nextRunAt: cronScheduler?.getNextRunTime(job.id)?.getTime() ?? null,
24
+ });
25
+ });
26
+
27
+ api.post("/cron/jobs", async (c) => {
28
+ const body = await c.req.json().catch(() => ({}));
29
+ try {
30
+ const job = cronStore.createJob({
31
+ name: body.name || "",
32
+ prompt: body.prompt || "",
33
+ schedule: body.schedule || "",
34
+ recurring: body.recurring ?? true,
35
+ backendType: body.backendType || "claude",
36
+ model: body.model || "",
37
+ cwd: body.cwd || "",
38
+ envSlug: body.envSlug,
39
+ enabled: body.enabled ?? true,
40
+ permissionMode: body.permissionMode || "bypassPermissions",
41
+ codexInternetAccess: body.codexInternetAccess,
42
+ });
43
+ if (job.enabled) cronScheduler?.scheduleJob(job);
44
+ return c.json(job, 201);
45
+ } catch (e: unknown) {
46
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
47
+ }
48
+ });
49
+
50
+ api.put("/cron/jobs/:id", async (c) => {
51
+ const id = c.req.param("id");
52
+ const body = await c.req.json().catch(() => ({}));
53
+ try {
54
+ // Only allow user-editable fields — prevent tampering with internal tracking
55
+ const allowed: Record<string, unknown> = {};
56
+ for (const key of ["name", "prompt", "schedule", "recurring", "backendType", "model", "cwd", "envSlug", "enabled", "permissionMode", "codexInternetAccess"] as const) {
57
+ if (key in body) allowed[key] = body[key];
58
+ }
59
+ const job = cronStore.updateJob(id, allowed);
60
+ if (!job) return c.json({ error: "Job not found" }, 404);
61
+ // Stop the old timer (id may differ from job.id after a rename)
62
+ if (job.id !== id) cronScheduler?.stopJob(id);
63
+ cronScheduler?.scheduleJob(job);
64
+ return c.json(job);
65
+ } catch (e: unknown) {
66
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
67
+ }
68
+ });
69
+
70
+ api.delete("/cron/jobs/:id", (c) => {
71
+ const id = c.req.param("id");
72
+ cronScheduler?.stopJob(id);
73
+ const deleted = cronStore.deleteJob(id);
74
+ if (!deleted) return c.json({ error: "Job not found" }, 404);
75
+ return c.json({ ok: true });
76
+ });
77
+
78
+ api.post("/cron/jobs/:id/toggle", (c) => {
79
+ const id = c.req.param("id");
80
+ const job = cronStore.getJob(id);
81
+ if (!job) return c.json({ error: "Job not found" }, 404);
82
+ const updated = cronStore.updateJob(id, { enabled: !job.enabled });
83
+ if (updated?.enabled) {
84
+ cronScheduler?.scheduleJob(updated);
85
+ } else {
86
+ cronScheduler?.stopJob(id);
87
+ }
88
+ return c.json(updated);
89
+ });
90
+
91
+ api.post("/cron/jobs/:id/run", (c) => {
92
+ const id = c.req.param("id");
93
+ const job = cronStore.getJob(id);
94
+ if (!job) return c.json({ error: "Job not found" }, 404);
95
+ cronScheduler?.executeJobManually(id);
96
+ return c.json({ ok: true, message: "Job triggered" });
97
+ });
98
+
99
+ api.get("/cron/jobs/:id/executions", (c) => {
100
+ const id = c.req.param("id");
101
+ return c.json(cronScheduler?.getExecutions(id) ?? []);
102
+ });
103
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { Hono } from "hono";
3
+ import { join } from "node:path";
4
+ import * as envManager from "../env-manager.js";
5
+ import { containerManager } from "../container-manager.js";
6
+ import { imagePullManager } from "../image-pull-manager.js";
7
+
8
+ export function registerEnvRoutes(
9
+ api: Hono,
10
+ options: { webDir: string },
11
+ ): void {
12
+ api.get("/envs", (c) => {
13
+ try {
14
+ return c.json(envManager.listEnvs());
15
+ } catch (e: unknown) {
16
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
17
+ }
18
+ });
19
+
20
+ api.get("/envs/:slug", (c) => {
21
+ const env = envManager.getEnv(c.req.param("slug"));
22
+ if (!env) return c.json({ error: "Environment not found" }, 404);
23
+ return c.json(env);
24
+ });
25
+
26
+ api.post("/envs", async (c) => {
27
+ const body = await c.req.json().catch(() => ({}));
28
+ try {
29
+ const env = envManager.createEnv(body.name, body.variables || {});
30
+ return c.json(env, 201);
31
+ } catch (e: unknown) {
32
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
33
+ }
34
+ });
35
+
36
+ api.put("/envs/:slug", async (c) => {
37
+ const slug = c.req.param("slug");
38
+ const body = await c.req.json().catch(() => ({}));
39
+ try {
40
+ const env = envManager.updateEnv(slug, {
41
+ name: body.name,
42
+ variables: body.variables,
43
+ });
44
+ if (!env) return c.json({ error: "Environment not found" }, 404);
45
+ return c.json(env);
46
+ } catch (e: unknown) {
47
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
48
+ }
49
+ });
50
+
51
+ api.delete("/envs/:slug", (c) => {
52
+ try {
53
+ const deleted = envManager.deleteEnv(c.req.param("slug"));
54
+ if (!deleted) return c.json({ error: "Environment not found" }, 404);
55
+ return c.json({ ok: true });
56
+ } catch (e: unknown) {
57
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
58
+ }
59
+ });
60
+
61
+ api.post("/docker/build-base", async (c) => {
62
+ if (!containerManager.checkDocker()) return c.json({ error: "Docker is not available" }, 503);
63
+ const dockerfilePath = join(options.webDir, "docker", "Dockerfile.the-companion");
64
+ if (!existsSync(dockerfilePath)) {
65
+ return c.json({ error: "Base Dockerfile not found at " + dockerfilePath }, 404);
66
+ }
67
+ try {
68
+ const log = containerManager.buildImage(dockerfilePath, "the-companion:latest");
69
+ return c.json({ success: true, log });
70
+ } catch (e: unknown) {
71
+ return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500);
72
+ }
73
+ });
74
+
75
+ api.get("/docker/base-image", (c) => {
76
+ const exists = containerManager.imageExists("the-companion:latest");
77
+ return c.json({ exists, image: "the-companion:latest" });
78
+ });
79
+
80
+ api.get("/images/:tag/status", (c) => {
81
+ const tag = decodeURIComponent(c.req.param("tag"));
82
+ if (!tag) return c.json({ error: "Image tag is required" }, 400);
83
+ return c.json(imagePullManager.getState(tag));
84
+ });
85
+
86
+ api.post("/images/:tag/pull", (c) => {
87
+ const tag = decodeURIComponent(c.req.param("tag"));
88
+ if (!tag) return c.json({ error: "Image tag is required" }, 400);
89
+ if (!containerManager.checkDocker()) {
90
+ return c.json({ error: "Docker is not available" }, 503);
91
+ }
92
+ imagePullManager.pull(tag);
93
+ return c.json({ ok: true, state: imagePullManager.getState(tag) });
94
+ });
95
+ }
@@ -0,0 +1,76 @@
1
+ // ─── Federation REST API Routes ───────────────────────────────────────────────
2
+
3
+ import { Hono } from "hono";
4
+ import { getNodeIdentity, updateNodeName, addNode, removeNode } from "../federation/node-store.js";
5
+ import { nodeManager } from "../federation/node-manager.js";
6
+
7
+ export function registerFederationRoutes(api: Hono): void {
8
+ // GET /api/federation/identity — public (no auth needed for node discovery)
9
+ api.get("/federation/identity", (c) => {
10
+ return c.json(getNodeIdentity());
11
+ });
12
+
13
+ // PUT /api/federation/identity — update node name
14
+ api.put("/federation/identity", async (c) => {
15
+ const body = await c.req.json<{ name?: string }>().catch(() => ({} as { name?: string }));
16
+ const name = body.name?.trim();
17
+ if (!name) return c.json({ error: "name required" }, 400);
18
+ return c.json({ ok: true, ...updateNodeName(name) });
19
+ });
20
+
21
+ // GET /api/federation/nodes — list nodes with connection status
22
+ api.get("/federation/nodes", (c) => {
23
+ return c.json({
24
+ identity: getNodeIdentity(),
25
+ nodes: nodeManager.getNodeStatuses(),
26
+ });
27
+ });
28
+
29
+ // POST /api/federation/nodes — add a new node
30
+ api.post("/federation/nodes", async (c) => {
31
+ const body = await c.req.json<{ url?: string; secret?: string; name?: string }>().catch(() => ({} as { url?: string; secret?: string; name?: string }));
32
+ const url = body.url?.trim();
33
+ const secret = body.secret?.trim();
34
+ const name = body.name?.trim() || "";
35
+
36
+ if (!url) return c.json({ error: "url required" }, 400);
37
+ if (!secret) return c.json({ error: "secret required" }, 400);
38
+
39
+ const node = addNode({ url, secret, name });
40
+ nodeManager.connectNode(node.id);
41
+ return c.json({ ok: true, node }, 201);
42
+ });
43
+
44
+ // DELETE /api/federation/nodes/:id — remove and disconnect
45
+ api.delete("/federation/nodes/:id", (c) => {
46
+ const id = c.req.param("id");
47
+ nodeManager.disconnectNode(id);
48
+ removeNode(id);
49
+ return c.json({ ok: true });
50
+ });
51
+
52
+ // POST /api/federation/nodes/:id/test — check connection status
53
+ api.post("/federation/nodes/:id/test", (c) => {
54
+ const id = c.req.param("id");
55
+ const status = nodeManager.getNodeStatuses().find((n) => n.id === id);
56
+ if (!status) return c.json({ error: "node not found" }, 404);
57
+ return c.json({ ok: true, connected: status.connected, node: status });
58
+ });
59
+
60
+ // GET /api/federation/remote-sessions — list sessions from all connected peers
61
+ api.get("/federation/remote-sessions", (c) => {
62
+ return c.json({ sessions: nodeManager.getRemoteSessions() });
63
+ });
64
+
65
+ // POST /api/federation/proxy — send message to a remote session
66
+ api.post("/federation/proxy", async (c) => {
67
+ const body = await c.req.json<{ sessionId?: string; text?: string }>().catch(() => ({} as { sessionId?: string; text?: string }));
68
+ const sessionId = body.sessionId?.trim();
69
+ const text = body.text?.trim();
70
+ if (!sessionId || !text) return c.json({ error: "sessionId and text required" }, 400);
71
+
72
+ const result = await nodeManager.sendToRemoteSession(sessionId, text);
73
+ if (result.error) return c.json({ error: result.error }, 502);
74
+ return c.json(result);
75
+ });
76
+ }