@apilift/cli 1.5.0 → 1.5.1

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.
@@ -28011,8 +28011,8 @@ var init_schema = __esm({
28011
28011
  if (v2.generated && (v2.path !== void 0 || v2.fields !== void 0))
28012
28012
  ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
28013
28013
  });
28014
- proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
28015
- proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
28014
+ proofPointer = pointerSchema.refine((value) => value.split("/").every((part) => !/\{(?:input|result|generated)\./u.test(part) || /^\{input\.[A-Za-z][A-Za-z0-9_-]*\}$/u.test(part)), "A dynamic JSON Pointer segment must be exactly {input.name}");
28015
+ proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: pointerSchema }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
28016
28016
  proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v2, ctx) => {
28017
28017
  if (!!v2.equals === !!v2.absent)
28018
28018
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
@@ -28092,7 +28092,9 @@ var init_schema = __esm({
28092
28092
  if (!v2.allowedOrigins.includes(origin))
28093
28093
  ctx.addIssue({ code: "custom", path: ["probes", origin], message: "Probe origin must be allowed" });
28094
28094
  const probeUrl = parseUrl(probe.request.url);
28095
- if (!["GET", "HEAD"].includes(probe.request.method) || probeUrl && probeUrl.origin !== origin)
28095
+ if (!probe.effect && !["GET", "HEAD"].includes(probe.request.method))
28096
+ ctx.addIssue({ code: "custom", path: ["probes", origin, "request", "method"], message: "Non-GET/HEAD read probes require effect: read" });
28097
+ if (probeUrl && probeUrl.origin !== origin)
28096
28098
  ctx.addIssue({ code: "custom", path: ["probes", origin, "request"], message: "Probe must be a read request to its origin" });
28097
28099
  }
28098
28100
  });
@@ -28354,7 +28356,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
28354
28356
  const transport = async (input2, init) => {
28355
28357
  const method = init?.method ?? "GET";
28356
28358
  const headers = new Headers(init?.headers);
28357
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
28359
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2,dynamic-bindings-v1");
28358
28360
  headers.set("Apilift-Client", `cli/${cliVersion}`);
28359
28361
  if (token) headers.set("Authorization", `Bearer ${token}`);
28360
28362
  if (sessionId) headers.set("Apilift-Session", sessionId);
@@ -29457,8 +29457,8 @@ var responseSchema = external_exports.strictObject({ schema: jsonSchema.optional
29457
29457
  if (v.generated && (v.path !== void 0 || v.fields !== void 0))
29458
29458
  ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
29459
29459
  });
29460
- var proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
29461
- var proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
29460
+ var proofPointer = pointerSchema.refine((value) => value.split("/").every((part) => !/\{(?:input|result|generated)\./u.test(part) || /^\{input\.[A-Za-z][A-Za-z0-9_-]*\}$/u.test(part)), "A dynamic JSON Pointer segment must be exactly {input.name}");
29461
+ var proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: pointerSchema }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
29462
29462
  var proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
29463
29463
  if (!!v.equals === !!v.absent)
29464
29464
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
@@ -29538,7 +29538,9 @@ var transportDefinitionSchema = external_exports.strictObject({ title, descripti
29538
29538
  if (!v.allowedOrigins.includes(origin))
29539
29539
  ctx.addIssue({ code: "custom", path: ["probes", origin], message: "Probe origin must be allowed" });
29540
29540
  const probeUrl = parseUrl(probe.request.url);
29541
- if (!["GET", "HEAD"].includes(probe.request.method) || probeUrl && probeUrl.origin !== origin)
29541
+ if (!probe.effect && !["GET", "HEAD"].includes(probe.request.method))
29542
+ ctx.addIssue({ code: "custom", path: ["probes", origin, "request", "method"], message: "Non-GET/HEAD read probes require effect: read" });
29543
+ if (probeUrl && probeUrl.origin !== origin)
29542
29544
  ctx.addIssue({ code: "custom", path: ["probes", origin, "request"], message: "Probe must be a read request to its origin" });
29543
29545
  }
29544
29546
  });
@@ -29587,8 +29589,13 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
29587
29589
  issues.push({ path: path2, message: "Unknown binding" });
29588
29590
  return;
29589
29591
  }
29590
- for (const [key, child] of entries)
29592
+ for (const [key, child] of entries) {
29593
+ if ((path2 === `${base2}/body` || path2.startsWith(`${base2}/body/`)) && /\{(?:input|generated|result|unresolved)\./u.test(key))
29594
+ inspectText(key, `${path2}/${key}`);
29595
+ else if (/\{(?:input|generated|result)\./u.test(key))
29596
+ issues.push({ path: `${path2}/${key}`, message: "Dynamic keys are supported only in JSON request bodies" });
29591
29597
  walk(child, `${path2}/${key}`);
29598
+ }
29592
29599
  };
29593
29600
  inspectText(request2.url, `${base2}/url`);
29594
29601
  for (const [header, value] of Object.entries(request2.headers ?? {})) {
@@ -29637,6 +29644,11 @@ function validateDefinition(kind, name, value) {
29637
29644
  for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
29638
29645
  if ("input" in comparison && !names2.includes(comparison.input))
29639
29646
  issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
29647
+ const proofPaths = [...definition2.proof?.equals ?? [], ...definition2.proof?.select?.where ?? [], ...definition2.proof?.select ? [definition2.proof.select] : []];
29648
+ for (const item of proofPaths)
29649
+ for (const match of item.path.matchAll(/\{input\.([^{}]+)\}/gu))
29650
+ if (!names2.includes(match[1]))
29651
+ issues.push({ path: "/proof", message: `Unknown pointer input ${match[1]}` });
29640
29652
  if (definition2.response?.generated && !generated.has(definition2.response.generated))
29641
29653
  issues.push({ path: "/response/generated", message: "Unknown generated value" });
29642
29654
  }
@@ -29821,7 +29833,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
29821
29833
  const transport = async (input2, init) => {
29822
29834
  const method = init?.method ?? "GET";
29823
29835
  const headers = new Headers(init?.headers);
29824
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
29836
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2,dynamic-bindings-v1");
29825
29837
  headers.set("Apilift-Client", `cli/${cliVersion}`);
29826
29838
  if (token) headers.set("Authorization", `Bearer ${token}`);
29827
29839
  if (sessionId) headers.set("Apilift-Session", sessionId);
package/dist/apilift.js CHANGED
@@ -28075,8 +28075,8 @@ var init_schema = __esm({
28075
28075
  if (v.generated && (v.path !== void 0 || v.fields !== void 0))
28076
28076
  ctx.addIssue({ code: "custom", message: "generated excludes path and fields" });
28077
28077
  });
28078
- proofPointer = pointerSchema.refine((value) => !/\{(?:input|result|generated)\.[^{}]*\}/u.test(value), "JSON Pointers are literal; use proof.select to find an item by input or result");
28079
- proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: proofPointer }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
28078
+ proofPointer = pointerSchema.refine((value) => value.split("/").every((part) => !/\{(?:input|result|generated)\./u.test(part) || /^\{input\.[A-Za-z][A-Za-z0-9_-]*\}$/u.test(part)), "A dynamic JSON Pointer segment must be exactly {input.name}");
28079
+ proofComparison = external_exports.union([external_exports.strictObject({ path: proofPointer, input: inputNameSchema }), external_exports.strictObject({ path: proofPointer, result: pointerSchema }), external_exports.strictObject({ path: proofPointer, value: valueSchema })]);
28080
28080
  proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), select: external_exports.strictObject({ path: proofPointer, where: external_exports.array(proofComparison).min(1) }).optional(), equals: external_exports.array(proofComparison).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
28081
28081
  if (!!v.equals === !!v.absent)
28082
28082
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
@@ -28156,7 +28156,9 @@ var init_schema = __esm({
28156
28156
  if (!v.allowedOrigins.includes(origin))
28157
28157
  ctx.addIssue({ code: "custom", path: ["probes", origin], message: "Probe origin must be allowed" });
28158
28158
  const probeUrl = parseUrl(probe.request.url);
28159
- if (!["GET", "HEAD"].includes(probe.request.method) || probeUrl && probeUrl.origin !== origin)
28159
+ if (!probe.effect && !["GET", "HEAD"].includes(probe.request.method))
28160
+ ctx.addIssue({ code: "custom", path: ["probes", origin, "request", "method"], message: "Non-GET/HEAD read probes require effect: read" });
28161
+ if (probeUrl && probeUrl.origin !== origin)
28160
28162
  ctx.addIssue({ code: "custom", path: ["probes", origin, "request"], message: "Probe must be a read request to its origin" });
28161
28163
  }
28162
28164
  });
@@ -28439,8 +28441,13 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
28439
28441
  issues.push({ path: path2, message: "Unknown binding" });
28440
28442
  return;
28441
28443
  }
28442
- for (const [key, child] of entries)
28444
+ for (const [key, child] of entries) {
28445
+ if ((path2 === `${base2}/body` || path2.startsWith(`${base2}/body/`)) && /\{(?:input|generated|result|unresolved)\./u.test(key))
28446
+ inspectText(key, `${path2}/${key}`);
28447
+ else if (/\{(?:input|generated|result)\./u.test(key))
28448
+ issues.push({ path: `${path2}/${key}`, message: "Dynamic keys are supported only in JSON request bodies" });
28443
28449
  walk(child, `${path2}/${key}`);
28450
+ }
28444
28451
  };
28445
28452
  inspectText(request2.url, `${base2}/url`);
28446
28453
  for (const [header, value] of Object.entries(request2.headers ?? {})) {
@@ -28489,6 +28496,11 @@ function validateDefinition(kind, name, value) {
28489
28496
  for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
28490
28497
  if ("input" in comparison && !names2.includes(comparison.input))
28491
28498
  issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
28499
+ const proofPaths = [...definition2.proof?.equals ?? [], ...definition2.proof?.select?.where ?? [], ...definition2.proof?.select ? [definition2.proof.select] : []];
28500
+ for (const item of proofPaths)
28501
+ for (const match of item.path.matchAll(/\{input\.([^{}]+)\}/gu))
28502
+ if (!names2.includes(match[1]))
28503
+ issues.push({ path: "/proof", message: `Unknown pointer input ${match[1]}` });
28492
28504
  if (definition2.response?.generated && !generated.has(definition2.response.generated))
28493
28505
  issues.push({ path: "/response/generated", message: "Unknown generated value" });
28494
28506
  }
@@ -28813,7 +28825,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
28813
28825
  const transport = async (input2, init) => {
28814
28826
  const method = init?.method ?? "GET";
28815
28827
  const headers = new Headers(init?.headers);
28816
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
28828
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2,dynamic-bindings-v1");
28817
28829
  headers.set("Apilift-Client", `cli/${cliVersion}`);
28818
28830
  if (token) headers.set("Authorization", `Bearer ${token}`);
28819
28831
  if (sessionId) headers.set("Apilift-Session", sessionId);
@@ -30782,7 +30794,7 @@ function draftTemplate(kind) {
30782
30794
  const probeGuidance = kind === "transport" ? [
30783
30795
  "// probes maps every exact allowedOrigins origin to a harmless documented read request and response contract.",
30784
30796
  '// Shape for each origin: { "request": { "method": "GET", "url": "<documented read URL>" }, "response": {} }',
30785
- "// The origin key has no path or trailing slash. request.method is GET or HEAD; request.url may be relative to baseUrl or an absolute URL at that origin.",
30797
+ '// The origin key has no path or trailing slash. request.method is GET or HEAD, or set effect: "read" on that probe for a documented/observed read through POST or another method; request.url may be relative to baseUrl or an absolute URL at that origin.',
30786
30798
  "// response may be empty, or contain schema (JSON Schema) and path (JSON Pointer selecting the result). It has no status, body or json fields.",
30787
30799
  "// A documented HTML or plain-text read can use response: {}; a successful non-JSON body is reported only as a format/status summary, never copied into verification output.",
30788
30800
  "// A non-JSON summary proves reachability only; inspect the signed-in UI and confirm the page's meaning before accepting verification.review.",
@@ -30794,7 +30806,7 @@ function draftTemplate(kind) {
30794
30806
  ""
30795
30807
  ].join("\n") : "";
30796
30808
  const tabGuidance = ["action", "transport"].includes(kind) ? "// capture.tab is a selector returned by browser tabs; it selects the same signed-in Chrome tab for capture and discover run, including connection probes. Leave other sessions' tabs untouched.\n" : "";
30797
- const captureGuidance = kind === "action" ? '// Public descriptions explain behavior, consequences and limits; do not instruct consumers to repeat runtime proof or browser checks. Author verification is separate from consumer judgment.\n// Request JSON/query uses typed {"$input":"name"} bindings; URL segments use {input.name}. Resolve every {unresolved.N} marker from retained evidence before running.\n// Discovery-only mutation proof compares a readback path with exactly one input, primary result path or internal JSON value: equals: [{"path":"/ready","value":true}]. Public literals require semantic review; fixed proof expectations are not caller flags.\n// Proof URLs bind returned identifiers with {result./id}; JSON bodies/query use {"$result":"/id"}. Pointers are literal, never templates.\n// For one member of a nested list: select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}; equals paths then refer to the single matching member. Compare identity and meaningful changed fields, not entire objects.\n// Repair a failed response/proof with discover run --proof-only; it reuses the saved primary response and sends only the readback. Publication still needs a successful test and accepted semantic review.\n// For large collections, investigate native search/filter/detail routes before enumeration. Search saved results locally and inspect only matching records; preserve structured dynamic fields.\n// After selecting a captured request, discover capture auth runs a bounded default search without authField or authScope. Set either only to narrow an observed field or location; browser-managed cookies need no locator.\n' : "";
30809
+ const captureGuidance = kind === "action" ? '// Public descriptions explain behavior, consequences and limits; do not instruct consumers to repeat runtime proof or browser checks. Author verification is separate from consumer judgment.\n// Request JSON/query uses typed {"$input":"name"} bindings; URL segments use {input.name}. Resolve every {unresolved.N} marker from retained evidence before running.\n// A readback through POST requires proof.effect: "read" alongside proof.request; HTTP method alone does not determine semantic effect.\n// JSON object keys may use {input.name}, e.g. {"{input.attribute_id}":{"$input":"value"}}.\n// Discovery-only mutation proof compares a readback path with exactly one input, primary result path or internal JSON value: equals: [{"path":"/ready","value":true}]. Public literals require semantic review; fixed proof expectations are not caller flags.\n// Proof URLs bind returned identifiers with {result./id}; JSON bodies/query use {"$result":"/id"}. Readback pointers may use a complete {input.name} segment for a dynamic key; slashes and tildes in its value are escaped.\n// For one member of a nested list: select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}; equals paths then refer to the single matching member. Compare identity and meaningful changed fields, not entire objects.\n// Repair a failed response/proof with discover run --proof-only; it reuses the saved primary response and sends only the readback. Publication still needs a successful test and accepted semantic review.\n// For large collections, investigate native search/filter/detail routes before enumeration. Search saved results locally and inspect only matching records; preserve structured dynamic fields.\n// After selecting a captured request, discover capture auth runs a bounded default search without authField or authScope. Set either only to narrow an observed field or location; browser-managed cookies need no locator.\n' : "";
30798
30810
  return `${head}${probeGuidance}${tabGuidance}${captureGuidance}${JSON.stringify(templates[kind], null, 2)}
30799
30811
  `;
30800
30812
  }
@@ -30813,7 +30825,7 @@ var init_envelope = __esm({
30813
30825
 
30814
30826
  // src/discovery/drafts.ts
30815
30827
  import { randomUUID as randomUUID4 } from "node:crypto";
30816
- import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
30828
+ import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, rmSync as rmSync3, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
30817
30829
  import { basename, dirname as dirname2, join as join5 } from "node:path";
30818
30830
  function pathFor(root, appSlug, kind, name) {
30819
30831
  if (!slugPattern.test(appSlug)) throw new Error("App slug is invalid");
@@ -30845,6 +30857,7 @@ var init_drafts = __esm({
30845
30857
  this.state = state;
30846
30858
  this.root = root;
30847
30859
  this.state.db.exec("CREATE TABLE IF NOT EXISTS discovery_draft (id TEXT PRIMARY KEY, app_slug TEXT NOT NULL, app_id TEXT, kind TEXT NOT NULL, name TEXT NOT NULL, owner_session_id TEXT, file_path TEXT NOT NULL, base_version INTEGER, UNIQUE(app_slug,kind,name)); CREATE TABLE IF NOT EXISTS selected_discovery_draft (session_id TEXT PRIMARY KEY, draft_id TEXT NOT NULL REFERENCES discovery_draft(id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS discovery_session (session_id TEXT PRIMARY KEY, app_slug TEXT NOT NULL, app_id TEXT, title TEXT NOT NULL); CREATE TABLE IF NOT EXISTS discovery_publication (draft_id TEXT PRIMARY KEY REFERENCES discovery_draft(id) ON DELETE CASCADE, request_id TEXT NOT NULL, request_body TEXT NOT NULL); CREATE TABLE IF NOT EXISTS discovery_evidence (draft_id TEXT PRIMARY KEY REFERENCES discovery_draft(id) ON DELETE CASCADE, fingerprint TEXT NOT NULL, verified_at TEXT NOT NULL, result_path TEXT NOT NULL, data_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS discovery_attempt (draft_id TEXT PRIMARY KEY REFERENCES discovery_draft(id) ON DELETE CASCADE, fingerprint TEXT NOT NULL, state TEXT NOT NULL, recorded_at TEXT NOT NULL)");
30860
+ this.state.db.exec("CREATE TABLE IF NOT EXISTS discovery_deferred (draft_id TEXT PRIMARY KEY REFERENCES discovery_draft(id) ON DELETE CASCADE, session_id TEXT NOT NULL); CREATE INDEX IF NOT EXISTS discovery_deferred_session ON discovery_deferred(session_id); CREATE TABLE IF NOT EXISTS discovery_finished (session_id TEXT PRIMARY KEY, result_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS discovery_cleanup (draft_id TEXT PRIMARY KEY, file_path TEXT NOT NULL UNIQUE, paths_json TEXT NOT NULL, scope_id TEXT)");
30848
30861
  this.state.db.exec("SAVEPOINT discovery_draft_schema");
30849
30862
  try {
30850
30863
  this.state.db.prepare("UPDATE discovery_draft SET id=id WHERE 0").run();
@@ -30933,7 +30946,24 @@ var init_drafts = __esm({
30933
30946
  close(sessionId) {
30934
30947
  const current = this.session(sessionId);
30935
30948
  if (!current) return null;
30936
- this.defer(sessionId);
30949
+ this.discard(sessionId);
30950
+ while (true) {
30951
+ this.state.db.exec("BEGIN IMMEDIATE");
30952
+ let next;
30953
+ try {
30954
+ next = this.state.db.prepare("SELECT d.id FROM discovery_draft d JOIN discovery_deferred r ON r.draft_id=d.id WHERE r.session_id=? AND d.owner_session_id IS NULL LIMIT 1").get(sessionId);
30955
+ if (next) {
30956
+ this.state.db.prepare("UPDATE discovery_draft SET owner_session_id=? WHERE id=?").run(sessionId, next.id);
30957
+ this.state.db.prepare("INSERT INTO selected_discovery_draft (session_id,draft_id) VALUES (?,?)").run(sessionId, next.id);
30958
+ }
30959
+ this.state.db.exec("COMMIT");
30960
+ } catch (error62) {
30961
+ this.state.db.exec("ROLLBACK");
30962
+ throw error62;
30963
+ }
30964
+ if (!next) break;
30965
+ this.discard(sessionId);
30966
+ }
30937
30967
  this.state.db.prepare("DELETE FROM discovery_session WHERE session_id=?").run(sessionId);
30938
30968
  return current;
30939
30969
  }
@@ -30984,40 +31014,64 @@ var init_drafts = __esm({
30984
31014
  this.requestAttempts();
30985
31015
  this.state.db.prepare("INSERT INTO discovery_request_attempt (draft_id,fingerprint,state,response_path,response_digest) VALUES (?,?,?,?,?) ON CONFLICT(draft_id,fingerprint) DO UPDATE SET state=excluded.state,response_path=COALESCE(excluded.response_path,discovery_request_attempt.response_path),response_digest=COALESCE(excluded.response_digest,discovery_request_attempt.response_digest)").run(draftId, fingerprint2, state, responsePath ?? null, responseDigest ?? null);
30986
31016
  }
30987
- discard(sessionId) {
31017
+ artifactPath(draftId, name) {
31018
+ const directory = join5(this.root, "draft-evidence", draftId, randomUUID4());
31019
+ mkdirSync5(directory, { recursive: true, mode: 448 });
31020
+ return join5(directory, name);
31021
+ }
31022
+ finished(sessionId) {
31023
+ const row = this.state.db.prepare("SELECT result_json FROM discovery_finished WHERE session_id=?").get(sessionId);
31024
+ return row ? JSON.parse(row.result_json) : null;
31025
+ }
31026
+ retire(sessionId, publishedResult) {
30988
31027
  const draft = this.selected(sessionId);
30989
31028
  if (!draft) return null;
30990
- if (this.publication(draft.id)) throw new DraftConflict("selected-draft", "A publication request may have been sent; reconcile it before discarding this draft");
31029
+ if (publishedResult === void 0 && this.publication(draft.id)) throw new DraftConflict("selected-draft", "A publication request may have been sent; reconcile it before discarding this draft");
30991
31030
  withBaselines(this.root, (baselines) => baselines.closeObservation(draft.id, "released"));
31031
+ const paths2 = [draft.filePath, `${draft.filePath}.current.json`, join5(this.root, "draft-evidence", draft.id)];
31032
+ const evidence = this.evidence(draft.id);
31033
+ if (evidence) paths2.push(evidence.resultPath);
31034
+ if (this.hasTable("discovery_request_attempt")) for (const row of this.state.db.prepare("SELECT response_path FROM discovery_request_attempt WHERE draft_id=? AND response_path IS NOT NULL").all(draft.id)) paths2.push(row.response_path);
31035
+ const scope2 = this.hasTable("discovery_capture") ? this.state.db.prepare("SELECT evidence_scope_id FROM discovery_capture WHERE draft_id=?").get(draft.id) : null;
30992
31036
  this.state.db.exec("BEGIN IMMEDIATE");
30993
31037
  try {
30994
- this.state.db.prepare("DELETE FROM selected_discovery_draft WHERE session_id=?").run(sessionId);
31038
+ this.state.db.prepare("INSERT INTO discovery_cleanup (draft_id,file_path,paths_json,scope_id) VALUES (?,?,?,?)").run(draft.id, draft.filePath, JSON.stringify(paths2), scope2?.evidence_scope_id ?? null);
31039
+ if (publishedResult !== void 0) this.state.db.prepare("INSERT INTO discovery_finished (session_id,result_json) VALUES (?,?) ON CONFLICT(session_id) DO UPDATE SET result_json=excluded.result_json").run(sessionId, JSON.stringify(publishedResult));
31040
+ if (this.hasTable("discovery_capture")) this.state.db.prepare("DELETE FROM discovery_capture WHERE draft_id=?").run(draft.id);
30995
31041
  this.state.db.prepare("DELETE FROM discovery_draft WHERE id=? AND owner_session_id=?").run(draft.id, sessionId);
30996
31042
  this.state.db.exec("COMMIT");
30997
- try {
30998
- unlinkSync2(draft.filePath);
30999
- } catch {
31000
- }
31001
31043
  return draft;
31002
31044
  } catch (error62) {
31003
31045
  this.state.db.exec("ROLLBACK");
31004
31046
  throw error62;
31005
31047
  }
31006
31048
  }
31007
- published(sessionId, version2) {
31008
- const draft = this.selected(sessionId);
31009
- if (!draft) return null;
31010
- withBaselines(this.root, (baselines) => baselines.closeObservation(draft.id, "released"));
31011
- this.state.db.exec("BEGIN IMMEDIATE");
31012
- try {
31013
- this.state.db.prepare("DELETE FROM selected_discovery_draft WHERE session_id=?").run(sessionId);
31014
- this.state.db.prepare("DELETE FROM discovery_publication WHERE draft_id=?").run(draft.id);
31015
- this.state.db.prepare("UPDATE discovery_draft SET base_version=?,owner_session_id=NULL WHERE id=?").run(version2, draft.id);
31016
- this.state.db.exec("COMMIT");
31017
- return draft;
31018
- } catch (error62) {
31019
- this.state.db.exec("ROLLBACK");
31020
- throw error62;
31049
+ discard(sessionId) {
31050
+ return this.retire(sessionId);
31051
+ }
31052
+ published(sessionId, result3) {
31053
+ return this.retire(sessionId, result3);
31054
+ }
31055
+ hasCleanup() {
31056
+ return Boolean(this.state.db.prepare("SELECT 1 FROM discovery_cleanup LIMIT 1").get());
31057
+ }
31058
+ /** Durable retirement precedes filesystem/broker cleanup, so a retry never republishes. */
31059
+ async cleanup(closeScope) {
31060
+ for (const row of this.state.db.prepare("SELECT * FROM discovery_cleanup").all()) {
31061
+ for (const path2 of new Set(JSON.parse(row.paths_json))) {
31062
+ rmSync3(path2, { force: true, recursive: path2 === join5(this.root, "draft-evidence", row.draft_id) });
31063
+ let parent = dirname2(path2);
31064
+ while (parent !== this.root && parent.startsWith(`${join5(this.root, "drafts")}/`)) {
31065
+ try {
31066
+ rmdirSync(parent);
31067
+ parent = dirname2(parent);
31068
+ } catch {
31069
+ break;
31070
+ }
31071
+ }
31072
+ }
31073
+ if (row.scope_id) await closeScope(row.scope_id);
31074
+ this.state.db.prepare("DELETE FROM discovery_cleanup WHERE draft_id=?").run(row.draft_id);
31021
31075
  }
31022
31076
  }
31023
31077
  selected(sessionId) {
@@ -31029,6 +31083,7 @@ var init_drafts = __esm({
31029
31083
  let filePath = pathFor(this.root, input2.appSlug, input2.kind, name);
31030
31084
  this.state.db.exec("BEGIN IMMEDIATE");
31031
31085
  try {
31086
+ if (this.state.db.prepare("SELECT 1 FROM discovery_cleanup WHERE file_path=?").get(filePath)) throw new DraftConflict("selected-draft", "Draft cleanup is pending; finish cleanup before reopening this resource");
31032
31087
  const selected = this.selected(input2.sessionId);
31033
31088
  if (selected && (selected.appSlug !== input2.appSlug || selected.kind !== input2.kind || selected.name !== name)) throw new DraftConflict("selected-draft", `This agent already selected ${selected.kind} ${selected.name}; defer or finish it before opening another resource`);
31034
31089
  const existing = this.state.db.prepare("SELECT * FROM discovery_draft WHERE app_slug=? AND kind=? AND name=?").get(input2.appSlug, input2.kind, name);
@@ -31038,6 +31093,7 @@ var init_drafts = __esm({
31038
31093
  const id2 = existing ? String(existing.id) : randomUUID4();
31039
31094
  if (!existing) this.state.db.prepare("INSERT INTO discovery_draft (id,app_slug,app_id,kind,name,owner_session_id,file_path,base_version) VALUES (?,?,?,?,?,?,?,?)").run(id2, input2.appSlug, input2.appId ?? null, input2.kind, name, input2.sessionId, filePath, input2.baseVersion ?? null);
31040
31095
  else this.state.db.prepare("UPDATE discovery_draft SET owner_session_id=? WHERE id=?").run(input2.sessionId, id2);
31096
+ this.state.db.prepare("DELETE FROM discovery_deferred WHERE draft_id=?").run(id2);
31041
31097
  this.state.db.prepare("INSERT INTO selected_discovery_draft (session_id,draft_id) VALUES (?,?) ON CONFLICT(session_id) DO UPDATE SET draft_id=excluded.draft_id").run(input2.sessionId, id2);
31042
31098
  mkdirSync5(dirname2(filePath), { recursive: true, mode: 448 });
31043
31099
  if (!existsSync2(filePath) && existing?.intent !== "remove") writeFileSync5(filePath, draftTemplate(input2.kind), { flag: "wx", mode: 384 });
@@ -31054,6 +31110,7 @@ var init_drafts = __esm({
31054
31110
  const filePath = pathFor(this.root, input2.appSlug, input2.kind, input2.name).replace(/\.jsonc$/, ".delete.jsonc");
31055
31111
  this.state.db.exec("BEGIN IMMEDIATE");
31056
31112
  try {
31113
+ if (this.state.db.prepare("SELECT 1 FROM discovery_cleanup WHERE file_path=?").get(filePath)) throw new DraftConflict("selected-draft", "Draft cleanup is pending; finish cleanup before reopening this resource");
31057
31114
  const selected = this.selected(input2.sessionId);
31058
31115
  const existing = this.state.db.prepare("SELECT * FROM discovery_draft WHERE app_slug=? AND kind=? AND name=?").get(input2.appSlug, input2.kind, input2.name);
31059
31116
  if (selected && selected.id !== existing?.id) throw new DraftConflict("selected-draft", "Finish or defer the selected draft before preparing a removal");
@@ -31085,6 +31142,7 @@ ${JSON.stringify({ remove: true, expectedVersion: input2.baseVersion }, null, 2)
31085
31142
  const draft = this.selected(sessionId);
31086
31143
  if (draft) {
31087
31144
  withBaselines(this.root, (baselines) => baselines.closeObservation(draft.id, "released"));
31145
+ this.state.db.prepare("INSERT INTO discovery_deferred (draft_id,session_id) VALUES (?,?) ON CONFLICT(draft_id) DO UPDATE SET session_id=excluded.session_id").run(draft.id, sessionId);
31088
31146
  this.state.db.prepare("DELETE FROM selected_discovery_draft WHERE session_id=?").run(sessionId);
31089
31147
  this.state.db.prepare("UPDATE discovery_draft SET owner_session_id=NULL WHERE id=? AND owner_session_id=?").run(draft.id, sessionId);
31090
31148
  }
@@ -31710,7 +31768,7 @@ var init_process = __esm({
31710
31768
 
31711
31769
  // src/capture-server/client.ts
31712
31770
  import { createHash as createHash4 } from "node:crypto";
31713
- import { chmodSync as chmodSync5, existsSync as existsSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync as rmSync3 } from "node:fs";
31771
+ import { chmodSync as chmodSync5, existsSync as existsSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync as rmSync4 } from "node:fs";
31714
31772
  import { dirname as dirname3, join as join6 } from "node:path";
31715
31773
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
31716
31774
  function knownLegacyBroker(version2) {
@@ -31800,7 +31858,7 @@ async function ensureCaptureServerUnderUpdateLock(options2) {
31800
31858
  if (error62.code === "capture-server-busy") throw error62;
31801
31859
  }
31802
31860
  try {
31803
- rmSync3(join6(root, "server.json"));
31861
+ rmSync4(join6(root, "server.json"));
31804
31862
  } catch {
31805
31863
  }
31806
31864
  }
@@ -32443,7 +32501,7 @@ var init_vault = __esm({
32443
32501
 
32444
32502
  // src/commands/run.ts
32445
32503
  import { randomUUID as randomUUID7 } from "node:crypto";
32446
- import { readFileSync as readFileSync7, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
32504
+ import { readFileSync as readFileSync7, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
32447
32505
  import { dirname as dirname5, resolve as resolve4, join as join10 } from "node:path";
32448
32506
  function pointer2(value, path2) {
32449
32507
  if (path2 === "") return value;
@@ -32569,10 +32627,18 @@ function bind(value, inputs, generated, output2, insideArray = false) {
32569
32627
  if (typeof object4.$generated === "string") return generated[object4.$generated];
32570
32628
  if (typeof object4.$result === "string") return pointer2(output2, object4.$result);
32571
32629
  }
32572
- const target = {};
32630
+ const target = /* @__PURE__ */ Object.create(null);
32573
32631
  for (const [name, item] of Object.entries(object4)) {
32574
32632
  const resolved = bind(item, inputs, generated, output2);
32575
- if (resolved !== omit2) target[name] = resolved;
32633
+ if (resolved !== omit2) {
32634
+ const key = name.replace(/\{(input|generated|result)\.([^{}]*)\}/gu, (_, kind, ref) => {
32635
+ const selected = kind === "input" ? inputs[ref] : kind === "generated" ? generated[ref] : pointer2(output2, ref);
32636
+ if (typeof selected !== "string" && typeof selected !== "number") throw new Error(`Object key ${kind}.${ref} needs a string or number`);
32637
+ return String(selected);
32638
+ });
32639
+ if (Object.hasOwn(target, key)) throw new Error("Object key bindings collide");
32640
+ target[key] = resolved;
32641
+ }
32576
32642
  }
32577
32643
  if (!Object.keys(target).length && Object.keys(object4).length && !insideArray) return omit2;
32578
32644
  return target;
@@ -32663,7 +32729,7 @@ function writeOutput(path2, content) {
32663
32729
  renameSync2(pending, path2);
32664
32730
  } catch (error62) {
32665
32731
  try {
32666
- unlinkSync3(pending);
32732
+ unlinkSync2(pending);
32667
32733
  } catch {
32668
32734
  }
32669
32735
  throw error62;
@@ -32776,6 +32842,13 @@ async function sendBrowser(root, appId, contextName, pageOrigin, effect2, transp
32776
32842
  if (!selection.execution) await client.closeScope(scope2.id);
32777
32843
  }
32778
32844
  }
32845
+ function proofPath(path2, inputs) {
32846
+ return path2.replace(/\{input\.([^{}]+)\}/gu, (_, name) => {
32847
+ const value = inputs[name];
32848
+ if (typeof value !== "string" && typeof value !== "number") throw new Error(`Pointer input ${name} needs a string or number`);
32849
+ return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
32850
+ });
32851
+ }
32779
32852
  function confirmProof(proof, body, primary, inputs, status) {
32780
32853
  if (proof.absent) {
32781
32854
  if (status !== 404) throw new ResponseContractFailure(`Absence proof expected HTTP 404; received HTTP ${status}`);
@@ -32786,7 +32859,7 @@ function confirmProof(proof, body, primary, inputs, status) {
32786
32859
  const expected = "input" in comparison ? inputs[comparison.input] : "result" in comparison ? pointer2(primary, comparison.result) : comparison.value;
32787
32860
  let actual;
32788
32861
  try {
32789
- actual = pointer2(selected2, comparison.path);
32862
+ actual = pointer2(selected2, proofPath(comparison.path, inputs));
32790
32863
  } catch (error62) {
32791
32864
  if (missingIsMismatch) return false;
32792
32865
  throw error62;
@@ -32795,7 +32868,7 @@ function confirmProof(proof, body, primary, inputs, status) {
32795
32868
  };
32796
32869
  let selected = body;
32797
32870
  if (proof.select) {
32798
- const collection = pointer2(body, proof.select.path);
32871
+ const collection = pointer2(body, proofPath(proof.select.path, inputs));
32799
32872
  if (!Array.isArray(collection)) throw new ResponseContractFailure(`Readback selection ${proof.select.path || "/"} must be an array`);
32800
32873
  const found = collection.filter((item) => proof.select.where.every((comparison) => matches(comparison, item, true)));
32801
32874
  if (found.length !== 1) throw new ResponseContractFailure(`Readback selection matched ${found.length} items; expected exactly one`);
@@ -32999,7 +33072,7 @@ Inspect saved entries: ${prefix(`credentials list ${appName} ${action.transport}
32999
33072
  `);
33000
33073
  } else output2 = result(serialized, true);
33001
33074
  try {
33002
- unlinkSync3(recoveryPath);
33075
+ unlinkSync2(recoveryPath);
33003
33076
  } catch {
33004
33077
  }
33005
33078
  } catch (error62) {
@@ -33047,6 +33120,7 @@ async function executeDraftVerification(input2) {
33047
33120
  const generated = input2.primaryEvidence?.generated ?? (input2.kind === "action" && action && "generated" in action ? Object.fromEntries(Object.entries(action.generated ?? {}).map(([name, spec]) => [name, `${spec.prefix ?? ""}${randomUUID7()}`])) : {});
33048
33121
  try {
33049
33122
  values = effectiveInputs(schema, input2.primaryEvidence?.inputs ?? input2.sampleInputs, {}, true).values;
33123
+ if (action && "proof" in action && action.proof) for (const item of [...action.proof.equals ?? [], ...action.proof.select?.where ?? [], ...action.proof.select ? [action.proof.select] : []]) proofPath(item.path, values);
33050
33124
  const request2 = input2.kind === "action" ? action.request : probe.request;
33051
33125
  prepared = prepareRequest(request2, input2.kind === "transport" && input2.probeOrigin ? { ...transport, baseUrl: input2.probeOrigin } : transport, values, generated);
33052
33126
  } catch (error62) {
@@ -33863,7 +33937,10 @@ function deriveRequest(entry2, inputs) {
33863
33937
  if (name) return { $input: name };
33864
33938
  if (value === null) return null;
33865
33939
  if (Array.isArray(value)) return value.map((child, index) => bind2(child, `${path3}/${index}`, wireText));
33866
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, sensitive.test(key) ? pending(`${path3}/${key}`) : bind2(child, `${path3}/${key}`)]));
33940
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => {
33941
+ const keyInput = match(key, true);
33942
+ return [keyInput ? `{input.${keyInput}}` : key, sensitive.test(key) ? pending(`${path3}/${key}`) : bind2(child, `${path3}/${key}`)];
33943
+ }));
33867
33944
  return pending(path3);
33868
33945
  };
33869
33946
  const path2 = url3.pathname.split("/").map((segment, index) => {
@@ -33955,14 +34032,38 @@ function inferResponseSchema(value, limit = 2e4) {
33955
34032
  };
33956
34033
  return { schema: visit4(value, 0), complete };
33957
34034
  }
33958
- function rankCandidates(entries, inputs) {
33959
- const samples = Object.values(inputs).filter((value) => ["string", "number"].includes(typeof value) && String(value).length > 1).map(String);
34035
+ function rankCandidates(entries, inputs, effect2, allowedOrigins = []) {
34036
+ const samples = [...new Set(Object.values(inputs).filter((value) => ["string", "number", "boolean"].includes(typeof value)).map((value) => canonicalJson(value)))];
33960
34037
  const score = ({ entry: entry2 }) => {
33961
34038
  let value = 0;
33962
34039
  const url3 = String(entry2.url ?? ""), body = typeof entry2.reqBody === "string" ? entry2.reqBody : "";
33963
- if (samples.some((sample) => url3.includes(encodeURIComponent(sample)) || body.includes(JSON.stringify(sample)))) value += 100;
34040
+ const observed = /* @__PURE__ */ new Set();
34041
+ const collect = (item) => {
34042
+ if (Array.isArray(item)) item.forEach(collect);
34043
+ else if (item && typeof item === "object") for (const [key, child] of Object.entries(item)) {
34044
+ observed.add(canonicalJson(key));
34045
+ collect(child);
34046
+ }
34047
+ else observed.add(canonicalJson(item));
34048
+ };
34049
+ try {
34050
+ collect(JSON.parse(body));
34051
+ } catch {
34052
+ }
34053
+ try {
34054
+ const target = new URL(url3);
34055
+ if (allowedOrigins.includes(target.origin)) value += 25;
34056
+ for (const item of [...target.pathname.split("/").map(decodeURIComponent), ...target.searchParams.values()]) {
34057
+ observed.add(canonicalJson(item));
34058
+ for (const sample of samples) if (String(JSON.parse(sample)) === item) observed.add(sample);
34059
+ }
34060
+ } catch {
34061
+ }
34062
+ value += samples.filter((sample) => observed.has(sample)).length * 100;
34063
+ const methods = effect2 === "create" ? ["POST"] : effect2 === "update" ? ["PATCH", "PUT", "POST"] : effect2 === "delete" ? ["DELETE"] : [];
34064
+ if (methods.includes(String(entry2.method))) value += effect2 === "update" && entry2.method === "POST" ? 10 : 30;
33964
34065
  const contentType = Object.entries(entry2.resHeaders ?? {}).find(([key]) => key.toLowerCase() === "content-type")?.[1];
33965
- if (String(contentType ?? "").includes("json") || entry2.resBodyArtifact || typeof entry2.resBody === "string" && /^\s*(?:\[|{)/.test(entry2.resBody)) value += 20;
34066
+ if (String(contentType ?? "").includes("json") || typeof entry2.resBody === "string" && /^\s*(?:\[|{)/.test(entry2.resBody)) value += 20;
33966
34067
  if (Number(entry2.status) >= 200 && Number(entry2.status) < 300) value += 5;
33967
34068
  if (/\.(?:js|css|png|jpg|svg|woff2?)(?:[?#]|$)/i.test(url3)) value -= 50;
33968
34069
  if (entry2.method === "OPTIONS") value -= 50;
@@ -33982,9 +34083,8 @@ var init_capture_derivation = __esm({
33982
34083
  });
33983
34084
 
33984
34085
  // src/commands/discover.ts
33985
- import { existsSync as existsSync4, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "node:fs";
34086
+ import { existsSync as existsSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "node:fs";
33986
34087
  import { createHash as createHash5, randomUUID as randomUUID9 } from "node:crypto";
33987
- import { tmpdir as tmpdir2 } from "node:os";
33988
34088
  import { dirname as dirname6, join as join12 } from "node:path";
33989
34089
  async function currentResource(api, appId, kind, name) {
33990
34090
  try {
@@ -34116,7 +34216,7 @@ Abandon removal: ${command4(invocation, "discover discard")}
34116
34216
  const local = `Check edits: ${command4(invocation, "discover check")}
34117
34217
  Publish: ${command4(invocation, "discover publish")}
34118
34218
  Recover this file: ${command4(invocation, "discover status")}
34119
- Keep for later: ${command4(invocation, "discover defer")}
34219
+ Keep for later (including evidence): ${command4(invocation, "discover defer")}
34120
34220
  Abandon unpublished edits: ${command4(invocation, "discover discard")}
34121
34221
  `;
34122
34222
  if (draft.kind === "app") return `Created an app draft for ${draft.appSlug}. Nothing has been published.
@@ -34124,7 +34224,7 @@ Edit: ${draft.filePath}
34124
34224
 
34125
34225
  Fill the display name, product description, main website, category and visibility. The file lists supported fields and allowed values. Do not put credentials or local context values in this definition.
34126
34226
 
34127
- ${local}Leave discovery: ${command4(invocation, "discover close")}
34227
+ ${local}Close and abandon this session\u2019s drafts: ${command4(invocation, "discover close")}. Deferred drafts survive action switches until discovery closes.
34128
34228
  `;
34129
34229
  if (draft.kind === "action" && unavailable) return `Editing documented unavailability for ${draft.name}.
34130
34230
  Edit: ${draft.filePath}
@@ -34305,7 +34405,8 @@ async function handleDiscover(invocation, api, state, session, fetcher, root, in
34305
34405
  if (!session) return sessionRequired(invocation);
34306
34406
  let release;
34307
34407
  try {
34308
- release = lockDiscovery(root, session.id);
34408
+ release = ["appsHistory", "discoverCaptureShow", "discoverCaptureList"].includes(invocation.key) ? () => {
34409
+ } : lockDiscovery(root, session.id);
34309
34410
  } catch (error62) {
34310
34411
  return fail5(`${error62 instanceof Error ? error62.message : "discovery-command-busy"}. No state changed.
34311
34412
  `, 8);
@@ -34364,7 +34465,7 @@ Inspect prior publications for this app:
34364
34465
  ${command4(invocation, `apps history ${app2.slug}`)}
34365
34466
  Narrow history to a known published resource with --resource <kind:name>.
34366
34467
  Resume current work: ${command4(invocation, "discover status")}
34367
- Leave discovery: ${command4(invocation, "discover close")}
34468
+ Close and abandon this session\u2019s drafts: ${command4(invocation, "discover close")}. Deferred drafts survive action switches until discovery closes.
34368
34469
  `);
34369
34470
  }
34370
34471
  if (invocation.key === "appsHistory") {
@@ -34409,7 +34510,7 @@ ${pending ? `A publication request may have been sent. Restore the original inte
34409
34510
  ` : `Nothing has been removed.
34410
34511
  Check dependencies: ${command4(invocation, "discover check")}
34411
34512
  Publish removal: ${command4(invocation, "discover publish")}
34412
- Keep for later: ${command4(invocation, "discover defer")}
34513
+ Keep for later (including evidence): ${command4(invocation, "discover defer")}
34413
34514
  Abandon removal: ${command4(invocation, "discover discard")}
34414
34515
  `}`);
34415
34516
  }
@@ -34423,7 +34524,7 @@ ${evidence ? `Latest saved execution: ${evidence.resultPath}
34423
34524
  ` : ""}
34424
34525
  Check: ${command4(invocation, "discover check")}
34425
34526
  Publish: ${command4(invocation, "discover publish")}
34426
- Keep for later: ${command4(invocation, "discover defer")}
34527
+ Keep for later (including evidence): ${command4(invocation, "discover defer")}
34427
34528
  Abandon unpublished edits: ${command4(invocation, "discover discard")}
34428
34529
  `);
34429
34530
  }
@@ -34441,23 +34542,38 @@ ${retained.map((item) => ` ${item.kind} ${item.name}: ${item.filePath}
34441
34542
  if (store.publication(store.selected(session.id)?.id ?? "")) return fail5(`A publication request may have been sent. Reconcile with ${command4(invocation, "discover publish")} before deferring.
34442
34543
  `);
34443
34544
  const draft = store.defer(session.id);
34444
- return draft ? ok4(`Kept ${draft.name} and its selected evidence; released editing ownership.
34545
+ return draft ? ok4(`Kept ${draft.name} and its selected evidence until discovery closes; released editing ownership.
34445
34546
  Resume: ${command4(invocation, `discover ${draft.intent === "remove" ? "remove" : "draft"} ${draft.kind}${draft.kind === "app" ? "" : ` ${draft.name}`}`)}
34446
34547
  `) : fail5("No owned draft is selected.\n");
34447
34548
  }
34448
34549
  if (invocation.key === "discoverDiscard") {
34449
34550
  if (captureRow(state, store.selected(session.id)?.id ?? "")?.scope_id) return fail5(`A recording is active. Stop it with ${command4(invocation, "discover capture stop")} or cancel it with ${command4(invocation, "discover capture cancel")} before discarding.
34450
34551
  `);
34451
- const evidenceScope = captureRow(state, store.selected(session.id)?.id ?? "")?.evidence_scope_id;
34552
+ const pending = store.hasCleanup();
34452
34553
  const draft = store.discard(session.id);
34453
- if (draft && evidenceScope) await (await captureClient(root)).closeScope(evidenceScope);
34554
+ try {
34555
+ await store.cleanup(async (id2) => (await captureClient(root, false)).closeScope(id2));
34556
+ } catch {
34557
+ return fail5(`local-state-error \u2014 Draft retirement is saved; cleanup remains pending. Retry ${command4(invocation, "discover discard")}.
34558
+ `, 12);
34559
+ }
34560
+ if (!draft && pending) return ok4("Discard cleanup completed.\n");
34454
34561
  return draft ? ok4(`Discarded the owned unpublished draft for ${draft.name}. Its published definition is unchanged.
34455
34562
  Start again: ${command4(invocation, `discover ${draft.intent === "remove" ? "remove" : "draft"} ${draft.kind}${draft.kind === "app" ? "" : ` ${draft.name}`}`)}
34456
34563
  `) : fail5("No owned draft is selected.\n");
34457
34564
  }
34458
34565
  if (invocation.key === "discoverClose") {
34459
34566
  const current = store.session(session.id);
34460
- if (!current) return fail5("No discovery session is open.\n");
34567
+ if (!current) {
34568
+ if (!store.hasCleanup()) return fail5("No discovery session is open.\n");
34569
+ try {
34570
+ await store.cleanup(async (id2) => (await captureClient(root, false)).closeScope(id2));
34571
+ } catch {
34572
+ return fail5(`local-state-error \u2014 Discovery is closed; cleanup remains pending. Retry ${command4(invocation, "discover close")}.
34573
+ `, 12);
34574
+ }
34575
+ return ok4("Closed discovery cleanup completed.\n");
34576
+ }
34461
34577
  if (store.publication(store.selected(session.id)?.id ?? "")) return fail5(`A publication request may be in flight. Reconcile with ${command4(invocation, "discover publish")} before closing.
34462
34578
  `);
34463
34579
  if (captureRow(state, store.selected(session.id)?.id ?? "")?.scope_id) return fail5(`A recording is active. Stop it with ${command4(invocation, "discover capture stop")} or cancel it with ${command4(invocation, "discover capture cancel")} before closing.
@@ -34465,7 +34581,13 @@ Start again: ${command4(invocation, `discover ${draft.intent === "remove" ? "rem
34465
34581
  const installation = state.installation();
34466
34582
  if (installation) await api.call("releaseSession", { params: { id: apiSessionId(installation.id, session) } });
34467
34583
  store.close(session.id);
34468
- return ok4(`Closed discovery for ${current.title}. Published actions remain usable.
34584
+ try {
34585
+ await store.cleanup(async (id2) => (await captureClient(root, false)).closeScope(id2));
34586
+ } catch {
34587
+ return fail5(`local-state-error \u2014 Discovery is closed; cleanup remains pending. Retry ${command4(invocation, "discover close")}.
34588
+ `, 12);
34589
+ }
34590
+ return ok4(`Closed discovery for ${current.title}; discarded this session\u2019s selected and deferred drafts. Published actions remain usable.
34469
34591
  To continue later: ${command4(invocation, `apps update ${current.appSlug}`)}
34470
34592
  `);
34471
34593
  }
@@ -34498,16 +34620,12 @@ Abandon removal: ${command4(invocation, "discover discard")}
34498
34620
  if (invocation.key === "discoverPublish") {
34499
34621
  if (captureRow(state, store.selected(session.id)?.id ?? "")?.scope_id) return fail5(`A recording is active. Stop or cancel it before publication. No app request was sent.
34500
34622
  `);
34501
- const draftId = store.selected(session.id)?.id;
34502
- const result3 = await publishDraft(invocation, api, store, session, root);
34503
- if (result3.exitCode === 0 && draftId && !store.selected(session.id)) {
34504
- const evidenceScope = captureRow(state, draftId)?.evidence_scope_id;
34505
- try {
34506
- if (evidenceScope) await (await captureClient(root)).closeScope(evidenceScope);
34507
- state.db.prepare("DELETE FROM discovery_capture WHERE draft_id=?").run(draftId);
34508
- } catch {
34509
- result3.stderr += "Publication succeeded; private capture cleanup remains pending. No app request was repeated.\n";
34510
- }
34623
+ const result3 = !store.selected(session.id) && store.finished(session.id) ? store.finished(session.id) : await publishDraft(invocation, api, store, session, root);
34624
+ try {
34625
+ await store.cleanup(async (id2) => (await captureClient(root, false)).closeScope(id2));
34626
+ } catch {
34627
+ return fail5(`local-state-error \u2014 Publication result is retained; cleanup remains pending. Retry ${command4(invocation, "discover publish")} to finish cleanup without publishing again.
34628
+ `, 12, result3.dispatched);
34511
34629
  }
34512
34630
  return result3;
34513
34631
  }
@@ -34565,6 +34683,10 @@ File: ${checked.draft.filePath}
34565
34683
  }
34566
34684
  const { draft } = checked;
34567
34685
  const removing = draft.intent === "remove";
34686
+ const complete = (result3) => {
34687
+ store.published(session.id, result3);
34688
+ return result3;
34689
+ };
34568
34690
  if (!removing && !checked.definition) return fail5(`The file is incomplete: ${draft.filePath}
34569
34691
  `);
34570
34692
  let verification;
@@ -34639,13 +34761,12 @@ Fix the named issue, then ${command4(invocation, "discover check")} and ${comman
34639
34761
  }
34640
34762
  const appId = String(created.id);
34641
34763
  store.setAppId(draft.appSlug, appId);
34642
- store.published(session.id, 1);
34643
- return ok4(`Published ${checked.definition && "title" in checked.definition ? checked.definition.title : draft.appSlug}'s metadata. No executable action exists yet.
34764
+ return complete(ok4(`Published ${checked.definition && "title" in checked.definition ? checked.definition.title : draft.appSlug}'s metadata. No executable action exists yet.
34644
34765
  ${actionNameHelp}
34645
34766
  Check the requested operation in official API and access documentation:
34646
34767
  ${command4(invocation, "discover api assess <entity.verb>")}
34647
34768
  If no usable public route is found, record the evidence and use browser transport.
34648
- `, true);
34769
+ `, true));
34649
34770
  }
34650
34771
  let published;
34651
34772
  try {
@@ -34653,29 +34774,28 @@ If no usable public route is found, record the evidence and use browser transpor
34653
34774
  } catch (error62) {
34654
34775
  return publicationError(error62);
34655
34776
  }
34656
- store.published(session.id, published.resource.version);
34657
34777
  if (!removing && draft.kind === "action" && published.executableHash) withBaselines(root, (baselines) => baselines.published(draft.id, draft.name, published.executableHash, /* @__PURE__ */ new Date(), published.resource.version));
34658
- if (published.distribution && published.distribution.state !== "available") return ok4(`Published ${label(draft.kind)} ${draft.name}. Distribution is ${published.distribution.state}. ${published.distribution.reason === "confirmed-defect" ? "This executable revision was withdrawn. Continue developing and submit a corrected definition." : "You can continue developing and using your accepted revision. Wider distribution advances automatically as independent workspaces use it."}${published.distribution.until ? ` Protective pause ends ${published.distribution.until}.` : ""}
34778
+ if (published.distribution && published.distribution.state !== "available") return complete(ok4(`Published ${label(draft.kind)} ${draft.name}. Distribution is ${published.distribution.state}. ${published.distribution.reason === "confirmed-defect" ? "This executable revision was withdrawn. Continue developing and submit a corrected definition." : "You can continue developing and using your accepted revision. Wider distribution advances automatically as independent workspaces use it."}${published.distribution.until ? ` Protective pause ends ${published.distribution.until}.` : ""}
34659
34779
  Inspect distribution: ${command4(invocation, `apps history ${draft.appSlug} --resource ${backendKind(draft.kind)}:${draft.name}`)}
34660
- `, true);
34661
- if (removing) return ok4(`Removed ${draft.kind} ${draft.name} from the published interface. App records were unchanged.
34662
- `, true);
34663
- if (draft.kind === "action" && checked.definition && "unavailable" in checked.definition) return ok4(`Published the documented unavailability of ${draft.name}. No app request was sent and this action is not executable.
34780
+ `, true));
34781
+ if (removing) return complete(ok4(`Removed ${draft.kind} ${draft.name} from the published interface. App records were unchanged.
34782
+ `, true));
34783
+ if (draft.kind === "action" && checked.definition && "unavailable" in checked.definition) return complete(ok4(`Published the documented unavailability of ${draft.name}. No app request was sent and this action is not executable.
34664
34784
  Inspect the finding: ${command4(invocation, `actions show ${draft.appSlug} ${draft.name}`)}
34665
34785
  When finished authoring: ${command4(invocation, "discover close")}
34666
- `, true);
34667
- if (draft.kind === "action") return ok4(`Published ${draft.name}. Its reviewed test already returned the requested data; use the saved result instead of executing again.
34786
+ `, true));
34787
+ if (draft.kind === "action") return complete(ok4(`Published ${draft.name}. Its reviewed test already returned the requested data. Temporary draft and verification files were cleared; do not repeat the test.
34668
34788
  Inspect arguments for future use: ${command4(invocation, `actions show ${draft.appSlug} ${draft.name}`)}
34669
34789
  When finished authoring: ${command4(invocation, "discover close")}
34670
- `, true);
34671
- if (draft.kind === "transport") return ok4(`Published connection ${draft.name}. Credential values and local test inputs were not shared.
34790
+ `, true));
34791
+ if (draft.kind === "transport") return complete(ok4(`Published connection ${draft.name}. Credential values and local test inputs were not shared.
34672
34792
  ${actionNameHelp}
34673
34793
  Create an operation that uses it: ${command4(invocation, "discover draft action <entity.verb>")}
34674
34794
  Set dependencies.transport to ${draft.name} in that action file.
34675
- `, true);
34676
- return ok4(`Published ${label(draft.kind)} ${draft.name}.
34795
+ `, true));
34796
+ return complete(ok4(`Published ${label(draft.kind)} ${draft.name}.
34677
34797
  Continue with ${command4(invocation, "discover draft <kind> <name>")} or ${command4(invocation, "discover close")}
34678
- `, true);
34798
+ `, true));
34679
34799
  }
34680
34800
  async function rebaseDraft(invocation, api, store, session) {
34681
34801
  const draft = store.selected(session.id);
@@ -34695,7 +34815,7 @@ async function rebaseDraft(invocation, api, store, session) {
34695
34815
  if (checked.issues.length) return checkResponse(checked, invocation);
34696
34816
  const source = readFileSync8(draft.filePath, "utf8");
34697
34817
  const updated = applyEdits(source, modify(source, ["expectedVersion"], current.version, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
34698
- const temporary = `${draft.filePath}.${randomUUID9()}.tmp`;
34818
+ const temporary = store.artifactPath(draft.id, "rebase.jsonc");
34699
34819
  writeFileSync8(temporary, updated, { flag: "wx", mode: 384 });
34700
34820
  renameSync3(temporary, draft.filePath);
34701
34821
  store.setBaseVersion(draft.id, current.version);
@@ -34765,8 +34885,7 @@ Review it and publish; no app request was sent.
34765
34885
  let responsePath = previous?.responsePath;
34766
34886
  let readbackPath;
34767
34887
  const savePrivate = (value, name) => {
34768
- const directory = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
34769
- const path2 = join12(directory, name);
34888
+ const path2 = store.artifactPath(draft.id, name);
34770
34889
  writeFileSync8(path2, `${JSON.stringify(value, null, 2)}
34771
34890
  `, { flag: "wx", mode: 384 });
34772
34891
  return path2;
@@ -34791,8 +34910,7 @@ ${responsePath ? `Saved primary response and test inputs: ${responsePath}
34791
34910
  }
34792
34911
  withBaselines(root, (baselines) => baselines.finishPreparation(session.id, draft.appSlug, draft.appId, transport.mode));
34793
34912
  if (write) store.saveRequestAttempt(draft.id, primaryFingerprint, "success");
34794
- const dir = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
34795
- const resultPath = join12(dir, "verification.json");
34913
+ const resultPath = store.artifactPath(draft.id, "verification.json");
34796
34914
  writeFileSync8(resultPath, `${JSON.stringify(executed.output, null, 2)}
34797
34915
  `, { flag: "wx", mode: 384 });
34798
34916
  const prior = store.evidence(draft.id);
@@ -34816,6 +34934,7 @@ function captureTable(state) {
34816
34934
  state.db.exec("CREATE TABLE IF NOT EXISTS discovery_capture (draft_id TEXT PRIMARY KEY REFERENCES discovery_draft(id) ON DELETE CASCADE, scope_id TEXT, job_id TEXT, phase TEXT NOT NULL, entries_json TEXT)");
34817
34935
  state.db.prepare("UPDATE discovery_capture SET draft_id=draft_id WHERE 0").run();
34818
34936
  const columns = state.db.prepare("PRAGMA table_info(discovery_capture)").all();
34937
+ if (!columns.some((column) => column.name === "origins_json")) state.db.exec("ALTER TABLE discovery_capture ADD COLUMN origins_json TEXT");
34819
34938
  if (!columns.some((column) => column.name === "evidence_scope_id")) state.db.exec("ALTER TABLE discovery_capture ADD COLUMN evidence_scope_id TEXT");
34820
34939
  state.db.exec("RELEASE discovery_capture_schema");
34821
34940
  } catch (error62) {
@@ -35104,7 +35223,7 @@ ${detail}
35104
35223
  if (previousEvidence) await client.closeScope(previousEvidence);
35105
35224
  const id2 = randomUUID9();
35106
35225
  state.db.prepare("INSERT INTO discovery_capture (draft_id,scope_id,job_id,phase,entries_json) VALUES (?,?,?,?,NULL) ON CONFLICT(draft_id) DO UPDATE SET scope_id=excluded.scope_id,job_id=excluded.job_id,phase=excluded.phase,entries_json=NULL,evidence_scope_id=excluded.scope_id").run(draft.id, scope2.id, id2, "starting");
35107
- state.db.prepare("UPDATE discovery_capture SET evidence_scope_id=? WHERE draft_id=?").run(scope2.id, draft.id);
35226
+ state.db.prepare("UPDATE discovery_capture SET evidence_scope_id=?,origins_json=? WHERE draft_id=?").run(scope2.id, JSON.stringify(transport.allowedOrigins), draft.id);
35108
35227
  await client.enqueueJob(scope2.id, { job: { id: id2, kind: "begin", label: draft.name, entity: nameParts[0], operation: nameParts[1], effect: effect2, inputs: envelope.verification.inputs } });
35109
35228
  const job = await awaitCaptureJob(client, id2);
35110
35229
  if (job.status !== "delivered" || job.result?.state !== "recording") return fail5(`Recording start is unconfirmed (${job.status}). Do not operate the app. Retry ${command4(invocation, "discover capture start")} after inspecting ${command4(invocation, "discover status")}.
@@ -35182,7 +35301,7 @@ Review: ${command4(invocation, "discover status")}
35182
35301
  }
35183
35302
  withBaselines(root, (baselines) => baselines.stopCapture(draft.id));
35184
35303
  return ok4(`Recording stopped; admission is confirmed closed. ${entries2.length} requests captured; showing up to five ranked candidates. Ranking is a review aid; all metadata remains available.
35185
- ${rankCandidates(entries2, parseDraftEnvelope(source).envelope?.verification?.inputs ?? {}).map(candidateLine).join("\n")}
35304
+ ${rankCandidates(entries2, parseDraftEnvelope(source).envelope?.verification?.inputs ?? {}, String(parseDraftEnvelope(source).envelope?.definition.effect ?? ""), row.origins_json ? JSON.parse(row.origins_json) : []).map(candidateLine).join("\n")}
35186
35305
  Use a cN candidate ID returned in this recording:
35187
35306
  Inspect its retained request or response: ${command4(invocation, "discover capture show <candidate>")}
35188
35307
  Select the causal request for the draft: ${command4(invocation, "discover capture select <candidate>")}
@@ -35262,7 +35381,7 @@ Inspect a returned candidate: ${command4(invocation, `discover capture show c${p
35262
35381
  const result3 = await awaitCaptureJob(client, id2);
35263
35382
  if (result3.status !== "delivered") return fail5(`Auth source probe is ${result3.status}; no source was confirmed. Retry only this probe while evidence is live.
35264
35383
  `, 12);
35265
- const path2 = join12(mkdtempSync2(join12(tmpdir2(), "apilift-auth-source-")), "source.json");
35384
+ const path2 = store.artifactPath(draft.id, "source.json");
35266
35385
  writeFileSync8(path2, `${JSON.stringify(result3.result, null, 2)}
35267
35386
  `, { mode: 384 });
35268
35387
  await client.acknowledgeJob(id2);
@@ -35320,7 +35439,7 @@ var init_discover = __esm({
35320
35439
  // src/output.ts
35321
35440
  import { randomUUID as randomUUID10 } from "node:crypto";
35322
35441
  import { chmod, mkdtemp, open as open3, rename } from "node:fs/promises";
35323
- import { tmpdir as tmpdir3 } from "node:os";
35442
+ import { tmpdir as tmpdir2 } from "node:os";
35324
35443
  import { join as join13 } from "node:path";
35325
35444
  async function completedFile(directory, name, content) {
35326
35445
  const target = join13(directory, name);
@@ -35336,7 +35455,7 @@ async function completedFile(directory, name, content) {
35336
35455
  await chmod(target, 384);
35337
35456
  return target;
35338
35457
  }
35339
- async function finalizeOutput(output2, sink, directory = tmpdir3()) {
35458
+ async function finalizeOutput(output2, sink, directory = tmpdir2()) {
35340
35459
  let stdout = output2.stdout;
35341
35460
  let stderr = output2.stderr;
35342
35461
  const size = () => Buffer.byteLength(stdout) + Buffer.byteLength(stderr);
@@ -35783,7 +35902,7 @@ var init_main2 = __esm({
35783
35902
 
35784
35903
  // src/plugin.ts
35785
35904
  import { createHash as createHash6 } from "node:crypto";
35786
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
35905
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
35787
35906
  import { homedir as homedir2 } from "node:os";
35788
35907
  import { dirname as dirname8, join as join14, resolve as resolve7 } from "node:path";
35789
35908
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -35885,7 +36004,7 @@ function ensureAgentPlugin(agent2, env, sourceOverride) {
35885
36004
  const current = marker?.bundleDigest === bundleDigest && marker.version === version2 && completeCopy(source, plugin);
35886
36005
  if (!current) {
35887
36006
  const staging = `${plugin}.stage-${process.pid}`;
35888
- rmSync4(staging, { recursive: true, force: true });
36007
+ rmSync5(staging, { recursive: true, force: true });
35889
36008
  mkdirSync10(dirname8(staging), { recursive: true });
35890
36009
  cpSync(source, staging, { recursive: true });
35891
36010
  const manifestPath = join14(staging, `.${agent2}-plugin`, "plugin.json");
@@ -35902,7 +36021,7 @@ function ensureAgentPlugin(agent2, env, sourceOverride) {
35902
36021
  if (existsSync5(backup)) renameSync4(backup, plugin);
35903
36022
  throw error62;
35904
36023
  }
35905
- rmSync4(backup, { recursive: true, force: true });
36024
+ rmSync5(backup, { recursive: true, force: true });
35906
36025
  }
35907
36026
  const marketplacePath = join14(marketplace, "marketplace.json");
35908
36027
  const document = readJson(marketplacePath) ?? { name: "personal", interface: { displayName: "Personal" }, plugins: [] };
@@ -36129,7 +36248,7 @@ var init_analytics2 = __esm({
36129
36248
  });
36130
36249
 
36131
36250
  // src/usage/ownership.ts
36132
- import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync5, unlinkSync as unlinkSync4, statSync as statSync3 } from "node:fs";
36251
+ import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync5, unlinkSync as unlinkSync3, statSync as statSync3 } from "node:fs";
36133
36252
  import { createHash as createHash7, randomUUID as randomUUID12 } from "node:crypto";
36134
36253
  import { join as join15, dirname as dirname9, isAbsolute as isAbsolute2 } from "node:path";
36135
36254
  import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
@@ -36143,7 +36262,7 @@ function requestCollectorTarget(root, entry2, env) {
36143
36262
  renameSync5(pending, targetFile(root));
36144
36263
  } finally {
36145
36264
  try {
36146
- unlinkSync4(pending);
36265
+ unlinkSync3(pending);
36147
36266
  } catch {
36148
36267
  }
36149
36268
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apilift",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Use logged-in apps and discover missing actions with Apilift.",
5
5
  "author": {
6
6
  "name": "Apilift"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apilift",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Use logged-in apps and discover missing actions with Apilift.",
5
5
  "author": {
6
6
  "name": "Apilift"
@@ -21,6 +21,6 @@ Check the selected file with `apilift discover check`; it sends no app request a
21
21
 
22
22
  ## Recover and finish
23
23
 
24
- Use `apilift discover status` to recover the selected draft, result path, recording phase, or pending publication. A lost publication response is reconciled with the exact retained `discover publish` request before editing, deferring, or discarding. `discover rebase` is for a version conflict after reviewing the current resource. `discover defer` retains owned work, `discover discard` abandons only its unpublished draft, and `discover close` leaves the app when owned in-flight work has settled. A stopped recording must be confirmed or canceled before changing drafts or tabs. Follow [recovery.md](references/recovery.md) before treating a failed route as a terminal blocker.
24
+ Use `apilift discover status` to recover the selected draft, result path, recording phase, or pending publication. A lost publication response is reconciled with the exact retained `discover publish` request before editing, deferring, or discarding. `discover rebase` is for a version conflict after reviewing the current resource. `discover defer` retains owned work, `discover discard` abandons only its unpublished draft, and `discover close` abandons that session’s selected and deferred drafts and leaves the app when owned in-flight work has settled. Publication and abandonment delete that draft and temporary verification/capture evidence. Use `discover defer` before switching resources to preserve unfinished work and its evidence while discovery stays open; resuming it requires no new capture. Closing discovery ends that retention and preserves other sessions’ drafts. A stopped recording must be confirmed or canceled before changing drafts or tabs. Follow [recovery.md](references/recovery.md) before treating a failed route as a terminal blocker.
25
25
 
26
26
  Completion means the requested actions are published and the original authorized outcome is handled, or a specific human-only or external prerequisite is identified. Review consumer descriptions for behavior, consequences and limits: author verification establishes publication evidence, while published execution validates its response without automatic readback. Do not prescribe routine browser inspection. Consumers decide whether their task needs additional verification. Inspect published consumer help from a fresh session and obtain required values through its published source actions; the author's local verification inputs are not consumer coverage. Do not send an extra mutation merely to demonstrate publication. Report actual verification, authorized test state, one optional cleanup attempt if relevant, and remaining gaps.
@@ -12,7 +12,7 @@ Select the useful response subtree with `response.path` when needed and validate
12
12
 
13
13
  During discovery, a mutation needs a separate read-only proof request and meaningful comparisons to the intended state. A proof failure leaves the write possibly applied. Repair the proof and use `apilift discover run --proof-only` against the saved primary response; this sends only the readback. Never repeat a possibly sent write just to get stronger proof. For a delete, an explicit absence proof may establish success. A readback or probe using POST may declare semantic `effect: "read"`; method alone is not the effect.
14
14
 
15
- Bind a returned identifier in the proof URL with `{result./id}`, or in its JSON body/query with `{"$result":"/id"}`; result pointers refer to the projected primary result. For a nested collection, use `proof.select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}` and compare `equals` paths relative to its single matching member. The selector must match exactly one item. Check identity and the fields that establish the intended effect, not an entire server-maintained object or a fixed array position. JSON Pointers are literal, not templates. Prefer a targeted detail or search read; absence from a partial list does not prove deletion.
15
+ Bind a returned identifier in the proof URL with `{result./id}`, or in its JSON body/query with `{"$result":"/id"}`; result pointers refer to the projected primary result. For a nested collection, use `proof.select: {"path":"/ticket/fields","where":[{"path":"/id","result":"/id"}]}` and compare `equals` paths relative to its single matching member. The selector must match exactly one item. Check identity and the fields that establish the intended effect, not an entire server-maintained object or a fixed array position. For a dynamic object key, use `{"attributes":{"{input.attribute_id}":{"$input":"value"}}}` in the request body. A proof path such as `/attributes/{input.attribute_id}` substitutes one complete pointer segment and escapes `/` and `~` in its value; other pointer segments remain literal. Prefer a targeted detail or search read; absence from a partial list does not prove deletion.
16
16
 
17
17
  When the task needs a few records from a large collection, look for native search, filters or a detail endpoint in documentation or observed UI requests before enumerating the list. Expose observed parameters and verify that they narrow the results. If only a full response is available, search its saved file and extract matching records or fields; do not load the entire catalog into context. Keep stable identity and summary fields in list results; inspect variable configuration only for the selected item. Preserve dynamic configuration as structured JSON with an appropriate schema, rather than enumerating catalog entries or removing validation to pass.
18
18
 
@@ -4,4 +4,4 @@
4
4
  2. Read the complete bounded diagnostic and exact continuation. Use `apilift doctor`, selected CLI help or the editable file only for the uncertainty at hand. Retry after a relevant state change or an explicitly safe recovery instruction; never infer success from a shell wrapper without the process exit status.
5
5
  3. Reassess the requested outcome. A local command, browser or credential failure is evidence about that route, not proof that the app lacks the capability. Continue through another authorized documented route or independent action when it can advance the outcome without weakening proof or policy.
6
6
  4. Ask the person only for login, MFA, CAPTCHA, WebAuthn, credential provisioning in the secure UI, new permission or a material scope choice they alone can supply. Do not ask them to paste a secret into chat or a command.
7
- 5. If no authorized route remains, report the precise missing outcome, retained owned state, materially different routes checked and exact prerequisite. Use `discover defer` to retain retryable work and `discover close` only after selected in-flight work is settled.
7
+ 5. If no authorized route remains, report the precise missing outcome, retained owned state, materially different routes checked and exact prerequisite. Use `discover defer` to retain retryable work while continuing discovery. `discover close` abandons the session’s selected and deferred drafts after selected in-flight work is settled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apilift/cli",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "engines": {
37
37
  "node": ">=24.16.0 <27"
38
38
  },
39
- "gitHead": "c42fa5b76367755fd456e083943e10356591aa33",
39
+ "gitHead": "7fcce0197a43f41c8f4e5dfa909650baf4be49de",
40
40
  "repository": {
41
41
  "type": "git",
42
42
  "url": "git+https://github.com/egor-sergeev/apilift.git",