agent-relay-server 0.32.4 → 0.33.1
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/package.json +2 -2
- package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
- package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
- package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
- package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
- package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
- package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
- package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
- package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
- package/public/assets/automation-Dm6rXNxK.js +2 -0
- package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
- package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
- package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
- package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
- package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
- package/public/assets/chat-JZAEDGfX.js +2 -0
- package/public/assets/chat-JZAEDGfX.js.map +1 -0
- package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
- package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
- package/public/assets/display-Bebqs1qu.js +3 -0
- package/public/assets/display-Bebqs1qu.js.map +1 -0
- package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
- package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
- package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
- package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
- package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
- package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
- package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
- package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
- package/public/assets/maintenance-CsoOFBXx.js +2 -0
- package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
- package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
- package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
- package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
- package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
- package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
- package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
- package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
- package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
- package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
- package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
- package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
- package/public/assets/pairs-unqjPlmq.js +2 -0
- package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
- package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
- package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
- package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
- package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
- package/public/assets/store-DiSzYHj9.js +9 -0
- package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
- package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
- package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
- package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
- package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
- package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
- package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +1 -1
- package/src/agent-lifecycle-events.ts +137 -0
- package/src/artifact-storage.ts +3 -5
- package/src/channel-target.ts +24 -0
- package/src/cli/_shared.ts +80 -0
- package/src/cli/agent-detect.ts +188 -0
- package/src/cli/agent-meta.ts +95 -0
- package/src/cli/context-probe.ts +88 -0
- package/src/cli/daemon.ts +111 -0
- package/src/cli/dev.ts +173 -0
- package/src/cli/index.ts +361 -0
- package/src/cli/introspect.ts +73 -0
- package/src/cli/memory.ts +37 -0
- package/src/cli/message.ts +201 -0
- package/src/cli/orchestrator.ts +227 -0
- package/src/cli/pair.ts +125 -0
- package/src/cli/provider.ts +209 -0
- package/src/cli/recipe.ts +110 -0
- package/src/cli/reply.ts +141 -0
- package/src/cli/setup.ts +57 -0
- package/src/cli/steward.ts +59 -0
- package/src/cli/token.ts +81 -0
- package/src/cli/upgrade.ts +193 -0
- package/src/cli/workspace.ts +215 -0
- package/src/cli.ts +4 -2718
- package/src/config-store.ts +10 -6
- package/src/db/activity.ts +194 -0
- package/src/db/agent-search.ts +174 -0
- package/src/db/agents.ts +551 -0
- package/src/db/artifacts.ts +342 -0
- package/src/db/channels.ts +576 -0
- package/src/db/connection.ts +71 -0
- package/src/db/delivery.ts +395 -0
- package/src/db/inbox.ts +249 -0
- package/src/db/index.ts +23 -0
- package/src/db/integrations.ts +339 -0
- package/src/db/mappers.ts +397 -0
- package/src/db/merge-lease.ts +160 -0
- package/src/db/message-reads.ts +304 -0
- package/src/db/messages.ts +434 -0
- package/src/db/migrations.ts +431 -0
- package/src/db/orchestrators.ts +358 -0
- package/src/db/pairs.ts +324 -0
- package/src/db/schema.ts +758 -0
- package/src/db/stats.ts +337 -0
- package/src/db/tasks.ts +407 -0
- package/src/db/workspaces.ts +440 -0
- package/src/db.ts +4 -5721
- package/src/maintenance.ts +4 -0
- package/src/mcp-errors.ts +7 -0
- package/src/mcp.ts +32 -34
- package/src/routes/agents-spawn.ts +9 -1
- package/src/routes/agents.ts +5 -0
- package/src/routes/commands.ts +15 -0
- package/src/routes/integrations.ts +6 -8
- package/src/spawn-targets.ts +159 -0
- package/src/utils.ts +16 -1
- package/public/assets/automation-CiaLThdO.js +0 -2
- package/public/assets/chat-5hvHZcAe.js +0 -2
- package/public/assets/chat-5hvHZcAe.js.map +0 -1
- package/public/assets/display-JI19Vc7L.js +0 -3
- package/public/assets/display-JI19Vc7L.js.map +0 -1
- package/public/assets/maintenance-DiFNzNPN.js +0 -2
- package/public/assets/pairs-WpKCPE1n.js +0 -2
- package/public/assets/store-C9VcSo05.js +0 -9
package/src/cli.ts
CHANGED
|
@@ -1,2718 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
createDaemonPlan,
|
|
8
|
-
detectDaemonEnvironment,
|
|
9
|
-
executeDaemonPlan,
|
|
10
|
-
formatDaemonPlan,
|
|
11
|
-
type DaemonAction,
|
|
12
|
-
type DaemonScope,
|
|
13
|
-
} from "./daemon";
|
|
14
|
-
import {
|
|
15
|
-
createSetupPlan,
|
|
16
|
-
executeSetupPlan,
|
|
17
|
-
formatSetupPlan,
|
|
18
|
-
pathExists,
|
|
19
|
-
renderEnvFile,
|
|
20
|
-
} from "./setup";
|
|
21
|
-
import { defaultRuntimePrefix, runtimeBinPath } from "./runtime-prefix";
|
|
22
|
-
import {
|
|
23
|
-
createDevInstallPlan,
|
|
24
|
-
createDevPackPlan,
|
|
25
|
-
createDevServicePlan,
|
|
26
|
-
defaultDevRoot,
|
|
27
|
-
executeDevInstallPlan,
|
|
28
|
-
executeDevPackPlan,
|
|
29
|
-
executeDevServicePlan,
|
|
30
|
-
executeDevSmoke,
|
|
31
|
-
formatDevInstallPlan,
|
|
32
|
-
formatDevPackPlan,
|
|
33
|
-
formatDevServicePlan,
|
|
34
|
-
parseDevPackages,
|
|
35
|
-
type DevServiceAction,
|
|
36
|
-
} from "./dev";
|
|
37
|
-
import {
|
|
38
|
-
createUpgradePlan,
|
|
39
|
-
detectUpgradeSnapshot,
|
|
40
|
-
executeUpgradePlan,
|
|
41
|
-
formatUpgradePlan,
|
|
42
|
-
resolveLocalOrchestratorId,
|
|
43
|
-
type UpgradeProvider,
|
|
44
|
-
} from "./upgrade";
|
|
45
|
-
import { formatMemoryBrokerSmokeResult, runMemoryBrokerSmoke } from "./memory-broker-smoke";
|
|
46
|
-
import { MAX_BODY_BYTES, VERSION } from "./config";
|
|
47
|
-
import { runContextProbe } from "agent-relay-sdk/context-probe";
|
|
48
|
-
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
49
|
-
import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
50
|
-
import type { WorkspaceDepsRefreshResult } from "agent-relay-sdk";
|
|
51
|
-
import { describeWorkspacePhase, readyContract, type WorkspacePhaseView } from "./workspace-phase";
|
|
52
|
-
|
|
53
|
-
export const WORKSPACE_USAGE = "Usage: agent-relay workspace <status|diagnostics|ready|land|claim|release|list|cleanup-stale|deps> [--id ID] [--strategy ...] [--purpose TEXT] [--repo PATH] [--wait] [--timeout SECONDS] [--check] [--execute] [--json]";
|
|
54
|
-
|
|
55
|
-
const HELP = `
|
|
56
|
-
agent-relay ${VERSION}
|
|
57
|
-
|
|
58
|
-
Usage:
|
|
59
|
-
agent-relay [start]
|
|
60
|
-
agent-relay setup [--yes] [--dry-run] [--force] [--env-file PATH] [--runtime-prefix DIR] [--host HOST] [--port PORT] [--db-path PATH] [--token TOKEN|--no-token]
|
|
61
|
-
agent-relay upgrade [--dry-run] [--version VERSION] [--runtime-prefix DIR] [--providers auto|all|codex|claude|orchestrator] [--no-restart] [--yes]
|
|
62
|
-
agent-relay upgrade --host ID [--host ID2 ...] [--version VERSION] [--providers ...] (upgrade remote orchestrator hosts over the relay)
|
|
63
|
-
agent-relay upgrade --all-hosts [...] (upgrade this host, then every behind remote host)
|
|
64
|
-
agent-relay setup upgrade [same options as upgrade]
|
|
65
|
-
agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]
|
|
66
|
-
agent-relay orchestrator install [options]
|
|
67
|
-
agent-relay dev <pack|install|service|smoke> [options]
|
|
68
|
-
agent-relay memory broker smoke [options]
|
|
69
|
-
agent-relay recipe <list|show|start|stop|status> [options]
|
|
70
|
-
agent-relay provider <wrap|unwrap> <claude|codex>
|
|
71
|
-
agent-relay context-probe [print-status-line] [--wrap COMMAND] [--agent-id ID] [--state-dir DIR] [--standalone]
|
|
72
|
-
agent-relay token <create|list|revoke|verify> [options]
|
|
73
|
-
agent-relay pair <target|create|status|accept|reject|hangup|send> [options]
|
|
74
|
-
agent-relay workspace <status|diagnostics|ready|land|claim|release|list|cleanup-stale|deps> [--id ID] [--strategy ...] [--purpose TEXT] [--repo PATH] [--wait] [--timeout SECONDS] [--check] [--execute] [--json]
|
|
75
|
-
agent-relay steward <queue|inspect|checks> [WORKSPACE_ID] [--repo PATH] [--json]
|
|
76
|
-
agent-relay message <target> <body> [options]
|
|
77
|
-
agent-relay get-message <messageId> [--json|--body]
|
|
78
|
-
agent-relay /pair <target|accept|reject|send|status> [...]
|
|
79
|
-
agent-relay /message <target> <body>
|
|
80
|
-
agent-relay /reply <messageId> <body|--stdin|--body-file PATH> [--format text|markdown|markdownv2]
|
|
81
|
-
agent-relay /react <messageId> <emoji> [--remove]
|
|
82
|
-
agent-relay /send-claimable <target> <body>
|
|
83
|
-
agent-relay /disconnect [PAIR_ID]
|
|
84
|
-
agent-relay /status
|
|
85
|
-
agent-relay /guide
|
|
86
|
-
agent-relay /label [LABEL]
|
|
87
|
-
agent-relay /tags [TAG ...]
|
|
88
|
-
agent-relay /introspect [--thin TEXT] [--worked-around TEXT] [--would-have-helped TEXT] [--stdin]
|
|
89
|
-
agent-relay --help
|
|
90
|
-
|
|
91
|
-
Pair examples:
|
|
92
|
-
agent-relay pair codex --objective "Debug flaky tests"
|
|
93
|
-
agent-relay /pair codex "Debug flaky tests"
|
|
94
|
-
agent-relay pair status
|
|
95
|
-
agent-relay pair accept PAIR_ID --agent AGENT_ID
|
|
96
|
-
agent-relay pair send PAIR_ID --from AGENT_ID --body "What do you see?"
|
|
97
|
-
agent-relay /message codex "Can you look at that failing action?"
|
|
98
|
-
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
99
|
-
agent-relay /react 206 👍
|
|
100
|
-
agent-relay /reply 206 --stdin < result.md
|
|
101
|
-
agent-relay get-message 206 --json
|
|
102
|
-
agent-relay /reply 206 --format markdown "Here is **formatted** text"
|
|
103
|
-
agent-relay /send-claimable tag:backend "Please claim and fix the failing API test"
|
|
104
|
-
agent-relay /disconnect
|
|
105
|
-
agent-relay /status
|
|
106
|
-
agent-relay /guide
|
|
107
|
-
agent-relay /label backend-fixer
|
|
108
|
-
agent-relay /tags backend tests urgent
|
|
109
|
-
|
|
110
|
-
Daemon options:
|
|
111
|
-
--env-file PATH Env file sourced by the daemon (default: platform user config dir)
|
|
112
|
-
--runtime-prefix DIR Isolated Agent Relay npm prefix (default: ~/.agent-relay/runtime)
|
|
113
|
-
--binary PATH Stable agent-relay binary/script path for the service
|
|
114
|
-
--path-prefix DIR Prepend a directory to daemon PATH; repeatable
|
|
115
|
-
--name NAME Service name (default: agent-relay)
|
|
116
|
-
--host HOST Display/listen host for generated plan (default: 127.0.0.1)
|
|
117
|
-
--port PORT Display/listen port for generated plan (default: 4850)
|
|
118
|
-
--user|--system User service by default, system service when explicitly requested
|
|
119
|
-
--enable Enable service at login/boot during install
|
|
120
|
-
--start Start service after install
|
|
121
|
-
--dry-run Print the plan without writing files or running service commands
|
|
122
|
-
--yes Skip confirmation prompts
|
|
123
|
-
--force Overwrite/remove managed-file guardrails
|
|
124
|
-
--json Print structured output
|
|
125
|
-
|
|
126
|
-
Orchestrator install options:
|
|
127
|
-
--relay-url URL Agent Relay server URL reachable from this host
|
|
128
|
-
--token TOKEN Scoped runtime orchestrator token
|
|
129
|
-
--bootstrap-token TOKEN Short-lived dashboard bootstrap token to exchange during install
|
|
130
|
-
--id ID Orchestrator id (default: sanitized hostname)
|
|
131
|
-
--base-dir PATH Base directory for agent working directories (default: ~/projects)
|
|
132
|
-
--providers LIST Providers to probe/enable (default: claude,codex)
|
|
133
|
-
--api-port PORT Orchestrator local API port (default: 4860)
|
|
134
|
-
--runtime-prefix DIR Isolated npm prefix (default: ~/.agent-relay/runtime)
|
|
135
|
-
--path-prefix DIR Prepend a directory to daemon PATH; repeatable
|
|
136
|
-
--version VERSION Package version to install (default: this CLI version)
|
|
137
|
-
--dry-run Print the plan without writing files, installing, or starting
|
|
138
|
-
--yes Skip confirmation prompts
|
|
139
|
-
|
|
140
|
-
Upgrade options:
|
|
141
|
-
--version VERSION Target version (default: latest published server version)
|
|
142
|
-
--runtime-prefix DIR Isolated Agent Relay npm prefix (default: ~/.agent-relay/runtime)
|
|
143
|
-
--providers LIST Provider integrations to upgrade: auto, all, codex, claude, orchestrator
|
|
144
|
-
--host ID Upgrade a remote orchestrator host over the relay (repeatable). Skips the local upgrade
|
|
145
|
-
--all-hosts Upgrade this host, then fan out to every connected remote host that is behind
|
|
146
|
-
--no-restart Do not restart agent-relay.service (warns you to restart it manually)
|
|
147
|
-
--restart-deferred Like --no-restart, but the caller restarts the services itself; suppresses the manual-restart warning (used by the release script)
|
|
148
|
-
--dry-run Print detected state and planned commands
|
|
149
|
-
--yes Skip confirmation prompts
|
|
150
|
-
|
|
151
|
-
Dev options:
|
|
152
|
-
agent-relay dev pack [--packages LIST] [--out DIR]
|
|
153
|
-
agent-relay dev install [--packages LIST] [--prefix DIR] [--out DIR] [--dry-run] [--json]
|
|
154
|
-
agent-relay dev service <install|uninstall|start|stop|restart|status|logs> [--prefix DIR] [--root DIR] [--port PORT] [--api-port PORT] [--base-dir DIR] [--enable] [--start] [--dry-run] [--yes] [--force] [--json]
|
|
155
|
-
agent-relay dev smoke [--root DIR] [--providers LIST] [--cwd DIR] [--timeout MS]
|
|
156
|
-
|
|
157
|
-
Memory options:
|
|
158
|
-
agent-relay memory broker smoke [--relay-url URL] [--token TOKEN] [--agent-id ID] [--scope SCOPE] [--tag TAG] [--no-cleanup] [--json]
|
|
159
|
-
|
|
160
|
-
Recipe examples:
|
|
161
|
-
agent-relay recipe list
|
|
162
|
-
agent-relay recipe start code-review --cwd /repo --orchestrator local
|
|
163
|
-
agent-relay recipe status
|
|
164
|
-
agent-relay recipe stop INSTANCE_ID
|
|
165
|
-
|
|
166
|
-
Token examples:
|
|
167
|
-
agent-relay token create --role orchestrator --sub macmini --ttl 86400
|
|
168
|
-
agent-relay token list
|
|
169
|
-
agent-relay token revoke JTI
|
|
170
|
-
|
|
171
|
-
Context probe examples:
|
|
172
|
-
agent-relay context-probe print-status-line --wrap 'ccstatusline'
|
|
173
|
-
agent-relay context-probe --wrap 'ccstatusline'
|
|
174
|
-
agent-relay context-probe --standalone
|
|
175
|
-
`.trim();
|
|
176
|
-
|
|
177
|
-
const AGENT_GUIDE = `
|
|
178
|
-
Agent Relay guide for coding agents
|
|
179
|
-
|
|
180
|
-
Agent Relay is a message bus between local or managed agents. Use the
|
|
181
|
-
agent-relay CLI for relay communication.
|
|
182
|
-
|
|
183
|
-
Current session
|
|
184
|
-
agent-relay /status --json
|
|
185
|
-
Shows your relay agent id, label, tags, health, and active pair state.
|
|
186
|
-
|
|
187
|
-
Replying
|
|
188
|
-
agent-relay /reply <messageId> "<response>"
|
|
189
|
-
agent-relay /reply <messageId> --stdin < response.md
|
|
190
|
-
agent-relay /reply <messageId> --body-file response.md
|
|
191
|
-
Reply to the sender of a relay message. Prefer this over /message when
|
|
192
|
-
handling an incoming relay message because the server keeps the routing
|
|
193
|
-
and channel context. Large replies are uploaded as an artifact
|
|
194
|
-
automatically and sent as a concise attached reply.
|
|
195
|
-
|
|
196
|
-
Reactions
|
|
197
|
-
agent-relay /react <messageId> <emoji>
|
|
198
|
-
agent-relay /react <messageId> <emoji> --remove
|
|
199
|
-
Use reactions for lightweight acknowledgement, approval, thanks, or
|
|
200
|
-
"good job" when no text response is needed. Do not use reactions when the
|
|
201
|
-
sender asked a question, gave a new task, or needs a result in text.
|
|
202
|
-
|
|
203
|
-
Reading full messages
|
|
204
|
-
agent-relay get-message <messageId>
|
|
205
|
-
agent-relay read-message <messageId> --json
|
|
206
|
-
agent-relay get-message <messageId> --body
|
|
207
|
-
Fetch the full message body, attachments, metadata, and thread context.
|
|
208
|
-
|
|
209
|
-
Sending messages
|
|
210
|
-
agent-relay /message <target> "<message>"
|
|
211
|
-
Send a direct one-off relay message.
|
|
212
|
-
|
|
213
|
-
agent-relay /send-claimable <target> "<work item>"
|
|
214
|
-
Queue work that one matching agent can claim and handle.
|
|
215
|
-
|
|
216
|
-
Targets
|
|
217
|
-
<agent-id> A specific live agent id.
|
|
218
|
-
<label> A registered agent label.
|
|
219
|
-
tag:<tag> Any agent with that tag.
|
|
220
|
-
cap:<capability> Any agent with that capability.
|
|
221
|
-
policy:<name> A managed policy target that can survive restarts.
|
|
222
|
-
broadcast All reachable agents.
|
|
223
|
-
|
|
224
|
-
Pair sessions
|
|
225
|
-
agent-relay /pair <target> "<objective>"
|
|
226
|
-
agent-relay /pair status
|
|
227
|
-
agent-relay /pair accept <pairId>
|
|
228
|
-
agent-relay /pair reject <pairId>
|
|
229
|
-
agent-relay /pair send <pairId> "<message>"
|
|
230
|
-
agent-relay /disconnect [pairId]
|
|
231
|
-
|
|
232
|
-
Labels and tags
|
|
233
|
-
agent-relay /label [LABEL]
|
|
234
|
-
agent-relay /tags [TAG ...]
|
|
235
|
-
|
|
236
|
-
Isolated workspaces
|
|
237
|
-
If you are working in an isolated workspace (a git worktree on an agent
|
|
238
|
-
branch, not the main checkout), you do NOT rebase, merge, or push yourself —
|
|
239
|
-
Relay does. Just commit your work in the worktree, then:
|
|
240
|
-
agent-relay workspace ready Hand off: Relay rebases onto the latest base,
|
|
241
|
-
lands your work, and pushes.
|
|
242
|
-
agent-relay workspace status Show your workspace's state + what to do next.
|
|
243
|
-
agent-relay workspace status --wait
|
|
244
|
-
Block until your branch lands (returns the
|
|
245
|
-
moment the auto-merge completes).
|
|
246
|
-
After "ready", status is "review_requested" — this is NORMAL, not an
|
|
247
|
-
escalation. Relay auto-merges clean rebases ~every 2 min; a steward agent is
|
|
248
|
-
spawned only if it can't land deterministically, so no steward = healthy. On
|
|
249
|
-
landing you move onto a fresh rebased branch (name gains a "--N" suffix).
|
|
250
|
-
The base branch will move as other agents land in parallel — that is normal,
|
|
251
|
-
let the merge handle it. Never push, merge, resolve conflicts, or touch the
|
|
252
|
-
main checkout yourself; it is local-only and Relay (and the steward) own that.
|
|
253
|
-
If typecheck/build fails on a missing module (a dep added to the base after
|
|
254
|
-
your worktree was created), do NOT run a clean install — it mutates the shared
|
|
255
|
-
node_modules. Instead refresh your worktree's deps in isolation:
|
|
256
|
-
agent-relay workspace deps Re-provision deps that have gone stale.
|
|
257
|
-
agent-relay workspace deps --check Report staleness without installing.
|
|
258
|
-
|
|
259
|
-
Rules of thumb
|
|
260
|
-
If you are handling relay message #123, reply with:
|
|
261
|
-
agent-relay /reply 123 "<response>"
|
|
262
|
-
|
|
263
|
-
If the delivered preview says it was truncated, fetch the full message with:
|
|
264
|
-
agent-relay get-message 123
|
|
265
|
-
|
|
266
|
-
If your reply is long, avoid shell quoting and use:
|
|
267
|
-
agent-relay /reply 123 --stdin < response.md
|
|
268
|
-
|
|
269
|
-
If you need to know who you are in Relay, run:
|
|
270
|
-
agent-relay /status --json
|
|
271
|
-
|
|
272
|
-
Recording session friction (optional, helps improve your standing context)
|
|
273
|
-
agent-relay /introspect --thin "..." --worked-around "..." --would-have-helped "..."
|
|
274
|
-
When a session involved real work, you can record a short 3-field note about
|
|
275
|
-
where context was thin, what tooling/instruction gaps you routed around, and
|
|
276
|
-
what would have saved the read-up. Keep each field to a sentence or two. This
|
|
277
|
-
feeds the relay's self-improvement signal so the operator can close the gaps —
|
|
278
|
-
it is not a reply and needs no message id. Skip it for trivial sessions.
|
|
279
|
-
|
|
280
|
-
Use the HTTP API for integrations and debugging. For normal agent-to-agent
|
|
281
|
-
communication, use the CLI commands above.
|
|
282
|
-
`.trim();
|
|
283
|
-
|
|
284
|
-
const DAEMON_ACTIONS = new Set<DaemonAction>([
|
|
285
|
-
"install",
|
|
286
|
-
"uninstall",
|
|
287
|
-
"start",
|
|
288
|
-
"stop",
|
|
289
|
-
"restart",
|
|
290
|
-
"enable",
|
|
291
|
-
"disable",
|
|
292
|
-
"status",
|
|
293
|
-
"logs",
|
|
294
|
-
]);
|
|
295
|
-
|
|
296
|
-
export async function handleCli(args: string[]): Promise<"start" | "handled"> {
|
|
297
|
-
const command = args[0];
|
|
298
|
-
if (!command || command === "start") return "start";
|
|
299
|
-
if (command === "--help" || command === "-h" || command === "help") {
|
|
300
|
-
console.log(HELP);
|
|
301
|
-
return "handled";
|
|
302
|
-
}
|
|
303
|
-
if (command === "--version" || command === "-v") {
|
|
304
|
-
console.log(VERSION);
|
|
305
|
-
return "handled";
|
|
306
|
-
}
|
|
307
|
-
if (command === "guide" || command === "/guide" || command === "agent-guide" || command === "/agent-guide") {
|
|
308
|
-
console.log(AGENT_GUIDE);
|
|
309
|
-
return "handled";
|
|
310
|
-
}
|
|
311
|
-
if (command === "upgrade" || (command === "setup" && args[1] === "upgrade")) {
|
|
312
|
-
await handleUpgradeCommand(command === "setup" ? args.slice(2) : args.slice(1));
|
|
313
|
-
return "handled";
|
|
314
|
-
}
|
|
315
|
-
if (command === "setup" || command === "init") {
|
|
316
|
-
await handleSetupCommand(args.slice(1));
|
|
317
|
-
return "handled";
|
|
318
|
-
}
|
|
319
|
-
if (command === "daemon") {
|
|
320
|
-
await handleDaemonCommand(args.slice(1));
|
|
321
|
-
return "handled";
|
|
322
|
-
}
|
|
323
|
-
if (command === "orchestrator" || command === "orchestrators") {
|
|
324
|
-
await handleOrchestratorCommand(args.slice(1));
|
|
325
|
-
return "handled";
|
|
326
|
-
}
|
|
327
|
-
if (command === "dev") {
|
|
328
|
-
await handleDevCommand(args.slice(1));
|
|
329
|
-
return "handled";
|
|
330
|
-
}
|
|
331
|
-
if (command === "memory") {
|
|
332
|
-
await handleMemoryCommand(args.slice(1));
|
|
333
|
-
return "handled";
|
|
334
|
-
}
|
|
335
|
-
if (command === "recipe" || command === "recipes") {
|
|
336
|
-
await handleRecipeCommand(args.slice(1));
|
|
337
|
-
return "handled";
|
|
338
|
-
}
|
|
339
|
-
if (command === "provider" || command === "providers") {
|
|
340
|
-
await handleProviderCommand(args.slice(1));
|
|
341
|
-
return "handled";
|
|
342
|
-
}
|
|
343
|
-
if (command === "context-probe") {
|
|
344
|
-
await handleContextProbeCommand(args.slice(1));
|
|
345
|
-
return "handled";
|
|
346
|
-
}
|
|
347
|
-
if (command === "token" || command === "tokens") {
|
|
348
|
-
await handleTokenCommand(args.slice(1));
|
|
349
|
-
return "handled";
|
|
350
|
-
}
|
|
351
|
-
if (command === "pair" || command === "/pair" || command === "/disconnect") {
|
|
352
|
-
await handleSlashOrPairCommand(command, args.slice(1));
|
|
353
|
-
return "handled";
|
|
354
|
-
}
|
|
355
|
-
if (command === "message" || command === "send" || command === "/message" || command === "/send" || command === "/send-claimable") {
|
|
356
|
-
await handleMessageCommand(args.slice(1), { claimable: command === "/send-claimable" });
|
|
357
|
-
return "handled";
|
|
358
|
-
}
|
|
359
|
-
if (command === "get-message" || command === "read-message" || command === "/get-message" || command === "/read-message") {
|
|
360
|
-
await handleGetMessageCommand(args.slice(1));
|
|
361
|
-
return "handled";
|
|
362
|
-
}
|
|
363
|
-
if (command === "reply" || command === "/reply") {
|
|
364
|
-
await handleReplyCommand(args.slice(1));
|
|
365
|
-
return "handled";
|
|
366
|
-
}
|
|
367
|
-
if (command === "react" || command === "/react" || command === "reaction" || command === "/reaction") {
|
|
368
|
-
await handleReactCommand(args.slice(1));
|
|
369
|
-
return "handled";
|
|
370
|
-
}
|
|
371
|
-
if (command === "introspect" || command === "/introspect") {
|
|
372
|
-
await handleIntrospectCommand(args.slice(1));
|
|
373
|
-
return "handled";
|
|
374
|
-
}
|
|
375
|
-
if (command === "/status" || command === "status") {
|
|
376
|
-
await handleStatusCommand(args.slice(1));
|
|
377
|
-
return "handled";
|
|
378
|
-
}
|
|
379
|
-
if (command === "/label" || command === "label") {
|
|
380
|
-
await handleLabelCommand(args.slice(1));
|
|
381
|
-
return "handled";
|
|
382
|
-
}
|
|
383
|
-
if (command === "/tags" || command === "tags") {
|
|
384
|
-
await handleTagsCommand(args.slice(1));
|
|
385
|
-
return "handled";
|
|
386
|
-
}
|
|
387
|
-
if (command === "workspace" || command === "workspaces") {
|
|
388
|
-
await handleWorkspaceCommand(args.slice(1));
|
|
389
|
-
return "handled";
|
|
390
|
-
}
|
|
391
|
-
if (command === "steward" || command === "stewards") {
|
|
392
|
-
await handleStewardCommand(args.slice(1));
|
|
393
|
-
return "handled";
|
|
394
|
-
}
|
|
395
|
-
if (command === "/reconnect") {
|
|
396
|
-
console.log("Reconnect is handled automatically by provider runners; use `agent-relay pair status` to inspect current pair state.");
|
|
397
|
-
return "handled";
|
|
398
|
-
}
|
|
399
|
-
throw new Error(`Unknown command "${command}". Run agent-relay --help.`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async function handleUpgradeCommand(args: string[]): Promise<void> {
|
|
403
|
-
let targetVersion: string | undefined;
|
|
404
|
-
let dryRun = false;
|
|
405
|
-
let noRestart = false;
|
|
406
|
-
let restartDeferred = false;
|
|
407
|
-
let yes = false;
|
|
408
|
-
let json = false;
|
|
409
|
-
let runtimePrefix: string | undefined;
|
|
410
|
-
const pathPrefix: string[] = [];
|
|
411
|
-
const providers: UpgradeProvider[] = [];
|
|
412
|
-
const hosts: string[] = [];
|
|
413
|
-
let allHosts = false;
|
|
414
|
-
|
|
415
|
-
for (let i = 0; i < args.length; i++) {
|
|
416
|
-
const arg = args[i];
|
|
417
|
-
if (arg === "--version" && i + 1 < args.length) targetVersion = args[++i];
|
|
418
|
-
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
|
|
419
|
-
else if (arg === "--providers" && i + 1 < args.length) {
|
|
420
|
-
for (const provider of args[++i]!.split(",")) providers.push(parseUpgradeProvider(provider));
|
|
421
|
-
} else if (arg === "--provider" && i + 1 < args.length) providers.push(parseUpgradeProvider(args[++i]!));
|
|
422
|
-
else if (arg === "--host" && i + 1 < args.length) hosts.push(args[++i]!);
|
|
423
|
-
else if (arg === "--all-hosts") allHosts = true;
|
|
424
|
-
else if (arg === "--codex") providers.push("codex");
|
|
425
|
-
else if (arg === "--claude") providers.push("claude");
|
|
426
|
-
else if (arg === "--orchestrator") providers.push("orchestrator");
|
|
427
|
-
else if (arg === "--all") providers.push("all");
|
|
428
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
429
|
-
else if (arg === "--no-restart") noRestart = true;
|
|
430
|
-
else if (arg === "--restart-deferred") restartDeferred = true;
|
|
431
|
-
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
432
|
-
else if (arg === "--json") json = true;
|
|
433
|
-
else throw new Error(`Unknown upgrade option "${arg}"`);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Remote-only: drive named hosts' self-upgrade over the relay command bus and
|
|
437
|
-
// skip the local upgrade entirely (#210). `--all-hosts` instead upgrades this
|
|
438
|
-
// host first, then fans out to every behind remote (handled after the local run).
|
|
439
|
-
if (hosts.length && !allHosts) {
|
|
440
|
-
await runRemoteOrchestratorUpgrades({ hosts, targetVersion, providers, json, dryRun, yes });
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const snapshot = await detectUpgradeSnapshot({
|
|
445
|
-
...(targetVersion ? { targetVersion } : {}),
|
|
446
|
-
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
447
|
-
providers,
|
|
448
|
-
noRestart,
|
|
449
|
-
});
|
|
450
|
-
const plan = createUpgradePlan(snapshot, {
|
|
451
|
-
...(targetVersion ? { targetVersion } : {}),
|
|
452
|
-
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
453
|
-
providers,
|
|
454
|
-
noRestart,
|
|
455
|
-
restartDeferred,
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
if (json) {
|
|
459
|
-
console.log(JSON.stringify({ plan }, null, 2));
|
|
460
|
-
if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, dryRun: true });
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (dryRun) {
|
|
465
|
-
console.log(formatUpgradePlan(plan, { dryRun: true }));
|
|
466
|
-
if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, dryRun: true });
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (!yes) {
|
|
471
|
-
console.log(formatUpgradePlan(plan));
|
|
472
|
-
const ok = await confirm("Run this upgrade plan?");
|
|
473
|
-
if (!ok) {
|
|
474
|
-
console.log("Upgrade cancelled.");
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
console.log(await executeUpgradePlan(plan));
|
|
480
|
-
|
|
481
|
-
if (allHosts) {
|
|
482
|
-
await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, yes: true });
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Trigger orchestrator self-upgrade on remote hosts via the relay command bus
|
|
488
|
-
* (#210). Each host installs its own runtime + self-restarts; the relay settles
|
|
489
|
-
* the version when it re-registers. Replaces the manual ssh + npm install dance.
|
|
490
|
-
*/
|
|
491
|
-
async function runRemoteOrchestratorUpgrades(opts: {
|
|
492
|
-
hosts?: string[];
|
|
493
|
-
allBehind?: boolean;
|
|
494
|
-
targetVersion?: string;
|
|
495
|
-
providers: UpgradeProvider[];
|
|
496
|
-
json?: boolean;
|
|
497
|
-
dryRun?: boolean;
|
|
498
|
-
yes?: boolean;
|
|
499
|
-
}): Promise<void> {
|
|
500
|
-
const targetVersion = opts.targetVersion ?? VERSION;
|
|
501
|
-
const orchestrators = (await apiRequest("GET", "/api/orchestrators")) as Array<{
|
|
502
|
-
id: string;
|
|
503
|
-
version?: string;
|
|
504
|
-
}>;
|
|
505
|
-
const byId = new Map(orchestrators.map((orch) => [orch.id, orch]));
|
|
506
|
-
const localId = resolveLocalOrchestratorId();
|
|
507
|
-
// Default to "all" so a remote host's provider runner is upgraded too, not just
|
|
508
|
-
// the orchestrator package (matters for hosts running claude/codex agents).
|
|
509
|
-
const remoteProviders: UpgradeProvider[] = opts.providers.length ? opts.providers : ["all"];
|
|
510
|
-
|
|
511
|
-
let targets: string[];
|
|
512
|
-
if (opts.allBehind) {
|
|
513
|
-
targets = orchestrators
|
|
514
|
-
.filter((orch) => orch.id !== localId && orch.version && orch.version !== targetVersion)
|
|
515
|
-
.map((orch) => orch.id);
|
|
516
|
-
if (!targets.length) {
|
|
517
|
-
if (opts.json) console.log(JSON.stringify({ remoteUpgrades: [] }, null, 2));
|
|
518
|
-
else console.log(`No remote orchestrators behind ${targetVersion}.`);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
targets = opts.hosts ?? [];
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (opts.dryRun) {
|
|
526
|
-
const lines = targets.map((id) => {
|
|
527
|
-
const orch = byId.get(id);
|
|
528
|
-
const from = orch ? orch.version ?? "unknown" : "(not connected)";
|
|
529
|
-
return ` ${id}: ${from} → ${targetVersion} (providers: ${remoteProviders.join(",")})`;
|
|
530
|
-
});
|
|
531
|
-
if (opts.json) console.log(JSON.stringify({ remoteUpgrades: targets.map((id) => ({ id, targetVersion, providers: remoteProviders, dryRun: true })) }, null, 2));
|
|
532
|
-
else console.log(`Remote orchestrator upgrade plan → ${targetVersion}:\n${lines.join("\n")}`);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (!opts.yes && !opts.json) {
|
|
537
|
-
console.log(`Trigger remote orchestrator upgrade → ${targetVersion} for: ${targets.join(", ")}`);
|
|
538
|
-
const ok = await confirm("Send remote upgrade command(s)?");
|
|
539
|
-
if (!ok) {
|
|
540
|
-
console.log("Remote upgrade cancelled.");
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const results: Array<{ id: string; ok: boolean; message: string }> = [];
|
|
546
|
-
for (const id of targets) {
|
|
547
|
-
if (!byId.has(id)) {
|
|
548
|
-
results.push({ id, ok: false, message: "not connected to the relay" });
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
const res = (await apiRequest("POST", `/api/orchestrators/${encodeURIComponent(id)}/actions`, {
|
|
553
|
-
action: "upgrade",
|
|
554
|
-
targetVersion,
|
|
555
|
-
providers: remoteProviders,
|
|
556
|
-
})) as { command?: { id?: string } };
|
|
557
|
-
const from = byId.get(id)?.version;
|
|
558
|
-
results.push({ id, ok: true, message: `queued ${from ?? "?"} → ${targetVersion} (command ${res?.command?.id ?? "?"})` });
|
|
559
|
-
} catch (err) {
|
|
560
|
-
results.push({ id, ok: false, message: errMessage(err) });
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
if (opts.json) {
|
|
565
|
-
console.log(JSON.stringify({ remoteUpgrades: results }, null, 2));
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
console.log(`\nRemote orchestrator upgrades → ${targetVersion}:`);
|
|
569
|
-
for (const result of results) console.log(` ${result.ok ? "✓" : "✗"} ${result.id}: ${result.message}`);
|
|
570
|
-
console.log("\nEach host installs and self-restarts; the relay reconciles the version when it re-registers.");
|
|
571
|
-
console.log("Track progress in the dashboard Orchestrators view or via GET /api/orchestrators.");
|
|
572
|
-
if (results.some((result) => !result.ok)) {
|
|
573
|
-
process.exitCode = 1;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function parseUpgradeProvider(value: string): UpgradeProvider {
|
|
578
|
-
if (value === "auto" || value === "all" || value === "codex" || value === "claude" || value === "orchestrator") return value;
|
|
579
|
-
throw new Error(`Unknown upgrade provider "${value}". Expected auto, all, codex, claude, or orchestrator.`);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
async function handleProviderCommand(args: string[]): Promise<void> {
|
|
583
|
-
const action = args[0];
|
|
584
|
-
const provider = args[1];
|
|
585
|
-
if ((action !== "wrap" && action !== "unwrap") || (provider !== "claude" && provider !== "codex")) {
|
|
586
|
-
throw new Error("Usage: agent-relay provider <wrap|unwrap> <claude|codex>");
|
|
587
|
-
}
|
|
588
|
-
const dir = join(process.env.HOME || homedir(), ".agent-relay", "bin");
|
|
589
|
-
const shims = providerShimPaths(provider);
|
|
590
|
-
if (provider === "codex") cleanLegacyCodexSessionStartHook();
|
|
591
|
-
if (action === "wrap") {
|
|
592
|
-
for (const shim of shims) {
|
|
593
|
-
mkdirSync(dirname(shim), { recursive: true });
|
|
594
|
-
writeFileSync(shim, providerShimContent(provider), "utf8");
|
|
595
|
-
chmodSync(shim, 0o755);
|
|
596
|
-
console.log(`Wrapped ${provider}: ${shim}`);
|
|
597
|
-
}
|
|
598
|
-
console.log(`Ensure ${dir} is before the provider binary on PATH.`);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
for (const shim of shims) {
|
|
602
|
-
if (existsSync(shim)) unlinkSync(shim);
|
|
603
|
-
console.log(`Unwrapped ${provider}: ${shim}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function cleanLegacyCodexSessionStartHook(): void {
|
|
608
|
-
const configPath = join(process.env.HOME || homedir(), ".codex", "config.toml");
|
|
609
|
-
if (!existsSync(configPath)) return;
|
|
610
|
-
const before = readFileSync(configPath, "utf8");
|
|
611
|
-
const after = removeLegacyCodexSessionStartHookToml(before);
|
|
612
|
-
if (after === before) return;
|
|
613
|
-
writeFileSync(configPath, after, "utf8");
|
|
614
|
-
console.log(`Removed legacy Agent Relay Codex hook entries from ${configPath}`);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
export function removeLegacyCodexSessionStartHookToml(input: string): string {
|
|
618
|
-
const blocks = tomlHookBlocks(input);
|
|
619
|
-
let output = "";
|
|
620
|
-
for (let index = 0; index < blocks.length;) {
|
|
621
|
-
const block = blocks[index];
|
|
622
|
-
if (isCodexHookGroupHeader(block?.header)) {
|
|
623
|
-
const group = block!;
|
|
624
|
-
const hooks: typeof blocks = [];
|
|
625
|
-
const hookHeader = codexHookHandlerHeader(group.header!);
|
|
626
|
-
index += 1;
|
|
627
|
-
while (index < blocks.length && blocks[index]?.header?.trim() === hookHeader) {
|
|
628
|
-
hooks.push(blocks[index]!);
|
|
629
|
-
index += 1;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const keptHooks = hooks.filter((hook) => !isLegacyCodexSessionStartHook(hook.text));
|
|
633
|
-
const groupIsLegacyOnly = isLegacyCodexSessionStartHook(group.text) || (hooks.length > 0 && keptHooks.length === 0);
|
|
634
|
-
if (!groupIsLegacyOnly || keptHooks.length > 0) {
|
|
635
|
-
output += `${group.text}\n`;
|
|
636
|
-
for (const hook of keptHooks) output += `${hook.text}\n`;
|
|
637
|
-
}
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (isCodexHookHandlerHeader(block?.header) && isLegacyCodexSessionStartHook(block?.text ?? "")) {
|
|
642
|
-
index += 1;
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (block) output += `${block.text}\n`;
|
|
647
|
-
index += 1;
|
|
648
|
-
}
|
|
649
|
-
return output.trimEnd() + "\n";
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function tomlHookBlocks(input: string): Array<{ header: string | null; text: string }> {
|
|
653
|
-
const lines = input.split(/\r?\n/);
|
|
654
|
-
const blocks: Array<{ header: string | null; text: string }> = [];
|
|
655
|
-
let current: { header: string | null; lines: string[] } = { header: null, lines: [] };
|
|
656
|
-
for (const line of lines) {
|
|
657
|
-
const header = line.match(/^\s*\[\[hooks\.[^\]]+\]\]\s*$/)?.[0] ?? null;
|
|
658
|
-
if (header) {
|
|
659
|
-
if (current.lines.length > 0) blocks.push({ header: current.header, text: current.lines.join("\n").trimEnd() });
|
|
660
|
-
current = { header, lines: [line] };
|
|
661
|
-
} else {
|
|
662
|
-
current.lines.push(line);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
if (current.lines.length > 0) blocks.push({ header: current.header, text: current.lines.join("\n").trimEnd() });
|
|
666
|
-
return blocks.filter((block) => block.text.trim().length > 0);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function isCodexHookGroupHeader(header: string | null | undefined): boolean {
|
|
670
|
-
return /^\[\[hooks\.[^.]+\]\]$/.test(header?.trim() ?? "");
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function isCodexHookHandlerHeader(header: string | null | undefined): boolean {
|
|
674
|
-
return /^\[\[hooks\.[^.]+\.hooks\]\]$/.test(header?.trim() ?? "");
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function codexHookHandlerHeader(groupHeader: string): string {
|
|
678
|
-
return groupHeader.trim().replace(/\]\]$/, ".hooks]]");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function isLegacyCodexSessionStartHook(text: string): boolean {
|
|
682
|
-
return /\.agent-relay\/codex\/package\/hooks\//.test(text) ||
|
|
683
|
-
/agent-relay-codex.*hook/i.test(text) ||
|
|
684
|
-
/agent-relay.*(SessionStart|UserPromptSubmit|Stop).*hook/i.test(text);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async function handleContextProbeCommand(args: string[]): Promise<void> {
|
|
688
|
-
const printStatusLine = args[0] === "print-status-line";
|
|
689
|
-
const inputArgs = printStatusLine ? args.slice(1) : args;
|
|
690
|
-
let wrapCommand: string | undefined;
|
|
691
|
-
let wrapRequested = false;
|
|
692
|
-
let agentId: string | undefined;
|
|
693
|
-
let stateDir: string | undefined;
|
|
694
|
-
let standalone = false;
|
|
695
|
-
|
|
696
|
-
for (let i = 0; i < inputArgs.length; i++) {
|
|
697
|
-
const arg = inputArgs[i];
|
|
698
|
-
if (arg === "--wrap") {
|
|
699
|
-
wrapRequested = true;
|
|
700
|
-
const next = inputArgs[i + 1];
|
|
701
|
-
if (next && !next.startsWith("--")) {
|
|
702
|
-
wrapCommand = next;
|
|
703
|
-
i++;
|
|
704
|
-
}
|
|
705
|
-
} else if (arg === "--agent-id") {
|
|
706
|
-
agentId = inputArgs[++i];
|
|
707
|
-
} else if (arg === "--state-dir") {
|
|
708
|
-
stateDir = inputArgs[++i] ?? stateDir;
|
|
709
|
-
} else if (arg === "--standalone") {
|
|
710
|
-
standalone = true;
|
|
711
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
712
|
-
console.log("Usage: agent-relay context-probe [print-status-line] [--wrap COMMAND] [--agent-id ID] [--state-dir DIR] [--standalone]");
|
|
713
|
-
return;
|
|
714
|
-
} else {
|
|
715
|
-
throw new Error(`Unknown context-probe option "${arg}"`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (printStatusLine) {
|
|
720
|
-
const command = [
|
|
721
|
-
"agent-relay",
|
|
722
|
-
"context-probe",
|
|
723
|
-
...(wrapRequested ? ["--wrap", ...(wrapCommand ? [shellQuote(wrapCommand)] : [])] : ["--standalone"]),
|
|
724
|
-
...(agentId ? ["--agent-id", shellQuote(agentId)] : []),
|
|
725
|
-
...(stateDir ? ["--state-dir", shellQuote(stateDir)] : []),
|
|
726
|
-
].join(" ");
|
|
727
|
-
console.log(command);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (wrapRequested && !wrapCommand) wrapCommand = currentClaudeStatusLineCommand();
|
|
732
|
-
if (wrapCommand && commandLooksLikeContextProbe(wrapCommand)) {
|
|
733
|
-
wrapCommand = undefined;
|
|
734
|
-
standalone = true;
|
|
735
|
-
}
|
|
736
|
-
if (!wrapRequested && !standalone) {
|
|
737
|
-
throw new Error("Usage: agent-relay context-probe [--wrap COMMAND|--standalone] [--agent-id ID] [--state-dir DIR]");
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const input = await readStdin();
|
|
741
|
-
const result = runContextProbe(input, { wrapCommand, agentId, stateDir, standalone });
|
|
742
|
-
if (result.wrappedStderr) process.stderr.write(result.wrappedStderr);
|
|
743
|
-
if (result.output) process.stdout.write(result.output);
|
|
744
|
-
if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
|
|
745
|
-
if (typeof result.wrappedExitCode === "number" && result.wrappedExitCode !== 0) process.exit(result.wrappedExitCode);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function providerShimPaths(provider: "claude" | "codex"): string[] {
|
|
749
|
-
const root = join(process.env.HOME || homedir(), ".agent-relay");
|
|
750
|
-
const paths = [join(root, "bin", provider)];
|
|
751
|
-
if (provider === "codex") paths.push(join(root, "codex", "bin", "codex"));
|
|
752
|
-
return paths;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async function readStdin(): Promise<string> {
|
|
756
|
-
if (typeof Bun !== "undefined" && Bun.stdin && typeof Bun.stdin.text === "function") {
|
|
757
|
-
return Bun.stdin.text();
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
let value = "";
|
|
761
|
-
for await (const chunk of process.stdin) value += String(chunk);
|
|
762
|
-
return value;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function currentClaudeStatusLineCommand(): string | undefined {
|
|
766
|
-
const settingsPath = join(process.env.HOME || homedir(), ".claude", "settings.json");
|
|
767
|
-
try {
|
|
768
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as unknown;
|
|
769
|
-
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return undefined;
|
|
770
|
-
const statusLine = (settings as Record<string, unknown>).statusLine;
|
|
771
|
-
if (!statusLine || typeof statusLine !== "object" || Array.isArray(statusLine)) return undefined;
|
|
772
|
-
const command = (statusLine as Record<string, unknown>).command;
|
|
773
|
-
return typeof command === "string" && command.trim() ? command : undefined;
|
|
774
|
-
} catch {
|
|
775
|
-
return undefined;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function commandLooksLikeContextProbe(command: string): boolean {
|
|
780
|
-
return /\bagent-relay\s+context-probe\b/.test(command);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
function providerShimContent(provider: "claude" | "codex"): string {
|
|
784
|
-
if (provider === "claude") {
|
|
785
|
-
return `#!/usr/bin/env bash\nexec claude-relay claude -- "$@"\n`;
|
|
786
|
-
}
|
|
787
|
-
return `#!/usr/bin/env bash
|
|
788
|
-
set -e
|
|
789
|
-
|
|
790
|
-
resolve_path() {
|
|
791
|
-
if command -v realpath >/dev/null 2>&1; then
|
|
792
|
-
realpath "$1" 2>/dev/null || printf '%s\\n' "$1"
|
|
793
|
-
elif command -v readlink >/dev/null 2>&1; then
|
|
794
|
-
readlink -f "$1" 2>/dev/null || printf '%s\\n' "$1"
|
|
795
|
-
else
|
|
796
|
-
printf '%s\\n' "$1"
|
|
797
|
-
fi
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
find_real_codex() {
|
|
801
|
-
local self resolved candidate
|
|
802
|
-
self="$(resolve_path "$0")"
|
|
803
|
-
IFS=: read -r -a path_parts <<< "$PATH"
|
|
804
|
-
for dir in "\${path_parts[@]}"; do
|
|
805
|
-
candidate="$dir/codex"
|
|
806
|
-
[ -x "$candidate" ] || continue
|
|
807
|
-
resolved="$(resolve_path "$candidate")"
|
|
808
|
-
[ "$resolved" = "$self" ] && continue
|
|
809
|
-
printf '%s\\n' "$candidate"
|
|
810
|
-
return 0
|
|
811
|
-
done
|
|
812
|
-
return 1
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
first_command() {
|
|
816
|
-
local skip_next=0
|
|
817
|
-
for arg in "$@"; do
|
|
818
|
-
if [ "$skip_next" = 1 ]; then
|
|
819
|
-
skip_next=0
|
|
820
|
-
continue
|
|
821
|
-
fi
|
|
822
|
-
case "$arg" in
|
|
823
|
-
--) break ;;
|
|
824
|
-
-c|--config|--enable|--disable|-i|--image|-m|--model|--local-provider|-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--remote|--remote-auth-token-env)
|
|
825
|
-
skip_next=1
|
|
826
|
-
;;
|
|
827
|
-
--config=*|--enable=*|--disable=*|--image=*|--model=*|--local-provider=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
|
|
828
|
-
;;
|
|
829
|
-
-*)
|
|
830
|
-
;;
|
|
831
|
-
*)
|
|
832
|
-
printf '%s\\n' "$arg"
|
|
833
|
-
return 0
|
|
834
|
-
;;
|
|
835
|
-
esac
|
|
836
|
-
done
|
|
837
|
-
return 0
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
has_passthrough_option() {
|
|
841
|
-
for arg in "$@"; do
|
|
842
|
-
case "$arg" in
|
|
843
|
-
-h|--help|-V|--version)
|
|
844
|
-
return 0
|
|
845
|
-
;;
|
|
846
|
-
esac
|
|
847
|
-
done
|
|
848
|
-
return 1
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
exec_real_codex() {
|
|
852
|
-
real_codex="$(find_real_codex)" || {
|
|
853
|
-
echo "agent-relay codex shim could not find the real codex binary on PATH" >&2
|
|
854
|
-
exit 127
|
|
855
|
-
}
|
|
856
|
-
exec "$real_codex" "$@"
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if has_passthrough_option "$@"; then
|
|
860
|
-
exec_real_codex "$@"
|
|
861
|
-
fi
|
|
862
|
-
|
|
863
|
-
case "$(first_command "$@")" in
|
|
864
|
-
exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|sandbox|debug|apply|a|cloud|exec-server|features|help)
|
|
865
|
-
exec_real_codex "$@"
|
|
866
|
-
;;
|
|
867
|
-
esac
|
|
868
|
-
|
|
869
|
-
exec codex-relay codex -- "$@"
|
|
870
|
-
`;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
async function handleSlashOrPairCommand(command: string, args: string[]): Promise<void> {
|
|
874
|
-
if (command === "/disconnect") {
|
|
875
|
-
await handlePairCommand(["hangup", ...args]);
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
if (command === "/pair") {
|
|
879
|
-
await handlePairCommand(args);
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
await handlePairCommand(args);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
async function handleSetupCommand(args: string[]): Promise<void> {
|
|
886
|
-
let envFile: string | undefined;
|
|
887
|
-
let host: string | undefined;
|
|
888
|
-
let port: number | undefined;
|
|
889
|
-
let dbPath: string | undefined;
|
|
890
|
-
let runtimePrefix: string | undefined;
|
|
891
|
-
let token: string | undefined;
|
|
892
|
-
let generateToken = true;
|
|
893
|
-
let dryRun = false;
|
|
894
|
-
let force = false;
|
|
895
|
-
let yes = false;
|
|
896
|
-
let json = false;
|
|
897
|
-
|
|
898
|
-
for (let i = 0; i < args.length; i++) {
|
|
899
|
-
const arg = args[i];
|
|
900
|
-
if (arg === "--env-file" && i + 1 < args.length) envFile = args[++i];
|
|
901
|
-
else if (arg === "--host" && i + 1 < args.length) host = args[++i];
|
|
902
|
-
else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
|
|
903
|
-
else if (arg === "--db-path" && i + 1 < args.length) dbPath = args[++i];
|
|
904
|
-
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
|
|
905
|
-
else if (arg === "--token" && i + 1 < args.length) token = args[++i];
|
|
906
|
-
else if (arg === "--generate-token") generateToken = true;
|
|
907
|
-
else if (arg === "--no-token") generateToken = false;
|
|
908
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
909
|
-
else if (arg === "--force") force = true;
|
|
910
|
-
else if (arg === "--yes") yes = true;
|
|
911
|
-
else if (arg === "--json") json = true;
|
|
912
|
-
else throw new Error(`Unknown setup option "${arg}"`);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const plan = createSetupPlan({
|
|
916
|
-
...(envFile ? { envFile } : {}),
|
|
917
|
-
...(host ? { host } : {}),
|
|
918
|
-
...(port !== undefined ? { port } : {}),
|
|
919
|
-
...(dbPath ? { dbPath } : {}),
|
|
920
|
-
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
921
|
-
...(token ? { token } : {}),
|
|
922
|
-
generateToken,
|
|
923
|
-
force,
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
if (!dryRun && !yes && await pathExists(plan.envFile)) {
|
|
927
|
-
const ok = await confirm(`Overwrite ${plan.envFile}?`);
|
|
928
|
-
if (!ok) {
|
|
929
|
-
console.log("Setup cancelled.");
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const result = await executeSetupPlan(plan, { dryRun, force });
|
|
935
|
-
if (json) console.log(JSON.stringify({ plan, output: result }, null, 2));
|
|
936
|
-
else console.log(dryRun ? formatSetupPlan(plan) : result);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
async function handleDaemonCommand(args: string[]): Promise<void> {
|
|
940
|
-
const action = parseDaemonAction(args[0]);
|
|
941
|
-
let name: string | undefined;
|
|
942
|
-
let envFile: string | undefined;
|
|
943
|
-
let port: number | undefined;
|
|
944
|
-
let host: string | undefined;
|
|
945
|
-
let scope: DaemonScope | undefined;
|
|
946
|
-
let binaryPath: string | undefined;
|
|
947
|
-
let runtimePrefix: string | undefined;
|
|
948
|
-
const pathPrefix: string[] = [];
|
|
949
|
-
let start = false;
|
|
950
|
-
let enable = false;
|
|
951
|
-
let dryRun = false;
|
|
952
|
-
let force = false;
|
|
953
|
-
let yes = false;
|
|
954
|
-
let json = false;
|
|
955
|
-
|
|
956
|
-
for (let i = 1; i < args.length; i++) {
|
|
957
|
-
const arg = args[i];
|
|
958
|
-
if (arg === "--name" && i + 1 < args.length) name = args[++i];
|
|
959
|
-
else if (arg === "--env-file" && i + 1 < args.length) envFile = args[++i];
|
|
960
|
-
else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
|
|
961
|
-
else if (arg === "--host" && i + 1 < args.length) host = args[++i];
|
|
962
|
-
else if (arg === "--user") scope = "user";
|
|
963
|
-
else if (arg === "--system") scope = "system";
|
|
964
|
-
else if (arg === "--binary" && i + 1 < args.length) binaryPath = args[++i];
|
|
965
|
-
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
|
|
966
|
-
else if (arg === "--path-prefix" && i + 1 < args.length) pathPrefix.push(args[++i]!);
|
|
967
|
-
else if (arg === "--start") start = true;
|
|
968
|
-
else if (arg === "--enable") enable = true;
|
|
969
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
970
|
-
else if (arg === "--force") force = true;
|
|
971
|
-
else if (arg === "--yes") yes = true;
|
|
972
|
-
else if (arg === "--json") json = true;
|
|
973
|
-
else throw new Error(`Unknown daemon option "${arg}"`);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
if (action === "install" && !dryRun && !envFile && !(await pathExists(createSetupPlan().envFile))) {
|
|
977
|
-
const setupPlan = createSetupPlan({
|
|
978
|
-
...(host ? { host } : {}),
|
|
979
|
-
...(port !== undefined ? { port } : {}),
|
|
980
|
-
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
981
|
-
});
|
|
982
|
-
const ok = yes || await confirm(`Create daemon env file at ${setupPlan.envFile}?`);
|
|
983
|
-
if (!ok) throw new Error("Daemon install needs an env file. Run `agent-relay setup` first.");
|
|
984
|
-
console.log(await executeSetupPlan(setupPlan, { force }));
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const env = await detectDaemonEnvironment();
|
|
988
|
-
const plan = createDaemonPlan({
|
|
989
|
-
action,
|
|
990
|
-
...(name ? { name } : {}),
|
|
991
|
-
...(envFile ? { envFile } : {}),
|
|
992
|
-
...(port !== undefined ? { port } : {}),
|
|
993
|
-
...(host ? { host } : {}),
|
|
994
|
-
...(scope ? { scope } : {}),
|
|
995
|
-
...(binaryPath ? { binaryPath } : {}),
|
|
996
|
-
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
997
|
-
...(pathPrefix.length ? { pathPrefix } : {}),
|
|
998
|
-
start,
|
|
999
|
-
enable,
|
|
1000
|
-
}, env);
|
|
1001
|
-
|
|
1002
|
-
if (!dryRun && !json && (action === "install" || action === "uninstall") && !yes) {
|
|
1003
|
-
const ok = await confirm(
|
|
1004
|
-
action === "install"
|
|
1005
|
-
? `Install ${plan.kind} ${plan.scope} daemon "${plan.name}"?`
|
|
1006
|
-
: `Uninstall daemon "${plan.name}"?`,
|
|
1007
|
-
);
|
|
1008
|
-
if (!ok) {
|
|
1009
|
-
console.log("Daemon command cancelled.");
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const result = await executeDaemonPlan(plan, { dryRun, force });
|
|
1015
|
-
if (json) console.log(JSON.stringify({ plan: result.plan, output: result.output }, null, 2));
|
|
1016
|
-
else console.log(dryRun ? formatDaemonPlan(plan) : result.output);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
async function handleOrchestratorCommand(args: string[]): Promise<void> {
|
|
1020
|
-
const subcommand = args[0];
|
|
1021
|
-
if (subcommand !== "install") {
|
|
1022
|
-
throw new Error("Usage: agent-relay orchestrator install --relay-url URL --token TOKEN|--bootstrap-token TOKEN [options]");
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
let relayUrl = "";
|
|
1026
|
-
let token = "";
|
|
1027
|
-
let bootstrapToken = "";
|
|
1028
|
-
let id = sanitizeOrchestratorId(osHostname());
|
|
1029
|
-
let baseDir = join(homedir(), "projects");
|
|
1030
|
-
let providers = "claude,codex";
|
|
1031
|
-
let apiPort = 4860;
|
|
1032
|
-
let runtimePrefix = defaultRuntimePrefix();
|
|
1033
|
-
let serviceName = "agent-relay-orchestrator";
|
|
1034
|
-
let version = VERSION;
|
|
1035
|
-
const pathPrefix: string[] = [];
|
|
1036
|
-
let dryRun = false;
|
|
1037
|
-
let yes = false;
|
|
1038
|
-
let force = false;
|
|
1039
|
-
let json = false;
|
|
1040
|
-
|
|
1041
|
-
for (let i = 1; i < args.length; i++) {
|
|
1042
|
-
const arg = args[i];
|
|
1043
|
-
if (arg === "--relay-url" && i + 1 < args.length) relayUrl = args[++i]!;
|
|
1044
|
-
else if (arg === "--token" && i + 1 < args.length) token = args[++i]!;
|
|
1045
|
-
else if (arg === "--bootstrap-token" && i + 1 < args.length) bootstrapToken = args[++i]!;
|
|
1046
|
-
else if (arg === "--id" && i + 1 < args.length) id = sanitizeOrchestratorId(args[++i]!);
|
|
1047
|
-
else if (arg === "--base-dir" && i + 1 < args.length) baseDir = resolve(expandHomePath(args[++i]!));
|
|
1048
|
-
else if (arg === "--providers" && i + 1 < args.length) providers = args[++i]!;
|
|
1049
|
-
else if (arg === "--api-port" && i + 1 < args.length) apiPort = parseInt(args[++i]!, 10);
|
|
1050
|
-
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = resolve(expandHomePath(args[++i]!));
|
|
1051
|
-
else if (arg === "--service-name" && i + 1 < args.length) serviceName = args[++i]!;
|
|
1052
|
-
else if (arg === "--version" && i + 1 < args.length) version = args[++i]!;
|
|
1053
|
-
else if (arg === "--path-prefix" && i + 1 < args.length) pathPrefix.push(resolve(expandHomePath(args[++i]!)));
|
|
1054
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
1055
|
-
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
1056
|
-
else if (arg === "--force") force = true;
|
|
1057
|
-
else if (arg === "--json") json = true;
|
|
1058
|
-
else throw new Error(`Unknown orchestrator install option "${arg}"`);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (!relayUrl) throw new Error("--relay-url is required");
|
|
1062
|
-
if (!token && !bootstrapToken) throw new Error("--token or --bootstrap-token is required");
|
|
1063
|
-
if (!Number.isInteger(apiPort) || apiPort < 1 || apiPort > 65535) throw new Error("--api-port must be an integer from 1 to 65535");
|
|
1064
|
-
|
|
1065
|
-
const providerList = providers.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1066
|
-
for (const provider of providerList) {
|
|
1067
|
-
if (provider !== "claude" && provider !== "codex") throw new Error(`Unknown provider "${provider}". Expected claude or codex.`);
|
|
1068
|
-
}
|
|
1069
|
-
const detectedPathPrefix = [...pathPrefix, ...detectProviderPathPrefixes(providerList as Array<"claude" | "codex">)];
|
|
1070
|
-
const uniquePathPrefix = [...new Set(detectedPathPrefix)];
|
|
1071
|
-
const envToken = token || (dryRun || json ? "<runtime-token-from-bootstrap>" : await exchangeOrchestratorBootstrapToken(relayUrl, bootstrapToken, id, baseDir));
|
|
1072
|
-
const envFile = defaultOrchestratorEnvFile();
|
|
1073
|
-
const binaryPath = runtimeBinPath("agent-relay-orchestrator", runtimePrefix);
|
|
1074
|
-
const envValues: Record<string, string> = {
|
|
1075
|
-
AGENT_RELAY_URL: relayUrl,
|
|
1076
|
-
AGENT_RELAY_TOKEN: envToken,
|
|
1077
|
-
AGENT_RELAY_ORCHESTRATOR_ID: id,
|
|
1078
|
-
AGENT_RELAY_ORCHESTRATOR_HOSTNAME: osHostname(),
|
|
1079
|
-
AGENT_RELAY_ORCHESTRATOR_BASE_DIR: baseDir,
|
|
1080
|
-
AGENT_RELAY_ORCHESTRATOR_PROVIDERS: providerList.join(","),
|
|
1081
|
-
AGENT_RELAY_ORCHESTRATOR_API_PORT: String(apiPort),
|
|
1082
|
-
};
|
|
1083
|
-
if (uniquePathPrefix.length) envValues.AGENT_RELAY_ORCHESTRATOR_PATH_PREFIX = uniquePathPrefix.join(":");
|
|
1084
|
-
|
|
1085
|
-
const daemonEnv = await detectDaemonEnvironment();
|
|
1086
|
-
const daemonPlan = createDaemonPlan({
|
|
1087
|
-
action: "install",
|
|
1088
|
-
name: serviceName,
|
|
1089
|
-
envFile,
|
|
1090
|
-
binaryPath,
|
|
1091
|
-
runtimePrefix,
|
|
1092
|
-
pathPrefix: uniquePathPrefix,
|
|
1093
|
-
enable: true,
|
|
1094
|
-
start: true,
|
|
1095
|
-
}, daemonEnv);
|
|
1096
|
-
const packageCommand = ["npm", "install", "--prefix", runtimePrefix, `agent-relay-runner@${version}`, `agent-relay-orchestrator@${version}`];
|
|
1097
|
-
|
|
1098
|
-
if (json || dryRun) {
|
|
1099
|
-
const plan = {
|
|
1100
|
-
id,
|
|
1101
|
-
relayUrl,
|
|
1102
|
-
baseDir,
|
|
1103
|
-
providers: providerList,
|
|
1104
|
-
apiPort,
|
|
1105
|
-
runtimePrefix,
|
|
1106
|
-
envFile,
|
|
1107
|
-
binaryPath,
|
|
1108
|
-
serviceName,
|
|
1109
|
-
version,
|
|
1110
|
-
pathPrefix: uniquePathPrefix,
|
|
1111
|
-
packageCommand,
|
|
1112
|
-
daemon: daemonPlan,
|
|
1113
|
-
};
|
|
1114
|
-
if (json) console.log(JSON.stringify({ plan }, null, 2));
|
|
1115
|
-
else console.log(formatOrchestratorInstallPlan(plan));
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
if (!yes) {
|
|
1120
|
-
const ok = await confirm(`Install orchestrator "${id}" as daemon "${serviceName}"?`);
|
|
1121
|
-
if (!ok) {
|
|
1122
|
-
console.log("Orchestrator install cancelled.");
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
mkdirSync(dirname(envFile), { recursive: true });
|
|
1128
|
-
mkdirSync(baseDir, { recursive: true });
|
|
1129
|
-
writeFileSync(envFile, renderEnvFile(envValues), { mode: 0o600 });
|
|
1130
|
-
chmodSync(envFile, 0o600);
|
|
1131
|
-
console.log(`Wrote ${envFile}`);
|
|
1132
|
-
|
|
1133
|
-
runChecked(packageCommand);
|
|
1134
|
-
const result = await executeDaemonPlan(daemonPlan, { force });
|
|
1135
|
-
console.log(result.output);
|
|
1136
|
-
console.log(`Orchestrator install complete. Verify in Relay: ${relayUrl}`);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function defaultOrchestratorEnvFile(): string {
|
|
1140
|
-
const platform = process.platform;
|
|
1141
|
-
if (platform === "darwin") return join(homedir(), "Library", "Application Support", "agent-relay", "orchestrator.env");
|
|
1142
|
-
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "agent-relay", "orchestrator.env");
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
async function exchangeOrchestratorBootstrapToken(relayUrl: string, bootstrapToken: string, id: string, baseDir: string): Promise<string> {
|
|
1146
|
-
const response = await fetch(new URL("/api/orchestrators/bootstrap/exchange", relayUrl), {
|
|
1147
|
-
method: "POST",
|
|
1148
|
-
headers: {
|
|
1149
|
-
"Content-Type": "application/json",
|
|
1150
|
-
[RELAY_TOKEN_HEADER]: bootstrapToken,
|
|
1151
|
-
},
|
|
1152
|
-
body: JSON.stringify({ id, baseDir }),
|
|
1153
|
-
});
|
|
1154
|
-
const payload = await response.json().catch(() => null) as { token?: string; error?: string } | null;
|
|
1155
|
-
if (!response.ok || !payload?.token) {
|
|
1156
|
-
throw new Error(`bootstrap token exchange failed (${response.status}): ${payload?.error ?? response.statusText}`);
|
|
1157
|
-
}
|
|
1158
|
-
return payload.token;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
function sanitizeOrchestratorId(value: string): string {
|
|
1162
|
-
const clean = value.trim().replace(/\./g, "-").replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1163
|
-
if (!clean) throw new Error("--id must contain at least one letter or number");
|
|
1164
|
-
return clean.slice(0, 120);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function expandHomePath(value: string): string {
|
|
1168
|
-
if (value === "~") return homedir();
|
|
1169
|
-
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
1170
|
-
return value;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function detectProviderPathPrefixes(providers: Array<"claude" | "codex">): string[] {
|
|
1174
|
-
const prefixes: string[] = [];
|
|
1175
|
-
for (const provider of providers) {
|
|
1176
|
-
const path = resolveExecutableOnPath(provider);
|
|
1177
|
-
if (path) prefixes.push(dirname(path));
|
|
1178
|
-
}
|
|
1179
|
-
return prefixes;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function resolveExecutableOnPath(command: string): string | undefined {
|
|
1183
|
-
if (command.includes("/")) return existsSync(command) ? command : undefined;
|
|
1184
|
-
for (const entry of (process.env.PATH ?? "").split(":")) {
|
|
1185
|
-
if (!entry) continue;
|
|
1186
|
-
const candidate = join(entry, command);
|
|
1187
|
-
try {
|
|
1188
|
-
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
1189
|
-
} catch {}
|
|
1190
|
-
}
|
|
1191
|
-
return undefined;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function runChecked(command: string[]): void {
|
|
1195
|
-
console.log(`$ ${command.map(shellQuote).join(" ")}`);
|
|
1196
|
-
const result = Bun.spawnSync(command, { stdin: "ignore", stdout: "inherit", stderr: "inherit" });
|
|
1197
|
-
if (result.exitCode !== 0) throw new Error(`${command[0]} exited with code ${result.exitCode}`);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function formatOrchestratorInstallPlan(plan: {
|
|
1201
|
-
id: string;
|
|
1202
|
-
relayUrl: string;
|
|
1203
|
-
baseDir: string;
|
|
1204
|
-
providers: string[];
|
|
1205
|
-
apiPort: number;
|
|
1206
|
-
runtimePrefix: string;
|
|
1207
|
-
envFile: string;
|
|
1208
|
-
binaryPath: string;
|
|
1209
|
-
serviceName: string;
|
|
1210
|
-
version: string;
|
|
1211
|
-
pathPrefix: string[];
|
|
1212
|
-
packageCommand: string[];
|
|
1213
|
-
daemon: ReturnType<typeof createDaemonPlan>;
|
|
1214
|
-
}): string {
|
|
1215
|
-
const lines = [
|
|
1216
|
-
"Orchestrator install plan",
|
|
1217
|
-
`ID: ${plan.id}`,
|
|
1218
|
-
`Relay: ${plan.relayUrl}`,
|
|
1219
|
-
`Base dir: ${plan.baseDir}`,
|
|
1220
|
-
`Providers: ${plan.providers.join(", ") || "(none)"}`,
|
|
1221
|
-
`API port: ${plan.apiPort}`,
|
|
1222
|
-
`Runtime prefix: ${plan.runtimePrefix}`,
|
|
1223
|
-
`Env file: ${plan.envFile}`,
|
|
1224
|
-
`Binary: ${plan.binaryPath}`,
|
|
1225
|
-
`Service: ${plan.serviceName}`,
|
|
1226
|
-
`Package version: ${plan.version}`,
|
|
1227
|
-
];
|
|
1228
|
-
if (plan.pathPrefix.length) lines.push(`PATH prefix: ${plan.pathPrefix.join(":")}`);
|
|
1229
|
-
lines.push("", "Package command:", ` ${plan.packageCommand.join(" ")}`);
|
|
1230
|
-
lines.push("", formatDaemonPlan(plan.daemon));
|
|
1231
|
-
return lines.join("\n");
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
function parseDaemonAction(value: string | undefined): DaemonAction {
|
|
1235
|
-
if (!value || !DAEMON_ACTIONS.has(value as DaemonAction)) {
|
|
1236
|
-
throw new Error("Usage: agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]");
|
|
1237
|
-
}
|
|
1238
|
-
return value as DaemonAction;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
async function handleDevCommand(args: string[]): Promise<void> {
|
|
1242
|
-
const action = args[0];
|
|
1243
|
-
if (!action) throw new Error("Usage: agent-relay dev <pack|install|service|smoke> [options]");
|
|
1244
|
-
if (action === "pack") {
|
|
1245
|
-
let packages: string | undefined;
|
|
1246
|
-
let outDir: string | undefined;
|
|
1247
|
-
let dryRun = false;
|
|
1248
|
-
let json = false;
|
|
1249
|
-
for (let i = 1; i < args.length; i++) {
|
|
1250
|
-
const arg = args[i];
|
|
1251
|
-
if (arg === "--packages" && i + 1 < args.length) packages = args[++i];
|
|
1252
|
-
else if (arg === "--out" && i + 1 < args.length) outDir = args[++i];
|
|
1253
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
1254
|
-
else if (arg === "--json") json = true;
|
|
1255
|
-
else throw new Error(`Unknown dev pack option "${arg}"`);
|
|
1256
|
-
}
|
|
1257
|
-
const plan = createDevPackPlan({
|
|
1258
|
-
...(packages ? { packages: parseDevPackages(packages) } : {}),
|
|
1259
|
-
...(outDir ? { outDir } : {}),
|
|
1260
|
-
});
|
|
1261
|
-
const result = await executeDevPackPlan(plan, { dryRun });
|
|
1262
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1263
|
-
else console.log(dryRun ? formatDevPackPlan(plan) : result.output);
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (action === "install") {
|
|
1268
|
-
let packages: string | undefined;
|
|
1269
|
-
let outDir: string | undefined;
|
|
1270
|
-
let prefix: string | undefined;
|
|
1271
|
-
let dryRun = false;
|
|
1272
|
-
let json = false;
|
|
1273
|
-
for (let i = 1; i < args.length; i++) {
|
|
1274
|
-
const arg = args[i];
|
|
1275
|
-
if (arg === "--packages" && i + 1 < args.length) packages = args[++i];
|
|
1276
|
-
else if (arg === "--out" && i + 1 < args.length) outDir = args[++i];
|
|
1277
|
-
else if (arg === "--prefix" && i + 1 < args.length) prefix = args[++i];
|
|
1278
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
1279
|
-
else if (arg === "--json") json = true;
|
|
1280
|
-
else throw new Error(`Unknown dev install option "${arg}"`);
|
|
1281
|
-
}
|
|
1282
|
-
const plan = createDevInstallPlan({
|
|
1283
|
-
...(packages ? { packages: parseDevPackages(packages) } : {}),
|
|
1284
|
-
...(outDir ? { outDir } : {}),
|
|
1285
|
-
...(prefix ? { prefix } : {}),
|
|
1286
|
-
});
|
|
1287
|
-
const result = await executeDevInstallPlan(plan, { dryRun });
|
|
1288
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1289
|
-
else console.log(dryRun ? formatDevInstallPlan(plan) : result.output);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (action === "service") {
|
|
1294
|
-
await handleDevServiceCommand(args.slice(1));
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
if (action === "smoke") {
|
|
1299
|
-
let rootDir: string | undefined;
|
|
1300
|
-
let providers: string | undefined;
|
|
1301
|
-
let cwd: string | undefined;
|
|
1302
|
-
let orchestratorId: string | undefined;
|
|
1303
|
-
let timeoutMs: number | undefined;
|
|
1304
|
-
for (let i = 1; i < args.length; i++) {
|
|
1305
|
-
const arg = args[i];
|
|
1306
|
-
if (arg === "--root" && i + 1 < args.length) rootDir = args[++i];
|
|
1307
|
-
else if (arg === "--providers" && i + 1 < args.length) providers = args[++i];
|
|
1308
|
-
else if (arg === "--cwd" && i + 1 < args.length) cwd = args[++i];
|
|
1309
|
-
else if (arg === "--orchestrator" && i + 1 < args.length) orchestratorId = args[++i];
|
|
1310
|
-
else if (arg === "--timeout" && i + 1 < args.length) timeoutMs = parseInt(args[++i]!, 10);
|
|
1311
|
-
else throw new Error(`Unknown dev smoke option "${arg}"`);
|
|
1312
|
-
}
|
|
1313
|
-
const result = await executeDevSmoke({
|
|
1314
|
-
...(rootDir ? { rootDir } : {}),
|
|
1315
|
-
...(providers ? { providers } : {}),
|
|
1316
|
-
...(cwd ? { cwd } : {}),
|
|
1317
|
-
...(orchestratorId ? { orchestratorId } : {}),
|
|
1318
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1319
|
-
});
|
|
1320
|
-
console.log(result.output);
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
throw new Error("Usage: agent-relay dev <pack|install|service|smoke> [options]");
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
async function handleMemoryCommand(args: string[]): Promise<void> {
|
|
1328
|
-
const section = args[0];
|
|
1329
|
-
const action = args[1];
|
|
1330
|
-
if (section !== "broker" || action !== "smoke") {
|
|
1331
|
-
throw new Error("Usage: agent-relay memory broker smoke [--relay-url URL] [--token TOKEN] [--agent-id ID] [--scope SCOPE] [--tag TAG] [--no-cleanup] [--json]");
|
|
1332
|
-
}
|
|
1333
|
-
let baseUrl: string | undefined;
|
|
1334
|
-
let token: string | undefined;
|
|
1335
|
-
let agentId: string | undefined;
|
|
1336
|
-
let scope: string | undefined;
|
|
1337
|
-
let tag: string | undefined;
|
|
1338
|
-
let cleanup = true;
|
|
1339
|
-
let json = false;
|
|
1340
|
-
for (let i = 2; i < args.length; i++) {
|
|
1341
|
-
const arg = args[i];
|
|
1342
|
-
if (arg === "--relay-url" && i + 1 < args.length) baseUrl = args[++i];
|
|
1343
|
-
else if (arg === "--token" && i + 1 < args.length) token = args[++i];
|
|
1344
|
-
else if (arg === "--agent-id" && i + 1 < args.length) agentId = args[++i];
|
|
1345
|
-
else if (arg === "--scope" && i + 1 < args.length) scope = args[++i];
|
|
1346
|
-
else if (arg === "--tag" && i + 1 < args.length) tag = args[++i];
|
|
1347
|
-
else if (arg === "--no-cleanup") cleanup = false;
|
|
1348
|
-
else if (arg === "--json") json = true;
|
|
1349
|
-
else throw new Error(`Unknown memory broker smoke option "${arg}"`);
|
|
1350
|
-
}
|
|
1351
|
-
const result = await runMemoryBrokerSmoke({
|
|
1352
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
1353
|
-
...(token ? { token } : {}),
|
|
1354
|
-
...(agentId ? { agentId } : {}),
|
|
1355
|
-
...(scope ? { scope } : {}),
|
|
1356
|
-
...(tag ? { tag } : {}),
|
|
1357
|
-
cleanup,
|
|
1358
|
-
});
|
|
1359
|
-
console.log(json ? JSON.stringify(result, null, 2) : formatMemoryBrokerSmokeResult(result));
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
async function handleDevServiceCommand(args: string[]): Promise<void> {
|
|
1363
|
-
const action = parseDevServiceAction(args[0]);
|
|
1364
|
-
let prefix: string | undefined;
|
|
1365
|
-
let rootDir: string | undefined;
|
|
1366
|
-
let port: number | undefined;
|
|
1367
|
-
let apiPort: number | undefined;
|
|
1368
|
-
let baseDir: string | undefined;
|
|
1369
|
-
let orchestratorId: string | undefined;
|
|
1370
|
-
let token: string | undefined;
|
|
1371
|
-
let start = false;
|
|
1372
|
-
let enable = false;
|
|
1373
|
-
let dryRun = false;
|
|
1374
|
-
let force = false;
|
|
1375
|
-
let yes = false;
|
|
1376
|
-
let json = false;
|
|
1377
|
-
|
|
1378
|
-
for (let i = 1; i < args.length; i++) {
|
|
1379
|
-
const arg = args[i];
|
|
1380
|
-
if (arg === "--prefix" && i + 1 < args.length) prefix = args[++i];
|
|
1381
|
-
else if (arg === "--root" && i + 1 < args.length) rootDir = args[++i];
|
|
1382
|
-
else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
|
|
1383
|
-
else if (arg === "--api-port" && i + 1 < args.length) apiPort = parseInt(args[++i]!, 10);
|
|
1384
|
-
else if (arg === "--base-dir" && i + 1 < args.length) baseDir = args[++i];
|
|
1385
|
-
else if (arg === "--orchestrator-id" && i + 1 < args.length) orchestratorId = args[++i];
|
|
1386
|
-
else if (arg === "--token" && i + 1 < args.length) token = args[++i];
|
|
1387
|
-
else if (arg === "--start") start = true;
|
|
1388
|
-
else if (arg === "--enable") enable = true;
|
|
1389
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
1390
|
-
else if (arg === "--force") force = true;
|
|
1391
|
-
else if (arg === "--yes") yes = true;
|
|
1392
|
-
else if (arg === "--json") json = true;
|
|
1393
|
-
else throw new Error(`Unknown dev service option "${arg}"`);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const root = rootDir ?? defaultDevRoot();
|
|
1397
|
-
const plan = createDevServicePlan({
|
|
1398
|
-
action,
|
|
1399
|
-
...(prefix ? { prefix } : {}),
|
|
1400
|
-
rootDir: root,
|
|
1401
|
-
...(port !== undefined ? { port } : {}),
|
|
1402
|
-
...(apiPort !== undefined ? { apiPort } : {}),
|
|
1403
|
-
...(baseDir ? { baseDir } : {}),
|
|
1404
|
-
...(orchestratorId ? { orchestratorId } : {}),
|
|
1405
|
-
...(token ? { token } : {}),
|
|
1406
|
-
start,
|
|
1407
|
-
enable,
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
if (!dryRun && !json && (action === "install" || action === "uninstall") && !yes) {
|
|
1411
|
-
const ok = await confirm(action === "install" ? `Install dev service profile under ${root}?` : `Uninstall dev service profile under ${root}?`);
|
|
1412
|
-
if (!ok) {
|
|
1413
|
-
console.log("Dev service command cancelled.");
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
const result = await executeDevServicePlan(plan, { dryRun, force });
|
|
1419
|
-
if (json) console.log(JSON.stringify({ plan: result.plan, output: result.output }, null, 2));
|
|
1420
|
-
else console.log(dryRun ? formatDevServicePlan(plan) : result.output);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
function parseDevServiceAction(value: string | undefined): DevServiceAction {
|
|
1424
|
-
const allowed = new Set<DevServiceAction>(["install", "uninstall", "start", "stop", "restart", "status", "logs"]);
|
|
1425
|
-
if (!value || !allowed.has(value as DevServiceAction)) {
|
|
1426
|
-
throw new Error("Usage: agent-relay dev service <install|uninstall|start|stop|restart|status|logs> [options]");
|
|
1427
|
-
}
|
|
1428
|
-
return value as DevServiceAction;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
async function handlePairCommand(args: string[]): Promise<void> {
|
|
1432
|
-
if (!args.length) throw new Error("Usage: agent-relay pair <target|create|status|accept|reject|hangup|send> [options]");
|
|
1433
|
-
const knownActions = new Set(["create", "status", "list", "accept", "reject", "hangup", "disconnect", "send"]);
|
|
1434
|
-
const action = knownActions.has(args[0]!) ? args[0]! : "create";
|
|
1435
|
-
const rest = action === "create" && args[0] !== "create" ? args : args.slice(1);
|
|
1436
|
-
|
|
1437
|
-
if (action === "status" || action === "list") {
|
|
1438
|
-
let agent: string | undefined = await detectAgentId();
|
|
1439
|
-
let status: string | undefined;
|
|
1440
|
-
let json = false;
|
|
1441
|
-
for (let i = 0; i < rest.length; i++) {
|
|
1442
|
-
const arg = rest[i];
|
|
1443
|
-
if (arg === "--agent" && i + 1 < rest.length) agent = rest[++i];
|
|
1444
|
-
else if (arg === "--status" && i + 1 < rest.length) status = rest[++i];
|
|
1445
|
-
else if (arg === "--json") json = true;
|
|
1446
|
-
else throw new Error(`Unknown pair status option "${arg}"`);
|
|
1447
|
-
}
|
|
1448
|
-
const query = new URLSearchParams();
|
|
1449
|
-
if (agent) query.set("agent", agent);
|
|
1450
|
-
if (status) query.set("status", status);
|
|
1451
|
-
const pairs = await apiRequest("GET", `/api/pairs${query.size ? `?${query}` : ""}`);
|
|
1452
|
-
if (json) console.log(JSON.stringify(pairs, null, 2));
|
|
1453
|
-
else console.log(formatPairs(pairs as any[]));
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (action === "create") {
|
|
1458
|
-
const target = rest[0];
|
|
1459
|
-
if (!target || target.startsWith("--")) throw new Error("Usage: agent-relay pair <target> [--from AGENT_ID] [--objective TEXT]");
|
|
1460
|
-
let from = await detectAgentId();
|
|
1461
|
-
let objective: string | undefined;
|
|
1462
|
-
let ttlMs: number | undefined;
|
|
1463
|
-
let json = false;
|
|
1464
|
-
const objectiveParts: string[] = [];
|
|
1465
|
-
for (let i = 1; i < rest.length; i++) {
|
|
1466
|
-
const arg = rest[i];
|
|
1467
|
-
if (arg === "--from" && i + 1 < rest.length) from = rest[++i];
|
|
1468
|
-
else if (arg === "--objective" && i + 1 < rest.length) objective = rest[++i];
|
|
1469
|
-
else if (arg === "--ttl-ms" && i + 1 < rest.length) ttlMs = parseInt(rest[++i]!, 10);
|
|
1470
|
-
else if (arg === "--json") json = true;
|
|
1471
|
-
else objectiveParts.push(arg!);
|
|
1472
|
-
}
|
|
1473
|
-
objective ??= objectiveParts.length ? objectiveParts.join(" ") : undefined;
|
|
1474
|
-
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
1475
|
-
const result = await apiRequest("POST", "/api/pairs", { from, target, objective, ttlMs });
|
|
1476
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1477
|
-
else {
|
|
1478
|
-
const pair = (result as any).pair;
|
|
1479
|
-
console.log(`Pair invite ${pair.id} sent: ${pair.requesterId} -> ${pair.targetId}`);
|
|
1480
|
-
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
if (action === "accept" || action === "reject" || action === "hangup" || action === "disconnect") {
|
|
1485
|
-
const pairId = rest[0];
|
|
1486
|
-
let agentId = await detectAgentId();
|
|
1487
|
-
let reason: string | undefined;
|
|
1488
|
-
let json = false;
|
|
1489
|
-
let startIndex = 0;
|
|
1490
|
-
if (pairId && !pairId.startsWith("--")) startIndex = 1;
|
|
1491
|
-
for (let i = startIndex; i < rest.length; i++) {
|
|
1492
|
-
const arg = rest[i];
|
|
1493
|
-
if (arg === "--agent" && i + 1 < rest.length) agentId = rest[++i];
|
|
1494
|
-
else if (arg === "--reason" && i + 1 < rest.length) reason = rest[++i];
|
|
1495
|
-
else if (arg === "--json") json = true;
|
|
1496
|
-
else throw new Error(`Unknown pair ${action} option "${arg}"`);
|
|
1497
|
-
}
|
|
1498
|
-
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
1499
|
-
const resolvedPairId = pairId && !pairId.startsWith("--") ? pairId : await detectActivePairId(agentId);
|
|
1500
|
-
if (!resolvedPairId) throw new Error(`Usage: agent-relay pair ${action} PAIR_ID --agent AGENT_ID`);
|
|
1501
|
-
const endpoint = action === "disconnect" ? "hangup" : action;
|
|
1502
|
-
const pair = await apiRequest("POST", `/api/pairs/${encodeURIComponent(resolvedPairId)}/${endpoint}`, { agentId, reason });
|
|
1503
|
-
if (json) console.log(JSON.stringify(pair, null, 2));
|
|
1504
|
-
else console.log(`Pair ${resolvedPairId}: ${(pair as any).status}`);
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
if (action === "send") {
|
|
1509
|
-
const pairId = rest[0];
|
|
1510
|
-
if (!pairId || pairId.startsWith("--")) throw new Error("Usage: agent-relay pair send PAIR_ID --from AGENT_ID --body TEXT");
|
|
1511
|
-
let from = await detectAgentId();
|
|
1512
|
-
let body: string | undefined;
|
|
1513
|
-
let subject: string | undefined;
|
|
1514
|
-
let json = false;
|
|
1515
|
-
for (let i = 1; i < rest.length; i++) {
|
|
1516
|
-
const arg = rest[i];
|
|
1517
|
-
if (arg === "--from" && i + 1 < rest.length) from = rest[++i];
|
|
1518
|
-
else if (arg === "--body" && i + 1 < rest.length) body = rest[++i];
|
|
1519
|
-
else if (arg === "--subject" && i + 1 < rest.length) subject = rest[++i];
|
|
1520
|
-
else if (arg === "--json") json = true;
|
|
1521
|
-
else throw new Error(`Unknown pair send option "${arg}"`);
|
|
1522
|
-
}
|
|
1523
|
-
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
1524
|
-
if (!body) throw new Error("--body TEXT required");
|
|
1525
|
-
const result = await apiRequest("POST", `/api/pairs/${encodeURIComponent(pairId)}/messages`, { from, body, subject });
|
|
1526
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
1527
|
-
else console.log(`Pair message sent: ${(result as any).message.id}`);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// The agent's own isolated-workspace id, published in AGENT_RELAY_WORKSPACE_JSON
|
|
1533
|
-
// by the orchestrator at spawn. Undefined for shared-workspace / non-managed agents.
|
|
1534
|
-
function currentWorkspaceId(): string | undefined {
|
|
1535
|
-
const json = process.env.AGENT_RELAY_WORKSPACE_JSON;
|
|
1536
|
-
if (!json) return undefined;
|
|
1537
|
-
try {
|
|
1538
|
-
const parsed = JSON.parse(json) as { id?: string };
|
|
1539
|
-
return typeof parsed.id === "string" && parsed.id ? parsed.id : undefined;
|
|
1540
|
-
} catch {
|
|
1541
|
-
return undefined;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function formatWorkspaceStatus(ws: any, extra?: { guidance?: WorkspacePhaseView; landed?: string | null }): string {
|
|
1546
|
-
// Render the directive projection so the agent gets "what does this mean / what
|
|
1547
|
-
// do I do next" inline, not a bare enum it has to decode (#235). Computed
|
|
1548
|
-
// client-side from the record (the projection is pure) unless the wait response
|
|
1549
|
-
// already carried it.
|
|
1550
|
-
const guidance = extra?.guidance ?? describeWorkspacePhase(ws);
|
|
1551
|
-
const lines = [
|
|
1552
|
-
`Workspace ${ws.id}`,
|
|
1553
|
-
` status: ${ws.status} (${guidance.phase}${guidance.actionNeeded ? "" : " — no action needed"})`,
|
|
1554
|
-
` branch: ${ws.branch ?? "(none)"}`,
|
|
1555
|
-
` base: ${ws.baseRef ?? "(none)"}`,
|
|
1556
|
-
` worktree: ${ws.worktreePath ?? "(none)"}`,
|
|
1557
|
-
"",
|
|
1558
|
-
` ${guidance.headline}`,
|
|
1559
|
-
` ${guidance.hint}`,
|
|
1560
|
-
];
|
|
1561
|
-
if (guidance.blockers.length) {
|
|
1562
|
-
lines.push("", " Blockers:");
|
|
1563
|
-
for (const b of guidance.blockers) lines.push(` - ${b}`);
|
|
1564
|
-
}
|
|
1565
|
-
if (guidance.nextActions.length) {
|
|
1566
|
-
lines.push("", " Next:");
|
|
1567
|
-
for (const a of guidance.nextActions) lines.push(` - ${a.cli ?? a.tool} — ${a.when}`);
|
|
1568
|
-
}
|
|
1569
|
-
if (extra?.landed) lines.push("", ` ${extra.landed}`);
|
|
1570
|
-
return lines.join("\n");
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// Poll a command to a terminal state (succeeded/failed). Returns undefined on
|
|
1574
|
-
// timeout so the caller can degrade to "dispatched, check later".
|
|
1575
|
-
async function pollCommand(id: string, timeoutMs: number): Promise<{ status?: string; result?: unknown; error?: string } | undefined> {
|
|
1576
|
-
const deadline = Date.now() + timeoutMs;
|
|
1577
|
-
while (Date.now() < deadline) {
|
|
1578
|
-
const cmd = await apiRequest("GET", `/api/commands/${encodeURIComponent(id)}`) as { status?: string; result?: unknown; error?: string };
|
|
1579
|
-
if (cmd.status === "succeeded" || cmd.status === "failed") return cmd;
|
|
1580
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1581
|
-
}
|
|
1582
|
-
return undefined;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function formatDepsRefresh(result: WorkspaceDepsRefreshResult, checkOnly: boolean): string {
|
|
1586
|
-
if (result.error && (!result.dirs || result.dirs.length === 0)) return `Deps ${checkOnly ? "check" : "refresh"}: ${result.error}`;
|
|
1587
|
-
const lines: string[] = [];
|
|
1588
|
-
for (const d of result.dirs) {
|
|
1589
|
-
const icon = d.status === "installed" ? "↻" : d.status === "stale" ? "✗" : d.status === "failed" ? "!" : "✓";
|
|
1590
|
-
const detail = d.status === "ok" ? "up to date"
|
|
1591
|
-
: d.status === "installed" ? `reinstalled${d.wasSymlink ? " (was symlinked)" : ""}`
|
|
1592
|
-
: d.status === "stale" ? `stale — missing ${d.missing?.join(", ") ?? "?"}`
|
|
1593
|
-
: `failed — ${d.error ?? "unknown"}`;
|
|
1594
|
-
lines.push(` ${icon} ${d.dir}: ${detail}`);
|
|
1595
|
-
}
|
|
1596
|
-
const header = checkOnly
|
|
1597
|
-
? (result.stale ? "Deps check: stale dirs found — run `agent-relay workspace deps` to refresh" : "Deps check: all dirs up to date")
|
|
1598
|
-
: (result.refreshed ? "Deps refreshed" : result.error ? "Deps refresh hit errors" : "Deps already up to date");
|
|
1599
|
-
return [header, ...lines].join("\n");
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// Self-service workspace lifecycle for agents in isolated worktrees (#205) plus
|
|
1603
|
-
// steward coordination (#208).
|
|
1604
|
-
// status — read your workspace row ready — hand off for review/landing
|
|
1605
|
-
// land — request a base merge (operator) list — all workspaces
|
|
1606
|
-
// diagnostics — joined briefing + recommended action
|
|
1607
|
-
// claim/release — TTL'd steward lease auto-merge yields to
|
|
1608
|
-
// cleanup-stale — guarded batch cleanup of stale worktrees (dry-run by default)
|
|
1609
|
-
async function handleWorkspaceCommand(args: string[]): Promise<void> {
|
|
1610
|
-
const action = args[0];
|
|
1611
|
-
const valid = new Set(["status", "ready", "land", "list", "diagnostics", "diag", "claim", "release", "cleanup-stale", "deps"]);
|
|
1612
|
-
if (action === "--help" || action === "-h" || action === "help") {
|
|
1613
|
-
console.log(WORKSPACE_USAGE);
|
|
1614
|
-
return;
|
|
1615
|
-
}
|
|
1616
|
-
if (!action || !valid.has(action)) {
|
|
1617
|
-
throw new Error(WORKSPACE_USAGE);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
let id = currentWorkspaceId(), idExplicit = false; // idExplicit: --id was passed, not the ambient default (#307)
|
|
1621
|
-
let strategy: string | undefined;
|
|
1622
|
-
let purpose: string | undefined;
|
|
1623
|
-
let repo: string | undefined;
|
|
1624
|
-
let execute = false;
|
|
1625
|
-
let check = false;
|
|
1626
|
-
let json = false;
|
|
1627
|
-
let wait = false;
|
|
1628
|
-
let timeoutSeconds: number | undefined;
|
|
1629
|
-
for (let i = 1; i < args.length; i++) {
|
|
1630
|
-
const arg = args[i];
|
|
1631
|
-
if (arg === "--id" && i + 1 < args.length) { id = args[++i]; idExplicit = true; }
|
|
1632
|
-
else if (arg === "--strategy" && i + 1 < args.length) strategy = args[++i];
|
|
1633
|
-
else if (arg === "--purpose" && i + 1 < args.length) purpose = args[++i];
|
|
1634
|
-
else if (arg === "--repo" && i + 1 < args.length) repo = args[++i];
|
|
1635
|
-
else if (arg === "--execute") execute = true;
|
|
1636
|
-
else if (arg === "--check") check = true;
|
|
1637
|
-
else if (arg === "--refresh") check = false; // explicit no-op default for clarity
|
|
1638
|
-
else if (arg === "--wait") wait = true;
|
|
1639
|
-
else if (arg === "--timeout" && i + 1 < args.length) {
|
|
1640
|
-
const parsed = Number.parseInt(args[++i]!, 10);
|
|
1641
|
-
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error("--timeout must be a positive number of seconds");
|
|
1642
|
-
timeoutSeconds = parsed;
|
|
1643
|
-
}
|
|
1644
|
-
else if (arg === "--json") json = true;
|
|
1645
|
-
else throw new Error(`Unknown workspace option "${arg}".`);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
if (action === "list") {
|
|
1649
|
-
console.log(JSON.stringify(await apiRequest("GET", "/api/workspaces"), null, 2));
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (action === "cleanup-stale") {
|
|
1654
|
-
const result = await apiRequest("POST", "/api/workspaces/actions/cleanup-stale", { repoRoot: repo, dryRun: !execute, ...(idExplicit && id ? { workspaceId: id } : {}) });
|
|
1655
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
if (!id) throw new Error("No current workspace detected (AGENT_RELAY_WORKSPACE_JSON unset). Pass --id WORKSPACE_ID — only isolated-workspace agents have one.");
|
|
1660
|
-
|
|
1661
|
-
if (action === "status") {
|
|
1662
|
-
// --wait long-polls via the action endpoint (server blocks until the status
|
|
1663
|
-
// changes — the blessed way to wait for an auto-merge to land, #235), and the
|
|
1664
|
-
// response carries the directive projection + land receipt. Plain status is a
|
|
1665
|
-
// bare GET; the projection is computed client-side for rendering.
|
|
1666
|
-
if (wait) {
|
|
1667
|
-
const res = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, {
|
|
1668
|
-
action: "status",
|
|
1669
|
-
wait: true,
|
|
1670
|
-
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
|
1671
|
-
}) as { workspace?: any; guidance?: WorkspacePhaseView; landed?: string | null };
|
|
1672
|
-
if (json) { console.log(JSON.stringify(res, null, 2)); return; }
|
|
1673
|
-
console.log(formatWorkspaceStatus(res.workspace ?? res, { guidance: res.guidance, landed: res.landed }));
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
const ws = await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}`);
|
|
1677
|
-
if (json) console.log(JSON.stringify(ws, null, 2));
|
|
1678
|
-
else console.log(formatWorkspaceStatus(ws));
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
if (action === "diagnostics" || action === "diag") {
|
|
1683
|
-
console.log(JSON.stringify(await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diagnostics`), null, 2));
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Refresh (or --check) deps the shared symlinked node_modules has gone stale on
|
|
1688
|
-
// (#51). Emits a host command; poll it to a terminal state so the agent gets a
|
|
1689
|
-
// synchronous result and knows when to re-run typecheck.
|
|
1690
|
-
if (action === "deps") {
|
|
1691
|
-
const from = await detectAgentId();
|
|
1692
|
-
const res = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, { action: "deps-refresh", agentId: from, checkOnly: check }) as { command?: { id?: string } };
|
|
1693
|
-
const commandId = res.command?.id;
|
|
1694
|
-
const settled = commandId ? await pollCommand(commandId, 180_000) : undefined;
|
|
1695
|
-
const result = (settled?.result ?? null) as WorkspaceDepsRefreshResult | null;
|
|
1696
|
-
if (json) {
|
|
1697
|
-
console.log(JSON.stringify(settled ?? res, null, 2));
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
if (settled?.status === "failed") {
|
|
1701
|
-
console.error(`Deps ${check ? "check" : "refresh"} failed: ${settled.error ?? "unknown error"}`);
|
|
1702
|
-
process.exitCode = 1;
|
|
1703
|
-
return;
|
|
1704
|
-
}
|
|
1705
|
-
if (!result) {
|
|
1706
|
-
console.log(`Deps ${check ? "check" : "refresh"} dispatched (command ${commandId ?? "?"}) — host did not report back in time. Check \`agent-relay workspace deps --json\`.`);
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
console.log(formatDepsRefresh(result, check));
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
const from = await detectAgentId();
|
|
1714
|
-
const actionBody: Record<string, unknown> =
|
|
1715
|
-
action === "ready" ? { action: "request-review", agentId: from }
|
|
1716
|
-
: action === "claim" ? { action: "claim", agentId: from, purpose }
|
|
1717
|
-
: action === "release" ? { action: "release-claim", agentId: from }
|
|
1718
|
-
: { action: "merge", agentId: from, ...(strategy ? { strategy } : {}) };
|
|
1719
|
-
const result = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, actionBody);
|
|
1720
|
-
if (json) {
|
|
1721
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
if (action === "ready") {
|
|
1725
|
-
// Print the whole contract up front so the agent isn't left decoding status
|
|
1726
|
-
// enums over the next minutes (#235). `result.workspace` is the post-ready row.
|
|
1727
|
-
const ws = (result as { workspace?: any }).workspace ?? { status: "review_requested" };
|
|
1728
|
-
console.log(`Workspace ${id} marked ready.\n\n${readyContract(ws)}`);
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
console.log(
|
|
1732
|
-
action === "claim" ? `Workspace ${id} claimed${purpose ? ` (${purpose})` : ""} — auto-merge will yield until released or the claim expires.`
|
|
1733
|
-
: action === "release" ? `Workspace ${id} claim released.`
|
|
1734
|
-
: `Workspace ${id} merge requested (${strategy ?? "auto"}).`,
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Steward briefing commands (#208): queue of workspaces needing attention, a
|
|
1739
|
-
// per-workspace diagnostics inspection, and a check-command suggestion.
|
|
1740
|
-
async function handleStewardCommand(args: string[]): Promise<void> {
|
|
1741
|
-
const action = args[0];
|
|
1742
|
-
if (!action || !["queue", "inspect", "checks"].includes(action)) {
|
|
1743
|
-
throw new Error("Usage: agent-relay steward <queue|inspect|checks> [WORKSPACE_ID] [--repo PATH] [--json]");
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
let repo: string | undefined;
|
|
1747
|
-
let json = false;
|
|
1748
|
-
const positional: string[] = [];
|
|
1749
|
-
for (let i = 1; i < args.length; i++) {
|
|
1750
|
-
const arg = args[i];
|
|
1751
|
-
if (arg === "--repo" && i + 1 < args.length) repo = args[++i];
|
|
1752
|
-
else if (arg === "--json") json = true;
|
|
1753
|
-
else if (!arg!.startsWith("--")) positional.push(arg!);
|
|
1754
|
-
else throw new Error(`Unknown steward option "${arg}".`);
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
if (action === "queue") {
|
|
1758
|
-
const all = await apiRequest("GET", "/api/workspaces") as any[];
|
|
1759
|
-
const attention = new Set(["conflict", "review_requested", "merge_planned"]);
|
|
1760
|
-
const queue = all.filter((ws) => attention.has(ws.status) && (!repo || ws.repoRoot === repo));
|
|
1761
|
-
if (json) { console.log(JSON.stringify(queue, null, 2)); return; }
|
|
1762
|
-
if (!queue.length) { console.log("Steward queue empty — no workspaces awaiting review, merge, or conflict resolution."); return; }
|
|
1763
|
-
for (const ws of queue) console.log(`${ws.status.padEnd(16)} ${ws.branch ?? ws.id} (${ws.repoRoot})`);
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
const id = positional[0];
|
|
1768
|
-
if (!id) throw new Error(`Usage: agent-relay steward ${action} WORKSPACE_ID [--json]`);
|
|
1769
|
-
|
|
1770
|
-
if (action === "inspect") {
|
|
1771
|
-
console.log(JSON.stringify(await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diagnostics`), null, 2));
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
// checks: suggest validation commands from the workspace's changed files.
|
|
1776
|
-
const diff = await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diff?patch=0`) as any;
|
|
1777
|
-
const files: string[] = Array.isArray(diff?.files) ? diff.files.map((f: any) => f.path) : [];
|
|
1778
|
-
const checks = suggestStewardChecks(files);
|
|
1779
|
-
console.log(JSON.stringify({ workspaceId: id, changedFiles: files.length, checks }, null, 2));
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
// Heuristic check suggestions from changed file paths. Repo-agnostic defaults a
|
|
1783
|
-
// steward can refine; cheaper than re-deriving from project docs every run.
|
|
1784
|
-
function suggestStewardChecks(files: string[]): Array<{ command: string; reason: string }> {
|
|
1785
|
-
const checks: Array<{ command: string; reason: string }> = [];
|
|
1786
|
-
const has = (re: RegExp) => files.some((f) => re.test(f));
|
|
1787
|
-
if (has(/\.(ts|tsx|mts|cts)$/)) checks.push({ command: "bun run typecheck", reason: "TypeScript files changed" });
|
|
1788
|
-
if (has(/\.test\.|(^|\/)tests?\//)) checks.push({ command: "bun test", reason: "test files changed" });
|
|
1789
|
-
else if (files.length) checks.push({ command: "bun test", reason: "repo default" });
|
|
1790
|
-
if (has(/(^|\/)dashboard\//)) checks.push({ command: "bun run build:dashboard", reason: "dashboard sources changed" });
|
|
1791
|
-
return checks;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
async function handleMessageCommand(args: string[], defaults: { claimable?: boolean } = {}): Promise<void> {
|
|
1795
|
-
const target = args[0];
|
|
1796
|
-
if (!target || target.startsWith("--")) {
|
|
1797
|
-
throw new Error("Usage: agent-relay message <target> <body> [--from AGENT_ID] [--subject TEXT] [--channel NAME] [--reply-to ID] [--claimable]");
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
let from = await detectAgentId();
|
|
1801
|
-
let subject: string | undefined;
|
|
1802
|
-
let channel: string | undefined;
|
|
1803
|
-
let replyTo: number | undefined;
|
|
1804
|
-
let idempotencyKey: string | undefined;
|
|
1805
|
-
let json = false;
|
|
1806
|
-
let claimable = defaults.claimable ?? false;
|
|
1807
|
-
const bodyParts: string[] = [];
|
|
1808
|
-
|
|
1809
|
-
for (let i = 1; i < args.length; i++) {
|
|
1810
|
-
const arg = args[i];
|
|
1811
|
-
if (arg === "--from" && i + 1 < args.length) from = args[++i];
|
|
1812
|
-
else if (arg === "--subject" && i + 1 < args.length) subject = args[++i];
|
|
1813
|
-
else if (arg === "--channel" && i + 1 < args.length) channel = args[++i];
|
|
1814
|
-
else if (arg === "--reply-to" && i + 1 < args.length) {
|
|
1815
|
-
const parsed = Number.parseInt(args[++i]!, 10);
|
|
1816
|
-
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error("--reply-to must be a positive message id");
|
|
1817
|
-
replyTo = parsed;
|
|
1818
|
-
} else if (arg === "--idempotency-key" && i + 1 < args.length) idempotencyKey = args[++i];
|
|
1819
|
-
else if (arg === "--claimable") claimable = true;
|
|
1820
|
-
else if (arg === "--json") json = true;
|
|
1821
|
-
else bodyParts.push(arg!);
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
const body = bodyParts.join(" ").trim();
|
|
1825
|
-
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
1826
|
-
if (!body) throw new Error("Message body required.");
|
|
1827
|
-
|
|
1828
|
-
const message = await apiRequest("POST", "/api/messages", {
|
|
1829
|
-
from,
|
|
1830
|
-
to: target,
|
|
1831
|
-
kind: claimable ? "task" : "chat",
|
|
1832
|
-
subject,
|
|
1833
|
-
channel,
|
|
1834
|
-
body,
|
|
1835
|
-
replyTo,
|
|
1836
|
-
claimable,
|
|
1837
|
-
payload: claimable ? { title: subject || "Claimable task" } : undefined,
|
|
1838
|
-
idempotencyKey,
|
|
1839
|
-
});
|
|
1840
|
-
if (json) console.log(JSON.stringify(message, null, 2));
|
|
1841
|
-
else console.log(`${claimable ? "Claimable message" : "Message"} sent: ${(message as any).id} -> ${target}`);
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
async function handleGetMessageCommand(args: string[]): Promise<void> {
|
|
1845
|
-
const msgIdRaw = args[0];
|
|
1846
|
-
if (!msgIdRaw || msgIdRaw.startsWith("--")) {
|
|
1847
|
-
throw new Error("Usage: agent-relay get-message <messageId> [--json] [--body]");
|
|
1848
|
-
}
|
|
1849
|
-
const messageId = Number.parseInt(msgIdRaw, 10);
|
|
1850
|
-
if (!Number.isFinite(messageId) || messageId <= 0) throw new Error("messageId must be a positive integer");
|
|
1851
|
-
|
|
1852
|
-
let json = false;
|
|
1853
|
-
let bodyOnly = false;
|
|
1854
|
-
for (let i = 1; i < args.length; i++) {
|
|
1855
|
-
const arg = args[i];
|
|
1856
|
-
if (arg === "--json") json = true;
|
|
1857
|
-
else if (arg === "--body") bodyOnly = true;
|
|
1858
|
-
else throw new Error(`Unknown get-message option "${arg}".`);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
const [message, artifacts, thread] = await Promise.all([
|
|
1862
|
-
apiRequest("GET", `/api/messages/${messageId}`),
|
|
1863
|
-
apiRequest("GET", `/api/messages/${messageId}/artifacts`).catch(() => []),
|
|
1864
|
-
apiRequest("GET", `/api/messages/${messageId}/thread`).catch(() => []),
|
|
1865
|
-
]);
|
|
1866
|
-
if (bodyOnly) {
|
|
1867
|
-
console.log(String((message as any).body ?? ""));
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
const msg = message as any;
|
|
1872
|
-
const attachments = Array.isArray(msg.payload?.attachments)
|
|
1873
|
-
? msg.payload.attachments
|
|
1874
|
-
: Array.isArray(msg.attachments)
|
|
1875
|
-
? msg.attachments
|
|
1876
|
-
: [];
|
|
1877
|
-
const payload = {
|
|
1878
|
-
id: msg.id,
|
|
1879
|
-
from: msg.from,
|
|
1880
|
-
to: msg.to,
|
|
1881
|
-
kind: msg.kind,
|
|
1882
|
-
body: msg.body,
|
|
1883
|
-
subject: msg.subject,
|
|
1884
|
-
channel: msg.channel,
|
|
1885
|
-
replyToId: msg.replyTo,
|
|
1886
|
-
threadId: msg.threadId,
|
|
1887
|
-
createdAt: msg.createdAt,
|
|
1888
|
-
payload: msg.payload,
|
|
1889
|
-
meta: msg.meta,
|
|
1890
|
-
attachments,
|
|
1891
|
-
message,
|
|
1892
|
-
artifacts,
|
|
1893
|
-
thread,
|
|
1894
|
-
replyTo: msg.replyTo ? (thread as any[]).find((item) => item.id === msg.replyTo) ?? null : null,
|
|
1895
|
-
};
|
|
1896
|
-
if (json) {
|
|
1897
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
1898
|
-
return;
|
|
1899
|
-
}
|
|
1900
|
-
console.log(formatMessageDetails(payload));
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
async function handleReplyCommand(args: string[]): Promise<void> {
|
|
1904
|
-
const msgIdRaw = args[0];
|
|
1905
|
-
if (!msgIdRaw || msgIdRaw.startsWith("--")) {
|
|
1906
|
-
throw new Error("Usage: agent-relay /reply <messageId> <body|--stdin|--body-file PATH> [--from AGENT_ID] [--subject TEXT] [--format text|markdown|markdownv2] [--json]");
|
|
1907
|
-
}
|
|
1908
|
-
const replyTo = Number.parseInt(msgIdRaw, 10);
|
|
1909
|
-
if (!Number.isFinite(replyTo) || replyTo <= 0) throw new Error("messageId must be a positive integer");
|
|
1910
|
-
|
|
1911
|
-
let from = await detectAgentId();
|
|
1912
|
-
let subject: string | undefined;
|
|
1913
|
-
let format: string | undefined;
|
|
1914
|
-
let json = false;
|
|
1915
|
-
let stdinBody = false;
|
|
1916
|
-
let bodyFile: string | undefined;
|
|
1917
|
-
const bodyParts: string[] = [];
|
|
1918
|
-
|
|
1919
|
-
for (let i = 1; i < args.length; i++) {
|
|
1920
|
-
const arg = args[i];
|
|
1921
|
-
if (arg === "--from" && i + 1 < args.length) from = args[++i];
|
|
1922
|
-
else if (arg === "--subject" && i + 1 < args.length) subject = args[++i];
|
|
1923
|
-
else if (arg === "--stdin") stdinBody = true;
|
|
1924
|
-
else if ((arg === "--body-file" || arg === "--file") && i + 1 < args.length) bodyFile = args[++i];
|
|
1925
|
-
else if (arg === "--format" && i + 1 < args.length) {
|
|
1926
|
-
const parsed = parseReplyFormat(args[++i]!);
|
|
1927
|
-
if (!parsed) throw new Error("--format must be text, markdown, or markdownv2");
|
|
1928
|
-
format = parsed;
|
|
1929
|
-
}
|
|
1930
|
-
else if (arg === "--json") json = true;
|
|
1931
|
-
else bodyParts.push(arg!);
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
const body = (await resolveBodyInput({ bodyParts, stdinBody, bodyFile })).trim();
|
|
1935
|
-
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
1936
|
-
if (!body) throw new Error("Reply body required.");
|
|
1937
|
-
|
|
1938
|
-
const prepared = await prepareReplyBody({ body, from, replyTo, format, subject });
|
|
1939
|
-
const message = await apiRequest("POST", "/api/messages", {
|
|
1940
|
-
from,
|
|
1941
|
-
body: prepared.body,
|
|
1942
|
-
subject: prepared.subject,
|
|
1943
|
-
replyTo,
|
|
1944
|
-
attachments: prepared.attachments,
|
|
1945
|
-
payload: prepared.payload,
|
|
1946
|
-
});
|
|
1947
|
-
const msg = message as any;
|
|
1948
|
-
if (json) console.log(JSON.stringify(msg, null, 2));
|
|
1949
|
-
else console.log(`Reply sent: ${msg.id} -> ${msg.to} (reply to #${replyTo})`);
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
async function handleReactCommand(args: string[]): Promise<void> {
|
|
1953
|
-
const msgIdRaw = args[0];
|
|
1954
|
-
const emoji = args[1];
|
|
1955
|
-
if (!msgIdRaw || msgIdRaw.startsWith("--") || !emoji || emoji.startsWith("--")) {
|
|
1956
|
-
throw new Error("Usage: agent-relay /react <messageId> <emoji> [--from AGENT_ID] [--remove] [--json]");
|
|
1957
|
-
}
|
|
1958
|
-
const messageId = Number.parseInt(msgIdRaw, 10);
|
|
1959
|
-
if (!Number.isFinite(messageId) || messageId <= 0) throw new Error("messageId must be a positive integer");
|
|
1960
|
-
|
|
1961
|
-
let actorId = await detectAgentId();
|
|
1962
|
-
let action: "add" | "remove" = "add";
|
|
1963
|
-
let json = false;
|
|
1964
|
-
for (let i = 2; i < args.length; i++) {
|
|
1965
|
-
const arg = args[i];
|
|
1966
|
-
if (arg === "--from" && i + 1 < args.length) actorId = args[++i];
|
|
1967
|
-
else if (arg === "--remove") action = "remove";
|
|
1968
|
-
else if (arg === "--json") json = true;
|
|
1969
|
-
else throw new Error(`Unknown react option "${arg}"`);
|
|
1970
|
-
}
|
|
1971
|
-
if (!actorId) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
1972
|
-
|
|
1973
|
-
const message = await apiRequest("POST", `/api/messages/${messageId}/reactions`, {
|
|
1974
|
-
actorId,
|
|
1975
|
-
emoji,
|
|
1976
|
-
action,
|
|
1977
|
-
});
|
|
1978
|
-
if (json) console.log(JSON.stringify(message, null, 2));
|
|
1979
|
-
else console.log(`${action === "remove" ? "Reaction removed" : "Reaction sent"}: ${emoji} on #${messageId}`);
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// Insights #185: capture the agent's end-of-session self-view as a bounded, structured
|
|
1983
|
-
// artifact (epic #183, docs/self-improvement.md). Manual rail in v0 — the agent or
|
|
1984
|
-
// operator runs this; the auto-trigger is chosen later on real data. The relay drops it
|
|
1985
|
-
// (409) if Insights or the introspection feature is toggled off.
|
|
1986
|
-
const INTROSPECT_FIELD_MAX = 600;
|
|
1987
|
-
|
|
1988
|
-
async function handleIntrospectCommand(args: string[]): Promise<void> {
|
|
1989
|
-
let from = await detectAgentId();
|
|
1990
|
-
let project: string | undefined;
|
|
1991
|
-
let sessionId: string | undefined;
|
|
1992
|
-
let thin: string | undefined;
|
|
1993
|
-
let workedAround: string | undefined;
|
|
1994
|
-
let wouldHaveHelped: string | undefined;
|
|
1995
|
-
let stdin = false;
|
|
1996
|
-
let json = false;
|
|
1997
|
-
|
|
1998
|
-
for (let i = 0; i < args.length; i++) {
|
|
1999
|
-
const arg = args[i];
|
|
2000
|
-
if (arg === "--from" && i + 1 < args.length) from = args[++i];
|
|
2001
|
-
else if (arg === "--project" && i + 1 < args.length) project = args[++i];
|
|
2002
|
-
else if (arg === "--session" && i + 1 < args.length) sessionId = args[++i];
|
|
2003
|
-
else if (arg === "--thin" && i + 1 < args.length) thin = args[++i];
|
|
2004
|
-
else if (arg === "--worked-around" && i + 1 < args.length) workedAround = args[++i];
|
|
2005
|
-
else if (arg === "--would-have-helped" && i + 1 < args.length) wouldHaveHelped = args[++i];
|
|
2006
|
-
else if (arg === "--stdin") stdin = true;
|
|
2007
|
-
else if (arg === "--json") json = true;
|
|
2008
|
-
else throw new Error(`Unknown introspect option "${arg}"`);
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
// --stdin reads a JSON object { thin, workedAround, wouldHaveHelped } — lets agents
|
|
2012
|
-
// pipe a file without shell-quoting three multi-line fields.
|
|
2013
|
-
if (stdin) {
|
|
2014
|
-
const raw = (await readStdin()).trim();
|
|
2015
|
-
if (!raw) throw new Error("--stdin given but no input received.");
|
|
2016
|
-
let parsed: unknown;
|
|
2017
|
-
try {
|
|
2018
|
-
parsed = JSON.parse(raw);
|
|
2019
|
-
} catch {
|
|
2020
|
-
throw new Error('--stdin must be JSON: { "thin": "...", "workedAround": "...", "wouldHaveHelped": "..." }');
|
|
2021
|
-
}
|
|
2022
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--stdin JSON must be an object");
|
|
2023
|
-
const obj = parsed as Record<string, unknown>;
|
|
2024
|
-
if (typeof obj.thin === "string") thin = obj.thin;
|
|
2025
|
-
if (typeof obj.workedAround === "string") workedAround = obj.workedAround;
|
|
2026
|
-
if (typeof obj.wouldHaveHelped === "string") wouldHaveHelped = obj.wouldHaveHelped;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
const clip = (v: string | undefined): string => (v ?? "").trim().slice(0, INTROSPECT_FIELD_MAX);
|
|
2030
|
-
const value = { thin: clip(thin), workedAround: clip(workedAround), wouldHaveHelped: clip(wouldHaveHelped) };
|
|
2031
|
-
if (!value.thin && !value.workedAround && !value.wouldHaveHelped) {
|
|
2032
|
-
throw new Error(
|
|
2033
|
-
"Usage: agent-relay /introspect [--thin TEXT] [--worked-around TEXT] [--would-have-helped TEXT] [--stdin] [--from AGENT_ID]\n" +
|
|
2034
|
-
"At least one field is required. The three fields: what context was thin, what you worked around, what would've helped.",
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
2038
|
-
|
|
2039
|
-
const observation = await apiRequest("POST", "/api/insights/observations", {
|
|
2040
|
-
sessionId: sessionId || process.env.AGENT_RELAY_PROVIDER_SESSION_ID || `manual-${from}`,
|
|
2041
|
-
project: project || process.env.AGENT_RELAY_PROJECT || process.cwd().split("/").filter(Boolean).at(-1) || process.cwd(),
|
|
2042
|
-
agentId: from,
|
|
2043
|
-
signal: "introspection",
|
|
2044
|
-
source: "agent",
|
|
2045
|
-
value,
|
|
2046
|
-
});
|
|
2047
|
-
if (json) console.log(JSON.stringify(observation, null, 2));
|
|
2048
|
-
else console.log("Introspection recorded.");
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function parseReplyFormat(value: string): "text" | "markdown" | "markdownv2" | undefined {
|
|
2052
|
-
const normalized = value.trim().toLowerCase();
|
|
2053
|
-
if (normalized === "text" || normalized === "markdown" || normalized === "markdownv2") return normalized;
|
|
2054
|
-
return undefined;
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
const MAX_DIRECT_REPLY_BODY_BYTES = Math.floor(MAX_BODY_BYTES * 0.75);
|
|
2058
|
-
|
|
2059
|
-
async function resolveBodyInput(options: {
|
|
2060
|
-
bodyParts: string[];
|
|
2061
|
-
stdinBody: boolean;
|
|
2062
|
-
bodyFile?: string;
|
|
2063
|
-
}): Promise<string> {
|
|
2064
|
-
const sources = [
|
|
2065
|
-
options.bodyParts.length > 0 ? "argv" : "",
|
|
2066
|
-
options.stdinBody ? "stdin" : "",
|
|
2067
|
-
options.bodyFile ? "body file" : "",
|
|
2068
|
-
].filter(Boolean);
|
|
2069
|
-
if (sources.length > 1) throw new Error("Reply body must come from exactly one source: arguments, --stdin, or --body-file.");
|
|
2070
|
-
if (options.stdinBody) return readStdin();
|
|
2071
|
-
if (options.bodyFile) return readFileSync(resolve(options.bodyFile), "utf8");
|
|
2072
|
-
return options.bodyParts.join(" ");
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
async function prepareReplyBody(options: {
|
|
2076
|
-
body: string;
|
|
2077
|
-
from: string;
|
|
2078
|
-
replyTo: number;
|
|
2079
|
-
format?: string;
|
|
2080
|
-
subject?: string;
|
|
2081
|
-
}): Promise<{
|
|
2082
|
-
body: string;
|
|
2083
|
-
subject?: string;
|
|
2084
|
-
attachments?: Array<Record<string, unknown>>;
|
|
2085
|
-
payload?: Record<string, unknown>;
|
|
2086
|
-
}> {
|
|
2087
|
-
const payload: Record<string, unknown> = options.format ? { message: { format: options.format } } : {};
|
|
2088
|
-
if (encodedLength(options.body) <= MAX_DIRECT_REPLY_BODY_BYTES) {
|
|
2089
|
-
return {
|
|
2090
|
-
body: options.body,
|
|
2091
|
-
subject: options.subject,
|
|
2092
|
-
payload: Object.keys(payload).length ? payload : undefined,
|
|
2093
|
-
};
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
const filename = `relay-reply-${options.replyTo}-${Date.now()}.${options.format === "markdown" ? "md" : "txt"}`;
|
|
2097
|
-
const artifact = await apiRawRequest("POST", "/api/artifacts", options.body, {
|
|
2098
|
-
"Content-Type": options.format === "markdown" ? "text/markdown; charset=utf-8" : "text/plain; charset=utf-8",
|
|
2099
|
-
"X-Artifact-Filename": filename,
|
|
2100
|
-
"X-Artifact-Kind": "document",
|
|
2101
|
-
"X-Artifact-Sensitivity": "normal",
|
|
2102
|
-
}) as { id: string; size?: number };
|
|
2103
|
-
const preview = truncateText(options.body, 1800);
|
|
2104
|
-
const body = [
|
|
2105
|
-
`Full reply attached as Agent Relay artifact ${artifact.id} (${filename}).`,
|
|
2106
|
-
"",
|
|
2107
|
-
"Preview:",
|
|
2108
|
-
preview,
|
|
2109
|
-
].join("\n");
|
|
2110
|
-
return {
|
|
2111
|
-
body,
|
|
2112
|
-
subject: options.subject ?? "Long reply attached",
|
|
2113
|
-
attachments: [{
|
|
2114
|
-
artifactId: artifact.id,
|
|
2115
|
-
kind: "document",
|
|
2116
|
-
role: "report",
|
|
2117
|
-
title: "Full reply",
|
|
2118
|
-
metadata: {
|
|
2119
|
-
source: "agent-relay-cli",
|
|
2120
|
-
replyTo: options.replyTo,
|
|
2121
|
-
originalBytes: encodedLength(options.body),
|
|
2122
|
-
},
|
|
2123
|
-
}],
|
|
2124
|
-
payload: {
|
|
2125
|
-
...payload,
|
|
2126
|
-
relay: {
|
|
2127
|
-
longReply: true,
|
|
2128
|
-
artifactId: artifact.id,
|
|
2129
|
-
originalBytes: encodedLength(options.body),
|
|
2130
|
-
},
|
|
2131
|
-
},
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
function encodedLength(value: string): number {
|
|
2136
|
-
return new TextEncoder().encode(value).byteLength;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
function truncateText(value: string, maxChars: number): string {
|
|
2140
|
-
if (value.length <= maxChars) return value;
|
|
2141
|
-
return `${value.slice(0, maxChars)}\n\n[truncated preview; full content is attached]`;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
function formatMessageDetails(payload: {
|
|
2145
|
-
message: any;
|
|
2146
|
-
artifacts: any;
|
|
2147
|
-
thread: any;
|
|
2148
|
-
replyTo: any;
|
|
2149
|
-
}): string {
|
|
2150
|
-
const message = payload.message;
|
|
2151
|
-
const lines = [
|
|
2152
|
-
`Message #${message.id}`,
|
|
2153
|
-
`From: ${message.from}`,
|
|
2154
|
-
`To: ${message.to}`,
|
|
2155
|
-
`Kind: ${message.kind}`,
|
|
2156
|
-
];
|
|
2157
|
-
if (message.subject) lines.push(`Subject: ${message.subject}`);
|
|
2158
|
-
if (message.channel) lines.push(`Channel: ${message.channel}`);
|
|
2159
|
-
if (message.replyTo) lines.push(`Reply-to: #${message.replyTo}`);
|
|
2160
|
-
if (message.threadId) lines.push(`Thread: #${message.threadId}`);
|
|
2161
|
-
if (message.createdAt) lines.push(`Created: ${new Date(message.createdAt).toISOString()}`);
|
|
2162
|
-
if (message.deliveryStatus) lines.push(`Delivery: ${message.deliveryStatus}`);
|
|
2163
|
-
if (message.resolvedToAgent) lines.push(`Resolved-to: ${message.resolvedToAgent}`);
|
|
2164
|
-
lines.push("", "Body:", String(message.body ?? ""));
|
|
2165
|
-
|
|
2166
|
-
if (message.payload && Object.keys(message.payload).length) {
|
|
2167
|
-
lines.push("", "Payload:", JSON.stringify(message.payload, null, 2));
|
|
2168
|
-
}
|
|
2169
|
-
if (message.meta && Object.keys(message.meta).length) {
|
|
2170
|
-
lines.push("", "Meta:", JSON.stringify(message.meta, null, 2));
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
const artifacts = Array.isArray(payload.artifacts) ? payload.artifacts : [];
|
|
2174
|
-
if (artifacts.length) {
|
|
2175
|
-
lines.push("", "Attachments:");
|
|
2176
|
-
for (const artifact of artifacts) {
|
|
2177
|
-
lines.push(`- ${artifact.id ?? artifact.artifactId} ${artifact.filename ? `(${artifact.filename}) ` : ""}${artifact.mediaType ?? artifact.kind ?? ""}`.trim());
|
|
2178
|
-
}
|
|
2179
|
-
} else if (Array.isArray(message.payload?.attachments) && message.payload.attachments.length) {
|
|
2180
|
-
lines.push("", "Attachments:");
|
|
2181
|
-
for (const attachment of message.payload.attachments) {
|
|
2182
|
-
lines.push(`- ${attachment.artifactId}${attachment.title ? ` (${attachment.title})` : ""}`);
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
if (payload.replyTo) {
|
|
2187
|
-
lines.push("", "Reply Context:", `Parent: #${payload.replyTo.id} from ${payload.replyTo.from} to ${payload.replyTo.to}`);
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
const thread = Array.isArray(payload.thread) ? payload.thread : [];
|
|
2191
|
-
if (thread.length) {
|
|
2192
|
-
lines.push("", "Thread:");
|
|
2193
|
-
for (const item of thread) {
|
|
2194
|
-
const subject = item.subject ? ` ${item.subject}` : "";
|
|
2195
|
-
lines.push(`- #${item.id} ${item.from} -> ${item.to}${subject}`);
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
return lines.join("\n");
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
async function handleStatusCommand(args: string[]): Promise<void> {
|
|
2203
|
-
let agentId = await detectAgentId();
|
|
2204
|
-
let json = false;
|
|
2205
|
-
for (let i = 0; i < args.length; i++) {
|
|
2206
|
-
const arg = args[i];
|
|
2207
|
-
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
2208
|
-
else if (arg === "--json") json = true;
|
|
2209
|
-
else throw new Error(`Unknown status option "${arg}"`);
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
const [stats, health, pairs, agent] = await Promise.all([
|
|
2213
|
-
apiRequest("GET", "/api/stats"),
|
|
2214
|
-
apiRequest("GET", "/api/health"),
|
|
2215
|
-
agentId ? apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}`) : Promise.resolve([]),
|
|
2216
|
-
agentId ? apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`).catch(() => null) : Promise.resolve(null),
|
|
2217
|
-
]);
|
|
2218
|
-
const payload = { agent, stats, health, pairs };
|
|
2219
|
-
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
2220
|
-
else console.log(formatStatus(payload));
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
async function handleLabelCommand(args: string[]): Promise<void> {
|
|
2224
|
-
let agentId = await detectAgentId();
|
|
2225
|
-
let label: string | null | undefined;
|
|
2226
|
-
let json = false;
|
|
2227
|
-
for (let i = 0; i < args.length; i++) {
|
|
2228
|
-
const arg = args[i];
|
|
2229
|
-
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
2230
|
-
else if (arg === "--clear") label = null;
|
|
2231
|
-
else if (arg === "--json") json = true;
|
|
2232
|
-
else if (label === undefined) label = args.slice(i).join(" ");
|
|
2233
|
-
}
|
|
2234
|
-
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
2235
|
-
if (label === undefined) {
|
|
2236
|
-
const agent = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { label?: string };
|
|
2237
|
-
console.log(agent.label ?? "(no label)");
|
|
2238
|
-
return;
|
|
2239
|
-
}
|
|
2240
|
-
const result = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/label`, { label });
|
|
2241
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
2242
|
-
else console.log(label ? `Label set: ${label}` : "Label cleared.");
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
async function handleTagsCommand(args: string[]): Promise<void> {
|
|
2246
|
-
let agentId = await detectAgentId();
|
|
2247
|
-
let json = false;
|
|
2248
|
-
let listOnly = false;
|
|
2249
|
-
let add: string[] = [];
|
|
2250
|
-
let remove: string[] = [];
|
|
2251
|
-
const positional: string[] = [];
|
|
2252
|
-
for (let i = 0; i < args.length; i++) {
|
|
2253
|
-
const arg = args[i];
|
|
2254
|
-
if (arg === "--agent" && i + 1 < args.length) agentId = args[++i];
|
|
2255
|
-
else if (arg === "--json") json = true;
|
|
2256
|
-
else if (arg === "--list") listOnly = true;
|
|
2257
|
-
else if (arg === "--add" && i + 1 < args.length) add = add.concat(splitTagArgs(args[++i]!));
|
|
2258
|
-
else if (arg === "--remove" && i + 1 < args.length) remove = remove.concat(splitTagArgs(args[++i]!));
|
|
2259
|
-
else positional.push(...splitTagArgs(arg!));
|
|
2260
|
-
}
|
|
2261
|
-
if (!agentId) throw new Error("Could not detect current Agent Relay ID. Pass --agent AGENT_ID or set AGENT_RELAY_ID.");
|
|
2262
|
-
const current = await apiRequest("GET", `/api/agents/${encodeURIComponent(agentId)}`) as { tags?: string[] };
|
|
2263
|
-
if (listOnly || (positional.length === 0 && add.length === 0 && remove.length === 0)) {
|
|
2264
|
-
console.log((current.tags ?? []).join(", ") || "(no tags)");
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
const next = positional.length > 0
|
|
2268
|
-
? uniqueStrings(positional)
|
|
2269
|
-
: uniqueStrings([...(current.tags ?? []), ...add]).filter((tag) => !remove.includes(tag));
|
|
2270
|
-
const updated = await apiRequest("PATCH", `/api/agents/${encodeURIComponent(agentId)}/tags`, { tags: next });
|
|
2271
|
-
if (json) console.log(JSON.stringify(updated, null, 2));
|
|
2272
|
-
else console.log(`Tags: ${next.join(", ") || "(none)"}`);
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
async function handleRecipeCommand(args: string[]): Promise<void> {
|
|
2276
|
-
const action = args[0] ?? "list";
|
|
2277
|
-
const rest = args.slice(1);
|
|
2278
|
-
if (action === "list") {
|
|
2279
|
-
const json = rest.includes("--json");
|
|
2280
|
-
const recipes = await apiRequest("GET", "/api/recipes");
|
|
2281
|
-
if (json) console.log(JSON.stringify(recipes, null, 2));
|
|
2282
|
-
else console.log(formatRecipes(recipes as any[]));
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
if (action === "show") {
|
|
2286
|
-
const name = rest.find((arg) => !arg.startsWith("--"));
|
|
2287
|
-
const json = rest.includes("--json");
|
|
2288
|
-
if (!name) throw new Error("Usage: agent-relay recipe show NAME [--json]");
|
|
2289
|
-
const recipe = await apiRequest("GET", `/api/recipes/${encodeURIComponent(name)}`);
|
|
2290
|
-
if (json) console.log(JSON.stringify(recipe, null, 2));
|
|
2291
|
-
else console.log(formatRecipe(recipe as any));
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
if (action === "start") {
|
|
2295
|
-
const name = rest[0];
|
|
2296
|
-
if (!name || name.startsWith("--")) throw new Error("Usage: agent-relay recipe start NAME [--cwd PATH] [--orchestrator ID] [--by NAME] [--json]");
|
|
2297
|
-
let cwd: string | undefined;
|
|
2298
|
-
let orchestratorId: string | undefined;
|
|
2299
|
-
let startedBy: string | undefined;
|
|
2300
|
-
let json = false;
|
|
2301
|
-
for (let i = 1; i < rest.length; i++) {
|
|
2302
|
-
const arg = rest[i];
|
|
2303
|
-
if (arg === "--cwd" && i + 1 < rest.length) cwd = rest[++i];
|
|
2304
|
-
else if ((arg === "--orchestrator" || arg === "--orchestrator-id") && i + 1 < rest.length) orchestratorId = rest[++i];
|
|
2305
|
-
else if (arg === "--by" && i + 1 < rest.length) startedBy = rest[++i];
|
|
2306
|
-
else if (arg === "--json") json = true;
|
|
2307
|
-
else throw new Error(`Unknown recipe start option "${arg}"`);
|
|
2308
|
-
}
|
|
2309
|
-
const result = await apiRequest("POST", "/api/recipes/start", { name, cwd, orchestratorId, startedBy });
|
|
2310
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
2311
|
-
else {
|
|
2312
|
-
const payload = result as any;
|
|
2313
|
-
console.log(`Recipe ${payload.instance.recipeName} started: ${payload.instance.id} (${payload.commands.length} command(s))`);
|
|
2314
|
-
}
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
if (action === "stop") {
|
|
2318
|
-
const id = rest[0];
|
|
2319
|
-
if (!id || id.startsWith("--")) throw new Error("Usage: agent-relay recipe stop INSTANCE_ID [--by NAME] [--json]");
|
|
2320
|
-
let stoppedBy: string | undefined;
|
|
2321
|
-
let json = false;
|
|
2322
|
-
for (let i = 1; i < rest.length; i++) {
|
|
2323
|
-
const arg = rest[i];
|
|
2324
|
-
if (arg === "--by" && i + 1 < rest.length) stoppedBy = rest[++i];
|
|
2325
|
-
else if (arg === "--json") json = true;
|
|
2326
|
-
else throw new Error(`Unknown recipe stop option "${arg}"`);
|
|
2327
|
-
}
|
|
2328
|
-
const result = await apiRequest("POST", `/api/recipes/instances/${encodeURIComponent(id)}/stop`, { stoppedBy });
|
|
2329
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
2330
|
-
else {
|
|
2331
|
-
const payload = result as any;
|
|
2332
|
-
console.log(`Recipe ${payload.instance.recipeName} stopped: ${payload.instance.id} (${payload.commands.length} command(s))`);
|
|
2333
|
-
}
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (action === "status" || action === "instances") {
|
|
2337
|
-
const json = rest.includes("--json");
|
|
2338
|
-
const instances = await apiRequest("GET", "/api/recipes/instances");
|
|
2339
|
-
if (json) console.log(JSON.stringify(instances, null, 2));
|
|
2340
|
-
else console.log(formatRecipeInstances(instances as any[]));
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
throw new Error("Usage: agent-relay recipe <list|show|start|stop|status> [options]");
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
async function handleTokenCommand(args: string[]): Promise<void> {
|
|
2347
|
-
const action = args[0] ?? "list";
|
|
2348
|
-
const rest = args.slice(1);
|
|
2349
|
-
if (action === "list") {
|
|
2350
|
-
const json = rest.includes("--json");
|
|
2351
|
-
const tokens = await apiRequest("GET", "/api/tokens");
|
|
2352
|
-
if (json) console.log(JSON.stringify(tokens, null, 2));
|
|
2353
|
-
else console.log(formatTokens(tokens as any[]));
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
if (action === "create") {
|
|
2357
|
-
let role: string | undefined;
|
|
2358
|
-
let sub: string | undefined;
|
|
2359
|
-
let ttlSeconds: number | undefined;
|
|
2360
|
-
let json = false;
|
|
2361
|
-
let scope: string[] | undefined;
|
|
2362
|
-
for (let i = 0; i < rest.length; i++) {
|
|
2363
|
-
const arg = rest[i];
|
|
2364
|
-
if (arg === "--role" && i + 1 < rest.length) role = rest[++i];
|
|
2365
|
-
else if (arg === "--sub" && i + 1 < rest.length) sub = rest[++i];
|
|
2366
|
-
else if ((arg === "--scope" || arg === "--scopes") && i + 1 < rest.length) scope = splitTagArgs(rest[++i]!);
|
|
2367
|
-
else if ((arg === "--ttl" || arg === "--ttl-seconds") && i + 1 < rest.length) ttlSeconds = parseInt(rest[++i]!, 10);
|
|
2368
|
-
else if (arg === "--json") json = true;
|
|
2369
|
-
else throw new Error(`Unknown token create option "${arg}"`);
|
|
2370
|
-
}
|
|
2371
|
-
if (!role) throw new Error("Usage: agent-relay token create --role ROLE [--sub SUBJECT] [--scope a,b] [--ttl SECONDS]");
|
|
2372
|
-
const result = await apiRequest("POST", "/api/tokens", { role, sub, scope, ttlSeconds, createdBy: "cli" });
|
|
2373
|
-
if (json) console.log(JSON.stringify(result, null, 2));
|
|
2374
|
-
else {
|
|
2375
|
-
const payload = result as any;
|
|
2376
|
-
console.log(payload.token);
|
|
2377
|
-
console.error(`Issued ${payload.record.role} token ${payload.record.jti} for ${payload.record.sub}`);
|
|
2378
|
-
}
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
if (action === "revoke") {
|
|
2382
|
-
const jti = rest.find((arg) => !arg.startsWith("--"));
|
|
2383
|
-
if (!jti) throw new Error("Usage: agent-relay token revoke JTI");
|
|
2384
|
-
await apiRequest("POST", `/api/tokens/${encodeURIComponent(jti)}/revoke`, {});
|
|
2385
|
-
console.log(`Token revoked: ${jti}`);
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2388
|
-
if (action === "verify") {
|
|
2389
|
-
const token = rest.find((arg) => !arg.startsWith("--")) ?? process.env.AGENT_RELAY_TOKEN;
|
|
2390
|
-
if (!token) throw new Error("Usage: agent-relay token verify TOKEN");
|
|
2391
|
-
const payload = decodeJwtPayload(token);
|
|
2392
|
-
if (!payload) throw new Error("not a component JWT");
|
|
2393
|
-
let record: unknown = null;
|
|
2394
|
-
if (typeof payload.jti === "string") {
|
|
2395
|
-
record = await apiRequest("GET", `/api/tokens/${encodeURIComponent(payload.jti)}`).catch(() => null);
|
|
2396
|
-
}
|
|
2397
|
-
console.log(JSON.stringify({ payload, record }, null, 2));
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
throw new Error("Usage: agent-relay token <create|list|revoke|verify> [options]");
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
2404
|
-
const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
2405
|
-
const headers: Record<string, string> = {};
|
|
2406
|
-
const token = process.env.AGENT_RELAY_TOKEN;
|
|
2407
|
-
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
2408
|
-
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
2409
|
-
const response = await fetch(new URL(path, baseUrl), {
|
|
2410
|
-
method,
|
|
2411
|
-
headers,
|
|
2412
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
2413
|
-
});
|
|
2414
|
-
const text = await response.text();
|
|
2415
|
-
const payload = text ? JSON.parse(text) : null;
|
|
2416
|
-
if (!response.ok) {
|
|
2417
|
-
const message = payload && typeof payload === "object" && "error" in payload ? String((payload as any).error) : text;
|
|
2418
|
-
throw new Error(`agent-relay ${method} ${path} failed (${response.status}): ${message}`);
|
|
2419
|
-
}
|
|
2420
|
-
return payload;
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
async function apiRawRequest(method: string, path: string, body: BodyInit, extraHeaders: Record<string, string> = {}): Promise<unknown> {
|
|
2424
|
-
const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
2425
|
-
const headers: Record<string, string> = { ...extraHeaders };
|
|
2426
|
-
const token = process.env.AGENT_RELAY_TOKEN;
|
|
2427
|
-
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
2428
|
-
const response = await fetch(new URL(path, baseUrl), { method, headers, body });
|
|
2429
|
-
const text = await response.text();
|
|
2430
|
-
const payload = text ? JSON.parse(text) : null;
|
|
2431
|
-
if (!response.ok) {
|
|
2432
|
-
const message = payload && typeof payload === "object" && "error" in payload ? String((payload as any).error) : text;
|
|
2433
|
-
throw new Error(`agent-relay ${method} ${path} failed (${response.status}): ${message}`);
|
|
2434
|
-
}
|
|
2435
|
-
return payload;
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
function splitTagArgs(raw: string): string[] {
|
|
2439
|
-
return raw.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
function uniqueStrings(values: string[]): string[] {
|
|
2443
|
-
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
async function detectActivePairId(agentId: string): Promise<string | undefined> {
|
|
2447
|
-
const pairs = await apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}&status=active`) as Array<{ id?: string }>;
|
|
2448
|
-
return Array.isArray(pairs) && typeof pairs[0]?.id === "string" ? pairs[0].id : undefined;
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
async function detectAgentId(): Promise<string | undefined> {
|
|
2452
|
-
const explicit = process.env.AGENT_RELAY_ID;
|
|
2453
|
-
if (explicit) return explicit;
|
|
2454
|
-
|
|
2455
|
-
const contextMatch = currentAgentContextId();
|
|
2456
|
-
if (contextMatch) return contextMatch;
|
|
2457
|
-
|
|
2458
|
-
const cwd = process.cwd();
|
|
2459
|
-
const explicitCodexState = process.env.AGENT_RELAY_CODEX_STATE_PATH
|
|
2460
|
-
? readCodexState(process.env.AGENT_RELAY_CODEX_STATE_PATH)
|
|
2461
|
-
: null;
|
|
2462
|
-
if (explicitCodexState?.agentId) return explicitCodexState.agentId;
|
|
2463
|
-
|
|
2464
|
-
const stateCandidates = [
|
|
2465
|
-
resolve(cwd, "codex/runtime/live-state.json"),
|
|
2466
|
-
...collectCodexStateFiles(),
|
|
2467
|
-
].filter((path): path is string => Boolean(path));
|
|
2468
|
-
|
|
2469
|
-
const codexMatch = newestCodexAgentId(stateCandidates, cwd);
|
|
2470
|
-
if (codexMatch) return codexMatch;
|
|
2471
|
-
|
|
2472
|
-
const claudeMatch = currentClaudeAgentId();
|
|
2473
|
-
if (claudeMatch) return claudeMatch;
|
|
2474
|
-
|
|
2475
|
-
try {
|
|
2476
|
-
const agents = await apiRequest("GET", "/api/agents") as Array<{ id?: string; status?: string; ready?: boolean; meta?: { cwd?: unknown }; lastSeen?: number }>;
|
|
2477
|
-
const cwdAgents = agents
|
|
2478
|
-
.filter((agent) => agent.status !== "offline" && agent.ready !== false && agent.meta?.cwd === cwd && typeof agent.id === "string")
|
|
2479
|
-
.sort((a, b) => (b.lastSeen ?? 0) - (a.lastSeen ?? 0));
|
|
2480
|
-
const uniqueAgentIds = uniqueStrings(cwdAgents.map((agent) => agent.id!));
|
|
2481
|
-
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
2482
|
-
} catch {
|
|
2483
|
-
return undefined;
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
function currentAgentContextId(): string | undefined {
|
|
2488
|
-
const explicitPath = process.env.AGENT_RELAY_CONTEXT_PATH;
|
|
2489
|
-
if (explicitPath) {
|
|
2490
|
-
const explicit = readAgentContext(explicitPath);
|
|
2491
|
-
if (explicit?.agentId && contextMatchesCurrentProcess(explicit)) return explicit.agentId;
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
const candidates = collectAgentContextFiles();
|
|
2495
|
-
const matches = candidates
|
|
2496
|
-
.map((path) => readAgentContext(path))
|
|
2497
|
-
.filter((context): context is AgentContextState => Boolean(context))
|
|
2498
|
-
.filter((context) => contextMatchesCurrentProcess(context))
|
|
2499
|
-
.filter((context) => context.matchEnv.some((match) => process.env[match.name] === match.value))
|
|
2500
|
-
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
2501
|
-
|
|
2502
|
-
const uniqueAgentIds = uniqueStrings(matches.map((context) => context.agentId));
|
|
2503
|
-
return uniqueAgentIds.length === 1 ? uniqueAgentIds[0] : undefined;
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
interface AgentContextState {
|
|
2507
|
-
agentId: string;
|
|
2508
|
-
cwd?: string;
|
|
2509
|
-
updatedAtMs: number;
|
|
2510
|
-
matchEnv: Array<{ name: string; value: string }>;
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
function contextMatchesCurrentProcess(context: AgentContextState): boolean {
|
|
2514
|
-
return !context.cwd || context.cwd === process.cwd();
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
function readAgentContext(path: string): AgentContextState | null {
|
|
2518
|
-
if (!existsSync(path)) return null;
|
|
2519
|
-
try {
|
|
2520
|
-
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
2521
|
-
agentId?: unknown;
|
|
2522
|
-
cwd?: unknown;
|
|
2523
|
-
updatedAt?: unknown;
|
|
2524
|
-
matchEnv?: unknown;
|
|
2525
|
-
};
|
|
2526
|
-
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
2527
|
-
const matchEnv = Array.isArray(parsed.matchEnv)
|
|
2528
|
-
? parsed.matchEnv.flatMap((item) => {
|
|
2529
|
-
if (!item || typeof item !== "object") return [];
|
|
2530
|
-
const record = item as { name?: unknown; value?: unknown };
|
|
2531
|
-
return typeof record.name === "string" && typeof record.value === "string"
|
|
2532
|
-
? [{ name: record.name, value: record.value }]
|
|
2533
|
-
: [];
|
|
2534
|
-
})
|
|
2535
|
-
: [];
|
|
2536
|
-
const stat = statSync(path);
|
|
2537
|
-
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
2538
|
-
return {
|
|
2539
|
-
agentId: parsed.agentId,
|
|
2540
|
-
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
2541
|
-
matchEnv,
|
|
2542
|
-
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
2543
|
-
};
|
|
2544
|
-
} catch {
|
|
2545
|
-
return null;
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
function collectAgentContextFiles(): string[] {
|
|
2550
|
-
const roots = [
|
|
2551
|
-
join(process.env.HOME || "", ".agent-relay", "contexts"),
|
|
2552
|
-
].filter((root) => root && existsSync(root));
|
|
2553
|
-
const files: string[] = [];
|
|
2554
|
-
for (const root of roots) collectFiles(root, ".json", files, 2);
|
|
2555
|
-
return files;
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
function newestCodexAgentId(paths: string[], cwd: string): string | undefined {
|
|
2559
|
-
const states = paths
|
|
2560
|
-
.map((path) => readCodexState(path))
|
|
2561
|
-
.filter((state): state is { agentId: string; cwd?: string; updatedAtMs: number } => Boolean(state))
|
|
2562
|
-
.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
2563
|
-
const cwdAgentIds = uniqueStrings(states.filter((state) => state.cwd === cwd).map((state) => state.agentId));
|
|
2564
|
-
return cwdAgentIds.length === 1 ? cwdAgentIds[0] : undefined;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
function readCodexState(path: string): { agentId: string; cwd?: string; updatedAtMs: number } | null {
|
|
2568
|
-
if (!existsSync(path)) return null;
|
|
2569
|
-
try {
|
|
2570
|
-
const parsed = JSON.parse(readFileSync(path, "utf8")) as { agentId?: unknown; cwd?: unknown; updatedAt?: unknown };
|
|
2571
|
-
if (typeof parsed.agentId !== "string" || !parsed.agentId) return null;
|
|
2572
|
-
const stat = statSync(path);
|
|
2573
|
-
const updatedAt = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
2574
|
-
return {
|
|
2575
|
-
agentId: parsed.agentId,
|
|
2576
|
-
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
2577
|
-
updatedAtMs: Number.isFinite(updatedAt) ? updatedAt : stat.mtimeMs,
|
|
2578
|
-
};
|
|
2579
|
-
} catch {
|
|
2580
|
-
return null;
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
function collectCodexStateFiles(): string[] {
|
|
2585
|
-
const roots = [
|
|
2586
|
-
join(process.env.HOME || "", ".agent-relay", "codex", "runtime"),
|
|
2587
|
-
resolve(process.cwd(), "codex", "runtime"),
|
|
2588
|
-
].filter((root) => root && existsSync(root));
|
|
2589
|
-
const files: string[] = [];
|
|
2590
|
-
for (const root of roots) collectFiles(root, "live-state.json", files, 4);
|
|
2591
|
-
return files;
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
function collectFiles(dir: string, name: string, output: string[], depth: number): void {
|
|
2595
|
-
if (depth < 0) return;
|
|
2596
|
-
let entries: string[];
|
|
2597
|
-
try {
|
|
2598
|
-
entries = readdirSync(dir);
|
|
2599
|
-
} catch {
|
|
2600
|
-
return;
|
|
2601
|
-
}
|
|
2602
|
-
for (const entry of entries) {
|
|
2603
|
-
const path = join(dir, entry);
|
|
2604
|
-
try {
|
|
2605
|
-
const stat = statSync(path);
|
|
2606
|
-
if (stat.isDirectory()) collectFiles(path, name, output, depth - 1);
|
|
2607
|
-
else if (name.startsWith(".") ? entry.endsWith(name) : entry === name) output.push(path);
|
|
2608
|
-
} catch {
|
|
2609
|
-
// Ignore state files that disappear while scanning.
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
function currentClaudeAgentId(): string | undefined {
|
|
2615
|
-
const sessionKey = process.env.CLAUDE_CODE_SESSION_ID || String(process.ppid || "");
|
|
2616
|
-
if (!sessionKey) return undefined;
|
|
2617
|
-
const safeSessionKey = sessionKey.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
2618
|
-
const statePath = join("/tmp", `agent-relay-instance-${safeSessionKey}.state`);
|
|
2619
|
-
if (!existsSync(statePath)) return undefined;
|
|
2620
|
-
try {
|
|
2621
|
-
return readFileSync(statePath, "utf8").split(/\r?\n/)[0]?.trim() || undefined;
|
|
2622
|
-
} catch {
|
|
2623
|
-
return undefined;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
function formatPairs(pairs: any[]): string {
|
|
2628
|
-
if (!pairs.length) return "No pair sessions.";
|
|
2629
|
-
return pairs
|
|
2630
|
-
.map((pair) => `${pair.id} ${pair.status} ${pair.requesterId} <-> ${pair.targetId}${pair.objective ? ` ${pair.objective}` : ""}`)
|
|
2631
|
-
.join("\n");
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
function formatStatus(payload: any): string {
|
|
2635
|
-
const agent = payload.agent;
|
|
2636
|
-
const stats = payload.stats ?? {};
|
|
2637
|
-
const health = payload.health ?? {};
|
|
2638
|
-
const pairs = Array.isArray(payload.pairs) ? payload.pairs : [];
|
|
2639
|
-
const activePair = pairs.find((pair: any) => pair.status === "active") ?? pairs.find((pair: any) => pair.status === "pending");
|
|
2640
|
-
return [
|
|
2641
|
-
`Relay: ${health.status ?? "unknown"} version=${stats.version ?? "unknown"}`,
|
|
2642
|
-
`Agents: ${stats.online ?? "?"}/${stats.agents ?? "?"} online Messages: ${stats.messages ?? "?"} Tasks: ${stats.openTasks ?? "?"}/${stats.tasks ?? "?"} open`,
|
|
2643
|
-
agent
|
|
2644
|
-
? `Current: ${agent.id} status=${agent.status} ready=${agent.ready ? "yes" : "no"} label=${agent.label ?? "(none)"} tags=${(agent.tags ?? []).join(", ") || "(none)"}`
|
|
2645
|
-
: "Current: unknown",
|
|
2646
|
-
activePair
|
|
2647
|
-
? `Pair: ${activePair.id} ${activePair.status} ${activePair.requesterId} <-> ${activePair.targetId}`
|
|
2648
|
-
: "Pair: none active",
|
|
2649
|
-
].join("\n");
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
function formatRecipes(recipes: any[]): string {
|
|
2653
|
-
if (!recipes.length) return "No recipes.";
|
|
2654
|
-
return recipes
|
|
2655
|
-
.map((entry) => {
|
|
2656
|
-
const recipe = entry.recipe ?? {};
|
|
2657
|
-
const agents = Array.isArray(recipe.agents)
|
|
2658
|
-
? recipe.agents.map((agent: any) => `${agent.count ?? 1}x ${agent.provider}:${agent.role}`).join(", ")
|
|
2659
|
-
: "no agents";
|
|
2660
|
-
return `${entry.name} ${entry.source} ${agents} ${recipe.description ?? ""}`.trim();
|
|
2661
|
-
})
|
|
2662
|
-
.join("\n");
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
function formatRecipe(entry: any): string {
|
|
2666
|
-
const recipe = entry.recipe ?? {};
|
|
2667
|
-
const agents = Array.isArray(recipe.agents)
|
|
2668
|
-
? recipe.agents.map((agent: any) => ` - ${agent.count ?? 1}x ${agent.provider}:${agent.role}`).join("\n")
|
|
2669
|
-
: " (none)";
|
|
2670
|
-
return [
|
|
2671
|
-
`${entry.name} (${entry.source})`,
|
|
2672
|
-
recipe.description,
|
|
2673
|
-
"Agents:",
|
|
2674
|
-
agents,
|
|
2675
|
-
].filter(Boolean).join("\n");
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
function formatRecipeInstances(instances: any[]): string {
|
|
2679
|
-
if (!instances.length) return "No recipe instances.";
|
|
2680
|
-
return instances
|
|
2681
|
-
.map((instance) => {
|
|
2682
|
-
const agents = Array.isArray(instance.agents) ? instance.agents.length : 0;
|
|
2683
|
-
return `${instance.id} ${instance.status} ${instance.recipeName} agents=${agents} cwd=${instance.cwd}`;
|
|
2684
|
-
})
|
|
2685
|
-
.join("\n");
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
function formatTokens(tokens: any[]): string {
|
|
2689
|
-
if (!tokens.length) return "No component tokens.";
|
|
2690
|
-
return tokens
|
|
2691
|
-
.map((token) => {
|
|
2692
|
-
const state = token.revokedAt ? "revoked" : token.expiresAt && token.expiresAt <= Math.floor(Date.now() / 1000) ? "expired" : "active";
|
|
2693
|
-
return `${token.jti} ${state} ${token.role} ${token.sub} ${(token.scope ?? []).join(",")}`;
|
|
2694
|
-
})
|
|
2695
|
-
.join("\n");
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
2699
|
-
const payload = token.split(".")[1];
|
|
2700
|
-
if (!payload) return null;
|
|
2701
|
-
try {
|
|
2702
|
-
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
2703
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2704
|
-
} catch {
|
|
2705
|
-
return null;
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
async function confirm(message: string): Promise<boolean> {
|
|
2710
|
-
if (!input.isTTY) return false;
|
|
2711
|
-
const rl = createInterface({ input, output });
|
|
2712
|
-
try {
|
|
2713
|
-
const answer = await rl.question(`${message} [y/N] `);
|
|
2714
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
2715
|
-
} finally {
|
|
2716
|
-
rl.close();
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
1
|
+
// Barrel shim — cli.ts split into src/cli/ per-domain command modules (#294, epic
|
|
2
|
+
// #291). Keeps the import sites (src/index.ts + cli.test.ts importing handleCli /
|
|
3
|
+
// WORKSPACE_USAGE / removeLegacyCodexSessionStartHookToml) untouched.
|
|
4
|
+
export * from "./cli/index";
|