@fulldivevr/workcord-cli 1.2.1 → 1.3.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 +70 -19
- package/dist/cli.js +3 -1
- package/dist/commands/agent.js +191 -0
- package/dist/commands/custom-agents.js +3 -3
- package/dist/commands/hermes.js +12 -7
- package/dist/generated/commands/call-recordings.js +63 -0
- package/dist/generated/commands/internal.js +14 -0
- package/dist/generated/commands/messages.js +15 -0
- package/dist/generated/index.js +2 -0
- package/dist/hermes-guards.js +1 -1
- package/package.json +2 -2
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.
|
|
@@ -206,7 +250,7 @@ workcord --as USER rooms discover --search "ai" --tags ai,tools --limit 20
|
|
|
206
250
|
workcord --as USER rooms create-with-message --name "ops" --public --tags ai,news --text "First post."
|
|
207
251
|
workcord --as USER rooms members --room R
|
|
208
252
|
workcord --as USER rooms upload-icon-file --room R --file ./icon.png
|
|
209
|
-
workcord --as USER
|
|
253
|
+
workcord --as USER agent chat new --message "Create a room helper that can research the web."
|
|
210
254
|
workcord --as USER room-agents install-hosted --room R --agent system-wizeup
|
|
211
255
|
workcord --as USER messages react --room R --message M --emoji 👍
|
|
212
256
|
workcord --as USER dm send-text --to USER_B --text "hi"
|
|
@@ -272,12 +316,14 @@ workcord --as $U dm send-text --to $OTHER --text "hey"
|
|
|
272
316
|
# → composite: opens (or finds) the DM room, sends the message, returns both
|
|
273
317
|
```
|
|
274
318
|
|
|
275
|
-
## Hermes
|
|
319
|
+
## Advanced Hermes diagnostics and recovery (`hermes` namespace)
|
|
276
320
|
|
|
277
|
-
WorkCord provisions a **Hermes Hub tenant** for each room-agent
|
|
278
|
-
|
|
279
|
-
files, skills, flavor pin, A2A peers and
|
|
280
|
-
|
|
321
|
+
WorkCord provisions a **Hermes Hub tenant** for each room-agent, Hermes custom
|
|
322
|
+
agent, and room-scoped marketplace flavor install. That tenant is where the
|
|
323
|
+
agent actually runs: its chats, runs, files, skills, flavor pin, A2A peers and
|
|
324
|
+
eval runs all live on the Hub, not in WorkCord. Normal changes should be
|
|
325
|
+
requested with `workcord agent chat`; the `hermes` namespace exists for direct
|
|
326
|
+
inspection, diagnostics, and recovery.
|
|
281
327
|
|
|
282
328
|
This is the one namespace that talks to a **second origin**. Every other command
|
|
283
329
|
goes to the WorkCord API; `hermes` uses the WorkCord API only to reveal the
|
|
@@ -291,8 +337,11 @@ Every `hermes` command takes exactly one of:
|
|
|
291
337
|
| --- | --- |
|
|
292
338
|
| `--agent <id>` | a custom agent id, used verbatim |
|
|
293
339
|
| `--room-agent <id>` | a room agent id, resolved as `workcord-room-agent:<id>` |
|
|
340
|
+
| `--marketplace-install <id>` | an isolated marketplace flavor install id or its room agent id |
|
|
294
341
|
|
|
295
|
-
Passing
|
|
342
|
+
Passing more than one, or none, is an error. Marketplace install access follows
|
|
343
|
+
current room ownership; transferring room ownership revokes the former
|
|
344
|
+
creator's ability to reveal the tenant credential.
|
|
296
345
|
|
|
297
346
|
### Credentials
|
|
298
347
|
|
|
@@ -346,6 +395,7 @@ material.
|
|
|
346
395
|
|
|
347
396
|
```bash
|
|
348
397
|
workcord hermes whoami --room-agent $RA
|
|
398
|
+
workcord hermes whoami --marketplace-install $MARKETPLACE_INSTALL
|
|
349
399
|
workcord hermes chat send --room-agent $RA --chat $CHAT --text "status?"
|
|
350
400
|
workcord hermes flavor current --agent $AGENT
|
|
351
401
|
workcord hermes peers find --agent $AGENT --query "translation" --limit 5
|
|
@@ -355,15 +405,16 @@ Version flags are named `--flavor-version` and `--skill-version`, not
|
|
|
355
405
|
`--version`: commander's program-level `.version()` matches `--version`
|
|
356
406
|
anywhere in argv, prints the CLI version and exits before the subcommand runs.
|
|
357
407
|
|
|
358
|
-
###
|
|
408
|
+
### Direct flavor and skill recovery
|
|
409
|
+
|
|
410
|
+
For normal work, ask the Assistant or another owned agent to create, update,
|
|
411
|
+
validate, or activate its own flavor and skills. Hermes Hub supports
|
|
412
|
+
tenant-owned skill and flavor namespaces; ownership defaults to whichever agent
|
|
413
|
+
tenant performs the creation.
|
|
359
414
|
|
|
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.
|
|
415
|
+
The direct `flavor publish`, `flavor rollout`, and `skill publish` commands are
|
|
416
|
+
kept for advanced recovery and authorized publisher workflows. They operate as
|
|
417
|
+
the selected tenant and can only mutate namespaces that tenant owns.
|
|
367
418
|
|
|
368
419
|
The CLI does **not** pack or untar archives. The authoring loop is:
|
|
369
420
|
|
package/dist/cli.js
CHANGED
|
@@ -13,13 +13,14 @@ 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";
|
|
16
17
|
import { registerAll as registerGenerated } from "./generated/index.js";
|
|
17
18
|
const program = new Command();
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
19
20
|
const { version } = require("../package.json");
|
|
20
21
|
program
|
|
21
22
|
.name("workcord")
|
|
22
|
-
.description("
|
|
23
|
+
.description("Talk to WorkCord agents through Hermes tenant chat, with advanced API diagnostics.")
|
|
23
24
|
.version(version)
|
|
24
25
|
.option("--as <userId>", "user to act as")
|
|
25
26
|
.option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL and saved configuration)");
|
|
@@ -27,6 +28,7 @@ program.hook("preAction", () => {
|
|
|
27
28
|
setBaseUrlOverride(program.opts().baseUrl);
|
|
28
29
|
});
|
|
29
30
|
registerWhoami(program);
|
|
31
|
+
registerAgent(program);
|
|
30
32
|
registerAuth(program);
|
|
31
33
|
registerMessages(program);
|
|
32
34
|
registerRooms(program);
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { call } from "../../client.js";
|
|
2
|
+
import { actingUserId } from "../../auth.js";
|
|
3
|
+
import { getOrCreateGroup, hasSubcommand } from "../_helpers.js";
|
|
4
|
+
export function register(program) {
|
|
5
|
+
const group = getOrCreateGroup(program, "call-recordings", "CallRecordings operations");
|
|
6
|
+
if (!hasSubcommand(group, "live-state")) {
|
|
7
|
+
group
|
|
8
|
+
.command("live-state")
|
|
9
|
+
.description("GET /api/v1/rooms/{roomId}/call/recording")
|
|
10
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const as = actingUserId(program);
|
|
13
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call/recording`;
|
|
14
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
15
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (!hasSubcommand(group, "live-segments")) {
|
|
19
|
+
group
|
|
20
|
+
.command("live-segments")
|
|
21
|
+
.description("GET /api/v1/rooms/{roomId}/call/transcript/live")
|
|
22
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
const as = actingUserId(program);
|
|
25
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call/transcript/live`;
|
|
26
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
27
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (!hasSubcommand(group, "list")) {
|
|
31
|
+
group
|
|
32
|
+
.command("list")
|
|
33
|
+
.description("GET /api/v1/rooms/{roomId}/call/recordings")
|
|
34
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
35
|
+
.option("--limit <value>", "string")
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const as = actingUserId(program);
|
|
38
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call/recordings`;
|
|
39
|
+
const query = {};
|
|
40
|
+
{
|
|
41
|
+
const v = opts.limit;
|
|
42
|
+
if (v !== undefined)
|
|
43
|
+
query["limit"] = v;
|
|
44
|
+
}
|
|
45
|
+
const data = await call({ method: "GET", path, asUserId: as, query });
|
|
46
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!hasSubcommand(group, "transcript")) {
|
|
50
|
+
group
|
|
51
|
+
.command("transcript")
|
|
52
|
+
.description("GET /api/v1/rooms/{roomId}/call/recordings/{recordingId}/transcript")
|
|
53
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
54
|
+
.requiredOption("--recording-id <value>", "recordingId (path param)")
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const as = actingUserId(program);
|
|
57
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call/recordings/${encodeURIComponent(opts.recordingId)}/transcript`;
|
|
58
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
59
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=call-recordings.js.map
|
|
@@ -3,6 +3,20 @@ import { actingUserId } from "../../auth.js";
|
|
|
3
3
|
import { getOrCreateGroup, hasSubcommand } from "../_helpers.js";
|
|
4
4
|
export function register(program) {
|
|
5
5
|
const group = getOrCreateGroup(program, "internal", "Internal operations");
|
|
6
|
+
if (!hasSubcommand(group, "append-call-segments")) {
|
|
7
|
+
group
|
|
8
|
+
.command("append-call-segments")
|
|
9
|
+
.description("POST /api/v1/internal/calls/{callId}/segments")
|
|
10
|
+
.requiredOption("--call-id <value>", "callId (path param)")
|
|
11
|
+
.option("--body <json>", "request body as JSON (no schema available)")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const as = actingUserId(program);
|
|
14
|
+
const path = `/api/v1/internal/calls/${encodeURIComponent(opts.callId)}/segments`;
|
|
15
|
+
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
16
|
+
const data = await call({ method: "POST", path, asUserId: as, body });
|
|
17
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
18
|
+
});
|
|
19
|
+
}
|
|
6
20
|
if (!hasSubcommand(group, "emit-message")) {
|
|
7
21
|
group
|
|
8
22
|
.command("emit-message")
|
|
@@ -383,5 +383,20 @@ export function register(program) {
|
|
|
383
383
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
|
+
if (!hasSubcommand(group, "submit-agent-feedback")) {
|
|
387
|
+
group
|
|
388
|
+
.command("submit-agent-feedback")
|
|
389
|
+
.description("POST /api/v1/rooms/{roomId}/messages/{messageId}/agent-feedback")
|
|
390
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
391
|
+
.requiredOption("--message-id <value>", "messageId (path param)")
|
|
392
|
+
.option("--body <json>", "request body as JSON (no schema available)")
|
|
393
|
+
.action(async (opts) => {
|
|
394
|
+
const as = actingUserId(program);
|
|
395
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/messages/${encodeURIComponent(opts.messageId)}/agent-feedback`;
|
|
396
|
+
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
397
|
+
const data = await call({ method: "POST", path, asUserId: as, body });
|
|
398
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
399
|
+
});
|
|
400
|
+
}
|
|
386
401
|
}
|
|
387
402
|
//# sourceMappingURL=messages.js.map
|
package/dist/generated/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { register as register_ambassador } from "./commands/ambassador.js";
|
|
|
4
4
|
import { register as register_apple_iap } from "./commands/apple-iap.js";
|
|
5
5
|
import { register as register_attribution } from "./commands/attribution.js";
|
|
6
6
|
import { register as register_auth } from "./commands/auth.js";
|
|
7
|
+
import { register as register_call_recordings } from "./commands/call-recordings.js";
|
|
7
8
|
import { register as register_challenges } from "./commands/challenges.js";
|
|
8
9
|
import { register as register_clips } from "./commands/clips.js";
|
|
9
10
|
import { register as register_contact_match } from "./commands/contact-match.js";
|
|
@@ -70,6 +71,7 @@ export function registerAll(program) {
|
|
|
70
71
|
register_apple_iap(program);
|
|
71
72
|
register_attribution(program);
|
|
72
73
|
register_auth(program);
|
|
74
|
+
register_call_recordings(program);
|
|
73
75
|
register_challenges(program);
|
|
74
76
|
register_clips(program);
|
|
75
77
|
register_contact_match(program);
|
package/dist/hermes-guards.js
CHANGED
|
@@ -51,7 +51,7 @@ export function assertArchiveSize(bytes, path) {
|
|
|
51
51
|
}
|
|
52
52
|
export function ownershipError(kind, namespaceId, tenantId, body) {
|
|
53
53
|
const who = tenantId || "this tenant";
|
|
54
|
-
const err = new Error(`Hermes Hub refused this as ${who}: it does not own the ${kind} namespace "${namespaceId}".
|
|
54
|
+
const err = new Error(`Hermes Hub refused this as ${who}: it does not own the ${kind} namespace "${namespaceId}". For normal customization, ask the owning agent with workcord agent chat; it can create or update resources in its own namespace. Direct Hermes publishing is advanced recovery and requires an owned namespace.`);
|
|
55
55
|
err.status = 403;
|
|
56
56
|
err.body = body;
|
|
57
57
|
return err;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fulldivevr/workcord-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Agent-first CLI using persistent Hermes tenant chat, with advanced API diagnostics.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|