@caphub/cli 0.1.0 → 0.1.2
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/bin/caphub.js +212 -16
- package/package.json +1 -1
package/bin/caphub.js
CHANGED
|
@@ -13,6 +13,8 @@ const CONFIG_PATH = resolve(CONFIG_DIR, "config.json");
|
|
|
13
13
|
|
|
14
14
|
const ROOT_HELP = `caphub
|
|
15
15
|
|
|
16
|
+
Caphub is hosted infrastructure for agent-ready capabilities such as search and query expansion.
|
|
17
|
+
|
|
16
18
|
purpose: root CLI for Caphub agent capabilities
|
|
17
19
|
auth: CAPHUB_API_KEY env or ${CONFIG_PATH}
|
|
18
20
|
api: ${DEFAULT_API_URL}
|
|
@@ -21,18 +23,43 @@ commands:
|
|
|
21
23
|
help explain the platform and command layout
|
|
22
24
|
help <capability> show capability-specific help from the API
|
|
23
25
|
capabilities list live capabilities with short descriptions
|
|
24
|
-
auth login
|
|
26
|
+
auth show current login state
|
|
27
|
+
auth login website login flow; stores api key locally after approval
|
|
25
28
|
auth whoami verify the current api key against the API
|
|
26
29
|
auth logout remove stored api key from local config
|
|
27
30
|
<capability> <json> run a capability directly, e.g. search or search-ideas
|
|
28
31
|
|
|
32
|
+
agent workflow:
|
|
33
|
+
1. caphub capabilities
|
|
34
|
+
2. caphub help <capability>
|
|
35
|
+
3. caphub auth login
|
|
36
|
+
4. caphub <capability> '<json>'
|
|
37
|
+
|
|
38
|
+
recovery:
|
|
39
|
+
no api key caphub auth login
|
|
40
|
+
invalid api key generate a new key in https://caphub.io/dashboard/
|
|
41
|
+
insufficient credits top up in https://caphub.io/dashboard/
|
|
42
|
+
bad json caphub help <capability>
|
|
43
|
+
capability unknown caphub capabilities
|
|
44
|
+
|
|
29
45
|
examples:
|
|
46
|
+
caphub auth
|
|
30
47
|
caphub capabilities
|
|
48
|
+
caphub auth login
|
|
31
49
|
caphub auth login --api-key csk_live_...
|
|
32
50
|
caphub help search
|
|
33
51
|
caphub search '{"queries":["site:github.com awesome ai agents"]}'
|
|
34
52
|
`;
|
|
35
53
|
|
|
54
|
+
class ApiError extends Error {
|
|
55
|
+
constructor(message, status = 0, data = null) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "ApiError";
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.data = data;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
function readConfig() {
|
|
37
64
|
try {
|
|
38
65
|
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
@@ -82,22 +109,27 @@ async function fetchJson(url, { method = "GET", body, apiKey = "" } = {}) {
|
|
|
82
109
|
const headers = { "Content-Type": "application/json" };
|
|
83
110
|
if (apiKey) headers["X-API-Key"] = apiKey;
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
let resp;
|
|
113
|
+
try {
|
|
114
|
+
resp = await fetch(url, {
|
|
115
|
+
method,
|
|
116
|
+
headers,
|
|
117
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new ApiError(`request failed: ${error.message}`, 0, null);
|
|
121
|
+
}
|
|
90
122
|
|
|
91
123
|
const text = await resp.text();
|
|
92
124
|
let data;
|
|
93
125
|
try {
|
|
94
126
|
data = text ? JSON.parse(text) : {};
|
|
95
127
|
} catch {
|
|
96
|
-
throw new
|
|
128
|
+
throw new ApiError(`non-JSON response from ${url} (HTTP ${resp.status})`, resp.status, null);
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
if (!resp.ok) {
|
|
100
|
-
throw new
|
|
132
|
+
throw new ApiError(data?.error || `HTTP ${resp.status}`, resp.status, data);
|
|
101
133
|
}
|
|
102
134
|
|
|
103
135
|
return data;
|
|
@@ -109,8 +141,71 @@ function parseFlag(args, name) {
|
|
|
109
141
|
return args[idx + 1] || "";
|
|
110
142
|
}
|
|
111
143
|
|
|
144
|
+
function buildRecoveryHints(error, context = {}) {
|
|
145
|
+
const hints = [];
|
|
146
|
+
const capability = context.capability || "<capability>";
|
|
147
|
+
const message = String(error?.message || "").toLowerCase();
|
|
148
|
+
const status = Number(error?.status || 0);
|
|
149
|
+
|
|
150
|
+
if (status === 401 || message.includes("missing x-api-key header")) {
|
|
151
|
+
hints.push("login: caphub auth login");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (status === 403 || message.includes("invalid api key")) {
|
|
155
|
+
hints.push("generate a new key in https://caphub.io/dashboard/");
|
|
156
|
+
hints.push("login again: caphub auth login --api-key csk_live_...");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (status === 402 || message.includes("insufficient credits")) {
|
|
160
|
+
hints.push("top up credits in https://caphub.io/dashboard/");
|
|
161
|
+
hints.push("check account state: caphub auth whoami");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
status === 400 ||
|
|
166
|
+
message.includes("queries array is required") ||
|
|
167
|
+
message.includes("unsupported function") ||
|
|
168
|
+
message.includes("input must be valid json") ||
|
|
169
|
+
message.includes("input json is required")
|
|
170
|
+
) {
|
|
171
|
+
hints.push(`capability contract: caphub help ${capability}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (message.includes("request failed:")) {
|
|
175
|
+
hints.push(`api url: ${getApiUrl()}`);
|
|
176
|
+
hints.push(`health: curl -sS ${getApiUrl()}/health`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!hints.length) {
|
|
180
|
+
hints.push("discover capabilities: caphub capabilities");
|
|
181
|
+
hints.push(`capability contract: caphub help ${capability}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return hints;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function failWithHints(message, error, context = {}) {
|
|
188
|
+
const lines = [`Error: ${message}`];
|
|
189
|
+
const hints = buildRecoveryHints(error, context);
|
|
190
|
+
if (hints.length) {
|
|
191
|
+
lines.push("", "next:");
|
|
192
|
+
for (const hint of hints) lines.push(` - ${hint}`);
|
|
193
|
+
}
|
|
194
|
+
fail(lines.join("\n"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sleep(ms) {
|
|
198
|
+
return new Promise((resolveWait) => setTimeout(resolveWait, ms));
|
|
199
|
+
}
|
|
200
|
+
|
|
112
201
|
function printCapabilities(payload) {
|
|
113
|
-
const lines = [
|
|
202
|
+
const lines = [
|
|
203
|
+
"caphub capabilities",
|
|
204
|
+
"",
|
|
205
|
+
"Live capabilities available through the current API.",
|
|
206
|
+
"Use 'caphub help <capability>' before first use.",
|
|
207
|
+
"",
|
|
208
|
+
];
|
|
114
209
|
for (const capability of payload.capabilities || []) {
|
|
115
210
|
lines.push(`/${capability.capability} - ${capability.purpose}`);
|
|
116
211
|
if (capability.credits) lines.push(` credits: ${capability.credits}`);
|
|
@@ -137,6 +232,15 @@ function printCapabilityHelp(payload) {
|
|
|
137
232
|
"",
|
|
138
233
|
"output:",
|
|
139
234
|
JSON.stringify(payload.output_contract, null, 2),
|
|
235
|
+
payload.notes_for_agents?.length ? "" : null,
|
|
236
|
+
payload.notes_for_agents?.length ? "notes:" : null,
|
|
237
|
+
...(payload.notes_for_agents || []).map((note) => `- ${note}`),
|
|
238
|
+
"",
|
|
239
|
+
"common recovery:",
|
|
240
|
+
"- auth missing: caphub auth login",
|
|
241
|
+
"- auth invalid: generate a new key in https://caphub.io/dashboard/",
|
|
242
|
+
"- low credits: top up in https://caphub.io/dashboard/",
|
|
243
|
+
`- bad payload: caphub help ${payload.capability}`,
|
|
140
244
|
].filter(Boolean);
|
|
141
245
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
142
246
|
}
|
|
@@ -167,10 +271,92 @@ async function commandAuth(args) {
|
|
|
167
271
|
const sub = args[0];
|
|
168
272
|
const config = readConfig();
|
|
169
273
|
|
|
274
|
+
if (!sub) {
|
|
275
|
+
const apiKey = getApiKey();
|
|
276
|
+
if (!apiKey) {
|
|
277
|
+
process.stdout.write([
|
|
278
|
+
"caphub auth",
|
|
279
|
+
"",
|
|
280
|
+
"status: not logged in",
|
|
281
|
+
"",
|
|
282
|
+
"next:",
|
|
283
|
+
" - caphub auth login",
|
|
284
|
+
" - or for headless/cloud agents: caphub auth login --api-key csk_live_...",
|
|
285
|
+
"",
|
|
286
|
+
].join("\n"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const payload = await fetchJson(`${getApiUrl()}/v1/me`, { apiKey });
|
|
292
|
+
process.stdout.write([
|
|
293
|
+
"caphub auth",
|
|
294
|
+
"",
|
|
295
|
+
"status: logged in",
|
|
296
|
+
`email: ${payload.user.email}`,
|
|
297
|
+
`user_id: ${payload.user.id}`,
|
|
298
|
+
`credits_remaining: ${payload.total_usage.credits_remaining}`,
|
|
299
|
+
`total_credits_used: ${payload.total_usage.total_credits_used}`,
|
|
300
|
+
"",
|
|
301
|
+
"next:",
|
|
302
|
+
" - caphub capabilities",
|
|
303
|
+
" - caphub help search",
|
|
304
|
+
"",
|
|
305
|
+
].join("\n"));
|
|
306
|
+
return;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
failWithHints("stored credentials are not valid", error, { capability: "search" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
170
312
|
if (sub === "login") {
|
|
171
|
-
const
|
|
313
|
+
const explicitApiKey = parseFlag(args, "--api-key");
|
|
314
|
+
if (!explicitApiKey && !args.includes("--api-key")) {
|
|
315
|
+
const apiUrl = getApiUrl();
|
|
316
|
+
const started = await fetchJson(`${apiUrl}/v1/auth/cli/start`, { method: "POST", body: {} });
|
|
317
|
+
process.stdout.write([
|
|
318
|
+
"caphub auth login",
|
|
319
|
+
"",
|
|
320
|
+
"Open this URL in your browser and finish sign-in:",
|
|
321
|
+
started.approval_url,
|
|
322
|
+
"",
|
|
323
|
+
`code: ${started.code}`,
|
|
324
|
+
`expires_in_seconds: ${started.expires_in_seconds}`,
|
|
325
|
+
"",
|
|
326
|
+
"Waiting for website approval...",
|
|
327
|
+
"",
|
|
328
|
+
].join("\n"));
|
|
329
|
+
|
|
330
|
+
const deadline = Date.now() + Number(started.expires_in_seconds || 600) * 1000;
|
|
331
|
+
const intervalMs = Number(started.poll_interval_seconds || 2) * 1000;
|
|
332
|
+
while (Date.now() < deadline) {
|
|
333
|
+
const polled = await fetchJson(`${apiUrl}/v1/auth/cli/poll`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
body: {
|
|
336
|
+
session_id: started.session_id,
|
|
337
|
+
poll_token: started.poll_token,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (polled.status === "approved" && polled.api_key) {
|
|
342
|
+
writeConfig({
|
|
343
|
+
...config,
|
|
344
|
+
api_key: polled.api_key,
|
|
345
|
+
api_url: apiUrl,
|
|
346
|
+
});
|
|
347
|
+
process.stdout.write(`${JSON.stringify({ ok: true, config_path: CONFIG_PATH, api_url: apiUrl }, null, 2)}\n`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await sleep(intervalMs);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fail("Error: login approval timed out.\n\nnext:\n - rerun: caphub auth login\n - or open https://caphub.io/dashboard/");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const apiKey = explicitApiKey || getApiKey();
|
|
172
358
|
const apiUrl = parseFlag(args, "--api-url") || getApiUrl();
|
|
173
|
-
if (!apiKey) fail("Error: auth login requires --api-key or CAPHUB_API_KEY
|
|
359
|
+
if (!apiKey) fail("Error: auth login requires --api-key or CAPHUB_API_KEY.\n\nnext:\n - caphub auth login --api-key csk_live_...");
|
|
174
360
|
writeConfig({
|
|
175
361
|
...config,
|
|
176
362
|
api_key: apiKey,
|
|
@@ -182,7 +368,7 @@ async function commandAuth(args) {
|
|
|
182
368
|
|
|
183
369
|
if (sub === "whoami") {
|
|
184
370
|
const apiKey = getApiKey();
|
|
185
|
-
if (!apiKey) fail(`Error: no api key configured
|
|
371
|
+
if (!apiKey) fail(`Error: no api key configured.\n\nnext:\n - caphub auth login --api-key csk_live_...\n - or set api_key in ${CONFIG_PATH}`);
|
|
186
372
|
const payload = await fetchJson(`${getApiUrl()}/v1/me`, { apiKey });
|
|
187
373
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
188
374
|
return;
|
|
@@ -201,7 +387,11 @@ async function commandAuth(args) {
|
|
|
201
387
|
|
|
202
388
|
async function commandCapability(capability, args) {
|
|
203
389
|
const apiKey = getApiKey();
|
|
204
|
-
if (!apiKey)
|
|
390
|
+
if (!apiKey) {
|
|
391
|
+
fail(
|
|
392
|
+
`Error: no api key configured for capability "${capability}".\n\nnext:\n - caphub auth login --api-key csk_live_...\n - then retry: caphub ${capability} '<json>'\n - contract: caphub help ${capability}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
205
395
|
|
|
206
396
|
if (args[0] === "--help" || args[0] === "help") {
|
|
207
397
|
await commandHelp([capability]);
|
|
@@ -211,14 +401,14 @@ async function commandCapability(capability, args) {
|
|
|
211
401
|
const arg = args[0];
|
|
212
402
|
const rawInput = arg ?? (process.stdin.isTTY ? "" : await readStdin());
|
|
213
403
|
if (!rawInput.trim()) {
|
|
214
|
-
fail(`Error: input JSON is required
|
|
404
|
+
fail(`Error: input JSON is required.\n\nnext:\n - caphub help ${capability}\n - then run: caphub ${capability} '<json>'`);
|
|
215
405
|
}
|
|
216
406
|
|
|
217
407
|
let body;
|
|
218
408
|
try {
|
|
219
409
|
body = JSON.parse(rawInput);
|
|
220
410
|
} catch {
|
|
221
|
-
fail(
|
|
411
|
+
fail(`Error: input must be valid JSON.\n\nnext:\n - caphub help ${capability}\n - pass exactly one JSON object`);
|
|
222
412
|
}
|
|
223
413
|
|
|
224
414
|
if (!("function" in body)) body.function = capability;
|
|
@@ -258,4 +448,10 @@ async function main() {
|
|
|
258
448
|
await commandCapability(cmd, args.slice(1));
|
|
259
449
|
}
|
|
260
450
|
|
|
261
|
-
await main().catch((error) =>
|
|
451
|
+
await main().catch((error) => {
|
|
452
|
+
const cmd = process.argv[2];
|
|
453
|
+
const capability = cmd && !["help", "--help", "-h", "capabilities", "auth", "--version", "-v", "version"].includes(cmd)
|
|
454
|
+
? cmd
|
|
455
|
+
: undefined;
|
|
456
|
+
failWithHints(error.message || "unknown error", error, { capability });
|
|
457
|
+
});
|