@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/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
|
|
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
|
import { HeySummonClient, HeySummonHttpError } from "./client.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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
|
-
|
|
44
|
-
|
|
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> [--
|
|
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
|
|
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
|
|
56
|
+
const expertsFile = optEnv("HEYSUMMON_EXPERTS_FILE", "");
|
|
172
57
|
// Resolve API key
|
|
173
58
|
let apiKey = process.env.HEYSUMMON_API_KEY || "";
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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(`
|
|
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
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
251
|
-
if (
|
|
252
|
-
if (
|
|
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(
|
|
167
|
+
process.stdout.write(expertMsg.plaintext + "\n");
|
|
255
168
|
await client.ackEvent(result.requestId).catch(() => { });
|
|
256
169
|
return;
|
|
257
170
|
}
|
|
258
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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("
|
|
287
|
-
process.stderr.write("This is
|
|
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
|
|
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
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
301
|
-
process.stderr.write("Could not fetch
|
|
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 ||
|
|
305
|
-
const store = new
|
|
219
|
+
const name = alias || expertName;
|
|
220
|
+
const store = new ExpertStore(expertsFile);
|
|
306
221
|
store.add({
|
|
307
222
|
name,
|
|
308
223
|
apiKey: key,
|
|
309
|
-
|
|
310
|
-
|
|
224
|
+
expertId,
|
|
225
|
+
expertName,
|
|
311
226
|
});
|
|
312
227
|
const count = store.load().length;
|
|
313
|
-
process.stdout.write(`
|
|
314
|
-
process.stdout.write(`
|
|
228
|
+
process.stdout.write(`Expert added: ${name} (${expertName})\n`);
|
|
229
|
+
process.stdout.write(`Experts registered: ${count}\n`);
|
|
315
230
|
}
|
|
316
|
-
async function
|
|
317
|
-
const
|
|
318
|
-
if (!
|
|
319
|
-
process.stdout.write("No
|
|
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
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
process.stdout.write("No
|
|
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
|
|
329
|
-
for (let i = 0; i <
|
|
330
|
-
const p =
|
|
331
|
-
const nameExtra = p.
|
|
332
|
-
process.stdout.write(` ${i + 1}. ${p.name}${nameExtra}
|
|
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] || "
|
|
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-
|
|
576
|
-
"list-
|
|
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]) {
|