@apilift/cli 1.4.39 → 1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc

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.
@@ -27953,7 +27953,7 @@ function parseUrl(value) {
27953
27953
  function loopback(host) {
27954
27954
  return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
27955
27955
  }
27956
- var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
27956
+ var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofPointer, proofComparison, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
27957
27957
  var init_schema = __esm({
27958
27958
  "../../packages/core/dist/definitions/schema.js"() {
27959
27959
  "use strict";
@@ -28011,9 +28011,13 @@ 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
- proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), equals: external_exports.array(external_exports.union([external_exports.strictObject({ path: pointerSchema, input: inputNameSchema }), external_exports.strictObject({ path: pointerSchema, result: pointerSchema }), external_exports.strictObject({ path: pointerSchema, value: valueSchema })])).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v2, ctx) => {
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 })]);
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) => {
28015
28017
  if (!!v2.equals === !!v2.absent)
28016
28018
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
28019
+ if (v2.select && v2.absent)
28020
+ ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
28017
28021
  if (!v2.effect && !["GET", "HEAD"].includes(v2.request.method))
28018
28022
  ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
28019
28023
  });
@@ -28350,7 +28354,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
28350
28354
  const transport = async (input2, init) => {
28351
28355
  const method = init?.method ?? "GET";
28352
28356
  const headers = new Headers(init?.headers);
28353
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
28357
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
28354
28358
  headers.set("Apilift-Client", `cli/${cliVersion}`);
28355
28359
  if (token) headers.set("Authorization", `Bearer ${token}`);
28356
28360
  if (sessionId) headers.set("Apilift-Session", sessionId);
@@ -28454,6 +28458,9 @@ var init_baselines = __esm({
28454
28458
  startPreparation(sessionId, appSlug, at = /* @__PURE__ */ new Date()) {
28455
28459
  this.ledger.db.prepare("INSERT OR IGNORE INTO baseline_preparation_start (session_id,app_slug,started_at) VALUES (?,?,?)").run(sessionId, appSlug, at.toISOString());
28456
28460
  }
28461
+ clearPreparation(sessionId, appSlug) {
28462
+ this.ledger.db.prepare("DELETE FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").run(sessionId, appSlug);
28463
+ }
28457
28464
  finishPreparation(sessionId, appSlug, appId, method, at = /* @__PURE__ */ new Date()) {
28458
28465
  const row = this.ledger.db.prepare("SELECT started_at FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").get(sessionId, appSlug);
28459
28466
  if (!row || this.ledger.db.prepare("SELECT 1 FROM baseline_candidate WHERE draft_id=?").get(`preparation:${sessionId}:${appId}:${method}`)) return;
@@ -29457,9 +29457,13 @@ 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 proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), equals: external_exports.array(external_exports.union([external_exports.strictObject({ path: pointerSchema, input: inputNameSchema }), external_exports.strictObject({ path: pointerSchema, result: pointerSchema }), external_exports.strictObject({ path: pointerSchema, value: valueSchema })])).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
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 })]);
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) => {
29461
29463
  if (!!v.equals === !!v.absent)
29462
29464
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
29465
+ if (v.select && v.absent)
29466
+ ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
29463
29467
  if (!v.effect && !["GET", "HEAD"].includes(v.request.method))
29464
29468
  ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
29465
29469
  });
@@ -29559,7 +29563,7 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
29559
29563
  continue;
29560
29564
  if (proof && token.startsWith("result.") && pointerSchema.safeParse(token.slice(7)).success)
29561
29565
  continue;
29562
- issues.push({ path: path2, message: `Unknown template ${match[0]}` });
29566
+ issues.push({ path: path2, message: `Unknown template ${match[0]}${proof ? '; result URLs use {result./id}, and JSON values use {"$result":"/id"}' : ""}` });
29563
29567
  }
29564
29568
  };
29565
29569
  const walk = (value, path2) => {
@@ -29630,6 +29634,9 @@ function validateDefinition(kind, name, value) {
29630
29634
  for (const [i, comparison] of (definition2.proof?.equals ?? []).entries())
29631
29635
  if ("input" in comparison && !names2.includes(comparison.input))
29632
29636
  issues.push({ path: `/proof/equals/${i}/input`, message: "Unknown input" });
29637
+ for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
29638
+ if ("input" in comparison && !names2.includes(comparison.input))
29639
+ issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
29633
29640
  if (definition2.response?.generated && !generated.has(definition2.response.generated))
29634
29641
  issues.push({ path: "/response/generated", message: "Unknown generated value" });
29635
29642
  }
@@ -29814,7 +29821,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
29814
29821
  const transport = async (input2, init) => {
29815
29822
  const method = init?.method ?? "GET";
29816
29823
  const headers = new Headers(init?.headers);
29817
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
29824
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
29818
29825
  headers.set("Apilift-Client", `cli/${cliVersion}`);
29819
29826
  if (token) headers.set("Authorization", `Bearer ${token}`);
29820
29827
  if (sessionId) headers.set("Apilift-Session", sessionId);
package/dist/apilift.js CHANGED
@@ -28017,7 +28017,7 @@ function parseUrl(value) {
28017
28017
  function loopback(host) {
28018
28018
  return host === "localhost" || host === "127.0.0.1" || host === "[::1]";
28019
28019
  }
28020
- var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
28020
+ var import__, resourceKinds, resourceKindSchema, segmentSchema, appSlugSchema, actionNameSchema, generatedNameSchema, inputNameSchema, pointerSchema, categorySlugs, categorySchema, title, description, requiredDescription, originSchema, httpUrlSchema, requestUrlSchema, ajv, jsonSchema, valueSchema, requestSchema, responseSchema, proofPointer, proofComparison, proofSchema, appDefinitionSchema, entityDefinitionSchema, generatedSchema, executableActionSchema, unavailableActionSchema, actionDefinitionSchema, locatorSchema, observeSchema, injectSchema, credentialSchema, probeSchema, transportDefinitionSchema, browserContextDefinitionSchema, definitionSchemas;
28021
28021
  var init_schema = __esm({
28022
28022
  "../../packages/core/dist/definitions/schema.js"() {
28023
28023
  "use strict";
@@ -28075,9 +28075,13 @@ 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
- proofSchema = external_exports.strictObject({ request: requestSchema, effect: external_exports.literal("read").optional(), equals: external_exports.array(external_exports.union([external_exports.strictObject({ path: pointerSchema, input: inputNameSchema }), external_exports.strictObject({ path: pointerSchema, result: pointerSchema }), external_exports.strictObject({ path: pointerSchema, value: valueSchema })])).min(1).optional(), absent: external_exports.literal(true).optional() }).superRefine((v, ctx) => {
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 })]);
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) => {
28079
28081
  if (!!v.equals === !!v.absent)
28080
28082
  ctx.addIssue({ code: "custom", message: "Exactly one of equals and absent is required" });
28083
+ if (v.select && v.absent)
28084
+ ctx.addIssue({ code: "custom", path: ["select"], message: "Collection selection cannot prove absence; use a targeted HTTP-404 read" });
28081
28085
  if (!v.effect && !["GET", "HEAD"].includes(v.request.method))
28082
28086
  ctx.addIssue({ code: "custom", path: ["request", "method"], message: "Proof request must be GET or HEAD unless effect is read" });
28083
28087
  });
@@ -28411,7 +28415,7 @@ function inspectRequest(request2, inputs, generated, proof, base2, issues) {
28411
28415
  continue;
28412
28416
  if (proof && token.startsWith("result.") && pointerSchema.safeParse(token.slice(7)).success)
28413
28417
  continue;
28414
- issues.push({ path: path2, message: `Unknown template ${match[0]}` });
28418
+ issues.push({ path: path2, message: `Unknown template ${match[0]}${proof ? '; result URLs use {result./id}, and JSON values use {"$result":"/id"}' : ""}` });
28415
28419
  }
28416
28420
  };
28417
28421
  const walk = (value, path2) => {
@@ -28482,6 +28486,9 @@ function validateDefinition(kind, name, value) {
28482
28486
  for (const [i, comparison] of (definition2.proof?.equals ?? []).entries())
28483
28487
  if ("input" in comparison && !names2.includes(comparison.input))
28484
28488
  issues.push({ path: `/proof/equals/${i}/input`, message: "Unknown input" });
28489
+ for (const [i, comparison] of (definition2.proof?.select?.where ?? []).entries())
28490
+ if ("input" in comparison && !names2.includes(comparison.input))
28491
+ issues.push({ path: `/proof/select/where/${i}/input`, message: "Unknown input" });
28485
28492
  if (definition2.response?.generated && !generated.has(definition2.response.generated))
28486
28493
  issues.push({ path: "/response/generated", message: "Unknown generated value" });
28487
28494
  }
@@ -28806,7 +28813,7 @@ function createCliApi(baseUrl, fetcher = fetch, token, sessionId) {
28806
28813
  const transport = async (input2, init) => {
28807
28814
  const method = init?.method ?? "GET";
28808
28815
  const headers = new Headers(init?.headers);
28809
- headers.set("Apilift-Capabilities", "distributed-rollouts-v1");
28816
+ headers.set("Apilift-Capabilities", "distributed-rollouts-v1,discovery-proof-v2");
28810
28817
  headers.set("Apilift-Client", `cli/${cliVersion}`);
28811
28818
  if (token) headers.set("Authorization", `Bearer ${token}`);
28812
28819
  if (sessionId) headers.set("Apilift-Session", sessionId);
@@ -28961,7 +28968,7 @@ var init_registry = __esm({
28961
28968
  contextClear: { path: ["context", "clear"], min: 0, max: 0, values: ["app"] },
28962
28969
  discoverDraft: { path: ["discover", "draft"], min: 1, max: 2 },
28963
28970
  discoverCheck: { path: ["discover", "check"], min: 0, max: 0 },
28964
- discoverRun: { path: ["discover", "run"], min: 0, max: 0 },
28971
+ discoverRun: { path: ["discover", "run"], min: 0, max: 0, flags: ["proof-only"] },
28965
28972
  discoverPublish: { path: ["discover", "publish"], min: 0, max: 0 },
28966
28973
  discoverStatus: { path: ["discover", "status"], min: 0, max: 0 },
28967
28974
  discoverDefer: { path: ["discover", "defer"], min: 0, max: 0 },
@@ -29230,6 +29237,9 @@ var init_baselines = __esm({
29230
29237
  startPreparation(sessionId, appSlug, at = /* @__PURE__ */ new Date()) {
29231
29238
  this.ledger.db.prepare("INSERT OR IGNORE INTO baseline_preparation_start (session_id,app_slug,started_at) VALUES (?,?,?)").run(sessionId, appSlug, at.toISOString());
29232
29239
  }
29240
+ clearPreparation(sessionId, appSlug) {
29241
+ this.ledger.db.prepare("DELETE FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").run(sessionId, appSlug);
29242
+ }
29233
29243
  finishPreparation(sessionId, appSlug, appId, method, at = /* @__PURE__ */ new Date()) {
29234
29244
  const row = this.ledger.db.prepare("SELECT started_at FROM baseline_preparation_start WHERE session_id=? AND app_slug=?").get(sessionId, appSlug);
29235
29245
  if (!row || this.ledger.db.prepare("SELECT 1 FROM baseline_candidate WHERE draft_id=?").get(`preparation:${sessionId}:${appId}:${method}`)) return;
@@ -30784,7 +30794,7 @@ function draftTemplate(kind) {
30784
30794
  ""
30785
30795
  ].join("\n") : "";
30786
30796
  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" : "";
30787
- 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// 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// 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' : "";
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' : "";
30788
30798
  return `${head}${probeGuidance}${tabGuidance}${captureGuidance}${JSON.stringify(templates[kind], null, 2)}
30789
30799
  `;
30790
30800
  }
@@ -30803,8 +30813,8 @@ var init_envelope = __esm({
30803
30813
 
30804
30814
  // src/discovery/drafts.ts
30805
30815
  import { randomUUID as randomUUID4 } from "node:crypto";
30806
- import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
30807
- import { dirname as dirname2, join as join5 } from "node:path";
30816
+ import { chmodSync as chmodSync4, existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
30817
+ import { basename, dirname as dirname2, join as join5 } from "node:path";
30808
30818
  function pathFor(root, appSlug, kind, name) {
30809
30819
  if (!slugPattern.test(appSlug)) throw new Error("App slug is invalid");
30810
30820
  if (kind === "action" ? !actionPattern.test(name) : kind !== "app" && !resourcePattern.test(name)) throw new Error("Resource name is invalid");
@@ -30860,12 +30870,65 @@ var init_drafts = __esm({
30860
30870
  if (!slugPattern.test(appSlug)) throw new Error("App slug is invalid");
30861
30871
  const current = this.session(sessionId);
30862
30872
  if (current && current.appSlug !== appSlug) throw new DraftConflict("selected-draft", `Discovery is open for ${current.appSlug}; close it before opening another app`);
30863
- this.state.db.prepare("INSERT INTO discovery_session (session_id,app_slug,app_id,title) VALUES (?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET app_id=excluded.app_id,title=excluded.title").run(sessionId, appSlug, appId, title2);
30864
- return this.session(sessionId);
30873
+ const moved = [];
30874
+ this.state.db.exec("BEGIN IMMEDIATE");
30875
+ try {
30876
+ const stale = this.retained(appSlug).filter((draft) => draft.appId !== appId);
30877
+ for (const draft of stale) {
30878
+ if (draft.ownerSessionId && draft.ownerSessionId !== sessionId) throw new DraftConflict("owned-draft", `The previous app's ${draft.kind} ${draft.name} is owned by another agent; that agent must finish or defer it before reopening this slug`);
30879
+ if (this.publication(draft.id)) throw new DraftConflict("selected-draft", "A previous app publication may have been sent; its owner must reconcile discover publish before reopening this slug");
30880
+ if (this.hasTable("discovery_capture")) {
30881
+ const capture = this.state.db.prepare("SELECT scope_id FROM discovery_capture WHERE draft_id=?").get(draft.id);
30882
+ if (capture?.scope_id) throw new DraftConflict("selected-draft", "A previous app recording is still active or unconfirmed; its owner must settle discover capture cancel before reopening this slug");
30883
+ }
30884
+ }
30885
+ const previousSessions = this.state.db.prepare("SELECT session_id FROM discovery_session WHERE app_slug=? AND app_id IS NOT ?").all(appSlug, appId);
30886
+ const changed = stale.length > 0 || previousSessions.length > 0;
30887
+ const archivedDrafts = changed ? join5(this.root, "retired-drafts", randomUUID4()) : void 0;
30888
+ if (archivedDrafts) mkdirSync5(archivedDrafts, { recursive: true, mode: 448 });
30889
+ for (const draft of stale) {
30890
+ if (existsSync2(draft.filePath)) {
30891
+ const archived = join5(archivedDrafts, paths[draft.kind], basename(draft.filePath));
30892
+ mkdirSync5(dirname2(archived), { recursive: true, mode: 448 });
30893
+ renameSync(draft.filePath, archived);
30894
+ moved.push({ from: draft.filePath, to: archived });
30895
+ }
30896
+ for (const table of ["selected_discovery_draft", "discovery_publication", "discovery_evidence", "discovery_attempt", "discovery_capture"]) {
30897
+ if (this.hasTable(table)) this.state.db.prepare(`DELETE FROM ${table} WHERE draft_id=?`).run(draft.id);
30898
+ }
30899
+ this.state.db.prepare("DELETE FROM discovery_draft WHERE id=?").run(draft.id);
30900
+ }
30901
+ if (changed && this.hasTable("discovery_research")) {
30902
+ const research = this.state.db.prepare("SELECT file_path FROM discovery_research WHERE app_slug=?").all(appSlug);
30903
+ for (const row of research) if (existsSync2(row.file_path)) {
30904
+ const archived = join5(archivedDrafts, "api", basename(row.file_path));
30905
+ mkdirSync5(dirname2(archived), { recursive: true, mode: 448 });
30906
+ renameSync(row.file_path, archived);
30907
+ moved.push({ from: row.file_path, to: archived });
30908
+ }
30909
+ if (this.hasTable("selected_discovery_research")) this.state.db.prepare("DELETE FROM selected_discovery_research WHERE app_slug=?").run(appSlug);
30910
+ this.state.db.prepare("DELETE FROM discovery_research WHERE app_slug=?").run(appSlug);
30911
+ }
30912
+ this.state.db.prepare("DELETE FROM discovery_session WHERE app_slug=? AND app_id IS NOT ?").run(appSlug, appId);
30913
+ this.state.db.prepare("INSERT INTO discovery_session (session_id,app_slug,app_id,title) VALUES (?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET app_id=excluded.app_id,title=excluded.title").run(sessionId, appSlug, appId, title2);
30914
+ this.state.db.exec("COMMIT");
30915
+ if (changed) withBaselines(this.root, (baselines) => {
30916
+ for (const draft of stale) baselines.closeObservation(draft.id, "app-identity-changed");
30917
+ for (const id2 of /* @__PURE__ */ new Set([sessionId, ...previousSessions.map((row) => row.session_id)])) baselines.clearPreparation(id2, appSlug);
30918
+ });
30919
+ return { ...this.session(sessionId), ...archivedDrafts ? { archivedDrafts } : {} };
30920
+ } catch (error62) {
30921
+ this.state.db.exec("ROLLBACK");
30922
+ for (const { from, to } of moved.reverse()) renameSync(to, from);
30923
+ throw error62;
30924
+ }
30925
+ }
30926
+ hasTable(name) {
30927
+ return Boolean(this.state.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
30865
30928
  }
30866
30929
  setAppId(appSlug, appId) {
30867
- this.state.db.prepare("UPDATE discovery_session SET app_id=? WHERE app_slug=?").run(appId, appSlug);
30868
- this.state.db.prepare("UPDATE discovery_draft SET app_id=? WHERE app_slug=?").run(appId, appSlug);
30930
+ this.state.db.prepare("UPDATE discovery_session SET app_id=? WHERE app_slug=? AND app_id IS NULL").run(appId, appSlug);
30931
+ this.state.db.prepare("UPDATE discovery_draft SET app_id=? WHERE app_slug=? AND app_id IS NULL").run(appId, appSlug);
30869
30932
  }
30870
30933
  close(sessionId) {
30871
30934
  const current = this.session(sessionId);
@@ -30909,6 +30972,18 @@ var init_drafts = __esm({
30909
30972
  saveAttempt(draftId, fingerprint2, state) {
30910
30973
  this.state.db.prepare("INSERT INTO discovery_attempt (draft_id,fingerprint,state,recorded_at) VALUES (?,?,?,?) ON CONFLICT(draft_id) DO UPDATE SET fingerprint=excluded.fingerprint,state=excluded.state,recorded_at=excluded.recorded_at").run(draftId, fingerprint2, state, (/* @__PURE__ */ new Date()).toISOString());
30911
30974
  }
30975
+ requestAttempts() {
30976
+ this.state.db.exec("CREATE TABLE IF NOT EXISTS discovery_request_attempt (draft_id TEXT NOT NULL REFERENCES discovery_draft(id) ON DELETE CASCADE, fingerprint TEXT NOT NULL, state TEXT NOT NULL, response_path TEXT, response_digest TEXT, PRIMARY KEY(draft_id,fingerprint))");
30977
+ }
30978
+ requestAttempt(draftId, fingerprint2) {
30979
+ this.requestAttempts();
30980
+ const row = this.state.db.prepare("SELECT state,response_path,response_digest FROM discovery_request_attempt WHERE draft_id=? AND fingerprint=?").get(draftId, fingerprint2);
30981
+ return row ? { state: row.state, responsePath: row.response_path, responseDigest: row.response_digest } : null;
30982
+ }
30983
+ saveRequestAttempt(draftId, fingerprint2, state, responsePath, responseDigest) {
30984
+ this.requestAttempts();
30985
+ 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
+ }
30912
30987
  discard(sessionId) {
30913
30988
  const draft = this.selected(sessionId);
30914
30989
  if (!draft) return null;
@@ -30958,6 +31033,7 @@ var init_drafts = __esm({
30958
31033
  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`);
30959
31034
  const existing = this.state.db.prepare("SELECT * FROM discovery_draft WHERE app_slug=? AND kind=? AND name=?").get(input2.appSlug, input2.kind, name);
30960
31035
  if (existing?.owner_session_id && existing.owner_session_id !== input2.sessionId) throw new DraftConflict("owned-draft", `${input2.kind} ${name} is owned by another agent session; its file was preserved`);
31036
+ if (existing && existing.app_id !== (input2.appId ?? null)) throw new DraftConflict("selected-draft", "The retained draft belongs to a different app; reopen the app from its current registry identity before editing");
30961
31037
  if (existing) filePath = String(existing.file_path);
30962
31038
  const id2 = existing ? String(existing.id) : randomUUID4();
30963
31039
  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);
@@ -32367,7 +32443,7 @@ var init_vault = __esm({
32367
32443
 
32368
32444
  // src/commands/run.ts
32369
32445
  import { randomUUID as randomUUID7 } from "node:crypto";
32370
- import { readFileSync as readFileSync7, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
32446
+ import { readFileSync as readFileSync7, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
32371
32447
  import { dirname as dirname5, resolve as resolve4, join as join10 } from "node:path";
32372
32448
  function pointer2(value, path2) {
32373
32449
  if (path2 === "") return value;
@@ -32375,7 +32451,7 @@ function pointer2(value, path2) {
32375
32451
  let current = value;
32376
32452
  for (const raw of path2.slice(1).split("/")) {
32377
32453
  const name = raw.replaceAll("~1", "/").replaceAll("~0", "~");
32378
- if (typeof current !== "object" || current === null || !(name in current)) throw new Error(`Result path ${path2} is missing`);
32454
+ if (typeof current !== "object" || current === null || !Object.hasOwn(current, name)) throw new Error(`Result path ${path2} is missing`);
32379
32455
  current = current[name];
32380
32456
  }
32381
32457
  return current;
@@ -32584,7 +32660,7 @@ function writeOutput(path2, content) {
32584
32660
  const pending = join10(dirname5(path2), `.apilift-output-${randomUUID7()}`);
32585
32661
  try {
32586
32662
  writeFileSync6(pending, content, { flag: "wx", mode: 384 });
32587
- renameSync(pending, path2);
32663
+ renameSync2(pending, path2);
32588
32664
  } catch (error62) {
32589
32665
  try {
32590
32666
  unlinkSync3(pending);
@@ -32611,7 +32687,19 @@ function projectResponse(body, action, generated) {
32611
32687
  }
32612
32688
  if (response?.schema) {
32613
32689
  const validate2 = ajv3.compile(response.schema);
32614
- if (!validate2(value)) throw new Error("The app response does not match this action's output contract");
32690
+ if (!validate2(value)) {
32691
+ const issues = (validate2.errors ?? []).slice(0, 5).map((error62) => {
32692
+ let actual;
32693
+ try {
32694
+ actual = pointer2(value, error62.instancePath);
32695
+ } catch {
32696
+ actual = void 0;
32697
+ }
32698
+ const type = actual === null ? "null" : Array.isArray(actual) ? "array" : typeof actual;
32699
+ return `${error62.instancePath || "/"}: ${error62.message ?? "schema mismatch"}; actual type ${type}`;
32700
+ });
32701
+ throw new Error(`Response contract mismatch: ${issues.join("; ")}`);
32702
+ }
32615
32703
  }
32616
32704
  return value;
32617
32705
  }
@@ -32688,14 +32776,34 @@ async function sendBrowser(root, appId, contextName, pageOrigin, effect2, transp
32688
32776
  if (!selection.execution) await client.closeScope(scope2.id);
32689
32777
  }
32690
32778
  }
32691
- function proofPassed(proof, body, primary, inputs, status) {
32692
- if (proof.absent) return status === 404;
32693
- if (status < 200 || status >= 300 || !proof.equals) return false;
32694
- return proof.equals.every((comparison) => {
32695
- const actual = pointer2(body, comparison.path);
32779
+ function confirmProof(proof, body, primary, inputs, status) {
32780
+ if (proof.absent) {
32781
+ if (status !== 404) throw new ResponseContractFailure(`Absence proof expected HTTP 404; received HTTP ${status}`);
32782
+ return;
32783
+ }
32784
+ if (status < 200 || status >= 300) throw new ResponseContractFailure(`Readback returned HTTP ${status}`);
32785
+ const matches = (comparison, selected2, missingIsMismatch = false) => {
32696
32786
  const expected = "input" in comparison ? inputs[comparison.input] : "result" in comparison ? pointer2(primary, comparison.result) : comparison.value;
32787
+ let actual;
32788
+ try {
32789
+ actual = pointer2(selected2, comparison.path);
32790
+ } catch (error62) {
32791
+ if (missingIsMismatch) return false;
32792
+ throw error62;
32793
+ }
32697
32794
  return actual !== void 0 && expected !== void 0 && canonicalJson(actual) === canonicalJson(expected);
32698
- });
32795
+ };
32796
+ let selected = body;
32797
+ if (proof.select) {
32798
+ const collection = pointer2(body, proof.select.path);
32799
+ if (!Array.isArray(collection)) throw new ResponseContractFailure(`Readback selection ${proof.select.path || "/"} must be an array`);
32800
+ const found = collection.filter((item) => proof.select.where.every((comparison) => matches(comparison, item, true)));
32801
+ if (found.length !== 1) throw new ResponseContractFailure(`Readback selection matched ${found.length} items; expected exactly one`);
32802
+ selected = found[0];
32803
+ }
32804
+ for (const comparison of proof.equals ?? []) {
32805
+ if (!matches(comparison, selected)) throw new ResponseContractFailure(`Readback comparison failed at ${comparison.path || "/"}`);
32806
+ }
32699
32807
  }
32700
32808
  async function handleRun(invocation, api, state, session, fetcher, root, stdin = () => readFileSync7(0, "utf8"), onReport) {
32701
32809
  const [appName, actionName] = invocation.positionals;
@@ -32768,7 +32876,7 @@ No app request was sent.
32768
32876
  const transportVersion = bundle2.closure.versions.find((item) => item.kind === "transport" && item.name === (action && "transport" in action ? action.transport : ""));
32769
32877
  const transport = transportVersion?.definition;
32770
32878
  if (!action || !("request" in action) || !transport) return fail2("execution-bundle-invalid \u2014 The selected action or transport is missing. No app request was sent.\n", 5);
32771
- if (action.effect !== "read" && !action.proof) return fail2("execution-bundle-invalid \u2014 A mutating action has no readback proof. No app request was sent.\n", 5);
32879
+ if (action.effect !== "read" && !action.proof) return fail2("execution-bundle-invalid \u2014 A mutating action has no discovery proof specification. No app request was sent.\n", 5);
32772
32880
  const selectedCredential = state.useSession(session.id, bundle2.app.id)?.credentials[action.transport];
32773
32881
  if (transport.credentials?.length && !selectedCredential) return fail2(`credential-required \u2014 ${actionName} needs a selected credential for ${action.transport}. Run ${prefix(`credentials list ${appName} ${action.transport}`)}, then credentials use or add. No app request was sent.
32774
32882
  `, 5);
@@ -32840,7 +32948,6 @@ Inspect current action documentation: ${prefix(`actions show ${appName} ${action
32840
32948
  let outcome = "network-error";
32841
32949
  let diagnostic;
32842
32950
  let output2;
32843
- let verified;
32844
32951
  try {
32845
32952
  const primary = await sendRequest(prepared, action.effect);
32846
32953
  if (!primary.response.ok) {
@@ -32849,29 +32956,9 @@ Inspect current action documentation: ${prefix(`actions show ${appName} ${action
32849
32956
  ${action.effect !== "read" && primary.response.status >= 500 ? readRecovery : ""}`, 4, true);
32850
32957
  } else {
32851
32958
  const value = projected(primary.body, action, generated);
32852
- if (action.effect !== "read" && action.proof) {
32853
- const preparedProof = prepareRequest(action.proof.request, transport, inputs.values, generated, value);
32854
- const proof = await timed(timings, "proofMs", () => sendRequest(preparedProof, "read"));
32855
- let passed = false;
32856
- try {
32857
- passed = proofPassed(action.proof, proof.body, value, inputs.values, proof.response.status);
32858
- } catch {
32859
- }
32860
- if (!passed) {
32861
- outcome = "interface-error";
32862
- diagnostic = "proof-failed";
32863
- output2 = fail2(`proof-failed \u2014 ${actionName} was sent, but its readback did not confirm success. ${readRecovery}`, 12, true);
32864
- } else {
32865
- outcome = "success";
32866
- verified = action.proof.absent ? "absence" : "readback";
32867
- output2 = result(`${JSON.stringify(value, null, 2)}
32868
- `, true);
32869
- }
32870
- } else {
32871
- outcome = "success";
32872
- output2 = result(`${JSON.stringify(value, null, 2)}
32959
+ outcome = "success";
32960
+ output2 = result(`${JSON.stringify(value, null, 2)}
32873
32961
  `, true);
32874
- }
32875
32962
  }
32876
32963
  } catch (error62) {
32877
32964
  if (error62 instanceof ResponseContractFailure && dispatchedAt) {
@@ -32890,7 +32977,7 @@ Inspect saved entries: ${prefix(`credentials list ${appName} ${action.transport}
32890
32977
  }
32891
32978
  if (browserExecution.scope) await liveBrowser?.closeScope(browserExecution.scope.id).catch(() => {
32892
32979
  });
32893
- const report = { id: reportId, connectionFingerprint: await hashJson({ transport: transportVersion.executableHash, credential: selectedCredential ?? null, browser: browserPage?.pageOrigin ?? null }), ...bundle2.receipt ? { receiptId: bundle2.receipt.id } : {}, ...outcome === "success" && action.effect !== "read" ? { proof: "passed" } : {}, appId: bundle2.app.id, actionName, actionVersion: actionVersion.version, closureHash: bundle2.closure.hash, purpose: "run", clientFamily: "cli", agent: session.agent, outcome, ...diagnostic ? { diagnostic } : {}, startedAt, ...dispatchedAt ? { dispatchedAt } : {}, finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
32980
+ const report = { id: reportId, connectionFingerprint: await hashJson({ transport: transportVersion.executableHash, credential: selectedCredential ?? null, browser: browserPage?.pageOrigin ?? null }), ...bundle2.receipt ? { receiptId: bundle2.receipt.id } : {}, appId: bundle2.app.id, actionName, actionVersion: actionVersion.version, closureHash: bundle2.closure.hash, purpose: "run", clientFamily: "cli", agent: session.agent, outcome, ...diagnostic ? { diagnostic } : {}, startedAt, ...dispatchedAt ? { dispatchedAt } : {}, finishedAt: (/* @__PURE__ */ new Date()).toISOString() };
32894
32981
  try {
32895
32982
  state.queueExecutionReport(reportId, report);
32896
32983
  onReport?.(reportId);
@@ -32921,21 +33008,33 @@ Inspect saved entries: ${prefix(`credentials list ${appName} ${action.transport}
32921
33008
  saved = readFileSync7(recoveryPath, "utf8") === completeResult;
32922
33009
  } catch {
32923
33010
  }
32924
- output2 = { ...fail2(`output-failed \u2014 The app request completed, but the result could not be saved or selected: ${error62 instanceof Error ? error62.message : "unknown error"}. ${saved ? `Complete result: ${recoveryPath}. Read that local JSON file to recover the returned identifiers` : "The complete result is on stdout"}.${verified ? ` The ${verified} proof completed, but result delivery failed.` : ""} Do not repeat the app mutation.
33011
+ output2 = { ...fail2(`output-failed \u2014 The app request completed, but the result could not be saved or selected: ${error62 instanceof Error ? error62.message : "unknown error"}. ${saved ? `Complete result: ${recoveryPath}. Read that local JSON file to recover the returned identifiers` : "The complete result is on stdout"}. Do not repeat the app mutation.
32925
33012
  `, 12, true), ...saved ? {} : { stdout: completeResult, stdoutJson: true } };
32926
33013
  }
32927
33014
  }
32928
- if (verified && output2.exitCode === 0) {
32929
- output2.stderr += `Verified: ${verified}.
32930
- `;
32931
- output2.verification = verified;
32932
- }
32933
33015
  if (bundle2.selection.fallback && !["prefer-verified", "pinned-selection"].includes(bundle2.selection.replaced?.reason ?? "")) output2.stderr += `Using a previously working revision (${bundle2.selection.replaced?.reason ?? "compatible-fallback"}). Selection recovers automatically when an eligible revision is available.${bundle2.selection.replaced?.until ? ` Protective pause ends ${bundle2.selection.replaced.until}.` : ""} Inspect distribution with ${prefix(`apps history ${appName}`)}.
32934
33016
  `;
32935
33017
  output2.dispatched = Boolean(dispatchedAt);
32936
33018
  saveTimings(timings, performance.now() - commandStarted);
32937
33019
  return output2;
32938
33020
  }
33021
+ function privateEvidence(value, credentials) {
33022
+ const secrets = new Set(credentials.filter(Boolean));
33023
+ const sensitive2 = /^(?:password|secret|access[_-]?token|refresh[_-]?token|token|authorization|cookie|api[_-]?key)$/iu;
33024
+ JSON.stringify(value, (key, child) => {
33025
+ if (sensitive2.test(key) && typeof child === "string" && child) secrets.add(child);
33026
+ return child;
33027
+ });
33028
+ let redacted = false;
33029
+ const text2 = JSON.stringify(value, (_key, child) => {
33030
+ if (typeof child === "string" && (secrets.has(child) || credentials.some((secret) => secret && child.includes(secret)))) {
33031
+ redacted = true;
33032
+ return "[REDACTED]";
33033
+ }
33034
+ return child;
33035
+ });
33036
+ return { value: JSON.parse(text2), redacted };
33037
+ }
32939
33038
  async function executeDraftVerification(input2) {
32940
33039
  const { transport } = input2;
32941
33040
  const probe = input2.kind === "transport" ? transport.probes[input2.probeOrigin ?? ""] : void 0;
@@ -32945,9 +33044,9 @@ async function executeDraftVerification(input2) {
32945
33044
  const schema = input2.kind === "action" ? action.inputs ?? { type: "object", properties: {}, additionalProperties: false } : probe.inputs ?? { type: "object", properties: {}, additionalProperties: false };
32946
33045
  let values;
32947
33046
  let prepared;
32948
- const generated = input2.kind === "action" && action && "generated" in action ? Object.fromEntries(Object.entries(action.generated ?? {}).map(([name, spec]) => [name, `${spec.prefix ?? ""}${randomUUID7()}`])) : {};
33047
+ 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()}`])) : {});
32949
33048
  try {
32950
- values = effectiveInputs(schema, input2.sampleInputs, {}, true).values;
33049
+ values = effectiveInputs(schema, input2.primaryEvidence?.inputs ?? input2.sampleInputs, {}, true).values;
32951
33050
  const request2 = input2.kind === "action" ? action.request : probe.request;
32952
33051
  prepared = prepareRequest(request2, input2.kind === "transport" && input2.probeOrigin ? { ...transport, baseUrl: input2.probeOrigin } : transport, values, generated);
32953
33052
  } catch (error62) {
@@ -32965,11 +33064,12 @@ async function executeDraftVerification(input2) {
32965
33064
  return { ok: false, message: error62 instanceof Error ? error62.message : "Browser page is unavailable", exitCode: 5, dispatched: false };
32966
33065
  }
32967
33066
  }
32968
- let dispatched = false;
33067
+ let dispatched = Boolean(input2.primaryEvidence);
32969
33068
  let dispatchStarted = 0;
32970
33069
  let dispatchAt = null;
32971
33070
  const browserExecution = {};
32972
33071
  let liveBrowser;
33072
+ const knownCredentials = [];
32973
33073
  const sendConfigured = async (request2, effect2) => {
32974
33074
  const allowNonJsonProbe = input2.kind === "transport" && probe?.response.schema === void 0 && probe?.response.path === void 0;
32975
33075
  let credentialDraftId;
@@ -33009,6 +33109,7 @@ async function executeDraftVerification(input2) {
33009
33109
  const installation = input2.state.installation();
33010
33110
  if (!installation) throw new CredentialError("installation-required", "Installation is no longer linked");
33011
33111
  const values2 = await store.read(credential2, { workspaceId: installation.workspaceId, ...credentialDraftId ? { draftId: credentialDraftId } : {}, appId: input2.appId, appTitle: input2.appSlug, transportName: input2.kind === "action" && action && "transport" in action ? action.transport : input2.name, requirements: transport.credentials ?? [], credentialContract: credentialContractKey(transport), destinations: transport.allowedOrigins });
33112
+ knownCredentials.push(...Object.values(values2));
33012
33113
  const headers = new Headers(request2.init.headers);
33013
33114
  for (const requirement2 of transport.credentials ?? []) headers.set(requirement2.header, `${requirement2.prefix ?? ""}${values2[requirement2.name]}`);
33014
33115
  request2 = { ...request2, init: { ...request2.init, headers } };
@@ -33023,9 +33124,13 @@ async function executeDraftVerification(input2) {
33023
33124
  return send(input2.fetcher, request2, allowNonJsonProbe);
33024
33125
  };
33025
33126
  try {
33026
- const primary = await sendConfigured(prepared, input2.kind === "action" ? action.effect : "read");
33027
- if (!primary.response.ok) return { ok: false, message: `The app returned HTTP ${primary.response.status}${input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "" : ". Inspect its state before retrying a mutation"}`, exitCode: 4, dispatched };
33028
- if (input2.kind === "action" && transport.mode === "direct" && dispatchStarted) input2.onDirectBaseline?.(Math.max(0, performance.now() - dispatchStarted), JSON.stringify({ method: prepared.init.method, url: prepared.url.toString(), body: prepared.init.body ?? null }), JSON.stringify(primary.body), dispatchAt, (/* @__PURE__ */ new Date()).toISOString());
33127
+ const primary = input2.primaryEvidence ? { response: new Response(null, { status: input2.primaryEvidence.status }), body: input2.primaryEvidence.body } : await sendConfigured(prepared, input2.kind === "action" ? action.effect : "read");
33128
+ if (!primary.response.ok) return { ok: false, message: `The app returned HTTP ${primary.response.status}${input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "" : ". Inspect its state before retrying a mutation"}`, exitCode: 4, dispatched, clientRejected: primary.response.status >= 400 && primary.response.status < 500 };
33129
+ if (!input2.primaryEvidence && input2.kind === "action" && transport.mode === "direct" && dispatchStarted) input2.onDirectBaseline?.(Math.max(0, performance.now() - dispatchStarted), JSON.stringify({ method: prepared.init.method, url: prepared.url.toString(), body: prepared.init.body ?? null }), JSON.stringify(primary.body), dispatchAt, (/* @__PURE__ */ new Date()).toISOString());
33130
+ if (!input2.primaryEvidence && input2.onPrimary) {
33131
+ const saved = privateEvidence({ status: primary.response.status, body: primary.body, inputs: values, generated }, knownCredentials);
33132
+ input2.onPrimary({ ...saved.value, redacted: saved.redacted });
33133
+ }
33029
33134
  if (input2.kind === "transport") {
33030
33135
  const selected = probe.response.path !== void 0 ? pointer2(primary.body, probe.response.path) : primary.body;
33031
33136
  if (probe.response.schema && !ajv3.compile(probe.response.schema)(selected)) return { ok: false, message: "Probe response does not match its declared schema", exitCode: 12, dispatched };
@@ -33036,12 +33141,17 @@ async function executeDraftVerification(input2) {
33036
33141
  if (executable.effect !== "read" && executable.proof) {
33037
33142
  const preparedProof = prepareRequest(executable.proof.request, transport, values, generated, value);
33038
33143
  const proof = await sendConfigured(preparedProof, "read");
33039
- if (!proofPassed(executable.proof, proof.body, value, values, proof.response.status)) return { ok: false, message: "The mutation was sent, but its readback did not confirm success; inspect the app before another mutation", exitCode: 12, dispatched: true };
33144
+ input2.onReadback?.(privateEvidence(proof.body, knownCredentials).value);
33145
+ try {
33146
+ confirmProof(executable.proof, proof.body, value, values, proof.response.status);
33147
+ } catch (error62) {
33148
+ throw new ResponseContractFailure(error62 instanceof Error ? error62.message : "Readback did not confirm success");
33149
+ }
33040
33150
  return { ok: true, output: value, proof: "passed", dispatched: true };
33041
33151
  }
33042
33152
  return { ok: true, output: value, proof: "not-applicable", dispatched: true };
33043
33153
  } catch (error62) {
33044
- return { ok: false, message: `${(error62 instanceof Error ? error62.message : "The test did not return").replace(/[.\s]+$/u, "")}. ${dispatched ? input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "The read did not return complete evidence" : "The request may have reached the app; inspect its state before another mutation" : "No app request was sent"}`, exitCode: dispatched || error62 instanceof BrowserControlError || ["extension-unavailable", "capture-server-unavailable", "native-host-upgrade-required"].includes(error62?.code ?? "") ? 5 : 2, dispatched };
33154
+ return { ok: false, message: `${(error62 instanceof Error ? error62.message : "The test did not return").replace(/[.\s]+$/u, "")}. ${dispatched ? input2.kind === "transport" || action && "effect" in action && action.effect === "read" ? "The read did not return complete evidence" : "The request may have reached the app; inspect its state before another mutation" : "No app request was sent"}`, exitCode: error62 instanceof ResponseContractFailure ? 12 : dispatched || error62 instanceof BrowserControlError || ["extension-unavailable", "capture-server-unavailable", "native-host-upgrade-required"].includes(error62?.code ?? "") ? 5 : 2, dispatched };
33045
33155
  } finally {
33046
33156
  if (browserExecution.scope) await liveBrowser?.closeScope(browserExecution.scope.id).catch(() => {
33047
33157
  });
@@ -33497,7 +33607,7 @@ Investigate: ${prefix(`apps update ${appName}`)}
33497
33607
  const flags = inputs.requiredNames.map((name) => ` --${name.replaceAll("_", "-")} <chosen-value>`).join("");
33498
33608
  return `${doc.name} \u2014 ${doc.title}
33499
33609
  ${doc.description}
33500
- Effect: ${doc.effect ?? "unknown"}. ${doc.transport ? `Connection: ${doc.transport.mode}${doc.transport.credentials.length ? `; saved fields: ${doc.transport.credentials.map((f) => f.name).join(", ")}` : "; no saved credential required"}. ` : ""}${doc.proof ? `Execution verifies ${doc.proof}; additional validation is your decision.` : ""}
33610
+ Effect: ${doc.effect ?? "unknown"}. ${doc.transport ? `Connection: ${doc.transport.mode}${doc.transport.credentials.length ? `; saved fields: ${doc.transport.credentials.map((f) => f.name).join(", ")}` : "; no saved credential required"}. ` : ""}${doc.proof ? `Discovery tested ${doc.proof}; this run checks the response without automatic readback. Use a published read when the task needs state confirmation.` : ""}
33501
33611
  ${inputs.required.length ? `Required inputs
33502
33612
  ${inputs.required.join("\n")}
33503
33613
  ` : "No required caller inputs.\n"}${inputs.optional.length ? `Optional inputs
@@ -33872,8 +33982,8 @@ var init_capture_derivation = __esm({
33872
33982
  });
33873
33983
 
33874
33984
  // src/commands/discover.ts
33875
- import { existsSync as existsSync4, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync8 } from "node:fs";
33876
- import { randomUUID as randomUUID9 } from "node:crypto";
33985
+ import { existsSync as existsSync4, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "node:fs";
33986
+ import { createHash as createHash5, randomUUID as randomUUID9 } from "node:crypto";
33877
33987
  import { tmpdir as tmpdir2 } from "node:os";
33878
33988
  import { dirname as dirname6, join as join12 } from "node:path";
33879
33989
  async function currentResource(api, appId, kind, name) {
@@ -34028,6 +34138,7 @@ Edit: ${draft.filePath}
34028
34138
  Publish entity ${draft.name.split(".")[0]} and the selected connection before this action. Fill the action meaning, effect, public input descriptions and their value sources. Select a published connection under dependencies.transport; put representative values in verification.inputs. For browser discovery, declare the intended operation before recording, then capture selection will fill observed structure.
34029
34139
 
34030
34140
  ${local}When ready to test the configured request: ${command4(invocation, "discover run")}
34141
+ For a mutation already sent, repair its response/proof and check only readback: ${command4(invocation, "discover run --proof-only")}
34031
34142
  For browser observation: ${command4(invocation, "discover capture start")}
34032
34143
  `;
34033
34144
  if (draft.kind === "transport") {
@@ -34211,26 +34322,32 @@ async function handleDiscover(invocation, api, state, session, fetcher, root, in
34211
34322
  } catch (error62) {
34212
34323
  if (!(error62 instanceof ApiClientError && error62.status === 404)) throw error62;
34213
34324
  }
34325
+ let current;
34214
34326
  try {
34215
- store.start(session.id, slug, null, slug);
34327
+ current = store.start(session.id, slug, null, slug);
34216
34328
  } catch (error62) {
34217
34329
  return fail5(`${error62 instanceof Error ? error62.message : "Could not start discovery"}.
34330
+ Retry: ${command4(invocation, `apps new ${slug}`)}
34218
34331
  `);
34219
34332
  }
34220
34333
  withBaselines(root, (baselines) => baselines.startPreparation(session.id, slug));
34221
- return await openDraft(invocation, api, store, session, root, "app");
34334
+ const opened = await openDraft(invocation, api, store, session, root, "app");
34335
+ if (current.archivedDrafts) opened.stdout = `Drafts for the previous app were archived: ${current.archivedDrafts}
34336
+ ${opened.stdout}`;
34337
+ return opened;
34222
34338
  }
34223
34339
  if (invocation.key === "appsUpdate") {
34224
34340
  const app2 = await api.call("getApp", { params: { app: invocation.positionals[0] } });
34225
34341
  const selected = store.selected(session.id);
34226
- if (selected) return fail5(`This agent is already editing ${selected.kind} ${selected.name} for ${selected.appSlug}. Its draft is preserved. Finish, defer or discard it before opening another app. Inspect ${command4(invocation, "discover status")}.
34342
+ if (selected && (selected.appSlug !== app2.slug || selected.appId === app2.id)) return fail5(`This agent is already editing ${selected.kind} ${selected.name} for ${selected.appSlug}. Its draft is preserved. Finish, defer or discard it before opening another app. Inspect ${command4(invocation, "discover status")}.
34227
34343
  `, 8);
34228
- store.start(session.id, app2.slug, app2.id, app2.title);
34344
+ const current = store.start(session.id, app2.slug, app2.id, app2.title);
34229
34345
  withBaselines(root, (baselines) => baselines.startPreparation(session.id, app2.slug));
34230
34346
  const accessGuidance = app2.transports.some((item) => item.mode === "direct" && Array.isArray(item.credentials) && item.credentials.length) ? `
34231
34347
  For another user's key, inspect the published connection first: ${command4(invocation, `transports show ${app2.slug}`)}. If its issuance steps or permissions are missing, research the app's official access documentation, then revise that same connection with ${command4(invocation, "discover draft transport <published-name>")}. Keep its mode, destination, credential fields and probe unless the official contract changed. Draft an action only when the requested operation itself is missing.
34232
34348
  ` : "";
34233
- return ok4(`Opened ${app2.title} for changes. No draft is selected.
34349
+ return ok4(`${current.archivedDrafts ? `Drafts for the previous app were archived: ${current.archivedDrafts}
34350
+ ` : ""}Opened ${app2.title} for changes. No draft is selected.
34234
34351
  ${accessGuidance}First check whether the requested operation exists: ${command4(invocation, `actions search ${app2.slug} "<keyword>"`)}
34235
34352
 
34236
34353
  Create or edit one resource. ${actionNameHelp}
@@ -34310,7 +34427,7 @@ Keep for later: ${command4(invocation, "discover defer")}
34310
34427
  Abandon unpublished edits: ${command4(invocation, "discover discard")}
34311
34428
  `);
34312
34429
  }
34313
- const retained = store.retained(current.appSlug);
34430
+ const retained = store.retained(current.appSlug).filter((draft2) => draft2.appId === current.appId);
34314
34431
  return ok4(`Editing ${current.title}. No draft is selected.
34315
34432
  ${retained.length ? `Retained files:
34316
34433
  ${retained.map((item) => ` ${item.kind} ${item.name}: ${item.filePath}
@@ -34430,7 +34547,8 @@ async function verificationIdentity(api, checked) {
34430
34547
  }
34431
34548
  const closure = await closureHash(executable, deps);
34432
34549
  const fingerprint3 = await hashJson({ executable, closure, inputs: checked.envelope?.verification?.inputs ?? {} });
34433
- return { executable, closure, fingerprint: fingerprint3 };
34550
+ const primary = await hashJson({ appId: checked.draft.appId, name: checked.draft.name, effect: "effect" in action ? action.effect : null, request: "request" in action ? action.request : null, inputs: "inputs" in action ? action.inputs ? semanticSchema(action.inputs) : null : null, generated: "generated" in action ? action.generated ?? null : null, samples: checked.envelope?.verification?.inputs ?? {}, deps });
34551
+ return { executable, closure, fingerprint: fingerprint3, primary };
34434
34552
  }
34435
34553
  const fingerprint2 = await hashJson({ executable, inputs: checked.envelope?.verification?.inputs ?? {} });
34436
34554
  return { executable, fingerprint: fingerprint2 };
@@ -34485,6 +34603,8 @@ File: ${draft.filePath}
34485
34603
  if (parsed.success && error62.status < 500) {
34486
34604
  store.clearPublication(draft.id);
34487
34605
  const issue2 = parsed.data.error;
34606
+ if (issue2.code === "app-not-found") return fail5(`${issue2.code} \u2014 The published app is no longer accessible. Recheck its current registry identity with ${command4(invocation, `apps new ${draft.appSlug}`)}. Local drafts are preserved.
34607
+ `, cliExitForError(issue2), true);
34488
34608
  if (issue2.code === "version-conflict") return fail5(`The published resource changed after this draft was opened. Your edits are retained. Reconcile with ${command4(invocation, "discover rebase")}. No new version was published.
34489
34609
  `, 8, true);
34490
34610
  if (removing && issue2.code === "dependents-exist" && Array.isArray(issue2.details.dependents)) {
@@ -34577,7 +34697,7 @@ async function rebaseDraft(invocation, api, store, session) {
34577
34697
  const updated = applyEdits(source, modify(source, ["expectedVersion"], current.version, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
34578
34698
  const temporary = `${draft.filePath}.${randomUUID9()}.tmp`;
34579
34699
  writeFileSync8(temporary, updated, { flag: "wx", mode: 384 });
34580
- renameSync2(temporary, draft.filePath);
34700
+ renameSync3(temporary, draft.filePath);
34581
34701
  store.setBaseVersion(draft.id, current.version);
34582
34702
  return ok4(`The published ${label(draft.kind)} ${draft.name} changed. The removal now targets version ${current.version}.
34583
34703
  Review the current definition: ${comparisonPath}
@@ -34618,28 +34738,59 @@ async function verifyDraft(invocation, api, state, store, session, fetcher, root
34618
34738
  const installation = state.installation();
34619
34739
  if (installation) await api.call("acquireSession", { params: { id: apiSessionId(installation.id, session) }, body: { purpose: "discovery", appId: draft.appId } });
34620
34740
  const write = Boolean(action && "effect" in action && action.effect !== "read");
34621
- const previous = store.attempt(draft.id);
34622
- if (write && previous?.fingerprint === identity2.fingerprint && previous.state === "success") {
34623
- const evidence = store.evidence(draft.id);
34624
- return evidence?.fingerprint === identity2.fingerprint ? ok4(`This configured mutation was already tested successfully. Saved output: ${evidence.resultPath}
34741
+ const proofOnly = invocation.flags.includes("proof-only");
34742
+ if (proofOnly && !write) return fail5("--proof-only requires a mutation draft with a saved successful primary response. No app request was sent.\n");
34743
+ const primaryFingerprint = await hashJson({ primary: "primary" in identity2 ? identity2.primary : identity2.fingerprint, browserTab: transport.mode === "browser" ? envelope.capture?.tab || state.browserTab(session.id, draft.appId) || null : null, credential: state.useSession(session.id, draft.appId)?.credentials[action && "transport" in action ? action.transport : draft.name] ?? null });
34744
+ const previous = store.requestAttempt(draft.id, primaryFingerprint);
34745
+ const legacy = store.attempt(draft.id);
34746
+ const proofCommand = command4(invocation, "discover run --proof-only");
34747
+ const evidence = store.evidence(draft.id);
34748
+ if (write && !proofOnly && evidence?.fingerprint === identity2.fingerprint && (previous?.state === "success" || legacy?.fingerprint === identity2.fingerprint && legacy.state === "success")) return ok4(`This configured mutation was already tested successfully. Saved output: ${evidence.resultPath}
34625
34749
  Review it and publish; no app request was sent.
34626
- `) : fail5(`This configured mutation was already sent. Its result is unavailable, so do not repeat it. Inspect the app before any new test.
34750
+ `);
34751
+ if (write && !proofOnly && (previous && previous.state !== "client-rejected" || !previous && legacy && legacy.state !== "client-rejected")) return fail5(`This mutation was already sent or may have reached the app. Do not repeat it. ${previous?.responsePath ? `Inspect the saved response: ${previous.responsePath}. After repairing response/proof, check without repeating the write: ${proofCommand}` : "No reusable response is available; inspect the app through an authorized read before choosing another test."}
34627
34752
  `, 12);
34753
+ let primaryEvidence;
34754
+ if (proofOnly) {
34755
+ if (!previous?.responsePath) return fail5("No saved primary response matches this request, inputs and connection. No app request was sent. Restore the original test definition or inspect the app through a read; do not repeat an uncertain write.\n", 12);
34756
+ try {
34757
+ const saved = readFileSync8(previous.responsePath, "utf8");
34758
+ if (createHash5("sha256").update(saved).digest("hex") !== previous.responseDigest) throw new Error("Saved response changed");
34759
+ primaryEvidence = JSON.parse(saved);
34760
+ if (primaryEvidence.redacted) return fail5("Saved evidence contains redacted credentials and cannot be reused as proof. No app request was sent; inspect the intended state through an authorized read.\n", 12);
34761
+ } catch {
34762
+ return fail5("The saved primary response is unavailable. No app request was sent; inspect the app through a read.\n", 12);
34763
+ }
34628
34764
  }
34629
- if (write && previous?.fingerprint === identity2.fingerprint && previous.state === "possibly-dispatched") return fail5(`This configured mutation may already have reached the app. Do not repeat it. Inspect the app and use a changed, authorized test request before running again.
34630
- `, 12);
34631
- if (write) store.saveAttempt(draft.id, identity2.fingerprint, "possibly-dispatched");
34632
- const executed = await executeDraftVerification({ api, state, session, root, fetcher, appId: draft.appId, appSlug: draft.appSlug, kind: draft.kind, name: draft.name, action, transport, commandPrefix: command4(invocation, "").trim(), browserTab: envelope.capture?.tab || void 0, probeOrigin: envelope.verification?.probeOrigin, sampleInputs: envelope.verification?.inputs ?? {}, onDispatch: () => withBaselines(root, (baselines) => baselines.closeObservation(draft.id, "dispatch")), onDirectBaseline: (durationMs, request2, response, startedAt, finishedAt) => {
34765
+ let responsePath = previous?.responsePath;
34766
+ let readbackPath;
34767
+ const savePrivate = (value, name) => {
34768
+ const directory = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
34769
+ const path2 = join12(directory, name);
34770
+ writeFileSync8(path2, `${JSON.stringify(value, null, 2)}
34771
+ `, { flag: "wx", mode: 384 });
34772
+ return path2;
34773
+ };
34774
+ if (write && !proofOnly) store.saveRequestAttempt(draft.id, primaryFingerprint, "possibly-dispatched");
34775
+ const executed = await executeDraftVerification({ api, state, session, root, fetcher, appId: draft.appId, appSlug: draft.appSlug, kind: draft.kind, name: draft.name, action, transport, commandPrefix: command4(invocation, "").trim(), browserTab: envelope.capture?.tab || void 0, probeOrigin: envelope.verification?.probeOrigin, sampleInputs: envelope.verification?.inputs ?? {}, primaryEvidence, onPrimary: (saved) => {
34776
+ responsePath = savePrivate(saved, "primary.json");
34777
+ store.saveRequestAttempt(draft.id, primaryFingerprint, "possibly-dispatched", responsePath, createHash5("sha256").update(readFileSync8(responsePath)).digest("hex"));
34778
+ }, onReadback: (body) => {
34779
+ readbackPath = savePrivate(body, "readback.json");
34780
+ }, onDispatch: () => withBaselines(root, (baselines) => baselines.closeObservation(draft.id, "dispatch")), onDirectBaseline: (durationMs, request2, response, startedAt, finishedAt) => {
34633
34781
  if (draft.kind === "action") withBaselines(root, (baselines) => baselines.recordDirect({ draftId: draft.id, appId: draft.appId, sessionId: session.id, durationMs, request: request2, response, invocationId, startedAt, finishedAt }));
34634
34782
  } });
34635
34783
  if (!executed.ok) {
34636
- if (write && !executed.dispatched) store.saveAttempt(draft.id, identity2.fingerprint, "client-rejected");
34784
+ if (write && !proofOnly && (!executed.dispatched || executed.clientRejected)) store.saveRequestAttempt(draft.id, primaryFingerprint, "client-rejected");
34637
34785
  return fail5(`discover-run-failed \u2014 ${executed.message}.
34638
34786
  File: ${draft.filePath}
34639
- `, executed.exitCode, executed.dispatched);
34787
+ ${responsePath ? `Saved primary response and test inputs: ${responsePath}
34788
+ ` : ""}${readbackPath ? `Saved readback: ${readbackPath}
34789
+ ` : ""}${write && responsePath ? `Repair the response/proof, then check without repeating the write: ${proofCommand}
34790
+ ` : ""}`, executed.exitCode, executed.dispatched);
34640
34791
  }
34641
34792
  withBaselines(root, (baselines) => baselines.finishPreparation(session.id, draft.appSlug, draft.appId, transport.mode));
34642
- if (write) store.saveAttempt(draft.id, identity2.fingerprint, "success");
34793
+ if (write) store.saveRequestAttempt(draft.id, primaryFingerprint, "success");
34643
34794
  const dir = mkdtempSync2(join12(tmpdir2(), "apilift-discover-"));
34644
34795
  const resultPath = join12(dir, "verification.json");
34645
34796
  writeFileSync8(resultPath, `${JSON.stringify(executed.output, null, 2)}
@@ -34647,8 +34798,10 @@ File: ${draft.filePath}
34647
34798
  const prior = store.evidence(draft.id);
34648
34799
  const probes = draft.kind === "transport" ? [...((prior?.fingerprint === identity2.fingerprint ? prior.data.probes : []) ?? []).filter((item) => item.origin !== envelope.verification?.probeOrigin), { origin: envelope.verification.probeOrigin, verifiedAt: (/* @__PURE__ */ new Date()).toISOString() }] : void 0;
34649
34800
  store.saveEvidence(draft.id, identity2.fingerprint, resultPath, probes ? { probes } : { proof: executed.proof });
34801
+ const source = readFileSync8(draft.filePath, "utf8");
34802
+ writeFileSync8(draft.filePath, applyEdits(source, modify(source, ["verification", "review"], "pending", { formattingOptions: { insertSpaces: true, tabSize: 2 } })), { mode: 384 });
34650
34803
  return { stdout: `${JSON.stringify(executed.output, null, 2)}
34651
- `, stderr: `Executed the configured ${draft.kind === "transport" ? "probe" : "action"} ${draft.name} and checked its response contract.
34804
+ `, stderr: `${proofOnly ? "Checked the saved primary response and executed only its readback" : `Executed the configured ${draft.kind === "transport" ? "probe" : "action"} ${draft.name} and checked its response contract`}.${write ? " Discovery readback confirmed the intended state." : ""}
34652
34805
  Saved output: ${resultPath}
34653
34806
 
34654
34807
  Review the actual objects and field meanings against the official docs or UI.
@@ -35207,6 +35360,8 @@ async function finalizeOutput(output2, sink, directory = tmpdir3()) {
35207
35360
  if (size() > displayBudget) {
35208
35361
  throw new Error("output notices exceed display budget");
35209
35362
  }
35363
+ const guidance = "Search the saved file for the needed identifier or name, then extract matching records with jq or focused file reads. Do not load the entire file into context; it contains only fetched data.\n";
35364
+ if (size() + Buffer.byteLength(guidance) <= displayBudget) stderr += guidance;
35210
35365
  }
35211
35366
  if (stdout) sink.stdout(stdout);
35212
35367
  if (stderr) sink.stderr(stderr);
@@ -35229,7 +35384,7 @@ var init_output = __esm({
35229
35384
  // ../../node_modules/.pnpm/tinyexec@1.3.1/node_modules/tinyexec/dist/main.mjs
35230
35385
  import { spawn as spawn2, spawnSync } from "node:child_process";
35231
35386
  import { cwd } from "node:process";
35232
- import { basename, delimiter, dirname as dirname7, normalize, resolve as resolve6 } from "node:path";
35387
+ import { basename as basename2, delimiter, dirname as dirname7, normalize, resolve as resolve6 } from "node:path";
35233
35388
  import { fileURLToPath } from "node:url";
35234
35389
  import { pipeline } from "node:stream/promises";
35235
35390
  import { PassThrough } from "node:stream";
@@ -35299,7 +35454,7 @@ function normalizeSpawnCommand(command6, args = [], options2 = {}) {
35299
35454
  const separatorIndex = line.indexOf(" ");
35300
35455
  const path2 = separatorIndex !== -1 ? line.slice(0, separatorIndex) : line;
35301
35456
  const argument = separatorIndex !== -1 ? line.slice(separatorIndex + 1) : "";
35302
- const binary = basename(path2);
35457
+ const binary = basename2(path2);
35303
35458
  shebang = binary === "env" ? argument || null : binary;
35304
35459
  }
35305
35460
  }
@@ -35627,8 +35782,8 @@ var init_main2 = __esm({
35627
35782
  });
35628
35783
 
35629
35784
  // src/plugin.ts
35630
- import { createHash as createHash5 } from "node:crypto";
35631
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
35785
+ 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";
35632
35787
  import { homedir as homedir2 } from "node:os";
35633
35788
  import { dirname as dirname8, join as join14, resolve as resolve7 } from "node:path";
35634
35789
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -35645,7 +35800,7 @@ function files(root, relative = "") {
35645
35800
  }).sort();
35646
35801
  }
35647
35802
  function digest(root) {
35648
- const hash4 = createHash5("sha256");
35803
+ const hash4 = createHash6("sha256");
35649
35804
  for (const path2 of files(root)) hash4.update(path2).update("\0").update(readFileSync9(join14(root, path2))).update("\0");
35650
35805
  return hash4.digest("hex");
35651
35806
  }
@@ -35740,11 +35895,11 @@ function ensureAgentPlugin(agent2, env, sourceOverride) {
35740
35895
  writeJson(manifestPath, manifest);
35741
35896
  writeJson(join14(staging, markerName), { schema: "apilift.plugin-install/v1", package: "apilift", version: version2, bundleDigest });
35742
35897
  const backup = `${plugin}.backup-${process.pid}`;
35743
- if (existsSync5(plugin)) renameSync3(plugin, backup);
35898
+ if (existsSync5(plugin)) renameSync4(plugin, backup);
35744
35899
  try {
35745
- renameSync3(staging, plugin);
35900
+ renameSync4(staging, plugin);
35746
35901
  } catch (error62) {
35747
- if (existsSync5(backup)) renameSync3(backup, plugin);
35902
+ if (existsSync5(backup)) renameSync4(backup, plugin);
35748
35903
  throw error62;
35749
35904
  }
35750
35905
  rmSync4(backup, { recursive: true, force: true });
@@ -35974,8 +36129,8 @@ var init_analytics2 = __esm({
35974
36129
  });
35975
36130
 
35976
36131
  // src/usage/ownership.ts
35977
- import { mkdirSync as mkdirSync11, chmodSync as chmodSync7, existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync4, statSync as statSync3 } from "node:fs";
35978
- import { createHash as createHash6, randomUUID as randomUUID12 } from "node:crypto";
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";
36133
+ import { createHash as createHash7, randomUUID as randomUUID12 } from "node:crypto";
35979
36134
  import { join as join15, dirname as dirname9, isAbsolute as isAbsolute2 } from "node:path";
35980
36135
  import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
35981
36136
  function requestCollectorTarget(root, entry2, env) {
@@ -35985,7 +36140,7 @@ function requestCollectorTarget(root, entry2, env) {
35985
36140
  try {
35986
36141
  const target = { owner: owner.id, entry: entry2, interpreter: process.execPath, fingerprint: sourceFingerprint(entry2), env: Object.fromEntries(runtimeKeys.map((key) => [key, env[key] ?? null])) };
35987
36142
  writeFileSync10(pending, JSON.stringify(target), { mode: 384 });
35988
- renameSync4(pending, targetFile(root));
36143
+ renameSync5(pending, targetFile(root));
35989
36144
  } finally {
35990
36145
  try {
35991
36146
  unlinkSync4(pending);
@@ -36031,7 +36186,7 @@ var init_ownership = __esm({
36031
36186
  "use strict";
36032
36187
  ownerLifetimeMs = 6e4;
36033
36188
  targetFile = (root) => join15(root, "usage", "collector-target.json");
36034
- sourceFingerprint = (entry2) => createHash6("sha256").update(readFileSync10(entry2)).digest("hex");
36189
+ sourceFingerprint = (entry2) => createHash7("sha256").update(readFileSync10(entry2)).digest("hex");
36035
36190
  runtimeKeys = ["APILIFT_URL", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "APILIFT_BIN", "APILIFT_DEV_PLUGIN_ROOT"];
36036
36191
  ownerFile = (root) => join15(root, "usage", "collector-owner.json");
36037
36192
  }
@@ -38102,7 +38257,7 @@ var init_compat2 = __esm({
38102
38257
  });
38103
38258
 
38104
38259
  // src/native-host/registration.ts
38105
- import { createHash as createHash7, randomUUID as randomUUID13 } from "node:crypto";
38260
+ import { createHash as createHash8, randomUUID as randomUUID13 } from "node:crypto";
38106
38261
  import { execFileSync as execFileSync2 } from "node:child_process";
38107
38262
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "node:fs";
38108
38263
  import { homedir as homedir3, platform as platform2 } from "node:os";
@@ -38249,7 +38404,7 @@ var fingerprint, hostNames, validId, posixQuote, absoluteEnv;
38249
38404
  var init_registration = __esm({
38250
38405
  "src/native-host/registration.ts"() {
38251
38406
  "use strict";
38252
- fingerprint = (bytes) => createHash7("sha256").update(bytes).digest("hex");
38407
+ fingerprint = (bytes) => createHash8("sha256").update(bytes).digest("hex");
38253
38408
  hostNames = /* @__PURE__ */ new Set(["com.apilift.browser", "com.apilift.browser.dev"]);
38254
38409
  validId = /^[a-p]{32}$/;
38255
38410
  posixQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
@@ -38422,7 +38577,7 @@ var init_update = __esm({
38422
38577
  });
38423
38578
 
38424
38579
  // src/setup-check.ts
38425
- import { createHash as createHash8, randomUUID as randomUUID14 } from "node:crypto";
38580
+ import { createHash as createHash9, randomUUID as randomUUID14 } from "node:crypto";
38426
38581
  import { existsSync as existsSync8, readFileSync as readFileSync12, realpathSync as realpathSync3, writeFileSync as writeFileSync13 } from "node:fs";
38427
38582
  import { dirname as dirname12, join as join18 } from "node:path";
38428
38583
  async function setupCheckBody(client, scopeId, value) {
@@ -38432,7 +38587,7 @@ async function setupCheckBody(client, scopeId, value) {
38432
38587
  if (!artifact || typeof artifact.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(artifact.sha256) || !Number.isInteger(artifact.bytes) || Number(artifact.bytes) < 1 || Number(artifact.bytes) > maxCheckBodyBytes || artifact.encoding !== "utf-8") return null;
38433
38588
  try {
38434
38589
  const bytes = await client.blob(scopeId, artifact.sha256);
38435
- if (!bytes || bytes.length !== artifact.bytes || createHash8("sha256").update(bytes).digest("hex") !== artifact.sha256) return null;
38590
+ if (!bytes || bytes.length !== artifact.bytes || createHash9("sha256").update(bytes).digest("hex") !== artifact.sha256) return null;
38436
38591
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
38437
38592
  } catch {
38438
38593
  return null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apilift",
3
- "version": "1.4.39",
3
+ "version": "1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc",
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.4.39",
3
+ "version": "1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc",
4
4
  "description": "Use logged-in apps and discover missing actions with Apilift.",
5
5
  "author": {
6
6
  "name": "Apilift"
@@ -17,10 +17,10 @@ For every requested action, assess current first-party operation and access docu
17
17
 
18
18
  Use `apilift discover draft <kind> [name]` for `app`, `entity`, `transport`, `browser-page`, or `action`. Its response gives a private JSONC path; edit that file. The file contains the published `definition` and local `dependencies`, `verification`, and `capture` settings. App origin is its primary UI location, not every request destination. Publish missing entity and connection resources before an action that depends on them; a browser connection also needs a published browser page. Keep credential values and recurring local context values out of every definition. Read [operation-contract.md](references/operation-contract.md) before defining an action.
19
19
 
20
- Check the selected file with `apilift discover check`; it sends no app request and reports independent field errors. An executable connection or action needs an explicit `apilift discover run` with representative values in `verification.inputs`; a connection also selects `verification.probeOrigin`. Inspect saved output against official documentation or observed app state and set `verification.review` to `accepted` only when its meaning is correct. A schema match or HTTP success alone is insufficient. For a mutation, assess its separate readback proof and never repeat the write merely to improve evidence. Metadata resources need no app execution. `apilift discover publish` publishes the exact selected version after its gates and sends no app request.
20
+ Check the selected file with `apilift discover check`; it sends no app request and reports independent field errors. An executable connection or action needs an explicit `apilift discover run` with representative values in `verification.inputs`; a connection also selects `verification.probeOrigin`. Inspect saved output against official documentation or observed app state and set `verification.review` to `accepted` only when its meaning is correct. A schema match or HTTP success alone is insufficient. For a mutation, assess its separate readback proof and never repeat the write merely to improve evidence. After repairing a failed response or proof, `apilift discover run --proof-only` checks the saved response and sends only the readback. Metadata resources need no app execution. `apilift discover publish` publishes the exact selected version after its gates and sends no app request.
21
21
 
22
22
  ## Recover and finish
23
23
 
24
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.
25
25
 
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: do not turn author verification or automatic runtime proofs into instructions for consumers to repeat readbacks or 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.
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.
@@ -4,13 +4,17 @@ Name a precise domain `entity.verb` for the requested outcome. Before transport
4
4
 
5
5
  An action's published `definition` describes its effect, JSON Schema `inputs`, request, response and, for a mutation, readback proof. `dependencies.transport` selects one published connection and is translated to the action's selected transport at publication. Request fields include method, URL, optional headers, query and JSON body. Use typed `{"$input":"name"}` and encoded `{input.name}` bindings for caller values. Public protocol literals are allowed; captured account identifiers, credential values and local defaults are not. Use generated bindings for fresh request identifiers, never a caller flag invented from a captured identifier.
6
6
 
7
- Public action descriptions state behavior, consequences and limits. Author verification establishes publication evidence; runtime proofs execute automatically; consumers decide whether their task needs additional validation. Do not instruct consumers to repeat those proofs or inspect the browser routinely.
7
+ Public action descriptions state behavior, consequences and limits. Author verification establishes publication evidence; published execution validates the response without automatic readback. Consumers decide whether their task needs state confirmation through a published read; do not prescribe routine browser inspection.
8
8
 
9
9
  Each visible input and response field needs a semantic description; caller inputs also need synthetic examples. A value owned by the app needs a published source action with a compatible selected result path. Producers never run implicitly: the consumer runs the documented source and supplies its selected value. Do not use a prose-only dependency, a published context-variable resource, or the author's local verification value as a source. Per-app scalar `context` defaults are a consumer convenience with exact input-name matching, not an action contract or account model. Ambiguous generic names are passed explicitly.
10
10
 
11
11
  Select the useful response subtree with `response.path` when needed and validate it with `response.schema`; projected fields use paths relative to the selected subtree or array member. Preserve meaningful variants and nullable fields observed in real results. If verification reports a mismatch, inspect the selected value and correct its type or projection rather than weakening the contract to pass. Keep identity and relationship fields needed for further discovery.
12
12
 
13
- A mutation needs a separate read-only proof request and meaningful comparisons to the intended state. A proof failure leaves the write possibly applied; inspect state through an authorized read and 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.
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
+
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.
16
+
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.
14
18
 
15
19
  Put representative local values in `verification.inputs`, run `apilift discover check`, then explicitly `apilift discover run`. Judge the saved output against documentation or observed UI state. Set `verification.review` to `accepted` only after that judgment and `rejected` with notes otherwise; `discover publish` does not execute the app operation again. After publication, inspect `actions show` as a fresh consumer, obtain every required value through published source actions and finish the originally authorized outcome. Do not send a second mutation merely to test publication.
16
20
 
@@ -13,6 +13,6 @@ Use explicit flags for ambiguous values. `apilift context set <name> <scalar> --
13
13
 
14
14
  For a direct connection requiring a saved credential, use the exact `credentials list`, `use`, or `add` continuation printed for that app and connection. Selection belongs to this agent session and does not switch browser login. Only the person uses the secure credential form; never ask for a secret in chat, shell arguments, environment variables, files, or a generic dashboard field. `credentials unbound` and `attach` are explicit recovery paths after a registry reset, never an invitation to match a secret by app name or origin. Run published browser actions directly: Apilift manages its browser connection and selected tab internally. Do not run browser preflight or use agent browser tools during covered app work unless the user requests browser work or a CLI failure explicitly requires it. Follow returned CLI tab-selection commands when needed; ask the user to sign in only on the returned authentication recovery. An unusable Chrome connection is a local blocker, not evidence that the app lacks an action.
15
15
 
16
- Respect policy, entitlement, effect, cost, and unavailable boundaries before dispatch. A refusal does not authorize direct HTTP or a new interface around it. Do not automatically retry a mutation that may have reached the app, including one whose result or readback proof was lost. Follow the retained invocation's recovery instruction and inspect state through a documented CLI read. A successful runtime proof is already verification; decide whether the user's outcome requires additional checks, without routinely repeating it or inspecting the UI. Check returned objects against the requested domain meaning, traverse pagination and containers where needed, and do not claim completion from a title or status code alone. Read only the selected result fields needed for the decision; oversized output is saved to the path printed by the CLI.
16
+ Respect policy, entitlement, effect, cost, and unavailable boundaries before dispatch. A refusal does not authorize direct HTTP or a new interface around it. Do not automatically retry a mutation that may have reached the app, including one whose result was lost. Follow the retained invocation's recovery instruction and inspect state through a documented CLI read. Published execution checks the response without automatic readback. Decide whether the user's outcome needs state confirmation through a published read; do not routinely inspect the UI. Check returned objects against the requested domain meaning, traverse pagination and containers where needed, and do not claim completion from a title or status code alone. Prefer published search, filter and detail actions when only a few records are needed. Read only the selected result fields needed for the decision; oversized output is saved to the path printed by the CLI. Search that file for relevant names or identifiers and extract matching records with ordinary file tools instead of loading the whole file into context. A saved response contains only fetched data, not unfetched upstream pages.
17
17
 
18
18
  If the app, action, required input source, or result field is absent, tell the discovery owner the user outcome, the precise missing consumer capability, semantic input sources, required result, official documentation supplied by the user, and which published actions must remain intact. One owner should extend the app with the `discover` skill, then the consumer should read the newly published action help and complete the original task. Ask about test-state effects only when they are outside existing authorization. `apilift apps release <app>` ends this use session and clears its local selections and defaults when the work is done.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apilift/cli",
3
- "version": "1.4.39",
3
+ "version": "1.4.40-dev.1792138042.e8a4f6750dc5d2eba58515d30b501f737c2846dc",
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": "f81711fbaca403338ca7ecb4695d27d684c09fe1",
39
+ "gitHead": "e8a4f6750dc5d2eba58515d30b501f737c2846dc",
40
40
  "repository": {
41
41
  "type": "git",
42
42
  "url": "git+https://github.com/egor-sergeev/apilift.git",