apiblaze 0.21.66 → 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.
Files changed (2) hide show
  1. package/dist/index.js +712 -317
  2. 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.66";
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 invalidArgs(t, known = {}) {
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, 200) : "";
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
- return JSON.stringify({ mcpServers: { abz: { type: "http", url, ...bearer ? { headers: { Authorization: `Bearer ${bearer}` } } : {} } } }, null, 2) + "\n";
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,10 +7670,10 @@ async function runConnectorExercise(project, opts = {}) {
7745
7670
  if (!json) console.log(s);
7746
7671
  };
7747
7672
  const deps = opts.deps ?? {};
7748
- say(import_chalk27.default.cyan(` Report: ${reportLink(proj2.projectId, proj2.apiVersion)}`));
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: projectPath2(proj2), body: { directory_metadata: { empty_ok: opts.emptyOk } }, summary: "Mark tools legitimately empty" });
7751
- say(import_chalk27.default.dim(` marked legitimately empty: ${opts.emptyOk.join(", ")}`));
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 ?? {};
@@ -7758,47 +7683,74 @@ async function runConnectorExercise(project, opts = {}) {
7758
7683
  if (!tools.length) throw new Error(`${proj2.projectName} exposes no tools to exercise.`);
7759
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: `${projectPath2(proj2)}/directory-runs`, body: { runner: "cli-direct" }, summary: "Open an exercise run" });
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: `${projectPath2(proj2)}/directory-packet?probe=0&force=1`, summary: "Reviewer account (for the login)" });
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
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
- say(import_chalk27.default.dim(` signed in as the reviewer (${acct.username}); the token is revoked at the end`));
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(import_chalk27.default.dim(" open mode: no sign-in; this run's calls count as yours"));
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
- for (const t of tools) {
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
- const args = validArgs(t, created);
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(` ${import_chalk27.default.yellow("\u2013")} ${t.name} not exercised (${interactive2 ? "declined" : "non-interactive: writes need a y"})`);
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
- const v = localOutcome(await call3(t.name, args));
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,43 +7760,52 @@ 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} ${import_chalk27.default.dim(v.text.slice(0, 80))}${row.invalid !== "skipped" ? import_chalk27.default.dim(` \xB7 bad arg: ${row.invalid}`) : ""}`);
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 ?? import_child_process6.spawnSync;
7823
- configDir = import_fs6.default.mkdtempSync(import_path5.default.join(deps.tmpdir ?? import_os3.default.tmpdir(), "abz-exercise-"));
7824
- import_fs6.default.chmodSync(configDir, 448);
7825
- const cfgPath = import_path5.default.join(configDir, "mcp.json");
7826
- import_fs6.default.writeFileSync(cfgPath, mcpConfigDocument(publishedUrl, bearer), { mode: 384 });
7827
- const prompts = (target2.metadata?.examplePrompts ?? []).filter((p) => typeof p === "string" && p.trim()).slice(0, 3);
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
- ...prompts.map((p) => ({ prompt: p, allowed: ["mcp__abz__*"], kind: "prompt" }))
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
- say(import_chalk27.default.dim(` Claude Code pass: ${runs.length} one-shot${runs.length > 1 ? "s" : ""} (10\u201325 s each)`));
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 res = spawn5("claude", claudeRecipe({ prompt: x.prompt, mcpConfigPath: cfgPath, allowedTools: x.allowed }), { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, env: { ...process.env, CLAUDECODE: "" } });
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 ? import_chalk27.default.green("\u2713") : import_chalk27.default.yellow("\u2013")} claude: "${x.prompt.slice(0, 60)}" \u2192 ${calls.length ? `called ${calls.join(", ")}` : "no tool call"}${res.status !== 0 && res.status != null ? import_chalk27.default.dim(` (exit ${res.status})`) : ""}`);
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(import_chalk27.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)."));
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
- import_fs6.default.rmSync(configDir, { recursive: true, force: true });
7808
+ import_fs5.default.rmSync(configDir, { recursive: true, force: true });
7848
7809
  } catch {
7849
7810
  }
7850
7811
  }
@@ -7854,18 +7815,94 @@ async function runConnectorExercise(project, opts = {}) {
7854
7815
  }
7855
7816
  r = await fetchDirectoryReport(caller, proj2, { publishedUrl, ...opts.platforms ? { platforms: opts.platforms } : {} });
7856
7817
  const verdicts = r.report.verdicts ?? null;
7857
- const closed = await producer(caller, { method: "PATCH", path: `${projectPath2(proj2)}/directory-runs/${encodeURIComponent(runId2)}`, body: { verdicts, scores: verdicts?.groups ?? null, notes, runner: "cli-direct", ...opts.noEmail ? { email: false } : {} }, summary: "Close the exercise run" });
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
+ };
7858
7830
  if (json) {
7859
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");
7860
- return;
7832
+ return summary;
7861
7833
  }
7862
- console.log();
7863
- if (opts.report !== false) printReport(r, false);
7864
- console.log(import_chalk27.default.cyan(` Report: ${reportLink(proj2.projectId, proj2.apiVersion)}`));
7865
- console.log(import_chalk27.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)"));
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
+ };
7866
7903
  }
7867
7904
  function mark(o) {
7868
- return o === "ok" ? import_chalk27.default.green("\u2713") : o === "error" ? import_chalk27.default.yellow("!") : import_chalk27.default.red("\u2717");
7905
+ return o === "ok" ? import_chalk26.default.green("\u2713") : o === "error" ? import_chalk26.default.yellow("!") : import_chalk26.default.red("\u2717");
7869
7906
  }
7870
7907
  function parseResultJson(text) {
7871
7908
  try {
@@ -7897,36 +7934,51 @@ async function runConnectorAnswers(project, opts = {}) {
7897
7934
  if (health) meta.health_data = health;
7898
7935
  if (sponsored) meta.sponsored_content = sponsored;
7899
7936
  if (interactive2) {
7900
- const current = await producer(caller, { method: "POST", path: `${projectPath2(proj2)}/directory-report`, body: { probe: false }, summary: "Current answers" });
7937
+ const current = await producer(caller, { method: "POST", path: `${projectPath(proj2)}/directory-report`, body: { probe: false }, summary: "Current answers" });
7901
7938
  const have = current.target?.metadata?.portalAnswers ?? {};
7902
7939
  const { default: inquirer3 } = await import("inquirer");
7903
7940
  const saveNow = async (part) => {
7904
7941
  try {
7905
- await producer(caller, { method: "PATCH", path: projectPath2(proj2), body: part, summary: "Save an answer" });
7942
+ await producer(caller, { method: "PATCH", path: projectPath(proj2), body: part, summary: "Save an answer" });
7906
7943
  } catch (e) {
7907
- console.log(import_chalk27.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
7944
+ console.log(import_chalk26.default.yellow(` not saved: ${e instanceof Error ? e.message : String(e)}`));
7908
7945
  }
7909
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
+ }
7910
7961
  for (const q of QUESTIONS) {
7911
7962
  if (answers[q.field] !== void 0) continue;
7963
+ if (opts.derived === false && (q.field === "use_cases" || q.field === "reviewer_instructions")) continue;
7912
7964
  const existing = typeof have[q.field] === "string" ? String(have[q.field]) : "";
7913
7965
  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` }]);
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` }]);
7915
7967
  answers[q.field] = a.value.trim();
7916
7968
  await saveNow({ directory_portal_answers: { [q.field]: a.value.trim() } });
7917
7969
  }
7918
7970
  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"] }]);
7971
+ const a = await ask3([{ type: "list", name: "value", message: "What the tools do:", choices: ["read", "write", "both"] }]);
7920
7972
  answers.access = a.value;
7921
7973
  await saveNow({ directory_portal_answers: { access: a.value } });
7922
7974
  }
7923
7975
  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" }]);
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" }]);
7925
7977
  answers.categories = a.value.split(",").map((c) => c.trim()).filter(Boolean).slice(0, 5);
7926
7978
  await saveNow({ directory_portal_answers: { categories: answers.categories } });
7927
7979
  }
7928
7980
  if (!meta.data_handling && !current.target?.metadata?.dataHandling) {
7929
- const a = await inquirer3.prompt([
7981
+ const a = await ask3([
7930
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" }] },
7931
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" }
7932
7984
  ]);
@@ -7934,12 +7986,12 @@ async function runConnectorAnswers(project, opts = {}) {
7934
7986
  await saveNow({ directory_metadata: { data_handling: meta.data_handling } });
7935
7987
  }
7936
7988
  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 }]);
7989
+ const a = await ask3([{ type: "confirm", name: "value", message: "Do the tools handle health data?", default: false }]);
7938
7990
  meta.health_data = a.value ? "yes" : "no";
7939
7991
  await saveNow({ directory_metadata: { health_data: meta.health_data } });
7940
7992
  }
7941
7993
  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 }]);
7994
+ const a = await ask3([{ type: "confirm", name: "value", message: "Do tool results carry sponsored content?", default: false }]);
7943
7995
  meta.sponsored_content = a.value ? "yes" : "no";
7944
7996
  await saveNow({ directory_metadata: { sponsored_content: meta.sponsored_content } });
7945
7997
  }
@@ -7949,38 +8001,39 @@ async function runConnectorAnswers(project, opts = {}) {
7949
8001
  if (Object.keys(meta).length) body.directory_metadata = meta;
7950
8002
  if (!Object.keys(body).length) {
7951
8003
  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."));
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."));
7953
8005
  return;
7954
8006
  }
7955
- await producer(caller, { method: "PATCH", path: projectPath2(proj2), body, summary: "Store the portal answers" });
7956
- const r = await fetchDirectoryReport(caller, proj2, { probe: false });
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 } : {} });
7957
8009
  if (json) {
7958
8010
  process.stdout.write(JSON.stringify({ project: proj2.projectId, changed: Object.keys({ ...answers, ...meta }), headline: r.rendered.headline, report: r.report }) + "\n");
7959
8011
  return;
7960
8012
  }
7961
- console.log(import_chalk27.default.green(` \u2713 recorded: ${Object.keys({ ...answers, ...meta }).join(", ")}`));
7962
- console.log(import_chalk27.default.dim(` ${r.rendered.headline}`));
8013
+ console.log(import_chalk26.default.green(` \u2713 recorded: ${Object.keys({ ...answers, ...meta }).join(", ")}`));
8014
+ console.log(import_chalk26.default.dim(` ${r.rendered.headline}`));
7963
8015
  }
7964
- var import_chalk27, import_fs6, import_os3, import_path5, import_child_process6, DASHBOARD_BASE8, RUN_META_KEY, RUN_HEADER, projectPath2, QUESTIONS;
8016
+ var import_chalk26, import_fs5, import_os2, import_path4, import_child_process5, DASHBOARD_BASE8, RUN_META_KEY, RUN_HEADER, projectPath, QUESTIONS;
7965
8017
  var init_connector_exercise = __esm({
7966
8018
  "src/commands/connector-exercise.ts"() {
7967
8019
  "use strict";
7968
- import_chalk27 = __toESM(require("chalk"));
7969
- import_fs6 = __toESM(require("fs"));
7970
- import_os3 = __toESM(require("os"));
7971
- import_path5 = __toESM(require("path"));
7972
- import_child_process6 = require("child_process");
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");
7973
8025
  init_package();
7974
8026
  init_caller();
7975
8027
  init_directory();
7976
8028
  init_headless_login();
7977
8029
  init_exercise();
7978
8030
  init_brand();
8031
+ init_portal_drafts();
7979
8032
  init_external_mcp();
7980
8033
  DASHBOARD_BASE8 = process.env.APIBLAZE_DASHBOARD_BASE || "https://dashboard.apiblaze.com";
7981
8034
  RUN_META_KEY = "com.apiblaze/run";
7982
8035
  RUN_HEADER = "apiblaze-directory-run";
7983
- projectPath2 = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
8036
+ projectPath = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
7984
8037
  QUESTIONS = [
7985
8038
  { key: "useCases", field: "use_cases", message: "Use cases (what a person asks it, in their words; 200+ characters):", min: 200 },
7986
8039
  { key: "prerequisites", field: "prerequisites", message: "Prerequisites (one sentence: what a user needs before it works):", min: 1 },
@@ -7992,6 +8045,225 @@ var init_connector_exercise = __esm({
7992
8045
  }
7993
8046
  });
7994
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;
8098
+ } catch {
8099
+ }
8100
+ }
8101
+ }
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;
8147
+ }
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})` : ""}` };
8164
+ }
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 ?? [];
8170
+ }
8171
+ async function verifyDocsUrl(url, serverName, f2 = fetch) {
8172
+ if (!/^https:\/\/\S+$/i.test(url)) return { ok: false, reason: "not an https URL" };
8173
+ try {
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" };
8181
+ }
8182
+ }
8183
+ async function runConnectorDocs(project, opts = {}) {
8184
+ const caller = await requireCaller();
8185
+ const { teamId } = await resolveActingTeam(caller, opts.team);
8186
+ const proj2 = await resolveProjectVia(caller, teamId, project, opts.apiversion);
8187
+ const json = !!opts.json;
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>`));
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;
8217
+ }
8218
+ const changed = await storeGeneratedDocs(caller, proj2, docs, { factsHash: fr.facts_hash, reviewerUsername: fr.facts.reviewer_username ?? null, useGeneratedDocsUrl: true });
8219
+ if (json) {
8220
+ process.stdout.write(JSON.stringify({ project: proj2.projectId, changed, docs_url: fr.generated_docs_url, how }) + "\n");
8221
+ return;
8222
+ }
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.`);
8225
+ }
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"() {
8253
+ "use strict";
8254
+ import_chalk27 = __toESM(require("chalk"));
8255
+ import_fs6 = __toESM(require("fs"));
8256
+ import_os3 = __toESM(require("os"));
8257
+ import_path5 = __toESM(require("path"));
8258
+ import_child_process6 = require("child_process");
8259
+ init_caller();
8260
+ init_directory_docs();
8261
+ init_external_mcp();
8262
+ init_brand();
8263
+ projectPath2 = (p) => `/projects/${encodeURIComponent(p.projectId)}/${encodeURIComponent(p.apiVersion)}`;
8264
+ }
8265
+ });
8266
+
7995
8267
  // src/lib/agent-chat.ts
7996
8268
  async function runAgentChatRepl(opts) {
7997
8269
  const { title, subtitle, endpoint, buildBody, seedPrompt, summarizeProposal, commands } = opts;
@@ -8172,10 +8444,10 @@ function derivedBehavior(method) {
8172
8444
  }
8173
8445
  function effectiveBehavior(method, o) {
8174
8446
  const d = derivedBehavior(method);
8175
- const readOnly = o?.readOnly ?? d.readOnly;
8176
- const destructive = readOnly ? false : o?.destructive ?? d.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
- lines.push(line.trim());
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,30 +12555,28 @@ 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
- say(import_chalk30.default.bold(" The review answers the portal asks for (saved as you go):"));
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
12563
  if (opts.exercise !== false) {
12267
12564
  let run2 = !!opts.yes;
12268
12565
  if (interactive2) {
12269
12566
  const { default: inquirer3 } = await import("inquirer");
12270
- run2 = (await inquirer3.prompt([{ type: "confirm", name: "run", message: "Run every tool now, as the reviewer, so the report can prove it works? (Y/n)", default: true }])).run;
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;
12271
12568
  }
12272
12569
  if (run2) {
12273
12570
  const { runConnectorExercise: runConnectorExercise2 } = await Promise.resolve().then(() => (init_connector_exercise(), connector_exercise_exports));
12274
- await runConnectorExercise2(proj2.projectName, { team: opts.team, apiversion: opts.apiversion, json, yes: opts.yes, noEmail: opts.email === false, report: false, platforms: [...platforms] });
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);
12275
12580
  r = await fetchDirectoryReport(caller, proj2, { platforms, publishedUrl: r.report.project.publishedUrl ?? null });
12276
12581
  }
12277
12582
  }
@@ -12283,7 +12588,8 @@ async function runConnector(target2, opts) {
12283
12588
  exitWithReport(r, proj2, platforms, json, interactive2);
12284
12589
  }
12285
12590
  r = await fetchDirectoryReport(caller, proj2, { platforms, refreshSnapshot: true, publishedUrl: r.report.project.publishedUrl ?? null });
12286
- const packet = await producer(caller, { method: "GET", path: `${projectPath3(proj2)}/directory-packet`, summary: "Submission packet" });
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" });
12287
12593
  const dir = opts.out ?? process.cwd();
12288
12594
  const mdPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.md`);
12289
12595
  const jsonPath = import_path6.default.join(dir, `connector-packet-${proj2.projectId}.json`);
@@ -12303,10 +12609,91 @@ async function runConnector(target2, opts) {
12303
12609
  console.log(import_chalk30.default.green.bold(` ${r.rendered.headline}`));
12304
12610
  console.log(` Submission packet: ${import_chalk30.default.cyan(mdPath)} and ${import_chalk30.default.cyan(jsonPath)}`);
12305
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)"));
12306
- printSubmitSteps(r, mdPath);
12612
+ printSubmitSteps(r, mdPath, platforms);
12307
12613
  console.log(import_chalk30.default.dim(` ${r.report.disclaimer}`));
12308
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
+ }
12309
12695
  var SNAPSHOT_RULE = "LISTING_SNAPSHOT_MATCH";
12696
+ var withProject = (s, projectId) => s.replace(/<project>/g, projectId);
12310
12697
  var ANSWER_RULES = ["PORTAL_ANSWERS_COMPLETE", "HEALTH_SPONSORED_DECLARED", "API_OWNERSHIP_DOMAIN"];
12311
12698
  function exitWithReport(r, proj2, platforms, json, interactive2 = false) {
12312
12699
  if (json) {
@@ -12321,8 +12708,8 @@ function exitWithReport(r, proj2, platforms, json, interactive2 = false) {
12321
12708
  console.error(import_chalk30.default.red(`
12322
12709
  Not ready: ${blocking.length} blocker${blocking.length === 1 ? "" : "s"} (${blocking.map((x) => x.id).join(", ")}).`));
12323
12710
  const name = proj2.projectId;
12324
- if (!interactive2 && real.some((x) => ANSWER_RULES.includes(x.id))) console.error(` Next: the review answers \u2014 run \`${B.cli} connector ${name}\` in a terminal (it asks them), or pass them: \`${B.cli} connector answers ${name} --use-cases \u2026 --data-handling own --health-data no --sponsored-content no\`.`);
12325
- else if (first) console.error(` Next: ${first.fix ?? first.message ?? first.title}`);
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)}`);
12326
12713
  console.error(import_chalk30.default.dim(` Report page: ${DASHBOARD_BASE6}/directory/${encodeURIComponent(proj2.projectId)}/${encodeURIComponent(proj2.apiVersion)}`));
12327
12714
  process.exit(1);
12328
12715
  }
@@ -12438,15 +12825,21 @@ async function headStatus(u) {
12438
12825
  return 0;
12439
12826
  }
12440
12827
  }
12441
- function printSubmitSteps(r, mdPath) {
12828
+ function printSubmitSteps(r, mdPath, platforms = PLATFORMS) {
12442
12829
  const url = r.report.project.publishedUrl ?? "(the MCP URL in the packet)";
12443
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
+ ];
12444
12840
  console.log();
12445
12841
  console.log(import_chalk30.default.bold(" Submit (you do this part; the packet has every value to paste):"));
12446
- console.log(` 1. Test it yourself first: add ${import_chalk30.default.cyan(url)} as a custom connector in Claude (Settings \u2192 Connectors) and in ChatGPT (Developer mode), ${open ? "no sign-in needed (open mode)" : "sign in with the reviewer account"}, run a tool.`);
12447
- 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))}.`);
12448
- console.log(` 3. ChatGPT: ${import_chalk30.default.underline(PORTALS.chatgpt)} \u2192 the same packet, plus the 5 + 3 test cases it lists.`);
12449
- 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}`));
12450
12843
  console.log();
12451
12844
  }
12452
12845
 
@@ -21261,7 +21654,9 @@ var connector = program.command("connector").description(`A compliant Claude con
21261
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>`);
21262
21655
  await runConnector(project, opts);
21263
21656
  }));
21264
- 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) => runConnectorExercise(project, { ...withConnectorOpts(opts), noClaude: opts.claude === false, noEmail: opts.email === false })));
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
+ }));
21265
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))));
21266
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))));
21267
21662
  var withConnectorOpts = (opts) => ({ ...connector.opts(), ...opts ?? {} });