@love-moon/ai-sdk 0.8.0 → 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 +6 -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 +1 -1
  26. package/dist/providers/codex-app-server-session.js +14 -12
  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 +6 -1
  34. package/dist/providers/kimi-cli-session.js +13 -3
  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
@@ -0,0 +1,21 @@
1
+ export function normalizeMediaInputs(value: any): {
2
+ kind: any;
3
+ path: any;
4
+ mimeType: any;
5
+ name: string;
6
+ sizeBytes: number;
7
+ }[];
8
+ export function resolveTurnMedia(sessionOptions: any, turnOptions?: {}): {
9
+ kind: any;
10
+ path: any;
11
+ mimeType: any;
12
+ name: string;
13
+ sizeBytes: number;
14
+ }[];
15
+ export function assertMediaCapabilities(media: any, backend: any, capabilities: any): void;
16
+ export function fileMediaToDataUri(media: any): string;
17
+ export function fileMediaToBase64(media: any): string;
18
+ export function createMediaInputError(message: any, extras?: {}): Error;
19
+ export function defaultPromptForMedia(media: any): string;
20
+ export const MEDIA_CAPABILITY_NATIVE: "native";
21
+ export const MEDIA_CAPABILITY_UNSUPPORTED: "unsupported";
@@ -0,0 +1,197 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const SUPPORTED_KINDS = new Set(["image"]);
4
+ const SUPPORTED_MIME_TYPES = new Set([
5
+ "image/gif",
6
+ "image/jpeg",
7
+ "image/png",
8
+ "image/webp",
9
+ ]);
10
+ const MAX_MEDIA_ITEMS = 20;
11
+ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
12
+ const MAX_TOTAL_MEDIA_BYTES = 100 * 1024 * 1024;
13
+ export const MEDIA_CAPABILITY_NATIVE = "native";
14
+ export const MEDIA_CAPABILITY_UNSUPPORTED = "unsupported";
15
+ function inferKind(mimeType) {
16
+ if (mimeType.startsWith("image/"))
17
+ return "image";
18
+ return "";
19
+ }
20
+ function inferMimeType(filePath, kind) {
21
+ const extension = path.extname(filePath).toLowerCase();
22
+ const known = {
23
+ ".bmp": "image/bmp",
24
+ ".gif": "image/gif",
25
+ ".heic": "image/heic",
26
+ ".jpeg": "image/jpeg",
27
+ ".jpg": "image/jpeg",
28
+ ".png": "image/png",
29
+ ".webp": "image/webp",
30
+ };
31
+ return known[extension] || `${kind}/unknown`;
32
+ }
33
+ function hasExpectedSignature(filePath, mimeType) {
34
+ const handle = fs.openSync(filePath, "r");
35
+ try {
36
+ const header = Buffer.alloc(12);
37
+ const bytesRead = fs.readSync(handle, header, 0, header.length, 0);
38
+ if (bytesRead < 4)
39
+ return false;
40
+ if (mimeType === "image/png")
41
+ return header.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"));
42
+ if (mimeType === "image/jpeg")
43
+ return header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff;
44
+ if (mimeType === "image/gif")
45
+ return header.subarray(0, 6).toString("ascii") === "GIF87a" || header.subarray(0, 6).toString("ascii") === "GIF89a";
46
+ if (mimeType === "image/webp")
47
+ return header.subarray(0, 4).toString("ascii") === "RIFF" && header.subarray(8, 12).toString("ascii") === "WEBP";
48
+ return false;
49
+ }
50
+ finally {
51
+ fs.closeSync(handle);
52
+ }
53
+ }
54
+ export function normalizeMediaInputs(value) {
55
+ if (value === undefined || value === null)
56
+ return [];
57
+ if (!Array.isArray(value)) {
58
+ throw createMediaInputError("media must be an array", { reason: "invalid_media_input" });
59
+ }
60
+ if (value.length > MAX_MEDIA_ITEMS) {
61
+ throw createMediaInputError(`media cannot contain more than ${MAX_MEDIA_ITEMS} files`, {
62
+ reason: "media_limit_exceeded",
63
+ });
64
+ }
65
+ let totalBytes = 0;
66
+ return value.map((item, index) => {
67
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
68
+ throw createMediaInputError(`media[${index}] must be an object`, {
69
+ reason: "invalid_media_input",
70
+ mediaIndex: index,
71
+ });
72
+ }
73
+ const filePath = typeof item.path === "string" ? item.path.trim() : "";
74
+ const mimeType = typeof item.mimeType === "string" ? item.mimeType.trim().toLowerCase() : "";
75
+ const explicitKind = typeof item.kind === "string" ? item.kind.trim().toLowerCase() : "";
76
+ const kind = explicitKind || inferKind(mimeType);
77
+ if (!SUPPORTED_KINDS.has(kind)) {
78
+ throw createMediaInputError(`media[${index}] kind must be image`, {
79
+ reason: "invalid_media_input",
80
+ mediaIndex: index,
81
+ });
82
+ }
83
+ if (!filePath || !path.isAbsolute(filePath)) {
84
+ throw createMediaInputError(`media[${index}] path must be absolute`, {
85
+ reason: "invalid_media_input",
86
+ mediaIndex: index,
87
+ });
88
+ }
89
+ let stat;
90
+ try {
91
+ const linkStat = fs.lstatSync(filePath);
92
+ if (linkStat.isSymbolicLink()) {
93
+ throw new Error("symbolic links are not allowed");
94
+ }
95
+ stat = fs.statSync(filePath);
96
+ }
97
+ catch {
98
+ throw createMediaInputError(`media[${index}] file does not exist`, {
99
+ reason: "invalid_media_input",
100
+ mediaIndex: index,
101
+ });
102
+ }
103
+ if (!stat.isFile()) {
104
+ throw createMediaInputError(`media[${index}] path must reference a file`, {
105
+ reason: "invalid_media_input",
106
+ mediaIndex: index,
107
+ });
108
+ }
109
+ const normalizedMimeType = mimeType || inferMimeType(filePath, kind);
110
+ if (!SUPPORTED_MIME_TYPES.has(normalizedMimeType) || inferKind(normalizedMimeType) !== kind) {
111
+ throw createMediaInputError(`media[${index}] kind does not match mimeType`, {
112
+ reason: "invalid_media_input",
113
+ mediaIndex: index,
114
+ });
115
+ }
116
+ if (!hasExpectedSignature(filePath, normalizedMimeType)) {
117
+ throw createMediaInputError(`media[${index}] content does not match mimeType`, {
118
+ reason: "invalid_media_input",
119
+ mediaIndex: index,
120
+ });
121
+ }
122
+ const itemLimit = MAX_IMAGE_BYTES;
123
+ if (stat.size > itemLimit) {
124
+ throw createMediaInputError(`media[${index}] exceeds the ${kind} size limit`, {
125
+ reason: "media_limit_exceeded",
126
+ mediaIndex: index,
127
+ maxBytes: itemLimit,
128
+ });
129
+ }
130
+ totalBytes += stat.size;
131
+ if (totalBytes > MAX_TOTAL_MEDIA_BYTES) {
132
+ throw createMediaInputError("media exceeds the total size limit", {
133
+ reason: "media_limit_exceeded",
134
+ maxBytes: MAX_TOTAL_MEDIA_BYTES,
135
+ });
136
+ }
137
+ return {
138
+ kind,
139
+ path: filePath,
140
+ mimeType: normalizedMimeType,
141
+ name: typeof item.name === "string" && item.name.trim()
142
+ ? path.basename(item.name.trim())
143
+ : path.basename(filePath),
144
+ sizeBytes: stat.size,
145
+ };
146
+ });
147
+ }
148
+ export function resolveTurnMedia(sessionOptions, turnOptions = {}) {
149
+ const media = normalizeMediaInputs(turnOptions.media);
150
+ if (!turnOptions.useInitialImages || !Array.isArray(sessionOptions?.initialImages)) {
151
+ return media;
152
+ }
153
+ const legacy = sessionOptions.initialImages
154
+ .filter((item) => typeof item === "string" && item.trim())
155
+ .map((filePath) => ({
156
+ kind: "image",
157
+ path: path.resolve(sessionOptions?.cwd || process.cwd(), filePath.trim()),
158
+ mimeType: inferMimeType(filePath.trim(), "image"),
159
+ }));
160
+ const combined = [...normalizeMediaInputs(legacy), ...media];
161
+ const seen = new Set();
162
+ return combined.filter((item) => {
163
+ const key = `${item.kind}:${item.path}`;
164
+ if (seen.has(key))
165
+ return false;
166
+ seen.add(key);
167
+ return true;
168
+ });
169
+ }
170
+ export function assertMediaCapabilities(media, backend, capabilities) {
171
+ for (const [index, item] of media.entries()) {
172
+ if (capabilities?.[item.kind] !== MEDIA_CAPABILITY_NATIVE) {
173
+ throw createMediaInputError(`${backend} does not support ${item.kind} input`, {
174
+ reason: "unsupported_media",
175
+ backend,
176
+ mediaKind: item.kind,
177
+ mediaIndex: index,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ export function fileMediaToDataUri(media) {
183
+ return `data:${media.mimeType};base64,${fs.readFileSync(media.path).toString("base64")}`;
184
+ }
185
+ export function fileMediaToBase64(media) {
186
+ return fs.readFileSync(media.path).toString("base64");
187
+ }
188
+ export function createMediaInputError(message, extras = {}) {
189
+ const error = new Error(message);
190
+ for (const [key, value] of Object.entries(extras)) {
191
+ error[key] = value;
192
+ }
193
+ return error;
194
+ }
195
+ export function defaultPromptForMedia(media) {
196
+ return "Analyze the attached images.";
197
+ }
@@ -109,6 +109,11 @@ export class ChatWebSession extends EventEmitter<[never]> {
109
109
  session_file_path: undefined;
110
110
  updated_at: string;
111
111
  } | null;
112
+ capabilities: {
113
+ media: Readonly<{
114
+ image: "unsupported";
115
+ }>;
116
+ };
112
117
  chatWebProvider: string;
113
118
  providerConversationId: any;
114
119
  providerUrl: string | undefined;
@@ -173,7 +178,8 @@ export class ChatWebSession extends EventEmitter<[never]> {
173
178
  * render "open in ChatGPT" / "open in AI Studio" links.
174
179
  */
175
180
  providerConversationUrl(): string | undefined;
176
- runTurn(promptText: any, { onProgress }?: {
181
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress }?: {
182
+ useInitialImages?: boolean | undefined;
177
183
  onProgress?: null | undefined;
178
184
  }): Promise<{
179
185
  text: string;
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { CHAT_WEB_SESSION_VARIANT } from "../built-in-backends.js";
3
+ import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
4
+ import { assertMediaCapabilities, resolveTurnMedia } from "../media-input.js";
5
+ import { normalizeContextFiles } from "../context-files.js";
3
6
  import { emitLog, normalizeLogger } from "../shared.js";
4
7
  const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini"]);
5
8
  const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
@@ -209,6 +212,7 @@ export class ChatWebSession extends EventEmitter {
209
212
  ? { ready: true, command: this.providerConversationUrl() }
210
213
  : null,
211
214
  currentTurnStatus: this.getCurrentTurnStatus(),
215
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
212
216
  chatWebProvider: this.chatWebProvider,
213
217
  providerConversationId: this.providerConversationId,
214
218
  providerUrl: this.providerConversationUrl(),
@@ -401,7 +405,14 @@ export class ChatWebSession extends EventEmitter {
401
405
  return undefined;
402
406
  }
403
407
  }
404
- async runTurn(promptText, { onProgress = null } = {}) {
408
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
409
+ if (normalizeContextFiles(contextFiles).length > 0) {
410
+ const error = new Error("chat-web does not support context file input");
411
+ error.reason = "unsupported_context_files";
412
+ throw error;
413
+ }
414
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
415
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT]);
405
416
  const prompt = String(promptText || "").trim();
406
417
  if (!prompt) {
407
418
  return {
@@ -1,10 +1,16 @@
1
1
  export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
2
2
  static capabilities: Readonly<{
3
3
  goal: true;
4
+ media: Readonly<{
5
+ image: "native";
6
+ }>;
4
7
  }>;
5
8
  constructor(backend: any, options?: {});
6
9
  getCapabilities(): {
7
10
  goal: true;
11
+ media: Readonly<{
12
+ image: "native";
13
+ }>;
8
14
  };
9
15
  backend: string;
10
16
  options: {};
@@ -68,6 +74,9 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
68
74
  currentTurnStatus: any;
69
75
  capabilities: {
70
76
  goal: true;
77
+ media: Readonly<{
78
+ image: "native";
79
+ }>;
71
80
  };
72
81
  };
73
82
  getSessionInfo(): {
@@ -131,7 +140,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
131
140
  onProgress: any;
132
141
  }): Promise<void>;
133
142
  interruptCurrentTurn(): Promise<boolean>;
134
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
143
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
135
144
  useInitialImages?: boolean | undefined;
136
145
  onProgress?: null | undefined;
137
146
  jsonSchema?: null | undefined;
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in-backends.js";
3
+ import { appendContextFilesToPrompt } from "../context-files.js";
4
+ import { PROVIDER_MEDIA_CAPABILITIES, buildClaudeContent } from "../media-adapters.js";
5
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
3
6
  import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
4
7
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
5
8
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -8,6 +11,16 @@ const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
8
11
  function waitForever() {
9
12
  return new Promise(() => { });
10
13
  }
14
+ async function* buildClaudeInput(promptText, media) {
15
+ yield {
16
+ type: "user",
17
+ message: {
18
+ role: "user",
19
+ content: buildClaudeContent(promptText, media),
20
+ },
21
+ parent_tool_use_id: null,
22
+ };
23
+ }
11
24
  function createTurnError(message, extras = {}) {
12
25
  const error = new Error(message);
13
26
  for (const [key, value] of Object.entries(extras)) {
@@ -129,7 +142,10 @@ export class ClaudeAgentSdkSession extends EventEmitter {
129
142
  // Capability advertised via getSnapshot().capabilities so worker proxies
130
143
  // can short-circuit runGoal without an IPC round trip. Claude exposes
131
144
  // native `/goal` slash command, so this is true.
132
- static capabilities = Object.freeze({ goal: true });
145
+ static capabilities = Object.freeze({
146
+ goal: true,
147
+ media: PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT],
148
+ });
133
149
  getCapabilities() {
134
150
  return { ...ClaudeAgentSdkSession.capabilities };
135
151
  }
@@ -722,11 +738,15 @@ export class ClaudeAgentSdkSession extends EventEmitter {
722
738
  }
723
739
  return true;
724
740
  }
725
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
741
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
726
742
  if (this.closeRequested) {
727
743
  throw this.createSessionClosedError();
728
744
  }
729
- const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
745
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
746
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT]);
747
+ let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
748
+ (media.length ? defaultPromptForMedia(media) : "");
749
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
730
750
  if (!effectivePrompt) {
731
751
  return {
732
752
  text: "",
@@ -776,7 +796,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
776
796
  status_line: "claude is working",
777
797
  }, onProgress);
778
798
  const query = sdkModule.query({
779
- prompt: effectivePrompt,
799
+ prompt: media.length ? buildClaudeInput(effectivePrompt, media) : effectivePrompt,
780
800
  options: this.buildSdkOptions(abortController),
781
801
  });
782
802
  currentTurn.query = query;
@@ -223,7 +223,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
223
223
  handleTransportExit(payload: any): void;
224
224
  maybeEmitAuthRequired(error: any): void;
225
225
  interruptCurrentTurn(): Promise<boolean>;
226
- runTurn(promptText: any, { useInitialImages, jsonSchema }?: {
226
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, jsonSchema }?: {
227
227
  useInitialImages?: boolean | undefined;
228
228
  jsonSchema?: null | undefined;
229
229
  }): Promise<{
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { CODEX_APP_SERVER_VARIANT } from "../built-in-backends.js";
3
+ import { appendContextFilesToPrompt } from "../context-files.js";
4
+ import { PROVIDER_MEDIA_CAPABILITIES, buildCodexAppServerInput } from "../media-adapters.js";
5
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
3
6
  import { CodexAppServerTransport } from "../transports/codex-app-server-transport.js";
4
7
  import { TERMINAL_GOAL_STATUSES, emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
5
8
  const TERMINAL_GOAL_STATUSES_LOCAL = new Set(TERMINAL_GOAL_STATUSES);
@@ -35,14 +38,6 @@ function extractSessionConfiguredValue(params, ...keys) {
35
38
  }
36
39
  return undefined;
37
40
  }
38
- function buildTurnInput(promptText) {
39
- return [
40
- {
41
- type: "text",
42
- text: promptText,
43
- },
44
- ];
45
- }
46
41
  function injectJsonSchemaPrompt(promptText, jsonSchema) {
47
42
  const schemaText = typeof jsonSchema === "string" ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
48
43
  return `You must respond with valid JSON that strictly conforms to the following JSON Schema. Do not include any markdown formatting or explanation outside the JSON object.
@@ -179,7 +174,10 @@ export class CodexAppServerSession extends EventEmitter {
179
174
  * Treat as read-only.
180
175
  * @type {Readonly<import("../shared.js").SessionCapabilities>}
181
176
  */
182
- static capabilities = Object.freeze({ goal: true });
177
+ static capabilities = Object.freeze({
178
+ goal: true,
179
+ media: PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT],
180
+ });
183
181
  /**
184
182
  * @returns {import("../shared.js").SessionCapabilities}
185
183
  */
@@ -1029,11 +1027,15 @@ export class CodexAppServerSession extends EventEmitter {
1029
1027
  }
1030
1028
  return false;
1031
1029
  }
1032
- async runTurn(promptText, { useInitialImages = false, jsonSchema = null } = {}) {
1030
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, jsonSchema = null } = {}) {
1033
1031
  if (this.closeRequested) {
1034
1032
  throw this.createSessionClosedError();
1035
1033
  }
1036
- let effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
1034
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
1035
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CODEX_APP_SERVER_VARIANT]);
1036
+ let effectivePrompt = this.buildPrompt(promptText, { useInitialImages: false }) ||
1037
+ (media.length ? defaultPromptForMedia(media) : "");
1038
+ effectivePrompt = appendContextFilesToPrompt(effectivePrompt, contextFiles).prompt;
1037
1039
  if (jsonSchema && typeof jsonSchema === "object" && effectivePrompt) {
1038
1040
  effectivePrompt = injectJsonSchemaPrompt(effectivePrompt, jsonSchema);
1039
1041
  }
@@ -1081,7 +1083,7 @@ export class CodexAppServerSession extends EventEmitter {
1081
1083
  threadId: this.sessionId,
1082
1084
  cwd: this.cwd,
1083
1085
  approvalPolicy: "never",
1084
- input: buildTurnInput(effectivePrompt),
1086
+ input: buildCodexAppServerInput(effectivePrompt, media),
1085
1087
  });
1086
1088
  const turnResult = await Promise.race([
1087
1089
  (async () => {
@@ -52,6 +52,11 @@ export class CodexExecSession extends EventEmitter<[never]> {
52
52
  resumeReady: boolean;
53
53
  manualResume: null;
54
54
  currentTurnStatus: any;
55
+ capabilities: {
56
+ media: Readonly<{
57
+ image: "native";
58
+ }>;
59
+ };
55
60
  pid: any;
56
61
  };
57
62
  getSessionInfo(): {
@@ -85,13 +90,13 @@ export class CodexExecSession extends EventEmitter<[never]> {
85
90
  emitWorkingStatus(payload: any, onProgress?: null): Promise<any>;
86
91
  emitAssistantMessage(text: any): Promise<void>;
87
92
  buildPrompt(promptText: any): string;
88
- buildExecArgs({ useInitialImages, schemaFilePath, lastMessageFilePath }?: {
89
- useInitialImages?: boolean | undefined;
93
+ buildExecArgs({ media, schemaFilePath, lastMessageFilePath }?: {
94
+ media?: never[] | undefined;
90
95
  schemaFilePath?: string | undefined;
91
96
  lastMessageFilePath?: string | undefined;
92
97
  }): string[];
93
98
  maybeEmitAuthRequired(stderrTail: any): void;
94
- runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
99
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
95
100
  useInitialImages?: boolean | undefined;
96
101
  onProgress?: null | undefined;
97
102
  jsonSchema?: null | undefined;
@@ -6,6 +6,9 @@ import { randomUUID } from "node:crypto";
6
6
  import { EventEmitter } from "node:events";
7
7
  import readline from "node:readline";
8
8
  import { CODEX_EXEC_VARIANT as CODEX_EXEC_PROVIDER_VARIANT } from "../built-in-backends.js";
9
+ import { appendContextFilesToPrompt } from "../context-files.js";
10
+ import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
11
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
9
12
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
10
13
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
11
14
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -239,6 +242,7 @@ export class CodexExecSession extends EventEmitter {
239
242
  resumeReady: false,
240
243
  manualResume: null,
241
244
  currentTurnStatus: this.getCurrentTurnStatus(),
245
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT] },
242
246
  pid: this.currentTurn?.child?.pid || undefined,
243
247
  };
244
248
  }
@@ -338,7 +342,7 @@ export class CodexExecSession extends EventEmitter {
338
342
  buildPrompt(promptText) {
339
343
  return buildHistoryPrompt(this.history, promptText);
340
344
  }
341
- buildExecArgs({ useInitialImages = false, schemaFilePath = "", lastMessageFilePath = "" } = {}) {
345
+ buildExecArgs({ media = [], schemaFilePath = "", lastMessageFilePath = "" } = {}) {
342
346
  const args = [...this.baseArgs];
343
347
  args.push("exec");
344
348
  args.push("--json");
@@ -354,11 +358,8 @@ export class CodexExecSession extends EventEmitter {
354
358
  if (typeof this.options.model === "string" && this.options.model.trim()) {
355
359
  args.push("--model", this.options.model.trim());
356
360
  }
357
- const images = useInitialImages && Array.isArray(this.options.initialImages)
358
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
359
- : [];
360
- for (const imagePath of images) {
361
- args.push("--image", imagePath);
361
+ for (const image of media.filter((item) => item.kind === "image")) {
362
+ args.push("--image", image.path);
362
363
  }
363
364
  return args;
364
365
  }
@@ -376,7 +377,7 @@ export class CodexExecSession extends EventEmitter {
376
377
  message: lastMessage || "Codex authentication required",
377
378
  });
378
379
  }
379
- async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
380
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
380
381
  if (this.closeRequested || this.closed) {
381
382
  throw this.createSessionClosedError();
382
383
  }
@@ -385,12 +386,11 @@ export class CodexExecSession extends EventEmitter {
385
386
  reason: "turn_already_running",
386
387
  });
387
388
  }
389
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
390
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[CODEX_EXEC_PROVIDER_VARIANT]);
388
391
  const effectivePrompt = this.buildPrompt(promptText);
389
- const imagePaths = useInitialImages && Array.isArray(this.options.initialImages)
390
- ? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
391
- : [];
392
- const stdinPrompt = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached image." : "");
393
- if (!stdinPrompt && imagePaths.length === 0) {
392
+ const stdinPrompt = appendContextFilesToPrompt(effectivePrompt || (media.length > 0 ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
393
+ if (!stdinPrompt && media.length === 0) {
394
394
  return buildEmptyTurnResult();
395
395
  }
396
396
  const normalizedSchema = normalizeJsonSchema(jsonSchema);
@@ -416,7 +416,7 @@ export class CodexExecSession extends EventEmitter {
416
416
  status_line: statusLineForPhase("turn_started"),
417
417
  }, onProgress);
418
418
  const args = this.buildExecArgs({
419
- useInitialImages,
419
+ media,
420
420
  schemaFilePath,
421
421
  lastMessageFilePath,
422
422
  });
@@ -83,6 +83,11 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
83
83
  command: string;
84
84
  } | null;
85
85
  currentTurnStatus: any;
86
+ capabilities: {
87
+ media: Readonly<{
88
+ image: "native";
89
+ }>;
90
+ };
86
91
  };
87
92
  getSessionInfo(): {
88
93
  backend: string;
@@ -165,7 +170,7 @@ export class CopilotSdkSession extends EventEmitter<[never]> {
165
170
  handleCopilotEventFailure(error: any): void;
166
171
  handleCopilotEvent(event: any): Promise<void>;
167
172
  interruptCurrentTurn(): Promise<void>;
168
- runTurn(promptText: any, { useInitialImages, onProgress }?: {
173
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress }?: {
169
174
  useInitialImages?: boolean | undefined;
170
175
  onProgress?: null | undefined;
171
176
  }): Promise<{
@@ -3,6 +3,9 @@ import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { COPILOT_SDK_VARIANT as COPILOT_PROVIDER_VARIANT } from "../built-in-backends.js";
6
+ import { appendContextFilesToPrompt } from "../context-files.js";
7
+ import { PROVIDER_MEDIA_CAPABILITIES, buildCopilotAttachments } from "../media-adapters.js";
8
+ import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
6
9
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
7
10
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
8
11
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
@@ -591,6 +594,7 @@ export class CopilotSdkSession extends EventEmitter {
591
594
  }
592
595
  : null,
593
596
  currentTurnStatus: this.getCurrentTurnStatus(),
597
+ capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT] },
594
598
  };
595
599
  }
596
600
  getSessionInfo() {
@@ -1368,12 +1372,14 @@ export class CopilotSdkSession extends EventEmitter {
1368
1372
  }
1369
1373
  }
1370
1374
  }
1371
- async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
1375
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null } = {}) {
1372
1376
  if (this.currentTurn) {
1373
1377
  throw this.createTurnAlreadyRunningError();
1374
1378
  }
1375
- const prompt = this.buildPrompt(promptText, { useInitialImages });
1376
- if (!prompt) {
1379
+ const media = resolveTurnMedia(this.options, { useInitialImages, media: mediaInput });
1380
+ assertMediaCapabilities(media, this.backend, PROVIDER_MEDIA_CAPABILITIES[COPILOT_PROVIDER_VARIANT]);
1381
+ const prompt = appendContextFilesToPrompt(this.buildPrompt(promptText, { useInitialImages: false }) || (media.length ? defaultPromptForMedia(media) : ""), contextFiles).prompt;
1382
+ if (!prompt && media.length === 0) {
1377
1383
  return {
1378
1384
  text: "",
1379
1385
  usage: this.lastUsage ? { ...this.lastUsage } : null,
@@ -1419,6 +1425,7 @@ export class CopilotSdkSession extends EventEmitter {
1419
1425
  const completion = await Promise.race([
1420
1426
  this.requestOrThrow(this.session.sendAndWait({
1421
1427
  prompt,
1428
+ attachments: buildCopilotAttachments(media),
1422
1429
  mode: "immediate",
1423
1430
  }, this.turnDeadlineMs + SDK_SEND_AND_WAIT_TIMEOUT_GRACE_MS)),
1424
1431
  closeGuard.promise,