@allocator-one/harness-cli 1.10.0 → 1.12.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 +48 -0
- package/dist/{chunk-KKAAKRKF.js → chunk-WY6F3IJB.js} +24 -14
- package/dist/cli.js +210 -9
- package/dist/{server-QVYSED3Z.js → server-5YNUXJO2.js} +2 -2
- package/package.json +1 -1
- package/spec/openapi.json +14 -4
package/README.md
CHANGED
|
@@ -39,6 +39,37 @@ Headless contexts (agents, CI): mint an `aone_…` API token (admin → API
|
|
|
39
39
|
tokens) and set `HARNESS_API_TOKEN`. Do not set it in interactive sessions —
|
|
40
40
|
the browser login attributes writes to the real user.
|
|
41
41
|
|
|
42
|
+
### Provider keys
|
|
43
|
+
|
|
44
|
+
Admins store the org's provider API keys (Anthropic, OpenAI, Google,
|
|
45
|
+
OpenRouter, Brave Search, Perplexity) in Harness under `Settings → Model
|
|
46
|
+
keys`. The CLI hands them to tooling without ever printing them:
|
|
47
|
+
|
|
48
|
+
- `harness keys list` — provider slugs the org has keys for and the env var
|
|
49
|
+
each maps to (`--json` for `{providers: [{provider, env}]}`); values are
|
|
50
|
+
never shown.
|
|
51
|
+
- `harness keys exec [--only <slugs>] [--override] -- <command> [args…]` —
|
|
52
|
+
runs the command with the keys in its environment and exits with the
|
|
53
|
+
command's exit code (128 + signal number when it was signalled). `--only
|
|
54
|
+
brave,perplexity` passes just those providers and withholds your shell's
|
|
55
|
+
own copies of the other provider variables, so the tool sees no more than
|
|
56
|
+
it needs; a requested provider the org has no key for keeps your shell's
|
|
57
|
+
variable (and says so on stderr). Slugs are trimmed and case-insensitive,
|
|
58
|
+
and an allowlist that names nothing (`--only ,`) is a usage error, not
|
|
59
|
+
"everything". Otherwise a variable already set in your shell wins unless
|
|
60
|
+
`--override` is given; a blank variable counts as unset. `--json` is
|
|
61
|
+
accepted for uniformity but changes nothing: stdout belongs to the command. A SIGTERM sent to
|
|
62
|
+
`harness` is forwarded to the command; Ctrl+C reaches the command from the
|
|
63
|
+
terminal directly and `harness` waits to report its exit. macOS and Linux
|
|
64
|
+
only.
|
|
65
|
+
|
|
66
|
+
Slug → variable, the same table rcl uses: anthropic → `ANTHROPIC_API_KEY`,
|
|
67
|
+
openai → `OPENAI_API_KEY`, google → `GEMINI_API_KEY`, openrouter →
|
|
68
|
+
`OPENROUTER_API_KEY`, brave → `BRAVE_API_KEY`, perplexity →
|
|
69
|
+
`PERPLEXITY_API_KEY`. Reading keys needs the `model-keys:read` scope: login
|
|
70
|
+
tokens carry it, `aone_` tokens must be minted with it. There is deliberately
|
|
71
|
+
no MCP tool for this — a tool result is model-visible text.
|
|
72
|
+
|
|
42
73
|
## Environment variables
|
|
43
74
|
|
|
44
75
|
| Variable | Effect |
|
|
@@ -147,6 +178,23 @@ error rather than a silent precedence, and only one field per invocation may
|
|
|
147
178
|
read stdin. Both are checked before anything is read, so a rejected command
|
|
148
179
|
never consumes your pipe. One trailing newline is dropped, like `$(cat file)`.
|
|
149
180
|
|
|
181
|
+
## Image and video uploads
|
|
182
|
+
|
|
183
|
+
`harness upload ./demo.mp4` prints a reusable markdown reference. Paste it
|
|
184
|
+
into an issue description or comment to show an inline video player. Use
|
|
185
|
+
`--json` for the server's URL, filename, content type, byte size, and markdown.
|
|
186
|
+
The MCP `upload` tool accepts the same local path and returns the same metadata.
|
|
187
|
+
|
|
188
|
+
Images (PNG, JPG, WebP, GIF) can be up to 25 MB. Videos must be `.mp4`, up to
|
|
189
|
+
100 MB, with H.264 8-bit 4:2:0 video, optional AAC audio, and optional mov_text
|
|
190
|
+
subtitles. The server validates the media; it does not transcode other formats.
|
|
191
|
+
|
|
192
|
+
A newly uploaded video is private to its uploader until that user saves its
|
|
193
|
+
link in an issue description or comment. It then follows the owning issue's
|
|
194
|
+
access policy. Reusing the link elsewhere does not change that ownership.
|
|
195
|
+
`harness show` continues downloading images only, never linked videos.
|
|
196
|
+
The older `attach-image` command retains its 25 MB limit.
|
|
197
|
+
|
|
150
198
|
## Repo config
|
|
151
199
|
|
|
152
200
|
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,12 +82,194 @@ 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";
|
|
89
89
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
90
90
|
|
|
91
|
+
// src/commands/keys.ts
|
|
92
|
+
import { spawn } from "child_process";
|
|
93
|
+
import { constants } from "os";
|
|
94
|
+
var KEY_ENV_VARS = /* @__PURE__ */ new Map([
|
|
95
|
+
["anthropic", "ANTHROPIC_API_KEY"],
|
|
96
|
+
["openai", "OPENAI_API_KEY"],
|
|
97
|
+
["google", "GEMINI_API_KEY"],
|
|
98
|
+
["openrouter", "OPENROUTER_API_KEY"],
|
|
99
|
+
["brave", "BRAVE_API_KEY"],
|
|
100
|
+
["perplexity", "PERPLEXITY_API_KEY"]
|
|
101
|
+
]);
|
|
102
|
+
function isRecord(value) {
|
|
103
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
104
|
+
}
|
|
105
|
+
async function fetchKeys(ctx) {
|
|
106
|
+
let body;
|
|
107
|
+
try {
|
|
108
|
+
body = await ctx.client.get("/api/v1/model-keys");
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof HttpError && error.status === 403 && error.message.includes("model-keys:read")) {
|
|
111
|
+
throw new CommandError(
|
|
112
|
+
`${error.message} \u2014 login tokens carry model-keys:read; aone_ API tokens must be minted with it`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const data = isRecord(body) ? body["data"] : void 0;
|
|
118
|
+
const keys2 = isRecord(data) ? data["keys"] : void 0;
|
|
119
|
+
if (!isRecord(keys2)) throw new CommandError("unexpected model-keys response: no keys map");
|
|
120
|
+
const stored = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const [slug, value] of Object.entries(keys2)) {
|
|
122
|
+
const canonical = slug.trim().toLowerCase();
|
|
123
|
+
if (stored.has(canonical)) continue;
|
|
124
|
+
if (typeof value === "string" && value.trim() !== "" && !value.includes("\0")) stored.set(canonical, value);
|
|
125
|
+
}
|
|
126
|
+
return stored;
|
|
127
|
+
}
|
|
128
|
+
function envVarFor(slug) {
|
|
129
|
+
return KEY_ENV_VARS.get(slug.trim().toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
function normalizeSlugs(only) {
|
|
132
|
+
return [...new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
|
|
133
|
+
}
|
|
134
|
+
function selectSlugs(stored, only) {
|
|
135
|
+
if (only === void 0 || only.length === 0) return new Map(stored);
|
|
136
|
+
const wanted = new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""));
|
|
137
|
+
return new Map([...stored].filter(([slug]) => wanted.has(slug)));
|
|
138
|
+
}
|
|
139
|
+
function unknownSlugs(only) {
|
|
140
|
+
return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
|
|
141
|
+
}
|
|
142
|
+
function withholdOthers(base, requested) {
|
|
143
|
+
const keep = new Set(requested.map((slug) => envVarFor(slug)));
|
|
144
|
+
const env = { ...base };
|
|
145
|
+
for (const name of KEY_ENV_VARS.values()) {
|
|
146
|
+
if (!keep.has(name)) delete env[name];
|
|
147
|
+
}
|
|
148
|
+
return env;
|
|
149
|
+
}
|
|
150
|
+
function unstoredSlugs(requested, stored) {
|
|
151
|
+
const canonical = [...new Set(requested.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
|
|
152
|
+
return canonical.filter((slug) => !stored.has(slug));
|
|
153
|
+
}
|
|
154
|
+
function toEnv(stored) {
|
|
155
|
+
const env = {};
|
|
156
|
+
for (const [slug, value] of stored) {
|
|
157
|
+
const name = envVarFor(slug);
|
|
158
|
+
if (name !== void 0) env[name] = value;
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|
|
162
|
+
function childEnv(base, fetched, override) {
|
|
163
|
+
const env = {};
|
|
164
|
+
for (const [name, value] of Object.entries(base)) {
|
|
165
|
+
if (value !== void 0) env[name] = value;
|
|
166
|
+
}
|
|
167
|
+
for (const [name, value] of Object.entries(fetched)) {
|
|
168
|
+
const existing = env[name];
|
|
169
|
+
if (override || existing === void 0 || existing.trim() === "") env[name] = value;
|
|
170
|
+
}
|
|
171
|
+
return env;
|
|
172
|
+
}
|
|
173
|
+
function exitCodeFor(code, signal) {
|
|
174
|
+
if (code !== null) return code;
|
|
175
|
+
if (signal !== null) {
|
|
176
|
+
const number = constants.signals[signal];
|
|
177
|
+
if (typeof number === "number") return 128 + number;
|
|
178
|
+
}
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
async function list2(ctx) {
|
|
182
|
+
const stored = await fetchKeys(ctx);
|
|
183
|
+
const providers = [...stored.keys()].sort().map((provider) => ({ provider, env: envVarFor(provider) ?? null }));
|
|
184
|
+
const human = providers.map(({ provider, env }) => `${clean(provider)} ${env ?? "(no env var)"}
|
|
185
|
+
`).join("");
|
|
186
|
+
return { json: { providers }, human };
|
|
187
|
+
}
|
|
188
|
+
var FORWARDED_SIGNALS = ["SIGTERM"];
|
|
189
|
+
var HELD_SIGNALS = ["SIGINT"];
|
|
190
|
+
async function exec(ctx, opts) {
|
|
191
|
+
const [program2, ...args] = opts.command;
|
|
192
|
+
if (program2 === void 0 || program2 === "") {
|
|
193
|
+
throw new UsageError("keys exec: name the command to run after `--`, e.g. `harness keys exec -- npm test`");
|
|
194
|
+
}
|
|
195
|
+
if (process.platform === "win32") {
|
|
196
|
+
throw new LocalError("keys exec is available on macOS and Linux only");
|
|
197
|
+
}
|
|
198
|
+
const onlyGiven = opts.only !== void 0 && opts.only.length > 0;
|
|
199
|
+
if (onlyGiven && normalizeSlugs(opts.only ?? []).length === 0) {
|
|
200
|
+
throw new UsageError(`keys exec: --only needs at least one provider slug \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
const unknown = unknownSlugs(opts.only ?? []);
|
|
203
|
+
if (unknown.length > 0) {
|
|
204
|
+
throw new UsageError(
|
|
205
|
+
`keys exec: unknown provider slug ${unknown.map(clean).join(", ")} \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const stored = await fetchKeys(ctx);
|
|
209
|
+
const requested = normalizeSlugs(opts.only ?? []);
|
|
210
|
+
const narrowed = requested.length > 0;
|
|
211
|
+
if (narrowed) {
|
|
212
|
+
const missing = unstoredSlugs(requested, stored);
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
`harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
|
|
216
|
+
`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const env = childEnv(
|
|
221
|
+
narrowed ? withholdOthers(process.env, requested) : process.env,
|
|
222
|
+
toEnv(selectSlugs(stored, requested)),
|
|
223
|
+
opts.override
|
|
224
|
+
);
|
|
225
|
+
const name = clean(program2);
|
|
226
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
227
|
+
let child;
|
|
228
|
+
try {
|
|
229
|
+
child = spawn(program2, args, { stdio: "inherit", env });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const code = error.code ?? "invalid arguments";
|
|
232
|
+
reject(new CommandError(`cannot start ${name}: ${code}`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const handlers = [];
|
|
236
|
+
for (const signal of FORWARDED_SIGNALS) {
|
|
237
|
+
const handler = () => {
|
|
238
|
+
child.kill(signal);
|
|
239
|
+
};
|
|
240
|
+
process.on(signal, handler);
|
|
241
|
+
handlers.push([signal, handler]);
|
|
242
|
+
}
|
|
243
|
+
for (const signal of HELD_SIGNALS) {
|
|
244
|
+
const handler = () => {
|
|
245
|
+
};
|
|
246
|
+
process.on(signal, handler);
|
|
247
|
+
handlers.push([signal, handler]);
|
|
248
|
+
}
|
|
249
|
+
let settled = false;
|
|
250
|
+
let spawned = false;
|
|
251
|
+
const settle = (outcome) => {
|
|
252
|
+
if (settled) return;
|
|
253
|
+
settled = true;
|
|
254
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
255
|
+
outcome();
|
|
256
|
+
};
|
|
257
|
+
child.once("spawn", () => {
|
|
258
|
+
spawned = true;
|
|
259
|
+
});
|
|
260
|
+
child.on("error", (error) => {
|
|
261
|
+
settle(() => {
|
|
262
|
+
const code = error.code ?? error.message;
|
|
263
|
+
reject(new CommandError(spawned ? `${name} failed: ${code}` : `cannot run ${name}: ${code}`));
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
child.on("exit", (code, signal) => {
|
|
267
|
+
settle(() => resolve(exitCodeFor(code, signal)));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return { json: { exit_code: exitCode }, human: "", exitCode };
|
|
271
|
+
}
|
|
272
|
+
|
|
91
273
|
// src/core/auth.ts
|
|
92
274
|
import { createHash, randomBytes } from "crypto";
|
|
93
275
|
import { execFile } from "child_process";
|
|
@@ -744,10 +926,10 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
744
926
|
}
|
|
745
927
|
|
|
746
928
|
// src/core/upgrade.ts
|
|
747
|
-
import { spawn, spawnSync } from "child_process";
|
|
929
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
748
930
|
import {
|
|
749
931
|
accessSync as accessSync2,
|
|
750
|
-
constants,
|
|
932
|
+
constants as constants2,
|
|
751
933
|
existsSync,
|
|
752
934
|
linkSync,
|
|
753
935
|
mkdirSync,
|
|
@@ -925,7 +1107,7 @@ function spawnRefresh() {
|
|
|
925
1107
|
const self = process.argv[1];
|
|
926
1108
|
if (self === void 0 || self === "") return;
|
|
927
1109
|
try {
|
|
928
|
-
const child =
|
|
1110
|
+
const child = spawn2(process.execPath, [self, UPDATE_CHECK_COMMAND], {
|
|
929
1111
|
detached: true,
|
|
930
1112
|
stdio: "ignore"
|
|
931
1113
|
});
|
|
@@ -1046,7 +1228,7 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1046
1228
|
};
|
|
1047
1229
|
}
|
|
1048
1230
|
try {
|
|
1049
|
-
accessSync2(globalRoot,
|
|
1231
|
+
accessSync2(globalRoot, constants2.W_OK);
|
|
1050
1232
|
} catch {
|
|
1051
1233
|
return {
|
|
1052
1234
|
ok: false,
|
|
@@ -1328,7 +1510,7 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1328
1510
|
process.stderr.write(hostNotice);
|
|
1329
1511
|
if (output.notice !== void 0) process.stderr.write(output.notice);
|
|
1330
1512
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1331
|
-
return exitWith(output.partial === true ? 1 : 0);
|
|
1513
|
+
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1332
1514
|
} catch (error) {
|
|
1333
1515
|
return fail(error, hostNotice);
|
|
1334
1516
|
}
|
|
@@ -1901,19 +2083,38 @@ user.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
|
1901
2083
|
(ctx) => userList(ctx)
|
|
1902
2084
|
);
|
|
1903
2085
|
});
|
|
1904
|
-
program.command("upload").description("upload
|
|
2086
|
+
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
2087
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1906
2088
|
if (path === void 0)
|
|
1907
2089
|
throw new UsageError(
|
|
1908
|
-
"usage: harness upload <path> (png/jpg/webp/gif
|
|
2090
|
+
"usage: harness upload <path> (png/jpg/webp/gif up to 25 MB; H.264 MP4 with optional AAC audio up to 100 MB)"
|
|
1909
2091
|
);
|
|
1910
2092
|
return upload(ctx, path);
|
|
1911
2093
|
});
|
|
1912
2094
|
});
|
|
1913
2095
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
1914
|
-
const { startMcpServer } = await import("./server-
|
|
2096
|
+
const { startMcpServer } = await import("./server-5YNUXJO2.js");
|
|
1915
2097
|
await startMcpServer();
|
|
1916
2098
|
});
|
|
2099
|
+
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
2100
|
+
keys.command("list").description("provider slugs the org has keys for and the env var each maps to \u2014 never the values").option("--json", "emit JSON").action(async (opts) => {
|
|
2101
|
+
await withConfig(opts.json === true, false, (ctx) => list2(ctx));
|
|
2102
|
+
});
|
|
2103
|
+
keys.command("exec").description(
|
|
2104
|
+
"run a command with the org's keys in its environment and exit with its exit code: harness keys exec [--override] -- <command> [args...]"
|
|
2105
|
+
).argument("[command...]", "the command and its arguments").option("--override", "let fetched keys replace variables already set in this shell").option(
|
|
2106
|
+
"--only <slugs>",
|
|
2107
|
+
"pass only these provider slugs (comma-separated, repeatable); default: everything stored",
|
|
2108
|
+
collect,
|
|
2109
|
+
[]
|
|
2110
|
+
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command, opts) => {
|
|
2111
|
+
const only = opts.only.flatMap((value) => value.split(","));
|
|
2112
|
+
await withConfig(
|
|
2113
|
+
false,
|
|
2114
|
+
false,
|
|
2115
|
+
(ctx) => exec(ctx, { command, override: opts.override === true, only })
|
|
2116
|
+
);
|
|
2117
|
+
});
|
|
1917
2118
|
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(
|
|
1918
2119
|
"--timeout <ms>",
|
|
1919
2120
|
"how long to wait for the browser callback",
|
|
@@ -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
|
},
|