@haystackeditor/cli 0.22.2 → 0.23.0

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.
@@ -8,7 +8,7 @@ import chalk from 'chalk';
8
8
  import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
9
9
  import { findGitRoot, parseRemoteUrl, } from '../utils/git.js';
10
10
  import { resolveVerifyBaseSha } from '../utils/verify-base.js';
11
- import { HaystackApiError, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
11
+ import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
12
12
  import { trackPrecomputeEvent } from '../utils/telemetry.js';
13
13
  import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
14
14
  import { startPrecomputeDelivery } from './precompute-delivery.js';
@@ -313,13 +313,45 @@ function precomputeResult(response) {
313
313
  return { outcome: 'existing', ...reported };
314
314
  throw new PrecomputeFailure('The precompute service returned an invalid response.');
315
315
  }
316
+ /** The captured change as the service takes it (CRAWL-V1 amendment 14): uploaded once, content-addressed by its sha256 in
317
+ * the repository's scope (a HEAD finds an earlier upload; a PUT only when it is missing), and named in the front-half
318
+ * request by digest and size instead of carried inline. Nothing on the way holds a size limit of its own: tldraw's 13.8 MB
319
+ * capture was refused at the 8 MiB inline bound (2026-10-02). `force` uploads even when the HEAD found it (a 409 says the
320
+ * service no longer has it). */
321
+ async function uploadSourcePatch(request, token, signal, force = false) {
322
+ const gzipped = Buffer.from(request.patchGzBase64, 'base64');
323
+ const path = `/api/agent/cloud-verifier/source-patches/${request.patchSha256}`
324
+ + `?owner=${encodeURIComponent(request.owner)}&repository=${encodeURIComponent(request.repository)}`;
325
+ const found = force ? null : await haystackFetch(path, token, { method: 'HEAD', signal }, [404]);
326
+ if (found === null || found.status === 404) {
327
+ await haystackFetch(path, token, { method: 'PUT', headers: { 'Content-Type': 'application/gzip' }, body: gzipped, signal });
328
+ }
329
+ return gzipped.byteLength;
330
+ }
331
+ /** One front-half submission of a captured request: the change uploaded (uploadSourcePatch), then the request naming it. A
332
+ * 409 (the service has no such upload: it expired between the HEAD and the POST) uploads it again and submits once more. */
333
+ async function submitCapture(request, token, signal) {
334
+ const { patchGzBase64: _inline, ...named } = request;
335
+ const submit = async (force) => {
336
+ const patchBytes = await uploadSourcePatch(request, token, signal, force);
337
+ return haystackJson('/api/agent/cloud-verifier/front-half', token, { method: 'POST',
338
+ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...named, patchBytes }), signal });
339
+ };
340
+ try {
341
+ return await submit(false);
342
+ }
343
+ catch (error) {
344
+ if (!(error instanceof HaystackApiError && error.status === 409))
345
+ throw error;
346
+ return submit(true);
347
+ }
348
+ }
316
349
  /** One authenticated submission of the already captured request. No source reads or retries. */
317
350
  export const deliverPrecomputeRequest = async (request, signal) => {
318
351
  signal.throwIfAborted();
319
352
  const auth = await resolveAuthContext({ owner: request.owner, repo: request.repository });
320
353
  signal.throwIfAborted();
321
- return precomputeResult(await haystackJson('/api/agent/cloud-verifier/front-half', auth.token, { method: 'POST', headers: { 'Content-Type': 'application/json' },
322
- body: JSON.stringify(request), signal }));
354
+ return precomputeResult(await submitCapture(request, auth.token, signal));
323
355
  };
324
356
  function readWalkerStatus(value) {
325
357
  if (value && typeof value === 'object' && !Array.isArray(value)) {
@@ -506,12 +538,7 @@ export async function postPrecomputeCapture(capture, token, deadline) {
506
538
  const fetchTimer = setTimeout(() => controller.abort(), remainingMs(deadline));
507
539
  let response;
508
540
  try {
509
- response = await haystackJson('/api/agent/cloud-verifier/front-half', token, {
510
- method: 'POST',
511
- headers: { 'Content-Type': 'application/json' },
512
- body: JSON.stringify(capture.derivation.request),
513
- signal: controller.signal,
514
- });
541
+ response = await submitCapture(capture.derivation.request, token, controller.signal);
515
542
  }
516
543
  finally {
517
544
  clearTimeout(fetchTimer);
@@ -183,6 +183,11 @@ export async function classifyHttpError(response, context) {
183
183
  return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
184
184
  }
185
185
  export async function haystackJson(path, token, init = {}, acceptedStatuses = []) {
186
+ const response = await haystackFetch(path, token, init, acceptedStatuses);
187
+ return response.json();
188
+ }
189
+ /** An authenticated request whose response is not JSON (a HEAD, an upload): the response itself, or the classified error. */
190
+ export async function haystackFetch(path, token, init = {}, acceptedStatuses = []) {
186
191
  const response = await fetch(haystackApiUrl(path), {
187
192
  ...init,
188
193
  headers: headers(token, init.headers),
@@ -190,7 +195,7 @@ export async function haystackJson(path, token, init = {}, acceptedStatuses = []
190
195
  if (!response.ok && !acceptedStatuses.includes(response.status)) {
191
196
  throw await classifyHttpError(response, `Haystack API ${path}`);
192
197
  }
193
- return response.json();
198
+ return response;
194
199
  }
195
200
  export async function fetchCliIdentity(token) {
196
201
  return haystackJson('/api/cli/me', token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "description": "haystack verify: run your app with and without a change, and see what the change broke",
5
5
  "type": "module",
6
6
  "bin": {