dsh-config-manager 0.1.78 → 0.1.80

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 (302) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +401 -112
  26. package/lib/client.js +2899 -1525
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/ancestor.d.ts +22 -0
  82. package/lib/sync/ancestor.js +66 -2
  83. package/lib/sync/autosync-config.d.ts +73 -0
  84. package/lib/sync/autosync-config.js +89 -0
  85. package/lib/sync/autosync-scheduler.d.ts +80 -6
  86. package/lib/sync/autosync-scheduler.js +263 -40
  87. package/lib/sync/gist/gist-transport.d.ts +18 -1
  88. package/lib/sync/gist/gist-transport.js +42 -3
  89. package/lib/sync/gist/messages.d.ts +2 -0
  90. package/lib/sync/gist/messages.js +5 -0
  91. package/lib/sync/git/git-transport.d.ts +42 -3
  92. package/lib/sync/git/git-transport.js +146 -3
  93. package/lib/sync/merge.d.ts +47 -14
  94. package/lib/sync/merge.js +242 -222
  95. package/lib/sync/messages.d.ts +8 -0
  96. package/lib/sync/messages.js +19 -0
  97. package/lib/sync/risk.d.ts +19 -2
  98. package/lib/sync/risk.js +10 -3
  99. package/lib/sync/s3/messages.d.ts +6 -0
  100. package/lib/sync/s3/messages.js +13 -0
  101. package/lib/sync/s3/s3-transport.d.ts +9 -1
  102. package/lib/sync/s3/s3-transport.js +78 -6
  103. package/lib/sync/s3/sigv4.js +11 -2
  104. package/lib/sync/sync-engine.d.ts +212 -8
  105. package/lib/sync/sync-engine.js +514 -58
  106. package/lib/sync/sync-history.d.ts +11 -0
  107. package/lib/sync/sync-history.js +20 -0
  108. package/lib/sync/sync-state.d.ts +43 -0
  109. package/lib/sync/sync-state.js +44 -0
  110. package/lib/sync/sync-watcher.d.ts +76 -0
  111. package/lib/sync/sync-watcher.js +202 -0
  112. package/lib/sync/text-merge.d.ts +85 -0
  113. package/lib/sync/text-merge.js +513 -0
  114. package/lib/sync/transport.d.ts +48 -0
  115. package/lib/sync/transport.js +12 -0
  116. package/lib/sync/watch-scope.d.ts +38 -0
  117. package/lib/sync/watch-scope.js +95 -0
  118. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  119. package/lib/sync/webdav/webdav-transport.js +128 -11
  120. package/lib/ui/foreign-view.js +10 -0
  121. package/lib/ui/i18n.d.ts +9 -0
  122. package/lib/ui/i18n.js +22 -0
  123. package/lib/ui/rollback-view.d.ts +12 -0
  124. package/lib/ui/rollback-view.js +2 -0
  125. package/lib/ui/sync-settings-view.d.ts +47 -0
  126. package/lib/ui/sync-settings-view.js +24 -0
  127. package/lib/ui/types.d.ts +21 -0
  128. package/lib/utils/env-lock.d.ts +1 -0
  129. package/lib/utils/env-lock.js +7 -4
  130. package/package.json +6 -6
  131. package/src/adapters/credentials.ts +22 -4
  132. package/src/adapters/file-collection.ts +2 -1
  133. package/src/adapters/mcp.test.ts +235 -1
  134. package/src/adapters/mcp.ts +276 -5
  135. package/src/adapters/messages.ts +7 -0
  136. package/src/adapters/plugin-files.ts +2 -1
  137. package/src/adapters/plugins.test.ts +176 -2
  138. package/src/adapters/plugins.ts +107 -21
  139. package/src/adapters/pnpm-workspace.test.ts +89 -1
  140. package/src/adapters/pnpm-workspace.ts +127 -2
  141. package/src/adapters/prompts.ts +20 -3
  142. package/src/adapters/providers.ts +62 -4
  143. package/src/adapters/settings.ts +16 -3
  144. package/src/adapters/skills.test.ts +141 -0
  145. package/src/adapters/skills.ts +58 -2
  146. package/src/adapters/ui.ts +16 -3
  147. package/src/adapters/units-entries.test.ts +190 -0
  148. package/src/adapters/units.ts +50 -0
  149. package/src/adapters/workspaces.ts +2 -1
  150. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  151. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  152. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  153. package/src/client/common/Skeleton.tsx +32 -0
  154. package/src/client/common/api-base.test.ts +36 -0
  155. package/src/client/common/http.test.ts +12 -1
  156. package/src/client/common/http.ts +8 -2
  157. package/src/client/common/routes.ts +46 -3
  158. package/src/client/config-manager.module.css +321 -11
  159. package/src/client/home/HomePanel.tsx +14 -6
  160. package/src/client/import/ForeignImportView.tsx +3 -1
  161. package/src/client/import/import-wizard-steps.tsx +2 -1
  162. package/src/client/library/ArtifactRow.tsx +12 -2
  163. package/src/client/library/LibraryActions.tsx +6 -3
  164. package/src/client/library/LibraryPanel.tsx +21 -3
  165. package/src/client/library/RestorePlanView.tsx +4 -2
  166. package/src/client/locales.ts +3 -0
  167. package/src/client/market/market-api.test.ts +3 -2
  168. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  169. package/src/client/recovery/RollbackCard.tsx +7 -2
  170. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  171. package/src/client/recovery/recovery-locales.ts +20 -2
  172. package/src/client/recovery/recovery-view.test.ts +39 -1
  173. package/src/client/recovery/recovery-view.ts +23 -1
  174. package/src/client/run-store.test.ts +1 -1
  175. package/src/client/run-store.ts +4 -0
  176. package/src/client/sync/AutosyncCard.tsx +49 -6
  177. package/src/client/sync/MergeModal.tsx +220 -0
  178. package/src/client/sync/MergePreviewView.tsx +306 -0
  179. package/src/client/sync/SyncActivityBar.tsx +31 -0
  180. package/src/client/sync/SyncConfirmView.tsx +2 -1
  181. package/src/client/sync/SyncPanel.tsx +130 -87
  182. package/src/client/sync/SyncProbeRow.tsx +162 -0
  183. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  184. package/src/client/sync/WorkDirCard.tsx +44 -6
  185. package/src/client/sync/history-model.ts +4 -1
  186. package/src/client/sync/merge-view.test.ts +248 -0
  187. package/src/client/sync/merge-view.ts +398 -0
  188. package/src/client/sync/probe-view.test.ts +90 -0
  189. package/src/client/sync/probe-view.ts +63 -0
  190. package/src/client/sync/remote-device-view.test.ts +35 -0
  191. package/src/client/sync/remote-device-view.ts +25 -0
  192. package/src/client/sync/sync-api.test.ts +5 -3
  193. package/src/client/sync/sync-api.ts +212 -2
  194. package/src/client/sync/sync-entry.test.ts +78 -0
  195. package/src/client/sync/sync-entry.ts +53 -0
  196. package/src/client/sync/sync-locales.ts +234 -2
  197. package/src/client/sync/sync-view.test.ts +18 -1
  198. package/src/client/sync/sync-view.ts +20 -0
  199. package/src/core/backup.test.ts +41 -0
  200. package/src/core/backup.ts +20 -0
  201. package/src/core/mcp-connector-store.test.ts +265 -0
  202. package/src/core/mcp-connector-store.ts +516 -0
  203. package/src/core/messages-core.ts +4 -0
  204. package/src/core/messages.ts +46 -0
  205. package/src/core/phase3-host.ts +18 -1
  206. package/src/core/phase3-production-integration.test.ts +4 -0
  207. package/src/core/plugin-cli.test.ts +26 -0
  208. package/src/core/plugin-cli.ts +30 -4
  209. package/src/core/recovery-orchestrator.test.ts +104 -5
  210. package/src/core/recovery-orchestrator.ts +74 -17
  211. package/src/core/rollback-activity.test.ts +311 -0
  212. package/src/core/rollback-activity.ts +367 -0
  213. package/src/core/rollback-cross-session.test.ts +694 -0
  214. package/src/core/rollback-interrupted.test.ts +137 -0
  215. package/src/core/rollback-ledger.ts +32 -0
  216. package/src/core/rollback-merge.test.ts +130 -0
  217. package/src/core/rollback-merge.ts +78 -14
  218. package/src/core/rollback-point.ts +468 -91
  219. package/src/core/rollback-undo-journal.test.ts +508 -0
  220. package/src/core/rollback-undo-journal.ts +571 -0
  221. package/src/core/self-update.ts +2 -5
  222. package/src/core/text-fidelity.test.ts +189 -0
  223. package/src/core/text-fidelity.ts +197 -0
  224. package/src/core/types.ts +11 -3
  225. package/src/core/update-check.ts +2 -5
  226. package/src/core/validator.ts +14 -0
  227. package/src/foreign/bundle-origin.test.ts +265 -0
  228. package/src/foreign/bundle.ts +45 -5
  229. package/src/foreign/claude-code.test.ts +268 -0
  230. package/src/foreign/claude-code.ts +406 -25
  231. package/src/foreign/claude-plugins.test.ts +234 -0
  232. package/src/foreign/claude-sessions.test.ts +141 -0
  233. package/src/foreign/claude-sessions.ts +179 -23
  234. package/src/foreign/file-budget.test.ts +4 -1
  235. package/src/foreign/read-claude-code.test.ts +320 -0
  236. package/src/foreign/read-claude-code.ts +490 -71
  237. package/src/foreign/read-claude-plugins.ts +465 -0
  238. package/src/foreign/registry.ts +25 -2
  239. package/src/foreign/session-ir.test.ts +65 -0
  240. package/src/foreign/truth-table.test.ts +81 -0
  241. package/src/foreign/truth-table.ts +6 -3
  242. package/src/foreign/types.ts +162 -2
  243. package/src/index.facade.test.ts +26 -4
  244. package/src/index.ts +76 -19
  245. package/src/market/git-file-writer.test.ts +4 -0
  246. package/src/market/git-file-writer.ts +7 -1
  247. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  248. package/src/profiles/dsh-profile-launcher.ts +56 -1
  249. package/src/profiles/dsh-profile-shared.ts +8 -2
  250. package/src/routes/foreign.ts +8 -1
  251. package/src/routes/index.ts +1 -1
  252. package/src/routes/kit.ts +2 -2
  253. package/src/routes/sync.ts +176 -9
  254. package/src/schema/manifest-schema.test.ts +87 -2
  255. package/src/schema/manifest.ts +57 -1
  256. package/src/schema/types.ts +45 -2
  257. package/src/sync/ancestor.ts +76 -2
  258. package/src/sync/autosync-config.test.ts +73 -1
  259. package/src/sync/autosync-config.ts +124 -0
  260. package/src/sync/autosync-risk-tier.test.ts +38 -2
  261. package/src/sync/autosync-scheduler.test.ts +264 -12
  262. package/src/sync/autosync-scheduler.ts +296 -44
  263. package/src/sync/gist/gist-conditional-write.test.ts +203 -0
  264. package/src/sync/gist/gist-transport.ts +53 -5
  265. package/src/sync/gist/messages.ts +5 -0
  266. package/src/sync/git/git-probe.test.ts +80 -0
  267. package/src/sync/git/git-transport.test.ts +185 -0
  268. package/src/sync/git/git-transport.ts +178 -5
  269. package/src/sync/merge.test.ts +188 -55
  270. package/src/sync/merge.ts +282 -224
  271. package/src/sync/messages.ts +19 -0
  272. package/src/sync/risk.ts +29 -5
  273. package/src/sync/s3/messages.ts +13 -0
  274. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  275. package/src/sync/s3/s3-transport.test.ts +2 -1
  276. package/src/sync/s3/s3-transport.ts +87 -9
  277. package/src/sync/s3/sigv4.test.ts +4 -1
  278. package/src/sync/s3/sigv4.ts +11 -2
  279. package/src/sync/sync-device-identity.test.ts +267 -0
  280. package/src/sync/sync-engine.test.ts +410 -19
  281. package/src/sync/sync-engine.ts +602 -57
  282. package/src/sync/sync-history.test.ts +16 -1
  283. package/src/sync/sync-history.ts +26 -0
  284. package/src/sync/sync-state.test.ts +64 -0
  285. package/src/sync/sync-state.ts +79 -0
  286. package/src/sync/sync-watcher.test.ts +197 -0
  287. package/src/sync/sync-watcher.ts +246 -0
  288. package/src/sync/text-merge.test.ts +358 -0
  289. package/src/sync/text-merge.ts +554 -0
  290. package/src/sync/transport.ts +57 -0
  291. package/src/sync/watch-scope.test.ts +122 -0
  292. package/src/sync/watch-scope.ts +120 -0
  293. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  294. package/src/sync/webdav/webdav-transport.ts +137 -14
  295. package/src/ui/foreign-view.test.ts +34 -0
  296. package/src/ui/foreign-view.ts +10 -0
  297. package/src/ui/i18n.ts +22 -0
  298. package/src/ui/rollback-view.ts +14 -0
  299. package/src/ui/sync-settings-view.test.ts +28 -1
  300. package/src/ui/sync-settings-view.ts +43 -0
  301. package/src/ui/types.ts +22 -0
  302. package/src/utils/env-lock.ts +8 -5
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Claude Code **插件**目录的读盘层(宿主侧只读;2026-10-18 补)。
3
+ *
4
+ * 真实布局(本机实测取证;只记录键名,绝不过问值):
5
+ * - `plugins/known_marketplaces.json`:`{ "<市场名>": { source: { source, repo }, installLocation, lastUpdated } }`;
6
+ * - `plugins/marketplaces/<市场>/.claude-plugin/marketplace.json`:`{ name, description, owner, plugins: [...] }`;
7
+ * - `plugins/marketplaces/<市场>/plugins/<插件>/`(或 `external_plugins/<插件>/`);
8
+ * - 插件目录:`.claude-plugin/plugin.json`、commands/、skills/、agents/、hooks/、.mcp.json
9
+ * (`.mcp.json` 形如 `{ "<server>": { type, url, headers } }` —— **headers 里可能是真凭据**)。
10
+ *
11
+ * 三条结构性纪律:
12
+ * ① **绝不执行、绝不读取插件里的可执行代码内容**:commands/agents/hooks 只做「文件名 / 文件数」
13
+ * 统计;允许进包的只有技能文本与 `.mcp.json`(且 .mcp.json 原样交给翻译层,凭据剥离在
14
+ * kernel 的剥离器里做 —— 本层不复制、不改写凭据,只保证「不额外扩散」);
15
+ * ② **不猜**:启用清单只认两个有形状可自证的位置(settings.enabledPlugins 与 plugins/config.json
16
+ * 的可识别形状),认不出来就报 plugins-config-unknown-shape,绝不把任意 JSON 当插件清单;
17
+ * ③ **不静默**:市场和插件都读不到 / 找不到安装目录 / 触顶(插件数、单插件文件数)一律报稳定机器码。
18
+ *
19
+ * 归属规则(与 read-claude-code 一致):能落到某个插件上的发现写进该插件的 readFindings(由翻译层
20
+ * 原样带出);落不到插件上的(市场清单、启用清单、找不到安装目录、插件数触顶)进顶层 skipped。
21
+ *
22
+ * 为什么本文件不 import `node:fs`:`src/foreign/file-budget.test.ts` 的 B 档要求 import fs 的模块
23
+ * **恰好**等于白名单。这里复用 `session-read.ts`(共享只读内核)的原语,实现同样「只读、不跟随
24
+ * 符号链接、有上限」的行为,而不新增一个 fs 入口。
25
+ */
26
+ import path from 'node:path';
27
+ import { isRecord } from '../utils/guards.js';
28
+ import { stripUserInfo } from './kernel.js';
29
+ import { listDirSafe, readJsonSafe, statOrNull, toPosixPath, walkFiles } from './session-read.js';
30
+ import { readSkillUnits } from './read-claude-code.js';
31
+ /** 插件数上限触顶(count = 上限) */
32
+ export const MAX_PLUGINS_REACHED = 'max-plugins-reached';
33
+ /** 单插件文件数上限触顶(count = 上限) */
34
+ export const MAX_PLUGIN_FILES_REACHED = 'max-plugin-files-reached';
35
+ /** plugins/config.json 存在但形状认不出来(**绝不猜**) */
36
+ export const PLUGINS_CONFIG_UNKNOWN_SHAPE = 'plugins-config-unknown-shape';
37
+ const DEFAULT_MAX_FILE_BYTES = 8 * 1024 * 1024;
38
+ const DEFAULT_MAX_PLUGINS = 50;
39
+ const DEFAULT_MAX_PLUGIN_FILES = 200;
40
+ /** 目录走盘深度(与 session-read 的 WalkOptions 缺省一致) */
41
+ const WALK_MAX_DEPTH = 8;
42
+ /** 跳过原因:符号链接(与 read-claude-code 同码同义) */
43
+ const DETAIL_SYMLINK = 'symlink-skipped';
44
+ /** 有上限的整数解析:只用安全正整数(脏值一律回落缺省) */
45
+ function positiveLimit(value, fallback) {
46
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : fallback;
47
+ }
48
+ /** source.repo 或 source.url(两者都可能带 userinfo;剥离在调用处统一做) */
49
+ function repoOfSource(source) {
50
+ const repo = source['repo'];
51
+ if (typeof repo === 'string' && repo.trim() !== '')
52
+ return repo.trim();
53
+ const url = source['url'];
54
+ if (typeof url === 'string' && url.trim() !== '')
55
+ return url.trim();
56
+ return undefined;
57
+ }
58
+ /**
59
+ * 读 `plugins/known_marketplaces.json`。
60
+ * 文件不存在 = 没有登记任何市场(正常状态,不报码);存在但读不出来 / 形状不对 = source-unreadable。
61
+ */
62
+ async function readKnownMarketplaces(pluginsDir, maxFileBytes, skipped) {
63
+ const file = path.join(pluginsDir, 'known_marketplaces.json');
64
+ const jr = await readJsonSafe(file, maxFileBytes);
65
+ if (!jr.ok) {
66
+ if (jr.problem !== 'missing') {
67
+ skipped.push({ code: 'source-unreadable', origin: 'plugins/known_marketplaces.json', detail: jr.problem });
68
+ }
69
+ return [];
70
+ }
71
+ if (!isRecord(jr.value)) {
72
+ skipped.push({
73
+ code: 'source-unreadable',
74
+ origin: 'plugins/known_marketplaces.json',
75
+ detail: 'not-mapping',
76
+ });
77
+ return [];
78
+ }
79
+ const out = [];
80
+ for (const name of Object.keys(jr.value).sort()) {
81
+ const raw = jr.value[name];
82
+ const record = { name };
83
+ if (isRecord(raw)) {
84
+ const source = raw['source'];
85
+ if (isRecord(source)) {
86
+ const repo = repoOfSource(source);
87
+ // **userinfo 绝不进包**:https://user:token@host/x.git → https://host/x.git
88
+ if (repo !== undefined)
89
+ record.repo = stripUserInfo(repo).url;
90
+ }
91
+ const installLocation = raw['installLocation'];
92
+ if (typeof installLocation === 'string' && installLocation.trim() !== '') {
93
+ record.installLocation = path.resolve(installLocation.trim());
94
+ }
95
+ }
96
+ out.push(record);
97
+ }
98
+ return out;
99
+ }
100
+ /** settings.enabledPlugins 的启用键(**真值才算启用**;非对象 = 没有这份清单) */
101
+ function enabledSpecsFromSettings(settings) {
102
+ if (!isRecord(settings))
103
+ return [];
104
+ const enabled = settings['enabledPlugins'];
105
+ if (!isRecord(enabled))
106
+ return [];
107
+ return Object.keys(enabled)
108
+ .filter((key) => Boolean(enabled[key]))
109
+ .sort();
110
+ }
111
+ /**
112
+ * 按**形状自证**识别 plugins/config.json(认不出来返回 null,绝不猜):
113
+ * - `{ plugins: string[] }`:插件名清单;
114
+ * - `{ plugins: { [name@market]: boolean } }`:启用表;
115
+ * - `{ enabledPlugins: { ... } }`:与 settings 同形的启用表;
116
+ * - 顶层直接就是 `{ "name@market": true }`(键必须都带 @)。
117
+ */
118
+ function recognizePluginsConfig(value) {
119
+ if (!isRecord(value))
120
+ return null;
121
+ const plugins = value['plugins'];
122
+ if (Array.isArray(plugins)) {
123
+ if (plugins.length === 0)
124
+ return [];
125
+ if (!plugins.every((item) => typeof item === 'string' && item.trim() !== ''))
126
+ return null;
127
+ return [...new Set(plugins.map((item) => item.trim()))].sort();
128
+ }
129
+ if (isRecord(plugins)) {
130
+ if (!Object.values(plugins).every((item) => typeof item === 'boolean'))
131
+ return null;
132
+ return Object.keys(plugins).filter((key) => plugins[key] === true).sort();
133
+ }
134
+ const enabled = value['enabledPlugins'];
135
+ if (isRecord(enabled)) {
136
+ if (!Object.values(enabled).every((item) => typeof item === 'boolean'))
137
+ return null;
138
+ return Object.keys(enabled).filter((key) => enabled[key] === true).sort();
139
+ }
140
+ const keys = Object.keys(value);
141
+ if (keys.length > 0 && keys.every((key) => key.includes('@')) && keys.every((key) => typeof value[key] === 'boolean')) {
142
+ return keys.filter((key) => value[key] === true).sort();
143
+ }
144
+ return null;
145
+ }
146
+ /**
147
+ * 启用插件清单:settings.enabledPlugins 优先(有内容就用它),否则按形状读 plugins/config.json。
148
+ * 两者都没有 → 空数组、不报码(诚实:没有启用的插件)。
149
+ */
150
+ async function enabledPluginSpecs(pluginsDir, settings, maxFileBytes, skipped) {
151
+ const fromSettings = enabledSpecsFromSettings(settings);
152
+ if (fromSettings.length > 0)
153
+ return fromSettings;
154
+ const configPath = path.join(pluginsDir, 'config.json');
155
+ const jr = await readJsonSafe(configPath, maxFileBytes);
156
+ if (!jr.ok) {
157
+ if (jr.problem !== 'missing') {
158
+ skipped.push({ code: 'source-unreadable', origin: 'plugins/config.json', detail: jr.problem });
159
+ }
160
+ return [];
161
+ }
162
+ const recognized = recognizePluginsConfig(jr.value);
163
+ if (recognized === null) {
164
+ skipped.push({ code: 'source-unreadable', origin: 'plugins/config.json', detail: PLUGINS_CONFIG_UNKNOWN_SHAPE });
165
+ return [];
166
+ }
167
+ return recognized;
168
+ }
169
+ /** `name@marketplace` → { name, marketplace };没有 @ 或 @ 在边界上 = 只有名字 */
170
+ function splitPluginSpec(spec) {
171
+ const at = spec.lastIndexOf('@');
172
+ if (at <= 0 || at === spec.length - 1)
173
+ return { name: spec };
174
+ return { name: spec.slice(0, at), marketplace: spec.slice(at + 1) };
175
+ }
176
+ /** 某个父目录下名为 name 的条目(**不跟链接**:dirent 自带 isSymbolicLink) */
177
+ async function direntOf(target) {
178
+ const parent = path.dirname(target);
179
+ const base = path.basename(target);
180
+ for (const entry of await listDirSafe(parent)) {
181
+ if (entry.name === base)
182
+ return entry;
183
+ }
184
+ return null;
185
+ }
186
+ /**
187
+ * 定位插件安装目录(顺序:市场下 plugins/ → external_plugins/ → 市场 installLocation 的同名目录及其
188
+ * 两种标准子目录)。命中符号链接 → symlink(不跟随);都不在 → missing(由调用方报码)。
189
+ */
190
+ async function locatePluginDir(pluginsDir, name, marketplace, market) {
191
+ const candidates = [];
192
+ if (marketplace !== undefined) {
193
+ const marketDir = path.join(pluginsDir, 'marketplaces', marketplace);
194
+ candidates.push(path.join(marketDir, 'plugins', name));
195
+ candidates.push(path.join(marketDir, 'external_plugins', name));
196
+ }
197
+ else {
198
+ candidates.push(path.join(pluginsDir, name));
199
+ }
200
+ if (market?.installLocation !== undefined) {
201
+ candidates.push(path.join(market.installLocation, name));
202
+ candidates.push(path.join(market.installLocation, 'plugins', name));
203
+ candidates.push(path.join(market.installLocation, 'external_plugins', name));
204
+ }
205
+ let symlinkHit = false;
206
+ for (const candidate of candidates) {
207
+ const entry = await direntOf(candidate);
208
+ if (entry === null)
209
+ continue;
210
+ if (entry.isSymbolicLink()) {
211
+ symlinkHit = true;
212
+ continue;
213
+ }
214
+ if (entry.isDirectory())
215
+ return { kind: 'dir', dir: candidate };
216
+ }
217
+ return symlinkHit ? { kind: 'symlink' } : { kind: 'missing' };
218
+ }
219
+ /** 插件自述(.claude-plugin/plugin.json):解析失败用目录名并报码,绝不猜 */
220
+ async function readPluginManifest(pluginDir, fallbackName, maxFileBytes, findings) {
221
+ const out = { name: fallbackName };
222
+ const manifestPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
223
+ const jr = await readJsonSafe(manifestPath, maxFileBytes);
224
+ if (!jr.ok) {
225
+ if (jr.problem !== 'missing') {
226
+ findings.push({ code: 'source-unreadable', origin: 'plugin:' + fallbackName + '/plugin.json', detail: jr.problem });
227
+ }
228
+ return out;
229
+ }
230
+ if (!isRecord(jr.value)) {
231
+ findings.push({ code: 'source-unreadable', origin: 'plugin:' + fallbackName + '/plugin.json', detail: 'not-mapping' });
232
+ return out;
233
+ }
234
+ const name = jr.value['name'];
235
+ if (typeof name === 'string' && name.trim() !== '')
236
+ out.name = name.trim();
237
+ const version = jr.value['version'];
238
+ if (typeof version === 'string' && version.trim() !== '')
239
+ out.version = version.trim();
240
+ return out;
241
+ }
242
+ /**
243
+ * 读一个已定位的插件目录。**只读元数据与组件清单**:
244
+ * - skills:技能文本(唯一允许进包的文件内容,凭据扫描在翻译层);
245
+ * - .mcp.json:原样交翻译层(凭据剥离在 kernel);
246
+ * - commands / agents / hooks:**只数文件 / 只取文件名**,内容一个字节都不读。
247
+ */
248
+ async function readPluginAt(pluginDir, specName, marketplace, market, options) {
249
+ const findings = [];
250
+ const manifest = await readPluginManifest(pluginDir, specName, options.maxFileBytes, findings);
251
+ const plugin = { name: manifest.name, components: [] };
252
+ if (manifest.version !== undefined)
253
+ plugin.version = manifest.version;
254
+ if (options.installRel !== undefined)
255
+ plugin.installRel = options.installRel;
256
+ if (marketplace !== undefined)
257
+ plugin.marketplace = marketplace;
258
+ if (market?.repo !== undefined)
259
+ plugin.repo = market.repo;
260
+ /* skills(有总量上限:跨技能累计,触顶报 max-plugin-files-reached) */
261
+ const skills = [];
262
+ let filesUsed = 0;
263
+ let filesTruncated = false;
264
+ const units = await readSkillUnits(path.join(pluginDir, 'skills'), {
265
+ maxFiles: options.maxPluginFiles,
266
+ maxBytes: options.maxFileBytes,
267
+ origin: 'plugin:' + specName + '/skills',
268
+ skips: findings,
269
+ });
270
+ for (const unit of units) {
271
+ if (filesUsed >= options.maxPluginFiles) {
272
+ filesTruncated = true;
273
+ break;
274
+ }
275
+ const room = options.maxPluginFiles - filesUsed;
276
+ const files = unit.files.slice(0, room);
277
+ if (files.length < unit.files.length)
278
+ filesTruncated = true;
279
+ if (files.length === 0)
280
+ continue;
281
+ skills.push({ name: unit.name, files });
282
+ filesUsed += files.length;
283
+ }
284
+ if (skills.length > 0) {
285
+ plugin.skills = skills;
286
+ plugin.components.push('skills');
287
+ }
288
+ /* .mcp.json:原始 JSON 交给翻译层(本层不读 headers 的值语义,也不改写) */
289
+ const mcp = await readJsonSafe(path.join(pluginDir, '.mcp.json'), options.maxFileBytes);
290
+ if (mcp.ok) {
291
+ plugin.mcpJson = mcp.value;
292
+ plugin.components.push('mcp');
293
+ }
294
+ else if (mcp.problem !== 'missing') {
295
+ findings.push({ code: 'source-unreadable', origin: 'plugin:' + specName + '/.mcp.json', detail: mcp.problem });
296
+ }
297
+ /* commands/**:递归计数(只数,不读内容) */
298
+ const commandFiles = await walkFiles(path.join(pluginDir, 'commands'), {
299
+ maxDepth: WALK_MAX_DEPTH,
300
+ maxFiles: options.maxPluginFiles,
301
+ });
302
+ if (commandFiles.length > 0) {
303
+ plugin.commandCount = commandFiles.length;
304
+ plugin.components.push('commands');
305
+ }
306
+ if (commandFiles.length >= options.maxPluginFiles)
307
+ filesTruncated = true;
308
+ /* agents/*.md:只取文件名(不读内容) */
309
+ const agentFiles = await walkFiles(path.join(pluginDir, 'agents'), {
310
+ maxDepth: 0,
311
+ maxFiles: options.maxPluginFiles,
312
+ match: (name) => name.endsWith('.md'),
313
+ });
314
+ if (agentFiles.length > 0) {
315
+ plugin.agents = agentFiles.map((file) => file.name).sort();
316
+ plugin.components.push('agents');
317
+ }
318
+ if (agentFiles.length >= options.maxPluginFiles)
319
+ filesTruncated = true;
320
+ /* hooks/ 与 hooks.json:**只数文件数**(hooks 是执行面,内容一律不读) */
321
+ const hookFiles = await walkFiles(path.join(pluginDir, 'hooks'), {
322
+ maxDepth: WALK_MAX_DEPTH,
323
+ maxFiles: options.maxPluginFiles,
324
+ });
325
+ let hookCount = hookFiles.length;
326
+ if (hookCount >= options.maxPluginFiles)
327
+ filesTruncated = true;
328
+ const hooksJson = await statOrNull(path.join(pluginDir, 'hooks.json'));
329
+ if (hooksJson !== null && hooksJson.isFile())
330
+ hookCount += 1;
331
+ if (hookCount > 0) {
332
+ plugin.hookFiles = hookCount;
333
+ plugin.components.push('hooks');
334
+ }
335
+ if (filesTruncated) {
336
+ findings.push({
337
+ code: 'source-unreadable',
338
+ origin: 'plugin:' + specName,
339
+ detail: MAX_PLUGIN_FILES_REACHED,
340
+ count: options.maxPluginFiles,
341
+ });
342
+ }
343
+ if (findings.length > 0)
344
+ plugin.readFindings = findings;
345
+ return plugin;
346
+ }
347
+ export async function readClaudePlugins(opts) {
348
+ const skipped = [];
349
+ const plugins = [];
350
+ const counts = {};
351
+ const maxFileBytes = positiveLimit(opts.maxFileBytes, DEFAULT_MAX_FILE_BYTES);
352
+ const maxPlugins = positiveLimit(opts.limits?.maxPlugins, DEFAULT_MAX_PLUGINS);
353
+ const maxPluginFiles = positiveLimit(opts.limits?.maxPluginFiles, DEFAULT_MAX_PLUGIN_FILES);
354
+ const pluginsDir = path.join(opts.claudeDir, 'plugins');
355
+ const markets = await readKnownMarketplaces(pluginsDir, maxFileBytes, skipped);
356
+ const marketByName = new Map(markets.map((market) => [market.name, market]));
357
+ const specs = await enabledPluginSpecs(pluginsDir, opts.settings, maxFileBytes, skipped);
358
+ counts['marketplaces'] = markets.length;
359
+ // 「启用条数」:值为真值的键(含找不到安装目录的那些,它们会在 skipped 里各留一条)
360
+ counts['enabledPlugins'] = specs.length;
361
+ const limited = specs.slice(0, maxPlugins);
362
+ if (specs.length > maxPlugins) {
363
+ skipped.push({ code: 'source-unreadable', origin: 'plugins', detail: MAX_PLUGINS_REACHED, count: maxPlugins });
364
+ }
365
+ let pluginSkills = 0;
366
+ let pluginCommands = 0;
367
+ let pluginAgents = 0;
368
+ let pluginHooks = 0;
369
+ for (const spec of limited) {
370
+ const { name, marketplace } = splitPluginSpec(spec);
371
+ const market = marketplace !== undefined ? marketByName.get(marketplace) : undefined;
372
+ const located = await locatePluginDir(pluginsDir, name, marketplace, market);
373
+ if (located.kind === 'missing') {
374
+ // 有启用记录但找不到安装目录:如实报码(不产出半成品条目)
375
+ skipped.push({ code: 'source-unreadable', origin: 'plugin:' + name });
376
+ continue;
377
+ }
378
+ if (located.kind === 'symlink') {
379
+ skipped.push({ code: 'source-unreadable', origin: 'plugin:' + name, detail: DETAIL_SYMLINK });
380
+ continue;
381
+ }
382
+ // 安装位置只进溯源元数据(相对 plugins 目录的 POSIX 路径;逃出该目录时**省略**而不是回传机器路径)
383
+ const rel = toPosixPath(path.relative(pluginsDir, located.dir));
384
+ const installRel = rel !== '' && !rel.startsWith('..') ? rel : undefined;
385
+ const plugin = await readPluginAt(located.dir, name, marketplace, market, {
386
+ maxPluginFiles,
387
+ maxFileBytes,
388
+ ...(installRel !== undefined ? { installRel } : {}),
389
+ });
390
+ plugins.push(plugin);
391
+ pluginSkills += plugin.skills?.length ?? 0;
392
+ pluginCommands += plugin.commandCount ?? 0;
393
+ pluginAgents += plugin.agents?.length ?? 0;
394
+ pluginHooks += plugin.hookFiles ?? 0;
395
+ }
396
+ counts['plugins'] = plugins.length;
397
+ counts['pluginSkills'] = pluginSkills;
398
+ counts['pluginCommands'] = pluginCommands;
399
+ counts['pluginAgents'] = pluginAgents;
400
+ counts['pluginHooks'] = pluginHooks;
401
+ return { plugins, marketplaces: markets.map((market) => (market.repo !== undefined ? { name: market.name, repo: market.repo } : { name: market.name })), skipped, counts };
402
+ }
403
+ //# sourceMappingURL=read-claude-plugins.js.map
@@ -310,11 +310,34 @@ export function builtinForeignSources() {
310
310
  { rel: '.claude/skills', dir: true },
311
311
  { rel: '.claude/commands', dir: true },
312
312
  { rel: '.claude/projects', dir: true },
313
+ // 2026-10-18 补齐:插件目录 / 子代理定义 / 规则目录(本机实测存在 .claude/plugins 与 .claude/hooks)
314
+ { rel: '.claude/plugins', dir: true },
315
+ { rel: '.claude/agents', dir: true },
316
+ { rel: '.claude/rules', dir: true },
313
317
  ]);
314
- return { found: probed.paths.length > 0, paths: probed.paths, skipped: probed.skipped };
318
+ const paths = [...probed.paths];
319
+ const skipped = [...probed.skipped];
320
+ // CLAUDE_CONFIG_DIR 整体替换配置目录(文档取证:code.claude.com/docs/en/settings)。
321
+ // **不改既有 home 相对标签**(那会动 UI 文案与既有断言),只在覆盖生效时**追加**一组
322
+ // 带前缀的标签;真正的读取与 source-location-overridden 报码由读盘层负责。
323
+ const override = ctx.env['CLAUDE_CONFIG_DIR'];
324
+ if (override !== undefined && override.trim() !== '') {
325
+ const alt = await probeTree(override, [
326
+ { rel: 'settings.json' },
327
+ { rel: 'CLAUDE.md' },
328
+ { rel: 'skills', dir: true },
329
+ ], 'CLAUDE_CONFIG_DIR/');
330
+ paths.push(...alt.paths);
331
+ skipped.push(...alt.skipped);
332
+ }
333
+ return { found: paths.length > 0, paths, skipped };
315
334
  },
316
335
  async build(ctx) {
317
- const read = await readClaudeCode({ homeDir: ctx.homeDir, ...(ctx.limits !== undefined ? { limits: ctx.limits } : {}) });
336
+ const read = await readClaudeCode({
337
+ homeDir: ctx.homeDir,
338
+ env: ctx.env,
339
+ ...(ctx.limits !== undefined ? { limits: ctx.limits } : {}),
340
+ });
318
341
  // 会话转码版本必须由宿主解析后传入(绝不猜);缺省 = 一条都不转并整批报码
319
342
  if (ctx.targetSessionFormatVersion !== undefined) {
320
343
  read.input.targetSessionFormatVersion = ctx.targetSessionFormatVersion;
@@ -43,12 +43,14 @@ export const FOREIGN_TRUTH_TABLES = [
43
43
  /* ============ 配置类(v1 既有六个;本次**只补 probePaths/evidence**,路径表不动) ============ */
44
44
  {
45
45
  id: 'claude-code',
46
- defaults: all3('<home>/.claude', '<home>/.claude.json', '<home>/.claude/settings.json', '<home>/.claude/CLAUDE.md', '<home>/.claude/skills', '<home>/.claude/commands', '<home>/.claude/projects'),
47
- envKeys: [],
46
+ defaults: all3('<home>/.claude', '<home>/.claude.json', '<home>/.claude/settings.json', '<home>/.claude/CLAUDE.md', '<home>/.claude/skills', '<home>/.claude/commands', '<home>/.claude/projects',
47
+ // 2026-10-18 实测补齐:插件注册表(known_marketplaces.json)/ 子代理定义 / 规则目录
48
+ '<home>/.claude/plugins', '<home>/.claude/plugins/known_marketplaces.json', '<home>/.claude/agents', '<home>/.claude/rules'),
49
+ envKeys: [{ key: 'CLAUDE_CONFIG_DIR', mode: 'replace', note: '整体替换配置目录(命中即报 source-location-overridden)' }],
48
50
  autoRoots: true,
49
51
  evidence: 'measured',
50
- refs: 'chat-import §3.1 claude 行(projects + Claude-3p 第二根);本仓本机实测(~/.claude、~/.claude.json 存在)',
51
- dynamic: '会话在 ~/.claude/projects/<slug>/<uuid>.jsonl(v1 已实现会话转码)',
52
+ refs: 'chat-import §3.1 claude 行(projects + Claude-3p 第二根);本仓本机实测(~/.claude、~/.claude.json 存在);2026-10-18 本机实测 plugins/known_marketplaces.json 与 marketplaces/<市场>/.claude-plugin/{marketplace,plugin}.json 结构',
53
+ dynamic: '会话在 ~/.claude/projects/<slug>/<uuid>.jsonl(v1 已实现会话转码);项目级配置根来自 ~/.claude.json 的 projects 键(**只取真实存在的目录,绝不猜路径**);插件启用清单来自 settings.json 的 enabledPlugins(本机缺省为空 = 没有启用的插件,不报码)',
52
54
  },
53
55
  {
54
56
  id: 'hermes',
@@ -127,7 +127,23 @@ export type ForeignSkipCode =
127
127
  * 该来源**没有自动根**(竞品 `defaultRoots.chatgpt = null`)—— 必须由用户显式给出导出文件
128
128
  * 路径;本机自动探测永远不会命中,如实报码而不是假装「未安装」。
129
129
  */
130
- | 'source-needs-explicit-path';
130
+ | 'source-needs-explicit-path'
131
+ /** settings.json 的 permissions(allow/ask/deny):DSH 无权限规则文件对等结构,只报告条数 */
132
+ | 'unsupported-permissions'
133
+ /** settings.json 里其余未迁移的顶层键(只记键名与条数,**值一律不读**) */
134
+ | 'unsupported-settings-keys'
135
+ /** .claude/agents/*.md 子代理定义:DSH 的 subagent 是运行时 provider 注册制,无文件级对等结构 */
136
+ | 'unsupported-agents'
137
+ /** .claude/rules/*.md 带 frontmatter 作用域:合并进全局指令会丢作用域,故只报告 */
138
+ | 'unsupported-rules'
139
+ /** Claude Code 插件本体(插件 API / hooks / commands 的执行面):DSH 无法加载,**绝不复制其代码** */
140
+ | 'unsupported-plugins'
141
+ /** 会话记录里的 sessionId 与文件名 stem 不一致(DSH 以目录/文件名定位会话,故以文件名为准并如实报码) */
142
+ | 'session-id-mismatch'
143
+ /** 同名 MCP server 在多处出现(全局 / 项目级 / 插件):先到先得,其余只报告 */
144
+ | 'mcp-server-id-conflict'
145
+ /** 项目级扫描触顶(maxProjects):**不静默**,如实报码 */
146
+ | 'project-scan-truncated';
131
147
  /**
132
148
  * 冲突语义(**契约冻结**,§8.4):同 id 会话恒「不覆盖、跳过并报码」。
133
149
  *
@@ -163,7 +179,7 @@ export interface ClaudeCodeInput {
163
179
  skills?: ClaudeSkillInput[];
164
180
  /** ~/.claude/CLAUDE.md 原文(全局指令) */
165
181
  memory?: string;
166
- /** ~/.claude/commands/* 的普通文件数(本期只报不搬) */
182
+ /** ~/.claude/commands/** 的普通文件数(**递归**;只报不搬) */
167
183
  commandCount?: number;
168
184
  /** ~/.claude/projects/<项目>/<uuid>.jsonl 的原文(转码在翻译层做) */
169
185
  sessions?: ClaudeSessionInput[];
@@ -172,12 +188,120 @@ export interface ClaudeCodeInput {
172
188
  * 缺省 = 不转码任何会话并逐条报 session-format-version-unknown,绝不猜版本。
173
189
  */
174
190
  targetSessionFormatVersion?: number;
191
+ /**
192
+ * 实际使用的 Claude 配置目录绝对路径(CLAUDE_CONFIG_DIR 覆盖时为该值,否则 <home>/.claude)。
193
+ * **只进溯源元数据**,不进任何分区载荷。
194
+ */
195
+ configDir?: string;
196
+ /** 位置被 CLAUDE_CONFIG_DIR 覆盖(命中即报 source-location-overridden) */
197
+ configDirOverridden?: boolean;
198
+ /** 来源侧版本标记(~/.claude.json 的 lastVersionBase 等;拿不到即省略) */
199
+ sourceVersion?: string;
200
+ /** 项目级配置的已读形态(项目根来自 ~/.claude.json 的 projects 键;**不猜路径**) */
201
+ projects?: ClaudeProjectInput[];
202
+ /** 用户级 Claude Code 插件的已读形态(只读元数据与组件,**绝不执行插件代码**) */
203
+ plugins?: ClaudePluginInput[];
204
+ /** 已登记的市场(name + 来源仓库;URL 的 userinfo 已剥离) */
205
+ marketplaces?: {
206
+ name: string;
207
+ repo?: string;
208
+ }[];
209
+ }
210
+ /**
211
+ * 一个 Claude Code **项目级**配置目录的已读形态(纯数据;翻译层不做任何 fs 访问)。
212
+ *
213
+ * 位置口径(实测取证 2026-10-18,本机 ~/.claude.json 的 projects 键):项目根来自
214
+ * `~/.claude.json` 的 `projects` 键(绝对路径,Claude Code 自己写的权威清单)——
215
+ * **不按目录名猜项目路径**(猜 = 把会话归到不存在的目录)。
216
+ */
217
+ export interface ClaudeProjectInput {
218
+ /** 项目根绝对路径(**仅进溯源元数据与报码,不进任何分区载荷**) */
219
+ root: string;
220
+ /** `<root>/.claude` 是否存在(false = 该项目没有 Claude 配置,读盘层不产出任何 finding) */
221
+ hasClaudeDir?: boolean;
222
+ /** `<root>/.claude/settings.json`(已解析) */
223
+ settings?: unknown;
224
+ /** `<root>/.claude/settings.local.json`(已解析;值一律不读,只做诊断) */
225
+ settingsLocal?: unknown;
226
+ /** `<root>/CLAUDE.md` 原文(项目级指令) */
227
+ memory?: string;
228
+ /** `<root>/CLAUDE.local.md` 原文(本地覆盖指令) */
229
+ memoryLocal?: string;
230
+ /** `<root>/.claude/rules/*.md`:文件名 + 原文(带 frontmatter 的按 unsupported-rules 只报告) */
231
+ rules?: {
232
+ name: string;
233
+ text: string;
234
+ }[];
235
+ /** `<root>/.mcp.json`(已解析;Claude Code 的项目级 MCP 载体) */
236
+ mcpJson?: unknown;
237
+ /** `~/.claude.json` → `projects[<root>].mcpServers`(已解析) */
238
+ projectMcp?: unknown;
239
+ /** `<root>/.claude/skills/*` 的单元 */
240
+ skills?: ClaudeSkillInput[];
241
+ /** `<root>/.claude/commands/**` 的普通文件数(**递归**;只报不搬) */
242
+ commandCount?: number;
243
+ /** `<root>/.claude/agents/*.md`:名字 + 文件名(只报告,无文件级对等结构) */
244
+ agents?: {
245
+ name: string;
246
+ file: string;
247
+ }[];
248
+ /** 读盘层发现的问题(读不到 / 超限 / 非法)——由翻译层原样带出 */
249
+ readFindings?: ForeignSkip[];
250
+ }
251
+ /**
252
+ * 一个 Claude Code **插件**的已读形态(纯数据)。
253
+ *
254
+ * 三条结构性保证:
255
+ * ① **只读元数据与组件,绝不执行插件代码**(Claude 插件的 hooks/commands 是 JS 执行面);
256
+ * ② 安装位置/来源只进**溯源元数据**(bundle 的 foreign 块),不由本层写进任何分区;
257
+ * ③ 能无损复用的组件只有 skills 与 .mcp.json 里的 MCP 定义(凭据先剥离),其余一律报码。
258
+ */
259
+ export interface ClaudePluginInput {
260
+ /** 插件名(plugin.json 的 name;缺失时回落目录名) */
261
+ name: string;
262
+ /** 插件版本(plugin.json 的 version;缺失即省略) */
263
+ version?: string;
264
+ /** 来源市场名(known_marketplaces.json / marketplace.json 的键) */
265
+ marketplace?: string;
266
+ /** 来源仓库(owner/repo 或 URL;**已剥离 userinfo,绝不含凭据**) */
267
+ repo?: string;
268
+ /** 来源 ref / sha(若有) */
269
+ ref?: string;
270
+ /** 安装位置(相对 ~/.claude/plugins 的 POSIX 相对路径;进溯源用) */
271
+ installRel?: string;
272
+ /** 实际存在的组件种类(skills / commands / agents / hooks / mcp),供诊断文案用 */
273
+ components: string[];
274
+ /** 插件自带的 skills(name = 压平后的技能名) */
275
+ skills?: ClaudeSkillInput[];
276
+ /** 插件自带 `.mcp.json`(已解析) */
277
+ mcpJson?: unknown;
278
+ /** 插件自带 commands 的文件数(**递归**;只报不搬) */
279
+ commandCount?: number;
280
+ /** 插件自带 agents 的文件名(只报告) */
281
+ agents?: string[];
282
+ /** 插件自带 hooks 的文件数(只报告;绝不只是不执行) */
283
+ hookFiles?: number;
284
+ /** 读盘层发现的问题 */
285
+ readFindings?: ForeignSkip[];
175
286
  }
176
287
  /** 一个待转码的 Claude Code 会话文件 */
177
288
  export interface ClaudeSessionInput {
178
289
  /** 文件名(不含 .jsonl);同时作为 DSH 侧会话 id */
179
290
  id: string;
180
291
  text: string;
292
+ /**
293
+ * 记录里首个出现的 sessionId。与 id(文件名 stem)不一致时由翻译层报 session-id-mismatch:
294
+ * DSH 以「目录名/文件名」定位会话,故仍以文件名为准,但**不静默**。
295
+ */
296
+ recordSessionId?: string;
297
+ /**
298
+ * 确定性 createdAt 回落值(毫秒):读盘层给「最早可解析记录时间」,没有时间戳时给文件 mtime。
299
+ *
300
+ * 为什么必须由读盘层给:DSH 侧「同内容 → Skip」的判定是**逐字节**的,而会话产物里带着
301
+ * createdAt/updatedAt;若回落用 Date.now(),同一来源转码两次字节必然不同 ⇒ 重复导入恒判
302
+ * Conflict 而不是 Skip(实测缺陷)。由文件自身 mtime 回落即可让转码**幂等**。
303
+ */
304
+ createdAtMs?: number;
181
305
  }
182
306
  /** 一个 Hermes skill 单元:**已压平**的叶子技能名 + 目录内文件(category 只用于报告,不参与命名) */
183
307
  export interface HermesSkillInput {
@@ -269,4 +393,48 @@ export interface ForeignImportResult {
269
393
  */
270
394
  credentialRefs: string[];
271
395
  counts: Record<string, number>;
396
+ /**
397
+ * 溯源块(进 bundle 的 manifest.foreign;缺省 = 不写 —— 旧行为逐字不变)。
398
+ * 由**翻译层**装配(只有它同时看得到位置、项目根与插件清单)。
399
+ */
400
+ origin?: ForeignOrigin;
401
+ }
402
+ /**
403
+ * 外部来源的**溯源块**(进 bundle 的 manifest.foreign,见 src/schema/types.ts)。
404
+ *
405
+ * 为什么必须有:标准 bundle v1 的 manifest 原本没有任何「这包从哪来」的字段,落盘后
406
+ * 包与 DSH 自产备份**逐字同构**(实测缺陷)—— 事后无法复核来源、兼容性评分也因
407
+ * source.dshVersion 被填成目标机版本而被构造性拉满。
408
+ *
409
+ * 三条纪律:
410
+ * ① **只放标识与计数,绝不放值**(凭据引用只有名字;错误只有机器码);
411
+ * ② 绝对路径只允许出现在 projects[] 与 installPath 这类「用户自己机器的来源位置」上,
412
+ * 且**不得**写进任何分区载荷;
413
+ * ③ 缺字段即「不知道」,绝不填默认值假装知道。
414
+ */
415
+ export interface ForeignOrigin {
416
+ /** 来源 agent id(ForeignSourceId 的运行期字面量) */
417
+ source: string;
418
+ /** 实际读取的位置(相对 home 的 POSIX 路径;不含任何值) */
419
+ locations: string[];
420
+ /** 来源侧版本标记(如 Claude Code `~/.claude.json` 的 lastVersionBase;读不到即省略) */
421
+ sourceVersion?: string;
422
+ /** 被扫描的项目根绝对路径(仅溯源,供用户核对) */
423
+ projects?: string[];
424
+ /** 插件清单(名称 / 版本 / 市场 / 来源仓库 / ref / 组件种类;**不含凭据**) */
425
+ plugins?: {
426
+ name: string;
427
+ version?: string;
428
+ marketplace?: string;
429
+ repo?: string;
430
+ ref?: string;
431
+ components?: string[];
432
+ }[];
433
+ /** 跳过与诊断汇总(机器码 + 条数,**不含任何值**) */
434
+ skipped?: {
435
+ code: string;
436
+ count: number;
437
+ }[];
438
+ /** 需要用户在目标机补录的凭据引用名(只有名字) */
439
+ credentialRefs?: string[];
272
440
  }