@workerdeck/server 2.10.0 → 2.11.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/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
2
  import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, CostLedgerState, EngineAdapter, EngineSessionOptions, HostToolDefinition, LanguageModel, McpConnection, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionCall, ToolExecutionResult, ToolExecutor, ToolSet } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
- import { ByModel, CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, ProfileSpend, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
4
+ import { ByModel, CreateSessionRequest, JobEvent, MessageAttachment, ModelRate, ProfileEngine, ProfileInfo, ProfileSpend, ProfileUsage, ProviderConfig, SdkSessionSummary, ServerFrame, SessionCapability, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
5
5
  //#region src/services/bridge.d.ts
6
6
  type BridgeHubOptions = {
7
7
  timeoutMs?: number;
@@ -139,6 +139,14 @@ declare class SessionParkManager {
139
139
  close(): void;
140
140
  }
141
141
  //#endregion
142
+ //#region src/services/peers.d.ts
143
+ type PeerServiceOptions = {
144
+ enabled?: boolean;
145
+ maxMessageChars?: number;
146
+ perMinute?: number;
147
+ maxHops?: number;
148
+ };
149
+ //#endregion
142
150
  //#region src/services/profile-store.d.ts
143
151
  type ProfileStore = {
144
152
  list(): ProfileInfo[] | Promise<ProfileInfo[]>;
@@ -223,6 +231,7 @@ type WorkerServerOptions = {
223
231
  requireAvailableProfile?: boolean;
224
232
  listSdkSessions?: SdkSessionLister;
225
233
  queue?: QueueServerOptions;
234
+ peers?: PeerServiceOptions;
226
235
  notifications?: SessionNotificationOptions;
227
236
  bridge?: BridgeHubOptions;
228
237
  parking?: {
@@ -232,6 +241,9 @@ type WorkerServerOptions = {
232
241
  persistLive?: boolean;
233
242
  onError?: (error: unknown, context: ParkErrorContext) => void;
234
243
  };
244
+ pricing?: {
245
+ overrides?: Record<string, ModelRate>;
246
+ };
235
247
  spend?: {
236
248
  store?: SpendStore;
237
249
  monthlySubscriptionUsd?: Record<string, number>;
package/build/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createServer } from "node:http";
2
2
  import { WebSocketServer } from "ws";
3
- import { BrowserBridgeExecutor, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, normalizeMediaType } from "@workerdeck/core";
3
+ import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, installPeerDirectory, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines } from "@workerdeck/core";
4
4
  import { JobQueue } from "@workerdeck/queue";
5
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, sessionState, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
5
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { constants as constants$1, homedir } from "node:os";
@@ -1027,7 +1027,8 @@ const EPHEMERAL_CONFIG_KEYS = [
1027
1027
  "queryFn",
1028
1028
  "historyFn",
1029
1029
  "extraOptions",
1030
- "env"
1030
+ "env",
1031
+ "peers"
1031
1032
  ];
1032
1033
  function toDurableRecord(record) {
1033
1034
  const config = { ...record.config };
@@ -1290,6 +1291,7 @@ const PROJECT_FILE = ".workerdeck.json";
1290
1291
  const MAX_PROJECT_FILE_BYTES = 65536;
1291
1292
  const MAX_NAME_CHARS = 80;
1292
1293
  const GLYPH_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1294
+ const SHORTCODE_RE = /^[A-Z0-9]{2,5}$/;
1293
1295
  const DEFAULT_TTL_MS = 3e4;
1294
1296
  const SWEEP_ABOVE = 256;
1295
1297
  var ProjectInfoService = class {
@@ -1361,15 +1363,20 @@ function tryLoad(file, root) {
1361
1363
  const raw = parsed;
1362
1364
  const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, MAX_NAME_CHARS) : basename(root);
1363
1365
  const icon = classifyIcon(raw.icon, root);
1366
+ const shortcode = classifyShortcode(raw.shortcode);
1364
1367
  return {
1365
1368
  project: {
1366
1369
  name,
1367
1370
  root,
1368
- ...icon ? { icon: icon.wire } : {}
1371
+ ...icon ? { icon: icon.wire } : {},
1372
+ ...shortcode ? { shortcode } : {}
1369
1373
  },
1370
1374
  ...icon?.resolved ? { icon: icon.resolved } : {}
1371
1375
  };
1372
1376
  }
1377
+ function classifyShortcode(value) {
1378
+ return typeof value === "string" && SHORTCODE_RE.test(value) ? value : void 0;
1379
+ }
1373
1380
  function classifyIcon(value, root) {
1374
1381
  if (typeof value !== "string") return;
1375
1382
  const declared = value.trim();
@@ -1806,7 +1813,8 @@ function attachClient(ctx, ws, runner, req, access) {
1806
1813
  protocolVersion: PROTOCOL_VERSION,
1807
1814
  session: ctx.projects.withProject(runner.info()),
1808
1815
  replayingFrom: afterSeq,
1809
- ...shellPermitted(ctx.shell, runner, access.operator) ? { shell: true } : {}
1816
+ ...shellPermitted(ctx.shell, runner, access.operator) ? { shell: true } : {},
1817
+ ...ctx.pricingOverrides ? { pricingOverrides: ctx.pricingOverrides } : {}
1810
1818
  });
1811
1819
  const unsubscribe = runner.subscribe((event) => send({
1812
1820
  type: "event",
@@ -3030,6 +3038,143 @@ function createFileSpendStore(path, onError) {
3030
3038
  };
3031
3039
  }
3032
3040
  //#endregion
3041
+ //#region src/services/peers.ts
3042
+ const DEFAULT_PER_MINUTE = 10;
3043
+ const DEFAULT_MAX_HOPS = 12;
3044
+ const WINDOW_MS = 6e4;
3045
+ function visible(from, to) {
3046
+ return to.id !== from.id && scopeMatches(from.scope, to.scope);
3047
+ }
3048
+ function createPeerService(deps) {
3049
+ const perMinute = deps.options?.perMinute ?? DEFAULT_PER_MINUTE;
3050
+ const maxChars = deps.options?.maxMessageChars ?? PEER_MESSAGE_MAX_CHARS;
3051
+ const maxHops = deps.options?.maxHops ?? DEFAULT_MAX_HOPS;
3052
+ const sent = /* @__PURE__ */ new Map();
3053
+ const inbound = /* @__PURE__ */ new Map();
3054
+ const registry = () => {
3055
+ const found = deps.refs.registry;
3056
+ if (!found) throw new Error("peer messaging is not ready");
3057
+ return found;
3058
+ };
3059
+ const allSessions = async () => {
3060
+ const live = registry().list();
3061
+ const dormant = await deps.refs.parking?.listInfo() ?? [];
3062
+ return [...live, ...dormant].map((info) => deps.projects.withProject(info));
3063
+ };
3064
+ const infoOf = async (id) => {
3065
+ const live = registry().get(id)?.info();
3066
+ if (live) return deps.projects.withProject(live);
3067
+ const record = await deps.refs.parking?.get(id);
3068
+ return record ? deps.projects.withProject(record.info) : void 0;
3069
+ };
3070
+ const sender = async (from) => {
3071
+ const info = await infoOf(from);
3072
+ if (!info) throw new Error(`unknown sender session: ${from}`);
3073
+ return info;
3074
+ };
3075
+ const underRateLimit = (from, to) => {
3076
+ const key = `${from}->${to}`;
3077
+ const now = Date.now();
3078
+ const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
3079
+ if (recent.length >= perMinute) {
3080
+ sent.set(key, recent);
3081
+ return false;
3082
+ }
3083
+ recent.push(now);
3084
+ sent.set(key, recent);
3085
+ return true;
3086
+ };
3087
+ const list = async (from) => {
3088
+ const me = await sender(from);
3089
+ return (await allSessions()).filter((info) => visible(me, info)).sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt)).map(peerSummary);
3090
+ };
3091
+ const peek = async (from, sessionId, options) => {
3092
+ const me = await sender(from);
3093
+ const info = await infoOf(sessionId);
3094
+ if (!info || !visible(me, info)) return;
3095
+ const limit = Math.min(Math.max(options?.recent ?? PEER_RECENT_DEFAULT, 0), PEER_RECENT_MAX);
3096
+ const runner = registry().get(sessionId);
3097
+ const events = [];
3098
+ if (runner && limit > 0) runner.subscribe((event) => events.push(event), 0, {
3099
+ truncateResults: true,
3100
+ imageRefs: true
3101
+ })();
3102
+ return {
3103
+ ...peerSummary(info),
3104
+ live: runner !== void 0,
3105
+ checklist: info.checklist,
3106
+ pendingApprovals: runner ? runner.pendingApprovals.map((request) => request.title ?? request.toolName) : [],
3107
+ recent: recentLines(events, limit)
3108
+ };
3109
+ };
3110
+ const send = async (from, sessionId, text, options) => {
3111
+ const me = await sender(from);
3112
+ if (sessionId === from) return {
3113
+ delivered: false,
3114
+ reason: "that is this session"
3115
+ };
3116
+ if (text.length > maxChars) return {
3117
+ delivered: false,
3118
+ reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
3119
+ };
3120
+ const target = await infoOf(sessionId);
3121
+ if (!target || !visible(me, target)) return {
3122
+ delivered: false,
3123
+ reason: `no such session: ${sessionId}`
3124
+ };
3125
+ if (target.status === "closed" || target.status === "failed") return {
3126
+ delivered: false,
3127
+ reason: `session ${sessionId} is ${target.status}`
3128
+ };
3129
+ const hops = [...options?.hops ?? inbound.get(from) ?? [], from];
3130
+ if (hops.length > maxHops) return {
3131
+ delivered: false,
3132
+ reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
3133
+ };
3134
+ if (!underRateLimit(from, sessionId)) return {
3135
+ delivered: false,
3136
+ reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
3137
+ };
3138
+ const runner = await deps.refs.parking?.ensureLive(sessionId) ?? registry().get(sessionId);
3139
+ if (!runner) return {
3140
+ delivered: false,
3141
+ reason: `session ${sessionId} could not be woken`
3142
+ };
3143
+ const before = runner.info().status;
3144
+ const origin = {
3145
+ kind: "peer",
3146
+ sessionId: from,
3147
+ name: me.title,
3148
+ engine: me.engine,
3149
+ hops
3150
+ };
3151
+ try {
3152
+ runner.sendMessage(text, void 0, { origin });
3153
+ } catch (error) {
3154
+ return {
3155
+ delivered: false,
3156
+ reason: error instanceof Error ? error.message : String(error)
3157
+ };
3158
+ }
3159
+ inbound.set(sessionId, hops);
3160
+ return {
3161
+ delivered: true,
3162
+ sessionId,
3163
+ name: target.title,
3164
+ queued: before === "running" || before === "awaiting_approval"
3165
+ };
3166
+ };
3167
+ const watch = (runner) => runner.subscribe((event) => {
3168
+ if (event.type === "user_message" && !event.origin && !event.synthetic && event.parentToolUseId == null && !event.replay) inbound.delete(runner.id);
3169
+ }, runner.info().lastSeq);
3170
+ return {
3171
+ list,
3172
+ peek,
3173
+ send,
3174
+ watch
3175
+ };
3176
+ }
3177
+ //#endregion
3033
3178
  //#region src/services/registry.ts
3034
3179
  var SessionRegistry = class {
3035
3180
  #sessions = /* @__PURE__ */ new Map();
@@ -3209,7 +3354,11 @@ function createSessionFactory(deps) {
3209
3354
  return engineOf(profile) === "claude" ? claudeSessionEnv(profile, process.env) : process.env;
3210
3355
  }
3211
3356
  };
3212
- const buildRunner = async (config, restore, id) => {
3357
+ const buildRunner = async (built, restore, id) => {
3358
+ const config = deps.peers ? {
3359
+ ...built,
3360
+ peers: deps.peers
3361
+ } : built;
3213
3362
  const name = config.profile;
3214
3363
  const profile = name !== void 0 ? profiles.get(name) : void 0;
3215
3364
  if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
@@ -3330,6 +3479,8 @@ function createWorkerServer(options = {}) {
3330
3479
  const corsOrigins = options.cors?.origins.length ? new Set(options.cors.origins) : void 0;
3331
3480
  const maxBodyBytes = options.maxBodyBytes ?? 1048576;
3332
3481
  const adapterFor = (engine) => options.engines?.[engine ?? "claude"] ?? getEngineAdapter(engine);
3482
+ const pricing = setPricingOverrides(options.pricing?.overrides);
3483
+ if (pricing.dropped.length > 0) console.warn(`[workerdeck] Ignoring pricing.overrides for ${pricing.dropped.join(", ")}: each entry needs input, output, cacheWrite5m, cacheWrite1h and cacheRead in USD per million tokens`);
3333
3484
  const profileDefaultModels = /* @__PURE__ */ new Map();
3334
3485
  const profileUsage = new ProfileUsageTracker();
3335
3486
  const spendLedger = new SpendLedger({
@@ -3375,6 +3526,13 @@ function createWorkerServer(options = {}) {
3375
3526
  if (invalid) throw new Error(`createWorkerServer: ${invalid}`);
3376
3527
  }
3377
3528
  const refs = {};
3529
+ const projects = new ProjectInfoService();
3530
+ const peers = options.peers?.enabled === false ? void 0 : createPeerService({
3531
+ refs,
3532
+ projects,
3533
+ options: options.peers
3534
+ });
3535
+ if (peers) installPeerDirectory(peers);
3378
3536
  const factory = createSessionFactory({
3379
3537
  adapterFor,
3380
3538
  profiles,
@@ -3384,6 +3542,7 @@ function createWorkerServer(options = {}) {
3384
3542
  disableBypassPermissions: options.disableBypassPermissions,
3385
3543
  approvalTimeoutMs: options.approvalTimeoutMs,
3386
3544
  requireApiKey: options.requireApiKey,
3545
+ peers: peers ? peerDirectoryHandle() : void 0,
3387
3546
  refs
3388
3547
  });
3389
3548
  const availability = new AvailabilityTracker({
@@ -3392,7 +3551,6 @@ function createWorkerServer(options = {}) {
3392
3551
  adapterFor,
3393
3552
  sessionEnvFor: factory.sessionEnvFor
3394
3553
  });
3395
- const projects = new ProjectInfoService();
3396
3554
  const notifier = new SessionNotifier({
3397
3555
  ...options.notifications,
3398
3556
  decorateInfo: (info) => projects.withProject(info)
@@ -3405,7 +3563,8 @@ function createWorkerServer(options = {}) {
3405
3563
  producedFiles.watch(runner),
3406
3564
  profileUsage.watch(runner),
3407
3565
  spendLedger.watch(runner),
3408
- shell?.watch(runner)
3566
+ shell?.watch(runner),
3567
+ peers?.watch(runner)
3409
3568
  ];
3410
3569
  const profile = runner.info().profile;
3411
3570
  if (profile) detachers.push(runner.subscribe((event) => {
@@ -3514,7 +3673,8 @@ function createWorkerServer(options = {}) {
3514
3673
  hostFilesWritable: options.hostFiles?.write === true,
3515
3674
  maxHostFileBytes: options.hostFiles?.maxFileBytes ?? 1048576,
3516
3675
  maxHostDirEntries: options.hostFiles?.maxEntries ?? 5e3,
3517
- shell
3676
+ shell,
3677
+ pricingOverrides: Object.keys(pricing.overrides).length > 0 ? pricing.overrides : void 0
3518
3678
  };
3519
3679
  const handleRequest = async (req, res) => {
3520
3680
  const pathname = new URL(req.url ?? "/", "http://internal").pathname;