@buildinternet/uploads 0.10.1 → 0.11.1
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 +18 -4
- package/dist/async.d.ts +5 -0
- package/dist/async.js +21 -0
- package/dist/cli-catalog.js +18 -4
- package/dist/cli-help.js +1 -1
- package/dist/cli.js +59 -7
- package/dist/commands/install.js +33 -15
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +164 -0
- package/dist/commands/telemetry.d.ts +4 -0
- package/dist/commands/telemetry.js +91 -0
- package/dist/commands.d.ts +92 -1
- package/dist/commands.js +364 -150
- package/dist/format-usage.d.ts +38 -0
- package/dist/format-usage.js +125 -0
- package/dist/mcp/batch-error.d.ts +16 -0
- package/dist/mcp/batch-error.js +24 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +33 -1
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +254 -94
- package/dist/report.d.ts +68 -0
- package/dist/report.js +152 -0
- package/dist/telemetry.d.ts +60 -0
- package/dist/telemetry.js +290 -0
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP tool set mirroring the CLI commands (put, attach, list, delete,
|
|
3
|
-
* usage, reconcile, purge_expired, comment, health, doctor). Config is
|
|
4
|
-
* resolved fresh per tool call so a
|
|
5
|
-
* per-call `workspace` argument behaves like the CLI's --workspace flag, and
|
|
6
|
-
* a missing token surfaces as a tool error rather than a startup failure.
|
|
7
|
-
*/
|
|
8
|
-
import { basename } from "node:path";
|
|
9
1
|
import { createUploadsClient } from "../client.js";
|
|
10
|
-
import { buildDoctorReport, makeGhTarget, prepareImageForUpload,
|
|
2
|
+
import { buildDoctorReport, makeGhTarget, prepareImageForUpload, syncAttachmentsComment, uploadAttachments, uploadPuts, } from "../commands.js";
|
|
11
3
|
import { resolveFrameId } from "../frame.js";
|
|
12
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
13
5
|
import { buildMarkdown } from "../embed.js";
|
|
@@ -19,6 +11,9 @@ import { rewriteKeyExtension } from "../optimize.js";
|
|
|
19
11
|
import { buildCliProvenance } from "../provenance.js";
|
|
20
12
|
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
21
13
|
import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
14
|
+
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
15
|
+
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
16
|
+
import { resolveApiUrl } from "../config.js";
|
|
22
17
|
function optBool(args, name) {
|
|
23
18
|
const v = args[name];
|
|
24
19
|
if (v === undefined || v === null)
|
|
@@ -268,13 +263,18 @@ export function createUploadsMcpTools(opts) {
|
|
|
268
263
|
},
|
|
269
264
|
{
|
|
270
265
|
name: "put",
|
|
271
|
-
description: "Upload
|
|
266
|
+
description: "Upload one or more files to uploads.sh and get public URL(s) plus GitHub-ready embed markdown. Single-file: pass `file` or `contentBase64`+`filename` (flat result with `url`/`embedUrl`/`markdown`). Multi-file: pass `files` (paths; parallel; returns `uploads`+`failures`). Prefer `embedUrl` in PR/issue markdown. With `pr`/`issue` keys are stable and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable — upload only non-sensitive media.",
|
|
272
267
|
inputSchema: {
|
|
273
268
|
type: "object",
|
|
274
269
|
properties: {
|
|
275
270
|
file: {
|
|
276
271
|
type: "string",
|
|
277
|
-
description: "Path of
|
|
272
|
+
description: "Path of a single file to upload. Exactly one of file, files, or contentBase64 is required.",
|
|
273
|
+
},
|
|
274
|
+
files: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" },
|
|
277
|
+
description: "Paths of multiple files to upload in parallel. Returns { uploads, failures }. Cannot combine with file, contentBase64, key, or filename.",
|
|
278
278
|
},
|
|
279
279
|
contentBase64: {
|
|
280
280
|
type: "string",
|
|
@@ -282,11 +282,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
282
282
|
},
|
|
283
283
|
filename: {
|
|
284
284
|
type: "string",
|
|
285
|
-
description: "Filename for contentBase64 content (drives the key and content type). With `file`, overrides the key's leaf (clean name) while keeping the pr/default path.",
|
|
285
|
+
description: "Filename for contentBase64 content (drives the key and content type). With single `file`, overrides the key's leaf (clean name) while keeping the pr/default path.",
|
|
286
286
|
},
|
|
287
287
|
key: {
|
|
288
288
|
type: "string",
|
|
289
|
-
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>).
|
|
289
|
+
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>). Single file only; cannot be combined with pr/issue.",
|
|
290
290
|
},
|
|
291
291
|
destination: {
|
|
292
292
|
type: "string",
|
|
@@ -306,7 +306,10 @@ export function createUploadsMcpTools(opts) {
|
|
|
306
306
|
type: "string",
|
|
307
307
|
description: "PR/issue/branch key segment (default: today, or UPLOADS_DEFAULT_REF). Cannot be combined with pr/issue.",
|
|
308
308
|
},
|
|
309
|
-
alt: {
|
|
309
|
+
alt: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Alt text for the markdown (default: each file's name; with multiple files applies to all).",
|
|
312
|
+
},
|
|
310
313
|
width: {
|
|
311
314
|
type: "number",
|
|
312
315
|
description: "Emit <img width=…> markdown instead of a plain image embed.",
|
|
@@ -348,14 +351,24 @@ export function createUploadsMcpTools(opts) {
|
|
|
348
351
|
},
|
|
349
352
|
async handler(args) {
|
|
350
353
|
const file = optString(args, "file");
|
|
354
|
+
const filesArg = optStringArray(args, "files");
|
|
351
355
|
const contentBase64 = optString(args, "contentBase64");
|
|
352
|
-
if (
|
|
353
|
-
usage("
|
|
356
|
+
if (filesArg !== undefined && filesArg.length === 0) {
|
|
357
|
+
usage("files must be a non-empty array of paths");
|
|
358
|
+
}
|
|
359
|
+
const multi = filesArg !== undefined;
|
|
360
|
+
const sources = [file !== undefined, multi, contentBase64 !== undefined];
|
|
361
|
+
if (sources.filter(Boolean).length !== 1) {
|
|
362
|
+
usage("exactly one of file, files, or contentBase64 is required");
|
|
354
363
|
}
|
|
355
364
|
const filenameArg = optString(args, "filename");
|
|
356
365
|
if (contentBase64 !== undefined && !filenameArg) {
|
|
357
366
|
usage("filename is required with contentBase64");
|
|
358
367
|
}
|
|
368
|
+
if (multi && filenameArg)
|
|
369
|
+
usage("filename cannot be combined with files");
|
|
370
|
+
if (multi && optString(args, "key"))
|
|
371
|
+
usage("key cannot be combined with files");
|
|
359
372
|
const target = ghTargetFromArgs(args, run);
|
|
360
373
|
const wantComment = optBool(args, "comment");
|
|
361
374
|
const dryRun = optBool(args, "dryRun");
|
|
@@ -394,67 +407,130 @@ export function createUploadsMcpTools(opts) {
|
|
|
394
407
|
usage(err instanceof Error ? err.message : String(err));
|
|
395
408
|
}
|
|
396
409
|
const { client } = clientFor(args);
|
|
397
|
-
const bytes = file !== undefined
|
|
398
|
-
? readFileArg(file)
|
|
399
|
-
: new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
400
|
-
const sourceName = file !== undefined ? (filenameArg ?? basename(file)) : filenameArg;
|
|
401
410
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
402
411
|
const frameOpts = mcpFrameOptions(args);
|
|
403
412
|
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
404
|
-
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
405
|
-
...frameOpts,
|
|
406
|
-
optimize: optimizeOpts,
|
|
407
|
-
});
|
|
408
|
-
const filename = prepared.filename;
|
|
409
|
-
let key = target ? ghAttachmentKey(target, filename) : keyArg;
|
|
410
|
-
if (key && prepared.optimized)
|
|
411
|
-
key = rewriteKeyExtension(key, filename);
|
|
412
413
|
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const alt = optString(args, "alt");
|
|
415
|
+
const width = optPosInt(args, "width") ?? defaults.width;
|
|
416
|
+
const contentType = optString(args, "contentType");
|
|
417
|
+
const putShared = {
|
|
418
|
+
client,
|
|
419
|
+
ghTarget: target,
|
|
416
420
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
417
421
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
418
422
|
ref: refArg ?? defaults.ref,
|
|
419
|
-
contentType: prepared.optimized ? prepared.contentType : optString(args, "contentType"),
|
|
420
423
|
deriveRepoFromGit: !noGit,
|
|
424
|
+
contentType,
|
|
421
425
|
dryRun,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
client: "uploads-mcp",
|
|
425
|
-
optimized: prepared.optimized,
|
|
426
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
427
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
428
|
-
}),
|
|
426
|
+
optimize: optimizeOpts,
|
|
427
|
+
frame: frameOpts,
|
|
429
428
|
metadata,
|
|
429
|
+
provenanceClient: "uploads-mcp",
|
|
430
|
+
alt,
|
|
431
|
+
width,
|
|
432
|
+
};
|
|
433
|
+
// Multi-file path (paths only — no base64 batch).
|
|
434
|
+
if (multi) {
|
|
435
|
+
const { uploads, failures } = await uploadPuts({
|
|
436
|
+
...putShared,
|
|
437
|
+
files: filesArg,
|
|
438
|
+
});
|
|
439
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
440
|
+
throw new ToolBatchError(batchFailureMessage(failures), { uploads, failures });
|
|
441
|
+
}
|
|
442
|
+
if (wantComment && target && uploads.length > 0) {
|
|
443
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
444
|
+
return { uploads, failures, comment, commentError };
|
|
445
|
+
}
|
|
446
|
+
return { uploads, failures };
|
|
447
|
+
}
|
|
448
|
+
// Single-file: contentBase64 still supported; paths go through uploadPuts.
|
|
449
|
+
if (contentBase64 !== undefined) {
|
|
450
|
+
const sourceName = filenameArg;
|
|
451
|
+
const bytes = new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
452
|
+
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
453
|
+
...frameOpts,
|
|
454
|
+
optimize: optimizeOpts,
|
|
455
|
+
});
|
|
456
|
+
const filename = prepared.filename;
|
|
457
|
+
let key = target ? ghAttachmentKey(target, filename) : keyArg;
|
|
458
|
+
if (key && prepared.optimized)
|
|
459
|
+
key = rewriteKeyExtension(key, filename);
|
|
460
|
+
const result = await client.put(prepared.bytes, {
|
|
461
|
+
filename,
|
|
462
|
+
key,
|
|
463
|
+
prefix: resolvedPrefix ?? defaults.prefix,
|
|
464
|
+
repo: optString(args, "repo") ?? defaults.repo,
|
|
465
|
+
ref: refArg ?? defaults.ref,
|
|
466
|
+
contentType: prepared.optimized ? prepared.contentType : contentType,
|
|
467
|
+
deriveRepoFromGit: !noGit,
|
|
468
|
+
dryRun,
|
|
469
|
+
provenance: buildCliProvenance({
|
|
470
|
+
sourceName,
|
|
471
|
+
client: "uploads-mcp",
|
|
472
|
+
optimized: prepared.optimized,
|
|
473
|
+
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
474
|
+
keepExif: optimizeOpts.keepExif === true,
|
|
475
|
+
}),
|
|
476
|
+
metadata,
|
|
477
|
+
});
|
|
478
|
+
const markdown = buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
479
|
+
alt: alt ?? sourceName,
|
|
480
|
+
width,
|
|
481
|
+
});
|
|
482
|
+
const optimize = {
|
|
483
|
+
optimized: prepared.optimized,
|
|
484
|
+
skippedReason: prepared.skippedReason,
|
|
485
|
+
originalBytes: prepared.originalBytes,
|
|
486
|
+
outputBytes: prepared.outputBytes,
|
|
487
|
+
filename: prepared.filename,
|
|
488
|
+
};
|
|
489
|
+
if (wantComment && target) {
|
|
490
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
491
|
+
return { ...result, markdown, optimize, frame: prepared.frame, comment, commentError };
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
...result,
|
|
495
|
+
markdown,
|
|
496
|
+
optimize,
|
|
497
|
+
frame: prepared.frame,
|
|
498
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const { uploads, failures, firstError } = await uploadPuts({
|
|
502
|
+
...putShared,
|
|
503
|
+
files: [file],
|
|
504
|
+
nameOverride: filenameArg,
|
|
505
|
+
explicitKey: keyArg,
|
|
430
506
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
507
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
508
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
509
|
+
}
|
|
510
|
+
const u = uploads[0];
|
|
511
|
+
const flat = {
|
|
512
|
+
workspace: u.workspace,
|
|
513
|
+
key: u.key,
|
|
514
|
+
url: u.url,
|
|
515
|
+
embedUrl: u.embedUrl,
|
|
516
|
+
size: u.size,
|
|
517
|
+
contentType: u.contentType,
|
|
518
|
+
replaced: u.replaced,
|
|
519
|
+
markdown: u.markdown,
|
|
520
|
+
optimize: u.optimize,
|
|
521
|
+
frame: u.frame,
|
|
522
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
441
523
|
};
|
|
442
524
|
if (wantComment && target) {
|
|
443
525
|
const { comment, commentError } = await syncComment(client, target);
|
|
444
|
-
return { ...
|
|
526
|
+
return { ...flat, comment, commentError };
|
|
445
527
|
}
|
|
446
|
-
return
|
|
447
|
-
...result,
|
|
448
|
-
markdown,
|
|
449
|
-
optimize,
|
|
450
|
-
frame: prepared.frame,
|
|
451
|
-
...(dryRun ? { dryRun: true } : {}),
|
|
452
|
-
};
|
|
528
|
+
return flat;
|
|
453
529
|
},
|
|
454
530
|
},
|
|
455
531
|
{
|
|
456
532
|
name: "attach",
|
|
457
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a
|
|
533
|
+
description: "Upload one or more files as stable PR/issue attachments (in parallel) and maintain a managed GitHub comment. Returns `uploads` and `failures` (one bad file does not abort the batch). Each success has `url`, `embedUrl`, and `markdown` (prefer embedUrl for GitHub). With no pr/issue, targets the current branch PR. Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
458
534
|
inputSchema: {
|
|
459
535
|
type: "object",
|
|
460
536
|
properties: {
|
|
@@ -521,45 +597,28 @@ export function createUploadsMcpTools(opts) {
|
|
|
521
597
|
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
522
598
|
if (Object.keys(metadata).length > 0)
|
|
523
599
|
validateMetaMap(metadata);
|
|
524
|
-
const uploads =
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
541
|
-
}),
|
|
542
|
-
metadata,
|
|
543
|
-
});
|
|
544
|
-
uploads.push({
|
|
545
|
-
...result,
|
|
546
|
-
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
547
|
-
alt: sourceName,
|
|
548
|
-
}),
|
|
549
|
-
frame: prepared.frame,
|
|
550
|
-
optimize: {
|
|
551
|
-
optimized: prepared.optimized,
|
|
552
|
-
skippedReason: prepared.skippedReason,
|
|
553
|
-
originalBytes: prepared.originalBytes,
|
|
554
|
-
outputBytes: prepared.outputBytes,
|
|
555
|
-
filename: prepared.filename,
|
|
556
|
-
},
|
|
600
|
+
const { uploads, failures } = await uploadAttachments({
|
|
601
|
+
client,
|
|
602
|
+
target,
|
|
603
|
+
files,
|
|
604
|
+
contentType,
|
|
605
|
+
optimize: optimizeOpts,
|
|
606
|
+
frame: frameOpts,
|
|
607
|
+
metadata,
|
|
608
|
+
provenanceClient: "uploads-mcp",
|
|
609
|
+
});
|
|
610
|
+
// Total failure → isError with full failures[] for agents.
|
|
611
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
612
|
+
throw new ToolBatchError(batchFailureMessage(failures), {
|
|
613
|
+
target,
|
|
614
|
+
uploads,
|
|
615
|
+
failures,
|
|
557
616
|
});
|
|
558
617
|
}
|
|
559
618
|
if (optBool(args, "noComment"))
|
|
560
|
-
return { target, uploads };
|
|
619
|
+
return { target, uploads, failures };
|
|
561
620
|
const { comment, commentError } = await syncComment(client, target);
|
|
562
|
-
return { target, uploads, comment, commentError };
|
|
621
|
+
return { target, uploads, failures, comment, commentError };
|
|
563
622
|
},
|
|
564
623
|
},
|
|
565
624
|
{
|
|
@@ -626,9 +685,28 @@ export function createUploadsMcpTools(opts) {
|
|
|
626
685
|
return client.delete(key);
|
|
627
686
|
},
|
|
628
687
|
},
|
|
688
|
+
{
|
|
689
|
+
name: "get_metadata",
|
|
690
|
+
description: "Read an object's queryable custom metadata (D1 key-value pairs, not R2 provenance). Returns `{ metadata }` (empty when none). Object must exist. Same as `uploads meta get`.",
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: "object",
|
|
693
|
+
properties: {
|
|
694
|
+
key: { type: "string", description: "Object key to inspect." },
|
|
695
|
+
workspace: workspaceProp,
|
|
696
|
+
},
|
|
697
|
+
required: ["key"],
|
|
698
|
+
additionalProperties: false,
|
|
699
|
+
},
|
|
700
|
+
async handler(args) {
|
|
701
|
+
const key = optString(args, "key");
|
|
702
|
+
if (!key)
|
|
703
|
+
usage("key is required");
|
|
704
|
+
return clientFor(args).client.getMetadata(key);
|
|
705
|
+
},
|
|
706
|
+
},
|
|
629
707
|
{
|
|
630
708
|
name: "set_metadata",
|
|
631
|
-
description: "Merge-set and/or delete an object's queryable custom metadata (D1
|
|
709
|
+
description: "Merge-set and/or delete an object's queryable custom metadata (D1 key-value pairs, not R2 provenance). `set` wins over `delete` for the same key. " +
|
|
632
710
|
METADATA_DESCRIPTION +
|
|
633
711
|
" Requires at least one of `set` or `delete`. Same as `uploads meta set`.",
|
|
634
712
|
inputSchema: {
|
|
@@ -773,5 +851,87 @@ export function createUploadsMcpTools(opts) {
|
|
|
773
851
|
return buildDoctorReport(config, client);
|
|
774
852
|
},
|
|
775
853
|
},
|
|
854
|
+
{
|
|
855
|
+
name: "report",
|
|
856
|
+
description: "Send an explicit diagnostic report to the uploads team (message + optional text log). " +
|
|
857
|
+
"Only call this when the user asked to submit feedback, a bug report, or error logs — " +
|
|
858
|
+
"never automatically. Do not include tokens, secrets, or private file contents. " +
|
|
859
|
+
"Same as `uploads report`.",
|
|
860
|
+
inputSchema: {
|
|
861
|
+
type: "object",
|
|
862
|
+
properties: {
|
|
863
|
+
message: {
|
|
864
|
+
type: "string",
|
|
865
|
+
description: "Short description of the problem (required, 5–4000 chars).",
|
|
866
|
+
},
|
|
867
|
+
type: {
|
|
868
|
+
type: "string",
|
|
869
|
+
description: `One of: ${REPORT_TYPES.join(", ")} (default: other).`,
|
|
870
|
+
},
|
|
871
|
+
contact: {
|
|
872
|
+
type: "string",
|
|
873
|
+
description: "Optional contact for follow-up (email or handle).",
|
|
874
|
+
},
|
|
875
|
+
command: {
|
|
876
|
+
type: "string",
|
|
877
|
+
description: "Command that failed (e.g. put) — name only, no paths or args.",
|
|
878
|
+
},
|
|
879
|
+
errorCode: {
|
|
880
|
+
type: "string",
|
|
881
|
+
description: "Optional UploadsError code (e.g. KEY_POLICY).",
|
|
882
|
+
},
|
|
883
|
+
attachmentText: {
|
|
884
|
+
type: "string",
|
|
885
|
+
description: "Optional text log/trace body the user consented to send (max 256 KiB). Not a file path.",
|
|
886
|
+
},
|
|
887
|
+
attachmentFilename: {
|
|
888
|
+
type: "string",
|
|
889
|
+
description: "Filename label for attachmentText (default: trace.txt).",
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
required: ["message"],
|
|
893
|
+
additionalProperties: false,
|
|
894
|
+
},
|
|
895
|
+
async handler(args) {
|
|
896
|
+
const messageRaw = optString(args, "message");
|
|
897
|
+
if (!messageRaw)
|
|
898
|
+
usage("message is required");
|
|
899
|
+
const validated = validateReportMessage(messageRaw);
|
|
900
|
+
if (!validated.ok)
|
|
901
|
+
usage(validated.error);
|
|
902
|
+
const typeRaw = optString(args, "type");
|
|
903
|
+
if (typeRaw && !parseReportType(typeRaw)) {
|
|
904
|
+
usage(`type must be one of: ${REPORT_TYPES.join(", ")}`);
|
|
905
|
+
}
|
|
906
|
+
const type = parseReportType(typeRaw) ?? "other";
|
|
907
|
+
let attachment;
|
|
908
|
+
const attachmentText = optString(args, "attachmentText");
|
|
909
|
+
if (attachmentText) {
|
|
910
|
+
try {
|
|
911
|
+
attachment = attachmentFromText(attachmentText, optString(args, "attachmentFilename") ?? "trace.txt");
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
usage(err instanceof Error ? err.message : String(err));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const payload = buildReportPayload(validated.message, {
|
|
918
|
+
type,
|
|
919
|
+
contact: optString(args, "contact"),
|
|
920
|
+
surface: "mcp",
|
|
921
|
+
command: optString(args, "command"),
|
|
922
|
+
errorCode: optString(args, "errorCode"),
|
|
923
|
+
attachment,
|
|
924
|
+
});
|
|
925
|
+
const apiUrl = resolveApiUrl(globals);
|
|
926
|
+
const result = await submitReport(payload, { apiUrl });
|
|
927
|
+
if (!result.ok)
|
|
928
|
+
usage(`couldn't send report: ${result.error}`);
|
|
929
|
+
return {
|
|
930
|
+
ok: true,
|
|
931
|
+
id: result.id,
|
|
932
|
+
hasAttachment: result.hasAttachment,
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
},
|
|
776
936
|
];
|
|
777
937
|
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const REPORT_TYPES: readonly ["bug", "error", "idea", "other"];
|
|
2
|
+
export type ReportType = (typeof REPORT_TYPES)[number];
|
|
3
|
+
export declare const MIN_REPORT_MESSAGE = 5;
|
|
4
|
+
export declare const MAX_REPORT_MESSAGE = 4000;
|
|
5
|
+
export declare const MAX_REPORT_ATTACHMENT_BYTES: number;
|
|
6
|
+
export interface ReportAttachment {
|
|
7
|
+
filename: string;
|
|
8
|
+
contentType: string;
|
|
9
|
+
body: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ReportPayload {
|
|
12
|
+
message: string;
|
|
13
|
+
type: ReportType;
|
|
14
|
+
contact?: string;
|
|
15
|
+
surface: "cli" | "mcp";
|
|
16
|
+
cliVersion: string;
|
|
17
|
+
clientKind: string;
|
|
18
|
+
agentName?: string;
|
|
19
|
+
anonId?: string;
|
|
20
|
+
os: string;
|
|
21
|
+
arch: string;
|
|
22
|
+
runtime: string;
|
|
23
|
+
command?: string;
|
|
24
|
+
errorCode?: string;
|
|
25
|
+
attachment?: ReportAttachment;
|
|
26
|
+
}
|
|
27
|
+
export interface SubmitReportOptions {
|
|
28
|
+
apiUrl?: string;
|
|
29
|
+
fetchImpl?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export type ValidateMessageResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
message: string;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function validateReportMessage(raw: string): ValidateMessageResult;
|
|
39
|
+
export declare function parseReportType(raw: string | undefined): ReportType | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Load a local text file as an attachment. Rejects oversized / missing files.
|
|
42
|
+
* Callers must have obtained the path from explicit user input (`--file`).
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadReportAttachment(path: string): ReportAttachment;
|
|
45
|
+
/** Build attachment from an in-memory string (MCP / piped text). */
|
|
46
|
+
export declare function attachmentFromText(body: string, filename?: string, contentType?: string): ReportAttachment;
|
|
47
|
+
export declare function buildReportPayload(message: string, opts?: {
|
|
48
|
+
type?: ReportType;
|
|
49
|
+
contact?: string;
|
|
50
|
+
surface?: "cli" | "mcp";
|
|
51
|
+
command?: string;
|
|
52
|
+
errorCode?: string;
|
|
53
|
+
attachment?: ReportAttachment;
|
|
54
|
+
}, deps?: {
|
|
55
|
+
version?: string;
|
|
56
|
+
dataDir?: string;
|
|
57
|
+
includeAnonId?: boolean;
|
|
58
|
+
}): ReportPayload;
|
|
59
|
+
export type SubmitReportResult = {
|
|
60
|
+
ok: true;
|
|
61
|
+
id: string;
|
|
62
|
+
hasAttachment: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
export declare function submitReport(payload: ReportPayload, opts?: SubmitReportOptions): Promise<SubmitReportResult>;
|
|
68
|
+
export declare function reportFallbackHint(): string;
|