@rexvane/piweb 0.3.12 → 0.3.14

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 (177) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -7
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/react-loadable-manifest.json +2 -2
  6. package/.next/routes-manifest.json +6 -0
  7. package/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page.js +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  22. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  23. package/.next/server/app/api/agent/[id]/route.js +1 -1
  24. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/new/route.js +1 -1
  28. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  29. package/.next/server/app/api/config/route.js +1 -1
  30. package/.next/server/app/api/files/route.js +1 -1
  31. package/.next/server/app/api/git/route.js +2 -2
  32. package/.next/server/app/api/growth/route.js +1 -1
  33. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  34. package/.next/server/app/api/health/route.js +1 -1
  35. package/.next/server/app/api/models/route.js +1 -1
  36. package/.next/server/app/api/models/route.js.nft.json +1 -1
  37. package/.next/server/app/api/pi-settings/route.js +1 -1
  38. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  39. package/.next/server/app/api/plugins/route.js +1 -1
  40. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  41. package/.next/server/app/api/prompts/route.js +1 -1
  42. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  43. package/.next/server/app/api/security/route.js +1 -1
  44. package/.next/server/app/api/security/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sessions/import/route.js +6 -0
  50. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -0
  51. package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -0
  52. package/.next/server/app/api/sessions/route.js +1 -1
  53. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  54. package/.next/server/app/api/skills/route.js +2 -2
  55. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  56. package/.next/server/app/api/update/route.js +2 -2
  57. package/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/server/app/api/version/route.js +1 -1
  59. package/.next/server/app/api/web-auth/route.js +1 -1
  60. package/.next/server/app/api/workspaces/route.js +1 -1
  61. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  62. package/.next/server/app/icon.svg/route.js +1 -1
  63. package/.next/server/app/index.html +1 -1
  64. package/.next/server/app/index.rsc +3 -3
  65. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  66. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  67. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  68. package/.next/server/app/login/page.js +1 -1
  69. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/login.html +1 -1
  71. package/.next/server/app/login.rsc +2 -2
  72. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  73. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  75. package/.next/server/app/page.js +1 -1
  76. package/.next/server/app/page_client-reference-manifest.js +1 -1
  77. package/.next/server/app-paths-manifest.json +8 -7
  78. package/.next/server/chunks/496.js +1 -1
  79. package/.next/server/chunks/532.js +1 -0
  80. package/.next/server/chunks/686.js +2 -2
  81. package/.next/server/middleware-build-manifest.js +1 -1
  82. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  83. package/.next/server/pages/404.html +1 -1
  84. package/.next/server/pages/500.html +1 -1
  85. package/.next/server/server-reference-manifest.json +1 -1
  86. package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +1 -0
  87. package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
  88. package/.next/static/chunks/4686-7d694def920ff093.js +1 -0
  89. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  90. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  91. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  92. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  93. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  94. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  95. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  96. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  97. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/page-9a5440047c5a415c.js +1 -0
  114. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/{webpack-05a9e85529a18b93.js → webpack-d28e132e1b8c100c.js} +1 -1
  119. package/.next/types/app/api/sessions/import/route.ts +353 -0
  120. package/.next/types/routes.d.ts +2 -1
  121. package/.next/types/validator.ts +9 -0
  122. package/README.md +6 -2
  123. package/package.json +1 -1
  124. package/src/app/api/sessions/import/route.ts +74 -0
  125. package/src/components/PromptPanel.tsx +33 -31
  126. package/src/components/SessionImportSection.tsx +370 -0
  127. package/src/components/SettingsPanel.tsx +4 -1
  128. package/src/i18n.tsx +68 -6
  129. package/src/lib/prompt-sources.ts +29 -1
  130. package/src/lib/session-import/claude.ts +229 -0
  131. package/src/lib/session-import/codex.ts +223 -0
  132. package/src/lib/session-import/dsh-zstd.ts +69 -0
  133. package/src/lib/session-import/dsh.ts +259 -0
  134. package/src/lib/session-import/files.ts +84 -0
  135. package/src/lib/session-import/grok.ts +169 -0
  136. package/src/lib/session-import/index.ts +229 -0
  137. package/src/lib/session-import/oc-family.ts +275 -0
  138. package/src/lib/session-import/opencode.ts +7 -0
  139. package/src/lib/session-import/paths.ts +37 -0
  140. package/src/lib/session-import/sqlite.ts +89 -0
  141. package/src/lib/session-import/types.ts +171 -0
  142. package/src/lib/session-import/writer.ts +259 -0
  143. package/src/lib/session-import/zcode.ts +8 -0
  144. package/src/lib/skills-service.ts +51 -14
  145. package/.next/server/chunks/118.js +0 -1
  146. package/.next/static/2kt2UiWjv_viXpNlfjvBT/_buildManifest.js +0 -1
  147. package/.next/static/chunks/4127.dff5c3b33c6bfd1b.js +0 -1
  148. package/.next/static/chunks/4686-5bbee099126af797.js +0 -1
  149. package/.next/static/chunks/app/_global-error/page-961c055baebb3872.js +0 -1
  150. package/.next/static/chunks/app/api/agent/[id]/events/route-961c055baebb3872.js +0 -1
  151. package/.next/static/chunks/app/api/agent/[id]/route-961c055baebb3872.js +0 -1
  152. package/.next/static/chunks/app/api/agent/[id]/tree/route-961c055baebb3872.js +0 -1
  153. package/.next/static/chunks/app/api/agent/new/route-961c055baebb3872.js +0 -1
  154. package/.next/static/chunks/app/api/config/route-961c055baebb3872.js +0 -1
  155. package/.next/static/chunks/app/api/files/route-961c055baebb3872.js +0 -1
  156. package/.next/static/chunks/app/api/git/route-961c055baebb3872.js +0 -1
  157. package/.next/static/chunks/app/api/growth/route-961c055baebb3872.js +0 -1
  158. package/.next/static/chunks/app/api/health/route-961c055baebb3872.js +0 -1
  159. package/.next/static/chunks/app/api/models/route-961c055baebb3872.js +0 -1
  160. package/.next/static/chunks/app/api/pi-settings/route-961c055baebb3872.js +0 -1
  161. package/.next/static/chunks/app/api/plugins/route-961c055baebb3872.js +0 -1
  162. package/.next/static/chunks/app/api/prompts/route-961c055baebb3872.js +0 -1
  163. package/.next/static/chunks/app/api/security/route-961c055baebb3872.js +0 -1
  164. package/.next/static/chunks/app/api/sessions/[id]/export/route-961c055baebb3872.js +0 -1
  165. package/.next/static/chunks/app/api/sessions/[id]/route-961c055baebb3872.js +0 -1
  166. package/.next/static/chunks/app/api/sessions/route-961c055baebb3872.js +0 -1
  167. package/.next/static/chunks/app/api/skills/route-961c055baebb3872.js +0 -1
  168. package/.next/static/chunks/app/api/update/route-961c055baebb3872.js +0 -1
  169. package/.next/static/chunks/app/api/version/route-961c055baebb3872.js +0 -1
  170. package/.next/static/chunks/app/api/web-auth/route-961c055baebb3872.js +0 -1
  171. package/.next/static/chunks/app/api/workspaces/route-961c055baebb3872.js +0 -1
  172. package/.next/static/chunks/app/page-00c8ebf40ec5eaca.js +0 -1
  173. package/.next/static/chunks/next/dist/client/components/builtin/app-error-961c055baebb3872.js +0 -1
  174. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-961c055baebb3872.js +0 -1
  175. package/.next/static/chunks/next/dist/client/components/builtin/not-found-961c055baebb3872.js +0 -1
  176. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-961c055baebb3872.js +0 -1
  177. /package/.next/static/{2kt2UiWjv_viXpNlfjvBT → LOFl9bqqMV2goBV8erESO}/_ssgManifest.js +0 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Claude Code(~/.claude/projects/<slug>/*.jsonl)
3
+ *
4
+ * 每行自带 type,无统一头行;消息行是 {type:user|assistant, message:{role,model,content[],usage,stop_reason}}。
5
+ * content 块有 text / thinking / tool_use / tool_result / image;工具结果出现在后续 user 行里,
6
+ * 靠 tool_use_id 与前面的 tool_use 配对(这一步交给 writer 兜底修复)。
7
+ */
8
+ import path from "node:path";
9
+ import fs from "node:fs/promises";
10
+ import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
11
+ import { claudeRoot } from "./paths";
12
+ import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedMessage, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
13
+ import { cleanTitle, compactBlocks, outputBlocks, textBlock, toEpochMs } from "./types";
14
+
15
+ const ROOT = claudeRoot;
16
+
17
+ /** 合成信封(命令输出、系统提醒等)不是真人输入,导入时跳过 */
18
+ const SYNTHETIC_PREFIXES = ["<command-name>", "<command-message>", "<local-command-stdout>", "<local-command-caveat>", "<system-reminder>", "<user_info>", "<bash-input>", "<bash-stdout>"];
19
+
20
+ const isSynthetic = (text: string) => SYNTHETIC_PREFIXES.some((prefix) => text.startsWith(prefix));
21
+
22
+ /**
23
+ * 子代理的转写不能当作会话:Claude Code 把它们放在
24
+ * `<项目>/<会话 uuid>/subagents/` 下(含更深的 workflows/wf_<id>/ 层,本机 236 个子代理文件 vs 70 个主会话)。
25
+ * 那是主代理派出去的活,不是你在用的那条对话。
26
+ * 这些文件里的记录全是 isSidechain,本来也会被 isMessageLine 挡掉,
27
+ * 这里再按路径显式排除一次:意图更清楚,也不怕以后格式变化。
28
+ */
29
+ const isSubagentTranscript = (file: string) => file.split(/[/\\]/).includes("subagents");
30
+
31
+ function isMessageLine(record: Record<string, unknown>): boolean {
32
+ return (record.type === "user" || record.type === "assistant") && record.isSidechain !== true && Boolean(record.message);
33
+ }
34
+
35
+ function projectPathFrom(records: Record<string, unknown>[]): string | undefined {
36
+ for (const record of records) {
37
+ if (typeof record.cwd === "string" && record.cwd.trim()) return record.cwd;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function modelFrom(records: Record<string, unknown>[]): string | undefined {
43
+ for (const record of records) {
44
+ const message = record.message as { model?: unknown } | undefined;
45
+ if (typeof message?.model === "string" && message.model.trim()) return message.model;
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ /** 标题:优先会话摘要行,其次首条真人用户消息(Claude Code 不写标题字段) */
51
+ function titleFrom(records: Record<string, unknown>[]): string | undefined {
52
+ for (const record of records) {
53
+ if (record.type === "summary" && typeof record.summary === "string" && record.summary.trim()) return record.summary.trim();
54
+ }
55
+ for (const record of records) {
56
+ if (record.type !== "user") continue;
57
+ const plain = userTextOf((record.message as { content?: unknown } | undefined)?.content);
58
+ if (plain && !isSynthetic(plain)) return plain;
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ function userTextOf(content: unknown): string {
64
+ const items = Array.isArray(content) ? content : [content];
65
+ return items
66
+ .map((item) => (typeof item === "string" ? item : typeof (item as { text?: unknown })?.text === "string" ? ((item as { text: string }).text) : ""))
67
+ .join("\n")
68
+ .replace(/\s+/g, " ")
69
+ .trim();
70
+ }
71
+
72
+ /** assistant 消息的 content 数组 */
73
+ function assistantBlocks(content: unknown): ImportedBlock[] {
74
+ if (!Array.isArray(content)) return compactBlocks([textBlock(typeof content === "string" ? content : undefined)]);
75
+ return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
76
+ const block = raw as Record<string, unknown>;
77
+ if (block?.type === "text") return [textBlock(block.text)];
78
+ if (block?.type === "thinking") return [typeof block.thinking === "string" && block.thinking ? { type: "thinking", thinking: block.thinking } : null];
79
+ if (block?.type === "tool_use") {
80
+ const id = typeof block.id === "string" ? block.id : "";
81
+ const name = typeof block.name === "string" ? block.name : "";
82
+ return id && name ? [{ type: "toolCall" as const, id, name, arguments: block.input ?? {} }] : [];
83
+ }
84
+ if (block?.type === "image") {
85
+ const source = block.source as Record<string, unknown> | undefined;
86
+ if (source?.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") {
87
+ return [{ type: "image", data: source.data, mimeType: source.media_type }];
88
+ }
89
+ }
90
+ return [];
91
+ }));
92
+ }
93
+
94
+ /** user 行可能同时含文本与工具结果:文本进用户消息,工具结果单列 */
95
+ function splitUserContent(content: unknown): { text: ImportedBlock[]; results: Array<{ toolCallId: string; content: ImportedBlock[]; isError: boolean }> } {
96
+ const text: Array<ImportedBlock | null> = [];
97
+ const results: Array<{ toolCallId: string; content: ImportedBlock[]; isError: boolean }> = [];
98
+ const items = Array.isArray(content) ? content : [content];
99
+ for (const raw of items) {
100
+ if (typeof raw === "string") {
101
+ text.push(textBlock(raw));
102
+ continue;
103
+ }
104
+ const block = raw as Record<string, unknown>;
105
+ if (block?.type === "text") text.push(textBlock(block.text));
106
+ else if (block?.type === "tool_result" && typeof block.tool_use_id === "string") {
107
+ results.push({ toolCallId: block.tool_use_id, content: outputBlocks(block.content), isError: block.is_error === true });
108
+ } else if (block?.type === "image") {
109
+ const source = block.source as Record<string, unknown> | undefined;
110
+ if (source?.type === "base64" && typeof source.data === "string" && typeof source.media_type === "string") {
111
+ text.push({ type: "image", data: source.data, mimeType: source.media_type });
112
+ }
113
+ }
114
+ }
115
+ return { text: compactBlocks(text), results };
116
+ }
117
+
118
+ export const claudeSource: ImportSourceModule = {
119
+ id: "claude",
120
+
121
+ async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
122
+ const limit = options?.limit ?? 0;
123
+ const candidates = await sortByRecency(await listFiles(ROOT(), { match: (name) => name.endsWith(".jsonl") }));
124
+ const out: ExternalSessionSummary[] = [];
125
+ for (const { file, mtime } of candidates) {
126
+ if (reachedLimit(out.length, limit)) break; // 只读最近的若干条:后面的文件根本不打开
127
+ if (isSubagentTranscript(file)) continue; // 主代理派给子代理的活不算会话
128
+ let head: string;
129
+ try {
130
+ head = await readHead(file);
131
+ } catch {
132
+ continue;
133
+ }
134
+ const records = [...eachJsonLine(head)].filter((record) => record.type !== undefined).slice(0, 200);
135
+ const first = records.find(isMessageLine);
136
+ if (!first) continue; // 只有元信息、没有消息的文件不列
137
+ out.push({
138
+ source: "claude",
139
+ externalId: path.basename(file, ".jsonl"),
140
+ title: cleanTitle(titleFrom(records)),
141
+ projectPath: projectPathFrom(records),
142
+ model: modelFrom(records),
143
+ createdAt: toEpochMs(first.timestamp) ?? mtime,
144
+ updatedAt: mtime,
145
+ // 消息数需要整读,扫描阶段不统计(界面显示 —)
146
+ location: file,
147
+ });
148
+ }
149
+ return out;
150
+ },
151
+
152
+ async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
153
+ let text: string;
154
+ try {
155
+ text = await fs.readFile(summary.location, "utf8");
156
+ } catch {
157
+ return null;
158
+ }
159
+ const entries: ImportedEntry[] = [];
160
+ const skipped: string[] = [];
161
+ let sidechains = 0;
162
+ let synthetic = 0;
163
+ let projectPath = summary.projectPath;
164
+ let model = summary.model;
165
+ let provider: string | undefined;
166
+
167
+ for (const record of eachJsonLine(text)) {
168
+ if (record.isSidechain === true) {
169
+ sidechains += 1;
170
+ continue;
171
+ }
172
+ if (typeof record.cwd === "string" && record.cwd.trim()) projectPath = projectPath ?? record.cwd;
173
+ const message = record.message as Record<string, unknown> | undefined;
174
+ if (!message) continue;
175
+ if (typeof message.model === "string" && message.model.trim()) model = message.model;
176
+ if (typeof message.provider === "string" && message.provider.trim()) provider = message.provider;
177
+ const timestamp = toEpochMs(record.timestamp) ?? toEpochMs(message.timestamp) ?? summary.createdAt ?? Date.now();
178
+
179
+ if (record.type === "assistant") {
180
+ const content = assistantBlocks(message.content);
181
+ if (!content.length) continue;
182
+ const usage = message.usage as Record<string, unknown> | undefined;
183
+ entries.push({
184
+ type: "message",
185
+ message: {
186
+ role: "assistant",
187
+ content,
188
+ timestamp,
189
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
190
+ ...(typeof message.stop_reason === "string" && message.stop_reason ? { stopReason: message.stop_reason } : {}),
191
+ ...(usage
192
+ ? {
193
+ usage: {
194
+ input: Number(usage.input_tokens) || 0,
195
+ output: Number(usage.output_tokens) || 0,
196
+ cacheRead: Number(usage.cache_read_input_tokens) || 0,
197
+ cacheWrite: Number(usage.cache_creation_input_tokens) || 0,
198
+ },
199
+ }
200
+ : {}),
201
+ },
202
+ });
203
+ continue;
204
+ }
205
+
206
+ if (record.type === "user") {
207
+ const { text: textBlocks, results } = splitUserContent((message as { content?: unknown }).content);
208
+ const plain = textBlocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
209
+ const keepText = textBlocks.filter((block) => block.type !== "text");
210
+ if (plain && !isSynthetic(plain.trim()) && !/^Caveat:/i.test(plain.trim())) {
211
+ entries.push({ type: "message", message: { role: "user", content: [...keepText, { type: "text", text: plain }], timestamp } as ImportedMessage });
212
+ } else if (plain) {
213
+ synthetic += 1;
214
+ }
215
+ for (const result of results) {
216
+ entries.push({
217
+ type: "message",
218
+ message: { role: "toolResult", toolCallId: result.toolCallId, content: result.content, isError: result.isError || undefined, timestamp } as ImportedMessage,
219
+ });
220
+ }
221
+ }
222
+ }
223
+
224
+ if (sidechains) skipped.push(`跳过 ${sidechains} 条侧链(子代理)记录`);
225
+ if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入(命令回显/系统提醒)`);
226
+ if (!entries.some((entry) => entry.type === "message")) return null;
227
+ return { summary: { ...summary, projectPath, model, provider }, entries, skipped };
228
+ },
229
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Codex CLI(~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl)
3
+ *
4
+ * 每行统一 `{timestamp, type, payload}`;payload.type = session_meta / response_item / event_msg …
5
+ * response_item.payload 里才有对话:message / reasoning / function_call / custom_tool_call(+output)。
6
+ * 注意首行可达十几 KB(含 base_instructions),必须读到换行再 parse。
7
+ */
8
+ import path from "node:path";
9
+ import fs from "node:fs/promises";
10
+ import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
11
+ import { codexRoot } from "./paths";
12
+ import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
13
+ import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
14
+
15
+ const ROOT = codexRoot;
16
+
17
+ /**
18
+ * 合成输入前缀(与参考实现同口径:精确前缀,不用 "#" 通配,避免误杀用户粘贴的 markdown)
19
+ */
20
+ const SYNTHETIC_PREFIXES = [
21
+ "<",
22
+ "# AGENTS.md",
23
+ "# Context from my IDE setup",
24
+ "# In app browser:",
25
+ "# Browser comments:",
26
+ "# Files mentioned by the user:",
27
+ "# Diff comments:",
28
+ "# Selected text:",
29
+ "# Review findings:",
30
+ "You are Codex",
31
+ ];
32
+
33
+ const isSynthetic = (text: string) => SYNTHETIC_PREFIXES.some((prefix) => text.startsWith(prefix));
34
+
35
+ interface Meta {
36
+ cwd?: string;
37
+ createdAt?: number;
38
+ model?: string;
39
+ provider?: string;
40
+ /** true = 这条 rollout 是别的线程派出来的(子代理),不是主对话 */
41
+ subagent?: boolean;
42
+ }
43
+
44
+ /**
45
+ * 子代理线程的判定,依据 session_meta.payload(本机 51 个 rollout 里有 22 个是子代理):
46
+ * - parent_thread_id:它的存在就说明这条线程由另一条线程派生
47
+ * - source 是对象(`{"subagent":{…}}`)而主线程是字符串 "cli" / "vscode"
48
+ * - thread_source:主线程恒为 "user",子代理是 "subagent" / "guardian_review"
49
+ * 三个条件在本机实测完全一致(22 个子代理全部同时命中,29 个主线程一个都不命中)。
50
+ */
51
+ function isSubagentMeta(payload: Record<string, unknown>): boolean {
52
+ if (typeof payload.parent_thread_id === "string" && payload.parent_thread_id) return true;
53
+ if (payload.source && typeof payload.source === "object") return true;
54
+ return typeof payload.thread_source === "string" && payload.thread_source !== "user";
55
+ }
56
+
57
+ function metaFrom(records: Record<string, unknown>[]): Meta {
58
+ const meta: Meta = {};
59
+ for (const record of records) {
60
+ if (record.type !== "session_meta") continue;
61
+ const payload = record.payload as Record<string, unknown> | undefined;
62
+ if (!payload) continue;
63
+ if (typeof payload.cwd === "string") meta.cwd = payload.cwd;
64
+ meta.createdAt = toEpochMs(payload.timestamp) ?? toEpochMs(record.timestamp);
65
+ if (typeof payload.model_provider === "string") meta.provider = payload.model_provider;
66
+ if (typeof payload.model === "string") meta.model = payload.model;
67
+ if (isSubagentMeta(payload)) meta.subagent = true;
68
+ }
69
+ // 模型有时只在 turn_context 里出现
70
+ for (const record of records) {
71
+ if (record.type !== "turn_context") continue;
72
+ const payload = record.payload as Record<string, unknown> | undefined;
73
+ if (typeof payload?.model === "string") meta.model = payload.model;
74
+ }
75
+ return meta;
76
+ }
77
+
78
+ function messageBlocks(payload: Record<string, unknown>): ImportedBlock[] {
79
+ const content = payload.content;
80
+ if (!Array.isArray(content)) return compactBlocks([textBlock(typeof content === "string" ? content : undefined)]);
81
+ return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
82
+ const block = raw as Record<string, unknown>;
83
+ if (typeof block?.text === "string") return [textBlock(block.text)];
84
+ return [];
85
+ }));
86
+ }
87
+
88
+ function reasoningText(payload: Record<string, unknown>): ImportedBlock | null {
89
+ const summary = payload.summary;
90
+ if (typeof summary === "string") return summary.trim() ? { type: "thinking", thinking: summary } : null;
91
+ if (Array.isArray(summary)) {
92
+ const text = summary.map((item) => (item as { text?: unknown })?.text).filter((value): value is string => typeof value === "string").join("\n");
93
+ return text.trim() ? { type: "thinking", thinking: text } : null;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /** 标题:Codex 不写标题字段,用首条真人用户消息(rollout 头几十行里就有) */
99
+ function titleFrom(records: Record<string, unknown>[]): string | undefined {
100
+ for (const record of records) {
101
+ if (record.type !== "response_item") continue;
102
+ const payload = record.payload as Record<string, unknown> | undefined;
103
+ if (payload?.type !== "message" || payload.role !== "user") continue;
104
+ const blocks = messageBlocks(payload);
105
+ const plain = blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n").replace(/\s+/g, " ").trim();
106
+ if (plain && !isSynthetic(plain)) return plain;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ export const codexSource: ImportSourceModule = {
112
+ id: "codex",
113
+
114
+ async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
115
+ const limit = options?.limit ?? 0;
116
+ const candidates = await sortByRecency(await listFiles(ROOT(), { match: (name) => name.startsWith("rollout-") && name.endsWith(".jsonl") }));
117
+ const out: ExternalSessionSummary[] = [];
118
+ for (const { file, mtime } of candidates) {
119
+ if (reachedLimit(out.length, limit)) break; // 只读最近的若干条
120
+ let head: string;
121
+ try {
122
+ head = await readHead(file);
123
+ } catch {
124
+ continue;
125
+ }
126
+ const records = [...eachJsonLine(head)].slice(0, 300);
127
+ const meta = metaFrom(records);
128
+ if (meta.subagent) continue; // 主代理派给子代理的活,不算会话
129
+ const hasMessage = records.some((record) => record.type === "response_item");
130
+ if (!hasMessage) continue;
131
+ out.push({
132
+ source: "codex",
133
+ externalId: path.basename(file, ".jsonl"),
134
+ title: cleanTitle(titleFrom(records)),
135
+ projectPath: meta.cwd,
136
+ model: meta.model,
137
+ provider: meta.provider,
138
+ createdAt: meta.createdAt ?? mtime,
139
+ updatedAt: mtime,
140
+ location: file,
141
+ });
142
+ }
143
+ return out;
144
+ },
145
+
146
+ async read(summary: ExternalSessionSummary): Promise<ImportedSession | null> {
147
+ let text: string;
148
+ try {
149
+ text = await fs.readFile(summary.location, "utf8");
150
+ } catch {
151
+ return null;
152
+ }
153
+ const entries: ImportedEntry[] = [];
154
+ const skipped: string[] = [];
155
+ let synthetic = 0;
156
+ let reasoningSkipped = 0;
157
+ let meta: Meta = { cwd: summary.projectPath, createdAt: summary.createdAt, model: summary.model, provider: summary.provider };
158
+
159
+ for (const record of eachJsonLine(text)) {
160
+ if (record.type === "session_meta") {
161
+ meta = { ...meta, ...metaFrom([record]) };
162
+ continue;
163
+ }
164
+ if (record.type !== "response_item") continue;
165
+ const payload = record.payload as Record<string, unknown> | undefined;
166
+ if (!payload) continue;
167
+ const timestamp = toEpochMs(record.timestamp) ?? toEpochMs(payload.timestamp) ?? meta.createdAt ?? Date.now();
168
+
169
+ switch (payload.type) {
170
+ case "message": {
171
+ const role = payload.role;
172
+ if (role === "developer") continue; // 系统提示,不进对话
173
+ const blocks = messageBlocks(payload);
174
+ const plain = blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
175
+ if (role === "user") {
176
+ if (!plain || isSynthetic(plain)) {
177
+ synthetic += 1;
178
+ continue;
179
+ }
180
+ entries.push({ type: "message", message: { role: "user", content: blocks, timestamp } });
181
+ } else if (role === "assistant") {
182
+ if (!blocks.length) continue;
183
+ entries.push({ type: "message", message: { role: "assistant", content: blocks, timestamp, ...(meta.model ? { model: meta.model } : {}) } });
184
+ }
185
+ continue;
186
+ }
187
+ case "reasoning": {
188
+ // 有摘要就保留成 thinking;只有加密内容时跳过
189
+ const thinking = reasoningText(payload);
190
+ if (!thinking) {
191
+ reasoningSkipped += 1;
192
+ continue;
193
+ }
194
+ entries.push({ type: "message", message: { role: "assistant", content: [thinking], timestamp, ...(meta.model ? { model: meta.model } : {}) } });
195
+ continue;
196
+ }
197
+ case "function_call":
198
+ case "custom_tool_call": {
199
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
200
+ const name = typeof payload.name === "string" ? payload.name : "";
201
+ if (!callId || !name) continue;
202
+ const args = parseArguments(payload.arguments ?? payload.input ?? {});
203
+ entries.push({ type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: callId, name, arguments: args }], timestamp, stopReason: "toolUse", ...(meta.model ? { model: meta.model } : {}) } });
204
+ continue;
205
+ }
206
+ case "function_call_output":
207
+ case "custom_tool_call_output": {
208
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
209
+ if (!callId) continue;
210
+ entries.push({ type: "message", message: { role: "toolResult", toolCallId: callId, content: outputBlocks(payload.output ?? payload.content), timestamp } });
211
+ continue;
212
+ }
213
+ default:
214
+ continue;
215
+ }
216
+ }
217
+
218
+ if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入`);
219
+ if (reasoningSkipped) skipped.push(`跳过 ${reasoningSkipped} 条仅含加密内容的推理记录`);
220
+ if (!entries.some((entry) => entry.type === "message")) return null;
221
+ return { summary: { ...summary, ...meta, projectPath: meta.cwd, model: meta.model, provider: meta.provider }, entries, skipped };
222
+ },
223
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * dsh 的会话是 **多帧拼接的 zstd**(`session.jsonl.zstd`):`zstdDecompressSync` 一次只解出第一帧,
3
+ * 必须按 zstd magic(28 B5 2F FD)切帧后逐帧解压再拼起来,否则只能拿到第一行。
4
+ */
5
+ import fs from "node:fs/promises";
6
+ import { zstdDecompressSync } from "node:zlib";
7
+
8
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
9
+
10
+ /** 解压多帧 zstd;帧边界只是"看起来像 magic"的位置,解压失败就把这片并入下一帧 */
11
+ export function decodeZstdFrames(raw: Buffer): Buffer {
12
+ if (raw.length < MAGIC.length || !raw.subarray(0, MAGIC.length).equals(MAGIC)) {
13
+ // 不是多帧流(可能单帧或未压缩):交给单帧解压,失败则原样当作文本
14
+ try {
15
+ return zstdDecompressSync(raw);
16
+ } catch {
17
+ return raw;
18
+ }
19
+ }
20
+ const cuts: number[] = [];
21
+ for (let i = 0; (i = raw.indexOf(MAGIC, i)) !== -1; i += MAGIC.length) cuts.push(i);
22
+ cuts.push(raw.length);
23
+ const parts: Buffer[] = [];
24
+ let start = 0;
25
+ for (let i = 1; i < cuts.length; i += 1) {
26
+ try {
27
+ parts.push(zstdDecompressSync(raw.subarray(cuts[start], cuts[i])));
28
+ start = i;
29
+ } catch {
30
+ // 误判的帧边界:与下一帧合并后重试
31
+ }
32
+ }
33
+ return Buffer.concat(parts);
34
+ }
35
+
36
+ export async function readZstdJsonl(file: string): Promise<string> {
37
+ return decodeZstdFrames(await fs.readFile(file)).toString("utf8");
38
+ }
39
+
40
+ /**
41
+ * 只解出前面的行即可判断会话是否值得导入(扫描用):逐帧解压,凑够若干帧就停,
42
+ * 避免为了列个表把 39MB 全解出来。
43
+ *
44
+ * `complete` 表示"解出来的就是全部内容"——真实文件能到 7559 帧,前 40 帧只覆盖开头一小段,
45
+ * 所以只有 complete 为真时才能断言"这个会话里面没有消息"。
46
+ */
47
+ export async function readZstdHead(file: string, maxFrames = 40): Promise<{ text: string; complete: boolean }> {
48
+ const raw = await fs.readFile(file);
49
+ const cuts: number[] = [];
50
+ for (let i = 0; (i = raw.indexOf(MAGIC, i)) !== -1; i += MAGIC.length) cuts.push(i);
51
+ if (!cuts.length) {
52
+ try {
53
+ return { text: zstdDecompressSync(raw).toString("utf8").slice(0, 64 * 1024), complete: true };
54
+ } catch {
55
+ return { text: "", complete: false };
56
+ }
57
+ }
58
+ cuts.push(raw.length);
59
+ const total = cuts.length - 1;
60
+ const out: string[] = [];
61
+ for (let i = 1; i < cuts.length && i <= maxFrames; i += 1) {
62
+ try {
63
+ out.push(zstdDecompressSync(raw.subarray(cuts[i - 1], cuts[i])).toString("utf8"));
64
+ } catch {
65
+ // 边界误判:跳过这片
66
+ }
67
+ }
68
+ return { text: out.join(""), complete: total <= maxFrames };
69
+ }