@higherdev/cli 0.13.0 → 0.14.3

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
@@ -35,6 +35,7 @@ workspace-map config shapes are migrated automatically when they are read.
35
35
  | `hd epic rm ID` | Remove a draft epic |
36
36
  | `hd plan` | Point to the TUI `/architect` conversation |
37
37
  | `hd workspace ls` | List every workspace available to the configured key |
38
+ | `hd workspace use SLUG` | Switch the current workspace |
38
39
  | `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
39
40
  | `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
40
41
  | `hd workspace rotate-key` | Rotate the shared API key and save it locally |
@@ -43,6 +44,9 @@ workspace-map config shapes are migrated automatically when they are read.
43
44
  | `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
44
45
  | `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
45
46
  | `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
47
+ | `hd env ls` | List workspace environment variable names |
48
+ | `hd env set NAME=VALUE [NAME=VALUE...]` | Set workspace environment variables |
49
+ | `hd env rm NAME` | Remove a workspace environment variable |
46
50
  | `hd logs KEY [-f]` | Show or follow run events |
47
51
  | `hd msg KEY "message" [--interrupt]` | Message a builder |
48
52
  | `hd decide` | List open decisions |
@@ -64,7 +68,7 @@ the repository is missing, approve private creation interactively, use `--create
64
68
  `--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
65
69
 
66
70
  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`,
71
+ `/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
68
72
  `/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
69
73
  `/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
70
74
  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,13 +1,13 @@
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";
8
8
  import { epicProgressRows, readEpicSpec } from "./epics.js";
9
9
  import { banner, c, statusChip, table, truncate, usage } from "./out.js";
10
- import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet } from "./workspace-commands.js";
10
+ import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
11
11
  function fail(message) {
12
12
  console.error(message);
13
13
  process.exit(1);
@@ -252,6 +252,11 @@ async function cmdWorkspace(argv, deps = {}) {
252
252
  ])));
253
253
  return;
254
254
  }
255
+ if (action === "use") {
256
+ const workspace = await workspaceUse(rest);
257
+ console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
258
+ return;
259
+ }
255
260
  if (action === "rotate-key") {
256
261
  if (rest.length)
257
262
  fail(WORKSPACE_USAGE);
@@ -344,6 +349,32 @@ async function cmdCaps(argv) {
344
349
  const result = await updateCaps({ [provider]: cap });
345
350
  console.log(`${provider} ${result.provider_caps[provider]}`);
346
351
  }
352
+ async function cmdEnv(argv) {
353
+ const [action, ...args] = argv;
354
+ if (action === "ls") {
355
+ const { env } = await listWorkspaceEnv();
356
+ if (!env.length)
357
+ return console.log(c.dim("No workspace environment variables."));
358
+ console.log(table(["NAME", "UPDATED"], env.map((row) => [row.name, row.updated_at])));
359
+ return;
360
+ }
361
+ if (action === "set" && args.length) {
362
+ for (const assignment of args) {
363
+ const at = assignment.indexOf("=");
364
+ if (at < 1)
365
+ fail("usage: hd env set NAME=VALUE [NAME=VALUE...]");
366
+ await setWorkspaceEnv(assignment.slice(0, at), assignment.slice(at + 1));
367
+ }
368
+ console.log(`set ${args.length} variable${args.length === 1 ? "" : "s"}`);
369
+ return;
370
+ }
371
+ if (action === "rm" && args.length === 1) {
372
+ await removeWorkspaceEnv(args[0]);
373
+ console.log(`removed ${args[0]}`);
374
+ return;
375
+ }
376
+ fail("usage: hd env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME");
377
+ }
347
378
  export async function main(argv = process.argv.slice(2), deps = {}) {
348
379
  const [cmd, ...rest] = argv;
349
380
  try {
@@ -405,6 +436,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
405
436
  await cmdCaps(rest);
406
437
  return;
407
438
  }
439
+ if (cmd === "env") {
440
+ await cmdEnv(rest);
441
+ return;
442
+ }
408
443
  if (cmd === "pause" || cmd === "off") {
409
444
  await setPaused(true);
410
445
  console.log(cmd === "off" ? "off" : "paused");
package/dist/out.js CHANGED
@@ -67,9 +67,10 @@ export function usage() {
67
67
  ` ${c.blue("hd ticket list | show | new | 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
- ` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
70
+ ` ${c.blue("hd workspace ls | use | 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":
@@ -1,8 +1,8 @@
1
- import { createWorkspace, rotateWorkspaceApiKey, updateWorkspace } from "./api.js";
2
- import { loadConfig, writeHdConfig } from "./config.js";
1
+ import { createWorkspace, listWorkspaces, rotateWorkspaceApiKey, updateWorkspace } from "./api.js";
2
+ import { loadConfig, switchWorkspace, writeHdConfig } from "./config.js";
3
3
  import { promptOnStdin } from "./prompt.js";
4
4
  import { defaultGh, HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
5
- export const WORKSPACE_USAGE = "usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key";
5
+ export const WORKSPACE_USAGE = "usage: hd workspace ls | use SLUG | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key";
6
6
  function flags(argv) {
7
7
  const opts = {};
8
8
  const bools = new Set();
@@ -154,6 +154,17 @@ export async function workspaceSet(argv) {
154
154
  throw new Error(WORKSPACE_USAGE);
155
155
  return (await updateWorkspace(fields)).workspace;
156
156
  }
157
+ export async function workspaceUse(argv) {
158
+ if (argv.length !== 1 || !argv[0])
159
+ throw new Error(WORKSPACE_USAGE);
160
+ const workspaces = await listWorkspaces();
161
+ const selected = workspaces.find((workspace) => workspace.slug === argv[0]);
162
+ if (!selected) {
163
+ throw new Error(`No workspace ${argv[0]}. You can reach: ${workspaces.map((workspace) => workspace.slug).sort().join(", ")}.`);
164
+ }
165
+ switchWorkspace(selected.slug);
166
+ return selected;
167
+ }
157
168
  export async function workspaceRotateKey() {
158
169
  const config = loadConfig();
159
170
  const { api_key } = await rotateWorkspaceApiKey(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"