@buildinternet/uploads 0.42.1 → 0.43.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/README.md CHANGED
@@ -15,7 +15,7 @@ npx @buildinternet/uploads --help
15
15
  uploads setup
16
16
  uploads --version
17
17
  uploads attach ./before.png ./after.png
18
- uploads screenshot https://app.example --pr 123 --comment # capture + host in one step
18
+ uploads screenshot https://app.example --pr 123 # capture + host in one step
19
19
  uploads screenshot ./report.html --dark --selector "main"
20
20
  uploads screenshot http://localhost:3000 --via local --annotate ./callouts.json
21
21
  uploads annotate ./shot.png --spec ./callouts.json --out ./shot.marked.png
@@ -24,7 +24,7 @@ uploads put ./shot.png --destination screenshots
24
24
  uploads put ./shot.png --no-optimize
25
25
  uploads put ./mobile.png --frame phone
26
26
  uploads put ./ui.png --frame browser --frame-url "https://app.example"
27
- uploads put ./after.png --pr 123 --comment
27
+ uploads put ./after.png --pr 123
28
28
  uploads put ./capture-2026-…Z.png --pr 123 --name hero.png # clean leaf, stable path
29
29
  uploads put ./shot.png --pr 123 --name hero.png --dry-run --format url # preview URL, no upload
30
30
  uploads gallery create --title "Release screenshots"
@@ -190,7 +190,7 @@ When adding several keys, `uploads gallery add` processes them sequentially and
190
190
  individual failures in `--json` output. Gallery item updates use the API's current version to
191
191
  avoid overwriting concurrent changes.
192
192
 
193
- Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` (or use `put --comment`) to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
193
+ Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
194
194
 
195
195
  Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
196
196
 
package/dist/cli-help.js CHANGED
@@ -105,7 +105,7 @@ ${section(style, "Examples:")}
105
105
  ${style.command("uploads login")}
106
106
  ${style.command("uploads whoami")}
107
107
  ${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
108
- ${style.command("uploads put")} ./after.png --pr 123 --comment
108
+ ${style.command("uploads put")} ./after.png --pr 123
109
109
  ${style.command("uploads put")} ./bug.png --issue 45
110
110
  ${style.command("uploads put")} ./shot.png --meta path=/settings --state after
111
111
  ${style.command("uploads attach")} ./before.png ./after.png
@@ -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
@@ -154,7 +154,7 @@ ${section(style, "Examples:")}
154
154
  ${style.command("uploads login")}
155
155
  ${style.command("uploads whoami")}
156
156
  ${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
157
- ${style.command("uploads put")} ./after.png --pr 123 --comment
157
+ ${style.command("uploads put")} ./after.png --pr 123
158
158
  ${style.command("uploads put")} ./bug.png --issue 45 --repo myorg/myapp
159
159
  ${style.command("uploads put")} ./shot.png --dry-run --format url
160
160
  ${style.command("uploads put")} ./shot.png --meta path=/settings --state after
package/dist/client.d.ts CHANGED
@@ -565,7 +565,7 @@ export declare function mintWorkspaceToken(apiUrl: string, accessToken: string,
565
565
  workspace: string;
566
566
  scopes?: Array<TokenScope>;
567
567
  label?: string;
568
- ttlSeconds?: number;
568
+ ttlSeconds?: number | null;
569
569
  }): Promise<MintTokenResult>;
570
570
  /**
571
571
  * Parse API error bodies. Prefers the nested envelope
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
  }
@@ -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
@@ -19,6 +19,7 @@ Usage:
19
19
  uploads hook pre-pr-screenshot
20
20
 
21
21
  Invoked by Claude Code / Codex / Grok / Cursor hooks. Never blocks.
22
+ Harness manifests no-op (exit 0, no output) when this binary is not on PATH.
22
23
 
23
24
  pre-pr-screenshot
24
25
  If the shell command is \`gh pr create\`, the branch touches UI files, and
@@ -2,7 +2,7 @@ import { flagBool, flagString, parseCommandArgs, UsageError, } from "../cli-args
2
2
  import { resolveConfig } from "../config.js";
3
3
  import { execRunner } from "../github-gh.js";
4
4
  import { writeCommandHelp } from "../cli-style.js";
5
- import { HOOK_COMMAND, installHookManifests } from "../hooks-install.js";
5
+ import { HOOK_COMMAND, HOOK_INVOCATION, installHookManifests, } from "../hooks-install.js";
6
6
  export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
7
7
  const SKILL_SOURCE = "buildinternet/uploads";
8
8
  const SKILL_NAMES = ["uploads-cli", "github-screenshots", "annotate-screenshots"];
@@ -15,7 +15,7 @@ hook for Grok / Cursor when those tools are present. The remote MCP endpoint
15
15
  infers your workspace from the bearer token, so only the token is needed.
16
16
 
17
17
  Claude Code and Codex ship the same reminder via their plugins (same command:
18
- \`${HOOK_COMMAND}\`) — install those plugins instead of relying on this step.
18
+ \`${HOOK_INVOCATION}\`) — install those plugins instead of relying on this step.
19
19
 
20
20
  Safe to re-run. An MCP server already registered under this name is reported
21
21
  as \`already configured\` and left as-is — including the token it was created
@@ -8,6 +8,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";
@@ -388,6 +389,9 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
388
389
  else if (Object.keys(withFacts).length > 0) {
389
390
  validateMetaMap(withFacts);
390
391
  }
392
+ // #692 follow-up: same advisory as put — a capture with no repo/app context
393
+ // and only a local origin lands in the screenshots page's fallback buckets.
394
+ const contextNudge = !ctx.quiet && !putDefaults.noNudge && !noGit ? noProjectContextNudge(metadata) : undefined;
391
395
  const logHuman = !ctx.quiet && format === "human";
392
396
  if (logHuman)
393
397
  process.stderr.write(`>> capturing ${target}\n`);
@@ -562,6 +566,8 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
562
566
  }
563
567
  if (bindingWarning)
564
568
  process.stderr.write(`${bindingWarning}\n`);
569
+ if (contextNudge)
570
+ process.stderr.write(`${contextNudge}\n`);
565
571
  process.stderr.write("\n");
566
572
  }
567
573
  // One JSON `hint` slot (mirrors bare put): the clip note (issue #652) wins
@@ -576,7 +582,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
576
582
  const replacedHint = result.replaced && explicitMeta.state
577
583
  ? `re-capture replaced the previous state=${explicitMeta.state} object at ${result.key} — expected for repeat captures of the same URL + state`
578
584
  : undefined;
579
- const jsonHint = clipHint ?? bindingWarning ?? stagingNote ?? replacedHint;
585
+ const jsonHint = clipHint ?? bindingWarning ?? stagingNote ?? replacedHint ?? contextNudge;
580
586
  switch (format) {
581
587
  case "json":
582
588
  await writeJson({
package/dist/commands.js CHANGED
@@ -16,6 +16,7 @@ import { mergeSidecarMeta } from "./sidecar.js";
16
16
  import { ghAttachmentKeyForMode, ghBranchAttachmentKeyForMode, ghBranchKeyPrefix, ghKeyPrefix, ghPrivateKeyPrefix, ghPrivateBranchKeyPrefix, ghMetadataFromTarget, parseGhKey, parseGhPrivateKey, 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
18
  import { deriveRepoFromGit, deriveRepoSlugFromGit } from "./keys.js";
19
+ import { noProjectContextNudge } from "./project-context-nudge.js";
19
20
  import { resolvePutPrefix } from "./destinations.js";
20
21
  import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
21
22
  import { applyFrame, resolveFrameId } from "./frame.js";
@@ -1812,6 +1813,11 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
1812
1813
  if (slug)
1813
1814
  metadata = mergeDerivedMeta(metadata, { repo: slug });
1814
1815
  }
1816
+ // #692 follow-up: a path-tagged upload with no repo/gh.repo/app context and
1817
+ // no real (non-local) origin lands in the screenshots page's fallback
1818
+ // buckets — one advisory line at the moment the context went missing.
1819
+ // --no-git is an explicit choice, so it suppresses the nudge too.
1820
+ const contextNudge = !ctx.quiet && !defaults.noNudge && !noGit ? noProjectContextNudge(metadata) : undefined;
1815
1821
  // Bare-put nudge (issue #393): only relevant when staging didn't take over
1816
1822
  // — once `stagingTarget` resolves, staging IS the upgrade the nudge used to
1817
1823
  // point at, so this is skipped entirely rather than firing redundantly.
@@ -1894,7 +1900,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
1894
1900
  // warning); stderr prints the nudge/staging-note and binding-warning lines
1895
1901
  // independently, below. pathHint only ever fires on the ghTarget path, so
1896
1902
  // it never competes with the other three.
1897
- const jsonHint = nudge ?? bindingWarning ?? stagingNote ?? pathHint;
1903
+ const jsonHint = nudge ?? bindingWarning ?? stagingNote ?? pathHint ?? contextNudge;
1898
1904
  const galleriesByKey = new Map();
1899
1905
  let galleryHadError = false;
1900
1906
  if (galleryId && uploads.length > 0) {
@@ -1979,6 +1985,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
1979
1985
  process.stderr.write(`${bindingWarning}\n`);
1980
1986
  if (pathHint)
1981
1987
  process.stderr.write(`${pathHint}\n`);
1988
+ if (contextNudge)
1989
+ process.stderr.write(`${contextNudge}\n`);
1982
1990
  }
1983
1991
  return failures.length === 0 && !galleryHadError ? 0 : 1;
1984
1992
  }
@@ -2040,6 +2048,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
2040
2048
  process.stderr.write(`${bindingWarning}\n`);
2041
2049
  if (pathHint && format !== "json")
2042
2050
  process.stderr.write(`${pathHint}\n`);
2051
+ if (contextNudge && format !== "json")
2052
+ process.stderr.write(`${contextNudge}\n`);
2043
2053
  return gallery?.error ? 1 : 0;
2044
2054
  }
2045
2055
  // --- galleries ---
@@ -2726,7 +2736,9 @@ Scans the PR/issue description and comments for github.com/user-attachments
2726
2736
  media, mirrors new ones into the workspace (indexed, not added to the managed
2727
2737
  comment), and detaches ones no longer referenced. Works on any repo linked to
2728
2738
  the workspace; the .uploads.yml ingestGithubAttachments knob only gates the
2729
- automatic webhook path.
2739
+ automatic webhook path. Bot-authored attachments and images under 200px on
2740
+ either side are always skipped (the .uploads.yml ingestBotAttachments knob
2741
+ re-admits bot media on the webhook path only).
2730
2742
 
2731
2743
  Examples:
2732
2744
  uploads ingest --pr 123
@@ -1,59 +1,12 @@
1
1
  /**
2
- * Published-CLI COPY of the workspace parser/resolver (issue #307)
3
- * (packages/comment-config/src/index.ts). The published CLI imports no
4
- * @uploads/* package, so that private package cannot be shared here — its
5
- * parser/resolver is copied verbatim below. Kept in sync by
6
- * test/fixtures/comment-config-golden.json, asserted from both sides (this
7
- * package's comment-config.test.ts and the canonical package's
8
- * index.test.ts) — change both copies together. `readLocalRepoCommentConfig`
9
- * below is CLI-only: it reads the six candidate config paths off the working
10
- * tree (the server instead fetches them from GitHub's contents API —
11
- * apps/api/src/repo-comment-config.ts).
2
+ * CLI comment-config: shared parser from the generated copy of
3
+ * `@uploads/comment-config`, plus the local working-tree reader.
4
+ * `readLocalRepoCommentConfig` is CLI-only it reads the six candidate
5
+ * config paths off the working tree (the server instead fetches them from
6
+ * GitHub's contents API — apps/api/src/repo-comment-config.ts).
12
7
  */
13
- export interface RepoCommentConfig {
14
- imageWidth?: "auto" | "full" | number;
15
- maxInlineImages?: number;
16
- metaPath?: boolean;
17
- metaState?: boolean;
18
- linkToFilePage?: boolean;
19
- note?: string;
20
- ingestGithubAttachments?: boolean;
21
- }
22
- export interface WorkspaceCommentDefaults {
23
- imageWidth?: "full" | number;
24
- maxInlineImages?: number;
25
- showMetadata?: boolean;
26
- linkToFilePage?: boolean;
27
- note?: string;
28
- ingestGithubAttachments?: boolean;
29
- }
30
- export interface ResolvedCommentOptions {
31
- imageWidth: "auto" | "full" | number;
32
- maxInlineImages: number;
33
- metaPath: boolean;
34
- metaState: boolean;
35
- linkToFilePage: boolean;
36
- note: string | null;
37
- ingestGithubAttachments: boolean;
38
- }
39
- export type OptionSource = "repo" | "workspace" | "auto";
40
- export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
41
- export declare const NOTE_MAX_CHARS = 500;
42
- export declare function parseRepoCommentConfig(text: string, format: "yaml" | "json"): {
43
- config: RepoCommentConfig | null;
44
- warnings: string[];
45
- };
46
- export declare function resolveCommentOptions(repo: RepoCommentConfig | null, ws: WorkspaceCommentDefaults | null): {
47
- options: ResolvedCommentOptions;
48
- source: Record<keyof ResolvedCommentOptions, OptionSource>;
49
- };
50
- /**
51
- * Candidate paths, checked in this order — the first hit wins. Must match
52
- * the server's REPO_CONFIG_PATHS exactly (apps/api/src/repo-comment-config.ts)
53
- * so a committed config resolves identically whether the bot or the local
54
- * gh fallback renders the comment.
55
- */
56
- export declare const REPO_CONFIG_PATHS: readonly [".uploads.yml", ".uploads.yaml", ".uploads.json", ".github/uploads.yml", ".github/uploads.yaml", ".github/uploads.json"];
8
+ import { type RepoCommentConfig } from "./comment-config.generated.js";
9
+ export { AUTO_COMMENT_OPTIONS, NOTE_MAX_CHARS, REPO_CONFIG_PATHS, parseRepoCommentConfig, resolveCommentOptions, type OptionSource, type RepoCommentConfig, type ResolvedCommentOptions, type WorkspaceCommentDefaults, } from "./comment-config.generated.js";
57
10
  /**
58
11
  * Read the repo's comment config off the local working tree — the CLI has no
59
12
  * GitHub App installation token to fetch via the contents API, so this reads
@@ -0,0 +1,48 @@
1
+ /**
2
+ * GENERATED by packages/uploads/scripts/inline-shared.mjs — do not edit.
3
+ * Canonical source: packages/comment-config/src/index.ts
4
+ * The published CLI cannot import private @uploads/* packages, so this file
5
+ * is inlined into the tarball. Re-run the script after changing the source.
6
+ */
7
+ export interface RepoCommentConfig {
8
+ imageWidth?: "auto" | "full" | number;
9
+ maxInlineImages?: number;
10
+ metaPath?: boolean;
11
+ metaState?: boolean;
12
+ linkToFilePage?: boolean;
13
+ note?: string;
14
+ ingestGithubAttachments?: boolean;
15
+ ingestBotAttachments?: boolean;
16
+ }
17
+ export interface WorkspaceCommentDefaults {
18
+ imageWidth?: "full" | number;
19
+ maxInlineImages?: number;
20
+ showMetadata?: boolean;
21
+ linkToFilePage?: boolean;
22
+ note?: string;
23
+ ingestGithubAttachments?: boolean;
24
+ ingestBotAttachments?: boolean;
25
+ }
26
+ export interface ResolvedCommentOptions {
27
+ imageWidth: "auto" | "full" | number;
28
+ maxInlineImages: number;
29
+ metaPath: boolean;
30
+ metaState: boolean;
31
+ linkToFilePage: boolean;
32
+ note: string | null;
33
+ ingestGithubAttachments: boolean;
34
+ ingestBotAttachments: boolean;
35
+ }
36
+ export type OptionSource = "repo" | "workspace" | "auto";
37
+ export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
38
+ export declare const NOTE_MAX_CHARS = 500;
39
+ export declare function parseRepoCommentConfig(text: string, format: "yaml" | "json"): {
40
+ config: RepoCommentConfig | null;
41
+ warnings: string[];
42
+ };
43
+ export declare function resolveCommentOptions(repo: RepoCommentConfig | null, ws: WorkspaceCommentDefaults | null): {
44
+ options: ResolvedCommentOptions;
45
+ source: Record<keyof ResolvedCommentOptions, OptionSource>;
46
+ };
47
+ /** Candidate paths, checked in this order — the first hit wins. */
48
+ export declare const REPO_CONFIG_PATHS: readonly [".uploads.yml", ".uploads.yaml", ".uploads.json", ".github/uploads.yml", ".github/uploads.yaml", ".github/uploads.json"];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * GENERATED by packages/uploads/scripts/inline-shared.mjs — do not edit.
3
+ * Canonical source: packages/comment-config/src/index.ts
4
+ * The published CLI cannot import private @uploads/* packages, so this file
5
+ * is inlined into the tarball. Re-run the script after changing the source.
6
+ */
7
+ import { parse as parseYaml } from "yaml";
8
+ export const AUTO_COMMENT_OPTIONS = {
9
+ imageWidth: "auto",
10
+ maxInlineImages: 16, // MAX_INLINE_ATTACHMENT_IMAGES — renderer copies own the constant
11
+ metaPath: true,
12
+ metaState: true,
13
+ linkToFilePage: true,
14
+ note: null,
15
+ ingestGithubAttachments: true,
16
+ ingestBotAttachments: false,
17
+ };
18
+ export const NOTE_MAX_CHARS = 500;
19
+ const WIDTH_MIN = 160;
20
+ const WIDTH_MAX = 1000;
21
+ const MAX_INLINE_MIN = 1;
22
+ const MAX_INLINE_MAX = 48;
23
+ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, Math.round(n)));
24
+ export function parseRepoCommentConfig(text, format) {
25
+ const warnings = [];
26
+ let root;
27
+ try {
28
+ root = format === "json" ? JSON.parse(text) : parseYaml(text);
29
+ }
30
+ catch {
31
+ return { config: null, warnings: ["config file could not be parsed; ignoring it"] };
32
+ }
33
+ if (typeof root !== "object" || root === null || Array.isArray(root)) {
34
+ return { config: null, warnings };
35
+ }
36
+ const comment = root.comment;
37
+ if (typeof comment !== "object" || comment === null || Array.isArray(comment)) {
38
+ return { config: null, warnings };
39
+ }
40
+ const c = comment;
41
+ const config = {};
42
+ // imageWidth: "auto" | "full" | finite number (clamped)
43
+ if ("imageWidth" in c) {
44
+ const v = c.imageWidth;
45
+ if (v === "auto" || v === "full")
46
+ config.imageWidth = v;
47
+ else if (typeof v === "number" && Number.isFinite(v))
48
+ config.imageWidth = clamp(v, WIDTH_MIN, WIDTH_MAX);
49
+ else
50
+ warnings.push(`imageWidth: expected "auto", "full", or a number; dropped`);
51
+ }
52
+ // maxInlineImages: finite number (clamped)
53
+ if ("maxInlineImages" in c) {
54
+ const v = c.maxInlineImages;
55
+ if (typeof v === "number" && Number.isFinite(v))
56
+ config.maxInlineImages = clamp(v, MAX_INLINE_MIN, MAX_INLINE_MAX);
57
+ else
58
+ warnings.push(`maxInlineImages: expected a number; dropped`);
59
+ }
60
+ // linkToFilePage: boolean
61
+ if ("linkToFilePage" in c) {
62
+ const v = c.linkToFilePage;
63
+ if (typeof v === "boolean")
64
+ config.linkToFilePage = v;
65
+ else
66
+ warnings.push(`linkToFilePage: expected a boolean; dropped`);
67
+ }
68
+ // ingestGithubAttachments: boolean
69
+ if ("ingestGithubAttachments" in c) {
70
+ const v = c.ingestGithubAttachments;
71
+ if (typeof v === "boolean")
72
+ config.ingestGithubAttachments = v;
73
+ else
74
+ warnings.push(`ingestGithubAttachments: expected a boolean; dropped`);
75
+ }
76
+ // ingestBotAttachments: boolean
77
+ if ("ingestBotAttachments" in c) {
78
+ const v = c.ingestBotAttachments;
79
+ if (typeof v === "boolean")
80
+ config.ingestBotAttachments = v;
81
+ else
82
+ warnings.push(`ingestBotAttachments: expected a boolean; dropped`);
83
+ }
84
+ // meta.path / meta.state: booleans nested under `meta`
85
+ if ("meta" in c) {
86
+ const v = c.meta;
87
+ if (typeof v === "object" && v !== null && !Array.isArray(v)) {
88
+ const meta = v;
89
+ if ("path" in meta) {
90
+ if (typeof meta.path === "boolean")
91
+ config.metaPath = meta.path;
92
+ else
93
+ warnings.push(`meta.path: expected a boolean; dropped`);
94
+ }
95
+ if ("state" in meta) {
96
+ if (typeof meta.state === "boolean")
97
+ config.metaState = meta.state;
98
+ else
99
+ warnings.push(`meta.state: expected a boolean; dropped`);
100
+ }
101
+ }
102
+ else {
103
+ warnings.push(`meta: expected an object; dropped`);
104
+ }
105
+ }
106
+ // note: non-empty trimmed string, max NOTE_MAX_CHARS (never truncated)
107
+ if ("note" in c) {
108
+ const v = c.note;
109
+ if (typeof v === "string") {
110
+ const trimmed = v.trim();
111
+ if (trimmed.length === 0) {
112
+ // empty/whitespace note is treated as absent
113
+ }
114
+ else if (trimmed.length > NOTE_MAX_CHARS) {
115
+ warnings.push(`note: longer than ${NOTE_MAX_CHARS} characters; dropped (not truncated)`);
116
+ }
117
+ else {
118
+ config.note = trimmed;
119
+ }
120
+ }
121
+ else {
122
+ warnings.push(`note: expected a string; dropped`);
123
+ }
124
+ }
125
+ return { config, warnings };
126
+ }
127
+ export function resolveCommentOptions(repo, ws) {
128
+ const wsAsRepo = {
129
+ ...(ws?.imageWidth !== undefined ? { imageWidth: ws.imageWidth } : {}),
130
+ ...(ws?.maxInlineImages !== undefined ? { maxInlineImages: ws.maxInlineImages } : {}),
131
+ ...(ws?.showMetadata !== undefined
132
+ ? { metaPath: ws.showMetadata, metaState: ws.showMetadata }
133
+ : {}),
134
+ ...(ws?.linkToFilePage !== undefined ? { linkToFilePage: ws.linkToFilePage } : {}),
135
+ ...(ws?.note ? { note: ws.note } : {}),
136
+ ...(ws?.ingestGithubAttachments !== undefined
137
+ ? { ingestGithubAttachments: ws.ingestGithubAttachments }
138
+ : {}),
139
+ ...(ws?.ingestBotAttachments !== undefined
140
+ ? { ingestBotAttachments: ws.ingestBotAttachments }
141
+ : {}),
142
+ };
143
+ const options = { ...AUTO_COMMENT_OPTIONS };
144
+ const source = Object.fromEntries(Object.keys(AUTO_COMMENT_OPTIONS).map((k) => [k, "auto"]));
145
+ const apply = (cfg, from) => {
146
+ for (const key of [
147
+ "imageWidth",
148
+ "maxInlineImages",
149
+ "metaPath",
150
+ "metaState",
151
+ "linkToFilePage",
152
+ "ingestGithubAttachments",
153
+ "ingestBotAttachments",
154
+ ]) {
155
+ if (cfg[key] !== undefined && source[key] === "auto") {
156
+ options[key] = cfg[key];
157
+ source[key] = from;
158
+ }
159
+ }
160
+ if (cfg.note !== undefined && source.note === "auto") {
161
+ options.note = cfg.note;
162
+ source.note = from;
163
+ }
164
+ };
165
+ if (repo)
166
+ apply(repo, "repo");
167
+ apply(wsAsRepo, "workspace");
168
+ return { options, source };
169
+ }
170
+ /** Candidate paths, checked in this order — the first hit wins. */
171
+ export const REPO_CONFIG_PATHS = [
172
+ ".uploads.yml",
173
+ ".uploads.yaml",
174
+ ".uploads.json",
175
+ ".github/uploads.yml",
176
+ ".github/uploads.yaml",
177
+ ".github/uploads.json",
178
+ ];