dsh-codex-subscription 2.0.1 → 2.1.0-beta.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/lib/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
2
5
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
3
6
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
4
7
  import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
5
8
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
9
  import z from "@deepseek-ai/schemastery";
7
- import { createHash, randomBytes, randomUUID } from "node:crypto";
8
10
  import { execFile, spawn } from "node:child_process";
9
11
  import { request } from "node:https";
10
12
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -14,9 +16,7 @@ import { HttpsProxyAgent } from "https-proxy-agent";
14
16
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
15
17
  import { createModels } from "@earendil-works/pi-ai";
16
18
  import { WebError } from "@deepseek-ai/dsh-web";
17
- import { defineTool } from "@deepseek-ai/dsh-tools";
18
19
  import { constants } from "node:fs";
19
- import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
20
20
  import { dirname, join, resolve } from "node:path";
21
21
  //#region src/image-models.js
22
22
  const DEFAULT_IMAGE_MODEL = "gpt-image-2";
@@ -59,7 +59,8 @@ const IMAGE_FEATURE_DEFAULTS = Object.freeze({
59
59
  imageEditing: true,
60
60
  imageViewer: true,
61
61
  imageAnnotations: true,
62
- imageSketch: true
62
+ imageSketch: false,
63
+ imageSketchAgent: false
63
64
  });
64
65
  function readImageFeatures(value = {}) {
65
66
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -346,7 +347,11 @@ const RPC_ENDPOINTS = Object.freeze([
346
347
  "reset-credit/inspect",
347
348
  "reset-credit/prepare",
348
349
  "reset-credit/consume",
349
- "image/original/chunk"
350
+ "image/original/chunk",
351
+ "sketch/connect",
352
+ "sketch/poll",
353
+ "sketch/result",
354
+ "sketch/disconnect"
350
355
  ]);
351
356
  //#endregion
352
357
  //#region src/subscription-transport.js
@@ -401,6 +406,220 @@ function registerSubscriptionTransport(connection, handler) {
401
406
  };
402
407
  }
403
408
  //#endregion
409
+ //#region src/sketch-agent-bridge.js
410
+ function createSketchAgentBridge({ enabled, now = Date.now, timeoutMs = 2e4 }) {
411
+ const sessions = /* @__PURE__ */ new Map();
412
+ const fail = (entry, message) => {
413
+ for (const task of entry.tasks.values()) task.reject(Error(message));
414
+ entry.tasks.clear();
415
+ };
416
+ const find = (payload) => {
417
+ const entry = sessions.get(payload.sessionId);
418
+ if (!entry || entry.token !== payload.token || now() - entry.seen > 1e4) throw Error("Sketch connection expired");
419
+ entry.seen = now();
420
+ return entry;
421
+ };
422
+ return {
423
+ async rpc(endpoint, payload) {
424
+ try {
425
+ if (!enabled()) throw Error("Sketch is disabled");
426
+ if (!payload || typeof payload.sessionId !== "string" || !payload.sessionId.length || payload.sessionId.length > 200) throw Error("Invalid session");
427
+ if (endpoint === "sketch/connect") {
428
+ for (const [id, entry] of sessions) if (now() - entry.seen >= 1e4) {
429
+ fail(entry, "Sketch connection expired");
430
+ sessions.delete(id);
431
+ }
432
+ const previous = sessions.get(payload.sessionId);
433
+ if (previous && now() - previous.seen < 1e4) throw Error("Another board is connected to this session");
434
+ if (previous) fail(previous, "Sketch connection replaced");
435
+ const entry = {
436
+ token: randomUUID(),
437
+ seen: now(),
438
+ tasks: /* @__PURE__ */ new Map()
439
+ };
440
+ sessions.set(payload.sessionId, entry);
441
+ return {
442
+ ok: true,
443
+ value: { token: entry.token }
444
+ };
445
+ }
446
+ const entry = find(payload);
447
+ if (endpoint === "sketch/poll") return {
448
+ ok: true,
449
+ value: [...entry.tasks].filter(([, t]) => !t.delivered).map(([id, t]) => {
450
+ t.delivered = true;
451
+ return {
452
+ id,
453
+ request: t.request,
454
+ expiresAt: t.expiresAt
455
+ };
456
+ })
457
+ };
458
+ if (endpoint === "sketch/disconnect") {
459
+ fail(entry, "Sketch board closed");
460
+ sessions.delete(payload.sessionId);
461
+ return {
462
+ ok: true,
463
+ value: null
464
+ };
465
+ }
466
+ if (endpoint === "sketch/result") {
467
+ const task = entry.tasks.get(payload.id);
468
+ if (task) {
469
+ entry.tasks.delete(payload.id);
470
+ payload.error ? task.reject(Error(String(payload.error).slice(0, 500))) : task.resolve(payload.value);
471
+ }
472
+ return {
473
+ ok: true,
474
+ value: null
475
+ };
476
+ }
477
+ throw Error("Unknown sketch route");
478
+ } catch (error) {
479
+ return {
480
+ ok: false,
481
+ error: {
482
+ code: "invalid-input",
483
+ message: error.message,
484
+ details: { issues: [] }
485
+ }
486
+ };
487
+ }
488
+ },
489
+ request(sessionId, request, signal) {
490
+ if (!enabled()) return Promise.reject(Error("Sketch is disabled"));
491
+ const entry = sessions.get(sessionId);
492
+ if (!entry || now() - entry.seen > 1e4) return Promise.reject(Error("Switch to this session in DSH with sketch editing enabled"));
493
+ if (entry.tasks.size) return Promise.reject(Error("Another sketch operation is pending"));
494
+ if (JSON.stringify(request).length > 2e6) return Promise.reject(Error("Sketch batch is too large"));
495
+ return new Promise((resolve, reject) => {
496
+ const id = randomUUID();
497
+ const finish = (callback, value) => {
498
+ clearTimeout(timer);
499
+ signal?.removeEventListener("abort", abort);
500
+ entry.tasks.delete(id);
501
+ callback(value);
502
+ };
503
+ const abort = () => finish(reject, Error("Sketch operation interrupted; inspect before retrying"));
504
+ const timer = setTimeout(() => finish(reject, Error("Sketch response timed out; inspect before retrying")), timeoutMs);
505
+ entry.tasks.set(id, {
506
+ request,
507
+ expiresAt: now() + timeoutMs,
508
+ delivered: false,
509
+ resolve: (value) => finish(resolve, value),
510
+ reject: (error) => finish(reject, error)
511
+ });
512
+ signal?.addEventListener("abort", abort, { once: true });
513
+ if (signal?.aborted) abort();
514
+ });
515
+ },
516
+ dispose() {
517
+ for (const entry of sessions.values()) fail(entry, "Sketch service stopped");
518
+ sessions.clear();
519
+ }
520
+ };
521
+ }
522
+ //#endregion
523
+ //#region src/sketch-agent-tool.js
524
+ function createSketchAgentTool(bridge, attachments) {
525
+ return defineTool({
526
+ name: "codex_sketch",
527
+ description: "Edit the sketch board in this session using native editable strokes and layers. Only use when asked to draw or edit a sketch. Start with inspect for the documentId, revision and command reference. Apply atomic batches, preview between stages, and save the finished draft. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
528
+ parameters: {
529
+ action: {
530
+ type: "string",
531
+ required: true,
532
+ enum: [
533
+ "inspect",
534
+ "apply",
535
+ "preview",
536
+ "save"
537
+ ]
538
+ },
539
+ documentId: {
540
+ type: "string",
541
+ description: "From inspect; required except for inspect."
542
+ },
543
+ revision: {
544
+ type: "integer",
545
+ description: "From latest response; required for apply/save."
546
+ },
547
+ requestId: {
548
+ type: "string",
549
+ description: "Unique id for apply/save; exact retries are deduplicated."
550
+ },
551
+ commands: {
552
+ type: "string",
553
+ description: "JSON array of native commands described by inspect. Required for apply."
554
+ },
555
+ name: {
556
+ type: "string",
557
+ description: "Draft name for save."
558
+ }
559
+ },
560
+ timeoutMs: 25e3,
561
+ isConcurrencySafe: () => false,
562
+ async execute(args, exec) {
563
+ const sessionId = exec.agent?.id;
564
+ if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
565
+ const request = { ...args };
566
+ if (args.action === "apply") try {
567
+ request.commands = JSON.parse(args.commands);
568
+ } catch {
569
+ throw Error("commands must be a JSON array");
570
+ }
571
+ const value = await bridge.request(sessionId, request, exec.signal);
572
+ if (value.png) {
573
+ if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
574
+ const image = await attachments.saveImage({
575
+ data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
576
+ mediaType: "image/png",
577
+ name: "sketch-preview.png"
578
+ });
579
+ const { png, ...snapshot } = value;
580
+ return {
581
+ ...snapshot,
582
+ image
583
+ };
584
+ }
585
+ return value;
586
+ },
587
+ output: {
588
+ schema: {
589
+ type: "object",
590
+ additionalProperties: true
591
+ },
592
+ render: (_args, value) => [{
593
+ type: "text",
594
+ text: JSON.stringify({
595
+ ...value,
596
+ image: void 0
597
+ })
598
+ }, ...value.image ? [{
599
+ type: "image",
600
+ attachment: value.image
601
+ }] : []]
602
+ }
603
+ });
604
+ }
605
+ //#endregion
606
+ //#region src/sketch-codec-route.js
607
+ function registerSketchCodec(connection) {
608
+ let source;
609
+ return connection.fetch.register({
610
+ path: "/api/codex-subscription/sketch-psd-worker",
611
+ methods: ["GET"],
612
+ requestBody: "buffered",
613
+ async fetch() {
614
+ source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
615
+ return new Response(source, { headers: {
616
+ "content-type": "text/javascript; charset=utf-8",
617
+ "cache-control": "no-store"
618
+ } });
619
+ }
620
+ });
621
+ }
622
+ //#endregion
404
623
  //#region src/account-vault.js
405
624
  const VERSION = 1;
406
625
  const DEFAULT_LABEL = "Account 1";
@@ -1615,7 +1834,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1615
1834
  }
1616
1835
  //#endregion
1617
1836
  //#region src/version.js
1618
- const PACKAGE_VERSION = "2.0.1";
1837
+ const PACKAGE_VERSION = "2.1.0-beta.1";
1619
1838
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1620
1839
  //#endregion
1621
1840
  //#region src/model-catalog.js
@@ -4011,7 +4230,27 @@ function apply(ctx) {
4011
4230
  usageReader,
4012
4231
  fetch: (input, init) => network.fetch("quota-reset", input, init)
4013
4232
  });
4014
- const handler = createSubscriptionRpcHandler({
4233
+ const sketchBridge = createSketchAgentBridge({ enabled: () => settings.get().imageSketchAgent && settings.get().imageSketch && settings.get().imageEditing });
4234
+ ctx.effect(() => {
4235
+ let dispose;
4236
+ const sync = () => {
4237
+ const value = settings.get();
4238
+ if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments));
4239
+ else {
4240
+ dispose?.();
4241
+ dispose = void 0;
4242
+ sketchBridge.dispose();
4243
+ }
4244
+ };
4245
+ sync();
4246
+ const unwatch = settings.watch(sync);
4247
+ return () => {
4248
+ unwatch();
4249
+ dispose?.();
4250
+ sketchBridge.dispose();
4251
+ };
4252
+ }, "codex-subscription: native sketch tool");
4253
+ const subscriptionHandler = createSubscriptionRpcHandler({
4015
4254
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
4016
4255
  usageReader,
4017
4256
  resetCreditService,
@@ -4027,10 +4266,24 @@ function apply(ctx) {
4027
4266
  originalImages,
4028
4267
  resolveInheritedOriginal: (sessionId, assetId) => inheritedOriginalImageRef(ctx.get?.("sessions")?.get?.(sessionId), assetId)
4029
4268
  });
4269
+ const handler = (endpoint, payload, signal) => endpoint.startsWith("sketch/") ? sketchBridge.rpc(endpoint, payload) : subscriptionHandler(endpoint, payload, signal);
4030
4270
  ctx.effect(() => {
4031
4271
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
4032
4272
  }, "codex-subscription: official model catalog");
4033
- ctx.inject(["connection"], (connectionContext) => connectionContext.effect(() => registerSubscriptionTransport(connectionContext.connection, handler), "codex-subscription: DSH-trusted account RPC"));
4273
+ ctx.inject(["connection"], (connectionContext) => connectionContext.effect(() => {
4274
+ const transport = registerSubscriptionTransport(connectionContext.connection, handler);
4275
+ let codec;
4276
+ try {
4277
+ codec = registerSketchCodec(connectionContext.connection);
4278
+ } catch (error) {
4279
+ transport();
4280
+ throw error;
4281
+ }
4282
+ return () => {
4283
+ codec();
4284
+ transport();
4285
+ };
4286
+ }, "codex-subscription: DSH-trusted account RPC"));
4034
4287
  }
4035
4288
  //#endregion
4036
4289
  export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, normalizeContextMode, normalizeCustomContextWindow, openCodexAuthUrl, parseCodexUsage };