@fulldivevr/workcord-cli 1.2.1 → 1.6.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.
Files changed (40) hide show
  1. package/README.md +75 -19
  2. package/dist/cli.js +7 -1
  3. package/dist/client.js +8 -1
  4. package/dist/commands/agent-skills.js +127 -0
  5. package/dist/commands/agent.js +191 -0
  6. package/dist/commands/custom-agents.js +3 -3
  7. package/dist/commands/hermes.js +12 -7
  8. package/dist/commands/messages.js +150 -4
  9. package/dist/commands/reflections.js +96 -0
  10. package/dist/commands/rooms.js +7 -3
  11. package/dist/generated/commands/agent-feedback-admin.js +69 -0
  12. package/dist/generated/commands/ai.js +31 -0
  13. package/dist/generated/commands/call-recordings.js +119 -0
  14. package/dist/generated/commands/contact-match.js +20 -1
  15. package/dist/generated/commands/credit-packs.js +112 -0
  16. package/dist/generated/commands/custom-agents.js +483 -1
  17. package/dist/generated/commands/desktop-releases.js +2 -2
  18. package/dist/generated/commands/edge-sizecheck.js +25 -0
  19. package/dist/generated/commands/feed.js +6 -0
  20. package/dist/generated/commands/interactive-events.js +47 -0
  21. package/dist/generated/commands/internal.js +205 -0
  22. package/dist/generated/commands/meetings.js +12 -0
  23. package/dist/generated/commands/messages.js +30 -0
  24. package/dist/generated/commands/my-feedback.js +18 -0
  25. package/dist/generated/commands/pinned-messages.js +6 -0
  26. package/dist/generated/commands/proxy-nodes.js +83 -0
  27. package/dist/generated/commands/push.js +20 -0
  28. package/dist/generated/commands/reviews.js +96 -0
  29. package/dist/generated/commands/room-agents.js +243 -1
  30. package/dist/generated/commands/room-invites.js +12 -0
  31. package/dist/generated/commands/rooms.js +79 -1
  32. package/dist/generated/commands/search.js +1 -1
  33. package/dist/generated/commands/shadow-dispatch-push.js +20 -0
  34. package/dist/generated/commands/shadow-reflections.js +111 -0
  35. package/dist/generated/index.js +22 -0
  36. package/dist/hermes-guards.js +1 -1
  37. package/dist/http.js +6 -2
  38. package/dist/room-types.js +10 -0
  39. package/package.json +2 -2
  40. package/dist/generated/commands/health.js +0 -29
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # @fulldivevr/workcord-cli
2
2
 
3
- TypeScript CLI for the WorkCord API. **One invocation = one user action.**
4
- Designed for automated agents that emulate real users — wake up, do one
5
- thing, exit. Route-level commands are generated from the backend's own
6
- OpenAPI spec; a small set of hand-written semantic commands sits on top.
3
+ TypeScript CLI for talking to WorkCord agents through persistent Hermes tenant
4
+ chat and using the WorkCord API. **One invocation = one user action.** Designed
5
+ for automated agents such as Claude Code: ask the user's Assistant for the
6
+ outcome you need, let that tenant manage its own instructions, skills, and
7
+ flavors, then inspect its reply. Route-level commands and advanced diagnostics
8
+ remain available.
7
9
 
8
10
  Lives in the backend repo (`clients/workcord-cli`) so the spec it generates
9
11
  from is always the spec this server serves.
@@ -144,6 +146,48 @@ workcord [--as <userId>] <namespace> <command> [flags]
144
146
  `--as` is only needed when minting with `WORKCORD_JWT_SECRET`; with
145
147
  `WORKCORD_TOKEN` set, omit it everywhere.
146
148
 
149
+ ### Agent-first customization
150
+
151
+ To change agent behavior, create or update a skill, or maintain a flavor, talk
152
+ to the owning agent through Hermes tenant chat. Do not edit prompts or tenant
153
+ files directly. With no `--agent`, the user's non-deletable Assistant is the
154
+ target and becomes the default owner of anything it creates:
155
+
156
+ ```bash
157
+ workcord agent chat new --message "Create a skill that prepares a concise weekly sales report and enable it."
158
+ workcord agent chat new --message "Update my default flavor instructions to require sources in research reports."
159
+ ```
160
+
161
+ Use the returned `chatId` for follow-up, verification, or rollback:
162
+
163
+ ```bash
164
+ workcord agent chat send --chat "$CHAT_ID" --message "Verify the change, list the versions you created, and explain how to roll it back."
165
+ ```
166
+
167
+ Both commands wait up to two minutes by default and return the durable Hermes
168
+ message document. Use `--async` for a queued response, then continue with its
169
+ `messageId`:
170
+
171
+ ```bash
172
+ workcord agent chat wait --message-id "$MESSAGE_ID" --timeout 10m
173
+ workcord agent chat history --chat "$CHAT_ID"
174
+ ```
175
+
176
+ If a local wait times out, the response has `timedOut: true`; the remote agent
177
+ continues working and can be checked with `chat wait`.
178
+
179
+ To make another owned agent the explicit owner and conversation target:
180
+
181
+ ```bash
182
+ workcord agent chat new --agent "$AGENT_ID" --message "Create and maintain a finance-specific flavor for yourself."
183
+ ```
184
+
185
+ The Assistant or selected agent works within its tenant's Hub permissions. It
186
+ can safely validate changes and report what it activated. `agent chat` uses
187
+ Hermes' native durable `hub_chat` transport; it is not raw A2A wire traffic.
188
+ A2A remains available for external agent peers. Direct `hermes` commands are
189
+ advanced diagnostics and recovery tools, not the normal customization workflow.
190
+
147
191
  - `workcord --help` lists namespaces.
148
192
  - `workcord <namespace> --help` lists that namespace's commands.
149
193
  - `workcord <namespace> <command> --help` shows the command's flags.
@@ -161,6 +205,7 @@ workcord <namespace> <command> [--<path-param> <value>] [--body '<json>'] [--que
161
205
  ```bash
162
206
  workcord auth login --email you@company.com
163
207
  workcord --as USER messages send-text --room R --text "hello"
208
+ workcord messages ask-agent --room R --agent INSTALLATION_ID --text "Summarize the decision"
164
209
  ```
165
210
 
166
211
  Semantic aliases cover common agent/admin actions with consistent flags:
@@ -181,6 +226,10 @@ Server auto-extracts mention tokens from visible `@Name` text. Prefer
181
226
  member's display name and avoids exposing raw user ids. `auth login` never
182
227
  takes `--as` — it is how you obtain a `WORKCORD_TOKEN` in the first place.
183
228
 
229
+ `messages ask-agent` posts an ordinary visible `@Agent` request, then waits for
230
+ that installed agent's visible reply in the room or request thread. Pass the
231
+ `installationId` returned by Home to `--agent`.
232
+
184
233
  ### Error handling
185
234
 
186
235
  Every error is **a single JSON line on stderr**, with a non-zero exit code:
@@ -206,7 +255,7 @@ workcord --as USER rooms discover --search "ai" --tags ai,tools --limit 20
206
255
  workcord --as USER rooms create-with-message --name "ops" --public --tags ai,news --text "First post."
207
256
  workcord --as USER rooms members --room R
208
257
  workcord --as USER rooms upload-icon-file --room R --file ./icon.png
209
- workcord --as USER custom-agents create-hosted --name "Helper" --description "Room helper" --prompt-file ./prompt.txt --tools web_search
258
+ workcord --as USER agent chat new --message "Create a room helper that can research the web."
210
259
  workcord --as USER room-agents install-hosted --room R --agent system-wizeup
211
260
  workcord --as USER messages react --room R --message M --emoji 👍
212
261
  workcord --as USER dm send-text --to USER_B --text "hi"
@@ -272,12 +321,14 @@ workcord --as $U dm send-text --to $OTHER --text "hey"
272
321
  # → composite: opens (or finds) the DM room, sends the message, returns both
273
322
  ```
274
323
 
275
- ## Hermes Hub (`hermes` namespace)
324
+ ## Advanced Hermes diagnostics and recovery (`hermes` namespace)
276
325
 
277
- WorkCord provisions a **Hermes Hub tenant** for each room-agent and each Hermes
278
- custom agent. That tenant is where the agent actually runs: its chats, runs,
279
- files, skills, flavor pin, A2A peers and eval runs all live on the Hub, not in
280
- WorkCord. The `hermes` namespace drives that tenant directly.
326
+ WorkCord provisions a **Hermes Hub tenant** for each room-agent, Hermes custom
327
+ agent, and room-scoped marketplace flavor install. That tenant is where the
328
+ agent actually runs: its chats, runs, files, skills, flavor pin, A2A peers and
329
+ eval runs all live on the Hub, not in WorkCord. Normal changes should be
330
+ requested with `workcord agent chat`; the `hermes` namespace exists for direct
331
+ inspection, diagnostics, and recovery.
281
332
 
282
333
  This is the one namespace that talks to a **second origin**. Every other command
283
334
  goes to the WorkCord API; `hermes` uses the WorkCord API only to reveal the
@@ -291,8 +342,11 @@ Every `hermes` command takes exactly one of:
291
342
  | --- | --- |
292
343
  | `--agent <id>` | a custom agent id, used verbatim |
293
344
  | `--room-agent <id>` | a room agent id, resolved as `workcord-room-agent:<id>` |
345
+ | `--marketplace-install <id>` | an isolated marketplace flavor install id or its room agent id |
294
346
 
295
- Passing both, or neither, is an error.
347
+ Passing more than one, or none, is an error. Marketplace install access follows
348
+ current room ownership; transferring room ownership revokes the former
349
+ creator's ability to reveal the tenant credential.
296
350
 
297
351
  ### Credentials
298
352
 
@@ -346,6 +400,7 @@ material.
346
400
 
347
401
  ```bash
348
402
  workcord hermes whoami --room-agent $RA
403
+ workcord hermes whoami --marketplace-install $MARKETPLACE_INSTALL
349
404
  workcord hermes chat send --room-agent $RA --chat $CHAT --text "status?"
350
405
  workcord hermes flavor current --agent $AGENT
351
406
  workcord hermes peers find --agent $AGENT --query "translation" --limit 5
@@ -355,15 +410,16 @@ Version flags are named `--flavor-version` and `--skill-version`, not
355
410
  `--version`: commander's program-level `.version()` matches `--version`
356
411
  anywhere in argv, prints the CLI version and exits before the subcommand runs.
357
412
 
358
- ### Authoring flavors and skills
413
+ ### Direct flavor and skill recovery
414
+
415
+ For normal work, ask the Assistant or another owned agent to create, update,
416
+ validate, or activate its own flavor and skills. Hermes Hub supports
417
+ tenant-owned skill and flavor namespaces; ownership defaults to whichever agent
418
+ tenant performs the creation.
359
419
 
360
- `flavor publish`, `flavor rollout` and `skill publish` are **owner-gated on the
361
- Hub**. A WorkCord agent tenant is provisioned as a *consumer* and owns no flavor
362
- or skill namespace, so these will 403 out of the box. Ownership is
363
- admin-assigned only (`PUT /v1/flavors/{id}/owner`, likewise for skills) and
364
- there is **no self-service bootstrap** — a hub operator has to assign the
365
- namespace to your tenant first. The CLI turns that 403 into an explicit error
366
- naming the tenant and the namespace so you know exactly what to ask for.
420
+ The direct `flavor publish`, `flavor rollout`, and `skill publish` commands are
421
+ kept for advanced recovery and authorized publisher workflows. They operate as
422
+ the selected tenant and can only mutate namespaces that tenant owns.
367
423
 
368
424
  The CLI does **not** pack or untar archives. The authoring loop is:
369
425
 
package/dist/cli.js CHANGED
@@ -13,13 +13,16 @@ import { registerCustomAgents } from "./commands/custom-agents.js";
13
13
  import { registerChallenges } from "./commands/challenges.js";
14
14
  import { registerHealth } from "./commands/health.js";
15
15
  import { registerHermes } from "./commands/hermes.js";
16
+ import { registerAgent } from "./commands/agent.js";
17
+ import { registerAgentSkills } from "./commands/agent-skills.js";
18
+ import { registerReflections } from "./commands/reflections.js";
16
19
  import { registerAll as registerGenerated } from "./generated/index.js";
17
20
  const program = new Command();
18
21
  const require = createRequire(import.meta.url);
19
22
  const { version } = require("../package.json");
20
23
  program
21
24
  .name("workcord")
22
- .description("CLI for the WorkCord API. One invocation = one user action.")
25
+ .description("Talk to WorkCord agents through Hermes tenant chat, with advanced API diagnostics.")
23
26
  .version(version)
24
27
  .option("--as <userId>", "user to act as")
25
28
  .option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL and saved configuration)");
@@ -27,6 +30,7 @@ program.hook("preAction", () => {
27
30
  setBaseUrlOverride(program.opts().baseUrl);
28
31
  });
29
32
  registerWhoami(program);
33
+ registerAgent(program);
30
34
  registerAuth(program);
31
35
  registerMessages(program);
32
36
  registerRooms(program);
@@ -34,6 +38,8 @@ registerDm(program);
34
38
  registerUsers(program);
35
39
  registerRoomAgents(program);
36
40
  registerCustomAgents(program);
41
+ registerAgentSkills(program);
42
+ registerReflections(program);
37
43
  registerChallenges(program); // adds regenerate-cards (not in the OpenAPI spec)
38
44
  registerHealth(program);
39
45
  registerHermes(program);
package/dist/client.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { resolveAuth } from "./auth.js";
2
2
  import { DEFAULT_BASE_URL, readConfig } from "./config.js";
3
3
  import { applyQuery, fetchWithRetry, httpError, normalizeBaseUrl, safeJson } from "./http.js";
4
+ import { validateRoomCreateBody } from "./room-types.js";
4
5
  let baseUrlOverride;
5
6
  export function setBaseUrlOverride(url) {
6
7
  baseUrlOverride = url;
7
8
  }
8
9
  export async function getBaseUrl() {
9
10
  const config = await readConfig();
10
- return normalizeBaseUrl(baseUrlOverride || process.env.WORKCORD_API_URL || config.baseUrl || DEFAULT_BASE_URL);
11
+ return normalizeBaseUrl(baseUrlOverride
12
+ || process.env.WORKCORD_API_URL
13
+ || process.env.WORKCORD_API_URL_TOKEN
14
+ || config.baseUrl
15
+ || DEFAULT_BASE_URL);
11
16
  }
12
17
  export async function call({ method = "GET", path, query, body, headers, asUserId }) {
13
18
  const config = await readConfig();
@@ -18,6 +23,8 @@ export async function callPublic({ method = "GET", path, query, body, headers })
18
23
  return request({ method, path, query, body, headers });
19
24
  }
20
25
  async function request({ method = "GET", path, query, body, headers, bearer }) {
26
+ if (method === "POST" && path === "/api/v1/rooms")
27
+ validateRoomCreateBody(body);
21
28
  const base = await getBaseUrl();
22
29
  const url = applyQuery(new URL(base + path), query);
23
30
  const res = await fetchWithRetry(() => ({
@@ -0,0 +1,127 @@
1
+ import { actingUserId } from "../auth.js";
2
+ import { call } from "../client.js";
3
+ import { getOrCreateGroup } from "./_group.js";
4
+ const SKILL_ID = /^[a-z][a-z0-9_-]{0,63}$/;
5
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
6
+ export function parseSkillRef(ref) {
7
+ const at = ref.lastIndexOf("@");
8
+ if (at <= 0 || at === ref.length - 1)
9
+ throw new Error("skill must be ID@VERSION, e.g. web-research@1.0.0");
10
+ return { id: skillId(ref.slice(0, at)), version: semver(ref.slice(at + 1)) };
11
+ }
12
+ function skillId(id) {
13
+ if (!SKILL_ID.test(id))
14
+ throw new Error("skill id must be a lowercase slug");
15
+ return id;
16
+ }
17
+ function semver(version) {
18
+ if (!SEMVER.test(version))
19
+ throw new Error("version must be a semantic version, e.g. 1.0.0");
20
+ return version;
21
+ }
22
+ function bool(value) {
23
+ if (value === "true")
24
+ return true;
25
+ if (value === "false")
26
+ return false;
27
+ throw new Error("enabled must be true or false");
28
+ }
29
+ function skillPath(path) {
30
+ const segments = path.split("/");
31
+ if (segments.some((s) => !s || s === "." || s === ".."))
32
+ throw new Error("invalid skill file path");
33
+ return segments.map(encodeURIComponent).join("/");
34
+ }
35
+ // Agent ids are uuids, optionally behind the prefix a room agent is listed under.
36
+ const AGENT_ID = /^(?:workcord-room-agent:|marketplace-install:)?[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/i;
37
+ /** Pick the one agent named `name` (exact, case-insensitive) from a list-mine response. */
38
+ export function agentIdByName(name, mine) {
39
+ const agents = (mine?.data ?? []);
40
+ const wanted = name.toLowerCase();
41
+ const found = (Array.isArray(agents) ? agents : [])
42
+ .filter((a) => typeof a?.name === "string" && a.name.toLowerCase() === wanted);
43
+ if (found.length === 0)
44
+ throw new Error(`no agent named ${name}`);
45
+ if (found.length > 1) {
46
+ throw new Error(`more than one agent named ${name}: ${found.map((a) => `${a.name} (${a.id})`).join(", ")}`);
47
+ }
48
+ return String(found[0].id);
49
+ }
50
+ function agentPath(agentId, suffix) {
51
+ return `/api/v1/agents/${encodeURIComponent(agentId)}/${suffix}`;
52
+ }
53
+ export function registerAgentSkills(program, send = call, output = (value) => process.stdout.write(JSON.stringify(value) + "\n")) {
54
+ const agents = getOrCreateGroup(program, "custom-agents", "CustomAgents operations");
55
+ const skills = agents.command("skills").description("Inspect and manage skills on an agent you own");
56
+ const withActor = (req) => {
57
+ const asUserId = actingUserId(program);
58
+ return asUserId ? { ...req, asUserId } : req;
59
+ };
60
+ // The server takes ids only, so a name is resolved here against list-mine.
61
+ const resolve = async (agent) => {
62
+ if (AGENT_ID.test(agent))
63
+ return agent;
64
+ const mine = await send(withActor({ method: "GET", path: "/api/v1/agents/mine", query: { includeShadow: "true" } }));
65
+ return agentIdByName(agent, mine);
66
+ };
67
+ const run = async (agent, req) => {
68
+ const result = await send(withActor(req(await resolve(agent))));
69
+ if (result != null)
70
+ output(result);
71
+ };
72
+ agents.command("stop")
73
+ .description("Stop an agent you own. Its runtime returns to cold-idle.")
74
+ .argument("<agent>", "agent id or exact name")
75
+ .action((agent) => run(agent, (id) => ({ method: "POST", path: agentPath(id, "stop") })));
76
+ skills.command("list")
77
+ .description("List the agent's skills (core, flavor, private, and market).")
78
+ .argument("<agent>", "agent id or exact name")
79
+ .action((agent) => run(agent, (id) => ({ method: "GET", path: agentPath(id, "skills") })));
80
+ skills.command("files")
81
+ .description("List the files attached to one skill.")
82
+ .argument("<agent>", "agent id or exact name")
83
+ .argument("<category>", "skill category")
84
+ .argument("<skill>", "skill name")
85
+ .action((agent, category, skill) => run(agent, (id) => ({
86
+ method: "GET",
87
+ path: agentPath(id, `skills/${encodeURIComponent(category)}/${encodeURIComponent(skill)}/files`),
88
+ })));
89
+ skills.command("read")
90
+ .description("Read one skill file.")
91
+ .argument("<agent>", "agent id or exact name")
92
+ .argument("<path>", "path relative to the skill root, e.g. research/web-research/SKILL.md")
93
+ .action((agent, path) => {
94
+ const file = skillPath(path);
95
+ return run(agent, (id) => ({ method: "GET", path: agentPath(id, `skill-files/${file}`) }));
96
+ });
97
+ skills.command("enabled")
98
+ .description("Enable or disable one skill.")
99
+ .argument("<agent>", "agent id or exact name")
100
+ .argument("<name>", "skill name")
101
+ .argument("<true|false>", "new state")
102
+ .action((agent, name, enabled) => {
103
+ const body = { enabled: bool(enabled) };
104
+ return run(agent, (id) => ({ method: "PUT", path: agentPath(id, `skills/${encodeURIComponent(name)}/state`), body }));
105
+ });
106
+ skills.command("installed")
107
+ .description("List the market skills installed on the agent.")
108
+ .argument("<agent>", "agent id or exact name")
109
+ .action((agent) => run(agent, (id) => ({ method: "GET", path: agentPath(id, "market-skills") })));
110
+ skills.command("install")
111
+ .description("Install or update a market skill at an exact version.")
112
+ .argument("<agent>", "agent id or exact name")
113
+ .argument("<ID@VERSION>", "market skill and version, e.g. web-research@1.0.0")
114
+ .action((agent, ref) => {
115
+ const body = parseSkillRef(ref);
116
+ return run(agent, (id) => ({ method: "POST", path: agentPath(id, "market-skills"), body }));
117
+ });
118
+ skills.command("uninstall")
119
+ .description("Remove a market skill.")
120
+ .argument("<agent>", "agent id or exact name")
121
+ .argument("<id>", "market skill id")
122
+ .action((agent, id) => {
123
+ const skill = encodeURIComponent(skillId(id));
124
+ return run(agent, (agentId) => ({ method: "DELETE", path: agentPath(agentId, `market-skills/${skill}`) }));
125
+ });
126
+ }
127
+ //# sourceMappingURL=agent-skills.js.map
@@ -0,0 +1,191 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { actingUserId, resolveHermesKey } from "../auth.js";
4
+ import { call } from "../client.js";
5
+ import { hermesCall } from "../hermes-client.js";
6
+ export function messageText(opts) {
7
+ if (opts.message !== undefined && opts.messageFile !== undefined) {
8
+ throw new Error("Use either --message or --message-file, not both.");
9
+ }
10
+ const message = opts.messageFile !== undefined ? readFileSync(opts.messageFile, "utf8").trim() : String(opts.message || "").trim();
11
+ if (!message)
12
+ throw new Error("One of --message or --message-file is required and must not be blank.");
13
+ return message;
14
+ }
15
+ export function chatMessageBody(text) {
16
+ return { parts: [{ type: "text", text }] };
17
+ }
18
+ export function assistantIdFrom(value) {
19
+ const agents = Array.isArray(value?.data) ? value.data : [];
20
+ const assistant = agents.find((item) => item?.kind === "shadow");
21
+ if (!assistant?.id || typeof assistant.id !== "string") {
22
+ throw new Error("Your default Assistant is not ready. Open WorkCord once to provision it, or pass --agent <id>.");
23
+ }
24
+ return assistant.id;
25
+ }
26
+ export function chatIdFrom(value) {
27
+ const item = value;
28
+ const id = item?.chatId ?? item?.chat_id ?? item?.contextId ?? item?.context_id ?? item?.id;
29
+ if (typeof id !== "string" || !id)
30
+ throw new Error("Hub response did not contain a chat ID.");
31
+ return id;
32
+ }
33
+ export function messageIdFrom(value) {
34
+ const item = value;
35
+ const nested = item?.message;
36
+ const id = item?.messageId ?? item?.message_id ?? item?.id ?? nested?.messageId ?? nested?.message_id ?? nested?.id;
37
+ if (typeof id !== "string" || !id)
38
+ throw new Error("Hub response did not contain a chat message ID.");
39
+ return id;
40
+ }
41
+ export function messageState(value) {
42
+ const item = value;
43
+ const message = item?.message ?? item;
44
+ const status = message?.status;
45
+ const state = status && typeof status === "object" ? status.state : status ?? message?.state;
46
+ return String(state || "").toLowerCase();
47
+ }
48
+ export function messageRevision(value) {
49
+ const item = value;
50
+ const nested = item?.message;
51
+ const revision = Number(nested?.revision ?? item?.revision);
52
+ return Number.isSafeInteger(revision) && revision >= 0 ? revision : 0;
53
+ }
54
+ export function parseDuration(value = "2m") {
55
+ const match = /^(\d+)(ms|s|m|h)?$/.exec(value);
56
+ if (!match)
57
+ throw new Error(`Invalid duration: ${value}`);
58
+ const unit = (match[2] || "s");
59
+ const multiplier = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[unit];
60
+ const duration = Number(match[1]) * multiplier;
61
+ if (!Number.isSafeInteger(duration) || duration <= 0)
62
+ throw new Error("Timeout must be greater than zero.");
63
+ return duration;
64
+ }
65
+ export async function waitForChatMessage(initial, timeoutMs, getLatest) {
66
+ let latest = initial;
67
+ const messageId = messageIdFrom(latest);
68
+ const terminal = new Set(["completed", "failed", "canceled", "cancelled"]);
69
+ const deadline = Date.now() + timeoutMs;
70
+ while (!terminal.has(messageState(latest))) {
71
+ const remaining = deadline - Date.now();
72
+ if (remaining <= 0)
73
+ return { message: latest, timedOut: true };
74
+ const waitSeconds = Math.max(1, Math.min(30, Math.ceil(remaining / 1000)));
75
+ latest = await getLatest(messageId, messageRevision(latest), waitSeconds);
76
+ }
77
+ return { message: latest, timedOut: false };
78
+ }
79
+ function tenantPath(creds, suffix) {
80
+ if (!creds.tenantId)
81
+ throw new Error("Hermes tenant ID is unavailable. Re-run with --refresh.");
82
+ return `/v1/tenants/${encodeURIComponent(creds.tenantId)}${suffix}`;
83
+ }
84
+ function out(value) {
85
+ process.stdout.write(JSON.stringify(value) + "\n");
86
+ }
87
+ function chatFlags(command) {
88
+ return command
89
+ .option("--agent <agentId>", "explicit alternate owned agent; omit to use your default Assistant")
90
+ .option("--refresh", "re-reveal the tenant key instead of using the cached one")
91
+ .option("--no-cache", "do not read or write the local tenant-key cache");
92
+ }
93
+ function messageFlags(command) {
94
+ return command
95
+ .option("--message <text>", "message to send to the agent")
96
+ .option("--message-file <path>", "read the message from a file")
97
+ .option("--async", "return after Hermes queues the message")
98
+ .option("--timeout <duration>", "maximum local wait, such as 30s or 5m", "2m")
99
+ .option("--idempotency-key <key>", "stable retry key; generated when omitted");
100
+ }
101
+ async function selectedAgentId(program, opts) {
102
+ if (opts.agent?.trim())
103
+ return opts.agent.trim();
104
+ return assistantIdFrom(await call({
105
+ path: "/api/v1/agents/mine",
106
+ query: { includeShadow: true },
107
+ asUserId: actingUserId(program),
108
+ }));
109
+ }
110
+ async function credentials(program, opts) {
111
+ const agentId = await selectedAgentId(program, opts);
112
+ const creds = await resolveHermesKey(agentId, {
113
+ refresh: opts.refresh,
114
+ cache: opts.cache,
115
+ asUserId: actingUserId(program),
116
+ });
117
+ return { agentId, creds };
118
+ }
119
+ async function sendAndWait(creds, chatId, text, opts) {
120
+ const queued = await hermesCall({
121
+ ...creds,
122
+ method: "POST",
123
+ path: tenantPath(creds, `/chats/${encodeURIComponent(chatId)}/messages`),
124
+ headers: { "idempotency-key": opts.idempotencyKey ?? randomUUID() },
125
+ body: chatMessageBody(text),
126
+ });
127
+ if (opts.async)
128
+ return { message: queued, timedOut: false };
129
+ return waitForChatMessage(queued, parseDuration(opts.timeout), (messageId, afterRevision, waitSeconds) => hermesCall({
130
+ ...creds,
131
+ path: tenantPath(creds, `/chat-messages/${encodeURIComponent(messageId)}`),
132
+ query: { afterRevision, waitSeconds },
133
+ }));
134
+ }
135
+ export function registerAgent(program) {
136
+ const group = program.command("agent").description("Talk to your Assistant or another owned agent through Hermes tenant chat");
137
+ const chat = group.command("chat").description("Persistent tenant conversations for instructions, skills, flavors, and agent work");
138
+ messageFlags(chatFlags(chat.command("new").description("Create a chat, send its first message, and wait for the agent.")))
139
+ .option("--title <title>", "optional chat title")
140
+ .action(async (opts) => {
141
+ const text = messageText(opts);
142
+ const { agentId, creds } = await credentials(program, opts);
143
+ const created = await hermesCall({
144
+ ...creds,
145
+ method: "POST",
146
+ path: tenantPath(creds, "/chats"),
147
+ body: opts.title ? { title: opts.title } : {},
148
+ });
149
+ const chatId = chatIdFrom(created);
150
+ const result = await sendAndWait(creds, chatId, text, opts);
151
+ out({ agentId, chatId, ...result });
152
+ });
153
+ messageFlags(chatFlags(chat.command("send").description("Continue an existing chat and wait for the agent.")))
154
+ .requiredOption("--chat <chatId>", "chat id returned by chat new")
155
+ .action(async (opts) => {
156
+ const text = messageText(opts);
157
+ const { agentId, creds } = await credentials(program, opts);
158
+ out({ agentId, chatId: opts.chat, ...(await sendAndWait(creds, opts.chat, text, opts)) });
159
+ });
160
+ chatFlags(chat.command("list").description("List the selected agent's chats.")).action(async (opts) => {
161
+ const { agentId, creds } = await credentials(program, opts);
162
+ out({ agentId, result: await hermesCall({ ...creds, path: tenantPath(creds, "/chats") }) });
163
+ });
164
+ chatFlags(chat.command("history").description("List messages in one chat."))
165
+ .requiredOption("--chat <chatId>", "chat id")
166
+ .action(async (opts) => {
167
+ const { agentId, creds } = await credentials(program, opts);
168
+ out({
169
+ agentId,
170
+ chatId: opts.chat,
171
+ result: await hermesCall({ ...creds, path: tenantPath(creds, `/chats/${encodeURIComponent(opts.chat)}/messages`) }),
172
+ });
173
+ });
174
+ chatFlags(chat.command("wait").description("Wait for one queued chat message to finish."))
175
+ .requiredOption("--message-id <messageId>", "message id returned by chat new/send --async")
176
+ .option("--timeout <duration>", "maximum local wait, such as 30s or 5m", "2m")
177
+ .action(async (opts) => {
178
+ const { agentId, creds } = await credentials(program, opts);
179
+ const initial = await hermesCall({ ...creds, path: tenantPath(creds, `/chat-messages/${encodeURIComponent(opts.messageId)}`) });
180
+ out({
181
+ agentId,
182
+ messageId: opts.messageId,
183
+ ...(await waitForChatMessage(initial, parseDuration(opts.timeout), (messageId, afterRevision, waitSeconds) => hermesCall({
184
+ ...creds,
185
+ path: tenantPath(creds, `/chat-messages/${encodeURIComponent(messageId)}`),
186
+ query: { afterRevision, waitSeconds },
187
+ }))),
188
+ });
189
+ });
190
+ }
191
+ //# sourceMappingURL=agent.js.map
@@ -29,10 +29,10 @@ function promptFrom(opts) {
29
29
  return opts.promptFile ? readFileSync(opts.promptFile, "utf8") : opts.prompt;
30
30
  }
31
31
  export function registerCustomAgents(program) {
32
- const group = getOrCreateGroup(program, "custom-agents", "CustomAgents operations");
32
+ const group = getOrCreateGroup(program, "custom-agents", "Legacy hosted-agent administration; use agent chat for Hermes agents");
33
33
  group
34
34
  .command("create-hosted")
35
- .description("Create a WorkCord-hosted custom agent with typed flags.")
35
+ .description("Create a legacy WorkCord-hosted agent directly. For Hermes agents, ask your Assistant instead.")
36
36
  .requiredOption("--name <name>", "agent name")
37
37
  .requiredOption("--description <text>", "short description")
38
38
  .option("--prompt <text>", "system prompt")
@@ -66,7 +66,7 @@ export function registerCustomAgents(program) {
66
66
  });
67
67
  group
68
68
  .command("update-hosted")
69
- .description("Update a WorkCord-hosted custom agent with typed flags.")
69
+ .description("Update a legacy WorkCord-hosted agent directly. For Hermes agents, use agent chat.")
70
70
  .requiredOption("--agent <agentId>", "hosted custom agent id")
71
71
  .option("--name <name>", "agent name")
72
72
  .option("--description <text>", "short description")
@@ -7,19 +7,21 @@ import { hermesBinary, hermesCall, hermesStream, hermesUpload } from "../hermes-
7
7
  import { assertArchiveSize, flavorSetWarnings, guardOwnership, rolloutBody, } from "../hermes-guards.js";
8
8
  import { getOrCreateGroup } from "./_group.js";
9
9
  export function agentRef(opts) {
10
- if (opts.agent && opts.roomAgent) {
11
- throw new Error("Pass exactly one of --agent <id> or --room-agent <id>, not both.");
10
+ const refs = [opts.agent, opts.roomAgent, opts.marketplaceInstall].filter(Boolean);
11
+ if (refs.length !== 1) {
12
+ throw new Error("Pass exactly one of --agent <id>, --room-agent <id>, or --marketplace-install <id>.");
12
13
  }
13
- if (opts.agent)
14
- return opts.agent;
15
14
  if (opts.roomAgent)
16
15
  return `workcord-room-agent:${opts.roomAgent}`;
17
- throw new Error("One of --agent <id> or --room-agent <id> is required.");
16
+ if (opts.marketplaceInstall)
17
+ return `marketplace-install:${opts.marketplaceInstall}`;
18
+ return opts.agent;
18
19
  }
19
20
  function agentFlags(cmd) {
20
21
  return cmd
21
22
  .option("--agent <id>", "custom agent id")
22
23
  .option("--room-agent <id>", "room agent id (resolved as workcord-room-agent:<id>)")
24
+ .option("--marketplace-install <id>", "marketplace flavor install id or its room agent id")
23
25
  .option("--refresh", "re-reveal the tenant key instead of using the cached one")
24
26
  .option("--no-cache", "do not read or write the local tenant-key cache");
25
27
  }
@@ -39,6 +41,9 @@ function parseJson(raw, flag) {
39
41
  function out(data) {
40
42
  process.stdout.write(JSON.stringify(data) + "\n");
41
43
  }
44
+ export function hermesChatTextBody(text) {
45
+ return { parts: [{ type: "text", text }] };
46
+ }
42
47
  async function credentials(program, opts) {
43
48
  return resolveHermesKey(agentRef(opts), {
44
49
  refresh: opts.refresh,
@@ -98,7 +103,7 @@ async function readArchive(archivePath) {
98
103
  return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
99
104
  }
100
105
  export function registerHermes(program) {
101
- const group = getOrCreateGroup(program, "hermes", "Hermes Hub operations for a WorkCord agent's tenant");
106
+ const group = getOrCreateGroup(program, "hermes", "Advanced Hermes Hub diagnostics and recovery for an agent tenant");
102
107
  agentFlags(group.command("key").description("Reveal (and cache) the Hermes Hub tenant key for an agent.")).action(async (opts) => {
103
108
  const creds = await credentials(program, opts);
104
109
  out({ key: creds.key, tenantId: creds.tenantId, hubBaseUrl: creds.hubBaseUrl });
@@ -157,7 +162,7 @@ export function registerHermes(program) {
157
162
  .option("--idempotency-key <key>", "Idempotency-Key header (generated when omitted)")
158
163
  .action(async (opts) => {
159
164
  const creds = await credentials(program, opts);
160
- const body = parseJson(opts.body, "--body") ?? (opts.text !== undefined ? { text: opts.text } : undefined);
165
+ const body = parseJson(opts.body, "--body") ?? (opts.text !== undefined ? hermesChatTextBody(opts.text) : undefined);
161
166
  if (body === undefined) {
162
167
  throw new Error("One of --text or --body is required.");
163
168
  }