@allocator-one/harness-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ tool — issue_list, issue_show, issue_create, issue_update, issue_claim,
56
56
  issue_close, issue_comment, relation_add/rm, link_list/add/rm,
57
57
  project_list/show/create/update, milestone_list/create,
58
58
  document_list/show/create/update, label_list/create, team_list,
59
- workflow_states, user_list, stats, upload. Read-only tools carry
59
+ team_create, workflow_states, user_list, stats, upload. Read-only tools carry
60
60
  `readOnlyHint`. Results are compact JSON of the data envelope.
61
61
 
62
62
  Register it with Claude Code:
@@ -108,7 +108,11 @@ var Client = class {
108
108
  async getBytes(path2) {
109
109
  const response = await this.raw("GET", path2, {});
110
110
  if (!response.ok) throw await this.toHttpError(response);
111
- return new Uint8Array(await response.arrayBuffer());
111
+ try {
112
+ return new Uint8Array(await response.arrayBuffer());
113
+ } catch (error) {
114
+ throw new TransportError(transportMessage(error));
115
+ }
112
116
  }
113
117
  /**
114
118
  * The server's API version, for `version`'s server side. The header rides
@@ -155,7 +159,12 @@ var Client = class {
155
159
  }
156
160
  async json(response) {
157
161
  if (!response.ok) throw await this.toHttpError(response);
158
- const text = await response.text();
162
+ let text;
163
+ try {
164
+ text = await response.text();
165
+ } catch (error) {
166
+ throw new TransportError(transportMessage(error));
167
+ }
159
168
  if (text === "") return { data: void 0 };
160
169
  try {
161
170
  return JSON.parse(text);
@@ -505,7 +514,7 @@ function pageCount(meta) {
505
514
  function dedupe(records) {
506
515
  const seen = /* @__PURE__ */ new Set();
507
516
  return records.filter((record) => {
508
- const key = record["id"] ?? record;
517
+ const key = record["id"] ?? JSON.stringify(record);
509
518
  if (seen.has(key)) return false;
510
519
  seen.add(key);
511
520
  return true;
@@ -1016,7 +1025,7 @@ function str2(value) {
1016
1025
  }
1017
1026
 
1018
1027
  // src/commands/extended.ts
1019
- var SAFE_ID = /^[A-Za-z0-9._-]+$/;
1028
+ var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
1020
1029
  function requireSafeId(kind, id) {
1021
1030
  if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
1022
1031
  }
@@ -1219,7 +1228,7 @@ async function linkRm(ctx, issue, idOrUrl) {
1219
1228
  const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
1220
1229
  const link = body.data.find((candidate) => candidate["id"] === idOrUrl || candidate["url"] === idOrUrl);
1221
1230
  if (link === void 0) {
1222
- throw new UsageError(`no link on ${issue} matches ${idOrUrl} (by id or exact URL)`);
1231
+ throw new CommandError(`no link on ${issue} matches ${idOrUrl} (by id or exact URL)`);
1223
1232
  }
1224
1233
  await ctx.client.delete(`/api/v1/issues/${issue}/links/${str3(link["id"])}`);
1225
1234
  return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
@@ -1241,6 +1250,24 @@ async function labelCreate(ctx, name, color) {
1241
1250
  return { json: response.data, human: `Created label ${clean(response.data["name"])}
1242
1251
  ` };
1243
1252
  }
1253
+ var TEAM_CREATE_KEY = /^[A-Za-z][A-Za-z0-9]*$/;
1254
+ async function teamCreate(ctx, opts) {
1255
+ if (opts.key === void 0 || opts.name === void 0) {
1256
+ throw new UsageError("usage: harness team create --key <key> --name <name>");
1257
+ }
1258
+ if (!TEAM_CREATE_KEY.test(opts.key)) {
1259
+ throw new UsageError(`not a team key: ${opts.key} (expected e.g. ALL \u2014 letters and digits, letter first)`);
1260
+ }
1261
+ const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
1262
+ const team = created.data;
1263
+ const states = await ctx.client.get(`/api/v1/teams/${str3(team["key"])}/workflow_states`);
1264
+ const human = `Created team ${str3(team["key"])} \u2014 ${clean(team["name"])}
1265
+ ` + table(
1266
+ ["NAME", "TYPE"],
1267
+ states.data.map((state) => [str3(state["name"]), str3(state["type"])])
1268
+ );
1269
+ return { json: { ...team, workflow_states: states.data }, human };
1270
+ }
1244
1271
  async function teamList(ctx) {
1245
1272
  const body = await ctx.client.get("/api/v1/teams");
1246
1273
  const human = body.data.length === 0 ? "No teams.\n" : table(
@@ -1371,6 +1398,9 @@ function validateBaseUrl(baseUrl) {
1371
1398
  if (parsed.username !== "" || parsed.password !== "") {
1372
1399
  return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
1373
1400
  }
1401
+ if (parsed.search !== "" || parsed.hash !== "") {
1402
+ return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
1403
+ }
1374
1404
  if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
1375
1405
  if (parsed.protocol === "http:") {
1376
1406
  if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
@@ -1444,6 +1474,7 @@ export {
1444
1474
  linkRm,
1445
1475
  labelList,
1446
1476
  labelCreate,
1477
+ teamCreate,
1447
1478
  teamList,
1448
1479
  workflowStates,
1449
1480
  userList,
package/dist/cli.js CHANGED
@@ -45,6 +45,7 @@ import {
45
45
  search,
46
46
  show,
47
47
  stats,
48
+ teamCreate,
48
49
  teamList,
49
50
  update,
50
51
  upload,
@@ -53,7 +54,7 @@ import {
53
54
  version,
54
55
  workflowStates,
55
56
  writeCredentials
56
- } from "./chunk-KN5ILZSA.js";
57
+ } from "./chunk-JOT3TGQQ.js";
57
58
 
58
59
  // src/cli.ts
59
60
  import { Command, CommanderError, InvalidArgumentError } from "commander";
@@ -640,6 +641,9 @@ var team = program.command("team").description("teams");
640
641
  team.command("list").option("--json", "emit JSON").action(async (opts) => {
641
642
  await withConfig(opts.json === true, false, (ctx) => teamList(ctx));
642
643
  });
644
+ team.command("create").description("create a team, seeded with the default workflow states").option("--key <key>", "identifier prefix for the team's issues, e.g. IOS (required)").option("--name <name>", "display name (required)").option("--json", "emit JSON").action(async (opts) => {
645
+ await withConfig(opts.json === true, true, (ctx) => teamCreate(ctx, opts));
646
+ });
643
647
  program.command("states").description("a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
644
648
  await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
645
649
  });
@@ -654,7 +658,7 @@ program.command("upload").description("upload a file; prints markdown to referen
654
658
  });
655
659
  });
656
660
  program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
657
- const { startMcpServer } = await import("./server-PI2AZOVY.js");
661
+ const { startMcpServer } = await import("./server-GYP5FUKD.js");
658
662
  await startMcpServer();
659
663
  });
660
664
  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("--timeout <ms>", "how long to wait for the browser callback", integer).option("--json", "emit JSON").action(async (opts) => {
@@ -4,6 +4,7 @@ import {
4
4
  Client,
5
5
  HttpError,
6
6
  TransportError,
7
+ UsageError,
7
8
  archive,
8
9
  blocked,
9
10
  close,
@@ -31,12 +32,13 @@ import {
31
32
  resolveConfig,
32
33
  show,
33
34
  stats,
35
+ teamCreate,
34
36
  teamList,
35
37
  update,
36
38
  upload,
37
39
  userList,
38
40
  workflowStates
39
- } from "./chunk-KN5ILZSA.js";
41
+ } from "./chunk-JOT3TGQQ.js";
40
42
 
41
43
  // src/mcp/server.ts
42
44
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -105,6 +107,9 @@ var TOOLS = [
105
107
  scope: z.enum(["all", "archived"]).optional()
106
108
  },
107
109
  handler: (ctx, args) => {
110
+ if (args["ready"] === true && args["blocked"] === true) {
111
+ throw new UsageError("ready and blocked are mutually exclusive \u2014 pass one, not both");
112
+ }
108
113
  if (args["ready"] === true) return ready(ctx, args);
109
114
  if (args["blocked"] === true) return blocked(ctx, args);
110
115
  return list(ctx, args);
@@ -379,6 +384,16 @@ var TOOLS = [
379
384
  shape: {},
380
385
  handler: (ctx) => teamList(ctx)
381
386
  },
387
+ {
388
+ name: "team_create",
389
+ description: "Create a team. The key becomes the identifier prefix for issues imported under it (upcased, letters/digits, letter first; IO is reserved). The server seeds the default workflow states, so the team is immediately usable.",
390
+ readOnly: false,
391
+ shape: {
392
+ key: z.string().describe("team key, e.g. IOS"),
393
+ name: z.string().describe("display name")
394
+ },
395
+ handler: (ctx, args) => teamCreate(ctx, { key: args["key"], name: args["name"] })
396
+ },
382
397
  {
383
398
  name: "workflow_states",
384
399
  description: "A team's workflow states (name + type).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allocator-one/harness-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI and MCP server for Harness, allocator.one's issue tracker",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "scripts": {
25
25
  "build": "tsup",
26
26
  "typecheck": "tsc --noEmit",
27
+ "pretest": "npm run build",
27
28
  "test": "vitest run",
28
29
  "codegen": "openapi-typescript spec/openapi.json -o src/generated/api.ts",
29
30
  "spec:refresh": "node scripts/spec-refresh.mjs",
package/spec/openapi.json CHANGED
@@ -1866,6 +1866,53 @@
1866
1866
  },
1867
1867
  "summary": "List teams",
1868
1868
  "tags": []
1869
+ },
1870
+ "post": {
1871
+ "callbacks": {},
1872
+ "deprecated": false,
1873
+ "description": "Requires the `harness:write` scope. Creates a team seeded with the default workflow states (Backlog, Todo, In Progress, Done, Canceled), so it is immediately usable for the full issue lifecycle. The key is upcased, must match `^[A-Z][A-Z0-9]*$`, and `IO` is reserved for the issues Harness mints.",
1874
+ "parameters": [],
1875
+ "requestBody": {
1876
+ "content": {
1877
+ "application/json": {
1878
+ "schema": {
1879
+ "properties": {
1880
+ "key": {
1881
+ "description": "Identifier prefix for issues imported under the team (e.g. ALL). Upcased; create-only.",
1882
+ "type": "string"
1883
+ },
1884
+ "name": {
1885
+ "type": "string"
1886
+ }
1887
+ },
1888
+ "type": "object"
1889
+ }
1890
+ }
1891
+ },
1892
+ "required": true
1893
+ },
1894
+ "responses": {
1895
+ "200": {
1896
+ "description": "Success"
1897
+ },
1898
+ "400": {
1899
+ "description": "Bad request"
1900
+ },
1901
+ "401": {
1902
+ "description": "Missing or invalid token"
1903
+ },
1904
+ "403": {
1905
+ "description": "Token not permitted (org-scoped, non-admin, or missing scope)"
1906
+ },
1907
+ "404": {
1908
+ "description": "Not found"
1909
+ },
1910
+ "422": {
1911
+ "description": "Validation error"
1912
+ }
1913
+ },
1914
+ "summary": "Create a team",
1915
+ "tags": []
1869
1916
  }
1870
1917
  },
1871
1918
  "/api/v1/teams/{team_key}/issues": {