@fulldivevr/workcord-cli 1.6.0 → 2.17.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 +86 -41
- package/dist/auth.js +24 -8
- package/dist/cli.js +3 -3
- package/dist/commands/hub.js +667 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fulldivevr/workcord-cli
|
|
2
2
|
|
|
3
|
-
TypeScript CLI for talking to WorkCord agents through persistent
|
|
3
|
+
TypeScript CLI for talking to WorkCord agents through persistent Autoworker Hub tenant
|
|
4
4
|
chat and using the WorkCord API. **One invocation = one user action.** Designed
|
|
5
5
|
for automated agents such as Claude Code: ask the user's Assistant for the
|
|
6
6
|
outcome you need, let that tenant manage its own instructions, skills, and
|
|
@@ -149,7 +149,7 @@ workcord [--as <userId>] <namespace> <command> [flags]
|
|
|
149
149
|
### Agent-first customization
|
|
150
150
|
|
|
151
151
|
To change agent behavior, create or update a skill, or maintain a flavor, talk
|
|
152
|
-
to the owning agent through
|
|
152
|
+
to the owning agent through Hub tenant chat. Do not edit prompts or tenant
|
|
153
153
|
files directly. With no `--agent`, the user's non-deletable Assistant is the
|
|
154
154
|
target and becomes the default owner of anything it creates:
|
|
155
155
|
|
|
@@ -164,7 +164,7 @@ Use the returned `chatId` for follow-up, verification, or rollback:
|
|
|
164
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
165
|
```
|
|
166
166
|
|
|
167
|
-
Both commands wait up to two minutes by default and return the durable
|
|
167
|
+
Both commands wait up to two minutes by default and return the durable Hub
|
|
168
168
|
message document. Use `--async` for a queued response, then continue with its
|
|
169
169
|
`messageId`:
|
|
170
170
|
|
|
@@ -184,8 +184,8 @@ workcord agent chat new --agent "$AGENT_ID" --message "Create and maintain a fin
|
|
|
184
184
|
|
|
185
185
|
The Assistant or selected agent works within its tenant's Hub permissions. It
|
|
186
186
|
can safely validate changes and report what it activated. `agent chat` uses
|
|
187
|
-
|
|
188
|
-
A2A remains available for external agent peers. Direct `
|
|
187
|
+
the Hub's native durable `hub_chat` transport; it is not raw A2A wire traffic.
|
|
188
|
+
A2A remains available for external agent peers. Direct `hub` commands are
|
|
189
189
|
advanced diagnostics and recovery tools, not the normal customization workflow.
|
|
190
190
|
|
|
191
191
|
- `workcord --help` lists namespaces.
|
|
@@ -321,22 +321,25 @@ workcord --as $U dm send-text --to $OTHER --text "hey"
|
|
|
321
321
|
# → composite: opens (or finds) the DM room, sends the message, returns both
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
-
## Advanced
|
|
324
|
+
## Advanced Hub diagnostics and recovery (`hub` namespace)
|
|
325
325
|
|
|
326
|
-
WorkCord provisions
|
|
326
|
+
WorkCord provisions an **Autoworker Hub tenant** for each room-agent, custom
|
|
327
327
|
agent, and room-scoped marketplace flavor install. That tenant is where the
|
|
328
328
|
agent actually runs: its chats, runs, files, skills, flavor pin, A2A peers and
|
|
329
329
|
eval runs all live on the Hub, not in WorkCord. Normal changes should be
|
|
330
|
-
requested with `workcord agent chat`; the `
|
|
330
|
+
requested with `workcord agent chat`; the `hub` namespace exists for direct
|
|
331
331
|
inspection, diagnostics, and recovery.
|
|
332
332
|
|
|
333
333
|
This is the one namespace that talks to a **second origin**. Every other command
|
|
334
|
-
goes to the WorkCord API; `
|
|
334
|
+
goes to the WorkCord API; `hub` uses the WorkCord API only to reveal the
|
|
335
335
|
tenant key, then calls the Hub's `/v1/tenant-api/*` routes with it.
|
|
336
336
|
|
|
337
|
+
`workcord hub ...` is a hidden alias of `workcord hub ...`, kept for one
|
|
338
|
+
release. New scripts should use `hub`.
|
|
339
|
+
|
|
337
340
|
### Target selection
|
|
338
341
|
|
|
339
|
-
Every `
|
|
342
|
+
Every `hub` command takes exactly one of:
|
|
340
343
|
|
|
341
344
|
| Flag | Meaning |
|
|
342
345
|
| --- | --- |
|
|
@@ -355,11 +358,16 @@ and is **never** accepted as a flag:
|
|
|
355
358
|
|
|
356
359
|
| Env var | Meaning |
|
|
357
360
|
| --- | --- |
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
+
| `WORKCORD_HUB_KEY` | tenant key, used verbatim; requires `WORKCORD_HUB_URL` |
|
|
362
|
+
| `WORKCORD_HUB_URL` | Hub origin (bare origin, no path); requires `WORKCORD_HUB_KEY` |
|
|
363
|
+
| `WORKCORD_HUB_TENANT_ID` | tenant id for the tenant-scoped routes, alongside the two above |
|
|
364
|
+
| `WORKCORD_HUB_MANAGED` | `1` enables managed mode (see below) |
|
|
365
|
+
|
|
366
|
+
The older `WORKCORD_HERMES_KEY`, `WORKCORD_HERMES_HUB_URL`,
|
|
367
|
+
`WORKCORD_HERMES_TENANT_ID` and `WORKCORD_HERMES_MANAGED` names still work and
|
|
368
|
+
take precedence when both are set.
|
|
361
369
|
|
|
362
|
-
Setting only one of `
|
|
370
|
+
Setting only one of `WORKCORD_HUB_KEY` / `WORKCORD_HUB_URL` is an
|
|
363
371
|
error — the CLI will not half-guess a hub.
|
|
364
372
|
|
|
365
373
|
With none of them set, the CLI calls
|
|
@@ -376,34 +384,56 @@ under `hermes`, alongside the saved login. The file is written `0600` inside a
|
|
|
376
384
|
| `--refresh` | ignore the cached entry, re-reveal, rewrite the cache |
|
|
377
385
|
| `--no-cache` | neither read nor write the cache for this invocation |
|
|
378
386
|
|
|
379
|
-
Env credentials always win over the cache. `
|
|
387
|
+
Env credentials always win over the cache. `hub key` prints the resolved
|
|
380
388
|
credentials on stdout — that is its whole job; no other command emits key
|
|
381
389
|
material.
|
|
382
390
|
|
|
391
|
+
### Managed mode
|
|
392
|
+
|
|
393
|
+
A personal Shadow tenant runs this CLI with a WorkCord token that WorkCord
|
|
394
|
+
resolves to the owner user. It must use the same `hub` commands, but with a
|
|
395
|
+
per-call reveal and nothing on disk. Managed mode is enabled by
|
|
396
|
+
`WORKCORD_HUB_MANAGED=1` (or `WORKCORD_HERMES_MANAGED=1`), or per invocation
|
|
397
|
+
with `--managed`. In managed mode the CLI:
|
|
398
|
+
|
|
399
|
+
- always reveals the key through `POST /api/v1/agents/{id}/hermes-cli-key/reveal`,
|
|
400
|
+
- never reads or writes `~/.workcord/config.json` for hub credentials,
|
|
401
|
+
- ignores `WORKCORD_HUB_KEY` / `WORKCORD_HERMES_KEY`,
|
|
402
|
+
- rejects `--as` with an error,
|
|
403
|
+
- never prints the key (`hub key` prints only `tenantId` and `hubBaseUrl`).
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
export WORKCORD_TOKEN=... # the owner user's token
|
|
407
|
+
export WORKCORD_HUB_MANAGED=1
|
|
408
|
+
workcord hub flavor set --agent $AGENT --flavor-version 1.4.0
|
|
409
|
+
workcord hub flavor subscribe --agent $AGENT --policy stable
|
|
410
|
+
workcord hub agent restart --agent $AGENT
|
|
411
|
+
```
|
|
412
|
+
|
|
383
413
|
### Commands
|
|
384
414
|
|
|
385
415
|
| Group | What |
|
|
386
416
|
| --- | --- |
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
417
|
+
| `hub key` / `whoami` / `runtime` | reveal credentials, show the tenant, show its runtime |
|
|
418
|
+
| `hub agent status\|start\|stop\|restart\|card` | tenant agent lifecycle |
|
|
419
|
+
| `hub chat list\|create\|get\|messages\|send\|message\|stream\|stop\|approve` | chats and chat messages (`stream` is SSE) |
|
|
420
|
+
| `hub run create\|get\|stream\|stop\|approve` | runs |
|
|
421
|
+
| `hub files ls\|cat\|download` | tenant-visible files |
|
|
422
|
+
| `hub share groups\|ls\|read\|stat\|grep\|find\|write\|mkdir\|move\|rm` | share-group filesystem |
|
|
423
|
+
| `hub skills list\|enable\|disable\|files\|cat` | skills installed on this tenant |
|
|
424
|
+
| `hub flavor current\|catalog\|get\|set\|subscribe\|checkout\|publish\|rollout` | flavor marketplace and this tenant's pin |
|
|
425
|
+
| `hub skill catalog\|get\|checkout\|publish` | skill marketplace |
|
|
426
|
+
| `hub eval suites\|run\|list\|get\|cancel` | flavor eval suites and runs |
|
|
427
|
+
| `hub peers list\|get\|call\|find` | A2A peers |
|
|
428
|
+
| `hub conversations list\|get\|messages` | A2A conversations |
|
|
429
|
+
| `hub sessions list`, `hub artifacts` | agent sessions and artifacts |
|
|
400
430
|
|
|
401
431
|
```bash
|
|
402
|
-
workcord
|
|
403
|
-
workcord
|
|
404
|
-
workcord
|
|
405
|
-
workcord
|
|
406
|
-
workcord
|
|
432
|
+
workcord hub whoami --room-agent $RA
|
|
433
|
+
workcord hub whoami --marketplace-install $MARKETPLACE_INSTALL
|
|
434
|
+
workcord hub chat send --room-agent $RA --chat $CHAT --text "status?"
|
|
435
|
+
workcord hub flavor current --agent $AGENT
|
|
436
|
+
workcord hub peers find --agent $AGENT --query "translation" --limit 5
|
|
407
437
|
```
|
|
408
438
|
|
|
409
439
|
Version flags are named `--flavor-version` and `--skill-version`, not
|
|
@@ -413,7 +443,7 @@ anywhere in argv, prints the CLI version and exits before the subcommand runs.
|
|
|
413
443
|
### Direct flavor and skill recovery
|
|
414
444
|
|
|
415
445
|
For normal work, ask the Assistant or another owned agent to create, update,
|
|
416
|
-
validate, or activate its own flavor and skills.
|
|
446
|
+
validate, or activate its own flavor and skills. Autoworker Hub supports
|
|
417
447
|
tenant-owned skill and flavor namespaces; ownership defaults to whichever agent
|
|
418
448
|
tenant performs the creation.
|
|
419
449
|
|
|
@@ -424,9 +454,9 @@ the selected tenant and can only mutate namespaces that tenant owns.
|
|
|
424
454
|
The CLI does **not** pack or untar archives. The authoring loop is:
|
|
425
455
|
|
|
426
456
|
```bash
|
|
427
|
-
workcord
|
|
428
|
-
# unpack, edit, and repack with
|
|
429
|
-
workcord
|
|
457
|
+
workcord hub flavor checkout --agent $AGENT --flavor-version 1.4.0 --out flavor.tgz
|
|
458
|
+
# unpack, edit, and repack with the Hub's own tooling
|
|
459
|
+
workcord hub flavor publish --agent $AGENT --archive flavor.tgz
|
|
430
460
|
```
|
|
431
461
|
|
|
432
462
|
`--archive` must already be a gzipped package built beforehand; anything over
|
|
@@ -467,10 +497,10 @@ without running `regen` if you don't need the latest server changes.
|
|
|
467
497
|
src/
|
|
468
498
|
├── cli.ts commander entry, global --as flag
|
|
469
499
|
├── auth.ts auth resolution: WORKCORD_TOKEN vs minted --as JWT,
|
|
470
|
-
│ plus
|
|
500
|
+
│ plus Autoworker Hub key resolution
|
|
471
501
|
├── http.ts shared fetch primitives (retry, errors, query)
|
|
472
502
|
├── client.ts WorkCord fetch wrapper (JSON + multipart)
|
|
473
|
-
├── hermes-client.ts
|
|
503
|
+
├── hermes-client.ts Autoworker Hub fetch wrapper (JSON, upload, SSE, binary)
|
|
474
504
|
├── hermes-guards.ts rollout/ownership/archive guards, version compare
|
|
475
505
|
├── commands/ hand-written semantic commands
|
|
476
506
|
└── generated/ auto-generated from openapi.json
|
|
@@ -511,7 +541,21 @@ part of any automated gate.
|
|
|
511
541
|
|
|
512
542
|
## Release
|
|
513
543
|
|
|
514
|
-
|
|
544
|
+
The backend release workflow synchronizes API, CLI, and app release versions.
|
|
545
|
+
It regenerates commands, tests, builds, and packs the CLI before deployment.
|
|
546
|
+
After all deployments succeed, it publishes that archive to npmjs.org: `main`
|
|
547
|
+
updates `latest`, and `dev` updates `dev`. Manual backend deployments do not
|
|
548
|
+
publish. App workflows increment only the Flutter build number.
|
|
549
|
+
|
|
550
|
+
Configure npm trusted publishing for `fulldiveVR/wizeup-rooms-api`, workflow
|
|
551
|
+
`deploy-wize-rooms-api.yml`, with permission to publish and manage dist-tags for
|
|
552
|
+
`@fulldivevr/workcord-cli`. The API repository's `TOKEN_FOR_SUB` secret must also
|
|
553
|
+
have contents read/write access to `fulldiveVR/wizeup-rooms` so that release CI
|
|
554
|
+
can synchronize `pubspec.yaml`. Merge the app workflow changes before enabling
|
|
555
|
+
the backend pipeline. An existing npm version must match the verified archive;
|
|
556
|
+
release CI fails on a conflicting archive or a version that would lower a tag.
|
|
557
|
+
|
|
558
|
+
For a manual recovery publish:
|
|
515
559
|
|
|
516
560
|
1. `npm run regen` — refresh the spec and generated commands; commit the diff.
|
|
517
561
|
2. `npm run typecheck`
|
|
@@ -520,7 +564,8 @@ Publishing is a manual maintainer action — there is no auto-publish CI.
|
|
|
520
564
|
`README.md`, `LICENSE`, `package.json`. No sources, no `openapi.json`, no
|
|
521
565
|
`.npmrc`.
|
|
522
566
|
5. Smoke: `node dist/cli.js --help` and `node dist/cli.js --version`.
|
|
523
|
-
6.
|
|
567
|
+
6. Use the backend release version, then publish with explicit registry overrides:
|
|
568
|
+
`npm publish --access public --registry=https://registry.npmjs.org --@fulldivevr:registry=https://registry.npmjs.org`.
|
|
524
569
|
|
|
525
570
|
`prepack` re-runs `typecheck` + `test` + `build`, so steps 2–3 also gate the publish.
|
|
526
571
|
|
package/dist/auth.js
CHANGED
|
@@ -28,7 +28,14 @@ export function resolveAuth(asUserId, env = process.env, config = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
throw new Error("No credentials. Run `workcord auth login --email <address> --save`, or set WORKCORD_TOKEN. For privileged impersonation, set WORKCORD_JWT_SECRET and pass --as <userId>.");
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
function envFlag(value) {
|
|
32
|
+
const v = value?.trim().toLowerCase();
|
|
33
|
+
return v === "1" || v === "true" || v === "yes";
|
|
34
|
+
}
|
|
35
|
+
export function isManagedHubMode(env = process.env) {
|
|
36
|
+
return envFlag(env.WORKCORD_HERMES_MANAGED) || envFlag(env.WORKCORD_HUB_MANAGED);
|
|
37
|
+
}
|
|
38
|
+
export async function revealHermesKey(agentRef, asUserId) {
|
|
32
39
|
const { call } = await import("./client.js");
|
|
33
40
|
const res = (await call({
|
|
34
41
|
method: "POST",
|
|
@@ -36,25 +43,34 @@ async function revealHermesKey(agentRef, asUserId) {
|
|
|
36
43
|
asUserId,
|
|
37
44
|
}));
|
|
38
45
|
if (!res?.key) {
|
|
39
|
-
throw new Error(`Reveal for ${agentRef} returned no
|
|
46
|
+
throw new Error(`Reveal for ${agentRef} returned no hub tenant key`);
|
|
40
47
|
}
|
|
41
48
|
if (!res.hubBaseUrl) {
|
|
42
|
-
throw new Error(`Reveal for ${agentRef} returned no hubBaseUrl. Set
|
|
49
|
+
throw new Error(`Reveal for ${agentRef} returned no hubBaseUrl. Set WORKCORD_HUB_URL (with WORKCORD_HUB_KEY) to target the hub explicitly.`);
|
|
43
50
|
}
|
|
44
51
|
return { key: res.key, tenantId: res.tenantId ?? "", hubBaseUrl: res.hubBaseUrl };
|
|
45
52
|
}
|
|
46
53
|
export async function resolveHermesKey(agentRef, opts = {}) {
|
|
47
54
|
const env = opts.env ?? process.env;
|
|
48
|
-
const
|
|
49
|
-
|
|
55
|
+
const managed = opts.managed ?? isManagedHubMode(env);
|
|
56
|
+
if (managed) {
|
|
57
|
+
if (opts.asUserId) {
|
|
58
|
+
throw new Error("--as is not allowed in managed hub mode (WORKCORD_HUB_MANAGED): the hub key is always revealed for the token's own user.");
|
|
59
|
+
}
|
|
60
|
+
const revealed = await (opts.reveal ?? revealHermesKey)(agentRef);
|
|
61
|
+
return { ...revealed, source: "reveal" };
|
|
62
|
+
}
|
|
63
|
+
const envKey = env.WORKCORD_HERMES_KEY?.trim() || env.WORKCORD_HUB_KEY?.trim() || undefined;
|
|
64
|
+
const envHub = env.WORKCORD_HERMES_HUB_URL?.trim() || env.WORKCORD_HUB_URL?.trim() || undefined;
|
|
65
|
+
const envTenant = env.WORKCORD_HERMES_TENANT_ID?.trim() || env.WORKCORD_HUB_TENANT_ID?.trim() || "";
|
|
50
66
|
if (envKey) {
|
|
51
67
|
if (!envHub) {
|
|
52
|
-
throw new Error("WORKCORD_HERMES_KEY is set but WORKCORD_HERMES_HUB_URL is not. Set both, or unset the key to reveal it from WorkCord.");
|
|
68
|
+
throw new Error("WORKCORD_HUB_KEY (WORKCORD_HERMES_KEY) is set but WORKCORD_HUB_URL (WORKCORD_HERMES_HUB_URL) is not. Set both, or unset the key to reveal it from WorkCord.");
|
|
53
69
|
}
|
|
54
|
-
return { key: envKey, tenantId:
|
|
70
|
+
return { key: envKey, tenantId: envTenant, hubBaseUrl: envHub, source: "env" };
|
|
55
71
|
}
|
|
56
72
|
if (envHub && !envKey) {
|
|
57
|
-
throw new Error("WORKCORD_HERMES_HUB_URL is set but WORKCORD_HERMES_KEY is not. Set both, or unset the hub URL to reveal both from WorkCord.");
|
|
73
|
+
throw new Error("WORKCORD_HUB_URL (WORKCORD_HERMES_HUB_URL) is set but WORKCORD_HUB_KEY (WORKCORD_HERMES_KEY) is not. Set both, or unset the hub URL to reveal both from WorkCord.");
|
|
58
74
|
}
|
|
59
75
|
const useCache = opts.cache !== false;
|
|
60
76
|
const config = opts.config ?? (await readConfig());
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerRoomAgents } from "./commands/room-agents.js";
|
|
|
12
12
|
import { registerCustomAgents } from "./commands/custom-agents.js";
|
|
13
13
|
import { registerChallenges } from "./commands/challenges.js";
|
|
14
14
|
import { registerHealth } from "./commands/health.js";
|
|
15
|
-
import {
|
|
15
|
+
import { registerHub } from "./commands/hub.js";
|
|
16
16
|
import { registerAgent } from "./commands/agent.js";
|
|
17
17
|
import { registerAgentSkills } from "./commands/agent-skills.js";
|
|
18
18
|
import { registerReflections } from "./commands/reflections.js";
|
|
@@ -22,7 +22,7 @@ const require = createRequire(import.meta.url);
|
|
|
22
22
|
const { version } = require("../package.json");
|
|
23
23
|
program
|
|
24
24
|
.name("workcord")
|
|
25
|
-
.description("Talk to WorkCord agents through
|
|
25
|
+
.description("Talk to WorkCord agents through Autoworker Hub tenant chat, with advanced API diagnostics.")
|
|
26
26
|
.version(version)
|
|
27
27
|
.option("--as <userId>", "user to act as")
|
|
28
28
|
.option("--base-url <url>", "API base URL (overrides WORKCORD_API_URL and saved configuration)");
|
|
@@ -42,7 +42,7 @@ registerAgentSkills(program);
|
|
|
42
42
|
registerReflections(program);
|
|
43
43
|
registerChallenges(program); // adds regenerate-cards (not in the OpenAPI spec)
|
|
44
44
|
registerHealth(program);
|
|
45
|
-
|
|
45
|
+
registerHub(program);
|
|
46
46
|
registerGenerated(program);
|
|
47
47
|
program.parseAsync().catch((err) => {
|
|
48
48
|
const out = { error: err.message ?? String(err) };
|
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { Help, Option } from "commander";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { Writable } from "node:stream";
|
|
5
|
+
import { actingUserId, isManagedHubMode, resolveHermesKey } from "../auth.js";
|
|
6
|
+
import { hermesBinary, hermesCall, hermesStream, hermesUpload } from "../hermes-client.js";
|
|
7
|
+
import { assertArchiveSize, flavorSetWarnings, guardOwnership, rolloutBody, } from "../hermes-guards.js";
|
|
8
|
+
import { getOrCreateGroup } from "./_group.js";
|
|
9
|
+
export function agentRef(opts) {
|
|
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>.");
|
|
13
|
+
}
|
|
14
|
+
if (opts.roomAgent)
|
|
15
|
+
return `workcord-room-agent:${opts.roomAgent}`;
|
|
16
|
+
if (opts.marketplaceInstall)
|
|
17
|
+
return `marketplace-install:${opts.marketplaceInstall}`;
|
|
18
|
+
return opts.agent;
|
|
19
|
+
}
|
|
20
|
+
function agentFlags(cmd) {
|
|
21
|
+
return cmd
|
|
22
|
+
.option("--agent <id>", "custom agent id")
|
|
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")
|
|
25
|
+
.option("--refresh", "re-reveal the tenant key instead of using the cached one")
|
|
26
|
+
.option("--no-cache", "do not read or write the local tenant-key cache")
|
|
27
|
+
.option("--managed", "managed mode: always reveal, no cache, no --as (also WORKCORD_HUB_MANAGED=1)");
|
|
28
|
+
}
|
|
29
|
+
function jsonOption(cmd, name, description) {
|
|
30
|
+
return cmd.option(`--${name} <json>`, description);
|
|
31
|
+
}
|
|
32
|
+
function parseJson(raw, flag) {
|
|
33
|
+
if (raw === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`${flag} must be valid JSON`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function out(data) {
|
|
43
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
44
|
+
}
|
|
45
|
+
export function hermesChatTextBody(text) {
|
|
46
|
+
return { parts: [{ type: "text", text }] };
|
|
47
|
+
}
|
|
48
|
+
async function credentials(program, opts, deps) {
|
|
49
|
+
return resolveHermesKey(agentRef(opts), {
|
|
50
|
+
refresh: opts.refresh,
|
|
51
|
+
cache: opts.cache,
|
|
52
|
+
managed: opts.managed || undefined,
|
|
53
|
+
asUserId: actingUserId(program),
|
|
54
|
+
...deps,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function tenantPath(creds, suffix) {
|
|
58
|
+
if (!creds.tenantId) {
|
|
59
|
+
throw new Error("No tenantId available for this agent. Re-run with --refresh, or set WORKCORD_HUB_TENANT_ID alongside WORKCORD_HUB_KEY.");
|
|
60
|
+
}
|
|
61
|
+
return `/v1/tenants/${encodeURIComponent(creds.tenantId)}${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
function seg(value) {
|
|
64
|
+
return encodeURIComponent(value);
|
|
65
|
+
}
|
|
66
|
+
function segments(value) {
|
|
67
|
+
return value
|
|
68
|
+
.split("/")
|
|
69
|
+
.filter((part) => part.length > 0)
|
|
70
|
+
.map(seg)
|
|
71
|
+
.join("/");
|
|
72
|
+
}
|
|
73
|
+
function collect(value, previous = []) {
|
|
74
|
+
return [...previous, value];
|
|
75
|
+
}
|
|
76
|
+
async function currentFlavor(creds) {
|
|
77
|
+
const res = (await hermesCall({ ...creds, path: "/v1/tenant-api/flavors/current" }));
|
|
78
|
+
return res?.flavor ?? null;
|
|
79
|
+
}
|
|
80
|
+
async function resolveFlavorId(creds, explicit) {
|
|
81
|
+
if (explicit)
|
|
82
|
+
return explicit;
|
|
83
|
+
const current = await currentFlavor(creds);
|
|
84
|
+
if (!current?.id) {
|
|
85
|
+
throw new Error("This tenant has no current flavor. Pass --flavor <flavorId>.");
|
|
86
|
+
}
|
|
87
|
+
return current.id;
|
|
88
|
+
}
|
|
89
|
+
async function downloadToFile(creds, path, outPath, query) {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
const sink = new Writable({
|
|
92
|
+
write(chunk, _encoding, done) {
|
|
93
|
+
chunks.push(Buffer.from(chunk));
|
|
94
|
+
done();
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
await hermesBinary({ ...creds, path, query, out: sink });
|
|
98
|
+
const bytes = Buffer.concat(chunks);
|
|
99
|
+
await writeFile(outPath, bytes);
|
|
100
|
+
return bytes.byteLength;
|
|
101
|
+
}
|
|
102
|
+
async function readArchive(archivePath) {
|
|
103
|
+
const info = await stat(archivePath);
|
|
104
|
+
assertArchiveSize(info.size, archivePath);
|
|
105
|
+
const buffer = await readFile(archivePath);
|
|
106
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
107
|
+
}
|
|
108
|
+
export function registerHub(program, deps = {}) {
|
|
109
|
+
// `hermes` is a hidden alias of `hub`, kept for one release. The help formatter
|
|
110
|
+
// below replaces only the exact `name|alias` token so the alias stays out of help output.
|
|
111
|
+
const help = new Help();
|
|
112
|
+
const withoutAlias = (cmd, term) => {
|
|
113
|
+
const alias = cmd.aliases()[0];
|
|
114
|
+
return alias ? term.replace(`${cmd.name()}|${alias}`, cmd.name()) : term;
|
|
115
|
+
};
|
|
116
|
+
program.configureHelp({
|
|
117
|
+
subcommandTerm: (cmd) => withoutAlias(cmd, help.subcommandTerm(cmd)),
|
|
118
|
+
commandUsage: (cmd) => withoutAlias(cmd, help.commandUsage(cmd)),
|
|
119
|
+
});
|
|
120
|
+
const group = getOrCreateGroup(program, "hub", "Advanced Autoworker Hub diagnostics and recovery for an agent tenant").alias("hermes");
|
|
121
|
+
agentFlags(group.command("key").description("Reveal (and cache) the Autoworker Hub tenant key for an agent. In managed mode the key is not printed.")).action(async (opts) => {
|
|
122
|
+
const creds = await credentials(program, opts, deps);
|
|
123
|
+
if (opts.managed || isManagedHubMode()) {
|
|
124
|
+
out({ tenantId: creds.tenantId, hubBaseUrl: creds.hubBaseUrl, managed: true });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
out({ key: creds.key, tenantId: creds.tenantId, hubBaseUrl: creds.hubBaseUrl });
|
|
128
|
+
});
|
|
129
|
+
agentFlags(group.command("whoami").description("Show the Autoworker Hub tenant this agent's key belongs to.")).action(async (opts) => {
|
|
130
|
+
const creds = await credentials(program, opts, deps);
|
|
131
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant" }));
|
|
132
|
+
});
|
|
133
|
+
agentFlags(group.command("runtime").description("Show the tenant runtime.")).action(async (opts) => {
|
|
134
|
+
const creds = await credentials(program, opts, deps);
|
|
135
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/runtime" }));
|
|
136
|
+
});
|
|
137
|
+
const agent = group.command("agent").description("Tenant agent lifecycle");
|
|
138
|
+
agentFlags(agent.command("status").description("Show the tenant agent.")).action(async (opts) => {
|
|
139
|
+
const creds = await credentials(program, opts, deps);
|
|
140
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/agent" }));
|
|
141
|
+
});
|
|
142
|
+
for (const verb of ["start", "stop", "restart"]) {
|
|
143
|
+
agentFlags(agent.command(verb).description(`${verb[0].toUpperCase()}${verb.slice(1)} the tenant agent.`)).action(async (opts) => {
|
|
144
|
+
const creds = await credentials(program, opts, deps);
|
|
145
|
+
out(await hermesCall({ ...creds, method: "POST", path: `/v1/tenant-api/tenant/agent/${verb}` }));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
agentFlags(agent.command("card").description("Show the tenant agent card.")).action(async (opts) => {
|
|
149
|
+
const creds = await credentials(program, opts, deps);
|
|
150
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/agent-card" }));
|
|
151
|
+
});
|
|
152
|
+
const chat = group.command("chat").description("Tenant chats and chat messages");
|
|
153
|
+
jsonOption(agentFlags(chat.command("list").description("List chats.")), "query", "query parameters as JSON").action(async (opts) => {
|
|
154
|
+
const creds = await credentials(program, opts, deps);
|
|
155
|
+
out(await hermesCall({ ...creds, path: tenantPath(creds, "/chats"), query: parseJson(opts.query, "--query") }));
|
|
156
|
+
});
|
|
157
|
+
jsonOption(agentFlags(chat.command("create").description("Create a chat.")), "body", "request body as JSON").action(async (opts) => {
|
|
158
|
+
const creds = await credentials(program, opts, deps);
|
|
159
|
+
out(await hermesCall({ ...creds, method: "POST", path: tenantPath(creds, "/chats"), body: parseJson(opts.body, "--body") ?? {} }));
|
|
160
|
+
});
|
|
161
|
+
agentFlags(chat.command("get").description("Get one chat."))
|
|
162
|
+
.requiredOption("--chat <chatId>", "chat id")
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
const creds = await credentials(program, opts, deps);
|
|
165
|
+
out(await hermesCall({ ...creds, path: tenantPath(creds, `/chats/${encodeURIComponent(opts.chat)}`) }));
|
|
166
|
+
});
|
|
167
|
+
jsonOption(agentFlags(chat.command("messages").description("List messages in a chat.")), "query", "query parameters as JSON")
|
|
168
|
+
.requiredOption("--chat <chatId>", "chat id")
|
|
169
|
+
.action(async (opts) => {
|
|
170
|
+
const creds = await credentials(program, opts, deps);
|
|
171
|
+
out(await hermesCall({
|
|
172
|
+
...creds,
|
|
173
|
+
path: tenantPath(creds, `/chats/${encodeURIComponent(opts.chat)}/messages`),
|
|
174
|
+
query: parseJson(opts.query, "--query"),
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
jsonOption(agentFlags(chat.command("send").description("Send a message to a chat.")), "body", "request body as JSON (overrides --text)")
|
|
178
|
+
.requiredOption("--chat <chatId>", "chat id")
|
|
179
|
+
.option("--text <text>", "message text")
|
|
180
|
+
.option("--idempotency-key <key>", "Idempotency-Key header (generated when omitted)")
|
|
181
|
+
.action(async (opts) => {
|
|
182
|
+
const creds = await credentials(program, opts, deps);
|
|
183
|
+
const body = parseJson(opts.body, "--body") ?? (opts.text !== undefined ? hermesChatTextBody(opts.text) : undefined);
|
|
184
|
+
if (body === undefined) {
|
|
185
|
+
throw new Error("One of --text or --body is required.");
|
|
186
|
+
}
|
|
187
|
+
out(await hermesCall({
|
|
188
|
+
...creds,
|
|
189
|
+
method: "POST",
|
|
190
|
+
path: tenantPath(creds, `/chats/${encodeURIComponent(opts.chat)}/messages`),
|
|
191
|
+
headers: { "idempotency-key": opts.idempotencyKey ?? randomUUID() },
|
|
192
|
+
body,
|
|
193
|
+
}));
|
|
194
|
+
});
|
|
195
|
+
agentFlags(chat.command("message").description("Get one chat message."))
|
|
196
|
+
.requiredOption("--message <messageId>", "chat message id")
|
|
197
|
+
.option("--after-revision <n>", "only return when the revision is newer than this")
|
|
198
|
+
.option("--wait-seconds <n>", "long-poll for up to this many seconds")
|
|
199
|
+
.action(async (opts) => {
|
|
200
|
+
const creds = await credentials(program, opts, deps);
|
|
201
|
+
out(await hermesCall({
|
|
202
|
+
...creds,
|
|
203
|
+
path: tenantPath(creds, `/chat-messages/${encodeURIComponent(opts.message)}`),
|
|
204
|
+
query: { afterRevision: opts.afterRevision, waitSeconds: opts.waitSeconds },
|
|
205
|
+
}));
|
|
206
|
+
});
|
|
207
|
+
agentFlags(chat.command("stream").description("Stream a chat message's events (SSE)."))
|
|
208
|
+
.requiredOption("--message <messageId>", "chat message id")
|
|
209
|
+
.option("--last-event-id <id>", "resume from this event id")
|
|
210
|
+
.action(async (opts) => {
|
|
211
|
+
const creds = await credentials(program, opts, deps);
|
|
212
|
+
await hermesStream({
|
|
213
|
+
...creds,
|
|
214
|
+
path: tenantPath(creds, `/chat-messages/${encodeURIComponent(opts.message)}/events`),
|
|
215
|
+
lastEventId: opts.lastEventId,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
agentFlags(chat.command("stop").description("Stop a chat message."))
|
|
219
|
+
.requiredOption("--message <messageId>", "chat message id")
|
|
220
|
+
.action(async (opts) => {
|
|
221
|
+
const creds = await credentials(program, opts, deps);
|
|
222
|
+
out(await hermesCall({ ...creds, method: "POST", path: tenantPath(creds, `/chat-messages/${encodeURIComponent(opts.message)}/stop`) }));
|
|
223
|
+
});
|
|
224
|
+
jsonOption(agentFlags(chat.command("approve").description("Answer a chat message's approval request.")), "body", "request body as JSON")
|
|
225
|
+
.requiredOption("--message <messageId>", "chat message id")
|
|
226
|
+
.action(async (opts) => {
|
|
227
|
+
const creds = await credentials(program, opts, deps);
|
|
228
|
+
out(await hermesCall({
|
|
229
|
+
...creds,
|
|
230
|
+
method: "POST",
|
|
231
|
+
path: tenantPath(creds, `/chat-messages/${encodeURIComponent(opts.message)}/approval`),
|
|
232
|
+
body: parseJson(opts.body, "--body") ?? {},
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
const run = group.command("run").description("Tenant runs");
|
|
236
|
+
jsonOption(agentFlags(run.command("create").description("Create a run.")), "body", "request body as JSON").action(async (opts) => {
|
|
237
|
+
const creds = await credentials(program, opts, deps);
|
|
238
|
+
out(await hermesCall({ ...creds, method: "POST", path: tenantPath(creds, "/runs"), body: parseJson(opts.body, "--body") ?? {} }));
|
|
239
|
+
});
|
|
240
|
+
agentFlags(run.command("get").description("Get one run."))
|
|
241
|
+
.requiredOption("--run <runId>", "run id")
|
|
242
|
+
.action(async (opts) => {
|
|
243
|
+
const creds = await credentials(program, opts, deps);
|
|
244
|
+
out(await hermesCall({ ...creds, path: tenantPath(creds, `/runs/${encodeURIComponent(opts.run)}`) }));
|
|
245
|
+
});
|
|
246
|
+
agentFlags(run.command("stream").description("Stream a run's events (SSE)."))
|
|
247
|
+
.requiredOption("--run <runId>", "run id")
|
|
248
|
+
.option("--last-event-id <id>", "resume from this event id")
|
|
249
|
+
.action(async (opts) => {
|
|
250
|
+
const creds = await credentials(program, opts, deps);
|
|
251
|
+
await hermesStream({ ...creds, path: tenantPath(creds, `/runs/${encodeURIComponent(opts.run)}/events`), lastEventId: opts.lastEventId });
|
|
252
|
+
});
|
|
253
|
+
agentFlags(run.command("stop").description("Stop a run."))
|
|
254
|
+
.requiredOption("--run <runId>", "run id")
|
|
255
|
+
.action(async (opts) => {
|
|
256
|
+
const creds = await credentials(program, opts, deps);
|
|
257
|
+
out(await hermesCall({ ...creds, method: "POST", path: tenantPath(creds, `/runs/${encodeURIComponent(opts.run)}/stop`) }));
|
|
258
|
+
});
|
|
259
|
+
jsonOption(agentFlags(run.command("approve").description("Answer a run's approval request.")), "body", "request body as JSON")
|
|
260
|
+
.requiredOption("--run <runId>", "run id")
|
|
261
|
+
.action(async (opts) => {
|
|
262
|
+
const creds = await credentials(program, opts, deps);
|
|
263
|
+
out(await hermesCall({
|
|
264
|
+
...creds,
|
|
265
|
+
method: "POST",
|
|
266
|
+
path: tenantPath(creds, `/runs/${encodeURIComponent(opts.run)}/approval`),
|
|
267
|
+
body: parseJson(opts.body, "--body") ?? {},
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
const files = group.command("files").description("Tenant-visible files");
|
|
271
|
+
agentFlags(files.command("ls").description("List files under the tenant's virtual roots."))
|
|
272
|
+
.option("--path <path>", "directory path (empty means root)")
|
|
273
|
+
.action(async (opts) => {
|
|
274
|
+
const creds = await credentials(program, opts, deps);
|
|
275
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/files", query: { path: opts.path } }));
|
|
276
|
+
});
|
|
277
|
+
agentFlags(files.command("cat").description("Read one tenant-visible file."))
|
|
278
|
+
.requiredOption("--path <path>", "file path")
|
|
279
|
+
.action(async (opts) => {
|
|
280
|
+
const creds = await credentials(program, opts, deps);
|
|
281
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/files/content", query: { path: opts.path } }));
|
|
282
|
+
});
|
|
283
|
+
agentFlags(files.command("download").description("Download one tenant-visible file (raw bytes)."))
|
|
284
|
+
.requiredOption("--path <path>", "file path")
|
|
285
|
+
.option("--out <file>", "write to this file instead of stdout")
|
|
286
|
+
.action(async (opts) => {
|
|
287
|
+
const creds = await credentials(program, opts, deps);
|
|
288
|
+
const path = "/v1/tenant-api/tenant/files/download";
|
|
289
|
+
const query = { path: opts.path };
|
|
290
|
+
if (opts.out) {
|
|
291
|
+
out({ path: opts.out, bytes: await downloadToFile(creds, path, opts.out, query) });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await hermesBinary({ ...creds, path, query });
|
|
295
|
+
});
|
|
296
|
+
const share = group.command("share").description("Share-group filesystem");
|
|
297
|
+
agentFlags(share.command("groups").description("List share groups visible to this tenant.")).action(async (opts) => {
|
|
298
|
+
const creds = await credentials(program, opts, deps);
|
|
299
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/shares" }));
|
|
300
|
+
});
|
|
301
|
+
const shareGroupFlags = (cmd) => agentFlags(cmd).requiredOption("--group <groupId>", "share group id");
|
|
302
|
+
const sharePath = (groupId, action) => `/v1/tenant-api/tenant/shares/${seg(groupId)}/${action}`;
|
|
303
|
+
shareGroupFlags(share.command("ls").description("List one share-group directory."))
|
|
304
|
+
.option("--path <path>", "path relative to the share root")
|
|
305
|
+
.action(async (opts) => {
|
|
306
|
+
const creds = await credentials(program, opts, deps);
|
|
307
|
+
out(await hermesCall({ ...creds, path: sharePath(opts.group, "list"), query: { path: opts.path } }));
|
|
308
|
+
});
|
|
309
|
+
shareGroupFlags(share.command("read").description("Read one file from a share group."))
|
|
310
|
+
.requiredOption("--path <path>", "path relative to the share root")
|
|
311
|
+
.option("--offset <n>", "leading lines to skip")
|
|
312
|
+
.option("--limit <n>", "maximum lines to return")
|
|
313
|
+
.action(async (opts) => {
|
|
314
|
+
const creds = await credentials(program, opts, deps);
|
|
315
|
+
out(await hermesCall({
|
|
316
|
+
...creds,
|
|
317
|
+
path: sharePath(opts.group, "read"),
|
|
318
|
+
query: { path: opts.path, offset: opts.offset, limit: opts.limit },
|
|
319
|
+
}));
|
|
320
|
+
});
|
|
321
|
+
shareGroupFlags(share.command("stat").description("Stat one share-group path."))
|
|
322
|
+
.requiredOption("--path <path>", "path relative to the share root")
|
|
323
|
+
.action(async (opts) => {
|
|
324
|
+
const creds = await credentials(program, opts, deps);
|
|
325
|
+
out(await hermesCall({ ...creds, path: sharePath(opts.group, "stat"), query: { path: opts.path } }));
|
|
326
|
+
});
|
|
327
|
+
shareGroupFlags(share.command("grep").description("Search share-group file contents."))
|
|
328
|
+
.requiredOption("--pattern <regexp>", "Go regular expression")
|
|
329
|
+
.option("--path <path>", "path relative to the share root")
|
|
330
|
+
.option("--ignore-case", "case-insensitive match")
|
|
331
|
+
.option("--max <n>", "maximum results")
|
|
332
|
+
.action(async (opts) => {
|
|
333
|
+
const creds = await credentials(program, opts, deps);
|
|
334
|
+
out(await hermesCall({
|
|
335
|
+
...creds,
|
|
336
|
+
path: sharePath(opts.group, "grep"),
|
|
337
|
+
query: { pattern: opts.pattern, path: opts.path, ignore_case: opts.ignoreCase, max: opts.max },
|
|
338
|
+
}));
|
|
339
|
+
});
|
|
340
|
+
shareGroupFlags(share.command("find").description("Find share-group paths by base name."))
|
|
341
|
+
.option("--name <glob>", "glob matched against each entry's base name")
|
|
342
|
+
.option("--path <path>", "path relative to the share root")
|
|
343
|
+
.option("--max <n>", "maximum results")
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
const creds = await credentials(program, opts, deps);
|
|
346
|
+
out(await hermesCall({
|
|
347
|
+
...creds,
|
|
348
|
+
path: sharePath(opts.group, "find"),
|
|
349
|
+
query: { name: opts.name, path: opts.path, max: opts.max },
|
|
350
|
+
}));
|
|
351
|
+
});
|
|
352
|
+
shareGroupFlags(share.command("write").description("Write one file into a share group."))
|
|
353
|
+
.requiredOption("--path <path>", "path relative to the share root")
|
|
354
|
+
.option("--content <text>", "file contents", "")
|
|
355
|
+
.addOption(new Option("--mode <mode>", "write mode").choices(["overwrite", "append"]))
|
|
356
|
+
.action(async (opts) => {
|
|
357
|
+
const creds = await credentials(program, opts, deps);
|
|
358
|
+
out(await hermesCall({
|
|
359
|
+
...creds,
|
|
360
|
+
method: "POST",
|
|
361
|
+
path: sharePath(opts.group, "write"),
|
|
362
|
+
body: { path: opts.path, content: opts.content, ...(opts.mode ? { mode: opts.mode } : {}) },
|
|
363
|
+
}));
|
|
364
|
+
});
|
|
365
|
+
shareGroupFlags(share.command("mkdir").description("Create a share-group directory."))
|
|
366
|
+
.requiredOption("--path <path>", "directory path relative to the share root")
|
|
367
|
+
.action(async (opts) => {
|
|
368
|
+
const creds = await credentials(program, opts, deps);
|
|
369
|
+
out(await hermesCall({ ...creds, method: "POST", path: sharePath(opts.group, "mkdir"), body: { path: opts.path } }));
|
|
370
|
+
});
|
|
371
|
+
shareGroupFlags(share.command("move").description("Move a share-group path."))
|
|
372
|
+
.requiredOption("--from <path>", "source path relative to the share root")
|
|
373
|
+
.requiredOption("--to <path>", "destination path relative to the share root")
|
|
374
|
+
.action(async (opts) => {
|
|
375
|
+
const creds = await credentials(program, opts, deps);
|
|
376
|
+
out(await hermesCall({ ...creds, method: "POST", path: sharePath(opts.group, "move"), body: { from: opts.from, to: opts.to } }));
|
|
377
|
+
});
|
|
378
|
+
shareGroupFlags(share.command("rm").description("Remove a share-group path."))
|
|
379
|
+
.requiredOption("--path <path>", "path relative to the share root")
|
|
380
|
+
.option("--recursive", "required to remove a non-empty directory")
|
|
381
|
+
.action(async (opts) => {
|
|
382
|
+
const creds = await credentials(program, opts, deps);
|
|
383
|
+
out(await hermesCall({
|
|
384
|
+
...creds,
|
|
385
|
+
method: "POST",
|
|
386
|
+
path: sharePath(opts.group, "remove"),
|
|
387
|
+
body: { path: opts.path, recursive: Boolean(opts.recursive) },
|
|
388
|
+
}));
|
|
389
|
+
});
|
|
390
|
+
const skills = group.command("skills").description("Skills installed on this tenant");
|
|
391
|
+
agentFlags(skills.command("list").description("List this tenant's skills.")).action(async (opts) => {
|
|
392
|
+
const creds = await credentials(program, opts, deps);
|
|
393
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/skills" }));
|
|
394
|
+
});
|
|
395
|
+
for (const [verb, enabled] of [["enable", true], ["disable", false]]) {
|
|
396
|
+
agentFlags(skills.command(verb).description(`${verb[0].toUpperCase()}${verb.slice(1)} one skill on this tenant.`))
|
|
397
|
+
.requiredOption("--skill <name>", "skill name")
|
|
398
|
+
.action(async (opts) => {
|
|
399
|
+
const creds = await credentials(program, opts, deps);
|
|
400
|
+
out(await hermesCall({
|
|
401
|
+
...creds,
|
|
402
|
+
method: "PUT",
|
|
403
|
+
path: `/v1/tenant-api/tenant/skills/${seg(opts.skill)}/state`,
|
|
404
|
+
body: { enabled },
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
agentFlags(skills.command("files").description("List one skill's files."))
|
|
409
|
+
.requiredOption("--category <category>", "skill category")
|
|
410
|
+
.requiredOption("--skill <skill>", "skill name")
|
|
411
|
+
.action(async (opts) => {
|
|
412
|
+
const creds = await credentials(program, opts, deps);
|
|
413
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/tenant/skills/${seg(opts.category)}/${seg(opts.skill)}/files` }));
|
|
414
|
+
});
|
|
415
|
+
agentFlags(skills.command("cat").description("Read one skill file."))
|
|
416
|
+
.requiredOption("--path <skillPath>", "slash-delimited path relative to $HERMES_HOME/skills")
|
|
417
|
+
.action(async (opts) => {
|
|
418
|
+
const creds = await credentials(program, opts, deps);
|
|
419
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/tenant/skill-files/${segments(opts.path)}` }));
|
|
420
|
+
});
|
|
421
|
+
const flavor = group.command("flavor").description("Flavor marketplace and this tenant's flavor pin");
|
|
422
|
+
agentFlags(flavor.command("current").description("Show this tenant's current flavor pin.")).action(async (opts) => {
|
|
423
|
+
const creds = await credentials(program, opts, deps);
|
|
424
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/flavors/current" }));
|
|
425
|
+
});
|
|
426
|
+
agentFlags(flavor.command("catalog").description("List the flavor catalog.")).action(async (opts) => {
|
|
427
|
+
const creds = await credentials(program, opts, deps);
|
|
428
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/flavors" }));
|
|
429
|
+
});
|
|
430
|
+
agentFlags(flavor.command("get").description("Get one catalog flavor."))
|
|
431
|
+
.requiredOption("--flavor <flavorId>", "flavor id")
|
|
432
|
+
.action(async (opts) => {
|
|
433
|
+
const creds = await credentials(program, opts, deps);
|
|
434
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/flavors/${seg(opts.flavor)}` }));
|
|
435
|
+
});
|
|
436
|
+
agentFlags(flavor.command("set").description("Re-pin this tenant to another version of its flavor."))
|
|
437
|
+
.requiredOption("--flavor-version <version>", "flavor version")
|
|
438
|
+
.option("--flavor <flavorId>", "flavor id (defaults to the current pin)")
|
|
439
|
+
.option("--force", "proceed despite a downgrade or deprecated target")
|
|
440
|
+
.action(async (opts) => {
|
|
441
|
+
const creds = await credentials(program, opts, deps);
|
|
442
|
+
const current = await currentFlavor(creds);
|
|
443
|
+
const flavorId = opts.flavor ?? current?.id;
|
|
444
|
+
if (!flavorId) {
|
|
445
|
+
throw new Error("This tenant has no current flavor. Pass --flavor <flavorId>.");
|
|
446
|
+
}
|
|
447
|
+
const entry = (await hermesCall({ ...creds, path: `/v1/tenant-api/flavors/${seg(flavorId)}` }));
|
|
448
|
+
const warnings = flavorSetWarnings(flavorId === current?.id ? current?.version : undefined, opts.flavorVersion, entry?.versions ?? []);
|
|
449
|
+
if (warnings.length > 0) {
|
|
450
|
+
for (const warning of warnings) {
|
|
451
|
+
process.stderr.write(`warning: ${warning}\n`);
|
|
452
|
+
}
|
|
453
|
+
if (!opts.force) {
|
|
454
|
+
throw new Error(`Refusing to re-pin ${flavorId} to ${opts.flavorVersion}: ${warnings.join("; ")}. Re-run with --force to proceed.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
out(await hermesCall({
|
|
458
|
+
...creds,
|
|
459
|
+
method: "PUT",
|
|
460
|
+
path: "/v1/tenant-api/flavors/current",
|
|
461
|
+
body: { flavorId, flavorVersion: opts.flavorVersion },
|
|
462
|
+
}));
|
|
463
|
+
});
|
|
464
|
+
agentFlags(flavor.command("subscribe").description("Set this tenant's flavor update policy."))
|
|
465
|
+
.addOption(new Option("--policy <policy>", "update policy").choices(["pinned", "stable", "canary", "auto_patch"]).makeOptionMandatory())
|
|
466
|
+
.action(async (opts) => {
|
|
467
|
+
const creds = await credentials(program, opts, deps);
|
|
468
|
+
out(await hermesCall({
|
|
469
|
+
...creds,
|
|
470
|
+
method: "PUT",
|
|
471
|
+
path: "/v1/tenant-api/flavors/current/subscription",
|
|
472
|
+
body: { updatePolicy: opts.policy },
|
|
473
|
+
}));
|
|
474
|
+
});
|
|
475
|
+
agentFlags(flavor.command("checkout").description("Download one flavor version's source tarball."))
|
|
476
|
+
.requiredOption("--flavor-version <version>", "flavor version")
|
|
477
|
+
.requiredOption("--out <file>", "write the gzipped tarball here")
|
|
478
|
+
.option("--flavor <flavorId>", "flavor id (defaults to the current pin)")
|
|
479
|
+
.action(async (opts) => {
|
|
480
|
+
const creds = await credentials(program, opts, deps);
|
|
481
|
+
const flavorId = await resolveFlavorId(creds, opts.flavor);
|
|
482
|
+
const bytes = await downloadToFile(creds, `/v1/tenant-api/flavors/${seg(flavorId)}/versions/${seg(opts.flavorVersion)}/source`, opts.out);
|
|
483
|
+
out({ path: opts.out, bytes });
|
|
484
|
+
});
|
|
485
|
+
agentFlags(flavor.command("publish").description("Publish a new version of an owned flavor."))
|
|
486
|
+
.requiredOption("--archive <file>", "gzipped flavor package built beforehand")
|
|
487
|
+
.option("--flavor <flavorId>", "flavor id (defaults to the current pin)")
|
|
488
|
+
.action(async (opts) => {
|
|
489
|
+
const creds = await credentials(program, opts, deps);
|
|
490
|
+
const flavorId = await resolveFlavorId(creds, opts.flavor);
|
|
491
|
+
const data = await readArchive(opts.archive);
|
|
492
|
+
out(await guardOwnership("flavor", flavorId, creds.tenantId, () => hermesUpload({
|
|
493
|
+
...creds,
|
|
494
|
+
path: `/v1/tenant-api/flavors/${seg(flavorId)}/versions`,
|
|
495
|
+
contentType: "application/gzip",
|
|
496
|
+
data,
|
|
497
|
+
})));
|
|
498
|
+
});
|
|
499
|
+
agentFlags(flavor.command("rollout").description("Roll one flavor version out to tenants pinned to that flavor."))
|
|
500
|
+
.requiredOption("--flavor-version <version>", "flavor version")
|
|
501
|
+
.option("--flavor <flavorId>", "flavor id (defaults to the current pin)")
|
|
502
|
+
.option("--all-matching", "target every tenant pinned to this flavor id, ignoring their update policy (requires --force)")
|
|
503
|
+
.option("--canary", "restrict the automatic rollout to the canary cohort")
|
|
504
|
+
.option("--tenant <tenantId>", "target one tenant (repeatable)", collect)
|
|
505
|
+
.option("--force", "accept the blast radius of --all-matching")
|
|
506
|
+
.action(async (opts) => {
|
|
507
|
+
const body = rolloutBody(opts);
|
|
508
|
+
const creds = await credentials(program, opts, deps);
|
|
509
|
+
const flavorId = await resolveFlavorId(creds, opts.flavor);
|
|
510
|
+
const result = (await guardOwnership("flavor", flavorId, creds.tenantId, () => hermesCall({
|
|
511
|
+
...creds,
|
|
512
|
+
method: "POST",
|
|
513
|
+
path: `/v1/tenant-api/flavors/${seg(flavorId)}/versions/${seg(opts.flavorVersion)}/rollout`,
|
|
514
|
+
body,
|
|
515
|
+
})));
|
|
516
|
+
process.stderr.write(`rollout affected ${String(result?.updatedCount ?? 0)} of ${String(result?.matchedCount ?? 0)} matched tenants\n`);
|
|
517
|
+
out(result);
|
|
518
|
+
});
|
|
519
|
+
const skill = group.command("skill").description("Skill marketplace (owner-gated for publish)");
|
|
520
|
+
agentFlags(skill.command("catalog").description("List the skill catalog.")).action(async (opts) => {
|
|
521
|
+
const creds = await credentials(program, opts, deps);
|
|
522
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/skills" }));
|
|
523
|
+
});
|
|
524
|
+
agentFlags(skill.command("get").description("Get one catalog skill."))
|
|
525
|
+
.requiredOption("--skill <skillId>", "skill id")
|
|
526
|
+
.action(async (opts) => {
|
|
527
|
+
const creds = await credentials(program, opts, deps);
|
|
528
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/skills/${seg(opts.skill)}` }));
|
|
529
|
+
});
|
|
530
|
+
agentFlags(skill.command("checkout").description("Download one skill version's source tarball."))
|
|
531
|
+
.requiredOption("--skill <skillId>", "skill id")
|
|
532
|
+
.requiredOption("--skill-version <version>", "skill version")
|
|
533
|
+
.requiredOption("--out <file>", "write the gzipped tarball here")
|
|
534
|
+
.action(async (opts) => {
|
|
535
|
+
const creds = await credentials(program, opts, deps);
|
|
536
|
+
const bytes = await downloadToFile(creds, `/v1/tenant-api/skills/${seg(opts.skill)}/versions/${seg(opts.skillVersion)}/source`, opts.out);
|
|
537
|
+
out({ path: opts.out, bytes });
|
|
538
|
+
});
|
|
539
|
+
agentFlags(skill.command("publish").description("Publish a new version of an owned skill."))
|
|
540
|
+
.requiredOption("--skill <skillId>", "skill id")
|
|
541
|
+
.requiredOption("--archive <file>", "gzipped skill package built beforehand")
|
|
542
|
+
.action(async (opts) => {
|
|
543
|
+
const creds = await credentials(program, opts, deps);
|
|
544
|
+
const data = await readArchive(opts.archive);
|
|
545
|
+
out(await guardOwnership("skill", opts.skill, creds.tenantId, () => hermesUpload({
|
|
546
|
+
...creds,
|
|
547
|
+
path: `/v1/tenant-api/skills/${seg(opts.skill)}/versions`,
|
|
548
|
+
contentType: "application/gzip",
|
|
549
|
+
data,
|
|
550
|
+
})));
|
|
551
|
+
});
|
|
552
|
+
const evals = group.command("eval").description("Flavor eval suites and runs");
|
|
553
|
+
agentFlags(evals.command("suites").description("List eval suites for this tenant's flavor.")).action(async (opts) => {
|
|
554
|
+
const creds = await credentials(program, opts, deps);
|
|
555
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/flavor/eval-suites" }));
|
|
556
|
+
});
|
|
557
|
+
agentFlags(evals.command("run").description("Start an eval run."))
|
|
558
|
+
.requiredOption("--suite <suiteId>", "eval suite id")
|
|
559
|
+
.action(async (opts) => {
|
|
560
|
+
const creds = await credentials(program, opts, deps);
|
|
561
|
+
out(await hermesCall({ ...creds, method: "POST", path: "/v1/tenant-api/tenant/flavor/eval-runs", body: { suiteId: opts.suite } }));
|
|
562
|
+
});
|
|
563
|
+
agentFlags(evals.command("list").description("List this tenant's eval runs."))
|
|
564
|
+
.option("--suite <suiteId>", "filter by suite id")
|
|
565
|
+
.option("--status <status>", "filter by status")
|
|
566
|
+
.option("--limit <n>", "maximum runs to return")
|
|
567
|
+
.action(async (opts) => {
|
|
568
|
+
const creds = await credentials(program, opts, deps);
|
|
569
|
+
out(await hermesCall({
|
|
570
|
+
...creds,
|
|
571
|
+
path: "/v1/tenant-api/tenant/eval-runs",
|
|
572
|
+
query: { suite_id: opts.suite, status: opts.status, limit: opts.limit },
|
|
573
|
+
}));
|
|
574
|
+
});
|
|
575
|
+
agentFlags(evals.command("get").description("Get one eval run."))
|
|
576
|
+
.requiredOption("--run <evalRunId>", "eval run id")
|
|
577
|
+
.action(async (opts) => {
|
|
578
|
+
const creds = await credentials(program, opts, deps);
|
|
579
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/tenant/eval-runs/${seg(opts.run)}` }));
|
|
580
|
+
});
|
|
581
|
+
agentFlags(evals.command("cancel").description("Cancel one eval run."))
|
|
582
|
+
.requiredOption("--run <evalRunId>", "eval run id")
|
|
583
|
+
.action(async (opts) => {
|
|
584
|
+
const creds = await credentials(program, opts, deps);
|
|
585
|
+
out(await hermesCall({ ...creds, method: "POST", path: `/v1/tenant-api/tenant/eval-runs/${seg(opts.run)}:cancel` }));
|
|
586
|
+
});
|
|
587
|
+
const peers = group.command("peers").description("A2A peers");
|
|
588
|
+
agentFlags(peers.command("list").description("List this tenant's A2A peers.")).action(async (opts) => {
|
|
589
|
+
const creds = await credentials(program, opts, deps);
|
|
590
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/peers" }));
|
|
591
|
+
});
|
|
592
|
+
agentFlags(peers.command("get").description("Get one peer."))
|
|
593
|
+
.requiredOption("--peer <peerId>", "peer id")
|
|
594
|
+
.action(async (opts) => {
|
|
595
|
+
const creds = await credentials(program, opts, deps);
|
|
596
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/peers/${seg(opts.peer)}` }));
|
|
597
|
+
});
|
|
598
|
+
jsonOption(agentFlags(peers.command("call").description("Send a message to a peer.")), "body", "request body as JSON (overrides the other flags)")
|
|
599
|
+
.requiredOption("--peer <peerId>", "peer id")
|
|
600
|
+
.option("--message <text>", "message text")
|
|
601
|
+
.option("--context <contextId>", "conversation context id")
|
|
602
|
+
.option("--async", "return before the peer replies")
|
|
603
|
+
.option("--wait-seconds <n>", "seconds to wait for a reply")
|
|
604
|
+
.option("--idempotency-key <key>", "idempotency key (generated when omitted)")
|
|
605
|
+
.action(async (opts) => {
|
|
606
|
+
const creds = await credentials(program, opts, deps);
|
|
607
|
+
const body = parseJson(opts.body, "--body") ??
|
|
608
|
+
(opts.message !== undefined
|
|
609
|
+
? {
|
|
610
|
+
message: opts.message,
|
|
611
|
+
...(opts.context ? { contextId: opts.context } : {}),
|
|
612
|
+
...(opts.async ? { async: true } : {}),
|
|
613
|
+
...(opts.waitSeconds ? { waitSeconds: Number(opts.waitSeconds) } : {}),
|
|
614
|
+
idempotencyKey: opts.idempotencyKey ?? randomUUID(),
|
|
615
|
+
}
|
|
616
|
+
: undefined);
|
|
617
|
+
if (body === undefined) {
|
|
618
|
+
throw new Error("One of --message or --body is required.");
|
|
619
|
+
}
|
|
620
|
+
out(await hermesCall({ ...creds, method: "POST", path: `/v1/tenant-api/peers/${seg(opts.peer)}/messages`, body }));
|
|
621
|
+
});
|
|
622
|
+
jsonOption(agentFlags(peers.command("find").description("Find peers by query and capabilities.")), "body", "request body as JSON (overrides the other flags)")
|
|
623
|
+
.option("--query <text>", "free-text query")
|
|
624
|
+
.option("--required-capability <name>", "capability the peer must have (repeatable)", collect)
|
|
625
|
+
.option("--exclude-capability <name>", "capability the peer must not have (repeatable)", collect)
|
|
626
|
+
.option("--limit <n>", "maximum peers to return")
|
|
627
|
+
.action(async (opts) => {
|
|
628
|
+
const creds = await credentials(program, opts, deps);
|
|
629
|
+
const body = parseJson(opts.body, "--body") ?? {
|
|
630
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
631
|
+
...(opts.requiredCapability?.length ? { requiredCapabilities: opts.requiredCapability } : {}),
|
|
632
|
+
...(opts.excludeCapability?.length ? { excludeCapabilities: opts.excludeCapability } : {}),
|
|
633
|
+
...(opts.limit ? { limit: Number(opts.limit) } : {}),
|
|
634
|
+
};
|
|
635
|
+
out(await hermesCall({ ...creds, method: "POST", path: "/v1/tenant-api/peers:find", body }));
|
|
636
|
+
});
|
|
637
|
+
const conversations = group.command("conversations").description("A2A conversations");
|
|
638
|
+
agentFlags(conversations.command("list").description("List conversations.")).action(async (opts) => {
|
|
639
|
+
const creds = await credentials(program, opts, deps);
|
|
640
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/conversations" }));
|
|
641
|
+
});
|
|
642
|
+
agentFlags(conversations.command("get").description("Get one conversation."))
|
|
643
|
+
.requiredOption("--context <contextId>", "conversation context id")
|
|
644
|
+
.action(async (opts) => {
|
|
645
|
+
const creds = await credentials(program, opts, deps);
|
|
646
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/conversations/${seg(opts.context)}` }));
|
|
647
|
+
});
|
|
648
|
+
agentFlags(conversations.command("messages").description("List one conversation's messages."))
|
|
649
|
+
.requiredOption("--context <contextId>", "conversation context id")
|
|
650
|
+
.action(async (opts) => {
|
|
651
|
+
const creds = await credentials(program, opts, deps);
|
|
652
|
+
out(await hermesCall({ ...creds, path: `/v1/tenant-api/conversations/${seg(opts.context)}/messages` }));
|
|
653
|
+
});
|
|
654
|
+
const sessions = group.command("sessions").description("Tenant agent sessions");
|
|
655
|
+
agentFlags(sessions.command("list").description("List this tenant's sessions."))
|
|
656
|
+
.option("--limit <n>", "maximum sessions to return")
|
|
657
|
+
.option("--source <source>", "filter by source")
|
|
658
|
+
.action(async (opts) => {
|
|
659
|
+
const creds = await credentials(program, opts, deps);
|
|
660
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/sessions", query: { limit: opts.limit, source: opts.source } }));
|
|
661
|
+
});
|
|
662
|
+
agentFlags(group.command("artifacts").description("List this tenant's artifacts.")).action(async (opts) => {
|
|
663
|
+
const creds = await credentials(program, opts, deps);
|
|
664
|
+
out(await hermesCall({ ...creds, path: "/v1/tenant-api/tenant/artifacts" }));
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=hub.js.map
|