@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/LICENSE.md +87 -0
- package/README.md +225 -0
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +108 -381
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +20 -5
- package/dist/client.js +172 -8
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +40 -0
- package/dist/crypto.js +76 -0
- package/dist/crypto.js.map +1 -1
- package/dist/expert-store.d.ts +25 -0
- package/dist/{provider-store.js → expert-store.js} +15 -15
- package/dist/expert-store.js.map +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +44 -16
- package/package.json +7 -7
- package/src/cli.ts +117 -417
- package/src/client.ts +224 -9
- package/src/crypto.ts +145 -0
- package/src/{provider-store.ts → expert-store.ts} +24 -24
- package/src/index.ts +9 -6
- package/src/types.ts +48 -17
- package/dist/poller.d.ts +0 -28
- package/dist/poller.js +0 -57
- package/dist/poller.js.map +0 -1
- package/dist/provider-store.d.ts +0 -25
- package/dist/provider-store.js.map +0 -1
- package/dist/request-tracker.d.ts +0 -22
- package/dist/request-tracker.js +0 -66
- package/dist/request-tracker.js.map +0 -1
- package/src/poller.ts +0 -72
- package/src/request-tracker.ts +0 -72
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
|
|
7
|
-
* check-status,
|
|
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,
|
|
11
|
-
*
|
|
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 {
|
|
17
|
-
import {
|
|
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
|
-
|
|
55
|
-
|
|
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> [--
|
|
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
|
|
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
|
|
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 (
|
|
200
|
-
const store = new
|
|
201
|
-
if (
|
|
202
|
-
const match = store.findByName(
|
|
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(`
|
|
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
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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}]
|
|
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
|
|
291
|
-
if (
|
|
292
|
-
if (
|
|
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(
|
|
198
|
+
process.stdout.write(expertMsg.plaintext + "\n");
|
|
295
199
|
await client.ackEvent(result.requestId).catch(() => {});
|
|
296
200
|
return;
|
|
297
201
|
}
|
|
298
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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("
|
|
333
|
-
process.stderr.write("This is
|
|
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
|
|
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
|
|
348
|
-
const
|
|
254
|
+
const expertName = whoami.expert?.name || "";
|
|
255
|
+
const expertId = whoami.expert?.id || "";
|
|
349
256
|
|
|
350
|
-
if (!
|
|
351
|
-
process.stderr.write("Could not fetch
|
|
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 ||
|
|
356
|
-
const store = new
|
|
262
|
+
const name = alias || expertName;
|
|
263
|
+
const store = new ExpertStore(expertsFile);
|
|
357
264
|
store.add({
|
|
358
265
|
name,
|
|
359
266
|
apiKey: key,
|
|
360
|
-
|
|
361
|
-
|
|
267
|
+
expertId,
|
|
268
|
+
expertName,
|
|
362
269
|
});
|
|
363
270
|
|
|
364
271
|
const count = store.load().length;
|
|
365
|
-
process.stdout.write(`
|
|
366
|
-
process.stdout.write(`
|
|
272
|
+
process.stdout.write(`Expert added: ${name} (${expertName})\n`);
|
|
273
|
+
process.stdout.write(`Experts registered: ${count}\n`);
|
|
367
274
|
}
|
|
368
275
|
|
|
369
|
-
async function
|
|
370
|
-
const
|
|
276
|
+
async function cmdListExperts(): Promise<void> {
|
|
277
|
+
const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
|
|
371
278
|
|
|
372
|
-
if (!
|
|
373
|
-
process.stdout.write("No
|
|
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
|
|
378
|
-
const
|
|
284
|
+
const store = new ExpertStore(expertsFile);
|
|
285
|
+
const experts = store.load();
|
|
379
286
|
|
|
380
|
-
if (
|
|
381
|
-
process.stdout.write("No
|
|
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
|
|
386
|
-
for (let i = 0; i <
|
|
387
|
-
const p =
|
|
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.
|
|
296
|
+
p.expertName !== p.name ? ` (expert: ${p.expertName})` : "";
|
|
390
297
|
process.stdout.write(
|
|
391
|
-
` ${i + 1}. ${p.name}${nameExtra}
|
|
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] || "
|
|
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-
|
|
665
|
-
"list-
|
|
365
|
+
"add-expert": cmdAddExpert,
|
|
366
|
+
"list-experts": cmdListExperts,
|
|
666
367
|
"check-status": cmdCheckStatus,
|
|
667
|
-
watch: cmdWatch,
|
|
668
368
|
keygen: cmdKeygen,
|
|
669
369
|
};
|
|
670
370
|
|