@higherdev/cli 0.15.4 → 0.17.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
@@ -25,7 +25,7 @@ workspace-map config shapes are migrated automatically when they are read.
25
25
  | `hd --help` | Show the HigherDEV banner and usage |
26
26
  | `hd status` | Show workspace, ticket, run, and decision status |
27
27
  | `hd ticket list` | List tickets |
28
- | `hd ticket show KEY` | Show one ticket |
28
+ | `hd ticket show KEY [--json]` | Show one ticket (body, acceptance, timeline, messages, PR, runs, decisions) |
29
29
  | `hd ticket new [PATH.md]` | Create a ticket from the guided form or a Markdown spec |
30
30
  | `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket non-interactively |
31
31
  | `hd ticket queue KEY` | Queue a complete ticket now |
@@ -54,6 +54,7 @@ workspace-map config shapes are migrated automatically when they are read.
54
54
  | `hd host env rm NAME` | Remove a host-only environment variable |
55
55
  | `hd logs KEY [-f]` | Show or follow run events |
56
56
  | `hd msg KEY "message" [--interrupt]` | Message a builder |
57
+ | `hd inbox [--all] [--limit N] [--json]` | List unread inbox messages, or earlier history with `--all` |
57
58
  | `hd decide` | List open decisions |
58
59
  | `hd decide ID --answer TEXT` | Answer a decision |
59
60
  | `hd on` / `hd off` | Turn the workspace on or off |
@@ -95,7 +96,7 @@ Use `hd host env set VERCEL_TOKEN=... VERCEL_TEAM_ID=...` to let the runner adop
95
96
  Vercel project. It syncs `NEXT_PUBLIC_*`, names listed in `settings.vercel.env`, and existing Vercel-held names
96
97
  to production and preview. Later `hd env set` changes wake the orchestrator to update matching Vercel variables.
97
98
 
98
- Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/ticket new [PATH.md]`, `/queue`, `/cancel`, `/msg`, `/logs`, `/epic new`,
99
+ Inside the TUI, use `/board`, `/inbox`, `/inbox more`, `/ticket`, `/ticket new [PATH.md]`, `/queue`, `/cancel`, `/msg`, `/logs`, `/epic new`,
99
100
  `/epic approve`, `/epic rm`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
100
101
  `/workspace new`, `/workspace set`, `/workspace rotate-key`, `/workspace grant-runner-access`, `/feed`,
101
102
  `/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
@@ -0,0 +1,80 @@
1
+ import { createAgent, getStatus, listAgents } from "./api.js";
2
+ import { promptOnStdin } from "./prompt.js";
3
+ export const AGENT_ADD_USAGE = "usage: hd agents add [ROLE] --name NAME --provider P --model M --effort E";
4
+ const EFFORTS = ["low", "medium", "high"];
5
+ const ROLES = ["architect", "orchestrator", "reviewer", "builder"];
6
+ function parse(argv) {
7
+ const opts = {};
8
+ const rest = [];
9
+ const missingValues = [];
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const arg = argv[index];
12
+ if (!arg.startsWith("--")) {
13
+ rest.push(arg);
14
+ continue;
15
+ }
16
+ const name = arg.slice(2);
17
+ const value = argv[index + 1];
18
+ if (!value || value.startsWith("--")) {
19
+ missingValues.push(name);
20
+ continue;
21
+ }
22
+ opts[name] = value;
23
+ index += 1;
24
+ }
25
+ return { opts, rest, missingValues };
26
+ }
27
+ export function nextBuilderName(agents) {
28
+ return `Builder ${agents.filter((agent) => agent.role === "builder").length + 1}`;
29
+ }
30
+ export function availableProvider(capabilities, requested) {
31
+ const capability = capabilities.find((item) => item.provider === requested);
32
+ if (!capability?.available)
33
+ throw new Error(capability?.reason ?? `Unsupported provider ${requested}.`);
34
+ return capability;
35
+ }
36
+ function choose(answer, choices) {
37
+ if (!answer)
38
+ return choices[0];
39
+ return /^\d+$/.test(answer) ? choices[Number(answer) - 1] : answer;
40
+ }
41
+ export async function agentAdd(argv, deps = {}) {
42
+ const { opts, rest, missingValues } = parse(argv);
43
+ if (missingValues.length || rest.length > 1 || Object.keys(opts).some((key) => !["name", "provider", "model", "effort"].includes(key)))
44
+ throw new Error(AGENT_ADD_USAGE);
45
+ const role = rest[0] ?? "builder";
46
+ if (!ROLES.includes(role))
47
+ throw new Error(AGENT_ADD_USAGE);
48
+ const interactive = deps.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
49
+ const needsForm = ["name", "provider", "model", "effort"].some((field) => !opts[field]);
50
+ if (needsForm && !interactive)
51
+ throw new Error(AGENT_ADD_USAGE);
52
+ const [{ agents }, status] = await Promise.all([
53
+ (deps.listAgents ?? listAgents)(deps.config), (deps.getStatus ?? getStatus)(deps.config),
54
+ ]);
55
+ const prompt = deps.prompt ?? promptOnStdin;
56
+ const defaultName = nextBuilderName(agents);
57
+ const name = opts.name?.trim() || (await prompt(`Name [${defaultName}]: `)).trim() || defaultName;
58
+ const capabilities = status.workspace.provider_capabilities ?? [];
59
+ if (opts.provider?.trim())
60
+ availableProvider(capabilities, opts.provider.trim());
61
+ const providers = capabilities.filter((item) => item.available).map((item) => item.provider);
62
+ if (!providers.length && !opts.provider) {
63
+ throw new Error(`No providers are available on runner host ${status.workspace.default_host}.`);
64
+ }
65
+ const providerAnswer = opts.provider?.trim() || (await prompt(`Provider (${providers.map((item, index) => `${index + 1}=${item}`).join(", ")}) [${providers[0]}]: `)).trim();
66
+ const provider = choose(providerAnswer, providers);
67
+ if (!provider)
68
+ throw new Error("Provider is required.");
69
+ availableProvider(capabilities, provider);
70
+ const model = opts.model?.trim() || (await prompt("Model: ")).trim();
71
+ if (!model)
72
+ throw new Error("Model is required.");
73
+ const effortAnswer = opts.effort?.trim() || (await prompt("Effort (1=low, 2=medium, 3=high) [high]: ")).trim();
74
+ const effort = choose(effortAnswer || "high", [...EFFORTS]);
75
+ if (!effort || !EFFORTS.includes(effort)) {
76
+ throw new Error(`Choose effort from: ${EFFORTS.join(", ")}.`);
77
+ }
78
+ return (deps.createAgent ?? createAgent)({ role, provider, model, effort,
79
+ display_name: name }, deps.config);
80
+ }
package/dist/api.js CHANGED
@@ -58,14 +58,19 @@ export async function queueTicket(key, config = loadConfig()) {
58
58
  export async function cancelTicket(key, config = loadConfig()) {
59
59
  return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/cancel`);
60
60
  }
61
- export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
61
+ export async function listTicketRunEvents(key, afterAt, afterId, config = loadConfig()) {
62
62
  const query = new URLSearchParams();
63
63
  if (afterAt)
64
64
  query.set("after_at", afterAt);
65
65
  if (afterId)
66
66
  query.set("after_id", afterId);
67
- const suffix = query.size ? `?${query.toString()}` : "";
68
- return request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events${suffix}`);
67
+ const suffix = query.size ? `&${query.toString()}` : "";
68
+ const result = await request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events?stream=1${suffix}`);
69
+ const runs = new Map((result.runs ?? []).map((run) => [run.id, run]));
70
+ return { ...result, events: result.events.map((event) => ({ ...event, run: runs.get(event.run_id) })) };
71
+ }
72
+ export async function listTicketStory(key, config = loadConfig()) {
73
+ return request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events`);
69
74
  }
70
75
  export async function postMessage(fields, config = loadConfig()) {
71
76
  return request(config, "POST", `/api/w/${config.slug}/messages`, fields);
@@ -143,10 +148,16 @@ export async function listMessages(options = {}, config = loadConfig()) {
143
148
  query.set("since", options.since);
144
149
  if (options.limit)
145
150
  query.set("limit", String(options.limit));
151
+ if (options.offset)
152
+ query.set("offset", String(options.offset));
146
153
  if (options.toRoles?.length)
147
154
  query.set("to_role", options.toRoles.join(","));
148
155
  if (options.undelivered)
149
156
  query.set("undelivered", "true");
157
+ if (options.delivered)
158
+ query.set("delivered", "true");
159
+ if (options.order)
160
+ query.set("order", options.order);
150
161
  const suffix = query.size ? `?${query.toString()}` : "";
151
162
  return request(config, "GET", `/api/w/${config.slug}/messages${suffix}`);
152
163
  }
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
- import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listHostEnv, listWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
4
+ import { approveEpic, answerDecision, cancelTicket, createEpic, deleteAgent, deleteEpic, getStatus, getWorkspace, listAgents, listEpics, listHostEnv, listMessages, listWorkspaceEnv, listTicketRunEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
5
5
  import { initHost, parseHostFlags } from "./host.js";
6
6
  import { login, parseLoginFlags } from "./login.js";
7
7
  import { loadConfig } from "./config.js";
8
8
  import { epicProgressRows, readEpicSpec } from "./epics.js";
9
9
  import { banner, c, statusChip, table, truncate, usage } from "./out.js";
10
10
  import { ticketNew } from "./ticket-commands.js";
11
+ import { ticketViewLines } from "./tui/ticket-view.js";
12
+ import { agentAdd, AGENT_ADD_USAGE } from "./agent-commands.js";
11
13
  import { WORKSPACE_USAGE, workspaceGrantRunnerAccess, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
12
14
  import { defaultGh, hasWriteRepoPermission, HDX_RUNNER_GH_USER, requireAdminRepo, runnerRepoPermission } from "./workspace-preflight.js";
13
15
  function fail(message) {
@@ -53,13 +55,19 @@ function printTickets(tickets) {
53
55
  }
54
56
  console.log(table(["KEY", "STATUS", "PROVIDER", "TITLE", "WHY"], tickets.map((ticket) => [
55
57
  c.bold(ticket.key), statusChip(ticket.status), ticket.provider ?? c.dim("-"),
56
- truncate(ticket.title, 48), ticket.stuck_reason ? c.yellow(truncate(ticket.stuck_reason, 40)) : "",
58
+ truncate(ticket.title, 48), ticket.latest_headline ? c.yellow(truncate(ticket.latest_headline, 40))
59
+ : ticket.stuck_reason ? c.yellow(truncate(ticket.stuck_reason, 40)) : "",
57
60
  ])));
58
61
  }
59
62
  async function cmdStatus() {
60
63
  const data = await getStatus();
61
64
  const state = data.workspace.paused ? c.yellow("off") : c.green("on");
62
65
  console.log(`${c.bold(data.workspace.name)} ${c.dim(data.workspace.repo)} ${state}\n`);
66
+ const waiting = [...new Set(data.tickets.map((ticket) => ticket.stuck_reason
67
+ ?.match(/^Waiting on (\w+) until (\d{1,2}:\d{2})\.$/)).filter(Boolean)
68
+ .map((match) => `${match?.[1]} waiting until ${match?.[2]}`))];
69
+ if (waiting.length)
70
+ console.log(`${c.bold("Providers")} ${waiting.map((line) => c.yellow(line)).join(", ")}\n`);
63
71
  printTickets(data.tickets);
64
72
  if (data.live_runs.length > 0) {
65
73
  console.log(`\n${c.bold("Live runs")}`);
@@ -79,23 +87,31 @@ async function cmdTicket(argv, deps = {}) {
79
87
  return;
80
88
  }
81
89
  if (action === "show") {
82
- const key = rest[0];
83
- if (!key)
84
- fail("usage: hd ticket show KEY");
85
- const { ticket } = await showTicket(key);
86
- console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)} ${ticket.title}`);
87
- if (ticket.stuck_reason)
88
- console.log(`stuck: ${ticket.stuck_reason}`);
89
- if (ticket.provider)
90
- console.log(`provider: ${ticket.provider}`);
91
- if (ticket.area)
92
- console.log(`area: ${ticket.area}`);
93
- if (ticket.pr_url)
94
- console.log(`pr: ${ticket.pr_url}`);
95
- if (ticket.branch)
96
- console.log(`branch: ${ticket.branch}`);
97
- if (ticket.body_md)
98
- console.log(`\n${ticket.body_md}`);
90
+ const parsed = flags(rest);
91
+ const key = parsed.rest[0];
92
+ if (!key || parsed.rest.length !== 1)
93
+ fail("usage: hd ticket show KEY [--json]");
94
+ const data = await showTicket(key.toUpperCase());
95
+ if (parsed.bools.has("json")) {
96
+ console.log(JSON.stringify(data));
97
+ return;
98
+ }
99
+ const { ticket, pr, events, runs, messages, decisions } = data;
100
+ const width = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
101
+ for (const line of ticketViewLines({
102
+ ...ticket,
103
+ stuck: ticket.stuck_reason,
104
+ pr,
105
+ timeline: events,
106
+ messages,
107
+ runs: runs.map((run) => ({
108
+ id: run.id, kind: run.kind, provider: run.provider, status: run.status,
109
+ summary: run.summary, elapsed_ms: run.elapsed_ms,
110
+ })),
111
+ decisions,
112
+ }, width)) {
113
+ console.log(line.text);
114
+ }
99
115
  return;
100
116
  }
101
117
  if (action === "new") {
@@ -176,7 +192,7 @@ async function cmdLogs(argv) {
176
192
  let afterId;
177
193
  const follow = bools.has("follow");
178
194
  async function tick() {
179
- const { events } = await listTicketEvents(key, afterAt, afterId);
195
+ const { events } = await listTicketRunEvents(key, afterAt, afterId);
180
196
  let printed = false;
181
197
  for (const event of events) {
182
198
  if (seen.has(event.id))
@@ -209,6 +225,36 @@ async function cmdMsg(argv) {
209
225
  });
210
226
  console.log(`sent ${message.id}`);
211
227
  }
228
+ async function cmdInbox(argv) {
229
+ const { rest, opts, bools } = flags(argv);
230
+ if (rest.length || (bools.has("limit") && !opts.limit))
231
+ fail("usage: hd inbox [--all] [--limit N] [--json]");
232
+ const all = bools.has("all");
233
+ const json = bools.has("json");
234
+ const limit = opts.limit ? Number(opts.limit) : 50;
235
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500)
236
+ fail("usage: hd inbox [--all] [--limit N] [--json]");
237
+ const { messages } = await listMessages({
238
+ toRoles: ["human", "all"],
239
+ limit,
240
+ ...(all ? { order: "desc" } : { undelivered: true }),
241
+ });
242
+ if (json) {
243
+ console.log(JSON.stringify({ messages }));
244
+ return;
245
+ }
246
+ if (!messages.length) {
247
+ console.log(all ? "No messages." : "No unread messages.");
248
+ return;
249
+ }
250
+ for (const message of messages) {
251
+ const sender = message.from_name?.trim() || message.from_role;
252
+ const ticket = message.ticket_key?.trim();
253
+ console.log([message.created_at, sender, ticket].filter(Boolean).join(" "));
254
+ console.log(message.body_md);
255
+ console.log("");
256
+ }
257
+ }
212
258
  async function cmdDecide(argv) {
213
259
  const { rest, opts } = flags(argv);
214
260
  if (rest.length === 0) {
@@ -293,7 +339,7 @@ function selectAgent(agents, target, provider) {
293
339
  fail(`No unambiguous agent ${target}${provider ? ` for provider ${provider}` : ""}.`);
294
340
  return selected;
295
341
  }
296
- async function cmdAgents(argv) {
342
+ async function cmdAgents(argv, deps = {}) {
297
343
  const [action, ...rest] = argv;
298
344
  if (!action) {
299
345
  const { agents } = await listAgents();
@@ -307,14 +353,11 @@ async function cmdAgents(argv) {
307
353
  ])));
308
354
  return;
309
355
  }
310
- const agentUsage = "usage: hd agents add ROLE --provider P --model M [--effort E] [--name N] | rm ROLE|ID | set ROLE|ID [flags]";
356
+ const agentUsage = `${AGENT_ADD_USAGE} | hd agents rm ROLE|ID | hd agents set ROLE|ID [flags]`;
311
357
  const { rest: args, opts, bools } = flags(rest);
312
358
  const target = args[0];
313
359
  if (action === "add") {
314
- if (!target || !opts.provider || !opts.model)
315
- fail(agentUsage);
316
- const { agent } = await createAgent({ role: target, provider: opts.provider, model: opts.model,
317
- effort: opts.effort, display_name: opts.name ?? opts.provider });
360
+ const { agent } = await agentAdd(rest, deps);
318
361
  console.log(`${agent.id} ${agent.display_name} ${agent.role} ${agent.provider}`);
319
362
  return;
320
363
  }
@@ -339,11 +382,26 @@ async function cmdAgents(argv) {
339
382
  const { agent } = await updateAgent(current.id, fields);
340
383
  console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
341
384
  }
385
+ export function mailerCapsRow(auth) {
386
+ if (auth === "smtp")
387
+ return "mailer: custom SMTP";
388
+ if (auth === "built-in" || auth === "configured")
389
+ return "mailer: built-in (2/h)";
390
+ return null;
391
+ }
392
+ export function previewCapsRow(preview) {
393
+ return `preview: ${preview === "bypass" ? "bypass" : "protected"}`;
394
+ }
342
395
  async function cmdCaps(argv) {
343
396
  const [action, provider, raw, ...extra] = argv;
344
397
  if (!action) {
345
- const caps = (await getStatus()).workspace.provider_caps;
398
+ const [status, detail] = await Promise.all([getStatus(), getWorkspace()]);
399
+ const caps = status.workspace.provider_caps ?? {};
346
400
  console.log(table(["PROVIDER", "CAP"], Object.entries(caps).sort().map(([name, cap]) => [name, String(cap)])));
401
+ const mailer = mailerCapsRow(detail.workspace.settings?.infra?.auth);
402
+ if (mailer)
403
+ console.log(mailer);
404
+ console.log(previewCapsRow(detail.workspace.settings?.infra?.vercel_preview));
347
405
  return;
348
406
  }
349
407
  const cap = Number(raw);
@@ -490,6 +548,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
490
548
  await cmdMsg(rest);
491
549
  return;
492
550
  }
551
+ if (cmd === "inbox") {
552
+ await cmdInbox(rest);
553
+ return;
554
+ }
493
555
  if (cmd === "decide") {
494
556
  await cmdDecide(rest);
495
557
  return;
@@ -499,7 +561,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
499
561
  return;
500
562
  }
501
563
  if (cmd === "agents") {
502
- await cmdAgents(rest);
564
+ await cmdAgents(rest, deps);
503
565
  return;
504
566
  }
505
567
  if (cmd === "caps") {
package/dist/out.js CHANGED
@@ -64,7 +64,7 @@ export function usage() {
64
64
  return [
65
65
  c.bold("Usage"),
66
66
  ` ${c.blue("hd status")} workspace overview`,
67
- ` ${c.blue("hd ticket list | show | new [PATH] | queue | cancel")} ticket operations`,
67
+ ` ${c.blue("hd ticket list | show KEY [--json] | new [PATH] | queue | cancel")} ticket operations`,
68
68
  ` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
69
69
  ` ${c.blue("hd plan")} use /architect in the TUI`,
70
70
  ` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
@@ -74,6 +74,7 @@ export function usage() {
74
74
  ` ${c.blue("hd host env ls | set | rm")} host environment`,
75
75
  ` ${c.blue("hd logs KEY [-f]")} run events`,
76
76
  ` ${c.blue("hd msg KEY TEXT")} message a builder`,
77
+ ` ${c.blue("hd inbox [--all] [--limit N] [--json]")} inbox messages`,
77
78
  ` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
78
79
  ` ${c.blue("hd on | hd off")} workspace switch`,
79
80
  ` ${c.blue("hd login --url URL --api-key KEY")} client setup`,