dsh-packycode-api 1.1.9 → 1.2.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/lib/index.js CHANGED
@@ -2,10 +2,10 @@ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
4
4
  import z from "@deepseek-ai/schemastery";
5
- import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
+ import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
6
6
  import { spawn } from "node:child_process";
7
7
  import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
8
- import { mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
8
+ import { mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
9
9
  import { createServer } from "node:http";
10
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
11
11
  import { credentialKey } from "@deepseek-ai/dsh-credentials";
@@ -184,6 +184,8 @@ const CHANNEL = "/packycode-api";
184
184
  const SETTINGS_NAMESPACE = "packycode-api";
185
185
  /** Credential record id inside the plugin's own scope. */
186
186
  const KEY_RECORD_ID = "keys";
187
+ /** Image credentials never participate in the conversation key ring. */
188
+ const IMAGE_KEY_RECORD_ID = "image-keys";
187
189
  /**
188
190
  * Model-traffic host. Each model declares the protocol its gateway route
189
191
  * actually serves: `cf.api.fan/v1` answers chat/completions and responses,
@@ -270,6 +272,51 @@ function accountHostOrder(hosts) {
270
272
  ]);
271
273
  }
272
274
  //#endregion
275
+ //#region src/secret-redactor.js
276
+ /**
277
+ * Redaction of plugin-owned credentials and ephemeral bridge capabilities.
278
+ * Work on decoded strings where possible; JSON-escaped forms also cover
279
+ * diagnostics containing serialized data. Delta redaction holds only the
280
+ * longest suffix that could still become a secret in the next chunk.
281
+ */
282
+ const strings = (values) => [...new Set(values.filter((value) => typeof value === "string" && value !== ""))].sort((a, b) => b.length - a.length);
283
+ const variants = (values) => strings(values.flatMap((value) => typeof value === "string" ? [value, JSON.stringify(value).slice(1, -1)] : []));
284
+ function redactSecrets(text, values) {
285
+ let safe = String(text);
286
+ for (const secret of variants(values)) safe = safe.split(secret).join("[redacted]");
287
+ return safe;
288
+ }
289
+ /** Protect secrets fragmented across consecutive decoded text deltas. */
290
+ function createDeltaRedactor(values) {
291
+ const secrets = variants(values);
292
+ let pending = "";
293
+ return {
294
+ write(text) {
295
+ const safe = redactSecrets(pending + text, secrets);
296
+ let held = 0;
297
+ for (const secret of secrets) {
298
+ const maximum = Math.min(safe.length, secret.length - 1);
299
+ for (let length = maximum; length > held; length--) if (safe.endsWith(secret.slice(0, length))) {
300
+ held = length;
301
+ break;
302
+ }
303
+ }
304
+ pending = held === 0 ? "" : safe.slice(-held);
305
+ return held === 0 ? safe : safe.slice(0, -held);
306
+ },
307
+ /**
308
+ * At a block/tool boundary, mask a still-ambiguous prefix so a subsequent
309
+ * block cannot reconstruct the credential. At end of output it is safe
310
+ * to flush an incomplete prefix literally; no later delta can complete it.
311
+ */
312
+ flush({ boundary = false } = {}) {
313
+ const result = boundary && pending !== "" ? "[redacted]" : pending;
314
+ pending = "";
315
+ return result;
316
+ }
317
+ };
318
+ }
319
+ //#endregion
273
320
  //#region src/balance.js
274
321
  /**
275
322
  * Account and per-key balance.
@@ -376,7 +423,11 @@ async function getJson(url, key, { fetchImpl, timeoutMs, signal }) {
376
423
  throw new Error(error.message, { cause: error });
377
424
  }
378
425
  if (!response.ok) throw new Error(statusMessage(response.status));
379
- return response.json();
426
+ try {
427
+ return await response.json();
428
+ } catch {
429
+ throw new Error("the gateway returned unreadable balance data");
430
+ }
380
431
  }
381
432
  async function withFallback(hosts, path, key, options) {
382
433
  const tried = [];
@@ -401,7 +452,7 @@ async function withFallback(hosts, path, key, options) {
401
452
  * @param options - key listing, host resolution, and transport knobs.
402
453
  * @returns `{ read({force, signal}), invalidate() }`.
403
454
  */
404
- function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now = Date.now, ttlMs = DEFAULT_BALANCE_TTL_MS, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
455
+ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now = Date.now, ttlMs = DEFAULT_BALANCE_TTL_MS, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, includeAccount = true } = {}) {
405
456
  if (typeof listKeys !== "function") throw new Error("PackyCode balance reader requires listKeys");
406
457
  if (typeof getHosts !== "function") throw new Error("PackyCode balance reader requires getHosts");
407
458
  /** Last successful per-key usage, by key id, and the last account figures. */
@@ -458,17 +509,21 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
458
509
  signal?.throwIfAborted();
459
510
  const hosts = getHosts();
460
511
  const keys = await listKeys();
512
+ const secrets = keys.map((entry) => entry.key);
513
+ const safe = (value) => redactSecrets(value, secrets);
461
514
  for (const id of lastGood.keys()) if (!keys.some((entry) => entry.id === id)) lastGood.delete(id);
462
515
  const entries = [];
463
516
  const usableIds = [];
464
517
  for (const entry of keys) {
465
518
  const base = {
466
519
  id: entry.id,
467
- label: entry.label,
520
+ label: safe(entry.label),
468
521
  disabled: entry.disabled === true
469
522
  };
470
523
  try {
471
524
  const usage = await loadKey(entry, hosts, signal);
525
+ if (usage.name !== void 0) usage.name = safe(usage.name);
526
+ if (usage.resetPeriod !== void 0) usage.resetPeriod = safe(usage.resetPeriod);
472
527
  lastGood.set(entry.id, {
473
528
  usage,
474
529
  fetchedAt: now()
@@ -480,7 +535,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
480
535
  usableIds.push(entry.id);
481
536
  } catch (error) {
482
537
  signal?.throwIfAborted();
483
- const message = error instanceof Error ? error.message : String(error);
538
+ const message = safe(error instanceof Error ? error.message : String(error));
484
539
  const previous = lastGood.get(entry.id);
485
540
  entries.push(previous === void 0 ? {
486
541
  ...base,
@@ -496,7 +551,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
496
551
  }
497
552
  }
498
553
  let account;
499
- try {
554
+ if (includeAccount) try {
500
555
  account = await loadAccount(keys, hosts, signal, usableIds);
501
556
  lastAccount = {
502
557
  account,
@@ -504,7 +559,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
504
559
  };
505
560
  } catch (error) {
506
561
  signal?.throwIfAborted();
507
- const message = error instanceof Error ? error.message : String(error);
562
+ const message = safe(error instanceof Error ? error.message : String(error));
508
563
  account = lastAccount === void 0 || !keys.some((entry) => entry.id === lastAccount.account.keyId) ? { error: message } : {
509
564
  ...lastAccount.account,
510
565
  stale: {
@@ -515,7 +570,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
515
570
  }
516
571
  return {
517
572
  keys: entries,
518
- account,
573
+ ...includeAccount ? { account } : {},
519
574
  hosts,
520
575
  fetchedAt: now()
521
576
  };
@@ -2225,51 +2280,6 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
2225
2280
  };
2226
2281
  }
2227
2282
  //#endregion
2228
- //#region src/secret-redactor.js
2229
- /**
2230
- * Redaction of plugin-owned credentials and ephemeral bridge capabilities.
2231
- * Work on decoded strings where possible; JSON-escaped forms also cover
2232
- * diagnostics containing serialized data. Delta redaction holds only the
2233
- * longest suffix that could still become a secret in the next chunk.
2234
- */
2235
- const strings = (values) => [...new Set(values.filter((value) => typeof value === "string" && value !== ""))].sort((a, b) => b.length - a.length);
2236
- const variants = (values) => strings(values.flatMap((value) => typeof value === "string" ? [value, JSON.stringify(value).slice(1, -1)] : []));
2237
- function redactSecrets(text, values) {
2238
- let safe = String(text);
2239
- for (const secret of variants(values)) safe = safe.split(secret).join("[redacted]");
2240
- return safe;
2241
- }
2242
- /** Protect secrets fragmented across consecutive decoded text deltas. */
2243
- function createDeltaRedactor(values) {
2244
- const secrets = variants(values);
2245
- let pending = "";
2246
- return {
2247
- write(text) {
2248
- const safe = redactSecrets(pending + text, secrets);
2249
- let held = 0;
2250
- for (const secret of secrets) {
2251
- const maximum = Math.min(safe.length, secret.length - 1);
2252
- for (let length = maximum; length > held; length--) if (safe.endsWith(secret.slice(0, length))) {
2253
- held = length;
2254
- break;
2255
- }
2256
- }
2257
- pending = held === 0 ? "" : safe.slice(-held);
2258
- return held === 0 ? safe : safe.slice(0, -held);
2259
- },
2260
- /**
2261
- * At a block/tool boundary, mask a still-ambiguous prefix so a subsequent
2262
- * block cannot reconstruct the credential. At end of output it is safe
2263
- * to flush an incomplete prefix literally; no later delta can complete it.
2264
- */
2265
- flush({ boundary = false } = {}) {
2266
- const result = boundary && pending !== "" ? "[redacted]" : pending;
2267
- pending = "";
2268
- return result;
2269
- }
2270
- };
2271
- }
2272
- //#endregion
2273
2283
  //#region src/claude-cli.js
2274
2284
  /**
2275
2285
  * Bridge to the local Claude Code CLI.
@@ -4579,6 +4589,698 @@ function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
4579
4589
  });
4580
4590
  }
4581
4591
  //#endregion
4592
+ //#region src/image-models.js
4593
+ /** Images API models are separate from the conversational pi-ai catalog. */
4594
+ const DEFAULT_IMAGE_MODEL = "gpt-image-2";
4595
+ const IMAGE_MODELS = Object.freeze({
4596
+ "gpt-image-2": Object.freeze([
4597
+ "auto",
4598
+ "low",
4599
+ "medium",
4600
+ "high"
4601
+ ]),
4602
+ "gpt-image-2.5-flare": Object.freeze([
4603
+ "auto",
4604
+ "low",
4605
+ "medium",
4606
+ "high",
4607
+ "xhigh",
4608
+ "max"
4609
+ ]),
4610
+ "gpt-image-2.5-sunburst": Object.freeze([
4611
+ "auto",
4612
+ "low",
4613
+ "medium",
4614
+ "high",
4615
+ "xhigh",
4616
+ "max"
4617
+ ])
4618
+ });
4619
+ const IMAGE_MODEL_IDS = Object.freeze(Object.keys(IMAGE_MODELS));
4620
+ const IMAGE_QUALITIES = Object.freeze([
4621
+ "auto",
4622
+ "low",
4623
+ "medium",
4624
+ "high",
4625
+ "xhigh",
4626
+ "max"
4627
+ ]);
4628
+ function validateImageOptions({ model = DEFAULT_IMAGE_MODEL, quality = "auto", size = "auto", background = "auto" } = {}) {
4629
+ if (!IMAGE_MODEL_IDS.includes(model)) throw new Error("PackyCode image model is not supported");
4630
+ if (!IMAGE_MODELS[model].includes(quality)) throw new Error("PackyCode image quality is not supported by the selected model");
4631
+ if (![
4632
+ "auto",
4633
+ "transparent",
4634
+ "opaque"
4635
+ ].includes(background)) throw new Error("PackyCode image background is not supported");
4636
+ if (size !== "auto") {
4637
+ const match = typeof size === "string" ? /^(\d{1,4})x(\d{1,4})$/u.exec(size) : null;
4638
+ const width = Number(match?.[1]);
4639
+ const height = Number(match?.[2]);
4640
+ const pixels = width * height;
4641
+ if (match === null || width % 16 !== 0 || height % 16 !== 0 || Math.max(width, height) > 3840 || Math.max(width, height) > Math.min(width, height) * 3 || pixels < 655360 || pixels > 8294400) throw new Error("PackyCode image size must be auto or a supported widthxheight resolution");
4642
+ }
4643
+ return {
4644
+ model,
4645
+ quality,
4646
+ size,
4647
+ background
4648
+ };
4649
+ }
4650
+ //#endregion
4651
+ //#region src/preferences.js
4652
+ /**
4653
+ * Where the plugin's preferences are read from and written to, on both
4654
+ * settings generations DSH has shipped.
4655
+ *
4656
+ * - DSH 0.1.5 installs a settings *section*: the section hands back a live
4657
+ * source and writes go through `settings.update(namespace, patch)`.
4658
+ * - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
4659
+ * plugin's own row, read through a live reference, written through
4660
+ * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
4661
+ * Only volatile fields are writable there, so the host overrides stay
4662
+ * read-only while web search and model visibility are editable.
4663
+ *
4664
+ * @module dsh-packycode-api/preferences
4665
+ */
4666
+ /**
4667
+ * Mark a preference editable without restarting the plugin. DSH 0.2 stores
4668
+ * plugin preferences as volatile Config fields and writes only those, so the
4669
+ * schema version that implements `volatile()` is a peer requirement.
4670
+ *
4671
+ * Name the cause when it is missing instead of letting a bare
4672
+ * `schema.volatile is not a function` reach the host log: the Loader creates
4673
+ * no fiber for a plugin whose module throws while it is imported, so DSH shows
4674
+ * nothing at all — no settings section, no model route, no RPC channel — and
4675
+ * the only explanation is that one log line.
4676
+ */
4677
+ const live = (schema) => {
4678
+ if (typeof schema?.volatile !== "function") throw new Error("PackyCode needs @deepseek-ai/schemastery ^3.18.4, which implements Schema.volatile(); the copy resolved for this plugin is older. A plugin installed from a registry has no node_modules of its own, so this usually means a stale dependency tree inside the installed package directory (for example <profile>/node_modules/dsh-packycode-api/node_modules, left by an earlier local-path install) is shadowing the DSH installation. Remove that directory, or uninstall the plugin and install it again, then restart DSH.");
4679
+ return schema.volatile();
4680
+ };
4681
+ /** Read a Config field that may arrive as a volatile reference. */
4682
+ const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
4683
+ /** A preference target nothing can write to yet. */
4684
+ function unwritable(message) {
4685
+ return {
4686
+ writable: false,
4687
+ set: async () => {
4688
+ throw new Error(message);
4689
+ }
4690
+ };
4691
+ }
4692
+ /**
4693
+ * Connect the preferences to the settings service the host provides.
4694
+ * @param settingsCtx - the scope the `settings` inject handed over.
4695
+ * @param options - the plugin context and Config, the section namespace, the
4696
+ * current source and how to replace it, what to do when the section changes,
4697
+ * and the preference targets this fills in (host overrides, search, models).
4698
+ */
4699
+ function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference }) {
4700
+ const settings = settingsCtx.settings;
4701
+ if (typeof settings?.installSection === "function") {
4702
+ try {
4703
+ settings.installSection(ctx, namespace, Config, source(), {
4704
+ setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
4705
+ onChange
4706
+ });
4707
+ if (typeof settings.update === "function") {
4708
+ const write = (patch) => settings.update(namespace, patch);
4709
+ const target = {
4710
+ writable: settings.writable !== false,
4711
+ set: write
4712
+ };
4713
+ Object.assign(hostPreference, target);
4714
+ Object.assign(searchPreference, target);
4715
+ Object.assign(modelPreference, target);
4716
+ if (imagePreference !== void 0) Object.assign(imagePreference, target);
4717
+ }
4718
+ } catch (error) {
4719
+ ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
4720
+ }
4721
+ return;
4722
+ }
4723
+ if (typeof settings?.update !== "function") return;
4724
+ const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
4725
+ const target = {
4726
+ writable: settings.writable !== false,
4727
+ set: (patch) => settings.update(rowId, patch)
4728
+ };
4729
+ Object.assign(searchPreference, target);
4730
+ Object.assign(modelPreference, target);
4731
+ if (imagePreference !== void 0) Object.assign(imagePreference, target);
4732
+ if (typeof settings.configure === "function") {
4733
+ const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
4734
+ try {
4735
+ scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
4736
+ } catch (error) {
4737
+ ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
4738
+ }
4739
+ }
4740
+ }
4741
+ //#endregion
4742
+ //#region src/image-settings.js
4743
+ function readImagePreference(value = {}) {
4744
+ const model = fieldValue(value.imageModel);
4745
+ const imageModel = IMAGE_MODEL_IDS.includes(model) ? model : DEFAULT_IMAGE_MODEL;
4746
+ const quality = fieldValue(value.imageQuality);
4747
+ return {
4748
+ imageEnabled: fieldValue(value.imageEnabled) !== false,
4749
+ imageModel,
4750
+ imageQuality: IMAGE_MODELS[imageModel].includes(quality) ? quality : "auto"
4751
+ };
4752
+ }
4753
+ /** No request is sent by a settings or credential mutation. */
4754
+ function createImageController({ ring, balance, readPreference, writePreference, isWritable }) {
4755
+ let mounted;
4756
+ let disposeTool;
4757
+ let generation = 0;
4758
+ let closed = false;
4759
+ const preference = () => readImagePreference(readPreference());
4760
+ const reconcile = async () => {
4761
+ const current = ++generation;
4762
+ const key = await ring.activeKey();
4763
+ if (closed || current !== generation) return;
4764
+ const enabled = preference().imageEnabled && key !== void 0 && mounted !== void 0;
4765
+ if (enabled && disposeTool === void 0) disposeTool = mounted();
4766
+ if (!enabled && disposeTool !== void 0) {
4767
+ disposeTool();
4768
+ disposeTool = void 0;
4769
+ }
4770
+ };
4771
+ const status = async () => ({
4772
+ ...preference(),
4773
+ ...await ring.list(),
4774
+ writable: isWritable() === true,
4775
+ available: mounted !== void 0,
4776
+ toolRegistered: disposeTool !== void 0,
4777
+ models: IMAGE_MODEL_IDS.map((id) => ({
4778
+ id,
4779
+ qualities: [...IMAGE_MODELS[id]]
4780
+ }))
4781
+ });
4782
+ return {
4783
+ preference,
4784
+ status,
4785
+ invalidateBalance: () => balance?.invalidate(),
4786
+ async readBalance({ force = false, signal } = {}) {
4787
+ if (balance === void 0) throw new Error("PackyCode image balance is unavailable in this runtime");
4788
+ try {
4789
+ const [value, state] = await Promise.all([balance.read({
4790
+ force,
4791
+ signal
4792
+ }), ring.list()]);
4793
+ return {
4794
+ ...value,
4795
+ activeId: state.activeId
4796
+ };
4797
+ } catch {
4798
+ throw new Error("PackyCode image balance could not be read");
4799
+ }
4800
+ },
4801
+ reconcile,
4802
+ mount(register) {
4803
+ mounted = register;
4804
+ closed = false;
4805
+ reconcile().catch(() => void 0);
4806
+ return () => {
4807
+ closed = true;
4808
+ generation += 1;
4809
+ mounted = void 0;
4810
+ disposeTool?.();
4811
+ disposeTool = void 0;
4812
+ };
4813
+ },
4814
+ async update(value = {}) {
4815
+ if (isWritable() !== true) throw new Error("PackyCode image settings are not writable in this profile");
4816
+ const patch = {};
4817
+ if (Object.hasOwn(value, "imageEnabled")) {
4818
+ if (typeof value.imageEnabled !== "boolean") throw new Error("PackyCode image switch must be a boolean");
4819
+ patch.imageEnabled = value.imageEnabled;
4820
+ }
4821
+ if (Object.hasOwn(value, "imageModel")) patch.imageModel = value.imageModel;
4822
+ if (Object.hasOwn(value, "imageQuality")) patch.imageQuality = value.imageQuality;
4823
+ const next = {
4824
+ ...preference(),
4825
+ ...patch
4826
+ };
4827
+ if (patch.imageModel !== void 0 && patch.imageQuality === void 0 && !IMAGE_MODELS[patch.imageModel]?.includes(next.imageQuality)) patch.imageQuality = next.imageQuality = "auto";
4828
+ validateImageOptions({
4829
+ model: next.imageModel,
4830
+ quality: next.imageQuality
4831
+ });
4832
+ try {
4833
+ await writePreference(patch);
4834
+ } catch {
4835
+ throw new Error("PackyCode could not save the image settings");
4836
+ }
4837
+ await reconcile();
4838
+ return status();
4839
+ },
4840
+ async mutate(operation, payload = {}) {
4841
+ const operations = {
4842
+ add: () => ring.add(payload.key, payload.label),
4843
+ remove: () => ring.remove(String(payload.id ?? "")),
4844
+ select: () => ring.select(String(payload.id ?? "")),
4845
+ enable: () => ring.enable(String(payload.id ?? ""))
4846
+ };
4847
+ if (!Object.hasOwn(operations, operation)) throw new Error("Unknown PackyCode image key operation");
4848
+ await operations[operation]();
4849
+ balance?.invalidate();
4850
+ await reconcile();
4851
+ return status();
4852
+ }
4853
+ };
4854
+ }
4855
+ //#endregion
4856
+ //#region src/images.js
4857
+ /** Host-only Images API transport and DSH tool. No subscription/LLM key fallback. */
4858
+ const IMAGE_TOOL_NAME = "packycode_image_generate";
4859
+ const IMAGE_TIMEOUT_MS = 5 * 6e4;
4860
+ const MAX_IMAGE_BYTES = 48 * 1024 * 1024;
4861
+ const MAX_REFERENCE_IMAGES = 16;
4862
+ const ENVELOPE_BYTES = 1024 * 1024;
4863
+ const EDIT_MEDIA_TYPES = [
4864
+ "image/png",
4865
+ "image/jpeg",
4866
+ "image/webp"
4867
+ ];
4868
+ const SIGNATURE = "89504e470d0a1a0a";
4869
+ const ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
4870
+ var ImageRequestError = class extends Error {};
4871
+ const nonempty = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
4872
+ /** Some model calls fill unused object fields with a zero-valued reference. */
4873
+ function emptyMaskReference(value) {
4874
+ return value !== null && typeof value === "object" && (value.attachmentId === "" || value.attachmentId === `sha256:${"0".repeat(64)}`) && value.bytes === 0 && value.width === 0 && value.height === 0 && (value.name === void 0 || value.name === "") && (value.originalDimensions === void 0 || value.originalDimensions.width === 0 && value.originalDimensions.height === 0);
4875
+ }
4876
+ /** A bounded reader applies to both successful payloads and error envelopes. */
4877
+ async function readJson(response, maximum, signal) {
4878
+ const length = Number(response.headers.get("content-length"));
4879
+ if (Number.isFinite(length) && length > maximum || response.body === null) {
4880
+ await response.body?.cancel().catch(() => void 0);
4881
+ throw new ImageRequestError("PackyCode image response is unreadable or exceeds the size limit");
4882
+ }
4883
+ const reader = response.body.getReader();
4884
+ const cancel = () => {
4885
+ reader.cancel().catch(() => void 0);
4886
+ };
4887
+ signal.addEventListener("abort", cancel, { once: true });
4888
+ const chunks = [];
4889
+ let total = 0;
4890
+ try {
4891
+ signal.throwIfAborted();
4892
+ while (true) {
4893
+ const { done, value } = await reader.read();
4894
+ signal.throwIfAborted();
4895
+ if (done) break;
4896
+ total += value.byteLength;
4897
+ if (total > maximum) throw new ImageRequestError("PackyCode image response exceeds the size limit");
4898
+ chunks.push(Buffer.from(value));
4899
+ }
4900
+ try {
4901
+ return JSON.parse(Buffer.concat(chunks, total).toString("utf8"));
4902
+ } catch {
4903
+ throw new ImageRequestError("PackyCode returned an unreadable image response");
4904
+ }
4905
+ } finally {
4906
+ signal.removeEventListener("abort", cancel);
4907
+ await reader.cancel().catch(() => void 0);
4908
+ reader.releaseLock();
4909
+ }
4910
+ }
4911
+ function pngDimensions(data) {
4912
+ const buffer = Buffer.from(data);
4913
+ if (buffer.length < 24 || buffer.subarray(0, 8).toString("hex") !== SIGNATURE || buffer.readUInt32BE(8) !== 13 || buffer.subarray(12, 16).toString("ascii") !== "IHDR") throw new ImageRequestError("PackyCode returned an invalid PNG image");
4914
+ const width = buffer.readUInt32BE(16);
4915
+ const height = buffer.readUInt32BE(20);
4916
+ if (width === 0 || height === 0 || Math.max(width, height) > 3840 || width * height > 8294400) throw new ImageRequestError("PackyCode returned unsupported image dimensions");
4917
+ return {
4918
+ width,
4919
+ height
4920
+ };
4921
+ }
4922
+ function decodeImagePng(encoded, maximum = MAX_IMAGE_BYTES) {
4923
+ if (typeof encoded !== "string" || encoded.length === 0 || encoded.length > Math.ceil(maximum / 3) * 4 || encoded.length % 4 !== 0) throw new ImageRequestError("PackyCode returned invalid or oversized base64 image data");
4924
+ const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
4925
+ for (let index = 0; index < encoded.length - padding; index += 1) {
4926
+ const code = encoded.charCodeAt(index);
4927
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) throw new ImageRequestError("PackyCode returned invalid base64 image data");
4928
+ }
4929
+ const data = Buffer.from(encoded, "base64");
4930
+ if (data.length > maximum || data.toString("base64") !== encoded) throw new ImageRequestError("PackyCode returned invalid base64 image data");
4931
+ pngDimensions(data);
4932
+ return data;
4933
+ }
4934
+ function imageFailure(status, value) {
4935
+ if (value?.error?.code === "moderation_blocked") return "PackyCode image request was blocked by the image safety check; revise the prompt or references";
4936
+ if (status === 401 || status === 403) return "PackyCode image key was rejected; check the separate image key and its model permissions";
4937
+ if (status === 429) return "PackyCode image quota or rate limit is unavailable; check the separate image key";
4938
+ return `PackyCode image request failed (HTTP ${status}); check model, quality, and image key permissions`;
4939
+ }
4940
+ /** One paid attempt only. Invalid references never turn an edit into a generation. */
4941
+ function createPackyCodeImageClient({ resolveKey, getHost, fetchImpl = ambientFetch, timeoutMs = IMAGE_TIMEOUT_MS }) {
4942
+ return { async request(args, { signal, images, mask } = {}) {
4943
+ const options = validateImageOptions(args);
4944
+ const prompt = nonempty(args?.prompt);
4945
+ if (prompt === void 0 || prompt.length > 32e3) throw new ImageRequestError("PackyCode image prompt must contain 1-32000 characters");
4946
+ if (images !== void 0 && (!Array.isArray(images) || images.length === 0 || images.length > MAX_REFERENCE_IMAGES)) throw new ImageRequestError("PackyCode image edits require 1-16 reference images");
4947
+ if (mask !== void 0 && images === void 0) throw new ImageRequestError("PackyCode image mask requires a reference image");
4948
+ const timeout = AbortSignal.timeout(timeoutMs);
4949
+ const requestSignal = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
4950
+ let selected;
4951
+ try {
4952
+ selected = await waitForSharedRequest(Promise.resolve().then(resolveKey), requestSignal);
4953
+ } catch {
4954
+ if (signal?.aborted) throw new ImageRequestError("PackyCode image request was cancelled");
4955
+ if (timeout.aborted) throw new ImageRequestError("PackyCode image request timed out");
4956
+ throw new ImageRequestError("PackyCode image credentials could not be read");
4957
+ }
4958
+ if (typeof selected?.key !== "string" || selected.key === "") throw new ImageRequestError("PackyCode images need a separate image key; add one under Settings → PackyCode → Images");
4959
+ const fields = {
4960
+ ...options,
4961
+ prompt,
4962
+ n: 1,
4963
+ output_format: "png"
4964
+ };
4965
+ let body;
4966
+ const headers = {
4967
+ authorization: `Bearer ${selected.key}`,
4968
+ accept: "application/json"
4969
+ };
4970
+ if (images === void 0) {
4971
+ headers["content-type"] = "application/json";
4972
+ body = JSON.stringify(fields);
4973
+ } else {
4974
+ body = new FormData();
4975
+ for (const [key, value] of Object.entries(fields)) body.append(key, String(value));
4976
+ for (const [index, image] of images.entries()) {
4977
+ if (!EDIT_MEDIA_TYPES.includes(image?.mediaType) || !(image.data instanceof Uint8Array) || image.data.byteLength === 0 || image.data.byteLength >= 50 * 1024 * 1024) throw new ImageRequestError("PackyCode reference images must be PNG, JPEG or WebP smaller than 50 MB");
4978
+ const extension = image.mediaType === "image/jpeg" ? "jpg" : image.mediaType.split("/")[1];
4979
+ body.append("image[]", new Blob([image.data], { type: image.mediaType }), `reference-${index + 1}.${extension}`);
4980
+ }
4981
+ if (mask !== void 0) {
4982
+ if (mask.mediaType !== "image/png" || !(mask.data instanceof Uint8Array) || mask.data.byteLength >= 4 * 1024 * 1024) throw new ImageRequestError("PackyCode image mask must be a PNG smaller than 4 MB");
4983
+ const dimensions = pngDimensions(mask.data);
4984
+ if (dimensions.width !== images[0].width || dimensions.height !== images[0].height) throw new ImageRequestError("PackyCode image mask must match the first reference image dimensions");
4985
+ body.append("mask", new Blob([mask.data], { type: "image/png" }), "mask.png");
4986
+ }
4987
+ }
4988
+ try {
4989
+ requestSignal.throwIfAborted();
4990
+ const response = await fetchImpl(`${normalizeHost(getHost(), DEFAULT_API_HOST).replace(/\/v1$/u, "")}/v1/images/${images === void 0 ? "generations" : "edits"}`, {
4991
+ method: "POST",
4992
+ redirect: "error",
4993
+ headers,
4994
+ body,
4995
+ signal: requestSignal
4996
+ });
4997
+ if (!response.ok) {
4998
+ const value = await readJson(response, 64 * 1024, requestSignal).catch(() => void 0);
4999
+ requestSignal.throwIfAborted();
5000
+ throw new ImageRequestError(imageFailure(response.status, value));
5001
+ }
5002
+ const value = await readJson(response, Math.ceil(MAX_IMAGE_BYTES / 3) * 4 + ENVELOPE_BYTES, requestSignal);
5003
+ const data = decodeImagePng(value?.data?.[0]?.b64_json);
5004
+ return {
5005
+ data,
5006
+ ...pngDimensions(data),
5007
+ requestedModel: options.model,
5008
+ requestedQuality: options.quality,
5009
+ requestedSize: options.size,
5010
+ ...IMAGE_MODEL_IDS.includes(value?.model) ? { reportedModel: value.model } : {},
5011
+ ...IMAGE_QUALITIES.includes(value?.quality) ? { quality: value.quality } : {}
5012
+ };
5013
+ } catch (error) {
5014
+ if (signal?.aborted) throw new ImageRequestError("PackyCode image request was cancelled");
5015
+ if (timeout.aborted) throw new ImageRequestError("PackyCode image request timed out");
5016
+ if (error instanceof ImageRequestError) throw error;
5017
+ throw new ImageRequestError("PackyCode image connection failed");
5018
+ }
5019
+ } };
5020
+ }
5021
+ const referenceSchema = {
5022
+ type: "object",
5023
+ additionalProperties: false,
5024
+ properties: {
5025
+ attachmentId: {
5026
+ type: "string",
5027
+ required: true,
5028
+ description: "Complete sha256:<64 lowercase hex> image reference from the current session or read_image. Never a path or filename."
5029
+ },
5030
+ mediaType: {
5031
+ type: "string",
5032
+ required: true
5033
+ },
5034
+ bytes: {
5035
+ type: "integer",
5036
+ required: true
5037
+ },
5038
+ width: {
5039
+ type: "integer",
5040
+ required: true
5041
+ },
5042
+ height: {
5043
+ type: "integer",
5044
+ required: true
5045
+ },
5046
+ name: { type: "string" },
5047
+ originalDimensions: {
5048
+ type: "object",
5049
+ additionalProperties: false,
5050
+ properties: {
5051
+ width: {
5052
+ type: "integer",
5053
+ required: true
5054
+ },
5055
+ height: {
5056
+ type: "integer",
5057
+ required: true
5058
+ }
5059
+ }
5060
+ }
5061
+ }
5062
+ };
5063
+ /** Select only trusted references already present in this tool call's session. */
5064
+ async function readReferences(values, attachments, messages, signal) {
5065
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new ImageRequestError("PackyCode image edits require 1-16 reference images");
5066
+ const available = /* @__PURE__ */ new Map();
5067
+ const visit = (content) => {
5068
+ for (const block of content ?? []) {
5069
+ if (block?.type === "image" && ATTACHMENT_ID.test(block.attachment?.attachmentId)) available.set(block.attachment.attachmentId, block.attachment);
5070
+ if (block?.type === "tool-result" && Array.isArray(block.content)) visit(block.content);
5071
+ }
5072
+ };
5073
+ for (const message of messages ?? []) if (Array.isArray(message?.content)) visit(message.content);
5074
+ const ids = values.map((value) => value?.attachmentId);
5075
+ if (ids.some((id) => typeof id !== "string" || !ATTACHMENT_ID.test(id) || !available.has(id))) throw new ImageRequestError("PackyCode image reference is missing from this session; use read_image and copy its complete attachment reference");
5076
+ if (new Set(ids).size !== ids.length) throw new ImageRequestError("PackyCode image references must not contain duplicates");
5077
+ const images = [];
5078
+ let total = 0;
5079
+ for (const id of ids) {
5080
+ signal?.throwIfAborted();
5081
+ let stored;
5082
+ try {
5083
+ stored = await attachments.readImage(available.get(id), signal);
5084
+ } catch {
5085
+ throw new ImageRequestError("PackyCode could not read the selected image attachment");
5086
+ }
5087
+ const { data, ref } = stored;
5088
+ if (!(data instanceof Uint8Array) || !EDIT_MEDIA_TYPES.includes(ref?.mediaType)) throw new ImageRequestError("PackyCode image reference must be PNG, JPEG or WebP");
5089
+ total += data.byteLength;
5090
+ if (total > attachments.imageLimits.maxMessageImageBytes) throw new ImageRequestError("PackyCode image references exceed the DSH image limit");
5091
+ images.push({
5092
+ data,
5093
+ mediaType: ref.mediaType,
5094
+ width: ref.width,
5095
+ height: ref.height
5096
+ });
5097
+ }
5098
+ return images;
5099
+ }
5100
+ function attachmentProjection(ref) {
5101
+ return Object.fromEntries([
5102
+ "attachmentId",
5103
+ "mediaType",
5104
+ "bytes",
5105
+ "width",
5106
+ "height",
5107
+ "name",
5108
+ "originalDimensions"
5109
+ ].filter((key) => ref[key] !== void 0).map((key) => [key, ref[key]]));
5110
+ }
5111
+ const renderImage = (value) => [{
5112
+ type: "text",
5113
+ text: `PackyCode · ${value.requestedModel} · ${value.width} × ${value.height} · ${value.requestedQuality}\nOriginal PNG on the DSH host: ${value.localPath}`
5114
+ }, {
5115
+ type: "image",
5116
+ attachment: value.image
5117
+ }];
5118
+ function createPackyCodeImageTool({ client, attachments, getPreference, getSessionMessages, imageDirectory, onGenerated }) {
5119
+ return defineTool({
5120
+ name: IMAGE_TOOL_NAME,
5121
+ description: "Generate or edit images through PackyCode using the separately configured image API key. Use only when the user requests image output. This is independent of the conversation model and Codex subscription. For a new image omit referenceImages; for edits include only the intended images from this session or read_image. Never replace an edit with text-only generation, silently change models or downgrade quality. Return the resulting image to the user. Rendering may take several minutes.",
5122
+ parameters: {
5123
+ prompt: {
5124
+ type: "string",
5125
+ required: true,
5126
+ description: "Complete image description or precise editing instructions."
5127
+ },
5128
+ operation: {
5129
+ type: "string",
5130
+ enum: ["generate", "edit"],
5131
+ description: "Use generate for a new image; edit requires 1-16 real referenceImages. If omitted, nonempty references select editing, while an omitted or empty array selects generation."
5132
+ },
5133
+ model: {
5134
+ type: "string",
5135
+ enum: ["settings", ...IMAGE_MODEL_IDS],
5136
+ description: "Use settings (or omit) to follow the PackyCode image setting. Choose an explicit engine only when the user requests one."
5137
+ },
5138
+ quality: {
5139
+ type: "string",
5140
+ enum: ["settings", ...IMAGE_QUALITIES],
5141
+ description: "Use settings (or omit) to follow the user image setting. auto is an explicit API quality, not a request to follow settings. xhigh and max require a 2.5 model; never downgrade automatically."
5142
+ },
5143
+ size: {
5144
+ type: "string",
5145
+ description: "auto or widthxheight, e.g. 1024x1024; both edges multiples of 16, each <=3840, ratio <=3:1, pixels 655360..8294400."
5146
+ },
5147
+ background: {
5148
+ type: "string",
5149
+ enum: [
5150
+ "auto",
5151
+ "opaque",
5152
+ "transparent"
5153
+ ]
5154
+ },
5155
+ referenceImages: {
5156
+ type: "array",
5157
+ items: referenceSchema,
5158
+ description: "1-16 complete attachment references for editing. Omit or use [] for a new image. Paths and unrelated images are not accepted; never drop real references after an edit fails."
5159
+ },
5160
+ maskImage: {
5161
+ ...referenceSchema,
5162
+ description: "Omit for new images and edits without a mask. Never invent a reference. Optional PNG mask from this session, matching the first reference and under 4 MB; alpha=0 marks the area to edit."
5163
+ }
5164
+ },
5165
+ output: {
5166
+ schema: {
5167
+ type: "object",
5168
+ additionalProperties: false,
5169
+ properties: {
5170
+ image: {
5171
+ ...referenceSchema,
5172
+ required: true
5173
+ },
5174
+ localPath: {
5175
+ type: "string",
5176
+ required: true
5177
+ },
5178
+ width: {
5179
+ type: "integer",
5180
+ required: true
5181
+ },
5182
+ height: {
5183
+ type: "integer",
5184
+ required: true
5185
+ },
5186
+ requestedModel: {
5187
+ type: "string",
5188
+ required: true
5189
+ },
5190
+ requestedQuality: {
5191
+ type: "string",
5192
+ required: true
5193
+ },
5194
+ requestedSize: {
5195
+ type: "string",
5196
+ required: true
5197
+ },
5198
+ reportedModel: { type: "string" },
5199
+ quality: { type: "string" }
5200
+ }
5201
+ },
5202
+ render: (_args, value) => renderImage(value),
5203
+ presentationMeta: (_args, value) => ({
5204
+ kind: "packycode-image",
5205
+ requestedModel: value.requestedModel,
5206
+ requestedQuality: value.requestedQuality,
5207
+ width: value.width,
5208
+ height: value.height
5209
+ })
5210
+ },
5211
+ timeoutMs: IMAGE_TIMEOUT_MS,
5212
+ isConcurrencySafe: () => false,
5213
+ async execute(args, exec) {
5214
+ const preference = getPreference();
5215
+ if (!preference.imageEnabled) throw new ImageRequestError("PackyCode images are disabled in settings");
5216
+ if (exec?.agent?.id === void 0) throw new ImageRequestError("PackyCode images require a session-owned tool call");
5217
+ const signal = exec.signal;
5218
+ const hasReferences = Array.isArray(args.referenceImages) && args.referenceImages.length > 0;
5219
+ if (args.operation === "generate" && hasReferences) throw new ImageRequestError("PackyCode image generation cannot include reference images; use edit");
5220
+ const editing = args.operation === "edit" || hasReferences;
5221
+ const maskImage = emptyMaskReference(args.maskImage) ? void 0 : args.maskImage;
5222
+ if (maskImage !== void 0 && !editing) throw new ImageRequestError("PackyCode image mask requires referenceImages");
5223
+ let messages;
5224
+ try {
5225
+ messages = editing ? await getSessionMessages(exec.agent.id) : void 0;
5226
+ } catch {
5227
+ throw new ImageRequestError("PackyCode could not read the current session image references");
5228
+ }
5229
+ const images = editing ? await readReferences(args.referenceImages, attachments, messages, signal) : void 0;
5230
+ const masks = maskImage === void 0 ? void 0 : await readReferences([maskImage], attachments, messages, signal);
5231
+ if (!getPreference().imageEnabled) throw new ImageRequestError("PackyCode images are disabled in settings");
5232
+ const result = await client.request({
5233
+ ...args,
5234
+ model: args.model === "settings" ? preference.imageModel : args.model ?? preference.imageModel,
5235
+ quality: args.quality === "settings" ? preference.imageQuality : args.quality ?? preference.imageQuality
5236
+ }, {
5237
+ signal,
5238
+ images,
5239
+ mask: masks?.[0]
5240
+ });
5241
+ onGenerated?.();
5242
+ signal?.throwIfAborted();
5243
+ await mkdir(imageDirectory, {
5244
+ recursive: true,
5245
+ mode: 448
5246
+ });
5247
+ const localPath = join(imageDirectory, `${randomUUID()}.png`);
5248
+ let saved = false;
5249
+ try {
5250
+ const file = await open(localPath, "wx", 384);
5251
+ saved = true;
5252
+ try {
5253
+ await file.writeFile(result.data);
5254
+ } finally {
5255
+ await file.close();
5256
+ }
5257
+ const ref = await attachments.saveImage({
5258
+ data: result.data,
5259
+ mediaType: "image/png",
5260
+ name: "packycode-generated.png"
5261
+ });
5262
+ const { data: _data, ...metadata } = result;
5263
+ const value = {
5264
+ ...metadata,
5265
+ image: attachmentProjection(ref),
5266
+ localPath
5267
+ };
5268
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
5269
+ content: renderImage(value),
5270
+ source: {
5271
+ kind: "plugin",
5272
+ plugin: "packycode-api"
5273
+ }
5274
+ }));
5275
+ return value;
5276
+ } catch {
5277
+ if (saved) await rm(localPath, { force: true }).catch(() => void 0);
5278
+ throw new ImageRequestError("PackyCode could not save the generated image");
5279
+ }
5280
+ }
5281
+ });
5282
+ }
5283
+ //#endregion
4582
5284
  //#region src/key-chooser.js
4583
5285
  /**
4584
5286
  * Which stored key a request runs with, and what happens when the gateway
@@ -4730,7 +5432,6 @@ function createKeyChooser({ ring, groups, onRotated, logger, waitMs = DEFAULT_GR
4730
5432
  const LEGAL_KEY = /^[\x21-\x7E]+$/u;
4731
5433
  const RECORD_VERSION = 1;
4732
5434
  const MAX_KEYS = 32;
4733
- const RECORD_KEY = typeof credentialKey === "function" ? credentialKey(PLUGIN_ID, KEY_RECORD_ID) : void 0;
4734
5435
  const text$1 = (value) => typeof value === "string" ? value : "";
4735
5436
  /** Validate a pasted key before it is stored; the gateway never sees a bad one from us. */
4736
5437
  function assertApiKey(value) {
@@ -4794,16 +5495,18 @@ function projection(state) {
4794
5495
  }
4795
5496
  var PackyCodeKeyRing = class {
4796
5497
  #credentials;
4797
- constructor(credentials) {
4798
- if (RECORD_KEY === void 0) throw new Error("dsh-packycode-api requires @deepseek-ai/dsh-credentials with credentialKey()");
5498
+ #recordKey;
5499
+ constructor(credentials, { recordId = KEY_RECORD_ID } = {}) {
5500
+ if (typeof credentialKey !== "function") throw new Error("dsh-packycode-api requires @deepseek-ai/dsh-credentials with credentialKey()");
4799
5501
  if (credentials === null || typeof credentials !== "object" || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("dsh-packycode-api requires the DSH credential record API");
4800
5502
  this.#credentials = credentials;
5503
+ this.#recordKey = credentialKey(PLUGIN_ID, recordId);
4801
5504
  }
4802
5505
  async #read() {
4803
- return normalizeState(await this.#credentials.readRecord(RECORD_KEY));
5506
+ return normalizeState(await this.#credentials.readRecord(this.#recordKey));
4804
5507
  }
4805
5508
  async #write(mutate) {
4806
- return normalizeState(await this.#credentials.modifyRecord(RECORD_KEY, async (current) => {
5509
+ return normalizeState(await this.#credentials.modifyRecord(this.#recordKey, async (current) => {
4807
5510
  const updated = await mutate(normalizeState(current));
4808
5511
  if (updated === void 0) return current;
4809
5512
  return {
@@ -4953,95 +5656,6 @@ var PackyCodeKeyRing = class {
4953
5656
  }
4954
5657
  };
4955
5658
  //#endregion
4956
- //#region src/preferences.js
4957
- /**
4958
- * Where the plugin's preferences are read from and written to, on both
4959
- * settings generations DSH has shipped.
4960
- *
4961
- * - DSH 0.1.5 installs a settings *section*: the section hands back a live
4962
- * source and writes go through `settings.update(namespace, patch)`.
4963
- * - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
4964
- * plugin's own row, read through a live reference, written through
4965
- * `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
4966
- * Only volatile fields are writable there, so the host overrides stay
4967
- * read-only while web search and model visibility are editable.
4968
- *
4969
- * @module dsh-packycode-api/preferences
4970
- */
4971
- /**
4972
- * Mark a preference editable without restarting the plugin. DSH 0.2 stores
4973
- * plugin preferences as volatile Config fields and writes only those, so the
4974
- * schema version that implements `volatile()` is a peer requirement.
4975
- *
4976
- * Name the cause when it is missing instead of letting a bare
4977
- * `schema.volatile is not a function` reach the host log: the Loader creates
4978
- * no fiber for a plugin whose module throws while it is imported, so DSH shows
4979
- * nothing at all — no settings section, no model route, no RPC channel — and
4980
- * the only explanation is that one log line.
4981
- */
4982
- const live = (schema) => {
4983
- if (typeof schema?.volatile !== "function") throw new Error("PackyCode needs @deepseek-ai/schemastery ^3.18.4, which implements Schema.volatile(); the copy resolved for this plugin is older. A plugin installed from a registry has no node_modules of its own, so this usually means a stale dependency tree inside the installed package directory (for example <profile>/node_modules/dsh-packycode-api/node_modules, left by an earlier local-path install) is shadowing the DSH installation. Remove that directory, or uninstall the plugin and install it again, then restart DSH.");
4984
- return schema.volatile();
4985
- };
4986
- /** Read a Config field that may arrive as a volatile reference. */
4987
- const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
4988
- /** A preference target nothing can write to yet. */
4989
- function unwritable(message) {
4990
- return {
4991
- writable: false,
4992
- set: async () => {
4993
- throw new Error(message);
4994
- }
4995
- };
4996
- }
4997
- /**
4998
- * Connect the preferences to the settings service the host provides.
4999
- * @param settingsCtx - the scope the `settings` inject handed over.
5000
- * @param options - the plugin context and Config, the section namespace, the
5001
- * current source and how to replace it, what to do when the section changes,
5002
- * and the preference targets this fills in (host overrides, search, models).
5003
- */
5004
- function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference }) {
5005
- const settings = settingsCtx.settings;
5006
- if (typeof settings?.installSection === "function") {
5007
- try {
5008
- settings.installSection(ctx, namespace, Config, source(), {
5009
- setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
5010
- onChange
5011
- });
5012
- if (typeof settings.update === "function") {
5013
- const write = (patch) => settings.update(namespace, patch);
5014
- const target = {
5015
- writable: settings.writable !== false,
5016
- set: write
5017
- };
5018
- Object.assign(hostPreference, target);
5019
- Object.assign(searchPreference, target);
5020
- Object.assign(modelPreference, target);
5021
- }
5022
- } catch (error) {
5023
- ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
5024
- }
5025
- return;
5026
- }
5027
- if (typeof settings?.update !== "function") return;
5028
- const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
5029
- const target = {
5030
- writable: settings.writable !== false,
5031
- set: (patch) => settings.update(rowId, patch)
5032
- };
5033
- Object.assign(searchPreference, target);
5034
- Object.assign(modelPreference, target);
5035
- if (typeof settings.configure === "function") {
5036
- const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
5037
- try {
5038
- scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
5039
- } catch (error) {
5040
- ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
5041
- }
5042
- }
5043
- }
5044
- //#endregion
5045
5659
  //#region src/model-selection.js
5046
5660
  /** Profile preferences for the models advertised in PackyCode's picker. */
5047
5661
  /** Store exclusions so new catalog models are selected by default as well. */
@@ -5831,7 +6445,7 @@ const keyId = (payload) => String(payload?.id ?? "");
5831
6445
  * web search controller (without one, its endpoints refuse).
5832
6446
  * @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
5833
6447
  */
5834
- function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection }) {
6448
+ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection, images }) {
5835
6449
  const need = (service, message) => {
5836
6450
  if (service === void 0) throw new Error(message);
5837
6451
  return service;
@@ -5841,6 +6455,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
5841
6455
  const webSearch = () => need(search, "PackyCode web search is unavailable in this runtime");
5842
6456
  const groupReader = () => need(groups, "PackyCode key groups are unavailable in this runtime");
5843
6457
  const models = () => need(modelSelection, "PackyCode model selection is unavailable in this runtime");
6458
+ const imageSupport = () => need(images, "PackyCode images are unavailable in this runtime");
5844
6459
  const status = async () => ({
5845
6460
  ...await ring.list(),
5846
6461
  hosts: hosts(),
@@ -5889,6 +6504,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
5889
6504
  });
5890
6505
  balance.invalidate();
5891
6506
  groups?.invalidate();
6507
+ images?.invalidateBalance?.();
5892
6508
  return status();
5893
6509
  },
5894
6510
  "claude-cli/status": (payload) => cli().status({ force: payload?.force === true }),
@@ -5907,7 +6523,20 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
5907
6523
  searchBackend: payload?.searchBackend
5908
6524
  }),
5909
6525
  "models/status": () => models().status(),
5910
- "models/update": (payload) => models().update({ selectedIds: payload?.selectedIds })
6526
+ "models/update": (payload) => models().update({ selectedIds: payload?.selectedIds }),
6527
+ "images/status": () => imageSupport().status(),
6528
+ "images/balance": (payload, signal) => imageSupport().readBalance({
6529
+ force: payload?.force === true,
6530
+ signal
6531
+ }),
6532
+ "images/update": (payload) => imageSupport().update(payload),
6533
+ "images/keys/add": (payload) => imageSupport().mutate("add", {
6534
+ key: payload?.key,
6535
+ label: payload?.label
6536
+ }),
6537
+ "images/keys/remove": (payload) => imageSupport().mutate("remove", { id: payload?.id }),
6538
+ "images/keys/select": (payload) => imageSupport().mutate("select", { id: payload?.id }),
6539
+ "images/keys/enable": (payload) => imageSupport().mutate("enable", { id: payload?.id })
5911
6540
  };
5912
6541
  return async (endpoint, payload, signal) => {
5913
6542
  try {
@@ -6842,16 +7471,23 @@ const Config = z.object({
6842
7471
  /** Which PackyCode model runs a search: `deepseek` or `gpt`. */
6843
7472
  searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND)),
6844
7473
  /** Only the picker is filtered; an existing session can keep its model. */
6845
- hiddenModels: live(z.array(z.string()).default([]))
7474
+ hiddenModels: live(z.array(z.string()).default([])),
7475
+ imageEnabled: live(z.boolean().default(true)),
7476
+ imageModel: live(z.union([...IMAGE_MODEL_IDS]).default(DEFAULT_IMAGE_MODEL)),
7477
+ imageQuality: live(z.union([...IMAGE_QUALITIES]).default("auto"))
6846
7478
  });
6847
7479
  function apply(ctx, config) {
6848
7480
  const ring = new PackyCodeKeyRing(ctx.credentials);
7481
+ const imageRing = new PackyCodeKeyRing(ctx.credentials, { recordId: IMAGE_KEY_RECORD_ID });
6849
7482
  let source = () => ({
6850
7483
  apiHost: normalizeHost(config?.apiHost, DEFAULT_API_HOST),
6851
7484
  statusHost: normalizeHost(config?.statusHost, DEFAULT_STATUS_HOST),
6852
7485
  searchProvider: fieldValue(config?.searchProvider),
6853
7486
  searchBackend: fieldValue(config?.searchBackend),
6854
- hiddenModels: fieldValue(config?.hiddenModels)
7487
+ hiddenModels: fieldValue(config?.hiddenModels),
7488
+ imageEnabled: fieldValue(config?.imageEnabled),
7489
+ imageModel: fieldValue(config?.imageModel),
7490
+ imageQuality: fieldValue(config?.imageQuality)
6855
7491
  });
6856
7492
  const hosts = () => {
6857
7493
  const current = source() ?? {};
@@ -6864,6 +7500,11 @@ function apply(ctx, config) {
6864
7500
  listKeys: () => ring.secrets(),
6865
7501
  getHosts: hosts
6866
7502
  });
7503
+ const imageBalance = createBalanceReader({
7504
+ listKeys: () => imageRing.secrets(),
7505
+ getHosts: hosts,
7506
+ includeAccount: false
7507
+ });
6867
7508
  /**
6868
7509
  * Which token groups each key is in, and so which models it can reach.
6869
7510
  * Nothing reads it at boot: the settings panel asks for it, and a request
@@ -6998,7 +7639,22 @@ function apply(ctx, config) {
6998
7639
  writable: false,
6999
7640
  set: void 0
7000
7641
  };
7642
+ const imagePreference = {
7643
+ writable: false,
7644
+ set: void 0
7645
+ };
7001
7646
  const dshHome = typeof process.env.DSH_HOME === "string" && process.env.DSH_HOME !== "" ? process.env.DSH_HOME : join(homedir(), ".dsh");
7647
+ const images = createImageController({
7648
+ ring: imageRing,
7649
+ balance: imageBalance,
7650
+ readPreference: () => source() ?? {},
7651
+ writePreference: (patch) => imagePreference.set(patch),
7652
+ isWritable: () => imagePreference.writable === true && typeof imagePreference.set === "function"
7653
+ });
7654
+ const imageClient = createPackyCodeImageClient({
7655
+ resolveKey: () => imageRing.activeKey(),
7656
+ getHost: () => hosts().apiHost
7657
+ });
7002
7658
  const search = createSearchController({
7003
7659
  ring,
7004
7660
  hosts,
@@ -7037,16 +7693,20 @@ function apply(ctx, config) {
7037
7693
  },
7038
7694
  onChange: () => {
7039
7695
  balance.invalidate();
7696
+ imageBalance.invalidate();
7040
7697
  groups.invalidate();
7041
7698
  modelSelection.reconcile();
7699
+ images.reconcile().catch(() => void 0);
7042
7700
  search.reconcile();
7043
7701
  },
7044
7702
  hostPreference,
7045
7703
  searchPreference,
7046
- modelPreference
7704
+ modelPreference,
7705
+ imagePreference
7047
7706
  }));
7048
7707
  ctx.on?.("loader/volatile-update", () => {
7049
7708
  modelSelection.reconcile();
7709
+ images.reconcile().catch(() => void 0);
7050
7710
  search.reconcile();
7051
7711
  });
7052
7712
  /**
@@ -7069,7 +7729,8 @@ function apply(ctx, config) {
7069
7729
  claudeCli,
7070
7730
  pluginManager: createPackyCodePluginManager(),
7071
7731
  search,
7072
- modelSelection
7732
+ modelSelection,
7733
+ images
7073
7734
  });
7074
7735
  /**
7075
7736
  * The shipped Connection getter retains its own service context, whose row
@@ -7090,6 +7751,18 @@ function apply(ctx, config) {
7090
7751
  planActive: (session, agent) => planModeForToolExecution(ctx, session, agent)
7091
7752
  })), "packycode-api: claude_code tool");
7092
7753
  });
7754
+ ctx.inject(["tools", "attachments"], (imageCtx) => {
7755
+ const store = imageCtx.attachments;
7756
+ if (typeof store?.saveImage !== "function" || typeof store?.readImage !== "function") return;
7757
+ (typeof imageCtx.effect === "function" ? imageCtx : ctx).effect(() => images.mount(() => imageCtx.tools.register(createPackyCodeImageTool({
7758
+ client: imageClient,
7759
+ attachments: store,
7760
+ getPreference: images.preference,
7761
+ onGenerated: () => imageBalance.invalidate(),
7762
+ getSessionMessages: (id) => ctx.get?.("sessions")?.get?.(id)?.deriveMessages?.() ?? [],
7763
+ imageDirectory: join(dshHome, "packycode-api", "images")
7764
+ }))), "packycode-api: image tool availability");
7765
+ });
7093
7766
  }
7094
7767
  //#endregion
7095
- export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, planModeForToolExecution, readClaudeStreamEvents, readHiddenModels, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
7768
+ export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, planModeForToolExecution, pngDimensions, readClaudeStreamEvents, readHiddenModels, readImagePreference, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse, validateImageOptions };