@code-yeongyu/senpi 2026.7.11 → 2026.7.13

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 (267) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/core/agent-session.d.ts +9 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +56 -15
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts.map +1 -1
  7. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +76 -25
  8. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -1
  9. package/dist/core/resource-loader.d.ts +1 -0
  10. package/dist/core/resource-loader.d.ts.map +1 -1
  11. package/dist/core/resource-loader.js +20 -2
  12. package/dist/core/resource-loader.js.map +1 -1
  13. package/dist/modes/interactive/compaction-queue-transfer.d.ts +22 -0
  14. package/dist/modes/interactive/compaction-queue-transfer.d.ts.map +1 -0
  15. package/dist/modes/interactive/compaction-queue-transfer.js +82 -0
  16. package/dist/modes/interactive/compaction-queue-transfer.js.map +1 -0
  17. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  18. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  19. package/dist/modes/interactive/interactive-mode.js +83 -69
  20. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  21. package/node_modules/@babel/helper-string-parser/LICENSE +22 -0
  22. package/node_modules/@babel/helper-string-parser/README.md +19 -0
  23. package/node_modules/@babel/helper-string-parser/lib/index.js +295 -0
  24. package/node_modules/@babel/helper-string-parser/lib/index.js.map +1 -0
  25. package/node_modules/@babel/helper-string-parser/package.json +31 -0
  26. package/node_modules/@babel/helper-validator-identifier/LICENSE +22 -0
  27. package/node_modules/@babel/helper-validator-identifier/README.md +19 -0
  28. package/node_modules/@babel/helper-validator-identifier/lib/identifier.js +70 -0
  29. package/node_modules/@babel/helper-validator-identifier/lib/identifier.js.map +1 -0
  30. package/node_modules/@babel/helper-validator-identifier/lib/index.js +57 -0
  31. package/node_modules/@babel/helper-validator-identifier/lib/index.js.map +1 -0
  32. package/node_modules/@babel/helper-validator-identifier/lib/keyword.js +35 -0
  33. package/node_modules/@babel/helper-validator-identifier/lib/keyword.js.map +1 -0
  34. package/node_modules/@babel/helper-validator-identifier/package.json +31 -0
  35. package/node_modules/@babel/parser/LICENSE +19 -0
  36. package/node_modules/@babel/parser/README.md +19 -0
  37. package/node_modules/@babel/parser/bin/babel-parser.js +15 -0
  38. package/node_modules/@babel/parser/lib/index.js +14599 -0
  39. package/node_modules/@babel/parser/lib/index.js.map +1 -0
  40. package/node_modules/@babel/parser/package.json +50 -0
  41. package/node_modules/@babel/parser/typings/babel-parser.d.ts +262 -0
  42. package/node_modules/@babel/types/LICENSE +22 -0
  43. package/node_modules/@babel/types/README.md +19 -0
  44. package/node_modules/@babel/types/lib/asserts/assertNode.js +16 -0
  45. package/node_modules/@babel/types/lib/asserts/assertNode.js.map +1 -0
  46. package/node_modules/@babel/types/lib/asserts/generated/index.js +1251 -0
  47. package/node_modules/@babel/types/lib/asserts/generated/index.js.map +1 -0
  48. package/node_modules/@babel/types/lib/ast-types/generated/index.js +3 -0
  49. package/node_modules/@babel/types/lib/ast-types/generated/index.js.map +1 -0
  50. package/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js +18 -0
  51. package/node_modules/@babel/types/lib/builders/flow/createFlowUnionType.js.map +1 -0
  52. package/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js +31 -0
  53. package/node_modules/@babel/types/lib/builders/flow/createTypeAnnotationBasedOnTypeof.js.map +1 -0
  54. package/node_modules/@babel/types/lib/builders/generated/index.js +29 -0
  55. package/node_modules/@babel/types/lib/builders/generated/index.js.map +1 -0
  56. package/node_modules/@babel/types/lib/builders/generated/lowercase.js +2902 -0
  57. package/node_modules/@babel/types/lib/builders/generated/lowercase.js.map +1 -0
  58. package/node_modules/@babel/types/lib/builders/generated/uppercase.js +272 -0
  59. package/node_modules/@babel/types/lib/builders/generated/uppercase.js.map +1 -0
  60. package/node_modules/@babel/types/lib/builders/productions.js +12 -0
  61. package/node_modules/@babel/types/lib/builders/productions.js.map +1 -0
  62. package/node_modules/@babel/types/lib/builders/react/buildChildren.js +24 -0
  63. package/node_modules/@babel/types/lib/builders/react/buildChildren.js.map +1 -0
  64. package/node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js +22 -0
  65. package/node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js.map +1 -0
  66. package/node_modules/@babel/types/lib/builders/validateNode.js +21 -0
  67. package/node_modules/@babel/types/lib/builders/validateNode.js.map +1 -0
  68. package/node_modules/@babel/types/lib/clone/clone.js +12 -0
  69. package/node_modules/@babel/types/lib/clone/clone.js.map +1 -0
  70. package/node_modules/@babel/types/lib/clone/cloneDeep.js +12 -0
  71. package/node_modules/@babel/types/lib/clone/cloneDeep.js.map +1 -0
  72. package/node_modules/@babel/types/lib/clone/cloneDeepWithoutLoc.js +12 -0
  73. package/node_modules/@babel/types/lib/clone/cloneDeepWithoutLoc.js.map +1 -0
  74. package/node_modules/@babel/types/lib/clone/cloneNode.js +107 -0
  75. package/node_modules/@babel/types/lib/clone/cloneNode.js.map +1 -0
  76. package/node_modules/@babel/types/lib/clone/cloneWithoutLoc.js +12 -0
  77. package/node_modules/@babel/types/lib/clone/cloneWithoutLoc.js.map +1 -0
  78. package/node_modules/@babel/types/lib/comments/addComment.js +15 -0
  79. package/node_modules/@babel/types/lib/comments/addComment.js.map +1 -0
  80. package/node_modules/@babel/types/lib/comments/addComments.js +22 -0
  81. package/node_modules/@babel/types/lib/comments/addComments.js.map +1 -0
  82. package/node_modules/@babel/types/lib/comments/inheritInnerComments.js +12 -0
  83. package/node_modules/@babel/types/lib/comments/inheritInnerComments.js.map +1 -0
  84. package/node_modules/@babel/types/lib/comments/inheritLeadingComments.js +12 -0
  85. package/node_modules/@babel/types/lib/comments/inheritLeadingComments.js.map +1 -0
  86. package/node_modules/@babel/types/lib/comments/inheritTrailingComments.js +12 -0
  87. package/node_modules/@babel/types/lib/comments/inheritTrailingComments.js.map +1 -0
  88. package/node_modules/@babel/types/lib/comments/inheritsComments.js +17 -0
  89. package/node_modules/@babel/types/lib/comments/inheritsComments.js.map +1 -0
  90. package/node_modules/@babel/types/lib/comments/removeComments.js +15 -0
  91. package/node_modules/@babel/types/lib/comments/removeComments.js.map +1 -0
  92. package/node_modules/@babel/types/lib/constants/generated/index.js +60 -0
  93. package/node_modules/@babel/types/lib/constants/generated/index.js.map +1 -0
  94. package/node_modules/@babel/types/lib/constants/index.js +31 -0
  95. package/node_modules/@babel/types/lib/constants/index.js.map +1 -0
  96. package/node_modules/@babel/types/lib/converters/ensureBlock.js +14 -0
  97. package/node_modules/@babel/types/lib/converters/ensureBlock.js.map +1 -0
  98. package/node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js +65 -0
  99. package/node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js.map +1 -0
  100. package/node_modules/@babel/types/lib/converters/toBindingIdentifierName.js +14 -0
  101. package/node_modules/@babel/types/lib/converters/toBindingIdentifierName.js.map +1 -0
  102. package/node_modules/@babel/types/lib/converters/toBlock.js +29 -0
  103. package/node_modules/@babel/types/lib/converters/toBlock.js.map +1 -0
  104. package/node_modules/@babel/types/lib/converters/toComputedKey.js +14 -0
  105. package/node_modules/@babel/types/lib/converters/toComputedKey.js.map +1 -0
  106. package/node_modules/@babel/types/lib/converters/toExpression.js +28 -0
  107. package/node_modules/@babel/types/lib/converters/toExpression.js.map +1 -0
  108. package/node_modules/@babel/types/lib/converters/toIdentifier.js +25 -0
  109. package/node_modules/@babel/types/lib/converters/toIdentifier.js.map +1 -0
  110. package/node_modules/@babel/types/lib/converters/toKeyAlias.js +38 -0
  111. package/node_modules/@babel/types/lib/converters/toKeyAlias.js.map +1 -0
  112. package/node_modules/@babel/types/lib/converters/toSequenceExpression.js +19 -0
  113. package/node_modules/@babel/types/lib/converters/toSequenceExpression.js.map +1 -0
  114. package/node_modules/@babel/types/lib/converters/toStatement.js +39 -0
  115. package/node_modules/@babel/types/lib/converters/toStatement.js.map +1 -0
  116. package/node_modules/@babel/types/lib/converters/valueToNode.js +89 -0
  117. package/node_modules/@babel/types/lib/converters/valueToNode.js.map +1 -0
  118. package/node_modules/@babel/types/lib/definitions/core.js +1725 -0
  119. package/node_modules/@babel/types/lib/definitions/core.js.map +1 -0
  120. package/node_modules/@babel/types/lib/definitions/deprecated-aliases.js +11 -0
  121. package/node_modules/@babel/types/lib/definitions/deprecated-aliases.js.map +1 -0
  122. package/node_modules/@babel/types/lib/definitions/experimental.js +124 -0
  123. package/node_modules/@babel/types/lib/definitions/experimental.js.map +1 -0
  124. package/node_modules/@babel/types/lib/definitions/flow.js +495 -0
  125. package/node_modules/@babel/types/lib/definitions/flow.js.map +1 -0
  126. package/node_modules/@babel/types/lib/definitions/index.js +106 -0
  127. package/node_modules/@babel/types/lib/definitions/index.js.map +1 -0
  128. package/node_modules/@babel/types/lib/definitions/jsx.js +157 -0
  129. package/node_modules/@babel/types/lib/definitions/jsx.js.map +1 -0
  130. package/node_modules/@babel/types/lib/definitions/misc.js +31 -0
  131. package/node_modules/@babel/types/lib/definitions/misc.js.map +1 -0
  132. package/node_modules/@babel/types/lib/definitions/placeholders.js +27 -0
  133. package/node_modules/@babel/types/lib/definitions/placeholders.js.map +1 -0
  134. package/node_modules/@babel/types/lib/definitions/typescript.js +528 -0
  135. package/node_modules/@babel/types/lib/definitions/typescript.js.map +1 -0
  136. package/node_modules/@babel/types/lib/definitions/utils.js +296 -0
  137. package/node_modules/@babel/types/lib/definitions/utils.js.map +1 -0
  138. package/node_modules/@babel/types/lib/index-legacy.d.ts +2797 -0
  139. package/node_modules/@babel/types/lib/index.d.ts +3617 -0
  140. package/node_modules/@babel/types/lib/index.js +582 -0
  141. package/node_modules/@babel/types/lib/index.js.flow +2650 -0
  142. package/node_modules/@babel/types/lib/index.js.map +1 -0
  143. package/node_modules/@babel/types/lib/modifications/appendToMemberExpression.js +15 -0
  144. package/node_modules/@babel/types/lib/modifications/appendToMemberExpression.js.map +1 -0
  145. package/node_modules/@babel/types/lib/modifications/flow/removeTypeDuplicates.js +65 -0
  146. package/node_modules/@babel/types/lib/modifications/flow/removeTypeDuplicates.js.map +1 -0
  147. package/node_modules/@babel/types/lib/modifications/inherits.js +28 -0
  148. package/node_modules/@babel/types/lib/modifications/inherits.js.map +1 -0
  149. package/node_modules/@babel/types/lib/modifications/prependToMemberExpression.js +17 -0
  150. package/node_modules/@babel/types/lib/modifications/prependToMemberExpression.js.map +1 -0
  151. package/node_modules/@babel/types/lib/modifications/removeProperties.js +24 -0
  152. package/node_modules/@babel/types/lib/modifications/removeProperties.js.map +1 -0
  153. package/node_modules/@babel/types/lib/modifications/removePropertiesDeep.js +14 -0
  154. package/node_modules/@babel/types/lib/modifications/removePropertiesDeep.js.map +1 -0
  155. package/node_modules/@babel/types/lib/modifications/typescript/removeTypeDuplicates.js +66 -0
  156. package/node_modules/@babel/types/lib/modifications/typescript/removeTypeDuplicates.js.map +1 -0
  157. package/node_modules/@babel/types/lib/retrievers/getAssignmentIdentifiers.js +48 -0
  158. package/node_modules/@babel/types/lib/retrievers/getAssignmentIdentifiers.js.map +1 -0
  159. package/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js +102 -0
  160. package/node_modules/@babel/types/lib/retrievers/getBindingIdentifiers.js.map +1 -0
  161. package/node_modules/@babel/types/lib/retrievers/getFunctionName.js +63 -0
  162. package/node_modules/@babel/types/lib/retrievers/getFunctionName.js.map +1 -0
  163. package/node_modules/@babel/types/lib/retrievers/getOuterBindingIdentifiers.js +13 -0
  164. package/node_modules/@babel/types/lib/retrievers/getOuterBindingIdentifiers.js.map +1 -0
  165. package/node_modules/@babel/types/lib/traverse/traverse.js +50 -0
  166. package/node_modules/@babel/types/lib/traverse/traverse.js.map +1 -0
  167. package/node_modules/@babel/types/lib/traverse/traverseFast.js +40 -0
  168. package/node_modules/@babel/types/lib/traverse/traverseFast.js.map +1 -0
  169. package/node_modules/@babel/types/lib/utils/deprecationWarning.js +44 -0
  170. package/node_modules/@babel/types/lib/utils/deprecationWarning.js.map +1 -0
  171. package/node_modules/@babel/types/lib/utils/inherit.js +13 -0
  172. package/node_modules/@babel/types/lib/utils/inherit.js.map +1 -0
  173. package/node_modules/@babel/types/lib/utils/react/cleanJSXElementLiteralChild.js +40 -0
  174. package/node_modules/@babel/types/lib/utils/react/cleanJSXElementLiteralChild.js.map +1 -0
  175. package/node_modules/@babel/types/lib/utils/shallowEqual.js +17 -0
  176. package/node_modules/@babel/types/lib/utils/shallowEqual.js.map +1 -0
  177. package/node_modules/@babel/types/lib/validators/buildMatchMemberExpression.js +13 -0
  178. package/node_modules/@babel/types/lib/validators/buildMatchMemberExpression.js.map +1 -0
  179. package/node_modules/@babel/types/lib/validators/generated/index.js +2797 -0
  180. package/node_modules/@babel/types/lib/validators/generated/index.js.map +1 -0
  181. package/node_modules/@babel/types/lib/validators/is.js +27 -0
  182. package/node_modules/@babel/types/lib/validators/is.js.map +1 -0
  183. package/node_modules/@babel/types/lib/validators/isBinding.js +27 -0
  184. package/node_modules/@babel/types/lib/validators/isBinding.js.map +1 -0
  185. package/node_modules/@babel/types/lib/validators/isBlockScoped.js +13 -0
  186. package/node_modules/@babel/types/lib/validators/isBlockScoped.js.map +1 -0
  187. package/node_modules/@babel/types/lib/validators/isImmutable.js +21 -0
  188. package/node_modules/@babel/types/lib/validators/isImmutable.js.map +1 -0
  189. package/node_modules/@babel/types/lib/validators/isLet.js +13 -0
  190. package/node_modules/@babel/types/lib/validators/isLet.js.map +1 -0
  191. package/node_modules/@babel/types/lib/validators/isNode.js +12 -0
  192. package/node_modules/@babel/types/lib/validators/isNode.js.map +1 -0
  193. package/node_modules/@babel/types/lib/validators/isNodesEquivalent.js +57 -0
  194. package/node_modules/@babel/types/lib/validators/isNodesEquivalent.js.map +1 -0
  195. package/node_modules/@babel/types/lib/validators/isPlaceholderType.js +15 -0
  196. package/node_modules/@babel/types/lib/validators/isPlaceholderType.js.map +1 -0
  197. package/node_modules/@babel/types/lib/validators/isReferenced.js +96 -0
  198. package/node_modules/@babel/types/lib/validators/isReferenced.js.map +1 -0
  199. package/node_modules/@babel/types/lib/validators/isScope.js +18 -0
  200. package/node_modules/@babel/types/lib/validators/isScope.js.map +1 -0
  201. package/node_modules/@babel/types/lib/validators/isSpecifierDefault.js +14 -0
  202. package/node_modules/@babel/types/lib/validators/isSpecifierDefault.js.map +1 -0
  203. package/node_modules/@babel/types/lib/validators/isType.js +17 -0
  204. package/node_modules/@babel/types/lib/validators/isType.js.map +1 -0
  205. package/node_modules/@babel/types/lib/validators/isValidES3Identifier.js +13 -0
  206. package/node_modules/@babel/types/lib/validators/isValidES3Identifier.js.map +1 -0
  207. package/node_modules/@babel/types/lib/validators/isValidIdentifier.js +18 -0
  208. package/node_modules/@babel/types/lib/validators/isValidIdentifier.js.map +1 -0
  209. package/node_modules/@babel/types/lib/validators/isVar.js +15 -0
  210. package/node_modules/@babel/types/lib/validators/isVar.js.map +1 -0
  211. package/node_modules/@babel/types/lib/validators/matchesPattern.js +44 -0
  212. package/node_modules/@babel/types/lib/validators/matchesPattern.js.map +1 -0
  213. package/node_modules/@babel/types/lib/validators/react/isCompatTag.js +11 -0
  214. package/node_modules/@babel/types/lib/validators/react/isCompatTag.js.map +1 -0
  215. package/node_modules/@babel/types/lib/validators/react/isReactComponent.js +11 -0
  216. package/node_modules/@babel/types/lib/validators/react/isReactComponent.js.map +1 -0
  217. package/node_modules/@babel/types/lib/validators/validate.js +42 -0
  218. package/node_modules/@babel/types/lib/validators/validate.js.map +1 -0
  219. package/node_modules/@babel/types/package.json +39 -0
  220. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +10 -2
  221. package/node_modules/@code-yeongyu/senpi-codemode/README.md +128 -78
  222. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -3
  223. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +15 -2
  224. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/reserved.ts +9 -0
  225. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/agent-bridge.ts +197 -0
  226. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/output-bridge.ts +96 -0
  227. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/schema-injection.ts +3 -0
  228. package/node_modules/@code-yeongyu/senpi-codemode/src/completion/handler.ts +95 -26
  229. package/node_modules/@code-yeongyu/senpi-codemode/src/completion/tool-bridge.ts +11 -2
  230. package/node_modules/@code-yeongyu/senpi-codemode/src/config/settings.ts +96 -8
  231. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +24 -3
  232. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +155 -21
  233. package/node_modules/@code-yeongyu/senpi-codemode/src/interpreters/detect.ts +1 -1
  234. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/prelude.jl +252 -48
  235. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/jl/runner.jl +263 -83
  236. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +11 -5
  237. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/local-module-loader.ts +108 -0
  238. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/prelude.ts +8 -6
  239. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/rewrite-imports.ts +164 -0
  240. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +6 -1
  241. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-runtime.js +307 -41
  242. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +3 -2
  243. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +792 -79
  244. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/prelude.rb +234 -21
  245. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/rb/runner.rb +154 -51
  246. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +1 -0
  247. package/node_modules/@code-yeongyu/senpi-codemode/src/output/output-meta.ts +89 -0
  248. package/node_modules/@code-yeongyu/senpi-codemode/src/output/streaming-output.ts +296 -0
  249. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +107 -30
  250. package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/bridge-timeout.ts +2 -0
  251. package/node_modules/@code-yeongyu/senpi-codemode/src/timeouts/idle-timeout.ts +8 -19
  252. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-handler.ts +175 -113
  253. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +130 -120
  254. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/image.ts +274 -0
  255. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/json-tree.ts +247 -0
  256. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +641 -38
  257. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/status-events.ts +12 -0
  258. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +29 -1
  259. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  260. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +16 -16
  261. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  262. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +6 -6
  263. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  264. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  265. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  266. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  267. package/package.json +6 -6
@@ -1,33 +1,75 @@
1
1
  from __future__ import annotations
2
2
 
3
+ # noqa: SIZE_OK — this dependency-free subprocess prelude must ship as one file.
3
4
  import ast
4
- import asyncio
5
+ import asyncio # noqa: ANYIO_OK — stdlib-only embedded kernel runner.
5
6
  import base64
7
+ import codecs
6
8
  import contextlib
7
- import io
8
9
  import inspect
10
+ import io
9
11
  import json
12
+ import locale
10
13
  import os
14
+ import re
15
+ import subprocess
11
16
  import sys
12
17
  import time
13
18
  import traceback
14
19
  import urllib.error
15
20
  import urllib.request
16
21
  import uuid
17
- from concurrent.futures import ThreadPoolExecutor
22
+ from collections.abc import Iterable
23
+ from concurrent.futures import ThreadPoolExecutor, as_completed
18
24
  from pathlib import Path
19
- from typing import Any
25
+ from threading import Lock
26
+ from typing import Any, Callable
27
+ from urllib.parse import unquote
20
28
 
21
29
  SESSION_ID = ""
22
30
  CONNECTION: dict[str, Any] = {}
23
31
  USER_NS: dict[str, Any] = {"__name__": "__main__", "__doc__": None, "__builtins__": __builtins__}
24
32
  LOOP = asyncio.new_event_loop()
25
33
  asyncio.set_event_loop(LOOP)
34
+ EMIT_LOCK = Lock()
35
+
36
+ # Mirrors src/bridge/reserved.ts; this standalone subprocess asset cannot import TypeScript.
37
+ RESERVED_AGENT_TOOL = "__agent__"
38
+ RESERVED_OUTPUT_TOOL = "__output__"
39
+ TIMEOUT_PAUSE_OP = "timeout-pause"
40
+ TIMEOUT_RESUME_OP = "timeout-resume"
41
+
42
+
43
+ class PreludeRuntimeError(RuntimeError):
44
+ """Host bridge or magic execution failed."""
45
+
46
+
47
+ class PreludeValueError(ValueError):
48
+ """A helper received an invalid value."""
49
+
50
+
51
+ class PreludeTypeError(TypeError):
52
+ """A helper received an invalid value type."""
53
+
54
+ _INTERNAL_URL_RE = re.compile(r"^([a-z][a-z0-9+.-]*)://(.*)$", re.IGNORECASE)
55
+ _ASSIGN_LINE_RE = re.compile(
56
+ r"^(?P<indent>[ \t]*)(?P<lhs>[A-Za-z_][A-Za-z_0-9.\[\], ]*?)\s*=\s*(?P<rhs>.+)$"
57
+ )
58
+ _SHELL_READ_CHUNK_BYTES = 8192
59
+ _SHELL_CAPTURE_MAX_BYTES = 1024 * 1024
60
+ _SHELL_CAPTURE_MAX_LINES = 3000
61
+ _SHELL_TRUNCATION_NOTICE = (
62
+ f"[output truncated: shell helper exceeded {_SHELL_CAPTURE_MAX_BYTES} bytes "
63
+ f"or {_SHELL_CAPTURE_MAX_LINES} lines; remaining output discarded]\n"
64
+ )
65
+ os.environ.setdefault("MPLBACKEND", "Agg")
26
66
 
27
67
 
28
68
  def emit(frame: dict[str, Any]) -> None:
29
- sys.__stdout__.write(json.dumps(frame, ensure_ascii=False, default=repr) + "\n")
30
- sys.__stdout__.flush()
69
+ encoded = json.dumps(frame, ensure_ascii=False, default=repr) + "\n"
70
+ with EMIT_LOCK:
71
+ sys.__stdout__.write(encoded)
72
+ sys.__stdout__.flush()
31
73
 
32
74
 
33
75
  def bridge_error(exc: BaseException) -> dict[str, str]:
@@ -43,19 +85,126 @@ def text(stream: str, data: str) -> None:
43
85
  emit({"type": "text", "stream": stream, "data": data})
44
86
 
45
87
 
88
+ def b64_text(value: str) -> str:
89
+ return base64.b64encode(value.encode("utf-8")).decode("ascii")
90
+
91
+
92
+ def _emit_display(mime_type: str, data: Any) -> None:
93
+ if isinstance(data, (bytes, bytearray)):
94
+ encoded = base64.b64encode(bytes(data)).decode("ascii")
95
+ elif mime_type.startswith("image/"):
96
+ if isinstance(data, str):
97
+ encoded = data
98
+ else:
99
+ encoded = base64.b64encode(repr(data).encode("utf-8")).decode("ascii")
100
+ elif mime_type == "application/json":
101
+ encoded = b64_text(json.dumps(data, ensure_ascii=False, default=repr))
102
+ else:
103
+ encoded = b64_text(str(data))
104
+ emit({"type": "display", "mimeType": mime_type, "dataBase64": encoded})
105
+
106
+
107
+ def _display_bundle(bundle: dict[str, Any]) -> bool:
108
+ for mime_type in (
109
+ "image/png",
110
+ "image/jpeg",
111
+ "application/json",
112
+ "text/markdown",
113
+ "text/html",
114
+ "image/svg+xml",
115
+ "text/latex",
116
+ "text/plain",
117
+ ):
118
+ if mime_type in bundle:
119
+ _emit_display(mime_type, bundle[mime_type])
120
+ return True
121
+ return False
122
+
123
+
124
+ def _is_matplotlib_figure(value: Any) -> bool:
125
+ figure_module = sys.modules.get("matplotlib.figure")
126
+ figure_class = getattr(figure_module, "Figure", None)
127
+ if isinstance(figure_class, type) and isinstance(value, figure_class):
128
+ return True
129
+ value_type = type(value)
130
+ return value_type.__module__ == "matplotlib.figure" and value_type.__name__ == "Figure"
131
+
132
+
133
+ def _matplotlib_png(value: Any) -> bytes | None:
134
+ if not _is_matplotlib_figure(value):
135
+ return None
136
+ savefig = getattr(value, "savefig", None)
137
+ if not callable(savefig):
138
+ return None
139
+ try:
140
+ buffer = io.BytesIO()
141
+ savefig(buffer, format="png", bbox_inches="tight")
142
+ return buffer.getvalue()
143
+ except Exception: # noqa: BROAD_EXCEPT_OK — user-defined rendering hooks are isolated fallbacks.
144
+ return None
145
+
146
+
147
+ def _rich_bundle(value: Any) -> dict[str, Any]:
148
+ bundle: dict[str, Any] = {}
149
+ mime_bundle = getattr(value, "_repr_mimebundle_", None)
150
+ if callable(mime_bundle):
151
+ try:
152
+ data = mime_bundle()
153
+ if isinstance(data, tuple):
154
+ data = data[0]
155
+ if isinstance(data, dict):
156
+ bundle.update({str(key): item for key, item in data.items()})
157
+ except Exception: # noqa: BROAD_EXCEPT_OK — a broken repr must fall through to the next representation.
158
+ bundle.clear()
159
+
160
+ for attribute, mime_type in (
161
+ ("_repr_markdown_", "text/markdown"),
162
+ ("_repr_png_", "image/png"),
163
+ ("_repr_jpeg_", "image/jpeg"),
164
+ ("_repr_html_", "text/html"),
165
+ ("_repr_json_", "application/json"),
166
+ ("_repr_svg_", "image/svg+xml"),
167
+ ("_repr_latex_", "text/latex"),
168
+ ):
169
+ if mime_type in bundle:
170
+ continue
171
+ representation = getattr(value, attribute, None)
172
+ if not callable(representation):
173
+ continue
174
+ try:
175
+ data = representation()
176
+ except Exception: # noqa: BROAD_EXCEPT_OK — a broken repr must fall through to the next representation.
177
+ continue
178
+ if data is not None:
179
+ bundle[mime_type] = data
180
+
181
+ if "image/png" not in bundle:
182
+ figure_png = _matplotlib_png(value)
183
+ if figure_png is not None:
184
+ bundle["image/png"] = figure_png
185
+ return bundle
186
+
187
+
46
188
  def display(value: Any) -> None:
47
189
  if isinstance(value, (dict, list, tuple)):
48
- payload = json.dumps(value, ensure_ascii=False, default=repr)
49
- emit({"type": "display", "mimeType": "application/json", "dataBase64": b64(payload)})
190
+ _emit_display("application/json", value)
50
191
  return
51
192
  if isinstance(value, (bytes, bytearray)):
52
- emit({"type": "display", "mimeType": "application/octet-stream", "dataBase64": base64.b64encode(value).decode("ascii")})
193
+ _emit_display("application/octet-stream", bytes(value))
53
194
  return
54
- emit({"type": "display", "mimeType": "text/plain", "dataBase64": b64(str(value))})
195
+ bundle = _rich_bundle(value)
196
+ if bundle and _display_bundle(bundle):
197
+ return
198
+ _emit_display("text/plain", str(value))
55
199
 
56
200
 
57
- def b64(value: str) -> str:
58
- return base64.b64encode(value.encode("utf-8")).decode("ascii")
201
+ def _status_events_enabled() -> bool:
202
+ return CONNECTION.get("statusEvents", True) is not False
203
+
204
+
205
+ def emit_status(op: str, *, force: bool = False, **data: Any) -> None:
206
+ if force or _status_events_enabled():
207
+ emit({"type": "status", "event": {"op": op, **data}})
59
208
 
60
209
 
61
210
  def log(message: Any) -> None:
@@ -68,106 +217,657 @@ def phase(title: Any) -> None:
68
217
 
69
218
  def env(key: str | None = None, value: str | None = None) -> Any:
70
219
  if key is None:
71
- return dict(sorted(os.environ.items()))
220
+ items = dict(sorted(os.environ.items()))
221
+ emit_status("env", count=len(items), keys=list(items.keys())[:20])
222
+ return items
72
223
  if value is not None:
73
224
  os.environ[key] = value
225
+ emit_status("env", key=key, value=value, action="set")
74
226
  return value
75
- return os.environ.get(key)
227
+ resolved = os.environ.get(key)
228
+ emit_status("env", key=key, value=resolved, action="get")
229
+ return resolved
230
+
231
+
232
+ def _resolve_helper_path(path: str | Path) -> Path:
233
+ if not isinstance(path, str):
234
+ return Path(path)
235
+ match = _INTERNAL_URL_RE.match(path)
236
+ if not match:
237
+ return Path(path)
238
+ scheme = match.group(1).lower()
239
+ roots = CONNECTION.get("localRoots")
240
+ root = roots.get(scheme) if isinstance(roots, dict) else None
241
+ if not isinstance(root, str) or not root:
242
+ raise PreludeValueError(f"Protocol paths are not supported by this helper: {path}")
243
+ relative = unquote(match.group(2).replace("\\", "/"))
244
+ root_path = os.path.abspath(root)
245
+ if relative == "":
246
+ return Path(root_path)
247
+ relative_path = Path(relative)
248
+ if relative_path.is_absolute() or ".." in relative_path.parts:
249
+ raise PreludeValueError(f"Unsafe {scheme}:// path (absolute or traversal): {path}")
250
+ resolved = os.path.abspath(os.path.join(root_path, relative))
251
+ if resolved != root_path and not resolved.startswith(root_path + os.sep):
252
+ raise PreludeValueError(f"{scheme}:// path escapes its root: {path}")
253
+ return Path(resolved)
76
254
 
77
255
 
78
256
  def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:
79
- data = Path(path).read_text(encoding="utf-8")
257
+ target = _resolve_helper_path(path)
258
+ data = target.read_text(encoding="utf-8")
80
259
  if offset > 1 or limit is not None:
81
260
  lines = data.splitlines(keepends=True)
82
261
  start = max(0, offset - 1)
83
262
  end = start + limit if limit is not None else len(lines)
84
- return "".join(lines[start:end])
263
+ data = "".join(lines[start:end])
264
+ emit_status("read", path=str(target), chars=len(data), preview=data[:500])
85
265
  return data
86
266
 
87
267
 
88
- def write(path: str | Path, content: str) -> str:
89
- target = Path(path)
268
+ def write(path: str | Path, content: str) -> Path:
269
+ target = _resolve_helper_path(path)
90
270
  target.parent.mkdir(parents=True, exist_ok=True)
91
271
  target.write_text(content, encoding="utf-8")
92
- return str(target)
93
-
94
-
95
- def parallel(callables: list[Any], width: int = 4) -> list[Any]:
96
- with ThreadPoolExecutor(max_workers=max(1, width)) as pool:
97
- return list(pool.map(lambda fn: fn(), callables))
98
-
99
-
100
- def pipeline(items: list[Any], *stages: Any) -> list[Any]:
101
- values = items
102
- for stage in stages:
103
- values = parallel([lambda item=item: stage(item) for item in values])
104
- return values
105
-
106
-
107
- class ToolProxy:
108
- def __getattr__(self, name: str) -> Any:
109
- def call(args: Any = None) -> Any:
110
- return bridge_post("/call", {"callId": f"py-{uuid.uuid4()}", "toolName": name, "args": {} if args is None else args})
111
- return call
112
-
113
-
114
- def completion(prompt: str, **options: Any) -> Any:
115
- return bridge_post("/completion", {"prompt": prompt, "opts": options})
272
+ emit_status("write", path=str(target), chars=len(content))
273
+ return target
116
274
 
117
275
 
118
276
  def bridge_post(path: str, payload: dict[str, Any]) -> Any:
119
277
  port = CONNECTION.get("port")
120
278
  token = CONNECTION.get("token")
121
279
  if not isinstance(port, int) or not isinstance(token, str):
122
- raise RuntimeError("Python tool bridge is not initialized")
123
- data = json.dumps(payload, ensure_ascii=False, default=repr).encode("utf-8")
124
- req = urllib.request.Request(
280
+ raise PreludeRuntimeError("Python tool bridge is not initialized")
281
+ request_data = json.dumps(payload, ensure_ascii=False, default=repr).encode("utf-8")
282
+ request = urllib.request.Request(
125
283
  f"http://127.0.0.1:{port}{path}",
126
- data=data,
284
+ data=request_data,
127
285
  headers={"authorization": f"Bearer {token}", "content-type": "application/json"},
128
286
  method="POST",
129
287
  )
288
+ emit_status(TIMEOUT_PAUSE_OP, force=True)
130
289
  try:
131
- with urllib.request.urlopen(req, timeout=60) as response:
132
- body = json.loads(response.read().decode("utf-8"))
133
- except urllib.error.HTTPError as exc:
134
- body = json.loads(exc.read().decode("utf-8"))
290
+ try:
291
+ with urllib.request.urlopen(request, timeout=60) as response:
292
+ response_data = response.read()
293
+ except urllib.error.HTTPError as exc:
294
+ response_data = exc.read()
295
+ finally:
296
+ emit_status(TIMEOUT_RESUME_OP, force=True)
297
+
298
+ try:
299
+ body = json.loads(response_data.decode("utf-8"))
300
+ except json.JSONDecodeError as exc:
301
+ raise PreludeRuntimeError(f"Bridge returned invalid JSON: {response_data[:200]!r}") from exc
135
302
  if isinstance(body, dict) and body.get("ok") is True:
136
303
  return body.get("value")
137
304
  error = body.get("error") if isinstance(body, dict) else body
138
305
  if isinstance(error, dict):
139
- raise RuntimeError(str(error.get("message", error)))
140
- raise RuntimeError(str(error))
306
+ raise PreludeRuntimeError(str(error.get("message", error)))
307
+ raise PreludeRuntimeError(str(error))
308
+
309
+
310
+ class ToolCallable:
311
+ __slots__ = ("_name",)
312
+
313
+ def __init__(self, name: str) -> None:
314
+ self._name = name
315
+
316
+ def __repr__(self) -> str:
317
+ return f"<tool.{self._name}>"
318
+
319
+ def __call__(self, args: Any = None, /, **kwargs: Any) -> Any:
320
+ if args is None:
321
+ merged: dict[str, Any] = {}
322
+ elif isinstance(args, dict):
323
+ merged = dict(args)
324
+ else:
325
+ raise PreludeTypeError(
326
+ f"tool.{self._name}(...) expects a dict of arguments (got {type(args).__name__})"
327
+ )
328
+ merged.update(kwargs)
329
+ merged.setdefault("i", "py prelude")
330
+ return bridge_post(
331
+ "/call",
332
+ {"callId": f"py-{uuid.uuid4()}", "toolName": self._name, "args": merged},
333
+ )
334
+
335
+
336
+ class ToolProxy:
337
+ __slots__ = ()
338
+
339
+ def __getattr__(self, name: str) -> ToolCallable:
340
+ if name.startswith("_"):
341
+ raise AttributeError(name)
342
+ return ToolCallable(name)
343
+
344
+ def __getitem__(self, name: str) -> ToolCallable:
345
+ return ToolCallable(name)
346
+
347
+ def __repr__(self) -> str:
348
+ return "<tool proxy>"
141
349
 
142
350
 
143
351
  tool = ToolProxy()
144
- USER_NS.update({
145
- "display": display,
146
- "print": print,
147
- "log": log,
148
- "phase": phase,
149
- "env": env,
150
- "read": read,
151
- "write": write,
152
- "parallel": parallel,
153
- "pipeline": pipeline,
154
- "tool": tool,
155
- "completion": completion,
156
- })
352
+
353
+
354
+ def completion(
355
+ prompt: str,
356
+ model: str = "default",
357
+ system: str | None = None,
358
+ schema: dict[str, Any] | None = None,
359
+ **kwargs: Any,
360
+ ) -> Any:
361
+ options: dict[str, Any] = {}
362
+ if model != "default":
363
+ options["model"] = model
364
+ options.update(kwargs)
365
+ if system is not None:
366
+ options["system"] = system
367
+ if schema is not None:
368
+ options["schema"] = schema
369
+ response = bridge_post("/completion", {"prompt": prompt, "opts": options})
370
+ if not isinstance(response, dict):
371
+ return response
372
+ if "value" in response:
373
+ return response["value"]
374
+ return response.get("text", response)
375
+
376
+
377
+ def output(
378
+ *ids: str,
379
+ format: str = "raw",
380
+ offset: int | None = None,
381
+ limit: int | None = None,
382
+ ) -> Any:
383
+ if not ids:
384
+ raise PreludeValueError("At least one output ID is required")
385
+ if format not in ("raw", "tail"):
386
+ raise PreludeValueError("output() format must be 'raw' or 'tail'")
387
+ args: dict[str, Any] = {"ids": list(ids), "format": format}
388
+ if offset is not None:
389
+ args["offset"] = offset
390
+ if limit is not None:
391
+ args["limit"] = limit
392
+ return bridge_post(
393
+ "/call",
394
+ {"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_OUTPUT_TOOL, "args": args},
395
+ )
396
+
397
+
398
+ def agent(
399
+ prompt: str,
400
+ *,
401
+ agent: str | None = "task",
402
+ model: str | None = None,
403
+ label: str | None = None,
404
+ schema: dict[str, Any] | None = None,
405
+ isolated: bool | None = None,
406
+ apply: bool | None = None,
407
+ merge: bool | None = None,
408
+ handle: bool = False,
409
+ ) -> Any:
410
+ args: dict[str, Any] = {"prompt": prompt}
411
+ if agent is not None:
412
+ args["agent"] = agent
413
+ if model is not None:
414
+ args["model"] = model
415
+ if label is not None:
416
+ args["label"] = label
417
+ if schema is not None:
418
+ args["schema"] = schema
419
+ if isolated is not None:
420
+ args["isolated"] = bool(isolated)
421
+ if apply is not None:
422
+ args["apply"] = bool(apply)
423
+ if merge is not None:
424
+ args["merge"] = bool(merge)
425
+ if handle:
426
+ args["handle"] = True
427
+
428
+ response = bridge_post(
429
+ "/call",
430
+ {"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_AGENT_TOOL, "args": args},
431
+ )
432
+ response_record = response if isinstance(response, dict) else {}
433
+ text_value = response_record.get("text", response)
434
+ parsed = response_record.get("data")
435
+ if schema is not None and "data" not in response_record:
436
+ parsed = json.loads(str(text_value))
437
+ elif schema is None:
438
+ parsed = text_value
439
+ if not handle:
440
+ return parsed
441
+
442
+ agent_id = response_record.get("id")
443
+ handle_value = response_record.get("handle")
444
+ if handle_value is None and agent_id is not None:
445
+ handle_value = f"agent://{agent_id}"
446
+ node: dict[str, Any] = {
447
+ "text": text_value,
448
+ "output": text_value,
449
+ "handle": handle_value,
450
+ "id": agent_id,
451
+ "agent": response_record.get("agent", agent),
452
+ }
453
+ if schema is not None:
454
+ node["data"] = parsed
455
+ for key in (
456
+ "isolated",
457
+ "patch_path",
458
+ "branch_name",
459
+ "nested_patches",
460
+ "changes_applied",
461
+ "isolation_summary",
462
+ ):
463
+ if key in response_record:
464
+ node[key] = response_record[key]
465
+ return node
466
+
467
+
468
+ def _pool_map(items: Iterable[Any], function: Callable[[Any], Any]) -> list[Any]:
469
+ values = list(items)
470
+ if not values:
471
+ return []
472
+ configured_width = CONNECTION.get("parallelPoolWidth", 4)
473
+ width = (
474
+ int(configured_width)
475
+ if isinstance(configured_width, (int, float)) and not isinstance(configured_width, bool)
476
+ else 4
477
+ )
478
+ workers = min(max(1, width), len(values))
479
+ results: list[Any] = [None] * len(values)
480
+ errors: dict[int, BaseException] = {}
481
+ with ThreadPoolExecutor(max_workers=workers) as pool:
482
+ futures = {pool.submit(function, value): index for index, value in enumerate(values)}
483
+ for future in as_completed(futures):
484
+ index = futures[future]
485
+ try:
486
+ results[index] = future.result()
487
+ except BaseException as exc: # noqa: BROAD_EXCEPT_OK — preserve user thunk failures for deterministic re-raise.
488
+ errors[index] = exc
489
+ if errors:
490
+ raise errors[min(errors)]
491
+ return results
492
+
493
+
494
+ def parallel(callables: Iterable[Callable[[], Any]]) -> list[Any]:
495
+ thunks = list(callables)
496
+ for thunk in thunks:
497
+ if not callable(thunk):
498
+ raise PreludeTypeError("parallel() expects an iterable of zero-arg callables")
499
+ return _pool_map(thunks, lambda thunk: thunk())
500
+
501
+
502
+ def pipeline(items: Iterable[Any], *stages: Callable[[Any], Any]) -> list[Any]:
503
+ values = list(items)
504
+ for stage in stages:
505
+ if not callable(stage):
506
+ raise PreludeTypeError("pipeline() stages must be callables")
507
+ values = _pool_map(values, stage)
508
+ return values
509
+
510
+
511
+ def _fold_continuations(lines: list[str], start: int) -> tuple[str, int]:
512
+ parts: list[str] = []
513
+ index = start
514
+ while index < len(lines):
515
+ line = lines[index]
516
+ if line.endswith("\\"):
517
+ parts.append(line[:-1])
518
+ index += 1
519
+ continue
520
+ parts.append(line)
521
+ index += 1
522
+ break
523
+ return "".join(parts), index - start
524
+
525
+
526
+ def _quote_arg(text_value: str) -> str:
527
+ return json.dumps(text_value, ensure_ascii=False)
528
+
529
+
530
+ def _split_magic_head(text_value: str) -> tuple[str, str]:
531
+ stripped = text_value.lstrip()
532
+ if not stripped:
533
+ return "", ""
534
+ match = re.match(r"([A-Za-z_][A-Za-z_0-9]*)(?:\s+(.*))?$", stripped)
535
+ if not match:
536
+ return "", stripped
537
+ return match.group(1), (match.group(2) or "").rstrip()
538
+
539
+
540
+ def _is_escaped(text_value: str, index: int) -> bool:
541
+ backslashes = 0
542
+ cursor = index - 1
543
+ while cursor >= 0 and text_value[cursor] == "\\":
544
+ backslashes += 1
545
+ cursor -= 1
546
+ return backslashes % 2 == 1
547
+
548
+
549
+ def _advance_triple_quote_state(line: str, active_quote: str | None) -> str | None:
550
+ index = 0
551
+ quote = active_quote
552
+ while index < len(line):
553
+ if quote is not None:
554
+ closing = line.find(quote, index)
555
+ if closing < 0:
556
+ return quote
557
+ if _is_escaped(line, closing):
558
+ index = closing + 1
559
+ continue
560
+ quote = None
561
+ index = closing + 3
562
+ continue
563
+
564
+ character = line[index]
565
+ if character == "#":
566
+ return None
567
+ if character not in ("'", '"'):
568
+ index += 1
569
+ continue
570
+ triple = character * 3
571
+ if line.startswith(triple, index):
572
+ quote = triple
573
+ index += 3
574
+ continue
575
+ index += 1
576
+ while index < len(line):
577
+ if line[index] == character and not _is_escaped(line, index):
578
+ index += 1
579
+ break
580
+ index += 1
581
+ return quote
582
+
583
+
584
+ def transform_cell(source: str) -> str:
585
+ if "%" not in source and "!" not in source:
586
+ return source
587
+
588
+ lines = source.splitlines()
589
+ transformed: list[str] = []
590
+ index = 0
591
+ triple_quote: str | None = None
592
+ while index < len(lines):
593
+ line = lines[index]
594
+ protected = triple_quote is not None
595
+ stripped = line.lstrip()
596
+ indent = line[: len(line) - len(stripped)]
597
+
598
+ if not protected and stripped.startswith("%%"):
599
+ name, args = _split_magic_head(stripped[2:])
600
+ body = "\n".join(lines[index + 1 :])
601
+ transformed.append(
602
+ f"{indent}__senpi_magic_cell({_quote_arg(name)}, {_quote_arg(args)}, {_quote_arg(body)})"
603
+ )
604
+ return "\n".join(transformed)
605
+
606
+ if not protected and stripped.startswith("%"):
607
+ folded, consumed = _fold_continuations(lines, index)
608
+ folded_stripped = folded.lstrip()
609
+ folded_indent = folded[: len(folded) - len(folded_stripped)]
610
+ name, args = _split_magic_head(folded_stripped[1:])
611
+ transformed.append(f"{folded_indent}__senpi_magic({_quote_arg(name)}, {_quote_arg(args)})")
612
+ index += consumed
613
+ continue
614
+
615
+ if not protected and stripped.startswith("!"):
616
+ folded, consumed = _fold_continuations(lines, index)
617
+ folded_stripped = folded.lstrip()
618
+ folded_indent = folded[: len(folded) - len(folded_stripped)]
619
+ command = folded_stripped[1:].strip()
620
+ transformed.append(f"{folded_indent}__senpi_shell({_quote_arg(command)})")
621
+ index += consumed
622
+ continue
623
+
624
+ if not protected:
625
+ assignment = _ASSIGN_LINE_RE.match(line)
626
+ if assignment:
627
+ right_hand_side = assignment.group("rhs").strip()
628
+ if right_hand_side.startswith("!"):
629
+ command = right_hand_side[1:].strip()
630
+ transformed.append(
631
+ f"{assignment.group('indent')}{assignment.group('lhs').rstrip()} = "
632
+ f"__senpi_shell({_quote_arg(command)})"
633
+ )
634
+ index += 1
635
+ continue
636
+ if right_hand_side.startswith("%") and not right_hand_side.startswith("%%"):
637
+ name, args = _split_magic_head(right_hand_side[1:])
638
+ transformed.append(
639
+ f"{assignment.group('indent')}{assignment.group('lhs').rstrip()} = "
640
+ f"__senpi_magic({_quote_arg(name)}, {_quote_arg(args)})"
641
+ )
642
+ index += 1
643
+ continue
644
+
645
+ transformed.append(line)
646
+ triple_quote = _advance_triple_quote_state(line, triple_quote)
647
+ index += 1
648
+ return "\n".join(transformed)
649
+
650
+
651
+ def _magic_cd(args: str) -> str:
652
+ path = os.path.expanduser(args.strip()) or os.path.expanduser("~")
653
+ os.chdir(path)
654
+ cwd = os.getcwd()
655
+ emit_status("cd", path=cwd)
656
+ return cwd
657
+
658
+
659
+ def _magic_env(args: str) -> Any:
660
+ stripped = args.strip()
661
+ if not stripped:
662
+ return env()
663
+ if "=" in stripped:
664
+ key, value = stripped.split("=", 1)
665
+ return env(key.strip(), value.strip())
666
+ return env(stripped)
667
+
668
+
669
+ _LINE_MAGICS: dict[str, Callable[[str], Any]] = {
670
+ "cd": _magic_cd,
671
+ "env": _magic_env,
672
+ }
673
+
674
+
675
+ def _magic(name: str, args: str) -> Any:
676
+ handler = _LINE_MAGICS.get(name)
677
+ if handler is None:
678
+ raise PreludeRuntimeError(f"Unsupported line magic: %{name}")
679
+ return handler(args)
680
+
681
+
682
+ def _magic_cell(name: str, args: str, body: str) -> Any:
683
+ if name in ("bash", "sh"):
684
+ command = "\n".join(part for part in (args, body) if part)
685
+ return _shell(command)
686
+ raise PreludeRuntimeError(f"Unsupported cell magic: %%{name}")
687
+
688
+
689
+ def _take_prefix_by_lines(value: str, max_lines: int) -> str:
690
+ if max_lines <= 0:
691
+ return ""
692
+ cursor = 0
693
+ for _ in range(max_lines):
694
+ newline = value.find("\n", cursor)
695
+ if newline < 0:
696
+ return value
697
+ cursor = newline + 1
698
+ return value[:cursor]
699
+
700
+
701
+ def _take_prefix_by_encoded_bytes(value: str, max_bytes: int, encoding: str) -> str:
702
+ if max_bytes <= 0:
703
+ return ""
704
+ if len(value.encode(encoding, errors="replace")) <= max_bytes:
705
+ return value
706
+ low = 0
707
+ high = len(value)
708
+ while low < high:
709
+ middle = (low + high + 1) // 2
710
+ if len(value[:middle].encode(encoding, errors="replace")) <= max_bytes:
711
+ low = middle
712
+ else:
713
+ high = middle - 1
714
+ return value[:low]
715
+
716
+
717
+ class _ShellOutputLimiter:
718
+ def __init__(self, *, max_bytes: int, max_lines: int, encoding: str) -> None:
719
+ self._remaining_bytes = max_bytes
720
+ self._remaining_lines = max_lines
721
+ self._encoding = encoding
722
+ self._truncated = False
723
+ self._at_line_start = True
724
+
725
+ def write(self, value: str) -> None:
726
+ if not value or self._truncated:
727
+ return
728
+ line_limited = _take_prefix_by_lines(value, self._remaining_lines)
729
+ truncated = line_limited != value
730
+ byte_limited = _take_prefix_by_encoded_bytes(
731
+ line_limited,
732
+ self._remaining_bytes,
733
+ self._encoding,
734
+ )
735
+ truncated = truncated or byte_limited != line_limited
736
+ if byte_limited:
737
+ text("stdout", byte_limited)
738
+ self._remaining_bytes -= len(
739
+ byte_limited.encode(self._encoding, errors="replace")
740
+ )
741
+ self._remaining_lines -= byte_limited.count("\n")
742
+ self._at_line_start = byte_limited.endswith("\n")
743
+ if truncated:
744
+ self._emit_truncation_notice()
745
+
746
+ def _emit_truncation_notice(self) -> None:
747
+ if self._truncated:
748
+ return
749
+ prefix = "" if self._at_line_start else "\n"
750
+ text("stdout", prefix + _SHELL_TRUNCATION_NOTICE)
751
+ self._truncated = True
752
+
753
+
754
+ class _BoundedTextCapture:
755
+ def __init__(self, max_bytes: int, max_lines: int, encoding: str) -> None:
756
+ self._remaining_bytes = max_bytes
757
+ self._remaining_lines = max_lines
758
+ self._encoding = encoding
759
+ self._parts: list[str] = []
760
+
761
+ def add(self, value: str) -> None:
762
+ if self._remaining_bytes <= 0 or self._remaining_lines <= 0:
763
+ return
764
+ line_limited = _take_prefix_by_lines(value, self._remaining_lines)
765
+ part = _take_prefix_by_encoded_bytes(
766
+ line_limited,
767
+ self._remaining_bytes,
768
+ self._encoding,
769
+ )
770
+ if not part:
771
+ return
772
+ self._parts.append(part)
773
+ self._remaining_bytes -= len(part.encode(self._encoding, errors="replace"))
774
+ self._remaining_lines -= part.count("\n")
775
+
776
+ def value(self) -> str:
777
+ return "".join(self._parts)
778
+
779
+
780
+ class ShellResult(list[str]):
781
+ def __init__(self, lines: list[str], returncode: int) -> None:
782
+ super().__init__(lines)
783
+ self.returncode = returncode
784
+
785
+ @property
786
+ def n(self) -> str:
787
+ return "\n".join(self)
788
+
789
+ @property
790
+ def s(self) -> str:
791
+ return " ".join(self)
792
+
793
+
794
+ def _shell(command: str) -> ShellResult:
795
+ process = subprocess.Popen(
796
+ command,
797
+ shell=True,
798
+ stdout=subprocess.PIPE,
799
+ stderr=subprocess.STDOUT,
800
+ )
801
+ if process.stdout is None:
802
+ return ShellResult([], process.wait())
803
+
804
+ encoding = locale.getpreferredencoding(False) or "utf-8"
805
+ decoder = codecs.getincrementaldecoder(encoding)(errors="replace")
806
+ limiter = _ShellOutputLimiter(
807
+ max_bytes=_SHELL_CAPTURE_MAX_BYTES,
808
+ max_lines=_SHELL_CAPTURE_MAX_LINES,
809
+ encoding=encoding,
810
+ )
811
+ capture = _BoundedTextCapture(
812
+ _SHELL_CAPTURE_MAX_BYTES,
813
+ _SHELL_CAPTURE_MAX_LINES,
814
+ encoding,
815
+ )
816
+
817
+ def consume(chunk_text: str) -> None:
818
+ if not chunk_text:
819
+ return
820
+ limiter.write(chunk_text)
821
+ capture.add(chunk_text)
822
+
823
+ while True:
824
+ raw_chunk = os.read(process.stdout.fileno(), _SHELL_READ_CHUNK_BYTES)
825
+ if not raw_chunk:
826
+ break
827
+ consume(decoder.decode(raw_chunk))
828
+ consume(decoder.decode(b"", final=True))
829
+ return ShellResult(capture.value().splitlines(), process.wait())
830
+
831
+
832
+ USER_NS.update(
833
+ {
834
+ "display": display,
835
+ "print": print,
836
+ "log": log,
837
+ "phase": phase,
838
+ "env": env,
839
+ "read": read,
840
+ "write": write,
841
+ "parallel": parallel,
842
+ "pipeline": pipeline,
843
+ "tool": tool,
844
+ "completion": completion,
845
+ "agent": agent,
846
+ "output": output,
847
+ "__senpi_magic": _magic,
848
+ "__senpi_magic_cell": _magic_cell,
849
+ "__senpi_shell": _shell,
850
+ }
851
+ )
157
852
 
158
853
  TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
159
854
 
160
855
 
161
856
  def compile_cell(source: str) -> tuple[Any | None, Any | None]:
162
- module = ast.parse(source, mode="exec")
857
+ module = ast.parse(transform_cell(source), mode="exec")
163
858
  if not module.body:
164
859
  return None, None
165
860
  last = module.body[-1]
166
861
  if isinstance(last, ast.Expr):
167
862
  body = ast.Module(body=module.body[:-1], type_ignores=[])
168
- expr = ast.Expression(body=last.value)
169
- ast.copy_location(expr, last)
170
- return compile(body, "<cell>", "exec", flags=TLA_FLAG), compile(expr, "<cell>", "eval", flags=TLA_FLAG)
863
+ expression = ast.Expression(body=last.value)
864
+ ast.copy_location(expression, last)
865
+ return compile(body, "<cell>", "exec", flags=TLA_FLAG), compile(
866
+ expression,
867
+ "<cell>",
868
+ "eval",
869
+ flags=TLA_FLAG,
870
+ )
171
871
  return compile(module, "<cell>", "exec", flags=TLA_FLAG), None
172
872
 
173
873
 
@@ -189,19 +889,32 @@ def run_cell(cell_id: str, code: str) -> None:
189
889
  stderr = io.StringIO()
190
890
  try:
191
891
  with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
192
- body, expr = compile_cell(code)
892
+ body, expression = compile_cell(code)
193
893
  LOOP.run_until_complete(run_code(body, False))
194
- value = LOOP.run_until_complete(run_code(expr, True))
894
+ value = LOOP.run_until_complete(run_code(expression, True))
195
895
  text("stdout", stdout.getvalue())
196
896
  text("stderr", stderr.getvalue())
197
- result: dict[str, Any] = {"type": "result", "cellId": cell_id, "ok": True, "durationMs": elapsed(start)}
897
+ result: dict[str, Any] = {
898
+ "type": "result",
899
+ "cellId": cell_id,
900
+ "ok": True,
901
+ "durationMs": elapsed(start),
902
+ }
198
903
  if value is not None:
199
904
  result["valueRepr"] = repr(value)
200
905
  emit(result)
201
- except BaseException as exc:
906
+ except BaseException as exc: # noqa: BROAD_EXCEPT_OK — cell boundary serializes user errors and interrupts.
202
907
  text("stdout", stdout.getvalue())
203
908
  text("stderr", stderr.getvalue())
204
- emit({"type": "result", "cellId": cell_id, "ok": False, "error": bridge_error(exc), "durationMs": elapsed(start)})
909
+ emit(
910
+ {
911
+ "type": "result",
912
+ "cellId": cell_id,
913
+ "ok": False,
914
+ "error": bridge_error(exc),
915
+ "durationMs": elapsed(start),
916
+ }
917
+ )
205
918
 
206
919
 
207
920
  def elapsed(start: float) -> int:
@@ -210,8 +923,8 @@ def elapsed(start: float) -> int:
210
923
 
211
924
  def handle(message: dict[str, Any]) -> bool:
212
925
  global SESSION_ID, CONNECTION
213
- typ = message.get("type")
214
- if typ == "init":
926
+ message_type = message.get("type")
927
+ if message_type == "init":
215
928
  SESSION_ID = str(message.get("sessionId", ""))
216
929
  connection = message.get("connection")
217
930
  if not isinstance(connection, dict):
@@ -220,10 +933,10 @@ def handle(message: dict[str, Any]) -> bool:
220
933
  CONNECTION = connection
221
934
  emit({"type": "ready"})
222
935
  return True
223
- if typ == "run":
936
+ if message_type == "run":
224
937
  run_cell(str(message.get("cellId", "")), str(message.get("code", "")))
225
938
  return True
226
- if typ == "close":
939
+ if message_type == "close":
227
940
  emit({"type": "closed"})
228
941
  return False
229
942
  return True
@@ -234,7 +947,7 @@ def main() -> None:
234
947
  try:
235
948
  if not handle(json.loads(raw)):
236
949
  break
237
- except BaseException as exc:
950
+ except BaseException as exc: # noqa: BROAD_EXCEPT_OK — process boundary serializes malformed input and interrupts.
238
951
  emit({"type": "init-failed", "error": bridge_error(exc)})
239
952
 
240
953