@allocator-one/harness-cli 1.6.0 → 1.8.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 +24 -1
- package/dist/{chunk-DQDNAG5E.js → chunk-NQVQNKMC.js} +149 -6
- package/dist/cli.js +107 -16
- package/dist/{server-WBZ5OPHK.js → server-FWNT3HMX.js} +89 -5
- package/package.json +1 -1
- package/spec/openapi.json +570 -2
package/README.md
CHANGED
|
@@ -53,7 +53,9 @@ the browser login attributes writes to the real user.
|
|
|
53
53
|
|
|
54
54
|
`harness mcp` runs a stdio MCP server exposing every operation as a typed
|
|
55
55
|
tool — issue_list, issue_show, issue_create, issue_update, issue_claim,
|
|
56
|
-
issue_close, issue_comment,
|
|
56
|
+
issue_close, issue_comment, issue_comment_edit, issue_comment_delete,
|
|
57
|
+
relation_list/create/update/delete, relation_add/rm (blocking shorthand),
|
|
58
|
+
link_list/add/update/rm,
|
|
57
59
|
project_list/show/create/update, milestone_list/create,
|
|
58
60
|
document_list/show/create/update, label_list/create, team_list,
|
|
59
61
|
team_create, workflow_states, user_list, stats, upload. Read-only tools carry
|
|
@@ -69,6 +71,27 @@ Example tool call: `issue_list` with `{"ready": true, "team": "ALL"}`
|
|
|
69
71
|
returns the ready queue as JSON; `issue_claim` with `{"id": "IO-123"}`
|
|
70
72
|
claims atomically (a conflict means someone else holds it).
|
|
71
73
|
|
|
74
|
+
Typed issue relations are available without losing the concise blocking
|
|
75
|
+
workflow. `harness dep add|rm` and the `relation_add`/`relation_rm` MCP tools
|
|
76
|
+
remain the blocker-first shorthand; the full surface is:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
harness relation list IO-123
|
|
80
|
+
harness relation add IO-123 IO-456 --type related
|
|
81
|
+
harness relation update IO-123 IO-456 --type blocks
|
|
82
|
+
harness relation rm IO-123 IO-456
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Relation update and removal accept either the relation id from `relation list`
|
|
86
|
+
or a unique related issue identifier. If the same pair has more than one
|
|
87
|
+
relation, the CLI refuses to guess and requires the relation id. Link updates
|
|
88
|
+
use the same safe lookup rule by id or exact URL; the existing `link rm`
|
|
89
|
+
command keeps its first-match behavior for backward compatibility:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
harness link update IO-123 https://example.com/old --url https://example.com/new --title "Runbook"
|
|
93
|
+
```
|
|
94
|
+
|
|
72
95
|
Issue creation resolves `--label` names within the target team first, then at
|
|
73
96
|
workspace scope. Missing names are created at workspace scope before the issue
|
|
74
97
|
is filed, and the final label ids travel in the single issue-create request, so
|
|
@@ -538,6 +538,10 @@ var STATUS_STATE_TYPES = {
|
|
|
538
538
|
};
|
|
539
539
|
var READY_CLAIM_ATTEMPTS = 10;
|
|
540
540
|
var IDENTIFIER_FORMAT = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
541
|
+
var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
542
|
+
function isUuid(value) {
|
|
543
|
+
return UUID_FORMAT.test(value);
|
|
544
|
+
}
|
|
541
545
|
function requireIdentifiers(identifiers) {
|
|
542
546
|
const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
|
|
543
547
|
if (bad.length > 0) {
|
|
@@ -786,8 +790,9 @@ async function comments(ctx, identifier) {
|
|
|
786
790
|
if (body.data.length === 0) return { json: body.data, human: "No comments.\n" };
|
|
787
791
|
const human = body.data.map((comment2) => {
|
|
788
792
|
const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
|
|
793
|
+
const id = clean(comment2["id"] ?? "unknown id");
|
|
789
794
|
const text = cleanMultiline(str2(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
|
|
790
|
-
return `[${str2(comment2["inserted_at"])}] ${author}:
|
|
795
|
+
return `[${str2(comment2["inserted_at"])}] ${author} (${id}):
|
|
791
796
|
${text}
|
|
792
797
|
`;
|
|
793
798
|
}).join("\n");
|
|
@@ -815,6 +820,76 @@ async function dep(ctx, verb, issue, blocker) {
|
|
|
815
820
|
}
|
|
816
821
|
throw new UsageError("usage: harness dep add|rm <issue> <blocker> (the blocker blocks the issue)");
|
|
817
822
|
}
|
|
823
|
+
var RELATION_CREATE_TYPES = [
|
|
824
|
+
"blocks",
|
|
825
|
+
"blocked_by",
|
|
826
|
+
"related",
|
|
827
|
+
"duplicate",
|
|
828
|
+
"duplicate_of",
|
|
829
|
+
"duplicated_by"
|
|
830
|
+
];
|
|
831
|
+
var RELATION_UPDATE_TYPES = ["blocks", "blocked_by", "related", "duplicate_of"];
|
|
832
|
+
function requireRelationType(type, allowed, operation) {
|
|
833
|
+
if (!allowed.includes(type)) {
|
|
834
|
+
throw new UsageError(`relation ${operation} type must be one of: ${allowed.join(", ")}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function findRelation(ctx, issue, idOrIssue) {
|
|
838
|
+
const response = await ctx.client.get(`/api/v1/issues/${issue}/relations`);
|
|
839
|
+
const idMatch = response.data.find((candidate) => candidate["id"] === idOrIssue);
|
|
840
|
+
if (idMatch !== void 0) return idMatch;
|
|
841
|
+
const issueMatches = response.data.filter((candidate) => candidate["other_issue"] === idOrIssue);
|
|
842
|
+
if (issueMatches.length > 1) {
|
|
843
|
+
throw new CommandError(`multiple relations on ${issue} match ${idOrIssue}; use a relation id`);
|
|
844
|
+
}
|
|
845
|
+
if (issueMatches.length === 0) {
|
|
846
|
+
throw new CommandError(`no relation on ${issue} matches ${idOrIssue} (by id or related issue)`);
|
|
847
|
+
}
|
|
848
|
+
return issueMatches[0];
|
|
849
|
+
}
|
|
850
|
+
async function resolveRelationId(ctx, issue, idOrIssue) {
|
|
851
|
+
if (isUuid(idOrIssue)) return idOrIssue.toLowerCase();
|
|
852
|
+
const relation = await findRelation(ctx, issue, idOrIssue);
|
|
853
|
+
return String(relation["id"]);
|
|
854
|
+
}
|
|
855
|
+
async function relationList(ctx, issue) {
|
|
856
|
+
requireIdentifiers([issue]);
|
|
857
|
+
const response = await ctx.client.get(`/api/v1/issues/${issue}/relations`);
|
|
858
|
+
const human = response.data.length === 0 ? "No relations.\n" : table(
|
|
859
|
+
["ID", "DIRECTION", "ISSUE"],
|
|
860
|
+
response.data.map((relation) => [
|
|
861
|
+
String(relation["id"] ?? ""),
|
|
862
|
+
String(relation["direction"] ?? ""),
|
|
863
|
+
String(relation["other_issue"] ?? "")
|
|
864
|
+
])
|
|
865
|
+
);
|
|
866
|
+
return { json: response.data, human };
|
|
867
|
+
}
|
|
868
|
+
async function relationAdd(ctx, issue, relatedIssue, type) {
|
|
869
|
+
requireIdentifiers([issue, relatedIssue]);
|
|
870
|
+
requireRelationType(type, RELATION_CREATE_TYPES, "add");
|
|
871
|
+
const response = await ctx.client.post(`/api/v1/issues/${issue}/relations`, {
|
|
872
|
+
type,
|
|
873
|
+
related_issue: relatedIssue
|
|
874
|
+
});
|
|
875
|
+
return { json: response.data, human: `Related ${issue} to ${relatedIssue} as ${type}
|
|
876
|
+
` };
|
|
877
|
+
}
|
|
878
|
+
async function relationUpdate(ctx, issue, idOrIssue, type) {
|
|
879
|
+
requireIdentifiers([issue]);
|
|
880
|
+
requireRelationType(type, RELATION_UPDATE_TYPES, "update");
|
|
881
|
+
const id = await resolveRelationId(ctx, issue, idOrIssue);
|
|
882
|
+
const response = await ctx.client.patch(`/api/v1/issues/${issue}/relations/${id}`, { type });
|
|
883
|
+
return { json: response.data, human: `Updated relation ${id} on ${issue} to ${type}
|
|
884
|
+
` };
|
|
885
|
+
}
|
|
886
|
+
async function relationRm(ctx, issue, idOrIssue) {
|
|
887
|
+
requireIdentifiers([issue]);
|
|
888
|
+
const id = await resolveRelationId(ctx, issue, idOrIssue);
|
|
889
|
+
await ctx.client.delete(`/api/v1/issues/${issue}/relations/${id}`);
|
|
890
|
+
return { json: { deleted: id }, human: `Removed relation ${id} from ${issue}
|
|
891
|
+
` };
|
|
892
|
+
}
|
|
818
893
|
async function comment(ctx, identifier, textWords, mentions) {
|
|
819
894
|
requireIdentifiers([identifier]);
|
|
820
895
|
if (textWords.length === 0) {
|
|
@@ -826,6 +901,40 @@ async function comment(ctx, identifier, textWords, mentions) {
|
|
|
826
901
|
return { json: response.data, human: `Commented on ${identifier}
|
|
827
902
|
` };
|
|
828
903
|
}
|
|
904
|
+
async function editComment(ctx, identifier, commentId, textWords) {
|
|
905
|
+
requireIdentifiers([identifier]);
|
|
906
|
+
requireCommentId(commentId);
|
|
907
|
+
const body = textWords.join(" ");
|
|
908
|
+
if (body.trim() === "") {
|
|
909
|
+
throw new UsageError("the comment text is empty \u2014 nothing to edit");
|
|
910
|
+
}
|
|
911
|
+
const response = await ctx.client.patch(
|
|
912
|
+
`/api/v1/issues/${identifier}/comments/${commentId}`,
|
|
913
|
+
{ body }
|
|
914
|
+
);
|
|
915
|
+
return {
|
|
916
|
+
json: response.data,
|
|
917
|
+
human: `Edited comment ${commentId} on ${identifier}
|
|
918
|
+
`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
async function deleteComment(ctx, identifier, commentId) {
|
|
922
|
+
requireIdentifiers([identifier]);
|
|
923
|
+
requireCommentId(commentId);
|
|
924
|
+
const response = await ctx.client.delete(
|
|
925
|
+
`/api/v1/issues/${identifier}/comments/${commentId}`
|
|
926
|
+
);
|
|
927
|
+
return {
|
|
928
|
+
json: response.data,
|
|
929
|
+
human: `Deleted comment ${commentId} from ${identifier}
|
|
930
|
+
`
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function requireCommentId(commentId) {
|
|
934
|
+
if (!UUID_FORMAT.test(commentId)) {
|
|
935
|
+
throw new UsageError(`not a comment id: ${commentId} (expected a UUID)`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
829
938
|
var MAX_ATTACHMENT_BYTES2 = 25e6;
|
|
830
939
|
async function attachImage(ctx, path2) {
|
|
831
940
|
attachableFile(path2);
|
|
@@ -910,7 +1019,6 @@ function validateType(type) {
|
|
|
910
1019
|
throw new UsageError(`not an issue type: ${type} (expected ${ISSUE_TYPES.join(", ")})`);
|
|
911
1020
|
}
|
|
912
1021
|
}
|
|
913
|
-
var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
914
1022
|
function requestedLabelNames(names) {
|
|
915
1023
|
const unique = /* @__PURE__ */ new Map();
|
|
916
1024
|
for (const name of requestedLabelInputs(names)) {
|
|
@@ -1068,6 +1176,8 @@ function listParams(ctx, opts) {
|
|
|
1068
1176
|
params = putParam(params, "label", opts.label);
|
|
1069
1177
|
params = putParam(params, "scope", opts.scope);
|
|
1070
1178
|
params = putParam(params, "assignee", opts.assignee);
|
|
1179
|
+
params = putParam(params, "created_by", opts.createdBy);
|
|
1180
|
+
params = putParam(params, "involved", opts.involved);
|
|
1071
1181
|
if (opts.type !== void 0) params = putParam(params, "label", `type/${opts.type}`);
|
|
1072
1182
|
if (opts.status !== void 0) {
|
|
1073
1183
|
const stateTypes = Object.hasOwn(STATUS_STATE_TYPES, opts.status) ? STATUS_STATE_TYPES[opts.status] : void 0;
|
|
@@ -1349,13 +1459,36 @@ async function linkAdd(ctx, issue, url, title) {
|
|
|
1349
1459
|
return { json: response.data, human: `Linked ${clean(url)} to ${issue}
|
|
1350
1460
|
` };
|
|
1351
1461
|
}
|
|
1352
|
-
async function
|
|
1353
|
-
requireIdentifiers([issue]);
|
|
1462
|
+
async function findLink(ctx, issue, idOrUrl, allowDuplicateUrl = false) {
|
|
1354
1463
|
const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
|
|
1355
|
-
const
|
|
1356
|
-
if (
|
|
1464
|
+
const idMatch = body.data.find((candidate) => candidate["id"] === idOrUrl);
|
|
1465
|
+
if (idMatch !== void 0) return idMatch;
|
|
1466
|
+
const urlMatches = body.data.filter((candidate) => candidate["url"] === idOrUrl);
|
|
1467
|
+
if (urlMatches.length > 1 && !allowDuplicateUrl) {
|
|
1468
|
+
throw new CommandError(`multiple links on ${issue} match ${idOrUrl}; use a link id`);
|
|
1469
|
+
}
|
|
1470
|
+
if (urlMatches.length === 0) {
|
|
1357
1471
|
throw new CommandError(`no link on ${issue} matches ${idOrUrl} (by id or exact URL)`);
|
|
1358
1472
|
}
|
|
1473
|
+
return urlMatches[0];
|
|
1474
|
+
}
|
|
1475
|
+
async function linkUpdate(ctx, issue, idOrUrl, opts) {
|
|
1476
|
+
requireIdentifiers([issue]);
|
|
1477
|
+
let patch = {};
|
|
1478
|
+
patch = putOpt2(patch, "url", opts.url);
|
|
1479
|
+
patch = putOpt2(patch, "title", opts.title);
|
|
1480
|
+
if (Object.keys(patch).length === 0) {
|
|
1481
|
+
throw new UsageError("usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>");
|
|
1482
|
+
}
|
|
1483
|
+
const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str3((await findLink(ctx, issue, idOrUrl))["id"]);
|
|
1484
|
+
if (!isUuid(idOrUrl)) patch["expected_url"] = idOrUrl;
|
|
1485
|
+
const response = await ctx.client.patch(`/api/v1/issues/${issue}/links/${id}`, patch);
|
|
1486
|
+
return { json: response.data, human: `Updated link ${clean(response.data["url"])} on ${issue}
|
|
1487
|
+
` };
|
|
1488
|
+
}
|
|
1489
|
+
async function linkRm(ctx, issue, idOrUrl) {
|
|
1490
|
+
requireIdentifiers([issue]);
|
|
1491
|
+
const link = await findLink(ctx, issue, idOrUrl, true);
|
|
1359
1492
|
await ctx.client.delete(`/api/v1/issues/${issue}/links/${str3(link["id"])}`);
|
|
1360
1493
|
return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
|
|
1361
1494
|
` };
|
|
@@ -1827,7 +1960,16 @@ export {
|
|
|
1827
1960
|
archive,
|
|
1828
1961
|
comments,
|
|
1829
1962
|
dep,
|
|
1963
|
+
RELATION_CREATE_TYPES,
|
|
1964
|
+
RELATION_UPDATE_TYPES,
|
|
1965
|
+
relationList,
|
|
1966
|
+
relationAdd,
|
|
1967
|
+
relationUpdate,
|
|
1968
|
+
relationRm,
|
|
1830
1969
|
comment,
|
|
1970
|
+
editComment,
|
|
1971
|
+
deleteComment,
|
|
1972
|
+
requireCommentId,
|
|
1831
1973
|
attachImage,
|
|
1832
1974
|
projectList,
|
|
1833
1975
|
projectShow,
|
|
@@ -1841,6 +1983,7 @@ export {
|
|
|
1841
1983
|
docUpdate,
|
|
1842
1984
|
linkList,
|
|
1843
1985
|
linkAdd,
|
|
1986
|
+
linkUpdate,
|
|
1844
1987
|
linkRm,
|
|
1845
1988
|
labelList,
|
|
1846
1989
|
labelCreate,
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
DEFAULT_BASE_URL,
|
|
7
7
|
HttpError,
|
|
8
8
|
LocalError,
|
|
9
|
+
RELATION_CREATE_TYPES,
|
|
10
|
+
RELATION_UPDATE_TYPES,
|
|
9
11
|
TransportError,
|
|
10
12
|
UpgradeRequiredError,
|
|
11
13
|
UsageError,
|
|
@@ -18,12 +20,14 @@ import {
|
|
|
18
20
|
comments,
|
|
19
21
|
create,
|
|
20
22
|
defaultCredentialsPath,
|
|
23
|
+
deleteComment,
|
|
21
24
|
deleteCredentials,
|
|
22
25
|
dep,
|
|
23
26
|
docCreate,
|
|
24
27
|
docList,
|
|
25
28
|
docShow,
|
|
26
29
|
docUpdate,
|
|
30
|
+
editComment,
|
|
27
31
|
findRepoConfig,
|
|
28
32
|
isHostTrusted,
|
|
29
33
|
json,
|
|
@@ -34,6 +38,7 @@ import {
|
|
|
34
38
|
linkAdd,
|
|
35
39
|
linkList,
|
|
36
40
|
linkRm,
|
|
41
|
+
linkUpdate,
|
|
37
42
|
list,
|
|
38
43
|
loginHelp,
|
|
39
44
|
memberAdd,
|
|
@@ -48,6 +53,11 @@ import {
|
|
|
48
53
|
projectUpdate,
|
|
49
54
|
readCredentials,
|
|
50
55
|
ready,
|
|
56
|
+
relationAdd,
|
|
57
|
+
relationList,
|
|
58
|
+
relationRm,
|
|
59
|
+
relationUpdate,
|
|
60
|
+
requireCommentId,
|
|
51
61
|
requireIdentifiers,
|
|
52
62
|
resolveCacheHome,
|
|
53
63
|
resolveConfig,
|
|
@@ -70,7 +80,7 @@ import {
|
|
|
70
80
|
version,
|
|
71
81
|
workflowStates,
|
|
72
82
|
writeCredentials
|
|
73
|
-
} from "./chunk-
|
|
83
|
+
} from "./chunk-NQVQNKMC.js";
|
|
74
84
|
|
|
75
85
|
// src/cli.ts
|
|
76
86
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -1000,6 +1010,23 @@ function integer(value) {
|
|
|
1000
1010
|
function collect(value, previous) {
|
|
1001
1011
|
return [...previous, value];
|
|
1002
1012
|
}
|
|
1013
|
+
function resolvedCommentBody(text, bodyFile, action = "post") {
|
|
1014
|
+
if (bodyFile !== void 0 && text.length > 0) {
|
|
1015
|
+
throw new UsageError(
|
|
1016
|
+
"comment text and --body-file both given \u2014 pass one; there is no precedence between them"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const fromFile = resolveTextSources([
|
|
1020
|
+
{ name: "body", inline: void 0, file: bodyFile }
|
|
1021
|
+
]).body;
|
|
1022
|
+
const body = fromFile === void 0 ? text : [fromFile];
|
|
1023
|
+
if (body.join(" ").trim() === "") {
|
|
1024
|
+
throw new UsageError(
|
|
1025
|
+
bodyFile === void 0 ? `the comment text is empty \u2014 nothing to ${action}` : `--body-file ${bodyFile} produced an empty comment \u2014 nothing to ${action}`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return body;
|
|
1029
|
+
}
|
|
1003
1030
|
var requireIdentifiers2 = requireIdentifiers;
|
|
1004
1031
|
function interactive() {
|
|
1005
1032
|
const ci = process.env["CI"];
|
|
@@ -1132,6 +1159,9 @@ program.command("blocked").description("issues waiting on a blocker").option("--
|
|
|
1132
1159
|
program.command("list").description("list issues with filters").option("--status <status>", "open | in_progress | closed").option(
|
|
1133
1160
|
"--assignee <who>",
|
|
1134
1161
|
'me, a user id, an email, or a full name; "" for unassigned'
|
|
1162
|
+
).option("--created-by <who>", "me or a creator user id").option(
|
|
1163
|
+
"--involved <who>",
|
|
1164
|
+
"me for issues created by, assigned to, or mentioning you"
|
|
1135
1165
|
).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1136
1166
|
await withConfig(
|
|
1137
1167
|
opts.json === true,
|
|
@@ -1313,7 +1343,42 @@ program.command("dep").description("blocking dependencies: dep add|rm <issue> <b
|
|
|
1313
1343
|
});
|
|
1314
1344
|
}
|
|
1315
1345
|
);
|
|
1316
|
-
program.command("
|
|
1346
|
+
var relation = program.command("relation").description("typed issue relations; dep remains the blocking-dependency shorthand");
|
|
1347
|
+
relation.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
|
|
1348
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
1349
|
+
if (issue === void 0) throw new UsageError("usage: harness relation list <issue>");
|
|
1350
|
+
return relationList(ctx, issue);
|
|
1351
|
+
});
|
|
1352
|
+
});
|
|
1353
|
+
relation.command("add").argument("[issue]").argument("[related-issue]").requiredOption("--type <type>", `relation type: ${RELATION_CREATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
|
|
1354
|
+
async (issue, relatedIssue, opts) => {
|
|
1355
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1356
|
+
if (issue === void 0 || relatedIssue === void 0) {
|
|
1357
|
+
throw new UsageError("usage: harness relation add <issue> <related-issue> --type <type>");
|
|
1358
|
+
}
|
|
1359
|
+
return relationAdd(ctx, issue, relatedIssue, opts.type);
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
);
|
|
1363
|
+
relation.command("update").argument("[issue]").argument("[id-or-issue]").requiredOption("--type <type>", `relation type: ${RELATION_UPDATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
|
|
1364
|
+
async (issue, idOrIssue, opts) => {
|
|
1365
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1366
|
+
if (issue === void 0 || idOrIssue === void 0) {
|
|
1367
|
+
throw new UsageError("usage: harness relation update <issue> <relation-id-or-issue> --type <type>");
|
|
1368
|
+
}
|
|
1369
|
+
return relationUpdate(ctx, issue, idOrIssue, opts.type);
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
);
|
|
1373
|
+
relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--json", "emit JSON").action(async (issue, idOrIssue, opts) => {
|
|
1374
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1375
|
+
if (issue === void 0 || idOrIssue === void 0) {
|
|
1376
|
+
throw new UsageError("usage: harness relation rm <issue> <relation-id-or-issue>");
|
|
1377
|
+
}
|
|
1378
|
+
return relationRm(ctx, issue, idOrIssue);
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
var commentCommand = program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--body-file <path>", 'read the comment body from a file ("-" for stdin)').option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(
|
|
1317
1382
|
async (id, text, opts) => {
|
|
1318
1383
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1319
1384
|
if (id === void 0 || opts.bodyFile === void 0 && text.length === 0) {
|
|
@@ -1322,25 +1387,39 @@ program.command("comment").description("comment on an issue").argument("[id]").a
|
|
|
1322
1387
|
);
|
|
1323
1388
|
}
|
|
1324
1389
|
requireIdentifiers2([id]);
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1390
|
+
const body = resolvedCommentBody(text, opts.bodyFile);
|
|
1391
|
+
const mentions = opts.mention.filter((name) => name.trim() !== "");
|
|
1392
|
+
return comment(ctx, id, body, mentions);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
1396
|
+
commentCommand.command("edit").description("edit one of your issue comments").argument("[issue]").argument("[comment-id]").argument("[text...]").option("--body-file <path>", 'read the replacement body from a file ("-" for stdin)').option("--json", "emit JSON").action(
|
|
1397
|
+
async (issue, commentId, text, opts) => {
|
|
1398
|
+
const parentOpts = commentCommand.opts();
|
|
1399
|
+
const bodyFile = opts.bodyFile ?? parentOpts.bodyFile;
|
|
1400
|
+
await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
|
|
1401
|
+
if (issue === void 0 || commentId === void 0 || bodyFile === void 0 && text.length === 0) {
|
|
1335
1402
|
throw new UsageError(
|
|
1336
|
-
|
|
1403
|
+
"usage: harness comment edit <issue> <comment-id> <text> | harness comment edit <issue> <comment-id> --body-file <path>"
|
|
1337
1404
|
);
|
|
1338
1405
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1406
|
+
requireIdentifiers2([issue]);
|
|
1407
|
+
requireCommentId(commentId);
|
|
1408
|
+
return editComment(ctx, issue, commentId, resolvedCommentBody(text, bodyFile, "edit"));
|
|
1341
1409
|
});
|
|
1342
1410
|
}
|
|
1343
1411
|
);
|
|
1412
|
+
commentCommand.command("delete").alias("rm").description("delete one of your issue comments").argument("[issue]").argument("[comment-id]").option("--json", "emit JSON").action(async (issue, commentId, opts) => {
|
|
1413
|
+
const parentOpts = commentCommand.opts();
|
|
1414
|
+
await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
|
|
1415
|
+
if (issue === void 0 || commentId === void 0) {
|
|
1416
|
+
throw new UsageError("usage: harness comment delete <issue> <comment-id>");
|
|
1417
|
+
}
|
|
1418
|
+
requireIdentifiers2([issue]);
|
|
1419
|
+
requireCommentId(commentId);
|
|
1420
|
+
return deleteComment(ctx, issue, commentId);
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1344
1423
|
program.command("attach-image").description("upload an image; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
|
|
1345
1424
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1346
1425
|
if (path === void 0) {
|
|
@@ -1454,6 +1533,18 @@ link.command("add").argument("[issue]").argument("[url]").option("--title <title
|
|
|
1454
1533
|
});
|
|
1455
1534
|
}
|
|
1456
1535
|
);
|
|
1536
|
+
link.command("update").argument("[issue]").argument("[id-or-url]").option("--url <url>", "replacement URL").option("--title <title>", "replacement title; pass an empty value to clear").option("--json", "emit JSON").action(
|
|
1537
|
+
async (issue, idOrUrl, opts) => {
|
|
1538
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1539
|
+
if (issue === void 0 || idOrUrl === void 0) {
|
|
1540
|
+
throw new UsageError(
|
|
1541
|
+
"usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>"
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
return linkUpdate(ctx, issue, idOrUrl, opts);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1457
1548
|
link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
|
|
1458
1549
|
async (issue, idOrUrl, opts) => {
|
|
1459
1550
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -1567,7 +1658,7 @@ program.command("upload").description("upload a file; prints markdown to referen
|
|
|
1567
1658
|
});
|
|
1568
1659
|
});
|
|
1569
1660
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
1570
|
-
const { startMcpServer } = await import("./server-
|
|
1661
|
+
const { startMcpServer } = await import("./server-FWNT3HMX.js");
|
|
1571
1662
|
await startMcpServer();
|
|
1572
1663
|
});
|
|
1573
1664
|
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(
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CLI_VERSION,
|
|
4
4
|
Client,
|
|
5
5
|
HttpError,
|
|
6
|
+
RELATION_CREATE_TYPES,
|
|
7
|
+
RELATION_UPDATE_TYPES,
|
|
6
8
|
TransportError,
|
|
7
9
|
UsageError,
|
|
8
10
|
archive,
|
|
@@ -11,11 +13,13 @@ import {
|
|
|
11
13
|
comment,
|
|
12
14
|
comments,
|
|
13
15
|
create,
|
|
16
|
+
deleteComment,
|
|
14
17
|
dep,
|
|
15
18
|
docCreate,
|
|
16
19
|
docList,
|
|
17
20
|
docShow,
|
|
18
21
|
docUpdate,
|
|
22
|
+
editComment,
|
|
19
23
|
labelCreate,
|
|
20
24
|
labelDelete,
|
|
21
25
|
labelList,
|
|
@@ -23,6 +27,7 @@ import {
|
|
|
23
27
|
linkAdd,
|
|
24
28
|
linkList,
|
|
25
29
|
linkRm,
|
|
30
|
+
linkUpdate,
|
|
26
31
|
list,
|
|
27
32
|
memberAdd,
|
|
28
33
|
memberList,
|
|
@@ -34,6 +39,10 @@ import {
|
|
|
34
39
|
projectShow,
|
|
35
40
|
projectUpdate,
|
|
36
41
|
ready,
|
|
42
|
+
relationAdd,
|
|
43
|
+
relationList,
|
|
44
|
+
relationRm,
|
|
45
|
+
relationUpdate,
|
|
37
46
|
resolveConfig,
|
|
38
47
|
search,
|
|
39
48
|
show,
|
|
@@ -50,7 +59,7 @@ import {
|
|
|
50
59
|
upload,
|
|
51
60
|
userList,
|
|
52
61
|
workflowStates
|
|
53
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-NQVQNKMC.js";
|
|
54
63
|
|
|
55
64
|
// src/mcp/server.ts
|
|
56
65
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -98,6 +107,7 @@ var page = {
|
|
|
98
107
|
all: z.boolean().optional().describe("follow every page")
|
|
99
108
|
};
|
|
100
109
|
var identifier = z.string().describe("issue identifier, e.g. IO-123");
|
|
110
|
+
var commentId = z.string().uuid().describe("comment UUID from issue_show or comment_list");
|
|
101
111
|
var dateField = z.string().optional().describe('YYYY-MM-DD; "" clears the date');
|
|
102
112
|
var TOOLS = [
|
|
103
113
|
// ── Issues ──
|
|
@@ -111,6 +121,8 @@ var TOOLS = [
|
|
|
111
121
|
blocked: z.boolean().optional(),
|
|
112
122
|
status: z.enum(["open", "in_progress", "closed"]).optional(),
|
|
113
123
|
assignee: z.string().optional().describe('me, a user id, an email, or a full name; "" for unassigned'),
|
|
124
|
+
created_by: z.string().optional().describe("me or a creator user id"),
|
|
125
|
+
involved: z.literal("me").optional().describe("issues created by, assigned to, or mentioning the caller"),
|
|
114
126
|
priority: z.enum(["urgent", "high", "medium", "low", "none"]).optional(),
|
|
115
127
|
label: z.string().optional(),
|
|
116
128
|
type: z.enum(["task", "bug", "feature", "epic"]).optional(),
|
|
@@ -123,9 +135,10 @@ var TOOLS = [
|
|
|
123
135
|
if (args["ready"] === true && args["blocked"] === true) {
|
|
124
136
|
throw new UsageError("ready and blocked are mutually exclusive \u2014 pass one, not both");
|
|
125
137
|
}
|
|
126
|
-
|
|
127
|
-
if (args["
|
|
128
|
-
return
|
|
138
|
+
const opts = { ...args, createdBy: args["created_by"] };
|
|
139
|
+
if (args["ready"] === true) return ready(ctx, opts);
|
|
140
|
+
if (args["blocked"] === true) return blocked(ctx, opts);
|
|
141
|
+
return list(ctx, opts);
|
|
129
142
|
}
|
|
130
143
|
},
|
|
131
144
|
{
|
|
@@ -231,6 +244,27 @@ var TOOLS = [
|
|
|
231
244
|
},
|
|
232
245
|
handler: (ctx, args) => comment(ctx, args["id"], [args["body"]], args["mentions"] ?? [])
|
|
233
246
|
},
|
|
247
|
+
{
|
|
248
|
+
name: "issue_comment_edit",
|
|
249
|
+
description: "Edit one of your issue comments within its edit window.",
|
|
250
|
+
readOnly: false,
|
|
251
|
+
shape: {
|
|
252
|
+
id: identifier,
|
|
253
|
+
comment_id: commentId,
|
|
254
|
+
body: z.string().min(1).refine((body) => body.trim() !== "", "comment body must not be blank")
|
|
255
|
+
},
|
|
256
|
+
handler: (ctx, args) => editComment(ctx, args["id"], args["comment_id"], [args["body"]])
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "issue_comment_delete",
|
|
260
|
+
description: "Delete one of your issue comments.",
|
|
261
|
+
readOnly: false,
|
|
262
|
+
shape: {
|
|
263
|
+
id: identifier,
|
|
264
|
+
comment_id: commentId
|
|
265
|
+
},
|
|
266
|
+
handler: (ctx, args) => deleteComment(ctx, args["id"], args["comment_id"])
|
|
267
|
+
},
|
|
234
268
|
{
|
|
235
269
|
name: "issue_archive",
|
|
236
270
|
description: "Archive issues (or restore them with restore=true). Archived issues show up under scope=archived.",
|
|
@@ -245,7 +279,7 @@ var TOOLS = [
|
|
|
245
279
|
shape: { id: identifier },
|
|
246
280
|
handler: (ctx, args) => comments(ctx, args["id"])
|
|
247
281
|
},
|
|
248
|
-
// ── Relations
|
|
282
|
+
// ── Relations ──
|
|
249
283
|
{
|
|
250
284
|
name: "relation_add",
|
|
251
285
|
description: "Add a blocking dependency: blocker blocks issue.",
|
|
@@ -260,6 +294,42 @@ var TOOLS = [
|
|
|
260
294
|
shape: { issue: identifier, blocker: identifier },
|
|
261
295
|
handler: (ctx, args) => dep(ctx, "rm", args["issue"], args["blocker"])
|
|
262
296
|
},
|
|
297
|
+
{
|
|
298
|
+
name: "relation_list",
|
|
299
|
+
description: "List every typed relation on an issue, including stable relation ids.",
|
|
300
|
+
readOnly: true,
|
|
301
|
+
shape: { issue: identifier },
|
|
302
|
+
handler: (ctx, args) => relationList(ctx, args["issue"])
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "relation_create",
|
|
306
|
+
description: "Create a typed relation between two issues.",
|
|
307
|
+
readOnly: false,
|
|
308
|
+
shape: {
|
|
309
|
+
issue: identifier,
|
|
310
|
+
related_issue: identifier,
|
|
311
|
+
type: z.enum(RELATION_CREATE_TYPES)
|
|
312
|
+
},
|
|
313
|
+
handler: (ctx, args) => relationAdd(ctx, args["issue"], args["related_issue"], args["type"])
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "relation_update",
|
|
317
|
+
description: "Change a relation's direction, resolving it by relation id or unique related issue.",
|
|
318
|
+
readOnly: false,
|
|
319
|
+
shape: {
|
|
320
|
+
issue: identifier,
|
|
321
|
+
id_or_issue: z.string(),
|
|
322
|
+
type: z.enum(RELATION_UPDATE_TYPES)
|
|
323
|
+
},
|
|
324
|
+
handler: (ctx, args) => relationUpdate(ctx, args["issue"], args["id_or_issue"], args["type"])
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "relation_delete",
|
|
328
|
+
description: "Remove a typed relation by relation id or unique related issue.",
|
|
329
|
+
readOnly: false,
|
|
330
|
+
shape: { issue: identifier, id_or_issue: z.string() },
|
|
331
|
+
handler: (ctx, args) => relationRm(ctx, args["issue"], args["id_or_issue"])
|
|
332
|
+
},
|
|
263
333
|
// ── Links (URLs on an issue) ──
|
|
264
334
|
{
|
|
265
335
|
name: "link_list",
|
|
@@ -275,6 +345,20 @@ var TOOLS = [
|
|
|
275
345
|
shape: { issue: identifier, url: z.string(), title: z.string().optional() },
|
|
276
346
|
handler: (ctx, args) => linkAdd(ctx, args["issue"], args["url"], args["title"])
|
|
277
347
|
},
|
|
348
|
+
{
|
|
349
|
+
name: "link_update",
|
|
350
|
+
description: "Edit a link by link id or exact URL.",
|
|
351
|
+
readOnly: false,
|
|
352
|
+
shape: {
|
|
353
|
+
issue: identifier,
|
|
354
|
+
id_or_url: z.string(),
|
|
355
|
+
change: z.union([
|
|
356
|
+
z.object({ url: z.string(), title: z.string().optional() }),
|
|
357
|
+
z.object({ title: z.string(), url: z.string().optional() })
|
|
358
|
+
])
|
|
359
|
+
},
|
|
360
|
+
handler: (ctx, args) => linkUpdate(ctx, args["issue"], args["id_or_url"], args["change"])
|
|
361
|
+
},
|
|
278
362
|
{
|
|
279
363
|
name: "link_rm",
|
|
280
364
|
description: "Remove a link from an issue, by link id or exact URL.",
|
package/package.json
CHANGED
package/spec/openapi.json
CHANGED
|
@@ -288,6 +288,7 @@
|
|
|
288
288
|
"type": "string"
|
|
289
289
|
},
|
|
290
290
|
"name": {
|
|
291
|
+
"description": "Unique within its workspace or team after trimming Unicode boundary whitespace and Unicode-aware lowercasing. Interior whitespace and Unicode normalization forms remain distinct.",
|
|
291
292
|
"maxLength": 255,
|
|
292
293
|
"type": "string"
|
|
293
294
|
},
|
|
@@ -382,7 +383,9 @@
|
|
|
382
383
|
"schema": {
|
|
383
384
|
"properties": {
|
|
384
385
|
"label_ids": {
|
|
386
|
+
"description": "The complete replacement set. Every label must belong to this workspace and be workspace-scoped or owned by the issue's team; the update is rejected atomically otherwise.",
|
|
385
387
|
"items": {
|
|
388
|
+
"format": "uuid",
|
|
386
389
|
"type": "string"
|
|
387
390
|
},
|
|
388
391
|
"type": "array"
|
|
@@ -564,6 +567,15 @@
|
|
|
564
567
|
"type": "string"
|
|
565
568
|
}
|
|
566
569
|
},
|
|
570
|
+
{
|
|
571
|
+
"description": "\"me\" keeps issues created by, assigned to, or mentioning the token owner",
|
|
572
|
+
"in": "query",
|
|
573
|
+
"name": "involved",
|
|
574
|
+
"required": false,
|
|
575
|
+
"schema": {
|
|
576
|
+
"type": "string"
|
|
577
|
+
}
|
|
578
|
+
},
|
|
567
579
|
{
|
|
568
580
|
"description": "Filter by priority (0-4)",
|
|
569
581
|
"in": "query",
|
|
@@ -1361,7 +1373,7 @@
|
|
|
1361
1373
|
"get": {
|
|
1362
1374
|
"callbacks": {},
|
|
1363
1375
|
"deprecated": false,
|
|
1364
|
-
"description": "Requires the `harness:read` scope AND Harness administrator standing in the organization the credential is bound to — this is a settings write, gated exactly as the settings UI is. Everyone who can reach this organization's tracker: its harness grant holders plus its own admins, who get staff standing without a grant. Each entry carries both `role` (the grant rank, null when none) and `standing` — they differ, and the difference is the point: an org admin holds no rank and is still staff. Distinct from GET /users, which is the deployment-wide assignee picker and never lists a collaborator.",
|
|
1376
|
+
"description": "Requires the `harness:read` scope AND Harness administrator standing in the organization the credential is bound to — this is a settings write, gated exactly as the settings UI is. Everyone who can reach this organization's tracker: its harness grant holders plus its own admins, who get staff standing without a grant. Each entry carries both `role` (the grant rank, null when none) and `standing` — they differ, and the difference is the point: an org admin holds no rank and is still staff. Pending invitations appear with `standing: \"invited\"` and a null user id. Distinct from GET /users, which is the deployment-wide assignee picker and never lists a collaborator.",
|
|
1365
1377
|
"parameters": [],
|
|
1366
1378
|
"responses": {
|
|
1367
1379
|
"200": {
|
|
@@ -1389,7 +1401,7 @@
|
|
|
1389
1401
|
"post": {
|
|
1390
1402
|
"callbacks": {},
|
|
1391
1403
|
"deprecated": false,
|
|
1392
|
-
"description": "Requires the `harness:write` scope AND Harness administrator standing in the organization the credential is bound to — this is a settings write, gated exactly as the settings UI is. Mints the organization membership when the person has none, so joining the tracker does not detour through the organization's own people page.
|
|
1404
|
+
"description": "Requires the `harness:write` scope AND Harness administrator standing in the organization the credential is bound to — this is a settings write, gated exactly as the settings UI is. Mints the organization membership when the person has none, so joining the tracker does not detour through the organization's own people page. An address with no account is INVITED instead of refused (IO-10955): the response is the same member shape with `standing: \"invited\"`, and the rank applies when the invitation is accepted. Email matching is case-insensitive.",
|
|
1393
1405
|
"parameters": [],
|
|
1394
1406
|
"requestBody": {
|
|
1395
1407
|
"content": {
|
|
@@ -2158,6 +2170,7 @@
|
|
|
2158
2170
|
"type": "string"
|
|
2159
2171
|
},
|
|
2160
2172
|
"name": {
|
|
2173
|
+
"description": "Unique within its workspace or team after trimming Unicode boundary whitespace and Unicode-aware lowercasing. Interior whitespace and Unicode normalization forms remain distinct.",
|
|
2161
2174
|
"maxLength": 255,
|
|
2162
2175
|
"type": "string"
|
|
2163
2176
|
}
|
|
@@ -2281,6 +2294,302 @@
|
|
|
2281
2294
|
"tags": []
|
|
2282
2295
|
}
|
|
2283
2296
|
},
|
|
2297
|
+
"/api/v1/issues/{identifier}/comments/{comment_id}": {
|
|
2298
|
+
"delete": {
|
|
2299
|
+
"callbacks": {},
|
|
2300
|
+
"deprecated": false,
|
|
2301
|
+
"description": "Requires the `harness:write` scope. Only the underlying authenticated author may soft-delete a comment, and must also retain issue-comment capability, matching the UI. Missing, deleted, cross-issue, cross-organization, and other authors' comments all answer 404.",
|
|
2302
|
+
"parameters": [
|
|
2303
|
+
{
|
|
2304
|
+
"description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
|
|
2305
|
+
"in": "path",
|
|
2306
|
+
"name": "identifier",
|
|
2307
|
+
"required": true,
|
|
2308
|
+
"schema": {
|
|
2309
|
+
"type": "string"
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
"description": "Comment UUID",
|
|
2314
|
+
"in": "path",
|
|
2315
|
+
"name": "comment_id",
|
|
2316
|
+
"required": true,
|
|
2317
|
+
"schema": {
|
|
2318
|
+
"format": "uuid",
|
|
2319
|
+
"type": "string"
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
],
|
|
2323
|
+
"responses": {
|
|
2324
|
+
"200": {
|
|
2325
|
+
"content": {
|
|
2326
|
+
"application/json": {
|
|
2327
|
+
"schema": {
|
|
2328
|
+
"properties": {
|
|
2329
|
+
"data": {
|
|
2330
|
+
"properties": {
|
|
2331
|
+
"deleted": {
|
|
2332
|
+
"type": "boolean"
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
"required": [
|
|
2336
|
+
"deleted"
|
|
2337
|
+
],
|
|
2338
|
+
"type": "object"
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2341
|
+
"required": [
|
|
2342
|
+
"data"
|
|
2343
|
+
],
|
|
2344
|
+
"type": "object"
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
},
|
|
2348
|
+
"description": "Success"
|
|
2349
|
+
},
|
|
2350
|
+
"400": {
|
|
2351
|
+
"description": "Bad request"
|
|
2352
|
+
},
|
|
2353
|
+
"401": {
|
|
2354
|
+
"description": "Missing or invalid token"
|
|
2355
|
+
},
|
|
2356
|
+
"403": {
|
|
2357
|
+
"description": "Token not permitted (org-scoped, non-admin, or missing scope)"
|
|
2358
|
+
},
|
|
2359
|
+
"404": {
|
|
2360
|
+
"description": "Not found"
|
|
2361
|
+
},
|
|
2362
|
+
"422": {
|
|
2363
|
+
"description": "Validation error"
|
|
2364
|
+
}
|
|
2365
|
+
},
|
|
2366
|
+
"summary": "Delete an issue comment",
|
|
2367
|
+
"tags": []
|
|
2368
|
+
},
|
|
2369
|
+
"patch": {
|
|
2370
|
+
"callbacks": {},
|
|
2371
|
+
"deprecated": false,
|
|
2372
|
+
"description": "Requires the `harness:write` scope. Only the underlying authenticated author may edit a comment, within 15 minutes of creation. The author must also retain issue-comment capability, matching the UI. The body changes; attribution, metadata, reply target, mentions, and creation time do not. `actor` is accepted for CLI compatibility but ignored for ownership and attribution.",
|
|
2373
|
+
"parameters": [
|
|
2374
|
+
{
|
|
2375
|
+
"description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
|
|
2376
|
+
"in": "path",
|
|
2377
|
+
"name": "identifier",
|
|
2378
|
+
"required": true,
|
|
2379
|
+
"schema": {
|
|
2380
|
+
"type": "string"
|
|
2381
|
+
}
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
"description": "Comment UUID",
|
|
2385
|
+
"in": "path",
|
|
2386
|
+
"name": "comment_id",
|
|
2387
|
+
"required": true,
|
|
2388
|
+
"schema": {
|
|
2389
|
+
"format": "uuid",
|
|
2390
|
+
"type": "string"
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
],
|
|
2394
|
+
"requestBody": {
|
|
2395
|
+
"content": {
|
|
2396
|
+
"application/json": {
|
|
2397
|
+
"schema": {
|
|
2398
|
+
"anyOf": [
|
|
2399
|
+
{
|
|
2400
|
+
"properties": {
|
|
2401
|
+
"actor": {
|
|
2402
|
+
"description": "Accepted for CLI compatibility; ignored because editing never changes attribution",
|
|
2403
|
+
"type": "string"
|
|
2404
|
+
},
|
|
2405
|
+
"body": {
|
|
2406
|
+
"description": "Replacement comment body; takes precedence when content is also present",
|
|
2407
|
+
"minLength": 1,
|
|
2408
|
+
"type": "string"
|
|
2409
|
+
},
|
|
2410
|
+
"content": {
|
|
2411
|
+
"description": "Alias for body",
|
|
2412
|
+
"minLength": 1,
|
|
2413
|
+
"type": "string"
|
|
2414
|
+
}
|
|
2415
|
+
},
|
|
2416
|
+
"required": [
|
|
2417
|
+
"body"
|
|
2418
|
+
],
|
|
2419
|
+
"type": "object"
|
|
2420
|
+
},
|
|
2421
|
+
{
|
|
2422
|
+
"properties": {
|
|
2423
|
+
"actor": {
|
|
2424
|
+
"description": "Accepted for CLI compatibility; ignored because editing never changes attribution",
|
|
2425
|
+
"type": "string"
|
|
2426
|
+
},
|
|
2427
|
+
"body": {
|
|
2428
|
+
"description": "Replacement comment body; takes precedence when content is also present",
|
|
2429
|
+
"minLength": 1,
|
|
2430
|
+
"type": "string"
|
|
2431
|
+
},
|
|
2432
|
+
"content": {
|
|
2433
|
+
"description": "Alias for body",
|
|
2434
|
+
"minLength": 1,
|
|
2435
|
+
"type": "string"
|
|
2436
|
+
}
|
|
2437
|
+
},
|
|
2438
|
+
"required": [
|
|
2439
|
+
"content"
|
|
2440
|
+
],
|
|
2441
|
+
"type": "object"
|
|
2442
|
+
}
|
|
2443
|
+
]
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
2447
|
+
"required": true
|
|
2448
|
+
},
|
|
2449
|
+
"responses": {
|
|
2450
|
+
"200": {
|
|
2451
|
+
"content": {
|
|
2452
|
+
"application/json": {
|
|
2453
|
+
"schema": {
|
|
2454
|
+
"properties": {
|
|
2455
|
+
"data": {
|
|
2456
|
+
"properties": {
|
|
2457
|
+
"author": {
|
|
2458
|
+
"nullable": true,
|
|
2459
|
+
"properties": {
|
|
2460
|
+
"email": {
|
|
2461
|
+
"nullable": true,
|
|
2462
|
+
"type": "string"
|
|
2463
|
+
},
|
|
2464
|
+
"id": {
|
|
2465
|
+
"format": "uuid",
|
|
2466
|
+
"nullable": true,
|
|
2467
|
+
"type": "string"
|
|
2468
|
+
},
|
|
2469
|
+
"name": {
|
|
2470
|
+
"nullable": true,
|
|
2471
|
+
"type": "string"
|
|
2472
|
+
}
|
|
2473
|
+
},
|
|
2474
|
+
"required": [
|
|
2475
|
+
"id",
|
|
2476
|
+
"name",
|
|
2477
|
+
"email"
|
|
2478
|
+
],
|
|
2479
|
+
"type": "object"
|
|
2480
|
+
},
|
|
2481
|
+
"body": {
|
|
2482
|
+
"type": "string"
|
|
2483
|
+
},
|
|
2484
|
+
"deleted": {
|
|
2485
|
+
"type": "boolean"
|
|
2486
|
+
},
|
|
2487
|
+
"id": {
|
|
2488
|
+
"format": "uuid",
|
|
2489
|
+
"type": "string"
|
|
2490
|
+
},
|
|
2491
|
+
"inserted_at": {
|
|
2492
|
+
"format": "date-time",
|
|
2493
|
+
"type": "string"
|
|
2494
|
+
},
|
|
2495
|
+
"mentions": {
|
|
2496
|
+
"items": {
|
|
2497
|
+
"properties": {
|
|
2498
|
+
"email": {
|
|
2499
|
+
"nullable": true,
|
|
2500
|
+
"type": "string"
|
|
2501
|
+
},
|
|
2502
|
+
"name": {
|
|
2503
|
+
"nullable": true,
|
|
2504
|
+
"type": "string"
|
|
2505
|
+
},
|
|
2506
|
+
"user_id": {
|
|
2507
|
+
"format": "uuid",
|
|
2508
|
+
"nullable": true,
|
|
2509
|
+
"type": "string"
|
|
2510
|
+
}
|
|
2511
|
+
},
|
|
2512
|
+
"required": [
|
|
2513
|
+
"user_id",
|
|
2514
|
+
"name",
|
|
2515
|
+
"email"
|
|
2516
|
+
],
|
|
2517
|
+
"type": "object"
|
|
2518
|
+
},
|
|
2519
|
+
"type": "array"
|
|
2520
|
+
},
|
|
2521
|
+
"reactions": {
|
|
2522
|
+
"items": {
|
|
2523
|
+
"properties": {
|
|
2524
|
+
"count": {
|
|
2525
|
+
"type": "integer"
|
|
2526
|
+
},
|
|
2527
|
+
"emoji": {
|
|
2528
|
+
"type": "string"
|
|
2529
|
+
},
|
|
2530
|
+
"reacted_by_current_actor": {
|
|
2531
|
+
"type": "boolean"
|
|
2532
|
+
}
|
|
2533
|
+
},
|
|
2534
|
+
"required": [
|
|
2535
|
+
"emoji",
|
|
2536
|
+
"count",
|
|
2537
|
+
"reacted_by_current_actor"
|
|
2538
|
+
],
|
|
2539
|
+
"type": "object"
|
|
2540
|
+
},
|
|
2541
|
+
"type": "array"
|
|
2542
|
+
},
|
|
2543
|
+
"updated_at": {
|
|
2544
|
+
"format": "date-time",
|
|
2545
|
+
"type": "string"
|
|
2546
|
+
}
|
|
2547
|
+
},
|
|
2548
|
+
"required": [
|
|
2549
|
+
"id",
|
|
2550
|
+
"body",
|
|
2551
|
+
"deleted",
|
|
2552
|
+
"author",
|
|
2553
|
+
"mentions",
|
|
2554
|
+
"reactions",
|
|
2555
|
+
"inserted_at",
|
|
2556
|
+
"updated_at"
|
|
2557
|
+
],
|
|
2558
|
+
"type": "object"
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2561
|
+
"required": [
|
|
2562
|
+
"data"
|
|
2563
|
+
],
|
|
2564
|
+
"type": "object"
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
},
|
|
2568
|
+
"description": "Success"
|
|
2569
|
+
},
|
|
2570
|
+
"400": {
|
|
2571
|
+
"description": "Bad request"
|
|
2572
|
+
},
|
|
2573
|
+
"401": {
|
|
2574
|
+
"description": "Missing or invalid token"
|
|
2575
|
+
},
|
|
2576
|
+
"403": {
|
|
2577
|
+
"description": "Token not permitted (org-scoped, non-admin, or missing scope)"
|
|
2578
|
+
},
|
|
2579
|
+
"404": {
|
|
2580
|
+
"description": "Not found"
|
|
2581
|
+
},
|
|
2582
|
+
"409": {
|
|
2583
|
+
"description": "The 15-minute edit window has closed"
|
|
2584
|
+
},
|
|
2585
|
+
"422": {
|
|
2586
|
+
"description": "Validation error"
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
"summary": "Edit an issue comment",
|
|
2590
|
+
"tags": []
|
|
2591
|
+
}
|
|
2592
|
+
},
|
|
2284
2593
|
"/api/v1/model-keys": {
|
|
2285
2594
|
"get": {
|
|
2286
2595
|
"callbacks": {},
|
|
@@ -2510,6 +2819,144 @@
|
|
|
2510
2819
|
},
|
|
2511
2820
|
"summary": "Delete a relation",
|
|
2512
2821
|
"tags": []
|
|
2822
|
+
},
|
|
2823
|
+
"patch": {
|
|
2824
|
+
"callbacks": {},
|
|
2825
|
+
"deprecated": false,
|
|
2826
|
+
"description": "Requires the `harness:write` scope.",
|
|
2827
|
+
"parameters": [
|
|
2828
|
+
{
|
|
2829
|
+
"description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
|
|
2830
|
+
"in": "path",
|
|
2831
|
+
"name": "identifier",
|
|
2832
|
+
"required": true,
|
|
2833
|
+
"schema": {
|
|
2834
|
+
"type": "string"
|
|
2835
|
+
}
|
|
2836
|
+
},
|
|
2837
|
+
{
|
|
2838
|
+
"description": "Resource id",
|
|
2839
|
+
"in": "path",
|
|
2840
|
+
"name": "id",
|
|
2841
|
+
"required": true,
|
|
2842
|
+
"schema": {
|
|
2843
|
+
"type": "string"
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
],
|
|
2847
|
+
"requestBody": {
|
|
2848
|
+
"content": {
|
|
2849
|
+
"application/json": {
|
|
2850
|
+
"schema": {
|
|
2851
|
+
"properties": {
|
|
2852
|
+
"type": {
|
|
2853
|
+
"description": "Direction from the issue in the request path",
|
|
2854
|
+
"enum": [
|
|
2855
|
+
"blocks",
|
|
2856
|
+
"blocked_by",
|
|
2857
|
+
"related",
|
|
2858
|
+
"duplicate_of"
|
|
2859
|
+
],
|
|
2860
|
+
"type": "string"
|
|
2861
|
+
}
|
|
2862
|
+
},
|
|
2863
|
+
"required": [
|
|
2864
|
+
"type"
|
|
2865
|
+
],
|
|
2866
|
+
"type": "object"
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
},
|
|
2870
|
+
"required": true
|
|
2871
|
+
},
|
|
2872
|
+
"responses": {
|
|
2873
|
+
"200": {
|
|
2874
|
+
"content": {
|
|
2875
|
+
"application/json": {
|
|
2876
|
+
"schema": {
|
|
2877
|
+
"properties": {
|
|
2878
|
+
"data": {
|
|
2879
|
+
"properties": {
|
|
2880
|
+
"direction": {
|
|
2881
|
+
"enum": [
|
|
2882
|
+
"blocks",
|
|
2883
|
+
"blocked_by",
|
|
2884
|
+
"related",
|
|
2885
|
+
"duplicate_of",
|
|
2886
|
+
"duplicated_by"
|
|
2887
|
+
],
|
|
2888
|
+
"type": "string"
|
|
2889
|
+
},
|
|
2890
|
+
"id": {
|
|
2891
|
+
"format": "uuid",
|
|
2892
|
+
"type": "string"
|
|
2893
|
+
},
|
|
2894
|
+
"inserted_at": {
|
|
2895
|
+
"format": "date-time",
|
|
2896
|
+
"type": "string"
|
|
2897
|
+
},
|
|
2898
|
+
"issue": {
|
|
2899
|
+
"type": "string"
|
|
2900
|
+
},
|
|
2901
|
+
"other_issue": {
|
|
2902
|
+
"type": "string"
|
|
2903
|
+
},
|
|
2904
|
+
"related_issue": {
|
|
2905
|
+
"type": "string"
|
|
2906
|
+
},
|
|
2907
|
+
"type": {
|
|
2908
|
+
"enum": [
|
|
2909
|
+
"blocks",
|
|
2910
|
+
"related",
|
|
2911
|
+
"duplicate"
|
|
2912
|
+
],
|
|
2913
|
+
"type": "string"
|
|
2914
|
+
},
|
|
2915
|
+
"updated_at": {
|
|
2916
|
+
"format": "date-time",
|
|
2917
|
+
"type": "string"
|
|
2918
|
+
}
|
|
2919
|
+
},
|
|
2920
|
+
"required": [
|
|
2921
|
+
"id",
|
|
2922
|
+
"type",
|
|
2923
|
+
"issue",
|
|
2924
|
+
"related_issue",
|
|
2925
|
+
"direction",
|
|
2926
|
+
"other_issue",
|
|
2927
|
+
"inserted_at",
|
|
2928
|
+
"updated_at"
|
|
2929
|
+
],
|
|
2930
|
+
"type": "object"
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
"required": [
|
|
2934
|
+
"data"
|
|
2935
|
+
],
|
|
2936
|
+
"type": "object"
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
},
|
|
2940
|
+
"description": "Success"
|
|
2941
|
+
},
|
|
2942
|
+
"400": {
|
|
2943
|
+
"description": "Bad request"
|
|
2944
|
+
},
|
|
2945
|
+
"401": {
|
|
2946
|
+
"description": "Missing or invalid token"
|
|
2947
|
+
},
|
|
2948
|
+
"403": {
|
|
2949
|
+
"description": "Token not permitted (org-scoped, non-admin, or missing scope)"
|
|
2950
|
+
},
|
|
2951
|
+
"404": {
|
|
2952
|
+
"description": "Not found"
|
|
2953
|
+
},
|
|
2954
|
+
"422": {
|
|
2955
|
+
"description": "Validation error"
|
|
2956
|
+
}
|
|
2957
|
+
},
|
|
2958
|
+
"summary": "Update a relation",
|
|
2959
|
+
"tags": []
|
|
2513
2960
|
}
|
|
2514
2961
|
},
|
|
2515
2962
|
"/api/v1/issues/{identifier}/links/{id}": {
|
|
@@ -2559,6 +3006,127 @@
|
|
|
2559
3006
|
},
|
|
2560
3007
|
"summary": "Delete a link",
|
|
2561
3008
|
"tags": []
|
|
3009
|
+
},
|
|
3010
|
+
"patch": {
|
|
3011
|
+
"callbacks": {},
|
|
3012
|
+
"deprecated": false,
|
|
3013
|
+
"description": "Requires the `harness:write` scope.",
|
|
3014
|
+
"parameters": [
|
|
3015
|
+
{
|
|
3016
|
+
"description": "Issue identifier, e.g. ALL-4901 or CLI-12 (issues minted before team-prefixed numbering carry IO-)",
|
|
3017
|
+
"in": "path",
|
|
3018
|
+
"name": "identifier",
|
|
3019
|
+
"required": true,
|
|
3020
|
+
"schema": {
|
|
3021
|
+
"type": "string"
|
|
3022
|
+
}
|
|
3023
|
+
},
|
|
3024
|
+
{
|
|
3025
|
+
"description": "Resource id",
|
|
3026
|
+
"in": "path",
|
|
3027
|
+
"name": "id",
|
|
3028
|
+
"required": true,
|
|
3029
|
+
"schema": {
|
|
3030
|
+
"type": "string"
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
],
|
|
3034
|
+
"requestBody": {
|
|
3035
|
+
"content": {
|
|
3036
|
+
"application/json": {
|
|
3037
|
+
"schema": {
|
|
3038
|
+
"properties": {
|
|
3039
|
+
"expected_url": {
|
|
3040
|
+
"description": "Optional optimistic precondition; mismatch returns 409",
|
|
3041
|
+
"type": "string"
|
|
3042
|
+
},
|
|
3043
|
+
"title": {
|
|
3044
|
+
"type": "string"
|
|
3045
|
+
},
|
|
3046
|
+
"url": {
|
|
3047
|
+
"description": "http(s) URL",
|
|
3048
|
+
"type": "string"
|
|
3049
|
+
}
|
|
3050
|
+
},
|
|
3051
|
+
"type": "object"
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
},
|
|
3055
|
+
"required": true
|
|
3056
|
+
},
|
|
3057
|
+
"responses": {
|
|
3058
|
+
"200": {
|
|
3059
|
+
"content": {
|
|
3060
|
+
"application/json": {
|
|
3061
|
+
"schema": {
|
|
3062
|
+
"properties": {
|
|
3063
|
+
"data": {
|
|
3064
|
+
"properties": {
|
|
3065
|
+
"id": {
|
|
3066
|
+
"format": "uuid",
|
|
3067
|
+
"type": "string"
|
|
3068
|
+
},
|
|
3069
|
+
"inserted_at": {
|
|
3070
|
+
"format": "date-time",
|
|
3071
|
+
"type": "string"
|
|
3072
|
+
},
|
|
3073
|
+
"issue_id": {
|
|
3074
|
+
"format": "uuid",
|
|
3075
|
+
"type": "string"
|
|
3076
|
+
},
|
|
3077
|
+
"title": {
|
|
3078
|
+
"nullable": true,
|
|
3079
|
+
"type": "string"
|
|
3080
|
+
},
|
|
3081
|
+
"updated_at": {
|
|
3082
|
+
"format": "date-time",
|
|
3083
|
+
"type": "string"
|
|
3084
|
+
},
|
|
3085
|
+
"url": {
|
|
3086
|
+
"type": "string"
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
3089
|
+
"required": [
|
|
3090
|
+
"id",
|
|
3091
|
+
"url",
|
|
3092
|
+
"title",
|
|
3093
|
+
"issue_id",
|
|
3094
|
+
"inserted_at",
|
|
3095
|
+
"updated_at"
|
|
3096
|
+
],
|
|
3097
|
+
"type": "object"
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
"required": [
|
|
3101
|
+
"data"
|
|
3102
|
+
],
|
|
3103
|
+
"type": "object"
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
},
|
|
3107
|
+
"description": "Success"
|
|
3108
|
+
},
|
|
3109
|
+
"400": {
|
|
3110
|
+
"description": "Bad request"
|
|
3111
|
+
},
|
|
3112
|
+
"401": {
|
|
3113
|
+
"description": "Missing or invalid token"
|
|
3114
|
+
},
|
|
3115
|
+
"403": {
|
|
3116
|
+
"description": "Token not permitted (org-scoped, non-admin, or missing scope)"
|
|
3117
|
+
},
|
|
3118
|
+
"404": {
|
|
3119
|
+
"description": "Not found"
|
|
3120
|
+
},
|
|
3121
|
+
"409": {
|
|
3122
|
+
"description": "The link URL changed after the client resolved it"
|
|
3123
|
+
},
|
|
3124
|
+
"422": {
|
|
3125
|
+
"description": "Validation error"
|
|
3126
|
+
}
|
|
3127
|
+
},
|
|
3128
|
+
"summary": "Update a link",
|
|
3129
|
+
"tags": []
|
|
2562
3130
|
}
|
|
2563
3131
|
},
|
|
2564
3132
|
"/api/v1/issues/{identifier}/relations": {
|