@higherdev/cli 0.9.0 → 0.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 CHANGED
@@ -32,13 +32,19 @@ workspace-map config shapes are migrated automatically when they are read.
32
32
  | `hd ticket show KEY` | Show one ticket |
33
33
  | `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket |
34
34
  | `hd ticket queue KEY` | Queue a complete ticket now |
35
+ | `hd ticket cancel KEY` | Cancel a ticket |
35
36
  | `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
36
37
  | `hd epic list` | List epics and ticket progress |
37
38
  | `hd plan [--repo DIR]` | Hand the terminal to Codex to author an epic spec |
38
39
  | `hd workspace ls` | List every workspace available to the configured key |
39
40
  | `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
41
+ | `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
42
+ | `hd workspace rotate-key` | Rotate the shared API key and save it locally |
40
43
  | `hd agents` | List agents |
41
- | `hd agents set ROLE --provider P --model M [--effort E]` | Update an agent |
44
+ | `hd agents add ROLE --provider P --model M [options]` | Add an agent |
45
+ | `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
46
+ | `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
47
+ | `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
42
48
  | `hd logs KEY [-f]` | Show or follow run events |
43
49
  | `hd msg KEY "message" [--interrupt]` | Message a builder |
44
50
  | `hd decide` | List open decisions |
@@ -52,7 +58,7 @@ workspace-map config shapes are migrated automatically when they are read.
52
58
  branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
53
59
  `--runner-user USER` overrides it.
54
60
 
55
- Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/epic new`,
56
- `/epics`, `/plan`, `/decide`, `/agents`, `/settings`, `/workspace`, `/feed`,
61
+ Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
62
+ `/epics`, `/plan`, `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`, `/feed`,
57
63
  `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
58
64
  the HDX API every five seconds.
package/dist/api.js CHANGED
@@ -55,6 +55,9 @@ export async function createTicket(fields, config = loadConfig()) {
55
55
  export async function queueTicket(key, config = loadConfig()) {
56
56
  return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/queue`, undefined, true);
57
57
  }
58
+ export async function cancelTicket(key, config = loadConfig()) {
59
+ return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/cancel`);
60
+ }
58
61
  export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
59
62
  const query = new URLSearchParams();
60
63
  if (afterAt)
@@ -81,12 +84,27 @@ export async function createWorkspace(fields, config = loadConfig()) {
81
84
  export async function listWorkspaces(config = loadConfig()) {
82
85
  return request(config, "GET", "/api/workspaces");
83
86
  }
87
+ export async function getWorkspace(config = loadConfig()) {
88
+ return request(config, "GET", `/api/w/${config.slug}`);
89
+ }
90
+ export async function updateWorkspace(fields, config = loadConfig()) {
91
+ return request(config, "PATCH", `/api/w/${config.slug}`, fields);
92
+ }
93
+ export async function rotateWorkspaceApiKey(config = loadConfig()) {
94
+ return request(config, "POST", `/api/w/${config.slug}/api-key`);
95
+ }
84
96
  export async function listAgents(config = loadConfig()) {
85
97
  return request(config, "GET", `/api/w/${config.slug}/agents`);
86
98
  }
99
+ export async function createAgent(fields, config = loadConfig()) {
100
+ return request(config, "POST", `/api/w/${config.slug}/agents`, fields);
101
+ }
87
102
  export async function updateAgent(id, fields, config = loadConfig()) {
88
103
  return request(config, "PATCH", `/api/w/${config.slug}/agents`, { id, ...fields });
89
104
  }
105
+ export async function deleteAgent(id, config = loadConfig()) {
106
+ return request(config, "DELETE", `/api/w/${config.slug}/agents`, { id });
107
+ }
90
108
  export async function listEpics(config = loadConfig()) {
91
109
  return request(config, "GET", `/api/w/${config.slug}/epics`);
92
110
  }
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 { answerDecision, createEpic, createTicket, createWorkspace, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, } from "./api.js";
4
+ import { answerDecision, cancelTicket, createAgent, createEpic, createTicket, createWorkspace, deleteAgent, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, rotateWorkspaceApiKey, setPaused, showTicket, updateAgent, updateCaps, updateWorkspace, } from "./api.js";
5
5
  import { initHost, parseHostFlags } from "./host.js";
6
6
  import { loadConfig, writeHdConfig } from "./config.js";
7
7
  import { epicProgressRows, readEpicSpec } from "./epics.js";
@@ -119,7 +119,15 @@ async function cmdTicket(argv) {
119
119
  console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
120
120
  return;
121
121
  }
122
- fail("usage: hd ticket list | show KEY | new --title TITLE | queue KEY");
122
+ if (action === "cancel") {
123
+ const key = rest[0];
124
+ if (!key)
125
+ fail("usage: hd ticket cancel KEY");
126
+ const { ticket } = await cancelTicket(key.toUpperCase());
127
+ console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
128
+ return;
129
+ }
130
+ fail("usage: hd ticket list | show KEY | new --title TITLE | queue KEY | cancel KEY");
123
131
  }
124
132
  async function cmdEpic(argv) {
125
133
  const [action, ...rest] = argv;
@@ -233,7 +241,54 @@ async function cmdWorkspace(argv, deps = {}) {
233
241
  ])));
234
242
  return;
235
243
  }
236
- const workspaceUsage = "usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--slug S] [--branch B] [--host box] [--runner-user USER] [--no-bootstrap]";
244
+ const workspaceUsage = "usage: hd workspace ls | new ... | set [flags] | rotate-key";
245
+ if (action === "rotate-key") {
246
+ const config = loadConfig();
247
+ const { api_key } = await rotateWorkspaceApiKey(config);
248
+ writeHdConfig({ ...config, api_key });
249
+ console.log(`api key: ${api_key}`);
250
+ console.log("saved locally; update Mel's hd init configuration on the box.");
251
+ return;
252
+ }
253
+ if (action === "set") {
254
+ const { rest: args, opts, bools } = flags(rest);
255
+ if (args.length || bools.size)
256
+ fail(workspaceUsage);
257
+ const fields = {};
258
+ for (const [flag, field] of [["name", "name"], ["repo", "repo"], ["branch", "default_branch"], ["host", "default_host"]]) {
259
+ if (opts[flag])
260
+ fields[field] = opts[flag];
261
+ }
262
+ if (opts["auto-merge"]) {
263
+ if (!["on", "off"].includes(opts["auto-merge"]))
264
+ fail("--auto-merge must be on or off");
265
+ fields.auto_merge = opts["auto-merge"] === "on";
266
+ }
267
+ if (opts["max-turns"]) {
268
+ const maxTurns = {};
269
+ for (const entry of opts["max-turns"].split(",")) {
270
+ const [kind, raw, ...extra] = entry.split("=");
271
+ const value = Number(raw);
272
+ if (extra.length || !["build", "followup", "review", "orchestrate"].includes(kind)
273
+ || !Number.isInteger(value) || value < 1) {
274
+ fail("--max-turns must be KIND=N[,KIND=N...] for build, followup, review, or orchestrate");
275
+ }
276
+ maxTurns[kind] = value;
277
+ }
278
+ fields.max_turns = maxTurns;
279
+ }
280
+ if (opts["max-attempts"]) {
281
+ const value = Number(opts["max-attempts"]);
282
+ if (!Number.isInteger(value) || value < 1)
283
+ fail("--max-attempts must be an integer >= 1");
284
+ fields.max_attempts = value;
285
+ }
286
+ if (!Object.keys(fields).length)
287
+ fail(workspaceUsage);
288
+ const { workspace } = await updateWorkspace(fields);
289
+ console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
290
+ return;
291
+ }
237
292
  if (action !== "new")
238
293
  fail(workspaceUsage);
239
294
  const { opts, bools } = flags(rest);
@@ -252,11 +307,14 @@ async function cmdWorkspace(argv, deps = {}) {
252
307
  console.log("saved and switched; configure another machine with:");
253
308
  console.log(`hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}`);
254
309
  }
255
- function selectAgent(agents, role, provider) {
256
- const matches = agents.filter((agent) => agent.role === role);
310
+ function selectAgent(agents, target, provider) {
311
+ const exact = agents.find((agent) => agent.id === target);
312
+ if (exact)
313
+ return exact;
314
+ const matches = agents.filter((agent) => agent.role === target);
257
315
  const selected = matches.length === 1 ? matches[0] : matches.find((agent) => agent.provider === provider);
258
316
  if (!selected)
259
- fail(`No unambiguous ${role} agent found for provider ${provider}.`);
317
+ fail(`No unambiguous agent ${target}${provider ? ` for provider ${provider}` : ""}.`);
260
318
  return selected;
261
319
  }
262
320
  async function cmdAgents(argv) {
@@ -267,24 +325,57 @@ async function cmdAgents(argv) {
267
325
  console.log("No agents.");
268
326
  return;
269
327
  }
270
- console.log(table(["ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
271
- agent.role, agent.provider, agent.model, agent.effort, agent.enabled ? c.green("on") : c.dim("off"),
328
+ console.log(table(["ID", "NAME", "ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
329
+ agent.id, agent.display_name, agent.role, agent.provider, agent.model, agent.effort,
330
+ agent.enabled ? c.green("on") : c.dim("off"),
272
331
  ])));
273
332
  return;
274
333
  }
275
- if (action !== "set")
276
- fail("usage: hd agents [set ROLE --provider P --model M [--effort E]]");
277
- const { rest: args, opts } = flags(rest);
278
- const role = args[0];
279
- if (!role || !opts.provider || !opts.model) {
280
- fail("usage: hd agents set ROLE --provider P --model M [--effort E]");
334
+ const agentUsage = "usage: hd agents add ROLE --provider P --model M [--effort E] [--name N] | rm ROLE|ID | set ROLE|ID [flags]";
335
+ const { rest: args, opts, bools } = flags(rest);
336
+ const target = args[0];
337
+ if (action === "add") {
338
+ if (!target || !opts.provider || !opts.model)
339
+ fail(agentUsage);
340
+ const { agent } = await createAgent({ role: target, provider: opts.provider, model: opts.model,
341
+ effort: opts.effort, display_name: opts.name ?? opts.provider });
342
+ console.log(`${agent.id} ${agent.display_name} ${agent.role} ${agent.provider}`);
343
+ return;
281
344
  }
345
+ if (!target)
346
+ fail(agentUsage);
282
347
  const { agents } = await listAgents();
283
- const current = selectAgent(agents, role, opts.provider);
284
- const { agent } = await updateAgent(current.id, {
285
- provider: opts.provider, model: opts.model, ...(opts.effort ? { effort: opts.effort } : {}),
286
- });
287
- console.log(`${agent.role} ${agent.provider} ${agent.model} ${agent.effort}`);
348
+ const current = selectAgent(agents, target, opts.provider);
349
+ if (action === "rm") {
350
+ await deleteAgent(current.id);
351
+ console.log(`removed ${current.display_name} (${current.id})`);
352
+ return;
353
+ }
354
+ if (action !== "set" || (bools.has("on") && bools.has("off")))
355
+ fail(agentUsage);
356
+ const fields = {
357
+ ...(opts.provider ? { provider: opts.provider } : {}), ...(opts.model ? { model: opts.model } : {}),
358
+ ...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { display_name: opts.name } : {}),
359
+ ...(bools.has("on") || bools.has("off") ? { enabled: bools.has("on") } : {}),
360
+ };
361
+ if (!Object.keys(fields).length)
362
+ fail(agentUsage);
363
+ const { agent } = await updateAgent(current.id, fields);
364
+ console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
365
+ }
366
+ async function cmdCaps(argv) {
367
+ const [action, provider, raw, ...extra] = argv;
368
+ if (!action) {
369
+ const caps = (await getStatus()).workspace.provider_caps;
370
+ console.log(table(["PROVIDER", "CAP"], Object.entries(caps).sort().map(([name, cap]) => [name, String(cap)])));
371
+ return;
372
+ }
373
+ const cap = Number(raw);
374
+ if (action !== "set" || !provider || extra.length || !Number.isInteger(cap) || cap < 0) {
375
+ fail("usage: hd caps | hd caps set PROVIDER N");
376
+ }
377
+ const result = await updateCaps({ [provider]: cap });
378
+ console.log(`${provider} ${result.provider_caps[provider]}`);
288
379
  }
289
380
  export async function main(argv = process.argv.slice(2), deps = {}) {
290
381
  const [cmd, ...rest] = argv;
@@ -343,6 +434,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
343
434
  await cmdAgents(rest);
344
435
  return;
345
436
  }
437
+ if (cmd === "caps") {
438
+ await cmdCaps(rest);
439
+ return;
440
+ }
346
441
  if (cmd === "pause" || cmd === "off") {
347
442
  await setPaused(true);
348
443
  console.log(cmd === "off" ? "off" : "paused");
package/dist/out.js CHANGED
@@ -64,11 +64,12 @@ 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 | queue")} ticket operations`,
67
+ ` ${c.blue("hd ticket list | show | new | queue | cancel")} ticket operations`,
68
68
  ` ${c.blue("hd epic new PATH | list")} epic operations`,
69
69
  ` ${c.blue("hd plan [--repo DIR]")} author an epic with Codex`,
70
- ` ${c.blue("hd workspace ls | new")} list or create workspaces`,
71
- ` ${c.blue("hd agents [set]")} inspect or update agents`,
70
+ ` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
71
+ ` ${c.blue("hd agents [add | rm | set]")} manage agents`,
72
+ ` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
72
73
  ` ${c.blue("hd logs KEY [-f]")} run events`,
73
74
  ` ${c.blue("hd msg KEY TEXT")} message a builder`,
74
75
  ` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
package/dist/tui/App.js CHANGED
@@ -16,7 +16,7 @@ import { alertOnce } from "./alert.js";
16
16
  import { bubbleRows } from "./height.js";
17
17
  import { planLayout, splitPanels } from "./layout.js";
18
18
  import { parseLine } from "./parse.js";
19
- import { configuredSlugs, createEpicFromFile, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
19
+ import { configuredSlugs, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
20
20
  import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
21
21
  import { appendLines, runLabels, toStreamLines } from "./stream.js";
22
22
  import { UI } from "./theme.js";
@@ -149,9 +149,12 @@ export function App({ initial }) {
149
149
  if (edit.value.target === "cap") {
150
150
  await updateProviderCap(config, edit.value.provider, edit.value.cap);
151
151
  }
152
- else {
152
+ else if (edit.value.target === "agent") {
153
153
  await updateAgent(config, edit.value.id, edit.value.fields);
154
154
  }
155
+ else {
156
+ await updateWorkspace(config, edit.value.fields);
157
+ }
155
158
  setNotice(null);
156
159
  await refresh();
157
160
  }
@@ -348,6 +351,55 @@ export function App({ initial }) {
348
351
  setBusy(false);
349
352
  }
350
353
  return;
354
+ case "cancel":
355
+ setBusy(true);
356
+ try {
357
+ const { ticket: cancelled } = await cancelTicket(config, action.key);
358
+ say("system", `${cancelled.key} cancelled.`);
359
+ await refresh();
360
+ }
361
+ catch (error) {
362
+ setNotice(error instanceof Error ? error.message : String(error));
363
+ }
364
+ finally {
365
+ setBusy(false);
366
+ }
367
+ return;
368
+ case "agent-add":
369
+ setBusy(true);
370
+ try {
371
+ const { agent } = await createAgent(config, { role: action.role, provider: action.provider,
372
+ model: action.model, effort: action.effort, display_name: action.name ?? action.provider });
373
+ say("system", `Added ${agent.display_name} (${agent.id}).`);
374
+ await refresh();
375
+ }
376
+ catch (error) {
377
+ setNotice(error instanceof Error ? error.message : String(error));
378
+ }
379
+ finally {
380
+ setBusy(false);
381
+ }
382
+ return;
383
+ case "agent-rm": {
384
+ const matches = board.agents.filter((agent) => agent.id === action.target || agent.role === action.target);
385
+ if (matches.length !== 1) {
386
+ setNotice(`Agent ${action.target} is missing or ambiguous; use its ID.`);
387
+ return;
388
+ }
389
+ setBusy(true);
390
+ try {
391
+ await deleteAgent(config, matches[0].id);
392
+ say("system", `Removed ${matches[0].display_name}.`);
393
+ await refresh();
394
+ }
395
+ catch (error) {
396
+ setNotice(error instanceof Error ? error.message : String(error));
397
+ }
398
+ finally {
399
+ setBusy(false);
400
+ }
401
+ return;
402
+ }
351
403
  case "plan":
352
404
  setBusy(true);
353
405
  try {
package/dist/tui/Help.js CHANGED
@@ -9,11 +9,12 @@ export const COMMANDS = [
9
9
  { name: "/inbox", help: "decisions and messages waiting on you" },
10
10
  { name: "/ticket", args: "HD-12", help: "open one ticket" },
11
11
  { name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
12
+ { name: "/cancel", args: "HD-12", help: "cancel a ticket" },
12
13
  { name: "/epic", args: "new PATH", help: "create an epic from a Markdown spec" },
13
14
  { name: "/epics", help: "list epics and ticket progress" },
14
15
  { name: "/plan", help: "author an epic with your Codex CLI" },
15
16
  { name: "/decide", args: "2 | text", help: "answer the decision on screen" },
16
- { name: "/agents", help: "every agent in full, and the live run stream" },
17
+ { name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
17
18
  { name: "/settings", help: "change provider caps and agent settings" },
18
19
  { name: "/workspace", args: "[slug]", help: "switch workspace, or list the ones you can reach" },
19
20
  { name: "/feed", help: "what just happened" },
package/dist/tui/data.js CHANGED
@@ -1,4 +1,4 @@
1
- import { answerDecision, createEpic as postEpic, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
1
+ import { 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, 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,8 +22,9 @@ 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, ticketData, agentData, epicData, feedData] = await Promise.all([
25
+ const [status, workspaceData, ticketData, agentData, epicData, feedData] = await Promise.all([
26
26
  getStatus(config),
27
+ getWorkspace(config),
27
28
  listTickets(config),
28
29
  listAgents(config),
29
30
  listEpics(config),
@@ -44,7 +45,19 @@ export async function loadSnapshot(config = loadConfig()) {
44
45
  });
45
46
  return {
46
47
  config,
47
- workspace: status.workspace,
48
+ workspace: {
49
+ ...status.workspace,
50
+ ...workspaceData.workspace,
51
+ paused: status.workspace.paused,
52
+ provider_caps: status.workspace.provider_caps,
53
+ max_turns: {
54
+ build: Number(workspaceData.workspace.settings.max_turns?.build ?? 40),
55
+ followup: Number(workspaceData.workspace.settings.max_turns?.followup ?? 15),
56
+ review: Number(workspaceData.workspace.settings.max_turns?.review ?? 50),
57
+ orchestrate: Number(workspaceData.workspace.settings.max_turns?.orchestrate ?? 30),
58
+ },
59
+ max_attempts: Number(workspaceData.workspace.settings.max_attempts ?? 3),
60
+ },
48
61
  board: {
49
62
  tickets,
50
63
  agents: agentData.agents,
@@ -105,6 +118,15 @@ export async function createEpicFromFile(config, path) {
105
118
  export async function queueTicket(config, key) {
106
119
  return queueTicketNow(key, config);
107
120
  }
121
+ export async function cancelTicket(config, key) {
122
+ return cancelTicketNow(key, config);
123
+ }
124
+ export async function createAgent(config, fields) {
125
+ return postAgent(fields, config);
126
+ }
127
+ export async function deleteAgent(config, id) {
128
+ return removeAgent(id, config);
129
+ }
108
130
  export async function waitForReply(config, since, timeoutMs) {
109
131
  const deadline = Date.now() + timeoutMs;
110
132
  while (Date.now() <= deadline) {
@@ -126,6 +148,9 @@ export async function updateAgent(config, id, fields) {
126
148
  export async function updateProviderCap(config, provider, cap) {
127
149
  return updateCaps({ [provider]: cap }, config);
128
150
  }
151
+ export async function updateWorkspace(config, fields) {
152
+ return patchWorkspace(fields, config);
153
+ }
129
154
  export async function loadLiveEvents(config, board) {
130
155
  const liveIds = new Set(board.runs.filter((run) => run.status === "running").map((run) => run.id));
131
156
  const keys = new Set(board.runs
package/dist/tui/parse.js CHANGED
@@ -14,11 +14,25 @@ export function parseLine(raw) {
14
14
  case "orchestrator":
15
15
  return { kind: "mode", mode: "orchestrator" };
16
16
  case "board":
17
- case "agents":
18
17
  case "feed":
19
18
  case "inbox":
20
19
  case "settings":
21
20
  return { kind: "view", view: word.toLowerCase() };
21
+ case "agents": {
22
+ if (!argument)
23
+ return { kind: "view", view: "agents" };
24
+ const [verb, target, ...tail] = rest;
25
+ if (verb === "rm" && target && !tail.length)
26
+ return { kind: "agent-rm", target };
27
+ const opts = {};
28
+ for (let i = 0; i < tail.length; i += 2)
29
+ if (tail[i]?.startsWith("--") && tail[i + 1])
30
+ opts[tail[i].slice(2)] = tail[i + 1];
31
+ return verb === "add" && target && opts.provider && opts.model
32
+ ? { kind: "agent-add", role: target, provider: opts.provider, model: opts.model,
33
+ ...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { name: opts.name } : {}) }
34
+ : { kind: "unknown", command: "agents needs add ROLE --provider P --model M or rm ROLE|ID" };
35
+ }
22
36
  case "help":
23
37
  return { kind: "help" };
24
38
  case "workspace":
@@ -39,6 +53,9 @@ export function parseLine(raw) {
39
53
  return argument
40
54
  ? { kind: "queue", key: argument.toUpperCase() }
41
55
  : { kind: "unknown", command: "queue needs a key" };
56
+ case "cancel":
57
+ return argument ? { kind: "cancel", key: argument.toUpperCase() }
58
+ : { kind: "unknown", command: "cancel needs a key" };
42
59
  case "plan":
43
60
  return argument ? { kind: "unknown", command: "plan takes no arguments" } : { kind: "plan" };
44
61
  case "decide": {
@@ -2,8 +2,16 @@ import { efforts, providers } from "./data.js";
2
2
  export function settingsRows(workspace, agents) {
3
3
  const rows = [
4
4
  { key: "h:workspace", kind: "heading", label: workspace.slug, value: workspace.repo },
5
- { key: "w:branch", kind: "readonly", label: "branch", value: workspace.default_branch },
6
- { key: "w:host", kind: "readonly", label: "host", value: workspace.default_host },
5
+ { key: "w:name", kind: "text", label: "name", value: workspace.name },
6
+ { key: "w:repo", kind: "text", label: "repo", value: workspace.repo, hint: "owner/name" },
7
+ { key: "w:default_branch", kind: "text", label: "branch", value: workspace.default_branch },
8
+ { key: "w:default_host", kind: "text", label: "host", value: workspace.default_host },
9
+ { key: "w:auto_merge", kind: "toggle", label: "auto merge", value: workspace.auto_merge ? "yes" : "no" },
10
+ ...["build", "followup", "review", "orchestrate"].map((kind) => ({
11
+ key: `w:max_turns:${kind}`, kind: "number", label: `turns ${kind}`,
12
+ value: String(workspace.max_turns[kind]), hint: "integer >= 1",
13
+ })),
14
+ { key: "w:max_attempts", kind: "number", label: "max attempts", value: String(workspace.max_attempts) },
7
15
  ];
8
16
  for (const provider of providers) {
9
17
  rows.push({
@@ -16,6 +24,7 @@ export function settingsRows(workspace, agents) {
16
24
  }
17
25
  for (const agent of agents) {
18
26
  rows.push({ key: `h:${agent.id}`, kind: "heading", label: agent.display_name, value: agent.role });
27
+ rows.push({ key: `a:${agent.id}:display_name`, kind: "text", label: "name", value: agent.display_name });
19
28
  rows.push({
20
29
  key: `a:${agent.id}:enabled`, kind: "toggle", label: "enabled",
21
30
  value: agent.enabled ? "yes" : "no", agent: agent.display_name,
@@ -59,6 +68,25 @@ export function editFor(row, raw) {
59
68
  return { ok: false, error: `${field} cap must be an integer >= 0.` };
60
69
  return { ok: true, value: { target: "cap", provider: field, cap } };
61
70
  }
71
+ if (scope === "w" && id) {
72
+ if (id === "max_turns" && field) {
73
+ const number = Number(value);
74
+ if (!Number.isInteger(number) || number < 1)
75
+ return { ok: false, error: `max_turns.${field} must be an integer >= 1.` };
76
+ return { ok: true, value: { target: "workspace", fields: { max_turns: { [field]: number } } } };
77
+ }
78
+ if (id === "max_attempts") {
79
+ const number = Number(value);
80
+ if (!Number.isInteger(number) || number < 1)
81
+ return { ok: false, error: "max_attempts must be an integer >= 1." };
82
+ return { ok: true, value: { target: "workspace", fields: { max_attempts: number } } };
83
+ }
84
+ if (id === "auto_merge")
85
+ return { ok: true, value: { target: "workspace", fields: { auto_merge: value === "yes" } } };
86
+ if (!value)
87
+ return { ok: false, error: `${row.label} is required.` };
88
+ return { ok: true, value: { target: "workspace", fields: { [id]: value } } };
89
+ }
62
90
  if (scope !== "a" || !id || !field)
63
91
  return { ok: false, error: `Nothing to change on ${row.label}.` };
64
92
  if (field === "enabled") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"