@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
|
|
1093
|
+
var MAX_IMAGE_BYTES = 25e6;
|
|
1094
|
+
var MAX_VIDEO_BYTES = 1e8;
|
|
1094
1095
|
async function attachImage(ctx, path2) {
|
|
1095
|
-
|
|
1096
|
-
|
|
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",
|
|
1099
|
-
const body = await ctx.client.postMultipart("/api/v1/uploads", form,
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
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
|
|
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 (
|
|
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 ``; 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
|
},
|