apiblaze 0.21.65 → 0.21.67
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/index.js +813 -417
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__
|
|
|
34
34
|
var version;
|
|
35
35
|
var init_package = __esm({
|
|
36
36
|
"package.json"() {
|
|
37
|
-
version = "0.21.
|
|
37
|
+
version = "0.21.67";
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -5166,6 +5166,12 @@ var init_key = __esm({
|
|
|
5166
5166
|
});
|
|
5167
5167
|
|
|
5168
5168
|
// src/lib/directory-probes.ts
|
|
5169
|
+
var directory_probes_exports = {};
|
|
5170
|
+
__export(directory_probes_exports, {
|
|
5171
|
+
PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS,
|
|
5172
|
+
asMetadataUrl: () => asMetadataUrl,
|
|
5173
|
+
probeDirectoryTarget: () => probeDirectoryTarget
|
|
5174
|
+
});
|
|
5169
5175
|
async function probe(url, init, deps) {
|
|
5170
5176
|
const f2 = deps.fetch ?? fetch;
|
|
5171
5177
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -5311,6 +5317,7 @@ __export(directory_exports, {
|
|
|
5311
5317
|
ATTESTATIONS: () => ATTESTATIONS,
|
|
5312
5318
|
AUTH_MODES: () => AUTH_MODES,
|
|
5313
5319
|
DATA_HANDLING_MODES: () => DATA_HANDLING_MODES,
|
|
5320
|
+
DESCRIPTION_MIN: () => DESCRIPTION_MIN,
|
|
5314
5321
|
PLATFORMS: () => PLATFORMS,
|
|
5315
5322
|
attestationId: () => attestationId,
|
|
5316
5323
|
blockerLines: () => blockerLines,
|
|
@@ -5445,7 +5452,7 @@ function printReport(r, json) {
|
|
|
5445
5452
|
process.stdout.write(JSON.stringify(r) + "\n");
|
|
5446
5453
|
return;
|
|
5447
5454
|
}
|
|
5448
|
-
console.log(r.rendered.text);
|
|
5455
|
+
console.log(r.rendered.text.replace(/<project>/g, r.report.project.id));
|
|
5449
5456
|
if (r.probe_note) console.log(import_chalk17.default.dim(r.probe_note));
|
|
5450
5457
|
}
|
|
5451
5458
|
function blockerLines(r, platforms) {
|
|
@@ -5456,7 +5463,7 @@ function blockerLines(r, platforms) {
|
|
|
5456
5463
|
function isApiError(e, status2) {
|
|
5457
5464
|
return e instanceof ApiError && (status2 === void 0 || e.status === status2);
|
|
5458
5465
|
}
|
|
5459
|
-
var import_chalk17, PLATFORMS, ATTESTATIONS, AUTH_MODES, DATA_HANDLING_MODES, reportPath;
|
|
5466
|
+
var import_chalk17, PLATFORMS, ATTESTATIONS, AUTH_MODES, DATA_HANDLING_MODES, reportPath, DESCRIPTION_MIN;
|
|
5460
5467
|
var init_directory = __esm({
|
|
5461
5468
|
"src/lib/directory.ts"() {
|
|
5462
5469
|
"use strict";
|
|
@@ -5482,6 +5489,7 @@ var init_directory = __esm({
|
|
|
5482
5489
|
AUTH_MODES = ["signin", "open"];
|
|
5483
5490
|
DATA_HANDLING_MODES = ["own", "proxied", "third-party"];
|
|
5484
5491
|
reportPath = (proj2) => `/projects/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}/directory-report`;
|
|
5492
|
+
DESCRIPTION_MIN = 300;
|
|
5485
5493
|
}
|
|
5486
5494
|
});
|
|
5487
5495
|
|
|
@@ -7186,225 +7194,6 @@ var init_agent = __esm({
|
|
|
7186
7194
|
}
|
|
7187
7195
|
});
|
|
7188
7196
|
|
|
7189
|
-
// src/lib/directory-docs.ts
|
|
7190
|
-
function directoryDocsPrompt(facts) {
|
|
7191
|
-
return [
|
|
7192
|
-
`Write the three documents a connector listing needs for "${facts.name}", from the FACTS below and nothing else.`,
|
|
7193
|
-
"Rules: plain English, second person, no marketing, no prices, no promo. Never invent a result: when you describe what a tool returns, cite only the recorded result shapes in the facts (keys, item counts, heads); if none is recorded, say what the tool is for and that the assistant will show the result.",
|
|
7194
|
-
"Never write credentials, usernames, tokens or internal hostnames. Troubleshooting cites the failure modes with their exact texts.",
|
|
7195
|
-
`The sign-in: ${facts.sign_in_shape}`,
|
|
7196
|
-
`Answer with JSON matching the schema: docs_markdown (the public page "Using <name> in Claude": purpose \xB7 how it works from Claude's side \xB7 connect and sign in \xB7 the example prompts with what to expect \xB7 troubleshooting \xB7 support), reviewer_markdown (for someone who has never seen the product: add the connector, the sign-in screen, the account, what data it holds, one prompt per tool with the recorded result shape), portal_answers (use_cases 200+ chars, prerequisites one sentence, access, 1\u20135 categories).`,
|
|
7197
|
-
"",
|
|
7198
|
-
"FACTS:",
|
|
7199
|
-
JSON.stringify(facts, null, 2)
|
|
7200
|
-
].join("\n");
|
|
7201
|
-
}
|
|
7202
|
-
function lintDocument(markdown, opts = {}) {
|
|
7203
|
-
const out = [];
|
|
7204
|
-
const text = markdown ?? "";
|
|
7205
|
-
if (!text.trim()) out.push({ code: "EMPTY", detail: "the document is empty" });
|
|
7206
|
-
if (text.length > DIRECTORY_DOC_MAX_CHARS) out.push({ code: "TOO_LONG", detail: `${text.length} characters; the limit is ${DIRECTORY_DOC_MAX_CHARS}` });
|
|
7207
|
-
const u = (opts.reviewerUsername ?? "").trim();
|
|
7208
|
-
if (u.length >= 3 && text.toLowerCase().includes(u.toLowerCase())) out.push({ code: "REVIEWER_USERNAME", detail: "the reviewer username appears; it belongs in the packet only" });
|
|
7209
|
-
if (BEARER_LIKE.test(text)) out.push({ code: "BEARER_LIKE", detail: "a bearer-like or JWT-shaped string appears" });
|
|
7210
|
-
if (KEY_LIKE.test(text)) out.push({ code: "KEY_LIKE", detail: "a key-like string (sk_/pk_/\u2026) appears" });
|
|
7211
|
-
if (PASSWORD_LINE.test(text)) out.push({ code: "PASSWORD_LINE", detail: "a password-like line appears" });
|
|
7212
|
-
if (opts.public && INTERNAL_HOST.test(text)) out.push({ code: "INTERNAL_HOST", detail: "an upstream or platform-internal host appears" });
|
|
7213
|
-
return out;
|
|
7214
|
-
}
|
|
7215
|
-
function parseGeneratedDocs(stdout) {
|
|
7216
|
-
const tryObj = (v) => {
|
|
7217
|
-
if (!v || typeof v !== "object") return null;
|
|
7218
|
-
const o = v;
|
|
7219
|
-
const pa = o.portal_answers;
|
|
7220
|
-
if (typeof o.docs_markdown !== "string" || typeof o.reviewer_markdown !== "string" || !pa || typeof pa !== "object") return null;
|
|
7221
|
-
return { docs_markdown: o.docs_markdown, reviewer_markdown: o.reviewer_markdown, portal_answers: { use_cases: String(pa.use_cases ?? ""), prerequisites: String(pa.prerequisites ?? ""), access: ["read", "write", "both"].includes(String(pa.access)) ? String(pa.access) : "both", categories: Array.isArray(pa.categories) ? pa.categories.map(String).slice(0, 5) : [] } };
|
|
7222
|
-
};
|
|
7223
|
-
let parsed = null;
|
|
7224
|
-
try {
|
|
7225
|
-
parsed = JSON.parse(stdout.trim());
|
|
7226
|
-
} catch {
|
|
7227
|
-
parsed = null;
|
|
7228
|
-
}
|
|
7229
|
-
if (parsed && typeof parsed === "object") {
|
|
7230
|
-
const o = parsed;
|
|
7231
|
-
const direct = tryObj(o);
|
|
7232
|
-
if (direct) return direct;
|
|
7233
|
-
const so = tryObj(o.structured_output);
|
|
7234
|
-
if (so) return so;
|
|
7235
|
-
if (typeof o.result === "string") {
|
|
7236
|
-
try {
|
|
7237
|
-
const r = tryObj(JSON.parse(o.result));
|
|
7238
|
-
if (r) return r;
|
|
7239
|
-
} catch {
|
|
7240
|
-
}
|
|
7241
|
-
}
|
|
7242
|
-
}
|
|
7243
|
-
for (const line of stdout.split("\n").reverse()) {
|
|
7244
|
-
const s = line.trim();
|
|
7245
|
-
if (!s.startsWith("{")) continue;
|
|
7246
|
-
try {
|
|
7247
|
-
const ev = JSON.parse(s);
|
|
7248
|
-
const so = tryObj(ev.structured_output);
|
|
7249
|
-
if (so) return so;
|
|
7250
|
-
if (typeof ev.result === "string") {
|
|
7251
|
-
const r = tryObj(JSON.parse(ev.result));
|
|
7252
|
-
if (r) return r;
|
|
7253
|
-
}
|
|
7254
|
-
} catch {
|
|
7255
|
-
}
|
|
7256
|
-
}
|
|
7257
|
-
return null;
|
|
7258
|
-
}
|
|
7259
|
-
var DIRECTORY_DOCS_JSON_SCHEMA, DIRECTORY_DOC_MAX_CHARS, BEARER_LIKE, KEY_LIKE, PASSWORD_LINE, INTERNAL_HOST;
|
|
7260
|
-
var init_directory_docs = __esm({
|
|
7261
|
-
"src/lib/directory-docs.ts"() {
|
|
7262
|
-
"use strict";
|
|
7263
|
-
DIRECTORY_DOCS_JSON_SCHEMA = {
|
|
7264
|
-
type: "object",
|
|
7265
|
-
additionalProperties: false,
|
|
7266
|
-
required: ["docs_markdown", "reviewer_markdown", "portal_answers"],
|
|
7267
|
-
properties: {
|
|
7268
|
-
docs_markdown: { type: "string", description: `The public documentation page, Markdown: "Using <name> in Claude" \u2014 purpose, how it works from Claude's side, connect and sign in, the three prompts with what to expect, troubleshooting (the known failure modes with their exact texts), support.` },
|
|
7269
|
-
reviewer_markdown: { type: "string", description: "Reviewer instructions for someone who has never seen the product: add the connector, the sign-in screen, the account, what data it holds, one prompt per tool with the recorded result shape. No credentials." },
|
|
7270
|
-
portal_answers: {
|
|
7271
|
-
type: "object",
|
|
7272
|
-
additionalProperties: false,
|
|
7273
|
-
required: ["use_cases", "prerequisites", "access", "categories"],
|
|
7274
|
-
properties: {
|
|
7275
|
-
use_cases: { type: "string", description: "What a person asks it, in their words; 200+ characters." },
|
|
7276
|
-
prerequisites: { type: "string", description: "What a user needs before it works, one sentence." },
|
|
7277
|
-
access: { type: "string", enum: ["read", "write", "both"] },
|
|
7278
|
-
categories: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 5, description: "Free-text category suggestions; the producer picks the closest in the portal." }
|
|
7279
|
-
}
|
|
7280
|
-
}
|
|
7281
|
-
}
|
|
7282
|
-
};
|
|
7283
|
-
DIRECTORY_DOC_MAX_CHARS = 64e3;
|
|
7284
|
-
BEARER_LIKE = /(?:bearer\s+[A-Za-z0-9._~+/=-]{8,}|\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b)/i;
|
|
7285
|
-
KEY_LIKE = /\b(?:sk|pk|rk|key|tok|abz)_[A-Za-z0-9_-]{8,}\b/i;
|
|
7286
|
-
PASSWORD_LINE = /^\s*(?:[-*]\s*)?(?:\*\*)?(?:password|passwd|pwd|secret)(?:\*\*)?\s*[:=]\s*\S+/im;
|
|
7287
|
-
INTERNAL_HOST = /\b[a-z0-9.-]+\.(abz\.run|tryabz\.run|workers\.dev)\b/i;
|
|
7288
|
-
}
|
|
7289
|
-
});
|
|
7290
|
-
|
|
7291
|
-
// src/commands/connector-docs.ts
|
|
7292
|
-
var connector_docs_exports = {};
|
|
7293
|
-
__export(connector_docs_exports, {
|
|
7294
|
-
askDocsQuestion: () => askDocsQuestion,
|
|
7295
|
-
generateWithClaude: () => generateWithClaude,
|
|
7296
|
-
runConnectorDocs: () => runConnectorDocs,
|
|
7297
|
-
storeGeneratedDocs: () => storeGeneratedDocs,
|
|
7298
|
-
verifyDocsUrl: () => verifyDocsUrl
|
|
7299
|
-
});
|
|
7300
|
-
function generateWithClaude(prompt, deps = {}) {
|
|
7301
|
-
const spawn5 = deps.spawn ?? import_child_process5.spawnSync;
|
|
7302
|
-
const res = spawn5("claude", ["-p", prompt, "--output-format", "json", "--json-schema", JSON.stringify(DIRECTORY_DOCS_JSON_SCHEMA), "--tools", "", "--max-budget-usd", "2", "--permission-mode", "dontAsk"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 24e4, env: { ...process.env, CLAUDECODE: "" } });
|
|
7303
|
-
const docs = parseGeneratedDocs(String(res.stdout ?? ""));
|
|
7304
|
-
return { docs, detail: docs ? "generated on Claude Code" : `Claude Code answered without the three documents${res.status != null && res.status !== 0 ? ` (exit ${res.status})` : ""}` };
|
|
7305
|
-
}
|
|
7306
|
-
async function storeGeneratedDocs(caller, proj2, docs, o) {
|
|
7307
|
-
const bad = [...lintDocument(docs.docs_markdown, { reviewerUsername: o.reviewerUsername, public: true }).map((f2) => `docs: ${f2.code}`), ...lintDocument(docs.reviewer_markdown, { reviewerUsername: o.reviewerUsername }).map((f2) => `reviewer: ${f2.code}`)];
|
|
7308
|
-
if (bad.length) throw new Error(`The generated documents were refused by the lint (${bad.join(", ")}); nothing was stored. Run again, or write them by hand on the dashboard page.`);
|
|
7309
|
-
const out = await producer(caller, { method: "PUT", path: `${projectPath(proj2)}/directory-documents`, body: { docs_markdown: docs.docs_markdown, reviewer_markdown: docs.reviewer_markdown, portal_answers: docs.portal_answers, source: "generated", facts_hash: o.factsHash, use_generated_docs_url: o.useGeneratedDocsUrl }, summary: "Store the generated documents" });
|
|
7310
|
-
return out.changed ?? [];
|
|
7311
|
-
}
|
|
7312
|
-
async function verifyDocsUrl(url, serverName, f2 = fetch) {
|
|
7313
|
-
if (!/^https:\/\/\S+$/i.test(url)) return { ok: false, reason: "not an https URL" };
|
|
7314
|
-
try {
|
|
7315
|
-
const r = await f2(url, { redirect: "follow", signal: AbortSignal.timeout(8e3), headers: { accept: "text/html, */*;q=0.5", "user-agent": "apiblaze-cli directory-check" } });
|
|
7316
|
-
if (r.status !== 200) return { ok: false, reason: `answered ${r.status}` };
|
|
7317
|
-
const body = (await r.text().catch(() => "")).toLowerCase();
|
|
7318
|
-
if (serverName && !body.includes(serverName.toLowerCase())) return { ok: false, reason: `the page does not mention "${serverName}"` };
|
|
7319
|
-
return { ok: true, reason: "live" };
|
|
7320
|
-
} catch (e) {
|
|
7321
|
-
return { ok: false, reason: e instanceof Error ? e.message : "unreachable" };
|
|
7322
|
-
}
|
|
7323
|
-
}
|
|
7324
|
-
async function runConnectorDocs(project, opts = {}) {
|
|
7325
|
-
const caller = await requireCaller();
|
|
7326
|
-
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
7327
|
-
const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
|
|
7328
|
-
const json = !!opts.json;
|
|
7329
|
-
const say = (s) => {
|
|
7330
|
-
if (!json) console.log(s);
|
|
7331
|
-
};
|
|
7332
|
-
const deps = opts.deps ?? {};
|
|
7333
|
-
const fr = await producer(caller, { method: "GET", path: `${projectPath(proj2)}/directory-facts?view=packet`, summary: "The facts the generator reads" });
|
|
7334
|
-
const prompt = directoryDocsPrompt(fr.facts);
|
|
7335
|
-
let docs = null;
|
|
7336
|
-
let how = "";
|
|
7337
|
-
if (opts.from) {
|
|
7338
|
-
docs = parseGeneratedDocs(import_fs5.default.readFileSync(opts.from, "utf8"));
|
|
7339
|
-
if (!docs) throw new Error(`${opts.from} does not hold the three documents (docs_markdown, reviewer_markdown, portal_answers).`);
|
|
7340
|
-
how = `from ${opts.from}`;
|
|
7341
|
-
} else if (opts.promptOut) {
|
|
7342
|
-
import_fs5.default.writeFileSync(opts.promptOut, prompt + "\n", { mode: 384 });
|
|
7343
|
-
say(import_chalk26.default.green(` \u2713 prompt written to ${opts.promptOut}`));
|
|
7344
|
-
say(import_chalk26.default.dim(` Run it through a model that answers JSON matching the schema, save the answer, then: ${B.cli} connector docs ${proj2.projectName} --from <answer.json>`));
|
|
7345
|
-
return;
|
|
7346
|
-
} else {
|
|
7347
|
-
const installed3 = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
|
|
7348
|
-
if (!installed3) {
|
|
7349
|
-
const file2 = import_path4.default.join(import_os2.default.tmpdir(), `apiblaze-connector-docs-${proj2.projectId}.txt`);
|
|
7350
|
-
import_fs5.default.writeFileSync(file2, prompt + "\n", { mode: 384 });
|
|
7351
|
-
throw new Error(`Claude Code is not installed here, so the documents were not generated. The prompt is at ${file2}: run it through any model that answers JSON, then \`${B.cli} connector docs ${proj2.projectName} --from <answer.json>\`; or write them on the dashboard page.`);
|
|
7352
|
-
}
|
|
7353
|
-
say(import_chalk26.default.dim(" generating the three documents on Claude Code (30\u201390 s)\u2026"));
|
|
7354
|
-
const g = generateWithClaude(prompt, deps);
|
|
7355
|
-
if (!g.docs) throw new Error(`${g.detail}. Run again, or \`${B.cli} connector docs ${proj2.projectName} --prompt-out prompt.txt\` to use another model.`);
|
|
7356
|
-
docs = g.docs;
|
|
7357
|
-
how = g.detail;
|
|
7358
|
-
}
|
|
7359
|
-
const changed = await storeGeneratedDocs(caller, proj2, docs, { factsHash: fr.facts_hash, reviewerUsername: fr.facts.reviewer_username ?? null, useGeneratedDocsUrl: true });
|
|
7360
|
-
if (json) {
|
|
7361
|
-
process.stdout.write(JSON.stringify({ project: proj2.projectId, changed, docs_url: fr.generated_docs_url, how }) + "\n");
|
|
7362
|
-
return;
|
|
7363
|
-
}
|
|
7364
|
-
console.log(import_chalk26.default.green(` \u2713 documents stored (${how}): ${changed.join(", ")}`));
|
|
7365
|
-
console.log(` Documentation page: ${import_chalk26.default.cyan(fr.generated_docs_url)} \u2014 link it from your site; edit it on the dashboard any time.`);
|
|
7366
|
-
}
|
|
7367
|
-
async function askDocsQuestion(caller, proj2, state, interactive2, say, deps = {}) {
|
|
7368
|
-
if (!interactive2) return false;
|
|
7369
|
-
const { default: inquirer3 } = await import("inquirer");
|
|
7370
|
-
const a = await inquirer3.prompt([{ type: "input", name: "value", message: `Documentation for this connector on your site? Paste the URL, or N to use the generated page at ${state.generatedDocsUrl} (and link it from your site):`, default: state.docsUrl ?? "N" }]);
|
|
7371
|
-
const v = a.value.trim();
|
|
7372
|
-
if (!v || /^n(o)?$/i.test(v)) {
|
|
7373
|
-
say(import_chalk26.default.dim(" using the generated page"));
|
|
7374
|
-
try {
|
|
7375
|
-
await runConnectorDocs(proj2.projectName, { deps, yes: true });
|
|
7376
|
-
} catch (e) {
|
|
7377
|
-
say(import_chalk26.default.yellow(` ${e instanceof Error ? e.message : String(e)}`));
|
|
7378
|
-
await producer(caller, { method: "PUT", path: `${projectPath(proj2)}/directory-documents`, body: { use_generated_docs_url: true }, summary: "Point docs at the generated page" });
|
|
7379
|
-
}
|
|
7380
|
-
return true;
|
|
7381
|
-
}
|
|
7382
|
-
const check = await verifyDocsUrl(v, state.serverName, deps.fetch);
|
|
7383
|
-
if (!check.ok) {
|
|
7384
|
-
say(import_chalk26.default.yellow(` ${v} is not usable as documentation yet: ${check.reason}. Not stored.`));
|
|
7385
|
-
return false;
|
|
7386
|
-
}
|
|
7387
|
-
await producer(caller, { method: "PATCH", path: projectPath(proj2), body: { directory_metadata: { docs_url: v, docs_confirmed: true } }, summary: "Store the documentation URL" });
|
|
7388
|
-
say(import_chalk26.default.green(` \u2713 docs: ${v} (live, names ${state.serverName ?? "the server"})`));
|
|
7389
|
-
return true;
|
|
7390
|
-
}
|
|
7391
|
-
var import_chalk26, import_fs5, import_os2, import_path4, import_child_process5, projectPath;
|
|
7392
|
-
var init_connector_docs = __esm({
|
|
7393
|
-
"src/commands/connector-docs.ts"() {
|
|
7394
|
-
"use strict";
|
|
7395
|
-
import_chalk26 = __toESM(require("chalk"));
|
|
7396
|
-
import_fs5 = __toESM(require("fs"));
|
|
7397
|
-
import_os2 = __toESM(require("os"));
|
|
7398
|
-
import_path4 = __toESM(require("path"));
|
|
7399
|
-
import_child_process5 = require("child_process");
|
|
7400
|
-
init_caller();
|
|
7401
|
-
init_directory_docs();
|
|
7402
|
-
init_external_mcp();
|
|
7403
|
-
init_brand();
|
|
7404
|
-
projectPath = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
|
|
7405
|
-
}
|
|
7406
|
-
});
|
|
7407
|
-
|
|
7408
7197
|
// src/lib/headless-login.ts
|
|
7409
7198
|
async function discover(mcpUrl2, deps = {}) {
|
|
7410
7199
|
const out = { mcpUrl: mcpUrl2, unauthStatus: 0, prmUrl: null, prm: null, asUrl: null, as: null };
|
|
@@ -7579,7 +7368,8 @@ function orderTools(tools) {
|
|
|
7579
7368
|
const rank = { read: 0, create: 1, update: 2, write: 3, delete: 4 };
|
|
7580
7369
|
return [...tools].sort((a, b) => rank[toolRole(a)] - rank[toolRole(b)] || a.name.localeCompare(b.name));
|
|
7581
7370
|
}
|
|
7582
|
-
function exampleValue(name, p, known = {}) {
|
|
7371
|
+
function exampleValue(name, p, known = {}, given = {}) {
|
|
7372
|
+
if (given[name] !== void 0) return given[name];
|
|
7583
7373
|
if (p.example !== void 0 && p.example !== null) return p.example;
|
|
7584
7374
|
if (known[name] !== void 0) return known[name];
|
|
7585
7375
|
if (Array.isArray(p.enum) && p.enum.length) return p.enum[0];
|
|
@@ -7598,22 +7388,34 @@ function exampleValue(name, p, known = {}) {
|
|
|
7598
7388
|
if (/(^|_)(q|query|search|term|text|name|title)$/i.test(name)) return "test";
|
|
7599
7389
|
return "example";
|
|
7600
7390
|
}
|
|
7601
|
-
function validArgs(t, known = {}) {
|
|
7391
|
+
function validArgs(t, known = {}, given = {}) {
|
|
7602
7392
|
const out = {};
|
|
7603
7393
|
const props = t.inputProperties ?? {};
|
|
7604
7394
|
const pathParams = new Set([...(t.path ?? "").matchAll(/\{([^}]+)\}/g)].map((m) => m[1]));
|
|
7605
7395
|
for (const [name, p] of Object.entries(props)) {
|
|
7606
|
-
const wanted = p.required || pathParams.has(name) || p.example !== void 0 && p.example !== null || known[name] !== void 0;
|
|
7396
|
+
const wanted = p.required || pathParams.has(name) || p.example !== void 0 && p.example !== null || known[name] !== void 0 || given[name] !== void 0;
|
|
7607
7397
|
if (!wanted) continue;
|
|
7608
|
-
out[name] = exampleValue(name, p, known);
|
|
7398
|
+
out[name] = exampleValue(name, p, known, given);
|
|
7609
7399
|
}
|
|
7610
7400
|
return out;
|
|
7611
7401
|
}
|
|
7612
|
-
function
|
|
7402
|
+
function candidateFromPrompts(name, p, prompts) {
|
|
7403
|
+
const fmt = (p.format ?? "").toLowerCase();
|
|
7404
|
+
const text = prompts.join("\n");
|
|
7405
|
+
if (fmt === "uri" || fmt === "url" || URLISH.test(name)) {
|
|
7406
|
+
const m = text.match(/https?:\/\/[^\s"'<>)]+/);
|
|
7407
|
+
if (m) return m[0].replace(/[.,;:!?]+$/, "");
|
|
7408
|
+
const bare = text.match(/\b[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}(\/[^\s"'<>)]*)?/i);
|
|
7409
|
+
return bare ? bare[0].replace(/[.,;:!?]+$/, "") : null;
|
|
7410
|
+
}
|
|
7411
|
+
const quoted = text.match(/["“']([^"”']{1,80})["”']/);
|
|
7412
|
+
return quoted ? quoted[1].trim() : null;
|
|
7413
|
+
}
|
|
7414
|
+
function invalidArgs(t, known = {}, given = {}) {
|
|
7613
7415
|
const props = t.inputProperties ?? {};
|
|
7614
7416
|
const names = Object.keys(props);
|
|
7615
7417
|
if (!names.length) return null;
|
|
7616
|
-
const base2 = validArgs(t, known);
|
|
7418
|
+
const base2 = validArgs(t, known, given);
|
|
7617
7419
|
const withEnum = names.find((n) => Array.isArray(props[n].enum) && props[n].enum.length);
|
|
7618
7420
|
if (withEnum) return { args: { ...base2, [withEnum]: "not-a-valid-choice" }, broke: `${withEnum}: out of enum` };
|
|
7619
7421
|
const typed = names.find((n) => ["integer", "number", "boolean"].includes((props[n].type ?? "").toLowerCase()));
|
|
@@ -7656,7 +7458,7 @@ function localOutcome(r) {
|
|
|
7656
7458
|
if (b && typeof b === "object" && b.error) return { outcome: "no_call", text: typeof b.error.message === "string" ? b.error.message : "JSON-RPC error" };
|
|
7657
7459
|
const result = b && typeof b === "object" ? b.result : null;
|
|
7658
7460
|
if (result && typeof result === "object") {
|
|
7659
|
-
const text = Array.isArray(result.content) ? result.content.filter((c) => c && c.type === "text").map((c) => String(c.text)).join(" ").slice(0,
|
|
7461
|
+
const text = Array.isArray(result.content) ? result.content.filter((c) => c && c.type === "text").map((c) => String(c.text)).join(" ").slice(0, 65536) : "";
|
|
7660
7462
|
return result.isError ? { outcome: "error", text } : { outcome: "ok", text };
|
|
7661
7463
|
}
|
|
7662
7464
|
return { outcome: "no_call", text: `HTTP ${r.status}` };
|
|
@@ -7684,8 +7486,9 @@ function claudeRecipe(o) {
|
|
|
7684
7486
|
""
|
|
7685
7487
|
];
|
|
7686
7488
|
}
|
|
7687
|
-
function mcpConfigDocument(url, bearer) {
|
|
7688
|
-
|
|
7489
|
+
function mcpConfigDocument(url, bearer, runId2) {
|
|
7490
|
+
const headers = { ...bearer ? { Authorization: `Bearer ${bearer}` } : {}, ...runId2 ? { "apiblaze-directory-run": runId2 } : {} };
|
|
7491
|
+
return JSON.stringify({ mcpServers: { abz: { type: "http", url, ...Object.keys(headers).length ? { headers } : {} } } }, null, 2) + "\n";
|
|
7689
7492
|
}
|
|
7690
7493
|
function toolCallsInClaudeOutput(stdout) {
|
|
7691
7494
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -7707,21 +7510,143 @@ function toolCallsInClaudeOutput(stdout) {
|
|
|
7707
7510
|
function redactHead(text, max = 200) {
|
|
7708
7511
|
return text.replace(SECRETISH, "[redacted]").slice(0, max);
|
|
7709
7512
|
}
|
|
7710
|
-
var ID_PARAM, SECRETISH;
|
|
7513
|
+
var ID_PARAM, URLISH, SECRETISH;
|
|
7711
7514
|
var init_exercise = __esm({
|
|
7712
7515
|
"src/lib/exercise.ts"() {
|
|
7713
7516
|
"use strict";
|
|
7714
7517
|
ID_PARAM = /(^|_)(id|uuid|key|slug|number|no)$/i;
|
|
7518
|
+
URLISH = /(^|_)(url|uri|link|website|site|homepage|repo|repository)$/i;
|
|
7715
7519
|
SECRETISH = /(bearer\s+[A-Za-z0-9._~+/=-]{8,}|[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|\b(?:sk|pk|rk|key|tok|abz)_[A-Za-z0-9_-]{8,})/gi;
|
|
7716
7520
|
}
|
|
7717
7521
|
});
|
|
7718
7522
|
|
|
7523
|
+
// src/lib/portal-drafts.ts
|
|
7524
|
+
var portal_drafts_exports = {};
|
|
7525
|
+
__export(portal_drafts_exports, {
|
|
7526
|
+
ANSWER_MIN: () => ANSWER_MIN,
|
|
7527
|
+
describeResult: () => describeResult,
|
|
7528
|
+
draftDescription: () => draftDescription,
|
|
7529
|
+
draftMissing: () => draftMissing,
|
|
7530
|
+
draftReviewerInstructions: () => draftReviewerInstructions,
|
|
7531
|
+
draftUseCases: () => draftUseCases
|
|
7532
|
+
});
|
|
7533
|
+
function describeResult(text) {
|
|
7534
|
+
const t = (text ?? "").trim();
|
|
7535
|
+
if (!t) return "an empty answer";
|
|
7536
|
+
try {
|
|
7537
|
+
const v = JSON.parse(t);
|
|
7538
|
+
if (Array.isArray(v)) return v.length ? `a list of ${v.length} item${v.length === 1 ? "" : "s"}${v[0] && typeof v[0] === "object" ? ` with ${Object.keys(v[0]).slice(0, 8).join(", ")}` : ""}` : "an empty list";
|
|
7539
|
+
if (v && typeof v === "object") {
|
|
7540
|
+
const keys = Object.keys(v);
|
|
7541
|
+
return keys.length ? `a JSON answer with ${keys.slice(0, 8).join(", ")}${keys.length > 8 ? ` (+${keys.length - 8} more)` : ""}` : "an empty object";
|
|
7542
|
+
}
|
|
7543
|
+
} catch {
|
|
7544
|
+
}
|
|
7545
|
+
const head = t.replace(/\s+/g, " ").slice(0, 80);
|
|
7546
|
+
return `a text answer ("${head}${t.length > 80 ? "\u2026" : ""}")`;
|
|
7547
|
+
}
|
|
7548
|
+
function argsText(args) {
|
|
7549
|
+
if (!args || !Object.keys(args).length) return "no arguments";
|
|
7550
|
+
return Object.entries(args).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
7551
|
+
}
|
|
7552
|
+
function toolsCalledBy(i, prompt) {
|
|
7553
|
+
const names = i.promptTools[prompt] ?? [];
|
|
7554
|
+
return names.map((n) => i.tools.find((t) => t.name === n) ?? { name: n });
|
|
7555
|
+
}
|
|
7556
|
+
function draftUseCases(i) {
|
|
7557
|
+
const parts = [];
|
|
7558
|
+
const working = i.prompts.filter((p) => (i.promptTools[p] ?? []).length);
|
|
7559
|
+
const list = working.length ? working : i.prompts;
|
|
7560
|
+
if (list.length) parts.push(`People ask it, in their own words: ${list.map((p) => `"${p}"`).join(" \xB7 ")}.`);
|
|
7561
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7562
|
+
for (const p of list) {
|
|
7563
|
+
const called = toolsCalledBy(i, p);
|
|
7564
|
+
if (!called.length) continue;
|
|
7565
|
+
const key = called.map((t) => t.name).join("+");
|
|
7566
|
+
if (!groups.has(key)) groups.set(key, { tools: called, prompts: [] });
|
|
7567
|
+
groups.get(key).prompts.push(p);
|
|
7568
|
+
}
|
|
7569
|
+
const short = (p) => `"${p.length > 50 ? `${p.slice(0, 47)}\u2026` : p}"`;
|
|
7570
|
+
for (const g of groups.values()) {
|
|
7571
|
+
const row = i.rows.find((r) => r.tool === g.tools[0].name && r.valid === "ok");
|
|
7572
|
+
const which = g.prompts.length === list.length && list.length > 1 ? "Each of these" : g.prompts.map(short).join(", ");
|
|
7573
|
+
parts.push(`${which} make${g.prompts.length === 1 ? "s" : ""} Claude call ${g.tools.map(titleOf).join(" and ")}${row ? `, which returns ${shapeOf(row)}` : ""}.`);
|
|
7574
|
+
}
|
|
7575
|
+
const uncalled = i.tools.filter((t) => !Object.values(i.promptTools).some((ns) => ns.includes(t.name)));
|
|
7576
|
+
if (uncalled.length) parts.push(`Also available: ${uncalled.map((t) => `${titleOf(t)}${(t.description ?? "").trim() ? ` (${(t.description ?? "").trim().replace(/\.$/, "")})` : ""}`).join("; ")}.`);
|
|
7577
|
+
parts.push(readOnly(i) ? "Every tool only reads; nothing is created or changed." : "Write tools change data in the signed-in account only.");
|
|
7578
|
+
return parts.join(" ");
|
|
7579
|
+
}
|
|
7580
|
+
function draftReviewerInstructions(i) {
|
|
7581
|
+
const lines = [];
|
|
7582
|
+
lines.push(`Add ${i.publishedUrl} as a custom connector in Claude (Settings \u2192 Connectors \u2192 Add custom connector).`);
|
|
7583
|
+
lines.push(i.open ? "No sign-in: the data is public and every tool is read-only. Connect and ask." : `Sign in with the reviewer account in this packet${i.reviewerUsername ? ` (username ${i.reviewerUsername}; the password is in the packet, never in this text)` : ""}.`);
|
|
7584
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7585
|
+
for (const p of i.prompts) {
|
|
7586
|
+
const called = toolsCalledBy(i, p);
|
|
7587
|
+
if (!called.length) continue;
|
|
7588
|
+
const row = i.rows.find((r) => r.tool === called[0].name && r.valid === "ok");
|
|
7589
|
+
called.forEach((t) => seen.add(t.name));
|
|
7590
|
+
lines.push(`Ask: "${p}" \u2014 Claude calls ${called.map(titleOf).join(" and ")}${row ? `; expect ${shapeOf(row)}` : ""}.`);
|
|
7591
|
+
}
|
|
7592
|
+
for (const r of i.rows) {
|
|
7593
|
+
if (seen.has(r.tool) || r.valid !== "ok") continue;
|
|
7594
|
+
const t = i.tools.find((x) => x.name === r.tool) ?? { name: r.tool };
|
|
7595
|
+
lines.push(`Ask Claude to ${titleOf(t).replace(/^./, (c) => c.toLowerCase())} (${argsText(r.args)}); expect ${shapeOf(r)}.`);
|
|
7596
|
+
}
|
|
7597
|
+
const errs = i.rows.filter((r) => r.invalid === "error" && r.invalidText);
|
|
7598
|
+
for (const r of errs.slice(0, 2)) {
|
|
7599
|
+
const [broke, ...rest] = r.invalidText.split(" \u2192 ");
|
|
7600
|
+
const answer = rest.join(" \u2192 ").replace(/\s+/g, " ").slice(0, 100);
|
|
7601
|
+
lines.push(`A bad argument (${r.tool}, ${broke}) answers "${answer}" \u2014 a message, not a crash.`);
|
|
7602
|
+
}
|
|
7603
|
+
const declined = i.rows.filter((r) => /declined|not exercised/i.test(r.note ?? ""));
|
|
7604
|
+
if (declined.length) lines.push(`Not run in the test, by choice: ${declined.map((r) => r.tool).join(", ")} (write tools; they change data in the signed-in account only).`);
|
|
7605
|
+
lines.push(readOnly(i) ? "Every tool only reads; nothing is created or changed." : "Every write in the test was on data the test itself created.");
|
|
7606
|
+
return lines.map((l, n) => `${n + 1}. ${l}`).join("\n");
|
|
7607
|
+
}
|
|
7608
|
+
function draftDescription(i) {
|
|
7609
|
+
const parts = [];
|
|
7610
|
+
const base2 = (i.description ?? "").trim() || (i.tagline ?? "").trim();
|
|
7611
|
+
if (base2) parts.push(base2.replace(/\.?$/, "."));
|
|
7612
|
+
const toolLines = i.tools.map((t) => {
|
|
7613
|
+
const d = (t.description ?? "").trim().replace(/\.$/, "");
|
|
7614
|
+
return d && d.toLowerCase() !== titleOf(t).toLowerCase() ? `${titleOf(t)}: ${d}` : titleOf(t);
|
|
7615
|
+
});
|
|
7616
|
+
if (toolLines.length) parts.push(`${i.tools.length === 1 ? "One tool" : `${i.tools.length} tools`}: ${toolLines.join("; ")}.`);
|
|
7617
|
+
const working = i.prompts.filter((p) => (i.promptTools[p] ?? []).length);
|
|
7618
|
+
if (working.length) parts.push(`Ask it things like ${working.map((p) => `"${p}"`).join(", ")}.`);
|
|
7619
|
+
const ok = i.rows.find((r) => r.valid === "ok");
|
|
7620
|
+
if (ok) parts.push(`What comes back is ${shapeOf(ok)}, which Claude reads and explains.`);
|
|
7621
|
+
parts.push(readOnly(i) ? "Every tool only reads; nothing is created or changed." : "Writes change data in the signed-in account only.");
|
|
7622
|
+
return parts.join(" ");
|
|
7623
|
+
}
|
|
7624
|
+
function draftMissing(i, current) {
|
|
7625
|
+
const out = [];
|
|
7626
|
+
const len = (v) => (v ?? "").trim().length;
|
|
7627
|
+
if (len(current.description) < ANSWER_MIN.description) out.push({ key: "description", label: "Listing description", draft: draftDescription({ ...i, description: current.description ?? i.description }), current: (current.description ?? "").trim() });
|
|
7628
|
+
if (len(current.use_cases) < ANSWER_MIN.use_cases) out.push({ key: "use_cases", label: "Use cases", draft: draftUseCases(i), current: (current.use_cases ?? "").trim() });
|
|
7629
|
+
if (len(current.reviewer_instructions) < ANSWER_MIN.reviewer_instructions) out.push({ key: "reviewer_instructions", label: "Reviewer instructions", draft: draftReviewerInstructions(i), current: (current.reviewer_instructions ?? "").trim() });
|
|
7630
|
+
return out;
|
|
7631
|
+
}
|
|
7632
|
+
var ANSWER_MIN, titleOf, shapeOf, readOnly;
|
|
7633
|
+
var init_portal_drafts = __esm({
|
|
7634
|
+
"src/lib/portal-drafts.ts"() {
|
|
7635
|
+
"use strict";
|
|
7636
|
+
ANSWER_MIN = { use_cases: 200, description: 300, reviewer_instructions: 400 };
|
|
7637
|
+
titleOf = (t) => (t.title ?? "").trim() || t.name;
|
|
7638
|
+
shapeOf = (row) => row ? row.resultShape ?? describeResult(row.validText) : "";
|
|
7639
|
+
readOnly = (i) => i.tools.every((t) => i.rows.find((r) => r.tool === t.name)?.role === "read" || /^(get|head)$/i.test(t.method ?? "get"));
|
|
7640
|
+
}
|
|
7641
|
+
});
|
|
7642
|
+
|
|
7719
7643
|
// src/commands/connector-exercise.ts
|
|
7720
7644
|
var connector_exercise_exports = {};
|
|
7721
7645
|
__export(connector_exercise_exports, {
|
|
7722
7646
|
DASHBOARD_BASE: () => DASHBOARD_BASE8,
|
|
7723
7647
|
RUN_HEADER: () => RUN_HEADER,
|
|
7724
7648
|
RUN_META_KEY: () => RUN_META_KEY,
|
|
7649
|
+
createPromptTester: () => createPromptTester,
|
|
7725
7650
|
reportLink: () => reportLink,
|
|
7726
7651
|
runConnectorAnswers: () => runConnectorAnswers,
|
|
7727
7652
|
runConnectorExercise: () => runConnectorExercise
|
|
@@ -7745,60 +7670,87 @@ async function runConnectorExercise(project, opts = {}) {
|
|
|
7745
7670
|
if (!json) console.log(s);
|
|
7746
7671
|
};
|
|
7747
7672
|
const deps = opts.deps ?? {};
|
|
7748
|
-
say(
|
|
7673
|
+
if (opts.report !== false) say(import_chalk26.default.cyan(` Report: ${reportLink(proj2.projectId, proj2.apiVersion)}`));
|
|
7749
7674
|
if (opts.emptyOk?.length) {
|
|
7750
|
-
await producer(caller, { method: "PATCH", path:
|
|
7751
|
-
say(
|
|
7675
|
+
await producer(caller, { method: "PATCH", path: projectPath(proj2), body: { directory_metadata: { empty_ok: opts.emptyOk } }, summary: "Mark tools legitimately empty" });
|
|
7676
|
+
say(import_chalk26.default.dim(` marked legitimately empty: ${opts.emptyOk.join(", ")}`));
|
|
7752
7677
|
}
|
|
7753
7678
|
let r = await fetchDirectoryReport(caller, proj2, { probe: false });
|
|
7754
7679
|
const target2 = r.target ?? {};
|
|
7755
7680
|
const tools = orderTools(target2.tools ?? []);
|
|
7756
7681
|
const publishedUrl = r.report.project.publishedUrl;
|
|
7757
|
-
if (!publishedUrl) throw new Error(`${proj2.projectName} has no published MCP URL yet (run \`${B.cli} connector ${proj2.
|
|
7682
|
+
if (!publishedUrl) throw new Error(`${proj2.projectName} has no published MCP URL yet (run \`${B.cli} connector ${proj2.projectId}\` first).`);
|
|
7758
7683
|
if (!tools.length) throw new Error(`${proj2.projectName} exposes no tools to exercise.`);
|
|
7759
|
-
if (target2.project?.directoryReady === false) throw new Error(`Directory-ready mode is off for ${proj2.projectName} v${proj2.apiVersion}: nothing is recorded and the sign-in door is shut. Turn it on first (\`${B.cli} connector ${proj2.
|
|
7684
|
+
if (target2.project?.directoryReady === false) throw new Error(`Directory-ready mode is off for ${proj2.projectName} v${proj2.apiVersion}: nothing is recorded and the sign-in door is shut. Turn it on first (\`${B.cli} connector ${proj2.projectId}\`, or the switch on the dashboard) and run again.`);
|
|
7760
7685
|
const open = target2.project?.authMode === "open";
|
|
7761
|
-
const opened = await producer(caller, { method: "POST", path: `${
|
|
7686
|
+
const opened = await producer(caller, { method: "POST", path: `${projectPath(proj2)}/directory-runs`, body: { runner: "cli-direct" }, summary: "Open an exercise run" });
|
|
7762
7687
|
const runId2 = opened.run.run_id;
|
|
7763
7688
|
const notes = [];
|
|
7764
7689
|
const rows = [];
|
|
7765
7690
|
let bearer = null;
|
|
7766
7691
|
let asMeta = null;
|
|
7767
7692
|
let configDir = null;
|
|
7693
|
+
let reviewerUsername = null;
|
|
7768
7694
|
try {
|
|
7769
7695
|
if (!open) {
|
|
7770
|
-
const packet = await producer(caller, { method: "GET", path: `${
|
|
7696
|
+
const packet = await producer(caller, { method: "GET", path: `${projectPath(proj2)}/directory-packet?probe=0&force=1`, summary: "Reviewer account (for the login)" });
|
|
7771
7697
|
const acct = packet.json?.test_account ?? null;
|
|
7772
|
-
if (!acct) throw new Error(`No reviewer account on ${proj2.projectName}: create it first (\`${B.cli} connector reviewer ${proj2.
|
|
7698
|
+
if (!acct) throw new Error(`No reviewer account on ${proj2.projectName}: create it first (\`${B.cli} connector reviewer ${proj2.projectId}\`), or declare open mode for public read-only data.`);
|
|
7773
7699
|
const login = await headlessPasswordLogin({ mcpUrl: publishedUrl, username: acct.username, password: acct.password }, deps);
|
|
7774
7700
|
if (!login.accessToken) throw new Error(`The reviewer could not sign in at ${login.failedAt}: ${login.detail}. Fix the sign-in and run again.`);
|
|
7775
7701
|
bearer = login.accessToken;
|
|
7776
7702
|
asMeta = login.as;
|
|
7777
|
-
|
|
7703
|
+
reviewerUsername = acct.username;
|
|
7704
|
+
say(import_chalk26.default.dim(` signed in as the reviewer (${acct.username}); the token is revoked at the end`));
|
|
7778
7705
|
} else {
|
|
7779
|
-
say(
|
|
7706
|
+
say(import_chalk26.default.dim(" open mode: no sign-in; this run's calls count as yours"));
|
|
7780
7707
|
}
|
|
7781
7708
|
const ua = `apiblaze-cli/${version} directory-exercise`;
|
|
7782
7709
|
const headers = { [RUN_HEADER]: runId2 };
|
|
7783
7710
|
const call3 = (name, args) => mcpCall(publishedUrl, "tools/call", { name, arguments: args, _meta: { [RUN_META_KEY]: runId2 } }, { bearer, headers, userAgent: ua }, deps);
|
|
7784
7711
|
const created = {};
|
|
7785
|
-
|
|
7712
|
+
const examples = { ...target2.metadata?.toolExamples ?? {} };
|
|
7713
|
+
const learned = {};
|
|
7714
|
+
const prompts = (target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string");
|
|
7715
|
+
for (const t of opts.promptsOnly ? [] : tools) {
|
|
7786
7716
|
const role = toolRole(t);
|
|
7787
7717
|
const row = { tool: t.name, role, valid: "no_call", validText: "", invalid: "skipped", invalidText: "" };
|
|
7788
7718
|
rows.push(row);
|
|
7789
|
-
|
|
7719
|
+
let args = validArgs(t, created, examples[t.name]);
|
|
7790
7720
|
if (role !== "read") {
|
|
7791
7721
|
const go = await confirm(`${t.name} (${(t.method ?? "POST").toUpperCase()} ${t.path ?? ""}) with ${JSON.stringify(args)} \u2014 call it?`, interactive2);
|
|
7792
7722
|
if (!go) {
|
|
7793
7723
|
row.note = "not exercised: producer declined";
|
|
7794
7724
|
notes.push({ tool: t.name, note: "not exercised: producer declined" });
|
|
7795
|
-
say(` ${
|
|
7725
|
+
say(` ${import_chalk26.default.yellow("\u2013")} ${t.name} not exercised (${interactive2 ? "declined" : "non-interactive: writes need a y"})`);
|
|
7796
7726
|
continue;
|
|
7797
7727
|
}
|
|
7798
7728
|
}
|
|
7799
|
-
|
|
7729
|
+
let v = localOutcome(await call3(t.name, args));
|
|
7730
|
+
if (role === "read" && v.outcome !== "ok" && interactive2 && Object.keys(args).length) {
|
|
7731
|
+
const { default: inquirer3 } = await import("inquirer");
|
|
7732
|
+
say(` ${mark(v.outcome)} ${t.name} refused ${JSON.stringify(args)}: ${import_chalk26.default.dim(redactHead(v.text, 120) || v.outcome)}`);
|
|
7733
|
+
const fixed = {};
|
|
7734
|
+
for (const [name, cur] of Object.entries(args)) {
|
|
7735
|
+
const p = t.inputProperties?.[name] ?? {};
|
|
7736
|
+
const proposed = candidateFromPrompts(name, p, prompts);
|
|
7737
|
+
const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: `A value for ${name} that works (Enter to leave it):`, ...proposed && proposed !== String(cur) ? { default: proposed } : {} }]);
|
|
7738
|
+
const s = (value ?? "").trim();
|
|
7739
|
+
if (!s || s === String(cur)) continue;
|
|
7740
|
+
const type = (p.type ?? "string").toLowerCase();
|
|
7741
|
+
fixed[name] = type === "integer" || type === "number" ? Number.isFinite(Number(s)) ? Number(s) : s : type === "boolean" ? s === "true" : s;
|
|
7742
|
+
}
|
|
7743
|
+
if (Object.keys(fixed).length) {
|
|
7744
|
+
examples[t.name] = { ...examples[t.name] ?? {}, ...fixed };
|
|
7745
|
+
args = validArgs(t, created, examples[t.name]);
|
|
7746
|
+
v = localOutcome(await call3(t.name, args));
|
|
7747
|
+
if (v.outcome === "ok") learned[t.name] = fixed;
|
|
7748
|
+
}
|
|
7749
|
+
}
|
|
7800
7750
|
row.valid = v.outcome;
|
|
7801
7751
|
row.validText = redactHead(v.text);
|
|
7752
|
+
row.args = args;
|
|
7753
|
+
if (v.outcome === "ok") row.resultShape = describeResult(v.text);
|
|
7802
7754
|
if (v.outcome === "ok" && role === "create") {
|
|
7803
7755
|
const id = extractCreatedId(parseResultJson(v.text));
|
|
7804
7756
|
if (id) {
|
|
@@ -7808,187 +7760,507 @@ async function runConnectorExercise(project, opts = {}) {
|
|
|
7808
7760
|
}
|
|
7809
7761
|
}
|
|
7810
7762
|
if (role === "read") {
|
|
7811
|
-
const bad = invalidArgs(t, created);
|
|
7763
|
+
const bad = invalidArgs(t, created, examples[t.name]);
|
|
7812
7764
|
if (bad) {
|
|
7813
7765
|
const e = localOutcome(await call3(t.name, bad.args));
|
|
7814
7766
|
row.invalid = e.outcome;
|
|
7815
7767
|
row.invalidText = redactHead(`${bad.broke} \u2192 ${e.text}`);
|
|
7816
7768
|
}
|
|
7817
7769
|
}
|
|
7818
|
-
say(` ${mark(v.outcome)} ${t.name} ${
|
|
7770
|
+
say(` ${mark(v.outcome)} ${t.name} ${import_chalk26.default.dim(v.text.replace(/\s+/g, " ").slice(0, 80))}${row.invalid !== "skipped" ? import_chalk26.default.dim(` \xB7 bad arg: ${row.invalid === "error" ? "refused with a message" : row.invalid}`) : ""}`);
|
|
7771
|
+
}
|
|
7772
|
+
if (Object.keys(learned).length) {
|
|
7773
|
+
await producer(caller, { method: "PATCH", path: projectPath(proj2), body: { directory_metadata: { tool_examples: learned } }, summary: "Remember the working arguments" });
|
|
7774
|
+
say(import_chalk26.default.green(` \u2713 remembered the working arguments for ${Object.keys(learned).join(", ")} (used on every run from now on)`));
|
|
7819
7775
|
}
|
|
7820
7776
|
const claudeInstalled = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
|
|
7821
7777
|
if (!opts.noClaude && claudeInstalled) {
|
|
7822
|
-
const spawn5 = deps.spawn ??
|
|
7823
|
-
configDir =
|
|
7824
|
-
|
|
7825
|
-
const cfgPath =
|
|
7826
|
-
|
|
7827
|
-
const
|
|
7778
|
+
const spawn5 = deps.spawn ?? import_child_process5.spawnSync;
|
|
7779
|
+
configDir = import_fs5.default.mkdtempSync(import_path4.default.join(deps.tmpdir ?? import_os2.default.tmpdir(), "abz-exercise-"));
|
|
7780
|
+
import_fs5.default.chmodSync(configDir, 448);
|
|
7781
|
+
const cfgPath = import_path4.default.join(configDir, "mcp.json");
|
|
7782
|
+
import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
|
|
7783
|
+
const proven = new Map((opts.provenPromptNotes ?? []).map((n) => [n.prompt.trim(), n]));
|
|
7784
|
+
const prompts2 = (opts.promptsOnly ?? target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string" && p.trim()).slice(0, 3);
|
|
7828
7785
|
const firstRead = tools.find((t) => toolRole(t) === "read") ?? tools[0];
|
|
7829
7786
|
const runs = [
|
|
7830
|
-
{ prompt: `Use the ${firstRead.title ?? firstRead.name} tool once and summarise what it returned in one sentence.`, allowed: [`mcp__abz__${firstRead.name}`], kind: "tool" },
|
|
7831
|
-
...
|
|
7787
|
+
...opts.promptsOnly ? [] : [{ prompt: `Use the ${firstRead.title ?? firstRead.name} tool once and summarise what it returned in one sentence.`, allowed: [`mcp__abz__${firstRead.name}`], kind: "tool" }],
|
|
7788
|
+
...prompts2.filter((p) => !proven.has(p.trim())).map((p) => ({ prompt: p, allowed: ["mcp__abz__*"], kind: "prompt" }))
|
|
7832
7789
|
];
|
|
7833
|
-
|
|
7790
|
+
for (const p of prompts2) {
|
|
7791
|
+
const n = proven.get(p.trim());
|
|
7792
|
+
if (n) notes.push({ ...n });
|
|
7793
|
+
}
|
|
7794
|
+
if (proven.size) say(import_chalk26.default.dim(` ${proven.size} prompt${proven.size > 1 ? "s" : ""} already run through Claude Code as ${proven.size > 1 ? "they were" : "it was"} typed`));
|
|
7795
|
+
say(import_chalk26.default.dim(` Claude Code pass: ${runs.length} one-shot${runs.length > 1 ? "s" : ""} (10\u201325 s each)`));
|
|
7834
7796
|
for (const x of runs) {
|
|
7835
|
-
const
|
|
7836
|
-
const calls = toolCallsInClaudeOutput(String(res.stdout ?? ""));
|
|
7797
|
+
const { calls, status: status2 } = claudeOneShot2(spawn5, cfgPath, x.prompt, x.allowed);
|
|
7837
7798
|
if (x.kind === "prompt") notes.push({ kind: "prompt", prompt: x.prompt, tool_called: calls.length > 0, tools: calls });
|
|
7838
7799
|
else notes.push({ kind: "claude-tool", tool: firstRead.name, tool_called: calls.length > 0 });
|
|
7839
|
-
say(` ${calls.length ?
|
|
7800
|
+
say(` ${calls.length ? import_chalk26.default.green("\u2713") : import_chalk26.default.yellow("\u2013")} claude: "${x.prompt.slice(0, 60)}" \u2192 ${calls.length ? `called ${calls.join(", ")}` : "no tool call"}${status2 !== 0 && status2 != null ? import_chalk26.default.dim(` (exit ${status2})`) : ""}`);
|
|
7840
7801
|
}
|
|
7841
7802
|
} else if (!opts.noClaude) {
|
|
7842
|
-
say(
|
|
7803
|
+
say(import_chalk26.default.dim(" Claude Code is not installed here: the Claude pass is skipped (TESTED_FROM_CLAUDE and EXAMPLE_PROMPTS_WORK stay open with their instruction)."));
|
|
7843
7804
|
}
|
|
7844
7805
|
} finally {
|
|
7845
7806
|
if (configDir) {
|
|
7846
7807
|
try {
|
|
7847
|
-
|
|
7808
|
+
import_fs5.default.rmSync(configDir, { recursive: true, force: true });
|
|
7809
|
+
} catch {
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
if (bearer) {
|
|
7813
|
+
await revokeToken(asMeta, bearer, void 0, deps).catch(() => false);
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
r = await fetchDirectoryReport(caller, proj2, { publishedUrl, ...opts.platforms ? { platforms: opts.platforms } : {} });
|
|
7817
|
+
const verdicts = r.report.verdicts ?? null;
|
|
7818
|
+
const closed = await producer(caller, { method: "PATCH", path: `${projectPath(proj2)}/directory-runs/${encodeURIComponent(runId2)}`, body: { verdicts, scores: verdicts?.groups ?? null, notes, runner: "cli-direct", ...opts.platforms?.length ? { platforms: opts.platforms } : {}, ...opts.noEmail ? { email: false } : {} }, summary: "Close the exercise run" });
|
|
7819
|
+
const promptTools = {};
|
|
7820
|
+
for (const n of notes) if (n.kind === "prompt" && typeof n.prompt === "string") promptTools[n.prompt] = Array.isArray(n.tools) ? n.tools : [];
|
|
7821
|
+
const summary = {
|
|
7822
|
+
failedPrompts: notes.filter((n) => n.kind === "prompt" && n.tool_called === false && typeof n.prompt === "string").map((n) => n.prompt),
|
|
7823
|
+
rows,
|
|
7824
|
+
promptTools,
|
|
7825
|
+
publishedUrl,
|
|
7826
|
+
open,
|
|
7827
|
+
reviewerUsername,
|
|
7828
|
+
tools
|
|
7829
|
+
};
|
|
7830
|
+
if (json) {
|
|
7831
|
+
process.stdout.write(JSON.stringify({ project: proj2.projectId, run_id: runId2, link: reportLink(proj2.projectId, proj2.apiVersion), tools: rows, notes, headline: r.rendered.headline, verdicts, report: r.report }) + "\n");
|
|
7832
|
+
return summary;
|
|
7833
|
+
}
|
|
7834
|
+
if (opts.report !== false) {
|
|
7835
|
+
console.log();
|
|
7836
|
+
printReport(r, false);
|
|
7837
|
+
console.log(import_chalk26.default.cyan(` Report: ${reportLink(proj2.projectId, proj2.apiVersion)}`));
|
|
7838
|
+
}
|
|
7839
|
+
console.log(import_chalk26.default.dim(opts.noEmail ? " (no email for this run)" : closed?.emailed ? " (the result was emailed to you; --no-email or the project setting turns it off)" : " (no email: the run has no address to send to)"));
|
|
7840
|
+
return summary;
|
|
7841
|
+
}
|
|
7842
|
+
function claudeOneShot2(spawn5, cfgPath, prompt, allowed) {
|
|
7843
|
+
const res = spawn5("claude", claudeRecipe({ prompt, mcpConfigPath: cfgPath, allowedTools: allowed }), { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, env: { ...process.env, CLAUDECODE: "" } });
|
|
7844
|
+
return { calls: toolCallsInClaudeOutput(String(res.stdout ?? "")), status: res.status };
|
|
7845
|
+
}
|
|
7846
|
+
async function createPromptTester(caller, proj2, opts = {}) {
|
|
7847
|
+
const say = opts.say ?? ((s) => console.log(s));
|
|
7848
|
+
const deps = opts.deps ?? {};
|
|
7849
|
+
const claudeInstalled = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
|
|
7850
|
+
if (!claudeInstalled) {
|
|
7851
|
+
say(import_chalk26.default.dim(" (Claude Code is not installed here: each prompt is run at the end, in the exercise)"));
|
|
7852
|
+
return null;
|
|
7853
|
+
}
|
|
7854
|
+
const r = await fetchDirectoryReport(caller, proj2, { probe: false });
|
|
7855
|
+
const target2 = r.target ?? {};
|
|
7856
|
+
const publishedUrl = r.report.project.publishedUrl;
|
|
7857
|
+
if (!publishedUrl || target2.project?.directoryReady === false) return null;
|
|
7858
|
+
const open = target2.project?.authMode === "open";
|
|
7859
|
+
let bearer = null;
|
|
7860
|
+
let asMeta = null;
|
|
7861
|
+
if (!open) {
|
|
7862
|
+
const packet = await producer(caller, { method: "GET", path: `${projectPath(proj2)}/directory-packet?probe=0&force=1`, summary: "Reviewer account (for the login)" }).catch(() => null);
|
|
7863
|
+
const acct = packet?.json?.test_account ?? null;
|
|
7864
|
+
if (!acct) {
|
|
7865
|
+
say(import_chalk26.default.dim(" (no reviewer account yet: each prompt is run at the end, in the exercise)"));
|
|
7866
|
+
return null;
|
|
7867
|
+
}
|
|
7868
|
+
const login = await headlessPasswordLogin({ mcpUrl: publishedUrl, username: acct.username, password: acct.password }, deps);
|
|
7869
|
+
if (!login.accessToken) {
|
|
7870
|
+
say(import_chalk26.default.yellow(` (the reviewer could not sign in at ${login.failedAt}: ${login.detail}; each prompt is run at the end, in the exercise)`));
|
|
7871
|
+
return null;
|
|
7872
|
+
}
|
|
7873
|
+
bearer = login.accessToken;
|
|
7874
|
+
asMeta = login.as;
|
|
7875
|
+
}
|
|
7876
|
+
const opened = await producer(caller, { method: "POST", path: `${projectPath(proj2)}/directory-runs`, body: { runner: "cli-direct" }, summary: "Open a prompt-test run" });
|
|
7877
|
+
const runId2 = opened.run.run_id;
|
|
7878
|
+
const spawn5 = deps.spawn ?? import_child_process5.spawnSync;
|
|
7879
|
+
const configDir = import_fs5.default.mkdtempSync(import_path4.default.join(deps.tmpdir ?? import_os2.default.tmpdir(), "abz-prompt-"));
|
|
7880
|
+
import_fs5.default.chmodSync(configDir, 448);
|
|
7881
|
+
const cfgPath = import_path4.default.join(configDir, "mcp.json");
|
|
7882
|
+
import_fs5.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer, runId2), { mode: 384 });
|
|
7883
|
+
const notes = [];
|
|
7884
|
+
return {
|
|
7885
|
+
async test(prompt) {
|
|
7886
|
+
const { calls } = claudeOneShot2(spawn5, cfgPath, prompt, ["mcp__abz__*"]);
|
|
7887
|
+
const note = { kind: "prompt", prompt, tool_called: calls.length > 0, tools: calls };
|
|
7888
|
+
const at = notes.findIndex((n) => n.prompt === prompt);
|
|
7889
|
+
if (at >= 0) notes[at] = note;
|
|
7890
|
+
else notes.push(note);
|
|
7891
|
+
return { called: calls.length > 0, tools: calls };
|
|
7892
|
+
},
|
|
7893
|
+
notes: () => [...notes],
|
|
7894
|
+
async close() {
|
|
7895
|
+
try {
|
|
7896
|
+
import_fs5.default.rmSync(configDir, { recursive: true, force: true });
|
|
7897
|
+
} catch {
|
|
7898
|
+
}
|
|
7899
|
+
if (bearer) await revokeToken(asMeta, bearer, void 0, deps).catch(() => false);
|
|
7900
|
+
await producer(caller, { method: "PATCH", path: `${projectPath(proj2)}/directory-runs/${encodeURIComponent(runId2)}`, body: { notes, runner: "cli-direct", email: false }, summary: "Close the prompt-test run" }).catch(() => null);
|
|
7901
|
+
}
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
function mark(o) {
|
|
7905
|
+
return o === "ok" ? import_chalk26.default.green("\u2713") : o === "error" ? import_chalk26.default.yellow("!") : import_chalk26.default.red("\u2717");
|
|
7906
|
+
}
|
|
7907
|
+
function parseResultJson(text) {
|
|
7908
|
+
try {
|
|
7909
|
+
return JSON.parse(text);
|
|
7910
|
+
} catch {
|
|
7911
|
+
return null;
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7914
|
+
async function runConnectorAnswers(project, opts = {}) {
|
|
7915
|
+
const caller = await requireCaller();
|
|
7916
|
+
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
7917
|
+
const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
|
|
7918
|
+
const json = !!opts.json;
|
|
7919
|
+
const interactive2 = !!process.stdin.isTTY && !json && !opts.yes;
|
|
7920
|
+
const dh = parseDataHandling(opts.dataHandling, opts.dataHandlingStatement);
|
|
7921
|
+
const health = parseYesNo("--health-data", opts.healthData);
|
|
7922
|
+
const sponsored = parseYesNo("--sponsored-content", opts.sponsoredContent);
|
|
7923
|
+
const access = opts.access ? opts.access.trim().toLowerCase() : void 0;
|
|
7924
|
+
if (access && !["read", "write", "both"].includes(access)) throw new Error("--access must be read, write or both.");
|
|
7925
|
+
const answers = {};
|
|
7926
|
+
for (const q of QUESTIONS) {
|
|
7927
|
+
const v = opts[q.key];
|
|
7928
|
+
if (typeof v === "string" && v.trim()) answers[q.field] = v.trim();
|
|
7929
|
+
}
|
|
7930
|
+
if (access) answers.access = access;
|
|
7931
|
+
if (opts.category?.length) answers.categories = opts.category.map((c) => c.trim()).filter(Boolean).slice(0, 5);
|
|
7932
|
+
const meta = {};
|
|
7933
|
+
if (dh) meta.data_handling = dh;
|
|
7934
|
+
if (health) meta.health_data = health;
|
|
7935
|
+
if (sponsored) meta.sponsored_content = sponsored;
|
|
7936
|
+
if (interactive2) {
|
|
7937
|
+
const current = await producer(caller, { method: "POST", path: `${projectPath(proj2)}/directory-report`, body: { probe: false }, summary: "Current answers" });
|
|
7938
|
+
const have = current.target?.metadata?.portalAnswers ?? {};
|
|
7939
|
+
const { default: inquirer3 } = await import("inquirer");
|
|
7940
|
+
const saveNow = async (part) => {
|
|
7941
|
+
try {
|
|
7942
|
+
await producer(caller, { method: "PATCH", path: projectPath(proj2), body: part, summary: "Save an answer" });
|
|
7943
|
+
} catch (e) {
|
|
7944
|
+
console.log(import_chalk26.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
|
|
7945
|
+
}
|
|
7946
|
+
};
|
|
7947
|
+
let headed = opts.derived !== false;
|
|
7948
|
+
const ask3 = async (qs) => {
|
|
7949
|
+
if (!headed) {
|
|
7950
|
+
headed = true;
|
|
7951
|
+
console.log(import_chalk26.default.bold(" What the portal asks that only you can answer (saved as you go):"));
|
|
7952
|
+
}
|
|
7953
|
+
return inquirer3.prompt(qs);
|
|
7954
|
+
};
|
|
7955
|
+
const desc = (current.target?.metadata?.description ?? "").trim();
|
|
7956
|
+
if (opts.derived !== false && desc.length < DESCRIPTION_MIN) {
|
|
7957
|
+
const a = await ask3([{ type: "input", name: "value", message: `Description (what people can do with it; ${DESCRIPTION_MIN}\u20132000 characters):`, default: desc || void 0, validate: (v) => v.trim().length >= DESCRIPTION_MIN ? true : `${v.trim().length}/${DESCRIPTION_MIN} characters \u2014 a reviewer needs to know what it does and what comes back` }]);
|
|
7958
|
+
meta.description = a.value.trim();
|
|
7959
|
+
await saveNow({ directory_metadata: { description: meta.description } });
|
|
7960
|
+
}
|
|
7961
|
+
for (const q of QUESTIONS) {
|
|
7962
|
+
if (answers[q.field] !== void 0) continue;
|
|
7963
|
+
if (opts.derived === false && (q.field === "use_cases" || q.field === "reviewer_instructions")) continue;
|
|
7964
|
+
const existing = typeof have[q.field] === "string" ? String(have[q.field]) : "";
|
|
7965
|
+
if (existing.length >= (q.min ?? 1)) continue;
|
|
7966
|
+
const a = await ask3([{ type: "input", name: "value", message: q.message, default: existing || void 0, validate: (v) => v.trim().length >= (q.min ?? 1) ? true : `${q.min ?? 1}+ characters` }]);
|
|
7967
|
+
answers[q.field] = a.value.trim();
|
|
7968
|
+
await saveNow({ directory_portal_answers: { [q.field]: a.value.trim() } });
|
|
7969
|
+
}
|
|
7970
|
+
if (answers.access === void 0 && !have.access) {
|
|
7971
|
+
const a = await ask3([{ type: "list", name: "value", message: "What the tools do:", choices: ["read", "write", "both"] }]);
|
|
7972
|
+
answers.access = a.value;
|
|
7973
|
+
await saveNow({ directory_portal_answers: { access: a.value } });
|
|
7974
|
+
}
|
|
7975
|
+
if (answers.categories === void 0 && !(Array.isArray(have.categories) && have.categories.length)) {
|
|
7976
|
+
const a = await ask3([{ type: "input", name: "value", message: "Category suggestions (1\u20135, comma-separated; you pick the closest in the portal):", validate: (v) => v.trim() ? true : "one at least" }]);
|
|
7977
|
+
answers.categories = a.value.split(",").map((c) => c.trim()).filter(Boolean).slice(0, 5);
|
|
7978
|
+
await saveNow({ directory_portal_answers: { categories: answers.categories } });
|
|
7979
|
+
}
|
|
7980
|
+
if (!meta.data_handling && !current.target?.metadata?.dataHandling) {
|
|
7981
|
+
const a = await ask3([
|
|
7982
|
+
{ type: "list", name: "mode", message: "Whose API is behind this connector?", choices: [{ name: "own \u2014 my API, on my domain", value: "own" }, { name: "proxied \u2014 someone else's API I hold permission for", value: "proxied" }, { name: "third-party \u2014 someone else's API, no permission (cannot be listed)", value: "third-party" }] },
|
|
7983
|
+
{ type: "input", name: "statement", message: "The permission you hold (who granted what, and when; goes into portal step 7):", when: (ans) => ans.mode === "proxied", validate: (v) => v.trim().length >= 20 ? true : "a sentence, please" }
|
|
7984
|
+
]);
|
|
7985
|
+
meta.data_handling = { mode: a.mode, ...a.statement?.trim() ? { statement: a.statement.trim() } : {} };
|
|
7986
|
+
await saveNow({ directory_metadata: { data_handling: meta.data_handling } });
|
|
7987
|
+
}
|
|
7988
|
+
if (!meta.health_data && !current.target?.metadata?.healthData) {
|
|
7989
|
+
const a = await ask3([{ type: "confirm", name: "value", message: "Do the tools handle health data?", default: false }]);
|
|
7990
|
+
meta.health_data = a.value ? "yes" : "no";
|
|
7991
|
+
await saveNow({ directory_metadata: { health_data: meta.health_data } });
|
|
7992
|
+
}
|
|
7993
|
+
if (!meta.sponsored_content && !current.target?.metadata?.sponsoredContent) {
|
|
7994
|
+
const a = await ask3([{ type: "confirm", name: "value", message: "Do tool results carry sponsored content?", default: false }]);
|
|
7995
|
+
meta.sponsored_content = a.value ? "yes" : "no";
|
|
7996
|
+
await saveNow({ directory_metadata: { sponsored_content: meta.sponsored_content } });
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
const body = {};
|
|
8000
|
+
if (Object.keys(answers).length) body.directory_portal_answers = answers;
|
|
8001
|
+
if (Object.keys(meta).length) body.directory_metadata = meta;
|
|
8002
|
+
if (!Object.keys(body).length) {
|
|
8003
|
+
if (json) process.stdout.write(JSON.stringify({ project: proj2.projectId, changed: [] }) + "\n");
|
|
8004
|
+
else if (opts.derived !== false) console.log(import_chalk26.default.dim(" Nothing to record: pass flags (--use-cases, --data-handling \u2026) or run interactively."));
|
|
8005
|
+
return;
|
|
8006
|
+
}
|
|
8007
|
+
await producer(caller, { method: "PATCH", path: projectPath(proj2), body, summary: "Store the portal answers" });
|
|
8008
|
+
const r = await fetchDirectoryReport(caller, proj2, { probe: false, ...opts.platforms ? { platforms: opts.platforms } : {} });
|
|
8009
|
+
if (json) {
|
|
8010
|
+
process.stdout.write(JSON.stringify({ project: proj2.projectId, changed: Object.keys({ ...answers, ...meta }), headline: r.rendered.headline, report: r.report }) + "\n");
|
|
8011
|
+
return;
|
|
8012
|
+
}
|
|
8013
|
+
console.log(import_chalk26.default.green(` \u2713 recorded: ${Object.keys({ ...answers, ...meta }).join(", ")}`));
|
|
8014
|
+
console.log(import_chalk26.default.dim(` ${r.rendered.headline}`));
|
|
8015
|
+
}
|
|
8016
|
+
var import_chalk26, import_fs5, import_os2, import_path4, import_child_process5, DASHBOARD_BASE8, RUN_META_KEY, RUN_HEADER, projectPath, QUESTIONS;
|
|
8017
|
+
var init_connector_exercise = __esm({
|
|
8018
|
+
"src/commands/connector-exercise.ts"() {
|
|
8019
|
+
"use strict";
|
|
8020
|
+
import_chalk26 = __toESM(require("chalk"));
|
|
8021
|
+
import_fs5 = __toESM(require("fs"));
|
|
8022
|
+
import_os2 = __toESM(require("os"));
|
|
8023
|
+
import_path4 = __toESM(require("path"));
|
|
8024
|
+
import_child_process5 = require("child_process");
|
|
8025
|
+
init_package();
|
|
8026
|
+
init_caller();
|
|
8027
|
+
init_directory();
|
|
8028
|
+
init_headless_login();
|
|
8029
|
+
init_exercise();
|
|
8030
|
+
init_brand();
|
|
8031
|
+
init_portal_drafts();
|
|
8032
|
+
init_external_mcp();
|
|
8033
|
+
DASHBOARD_BASE8 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
|
|
8034
|
+
RUN_META_KEY = "com.apiblaze/run";
|
|
8035
|
+
RUN_HEADER = "apiblaze-directory-run";
|
|
8036
|
+
projectPath = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
|
|
8037
|
+
QUESTIONS = [
|
|
8038
|
+
{ key: "useCases", field: "use_cases", message: "Use cases (what a person asks it, in their words; 200+ characters):", min: 200 },
|
|
8039
|
+
{ key: "prerequisites", field: "prerequisites", message: "Prerequisites (one sentence: what a user needs before it works):", min: 1 },
|
|
8040
|
+
{ key: "companyName", field: "company_name", message: "Company name:", min: 1 },
|
|
8041
|
+
{ key: "companyWebsite", field: "company_website", message: "Company website (https):", min: 8 },
|
|
8042
|
+
{ key: "companyContact", field: "company_contact", message: "Primary contact (email or URL):", min: 3 },
|
|
8043
|
+
{ key: "reviewerInstructions", field: "reviewer_instructions", message: "Reviewer instructions (how to add it, sign in, one prompt per tool and what to expect; 400+ characters):", min: 400 }
|
|
8044
|
+
];
|
|
8045
|
+
}
|
|
8046
|
+
});
|
|
8047
|
+
|
|
8048
|
+
// src/lib/directory-docs.ts
|
|
8049
|
+
function directoryDocsPrompt(facts) {
|
|
8050
|
+
return [
|
|
8051
|
+
`Write the three documents a connector listing needs for "${facts.name}", from the FACTS below and nothing else.`,
|
|
8052
|
+
"Rules: plain English, second person, no marketing, no prices, no promo. Never invent a result: when you describe what a tool returns, cite only the recorded result shapes in the facts (keys, item counts, heads); if none is recorded, say what the tool is for and that the assistant will show the result.",
|
|
8053
|
+
"Never write credentials, usernames, tokens or internal hostnames. Troubleshooting cites the failure modes with their exact texts.",
|
|
8054
|
+
`The sign-in: ${facts.sign_in_shape}`,
|
|
8055
|
+
`Answer with JSON matching the schema: docs_markdown (the public page "Using <name> in Claude": purpose \xB7 how it works from Claude's side \xB7 connect and sign in \xB7 the example prompts with what to expect \xB7 troubleshooting \xB7 support), reviewer_markdown (for someone who has never seen the product: add the connector, the sign-in screen, the account, what data it holds, one prompt per tool with the recorded result shape), portal_answers (use_cases 200+ chars, prerequisites one sentence, access, 1\u20135 categories).`,
|
|
8056
|
+
"",
|
|
8057
|
+
"FACTS:",
|
|
8058
|
+
JSON.stringify(facts, null, 2)
|
|
8059
|
+
].join("\n");
|
|
8060
|
+
}
|
|
8061
|
+
function lintDocument(markdown, opts = {}) {
|
|
8062
|
+
const out = [];
|
|
8063
|
+
const text = markdown ?? "";
|
|
8064
|
+
if (!text.trim()) out.push({ code: "EMPTY", detail: "the document is empty" });
|
|
8065
|
+
if (text.length > DIRECTORY_DOC_MAX_CHARS) out.push({ code: "TOO_LONG", detail: `${text.length} characters; the limit is ${DIRECTORY_DOC_MAX_CHARS}` });
|
|
8066
|
+
const u = (opts.reviewerUsername ?? "").trim();
|
|
8067
|
+
if (u.length >= 3 && text.toLowerCase().includes(u.toLowerCase())) out.push({ code: "REVIEWER_USERNAME", detail: "the reviewer username appears; it belongs in the packet only" });
|
|
8068
|
+
if (BEARER_LIKE.test(text)) out.push({ code: "BEARER_LIKE", detail: "a bearer-like or JWT-shaped string appears" });
|
|
8069
|
+
if (KEY_LIKE.test(text)) out.push({ code: "KEY_LIKE", detail: "a key-like string (sk_/pk_/\u2026) appears" });
|
|
8070
|
+
if (PASSWORD_LINE.test(text)) out.push({ code: "PASSWORD_LINE", detail: "a password-like line appears" });
|
|
8071
|
+
if (opts.public && INTERNAL_HOST.test(text)) out.push({ code: "INTERNAL_HOST", detail: "an upstream or platform-internal host appears" });
|
|
8072
|
+
return out;
|
|
8073
|
+
}
|
|
8074
|
+
function parseGeneratedDocs(stdout) {
|
|
8075
|
+
const tryObj = (v) => {
|
|
8076
|
+
if (!v || typeof v !== "object") return null;
|
|
8077
|
+
const o = v;
|
|
8078
|
+
const pa = o.portal_answers;
|
|
8079
|
+
if (typeof o.docs_markdown !== "string" || typeof o.reviewer_markdown !== "string" || !pa || typeof pa !== "object") return null;
|
|
8080
|
+
return { docs_markdown: o.docs_markdown, reviewer_markdown: o.reviewer_markdown, portal_answers: { use_cases: String(pa.use_cases ?? ""), prerequisites: String(pa.prerequisites ?? ""), access: ["read", "write", "both"].includes(String(pa.access)) ? String(pa.access) : "both", categories: Array.isArray(pa.categories) ? pa.categories.map(String).slice(0, 5) : [] } };
|
|
8081
|
+
};
|
|
8082
|
+
let parsed = null;
|
|
8083
|
+
try {
|
|
8084
|
+
parsed = JSON.parse(stdout.trim());
|
|
8085
|
+
} catch {
|
|
8086
|
+
parsed = null;
|
|
8087
|
+
}
|
|
8088
|
+
if (parsed && typeof parsed === "object") {
|
|
8089
|
+
const o = parsed;
|
|
8090
|
+
const direct = tryObj(o);
|
|
8091
|
+
if (direct) return direct;
|
|
8092
|
+
const so = tryObj(o.structured_output);
|
|
8093
|
+
if (so) return so;
|
|
8094
|
+
if (typeof o.result === "string") {
|
|
8095
|
+
try {
|
|
8096
|
+
const r = tryObj(JSON.parse(o.result));
|
|
8097
|
+
if (r) return r;
|
|
7848
8098
|
} catch {
|
|
7849
8099
|
}
|
|
7850
|
-
}
|
|
7851
|
-
if (bearer) {
|
|
7852
|
-
await revokeToken(asMeta, bearer, void 0, deps).catch(() => false);
|
|
7853
8100
|
}
|
|
7854
8101
|
}
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
8102
|
+
for (const line of stdout.split("\n").reverse()) {
|
|
8103
|
+
const s = line.trim();
|
|
8104
|
+
if (!s.startsWith("{")) continue;
|
|
8105
|
+
try {
|
|
8106
|
+
const ev = JSON.parse(s);
|
|
8107
|
+
const so = tryObj(ev.structured_output);
|
|
8108
|
+
if (so) return so;
|
|
8109
|
+
if (typeof ev.result === "string") {
|
|
8110
|
+
const r = tryObj(JSON.parse(ev.result));
|
|
8111
|
+
if (r) return r;
|
|
8112
|
+
}
|
|
8113
|
+
} catch {
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
return null;
|
|
8117
|
+
}
|
|
8118
|
+
var DIRECTORY_DOCS_JSON_SCHEMA, DIRECTORY_DOC_MAX_CHARS, BEARER_LIKE, KEY_LIKE, PASSWORD_LINE, INTERNAL_HOST;
|
|
8119
|
+
var init_directory_docs = __esm({
|
|
8120
|
+
"src/lib/directory-docs.ts"() {
|
|
8121
|
+
"use strict";
|
|
8122
|
+
DIRECTORY_DOCS_JSON_SCHEMA = {
|
|
8123
|
+
type: "object",
|
|
8124
|
+
additionalProperties: false,
|
|
8125
|
+
required: ["docs_markdown", "reviewer_markdown", "portal_answers"],
|
|
8126
|
+
properties: {
|
|
8127
|
+
docs_markdown: { type: "string", description: `The public documentation page, Markdown: "Using <name> in Claude" \u2014 purpose, how it works from Claude's side, connect and sign in, the three prompts with what to expect, troubleshooting (the known failure modes with their exact texts), support.` },
|
|
8128
|
+
reviewer_markdown: { type: "string", description: "Reviewer instructions for someone who has never seen the product: add the connector, the sign-in screen, the account, what data it holds, one prompt per tool with the recorded result shape. No credentials." },
|
|
8129
|
+
portal_answers: {
|
|
8130
|
+
type: "object",
|
|
8131
|
+
additionalProperties: false,
|
|
8132
|
+
required: ["use_cases", "prerequisites", "access", "categories"],
|
|
8133
|
+
properties: {
|
|
8134
|
+
use_cases: { type: "string", description: "What a person asks it, in their words; 200+ characters." },
|
|
8135
|
+
prerequisites: { type: "string", description: "What a user needs before it works, one sentence." },
|
|
8136
|
+
access: { type: "string", enum: ["read", "write", "both"] },
|
|
8137
|
+
categories: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 5, description: "Free-text category suggestions; the producer picks the closest in the portal." }
|
|
8138
|
+
}
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
};
|
|
8142
|
+
DIRECTORY_DOC_MAX_CHARS = 64e3;
|
|
8143
|
+
BEARER_LIKE = /(?:bearer\s+[A-Za-z0-9._~+/=-]{8,}|\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b)/i;
|
|
8144
|
+
KEY_LIKE = /\b(?:sk|pk|rk|key|tok|abz)_[A-Za-z0-9_-]{8,}\b/i;
|
|
8145
|
+
PASSWORD_LINE = /^\s*(?:[-*]\s*)?(?:\*\*)?(?:password|passwd|pwd|secret)(?:\*\*)?\s*[:=]\s*\S+/im;
|
|
8146
|
+
INTERNAL_HOST = /\b[a-z0-9.-]+\.(abz\.run|tryabz\.run|workers\.dev)\b/i;
|
|
7861
8147
|
}
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
8148
|
+
});
|
|
8149
|
+
|
|
8150
|
+
// src/commands/connector-docs.ts
|
|
8151
|
+
var connector_docs_exports = {};
|
|
8152
|
+
__export(connector_docs_exports, {
|
|
8153
|
+
askDocsQuestion: () => askDocsQuestion,
|
|
8154
|
+
generateWithClaude: () => generateWithClaude,
|
|
8155
|
+
runConnectorDocs: () => runConnectorDocs,
|
|
8156
|
+
storeGeneratedDocs: () => storeGeneratedDocs,
|
|
8157
|
+
verifyDocsUrl: () => verifyDocsUrl
|
|
8158
|
+
});
|
|
8159
|
+
function generateWithClaude(prompt, deps = {}) {
|
|
8160
|
+
const spawn5 = deps.spawn ?? import_child_process6.spawnSync;
|
|
8161
|
+
const res = spawn5("claude", ["-p", prompt, "--output-format", "json", "--json-schema", JSON.stringify(DIRECTORY_DOCS_JSON_SCHEMA), "--tools", "", "--max-budget-usd", "2", "--permission-mode", "dontAsk"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 24e4, env: { ...process.env, CLAUDECODE: "" } });
|
|
8162
|
+
const docs = parseGeneratedDocs(String(res.stdout ?? ""));
|
|
8163
|
+
return { docs, detail: docs ? "generated on Claude Code" : `Claude Code answered without the three documents${res.status != null && res.status !== 0 ? ` (exit ${res.status})` : ""}` };
|
|
7866
8164
|
}
|
|
7867
|
-
function
|
|
7868
|
-
|
|
8165
|
+
async function storeGeneratedDocs(caller, proj2, docs, o) {
|
|
8166
|
+
const bad = [...lintDocument(docs.docs_markdown, { reviewerUsername: o.reviewerUsername, public: true }).map((f2) => `docs: ${f2.code}`), ...lintDocument(docs.reviewer_markdown, { reviewerUsername: o.reviewerUsername }).map((f2) => `reviewer: ${f2.code}`)];
|
|
8167
|
+
if (bad.length) throw new Error(`The generated documents were refused by the lint (${bad.join(", ")}); nothing was stored. Run again, or write them by hand on the dashboard page.`);
|
|
8168
|
+
const out = await producer(caller, { method: "PUT", path: `${projectPath2(proj2)}/directory-documents`, body: { docs_markdown: docs.docs_markdown, reviewer_markdown: docs.reviewer_markdown, portal_answers: docs.portal_answers, source: "generated", facts_hash: o.factsHash, use_generated_docs_url: o.useGeneratedDocsUrl }, summary: "Store the generated documents" });
|
|
8169
|
+
return out.changed ?? [];
|
|
7869
8170
|
}
|
|
7870
|
-
function
|
|
8171
|
+
async function verifyDocsUrl(url, serverName, f2 = fetch) {
|
|
8172
|
+
if (!/^https:\/\/\S+$/i.test(url)) return { ok: false, reason: "not an https URL" };
|
|
7871
8173
|
try {
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
8174
|
+
const r = await f2(url, { redirect: "follow", signal: AbortSignal.timeout(8e3), headers: { accept: "text/html, */*;q=0.5", "user-agent": "apiblaze-cli directory-check" } });
|
|
8175
|
+
if (r.status !== 200) return { ok: false, reason: `answered ${r.status}` };
|
|
8176
|
+
const body = (await r.text().catch(() => "")).toLowerCase();
|
|
8177
|
+
if (serverName && !body.includes(serverName.toLowerCase())) return { ok: false, reason: `the page does not mention "${serverName}"` };
|
|
8178
|
+
return { ok: true, reason: "live" };
|
|
8179
|
+
} catch (e) {
|
|
8180
|
+
return { ok: false, reason: e instanceof Error ? e.message : "unreachable" };
|
|
7875
8181
|
}
|
|
7876
8182
|
}
|
|
7877
|
-
async function
|
|
8183
|
+
async function runConnectorDocs(project, opts = {}) {
|
|
7878
8184
|
const caller = await requireCaller();
|
|
7879
8185
|
const { teamId } = await resolveActingTeam(caller, opts.team);
|
|
7880
8186
|
const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
|
|
7881
8187
|
const json = !!opts.json;
|
|
7882
|
-
const
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
const
|
|
7886
|
-
const
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
if (opts.
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
if (sponsored) meta.sponsored_content = sponsored;
|
|
7899
|
-
if (interactive2) {
|
|
7900
|
-
const current = await producer(caller, { method: "POST", path: `${projectPath2(proj2)}/directory-report`, body: { probe: false }, summary: "Current answers" });
|
|
7901
|
-
const have = current.target?.metadata?.portalAnswers ?? {};
|
|
7902
|
-
const { default: inquirer3 } = await import("inquirer");
|
|
7903
|
-
const saveNow = async (part) => {
|
|
7904
|
-
try {
|
|
7905
|
-
await producer(caller, { method: "PATCH", path: projectPath2(proj2), body: part, summary: "Save an answer" });
|
|
7906
|
-
} catch (e) {
|
|
7907
|
-
console.log(import_chalk27.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
|
|
7908
|
-
}
|
|
7909
|
-
};
|
|
7910
|
-
for (const q of QUESTIONS) {
|
|
7911
|
-
if (answers[q.field] !== void 0) continue;
|
|
7912
|
-
const existing = typeof have[q.field] === "string" ? String(have[q.field]) : "";
|
|
7913
|
-
if (existing.length >= (q.min ?? 1)) continue;
|
|
7914
|
-
const a = await inquirer3.prompt([{ type: "input", name: "value", message: q.message, default: existing || void 0, validate: (v) => v.trim().length >= (q.min ?? 1) ? true : `${q.min ?? 1}+ characters` }]);
|
|
7915
|
-
answers[q.field] = a.value.trim();
|
|
7916
|
-
await saveNow({ directory_portal_answers: { [q.field]: a.value.trim() } });
|
|
7917
|
-
}
|
|
7918
|
-
if (answers.access === void 0 && !have.access) {
|
|
7919
|
-
const a = await inquirer3.prompt([{ type: "list", name: "value", message: "What the tools do:", choices: ["read", "write", "both"] }]);
|
|
7920
|
-
answers.access = a.value;
|
|
7921
|
-
await saveNow({ directory_portal_answers: { access: a.value } });
|
|
7922
|
-
}
|
|
7923
|
-
if (answers.categories === void 0 && !(Array.isArray(have.categories) && have.categories.length)) {
|
|
7924
|
-
const a = await inquirer3.prompt([{ type: "input", name: "value", message: "Category suggestions (1\u20135, comma-separated; you pick the closest in the portal):", validate: (v) => v.trim() ? true : "one at least" }]);
|
|
7925
|
-
answers.categories = a.value.split(",").map((c) => c.trim()).filter(Boolean).slice(0, 5);
|
|
7926
|
-
await saveNow({ directory_portal_answers: { categories: answers.categories } });
|
|
7927
|
-
}
|
|
7928
|
-
if (!meta.data_handling && !current.target?.metadata?.dataHandling) {
|
|
7929
|
-
const a = await inquirer3.prompt([
|
|
7930
|
-
{ type: "list", name: "mode", message: "Whose API is behind this connector?", choices: [{ name: "own \u2014 my API, on my domain", value: "own" }, { name: "proxied \u2014 someone else's API I hold permission for", value: "proxied" }, { name: "third-party \u2014 someone else's API, no permission (cannot be listed)", value: "third-party" }] },
|
|
7931
|
-
{ type: "input", name: "statement", message: "The permission you hold (who granted what, and when; goes into portal step 7):", when: (ans) => ans.mode === "proxied", validate: (v) => v.trim().length >= 20 ? true : "a sentence, please" }
|
|
7932
|
-
]);
|
|
7933
|
-
meta.data_handling = { mode: a.mode, ...a.statement?.trim() ? { statement: a.statement.trim() } : {} };
|
|
7934
|
-
await saveNow({ directory_metadata: { data_handling: meta.data_handling } });
|
|
7935
|
-
}
|
|
7936
|
-
if (!meta.health_data && !current.target?.metadata?.healthData) {
|
|
7937
|
-
const a = await inquirer3.prompt([{ type: "confirm", name: "value", message: "Do the tools handle health data?", default: false }]);
|
|
7938
|
-
meta.health_data = a.value ? "yes" : "no";
|
|
7939
|
-
await saveNow({ directory_metadata: { health_data: meta.health_data } });
|
|
7940
|
-
}
|
|
7941
|
-
if (!meta.sponsored_content && !current.target?.metadata?.sponsoredContent) {
|
|
7942
|
-
const a = await inquirer3.prompt([{ type: "confirm", name: "value", message: "Do tool results carry sponsored content?", default: false }]);
|
|
7943
|
-
meta.sponsored_content = a.value ? "yes" : "no";
|
|
7944
|
-
await saveNow({ directory_metadata: { sponsored_content: meta.sponsored_content } });
|
|
7945
|
-
}
|
|
7946
|
-
}
|
|
7947
|
-
const body = {};
|
|
7948
|
-
if (Object.keys(answers).length) body.directory_portal_answers = answers;
|
|
7949
|
-
if (Object.keys(meta).length) body.directory_metadata = meta;
|
|
7950
|
-
if (!Object.keys(body).length) {
|
|
7951
|
-
if (json) process.stdout.write(JSON.stringify({ project: proj2.projectId, changed: [] }) + "\n");
|
|
7952
|
-
else console.log(import_chalk27.default.dim(" Nothing to record: pass flags (--use-cases, --data-handling \u2026) or run interactively."));
|
|
8188
|
+
const say = (s) => {
|
|
8189
|
+
if (!json) console.log(s);
|
|
8190
|
+
};
|
|
8191
|
+
const deps = opts.deps ?? {};
|
|
8192
|
+
const fr = await producer(caller, { method: "GET", path: `${projectPath2(proj2)}/directory-facts?view=packet`, summary: "The facts the generator reads" });
|
|
8193
|
+
const prompt = directoryDocsPrompt(fr.facts);
|
|
8194
|
+
let docs = null;
|
|
8195
|
+
let how = "";
|
|
8196
|
+
if (opts.from) {
|
|
8197
|
+
docs = parseGeneratedDocs(import_fs6.default.readFileSync(opts.from, "utf8"));
|
|
8198
|
+
if (!docs) throw new Error(`${opts.from} does not hold the three documents (docs_markdown, reviewer_markdown, portal_answers).`);
|
|
8199
|
+
how = `from ${opts.from}`;
|
|
8200
|
+
} else if (opts.promptOut) {
|
|
8201
|
+
import_fs6.default.writeFileSync(opts.promptOut, prompt + "\n", { mode: 384 });
|
|
8202
|
+
say(import_chalk27.default.green(` \u2713 prompt written to ${opts.promptOut}`));
|
|
8203
|
+
say(import_chalk27.default.dim(` Run it through a model that answers JSON matching the schema, save the answer, then: ${B.cli} connector docs ${proj2.projectName} --from <answer.json>`));
|
|
7953
8204
|
return;
|
|
8205
|
+
} else {
|
|
8206
|
+
const installed3 = deps.claudeInstalled ?? detectExternalClis().some((c) => c.kind === "claude");
|
|
8207
|
+
if (!installed3) {
|
|
8208
|
+
const file2 = import_path5.default.join(import_os3.default.tmpdir(), `apiblaze-connector-docs-${proj2.projectId}.txt`);
|
|
8209
|
+
import_fs6.default.writeFileSync(file2, prompt + "\n", { mode: 384 });
|
|
8210
|
+
throw new Error(`Claude Code is not installed here, so the documents were not generated. The prompt is at ${file2}: run it through any model that answers JSON, then \`${B.cli} connector docs ${proj2.projectName} --from <answer.json>\`; or write them on the dashboard page.`);
|
|
8211
|
+
}
|
|
8212
|
+
say(import_chalk27.default.dim(" generating the three documents on Claude Code (30\u201390 s)\u2026"));
|
|
8213
|
+
const g = generateWithClaude(prompt, deps);
|
|
8214
|
+
if (!g.docs) throw new Error(`${g.detail}. Run again, or \`${B.cli} connector docs ${proj2.projectName} --prompt-out prompt.txt\` to use another model.`);
|
|
8215
|
+
docs = g.docs;
|
|
8216
|
+
how = g.detail;
|
|
7954
8217
|
}
|
|
7955
|
-
await
|
|
7956
|
-
const r = await fetchDirectoryReport(caller, proj2, { probe: false });
|
|
8218
|
+
const changed = await storeGeneratedDocs(caller, proj2, docs, { factsHash: fr.facts_hash, reviewerUsername: fr.facts.reviewer_username ?? null, useGeneratedDocsUrl: true });
|
|
7957
8219
|
if (json) {
|
|
7958
|
-
process.stdout.write(JSON.stringify({ project: proj2.projectId, changed
|
|
8220
|
+
process.stdout.write(JSON.stringify({ project: proj2.projectId, changed, docs_url: fr.generated_docs_url, how }) + "\n");
|
|
7959
8221
|
return;
|
|
7960
8222
|
}
|
|
7961
|
-
console.log(import_chalk27.default.green(` \u2713
|
|
7962
|
-
console.log(import_chalk27.default.
|
|
8223
|
+
console.log(import_chalk27.default.green(` \u2713 documents stored (${how}): ${changed.join(", ")}`));
|
|
8224
|
+
console.log(` Documentation page: ${import_chalk27.default.cyan(fr.generated_docs_url)} \u2014 link it from your site; edit it on the dashboard any time.`);
|
|
7963
8225
|
}
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
"
|
|
8226
|
+
async function askDocsQuestion(caller, proj2, state, interactive2, say, deps = {}) {
|
|
8227
|
+
if (!interactive2) return false;
|
|
8228
|
+
const { default: inquirer3 } = await import("inquirer");
|
|
8229
|
+
const a = await inquirer3.prompt([{ type: "input", name: "value", message: `Documentation for this connector on your site? Paste the URL, or N to use the generated page at ${state.generatedDocsUrl} (and link it from your site):`, default: state.docsUrl ?? "N" }]);
|
|
8230
|
+
const v = a.value.trim();
|
|
8231
|
+
if (!v || /^n(o)?$/i.test(v)) {
|
|
8232
|
+
say(import_chalk27.default.dim(" using the generated page"));
|
|
8233
|
+
try {
|
|
8234
|
+
await runConnectorDocs(proj2.projectName, { deps, yes: true });
|
|
8235
|
+
} catch (e) {
|
|
8236
|
+
say(import_chalk27.default.yellow(` ${e instanceof Error ? e.message : String(e)}`));
|
|
8237
|
+
await producer(caller, { method: "PUT", path: `${projectPath2(proj2)}/directory-documents`, body: { use_generated_docs_url: true }, summary: "Point docs at the generated page" });
|
|
8238
|
+
}
|
|
8239
|
+
return true;
|
|
8240
|
+
}
|
|
8241
|
+
const check = await verifyDocsUrl(v, state.serverName, deps.fetch);
|
|
8242
|
+
if (!check.ok) {
|
|
8243
|
+
say(import_chalk27.default.yellow(` ${v} is not usable as documentation yet: ${check.reason}. Not stored.`));
|
|
8244
|
+
return false;
|
|
8245
|
+
}
|
|
8246
|
+
await producer(caller, { method: "PATCH", path: projectPath2(proj2), body: { directory_metadata: { docs_url: v, docs_confirmed: true } }, summary: "Store the documentation URL" });
|
|
8247
|
+
say(import_chalk27.default.green(` \u2713 docs: ${v} (live, names ${state.serverName ?? "the server"})`));
|
|
8248
|
+
return true;
|
|
8249
|
+
}
|
|
8250
|
+
var import_chalk27, import_fs6, import_os3, import_path5, import_child_process6, projectPath2;
|
|
8251
|
+
var init_connector_docs = __esm({
|
|
8252
|
+
"src/commands/connector-docs.ts"() {
|
|
7967
8253
|
"use strict";
|
|
7968
8254
|
import_chalk27 = __toESM(require("chalk"));
|
|
7969
8255
|
import_fs6 = __toESM(require("fs"));
|
|
7970
8256
|
import_os3 = __toESM(require("os"));
|
|
7971
8257
|
import_path5 = __toESM(require("path"));
|
|
7972
8258
|
import_child_process6 = require("child_process");
|
|
7973
|
-
init_package();
|
|
7974
8259
|
init_caller();
|
|
7975
|
-
|
|
7976
|
-
init_headless_login();
|
|
7977
|
-
init_exercise();
|
|
7978
|
-
init_brand();
|
|
8260
|
+
init_directory_docs();
|
|
7979
8261
|
init_external_mcp();
|
|
7980
|
-
|
|
7981
|
-
RUN_META_KEY = "com.apiblaze/run";
|
|
7982
|
-
RUN_HEADER = "apiblaze-directory-run";
|
|
8262
|
+
init_brand();
|
|
7983
8263
|
projectPath2 = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
|
|
7984
|
-
QUESTIONS = [
|
|
7985
|
-
{ key: "useCases", field: "use_cases", message: "Use cases (what a person asks it, in their words; 200+ characters):", min: 200 },
|
|
7986
|
-
{ key: "prerequisites", field: "prerequisites", message: "Prerequisites (one sentence: what a user needs before it works):", min: 1 },
|
|
7987
|
-
{ key: "companyName", field: "company_name", message: "Company name:", min: 1 },
|
|
7988
|
-
{ key: "companyWebsite", field: "company_website", message: "Company website (https):", min: 8 },
|
|
7989
|
-
{ key: "companyContact", field: "company_contact", message: "Primary contact (email or URL):", min: 3 },
|
|
7990
|
-
{ key: "reviewerInstructions", field: "reviewer_instructions", message: "Reviewer instructions (how to add it, sign in, one prompt per tool and what to expect; 400+ characters):", min: 400 }
|
|
7991
|
-
];
|
|
7992
8264
|
}
|
|
7993
8265
|
});
|
|
7994
8266
|
|
|
@@ -8172,10 +8444,10 @@ function derivedBehavior(method) {
|
|
|
8172
8444
|
}
|
|
8173
8445
|
function effectiveBehavior(method, o) {
|
|
8174
8446
|
const d = derivedBehavior(method);
|
|
8175
|
-
const
|
|
8176
|
-
const destructive =
|
|
8447
|
+
const readOnly2 = o?.readOnly ?? d.readOnly;
|
|
8448
|
+
const destructive = readOnly2 ? false : o?.destructive ?? d.destructive;
|
|
8177
8449
|
return {
|
|
8178
|
-
readOnly,
|
|
8450
|
+
readOnly: readOnly2,
|
|
8179
8451
|
destructive,
|
|
8180
8452
|
idempotent: o?.idempotent ?? d.idempotent,
|
|
8181
8453
|
confirm: o?.confirm ?? destructive,
|
|
@@ -11835,7 +12107,7 @@ function shortenTo(text, limit) {
|
|
|
11835
12107
|
const base2 = atWord >= Math.floor(limit * 0.4) ? cut.slice(0, atWord) : cut;
|
|
11836
12108
|
return base2.replace(/[\s,;:(\-–—]+$/g, "").trim();
|
|
11837
12109
|
}
|
|
11838
|
-
async function promptForMissing(r, interactive2, defaults = { prompts: [] }, platforms = PLATFORMS, given = {}, save) {
|
|
12110
|
+
async function promptForMissing(r, interactive2, defaults = { prompts: [] }, platforms = PLATFORMS, given = {}, save, testPrompt) {
|
|
11839
12111
|
const out = {};
|
|
11840
12112
|
const failing = r.report.results.filter((x) => !x.pass && !x.unverified);
|
|
11841
12113
|
const answered = (k) => {
|
|
@@ -11896,7 +12168,20 @@ async function promptForMissing(r, interactive2, defaults = { prompts: [] }, pla
|
|
|
11896
12168
|
if (i >= 3) break;
|
|
11897
12169
|
continue;
|
|
11898
12170
|
}
|
|
11899
|
-
|
|
12171
|
+
let text = line.trim();
|
|
12172
|
+
if (testPrompt) {
|
|
12173
|
+
let res = await testPrompt(text);
|
|
12174
|
+
if (!res.called) {
|
|
12175
|
+
say_(import_chalk30.default.yellow(` \u2013 Claude called no tool for "${text}". A prompt should say what the tool does (a real URL, name or id helps).`));
|
|
12176
|
+
const again = await inquirer3.prompt([{ type: "input", name: "line", message: "Reword it (Enter keeps it):", default: text, validate: withinLimit(a) }]);
|
|
12177
|
+
if (again.line?.trim() && again.line.trim() !== text) {
|
|
12178
|
+
text = again.line.trim();
|
|
12179
|
+
res = await testPrompt(text);
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
say_(res.called ? import_chalk30.default.green(` \u2713 Claude called ${res.tools.join(", ")}`) : import_chalk30.default.yellow(" \u2013 still no tool call; kept (the report will say so)"));
|
|
12183
|
+
}
|
|
12184
|
+
lines.push(text);
|
|
11900
12185
|
}
|
|
11901
12186
|
if (lines.length) {
|
|
11902
12187
|
out.prompt = lines;
|
|
@@ -12214,7 +12499,6 @@ async function runConnector(target2, opts) {
|
|
|
12214
12499
|
throw e;
|
|
12215
12500
|
});
|
|
12216
12501
|
for (const f2 of fixes.applied ?? []) say(import_chalk30.default.green(` \u2713 fixed: ${f2}`));
|
|
12217
|
-
for (const s of fixes.skipped ?? []) say(import_chalk30.default.dim(` \u2013 ${s.fix}: ${s.reason}`));
|
|
12218
12502
|
if (fixes.applied?.length) r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12219
12503
|
const domainWasBlocking = r.report.results.some((x) => x.id === DOMAIN_RULE && !x.pass);
|
|
12220
12504
|
const domain2 = await ensureMcpDomain(caller, proj2, r, opts, defaults, interactive2, say);
|
|
@@ -12224,6 +12508,15 @@ async function runConnector(target2, opts) {
|
|
|
12224
12508
|
if (changed) r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12225
12509
|
}
|
|
12226
12510
|
if (await askOpenMode(caller, proj2, r, opts, interactive2, say)) r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12511
|
+
let reviewer = null;
|
|
12512
|
+
if (r.report.results.some((x) => x.id === "TEST_ACCOUNT_SET" && !x.pass)) {
|
|
12513
|
+
reviewer = await ensureReviewerAccount(caller, teamId, proj2);
|
|
12514
|
+
if (!json) {
|
|
12515
|
+
console.log();
|
|
12516
|
+
printReviewer(reviewer, proj2);
|
|
12517
|
+
}
|
|
12518
|
+
r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12519
|
+
}
|
|
12227
12520
|
const saveAnswer = async (f2) => {
|
|
12228
12521
|
try {
|
|
12229
12522
|
await patchProject(caller, proj2, { ...directoryBodyFromFlags(f2) }, "Save an answer");
|
|
@@ -12233,14 +12526,28 @@ async function runConnector(target2, opts) {
|
|
|
12233
12526
|
}
|
|
12234
12527
|
};
|
|
12235
12528
|
const basics = await promptListingBasics(r, interactive2, defaults, opts, platforms, interactive2 ? saveAnswer : void 0);
|
|
12529
|
+
let tester = null;
|
|
12530
|
+
let testerTried = false;
|
|
12531
|
+
const testPrompt = async (p) => {
|
|
12532
|
+
if (!tester && !testerTried) {
|
|
12533
|
+
testerTried = true;
|
|
12534
|
+
const { createPromptTester: createPromptTester2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
|
|
12535
|
+
tester = await createPromptTester2(caller, proj2, { say });
|
|
12536
|
+
}
|
|
12537
|
+
if (!tester) return { called: true, tools: [] };
|
|
12538
|
+
say(import_chalk30.default.dim(" trying it through Claude Code\u2026"));
|
|
12539
|
+
return tester.test(p);
|
|
12540
|
+
};
|
|
12236
12541
|
if (!isReady(r, platforms) || Object.keys(basics).length) {
|
|
12237
|
-
const asked = { ...basics, ...isReady(r, platforms) ? {} : await promptForMissing(r, interactive2, defaults, platforms, basics, interactive2 ? saveAnswer : void 0) };
|
|
12542
|
+
const asked = { ...basics, ...isReady(r, platforms) ? {} : await promptForMissing(r, interactive2, defaults, platforms, basics, interactive2 ? saveAnswer : void 0, interactive2 && opts.exercise !== false ? testPrompt : void 0) };
|
|
12238
12543
|
const body = directoryBodyFromFlags(asked);
|
|
12239
12544
|
if (body.directory_metadata || body.directory_attestations || body.openai_challenge_token) {
|
|
12240
12545
|
await patchProject(caller, proj2, { ...body }, "Save listing details");
|
|
12241
12546
|
r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12242
12547
|
}
|
|
12243
12548
|
}
|
|
12549
|
+
const provenPromptNotes = tester ? tester.notes() : [];
|
|
12550
|
+
if (tester) await tester.close();
|
|
12244
12551
|
const DOCS_RULES = ["DOCS_URL", "DOCS_PAGE_LIVE", "DOCS_IS_PROSE"];
|
|
12245
12552
|
if (interactive2 && r.report.results.some((x) => DOCS_RULES.includes(x.id) && !x.pass)) {
|
|
12246
12553
|
const { askDocsQuestion: askDocsQuestion2 } = await Promise.resolve().then(() => (init_connector_docs(), connector_docs_exports));
|
|
@@ -12248,43 +12555,41 @@ async function runConnector(target2, opts) {
|
|
|
12248
12555
|
const generated = `https://${proj2.tenant ?? teamId}.portal.apiblaze.com/connector`;
|
|
12249
12556
|
if (await askDocsQuestion2(caller, proj2, { docsUrl: meta.docsUrl ?? null, serverName: meta.serverName ?? null, generatedDocsUrl: generated }, interactive2, say)) r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12250
12557
|
}
|
|
12251
|
-
let reviewer = null;
|
|
12252
|
-
if (r.report.results.some((x) => x.id === "TEST_ACCOUNT_SET" && !x.pass)) {
|
|
12253
|
-
reviewer = await ensureReviewerAccount(caller, teamId, proj2);
|
|
12254
|
-
if (!json) {
|
|
12255
|
-
console.log();
|
|
12256
|
-
printReviewer(reviewer, proj2);
|
|
12257
|
-
}
|
|
12258
|
-
r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12259
|
-
}
|
|
12260
12558
|
if (interactive2 && r.report.results.some((x) => ANSWER_RULES.includes(x.id) && !x.pass)) {
|
|
12261
12559
|
const { runConnectorAnswers: runConnectorAnswers2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
|
|
12262
|
-
|
|
12263
|
-
await runConnectorAnswers2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion });
|
|
12560
|
+
await runConnectorAnswers2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, platforms: [...platforms], derived: false });
|
|
12264
12561
|
r = await fetchDirectoryReport(caller, proj2, { platforms });
|
|
12265
12562
|
}
|
|
12266
|
-
if (!readyExcept(r, platforms, [SNAPSHOT_RULE, ...EVIDENCE_RULES])) {
|
|
12267
|
-
if (!domain2.active && domain2.hostname && !json) {
|
|
12268
|
-
console.error(import_chalk30.default.yellow(`
|
|
12269
|
-
${domain2.hostname} is not active yet. Once the CNAME is in place, run again: ${B.cli} connector ${proj2.projectName}`));
|
|
12270
|
-
}
|
|
12271
|
-
exitWithReport(r, proj2, platforms, json);
|
|
12272
|
-
}
|
|
12273
12563
|
if (opts.exercise !== false) {
|
|
12274
12564
|
let run2 = !!opts.yes;
|
|
12275
12565
|
if (interactive2) {
|
|
12276
12566
|
const { default: inquirer3 } = await import("inquirer");
|
|
12277
|
-
run2 = (await inquirer3.prompt([{ type: "confirm", name: "run", message:
|
|
12567
|
+
run2 = (await inquirer3.prompt([{ type: "confirm", name: "run", message: `Run every tool now, ${isOpenMode(r) ? "the way a reviewer would" : "as the reviewer"}, so the report can prove it works?`, default: true }])).run;
|
|
12278
12568
|
}
|
|
12279
12569
|
if (run2) {
|
|
12280
12570
|
const { runConnectorExercise: runConnectorExercise2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
|
|
12281
|
-
await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, yes: opts.yes, noEmail: opts.email === false });
|
|
12571
|
+
const ran = await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, yes: opts.yes, noEmail: opts.email === false, report: false, platforms: [...platforms], provenPromptNotes });
|
|
12572
|
+
if (interactive2 && ran.failedPrompts.length) {
|
|
12573
|
+
const { rewritten, prompts } = await rewriteFailedPrompts(r, ran.failedPrompts, saveAnswer, say);
|
|
12574
|
+
if (rewritten.length) {
|
|
12575
|
+
say(import_chalk30.default.dim(` Running the ${prompts.length} prompts through Claude Code again (the report reads the last run)\u2026`));
|
|
12576
|
+
await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, noEmail: opts.email === false, report: false, platforms: [...platforms], promptsOnly: prompts });
|
|
12577
|
+
}
|
|
12578
|
+
}
|
|
12579
|
+
if (interactive2 || opts.yes) await offerDrafts(caller, proj2, r, ran, interactive2, say);
|
|
12282
12580
|
r = await fetchDirectoryReport(caller, proj2, { platforms, publishedUrl: r.report.project.publishedUrl ?? null });
|
|
12283
12581
|
}
|
|
12284
12582
|
}
|
|
12285
|
-
if (!readyExceptSnapshot(r, platforms))
|
|
12583
|
+
if (!readyExceptSnapshot(r, platforms)) {
|
|
12584
|
+
if (!domain2.active && domain2.hostname && !json) {
|
|
12585
|
+
console.error(import_chalk30.default.yellow(`
|
|
12586
|
+
${domain2.hostname} is not active yet. Once the CNAME is in place, run again: ${B.cli} connector ${proj2.projectId}`));
|
|
12587
|
+
}
|
|
12588
|
+
exitWithReport(r, proj2, platforms, json, interactive2);
|
|
12589
|
+
}
|
|
12286
12590
|
r = await fetchDirectoryReport(caller, proj2, { platforms, refreshSnapshot: true, publishedUrl: r.report.project.publishedUrl ?? null });
|
|
12287
|
-
const
|
|
12591
|
+
const probes = await packetProbes(r);
|
|
12592
|
+
const packet = await producer(caller, { method: "POST", path: `${projectPath3(proj2)}/directory-packet`, body: { platforms, ...probes ? { probes } : {} }, summary: "Submission packet" });
|
|
12288
12593
|
const dir = opts.out ?? process.cwd();
|
|
12289
12594
|
const mdPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.md`);
|
|
12290
12595
|
const jsonPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.json`);
|
|
@@ -12304,13 +12609,93 @@ async function runConnector(target2, opts) {
|
|
|
12304
12609
|
console.log(import_chalk30.default.green.bold(` ${r.rendered.headline}`));
|
|
12305
12610
|
console.log(` Submission packet: ${import_chalk30.default.cyan(mdPath)} and ${import_chalk30.default.cyan(jsonPath)}`);
|
|
12306
12611
|
console.log(import_chalk30.default.dim(isOpenMode(r) ? " (open mode: no reviewer credentials in the packet; the connector takes no sign-in)" : " (the packet carries the reviewer credentials \u2014 keep it out of git)"));
|
|
12307
|
-
printSubmitSteps(r, mdPath);
|
|
12612
|
+
printSubmitSteps(r, mdPath, platforms);
|
|
12308
12613
|
console.log(import_chalk30.default.dim(` ${r.report.disclaimer}`));
|
|
12309
12614
|
}
|
|
12615
|
+
async function rewriteFailedPrompts(r, failed, save, say) {
|
|
12616
|
+
const current = currentListingValue(r, "prompt");
|
|
12617
|
+
const list = Array.isArray(current) ? [...current] : [];
|
|
12618
|
+
const { default: inquirer3 } = await import("inquirer");
|
|
12619
|
+
const rewritten = [];
|
|
12620
|
+
say(import_chalk30.default.yellow(` ${failed.length === 1 ? "This prompt" : `${failed.length} prompts`} made Claude call no tool; a prompt should name what the tool does${failed.length === 1 ? "" : ", one at a time"}:`));
|
|
12621
|
+
for (const old of failed) {
|
|
12622
|
+
const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: `Rewrite "${old}" (Enter keeps it):`, default: old, validate: (v) => v.trim().length <= 300 ? true : `${v.trim().length} characters; 300 at most` }]);
|
|
12623
|
+
const next = (value ?? "").trim();
|
|
12624
|
+
if (!next || next === old) continue;
|
|
12625
|
+
const at = list.indexOf(old);
|
|
12626
|
+
if (at >= 0) list[at] = next;
|
|
12627
|
+
else list.push(next);
|
|
12628
|
+
rewritten.push(next);
|
|
12629
|
+
}
|
|
12630
|
+
if (rewritten.length) {
|
|
12631
|
+
const why = await save({ prompt: list });
|
|
12632
|
+
if (why) {
|
|
12633
|
+
say_(import_chalk30.default.yellow(` not saved: ${why}`));
|
|
12634
|
+
return { rewritten: [], prompts: list };
|
|
12635
|
+
}
|
|
12636
|
+
}
|
|
12637
|
+
return { rewritten, prompts: list };
|
|
12638
|
+
}
|
|
12639
|
+
async function packetProbes(r) {
|
|
12640
|
+
const publishedUrl = r.report.project.publishedUrl;
|
|
12641
|
+
if (!publishedUrl) return null;
|
|
12642
|
+
const meta = r.target?.metadata ?? {};
|
|
12643
|
+
try {
|
|
12644
|
+
const { probeDirectoryTarget: probeDirectoryTarget2 } = await Promise.resolve().then(() => (init_directory_probes(), directory_probes_exports));
|
|
12645
|
+
return await probeDirectoryTarget2({ publishedUrl, privacyUrl: meta.privacyUrl ?? null, docsUrl: meta.docsUrl ?? null, serverName: meta.serverName ?? null });
|
|
12646
|
+
} catch {
|
|
12647
|
+
return null;
|
|
12648
|
+
}
|
|
12649
|
+
}
|
|
12650
|
+
async function offerDrafts(caller, proj2, r, ran, interactive2, say) {
|
|
12651
|
+
if (!ran || !Array.isArray(ran.rows) || !ran.publishedUrl) return false;
|
|
12652
|
+
const { draftMissing: draftMissing2, ANSWER_MIN: ANSWER_MIN2 } = await Promise.resolve().then(() => (init_portal_drafts(), portal_drafts_exports));
|
|
12653
|
+
const meta = r.target?.metadata ?? {};
|
|
12654
|
+
const prompts = (meta.examplePrompts ?? []).filter((p) => typeof p === "string" && !!p.trim());
|
|
12655
|
+
const drafts = draftMissing2({
|
|
12656
|
+
publishedUrl: ran.publishedUrl,
|
|
12657
|
+
open: ran.open,
|
|
12658
|
+
reviewerUsername: ran.reviewerUsername,
|
|
12659
|
+
tools: ran.tools,
|
|
12660
|
+
rows: ran.rows,
|
|
12661
|
+
prompts,
|
|
12662
|
+
promptTools: ran.promptTools,
|
|
12663
|
+
serverName: meta.serverName,
|
|
12664
|
+
tagline: meta.tagline,
|
|
12665
|
+
description: meta.description
|
|
12666
|
+
}, { use_cases: meta.portalAnswers?.use_cases, reviewer_instructions: meta.portalAnswers?.reviewer_instructions, description: meta.description });
|
|
12667
|
+
if (!drafts.length) return false;
|
|
12668
|
+
const inquirer3 = interactive2 ? (await import("inquirer")).default : null;
|
|
12669
|
+
let saved = false;
|
|
12670
|
+
say("");
|
|
12671
|
+
say(import_chalk30.default.bold(` Written from the run \u2014 the portal's ${drafts.map((d) => d.label.toLowerCase()).join(", ")}${drafts.length > 1 ? "" : ""}:`));
|
|
12672
|
+
for (const d of drafts) {
|
|
12673
|
+
let text = d.draft;
|
|
12674
|
+
say("");
|
|
12675
|
+
say(import_chalk30.default.bold(` ${d.label}`) + import_chalk30.default.dim(` (${d.draft.length} characters; the portal wants ${ANSWER_MIN2[d.key]}+)`));
|
|
12676
|
+
for (const line of d.draft.split("\n")) say(` ${line}`);
|
|
12677
|
+
if (inquirer3) {
|
|
12678
|
+
const { ok } = await inquirer3.prompt([{ type: "confirm", name: "ok", message: `Use this ${d.label.toLowerCase()}? (n = type your own)`, default: true }]);
|
|
12679
|
+
if (!ok) {
|
|
12680
|
+
const { value } = await inquirer3.prompt([{ type: "input", name: "value", message: `${d.label}:`, ...d.current ? { default: d.current } : {}, validate: (v) => v.trim().length >= ANSWER_MIN2[d.key] ? true : `${v.trim().length}/${ANSWER_MIN2[d.key]} characters` }]);
|
|
12681
|
+
text = value.trim();
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
const body = d.key === "description" ? { directory_metadata: { description: text } } : { directory_portal_answers: { [d.key]: text } };
|
|
12685
|
+
try {
|
|
12686
|
+
await patchProject(caller, proj2, body, `Save the ${d.label.toLowerCase()}`);
|
|
12687
|
+
saved = true;
|
|
12688
|
+
say(import_chalk30.default.green(` \u2713 ${d.label.toLowerCase()} saved`));
|
|
12689
|
+
} catch (e) {
|
|
12690
|
+
say(import_chalk30.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
|
|
12691
|
+
}
|
|
12692
|
+
}
|
|
12693
|
+
return saved;
|
|
12694
|
+
}
|
|
12310
12695
|
var SNAPSHOT_RULE = "LISTING_SNAPSHOT_MATCH";
|
|
12696
|
+
var withProject = (s, projectId) => s.replace(/<project>/g, projectId);
|
|
12311
12697
|
var ANSWER_RULES = ["PORTAL_ANSWERS_COMPLETE", "HEALTH_SPONSORED_DECLARED", "API_OWNERSHIP_DOMAIN"];
|
|
12312
|
-
|
|
12313
|
-
function exitWithReport(r, proj2, platforms, json) {
|
|
12698
|
+
function exitWithReport(r, proj2, platforms, json, interactive2 = false) {
|
|
12314
12699
|
if (json) {
|
|
12315
12700
|
process.stdout.write(JSON.stringify({ ready: false, project: proj2.projectId, headline: r.rendered.headline, report: r.report }) + "\n");
|
|
12316
12701
|
process.exit(1);
|
|
@@ -12318,10 +12703,13 @@ function exitWithReport(r, proj2, platforms, json) {
|
|
|
12318
12703
|
printReport(r, false);
|
|
12319
12704
|
const want = platforms;
|
|
12320
12705
|
const blocking = r.report.results.filter((x) => !x.pass && x.severity === "block" && !x.unverified && x.platforms.some((p) => want.includes(p)));
|
|
12321
|
-
const
|
|
12706
|
+
const real = blocking.filter((x) => x.id !== SNAPSHOT_RULE);
|
|
12707
|
+
const first = real.find((x) => x.tier !== "review") ?? real[0] ?? blocking[0];
|
|
12322
12708
|
console.error(import_chalk30.default.red(`
|
|
12323
12709
|
Not ready: ${blocking.length} blocker${blocking.length === 1 ? "" : "s"} (${blocking.map((x) => x.id).join(", ")}).`));
|
|
12324
|
-
|
|
12710
|
+
const name = proj2.projectId;
|
|
12711
|
+
if (real.some((x) => ANSWER_RULES.includes(x.id))) console.error(` Next: run \`${B.cli} connector ${name}\` in a terminal with the exercise on \u2014 it asks what only you can answer and writes the use cases, the reviewer instructions and the description from the run; or pass them: \`${B.cli} connector answers ${name} --use-cases \u2026 --data-handling own --health-data no --sponsored-content no\`.`);
|
|
12712
|
+
else if (first) console.error(` Next: ${withProject(first.fix ?? first.message ?? first.title, name)}`);
|
|
12325
12713
|
console.error(import_chalk30.default.dim(` Report page: ${DASHBOARD_BASE6}/directory/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}`));
|
|
12326
12714
|
process.exit(1);
|
|
12327
12715
|
}
|
|
@@ -12437,15 +12825,21 @@ async function headStatus(u) {
|
|
|
12437
12825
|
return 0;
|
|
12438
12826
|
}
|
|
12439
12827
|
}
|
|
12440
|
-
function printSubmitSteps(r, mdPath) {
|
|
12828
|
+
function printSubmitSteps(r, mdPath, platforms = PLATFORMS) {
|
|
12441
12829
|
const url = r.report.project.publishedUrl ?? "(the MCP URL in the packet)";
|
|
12442
12830
|
const open = isOpenMode(r);
|
|
12831
|
+
const claude = platforms.includes("claude");
|
|
12832
|
+
const chatgpt = platforms.includes("chatgpt");
|
|
12833
|
+
const where = [claude ? "Claude (Settings \u2192 Connectors)" : null, chatgpt ? "ChatGPT (Developer mode)" : null].filter(Boolean).join(" and in ");
|
|
12834
|
+
const steps = [
|
|
12835
|
+
`Test it yourself first: add ${import_chalk30.default.cyan(url)} as a custom connector in ${where}, ${open ? "no sign-in needed (open mode)" : "sign in with the reviewer account"}, run a tool.`,
|
|
12836
|
+
...claude ? [`Claude: ${import_chalk30.default.underline(PORTALS.claude)} \u2192 Submit new \u2192 MCP connector. Paste the listing, ${open ? "Authentication: None with the access instructions" : "the reviewer credentials"} and the test steps from ${import_chalk30.default.cyan(import_path6.default.basename(mdPath))}.`] : [],
|
|
12837
|
+
...chatgpt ? [`ChatGPT: ${import_chalk30.default.underline(PORTALS.chatgpt)} \u2192 ${claude ? "the same packet" : `the packet (${import_chalk30.default.cyan(import_path6.default.basename(mdPath))})`}, plus the 5 + 3 test cases it lists.`] : [],
|
|
12838
|
+
open ? "A nightly re-check emails you if the listing drifts." : "Keep the reviewer account alive; a nightly re-check emails you if the listing drifts."
|
|
12839
|
+
];
|
|
12443
12840
|
console.log();
|
|
12444
12841
|
console.log(import_chalk30.default.bold(" Submit (you do this part; the packet has every value to paste):"));
|
|
12445
|
-
console.log(`
|
|
12446
|
-
console.log(` 2. Claude: ${import_chalk30.default.underline(PORTALS.claude)} \u2192 Submit new \u2192 MCP connector. Paste the listing, ${open ? "Authentication: None with the access instructions" : "the reviewer credentials"} and the test steps from ${import_chalk30.default.cyan(import_path6.default.basename(mdPath))}.`);
|
|
12447
|
-
console.log(` 3. ChatGPT: ${import_chalk30.default.underline(PORTALS.chatgpt)} \u2192 the same packet, plus the 5 + 3 test cases it lists.`);
|
|
12448
|
-
console.log(` 4. Keep the reviewer account alive; a nightly re-check emails you if the listing drifts.`);
|
|
12842
|
+
steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
12449
12843
|
console.log();
|
|
12450
12844
|
}
|
|
12451
12845
|
|
|
@@ -21260,7 +21654,9 @@ var connector = program.command("connector").description(`A compliant Claude con
|
|
|
21260
21654
|
if (!project) throw new Error(`Which ${B.thingShort}? ${B.cli} connector <project> \xB7 ${B.cli} connector https://\u2026/openapi.yaml \xB7 ${B.cli} connector reviewer <project> \xB7 ${B.cli} connector exercise <project> \xB7 ${B.cli} connector answers <project>`);
|
|
21261
21655
|
await runConnector(project, opts);
|
|
21262
21656
|
}));
|
|
21263
|
-
connector.command("exercise").description("Call every tool the way a reviewer would, as the reviewer account (open mode: as you): reads with a valid and an invalid argument, writes only on ids the run created and only after a y, then the three listing prompts through Claude Code when it is installed. The gateway records the outcomes; the report reads them.").argument("<project>", `The ${B.thingShort} (project name)`).option("--empty-ok <tool>", 'A read-only tool whose correct answer is "nothing" (repeatable)', collect, []).option("--no-claude", "Skip the Claude Code pass").option("--no-email", "No email at the end of the run").option("-y, --yes", "No questions: writes are not exercised (they need a y), the rest runs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) =>
|
|
21657
|
+
connector.command("exercise").description("Call every tool the way a reviewer would, as the reviewer account (open mode: as you): reads with a valid and an invalid argument, writes only on ids the run created and only after a y, then the three listing prompts through Claude Code when it is installed. The gateway records the outcomes; the report reads them.").argument("<project>", `The ${B.thingShort} (project name)`).option("--empty-ok <tool>", 'A read-only tool whose correct answer is "nothing" (repeatable)', collect, []).option("--no-claude", "Skip the Claude Code pass").option("--no-email", "No email at the end of the run").option("-y, --yes", "No questions: writes are not exercised (they need a y), the rest runs").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action(async (project, opts) => {
|
|
21658
|
+
await runConnectorExercise(project, { ...withConnectorOpts(opts), noClaude: opts.claude === false, noEmail: opts.email === false });
|
|
21659
|
+
}));
|
|
21264
21660
|
connector.command("docs").description('Generate the three documents a listing needs \u2014 the public page "Using <name> in Claude" (served at <portal>/connector), the reviewer instructions and the portal answers \u2014 on your Claude Code from the facts of the connector; lint them, store them, point the docs URL at the page. Edit them on the dashboard any time.').argument("<project>", `The ${B.thingShort} (project name)`).option("--from <file>", "Paste back a JSON answer produced elsewhere (see --prompt-out)").option("--prompt-out <file>", "Write the prompt + facts to a file instead of running Claude Code").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runConnectorDocs(project, withConnectorOpts(opts))));
|
|
21265
21661
|
connector.command("answers").description("The portal's free-text steps 3\u20138 (use cases, prerequisites, access, categories, company, data handling, health data, sponsored content, reviewer instructions): asked when missing, or passed as flags").argument("<project>", `The ${B.thingShort} (project name)`).option("--use-cases <text>", "Use cases (200+ characters)").option("--prerequisites <text>", "What a user needs before it works (one sentence)").option("--access <read|write|both>", "What the tools do").option("--category <name>", "A category suggestion (repeatable, up to five; you pick the closest in the portal)", collect, []).option("--company-name <name>", "Company name").option("--company-website <url>", "Company website (https)").option("--company-contact <email|url>", "Primary contact").option("--reviewer-instructions <text>", "How to add it, sign in, one prompt per tool and what to expect (400+ characters)").option("--data-handling <mode>", "own \xB7 proxied (add --data-handling-statement) \xB7 third-party").option("--data-handling-statement <text>", "For proxied: the permission you hold").option("--health-data <yes|no>", "The tools handle health data").option("--sponsored-content <yes|no>", "Tool results carry sponsored content").option("-y, --yes", "No questions: record the flags only").option("--team <id|name>", "Team the project is in (defaults to your active team)").option("--apiversion <version>", "API version (defaults to the project's)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runConnectorAnswers(project, withConnectorOpts(opts))));
|
|
21266
21662
|
var withConnectorOpts = (opts) => ({ ...connector.opts(), ...opts ?? {} });
|