@buildinternet/uploads 0.37.3 → 0.40.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.
@@ -166,6 +166,11 @@ export const ROOT_COMMANDS = [
166
166
  { name: "doctor", summary: "Check the GitHub App's webhook event subscriptions" },
167
167
  ],
168
168
  },
169
+ {
170
+ name: "ingest",
171
+ usage: "ingest (--pr <n> | --issue <n>)",
172
+ summary: "Mirror GitHub-native user-attachments media from a PR/issue into the workspace",
173
+ },
169
174
  {
170
175
  name: "list",
171
176
  summary: "List objects (--meta k=v filters by queryable metadata)",
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { formatRootHelp, formatUnknownCommand, wantsFullHelp } from "./cli-help.
6
6
  import { commandSummary, suggestCommand } from "./cli-suggest.js";
7
7
  import { writeJson } from "./io.js";
8
8
  import { colorEnabled, createStyle } from "./cli-style.js";
9
- import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
9
+ import { runPut, runAttach, runStaged, runList, runFind, runMeta, runDelete, runHealth, runDoctor, runComment, runGithub, runIngest, runUsage, runReconcile, runPurgeExpired, runGallery, } from "./commands.js";
10
10
  import { runConfig } from "./commands/config.js";
11
11
  import { runSetup } from "./commands/setup.js";
12
12
  import { runLogin } from "./commands/login.js";
@@ -308,7 +308,8 @@ export async function runCli(argv) {
308
308
  case "purge-expired":
309
309
  case "doctor":
310
310
  case "comment":
311
- case "github": {
311
+ case "github":
312
+ case "ingest": {
312
313
  const ctx = createContext(parsed.globals, !showHelp, cmdArgs);
313
314
  switch (parsed.command) {
314
315
  case "attach":
@@ -332,6 +333,9 @@ export async function runCli(argv) {
332
333
  case "github":
333
334
  code = await runGithub(ctx, cmdArgs, showHelp);
334
335
  break;
336
+ case "ingest":
337
+ code = await runIngest(ctx, cmdArgs, showHelp);
338
+ break;
335
339
  case "list":
336
340
  code = await runList(ctx, cmdArgs, showHelp);
337
341
  break;
package/dist/client.d.ts CHANGED
@@ -256,7 +256,7 @@ export type GithubCommentResult = {
256
256
  fixUrl?: string;
257
257
  required?: string[];
258
258
  };
259
- /** `POST /v1/:workspace/github/promote` request/response (server contract, PR #310). */
259
+ /** `POST /v1/workspaces/:workspace/github/promote` request/response (server contract, PR #310). */
260
260
  export interface PromoteBranchAttachmentsOptions {
261
261
  repo: string;
262
262
  num: number;
@@ -270,7 +270,7 @@ export interface PromoteBranchAttachmentsResult {
270
270
  promoted: string[];
271
271
  skipped: PromoteSkip[];
272
272
  }
273
- /** `GET`/`POST /v1/:workspace/github/link` result (server contract, phase 4b). */
273
+ /** `GET`/`POST /v1/workspaces/:workspace/github/link` result (server contract, phase 4b). */
274
274
  export interface GithubLinkResult {
275
275
  repo: string;
276
276
  linked: boolean;
@@ -289,14 +289,14 @@ export interface GithubLinkClaimResult extends GithubLinkResult {
289
289
  */
290
290
  reason?: "not_authorized";
291
291
  }
292
- /** `DELETE /v1/:workspace/github/link` result (issue #318, self-serve unlink). */
292
+ /** `DELETE /v1/workspaces/:workspace/github/link` result (issue #318, self-serve unlink). */
293
293
  export interface GithubLinkUnlinkResult {
294
294
  repo: string;
295
295
  unlinked: boolean;
296
296
  reason?: "not_linked";
297
297
  }
298
298
  /**
299
- * `GET /v1/:workspace/github/repo-link` result (issue #398). Deliberately
299
+ * `GET /v1/workspaces/:workspace/github/repo-link` result (issue #398). Deliberately
300
300
  * minimal relative to `GithubLinkResult`: never names the owning workspace
301
301
  * when it isn't this one — "self"/"other"/"none" is all the stage-time
302
302
  * warning needs, and anything richer would leak cross-tenant info to a
@@ -305,10 +305,31 @@ export interface GithubLinkUnlinkResult {
305
305
  export interface GithubRepoLinkResult {
306
306
  binding: "self" | "other" | "none";
307
307
  }
308
+ /**
309
+ * `POST /v1/workspaces/:workspace/github/ingest` result (Task 6, manual/
310
+ * backfill entry point for GitHub-native `user-attachments` media). `repo`
311
+ * comes back lowercased by the server.
312
+ */
313
+ export interface IngestGithubResult {
314
+ repo: string;
315
+ kind: "pull" | "issues";
316
+ num: number;
317
+ /** Object keys newly mirrored into the workspace this call. */
318
+ ingested: string[];
319
+ /** Object keys whose previously-detached ledger row was un-detached. */
320
+ reattached: string[];
321
+ /** Object keys detached because they're no longer referenced. */
322
+ detached: string[];
323
+ /** Attachment URLs skipped, with the reason. */
324
+ skipped: {
325
+ url: string;
326
+ reason: string;
327
+ }[];
328
+ }
308
329
  export interface HealthResult {
309
330
  ok: boolean;
310
331
  }
311
- /** `GET /v1/:workspace/github/health` result (issue #293 follow-up). */
332
+ /** `GET /v1/workspaces/:workspace/github/health` result (issue #293 follow-up). */
312
333
  export interface GithubHealthResult {
313
334
  configured: boolean;
314
335
  ok: boolean;
@@ -617,6 +638,18 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
617
638
  * `UploadsError` (status 404) on an older/self-hosted server without
618
639
  * this route — callers treat that as "unknown", not "broken".
619
640
  */
641
+ /**
642
+ * `POST /v1/workspaces/:workspace/github/ingest` (Task 6) — manual/
643
+ * backfill mirror of a PR/issue's `github.com/user-attachments` media
644
+ * into the workspace. Only ever had the canonical `/v1/workspaces`
645
+ * route (no old bearer-only alias) — unlike the other `github/*` client
646
+ * methods above, which moved onto it in issue #613.
647
+ */
648
+ ingestGithub(input: {
649
+ repo: string;
650
+ kind: "pull" | "issues";
651
+ num: number;
652
+ }): Promise<IngestGithubResult>;
620
653
  githubHealth(): Promise<GithubHealthResult>;
621
654
  /**
622
655
  * Self-serve unlink (issue #318): removes `repo`'s binding, but only if
package/dist/client.js CHANGED
@@ -186,14 +186,19 @@ export function mintWorkspaceToken(apiUrl, accessToken, input) {
186
186
  function encodeKeyPath(key) {
187
187
  return key.split("/").map(encodeURIComponent).join("/");
188
188
  }
189
+ // Stays on the legacy `/v1/:workspace/files` wildcard for now (issue #613):
190
+ // the canonical files vertical has no upload PUT, POST /sign, or GET/PATCH
191
+ // /:key metadata yet, and its list/search adopted the session response shape
192
+ // rather than the bearer one. Move this once the canonical vertical grows
193
+ // those routes and the bearer list shape is reconciled.
189
194
  function filesBase(config) {
190
195
  return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/files`;
191
196
  }
192
197
  function usageBase(config) {
193
- return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/usage`;
198
+ return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/usage`;
194
199
  }
195
200
  function galleriesBase(config) {
196
- return `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/galleries`;
201
+ return `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/galleries`;
197
202
  }
198
203
  function mapApiError(status, error, code, requiredScope, existingUrl) {
199
204
  const normalized = error.toLowerCase();
@@ -499,7 +504,7 @@ export function createUploadsClient(config) {
499
504
  * the field.
500
505
  */
501
506
  async upsertGithubComment(opts) {
502
- return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/comment`, {
507
+ return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/comment`, {
503
508
  body: new TextEncoder().encode(JSON.stringify(opts)),
504
509
  headers: { "Content-Type": "application/json" },
505
510
  });
@@ -511,7 +516,7 @@ export function createUploadsClient(config) {
511
516
  * that doesn't have this route yet, as "nothing promoted").
512
517
  */
513
518
  async promoteBranchAttachments(opts) {
514
- return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/promote`, {
519
+ return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/promote`, {
515
520
  body: new TextEncoder().encode(JSON.stringify(opts)),
516
521
  headers: { "Content-Type": "application/json" },
517
522
  });
@@ -520,7 +525,7 @@ export function createUploadsClient(config) {
520
525
  * `UploadsError` (status 404) on an older/self-hosted server without this
521
526
  * route — callers treat that as "bindings unsupported". */
522
527
  async githubLinkStatus(repo) {
523
- return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
528
+ return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
524
529
  },
525
530
  /**
526
531
  * Explicitly claim `repo` for this workspace (first-claim-wins — see
@@ -530,7 +535,7 @@ export function createUploadsClient(config) {
530
535
  * server without this route.
531
536
  */
532
537
  async githubLinkClaim(repo) {
533
- return request("POST", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link`, {
538
+ return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link`, {
534
539
  body: new TextEncoder().encode(JSON.stringify({ repo })),
535
540
  headers: { "Content-Type": "application/json" },
536
541
  });
@@ -543,15 +548,31 @@ export function createUploadsClient(config) {
543
548
  * caller (the stage warning) treats ANY failure here as "stay silent".
544
549
  */
545
550
  async githubRepoLinkStatus(repo) {
546
- return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/repo-link?repo=${encodeURIComponent(repo)}`);
551
+ return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/repo-link?repo=${encodeURIComponent(repo)}`);
547
552
  },
548
553
  /**
549
554
  * GitHub App configuration + webhook event subscription check. Throws
550
555
  * `UploadsError` (status 404) on an older/self-hosted server without
551
556
  * this route — callers treat that as "unknown", not "broken".
552
557
  */
558
+ /**
559
+ * `POST /v1/workspaces/:workspace/github/ingest` (Task 6) — manual/
560
+ * backfill mirror of a PR/issue's `github.com/user-attachments` media
561
+ * into the workspace. Only ever had the canonical `/v1/workspaces`
562
+ * route (no old bearer-only alias) — unlike the other `github/*` client
563
+ * methods above, which moved onto it in issue #613.
564
+ */
565
+ async ingestGithub(input) {
566
+ const body = input.kind === "pull"
567
+ ? { repo: input.repo, pr: input.num }
568
+ : { repo: input.repo, issue: input.num };
569
+ return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/ingest`, {
570
+ body: new TextEncoder().encode(JSON.stringify(body)),
571
+ headers: { "Content-Type": "application/json" },
572
+ });
573
+ },
553
574
  async githubHealth() {
554
- return request("GET", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/health`);
575
+ return request("GET", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/health`);
555
576
  },
556
577
  /**
557
578
  * Self-serve unlink (issue #318): removes `repo`'s binding, but only if
@@ -561,7 +582,7 @@ export function createUploadsClient(config) {
561
582
  * older/self-hosted server without this route.
562
583
  */
563
584
  async githubLinkUnlink(repo) {
564
- return request("DELETE", `${config.apiUrl}/v1/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
585
+ return request("DELETE", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/link?repo=${encodeURIComponent(repo)}`);
565
586
  },
566
587
  async health() {
567
588
  return request("GET", `${config.apiUrl}/health`, { auth: false });
@@ -2,12 +2,13 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
3
  import { extractDashValue, flagBool, flagInt, flagString, flagValues, parseCommandArgs, UsageError, } from "../cli-args.js";
4
4
  import { writeCommandHelp } from "../cli-style.js";
5
- import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, } from "../commands.js";
5
+ import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, } from "../commands.js";
6
6
  import { resolvePutDefaults } from "../config.js";
7
7
  import { loadDefaultsRaw, resolveScreenshotDefaults } from "../config-file.js";
8
8
  import { resolvePutPrefix } from "../destinations.js";
9
9
  import { execRunner, ghMetadataFromTargetWithTitle, resolveRepo, } from "../github-gh.js";
10
10
  import { ghBranchAttachmentKey } from "../github.js";
11
+ import { deriveRepoSlugFromGit } from "../keys.js";
11
12
  import { safeCaptureFacts } from "../capture-facts.js";
12
13
  import { parseMetaFlags, validateMetaMap } from "../metadata.js";
13
14
  import { mergeDerivedMeta } from "../metadata-vocab.js";
@@ -31,6 +32,15 @@ localhost/private-network URLs are reachable only by the
31
32
  local backend — with --via remote (or auto falling back to remote) these
32
33
  fail fast with a clear error instead of sending a doomed request.
33
34
 
35
+ The object name is derived from the target URL (host + path), not chosen by
36
+ you — e.g. https://app.example/settings becomes app.example-settings.png.
37
+ --state folds into that derived name (a -before/-after/... suffix), so
38
+ capturing the same URL with --state before then --state after produces two
39
+ distinct objects instead of the second silently overwriting the first.
40
+ Re-capturing the same URL + --state replaces that object in place — the
41
+ intended idempotency for repeat captures. --key bypasses all of this and
42
+ sets the whole object key verbatim (no folding).
43
+
34
44
  After capture, screenshots share the put upload pipeline: optional --frame,
35
45
  optimize-by-default, --pr/--issue attachment + --comment, --gallery, --meta.
36
46
 
@@ -351,7 +361,11 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
351
361
  // Explicit input (--meta plus the dedicated flags) wins over capture facts.
352
362
  const explicitMeta = { ...metaExtras, ...stateAppMetaFromFlags(parsed.flags) };
353
363
  const deriveMeta = derivedMetaEnabled(parsed.flags, putDefaults);
354
- const withFacts = mergeDerivedMeta(explicitMeta, deriveMeta ? safeCaptureFacts(target, viewport, colorScheme) : {});
364
+ const repoSlug = deriveMeta && !noGit ? deriveRepoSlugFromGit(run) : undefined;
365
+ const withFacts = mergeDerivedMeta(explicitMeta, {
366
+ ...(deriveMeta ? safeCaptureFacts(target, viewport, colorScheme) : {}),
367
+ ...(repoSlug ? { repo: repoSlug } : {}),
368
+ });
355
369
  let metadata = withFacts;
356
370
  if (ghTarget) {
357
371
  metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
@@ -381,6 +395,9 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
381
395
  reducedMotion,
382
396
  evalJs,
383
397
  initScript,
398
+ // Skip folding when an explicit --key was given — --key sets the whole
399
+ // key, so there's no auto-derived name to fold state into.
400
+ state: keyHint ? undefined : explicitMeta.state,
384
401
  measureSelectors: annotateSelectors.length > 0 ? annotateSelectors : undefined,
385
402
  apiUrl: ctx.config.apiUrl,
386
403
  token: ctx.config.token,
@@ -497,6 +514,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
497
514
  if (prepared.optimized) {
498
515
  process.stderr.write(`>> optimized ${prepared.originalBytes} → ${prepared.outputBytes} bytes\n`);
499
516
  }
517
+ writeReplacedNote(result.replaced, ctx.quiet, dryRun, result.wouldRefuse);
500
518
  process.stderr.write(`>> key: ${result.key}${dryRun ? " (dry run — not uploaded)" : ""}\n`);
501
519
  if (stagingTarget !== undefined) {
502
520
  process.stderr.write(`>> find these later: uploads find gh.branch=${stagingTarget.branch.toLowerCase()}\n`);
@@ -514,8 +532,16 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
514
532
  process.stderr.write("\n");
515
533
  }
516
534
  // One JSON `hint` slot (mirrors bare put): the binding warning is more
517
- // actionable than the generic staging note, so it wins when both fire.
518
- const jsonHint = bindingWarning ?? stagingNote;
535
+ // actionable than the generic staging note, so it wins when both fire; a
536
+ // replaced-object note (issue #618) is the lowest priority of the three —
537
+ // it only surfaces when nothing else already claimed the slot. Since state
538
+ // folds into the derived key, replaced + state means a same-side re-capture,
539
+ // which is the intended replace-in-place flow — word it as informational,
540
+ // not as a problem.
541
+ const replacedHint = result.replaced && explicitMeta.state
542
+ ? `re-capture replaced the previous state=${explicitMeta.state} object at ${result.key} — expected for repeat captures of the same URL + state`
543
+ : undefined;
544
+ const jsonHint = bindingWarning ?? stagingNote ?? replacedHint;
519
545
  switch (format) {
520
546
  case "json":
521
547
  await writeJson({
@@ -63,6 +63,7 @@ export declare function warnNearMissMeta(ctx: CliContext, meta: Record<string, s
63
63
  * point is `--help` discoverability and `--state` validation.
64
64
  */
65
65
  export declare function stateAppMetaFromFlags(flags: CommandFlags["flags"]): Record<string, string>;
66
+ export declare function writeReplacedNote(replaced: boolean | undefined, quiet: boolean, dryRun?: boolean, wouldRefuse?: boolean): void;
66
67
  export type PreparedUpload = OptimizeImageResult & {
67
68
  frame?: Pick<FrameResult, "framed" | "frameId" | "skippedReason">;
68
69
  };
@@ -456,6 +457,7 @@ export declare function runFind(ctx: CliContext, args: string[], help?: boolean)
456
457
  export declare function runMeta(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
457
458
  export declare function runDelete(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
458
459
  export declare function runComment(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
460
+ export declare function runIngest(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
459
461
  export declare function runGithub(ctx: CliContext, args: string[], help?: boolean, run?: CommandRunner): Promise<number>;
460
462
  export declare function runUsage(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
461
463
  export declare function runReconcile(ctx: CliContext, args: string[], help?: boolean): Promise<number>;
package/dist/commands.js CHANGED
@@ -15,7 +15,7 @@ import { mergeDerivedMeta, nearMissMetaWarnings, validateStateValue } from "./me
15
15
  import { mergeSidecarMeta } from "./sidecar.js";
16
16
  import { ghAttachmentKey, ghBranchAttachmentKey, ghBranchKeyPrefix, ghKeyPrefix, ghMetadataFromTarget, parseGhKey, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, AUTO_RENDER_OPTIONS, GH_FALLBACK_AUTHOR_NOTE, normalizeGithubCoordinate, } from "./github.js";
17
17
  import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
18
- import { deriveRepoFromGit } from "./keys.js";
18
+ import { deriveRepoFromGit, deriveRepoSlugFromGit } from "./keys.js";
19
19
  import { resolvePutPrefix } from "./destinations.js";
20
20
  import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
21
21
  import { applyFrame, resolveFrameId } from "./frame.js";
@@ -315,7 +315,7 @@ function formatOptimizeNote(opt) {
315
315
  }
316
316
  return undefined;
317
317
  }
318
- function writeReplacedNote(replaced, quiet, dryRun = false, wouldRefuse = false) {
318
+ export function writeReplacedNote(replaced, quiet, dryRun = false, wouldRefuse = false) {
319
319
  if (quiet)
320
320
  return;
321
321
  if (dryRun && wouldRefuse) {
@@ -899,7 +899,7 @@ export async function uploadPuts(opts) {
899
899
  return { uploads, failures, firstError, sentMetadata };
900
900
  }
901
901
  /**
902
- * Best-effort call to `POST /v1/:workspace/github/promote` (server contract,
902
+ * Best-effort call to `POST /v1/workspaces/:workspace/github/promote` (server contract,
903
903
  * PR #310). Degrade-safe like `syncAttachmentsComment`'s bot path: an older
904
904
  * or self-hosted worker without this route (404), a forbidden token (403),
905
905
  * or a network error all collapse to "nothing promoted" — the caller must
@@ -1699,6 +1699,23 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
1699
1699
  }
1700
1700
  }
1701
1701
  }
1702
+ // Derived `repo` (spec: 2026-08-11-screenshots-project-grouping-design.md):
1703
+ // the capturing repo, on every layout including gh/staging. mergeDerivedMeta
1704
+ // keeps explicit --meta repo= wins and never breaks the caps.
1705
+ //
1706
+ // `metadata === undefined` means "leave whatever's already stored for this
1707
+ // key untouched" (client.ts only sends X-Uploads-Meta-* when the map is
1708
+ // defined; a defined map is a full replace server-side). A bare `uploads
1709
+ // put` with no --meta/gh/staging context leaves `metadata` undefined by
1710
+ // design — never synthesize `{ repo }` there just because a repo happens
1711
+ // to be derivable, or a plain re-upload of an existing key would silently
1712
+ // wipe everything already stored on it. Only fold repo in when `metadata`
1713
+ // is already a defined object for another reason.
1714
+ if (!noGit && derivedMetaEnabled(parsed.flags, defaults) && metadata !== undefined) {
1715
+ const slug = deriveRepoSlugFromGit(run);
1716
+ if (slug)
1717
+ metadata = mergeDerivedMeta(metadata, { repo: slug });
1718
+ }
1702
1719
  // Bare-put nudge (issue #393): only relevant when staging didn't take over
1703
1720
  // — once `stagingTarget` resolves, staging IS the upgrade the nudge used to
1704
1721
  // point at, so this is skipped entirely rather than firing redundantly.
@@ -2551,6 +2568,48 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
2551
2568
  }
2552
2569
  return 0;
2553
2570
  }
2571
+ // --- ingest ---
2572
+ const INGEST_HELP = `uploads ingest — mirror GitHub-native attachments from a PR/issue into the workspace
2573
+
2574
+ Usage:
2575
+ uploads ingest --pr <n> [--repo owner/name]
2576
+ uploads ingest --issue <n> [--repo owner/name]
2577
+
2578
+ Scans the PR/issue description and comments for github.com/user-attachments
2579
+ media, mirrors new ones into the workspace (indexed, not added to the managed
2580
+ comment), and detaches ones no longer referenced. Works on any repo linked to
2581
+ the workspace; the .uploads.yml ingestGithubAttachments knob only gates the
2582
+ automatic webhook path.
2583
+
2584
+ Examples:
2585
+ uploads ingest --pr 123
2586
+ uploads ingest --issue 45 --repo acme/app --format json
2587
+ `;
2588
+ export async function runIngest(ctx, args, help = false, run = execRunner) {
2589
+ const parsed = parseCommandArgs(args);
2590
+ if (help || parsed.help) {
2591
+ writeCommandHelp(INGEST_HELP);
2592
+ return 0;
2593
+ }
2594
+ const target = ghTargetFromFlags(parsed.flags, run);
2595
+ if (!target) {
2596
+ throw new UsageError("--pr or --issue required");
2597
+ }
2598
+ const result = await ctx.client.ingestGithub(target);
2599
+ const jsonMode = ctx.json || flagString(parsed.flags, "--format") === "json";
2600
+ if (jsonMode) {
2601
+ await writeJson(result);
2602
+ return 0;
2603
+ }
2604
+ if (!ctx.quiet) {
2605
+ let line = `Ingested ${result.ingested.length}, re-attached ${result.reattached.length}, detached ${result.detached.length}, skipped ${result.skipped.length}\n`;
2606
+ for (const skip of result.skipped) {
2607
+ line += ` skipped: ${skip.url} (${skip.reason})\n`;
2608
+ }
2609
+ process.stderr.write(line);
2610
+ }
2611
+ return 0;
2612
+ }
2554
2613
  // --- github link ---
2555
2614
  const GITHUB_HELP = `uploads github link [--repo <owner/name>] [--status] [--workspace <name>]
2556
2615
  uploads github unlink [--repo <owner/name>] [--workspace <name>]
@@ -2570,7 +2629,8 @@ owns it; an operator can reassign or remove that binding instead.
2570
2629
 
2571
2630
  \`doctor\` checks the GitHub App itself: whether it's configured on the
2572
2631
  server, and whether it's subscribed to the webhook events uploads.sh's
2573
- handler needs (issues, pull_request see docs/github-app). A missing
2632
+ handler needs (required: issues, pull_request; recommended: issue_comment
2633
+ see docs/github-app). A missing
2574
2634
  subscription is the classic silent failure: the App's ping stays green
2575
2635
  while webhook auto-promotion and title-cache invalidation quietly do
2576
2636
  nothing.
@@ -2590,7 +2650,7 @@ function recommendedNoteLine(result) {
2590
2650
  const missing = missingRecommendedEventsOf(result);
2591
2651
  if (missing.length === 0)
2592
2652
  return "";
2593
- return `note: not subscribed to ${missing.join(", ")} (recommended) — enables bot-comment self-healing; subscribe under the App's Permissions & events\n`;
2653
+ return `note: not subscribed to ${missing.join(", ")} (recommended) — enables bot-comment self-healing and comment-attachment ingestion; subscribe under the App's Permissions & events\n`;
2594
2654
  }
2595
2655
  function formatGithubDoctor(result) {
2596
2656
  if (!result.configured) {
@@ -2600,7 +2660,18 @@ function formatGithubDoctor(result) {
2600
2660
  return `github app: configured, but health check failed${result.hint ? ` — ${result.hint}` : ""}\n`;
2601
2661
  }
2602
2662
  if (result.ok) {
2603
- return (`github app: ok subscribed to ${result.requiredEvents.join(", ")}\n` +
2663
+ // The ACTUAL subscribed list, not just the required subset — printing
2664
+ // requiredEvents here once misdiagnosed a live App as "not subscribed to
2665
+ // issue_comment" when it was (2026-08-11). `events` is non-null on every
2666
+ // ok result (the null case returns above); the requiredEvents fallback
2667
+ // only guards a malformed payload from an older server.
2668
+ const subscribed = Array.isArray(result.events)
2669
+ ? [...result.events].sort()
2670
+ : [...result.requiredEvents];
2671
+ const allPresent = missingRecommendedEventsOf(result).length === 0;
2672
+ return (`github app: ok — subscribed to ${subscribed.join(", ")}` +
2673
+ (allPresent ? " (all required + recommended events)" : "") +
2674
+ "\n" +
2604
2675
  recommendedNoteLine(result));
2605
2676
  }
2606
2677
  return (`github app: missing webhook event subscription(s): ${result.missingEvents.join(", ")}\n` +
@@ -17,6 +17,7 @@ export interface RepoCommentConfig {
17
17
  metaState?: boolean;
18
18
  linkToFilePage?: boolean;
19
19
  note?: string;
20
+ ingestGithubAttachments?: boolean;
20
21
  }
21
22
  export interface WorkspaceCommentDefaults {
22
23
  imageWidth?: "full" | number;
@@ -24,6 +25,7 @@ export interface WorkspaceCommentDefaults {
24
25
  showMetadata?: boolean;
25
26
  linkToFilePage?: boolean;
26
27
  note?: string;
28
+ ingestGithubAttachments?: boolean;
27
29
  }
28
30
  export interface ResolvedCommentOptions {
29
31
  imageWidth: "auto" | "full" | number;
@@ -32,6 +34,7 @@ export interface ResolvedCommentOptions {
32
34
  metaState: boolean;
33
35
  linkToFilePage: boolean;
34
36
  note: string | null;
37
+ ingestGithubAttachments: boolean;
35
38
  }
36
39
  export type OptionSource = "repo" | "workspace" | "auto";
37
40
  export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
@@ -20,6 +20,7 @@ export const AUTO_COMMENT_OPTIONS = {
20
20
  metaState: true,
21
21
  linkToFilePage: true,
22
22
  note: null,
23
+ ingestGithubAttachments: false,
23
24
  };
24
25
  export const NOTE_MAX_CHARS = 500;
25
26
  const WIDTH_MIN = 160;
@@ -71,6 +72,14 @@ export function parseRepoCommentConfig(text, format) {
71
72
  else
72
73
  warnings.push(`linkToFilePage: expected a boolean; dropped`);
73
74
  }
75
+ // ingestGithubAttachments: boolean
76
+ if ("ingestGithubAttachments" in c) {
77
+ const v = c.ingestGithubAttachments;
78
+ if (typeof v === "boolean")
79
+ config.ingestGithubAttachments = v;
80
+ else
81
+ warnings.push(`ingestGithubAttachments: expected a boolean; dropped`);
82
+ }
74
83
  // meta.path / meta.state: booleans nested under `meta`
75
84
  if ("meta" in c) {
76
85
  const v = c.meta;
@@ -123,6 +132,9 @@ export function resolveCommentOptions(repo, ws) {
123
132
  : {}),
124
133
  ...(ws?.linkToFilePage !== undefined ? { linkToFilePage: ws.linkToFilePage } : {}),
125
134
  ...(ws?.note ? { note: ws.note } : {}),
135
+ ...(ws?.ingestGithubAttachments !== undefined
136
+ ? { ingestGithubAttachments: ws.ingestGithubAttachments }
137
+ : {}),
126
138
  };
127
139
  const options = { ...AUTO_COMMENT_OPTIONS };
128
140
  const source = Object.fromEntries(Object.keys(AUTO_COMMENT_OPTIONS).map((k) => [k, "auto"]));
@@ -133,6 +145,7 @@ export function resolveCommentOptions(repo, ws) {
133
145
  "metaPath",
134
146
  "metaState",
135
147
  "linkToFilePage",
148
+ "ingestGithubAttachments",
136
149
  ]) {
137
150
  if (cfg[key] !== undefined && source[key] === "auto") {
138
151
  options[key] = cfg[key];
@@ -1,5 +1,7 @@
1
1
  import type { UploadsClientConfig } from "./config.js";
2
- export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_SESSION_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META", "UPLOADS_SCREENSHOT_VIA", "UPLOADS_NO_NUDGE"];
2
+ export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN",
3
+ /** Better Auth device-flow session bearer — keeps session.cliVersion fresh. */
4
+ "UPLOADS_SESSION_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META", "UPLOADS_SCREENSHOT_VIA", "UPLOADS_NO_NUDGE"];
3
5
  export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
4
6
  export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
5
7
  export interface PutDefaults {
package/dist/keys.d.ts CHANGED
@@ -8,6 +8,12 @@ export declare function sha256Short(bytes: Uint8Array): Promise<string>;
8
8
  * every existing caller.
9
9
  */
10
10
  export declare function deriveRepoFromGit(run?: (cmd: string, args: string[], input?: string) => string): string | undefined;
11
+ /**
12
+ * Full `owner/name` slug (lowercase, matching the `gh.repo` convention) from
13
+ * the cwd's git remote — the derived `repo` metadata value. Unlike
14
+ * `deriveRepoFromGit` above (key-segment name only), this keeps the owner.
15
+ */
16
+ export declare function deriveRepoSlugFromGit(run?: (cmd: string, args: string[], input?: string) => string): string | undefined;
11
17
  export declare function buildScreenshotKey(opts: {
12
18
  filename: string;
13
19
  fileBytes: Uint8Array;
package/dist/keys.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { parseRepoFromRemoteUrl } from "./github.js";
2
3
  export function sanitizeKeySegment(s) {
3
4
  return s.replace(/[^A-Za-z0-9._-]/g, "-");
4
5
  }
@@ -28,6 +29,22 @@ export function deriveRepoFromGit(run) {
28
29
  return undefined;
29
30
  }
30
31
  }
32
+ /**
33
+ * Full `owner/name` slug (lowercase, matching the `gh.repo` convention) from
34
+ * the cwd's git remote — the derived `repo` metadata value. Unlike
35
+ * `deriveRepoFromGit` above (key-segment name only), this keeps the owner.
36
+ */
37
+ export function deriveRepoSlugFromGit(run) {
38
+ try {
39
+ const url = run
40
+ ? run("git", ["config", "--get", "remote.origin.url"])
41
+ : execSync("git config --get remote.origin.url", { encoding: "utf8" });
42
+ return parseRepoFromRemoteUrl(url)?.toLowerCase();
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
31
48
  export async function buildScreenshotKey(opts) {
32
49
  const dot = opts.filename.lastIndexOf(".");
33
50
  const ext = dot >= 0 ? opts.filename.slice(dot + 1) : "";
@@ -23,7 +23,7 @@ export declare const metadataProp: {
23
23
  };
24
24
  export declare const stateProp: {
25
25
  type: string;
26
- enum: ("before" | "after" | "empty" | "error" | "loading")[];
26
+ enum: ("after" | "before" | "empty" | "error" | "loading")[];
27
27
  description: string;
28
28
  };
29
29
  export declare const appProp: {
package/dist/mcp/tools.js CHANGED
@@ -5,6 +5,7 @@ import { resolveConfig, resolvePutDefaults, } from "../config.js";
5
5
  import { resolvePutPrefix } from "../destinations.js";
6
6
  import { ghBranchAttachmentKey, ghKeyPrefix } from "../github.js";
7
7
  import { safeCaptureFacts } from "../capture-facts.js";
8
+ import { deriveRepoSlugFromGit } from "../keys.js";
8
9
  import { validateMetaMap } from "../metadata.js";
9
10
  import { mergeDerivedMeta } from "../metadata-vocab.js";
10
11
  import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentBranch, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
@@ -430,7 +431,23 @@ export function createUploadsMcpTools(opts) {
430
431
  repoArg: optString(args, "repo") ?? defaults.repo,
431
432
  run,
432
433
  });
433
- const putMetadata = stagingTarget ? mergeStagingMeta(metadata, stagingTarget) : metadata;
434
+ // Derived `repo` metadata (spec: 2026-08-11-screenshots-project-grouping-design.md).
435
+ // Same derivation the CLI does; MCP always derives (no --no-auto), so
436
+ // this is only suppressed by noGit. metadataProp's contract: omitting
437
+ // `metadata` means "leave what's already stored for this key
438
+ // untouched" — an object (even {}) triggers a full replace. So only
439
+ // fold the derived repo into a defined object when the caller already
440
+ // supplied metadata, or branch staging is building one below anyway
441
+ // (mergeStagingMeta always returns a defined object); never
442
+ // synthesize metadata on a bare re-upload just to add repo, or a
443
+ // no-metadata put would silently wipe everything already stored.
444
+ const repoSlug = !noGit ? deriveRepoSlugFromGit(run) : undefined;
445
+ const metadataWithRepo = repoSlug !== undefined && (metadata !== undefined || stagingTarget !== undefined)
446
+ ? mergeDerivedMeta(metadata ?? {}, { repo: repoSlug })
447
+ : metadata;
448
+ const putMetadata = stagingTarget
449
+ ? mergeStagingMeta(metadataWithRepo, stagingTarget)
450
+ : metadataWithRepo;
434
451
  const putShared = {
435
452
  client,
436
453
  ghTarget: target,
@@ -737,9 +754,15 @@ export function createUploadsMcpTools(opts) {
737
754
  // Keep undefined when nothing at all was supplied or derived, so the
738
755
  // "omit to leave stored metadata untouched" contract still holds.
739
756
  const captureDerived = safeCaptureFacts(targetArg, viewport, colorSchemeArg);
740
- const metadataBase = metadata === undefined && Object.keys(captureDerived).length === 0
757
+ // Derived `repo` metadata (spec: 2026-08-11-screenshots-project-grouping-design.md).
758
+ // Same derivation the CLI does, suppressed by noGit.
759
+ const repoSlug = !noGit ? deriveRepoSlugFromGit(run) : undefined;
760
+ const captureDerivedWithRepo = repoSlug
761
+ ? { ...captureDerived, repo: repoSlug }
762
+ : captureDerived;
763
+ const metadataBase = metadata === undefined && Object.keys(captureDerivedWithRepo).length === 0
741
764
  ? undefined
742
- : mergeDerivedMeta(metadata ?? {}, captureDerived);
765
+ : mergeDerivedMeta(metadata ?? {}, captureDerivedWithRepo);
743
766
  // gh.* metadata: explicit pr/issue target wins; staging wins the same
744
767
  // way (matches attach --branch/bare put); otherwise capture-derived +
745
768
  // explicit only.
@@ -761,6 +784,10 @@ export function createUploadsMcpTools(opts) {
761
784
  hide: optStringArray(args, "hide"),
762
785
  hideDevTools: optBool(args, "noHideDevTools") ? false : undefined,
763
786
  reducedMotion: optBool(args, "reducedMotion"),
787
+ // Skip folding when an explicit key was given — key sets the
788
+ // whole object key, so there's no auto-derived name to fold
789
+ // state into.
790
+ state: keyArg ? undefined : metadataWithCaptureFacts?.state,
764
791
  apiUrl: config.apiUrl,
765
792
  token: config.token,
766
793
  });
@@ -1,8 +1,10 @@
1
- export declare const CANONICAL_META_KEYS: readonly ["url", "path", "env", "theme", "viewport", "device", "software", "captured", "state", "app"];
1
+ export declare const CANONICAL_META_KEYS: readonly ["url", "path", "env", "theme", "viewport", "device", "software", "captured", "state", "app", "repo"];
2
2
  export type CanonicalMetaKey = (typeof CANONICAL_META_KEYS)[number];
3
3
  /** Closed enum for `state` — the highest-value hand-supplied search facet. */
4
4
  export declare const META_STATE_VALUES: readonly ["before", "after", "empty", "error", "loading"];
5
5
  export type MetaStateValue = (typeof META_STATE_VALUES)[number];
6
+ /** True when `value` is exactly one of the canonical state values. */
7
+ export declare function isMetaStateValue(value: string): value is MetaStateValue;
6
8
  /**
7
9
  * Validate a `--state` value. Fails fast with a suggestion when the value is a
8
10
  * recognized near-miss, otherwise lists the valid set.
@@ -18,11 +18,16 @@ export const CANONICAL_META_KEYS = [
18
18
  "captured",
19
19
  "state",
20
20
  "app",
21
+ "repo",
21
22
  ];
22
23
  const CANONICAL_KEY_SET = new Set(CANONICAL_META_KEYS);
23
24
  /** Closed enum for `state` — the highest-value hand-supplied search facet. */
24
25
  export const META_STATE_VALUES = ["before", "after", "empty", "error", "loading"];
25
26
  const STATE_VALUE_SET = new Set(META_STATE_VALUES);
27
+ /** True when `value` is exactly one of the canonical state values. */
28
+ export function isMetaStateValue(value) {
29
+ return STATE_VALUE_SET.has(value);
30
+ }
26
31
  /** Common spellings agents reach for, mapped to the canonical `state` value. */
27
32
  const STATE_ALIASES = {
28
33
  pre: "before",
@@ -65,6 +65,14 @@ export interface CaptureScreenshotOptions {
65
65
  fullPage?: boolean;
66
66
  colorScheme?: "dark" | "light";
67
67
  waitUntil?: WaitUntil;
68
+ /**
69
+ * Folded into the auto-derived filename's stem (e.g. `-before`) so a
70
+ * before/after pair of the same URL yields two distinct object names
71
+ * instead of silently overwriting each other. Callers must omit this when
72
+ * the caller also gave an explicit object key/name — folding only applies
73
+ * to the auto-derived name.
74
+ */
75
+ state?: string;
68
76
  /** Extra CSS selectors to hide (display:none) before capture. */
69
77
  hide?: string[];
70
78
  /**
@@ -120,6 +128,26 @@ export interface CaptureScreenshotResult {
120
128
  /** Present when `measureSelectors` was given and the local backend ran. */
121
129
  measures?: Record<string, MeasuredBox>;
122
130
  }
131
+ /**
132
+ * Folds a `--state` value into an auto-derived filename's stem, e.g.
133
+ * `localhost-docs-mcp.png` + "before" → `localhost-docs-mcp-before.png`. This
134
+ * is what keeps a before/after pair (same URL, two states) from colliding on
135
+ * the same object key (issue #618) — without it, the second capture silently
136
+ * overwrote the first.
137
+ *
138
+ * No-op when `state` is undefined, and idempotent: re-folding a filename that
139
+ * already ends with `-<state>` (e.g. re-deriving from a stored filename)
140
+ * doesn't double-append.
141
+ *
142
+ * Only the canonical state values fold. Callers pass the merged metadata bag's
143
+ * `state`, which a free-form `--meta state=…` can populate with any printable
144
+ * ASCII (validateMetaMap allows `/` and spaces) — that stays metadata-only
145
+ * rather than entering the object key.
146
+ *
147
+ * Callers must skip this entirely when the caller gave an explicit key/name
148
+ * (e.g. `--key`) — this only applies to the auto-derived filename.
149
+ */
150
+ export declare function foldStateIntoFilename(filename: string, state: string | undefined): string;
123
151
  /**
124
152
  * Resolve target + options into PNG bytes via the local or remote backend.
125
153
  * Shared by the CLI `screenshot` command and the MCP `screenshot` tool.
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
12
12
  import { basename, resolve as resolvePath } from "node:path";
13
13
  import { pathToFileURL } from "node:url";
14
14
  import { UploadsError } from "./errors.js";
15
+ import { isMetaStateValue } from "./metadata-vocab.js";
15
16
  import { captureRemote, MAX_REMOTE_HTML_BYTES } from "./screenshot-remote.js";
16
17
  /**
17
18
  * Host selectors for framework dev toolbars/overlays that otherwise pollute a
@@ -159,6 +160,35 @@ function deriveFilename(target) {
159
160
  const stem = [url.hostname, pathPart].filter(Boolean).join("-");
160
161
  return `${stem || "screenshot"}.png`;
161
162
  }
163
+ /**
164
+ * Folds a `--state` value into an auto-derived filename's stem, e.g.
165
+ * `localhost-docs-mcp.png` + "before" → `localhost-docs-mcp-before.png`. This
166
+ * is what keeps a before/after pair (same URL, two states) from colliding on
167
+ * the same object key (issue #618) — without it, the second capture silently
168
+ * overwrote the first.
169
+ *
170
+ * No-op when `state` is undefined, and idempotent: re-folding a filename that
171
+ * already ends with `-<state>` (e.g. re-deriving from a stored filename)
172
+ * doesn't double-append.
173
+ *
174
+ * Only the canonical state values fold. Callers pass the merged metadata bag's
175
+ * `state`, which a free-form `--meta state=…` can populate with any printable
176
+ * ASCII (validateMetaMap allows `/` and spaces) — that stays metadata-only
177
+ * rather than entering the object key.
178
+ *
179
+ * Callers must skip this entirely when the caller gave an explicit key/name
180
+ * (e.g. `--key`) — this only applies to the auto-derived filename.
181
+ */
182
+ export function foldStateIntoFilename(filename, state) {
183
+ if (!state || !isMetaStateValue(state))
184
+ return filename;
185
+ const match = /^(.*?)(\.[^./]+)?$/.exec(filename);
186
+ const stem = match?.[1] ?? filename;
187
+ const ext = match?.[2] ?? "";
188
+ if (stem.endsWith(`-${state}`))
189
+ return filename;
190
+ return `${stem}-${state}${ext}`;
191
+ }
162
192
  /**
163
193
  * Best-effort local-browser probe used only to decide `auto` routing. Never
164
194
  * throws — any failure (e.g. optional playwright-core not installed) means
@@ -183,7 +213,7 @@ export async function captureScreenshot(opts) {
183
213
  const target = classifyTarget(opts.target);
184
214
  const viewport = opts.viewport ?? DEFAULT_SCREENSHOT_VIEWPORT;
185
215
  const waitUntil = opts.waitUntil ?? "load";
186
- const filename = deriveFilename(target);
216
+ const filename = foldStateIntoFilename(deriveFilename(target), opts.state);
187
217
  // Only private-network URLs are truly unreachable remotely; an .html file
188
218
  // is sent to the remote backend as an inline `html` body (though anything
189
219
  // it references via file:// or relative paths won't resolve there).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.37.3",
3
+ "version": "0.40.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,7 +38,7 @@
38
38
  "node": ">=22"
39
39
  },
40
40
  "peerDependencies": {
41
- "files-sdk": "^2.1.0"
41
+ "files-sdk": "^2.2.3"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "files-sdk": {
@@ -48,8 +48,8 @@
48
48
  "devDependencies": {
49
49
  "@types/node": "^26.1.0",
50
50
  "ai": "^6.0.0",
51
- "files-sdk": "^2.1.0",
52
- "typescript": "^6.0.3",
51
+ "files-sdk": "^2.2.3",
52
+ "typescript": "^7.0.2",
53
53
  "vitest": "^4.1.10"
54
54
  },
55
55
  "publishConfig": {