@allocator-one/harness-cli 1.4.1 → 1.5.1

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.
@@ -514,12 +514,19 @@ function pageCount(meta) {
514
514
  function dedupe(records) {
515
515
  const seen = /* @__PURE__ */ new Set();
516
516
  return records.filter((record) => {
517
- const key = record["id"] ?? JSON.stringify(record);
517
+ const id = record["id"];
518
+ const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
518
519
  if (seen.has(key)) return false;
519
520
  seen.add(key);
520
521
  return true;
521
522
  });
522
523
  }
524
+ function stableKey(value) {
525
+ return JSON.stringify(
526
+ value,
527
+ (_field, nested) => nested !== null && typeof nested === "object" && !Array.isArray(nested) ? Object.fromEntries(Object.entries(nested).sort(([a], [b]) => a < b ? -1 : 1)) : nested
528
+ );
529
+ }
523
530
 
524
531
  // src/commands/issues.ts
525
532
  var PRIORITY_NAMES = { urgent: 1, high: 2, medium: 3, low: 4, none: 0 };
@@ -1238,8 +1245,13 @@ async function linkRm(ctx, issue, idOrUrl) {
1238
1245
  async function labelList(ctx) {
1239
1246
  const body = await ctx.client.get("/api/v1/labels");
1240
1247
  const human = body.data.length === 0 ? "No labels.\n" : table(
1241
- ["NAME", "COLOR", "ID"],
1242
- body.data.map((label) => [str3(label["name"]), str3(label["color"]) || "-", str3(label["id"])])
1248
+ ["NAME", "COLOR", "ISSUES", "ID"],
1249
+ body.data.map((label) => [
1250
+ str3(label["name"]),
1251
+ str3(label["color"]) || "-",
1252
+ str3(label["issue_count"] ?? 0),
1253
+ str3(label["id"])
1254
+ ])
1243
1255
  );
1244
1256
  return { json: body.data, human };
1245
1257
  }
@@ -1272,8 +1284,13 @@ async function teamCreate(ctx, opts) {
1272
1284
  async function teamList(ctx) {
1273
1285
  const body = await ctx.client.get("/api/v1/teams");
1274
1286
  const human = body.data.length === 0 ? "No teams.\n" : table(
1275
- ["KEY", "NAME"],
1276
- body.data.map((team) => [str3(team["key"]), str3(team["name"])])
1287
+ ["KEY", "NAME", "ISSUES", "DEFAULT"],
1288
+ body.data.map((team) => [
1289
+ str3(team["key"]),
1290
+ str3(team["name"]),
1291
+ str3(team["issue_count"] ?? 0),
1292
+ team["is_default"] === true ? "yes" : ""
1293
+ ])
1277
1294
  );
1278
1295
  return { json: body.data, human };
1279
1296
  }
@@ -1281,8 +1298,13 @@ async function workflowStates(ctx, team) {
1281
1298
  const key = requiredTeam({ client: ctx.client, config: ctx.config }, team);
1282
1299
  const body = await ctx.client.get(`/api/v1/teams/${key}/workflow_states`);
1283
1300
  const human = body.data.length === 0 ? "No workflow states.\n" : table(
1284
- ["NAME", "TYPE"],
1285
- body.data.map((state) => [str3(state["name"]), str3(state["type"])])
1301
+ ["NAME", "TYPE", "COLOR", "ID"],
1302
+ body.data.map((state) => [
1303
+ str3(state["name"]),
1304
+ str3(state["type"]),
1305
+ str3(state["color"]) || "-",
1306
+ str3(state["id"])
1307
+ ])
1286
1308
  );
1287
1309
  return { json: body.data, human };
1288
1310
  }
@@ -1294,6 +1316,128 @@ async function userList(ctx) {
1294
1316
  );
1295
1317
  return { json: body.data, human };
1296
1318
  }
1319
+ async function teamRename(ctx, key, name) {
1320
+ if (key === void 0 || name === void 0) {
1321
+ throw new UsageError("usage: harness team rename <key> --name <name>");
1322
+ }
1323
+ const body = await ctx.client.patch(`/api/v1/teams/${encodeURIComponent(key)}`, { name });
1324
+ return { json: body.data, human: `Renamed ${str3(body.data["key"])} to ${clean(body.data["name"])}
1325
+ ` };
1326
+ }
1327
+ async function teamSetDefault(ctx, key) {
1328
+ if (key === void 0) throw new UsageError("usage: harness team set-default <key>");
1329
+ const body = await ctx.client.put(`/api/v1/teams/${encodeURIComponent(key)}/default`, {});
1330
+ return {
1331
+ json: body.data,
1332
+ human: `${str3(body.data["key"])} is now the default filing target
1333
+ `
1334
+ };
1335
+ }
1336
+ async function stateAdd(ctx, opts) {
1337
+ const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1338
+ if (opts.name === void 0 || opts.type === void 0) {
1339
+ throw new UsageError("usage: harness state add --name <name> --type <type> [--color <color>] [--team <key>]");
1340
+ }
1341
+ let payload = { name: opts.name, type: opts.type };
1342
+ payload = putOpt2(payload, "color", opts.color);
1343
+ const body = await ctx.client.post(`/api/v1/teams/${key}/workflow_states`, payload);
1344
+ return { json: body.data, human: `Added ${clean(body.data["name"])} to ${key}
1345
+ ` };
1346
+ }
1347
+ async function stateUpdate(ctx, id, opts) {
1348
+ const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1349
+ if (id === void 0) throw new UsageError("usage: harness state update <id> [--name <name>] [--type <type>] [--color <color>]");
1350
+ if (opts.name === void 0 && opts.type === void 0 && opts.color === void 0) {
1351
+ throw new UsageError("nothing to change \u2014 pass at least one of --name, --type, --color");
1352
+ }
1353
+ let payload = {};
1354
+ payload = putOpt2(payload, "name", opts.name);
1355
+ payload = putOpt2(payload, "type", opts.type);
1356
+ payload = putOpt2(payload, "color", opts.color);
1357
+ const body = await ctx.client.patch(`/api/v1/teams/${key}/workflow_states/${encodeURIComponent(id)}`, payload);
1358
+ return { json: body.data, human: `Updated ${clean(body.data["name"])}
1359
+ ` };
1360
+ }
1361
+ async function stateOrder(ctx, ids, opts) {
1362
+ const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1363
+ if (ids === void 0) {
1364
+ throw new UsageError("usage: harness state order <id,id,id> [--team <key>] \u2014 every state id, in the order you want");
1365
+ }
1366
+ const state_ids = ids.split(",").map((id) => id.trim()).filter((id) => id !== "");
1367
+ if (state_ids.length === 0) throw new UsageError("no state ids given");
1368
+ const body = await ctx.client.put(`/api/v1/teams/${key}/workflow_states/order`, { state_ids });
1369
+ const human = table(
1370
+ ["NAME", "TYPE"],
1371
+ body.data.map((state) => [str3(state["name"]), str3(state["type"])])
1372
+ );
1373
+ return { json: body.data, human };
1374
+ }
1375
+ async function stateDelete(ctx, id, opts) {
1376
+ const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1377
+ if (id === void 0) throw new UsageError("usage: harness state delete <id> --yes [--team <key>]");
1378
+ requireYes("state delete", id, opts.yes);
1379
+ await ctx.client.delete(`/api/v1/teams/${key}/workflow_states/${encodeURIComponent(id)}`);
1380
+ return { json: { deleted: id }, human: `Deleted workflow state ${id}
1381
+ ` };
1382
+ }
1383
+ async function labelUpdate(ctx, id, opts) {
1384
+ if (id === void 0) throw new UsageError("usage: harness label update <id> [--name <name>] [--color <color>]");
1385
+ if (opts.name === void 0 && opts.color === void 0) {
1386
+ throw new UsageError("nothing to change \u2014 pass --name, --color, or both");
1387
+ }
1388
+ let payload = {};
1389
+ payload = putOpt2(payload, "name", opts.name);
1390
+ payload = putOpt2(payload, "color", opts.color);
1391
+ const body = await ctx.client.patch(`/api/v1/labels/${encodeURIComponent(id)}`, payload);
1392
+ return { json: body.data, human: `Updated label ${clean(body.data["name"])}
1393
+ ` };
1394
+ }
1395
+ async function labelDelete(ctx, id, opts) {
1396
+ if (id === void 0) throw new UsageError("usage: harness label delete <id> --yes");
1397
+ requireYes("label delete", id, opts.yes);
1398
+ await ctx.client.delete(`/api/v1/labels/${encodeURIComponent(id)}`);
1399
+ return { json: { deleted: id }, human: `Deleted label ${id}
1400
+ ` };
1401
+ }
1402
+ async function memberList(ctx) {
1403
+ const body = await ctx.client.get("/api/v1/members");
1404
+ const human = body.data.length === 0 ? "No members.\n" : table(
1405
+ ["NAME", "EMAIL", "RANK", "STANDING", "ID"],
1406
+ body.data.map((member) => [
1407
+ str3(field(member, "user", "name")),
1408
+ str3(field(member, "user", "email")),
1409
+ str3(member["role"]) || "-",
1410
+ str3(member["standing"]),
1411
+ str3(field(member, "user", "id"))
1412
+ ])
1413
+ );
1414
+ return { json: body.data, human };
1415
+ }
1416
+ async function memberAdd(ctx, opts) {
1417
+ if (opts.email === void 0 || opts.role === void 0) {
1418
+ throw new UsageError("usage: harness member add --email <email> --role <admin|staff|collaborator>");
1419
+ }
1420
+ const body = await ctx.client.post("/api/v1/members", { email: opts.email, role: opts.role });
1421
+ return {
1422
+ json: body.data,
1423
+ human: `Added ${str3(field(body.data, "user", "email"))} as ${str3(body.data["role"])}
1424
+ `
1425
+ };
1426
+ }
1427
+ async function memberSetRole(ctx, userId, opts) {
1428
+ if (userId === void 0 || opts.role === void 0) {
1429
+ throw new UsageError("usage: harness member set-role <user-id> --role <admin|staff|collaborator|none>");
1430
+ }
1431
+ const body = await ctx.client.put(`/api/v1/members/${encodeURIComponent(userId)}/role`, { role: opts.role });
1432
+ const rank = str3(body.data["role"]);
1433
+ const human = rank === "" ? `Revoked the rank for ${str3(field(body.data, "user", "email"))}
1434
+ ` : `${str3(field(body.data, "user", "email"))} is now ${rank}
1435
+ `;
1436
+ return { json: body.data, human };
1437
+ }
1438
+ function requireYes(command, id, yes) {
1439
+ if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
1440
+ }
1297
1441
  async function upload(ctx, path2) {
1298
1442
  return attachImage(ctx, path2);
1299
1443
  }
@@ -1586,6 +1730,17 @@ export {
1586
1730
  teamList,
1587
1731
  workflowStates,
1588
1732
  userList,
1733
+ teamRename,
1734
+ teamSetDefault,
1735
+ stateAdd,
1736
+ stateUpdate,
1737
+ stateOrder,
1738
+ stateDelete,
1739
+ labelUpdate,
1740
+ labelDelete,
1741
+ memberList,
1742
+ memberAdd,
1743
+ memberSetRole,
1589
1744
  upload,
1590
1745
  defaultCredentialsPath,
1591
1746
  readCredentials,
package/dist/cli.js CHANGED
@@ -28,12 +28,17 @@ import {
28
28
  isHostTrusted,
29
29
  json,
30
30
  labelCreate,
31
+ labelDelete,
31
32
  labelList,
33
+ labelUpdate,
32
34
  linkAdd,
33
35
  linkList,
34
36
  linkRm,
35
37
  list,
36
38
  loginHelp,
39
+ memberAdd,
40
+ memberList,
41
+ memberSetRole,
37
42
  milestoneCreate,
38
43
  milestoneList,
39
44
  prime,
@@ -48,9 +53,15 @@ import {
48
53
  resolveConfig,
49
54
  search,
50
55
  show,
56
+ stateAdd,
57
+ stateDelete,
58
+ stateOrder,
59
+ stateUpdate,
51
60
  stats,
52
61
  teamCreate,
53
62
  teamList,
63
+ teamRename,
64
+ teamSetDefault,
54
65
  trustHost,
55
66
  update,
56
67
  upload,
@@ -59,7 +70,7 @@ import {
59
70
  version,
60
71
  workflowStates,
61
72
  writeCredentials
62
- } from "./chunk-MF7MQ2L6.js";
73
+ } from "./chunk-CIW4OU5G.js";
63
74
 
64
75
  // src/cli.ts
65
76
  import { createInterface as createInterface2 } from "readline";
@@ -150,7 +161,7 @@ function isSet(value) {
150
161
  }
151
162
  async function loopbackLogin(baseUrl, options) {
152
163
  const notify = options.notify ?? defaultNotify;
153
- const state = generateState();
164
+ const state2 = generateState();
154
165
  const verifier = generateVerifier();
155
166
  const challenge = challengeFor(verifier);
156
167
  const server = createServer();
@@ -165,13 +176,13 @@ async function loopbackLogin(baseUrl, options) {
165
176
  return { ok: false, message: "could not open a loopback port for the browser handoff" };
166
177
  }
167
178
  try {
168
- const query = new URLSearchParams({ port: String(port), state, challenge });
179
+ const query = new URLSearchParams({ port: String(port), state: state2, challenge });
169
180
  const url = `${baseUrl}/cli/login?${query.toString()}`;
170
181
  const openBrowser = options.openBrowser ?? defaultOpenBrowser;
171
182
  openBrowser(url);
172
183
  notify(`Opening ${url}`);
173
184
  notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
174
- const code = await awaitCallback(server, state, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
185
+ const code = await awaitCallback(server, state2, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
175
186
  if (!code.ok) return code;
176
187
  const token = await exchangeCode(baseUrl, code.code, verifier, options);
177
188
  if (!token.ok) return token;
@@ -181,7 +192,7 @@ async function loopbackLogin(baseUrl, options) {
181
192
  server.close();
182
193
  }
183
194
  }
184
- function awaitCallback(server, state, timeoutMs) {
195
+ function awaitCallback(server, state2, timeoutMs) {
185
196
  return new Promise((resolve) => {
186
197
  const timer = setTimeout(() => {
187
198
  resolve({ ok: false, message: "timed out waiting for the browser authorization" });
@@ -203,7 +214,7 @@ function awaitCallback(server, state, timeoutMs) {
203
214
  return;
204
215
  }
205
216
  const code = url.searchParams.get("code");
206
- if (url.searchParams.get("state") === state && code !== null && code !== "") {
217
+ if (url.searchParams.get("state") === state2 && code !== null && code !== "") {
207
218
  respond(200, "Signed in \u2014 you can close this tab and return to the terminal.");
208
219
  clearTimeout(timer);
209
220
  resolve({ ok: true, code });
@@ -1297,6 +1308,12 @@ label.command("create").argument("[name]").option("--color <color>", "label colo
1297
1308
  );
1298
1309
  }
1299
1310
  );
1311
+ label.command("update").description("rename or recolor a label").argument("[id]").option("--name <name>", "new name").option("--color <color>", "swatch name (red, blue, \u2026) or a hex code").option("--json", "emit JSON").action(async (id, opts) => {
1312
+ await withConfig(opts.json === true, true, (ctx) => labelUpdate(ctx, id, opts));
1313
+ });
1314
+ label.command("delete").description("delete a label (refused while issues carry it)").argument("[id]").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
1315
+ await withConfig(opts.json === true, true, (ctx) => labelDelete(ctx, id, opts));
1316
+ });
1300
1317
  var team = program.command("team").description("teams");
1301
1318
  team.command("list").option("--json", "emit JSON").action(async (opts) => {
1302
1319
  await withConfig(
@@ -1315,7 +1332,41 @@ team.command("create").description("create a team, seeded with the default workf
1315
1332
  (ctx) => teamCreate(ctx, opts)
1316
1333
  );
1317
1334
  });
1318
- program.command("states").description("a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
1335
+ team.command("rename").description("rename a team (the key is create-only)").argument("[key]").option("--name <name>", "new display name").option("--json", "emit JSON").action(async (key, opts) => {
1336
+ await withConfig(opts.json === true, true, (ctx) => teamRename(ctx, key, opts.name));
1337
+ });
1338
+ team.command("set-default").description("make this team the organization's default filing target").argument("[key]").option("--json", "emit JSON").action(async (key, opts) => {
1339
+ await withConfig(opts.json === true, true, (ctx) => teamSetDefault(ctx, key));
1340
+ });
1341
+ var state = program.command("state").description("a team's workflow states");
1342
+ state.command("list").description("list a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
1343
+ await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
1344
+ });
1345
+ state.command("add").description("add a workflow state, appended after the team's existing ones").option("--team <key>", "team key (or HARNESS_TEAM)").option("--name <name>", "display name (required)").option("--type <type>", "backlog | unstarted | started | completed | canceled (required)").option("--color <color>", "swatch name").option("--json", "emit JSON").action(async (opts) => {
1346
+ await withConfig(opts.json === true, true, (ctx) => stateAdd(ctx, opts));
1347
+ });
1348
+ state.command("update").description("rename, retype or recolor a workflow state").argument("[id]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--name <name>", "new name").option("--type <type>", "new type").option("--color <color>", "new swatch name").option("--json", "emit JSON").action(
1349
+ async (id, opts) => {
1350
+ await withConfig(opts.json === true, true, (ctx) => stateUpdate(ctx, id, opts));
1351
+ }
1352
+ );
1353
+ state.command("order").description("reorder a team's states \u2014 pass EVERY state id, comma separated, in the order you want").argument("[ids]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (ids, opts) => {
1354
+ await withConfig(opts.json === true, true, (ctx) => stateOrder(ctx, ids, opts));
1355
+ });
1356
+ state.command("delete").description("delete a workflow state (refused while issues sit in it)").argument("[id]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
1357
+ await withConfig(opts.json === true, true, (ctx) => stateDelete(ctx, id, opts));
1358
+ });
1359
+ var member = program.command("member").description("the organization's tracker roster");
1360
+ member.command("list").description("who can reach this organization's tracker, and at what rank").option("--json", "emit JSON").action(async (opts) => {
1361
+ await withConfig(opts.json === true, false, (ctx) => memberList(ctx));
1362
+ });
1363
+ member.command("add").description("grant a rank to an existing account, minting the membership").option("--email <email>", "address of an existing account (required)").option("--role <role>", "admin | staff | collaborator (required)").option("--json", "emit JSON").action(async (opts) => {
1364
+ await withConfig(opts.json === true, true, (ctx) => memberAdd(ctx, opts));
1365
+ });
1366
+ member.command("set-role").description("change or revoke a member's rank; none revokes").argument("[user-id]").option("--role <role>", "admin | staff | collaborator | none (required)").option("--json", "emit JSON").action(async (userId, opts) => {
1367
+ await withConfig(opts.json === true, true, (ctx) => memberSetRole(ctx, userId, opts));
1368
+ });
1369
+ program.command("states").description("a team's workflow states (alias of `state list`)").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
1319
1370
  await withConfig(
1320
1371
  opts.json === true,
1321
1372
  false,
@@ -1340,7 +1391,7 @@ program.command("upload").description("upload a file; prints markdown to referen
1340
1391
  });
1341
1392
  });
1342
1393
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
1343
- const { startMcpServer } = await import("./server-LJXJLWNK.js");
1394
+ const { startMcpServer } = await import("./server-J2V5JXFY.js");
1344
1395
  await startMcpServer();
1345
1396
  });
1346
1397
  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(
@@ -17,11 +17,16 @@ import {
17
17
  docShow,
18
18
  docUpdate,
19
19
  labelCreate,
20
+ labelDelete,
20
21
  labelList,
22
+ labelUpdate,
21
23
  linkAdd,
22
24
  linkList,
23
25
  linkRm,
24
26
  list,
27
+ memberAdd,
28
+ memberList,
29
+ memberSetRole,
25
30
  milestoneCreate,
26
31
  milestoneList,
27
32
  projectCreate,
@@ -32,14 +37,20 @@ import {
32
37
  resolveConfig,
33
38
  search,
34
39
  show,
40
+ stateAdd,
41
+ stateDelete,
42
+ stateOrder,
43
+ stateUpdate,
35
44
  stats,
36
45
  teamCreate,
37
46
  teamList,
47
+ teamRename,
48
+ teamSetDefault,
38
49
  update,
39
50
  upload,
40
51
  userList,
41
52
  workflowStates
42
- } from "./chunk-MF7MQ2L6.js";
53
+ } from "./chunk-CIW4OU5G.js";
43
54
 
44
55
  // src/mcp/server.ts
45
56
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -416,6 +427,120 @@ var TOOLS = [
416
427
  shape: { team: z.string().optional().describe("team key; defaults to HARNESS_TEAM") },
417
428
  handler: (ctx, args) => workflowStates(ctx, args["team"])
418
429
  },
430
+ {
431
+ name: "team_rename",
432
+ description: "Rename a team. The key is create-only \u2014 it is the identifier prefix of every issue filed under the team \u2014 so this changes the display name only. Needs Harness administrator standing.",
433
+ readOnly: false,
434
+ shape: {
435
+ key: z.string().describe("team key, e.g. IOS"),
436
+ name: z.string().describe("new display name")
437
+ },
438
+ handler: (ctx, args) => teamRename(ctx, args["key"], args["name"])
439
+ },
440
+ {
441
+ name: "team_set_default",
442
+ description: "Make a team the organization's default filing target, clearing the previous default. Needs Harness administrator standing.",
443
+ readOnly: false,
444
+ shape: { key: z.string().describe("team key") },
445
+ handler: (ctx, args) => teamSetDefault(ctx, args["key"])
446
+ },
447
+ {
448
+ name: "state_add",
449
+ description: "Add a workflow state to a team, appended after its existing states. Type is one of backlog, unstarted, started, completed, canceled. Needs Harness administrator standing.",
450
+ readOnly: false,
451
+ shape: {
452
+ name: z.string().describe("display name"),
453
+ type: z.string().describe("backlog | unstarted | started | completed | canceled"),
454
+ color: z.string().optional().describe("swatch name"),
455
+ team: z.string().optional().describe("team key; defaults to HARNESS_TEAM")
456
+ },
457
+ handler: (ctx, args) => stateAdd(ctx, { team: args["team"], name: args["name"], type: args["type"], color: args["color"] })
458
+ },
459
+ {
460
+ name: "state_update",
461
+ description: "Rename, retype or recolor one of a team's workflow states. Pass at least one of name, type, color. Needs Harness administrator standing.",
462
+ readOnly: false,
463
+ shape: {
464
+ id: z.string().describe("workflow state id (from workflow_states)"),
465
+ name: z.string().optional(),
466
+ type: z.string().optional().describe("backlog | unstarted | started | completed | canceled"),
467
+ color: z.string().optional().describe("swatch name"),
468
+ team: z.string().optional().describe("team key; defaults to HARNESS_TEAM")
469
+ },
470
+ handler: (ctx, args) => stateUpdate(ctx, args["id"], {
471
+ team: args["team"],
472
+ name: args["name"],
473
+ type: args["type"],
474
+ color: args["color"]
475
+ })
476
+ },
477
+ {
478
+ name: "state_order",
479
+ description: "Reorder a team's workflow states. Send EVERY state id, in the order you want them \u2014 a partial list is refused rather than half-applied, because renumbering part of the order would collide with the positions it left alone. Call workflow_states first to get the ids. Needs Harness administrator standing.",
480
+ readOnly: false,
481
+ shape: {
482
+ state_ids: z.string().describe("comma-separated state ids, in the desired order \u2014 all of them"),
483
+ team: z.string().optional().describe("team key; defaults to HARNESS_TEAM")
484
+ },
485
+ handler: (ctx, args) => stateOrder(ctx, args["state_ids"], { team: args["team"] })
486
+ },
487
+ {
488
+ name: "state_delete",
489
+ description: "Delete a workflow state. Refused while issues still sit in it \u2014 move them out first. Needs Harness administrator standing.",
490
+ readOnly: false,
491
+ shape: {
492
+ id: z.string().describe("workflow state id"),
493
+ team: z.string().optional().describe("team key; defaults to HARNESS_TEAM")
494
+ },
495
+ // `yes` is the CLI's typo guard for a human at a shell; an MCP call is
496
+ // already an explicit, named request to delete this id.
497
+ handler: (ctx, args) => stateDelete(ctx, args["id"], { team: args["team"], yes: true })
498
+ },
499
+ {
500
+ name: "label_update",
501
+ description: "Rename or recolor a label. Color is a swatch name (purple, green, red, yellow, pink, blue, orange, gray) or a hex code. Needs Harness administrator standing.",
502
+ readOnly: false,
503
+ shape: {
504
+ id: z.string().describe("label id (from label_list)"),
505
+ name: z.string().optional(),
506
+ color: z.string().optional().describe("swatch name or hex code")
507
+ },
508
+ handler: (ctx, args) => labelUpdate(ctx, args["id"], { name: args["name"], color: args["color"] })
509
+ },
510
+ {
511
+ name: "label_delete",
512
+ description: "Delete a label. Refused while issues still carry it, or while other labels are nested under it. Needs Harness administrator standing.",
513
+ readOnly: false,
514
+ shape: { id: z.string().describe("label id") },
515
+ handler: (ctx, args) => labelDelete(ctx, args["id"], { yes: true })
516
+ },
517
+ {
518
+ name: "member_list",
519
+ description: "Who can reach THIS organization's tracker, and at what rank \u2014 its grant holders plus its own admins. Not the same as user_list, which is the deployment-wide assignee picker and never lists a collaborator. Each entry carries both role (the granted rank, absent for an org admin) and standing (what that rank resolves to). Needs Harness administrator standing.",
520
+ readOnly: true,
521
+ shape: {},
522
+ handler: (ctx) => memberList(ctx)
523
+ },
524
+ {
525
+ name: "member_add",
526
+ description: "Grant a tracker rank to an existing account by email, minting the organization membership if they have none. The account must already exist. Refused if they are already on the roster \u2014 use member_set_role to change a rank. Needs Harness administrator standing.",
527
+ readOnly: false,
528
+ shape: {
529
+ email: z.string().describe("address of an existing account"),
530
+ role: z.string().describe("admin | staff | collaborator")
531
+ },
532
+ handler: (ctx, args) => memberAdd(ctx, { email: args["email"], role: args["role"] })
533
+ },
534
+ {
535
+ name: "member_set_role",
536
+ description: "Change or revoke a member's tracker rank; 'none' revokes. Refused when it would remove the organization's last Harness administrator. Needs Harness administrator standing.",
537
+ readOnly: false,
538
+ shape: {
539
+ user_id: z.string().describe("the member's user id (from member_list)"),
540
+ role: z.string().describe("admin | staff | collaborator | none")
541
+ },
542
+ handler: (ctx, args) => memberSetRole(ctx, args["user_id"], { role: args["role"] })
543
+ },
419
544
  {
420
545
  name: "user_list",
421
546
  description: "List users (for assignee resolution).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",