@studio-foundation/runner 0.3.0-beta.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 (268) hide show
  1. package/ARCHITECTURE.md +53 -0
  2. package/LICENSE +663 -0
  3. package/README.md +125 -0
  4. package/configs/agents/analyst.agent.yaml +31 -0
  5. package/configs/agents/code-generator.agent.yaml +31 -0
  6. package/configs/agents/generic.agent.yaml +23 -0
  7. package/dist/__tests__/script-executor.test.d.ts +2 -0
  8. package/dist/__tests__/script-executor.test.d.ts.map +1 -0
  9. package/dist/__tests__/script-executor.test.js +173 -0
  10. package/dist/__tests__/script-executor.test.js.map +1 -0
  11. package/dist/index.d.ts +38 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +76 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/integrations/integration-loader.d.ts +20 -0
  16. package/dist/integrations/integration-loader.d.ts.map +1 -0
  17. package/dist/integrations/integration-loader.js +71 -0
  18. package/dist/integrations/integration-loader.js.map +1 -0
  19. package/dist/integrations/integration-loader.test.d.ts +2 -0
  20. package/dist/integrations/integration-loader.test.d.ts.map +1 -0
  21. package/dist/integrations/integration-loader.test.js +77 -0
  22. package/dist/integrations/integration-loader.test.js.map +1 -0
  23. package/dist/middleware/anonymization.d.ts +22 -0
  24. package/dist/middleware/anonymization.d.ts.map +1 -0
  25. package/dist/middleware/anonymization.js +37 -0
  26. package/dist/middleware/anonymization.js.map +1 -0
  27. package/dist/plugins/index.d.ts +5 -0
  28. package/dist/plugins/index.d.ts.map +1 -0
  29. package/dist/plugins/index.js +10 -0
  30. package/dist/plugins/index.js.map +1 -0
  31. package/dist/plugins/mcp-client.d.ts +19 -0
  32. package/dist/plugins/mcp-client.d.ts.map +1 -0
  33. package/dist/plugins/mcp-client.js +126 -0
  34. package/dist/plugins/mcp-client.js.map +1 -0
  35. package/dist/plugins/mcp-client.test.d.ts +2 -0
  36. package/dist/plugins/mcp-client.test.d.ts.map +1 -0
  37. package/dist/plugins/mcp-client.test.js +160 -0
  38. package/dist/plugins/mcp-client.test.js.map +1 -0
  39. package/dist/plugins/oauth-provider.d.ts +27 -0
  40. package/dist/plugins/oauth-provider.d.ts.map +1 -0
  41. package/dist/plugins/oauth-provider.js +140 -0
  42. package/dist/plugins/oauth-provider.js.map +1 -0
  43. package/dist/plugins/oauth-provider.test.d.ts +2 -0
  44. package/dist/plugins/oauth-provider.test.d.ts.map +1 -0
  45. package/dist/plugins/oauth-provider.test.js +144 -0
  46. package/dist/plugins/oauth-provider.test.js.map +1 -0
  47. package/dist/plugins/plugin-loader.d.ts +29 -0
  48. package/dist/plugins/plugin-loader.d.ts.map +1 -0
  49. package/dist/plugins/plugin-loader.js +71 -0
  50. package/dist/plugins/plugin-loader.js.map +1 -0
  51. package/dist/plugins/plugin-loader.test.d.ts +2 -0
  52. package/dist/plugins/plugin-loader.test.d.ts.map +1 -0
  53. package/dist/plugins/plugin-loader.test.js +87 -0
  54. package/dist/plugins/plugin-loader.test.js.map +1 -0
  55. package/dist/prompt-builder.d.ts +47 -0
  56. package/dist/prompt-builder.d.ts.map +1 -0
  57. package/dist/prompt-builder.js +263 -0
  58. package/dist/prompt-builder.js.map +1 -0
  59. package/dist/prompt-builder.test.d.ts +2 -0
  60. package/dist/prompt-builder.test.d.ts.map +1 -0
  61. package/dist/prompt-builder.test.js +154 -0
  62. package/dist/prompt-builder.test.js.map +1 -0
  63. package/dist/providers/anthropic.d.ts +14 -0
  64. package/dist/providers/anthropic.d.ts.map +1 -0
  65. package/dist/providers/anthropic.js +118 -0
  66. package/dist/providers/anthropic.js.map +1 -0
  67. package/dist/providers/anthropic.test.d.ts +2 -0
  68. package/dist/providers/anthropic.test.d.ts.map +1 -0
  69. package/dist/providers/anthropic.test.js +76 -0
  70. package/dist/providers/anthropic.test.js.map +1 -0
  71. package/dist/providers/mock.d.ts +17 -0
  72. package/dist/providers/mock.d.ts.map +1 -0
  73. package/dist/providers/mock.js +39 -0
  74. package/dist/providers/mock.js.map +1 -0
  75. package/dist/providers/ollama.d.ts +18 -0
  76. package/dist/providers/ollama.d.ts.map +1 -0
  77. package/dist/providers/ollama.js +155 -0
  78. package/dist/providers/ollama.js.map +1 -0
  79. package/dist/providers/ollama.test.d.ts +2 -0
  80. package/dist/providers/ollama.test.d.ts.map +1 -0
  81. package/dist/providers/ollama.test.js +123 -0
  82. package/dist/providers/ollama.test.js.map +1 -0
  83. package/dist/providers/openai-responses.d.ts +18 -0
  84. package/dist/providers/openai-responses.d.ts.map +1 -0
  85. package/dist/providers/openai-responses.js +175 -0
  86. package/dist/providers/openai-responses.js.map +1 -0
  87. package/dist/providers/openai.d.ts +16 -0
  88. package/dist/providers/openai.d.ts.map +1 -0
  89. package/dist/providers/openai.js +132 -0
  90. package/dist/providers/openai.js.map +1 -0
  91. package/dist/providers/openai.test.d.ts +2 -0
  92. package/dist/providers/openai.test.d.ts.map +1 -0
  93. package/dist/providers/openai.test.js +51 -0
  94. package/dist/providers/openai.test.js.map +1 -0
  95. package/dist/providers/provider.d.ts +42 -0
  96. package/dist/providers/provider.d.ts.map +1 -0
  97. package/dist/providers/provider.js +10 -0
  98. package/dist/providers/provider.js.map +1 -0
  99. package/dist/providers/registry.d.ts +42 -0
  100. package/dist/providers/registry.d.ts.map +1 -0
  101. package/dist/providers/registry.js +65 -0
  102. package/dist/providers/registry.js.map +1 -0
  103. package/dist/runner.d.ts +46 -0
  104. package/dist/runner.d.ts.map +1 -0
  105. package/dist/runner.js +324 -0
  106. package/dist/runner.js.map +1 -0
  107. package/dist/runner.test.d.ts +2 -0
  108. package/dist/runner.test.d.ts.map +1 -0
  109. package/dist/runner.test.js +280 -0
  110. package/dist/runner.test.js.map +1 -0
  111. package/dist/script-executor.d.ts +11 -0
  112. package/dist/script-executor.d.ts.map +1 -0
  113. package/dist/script-executor.js +85 -0
  114. package/dist/script-executor.js.map +1 -0
  115. package/dist/tools/builtin/git.d.ts +6 -0
  116. package/dist/tools/builtin/git.d.ts.map +1 -0
  117. package/dist/tools/builtin/git.js +294 -0
  118. package/dist/tools/builtin/git.js.map +1 -0
  119. package/dist/tools/builtin/patch.d.ts +6 -0
  120. package/dist/tools/builtin/patch.d.ts.map +1 -0
  121. package/dist/tools/builtin/patch.js +238 -0
  122. package/dist/tools/builtin/patch.js.map +1 -0
  123. package/dist/tools/builtin/repo-manager.d.ts +6 -0
  124. package/dist/tools/builtin/repo-manager.d.ts.map +1 -0
  125. package/dist/tools/builtin/repo-manager.js +171 -0
  126. package/dist/tools/builtin/repo-manager.js.map +1 -0
  127. package/dist/tools/builtin/search.d.ts +6 -0
  128. package/dist/tools/builtin/search.d.ts.map +1 -0
  129. package/dist/tools/builtin/search.js +104 -0
  130. package/dist/tools/builtin/search.js.map +1 -0
  131. package/dist/tools/builtin/shell.d.ts +6 -0
  132. package/dist/tools/builtin/shell.d.ts.map +1 -0
  133. package/dist/tools/builtin/shell.js +78 -0
  134. package/dist/tools/builtin/shell.js.map +1 -0
  135. package/dist/tools/builtin/studio-run.d.ts +12 -0
  136. package/dist/tools/builtin/studio-run.d.ts.map +1 -0
  137. package/dist/tools/builtin/studio-run.js +61 -0
  138. package/dist/tools/builtin/studio-run.js.map +1 -0
  139. package/dist/tools/builtin/web-search.d.ts +7 -0
  140. package/dist/tools/builtin/web-search.d.ts.map +1 -0
  141. package/dist/tools/builtin/web-search.js +94 -0
  142. package/dist/tools/builtin/web-search.js.map +1 -0
  143. package/dist/tools/builtin/web-search.test.d.ts +5 -0
  144. package/dist/tools/builtin/web-search.test.d.ts.map +1 -0
  145. package/dist/tools/builtin/web-search.test.js +90 -0
  146. package/dist/tools/builtin/web-search.test.js.map +1 -0
  147. package/dist/tools/errors.d.ts +4 -0
  148. package/dist/tools/errors.d.ts.map +1 -0
  149. package/dist/tools/errors.js +11 -0
  150. package/dist/tools/errors.js.map +1 -0
  151. package/dist/tools/errors.test.d.ts +2 -0
  152. package/dist/tools/errors.test.d.ts.map +1 -0
  153. package/dist/tools/errors.test.js +14 -0
  154. package/dist/tools/errors.test.js.map +1 -0
  155. package/dist/tools/plugin-loader.d.ts +31 -0
  156. package/dist/tools/plugin-loader.d.ts.map +1 -0
  157. package/dist/tools/plugin-loader.js +183 -0
  158. package/dist/tools/plugin-loader.js.map +1 -0
  159. package/dist/tools/plugin-loader.test.d.ts +2 -0
  160. package/dist/tools/plugin-loader.test.d.ts.map +1 -0
  161. package/dist/tools/plugin-loader.test.js +116 -0
  162. package/dist/tools/plugin-loader.test.js.map +1 -0
  163. package/dist/tools/skills/skill-loader.d.ts +23 -0
  164. package/dist/tools/skills/skill-loader.d.ts.map +1 -0
  165. package/dist/tools/skills/skill-loader.js +58 -0
  166. package/dist/tools/skills/skill-loader.js.map +1 -0
  167. package/dist/tools/skills/skill-loader.test.d.ts +2 -0
  168. package/dist/tools/skills/skill-loader.test.d.ts.map +1 -0
  169. package/dist/tools/skills/skill-loader.test.js +92 -0
  170. package/dist/tools/skills/skill-loader.test.js.map +1 -0
  171. package/dist/tools/tool-executor.d.ts +12 -0
  172. package/dist/tools/tool-executor.d.ts.map +1 -0
  173. package/dist/tools/tool-executor.js +76 -0
  174. package/dist/tools/tool-executor.js.map +1 -0
  175. package/dist/tools/tool-executor.test.d.ts +2 -0
  176. package/dist/tools/tool-executor.test.d.ts.map +1 -0
  177. package/dist/tools/tool-executor.test.js +83 -0
  178. package/dist/tools/tool-executor.test.js.map +1 -0
  179. package/dist/tools/tool-registry.d.ts +65 -0
  180. package/dist/tools/tool-registry.d.ts.map +1 -0
  181. package/dist/tools/tool-registry.js +112 -0
  182. package/dist/tools/tool-registry.js.map +1 -0
  183. package/dist/tools/yaml-executor.d.ts +22 -0
  184. package/dist/tools/yaml-executor.d.ts.map +1 -0
  185. package/dist/tools/yaml-executor.js +80 -0
  186. package/dist/tools/yaml-executor.js.map +1 -0
  187. package/dist/utils/race-signal.d.ts +6 -0
  188. package/dist/utils/race-signal.d.ts.map +1 -0
  189. package/dist/utils/race-signal.js +21 -0
  190. package/dist/utils/race-signal.js.map +1 -0
  191. package/dist/utils/race-signal.test.d.ts +2 -0
  192. package/dist/utils/race-signal.test.d.ts.map +1 -0
  193. package/dist/utils/race-signal.test.js +48 -0
  194. package/dist/utils/race-signal.test.js.map +1 -0
  195. package/package.json +36 -0
  196. package/src/__tests__/script-executor.test.ts +180 -0
  197. package/src/index.ts +68 -0
  198. package/src/integrations/integration-loader.test.ts +88 -0
  199. package/src/integrations/integration-loader.ts +68 -0
  200. package/src/middleware/anonymization.ts +38 -0
  201. package/src/plugins/index.ts +4 -0
  202. package/src/plugins/mcp-client.test.ts +148 -0
  203. package/src/plugins/mcp-client.ts +128 -0
  204. package/src/plugins/oauth-provider.test.ts +167 -0
  205. package/src/plugins/oauth-provider.ts +175 -0
  206. package/src/plugins/plugin-loader.test.ts +114 -0
  207. package/src/plugins/plugin-loader.ts +90 -0
  208. package/src/prompt-builder.test.ts +167 -0
  209. package/src/prompt-builder.ts +332 -0
  210. package/src/providers/anthropic.test.ts +101 -0
  211. package/src/providers/anthropic.ts +135 -0
  212. package/src/providers/mock.ts +57 -0
  213. package/src/providers/ollama.test.ts +166 -0
  214. package/src/providers/ollama.ts +152 -0
  215. package/src/providers/openai-responses.ts +212 -0
  216. package/src/providers/openai.test.ts +67 -0
  217. package/src/providers/openai.ts +139 -0
  218. package/src/providers/provider.ts +54 -0
  219. package/src/providers/registry.ts +77 -0
  220. package/src/runner.test.ts +343 -0
  221. package/src/runner.ts +396 -0
  222. package/src/script-executor.ts +107 -0
  223. package/src/tools/builtin/git.ts +311 -0
  224. package/src/tools/builtin/patch.ts +257 -0
  225. package/src/tools/builtin/repo-manager.ts +142 -0
  226. package/src/tools/builtin/search.ts +108 -0
  227. package/src/tools/builtin/shell.ts +82 -0
  228. package/src/tools/builtin/studio-run.ts +73 -0
  229. package/src/tools/builtin/web-search.test.ts +122 -0
  230. package/src/tools/builtin/web-search.ts +101 -0
  231. package/src/tools/errors.test.ts +12 -0
  232. package/src/tools/errors.ts +6 -0
  233. package/src/tools/plugin-loader.test.ts +130 -0
  234. package/src/tools/plugin-loader.ts +203 -0
  235. package/src/tools/skills/README.md +49 -0
  236. package/src/tools/skills/skill-loader.test.ts +106 -0
  237. package/src/tools/skills/skill-loader.ts +62 -0
  238. package/src/tools/tool-executor.test.ts +88 -0
  239. package/src/tools/tool-executor.ts +84 -0
  240. package/src/tools/tool-registry.ts +130 -0
  241. package/src/tools/yaml-executor.ts +120 -0
  242. package/src/utils/race-signal.test.ts +50 -0
  243. package/src/utils/race-signal.ts +17 -0
  244. package/templates/integrations/linear.integration.yaml +35 -0
  245. package/templates/integrations/slack.integration.yaml +22 -0
  246. package/templates/integrations/webhook.integration.yaml +17 -0
  247. package/templates/tools/git.tool.yaml +80 -0
  248. package/templates/tools/repo-manager.tool.yaml +64 -0
  249. package/templates/tools/search.tool.yaml +22 -0
  250. package/templates/tools/shell.tool.yaml +19 -0
  251. package/templates/tools/web-search.tool.yaml +24 -0
  252. package/tests/anonymization-middleware.test.ts +61 -0
  253. package/tests/anthropic.test.ts +87 -0
  254. package/tests/apply-patch.test.ts +355 -0
  255. package/tests/fixtures/tools/test-builtin.tool.yaml +14 -0
  256. package/tests/fixtures/tools/test-shell.tool.yaml +19 -0
  257. package/tests/mock-provider.test.ts +104 -0
  258. package/tests/openai.test.ts +72 -0
  259. package/tests/plugin-loader.test.ts +54 -0
  260. package/tests/prompt-builder.test.ts +468 -0
  261. package/tests/runner-anonymization.test.ts +89 -0
  262. package/tests/runner.test.ts +885 -0
  263. package/tests/studio-run.test.ts +94 -0
  264. package/tests/tool-executor.test.ts +115 -0
  265. package/tests/tool-registry.test.ts +84 -0
  266. package/tests/yaml-executor.test.ts +76 -0
  267. package/tsconfig.json +20 -0
  268. package/vitest.config.ts +7 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"race-signal.js","sourceRoot":"","sources":["../../src/utils/race-signal.ts"],"names":[],"mappings":";;AAIA,gCAYC;AAhBD;;;GAGG;AACH,SAAgB,UAAU,CAAI,OAAmB,EAAE,MAAoB;IACrE,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;IAErF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QACxE,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAE1D,OAAO;aACJ,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aAC1E,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=race-signal.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"race-signal.test.d.ts","sourceRoot":"","sources":["../../src/utils/race-signal.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const race_signal_js_1 = require("./race-signal.js");
5
+ (0, vitest_1.describe)('raceSignal', () => {
6
+ (0, vitest_1.it)('resolves normally when no signal provided', async () => {
7
+ const result = await (0, race_signal_js_1.raceSignal)(Promise.resolve(42));
8
+ (0, vitest_1.expect)(result).toBe(42);
9
+ });
10
+ (0, vitest_1.it)('resolves normally when signal is not aborted', async () => {
11
+ const controller = new AbortController();
12
+ const result = await (0, race_signal_js_1.raceSignal)(Promise.resolve('hello'), controller.signal);
13
+ (0, vitest_1.expect)(result).toBe('hello');
14
+ });
15
+ (0, vitest_1.it)('rejects immediately if signal is already aborted', async () => {
16
+ const controller = new AbortController();
17
+ controller.abort();
18
+ await (0, vitest_1.expect)((0, race_signal_js_1.raceSignal)(new Promise(() => { }), controller.signal))
19
+ .rejects.toThrow('Aborted');
20
+ });
21
+ (0, vitest_1.it)('rejects when signal fires after creation', async () => {
22
+ const controller = new AbortController();
23
+ // A promise that never resolves on its own
24
+ const hanging = new Promise(() => { });
25
+ const raced = (0, race_signal_js_1.raceSignal)(hanging, controller.signal);
26
+ controller.abort();
27
+ await (0, vitest_1.expect)(raced).rejects.toThrow('Aborted');
28
+ });
29
+ (0, vitest_1.it)('resolves if promise resolves before signal fires', async () => {
30
+ const controller = new AbortController();
31
+ const result = await (0, race_signal_js_1.raceSignal)(Promise.resolve(99), controller.signal);
32
+ // Abort after the fact — should not cause rejection
33
+ controller.abort();
34
+ (0, vitest_1.expect)(result).toBe(99);
35
+ });
36
+ (0, vitest_1.it)('thrown error name is AbortError', async () => {
37
+ const controller = new AbortController();
38
+ controller.abort();
39
+ try {
40
+ await (0, race_signal_js_1.raceSignal)(new Promise(() => { }), controller.signal);
41
+ }
42
+ catch (e) {
43
+ (0, vitest_1.expect)(e).toBeInstanceOf(DOMException);
44
+ (0, vitest_1.expect)(e.name).toBe('AbortError');
45
+ }
46
+ });
47
+ });
48
+ //# sourceMappingURL=race-signal.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"race-signal.test.js","sourceRoot":"","sources":["../../src/utils/race-signal.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,qDAA8C;AAE9C,IAAA,iBAAQ,EAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,IAAA,WAAE,EAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,MAAM,MAAM,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QACrD,IAAA,eAAM,EAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7E,IAAA,eAAM,EAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,IAAA,eAAM,EAAC,IAAA,2BAAU,EAAC,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;aAC/D,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,2CAA2C;QAC3C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAA,2BAAU,EAAC,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACrD,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,MAAM,IAAA,eAAM,EAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAA,2BAAU,EAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QACxE,oDAAoD;QACpD,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,IAAA,eAAM,EAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,IAAA,2BAAU,EAAC,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAA,eAAM,EAAC,CAAC,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;YACvC,IAAA,eAAM,EAAE,CAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@studio-foundation/runner",
3
+ "version": "0.3.0-beta.1",
4
+ "description": "Multi-provider LLM agent runner with tool execution",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "llm",
9
+ "agent",
10
+ "tools",
11
+ "openai",
12
+ "anthropic"
13
+ ],
14
+ "author": "Ariane Guay",
15
+ "license": "AGPL-3.0-only",
16
+ "dependencies": {
17
+ "@anthropic-ai/sdk": "^0.33.1",
18
+ "@modelcontextprotocol/sdk": "^1.26.0",
19
+ "js-yaml": "^4.1.0",
20
+ "openai": "^4.77.0",
21
+ "@studio-foundation/anonymizer": "0.3.0-beta.1",
22
+ "@studio-foundation/contracts": "0.3.0-beta.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/js-yaml": "^4.0.9",
26
+ "@types/node": "^25.2.3",
27
+ "typescript": "^5.7.2",
28
+ "vitest": "^4.0.18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "clean": "rm -rf dist"
35
+ }
36
+ }
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as cp from 'node:child_process';
3
+ import * as fs from 'node:fs';
4
+ import { EventEmitter } from 'node:events';
5
+
6
+ // Mock child_process and fs before importing the module under test
7
+ vi.mock('node:child_process');
8
+ vi.mock('node:fs');
9
+
10
+ import { runScript } from '../script-executor.js';
11
+ import type { AgentContext } from '../prompt-builder.js';
12
+
13
+ function makeContext(overrides?: Partial<AgentContext>): AgentContext {
14
+ return {
15
+ input: 'test input',
16
+ ...overrides,
17
+ } as AgentContext;
18
+ }
19
+
20
+ function makeSpawnMock(opts: {
21
+ stdout?: string;
22
+ stderr?: string;
23
+ exitCode?: number;
24
+ errorEvent?: Error;
25
+ }) {
26
+ const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
27
+ (proc as any).stdout = new EventEmitter();
28
+ (proc as any).stderr = new EventEmitter();
29
+ (proc as any).stdin = { write: vi.fn(), end: vi.fn() };
30
+ (proc as any).kill = vi.fn();
31
+
32
+ vi.mocked(cp.spawn).mockReturnValue(proc as any);
33
+
34
+ // Simulate async process lifecycle
35
+ setTimeout(() => {
36
+ if (opts.errorEvent) {
37
+ proc.emit('error', opts.errorEvent);
38
+ return;
39
+ }
40
+ if (opts.stdout) (proc as any).stdout.emit('data', Buffer.from(opts.stdout));
41
+ if (opts.stderr) (proc as any).stderr.emit('data', Buffer.from(opts.stderr));
42
+ proc.emit('close', opts.exitCode ?? 0);
43
+ }, 10);
44
+
45
+ return proc;
46
+ }
47
+
48
+ beforeEach(() => {
49
+ vi.mocked(fs.existsSync).mockReturnValue(false);
50
+ vi.clearAllMocks();
51
+ });
52
+
53
+ describe('runScript', () => {
54
+ it('parses stdout JSON and returns output on exit 0', async () => {
55
+ const output = { result: 'ok', count: 42 };
56
+ makeSpawnMock({ stdout: JSON.stringify(output), exitCode: 0 });
57
+
58
+ const result = await runScript({
59
+ scriptPath: 'scripts/parse.py',
60
+ runtime: 'python',
61
+ context: makeContext(),
62
+ });
63
+
64
+ expect(result.output).toEqual(output);
65
+ expect(result.tool_calls).toEqual([]);
66
+ expect(result.tool_calls_count).toBe(0);
67
+ expect(result.error).toBeUndefined();
68
+ expect(result.duration_ms).toBeGreaterThanOrEqual(0);
69
+ });
70
+
71
+ it('sets error when script exits with non-zero code', async () => {
72
+ makeSpawnMock({ stdout: '', stderr: 'FileNotFoundError', exitCode: 1 });
73
+
74
+ const result = await runScript({
75
+ scriptPath: 'scripts/parse.py',
76
+ runtime: 'python',
77
+ context: makeContext(),
78
+ });
79
+
80
+ expect(result.output).toBeNull();
81
+ expect(result.error).toMatch(/exited with code 1/);
82
+ expect(result.error).toMatch(/FileNotFoundError/);
83
+ });
84
+
85
+ it('sets error when stdout is not valid JSON', async () => {
86
+ makeSpawnMock({ stdout: 'not json at all', exitCode: 0 });
87
+
88
+ const result = await runScript({
89
+ scriptPath: 'scripts/parse.py',
90
+ runtime: 'python',
91
+ context: makeContext(),
92
+ });
93
+
94
+ expect(result.output).toBeNull();
95
+ expect(result.error).toMatch(/not valid JSON/);
96
+ expect(result.error).toMatch(/not json at all/);
97
+ });
98
+
99
+ it('sets error on process spawn error', async () => {
100
+ const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
101
+ (proc as any).stdout = new EventEmitter();
102
+ (proc as any).stderr = new EventEmitter();
103
+ (proc as any).stdin = { write: vi.fn(), end: vi.fn() };
104
+ vi.mocked(cp.spawn).mockReturnValue(proc as any);
105
+ setTimeout(() => proc.emit('error', new Error('ENOENT: python3 not found')), 10);
106
+
107
+ const result = await runScript({
108
+ scriptPath: 'scripts/parse.py',
109
+ runtime: 'python',
110
+ context: makeContext(),
111
+ });
112
+
113
+ expect(result.error).toMatch(/process error/);
114
+ expect(result.error).toMatch(/ENOENT/);
115
+ });
116
+
117
+ it('uses python3 command for python runtime', async () => {
118
+ makeSpawnMock({ stdout: '{}', exitCode: 0 });
119
+
120
+ await runScript({ scriptPath: 'scripts/parse.py', runtime: 'python', context: makeContext() });
121
+
122
+ expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith(
123
+ 'python3',
124
+ ['scripts/parse.py'],
125
+ expect.objectContaining({ stdio: ['pipe', 'pipe', 'pipe'] }),
126
+ );
127
+ });
128
+
129
+ it('uses node command for node runtime', async () => {
130
+ makeSpawnMock({ stdout: '{}', exitCode: 0 });
131
+
132
+ await runScript({ scriptPath: 'scripts/parse.js', runtime: 'node', context: makeContext() });
133
+
134
+ expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith('node', ['scripts/parse.js'], expect.anything());
135
+ });
136
+
137
+ it('uses sh command for shell runtime', async () => {
138
+ makeSpawnMock({ stdout: '{}', exitCode: 0 });
139
+
140
+ await runScript({ scriptPath: 'scripts/run.sh', runtime: 'shell', context: makeContext() });
141
+
142
+ expect(vi.mocked(cp.spawn)).toHaveBeenCalledWith('sh', ['scripts/run.sh'], expect.anything());
143
+ });
144
+
145
+ it('activates venv when venv/ directory exists', async () => {
146
+ vi.mocked(fs.existsSync).mockImplementation((p) => String(p).endsWith('venv'));
147
+ makeSpawnMock({ stdout: '{}', exitCode: 0 });
148
+
149
+ await runScript({
150
+ scriptPath: 'scripts/parse.py',
151
+ runtime: 'python',
152
+ context: makeContext(),
153
+ cwd: '/project',
154
+ });
155
+
156
+ const spawnCall = vi.mocked(cp.spawn).mock.calls[0];
157
+ const spawnEnv = (spawnCall[2] as any).env;
158
+ expect(spawnEnv.VIRTUAL_ENV).toBe('/project/venv');
159
+ expect(spawnEnv.PATH).toContain('/project/venv/bin');
160
+ });
161
+
162
+ it('writes context JSON to stdin', async () => {
163
+ const stdinMock = { write: vi.fn(), end: vi.fn() };
164
+ const proc = new EventEmitter() as ReturnType<typeof cp.spawn>;
165
+ (proc as any).stdout = new EventEmitter();
166
+ (proc as any).stderr = new EventEmitter();
167
+ (proc as any).stdin = stdinMock;
168
+ vi.mocked(cp.spawn).mockReturnValue(proc as any);
169
+ setTimeout(() => {
170
+ (proc as any).stdout.emit('data', Buffer.from('{"ok":true}'));
171
+ proc.emit('close', 0);
172
+ }, 10);
173
+
174
+ const ctx = makeContext({ additional_context: 'hello world' });
175
+ await runScript({ scriptPath: 'scripts/parse.py', runtime: 'python', context: ctx });
176
+
177
+ expect(stdinMock.write).toHaveBeenCalledWith(JSON.stringify(ctx));
178
+ expect(stdinMock.end).toHaveBeenCalled();
179
+ });
180
+ });
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @studio/runner - Multi-provider LLM agent runner with tool execution
3
+ */
4
+
5
+ // Main runner function
6
+ export { runAgent } from './runner.js';
7
+ export type { RunAgentConfig, AgentRunResult } from './runner.js';
8
+
9
+ // Script executor
10
+ export { runScript } from './script-executor.js';
11
+ export type { ScriptExecutorConfig } from './script-executor.js';
12
+
13
+ // Prompt builder
14
+ export { buildPrompt } from './prompt-builder.js';
15
+ export type {
16
+ TaskInput,
17
+ AgentContext,
18
+ GroupFeedbackContext,
19
+ ExecutionContext,
20
+ PromptBuildConfig
21
+ } from './prompt-builder.js';
22
+
23
+ // Providers
24
+ export type { Provider, AgentLoopProvider, AgentLoopResult } from './providers/provider.js';
25
+ export { isAgentLoopProvider } from './providers/provider.js';
26
+ export { OpenAIProvider } from './providers/openai.js';
27
+ export { OpenAIResponsesProvider } from './providers/openai-responses.js';
28
+ export { AnthropicProvider } from './providers/anthropic.js';
29
+ export { OllamaProvider } from './providers/ollama.js';
30
+ export { ProviderRegistry, createDefaultRegistry } from './providers/registry.js';
31
+ export { MockProvider } from './providers/mock.js';
32
+ export type { MockStageConfig } from './providers/mock.js';
33
+
34
+ // Tools
35
+ export { ToolRegistry } from './tools/tool-registry.js';
36
+ export { ToolExecutor } from './tools/tool-executor.js';
37
+ export type { Tool, ToolResult } from './tools/tool-registry.js';
38
+
39
+ // Builtin tools
40
+ export { createRepoManagerTools } from './tools/builtin/repo-manager.js';
41
+ export { createShellTools } from './tools/builtin/shell.js';
42
+ export { createSearchTools } from './tools/builtin/search.js';
43
+ export { createPatchTools } from './tools/builtin/patch.js';
44
+ export { createGitTools } from './tools/builtin/git.js';
45
+ export { createStudioRunTool, STUDIO_RUN_PROMPT_SNIPPET } from './tools/builtin/studio-run.js';
46
+ export { createWebSearchTools, WEB_SEARCH_PROMPT_SNIPPET } from './tools/builtin/web-search.js';
47
+
48
+ export { loadProjectTools, listAvailableToolTemplates, getBundledToolTemplate, BUILTIN_TOOL_NAMES } from './tools/plugin-loader.js';
49
+ export type { LoadedPlugin } from './tools/plugin-loader.js';
50
+
51
+ // Anonymization middleware
52
+ export { AnonymizationMiddleware } from './middleware/anonymization.js';
53
+ export type { AnonymizerOptions } from '@studio-foundation/anonymizer';
54
+
55
+ // Plugin system (Claude Code plugin compatibility)
56
+ export { loadPlugins, MCPClient, StudioOAuthProvider } from './plugins/index.js';
57
+ export type { PluginManifest, MCPServerDef, SkillContent } from './plugins/index.js';
58
+
59
+ // Skill loader
60
+ export { loadSkills, loadSkill, validateSkillManifest } from './tools/skills/skill-loader.js';
61
+ export type { SkillManifest } from './tools/skills/skill-loader.js';
62
+
63
+ // Integration plugin system
64
+ export {
65
+ getBundledIntegrationTemplate,
66
+ listAvailableIntegrationTemplates,
67
+ loadProjectIntegrations,
68
+ } from './integrations/integration-loader.js';
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import {
6
+ getBundledIntegrationTemplate,
7
+ listAvailableIntegrationTemplates,
8
+ loadProjectIntegrations,
9
+ } from './integration-loader.js';
10
+
11
+ let tmpDir: string;
12
+
13
+ beforeAll(async () => {
14
+ tmpDir = await mkdtemp(join(tmpdir(), 'studio-integration-loader-test-'));
15
+ });
16
+
17
+ afterAll(async () => {
18
+ await rm(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ describe('getBundledIntegrationTemplate', () => {
22
+ it('returns YAML content for a known bundled integration', async () => {
23
+ const content = await getBundledIntegrationTemplate('linear');
24
+ expect(content).not.toBeNull();
25
+ expect(content).toContain('name: linear');
26
+ });
27
+
28
+ it('returns null for unknown integration name', async () => {
29
+ const content = await getBundledIntegrationTemplate('doesnotexist');
30
+ expect(content).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe('listAvailableIntegrationTemplates', () => {
35
+ it('returns at least linear, slack, webhook', async () => {
36
+ const templates = await listAvailableIntegrationTemplates();
37
+ const names = templates.map(t => t.name);
38
+ expect(names).toContain('linear');
39
+ expect(names).toContain('slack');
40
+ expect(names).toContain('webhook');
41
+ });
42
+
43
+ it('each entry has name and description', async () => {
44
+ const templates = await listAvailableIntegrationTemplates();
45
+ for (const t of templates) {
46
+ expect(typeof t.name).toBe('string');
47
+ expect(typeof t.description).toBe('string');
48
+ }
49
+ });
50
+ });
51
+
52
+ describe('loadProjectIntegrations', () => {
53
+ it('returns empty array when directory does not exist', async () => {
54
+ const result = await loadProjectIntegrations('/nonexistent/path');
55
+ expect(result).toEqual([]);
56
+ });
57
+
58
+ it('loads valid .integration.yaml files', async () => {
59
+ const intDir = join(tmpDir, 'integrations');
60
+ await mkdir(intDir, { recursive: true });
61
+ await writeFile(join(intDir, 'test.integration.yaml'), `
62
+ name: test
63
+ version: 1
64
+ description: "Test integration"
65
+ config:
66
+ required:
67
+ - TEST_API_KEY
68
+ test:
69
+ type: http
70
+ endpoint: https://api.test.com/health
71
+ expect:
72
+ status: 200
73
+ `);
74
+ const result = await loadProjectIntegrations(intDir);
75
+ expect(result).toHaveLength(1);
76
+ expect(result[0]!.name).toBe('test');
77
+ expect(result[0]!.config?.required).toEqual(['TEST_API_KEY']);
78
+ });
79
+
80
+ it('ignores non-.integration.yaml files', async () => {
81
+ const intDir = join(tmpDir, 'integrations-mixed');
82
+ await mkdir(intDir, { recursive: true });
83
+ await writeFile(join(intDir, 'readme.txt'), 'hello');
84
+ await writeFile(join(intDir, 'other.yaml'), 'name: other\nversion: 1');
85
+ const result = await loadProjectIntegrations(intDir);
86
+ expect(result).toHaveLength(0);
87
+ });
88
+ });
@@ -0,0 +1,68 @@
1
+ // runner/src/integrations/integration-loader.ts
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import type { IntegrationPluginDef } from '@studio-foundation/contracts';
7
+
8
+ const BUNDLED_INTEGRATION_TEMPLATES_DIR = resolve(
9
+ __dirname,
10
+ '../../templates/integrations'
11
+ );
12
+
13
+ /**
14
+ * Return the raw YAML content of a bundled integration template by name.
15
+ * Returns null if the integration does not exist in the bundled registry.
16
+ */
17
+ export async function getBundledIntegrationTemplate(name: string): Promise<string | null> {
18
+ const filePath = resolve(BUNDLED_INTEGRATION_TEMPLATES_DIR, `${name}.integration.yaml`);
19
+ try {
20
+ return await readFile(filePath, 'utf-8');
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * List all integration plugins available for installation from the bundled registry.
28
+ * Returns an array of { name, description } objects.
29
+ */
30
+ export async function listAvailableIntegrationTemplates(): Promise<{ name: string; description: string }[]> {
31
+ let files: string[];
32
+ try {
33
+ files = (await readdir(BUNDLED_INTEGRATION_TEMPLATES_DIR))
34
+ .filter(f => f.endsWith('.integration.yaml'))
35
+ .sort();
36
+ } catch {
37
+ return [];
38
+ }
39
+ const result: { name: string; description: string }[] = [];
40
+ for (const file of files) {
41
+ const content = await readFile(resolve(BUNDLED_INTEGRATION_TEMPLATES_DIR, file), 'utf-8');
42
+ const def = yaml.load(content) as IntegrationPluginDef;
43
+ result.push({ name: file.replace('.integration.yaml', ''), description: def.description ?? '' });
44
+ }
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Load all `.integration.yaml` files from a project's integrations directory.
50
+ * Returns an empty array if the directory does not exist.
51
+ */
52
+ export async function loadProjectIntegrations(integrationsDir: string): Promise<IntegrationPluginDef[]> {
53
+ if (!existsSync(integrationsDir)) return [];
54
+
55
+ let files: string[];
56
+ try {
57
+ files = (await readdir(integrationsDir)).filter(f => f.endsWith('.integration.yaml'));
58
+ } catch {
59
+ return [];
60
+ }
61
+
62
+ const result: IntegrationPluginDef[] = [];
63
+ for (const file of files.sort()) {
64
+ const content = await readFile(resolve(integrationsDir, file), 'utf-8');
65
+ result.push(yaml.load(content) as IntegrationPluginDef);
66
+ }
67
+ return result;
68
+ }
@@ -0,0 +1,38 @@
1
+ import { anonymize, deanonymize } from '@studio-foundation/anonymizer';
2
+ import type { AnonymizerOptions } from '@studio-foundation/anonymizer';
3
+
4
+ /**
5
+ * Stateful middleware that anonymizes text before sending to LLM
6
+ * and deanonymizes text after receiving from LLM.
7
+ * Accumulates the keymap across calls for cross-call consistency.
8
+ */
9
+ export class AnonymizationMiddleware {
10
+ private keymap: Record<string, string> = {};
11
+ private options?: Omit<AnonymizerOptions, 'seedKeymap'>;
12
+
13
+ constructor(options?: Omit<AnonymizerOptions, 'seedKeymap'>) {
14
+ this.options = options;
15
+ }
16
+
17
+ /**
18
+ * Anonymize text. Accumulated keymap grows with each call.
19
+ * Same PII value always gets the same token (guaranteed by seedKeymap).
20
+ */
21
+ anonymize(text: string): string {
22
+ const result = anonymize(text, { ...this.options, seedKeymap: this.keymap });
23
+ // result.keymap is the full accumulated keymap (seed + new entries)
24
+ this.keymap = result.keymap;
25
+ return result.text;
26
+ }
27
+
28
+ /**
29
+ * Deanonymize text using accumulated keymap.
30
+ */
31
+ deanonymize(text: string): string {
32
+ return deanonymize(text, this.keymap);
33
+ }
34
+
35
+ getKeymap(): Record<string, string> {
36
+ return { ...this.keymap };
37
+ }
38
+ }
@@ -0,0 +1,4 @@
1
+ export { loadPlugins } from './plugin-loader.js';
2
+ export type { PluginManifest, MCPServerDef, SkillContent } from './plugin-loader.js';
3
+ export { MCPClient } from './mcp-client.js';
4
+ export { StudioOAuthProvider } from './oauth-provider.js';