agent-relay-server 0.3.12 → 0.4.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/src/cli.ts ADDED
@@ -0,0 +1,217 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import {
4
+ createDaemonPlan,
5
+ detectDaemonEnvironment,
6
+ executeDaemonPlan,
7
+ formatDaemonPlan,
8
+ type DaemonAction,
9
+ type DaemonScope,
10
+ } from "./daemon";
11
+ import {
12
+ createSetupPlan,
13
+ executeSetupPlan,
14
+ formatSetupPlan,
15
+ pathExists,
16
+ } from "./setup";
17
+ import { VERSION } from "./config";
18
+
19
+ const HELP = `
20
+ agent-relay ${VERSION}
21
+
22
+ Usage:
23
+ agent-relay [start]
24
+ agent-relay setup [--yes] [--dry-run] [--force] [--env-file PATH] [--host HOST] [--port PORT] [--db-path PATH] [--token TOKEN|--no-token]
25
+ agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]
26
+ agent-relay --help
27
+
28
+ Daemon options:
29
+ --env-file PATH Env file sourced by the daemon (default: platform user config dir)
30
+ --binary PATH Stable agent-relay binary/script path for the service
31
+ --name NAME Service name (default: agent-relay)
32
+ --host HOST Display/listen host for generated plan (default: 127.0.0.1)
33
+ --port PORT Display/listen port for generated plan (default: 4850)
34
+ --user|--system User service by default, system service when explicitly requested
35
+ --enable Enable service at login/boot during install
36
+ --start Start service after install
37
+ --dry-run Print the plan without writing files or running service commands
38
+ --yes Skip confirmation prompts
39
+ --force Overwrite/remove managed-file guardrails
40
+ --json Print structured output
41
+ `.trim();
42
+
43
+ const DAEMON_ACTIONS = new Set<DaemonAction>([
44
+ "install",
45
+ "uninstall",
46
+ "start",
47
+ "stop",
48
+ "restart",
49
+ "enable",
50
+ "disable",
51
+ "status",
52
+ "logs",
53
+ ]);
54
+
55
+ export async function handleCli(args: string[]): Promise<"start" | "handled"> {
56
+ const command = args[0];
57
+ if (!command || command === "start") return "start";
58
+ if (command === "--help" || command === "-h" || command === "help") {
59
+ console.log(HELP);
60
+ return "handled";
61
+ }
62
+ if (command === "--version" || command === "-v") {
63
+ console.log(VERSION);
64
+ return "handled";
65
+ }
66
+ if (command === "setup" || command === "init") {
67
+ await handleSetupCommand(args.slice(1));
68
+ return "handled";
69
+ }
70
+ if (command === "daemon") {
71
+ await handleDaemonCommand(args.slice(1));
72
+ return "handled";
73
+ }
74
+ throw new Error(`Unknown command "${command}". Run agent-relay --help.`);
75
+ }
76
+
77
+ async function handleSetupCommand(args: string[]): Promise<void> {
78
+ let envFile: string | undefined;
79
+ let host: string | undefined;
80
+ let port: number | undefined;
81
+ let dbPath: string | undefined;
82
+ let token: string | undefined;
83
+ let generateToken = true;
84
+ let dryRun = false;
85
+ let force = false;
86
+ let yes = false;
87
+ let json = false;
88
+
89
+ for (let i = 0; i < args.length; i++) {
90
+ const arg = args[i];
91
+ if (arg === "--env-file" && i + 1 < args.length) envFile = args[++i];
92
+ else if (arg === "--host" && i + 1 < args.length) host = args[++i];
93
+ else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
94
+ else if (arg === "--db-path" && i + 1 < args.length) dbPath = args[++i];
95
+ else if (arg === "--token" && i + 1 < args.length) token = args[++i];
96
+ else if (arg === "--generate-token") generateToken = true;
97
+ else if (arg === "--no-token") generateToken = false;
98
+ else if (arg === "--dry-run") dryRun = true;
99
+ else if (arg === "--force") force = true;
100
+ else if (arg === "--yes") yes = true;
101
+ else if (arg === "--json") json = true;
102
+ else throw new Error(`Unknown setup option "${arg}"`);
103
+ }
104
+
105
+ const plan = createSetupPlan({
106
+ ...(envFile ? { envFile } : {}),
107
+ ...(host ? { host } : {}),
108
+ ...(port !== undefined ? { port } : {}),
109
+ ...(dbPath ? { dbPath } : {}),
110
+ ...(token ? { token } : {}),
111
+ generateToken,
112
+ force,
113
+ });
114
+
115
+ if (!dryRun && !yes && await pathExists(plan.envFile)) {
116
+ const ok = await confirm(`Overwrite ${plan.envFile}?`);
117
+ if (!ok) {
118
+ console.log("Setup cancelled.");
119
+ return;
120
+ }
121
+ }
122
+
123
+ const result = await executeSetupPlan(plan, { dryRun, force });
124
+ if (json) console.log(JSON.stringify({ plan, output: result }, null, 2));
125
+ else console.log(dryRun ? formatSetupPlan(plan) : result);
126
+ }
127
+
128
+ async function handleDaemonCommand(args: string[]): Promise<void> {
129
+ const action = parseDaemonAction(args[0]);
130
+ let name: string | undefined;
131
+ let envFile: string | undefined;
132
+ let port: number | undefined;
133
+ let host: string | undefined;
134
+ let scope: DaemonScope | undefined;
135
+ let binaryPath: string | undefined;
136
+ let start = false;
137
+ let enable = false;
138
+ let dryRun = false;
139
+ let force = false;
140
+ let yes = false;
141
+ let json = false;
142
+
143
+ for (let i = 1; i < args.length; i++) {
144
+ const arg = args[i];
145
+ if (arg === "--name" && i + 1 < args.length) name = args[++i];
146
+ else if (arg === "--env-file" && i + 1 < args.length) envFile = args[++i];
147
+ else if (arg === "--port" && i + 1 < args.length) port = parseInt(args[++i]!, 10);
148
+ else if (arg === "--host" && i + 1 < args.length) host = args[++i];
149
+ else if (arg === "--user") scope = "user";
150
+ else if (arg === "--system") scope = "system";
151
+ else if (arg === "--binary" && i + 1 < args.length) binaryPath = args[++i];
152
+ else if (arg === "--start") start = true;
153
+ else if (arg === "--enable") enable = true;
154
+ else if (arg === "--dry-run") dryRun = true;
155
+ else if (arg === "--force") force = true;
156
+ else if (arg === "--yes") yes = true;
157
+ else if (arg === "--json") json = true;
158
+ else throw new Error(`Unknown daemon option "${arg}"`);
159
+ }
160
+
161
+ if (action === "install" && !dryRun && !envFile && !(await pathExists(createSetupPlan().envFile))) {
162
+ const setupPlan = createSetupPlan({
163
+ ...(host ? { host } : {}),
164
+ ...(port !== undefined ? { port } : {}),
165
+ });
166
+ const ok = yes || await confirm(`Create daemon env file at ${setupPlan.envFile}?`);
167
+ if (!ok) throw new Error("Daemon install needs an env file. Run `agent-relay setup` first.");
168
+ console.log(await executeSetupPlan(setupPlan, { force }));
169
+ }
170
+
171
+ const env = await detectDaemonEnvironment();
172
+ const plan = createDaemonPlan({
173
+ action,
174
+ ...(name ? { name } : {}),
175
+ ...(envFile ? { envFile } : {}),
176
+ ...(port !== undefined ? { port } : {}),
177
+ ...(host ? { host } : {}),
178
+ ...(scope ? { scope } : {}),
179
+ ...(binaryPath ? { binaryPath } : {}),
180
+ start,
181
+ enable,
182
+ }, env);
183
+
184
+ if (!dryRun && !json && (action === "install" || action === "uninstall") && !yes) {
185
+ const ok = await confirm(
186
+ action === "install"
187
+ ? `Install ${plan.kind} ${plan.scope} daemon "${plan.name}"?`
188
+ : `Uninstall daemon "${plan.name}"?`,
189
+ );
190
+ if (!ok) {
191
+ console.log("Daemon command cancelled.");
192
+ return;
193
+ }
194
+ }
195
+
196
+ const result = await executeDaemonPlan(plan, { dryRun, force });
197
+ if (json) console.log(JSON.stringify({ plan: result.plan, output: result.output }, null, 2));
198
+ else console.log(dryRun ? formatDaemonPlan(plan) : result.output);
199
+ }
200
+
201
+ function parseDaemonAction(value: string | undefined): DaemonAction {
202
+ if (!value || !DAEMON_ACTIONS.has(value as DaemonAction)) {
203
+ throw new Error("Usage: agent-relay daemon <install|uninstall|start|stop|restart|enable|disable|status|logs> [options]");
204
+ }
205
+ return value as DaemonAction;
206
+ }
207
+
208
+ async function confirm(message: string): Promise<boolean> {
209
+ if (!input.isTTY) return false;
210
+ const rl = createInterface({ input, output });
211
+ try {
212
+ const answer = await rl.question(`${message} [y/N] `);
213
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
214
+ } finally {
215
+ rl.close();
216
+ }
217
+ }
package/src/config.ts CHANGED
@@ -23,6 +23,44 @@ export const REAP_INTERVAL_MS = envPositiveInt("REAP_INTERVAL_MS", 60_000); // r
23
23
 
24
24
  // Max body size for any POST/PATCH request (64 KiB).
25
25
  export const MAX_BODY_BYTES = 64 * 1024;
26
+ export const INTEGRATION_RATE_LIMIT_PER_MINUTE = envPositiveInt("AGENT_RELAY_INTEGRATION_RATE_LIMIT_PER_MINUTE", 120);
27
+
28
+ export const AUTH_TOKEN = process.env.AGENT_RELAY_TOKEN || "";
29
+ export const CORS_ORIGINS = (process.env.AGENT_RELAY_CORS_ORIGINS || "")
30
+ .split(",")
31
+ .map((origin) => origin.trim())
32
+ .filter(Boolean);
33
+
34
+ export type IntegrationTokenConfig = {
35
+ name: string;
36
+ token: string;
37
+ scopes: string[];
38
+ targets?: string[];
39
+ channels?: string[];
40
+ callbackUrl?: string;
41
+ };
42
+
43
+ export function getIntegrationTokens(): IntegrationTokenConfig[] {
44
+ const raw = process.env.AGENT_RELAY_INTEGRATIONS;
45
+ if (!raw) return [];
46
+ try {
47
+ const parsed = JSON.parse(raw) as unknown;
48
+ if (!Array.isArray(parsed)) return [];
49
+ return parsed
50
+ .filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
51
+ .map((item) => ({
52
+ name: typeof item.name === "string" ? item.name : "",
53
+ token: typeof item.token === "string" ? item.token : "",
54
+ scopes: Array.isArray(item.scopes) ? item.scopes.filter((scope): scope is string => typeof scope === "string") : [],
55
+ targets: Array.isArray(item.targets) ? item.targets.filter((target): target is string => typeof target === "string") : undefined,
56
+ channels: Array.isArray(item.channels) ? item.channels.filter((channel): channel is string => typeof channel === "string") : undefined,
57
+ callbackUrl: typeof item.callbackUrl === "string" ? item.callbackUrl : undefined,
58
+ }))
59
+ .filter((item) => item.name && item.token);
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
26
64
 
27
65
  // Default capabilities for session-start hook when AGENT_RELAY_CAPS is unset.
28
66
  export const DEFAULT_CAPABILITIES = ["chat"];