dsh-codex-subscription 2.0.0 → 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.0";
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
@@ -3882,6 +4101,7 @@ function apply(ctx) {
3882
4101
  displayName: "ChatGPT subscription",
3883
4102
  piProvider: provider,
3884
4103
  configuredMaxTokens: /* @__PURE__ */ new Map(),
4104
+ modelErrors: /* @__PURE__ */ new Map(),
3885
4105
  streamIdleTimeoutMs: 600 * 1e3,
3886
4106
  maxRequestImageBytes: MAX_REQUEST_IMAGE_BYTES,
3887
4107
  requestImagePixelBudget: REQUEST_IMAGE_PIXEL_BUDGET,
@@ -4010,7 +4230,27 @@ function apply(ctx) {
4010
4230
  usageReader,
4011
4231
  fetch: (input, init) => network.fetch("quota-reset", input, init)
4012
4232
  });
4013
- 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({
4014
4254
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
4015
4255
  usageReader,
4016
4256
  resetCreditService,
@@ -4026,10 +4266,24 @@ function apply(ctx) {
4026
4266
  originalImages,
4027
4267
  resolveInheritedOriginal: (sessionId, assetId) => inheritedOriginalImageRef(ctx.get?.("sessions")?.get?.(sessionId), assetId)
4028
4268
  });
4269
+ const handler = (endpoint, payload, signal) => endpoint.startsWith("sketch/") ? sketchBridge.rpc(endpoint, payload) : subscriptionHandler(endpoint, payload, signal);
4029
4270
  ctx.effect(() => {
4030
4271
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
4031
4272
  }, "codex-subscription: official model catalog");
4032
- 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"));
4033
4287
  }
4034
4288
  //#endregion
4035
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 };