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
package/bin/ctl.ts ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CLI handler module for `companion` management subcommands.
4
+ * Each subcommand maps 1:1 to a Companion REST API endpoint.
5
+ * All output is JSON to stdout for easy parsing by both humans and AI agents.
6
+ */
7
+
8
+ const DEFAULT_PORT = 3456;
9
+
10
+ function getPort(argv: string[]): number {
11
+ const idx = argv.indexOf("--port");
12
+ if (idx !== -1 && argv[idx + 1]) {
13
+ const p = Number(argv[idx + 1]);
14
+ if (!Number.isNaN(p) && p > 0) return p;
15
+ }
16
+ return Number(process.env.COMPANION_PORT) || DEFAULT_PORT;
17
+ }
18
+
19
+ function getBase(argv: string[]): string {
20
+ return `http://localhost:${getPort(argv)}/api`;
21
+ }
22
+
23
+ /** Strip --port <n> from argv so subcommand parsers don't see it */
24
+ function stripGlobalFlags(argv: string[]): string[] {
25
+ const result: string[] = [];
26
+ let i = 0;
27
+ while (i < argv.length) {
28
+ if (argv[i] === "--port" && argv[i + 1]) {
29
+ i += 2;
30
+ continue;
31
+ }
32
+ result.push(argv[i]);
33
+ i++;
34
+ }
35
+ return result;
36
+ }
37
+
38
+ async function apiGet(base: string, path: string): Promise<unknown> {
39
+ const res = await fetch(`${base}${path}`);
40
+ if (!res.ok) {
41
+ const body = await res.json().catch(() => ({ error: res.statusText }));
42
+ throw new Error((body as { error?: string }).error || `HTTP ${res.status}`);
43
+ }
44
+ return res.json();
45
+ }
46
+
47
+ async function apiPost(base: string, path: string, body?: unknown): Promise<unknown> {
48
+ const res = await fetch(`${base}${path}`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: body ? JSON.stringify(body) : undefined,
52
+ });
53
+ if (!res.ok) {
54
+ const data = await res.json().catch(() => ({ error: res.statusText }));
55
+ throw new Error((data as { error?: string }).error || `HTTP ${res.status}`);
56
+ }
57
+ return res.json();
58
+ }
59
+
60
+ async function apiPut(base: string, path: string, body: unknown): Promise<unknown> {
61
+ const res = await fetch(`${base}${path}`, {
62
+ method: "PUT",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(body),
65
+ });
66
+ if (!res.ok) {
67
+ const data = await res.json().catch(() => ({ error: res.statusText }));
68
+ throw new Error((data as { error?: string }).error || `HTTP ${res.status}`);
69
+ }
70
+ return res.json();
71
+ }
72
+
73
+ async function apiPatch(base: string, path: string, body: unknown): Promise<unknown> {
74
+ const res = await fetch(`${base}${path}`, {
75
+ method: "PATCH",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(body),
78
+ });
79
+ if (!res.ok) {
80
+ const data = await res.json().catch(() => ({ error: res.statusText }));
81
+ throw new Error((data as { error?: string }).error || `HTTP ${res.status}`);
82
+ }
83
+ return res.json();
84
+ }
85
+
86
+ async function apiDelete(base: string, path: string, body?: unknown): Promise<unknown> {
87
+ const res = await fetch(`${base}${path}`, {
88
+ method: "DELETE",
89
+ headers: { "Content-Type": "application/json" },
90
+ body: body ? JSON.stringify(body) : undefined,
91
+ });
92
+ if (!res.ok) {
93
+ const data = await res.json().catch(() => ({ error: res.statusText }));
94
+ throw new Error((data as { error?: string }).error || `HTTP ${res.status}`);
95
+ }
96
+ return res.json();
97
+ }
98
+
99
+ function out(data: unknown): void {
100
+ console.log(JSON.stringify(data, null, 2));
101
+ }
102
+
103
+ function err(message: string): never {
104
+ console.error(JSON.stringify({ error: message }));
105
+ process.exit(1);
106
+ }
107
+
108
+ /** Parse --key value pairs from argv. Supports --flag (boolean true). */
109
+ function parseFlags(argv: string[]): Record<string, string | boolean> {
110
+ const flags: Record<string, string | boolean> = {};
111
+ let i = 0;
112
+ while (i < argv.length) {
113
+ const arg = argv[i];
114
+ if (arg.startsWith("--")) {
115
+ const key = arg.slice(2);
116
+ const next = argv[i + 1];
117
+ if (next && !next.startsWith("--")) {
118
+ flags[key] = next;
119
+ i += 2;
120
+ } else {
121
+ flags[key] = true;
122
+ i++;
123
+ }
124
+ } else {
125
+ i++;
126
+ }
127
+ }
128
+ return flags;
129
+ }
130
+
131
+ /** Collect all --var KEY=VALUE pairs from argv */
132
+ function parseVars(argv: string[]): Record<string, string> {
133
+ const vars: Record<string, string> = {};
134
+ let i = 0;
135
+ while (i < argv.length) {
136
+ if (argv[i] === "--var" && argv[i + 1]) {
137
+ const eq = argv[i + 1].indexOf("=");
138
+ if (eq > 0) {
139
+ vars[argv[i + 1].slice(0, eq)] = argv[i + 1].slice(eq + 1);
140
+ }
141
+ i += 2;
142
+ } else {
143
+ i++;
144
+ }
145
+ }
146
+ return vars;
147
+ }
148
+
149
+ // ─── Subcommand handlers ────────────────────────────────────────────────────
150
+
151
+ async function handleStatus(base: string): Promise<void> {
152
+ const [sessions, backends] = await Promise.all([
153
+ apiGet(base, "/sessions") as Promise<unknown[]>,
154
+ apiGet(base, "/backends") as Promise<unknown[]>,
155
+ ]);
156
+ const active = (sessions as Array<{ state?: string; archived?: boolean }>).filter(
157
+ (s) => !s.archived && s.state !== "exited",
158
+ );
159
+ out({
160
+ activeSessions: active.length,
161
+ totalSessions: sessions.length,
162
+ backends,
163
+ });
164
+ }
165
+
166
+ async function handleSessions(base: string, args: string[]): Promise<void> {
167
+ const sub = args[0];
168
+ const rest = args.slice(1);
169
+
170
+ switch (sub) {
171
+ case "list": {
172
+ const sessions = await apiGet(base, "/sessions");
173
+ out(sessions);
174
+ break;
175
+ }
176
+ case "get": {
177
+ const id = rest[0];
178
+ if (!id) err("Usage: companion sessions get <sessionId>");
179
+ out(await apiGet(base, `/sessions/${encodeURIComponent(id)}`));
180
+ break;
181
+ }
182
+ case "create": {
183
+ const flags = parseFlags(rest);
184
+ const body: Record<string, unknown> = {};
185
+ if (flags.cwd) body.cwd = flags.cwd;
186
+ if (flags.model) body.model = flags.model;
187
+ if (flags["permission-mode"]) body.permissionMode = flags["permission-mode"];
188
+ if (flags.env) body.envSlug = flags.env;
189
+ if (flags.backend) body.backend = flags.backend;
190
+ if (flags.worktree) body.useWorktree = true;
191
+ if (flags.branch) body.branch = flags.branch;
192
+ if (flags["create-branch"]) body.createBranch = true;
193
+ out(await apiPost(base, "/sessions/create", body));
194
+ break;
195
+ }
196
+ case "kill": {
197
+ const id = rest[0];
198
+ if (!id) err("Usage: companion sessions kill <sessionId>");
199
+ out(await apiPost(base, `/sessions/${encodeURIComponent(id)}/kill`));
200
+ break;
201
+ }
202
+ case "relaunch": {
203
+ const id = rest[0];
204
+ if (!id) err("Usage: companion sessions relaunch <sessionId>");
205
+ out(await apiPost(base, `/sessions/${encodeURIComponent(id)}/relaunch`));
206
+ break;
207
+ }
208
+ case "archive": {
209
+ const id = rest[0];
210
+ if (!id) err("Usage: companion sessions archive <sessionId>");
211
+ out(await apiPost(base, `/sessions/${encodeURIComponent(id)}/archive`));
212
+ break;
213
+ }
214
+ case "rename": {
215
+ const id = rest[0];
216
+ const name = rest.slice(1).join(" ");
217
+ if (!id || !name) err("Usage: companion sessions rename <sessionId> <name>");
218
+ out(await apiPatch(base, `/sessions/${encodeURIComponent(id)}/name`, { name }));
219
+ break;
220
+ }
221
+ case "send-message": {
222
+ const id = rest[0];
223
+ const content = rest.slice(1).join(" ");
224
+ if (!id || !content) err("Usage: companion sessions send-message <sessionId> <message>");
225
+ out(await apiPost(base, `/sessions/${encodeURIComponent(id)}/message`, { content }));
226
+ break;
227
+ }
228
+ default:
229
+ err(`Unknown sessions subcommand: ${sub}. Available: list, get, create, kill, relaunch, archive, rename, send-message`);
230
+ }
231
+ }
232
+
233
+ async function handleEnvs(base: string, args: string[]): Promise<void> {
234
+ const sub = args[0];
235
+ const rest = args.slice(1);
236
+
237
+ switch (sub) {
238
+ case "list": {
239
+ out(await apiGet(base, "/envs"));
240
+ break;
241
+ }
242
+ case "get": {
243
+ const slug = rest[0];
244
+ if (!slug) err("Usage: companion envs get <slug>");
245
+ out(await apiGet(base, `/envs/${encodeURIComponent(slug)}`));
246
+ break;
247
+ }
248
+ case "create": {
249
+ const flags = parseFlags(rest);
250
+ const vars = parseVars(rest);
251
+ if (!flags.name) err("Usage: companion envs create --name <name> [--var KEY=VALUE ...]");
252
+ out(await apiPost(base, "/envs", { name: flags.name, variables: vars }));
253
+ break;
254
+ }
255
+ case "update": {
256
+ const slug = rest[0];
257
+ if (!slug) err("Usage: companion envs update <slug> [--name <name>] [--var KEY=VALUE ...]");
258
+ const flagArgs = rest.slice(1);
259
+ const flags = parseFlags(flagArgs);
260
+ const vars = parseVars(flagArgs);
261
+ const body: Record<string, unknown> = {};
262
+ if (flags.name) body.name = flags.name;
263
+ if (Object.keys(vars).length > 0) body.variables = vars;
264
+ out(await apiPut(base, `/envs/${encodeURIComponent(slug)}`, body));
265
+ break;
266
+ }
267
+ case "delete": {
268
+ const slug = rest[0];
269
+ if (!slug) err("Usage: companion envs delete <slug>");
270
+ out(await apiDelete(base, `/envs/${encodeURIComponent(slug)}`));
271
+ break;
272
+ }
273
+ default:
274
+ err(`Unknown envs subcommand: ${sub}. Available: list, get, create, update, delete`);
275
+ }
276
+ }
277
+
278
+ async function handleCron(base: string, args: string[]): Promise<void> {
279
+ const sub = args[0];
280
+ const rest = args.slice(1);
281
+
282
+ switch (sub) {
283
+ case "list": {
284
+ out(await apiGet(base, "/cron/jobs"));
285
+ break;
286
+ }
287
+ case "get": {
288
+ const id = rest[0];
289
+ if (!id) err("Usage: companion cron get <jobId>");
290
+ out(await apiGet(base, `/cron/jobs/${encodeURIComponent(id)}`));
291
+ break;
292
+ }
293
+ case "create": {
294
+ const flags = parseFlags(rest);
295
+ if (!flags.name || !flags.schedule || !flags.prompt)
296
+ err("Usage: companion cron create --name <name> --schedule <cron|datetime> --prompt <prompt> [--cwd <path>] [--model <model>] [--env <slug>] [--recurring] [--backend <type>] [--permission-mode <mode>]");
297
+ const body: Record<string, unknown> = {
298
+ name: flags.name,
299
+ schedule: flags.schedule,
300
+ prompt: flags.prompt,
301
+ };
302
+ if (flags.cwd) body.cwd = flags.cwd;
303
+ if (flags.model) body.model = flags.model;
304
+ if (flags.env) body.envSlug = flags.env;
305
+ if (flags.backend) body.backendType = flags.backend;
306
+ if (flags["permission-mode"]) body.permissionMode = flags["permission-mode"];
307
+ // Default: recurring=true for cron expressions, false if looks like a datetime
308
+ body.recurring = flags.recurring === true || flags.recurring === "true"
309
+ || (flags.recurring === undefined && !(flags.schedule as string).includes("T"));
310
+ out(await apiPost(base, "/cron/jobs", body));
311
+ break;
312
+ }
313
+ case "update": {
314
+ const id = rest[0];
315
+ if (!id) err("Usage: companion cron update <jobId> [--name <n>] [--schedule <s>] [--prompt <p>] ...");
316
+ const flagArgs = rest.slice(1);
317
+ const flags = parseFlags(flagArgs);
318
+ const body: Record<string, unknown> = {};
319
+ if (flags.name) body.name = flags.name;
320
+ if (flags.schedule) body.schedule = flags.schedule;
321
+ if (flags.prompt) body.prompt = flags.prompt;
322
+ if (flags.cwd) body.cwd = flags.cwd;
323
+ if (flags.model) body.model = flags.model;
324
+ if (flags.env) body.envSlug = flags.env;
325
+ if (flags.backend) body.backendType = flags.backend;
326
+ if (flags["permission-mode"]) body.permissionMode = flags["permission-mode"];
327
+ if (flags.recurring !== undefined) body.recurring = flags.recurring === true || flags.recurring === "true";
328
+ out(await apiPut(base, `/cron/jobs/${encodeURIComponent(id)}`, body));
329
+ break;
330
+ }
331
+ case "delete": {
332
+ const id = rest[0];
333
+ if (!id) err("Usage: companion cron delete <jobId>");
334
+ out(await apiDelete(base, `/cron/jobs/${encodeURIComponent(id)}`));
335
+ break;
336
+ }
337
+ case "toggle": {
338
+ const id = rest[0];
339
+ if (!id) err("Usage: companion cron toggle <jobId>");
340
+ out(await apiPost(base, `/cron/jobs/${encodeURIComponent(id)}/toggle`));
341
+ break;
342
+ }
343
+ case "run": {
344
+ const id = rest[0];
345
+ if (!id) err("Usage: companion cron run <jobId>");
346
+ out(await apiPost(base, `/cron/jobs/${encodeURIComponent(id)}/run`));
347
+ break;
348
+ }
349
+ case "executions": {
350
+ const id = rest[0];
351
+ if (!id) err("Usage: companion cron executions <jobId>");
352
+ out(await apiGet(base, `/cron/jobs/${encodeURIComponent(id)}/executions`));
353
+ break;
354
+ }
355
+ default:
356
+ err(`Unknown cron subcommand: ${sub}. Available: list, get, create, update, delete, toggle, run, executions`);
357
+ }
358
+ }
359
+
360
+ async function handleSettings(base: string, args: string[]): Promise<void> {
361
+ const sub = args[0];
362
+
363
+ switch (sub) {
364
+ case "get": {
365
+ out(await apiGet(base, "/settings"));
366
+ break;
367
+ }
368
+ case "set": {
369
+ const flags = parseFlags(args.slice(1));
370
+ const body: Record<string, unknown> = {};
371
+ if (flags["anthropic-key"]) body.anthropicApiKey = flags["anthropic-key"];
372
+ if (flags["anthropic-model"]) body.anthropicModel = flags["anthropic-model"];
373
+ if (Object.keys(body).length === 0) err("Usage: companion settings set --anthropic-key <key> or --anthropic-model <model>");
374
+ out(await apiPut(base, "/settings", body));
375
+ break;
376
+ }
377
+ default:
378
+ err(`Unknown settings subcommand: ${sub}. Available: get, set`);
379
+ }
380
+ }
381
+
382
+ async function handleAssistant(base: string, args: string[]): Promise<void> {
383
+ const sub = args[0];
384
+
385
+ switch (sub) {
386
+ case "status": {
387
+ out(await apiGet(base, "/assistant/status"));
388
+ break;
389
+ }
390
+ case "launch": {
391
+ out(await apiPost(base, "/assistant/launch"));
392
+ break;
393
+ }
394
+ case "stop": {
395
+ out(await apiPost(base, "/assistant/stop"));
396
+ break;
397
+ }
398
+ case "config": {
399
+ const action = args[1];
400
+ if (action === "set") {
401
+ const flags = parseFlags(args.slice(2));
402
+ const body: Record<string, unknown> = {};
403
+ if (flags.model) body.model = flags.model;
404
+ if (flags["permission-mode"]) body.permissionMode = flags["permission-mode"];
405
+ if (flags.enabled !== undefined) body.enabled = flags.enabled === true || flags.enabled === "true";
406
+ out(await apiPut(base, "/assistant/config", body));
407
+ } else {
408
+ out(await apiGet(base, "/assistant/config"));
409
+ }
410
+ break;
411
+ }
412
+ default:
413
+ err(`Unknown assistant subcommand: ${sub}. Available: status, launch, stop, config`);
414
+ }
415
+ }
416
+
417
+ async function handleSkills(base: string, args: string[]): Promise<void> {
418
+ const sub = args[0];
419
+ const rest = args.slice(1);
420
+
421
+ switch (sub) {
422
+ case "list": {
423
+ out(await apiGet(base, "/skills"));
424
+ break;
425
+ }
426
+ case "get": {
427
+ const slug = rest[0];
428
+ if (!slug) err("Usage: companion skills get <slug>");
429
+ out(await apiGet(base, `/skills/${encodeURIComponent(slug)}`));
430
+ break;
431
+ }
432
+ case "create": {
433
+ const flags = parseFlags(rest);
434
+ if (!flags.name) err("Usage: companion skills create --name <name> [--description <desc>] [--content <markdown>]");
435
+ const body: Record<string, unknown> = { name: flags.name };
436
+ if (flags.description) body.description = flags.description;
437
+ if (flags.content) body.content = flags.content;
438
+ out(await apiPost(base, "/skills", body));
439
+ break;
440
+ }
441
+ case "update": {
442
+ const slug = rest[0];
443
+ if (!slug) err("Usage: companion skills update <slug> --content <markdown>");
444
+ const flags = parseFlags(rest.slice(1));
445
+ if (!flags.content) err("Usage: companion skills update <slug> --content <full SKILL.md content>");
446
+ out(await apiPut(base, `/skills/${encodeURIComponent(slug)}`, { content: flags.content }));
447
+ break;
448
+ }
449
+ case "delete": {
450
+ const slug = rest[0];
451
+ if (!slug) err("Usage: companion skills delete <slug>");
452
+ out(await apiDelete(base, `/skills/${encodeURIComponent(slug)}`));
453
+ break;
454
+ }
455
+ default:
456
+ err(`Unknown skills subcommand: ${sub}. Available: list, get, create, update, delete`);
457
+ }
458
+ }
459
+
460
+ // ─── Main dispatch ──────────────────────────────────────────────────────────
461
+
462
+ function printCtlUsage(): void {
463
+ console.log(`
464
+ Management commands:
465
+
466
+ companion status Overall Companion status
467
+ companion sessions <subcommand> Manage sessions
468
+ companion envs <subcommand> Manage environment profiles
469
+ companion cron <subcommand> Manage scheduled jobs
470
+ companion skills <subcommand> Manage Claude Code skills
471
+ companion settings <subcommand> Manage settings
472
+ companion assistant <subcommand> Manage the Companion Assistant
473
+
474
+ Global options:
475
+ --port <n> Override the Companion API port (default: 3456, or COMPANION_PORT env)
476
+
477
+ Run 'companion <command>' without subcommand for available subcommands.
478
+ `);
479
+ }
480
+
481
+ export async function handleCtlCommand(command: string, rawArgv: string[]): Promise<void> {
482
+ const argv = stripGlobalFlags(rawArgv);
483
+ const base = getBase(rawArgv);
484
+
485
+ try {
486
+ switch (command) {
487
+ case "status":
488
+ await handleStatus(base);
489
+ break;
490
+ case "sessions":
491
+ if (argv.length === 0) err("Usage: companion sessions <list|get|create|kill|relaunch|archive|rename|send-message>");
492
+ await handleSessions(base, argv);
493
+ break;
494
+ case "envs":
495
+ if (argv.length === 0) err("Usage: companion envs <list|get|create|update|delete>");
496
+ await handleEnvs(base, argv);
497
+ break;
498
+ case "cron":
499
+ if (argv.length === 0) err("Usage: companion cron <list|get|create|update|delete|toggle|run|executions>");
500
+ await handleCron(base, argv);
501
+ break;
502
+ case "settings":
503
+ if (argv.length === 0) err("Usage: companion settings <get|set>");
504
+ await handleSettings(base, argv);
505
+ break;
506
+ case "skills":
507
+ if (argv.length === 0) err("Usage: companion skills <list|get|create|update|delete>");
508
+ await handleSkills(base, argv);
509
+ break;
510
+ case "assistant":
511
+ if (argv.length === 0) err("Usage: companion assistant <status|launch|stop|config>");
512
+ await handleAssistant(base, argv);
513
+ break;
514
+ case "ctl-help":
515
+ printCtlUsage();
516
+ break;
517
+ default:
518
+ err(`Unknown command: ${command}`);
519
+ }
520
+ } catch (e) {
521
+ const message = e instanceof Error ? e.message : String(e);
522
+ // Check if it's a connection error
523
+ if (message.includes("ECONNREFUSED") || message.includes("fetch failed")) {
524
+ err(`Cannot connect to The Companion at ${base}. Is the server running?`);
525
+ }
526
+ err(message);
527
+ }
528
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Generate (or regenerate) the HeyHank auth token.
4
+ *
5
+ * Usage:
6
+ * bun run generate-token # show current or auto-generated token
7
+ * bun run generate-token --force # force-regenerate a new token
8
+ */
9
+ import { existsSync, rmSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { homedir } from "node:os";
12
+
13
+ const AUTH_FILE = join(homedir(), ".heyhank", "auth.json");
14
+ const force = process.argv.includes("--force");
15
+
16
+ if (force && existsSync(AUTH_FILE)) {
17
+ rmSync(AUTH_FILE);
18
+ }
19
+
20
+ // Import after potential delete so getToken() generates fresh
21
+ const { getToken } = await import("../server/auth-manager.ts");
22
+
23
+ const token = getToken();
24
+ const isNew = force ? "New" : existsSync(AUTH_FILE) ? "Current" : "Generated";
25
+
26
+ console.log(`\n ${isNew} auth token: ${token}\n`);
27
+ console.log(` Stored at: ${AUTH_FILE}`);
28
+ console.log(` Tip: pass --force to regenerate\n`);
Binary file