@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.
- package/README.md +75 -19
- package/dist/cli.js +7 -1
- package/dist/client.js +8 -1
- package/dist/commands/agent-skills.js +127 -0
- package/dist/commands/agent.js +191 -0
- package/dist/commands/custom-agents.js +3 -3
- package/dist/commands/hermes.js +12 -7
- package/dist/commands/messages.js +150 -4
- package/dist/commands/reflections.js +96 -0
- package/dist/commands/rooms.js +7 -3
- package/dist/generated/commands/agent-feedback-admin.js +69 -0
- package/dist/generated/commands/ai.js +31 -0
- package/dist/generated/commands/call-recordings.js +119 -0
- package/dist/generated/commands/contact-match.js +20 -1
- package/dist/generated/commands/credit-packs.js +112 -0
- package/dist/generated/commands/custom-agents.js +483 -1
- package/dist/generated/commands/desktop-releases.js +2 -2
- package/dist/generated/commands/edge-sizecheck.js +25 -0
- package/dist/generated/commands/feed.js +6 -0
- package/dist/generated/commands/interactive-events.js +47 -0
- package/dist/generated/commands/internal.js +205 -0
- package/dist/generated/commands/meetings.js +12 -0
- package/dist/generated/commands/messages.js +30 -0
- package/dist/generated/commands/my-feedback.js +18 -0
- package/dist/generated/commands/pinned-messages.js +6 -0
- package/dist/generated/commands/proxy-nodes.js +83 -0
- package/dist/generated/commands/push.js +20 -0
- package/dist/generated/commands/reviews.js +96 -0
- package/dist/generated/commands/room-agents.js +243 -1
- package/dist/generated/commands/room-invites.js +12 -0
- package/dist/generated/commands/rooms.js +79 -1
- package/dist/generated/commands/search.js +1 -1
- package/dist/generated/commands/shadow-dispatch-push.js +20 -0
- package/dist/generated/commands/shadow-reflections.js +111 -0
- package/dist/generated/index.js +22 -0
- package/dist/hermes-guards.js +1 -1
- package/dist/http.js +6 -2
- package/dist/room-types.js +10 -0
- package/package.json +2 -2
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
324
|
+
## Advanced Hermes diagnostics and recovery (`hermes` namespace)
|
|
276
325
|
|
|
277
|
-
WorkCord provisions a **Hermes Hub tenant** for each room-agent
|
|
278
|
-
|
|
279
|
-
files, skills, flavor pin, A2A peers and
|
|
280
|
-
|
|
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
|
|
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
|
-
###
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
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("
|
|
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
|
|
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", "
|
|
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
|
|
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
|
|
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")
|
package/dist/commands/hermes.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
}
|