@mars-sea/dsh-commandcode-provider 0.1.6 → 0.1.9

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,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { CallId, LlmAdapter, LlmError, ReasoningEffortId, assertUsableApiKey, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
4
+ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
5
+ import { CallId, LlmAdapter, LlmError, ReasoningEffortId, assertUsableApiKey, attributionHeaders, errorChain, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
6
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
6
7
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
7
8
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
@@ -166,10 +167,69 @@ const KNOWN_EFFORTS = {
166
167
  "max"
167
168
  ]
168
169
  };
170
+ /**
171
+ * Models whose Capabilities include Vision, per the official Command Code
172
+ * model registry (`https://commandcode.ai/docs/reference/cli/models`, generated
173
+ * from the same registry as `cmd --list-models` / the `/model` picker).
174
+ *
175
+ * The Provider API does not expose modality metadata, so this snapshot is the
176
+ * source of truth for image-input gating. Command Code's own CLI falls back to
177
+ * a client-side VISION side-call for text-only models; this adapter does not
178
+ * reproduce that interactive feature, so images sent to a model outside this
179
+ * list are refused loudly (`UNSUPPORTED_CONTENT`) instead of being dropped or
180
+ * sent to a model that cannot read them.
181
+ *
182
+ * Keep in sync with the official registry when new models ship (see the
183
+ * dsh-commandcode-upstream skill).
184
+ */
185
+ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
186
+ "MiniMaxAI/MiniMax-M3",
187
+ "Qwen/Qwen3.6-Plus",
188
+ "Qwen/Qwen3.7-Flash",
189
+ "Qwen/Qwen3.7-Plus",
190
+ "Qwen/Qwen3.8-Max",
191
+ "claude-fable-5",
192
+ "claude-haiku-4-5-20251001",
193
+ "claude-opus-4-7",
194
+ "claude-opus-4-8",
195
+ "claude-opus-5",
196
+ "claude-sonnet-4-6",
197
+ "claude-sonnet-5",
198
+ "google/gemini-3.1-flash-lite",
199
+ "google/gemini-3.5-flash",
200
+ "google/gemini-3.5-flash-lite",
201
+ "google/gemini-3.6-flash",
202
+ "google/gemini-3.7-flash",
203
+ "gpt-5.3-codex",
204
+ "gpt-5.4",
205
+ "gpt-5.4-mini",
206
+ "gpt-5.5",
207
+ "gpt-5.6-luna",
208
+ "gpt-5.6-sol",
209
+ "gpt-5.6-terra",
210
+ "meta/muse-spark-1.1",
211
+ "meta/muse-spark-1.2",
212
+ "meta/muse-spark-1.2-contributor",
213
+ "moonshotai/Kimi-K2.5",
214
+ "moonshotai/Kimi-K2.6",
215
+ "moonshotai/Kimi-K2.7-Code",
216
+ "moonshotai/Kimi-K2.7-Code-Highspeed",
217
+ "moonshotai/Kimi-K3",
218
+ "sakana/fugu-ultra",
219
+ "stepfun/Step-3.7-Flash",
220
+ "thinkingmachines/inkling",
221
+ "thinkingmachines/inkling-small",
222
+ "xai/grok-4.5",
223
+ "xiaomi/mimo-v2.5"
224
+ ]);
169
225
  const COMMAND_CODE_CLI_VERSION = "1.26.0";
170
226
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
171
227
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
172
228
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
229
+ /** Head-of-request timeout: how long to wait for the first response byte. */
230
+ const DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
231
+ /** Stream idle timeout: a generation that stalls this long is a dead connection. */
232
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 12e4;
173
233
  const MODEL_CACHE_VERSION = 1;
174
234
  function isRecord(value) {
175
235
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -281,7 +341,24 @@ function hasImageContent(message) {
281
341
  const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
282
342
  return check(message.content);
283
343
  }
284
- function messagesToCC(messages) {
344
+ /**
345
+ * Convert one image reference to the Command Code wire format, as the official
346
+ * CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
347
+ * Bytes come from the durable attachment service; the media type is the one
348
+ * verified at save time.
349
+ */
350
+ async function imageToCommandCode(ref, readImage) {
351
+ const data = await readImage(ref);
352
+ return {
353
+ type: "image",
354
+ source: {
355
+ type: "base64",
356
+ media_type: ref.mediaType,
357
+ data: Buffer.from(data).toString("base64")
358
+ }
359
+ };
360
+ }
361
+ async function messagesToCC(messages, readImage) {
285
362
  const out = [];
286
363
  const paired = pairedToolCallIds(messages);
287
364
  for (const message of messages) {
@@ -293,7 +370,10 @@ function messagesToCC(messages) {
293
370
  type: "text",
294
371
  text: block.text
295
372
  });
296
- if (block.type === "image") throw new LlmError("Image input is not wired to the attachment service in this adapter yet", "UNSUPPORTED_CONTENT");
373
+ if (block.type === "image") {
374
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
375
+ parts.push(await imageToCommandCode(block.attachment, readImage));
376
+ }
297
377
  }
298
378
  out.push({
299
379
  role: "user",
@@ -345,10 +425,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
345
425
  deps;
346
426
  catalog = [];
347
427
  fetchImpl;
428
+ resolveAttachments;
348
429
  constructor(deps) {
349
430
  super();
350
431
  this.deps = deps;
351
432
  this.fetchImpl = deps.fetchImpl ?? fetch;
433
+ this.resolveAttachments = deps.resolveAttachments;
352
434
  }
353
435
  /**
354
436
  * Command Code is a metered subscription API: 429 (rate limit) and 5xx
@@ -382,21 +464,27 @@ var CommandCodeAdapter = class extends LlmAdapter {
382
464
  return this.catalog;
383
465
  }
384
466
  async listModels(provider) {
385
- return (await this.loadCatalog()).map((model) => ({
386
- provider,
387
- id: model.id,
388
- name: `${model.name} (CC)`,
389
- inputModalities: ["text"]
390
- }));
467
+ return (await this.loadCatalog()).map((model) => {
468
+ const vision = KNOWN_IMAGE_MODELS.has(model.id);
469
+ return {
470
+ provider,
471
+ id: model.id,
472
+ name: `${model.name} (CC)`,
473
+ description: vision ? "Supports image input" : "Text only",
474
+ inputModalities: vision ? ["text", "image"] : ["text"]
475
+ };
476
+ });
391
477
  }
392
478
  async resolveModel(provider, model, signal) {
393
479
  const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
394
480
  const efforts = KNOWN_EFFORTS[model];
481
+ const vision = KNOWN_IMAGE_MODELS.has(model);
395
482
  return {
396
483
  provider,
397
484
  id: model,
398
485
  name: entry ? `${entry.name} (CC)` : model,
399
- inputModalities: ["text"],
486
+ description: vision ? "Supports image input" : "Text only",
487
+ inputModalities: vision ? ["text", "image"] : ["text"],
400
488
  ...entry ? {
401
489
  context: { contextWindow: entry.contextWindow },
402
490
  defaultMaxTokens: Math.min(entry.maxTokens, DEFAULT_GENERATE_MAX_TOKENS)
@@ -485,7 +573,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
485
573
  }
486
574
  async *stream(options) {
487
575
  if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
488
- if (options.messages.some(hasImageContent)) throw new LlmError("Image input is not wired to the attachment service in this adapter yet", "UNSUPPORTED_CONTENT");
576
+ const hasImages = options.messages.some(hasImageContent);
577
+ let readImage;
578
+ if (hasImages) {
579
+ if (!KNOWN_IMAGE_MODELS.has(options.model)) throw new LlmError(`Command Code model "${options.model}" does not support image input; switch to a Vision-capable model (see the model registry)`, "UNSUPPORTED_CONTENT");
580
+ const attachments = this.resolveAttachments?.();
581
+ if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
582
+ readImage = (ref) => attachments.readImage(ref).then((stored) => stored.data);
583
+ }
489
584
  const connection = this.deps.options();
490
585
  const apiKey = await this.deps.resolveApiKey(connection);
491
586
  const modelMax = this.catalog.find((m) => m.id === options.model)?.maxTokens ?? 65536;
@@ -511,7 +606,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
511
606
  skills: null,
512
607
  params: {
513
608
  model: options.model,
514
- messages: messagesToCC(options.messages),
609
+ messages: await messagesToCC(options.messages, readImage),
515
610
  tools: (options.tools ?? []).map((tool) => ({
516
611
  type: "function",
517
612
  name: tool.name,
@@ -541,11 +636,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
541
636
  ...attributionHeaders()
542
637
  },
543
638
  body: JSON.stringify(body),
544
- ...options.signal ? { signal: options.signal } : {}
639
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(connection.requestTimeoutMs)]) : AbortSignal.timeout(connection.requestTimeoutMs)
545
640
  });
546
641
  } catch (error) {
547
642
  if (options.signal?.aborted) throw error;
548
- throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate failed`, "TRANSPORT", { cause: error });
643
+ if (error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)}`, "TIMEOUT", { cause: error });
644
+ throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate failed: ${errorChain(error)}`, "TRANSPORT", { cause: error });
549
645
  }
550
646
  if (!response.ok) {
551
647
  const errText = await response.text().catch(() => "");
@@ -562,6 +658,21 @@ var CommandCodeAdapter = class extends LlmAdapter {
562
658
  const reader = response.body.getReader();
563
659
  const decoder = new TextDecoder();
564
660
  let buffer = "";
661
+ let idleTimer;
662
+ let idleFired = false;
663
+ const armIdle = () => {
664
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
665
+ idleTimer = setTimeout(() => {
666
+ idleFired = true;
667
+ reader.cancel().catch(() => void 0);
668
+ }, connection.streamIdleTimeoutMs);
669
+ };
670
+ const clearIdle = () => {
671
+ if (idleTimer !== void 0) {
672
+ clearTimeout(idleTimer);
673
+ idleTimer = void 0;
674
+ }
675
+ };
565
676
  let nextIndex = 0;
566
677
  let textIndex = -1;
567
678
  let textContent = "";
@@ -708,14 +819,18 @@ var CommandCodeAdapter = class extends LlmAdapter {
708
819
  let finished = false;
709
820
  for (;;) {
710
821
  let read;
822
+ armIdle();
711
823
  try {
712
824
  read = await reader.read();
713
825
  } catch (error) {
714
826
  if (options.signal?.aborted) throw error;
715
- throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading`, "TRANSPORT", { cause: error });
827
+ throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)}`, "TRANSPORT", { cause: error });
828
+ } finally {
829
+ clearIdle();
716
830
  }
717
831
  const { done, value } = read;
718
832
  if (done) {
833
+ if (idleFired) throw new LlmError(`Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection`, "TIMEOUT");
719
834
  if (buffer.trim()) for (const chunk of handleEvent(parseStreamEventLine(buffer))) yield chunk;
720
835
  break;
721
836
  }
@@ -741,6 +856,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
741
856
  };
742
857
  }
743
858
  } finally {
859
+ clearIdle();
744
860
  await reader.cancel().catch(() => void 0);
745
861
  reader.releaseLock();
746
862
  }
@@ -844,11 +960,17 @@ function applyCommands(ctx, deps) {
844
960
  *
845
961
  * ```yaml
846
962
  * - id: llm-commandcode
847
- * name: dsh-commandcode-provider
963
+ * name: "@mars-sea/dsh-commandcode-provider"
848
964
  * config:
849
965
  * apiKeyEnv: COMMANDCODE_API_KEY
850
966
  * ```
851
967
  *
968
+ * The `name` is the full package specifier as installed in the profile's
969
+ * node_modules: the loader imports it as a module, and pnpm links packages by
970
+ * their true (scoped) name — a bare `dsh-commandcode-provider` fails to
971
+ * resolve (ERR_MODULE_NOT_FOUND) and crashes the app on boot. The value must
972
+ * be quoted in YAML: an unquoted scalar starting with `@` fails to parse.
973
+ *
852
974
  * @module dsh-commandcode-provider
853
975
  */
854
976
  const name = "llm-commandcode";
@@ -864,7 +986,9 @@ const Config = z.object({
864
986
  apiKey: z.string(),
865
987
  apiBase: z.string(),
866
988
  workingDir: z.string(),
867
- modelsCachePath: z.string()
989
+ modelsCachePath: z.string(),
990
+ requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
991
+ streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS)
868
992
  });
869
993
  /**
870
994
  * The one explicit resolve step from raw config to validated connection
@@ -877,7 +1001,9 @@ function resolveAdapterOptions(config) {
877
1001
  apiKeyEnv: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
878
1002
  apiBase: config.apiBase ?? "https://api.commandcode.ai",
879
1003
  workingDir: config.workingDir ?? process.cwd(),
880
- modelsCachePath: config.modelsCachePath ?? DEFAULT_MODELS_CACHE_PATH
1004
+ modelsCachePath: config.modelsCachePath ?? DEFAULT_MODELS_CACHE_PATH,
1005
+ requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
1006
+ streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 12e4
881
1007
  };
882
1008
  }
883
1009
  function apply(ctx, config) {
@@ -911,7 +1037,11 @@ function apply(ctx, config) {
911
1037
  };
912
1038
  const adapter = new CommandCodeAdapter({
913
1039
  options,
914
- resolveApiKey
1040
+ resolveApiKey,
1041
+ resolveAttachments: () => {
1042
+ const attachments = ctx.get("attachments");
1043
+ return attachments === void 0 ? void 0 : attachments;
1044
+ }
915
1045
  });
916
1046
  ctx.llm.registerConfigurableProviders([{
917
1047
  provider: PROVIDER,
@@ -931,6 +1061,6 @@ function apply(ctx, config) {
931
1061
  });
932
1062
  }
933
1063
  //#endregion
934
- export { COMMAND_CODE_CLI_VERSION, CommandCodeAdapter, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, KNOWN_EFFORTS, PROVIDER, apply, applyCommands, commandDefinition, inject, name, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey };
1064
+ export { COMMAND_CODE_CLI_VERSION, CommandCodeAdapter, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, PROVIDER, apply, applyCommands, commandDefinition, inject, name, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey };
935
1065
 
936
1066
  //# sourceMappingURL=index.js.map