@buildinternet/uploads 0.42.2 → 0.44.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.
package/dist/cli-help.js CHANGED
@@ -131,7 +131,7 @@ ${section(style, "Config")} ${style.muted("(first match wins, per key):")}
131
131
  ${section(style, "Workspace")} ${style.muted("(within config layers):")}
132
132
  --workspace, -w override — global (before command) or per-command (after)
133
133
  UPLOADS_WORKSPACE env / config file
134
- (else inferred from token up_<name>_…, else "default")
134
+ (else inferred from token up_<name>_…, or the API key's only workspace)
135
135
 
136
136
  ${section(style, "Other globals")} ${style.muted("(before command):")}
137
137
  --api-url <url> default: https://api.uploads.sh
package/dist/client.d.ts CHANGED
@@ -305,6 +305,32 @@ export interface PromoteBranchAttachmentsResult {
305
305
  promoted: string[];
306
306
  skipped: PromoteSkip[];
307
307
  }
308
+ /**
309
+ * `POST /v1/workspaces/:workspace/github/attach` request/response (server
310
+ * contract, issue #702). `source` is a raw object key or an uploads.sh URL
311
+ * (storage host, embed host, or `/f/` page). Exactly one of `pr`/`issue` is
312
+ * required. Copy by default; `move: true` deletes the source object after a
313
+ * successful copy.
314
+ */
315
+ export interface AttachExistingOptions {
316
+ source: string;
317
+ repo: string;
318
+ pr?: number;
319
+ issue?: number;
320
+ move?: boolean;
321
+ filename?: string;
322
+ }
323
+ export interface AttachExistingResult {
324
+ key: string;
325
+ url: string | null;
326
+ embedUrl: string | null;
327
+ pageUrl?: string;
328
+ moved: boolean;
329
+ source: {
330
+ key: string;
331
+ };
332
+ comment: GithubCommentResult;
333
+ }
308
334
  /** `GET`/`POST /v1/workspaces/:workspace/github/link` result (server contract, phase 4b). */
309
335
  export interface GithubLinkResult {
310
336
  repo: string;
@@ -565,7 +591,7 @@ export declare function mintWorkspaceToken(apiUrl: string, accessToken: string,
565
591
  workspace: string;
566
592
  scopes?: Array<TokenScope>;
567
593
  label?: string;
568
- ttlSeconds?: number;
594
+ ttlSeconds?: number | null;
569
595
  }): Promise<MintTokenResult>;
570
596
  /**
571
597
  * Parse API error bodies. Prefers the nested envelope
@@ -670,6 +696,15 @@ export declare function createUploadsClient(config: UploadsClientConfig): {
670
696
  * that doesn't have this route yet, as "nothing promoted").
671
697
  */
672
698
  promoteBranchAttachments(opts: PromoteBranchAttachmentsOptions): Promise<PromoteBranchAttachmentsResult>;
699
+ /**
700
+ * Attach an already-uploaded object to a PR/issue via a server-side copy
701
+ * (issue #702) — see `AttachExistingOptions`. Throws `UploadsError` on
702
+ * any failure (including a 404 from an older/self-hosted server without
703
+ * this route, or `source_not_found`) — unlike the degrade-safe promote/
704
+ * comment calls, this is the CLI's only way to move the object, so a
705
+ * failure must be visible, not silently swallowed.
706
+ */
707
+ attachExisting(opts: AttachExistingOptions): Promise<AttachExistingResult>;
673
708
  /** Current binding for `repo`, or `{ linked: false }` if unclaimed. Throws
674
709
  * `UploadsError` (status 404) on an older/self-hosted server without this
675
710
  * route — callers treat that as "bindings unsupported". */
package/dist/client.js CHANGED
@@ -179,7 +179,7 @@ export function mintWorkspaceToken(apiUrl, accessToken, input) {
179
179
  body: JSON.stringify({
180
180
  grants: [{ workspace: input.workspace, ...(input.scopes ? { scopes: input.scopes } : {}) }],
181
181
  ...(input.label ? { label: input.label } : {}),
182
- ...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}),
182
+ ...(input.ttlSeconds !== undefined ? { ttlSeconds: input.ttlSeconds } : {}),
183
183
  }),
184
184
  });
185
185
  }
@@ -582,6 +582,20 @@ export function createUploadsClient(config) {
582
582
  headers: { "Content-Type": "application/json" },
583
583
  });
584
584
  },
585
+ /**
586
+ * Attach an already-uploaded object to a PR/issue via a server-side copy
587
+ * (issue #702) — see `AttachExistingOptions`. Throws `UploadsError` on
588
+ * any failure (including a 404 from an older/self-hosted server without
589
+ * this route, or `source_not_found`) — unlike the degrade-safe promote/
590
+ * comment calls, this is the CLI's only way to move the object, so a
591
+ * failure must be visible, not silently swallowed.
592
+ */
593
+ async attachExisting(opts) {
594
+ return request("POST", `${config.apiUrl}/v1/workspaces/${encodeURIComponent(config.workspace)}/github/attach`, {
595
+ body: new TextEncoder().encode(JSON.stringify(opts)),
596
+ headers: { "Content-Type": "application/json" },
597
+ });
598
+ },
585
599
  /** Current binding for `repo`, or `{ linked: false }` if unclaimed. Throws
586
600
  * `UploadsError` (status 404) on an older/self-hosted server without this
587
601
  * route — callers treat that as "bindings unsupported". */
@@ -18,7 +18,7 @@ Subcommands:
18
18
  Keys:
19
19
  UPLOADS_API_URL API base URL (default: ${DEFAULT_API_URL})
20
20
  UPLOADS_WORKSPACE Workspace / bucket tenant (default: ${DEFAULT_WORKSPACE})
21
- UPLOADS_TOKEN Bearer token for the workspace
21
+ UPLOADS_TOKEN Bearer token (up_<workspace>_…)
22
22
  UPLOADS_SESSION_TOKEN Device-flow session (CLI version on account sessions)
23
23
  UPLOADS_DEFAULT_PREFIX Default key prefix for put/list
24
24
  UPLOADS_DEFAULT_REPO Default repo segment for put
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * `uploads hook pre-pr-screenshot` — agent PreToolUse / beforeShellExecution
3
- * handler. When the shell command is `gh pr create`, the branch touches UI
4
- * files, and nothing is staged on uploads.sh, emit a non-blocking advisory.
3
+ * handler, triggered on `gh pr create`. Two advisories, mutually exclusive:
4
+ * - staged-but-unattached files exist for the branch (any reason they got
5
+ * there) → a promote suggestion (issue #700): `uploads attach --promote`
6
+ * once the PR this command is about to open exists.
7
+ * - nothing is staged, but the branch touches UI files → the original
8
+ * (issue #379) "consider staging screenshots" advisory.
5
9
  *
6
10
  * Always fail-open. Disable with UPLOADS_HOOK_DISABLE=1.
7
11
  */
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * `uploads hook pre-pr-screenshot` — agent PreToolUse / beforeShellExecution
3
- * handler. When the shell command is `gh pr create`, the branch touches UI
4
- * files, and nothing is staged on uploads.sh, emit a non-blocking advisory.
3
+ * handler, triggered on `gh pr create`. Two advisories, mutually exclusive:
4
+ * - staged-but-unattached files exist for the branch (any reason they got
5
+ * there) → a promote suggestion (issue #700): `uploads attach --promote`
6
+ * once the PR this command is about to open exists.
7
+ * - nothing is staged, but the branch touches UI files → the original
8
+ * (issue #379) "consider staging screenshots" advisory.
5
9
  *
6
10
  * Always fail-open. Disable with UPLOADS_HOOK_DISABLE=1.
7
11
  */
@@ -22,9 +26,12 @@ Invoked by Claude Code / Codex / Grok / Cursor hooks. Never blocks.
22
26
  Harness manifests no-op (exit 0, no output) when this binary is not on PATH.
23
27
 
24
28
  pre-pr-screenshot
25
- If the shell command is \`gh pr create\`, the branch touches UI files, and
26
- no screenshots are staged for the branch, emit an advisory to stage with
27
- \`uploads attach --branch\`.
29
+ If the shell command is \`gh pr create\`:
30
+ - staged-but-unattached files exist for the branch suggest promoting
31
+ them into the PR's managed comment once it exists (issue #700):
32
+ \`uploads attach --promote --pr <num>\`.
33
+ - otherwise, if the branch touches UI files → suggest staging with
34
+ \`uploads attach … --branch\`.
28
35
 
29
36
  Disable with UPLOADS_HOOK_DISABLE=1.
30
37
  `;
@@ -158,14 +165,29 @@ export async function runPrePrScreenshot(deps) {
158
165
  const branch = git.branch();
159
166
  if (!branch)
160
167
  return null;
168
+ const staged = await (deps.countStaged ?? defaultCountStaged)(branch);
169
+ if (staged === null)
170
+ return null; // error/unconfigured → fail open
171
+ // Promote suggestion (issue #700): staged-but-unattached files already
172
+ // exist for this branch right as its PR is about to open. The PR doesn't
173
+ // exist yet at this PreToolUse point, so its number isn't knowable here —
174
+ // the wording still gives the exact command shape, and a bare
175
+ // `attach --promote` (which infers the PR from the branch) works too.
176
+ if (staged > 0) {
177
+ const fork = (deps.isFork ?? (() => defaultIsFork(cwd)))();
178
+ const forkNote = fork === true
179
+ ? " Note: this looks like a fork branch, so staged screenshots won't auto-promote into the PR comment yet (see issue #317) — attach them manually if you use uploads."
180
+ : "";
181
+ const message = `${staged} file${staged === 1 ? "" : "s"} staged for branch '${branch}' on uploads.sh ` +
182
+ `${staged === 1 ? "isn't" : "aren't"} attached to a pull request yet. Once this PR opens, run ` +
183
+ "`uploads attach --promote --pr <num>` (or a bare `uploads attach --promote`, which infers " +
184
+ `the PR from the branch) to collect ${staged === 1 ? "it" : "them"} into the managed attachments comment.${forkNote}`;
185
+ return formatAdvisory(message, isCursorHookInput(raw));
186
+ }
161
187
  const testFiles = deps.testFiles ?? process.env.UPLOADS_HOOK_TEST_FILES;
162
188
  const changed = testFiles ? testFiles.split("\n").filter(Boolean) : git.changedFiles();
163
189
  if (!anyVisual(changed))
164
190
  return null;
165
- const staged = await (deps.countStaged ?? defaultCountStaged)(branch);
166
- // null = error/unconfigured → fail open; >0 = already staged
167
- if (staged === null || staged > 0)
168
- return null;
169
191
  const fork = (deps.isFork ?? (() => defaultIsFork(cwd)))();
170
192
  const forkNote = fork === true
171
193
  ? " Note: this looks like a fork branch, so staged screenshots won't auto-promote into the PR comment yet (see issue #317) — attach them manually if you use uploads."
@@ -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, writeReplacedNote, resolveGhPrefixSafe, } from "../commands.js";
5
+ import { branchFromFlags, derivedMetaEnabled, frameOptionsFromFlags, ghTargetFromFlags, optimizeOptionsFromFlags, stateAppMetaFromFlags, warnNearMissMeta, syncAttachmentsComment, commentViaSuffix, uploadPreparedImage, resolvePutStagingTarget, putStagingNoteText, resolveStageBindingWarning, mergeStagingMeta, writeReplacedNote, resolveGhPrefixSafe, resolveAutoPrTarget, resolvePutNudgeContext, putNudgeText, autoPrNoteText, } 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 { deriveRepoSlugFromGit } from "../keys.js";
11
+ import { noProjectContextNudge } from "../project-context-nudge.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";
@@ -43,9 +44,18 @@ sets the whole object key verbatim (no folding).
43
44
  After capture, screenshots share the put upload pipeline: optional --frame,
44
45
  optimize-by-default, --pr/--issue attachment + --comment, --gallery, --meta.
45
46
 
46
- Branch staging by default (pre-PR): with no --pr/--issue/--branch/--key/--ref/
47
- --prefix/--destination, a screenshot taken on a non-default git branch stages
48
- under gh/<owner>/<repo>/branch/<branch>/<name> instead of the dated
47
+ Auto-PR context (issue #700): with no --pr/--issue/--branch/--key/--ref/
48
+ --prefix/--destination, a screenshot taken on a branch that maps to exactly
49
+ one open PR behaves as if --pr <n> had been passed — stable gh/ key, managed
50
+ comment sync (with --comment) — instead of branch staging below. A one-line
51
+ note announces this. Opt out with --no-pr, UPLOADS_NO_AUTO_PR=1, or config
52
+ UPLOADS_NO_AUTO_PR=1; never fires outside a git repo, on the default branch,
53
+ with --no-git, or when no single open PR can be resolved.
54
+
55
+ Branch staging by default (pre-PR): when auto-PR above doesn't apply and none
56
+ of --pr/--issue/--branch/--key/--ref/--prefix/--destination is given, a
57
+ screenshot taken on a non-default git branch stages under
58
+ gh/<owner>/<repo>/branch/<branch>/<name> instead of the dated
49
59
  screenshots/<repo>/<date>/... layout — same key/metadata as an explicit
50
60
  --branch, carrying every derived fact (path/url/env/viewport, --state) along.
51
61
  Staged files auto-attach with full metadata the first time you attach to that
@@ -101,6 +111,7 @@ Options:
101
111
  --optimize-max-edge <px> Max long edge when optimizing (default: 2400)
102
112
  --optimize-quality <1-100> WebP quality (default: 85)
103
113
  --keep-exif Keep EXIF/XMP/ICC when optimizing
114
+ --no-pr Skip auto-PR context (or UPLOADS_NO_AUTO_PR=1) — see above
104
115
  --pr <num> Attach to a pull request (stable URL, no hash)
105
116
  --issue <num> Attach to an issue
106
117
  --branch [name] Stage against a branch, pre-PR (default: current git branch):
@@ -319,7 +330,32 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
319
330
  }
320
331
  const putDefaults = resolvePutDefaults({ envFile: ctx.envFile }, rawDefaults);
321
332
  const noGit = flagBool(parsed.flags, "--no-git") || putDefaults.noGit === true;
333
+ if (parsed.flags.has("--no-pr") && typeof parsed.flags.get("--no-pr") === "string") {
334
+ throw new UsageError("--no-pr takes no value");
335
+ }
336
+ const noAutoPr = flagBool(parsed.flags, "--no-pr") || putDefaults.noAutoPr === true;
322
337
  const branchRepo = branchArg !== undefined ? resolveRepo(flagString(parsed.flags, "--repo"), run) : undefined;
338
+ // Auto-PR context (issue #700): when no --branch/--pr/--issue/--key/--ref/
339
+ // --prefix/--destination is given, git use isn't disabled, and --no-pr/
340
+ // UPLOADS_NO_AUTO_PR hasn't opted out, a screenshot taken on a branch that
341
+ // maps to exactly one open PR behaves as if --pr <n> had been passed —
342
+ // stable key + managed comment sync — instead of the #469 auto-staging
343
+ // default below. Mirrors put's #700 handling exactly (resolveAutoPrTarget).
344
+ const autoPrTarget = ghTarget || branchArg !== undefined
345
+ ? undefined
346
+ : resolveAutoPrTarget({
347
+ ghTarget,
348
+ keyHint,
349
+ refArg: flagString(parsed.flags, "--ref"),
350
+ prefixArg: prefixFlag,
351
+ destinationArg: destFlag,
352
+ branchArg,
353
+ noGit,
354
+ noAutoPr,
355
+ repoArg: flagString(parsed.flags, "--repo") ?? putDefaults.repo,
356
+ run,
357
+ });
358
+ const effectiveGhTarget = ghTarget ?? autoPrTarget;
323
359
  // Auto branch staging (issue #469 lever 1): mirrors bare `put`'s auto-staging
324
360
  // (issue #403). When no --branch/--pr/--issue/--key/--ref/--prefix/--destination
325
361
  // is given and git use isn't disabled, a screenshot taken on a non-default
@@ -328,10 +364,11 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
328
364
  // `screenshots/<repo>/<date>/...` layout. This is what lets derived
329
365
  // metadata (path/url/env/viewport, --state) ride through to PR-open
330
366
  // promotion when the capture happens before the PR exists. Skipped
331
- // entirely when --branch was given explicitly (already handled above).
367
+ // entirely when --branch was given explicitly (already handled above), or
368
+ // when the #700 auto-PR match above already took over.
332
369
  const autoStagingTarget = branchArg === undefined
333
370
  ? resolvePutStagingTarget({
334
- ghTarget,
371
+ ghTarget: effectiveGhTarget,
335
372
  keyHint,
336
373
  refArg: flagString(parsed.flags, "--ref"),
337
374
  prefixArg: prefixFlag,
@@ -348,7 +385,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
348
385
  destination: destFlag,
349
386
  prefix: prefixFlag,
350
387
  key: keyHint,
351
- ghAttachment: Boolean(ghTarget) || stagingTarget !== undefined,
388
+ ghAttachment: Boolean(effectiveGhTarget) || stagingTarget !== undefined,
352
389
  });
353
390
  }
354
391
  catch (err) {
@@ -378,8 +415,8 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
378
415
  ...(repoSlug ? { repo: repoSlug } : {}),
379
416
  });
380
417
  let metadata = withFacts;
381
- if (ghTarget) {
382
- metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(ghTarget, run) };
418
+ if (effectiveGhTarget) {
419
+ metadata = { ...withFacts, ...ghMetadataFromTargetWithTitle(effectiveGhTarget, run) };
383
420
  validateMetaMap(metadata);
384
421
  }
385
422
  else if (stagingTarget !== undefined) {
@@ -388,6 +425,9 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
388
425
  else if (Object.keys(withFacts).length > 0) {
389
426
  validateMetaMap(withFacts);
390
427
  }
428
+ // #692 follow-up: same advisory as put — a capture with no repo/app context
429
+ // and only a local origin lands in the screenshots page's fallback buckets.
430
+ const contextNudge = !ctx.quiet && !putDefaults.noNudge && !noGit ? noProjectContextNudge(metadata) : undefined;
391
431
  const logHuman = !ctx.quiet && format === "human";
392
432
  if (logHuman)
393
433
  process.stderr.write(`>> capturing ${target}\n`);
@@ -471,10 +511,10 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
471
511
  // Resolved once (issue #631), only when it's actually needed for the
472
512
  // upload about to happen (never for the noUpload/no-target bailouts
473
513
  // above) — never per file (screenshot only ever uploads one).
474
- const ghPrefix = ghTarget
514
+ const ghPrefix = effectiveGhTarget
475
515
  ? await resolveGhPrefixSafe(ctx.client, {
476
- repo: ghTarget.repo,
477
- target: { kind: ghTarget.kind, num: ghTarget.num },
516
+ repo: effectiveGhTarget.repo,
517
+ target: { kind: effectiveGhTarget.kind, num: effectiveGhTarget.num },
478
518
  })
479
519
  : stagingTarget !== undefined
480
520
  ? await resolveGhPrefixSafe(ctx.client, {
@@ -482,11 +522,29 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
482
522
  branch: stagingTarget.branch,
483
523
  })
484
524
  : undefined;
525
+ // Bare-screenshot nudge context (issue #393/#700): only relevant when
526
+ // neither auto-PR nor staging took over — mirrors put's handling exactly.
527
+ // Resolved before upload; finished into text below once the key is known.
528
+ const nudgeContext = effectiveGhTarget || stagingTarget
529
+ ? undefined
530
+ : resolvePutNudgeContext({
531
+ quiet: ctx.quiet,
532
+ noNudge: putDefaults.noNudge === true,
533
+ ghTarget,
534
+ keyHint,
535
+ hasBranchFlag: branchArg !== undefined,
536
+ noGit,
537
+ repoArg: flagString(parsed.flags, "--repo") ?? putDefaults.repo,
538
+ run,
539
+ });
540
+ const autoPrNote = autoPrTarget && !ctx.quiet && !putDefaults.noNudge
541
+ ? autoPrNoteText(autoPrTarget.num)
542
+ : undefined;
485
543
  const alt = altFlag ?? basename(captured.filename);
486
544
  const { result, prepared, markdown } = await uploadPreparedImage(ctx.client, finalPng, captured.filename, {
487
545
  frame: frameOpts,
488
546
  optimize: optimizeOpts,
489
- ghTarget,
547
+ ghTarget: effectiveGhTarget,
490
548
  ghBranchTarget: stagingTarget,
491
549
  ghPrefix,
492
550
  key: keyHint,
@@ -528,11 +586,16 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
528
586
  gallery = { id: galleryId, error: err instanceof Error ? err.message : String(err) };
529
587
  }
530
588
  }
589
+ // Concrete bare-screenshot nudge text (issue #700): built once the upload
590
+ // key exists, so the ready-made follow-up names it verbatim.
591
+ const nudge = nudgeContext
592
+ ? putNudgeText(nudgeContext.branch, nudgeContext.pr, [result.key])
593
+ : undefined;
531
594
  let comment;
532
595
  let commentError;
533
- if (wantComment && ghTarget) {
596
+ if (wantComment && effectiveGhTarget) {
534
597
  try {
535
- comment = await syncAttachmentsComment(ctx.client, ghTarget, run, ctx.config.workspace);
598
+ comment = await syncAttachmentsComment(ctx.client, effectiveGhTarget, run, ctx.config.workspace);
536
599
  if (logHuman)
537
600
  process.stderr.write(`>> attachments comment ${comment.action}${commentViaSuffix(comment.via)}\n`);
538
601
  }
@@ -560,23 +623,36 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
560
623
  `(or run \`uploads attach --promote\` after opening)\n`);
561
624
  }
562
625
  }
626
+ if (autoPrNote)
627
+ process.stderr.write(`${autoPrNote}\n`);
628
+ if (nudge)
629
+ process.stderr.write(`${nudge}\n`);
563
630
  if (bindingWarning)
564
631
  process.stderr.write(`${bindingWarning}\n`);
632
+ if (contextNudge)
633
+ process.stderr.write(`${contextNudge}\n`);
565
634
  process.stderr.write("\n");
566
635
  }
567
636
  // One JSON `hint` slot (mirrors bare put): the clip note (issue #652) wins
568
637
  // first — it's about the just-captured image itself, more immediately
569
638
  // actionable than the other three, which are about upload/staging
570
- // mechanics. Then the binding warning, more actionable than the generic
571
- // staging note; a replaced-object note (issue #618) is lowest priority
572
- // it only surfaces when nothing else already claimed the slot. Since state
573
- // folds into the derived key, replaced + state means a same-side re-capture,
639
+ // mechanics. Then the auto-PR note and the #393/#700 nudge (issue #700),
640
+ // then the binding warning, more actionable than the generic staging note;
641
+ // a replaced-object note (issue #618) is lowest priority it only
642
+ // surfaces when nothing else already claimed the slot. Since state folds
643
+ // into the derived key, replaced + state means a same-side re-capture,
574
644
  // which is the intended replace-in-place flow — word it as informational,
575
645
  // not as a problem.
576
646
  const replacedHint = result.replaced && explicitMeta.state
577
647
  ? `re-capture replaced the previous state=${explicitMeta.state} object at ${result.key} — expected for repeat captures of the same URL + state`
578
648
  : undefined;
579
- const jsonHint = clipHint ?? bindingWarning ?? stagingNote ?? replacedHint;
649
+ const jsonHint = clipHint ??
650
+ autoPrNote ??
651
+ nudge ??
652
+ bindingWarning ??
653
+ stagingNote ??
654
+ replacedHint ??
655
+ contextNudge;
580
656
  switch (format) {
581
657
  case "json":
582
658
  await writeJson({
@@ -411,6 +411,82 @@ export declare function resolveStageBindingWarning(opts: {
411
411
  defaults: PutDefaults;
412
412
  repo: string;
413
413
  }): Promise<string | undefined>;
414
+ /**
415
+ * The bare-put nudge's wording (issue #393, made concrete by issue #700):
416
+ * teaches `--pr`/`attach --branch` as an upgrade from a targetless `put`.
417
+ * `pr` present → names the PR and, once upload `keys` are known, appends a
418
+ * ready-made follow-up command naming them verbatim (e.g. `uploads attach
419
+ * --pr 1250 f/abc123.webp`); otherwise a generic variant that still points
420
+ * at `--pr <num>`. Used verbatim for both the human-mode stderr line and the
421
+ * JSON `hint` field.
422
+ */
423
+ export declare function putNudgeText(branch: string, pr: number | undefined, keys?: string[]): string;
424
+ /**
425
+ * Auto-PR note (issue #700): announces at the moment it fires that a bare
426
+ * put/screenshot on this branch was auto-attached to `pr` — the default
427
+ * behavior change this issue introduces — and how to opt out.
428
+ */
429
+ export declare function autoPrNoteText(pr: number): string;
430
+ /**
431
+ * Best-effort bare-put/screenshot nudge context (issue #393): resolves the
432
+ * branch and, when detectable, the open PR for it — fires only when there is
433
+ * no targeting flag at all (`--pr`/`--issue`/`--key`; `--branch` too, though
434
+ * `put` doesn't currently accept it — defensive parity with `attach`), is
435
+ * inside a git repo (reusing `deriveRepoFromGit`, the same detection the
436
+ * default screenshot key's repo segment uses), and the current branch isn't
437
+ * the default one. Never throws — any failure (not a repo, detached HEAD,
438
+ * `gh` missing/unauthenticated/timed out) degrades to "no nudge" or, once a
439
+ * branch is already known, to a context with `pr: undefined` (the generic
440
+ * no-PR wording). Must never affect put's exit code, stdout, or upload
441
+ * behavior. Callers turn the result into text via `putNudgeText`, once any
442
+ * upload keys are known.
443
+ */
444
+ export declare function resolvePutNudgeContext(opts: {
445
+ quiet: boolean;
446
+ noNudge: boolean;
447
+ ghTarget: GhTarget | undefined;
448
+ keyHint: string | undefined;
449
+ /** True when an explicit `--branch`-style flag was given (CLI `attach`
450
+ * parity; `put`/MCP `put` don't accept one — pass false there). */
451
+ hasBranchFlag?: boolean;
452
+ noGit: boolean;
453
+ repoArg: string | undefined;
454
+ run: CommandRunner;
455
+ }): {
456
+ branch: string;
457
+ pr: number | undefined;
458
+ } | undefined;
459
+ /**
460
+ * Auto-PR context (issue #700): when a bare put/screenshot has no explicit
461
+ * destination flag at all (`--pr`/`--issue`/`--key`/`--ref`/`--prefix`/
462
+ * `--destination`, and for `screenshot` no explicit `--branch`) and runs on a
463
+ * branch that maps to exactly one open PR, this resolves that PR so the
464
+ * caller can behave as if `--pr <n>` had been passed — stable key + managed
465
+ * comment sync — instead of the #403/#469 staging default or the plain dated
466
+ * layout. `resolveCurrentPullRequest`'s `gh pr view <branch>` lookup is
467
+ * already the unambiguous case: it names the single open PR whose head is
468
+ * that branch, or fails (no open PR, or `gh` unavailable/unauthenticated) —
469
+ * there is no "ambiguous, more than one" state to further disambiguate.
470
+ * Opt-out: `noAutoPr` (the caller folds in `--no-pr` and
471
+ * `UPLOADS_NO_AUTO_PR=1`/config). Never fires outside a git checkout, on the
472
+ * default branch, or with `--no-git`; any failure (not a repo, detached
473
+ * HEAD, gh missing/unauthenticated/timed out, no open PR) degrades to
474
+ * undefined so the caller falls back to its normal staging/dated behavior.
475
+ */
476
+ export declare function resolveAutoPrTarget(opts: {
477
+ ghTarget: GhTarget | undefined;
478
+ keyHint: string | undefined;
479
+ refArg: string | undefined;
480
+ prefixArg: string | undefined;
481
+ destinationArg: string | undefined;
482
+ /** Explicit `--branch` (screenshot only) also opts out — put has no
483
+ * `--branch` flag today, so callers pass undefined there. */
484
+ branchArg?: string | undefined;
485
+ noGit: boolean;
486
+ noAutoPr: boolean;
487
+ repoArg: string | undefined;
488
+ run: CommandRunner;
489
+ }): GhTarget | undefined;
414
490
  /**
415
491
  * Bare-put branch-staging trigger (issue #403): put on a non-default git
416
492
  * branch stages to the branch prefix by default — the branch becomes the