@heysummon/consumer-sdk 0.1.1

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/src/cli.ts ADDED
@@ -0,0 +1,683 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HeySummon Consumer SDK CLI
4
+ *
5
+ * Shared CLI entry point used by both Claude Code and OpenClaw skill scripts.
6
+ * Subcommands: submit, submit-and-poll, add-provider, list-providers,
7
+ * check-status, watch, keygen
8
+ *
9
+ * All config comes from environment variables (set by the calling bash wrapper):
10
+ * HEYSUMMON_BASE_URL, HEYSUMMON_API_KEY, HEYSUMMON_PROVIDERS_FILE,
11
+ * HEYSUMMON_KEY_DIR, HEYSUMMON_REQUESTS_DIR, HEYSUMMON_TIMEOUT,
12
+ * HEYSUMMON_POLL_INTERVAL
13
+ */
14
+
15
+ import { HeySummonClient, HeySummonHttpError } from "./client.js";
16
+ import { ProviderStore } from "./provider-store.js";
17
+ import { RequestTracker } from "./request-tracker.js";
18
+ import {
19
+ generateEphemeralKeys,
20
+ generatePersistentKeys,
21
+ loadPublicKeys,
22
+ } from "./crypto.js";
23
+ import type { PendingEvent, Message } from "./types.js";
24
+ import { execSync } from "node:child_process";
25
+ import { existsSync } from "node:fs";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Arg parsing helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function getArg(args: string[], flag: string): string | undefined {
32
+ const idx = args.indexOf(flag);
33
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
34
+ return args[idx + 1];
35
+ }
36
+
37
+ function requireEnv(name: string): string {
38
+ const val = process.env[name];
39
+ if (!val) {
40
+ process.stderr.write(`Missing required env var: ${name}\n`);
41
+ process.exit(1);
42
+ }
43
+ return val;
44
+ }
45
+
46
+ function optEnv(name: string, fallback: string): string {
47
+ return process.env[name] || fallback;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Commands
52
+ // ---------------------------------------------------------------------------
53
+
54
+ async function cmdSubmit(args: string[]): Promise<void> {
55
+ const question = getArg(args, "--question");
56
+ if (!question) {
57
+ process.stderr.write("Usage: cli submit --question <q> [--provider <name>] [--context <json>]\n");
58
+ process.exit(1);
59
+ }
60
+
61
+ const providerArg = getArg(args, "--provider");
62
+ const contextArg = getArg(args, "--context");
63
+ const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
64
+ const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
65
+ const keyDir = optEnv("HEYSUMMON_KEY_DIR", "");
66
+ const requestsDir = optEnv("HEYSUMMON_REQUESTS_DIR", "");
67
+
68
+ // Resolve API key from provider store or env
69
+ let apiKey = process.env.HEYSUMMON_API_KEY || "";
70
+ let resolvedProvider = "";
71
+
72
+ if (providersFile && existsSync(providersFile)) {
73
+ const store = new ProviderStore(providersFile);
74
+
75
+ if (providerArg) {
76
+ const match = store.findByName(providerArg);
77
+ if (match) {
78
+ apiKey = match.apiKey;
79
+ resolvedProvider = match.name;
80
+ } else {
81
+ process.stderr.write(`Provider '${providerArg}' not found.\n`);
82
+ const all = store.load();
83
+ if (all.length) {
84
+ process.stderr.write("Available:\n");
85
+ for (const p of all) {
86
+ process.stderr.write(` - ${p.name} (${p.providerName})\n`);
87
+ }
88
+ }
89
+ process.exit(1);
90
+ }
91
+ } else if (!apiKey) {
92
+ const def = store.getDefault();
93
+ if (def) {
94
+ apiKey = def.apiKey;
95
+ resolvedProvider = def.name;
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!apiKey) {
101
+ process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register a provider.\n");
102
+ process.exit(1);
103
+ }
104
+
105
+ // Generate or load keys
106
+ let signPublicKey: string;
107
+ let encryptPublicKey: string;
108
+
109
+ if (keyDir && existsSync(`${keyDir}/sign_public.pem`)) {
110
+ const keys = loadPublicKeys(keyDir);
111
+ signPublicKey = keys.signPublicKey;
112
+ encryptPublicKey = keys.encryptPublicKey;
113
+ } else if (keyDir) {
114
+ process.stderr.write(`Generating keypairs in ${keyDir}...\n`);
115
+ const keys = generatePersistentKeys(keyDir);
116
+ signPublicKey = keys.signPublicKey;
117
+ encryptPublicKey = keys.encryptPublicKey;
118
+ } else {
119
+ const keys = generateEphemeralKeys();
120
+ signPublicKey = keys.signPublicKey;
121
+ encryptPublicKey = keys.encryptPublicKey;
122
+ }
123
+
124
+ // Parse context messages
125
+ let messages: Array<{ role: string; content: string }> = [];
126
+ if (contextArg) {
127
+ try {
128
+ messages = JSON.parse(contextArg);
129
+ } catch {
130
+ // ignore parse errors
131
+ }
132
+ }
133
+
134
+ const client = new HeySummonClient({ baseUrl, apiKey });
135
+
136
+ if (resolvedProvider) {
137
+ process.stderr.write(`Provider: ${resolvedProvider}\n`);
138
+ }
139
+
140
+ const result = await client.submitRequest({
141
+ question,
142
+ messages: messages.length > 0 ? messages : undefined,
143
+ signPublicKey,
144
+ encryptPublicKey,
145
+ providerName: providerArg || undefined,
146
+ });
147
+
148
+ if (!result.requestId) {
149
+ process.stderr.write(`Request failed: ${JSON.stringify(result)}\n`);
150
+ process.exit(1);
151
+ }
152
+
153
+ // Track request
154
+ if (requestsDir) {
155
+ const tracker = new RequestTracker(requestsDir);
156
+ tracker.track(result.requestId, result.refCode, resolvedProvider || undefined);
157
+ }
158
+
159
+ // Sync provider name
160
+ if (providersFile && existsSync(providersFile) && apiKey) {
161
+ try {
162
+ const whoami = await client.whoami();
163
+ const pName = whoami.provider?.name || "";
164
+ if (pName) {
165
+ const store = new ProviderStore(providersFile);
166
+ const entry = store.findByKey(apiKey);
167
+ if (entry && entry.providerName !== pName) {
168
+ store.add({ ...entry, providerName: pName });
169
+ process.stderr.write(`Provider name updated: ${pName}\n`);
170
+ }
171
+ }
172
+ } catch {
173
+ // non-fatal
174
+ }
175
+ }
176
+
177
+ // Output result as JSON
178
+ process.stdout.write(JSON.stringify(result) + "\n");
179
+ }
180
+
181
+ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
182
+ const question = getArg(args, "--question");
183
+ if (!question) {
184
+ process.stderr.write("Usage: cli submit-and-poll --question <q> [--provider <name>] [--context <json>]\n");
185
+ process.exit(1);
186
+ }
187
+
188
+ const providerArg = getArg(args, "--provider");
189
+ const contextArg = getArg(args, "--context");
190
+ const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
191
+ const timeout = parseInt(optEnv("HEYSUMMON_TIMEOUT", "900"), 10);
192
+ const pollInterval = parseInt(optEnv("HEYSUMMON_POLL_INTERVAL", "3"), 10);
193
+ const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
194
+
195
+ // Resolve API key
196
+ let apiKey = process.env.HEYSUMMON_API_KEY || "";
197
+ let resolvedProvider = "";
198
+
199
+ if (providersFile && existsSync(providersFile)) {
200
+ const store = new ProviderStore(providersFile);
201
+ if (providerArg) {
202
+ const match = store.findByName(providerArg);
203
+ if (match) {
204
+ apiKey = match.apiKey;
205
+ resolvedProvider = match.name;
206
+ } else {
207
+ process.stderr.write(`Provider '${providerArg}' not found.\n`);
208
+ process.exit(1);
209
+ }
210
+ } else if (!apiKey) {
211
+ const def = store.getDefault();
212
+ if (def) {
213
+ apiKey = def.apiKey;
214
+ resolvedProvider = def.name;
215
+ }
216
+ }
217
+ }
218
+
219
+ if (!apiKey) {
220
+ process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register a provider.\n");
221
+ process.exit(1);
222
+ }
223
+
224
+ // Generate ephemeral keys (Claude Code style — no persistence needed)
225
+ const keys = generateEphemeralKeys();
226
+ const client = new HeySummonClient({ baseUrl, apiKey });
227
+
228
+ process.stderr.write("HeySummon: Submitting request to human...\n");
229
+
230
+ // Parse context
231
+ let messages: Array<{ role: string; content: string }> = [];
232
+ if (contextArg) {
233
+ try {
234
+ messages = JSON.parse(contextArg);
235
+ } catch {
236
+ // ignore
237
+ }
238
+ }
239
+
240
+ const result = await client.submitRequest({
241
+ question,
242
+ messages: messages.length > 0 ? messages : undefined,
243
+ signPublicKey: keys.signPublicKey,
244
+ encryptPublicKey: keys.encryptPublicKey,
245
+ providerName: providerArg || undefined,
246
+ });
247
+
248
+ if (result.providerUnavailable) {
249
+ const next = result.nextAvailableAt
250
+ ? ` (available at ${new Date(result.nextAvailableAt).toLocaleTimeString()})`
251
+ : "";
252
+ process.stderr.write(
253
+ `Provider currently unavailable${next} — request queued, waiting for response...\n`
254
+ );
255
+ }
256
+
257
+ if (!result.requestId) {
258
+ process.stderr.write(`Failed to submit request: ${JSON.stringify(result)}\n`);
259
+ process.exit(1);
260
+ }
261
+
262
+ const ref = result.refCode || result.requestId;
263
+ process.stderr.write(
264
+ `Request submitted [${ref}] — waiting for human response...\n`
265
+ );
266
+ process.stderr.write(
267
+ ` (timeout: ${timeout}s, polling every ${pollInterval}s)\n`
268
+ );
269
+
270
+ // Polling loop
271
+ let elapsed = 0;
272
+ while (elapsed < timeout) {
273
+ await sleep(pollInterval * 1000);
274
+ elapsed += pollInterval;
275
+
276
+ try {
277
+ // Check status endpoint
278
+ const status = await client.getRequestStatus(result.requestId);
279
+ if (
280
+ (status.status === "responded" || status.status === "closed") &&
281
+ status.response
282
+ ) {
283
+ process.stderr.write(`\nHuman responded [${ref}]\n`);
284
+ process.stdout.write(status.response + "\n");
285
+ return;
286
+ }
287
+
288
+ // Fallback: check messages for plaintext replies
289
+ const { messages: msgs } = await client.getMessages(result.requestId);
290
+ const providerMsg = msgs.filter((m: Message) => m.from === "provider").pop();
291
+ if (providerMsg) {
292
+ if (providerMsg.plaintext) {
293
+ process.stderr.write(`\nHuman responded [${ref}]\n`);
294
+ process.stdout.write(providerMsg.plaintext + "\n");
295
+ await client.ackEvent(result.requestId).catch(() => {});
296
+ return;
297
+ }
298
+ if (providerMsg.ciphertext) {
299
+ process.stderr.write(`\nHuman responded [${ref}]\n`);
300
+ process.stdout.write("(encrypted response received)\n");
301
+ await client.ackEvent(result.requestId).catch(() => {});
302
+ return;
303
+ }
304
+ }
305
+ } catch {
306
+ // polling error, continue
307
+ }
308
+
309
+ // Progress indicator
310
+ if (elapsed % 30 === 0) {
311
+ process.stderr.write(` Still waiting... (${elapsed}s elapsed)\n`);
312
+ }
313
+ }
314
+
315
+ process.stderr.write(`\nTimeout after ${timeout}s — no response received.\n`);
316
+ process.stderr.write(` Request ref: ${ref}\n`);
317
+ process.stdout.write(
318
+ `TIMEOUT: No response received after ${timeout}s for request ${ref}. The provider may still respond later.\n`
319
+ );
320
+ }
321
+
322
+ async function cmdAddProvider(args: string[]): Promise<void> {
323
+ const key = getArg(args, "--key");
324
+ const alias = getArg(args, "--alias");
325
+
326
+ if (!key) {
327
+ process.stderr.write("Usage: cli add-provider --key <api-key> [--alias <name>]\n");
328
+ process.exit(1);
329
+ }
330
+
331
+ // Validate key prefix
332
+ if (key.startsWith("hs_prov_") || key.startsWith("htl_prov_")) {
333
+ process.stderr.write("This is a provider key. Use a CLIENT key (hs_cli_... or htl_...).\n");
334
+ process.exit(1);
335
+ }
336
+ if (!key.startsWith("hs_cli_") && !key.startsWith("htl_")) {
337
+ process.stderr.write("Invalid key format. Must start with 'hs_cli_' or 'htl_'.\n");
338
+ process.exit(1);
339
+ }
340
+
341
+ const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
342
+ const providersFile = requireEnv("HEYSUMMON_PROVIDERS_FILE");
343
+
344
+ const client = new HeySummonClient({ baseUrl, apiKey: key });
345
+ const whoami = await client.whoami();
346
+
347
+ const providerName = whoami.provider?.name || "";
348
+ const providerId = whoami.provider?.id || "";
349
+
350
+ if (!providerName) {
351
+ process.stderr.write("Could not fetch provider info. Is the key valid?\n");
352
+ process.exit(1);
353
+ }
354
+
355
+ const name = alias || providerName;
356
+ const store = new ProviderStore(providersFile);
357
+ store.add({
358
+ name,
359
+ apiKey: key,
360
+ providerId,
361
+ providerName,
362
+ });
363
+
364
+ const count = store.load().length;
365
+ process.stdout.write(`Provider added: ${name} (${providerName})\n`);
366
+ process.stdout.write(`Providers registered: ${count}\n`);
367
+ }
368
+
369
+ async function cmdListProviders(): Promise<void> {
370
+ const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
371
+
372
+ if (!providersFile || !existsSync(providersFile)) {
373
+ process.stdout.write("No providers registered yet.\n");
374
+ return;
375
+ }
376
+
377
+ const store = new ProviderStore(providersFile);
378
+ const providers = store.load();
379
+
380
+ if (providers.length === 0) {
381
+ process.stdout.write("No providers registered yet.\n");
382
+ return;
383
+ }
384
+
385
+ process.stdout.write(`Registered providers (${providers.length}):\n`);
386
+ for (let i = 0; i < providers.length; i++) {
387
+ const p = providers[i];
388
+ const nameExtra =
389
+ p.providerName !== p.name ? ` (provider: ${p.providerName})` : "";
390
+ process.stdout.write(
391
+ ` ${i + 1}. ${p.name}${nameExtra} — key: ${p.apiKey.slice(0, 12)}...\n`
392
+ );
393
+ }
394
+ }
395
+
396
+ async function cmdCheckStatus(args: string[]): Promise<void> {
397
+ const ref = getArg(args, "--ref") || args[args.indexOf("check-status") + 1];
398
+
399
+ if (!ref) {
400
+ process.stderr.write("Usage: cli check-status --ref <refCode|requestId>\n");
401
+ process.exit(1);
402
+ }
403
+
404
+ const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
405
+ const apiKey = requireEnv("HEYSUMMON_API_KEY");
406
+ const client = new HeySummonClient({ baseUrl, apiKey });
407
+
408
+ try {
409
+ const data = await client.getRequestStatus(ref);
410
+ const icons: Record<string, string> = {
411
+ pending: "⏳",
412
+ responded: "✅",
413
+ resolved: "✅",
414
+ expired: "⏰",
415
+ cancelled: "❌",
416
+ };
417
+ const status = data.status || "unknown";
418
+ process.stdout.write(`${icons[status] || "•"} Status: ${status}\n`);
419
+ if (data.refCode) process.stdout.write(` Ref: ${data.refCode}\n`);
420
+ if (data.response || data.lastMessage) {
421
+ process.stdout.write(` Response: ${data.response || data.lastMessage}\n`);
422
+ }
423
+ } catch (err) {
424
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
425
+ process.exit(1);
426
+ }
427
+ }
428
+
429
+ async function cmdWatch(args: string[]): Promise<void> {
430
+ const notifyScript = getArg(args, "--notify-script");
431
+ const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
432
+ const providersFile = requireEnv("HEYSUMMON_PROVIDERS_FILE");
433
+ const requestsDir = optEnv("HEYSUMMON_REQUESTS_DIR", "");
434
+ const pollInterval = parseInt(optEnv("HEYSUMMON_POLL_INTERVAL", "5"), 10);
435
+
436
+ const store = new ProviderStore(providersFile);
437
+ const tracker = requestsDir ? new RequestTracker(requestsDir) : null;
438
+
439
+ // In-memory dedup set
440
+ const seen = new Set<string>();
441
+
442
+ // Track persistent auth errors per provider — auto-shutdown after 30 min
443
+ const AUTH_SHUTDOWN_MS = 30 * 60 * 1000; // 30 minutes
444
+ const authErrors = new Map<string, { firstAt: number; count: number }>();
445
+
446
+ process.stderr.write(`Platform watcher started (pid ${process.pid})\n`);
447
+ process.stderr.write(` Polling every ${pollInterval}s\n`);
448
+
449
+ // Write PID file for signaling
450
+ if (requestsDir) {
451
+ const { writeFileSync } = await import("node:fs");
452
+ const { join } = await import("node:path");
453
+ writeFileSync(join(requestsDir, ".watcher.pid"), String(process.pid));
454
+ }
455
+
456
+ while (true) {
457
+ const providers = store.load();
458
+ if (providers.length === 0) {
459
+ process.stderr.write("No providers registered.\n");
460
+ await sleep(pollInterval * 1000);
461
+ continue;
462
+ }
463
+
464
+ for (const provider of providers) {
465
+ try {
466
+ const client = new HeySummonClient({
467
+ baseUrl,
468
+ apiKey: provider.apiKey,
469
+ });
470
+ const { events } = await client.getPendingEvents();
471
+
472
+ for (const event of events) {
473
+ const from = event.from || "unknown";
474
+ const dedupKey = `${event.type}:${from}:${event.requestId}`;
475
+
476
+ // Stale check (>30 min)
477
+ if (event.createdAt) {
478
+ const age =
479
+ (Date.now() - new Date(event.createdAt).getTime()) / 1000 / 60;
480
+ if (age > 30) {
481
+ seen.add(dedupKey);
482
+ continue;
483
+ }
484
+ }
485
+
486
+ if (seen.has(dedupKey)) continue;
487
+ seen.add(dedupKey);
488
+
489
+ // Fetch response text for provider messages
490
+ let responseText = "";
491
+ if (
492
+ event.type === "new_message" &&
493
+ event.from === "provider" &&
494
+ event.requestId
495
+ ) {
496
+ try {
497
+ const { messages } = await client.getMessages(event.requestId);
498
+ const last = messages
499
+ .filter((m: Message) => m.from === "provider")
500
+ .pop();
501
+ if (last?.plaintext) {
502
+ responseText = last.plaintext;
503
+ } else if (last?.ciphertext) {
504
+ responseText = "(encrypted)";
505
+ }
506
+ } catch {
507
+ // non-fatal
508
+ }
509
+ }
510
+
511
+ // Build notification
512
+ const fileRef = tracker?.getRefCode(event.requestId) || "";
513
+ const ref = event.refCode || fileRef || event.requestId || "?";
514
+
515
+ let msg = "";
516
+ switch (event.type) {
517
+ case "keys_exchanged":
518
+ msg = `Key exchange completed for ${ref} — provider connected`;
519
+ break;
520
+ case "new_message":
521
+ if (event.from === "provider") {
522
+ msg = `New response from provider for ${ref}`;
523
+ if (responseText) msg += `\n${responseText}`;
524
+ }
525
+ break;
526
+ case "responded":
527
+ msg = `Provider responded to ${ref}`;
528
+ break;
529
+ case "closed":
530
+ msg = `Conversation ${ref} closed`;
531
+ if (tracker) tracker.remove(event.requestId);
532
+ break;
533
+ default:
534
+ msg = `HeySummon event (${event.type}) for ${ref}`;
535
+ }
536
+
537
+ if (!msg) continue;
538
+
539
+ // Build wake text with original question context
540
+ let wakeText = msg;
541
+ if (responseText && event.requestId) {
542
+ try {
543
+ const reqData = await client.getRequestByRef(fileRef || event.requestId);
544
+ const origQuestion = reqData.question || "";
545
+ const provName =
546
+ reqData.provider?.name || reqData.providerName || "the provider";
547
+
548
+ wakeText = `${ref} — ${provName} responded!`;
549
+ if (origQuestion)
550
+ wakeText += `\n\nYour question was: ${origQuestion}`;
551
+ wakeText += `\n\nAnswer: ${responseText}`;
552
+ wakeText += `\n\nProceed based on this answer.`;
553
+ } catch {
554
+ // non-fatal, use plain msg
555
+ }
556
+ }
557
+
558
+ // Deliver notification
559
+ if (notifyScript) {
560
+ try {
561
+ const eventJson = JSON.stringify({
562
+ event,
563
+ msg,
564
+ wakeText,
565
+ responseText,
566
+ ref,
567
+ });
568
+ execSync(`bash "${notifyScript}"`, {
569
+ input: eventJson,
570
+ stdio: ["pipe", "inherit", "inherit"],
571
+ timeout: 30_000,
572
+ });
573
+ } catch {
574
+ process.stderr.write(`Notify script failed for ${ref}\n`);
575
+ }
576
+ } else {
577
+ process.stdout.write(`${msg}\n`);
578
+ }
579
+
580
+ // ACK the event
581
+ await client.ackEvent(event.requestId).catch(() => {});
582
+ }
583
+ } catch (err) {
584
+ const isAuth = err instanceof HeySummonHttpError && err.isAuthError;
585
+
586
+ if (isAuth) {
587
+ const key = provider.apiKey;
588
+ const existing = authErrors.get(key);
589
+ if (!existing) {
590
+ authErrors.set(key, { firstAt: Date.now(), count: 1 });
591
+ process.stderr.write(
592
+ `Auth error for "${provider.name}" (${err instanceof HeySummonHttpError ? `HTTP ${err.status}` : "unknown"}). Will retry...\n`
593
+ );
594
+ } else {
595
+ existing.count++;
596
+ const elapsed = Date.now() - existing.firstAt;
597
+ if (elapsed >= AUTH_SHUTDOWN_MS) {
598
+ const status = err instanceof HeySummonHttpError ? err.status : "?";
599
+ const detail = err instanceof HeySummonHttpError ? err.body : String(err);
600
+ process.stderr.write(
601
+ `\n` +
602
+ `══════════════════════════════════════════════════════\n` +
603
+ ` HeySummon watcher shutting down\n` +
604
+ `\n` +
605
+ ` Provider "${provider.name}" has returned HTTP ${status}\n` +
606
+ ` for ${Math.round(elapsed / 60000)} minutes (${existing.count} attempts).\n` +
607
+ `\n` +
608
+ ` This usually means the API key was deleted or\n` +
609
+ ` deactivated on the HeySummon platform.\n` +
610
+ `\n` +
611
+ ` Detail: ${detail}\n` +
612
+ `\n` +
613
+ ` To fix: ask your provider for a new setup link,\n` +
614
+ ` then re-run the setup to get fresh credentials.\n` +
615
+ `══════════════════════════════════════════════════════\n`
616
+ );
617
+ process.exit(1);
618
+ }
619
+ }
620
+ } else {
621
+ // Clear auth error streak on non-auth errors (network issues etc.)
622
+ authErrors.delete(provider.apiKey);
623
+ process.stderr.write(
624
+ `Poll error for ${provider.name}: ${err instanceof Error ? err.message : String(err)}\n`
625
+ );
626
+ }
627
+ }
628
+ }
629
+
630
+ await sleep(pollInterval * 1000);
631
+ }
632
+ }
633
+
634
+ async function cmdKeygen(args: string[]): Promise<void> {
635
+ const dir = getArg(args, "--dir") || optEnv("HEYSUMMON_KEY_DIR", "");
636
+
637
+ if (!dir) {
638
+ process.stderr.write("Usage: cli keygen --dir <path>\n");
639
+ process.exit(1);
640
+ }
641
+
642
+ const keys = generatePersistentKeys(dir);
643
+ process.stderr.write(`Keypairs generated in ${dir}\n`);
644
+ process.stdout.write(JSON.stringify(keys) + "\n");
645
+ }
646
+
647
+ // ---------------------------------------------------------------------------
648
+ // Helpers
649
+ // ---------------------------------------------------------------------------
650
+
651
+ function sleep(ms: number): Promise<void> {
652
+ return new Promise((resolve) => setTimeout(resolve, ms));
653
+ }
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // Main
657
+ // ---------------------------------------------------------------------------
658
+
659
+ const [command, ...rest] = process.argv.slice(2);
660
+
661
+ const commands: Record<string, (args: string[]) => Promise<void>> = {
662
+ submit: cmdSubmit,
663
+ "submit-and-poll": cmdSubmitAndPoll,
664
+ "add-provider": cmdAddProvider,
665
+ "list-providers": cmdListProviders,
666
+ "check-status": cmdCheckStatus,
667
+ watch: cmdWatch,
668
+ keygen: cmdKeygen,
669
+ };
670
+
671
+ if (!command || !commands[command]) {
672
+ process.stderr.write(
673
+ `Usage: cli <command> [args]\n\nCommands:\n ${Object.keys(commands).join("\n ")}\n`
674
+ );
675
+ process.exit(1);
676
+ }
677
+
678
+ commands[command](rest).catch((err) => {
679
+ process.stderr.write(
680
+ `Error: ${err instanceof Error ? err.message : String(err)}\n`
681
+ );
682
+ process.exit(1);
683
+ });