@allocator-one/harness-cli 1.5.2 → 1.7.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/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-CIW4OU5G.js";
83
+ } from "./chunk-ZHFW6PRC.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"];
@@ -1208,10 +1235,15 @@ function requireNonBlankTitle(text, titleFile) {
1208
1235
  titleFile === void 0 ? "--title is empty \u2014 an issue needs a title" : `--title-file ${titleFile} produced an empty title`
1209
1236
  );
1210
1237
  }
1211
- program.command("create").description("create an issue").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
1238
+ program.command("create").description("create an issue atomically after resolving requested labels").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
1212
1239
  "--type <type>",
1213
1240
  "task | bug | feature | epic (applied as a type/ label)"
1214
- ).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--label <name>", "label to apply (repeatable)", collect, []).option("--json", "emit JSON").action(async (opts) => {
1241
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option(
1242
+ "--label <name>",
1243
+ "resolve team-first, then workspace; create missing workspace labels before the issue (repeatable)",
1244
+ collect,
1245
+ []
1246
+ ).option("--json", "emit JSON").action(async (opts) => {
1215
1247
  await withConfig(opts.json === true, true, (ctx) => {
1216
1248
  if (opts.title === void 0 && opts.titleFile === void 0) {
1217
1249
  throw new UsageError(
@@ -1227,6 +1259,16 @@ program.command("create").description("create an issue").option("--title <text>"
1227
1259
  program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'replace the notes from a file ("-" for stdin)').option("--append-notes-file <path>", 'append to the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option(
1228
1260
  "--assignee <who>",
1229
1261
  'me, a user id, an email, or a full name; "" unassigns'
1262
+ ).option(
1263
+ "--add-label <name-or-id>",
1264
+ "add a label without replacing existing labels (repeatable)",
1265
+ collect,
1266
+ []
1267
+ ).option(
1268
+ "--remove-label <name-or-id>",
1269
+ "remove an attached label without replacing others (repeatable)",
1270
+ collect,
1271
+ []
1230
1272
  ).option("--json", "emit JSON").action(
1231
1273
  async (id, opts) => {
1232
1274
  await withConfig(opts.json === true, true, (ctx) => {
@@ -1298,7 +1340,42 @@ program.command("dep").description("blocking dependencies: dep add|rm <issue> <b
1298
1340
  });
1299
1341
  }
1300
1342
  );
1301
- 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(
1343
+ var relation = program.command("relation").description("typed issue relations; dep remains the blocking-dependency shorthand");
1344
+ relation.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
1345
+ await withConfig(opts.json === true, false, (ctx) => {
1346
+ if (issue === void 0) throw new UsageError("usage: harness relation list <issue>");
1347
+ return relationList(ctx, issue);
1348
+ });
1349
+ });
1350
+ relation.command("add").argument("[issue]").argument("[related-issue]").requiredOption("--type <type>", `relation type: ${RELATION_CREATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
1351
+ async (issue, relatedIssue, opts) => {
1352
+ await withConfig(opts.json === true, true, (ctx) => {
1353
+ if (issue === void 0 || relatedIssue === void 0) {
1354
+ throw new UsageError("usage: harness relation add <issue> <related-issue> --type <type>");
1355
+ }
1356
+ return relationAdd(ctx, issue, relatedIssue, opts.type);
1357
+ });
1358
+ }
1359
+ );
1360
+ relation.command("update").argument("[issue]").argument("[id-or-issue]").requiredOption("--type <type>", `relation type: ${RELATION_UPDATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
1361
+ async (issue, idOrIssue, opts) => {
1362
+ await withConfig(opts.json === true, true, (ctx) => {
1363
+ if (issue === void 0 || idOrIssue === void 0) {
1364
+ throw new UsageError("usage: harness relation update <issue> <relation-id-or-issue> --type <type>");
1365
+ }
1366
+ return relationUpdate(ctx, issue, idOrIssue, opts.type);
1367
+ });
1368
+ }
1369
+ );
1370
+ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--json", "emit JSON").action(async (issue, idOrIssue, opts) => {
1371
+ await withConfig(opts.json === true, true, (ctx) => {
1372
+ if (issue === void 0 || idOrIssue === void 0) {
1373
+ throw new UsageError("usage: harness relation rm <issue> <relation-id-or-issue>");
1374
+ }
1375
+ return relationRm(ctx, issue, idOrIssue);
1376
+ });
1377
+ });
1378
+ 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(
1302
1379
  async (id, text, opts) => {
1303
1380
  await withConfig(opts.json === true, true, (ctx) => {
1304
1381
  if (id === void 0 || opts.bodyFile === void 0 && text.length === 0) {
@@ -1307,25 +1384,39 @@ program.command("comment").description("comment on an issue").argument("[id]").a
1307
1384
  );
1308
1385
  }
1309
1386
  requireIdentifiers2([id]);
1310
- if (opts.bodyFile !== void 0 && text.length > 0) {
1311
- throw new UsageError(
1312
- "comment text and --body-file both given \u2014 pass one; there is no precedence between them"
1313
- );
1314
- }
1315
- const fromFile = resolveTextSources([
1316
- { name: "body", inline: void 0, file: opts.bodyFile }
1317
- ]).body;
1318
- const body = fromFile === void 0 ? text : [fromFile];
1319
- if (body.join(" ").trim() === "") {
1387
+ const body = resolvedCommentBody(text, opts.bodyFile);
1388
+ const mentions = opts.mention.filter((name) => name.trim() !== "");
1389
+ return comment(ctx, id, body, mentions);
1390
+ });
1391
+ }
1392
+ );
1393
+ 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(
1394
+ async (issue, commentId, text, opts) => {
1395
+ const parentOpts = commentCommand.opts();
1396
+ const bodyFile = opts.bodyFile ?? parentOpts.bodyFile;
1397
+ await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
1398
+ if (issue === void 0 || commentId === void 0 || bodyFile === void 0 && text.length === 0) {
1320
1399
  throw new UsageError(
1321
- opts.bodyFile === void 0 ? "the comment text is empty \u2014 nothing to post" : `--body-file ${opts.bodyFile} produced an empty comment \u2014 nothing to post`
1400
+ "usage: harness comment edit <issue> <comment-id> <text> | harness comment edit <issue> <comment-id> --body-file <path>"
1322
1401
  );
1323
1402
  }
1324
- const mentions = opts.mention.filter((name) => name.trim() !== "");
1325
- return comment(ctx, id, body, mentions);
1403
+ requireIdentifiers2([issue]);
1404
+ requireCommentId(commentId);
1405
+ return editComment(ctx, issue, commentId, resolvedCommentBody(text, bodyFile, "edit"));
1326
1406
  });
1327
1407
  }
1328
1408
  );
1409
+ 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) => {
1410
+ const parentOpts = commentCommand.opts();
1411
+ await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
1412
+ if (issue === void 0 || commentId === void 0) {
1413
+ throw new UsageError("usage: harness comment delete <issue> <comment-id>");
1414
+ }
1415
+ requireIdentifiers2([issue]);
1416
+ requireCommentId(commentId);
1417
+ return deleteComment(ctx, issue, commentId);
1418
+ });
1419
+ });
1329
1420
  program.command("attach-image").description("upload an image; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
1330
1421
  await withConfig(opts.json === true, true, (ctx) => {
1331
1422
  if (path === void 0) {
@@ -1439,6 +1530,18 @@ link.command("add").argument("[issue]").argument("[url]").option("--title <title
1439
1530
  });
1440
1531
  }
1441
1532
  );
1533
+ 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(
1534
+ async (issue, idOrUrl, opts) => {
1535
+ await withConfig(opts.json === true, true, (ctx) => {
1536
+ if (issue === void 0 || idOrUrl === void 0) {
1537
+ throw new UsageError(
1538
+ "usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>"
1539
+ );
1540
+ }
1541
+ return linkUpdate(ctx, issue, idOrUrl, opts);
1542
+ });
1543
+ }
1544
+ );
1442
1545
  link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
1443
1546
  async (issue, idOrUrl, opts) => {
1444
1547
  await withConfig(opts.json === true, true, (ctx) => {
@@ -1552,7 +1655,7 @@ program.command("upload").description("upload a file; prints markdown to referen
1552
1655
  });
1553
1656
  });
1554
1657
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
1555
- const { startMcpServer } = await import("./server-J2V5JXFY.js");
1658
+ const { startMcpServer } = await import("./server-J6FQFVBJ.js");
1556
1659
  await startMcpServer();
1557
1660
  });
1558
1661
  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-CIW4OU5G.js";
62
+ } from "./chunk-ZHFW6PRC.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 ──
@@ -189,13 +199,17 @@ var TOOLS = [
189
199
  due_date: dateField,
190
200
  state: z.string().optional().describe("workflow state by name or type"),
191
201
  parent: z.string().optional(),
192
- assignee: z.string().optional().describe('"" unassigns')
202
+ assignee: z.string().optional().describe('"" unassigns'),
203
+ add_labels: z.array(z.string()).optional().describe("label names or ids to add without replacing existing labels"),
204
+ remove_labels: z.array(z.string()).optional().describe("attached label names or ids to remove without replacing others")
193
205
  },
194
206
  handler: (ctx, args) => update(ctx, args["id"], {
195
207
  ...args,
196
208
  acceptance: args["acceptance_criteria"],
197
209
  appendNotes: args["append_notes"],
198
- dueDate: args["due_date"]
210
+ dueDate: args["due_date"],
211
+ addLabel: args["add_labels"],
212
+ removeLabel: args["remove_labels"]
199
213
  })
200
214
  },
201
215
  {
@@ -227,6 +241,27 @@ var TOOLS = [
227
241
  },
228
242
  handler: (ctx, args) => comment(ctx, args["id"], [args["body"]], args["mentions"] ?? [])
229
243
  },
244
+ {
245
+ name: "issue_comment_edit",
246
+ description: "Edit one of your issue comments within its edit window.",
247
+ readOnly: false,
248
+ shape: {
249
+ id: identifier,
250
+ comment_id: commentId,
251
+ body: z.string().min(1).refine((body) => body.trim() !== "", "comment body must not be blank")
252
+ },
253
+ handler: (ctx, args) => editComment(ctx, args["id"], args["comment_id"], [args["body"]])
254
+ },
255
+ {
256
+ name: "issue_comment_delete",
257
+ description: "Delete one of your issue comments.",
258
+ readOnly: false,
259
+ shape: {
260
+ id: identifier,
261
+ comment_id: commentId
262
+ },
263
+ handler: (ctx, args) => deleteComment(ctx, args["id"], args["comment_id"])
264
+ },
230
265
  {
231
266
  name: "issue_archive",
232
267
  description: "Archive issues (or restore them with restore=true). Archived issues show up under scope=archived.",
@@ -241,7 +276,7 @@ var TOOLS = [
241
276
  shape: { id: identifier },
242
277
  handler: (ctx, args) => comments(ctx, args["id"])
243
278
  },
244
- // ── Relations (blocking dependencies) ──
279
+ // ── Relations ──
245
280
  {
246
281
  name: "relation_add",
247
282
  description: "Add a blocking dependency: blocker blocks issue.",
@@ -256,6 +291,42 @@ var TOOLS = [
256
291
  shape: { issue: identifier, blocker: identifier },
257
292
  handler: (ctx, args) => dep(ctx, "rm", args["issue"], args["blocker"])
258
293
  },
294
+ {
295
+ name: "relation_list",
296
+ description: "List every typed relation on an issue, including stable relation ids.",
297
+ readOnly: true,
298
+ shape: { issue: identifier },
299
+ handler: (ctx, args) => relationList(ctx, args["issue"])
300
+ },
301
+ {
302
+ name: "relation_create",
303
+ description: "Create a typed relation between two issues.",
304
+ readOnly: false,
305
+ shape: {
306
+ issue: identifier,
307
+ related_issue: identifier,
308
+ type: z.enum(RELATION_CREATE_TYPES)
309
+ },
310
+ handler: (ctx, args) => relationAdd(ctx, args["issue"], args["related_issue"], args["type"])
311
+ },
312
+ {
313
+ name: "relation_update",
314
+ description: "Change a relation's direction, resolving it by relation id or unique related issue.",
315
+ readOnly: false,
316
+ shape: {
317
+ issue: identifier,
318
+ id_or_issue: z.string(),
319
+ type: z.enum(RELATION_UPDATE_TYPES)
320
+ },
321
+ handler: (ctx, args) => relationUpdate(ctx, args["issue"], args["id_or_issue"], args["type"])
322
+ },
323
+ {
324
+ name: "relation_delete",
325
+ description: "Remove a typed relation by relation id or unique related issue.",
326
+ readOnly: false,
327
+ shape: { issue: identifier, id_or_issue: z.string() },
328
+ handler: (ctx, args) => relationRm(ctx, args["issue"], args["id_or_issue"])
329
+ },
259
330
  // ── Links (URLs on an issue) ──
260
331
  {
261
332
  name: "link_list",
@@ -271,6 +342,20 @@ var TOOLS = [
271
342
  shape: { issue: identifier, url: z.string(), title: z.string().optional() },
272
343
  handler: (ctx, args) => linkAdd(ctx, args["issue"], args["url"], args["title"])
273
344
  },
345
+ {
346
+ name: "link_update",
347
+ description: "Edit a link by link id or exact URL.",
348
+ readOnly: false,
349
+ shape: {
350
+ issue: identifier,
351
+ id_or_url: z.string(),
352
+ change: z.union([
353
+ z.object({ url: z.string(), title: z.string().optional() }),
354
+ z.object({ title: z.string(), url: z.string().optional() })
355
+ ])
356
+ },
357
+ handler: (ctx, args) => linkUpdate(ctx, args["issue"], args["id_or_url"], args["change"])
358
+ },
274
359
  {
275
360
  name: "link_rm",
276
361
  description: "Remove a link from an issue, by link id or exact URL.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.5.2",
3
+ "version": "1.7.0",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",