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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Provider shim commands — auto-split from cli.ts (#294). `provider wrap|unwrap`
|
|
2
|
+
// installs/removes the claude/codex PATH shims, and prunes legacy Codex
|
|
3
|
+
// SessionStart hook entries left by older versions (migration cruft, still wired
|
|
4
|
+
// because re-wrapping must clean a previously-hooked ~/.codex/config.toml).
|
|
5
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export async function handleProviderCommand(args: string[]): Promise<void> {
|
|
10
|
+
const action = args[0];
|
|
11
|
+
const provider = args[1];
|
|
12
|
+
if ((action !== "wrap" && action !== "unwrap") || (provider !== "claude" && provider !== "codex")) {
|
|
13
|
+
throw new Error("Usage: agent-relay provider <wrap|unwrap> <claude|codex>");
|
|
14
|
+
}
|
|
15
|
+
const dir = join(process.env.HOME || homedir(), ".agent-relay", "bin");
|
|
16
|
+
const shims = providerShimPaths(provider);
|
|
17
|
+
if (provider === "codex") cleanLegacyCodexSessionStartHook();
|
|
18
|
+
if (action === "wrap") {
|
|
19
|
+
for (const shim of shims) {
|
|
20
|
+
mkdirSync(dirname(shim), { recursive: true });
|
|
21
|
+
writeFileSync(shim, providerShimContent(provider), "utf8");
|
|
22
|
+
chmodSync(shim, 0o755);
|
|
23
|
+
console.log(`Wrapped ${provider}: ${shim}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`Ensure ${dir} is before the provider binary on PATH.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const shim of shims) {
|
|
29
|
+
if (existsSync(shim)) unlinkSync(shim);
|
|
30
|
+
console.log(`Unwrapped ${provider}: ${shim}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cleanLegacyCodexSessionStartHook(): void {
|
|
35
|
+
const configPath = join(process.env.HOME || homedir(), ".codex", "config.toml");
|
|
36
|
+
if (!existsSync(configPath)) return;
|
|
37
|
+
const before = readFileSync(configPath, "utf8");
|
|
38
|
+
const after = removeLegacyCodexSessionStartHookToml(before);
|
|
39
|
+
if (after === before) return;
|
|
40
|
+
writeFileSync(configPath, after, "utf8");
|
|
41
|
+
console.log(`Removed legacy Agent Relay Codex hook entries from ${configPath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function removeLegacyCodexSessionStartHookToml(input: string): string {
|
|
45
|
+
const blocks = tomlHookBlocks(input);
|
|
46
|
+
let output = "";
|
|
47
|
+
for (let index = 0; index < blocks.length;) {
|
|
48
|
+
const block = blocks[index];
|
|
49
|
+
if (isCodexHookGroupHeader(block?.header)) {
|
|
50
|
+
const group = block!;
|
|
51
|
+
const hooks: typeof blocks = [];
|
|
52
|
+
const hookHeader = codexHookHandlerHeader(group.header!);
|
|
53
|
+
index += 1;
|
|
54
|
+
while (index < blocks.length && blocks[index]?.header?.trim() === hookHeader) {
|
|
55
|
+
hooks.push(blocks[index]!);
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keptHooks = hooks.filter((hook) => !isLegacyCodexSessionStartHook(hook.text));
|
|
60
|
+
const groupIsLegacyOnly = isLegacyCodexSessionStartHook(group.text) || (hooks.length > 0 && keptHooks.length === 0);
|
|
61
|
+
if (!groupIsLegacyOnly || keptHooks.length > 0) {
|
|
62
|
+
output += `${group.text}\n`;
|
|
63
|
+
for (const hook of keptHooks) output += `${hook.text}\n`;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isCodexHookHandlerHeader(block?.header) && isLegacyCodexSessionStartHook(block?.text ?? "")) {
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (block) output += `${block.text}\n`;
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
return output.trimEnd() + "\n";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tomlHookBlocks(input: string): Array<{ header: string | null; text: string }> {
|
|
80
|
+
const lines = input.split(/\r?\n/);
|
|
81
|
+
const blocks: Array<{ header: string | null; text: string }> = [];
|
|
82
|
+
let current: { header: string | null; lines: string[] } = { header: null, lines: [] };
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const header = line.match(/^\s*\[\[hooks\.[^\]]+\]\]\s*$/)?.[0] ?? null;
|
|
85
|
+
if (header) {
|
|
86
|
+
if (current.lines.length > 0) blocks.push({ header: current.header, text: current.lines.join("\n").trimEnd() });
|
|
87
|
+
current = { header, lines: [line] };
|
|
88
|
+
} else {
|
|
89
|
+
current.lines.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (current.lines.length > 0) blocks.push({ header: current.header, text: current.lines.join("\n").trimEnd() });
|
|
93
|
+
return blocks.filter((block) => block.text.trim().length > 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isCodexHookGroupHeader(header: string | null | undefined): boolean {
|
|
97
|
+
return /^\[\[hooks\.[^.]+\]\]$/.test(header?.trim() ?? "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isCodexHookHandlerHeader(header: string | null | undefined): boolean {
|
|
101
|
+
return /^\[\[hooks\.[^.]+\.hooks\]\]$/.test(header?.trim() ?? "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function codexHookHandlerHeader(groupHeader: string): string {
|
|
105
|
+
return groupHeader.trim().replace(/\]\]$/, ".hooks]]");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isLegacyCodexSessionStartHook(text: string): boolean {
|
|
109
|
+
return /\.agent-relay\/codex\/package\/hooks\//.test(text) ||
|
|
110
|
+
/agent-relay-codex.*hook/i.test(text) ||
|
|
111
|
+
/agent-relay.*(SessionStart|UserPromptSubmit|Stop).*hook/i.test(text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function providerShimPaths(provider: "claude" | "codex"): string[] {
|
|
115
|
+
const root = join(process.env.HOME || homedir(), ".agent-relay");
|
|
116
|
+
const paths = [join(root, "bin", provider)];
|
|
117
|
+
if (provider === "codex") paths.push(join(root, "codex", "bin", "codex"));
|
|
118
|
+
return paths;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function providerShimContent(provider: "claude" | "codex"): string {
|
|
122
|
+
if (provider === "claude") {
|
|
123
|
+
return `#!/usr/bin/env bash\nexec claude-relay claude -- "$@"\n`;
|
|
124
|
+
}
|
|
125
|
+
return `#!/usr/bin/env bash
|
|
126
|
+
set -e
|
|
127
|
+
|
|
128
|
+
resolve_path() {
|
|
129
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
130
|
+
realpath "$1" 2>/dev/null || printf '%s\\n' "$1"
|
|
131
|
+
elif command -v readlink >/dev/null 2>&1; then
|
|
132
|
+
readlink -f "$1" 2>/dev/null || printf '%s\\n' "$1"
|
|
133
|
+
else
|
|
134
|
+
printf '%s\\n' "$1"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
find_real_codex() {
|
|
139
|
+
local self resolved candidate
|
|
140
|
+
self="$(resolve_path "$0")"
|
|
141
|
+
IFS=: read -r -a path_parts <<< "$PATH"
|
|
142
|
+
for dir in "\${path_parts[@]}"; do
|
|
143
|
+
candidate="$dir/codex"
|
|
144
|
+
[ -x "$candidate" ] || continue
|
|
145
|
+
resolved="$(resolve_path "$candidate")"
|
|
146
|
+
[ "$resolved" = "$self" ] && continue
|
|
147
|
+
printf '%s\\n' "$candidate"
|
|
148
|
+
return 0
|
|
149
|
+
done
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
first_command() {
|
|
154
|
+
local skip_next=0
|
|
155
|
+
for arg in "$@"; do
|
|
156
|
+
if [ "$skip_next" = 1 ]; then
|
|
157
|
+
skip_next=0
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
case "$arg" in
|
|
161
|
+
--) break ;;
|
|
162
|
+
-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)
|
|
163
|
+
skip_next=1
|
|
164
|
+
;;
|
|
165
|
+
--config=*|--enable=*|--disable=*|--image=*|--model=*|--local-provider=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
|
|
166
|
+
;;
|
|
167
|
+
-*)
|
|
168
|
+
;;
|
|
169
|
+
*)
|
|
170
|
+
printf '%s\\n' "$arg"
|
|
171
|
+
return 0
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
done
|
|
175
|
+
return 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
has_passthrough_option() {
|
|
179
|
+
for arg in "$@"; do
|
|
180
|
+
case "$arg" in
|
|
181
|
+
-h|--help|-V|--version)
|
|
182
|
+
return 0
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
done
|
|
186
|
+
return 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
exec_real_codex() {
|
|
190
|
+
real_codex="$(find_real_codex)" || {
|
|
191
|
+
echo "agent-relay codex shim could not find the real codex binary on PATH" >&2
|
|
192
|
+
exit 127
|
|
193
|
+
}
|
|
194
|
+
exec "$real_codex" "$@"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if has_passthrough_option "$@"; then
|
|
198
|
+
exec_real_codex "$@"
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
case "$(first_command "$@")" in
|
|
202
|
+
exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|sandbox|debug|apply|a|cloud|exec-server|features|help)
|
|
203
|
+
exec_real_codex "$@"
|
|
204
|
+
;;
|
|
205
|
+
esac
|
|
206
|
+
|
|
207
|
+
exec codex-relay codex -- "$@"
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Recipe commands — auto-split from cli.ts (#294). list/show/start/stop/status for
|
|
2
|
+
// multi-agent recipes.
|
|
3
|
+
import { apiRequest } from "./_shared";
|
|
4
|
+
|
|
5
|
+
export async function handleRecipeCommand(args: string[]): Promise<void> {
|
|
6
|
+
const action = args[0] ?? "list";
|
|
7
|
+
const rest = args.slice(1);
|
|
8
|
+
if (action === "list") {
|
|
9
|
+
const json = rest.includes("--json");
|
|
10
|
+
const recipes = await apiRequest("GET", "/api/recipes");
|
|
11
|
+
if (json) console.log(JSON.stringify(recipes, null, 2));
|
|
12
|
+
else console.log(formatRecipes(recipes as any[]));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (action === "show") {
|
|
16
|
+
const name = rest.find((arg) => !arg.startsWith("--"));
|
|
17
|
+
const json = rest.includes("--json");
|
|
18
|
+
if (!name) throw new Error("Usage: agent-relay recipe show NAME [--json]");
|
|
19
|
+
const recipe = await apiRequest("GET", `/api/recipes/${encodeURIComponent(name)}`);
|
|
20
|
+
if (json) console.log(JSON.stringify(recipe, null, 2));
|
|
21
|
+
else console.log(formatRecipe(recipe as any));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (action === "start") {
|
|
25
|
+
const name = rest[0];
|
|
26
|
+
if (!name || name.startsWith("--")) throw new Error("Usage: agent-relay recipe start NAME [--cwd PATH] [--orchestrator ID] [--by NAME] [--json]");
|
|
27
|
+
let cwd: string | undefined;
|
|
28
|
+
let orchestratorId: string | undefined;
|
|
29
|
+
let startedBy: string | undefined;
|
|
30
|
+
let json = false;
|
|
31
|
+
for (let i = 1; i < rest.length; i++) {
|
|
32
|
+
const arg = rest[i];
|
|
33
|
+
if (arg === "--cwd" && i + 1 < rest.length) cwd = rest[++i];
|
|
34
|
+
else if ((arg === "--orchestrator" || arg === "--orchestrator-id") && i + 1 < rest.length) orchestratorId = rest[++i];
|
|
35
|
+
else if (arg === "--by" && i + 1 < rest.length) startedBy = rest[++i];
|
|
36
|
+
else if (arg === "--json") json = true;
|
|
37
|
+
else throw new Error(`Unknown recipe start option "${arg}"`);
|
|
38
|
+
}
|
|
39
|
+
const result = await apiRequest("POST", "/api/recipes/start", { name, cwd, orchestratorId, startedBy });
|
|
40
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
else {
|
|
42
|
+
const payload = result as any;
|
|
43
|
+
console.log(`Recipe ${payload.instance.recipeName} started: ${payload.instance.id} (${payload.commands.length} command(s))`);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (action === "stop") {
|
|
48
|
+
const id = rest[0];
|
|
49
|
+
if (!id || id.startsWith("--")) throw new Error("Usage: agent-relay recipe stop INSTANCE_ID [--by NAME] [--json]");
|
|
50
|
+
let stoppedBy: string | undefined;
|
|
51
|
+
let json = false;
|
|
52
|
+
for (let i = 1; i < rest.length; i++) {
|
|
53
|
+
const arg = rest[i];
|
|
54
|
+
if (arg === "--by" && i + 1 < rest.length) stoppedBy = rest[++i];
|
|
55
|
+
else if (arg === "--json") json = true;
|
|
56
|
+
else throw new Error(`Unknown recipe stop option "${arg}"`);
|
|
57
|
+
}
|
|
58
|
+
const result = await apiRequest("POST", `/api/recipes/instances/${encodeURIComponent(id)}/stop`, { stoppedBy });
|
|
59
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
else {
|
|
61
|
+
const payload = result as any;
|
|
62
|
+
console.log(`Recipe ${payload.instance.recipeName} stopped: ${payload.instance.id} (${payload.commands.length} command(s))`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (action === "status" || action === "instances") {
|
|
67
|
+
const json = rest.includes("--json");
|
|
68
|
+
const instances = await apiRequest("GET", "/api/recipes/instances");
|
|
69
|
+
if (json) console.log(JSON.stringify(instances, null, 2));
|
|
70
|
+
else console.log(formatRecipeInstances(instances as any[]));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw new Error("Usage: agent-relay recipe <list|show|start|stop|status> [options]");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatRecipes(recipes: any[]): string {
|
|
77
|
+
if (!recipes.length) return "No recipes.";
|
|
78
|
+
return recipes
|
|
79
|
+
.map((entry) => {
|
|
80
|
+
const recipe = entry.recipe ?? {};
|
|
81
|
+
const agents = Array.isArray(recipe.agents)
|
|
82
|
+
? recipe.agents.map((agent: any) => `${agent.count ?? 1}x ${agent.provider}:${agent.role}`).join(", ")
|
|
83
|
+
: "no agents";
|
|
84
|
+
return `${entry.name} ${entry.source} ${agents} ${recipe.description ?? ""}`.trim();
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatRecipe(entry: any): string {
|
|
90
|
+
const recipe = entry.recipe ?? {};
|
|
91
|
+
const agents = Array.isArray(recipe.agents)
|
|
92
|
+
? recipe.agents.map((agent: any) => ` - ${agent.count ?? 1}x ${agent.provider}:${agent.role}`).join("\n")
|
|
93
|
+
: " (none)";
|
|
94
|
+
return [
|
|
95
|
+
`${entry.name} (${entry.source})`,
|
|
96
|
+
recipe.description,
|
|
97
|
+
"Agents:",
|
|
98
|
+
agents,
|
|
99
|
+
].filter(Boolean).join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatRecipeInstances(instances: any[]): string {
|
|
103
|
+
if (!instances.length) return "No recipe instances.";
|
|
104
|
+
return instances
|
|
105
|
+
.map((instance) => {
|
|
106
|
+
const agents = Array.isArray(instance.agents) ? instance.agents.length : 0;
|
|
107
|
+
return `${instance.id} ${instance.status} ${instance.recipeName} agents=${agents} cwd=${instance.cwd}`;
|
|
108
|
+
})
|
|
109
|
+
.join("\n");
|
|
110
|
+
}
|
package/src/cli/reply.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Reply command — auto-split from cli.ts (#294). Replies to a message by id
|
|
2
|
+
// (auto-routes to the sender). Large bodies are uploaded as an artifact and sent
|
|
3
|
+
// as a concise attached reply.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { MAX_BODY_BYTES } from "../config";
|
|
7
|
+
import { apiRawRequest, apiRequest, encodedLength, readStdin, truncateText } from "./_shared";
|
|
8
|
+
import { detectAgentId } from "./agent-detect";
|
|
9
|
+
|
|
10
|
+
export async function handleReplyCommand(args: string[]): Promise<void> {
|
|
11
|
+
const msgIdRaw = args[0];
|
|
12
|
+
if (!msgIdRaw || msgIdRaw.startsWith("--")) {
|
|
13
|
+
throw new Error("Usage: agent-relay /reply <messageId> <body|--stdin|--body-file PATH> [--from AGENT_ID] [--subject TEXT] [--format text|markdown|markdownv2] [--json]");
|
|
14
|
+
}
|
|
15
|
+
const replyTo = Number.parseInt(msgIdRaw, 10);
|
|
16
|
+
if (!Number.isFinite(replyTo) || replyTo <= 0) throw new Error("messageId must be a positive integer");
|
|
17
|
+
|
|
18
|
+
let from = await detectAgentId();
|
|
19
|
+
let subject: string | undefined;
|
|
20
|
+
let format: string | undefined;
|
|
21
|
+
let json = false;
|
|
22
|
+
let stdinBody = false;
|
|
23
|
+
let bodyFile: string | undefined;
|
|
24
|
+
const bodyParts: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 1; i < args.length; i++) {
|
|
27
|
+
const arg = args[i];
|
|
28
|
+
if (arg === "--from" && i + 1 < args.length) from = args[++i];
|
|
29
|
+
else if (arg === "--subject" && i + 1 < args.length) subject = args[++i];
|
|
30
|
+
else if (arg === "--stdin") stdinBody = true;
|
|
31
|
+
else if ((arg === "--body-file" || arg === "--file") && i + 1 < args.length) bodyFile = args[++i];
|
|
32
|
+
else if (arg === "--format" && i + 1 < args.length) {
|
|
33
|
+
const parsed = parseReplyFormat(args[++i]!);
|
|
34
|
+
if (!parsed) throw new Error("--format must be text, markdown, or markdownv2");
|
|
35
|
+
format = parsed;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "--json") json = true;
|
|
38
|
+
else bodyParts.push(arg!);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const body = (await resolveBodyInput({ bodyParts, stdinBody, bodyFile })).trim();
|
|
42
|
+
if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
|
|
43
|
+
if (!body) throw new Error("Reply body required.");
|
|
44
|
+
|
|
45
|
+
const prepared = await prepareReplyBody({ body, from, replyTo, format, subject });
|
|
46
|
+
const message = await apiRequest("POST", "/api/messages", {
|
|
47
|
+
from,
|
|
48
|
+
body: prepared.body,
|
|
49
|
+
subject: prepared.subject,
|
|
50
|
+
replyTo,
|
|
51
|
+
attachments: prepared.attachments,
|
|
52
|
+
payload: prepared.payload,
|
|
53
|
+
});
|
|
54
|
+
const msg = message as any;
|
|
55
|
+
if (json) console.log(JSON.stringify(msg, null, 2));
|
|
56
|
+
else console.log(`Reply sent: ${msg.id} -> ${msg.to} (reply to #${replyTo})`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseReplyFormat(value: string): "text" | "markdown" | "markdownv2" | undefined {
|
|
60
|
+
const normalized = value.trim().toLowerCase();
|
|
61
|
+
if (normalized === "text" || normalized === "markdown" || normalized === "markdownv2") return normalized;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const MAX_DIRECT_REPLY_BODY_BYTES = Math.floor(MAX_BODY_BYTES * 0.75);
|
|
66
|
+
|
|
67
|
+
async function resolveBodyInput(options: {
|
|
68
|
+
bodyParts: string[];
|
|
69
|
+
stdinBody: boolean;
|
|
70
|
+
bodyFile?: string;
|
|
71
|
+
}): Promise<string> {
|
|
72
|
+
const sources = [
|
|
73
|
+
options.bodyParts.length > 0 ? "argv" : "",
|
|
74
|
+
options.stdinBody ? "stdin" : "",
|
|
75
|
+
options.bodyFile ? "body file" : "",
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
if (sources.length > 1) throw new Error("Reply body must come from exactly one source: arguments, --stdin, or --body-file.");
|
|
78
|
+
if (options.stdinBody) return readStdin();
|
|
79
|
+
if (options.bodyFile) return readFileSync(resolve(options.bodyFile), "utf8");
|
|
80
|
+
return options.bodyParts.join(" ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function prepareReplyBody(options: {
|
|
84
|
+
body: string;
|
|
85
|
+
from: string;
|
|
86
|
+
replyTo: number;
|
|
87
|
+
format?: string;
|
|
88
|
+
subject?: string;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
body: string;
|
|
91
|
+
subject?: string;
|
|
92
|
+
attachments?: Array<Record<string, unknown>>;
|
|
93
|
+
payload?: Record<string, unknown>;
|
|
94
|
+
}> {
|
|
95
|
+
const payload: Record<string, unknown> = options.format ? { message: { format: options.format } } : {};
|
|
96
|
+
if (encodedLength(options.body) <= MAX_DIRECT_REPLY_BODY_BYTES) {
|
|
97
|
+
return {
|
|
98
|
+
body: options.body,
|
|
99
|
+
subject: options.subject,
|
|
100
|
+
payload: Object.keys(payload).length ? payload : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const filename = `relay-reply-${options.replyTo}-${Date.now()}.${options.format === "markdown" ? "md" : "txt"}`;
|
|
105
|
+
const artifact = await apiRawRequest("POST", "/api/artifacts", options.body, {
|
|
106
|
+
"Content-Type": options.format === "markdown" ? "text/markdown; charset=utf-8" : "text/plain; charset=utf-8",
|
|
107
|
+
"X-Artifact-Filename": filename,
|
|
108
|
+
"X-Artifact-Kind": "document",
|
|
109
|
+
"X-Artifact-Sensitivity": "normal",
|
|
110
|
+
}) as { id: string; size?: number };
|
|
111
|
+
const preview = truncateText(options.body, 1800);
|
|
112
|
+
const body = [
|
|
113
|
+
`Full reply attached as Agent Relay artifact ${artifact.id} (${filename}).`,
|
|
114
|
+
"",
|
|
115
|
+
"Preview:",
|
|
116
|
+
preview,
|
|
117
|
+
].join("\n");
|
|
118
|
+
return {
|
|
119
|
+
body,
|
|
120
|
+
subject: options.subject ?? "Long reply attached",
|
|
121
|
+
attachments: [{
|
|
122
|
+
artifactId: artifact.id,
|
|
123
|
+
kind: "document",
|
|
124
|
+
role: "report",
|
|
125
|
+
title: "Full reply",
|
|
126
|
+
metadata: {
|
|
127
|
+
source: "agent-relay-cli",
|
|
128
|
+
replyTo: options.replyTo,
|
|
129
|
+
originalBytes: encodedLength(options.body),
|
|
130
|
+
},
|
|
131
|
+
}],
|
|
132
|
+
payload: {
|
|
133
|
+
...payload,
|
|
134
|
+
relay: {
|
|
135
|
+
longReply: true,
|
|
136
|
+
artifactId: artifact.id,
|
|
137
|
+
originalBytes: encodedLength(options.body),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
package/src/cli/setup.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Setup command — auto-split from cli.ts (#294). Generates the relay env file.
|
|
2
|
+
import { createSetupPlan, executeSetupPlan, formatSetupPlan, pathExists } from "../setup";
|
|
3
|
+
import { confirm } from "./_shared";
|
|
4
|
+
|
|
5
|
+
export async function handleSetupCommand(args: string[]): Promise<void> {
|
|
6
|
+
let envFile: string | undefined;
|
|
7
|
+
let host: string | undefined;
|
|
8
|
+
let port: number | undefined;
|
|
9
|
+
let dbPath: string | undefined;
|
|
10
|
+
let runtimePrefix: string | undefined;
|
|
11
|
+
let token: string | undefined;
|
|
12
|
+
let generateToken = true;
|
|
13
|
+
let dryRun = false;
|
|
14
|
+
let force = false;
|
|
15
|
+
let yes = false;
|
|
16
|
+
let json = false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg === "--env-file" && i + 1 < args.length) envFile = args[++i];
|
|
21
|
+
else if (arg === "--host" && i + 1 < args.length) host = args[++i];
|
|
22
|
+
else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
|
|
23
|
+
else if (arg === "--db-path" && i + 1 < args.length) dbPath = args[++i];
|
|
24
|
+
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
|
|
25
|
+
else if (arg === "--token" && i + 1 < args.length) token = args[++i];
|
|
26
|
+
else if (arg === "--generate-token") generateToken = true;
|
|
27
|
+
else if (arg === "--no-token") generateToken = false;
|
|
28
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
29
|
+
else if (arg === "--force") force = true;
|
|
30
|
+
else if (arg === "--yes") yes = true;
|
|
31
|
+
else if (arg === "--json") json = true;
|
|
32
|
+
else throw new Error(`Unknown setup option "${arg}"`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const plan = createSetupPlan({
|
|
36
|
+
...(envFile ? { envFile } : {}),
|
|
37
|
+
...(host ? { host } : {}),
|
|
38
|
+
...(port !== undefined ? { port } : {}),
|
|
39
|
+
...(dbPath ? { dbPath } : {}),
|
|
40
|
+
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
41
|
+
...(token ? { token } : {}),
|
|
42
|
+
generateToken,
|
|
43
|
+
force,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!dryRun && !yes && await pathExists(plan.envFile)) {
|
|
47
|
+
const ok = await confirm(`Overwrite ${plan.envFile}?`);
|
|
48
|
+
if (!ok) {
|
|
49
|
+
console.log("Setup cancelled.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = await executeSetupPlan(plan, { dryRun, force });
|
|
55
|
+
if (json) console.log(JSON.stringify({ plan, output: result }, null, 2));
|
|
56
|
+
else console.log(dryRun ? formatSetupPlan(plan) : result);
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Steward commands — auto-split from cli.ts (#294). Briefing rail for the steward
|
|
2
|
+
// (#208): the attention queue, per-workspace diagnostics, and check suggestions.
|
|
3
|
+
import { apiRequest } from "./_shared";
|
|
4
|
+
|
|
5
|
+
// Steward briefing commands (#208): queue of workspaces needing attention, a
|
|
6
|
+
// per-workspace diagnostics inspection, and a check-command suggestion.
|
|
7
|
+
export async function handleStewardCommand(args: string[]): Promise<void> {
|
|
8
|
+
const action = args[0];
|
|
9
|
+
if (!action || !["queue", "inspect", "checks"].includes(action)) {
|
|
10
|
+
throw new Error("Usage: agent-relay steward <queue|inspect|checks> [WORKSPACE_ID] [--repo PATH] [--json]");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let repo: string | undefined;
|
|
14
|
+
let json = false;
|
|
15
|
+
const positional: string[] = [];
|
|
16
|
+
for (let i = 1; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
if (arg === "--repo" && i + 1 < args.length) repo = args[++i];
|
|
19
|
+
else if (arg === "--json") json = true;
|
|
20
|
+
else if (!arg!.startsWith("--")) positional.push(arg!);
|
|
21
|
+
else throw new Error(`Unknown steward option "${arg}".`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (action === "queue") {
|
|
25
|
+
const all = await apiRequest("GET", "/api/workspaces") as any[];
|
|
26
|
+
const attention = new Set(["conflict", "review_requested", "merge_planned"]);
|
|
27
|
+
const queue = all.filter((ws) => attention.has(ws.status) && (!repo || ws.repoRoot === repo));
|
|
28
|
+
if (json) { console.log(JSON.stringify(queue, null, 2)); return; }
|
|
29
|
+
if (!queue.length) { console.log("Steward queue empty — no workspaces awaiting review, merge, or conflict resolution."); return; }
|
|
30
|
+
for (const ws of queue) console.log(`${ws.status.padEnd(16)} ${ws.branch ?? ws.id} (${ws.repoRoot})`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const id = positional[0];
|
|
35
|
+
if (!id) throw new Error(`Usage: agent-relay steward ${action} WORKSPACE_ID [--json]`);
|
|
36
|
+
|
|
37
|
+
if (action === "inspect") {
|
|
38
|
+
console.log(JSON.stringify(await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diagnostics`), null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// checks: suggest validation commands from the workspace's changed files.
|
|
43
|
+
const diff = await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diff?patch=0`) as any;
|
|
44
|
+
const files: string[] = Array.isArray(diff?.files) ? diff.files.map((f: any) => f.path) : [];
|
|
45
|
+
const checks = suggestStewardChecks(files);
|
|
46
|
+
console.log(JSON.stringify({ workspaceId: id, changedFiles: files.length, checks }, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Heuristic check suggestions from changed file paths. Repo-agnostic defaults a
|
|
50
|
+
// steward can refine; cheaper than re-deriving from project docs every run.
|
|
51
|
+
function suggestStewardChecks(files: string[]): Array<{ command: string; reason: string }> {
|
|
52
|
+
const checks: Array<{ command: string; reason: string }> = [];
|
|
53
|
+
const has = (re: RegExp) => files.some((f) => re.test(f));
|
|
54
|
+
if (has(/\.(ts|tsx|mts|cts)$/)) checks.push({ command: "bun run typecheck", reason: "TypeScript files changed" });
|
|
55
|
+
if (has(/\.test\.|(^|\/)tests?\//)) checks.push({ command: "bun test", reason: "test files changed" });
|
|
56
|
+
else if (files.length) checks.push({ command: "bun test", reason: "repo default" });
|
|
57
|
+
if (has(/(^|\/)dashboard\//)) checks.push({ command: "bun run build:dashboard", reason: "dashboard sources changed" });
|
|
58
|
+
return checks;
|
|
59
|
+
}
|
package/src/cli/token.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Token commands — auto-split from cli.ts (#294). create/list/revoke/verify for
|
|
2
|
+
// component JWTs.
|
|
3
|
+
import { apiRequest, splitTagArgs } from "./_shared";
|
|
4
|
+
|
|
5
|
+
export async function handleTokenCommand(args: string[]): Promise<void> {
|
|
6
|
+
const action = args[0] ?? "list";
|
|
7
|
+
const rest = args.slice(1);
|
|
8
|
+
if (action === "list") {
|
|
9
|
+
const json = rest.includes("--json");
|
|
10
|
+
const tokens = await apiRequest("GET", "/api/tokens");
|
|
11
|
+
if (json) console.log(JSON.stringify(tokens, null, 2));
|
|
12
|
+
else console.log(formatTokens(tokens as any[]));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (action === "create") {
|
|
16
|
+
let role: string | undefined;
|
|
17
|
+
let sub: string | undefined;
|
|
18
|
+
let ttlSeconds: number | undefined;
|
|
19
|
+
let json = false;
|
|
20
|
+
let scope: string[] | undefined;
|
|
21
|
+
for (let i = 0; i < rest.length; i++) {
|
|
22
|
+
const arg = rest[i];
|
|
23
|
+
if (arg === "--role" && i + 1 < rest.length) role = rest[++i];
|
|
24
|
+
else if (arg === "--sub" && i + 1 < rest.length) sub = rest[++i];
|
|
25
|
+
else if ((arg === "--scope" || arg === "--scopes") && i + 1 < rest.length) scope = splitTagArgs(rest[++i]!);
|
|
26
|
+
else if ((arg === "--ttl" || arg === "--ttl-seconds") && i + 1 < rest.length) ttlSeconds = parseInt(rest[++i]!, 10);
|
|
27
|
+
else if (arg === "--json") json = true;
|
|
28
|
+
else throw new Error(`Unknown token create option "${arg}"`);
|
|
29
|
+
}
|
|
30
|
+
if (!role) throw new Error("Usage: agent-relay token create --role ROLE [--sub SUBJECT] [--scope a,b] [--ttl SECONDS]");
|
|
31
|
+
const result = await apiRequest("POST", "/api/tokens", { role, sub, scope, ttlSeconds, createdBy: "cli" });
|
|
32
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
else {
|
|
34
|
+
const payload = result as any;
|
|
35
|
+
console.log(payload.token);
|
|
36
|
+
console.error(`Issued ${payload.record.role} token ${payload.record.jti} for ${payload.record.sub}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (action === "revoke") {
|
|
41
|
+
const jti = rest.find((arg) => !arg.startsWith("--"));
|
|
42
|
+
if (!jti) throw new Error("Usage: agent-relay token revoke JTI");
|
|
43
|
+
await apiRequest("POST", `/api/tokens/${encodeURIComponent(jti)}/revoke`, {});
|
|
44
|
+
console.log(`Token revoked: ${jti}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (action === "verify") {
|
|
48
|
+
const token = rest.find((arg) => !arg.startsWith("--")) ?? process.env.AGENT_RELAY_TOKEN;
|
|
49
|
+
if (!token) throw new Error("Usage: agent-relay token verify TOKEN");
|
|
50
|
+
const payload = decodeJwtPayload(token);
|
|
51
|
+
if (!payload) throw new Error("not a component JWT");
|
|
52
|
+
let record: unknown = null;
|
|
53
|
+
if (typeof payload.jti === "string") {
|
|
54
|
+
record = await apiRequest("GET", `/api/tokens/${encodeURIComponent(payload.jti)}`).catch(() => null);
|
|
55
|
+
}
|
|
56
|
+
console.log(JSON.stringify({ payload, record }, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error("Usage: agent-relay token <create|list|revoke|verify> [options]");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatTokens(tokens: any[]): string {
|
|
63
|
+
if (!tokens.length) return "No component tokens.";
|
|
64
|
+
return tokens
|
|
65
|
+
.map((token) => {
|
|
66
|
+
const state = token.revokedAt ? "revoked" : token.expiresAt && token.expiresAt <= Math.floor(Date.now() / 1000) ? "expired" : "active";
|
|
67
|
+
return `${token.jti} ${state} ${token.role} ${token.sub} ${(token.scope ?? []).join(",")}`;
|
|
68
|
+
})
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
73
|
+
const payload = token.split(".")[1];
|
|
74
|
+
if (!payload) return null;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
77
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|