@allocator-one/harness-cli 1.8.0 → 1.10.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 +14 -3
- package/dist/{chunk-NQVQNKMC.js → chunk-KKAAKRKF.js} +161 -4
- package/dist/cli.js +307 -54
- package/dist/login-page-UVUKFFTT.js +292 -0
- package/dist/{server-FWNT3HMX.js → server-QVYSED3Z.js} +14 -1
- package/package.json +1 -1
- package/spec/openapi.json +361 -0
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ the browser login attributes writes to the real user.
|
|
|
52
52
|
## For agents
|
|
53
53
|
|
|
54
54
|
`harness mcp` runs a stdio MCP server exposing every operation as a typed
|
|
55
|
-
tool — issue_list, issue_show, issue_create, issue_update, issue_claim,
|
|
55
|
+
tool — issue_list, issue_show, activity_list, issue_create, issue_update, issue_claim,
|
|
56
56
|
issue_close, issue_comment, issue_comment_edit, issue_comment_delete,
|
|
57
57
|
relation_list/create/update/delete, relation_add/rm (blocking shorthand),
|
|
58
58
|
link_list/add/update/rm,
|
|
@@ -71,6 +71,12 @@ Example tool call: `issue_list` with `{"ready": true, "team": "ALL"}`
|
|
|
71
71
|
returns the ready queue as JSON; `issue_claim` with `{"id": "IO-123"}`
|
|
72
72
|
claims atomically (a conflict means someone else holds it).
|
|
73
73
|
|
|
74
|
+
Read the same mixed timeline shown in an issue's Activity section with
|
|
75
|
+
`harness activity IO-123`. It returns the newest 20 entries and points out
|
|
76
|
+
when older entries exist; pass `--all` to follow the opaque cursors through
|
|
77
|
+
the full history. `activity_list` exposes the same `id`, `limit`, and `all`
|
|
78
|
+
inputs to MCP clients.
|
|
79
|
+
|
|
74
80
|
Typed issue relations are available without losing the concise blocking
|
|
75
81
|
workflow. `harness dep add|rm` and the `relation_add`/`relation_rm` MCP tools
|
|
76
82
|
remain the blocker-first shorthand; the full surface is:
|
|
@@ -181,10 +187,15 @@ CI regenerates the types and fails on drift.
|
|
|
181
187
|
|
|
182
188
|
## Release
|
|
183
189
|
|
|
184
|
-
|
|
190
|
+
First bump `package.json` and `package-lock.json` through a pull request. After
|
|
191
|
+
that PR is squash-merged, fetch `main` and tag the merge commit — never the
|
|
192
|
+
pre-merge branch tip. Pushing the tag starts the Release workflow, which builds,
|
|
193
|
+
tests, and publishes to npm:
|
|
185
194
|
|
|
186
195
|
```sh
|
|
187
|
-
git
|
|
196
|
+
git fetch origin main
|
|
197
|
+
git tag -a vX.Y.Z MERGE_COMMIT_SHA -m "Release vX.Y.Z"
|
|
198
|
+
git push origin vX.Y.Z
|
|
188
199
|
```
|
|
189
200
|
|
|
190
201
|
Publishing requires the `NPM_TOKEN` actions secret (an automation token for
|
|
@@ -166,11 +166,16 @@ var Client = class {
|
|
|
166
166
|
throw new TransportError(transportMessage(error));
|
|
167
167
|
}
|
|
168
168
|
if (text === "") return { data: void 0 };
|
|
169
|
+
let parsed;
|
|
169
170
|
try {
|
|
170
|
-
|
|
171
|
+
parsed = JSON.parse(text);
|
|
171
172
|
} catch {
|
|
172
173
|
throw new TransportError(`the API answered with a non-JSON body (HTTP ${response.status})`);
|
|
173
174
|
}
|
|
175
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
176
|
+
throw new TransportError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
|
|
177
|
+
}
|
|
178
|
+
return parsed;
|
|
174
179
|
}
|
|
175
180
|
async toHttpError(response) {
|
|
176
181
|
let parsed = {};
|
|
@@ -255,6 +260,101 @@ ${indent(String(text))}
|
|
|
255
260
|
`).join("");
|
|
256
261
|
return header + fieldLines + sectionText + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
|
|
257
262
|
}
|
|
263
|
+
function activityEntries(entries) {
|
|
264
|
+
if (entries.length === 0) return "No activity.\n";
|
|
265
|
+
return entries.map(activityEntry).join("\n");
|
|
266
|
+
}
|
|
267
|
+
function activityEntry(entry) {
|
|
268
|
+
switch (entry["kind"]) {
|
|
269
|
+
case "comment":
|
|
270
|
+
return activityComment(entry);
|
|
271
|
+
case "state_event":
|
|
272
|
+
return activityStateEvent(entry);
|
|
273
|
+
default:
|
|
274
|
+
throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function activityComment(entry) {
|
|
278
|
+
const comment2 = requiredRecord(entry["comment"], "activity comment");
|
|
279
|
+
const authorValue = comment2["author"];
|
|
280
|
+
const author = authorValue === null ? "unknown" : optionalLabel(requiredRecord(authorValue, "comment author")["name"], "unknown", "comment author name");
|
|
281
|
+
const deleted = comment2["deleted"];
|
|
282
|
+
if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
|
|
283
|
+
const body = activityCommentBody(comment2["body"], deleted);
|
|
284
|
+
const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
|
|
285
|
+
return `[${clean(requiredString(entry["occurred_at"], "activity occurrence time"))}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
|
|
286
|
+
${text}
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
function activityCommentBody(value, deleted) {
|
|
290
|
+
if (deleted) return "[deleted comment]";
|
|
291
|
+
if (value === null || value === "") return "[comment has no text]";
|
|
292
|
+
return requiredString(value, "comment body");
|
|
293
|
+
}
|
|
294
|
+
function activityStateEvent(entry) {
|
|
295
|
+
const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
|
|
296
|
+
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
297
|
+
let description;
|
|
298
|
+
switch (entry["action"]) {
|
|
299
|
+
case "created":
|
|
300
|
+
description = `${actor} created the issue in ${stateName(entry["to_state"], "created to_state")}`;
|
|
301
|
+
break;
|
|
302
|
+
case "transition":
|
|
303
|
+
description = `${actor} moved the issue from ${nullableStateName(entry["from_state"], "transition from_state")} to ${stateName(entry["to_state"], "transition to_state")}`;
|
|
304
|
+
break;
|
|
305
|
+
case "milestone":
|
|
306
|
+
description = `${actor} recorded the ${stateName(entry["to_state"], "milestone to_state")} milestone`;
|
|
307
|
+
break;
|
|
308
|
+
default:
|
|
309
|
+
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
310
|
+
}
|
|
311
|
+
return `[${clean(requiredString(entry["occurred_at"], "activity occurrence time"))}] ${description} \xB7 ${provenance}
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
function activityActor(actor) {
|
|
315
|
+
switch (actor["kind"]) {
|
|
316
|
+
case "user":
|
|
317
|
+
return optionalLabel(actor["name"], "Unknown actor", "activity actor name");
|
|
318
|
+
case "system":
|
|
319
|
+
return "System";
|
|
320
|
+
case "deleted_user":
|
|
321
|
+
return "Deleted user";
|
|
322
|
+
case "unknown":
|
|
323
|
+
return "Unknown actor";
|
|
324
|
+
default:
|
|
325
|
+
throw new CommandError(`Unknown activity actor kind: ${clean(actor["kind"])}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function activityProvenance(provenance) {
|
|
329
|
+
switch (provenance["kind"]) {
|
|
330
|
+
case "live":
|
|
331
|
+
case "import":
|
|
332
|
+
case "backfill":
|
|
333
|
+
return String(provenance["kind"]);
|
|
334
|
+
default:
|
|
335
|
+
throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function stateName(value, field2) {
|
|
339
|
+
return optionalLabel(requiredRecord(value, field2)["name"], "Unknown state", `${field2} name`);
|
|
340
|
+
}
|
|
341
|
+
function nullableStateName(value, field2) {
|
|
342
|
+
return value === null ? "Unknown state" : stateName(value, field2);
|
|
343
|
+
}
|
|
344
|
+
function optionalLabel(value, fallback, field2) {
|
|
345
|
+
if (value === null || value === "") return fallback;
|
|
346
|
+
return clean(requiredString(value, field2));
|
|
347
|
+
}
|
|
348
|
+
function requiredRecord(value, field2) {
|
|
349
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
350
|
+
throw new CommandError(`Invalid ${field2}.`);
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function requiredString(value, field2) {
|
|
355
|
+
if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field2}.`);
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
258
358
|
function stats(data) {
|
|
259
359
|
const teams = data["teams"] ?? [];
|
|
260
360
|
const totals = data["totals"] ?? {};
|
|
@@ -594,6 +694,61 @@ async function show(ctx, identifier, opts) {
|
|
|
594
694
|
`).join("");
|
|
595
695
|
return notice === "" ? { json: json2, human } : { json: json2, human, notice };
|
|
596
696
|
}
|
|
697
|
+
var ACTIVITY_MAX_LIMIT = 100;
|
|
698
|
+
async function activity(ctx, identifier, opts) {
|
|
699
|
+
requireIdentifiers([identifier]);
|
|
700
|
+
let page = await activityPage(ctx, identifier, opts.limit);
|
|
701
|
+
let entries = page.entries;
|
|
702
|
+
if (opts.all === true) {
|
|
703
|
+
const pages = [page.entries];
|
|
704
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
705
|
+
while (page.hasMore) {
|
|
706
|
+
const cursor = page.nextCursor;
|
|
707
|
+
if (seenCursors.has(cursor)) {
|
|
708
|
+
throw new CommandError("Activity response repeated a cursor.");
|
|
709
|
+
}
|
|
710
|
+
seenCursors.add(cursor);
|
|
711
|
+
page = await activityPage(ctx, identifier, opts.limit, cursor);
|
|
712
|
+
pages.push(page.entries);
|
|
713
|
+
}
|
|
714
|
+
entries = pages.reverse().flat();
|
|
715
|
+
}
|
|
716
|
+
const notice = opts.all !== true && page.hasMore ? `harness: showing the newest ${entries.length} activity entries \u2014 pass --all for older activity.
|
|
717
|
+
` : void 0;
|
|
718
|
+
return { json: entries, human: activityEntries(entries), notice };
|
|
719
|
+
}
|
|
720
|
+
async function activityPage(ctx, identifier, limit, before) {
|
|
721
|
+
const body = await ctx.client.get(`/api/v1/issues/${identifier}/activity`, { limit, before });
|
|
722
|
+
if (!Array.isArray(body.data)) throw new CommandError("Activity response data is not an array.");
|
|
723
|
+
if (body.data.some((entry) => typeof entry !== "object" || entry === null || Array.isArray(entry))) {
|
|
724
|
+
throw new CommandError("Activity response contains a non-object entry.");
|
|
725
|
+
}
|
|
726
|
+
const meta = body.meta;
|
|
727
|
+
if (typeof meta !== "object" || meta === null) {
|
|
728
|
+
throw new CommandError("Activity response has no pagination metadata.");
|
|
729
|
+
}
|
|
730
|
+
const hasMore = meta.has_more;
|
|
731
|
+
const nextCursor = meta.next_cursor;
|
|
732
|
+
if (typeof hasMore !== "boolean") {
|
|
733
|
+
throw new CommandError("Activity response has an invalid has_more value.");
|
|
734
|
+
}
|
|
735
|
+
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
736
|
+
throw new CommandError("Activity response has an invalid next_cursor value.");
|
|
737
|
+
}
|
|
738
|
+
if (hasMore) {
|
|
739
|
+
if (nextCursor === null || nextCursor === "") {
|
|
740
|
+
throw new CommandError("Activity response says older entries exist but provides no cursor.");
|
|
741
|
+
}
|
|
742
|
+
if (body.data.length === 0) {
|
|
743
|
+
throw new CommandError("Activity response says older entries exist but contains no entries.");
|
|
744
|
+
}
|
|
745
|
+
return { entries: body.data, hasMore: true, nextCursor };
|
|
746
|
+
}
|
|
747
|
+
if (nextCursor !== null) {
|
|
748
|
+
throw new CommandError("Activity response provides a cursor when no older entries exist.");
|
|
749
|
+
}
|
|
750
|
+
return { entries: body.data, hasMore: false, nextCursor: null };
|
|
751
|
+
}
|
|
597
752
|
async function stats2(ctx) {
|
|
598
753
|
const body = await ctx.client.get("/api/v1/stats");
|
|
599
754
|
return { json: body.data, human: stats(body.data) };
|
|
@@ -992,19 +1147,19 @@ async function claimFirst(ctx, issues) {
|
|
|
992
1147
|
}
|
|
993
1148
|
throw new CommandError("No claimable ready issues.");
|
|
994
1149
|
}
|
|
995
|
-
async function setState(ctx, identifier,
|
|
1150
|
+
async function setState(ctx, identifier, stateName2) {
|
|
996
1151
|
const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
|
|
997
1152
|
const teamKey = str2((showBody.data["team"] ?? {})["key"]);
|
|
998
1153
|
const statesBody = await ctx.client.get(
|
|
999
1154
|
`/api/v1/teams/${teamKey}/workflow_states`
|
|
1000
1155
|
);
|
|
1001
|
-
const wanted =
|
|
1156
|
+
const wanted = stateName2.toLowerCase();
|
|
1002
1157
|
const state = statesBody.data.find(
|
|
1003
1158
|
(candidate) => str2(candidate["name"]).toLowerCase() === wanted || str2(candidate["type"]) === wanted
|
|
1004
1159
|
);
|
|
1005
1160
|
if (state === void 0) {
|
|
1006
1161
|
const names = statesBody.data.map((candidate) => str2(candidate["name"])).join(", ");
|
|
1007
|
-
throw new CommandError(`No workflow state matches "${
|
|
1162
|
+
throw new CommandError(`No workflow state matches "${stateName2}". States: ${names}`);
|
|
1008
1163
|
}
|
|
1009
1164
|
const body = await ctx.client.put(`/api/v1/issues/${identifier}/state`, { state_id: state["id"] });
|
|
1010
1165
|
return body.data;
|
|
@@ -1951,6 +2106,8 @@ export {
|
|
|
1951
2106
|
list,
|
|
1952
2107
|
search,
|
|
1953
2108
|
show,
|
|
2109
|
+
ACTIVITY_MAX_LIMIT,
|
|
2110
|
+
activity,
|
|
1954
2111
|
stats2 as stats,
|
|
1955
2112
|
prime,
|
|
1956
2113
|
version,
|