agent-relay-server 0.63.0 → 0.63.2

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.
Files changed (80) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +4 -2
  3. package/public/assets/{MessageBubble-Cl6ufPHw.js → MessageBubble-D130169C.js} +2 -2
  4. package/public/assets/{MessageBubble-Cl6ufPHw.js.map → MessageBubble-D130169C.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BnfjmkQ8.js → PlanGraphPanel-CbwbZmKY.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BnfjmkQ8.js.map → PlanGraphPanel-CbwbZmKY.js.map} +1 -1
  7. package/public/assets/{activity-aFxvGLtd.js → activity-SJjW9fgO.js} +2 -2
  8. package/public/assets/{activity-aFxvGLtd.js.map → activity-SJjW9fgO.js.map} +1 -1
  9. package/public/assets/{agents-DW5djToQ.js → agents-GPly8tGL.js} +2 -2
  10. package/public/assets/{agents-DW5djToQ.js.map → agents-GPly8tGL.js.map} +1 -1
  11. package/public/assets/{automation-Do4OpauI.js → automation-BFO5jLy8.js} +2 -2
  12. package/public/assets/{automation-Do4OpauI.js.map → automation-BFO5jLy8.js.map} +1 -1
  13. package/public/assets/chat-hP_8I1aE.js +2 -0
  14. package/public/assets/{chat-CvaQ2czL.js.map → chat-hP_8I1aE.js.map} +1 -1
  15. package/public/assets/{formatted-body-impl-0Lf8e1Ic.js → formatted-body-impl-DaBd6Vrd.js} +2 -2
  16. package/public/assets/{formatted-body-impl-0Lf8e1Ic.js.map → formatted-body-impl-DaBd6Vrd.js.map} +1 -1
  17. package/public/assets/{index-C3su8ezq.js → index-CmM_Ozy7.js} +5 -5
  18. package/public/assets/{index-C3su8ezq.js.map → index-CmM_Ozy7.js.map} +1 -1
  19. package/public/assets/{insights-64uExH1E.js → insights-4Z7kZ7NB.js} +2 -2
  20. package/public/assets/{insights-64uExH1E.js.map → insights-4Z7kZ7NB.js.map} +1 -1
  21. package/public/assets/{maintenance-EVBpKH07.js → maintenance-17uq77zC.js} +2 -2
  22. package/public/assets/{maintenance-EVBpKH07.js.map → maintenance-17uq77zC.js.map} +1 -1
  23. package/public/assets/{managed-agents-BLZPxGuw.js → managed-agents-CT_1x4C3.js} +2 -2
  24. package/public/assets/{managed-agents-BLZPxGuw.js.map → managed-agents-CT_1x4C3.js.map} +1 -1
  25. package/public/assets/{markdown-preview-impl-Bg6rCWYN.js → markdown-preview-impl-DzcW3gVL.js} +2 -2
  26. package/public/assets/{markdown-preview-impl-Bg6rCWYN.js.map → markdown-preview-impl-DzcW3gVL.js.map} +1 -1
  27. package/public/assets/{memory-Bjr8bgjj.js → memory-B-zfQSp3.js} +2 -2
  28. package/public/assets/{memory-Bjr8bgjj.js.map → memory-B-zfQSp3.js.map} +1 -1
  29. package/public/assets/{messages-CRYw1F4z.js → messages-Ckt9aNgZ.js} +2 -2
  30. package/public/assets/{messages-CRYw1F4z.js.map → messages-Ckt9aNgZ.js.map} +1 -1
  31. package/public/assets/{orchestrators-D-co1vdp.js → orchestrators-DVpGfTtB.js} +2 -2
  32. package/public/assets/{orchestrators-D-co1vdp.js.map → orchestrators-DVpGfTtB.js.map} +1 -1
  33. package/public/assets/{overview-Ddy5czri.js → overview-C8FoAUmj.js} +2 -2
  34. package/public/assets/{overview-Ddy5czri.js.map → overview-C8FoAUmj.js.map} +1 -1
  35. package/public/assets/{pairs-CFVM8uHL.js → pairs-WEM0MEmH.js} +2 -2
  36. package/public/assets/{pairs-CFVM8uHL.js.map → pairs-WEM0MEmH.js.map} +1 -1
  37. package/public/assets/{security-Df4_76bh.js → security-FzjxX7I2.js} +2 -2
  38. package/public/assets/{security-Df4_76bh.js.map → security-FzjxX7I2.js.map} +1 -1
  39. package/public/assets/{select-Cq3BBnL9.js → select-BPpSWAx4.js} +2 -2
  40. package/public/assets/{select-Cq3BBnL9.js.map → select-BPpSWAx4.js.map} +1 -1
  41. package/public/assets/{settings-DOLEYXZL.js → settings-Bl8wcAYn.js} +2 -2
  42. package/public/assets/{settings-DOLEYXZL.js.map → settings-Bl8wcAYn.js.map} +1 -1
  43. package/public/assets/{tasks-BcsmPi9_.js → tasks-Dk6TtgpH.js} +2 -2
  44. package/public/assets/{tasks-BcsmPi9_.js.map → tasks-Dk6TtgpH.js.map} +1 -1
  45. package/public/assets/{teams-BQrV3eKI.js → teams-DRJjIYRl.js} +2 -2
  46. package/public/assets/{teams-BQrV3eKI.js.map → teams-DRJjIYRl.js.map} +1 -1
  47. package/public/assets/{terminal-viewer-impl-Dle04qpH.js → terminal-viewer-impl-YWCrgF9G.js} +2 -2
  48. package/public/assets/{terminal-viewer-impl-Dle04qpH.js.map → terminal-viewer-impl-YWCrgF9G.js.map} +1 -1
  49. package/public/assets/{work-queue-BLhTLg4o.js → work-queue-DYtnYiKs.js} +2 -2
  50. package/public/assets/{work-queue-BLhTLg4o.js.map → work-queue-DYtnYiKs.js.map} +1 -1
  51. package/public/index.html +1 -1
  52. package/runner/src/adapter.ts +2 -1
  53. package/runner/src/config.ts +143 -5
  54. package/src/artifact-storage.ts +3 -5
  55. package/src/bus.ts +3 -4
  56. package/src/cli/_shared.ts +5 -4
  57. package/src/cli/agent-detect.ts +6 -4
  58. package/src/cli/introspect.ts +3 -2
  59. package/src/cli/token.ts +2 -1
  60. package/src/cli/workspace.ts +2 -1
  61. package/src/config.ts +176 -1
  62. package/src/connectors.ts +3 -3
  63. package/src/db/connection.ts +2 -1
  64. package/src/db/continuation-archives.ts +3 -3
  65. package/src/index.ts +6 -5
  66. package/src/maintenance/constants.ts +39 -22
  67. package/src/provider-config-store.ts +2 -1
  68. package/src/provider-state.ts +1 -3
  69. package/src/routes/agent-sessions.ts +5 -4
  70. package/src/routes/orchestrator-proxy.ts +5 -4
  71. package/src/routes/workspaces.ts +5 -4
  72. package/src/security.ts +13 -7
  73. package/src/services/issue-lifecycle.ts +1 -4
  74. package/src/services/plan-graph.ts +1 -4
  75. package/src/upgrade.ts +7 -9
  76. package/src/workspace-auto-merge.ts +4 -5
  77. package/src/workspace-claim.ts +1 -2
  78. package/src/workspace-orphans.ts +2 -7
  79. package/src/workspace-probe.ts +2 -1
  80. package/public/assets/chat-CvaQ2czL.js +0 -2
package/src/config.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // Shared runtime constants. Import from here rather than redefining.
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
4
5
  import { join, dirname } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
7
+ import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
6
8
  import { CONTRACT_VERSIONS } from "./contracts";
7
9
 
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,6 +21,31 @@ function envPositiveInt(name: string, fallback: number): number {
19
21
  return Math.floor(parsed);
20
22
  }
21
23
 
24
+ function envNumberOrDefault(name: string, fallback: number): number {
25
+ return Number(process.env[name]) || fallback;
26
+ }
27
+
28
+ function envNonNegativeNumber(name: string, fallback: number): number {
29
+ const parsed = Number(process.env[name]);
30
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
31
+ }
32
+
33
+ function envSafePositiveInteger(name: string, fallback: number): number {
34
+ const raw = process.env[name];
35
+ if (!raw) return fallback;
36
+ const parsed = Number(raw);
37
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
38
+ }
39
+
40
+ function envTrimmed(name: string): string {
41
+ return (process.env[name] || "").trim();
42
+ }
43
+
44
+ function envTrimmedOrUndefined(name: string): string | undefined {
45
+ const value = process.env[name]?.trim();
46
+ return value || undefined;
47
+ }
48
+
22
49
  export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 120_000); // 2min without heartbeat → offline
23
50
  // #451 — default TTL for fire-and-forget FAN-OUT messages (broadcast/tag/cap/label/team) that the
24
51
  // sender didn't bound. A stale fan-out push must not wake an agent that matches the target hours
@@ -42,12 +69,160 @@ export const MAIN_MERGE_PAUSE_TTL_MS = envPositiveInt("AGENT_RELAY_MAIN_MERGE_PA
42
69
  export const MAX_BODY_BYTES = 64 * 1024;
43
70
  export const INTEGRATION_RATE_LIMIT_PER_MINUTE = envPositiveInt("AGENT_RELAY_INTEGRATION_RATE_LIMIT_PER_MINUTE", 120);
44
71
 
45
- export const AUTH_TOKEN = process.env.AGENT_RELAY_TOKEN || "";
72
+ export const AUTOMATION_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_AUTOMATION_INTERVAL_MS", 30 * 1000);
73
+ export const IDLE_TIMEOUT_SECONDS = envNumberOrDefault("AGENT_RELAY_IDLE_TIMEOUT_SECONDS", 255);
74
+ export const LOG_REQUESTS = process.env.AGENT_RELAY_LOG_REQUESTS === "1";
75
+
76
+ const AUTH_TOKEN = process.env.AGENT_RELAY_TOKEN || "";
46
77
  export const CORS_ORIGINS = (process.env.AGENT_RELAY_CORS_ORIGINS || "")
47
78
  .split(",")
48
79
  .map((origin) => origin.trim())
49
80
  .filter(Boolean);
50
81
 
82
+ export function relayToken(): string {
83
+ return process.env.AGENT_RELAY_TOKEN ?? AUTH_TOKEN;
84
+ }
85
+
86
+ export function relayAuthHeaders(base: Record<string, string> = {}): Record<string, string> {
87
+ const token = relayToken();
88
+ return token ? { ...base, [RELAY_TOKEN_HEADER]: token } : base;
89
+ }
90
+
91
+ export function allowUnauthenticated(): boolean {
92
+ return process.env.AGENT_RELAY_ALLOW_UNAUTH === "1";
93
+ }
94
+
95
+ export function tokenSecretFromEnv(): string | undefined {
96
+ return process.env.AGENT_RELAY_TOKEN_SECRET;
97
+ }
98
+
99
+ export function corsOriginsFromEnv(): string[] | undefined {
100
+ const raw = process.env.AGENT_RELAY_CORS_ORIGINS;
101
+ if (raw === undefined) return undefined;
102
+ return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
103
+ }
104
+
105
+ export function connectorsDirFromEnv(): string | undefined {
106
+ return process.env.AGENT_RELAY_CONNECTORS_DIR;
107
+ }
108
+
109
+ export function connectorActionTimeoutMs(fallback: number): number {
110
+ const raw = Number(process.env.AGENT_RELAY_CONNECTOR_ACTION_TIMEOUT_MS);
111
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
112
+ }
113
+
114
+ export function rateLimitFallbackMs(): number {
115
+ return envNumberOrDefault("AGENT_RELAY_RATE_LIMIT_FALLBACK_MS", 60 * 60 * 1000);
116
+ }
117
+
118
+ export function artifactsDir(): string {
119
+ return process.env.AGENT_RELAY_ARTIFACTS_DIR || "~/.agent-relay/artifacts";
120
+ }
121
+
122
+ export function maxArtifactBytesFromEnv(fallback: number): number {
123
+ return envSafePositiveInteger("AGENT_RELAY_MAX_ARTIFACT_BYTES", fallback);
124
+ }
125
+
126
+ export const OUTBOX_RETENTION_MS = envNumberOrDefault("AGENT_RELAY_OUTBOX_RETENTION_MS", 60 * 60 * 1000);
127
+ export const TOKEN_RECORD_RETENTION_SECONDS = envNumberOrDefault("AGENT_RELAY_TOKEN_RECORD_RETENTION_SECONDS", 7 * 24 * 60 * 60);
128
+ export const CONFLICT_SCAN_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_CONFLICT_SCAN_INTERVAL_MS", 10 * 60 * 1000);
129
+ export const WORKSPACE_RETENTION_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_RETENTION_MS", DAY_MS);
130
+ export const DB_MAINTENANCE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_DB_MAINTENANCE_INTERVAL_MS", DAY_MS);
131
+ export const DB_VACUUM_EVERY = envNumberOrDefault("AGENT_RELAY_DB_VACUUM_EVERY", 7);
132
+ export const WORKSPACE_REVIEW_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_REVIEW_TTL_MS", 3 * DAY_MS);
133
+ export const WORKSPACE_GC_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS", 60 * 60 * 1000);
134
+ export const WORKSPACE_ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_ORPHAN_REAPER_INTERVAL_MS", 30 * 60 * 1000);
135
+ export const WORKSPACE_AUTO_MERGE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_AUTO_MERGE_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
136
+ export const ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_REAPER_INTERVAL_MS", 5 * 60 * 1000);
137
+ export const STEWARD_WAKE_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_STEWARD_WAKE_COOLDOWN_MS", 10 * 60 * 1000);
138
+ export const UNLANDED_FLAG_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_FLAG_COOLDOWN_MS", 6 * 60 * 60 * 1000);
139
+ export const STEWARD_CLAIM_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_CLAIM_TTL_MS", 15 * 60_000);
140
+ export const DB_SLOW_QUERY_MS = envNumberOrDefault("AGENT_RELAY_DB_SLOW_MS", 0);
141
+
142
+ export function stewardEscalationMs(): number {
143
+ return envNumberOrDefault("AGENT_RELAY_WORKSPACE_STEWARD_ESCALATION_MS", 60 * 60 * 1000);
144
+ }
145
+
146
+ export function stewardFallbackTarget(): string {
147
+ return envTrimmed("AGENT_RELAY_WORKSPACE_STEWARD_FALLBACK");
148
+ }
149
+
150
+ export function envMsOrDefault(name: string, fallback: number): number {
151
+ return envNonNegativeNumber(name, fallback);
152
+ }
153
+
154
+ export function orphanGraceMs(): number {
155
+ return envMsOrDefault("AGENT_RELAY_ORPHAN_GRACE_MS", 30 * 60 * 1000);
156
+ }
157
+
158
+ export function orphanReapCooldownMs(): number {
159
+ return envMsOrDefault("AGENT_RELAY_ORPHAN_REAP_COOLDOWN_MS", 5 * 60 * 1000);
160
+ }
161
+
162
+ export function orphanReapEnabled(): boolean {
163
+ return process.env.AGENT_RELAY_ORPHAN_REAP !== "0";
164
+ }
165
+
166
+ export function orphanWorktreeReapEnabled(): boolean {
167
+ return process.env.AGENT_RELAY_ORPHAN_WORKTREE_REAP !== "0";
168
+ }
169
+
170
+ export function workspaceAutoMergeEnabled(): boolean {
171
+ return process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE !== "0";
172
+ }
173
+
174
+ export function continuationArchiveFtsEnabled(): boolean {
175
+ return process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS !== "1";
176
+ }
177
+
178
+ export function busStaleGraceMs(): number {
179
+ return envNumberOrDefault("AGENT_RELAY_STALE_GRACE_MS", 120_000);
180
+ }
181
+
182
+ export function defaultIssueRepo(): string | undefined {
183
+ return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
184
+ }
185
+
186
+ export function cliRelayUrl(): string {
187
+ return process.env.AGENT_RELAY_URL || DEFAULT_RELAY_URL;
188
+ }
189
+
190
+ export function cliRelayToken(): string | undefined {
191
+ return process.env.AGENT_RELAY_TOKEN;
192
+ }
193
+
194
+ export function workspaceJsonFromEnv(): string | undefined {
195
+ return process.env.AGENT_RELAY_WORKSPACE_JSON;
196
+ }
197
+
198
+ export function providerSessionIdFromEnv(): string | undefined {
199
+ return process.env.AGENT_RELAY_PROVIDER_SESSION_ID;
200
+ }
201
+
202
+ export function projectFromEnv(): string | undefined {
203
+ return process.env.AGENT_RELAY_PROJECT;
204
+ }
205
+
206
+ export function agentIdFromEnv(): string | undefined {
207
+ return process.env.AGENT_RELAY_ID;
208
+ }
209
+
210
+ export function codexStatePathFromEnv(): string | undefined {
211
+ return process.env.AGENT_RELAY_CODEX_STATE_PATH;
212
+ }
213
+
214
+ export function contextPathFromEnv(): string | undefined {
215
+ return process.env.AGENT_RELAY_CONTEXT_PATH;
216
+ }
217
+
218
+ export function localOrchestratorIdFromEnv(): string | undefined {
219
+ return envTrimmedOrUndefined("AGENT_RELAY_ORCHESTRATOR_ID");
220
+ }
221
+
222
+ export function providerConfigHomeFromEnv(): string {
223
+ return process.env.AGENT_RELAY_HOME || join(process.env.HOME || ".", ".agent-relay");
224
+ }
225
+
51
226
  export type IntegrationTokenConfig = {
52
227
  name: string;
53
228
  token: string;
package/src/connectors.ts CHANGED
@@ -5,6 +5,7 @@ import type { ConnectorAction, ConnectorActionResult, ConnectorManifest, Connect
5
5
  import { ValidationError } from "./db";
6
6
  import { isRecord } from "agent-relay-sdk";
7
7
  import { registerConnectorConfigSetting, validateConnectorConfig } from "./settings/modules";
8
+ import { connectorActionTimeoutMs as configuredConnectorActionTimeoutMs, connectorsDirFromEnv } from "./config";
8
9
 
9
10
  const CONNECTOR_SCHEMA = "agent-relay.connector.v1";
10
11
  const VALID_KINDS = new Set(["channel", "event", "provider", "orchestrator"]);
@@ -12,7 +13,7 @@ const VALID_ACTIONS = new Set(["install", "uninstall", "enable", "disable", "sta
12
13
  const DEFAULT_ACTION_TIMEOUT_MS = 30_000;
13
14
 
14
15
  function connectorRegistryDir(): string {
15
- const configured = process.env.AGENT_RELAY_CONNECTORS_DIR;
16
+ const configured = connectorsDirFromEnv();
16
17
  if (configured) return resolveHome(configured);
17
18
  return join(homedir(), ".agent-relay", "connectors");
18
19
  }
@@ -177,8 +178,7 @@ export function writeConnectorConfig(id: string, config: Record<string, unknown>
177
178
  }
178
179
 
179
180
  function connectorActionTimeoutMs(): number {
180
- const raw = Number(process.env.AGENT_RELAY_CONNECTOR_ACTION_TIMEOUT_MS);
181
- return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_ACTION_TIMEOUT_MS;
181
+ return configuredConnectorActionTimeoutMs(DEFAULT_ACTION_TIMEOUT_MS);
182
182
  }
183
183
 
184
184
  const LIFECYCLE_ACTIONS = new Set(["install", "uninstall", "start", "stop", "restart", "enable", "disable"]);
@@ -1,4 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { DB_SLOW_QUERY_MS } from "../config";
2
3
 
3
4
  // The shared SQLite handle. Set once by initDb() (schema.ts) and read everywhere
4
5
  // else through getDb(). Kept module-private so the only mutation path is setDb().
@@ -33,7 +34,7 @@ export function applyConnectionPragmas(conn: Database): void {
33
34
  // Slow-query log. Off unless AGENT_RELAY_DB_SLOW_MS is set (>0). Wraps the
34
35
  // execution of the heavy dynamic queries so the next slow query surfaces before
35
36
  // it wedges something (see issue #197 — the 8.6s reply-obligation query).
36
- const SLOW_QUERY_MS = Number(process.env.AGENT_RELAY_DB_SLOW_MS) || 0;
37
+ const SLOW_QUERY_MS = DB_SLOW_QUERY_MS;
37
38
  export function timedQuery<T>(label: string, run: () => T): T {
38
39
  if (SLOW_QUERY_MS <= 0) return run();
39
40
  const start = performance.now();
@@ -1,4 +1,4 @@
1
- import { MAX_BODY_BYTES } from "../config";
1
+ import { MAX_BODY_BYTES, continuationArchiveFtsEnabled } from "../config";
2
2
  import { getContinuationEnvelope } from "./continuations.ts";
3
3
  import { getDb, ValidationError } from "./connection.ts";
4
4
 
@@ -28,7 +28,7 @@ interface ContinuationArchiveRow {
28
28
  const MAX_RECALL_LIMIT = 20;
29
29
 
30
30
  export function ensureContinuationArchiveSearchSchema(): void {
31
- if (process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS === "1") return;
31
+ if (!continuationArchiveFtsEnabled()) return;
32
32
  try {
33
33
  getDb().query(`
34
34
  CREATE VIRTUAL TABLE IF NOT EXISTS continuation_archive_segments
@@ -116,7 +116,7 @@ export function searchContinuationArchives(input: {
116
116
  }
117
117
 
118
118
  function continuationArchiveSearchMode(): "fts5" | "like" {
119
- if (process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS === "1") return "like";
119
+ if (!continuationArchiveFtsEnabled()) return "like";
120
120
  const row = getDb().query(`
121
121
  SELECT 1 FROM sqlite_master
122
122
  WHERE type = 'table' AND name = 'continuation_archive_segments'
package/src/index.ts CHANGED
@@ -15,7 +15,11 @@ import { startPlanLiveBindings } from "./services/plan-live-bindings";
15
15
  import { resolve, sep } from "path";
16
16
  import { gzipSync, brotliCompressSync, constants as zlibConstants } from "node:zlib";
17
17
  import {
18
+ AUTOMATION_INTERVAL_MS,
19
+ IDLE_TIMEOUT_SECONDS,
20
+ LOG_REQUESTS,
18
21
  VERSION,
22
+ relayToken,
19
23
  } from "./config";
20
24
  import { CONTRACT_VERSIONS } from "./contracts";
21
25
  import {
@@ -69,9 +73,6 @@ function startServer(): void {
69
73
  const PORT = Number(process.env.PORT) || 4850;
70
74
  const HOST = process.env.HOST || "127.0.0.1";
71
75
  const DB_PATH = process.env.DB_PATH || "agent-relay.db";
72
- const AUTOMATION_INTERVAL_MS = Number(process.env.AGENT_RELAY_AUTOMATION_INTERVAL_MS) || 30 * 1000;
73
- const IDLE_TIMEOUT_SECONDS = Number(process.env.AGENT_RELAY_IDLE_TIMEOUT_SECONDS) || 255;
74
- const LOG_REQUESTS = process.env.AGENT_RELAY_LOG_REQUESTS === "1";
75
76
 
76
77
  assertSafeNetworkConfig(HOST);
77
78
  initDb(DB_PATH);
@@ -246,9 +247,9 @@ export function createFetchHandler(
246
247
  upstream.protocol = upstream.protocol === "https:" ? "wss:" : "ws:";
247
248
  // Pass the relay token as a header (not a query param) so it never lands in
248
249
  // access/proxy logs (#149). The orchestrator accepts both, but headers are safer.
249
- const relayToken = process.env.AGENT_RELAY_TOKEN;
250
+ const token = relayToken();
250
251
  const upgraded = server.upgrade(req, {
251
- data: { kind: "terminal-proxy", upstreamUrl: upstream.toString(), upstreamToken: relayToken, pending: [] },
252
+ data: { kind: "terminal-proxy", upstreamUrl: upstream.toString(), upstreamToken: token, pending: [] },
252
253
  });
253
254
  if (!upgraded) return new Response("WebSocket upgrade failed", { status: 400 });
254
255
  return undefined;
@@ -1,4 +1,24 @@
1
- import { DAY_MS, REAP_INTERVAL_MS } from "../config";
1
+ import {
2
+ CONFLICT_SCAN_INTERVAL_MS,
3
+ DAY_MS,
4
+ DB_MAINTENANCE_INTERVAL_MS,
5
+ DB_VACUUM_EVERY,
6
+ ORPHAN_REAPER_INTERVAL_MS,
7
+ OUTBOX_RETENTION_MS,
8
+ REAP_INTERVAL_MS,
9
+ TOKEN_RECORD_RETENTION_SECONDS,
10
+ WORKSPACE_AUTO_MERGE_INTERVAL_MS,
11
+ WORKSPACE_GC_INTERVAL_MS,
12
+ WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
13
+ WORKSPACE_RETENTION_MS,
14
+ WORKSPACE_REVIEW_TTL_MS,
15
+ envMsOrDefault,
16
+ orphanReapCooldownMs,
17
+ orphanReapEnabled,
18
+ orphanGraceMs,
19
+ stewardEscalationMs,
20
+ stewardFallbackTarget,
21
+ } from "../config";
2
22
  import { READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
3
23
  import type { WorkspaceStatus } from "../types";
4
24
 
@@ -6,30 +26,27 @@ export { DAY_MS, REAP_INTERVAL_MS, TERMINAL_WORKSPACE_STATUSES };
6
26
 
7
27
  export const DEFAULT_TIMEOUT_MS = 30_000;
8
28
  export const SCHEDULER_TICK_MS = 10_000;
9
- export const OUTBOX_RETENTION_MS = Number(process.env.AGENT_RELAY_OUTBOX_RETENTION_MS) || 60 * 60 * 1000;
10
- export const TOKEN_RECORD_RETENTION_SECONDS = Number(process.env.AGENT_RELAY_TOKEN_RECORD_RETENTION_SECONDS) || 7 * 24 * 60 * 60;
11
- export const CONFLICT_SCAN_INTERVAL_MS = Number(process.env.AGENT_RELAY_CONFLICT_SCAN_INTERVAL_MS) || 10 * 60 * 1000;
12
- export const WORKSPACE_RETENTION_MS = Number(process.env.AGENT_RELAY_WORKSPACE_RETENTION_MS) || DAY_MS;
13
- export const DB_MAINTENANCE_INTERVAL_MS = Number(process.env.AGENT_RELAY_DB_MAINTENANCE_INTERVAL_MS) || DAY_MS;
14
- export const DB_VACUUM_EVERY = Number(process.env.AGENT_RELAY_DB_VACUUM_EVERY) || 7;
15
- export const WORKSPACE_REVIEW_TTL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_REVIEW_TTL_MS) || 3 * DAY_MS;
16
- export const WORKSPACE_GC_INTERVAL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS) || 60 * 60 * 1000;
17
- export const WORKSPACE_ORPHAN_REAPER_INTERVAL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_ORPHAN_REAPER_INTERVAL_MS) || 30 * 60 * 1000;
18
- export const WORKSPACE_AUTO_MERGE_INTERVAL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE_INTERVAL_MS) || CONFLICT_SCAN_INTERVAL_MS;
19
- export const ORPHAN_REAPER_INTERVAL_MS = Number(process.env.AGENT_RELAY_ORPHAN_REAPER_INTERVAL_MS) || 5 * 60 * 1000;
20
-
21
- export const stewardEscalationMs = () => Number(process.env.AGENT_RELAY_WORKSPACE_STEWARD_ESCALATION_MS) || 60 * 60 * 1000;
22
- export const stewardFallbackTarget = () => (process.env.AGENT_RELAY_WORKSPACE_STEWARD_FALLBACK || "").trim();
23
29
  export const STRANDABLE_STATUSES = new Set<WorkspaceStatus>([...READY_TO_LAND_STATUSES, "conflict"]);
24
30
  export const CONFLICT_SCAN_STATUSES = new Set<WorkspaceStatus>(["active", "ready", "review_requested", "merge_planned", "conflict"]);
25
31
  export const LANDED_RECONCILE_STATUSES = new Set<WorkspaceStatus>(["merge_planned", "review_requested", "conflict"]);
26
32
 
27
- export const envMsOrDefault = (name: string, fallback: number): number => {
28
- const v = Number(process.env[name]);
29
- return Number.isFinite(v) && v >= 0 ? v : fallback;
30
- };
31
- export const orphanGraceMs = () => envMsOrDefault("AGENT_RELAY_ORPHAN_GRACE_MS", 30 * 60 * 1000);
32
- export const orphanReapCooldownMs = () => envMsOrDefault("AGENT_RELAY_ORPHAN_REAP_COOLDOWN_MS", 5 * 60 * 1000);
33
33
  export const teamReapGraceMs = () => envMsOrDefault("TEAM_REAP_GRACE_MS", 30 * 60 * 1000);
34
34
  export const teamReapIntervalMs = () => envMsOrDefault("TEAM_REAP_INTERVAL_MS", 5 * 60 * 1000);
35
- export const orphanReapEnabled = () => process.env.AGENT_RELAY_ORPHAN_REAP !== "0";
35
+ export {
36
+ CONFLICT_SCAN_INTERVAL_MS,
37
+ DB_MAINTENANCE_INTERVAL_MS,
38
+ DB_VACUUM_EVERY,
39
+ ORPHAN_REAPER_INTERVAL_MS,
40
+ OUTBOX_RETENTION_MS,
41
+ TOKEN_RECORD_RETENTION_SECONDS,
42
+ WORKSPACE_AUTO_MERGE_INTERVAL_MS,
43
+ WORKSPACE_GC_INTERVAL_MS,
44
+ WORKSPACE_ORPHAN_REAPER_INTERVAL_MS,
45
+ WORKSPACE_RETENTION_MS,
46
+ WORKSPACE_REVIEW_TTL_MS,
47
+ orphanGraceMs,
48
+ orphanReapCooldownMs,
49
+ orphanReapEnabled,
50
+ stewardEscalationMs,
51
+ stewardFallbackTarget,
52
+ };
@@ -3,6 +3,7 @@ import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { ValidationError, listOrchestrators } from "./db";
5
5
  import { getConfig, listConfig, setConfig } from "./config-store";
6
+ import { providerConfigHomeFromEnv } from "./config";
6
7
  import { cleanString, cleanStringArray, optionalEnum } from "./validation";
7
8
  import { emitConfigChanged } from "./sse";
8
9
  import {
@@ -234,5 +235,5 @@ function maskEnv(env: Record<string, string>): Record<string, string> {
234
235
  }
235
236
 
236
237
  function agentRelayHome(): string {
237
- return process.env.AGENT_RELAY_HOME || join(process.env.HOME || ".", ".agent-relay");
238
+ return providerConfigHomeFromEnv();
238
239
  }
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "agent-relay-sdk";
2
+ import { rateLimitFallbackMs } from "./config";
2
3
 
3
4
  // One server-side home for reading an agent's provider runtime state (the
4
5
  // `meta.providerState` that both Claude and Codex ride for blocked/approval
@@ -48,9 +49,6 @@ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
48
49
  // rather than waiting the full (unknown) limit window. Re-entering the hold on a
49
50
  // still-limited retry is cheap, and a subscription limit almost always carries a
50
51
  // reset time. Read at call-time so operators/tests can tune it.
51
- const rateLimitFallbackMs = (): number =>
52
- Number(process.env.AGENT_RELAY_RATE_LIMIT_FALLBACK_MS) || 60 * 60 * 1000;
53
-
54
52
  /**
55
53
  * When a rate-limit hold should be lifted. The provider's `resetAt` is
56
54
  * authoritative; otherwise fall back to `enteredAt + fallback window`. Null only
@@ -14,6 +14,7 @@ import { authContextFromRequest } from "../services/auth-context";
14
14
  import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
15
15
  import { type AgentCard, type SpawnApprovalMode } from "../types";
16
16
  import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
17
+ import { relayToken } from "../config";
17
18
 
18
19
  const CLAUDE_RESUME_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
20
 
@@ -486,8 +487,8 @@ async function proxyOrchestratorDelete(req: Request, orchestratorId: string, pat
486
487
  const incoming = new URL(req.url);
487
488
  const proxyUrl = `${orch.apiUrl}${path}${incoming.search}`;
488
489
  const headers: Record<string, string> = {};
489
- const relayToken = process.env.AGENT_RELAY_TOKEN;
490
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
490
+ const token = relayToken();
491
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
491
492
  try {
492
493
  const res = await fetch(proxyUrl, {
493
494
  method: "DELETE",
@@ -507,8 +508,8 @@ async function proxyOrchestratorJson(orchestratorId: string, path: string, metho
507
508
  if (!orch.apiUrl) return error("orchestrator does not expose an API", 422);
508
509
  if (orch.status !== "online") return error("orchestrator is offline", 422);
509
510
  const headers: Record<string, string> = { "Content-Type": "application/json" };
510
- const relayToken = process.env.AGENT_RELAY_TOKEN;
511
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
511
+ const token = relayToken();
512
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
512
513
  try {
513
514
  const res = await fetch(`${orch.apiUrl}${path}`, {
514
515
  method,
@@ -5,6 +5,7 @@ import { authorizeRoute, cleanJsonArray, error, json, type Handler } from "./_sh
5
5
  import { cleanStringArray } from "../validation";
6
6
  import { emitOrchestratorStatus } from "../sse";
7
7
  import { type OrchestratorRuntimeInput, type SpawnProvider } from "../types";
8
+ import { relayToken } from "../config";
8
9
 
9
10
  export const getOrchestratorDirectories: Handler = async (req, params) => {
10
11
  const orch = getOrchestrator(params.id!);
@@ -63,8 +64,8 @@ async function proxyOrchestratorGet(req: Request, orchestratorId: string, path:
63
64
  const incoming = new URL(req.url);
64
65
  const proxyUrl = `${orch.apiUrl}${path}${incoming.search}`;
65
66
  const headers: Record<string, string> = {};
66
- const relayToken = process.env.AGENT_RELAY_TOKEN;
67
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
67
+ const token = relayToken();
68
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
68
69
  try {
69
70
  const res = await fetch(proxyUrl, { headers, signal: AbortSignal.timeout(10_000) });
70
71
  const contentType = res.headers.get("content-type") ?? "";
@@ -88,8 +89,8 @@ async function proxyOrchestratorPost(req: Request, orchestratorId: string, path:
88
89
  const headers: Record<string, string> = {
89
90
  "Content-Type": req.headers.get("content-type") ?? "application/json",
90
91
  };
91
- const relayToken = process.env.AGENT_RELAY_TOKEN;
92
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
92
+ const token = relayToken();
93
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
93
94
  try {
94
95
  const body = await req.text();
95
96
  const res = await fetch(proxyUrl, {
@@ -13,6 +13,7 @@ import { resolve } from "node:path";
13
13
  import { type WorkspaceAutoMergePolicy, type WorkspaceDiagnostics, type WorkspaceGitState, type WorkspaceMergeStrategy, type WorkspaceRecord, type WorkspaceStatus } from "../types";
14
14
  import { AUTO_MERGE_POLICIES } from "../workspace-merge";
15
15
  import { workspaceActiveClaim } from "../workspace-claim";
16
+ import { relayToken } from "../config";
16
17
 
17
18
  export const getWorkspaces: Handler = (req) => {
18
19
  try {
@@ -50,8 +51,8 @@ async function proxyWorkspaceHostGet(workspaceId: string, hostPath: string, extr
50
51
  if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
51
52
  for (const [key, value] of Object.entries(extraQuery ?? {})) query.set(key, value);
52
53
  const headers: Record<string, string> = {};
53
- const relayToken = process.env.AGENT_RELAY_TOKEN;
54
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
54
+ const token = relayToken();
55
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
55
56
  try {
56
57
  const res = await fetch(`${orch.apiUrl}${hostPath}?${query.toString()}`, {
57
58
  headers,
@@ -145,8 +146,8 @@ async function fetchWorkspaceGitState(workspace: WorkspaceRecord): Promise<{ sta
145
146
  if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
146
147
  if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
147
148
  const headers: Record<string, string> = {};
148
- const relayToken = process.env.AGENT_RELAY_TOKEN;
149
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
149
+ const token = relayToken();
150
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
150
151
  try {
151
152
  const res = await fetch(`${orch.apiUrl}/api/workspace/state?${query.toString()}`, { headers, signal: AbortSignal.timeout(10_000) });
152
153
  if (!res.ok) return { unavailable: `host returned ${res.status}` };
package/src/security.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { AUTH_TOKEN, CORS_ORIGINS, getIntegrationTokens, type IntegrationTokenConfig } from "./config";
1
+ import {
2
+ CORS_ORIGINS,
3
+ allowUnauthenticated,
4
+ corsOriginsFromEnv,
5
+ getIntegrationTokens,
6
+ relayToken,
7
+ tokenSecretFromEnv,
8
+ type IntegrationTokenConfig,
9
+ } from "./config";
2
10
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
12
  import { dirname, join, resolve } from "node:path";
@@ -14,7 +22,7 @@ export function isLoopbackHost(hostname: string): boolean {
14
22
  }
15
23
 
16
24
  export function assertSafeNetworkConfig(host: string): void {
17
- if (authToken() || process.env.AGENT_RELAY_ALLOW_UNAUTH === "1") return;
25
+ if (authToken() || allowUnauthenticated()) return;
18
26
  if (isLoopbackHost(host)) return;
19
27
  throw new Error(
20
28
  `Refusing to bind unauthenticated relay on ${host}. Set AGENT_RELAY_TOKEN or AGENT_RELAY_ALLOW_UNAUTH=1.`,
@@ -407,7 +415,7 @@ export function unauthorized(req: Request): Response {
407
415
  }
408
416
 
409
417
  function authToken(): string {
410
- return process.env.AGENT_RELAY_TOKEN ?? AUTH_TOKEN;
418
+ return relayToken();
411
419
  }
412
420
 
413
421
  function extractToken(req: Request): string | null {
@@ -417,7 +425,7 @@ function extractToken(req: Request): string | null {
417
425
  }
418
426
 
419
427
  function tokenSecret(): string {
420
- const env = process.env.AGENT_RELAY_TOKEN_SECRET;
428
+ const env = tokenSecretFromEnv();
421
429
  if (env) return env;
422
430
  const path = join(process.env.HOME || ".", ".agent-relay", "token-secret");
423
431
  if (existsSync(path)) return readFileSync(path, "utf8").trim();
@@ -542,7 +550,5 @@ function isTokenRevoked(jti: string): boolean {
542
550
  }
543
551
 
544
552
  function corsOrigins(): string[] {
545
- const raw = process.env.AGENT_RELAY_CORS_ORIGINS;
546
- if (raw === undefined) return CORS_ORIGINS;
547
- return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
553
+ return corsOriginsFromEnv() ?? CORS_ORIGINS;
548
554
  }
@@ -9,6 +9,7 @@ import {
9
9
  import { getIssueRef, resolveIssueRefByExternalKey, resolveIssueRefFromString, updateIssueRef } from "../db/issue-refs";
10
10
  import { associateIssue } from "../db/issue-associations";
11
11
  import { resolveIssueTrackerAdapter } from "../issue-tracker";
12
+ import { defaultIssueRepo } from "../config";
12
13
  import type { IssueTrackerAdapter } from "../issue-tracker";
13
14
  import { recordAndEmitIssueActivity } from "./issue-activity";
14
15
  import type { IssueAssociationEntityType, IssueProvider, IssueRef, WorkspaceRecord } from "../types";
@@ -78,10 +79,6 @@ function logFailure(opts: IssueLifecycleOptions, message: string): void {
78
79
  else console.warn(message);
79
80
  }
80
81
 
81
- function defaultIssueRepo(): string | undefined {
82
- return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
83
- }
84
-
85
82
  /**
86
83
  * `owner/repo` out of any git remote URL form: `git@github.com:owner/repo.git`,
87
84
  * `https://github.com/owner/repo.git`, `ssh://git@host/owner/repo`, trailing slash/`.git`
@@ -4,6 +4,7 @@ import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/
4
4
  import { resolveIssueRefFromString } from "../db/issue-refs";
5
5
  import { emitRelayEvent } from "../events";
6
6
  import { cleanString, cleanStringArray, optionalEnum } from "../validation";
7
+ import { defaultIssueRepo } from "../config";
7
8
  import type { AuthContext } from "./auth-context";
8
9
  import type { AgentCard, Plan, PlanEdge, PlanNode, PlanOwnerRef, PlanStatus } from "../types";
9
10
 
@@ -86,10 +87,6 @@ function cleanRefs(value: unknown): PlanNode["refs"] | undefined {
86
87
  return Object.keys(out).length ? out : undefined;
87
88
  }
88
89
 
89
- function defaultIssueRepo(): string | undefined {
90
- return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
91
- }
92
-
93
90
  function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
94
91
  if (!node.refs?.issueId) return;
95
92
  associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
package/src/upgrade.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir, hostname as osHostname } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
- import { VERSION } from "./config";
4
+ import { VERSION, cliRelayUrl, localOrchestratorIdFromEnv, relayAuthHeaders } from "./config";
5
5
  import type { RuntimeContracts, RuntimePackageMetadata } from "./contracts";
6
6
  import { defaultRuntimePrefix, runtimeBinPath } from "./runtime-prefix";
7
- import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
7
+ import { errMessage } from "agent-relay-sdk";
8
8
  import { shellEscape as shellQuote } from "agent-relay-sdk/shell-utils";
9
9
 
10
10
  export type UpgradeProvider = "auto" | "all" | "codex" | "claude" | "orchestrator";
@@ -462,9 +462,8 @@ async function npmViewVersion(spec: string): Promise<string | undefined> {
462
462
 
463
463
  async function runningServerVersion(): Promise<string | undefined> {
464
464
  try {
465
- const headers: Record<string, string> = {};
466
- if (process.env.AGENT_RELAY_TOKEN) headers[RELAY_TOKEN_HEADER] = process.env.AGENT_RELAY_TOKEN;
467
- const relayUrl = (process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850").replace(/\/+$/, "");
465
+ const headers = relayAuthHeaders();
466
+ const relayUrl = cliRelayUrl().replace(/\/+$/, "");
468
467
  const response = await fetch(`${relayUrl}/api/stats`, { headers });
469
468
  if (!response.ok) return undefined;
470
469
  const payload = await response.json() as { version?: string };
@@ -476,9 +475,8 @@ async function runningServerVersion(): Promise<string | undefined> {
476
475
 
477
476
  async function runningOrchestrators(): Promise<UpgradeSnapshot["runningOrchestrators"]> {
478
477
  try {
479
- const headers: Record<string, string> = {};
480
- if (process.env.AGENT_RELAY_TOKEN) headers[RELAY_TOKEN_HEADER] = process.env.AGENT_RELAY_TOKEN;
481
- const relayUrl = (process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850").replace(/\/+$/, "");
478
+ const headers = relayAuthHeaders();
479
+ const relayUrl = cliRelayUrl().replace(/\/+$/, "");
482
480
  const response = await fetch(`${relayUrl}/api/orchestrators`, { headers });
483
481
  if (!response.ok) return [];
484
482
  const payload = await response.json() as Array<{
@@ -588,7 +586,7 @@ export function resolveLocalOrchestratorId(homeDirOverride?: string): string {
588
586
  // Unparsable config — fall through to env/hostname.
589
587
  }
590
588
  }
591
- const envId = process.env.AGENT_RELAY_ORCHESTRATOR_ID?.trim();
589
+ const envId = localOrchestratorIdFromEnv();
592
590
  if (envId) return envId;
593
591
  return osHostname().replace(/\./g, "-");
594
592
  }