u1s1-cli 0.2.0 → 0.4.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.
@@ -0,0 +1,354 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { join } from "node:path";
4
+ import { formatHomePath } from "../brand.js";
5
+ import { agentDir } from "../config.js";
6
+ import { claudeAdapter, hydrateClaudeSession } from "./claude.js";
7
+ import { codexAdapter, hydrateCodexSession } from "./codex.js";
8
+ import { statSync } from "node:fs";
9
+ import { asRecord, asString, formatBytes, oneLine, sessionBelongsToCwd } from "./util.js";
10
+ import { readJsonIfExists, writeConvertedSession, writeJson } from "./write.js";
11
+ const ADAPTERS = {
12
+ claude: claudeAdapter,
13
+ codex: codexAdapter,
14
+ };
15
+ const ALL_SOURCES = ["claude", "codex"];
16
+ /** Skip giant rollouts that would blow memory on a one-shot import. */
17
+ const MAX_SOURCE_BYTES = 25 * 1024 * 1024;
18
+ function importIndexPath() {
19
+ return join(agentDir, "imported-sessions.json");
20
+ }
21
+ function loadIndex() {
22
+ const raw = readJsonIfExists(importIndexPath());
23
+ const rec = asRecord(raw);
24
+ const items = asRecord(rec?.["items"]);
25
+ if (!items)
26
+ return { version: 1, items: {} };
27
+ const out = { version: 1, items: {} };
28
+ for (const [key, value] of Object.entries(items)) {
29
+ const item = asRecord(value);
30
+ if (!item)
31
+ continue;
32
+ const source = asString(item["source"]);
33
+ const sourceId = asString(item["sourceId"]);
34
+ const sourcePath = asString(item["sourcePath"]);
35
+ const destPath = asString(item["destPath"]);
36
+ const cwd = asString(item["cwd"]);
37
+ const importedAt = asString(item["importedAt"]);
38
+ if (!source || !sourceId || !sourcePath || !destPath || !cwd || !importedAt)
39
+ continue;
40
+ if (source !== "claude" && source !== "codex")
41
+ continue;
42
+ out.items[key] = {
43
+ source,
44
+ sourceId,
45
+ sourcePath,
46
+ destPath,
47
+ cwd,
48
+ title: asString(item["title"]),
49
+ importedAt,
50
+ };
51
+ }
52
+ return out;
53
+ }
54
+ function recordKey(session) {
55
+ return `${session.source}:${session.sourceId}`;
56
+ }
57
+ function parseSourceArg(raw) {
58
+ if (!raw || raw === "all")
59
+ return [...ALL_SOURCES];
60
+ const aliases = {
61
+ claude: "claude",
62
+ "claude-code": "claude",
63
+ cc: "claude",
64
+ anthropic: "claude",
65
+ codex: "codex",
66
+ openai: "codex",
67
+ };
68
+ const id = aliases[raw.toLowerCase()];
69
+ if (!id) {
70
+ console.error(` 不认识来源「${raw}」,可选: claude / codex / all`);
71
+ process.exit(1);
72
+ }
73
+ return [id];
74
+ }
75
+ function parseFlags(args) {
76
+ let cwd = process.cwd();
77
+ let all = false;
78
+ let dryRun = false;
79
+ let force = false;
80
+ let sourceRaw;
81
+ let limit;
82
+ for (let i = 0; i < args.length; i++) {
83
+ const a = args[i];
84
+ if (a === "--all")
85
+ all = true;
86
+ else if (a === "--dry-run")
87
+ dryRun = true;
88
+ else if (a === "--force")
89
+ force = true;
90
+ else if (a === "--cwd") {
91
+ const next = args[++i];
92
+ if (!next) {
93
+ console.error(" --cwd 后面要跟一个目录");
94
+ process.exit(1);
95
+ }
96
+ cwd = next;
97
+ }
98
+ else if (a === "--limit") {
99
+ const next = args[++i];
100
+ const n = Number(next);
101
+ if (!Number.isFinite(n) || n <= 0) {
102
+ console.error(" --limit 要是正整数");
103
+ process.exit(1);
104
+ }
105
+ limit = Math.floor(n);
106
+ }
107
+ else if (a.startsWith("-")) {
108
+ console.error(` 不认识参数 ${a}`);
109
+ process.exit(1);
110
+ }
111
+ else if (!sourceRaw) {
112
+ sourceRaw = a;
113
+ }
114
+ else {
115
+ console.error(` 多余参数 ${a}`);
116
+ process.exit(1);
117
+ }
118
+ }
119
+ return {
120
+ sources: parseSourceArg(sourceRaw),
121
+ cwd: all ? undefined : cwd,
122
+ dryRun,
123
+ force,
124
+ limit,
125
+ };
126
+ }
127
+ function printHelp() {
128
+ console.log("");
129
+ console.log(" 把 Claude Code / Codex 的历史对话导入 u1s1");
130
+ console.log("");
131
+ console.log(" 用法:");
132
+ console.log(" u1s1 import 导入当前目录的对话");
133
+ console.log(" u1s1 import claude 只导 Claude Code");
134
+ console.log(" u1s1 import codex 只导 Codex");
135
+ console.log(" u1s1 import --all 导入本机找到的全部对话");
136
+ console.log(" u1s1 import --cwd 目录 指定项目目录(默认当前目录)");
137
+ console.log(" u1s1 import --dry-run 只看会导哪些,不写盘");
138
+ console.log(" u1s1 import --force 已经导过的也再导一遍");
139
+ console.log("");
140
+ console.log(" 导入后在对应项目里跑 u1s1,输入 /resume 就能看到。");
141
+ console.log("");
142
+ }
143
+ async function confirm(question) {
144
+ if (!input.isTTY || !output.isTTY)
145
+ return true;
146
+ const rl = createInterface({ input, output });
147
+ try {
148
+ const ans = (await rl.question(question)).trim().toLowerCase();
149
+ return ans === "" || ans === "y" || ans === "yes" || ans === "是";
150
+ }
151
+ finally {
152
+ rl.close();
153
+ }
154
+ }
155
+ async function hydrateSession(session) {
156
+ try {
157
+ if (session.source === "claude")
158
+ return await hydrateClaudeSession(session);
159
+ return await hydrateCodexSession(session);
160
+ }
161
+ catch {
162
+ return session;
163
+ }
164
+ }
165
+ async function discoverAll(opts) {
166
+ const found = [];
167
+ for (const id of opts.sources) {
168
+ found.push(...ADAPTERS[id].discover({ cwd: opts.cwd }));
169
+ }
170
+ if (opts.cwd) {
171
+ return found.filter((s) => !s.cwd || sessionBelongsToCwd(s.cwd, opts.cwd));
172
+ }
173
+ return found;
174
+ }
175
+ function sortSessions(sessions) {
176
+ return [...sessions].sort((a, b) => b.mtimeMs - a.mtimeMs);
177
+ }
178
+ function sourceLabel(id) {
179
+ return ADAPTERS[id].label;
180
+ }
181
+ export async function importCommand(args) {
182
+ if (args.includes("-h") || args.includes("--help")) {
183
+ printHelp();
184
+ return;
185
+ }
186
+ const yes = args.includes("-y") || args.includes("--yes");
187
+ const opts = parseFlags(args.filter((a) => a !== "-y" && a !== "--yes"));
188
+ const index = loadIndex();
189
+ const discovered = sortSessions(await discoverAll(opts));
190
+ const limited = opts.limit ? discovered.slice(0, opts.limit) : discovered;
191
+ if (limited.length === 0) {
192
+ const where = opts.cwd ? `当前目录 ${formatHomePath(opts.cwd)}` : "这台电脑";
193
+ const who = opts.sources.map(sourceLabel).join(" / ");
194
+ console.log("");
195
+ console.log(` 在${where}没找到 ${who} 的历史对话。`);
196
+ if (opts.cwd)
197
+ console.log(" 想全盘扫一遍可以: u1s1 import --all");
198
+ console.log("");
199
+ return;
200
+ }
201
+ const pending = [];
202
+ let already = 0;
203
+ for (const session of limited) {
204
+ const prev = index.items[recordKey(session)];
205
+ if (prev && !opts.force) {
206
+ already += 1;
207
+ continue;
208
+ }
209
+ pending.push(session);
210
+ }
211
+ console.log("");
212
+ console.log(` 找到 ${limited.length} 段对话` +
213
+ (opts.cwd ? `(${formatHomePath(opts.cwd)})` : "") +
214
+ (already ? `,其中 ${already} 段以前导过` : "") +
215
+ "。");
216
+ const preview = await Promise.all(pending.slice(0, 12).map(hydrateSession));
217
+ for (const session of preview) {
218
+ const title = session.title ? oneLine(session.title, 56) : "(无标题)";
219
+ console.log(` · [${sourceLabel(session.source)}] ${title}`);
220
+ if (!opts.cwd && session.cwd)
221
+ console.log(` ${formatHomePath(session.cwd)}`);
222
+ }
223
+ if (pending.length > preview.length) {
224
+ console.log(` …还有 ${pending.length - preview.length} 段`);
225
+ }
226
+ if (pending.length === 0) {
227
+ console.log("");
228
+ console.log(" 没有新的可导。想重导一遍就加 --force。");
229
+ console.log("");
230
+ return;
231
+ }
232
+ if (opts.dryRun) {
233
+ console.log("");
234
+ console.log(` 预演结束,以上 ${pending.length} 段还没真正导入。`);
235
+ console.log("");
236
+ return;
237
+ }
238
+ if (!yes) {
239
+ const ok = await confirm(` 导入这 ${pending.length} 段?(回车=好 / n=取消) `);
240
+ if (!ok) {
241
+ console.log(" 已取消。");
242
+ return;
243
+ }
244
+ }
245
+ const summary = runImport(pending, index, opts);
246
+ writeJson(importIndexPath(), index);
247
+ console.log("");
248
+ console.log(` ✓ 导入 ${summary.imported} 段` +
249
+ (summary.skipped ? `,跳过 ${summary.skipped}` : "") +
250
+ (summary.empty ? `,空对话 ${summary.empty}` : "") +
251
+ (summary.errors ? `,失败 ${summary.errors}` : "") +
252
+ "。");
253
+ if (summary.imported > 0) {
254
+ console.log(" 进对应项目跑 u1s1,输入 /resume 就能接着聊。");
255
+ }
256
+ for (const item of summary.items) {
257
+ if (item.status === "error")
258
+ console.log(` × ${item.title}: ${item.detail ?? "失败"}`);
259
+ else if (item.status === "skipped" && item.detail)
260
+ console.log(` · 跳过 ${item.title}: ${item.detail}`);
261
+ }
262
+ console.log("");
263
+ }
264
+ function runImport(sessions, index, opts) {
265
+ const items = [];
266
+ let imported = 0;
267
+ let skipped = 0;
268
+ let empty = 0;
269
+ let errors = 0;
270
+ for (const session of sessions) {
271
+ const adapter = ADAPTERS[session.source];
272
+ const title = session.title ? oneLine(session.title, 56) : session.sourceId;
273
+ try {
274
+ let size = 0;
275
+ try {
276
+ size = statSync(session.sourcePath).size;
277
+ }
278
+ catch {
279
+ size = 0;
280
+ }
281
+ if (size > MAX_SOURCE_BYTES) {
282
+ items.push({
283
+ source: session.source,
284
+ sourceId: session.sourceId,
285
+ title,
286
+ cwd: session.cwd,
287
+ status: "skipped",
288
+ detail: `原始记录太大(${formatBytes(size)}),先跳过`,
289
+ });
290
+ skipped += 1;
291
+ continue;
292
+ }
293
+ const converted = adapter.convert(session);
294
+ if (opts.cwd && converted.cwd && !sessionBelongsToCwd(converted.cwd, opts.cwd)) {
295
+ items.push({
296
+ source: session.source,
297
+ sourceId: session.sourceId,
298
+ title,
299
+ cwd: converted.cwd,
300
+ status: "skipped",
301
+ detail: "不属于当前目录",
302
+ });
303
+ skipped += 1;
304
+ continue;
305
+ }
306
+ if (converted.messages.length === 0) {
307
+ items.push({
308
+ source: session.source,
309
+ sourceId: session.sourceId,
310
+ title,
311
+ cwd: converted.cwd,
312
+ status: "empty",
313
+ });
314
+ empty += 1;
315
+ continue;
316
+ }
317
+ const named = converted.title?.trim() || title;
318
+ converted.title = `[${sourceLabel(session.source)}] ${named}`;
319
+ const destCwd = opts.cwd || converted.cwd;
320
+ const destPath = writeConvertedSession(converted, destCwd);
321
+ const rec = {
322
+ source: session.source,
323
+ sourceId: session.sourceId,
324
+ sourcePath: session.sourcePath,
325
+ destPath,
326
+ cwd: destCwd,
327
+ title: converted.title,
328
+ importedAt: new Date().toISOString(),
329
+ };
330
+ index.items[recordKey(session)] = rec;
331
+ items.push({
332
+ source: session.source,
333
+ sourceId: session.sourceId,
334
+ title: converted.title ?? title,
335
+ cwd: destCwd,
336
+ destPath,
337
+ status: "imported",
338
+ });
339
+ imported += 1;
340
+ }
341
+ catch (e) {
342
+ items.push({
343
+ source: session.source,
344
+ sourceId: session.sourceId,
345
+ title,
346
+ cwd: session.cwd,
347
+ status: "error",
348
+ detail: e instanceof Error ? e.message : String(e),
349
+ });
350
+ errors += 1;
351
+ }
352
+ }
353
+ return { items, imported, skipped, empty, errors };
354
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,231 @@
1
+ import { closeSync, existsSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export const MAX_TOOL_RESULT_CHARS = 80_000;
5
+ export const MAX_TEXT_CHARS = 200_000;
6
+ export const PREVIEW_TITLE_CHARS = 48;
7
+ export function resolveExistingDir(path) {
8
+ try {
9
+ if (!existsSync(path))
10
+ return undefined;
11
+ const real = realpathSync(path);
12
+ return statSync(real).isDirectory() ? real : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export function listHomeClaudeDirs() {
19
+ const home = homedir();
20
+ const out = [];
21
+ try {
22
+ for (const name of readdirSync(home)) {
23
+ if (name === ".claude" || name.startsWith(".claude-"))
24
+ out.push(join(home, name));
25
+ }
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ return out;
31
+ }
32
+ export function uniqueExistingDirs(paths) {
33
+ const seen = new Set();
34
+ const out = [];
35
+ for (const path of paths) {
36
+ if (!path)
37
+ continue;
38
+ const real = resolveExistingDir(path);
39
+ if (!real || seen.has(real))
40
+ continue;
41
+ seen.add(real);
42
+ out.push(real);
43
+ }
44
+ return out;
45
+ }
46
+ export function encodeClaudeProjectDir(cwd) {
47
+ const normalized = cwd.replace(/\\/g, "/");
48
+ return normalized.replace(/[/:]/g, "-");
49
+ }
50
+ export function samePath(a, b) {
51
+ const na = a.replace(/\\/g, "/").replace(/\/+$/, "");
52
+ const nb = b.replace(/\\/g, "/").replace(/\/+$/, "");
53
+ return na === nb;
54
+ }
55
+ function normPath(p) {
56
+ return p.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
57
+ }
58
+ /** Nearest git root at or above cwd. Stops at home so `~/` is never treated as a mega-project. */
59
+ export function projectRoot(cwd) {
60
+ const home = normPath(homedir());
61
+ let cur = normPath(cwd);
62
+ while (true) {
63
+ if (existsSync(join(cur, ".git")))
64
+ return cur;
65
+ if (cur === "/" || cur === home)
66
+ return undefined;
67
+ const slash = cur.lastIndexOf("/");
68
+ const next = slash <= 0 ? "/" : cur.slice(0, slash);
69
+ if (next === cur)
70
+ return undefined;
71
+ cur = next;
72
+ }
73
+ }
74
+ /** cwd + ancestors up to the git root (or just cwd when there is no repo). */
75
+ export function projectAncestors(cwd) {
76
+ const root = projectRoot(cwd);
77
+ const out = [];
78
+ let cur = normPath(cwd);
79
+ while (true) {
80
+ out.push(cur);
81
+ if (!root || cur === root)
82
+ break;
83
+ const slash = cur.lastIndexOf("/");
84
+ const next = slash <= 0 ? "/" : cur.slice(0, slash);
85
+ if (next === cur)
86
+ break;
87
+ cur = next;
88
+ }
89
+ return out;
90
+ }
91
+ /** Same folder, anywhere inside this git repo, or the repo root when you're in a subfolder. */
92
+ export function sessionBelongsToCwd(sessionCwd, wanted) {
93
+ if (!sessionCwd || !wanted)
94
+ return false;
95
+ const s = normPath(sessionCwd);
96
+ const w = normPath(wanted);
97
+ if (s === w)
98
+ return true;
99
+ const root = projectRoot(w);
100
+ if (!root)
101
+ return false;
102
+ return s === root || s.startsWith(`${root}/`);
103
+ }
104
+ export function parseJsonLine(line) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed)
107
+ return undefined;
108
+ try {
109
+ const value = JSON.parse(trimmed);
110
+ return value && typeof value === "object" && !Array.isArray(value)
111
+ ? value
112
+ : undefined;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ export function asRecord(value) {
119
+ return value && typeof value === "object" && !Array.isArray(value)
120
+ ? value
121
+ : undefined;
122
+ }
123
+ export function asString(value) {
124
+ return typeof value === "string" ? value : undefined;
125
+ }
126
+ export function parseTime(value) {
127
+ if (typeof value === "number" && Number.isFinite(value)) {
128
+ return value < 1e12 ? value * 1000 : value;
129
+ }
130
+ if (typeof value === "string") {
131
+ const ms = Date.parse(value);
132
+ return Number.isNaN(ms) ? undefined : ms;
133
+ }
134
+ return undefined;
135
+ }
136
+ export function truncateText(text, max) {
137
+ if (text.length <= max)
138
+ return text;
139
+ const omitted = text.length - max;
140
+ return `${text.slice(0, max)}\n\n[已截断,原文还有约 ${formatBytes(omitted)} ]`;
141
+ }
142
+ export function formatBytes(n) {
143
+ if (n < 1024)
144
+ return `${n} 字`;
145
+ if (n < 1024 * 1024)
146
+ return `${(n / 1024).toFixed(1)} KB`;
147
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
148
+ }
149
+ export function oneLine(text, max = PREVIEW_TITLE_CHARS) {
150
+ const compact = text.replace(/\s+/g, " ").trim();
151
+ if (compact.length <= max)
152
+ return compact;
153
+ return `${compact.slice(0, Math.max(0, max - 1))}…`;
154
+ }
155
+ export function firstMeaningfulLine(text) {
156
+ for (const line of text.split(/\r?\n/)) {
157
+ const t = line.trim();
158
+ if (!t)
159
+ continue;
160
+ if (/^<\/?[a-zA-Z][\w:-]*>$/.test(t))
161
+ continue;
162
+ return t.replace(/^<\/?[a-zA-Z][\w:-]*>\s*/, "").trim() || t;
163
+ }
164
+ return "";
165
+ }
166
+ export function parseArgsJson(raw) {
167
+ try {
168
+ const value = JSON.parse(raw);
169
+ if (value && typeof value === "object" && !Array.isArray(value)) {
170
+ return value;
171
+ }
172
+ }
173
+ catch {
174
+ // keep raw
175
+ }
176
+ return { raw };
177
+ }
178
+ export function fileMtimeMs(path) {
179
+ try {
180
+ return statSync(path).mtimeMs;
181
+ }
182
+ catch {
183
+ return 0;
184
+ }
185
+ }
186
+ export function isProbablyInjection(text) {
187
+ const start = text.trimStart();
188
+ return (start.startsWith("<environment_context>") ||
189
+ start.startsWith("<permissions instructions>") ||
190
+ start.startsWith("<skills_instructions>") ||
191
+ start.startsWith("<INSTRUCTIONS>") ||
192
+ start.startsWith("<turn_aborted>") ||
193
+ start.startsWith("<recommended_plugins>") ||
194
+ start.startsWith("# AGENTS.md"));
195
+ }
196
+ export function readFirstJsonObject(path) {
197
+ let fd;
198
+ try {
199
+ fd = openSync(path, "r");
200
+ const chunks = [];
201
+ let total = 0;
202
+ const max = 1024 * 1024;
203
+ while (total < max) {
204
+ const buf = Buffer.alloc(64 * 1024);
205
+ const n = readSync(fd, buf, 0, buf.length, null);
206
+ if (n <= 0)
207
+ break;
208
+ chunks.push(buf.subarray(0, n));
209
+ total += n;
210
+ if (buf.subarray(0, n).includes(0x0a))
211
+ break;
212
+ }
213
+ const chunk = Buffer.concat(chunks).toString("utf8");
214
+ const nl = chunk.indexOf("\n");
215
+ const line = nl === -1 ? chunk : chunk.slice(0, nl);
216
+ return parseJsonLine(line);
217
+ }
218
+ catch {
219
+ return undefined;
220
+ }
221
+ finally {
222
+ if (fd !== undefined) {
223
+ try {
224
+ closeSync(fd);
225
+ }
226
+ catch {
227
+ // ignore
228
+ }
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
4
+ import { agentDir, loadConfig, PROVIDER_ID, resolvePreferredModel } from "../config.js";
5
+ const EMPTY_USAGE = {
6
+ input: 0,
7
+ output: 0,
8
+ cacheRead: 0,
9
+ cacheWrite: 0,
10
+ totalTokens: 0,
11
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
12
+ };
13
+ function toUsage(raw) {
14
+ if (!raw)
15
+ return EMPTY_USAGE;
16
+ const totalTokens = raw.input + raw.output + raw.cacheRead + raw.cacheWrite;
17
+ return {
18
+ input: raw.input,
19
+ output: raw.output,
20
+ cacheRead: raw.cacheRead,
21
+ cacheWrite: raw.cacheWrite,
22
+ totalTokens,
23
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
24
+ };
25
+ }
26
+ export function writeConvertedSession(converted, destCwd) {
27
+ process.env["PI_CODING_AGENT_DIR"] = agentDir;
28
+ const cwd = destCwd || converted.cwd || process.cwd();
29
+ const sm = SessionManager.create(cwd);
30
+ for (const msg of converted.messages) {
31
+ if (msg.role === "user") {
32
+ sm.appendMessage({
33
+ role: "user",
34
+ content: [{ type: "text", text: msg.text }],
35
+ timestamp: msg.timestamp,
36
+ });
37
+ continue;
38
+ }
39
+ if (msg.role === "assistant") {
40
+ sm.appendMessage({
41
+ role: "assistant",
42
+ content: msg.content.map((block) => {
43
+ if (block.type === "text")
44
+ return { type: "text", text: block.text };
45
+ if (block.type === "thinking")
46
+ return { type: "thinking", thinking: block.thinking };
47
+ return {
48
+ type: "toolCall",
49
+ id: block.id,
50
+ name: block.name,
51
+ arguments: block.arguments,
52
+ };
53
+ }),
54
+ api: msg.api,
55
+ provider: msg.provider,
56
+ model: msg.model,
57
+ usage: toUsage(msg.usage),
58
+ stopReason: msg.stopReason,
59
+ timestamp: msg.timestamp,
60
+ });
61
+ continue;
62
+ }
63
+ sm.appendMessage({
64
+ role: "toolResult",
65
+ toolCallId: msg.toolCallId,
66
+ toolName: msg.toolName,
67
+ content: [{ type: "text", text: msg.text }],
68
+ isError: msg.isError,
69
+ timestamp: msg.timestamp,
70
+ });
71
+ }
72
+ const title = converted.title?.trim();
73
+ if (title)
74
+ sm.appendSessionInfo(title);
75
+ // Resume should keep using u1s1 models, not the original Claude/Codex id.
76
+ sm.appendModelChange(PROVIDER_ID, resolvePreferredModel(loadConfig().model));
77
+ const dest = sm.getSessionFile();
78
+ if (!dest)
79
+ throw new Error("会话写盘失败");
80
+ return dest;
81
+ }
82
+ export function readJsonIfExists(path) {
83
+ if (!existsSync(path))
84
+ return undefined;
85
+ try {
86
+ return JSON.parse(readFileSync(path, "utf8"));
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ export function writeJson(path, value) {
93
+ mkdirSync(dirname(path), { recursive: true });
94
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
95
+ }