@love-moon/ai-sdk 0.2.41 → 0.3.1
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/CHANGELOG.md +19 -0
- package/dist/built-in-backends.d.ts +51 -0
- package/dist/built-in-backends.js +78 -0
- package/dist/external-provider-registry.js +15 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/claude-agent-sdk-session.js +1 -1
- package/dist/providers/codex-app-server-session.js +6 -5
- package/dist/providers/codex-exec-session.js +1 -1
- package/dist/providers/copilot-sdk-session.js +1 -1
- package/dist/providers/kimi-cli-session.js +1 -1
- package/dist/providers/kimi-print-session.js +1 -1
- package/dist/providers/opencode-sdk-session.js +1 -1
- package/dist/resume/claude.d.ts +14 -0
- package/dist/resume/claude.js +138 -0
- package/dist/resume/codex.d.ts +14 -0
- package/dist/resume/codex.js +81 -0
- package/dist/resume/copilot.d.ts +14 -0
- package/dist/resume/copilot.js +375 -0
- package/dist/resume/index.d.ts +26 -0
- package/dist/resume/index.js +132 -0
- package/dist/resume/kimi.d.ts +14 -0
- package/dist/resume/kimi.js +89 -0
- package/dist/resume/opencode.d.ts +13 -0
- package/dist/resume/opencode.js +63 -0
- package/dist/resume/shared.d.ts +26 -0
- package/dist/resume/shared.js +115 -0
- package/dist/session-factory.d.ts +2 -1
- package/dist/session-factory.js +40 -62
- package/package.json +10 -4
- package/dist/resume.d.ts +0 -26
- package/dist/resume.js +0 -380
- package/dist/tui-session.d.ts +0 -153
- package/dist/tui-session.js +0 -941
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadAllowCliList, loadEnvConfig, parseCommandParts, proxyToEnv, withoutCopilotGithubTokenEnv, } from "../shared.js";
|
|
4
|
+
import { buildResumeContext, isExistingDirectory, normalizeProjectPathCandidate, normalizeSessionId, } from "./shared.js";
|
|
5
|
+
export const BACKEND = "copilot";
|
|
6
|
+
const LEGACY_COPILOT_CLI_ARGS = new Set(["--allow-all-paths", "--allow-all-tools"]);
|
|
7
|
+
const DEFAULT_COPILOT_RESUME_TIMEOUT_MS = 20_000;
|
|
8
|
+
const DEFAULT_COPILOT_RESUME_STOP_TIMEOUT_MS = 5_000;
|
|
9
|
+
const COPILOT_GITHUB_TOKEN_ENV_KEYS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
10
|
+
export function buildCliArgs(sessionId) {
|
|
11
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
12
|
+
if (!normalizedSessionId) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return [`--resume=${normalizedSessionId}`];
|
|
16
|
+
}
|
|
17
|
+
export async function findSessionPath() {
|
|
18
|
+
// Copilot sessions are managed through the GitHub Copilot SDK; they do not
|
|
19
|
+
// have a local session file path that we can enumerate directly.
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export async function resolveResumeContext(sessionId, options = {}) {
|
|
23
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
24
|
+
if (!normalizedSessionId) {
|
|
25
|
+
throw new Error("--resume requires a session id");
|
|
26
|
+
}
|
|
27
|
+
const sessionMetadata = await withCopilotClient(options, async (client) => {
|
|
28
|
+
const sessions = await client.listSessions();
|
|
29
|
+
return sessions.find((entry) => normalizeSessionId(entry?.sessionId) === normalizedSessionId) || null;
|
|
30
|
+
});
|
|
31
|
+
if (!sessionMetadata) {
|
|
32
|
+
throw new Error(`Invalid --resume session id for copilot: ${normalizedSessionId}`);
|
|
33
|
+
}
|
|
34
|
+
const cwd = normalizeProjectPathCandidate(sessionMetadata?.context?.cwd);
|
|
35
|
+
if (!cwd) {
|
|
36
|
+
throw new Error(`Could not resolve workspace for copilot session ${normalizedSessionId}`);
|
|
37
|
+
}
|
|
38
|
+
if (!(await isExistingDirectory(cwd))) {
|
|
39
|
+
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
40
|
+
}
|
|
41
|
+
return buildResumeContext({
|
|
42
|
+
provider: "copilot",
|
|
43
|
+
sessionId: normalizedSessionId,
|
|
44
|
+
sessionPath: null,
|
|
45
|
+
cwd,
|
|
46
|
+
cwdSource: "sdk_list_sessions",
|
|
47
|
+
extraDebug: {
|
|
48
|
+
context: sessionMetadata?.context && typeof sessionMetadata.context === "object"
|
|
49
|
+
? { ...sessionMetadata.context }
|
|
50
|
+
: undefined,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function getCopilotSdkModule(options = {}) {
|
|
55
|
+
if (options.copilotSdkModule && typeof options.copilotSdkModule === "object") {
|
|
56
|
+
return options.copilotSdkModule;
|
|
57
|
+
}
|
|
58
|
+
return import("@github/copilot-sdk");
|
|
59
|
+
}
|
|
60
|
+
async function withCopilotClient(options, fn) {
|
|
61
|
+
const sdkModule = await getCopilotSdkModule(options);
|
|
62
|
+
if (!sdkModule || typeof sdkModule.CopilotClient !== "function") {
|
|
63
|
+
throw new Error("GitHub Copilot SDK client is unavailable");
|
|
64
|
+
}
|
|
65
|
+
const timeoutMs = resolvePositiveTimeoutMs(options.copilotResumeTimeoutMs ?? options.timeoutMs, DEFAULT_COPILOT_RESUME_TIMEOUT_MS);
|
|
66
|
+
const startedAtMs = Date.now();
|
|
67
|
+
const client = new sdkModule.CopilotClient(await buildCopilotClientOptions(options));
|
|
68
|
+
try {
|
|
69
|
+
if (typeof client.start === "function") {
|
|
70
|
+
const startTimeoutMs = remainingTimeoutMs(startedAtMs, timeoutMs, "copilot resume lookup timed out");
|
|
71
|
+
await withTimeout(client.start(), startTimeoutMs, "copilot resume SDK start timed out");
|
|
72
|
+
}
|
|
73
|
+
const lookupTimeoutMs = remainingTimeoutMs(startedAtMs, timeoutMs, "copilot resume lookup timed out");
|
|
74
|
+
return await withTimeout(fn(client), lookupTimeoutMs, "copilot resume lookup timed out");
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
try {
|
|
78
|
+
if (typeof client.stop === "function") {
|
|
79
|
+
const stopTimeoutMs = resolvePositiveTimeoutMs(options.copilotResumeStopTimeoutMs, DEFAULT_COPILOT_RESUME_STOP_TIMEOUT_MS);
|
|
80
|
+
await withTimeout(client.stop(), stopTimeoutMs, "copilot resume SDK stop timed out");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
try {
|
|
85
|
+
await client.forceStop?.();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// best effort
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function buildCopilotClientOptions(options = {}) {
|
|
94
|
+
const clientOptions = options.copilotClientOptions && typeof options.copilotClientOptions === "object"
|
|
95
|
+
? { ...options.copilotClientOptions }
|
|
96
|
+
: {};
|
|
97
|
+
const configFilePath = options.configFilePath || options.configFile;
|
|
98
|
+
const allowCliList = options.allowCliList && typeof options.allowCliList === "object"
|
|
99
|
+
? options.allowCliList
|
|
100
|
+
: loadAllowCliList(configFilePath);
|
|
101
|
+
const configEnv = buildConfiguredEnvMap(configFilePath);
|
|
102
|
+
const commandLine = resolveCopilotCommandLine({ ...options, allowCliList });
|
|
103
|
+
const cliLaunch = resolveCopilotCliLaunch(commandLine, {
|
|
104
|
+
...process.env,
|
|
105
|
+
...configEnv,
|
|
106
|
+
...options.env,
|
|
107
|
+
});
|
|
108
|
+
if (cliLaunch &&
|
|
109
|
+
clientOptions.cliPath === undefined &&
|
|
110
|
+
clientOptions.cliArgs === undefined &&
|
|
111
|
+
clientOptions.cliUrl === undefined) {
|
|
112
|
+
if (cliLaunch.cliPath !== undefined) {
|
|
113
|
+
clientOptions.cliPath = cliLaunch.cliPath;
|
|
114
|
+
}
|
|
115
|
+
if (cliLaunch.cliArgs !== undefined) {
|
|
116
|
+
clientOptions.cliArgs = cliLaunch.cliArgs;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const explicitGithubToken = typeof clientOptions.githubToken === "string" && clientOptions.githubToken.trim()
|
|
120
|
+
? clientOptions.githubToken.trim()
|
|
121
|
+
: typeof options.githubToken === "string" && options.githubToken.trim()
|
|
122
|
+
? options.githubToken.trim()
|
|
123
|
+
: "";
|
|
124
|
+
if (clientOptions.githubToken === undefined && explicitGithubToken) {
|
|
125
|
+
clientOptions.githubToken = explicitGithubToken;
|
|
126
|
+
}
|
|
127
|
+
if (clientOptions.useLoggedInUser === undefined && typeof options.useLoggedInUser === "boolean") {
|
|
128
|
+
clientOptions.useLoggedInUser = options.useLoggedInUser;
|
|
129
|
+
}
|
|
130
|
+
let resolvedEnv;
|
|
131
|
+
if (clientOptions.env === undefined) {
|
|
132
|
+
resolvedEnv = {
|
|
133
|
+
...process.env,
|
|
134
|
+
...configEnv,
|
|
135
|
+
...(options.env && typeof options.env === "object" ? options.env : {}),
|
|
136
|
+
...(hasOwnEnumerableKeys(cliLaunch?.env) ? cliLaunch.env : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (hasOwnEnumerableKeys(cliLaunch?.env)) {
|
|
140
|
+
resolvedEnv = { ...clientOptions.env, ...cliLaunch.env };
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
resolvedEnv = { ...clientOptions.env };
|
|
144
|
+
}
|
|
145
|
+
clientOptions.env = explicitGithubToken
|
|
146
|
+
? resolvedEnv
|
|
147
|
+
: withoutCopilotGithubTokenEnv(resolvedEnv);
|
|
148
|
+
if (!explicitGithubToken && clientOptions.useLoggedInUser === undefined) {
|
|
149
|
+
clientOptions.useLoggedInUser = true;
|
|
150
|
+
}
|
|
151
|
+
if (clientOptions.cwd === undefined) {
|
|
152
|
+
const cwd = typeof options.cwd === "string" && options.cwd.trim()
|
|
153
|
+
? options.cwd.trim()
|
|
154
|
+
: process.cwd();
|
|
155
|
+
clientOptions.cwd = cwd;
|
|
156
|
+
}
|
|
157
|
+
return clientOptions;
|
|
158
|
+
}
|
|
159
|
+
function resolveCopilotCommandLine(options = {}) {
|
|
160
|
+
if (typeof options.commandLine === "string" && options.commandLine.trim()) {
|
|
161
|
+
return options.commandLine.trim();
|
|
162
|
+
}
|
|
163
|
+
const allowCliList = options.allowCliList && typeof options.allowCliList === "object" ? options.allowCliList : {};
|
|
164
|
+
const backendCandidates = [];
|
|
165
|
+
const pushCandidate = (backend) => {
|
|
166
|
+
const normalized = typeof backend === "string" ? backend.trim().toLowerCase() : "";
|
|
167
|
+
if (normalized && !backendCandidates.includes(normalized)) {
|
|
168
|
+
backendCandidates.push(normalized);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
pushCandidate(options.backend);
|
|
172
|
+
pushCandidate(options.runtimeBackend);
|
|
173
|
+
pushCandidate("copilot");
|
|
174
|
+
for (const candidate of backendCandidates) {
|
|
175
|
+
const entry = allowCliList[candidate];
|
|
176
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
177
|
+
return entry.trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
function resolveCopilotCliLaunch(commandLine, env = process.env) {
|
|
183
|
+
const normalized = typeof commandLine === "string" ? commandLine.trim() : "";
|
|
184
|
+
if (!normalized) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const parsed = parseCommandParts(normalized);
|
|
188
|
+
const unwrapped = unwrapEnvironmentCommand(parsed.command, parsed.args);
|
|
189
|
+
const command = unwrapped.command;
|
|
190
|
+
const args = unwrapped.args;
|
|
191
|
+
if (!command) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const cliArgs = normalizeCopilotCliArgs(args);
|
|
195
|
+
if (isDefaultCopilotCommand(command)) {
|
|
196
|
+
if (cliArgs.length === 0 && !hasOwnEnumerableKeys(unwrapped.env)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return { cliArgs, env: unwrapped.env };
|
|
200
|
+
}
|
|
201
|
+
const launchEnv = { ...process.env, ...env, ...unwrapped.env };
|
|
202
|
+
const resolvedPath = resolveExecutablePath(command, launchEnv);
|
|
203
|
+
return {
|
|
204
|
+
cliPath: resolvedPath || command,
|
|
205
|
+
cliArgs,
|
|
206
|
+
env: unwrapped.env,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function normalizeCopilotCliArgs(args) {
|
|
210
|
+
if (!Array.isArray(args)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return args.filter((item) => {
|
|
214
|
+
const normalized = typeof item === "string" ? item.trim().toLowerCase() : "";
|
|
215
|
+
return normalized && !LEGACY_COPILOT_CLI_ARGS.has(normalized);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function stripExecutableSuffix(name) {
|
|
219
|
+
return String(name || "")
|
|
220
|
+
.trim()
|
|
221
|
+
.toLowerCase()
|
|
222
|
+
.replace(/\.(cmd|bat|exe)$/i, "");
|
|
223
|
+
}
|
|
224
|
+
function isDefaultCopilotCommand(command) {
|
|
225
|
+
const normalized = String(command || "").trim();
|
|
226
|
+
if (!normalized || /[\\/]/.test(normalized)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return stripExecutableSuffix(normalized) === "copilot";
|
|
230
|
+
}
|
|
231
|
+
function isEnvironmentAssignment(token) {
|
|
232
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(token || "").trim());
|
|
233
|
+
}
|
|
234
|
+
function parseEnvironmentAssignment(token) {
|
|
235
|
+
const normalized = String(token || "");
|
|
236
|
+
const index = normalized.indexOf("=");
|
|
237
|
+
if (index <= 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
key: normalized.slice(0, index),
|
|
242
|
+
value: normalized.slice(index + 1),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function isEnvCommand(command) {
|
|
246
|
+
return stripExecutableSuffix(path.basename(String(command || ""))) === "env";
|
|
247
|
+
}
|
|
248
|
+
function isPathLikeCommand(command) {
|
|
249
|
+
const normalized = String(command || "").trim();
|
|
250
|
+
return (normalized.startsWith(".") ||
|
|
251
|
+
normalized.startsWith("/") ||
|
|
252
|
+
normalized.includes("/") ||
|
|
253
|
+
normalized.includes("\\") ||
|
|
254
|
+
/^[A-Za-z]:[\\/]/.test(normalized));
|
|
255
|
+
}
|
|
256
|
+
function resolveExecutablePath(command, env = process.env) {
|
|
257
|
+
const normalized = String(command || "").trim();
|
|
258
|
+
if (!normalized) {
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
if (isPathLikeCommand(normalized)) {
|
|
262
|
+
return normalized;
|
|
263
|
+
}
|
|
264
|
+
const pathEnv = typeof env?.PATH === "string" ? env.PATH : process.env.PATH || "";
|
|
265
|
+
const pathExt = process.platform === "win32" && !path.extname(normalized)
|
|
266
|
+
? String(env?.PATHEXT || process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
267
|
+
.split(";")
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
: [""];
|
|
270
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
271
|
+
if (!dir) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
for (const ext of pathExt) {
|
|
275
|
+
const candidate = path.join(dir, `${normalized}${ext}`);
|
|
276
|
+
if (fs.existsSync(candidate)) {
|
|
277
|
+
return candidate;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
function unwrapEnvironmentCommand(command, args) {
|
|
284
|
+
const parts = [command, ...args].filter((item) => typeof item === "string" && item.length > 0);
|
|
285
|
+
const extraEnv = {};
|
|
286
|
+
let index = 0;
|
|
287
|
+
while (index < parts.length && isEnvironmentAssignment(parts[index])) {
|
|
288
|
+
const assignment = parseEnvironmentAssignment(parts[index]);
|
|
289
|
+
if (assignment) {
|
|
290
|
+
extraEnv[assignment.key] = assignment.value;
|
|
291
|
+
}
|
|
292
|
+
index += 1;
|
|
293
|
+
}
|
|
294
|
+
if (index > 0) {
|
|
295
|
+
return {
|
|
296
|
+
command: parts[index] || "",
|
|
297
|
+
args: parts.slice(index + 1),
|
|
298
|
+
env: extraEnv,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!isEnvCommand(command)) {
|
|
302
|
+
return { command, args, env: extraEnv };
|
|
303
|
+
}
|
|
304
|
+
index = 0;
|
|
305
|
+
while (index < args.length) {
|
|
306
|
+
const token = args[index];
|
|
307
|
+
if (token === "--") {
|
|
308
|
+
index += 1;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (isEnvironmentAssignment(token)) {
|
|
312
|
+
const assignment = parseEnvironmentAssignment(token);
|
|
313
|
+
if (assignment) {
|
|
314
|
+
extraEnv[assignment.key] = assignment.value;
|
|
315
|
+
}
|
|
316
|
+
index += 1;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (String(token || "").startsWith("-")) {
|
|
320
|
+
return { command, args, env: extraEnv };
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
command: args[index] || "",
|
|
326
|
+
args: args.slice(index + 1),
|
|
327
|
+
env: extraEnv,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function hasOwnEnumerableKeys(value) {
|
|
331
|
+
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
332
|
+
}
|
|
333
|
+
function resolvePositiveTimeoutMs(value, fallback) {
|
|
334
|
+
const n = Number(value);
|
|
335
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : fallback;
|
|
336
|
+
}
|
|
337
|
+
function remainingTimeoutMs(startedAtMs, timeoutMs, message) {
|
|
338
|
+
const remaining = timeoutMs - (Date.now() - startedAtMs);
|
|
339
|
+
if (remaining <= 0) {
|
|
340
|
+
throw new Error(message);
|
|
341
|
+
}
|
|
342
|
+
return remaining;
|
|
343
|
+
}
|
|
344
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
345
|
+
let timer = null;
|
|
346
|
+
try {
|
|
347
|
+
return await Promise.race([
|
|
348
|
+
promise,
|
|
349
|
+
new Promise((_, reject) => {
|
|
350
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
351
|
+
}),
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
if (timer) {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function buildConfiguredEnvMap(configFilePath) {
|
|
361
|
+
const envConfig = loadEnvConfig(configFilePath);
|
|
362
|
+
if (!envConfig) {
|
|
363
|
+
return {};
|
|
364
|
+
}
|
|
365
|
+
const normalizedEnv = { ...proxyToEnv(envConfig) };
|
|
366
|
+
for (const [key, value] of Object.entries(envConfig)) {
|
|
367
|
+
const normalized = typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
368
|
+
if (normalized !== undefined) {
|
|
369
|
+
normalizedEnv[key] = normalized;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return normalizedEnv;
|
|
373
|
+
}
|
|
374
|
+
// Suppress unused-import lint for COPILOT_GITHUB_TOKEN_ENV_KEYS — retained for reference.
|
|
375
|
+
export const _COPILOT_GITHUB_TOKEN_ENV_KEYS = COPILOT_GITHUB_TOKEN_ENV_KEYS;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function resumeProviderForBackend(backend: any): "codex" | null;
|
|
2
|
+
export function buildResumeArgsForBackend(backend: any, sessionId: any): string[];
|
|
3
|
+
export function findSessionPath(provider: any, sessionId: any, options?: {}): Promise<string | null>;
|
|
4
|
+
export function resolveResumeContext(backend: any, sessionId: any, options?: {}): Promise<{
|
|
5
|
+
provider: any;
|
|
6
|
+
sessionId: any;
|
|
7
|
+
sessionPath: null;
|
|
8
|
+
cwd: any;
|
|
9
|
+
debugMetadata: {
|
|
10
|
+
cwdSource: any;
|
|
11
|
+
sessionPath: null;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export function inspectResumeTarget(backend: any, sessionId: any, options?: {}): Promise<{
|
|
15
|
+
provider: any;
|
|
16
|
+
sessionId: any;
|
|
17
|
+
sessionPath: null;
|
|
18
|
+
cwd: any;
|
|
19
|
+
debugMetadata: {
|
|
20
|
+
cwdSource: any;
|
|
21
|
+
sessionPath: null;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
import { resolveSessionRunDirectory } from "./shared.js";
|
|
25
|
+
import { BUILT_IN_BACKENDS } from "../built-in-backends.js";
|
|
26
|
+
export { resolveSessionRunDirectory, BUILT_IN_BACKENDS };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { BUILT_IN_BACKENDS, getBuiltInBackendEntry, normalizeBuiltInBackend, } from "../built-in-backends.js";
|
|
2
|
+
import { getExternalProviderDescriptor, resolveExternalBackend, } from "../external-provider-registry.js";
|
|
3
|
+
import * as claude from "./claude.js";
|
|
4
|
+
import * as codex from "./codex.js";
|
|
5
|
+
import * as copilot from "./copilot.js";
|
|
6
|
+
import * as kimi from "./kimi.js";
|
|
7
|
+
import * as opencode from "./opencode.js";
|
|
8
|
+
import { buildResumeContext, isExistingDirectory, normalizeBackend, normalizeProjectPathCandidate, normalizeSessionId, resolveSessionRunDirectory, } from "./shared.js";
|
|
9
|
+
/**
|
|
10
|
+
* Map of canonical built-in backend name → resume module.
|
|
11
|
+
*
|
|
12
|
+
* **Every built-in backend MUST be registered here.** Resume is a required
|
|
13
|
+
* capability for every provider in ai-sdk; the invariant is enforced by the
|
|
14
|
+
* module-load self-check below.
|
|
15
|
+
*
|
|
16
|
+
* Adding a new built-in backend:
|
|
17
|
+
* 1. Add an entry to {@link BUILT_IN_BACKENDS} in `../built-in-backends.js`.
|
|
18
|
+
* 2. Create `./<backend>.js` exporting
|
|
19
|
+
* `{ BACKEND, buildCliArgs, findSessionPath, resolveResumeContext }`.
|
|
20
|
+
* 3. Register the new module in this map.
|
|
21
|
+
*/
|
|
22
|
+
const RESUME_MODULES_BY_BACKEND = new Map([
|
|
23
|
+
["codex", codex],
|
|
24
|
+
["claude", claude],
|
|
25
|
+
["copilot", copilot],
|
|
26
|
+
["kimi", kimi],
|
|
27
|
+
["opencode", opencode],
|
|
28
|
+
]);
|
|
29
|
+
// Sanity-check at module load:
|
|
30
|
+
// (1) Every registered resume module must point at a real built-in backend.
|
|
31
|
+
// Catches typos like registering "kimii" by mistake.
|
|
32
|
+
// (2) Every built-in backend must have a registered resume module.
|
|
33
|
+
// Enforces the "all providers support resume" invariant — a new backend
|
|
34
|
+
// cannot be added to BUILT_IN_BACKENDS without its resume module.
|
|
35
|
+
for (const backend of RESUME_MODULES_BY_BACKEND.keys()) {
|
|
36
|
+
if (!getBuiltInBackendEntry(backend)) {
|
|
37
|
+
throw new Error(`ai-sdk resume dispatcher references unknown built-in backend "${backend}". `
|
|
38
|
+
+ `Did you forget to add it to BUILT_IN_BACKENDS in src/built-in-backends.js?`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const entry of BUILT_IN_BACKENDS) {
|
|
42
|
+
if (!RESUME_MODULES_BY_BACKEND.has(entry.backend)) {
|
|
43
|
+
throw new Error(`ai-sdk built-in backend "${entry.backend}" has no resume module registered. `
|
|
44
|
+
+ `Resume is required for every built-in backend. Add src/resume/${entry.backend}.js and `
|
|
45
|
+
+ `register it in RESUME_MODULES_BY_BACKEND in src/resume/index.js.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function lookupBuiltInProvider(backend) {
|
|
49
|
+
const canonical = normalizeBuiltInBackend(normalizeBackend(backend));
|
|
50
|
+
return RESUME_MODULES_BY_BACKEND.get(canonical) || null;
|
|
51
|
+
}
|
|
52
|
+
export function resumeProviderForBackend(backend) {
|
|
53
|
+
const provider = lookupBuiltInProvider(backend);
|
|
54
|
+
return provider ? provider.BACKEND : null;
|
|
55
|
+
}
|
|
56
|
+
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
57
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
58
|
+
if (!normalizedSessionId) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const provider = lookupBuiltInProvider(backend);
|
|
62
|
+
if (!provider) {
|
|
63
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
64
|
+
}
|
|
65
|
+
return provider.buildCliArgs(normalizedSessionId);
|
|
66
|
+
}
|
|
67
|
+
export async function findSessionPath(provider, sessionId, options = {}) {
|
|
68
|
+
const descriptor = lookupBuiltInProvider(provider);
|
|
69
|
+
if (!descriptor) {
|
|
70
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
71
|
+
}
|
|
72
|
+
return descriptor.findSessionPath(sessionId, options);
|
|
73
|
+
}
|
|
74
|
+
export { resolveSessionRunDirectory };
|
|
75
|
+
export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
76
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
77
|
+
if (!normalizedSessionId) {
|
|
78
|
+
throw new Error("--resume requires a session id");
|
|
79
|
+
}
|
|
80
|
+
const builtInProvider = lookupBuiltInProvider(backend);
|
|
81
|
+
if (builtInProvider) {
|
|
82
|
+
return builtInProvider.resolveResumeContext(normalizedSessionId, options);
|
|
83
|
+
}
|
|
84
|
+
const externalContext = await resolveExternalResumeContext(backend, normalizedSessionId, options);
|
|
85
|
+
if (externalContext) {
|
|
86
|
+
return externalContext;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
89
|
+
}
|
|
90
|
+
async function resolveExternalResumeContext(backend, sessionId, options = {}) {
|
|
91
|
+
const normalizedBackend = await resolveExternalBackend(normalizeBackend(backend), options);
|
|
92
|
+
if (!normalizedBackend) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const descriptor = await getExternalProviderDescriptor(normalizedBackend, options);
|
|
96
|
+
if (!descriptor) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (typeof descriptor.resolveResumeContext !== "function") {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const resolvedContext = await descriptor.resolveResumeContext(sessionId, options);
|
|
103
|
+
if (!resolvedContext) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const cwd = normalizeProjectPathCandidate(resolvedContext?.cwd);
|
|
107
|
+
if (!cwd) {
|
|
108
|
+
throw new Error(`Could not resolve workspace for backend "${normalizedBackend}" session ${sessionId}`);
|
|
109
|
+
}
|
|
110
|
+
if (!(await isExistingDirectory(cwd))) {
|
|
111
|
+
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
112
|
+
}
|
|
113
|
+
const sessionPath = normalizeProjectPathCandidate(resolvedContext?.sessionPath);
|
|
114
|
+
const cwdSource = typeof resolvedContext?.debugMetadata?.cwdSource === "string"
|
|
115
|
+
&& resolvedContext.debugMetadata.cwdSource.trim()
|
|
116
|
+
? resolvedContext.debugMetadata.cwdSource.trim()
|
|
117
|
+
: "provider";
|
|
118
|
+
return buildResumeContext({
|
|
119
|
+
provider: normalizedBackend,
|
|
120
|
+
sessionId,
|
|
121
|
+
sessionPath,
|
|
122
|
+
cwd,
|
|
123
|
+
cwdSource,
|
|
124
|
+
extraDebug: resolvedContext?.debugMetadata && typeof resolvedContext.debugMetadata === "object"
|
|
125
|
+
? resolvedContext.debugMetadata
|
|
126
|
+
: {},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export async function inspectResumeTarget(backend, sessionId, options = {}) {
|
|
130
|
+
return resolveResumeContext(backend, sessionId, options);
|
|
131
|
+
}
|
|
132
|
+
export { BUILT_IN_BACKENDS };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function buildCliArgs(sessionId: any): string[];
|
|
2
|
+
export function findSessionPath(sessionId: any, options?: {}): Promise<string | null>;
|
|
3
|
+
export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
4
|
+
provider: any;
|
|
5
|
+
sessionId: any;
|
|
6
|
+
sessionPath: null;
|
|
7
|
+
cwd: any;
|
|
8
|
+
debugMetadata: {
|
|
9
|
+
cwdSource: any;
|
|
10
|
+
sessionPath: null;
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
export function md5Hex(value: any): string;
|
|
14
|
+
export const BACKEND: "kimi";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { promises as fsp } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { buildResumeContext, isExistingDirectory, listCandidateWorkingDirectories, normalizeSessionId, pathExists, resolveHomeDir, } from "./shared.js";
|
|
5
|
+
export const BACKEND = "kimi";
|
|
6
|
+
export function buildCliArgs(sessionId) {
|
|
7
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
8
|
+
if (!normalizedSessionId) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return ["--session", normalizedSessionId];
|
|
12
|
+
}
|
|
13
|
+
export async function findSessionPath(sessionId, options = {}) {
|
|
14
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
15
|
+
if (!normalizedSessionId) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const homeDir = resolveHomeDir(options);
|
|
19
|
+
const sessionsDir = options.kimiSessionsDir || path.join(homeDir, ".kimi", "sessions");
|
|
20
|
+
return findKimiSessionDirectory(sessionsDir, normalizedSessionId);
|
|
21
|
+
}
|
|
22
|
+
export async function resolveResumeContext(sessionId, options = {}) {
|
|
23
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
24
|
+
if (!normalizedSessionId) {
|
|
25
|
+
throw new Error("--resume requires a session id");
|
|
26
|
+
}
|
|
27
|
+
const sessionPath = await findSessionPath(normalizedSessionId, options);
|
|
28
|
+
if (!sessionPath) {
|
|
29
|
+
throw new Error(`Invalid --resume session id for kimi: ${normalizedSessionId}`);
|
|
30
|
+
}
|
|
31
|
+
const cwdFromSession = await resolveKimiResumeCwd(sessionPath, normalizedSessionId, options);
|
|
32
|
+
if (!cwdFromSession) {
|
|
33
|
+
throw new Error(`Could not resolve workspace for Kimi session ${normalizedSessionId}. Re-run from the original workspace or resume a session previously started by conductor fire.`);
|
|
34
|
+
}
|
|
35
|
+
if (!(await isExistingDirectory(cwdFromSession))) {
|
|
36
|
+
throw new Error(`Resume workspace path does not exist: ${cwdFromSession}`);
|
|
37
|
+
}
|
|
38
|
+
return buildResumeContext({
|
|
39
|
+
provider: "kimi",
|
|
40
|
+
sessionId: normalizedSessionId,
|
|
41
|
+
sessionPath,
|
|
42
|
+
cwd: cwdFromSession,
|
|
43
|
+
cwdSource: "session",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function md5Hex(value) {
|
|
47
|
+
return crypto.createHash("md5").update(String(value ?? "")).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
async function resolveKimiResumeCwd(sessionPath, _sessionId, options = {}) {
|
|
50
|
+
const sessionDirectory = typeof sessionPath === "string" ? sessionPath.trim() : "";
|
|
51
|
+
if (!sessionDirectory) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const worktreeHash = path.basename(path.dirname(sessionDirectory));
|
|
55
|
+
if (!worktreeHash) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
for (const candidate of listCandidateWorkingDirectories(options)) {
|
|
59
|
+
if (md5Hex(candidate) === worktreeHash) {
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (typeof options.lookupWorkspaceByHash === "function") {
|
|
64
|
+
const resolved = await options.lookupWorkspaceByHash(worktreeHash, { sessionId: _sessionId });
|
|
65
|
+
if (typeof resolved === "string" && resolved.trim()) {
|
|
66
|
+
return resolved.trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
async function findKimiSessionDirectory(rootDir, sessionId) {
|
|
72
|
+
let hashDirs = [];
|
|
73
|
+
try {
|
|
74
|
+
hashDirs = await fsp.readdir(rootDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
for (const hashDir of hashDirs) {
|
|
80
|
+
if (!hashDir.isDirectory()) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const candidateDir = path.join(rootDir, hashDir.name, sessionId);
|
|
84
|
+
if (await pathExists(candidateDir, "directory")) {
|
|
85
|
+
return candidateDir;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function buildCliArgs(sessionId: any): string[];
|
|
2
|
+
export function findSessionPath(): Promise<null>;
|
|
3
|
+
export function resolveResumeContext(sessionId: any, options?: {}): Promise<{
|
|
4
|
+
provider: any;
|
|
5
|
+
sessionId: any;
|
|
6
|
+
sessionPath: null;
|
|
7
|
+
cwd: any;
|
|
8
|
+
debugMetadata: {
|
|
9
|
+
cwdSource: any;
|
|
10
|
+
sessionPath: null;
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
export const BACKEND: "opencode";
|