@buildinternet/uploads 0.43.0 → 0.45.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.
@@ -13,6 +13,7 @@ export interface RepoCommentConfig {
13
13
  note?: string;
14
14
  ingestGithubAttachments?: boolean;
15
15
  ingestBotAttachments?: boolean;
16
+ adoptLinkedFiles?: boolean;
16
17
  }
17
18
  export interface WorkspaceCommentDefaults {
18
19
  imageWidth?: "full" | number;
@@ -22,6 +23,7 @@ export interface WorkspaceCommentDefaults {
22
23
  note?: string;
23
24
  ingestGithubAttachments?: boolean;
24
25
  ingestBotAttachments?: boolean;
26
+ adoptLinkedFiles?: boolean;
25
27
  }
26
28
  export interface ResolvedCommentOptions {
27
29
  imageWidth: "auto" | "full" | number;
@@ -32,6 +34,7 @@ export interface ResolvedCommentOptions {
32
34
  note: string | null;
33
35
  ingestGithubAttachments: boolean;
34
36
  ingestBotAttachments: boolean;
37
+ adoptLinkedFiles: boolean;
35
38
  }
36
39
  export type OptionSource = "repo" | "workspace" | "auto";
37
40
  export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
@@ -14,6 +14,9 @@ export const AUTO_COMMENT_OPTIONS = {
14
14
  note: null,
15
15
  ingestGithubAttachments: true,
16
16
  ingestBotAttachments: false,
17
+ // Default ON for bound repos (issue #701) — binding is already an
18
+ // explicit opt-in relationship, same posture as ingestGithubAttachments.
19
+ adoptLinkedFiles: true,
17
20
  };
18
21
  export const NOTE_MAX_CHARS = 500;
19
22
  const WIDTH_MIN = 160;
@@ -81,6 +84,14 @@ export function parseRepoCommentConfig(text, format) {
81
84
  else
82
85
  warnings.push(`ingestBotAttachments: expected a boolean; dropped`);
83
86
  }
87
+ // adoptLinkedFiles: boolean
88
+ if ("adoptLinkedFiles" in c) {
89
+ const v = c.adoptLinkedFiles;
90
+ if (typeof v === "boolean")
91
+ config.adoptLinkedFiles = v;
92
+ else
93
+ warnings.push(`adoptLinkedFiles: expected a boolean; dropped`);
94
+ }
84
95
  // meta.path / meta.state: booleans nested under `meta`
85
96
  if ("meta" in c) {
86
97
  const v = c.meta;
@@ -139,6 +150,7 @@ export function resolveCommentOptions(repo, ws) {
139
150
  ...(ws?.ingestBotAttachments !== undefined
140
151
  ? { ingestBotAttachments: ws.ingestBotAttachments }
141
152
  : {}),
153
+ ...(ws?.adoptLinkedFiles !== undefined ? { adoptLinkedFiles: ws.adoptLinkedFiles } : {}),
142
154
  };
143
155
  const options = { ...AUTO_COMMENT_OPTIONS };
144
156
  const source = Object.fromEntries(Object.keys(AUTO_COMMENT_OPTIONS).map((k) => [k, "auto"]));
@@ -151,6 +163,7 @@ export function resolveCommentOptions(repo, ws) {
151
163
  "linkToFilePage",
152
164
  "ingestGithubAttachments",
153
165
  "ingestBotAttachments",
166
+ "adoptLinkedFiles",
154
167
  ]) {
155
168
  if (cfg[key] !== undefined && source[key] === "auto") {
156
169
  options[key] = cfg[key];
@@ -147,5 +147,8 @@ export declare function attachmentImageWidth(filename: string, density?: Attachm
147
147
  /**
148
148
  * Render the one marker-owned GitHub comment. When there are no galleries this
149
149
  * intentionally preserves the legacy attachment-only body byte-for-byte.
150
+ *
151
+ * `target` is optional so goldens, the settings preview, and other callers
152
+ * without a PR/issue stay on the generic `--pr <N>` hint.
150
153
  */
151
- export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[], marker?: string, options?: CommentRenderOptions): string;
154
+ export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[], marker?: string, options?: CommentRenderOptions, target?: Pick<GhTarget, "kind" | "num">): string;
@@ -390,11 +390,24 @@ function countInlinableMedia(sorted, maxInlineImages) {
390
390
  }
391
391
  return count;
392
392
  }
393
+ /**
394
+ * Quiet add-media footer. When `target` is a real PR/issue, the command
395
+ * uses that number (`--pr 12` / `--issue 45`) so anyone reading the comment
396
+ * can copy it. Without a target the generic `--pr <N>` placeholder stays.
397
+ */
398
+ function addMediaFooter(target) {
399
+ const flag = target?.kind === "issues" ? "--issue" : "--pr";
400
+ const n = target && Number.isInteger(target.num) && target.num > 0 ? String(target.num) : "&lt;N&gt;";
401
+ return `<sub>Maintained by <a href="https://uploads.sh">uploads.sh</a> · add media: <code>uploads put &lt;file&gt; ${flag} ${n}</code> · <a href="https://uploads.sh/docs/github-app">docs</a></sub>`;
402
+ }
393
403
  /**
394
404
  * Render the one marker-owned GitHub comment. When there are no galleries this
395
405
  * intentionally preserves the legacy attachment-only body byte-for-byte.
406
+ *
407
+ * `target` is optional so goldens, the settings preview, and other callers
408
+ * without a PR/issue stay on the generic `--pr <N>` hint.
396
409
  */
397
- export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMENTS_MARKER, options = AUTO_RENDER_OPTIONS) {
410
+ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMENTS_MARKER, options = AUTO_RENDER_OPTIONS, target) {
398
411
  // Non-mutating sort (equivalent to Array#toSorted) — the api worker's
399
412
  // tsconfig targets lib ES2022, which predates Array#toSorted (ES2023);
400
413
  // packages/uploads/src/github.ts uses toSorted directly.
@@ -529,6 +542,6 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
529
542
  // Footer condensed to a single quiet line — the old two-line explainer
530
543
  // ("re-uploading updates everywhere", full add-media flags) repeats on
531
544
  // every PR and lost value with each appearance; details live in the docs.
532
- lines.push('<sub>Maintained by <a href="https://uploads.sh">uploads.sh</a> · add media: <code>uploads put &lt;file&gt; --pr &lt;N&gt;</code> · <a href="https://uploads.sh/docs/github-app">docs</a></sub>');
545
+ lines.push(addMediaFooter(target));
533
546
  return lines.join("\n");
534
547
  }
@@ -1,7 +1,7 @@
1
1
  import type { UploadsClientConfig } from "./config.js";
2
2
  export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN",
3
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"];
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", "UPLOADS_NO_AUTO_PR"];
5
5
  export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
6
6
  export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
7
7
  export interface PutDefaults {
@@ -18,6 +18,9 @@ export interface PutDefaults {
18
18
  noAutoMeta?: boolean;
19
19
  /** When true, `put` never prints the bare-put --pr/attach nudge (issue #393). */
20
20
  noNudge?: boolean;
21
+ /** When true, a bare put/screenshot never auto-assumes an unambiguous open
22
+ * PR's context (issue #700) — falls back to the #403/#469 staging default. */
23
+ noAutoPr?: boolean;
21
24
  }
22
25
  declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
23
26
  export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
@@ -17,6 +17,7 @@ export const UPLOADS_CONFIG_KEYS = [
17
17
  "UPLOADS_NO_AUTO_META",
18
18
  "UPLOADS_SCREENSHOT_VIA",
19
19
  "UPLOADS_NO_NUDGE",
20
+ "UPLOADS_NO_AUTO_PR",
20
21
  ];
21
22
  const PUT_DEFAULT_KEY_MAP = {
22
23
  prefix: "UPLOADS_DEFAULT_PREFIX",
@@ -28,6 +29,7 @@ const PUT_DEFAULT_KEY_MAP = {
28
29
  keepExif: "UPLOADS_KEEP_EXIF",
29
30
  noAutoMeta: "UPLOADS_NO_AUTO_META",
30
31
  noNudge: "UPLOADS_NO_NUDGE",
32
+ noAutoPr: "UPLOADS_NO_AUTO_PR",
31
33
  };
32
34
  function isTruthyConfigFlag(value) {
33
35
  if (!value)
@@ -55,6 +57,8 @@ export function putDefaultsToConfigValues(defaults) {
55
57
  out.UPLOADS_NO_AUTO_META = "1";
56
58
  if (defaults.noNudge)
57
59
  out.UPLOADS_NO_NUDGE = "1";
60
+ if (defaults.noAutoPr)
61
+ out.UPLOADS_NO_AUTO_PR = "1";
58
62
  return out;
59
63
  }
60
64
  function parsePutDefaultsFromRaw(raw) {
@@ -80,6 +84,8 @@ function parsePutDefaultsFromRaw(raw) {
80
84
  out.noAutoMeta = true;
81
85
  if (isTruthyConfigFlag(raw.UPLOADS_NO_NUDGE))
82
86
  out.noNudge = true;
87
+ if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_PR))
88
+ out.noAutoPr = true;
83
89
  return out;
84
90
  }
85
91
  function parsePutDefaultsFromEnv() {
@@ -102,6 +108,8 @@ function parsePutDefaultsFromEnv() {
102
108
  raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
103
109
  if (process.env.UPLOADS_NO_NUDGE)
104
110
  raw.UPLOADS_NO_NUDGE = process.env.UPLOADS_NO_NUDGE;
111
+ if (process.env.UPLOADS_NO_AUTO_PR)
112
+ raw.UPLOADS_NO_AUTO_PR = process.env.UPLOADS_NO_AUTO_PR;
105
113
  return parsePutDefaultsFromRaw(raw);
106
114
  }
107
115
  /** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
@@ -175,6 +183,8 @@ export function mergePutDefaults(...layers) {
175
183
  out.noAutoMeta = layer.noAutoMeta;
176
184
  if (layer.noNudge != null)
177
185
  out.noNudge = layer.noNudge;
186
+ if (layer.noAutoPr != null)
187
+ out.noAutoPr = layer.noAutoPr;
178
188
  }
179
189
  return out;
180
190
  }
@@ -98,3 +98,30 @@ export declare function upsertAttachmentsComment(target: GhTarget, body: string,
98
98
  }): {
99
99
  action: "created" | "updated" | "skipped";
100
100
  };
101
+ /** Cheap, regex-only reject: no http(s) URL at all means nothing to scan for.
102
+ * Mirrors `hasLinkCandidate` in apps/api/src/github-link-adopt.ts. */
103
+ export declare function hasLinkCandidate(text: string): boolean;
104
+ /**
105
+ * Distinct http(s) URLs found in `text`, trailing prose punctuation
106
+ * stripped, order preserved, first occurrence wins on duplicates. Ported
107
+ * verbatim from apps/api/src/github-link-adopt.ts's `extractCandidateUrls`
108
+ * so the two paths recognize the same URL spellings — resolution itself
109
+ * (storage host / embed host / `/f/` page → key, and the bound-workspace
110
+ * check) happens server-side inside `POST .../github/attach`, so this file
111
+ * doesn't need its own copy of that logic.
112
+ */
113
+ export declare function extractCandidateUrls(text: string): string[];
114
+ /**
115
+ * Best-effort concatenation of a PR/issue's current body plus every comment's
116
+ * current body, for link-adoption scanning. Returns "" on any `gh` failure
117
+ * (not authenticated, network, repo/number not found) — adoption degrades to
118
+ * a no-op rather than blocking the comment sync it rides along with.
119
+ *
120
+ * Unlike the webhook path (which re-scans one specific body/comment ref per
121
+ * event), the CLI has no per-event ref to key off of — `uploads comment` is
122
+ * one ad-hoc invocation — so this scans the PR/issue body and every comment
123
+ * on the thread in one pass every time it runs. That's a documented
124
+ * divergence: harmless (adoption is idempotent) but does mean a link posted
125
+ * in comment #1 gets rescanned on every later `uploads comment` run too.
126
+ */
127
+ export declare function fetchAdoptionCandidateText(target: GhTarget, run: CommandRunner): string;
package/dist/github-gh.js CHANGED
@@ -344,6 +344,72 @@ function reconcileAfterCreate(target, body, run, marker, createdRaw) {
344
344
  return null;
345
345
  }
346
346
  }
347
+ // --- link adoption (issue #708, local-gh fallback parity with the bot's
348
+ // #701/apps/api/src/github-link-adopt.ts) ---
349
+ /** Cheap, regex-only reject: no http(s) URL at all means nothing to scan for.
350
+ * Mirrors `hasLinkCandidate` in apps/api/src/github-link-adopt.ts. */
351
+ export function hasLinkCandidate(text) {
352
+ return /https?:\/\//i.test(text);
353
+ }
354
+ const URL_RE = /https?:\/\/[^\s)"'<>\]]+/gi;
355
+ /**
356
+ * Distinct http(s) URLs found in `text`, trailing prose punctuation
357
+ * stripped, order preserved, first occurrence wins on duplicates. Ported
358
+ * verbatim from apps/api/src/github-link-adopt.ts's `extractCandidateUrls`
359
+ * so the two paths recognize the same URL spellings — resolution itself
360
+ * (storage host / embed host / `/f/` page → key, and the bound-workspace
361
+ * check) happens server-side inside `POST .../github/attach`, so this file
362
+ * doesn't need its own copy of that logic.
363
+ */
364
+ export function extractCandidateUrls(text) {
365
+ const seen = new Set();
366
+ const out = [];
367
+ for (const m of text.matchAll(URL_RE)) {
368
+ const url = m[0].replace(/[.,;:]+$/, "");
369
+ if (seen.has(url))
370
+ continue;
371
+ seen.add(url);
372
+ out.push(url);
373
+ }
374
+ return out;
375
+ }
376
+ /**
377
+ * Best-effort concatenation of a PR/issue's current body plus every comment's
378
+ * current body, for link-adoption scanning. Returns "" on any `gh` failure
379
+ * (not authenticated, network, repo/number not found) — adoption degrades to
380
+ * a no-op rather than blocking the comment sync it rides along with.
381
+ *
382
+ * Unlike the webhook path (which re-scans one specific body/comment ref per
383
+ * event), the CLI has no per-event ref to key off of — `uploads comment` is
384
+ * one ad-hoc invocation — so this scans the PR/issue body and every comment
385
+ * on the thread in one pass every time it runs. That's a documented
386
+ * divergence: harmless (adoption is idempotent) but does mean a link posted
387
+ * in comment #1 gets rescanned on every later `uploads comment` run too.
388
+ */
389
+ export function fetchAdoptionCandidateText(target, run) {
390
+ let body = "";
391
+ try {
392
+ body = run("gh", ["api", `repos/${target.repo}/issues/${target.num}`, "--jq", '.body // ""']);
393
+ }
394
+ catch {
395
+ // Not found / no access — fall through with an empty body; comments may
396
+ // still be readable.
397
+ }
398
+ let comments = "";
399
+ try {
400
+ comments = run("gh", [
401
+ "api",
402
+ `repos/${target.repo}/issues/${target.num}/comments?per_page=100`,
403
+ "--paginate",
404
+ "--jq",
405
+ '[.[].body] | join("\\n")',
406
+ ]);
407
+ }
408
+ catch {
409
+ // Same degrade — an empty comments blob just means nothing more to scan.
410
+ }
411
+ return `${body}\n${comments}`;
412
+ }
347
413
  /** PATCH one comment's body via stdin, so the body is never shell-interpolated. */
348
414
  function patchComment(target, run, id, body) {
349
415
  run("gh", ["api", `repos/${target.repo}/issues/comments/${id}`, "-X", "PATCH", "-F", "body=@-"], body);
@@ -43,6 +43,25 @@ const promoteResultSchema = objectSchema({
43
43
  ]),
44
44
  },
45
45
  }, ["promoted", "skipped"]);
46
+ /** Hosted `promote` tool's `attached[]` entry (issue #702, `AttachExistingResponse`). */
47
+ const attachExistingResultSchema = objectSchema({
48
+ key: { type: "string" },
49
+ url: nullableString,
50
+ embedUrl: nullableString,
51
+ pageUrl: { type: "string" },
52
+ moved: { type: "boolean" },
53
+ source: objectSchema({ key: { type: "string" } }, ["key"]),
54
+ comment: commentResultSchema,
55
+ }, ["key", "url", "embedUrl", "moved", "source", "comment"]);
56
+ /** Hosted `promote` tool's `attachFailures[]` entry — one per bad `keys` source. */
57
+ const attachFailureSchema = objectSchema({
58
+ key: { type: "string" },
59
+ error: objectSchema({
60
+ message: { type: "string" },
61
+ code: { type: "string" },
62
+ status: { type: "number" },
63
+ }, ["message"]),
64
+ }, ["key", "error"]);
46
65
  const putObjectFields = {
47
66
  key: { type: "string" },
48
67
  url: nullableString,
@@ -209,10 +228,14 @@ export const healthResultSchema = objectSchema({
209
228
  apiUrl: { type: "string" },
210
229
  });
211
230
  export const promoteToolResultSchema = objectSchema({
231
+ // `promotion` is optional (issue #702): a `keys`-only call (no `branch`)
232
+ // never runs the branch sweep, so there's nothing to report under it.
212
233
  promotion: promoteResultSchema,
234
+ attached: { type: "array", items: attachExistingResultSchema },
235
+ attachFailures: { type: "array", items: attachFailureSchema },
213
236
  comment: commentResultSchema,
214
237
  commentError: { type: "string" },
215
- }, ["promotion"]);
238
+ });
216
239
  const galleryItemSchema = objectSchema({
217
240
  id: { type: "string" },
218
241
  objectKey: { type: "string" },
package/dist/mcp/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createUploadsClient } from "../client.js";
2
- import { buildDoctorReport, ghListPrefixes, ghMergedList, makeGhTarget, mergeStagingMeta, resolveGhPrefixSafe, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
2
+ import { buildDoctorReport, ghListPrefixes, ghMergedList, makeGhTarget, mergeStagingMeta, resolveAutoPrTarget, resolveGhPrefixSafe, resolvePutNudgeContext, resolvePutStagingTarget, resolveStaged, syncAttachmentsComment, autoPrNoteText, putNudgeText, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
3
3
  import { resolveFrameId } from "../frame.js";
4
4
  import { resolveConfig, resolvePutDefaults, } from "../config.js";
5
5
  import { resolvePutPrefix } from "../destinations.js";
@@ -345,9 +345,13 @@ export function createUploadsMcpTools(opts) {
345
345
  },
346
346
  ...frameProps,
347
347
  noGit: { type: "boolean", description: "Don't derive the repo segment from git." },
348
+ noPr: {
349
+ type: "boolean",
350
+ description: "Skip auto-PR context (issue #700): without pr/issue/key/ref/prefix/destination, a call on a branch mapping to exactly one open PR otherwise behaves as if pr had been passed (stable key + managed comment sync when comment is set). Also opts out via UPLOADS_NO_AUTO_PR=1.",
351
+ },
348
352
  comment: {
349
353
  type: "boolean",
350
- description: "With pr/issue: create or update the managed attachments comment. Posts as uploads-sh[bot] when the GitHub App is installed on the repo; otherwise via local gh auth (best-effort).",
354
+ description: "With pr/issue (or auto-detected PR context): create or update the managed attachments comment. Posts as uploads-sh[bot] when the GitHub App is installed on the repo; otherwise via local gh auth (best-effort).",
351
355
  },
352
356
  dryRun: {
353
357
  type: "boolean",
@@ -431,16 +435,39 @@ export function createUploadsMcpTools(opts) {
431
435
  const frameOpts = mcpFrameOptions(args);
432
436
  const optimizeOpts = mcpOptimizeOptions(args, defaults);
433
437
  const noGit = optBool(args, "noGit") || defaults.noGit === true;
438
+ const noAutoPr = optBool(args, "noPr") || defaults.noAutoPr === true;
434
439
  const alt = optString(args, "alt");
435
440
  const width = optPosInt(args, "width") ?? defaults.width;
436
441
  const contentType = optString(args, "contentType");
442
+ // Auto-PR context (issue #700): local stdio MCP put mirrors the CLI
443
+ // default — no pr/issue/key/ref/prefix/destination, not noGit/noPr,
444
+ // on a branch that maps to exactly one open PR behaves as if `pr`
445
+ // had been passed (stable key + managed comment sync) instead of
446
+ // the #403 staging default below. Never throws — see
447
+ // resolveAutoPrTarget.
448
+ const autoPrTarget = target
449
+ ? undefined
450
+ : resolveAutoPrTarget({
451
+ ghTarget: target,
452
+ keyHint: keyArg,
453
+ refArg,
454
+ prefixArg,
455
+ destinationArg: destArg,
456
+ noGit,
457
+ noAutoPr,
458
+ repoArg: optString(args, "repo") ?? defaults.repo,
459
+ run,
460
+ });
461
+ const effectiveTarget = target ?? autoPrTarget;
437
462
  // Bare-put branch staging (issue #403): local stdio MCP put mirrors
438
463
  // the CLI default — no pr/issue/key/ref/prefix/destination, not
439
464
  // noGit, on a non-default git branch stages to the branch prefix
440
465
  // (identical key/metadata to `attach --branch`) instead of the
441
466
  // dated layout. Never throws — see resolvePutStagingTarget.
467
+ // effectiveTarget (explicit pr/issue OR the #700 auto-PR match)
468
+ // wins over staging, same as it wins over the dated layout.
442
469
  const stagingTarget = resolvePutStagingTarget({
443
- ghTarget: target,
470
+ ghTarget: effectiveTarget,
444
471
  keyHint: keyArg,
445
472
  refArg,
446
473
  prefixArg,
@@ -449,6 +476,21 @@ export function createUploadsMcpTools(opts) {
449
476
  repoArg: optString(args, "repo") ?? defaults.repo,
450
477
  run,
451
478
  });
479
+ // Bare-put nudge context (issue #393/#700): only relevant when
480
+ // neither auto-PR nor staging took over. Finished into a hint once
481
+ // upload keys exist, below.
482
+ const nudgeCtx = effectiveTarget || stagingTarget
483
+ ? undefined
484
+ : resolvePutNudgeContext({
485
+ quiet: false,
486
+ noNudge: defaults.noNudge === true,
487
+ ghTarget: target,
488
+ keyHint: keyArg,
489
+ noGit,
490
+ repoArg: optString(args, "repo") ?? defaults.repo,
491
+ run,
492
+ });
493
+ const autoPrHint = autoPrTarget ? autoPrNoteText(autoPrTarget.num) : undefined;
452
494
  // Derived `repo` metadata (spec: 2026-08-11-screenshots-project-grouping-design.md).
453
495
  // Same derivation the CLI does; MCP always derives (no --no-auto), so
454
496
  // this is only suppressed by noGit. metadataProp's contract: omitting
@@ -468,7 +510,7 @@ export function createUploadsMcpTools(opts) {
468
510
  : metadataWithRepo;
469
511
  const putShared = {
470
512
  client,
471
- ghTarget: target,
513
+ ghTarget: effectiveTarget,
472
514
  ghBranchTarget: stagingTarget,
473
515
  prefix: resolvedPrefix ?? defaults.prefix,
474
516
  repo: optString(args, "repo") ?? defaults.repo,
@@ -496,11 +538,15 @@ export function createUploadsMcpTools(opts) {
496
538
  if (uploads.length === 0 && failures.length > 0) {
497
539
  throw new ToolBatchError(batchFailureMessage(failures), { uploads, failures });
498
540
  }
499
- if (wantComment && target && uploads.length > 0) {
500
- const { comment, commentError } = await syncComment(client, target, config.workspace);
501
- return { uploads, failures, comment, commentError };
541
+ const hint = autoPrHint ??
542
+ (nudgeCtx && uploads.length > 0
543
+ ? putNudgeText(nudgeCtx.branch, nudgeCtx.pr, uploads.map((u) => u.key))
544
+ : undefined);
545
+ if (wantComment && effectiveTarget && uploads.length > 0) {
546
+ const { comment, commentError } = await syncComment(client, effectiveTarget, config.workspace);
547
+ return { uploads, failures, comment, commentError, ...(hint ? { hint } : {}) };
502
548
  }
503
- return { uploads, failures };
549
+ return { uploads, failures, ...(hint ? { hint } : {}) };
504
550
  }
505
551
  // Single-file: contentBase64 still supported; paths go through uploadPuts.
506
552
  if (contentBase64 !== undefined) {
@@ -509,7 +555,7 @@ export function createUploadsMcpTools(opts) {
509
555
  const { result, prepared, markdown } = await uploadPreparedImage(client, bytes, sourceName, {
510
556
  frame: frameOpts,
511
557
  optimize: optimizeOpts,
512
- ghTarget: target,
558
+ ghTarget: effectiveTarget,
513
559
  ghBranchTarget: stagingTarget,
514
560
  key: keyArg,
515
561
  prefix: resolvedPrefix ?? defaults.prefix,
@@ -531,9 +577,19 @@ export function createUploadsMcpTools(opts) {
531
577
  outputBytes: prepared.outputBytes,
532
578
  filename: prepared.filename,
533
579
  };
534
- if (wantComment && target) {
535
- const { comment, commentError } = await syncComment(client, target, config.workspace);
536
- return { ...result, markdown, optimize, frame: prepared.frame, comment, commentError };
580
+ const hint = autoPrHint ??
581
+ (nudgeCtx ? putNudgeText(nudgeCtx.branch, nudgeCtx.pr, [result.key]) : undefined);
582
+ if (wantComment && effectiveTarget) {
583
+ const { comment, commentError } = await syncComment(client, effectiveTarget, config.workspace);
584
+ return {
585
+ ...result,
586
+ markdown,
587
+ optimize,
588
+ frame: prepared.frame,
589
+ comment,
590
+ commentError,
591
+ ...(hint ? { hint } : {}),
592
+ };
537
593
  }
538
594
  return {
539
595
  ...result,
@@ -541,6 +597,7 @@ export function createUploadsMcpTools(opts) {
541
597
  optimize,
542
598
  frame: prepared.frame,
543
599
  ...(dryRun ? { dryRun: true } : {}),
600
+ ...(hint ? { hint } : {}),
544
601
  };
545
602
  }
546
603
  const { uploads, failures, firstError } = await uploadPuts({
@@ -553,6 +610,8 @@ export function createUploadsMcpTools(opts) {
553
610
  throw firstError instanceof Error ? firstError : new Error(String(firstError));
554
611
  }
555
612
  const u = uploads[0];
613
+ const hint = autoPrHint ??
614
+ (nudgeCtx ? putNudgeText(nudgeCtx.branch, nudgeCtx.pr, [u.key]) : undefined);
556
615
  const flat = {
557
616
  workspace: u.workspace,
558
617
  key: u.key,
@@ -566,9 +625,10 @@ export function createUploadsMcpTools(opts) {
566
625
  optimize: u.optimize,
567
626
  frame: u.frame,
568
627
  ...(dryRun ? { dryRun: true } : {}),
628
+ ...(hint ? { hint } : {}),
569
629
  };
570
- if (wantComment && target) {
571
- const { comment, commentError } = await syncComment(client, target, config.workspace);
630
+ if (wantComment && effectiveTarget) {
631
+ const { comment, commentError } = await syncComment(client, effectiveTarget, config.workspace);
572
632
  return { ...flat, comment, commentError };
573
633
  }
574
634
  return flat;
@@ -673,9 +733,13 @@ export function createUploadsMcpTools(opts) {
673
733
  keepExif: { type: "boolean", description: "Keep EXIF/XMP/ICC when optimizing." },
674
734
  ...frameProps,
675
735
  noGit: { type: "boolean", description: "Don't derive the repo segment from git." },
736
+ noPr: {
737
+ type: "boolean",
738
+ description: "Skip auto-PR context (issue #700): without pr/issue/key/ref/prefix/destination, a call on a branch mapping to exactly one open PR otherwise behaves as if pr had been passed. Also opts out via UPLOADS_NO_AUTO_PR=1.",
739
+ },
676
740
  comment: {
677
741
  type: "boolean",
678
- description: "With pr/issue: create/update the managed attachments comment (best-effort).",
742
+ description: "With pr/issue (or auto-detected PR context): create/update the managed attachments comment (best-effort).",
679
743
  },
680
744
  galleryId: {
681
745
  type: "string",
@@ -741,16 +805,38 @@ export function createUploadsMcpTools(opts) {
741
805
  const frameOpts = mcpFrameOptions(args);
742
806
  const optimizeOpts = mcpOptimizeOptions(args, defaults);
743
807
  const noGit = optBool(args, "noGit") || defaults.noGit === true;
808
+ const noAutoPr = optBool(args, "noPr") || defaults.noAutoPr === true;
744
809
  const alt = optString(args, "alt");
745
810
  const width = optPosInt(args, "width") ?? defaults.width;
811
+ // Auto-PR context (issue #700): mirrors the CLI screenshot command
812
+ // and the put tool above — no pr/issue/key/ref/prefix/destination,
813
+ // not noGit/noPr, on a branch that maps to exactly one open PR
814
+ // behaves as if `pr` had been passed (stable key + managed comment
815
+ // sync) instead of the #469 auto-staging default below. Never
816
+ // throws — see resolveAutoPrTarget.
817
+ const autoPrTarget = target
818
+ ? undefined
819
+ : resolveAutoPrTarget({
820
+ ghTarget: target,
821
+ keyHint: keyArg,
822
+ refArg,
823
+ prefixArg,
824
+ destinationArg: destArg,
825
+ noGit,
826
+ noAutoPr,
827
+ repoArg: optString(args, "repo") ?? defaults.repo,
828
+ run,
829
+ });
830
+ const effectiveTarget = target ?? autoPrTarget;
746
831
  // Auto branch staging (issue #469 lever 1): mirrors the CLI screenshot
747
832
  // command and the put tool above (issue #403) — no pr/issue/key/ref/
748
833
  // prefix/destination, not noGit, on a non-default git branch stages
749
834
  // to the branch prefix (identical key/metadata to `attach --branch`)
750
835
  // instead of the dated `screenshots/<repo>/<date>/...` layout. Never
751
- // throws — see resolvePutStagingTarget.
836
+ // throws — see resolvePutStagingTarget. effectiveTarget (explicit
837
+ // pr/issue OR the #700 auto-PR match) wins over staging.
752
838
  const stagingTarget = resolvePutStagingTarget({
753
- ghTarget: target,
839
+ ghTarget: effectiveTarget,
754
840
  keyHint: keyArg,
755
841
  refArg,
756
842
  prefixArg,
@@ -759,13 +845,27 @@ export function createUploadsMcpTools(opts) {
759
845
  repoArg: optString(args, "repo") ?? defaults.repo,
760
846
  run,
761
847
  });
848
+ // Bare-screenshot nudge context (issue #393/#700): only relevant
849
+ // when neither auto-PR nor staging took over.
850
+ const nudgeCtx = effectiveTarget || stagingTarget
851
+ ? undefined
852
+ : resolvePutNudgeContext({
853
+ quiet: false,
854
+ noNudge: defaults.noNudge === true,
855
+ ghTarget: target,
856
+ keyHint: keyArg,
857
+ noGit,
858
+ repoArg: optString(args, "repo") ?? defaults.repo,
859
+ run,
860
+ });
861
+ const autoPrHint = autoPrTarget ? autoPrNoteText(autoPrTarget.num) : undefined;
762
862
  let resolvedPrefix;
763
863
  try {
764
864
  resolvedPrefix = resolvePutPrefix({
765
865
  destination: destArg,
766
866
  prefix: prefixArg,
767
867
  key: keyArg,
768
- ghAttachment: Boolean(target) || stagingTarget !== undefined,
868
+ ghAttachment: Boolean(effectiveTarget) || stagingTarget !== undefined,
769
869
  });
770
870
  }
771
871
  catch (err) {
@@ -837,10 +937,10 @@ export function createUploadsMcpTools(opts) {
837
937
  }
838
938
  // Resolved once (issue #631), only now that upload is actually
839
939
  // about to happen — never per file (screenshot uploads exactly one).
840
- const ghPrefix = target
940
+ const ghPrefix = effectiveTarget
841
941
  ? await resolveGhPrefixSafe(client, {
842
- repo: target.repo,
843
- target: { kind: target.kind, num: target.num },
942
+ repo: effectiveTarget.repo,
943
+ target: { kind: effectiveTarget.kind, num: effectiveTarget.num },
844
944
  })
845
945
  : stagingTarget
846
946
  ? await resolveGhPrefixSafe(client, {
@@ -851,7 +951,7 @@ export function createUploadsMcpTools(opts) {
851
951
  const { result, prepared, markdown } = await uploadPreparedImage(client, captured.png, captured.filename, {
852
952
  frame: frameOpts,
853
953
  optimize: optimizeOpts,
854
- ghTarget: target,
954
+ ghTarget: effectiveTarget,
855
955
  ghBranchTarget: stagingTarget,
856
956
  ghPrefix,
857
957
  key: keyArg,
@@ -897,14 +997,19 @@ export function createUploadsMcpTools(opts) {
897
997
  frame: prepared.frame,
898
998
  gallery,
899
999
  ...(dryRun ? { dryRun: true } : {}),
900
- // Full-page height cap note (issue #652), mirrors the CLI's stderr
901
- // note + `hint` field.
1000
+ // Hint precedence (mirrors the CLI): the full-page height cap note
1001
+ // (issue #652) is about the just-captured image itself, more
1002
+ // immediately actionable than the #700 auto-PR/nudge notes below.
902
1003
  ...(captured.capped?.clipped
903
1004
  ? { hint: screenshotModule.clipHintText(captured.capped.maxHeightPx, "maxHeight") }
904
- : {}),
1005
+ : autoPrHint
1006
+ ? { hint: autoPrHint }
1007
+ : nudgeCtx
1008
+ ? { hint: putNudgeText(nudgeCtx.branch, nudgeCtx.pr, [result.key]) }
1009
+ : {}),
905
1010
  };
906
- if (wantComment && target) {
907
- const { comment, commentError } = await syncComment(client, target, config.workspace);
1011
+ if (wantComment && effectiveTarget) {
1012
+ const { comment, commentError } = await syncComment(client, effectiveTarget, config.workspace);
908
1013
  return { ...flat, comment, commentError };
909
1014
  }
910
1015
  return flat;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.43.0",
3
+ "version": "0.45.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,