workerdeck 3.5.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-C5j14c33.mjs";
2
+ import { a as ConfigError, d as parseArgs, f as resolveInstanceConfig, r as startInstance, u as loadConfigFile } from "./instance-BMMh875G.mjs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
@@ -114,6 +114,11 @@ Options
114
114
  the same for an agent's session, unless the agent sets
115
115
  its own sleepAfterMs (30m, 2h, never; config:
116
116
  agentSleepAfterMs). Default 15m.
117
+ --avatar-packs <list>
118
+ monkeyart packs new agent avatars are drawn from,
119
+ comma separated (monkey, toad, steampunk-bulldogs,
120
+ panda; config: avatarPacks). Default
121
+ monkey,steampunk-bulldogs,toad.
117
122
  --effort-default <model>=<effort>
118
123
  reasoning effort a session starts at, or switches to,
119
124
  on this model (opus=high, gpt-6-sol=xhigh; repeatable;
@@ -198,7 +203,7 @@ function line(text) {
198
203
  }
199
204
  async function main(argv) {
200
205
  if (argv[0] === "reload") {
201
- const { runReload } = await import("./reload-command-CIK0WSqa.mjs");
206
+ const { runReload } = await import("./reload-command-CTTfps7v.mjs");
202
207
  return await runReload(argv.slice(1));
203
208
  }
204
209
  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 --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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"}
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 --avatar-packs <list>\n monkeyart packs new agent avatars are drawn from,\n comma separated (monkey, toad, steampunk-bulldogs,\n panda; config: avatarPacks). Default\n monkey,steampunk-bulldogs,toad.\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
@@ -1,6 +1,7 @@
1
1
  import { Authenticator, CarriedSession, ShellAgentWriteOption, WorkerServer, WorkerServerOptions } from "@workerdeck/server";
2
2
  import { KeyObject } from "node:crypto";
3
3
  import { ProfileInfo, SessionNotification, SessionNotificationType, SubagentInfo } from "@workerdeck/protocol";
4
+ import { PackId } from "@monkeyart/packs";
4
5
  import { IncomingMessage, ServerResponse } from "node:http";
5
6
  //#region src/auth/auth-key.d.ts
6
7
  type MaterializedAuthKey = {
@@ -104,6 +105,7 @@ type WorkerDeckConfig = WorkerServerOptions & {
104
105
  keepAwake?: boolean;
105
106
  corsOrigins?: string[];
106
107
  apns?: ApnsConfig;
108
+ avatarPacks?: string[];
107
109
  };
108
110
  type CliFlags = {
109
111
  config?: string;
@@ -125,6 +127,7 @@ type CliFlags = {
125
127
  approvalTimeoutMs?: number | null;
126
128
  engineSleepAfterMs?: number;
127
129
  agentSleepAfterMs?: number;
130
+ avatarPacks?: string[];
128
131
  effortDefaults?: Record<string, string>;
129
132
  agentContextReset?: 'on' | 'off' | 'never';
130
133
  stateDir?: string;
@@ -160,6 +163,7 @@ type ResolvedConfig = {
160
163
  web: boolean;
161
164
  keepAwake: boolean;
162
165
  corsOrigins: string[];
166
+ avatarPacks?: PackId[];
163
167
  apns?: ApnsConfig;
164
168
  open: boolean;
165
169
  profileStore: 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-C5j14c33.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-BMMh875G.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 };
@@ -6,10 +6,10 @@ import { createHash, createHmac, createPrivateKey, randomBytes, sign, timingSafe
6
6
  import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
7
  import { connect, constants } from "node:http2";
8
8
  import { CHECKLIST_TEXT_MAX, isAgentRecord, parseUserQuestions, projectAccent, subagentLabel } from "@workerdeck/protocol";
9
+ import { defaultPackId, packIds, packUrl } from "@monkeyart/packs";
10
+ import { createRecipe, decodePack, resolveRecipe, toAnimationStripPng, toPng } from "@monkeyart/composer";
9
11
  import { homedir } from "node:os";
10
12
  import { pathToFileURL } from "node:url";
11
- import { createRecipe, decodePack, resolveRecipe, toAnimationStripPng, toPng } from "@monkeyart/composer";
12
- import { defaultPackId, packUrl } from "@monkeyart/packs";
13
13
  import { spawn } from "node:child_process";
14
14
  //#region src/lib/http.ts
15
15
  function readBody(req, maxBytes) {
@@ -1730,6 +1730,93 @@ function wantsJson(req) {
1730
1730
  return (req.headers.accept ?? "").includes("application/json");
1731
1731
  }
1732
1732
  //#endregion
1733
+ //#region src/lib/avatars.ts
1734
+ const EMBLEMS = {
1735
+ claude: "claude",
1736
+ codex: "codex",
1737
+ provider: "rings"
1738
+ };
1739
+ const DEFAULT_AVATAR_PACKS = [
1740
+ "monkey",
1741
+ "steampunk-bulldogs",
1742
+ "toad"
1743
+ ];
1744
+ function isPackId(id) {
1745
+ return packIds.includes(id);
1746
+ }
1747
+ var MonkeyartAvatars = class {
1748
+ #packs;
1749
+ #loaded = /* @__PURE__ */ new Map();
1750
+ #cache = /* @__PURE__ */ new Map();
1751
+ constructor(packs = DEFAULT_AVATAR_PACKS) {
1752
+ this.#packs = packs.length ? packs : [defaultPackId];
1753
+ }
1754
+ async roll(seed, engine, projectKey) {
1755
+ const id = this.#packFor(seed);
1756
+ const pack = await this.#load(id);
1757
+ const emblem = EMBLEMS[engine ?? "claude"];
1758
+ return {
1759
+ pack: id,
1760
+ recipe: createRecipe(pack, {
1761
+ seed,
1762
+ emblem: pack.emblems.some((e) => e.id === emblem) ? emblem : null,
1763
+ baseColor: projectAccent(projectKey ?? seed),
1764
+ backdrop: "blend"
1765
+ })
1766
+ };
1767
+ }
1768
+ async still(recipe) {
1769
+ return this.#render(recipe, "still", async (pack, resolved) => ({ png: await toPng(pack, resolved) }));
1770
+ }
1771
+ async busy(recipe) {
1772
+ const { pack } = this.#stored(recipe);
1773
+ if (!(await this.#load(pack)).animations.some((animation) => animation.id === "busy")) return;
1774
+ return this.#render(recipe, "busy", async (p, resolved) => {
1775
+ const strip = await toAnimationStripPng(p, resolved, "busy");
1776
+ return {
1777
+ png: strip.png,
1778
+ durations: strip.durations
1779
+ };
1780
+ });
1781
+ }
1782
+ #packFor(seed) {
1783
+ const prefix = seed.slice(0, seed.indexOf(":"));
1784
+ if (isPackId(prefix)) return prefix;
1785
+ const digest = createHash("sha256").update(seed).digest();
1786
+ return this.#packs[digest.readUInt32BE(0) % this.#packs.length];
1787
+ }
1788
+ #stored(recipe) {
1789
+ const held = recipe;
1790
+ if (held && typeof held === "object" && typeof held.pack === "string" && isPackId(held.pack) && held.recipe) return held;
1791
+ return {
1792
+ pack: defaultPackId,
1793
+ recipe
1794
+ };
1795
+ }
1796
+ async #render(recipe, kind, draw) {
1797
+ const stored = this.#stored(recipe);
1798
+ const pack = await this.#load(stored.pack);
1799
+ const key = `${kind}:${pack.checksum}:${JSON.stringify(stored.recipe)}`;
1800
+ const held = this.#cache.get(key);
1801
+ if (held) return held;
1802
+ const { recipe: resolved } = resolveRecipe(pack, stored.recipe);
1803
+ const image = {
1804
+ ...await draw(pack, resolved),
1805
+ etag: `"${createHash("sha256").update(key).digest("hex").slice(0, 32)}"`
1806
+ };
1807
+ this.#cache.set(key, image);
1808
+ return image;
1809
+ }
1810
+ #load(id) {
1811
+ let pack = this.#loaded.get(id);
1812
+ if (!pack) {
1813
+ pack = readFile(packUrl(id)).then((bytes) => decodePack(new Uint8Array(bytes)));
1814
+ this.#loaded.set(id, pack);
1815
+ }
1816
+ return pack;
1817
+ }
1818
+ };
1819
+ //#endregion
1733
1820
  //#region src/config.ts
1734
1821
  const CONFIG_BASENAMES = [
1735
1822
  "workerdeck.config.mjs",
@@ -1771,6 +1858,7 @@ const VALUED = /* @__PURE__ */ new Map([
1771
1858
  ["--approval-timeout", (f, v, name) => f.approvalTimeoutMs = parseDuration(v, name)],
1772
1859
  ["--engine-sleep-after", (f, v, name) => f.engineSleepAfterMs = parseDuration(v, name) ?? 0],
1773
1860
  ["--agent-sleep-after", (f, v, name) => f.agentSleepAfterMs = parseDuration(v, name) ?? 0],
1861
+ ["--avatar-packs", (f, v, name) => f.avatarPacks = parseAvatarPacks(v, name)],
1774
1862
  ["--effort-default", (f, v, name) => f.effortDefaults = {
1775
1863
  ...f.effortDefaults,
1776
1864
  ...parseEffortDefault(v, name)
@@ -1947,7 +2035,7 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
1947
2035
  if (parsed.origin !== origin) throw new ConfigError(`--cors-origin expects scheme://host[:port] with no path, got: ${origin}`);
1948
2036
  }
1949
2037
  if (!web && flags.open) throw new ConfigError("--open cannot be used with --no-web: there is no dashboard to open");
1950
- const { port: _p, host: _h, auth: _a, stateDir: _s, allowedHosts: _ah, insecureHosts: _ih, webRoot: _w, web: _web, keepAwake: _ka, corsOrigins: _cors, apns: _ap, ...serverOptions } = loaded.options;
2038
+ const { port: _p, host: _h, auth: _a, stateDir: _s, allowedHosts: _ah, insecureHosts: _ih, webRoot: _w, web: _web, keepAwake: _ka, corsOrigins: _cors, apns: _ap, avatarPacks: _packs, ...serverOptions } = loaded.options;
1951
2039
  const options = { ...serverOptions };
1952
2040
  if (cwdRoots?.length) options.allowedCwdRoots = cwdRoots;
1953
2041
  if (fsRoots?.length || flags.fsWrite) options.hostFiles = {
@@ -1996,6 +2084,7 @@ function resolveInstanceConfig(flags, loaded, env = process.env, cwd = process.c
1996
2084
  web,
1997
2085
  keepAwake,
1998
2086
  corsOrigins,
2087
+ avatarPacks: resolveAvatarPacks(flags.avatarPacks ?? loaded.options.avatarPacks),
1999
2088
  apns: resolveApns(loaded),
2000
2089
  open: flags.open ?? false,
2001
2090
  profileStore,
@@ -2017,6 +2106,15 @@ function resolveApns(loaded) {
2017
2106
  keyFile: resolve(base, apns.keyFile)
2018
2107
  };
2019
2108
  }
2109
+ function parseAvatarPacks(value, name) {
2110
+ const packs = value.split(",").map((pack) => pack.trim()).filter(Boolean);
2111
+ for (const pack of packs) if (!isPackId(pack)) throw new ConfigError(`${name}: unknown avatar pack "${pack}" (known: ${packIds.join(", ")})`);
2112
+ return packs;
2113
+ }
2114
+ function resolveAvatarPacks(packs) {
2115
+ if (packs === void 0) return;
2116
+ return parseAvatarPacks(packs.join(","), "avatarPacks");
2117
+ }
2020
2118
  //#endregion
2021
2119
  //#region src/auth/login-page.ts
2022
2120
  function escapeHtml(value) {
@@ -2093,57 +2191,6 @@ function renderLoginPage(options) {
2093
2191
  `;
2094
2192
  }
2095
2193
  //#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
2147
2194
  //#region src/lib/keep-awake.ts
2148
2195
  const AWAKE_STATUSES = /* @__PURE__ */ new Set([
2149
2196
  "starting",
@@ -2448,7 +2495,7 @@ async function startInstance(config, options = {}) {
2448
2495
  parking,
2449
2496
  shell,
2450
2497
  spend,
2451
- avatars: config.options.avatars ?? new MonkeyartAvatars(),
2498
+ avatars: config.options.avatars ?? new MonkeyartAvatars(config.avatarPacks),
2452
2499
  notifications: apns === void 0 ? config.options.notifications : {
2453
2500
  ...config.options.notifications,
2454
2501
  onNotification: (notification) => {
@@ -2575,4 +2622,4 @@ function bannerLines({ url, config, generated, authEnabled, push }) {
2575
2622
  //#endregion
2576
2623
  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 };
2577
2624
 
2578
- //# sourceMappingURL=instance-C5j14c33.mjs.map
2625
+ //# sourceMappingURL=instance-BMMh875G.mjs.map