workerdeck 3.2.0 → 3.4.0

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/build/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-BmQhZ3Px.mjs";
2
+ import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-DyBeVgsK.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
@@ -42,6 +42,7 @@ Usage
42
42
  workerdeck [options]
43
43
  workerdeck guard [options] check whether it is safe to restart an instance
44
44
  workerdeck reload [options] hot-reload a gateway started with --hot-reload
45
+ workerdeck relay <command> run or manage a cross-gateway peer relay (relay --help)
45
46
 
46
47
  Options
47
48
  -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)
@@ -104,6 +105,22 @@ Options
104
105
  config: approvalTimeoutMs). Default 'none' - a prompt
105
106
  waits as long as the session lives. A session may
106
107
  override it per request.
108
+ --engine-sleep-after <d>
109
+ stop an idle session's engine process once no client
110
+ has watched it for this long (30m, 2h; config:
111
+ engineSleepAfterMs). The session stays listed and
112
+ wakes on its next message. Default 'never'.
113
+ --effort-default <model>=<effort>
114
+ reasoning effort a session starts at, or switches to,
115
+ on this model (opus=high, gpt-6-sol=xhigh; repeatable;
116
+ config: effortDefaults). A profile's defaults.efforts
117
+ wins per model. Default: the engine's own.
118
+ --agent-context-reset <on|off|never>
119
+ whether sessions get the context_reset tool, which
120
+ lets an agent clear its own conversation between turns
121
+ and continue from a prompt it wrote (config:
122
+ agentContextReset). A request or profile may turn it
123
+ on per session; 'never' forbids it. Default 'off'.
107
124
  --state-dir <path> where parked sessions are persisted
108
125
  (default: beside the config file, else ~/.workerdeck)
109
126
  --no-parking-store keep parked sessions in memory only; a restart drops them
@@ -169,6 +186,7 @@ function openInBrowser(url) {
169
186
  function takeGatewayEnv() {
170
187
  const env = { ...process.env };
171
188
  delete process.env.WORKERDECK_AUTH_KEY;
189
+ delete process.env.WORKERDECK_RELAY_KEY;
172
190
  return env;
173
191
  }
174
192
  function line(text) {
@@ -176,9 +194,13 @@ function line(text) {
176
194
  }
177
195
  async function main(argv) {
178
196
  if (argv[0] === "reload") {
179
- const { runReload } = await import("./reload-command-CSaFgxYs.mjs");
197
+ const { runReload } = await import("./reload-command-BB_HdGxV.mjs");
180
198
  return await runReload(argv.slice(1));
181
199
  }
200
+ if (argv[0] === "relay") {
201
+ const { runRelayCli } = await import("@workerdeck/relay");
202
+ return await runRelayCli(argv.slice(1));
203
+ }
182
204
  if (argv[0] === "guard") {
183
205
  const { runGuard } = await import("./guard-C4ybpzbt.mjs").then((n) => n.t);
184
206
  return await runGuard(argv.slice(1));
@@ -194,7 +216,7 @@ async function main(argv) {
194
216
  return 0;
195
217
  }
196
218
  if (flags.hotReload) {
197
- const { runHotReload } = await import("./hot-reload-Duq3g2Lm.mjs");
219
+ const { runHotReload } = await import("./hot-reload-BU48Y18z.mjs");
198
220
  const result = await runHotReload(flags, env);
199
221
  if (result !== "unsupported") return result;
200
222
  }
package/build/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/lib/shutdown.ts","../src/cli.ts"],"sourcesContent":["import type { Instance } from './instance.ts'\n\nexport type ShutdownOptions = {\n // A getter, not an instance: under --hot-reload the instance is replaced on every reload, and a captured one\n // would drain a server that stopped listening several generations ago.\n current: () => Instance\n // Runners this process holds outside any instance, which under --hot-reload is what a failed generation leaves\n // behind: no registry holds them, so `close()` cannot reach them and nothing else ends their engine children.\n alsoClose?: () => { close: (reason?: 'client' | 'server' | 'error') => void }[]\n log?: (text: string) => void\n}\n\nexport function installShutdown({\n current,\n alsoClose,\n log = (text) => void process.stdout.write(`${text}\\n`),\n}: ShutdownOptions): (signal: string) => void {\n // A second signal must always be able to kill a shutdown that is taking too long. That used to work only by\n // accident - the second call re-entered `instance.close()` and got an immediate callback out of an already-closed\n // http server - so it evaporated the moment close stopped being idempotent-by-luck. Make it a real path.\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n process.stdout.write(`\\n[workerdeck] ${signal} again - terminating now\\n`)\n // Shell children are detached group leaders, so a plain exit orphans every one of them.\n current().server.shells?.killAllSync()\n process.exit(130)\n }\n shuttingDown = true\n log(`\\n[workerdeck] ${signal} - shutting down (press again to stop now)`)\n const instance = current()\n const running = instance.server.shells?.running() ?? []\n if (running.length > 0) {\n const named = running.map((shell) => `#${shell.ordinal} ${shell.label} (session ${shell.sessionId.slice(0, 8)})`).join(', ')\n log(`[workerdeck] ${running.length} shell(s) still running and will be stopped: ${named}`)\n }\n instance\n .drain({\n onProgress: (report) => {\n if (report.working.length > 0) {\n log(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`)\n }\n // Named, not waited for: nothing about shutting down answers a permission prompt.\n for (const id of report.awaitingHuman) {\n log(`[workerdeck] session ${id} is waiting on an approval - not waiting for it`)\n }\n if (report.timedOut) {\n log(`[workerdeck] ${report.working.length} session(s) still running - stopping anyway`)\n } else if (report.working.length === 0) {\n log('[workerdeck] all turns finished')\n }\n },\n })\n .catch(() => undefined)\n .then(() => instance.close())\n .then(() => {\n for (const runner of alsoClose?.() ?? []) {\n runner.close('server')\n }\n })\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n // Returned for the one caller that cannot rely on the signal: a raw-mode TTY delivers ctrl-c as a byte, not SIGINT.\n return shutdown\n}\n","#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './lib/instance.ts'\nimport { installShutdown } from './lib/shutdown.ts'\n\nconst HELP = `workerdeck - run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n workerdeck reload [options] hot-reload a gateway started with --hot-reload\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth - no key demanded,\n none generated - and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable). Declared\n this way a profile is fixed: it cannot be changed over the API.\n --profile-root <path> directory a profile created at runtime may point its config dir at\n (repeatable; default: your home directory). Narrow this to pin\n exactly which credential stores the dashboard may reach for.\n --no-profile-store refuse runtime profile management entirely - /v1/profiles stays\n read-only and profiles come from --profile and the config file\n alone. Profiles created at runtime are stored in\n <state-dir>/profiles.json, so --no-parking-store implies this.\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --fs-root <path> narrow which host directories /v1/fs serves\n (repeatable, WORKERDECK_FS_ROOTS as a ':'-separated\n list). Reading otherwise follows --cwd-root: a caller\n who may start a session in a tree can already read it\n through the agent. With neither, the routes 404.\n --fs-write also accept writes over /v1/fs. Its own switch because\n an agent's writes go through the permission flow and a\n PUT does not. Every write is still conditional on the\n hash the client last read.\n --shell let an operator run '$' shell commands on the host from\n the composer, in the session's cwd. Every one is a PTY\n with a tracked record. Off by default and its own switch:\n unlike an agent's Bash tool, a '$' command goes through\n no permission prompt at all. Never offered to a scoped\n principal. The agent gets shell_list and shell_read.\n --shell-agent-write <mode>\n what the agent may do to shells of its own with --shell:\n read-only (default: it only reads), gated (shell_run,\n shell_write and shell_kill behind a permission card,\n payload shown; config: shell.agentWrite) or allow (no\n card beyond the engine's own mode). Never a shell the\n user started, never a session a scoped principal made.\n --approval-timeout <d>\n how long a permission prompt or a question may wait for\n an answer before the engine denies it (300000, 30s, 5m;\n config: approvalTimeoutMs). Default 'none' - a prompt\n waits as long as the session lives. A session may\n override it per request.\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required - this only permits the call.\n --no-keep-awake don't hold the machine awake while a session is waiting\n on it (config: keepAwake: false). By default a turn in\n flight, or a session blocked on an approval, blocks idle\n sleep - a laptop sleeping mid-turn drops the socket and\n burns the turn. The screen is never kept on, and the\n lock releases the moment nothing is waiting.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\n --open open the dashboard in a browser once it is up\n --hot-reload dev only: re-evaluate the gateway's own source in place on\n ctrl-r, SIGUSR2 or \\`workerdeck reload\\`, carrying live\n sessions and their engine child processes across the swap.\n Needs a source checkout; the published CLI is one bundled\n file with nothing to swap.\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line - \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions - live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\nfunction openInBrowser(url: string): void {\n const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {}\n}\n\n// Read once and removed: every process this one spawns (engines, shells, the browser opener) inherits `process.env`.\nfunction takeGatewayEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env }\n delete process.env.WORKERDECK_AUTH_KEY\n return env\n}\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'reload') {\n const { runReload } = await import('./dev/reload-command.ts')\n return await runReload(argv.slice(1))\n }\n\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const env = takeGatewayEnv()\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n if (flags.hotReload) {\n const { runHotReload } = await import('./dev/hot-reload.ts')\n const result = await runHotReload(flags, env)\n if (result !== 'unsupported') {\n return result\n }\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded, env)\n const instance = await startInstance(config)\n\n if (config.open) {\n openInBrowser(instance.url)\n }\n\n installShutdown({ current: () => instance, log: line })\n\n // Resolves only on close: until then the process stays up, serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) {\n process.exit(code)\n }\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(`[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n process.exit(1)\n })\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/lib/shutdown.ts","../src/cli.ts"],"sourcesContent":["import type { Instance } from './instance.ts'\n\nexport type ShutdownOptions = {\n // A getter, not an instance: under --hot-reload the instance is replaced on every reload, and a captured one\n // would drain a server that stopped listening several generations ago.\n current: () => Instance\n // Runners this process holds outside any instance, which under --hot-reload is what a failed generation leaves\n // behind: no registry holds them, so `close()` cannot reach them and nothing else ends their engine children.\n alsoClose?: () => { close: (reason?: 'client' | 'server' | 'error') => void }[]\n log?: (text: string) => void\n}\n\nexport function installShutdown({\n current,\n alsoClose,\n log = (text) => void process.stdout.write(`${text}\\n`),\n}: ShutdownOptions): (signal: string) => void {\n // A second signal must always be able to kill a shutdown that is taking too long. That used to work only by\n // accident - the second call re-entered `instance.close()` and got an immediate callback out of an already-closed\n // http server - so it evaporated the moment close stopped being idempotent-by-luck. Make it a real path.\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n process.stdout.write(`\\n[workerdeck] ${signal} again - terminating now\\n`)\n // Shell children are detached group leaders, so a plain exit orphans every one of them.\n current().server.shells?.killAllSync()\n process.exit(130)\n }\n shuttingDown = true\n log(`\\n[workerdeck] ${signal} - shutting down (press again to stop now)`)\n const instance = current()\n const running = instance.server.shells?.running() ?? []\n if (running.length > 0) {\n const named = running.map((shell) => `#${shell.ordinal} ${shell.label} (session ${shell.sessionId.slice(0, 8)})`).join(', ')\n log(`[workerdeck] ${running.length} shell(s) still running and will be stopped: ${named}`)\n }\n instance\n .drain({\n onProgress: (report) => {\n if (report.working.length > 0) {\n log(`[workerdeck] waiting for ${report.working.length} session(s) to finish the current turn`)\n }\n // Named, not waited for: nothing about shutting down answers a permission prompt.\n for (const id of report.awaitingHuman) {\n log(`[workerdeck] session ${id} is waiting on an approval - not waiting for it`)\n }\n if (report.timedOut) {\n log(`[workerdeck] ${report.working.length} session(s) still running - stopping anyway`)\n } else if (report.working.length === 0) {\n log('[workerdeck] all turns finished')\n }\n },\n })\n .catch(() => undefined)\n .then(() => instance.close())\n .then(() => {\n for (const runner of alsoClose?.() ?? []) {\n runner.close('server')\n }\n })\n .then(() => process.exit(0))\n .catch(() => process.exit(1))\n }\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n // Returned for the one caller that cannot rely on the signal: a raw-mode TTY delivers ctrl-c as a byte, not SIGINT.\n return shutdown\n}\n","#!/usr/bin/env node\nimport { spawn } from 'node:child_process'\nimport { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { ConfigError, loadConfigFile, parseArgs, resolveInstanceConfig } from './config.ts'\nimport { startInstance } from './lib/instance.ts'\nimport { installShutdown } from './lib/shutdown.ts'\n\nconst HELP = `workerdeck - run a workerdeck instance: session gateway + dashboard, one port.\n\nUsage\n workerdeck [options]\n workerdeck guard [options] check whether it is safe to restart an instance\n workerdeck reload [options] hot-reload a gateway started with --hot-reload\n workerdeck relay <command> run or manage a cross-gateway peer relay (relay --help)\n\nOptions\n -p, --port <n> port to listen on (default 8787, WORKERDECK_PORT)\n --host <addr> interface to bind (default 127.0.0.1, WORKERDECK_HOST)\n --auth-key <secret> shared secret, min 12 chars; browsers log in with it,\n services send it as x-workerdeck-key\n (WORKERDECK_AUTH_KEY). Unset = no auth on loopback;\n on any other interface a key is generated instead,\n printed once, and stored in <state-dir>/auth-key for\n later starts to reuse.\n --trust-proxy trust x-forwarded-proto/-host/-for from one reverse\n proxy. Required behind TLS termination, or the session\n cookie loses its Secure flag and the origin check\n computes http:// where the browser says https://.\n --allowed-origin <o> extra origin accepted on browser requests, for when a\n proxy rewrites Host (repeatable)\n --allowed-host <name> extra Host header accepted when running without auth\n (repeatable; loopback names are always accepted)\n --insecure-host <name>\n bind host that may serve without auth - no key demanded,\n none generated - and, while unauthenticated, also\n accepted as a Host header (repeatable; config:\n insecureHosts). Names the host alone, no port.\n --profile <name=dir> Claude config dir a session may run under (repeatable). Declared\n this way a profile is fixed: it cannot be changed over the API.\n --profile-root <path> directory a profile created at runtime may point its config dir at\n (repeatable; default: your home directory). Narrow this to pin\n exactly which credential stores the dashboard may reach for.\n --no-profile-store refuse runtime profile management entirely - /v1/profiles stays\n read-only and profiles come from --profile and the config file\n alone. Profiles created at runtime are stored in\n <state-dir>/profiles.json, so --no-parking-store implies this.\n --cwd-root <path> restrict session cwds to this root (repeatable,\n WORKERDECK_CWD_ROOTS as a ':'-separated list)\n --fs-root <path> narrow which host directories /v1/fs serves\n (repeatable, WORKERDECK_FS_ROOTS as a ':'-separated\n list). Reading otherwise follows --cwd-root: a caller\n who may start a session in a tree can already read it\n through the agent. With neither, the routes 404.\n --fs-write also accept writes over /v1/fs. Its own switch because\n an agent's writes go through the permission flow and a\n PUT does not. Every write is still conditional on the\n hash the client last read.\n --shell let an operator run '$' shell commands on the host from\n the composer, in the session's cwd. Every one is a PTY\n with a tracked record. Off by default and its own switch:\n unlike an agent's Bash tool, a '$' command goes through\n no permission prompt at all. Never offered to a scoped\n principal. The agent gets shell_list and shell_read.\n --shell-agent-write <mode>\n what the agent may do to shells of its own with --shell:\n read-only (default: it only reads), gated (shell_run,\n shell_write and shell_kill behind a permission card,\n payload shown; config: shell.agentWrite) or allow (no\n card beyond the engine's own mode). Never a shell the\n user started, never a session a scoped principal made.\n --approval-timeout <d>\n how long a permission prompt or a question may wait for\n an answer before the engine denies it (300000, 30s, 5m;\n config: approvalTimeoutMs). Default 'none' - a prompt\n waits as long as the session lives. A session may\n override it per request.\n --engine-sleep-after <d>\n stop an idle session's engine process once no client\n has watched it for this long (30m, 2h; config:\n engineSleepAfterMs). The session stays listed and\n wakes on its next message. Default 'never'.\n --effort-default <model>=<effort>\n reasoning effort a session starts at, or switches to,\n on this model (opus=high, gpt-6-sol=xhigh; repeatable;\n config: effortDefaults). A profile's defaults.efforts\n wins per model. Default: the engine's own.\n --agent-context-reset <on|off|never>\n whether sessions get the context_reset tool, which\n lets an agent clear its own conversation between turns\n and continue from a prompt it wrote (config:\n agentContextReset). A request or profile may turn it\n on per session; 'never' forbids it. Default 'off'.\n --state-dir <path> where parked sessions are persisted\n (default: beside the config file, else ~/.workerdeck)\n --no-parking-store keep parked sessions in memory only; a restart drops them\n -c, --config <path> config file (default: ./workerdeck.config.mjs)\n --insecure allow no-auth on a non-loopback address. Only when\n something in front is doing the authenticating.\n --cors-origin <o> browser origin allowed to call /v1 cross-origin, for a\n dashboard served elsewhere (repeatable, exact origin, no\n wildcard; config: corsOrigins). Refused without auth. The\n key is still required - this only permits the call.\n --no-keep-awake don't hold the machine awake while a session is waiting\n on it (config: keepAwake: false). By default a turn in\n flight, or a session blocked on an approval, blocks idle\n sleep - a laptop sleeping mid-turn drops the socket and\n burns the turn. The screen is never kept on, and the\n lock releases the moment nothing is waiting.\n --no-web don't serve the web dashboard (config: web: false).\n /v1 and the auth routes are unchanged; everything else\n 404s. For a gateway reached only from the VS Code\n extension, the phone, or another host's dashboard.\n --open open the dashboard in a browser once it is up\n --hot-reload dev only: re-evaluate the gateway's own source in place on\n ctrl-r, SIGUSR2 or \\`workerdeck reload\\`, carrying live\n sessions and their engine child processes across the swap.\n Needs a source checkout; the published CLI is one bundled\n file with nothing to swap.\n -h, --help show this\n -v, --version print the version\n\nConfig file\n Options that cannot fit on a command line - \\`authenticate\\`, \\`buildRunnerConfig\\`,\n \\`createEngineRunner\\` are functions - live in workerdeck.config.mjs, which\n default-exports the createWorkerServer options (or a function returning them).\n Flags and env override it. Supplying your own \\`authenticate\\` turns the built-in\n shared-secret auth off entirely.\n\nCredentials\n workerdeck implements no Anthropic auth of its own: the official SDK/CLI\n resolves credentials from the environment, per profile. --auth-key protects this\n gateway, nothing else.\n`\n\nasync function readVersion(): Promise<string> {\n // src/cli.ts and build/cli.mjs are both one level under the package root.\n const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')\n try {\n const raw = await readFile(pkgPath, 'utf8')\n return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'\n } catch {\n return 'unknown'\n }\n}\n\nfunction openInBrowser(url: string): void {\n const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'\n try {\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n shell: process.platform === 'win32',\n })\n child.on('error', () => {})\n child.unref()\n } catch {}\n}\n\n// Read once and removed: every process this one spawns (engines, shells, the browser opener) inherits `process.env`.\nfunction takeGatewayEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env }\n delete process.env.WORKERDECK_AUTH_KEY\n delete process.env.WORKERDECK_RELAY_KEY\n return env\n}\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\nasync function main(argv: string[]): Promise<number> {\n if (argv[0] === 'reload') {\n const { runReload } = await import('./dev/reload-command.ts')\n return await runReload(argv.slice(1))\n }\n\n if (argv[0] === 'relay') {\n const { runRelayCli } = await import('@workerdeck/relay')\n return await runRelayCli(argv.slice(1))\n }\n\n if (argv[0] === 'guard') {\n const { runGuard } = await import('./lib/guard.ts')\n return await runGuard(argv.slice(1))\n }\n\n const env = takeGatewayEnv()\n const flags = parseArgs(argv)\n if (flags.help) {\n process.stdout.write(HELP)\n return 0\n }\n if (flags.version) {\n process.stdout.write(`${await readVersion()}\\n`)\n return 0\n }\n\n if (flags.hotReload) {\n const { runHotReload } = await import('./dev/hot-reload.ts')\n const result = await runHotReload(flags, env)\n if (result !== 'unsupported') {\n return result\n }\n }\n\n const loaded = await loadConfigFile(flags.config)\n const config = resolveInstanceConfig(flags, loaded, env)\n const instance = await startInstance(config)\n\n if (config.open) {\n openInBrowser(instance.url)\n }\n\n installShutdown({ current: () => instance, log: line })\n\n // Resolves only on close: until then the process stays up, serving.\n await instance.closed\n return 0\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n if (code !== 0) {\n process.exit(code)\n }\n })\n .catch((error: unknown) => {\n if (error instanceof ConfigError) {\n process.stderr.write(`[workerdeck] ${error.message}\\n`)\n process.exit(2)\n }\n process.stderr.write(`[workerdeck] ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`)\n process.exit(1)\n })\n"],"mappings":"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"}
@@ -102,6 +102,7 @@ async function runHotReload(flags, env = process.env) {
102
102
  if (released) orphans.push(released);
103
103
  }
104
104
  const endedShells = instance.server.shells?.running().length ?? 0;
105
+ instance.server.releaseRelay();
105
106
  await instance.close();
106
107
  generation += 1;
107
108
  instance = await start(orphans);
@@ -150,4 +151,4 @@ async function runHotReload(flags, env = process.env) {
150
151
  //#endregion
151
152
  export { runHotReload };
152
153
 
153
- //# sourceMappingURL=hot-reload-Duq3g2Lm.mjs.map
154
+ //# sourceMappingURL=hot-reload-BU48Y18z.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"hot-reload-Duq3g2Lm.mjs","names":[],"sources":["../src/dev/hot-reload.ts"],"sourcesContent":["import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'\nimport * as nodeModule from 'node:module'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport { createFileSessionStore, isDormant, reloadPlan, type CarriedSession, type SessionStore } from '@workerdeck/server'\nimport type { CliFlags } from '../config.ts'\nimport type { Instance } from '../lib/instance.ts'\nimport { installShutdown } from '../lib/shutdown.ts'\n\ntype Registry = Instance['server']['registry']\ntype CarriedRunner = NonNullable<ReturnType<Registry['get']>>\ntype Parking = Instance['server']['parking']\n\nconst here = fileURLToPath(import.meta.url)\nconst packagesDir = resolve(dirname(here), '..', '..', '..')\n\nconst CTRL_C = '\\u0003'\nconst CTRL_R = '\\u0012'\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\n// A provider session is never carried by identity: its executors close over the generation's bridge hub, so a\n// carried one fails every LATER bridged call with no_client while a client is attached, not merely the one in\n// flight. It takes the ordinary restart path instead, which needs its snapshot on disk before the close.\n// `snapshot()` is side-effect free, so probing it is how we learn whether an interrupt is owed.\nasync function persistProvider(parking: Parking, runner: CarriedRunner): Promise<'kept' | 'interrupted' | 'lost'> {\n let how: 'kept' | 'interrupted' = 'kept'\n if (!runner.snapshot?.()) {\n // The documented way out of every state snapshot refuses: it aborts the model call, settles pending executions\n // as interrupted and emits the failed turn_result whose write-through then runs.\n await runner.interrupt()\n how = 'interrupted'\n }\n const before = Date.now()\n parking.touch(runner)\n await parking.flush(runner.id)\n const record = await parking.get(runner.id)\n return record && !isDormant(record) && record.parkedAt >= before ? how : 'lost'\n}\n\n// Runs the gateway inside a shell that can re-evaluate every module under `packages/` in place, carrying the live\n// runners - and therefore their engine child processes, subagents and shell grandchildren - across the swap.\n// Dev only: the published CLI is a single bundled file with no subgraph to re-evaluate.\n//\n// `unsupported` rather than a throw: the flag reaches this CLI from a VS Code setting and a config file as well as\n// from a keyboard, and a gateway that refuses to start because one dev convenience is unavailable is the worse\n// failure. The caller serves without it and says so.\nexport async function runHotReload(flags: CliFlags, env: NodeJS.ProcessEnv = process.env): Promise<number | 'unsupported'> {\n if (!here.endsWith('.ts') || !existsSync(join(packagesDir, 'server', 'src'))) {\n process.stderr.write(\n '[workerdeck] --hot-reload needs a source checkout; this build is one bundled file with nothing to swap.\\n' +\n ' Serving without it. Run it as `pnpm cli --hot-reload` from the repo instead.\\n',\n )\n return 'unsupported'\n }\n\n // A namespace lookup, not a named import: `registerHooks` landed in Node 22.15, and a runtime without it (an\n // older Node, or Bun) must reach the unsupported path below rather than fail to load this module at all.\n const registerHooks = (nodeModule as Partial<typeof nodeModule>).registerHooks\n if (!registerHooks) {\n process.stderr.write(\n '[workerdeck] --hot-reload needs `module.registerHooks`, which this runtime does not provide (Node 22.15+).\\n' +\n ' Serving without it.\\n',\n )\n return 'unsupported'\n }\n\n const packagesUrl = `${pathToFileURL(packagesDir).href}/`\n let generation = 0\n\n // Only our own sources are versioned. node_modules stays at one copy per package on purpose: a second Agent SDK,\n // a second `ws`, or a second quickjs would each be a different kind of disaster.\n registerHooks({\n resolve(specifier, context, nextResolve) {\n const result = nextResolve(specifier, context)\n if (generation === 0 || typeof result.url !== 'string') {\n return result\n }\n if (!result.url.startsWith(packagesUrl) || result.url.includes('/node_modules/')) {\n return result\n }\n return { ...result, url: `${result.url}${result.url.includes('?') ? '&' : '?'}wdgen=${generation}` }\n },\n })\n\n let baseStore: SessionStore | undefined\n let stateDir: string | null = null\n let pidPath: string | undefined\n\n const start = async (carried: CarriedSession[]): Promise<Instance> => {\n const config = (await import(`${packagesUrl}cli/src/config.ts`)) as typeof import('../config.ts')\n const lib = (await import(`${packagesUrl}cli/src/lib/instance.ts`)) as typeof import('../lib/instance.ts')\n const loaded = await config.loadConfigFile(flags.config)\n const resolved = config.resolveInstanceConfig(flags, loaded, env)\n stateDir = resolved.stateDir\n // One store object for the whole process. A file store is single-process by contract, and two generations\n // holding two of them over one directory is exactly the two-servers-one-directory case it refuses to be; an\n // in-memory one is worse, because a reload would drop every record it holds.\n if (resolved.stateDir && !resolved.options.parking?.store) {\n baseStore ??= createFileSessionStore({ dir: join(resolved.stateDir, 'parked') })\n }\n resolved.options.parking = {\n ...resolved.options.parking,\n ...(baseStore ? { store: baseStore } : {}),\n // Forced: the seam persists provider sessions rather than carrying them, and without this their snapshots\n // are never written, so a reload would end them.\n persistLive: true,\n }\n return await lib.startInstance(resolved, { quiet: generation > 0, carried })\n }\n\n let instance = await start([])\n let orphans: CarriedSession[] = []\n\n if (stateDir) {\n pidPath = join(stateDir, 'gateway.pid')\n // The state dir is created lazily by whoever writes into it first, and on a cold one that is nobody yet.\n mkdirSync(stateDir, { recursive: true })\n writeFileSync(pidPath, `${process.pid}\\n`)\n process.on('exit', () => {\n try {\n rmSync(pidPath!)\n } catch {}\n })\n }\n\n let reloading = false\n const reload = async (trigger: string): Promise<void> => {\n if (reloading) {\n return\n }\n reloading = true\n const started = Date.now()\n const persisted: string[] = []\n const dropped: string[] = []\n try {\n // One synchronous pass, because every later step keys off this classification and `info()` is synchronous.\n const carry: string[] = []\n const persist: CarriedRunner[] = []\n for (const info of instance.server.registry.list()) {\n const runner = instance.server.registry.get(info.id)\n if (!runner) {\n continue\n }\n const plan = reloadPlan(runner)\n if (plan === 'carry') {\n carry.push(runner.id)\n } else if (plan === 'persist') {\n persist.push(runner)\n } else {\n dropped.push(runner.id)\n }\n }\n\n // While this generation still owns them: `touch()` writes nothing for a runner its registry does not hold.\n for (const runner of persist) {\n persisted.push(`${runner.id} (${await persistProvider(instance.server.parking, runner)})`)\n }\n // Both halves matter: this generation's queued writes must land before `close()` skips them, and two managers\n // in one process share the file store's `${path}.${pid}.tmp`, so their writes must never overlap.\n await instance.server.parking.flush()\n\n for (const id of carry) {\n const released = instance.server.releaseSession(id)\n if (released) {\n orphans.push(released)\n }\n }\n const endedShells = instance.server.shells?.running().length ?? 0\n await instance.close()\n generation += 1\n instance = await start(orphans)\n const adopted = new Set(instance.adopted)\n const ended = orphans.filter((carried) => !adopted.has(carried.runner.id)).map((carried) => carried.runner.id)\n orphans = []\n line(\n `[workerdeck] reloaded on ${trigger}: gen ${generation} in ${Date.now() - started}ms, ` +\n `${adopted.size} carried${persisted.length ? `, persisted ${persisted.join(', ')}` : ''}` +\n `${dropped.length ? `, dropped ${dropped.join(', ')}` : ''}${ended.length ? `, ended while held ${ended.join(', ')}` : ''}` +\n `${endedShells > 0 ? `, ended ${endedShells} shell(s)` : ''}`,\n )\n } catch (error) {\n line(`[workerdeck] reload failed at gen ${generation}: ${error instanceof Error ? error.message : String(error)}`)\n if (orphans.length > 0) {\n line(`[workerdeck] holding ${orphans.length} session(s): ${orphans.map((carried) => carried.runner.id).join(', ')}`)\n line('[workerdeck] their turns keep running and their transcripts are buffered, but nothing can attach and')\n line('[workerdeck] no push or webhook goes out until a generation starts. Fix the error and reload again,')\n line('[workerdeck] or ctrl-c to stop (the sessions close; their records are kept).')\n }\n } finally {\n reloading = false\n }\n }\n\n const shutdown = installShutdown({\n current: () => instance,\n // Held across a failed generation, so no registry holds them and `close()` cannot reach them. Nothing else\n // would end their engine children promptly.\n alsoClose: () => orphans.map((carried) => carried.runner),\n log: line,\n })\n\n process.on('SIGUSR2', () => void reload('SIGUSR2'))\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(true)\n process.stdin.resume()\n process.stdin.setEncoding('utf8')\n process.stdin.on('data', (key: string) => {\n if (key === CTRL_R) {\n void reload('ctrl-r')\n return\n }\n // Raw mode swallows SIGINT, so ctrl-c arrives as a byte and has to be re-spelled as the shutdown it was.\n if (key === CTRL_C) {\n shutdown('SIGINT')\n }\n })\n process.on('exit', () => {\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(false)\n }\n })\n }\n\n line(` hot reload: ctrl-r${pidPath ? ', `workerdeck reload`' : ''}, or SIGUSR2 to pid ${process.pid}`)\n line('')\n\n // Never resolves: every exit from here runs through `installShutdown`, which closes the current instance and\n // exits. Awaiting `instance.closed` instead would return the moment a reload closed the generation it captured.\n await new Promise<void>(() => {})\n return 0\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"hot-reload-BU48Y18z.mjs","names":[],"sources":["../src/dev/hot-reload.ts"],"sourcesContent":["import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'\nimport * as nodeModule from 'node:module'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport { createFileSessionStore, isDormant, reloadPlan, type CarriedSession, type SessionStore } from '@workerdeck/server'\nimport type { CliFlags } from '../config.ts'\nimport type { Instance } from '../lib/instance.ts'\nimport { installShutdown } from '../lib/shutdown.ts'\n\ntype Registry = Instance['server']['registry']\ntype CarriedRunner = NonNullable<ReturnType<Registry['get']>>\ntype Parking = Instance['server']['parking']\n\nconst here = fileURLToPath(import.meta.url)\nconst packagesDir = resolve(dirname(here), '..', '..', '..')\n\nconst CTRL_C = '\\u0003'\nconst CTRL_R = '\\u0012'\n\nfunction line(text: string): void {\n process.stdout.write(`${text}\\n`)\n}\n\n// A provider session is never carried by identity: its executors close over the generation's bridge hub, so a\n// carried one fails every LATER bridged call with no_client while a client is attached, not merely the one in\n// flight. It takes the ordinary restart path instead, which needs its snapshot on disk before the close.\n// `snapshot()` is side-effect free, so probing it is how we learn whether an interrupt is owed.\nasync function persistProvider(parking: Parking, runner: CarriedRunner): Promise<'kept' | 'interrupted' | 'lost'> {\n let how: 'kept' | 'interrupted' = 'kept'\n if (!runner.snapshot?.()) {\n // The documented way out of every state snapshot refuses: it aborts the model call, settles pending executions\n // as interrupted and emits the failed turn_result whose write-through then runs.\n await runner.interrupt()\n how = 'interrupted'\n }\n const before = Date.now()\n parking.touch(runner)\n await parking.flush(runner.id)\n const record = await parking.get(runner.id)\n return record && !isDormant(record) && record.parkedAt >= before ? how : 'lost'\n}\n\n// Runs the gateway inside a shell that can re-evaluate every module under `packages/` in place, carrying the live\n// runners - and therefore their engine child processes, subagents and shell grandchildren - across the swap.\n// Dev only: the published CLI is a single bundled file with no subgraph to re-evaluate.\n//\n// `unsupported` rather than a throw: the flag reaches this CLI from a VS Code setting and a config file as well as\n// from a keyboard, and a gateway that refuses to start because one dev convenience is unavailable is the worse\n// failure. The caller serves without it and says so.\nexport async function runHotReload(flags: CliFlags, env: NodeJS.ProcessEnv = process.env): Promise<number | 'unsupported'> {\n if (!here.endsWith('.ts') || !existsSync(join(packagesDir, 'server', 'src'))) {\n process.stderr.write(\n '[workerdeck] --hot-reload needs a source checkout; this build is one bundled file with nothing to swap.\\n' +\n ' Serving without it. Run it as `pnpm cli --hot-reload` from the repo instead.\\n',\n )\n return 'unsupported'\n }\n\n // A namespace lookup, not a named import: `registerHooks` landed in Node 22.15, and a runtime without it (an\n // older Node, or Bun) must reach the unsupported path below rather than fail to load this module at all.\n const registerHooks = (nodeModule as Partial<typeof nodeModule>).registerHooks\n if (!registerHooks) {\n process.stderr.write(\n '[workerdeck] --hot-reload needs `module.registerHooks`, which this runtime does not provide (Node 22.15+).\\n' +\n ' Serving without it.\\n',\n )\n return 'unsupported'\n }\n\n const packagesUrl = `${pathToFileURL(packagesDir).href}/`\n let generation = 0\n\n // Only our own sources are versioned. node_modules stays at one copy per package on purpose: a second Agent SDK,\n // a second `ws`, or a second quickjs would each be a different kind of disaster.\n registerHooks({\n resolve(specifier, context, nextResolve) {\n const result = nextResolve(specifier, context)\n if (generation === 0 || typeof result.url !== 'string') {\n return result\n }\n if (!result.url.startsWith(packagesUrl) || result.url.includes('/node_modules/')) {\n return result\n }\n return { ...result, url: `${result.url}${result.url.includes('?') ? '&' : '?'}wdgen=${generation}` }\n },\n })\n\n let baseStore: SessionStore | undefined\n let stateDir: string | null = null\n let pidPath: string | undefined\n\n const start = async (carried: CarriedSession[]): Promise<Instance> => {\n const config = (await import(`${packagesUrl}cli/src/config.ts`)) as typeof import('../config.ts')\n const lib = (await import(`${packagesUrl}cli/src/lib/instance.ts`)) as typeof import('../lib/instance.ts')\n const loaded = await config.loadConfigFile(flags.config)\n const resolved = config.resolveInstanceConfig(flags, loaded, env)\n stateDir = resolved.stateDir\n // One store object for the whole process. A file store is single-process by contract, and two generations\n // holding two of them over one directory is exactly the two-servers-one-directory case it refuses to be; an\n // in-memory one is worse, because a reload would drop every record it holds.\n if (resolved.stateDir && !resolved.options.parking?.store) {\n baseStore ??= createFileSessionStore({ dir: join(resolved.stateDir, 'parked') })\n }\n resolved.options.parking = {\n ...resolved.options.parking,\n ...(baseStore ? { store: baseStore } : {}),\n // Forced: the seam persists provider sessions rather than carrying them, and without this their snapshots\n // are never written, so a reload would end them.\n persistLive: true,\n }\n return await lib.startInstance(resolved, { quiet: generation > 0, carried })\n }\n\n let instance = await start([])\n let orphans: CarriedSession[] = []\n\n if (stateDir) {\n pidPath = join(stateDir, 'gateway.pid')\n // The state dir is created lazily by whoever writes into it first, and on a cold one that is nobody yet.\n mkdirSync(stateDir, { recursive: true })\n writeFileSync(pidPath, `${process.pid}\\n`)\n process.on('exit', () => {\n try {\n rmSync(pidPath!)\n } catch {}\n })\n }\n\n let reloading = false\n const reload = async (trigger: string): Promise<void> => {\n if (reloading) {\n return\n }\n reloading = true\n const started = Date.now()\n const persisted: string[] = []\n const dropped: string[] = []\n try {\n // One synchronous pass, because every later step keys off this classification and `info()` is synchronous.\n const carry: string[] = []\n const persist: CarriedRunner[] = []\n for (const info of instance.server.registry.list()) {\n const runner = instance.server.registry.get(info.id)\n if (!runner) {\n continue\n }\n const plan = reloadPlan(runner)\n if (plan === 'carry') {\n carry.push(runner.id)\n } else if (plan === 'persist') {\n persist.push(runner)\n } else {\n dropped.push(runner.id)\n }\n }\n\n // While this generation still owns them: `touch()` writes nothing for a runner its registry does not hold.\n for (const runner of persist) {\n persisted.push(`${runner.id} (${await persistProvider(instance.server.parking, runner)})`)\n }\n // Both halves matter: this generation's queued writes must land before `close()` skips them, and two managers\n // in one process share the file store's `${path}.${pid}.tmp`, so their writes must never overlap.\n await instance.server.parking.flush()\n\n for (const id of carry) {\n const released = instance.server.releaseSession(id)\n if (released) {\n orphans.push(released)\n }\n }\n const endedShells = instance.server.shells?.running().length ?? 0\n instance.server.releaseRelay()\n await instance.close()\n generation += 1\n instance = await start(orphans)\n const adopted = new Set(instance.adopted)\n const ended = orphans.filter((carried) => !adopted.has(carried.runner.id)).map((carried) => carried.runner.id)\n orphans = []\n line(\n `[workerdeck] reloaded on ${trigger}: gen ${generation} in ${Date.now() - started}ms, ` +\n `${adopted.size} carried${persisted.length ? `, persisted ${persisted.join(', ')}` : ''}` +\n `${dropped.length ? `, dropped ${dropped.join(', ')}` : ''}${ended.length ? `, ended while held ${ended.join(', ')}` : ''}` +\n `${endedShells > 0 ? `, ended ${endedShells} shell(s)` : ''}`,\n )\n } catch (error) {\n line(`[workerdeck] reload failed at gen ${generation}: ${error instanceof Error ? error.message : String(error)}`)\n if (orphans.length > 0) {\n line(`[workerdeck] holding ${orphans.length} session(s): ${orphans.map((carried) => carried.runner.id).join(', ')}`)\n line('[workerdeck] their turns keep running and their transcripts are buffered, but nothing can attach and')\n line('[workerdeck] no push or webhook goes out until a generation starts. Fix the error and reload again,')\n line('[workerdeck] or ctrl-c to stop (the sessions close; their records are kept).')\n }\n } finally {\n reloading = false\n }\n }\n\n const shutdown = installShutdown({\n current: () => instance,\n // Held across a failed generation, so no registry holds them and `close()` cannot reach them. Nothing else\n // would end their engine children promptly.\n alsoClose: () => orphans.map((carried) => carried.runner),\n log: line,\n })\n\n process.on('SIGUSR2', () => void reload('SIGUSR2'))\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(true)\n process.stdin.resume()\n process.stdin.setEncoding('utf8')\n process.stdin.on('data', (key: string) => {\n if (key === CTRL_R) {\n void reload('ctrl-r')\n return\n }\n // Raw mode swallows SIGINT, so ctrl-c arrives as a byte and has to be re-spelled as the shutdown it was.\n if (key === CTRL_C) {\n shutdown('SIGINT')\n }\n })\n process.on('exit', () => {\n if (process.stdin.isTTY) {\n process.stdin.setRawMode(false)\n }\n })\n }\n\n line(` hot reload: ctrl-r${pidPath ? ', `workerdeck reload`' : ''}, or SIGUSR2 to pid ${process.pid}`)\n line('')\n\n // Never resolves: every exit from here runs through `installShutdown`, which closes the current instance and\n // exits. Awaiting `instance.closed` instead would return the moment a reload closed the generation it captured.\n await new Promise<void>(() => {})\n return 0\n}\n"],"mappings":"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"}
package/build/index.d.mts CHANGED
@@ -123,6 +123,9 @@ type CliFlags = {
123
123
  insecureHosts: string[];
124
124
  trustProxy?: boolean;
125
125
  approvalTimeoutMs?: number | null;
126
+ engineSleepAfterMs?: number;
127
+ effortDefaults?: Record<string, string>;
128
+ agentContextReset?: 'on' | 'off' | 'never';
126
129
  stateDir?: string;
127
130
  parking?: boolean;
128
131
  insecure?: boolean;
package/build/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as runGuard } from "./guard-C4ybpzbt.mjs";
2
- import { C as createActivityRegistry, S as createDeviceRoute, _ as createApnsForwarder, a as ConfigError, b as createApnsRoute, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, g as buildPush, h as materializeAuthKey, i as renderLoginPage, l as isLoopbackHostname, m as createAuthSessionStore, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile, v as createApnsClient, w as createActivityRoute, x as createDeviceRegistry, y as loadApnsKey } from "./instance-BmQhZ3Px.mjs";
2
+ import { C as createActivityRegistry, S as createDeviceRoute, _ as createApnsForwarder, a as ConfigError, b as createApnsRoute, c as isLoopback, d as parseArgs, f as resolveInstanceConfig, g as buildPush, h as materializeAuthKey, i as renderLoginPage, l as isLoopbackHostname, m as createAuthSessionStore, n as resolveWebRoot, o as defaultStateDir, p as createCliAuth, r as startInstance, s as hostnameOf, t as createHostGuard, u as loadConfigFile, v as createApnsClient, w as createActivityRoute, x as createDeviceRegistry, y as loadApnsKey } from "./instance-DyBeVgsK.mjs";
3
3
  export { ConfigError, buildPush, createActivityRegistry, createActivityRoute, createApnsClient, createApnsForwarder, createApnsRoute, createAuthSessionStore, createCliAuth, createDeviceRegistry, createDeviceRoute, createHostGuard, defaultStateDir, hostnameOf, isLoopback, isLoopbackHostname, loadApnsKey, loadConfigFile, materializeAuthKey, parseArgs, renderLoginPage, resolveInstanceConfig, resolveWebRoot, runGuard, startInstance };
@@ -1,4 +1,4 @@
1
- import { createReadStream, existsSync } from "node:fs";
1
+ import { createReadStream, existsSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
3
3
  import { createFileProfileStore, createFileSessionStore, createFileSpendStore, createWorkerServer, readJsonOr, writeJsonAtomic } from "@workerdeck/server";
4
4
  import { dashboardDir } from "@workerdeck/web";
@@ -1767,6 +1767,12 @@ const VALUED = /* @__PURE__ */ new Map([
1767
1767
  ["--allowed-host", (f, v) => f.allowedHosts.push(v)],
1768
1768
  ["--insecure-host", (f, v) => f.insecureHosts.push(v)],
1769
1769
  ["--approval-timeout", (f, v, name) => f.approvalTimeoutMs = parseDuration(v, name)],
1770
+ ["--engine-sleep-after", (f, v, name) => f.engineSleepAfterMs = parseDuration(v, name) ?? 0],
1771
+ ["--effort-default", (f, v, name) => f.effortDefaults = {
1772
+ ...f.effortDefaults,
1773
+ ...parseEffortDefault(v, name)
1774
+ }],
1775
+ ["--agent-context-reset", (f, v, name) => f.agentContextReset = parseAgentContextReset(v, name)],
1770
1776
  ["--state-dir", (f, v) => f.stateDir = resolve(v)],
1771
1777
  ["--cors-origin", (f, v) => f.corsOrigins.push(v)]
1772
1778
  ]);
@@ -1783,6 +1789,15 @@ function parseDuration(raw, source) {
1783
1789
  const ms = Number(match[1]) * scale;
1784
1790
  return ms > 0 ? ms : null;
1785
1791
  }
1792
+ function parseEffortDefault(raw, source) {
1793
+ const match = /^([^=\s]+)=([^=\s]+)$/.exec(raw);
1794
+ if (!match) throw new ConfigError(`${source}: expected <model>=<effort>, like opus=high; got: ${raw}`);
1795
+ return { [match[1]]: match[2] };
1796
+ }
1797
+ function parseAgentContextReset(raw, source) {
1798
+ if (raw === "on" || raw === "off" || raw === "never") return raw;
1799
+ throw new ConfigError(`${source}: expected on, off or never; got: ${raw}`);
1800
+ }
1786
1801
  function parseShellAgentWrite(value, name) {
1787
1802
  if (value === "read-only" || value === "gated" || value === "allow") return value;
1788
1803
  throw new ConfigError(`${name} must be read-only, gated or allow (got '${value}')`);
@@ -1842,7 +1857,7 @@ async function loadConfigFile(explicit, cwd = process.cwd()) {
1842
1857
  };
1843
1858
  let mod;
1844
1859
  try {
1845
- mod = await import(pathToFileURL(path).href);
1860
+ mod = await import(`${pathToFileURL(path).href}?mtime=${statSync(path).mtimeMs}`);
1846
1861
  } catch (error) {
1847
1862
  throw new ConfigError(`failed to load ${path}: ${error instanceof Error ? error.message : String(error)}`);
1848
1863
  }
@@ -1942,8 +1957,23 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
1942
1957
  ...flags.shell ? { enabled: true } : {},
1943
1958
  ...flags.shellAgentWrite !== void 0 ? { agentWrite: flags.shellAgentWrite } : {}
1944
1959
  };
1960
+ if (loaded.options.relay) options.relay = {
1961
+ ...loaded.options.relay,
1962
+ key: env.WORKERDECK_RELAY_KEY || loaded.options.relay.key || void 0,
1963
+ log: loaded.options.relay.log ?? ((message) => process.stderr.write(`[workerdeck] ${message}\n`))
1964
+ };
1945
1965
  if (flags.profiles.length) options.profiles = flags.profiles;
1946
1966
  if (flags.approvalTimeoutMs !== void 0) options.approvalTimeoutMs = flags.approvalTimeoutMs;
1967
+ if (flags.engineSleepAfterMs !== void 0) options.engineSleepAfterMs = flags.engineSleepAfterMs;
1968
+ if (flags.agentContextReset === "never") options.agentContextReset = false;
1969
+ else if (flags.agentContextReset !== void 0) options.agentContextReset = {
1970
+ ...loaded.options.agentContextReset,
1971
+ default: flags.agentContextReset === "on"
1972
+ };
1973
+ if (flags.effortDefaults) options.effortDefaults = {
1974
+ ...loaded.options.effortDefaults,
1975
+ ...flags.effortDefaults
1976
+ };
1947
1977
  if (flags.profileStore === false) delete options.profileStore;
1948
1978
  const manageProfiles = flags.profileStore !== false;
1949
1979
  if (manageProfiles && !options.allowedConfigDirRoots) options.allowedConfigDirRoots = flags.profileRoots.length ? flags.profileRoots : [homedir()];
@@ -2487,4 +2517,4 @@ function bannerLines({ url, config, generated, authEnabled, push }) {
2487
2517
  //#endregion
2488
2518
  export { createActivityRegistry as C, createDeviceRoute as S, createApnsForwarder as _, ConfigError as a, createApnsRoute as b, isLoopback as c, parseArgs as d, resolveInstanceConfig as f, buildPush as g, materializeAuthKey as h, renderLoginPage as i, isLoopbackHostname as l, createAuthSessionStore as m, resolveWebRoot as n, defaultStateDir as o, createCliAuth as p, startInstance as r, hostnameOf as s, createHostGuard as t, loadConfigFile as u, createApnsClient as v, createActivityRoute as w, createDeviceRegistry as x, loadApnsKey as y };
2489
2519
 
2490
- //# sourceMappingURL=instance-BmQhZ3Px.mjs.map
2520
+ //# sourceMappingURL=instance-DyBeVgsK.mjs.map