@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,50 +1,170 @@
1
1
  require "base64"
2
+ require "fileutils"
2
3
  require "json"
4
+ require "uri"
5
+
6
+ SENPI_RESERVED_AGENT_TOOL = "__agent__"
7
+ SENPI_RESERVED_OUTPUT_TOOL = "__output__"
8
+ SENPI_INTERNAL_URL = Regexp.new("\\A([a-z][a-z0-9+.\\-]*)://(.*)\\z", Regexp::IGNORECASE)
9
+
10
+ def __senpi_status_enabled?
11
+ connection = $__senpi_connection
12
+ return true unless connection.is_a?(Hash)
13
+ connection["statusEvents"] != false
14
+ end
15
+
16
+ def __senpi_emit_status(op, fields = {}, force: false)
17
+ return unless force || __senpi_status_enabled?
18
+ __senpi_emit({ "type" => "status", "event" => { "op" => op }.merge(fields.transform_keys(&:to_s)) })
19
+ end
20
+
21
+ def __senpi_resolve_path(value)
22
+ raw = value.to_s
23
+ match = SENPI_INTERNAL_URL.match(raw)
24
+ return File.expand_path(raw) unless match
25
+
26
+ scheme = match[1].downcase
27
+ roots = $__senpi_connection.is_a?(Hash) ? $__senpi_connection["localRoots"] : nil
28
+ root = roots[scheme] if roots.is_a?(Hash)
29
+ raise "Protocol paths are not supported by this helper: #{raw}" unless root.is_a?(String) && !root.empty?
30
+
31
+ relative = URI::DEFAULT_PARSER.unescape(match[2].tr("\\", "/"))
32
+ root_path = File.expand_path(root)
33
+ return root_path if relative.empty?
34
+ if relative.start_with?("/") || relative.split("/").include?("..")
35
+ raise "Unsafe #{scheme}:// path (absolute or traversal): #{raw}"
36
+ end
37
+
38
+ resolved = File.expand_path(relative, root_path)
39
+ unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)
40
+ raise "#{scheme}:// path escapes its root: #{raw}"
41
+ end
42
+ resolved
43
+ end
44
+
45
+ def __senpi_display_payload(value)
46
+ if value.is_a?(Hash)
47
+ kind = value["type"] || value[:type]
48
+ text_value = value["text"] || value[:text]
49
+ return ["text/markdown", text_value.to_s] if kind == "markdown" && !text_value.nil?
50
+ return ["image/png", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/png"
51
+ return ["image/jpeg", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/jpeg"
52
+ return ["application/json", JSON.generate(value)]
53
+ end
54
+ return ["application/json", JSON.generate(value)] if value.is_a?(Array)
55
+ ["text/plain", value.to_s]
56
+ end
3
57
 
4
58
  def display(value)
5
- payload = Base64.strict_encode64(JSON.generate(value))
6
- __senpi_emit({ "type" => "display", "mimeType" => "application/json", "dataBase64" => payload })
59
+ mime_type, payload = __senpi_display_payload(value)
60
+ data_base64 = mime_type.start_with?("image/") ? payload : Base64.strict_encode64(payload)
61
+ __senpi_emit({ "type" => "display", "mimeType" => mime_type, "dataBase64" => data_base64 })
7
62
  nil
8
63
  end
9
64
 
10
- def print(*values)
11
- __senpi_emit({ "type" => "text", "stream" => "stdout", "data" => values.join })
65
+ def display_image(base64, mime_type: "image/png")
66
+ __senpi_emit({ "type" => "display", "mimeType" => mime_type.to_s, "dataBase64" => base64.to_s })
67
+ nil
68
+ end
69
+
70
+ def text(data)
71
+ __senpi_emit({ "type" => "text", "stream" => "stdout", "data" => data.to_s })
12
72
  nil
13
73
  end
14
74
 
15
- def read(path)
16
- File.read(path.to_s)
75
+ def print(*values)
76
+ text(values.join)
77
+ end
78
+
79
+ def read(path, offset = 1, limit = nil)
80
+ resolved = __senpi_resolve_path(path)
81
+ data = File.read(resolved, encoding: Encoding::UTF_8)
82
+ if offset > 1 || !limit.nil?
83
+ lines = data.lines
84
+ start = [offset.to_i - 1, 0].max
85
+ data = lines[start, limit || lines.length].to_a.join
86
+ end
87
+ __senpi_emit_status("read", { "path" => resolved, "chars" => data.length, "preview" => data[0, 500].to_s })
88
+ data
17
89
  end
18
90
 
19
91
  def write(path, content)
20
- File.write(path.to_s, content.to_s)
21
- path.to_s
92
+ resolved = __senpi_resolve_path(path)
93
+ FileUtils.mkdir_p(File.dirname(resolved))
94
+ data = content.to_s
95
+ File.write(resolved, data)
96
+ __senpi_emit_status("write", { "path" => resolved, "chars" => data.length })
97
+ resolved
22
98
  end
23
99
 
24
100
  def env(key = nil, value = nil)
25
- return ENV.to_h if key.nil?
101
+ if key.nil?
102
+ entries = ENV.to_h.sort.to_h
103
+ __senpi_emit_status("env", { "count" => entries.length, "keys" => entries.keys.first(20) })
104
+ return entries
105
+ end
106
+
107
+ name = key.to_s
26
108
  if value.nil?
27
- ENV[key.to_s]
28
- else
29
- ENV[key.to_s] = value.to_s
109
+ resolved = ENV[name]
110
+ __senpi_emit_status("env", { "key" => name, "value" => resolved, "action" => "get" })
111
+ return resolved
30
112
  end
113
+
114
+ resolved = value.to_s
115
+ ENV[name] = resolved
116
+ __senpi_emit_status("env", { "key" => name, "value" => resolved, "action" => "set" })
117
+ resolved
31
118
  end
32
119
 
33
- def log(message)
34
- __senpi_emit({ "type" => "log", "message" => message.to_s })
35
- nil
120
+ def __senpi_bridge_request(path, payload)
121
+ connection = $__senpi_connection
122
+ raise "Ruby tool bridge is not initialized" unless connection.is_a?(Hash)
123
+ port = connection["port"]
124
+ token = connection["token"]
125
+ raise "Ruby tool bridge is not initialized" unless port.is_a?(Integer) && token.is_a?(String)
126
+
127
+ uri = URI("http://127.0.0.1:#{port}#{path}")
128
+ request = Net::HTTP::Post.new(uri)
129
+ request["authorization"] = "Bearer #{token}"
130
+ request["content-type"] = "application/json"
131
+ request.body = JSON.generate(payload)
132
+ __senpi_emit_status("timeout-pause", force: true)
133
+ begin
134
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 10, read_timeout: 60) { |http| http.request(request) }
135
+ ensure
136
+ __senpi_emit_status("timeout-resume", force: true)
137
+ end
138
+ body = JSON.parse(response.body.to_s)
139
+ return body["value"] if body.is_a?(Hash) && body["ok"] == true
140
+
141
+ error = body.is_a?(Hash) ? body["error"] : body
142
+ raise(error.is_a?(Hash) ? error["message"].to_s : error.to_s)
36
143
  end
37
144
 
38
- def phase(title)
39
- __senpi_emit({ "type" => "phase", "title" => title.to_s })
40
- nil
145
+ def __senpi_call_tool(name, args)
146
+ __senpi_bridge_request("/call", { "callId" => "rb-#{Process.pid}-#{rand(1_000_000)}", "toolName" => name, "args" => args })
147
+ end
148
+
149
+ class SenpiToolCallable
150
+ def initialize(name)
151
+ @name = name
152
+ end
153
+
154
+ def call(args = nil, **kwargs)
155
+ merged = args.nil? ? {} : args.is_a?(Hash) ? args.transform_keys(&:to_s) : raise(ArgumentError, "tool.#{@name}(...) expects a Hash of arguments")
156
+ kwargs.each { |key, value| merged[key.to_s] = value }
157
+ __senpi_call_tool(@name, merged)
158
+ end
41
159
  end
42
160
 
43
161
  class SenpiToolProxy < BasicObject
44
162
  def method_missing(name, args = nil, **kwargs)
45
- merged = args.is_a?(::Hash) ? args.transform_keys(&:to_s) : {}
46
- kwargs.each { |key, value| merged[key.to_s] = value }
47
- ::Object.__send__(:__senpi_call_tool, name.to_s, merged)
163
+ ::SenpiToolCallable.new(name.to_s).call(args, **kwargs)
164
+ end
165
+
166
+ def [](name)
167
+ ::SenpiToolCallable.new(name.to_s)
48
168
  end
49
169
 
50
170
  def respond_to_missing?(_name, _private = false)
@@ -55,3 +175,96 @@ end
55
175
  def tool
56
176
  $__senpi_tool_proxy ||= SenpiToolProxy.new
57
177
  end
178
+
179
+ def completion(prompt, model: "default", system: nil, schema: nil, **kwargs)
180
+ options = { "model" => model }.merge(kwargs.transform_keys(&:to_s))
181
+ options["system"] = system unless system.nil?
182
+ options["schema"] = schema unless schema.nil?
183
+ result = __senpi_bridge_request("/completion", { "prompt" => prompt.to_s, "opts" => options })
184
+ return result unless result.is_a?(Hash)
185
+ return result["value"] if result.key?("value")
186
+
187
+ result.fetch("text", result)
188
+ end
189
+
190
+ def output(*ids, format: "raw", offset: nil, limit: nil)
191
+ raise ArgumentError, "At least one output ID is required" if ids.empty?
192
+ raise ArgumentError, "output() format must be 'raw' or 'tail'" unless ["raw", "tail"].include?(format)
193
+ args = { "ids" => ids.map(&:to_s), "format" => format }
194
+ args["offset"] = offset unless offset.nil?
195
+ args["limit"] = limit unless limit.nil?
196
+ __senpi_call_tool(SENPI_RESERVED_OUTPUT_TOOL, args)
197
+ end
198
+
199
+ def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, isolated: nil, apply: nil, merge: nil, handle: false)
200
+ args = { "prompt" => prompt.to_s, "agent" => agent }
201
+ { "model" => model, "label" => label, "schema" => schema, "isolated" => isolated, "apply" => apply, "merge" => merge }.each do |key, value|
202
+ args[key] = value unless value.nil?
203
+ end
204
+ args["handle"] = true if handle
205
+ response = __senpi_call_tool(SENPI_RESERVED_AGENT_TOOL, args)
206
+ record = response.is_a?(Hash) ? response : {}
207
+ text_value = record.fetch("text", response)
208
+ result = schema.nil? ? text_value : record.key?("data") ? record["data"] : JSON.parse(text_value.to_s)
209
+ return result unless handle
210
+ { "text" => text_value, "output" => text_value, "handle" => record["handle"] || (record["id"] && "agent://#{record["id"]}"), "id" => record["id"], "agent" => record.fetch("agent", agent) }.tap do |node|
211
+ node["data"] = result unless schema.nil?
212
+ end
213
+ end
214
+
215
+ def __senpi_pool_map(items)
216
+ values = items.to_a
217
+ return [] if values.empty?
218
+ connection = $__senpi_connection
219
+ configured_width = connection.is_a?(Hash) ? connection["parallelPoolWidth"] : nil
220
+ width = configured_width.is_a?(Numeric) ? configured_width.to_i : 4
221
+ workers = [[width, 1].max, values.length].min
222
+ results = Array.new(values.length)
223
+ failures = {}
224
+ failure_mutex = Mutex.new
225
+ queue = Queue.new
226
+ values.each_index { |index| queue << index }
227
+ threads = workers.times.map do
228
+ Thread.new do
229
+ loop do
230
+ index = queue.pop(true) rescue nil
231
+ break if index.nil?
232
+ begin
233
+ results[index] = yield(values[index])
234
+ rescue Exception => error
235
+ failure_mutex.synchronize { failures[index] = error }
236
+ end
237
+ end
238
+ end
239
+ end
240
+ threads.each(&:join)
241
+ raise failures[failures.keys.min] unless failures.empty?
242
+ results
243
+ end
244
+
245
+ def parallel(thunks)
246
+ values = thunks.to_a
247
+ values.each do |thunk|
248
+ raise TypeError, "parallel() expects an iterable of zero-arg callables" unless thunk.respond_to?(:call)
249
+ end
250
+ __senpi_pool_map(values) { |thunk| thunk.call }
251
+ end
252
+
253
+ def pipeline(items, *stages)
254
+ values = items.to_a
255
+ stages.each do |stage|
256
+ raise TypeError, "pipeline() stages must be callables" unless stage.respond_to?(:call)
257
+ values = __senpi_pool_map(values) { |value| stage.call(value) }
258
+ end
259
+ values
260
+ end
261
+
262
+ def log(message)
263
+ __senpi_emit({ "type" => "log", "message" => message.to_s })
264
+ nil
265
+ end
266
+
267
+ def phase(title)
268
+ __senpi_emit({ "type" => "phase", "title" => title.to_s })
269
+ nil
270
+ end
@@ -1,92 +1,193 @@
1
1
  require "json"
2
2
  require "net/http"
3
- require "stringio"
4
3
  require "uri"
5
- require_relative "prelude"
6
4
 
7
5
  $__senpi_binding = TOPLEVEL_BINDING
8
- $__senpi_mutex = Mutex.new
6
+ $__senpi_frame_mutex = Mutex.new
9
7
  $__senpi_current_cell = nil
8
+ $__senpi_capture_cell = nil
10
9
  $__senpi_connection = nil
10
+ $__senpi_frame_io = STDOUT.dup
11
+ $__senpi_frame_io.sync = true
12
+
13
+ begin
14
+ stdout_read, stdout_write = IO.pipe
15
+ STDOUT.reopen(stdout_write)
16
+ STDOUT.sync = true
17
+ stdout_write.close
18
+ $__senpi_stdout_capture = stdout_read
19
+ rescue StandardError
20
+ $__senpi_stdout_capture = nil
21
+ end
22
+
23
+ begin
24
+ stderr_read, stderr_write = IO.pipe
25
+ STDERR.reopen(stderr_write)
26
+ STDERR.sync = true
27
+ stderr_write.close
28
+ $__senpi_stderr_capture = stderr_read
29
+ rescue StandardError
30
+ $__senpi_stderr_capture = nil
31
+ end
32
+
33
+ begin
34
+ $__senpi_protocol_stdin = STDIN.dup
35
+ STDIN.reopen(File.open(File::NULL, "r"))
36
+ rescue StandardError
37
+ $__senpi_protocol_stdin = STDIN
38
+ end
11
39
 
12
40
  def __senpi_emit(frame)
13
- STDOUT.write(JSON.generate(frame))
14
- STDOUT.write("\n")
15
- STDOUT.flush
41
+ line = JSON.generate(frame)
42
+ $__senpi_frame_mutex.synchronize do
43
+ $__senpi_frame_io.write(line)
44
+ $__senpi_frame_io.write("\n")
45
+ $__senpi_frame_io.flush
46
+ end
47
+ rescue StandardError
48
+ nil
16
49
  end
17
50
 
18
51
  def __senpi_error(error)
19
- { "name" => error.class.name, "message" => error.message, "stack" => error.backtrace&.join("\n") }
52
+ { "name" => error.class.name, "message" => error.message.to_s, "stack" => error.backtrace&.join("\n") }
53
+ end
54
+
55
+ def __senpi_emit_stream(stream, data)
56
+ return if data.nil? || data.empty? || $__senpi_capture_cell.nil?
57
+ __senpi_emit({ "type" => "text", "stream" => stream, "data" => data })
58
+ end
59
+
60
+ class SenpiStreamProxy
61
+ def initialize(stream)
62
+ @stream = stream
63
+ end
64
+
65
+ def write(*values)
66
+ data = values.join
67
+ __senpi_emit_stream(@stream, data)
68
+ data.bytesize
69
+ end
70
+
71
+ def print(*values)
72
+ write(*values)
73
+ nil
74
+ end
75
+
76
+ def puts(*values)
77
+ values = [""] if values.empty?
78
+ values.each { |value| write(value.to_s.end_with?("\n") ? value.to_s : "#{value}\n") }
79
+ nil
80
+ end
81
+
82
+ def printf(format, *values)
83
+ write(Kernel.format(format, *values))
84
+ nil
85
+ end
86
+
87
+ def flush
88
+ self
89
+ end
90
+
91
+ def sync
92
+ true
93
+ end
94
+
95
+ def sync=(_value)
96
+ true
97
+ end
98
+
99
+ def tty?
100
+ false
101
+ end
20
102
  end
21
103
 
22
- def __senpi_call_tool(name, args)
23
- connection = $__senpi_connection
24
- raise "Ruby tool bridge is not initialized" unless connection.is_a?(Hash)
25
- port = connection["port"]
26
- token = connection["token"]
27
- raise "Ruby tool bridge is not initialized" unless port.is_a?(Integer) && token.is_a?(String)
28
-
29
- call_id = "#{Time.now.to_i}-#{rand(1_000_000)}"
30
- uri = URI("http://127.0.0.1:#{port}/call")
31
- request = Net::HTTP::Post.new(uri)
32
- request["authorization"] = "Bearer #{token}"
33
- request["content-type"] = "application/json"
34
- request.body = JSON.generate({ "callId" => call_id, "toolName" => name, "args" => args })
35
- response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: 60) { |http| http.request(request) }
36
- body = JSON.parse(response.body.to_s)
37
- return body["value"] if body.is_a?(Hash) && body["ok"] == true
38
-
39
- error = body.is_a?(Hash) ? body["error"] : body
40
- if error.is_a?(Hash)
41
- raise error["message"].to_s
104
+ def __senpi_start_capture(io, stream)
105
+ return if io.nil?
106
+ Thread.new do
107
+ loop do
108
+ data = io.readpartial(65_536)
109
+ __senpi_emit_stream(stream, data)
110
+ rescue EOFError, IOError, Errno::EBADF
111
+ break
112
+ end
42
113
  end
43
- raise error.to_s
44
114
  end
45
115
 
46
- # Capture the cell's $stdout writes (puts/print) and emit them as `text` frames.
47
- # $stdout is redirected to a buffer during eval so user output never contaminates
48
- # the JSONL protocol channel, which __senpi_emit writes to via the STDOUT constant
49
- # (the STDOUT constant is unaffected by reassigning the $stdout global).
116
+ def __senpi_value_repr(value)
117
+ JSON.generate(value)
118
+ rescue JSON::GeneratorError
119
+ value.inspect
120
+ end
121
+
122
+ SENPI_NON_DISPLAY_NODES = %i[
123
+ LASGN IASGN GASGN CVASGN DASGN OP_ASGN OP_CDECL CDECL MASGN CASGN
124
+ DEFN DEFS CLASS MODULE SCLASS ALIAS UNDEF
125
+ ].freeze
126
+
127
+ def __senpi_ast_last(node)
128
+ return nil unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
129
+ case node.type
130
+ when :SCOPE
131
+ __senpi_ast_last(node.children[2])
132
+ when :BLOCK
133
+ children = node.children.compact
134
+ children.empty? ? nil : __senpi_ast_last(children.last)
135
+ else
136
+ node
137
+ end
138
+ end
139
+
140
+ def __senpi_should_display_result?(source)
141
+ return true unless defined?(RubyVM::AbstractSyntaxTree)
142
+ node = RubyVM::AbstractSyntaxTree.parse(source)
143
+ last = __senpi_ast_last(node)
144
+ return true if last.nil?
145
+ !SENPI_NON_DISPLAY_NODES.include?(last.type)
146
+ rescue StandardError, SyntaxError
147
+ true
148
+ end
149
+
150
+ require_relative "prelude"
151
+
152
+ $stdout = SenpiStreamProxy.new("stdout")
153
+ $stderr = SenpiStreamProxy.new("stderr")
154
+ __senpi_start_capture($__senpi_stdout_capture, "stdout")
155
+ __senpi_start_capture($__senpi_stderr_capture, "stderr")
156
+
50
157
  def __senpi_run_cell(message)
51
158
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
- $__senpi_current_cell = message["cellId"]
53
- captured = StringIO.new
54
- previous_stdout = $stdout
55
- $stdout = captured
159
+ cell_id = message["cellId"].to_s
160
+ $__senpi_current_cell = cell_id
161
+ $__senpi_capture_cell = cell_id
56
162
  begin
57
- value = eval(message["code"].to_s, $__senpi_binding, "(senpi-rb)")
58
- $stdout = previous_stdout
59
- __senpi_flush_stdout(captured)
163
+ source = message["code"].to_s
164
+ value = eval(source, $__senpi_binding, "(senpi-rb)")
165
+ STDOUT.flush
166
+ STDERR.flush
167
+ Thread.pass
60
168
  frame = {
61
169
  "type" => "result",
62
- "cellId" => message["cellId"],
170
+ "cellId" => cell_id,
63
171
  "ok" => true,
64
172
  "durationMs" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
65
173
  }
66
- frame["valueRepr"] = JSON.generate(value) unless value.nil?
174
+ frame["valueRepr"] = __senpi_value_repr(value) if !value.nil? && __senpi_should_display_result?(source)
67
175
  __senpi_emit(frame)
68
176
  rescue Exception => error
69
- $stdout = previous_stdout
70
- __senpi_flush_stdout(captured)
71
177
  __senpi_emit({
72
178
  "type" => "result",
73
- "cellId" => message["cellId"],
179
+ "cellId" => cell_id,
74
180
  "ok" => false,
75
181
  "error" => __senpi_error(error),
76
182
  "durationMs" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
77
183
  })
78
184
  ensure
79
- $stdout = previous_stdout
185
+ $__senpi_capture_cell = nil
80
186
  $__senpi_current_cell = nil
81
187
  end
82
188
  end
83
189
 
84
- def __senpi_flush_stdout(captured)
85
- data = captured.string
86
- __senpi_emit({ "type" => "text", "stream" => "stdout", "data" => data }) unless data.empty?
87
- end
88
-
89
- STDIN.each_line do |line|
190
+ $__senpi_protocol_stdin.each_line do |line|
90
191
  message = JSON.parse(line)
91
192
  case message["type"]
92
193
  when "init"
@@ -98,4 +199,6 @@ STDIN.each_line do |line|
98
199
  __senpi_emit({ "type" => "closed" })
99
200
  exit 0
100
201
  end
202
+ rescue JSON::ParserError => error
203
+ __senpi_emit({ "type" => "init-failed", "error" => __senpi_error(error) })
101
204
  end
@@ -84,6 +84,7 @@ export class SubprocessRunQueue {
84
84
  case "display":
85
85
  case "log":
86
86
  case "phase":
87
+ case "status":
87
88
  if (this.#active) onMessage?.(message);
88
89
  return false;
89
90
  case "ready":
@@ -0,0 +1,89 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export interface SessionArtifactsDir {
7
+ readonly dir: string;
8
+ readonly temp: boolean;
9
+ }
10
+
11
+ export function formatMiddleElisionMarker(elidedLines: number, elidedBytes: number): string {
12
+ return elidedLines <= 1 ? `[…${elidedBytes}B elided…]` : `[…${elidedLines}ln elided…]`;
13
+ }
14
+
15
+ export function artifactNotice(path: string): string {
16
+ return `[Full output: ${path}]`;
17
+ }
18
+
19
+ export function resolveSessionArtifactsDir(sessionFile: string | undefined): SessionArtifactsDir {
20
+ const temp = sessionFile === undefined;
21
+ const dir =
22
+ sessionFile === undefined
23
+ ? join(tmpdir(), `senpi-codemode-${randomBytes(8).toString("hex")}`)
24
+ : `${sessionFile.endsWith(".jsonl") ? sessionFile.slice(0, -6) : sessionFile}-artifacts`;
25
+ mkdirSync(dir, { recursive: true });
26
+ return { dir, temp };
27
+ }
28
+
29
+ export interface TruncationMeta {
30
+ readonly direction: "head" | "tail" | "middle";
31
+ readonly truncatedBy: "lines" | "bytes" | "middle";
32
+ readonly totalLines: number;
33
+ readonly totalBytes: number;
34
+ readonly outputLines: number;
35
+ readonly outputBytes: number;
36
+ readonly maxBytes?: number;
37
+ readonly shownRange?: { readonly start: number; readonly end: number };
38
+ readonly headRange?: { readonly start: number; readonly end: number };
39
+ readonly tailRange?: { readonly start: number; readonly end: number };
40
+ readonly elidedBytes?: number;
41
+ readonly elidedLines?: number;
42
+ /** Plain absolute artifact path; retained under the upstream field name. */
43
+ readonly artifactId?: string;
44
+ }
45
+
46
+ function formatBytes(bytes: number): string {
47
+ if (bytes < 1024) return `${bytes}B`;
48
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
49
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
50
+ }
51
+
52
+ function assertNever(value: never): never {
53
+ throw new TypeError(`Unhandled truncation direction: ${String(value)}`);
54
+ }
55
+
56
+ export function formatTruncationWarning(meta: TruncationMeta | undefined): string | null {
57
+ if (meta === undefined) return null;
58
+ let message: string;
59
+ switch (meta.direction) {
60
+ case "middle": {
61
+ const elidedLines = meta.elidedLines ?? Math.max(0, meta.totalLines - meta.outputLines);
62
+ const elidedBytes = meta.elidedBytes ?? Math.max(0, meta.totalBytes - meta.outputBytes);
63
+ message =
64
+ meta.headRange !== undefined && meta.tailRange !== undefined
65
+ ? `Showing lines ${meta.headRange.start}-${meta.headRange.end} and ${meta.tailRange.start}-${meta.tailRange.end} of ${meta.totalLines}; ${elidedLines} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`
66
+ : `Showing ${meta.outputLines} of ${meta.totalLines} lines; middle elided`;
67
+ break;
68
+ }
69
+ case "head":
70
+ case "tail":
71
+ message =
72
+ meta.shownRange !== undefined && meta.shownRange.end >= meta.shownRange.start
73
+ ? `Showing lines ${meta.shownRange.start}-${meta.shownRange.end} of ${meta.totalLines}`
74
+ : `Showing ${meta.outputLines} of ${meta.totalLines} lines`;
75
+ if (meta.truncatedBy === "bytes") message += ` (${formatBytes(meta.maxBytes ?? meta.outputBytes)} limit)`;
76
+ break;
77
+ default:
78
+ return assertNever(meta.direction);
79
+ }
80
+ if (meta.artifactId !== undefined) message += `. Full output: ${meta.artifactId}`;
81
+ return `[${message}]`;
82
+ }
83
+
84
+ export function stripOutputNotice(text: string, meta: TruncationMeta | undefined): string {
85
+ const notice = formatTruncationWarning(meta);
86
+ if (notice === null) return text;
87
+ const trimmed = text.trimEnd();
88
+ return trimmed.endsWith(notice) ? trimmed.slice(0, -notice.length).trimEnd() : text;
89
+ }