@love-moon/ai-sdk 0.7.7 → 0.9.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/context-files.d.ts +16 -0
  5. package/dist/context-files.js +86 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +4 -1
  8. package/dist/manager/index.d.ts +1 -0
  9. package/dist/manager/index.js +1 -0
  10. package/dist/manager/install.js +29 -2
  11. package/dist/manager/manager.d.ts +5 -1
  12. package/dist/manager/manager.js +8 -0
  13. package/dist/manager/network.js +4 -2
  14. package/dist/manager/quota/dsh.d.ts +55 -0
  15. package/dist/manager/quota/dsh.js +146 -0
  16. package/dist/manager/types.d.ts +26 -1
  17. package/dist/media-adapters.d.ts +34 -0
  18. package/dist/media-adapters.js +57 -0
  19. package/dist/media-input.d.ts +21 -0
  20. package/dist/media-input.js +197 -0
  21. package/dist/providers/chat-web-session.d.ts +7 -1
  22. package/dist/providers/chat-web-session.js +12 -1
  23. package/dist/providers/claude-agent-sdk-session.d.ts +10 -1
  24. package/dist/providers/claude-agent-sdk-session.js +24 -4
  25. package/dist/providers/codex-app-server-session.d.ts +12 -1
  26. package/dist/providers/codex-app-server-session.js +49 -20
  27. package/dist/providers/codex-exec-session.d.ts +8 -3
  28. package/dist/providers/codex-exec-session.js +13 -13
  29. package/dist/providers/copilot-sdk-session.d.ts +6 -1
  30. package/dist/providers/copilot-sdk-session.js +10 -3
  31. package/dist/providers/dsh-sdk-session.d.ts +194 -0
  32. package/dist/providers/dsh-sdk-session.js +951 -0
  33. package/dist/providers/kimi-cli-session.d.ts +11 -1
  34. package/dist/providers/kimi-cli-session.js +58 -4
  35. package/dist/providers/kimi-print-session.d.ts +10 -3
  36. package/dist/providers/kimi-print-session.js +23 -53
  37. package/dist/providers/opencode-sdk-session.d.ts +6 -1
  38. package/dist/providers/opencode-sdk-session.js +11 -9
  39. package/dist/resume/dsh.d.ts +23 -0
  40. package/dist/resume/dsh.js +122 -0
  41. package/dist/resume/index.js +2 -0
  42. package/dist/session-factory.d.ts +3 -1
  43. package/dist/session-factory.js +5 -1
  44. package/dsh/cordis.yml +100 -0
  45. package/package.json +24 -3
@@ -22,6 +22,9 @@ export class KimiCliSession extends EventEmitter<[never]> {
22
22
  currentTurn: {
23
23
  fullText: string;
24
24
  bufferedAssistantText: string;
25
+ thinkText: string;
26
+ thinkStatusLine: string;
27
+ thinkStatusLineAt: number;
25
28
  items: never[];
26
29
  toolCalls: Map<any, any>;
27
30
  onProgress: null;
@@ -80,6 +83,11 @@ export class KimiCliSession extends EventEmitter<[never]> {
80
83
  command: any;
81
84
  } | null;
82
85
  currentTurnStatus: any;
86
+ capabilities: {
87
+ media: Readonly<{
88
+ image: "native";
89
+ }>;
90
+ };
83
91
  pid: any;
84
92
  };
85
93
  getSessionInfo(): {
@@ -136,6 +144,8 @@ export class KimiCliSession extends EventEmitter<[never]> {
136
144
  bootInternal(): Promise<void>;
137
145
  updateUsageFromStatus(payload: any): void;
138
146
  appendAssistantText(currentTurn: any, text: any): void;
147
+ appendThinkText(currentTurn: any, text: any): void;
148
+ thinkStatusLine(currentTurn: any): any;
139
149
  finalizeAssistantMessage(currentTurn: any): Promise<boolean>;
140
150
  maybeEmitAuthRequired(error: any): void;
141
151
  failUnexpectedInteractiveRequest(id: any, requestType: any, payload: any): Promise<void>;
@@ -144,7 +154,7 @@ export class KimiCliSession extends EventEmitter<[never]> {
144
154
  handleTransportFailure(error: any): void;
145
155
  handleTransportExit(payload: any): void;
146
156
  interruptCurrentTurn(): Promise<boolean>;
147
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
157
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
148
158
  useInitialImages?: boolean | undefined;
149
159
  onProgress?: null | undefined;
150
160
  jsonSchema?: null | undefined;
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { KIMI_CLI_WIRE_VARIANT as KIMI_PROVIDER_VARIANT } from "../built-in-backends.js";
4
+ import { appendContextFilesToPrompt } from "../context-files.js";
5
+ import { PROVIDER_MEDIA_CAPABILITIES, buildKimiContent } from "../media-adapters.js";
6
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
4
7
  import { KimiWireTransport } from "../transports/kimi-wire-transport.js";
5
8
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
6
9
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
@@ -11,6 +14,8 @@ const DEFAULT_STATUS_THROTTLE_MS = 450;
11
14
  const DEFAULT_REASONING_STATUS_THROTTLE_MS = 2500;
12
15
  const DEFAULT_STATUS_PREVIEW_DELTA_CHARS = 24;
13
16
  const MAX_STATUS_TIMING_MS = 10 * 1000;
17
+ const THINK_STATUS_LINE_MAX_CHARS = 120;
18
+ const THINK_TAIL_BUFFER_CHARS = 400;
14
19
  function waitForever() {
15
20
  return new Promise(() => { });
16
21
  }
@@ -228,6 +233,7 @@ export class KimiCliSession extends EventEmitter {
228
233
  }
229
234
  : null,
230
235
  currentTurnStatus: this.getCurrentTurnStatus(),
236
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT] },
231
237
  pid: this.transport.pid || undefined,
232
238
  };
233
239
  }
@@ -603,6 +609,29 @@ export class KimiCliSession extends EventEmitter {
603
609
  }
604
610
  currentTurn.bufferedAssistantText += normalized;
605
611
  }
612
+ appendThinkText(currentTurn, text) {
613
+ const normalized = normalizeText(text);
614
+ if (!normalized) {
615
+ return;
616
+ }
617
+ currentTurn.thinkText = `${currentTurn.thinkText}${normalized}`.slice(-THINK_TAIL_BUFFER_CHARS);
618
+ }
619
+ thinkStatusLine(currentTurn) {
620
+ const now = this.now();
621
+ if (currentTurn.thinkStatusLine &&
622
+ now - currentTurn.thinkStatusLineAt < this.reasoningStatusThrottleMs) {
623
+ return currentTurn.thinkStatusLine;
624
+ }
625
+ const sanitized = sanitizeSummary(currentTurn.thinkText, THINK_TAIL_BUFFER_CHARS);
626
+ const snippet = sanitized.length > THINK_STATUS_LINE_MAX_CHARS
627
+ ? `…${sanitized.slice(-THINK_STATUS_LINE_MAX_CHARS)}`
628
+ : sanitized;
629
+ if (snippet && snippet !== currentTurn.thinkStatusLine) {
630
+ currentTurn.thinkStatusLine = snippet;
631
+ currentTurn.thinkStatusLineAt = now;
632
+ }
633
+ return currentTurn.thinkStatusLine || statusLineForPhase("reasoning");
634
+ }
606
635
  async finalizeAssistantMessage(currentTurn) {
607
636
  if (!currentTurn) {
608
637
  return false;
@@ -707,8 +736,23 @@ export class KimiCliSession extends EventEmitter {
707
736
  return;
708
737
  case "TurnEnd":
709
738
  currentTurn.seenTurnEnd = true;
739
+ if (currentTurn.bufferedAssistantText) {
740
+ await this.finalizeAssistantMessage(currentTurn);
741
+ }
742
+ // Clear reply_in_progress as soon as the wire reports the turn ended,
743
+ // decoupled from the `prompt` JSON-RPC promise. Without this, a delayed
744
+ // or dropped prompt RPC response leaves the web UI pinned on
745
+ // "Kimi is writing the reply" until reload. emitTerminalWorkingStatus is
746
+ // idempotent, so the later runTurn call becomes a harmless no-op.
747
+ await this.emitTerminalWorkingStatus(currentTurn, {
748
+ phase: "turn_completed",
749
+ status_done_line: "Kimi finished",
750
+ }, currentTurn.onProgress);
710
751
  return;
711
752
  case "StepBegin":
753
+ if (currentTurn.bufferedAssistantText) {
754
+ await this.finalizeAssistantMessage(currentTurn);
755
+ }
712
756
  await this.emitWorkingStatus({
713
757
  phase: "reasoning",
714
758
  reply_in_progress: true,
@@ -734,10 +778,11 @@ export class KimiCliSession extends EventEmitter {
734
778
  return;
735
779
  case "ContentPart":
736
780
  if (normalizedPayload.type === "think") {
781
+ this.appendThinkText(currentTurn, normalizedPayload.think);
737
782
  await this.emitWorkingStatus({
738
783
  phase: "reasoning",
739
784
  reply_in_progress: true,
740
- status_line: statusLineForPhase("reasoning"),
785
+ status_line: this.thinkStatusLine(currentTurn),
741
786
  }, currentTurn.onProgress);
742
787
  return;
743
788
  }
@@ -832,11 +877,15 @@ export class KimiCliSession extends EventEmitter {
832
877
  }
833
878
  return false;
834
879
  }
835
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
880
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
836
881
  if (this.closeRequested || this.closed) {
837
882
  throw this.createSessionClosedError();
838
883
  }
839
- let effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
884
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
885
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[KIMI_PROVIDER_VARIANT]);
886
+ let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
887
+ (media.length ? defaultPromptForMedia(media) : "");
888
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
840
889
  if (jsonSchema && typeof jsonSchema === "object" && effectivePrompt) {
841
890
  effectivePrompt = injectJsonSchemaPrompt(effectivePrompt, jsonSchema);
842
891
  }
@@ -860,6 +909,9 @@ export class KimiCliSession extends EventEmitter {
860
909
  const currentTurn = {
861
910
  fullText: "",
862
911
  bufferedAssistantText: "",
912
+ thinkText: "",
913
+ thinkStatusLine: "",
914
+ thinkStatusLineAt: 0,
863
915
  items: [],
864
916
  toolCalls: new Map(),
865
917
  onProgress,
@@ -886,7 +938,9 @@ export class KimiCliSession extends EventEmitter {
886
938
  }, onProgress);
887
939
  const promptResult = await Promise.race([
888
940
  this.transport.request("prompt", {
889
- user_input: effectivePrompt,
941
+ user_input: media.length === 0
942
+ ? effectivePrompt
943
+ : buildKimiContent(effectivePrompt, media),
890
944
  }),
891
945
  turnFailurePromise,
892
946
  closeGuard.promise,
@@ -60,6 +60,13 @@ export class KimiPrintSession extends EventEmitter<[never]> {
60
60
  command: string;
61
61
  } | null;
62
62
  currentTurnStatus: any;
63
+ capabilities: {
64
+ media: Readonly<{
65
+ image: "unsupported";
66
+ }> | Readonly<{
67
+ image: "native";
68
+ }>;
69
+ };
63
70
  pid: any;
64
71
  };
65
72
  getSessionInfo(): {
@@ -100,15 +107,15 @@ export class KimiPrintSession extends EventEmitter<[never]> {
100
107
  emitAssistantMessage(text: any): Promise<void>;
101
108
  buildPrompt(promptText: any): string;
102
109
  buildPrintArgs(promptText?: string): string[];
103
- buildUserMessage(promptText: any, { useInitialImages }?: {
104
- useInitialImages?: boolean | undefined;
110
+ buildUserMessage(promptText: any, { media }?: {
111
+ media?: never[] | undefined;
105
112
  }): {
106
113
  role: string;
107
114
  content: any;
108
115
  };
109
116
  maybeEmitAuthRequired(stderrTail: any): void;
110
117
  interruptCurrentTurn(): Promise<boolean>;
111
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
118
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
112
119
  useInitialImages?: boolean | undefined;
113
120
  onProgress?: null | undefined;
114
121
  jsonSchema?: null | undefined;
@@ -5,6 +5,9 @@ import { randomUUID } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import readline from "node:readline";
7
7
  import { KIMI_CLI_PRINT_VARIANT as KIMI_PRINT_PROVIDER_VARIANT } from "../built-in-backends.js";
8
+ import { appendContextFilesToPrompt } from "../context-files.js";
9
+ import { PROVIDER_MEDIA_CAPABILITIES, UNSUPPORTED_MEDIA_CAPABILITIES, buildKimiContent, } from "../media-adapters.js";
10
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
8
11
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
9
12
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
10
13
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -165,31 +168,6 @@ function normalizeTextContent(content) {
165
168
  }
166
169
  return "";
167
170
  }
168
- function guessMimeType(filePath) {
169
- const ext = path.extname(String(filePath || "")).toLowerCase();
170
- switch (ext) {
171
- case ".png":
172
- return "image/png";
173
- case ".jpg":
174
- case ".jpeg":
175
- return "image/jpeg";
176
- case ".gif":
177
- return "image/gif";
178
- case ".webp":
179
- return "image/webp";
180
- case ".bmp":
181
- return "image/bmp";
182
- case ".svg":
183
- return "image/svg+xml";
184
- default:
185
- return "application/octet-stream";
186
- }
187
- }
188
- function filePathToDataUri(filePath) {
189
- const buffer = fs.readFileSync(filePath);
190
- const mimeType = guessMimeType(filePath);
191
- return `data:${mimeType};base64,${buffer.toString("base64")}`;
192
- }
193
171
  export class KimiPrintSession extends EventEmitter {
194
172
  constructor(backend, options = {}) {
195
173
  super();
@@ -291,6 +269,11 @@ export class KimiPrintSession extends EventEmitter {
291
269
  }
292
270
  : null,
293
271
  currentTurnStatus: this.getCurrentTurnStatus(),
272
+ capabilities: {
273
+ media: this.cliMode === "prompt"
274
+ ? UNSUPPORTED_MEDIA_CAPABILITIES
275
+ : PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT],
276
+ },
294
277
  pid: this.currentTurn?.child?.pid || undefined,
295
278
  };
296
279
  }
@@ -444,31 +427,16 @@ export class KimiPrintSession extends EventEmitter {
444
427
  }
445
428
  return args;
446
429
  }
447
- buildUserMessage(promptText, { useInitialImages = false } = {}) {
448
- const images = useInitialImages && Array.isArray(this.options.initialImages)
449
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
450
- : [];
451
- if (images.length === 0) {
430
+ buildUserMessage(promptText, { media = [] } = {}) {
431
+ if (media.length === 0) {
452
432
  return {
453
433
  role: "user",
454
434
  content: promptText,
455
435
  };
456
436
  }
457
- const content = [];
458
- if (promptText) {
459
- content.push({ type: "text", text: promptText });
460
- }
461
- for (const imagePath of images) {
462
- content.push({
463
- type: "image_url",
464
- image_url: {
465
- url: filePathToDataUri(imagePath),
466
- },
467
- });
468
- }
469
437
  return {
470
438
  role: "user",
471
- content,
439
+ content: buildKimiContent(promptText, media),
472
440
  };
473
441
  }
474
442
  maybeEmitAuthRequired(stderrTail) {
@@ -500,7 +468,7 @@ export class KimiPrintSession extends EventEmitter {
500
468
  return false;
501
469
  }
502
470
  }
503
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
471
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
504
472
  if (this.closeRequested || this.closed) {
505
473
  throw this.createSessionClosedError();
506
474
  }
@@ -509,21 +477,23 @@ export class KimiPrintSession extends EventEmitter {
509
477
  reason: "turn_already_running",
510
478
  });
511
479
  }
480
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
481
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT]);
512
482
  let effectivePrompt = this.buildPrompt(promptText);
513
- const imagePaths = useInitialImages && Array.isArray(this.options.initialImages)
514
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
515
- : [];
483
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
516
484
  if (jsonSchema && typeof jsonSchema === "object") {
517
- const promptWithSchema = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached images." : "");
485
+ const promptWithSchema = effectivePrompt || (media.length > 0 ? defaultPromptForMedia(media) : "");
518
486
  if (promptWithSchema) {
519
487
  effectivePrompt = injectJsonSchemaPrompt(promptWithSchema, jsonSchema);
520
488
  }
521
489
  }
522
- if (this.cliMode === "prompt" && imagePaths.length > 0) {
523
- const imageContext = imagePaths.map((item, index) => `${index + 1}. ${item}`).join("\n");
524
- effectivePrompt = `${effectivePrompt || "Analyze the attached images."}\n\nAttached image files:\n${imageContext}`;
490
+ if (this.cliMode === "prompt" && media.length > 0) {
491
+ throw createTurnError("Kimi prompt mode does not support media input", {
492
+ reason: "unsupported_media",
493
+ backend: this.backend,
494
+ });
525
495
  }
526
- if (!effectivePrompt && imagePaths.length === 0) {
496
+ if (!effectivePrompt && media.length === 0) {
527
497
  return buildEmptyTurnResult();
528
498
  }
529
499
  this.announceSession();
@@ -685,7 +655,7 @@ export class KimiPrintSession extends EventEmitter {
685
655
  });
686
656
  });
687
657
  if (this.cliMode === "legacy-print") {
688
- const userMessage = this.buildUserMessage(effectivePrompt, { useInitialImages });
658
+ const userMessage = this.buildUserMessage(effectivePrompt || defaultPromptForMedia(media), { media });
689
659
  child.stdin.write(`${JSON.stringify(userMessage)}\n`);
690
660
  }
691
661
  child.stdin.end();
@@ -78,6 +78,11 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
78
78
  resumeReady: boolean;
79
79
  manualResume: null;
80
80
  currentTurnStatus: any;
81
+ capabilities: {
82
+ media: Readonly<{
83
+ image: "native";
84
+ }>;
85
+ };
81
86
  pid: any;
82
87
  };
83
88
  getSessionInfo(): {
@@ -164,7 +169,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
164
169
  handleTransportFailure(error: any): void;
165
170
  handleTransportExit(payload: any): void;
166
171
  interruptCurrentTurn(): Promise<boolean>;
167
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
172
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
168
173
  useInitialImages?: boolean | undefined;
169
174
  onProgress?: null | undefined;
170
175
  jsonSchema?: null | undefined;
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { OPENCODE_SDK_VARIANT as OPENCODE_PROVIDER_VARIANT } from "../built-in-backends.js";
3
+ import { appendContextFilesToPrompt } from "../context-files.js";
4
+ import { PROVIDER_MEDIA_CAPABILITIES, buildOpencodeParts } from "../media-adapters.js";
5
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
3
6
  import { OpencodeServerTransport } from "../transports/opencode-server-transport.js";
4
7
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
5
8
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
@@ -223,6 +226,7 @@ export class OpencodeSdkSession extends EventEmitter {
223
226
  resumeReady: Boolean(this.sessionId),
224
227
  manualResume: null,
225
228
  currentTurnStatus: this.getCurrentTurnStatus(),
229
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
226
230
  pid: this.transport.pid || undefined,
227
231
  };
228
232
  }
@@ -1136,12 +1140,15 @@ export class OpencodeSdkSession extends EventEmitter {
1136
1140
  }
1137
1141
  return true;
1138
1142
  }
1139
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
1143
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
1140
1144
  if (this.closeRequested) {
1141
1145
  throw this.createSessionClosedError();
1142
1146
  }
1143
- const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
1144
- if (!effectivePrompt) {
1147
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
1148
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT]);
1149
+ const effectivePrompt = appendContextFilesToPrompt(this.buildPrompt(promptText, { useInitialImages: false }) ||
1150
+ (media.length ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
1151
+ if (!effectivePrompt && media.length === 0) {
1145
1152
  return buildEmptyTurnResult();
1146
1153
  }
1147
1154
  if (this.currentTurn) {
@@ -1216,12 +1223,7 @@ export class OpencodeSdkSession extends EventEmitter {
1216
1223
  format: jsonSchema && typeof jsonSchema === "object"
1217
1224
  ? { type: "json_schema", schema: jsonSchema }
1218
1225
  : undefined,
1219
- parts: [
1220
- {
1221
- type: "text",
1222
- text: effectivePrompt,
1223
- },
1224
- ],
1226
+ parts: buildOpencodeParts(effectivePrompt, media),
1225
1227
  }, {
1226
1228
  throwOnError: true,
1227
1229
  responseStyle: "data",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * dsh sessions persist as JSONL logs written by the runtime's
3
+ * `@deepseek-ai/dsh-session-persistence-jsonl` plugin under the session root
4
+ * DshSdkSession sets via DSH_SESSION_ROOT:
5
+ *
6
+ * <root>/<projectKey(cwd)>/<sessionId>/session.jsonl
7
+ *
8
+ * The first line of session.jsonl is a `{"type":"session",...}` header that
9
+ * carries the workspace `cwd`, which is what resume needs back.
10
+ */
11
+ export function buildCliArgs(sessionId: any): string[];
12
+ export function findSessionPath(sessionId: any, options?: {}): Promise<string | null>;
13
+ export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
14
+ provider: any;
15
+ sessionId: any;
16
+ sessionPath: null;
17
+ cwd: any;
18
+ debugMetadata: {
19
+ cwdSource: any;
20
+ sessionPath: null;
21
+ };
22
+ }>;
23
+ export const BACKEND: "dsh";
@@ -0,0 +1,122 @@
1
+ import { promises as fsp } from "node:fs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { defaultDshSessionRoot } from "../providers/dsh-sdk-session.js";
6
+ import { buildResumeContext, isExistingDirectory, normalizeSessionId, } from "./shared.js";
7
+ export const BACKEND = "dsh";
8
+ /**
9
+ * dsh sessions persist as JSONL logs written by the runtime's
10
+ * `@deepseek-ai/dsh-session-persistence-jsonl` plugin under the session root
11
+ * DshSdkSession sets via DSH_SESSION_ROOT:
12
+ *
13
+ * <root>/<projectKey(cwd)>/<sessionId>/session.jsonl
14
+ *
15
+ * The first line of session.jsonl is a `{"type":"session",...}` header that
16
+ * carries the workspace `cwd`, which is what resume needs back.
17
+ */
18
+ export function buildCliArgs(sessionId) {
19
+ // dsh has no standalone CLI resume command inside conductor (the runtime is
20
+ // relaunched by DshSdkSession through the SDK client), but the resume
21
+ // contract requires a stable arg vector; consumers ignore it for dsh.
22
+ const normalizedSessionId = normalizeSessionId(sessionId);
23
+ if (!normalizedSessionId) {
24
+ return [];
25
+ }
26
+ return [`--resume-session-id=${normalizedSessionId}`];
27
+ }
28
+ function resolveSessionRoot(options = {}) {
29
+ if (typeof options.dshSessionRoot === "string" && options.dshSessionRoot.trim()) {
30
+ return options.dshSessionRoot.trim();
31
+ }
32
+ return defaultDshSessionRoot();
33
+ }
34
+ export async function findSessionPath(sessionId, options = {}) {
35
+ const normalizedSessionId = normalizeSessionId(sessionId);
36
+ if (!normalizedSessionId) {
37
+ return null;
38
+ }
39
+ const root = resolveSessionRoot(options);
40
+ let projectEntries = [];
41
+ try {
42
+ projectEntries = await fsp.readdir(root, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ for (const projectEntry of projectEntries) {
48
+ if (!projectEntry.isDirectory()) {
49
+ continue;
50
+ }
51
+ const sessionDir = path.join(root, projectEntry.name, normalizedSessionId);
52
+ for (const filename of ["session.jsonl", "session.jsonl.zstd"]) {
53
+ const candidate = path.join(sessionDir, filename);
54
+ try {
55
+ const stats = await fsp.stat(candidate);
56
+ if (stats.isFile()) {
57
+ return candidate;
58
+ }
59
+ }
60
+ catch {
61
+ // keep scanning
62
+ }
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ async function readHeaderLine(sessionPath) {
68
+ if (!sessionPath || !sessionPath.endsWith(".jsonl")) {
69
+ return null;
70
+ }
71
+ const rl = readline.createInterface({
72
+ input: fs.createReadStream(sessionPath),
73
+ crlfDelay: Infinity,
74
+ });
75
+ try {
76
+ for await (const line of rl) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) {
79
+ continue;
80
+ }
81
+ try {
82
+ return JSON.parse(trimmed);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ }
89
+ finally {
90
+ rl.close();
91
+ }
92
+ return null;
93
+ }
94
+ export async function resolveResumeContext(sessionId, options = {}) {
95
+ const normalizedSessionId = normalizeSessionId(sessionId);
96
+ if (!normalizedSessionId) {
97
+ throw new Error("--resume requires a session id");
98
+ }
99
+ const sessionPath = await findSessionPath(normalizedSessionId, options);
100
+ if (!sessionPath) {
101
+ throw new Error(`Invalid --resume session id for dsh: ${normalizedSessionId}`);
102
+ }
103
+ const header = await readHeaderLine(sessionPath);
104
+ const cwdFromHeader = header && header.type === "session" && typeof header.cwd === "string" && header.cwd.trim()
105
+ ? header.cwd.trim()
106
+ : "";
107
+ const cwd = cwdFromHeader ||
108
+ (typeof options?.cwd === "string" && options.cwd.trim() ? options.cwd.trim() : "");
109
+ if (!cwd) {
110
+ throw new Error(`Could not resolve workspace for dsh session ${normalizedSessionId}`);
111
+ }
112
+ if (!(await isExistingDirectory(cwd))) {
113
+ throw new Error(`Resume workspace path does not exist: ${cwd}`);
114
+ }
115
+ return buildResumeContext({
116
+ provider: "dsh",
117
+ sessionId: normalizedSessionId,
118
+ sessionPath,
119
+ cwd,
120
+ cwdSource: cwdFromHeader ? "session" : "options_cwd",
121
+ });
122
+ }
@@ -4,6 +4,7 @@ import * as chatWeb from "./chat-web.js";
4
4
  import * as claude from "./claude.js";
5
5
  import * as codex from "./codex.js";
6
6
  import * as copilot from "./copilot.js";
7
+ import * as dsh from "./dsh.js";
7
8
  import * as kimi from "./kimi.js";
8
9
  import * as opencode from "./opencode.js";
9
10
  import { buildResumeContext, isExistingDirectory, normalizeBackend, normalizeProjectPathCandidate, normalizeSessionId, resolveSessionRunDirectory, } from "./shared.js";
@@ -27,6 +28,7 @@ const RESUME_MODULES_BY_BACKEND = new Map([
27
28
  ["kimi", kimi],
28
29
  ["opencode", opencode],
29
30
  ["chat-web", chatWeb],
31
+ ["dsh", dsh],
30
32
  ]);
31
33
  // Sanity-check at module load:
32
34
  // (1) Every registered resume module must point at a real built-in backend.
@@ -11,6 +11,7 @@ export const KIMI_PROVIDER_VARIANT: "kimi-cli-wire";
11
11
  export const KIMI_PRINT_PROVIDER_VARIANT: "kimi-cli-print";
12
12
  export const OPENCODE_PROVIDER_VARIANT: "opencode-sdk";
13
13
  export const CHAT_WEB_PROVIDER_VARIANT: "chat-web-session";
14
+ export const DSH_PROVIDER_VARIANT: "dsh-sdk";
14
15
  import { BUILT_IN_BACKENDS } from "./built-in-backends.js";
15
16
  import { ChatWebSession } from "./providers/chat-web-session.js";
16
17
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
@@ -20,4 +21,5 @@ import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
20
21
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
21
22
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
22
23
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
23
- export { BUILT_IN_BACKENDS, ChatWebSession, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession };
24
+ import { DshSdkSession } from "./providers/dsh-sdk-session.js";
25
+ export { BUILT_IN_BACKENDS, ChatWebSession, CodexAppServerSession, CodexExecSession, ClaudeAgentSdkSession, CopilotSdkSession, KimiCliSession, KimiPrintSession, OpencodeSdkSession, DshSdkSession };
@@ -1,9 +1,10 @@
1
- import { BUILT_IN_BACKENDS, CHAT_WEB_SESSION_VARIANT, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
1
+ import { BUILT_IN_BACKENDS, CHAT_WEB_SESSION_VARIANT, CLAUDE_AGENT_SDK_VARIANT, CODEX_APP_SERVER_VARIANT, CODEX_EXEC_VARIANT, COPILOT_SDK_VARIANT, DSH_SDK_VARIANT, KIMI_CLI_PRINT_VARIANT, KIMI_CLI_WIRE_VARIANT, OPENCODE_SDK_VARIANT, getBuiltInBackendEntry, isBuiltInBackend, listBuiltInBackends, normalizeBuiltInBackend, } from "./built-in-backends.js";
2
2
  import { ChatWebSession } from "./providers/chat-web-session.js";
3
3
  import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
4
4
  import { CodexExecSession } from "./providers/codex-exec-session.js";
5
5
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
6
6
  import { CopilotSdkSession } from "./providers/copilot-sdk-session.js";
7
+ import { DshSdkSession } from "./providers/dsh-sdk-session.js";
7
8
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
8
9
  import { resolveKimiCliMode } from "./providers/kimi-cli-mode.js";
9
10
  import { KimiPrintSession } from "./providers/kimi-print-session.js";
@@ -17,6 +18,7 @@ export const KIMI_PROVIDER_VARIANT = KIMI_CLI_WIRE_VARIANT;
17
18
  export const KIMI_PRINT_PROVIDER_VARIANT = KIMI_CLI_PRINT_VARIANT;
18
19
  export const OPENCODE_PROVIDER_VARIANT = OPENCODE_SDK_VARIANT;
19
20
  export const CHAT_WEB_PROVIDER_VARIANT = CHAT_WEB_SESSION_VARIANT;
21
+ export const DSH_PROVIDER_VARIANT = DSH_SDK_VARIANT;
20
22
  const SESSION_FACTORIES_BY_BACKEND = new Map([
21
23
  [
22
24
  "codex",
@@ -47,6 +49,7 @@ const SESSION_FACTORIES_BY_BACKEND = new Map([
47
49
  ],
48
50
  ["opencode", (backend, options) => new OpencodeSdkSession(backend, options)],
49
51
  ["chat-web", (backend, options) => new ChatWebSession(backend, options)],
52
+ ["dsh", (backend, options) => new DshSdkSession(backend, options)],
50
53
  ]);
51
54
  function hasStructuredOutputPreference(options = {}) {
52
55
  if (!options || typeof options !== "object") {
@@ -131,3 +134,4 @@ export { CopilotSdkSession };
131
134
  export { KimiCliSession };
132
135
  export { KimiPrintSession };
133
136
  export { OpencodeSdkSession };
137
+ export { DshSdkSession };