dsh-config-manager 0.1.69 → 0.1.71

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 (719) hide show
  1. package/README.md +59 -18
  2. package/README.zh-CN.md +29 -9
  3. package/lib/adapters/credentials.js +6 -3
  4. package/lib/adapters/file-collection.d.ts +21 -1
  5. package/lib/adapters/file-collection.js +32 -8
  6. package/lib/adapters/mcp.d.ts +12 -2
  7. package/lib/adapters/mcp.js +30 -13
  8. package/lib/adapters/plugin-files.d.ts +8 -0
  9. package/lib/adapters/plugin-files.js +36 -4
  10. package/lib/adapters/plugins.d.ts +28 -2
  11. package/lib/adapters/plugins.js +204 -55
  12. package/lib/adapters/prompts.d.ts +12 -2
  13. package/lib/adapters/prompts.js +34 -19
  14. package/lib/adapters/providers.js +5 -4
  15. package/lib/adapters/self.d.ts +20 -0
  16. package/lib/adapters/self.js +73 -0
  17. package/lib/adapters/sessions.d.ts +8 -0
  18. package/lib/adapters/sessions.js +47 -1
  19. package/lib/adapters/settings.d.ts +21 -3
  20. package/lib/adapters/settings.js +44 -7
  21. package/lib/adapters/skills.d.ts +36 -4
  22. package/lib/adapters/skills.js +222 -3
  23. package/lib/adapters/test-helpers.d.ts +31 -2
  24. package/lib/adapters/test-helpers.js +42 -5
  25. package/lib/adapters/workspaces.js +97 -5
  26. package/lib/cli/actions.d.ts +50 -3
  27. package/lib/cli/actions.js +103 -11
  28. package/lib/cli/help.d.ts +65 -0
  29. package/lib/cli/help.js +517 -0
  30. package/lib/cli/import-source.d.ts +32 -0
  31. package/lib/cli/import-source.js +178 -0
  32. package/lib/cli/index.d.ts +18 -2
  33. package/lib/cli/index.js +118 -90
  34. package/lib/cli/web/client-script.d.ts +8 -0
  35. package/lib/cli/web/client-script.js +11 -0
  36. package/lib/cli/web/http.d.ts +5 -0
  37. package/lib/cli/web/http.js +2 -1
  38. package/lib/cli/web/page.d.ts +16 -6
  39. package/lib/cli/web/page.js +102 -25
  40. package/lib/cli/web/routes.js +24 -5
  41. package/lib/cli/web/server.js +23 -2
  42. package/lib/client.d.ts +1019 -36
  43. package/lib/client.js +5522 -1564
  44. package/lib/core/analyzer.d.ts +15 -1
  45. package/lib/core/analyzer.js +114 -13
  46. package/lib/core/backup-plan.js +17 -1
  47. package/lib/core/backup.d.ts +8 -0
  48. package/lib/core/backup.js +28 -6
  49. package/lib/core/cache-cleaner.d.ts +10 -0
  50. package/lib/core/cache-cleaner.js +63 -22
  51. package/lib/core/checkpoint.d.ts +362 -0
  52. package/lib/core/checkpoint.js +1309 -0
  53. package/lib/core/journal.d.ts +37 -0
  54. package/lib/core/journal.js +69 -0
  55. package/lib/core/messages.d.ts +13 -0
  56. package/lib/core/messages.js +26 -0
  57. package/lib/core/model-tools.js +3 -0
  58. package/lib/core/patch-layers.d.ts +83 -0
  59. package/lib/core/patch-layers.js +110 -0
  60. package/lib/core/restore.d.ts +21 -0
  61. package/lib/core/restore.js +37 -3
  62. package/lib/core/rollback.js +6 -3
  63. package/lib/core/self-update.d.ts +52 -0
  64. package/lib/core/self-update.js +70 -0
  65. package/lib/core/session-health.d.ts +29 -5
  66. package/lib/core/session-health.js +28 -3
  67. package/lib/core/types.d.ts +126 -0
  68. package/lib/foreign/antigravity.d.ts +28 -0
  69. package/lib/foreign/antigravity.js +157 -0
  70. package/lib/foreign/bundle.d.ts +25 -0
  71. package/lib/foreign/bundle.js +79 -0
  72. package/lib/foreign/chatgpt.d.ts +6 -0
  73. package/lib/foreign/chatgpt.js +25 -0
  74. package/lib/foreign/claude-code.d.ts +3 -0
  75. package/lib/foreign/claude-code.js +85 -0
  76. package/lib/foreign/claude-sessions.d.ts +108 -0
  77. package/lib/foreign/claude-sessions.js +299 -0
  78. package/lib/foreign/cline.d.ts +6 -0
  79. package/lib/foreign/cline.js +27 -0
  80. package/lib/foreign/codex.d.ts +83 -0
  81. package/lib/foreign/codex.js +348 -0
  82. package/lib/foreign/continue.d.ts +9 -0
  83. package/lib/foreign/continue.js +65 -0
  84. package/lib/foreign/copilot.d.ts +39 -0
  85. package/lib/foreign/copilot.js +95 -0
  86. package/lib/foreign/crush.d.ts +11 -0
  87. package/lib/foreign/crush.js +57 -0
  88. package/lib/foreign/cursor.d.ts +87 -0
  89. package/lib/foreign/cursor.js +171 -0
  90. package/lib/foreign/dsh.d.ts +30 -0
  91. package/lib/foreign/dsh.js +101 -0
  92. package/lib/foreign/dsh4.d.ts +3 -0
  93. package/lib/foreign/dsh4.js +13 -0
  94. package/lib/foreign/gemini.d.ts +7 -0
  95. package/lib/foreign/gemini.js +44 -0
  96. package/lib/foreign/goose.d.ts +11 -0
  97. package/lib/foreign/goose.js +53 -0
  98. package/lib/foreign/grokbuild.d.ts +8 -0
  99. package/lib/foreign/grokbuild.js +25 -0
  100. package/lib/foreign/hermes.d.ts +2 -0
  101. package/lib/foreign/hermes.js +78 -0
  102. package/lib/foreign/kernel.d.ts +132 -0
  103. package/lib/foreign/kernel.js +569 -0
  104. package/lib/foreign/kilocode.d.ts +11 -0
  105. package/lib/foreign/kilocode.js +51 -0
  106. package/lib/foreign/kimi.d.ts +7 -0
  107. package/lib/foreign/kimi.js +47 -0
  108. package/lib/foreign/mimocode.d.ts +11 -0
  109. package/lib/foreign/mimocode.js +51 -0
  110. package/lib/foreign/openclaw.d.ts +7 -0
  111. package/lib/foreign/openclaw.js +46 -0
  112. package/lib/foreign/opencode.d.ts +11 -0
  113. package/lib/foreign/opencode.js +52 -0
  114. package/lib/foreign/pi.d.ts +7 -0
  115. package/lib/foreign/pi.js +48 -0
  116. package/lib/foreign/platform-paths.d.ts +66 -0
  117. package/lib/foreign/platform-paths.js +141 -0
  118. package/lib/foreign/qoder.d.ts +10 -0
  119. package/lib/foreign/qoder.js +49 -0
  120. package/lib/foreign/qwen.d.ts +7 -0
  121. package/lib/foreign/qwen.js +45 -0
  122. package/lib/foreign/read-antigravity.d.ts +57 -0
  123. package/lib/foreign/read-antigravity.js +504 -0
  124. package/lib/foreign/read-chatgpt.d.ts +20 -0
  125. package/lib/foreign/read-chatgpt.js +509 -0
  126. package/lib/foreign/read-claude-code.d.ts +24 -0
  127. package/lib/foreign/read-claude-code.js +234 -0
  128. package/lib/foreign/read-cline.d.ts +53 -0
  129. package/lib/foreign/read-cline.js +479 -0
  130. package/lib/foreign/read-codex.d.ts +75 -0
  131. package/lib/foreign/read-codex.js +842 -0
  132. package/lib/foreign/read-continue.d.ts +87 -0
  133. package/lib/foreign/read-continue.js +489 -0
  134. package/lib/foreign/read-copilot.d.ts +42 -0
  135. package/lib/foreign/read-copilot.js +267 -0
  136. package/lib/foreign/read-crush.d.ts +54 -0
  137. package/lib/foreign/read-crush.js +478 -0
  138. package/lib/foreign/read-cursor.d.ts +141 -0
  139. package/lib/foreign/read-cursor.js +705 -0
  140. package/lib/foreign/read-dsh.d.ts +43 -0
  141. package/lib/foreign/read-dsh.js +205 -0
  142. package/lib/foreign/read-dsh4.d.ts +14 -0
  143. package/lib/foreign/read-dsh4.js +27 -0
  144. package/lib/foreign/read-gemini.d.ts +55 -0
  145. package/lib/foreign/read-gemini.js +273 -0
  146. package/lib/foreign/read-goose.d.ts +25 -0
  147. package/lib/foreign/read-goose.js +322 -0
  148. package/lib/foreign/read-grokbuild.d.ts +34 -0
  149. package/lib/foreign/read-grokbuild.js +455 -0
  150. package/lib/foreign/read-hermes.d.ts +74 -0
  151. package/lib/foreign/read-hermes.js +690 -0
  152. package/lib/foreign/read-kilocode.d.ts +17 -0
  153. package/lib/foreign/read-kilocode.js +46 -0
  154. package/lib/foreign/read-kimi.d.ts +63 -0
  155. package/lib/foreign/read-kimi.js +719 -0
  156. package/lib/foreign/read-mimocode.d.ts +12 -0
  157. package/lib/foreign/read-mimocode.js +56 -0
  158. package/lib/foreign/read-openclaw.d.ts +51 -0
  159. package/lib/foreign/read-openclaw.js +445 -0
  160. package/lib/foreign/read-opencode.d.ts +150 -0
  161. package/lib/foreign/read-opencode.js +896 -0
  162. package/lib/foreign/read-pi.d.ts +56 -0
  163. package/lib/foreign/read-pi.js +411 -0
  164. package/lib/foreign/read-qoder.d.ts +54 -0
  165. package/lib/foreign/read-qoder.js +317 -0
  166. package/lib/foreign/read-qwen.d.ts +40 -0
  167. package/lib/foreign/read-qwen.js +326 -0
  168. package/lib/foreign/read-reasonix.d.ts +66 -0
  169. package/lib/foreign/read-reasonix.js +537 -0
  170. package/lib/foreign/read-teleagent.d.ts +15 -0
  171. package/lib/foreign/read-teleagent.js +68 -0
  172. package/lib/foreign/read-trae.d.ts +67 -0
  173. package/lib/foreign/read-trae.js +492 -0
  174. package/lib/foreign/read-vibe.d.ts +28 -0
  175. package/lib/foreign/read-vibe.js +364 -0
  176. package/lib/foreign/read-workbuddy.d.ts +49 -0
  177. package/lib/foreign/read-workbuddy.js +346 -0
  178. package/lib/foreign/read-zcode.d.ts +12 -0
  179. package/lib/foreign/read-zcode.js +38 -0
  180. package/lib/foreign/read-zed.d.ts +30 -0
  181. package/lib/foreign/read-zed.js +473 -0
  182. package/lib/foreign/reasonix.d.ts +6 -0
  183. package/lib/foreign/reasonix.js +20 -0
  184. package/lib/foreign/registry.d.ts +196 -0
  185. package/lib/foreign/registry.js +573 -0
  186. package/lib/foreign/session-ir.d.ts +186 -0
  187. package/lib/foreign/session-ir.js +427 -0
  188. package/lib/foreign/session-read.d.ts +102 -0
  189. package/lib/foreign/session-read.js +254 -0
  190. package/lib/foreign/session-source.d.ts +216 -0
  191. package/lib/foreign/session-source.js +453 -0
  192. package/lib/foreign/source-modules.d.ts +45 -0
  193. package/lib/foreign/source-modules.js +52 -0
  194. package/lib/foreign/sqlite.d.ts +61 -0
  195. package/lib/foreign/sqlite.js +404 -0
  196. package/lib/foreign/teleagent.d.ts +11 -0
  197. package/lib/foreign/teleagent.js +53 -0
  198. package/lib/foreign/trae.d.ts +6 -0
  199. package/lib/foreign/trae.js +20 -0
  200. package/lib/foreign/truth-table.d.ts +62 -0
  201. package/lib/foreign/truth-table.js +333 -0
  202. package/lib/foreign/types.d.ts +272 -0
  203. package/lib/foreign/types.js +2 -0
  204. package/lib/foreign/vibe.d.ts +6 -0
  205. package/lib/foreign/vibe.js +23 -0
  206. package/lib/foreign/workbuddy.d.ts +7 -0
  207. package/lib/foreign/workbuddy.js +45 -0
  208. package/lib/foreign/zcode.d.ts +11 -0
  209. package/lib/foreign/zcode.js +51 -0
  210. package/lib/foreign/zed.d.ts +11 -0
  211. package/lib/foreign/zed.js +53 -0
  212. package/lib/index.d.ts +154 -13
  213. package/lib/index.js +661 -84
  214. package/lib/market/github-repos.d.ts +28 -0
  215. package/lib/market/github-repos.js +43 -1
  216. package/lib/market/types.d.ts +1 -3
  217. package/lib/profiles/dsh-profile-manager.d.ts +54 -2
  218. package/lib/profiles/dsh-profile-manager.js +182 -11
  219. package/lib/profiles/dsh-profile-shared.d.ts +47 -0
  220. package/lib/profiles/dsh-profile-shared.js +15 -1
  221. package/lib/profiles/index.d.ts +1 -1
  222. package/lib/profiles/index.js +1 -1
  223. package/lib/routes/consult.js +51 -5
  224. package/lib/routes/foreign-context.d.ts +43 -0
  225. package/lib/routes/foreign-context.js +65 -0
  226. package/lib/routes/foreign.d.ts +11 -0
  227. package/lib/routes/foreign.js +208 -0
  228. package/lib/routes/import.d.ts +8 -0
  229. package/lib/routes/import.js +22 -1
  230. package/lib/routes/index.d.ts +2 -2
  231. package/lib/routes/index.js +4 -0
  232. package/lib/routes/kit.d.ts +4 -3
  233. package/lib/routes/market.js +0 -1
  234. package/lib/routes/prefs.d.ts +2 -1
  235. package/lib/routes/prefs.js +14 -2
  236. package/lib/routes/recovery.js +363 -3
  237. package/lib/routes/session-export.d.ts +117 -0
  238. package/lib/routes/session-export.js +270 -0
  239. package/lib/routes/snapshots.js +12 -2
  240. package/lib/routes/sync.js +82 -10
  241. package/lib/security/redaction.d.ts +14 -0
  242. package/lib/security/redaction.js +24 -9
  243. package/lib/security/secret-scanner.d.ts +27 -0
  244. package/lib/security/secret-scanner.js +54 -1
  245. package/lib/sync/autosync-scheduler.d.ts +13 -1
  246. package/lib/sync/autosync-scheduler.js +49 -26
  247. package/lib/sync/backup-files.js +6 -0
  248. package/lib/sync/gist/gist-transport.d.ts +91 -0
  249. package/lib/sync/gist/gist-transport.js +406 -0
  250. package/lib/sync/gist/messages.d.ts +22 -0
  251. package/lib/sync/gist/messages.js +46 -0
  252. package/lib/sync/s3/messages.d.ts +53 -0
  253. package/lib/sync/s3/messages.js +83 -0
  254. package/lib/sync/s3/s3-providers.d.ts +78 -0
  255. package/lib/sync/s3/s3-providers.js +156 -0
  256. package/lib/sync/s3/s3-transport.d.ts +126 -0
  257. package/lib/sync/s3/s3-transport.js +551 -0
  258. package/lib/sync/s3/sigv4.d.ts +107 -0
  259. package/lib/sync/s3/sigv4.js +196 -0
  260. package/lib/sync/sync-config.d.ts +250 -4
  261. package/lib/sync/sync-config.js +658 -57
  262. package/lib/sync/sync-engine.d.ts +1 -0
  263. package/lib/sync/sync-engine.js +4 -2
  264. package/lib/sync/sync-history.d.ts +1 -1
  265. package/lib/sync/webdav/webdav-transport.d.ts +31 -3
  266. package/lib/sync/webdav/webdav-transport.js +102 -18
  267. package/lib/ui/commands.d.ts +4 -3
  268. package/lib/ui/commands.js +41 -9
  269. package/lib/ui/disk-usage-view.d.ts +44 -0
  270. package/lib/ui/disk-usage-view.js +55 -1
  271. package/lib/ui/dsh-profiles-view.d.ts +53 -0
  272. package/lib/ui/dsh-profiles-view.js +56 -0
  273. package/lib/ui/foreign-view.d.ts +162 -0
  274. package/lib/ui/foreign-view.js +225 -0
  275. package/lib/ui/i18n.d.ts +77 -0
  276. package/lib/ui/i18n.js +164 -0
  277. package/lib/ui/session-inventory-view.d.ts +106 -0
  278. package/lib/ui/session-inventory-view.js +58 -0
  279. package/lib/ui/session-layout-view.d.ts +265 -0
  280. package/lib/ui/session-layout-view.js +271 -0
  281. package/lib/ui/sync-repository-picker.d.ts +98 -0
  282. package/lib/ui/sync-repository-picker.js +145 -0
  283. package/lib/ui/sync-settings-view.d.ts +143 -4
  284. package/lib/ui/sync-settings-view.js +231 -7
  285. package/lib/ui/types.d.ts +47 -0
  286. package/lib/utils/asar-read.d.ts +113 -0
  287. package/lib/utils/asar-read.js +523 -0
  288. package/lib/utils/env-lock.js +7 -2
  289. package/lib/utils/patch-yaml.d.ts +25 -0
  290. package/lib/utils/patch-yaml.js +62 -0
  291. package/lib/utils/paths.d.ts +10 -1
  292. package/lib/utils/paths.js +24 -3
  293. package/lib/utils/recursive-walk.js +12 -13
  294. package/lib/utils/session-format.d.ts +35 -2
  295. package/lib/utils/session-format.js +131 -5
  296. package/lib/utils/session-health-scan.d.ts +7 -1
  297. package/lib/utils/session-health-scan.js +369 -12
  298. package/lib/utils/session-layout-repair-service.d.ts +98 -0
  299. package/lib/utils/session-layout-repair-service.js +457 -0
  300. package/lib/utils/session-log-repair.d.ts +27 -0
  301. package/lib/utils/session-log-repair.js +233 -62
  302. package/lib/utils/session-repair-service.d.ts +32 -3
  303. package/lib/utils/session-repair-service.js +148 -9
  304. package/lib/utils/session-row-facts.d.ts +73 -0
  305. package/lib/utils/session-row-facts.js +182 -0
  306. package/lib/utils/session-verify.d.ts +118 -0
  307. package/lib/utils/session-verify.js +705 -0
  308. package/lib/utils/shared-constants.d.ts +59 -0
  309. package/lib/utils/shared-constants.js +71 -0
  310. package/package.json +1 -1
  311. package/src/adapters/credentials.test.ts +27 -0
  312. package/src/adapters/credentials.ts +6 -3
  313. package/src/adapters/file-collection.ts +33 -8
  314. package/src/adapters/files.test.ts +222 -0
  315. package/src/adapters/mcp.test.ts +43 -0
  316. package/src/adapters/mcp.ts +37 -14
  317. package/src/adapters/plugin-files.ts +34 -4
  318. package/src/adapters/plugins.test.ts +241 -0
  319. package/src/adapters/plugins.ts +201 -53
  320. package/src/adapters/prompts.test.ts +49 -0
  321. package/src/adapters/prompts.ts +41 -20
  322. package/src/adapters/providers.test.ts +16 -0
  323. package/src/adapters/providers.ts +4 -3
  324. package/src/adapters/self.test.ts +77 -0
  325. package/src/adapters/self.ts +75 -0
  326. package/src/adapters/sessions.test.ts +77 -0
  327. package/src/adapters/sessions.ts +47 -1
  328. package/src/adapters/settings.test.ts +78 -0
  329. package/src/adapters/settings.ts +42 -6
  330. package/src/adapters/skills.ts +221 -3
  331. package/src/adapters/test-helpers.ts +43 -5
  332. package/src/adapters/workspaces-dup-id.test.ts +144 -0
  333. package/src/adapters/workspaces.test.ts +10 -0
  334. package/src/adapters/workspaces.ts +97 -5
  335. package/src/cli/actions.test.ts +117 -2
  336. package/src/cli/actions.ts +132 -12
  337. package/src/cli/help.ts +570 -0
  338. package/src/cli/import-source.test.ts +193 -0
  339. package/src/cli/import-source.ts +229 -0
  340. package/src/cli/index.ts +129 -94
  341. package/src/cli/web/client-script.ts +12 -0
  342. package/src/cli/web/console-fixes.test.ts +36 -0
  343. package/src/cli/web/http.ts +7 -1
  344. package/src/cli/web/page.ts +108 -25
  345. package/src/cli/web/routes.ts +23 -4
  346. package/src/cli/web/script-anchor.test.ts +33 -0
  347. package/src/cli/web/server.ts +28 -2
  348. package/src/cli/web/web.test.ts +447 -4
  349. package/src/client/ConfigManagerSection.tsx +118 -63
  350. package/src/client/about/AboutPanel.tsx +42 -3
  351. package/src/client/about/PluginUpdateDialog.tsx +185 -0
  352. package/src/client/about/ReleaseBody.tsx +144 -0
  353. package/src/client/about/ReleaseNotesDialog.tsx +3 -130
  354. package/src/client/about/about-view.ts +7 -5
  355. package/src/client/about/plugin-update-view.test.ts +25 -0
  356. package/src/client/about/plugin-update-view.ts +26 -0
  357. package/src/client/about/release-notes-view.test.ts +33 -0
  358. package/src/client/about/release-notes-view.ts +34 -0
  359. package/src/client/api.ts +145 -1
  360. package/src/client/common/Icon.tsx +3 -0
  361. package/src/client/common/RunsCenter.tsx +8 -2
  362. package/src/client/common/Select.tsx +9 -2
  363. package/src/client/common/info-hint-guard.test.ts +63 -18
  364. package/src/client/common/plan-text-redaction.test.ts +193 -8
  365. package/src/client/common/routes.ts +15 -0
  366. package/src/client/config-manager.module.css +506 -354
  367. package/src/client/css-scale-guard.test.ts +122 -0
  368. package/src/client/css-token-guard.test.ts +82 -0
  369. package/src/client/environment/EnvironmentPanel.tsx +45 -16
  370. package/src/client/{snapshots → environment/maintenance}/DiskUsageCard.tsx +21 -17
  371. package/src/client/environment/profiles-incomplete-ui.test.ts +103 -0
  372. package/src/client/export/ExportView.tsx +9 -2
  373. package/src/client/{snapshots → home}/BackupScheduleCard.tsx +13 -4
  374. package/src/client/home/HomePanel.tsx +69 -157
  375. package/src/client/icon-layer-guard.test.ts +1 -1
  376. package/src/client/import/ForeignImportView.tsx +318 -0
  377. package/src/client/import/ImportWizardView.tsx +3 -1
  378. package/src/client/import/foreign-import-view-guard.test.ts +195 -0
  379. package/src/client/import/import-file-select.test.ts +6 -6
  380. package/src/client/import/import-wizard-steps.tsx +17 -1
  381. package/src/client/import/use-import-wizard-controller.ts +3 -3
  382. package/src/client/index.ts +1 -1
  383. package/src/client/library/LibraryActions.tsx +31 -34
  384. package/src/client/library/LibraryPanel.tsx +4 -1
  385. package/src/client/{snapshots → library}/RestorePlanView.tsx +1 -1
  386. package/src/client/{snapshots → library}/files-layout.test.ts +2 -2
  387. package/src/client/{snapshots → library}/report-dismiss.test.ts +26 -3
  388. package/src/client/library/task-panel-visibility.test.ts +25 -17
  389. package/src/client/locales.ts +278 -36
  390. package/src/client/lucide-icons.d.ts +1 -0
  391. package/src/client/market/MarketPanel.tsx +11 -2
  392. package/src/client/recovery/IncompleteCopiesSection.tsx +132 -0
  393. package/src/client/recovery/RecoveryPanel.tsx +486 -59
  394. package/src/client/recovery/recovery-api.ts +30 -0
  395. package/src/client/recovery/recovery-incomplete-copies.test.ts +151 -0
  396. package/src/client/recovery/recovery-locales.test.ts +88 -0
  397. package/src/client/recovery/recovery-locales.ts +321 -6
  398. package/src/client/recovery/recovery-panel-layout.test.ts +52 -0
  399. package/src/client/recovery/recovery-view.ts +92 -2
  400. package/src/client/run-store.test.ts +201 -89
  401. package/src/client/run-store.ts +114 -143
  402. package/src/client/session-export/session-export-view.test.ts +103 -0
  403. package/src/client/session-export/session-export-view.ts +118 -0
  404. package/src/client/sync/ChannelConfigDialog.tsx +219 -14
  405. package/src/client/sync/SyncChannelEntryCard.tsx +52 -4
  406. package/src/client/sync/SyncLogList.tsx +1 -1
  407. package/src/client/sync/SyncPanel.tsx +166 -102
  408. package/src/client/sync/SyncRepositoryPicker.tsx +216 -0
  409. package/src/client/sync/SyncSettingsView.test.ts +6 -1
  410. package/src/client/sync/history-model.test.ts +94 -57
  411. package/src/client/sync/history-model.ts +44 -25
  412. package/src/client/sync/sync-api.test.ts +56 -0
  413. package/src/client/sync/sync-api.ts +92 -0
  414. package/src/client/sync/sync-locales.ts +162 -2
  415. package/src/client/sync/sync-view.test.ts +3 -8
  416. package/src/client/sync/sync-view.ts +111 -21
  417. package/src/core/analyzer-cache.test.ts +47 -0
  418. package/src/core/analyzer.ts +115 -14
  419. package/src/core/backup-plan.test.ts +35 -0
  420. package/src/core/backup-plan.ts +19 -1
  421. package/src/core/backup.test.ts +37 -1
  422. package/src/core/backup.ts +27 -6
  423. package/src/core/cache-cleaner.test.ts +120 -1
  424. package/src/core/cache-cleaner.ts +59 -22
  425. package/src/core/checkpoint.test.ts +665 -0
  426. package/src/core/checkpoint.ts +1614 -0
  427. package/src/core/credential-plan-consistency.test.ts +93 -0
  428. package/src/core/incident-wiring.test.ts +6 -2
  429. package/src/core/journal.test.ts +70 -0
  430. package/src/core/journal.ts +68 -0
  431. package/src/core/messages.ts +26 -0
  432. package/src/core/model-tools.ts +3 -0
  433. package/src/core/patch-layers.test.ts +94 -0
  434. package/src/core/patch-layers.ts +150 -0
  435. package/src/core/restore.ts +59 -3
  436. package/src/core/rollback.ts +6 -3
  437. package/src/core/section-content-consistency.test.ts +185 -0
  438. package/src/core/self-update.test.ts +110 -0
  439. package/src/core/self-update.ts +118 -0
  440. package/src/core/session-health.test.ts +91 -1
  441. package/src/core/session-health.ts +56 -9
  442. package/src/core/types.ts +127 -1
  443. package/src/foreign/antigravity.test.ts +271 -0
  444. package/src/foreign/antigravity.ts +188 -0
  445. package/src/foreign/bundle.ts +108 -0
  446. package/src/foreign/chatgpt.test.ts +279 -0
  447. package/src/foreign/chatgpt.ts +30 -0
  448. package/src/foreign/claude-code.test.ts +234 -0
  449. package/src/foreign/claude-code.ts +106 -0
  450. package/src/foreign/claude-sessions-bytes.test.ts +227 -0
  451. package/src/foreign/claude-sessions.test.ts +180 -0
  452. package/src/foreign/claude-sessions.ts +461 -0
  453. package/src/foreign/cline.test.ts +274 -0
  454. package/src/foreign/cline.ts +32 -0
  455. package/src/foreign/codex.test.ts +585 -0
  456. package/src/foreign/codex.ts +409 -0
  457. package/src/foreign/continue.test.ts +365 -0
  458. package/src/foreign/continue.ts +73 -0
  459. package/src/foreign/copilot.test.ts +273 -0
  460. package/src/foreign/copilot.ts +133 -0
  461. package/src/foreign/crush.test.ts +234 -0
  462. package/src/foreign/crush.ts +66 -0
  463. package/src/foreign/cursor.test.ts +509 -0
  464. package/src/foreign/cursor.ts +259 -0
  465. package/src/foreign/dsh.test.ts +198 -0
  466. package/src/foreign/dsh.ts +117 -0
  467. package/src/foreign/dsh4.test.ts +127 -0
  468. package/src/foreign/dsh4.ts +15 -0
  469. package/src/foreign/file-budget.test.ts +256 -0
  470. package/src/foreign/fixtures/antigravity/README.md +18 -0
  471. package/src/foreign/fixtures/antigravity/basic/gemini/antigravity/mcp_config.json +13 -0
  472. package/src/foreign/fixtures/antigravity/basic/gemini/config/mcp_config.json +24 -0
  473. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_config.json +0 -0
  474. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_oauth_tokens.json +0 -0
  475. package/src/foreign/fixtures/antigravity/empty/gemini/config/mcp_config.json +0 -0
  476. package/src/foreign/fixtures/codex/README.md +31 -0
  477. package/src/foreign/fixtures/codex/basic/.agents/skills/hello/SKILL.md +6 -0
  478. package/src/foreign/fixtures/codex/basic/.agents/skills/wrapper/nested/SKILL.md +6 -0
  479. package/src/foreign/fixtures/codex/basic/.codex/AGENTS.md +4 -0
  480. package/src/foreign/fixtures/codex/basic/.codex/config.toml +28 -0
  481. package/src/foreign/fixtures/codex/malformed/.codex/config.toml +3 -0
  482. package/src/foreign/fixtures/codex/override/.codex/AGENTS.md +1 -0
  483. package/src/foreign/fixtures/codex/override/.codex/AGENTS.override.md +3 -0
  484. package/src/foreign/fixtures/codex/sessions/.codex/archived_sessions/rollout-2026-10-05T09-00-00-22222222-2222-4222-8222-222222222222.jsonl +2 -0
  485. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T10-00-00-11111111-1111-4111-8111-111111111111.jsonl +15 -0
  486. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-00-00-33333333-3333-4333-8333-333333333333.jsonl +2 -0
  487. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-30-00-55555555-5555-4555-8555-555555555555.jsonl +2 -0
  488. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T12-00-00-44444444-4444-4444-8444-444444444444.jsonl +2 -0
  489. package/src/foreign/fixtures/codex/sessions/.codex/sessions/notes.jsonl +1 -0
  490. package/src/foreign/fixtures/copilot/README.md +22 -0
  491. package/src/foreign/fixtures/copilot/basic/copilot-instructions.md +3 -0
  492. package/src/foreign/fixtures/copilot/basic/instructions/empty.instructions.md +1 -0
  493. package/src/foreign/fixtures/copilot/basic/instructions/repo.instructions.md +5 -0
  494. package/src/foreign/fixtures/copilot/basic/mcp-config.json +24 -0
  495. package/src/foreign/fixtures/copilot/basic/skills/hello/SKILL.md +6 -0
  496. package/src/foreign/fixtures/copilot/overridden/mcp-config.json +10 -0
  497. package/src/foreign/fixtures/cursor/README.md +17 -0
  498. package/src/foreign/fixtures/cursor/alt/.cursor/mcp.json +5 -0
  499. package/src/foreign/fixtures/cursor/basic/.cursor/mcp.json +18 -0
  500. package/src/foreign/fixtures/cursor/basic/.cursor/rules/agent.mdc +5 -0
  501. package/src/foreign/fixtures/cursor/basic/.cursor/rules/always.mdc +6 -0
  502. package/src/foreign/fixtures/cursor/basic/.cursor/rules/auto.mdc +7 -0
  503. package/src/foreign/fixtures/cursor/basic/.cursor/rules/manual.mdc +3 -0
  504. package/src/foreign/fixtures/cursor/basic/.cursor/skills/.hidden/SKILL.md +6 -0
  505. package/src/foreign/fixtures/cursor/basic/.cursor/skills/hello/SKILL.md +6 -0
  506. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/SKILL.md +6 -0
  507. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/notes.md +1 -0
  508. package/src/foreign/fixtures/cursor/basic/.cursorrules +1 -0
  509. package/src/foreign/fixtures/cursor/basic/project/.cursor/mcp.json +9 -0
  510. package/src/foreign/fixtures/cursor/basic/project/.cursor/rules/project-always.mdc +5 -0
  511. package/src/foreign/fixtures/cursor/basic/project/.cursor/skills/hello/SKILL.md +6 -0
  512. package/src/foreign/fixtures/cursor/basic/project/.cursorrules +1 -0
  513. package/src/foreign/fixtures/cursor/empty/.cursor/mcp.json +0 -0
  514. package/src/foreign/fixtures/cursor/malformed/.cursor/mcp.json +3 -0
  515. package/src/foreign/fixtures/cursor/malformed/.cursor/rules/broken.mdc +6 -0
  516. package/src/foreign/fixtures/cursor/shape/.cursor/mcp.json +3 -0
  517. package/src/foreign/fixtures/hermes/README.md +14 -0
  518. package/src/foreign/fixtures/hermes/basic/SOUL.md +3 -0
  519. package/src/foreign/fixtures/hermes/basic/config.yaml +48 -0
  520. package/src/foreign/fixtures/hermes/basic/dotenv.fixture +1 -0
  521. package/src/foreign/fixtures/hermes/basic/memories/MEMORY.md +1 -0
  522. package/src/foreign/fixtures/hermes/basic/memories/USER.md +1 -0
  523. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/SKILL.md +6 -0
  524. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/examples/usage.md +1 -0
  525. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/SKILL.md +6 -0
  526. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/notes.md +1 -0
  527. package/src/foreign/fixtures/hermes/basic/state.db +1 -0
  528. package/src/foreign/fixtures/hermes/empty/config.yaml +0 -0
  529. package/src/foreign/gemini.test.ts +291 -0
  530. package/src/foreign/gemini.ts +52 -0
  531. package/src/foreign/goose.test.ts +196 -0
  532. package/src/foreign/goose.ts +62 -0
  533. package/src/foreign/grokbuild.test.ts +246 -0
  534. package/src/foreign/grokbuild.ts +30 -0
  535. package/src/foreign/hermes.test.ts +545 -0
  536. package/src/foreign/hermes.ts +86 -0
  537. package/src/foreign/kernel-credentials.test.ts +88 -0
  538. package/src/foreign/kernel-workspaces.test.ts +88 -0
  539. package/src/foreign/kernel.ts +584 -0
  540. package/src/foreign/kilocode.test.ts +186 -0
  541. package/src/foreign/kilocode.ts +60 -0
  542. package/src/foreign/kimi.test.ts +259 -0
  543. package/src/foreign/kimi.ts +55 -0
  544. package/src/foreign/mcp-value-shape.test.ts +286 -0
  545. package/src/foreign/mimocode.test.ts +178 -0
  546. package/src/foreign/mimocode.ts +60 -0
  547. package/src/foreign/openclaw.test.ts +248 -0
  548. package/src/foreign/openclaw.ts +54 -0
  549. package/src/foreign/opencode.test.ts +376 -0
  550. package/src/foreign/opencode.ts +61 -0
  551. package/src/foreign/pi.test.ts +275 -0
  552. package/src/foreign/pi.ts +56 -0
  553. package/src/foreign/platform-paths.ts +156 -0
  554. package/src/foreign/qoder.test.ts +266 -0
  555. package/src/foreign/qoder.ts +57 -0
  556. package/src/foreign/qwen.test.ts +203 -0
  557. package/src/foreign/qwen.ts +53 -0
  558. package/src/foreign/read-antigravity.ts +569 -0
  559. package/src/foreign/read-chatgpt.ts +506 -0
  560. package/src/foreign/read-claude-code.ts +264 -0
  561. package/src/foreign/read-cline.ts +517 -0
  562. package/src/foreign/read-codex.ts +823 -0
  563. package/src/foreign/read-continue.ts +557 -0
  564. package/src/foreign/read-copilot.ts +304 -0
  565. package/src/foreign/read-crush.ts +503 -0
  566. package/src/foreign/read-cursor.ts +778 -0
  567. package/src/foreign/read-dsh.ts +215 -0
  568. package/src/foreign/read-dsh4.ts +35 -0
  569. package/src/foreign/read-gemini.ts +306 -0
  570. package/src/foreign/read-goose.ts +333 -0
  571. package/src/foreign/read-grokbuild.ts +475 -0
  572. package/src/foreign/read-hermes.ts +724 -0
  573. package/src/foreign/read-kilocode.ts +55 -0
  574. package/src/foreign/read-kimi.ts +768 -0
  575. package/src/foreign/read-mimocode.ts +66 -0
  576. package/src/foreign/read-openclaw.ts +505 -0
  577. package/src/foreign/read-opencode.ts +994 -0
  578. package/src/foreign/read-pi.ts +459 -0
  579. package/src/foreign/read-qoder.ts +351 -0
  580. package/src/foreign/read-qwen.ts +342 -0
  581. package/src/foreign/read-reasonix.ts +590 -0
  582. package/src/foreign/read-teleagent.ts +82 -0
  583. package/src/foreign/read-trae.ts +488 -0
  584. package/src/foreign/read-vibe.ts +353 -0
  585. package/src/foreign/read-workbuddy.ts +374 -0
  586. package/src/foreign/read-zcode.ts +48 -0
  587. package/src/foreign/read-zed.ts +479 -0
  588. package/src/foreign/reasonix.test.ts +336 -0
  589. package/src/foreign/reasonix.ts +25 -0
  590. package/src/foreign/registry.test.ts +377 -0
  591. package/src/foreign/registry.ts +750 -0
  592. package/src/foreign/session-ir.test.ts +189 -0
  593. package/src/foreign/session-ir.ts +614 -0
  594. package/src/foreign/session-read.ts +295 -0
  595. package/src/foreign/session-source.ts +625 -0
  596. package/src/foreign/skill-file-caps.test.ts +171 -0
  597. package/src/foreign/source-modules.ts +86 -0
  598. package/src/foreign/source-registry.test.ts +190 -0
  599. package/src/foreign/sqlite.test.ts +313 -0
  600. package/src/foreign/sqlite.ts +446 -0
  601. package/src/foreign/teleagent.test.ts +147 -0
  602. package/src/foreign/teleagent.ts +62 -0
  603. package/src/foreign/trae.test.ts +316 -0
  604. package/src/foreign/trae.ts +25 -0
  605. package/src/foreign/truncation-report.test.ts +238 -0
  606. package/src/foreign/truth-table.ts +453 -0
  607. package/src/foreign/types.ts +312 -0
  608. package/src/foreign/vibe.test.ts +209 -0
  609. package/src/foreign/vibe.ts +28 -0
  610. package/src/foreign/workbuddy.test.ts +207 -0
  611. package/src/foreign/workbuddy.ts +53 -0
  612. package/src/foreign/zcode.test.ts +224 -0
  613. package/src/foreign/zcode.ts +60 -0
  614. package/src/foreign/zed.test.ts +240 -0
  615. package/src/foreign/zed.ts +62 -0
  616. package/src/index.facade.test.ts +254 -16
  617. package/src/index.ts +704 -82
  618. package/src/market/github-repos.test.ts +98 -1
  619. package/src/market/github-repos.ts +63 -1
  620. package/src/market/types.ts +3 -3
  621. package/src/profiles/dsh-profile-incomplete-copy.test.ts +177 -0
  622. package/src/profiles/dsh-profile-manager.test.ts +37 -0
  623. package/src/profiles/dsh-profile-manager.ts +188 -13
  624. package/src/profiles/dsh-profile-shared.ts +55 -1
  625. package/src/profiles/index.ts +2 -1
  626. package/src/routes/consult.ts +53 -4
  627. package/src/routes/foreign-context.ts +89 -0
  628. package/src/routes/foreign.test.ts +443 -0
  629. package/src/routes/foreign.ts +226 -0
  630. package/src/routes/import.ts +23 -1
  631. package/src/routes/index.ts +6 -2
  632. package/src/routes/kit.ts +4 -3
  633. package/src/routes/market.ts +0 -1
  634. package/src/routes/prefs.ts +15 -1
  635. package/src/routes/recovery.test.ts +167 -0
  636. package/src/routes/recovery.ts +331 -1
  637. package/src/routes/session-export.ts +308 -0
  638. package/src/routes/snapshots.ts +12 -2
  639. package/src/routes/sync.ts +91 -7
  640. package/src/security/redaction.test.ts +149 -0
  641. package/src/security/redaction.ts +25 -9
  642. package/src/security/secret-scanner.test.ts +186 -0
  643. package/src/security/secret-scanner.ts +57 -1
  644. package/src/sync/autosync-risk-tier.test.ts +61 -0
  645. package/src/sync/autosync-scheduler.test.ts +48 -1
  646. package/src/sync/autosync-scheduler.ts +50 -26
  647. package/src/sync/backup-files.ts +5 -0
  648. package/src/sync/gist/gist-msgkeys-catalog.test.ts +33 -0
  649. package/src/sync/gist/gist-msgkeys.test.ts +71 -0
  650. package/src/sync/gist/gist-transport.test.ts +308 -0
  651. package/src/sync/gist/gist-transport.ts +550 -0
  652. package/src/sync/gist/messages.ts +49 -0
  653. package/src/sync/retention-prune-all-off.test.ts +50 -0
  654. package/src/sync/s3/cloud-config.test.ts +283 -0
  655. package/src/sync/s3/messages.ts +102 -0
  656. package/src/sync/s3/s3-delete-index-404.test.ts +170 -0
  657. package/src/sync/s3/s3-delete-index-unreadable.test.ts +141 -0
  658. package/src/sync/s3/s3-msgkeys.test.ts +101 -0
  659. package/src/sync/s3/s3-providers.ts +213 -0
  660. package/src/sync/s3/s3-transport.test.ts +394 -0
  661. package/src/sync/s3/s3-transport.ts +651 -0
  662. package/src/sync/s3/s3-upload-index-404.test.ts +171 -0
  663. package/src/sync/s3/sigv4.test.ts +219 -0
  664. package/src/sync/s3/sigv4.ts +283 -0
  665. package/src/sync/sync-config.test.ts +237 -5
  666. package/src/sync/sync-config.ts +841 -52
  667. package/src/sync/sync-engine-download-tombstone.test.ts +73 -0
  668. package/src/sync/sync-engine.ts +5 -3
  669. package/src/sync/sync-history.ts +1 -1
  670. package/src/sync/webdav/webdav-delete-index-404.test.ts +178 -0
  671. package/src/sync/webdav/webdav-delete-index-unreadable.test.ts +143 -0
  672. package/src/sync/webdav/webdav-msgkeys.test.ts +53 -0
  673. package/src/sync/webdav/webdav-transport.test.ts +9 -5
  674. package/src/sync/webdav/webdav-transport.ts +99 -17
  675. package/src/sync/webdav/webdav-upload-index-404.test.ts +177 -0
  676. package/src/ui/commands.test.ts +4 -1
  677. package/src/ui/commands.ts +46 -12
  678. package/src/ui/disk-usage-view.test.ts +63 -1
  679. package/src/ui/disk-usage-view.ts +93 -1
  680. package/src/ui/dsh-profiles-incomplete-view.test.ts +102 -0
  681. package/src/ui/dsh-profiles-view.ts +94 -0
  682. package/src/ui/foreign-view.test.ts +241 -0
  683. package/src/ui/foreign-view.ts +362 -0
  684. package/src/ui/i18n.ts +164 -0
  685. package/src/ui/mid-ellipsis.test.ts +5 -10
  686. package/src/ui/session-inventory-view.test.ts +142 -2
  687. package/src/ui/session-inventory-view.ts +127 -0
  688. package/src/ui/session-layout-view.test.ts +361 -0
  689. package/src/ui/session-layout-view.ts +444 -0
  690. package/src/ui/sync-repository-picker.test.ts +130 -0
  691. package/src/ui/sync-repository-picker.ts +180 -0
  692. package/src/ui/sync-settings-view.test.ts +166 -0
  693. package/src/ui/sync-settings-view.ts +319 -9
  694. package/src/ui/types.ts +42 -0
  695. package/src/utils/asar-read.test.ts +330 -0
  696. package/src/utils/asar-read.ts +606 -0
  697. package/src/utils/env-lock.test.ts +55 -0
  698. package/src/utils/env-lock.ts +6 -1
  699. package/src/utils/patch-yaml.test.ts +112 -0
  700. package/src/utils/patch-yaml.ts +72 -0
  701. package/src/utils/paths.test.ts +63 -0
  702. package/src/utils/paths.ts +25 -3
  703. package/src/utils/recursive-walk.test.ts +20 -0
  704. package/src/utils/recursive-walk.ts +12 -13
  705. package/src/utils/session-format.test.ts +113 -2
  706. package/src/utils/session-format.ts +127 -5
  707. package/src/utils/session-health-scan.test.ts +364 -1
  708. package/src/utils/session-health-scan.ts +434 -15
  709. package/src/utils/session-layout-repair-service.test.ts +362 -0
  710. package/src/utils/session-layout-repair-service.ts +568 -0
  711. package/src/utils/session-log-repair.test.ts +260 -1
  712. package/src/utils/session-log-repair.ts +294 -55
  713. package/src/utils/session-repair-service.test.ts +148 -1
  714. package/src/utils/session-repair-service.ts +165 -10
  715. package/src/utils/session-row-facts.test.ts +159 -0
  716. package/src/utils/session-row-facts.ts +187 -0
  717. package/src/utils/session-verify.test.ts +762 -0
  718. package/src/utils/session-verify.ts +784 -0
  719. package/src/utils/shared-constants.ts +82 -0
@@ -0,0 +1,994 @@
1
+ /**
2
+ * opencode 家族(opencode / mimocode / kilocode / teleagent / zcode)的**共享读盘层**,
3
+ * 同时承载 SQLite 批(8 个来源)共用的最小工具:能力探测门、只读打开、时间与 JSON 单元格取值。
4
+ *
5
+ * 为什么五家共用一份(而不是每源抄一遍):mimocode / kilocode 是 opencode 的 fork,teleagent 与
6
+ * zcode 是同一个「session / message / part 三表 + JSON TEXT 列」形态(见
7
+ * outputs/competitor-recon-2026-10-05/read-chat-import.md §3.1 与 read-claude-move.md §7.2)。
8
+ * 同一套解析逻辑出现第 3 份副本正是竞品纪律明令停止的事;本仓库把它收敛成一个函数。
9
+ * **本文件是 SQLite 批的共用面**:其余 7 个 `read-<id>.ts` 从这里 import 类型与工具,
10
+ * 但每个来源的真值表路径函数一律留在自己的文件里(那才是逐源需要独立取证的部分)。
11
+ *
12
+ * 三条硬纪律(t1 调研确证 + 本任务实测):
13
+ * ① **动态 import + 能力探测**:一律经 `sqlite.ts` 的 `sqliteCapability()` —— 静态 import
14
+ * `node:sqlite` 会在缺该能力的宿主上把**整个插件**加载失败(表现是「插件没装」而
15
+ * 不是「这一个来源读不到」)。能力缺失是**结构化降级**(0 文件 + 一条 skip),绝不崩。
16
+ * ② **只读打开 + PRAGMA table_info 自适应列**:`sqlite.ts` 的三态打开计划
17
+ * (direct / immutable / copy —— **绝不往用户目录写文件**,t9 处置见 sqlite.ts 文件头)
18
+ * + `db.columns()`;表名只允许来自本文件的编译期候选表,绝不拼调用方给的字符串。
19
+ * ③ **读不到一律返回 null(统一口径)**:本文件的 `readOpencodeFamilyDatabase` 在
20
+ * 「打不开 / 不是这个来源的库 / 表结构不符 / 查询失败」时返回 **null**;把 null 转成
21
+ * 一条 `source-unreadable` skip 的动作**只有 `readOpencodeFamily` 一处**。
22
+ * 「能打开但一条会话都没有」是**空数组**,与 null 严格区分。
23
+ * **未安装**(库文件不存在)不报码:未安装是正常状态,报码只用于「存在但读不到」。
24
+ */
25
+ import { joinFor, normalizePlatform, posixLocalShare } from './platform-paths.ts';
26
+ import { isFile } from './session-read.ts';
27
+ import {
28
+ cellText,
29
+ openSqliteReadOnlyEx,
30
+ sqliteCapability,
31
+ } from './sqlite.ts';
32
+ import type { SqliteCapability, SqliteHandle, SqliteOpenProblem, SqliteOpenResult, SqliteRow } from './sqlite.ts';
33
+ import { firstUserText, genericBlocksOf, flattenText } from './session-source.ts';
34
+ import type { ParsedTranscript, SessionReadOutcome, TranscriptRecord } from './session-source.ts';
35
+ import {
36
+ irBump,
37
+ irEarlier,
38
+ irSafeTime,
39
+ irTextBlock,
40
+ irToolCallBlock,
41
+ irToolResultBlock,
42
+ } from './session-ir.ts';
43
+ import type { IrBlock, IrTimeMs } from './session-ir.ts';
44
+ import { isRecord } from '../utils/guards.ts';
45
+ import type { ForeignSkip } from './types.ts';
46
+
47
+ /* ---------------- ① SQLite 批共用形状与工具 ---------------- */
48
+
49
+ /** 一个已读成归一记录的 SQLite 会话(8 个 SQLite 来源共用同一形状) */
50
+ export interface SqliteSessionFile {
51
+ readonly id: string;
52
+ readonly parsed: ParsedTranscript;
53
+ }
54
+
55
+ /**
56
+ * 可注入的 SQLite 依赖(默认 = `sqlite.ts` 的真实实现)。
57
+ *
58
+ * 注入面只有两个函数,用途**仅限测试**:验证「宿主没有 `node:sqlite`」时的结构化降级路径
59
+ * (竞品调研 read-claude-move §7.2 的建议:先落地一个可注入的最小适配器 + fake 实现)。
60
+ * 生产路径(wiring 的 read)永远不传它。
61
+ */
62
+ export interface SqliteDeps {
63
+ readonly capability?: () => Promise<SqliteCapability>;
64
+ readonly open?: (file: string) => Promise<SqliteHandle | null>;
65
+ }
66
+
67
+ /** 一个来源/一个库的读盘结果(0 文件也是正常结果) */
68
+ export interface SqliteReadPart {
69
+ readonly files: readonly SqliteSessionFile[];
70
+ readonly skipped: readonly ForeignSkip[];
71
+ readonly counts: Readonly<Record<string, number>>;
72
+ }
73
+
74
+ /**
75
+ * 逐会话剔除谓词看到的会话视图(**只由本仓库的 `read-<id>.ts` 使用**,绝不接受用户输入)。
76
+ *
77
+ * 三个 fork 的剔除事实所在层不同,因此视图把三层一次给全:`row`(fork 专有列,如 kilocode 的
78
+ * `parent_id` / `time_archived`)、`messages`(mimocode 的后台标记在 `data.agent` 里 —— 与竞品
79
+ * `readOpencodeDb` 抽 `agent` 供 `isMimocodeBackgroundSession` 过滤同一理由)、`parsed`(已归一记录)。
80
+ */
81
+ export interface FamilySessionView {
82
+ readonly id: string;
83
+ readonly row: SqliteRow;
84
+ readonly title: string;
85
+ readonly cwd?: string | undefined;
86
+ readonly time?: IrTimeMs | undefined;
87
+ readonly messages: readonly SqliteRow[];
88
+ readonly parsed: ParsedTranscript;
89
+ }
90
+
91
+ /** 家族读器的逐源差异(缺省 = 不过滤,opencode / teleagent 口径) */
92
+ export interface FamilyReadOptions {
93
+ /** 返回 true = 该会话不进结果集(逐源差异,见 `FamilySessionView`);剔除数进 `sessions.dropped` */
94
+ readonly dropSession?: ((session: FamilySessionView) => boolean) | undefined;
95
+ }
96
+
97
+ /** 读不到的**稳定机器码**:code 固定 source-unreadable,detail 区分原因(绝不进用户可见文案) */
98
+ export function sqliteSkip(origin: string, detail: string): ForeignSkip {
99
+ return { code: 'source-unreadable', origin, detail };
100
+ }
101
+
102
+ /**
103
+ * 能力探测门:可用 → undefined;宿主缺 `node:sqlite` → 一条结构化降级 skip。
104
+ *
105
+ * **不抛、不返回 null**:能力缺失是「宿主不支持」这个**已知事实**,必须能被上层看见
106
+ * (与「库读不到」不是同一件事)。
107
+ */
108
+ export async function sqliteGate(origin: string, deps?: SqliteDeps): Promise<ForeignSkip | undefined> {
109
+ const probe = await (deps?.capability ?? sqliteCapability)();
110
+ if (probe.available) return undefined;
111
+ return sqliteSkip(origin, 'sqlite-capability:' + (probe.reason ?? 'unavailable'));
112
+ }
113
+
114
+ /**
115
+ * 只读打开(能力已探测):默认走 `sqlite.ts` 的三态计划
116
+ * (direct / immutable / copy —— **绝不往用户目录写文件**,见 sqlite.ts 文件头),
117
+ * 并把失败原因带出来(`open-failed:<problem>`,如 `copy-too-large`)。
118
+ * 注入的 `deps.open` 只能给句柄(测试用),没有细分原因 → detail 就是 `open-failed`。
119
+ */
120
+ export async function sqliteOpen(file: string, deps?: SqliteDeps): Promise<SqliteOpenResult> {
121
+ if (deps?.open !== undefined) return { db: await deps.open(file) };
122
+ return await openSqliteReadOnlyEx(file);
123
+ }
124
+
125
+ /** 打开失败的 detail(稳定机器码;`problem` 缺省说明失败发生在注入的 open 里) */
126
+ export function openDetail(problem: SqliteOpenProblem | undefined): string {
127
+ return problem === undefined ? 'open-failed' : 'open-failed:' + problem;
128
+ }
129
+
130
+ /**
131
+ * 从「本平台拼接」的常量表名里取第一个存在的候选(表名绝不来自调用方输入)。
132
+ * 表名一律加双引号(SQLite 标识符引号),避免与关键字相撞。
133
+ */
134
+ function firstOf(tables: readonly string[], candidates: readonly string[]): string | undefined {
135
+ return candidates.find((name) => tables.includes(name));
136
+ }
137
+
138
+ /** 默认排序键候选(自适应列:取**实际存在**的第一个时间列,再拿 `id` 兜底) */
139
+ const ORDER_TIME_KEYS = ['time_created', 'time_updated', 'created_at', 'createdAt', 'time'];
140
+
141
+ /**
142
+ * 一张表的全部行(表名只允许编译期常量;读不到 → null)。**必须带 ORDER BY**(竞品同口径的
143
+ * `ORDER BY time_created, id`):SQLite 无 ORDER BY 时行序是实现定义的,一旦漂移,工具 call/result
144
+ * 配对与轮次切分就跟着错。排序键从**实际存在的列**里取(自适应列,见文件头纪律②);一个都没有时不排序。
145
+ */
146
+ function rowsOfTable(
147
+ db: SqliteHandle,
148
+ table: string,
149
+ cols: readonly string[],
150
+ timeKeys: readonly string[] = ORDER_TIME_KEYS,
151
+ ): SqliteRow[] | null {
152
+ const order: string[] = [];
153
+ const time = timeKeys.find((key) => cols.includes(key));
154
+ if (time !== undefined) order.push(time);
155
+ if (cols.includes('id')) order.push('id');
156
+ const suffix = order.length === 0 ? '' : ' ORDER BY ' + order.map((key) => '"' + key + '"').join(', ');
157
+ return db.all('SELECT * FROM "' + table + '"' + suffix);
158
+ }
159
+
160
+ /* ---------------- ② 单元格取值(自适应列的唯一口径) ---------------- */
161
+
162
+ /** 秒/毫秒自适应的**上界**:小于它的正数时间戳按「秒」解释(毫秒量级的 2026 年是 1.7e12) */
163
+ const SECONDS_CEILING = 100_000_000_000;
164
+
165
+ /**
166
+ * 单元格 → 毫秒安全整数。
167
+ *
168
+ * 与 `session-ir.ts` 的 `irSafeTime` 的差异只有一条:SQLite 来源常见**秒**时间戳
169
+ * (竞品 core 同款「秒/毫秒自适应 + 必须取整」),而 DSH 的时间字段要求安全整数毫秒。
170
+ * 判据是**有界的**:只有正数且小于 `SECONDS_CEILING` 的整数才乘 1000,其余原样;
171
+ * 负值/0 不做任何换算(绝不猜)。
172
+ */
173
+ export function msTime(v: unknown): IrTimeMs | undefined {
174
+ if (typeof v !== 'number') return irSafeTime(v);
175
+ if (!Number.isFinite(v)) return undefined;
176
+ const n = Math.trunc(v);
177
+ if (!Number.isSafeInteger(n)) return undefined;
178
+ if (n > 0 && n < SECONDS_CEILING) {
179
+ const ms = n * 1000;
180
+ return Number.isSafeInteger(ms) ? ms : undefined;
181
+ }
182
+ return n;
183
+ }
184
+
185
+ /** 取第一个非空字符串列值(列**必须存在**才取得到 —— 缺列只丢该字段,不抛) */
186
+ export function pickString(row: SqliteRow, keys: readonly string[]): string | undefined {
187
+ for (const key of keys) {
188
+ const v = cellText(row, key);
189
+ if (v !== undefined && v !== '') return v;
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ /** 取第一个可解析时间列值(自适应列 + 秒/毫秒自适应) */
195
+ export function pickTime(row: SqliteRow, keys: readonly string[]): IrTimeMs | undefined {
196
+ for (const key of keys) {
197
+ const raw = row[key];
198
+ if (raw === undefined || raw === null) continue;
199
+ const t = msTime(raw);
200
+ if (t !== undefined) return t;
201
+ }
202
+ return undefined;
203
+ }
204
+
205
+ /** JSON 列的取值结果(`absent` 与 `unparsable` 必须能区分:前者不是错误,后者要计数) */
206
+ export type JsonCell =
207
+ | { readonly ok: true; readonly value: unknown }
208
+ | { readonly ok: false; readonly problem: 'absent' | 'unparsable' };
209
+
210
+ /** 一行 JSON 列:TEXT/BLOB 里是 JSON 就解析;纯文本原样返回;空/缺 → absent;畸形 → unparsable */
211
+ export function jsonCell(row: SqliteRow, keys: readonly string[]): JsonCell {
212
+ for (const key of keys) {
213
+ const v = row[key];
214
+ if (v === undefined || v === null) continue;
215
+ let text: string | undefined;
216
+ if (typeof v === 'string') text = v;
217
+ else if (v instanceof Uint8Array) text = new TextDecoder().decode(v);
218
+ else if (typeof v === 'object') return { ok: true, value: v };
219
+ else continue;
220
+ const trimmed = text.trim();
221
+ if (trimmed === '') return { ok: false, problem: 'absent' };
222
+ const head = trimmed.charAt(0);
223
+ if (head === '{' || head === '[') {
224
+ try {
225
+ return { ok: true, value: JSON.parse(trimmed) };
226
+ } catch {
227
+ return { ok: false, problem: 'unparsable' };
228
+ }
229
+ }
230
+ return { ok: true, value: text };
231
+ }
232
+ return { ok: false, problem: 'absent' };
233
+ }
234
+
235
+ /* ---------------- ③ opencode 家族的表/列候选(真值表见 chat-import §3.1) ---------------- */
236
+
237
+ /** V1 三表(opencode 1.x)与 V2 两表(opencode 2.x)的表名(**编译期常量**,绝不拼调用方输入) */
238
+ const SESSION_TABLE_V1 = 'session';
239
+ const SESSION_TABLE_V2 = 'session_v2';
240
+ const TRANSCRIPT_TABLE_V2 = 'session_message';
241
+ const MESSAGE_TABLES = ['message', 'session_message'];
242
+ const PART_TABLES = ['part', 'session_part'];
243
+
244
+ /**
245
+ * 库属于哪一代:`'v2'`(session_v2)/ `'v1'`(session)/ `undefined`(两者都没有)。
246
+ *
247
+ * **v2 必须优先**(竞品 `opencodeSchemaGeneration`):V1→V2 迁移后旧三表仍留在同一个库里
248
+ * (只是迁移的来源),先取 `session` 会读到**过期**数据 —— 新会话在 v2 表里、老会话在 v1 表里,
249
+ * 两边都读还会让同一会话被导入两次。
250
+ */
251
+ export function opencodeSchemaGeneration(tables: readonly string[]): 'v1' | 'v2' | undefined {
252
+ if (tables.includes(SESSION_TABLE_V2)) return 'v2';
253
+ if (tables.includes(SESSION_TABLE_V1)) return 'v1';
254
+ return undefined;
255
+ }
256
+
257
+ const SESSION_ID_KEYS = ['id', 'session_id', 'sessionId', 'sessionID'];
258
+ /** 会话表的 cwd 候选(opencode 用 directory;fork 各自有别) */
259
+ const SESSION_CWD_KEYS = ['directory', 'cwd', 'workdir', 'working_directory', 'worktree', 'project_path', 'project_dir', 'path'];
260
+ const SESSION_TITLE_KEYS = ['title', 'name', 'summary', 'slug'];
261
+ const SESSION_TIME_KEYS = ['time_created', 'created_at', 'createdAt', 'time', 'time_updated', 'updated_at', 'updatedAt'];
262
+
263
+ const MESSAGE_SESSION_KEYS = ['session_id', 'sessionId', 'sessionID', 'session'];
264
+ const MESSAGE_ID_KEYS = ['id', 'message_id', 'messageId', 'messageID'];
265
+ /** 消息 JSON 列候选(**导出**:逐源剔除谓词要读消息行里的源侧字段,如 mimocode 的 data.agent) */
266
+ export const MESSAGE_DATA_KEYS = ['data', 'json', 'content', 'message', 'payload'];
267
+ const MESSAGE_ROLE_KEYS = ['role', 'sender'];
268
+ const MESSAGE_TIME_KEYS = ['time_created', 'created_at', 'createdAt', 'time', 'time_updated', 'updated_at', 'updatedAt'];
269
+ const MESSAGE_MODEL_KEYS = ['model', 'model_id', 'modelId'];
270
+ /** 会话级模型列(opencode 的 `session.model` 是 JSON 字符串 `{id, providerID, variant}`) */
271
+ const SESSION_MODEL_KEYS = ['model', 'model_id', 'modelId'];
272
+ /** 会话/消息级模型对象里取 id 的候选(opencode 用 `id`,fork 可能写 `modelID`) */
273
+ const MODEL_ID_KEYS = ['id', 'modelID', 'model_id', 'modelId'];
274
+
275
+ /** V2(session_message)的专属列候选 */
276
+ const V2_TYPE_KEYS = ['type', 'kind'];
277
+ const V2_MESSAGE_ORDER_KEYS = ['seq', 'time_created', 'id'];
278
+
279
+ const PART_MESSAGE_KEYS = ['message_id', 'messageId', 'messageID', 'message', 'parent_id', 'parentID'];
280
+ const PART_DATA_KEYS = ['data', 'json', 'content', 'part', 'payload'];
281
+
282
+ /** 记账累加器(未迁移类型逐类计数 + 解析不出来的条数;两处都绝不静默) */
283
+ interface Accumulator {
284
+ ignored: Record<string, number>;
285
+ bad: number;
286
+ }
287
+
288
+ /** 会话行(只在解析期用) */
289
+ interface FamilySession {
290
+ readonly id: string;
291
+ /** 会话行原样(逐源剔除谓词要读 fork 专有列,如 kilocode 的 parent_id / time_archived) */
292
+ readonly row: SqliteRow;
293
+ readonly cwd?: string | undefined;
294
+ readonly title: string;
295
+ readonly time?: IrTimeMs | undefined;
296
+ /** 会话级模型(消息级缺席时的兜底;见 sessionModelOf) */
297
+ readonly model?: string | undefined;
298
+ }
299
+
300
+ function nestedTime(payload: Record<string, unknown> | undefined): IrTimeMs | undefined {
301
+ if (payload === undefined) return undefined;
302
+ const time = payload['time'];
303
+ if (isRecord(time)) {
304
+ const t = pickTime(time, ['created', 'createdAt', 'start', 'updated', 'completed']);
305
+ if (t !== undefined) return t;
306
+ }
307
+ return pickTime(payload, ['time', 'timestamp', 'createdAt', 'created_at']);
308
+ }
309
+
310
+ /** 消息级模型(竞品 convert/opencode.mjs:249-255 的回退链:平铺 modelID 优先,其次 model.modelID / model 字符串) */
311
+ function payloadModel(payload: Record<string, unknown>): string | undefined {
312
+ const flat = payload['modelID'] ?? payload['model_id'] ?? payload['modelId'];
313
+ if (typeof flat === 'string' && flat !== '') return flat;
314
+ const model = payload['model'];
315
+ if (isRecord(model)) {
316
+ const nested = pickString(model, MODEL_ID_KEYS);
317
+ if (nested !== undefined) return nested;
318
+ }
319
+ if (typeof model === 'string' && model !== '') return model;
320
+ return undefined;
321
+ }
322
+
323
+ /** 会话级模型(opencode 的 `session.model` 是 JSON 串 `{id, providerID, variant}`;fork 可能是对象/纯串;脏值不猜) */
324
+ function sessionModelOf(row: SqliteRow): string | undefined {
325
+ for (const key of SESSION_MODEL_KEYS) {
326
+ const raw = row[key];
327
+ if (raw === undefined || raw === null) continue;
328
+ if (isRecord(raw)) {
329
+ const id = pickString(raw, MODEL_ID_KEYS);
330
+ if (id !== undefined) return id;
331
+ continue;
332
+ }
333
+ const text = cellText(row, key);
334
+ if (text === undefined) continue;
335
+ const trimmed = text.trim();
336
+ if (trimmed === '') continue;
337
+ if (trimmed.charAt(0) !== '{') return trimmed;
338
+ try {
339
+ const parsed: unknown = JSON.parse(trimmed);
340
+ if (isRecord(parsed)) {
341
+ const id = pickString(parsed, MODEL_ID_KEYS);
342
+ if (id !== undefined) return id;
343
+ }
344
+ } catch {
345
+ // 非 JSON(个别脏数据)→ 无会话级模型,回退链继续走消息级
346
+ }
347
+ }
348
+ return undefined;
349
+ }
350
+
351
+ /** `data.tokens`(`{input,output,reasoning,cache:{read,write}}`)→ IR usage 键名(对齐 `irUsageOf`;`cache.write` 不映射) */
352
+ function usageOf(tokens: unknown): unknown {
353
+ if (!isRecord(tokens)) return undefined;
354
+ const cache = isRecord(tokens['cache']) ? tokens['cache'] : undefined;
355
+ return {
356
+ inputTokens: tokens['input'],
357
+ outputTokens: tokens['output'],
358
+ reasoningTokens: tokens['reasoning'],
359
+ cacheReadTokens: cache === undefined ? undefined : cache['read'],
360
+ };
361
+ }
362
+
363
+ /** 消息级摘要正文(zcode 的 `data.summary.body`;其它来源可能是纯字符串 `data.summary`) */
364
+ function summaryBodyOf(payload: Record<string, unknown>): string | undefined {
365
+ const summary = payload['summary'];
366
+ if (typeof summary === 'string') return summary.trim() === '' ? undefined : summary;
367
+ if (!isRecord(summary)) return undefined;
368
+ const body = summary['body'];
369
+ return typeof body === 'string' && body.trim() !== '' ? body : undefined;
370
+ }
371
+
372
+ /**
373
+ * opencode 家族的 part → IR 块。
374
+ *
375
+ * 块的词汇由本函数**逐个显式映射**(text / tool / tool-result / reasoning),其余一律
376
+ * 按 `genericBlocksOf` 兜底并逐类计数 —— 不认识的 part 类型不会消失,而是出现在
377
+ * `unsupported-session-record` 的 detail 里。
378
+ */
379
+ function partDataBlocks(data: Record<string, unknown>, acc: Accumulator): IrBlock[] {
380
+ const type = (pickString(data, ['type']) ?? '').toLowerCase();
381
+ if (type === 'text') {
382
+ const text = typeof data['text'] === 'string' ? data['text'] : flattenText(data['content']);
383
+ return text === '' ? [] : [irTextBlock(text)];
384
+ }
385
+ if (type === 'tool' || type === 'tool-call' || type === 'tool_call') {
386
+ const state = isRecord(data['state']) ? data['state'] : undefined;
387
+ const id = pickString(data, ['callID', 'call_id', 'id']) ?? '';
388
+ const name = pickString(data, ['tool', 'name']) ?? '';
389
+ const input = state?.['input'] ?? data['input'] ?? data['arguments'];
390
+ const blocks: IrBlock[] = [irToolCallBlock(id, name, input)];
391
+ // **output 缺失也发空结果**(竞品 convert/opencode.mjs:187-191 同口径):只发 tool_call 会留下
392
+ // 「有 call 无 result」的断链,合成器永远配不上对;空文本结果让它成对闭合。
393
+ const output = state?.['output'] ?? data['output'];
394
+ const status = (pickString(state ?? {}, ['status']) ?? '').toLowerCase();
395
+ const isError = status === 'error' || status === 'failed' || (state !== undefined && state['error'] !== undefined);
396
+ blocks.push(irToolResultBlock(id, flattenText(output, acc.ignored, 'tool-output'), isError));
397
+ return blocks;
398
+ }
399
+ if (type === 'tool-result' || type === 'tool_result' || type === 'toolresult') {
400
+ const id = pickString(data, ['callID', 'call_id', 'tool_use_id', 'id']) ?? '';
401
+ const text = flattenText(data['content'] ?? data['output'] ?? data['result'] ?? data['text'], acc.ignored, 'tool-output');
402
+ const isError = data['is_error'] === true || data['isError'] === true || data['error'] === true;
403
+ return [irToolResultBlock(id, text, isError)];
404
+ }
405
+ if (type === 'reasoning' || type === 'thinking') {
406
+ // DSH 的 IR 没有 reasoning 块;**不伪装成正文**,逐类计数后交给上层报码
407
+ irBump(acc.ignored, 'part:' + type);
408
+ return [];
409
+ }
410
+ if (type === 'file') {
411
+ // 图片/附件部分:本地 IR **没有 image 块**(见 session-ir.ts 的块词表),因此不可能落成宿主附件。
412
+ // **不伪装成正文** —— 用 `[image: <name>]` 文本占位(竞品拿不到内联字节时的同款降级文案)并逐张计数。
413
+ irBump(acc.ignored, 'part:file');
414
+ return [irTextBlock('[image: ' + (pickString(data, ['filename', 'name']) ?? 'unknown') + ']')];
415
+ }
416
+ if (type === 'patch') {
417
+ // 补丁块原文不落地(竞品只留条数摘要),但**不静默丢**:文件数进正文占位
418
+ const files = Array.isArray(data['files']) ? data['files'].length : 0;
419
+ return [irTextBlock('[patch: ' + String(files) + ' files]')];
420
+ }
421
+ if (type === 'subtask') {
422
+ const command = typeof data['command'] === 'string' ? data['command'] : '';
423
+ const description = typeof data['description'] === 'string' ? data['description'] : '';
424
+ return [irTextBlock('[subtask: ' + command + ' — ' + description + ']')];
425
+ }
426
+ if (type === 'step-start' || type === 'step-finish' || type === 'snapshot' || type === 'agent') {
427
+ irBump(acc.ignored, 'part:' + type);
428
+ return [];
429
+ }
430
+ if (type === '') return genericBlocksOf(data, acc.ignored, 'part');
431
+ irBump(acc.ignored, 'part:' + type);
432
+ return [];
433
+ }
434
+
435
+ function partBlocks(row: SqliteRow, acc: Accumulator): IrBlock[] {
436
+ const cell = jsonCell(row, PART_DATA_KEYS);
437
+ if (!cell.ok) {
438
+ if (cell.problem === 'unparsable') acc.bad++;
439
+ return [];
440
+ }
441
+ if (typeof cell.value === 'string') return cell.value === '' ? [] : [irTextBlock(cell.value)];
442
+ if (!isRecord(cell.value)) {
443
+ acc.bad++;
444
+ return [];
445
+ }
446
+ return partDataBlocks(cell.value, acc);
447
+ }
448
+
449
+ /**
450
+ * 一个会话的消息行 → 归一记录。
451
+ *
452
+ * 两条结构性决定(都是 fixture 级取证,见各来源的 evidence 字段):
453
+ * ① 工具生命周期在这里**成对闭合**:一个 part 同时带 input/output 时拆成两条记录
454
+ * (助手侧的 tool_call + 用户侧的 tool_result),因为合成器只把 tool_result 归入
455
+ * **已打开的 step**;两条塞进同一条助手记录会让结果被静默丢掉。
456
+ * ② 消息的 `data` JSON 是主形态,缺它时退回行上的 `role`/`content` 列(fork 差异)。
457
+ */
458
+ function recordsOf(
459
+ rows: readonly SqliteRow[],
460
+ partsByMessage: ReadonlyMap<string, readonly SqliteRow[]>,
461
+ sessionModel: string | undefined,
462
+ ): { records: TranscriptRecord[]; ignored: Record<string, number>; bad: number } {
463
+ const acc: Accumulator = { ignored: {}, bad: 0 };
464
+ const records: TranscriptRecord[] = [];
465
+ for (const row of rows) {
466
+ const cell = jsonCell(row, MESSAGE_DATA_KEYS);
467
+ let payload: Record<string, unknown> | undefined;
468
+ if (cell.ok) {
469
+ if (isRecord(cell.value)) payload = cell.value;
470
+ else if (typeof cell.value === 'string' && cell.value !== '') payload = { content: cell.value };
471
+ } else if (cell.problem === 'unparsable') {
472
+ acc.bad++;
473
+ }
474
+ const roleRaw = (payload === undefined ? undefined : pickString(payload, ['role'])) ?? pickString(row, MESSAGE_ROLE_KEYS) ?? '';
475
+ const role = roleRaw.toLowerCase();
476
+ if (role !== 'user' && role !== 'assistant') {
477
+ irBump(acc.ignored, role === '' ? 'message-no-role' : role);
478
+ continue;
479
+ }
480
+ const messageId = pickString(row, MESSAGE_ID_KEYS);
481
+ const time = pickTime(row, MESSAGE_TIME_KEYS) ?? nestedTime(payload);
482
+ // 模型回退链(竞品 convert/opencode.mjs:248-266 同口径,只多一条行级 model 列):
483
+ // 行级 model 列 → data.modelID → data.model.modelID / data.model(字符串)→ 会话级 model
484
+ const model = pickString(row, MESSAGE_MODEL_KEYS)
485
+ ?? (payload === undefined ? undefined : payloadModel(payload))
486
+ ?? sessionModel;
487
+ // provider 回报用量(assistant 消息的 data.tokens);user 消息/老库缺席 → 不占键
488
+ const usage = payload === undefined ? undefined : usageOf(payload['tokens']);
489
+ // 消息级压缩摘要(zcode 的 data.summary.body):源侧压缩标记,正文由**会话级摘要**承载 ——
490
+ // 本地 IR 没有压缩通道(session-ir.ts 的「补头/注入/压缩属于待办能力」),正文进不了对话;
491
+ // 但**绝不静默**:逐条计数(message:summary)让缺口在 unsupported-session-record 里可见。
492
+ if (payload !== undefined && summaryBodyOf(payload) !== undefined) irBump(acc.ignored, 'message:summary');
493
+ const blocks: IrBlock[] = [];
494
+ if (messageId !== undefined) {
495
+ for (const part of partsByMessage.get(messageId) ?? []) blocks.push(...partBlocks(part, acc));
496
+ }
497
+ if (blocks.length === 0 && payload !== undefined) {
498
+ const content = payload['content'] ?? payload['text'];
499
+ if (content !== undefined) blocks.push(...genericBlocksOf(content, acc.ignored, 'message'));
500
+ }
501
+ const pushed = splitToolResults(role as 'user' | 'assistant', blocks, { id: messageId, time, model, usage });
502
+ for (const rec of pushed) records.push(rec);
503
+ if (pushed.length === 0) irBump(acc.ignored, 'message-empty');
504
+ }
505
+ return { records, ignored: acc.ignored, bad: acc.bad };
506
+ }
507
+
508
+ /**
509
+ * 一条源记录 → 1~2 条归一记录(**工具生命周期闭合的唯一实现**,SQLite 批四个读器共用)。
510
+ *
511
+ * 为什么必须拆:合成器只把 `tool_result` 归入**已打开的 step**(助手侧消息),一条记录里
512
+ * 同时放 tool_call 与 tool_result 时结果会被**静默丢弃**(合成器的助手分支只取 tool_call)。
513
+ * 因此 tool_result 一律拆到紧随其后的**用户侧记录**,与真实日志的「同一 step 内闭合」同形。
514
+ */
515
+ export function splitToolResults(
516
+ role: 'user' | 'assistant',
517
+ blocks: readonly IrBlock[],
518
+ meta: {
519
+ readonly id?: string | undefined;
520
+ readonly time?: IrTimeMs | undefined;
521
+ readonly model?: string | undefined;
522
+ /** provider 回报用量(assistant 侧;user 侧合成器不看,透传无害) */
523
+ readonly usage?: unknown;
524
+ },
525
+ ): TranscriptRecord[] {
526
+ const rest = blocks.filter((b) => b.type !== 'tool_result');
527
+ const results = blocks.filter((b) => b.type === 'tool_result');
528
+ const out: TranscriptRecord[] = [];
529
+ if (rest.length > 0) out.push({ role, blocks: rest, time: meta.time, id: meta.id, model: meta.model, usage: meta.usage });
530
+ if (results.length > 0) {
531
+ out.push({ role: 'user', blocks: results, time: meta.time, id: meta.id === undefined ? undefined : meta.id + '-result' });
532
+ }
533
+ return out;
534
+ }
535
+
536
+ function earliestTime(records: readonly TranscriptRecord[]): IrTimeMs | undefined {
537
+ let earliest: IrTimeMs | undefined;
538
+ for (const rec of records) earliest = irEarlier(earliest, rec.time);
539
+ return earliest;
540
+ }
541
+
542
+ /**
543
+ * 打开着的库 → 每个会话一个归一文件。
544
+ *
545
+ * **返回 null ⇔ 读不到**:表读不出来 / 既无 `session` 也无 `session_v2` 表 /
546
+ * 消息表查询失败。返回空数组才是「这就是该库,但里面没有会话」。
547
+ *
548
+ * **世代在这里先分派**(见 opencodeSchemaGeneration):V2 库走 session_v2 + session_message,
549
+ * V1 库走三表;两条路径产出的形状完全一致,差异只在读取层。
550
+ */
551
+ export function readOpencodeFamilyDatabase(db: SqliteHandle, options?: FamilyReadOptions): SqliteReadPart | null {
552
+ const tables = db.tables();
553
+ if (tables === null) return null;
554
+ const generation = opencodeSchemaGeneration(tables);
555
+ if (generation === undefined) return null;
556
+ const sessionTable = generation === 'v2' ? SESSION_TABLE_V2 : SESSION_TABLE_V1;
557
+ // **PRAGMA table_info 自适应列**:列清单先读出来做结构自证(缺 id 列的 "session" 表不是本家族的库)
558
+ const sessionCols = db.columns(sessionTable);
559
+ if (sessionCols === null) return null;
560
+ if (!SESSION_ID_KEYS.some((key) => sessionCols.includes(key))) return null;
561
+ const sessionRows = rowsOfTable(db, sessionTable, sessionCols);
562
+ if (sessionRows === null) return null;
563
+
564
+ const counts: Record<string, number> = {};
565
+ const sessions: FamilySession[] = [];
566
+ let noId = 0;
567
+ for (const row of sessionRows) {
568
+ const id = pickString(row, SESSION_ID_KEYS);
569
+ if (id === undefined) {
570
+ noId++;
571
+ continue;
572
+ }
573
+ sessions.push({
574
+ id,
575
+ row,
576
+ cwd: pickString(row, SESSION_CWD_KEYS),
577
+ title: pickString(row, SESSION_TITLE_KEYS) ?? '',
578
+ time: pickTime(row, SESSION_TIME_KEYS),
579
+ model: sessionModelOf(row),
580
+ });
581
+ }
582
+ if (noId > 0) counts['sessions.without-id'] = noId;
583
+ return generation === 'v2'
584
+ ? readV2Part(db, tables, sessions, counts, options)
585
+ : readV1Part(db, tables, sessions, counts, options);
586
+ }
587
+
588
+ /**
589
+ * 会话 → 归一文件(V1 / V2 两条路径共用):记录解析由调用方给,逐源剔除谓词在这里统一收口
590
+ * (见 FamilySessionView)。剔除**绝不静默**(计数进 `sessions.dropped`,与 `sessions.without-id` 同口径)。
591
+ */
592
+ function familyFiles(
593
+ sessions: readonly FamilySession[],
594
+ counts: Record<string, number>,
595
+ options: FamilyReadOptions | undefined,
596
+ recordsOfSession: (session: FamilySession) => { records: TranscriptRecord[]; ignored: Record<string, number>; bad: number },
597
+ rowsOfSession: (session: FamilySession) => readonly SqliteRow[],
598
+ ): SqliteSessionFile[] {
599
+ const files: SqliteSessionFile[] = [];
600
+ let dropped = 0;
601
+ for (const session of sessions) {
602
+ const raw = rowsOfSession(session);
603
+ const parsed0 = recordsOfSession(session);
604
+ const parsed: ParsedTranscript = {
605
+ records: parsed0.records,
606
+ cwd: session.cwd,
607
+ createdAt: irEarlier(session.time, earliestTime(parsed0.records)),
608
+ title: session.title !== '' ? session.title : firstUserText(parsed0.records),
609
+ raw: raw.length,
610
+ bad: parsed0.bad,
611
+ ignored: parsed0.ignored,
612
+ };
613
+ const view: FamilySessionView = {
614
+ id: session.id,
615
+ row: session.row,
616
+ title: session.title,
617
+ cwd: session.cwd,
618
+ time: session.time,
619
+ messages: raw,
620
+ parsed,
621
+ };
622
+ if (options?.dropSession !== undefined && options.dropSession(view)) {
623
+ dropped++;
624
+ continue;
625
+ }
626
+ files.push({ id: session.id, parsed });
627
+ }
628
+ counts['sessions'] = files.length;
629
+ if (dropped > 0) counts['sessions.dropped'] = dropped;
630
+ return files;
631
+ }
632
+
633
+ /** V1 库(session / message / part 三表)→ 归一文件;表结构不符一律 null(统一判负层次) */
634
+ function readV1Part(
635
+ db: SqliteHandle,
636
+ tables: readonly string[],
637
+ sessions: readonly FamilySession[],
638
+ counts: Record<string, number>,
639
+ options: FamilyReadOptions | undefined,
640
+ ): SqliteReadPart | null {
641
+ const messagesBySession = new Map<string, SqliteRow[]>();
642
+ let messageRows = 0;
643
+ const messageTable = firstOf(tables, MESSAGE_TABLES);
644
+ if (messageTable !== undefined) {
645
+ const messageCols = db.columns(messageTable);
646
+ if (messageCols === null) return null;
647
+ if (!MESSAGE_SESSION_KEYS.some((key) => messageCols.includes(key))) return null;
648
+ const rows = rowsOfTable(db, messageTable, messageCols);
649
+ if (rows === null) return null;
650
+ for (const row of rows) {
651
+ const key = pickString(row, MESSAGE_SESSION_KEYS);
652
+ if (key === undefined) continue;
653
+ messageRows++;
654
+ const list = messagesBySession.get(key);
655
+ if (list === undefined) messagesBySession.set(key, [row]);
656
+ else list.push(row);
657
+ }
658
+ }
659
+
660
+ const partsByMessage = new Map<string, SqliteRow[]>();
661
+ // 计数口径 = **part 行数**(此前记的是去重后的消息键数,与键名 parts 名不副实)
662
+ let partRows = 0;
663
+ const partTable = firstOf(tables, PART_TABLES);
664
+ if (partTable !== undefined) {
665
+ const partCols = db.columns(partTable);
666
+ if (partCols === null) return null;
667
+ if (!PART_MESSAGE_KEYS.some((key) => partCols.includes(key))) return null;
668
+ const rows = rowsOfTable(db, partTable, partCols);
669
+ if (rows === null) return null;
670
+ for (const row of rows) {
671
+ const key = pickString(row, PART_MESSAGE_KEYS);
672
+ if (key === undefined) continue;
673
+ partRows++;
674
+ const list = partsByMessage.get(key);
675
+ if (list === undefined) partsByMessage.set(key, [row]);
676
+ else list.push(row);
677
+ }
678
+ }
679
+
680
+ const files = familyFiles(
681
+ sessions,
682
+ counts,
683
+ options,
684
+ (session) => recordsOf(messagesBySession.get(session.id) ?? [], partsByMessage, session.model),
685
+ (session) => messagesBySession.get(session.id) ?? [],
686
+ );
687
+ counts['messages'] = messageRows;
688
+ counts['parts'] = partRows;
689
+ if (messageTable === undefined) counts['messages.table-missing'] = 1;
690
+ return { files, skipped: [], counts };
691
+ }
692
+
693
+ /**
694
+ * V2 库(session_v2 + session_message)→ 归一文件。V2 没有 part 表:**一条 session_message 行
695
+ * 就是一个内容单元**,靠行上的 `type` 列分派。缺 `session_message` 表时不假装没有会话:
696
+ * 会话元数据照读、逐条落 session-empty(与 zcode「message 表可能缺失」同口径)。
697
+ */
698
+ function readV2Part(
699
+ db: SqliteHandle,
700
+ tables: readonly string[],
701
+ sessions: readonly FamilySession[],
702
+ counts: Record<string, number>,
703
+ options: FamilyReadOptions | undefined,
704
+ ): SqliteReadPart | null {
705
+ const bySession = new Map<string, SqliteRow[]>();
706
+ let messageRows = 0;
707
+ const hasTranscript = tables.includes(TRANSCRIPT_TABLE_V2);
708
+ if (hasTranscript) {
709
+ const cols = db.columns(TRANSCRIPT_TABLE_V2);
710
+ if (cols === null) return null;
711
+ if (!MESSAGE_SESSION_KEYS.some((key) => cols.includes(key))) return null;
712
+ // 转录按 **seq** 升序重建(竞品 ORDER BY seq;行序漂移会让消息顺序错乱)
713
+ const rows = rowsOfTable(db, TRANSCRIPT_TABLE_V2, cols, V2_MESSAGE_ORDER_KEYS);
714
+ if (rows === null) return null;
715
+ for (const row of rows) {
716
+ const key = pickString(row, MESSAGE_SESSION_KEYS);
717
+ if (key === undefined) continue;
718
+ messageRows++;
719
+ const list = bySession.get(key);
720
+ if (list === undefined) bySession.set(key, [row]);
721
+ else list.push(row);
722
+ }
723
+ }
724
+
725
+ const files = familyFiles(
726
+ sessions,
727
+ counts,
728
+ options,
729
+ (session) => v2RecordsOf(bySession.get(session.id) ?? [], session.model),
730
+ (session) => bySession.get(session.id) ?? [],
731
+ );
732
+ counts['messages'] = messageRows;
733
+ // V2 没有 part 行;「一条转录行 = 一个内容单元」,不再用去重后的消息键数冒充 part 数
734
+ if (!hasTranscript) counts['messages.table-missing'] = 1;
735
+ return { files, skipped: [], counts };
736
+ }
737
+
738
+ /**
739
+ * V2 转录行 → 归一记录(**按行上的 `type` 列分派**,竞品 readOpencodeV2 同口径)。不能沿用 V1 的
740
+ * 「按 data.role 判角色」:V2 的 `data` 里没有 role(role 在行的 `type` 列上)→ 旧实现整会话 session-empty。
741
+ */
742
+ function v2RecordsOf(
743
+ rows: readonly SqliteRow[],
744
+ sessionModel: string | undefined,
745
+ ): { records: TranscriptRecord[]; ignored: Record<string, number>; bad: number } {
746
+ const acc: Accumulator = { ignored: {}, bad: 0 };
747
+ const records: TranscriptRecord[] = [];
748
+ const push = (role: 'user' | 'assistant', blocks: readonly IrBlock[], meta: { id?: string | undefined; time?: IrTimeMs | undefined; model?: string | undefined; usage?: unknown }): void => {
749
+ const pushed = splitToolResults(role, blocks, meta);
750
+ for (const rec of pushed) records.push(rec);
751
+ if (pushed.length === 0) irBump(acc.ignored, 'message-empty');
752
+ };
753
+ for (const row of rows) {
754
+ const cell = jsonCell(row, MESSAGE_DATA_KEYS);
755
+ let data: Record<string, unknown> | undefined;
756
+ if (cell.ok) {
757
+ if (isRecord(cell.value)) data = cell.value;
758
+ } else if (cell.problem === 'unparsable') {
759
+ acc.bad++;
760
+ }
761
+ const type = (pickString(row, V2_TYPE_KEYS) ?? '').toLowerCase();
762
+ const id = pickString(row, MESSAGE_ID_KEYS);
763
+ const time = pickTime(row, MESSAGE_TIME_KEYS);
764
+ if (type === 'user') {
765
+ push('user', v2UserBlocks(data, acc), { id, time });
766
+ continue;
767
+ }
768
+ if (type === 'assistant') {
769
+ const model = v2MessageModel(data) ?? sessionModel;
770
+ const usage = data === undefined ? undefined : usageOf(data['tokens']);
771
+ push('assistant', v2AssistantBlocks(data, acc), { id, time, model, usage });
772
+ continue;
773
+ }
774
+ if (type === 'compaction') {
775
+ const body = v2CompactionBody(data);
776
+ const status = data === undefined ? '' : (pickString(data, ['status']) ?? '').toLowerCase();
777
+ if (status !== 'completed') {
778
+ // running / failed:不是「模型可见的压缩边界」→ 正文按普通内容保留(竞品同口径,绝不静默丢)
779
+ if (body !== '') push('user', [irTextBlock(body)], { id, time });
780
+ else irBump(acc.ignored, 'message:compaction-' + (status === '' ? 'unknown' : status));
781
+ } else {
782
+ // completed 是模型可见的边界:正文由 DSH 原生压缩检查点承载,本地 IR 无该通道(见 session-ir.ts 的「待办能力」)
783
+ irBump(acc.ignored, 'message:compaction');
784
+ }
785
+ continue;
786
+ }
787
+ if (type === 'synthetic' || type === 'system' || type === 'skill') {
788
+ // 注入文本(工具改名通知 / 合成续跑提示 / 技能正文):当用户文本保留
789
+ const text = data === undefined ? undefined : data['text'];
790
+ if (typeof text === 'string' && text.trim() !== '') push('user', [irTextBlock(text)], { id, time });
791
+ else irBump(acc.ignored, 'message:' + type);
792
+ continue;
793
+ }
794
+ if (type === 'shell') {
795
+ const text = v2ShellText(data);
796
+ if (text !== '') push('user', [irTextBlock(text)], { id, time });
797
+ else irBump(acc.ignored, 'message:shell');
798
+ continue;
799
+ }
800
+ // idle / agent-switched / model-switched / location-switched(无正文的结构性标记)与未知类型:
801
+ // 不进对话,但逐类计数(绝不静默)
802
+ irBump(acc.ignored, 'message:' + (type === '' ? 'no-type' : type));
803
+ }
804
+ return { records, ignored: acc.ignored, bad: acc.bad };
805
+ }
806
+
807
+ /** V2 user 消息 → 块(text + 附件;本地 IR 无 image 块,附件一律走文本占位并计数) */
808
+ function v2UserBlocks(data: Record<string, unknown> | undefined, acc: Accumulator): IrBlock[] {
809
+ if (data === undefined) return [];
810
+ const blocks: IrBlock[] = [];
811
+ const text = data['text'];
812
+ if (typeof text === 'string' && text.trim() !== '') blocks.push(irTextBlock(text));
813
+ const files = data['files'];
814
+ if (Array.isArray(files)) {
815
+ for (const file of files) {
816
+ if (!isRecord(file)) continue;
817
+ // 有内联字节也落不成附件(本地 IR 没有 image 块)→ 与竞品「拿不到字节」同款占位文案
818
+ irBump(acc.ignored, 'message:attachment');
819
+ const name = pickString(file, ['name', 'filename', 'mime']) ?? 'unknown';
820
+ blocks.push(irTextBlock('[attachment: ' + name + ']'));
821
+ }
822
+ }
823
+ return blocks;
824
+ }
825
+
826
+ /** V2 assistant 消息的 content[] → 块(text / reasoning 计数 / tool 成对) */
827
+ function v2AssistantBlocks(data: Record<string, unknown> | undefined, acc: Accumulator): IrBlock[] {
828
+ if (data === undefined) return [];
829
+ const blocks: IrBlock[] = [];
830
+ const content = data['content'];
831
+ if (!Array.isArray(content)) {
832
+ if (content !== undefined) irBump(acc.ignored, 'message:content-unmapped');
833
+ return blocks;
834
+ }
835
+ for (const item of content) {
836
+ if (!isRecord(item)) continue;
837
+ const type = (pickString(item, ['type']) ?? '').toLowerCase();
838
+ if (type === 'text') {
839
+ if (typeof item['text'] === 'string') blocks.push(irTextBlock(item['text']));
840
+ continue;
841
+ }
842
+ if (type === 'reasoning') {
843
+ // 同 V1:本地 IR 没有 reasoning 块,不伪装成正文,逐类计数
844
+ irBump(acc.ignored, 'part:reasoning');
845
+ continue;
846
+ }
847
+ if (type === 'tool') {
848
+ blocks.push(...v2ToolBlocks(item, acc));
849
+ continue;
850
+ }
851
+ irBump(acc.ignored, 'part:' + (type === '' ? 'unmapped' : type));
852
+ }
853
+ return blocks;
854
+ }
855
+
856
+ /**
857
+ * V2 tool 内容项 → tool_call + tool_result(**恒成对**)。input 在 streaming 状态下是未完成的
858
+ * JSON 字符串:能解析就解析,不能就放进 `partial`(竞品同口径)。输出取 `state.content` 的文本、
859
+ * 空时回退 `state.error.message`;都空也发空结果(保住 call/result 配对)。
860
+ */
861
+ function v2ToolBlocks(item: Record<string, unknown>, acc: Accumulator): IrBlock[] {
862
+ const state = isRecord(item['state']) ? item['state'] : undefined;
863
+ const id = pickString(item, ['id', 'callID', 'call_id']) ?? '';
864
+ const name = pickString(item, ['name', 'tool']) ?? '';
865
+ let input = state?.['input'];
866
+ if (typeof input === 'string') {
867
+ const trimmed = input.trim();
868
+ try {
869
+ input = JSON.parse(trimmed);
870
+ } catch {
871
+ input = trimmed === '' ? {} : { partial: input };
872
+ }
873
+ }
874
+ const status = (pickString(state ?? {}, ['status']) ?? '').toLowerCase();
875
+ const isError = status === 'error' || status === 'failed' || (state !== undefined && state['error'] !== undefined);
876
+ let output = '';
877
+ const content = state?.['content'];
878
+ if (Array.isArray(content)) {
879
+ output = content
880
+ .map((entry) => (isRecord(entry) && entry['type'] === 'text' && typeof entry['text'] === 'string'
881
+ ? entry['text']
882
+ : '[file: ' + (isRecord(entry) ? (pickString(entry, ['name', 'uri']) ?? 'unknown') : 'unknown') + ']'))
883
+ .join('\n');
884
+ }
885
+ if (output === '') {
886
+ const error = state?.['error'];
887
+ if (isRecord(error) && typeof error['message'] === 'string') output = error['message'];
888
+ }
889
+ return [irToolCallBlock(id, name, input), irToolResultBlock(id, output, isError)];
890
+ }
891
+
892
+ /** V2 assistant 消息级模型(竞品:data.model.id;兼容平铺 modelID 与字符串 model) */
893
+ function v2MessageModel(data: Record<string, unknown> | undefined): string | undefined {
894
+ if (data === undefined) return undefined;
895
+ const flat = data['modelID'];
896
+ if (typeof flat === 'string' && flat !== '') return flat;
897
+ const model = data['model'];
898
+ if (isRecord(model)) {
899
+ const id = pickString(model, ['id', 'modelID', 'model_id', 'modelId']);
900
+ if (id !== undefined) return id;
901
+ }
902
+ if (typeof model === 'string' && model !== '') return model;
903
+ return undefined;
904
+ }
905
+
906
+ /** V2 compaction 行正文 = summary + recent(非 completed 时才进对话;completed 由检查点承载) */
907
+ function v2CompactionBody(data: Record<string, unknown> | undefined): string {
908
+ if (data === undefined) return '';
909
+ return [data['summary'], data['recent']]
910
+ .filter((s): s is string => typeof s === 'string' && s.trim() !== '')
911
+ .join('\n\n')
912
+ .trim();
913
+ }
914
+
915
+ /** V2 shell 行 → 可见文本(无样本来源的保守兜底:命令 + 状态 + 输出) */
916
+ function v2ShellText(data: Record<string, unknown> | undefined): string {
917
+ if (data === undefined) return '';
918
+ const command = typeof data['command'] === 'string' ? data['command'] : '';
919
+ const output = typeof data['output'] === 'string' ? data['output'] : '';
920
+ const status = typeof data['status'] === 'string' ? data['status'] : '';
921
+ const head = '[shell' + (status === '' ? '' : ':' + status) + '] ' + command;
922
+ return [head, output].filter((s) => s.trim() !== '').join('\n').trim();
923
+ }
924
+
925
+ /**
926
+ * 家族入口:存在性 → 能力门 → 只读打开 → 解析,四步的错误各有稳定码。
927
+ *
928
+ * **库文件不存在 = 未安装**:不报码(与 detect 的 found:false 一致);
929
+ * 「存在但打不开 / 不是这个来源的库 / 查询失败」= 一条 `source-unreadable`(响亮)。
930
+ */
931
+ export async function readOpencodeFamily(
932
+ dbFile: string,
933
+ label: string,
934
+ deps?: SqliteDeps,
935
+ options?: FamilyReadOptions,
936
+ ): Promise<SqliteReadPart> {
937
+ if (!(await isFile(dbFile))) return { files: [], skipped: [], counts: {} };
938
+ const gate = await sqliteGate(label, deps);
939
+ if (gate !== undefined) return { files: [], skipped: [gate], counts: {} };
940
+ const opened = await sqliteOpen(dbFile, deps);
941
+ if (opened.db === null) return { files: [], skipped: [sqliteSkip(label, openDetail(opened.problem))], counts: {} };
942
+ const db = opened.db;
943
+ try {
944
+ const read = readOpencodeFamilyDatabase(db, options);
945
+ if (read === null) return { files: [], skipped: [sqliteSkip(label, 'shape-mismatch')], counts: {} };
946
+ return read;
947
+ } finally {
948
+ db.close();
949
+ }
950
+ }
951
+
952
+ /* ---------------- ④ opencode 本体的真值表路径 ---------------- */
953
+
954
+ /** 真值表路径函数的入参(与 `session-source.ts` 的 RootProbeOptions 同形,只取用得到的字段) */
955
+ export interface SqlitePathInput {
956
+ readonly homeDir: string;
957
+ readonly platform: string;
958
+ }
959
+
960
+ /**
961
+ * 三平台同形的 `<home>/.local/share/<product>/<name>`。
962
+ *
963
+ * 这条规则**Windows 也不走 %APPDATA%**(chat-import discovery.mjs:125-131 的
964
+ * `joinFor('win32', home, '.local','share', ...)`);read-claude-move 的 %APPDATA% 说法
965
+ * 与 chat-import 冲突,按「有出入以 chat-import 为准」取它(真值表同款结论)。
966
+ */
967
+ export function posixShareDbPath(platform: string, homeDir: string, product: string, name: string): string {
968
+ const p = normalizePlatform(platform);
969
+ return joinFor(p, posixLocalShare(p, homeDir), product, name);
970
+ }
971
+
972
+ /** opencode 的库:`<home>/.local/share/opencode/opencode.db` */
973
+ export function opencodeDbPath(opts: SqlitePathInput): string {
974
+ return posixShareDbPath(opts.platform, opts.homeDir, 'opencode', 'opencode.db');
975
+ }
976
+
977
+ /** opencode 的读盘入口(wiring 的 `read`) */
978
+ export async function readOpencode(
979
+ opts: SqlitePathInput & { readonly sqliteDeps?: SqliteDeps | undefined },
980
+ ): Promise<SessionReadOutcome<SqliteSessionFile>> {
981
+ const read = await readOpencodeFamily(opencodeDbPath(opts), 'opencode.db', opts.sqliteDeps);
982
+ return {
983
+ files: read.files,
984
+ readFindings: read.skipped,
985
+ extraCounts: { 'opencode.sessions': read.files.length, ...prefixCounts('opencode', read.counts) },
986
+ };
987
+ }
988
+
989
+ /** 把家族计数键加上来源前缀(避免多来源合并时撞键) */
990
+ export function prefixCounts(prefix: string, counts: Readonly<Record<string, number>>): Record<string, number> {
991
+ const out: Record<string, number> = {};
992
+ for (const [key, value] of Object.entries(counts)) out[prefix + '.' + key] = value;
993
+ return out;
994
+ }