@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/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";
@@ -108,7 +108,7 @@ export function createUploadsMcpTools(opts) {
108
108
  const { globals } = opts;
109
109
  const run = opts.runner ?? execRunner;
110
110
  const clientFactory = opts.clientFactory ?? createUploadsClient;
111
- function clientFor(args, requireToken = true) {
111
+ async function clientFor(args, requireToken = true) {
112
112
  const config = resolveConfig({
113
113
  apiUrl: globals.apiUrl,
114
114
  token: globals.token,
@@ -151,7 +151,7 @@ export function createUploadsMcpTools(opts) {
151
151
  const title = optString(args, "title");
152
152
  if (!title)
153
153
  usage("title is required");
154
- const { client } = clientFor(args);
154
+ const { client } = await clientFor(args);
155
155
  return client.createGallery({ title, description: optString(args, "description") });
156
156
  },
157
157
  },
@@ -171,7 +171,7 @@ export function createUploadsMcpTools(opts) {
171
171
  additionalProperties: false,
172
172
  },
173
173
  async handler(args) {
174
- const { client } = clientFor(args);
174
+ const { client } = await clientFor(args);
175
175
  return client.getGallery(galleryId(args));
176
176
  },
177
177
  },
@@ -197,7 +197,7 @@ export function createUploadsMcpTools(opts) {
197
197
  const objectKey = optString(args, "objectKey");
198
198
  if (!objectKey)
199
199
  usage("objectKey is required");
200
- const { client } = clientFor(args);
200
+ const { client } = await clientFor(args);
201
201
  const id = galleryId(args);
202
202
  const current = await client.getGallery(id);
203
203
  return client.addGalleryItem(id, objectKey, {
@@ -228,7 +228,7 @@ export function createUploadsMcpTools(opts) {
228
228
  additionalProperties: false,
229
229
  },
230
230
  async handler(args) {
231
- const { client } = clientFor(args);
231
+ const { client } = await clientFor(args);
232
232
  const id = galleryId(args);
233
233
  const current = await client.getGallery(id);
234
234
  return client.linkGalleryExternalReference(id, {
@@ -259,7 +259,7 @@ export function createUploadsMcpTools(opts) {
259
259
  additionalProperties: false,
260
260
  },
261
261
  async handler(args) {
262
- const { client } = clientFor(args);
262
+ const { client } = await clientFor(args);
263
263
  return client.findGalleriesByReference({
264
264
  ...galleryReference(args),
265
265
  limit: optPosInt(args, "limit"),
@@ -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",
@@ -426,21 +430,44 @@ export function createUploadsMcpTools(opts) {
426
430
  catch (err) {
427
431
  usage(err instanceof Error ? err.message : String(err));
428
432
  }
429
- const { config, client } = clientFor(args);
433
+ const { config, client } = await clientFor(args);
430
434
  const defaults = resolvePutDefaults({ envFile: globals.envFile });
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",
@@ -736,21 +800,43 @@ export function createUploadsMcpTools(opts) {
736
800
  const metadata = metadataArgWithCanonical(args);
737
801
  if (metadata)
738
802
  validateMetaMap(metadata);
739
- const { config, client } = clientFor(args);
803
+ const { config, client } = await clientFor(args);
740
804
  const defaults = resolvePutDefaults({ envFile: globals.envFile });
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;
@@ -969,7 +1074,7 @@ export function createUploadsMcpTools(opts) {
969
1074
  const explicitTarget = ghTargetFromArgs(args, run);
970
1075
  const target = explicitTarget ??
971
1076
  resolveCurrentPullRequest(resolveRepo(optString(args, "repo"), run), run);
972
- const { config, client } = clientFor(args);
1077
+ const { config, client } = await clientFor(args);
973
1078
  const contentType = optString(args, "contentType");
974
1079
  const defaults = resolvePutDefaults({ envFile: globals.envFile });
975
1080
  const frameOpts = mcpFrameOptions(args);
@@ -1037,7 +1142,7 @@ export function createUploadsMcpTools(opts) {
1037
1142
  const prefixArg = optString(args, "prefix");
1038
1143
  let prefix = prefixArg ?? (defaults.prefix ? `${defaults.prefix}/` : undefined);
1039
1144
  const target = ghTargetFromArgs(args, run);
1040
- const { client } = clientFor(args);
1145
+ const { client } = await clientFor(args);
1041
1146
  // Also list every active private prefix, if any (issue #631) —
1042
1147
  // mirrors syncAttachmentsComment's gh-fallback gather: a repo's
1043
1148
  // attachment history can be split across the plain shape and
@@ -1093,7 +1198,7 @@ export function createUploadsMcpTools(opts) {
1093
1198
  additionalProperties: false,
1094
1199
  },
1095
1200
  async handler(args) {
1096
- const { client } = clientFor(args);
1201
+ const { client } = await clientFor(args);
1097
1202
  const repo = resolveRepo(optString(args, "repo"), run);
1098
1203
  const branch = optString(args, "branch") ?? resolveCurrentBranch(run);
1099
1204
  return resolveStaged({ client, repo, branch });
@@ -1124,7 +1229,7 @@ export function createUploadsMcpTools(opts) {
1124
1229
  usage("key is required");
1125
1230
  if (optBool(args, "dryRun"))
1126
1231
  return { key, deleted: false, dryRun: true };
1127
- const { client } = clientFor(args);
1232
+ const { client } = await clientFor(args);
1128
1233
  return client.delete(key);
1129
1234
  },
1130
1235
  },
@@ -1147,7 +1252,7 @@ export function createUploadsMcpTools(opts) {
1147
1252
  const key = optString(args, "key");
1148
1253
  if (!key)
1149
1254
  usage("key is required");
1150
- return clientFor(args).client.getMetadata(key);
1255
+ return (await clientFor(args)).client.getMetadata(key);
1151
1256
  },
1152
1257
  },
1153
1258
  {
@@ -1184,7 +1289,7 @@ export function createUploadsMcpTools(opts) {
1184
1289
  }
1185
1290
  if (set)
1186
1291
  validateMetaMap(set);
1187
- const { client } = clientFor(args);
1292
+ const { client } = await clientFor(args);
1188
1293
  return client.patchMetadata(key, { set, delete: del });
1189
1294
  },
1190
1295
  },
@@ -1223,7 +1328,7 @@ export function createUploadsMcpTools(opts) {
1223
1328
  }
1224
1329
  if (hasMeta)
1225
1330
  validateMetaMap(filters);
1226
- const { client } = clientFor(args);
1331
+ const { client } = await clientFor(args);
1227
1332
  return client.findFiles(filters, {
1228
1333
  name,
1229
1334
  prefix: optString(args, "prefix"),
@@ -1249,7 +1354,7 @@ export function createUploadsMcpTools(opts) {
1249
1354
  additionalProperties: false,
1250
1355
  },
1251
1356
  async handler(args) {
1252
- const { client } = clientFor(args);
1357
+ const { client } = await clientFor(args);
1253
1358
  const key = optString(args, "key");
1254
1359
  return key ? client.listMetadataValues(key) : client.listMetadataKeys();
1255
1360
  },
@@ -1266,7 +1371,7 @@ export function createUploadsMcpTools(opts) {
1266
1371
  additionalProperties: false,
1267
1372
  },
1268
1373
  async handler(args) {
1269
- const { client } = clientFor(args);
1374
+ const { client } = await clientFor(args);
1270
1375
  return client.usage();
1271
1376
  },
1272
1377
  },
@@ -1282,7 +1387,7 @@ export function createUploadsMcpTools(opts) {
1282
1387
  additionalProperties: false,
1283
1388
  },
1284
1389
  async handler(args) {
1285
- const { client } = clientFor(args);
1390
+ const { client } = await clientFor(args);
1286
1391
  return client.reconcile();
1287
1392
  },
1288
1393
  },
@@ -1298,7 +1403,7 @@ export function createUploadsMcpTools(opts) {
1298
1403
  additionalProperties: false,
1299
1404
  },
1300
1405
  async handler(args) {
1301
- const { client } = clientFor(args);
1406
+ const { client } = await clientFor(args);
1302
1407
  return client.purgeExpired();
1303
1408
  },
1304
1409
  },
@@ -1320,7 +1425,7 @@ export function createUploadsMcpTools(opts) {
1320
1425
  const target = ghTargetFromArgs(args, run);
1321
1426
  if (!target)
1322
1427
  usage("comment requires pr or issue");
1323
- const { config, client } = clientFor(args);
1428
+ const { config, client } = await clientFor(args);
1324
1429
  // Explicit resync, same as `uploads comment` (issue #480).
1325
1430
  const result = await syncAttachmentsComment(client, target, run, config.workspace, {
1326
1431
  resync: true,
@@ -1336,7 +1441,7 @@ export function createUploadsMcpTools(opts) {
1336
1441
  description: "Check uploads.sh API liveness. No auth or arguments required.",
1337
1442
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
1338
1443
  async handler(args) {
1339
- const { config, client } = clientFor(args, false);
1444
+ const { config, client } = await clientFor(args, false);
1340
1445
  const result = await client.health();
1341
1446
  return { ...result, apiUrl: config.apiUrl };
1342
1447
  },
@@ -1353,7 +1458,7 @@ export function createUploadsMcpTools(opts) {
1353
1458
  additionalProperties: false,
1354
1459
  },
1355
1460
  async handler(args) {
1356
- const { config, client } = clientFor(args);
1461
+ const { config, client } = await clientFor(args);
1357
1462
  return buildDoctorReport(config, client);
1358
1463
  },
1359
1464
  },
@@ -0,0 +1,9 @@
1
+ /**
2
+ * No-project-context nudge (issue #692 follow-up): a path-tagged upload with
3
+ * no repo/gh.repo/app metadata and no real (non-local) origin lands in the
4
+ * screenshots page's "local dev" / "Other" fallback buckets. One advisory
5
+ * stderr line teaches the fix at the moment the context went missing. The
6
+ * predicate mirrors apps/api's projectLabelFromMeta fallback rules exactly —
7
+ * a real URL host is a meaningful group, so it never fires there.
8
+ */
9
+ export declare function noProjectContextNudge(meta: Record<string, string> | undefined): string | undefined;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * No-project-context nudge (issue #692 follow-up): a path-tagged upload with
3
+ * no repo/gh.repo/app metadata and no real (non-local) origin lands in the
4
+ * screenshots page's "local dev" / "Other" fallback buckets. One advisory
5
+ * stderr line teaches the fix at the moment the context went missing. The
6
+ * predicate mirrors apps/api's projectLabelFromMeta fallback rules exactly —
7
+ * a real URL host is a meaningful group, so it never fires there.
8
+ */
9
+ /** Hosts that identify a dev machine, not an app — same set as the API's
10
+ * isLocalHostname (apps/api/src/file-metadata.ts). */
11
+ function isLocalHostname(hostname) {
12
+ const bare = hostname.toLowerCase();
13
+ return (bare === "localhost" ||
14
+ bare.endsWith(".localhost") ||
15
+ bare === "127.0.0.1" ||
16
+ bare === "0.0.0.0" ||
17
+ bare === "[::1]");
18
+ }
19
+ export function noProjectContextNudge(meta) {
20
+ // Only `path`-tagged uploads appear on the screenshots page at all.
21
+ if (!meta?.path)
22
+ return undefined;
23
+ if (meta.repo || meta["gh.repo"] || meta.app)
24
+ return undefined;
25
+ let bucket = "Other";
26
+ if (meta.url) {
27
+ try {
28
+ const parsed = new URL(meta.url);
29
+ if (!isLocalHostname(parsed.hostname))
30
+ return undefined; // real host = real group
31
+ bucket = "local dev";
32
+ }
33
+ catch {
34
+ // unparseable url is just "no url"
35
+ }
36
+ }
37
+ return (`note: no repo detected — the screenshots page will group this under "${bucket}". ` +
38
+ `Run from inside your project repo, or pass --app <name>.`);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.42.2",
3
+ "version": "0.44.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,