@wrongstack/tools 0.293.0 → 0.295.1

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 (258) hide show
  1. package/dist/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/auto-proceed-loop-guard.d.ts +31 -0
  13. package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
  14. package/dist/auto-proceed-loop-guard.js +24 -1
  15. package/dist/auto-proceed-loop-guard.js.map +2 -2
  16. package/dist/bash.d.ts +1 -1
  17. package/dist/bash.d.ts.map +1 -1
  18. package/dist/bash.js +105 -75
  19. package/dist/bash.js.map +3 -3
  20. package/dist/batch-tool-use.d.ts +1 -1
  21. package/dist/batch-tool-use.d.ts.map +1 -1
  22. package/dist/batch-tool-use.js +1 -1
  23. package/dist/batch-tool-use.js.map +1 -1
  24. package/dist/browser/index.js +10 -1
  25. package/dist/browser/index.js.map +2 -2
  26. package/dist/browser/manager.d.ts +2 -0
  27. package/dist/browser/manager.d.ts.map +1 -1
  28. package/dist/browser/tools.d.ts +1 -1
  29. package/dist/browser/tools.d.ts.map +1 -1
  30. package/dist/builtin.d.ts +1 -1
  31. package/dist/builtin.d.ts.map +1 -1
  32. package/dist/builtin.js +1650 -640
  33. package/dist/builtin.js.map +4 -4
  34. package/dist/codebase-index/background-indexer.d.ts +2 -2
  35. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  36. package/dist/codebase-index/bm25.d.ts +3 -1
  37. package/dist/codebase-index/bm25.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  41. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  42. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  43. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  44. package/dist/codebase-index/generic-parser.d.ts +34 -0
  45. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  46. package/dist/codebase-index/go-parser.d.ts +1 -1
  47. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  48. package/dist/codebase-index/index.d.ts +4 -2
  49. package/dist/codebase-index/index.d.ts.map +1 -1
  50. package/dist/codebase-index/index.js +1068 -372
  51. package/dist/codebase-index/index.js.map +4 -4
  52. package/dist/codebase-index/indexer.d.ts +6 -1
  53. package/dist/codebase-index/indexer.d.ts.map +1 -1
  54. package/dist/codebase-index/json-parser.d.ts +1 -1
  55. package/dist/codebase-index/languages.d.ts +25 -0
  56. package/dist/codebase-index/languages.d.ts.map +1 -0
  57. package/dist/codebase-index/py-parser.d.ts +9 -1
  58. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  59. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  60. package/dist/codebase-index/rs-parser.d.ts +1 -1
  61. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  62. package/dist/codebase-index/schema.d.ts +9 -2
  63. package/dist/codebase-index/schema.d.ts.map +1 -1
  64. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  65. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  66. package/dist/codebase-index/ts-parser.d.ts +2 -2
  67. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  68. package/dist/codebase-index/worker.js +993 -348
  69. package/dist/codebase-index/worker.js.map +4 -4
  70. package/dist/codebase-index/writer.d.ts +83 -4
  71. package/dist/codebase-index/writer.d.ts.map +1 -1
  72. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  73. package/dist/design.d.ts +5 -2
  74. package/dist/design.d.ts.map +1 -1
  75. package/dist/design.js +36 -3
  76. package/dist/design.js.map +2 -2
  77. package/dist/diff.d.ts +1 -1
  78. package/dist/diff.d.ts.map +1 -1
  79. package/dist/diff.js +2 -2
  80. package/dist/diff.js.map +1 -1
  81. package/dist/document.d.ts +1 -1
  82. package/dist/document.d.ts.map +1 -1
  83. package/dist/document.js +1 -1
  84. package/dist/document.js.map +1 -1
  85. package/dist/e2e.d.ts +1 -1
  86. package/dist/e2e.d.ts.map +1 -1
  87. package/dist/e2e.js +1 -1
  88. package/dist/e2e.js.map +1 -1
  89. package/dist/edit.d.ts +1 -1
  90. package/dist/edit.d.ts.map +1 -1
  91. package/dist/edit.js +3 -3
  92. package/dist/edit.js.map +2 -2
  93. package/dist/exec.d.ts +1 -1
  94. package/dist/exec.d.ts.map +1 -1
  95. package/dist/exec.js +99 -44
  96. package/dist/exec.js.map +3 -3
  97. package/dist/fetch.d.ts +1 -1
  98. package/dist/fetch.d.ts.map +1 -1
  99. package/dist/fetch.js +4 -5
  100. package/dist/fetch.js.map +2 -2
  101. package/dist/format.d.ts +1 -1
  102. package/dist/format.d.ts.map +1 -1
  103. package/dist/format.js +81 -16
  104. package/dist/format.js.map +2 -2
  105. package/dist/git.d.ts +1 -1
  106. package/dist/git.d.ts.map +1 -1
  107. package/dist/git.js +3 -2
  108. package/dist/git.js.map +2 -2
  109. package/dist/glob.d.ts +1 -1
  110. package/dist/glob.d.ts.map +1 -1
  111. package/dist/glob.js +81 -31
  112. package/dist/glob.js.map +4 -4
  113. package/dist/grep.d.ts +1 -1
  114. package/dist/grep.d.ts.map +1 -1
  115. package/dist/grep.js +90 -22
  116. package/dist/grep.js.map +4 -4
  117. package/dist/index.d.ts +5 -4
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1895 -771
  120. package/dist/index.js.map +4 -4
  121. package/dist/install.d.ts +1 -1
  122. package/dist/install.d.ts.map +1 -1
  123. package/dist/install.js +85 -17
  124. package/dist/install.js.map +2 -2
  125. package/dist/json.d.ts +1 -1
  126. package/dist/json.d.ts.map +1 -1
  127. package/dist/json.js +2 -2
  128. package/dist/json.js.map +2 -2
  129. package/dist/kanban-evidence-bridge.d.ts +21 -0
  130. package/dist/kanban-evidence-bridge.d.ts.map +1 -0
  131. package/dist/kanban.d.ts +20 -3
  132. package/dist/kanban.d.ts.map +1 -1
  133. package/dist/kanban.js +256 -32
  134. package/dist/kanban.js.map +3 -3
  135. package/dist/languages/execute-tool.d.ts +1 -1
  136. package/dist/languages/execute-tool.d.ts.map +1 -1
  137. package/dist/languages/execute.d.ts +1 -1
  138. package/dist/languages/execute.d.ts.map +1 -1
  139. package/dist/languages/index.d.ts +1 -1
  140. package/dist/languages/index.d.ts.map +1 -1
  141. package/dist/languages/index.js +99 -17
  142. package/dist/languages/index.js.map +2 -2
  143. package/dist/languages/package-tool.d.ts +3 -4
  144. package/dist/languages/package-tool.d.ts.map +1 -1
  145. package/dist/languages/plan.d.ts.map +1 -1
  146. package/dist/languages/profiles/additional.d.ts.map +1 -1
  147. package/dist/languages/profiles/primary.d.ts.map +1 -1
  148. package/dist/languages/tool.d.ts +1 -1
  149. package/dist/languages/tool.d.ts.map +1 -1
  150. package/dist/lint.d.ts +1 -1
  151. package/dist/lint.d.ts.map +1 -1
  152. package/dist/lint.js +81 -16
  153. package/dist/lint.js.map +2 -2
  154. package/dist/logs.d.ts +1 -1
  155. package/dist/logs.d.ts.map +1 -1
  156. package/dist/logs.js +2 -2
  157. package/dist/logs.js.map +1 -1
  158. package/dist/memory.d.ts +1 -1
  159. package/dist/memory.d.ts.map +1 -1
  160. package/dist/memory.js +1 -1
  161. package/dist/memory.js.map +1 -1
  162. package/dist/mode.d.ts +1 -1
  163. package/dist/mode.d.ts.map +1 -1
  164. package/dist/mode.js.map +1 -1
  165. package/dist/outdated.d.ts +1 -1
  166. package/dist/outdated.d.ts.map +1 -1
  167. package/dist/outdated.js +101 -19
  168. package/dist/outdated.js.map +2 -2
  169. package/dist/pack.d.ts +1 -1
  170. package/dist/pack.js +1650 -640
  171. package/dist/pack.js.map +4 -4
  172. package/dist/patch.d.ts +1 -1
  173. package/dist/patch.d.ts.map +1 -1
  174. package/dist/patch.js +2 -2
  175. package/dist/patch.js.map +1 -1
  176. package/dist/plan.d.ts +1 -1
  177. package/dist/plan.d.ts.map +1 -1
  178. package/dist/plan.js +7 -7
  179. package/dist/plan.js.map +2 -2
  180. package/dist/process-guardian.d.ts +7 -0
  181. package/dist/process-guardian.d.ts.map +1 -1
  182. package/dist/process-registry-persistent.d.ts +5 -0
  183. package/dist/process-registry-persistent.d.ts.map +1 -1
  184. package/dist/process-registry.d.ts +9 -1
  185. package/dist/process-registry.d.ts.map +1 -1
  186. package/dist/process-registry.js +18 -3
  187. package/dist/process-registry.js.map +2 -2
  188. package/dist/ps-slash.js +93 -38
  189. package/dist/ps-slash.js.map +2 -2
  190. package/dist/read.d.ts +1 -1
  191. package/dist/read.d.ts.map +1 -1
  192. package/dist/read.js +3 -2
  193. package/dist/read.js.map +2 -2
  194. package/dist/replace.d.ts +1 -1
  195. package/dist/replace.d.ts.map +1 -1
  196. package/dist/replace.js +4 -4
  197. package/dist/replace.js.map +2 -2
  198. package/dist/scaffold.d.ts +1 -1
  199. package/dist/scaffold.d.ts.map +1 -1
  200. package/dist/scaffold.js +2 -2
  201. package/dist/scaffold.js.map +1 -1
  202. package/dist/search.d.ts +1 -1
  203. package/dist/search.d.ts.map +1 -1
  204. package/dist/search.js +4 -4
  205. package/dist/search.js.map +2 -2
  206. package/dist/session-kanban.d.ts +4 -1
  207. package/dist/session-kanban.d.ts.map +1 -1
  208. package/dist/session-kanban.js +23 -15
  209. package/dist/session-kanban.js.map +2 -2
  210. package/dist/set-working-dir.d.ts +1 -1
  211. package/dist/set-working-dir.d.ts.map +1 -1
  212. package/dist/skill.d.ts +1 -1
  213. package/dist/skill.d.ts.map +1 -1
  214. package/dist/skill.js +2 -5
  215. package/dist/skill.js.map +2 -2
  216. package/dist/spawn-background.d.ts.map +1 -1
  217. package/dist/task.d.ts +3 -2
  218. package/dist/task.d.ts.map +1 -1
  219. package/dist/task.js +6 -8
  220. package/dist/task.js.map +2 -2
  221. package/dist/test.d.ts +1 -1
  222. package/dist/test.d.ts.map +1 -1
  223. package/dist/test.js +81 -16
  224. package/dist/test.js.map +2 -2
  225. package/dist/todo.d.ts +2 -1
  226. package/dist/todo.d.ts.map +1 -1
  227. package/dist/todo.js +4 -4
  228. package/dist/todo.js.map +2 -2
  229. package/dist/tool-help.d.ts +1 -1
  230. package/dist/tool-help.d.ts.map +1 -1
  231. package/dist/tool-help.js.map +1 -1
  232. package/dist/tool-search.d.ts +1 -1
  233. package/dist/tool-search.d.ts.map +1 -1
  234. package/dist/tool-search.js.map +1 -1
  235. package/dist/tool-tier.d.ts +16 -0
  236. package/dist/tool-tier.d.ts.map +1 -0
  237. package/dist/tool-tier.js +22402 -0
  238. package/dist/tool-tier.js.map +7 -0
  239. package/dist/tool-use.d.ts +1 -1
  240. package/dist/tool-use.d.ts.map +1 -1
  241. package/dist/tool-use.js.map +1 -1
  242. package/dist/tree.d.ts +1 -1
  243. package/dist/tree.d.ts.map +1 -1
  244. package/dist/tree.js +3 -15
  245. package/dist/tree.js.map +2 -2
  246. package/dist/typecheck.d.ts +1 -1
  247. package/dist/typecheck.d.ts.map +1 -1
  248. package/dist/typecheck.js +81 -16
  249. package/dist/typecheck.js.map +2 -2
  250. package/dist/win32.d.ts +10 -0
  251. package/dist/win32.d.ts.map +1 -0
  252. package/dist/win32.js +67 -0
  253. package/dist/win32.js.map +7 -0
  254. package/dist/write.d.ts +1 -1
  255. package/dist/write.d.ts.map +1 -1
  256. package/dist/write.js +3 -2
  257. package/dist/write.js.map +2 -2
  258. package/package.json +11 -3
@@ -1,4 +1,4 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  interface ToolUseInput {
3
3
  tool: string;
4
4
  input: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"tool-use.d.ts","sourceRoot":"","sources":["../src/tool-use.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,CA2FzD,CAAC"}
1
+ {"version":3,"file":"tool-use.d.ts","sourceRoot":"","sources":["../src/tool-use.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,CA2FzD,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/tool-use.ts"],
4
- "sourcesContent": ["import type { Tool } from '@wrongstack/core';\n\ninterface ToolUseInput {\n tool: string;\n input: Record<string, unknown>;\n}\n\ninterface ToolUseOutput {\n tool: string;\n success: boolean;\n result?: unknown | undefined;\n error?: string | undefined;\n executionMs: number;\n}\n\nexport const toolUseTool: Tool<ToolUseInput, ToolUseOutput> = {\n name: 'tool_use',\n category: 'Meta',\n description:\n 'Directly execute any registered tool by its exact name, bypassing normal discovery. ' +\n 'This is a powerful meta-tool intended for cases where the agent has a clear plan and knows precisely which tool to invoke.',\n usageHint:\n 'ADVANCED META TOOL \u2014 USE WITH CARE:\\n\\n' +\n '- Only use when you are certain of the exact tool name and its expected input shape.\\n' +\n '- Prefer using the normal tool calling mechanism when possible.\\n' +\n '- Very useful in batch-tool-use or when orchestrating complex workflows programmatically.\\n' +\n '- The call still goes through full permission checks and capability validation.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 60_000,\n capabilities: ['tool.mutate.any'],\n icon: 'meta',\n inputSchema: {\n type: 'object',\n properties: {\n tool: {\n type: 'string',\n description: 'The exact registered name of the tool to invoke (e.g. \"bash\", \"read\", \"codebase-search\").',\n },\n input: {\n type: 'object',\n description: 'The input object matching the target tool\\'s inputSchema.',\n },\n },\n required: ['tool'],\n },\n async execute(input, ctx, opts) {\n const start = Date.now();\n\n if (!input?.tool) {\n return {\n tool: 'unknown',\n success: false,\n error: 'tool_use: tool name is required',\n executionMs: 0,\n };\n }\n\n const tool = ctx.tools.find((t: Tool) => t.name === input.tool);\n if (!tool) {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" not found`,\n executionMs: Date.now() - start,\n };\n }\n\n // `deny` is a hard policy gate \u2014 bypassing it through a meta-tool\n // would defeat the whole point of the permission system. Keep this\n // check even though the outer `tool_use` already requires `confirm`.\n if (tool.permission === 'deny') {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" is denied by policy`,\n executionMs: Date.now() - start,\n };\n }\n\n // Note: inner `permission === 'confirm'` is intentionally NOT short-\n // circuited here. The outer `tool_use` itself has `permission: 'confirm'`,\n // so the user already saw the full args (including which inner tool will\n // run, and with what input) before approving the meta-call. Duplicating\n // the check inside execute() turned every confirm-tool dispatch through\n // `tool_use` into a hard failure \u2014 the model would see \"requires\n // confirmation\" with no way to proceed, even after the user said yes.\n // `batch_tool_use` already follows this same model.\n\n try {\n const result = await tool.execute(input.input, ctx, opts);\n return {\n tool: input.tool,\n success: true,\n result,\n executionMs: Date.now() - start,\n };\n } catch (e) {\n return {\n tool: input.tool,\n success: false,\n error: e instanceof Error ? e.message : String(e),\n executionMs: Date.now() - start,\n };\n }\n },\n};\n"],
4
+ "sourcesContent": ["import type { Tool } from '@wrongstack/core/types';\n\ninterface ToolUseInput {\n tool: string;\n input: Record<string, unknown>;\n}\n\ninterface ToolUseOutput {\n tool: string;\n success: boolean;\n result?: unknown | undefined;\n error?: string | undefined;\n executionMs: number;\n}\n\nexport const toolUseTool: Tool<ToolUseInput, ToolUseOutput> = {\n name: 'tool_use',\n category: 'Meta',\n description:\n 'Directly execute any registered tool by its exact name, bypassing normal discovery. ' +\n 'This is a powerful meta-tool intended for cases where the agent has a clear plan and knows precisely which tool to invoke.',\n usageHint:\n 'ADVANCED META TOOL \u2014 USE WITH CARE:\\n\\n' +\n '- Only use when you are certain of the exact tool name and its expected input shape.\\n' +\n '- Prefer using the normal tool calling mechanism when possible.\\n' +\n '- Very useful in batch-tool-use or when orchestrating complex workflows programmatically.\\n' +\n '- The call still goes through full permission checks and capability validation.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 60_000,\n capabilities: ['tool.mutate.any'],\n icon: 'meta',\n inputSchema: {\n type: 'object',\n properties: {\n tool: {\n type: 'string',\n description: 'The exact registered name of the tool to invoke (e.g. \"bash\", \"read\", \"codebase-search\").',\n },\n input: {\n type: 'object',\n description: 'The input object matching the target tool\\'s inputSchema.',\n },\n },\n required: ['tool'],\n },\n async execute(input, ctx, opts) {\n const start = Date.now();\n\n if (!input?.tool) {\n return {\n tool: 'unknown',\n success: false,\n error: 'tool_use: tool name is required',\n executionMs: 0,\n };\n }\n\n const tool = ctx.tools.find((t: Tool) => t.name === input.tool);\n if (!tool) {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" not found`,\n executionMs: Date.now() - start,\n };\n }\n\n // `deny` is a hard policy gate \u2014 bypassing it through a meta-tool\n // would defeat the whole point of the permission system. Keep this\n // check even though the outer `tool_use` already requires `confirm`.\n if (tool.permission === 'deny') {\n return {\n tool: input.tool,\n success: false,\n error: `tool_use: tool \"${input.tool}\" is denied by policy`,\n executionMs: Date.now() - start,\n };\n }\n\n // Note: inner `permission === 'confirm'` is intentionally NOT short-\n // circuited here. The outer `tool_use` itself has `permission: 'confirm'`,\n // so the user already saw the full args (including which inner tool will\n // run, and with what input) before approving the meta-call. Duplicating\n // the check inside execute() turned every confirm-tool dispatch through\n // `tool_use` into a hard failure \u2014 the model would see \"requires\n // confirmation\" with no way to proceed, even after the user said yes.\n // `batch_tool_use` already follows this same model.\n\n try {\n const result = await tool.execute(input.input, ctx, opts);\n return {\n tool: input.tool,\n success: true,\n result,\n executionMs: Date.now() - start,\n };\n } catch (e) {\n return {\n tool: input.tool,\n success: false,\n error: e instanceof Error ? e.message : String(e),\n executionMs: Date.now() - start,\n };\n }\n },\n};\n"],
5
5
  "mappings": ";AAeO,IAAM,cAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aACE;AAAA,EAEF,WACE;AAAA,EAKF,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,cAAc,CAAC,iBAAiB;AAAA,EAChC,MAAM;AAAA,EACN,aAAa;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,MACV,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA,OAAO;AAAA,QACL,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,UAAU,CAAC,MAAM;AAAA,EACnB;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK,MAAM;AAC9B,UAAM,QAAQ,KAAK,IAAI;AAEvB,QAAI,CAAC,OAAO,MAAM;AAChB,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO;AAAA,QACP,aAAa;AAAA,MACf;AAAA,IACF;AAEA,UAAM,OAAO,IAAI,MAAM,KAAK,CAAC,MAAY,EAAE,SAAS,MAAM,IAAI;AAC9D,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,SAAS;AAAA,QACT,OAAO,mBAAmB,MAAM,IAAI;AAAA,QACpC,aAAa,KAAK,IAAI,IAAI;AAAA,MAC5B;AAAA,IACF;AAKA,QAAI,KAAK,eAAe,QAAQ;AAC9B,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,SAAS;AAAA,QACT,OAAO,mBAAmB,MAAM,IAAI;AAAA,QACpC,aAAa,KAAK,IAAI,IAAI;AAAA,MAC5B;AAAA,IACF;AAWA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,QAAQ,MAAM,OAAO,KAAK,IAAI;AACxD,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,SAAS;AAAA,QACT;AAAA,QACA,aAAa,KAAK,IAAI,IAAI;AAAA,MAC5B;AAAA,IACF,SAAS,GAAG;AACV,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,SAAS;AAAA,QACT,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAAA,QAChD,aAAa,KAAK,IAAI,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/dist/tree.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  interface TreeInput {
3
3
  path?: string | undefined;
4
4
  depth?: number | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAsC,MAAM,kBAAkB,CAAC;AAgBjF,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAgJhD,CAAC"}
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAsC,MAAM,wBAAwB,CAAC;AAMvF,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACnC;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAgJhD,CAAC"}
package/dist/tree.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/tree.ts
2
- import { expectDefined } from "@wrongstack/core";
2
+ import { DEFAULT_WALK_IGNORE_DIRS, expectDefined } from "@wrongstack/core/utils";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path2 from "node:path";
5
5
 
6
6
  // src/_util.ts
7
7
  import * as path from "node:path";
8
- import * as Core from "@wrongstack/core";
8
+ import * as Core from "@wrongstack/core/utils";
9
9
  function resolvePath(input, ctx) {
10
10
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
11
11
  }
@@ -29,19 +29,7 @@ function safeResolve(input, ctx) {
29
29
  }
30
30
 
31
31
  // src/tree.ts
32
- var DEFAULT_IGNORE = [
33
- "node_modules",
34
- ".git",
35
- "dist",
36
- "build",
37
- ".next",
38
- "coverage",
39
- "__pycache__",
40
- ".wrongstack",
41
- ".ssh",
42
- ".gnupg",
43
- ".aws"
44
- ];
32
+ var DEFAULT_IGNORE = [...DEFAULT_WALK_IGNORE_DIRS, ".wrongstack", ".ssh", ".gnupg", ".aws"];
45
33
  var treeTool = {
46
34
  name: "tree",
47
35
  category: "Filesystem",
package/dist/tree.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/tree.ts", "../src/_util.ts"],
4
- "sourcesContent": ["import { expectDefined } from '@wrongstack/core';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool, ToolProgressEvent, ToolStreamEvent } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\nconst DEFAULT_IGNORE = [\n 'node_modules',\n '.git',\n 'dist',\n 'build',\n '.next',\n 'coverage',\n '__pycache__',\n '.wrongstack',\n '.ssh',\n '.gnupg',\n '.aws',\n];\n\ninterface TreeInput {\n path?: string | undefined;\n depth?: number | undefined;\n glob?: string | undefined;\n exclude?: string[] | undefined;\n show_files?: boolean | undefined;\n show_dirs?: boolean | undefined;\n show_hidden?: boolean | undefined;\n}\n\ninterface TreeOutput {\n tree: string;\n total_files: number;\n total_dirs: number;\n truncated: boolean;\n path: string;\n}\n\nexport const treeTool: Tool<TreeInput, TreeOutput> = {\n name: 'tree',\n category: 'Filesystem',\n description:\n 'Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.',\n usageHint:\n 'DIRECTORY-LAYOUT EXPLORATION:\\n\\n' +\n '- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\\n' +\n '- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\\n' +\n '- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\\n' +\n 'Output is truncated for very large trees.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'tree',\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Root directory to display the tree from (defaults to project root).',\n },\n depth: {\n type: 'integer',\n description: 'Maximum directory depth to traverse (default 3, use 0 for unlimited).',\n minimum: 0,\n maximum: 20,\n },\n glob: {\n type: 'string',\n description: 'Only include files matching this glob pattern.',\n },\n exclude: {\n type: 'array',\n items: { type: 'string' },\n description: 'List of directory names to completely ignore.',\n },\n show_files: {\n type: 'boolean',\n description: 'Whether to show individual files (default true).',\n },\n show_dirs: {\n type: 'boolean',\n description: 'Whether to show directories (default true).',\n },\n show_hidden: {\n type: 'boolean',\n description: 'Show hidden files starting with . (default: false)',\n },\n },\n },\n async execute(input, ctx, opts) {\n let final: TreeOutput | undefined;\n const executeStream = treeTool.executeStream;\n if (!executeStream) throw new Error('treeTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('tree: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx): AsyncGenerator<ToolStreamEvent<TreeOutput>> {\n const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\n const maxDepth = input.depth ?? 3;\n const showFiles = input.show_files ?? true;\n const showDirs = input.show_dirs ?? true;\n const showHidden = input.show_hidden ?? false;\n const exclude = new Set([...DEFAULT_IGNORE, ...(input.exclude ?? [])]);\n const filterGlob = input.glob;\n\n const lines: string[] = [basePath];\n const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };\n\n // Walker pushes progress into an async queue; the generator drains it.\n const queue: ToolProgressEvent[] = [];\n const FLUSH_EVERY = 200; // emit metric every 200 entries seen\n let lastEmittedTotal = 0;\n\n const tickProgress = () => {\n const seen = totals.totalFiles.value + totals.totalDirs.value;\n if (seen - lastEmittedTotal >= FLUSH_EVERY) {\n queue.push({\n type: 'metric',\n text: `${seen} entries`,\n data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value },\n });\n lastEmittedTotal = seen;\n }\n };\n\n const walkPromise = walkDir(basePath, 0, {\n maxDepth,\n exclude,\n showFiles,\n showDirs,\n showHidden,\n filterGlob,\n lines,\n prefix: '',\n isLast: true,\n totalFiles: totals.totalFiles,\n totalDirs: totals.totalDirs,\n onProgress: tickProgress,\n });\n\n // Race the walk against periodic flushes \u2014 yield metrics while it runs.\n let walkDone = false;\n walkPromise.finally(() => {\n walkDone = true;\n });\n\n while (!walkDone || queue.length > 0) {\n if (queue.length > 0) {\n yield expectDefined(queue.shift());\n } else {\n // Race the walk completion against a short tick so we don't busy-\n // spin while the producer fills the queue. Previously the\n // setTimeout was never cleared when walkPromise won \u2014 one stray\n // timer per drain iteration accumulated on the event loop.\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n const poll = new Promise<void>((r) => {\n pollTimer = setTimeout(r, 50);\n });\n try {\n await Promise.race([walkPromise, poll]).catch(() => undefined);\n } finally {\n if (pollTimer) clearTimeout(pollTimer);\n }\n }\n }\n await walkPromise; // surface any error\n\n yield {\n type: 'final',\n output: {\n tree: lines.join('\\n'),\n total_files: totals.totalFiles.value,\n total_dirs: totals.totalDirs.value,\n truncated: false,\n path: basePath,\n },\n };\n },\n};\n\ninterface WalkOptions {\n maxDepth: number;\n exclude: Set<string>;\n showFiles: boolean;\n showDirs: boolean;\n showHidden: boolean;\n filterGlob?: string | undefined;\n lines: string[];\n prefix: string;\n isLast: boolean;\n totalFiles: { value: number };\n totalDirs: { value: number };\n onProgress?: (() => void) | undefined;\n}\n\nasync function walkDir(dir: string, depth: number, opts: WalkOptions): Promise<void> {\n const entries = await fs\n .readdir(dir, { withFileTypes: true })\n .catch(() => [] as import('node:fs').Dirent[]);\n\n const filtered = entries.filter((e) => {\n if (!opts.showHidden && e.name.startsWith('.')) return false;\n if (opts.exclude.has(e.name)) return false;\n return true;\n });\n\n if (depth > 0) {\n const dirCount = filtered.filter((e) => e.isDirectory()).length;\n const fileCount = filtered.filter((e) => e.isFile()).length;\n opts.totalDirs.value += dirCount;\n opts.totalFiles.value += fileCount;\n opts.onProgress?.();\n }\n\n const items = filtered.sort((a, b) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n });\n\n for (let i = 0; i < items.length; i++) {\n const entry = items[i];\n /* v8 ignore next -- i is bounded by items.length, so entry is always defined; defensive. */\n if (!entry) continue;\n const isLast = i === items.length - 1;\n const connector = opts.isLast ? ' ' : '\u2502 ';\n const branch = isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';\n const displayName = entry.name + (entry.isDirectory() ? '/' : '');\n\n if (!opts.showDirs && entry.isDirectory()) continue;\n if (!opts.showFiles && entry.isFile()) continue;\n\n opts.lines.push(opts.prefix + branch + displayName);\n\n if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {\n const childPrefix = opts.prefix + connector;\n await walkDir(path.join(dir, entry.name), depth + 1, {\n ...opts,\n prefix: childPrefix,\n isLast,\n });\n }\n }\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { DEFAULT_WALK_IGNORE_DIRS, expectDefined } from '@wrongstack/core/utils';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool, ToolProgressEvent, ToolStreamEvent } from '@wrongstack/core/types';\nimport { safeResolve } from './_util.js';\n// Shared artifact/dependency dirs, plus tree-specific privacy dirs \u2014 tree can\n// be pointed at $HOME, where listing key material is never wanted.\nconst DEFAULT_IGNORE = [...DEFAULT_WALK_IGNORE_DIRS, '.wrongstack', '.ssh', '.gnupg', '.aws'];\n\ninterface TreeInput {\n path?: string | undefined;\n depth?: number | undefined;\n glob?: string | undefined;\n exclude?: string[] | undefined;\n show_files?: boolean | undefined;\n show_dirs?: boolean | undefined;\n show_hidden?: boolean | undefined;\n}\n\ninterface TreeOutput {\n tree: string;\n total_files: number;\n total_dirs: number;\n truncated: boolean;\n path: string;\n}\n\nexport const treeTool: Tool<TreeInput, TreeOutput> = {\n name: 'tree',\n category: 'Filesystem',\n description:\n 'Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.',\n usageHint:\n 'DIRECTORY-LAYOUT EXPLORATION:\\n\\n' +\n '- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\\n' +\n '- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\\n' +\n '- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\\n' +\n 'Output is truncated for very large trees.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'tree',\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Root directory to display the tree from (defaults to project root).',\n },\n depth: {\n type: 'integer',\n description: 'Maximum directory depth to traverse (default 3, use 0 for unlimited).',\n minimum: 0,\n maximum: 20,\n },\n glob: {\n type: 'string',\n description: 'Only include files matching this glob pattern.',\n },\n exclude: {\n type: 'array',\n items: { type: 'string' },\n description: 'List of directory names to completely ignore.',\n },\n show_files: {\n type: 'boolean',\n description: 'Whether to show individual files (default true).',\n },\n show_dirs: {\n type: 'boolean',\n description: 'Whether to show directories (default true).',\n },\n show_hidden: {\n type: 'boolean',\n description: 'Show hidden files starting with . (default: false)',\n },\n },\n },\n async execute(input, ctx, opts) {\n let final: TreeOutput | undefined;\n const executeStream = treeTool.executeStream;\n if (!executeStream) throw new Error('treeTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('tree: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx): AsyncGenerator<ToolStreamEvent<TreeOutput>> {\n const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\n const maxDepth = input.depth ?? 3;\n const showFiles = input.show_files ?? true;\n const showDirs = input.show_dirs ?? true;\n const showHidden = input.show_hidden ?? false;\n const exclude = new Set([...DEFAULT_IGNORE, ...(input.exclude ?? [])]);\n const filterGlob = input.glob;\n\n const lines: string[] = [basePath];\n const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };\n\n // Walker pushes progress into an async queue; the generator drains it.\n const queue: ToolProgressEvent[] = [];\n const FLUSH_EVERY = 200; // emit metric every 200 entries seen\n let lastEmittedTotal = 0;\n\n const tickProgress = () => {\n const seen = totals.totalFiles.value + totals.totalDirs.value;\n if (seen - lastEmittedTotal >= FLUSH_EVERY) {\n queue.push({\n type: 'metric',\n text: `${seen} entries`,\n data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value },\n });\n lastEmittedTotal = seen;\n }\n };\n\n const walkPromise = walkDir(basePath, 0, {\n maxDepth,\n exclude,\n showFiles,\n showDirs,\n showHidden,\n filterGlob,\n lines,\n prefix: '',\n isLast: true,\n totalFiles: totals.totalFiles,\n totalDirs: totals.totalDirs,\n onProgress: tickProgress,\n });\n\n // Race the walk against periodic flushes \u2014 yield metrics while it runs.\n let walkDone = false;\n walkPromise.finally(() => {\n walkDone = true;\n });\n\n while (!walkDone || queue.length > 0) {\n if (queue.length > 0) {\n yield expectDefined(queue.shift());\n } else {\n // Race the walk completion against a short tick so we don't busy-\n // spin while the producer fills the queue. Previously the\n // setTimeout was never cleared when walkPromise won \u2014 one stray\n // timer per drain iteration accumulated on the event loop.\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n const poll = new Promise<void>((r) => {\n pollTimer = setTimeout(r, 50);\n });\n try {\n await Promise.race([walkPromise, poll]).catch(() => undefined);\n } finally {\n if (pollTimer) clearTimeout(pollTimer);\n }\n }\n }\n await walkPromise; // surface any error\n\n yield {\n type: 'final',\n output: {\n tree: lines.join('\\n'),\n total_files: totals.totalFiles.value,\n total_dirs: totals.totalDirs.value,\n truncated: false,\n path: basePath,\n },\n };\n },\n};\n\ninterface WalkOptions {\n maxDepth: number;\n exclude: Set<string>;\n showFiles: boolean;\n showDirs: boolean;\n showHidden: boolean;\n filterGlob?: string | undefined;\n lines: string[];\n prefix: string;\n isLast: boolean;\n totalFiles: { value: number };\n totalDirs: { value: number };\n onProgress?: (() => void) | undefined;\n}\n\nasync function walkDir(dir: string, depth: number, opts: WalkOptions): Promise<void> {\n const entries = await fs\n .readdir(dir, { withFileTypes: true })\n .catch(() => [] as import('node:fs').Dirent[]);\n\n const filtered = entries.filter((e) => {\n if (!opts.showHidden && e.name.startsWith('.')) return false;\n if (opts.exclude.has(e.name)) return false;\n return true;\n });\n\n if (depth > 0) {\n const dirCount = filtered.filter((e) => e.isDirectory()).length;\n const fileCount = filtered.filter((e) => e.isFile()).length;\n opts.totalDirs.value += dirCount;\n opts.totalFiles.value += fileCount;\n opts.onProgress?.();\n }\n\n const items = filtered.sort((a, b) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n });\n\n for (let i = 0; i < items.length; i++) {\n const entry = items[i];\n /* v8 ignore next -- i is bounded by items.length, so entry is always defined; defensive. */\n if (!entry) continue;\n const isLast = i === items.length - 1;\n const connector = opts.isLast ? ' ' : '\u2502 ';\n const branch = isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';\n const displayName = entry.name + (entry.isDirectory() ? '/' : '');\n\n if (!opts.showDirs && entry.isDirectory()) continue;\n if (!opts.showFiles && entry.isFile()) continue;\n\n opts.lines.push(opts.prefix + branch + displayName);\n\n if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {\n const childPrefix = opts.prefix + connector;\n await walkDir(path.join(dir, entry.name), depth + 1, {\n ...opts,\n prefix: childPrefix,\n isLast,\n });\n }\n }\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["path"]
7
7
  }
@@ -1,4 +1,4 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  interface TypecheckInput {
3
3
  project?: string | undefined;
4
4
  cwd?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"typecheck.d.ts","sourceRoot":"","sources":["../src/typecheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AAK9D,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CA6G/D,CAAC"}
1
+ {"version":3,"file":"typecheck.d.ts","sourceRoot":"","sources":["../src/typecheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,wBAAwB,CAAC;AAKpE,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CA6G/D,CAAC"}
package/dist/typecheck.js CHANGED
@@ -4,17 +4,17 @@ import * as path9 from "node:path";
4
4
  // src/_spawn-stream.ts
5
5
  import { spawn as spawn2 } from "node:child_process";
6
6
  import {
7
- buildChildEnv,
8
7
  emitProcessCompleted,
9
8
  emitProcessOutput,
10
9
  emitProcessStarted
11
- } from "@wrongstack/core";
10
+ } from "@wrongstack/core/observability";
11
+ import { buildChildEnv } from "@wrongstack/core/utils";
12
12
 
13
13
  // src/_output-spool.ts
14
14
  import { createWriteStream, mkdirSync } from "node:fs";
15
15
  import * as fsp from "node:fs/promises";
16
16
  import * as path from "node:path";
17
- import { wstackGlobalRoot } from "@wrongstack/core";
17
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
18
18
  var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
19
19
  var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
20
20
  var sweepStarted = false;
@@ -314,7 +314,7 @@ var CircuitBreaker = class {
314
314
  };
315
315
 
316
316
  // src/_redact-command.ts
317
- import { expectDefined } from "@wrongstack/core";
317
+ import { expectDefined } from "@wrongstack/core/utils";
318
318
  var SENSITIVE_FLAG_PATTERNS = [
319
319
  // --flag=value or --flag "value" (value captured up to next space or comma)
320
320
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
@@ -408,7 +408,12 @@ var ProcessRegistryImpl = class {
408
408
  this.breaker.setEnabled(false);
409
409
  }
410
410
  register(info) {
411
- this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
411
+ this.processes.set(info.pid, {
412
+ ...info,
413
+ killed: false,
414
+ protected: info.protected ?? false,
415
+ background: info.background ?? false
416
+ });
412
417
  }
413
418
  _isSafeSignalPid(pid) {
414
419
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
@@ -461,12 +466,21 @@ var ProcessRegistryImpl = class {
461
466
  }
462
467
  return n;
463
468
  }
469
+ /** Count of active jobs explicitly launched in background mode. */
470
+ get activeBackgroundCount() {
471
+ let n = 0;
472
+ for (const p of this.processes.values()) {
473
+ if (p.background && !p.killed) n++;
474
+ }
475
+ return n;
476
+ }
464
477
  /**
465
478
  * Combined stats for observability — used by /ps and the TUI status bar.
466
479
  */
467
480
  stats() {
468
481
  return {
469
482
  activeCount: this.activeCount,
483
+ backgroundCount: this.activeBackgroundCount,
470
484
  totalCount: this.processes.size,
471
485
  breaker: this.breaker.snapshot()
472
486
  };
@@ -567,7 +581,7 @@ var ProcessRegistryImpl = class {
567
581
  this.autoKillTimer = setTimeout(() => {
568
582
  this.autoKillTimer = null;
569
583
  this.autoKillArmedAt = null;
570
- this.killAll({ force: false });
584
+ this.killAll({ force: false, preserveBackground: true });
571
585
  this.breaker.forceReset();
572
586
  this._emitBreakerCountdown();
573
587
  }, this.autoKillResetMs);
@@ -607,6 +621,7 @@ var ProcessRegistryImpl = class {
607
621
  if (!p) return false;
608
622
  if (p.killed) return true;
609
623
  if (p.protected) return false;
624
+ if (opts.preserveBackground && p.background) return false;
610
625
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
611
626
  const isWin2 = os.platform() === "win32";
612
627
  if (isWin2) {
@@ -958,7 +973,7 @@ async function* spawnStream(opts) {
958
973
 
959
974
  // src/_util.ts
960
975
  import * as path3 from "node:path";
961
- import * as Core from "@wrongstack/core";
976
+ import * as Core from "@wrongstack/core/utils";
962
977
  function resolvePath(input, ctx) {
963
978
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
964
979
  }
@@ -1144,7 +1159,20 @@ function pythonProfile() {
1144
1159
  ]),
1145
1160
  ignoredDirectories: IGNORES,
1146
1161
  packageManagers: Object.freeze(["pip", "poetry", "pipenv", "uv"]),
1147
- executables: Object.freeze(["python", "python3", "ruff", "black", "pytest", "mypy", "pyright"]),
1162
+ executables: Object.freeze([
1163
+ "python",
1164
+ "python3",
1165
+ "pip",
1166
+ "pip3",
1167
+ "poetry",
1168
+ "pipenv",
1169
+ "uv",
1170
+ "ruff",
1171
+ "black",
1172
+ "pytest",
1173
+ "mypy",
1174
+ "pyright"
1175
+ ]),
1148
1176
  operations: Object.freeze({
1149
1177
  syntax: async (ctx) => {
1150
1178
  const py = ctx.options.target ? "python3" : "python3";
@@ -1204,6 +1232,14 @@ function pythonProfile() {
1204
1232
  network: true
1205
1233
  });
1206
1234
  },
1235
+ "package-remove": async (ctx) => {
1236
+ const names = packageNames(ctx);
1237
+ return names.length === 0 ? unavailable(ctx, "package-remove", "At least one package name is required.") : processPlan(ctx, "package-remove", "pip", ["uninstall", "--yes", ...names], {
1238
+ parser: "package-text",
1239
+ reason: "Remove validated Python packages.",
1240
+ mutating: true
1241
+ });
1242
+ },
1207
1243
  "package-audit": async (ctx) => processPlan(ctx, "package-audit", "pip", ["audit"], {
1208
1244
  parser: "pip-audit",
1209
1245
  reason: "Audit Python dependencies for vulnerabilities.",
@@ -1811,6 +1847,19 @@ function typescriptProfile() {
1811
1847
  network: true
1812
1848
  });
1813
1849
  },
1850
+ "package-remove": async (ctx) => {
1851
+ const names = packageNames(ctx);
1852
+ if (names.length === 0)
1853
+ return unavailable(ctx, "package-remove", "At least one package name is required.");
1854
+ const manager = ctx.workspace.packageManager ?? "npm";
1855
+ const args = manager === "npm" ? ["uninstall", "--ignore-scripts", ...names] : ["remove", "--ignore-scripts", ...names];
1856
+ return processPlan(ctx, "package-remove", manager, args, {
1857
+ parser: "package-text",
1858
+ reason: `Remove validated packages with ${manager} and lifecycle scripts disabled.`,
1859
+ mutating: true,
1860
+ network: true
1861
+ });
1862
+ },
1814
1863
  "package-audit": async (ctx) => {
1815
1864
  const manager = ctx.workspace.packageManager ?? "npm";
1816
1865
  return processPlan(ctx, "package-audit", manager, ["audit", "--json"], {
@@ -2246,13 +2295,28 @@ var csharpProfile = {
2246
2295
  }),
2247
2296
  "package-add": async (ctx) => {
2248
2297
  const names = packageNames(ctx);
2249
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : processPlan(ctx, "package-add", "dotnet", ["add", "package", ...names], {
2250
- parser: "dotnet-package",
2251
- reason: "Add validated NuGet packages.",
2252
- mutating: true,
2253
- network: true,
2254
- executesProjectCode: true
2255
- });
2298
+ const [spec] = names;
2299
+ const versionAt = spec?.lastIndexOf("@") ?? -1;
2300
+ const packageName = versionAt > 0 ? spec?.slice(0, versionAt) : spec;
2301
+ const packageVersion = versionAt > 0 ? spec?.slice(versionAt + 1) : void 0;
2302
+ return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : names.length > 1 ? unavailable(ctx, "package-add", "NuGet package changes run one package at a time.") : processPlan(
2303
+ ctx,
2304
+ "package-add",
2305
+ "dotnet",
2306
+ [
2307
+ "add",
2308
+ "package",
2309
+ packageName ?? "",
2310
+ ...packageVersion ? ["--version", packageVersion] : []
2311
+ ],
2312
+ {
2313
+ parser: "dotnet-package",
2314
+ reason: "Add validated NuGet packages.",
2315
+ mutating: true,
2316
+ network: true,
2317
+ executesProjectCode: true
2318
+ }
2319
+ );
2256
2320
  },
2257
2321
  "package-remove": async (ctx) => {
2258
2322
  const names = packageNames(ctx);
@@ -2906,6 +2970,7 @@ var MAX_ARGUMENT_LENGTH = 4096;
2906
2970
  var PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
2907
2971
  var COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
2908
2972
  var GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
2973
+ var PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
2909
2974
  async function planLanguageOperation(options) {
2910
2975
  validateOperationOptions(options.operation, options.operationOptions?.packages);
2911
2976
  const detection = await detectLanguageWorkspaces(options);
@@ -3089,7 +3154,7 @@ function validateOperationOptions(operation, packages) {
3089
3154
  if (value.startsWith(".") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.includes("://")) {
3090
3155
  throw new Error(`Package paths and URLs are not supported: "${value}"`);
3091
3156
  }
3092
- if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value)) {
3157
+ if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value) && !PYTHON_PACKAGE_RE.test(value)) {
3093
3158
  throw new Error(`Invalid package identifier "${value}"`);
3094
3159
  }
3095
3160
  }