@allocator-one/harness-cli 1.10.0 → 1.11.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
@@ -147,6 +147,23 @@ error rather than a silent precedence, and only one field per invocation may
147
147
  read stdin. Both are checked before anything is read, so a rejected command
148
148
  never consumes your pipe. One trailing newline is dropped, like `$(cat file)`.
149
149
 
150
+ ## Image and video uploads
151
+
152
+ `harness upload ./demo.mp4` prints a reusable markdown reference. Paste it
153
+ into an issue description or comment to show an inline video player. Use
154
+ `--json` for the server's URL, filename, content type, byte size, and markdown.
155
+ The MCP `upload` tool accepts the same local path and returns the same metadata.
156
+
157
+ Images (PNG, JPG, WebP, GIF) can be up to 25 MB. Videos must be `.mp4`, up to
158
+ 100 MB, with H.264 8-bit 4:2:0 video, optional AAC audio, and optional mov_text
159
+ subtitles. The server validates the media; it does not transcode other formats.
160
+
161
+ A newly uploaded video is private to its uploader until that user saves its
162
+ link in an issue description or comment. It then follows the owning issue's
163
+ access policy. Reusing the link elsewhere does not change that ownership.
164
+ `harness show` continues downloading images only, never linked videos.
165
+ The older `attach-image` command retains its 25 MB limit.
166
+
150
167
  ## Repo config
151
168
 
152
169
  A committed `.harness-cli/config.json`, discovered git-style by walking up
@@ -444,8 +444,8 @@ function path(record, ...keys) {
444
444
  }
445
445
 
446
446
  // src/commands/issues.ts
447
- import { openSync, closeSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
448
- import { basename } from "path";
447
+ import { openAsBlob, openSync, closeSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
448
+ import { basename, extname } from "path";
449
449
 
450
450
  // src/core/attachments.ts
451
451
  import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
@@ -1090,19 +1090,28 @@ function requireCommentId(commentId) {
1090
1090
  throw new UsageError(`not a comment id: ${commentId} (expected a UUID)`);
1091
1091
  }
1092
1092
  }
1093
- var MAX_ATTACHMENT_BYTES2 = 25e6;
1093
+ var MAX_IMAGE_BYTES = 25e6;
1094
+ var MAX_VIDEO_BYTES = 1e8;
1094
1095
  async function attachImage(ctx, path2) {
1095
- attachableFile(path2);
1096
- const bytes = readFileSync2(path2);
1096
+ return uploadAttachment(ctx, path2, MAX_IMAGE_BYTES);
1097
+ }
1098
+ async function attachFile(ctx, path2) {
1099
+ const maxBytes = extname(path2).toLowerCase() === ".mp4" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
1100
+ return uploadAttachment(ctx, path2, maxBytes);
1101
+ }
1102
+ async function uploadAttachment(ctx, path2, maxBytes) {
1103
+ attachableFile(path2, maxBytes);
1104
+ const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([readFileSync2(path2)], basename(path2));
1105
+ requireAttachmentSize(path2, file.size, maxBytes);
1097
1106
  const form = new FormData();
1098
- form.set("file", new File([bytes], basename(path2)));
1099
- const body = await ctx.client.postMultipart("/api/v1/uploads", form, bytes.byteLength);
1107
+ form.set("file", file);
1108
+ const body = await ctx.client.postMultipart("/api/v1/uploads", form, file.size);
1100
1109
  const attachment = body.data;
1101
1110
  const markdown = attachment["markdown"];
1102
1111
  if (typeof markdown !== "string") throw new CommandError("the upload response carried no markdown reference");
1103
1112
  return { json: attachment, human: clean(markdown) + "\n" };
1104
1113
  }
1105
- function attachableFile(path2) {
1114
+ function attachableFile(path2, maxBytes) {
1106
1115
  let fileStats;
1107
1116
  try {
1108
1117
  fileStats = statSync2(path2);
@@ -1113,17 +1122,18 @@ function attachableFile(path2) {
1113
1122
  const kind = fileStats.isDirectory() ? "directory" : "special file";
1114
1123
  throw new LocalError(`${path2} is a ${kind}, not a file.`);
1115
1124
  }
1116
- if (fileStats.size > MAX_ATTACHMENT_BYTES2) {
1117
- throw new LocalError(
1118
- `${path2} is too large: ${fileStats.size} bytes, and attachments are capped at ${MAX_ATTACHMENT_BYTES2}.`
1119
- );
1120
- }
1125
+ requireAttachmentSize(path2, fileStats.size, maxBytes);
1121
1126
  try {
1122
1127
  closeSync(openSync(path2, "r"));
1123
1128
  } catch (error) {
1124
1129
  throw new LocalError(`cannot read ${path2}: ${posixDetail(error)}`);
1125
1130
  }
1126
1131
  }
1132
+ function requireAttachmentSize(path2, size, maxBytes) {
1133
+ if (size > maxBytes) {
1134
+ throw new LocalError(`${path2} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
1135
+ }
1136
+ }
1127
1137
  function posixDetail(error) {
1128
1138
  const code = error.code;
1129
1139
  if (code === "ENOENT") return "no such file or directory";
@@ -1845,7 +1855,7 @@ function requireYes(command, id, yes) {
1845
1855
  if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
1846
1856
  }
1847
1857
  async function upload(ctx, path2) {
1848
- return attachImage(ctx, path2);
1858
+ return attachFile(ctx, path2);
1849
1859
  }
1850
1860
  function indent2(text) {
1851
1861
  return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
package/dist/cli.js CHANGED
@@ -82,7 +82,7 @@ import {
82
82
  version,
83
83
  workflowStates,
84
84
  writeCredentials
85
- } from "./chunk-KKAAKRKF.js";
85
+ } from "./chunk-WY6F3IJB.js";
86
86
 
87
87
  // src/cli.ts
88
88
  import { createInterface as createInterface2 } from "readline";
@@ -1901,17 +1901,17 @@ user.command("list").option("--json", "emit JSON").action(async (opts) => {
1901
1901
  (ctx) => userList(ctx)
1902
1902
  );
1903
1903
  });
1904
- program.command("upload").description("upload a file; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
1904
+ program.command("upload").description("upload an image (25 MB) or H.264 MP4 video (100 MB, optional AAC audio); prints markdown").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
1905
1905
  await withConfig(opts.json === true, true, (ctx) => {
1906
1906
  if (path === void 0)
1907
1907
  throw new UsageError(
1908
- "usage: harness upload <path> (png/jpg/webp/gif, up to 25 MB)"
1908
+ "usage: harness upload <path> (png/jpg/webp/gif up to 25 MB; H.264 MP4 with optional AAC audio up to 100 MB)"
1909
1909
  );
1910
1910
  return upload(ctx, path);
1911
1911
  });
1912
1912
  });
1913
1913
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
1914
- const { startMcpServer } = await import("./server-QVYSED3Z.js");
1914
+ const { startMcpServer } = await import("./server-5YNUXJO2.js");
1915
1915
  await startMcpServer();
1916
1916
  });
1917
1917
  program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
@@ -61,7 +61,7 @@ import {
61
61
  upload,
62
62
  userList,
63
63
  workflowStates
64
- } from "./chunk-KKAAKRKF.js";
64
+ } from "./chunk-WY6F3IJB.js";
65
65
 
66
66
  // src/mcp/server.ts
67
67
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -659,7 +659,7 @@ var TOOLS = [
659
659
  // ── Uploads ──
660
660
  {
661
661
  name: "upload",
662
- description: "Upload an image file (png/jpg/webp/gif, \u2264 25 MB) from a local path; returns markdown to reference it in issue fields or comments.",
662
+ description: "Upload a local image (png/jpg/webp/gif, up to 25 MB) or H.264 MP4 video (8-bit 4:2:0, optional AAC audio and mov_text subtitles, up to 100 MB). Returns metadata and markdown: images use image markdown; videos use ordinary links rendered as inline players in Harness issues. A video is private to its uploader until that user saves its link in an issue description or comment. Reading an issue does not automatically download videos.",
663
663
  readOnly: false,
664
664
  shape: { path: z.string().describe("local file path") },
665
665
  handler: (ctx, args) => upload(ctx, args["path"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/spec/openapi.json CHANGED
@@ -425,7 +425,7 @@
425
425
  "get": {
426
426
  "callbacks": {},
427
427
  "deprecated": false,
428
- "description": "Requires the `harness:read` scope. Streams the attachment bytes (binary response).",
428
+ "description": "Requires the `harness:read` scope. Streams attachment bytes. Videos recheck the owning organization's and issue's access on every request and use private, no-store caching. Supports one bytes Range: bounded, open-ended, or suffix; returns 206 with Content-Range, or 416 for an unsatisfiable range. Malformed, multipart, and conditional ranges fall back to a full 200. Images retain full-response behavior. Add ?mode=download for attachment disposition.",
429
429
  "parameters": [
430
430
  {
431
431
  "description": "Attachment UUID (the directory segment of the upload's URL)",
@@ -463,6 +463,12 @@
463
463
  "404": {
464
464
  "description": "Not found"
465
465
  },
466
+ "206": {
467
+ "description": "Requested video byte range, with Content-Range and Content-Length"
468
+ },
469
+ "416": {
470
+ "description": "Unsatisfiable range; Content-Range contains the actual file size"
471
+ },
466
472
  "422": {
467
473
  "description": "Validation error"
468
474
  }
@@ -791,7 +797,7 @@
791
797
  "post": {
792
798
  "callbacks": {},
793
799
  "deprecated": false,
794
- "description": "Requires the `harness:write` scope. Multipart upload of a png, jpg, webp, or gif (max 25 MB). Returns the attachment URL and a ready-to-paste `![alt](url)` markdown snippet for issue descriptions and comments. Attachments are private: the URL serves admins with a session; API clients fetch the bytes via `GET /api/v1/attachments/{attachment_id}/{file}`.",
800
+ "description": "Requires the `harness:write` scope. Multipart upload of a png, jpg, webp, or gif (25 MB), or H.264 MP4 video (100 MB; 8-bit 4:2:0 video, optional AAC audio and mov_text subtitles). No transcoding. Returns data.url, filename, content_type, byte_size, and markdown. Images use `![alt](url)`; videos use ordinary `[filename](url)` links. Supply an optional issue identifier to bind the video to that issue's access policy. Without it, the video is private to its uploader until that user saves its link in an issue description or comment. Copying a bound video link does not change its owning issue. Browser URLs require a session; API clients use GET /api/v1/attachments/{attachment_id}/{file} with a read-scoped token bound to the owning organization. Invalid media answers 422; an inaccessible issue answers 404. Existing image access remains unchanged.",
795
801
  "parameters": [],
796
802
  "requestBody": {
797
803
  "content": {
@@ -799,9 +805,13 @@
799
805
  "schema": {
800
806
  "properties": {
801
807
  "file": {
802
- "description": "png, jpg, webp, or gif up to 25 MB",
808
+ "description": "png, jpg, webp, or gif up to 25 MB; H.264 MP4 up to 100 MB",
803
809
  "format": "binary",
804
810
  "type": "string"
811
+ },
812
+ "issue": {
813
+ "description": "Optional owning issue identifier, for example IO-123. Must be accessible in the token's organization.",
814
+ "type": "string"
805
815
  }
806
816
  },
807
817
  "required": [
@@ -836,7 +846,7 @@
836
846
  "description": "File storage failed (transient) — retry"
837
847
  }
838
848
  },
839
- "summary": "Upload an image attachment",
849
+ "summary": "Upload an image or video attachment",
840
850
  "tags": []
841
851
  }
842
852
  },