@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/dist/cli.js CHANGED
@@ -3,19 +3,17 @@
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
  import { HeySummonClient, HeySummonHttpError } from "./client.js";
15
- import { ProviderStore } from "./provider-store.js";
16
- import { RequestTracker } from "./request-tracker.js";
17
- import { generateEphemeralKeys, generatePersistentKeys, loadPublicKeys, } from "./crypto.js";
18
- import { execSync } from "node:child_process";
15
+ import { ExpertStore } from "./expert-store.js";
16
+ import { generateEphemeralKeys, generatePersistentKeys } from "./crypto.js";
19
17
  import { existsSync } from "node:fs";
20
18
  // ---------------------------------------------------------------------------
21
19
  // Arg parsing helpers
@@ -40,148 +38,33 @@ function optEnv(name, fallback) {
40
38
  // ---------------------------------------------------------------------------
41
39
  // Commands
42
40
  // ---------------------------------------------------------------------------
43
- async function cmdSubmit(args) {
44
- const question = getArg(args, "--question");
45
- if (!question) {
46
- process.stderr.write("Usage: cli submit --question <q> [--provider <name>] [--context <json>]\n");
47
- process.exit(1);
48
- }
49
- const providerArg = getArg(args, "--provider");
50
- const contextArg = getArg(args, "--context");
51
- const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
52
- const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
53
- const keyDir = optEnv("HEYSUMMON_KEY_DIR", "");
54
- const requestsDir = optEnv("HEYSUMMON_REQUESTS_DIR", "");
55
- // Resolve API key from provider store or env
56
- let apiKey = process.env.HEYSUMMON_API_KEY || "";
57
- let resolvedProvider = "";
58
- if (providersFile && existsSync(providersFile)) {
59
- const store = new ProviderStore(providersFile);
60
- if (providerArg) {
61
- const match = store.findByName(providerArg);
62
- if (match) {
63
- apiKey = match.apiKey;
64
- resolvedProvider = match.name;
65
- }
66
- else {
67
- process.stderr.write(`Provider '${providerArg}' not found.\n`);
68
- const all = store.load();
69
- if (all.length) {
70
- process.stderr.write("Available:\n");
71
- for (const p of all) {
72
- process.stderr.write(` - ${p.name} (${p.providerName})\n`);
73
- }
74
- }
75
- process.exit(1);
76
- }
77
- }
78
- else if (!apiKey) {
79
- const def = store.getDefault();
80
- if (def) {
81
- apiKey = def.apiKey;
82
- resolvedProvider = def.name;
83
- }
84
- }
85
- }
86
- if (!apiKey) {
87
- process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register a provider.\n");
88
- process.exit(1);
89
- }
90
- // Generate or load keys
91
- let signPublicKey;
92
- let encryptPublicKey;
93
- if (keyDir && existsSync(`${keyDir}/sign_public.pem`)) {
94
- const keys = loadPublicKeys(keyDir);
95
- signPublicKey = keys.signPublicKey;
96
- encryptPublicKey = keys.encryptPublicKey;
97
- }
98
- else if (keyDir) {
99
- process.stderr.write(`Generating keypairs in ${keyDir}...\n`);
100
- const keys = generatePersistentKeys(keyDir);
101
- signPublicKey = keys.signPublicKey;
102
- encryptPublicKey = keys.encryptPublicKey;
103
- }
104
- else {
105
- const keys = generateEphemeralKeys();
106
- signPublicKey = keys.signPublicKey;
107
- encryptPublicKey = keys.encryptPublicKey;
108
- }
109
- // Parse context messages
110
- let messages = [];
111
- if (contextArg) {
112
- try {
113
- messages = JSON.parse(contextArg);
114
- }
115
- catch {
116
- // ignore parse errors
117
- }
118
- }
119
- const client = new HeySummonClient({ baseUrl, apiKey });
120
- if (resolvedProvider) {
121
- process.stderr.write(`Provider: ${resolvedProvider}\n`);
122
- }
123
- const result = await client.submitRequest({
124
- question,
125
- messages: messages.length > 0 ? messages : undefined,
126
- signPublicKey,
127
- encryptPublicKey,
128
- providerName: providerArg || undefined,
129
- });
130
- if (!result.requestId) {
131
- process.stderr.write(`Request failed: ${JSON.stringify(result)}\n`);
132
- process.exit(1);
133
- }
134
- // Track request
135
- if (requestsDir) {
136
- const tracker = new RequestTracker(requestsDir);
137
- tracker.track(result.requestId, result.refCode, resolvedProvider || undefined);
138
- }
139
- // Sync provider name
140
- if (providersFile && existsSync(providersFile) && apiKey) {
141
- try {
142
- const whoami = await client.whoami();
143
- const pName = whoami.provider?.name || "";
144
- if (pName) {
145
- const store = new ProviderStore(providersFile);
146
- const entry = store.findByKey(apiKey);
147
- if (entry && entry.providerName !== pName) {
148
- store.add({ ...entry, providerName: pName });
149
- process.stderr.write(`Provider name updated: ${pName}\n`);
150
- }
151
- }
152
- }
153
- catch {
154
- // non-fatal
155
- }
156
- }
157
- // Output result as JSON
158
- process.stdout.write(JSON.stringify(result) + "\n");
41
+ function hasFlag(args, flag) {
42
+ return args.includes(flag);
159
43
  }
160
44
  async function cmdSubmitAndPoll(args) {
161
45
  const question = getArg(args, "--question");
162
46
  if (!question) {
163
- process.stderr.write("Usage: cli submit-and-poll --question <q> [--provider <name>] [--context <json>]\n");
47
+ process.stderr.write("Usage: cli submit-and-poll --question <q> [--expert <name>] [--context <json>] [--requires-approval]\n");
164
48
  process.exit(1);
165
49
  }
166
- const providerArg = getArg(args, "--provider");
50
+ const expertArg = getArg(args, "--expert");
167
51
  const contextArg = getArg(args, "--context");
52
+ const requiresApproval = hasFlag(args, "--requires-approval");
168
53
  const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
169
54
  const timeout = parseInt(optEnv("HEYSUMMON_TIMEOUT", "900"), 10);
170
55
  const pollInterval = parseInt(optEnv("HEYSUMMON_POLL_INTERVAL", "3"), 10);
171
- const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
56
+ const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
172
57
  // Resolve API key
173
58
  let apiKey = process.env.HEYSUMMON_API_KEY || "";
174
- let resolvedProvider = "";
175
- if (providersFile && existsSync(providersFile)) {
176
- const store = new ProviderStore(providersFile);
177
- if (providerArg) {
178
- const match = store.findByName(providerArg);
59
+ if (expertsFile && existsSync(expertsFile)) {
60
+ const store = new ExpertStore(expertsFile);
61
+ if (expertArg) {
62
+ const match = store.findByName(expertArg);
179
63
  if (match) {
180
64
  apiKey = match.apiKey;
181
- resolvedProvider = match.name;
182
65
  }
183
66
  else {
184
- process.stderr.write(`Provider '${providerArg}' not found.\n`);
67
+ process.stderr.write(`Expert '${expertArg}' not found.\n`);
185
68
  process.exit(1);
186
69
  }
187
70
  }
@@ -189,15 +72,14 @@ async function cmdSubmitAndPoll(args) {
189
72
  const def = store.getDefault();
190
73
  if (def) {
191
74
  apiKey = def.apiKey;
192
- resolvedProvider = def.name;
193
75
  }
194
76
  }
195
77
  }
196
78
  if (!apiKey) {
197
- process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register a provider.\n");
79
+ process.stderr.write("No API key. Set HEYSUMMON_API_KEY or register an expert.\n");
198
80
  process.exit(1);
199
81
  }
200
- // Generate ephemeral keys (Claude Code style no persistence needed)
82
+ // Generate ephemeral keys (Claude Code style -- no persistence needed)
201
83
  const keys = generateEphemeralKeys();
202
84
  const client = new HeySummonClient({ baseUrl, apiKey });
203
85
  process.stderr.write("HeySummon: Submitting request to human...\n");
@@ -211,25 +93,48 @@ async function cmdSubmitAndPoll(args) {
211
93
  // ignore
212
94
  }
213
95
  }
214
- const result = await client.submitRequest({
215
- question,
216
- messages: messages.length > 0 ? messages : undefined,
217
- signPublicKey: keys.signPublicKey,
218
- encryptPublicKey: keys.encryptPublicKey,
219
- providerName: providerArg || undefined,
220
- });
221
- if (result.providerUnavailable) {
222
- const next = result.nextAvailableAt
223
- ? ` (available at ${new Date(result.nextAvailableAt).toLocaleTimeString()})`
224
- : "";
225
- process.stderr.write(`Provider currently unavailable${next} — request queued, waiting for response...\n`);
96
+ let result;
97
+ try {
98
+ result = await client.submitRequest({
99
+ question,
100
+ messages: messages.length > 0 ? messages : undefined,
101
+ signPublicKey: keys.signPublicKey,
102
+ encryptPublicKey: keys.encryptPublicKey,
103
+ expertName: expertArg || undefined,
104
+ requiresApproval: requiresApproval || undefined,
105
+ });
106
+ }
107
+ catch (err) {
108
+ if (err instanceof HeySummonHttpError && err.status === 403) {
109
+ let parsed = {};
110
+ try {
111
+ parsed = JSON.parse(err.body);
112
+ }
113
+ catch { /* ignore */ }
114
+ const ip = parsed.ip || "unknown";
115
+ const hint = parsed.hint || "";
116
+ process.stderr.write(`IP address ${ip} is not authorized for this API key.\n` +
117
+ `${hint ? hint + "\n" : ""}` +
118
+ `Your expert needs to allow this IP address in the HeySummon dashboard under Clients > IP Security.\n` +
119
+ `Once allowed, re-run this command.\n`);
120
+ process.stdout.write(`IP_NOT_ALLOWED: IP address ${ip} is not authorized. Ask your expert to allow it in their HeySummon dashboard.\n`);
121
+ return;
122
+ }
123
+ throw err;
226
124
  }
227
125
  if (!result.requestId) {
228
126
  process.stderr.write(`Failed to submit request: ${JSON.stringify(result)}\n`);
229
127
  process.exit(1);
230
128
  }
231
129
  const ref = result.refCode || result.requestId;
232
- process.stderr.write(`Request submitted [${ref}] waiting for human response...\n`);
130
+ // When expert is unavailable, the platform rejects the request
131
+ if (result.rejected) {
132
+ const msg = result.message || "Expert is not available right now.";
133
+ process.stderr.write(`${msg}\n`);
134
+ process.stdout.write(`EXPERT_UNAVAILABLE: ${msg}${result.nextAvailableAt ? ` Next available: ${result.nextAvailableAt}` : ""}\n`);
135
+ return;
136
+ }
137
+ process.stderr.write(`Request submitted [${ref}] -- waiting for human response...\n`);
233
138
  process.stderr.write(` (timeout: ${timeout}s, polling every ${pollInterval}s)\n`);
234
139
  // Polling loop
235
140
  let elapsed = 0;
@@ -239,6 +144,14 @@ async function cmdSubmitAndPoll(args) {
239
144
  try {
240
145
  // Check status endpoint
241
146
  const status = await client.getRequestStatus(result.requestId);
147
+ // Approval decision (Approve/Deny buttons)
148
+ if ((status.status === "responded" || status.status === "closed") &&
149
+ status.approvalDecision) {
150
+ process.stderr.write(`\nHuman responded [${ref}] -- decision: ${status.approvalDecision}\n`);
151
+ process.stdout.write(`${status.approvalDecision.toUpperCase()}\n`);
152
+ await client.ackEvent(result.requestId).catch(() => { });
153
+ return;
154
+ }
242
155
  if ((status.status === "responded" || status.status === "closed") &&
243
156
  status.response) {
244
157
  process.stderr.write(`\nHuman responded [${ref}]\n`);
@@ -247,15 +160,15 @@ async function cmdSubmitAndPoll(args) {
247
160
  }
248
161
  // Fallback: check messages for plaintext replies
249
162
  const { messages: msgs } = await client.getMessages(result.requestId);
250
- const providerMsg = msgs.filter((m) => m.from === "provider").pop();
251
- if (providerMsg) {
252
- if (providerMsg.plaintext) {
163
+ const expertMsg = msgs.filter((m) => m.from === "expert").pop();
164
+ if (expertMsg) {
165
+ if (expertMsg.plaintext) {
253
166
  process.stderr.write(`\nHuman responded [${ref}]\n`);
254
- process.stdout.write(providerMsg.plaintext + "\n");
167
+ process.stdout.write(expertMsg.plaintext + "\n");
255
168
  await client.ackEvent(result.requestId).catch(() => { });
256
169
  return;
257
170
  }
258
- if (providerMsg.ciphertext) {
171
+ if (expertMsg.ciphertext) {
259
172
  process.stderr.write(`\nHuman responded [${ref}]\n`);
260
173
  process.stdout.write("(encrypted response received)\n");
261
174
  await client.ackEvent(result.requestId).catch(() => { });
@@ -271,20 +184,22 @@ async function cmdSubmitAndPoll(args) {
271
184
  process.stderr.write(` Still waiting... (${elapsed}s elapsed)\n`);
272
185
  }
273
186
  }
274
- process.stderr.write(`\nTimeout after ${timeout}s no response received.\n`);
187
+ // Report timeout to server (notifies expert, records timestamp)
188
+ await client.reportTimeout(result.requestId).catch(() => { });
189
+ process.stderr.write(`\nTimeout after ${timeout}s -- no response received.\n`);
275
190
  process.stderr.write(` Request ref: ${ref}\n`);
276
- process.stdout.write(`TIMEOUT: No response received after ${timeout}s for request ${ref}. The provider may still respond later.\n`);
191
+ process.stdout.write(`TIMEOUT: No answer came back from the expert within the ${timeout}s timeout window. Request ref: ${ref}\n`);
277
192
  }
278
- async function cmdAddProvider(args) {
193
+ async function cmdAddExpert(args) {
279
194
  const key = getArg(args, "--key");
280
195
  const alias = getArg(args, "--alias");
281
196
  if (!key) {
282
- process.stderr.write("Usage: cli add-provider --key <api-key> [--alias <name>]\n");
197
+ process.stderr.write("Usage: cli add-expert --key <api-key> [--alias <name>]\n");
283
198
  process.exit(1);
284
199
  }
285
200
  // Validate key prefix
286
- if (key.startsWith("hs_prov_") || key.startsWith("htl_prov_")) {
287
- process.stderr.write("This is a provider key. Use a CLIENT key (hs_cli_... or htl_...).\n");
201
+ if (key.startsWith("hs_exp_") || key.startsWith("htl_exp_")) {
202
+ process.stderr.write("This is an expert key. Use a CLIENT key (hs_cli_... or htl_...).\n");
288
203
  process.exit(1);
289
204
  }
290
205
  if (!key.startsWith("hs_cli_") && !key.startsWith("htl_")) {
@@ -292,44 +207,44 @@ async function cmdAddProvider(args) {
292
207
  process.exit(1);
293
208
  }
294
209
  const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
295
- const providersFile = requireEnv("HEYSUMMON_PROVIDERS_FILE");
210
+ const expertsFile = requireEnv("HEYSUMMON_EXPERTS_FILE");
296
211
  const client = new HeySummonClient({ baseUrl, apiKey: key });
297
212
  const whoami = await client.whoami();
298
- const providerName = whoami.provider?.name || "";
299
- const providerId = whoami.provider?.id || "";
300
- if (!providerName) {
301
- process.stderr.write("Could not fetch provider info. Is the key valid?\n");
213
+ const expertName = whoami.expert?.name || "";
214
+ const expertId = whoami.expert?.id || "";
215
+ if (!expertName) {
216
+ process.stderr.write("Could not fetch expert info. Is the key valid?\n");
302
217
  process.exit(1);
303
218
  }
304
- const name = alias || providerName;
305
- const store = new ProviderStore(providersFile);
219
+ const name = alias || expertName;
220
+ const store = new ExpertStore(expertsFile);
306
221
  store.add({
307
222
  name,
308
223
  apiKey: key,
309
- providerId,
310
- providerName,
224
+ expertId,
225
+ expertName,
311
226
  });
312
227
  const count = store.load().length;
313
- process.stdout.write(`Provider added: ${name} (${providerName})\n`);
314
- process.stdout.write(`Providers registered: ${count}\n`);
228
+ process.stdout.write(`Expert added: ${name} (${expertName})\n`);
229
+ process.stdout.write(`Experts registered: ${count}\n`);
315
230
  }
316
- async function cmdListProviders() {
317
- const providersFile = optEnv("HEYSUMMON_PROVIDERS_FILE", "");
318
- if (!providersFile || !existsSync(providersFile)) {
319
- process.stdout.write("No providers registered yet.\n");
231
+ async function cmdListExperts() {
232
+ const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
233
+ if (!expertsFile || !existsSync(expertsFile)) {
234
+ process.stdout.write("No experts registered yet.\n");
320
235
  return;
321
236
  }
322
- const store = new ProviderStore(providersFile);
323
- const providers = store.load();
324
- if (providers.length === 0) {
325
- process.stdout.write("No providers registered yet.\n");
237
+ const store = new ExpertStore(expertsFile);
238
+ const experts = store.load();
239
+ if (experts.length === 0) {
240
+ process.stdout.write("No experts registered yet.\n");
326
241
  return;
327
242
  }
328
- process.stdout.write(`Registered providers (${providers.length}):\n`);
329
- for (let i = 0; i < providers.length; i++) {
330
- const p = providers[i];
331
- const nameExtra = p.providerName !== p.name ? ` (provider: ${p.providerName})` : "";
332
- process.stdout.write(` ${i + 1}. ${p.name}${nameExtra} key: ${p.apiKey.slice(0, 12)}...\n`);
243
+ process.stdout.write(`Registered experts (${experts.length}):\n`);
244
+ for (let i = 0; i < experts.length; i++) {
245
+ const p = experts[i];
246
+ const nameExtra = p.expertName !== p.name ? ` (expert: ${p.expertName})` : "";
247
+ process.stdout.write(` ${i + 1}. ${p.name}${nameExtra} -- key: ${p.apiKey.slice(0, 12)}...\n`);
333
248
  }
334
249
  }
335
250
  async function cmdCheckStatus(args) {
@@ -344,14 +259,14 @@ async function cmdCheckStatus(args) {
344
259
  try {
345
260
  const data = await client.getRequestStatus(ref);
346
261
  const icons = {
347
- pending: "",
348
- responded: "",
349
- resolved: "",
350
- expired: "",
351
- cancelled: "",
262
+ pending: "...",
263
+ responded: "[ok]",
264
+ resolved: "[ok]",
265
+ expired: "[expired]",
266
+ cancelled: "[cancelled]",
352
267
  };
353
268
  const status = data.status || "unknown";
354
- process.stdout.write(`${icons[status] || ""} Status: ${status}\n`);
269
+ process.stdout.write(`${icons[status] || "-"} Status: ${status}\n`);
355
270
  if (data.refCode)
356
271
  process.stdout.write(` Ref: ${data.refCode}\n`);
357
272
  if (data.response || data.lastMessage) {
@@ -363,192 +278,6 @@ async function cmdCheckStatus(args) {
363
278
  process.exit(1);
364
279
  }
365
280
  }
366
- async function cmdWatch(args) {
367
- const notifyScript = getArg(args, "--notify-script");
368
- const baseUrl = requireEnv("HEYSUMMON_BASE_URL");
369
- const providersFile = requireEnv("HEYSUMMON_PROVIDERS_FILE");
370
- const requestsDir = optEnv("HEYSUMMON_REQUESTS_DIR", "");
371
- const pollInterval = parseInt(optEnv("HEYSUMMON_POLL_INTERVAL", "5"), 10);
372
- const store = new ProviderStore(providersFile);
373
- const tracker = requestsDir ? new RequestTracker(requestsDir) : null;
374
- // In-memory dedup set
375
- const seen = new Set();
376
- // Track persistent auth errors per provider — auto-shutdown after 30 min
377
- const AUTH_SHUTDOWN_MS = 30 * 60 * 1000; // 30 minutes
378
- const authErrors = new Map();
379
- process.stderr.write(`Platform watcher started (pid ${process.pid})\n`);
380
- process.stderr.write(` Polling every ${pollInterval}s\n`);
381
- // Write PID file for signaling
382
- if (requestsDir) {
383
- const { writeFileSync } = await import("node:fs");
384
- const { join } = await import("node:path");
385
- writeFileSync(join(requestsDir, ".watcher.pid"), String(process.pid));
386
- }
387
- while (true) {
388
- const providers = store.load();
389
- if (providers.length === 0) {
390
- process.stderr.write("No providers registered.\n");
391
- await sleep(pollInterval * 1000);
392
- continue;
393
- }
394
- for (const provider of providers) {
395
- try {
396
- const client = new HeySummonClient({
397
- baseUrl,
398
- apiKey: provider.apiKey,
399
- });
400
- const { events } = await client.getPendingEvents();
401
- for (const event of events) {
402
- const from = event.from || "unknown";
403
- const dedupKey = `${event.type}:${from}:${event.requestId}`;
404
- // Stale check (>30 min)
405
- if (event.createdAt) {
406
- const age = (Date.now() - new Date(event.createdAt).getTime()) / 1000 / 60;
407
- if (age > 30) {
408
- seen.add(dedupKey);
409
- continue;
410
- }
411
- }
412
- if (seen.has(dedupKey))
413
- continue;
414
- seen.add(dedupKey);
415
- // Fetch response text for provider messages
416
- let responseText = "";
417
- if (event.type === "new_message" &&
418
- event.from === "provider" &&
419
- event.requestId) {
420
- try {
421
- const { messages } = await client.getMessages(event.requestId);
422
- const last = messages
423
- .filter((m) => m.from === "provider")
424
- .pop();
425
- if (last?.plaintext) {
426
- responseText = last.plaintext;
427
- }
428
- else if (last?.ciphertext) {
429
- responseText = "(encrypted)";
430
- }
431
- }
432
- catch {
433
- // non-fatal
434
- }
435
- }
436
- // Build notification
437
- const fileRef = tracker?.getRefCode(event.requestId) || "";
438
- const ref = event.refCode || fileRef || event.requestId || "?";
439
- let msg = "";
440
- switch (event.type) {
441
- case "keys_exchanged":
442
- msg = `Key exchange completed for ${ref} — provider connected`;
443
- break;
444
- case "new_message":
445
- if (event.from === "provider") {
446
- msg = `New response from provider for ${ref}`;
447
- if (responseText)
448
- msg += `\n${responseText}`;
449
- }
450
- break;
451
- case "responded":
452
- msg = `Provider responded to ${ref}`;
453
- break;
454
- case "closed":
455
- msg = `Conversation ${ref} closed`;
456
- if (tracker)
457
- tracker.remove(event.requestId);
458
- break;
459
- default:
460
- msg = `HeySummon event (${event.type}) for ${ref}`;
461
- }
462
- if (!msg)
463
- continue;
464
- // Build wake text with original question context
465
- let wakeText = msg;
466
- if (responseText && event.requestId) {
467
- try {
468
- const reqData = await client.getRequestByRef(fileRef || event.requestId);
469
- const origQuestion = reqData.question || "";
470
- const provName = reqData.provider?.name || reqData.providerName || "the provider";
471
- wakeText = `${ref} — ${provName} responded!`;
472
- if (origQuestion)
473
- wakeText += `\n\nYour question was: ${origQuestion}`;
474
- wakeText += `\n\nAnswer: ${responseText}`;
475
- wakeText += `\n\nProceed based on this answer.`;
476
- }
477
- catch {
478
- // non-fatal, use plain msg
479
- }
480
- }
481
- // Deliver notification
482
- if (notifyScript) {
483
- try {
484
- const eventJson = JSON.stringify({
485
- event,
486
- msg,
487
- wakeText,
488
- responseText,
489
- ref,
490
- });
491
- execSync(`bash "${notifyScript}"`, {
492
- input: eventJson,
493
- stdio: ["pipe", "inherit", "inherit"],
494
- timeout: 30_000,
495
- });
496
- }
497
- catch {
498
- process.stderr.write(`Notify script failed for ${ref}\n`);
499
- }
500
- }
501
- else {
502
- process.stdout.write(`${msg}\n`);
503
- }
504
- // ACK the event
505
- await client.ackEvent(event.requestId).catch(() => { });
506
- }
507
- }
508
- catch (err) {
509
- const isAuth = err instanceof HeySummonHttpError && err.isAuthError;
510
- if (isAuth) {
511
- const key = provider.apiKey;
512
- const existing = authErrors.get(key);
513
- if (!existing) {
514
- authErrors.set(key, { firstAt: Date.now(), count: 1 });
515
- process.stderr.write(`Auth error for "${provider.name}" (${err instanceof HeySummonHttpError ? `HTTP ${err.status}` : "unknown"}). Will retry...\n`);
516
- }
517
- else {
518
- existing.count++;
519
- const elapsed = Date.now() - existing.firstAt;
520
- if (elapsed >= AUTH_SHUTDOWN_MS) {
521
- const status = err instanceof HeySummonHttpError ? err.status : "?";
522
- const detail = err instanceof HeySummonHttpError ? err.body : String(err);
523
- process.stderr.write(`\n` +
524
- `══════════════════════════════════════════════════════\n` +
525
- ` HeySummon watcher shutting down\n` +
526
- `\n` +
527
- ` Provider "${provider.name}" has returned HTTP ${status}\n` +
528
- ` for ${Math.round(elapsed / 60000)} minutes (${existing.count} attempts).\n` +
529
- `\n` +
530
- ` This usually means the API key was deleted or\n` +
531
- ` deactivated on the HeySummon platform.\n` +
532
- `\n` +
533
- ` Detail: ${detail}\n` +
534
- `\n` +
535
- ` To fix: ask your provider for a new setup link,\n` +
536
- ` then re-run the setup to get fresh credentials.\n` +
537
- `══════════════════════════════════════════════════════\n`);
538
- process.exit(1);
539
- }
540
- }
541
- }
542
- else {
543
- // Clear auth error streak on non-auth errors (network issues etc.)
544
- authErrors.delete(provider.apiKey);
545
- process.stderr.write(`Poll error for ${provider.name}: ${err instanceof Error ? err.message : String(err)}\n`);
546
- }
547
- }
548
- }
549
- await sleep(pollInterval * 1000);
550
- }
551
- }
552
281
  async function cmdKeygen(args) {
553
282
  const dir = getArg(args, "--dir") || optEnv("HEYSUMMON_KEY_DIR", "");
554
283
  if (!dir) {
@@ -570,12 +299,10 @@ function sleep(ms) {
570
299
  // ---------------------------------------------------------------------------
571
300
  const [command, ...rest] = process.argv.slice(2);
572
301
  const commands = {
573
- submit: cmdSubmit,
574
302
  "submit-and-poll": cmdSubmitAndPoll,
575
- "add-provider": cmdAddProvider,
576
- "list-providers": cmdListProviders,
303
+ "add-expert": cmdAddExpert,
304
+ "list-experts": cmdListExperts,
577
305
  "check-status": cmdCheckStatus,
578
- watch: cmdWatch,
579
306
  keygen: cmdKeygen,
580
307
  };
581
308
  if (!command || !commands[command]) {