@rexvane/piweb 0.3.14 → 0.3.16

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 (262) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +12 -9
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +16 -4
  6. package/.next/required-server-files.js +3 -1
  7. package/.next/required-server-files.json +3 -1
  8. package/.next/routes-manifest.json +20 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +7 -4
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/swarm/[id]/route.js +1 -0
  61. package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
  62. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
  63. package/.next/server/app/api/swarm/route.js +1 -0
  64. package/.next/server/app/api/swarm/route.js.nft.json +1 -0
  65. package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
  66. package/.next/server/app/api/tests/route.js +2 -0
  67. package/.next/server/app/api/tests/route.js.nft.json +1 -0
  68. package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
  69. package/.next/server/app/api/update/route.js +2 -2
  70. package/.next/server/app/api/update/route.js.nft.json +1 -1
  71. package/.next/server/app/api/version/route.js +1 -1
  72. package/.next/server/app/api/version/route.js.nft.json +1 -1
  73. package/.next/server/app/api/web-auth/route.js +1 -1
  74. package/.next/server/app/api/workspaces/route.js +1 -1
  75. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  76. package/.next/server/app/icon.svg/route.js +1 -1
  77. package/.next/server/app/index.html +1 -1
  78. package/.next/server/app/index.rsc +9 -6
  79. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
  80. package/.next/server/app/index.segments/_full.segment.rsc +9 -6
  81. package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
  82. package/.next/server/app/login/page.js +1 -1
  83. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/login.html +1 -1
  85. package/.next/server/app/login.rsc +8 -5
  86. package/.next/server/app/login.segments/_full.segment.rsc +8 -5
  87. package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
  88. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
  89. package/.next/server/app/page.js +1 -1
  90. package/.next/server/app/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app-paths-manifest.json +12 -9
  92. package/.next/server/chunks/496.js +1 -1
  93. package/.next/server/chunks/532.js +1 -1
  94. package/.next/server/chunks/686.js +6 -2
  95. package/.next/server/chunks/757.js +1 -0
  96. package/.next/server/chunks/775.js +3 -0
  97. package/.next/server/middleware-build-manifest.js +1 -1
  98. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  99. package/.next/server/pages/404.html +1 -1
  100. package/.next/server/pages/500.html +1 -1
  101. package/.next/server/server-reference-manifest.json +1 -1
  102. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
  103. package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
  104. package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
  105. package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
  106. package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
  107. package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
  108. package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
  109. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
  110. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
  111. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
  112. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
  113. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
  114. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
  115. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
  116. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
  117. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
  118. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
  119. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
  120. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
  121. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
  122. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
  123. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
  124. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
  125. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
  126. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
  127. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
  128. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
  129. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
  130. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
  131. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
  132. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
  133. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
  134. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
  135. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
  136. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
  137. package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
  138. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
  139. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
  140. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
  141. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
  142. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
  143. package/.next/static/css/d0a0162cb481abe8.css +1 -0
  144. package/.next/types/app/api/swarm/[id]/route.ts +353 -0
  145. package/.next/types/app/api/swarm/route.ts +353 -0
  146. package/.next/types/app/api/tests/route.ts +353 -0
  147. package/.next/types/routes.d.ts +4 -1
  148. package/.next/types/validator.ts +27 -0
  149. package/README.md +61 -7
  150. package/assets/archive.png +0 -0
  151. package/assets/growth-1.png +0 -0
  152. package/assets/growth-2.png +0 -0
  153. package/assets/import.png +0 -0
  154. package/assets/main.png +0 -0
  155. package/assets/outline.png +0 -0
  156. package/assets/prompts-1.png +0 -0
  157. package/assets/prompts-2.png +0 -0
  158. package/assets/providers-1.png +0 -0
  159. package/assets/providers-2.png +0 -0
  160. package/assets/providers-3.png +0 -0
  161. package/assets/trace-1.png +0 -0
  162. package/assets/trace-2.png +0 -0
  163. package/assets/trace-3.png +0 -0
  164. package/assets/trace-4.png +0 -0
  165. package/assets/viewer-1.png +0 -0
  166. package/assets/viewer-2.png +0 -0
  167. package/next.config.ts +6 -2
  168. package/package.json +10 -5
  169. package/scripts/launcher.mjs +1 -1
  170. package/scripts/managed-niubash-assets.mjs +146 -0
  171. package/scripts/pick-folder.ps1 +11 -2
  172. package/scripts/prepare-niubash-packages.mjs +182 -0
  173. package/scripts/release.mjs +11 -31
  174. package/scripts/run-test-file.mjs +34 -0
  175. package/scripts/smoke-managed-niubash.mjs +62 -0
  176. package/scripts/verify-package.mjs +53 -0
  177. package/scripts/verify-release-config.mjs +24 -0
  178. package/src/app/api/config/route.ts +4 -1
  179. package/src/app/api/security/route.ts +8 -3
  180. package/src/app/api/swarm/[id]/route.ts +28 -0
  181. package/src/app/api/swarm/route.ts +24 -0
  182. package/src/app/api/tests/route.ts +27 -0
  183. package/src/app/api/update/route.ts +8 -2
  184. package/src/app/globals.css +777 -0
  185. package/src/app/layout.tsx +13 -6
  186. package/src/components/AppShell.tsx +505 -199
  187. package/src/components/ChatInput.tsx +153 -26
  188. package/src/components/ChatWindow.tsx +46 -40
  189. package/src/components/ContextMeter.tsx +11 -17
  190. package/src/components/FileViewer.tsx +1 -1
  191. package/src/components/GitPanel.tsx +13 -11
  192. package/src/components/ModelSelector.tsx +19 -6
  193. package/src/components/PiMark.tsx +5 -38
  194. package/src/components/ProjectPanel.tsx +1 -0
  195. package/src/components/PromptPanel.tsx +132 -51
  196. package/src/components/SessionImportSection.tsx +53 -37
  197. package/src/components/SessionSidebar.tsx +159 -57
  198. package/src/components/SettingsPanel.tsx +330 -193
  199. package/src/components/SwarmCoordinatorPod.tsx +1021 -0
  200. package/src/components/TestInspectorPod.tsx +549 -0
  201. package/src/components/icons.tsx +23 -0
  202. package/src/hooks/usePiWeb.ts +112 -8
  203. package/src/i18n.tsx +59 -3
  204. package/src/lib/agent-manager.ts +182 -11
  205. package/src/lib/command-validation.ts +9 -1
  206. package/src/lib/delegation.ts +93 -0
  207. package/src/lib/managed-niubash.ts +231 -0
  208. package/src/lib/pi.ts +3 -2
  209. package/src/lib/runtime-activity.ts +27 -0
  210. package/src/lib/session-import/claude.ts +13 -2
  211. package/src/lib/session-import/codex.ts +44 -8
  212. package/src/lib/session-import/files.ts +14 -0
  213. package/src/lib/session-import/grok.ts +54 -24
  214. package/src/lib/session-import/index.ts +5 -0
  215. package/src/lib/session-import/lock.ts +25 -0
  216. package/src/lib/session-import/types.ts +12 -0
  217. package/src/lib/session-import/writer.ts +8 -3
  218. package/src/lib/skills-service.ts +16 -9
  219. package/src/lib/swarm-service.ts +371 -0
  220. package/src/lib/test-run-service.ts +122 -0
  221. package/src/lib/theme.ts +68 -1
  222. package/src/lib/trajectory.ts +1 -0
  223. package/src/lib/types.ts +14 -0
  224. package/src/lib/workflow.ts +55 -0
  225. package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
  226. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  227. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  228. package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
  229. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  253. package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
  259. package/.next/static/css/b2d35abc828d4713.css +0 -1
  260. package/assets/showcase.png +0 -0
  261. package/bin/pi.js +0 -50
  262. /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
@@ -0,0 +1,231 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import * as nodeModule from "node:module";
4
+ import path from "node:path";
5
+ import { createBashToolDefinition, type ToolDefinition } from "@earendil-works/pi-coding-agent";
6
+ import { APP_ROOT } from "./app-root";
7
+
8
+ const DISTRIBUTION_VERSION = "1.1.4-piweb.0";
9
+ const NIUBASH_VERSION = "1.1.4";
10
+ const SOURCE_REPOSITORY = "https://github.com/unixwin/niubash";
11
+ const SOURCE_TAG = "v1.1.4";
12
+
13
+ const PACKAGE_BY_ARCH = {
14
+ x64: {
15
+ packageName: "@rexvane/piweb-niubash-win32-x64",
16
+ directory: "piweb-niubash-win32-x64",
17
+ asset: "niubash-v1.1.4-win-x64.zip",
18
+ assetSha256: "9f543634288a316f7ed8e5a76ff794eca402cb21e74b8f8a551daffafde162f5",
19
+ machine: 0x8664,
20
+ },
21
+ arm64: {
22
+ packageName: "@rexvane/piweb-niubash-win32-arm64",
23
+ directory: "piweb-niubash-win32-arm64",
24
+ asset: "niubash-v1.1.4-win-arm64.zip",
25
+ assetSha256: "77e98ba8d7f6be70c35388bcc28bdbc883c06bcaafffbe186a88788ad32ccf00",
26
+ machine: 0xaa64,
27
+ },
28
+ } as const;
29
+
30
+ const LICENSE_HASHES = {
31
+ "NIUBASH.txt": "1290aae99e71239ee3154e7847830e49e7a36c758fa6d659d73fa752dc9f81c8",
32
+ "RUBASH.txt": "e426a75a20633f3088da22aef61a5a44d06f1f884c600f6011d00702c3989079",
33
+ "WINUXCMD.txt": "8ffc4ed56f5e6ab8183a7ece2e3aa6ea4800b0a4e8170d11bd60856f2dd760f3",
34
+ } as const;
35
+
36
+ export type SupportedArch = keyof typeof PACKAGE_BY_ARCH;
37
+
38
+ type PackageExport = {
39
+ schema?: unknown;
40
+ packageName?: unknown;
41
+ distributionVersion?: unknown;
42
+ niubashVersion?: unknown;
43
+ platform?: unknown;
44
+ arch?: unknown;
45
+ runtimeDir?: unknown;
46
+ shellPath?: unknown;
47
+ manifestPath?: unknown;
48
+ };
49
+
50
+ type LoadedPackage = { entryPath: string; value: unknown };
51
+
52
+ /**
53
+ * npm 安装(全局或本地)普通情况下由 Node 解析:npm 会把可选包提升到任意祖先
54
+ * node_modules,所以解析必须从包自己的 package.json 出发。仓库检出(开发 / CI /
55
+ * 发布前的 runtime 准备)没有安装这一步,此时按约定找 platform-packages/<dir>——
56
+ * 这与平台包发布后的目录布局一一对应,不引入第二套解析规则。
57
+ */
58
+ export function resolveManagedNiubashPackageEntry(packageName: string, arch: SupportedArch, root = APP_ROOT): LoadedPackage {
59
+ // 必须通过命名空间调用。webpack 会改写从 node:module 具名导入的 createRequire:
60
+ // 参数不是字符串字面量时把调用擦掉,运行时变成对 undefined 调用 resolve。
61
+ const loadPackage = nodeModule.createRequire(path.join(root, "package.json"));
62
+ const resolveEntry = loadPackage.resolve.bind(loadPackage);
63
+ try {
64
+ const entryPath = resolveEntry(packageName);
65
+ return { entryPath, value: loadPackage(packageName) };
66
+ } catch (error) {
67
+ const checkoutEntry = path.join(root, "platform-packages", PACKAGE_BY_ARCH[arch].directory, "index.cjs");
68
+ if (fs.existsSync(checkoutEntry)) return { entryPath: checkoutEntry, value: loadPackage(checkoutEntry) };
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export type ManagedNiubashRuntime = {
74
+ packageName: string;
75
+ distributionVersion: string;
76
+ niubashVersion: string;
77
+ arch: SupportedArch;
78
+ runtimeDir: string;
79
+ shellPath: string;
80
+ };
81
+
82
+ export type ManagedNiubashResolveOptions = {
83
+ platform?: string;
84
+ arch?: string;
85
+ /** Base for package resolution; tests and the prepare script point it at a checkout root. */
86
+ root?: string;
87
+ loadPackage?: (packageName: string, arch: SupportedArch) => LoadedPackage;
88
+ warn?: (message: string) => void;
89
+ };
90
+
91
+ function sha256File(filename: string): string {
92
+ return crypto.createHash("sha256").update(fs.readFileSync(filename)).digest("hex");
93
+ }
94
+
95
+ function readJson(filename: string): any {
96
+ const stat = fs.statSync(filename);
97
+ if (!stat.isFile() || stat.size > 2 * 1024 * 1024) throw new Error(`${path.basename(filename)} is missing or too large`);
98
+ return JSON.parse(fs.readFileSync(filename, "utf8"));
99
+ }
100
+
101
+ function samePath(left: string, right: string): boolean {
102
+ const a = path.resolve(left);
103
+ const b = path.resolve(right);
104
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
105
+ }
106
+
107
+ function peMachine(filename: string): number {
108
+ const data = fs.readFileSync(filename);
109
+ if (data.length < 64 || data[0] !== 0x4d || data[1] !== 0x5a) throw new Error(`${path.basename(filename)} is not a PE executable`);
110
+ const offset = data.readUInt32LE(0x3c);
111
+ if (offset + 6 > data.length || data.toString("ascii", offset, offset + 4) !== "PE\0\0") {
112
+ throw new Error(`${path.basename(filename)} has an invalid PE header`);
113
+ }
114
+ return data.readUInt16LE(offset + 4);
115
+ }
116
+
117
+ function validatePackage(loaded: LoadedPackage, arch: SupportedArch): ManagedNiubashRuntime {
118
+ const expected = PACKAGE_BY_ARCH[arch];
119
+ if (!loaded.value || typeof loaded.value !== "object") throw new Error(`${expected.packageName} has no runtime export`);
120
+ const exported = loaded.value as PackageExport;
121
+ const packageRoot = path.dirname(path.resolve(loaded.entryPath));
122
+ const runtimeDir = path.join(packageRoot, "runtime");
123
+ const shellPath = path.join(runtimeDir, "niu.exe");
124
+ const manifestPath = path.join(packageRoot, "runtime-manifest.json");
125
+
126
+ const checks: Array<[unknown, unknown, string]> = [
127
+ [exported.schema, 1, "export schema"],
128
+ [exported.packageName, expected.packageName, "export package name"],
129
+ [exported.distributionVersion, DISTRIBUTION_VERSION, "export distribution version"],
130
+ [exported.niubashVersion, NIUBASH_VERSION, "export niubash version"],
131
+ [exported.platform, "win32", "export platform"],
132
+ [exported.arch, arch, "export architecture"],
133
+ ];
134
+ for (const [actual, wanted, label] of checks) {
135
+ if (actual !== wanted) throw new Error(`${label} mismatch`);
136
+ }
137
+ if (typeof exported.runtimeDir !== "string" || !samePath(exported.runtimeDir, runtimeDir)) throw new Error("runtime directory escapes its package");
138
+ if (typeof exported.shellPath !== "string" || !samePath(exported.shellPath, shellPath)) throw new Error("shell path escapes its package");
139
+ if (typeof exported.manifestPath !== "string" || !samePath(exported.manifestPath, manifestPath)) throw new Error("manifest path escapes its package");
140
+
141
+ const packageInfo = readJson(path.join(packageRoot, "package.json"));
142
+ if (packageInfo.name !== expected.packageName || packageInfo.version !== DISTRIBUTION_VERSION) throw new Error("package identity mismatch");
143
+ if (packageInfo.os?.length !== 1 || packageInfo.os[0] !== "win32") throw new Error("package OS restriction mismatch");
144
+ if (packageInfo.cpu?.length !== 1 || packageInfo.cpu[0] !== arch) throw new Error("package CPU restriction mismatch");
145
+
146
+ const manifest = readJson(manifestPath);
147
+ if (manifest.schema !== 1
148
+ || manifest.source?.repository !== SOURCE_REPOSITORY
149
+ || manifest.source?.tag !== SOURCE_TAG
150
+ || manifest.source?.asset !== expected.asset
151
+ || manifest.source?.sha256 !== expected.assetSha256) {
152
+ throw new Error("runtime provenance mismatch");
153
+ }
154
+ if (!manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) throw new Error("runtime file manifest is missing");
155
+ const files = Object.entries(manifest.files) as Array<[string, unknown]>;
156
+ if (files.length < 300 || files.length > 1000) throw new Error(`runtime file count is invalid (${files.length})`);
157
+ for (const [relative, digest] of files) {
158
+ if (!relative || relative.includes("\\") || path.posix.isAbsolute(relative) || /^[A-Za-z]:/.test(relative) || relative.split("/").includes("..")) {
159
+ throw new Error(`unsafe runtime path: ${relative}`);
160
+ }
161
+ if (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest)) throw new Error(`invalid runtime hash: ${relative}`);
162
+ const filename = path.join(runtimeDir, ...relative.split("/"));
163
+ if (!fs.statSync(filename).isFile()) throw new Error(`runtime file is missing: ${relative}`);
164
+ if (sha256File(filename) !== digest) throw new Error(`runtime file failed integrity check: ${relative}`);
165
+ }
166
+ for (const required of ["niu.exe", "winuxcmd/usr/bin/winuxcmd.exe", "bundles/oh-my-niu/bundle.toml"]) {
167
+ if (!(required in manifest.files)) throw new Error(`runtime file is missing: ${required}`);
168
+ }
169
+ for (const executable of [shellPath, path.join(runtimeDir, "winuxcmd", "usr", "bin", "winuxcmd.exe")]) {
170
+ if (peMachine(executable) !== expected.machine) throw new Error(`${path.basename(executable)} architecture mismatch`);
171
+ }
172
+ for (const [name, digest] of Object.entries(LICENSE_HASHES)) {
173
+ if (sha256File(path.join(packageRoot, "licenses", name)) !== digest) throw new Error(`license integrity check failed: ${name}`);
174
+ }
175
+
176
+ return Object.freeze({
177
+ packageName: expected.packageName,
178
+ distributionVersion: DISTRIBUTION_VERSION,
179
+ niubashVersion: NIUBASH_VERSION,
180
+ arch,
181
+ runtimeDir,
182
+ shellPath,
183
+ });
184
+ }
185
+
186
+ function reasonOf(error: unknown, packageName: string): string {
187
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "MODULE_NOT_FOUND") return `${packageName} is not installed`;
188
+ return error instanceof Error ? error.message : String(error);
189
+ }
190
+
191
+ /** Resolve and fully validate the platform package. Failures deliberately fall back to Pi's normal Bash. */
192
+ export function resolveManagedNiubash(options: ManagedNiubashResolveOptions = {}): ManagedNiubashRuntime | null {
193
+ const platform = options.platform ?? process.platform;
194
+ const arch = options.arch ?? process.arch;
195
+ if (platform !== "win32") return null;
196
+ const expected = PACKAGE_BY_ARCH[arch as SupportedArch];
197
+ if (!expected) {
198
+ (options.warn ?? console.warn)(`[piweb] Managed niubash does not support Windows ${arch}; using Pi's configured/default Bash.`);
199
+ return null;
200
+ }
201
+ const supported = arch as SupportedArch;
202
+ try {
203
+ const loaded = options.loadPackage
204
+ ? options.loadPackage(expected.packageName, supported)
205
+ : resolveManagedNiubashPackageEntry(expected.packageName, supported, options.root);
206
+ return validatePackage(loaded, supported);
207
+ } catch (error) {
208
+ (options.warn ?? console.warn)(
209
+ `[piweb] Managed niubash is unavailable (${reasonOf(error, expected.packageName)}); using Pi's configured/default Bash. `
210
+ + "Reinstall @rexvane/piweb to restore the bundled Windows shell (`npm run prepare:niubash` in a checkout).",
211
+ );
212
+ return null;
213
+ }
214
+ }
215
+
216
+ const globalForManagedNiubash = globalThis as typeof globalThis & {
217
+ __piWebManagedNiubash?: { resolved: ManagedNiubashRuntime | null };
218
+ };
219
+
220
+ function defaultRuntime(): ManagedNiubashRuntime | null {
221
+ if (!globalForManagedNiubash.__piWebManagedNiubash) {
222
+ globalForManagedNiubash.__piWebManagedNiubash = { resolved: resolveManagedNiubash() };
223
+ }
224
+ return globalForManagedNiubash.__piWebManagedNiubash.resolved;
225
+ }
226
+
227
+ /** Same-name custom tool intentionally replaces Pi's built-in bash only inside PiWeb sessions. */
228
+ export function createManagedNiubashTools(cwd: string, options?: ManagedNiubashResolveOptions): ToolDefinition[] {
229
+ const runtime = options ? resolveManagedNiubash(options) : defaultRuntime();
230
+ return runtime ? [createBashToolDefinition(cwd, { shellPath: runtime.shellPath }) as ToolDefinition] : [];
231
+ }
package/src/lib/pi.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type SettingsManager,
10
10
  DefaultPackageManager,
11
11
  DefaultResourceLoader,
12
+ type InlineExtension,
12
13
  ModelRuntime,
13
14
  ProjectTrustStore,
14
15
  SessionManager,
@@ -157,10 +158,10 @@ function loaderKey(cwd: string): string {
157
158
  }
158
159
 
159
160
  /** 每个热会话必须拥有自己的扩展 runtime;这里只创建,不加入发现缓存。 */
160
- export async function createSessionResourceLoader(cwd: string): Promise<DefaultResourceLoader> {
161
+ export async function createSessionResourceLoader(cwd: string, extensionFactories: InlineExtension[] = []): Promise<DefaultResourceLoader> {
161
162
  const settingsManager = getSettingsManager(cwd);
162
163
  settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
163
- const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager });
164
+ const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager, extensionFactories });
164
165
  const reload = loader.reload.bind(loader);
165
166
  loader.reload = async (options) => {
166
167
  settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
@@ -0,0 +1,27 @@
1
+ type ActivityKind = "session" | "swarm" | "tests";
2
+ interface Registry { active: Map<symbol, ActivityKind>; maintenance: boolean }
3
+
4
+ // Route bundles must share a process-wide gate, not separate module-local registries.
5
+ const globals = globalThis as typeof globalThis & { __piwebRuntimeActivity?: Registry };
6
+ const registry = globals.__piwebRuntimeActivity ??= { active: new Map(), maintenance: false };
7
+
8
+ export class RuntimeBusyError extends Error {}
9
+
10
+ export function beginActivity(kind: ActivityKind): () => void {
11
+ if (registry.maintenance) throw new RuntimeBusyError("update maintenance is active; wait for the release to finish");
12
+ const token = Symbol(kind);
13
+ registry.active.set(token, kind);
14
+ return () => { registry.active.delete(token); };
15
+ }
16
+
17
+ export function beginMaintenance(): () => void {
18
+ if (registry.maintenance || registry.active.size) {
19
+ throw new RuntimeBusyError("work is active; wait for sessions, swarms and tests to finish before entering the maintenance window");
20
+ }
21
+ registry.maintenance = true;
22
+ let released = false;
23
+ return () => {
24
+ if (!released) registry.maintenance = false;
25
+ released = true;
26
+ };
27
+ }
@@ -160,6 +160,7 @@ export const claudeSource: ImportSourceModule = {
160
160
  const skipped: string[] = [];
161
161
  let sidechains = 0;
162
162
  let synthetic = 0;
163
+ let runtimeContext = 0;
163
164
  let projectPath = summary.projectPath;
164
165
  let model = summary.model;
165
166
  let provider: string | undefined;
@@ -169,6 +170,12 @@ export const claudeSource: ImportSourceModule = {
169
170
  sidechains += 1;
170
171
  continue;
171
172
  }
173
+ // Claude 的 attachment/system 是技能列表、token 提醒、环境快照等运行时上下文;
174
+ // pi 会按目标工作区生成自己的版本,不能伪装成真人 user 消息,但要在报告中明确告知。
175
+ if (record.type === "attachment" || record.type === "system") {
176
+ runtimeContext += 1;
177
+ continue;
178
+ }
172
179
  if (typeof record.cwd === "string" && record.cwd.trim()) projectPath = projectPath ?? record.cwd;
173
180
  const message = record.message as Record<string, unknown> | undefined;
174
181
  if (!message) continue;
@@ -206,9 +213,12 @@ export const claudeSource: ImportSourceModule = {
206
213
  if (record.type === "user") {
207
214
  const { text: textBlocks, results } = splitUserContent((message as { content?: unknown }).content);
208
215
  const plain = textBlocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
209
- const keepText = textBlocks.filter((block) => block.type !== "text");
216
+ const nonText = textBlocks.filter((block) => block.type !== "text");
210
217
  if (plain && !isSynthetic(plain.trim()) && !/^Caveat:/i.test(plain.trim())) {
211
- entries.push({ type: "message", message: { role: "user", content: [...keepText, { type: "text", text: plain }], timestamp } as ImportedMessage });
218
+ entries.push({ type: "message", message: { role: "user", content: [...nonText, { type: "text", text: plain }], timestamp } as ImportedMessage });
219
+ } else if (!plain && nonText.length) {
220
+ // 图片可以是用户消息的全部内容,不能因为没有文字就把整轮丢掉
221
+ entries.push({ type: "message", message: { role: "user", content: nonText, timestamp } as ImportedMessage });
212
222
  } else if (plain) {
213
223
  synthetic += 1;
214
224
  }
@@ -223,6 +233,7 @@ export const claudeSource: ImportSourceModule = {
223
233
 
224
234
  if (sidechains) skipped.push(`跳过 ${sidechains} 条侧链(子代理)记录`);
225
235
  if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入(命令回显/系统提醒)`);
236
+ if (runtimeContext) skipped.push(`跳过 ${runtimeContext} 条源工具运行时上下文(pi 会按当前工作区重新生成)`);
226
237
  if (!entries.some((entry) => entry.type === "message")) return null;
227
238
  return { summary: { ...summary, projectPath, model, provider }, entries, skipped };
228
239
  },
@@ -7,10 +7,10 @@
7
7
  */
8
8
  import path from "node:path";
9
9
  import fs from "node:fs/promises";
10
- import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
10
+ import { eachJsonLine, listFiles, readHead, readTail, reachedLimit, statMtime } from "./files";
11
11
  import { codexRoot } from "./paths";
12
12
  import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
13
- import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
13
+ import { cleanTitle, compactBlocks, dataUrlImageBlock, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
14
14
 
15
15
  const ROOT = codexRoot;
16
16
 
@@ -80,6 +80,7 @@ function messageBlocks(payload: Record<string, unknown>): ImportedBlock[] {
80
80
  if (!Array.isArray(content)) return compactBlocks([textBlock(typeof content === "string" ? content : undefined)]);
81
81
  return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
82
82
  const block = raw as Record<string, unknown>;
83
+ if (block?.type === "input_image") return [dataUrlImageBlock(block.image_url, block.mime_type)];
83
84
  if (typeof block?.text === "string") return [textBlock(block.text)];
84
85
  return [];
85
86
  }));
@@ -95,6 +96,17 @@ function reasoningText(payload: Record<string, unknown>): ImportedBlock | null {
95
96
  return null;
96
97
  }
97
98
 
99
+ /** 文件 mtime 可能因复制/恢复失真;Codex 每行自带时间,末尾记录才是权威的最近活动时间 */
100
+ function latestTimestamp(records: Record<string, unknown>[]): number | undefined {
101
+ let latest: number | undefined;
102
+ for (const record of records) {
103
+ const payload = record.payload as Record<string, unknown> | undefined;
104
+ const at = toEpochMs(record.timestamp) ?? toEpochMs(payload?.timestamp);
105
+ if (at !== undefined) latest = Math.max(latest ?? at, at);
106
+ }
107
+ return latest;
108
+ }
109
+
98
110
  /** 标题:Codex 不写标题字段,用首条真人用户消息(rollout 头几十行里就有) */
99
111
  function titleFrom(records: Record<string, unknown>[]): string | undefined {
100
112
  for (const record of records) {
@@ -113,10 +125,22 @@ export const codexSource: ImportSourceModule = {
113
125
 
114
126
  async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
115
127
  const limit = options?.limit ?? 0;
116
- const candidates = await sortByRecency(await listFiles(ROOT(), { match: (name) => name.startsWith("rollout-") && name.endsWith(".jsonl") }));
128
+ const files = await listFiles(ROOT(), { match: (name) => name.startsWith("rollout-") && name.endsWith(".jsonl") });
129
+ // 只读每个文件末尾 64KB 就能按记录时间正确排序;不能先按 mtime 截断,否则复制过的旧文件会挤掉真正最近的会话。
130
+ const candidates = await Promise.all(files.map(async (file) => {
131
+ const mtime = await statMtime(file);
132
+ try {
133
+ const updatedAt = latestTimestamp([...eachJsonLine(await readTail(file))]);
134
+ return { file, mtime, updatedAt: updatedAt ?? mtime };
135
+ } catch {
136
+ return { file, mtime, updatedAt: mtime };
137
+ }
138
+ }));
139
+ candidates.sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
140
+
117
141
  const out: ExternalSessionSummary[] = [];
118
- for (const { file, mtime } of candidates) {
119
- if (reachedLimit(out.length, limit)) break; // 只读最近的若干条
142
+ for (const { file, mtime, updatedAt } of candidates) {
143
+ if (reachedLimit(out.length, limit)) break;
120
144
  let head: string;
121
145
  try {
122
146
  head = await readHead(file);
@@ -136,7 +160,7 @@ export const codexSource: ImportSourceModule = {
136
160
  model: meta.model,
137
161
  provider: meta.provider,
138
162
  createdAt: meta.createdAt ?? mtime,
139
- updatedAt: mtime,
163
+ updatedAt,
140
164
  location: file,
141
165
  });
142
166
  }
@@ -154,11 +178,18 @@ export const codexSource: ImportSourceModule = {
154
178
  const skipped: string[] = [];
155
179
  let synthetic = 0;
156
180
  let reasoningSkipped = 0;
181
+ let runtimeContext = 0;
157
182
  let meta: Meta = { cwd: summary.projectPath, createdAt: summary.createdAt, model: summary.model, provider: summary.provider };
158
183
 
159
184
  for (const record of eachJsonLine(text)) {
160
185
  if (record.type === "session_meta") {
161
186
  meta = { ...meta, ...metaFrom([record]) };
187
+ const payload = record.payload as Record<string, unknown> | undefined;
188
+ if (payload?.base_instructions) runtimeContext += 1;
189
+ continue;
190
+ }
191
+ if (record.type === "world_state" || record.type === "turn_context") {
192
+ runtimeContext += 1;
162
193
  continue;
163
194
  }
164
195
  if (record.type !== "response_item") continue;
@@ -169,11 +200,15 @@ export const codexSource: ImportSourceModule = {
169
200
  switch (payload.type) {
170
201
  case "message": {
171
202
  const role = payload.role;
172
- if (role === "developer") continue; // 系统提示,不进对话
203
+ if (role === "developer") {
204
+ runtimeContext += 1; // 系统/开发者上下文由 pi 按当前工作区重新生成
205
+ continue;
206
+ }
173
207
  const blocks = messageBlocks(payload);
174
208
  const plain = blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
175
209
  if (role === "user") {
176
- if (!plain || isSynthetic(plain)) {
210
+ // 只有图片的真人输入也必须保留;纯文本的环境/AGENTS 注入仍不冒充用户消息
211
+ if (!blocks.length || (plain && isSynthetic(plain))) {
177
212
  synthetic += 1;
178
213
  continue;
179
214
  }
@@ -217,6 +252,7 @@ export const codexSource: ImportSourceModule = {
217
252
 
218
253
  if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入`);
219
254
  if (reasoningSkipped) skipped.push(`跳过 ${reasoningSkipped} 条仅含加密内容的推理记录`);
255
+ if (runtimeContext) skipped.push(`跳过 ${runtimeContext} 条源工具运行时上下文(pi 会按当前工作区重新生成)`);
220
256
  if (!entries.some((entry) => entry.type === "message")) return null;
221
257
  return { summary: { ...summary, ...meta, projectPath: meta.cwd, model: meta.model, provider: meta.provider }, entries, skipped };
222
258
  },
@@ -13,6 +13,20 @@ export async function readHead(file: string, bytes = 64 * 1024): Promise<string>
13
13
  }
14
14
  }
15
15
 
16
+ /** 读文件末尾若干字节;首行可能是被截断的半行,JSONL 解析时会自然跳过 */
17
+ export async function readTail(file: string, bytes = 64 * 1024): Promise<string> {
18
+ const handle = await fs.open(file, "r");
19
+ try {
20
+ const size = (await handle.stat()).size;
21
+ const length = Math.min(size, bytes);
22
+ const buffer = Buffer.alloc(length);
23
+ const { bytesRead } = await handle.read(buffer, 0, length, size - length);
24
+ return buffer.subarray(0, bytesRead).toString("utf8");
25
+ } finally {
26
+ await handle.close();
27
+ }
28
+ }
29
+
16
30
  /** 逐行解析 JSONL;单行坏掉只跳过该行,不放弃整个文件 */
17
31
  export function* eachJsonLine(text: string): Generator<Record<string, unknown>> {
18
32
  for (const line of text.split("\n")) {
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import path from "node:path";
8
8
  import fs from "node:fs/promises";
9
- import { eachJsonLine, listFiles, readHead, reachedLimit, sortByRecency } from "./files";
9
+ import { eachJsonLine, listFiles, readHead, reachedLimit, statMtime } from "./files";
10
10
  import { grokRoot } from "./paths";
11
11
  import type { ExternalSessionSummary, ImportedBlock, ImportedEntry, ImportedSession, ImportSourceModule, ScanOptions } from "./types";
12
- import { cleanTitle, compactBlocks, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
12
+ import { cleanTitle, compactBlocks, dataUrlImageBlock, outputBlocks, parseArguments, textBlock, toEpochMs } from "./types";
13
13
 
14
14
  const ROOT = grokRoot;
15
15
 
@@ -30,10 +30,10 @@ function decodeProjectDir(name: string): string | undefined {
30
30
  function importable(records: Record<string, unknown>[]): boolean {
31
31
  for (const record of records) {
32
32
  if (record.type === "assistant" || record.type === "tool_result") return true;
33
- if (record.type === "reasoning" && typeof record.summary === "string" && record.summary.trim()) return true;
33
+ if (record.type === "reasoning" && reasoningText(record.summary)) return true;
34
34
  if (record.type !== "user" || record.synthetic_reason) continue;
35
35
  const blocks = userBlocks(record.content);
36
- if (blocks.some((block) => block.type === "text" && block.text.replace(/<[^>]+>/g, "").trim())) return true;
36
+ if (blocks.some((block) => block.type === "image" || (block.type === "text" && block.text.replace(/<[^>]+>/g, "").trim()))) return true;
37
37
  }
38
38
  return false;
39
39
  }
@@ -46,41 +46,69 @@ async function hasRealMessage(history: string): Promise<boolean> {
46
46
  }
47
47
  }
48
48
 
49
- /** user 的 content 可能是字符串或块数组 */function userBlocks(content: unknown): ImportedBlock[] {
49
+ /** user 的 content 可能是字符串、文本块或 data URL 图片块 */
50
+ function userBlocks(content: unknown): ImportedBlock[] {
50
51
  if (typeof content === "string") return compactBlocks([textBlock(content)]);
51
52
  if (!Array.isArray(content)) return [];
52
53
  return compactBlocks(content.flatMap((raw): Array<ImportedBlock | null> => {
53
54
  if (typeof raw === "string") return [textBlock(raw)];
54
55
  const block = raw as Record<string, unknown>;
55
56
  if (typeof block?.text === "string") return [textBlock(block.text)];
57
+ if (block?.type === "image") return [dataUrlImageBlock(block.url, block.mime ?? block.mimeType)];
56
58
  return [];
57
59
  }));
58
60
  }
59
61
 
62
+ /** Grok 新版把 reasoning.summary 从字符串改成了 summary_text 块数组;两代格式都要读 */
63
+ function reasoningText(summary: unknown): string {
64
+ if (typeof summary === "string") return summary.trim();
65
+ if (!Array.isArray(summary)) return "";
66
+ return summary
67
+ .map((item) => {
68
+ if (typeof item === "string") return item;
69
+ const block = item as { text?: unknown; summary_text?: unknown };
70
+ return typeof block?.text === "string" ? block.text : typeof block?.summary_text === "string" ? block.summary_text : "";
71
+ })
72
+ .filter(Boolean)
73
+ .join("\n")
74
+ .trim();
75
+ }
76
+
60
77
  export const grokSource: ImportSourceModule = {
61
78
  id: "grok",
62
79
 
63
80
  async scan(options?: ScanOptions): Promise<ExternalSessionSummary[]> {
64
- // 先按 chat_history 的 mtime 排序(只 stat,不读内容),再从最近往下读:
65
- // summary.json 里有标题/模型/时间/消息数,读它即可,正文只在确认有真实对话时碰一下头部
66
81
  const limit = options?.limit ?? 0;
67
82
  const files = await listFiles(ROOT(), { match: (name) => name === "summary.json" });
68
- const candidates = await sortByRecency(files.map((file) => path.join(path.dirname(file), "chat_history.jsonl")));
69
- const out: ExternalSessionSummary[] = [];
70
- for (const { file: history, mtime } of candidates) {
71
- if (reachedLimit(out.length, limit)) break; // 只读最近的若干条
72
- const dir = path.dirname(history);
73
- let summary: Record<string, unknown>;
83
+ // summary.updated_at / last_active_at 是 Grok 的权威活动时间。必须先按它排序再 LIMIT,
84
+ // 不能用复制、恢复备份时会变化的 chat_history mtime 决定“最近 15 条”。
85
+ const candidates = (await Promise.all(files.map(async (summaryFile) => {
86
+ const dir = path.dirname(summaryFile);
87
+ const history = path.join(dir, "chat_history.jsonl");
74
88
  try {
75
- summary = JSON.parse(await fs.readFile(path.join(dir, "summary.json"), "utf8")) as Record<string, unknown>;
89
+ const summary = JSON.parse(await fs.readFile(summaryFile, "utf8")) as Record<string, unknown>;
90
+ const mtime = await statMtime(history);
91
+ if (mtime === undefined) return null;
92
+ return {
93
+ dir,
94
+ history,
95
+ summary,
96
+ mtime,
97
+ updatedAt: toEpochMs(summary.updated_at ?? summary.last_active_at) ?? mtime,
98
+ };
76
99
  } catch {
77
- continue;
100
+ return null;
78
101
  }
79
- if (mtime === undefined) continue; // 没有正文就没什么可导
102
+ }))).filter((candidate): candidate is NonNullable<typeof candidate> => candidate !== null)
103
+ .sort((left, right) => right.updatedAt - left.updatedAt);
104
+
105
+ const out: ExternalSessionSummary[] = [];
106
+ for (const { dir, history, summary, mtime, updatedAt } of candidates) {
107
+ if (reachedLimit(out.length, limit)) break;
80
108
  const info = summary.info as Record<string, unknown> | undefined;
81
- const numMessages = Number(summary.num_messages ?? summary.num_chat_messages);
109
+ // num_messages 包含 trace/内部事件;num_chat_messages 才对应 chat_history 的会话记录数
110
+ const numMessages = Number(summary.num_chat_messages ?? summary.num_messages);
82
111
  // 子代理会话(grok 的 spawn_subagent:subagent / subagent_fork / subagent_resume,本机 24 条)
83
- // 以及它们的派生:agent_name 是 general-purpose / explore 之类,不是你在用的那条对话
84
112
  if (typeof summary.session_kind === "string" && summary.session_kind.startsWith("subagent")) continue;
85
113
  if (numMessages === 0) continue; // 开了没用的空会话
86
114
  // 只有系统提示与合成提醒(system_reminder)的会话读不出对话,别列进列表
@@ -92,7 +120,7 @@ export const grokSource: ImportSourceModule = {
92
120
  projectPath: typeof info?.cwd === "string" ? info.cwd : decodeProjectDir(path.basename(path.dirname(dir))),
93
121
  model: typeof summary.current_model_id === "string" ? summary.current_model_id : undefined,
94
122
  createdAt: toEpochMs(summary.created_at) ?? mtime,
95
- updatedAt: toEpochMs(summary.updated_at ?? summary.last_active_at) ?? mtime,
123
+ updatedAt,
96
124
  ...(Number.isFinite(numMessages) ? { messageCount: numMessages } : {}),
97
125
  location: history,
98
126
  });
@@ -111,17 +139,18 @@ export const grokSource: ImportSourceModule = {
111
139
  const skipped: string[] = [];
112
140
  let encryptedReasoning = 0;
113
141
  let synthetic = 0;
142
+ let runtimeContext = 0;
114
143
  let model = summary.model;
115
144
 
116
145
  for (const record of eachJsonLine(text)) {
117
146
  const timestamp = toEpochMs(record.timestamp ?? record.time ?? record.created_at) ?? summary.createdAt ?? Date.now();
118
147
  switch (record.type) {
119
148
  case "system":
120
- continue; // 系统提示不进对话
149
+ runtimeContext += 1;
150
+ continue; // 系统提示由 pi 按目标工作区重新生成,不冒充用户消息
121
151
  case "user": {
122
152
  const blocks = userBlocks(record.content);
123
- const plain = blocks.map((block) => (block.type === "text" ? block.text : "")).join("\n").trim();
124
- if (record.synthetic_reason || !plain) {
153
+ if (record.synthetic_reason || !blocks.length) {
125
154
  synthetic += 1;
126
155
  continue;
127
156
  }
@@ -145,8 +174,8 @@ export const grokSource: ImportSourceModule = {
145
174
  continue;
146
175
  }
147
176
  case "reasoning": {
148
- const text = typeof record.summary === "string" ? record.summary : "";
149
- if (text.trim()) entries.push({ type: "message", message: { role: "assistant", content: [{ type: "thinking", thinking: text }], timestamp, ...(model ? { model } : {}) } });
177
+ const text = reasoningText(record.summary);
178
+ if (text) entries.push({ type: "message", message: { role: "assistant", content: [{ type: "thinking", thinking: text }], timestamp, ...(model ? { model } : {}) } });
150
179
  else encryptedReasoning += 1; // 加密推理无正文可导:计数后汇总成一行,不要刷屏
151
180
  continue;
152
181
  }
@@ -163,6 +192,7 @@ export const grokSource: ImportSourceModule = {
163
192
 
164
193
  if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入`);
165
194
  if (encryptedReasoning) skipped.push(`跳过 ${encryptedReasoning} 条仅含加密内容的推理记录`);
195
+ if (runtimeContext) skipped.push(`跳过 ${runtimeContext} 条源工具运行时上下文(pi 会按当前工作区重新生成)`);
166
196
  if (!entries.some((entry) => entry.type === "message")) return null;
167
197
  return { summary: { ...summary, model }, entries, skipped };
168
198
  },
@@ -18,6 +18,7 @@ import { dshSource } from "./dsh";
18
18
  import { zcodeSource } from "./zcode";
19
19
  import { opencodeSource } from "./opencode";
20
20
  import { importedKey, listImportedKeys, findImported, writeImportedSession } from "./writer";
21
+ import { withImportLock } from "./lock";
21
22
 
22
23
  export const IMPORT_SOURCE_MODULES: Record<ImportSource, ImportSourceModule> = {
23
24
  claude: claudeSource,
@@ -146,6 +147,10 @@ async function oversize(location: string): Promise<boolean> {
146
147
  }
147
148
 
148
149
  export async function importSessions(items: ImportSelection[], options: ImportOptions): Promise<ImportReport> {
150
+ return withImportLock(options.agentDir, () => importSessionsLocked(items, options));
151
+ }
152
+
153
+ async function importSessionsLocked(items: ImportSelection[], options: ImportOptions): Promise<ImportReport> {
149
154
  const results: ImportOutcome[] = [];
150
155
  const workspaces = new Set<string>();
151
156
  const fallback = (await isDirectory(options.fallbackCwd)) ? path.resolve(options.fallbackCwd as string) : undefined;