@ycodium-ai/plugin-session-import 0.2.2990 → 0.2.2992

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 (2) hide show
  1. package/dist/index.js +489 -487
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -165,7 +165,6 @@ var sessionImportSyncResultSchema = {
165
165
 
166
166
  // src/sessionImportRuntime.ts
167
167
  import * as NodeCrypto3 from "node:crypto";
168
- import * as NodeProcess2 from "node:process";
169
168
 
170
169
  // src/claudeSessionScan.ts
171
170
  import * as NodePath from "node:path";
@@ -962,144 +961,6 @@ async function claudeSessionModifiedAt(input) {
962
961
  }
963
962
  }
964
963
 
965
- // src/codexAppServer.ts
966
- var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
967
- var STDERR_TAIL_LIMIT = 2e3;
968
- var CODEX_INITIALIZE_PARAMS = {
969
- clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
970
- capabilities: { experimentalApi: true }
971
- };
972
- async function openCodexAppServer(input) {
973
- const handle = await input.process.spawn({
974
- program: input.program,
975
- args: ["app-server", ...input.args],
976
- ...input.env === void 0 ? {} : { env: input.env }
977
- });
978
- const pending = /* @__PURE__ */ new Map();
979
- const decoder = new TextDecoder();
980
- let nextId = 1;
981
- let stderrTail = "";
982
- let exitReason = null;
983
- const failAll = (reason) => {
984
- exitReason = reason;
985
- for (const [, entry] of pending) {
986
- entry.reject(new Error(`codex app-server ${entry.method} failed: ${reason}`));
987
- }
988
- pending.clear();
989
- };
990
- const dispatch = (line) => {
991
- if (line.trim().length === 0) return;
992
- let message;
993
- try {
994
- message = JSON.parse(line);
995
- } catch {
996
- return;
997
- }
998
- if (!message || typeof message !== "object") return;
999
- const typed = message;
1000
- if (typeof typed.id !== "number") return;
1001
- const entry = pending.get(typed.id);
1002
- if (entry === void 0) return;
1003
- pending.delete(typed.id);
1004
- if (typed.error !== void 0 && typed.error !== null) {
1005
- const detail = typeof typed.error.message === "string" ? typed.error.message : JSON.stringify(typed.error);
1006
- entry.reject(new Error(`codex app-server ${entry.method} failed: ${detail}`));
1007
- return;
1008
- }
1009
- entry.resolve(typed.result);
1010
- };
1011
- const pumpStdout = (async () => {
1012
- const fragments = [];
1013
- for await (const chunk of handle.stdout) {
1014
- const text = decoder.decode(chunk, { stream: true });
1015
- let start = 0;
1016
- for (let newline = text.indexOf("\n"); newline !== -1; newline = text.indexOf("\n", start)) {
1017
- fragments.push(text.slice(start, newline));
1018
- const line = fragments.join("");
1019
- fragments.length = 0;
1020
- dispatch(line);
1021
- start = newline + 1;
1022
- }
1023
- if (start < text.length) fragments.push(text.slice(start));
1024
- }
1025
- dispatch(fragments.join(""));
1026
- })().catch(() => void 0);
1027
- const pumpStderr = (async () => {
1028
- const errorDecoder = new TextDecoder();
1029
- for await (const chunk of handle.stderr) {
1030
- stderrTail = `${stderrTail}${errorDecoder.decode(chunk, { stream: true })}`.slice(
1031
- -STDERR_TAIL_LIMIT
1032
- );
1033
- }
1034
- })().catch(() => void 0);
1035
- void handle.exited.then((code) => {
1036
- failAll(
1037
- `the process exited with code ${String(code)}${stderrTail.length > 0 ? `: ${stderrTail.trim()}` : ""}`
1038
- );
1039
- }).catch(() => failAll("the process ended unexpectedly"));
1040
- const timeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1041
- const request = async (method, params) => {
1042
- if (exitReason !== null) throw new Error(`codex app-server ${method} failed: ${exitReason}`);
1043
- const id = nextId;
1044
- nextId += 1;
1045
- const settled = new Promise((resolve, reject) => {
1046
- pending.set(id, { resolve, reject, method });
1047
- });
1048
- try {
1049
- await Promise.race([
1050
- handle.writeStdin(
1051
- `${JSON.stringify({ id, method, ...params === void 0 ? {} : { params } })}
1052
- `
1053
- ),
1054
- settled
1055
- ]);
1056
- } catch (error) {
1057
- pending.delete(id);
1058
- throw error;
1059
- }
1060
- let timer;
1061
- try {
1062
- return await Promise.race([
1063
- settled,
1064
- new Promise((_, reject) => {
1065
- timer = setTimeout(() => {
1066
- pending.delete(id);
1067
- reject(new Error(`codex app-server ${method} timed out after ${String(timeoutMs)}ms`));
1068
- }, timeoutMs);
1069
- })
1070
- ]);
1071
- } finally {
1072
- if (timer !== void 0) clearTimeout(timer);
1073
- }
1074
- };
1075
- const notify = async (method, params) => {
1076
- await handle.writeStdin(
1077
- `${JSON.stringify({ method, ...params === void 0 ? {} : { params } })}
1078
- `
1079
- );
1080
- };
1081
- const close = async () => {
1082
- try {
1083
- await handle.closeStdin();
1084
- } catch {
1085
- }
1086
- try {
1087
- await handle.kill();
1088
- } catch {
1089
- }
1090
- failAll("the client was closed");
1091
- await Promise.all([pumpStdout, pumpStderr]);
1092
- };
1093
- try {
1094
- await request("initialize", CODEX_INITIALIZE_PARAMS);
1095
- await notify("initialized");
1096
- } catch (error) {
1097
- await close();
1098
- throw error;
1099
- }
1100
- return { request, notify, close, pendingCountForTests: () => pending.size };
1101
- }
1102
-
1103
964
  // src/codexSessionScan.ts
1104
965
  import * as NodePath2 from "node:path";
1105
966
  var PAGE_LIMIT = 50;
@@ -1468,102 +1329,498 @@ function reconcileImportedTimeline(input) {
1468
1329
  return { messages, activities, hasLegacyMatches, appendedMessageCount };
1469
1330
  }
1470
1331
 
1471
- // src/sessionTranscript.ts
1472
- import * as NodeCrypto2 from "node:crypto";
1332
+ // src/sessionImportListing.ts
1333
+ import * as NodeProcess2 from "node:process";
1473
1334
 
1474
- // src/wireTypes.ts
1475
- var SESSION_IMPORT_MESSAGE_LIMIT = 200;
1476
- var SessionImportFailure = class extends Error {
1477
- code;
1478
- constructor(code, message) {
1479
- super(message);
1480
- this.name = "SessionImportFailure";
1481
- this.code = code;
1482
- }
1335
+ // src/codexAppServer.ts
1336
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
1337
+ var STDERR_TAIL_LIMIT = 2e3;
1338
+ var CODEX_INITIALIZE_PARAMS = {
1339
+ clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
1340
+ capabilities: { experimentalApi: true }
1483
1341
  };
1484
-
1485
- // src/sessionTranscript.ts
1486
- var SESSION_IMPORT_ACTIVITY_LIMIT = 1e3;
1487
- var SESSION_IMPORT_ACTIVITY_TEXT_LIMIT = 32e3;
1488
- var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1489
- var nonempty = (value) => typeof value === "string" && value.trim().length > 0 ? value : void 0;
1490
- function nativeModel2(value) {
1491
- const model = nonempty(value);
1492
- return model?.trim() === "<synthetic>" ? void 0 : model;
1493
- }
1494
- function fallbackId(value) {
1495
- let text;
1496
- try {
1497
- text = JSON.stringify(value) ?? "";
1498
- } catch {
1499
- text = "";
1500
- }
1501
- return `record:${NodeCrypto2.createHash("sha256").update(text).digest("hex").slice(0, 20)}`;
1502
- }
1503
- function readableContent(value) {
1504
- if (typeof value === "string") return value;
1505
- if (!Array.isArray(value)) return "";
1506
- return value.flatMap((entry) => {
1507
- if (typeof entry === "string") return [entry];
1508
- const block = record2(entry);
1509
- return block && ["text", "input_text", "output_text", "summary_text", "reasoning_text"].includes(
1510
- String(block.type)
1511
- ) && typeof block.text === "string" ? [block.text] : [];
1512
- }).join("\n\n");
1513
- }
1514
- function toolInput(value) {
1515
- if (typeof value === "string") return value;
1516
- const input = record2(value);
1517
- if (input && Object.keys(input).length === 1 && typeof input.command === "string")
1518
- return input.command;
1519
- if (input && Object.keys(input).length === 1 && typeof input.cmd === "string") return input.cmd;
1520
- if (value === void 0 || value === null) return void 0;
1521
- try {
1522
- return JSON.stringify(value, null, 2);
1523
- } catch {
1524
- return void 0;
1525
- }
1526
- }
1527
- function toolType(name) {
1528
- const normalized = name.toLowerCase();
1529
- if (/^(bash|shell|command|terminal|exec_command|write_stdin)$/u.test(normalized))
1530
- return "command_execution";
1531
- if (/^(edit|write|multiedit|apply_patch|applypatch|patch)$/u.test(normalized))
1532
- return "file_change";
1533
- if (/^(agent|task|workflow)$/u.test(normalized)) return "dynamic_tool_call";
1534
- if (normalized.startsWith("mcp")) return "mcp_tool_call";
1535
- if (/^(websearch|webfetch|web_search)$/u.test(normalized)) return "web_search";
1536
- return "dynamic_tool_call";
1537
- }
1538
- function limit(messages, activities) {
1539
- const availableMessages = messages.length;
1540
- const retainedMessages = messages.slice(-SESSION_IMPORT_MESSAGE_LIMIT);
1541
- const floor = availableMessages > SESSION_IMPORT_MESSAGE_LIMIT ? retainedMessages[0]?.sourceOrder ?? 0 : 0;
1542
- const visibleActivities = activities.filter((activity) => activity.sourceOrder >= floor);
1543
- const retainedActivities = visibleActivities.slice(-SESSION_IMPORT_ACTIVITY_LIMIT);
1544
- return {
1545
- availableMessages,
1546
- availableActivities: activities.length,
1547
- ...retainedActivities.length < activities.length ? { activityTruncated: true } : {},
1548
- messages: retainedMessages,
1549
- activities: retainedActivities
1550
- };
1551
- }
1552
- function bounded(value, tail = false) {
1553
- if (value.length <= SESSION_IMPORT_ACTIVITY_TEXT_LIMIT) return { text: value, truncated: false };
1554
- const offset = tail ? value.length - SESSION_IMPORT_ACTIVITY_TEXT_LIMIT : SESSION_IMPORT_ACTIVITY_TEXT_LIMIT;
1555
- const character = value.charCodeAt(offset);
1556
- const previous = value.charCodeAt(offset - 1);
1557
- const splitPair = character >= 56320 && character <= 57343 && previous >= 55296 && previous <= 56319;
1558
- return {
1559
- text: tail ? value.slice(offset + (splitPair ? 1 : 0)) : value.slice(0, offset - (splitPair ? 1 : 0)),
1560
- truncated: true
1342
+ async function openCodexAppServer(input) {
1343
+ const handle = await input.process.spawn({
1344
+ program: input.program,
1345
+ args: ["app-server", ...input.args],
1346
+ ...input.env === void 0 ? {} : { env: input.env }
1347
+ });
1348
+ const pending = /* @__PURE__ */ new Map();
1349
+ const decoder = new TextDecoder();
1350
+ let nextId = 1;
1351
+ let stderrTail = "";
1352
+ let exitReason = null;
1353
+ const failAll = (reason) => {
1354
+ exitReason = reason;
1355
+ for (const [, entry] of pending) {
1356
+ entry.reject(new Error(`codex app-server ${entry.method} failed: ${reason}`));
1357
+ }
1358
+ pending.clear();
1561
1359
  };
1562
- }
1563
- function inputPayload(input) {
1564
- if (input === void 0) return {};
1565
- const result = bounded(input);
1566
- return { input: result.text, ...result.truncated ? { inputTruncated: true } : {} };
1360
+ const dispatch = (line) => {
1361
+ if (line.trim().length === 0) return;
1362
+ let message;
1363
+ try {
1364
+ message = JSON.parse(line);
1365
+ } catch {
1366
+ return;
1367
+ }
1368
+ if (!message || typeof message !== "object") return;
1369
+ const typed = message;
1370
+ if (typeof typed.id !== "number") return;
1371
+ const entry = pending.get(typed.id);
1372
+ if (entry === void 0) return;
1373
+ pending.delete(typed.id);
1374
+ if (typed.error !== void 0 && typed.error !== null) {
1375
+ const detail = typeof typed.error.message === "string" ? typed.error.message : JSON.stringify(typed.error);
1376
+ entry.reject(new Error(`codex app-server ${entry.method} failed: ${detail}`));
1377
+ return;
1378
+ }
1379
+ entry.resolve(typed.result);
1380
+ };
1381
+ const pumpStdout = (async () => {
1382
+ const fragments = [];
1383
+ for await (const chunk of handle.stdout) {
1384
+ const text = decoder.decode(chunk, { stream: true });
1385
+ let start = 0;
1386
+ for (let newline = text.indexOf("\n"); newline !== -1; newline = text.indexOf("\n", start)) {
1387
+ fragments.push(text.slice(start, newline));
1388
+ const line = fragments.join("");
1389
+ fragments.length = 0;
1390
+ dispatch(line);
1391
+ start = newline + 1;
1392
+ }
1393
+ if (start < text.length) fragments.push(text.slice(start));
1394
+ }
1395
+ dispatch(fragments.join(""));
1396
+ })().catch(() => void 0);
1397
+ const pumpStderr = (async () => {
1398
+ const errorDecoder = new TextDecoder();
1399
+ for await (const chunk of handle.stderr) {
1400
+ stderrTail = `${stderrTail}${errorDecoder.decode(chunk, { stream: true })}`.slice(
1401
+ -STDERR_TAIL_LIMIT
1402
+ );
1403
+ }
1404
+ })().catch(() => void 0);
1405
+ void handle.exited.then((code) => {
1406
+ failAll(
1407
+ `the process exited with code ${String(code)}${stderrTail.length > 0 ? `: ${stderrTail.trim()}` : ""}`
1408
+ );
1409
+ }).catch(() => failAll("the process ended unexpectedly"));
1410
+ const timeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1411
+ const request = async (method, params) => {
1412
+ if (exitReason !== null) throw new Error(`codex app-server ${method} failed: ${exitReason}`);
1413
+ const id = nextId;
1414
+ nextId += 1;
1415
+ const settled = new Promise((resolve, reject) => {
1416
+ pending.set(id, { resolve, reject, method });
1417
+ });
1418
+ try {
1419
+ await Promise.race([
1420
+ handle.writeStdin(
1421
+ `${JSON.stringify({ id, method, ...params === void 0 ? {} : { params } })}
1422
+ `
1423
+ ),
1424
+ settled
1425
+ ]);
1426
+ } catch (error) {
1427
+ pending.delete(id);
1428
+ throw error;
1429
+ }
1430
+ let timer;
1431
+ try {
1432
+ return await Promise.race([
1433
+ settled,
1434
+ new Promise((_, reject) => {
1435
+ timer = setTimeout(() => {
1436
+ pending.delete(id);
1437
+ reject(new Error(`codex app-server ${method} timed out after ${String(timeoutMs)}ms`));
1438
+ }, timeoutMs);
1439
+ })
1440
+ ]);
1441
+ } finally {
1442
+ if (timer !== void 0) clearTimeout(timer);
1443
+ }
1444
+ };
1445
+ const notify = async (method, params) => {
1446
+ await handle.writeStdin(
1447
+ `${JSON.stringify({ method, ...params === void 0 ? {} : { params } })}
1448
+ `
1449
+ );
1450
+ };
1451
+ const close = async () => {
1452
+ try {
1453
+ await handle.closeStdin();
1454
+ } catch {
1455
+ }
1456
+ try {
1457
+ await handle.kill();
1458
+ } catch {
1459
+ }
1460
+ failAll("the client was closed");
1461
+ await Promise.all([pumpStdout, pumpStderr]);
1462
+ };
1463
+ try {
1464
+ await request("initialize", CODEX_INITIALIZE_PARAMS);
1465
+ await notify("initialized");
1466
+ } catch (error) {
1467
+ await close();
1468
+ throw error;
1469
+ }
1470
+ return { request, notify, close, pendingCountForTests: () => pending.size };
1471
+ }
1472
+
1473
+ // src/wireTypes.ts
1474
+ var SESSION_IMPORT_MESSAGE_LIMIT = 200;
1475
+ var SessionImportFailure = class extends Error {
1476
+ code;
1477
+ constructor(code, message) {
1478
+ super(message);
1479
+ this.name = "SessionImportFailure";
1480
+ this.code = code;
1481
+ }
1482
+ };
1483
+
1484
+ // src/sessionImportListing.ts
1485
+ var CLAUDE_DRIVER = "claudeAgent";
1486
+ var CODEX_DRIVER = "codex";
1487
+ var providerKindOfDriver = (driverKind) => driverKind === CLAUDE_DRIVER ? "claude" : driverKind === CODEX_DRIVER ? "codex" : void 0;
1488
+ var importedThreadKey = (provider, nativeSessionId) => `${provider}:${nativeSessionId}`;
1489
+ var caseInsensitivePaths = () => NodeProcess2.platform === "win32";
1490
+ function tokenizeLaunchArgs(value) {
1491
+ const input = value.trim();
1492
+ if (input.length === 0) return [];
1493
+ const tokens = [];
1494
+ let current = "";
1495
+ let quote = null;
1496
+ let quoted = false;
1497
+ for (const char of input) {
1498
+ if (quote !== null) {
1499
+ if (char === quote) quote = null;
1500
+ else current += char;
1501
+ continue;
1502
+ }
1503
+ if (char === "'" || char === '"') {
1504
+ quote = char;
1505
+ quoted = true;
1506
+ continue;
1507
+ }
1508
+ if (/\s/u.test(char)) {
1509
+ if (current.length > 0 || quoted) tokens.push(current);
1510
+ current = "";
1511
+ quoted = false;
1512
+ continue;
1513
+ }
1514
+ current += char;
1515
+ }
1516
+ if (current.length > 0 || quoted) tokens.push(current);
1517
+ return tokens;
1518
+ }
1519
+ var nativeSessionIdOf = (provider, cursor) => {
1520
+ if (cursor === null || typeof cursor !== "object") return null;
1521
+ const record3 = cursor;
1522
+ const value = provider === "claude" ? record3.resume ?? record3.sessionId : record3.threadId;
1523
+ return typeof value === "string" && value.length > 0 ? value : null;
1524
+ };
1525
+ async function readImportedThreads(ctx) {
1526
+ const imported = /* @__PURE__ */ new Map();
1527
+ const bindings = await ctx.providers.sessions.list().catch(() => []);
1528
+ for (const binding of bindings) {
1529
+ const provider = providerKindOfDriver(binding.driver);
1530
+ if (provider === void 0) continue;
1531
+ const nativeId = nativeSessionIdOf(provider, binding.resumeCursor);
1532
+ if (nativeId === null) continue;
1533
+ imported.set(importedThreadKey(provider, nativeId), binding.threadId);
1534
+ }
1535
+ return imported;
1536
+ }
1537
+ var candidateOf = (input) => ({
1538
+ provider: input.provider,
1539
+ instanceId: input.instanceId,
1540
+ nativeSessionId: input.nativeSessionId,
1541
+ cwd: input.cwd,
1542
+ title: input.title,
1543
+ preview: input.preview,
1544
+ model: input.model,
1545
+ turnCount: input.turnCount,
1546
+ updatedAt: input.updatedAt,
1547
+ availability: input.importedThreadId !== void 0 ? "alreadyImported" : "importable",
1548
+ openElsewhere: input.openElsewhere,
1549
+ ...input.importedThreadId !== void 0 ? { importedThreadId: input.importedThreadId } : {}
1550
+ });
1551
+ async function openCodexFor(ctx, instance) {
1552
+ const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
1553
+ if (binaryPath === void 0 || binaryPath.length === 0) {
1554
+ throw new Error(`No codex binary is configured for provider instance ${instance.instanceId}.`);
1555
+ }
1556
+ return openCodexAppServer({
1557
+ process: ctx.process,
1558
+ // 白名单条目的键就是 instanceId:路径由宿主查,插件手上不留第二份。
1559
+ program: { key: instance.instanceId },
1560
+ args: [...tokenizeLaunchArgs(instance.launchArgs.trim())],
1561
+ // 转录始终在 CODEX_HOME 下;实例的 home 是宿主解析过的那个(含 auth overlay)。
1562
+ ...instance.homePath === null ? {} : { env: { CODEX_HOME: instance.homePath } }
1563
+ });
1564
+ }
1565
+ async function withCodex(ctx, instance, use) {
1566
+ const client = await openCodexFor(ctx, instance);
1567
+ try {
1568
+ return await use(client);
1569
+ } finally {
1570
+ await client.close();
1571
+ }
1572
+ }
1573
+ async function nativeSessionsOf(ctx, instanceId) {
1574
+ const resolve = ctx.providers.nativeSessions;
1575
+ if (typeof resolve !== "function") return null;
1576
+ try {
1577
+ return await resolve(instanceId);
1578
+ } catch (error) {
1579
+ ctx.log.warn("session-import could not reach the executor's own session scan", {
1580
+ instanceId,
1581
+ detail: describeError(error)
1582
+ });
1583
+ return null;
1584
+ }
1585
+ }
1586
+ async function listSessions(ctx, input) {
1587
+ const project = await ctx.projections.getProject(input.projectId);
1588
+ if (project === null) {
1589
+ throw new SessionImportFailure("notFound", "Project not found.");
1590
+ }
1591
+ const directory = input.directory ?? project.workspaceRoot;
1592
+ const caseInsensitive = caseInsensitivePaths();
1593
+ const instances = await ctx.providers.listInstances();
1594
+ const imported = await readImportedThreads(ctx);
1595
+ const sources = [];
1596
+ const scannedKeys = /* @__PURE__ */ new Set();
1597
+ for (const instance of instances) {
1598
+ if (!instance.enabled) continue;
1599
+ const provider = providerKindOfDriver(instance.driver);
1600
+ if (provider === void 0) continue;
1601
+ const hand = await nativeSessionsOf(ctx, instance.instanceId);
1602
+ if (hand !== null) {
1603
+ const scanKey2 = `hand:${instance.driver}:${instance.homePath ?? instance.instanceId}`;
1604
+ if (scannedKeys.has(scanKey2)) continue;
1605
+ scannedKeys.add(scanKey2);
1606
+ try {
1607
+ const scanned = await hand.scan({ projectId: input.projectId, directory });
1608
+ sources.push({
1609
+ instanceId: instance.instanceId,
1610
+ provider,
1611
+ candidates: scanned.candidates.map(
1612
+ (row) => candidateOf({
1613
+ provider,
1614
+ instanceId: instance.instanceId,
1615
+ nativeSessionId: row.nativeSessionId,
1616
+ cwd: row.cwd,
1617
+ title: row.title,
1618
+ preview: row.preview,
1619
+ model: row.model,
1620
+ turnCount: row.turnCount,
1621
+ updatedAt: row.updatedAt,
1622
+ openElsewhere: row.openElsewhere,
1623
+ // 「已经有 thread 绑着它」是宿主的账,手报不出来,一律盖上去。
1624
+ importedThreadId: imported.get(importedThreadKey(provider, row.nativeSessionId))
1625
+ })
1626
+ ),
1627
+ unavailableReason: null
1628
+ });
1629
+ } catch (error) {
1630
+ ctx.log.warn("session-import could not read sessions from the executor", {
1631
+ instanceId: instance.instanceId,
1632
+ detail: describeError(error)
1633
+ });
1634
+ sources.push({
1635
+ instanceId: instance.instanceId,
1636
+ provider,
1637
+ candidates: [],
1638
+ unavailableReason: provider === "codex" ? "Codex sessions could not be read." : "Claude sessions could not be read."
1639
+ });
1640
+ }
1641
+ continue;
1642
+ }
1643
+ if (instance.driver === CODEX_DRIVER) {
1644
+ const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
1645
+ if (binaryPath === void 0) continue;
1646
+ const scanKey2 = `codex:${instance.homePath ?? ""}:${binaryPath}`;
1647
+ if (scannedKeys.has(scanKey2)) continue;
1648
+ scannedKeys.add(scanKey2);
1649
+ try {
1650
+ const rows = await withCodex(
1651
+ ctx,
1652
+ instance,
1653
+ (client) => scanCodexSessions({ client, directory, caseInsensitive })
1654
+ );
1655
+ sources.push({
1656
+ instanceId: instance.instanceId,
1657
+ provider: "codex",
1658
+ candidates: rows.map(
1659
+ (row) => candidateOf({
1660
+ provider: "codex",
1661
+ instanceId: instance.instanceId,
1662
+ nativeSessionId: row.threadId,
1663
+ cwd: row.cwd,
1664
+ title: row.title,
1665
+ preview: row.preview,
1666
+ model: row.model,
1667
+ turnCount: row.turnCount,
1668
+ updatedAt: row.updatedAt,
1669
+ // Codex 判不了活,双开由它自己在 resume 上挡。
1670
+ openElsewhere: false,
1671
+ importedThreadId: imported.get(importedThreadKey("codex", row.threadId))
1672
+ })
1673
+ ),
1674
+ unavailableReason: null
1675
+ });
1676
+ } catch (error) {
1677
+ ctx.log.warn("session-import could not read codex sessions", {
1678
+ instanceId: instance.instanceId,
1679
+ detail: describeError(error)
1680
+ });
1681
+ sources.push({
1682
+ instanceId: instance.instanceId,
1683
+ provider: "codex",
1684
+ candidates: [],
1685
+ unavailableReason: "Codex sessions could not be read."
1686
+ });
1687
+ }
1688
+ continue;
1689
+ }
1690
+ if (instance.driver !== CLAUDE_DRIVER) continue;
1691
+ if (instance.transcriptDir === null || instance.configDirPath === null) continue;
1692
+ const scanKey = `claude:${instance.transcriptDir}`;
1693
+ if (scannedKeys.has(scanKey)) continue;
1694
+ scannedKeys.add(scanKey);
1695
+ try {
1696
+ const rows = await scanClaudeSessions({
1697
+ fs: ctx.fs,
1698
+ cache: ctx.cache,
1699
+ transcriptDir: instance.transcriptDir,
1700
+ configDir: instance.configDirPath,
1701
+ directory,
1702
+ caseInsensitive
1703
+ });
1704
+ sources.push({
1705
+ instanceId: instance.instanceId,
1706
+ provider: "claude",
1707
+ candidates: rows.map(
1708
+ (row) => candidateOf({
1709
+ provider: "claude",
1710
+ instanceId: instance.instanceId,
1711
+ nativeSessionId: row.sessionId,
1712
+ cwd: row.cwd,
1713
+ title: row.title,
1714
+ preview: row.preview,
1715
+ model: row.model,
1716
+ turnCount: row.turnCount,
1717
+ updatedAt: row.updatedAt,
1718
+ openElsewhere: row.openElsewhere,
1719
+ importedThreadId: imported.get(importedThreadKey("claude", row.sessionId))
1720
+ })
1721
+ ),
1722
+ unavailableReason: null
1723
+ });
1724
+ } catch (error) {
1725
+ ctx.log.warn("session-import could not read claude sessions", {
1726
+ instanceId: instance.instanceId,
1727
+ detail: describeError(error)
1728
+ });
1729
+ sources.push({
1730
+ instanceId: instance.instanceId,
1731
+ provider: "claude",
1732
+ candidates: [],
1733
+ unavailableReason: "Claude sessions could not be read."
1734
+ });
1735
+ }
1736
+ }
1737
+ return { sources, directory };
1738
+ }
1739
+ var describeError = (error) => error instanceof Error ? error.message : String(error);
1740
+
1741
+ // src/sessionTranscript.ts
1742
+ import * as NodeCrypto2 from "node:crypto";
1743
+ var SESSION_IMPORT_ACTIVITY_LIMIT = 1e3;
1744
+ var SESSION_IMPORT_ACTIVITY_TEXT_LIMIT = 32e3;
1745
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1746
+ var nonempty = (value) => typeof value === "string" && value.trim().length > 0 ? value : void 0;
1747
+ function nativeModel2(value) {
1748
+ const model = nonempty(value);
1749
+ return model?.trim() === "<synthetic>" ? void 0 : model;
1750
+ }
1751
+ function fallbackId(value) {
1752
+ let text;
1753
+ try {
1754
+ text = JSON.stringify(value) ?? "";
1755
+ } catch {
1756
+ text = "";
1757
+ }
1758
+ return `record:${NodeCrypto2.createHash("sha256").update(text).digest("hex").slice(0, 20)}`;
1759
+ }
1760
+ function readableContent(value) {
1761
+ if (typeof value === "string") return value;
1762
+ if (!Array.isArray(value)) return "";
1763
+ return value.flatMap((entry) => {
1764
+ if (typeof entry === "string") return [entry];
1765
+ const block = record2(entry);
1766
+ return block && ["text", "input_text", "output_text", "summary_text", "reasoning_text"].includes(
1767
+ String(block.type)
1768
+ ) && typeof block.text === "string" ? [block.text] : [];
1769
+ }).join("\n\n");
1770
+ }
1771
+ function toolInput(value) {
1772
+ if (typeof value === "string") return value;
1773
+ const input = record2(value);
1774
+ if (input && Object.keys(input).length === 1 && typeof input.command === "string")
1775
+ return input.command;
1776
+ if (input && Object.keys(input).length === 1 && typeof input.cmd === "string") return input.cmd;
1777
+ if (value === void 0 || value === null) return void 0;
1778
+ try {
1779
+ return JSON.stringify(value, null, 2);
1780
+ } catch {
1781
+ return void 0;
1782
+ }
1783
+ }
1784
+ function toolType(name) {
1785
+ const normalized = name.toLowerCase();
1786
+ if (/^(bash|shell|command|terminal|exec_command|write_stdin)$/u.test(normalized))
1787
+ return "command_execution";
1788
+ if (/^(edit|write|multiedit|apply_patch|applypatch|patch)$/u.test(normalized))
1789
+ return "file_change";
1790
+ if (/^(agent|task|workflow)$/u.test(normalized)) return "dynamic_tool_call";
1791
+ if (normalized.startsWith("mcp")) return "mcp_tool_call";
1792
+ if (/^(websearch|webfetch|web_search)$/u.test(normalized)) return "web_search";
1793
+ return "dynamic_tool_call";
1794
+ }
1795
+ function limit(messages, activities) {
1796
+ const availableMessages = messages.length;
1797
+ const retainedMessages = messages.slice(-SESSION_IMPORT_MESSAGE_LIMIT);
1798
+ const floor = availableMessages > SESSION_IMPORT_MESSAGE_LIMIT ? retainedMessages[0]?.sourceOrder ?? 0 : 0;
1799
+ const visibleActivities = activities.filter((activity) => activity.sourceOrder >= floor);
1800
+ const retainedActivities = visibleActivities.slice(-SESSION_IMPORT_ACTIVITY_LIMIT);
1801
+ return {
1802
+ availableMessages,
1803
+ availableActivities: activities.length,
1804
+ ...retainedActivities.length < activities.length ? { activityTruncated: true } : {},
1805
+ messages: retainedMessages,
1806
+ activities: retainedActivities
1807
+ };
1808
+ }
1809
+ function bounded(value, tail = false) {
1810
+ if (value.length <= SESSION_IMPORT_ACTIVITY_TEXT_LIMIT) return { text: value, truncated: false };
1811
+ const offset = tail ? value.length - SESSION_IMPORT_ACTIVITY_TEXT_LIMIT : SESSION_IMPORT_ACTIVITY_TEXT_LIMIT;
1812
+ const character = value.charCodeAt(offset);
1813
+ const previous = value.charCodeAt(offset - 1);
1814
+ const splitPair = character >= 56320 && character <= 57343 && previous >= 55296 && previous <= 56319;
1815
+ return {
1816
+ text: tail ? value.slice(offset + (splitPair ? 1 : 0)) : value.slice(0, offset - (splitPair ? 1 : 0)),
1817
+ truncated: true
1818
+ };
1819
+ }
1820
+ function inputPayload(input) {
1821
+ if (input === void 0) return {};
1822
+ const result = bounded(input);
1823
+ return { input: result.text, ...result.truncated ? { inputTruncated: true } : {} };
1567
1824
  }
1568
1825
  function outputPayload(output) {
1569
1826
  if (output === void 0) return {};
@@ -1891,261 +2148,6 @@ var SKIPPED_ACTIVE = {
1891
2148
  appended: 0
1892
2149
  };
1893
2150
  var IMPORTED_TITLE_MAX_LENGTH = 80;
1894
- var CLAUDE_DRIVER = "claudeAgent";
1895
- var CODEX_DRIVER = "codex";
1896
- var providerKindOfDriver = (driverKind) => driverKind === CLAUDE_DRIVER ? "claude" : driverKind === CODEX_DRIVER ? "codex" : void 0;
1897
- var importedThreadKey = (provider, nativeSessionId) => `${provider}:${nativeSessionId}`;
1898
- var caseInsensitivePaths = () => NodeProcess2.platform === "win32";
1899
- function tokenizeLaunchArgs(value) {
1900
- const input = value.trim();
1901
- if (input.length === 0) return [];
1902
- const tokens = [];
1903
- let current = "";
1904
- let quote = null;
1905
- let quoted = false;
1906
- for (const char of input) {
1907
- if (quote !== null) {
1908
- if (char === quote) quote = null;
1909
- else current += char;
1910
- continue;
1911
- }
1912
- if (char === "'" || char === '"') {
1913
- quote = char;
1914
- quoted = true;
1915
- continue;
1916
- }
1917
- if (/\s/u.test(char)) {
1918
- if (current.length > 0 || quoted) tokens.push(current);
1919
- current = "";
1920
- quoted = false;
1921
- continue;
1922
- }
1923
- current += char;
1924
- }
1925
- if (current.length > 0 || quoted) tokens.push(current);
1926
- return tokens;
1927
- }
1928
- var nativeSessionIdOf = (provider, cursor) => {
1929
- if (cursor === null || typeof cursor !== "object") return null;
1930
- const record3 = cursor;
1931
- const value = provider === "claude" ? record3.resume ?? record3.sessionId : record3.threadId;
1932
- return typeof value === "string" && value.length > 0 ? value : null;
1933
- };
1934
- async function readImportedThreads(ctx) {
1935
- const imported = /* @__PURE__ */ new Map();
1936
- const bindings = await ctx.providers.sessions.list().catch(() => []);
1937
- for (const binding of bindings) {
1938
- const provider = providerKindOfDriver(binding.driver);
1939
- if (provider === void 0) continue;
1940
- const nativeId = nativeSessionIdOf(provider, binding.resumeCursor);
1941
- if (nativeId === null) continue;
1942
- imported.set(importedThreadKey(provider, nativeId), binding.threadId);
1943
- }
1944
- return imported;
1945
- }
1946
- var candidateOf = (input) => ({
1947
- provider: input.provider,
1948
- instanceId: input.instanceId,
1949
- nativeSessionId: input.nativeSessionId,
1950
- cwd: input.cwd,
1951
- title: input.title,
1952
- preview: input.preview,
1953
- model: input.model,
1954
- turnCount: input.turnCount,
1955
- updatedAt: input.updatedAt,
1956
- availability: input.importedThreadId !== void 0 ? "alreadyImported" : "importable",
1957
- openElsewhere: input.openElsewhere,
1958
- ...input.importedThreadId !== void 0 ? { importedThreadId: input.importedThreadId } : {}
1959
- });
1960
- async function openCodexFor(ctx, instance) {
1961
- const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
1962
- if (binaryPath === void 0 || binaryPath.length === 0) {
1963
- throw new Error(`No codex binary is configured for provider instance ${instance.instanceId}.`);
1964
- }
1965
- return openCodexAppServer({
1966
- process: ctx.process,
1967
- // 白名单条目的键就是 instanceId:路径由宿主查,插件手上不留第二份。
1968
- program: { key: instance.instanceId },
1969
- args: [...tokenizeLaunchArgs(instance.launchArgs.trim())],
1970
- // 转录始终在 CODEX_HOME 下;实例的 home 是宿主解析过的那个(含 auth overlay)。
1971
- ...instance.homePath === null ? {} : { env: { CODEX_HOME: instance.homePath } }
1972
- });
1973
- }
1974
- async function withCodex(ctx, instance, use) {
1975
- const client = await openCodexFor(ctx, instance);
1976
- try {
1977
- return await use(client);
1978
- } finally {
1979
- await client.close();
1980
- }
1981
- }
1982
- async function nativeSessionsOf(ctx, instanceId) {
1983
- const resolve = ctx.providers.nativeSessions;
1984
- if (typeof resolve !== "function") return null;
1985
- try {
1986
- return await resolve(instanceId);
1987
- } catch (error) {
1988
- ctx.log.warn("session-import could not reach the executor's own session scan", {
1989
- instanceId,
1990
- detail: describeError(error)
1991
- });
1992
- return null;
1993
- }
1994
- }
1995
- async function listSessions(ctx, input) {
1996
- const project = await ctx.projections.getProject(input.projectId);
1997
- if (project === null) {
1998
- throw new SessionImportFailure("notFound", "Project not found.");
1999
- }
2000
- const directory = input.directory ?? project.workspaceRoot;
2001
- const caseInsensitive = caseInsensitivePaths();
2002
- const instances = await ctx.providers.listInstances();
2003
- const imported = await readImportedThreads(ctx);
2004
- const sources = [];
2005
- const scannedKeys = /* @__PURE__ */ new Set();
2006
- for (const instance of instances) {
2007
- if (!instance.enabled) continue;
2008
- const provider = providerKindOfDriver(instance.driver);
2009
- if (provider === void 0) continue;
2010
- const hand = await nativeSessionsOf(ctx, instance.instanceId);
2011
- if (hand !== null) {
2012
- const scanKey2 = `hand:${instance.driver}:${instance.homePath ?? instance.instanceId}`;
2013
- if (scannedKeys.has(scanKey2)) continue;
2014
- scannedKeys.add(scanKey2);
2015
- try {
2016
- const scanned = await hand.scan({ projectId: input.projectId, directory });
2017
- sources.push({
2018
- instanceId: instance.instanceId,
2019
- provider,
2020
- candidates: scanned.candidates.map(
2021
- (row) => candidateOf({
2022
- provider,
2023
- instanceId: instance.instanceId,
2024
- nativeSessionId: row.nativeSessionId,
2025
- cwd: row.cwd,
2026
- title: row.title,
2027
- preview: row.preview,
2028
- model: row.model,
2029
- turnCount: row.turnCount,
2030
- updatedAt: row.updatedAt,
2031
- openElsewhere: row.openElsewhere,
2032
- // 「已经有 thread 绑着它」是宿主的账,手报不出来,一律盖上去。
2033
- importedThreadId: imported.get(importedThreadKey(provider, row.nativeSessionId))
2034
- })
2035
- ),
2036
- unavailableReason: null
2037
- });
2038
- } catch (error) {
2039
- ctx.log.warn("session-import could not read sessions from the executor", {
2040
- instanceId: instance.instanceId,
2041
- detail: describeError(error)
2042
- });
2043
- sources.push({
2044
- instanceId: instance.instanceId,
2045
- provider,
2046
- candidates: [],
2047
- unavailableReason: provider === "codex" ? "Codex sessions could not be read." : "Claude sessions could not be read."
2048
- });
2049
- }
2050
- continue;
2051
- }
2052
- if (instance.driver === CODEX_DRIVER) {
2053
- const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
2054
- if (binaryPath === void 0) continue;
2055
- const scanKey2 = `codex:${instance.homePath ?? ""}:${binaryPath}`;
2056
- if (scannedKeys.has(scanKey2)) continue;
2057
- scannedKeys.add(scanKey2);
2058
- try {
2059
- const rows = await withCodex(
2060
- ctx,
2061
- instance,
2062
- (client) => scanCodexSessions({ client, directory, caseInsensitive })
2063
- );
2064
- sources.push({
2065
- instanceId: instance.instanceId,
2066
- provider: "codex",
2067
- candidates: rows.map(
2068
- (row) => candidateOf({
2069
- provider: "codex",
2070
- instanceId: instance.instanceId,
2071
- nativeSessionId: row.threadId,
2072
- cwd: row.cwd,
2073
- title: row.title,
2074
- preview: row.preview,
2075
- model: row.model,
2076
- turnCount: row.turnCount,
2077
- updatedAt: row.updatedAt,
2078
- // Codex 判不了活,双开由它自己在 resume 上挡。
2079
- openElsewhere: false,
2080
- importedThreadId: imported.get(importedThreadKey("codex", row.threadId))
2081
- })
2082
- ),
2083
- unavailableReason: null
2084
- });
2085
- } catch (error) {
2086
- ctx.log.warn("session-import could not read codex sessions", {
2087
- instanceId: instance.instanceId,
2088
- detail: describeError(error)
2089
- });
2090
- sources.push({
2091
- instanceId: instance.instanceId,
2092
- provider: "codex",
2093
- candidates: [],
2094
- unavailableReason: "Codex sessions could not be read."
2095
- });
2096
- }
2097
- continue;
2098
- }
2099
- if (instance.driver !== CLAUDE_DRIVER) continue;
2100
- if (instance.transcriptDir === null || instance.configDirPath === null) continue;
2101
- const scanKey = `claude:${instance.transcriptDir}`;
2102
- if (scannedKeys.has(scanKey)) continue;
2103
- scannedKeys.add(scanKey);
2104
- try {
2105
- const rows = await scanClaudeSessions({
2106
- fs: ctx.fs,
2107
- cache: ctx.cache,
2108
- transcriptDir: instance.transcriptDir,
2109
- configDir: instance.configDirPath,
2110
- directory,
2111
- caseInsensitive
2112
- });
2113
- sources.push({
2114
- instanceId: instance.instanceId,
2115
- provider: "claude",
2116
- candidates: rows.map(
2117
- (row) => candidateOf({
2118
- provider: "claude",
2119
- instanceId: instance.instanceId,
2120
- nativeSessionId: row.sessionId,
2121
- cwd: row.cwd,
2122
- title: row.title,
2123
- preview: row.preview,
2124
- model: row.model,
2125
- turnCount: row.turnCount,
2126
- updatedAt: row.updatedAt,
2127
- openElsewhere: row.openElsewhere,
2128
- importedThreadId: imported.get(importedThreadKey("claude", row.sessionId))
2129
- })
2130
- ),
2131
- unavailableReason: null
2132
- });
2133
- } catch (error) {
2134
- ctx.log.warn("session-import could not read claude sessions", {
2135
- instanceId: instance.instanceId,
2136
- detail: describeError(error)
2137
- });
2138
- sources.push({
2139
- instanceId: instance.instanceId,
2140
- provider: "claude",
2141
- candidates: [],
2142
- unavailableReason: "Claude sessions could not be read."
2143
- });
2144
- }
2145
- }
2146
- return { sources, directory };
2147
- }
2148
- var describeError = (error) => error instanceof Error ? error.message : String(error);
2149
2151
  function resolveImportedTitle(transcript) {
2150
2152
  const first = transcript.messages.find((message) => message.role === "user");
2151
2153
  const text = (first?.text ?? "").replace(/\s+/gu, " ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-session-import",
3
- "version": "0.2.2990",
3
+ "version": "0.2.2992",
4
4
  "description": "Brings a session you started in a terminal into Ycodium.",
5
5
  "keywords": [
6
6
  "session-import",
@@ -20,7 +20,7 @@
20
20
  "effect": "4.0.0-beta.103",
21
21
  "esbuild": "0.28.1",
22
22
  "vite-plus": "0.2.2",
23
- "@ycodium-ai/plugin-api": "0.2.2990",
23
+ "@ycodium-ai/plugin-api": "0.2.2992",
24
24
  "ycodium-jsonl-stream": "0.0.0"
25
25
  },
26
26
  "ycodium": {