workerdeck 3.4.0 → 3.5.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-DyBeVgsK.mjs";
2
+ import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-C5j14c33.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
@@ -110,6 +110,10 @@ Options
110
110
  has watched it for this long (30m, 2h; config:
111
111
  engineSleepAfterMs). The session stays listed and
112
112
  wakes on its next message. Default 'never'.
113
+ --agent-sleep-after <d>
114
+ the same for an agent's session, unless the agent sets
115
+ its own sleepAfterMs (30m, 2h, never; config:
116
+ agentSleepAfterMs). Default 15m.
113
117
  --effort-default <model>=<effort>
114
118
  reasoning effort a session starts at, or switches to,
115
119
  on this model (opus=high, gpt-6-sol=xhigh; repeatable;
@@ -194,7 +198,7 @@ function line(text) {
194
198
  }
195
199
  async function main(argv) {
196
200
  if (argv[0] === "reload") {
197
- const { runReload } = await import("./reload-command-BB_HdGxV.mjs");
201
+ const { runReload } = await import("./reload-command-CIK0WSqa.mjs");
198
202
  return await runReload(argv.slice(1));
199
203
  }
200
204
  if (argv[0] === "relay") {
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 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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"}
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 --agent-sleep-after <d>\n the same for an agent's session, unless the agent sets\n its own sleepAfterMs (30m, 2h, never; config:\n agentSleepAfterMs). Default 15m.\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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"}
package/build/index.d.mts CHANGED
@@ -124,6 +124,7 @@ type CliFlags = {
124
124
  trustProxy?: boolean;
125
125
  approvalTimeoutMs?: number | null;
126
126
  engineSleepAfterMs?: number;
127
+ agentSleepAfterMs?: number;
127
128
  effortDefaults?: Record<string, string>;
128
129
  agentContextReset?: 'on' | 'off' | 'never';
129
130
  stateDir?: string;
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-DyBeVgsK.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-C5j14c33.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,13 +1,15 @@
1
1
  import { createReadStream, existsSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
3
- import { createFileProfileStore, createFileSessionStore, createFileSpendStore, createWorkerServer, readJsonOr, writeJsonAtomic } from "@workerdeck/server";
3
+ import { createFileAgentStore, createFileProfileStore, createFileSessionStore, createFileSpendStore, createWorkerServer, readJsonOr, writeJsonAtomic } from "@workerdeck/server";
4
4
  import { dashboardDir } from "@workerdeck/web";
5
5
  import { createHash, createHmac, createPrivateKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
6
6
  import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
7
  import { connect, constants } from "node:http2";
8
- import { CHECKLIST_TEXT_MAX, isAgentRecord, parseUserQuestions, subagentLabel } from "@workerdeck/protocol";
8
+ import { CHECKLIST_TEXT_MAX, isAgentRecord, parseUserQuestions, projectAccent, subagentLabel } from "@workerdeck/protocol";
9
9
  import { homedir } from "node:os";
10
10
  import { pathToFileURL } from "node:url";
11
+ import { createRecipe, decodePack, resolveRecipe, toAnimationStripPng, toPng } from "@monkeyart/composer";
12
+ import { defaultPackId, packUrl } from "@monkeyart/packs";
11
13
  import { spawn } from "node:child_process";
12
14
  //#region src/lib/http.ts
13
15
  function readBody(req, maxBytes) {
@@ -1768,6 +1770,7 @@ const VALUED = /* @__PURE__ */ new Map([
1768
1770
  ["--insecure-host", (f, v) => f.insecureHosts.push(v)],
1769
1771
  ["--approval-timeout", (f, v, name) => f.approvalTimeoutMs = parseDuration(v, name)],
1770
1772
  ["--engine-sleep-after", (f, v, name) => f.engineSleepAfterMs = parseDuration(v, name) ?? 0],
1773
+ ["--agent-sleep-after", (f, v, name) => f.agentSleepAfterMs = parseDuration(v, name) ?? 0],
1771
1774
  ["--effort-default", (f, v, name) => f.effortDefaults = {
1772
1775
  ...f.effortDefaults,
1773
1776
  ...parseEffortDefault(v, name)
@@ -1965,6 +1968,7 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
1965
1968
  if (flags.profiles.length) options.profiles = flags.profiles;
1966
1969
  if (flags.approvalTimeoutMs !== void 0) options.approvalTimeoutMs = flags.approvalTimeoutMs;
1967
1970
  if (flags.engineSleepAfterMs !== void 0) options.engineSleepAfterMs = flags.engineSleepAfterMs;
1971
+ if (flags.agentSleepAfterMs !== void 0) options.agentSleepAfterMs = flags.agentSleepAfterMs;
1968
1972
  if (flags.agentContextReset === "never") options.agentContextReset = false;
1969
1973
  else if (flags.agentContextReset !== void 0) options.agentContextReset = {
1970
1974
  ...loaded.options.agentContextReset,
@@ -2089,6 +2093,57 @@ function renderLoginPage(options) {
2089
2093
  `;
2090
2094
  }
2091
2095
  //#endregion
2096
+ //#region src/lib/avatars.ts
2097
+ const EMBLEMS = {
2098
+ claude: "claude",
2099
+ codex: "codex",
2100
+ provider: "rings"
2101
+ };
2102
+ var MonkeyartAvatars = class {
2103
+ #pack;
2104
+ #cache = /* @__PURE__ */ new Map();
2105
+ async roll(seed, engine, projectKey) {
2106
+ const pack = await this.#loadPack();
2107
+ const emblem = EMBLEMS[engine ?? "claude"];
2108
+ return createRecipe(pack, {
2109
+ seed,
2110
+ emblem: pack.emblems.some((e) => e.id === emblem) ? emblem : null,
2111
+ baseColor: projectAccent(projectKey ?? seed),
2112
+ backdrop: "blend"
2113
+ });
2114
+ }
2115
+ async still(recipe) {
2116
+ return this.#render(recipe, "still", async (pack, resolved) => ({ png: await toPng(pack, resolved) }));
2117
+ }
2118
+ async busy(recipe) {
2119
+ if (!(await this.#loadPack()).animations.some((animation) => animation.id === "busy")) return;
2120
+ return this.#render(recipe, "busy", async (p, resolved) => {
2121
+ const strip = await toAnimationStripPng(p, resolved, "busy");
2122
+ return {
2123
+ png: strip.png,
2124
+ durations: strip.durations
2125
+ };
2126
+ });
2127
+ }
2128
+ async #render(recipe, kind, draw) {
2129
+ const pack = await this.#loadPack();
2130
+ const key = `${kind}:${pack.checksum}:${JSON.stringify(recipe)}`;
2131
+ const held = this.#cache.get(key);
2132
+ if (held) return held;
2133
+ const { recipe: resolved } = resolveRecipe(pack, recipe);
2134
+ const image = {
2135
+ ...await draw(pack, resolved),
2136
+ etag: `"${createHash("sha256").update(key).digest("hex").slice(0, 32)}"`
2137
+ };
2138
+ this.#cache.set(key, image);
2139
+ return image;
2140
+ }
2141
+ #loadPack() {
2142
+ this.#pack ??= readFile(packUrl(defaultPackId)).then((bytes) => decodePack(new Uint8Array(bytes)));
2143
+ return this.#pack;
2144
+ }
2145
+ };
2146
+ //#endregion
2092
2147
  //#region src/lib/keep-awake.ts
2093
2148
  const AWAKE_STATUSES = /* @__PURE__ */ new Set([
2094
2149
  "starting",
@@ -2384,14 +2439,16 @@ async function startInstance(config, options = {}) {
2384
2439
  authenticate: authenticatePush
2385
2440
  });
2386
2441
  const fallback = createFallback(auth, webRoot, hostAllowed, apns?.handleRequest ?? createApnsRoute(null, authenticatePush));
2387
- const { parking, profileStore, shell, spend } = stateDirDefaults(config);
2442
+ const { agentStore, parking, profileStore, shell, spend } = stateDirDefaults(config);
2388
2443
  const server = createWorkerServer({
2389
2444
  ...config.options,
2390
2445
  ...profileStore ? { profileStore } : {},
2446
+ agentStore,
2391
2447
  checkCredentials: config.options.checkCredentials ?? true,
2392
2448
  parking,
2393
2449
  shell,
2394
2450
  spend,
2451
+ avatars: config.options.avatars ?? new MonkeyartAvatars(),
2395
2452
  notifications: apns === void 0 ? config.options.notifications : {
2396
2453
  ...config.options.notifications,
2397
2454
  onNotification: (notification) => {
@@ -2494,6 +2551,7 @@ function stateDirDefaults(config) {
2494
2551
  parking,
2495
2552
  shell,
2496
2553
  spend,
2554
+ agentStore: config.options.agentStore ?? createFileAgentStore(join(stateDir, "agents.json")),
2497
2555
  ...profileStore ? { profileStore } : {}
2498
2556
  };
2499
2557
  }
@@ -2517,4 +2575,4 @@ function bannerLines({ url, config, generated, authEnabled, push }) {
2517
2575
  //#endregion
2518
2576
  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 };
2519
2577
 
2520
- //# sourceMappingURL=instance-DyBeVgsK.mjs.map
2578
+ //# sourceMappingURL=instance-C5j14c33.mjs.map