@love-moon/ai-sdk 0.13.3 → 0.15.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 (41) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/client.d.ts +5 -0
  3. package/dist/client.js +22 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/providers/chat-web-session.d.ts +18 -0
  7. package/dist/providers/chat-web-session.js +44 -1
  8. package/dist/providers/claude-agent-sdk-session.d.ts +34 -0
  9. package/dist/providers/claude-agent-sdk-session.js +33 -0
  10. package/dist/providers/codex-app-server-session.d.ts +76 -6
  11. package/dist/providers/codex-app-server-session.js +114 -14
  12. package/dist/providers/codex-exec-session.d.ts +14 -0
  13. package/dist/providers/codex-exec-session.js +20 -1
  14. package/dist/providers/copilot-sdk-session.d.ts +21 -0
  15. package/dist/providers/copilot-sdk-session.js +55 -12
  16. package/dist/providers/dsh-sdk-session.d.ts +53 -3
  17. package/dist/providers/dsh-sdk-session.js +24 -1
  18. package/dist/providers/kimi-cli-session.d.ts +22 -0
  19. package/dist/providers/kimi-cli-session.js +23 -1
  20. package/dist/providers/kimi-print-session.d.ts +13 -0
  21. package/dist/providers/kimi-print-session.js +29 -0
  22. package/dist/providers/kimi-slash-commands.d.ts +1 -0
  23. package/dist/providers/kimi-slash-commands.js +23 -0
  24. package/dist/providers/opencode-sdk-session.d.ts +52 -3
  25. package/dist/providers/opencode-sdk-session.js +33 -1
  26. package/dist/resume/claude.d.ts +8 -0
  27. package/dist/resume/claude.js +28 -1
  28. package/dist/resume/codex.d.ts +8 -0
  29. package/dist/resume/codex.js +37 -1
  30. package/dist/resume/index.d.ts +8 -0
  31. package/dist/resume/shared.d.ts +18 -0
  32. package/dist/resume/shared.js +65 -1
  33. package/dist/shared.d.ts +46 -0
  34. package/dist/shared.js +18 -1
  35. package/dist/transports/codex-app-server-transport.js +22 -1
  36. package/dist/transports/kimi-wire-transport.d.ts +12 -0
  37. package/dist/transports/kimi-wire-transport.js +40 -1
  38. package/dist/turn-usage.d.ts +31 -0
  39. package/dist/turn-usage.js +44 -0
  40. package/dist/worker.js +1 -1
  41. package/package.json +3 -3
@@ -6,6 +6,7 @@ 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
8
  import { appendContextFilesToPrompt } from "../context-files.js";
9
+ import { assertKimiClearReply } from "./kimi-slash-commands.js";
9
10
  import { PROVIDER_MEDIA_CAPABILITIES, UNSUPPORTED_MEDIA_CAPABILITIES, buildKimiContent, } from "../media-adapters.js";
10
11
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
11
12
  import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, parseCommandParts, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
@@ -272,6 +273,7 @@ export class KimiPrintSession extends EventEmitter {
272
273
  capabilities: {
273
274
  // Kimi Code's `--prompt` CLI is unverified for slash commands.
274
275
  compact: this.cliMode !== "prompt",
276
+ clear: this.cliMode !== "prompt",
275
277
  media: this.cliMode === "prompt"
276
278
  ? UNSUPPORTED_MEDIA_CAPABILITIES
277
279
  : PROVIDER_MEDIA_CAPABILITIES[KIMI_PRINT_PROVIDER_VARIANT],
@@ -505,6 +507,33 @@ export class KimiPrintSession extends EventEmitter {
505
507
  metadata: turnResult.metadata,
506
508
  };
507
509
  }
510
+ /**
511
+ * Native `/clear`: kimi-cli's built-in slash command, answered locally without
512
+ * a model call. Print mode spawns a child per turn, so nothing is torn down;
513
+ * the `--session` id stays the same and only its context file rotates.
514
+ *
515
+ * @param {Record<string, unknown>} [request]
516
+ * @param {{ onProgress?: Function }} [options]
517
+ * @returns {Promise<import("../shared.js").ClearResult>}
518
+ */
519
+ async runClear(request = {}, { onProgress = null } = {}) {
520
+ if (this.cliMode === "prompt") {
521
+ throw createTurnError("Kimi prompt mode does not support /clear", { reason: "unsupported_clear" });
522
+ }
523
+ if (this.pendingHistorySeed) {
524
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
525
+ }
526
+ const turnResult = await this.runTurn("/clear", { onProgress, suppressReply: true });
527
+ // Print mode exits 0 even when the model call fails, so only kimi-cli's
528
+ // fixed reply is trusted.
529
+ assertKimiClearReply(turnResult.text);
530
+ this.history = [];
531
+ return {
532
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
533
+ usage: turnResult.usage,
534
+ metadata: turnResult.metadata,
535
+ };
536
+ }
508
537
  async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
509
538
  if (this.closeRequested || this.closed) {
510
539
  throw this.createSessionClosedError();
@@ -0,0 +1 @@
1
+ export function assertKimiClearReply(reply: any): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * kimi-cli answers `/clear` itself, without calling the model, with a fixed
3
+ * line (see kimi_cli/soul/slash.py). Both kimi modes verify that line before
4
+ * reporting success: if a kimi version ever stops treating `/clear` as a
5
+ * built-in, the text reaches the model as an ordinary turn — which grows the
6
+ * context instead of clearing it — and we must fail loudly rather than tell the
7
+ * user their context is gone.
8
+ *
9
+ * Kept in one place so the two modes cannot drift apart, in either direction.
10
+ */
11
+ const KIMI_CLEAR_ACK = /context has been cleared/i;
12
+ function createClearFailedError(message) {
13
+ const error = new Error(message);
14
+ error.reason = "clear_failed";
15
+ return error;
16
+ }
17
+ export function assertKimiClearReply(reply) {
18
+ const text = String(reply || "");
19
+ if (KIMI_CLEAR_ACK.test(text)) {
20
+ return;
21
+ }
22
+ throw createClearFailedError(`Kimi clear failed${text.trim() ? `: ${text.trim()}` : ""}`);
23
+ }
@@ -80,6 +80,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
80
80
  currentTurnStatus: any;
81
81
  capabilities: {
82
82
  compact: boolean;
83
+ clear: boolean;
83
84
  media: Readonly<{
84
85
  image: "native";
85
86
  }>;
@@ -208,9 +209,57 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
208
209
  * @param {{ onProgress?: Function }} [options]
209
210
  * @returns {Promise<import("../shared.js").CompactResult>}
210
211
  */
211
- runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
212
- onProgress?: Function;
213
- }): Promise<import("../shared.js").CompactResult>;
212
+ /**
213
+ * Native clear: create a new session on the SAME opencode server (the server
214
+ * process and its event stream stay up); the old session is left untouched on
215
+ * disk, exactly like the boot path's `session.create`.
216
+ */
217
+ runClear(): Promise<{
218
+ clear: {
219
+ status: string;
220
+ sessionId?: undefined;
221
+ };
222
+ usage: null;
223
+ metadata: {};
224
+ } | {
225
+ clear: {
226
+ status: string;
227
+ sessionId: any;
228
+ };
229
+ usage: null;
230
+ metadata: {};
231
+ }>;
232
+ runCompact(request?: {}, { onProgress }?: {
233
+ onProgress?: null | undefined;
234
+ }): Promise<{
235
+ compact: {
236
+ status: string;
237
+ instructionsApplied: boolean;
238
+ };
239
+ usage: null;
240
+ metadata: {};
241
+ } | {
242
+ compact: {
243
+ status: string;
244
+ instructionsApplied: boolean;
245
+ };
246
+ usage: {
247
+ total: number | undefined;
248
+ input: number | undefined;
249
+ output: number | undefined;
250
+ reasoning: number | undefined;
251
+ cache: {
252
+ read: number | undefined;
253
+ write: number | undefined;
254
+ };
255
+ } | null;
256
+ metadata: {
257
+ providerID: any;
258
+ modelID: any;
259
+ source: string;
260
+ sessionId: any;
261
+ };
262
+ }>;
214
263
  runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
215
264
  useInitialImages?: boolean | undefined;
216
265
  onProgress?: null | undefined;
@@ -231,7 +231,7 @@ export class OpencodeSdkSession extends EventEmitter {
231
231
  resumeReady: Boolean(this.sessionId),
232
232
  manualResume: null,
233
233
  currentTurnStatus: this.getCurrentTurnStatus(),
234
- capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
234
+ capabilities: { compact: true, clear: true, media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
235
235
  pid: this.transport.pid || undefined,
236
236
  };
237
237
  }
@@ -1227,6 +1227,38 @@ export class OpencodeSdkSession extends EventEmitter {
1227
1227
  * @param {{ onProgress?: Function }} [options]
1228
1228
  * @returns {Promise<import("../shared.js").CompactResult>}
1229
1229
  */
1230
+ /**
1231
+ * Native clear: create a new session on the SAME opencode server (the server
1232
+ * process and its event stream stay up); the old session is left untouched on
1233
+ * disk, exactly like the boot path's `session.create`.
1234
+ */
1235
+ async runClear() {
1236
+ if (this.closeRequested) {
1237
+ throw this.createSessionClosedError();
1238
+ }
1239
+ if (this.currentTurn) {
1240
+ throw createTurnError("Opencode turn already running", {
1241
+ reason: "turn_already_running",
1242
+ });
1243
+ }
1244
+ if (this.pendingHistorySeed || (!this.resumeSessionId && !this.sessionId && this.history.length === 0)) {
1245
+ return { clear: { status: "noop" }, usage: null, metadata: {} };
1246
+ }
1247
+ await this.boot();
1248
+ if (typeof this.client?.session?.create !== "function") {
1249
+ throw new Error("Opencode session create API is unavailable");
1250
+ }
1251
+ const session = await this.requestOrThrow(this.client.session.create({}, { throwOnError: true, responseStyle: "data" }));
1252
+ this.applySessionInfo(session);
1253
+ // A later re-boot must not reattach to the session we walked away from.
1254
+ this.resumeSessionId = "";
1255
+ this.history = [];
1256
+ return {
1257
+ clear: { status: "cleared", sessionId: this.sessionId || undefined },
1258
+ usage: null,
1259
+ metadata: {},
1260
+ };
1261
+ }
1230
1262
  async runCompact(request = {}, { onProgress = null } = {}) {
1231
1263
  if (this.closeRequested) {
1232
1264
  throw this.createSessionClosedError();
@@ -24,5 +24,13 @@ export function listSessions(options?: {}): Promise<{
24
24
  cwd: any;
25
25
  title: string | null;
26
26
  updatedAt: any;
27
+ preview: {
28
+ firstUserMessage: string | null;
29
+ firstReply: string | null;
30
+ lastMessage: {
31
+ role: any;
32
+ text: string | null;
33
+ } | null;
34
+ };
27
35
  }[]>;
28
36
  export const BACKEND: "claude";
@@ -3,7 +3,7 @@ import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { resolveClaudeConfigDirs } from "../manager/paths.js";
6
- import { buildResumeContext, isExistingDirectory, normalizeSessionId, normalizeSessionTitle, pathExists, readJsonlHeadEntries, resolveSessionRunDirectory, } from "./shared.js";
6
+ import { buildResumeContext, buildSessionPreview, isExistingDirectory, normalizeSessionId, normalizeSessionTitle, pathExists, readJsonlHeadEntries, readJsonlTailEntries, resolveSessionRunDirectory, } from "./shared.js";
7
7
  export const BACKEND = "claude";
8
8
  export function buildCliArgs(sessionId) {
9
9
  const normalizedSessionId = normalizeSessionId(sessionId);
@@ -156,12 +156,14 @@ export async function listSessions(options = {}) {
156
156
  const sessions = [];
157
157
  for (const candidate of selected) {
158
158
  const entries = await readJsonlHeadEntries(candidate.sessionFilePath);
159
+ const tailEntries = await readJsonlTailEntries(candidate.sessionFilePath);
159
160
  sessions.push({
160
161
  sessionId: candidate.sessionId,
161
162
  sessionFilePath: candidate.sessionFilePath,
162
163
  cwd: extractHeadCwd(entries),
163
164
  title: extractClaudeTitle(entries),
164
165
  updatedAt: candidate.updatedAt,
166
+ preview: buildSessionPreview(extractClaudeMessages(entries), extractClaudeMessages(tailEntries)),
165
167
  });
166
168
  }
167
169
  return sessions;
@@ -174,6 +176,31 @@ function extractHeadCwd(entries) {
174
176
  }
175
177
  return null;
176
178
  }
179
+ /**
180
+ * User/assistant text messages. Tool calls, tool results, meta entries and
181
+ * wrapped slash-command output (<command-name>, <local-command-stdout>) are skipped.
182
+ */
183
+ function extractClaudeMessages(entries) {
184
+ const messages = [];
185
+ for (const entry of entries) {
186
+ if ((entry?.type !== "user" && entry?.type !== "assistant") || entry?.isMeta === true) {
187
+ continue;
188
+ }
189
+ const content = entry?.message?.content;
190
+ const text = typeof content === "string"
191
+ ? content
192
+ : Array.isArray(content)
193
+ ? content
194
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
195
+ .map((item) => item.text)
196
+ .join("\n")
197
+ : "";
198
+ if (text.trim() && !text.trim().startsWith("<")) {
199
+ messages.push({ role: entry.type, text });
200
+ }
201
+ }
202
+ return messages;
203
+ }
177
204
  function extractClaudeTitle(entries) {
178
205
  for (const entry of entries) {
179
206
  if (entry?.type !== "user" || entry?.isMeta === true) {
@@ -24,5 +24,13 @@ export function listSessions(options?: {}): Promise<{
24
24
  cwd: string | null;
25
25
  title: string | null;
26
26
  updatedAt: number;
27
+ preview: {
28
+ firstUserMessage: string | null;
29
+ firstReply: string | null;
30
+ lastMessage: {
31
+ role: any;
32
+ text: string | null;
33
+ } | null;
34
+ };
27
35
  }[]>;
28
36
  export const BACKEND: "codex";
@@ -1,6 +1,6 @@
1
1
  import { promises as fsp } from "node:fs";
2
2
  import path from "node:path";
3
- import { buildResumeContext, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, normalizeSessionTitle, readJsonlHeadEntries, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
3
+ import { buildResumeContext, buildSessionPreview, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, normalizeSessionTitle, readJsonlHeadEntries, readJsonlTailEntries, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
4
4
  export const BACKEND = "codex";
5
5
  export function buildCliArgs(sessionId) {
6
6
  const normalizedSessionId = normalizeSessionId(sessionId);
@@ -111,12 +111,14 @@ export async function listSessions(options = {}) {
111
111
  continue;
112
112
  }
113
113
  const metaCwd = meta?.payload?.cwd;
114
+ const tailEntries = await readJsonlTailEntries(candidate.filePath);
114
115
  sessions.push({
115
116
  sessionId,
116
117
  sessionFilePath: candidate.filePath,
117
118
  cwd: typeof metaCwd === "string" && metaCwd.trim() ? metaCwd.trim() : null,
118
119
  title: extractCodexTitle(headEntries),
119
120
  updatedAt: candidate.updatedAt,
121
+ preview: buildSessionPreview(extractCodexMessages(headEntries), extractCodexMessages(tailEntries)),
120
122
  });
121
123
  }
122
124
  return sessions;
@@ -126,6 +128,40 @@ function extractSessionIdFromFileName(filePath) {
126
128
  const match = path.basename(filePath).match(SESSION_ID_IN_FILE_NAME);
127
129
  return match ? match[0] : "";
128
130
  }
131
+ // The event stream's user/agent messages: what the user typed and the
132
+ // assistant's replies, without injected context or tool output.
133
+ const CODEX_MESSAGE_ROLES = { user_message: "user", agent_message: "assistant" };
134
+ const CODEX_ITEM_TEXT_TYPES = { user: "input_text", assistant: "output_text" };
135
+ function extractCodexMessages(entries) {
136
+ const messages = [];
137
+ for (const entry of entries) {
138
+ const role = entry?.type === "event_msg" ? CODEX_MESSAGE_ROLES[entry?.payload?.type] : null;
139
+ const text = entry?.payload?.message;
140
+ if (role && typeof text === "string" && text.trim()) {
141
+ messages.push({ role, text });
142
+ }
143
+ }
144
+ if (messages.length > 0) {
145
+ return messages;
146
+ }
147
+ // Sessions without those events only carry response items; skip injected
148
+ // wrappers such as <environment_context>, as the title does.
149
+ for (const entry of entries) {
150
+ const payload = entry?.type === "response_item" ? entry.payload : null;
151
+ const textType = payload?.type === "message" ? CODEX_ITEM_TEXT_TYPES[payload.role] : null;
152
+ if (!textType || !Array.isArray(payload.content)) {
153
+ continue;
154
+ }
155
+ const text = payload.content
156
+ .filter((item) => item?.type === textType && typeof item.text === "string")
157
+ .map((item) => item.text)
158
+ .join("\n");
159
+ if (text.trim() && !text.trim().startsWith("<")) {
160
+ messages.push({ role: payload.role, text });
161
+ }
162
+ }
163
+ return messages;
164
+ }
129
165
  function extractCodexTitle(entries) {
130
166
  for (const entry of entries) {
131
167
  if (entry?.type === "event_msg" && entry?.payload?.type === "user_message") {
@@ -11,6 +11,14 @@ export function listSessionsForBackend(backendType: any, opts?: {}): Promise<{
11
11
  cwd: string | null;
12
12
  title: string | null;
13
13
  updatedAt: number;
14
+ preview: {
15
+ firstUserMessage: string | null;
16
+ firstReply: string | null;
17
+ lastMessage: {
18
+ role: any;
19
+ text: string | null;
20
+ } | null;
21
+ };
14
22
  }[]>;
15
23
  export function resolveResumeContext(backend: any, sessionId: any, options?: {}): Promise<{
16
24
  provider: any;
@@ -11,6 +11,24 @@ export function normalizeSessionTitle(text: any, maxLength?: number): string | n
11
11
  * are skipped.
12
12
  */
13
13
  export function readJsonlHeadEntries(filePath: any, maxLines?: number, maxBytes?: number): Promise<any[]>;
14
+ /**
15
+ * Parses the tail of a jsonl file (last `maxBytes`); the first, possibly
16
+ * partial, line is dropped unless the read started at the file's beginning.
17
+ */
18
+ export function readJsonlTailEntries(filePath: any, maxBytes?: number): Promise<any[]>;
19
+ /**
20
+ * Session preview for the resume picker, from `{ role, text }` messages read
21
+ * off the head and tail of a session file: the first user message, the
22
+ * assistant text that answered it, and the session's last message.
23
+ */
24
+ export function buildSessionPreview(headMessages: any, tailMessages: any): {
25
+ firstUserMessage: string | null;
26
+ firstReply: string | null;
27
+ lastMessage: {
28
+ role: any;
29
+ text: string | null;
30
+ } | null;
31
+ };
14
32
  export function isExistingDirectory(targetPath: any): Promise<boolean>;
15
33
  export function resolveSessionRunDirectory(sessionPath: any): Promise<string>;
16
34
  export function listCandidateWorkingDirectories(options?: {}): any[];
@@ -46,7 +46,7 @@ export function normalizeSessionTitle(text, maxLength = 80) {
46
46
  * session files stay cheap to inspect). Unreadable files and unparsable lines
47
47
  * are skipped.
48
48
  */
49
- export async function readJsonlHeadEntries(filePath, maxLines = 50, maxBytes = 128 * 1024) {
49
+ export async function readJsonlHeadEntries(filePath, maxLines = 200, maxBytes = 128 * 1024) {
50
50
  const entries = [];
51
51
  const input = fs.createReadStream(filePath, { end: maxBytes - 1 });
52
52
  const rl = readline.createInterface({ input, crlfDelay: Infinity });
@@ -78,6 +78,70 @@ export async function readJsonlHeadEntries(filePath, maxLines = 50, maxBytes = 1
78
78
  }
79
79
  return entries;
80
80
  }
81
+ /**
82
+ * Parses the tail of a jsonl file (last `maxBytes`); the first, possibly
83
+ * partial, line is dropped unless the read started at the file's beginning.
84
+ */
85
+ export async function readJsonlTailEntries(filePath, maxBytes = 64 * 1024) {
86
+ let handle;
87
+ try {
88
+ handle = await fsp.open(filePath, "r");
89
+ const { size } = await handle.stat();
90
+ const start = Math.max(0, size - maxBytes);
91
+ const buffer = Buffer.alloc(size - start);
92
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
93
+ const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
94
+ if (start > 0) {
95
+ lines.shift();
96
+ }
97
+ const entries = [];
98
+ for (const line of lines) {
99
+ try {
100
+ if (line.trim())
101
+ entries.push(JSON.parse(line));
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ }
107
+ return entries;
108
+ }
109
+ catch {
110
+ return [];
111
+ }
112
+ finally {
113
+ await handle?.close();
114
+ }
115
+ }
116
+ function clipPreviewText(text, maxLength = 2000) {
117
+ const trimmed = String(text || "").trim();
118
+ if (!trimmed) {
119
+ return null;
120
+ }
121
+ return trimmed.length > maxLength ? `${trimmed.slice(0, maxLength)}…` : trimmed;
122
+ }
123
+ /**
124
+ * Session preview for the resume picker, from `{ role, text }` messages read
125
+ * off the head and tail of a session file: the first user message, the
126
+ * assistant text that answered it, and the session's last message.
127
+ */
128
+ export function buildSessionPreview(headMessages, tailMessages) {
129
+ const firstUserIndex = headMessages.findIndex((message) => message.role === "user");
130
+ const replies = [];
131
+ if (firstUserIndex >= 0) {
132
+ for (const message of headMessages.slice(firstUserIndex + 1)) {
133
+ if (message.role === "user")
134
+ break;
135
+ replies.push(message.text);
136
+ }
137
+ }
138
+ const last = tailMessages[tailMessages.length - 1] || headMessages[headMessages.length - 1] || null;
139
+ return {
140
+ firstUserMessage: firstUserIndex >= 0 ? clipPreviewText(headMessages[firstUserIndex].text) : null,
141
+ firstReply: clipPreviewText(replies.join("\n\n")),
142
+ lastMessage: last ? { role: last.role, text: clipPreviewText(last.text) } : null,
143
+ };
144
+ }
81
145
  export async function isExistingDirectory(targetPath) {
82
146
  const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
83
147
  if (!normalizedPath) {
package/dist/shared.d.ts CHANGED
@@ -100,6 +100,7 @@ export const TERMINAL_GOAL_STATUSES: ReadonlyArray<GoalStatus>;
100
100
  * @typedef {Object} SessionCapabilities
101
101
  * @property {boolean} [goal]
102
102
  * @property {boolean} [compact]
103
+ * @property {boolean} [clear]
103
104
  */
104
105
  /**
105
106
  * Manual context compaction (`/compact`) contract for providers that
@@ -124,6 +125,22 @@ export const TERMINAL_GOAL_STATUSES: ReadonlyArray<GoalStatus>;
124
125
  * @property {unknown} [usage]
125
126
  * @property {Record<string, unknown>} [metadata]
126
127
  */
128
+ /**
129
+ * Native context clear (`/clear`) contract for providers that implement the
130
+ * optional `runClear` method and advertise `capabilities.clear === true`.
131
+ * Providers must NOT emit assistant messages while clearing; the caller
132
+ * renders the user-facing confirmation.
133
+ *
134
+ * @typedef {Object} ClearState
135
+ * @property {"cleared"|"noop"} status `noop` = there was nothing to clear.
136
+ * @property {string} [sessionId] New session id when the backend starts a new
137
+ * conversation; callers rebind the task to it.
138
+ *
139
+ * @typedef {Object} ClearResult
140
+ * @property {ClearState} clear
141
+ * @property {unknown} [usage]
142
+ * @property {Record<string, unknown>} [metadata]
143
+ */
127
144
  /**
128
145
  * Default capabilities assigned to providers that do not opt into any optional
129
146
  * feature. Treat the returned object as read-only; clone before mutating.
@@ -212,6 +229,7 @@ export type GoalResult = {
212
229
  export type SessionCapabilities = {
213
230
  goal?: boolean | undefined;
214
231
  compact?: boolean | undefined;
232
+ clear?: boolean | undefined;
215
233
  };
216
234
  /**
217
235
  * Manual context compaction (`/compact`) contract for providers that
@@ -254,3 +272,31 @@ export type CompactResult = {
254
272
  usage?: unknown;
255
273
  metadata?: Record<string, unknown> | undefined;
256
274
  };
275
+ /**
276
+ * Native context clear (`/clear`) contract for providers that implement the
277
+ * optional `runClear` method and advertise `capabilities.clear === true`.
278
+ * Providers must NOT emit assistant messages while clearing; the caller
279
+ * renders the user-facing confirmation.
280
+ */
281
+ export type ClearState = {
282
+ /**
283
+ * `noop` = there was nothing to clear.
284
+ */
285
+ status: "cleared" | "noop";
286
+ /**
287
+ * New session id when the backend starts a new
288
+ * conversation; callers rebind the task to it.
289
+ */
290
+ sessionId?: string | undefined;
291
+ };
292
+ /**
293
+ * Native context clear (`/clear`) contract for providers that implement the
294
+ * optional `runClear` method and advertise `capabilities.clear === true`.
295
+ * Providers must NOT emit assistant messages while clearing; the caller
296
+ * renders the user-facing confirmation.
297
+ */
298
+ export type ClearResult = {
299
+ clear: ClearState;
300
+ usage?: unknown;
301
+ metadata?: Record<string, unknown> | undefined;
302
+ };
package/dist/shared.js CHANGED
@@ -81,6 +81,7 @@ export function isTerminalGoalStatus(value) {
81
81
  * @typedef {Object} SessionCapabilities
82
82
  * @property {boolean} [goal]
83
83
  * @property {boolean} [compact]
84
+ * @property {boolean} [clear]
84
85
  */
85
86
  /**
86
87
  * Manual context compaction (`/compact`) contract for providers that
@@ -105,13 +106,29 @@ export function isTerminalGoalStatus(value) {
105
106
  * @property {unknown} [usage]
106
107
  * @property {Record<string, unknown>} [metadata]
107
108
  */
109
+ /**
110
+ * Native context clear (`/clear`) contract for providers that implement the
111
+ * optional `runClear` method and advertise `capabilities.clear === true`.
112
+ * Providers must NOT emit assistant messages while clearing; the caller
113
+ * renders the user-facing confirmation.
114
+ *
115
+ * @typedef {Object} ClearState
116
+ * @property {"cleared"|"noop"} status `noop` = there was nothing to clear.
117
+ * @property {string} [sessionId] New session id when the backend starts a new
118
+ * conversation; callers rebind the task to it.
119
+ *
120
+ * @typedef {Object} ClearResult
121
+ * @property {ClearState} clear
122
+ * @property {unknown} [usage]
123
+ * @property {Record<string, unknown>} [metadata]
124
+ */
108
125
  /**
109
126
  * Default capabilities assigned to providers that do not opt into any optional
110
127
  * feature. Treat the returned object as read-only; clone before mutating.
111
128
  *
112
129
  * @type {Readonly<SessionCapabilities>}
113
130
  */
114
- export const DEFAULT_SESSION_CAPABILITIES = Object.freeze({ goal: false, compact: false });
131
+ export const DEFAULT_SESSION_CAPABILITIES = Object.freeze({ goal: false, compact: false, clear: false });
115
132
  /**
116
133
  * Resolve a session's capability snapshot, falling back to
117
134
  * {@link DEFAULT_SESSION_CAPABILITIES}.
@@ -46,6 +46,27 @@ function injectEnableGoalsArgs(args, enableGoals) {
46
46
  normalized.splice(insertAt, 0, "--enable", "goals");
47
47
  return normalized;
48
48
  }
49
+ /**
50
+ * Add one `-c key=value` per config override (e.g. the per-task MCP server a
51
+ * remote-worktree task needs), before `--listen` like `--enable goals`.
52
+ *
53
+ * @param {string[]} args
54
+ * @param {unknown} overrides
55
+ * @returns {string[]}
56
+ */
57
+ function injectConfigOverrideArgs(args, overrides) {
58
+ const pairs = Array.isArray(overrides)
59
+ ? overrides.filter((entry) => typeof entry === "string" && entry.includes("="))
60
+ : [];
61
+ if (pairs.length === 0) {
62
+ return args;
63
+ }
64
+ const normalized = [...args];
65
+ const listenIndex = normalized.indexOf("--listen");
66
+ const insertAt = listenIndex >= 0 ? listenIndex : normalized.length;
67
+ normalized.splice(insertAt, 0, ...pairs.flatMap((pair) => ["-c", pair]));
68
+ return normalized;
69
+ }
49
70
  function createRpcError(payload) {
50
71
  const message = String(payload?.message || payload?.error?.message || "Codex app-server request failed");
51
72
  const error = new Error(message);
@@ -78,7 +99,7 @@ export class CodexAppServerTransport extends EventEmitter {
78
99
  }
79
100
  this.command = command;
80
101
  this.enableGoals = options.enableGoals === true;
81
- this.args = injectEnableGoalsArgs(args, this.enableGoals);
102
+ this.args = injectConfigOverrideArgs(injectEnableGoalsArgs(args, this.enableGoals), options.configOverrides);
82
103
  this.env = options.env && typeof options.env === "object" ? { ...options.env } : {};
83
104
  this.ignoreCodexApiKey = options.ignoreCodexApiKey === true;
84
105
  this.child = null;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Drop resume flags from the configured command line. The session id is owned
3
+ * by the caller (we always append our own `--session=`), so a configured
4
+ * `--continue`/`--session`/`--resume` would silently reattach a fresh session —
5
+ * e.g. a `/clear` — to the previous conversation.
6
+ *
7
+ * Only those flags are touched. Notably `-c` is kimi's alias for
8
+ * `--prompt`/`--command` (it takes a value), not a continue switch, so it is
9
+ * passed through untouched — dropping the flag alone would leave its value
10
+ * behind as a stray positional argument.
11
+ */
12
+ export function filterKimiWireBaseArgs(args: any): string[];
1
13
  export class KimiWireTransport extends EventEmitter<[never]> {
2
14
  constructor(options?: {});
3
15
  options: {};