@electric-ax/agents 0.3.0 → 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.
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import { createServer } from "node:http";
4
3
  import fs from "node:fs";
5
4
  import pino from "pino";
6
5
  import { fileURLToPath } from "node:url";
7
- import { completeWithLowCostModel, createEntityRegistry, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
6
+ import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
8
7
  import { eq, not, queryOnce } from "@durable-streams/state";
9
8
  import { z } from "zod";
10
9
  import { createHash } from "node:crypto";
@@ -22,8 +21,12 @@ const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(
22
21
  fs.mkdirSync(LOG_DIR, { recursive: true });
23
22
  const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
24
23
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
25
- const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST;
26
- const streams = [{ stream: pino.destination(LOG_FILE) }];
24
+ const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
25
+ const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
26
+ const streams = [{ stream: pino.destination({
27
+ dest: LOG_FILE,
28
+ sync: IS_ELECTRON_MAIN
29
+ }) }];
27
30
  if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
28
31
  target: `pino-pretty`,
29
32
  options: {
@@ -622,8 +625,8 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
622
625
  logPrefix: `[horton-docs]`
623
626
  });
624
627
  function resolveCurrentQuestion(wake, events, inbox) {
625
- if (wake.type === `message_received`) {
626
- const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `message_received`).map((event) => event.value));
628
+ if (wake.type === `inbox`) {
629
+ const eventQuestion = findLatestQuestion(events.filter((event) => event.type === `inbox`).map((event) => event.value));
627
630
  if (eventQuestion) return eventQuestion;
628
631
  }
629
632
  const wakeQuestion = payloadToText(wake.payload).trim();
@@ -1423,7 +1426,8 @@ function registerHorton(registry, options) {
1423
1426
  const { workingDirectory, streamFn, skillsRegistry = null, modelCatalog } = options;
1424
1427
  const docsUrl = options.docsUrl ?? process.env.HORTON_DOCS_URL;
1425
1428
  if (process.env.BRAVE_SEARCH_API_KEY) serverLog.info(`[horton] Web search: using Brave Search API`);
1426
- else serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY not set — web search will fall back to Anthropic built-in search (uses your ANTHROPIC_API_KEY)`);
1429
+ else if (process.env.ANTHROPIC_API_KEY) serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY not set — web search will fall back to Anthropic built-in search`);
1430
+ else serverLog.warn(`[horton] BRAVE_SEARCH_API_KEY and ANTHROPIC_API_KEY not set — web search tool will be unavailable`);
1427
1431
  const docsSupport = createHortonDocsSupport(workingDirectory);
1428
1432
  const docsSearchTool = docsSupport?.createSearchTool();
1429
1433
  docsSupport?.ensureReady().catch((error) => {
@@ -1898,9 +1902,8 @@ function truncate(str, max) {
1898
1902
 
1899
1903
  //#endregion
1900
1904
  //#region src/bootstrap.ts
1901
- const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
1902
1905
  async function createBuiltinAgentHandler(options) {
1903
- const { agentServerUrl, serveEndpoint = `${agentServerUrl}${DEFAULT_BUILTIN_AGENT_HANDLER_PATH}`, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName } = options;
1906
+ const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName, baseSkillsDir: baseSkillsDirOverride, serverHeaders, defaultDispatchPolicyForType } = options;
1904
1907
  const modelCatalog = await createBuiltinModelCatalog({ allowMockFallback: Boolean(streamFn) });
1905
1908
  if (!modelCatalog) {
1906
1909
  serverLog.warn(`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`);
@@ -1908,7 +1911,7 @@ async function createBuiltinAgentHandler(options) {
1908
1911
  }
1909
1912
  const cwd = workingDirectory ?? process.cwd();
1910
1913
  const here = path.dirname(fileURLToPath(import.meta.url));
1911
- const baseSkillsDir = path.resolve(here, `../skills`);
1914
+ const baseSkillsDir = baseSkillsDirOverride ?? path.resolve(here, `../skills`);
1912
1915
  let skillsRegistry = null;
1913
1916
  try {
1914
1917
  skillsRegistry = await createSkillsRegistry({
@@ -1938,6 +1941,8 @@ async function createBuiltinAgentHandler(options) {
1938
1941
  serveEndpoint,
1939
1942
  registry,
1940
1943
  subscriptionPathForType: (name) => `/${name}/*/main`,
1944
+ defaultDispatchPolicyForType,
1945
+ serverHeaders,
1941
1946
  idleTimeout: 5e3,
1942
1947
  createElectricTools,
1943
1948
  publicUrl,
@@ -1959,15 +1964,13 @@ async function registerBuiltinAgentTypes(bootstrap) {
1959
1964
  //#endregion
1960
1965
  //#region src/server.ts
1961
1966
  var BuiltinAgentsServer = class {
1962
- server = null;
1963
1967
  bootstrap = null;
1964
- _url = null;
1965
- publicBaseUrl = null;
1966
1968
  _mcpRegistry = null;
1967
1969
  mcpWatcherCloser = null;
1968
1970
  mcpToolProviderName = null;
1969
1971
  mcpApplyInFlight = new Set();
1970
1972
  mcpStopping = false;
1973
+ pullWakeRunner = null;
1971
1974
  options;
1972
1975
  constructor(options) {
1973
1976
  this.options = options;
@@ -1976,171 +1979,167 @@ var BuiltinAgentsServer = class {
1976
1979
  get mcpRegistry() {
1977
1980
  return this._mcpRegistry;
1978
1981
  }
1979
- get url() {
1980
- if (!this._url) throw new Error(`Builtin agents server not started`);
1981
- return this._url;
1982
- }
1983
- get registeredBaseUrl() {
1984
- if (!this.publicBaseUrl) throw new Error(`Builtin agents server not started`);
1985
- return this.publicBaseUrl;
1986
- }
1987
1982
  async start() {
1988
- if (this.server) throw new Error(`Builtin agents server already started`);
1989
- return new Promise((resolve, reject) => {
1990
- this.server = createServer((req, res) => {
1991
- this.handleRequest(req, res).catch((error) => {
1992
- serverLog.error(`[builtin-agents] unhandled request error`, error);
1993
- if (!res.headersSent) {
1994
- res.writeHead(500, { "content-type": `application/json` });
1995
- res.end(JSON.stringify({ error: `Internal server error` }));
1996
- }
1997
- });
1983
+ if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1984
+ const pullWake = this.options.pullWake;
1985
+ if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1986
+ try {
1987
+ const publicUrl = this.options.mcpOAuthRedirectBase ?? this.options.agentServerUrl;
1988
+ const mcpRegistry = createRegistry({
1989
+ publicUrl,
1990
+ openAuthorizeUrl: this.options.openAuthorizeUrl
1998
1991
  });
1999
- this.server.on(`error`, reject);
2000
- const host = this.options.host ?? `127.0.0.1`;
2001
- this.server.listen(this.options.port, host, async () => {
2002
- try {
2003
- const addr = this.server.address();
2004
- if (typeof addr === `string`) this._url = addr;
2005
- else if (addr) {
2006
- const resolvedHost = host === `0.0.0.0` ? `127.0.0.1` : host;
2007
- this._url = `http://${resolvedHost}:${addr.port}`;
2008
- } else throw new Error(`Could not determine builtin agents server address`);
2009
- this.publicBaseUrl = this.options.baseUrl ?? this._url;
2010
- const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
2011
- const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
2012
- const publicUrl = this.options.mcpOAuthRedirectBase ?? this.publicBaseUrl;
2013
- const mcpRegistry = createRegistry({
2014
- publicUrl,
2015
- openAuthorizeUrl: this.options.openAuthorizeUrl
2016
- });
2017
- this._mcpRegistry = mcpRegistry;
2018
- const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
2019
- const extras = this.options.extraMcpServers ?? [];
2020
- const wirePersistence = async (cfg) => {
2021
- const servers = [];
2022
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
2023
- const persist = await keychainPersistence({ server: s.name });
2024
- servers.push({
2025
- ...s,
2026
- auth: {
2027
- ...s.auth,
2028
- ...persist
2029
- }
2030
- });
2031
- } else servers.push(s);
2032
- return {
2033
- ...cfg,
2034
- servers
2035
- };
2036
- };
2037
- const merge = (jsonCfg) => {
2038
- const jsonServers = jsonCfg?.servers ?? [];
2039
- const jsonNames = new Set(jsonServers.map((s) => s.name));
2040
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
2041
- return {
2042
- servers: [...filteredExtras, ...jsonServers],
2043
- raw: jsonCfg?.raw
2044
- };
2045
- };
2046
- const onConfigError = this.options.onConfigError;
2047
- const runApply = async (jsonCfg) => {
2048
- if (this.mcpStopping) return;
2049
- try {
2050
- const wired = await wirePersistence(merge(jsonCfg));
2051
- if (this.mcpStopping) return;
2052
- await mcpRegistry.applyConfig(wired);
2053
- } catch (e) {
2054
- serverLog.error(`[mcp] applyConfig:`, e);
2055
- try {
2056
- onConfigError?.(e);
2057
- } catch (cbErr) {
2058
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
2059
- }
2060
- }
2061
- };
2062
- const applyMerged = (jsonCfg) => {
2063
- const p = runApply(jsonCfg);
2064
- this.mcpApplyInFlight.add(p);
2065
- p.finally(() => this.mcpApplyInFlight.delete(p));
2066
- return p;
2067
- };
2068
- if (mcpConfigPath) {
2069
- try {
2070
- const cfg = await loadConfig(mcpConfigPath, process.env);
2071
- applyMerged(cfg);
2072
- } catch (err) {
2073
- if (err.code !== `ENOENT`) throw err;
2074
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
2075
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
2076
- applyMerged(null);
2077
- }
2078
- try {
2079
- this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
2080
- onChange: (cfg) => void applyMerged(cfg),
2081
- onError: (e) => serverLog.error(`[mcp] config error:`, e)
2082
- });
2083
- } catch (e) {
2084
- serverLog.error(`[mcp] config watcher failed to start:`, e);
2085
- }
2086
- } else {
2087
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
2088
- applyMerged(null);
2089
- }
2090
- this.mcpToolProviderName = `mcp`;
2091
- registerToolProvider({
2092
- name: `mcp`,
2093
- tools: () => {
2094
- const tools = [];
2095
- for (const entry of mcpRegistry.list()) {
2096
- if (entry.status !== `ready`) continue;
2097
- const live = mcpRegistry.get(entry.name);
2098
- if (!live?.transport) continue;
2099
- for (const t of entry.tools) tools.push(bridgeMcpTool({
2100
- server: entry.name,
2101
- tool: t,
2102
- client: live.transport.client,
2103
- timeoutMs: live.config.timeoutMs
2104
- }));
2105
- const caps = live.transport.client.getServerCapabilities?.();
2106
- if (caps?.resources) tools.push(...buildResourceTools({
2107
- server: entry.name,
2108
- client: live.transport.client,
2109
- timeoutMs: live.config.timeoutMs
2110
- }));
2111
- if (caps?.prompts) tools.push(...buildPromptTools({
2112
- server: entry.name,
2113
- client: live.transport.client,
2114
- timeoutMs: live.config.timeoutMs
2115
- }));
2116
- }
2117
- return tools;
1992
+ this._mcpRegistry = mcpRegistry;
1993
+ const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1994
+ const extras = this.options.extraMcpServers ?? [];
1995
+ const wirePersistence = async (cfg) => {
1996
+ const servers = [];
1997
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1998
+ const persist = await keychainPersistence({ server: s.name });
1999
+ servers.push({
2000
+ ...s,
2001
+ auth: {
2002
+ ...s.auth,
2003
+ ...persist
2118
2004
  }
2119
2005
  });
2120
- this.bootstrap = await createBuiltinAgentHandler({
2121
- agentServerUrl: this.options.agentServerUrl,
2122
- serveEndpoint,
2123
- workingDirectory: this.options.workingDirectory,
2124
- streamFn: this.options.mockStreamFn,
2125
- createElectricTools: this.options.createElectricTools,
2126
- publicUrl,
2127
- runtimeName: `builtin-agents`
2006
+ } else servers.push(s);
2007
+ return {
2008
+ ...cfg,
2009
+ servers
2010
+ };
2011
+ };
2012
+ const merge = (jsonCfg) => {
2013
+ const jsonServers = jsonCfg?.servers ?? [];
2014
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
2015
+ const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
2016
+ return {
2017
+ servers: [...filteredExtras, ...jsonServers],
2018
+ raw: jsonCfg?.raw
2019
+ };
2020
+ };
2021
+ const onConfigError = this.options.onConfigError;
2022
+ const runApply = async (jsonCfg) => {
2023
+ if (this.mcpStopping) return;
2024
+ try {
2025
+ const wired = await wirePersistence(merge(jsonCfg));
2026
+ if (this.mcpStopping) return;
2027
+ await mcpRegistry.applyConfig(wired);
2028
+ } catch (e) {
2029
+ serverLog.error(`[mcp] applyConfig:`, e);
2030
+ try {
2031
+ onConfigError?.(e);
2032
+ } catch (cbErr) {
2033
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
2034
+ }
2035
+ }
2036
+ };
2037
+ const applyMerged = (jsonCfg) => {
2038
+ const p = runApply(jsonCfg);
2039
+ this.mcpApplyInFlight.add(p);
2040
+ p.finally(() => this.mcpApplyInFlight.delete(p));
2041
+ return p;
2042
+ };
2043
+ if (mcpConfigPath) {
2044
+ try {
2045
+ const cfg = await loadConfig(mcpConfigPath, process.env);
2046
+ applyMerged(cfg);
2047
+ } catch (err) {
2048
+ if (err.code !== `ENOENT`) throw err;
2049
+ if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
2050
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
2051
+ applyMerged(null);
2052
+ }
2053
+ try {
2054
+ this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
2055
+ onChange: (cfg) => void applyMerged(cfg),
2056
+ onError: (e) => serverLog.error(`[mcp] config error:`, e)
2128
2057
  });
2129
- if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
2130
- await registerBuiltinAgentTypes(this.bootstrap);
2131
- serverLog.info(`[builtin-agents] webhook handler listening at ${serveEndpoint}`);
2132
- resolve(this._url);
2133
- } catch (error) {
2134
- await this.stop().catch(() => {});
2135
- reject(error);
2058
+ } catch (e) {
2059
+ serverLog.error(`[mcp] config watcher failed to start:`, e);
2060
+ }
2061
+ } else {
2062
+ if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
2063
+ applyMerged(null);
2064
+ }
2065
+ this.mcpToolProviderName = `mcp`;
2066
+ registerToolProvider({
2067
+ name: `mcp`,
2068
+ tools: () => {
2069
+ const tools = [];
2070
+ for (const entry of mcpRegistry.list()) {
2071
+ if (entry.status !== `ready`) continue;
2072
+ const live = mcpRegistry.get(entry.name);
2073
+ if (!live?.transport) continue;
2074
+ for (const t of entry.tools) tools.push(bridgeMcpTool({
2075
+ server: entry.name,
2076
+ tool: t,
2077
+ client: live.transport.client,
2078
+ timeoutMs: live.config.timeoutMs
2079
+ }));
2080
+ const caps = live.transport.client.getServerCapabilities?.();
2081
+ if (caps?.resources) tools.push(...buildResourceTools({
2082
+ server: entry.name,
2083
+ client: live.transport.client,
2084
+ timeoutMs: live.config.timeoutMs
2085
+ }));
2086
+ if (caps?.prompts) tools.push(...buildPromptTools({
2087
+ server: entry.name,
2088
+ client: live.transport.client,
2089
+ timeoutMs: live.config.timeoutMs
2090
+ }));
2091
+ }
2092
+ return tools;
2136
2093
  }
2137
2094
  });
2138
- });
2095
+ this.bootstrap = await createBuiltinAgentHandler({
2096
+ agentServerUrl: this.options.agentServerUrl,
2097
+ workingDirectory: this.options.workingDirectory,
2098
+ streamFn: this.options.mockStreamFn,
2099
+ createElectricTools: this.options.createElectricTools,
2100
+ publicUrl,
2101
+ runtimeName: `builtin-agents`,
2102
+ baseSkillsDir: this.options.baseSkillsDir,
2103
+ serverHeaders: pullWake.headers
2104
+ });
2105
+ if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
2106
+ await registerBuiltinAgentTypes(this.bootstrap);
2107
+ const registeredRunner = pullWake.registerRunner ? await this.registerPullWakeRunner(pullWake) : null;
2108
+ this.pullWakeRunner = createPullWakeRunner({
2109
+ baseUrl: this.options.agentServerUrl,
2110
+ runnerId: pullWake.runnerId,
2111
+ runtime: this.bootstrap.runtime,
2112
+ headers: pullWake.headers,
2113
+ claimHeaders: pullWake.claimHeaders,
2114
+ claimTokenHeader: pullWake.claimTokenHeader,
2115
+ heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
2116
+ leaseMs: pullWake.leaseMs,
2117
+ offset: registeredRunner?.wake_stream_offset,
2118
+ onError: (error) => {
2119
+ serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
2120
+ return true;
2121
+ }
2122
+ });
2123
+ this.pullWakeRunner.start();
2124
+ serverLog.info(`[builtin-agents] pull-wake runner started: ${pullWake.runnerId}`);
2125
+ return `pull-wake:${pullWake.runnerId}`;
2126
+ } catch (error) {
2127
+ await this.stop().catch(() => {});
2128
+ throw error;
2129
+ }
2139
2130
  }
2140
2131
  async stop() {
2132
+ if (this.pullWakeRunner) {
2133
+ await this.pullWakeRunner.stop().catch((e) => {
2134
+ serverLog.error(`[builtin-agents] pull-wake runner stop failed`, e);
2135
+ });
2136
+ this.pullWakeRunner = null;
2137
+ }
2141
2138
  if (this.bootstrap) {
2142
2139
  this.bootstrap.runtime.abortWakes();
2143
- await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2140
+ await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
2141
+ serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
2142
+ }), new Promise((resolve) => setTimeout(resolve, 5e3))]);
2144
2143
  this.bootstrap = null;
2145
2144
  }
2146
2145
  this.mcpStopping = true;
@@ -2163,39 +2162,29 @@ var BuiltinAgentsServer = class {
2163
2162
  });
2164
2163
  this._mcpRegistry = null;
2165
2164
  }
2166
- if (this.server) {
2167
- const server = this.server;
2168
- await new Promise((resolve) => {
2169
- server.close(() => resolve());
2170
- });
2171
- this.server = null;
2172
- }
2173
2165
  this.mcpStopping = false;
2174
- this._url = null;
2175
- this.publicBaseUrl = null;
2176
2166
  }
2177
- async handleRequest(req, res) {
2178
- const method = req.method?.toUpperCase();
2179
- const pathname = new URL(req.url ?? `/`, `http://localhost`).pathname;
2180
- const webhookPath = this.options.webhookPath ?? DEFAULT_BUILTIN_AGENT_HANDLER_PATH;
2181
- if (pathname === `/_electric/health` && method === `GET`) {
2182
- res.writeHead(200, { "content-type": `application/json` });
2183
- res.end(JSON.stringify({ status: `ok` }));
2184
- return;
2185
- }
2186
- if (pathname === webhookPath && method === `POST` && this.bootstrap) {
2187
- await this.bootstrap.handler(req, res);
2188
- return;
2189
- }
2190
- res.writeHead(404, { "content-type": `application/json` });
2191
- res.end(JSON.stringify({ error: `Not found` }));
2167
+ async registerPullWakeRunner(pullWake) {
2168
+ const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
2169
+ headers.set(`content-type`, `application/json`);
2170
+ const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
2171
+ method: `POST`,
2172
+ headers,
2173
+ body: JSON.stringify({
2174
+ id: pullWake.runnerId,
2175
+ owner_user_id: pullWake.ownerUserId,
2176
+ label: pullWake.label ?? `Built-in agents`,
2177
+ kind: `local`,
2178
+ admin_status: `enabled`
2179
+ })
2180
+ });
2181
+ if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
2182
+ return await response.json();
2192
2183
  }
2193
2184
  };
2194
2185
 
2195
2186
  //#endregion
2196
2187
  //#region src/entrypoint-lib.ts
2197
- const DEFAULT_HOST = `127.0.0.1`;
2198
- const DEFAULT_PORT = 4448;
2199
2188
  function readEnv(env, names) {
2200
2189
  for (const name of names) {
2201
2190
  const value = env[name]?.trim();
@@ -2208,13 +2197,6 @@ function readRequiredEnv(env, names, description) {
2208
2197
  if (value) return value;
2209
2198
  throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
2210
2199
  }
2211
- function readPort(env) {
2212
- const raw = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_PORT`, `PORT`]);
2213
- if (!raw) return DEFAULT_PORT;
2214
- const port = Number(raw);
2215
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid builtin agents port "${raw}". Expected an integer between 1 and 65535.`);
2216
- return port;
2217
- }
2218
2200
  function validateUrl(name, value) {
2219
2201
  try {
2220
2202
  new URL(value);
@@ -2223,20 +2205,55 @@ function validateUrl(name, value) {
2223
2205
  throw new Error(`Invalid ${name}: "${value}"`);
2224
2206
  }
2225
2207
  }
2208
+ function parseAdditionalServerHeaders(env) {
2209
+ const raw = readEnv(env, [`ELECTRIC_AGENTS_SERVER_HEADERS`]);
2210
+ if (!raw) return void 0;
2211
+ let parsed;
2212
+ try {
2213
+ parsed = JSON.parse(raw);
2214
+ } catch {
2215
+ throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected JSON`);
2216
+ }
2217
+ if (!parsed || typeof parsed !== `object` || Array.isArray(parsed)) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected a JSON object`);
2218
+ const headers = new Headers();
2219
+ for (const [name, value] of Object.entries(parsed)) {
2220
+ if (typeof value !== `string`) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: header "${name}" must be a string`);
2221
+ headers.set(name, value);
2222
+ }
2223
+ const normalized = Object.fromEntries(headers.entries());
2224
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
2225
+ }
2226
+ function mergeHeaders(...sources) {
2227
+ const headers = new Headers();
2228
+ for (const source of sources) {
2229
+ if (!source) continue;
2230
+ new Headers(source).forEach((value, key) => headers.set(key, value));
2231
+ }
2232
+ const merged = Object.fromEntries(headers.entries());
2233
+ return Object.keys(merged).length > 0 ? merged : void 0;
2234
+ }
2235
+ function hasHeader(headers, name) {
2236
+ return headers ? new Headers(headers).has(name) : false;
2237
+ }
2226
2238
  function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
2227
2239
  const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
2228
- const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_BASE_URL`, `BUILTIN_AGENTS_BASE_URL`]);
2240
+ const runnerId = readRequiredEnv(env, [`ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`, `PULL_WAKE_RUNNER_ID`], `pull-wake runner id`);
2241
+ const serverHeaders = mergeHeaders(parseAdditionalServerHeaders(env));
2229
2242
  return {
2230
2243
  agentServerUrl,
2231
- baseUrl: baseUrl ? validateUrl(`builtin agents base URL`, baseUrl) : void 0,
2232
- host: readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_HOST`, `HOST`]) ?? DEFAULT_HOST,
2233
- port: readPort(env),
2234
- workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd
2244
+ workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd,
2245
+ pullWake: {
2246
+ runnerId,
2247
+ registerRunner: readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `true` || readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `1`,
2248
+ headers: serverHeaders,
2249
+ claimHeaders: serverHeaders,
2250
+ claimTokenHeader: hasHeader(serverHeaders, `authorization`) ? `electric-claim-token` : void 0
2251
+ }
2235
2252
  };
2236
2253
  }
2237
- async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer: createServer$1 = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
2254
+ async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
2238
2255
  const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
2239
- const server = createServer$1(options);
2256
+ const server = createServer(options);
2240
2257
  const url = await server.start();
2241
2258
  return {
2242
2259
  options,
@@ -2260,10 +2277,9 @@ async function main() {
2260
2277
  try {
2261
2278
  const started = await runBuiltinAgentsEntrypoint();
2262
2279
  server = started.server;
2263
- console.log(`Builtin agents server running at ${started.url}`);
2280
+ console.log(`Builtin agents pull-wake runner started at ${started.url}`);
2264
2281
  console.log(`Registering against: ${started.options.agentServerUrl}`);
2265
2282
  console.log(`Working directory: ${started.options.workingDirectory}`);
2266
- if (started.options.baseUrl) console.log(`Public webhook base URL: ${started.options.baseUrl}`);
2267
2283
  process.on(`SIGINT`, () => {
2268
2284
  stop(0);
2269
2285
  });