@heysummon/consumer-sdk 0.1.1 → 0.2.7

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 CHANGED
@@ -3,25 +3,19 @@
3
3
  * HeySummon Consumer SDK CLI
4
4
  *
5
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
6
+ * Subcommands: submit-and-poll, add-expert, list-experts,
7
+ * check-status, keygen
8
8
  *
9
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,
10
+ * HEYSUMMON_BASE_URL, HEYSUMMON_API_KEY, HEYSUMMON_EXPERTS_FILE,
11
+ * HEYSUMMON_TIMEOUT,
12
12
  * HEYSUMMON_POLL_INTERVAL
13
13
  */
14
14
 
15
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";
16
+ import { ExpertStore } from "./expert-store.js";
17
+ import { generateEphemeralKeys, generatePersistentKeys } from "./crypto.js";
18
+ import type { DecryptedMessage } from "./types.js";
25
19
  import { existsSync } from "node:fs";
26
20
 
27
21
  // ---------------------------------------------------------------------------
@@ -51,177 +45,52 @@ function optEnv(name: string, fallback: string): string {
51
45
  // Commands
52
46
  // ---------------------------------------------------------------------------
53
47
 
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");
48
+ function hasFlag(args: string[], flag: string): boolean {
49
+ return args.includes(flag);
179
50
  }
180
51
 
181
52
  async function cmdSubmitAndPoll(args: string[]): Promise<void> {
182
53
  const question = getArg(args, "--question");
183
54
  if (!question) {
184
- process.stderr.write("Usage: cli submit-and-poll --question <q> [--provider <name>] [--context <json>]\n");
55
+ process.stderr.write("Usage: cli submit-and-poll --question <q> [--expert <name>] [--context <json>] [--requires-approval]\n");
185
56
  process.exit(1);
186
57
  }
187
58
 
188
- const providerArg = getArg(args, "--provider");
59
+ const expertArg = getArg(args, "--expert");
189
60
  const contextArg = getArg(args, "--context");
61
+ const requiresApproval = hasFlag(args, "--requires-approval");
190
62
  const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
191
63
  const timeout = parseInt(optEnv("HEYSUMMON_TIMEOUT", "900"), 10);
192
64
  const pollInterval = parseInt(optEnv("HEYSUMMON_POLL_INTERVAL", "3"), 10);
193
- const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
65
+ const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
194
66
 
195
67
  // Resolve API key
196
68
  let apiKey = process.env.HEYSUMMON_API_KEY || "";
197
- let resolvedProvider = "";
198
69
 
199
- if (providersFile && existsSync(providersFile)) {
200
- const store = new ProviderStore(providersFile);
201
- if (providerArg) {
202
- const match = store.findByName(providerArg);
70
+ if (expertsFile && existsSync(expertsFile)) {
71
+ const store = new ExpertStore(expertsFile);
72
+ if (expertArg) {
73
+ const match = store.findByName(expertArg);
203
74
  if (match) {
204
75
  apiKey = match.apiKey;
205
- resolvedProvider = match.name;
206
76
  } else {
207
- process.stderr.write(`Provider '${providerArg}' not found.\n`);
77
+ process.stderr.write(`Expert '${expertArg}' not found.\n`);
208
78
  process.exit(1);
209
79
  }
210
80
  } else if (!apiKey) {
211
81
  const def = store.getDefault();
212
82
  if (def) {
213
83
  apiKey = def.apiKey;
214
- resolvedProvider = def.name;
215
84
  }
216
85
  }
217
86
  }
218
87
 
219
88
  if (!apiKey) {
220
- process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register a provider.\n");
89
+ process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register an expert.\n");
221
90
  process.exit(1);
222
91
  }
223
92
 
224
- // Generate ephemeral keys (Claude Code style no persistence needed)
93
+ // Generate ephemeral keys (Claude Code style -- no persistence needed)
225
94
  const keys = generateEphemeralKeys();
226
95
  const client = new HeySummonClient({ baseUrl, apiKey });
227
96
 
@@ -237,21 +106,33 @@ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
237
106
  }
238
107
  }
239
108
 
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
- );
109
+ let result;
110
+ try {
111
+ result = await client.submitRequest({
112
+ question,
113
+ messages: messages.length > 0 ? messages : undefined,
114
+ signPublicKey: keys.signPublicKey,
115
+ encryptPublicKey: keys.encryptPublicKey,
116
+ expertName: expertArg || undefined,
117
+ requiresApproval: requiresApproval || undefined,
118
+ });
119
+ } catch (err) {
120
+ if (err instanceof HeySummonHttpError && err.status === 403) {
121
+ let parsed: { error?: string; hint?: string; ip?: string } = {};
122
+ try { parsed = JSON.parse(err.body); } catch { /* ignore */ }
123
+
124
+ const ip = parsed.ip || "unknown";
125
+ const hint = parsed.hint || "";
126
+ process.stderr.write(
127
+ `IP address ${ip} is not authorized for this API key.\n` +
128
+ `${hint ? hint + "\n" : ""}` +
129
+ `Your expert needs to allow this IP address in the HeySummon dashboard under Clients > IP Security.\n` +
130
+ `Once allowed, re-run this command.\n`
131
+ );
132
+ process.stdout.write(`IP_NOT_ALLOWED: IP address ${ip} is not authorized. Ask your expert to allow it in their HeySummon dashboard.\n`);
133
+ return;
134
+ }
135
+ throw err;
255
136
  }
256
137
 
257
138
  if (!result.requestId) {
@@ -260,8 +141,19 @@ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
260
141
  }
261
142
 
262
143
  const ref = result.refCode || result.requestId;
144
+
145
+ // When expert is unavailable, the platform rejects the request
146
+ if (result.rejected) {
147
+ const msg = result.message || "Expert is not available right now.";
148
+ process.stderr.write(`${msg}\n`);
149
+ process.stdout.write(
150
+ `EXPERT_UNAVAILABLE: ${msg}${result.nextAvailableAt ? ` Next available: ${result.nextAvailableAt}` : ""}\n`
151
+ );
152
+ return;
153
+ }
154
+
263
155
  process.stderr.write(
264
- `Request submitted [${ref}] waiting for human response...\n`
156
+ `Request submitted [${ref}] -- waiting for human response...\n`
265
157
  );
266
158
  process.stderr.write(
267
159
  ` (timeout: ${timeout}s, polling every ${pollInterval}s)\n`
@@ -276,6 +168,18 @@ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
276
168
  try {
277
169
  // Check status endpoint
278
170
  const status = await client.getRequestStatus(result.requestId);
171
+
172
+ // Approval decision (Approve/Deny buttons)
173
+ if (
174
+ (status.status === "responded" || status.status === "closed") &&
175
+ status.approvalDecision
176
+ ) {
177
+ process.stderr.write(`\nHuman responded [${ref}] -- decision: ${status.approvalDecision}\n`);
178
+ process.stdout.write(`${status.approvalDecision.toUpperCase()}\n`);
179
+ await client.ackEvent(result.requestId).catch(() => {});
180
+ return;
181
+ }
182
+
279
183
  if (
280
184
  (status.status === "responded" || status.status === "closed") &&
281
185
  status.response
@@ -287,15 +191,15 @@ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
287
191
 
288
192
  // Fallback: check messages for plaintext replies
289
193
  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) {
194
+ const expertMsg = msgs.filter((m: DecryptedMessage) => m.from === "expert").pop();
195
+ if (expertMsg) {
196
+ if (expertMsg.plaintext) {
293
197
  process.stderr.write(`\nHuman responded [${ref}]\n`);
294
- process.stdout.write(providerMsg.plaintext + "\n");
198
+ process.stdout.write(expertMsg.plaintext + "\n");
295
199
  await client.ackEvent(result.requestId).catch(() => {});
296
200
  return;
297
201
  }
298
- if (providerMsg.ciphertext) {
202
+ if (expertMsg.ciphertext) {
299
203
  process.stderr.write(`\nHuman responded [${ref}]\n`);
300
204
  process.stdout.write("(encrypted response received)\n");
301
205
  await client.ackEvent(result.requestId).catch(() => {});
@@ -312,25 +216,28 @@ async function cmdSubmitAndPoll(args: string[]): Promise<void> {
312
216
  }
313
217
  }
314
218
 
315
- process.stderr.write(`\nTimeout after ${timeout}s no response received.\n`);
219
+ // Report timeout to server (notifies expert, records timestamp)
220
+ await client.reportTimeout(result.requestId).catch(() => {});
221
+
222
+ process.stderr.write(`\nTimeout after ${timeout}s -- no response received.\n`);
316
223
  process.stderr.write(` Request ref: ${ref}\n`);
317
224
  process.stdout.write(
318
- `TIMEOUT: No response received after ${timeout}s for request ${ref}. The provider may still respond later.\n`
225
+ `TIMEOUT: No answer came back from the expert within the ${timeout}s timeout window. Request ref: ${ref}\n`
319
226
  );
320
227
  }
321
228
 
322
- async function cmdAddProvider(args: string[]): Promise<void> {
229
+ async function cmdAddExpert(args: string[]): Promise<void> {
323
230
  const key = getArg(args, "--key");
324
231
  const alias = getArg(args, "--alias");
325
232
 
326
233
  if (!key) {
327
- process.stderr.write("Usage: cli add-provider --key <api-key> [--alias <name>]\n");
234
+ process.stderr.write("Usage: cli add-expert --key <api-key> [--alias <name>]\n");
328
235
  process.exit(1);
329
236
  }
330
237
 
331
238
  // 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");
239
+ if (key.startsWith("hs_exp_") || key.startsWith("htl_exp_")) {
240
+ process.stderr.write("This is an expert key. Use a CLIENT key (hs_cli_... or htl_...).\n");
334
241
  process.exit(1);
335
242
  }
336
243
  if (!key.startsWith("hs_cli_") && !key.startsWith("htl_")) {
@@ -339,56 +246,56 @@ async function cmdAddProvider(args: string[]): Promise<void> {
339
246
  }
340
247
 
341
248
  const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
342
- const providersFile = requireEnv("HEYSUMMON_PROVIDERS_FILE");
249
+ const expertsFile = requireEnv("HEYSUMMON_EXPERTS_FILE");
343
250
 
344
251
  const client = new HeySummonClient({ baseUrl, apiKey: key });
345
252
  const whoami = await client.whoami();
346
253
 
347
- const providerName = whoami.provider?.name || "";
348
- const providerId = whoami.provider?.id || "";
254
+ const expertName = whoami.expert?.name || "";
255
+ const expertId = whoami.expert?.id || "";
349
256
 
350
- if (!providerName) {
351
- process.stderr.write("Could not fetch provider info. Is the key valid?\n");
257
+ if (!expertName) {
258
+ process.stderr.write("Could not fetch expert info. Is the key valid?\n");
352
259
  process.exit(1);
353
260
  }
354
261
 
355
- const name = alias || providerName;
356
- const store = new ProviderStore(providersFile);
262
+ const name = alias || expertName;
263
+ const store = new ExpertStore(expertsFile);
357
264
  store.add({
358
265
  name,
359
266
  apiKey: key,
360
- providerId,
361
- providerName,
267
+ expertId,
268
+ expertName,
362
269
  });
363
270
 
364
271
  const count = store.load().length;
365
- process.stdout.write(`Provider added: ${name} (${providerName})\n`);
366
- process.stdout.write(`Providers registered: ${count}\n`);
272
+ process.stdout.write(`Expert added: ${name} (${expertName})\n`);
273
+ process.stdout.write(`Experts registered: ${count}\n`);
367
274
  }
368
275
 
369
- async function cmdListProviders(): Promise<void> {
370
- const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
276
+ async function cmdListExperts(): Promise<void> {
277
+ const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
371
278
 
372
- if (!providersFile || !existsSync(providersFile)) {
373
- process.stdout.write("No providers registered yet.\n");
279
+ if (!expertsFile || !existsSync(expertsFile)) {
280
+ process.stdout.write("No experts registered yet.\n");
374
281
  return;
375
282
  }
376
283
 
377
- const store = new ProviderStore(providersFile);
378
- const providers = store.load();
284
+ const store = new ExpertStore(expertsFile);
285
+ const experts = store.load();
379
286
 
380
- if (providers.length === 0) {
381
- process.stdout.write("No providers registered yet.\n");
287
+ if (experts.length === 0) {
288
+ process.stdout.write("No experts registered yet.\n");
382
289
  return;
383
290
  }
384
291
 
385
- process.stdout.write(`Registered providers (${providers.length}):\n`);
386
- for (let i = 0; i < providers.length; i++) {
387
- const p = providers[i];
292
+ process.stdout.write(`Registered experts (${experts.length}):\n`);
293
+ for (let i = 0; i < experts.length; i++) {
294
+ const p = experts[i];
388
295
  const nameExtra =
389
- p.providerName !== p.name ? ` (provider: ${p.providerName})` : "";
296
+ p.expertName !== p.name ? ` (expert: ${p.expertName})` : "";
390
297
  process.stdout.write(
391
- ` ${i + 1}. ${p.name}${nameExtra} key: ${p.apiKey.slice(0, 12)}...\n`
298
+ ` ${i + 1}. ${p.name}${nameExtra} -- key: ${p.apiKey.slice(0, 12)}...\n`
392
299
  );
393
300
  }
394
301
  }
@@ -408,14 +315,14 @@ async function cmdCheckStatus(args: string[]): Promise<void> {
408
315
  try {
409
316
  const data = await client.getRequestStatus(ref);
410
317
  const icons: Record<string, string> = {
411
- pending: "",
412
- responded: "",
413
- resolved: "",
414
- expired: "",
415
- cancelled: "",
318
+ pending: "...",
319
+ responded: "[ok]",
320
+ resolved: "[ok]",
321
+ expired: "[expired]",
322
+ cancelled: "[cancelled]",
416
323
  };
417
324
  const status = data.status || "unknown";
418
- process.stdout.write(`${icons[status] || ""} Status: ${status}\n`);
325
+ process.stdout.write(`${icons[status] || "-"} Status: ${status}\n`);
419
326
  if (data.refCode) process.stdout.write(` Ref: ${data.refCode}\n`);
420
327
  if (data.response || data.lastMessage) {
421
328
  process.stdout.write(` Response: ${data.response || data.lastMessage}\n`);
@@ -426,211 +333,6 @@ async function cmdCheckStatus(args: string[]): Promise<void> {
426
333
  }
427
334
  }
428
335
 
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
336
  async function cmdKeygen(args: string[]): Promise<void> {
635
337
  const dir = getArg(args, "--dir") || optEnv("HEYSUMMON_KEY_DIR", "");
636
338
 
@@ -659,12 +361,10 @@ function sleep(ms: number): Promise<void> {
659
361
  const [command, ...rest] = process.argv.slice(2);
660
362
 
661
363
  const commands: Record<string, (args: string[]) => Promise<void>> = {
662
- submit: cmdSubmit,
663
364
  "submit-and-poll": cmdSubmitAndPoll,
664
- "add-provider": cmdAddProvider,
665
- "list-providers": cmdListProviders,
365
+ "add-expert": cmdAddExpert,
366
+ "list-experts": cmdListExperts,
666
367
  "check-status": cmdCheckStatus,
667
- watch: cmdWatch,
668
368
  keygen: cmdKeygen,
669
369
  };
670
370