@bli-cockpit/cli 0.2.56 → 0.2.58

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.
Files changed (43) hide show
  1. package/dist/commands/agent-door.js +85 -0
  2. package/dist/commands/docs.js +227 -0
  3. package/dist/commands/issue-contracts.js +99 -0
  4. package/dist/commands/issue-write.js +129 -0
  5. package/dist/commands/issue.js +189 -0
  6. package/dist/commands/local-args-tower-docs-msg.js +126 -0
  7. package/dist/commands/local-args-tower-work.js +178 -0
  8. package/dist/commands/local-args-tower.js +7 -1
  9. package/dist/commands/local-args.js +10 -2
  10. package/dist/commands/local-help.js +70 -0
  11. package/dist/commands/local.js +12 -0
  12. package/dist/commands/mcp-bin-resolve.js +102 -0
  13. package/dist/commands/memory-install-claude.js +13 -5
  14. package/dist/commands/memory-install-config.js +140 -0
  15. package/dist/commands/memory-install-report.js +89 -0
  16. package/dist/commands/memory-install.js +51 -362
  17. package/dist/commands/msg.js +188 -0
  18. package/dist/commands/notes-door.js +120 -0
  19. package/dist/commands/notes-reads.js +134 -0
  20. package/dist/commands/notes-writes.js +208 -0
  21. package/dist/commands/notes.js +16 -442
  22. package/dist/commands/ops-render.js +18 -2
  23. package/dist/commands/ops.js +9 -2
  24. package/dist/commands/project.js +38 -0
  25. package/dist/commands/public-root.js +1 -1
  26. package/dist/commands/tower-mcp-claude.js +30 -0
  27. package/dist/commands/tower-mcp-codex.js +100 -0
  28. package/dist/commands/tower-mcp-contract.js +39 -0
  29. package/dist/commands/tower-mcp-install.js +75 -0
  30. package/dist/repo-identity-fingerprint.js +88 -0
  31. package/dist/repo-identity-git.js +76 -0
  32. package/dist/repo-identity-linked-worktrees.js +81 -0
  33. package/dist/repo-identity.js +5 -222
  34. package/dist/upload-envelope-build.js +240 -0
  35. package/dist/upload-envelope-event.js +198 -0
  36. package/dist/upload-envelope.js +16 -427
  37. package/dist/upload-ingest-receipt.js +121 -0
  38. package/dist/upload-session-reports-queue.js +156 -0
  39. package/dist/upload-session-reports-wire.js +275 -0
  40. package/dist/upload-session-reports.js +14 -425
  41. package/dist/upload-sync.js +291 -0
  42. package/dist/upload.js +24 -396
  43. package/package.json +6 -5
@@ -0,0 +1,85 @@
1
+ /**
2
+ * One request, and a refusal that keeps the DOOR'S own words (BLI-3706).
3
+ *
4
+ * `docs`/`msg` answer a refusal with `{ code: "BLI-E<status>", reason, message }`
5
+ * (`lib/docs/api-doors.ts` / `lib/msg/api-doors.ts`), a different shape from
6
+ * the meeting-notes doors' `{ headline, lines, reason }` that `notes.ts`'s own
7
+ * `ask()` reads. This is that same discipline for the newer shape: the
8
+ * `reason` label a door chose travels to the terminal UNCHANGED, and `message`
9
+ * is the sentence a person reads — never a second wording invented here.
10
+ *
11
+ * `cockpit docs` and `cockpit msg` both call this rather than keeping two
12
+ * copies, because the shape is identical on both surfaces (BLI-3654 Wave 1a
13
+ * and Wave 3a share `resolveCaller` + a `{code,reason,message}` refusal).
14
+ */
15
+ import { writeLine } from "./cli-io.js";
16
+ import { loadPairedSession, towerFailureDetail, towerRequest, } from "../tower-client.js";
17
+ import { readResponseJson } from "../upload-http.js";
18
+ /** Loads the paired session and builds the door every verb in one command run shares. */
19
+ export async function openAgentDoor(commandName, command, io) {
20
+ const session = await loadPairedSession(commandName, command.homeDir);
21
+ return {
22
+ dashboardUrl: command.dashboardUrl ?? session.dashboard_url,
23
+ deviceToken: session.device_token,
24
+ io,
25
+ json: command.json,
26
+ };
27
+ }
28
+ export async function askAgentDoor(door, options) {
29
+ const result = await towerRequest({
30
+ dashboardUrl: door.dashboardUrl,
31
+ path: options.path,
32
+ deviceToken: door.deviceToken,
33
+ fetch: door.io.fetch,
34
+ method: options.method,
35
+ label: options.label,
36
+ timeoutMs: options.timeoutMs,
37
+ ...(options.body === undefined ? {} : { body: options.body }),
38
+ log: (line) => writeLine(door.io.stderr, line),
39
+ });
40
+ if (!result.ok) {
41
+ const failure = result;
42
+ return { ok: false, reason: failure.reason, detail: towerFailureDetail(failure.reason, failure.detail) };
43
+ }
44
+ const body = await readResponseJson(result.response);
45
+ if (!result.response.ok) {
46
+ const status = result.response.status;
47
+ return {
48
+ ok: false,
49
+ httpStatus: status,
50
+ reason: doorReason(body) ?? `http_${status}`,
51
+ detail: doorMessage(body) ?? `Tower answered ${status} and said nothing about why.`,
52
+ };
53
+ }
54
+ return { ok: true, httpStatus: result.response.status, body };
55
+ }
56
+ /** The door's own `reason` label (`document_not_found_or_unreadable`, `needs_rls_client`, …), verbatim. */
57
+ function doorReason(body) {
58
+ if (!body || typeof body !== "object")
59
+ return null;
60
+ const record = body;
61
+ const value = record["reason"] ?? record["error"];
62
+ return typeof value === "string" && value.trim() !== "" ? value : null;
63
+ }
64
+ function doorMessage(body) {
65
+ if (!body || typeof body !== "object")
66
+ return null;
67
+ const record = body;
68
+ const value = record["message"];
69
+ return typeof value === "string" && value.trim() !== "" ? value : null;
70
+ }
71
+ /** One machine-readable object on stdout, and nothing else on it. */
72
+ export function emitAgentDoor(door, body, exitCode = 0) {
73
+ writeLine(door.io.stdout, JSON.stringify(body));
74
+ return exitCode;
75
+ }
76
+ export function failAgentDoor(door, tag, reason, detail) {
77
+ writeLine(door.io.stderr, `${tag} refused ${JSON.stringify({ reason })}`);
78
+ if (door.json) {
79
+ writeLine(door.io.stdout, JSON.stringify({ ok: false, error: reason, detail }));
80
+ }
81
+ else {
82
+ writeLine(door.io.stderr, detail);
83
+ }
84
+ return 1;
85
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `cockpit docs` — the document library, typed (BLI-3706).
3
+ *
4
+ * Five verbs over `/api/docs/**`, the same doors the `/workbook`… no, the
5
+ * SEPARATE `/api/docs/**` surface (BLI-3654 Wave 3a) that the browser's own
6
+ * document editor reads and writes. Every `/api/docs/**` route already opted
7
+ * into the collector device token via `resolveCaller({ allowDeviceToken: true
8
+ * })` — this command needed no server-side door change, only a terminal in
9
+ * front of the existing one.
10
+ *
11
+ * `read <id|slug>` and `update <id>` both accept a full document id OR its
12
+ * slug. There is no `GET` by slug on the server (`getDocument` reads by id
13
+ * only), so a slug is resolved locally against the flat list `GET
14
+ * /api/docs/documents` already returns — the same read `docs list` prints,
15
+ * never a second query the server does not already answer.
16
+ *
17
+ * A refusal keeps the door's own `reason` label verbatim (`agent-door.ts`):
18
+ * `needs_rls_client`, `document_not_found_or_unreadable`,
19
+ * `document_not_writable`, `title_too_long`, `content_too_long`,
20
+ * `circular_parent`, and so on — never rephrased here.
21
+ */
22
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
23
+ import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
24
+ import { readNoteFile } from "./notes-file.js";
25
+ const TAG = "[docs cli]";
26
+ const READ_DEADLINE_MS = 30_000;
27
+ const WRITE_DEADLINE_MS = 60_000;
28
+ const BODY_MAX_CHARS = 2_000_000;
29
+ export async function runDocs(command, io) {
30
+ const door = await openAgentDoor("docs", command, io);
31
+ switch (command.action) {
32
+ case "list":
33
+ return listDocs(door);
34
+ case "tree":
35
+ return treeDocs(door);
36
+ case "read":
37
+ return readDoc(command, door);
38
+ case "create":
39
+ return createDoc(command, door);
40
+ case "update":
41
+ return updateDoc(command, door);
42
+ }
43
+ }
44
+ async function listDocs(door) {
45
+ const answer = await askAgentDoor(door, {
46
+ path: "/api/docs/documents",
47
+ method: "GET",
48
+ label: "docs list",
49
+ timeoutMs: READ_DEADLINE_MS,
50
+ });
51
+ if (!answer.ok)
52
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
53
+ const documents = (answer.body.documents ?? []);
54
+ if (door.json)
55
+ return emitAgentDoor(door, { ok: true, documents });
56
+ if (documents.length === 0) {
57
+ writeLine(door.io.stdout, "No documents.");
58
+ return 0;
59
+ }
60
+ for (const doc of documents) {
61
+ writeLine(door.io.stdout, `${doc.id} ${doc.visibility.padEnd(7)} ${doc.slug ?? "(no slug)"} ${doc.title}`);
62
+ }
63
+ writeLine(door.io.stdout, "");
64
+ writeLine(door.io.stdout, `${documents.length} document(s).`);
65
+ return 0;
66
+ }
67
+ function renderTree(nodes, io, depth) {
68
+ for (const node of nodes) {
69
+ writeLine(io.stdout, `${" ".repeat(depth)}${node.title} (${node.slug ?? node.id})`);
70
+ if (node.children.length > 0)
71
+ renderTree(node.children, io, depth + 1);
72
+ }
73
+ }
74
+ async function treeDocs(door) {
75
+ const answer = await askAgentDoor(door, {
76
+ path: "/api/docs/tree",
77
+ method: "GET",
78
+ label: "docs tree",
79
+ timeoutMs: READ_DEADLINE_MS,
80
+ });
81
+ if (!answer.ok)
82
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
83
+ const tree = (answer.body.tree ?? []);
84
+ if (door.json)
85
+ return emitAgentDoor(door, { ok: true, tree });
86
+ if (tree.length === 0) {
87
+ writeLine(door.io.stdout, "No documents.");
88
+ return 0;
89
+ }
90
+ renderTree(tree, door.io, 0);
91
+ return 0;
92
+ }
93
+ /** Finds a document id by exact id or exact slug against the flat list. Never fuzzy. */
94
+ async function resolveDocId(door, ref) {
95
+ const answer = await askAgentDoor(door, {
96
+ path: "/api/docs/documents",
97
+ method: "GET",
98
+ label: "docs resolve",
99
+ timeoutMs: READ_DEADLINE_MS,
100
+ });
101
+ if (!answer.ok)
102
+ return { status: "list_failed", reason: answer.reason, detail: answer.detail };
103
+ const documents = (answer.body.documents ?? []);
104
+ const byId = documents.find((doc) => doc.id === ref);
105
+ if (byId)
106
+ return { status: "ok", id: byId.id };
107
+ const bySlug = documents.find((doc) => doc.slug === ref);
108
+ if (bySlug)
109
+ return { status: "ok", id: bySlug.id };
110
+ return { status: "not_found" };
111
+ }
112
+ async function readDoc(command, door) {
113
+ const ref = command.docRef ?? "";
114
+ const resolved = await resolveDocId(door, ref);
115
+ if (resolved.status === "list_failed")
116
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
117
+ if (resolved.status === "not_found") {
118
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot read it: ${ref}`);
119
+ }
120
+ const answer = await askAgentDoor(door, {
121
+ path: `/api/docs/documents/${encodeURIComponent(resolved.id)}`,
122
+ method: "GET",
123
+ label: "docs read",
124
+ timeoutMs: READ_DEADLINE_MS,
125
+ });
126
+ if (!answer.ok)
127
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
128
+ const document = answer.body.document ?? {};
129
+ if (door.json)
130
+ return emitAgentDoor(door, { ok: true, document });
131
+ writeLine(door.io.stdout, String(document["title"] ?? ""));
132
+ writeLine(door.io.stdout, `${document["slug"] ?? "(no slug)"} · ${document["visibility"] ?? ""} · ${document["source"] ?? ""}`);
133
+ writeLine(door.io.stdout, "");
134
+ writeLine(door.io.stdout, String(document["body_markdown"] ?? ""));
135
+ return 0;
136
+ }
137
+ /** Body on stdin, never argv (BLI-3706) — `--file` is the safest way on Windows, same as `cockpit notes paste`. */
138
+ async function readBody(command, io) {
139
+ if (command.filePath) {
140
+ const read = await readNoteFile(command.filePath);
141
+ if (!read.ok)
142
+ return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
143
+ return { ok: true, text: read.bytes.toString("utf8") };
144
+ }
145
+ if (isInteractiveStdin(io))
146
+ return { ok: true, text: "" };
147
+ try {
148
+ const text = await readPipedText(io.stdin, {
149
+ maxChars: BODY_MAX_CHARS,
150
+ overflowMessage: `A document body is limited to ${BODY_MAX_CHARS} characters.`,
151
+ });
152
+ return { ok: true, text };
153
+ }
154
+ catch (error) {
155
+ return { ok: false, reason: "body_too_long", detail: error instanceof Error ? error.message : String(error) };
156
+ }
157
+ }
158
+ async function createDoc(command, door) {
159
+ if (!command.title)
160
+ return failAgentDoor(door, TAG, "invalid_body", "docs create needs --title.");
161
+ const body = await readBody(command, door.io);
162
+ if (!body.ok)
163
+ return failAgentDoor(door, TAG, body.reason, body.detail);
164
+ const answer = await askAgentDoor(door, {
165
+ path: "/api/docs/documents",
166
+ method: "POST",
167
+ label: "docs create",
168
+ timeoutMs: WRITE_DEADLINE_MS,
169
+ body: {
170
+ title: command.title,
171
+ body_markdown: body.text,
172
+ ...(command.parentId ? { parent_id: command.parentId } : {}),
173
+ ...(command.visibility ? { visibility: command.visibility } : {}),
174
+ },
175
+ });
176
+ if (!answer.ok)
177
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
178
+ const document = answer.body.document ?? {};
179
+ writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({ document_id: document["id"] ?? null, byte_size: Buffer.byteLength(body.text, "utf8") })}`);
180
+ if (door.json)
181
+ return emitAgentDoor(door, { ok: true, document });
182
+ writeLine(door.io.stdout, `Created ${String(document["title"] ?? "")} (${String(document["id"] ?? "")}).`);
183
+ return 0;
184
+ }
185
+ async function updateDoc(command, door) {
186
+ const ref = command.docRef ?? "";
187
+ const resolved = await resolveDocId(door, ref);
188
+ if (resolved.status === "list_failed")
189
+ return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
190
+ if (resolved.status === "not_found") {
191
+ return failAgentDoor(door, TAG, "document_not_found_or_unreadable", `That document does not exist, or you cannot write to it: ${ref}`);
192
+ }
193
+ let bodyMarkdown;
194
+ if (command.bodyStdin || command.filePath) {
195
+ const body = await readBody(command, door.io);
196
+ if (!body.ok)
197
+ return failAgentDoor(door, TAG, body.reason, body.detail);
198
+ bodyMarkdown = body.text;
199
+ }
200
+ if (bodyMarkdown === undefined
201
+ && command.title === undefined
202
+ && command.visibility === undefined
203
+ && command.parentId === undefined
204
+ && !command.clearParent) {
205
+ return failAgentDoor(door, TAG, "invalid_body", "docs update needs at least one of --title, --visibility, --parent/--clear-parent, or a body on --body-stdin/--file.");
206
+ }
207
+ const answer = await askAgentDoor(door, {
208
+ path: `/api/docs/documents/${encodeURIComponent(resolved.id)}`,
209
+ method: "PATCH",
210
+ label: "docs update",
211
+ timeoutMs: WRITE_DEADLINE_MS,
212
+ body: {
213
+ ...(command.title !== undefined ? { title: command.title } : {}),
214
+ ...(bodyMarkdown !== undefined ? { body_markdown: bodyMarkdown } : {}),
215
+ ...(command.visibility !== undefined ? { visibility: command.visibility } : {}),
216
+ ...(command.clearParent ? { parent_id: null } : command.parentId ? { parent_id: command.parentId } : {}),
217
+ },
218
+ });
219
+ if (!answer.ok)
220
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
221
+ const document = answer.body.document ?? {};
222
+ writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ document_id: document["id"] ?? resolved.id })}`);
223
+ if (door.json)
224
+ return emitAgentDoor(door, { ok: true, document });
225
+ writeLine(door.io.stdout, `Updated ${String(document["title"] ?? "")} (${String(document["id"] ?? resolved.id)}).`);
226
+ return 0;
227
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * What `cockpit issue` hands between its own halves (BLI-3716): the row
3
+ * shapes the `/api/work/**` doors answer with, the two deadlines, and the
4
+ * three "which one did you mean" resolvers both the read half (`issue.ts`)
5
+ * and the write half (`issue-write.ts`) need.
6
+ *
7
+ * Split out so neither half carries the other's weight — the same table-of-
8
+ * contents discipline `commands/session-sync*.ts` and `commands/backfill*.ts`
9
+ * follow, applied before the file got large rather than after.
10
+ */
11
+ import { askAgentDoor } from "./agent-door.js";
12
+ import { isInteractiveStdin, readPipedText } from "./cli-io.js";
13
+ import { readNoteFile } from "./notes-file.js";
14
+ export const TAG = "[issue cli]";
15
+ export const READ_DEADLINE_MS = 30_000;
16
+ export const WRITE_DEADLINE_MS = 60_000;
17
+ export const BODY_MAX_CHARS = 200_000;
18
+ /** A project id from a project NAME or a uuid — exact after case-folding. */
19
+ export async function resolveProjectId(door, ref) {
20
+ const answer = await askAgentDoor(door, {
21
+ path: "/api/work/projects?include_archived=true",
22
+ method: "GET",
23
+ label: "issue project resolve",
24
+ timeoutMs: READ_DEADLINE_MS,
25
+ });
26
+ if (!answer.ok)
27
+ return { status: "list_failed", reason: answer.reason, detail: answer.detail };
28
+ const projects = answer.body.projects ?? [];
29
+ const wanted = ref.toLowerCase();
30
+ const match = projects.find((project) => project.id === ref || project.name.toLowerCase() === wanted);
31
+ return match ? { status: "ok", id: match.id } : { status: "not_found" };
32
+ }
33
+ /** A parent issue's uuid from any issue reference, via the door's own resolver. */
34
+ export async function resolveIssueUuid(door, ref) {
35
+ const answer = await askAgentDoor(door, {
36
+ path: `/api/work/issues/${encodeURIComponent(ref)}`,
37
+ method: "GET",
38
+ label: "issue parent resolve",
39
+ timeoutMs: READ_DEADLINE_MS,
40
+ });
41
+ if (!answer.ok)
42
+ return { status: "failed", reason: answer.reason, detail: answer.detail };
43
+ const issue = answer.body.issue;
44
+ if (!issue?.id) {
45
+ return {
46
+ status: "failed",
47
+ reason: "issue_not_found_or_unreadable",
48
+ detail: `That issue does not exist, or you cannot read it: ${ref}`,
49
+ };
50
+ }
51
+ return { status: "ok", id: issue.id };
52
+ }
53
+ /** Body on stdin, never argv — `--file` is the safest way on Windows. */
54
+ export async function readBody(command, io) {
55
+ if (command.filePath) {
56
+ const read = await readNoteFile(command.filePath);
57
+ if (!read.ok) {
58
+ return { ok: false, reason: read.refusal, detail: `Could not read ${command.filePath}: ${read.detail}` };
59
+ }
60
+ return { ok: true, text: read.bytes.toString("utf8") };
61
+ }
62
+ if (isInteractiveStdin(io))
63
+ return { ok: true, text: "" };
64
+ try {
65
+ const text = await readPipedText(io.stdin, {
66
+ maxChars: BODY_MAX_CHARS,
67
+ overflowMessage: `An issue description is limited to ${BODY_MAX_CHARS} characters.`,
68
+ });
69
+ return { ok: true, text };
70
+ }
71
+ catch (error) {
72
+ return { ok: false, reason: "description_too_long", detail: error instanceof Error ? error.message : String(error) };
73
+ }
74
+ }
75
+ /** The project/parent halves both `create` and `update` need, resolved once. */
76
+ export async function resolveRefs(command, door) {
77
+ let projectId;
78
+ let parentId;
79
+ if (command.project) {
80
+ const resolved = await resolveProjectId(door, command.project);
81
+ if (resolved.status === "list_failed")
82
+ return { ok: false, reason: resolved.reason, detail: resolved.detail };
83
+ if (resolved.status === "not_found") {
84
+ return {
85
+ ok: false,
86
+ reason: "project_not_found_or_unreadable",
87
+ detail: `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`,
88
+ };
89
+ }
90
+ projectId = resolved.id;
91
+ }
92
+ if (command.parentRef) {
93
+ const resolved = await resolveIssueUuid(door, command.parentRef);
94
+ if (resolved.status === "failed")
95
+ return { ok: false, reason: resolved.reason, detail: resolved.detail };
96
+ parentId = resolved.id;
97
+ }
98
+ return { ok: true, ...(projectId ? { projectId } : {}), ...(parentId ? { parentId } : {}) };
99
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The verbs that CHANGE something: `cockpit issue create|update|move|comment`
3
+ * (BLI-3716). The reads and the router live in `issue.ts`, the shared shapes
4
+ * and resolvers in `issue-contracts.ts`.
5
+ *
6
+ * Every write prints a receipt line on stderr naming what it did and to
7
+ * which row, success included — `--json` owns stdout, so a receipt that went
8
+ * there would corrupt the one machine-readable object a caller parses.
9
+ */
10
+ import { askAgentDoor, emitAgentDoor, failAgentDoor, } from "./agent-door.js";
11
+ import { writeLine } from "./cli-io.js";
12
+ import { TAG, WRITE_DEADLINE_MS, readBody, resolveRefs, } from "./issue-contracts.js";
13
+ export async function createIssue(command, door) {
14
+ if (!command.title)
15
+ return failAgentDoor(door, TAG, "invalid_body", "issue create needs --title.");
16
+ const refs = await resolveRefs(command, door);
17
+ if (!refs.ok)
18
+ return failAgentDoor(door, TAG, refs.reason, refs.detail);
19
+ const body = await readBody(command, door.io);
20
+ if (!body.ok)
21
+ return failAgentDoor(door, TAG, body.reason, body.detail);
22
+ const answer = await askAgentDoor(door, {
23
+ path: "/api/work/issues",
24
+ method: "POST",
25
+ label: "issue create",
26
+ timeoutMs: WRITE_DEADLINE_MS,
27
+ body: {
28
+ title: command.title,
29
+ ...(body.text ? { description: body.text } : {}),
30
+ ...(refs.projectId ? { project_id: refs.projectId } : {}),
31
+ ...(refs.parentId ? { parent_id: refs.parentId } : {}),
32
+ ...(command.priority === undefined ? {} : { priority: command.priority }),
33
+ ...(command.assignee ? { assignee_id: command.assignee } : {}),
34
+ },
35
+ });
36
+ if (!answer.ok)
37
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
38
+ const issue = answer.body.issue;
39
+ writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, description_bytes: Buffer.byteLength(body.text, "utf8") })}`);
40
+ if (door.json)
41
+ return emitAgentDoor(door, { ok: true, issue });
42
+ writeLine(door.io.stdout, `Created ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`);
43
+ return 0;
44
+ }
45
+ export async function updateIssue(command, door) {
46
+ const ref = command.issueRef ?? "";
47
+ const refs = await resolveRefs(command, door);
48
+ if (!refs.ok)
49
+ return failAgentDoor(door, TAG, refs.reason, refs.detail);
50
+ let description;
51
+ if (command.bodyStdin || command.filePath) {
52
+ const body = await readBody(command, door.io);
53
+ if (!body.ok)
54
+ return failAgentDoor(door, TAG, body.reason, body.detail);
55
+ description = body.text;
56
+ }
57
+ if (description === undefined
58
+ && command.title === undefined
59
+ && command.priority === undefined
60
+ && command.assignee === undefined
61
+ && refs.projectId === undefined
62
+ && refs.parentId === undefined) {
63
+ return failAgentDoor(door, TAG, "invalid_body", "issue update needs at least one of --title, --priority, --assignee, --project, --parent, or a description on --body-stdin/--file.");
64
+ }
65
+ const answer = await askAgentDoor(door, {
66
+ path: `/api/work/issues/${encodeURIComponent(ref)}`,
67
+ method: "PATCH",
68
+ label: "issue update",
69
+ timeoutMs: WRITE_DEADLINE_MS,
70
+ body: {
71
+ ...(command.title !== undefined ? { title: command.title } : {}),
72
+ ...(description !== undefined ? { description } : {}),
73
+ ...(command.priority === undefined ? {} : { priority: command.priority }),
74
+ ...(command.assignee ? { assignee_id: command.assignee } : {}),
75
+ ...(refs.projectId ? { project_id: refs.projectId } : {}),
76
+ ...(refs.parentId ? { parent_id: refs.parentId } : {}),
77
+ },
78
+ });
79
+ if (!answer.ok)
80
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
81
+ const issue = answer.body.issue;
82
+ writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null })}`);
83
+ if (door.json)
84
+ return emitAgentDoor(door, { ok: true, issue });
85
+ writeLine(door.io.stdout, `Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`);
86
+ return 0;
87
+ }
88
+ export async function moveIssue(command, door) {
89
+ const ref = command.issueRef ?? "";
90
+ const answer = await askAgentDoor(door, {
91
+ path: `/api/work/issues/${encodeURIComponent(ref)}/state`,
92
+ method: "POST",
93
+ label: "issue move",
94
+ timeoutMs: WRITE_DEADLINE_MS,
95
+ body: { state: command.moveState },
96
+ });
97
+ if (!answer.ok)
98
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
99
+ const issue = answer.body.issue;
100
+ writeLine(door.io.stderr, `${TAG} moved ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, to_state: command.moveState })}`);
101
+ if (door.json)
102
+ return emitAgentDoor(door, { ok: true, issue });
103
+ writeLine(door.io.stdout, `${issue?.identifier ?? ref} is now ${issue?.state ?? command.moveState}.`);
104
+ return 0;
105
+ }
106
+ export async function commentIssue(command, door) {
107
+ const ref = command.issueRef ?? "";
108
+ const body = await readBody(command, door.io);
109
+ if (!body.ok)
110
+ return failAgentDoor(door, TAG, body.reason, body.detail);
111
+ if (body.text.trim() === "") {
112
+ return failAgentDoor(door, TAG, "invalid_body", 'A comment is never taken on the command line — pipe it in: `echo "..." | cockpit issue comment BLI-3654`.');
113
+ }
114
+ const answer = await askAgentDoor(door, {
115
+ path: `/api/work/issues/${encodeURIComponent(ref)}/comments`,
116
+ method: "POST",
117
+ label: "issue comment",
118
+ timeoutMs: WRITE_DEADLINE_MS,
119
+ body: { body_markdown: body.text },
120
+ });
121
+ if (!answer.ok)
122
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
123
+ const comment = answer.body.comment;
124
+ writeLine(door.io.stderr, `${TAG} commented ${JSON.stringify({ comment_id: comment?.id ?? null, byte_size: Buffer.byteLength(body.text, "utf8") })}`);
125
+ if (door.json)
126
+ return emitAgentDoor(door, { ok: true, comment });
127
+ writeLine(door.io.stdout, `Commented on ${ref} (${comment?.id ?? ""}).`);
128
+ return 0;
129
+ }