@higherdev/cli 0.13.0 → 0.14.2

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
@@ -43,6 +43,9 @@ workspace-map config shapes are migrated automatically when they are read.
43
43
  | `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
44
44
  | `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
45
45
  | `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
46
+ | `hd env ls` | List workspace environment variable names |
47
+ | `hd env set NAME=VALUE [NAME=VALUE...]` | Set workspace environment variables |
48
+ | `hd env rm NAME` | Remove a workspace environment variable |
46
49
  | `hd logs KEY [-f]` | Show or follow run events |
47
50
  | `hd msg KEY "message" [--interrupt]` | Message a builder |
48
51
  | `hd decide` | List open decisions |
@@ -64,7 +67,7 @@ the repository is missing, approve private creation interactively, use `--create
64
67
  `--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
65
68
 
66
69
  Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
67
- `/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
70
+ `/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
68
71
  `/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
69
72
  `/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
70
73
  the HDX API every five seconds.
package/dist/api.js CHANGED
@@ -105,6 +105,15 @@ export async function updateAgent(id, fields, config = loadConfig()) {
105
105
  export async function deleteAgent(id, config = loadConfig()) {
106
106
  return request(config, "DELETE", `/api/w/${config.slug}/agents`, { id });
107
107
  }
108
+ export async function listWorkspaceEnv(config = loadConfig()) {
109
+ return request(config, "GET", `/api/w/${config.slug}/env`);
110
+ }
111
+ export async function setWorkspaceEnv(name, value, config = loadConfig()) {
112
+ return request(config, "PUT", `/api/w/${config.slug}/env`, { name, value });
113
+ }
114
+ export async function removeWorkspaceEnv(name, config = loadConfig()) {
115
+ return request(config, "DELETE", `/api/w/${config.slug}/env`, { name });
116
+ }
108
117
  export async function listEpics(config = loadConfig()) {
109
118
  return request(config, "GET", `/api/w/${config.slug}/epics`);
110
119
  }
@@ -125,9 +134,16 @@ export async function listMessages(options = {}, config = loadConfig()) {
125
134
  query.set("since", options.since);
126
135
  if (options.limit)
127
136
  query.set("limit", String(options.limit));
137
+ if (options.toRoles?.length)
138
+ query.set("to_role", options.toRoles.join(","));
139
+ if (options.undelivered)
140
+ query.set("undelivered", "true");
128
141
  const suffix = query.size ? `?${query.toString()}` : "";
129
142
  return request(config, "GET", `/api/w/${config.slug}/messages${suffix}`);
130
143
  }
144
+ export async function markMessagesDelivered(ids, config = loadConfig()) {
145
+ return request(config, "PATCH", `/api/w/${config.slug}/messages`, { ids });
146
+ }
131
147
  export async function updateCaps(provider_caps, config = loadConfig()) {
132
148
  return request(config, "PATCH", `/api/w/${config.slug}/caps`, { provider_caps });
133
149
  }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
4
+ import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, setPaused, setWorkspaceEnv, 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";
@@ -344,6 +344,32 @@ async function cmdCaps(argv) {
344
344
  const result = await updateCaps({ [provider]: cap });
345
345
  console.log(`${provider} ${result.provider_caps[provider]}`);
346
346
  }
347
+ async function cmdEnv(argv) {
348
+ const [action, ...args] = argv;
349
+ if (action === "ls") {
350
+ const { env } = await listWorkspaceEnv();
351
+ if (!env.length)
352
+ return console.log(c.dim("No workspace environment variables."));
353
+ console.log(table(["NAME", "UPDATED"], env.map((row) => [row.name, row.updated_at])));
354
+ return;
355
+ }
356
+ if (action === "set" && args.length) {
357
+ for (const assignment of args) {
358
+ const at = assignment.indexOf("=");
359
+ if (at < 1)
360
+ fail("usage: hd env set NAME=VALUE [NAME=VALUE...]");
361
+ await setWorkspaceEnv(assignment.slice(0, at), assignment.slice(at + 1));
362
+ }
363
+ console.log(`set ${args.length} variable${args.length === 1 ? "" : "s"}`);
364
+ return;
365
+ }
366
+ if (action === "rm" && args.length === 1) {
367
+ await removeWorkspaceEnv(args[0]);
368
+ console.log(`removed ${args[0]}`);
369
+ return;
370
+ }
371
+ fail("usage: hd env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME");
372
+ }
347
373
  export async function main(argv = process.argv.slice(2), deps = {}) {
348
374
  const [cmd, ...rest] = argv;
349
375
  try {
@@ -405,6 +431,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
405
431
  await cmdCaps(rest);
406
432
  return;
407
433
  }
434
+ if (cmd === "env") {
435
+ await cmdEnv(rest);
436
+ return;
437
+ }
408
438
  if (cmd === "pause" || cmd === "off") {
409
439
  await setPaused(true);
410
440
  console.log(cmd === "off" ? "off" : "paused");
package/dist/out.js CHANGED
@@ -70,6 +70,7 @@ export function usage() {
70
70
  ` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
71
71
  ` ${c.blue("hd agents [add | rm | set]")} manage agents`,
72
72
  ` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
73
+ ` ${c.blue("hd env ls | set | rm")} workspace environment`,
73
74
  ` ${c.blue("hd logs KEY [-f]")} run events`,
74
75
  ` ${c.blue("hd msg KEY TEXT")} message a builder`,
75
76
  ` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
package/dist/tui/App.js CHANGED
@@ -18,7 +18,7 @@ import { alertOnce } from "./alert.js";
18
18
  import { bubbleRows } from "./height.js";
19
19
  import { planLayout, splitPanels } from "./layout.js";
20
20
  import { parseLine } from "./parse.js";
21
- import { configuredSlugs, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
21
+ import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
22
22
  import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
23
23
  import { appendLines, runLabels, toStreamLines } from "./stream.js";
24
24
  import { UI } from "./theme.js";
@@ -57,6 +57,7 @@ export function App({ initial }) {
57
57
  const history = useRef([]);
58
58
  const historyAt = useRef(-1);
59
59
  const refreshRef = useRef(null);
60
+ const acknowledgedMessages = useRef(new Set());
60
61
  const loads = useRef(new WorkspaceLoads(initial.workspace.id));
61
62
  editingRef.current = editing;
62
63
  useEffect(() => {
@@ -143,6 +144,19 @@ export function App({ initial }) {
143
144
  useEffect(() => {
144
145
  setInboxFocus((current) => Math.max(0, Math.min(inbox.length - 1, current)));
145
146
  }, [inbox.length]);
147
+ useEffect(() => {
148
+ if (view !== "inbox")
149
+ return;
150
+ const ids = (board.messages ?? []).map((message) => message.id)
151
+ .filter((id) => !acknowledgedMessages.current.has(id));
152
+ if (!ids.length)
153
+ return;
154
+ ids.forEach((id) => acknowledgedMessages.current.add(id));
155
+ void acknowledgeInbox(config, ids).catch((error) => {
156
+ ids.forEach((id) => acknowledgedMessages.current.delete(id));
157
+ setNotice(error instanceof Error ? error.message : String(error));
158
+ });
159
+ }, [view, board.messages, config]);
146
160
  const refresh = useCallback(async () => {
147
161
  await refreshRef.current?.();
148
162
  }, []);
@@ -186,6 +200,7 @@ export function App({ initial }) {
186
200
  setBoard(snapshot.board);
187
201
  setFeed(snapshot.feed);
188
202
  setStream([]);
203
+ acknowledgedMessages.current.clear();
189
204
  setCursor(null);
190
205
  selectedRef.current = null;
191
206
  setView("home");
@@ -472,6 +487,19 @@ export function App({ initial }) {
472
487
  }
473
488
  return;
474
489
  }
490
+ case "env":
491
+ setBusy(true);
492
+ try {
493
+ const { env } = await listWorkspaceEnv(config);
494
+ say("system", env.length ? env.map((row) => row.name).join("\n") : "No workspace environment variables.");
495
+ }
496
+ catch (error) {
497
+ setNotice(error instanceof Error ? error.message : String(error));
498
+ }
499
+ finally {
500
+ setBusy(false);
501
+ }
502
+ return;
475
503
  case "decide": {
476
504
  const decision = board.decisions[0];
477
505
  if (!decision) {
@@ -117,10 +117,10 @@ export function AgentsColumn({ board, width, rows, }) {
117
117
  ]
118
118
  : []),
119
119
  ...shown.map((row) => {
120
- const tone = row.run ? "blue" : row.state === "offline" ? "warning" : "muted";
120
+ const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited" ? "warning" : "muted";
121
121
  return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsxs(Text, { color: UI.text, wrap: "truncate", children: [row.name, _jsx(Text, { color: UI.dim, children: row.run
122
122
  ? ` · ${row.ticket?.key ?? row.run.kind} ${elapsed(row.run.started_at ?? row.run.created_at)}`
123
- : ` · ${row.state}` })] })] }, row.key));
123
+ : ` · ${row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state}` })] })] }, row.key));
124
124
  }),
125
125
  _jsx(More, { count: displayRows.length - shown.length }, "more"),
126
126
  ] }));
package/dist/tui/Help.js CHANGED
@@ -16,6 +16,7 @@ export const COMMANDS = [
16
16
  { name: "/plan", help: "alias for /architect" },
17
17
  { name: "/decide", args: "2 | text", help: "answer the decision on screen" },
18
18
  { name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
19
+ { name: "/env", help: "list workspace environment variable names" },
19
20
  { name: "/settings", help: "change provider caps and agent settings" },
20
21
  { name: "/workspace", args: "[slug | new | set | rotate-key]", help: "list, switch, create, or configure" },
21
22
  { name: "/on", help: "turn on the current workspace" },
@@ -25,8 +25,9 @@ export function AgentsPanel({ board, width = 80, rows = 12, }) {
25
25
  ]
26
26
  : []),
27
27
  ...shown.map((row) => {
28
- const tone = row.run ? "blue" : row.state === "offline" ? "warning" : "muted";
29
- return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Text, { color: UI.text, children: pad(truncate(row.name, 13), 14) }), _jsx(Text, { color: UI.dim, children: pad(row.agent?.role ?? row.run?.kind ?? "", 13) }), _jsx(Text, { color: UI.dim, children: pad(truncate(row.agent?.model ?? "", 23), 24) }), _jsx(Text, { color: UI.text, children: pad(row.state, 9) }), _jsxs(Text, { color: UI.dim, children: [row.ticket ? `${row.ticket.key} ` : "", row.run ? elapsed(row.run.started_at ?? row.run.created_at) : ""] })] }, row.key));
28
+ const tone = row.run ? "blue" : row.state === "offline" || row.state === "limited" ? "warning" : "muted";
29
+ const state = row.limitedUntil ? `limited until ${row.limitedUntil}` : row.state;
30
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Text, { color: UI.text, children: pad(truncate(row.name, 13), 14) }), _jsx(Text, { color: UI.dim, children: pad(row.agent?.role ?? row.run?.kind ?? "", 13) }), _jsx(Text, { color: UI.dim, children: pad(truncate(row.agent?.model ?? "", 23), 24) }), _jsx(Text, { color: UI.text, children: pad(state, row.limitedUntil ? 22 : 9) }), _jsxs(Text, { color: UI.dim, children: [row.ticket ? `${row.ticket.key} ` : "", row.run ? elapsed(row.run.started_at ?? row.run.created_at) : ""] })] }, row.key));
30
31
  }),
31
32
  _jsx(More, { count: displayRows.length - shown.length }, "more"),
32
33
  ] }));
@@ -85,14 +86,16 @@ export function FeedPanel({ entries, width = 80, rows = 12, }) {
85
86
  */
86
87
  export function InboxPanel({ board, width = 80, rows = 12, focus = 0, }) {
87
88
  const entries = inboxEntries(board, width);
89
+ const messages = board.messages ?? [];
88
90
  const inner = Math.max(0, rows - 1);
89
91
  const window = scrollWindow(entries.length, inner, focus);
90
92
  const hiddenAbove = window.start;
91
93
  const hiddenBelow = entries.length - window.end;
92
- const note = `${board.decisions.length}${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
94
+ const note = `${board.decisions.length}d · ${messages.length}m`
95
+ + `${hiddenAbove ? ` ${hiddenAbove}↑` : ""}${hiddenBelow ? ` ${hiddenBelow}↓` : ""}`;
93
96
  return (_jsx(Panel, { width: width, rows: rows, children: [
94
- _jsx(Heading, { text: "Decisions", note: note }, "h"),
95
- ...(board.decisions.length === 0
97
+ _jsx(Heading, { text: "Inbox", note: note }, "h"),
98
+ ...(board.decisions.length === 0 && messages.length === 0
96
99
  ? [
97
100
  _jsx(Text, { color: UI.dim, children: "Nothing waiting on you." }, "empty"),
98
101
  ]
@@ -115,6 +118,16 @@ export function inboxEntries(board, width) {
115
118
  entries.push({ key: `${decision.id}:body:${line}`, kind: "body", text });
116
119
  });
117
120
  });
121
+ (board.messages ?? []).forEach((message) => {
122
+ entries.push({
123
+ key: `${message.id}:header`,
124
+ kind: "header",
125
+ text: `Message from ${message.from_name ?? message.from_role}`,
126
+ });
127
+ wrapLines(message.body_md.trim(), bodyWidth).forEach((text, line) => {
128
+ entries.push({ key: `${message.id}:body:${line}`, kind: "body", text });
129
+ });
130
+ });
118
131
  return entries;
119
132
  }
120
133
  function wrapLines(text, width) {
@@ -1,3 +1,13 @@
1
+ const LIMITED_UNTIL = /^(\w+) limited until (\d{1,2}:\d{2})$/;
2
+ export function limitedUntilByProvider(tickets) {
3
+ const limited = new Map();
4
+ for (const ticket of tickets) {
5
+ const match = ticket.stuck_reason?.match(LIMITED_UNTIL);
6
+ if (match)
7
+ limited.set(match[1], match[2]);
8
+ }
9
+ return limited;
10
+ }
1
11
  const roleForKind = {
2
12
  architect: "architect",
3
13
  build: "builder",
@@ -38,20 +48,28 @@ export function agentDisplayRows(board) {
38
48
  run,
39
49
  ticket,
40
50
  state: run.status,
51
+ limitedUntil: null,
41
52
  };
42
53
  });
54
+ const limited = limitedUntilByProvider(board.tickets);
43
55
  const idle = board.agents
44
56
  .filter((agent) => agent.enabled && !activeAgents.has(agent.id))
45
57
  .sort((left, right) => left.display_name.localeCompare(right.display_name))
46
- .map((agent) => ({
47
- key: `idle:${agent.id}`,
48
- name: agent.display_name,
49
- agent,
50
- run: null,
51
- ticket: null,
52
- state: board.availability.some((row) => row.provider === agent.provider && !row.available)
53
- ? "offline"
54
- : "idle",
55
- }));
58
+ .map((agent) => {
59
+ const until = limited.get(agent.provider);
60
+ return {
61
+ key: `idle:${agent.id}`,
62
+ name: agent.display_name,
63
+ agent,
64
+ run: null,
65
+ ticket: null,
66
+ state: until
67
+ ? "limited"
68
+ : board.availability.some((row) => row.provider === agent.provider && !row.available)
69
+ ? "offline"
70
+ : "idle",
71
+ limitedUntil: until ?? null,
72
+ };
73
+ });
56
74
  return [...live, ...idle];
57
75
  }
package/dist/tui/data.js CHANGED
@@ -1,4 +1,4 @@
1
- import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
1
+ import { approveEpic as approveEpicNow, answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, markMessagesDelivered, listWorkspaceEnv as getWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, setPaused as setPausedNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
2
2
  import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
3
3
  import { readEpicSpec } from "../epics.js";
4
4
  export const POLL_MS = 5_000;
@@ -22,13 +22,14 @@ export async function configuredSlugs(config = loadConfig()) {
22
22
  return (await listWorkspaces(config)).map((workspace) => workspace.slug).sort();
23
23
  }
24
24
  export async function loadSnapshot(config = loadConfig()) {
25
- const [status, workspaceData, ticketData, agentData, epicData, feedData] = await Promise.all([
25
+ const [status, workspaceData, ticketData, agentData, epicData, feedData, messageData] = await Promise.all([
26
26
  getStatus(config),
27
27
  getWorkspace(config),
28
28
  listTickets(config),
29
29
  listAgents(config),
30
30
  listEpics(config),
31
31
  listFeed(config),
32
+ listMessages({ toRoles: ["human", "all"], undelivered: true, limit: 500 }, config),
32
33
  ]);
33
34
  const byId = new Map(ticketData.tickets.map((ticket) => [ticket.id, ticket]));
34
35
  const agents = new Map(agentData.agents.map((agent) => [agent.id, agent]));
@@ -63,6 +64,7 @@ export async function loadSnapshot(config = loadConfig()) {
63
64
  agents: agentData.agents,
64
65
  epics: epicData.epics,
65
66
  decisions: status.decisions,
67
+ messages: messageData.messages,
66
68
  availability,
67
69
  runs: status.live_runs,
68
70
  },
@@ -127,6 +129,9 @@ export async function cancelTicket(config, key) {
127
129
  export async function setWorkspacePaused(config, paused) {
128
130
  return setPausedNow(paused, config);
129
131
  }
132
+ export async function listWorkspaceEnv(config) {
133
+ return getWorkspaceEnv(config);
134
+ }
130
135
  export async function createAgent(config, fields) {
131
136
  return postAgent(fields, config);
132
137
  }
@@ -148,6 +153,10 @@ export async function waitForReply(config, role, since, timeoutMs) {
148
153
  export async function resolveDecision(config, id, answer) {
149
154
  await answerDecision(id, answer, config);
150
155
  }
156
+ export async function acknowledgeInbox(config, ids) {
157
+ if (ids.length)
158
+ await markMessagesDelivered(ids, config);
159
+ }
151
160
  export async function updateAgent(config, id, fields) {
152
161
  return patchAgent(id, fields, config);
153
162
  }
package/dist/tui/parse.js CHANGED
@@ -37,6 +37,8 @@ export function parseLine(raw) {
37
37
  ...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { name: opts.name } : {}) }
38
38
  : { kind: "unknown", command: "agents needs add ROLE --provider P --model M or rm ROLE|ID" };
39
39
  }
40
+ case "env":
41
+ return rest.length ? { kind: "unknown", command: "env takes no arguments" } : { kind: "env" };
40
42
  case "help":
41
43
  return { kind: "help" };
42
44
  case "workspace":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"