@moxxy/core 0.3.0

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 (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/events/factory.d.ts +6 -0
  4. package/dist/events/factory.d.ts.map +1 -0
  5. package/dist/events/factory.js +14 -0
  6. package/dist/events/factory.js.map +1 -0
  7. package/dist/events/log.d.ts +92 -0
  8. package/dist/events/log.d.ts.map +1 -0
  9. package/dist/events/log.js +244 -0
  10. package/dist/events/log.js.map +1 -0
  11. package/dist/index.d.ts +40 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +41 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +16 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +27 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/permissions/engine.d.ts +94 -0
  20. package/dist/permissions/engine.d.ts.map +1 -0
  21. package/dist/permissions/engine.js +203 -0
  22. package/dist/permissions/engine.js.map +1 -0
  23. package/dist/permissions/resolvers.d.ts +8 -0
  24. package/dist/permissions/resolvers.d.ts.map +1 -0
  25. package/dist/permissions/resolvers.js +8 -0
  26. package/dist/permissions/resolvers.js.map +1 -0
  27. package/dist/plugins/discovery.d.ts +9 -0
  28. package/dist/plugins/discovery.d.ts.map +1 -0
  29. package/dist/plugins/discovery.js +112 -0
  30. package/dist/plugins/discovery.js.map +1 -0
  31. package/dist/plugins/host-options.d.ts +72 -0
  32. package/dist/plugins/host-options.d.ts.map +1 -0
  33. package/dist/plugins/host-options.js +2 -0
  34. package/dist/plugins/host-options.js.map +1 -0
  35. package/dist/plugins/host.d.ts +53 -0
  36. package/dist/plugins/host.d.ts.map +1 -0
  37. package/dist/plugins/host.js +269 -0
  38. package/dist/plugins/host.js.map +1 -0
  39. package/dist/plugins/lifecycle.d.ts +31 -0
  40. package/dist/plugins/lifecycle.d.ts.map +1 -0
  41. package/dist/plugins/lifecycle.js +100 -0
  42. package/dist/plugins/lifecycle.js.map +1 -0
  43. package/dist/plugins/loader.d.ts +7 -0
  44. package/dist/plugins/loader.d.ts.map +1 -0
  45. package/dist/plugins/loader.js +68 -0
  46. package/dist/plugins/loader.js.map +1 -0
  47. package/dist/plugins/package-requirements.d.ts +16 -0
  48. package/dist/plugins/package-requirements.d.ts.map +1 -0
  49. package/dist/plugins/package-requirements.js +52 -0
  50. package/dist/plugins/package-requirements.js.map +1 -0
  51. package/dist/plugins/registry-kinds.d.ts +58 -0
  52. package/dist/plugins/registry-kinds.d.ts.map +1 -0
  53. package/dist/plugins/registry-kinds.js +139 -0
  54. package/dist/plugins/registry-kinds.js.map +1 -0
  55. package/dist/plugins/toposort.d.ts +24 -0
  56. package/dist/plugins/toposort.d.ts.map +1 -0
  57. package/dist/plugins/toposort.js +67 -0
  58. package/dist/plugins/toposort.js.map +1 -0
  59. package/dist/preferences.d.ts +40 -0
  60. package/dist/preferences.d.ts.map +1 -0
  61. package/dist/preferences.js +58 -0
  62. package/dist/preferences.js.map +1 -0
  63. package/dist/registries/active-backend-registry.d.ts +65 -0
  64. package/dist/registries/active-backend-registry.d.ts.map +1 -0
  65. package/dist/registries/active-backend-registry.js +108 -0
  66. package/dist/registries/active-backend-registry.js.map +1 -0
  67. package/dist/registries/active-def-registry.d.ts +59 -0
  68. package/dist/registries/active-def-registry.d.ts.map +1 -0
  69. package/dist/registries/active-def-registry.js +63 -0
  70. package/dist/registries/active-def-registry.js.map +1 -0
  71. package/dist/registries/agents.d.ts +16 -0
  72. package/dist/registries/agents.d.ts.map +1 -0
  73. package/dist/registries/agents.js +17 -0
  74. package/dist/registries/agents.js.map +1 -0
  75. package/dist/registries/cache-strategies.d.ts +12 -0
  76. package/dist/registries/cache-strategies.d.ts.map +1 -0
  77. package/dist/registries/cache-strategies.js +13 -0
  78. package/dist/registries/cache-strategies.js.map +1 -0
  79. package/dist/registries/channels.d.ts +16 -0
  80. package/dist/registries/channels.d.ts.map +1 -0
  81. package/dist/registries/channels.js +35 -0
  82. package/dist/registries/channels.js.map +1 -0
  83. package/dist/registries/commands.d.ts +29 -0
  84. package/dist/registries/commands.d.ts.map +1 -0
  85. package/dist/registries/commands.js +72 -0
  86. package/dist/registries/commands.js.map +1 -0
  87. package/dist/registries/compactors.d.ts +12 -0
  88. package/dist/registries/compactors.d.ts.map +1 -0
  89. package/dist/registries/compactors.js +13 -0
  90. package/dist/registries/compactors.js.map +1 -0
  91. package/dist/registries/def-map-registry.d.ts +33 -0
  92. package/dist/registries/def-map-registry.d.ts.map +1 -0
  93. package/dist/registries/def-map-registry.js +36 -0
  94. package/dist/registries/def-map-registry.js.map +1 -0
  95. package/dist/registries/embedders.d.ts +16 -0
  96. package/dist/registries/embedders.d.ts.map +1 -0
  97. package/dist/registries/embedders.js +17 -0
  98. package/dist/registries/embedders.js.map +1 -0
  99. package/dist/registries/isolators.d.ts +21 -0
  100. package/dist/registries/isolators.d.ts.map +1 -0
  101. package/dist/registries/isolators.js +32 -0
  102. package/dist/registries/isolators.js.map +1 -0
  103. package/dist/registries/modes.d.ts +28 -0
  104. package/dist/registries/modes.d.ts.map +1 -0
  105. package/dist/registries/modes.js +81 -0
  106. package/dist/registries/modes.js.map +1 -0
  107. package/dist/registries/providers.d.ts +44 -0
  108. package/dist/registries/providers.d.ts.map +1 -0
  109. package/dist/registries/providers.js +95 -0
  110. package/dist/registries/providers.js.map +1 -0
  111. package/dist/registries/skills.d.ts +24 -0
  112. package/dist/registries/skills.d.ts.map +1 -0
  113. package/dist/registries/skills.js +81 -0
  114. package/dist/registries/skills.js.map +1 -0
  115. package/dist/registries/surfaces.d.ts +13 -0
  116. package/dist/registries/surfaces.d.ts.map +1 -0
  117. package/dist/registries/surfaces.js +14 -0
  118. package/dist/registries/surfaces.js.map +1 -0
  119. package/dist/registries/synthesizers.d.ts +26 -0
  120. package/dist/registries/synthesizers.d.ts.map +1 -0
  121. package/dist/registries/synthesizers.js +34 -0
  122. package/dist/registries/synthesizers.js.map +1 -0
  123. package/dist/registries/tools.d.ts +49 -0
  124. package/dist/registries/tools.d.ts.map +1 -0
  125. package/dist/registries/tools.js +98 -0
  126. package/dist/registries/tools.js.map +1 -0
  127. package/dist/registries/transcribers.d.ts +17 -0
  128. package/dist/registries/transcribers.d.ts.map +1 -0
  129. package/dist/registries/transcribers.js +18 -0
  130. package/dist/registries/transcribers.js.map +1 -0
  131. package/dist/registries/tunnel-providers.d.ts +12 -0
  132. package/dist/registries/tunnel-providers.d.ts.map +1 -0
  133. package/dist/registries/tunnel-providers.js +13 -0
  134. package/dist/registries/tunnel-providers.js.map +1 -0
  135. package/dist/registries/view-renderers.d.ts +13 -0
  136. package/dist/registries/view-renderers.d.ts.map +1 -0
  137. package/dist/registries/view-renderers.js +14 -0
  138. package/dist/registries/view-renderers.js.map +1 -0
  139. package/dist/registries/workflow-executors.d.ts +13 -0
  140. package/dist/registries/workflow-executors.d.ts.map +1 -0
  141. package/dist/registries/workflow-executors.js +14 -0
  142. package/dist/registries/workflow-executors.js.map +1 -0
  143. package/dist/requirements.d.ts +59 -0
  144. package/dist/requirements.d.ts.map +1 -0
  145. package/dist/requirements.js +149 -0
  146. package/dist/requirements.js.map +1 -0
  147. package/dist/run-turn.d.ts +6 -0
  148. package/dist/run-turn.d.ts.map +1 -0
  149. package/dist/run-turn.js +141 -0
  150. package/dist/run-turn.js.map +1 -0
  151. package/dist/session-runtime.d.ts +67 -0
  152. package/dist/session-runtime.d.ts.map +1 -0
  153. package/dist/session-runtime.js +2 -0
  154. package/dist/session-runtime.js.map +1 -0
  155. package/dist/session.d.ts +203 -0
  156. package/dist/session.d.ts.map +1 -0
  157. package/dist/session.js +399 -0
  158. package/dist/session.js.map +1 -0
  159. package/dist/sessions/persistence.d.ts +184 -0
  160. package/dist/sessions/persistence.d.ts.map +1 -0
  161. package/dist/sessions/persistence.js +442 -0
  162. package/dist/sessions/persistence.js.map +1 -0
  163. package/dist/setup-agent.d.ts +91 -0
  164. package/dist/setup-agent.d.ts.map +1 -0
  165. package/dist/setup-agent.js +138 -0
  166. package/dist/setup-agent.js.map +1 -0
  167. package/dist/skills/index.d.ts +4 -0
  168. package/dist/skills/index.d.ts.map +1 -0
  169. package/dist/skills/index.js +4 -0
  170. package/dist/skills/index.js.map +1 -0
  171. package/dist/skills/loader.d.ts +16 -0
  172. package/dist/skills/loader.d.ts.map +1 -0
  173. package/dist/skills/loader.js +99 -0
  174. package/dist/skills/loader.js.map +1 -0
  175. package/dist/skills/parse.d.ts +5 -0
  176. package/dist/skills/parse.d.ts.map +1 -0
  177. package/dist/skills/parse.js +8 -0
  178. package/dist/skills/parse.js.map +1 -0
  179. package/dist/skills/synthesize-draft.d.ts +8 -0
  180. package/dist/skills/synthesize-draft.d.ts.map +1 -0
  181. package/dist/skills/synthesize-draft.js +32 -0
  182. package/dist/skills/synthesize-draft.js.map +1 -0
  183. package/dist/skills/synthesize.d.ts +33 -0
  184. package/dist/skills/synthesize.d.ts.map +1 -0
  185. package/dist/skills/synthesize.js +241 -0
  186. package/dist/skills/synthesize.js.map +1 -0
  187. package/dist/subagents/events.d.ts +20 -0
  188. package/dist/subagents/events.d.ts.map +1 -0
  189. package/dist/subagents/events.js +146 -0
  190. package/dist/subagents/events.js.map +1 -0
  191. package/dist/subagents/registry.d.ts +40 -0
  192. package/dist/subagents/registry.d.ts.map +1 -0
  193. package/dist/subagents/registry.js +42 -0
  194. package/dist/subagents/registry.js.map +1 -0
  195. package/dist/subagents/run-child.d.ts +43 -0
  196. package/dist/subagents/run-child.d.ts.map +1 -0
  197. package/dist/subagents/run-child.js +276 -0
  198. package/dist/subagents/run-child.js.map +1 -0
  199. package/dist/subagents/spawn.d.ts +8 -0
  200. package/dist/subagents/spawn.d.ts.map +1 -0
  201. package/dist/subagents/spawn.js +8 -0
  202. package/dist/subagents/spawn.js.map +1 -0
  203. package/dist/subagents/tools.d.ts +4 -0
  204. package/dist/subagents/tools.d.ts.map +1 -0
  205. package/dist/subagents/tools.js +13 -0
  206. package/dist/subagents/tools.js.map +1 -0
  207. package/dist/subagents.d.ts +3 -0
  208. package/dist/subagents.d.ts.map +1 -0
  209. package/dist/subagents.js +3 -0
  210. package/dist/subagents.js.map +1 -0
  211. package/dist/surfaces/host.d.ts +53 -0
  212. package/dist/surfaces/host.d.ts.map +1 -0
  213. package/dist/surfaces/host.js +177 -0
  214. package/dist/surfaces/host.js.map +1 -0
  215. package/dist/tunnel/localhost.d.ts +7 -0
  216. package/dist/tunnel/localhost.d.ts.map +1 -0
  217. package/dist/tunnel/localhost.js +21 -0
  218. package/dist/tunnel/localhost.js.map +1 -0
  219. package/dist/usage-stats.d.ts +42 -0
  220. package/dist/usage-stats.d.ts.map +1 -0
  221. package/dist/usage-stats.js +83 -0
  222. package/dist/usage-stats.js.map +1 -0
  223. package/dist/view/default-renderer.d.ts +7 -0
  224. package/dist/view/default-renderer.d.ts.map +1 -0
  225. package/dist/view/default-renderer.js +14 -0
  226. package/dist/view/default-renderer.js.map +1 -0
  227. package/dist/view/parse.d.ts +7 -0
  228. package/dist/view/parse.d.ts.map +1 -0
  229. package/dist/view/parse.js +479 -0
  230. package/dist/view/parse.js.map +1 -0
  231. package/package.json +61 -0
  232. package/src/events/factory.ts +27 -0
  233. package/src/events/log.test.ts +470 -0
  234. package/src/events/log.ts +256 -0
  235. package/src/index.ts +102 -0
  236. package/src/logger.ts +46 -0
  237. package/src/permissions/engine.test.ts +206 -0
  238. package/src/permissions/engine.ts +247 -0
  239. package/src/permissions/resolvers.test.ts +40 -0
  240. package/src/permissions/resolvers.ts +17 -0
  241. package/src/plugins/discovery.test.ts +115 -0
  242. package/src/plugins/discovery.ts +124 -0
  243. package/src/plugins/host-options.ts +73 -0
  244. package/src/plugins/host-view-tunnel.test.ts +86 -0
  245. package/src/plugins/host.test.ts +434 -0
  246. package/src/plugins/host.ts +327 -0
  247. package/src/plugins/lifecycle.test.ts +187 -0
  248. package/src/plugins/lifecycle.ts +136 -0
  249. package/src/plugins/loader.ts +79 -0
  250. package/src/plugins/package-requirements.ts +50 -0
  251. package/src/plugins/registry-kinds.test.ts +162 -0
  252. package/src/plugins/registry-kinds.ts +210 -0
  253. package/src/plugins/toposort.test.ts +66 -0
  254. package/src/plugins/toposort.ts +69 -0
  255. package/src/preferences.test.ts +105 -0
  256. package/src/preferences.ts +84 -0
  257. package/src/registries/active-backend-registry.ts +142 -0
  258. package/src/registries/active-def-registry.test.ts +103 -0
  259. package/src/registries/active-def-registry.ts +97 -0
  260. package/src/registries/agents.ts +18 -0
  261. package/src/registries/cache-strategies.ts +14 -0
  262. package/src/registries/channels.ts +42 -0
  263. package/src/registries/commands.test.ts +71 -0
  264. package/src/registries/commands.ts +78 -0
  265. package/src/registries/compactors.ts +14 -0
  266. package/src/registries/def-map-registry.test.ts +67 -0
  267. package/src/registries/def-map-registry.ts +59 -0
  268. package/src/registries/embedders.test.ts +54 -0
  269. package/src/registries/embedders.ts +18 -0
  270. package/src/registries/isolators.test.ts +43 -0
  271. package/src/registries/isolators.ts +38 -0
  272. package/src/registries/modes.test.ts +66 -0
  273. package/src/registries/modes.ts +80 -0
  274. package/src/registries/providers.test.ts +50 -0
  275. package/src/registries/providers.ts +97 -0
  276. package/src/registries/semantics.test.ts +117 -0
  277. package/src/registries/skills.ts +87 -0
  278. package/src/registries/surfaces.ts +18 -0
  279. package/src/registries/synthesizers.test.ts +63 -0
  280. package/src/registries/synthesizers.ts +36 -0
  281. package/src/registries/tools.test.ts +142 -0
  282. package/src/registries/tools.ts +145 -0
  283. package/src/registries/transcribers.test.ts +70 -0
  284. package/src/registries/transcribers.ts +19 -0
  285. package/src/registries/tunnel-providers.test.ts +30 -0
  286. package/src/registries/tunnel-providers.ts +14 -0
  287. package/src/registries/view-renderers.test.ts +68 -0
  288. package/src/registries/view-renderers.ts +15 -0
  289. package/src/registries/workflow-executors.test.ts +53 -0
  290. package/src/registries/workflow-executors.ts +15 -0
  291. package/src/requirements.targetinfo.test.ts +70 -0
  292. package/src/requirements.test.ts +111 -0
  293. package/src/requirements.ts +262 -0
  294. package/src/run-turn.test.ts +178 -0
  295. package/src/run-turn.ts +156 -0
  296. package/src/session-runtime.ts +75 -0
  297. package/src/session.test.ts +216 -0
  298. package/src/session.ts +491 -0
  299. package/src/sessions/persistence.flush.test.ts +100 -0
  300. package/src/sessions/persistence.ready.test.ts +91 -0
  301. package/src/sessions/persistence.test.ts +274 -0
  302. package/src/sessions/persistence.ts +498 -0
  303. package/src/setup-agent.test.ts +72 -0
  304. package/src/setup-agent.ts +189 -0
  305. package/src/skills/index.ts +14 -0
  306. package/src/skills/loader.test.ts +102 -0
  307. package/src/skills/loader.ts +114 -0
  308. package/src/skills/parse.test.ts +59 -0
  309. package/src/skills/parse.ts +11 -0
  310. package/src/skills/synthesize-draft.test.ts +79 -0
  311. package/src/skills/synthesize-draft.ts +46 -0
  312. package/src/skills/synthesize.test.ts +170 -0
  313. package/src/skills/synthesize.ts +304 -0
  314. package/src/subagents/events.test.ts +56 -0
  315. package/src/subagents/events.ts +194 -0
  316. package/src/subagents/registry.ts +62 -0
  317. package/src/subagents/run-child.test.ts +185 -0
  318. package/src/subagents/run-child.ts +425 -0
  319. package/src/subagents/spawn.ts +12 -0
  320. package/src/subagents/tools.ts +19 -0
  321. package/src/subagents.ts +2 -0
  322. package/src/surfaces/host.test.ts +330 -0
  323. package/src/surfaces/host.ts +192 -0
  324. package/src/tunnel/localhost.test.ts +22 -0
  325. package/src/tunnel/localhost.ts +21 -0
  326. package/src/usage-stats.test.ts +77 -0
  327. package/src/usage-stats.ts +119 -0
  328. package/src/view/default-renderer.test.ts +29 -0
  329. package/src/view/default-renderer.ts +14 -0
  330. package/src/view/parse-extended.test.ts +255 -0
  331. package/src/view/parse.test.ts +232 -0
  332. package/src/view/parse.ts +520 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `setupAgent` — the one-call ergonomic entry to the moxxy runtime.
3
+ *
4
+ * Constructing a {@link Session}, registering a mode + provider plugins + tools,
5
+ * activating a provider, and calling {@link runTurn} is the standard boilerplate.
6
+ * This wraps all of it in a single SYNCHRONOUS call and hands back a small
7
+ * {@link Agent} you destructure:
8
+ *
9
+ * import { setupAgent } from '@moxxy/core';
10
+ * import defaultMode from '@moxxy/mode-default';
11
+ * import openai from '@moxxy/plugin-provider-openai';
12
+ *
13
+ * const { ask, stream, session } = setupAgent({
14
+ * plugins: [defaultMode, openai],
15
+ * provider: { name: 'openai', config: { apiKey: process.env.OPENAI_API_KEY } },
16
+ * tools: [myTool],
17
+ * });
18
+ *
19
+ * console.log(await ask('Hello!')); // final reply text (async)
20
+ * for await (const e of stream('Go on…')) { … } // event stream (async generator)
21
+ *
22
+ * Two ways to run a turn: `stream()` is a proper async generator that YIELDS each
23
+ * {@link MoxxyEvent} as it happens; `ask()`/`collect()` are async — the final
24
+ * text, or every event. The methods close over the session (no `this`), so
25
+ * destructuring is safe. `session` is the LIVE session — hot-change blocks
26
+ * between turns through it (or the `setProvider`/`setMode`/`use`/`addTool` sugar).
27
+ * It stays block-agnostic: you pass the provider/mode PLUGINS in, so `@moxxy/core`
28
+ * never depends on a vendor.
29
+ */
30
+
31
+ import type { MoxxyEvent, PermissionResolver, Plugin, RunTurnOptions, ToolDef } from '@moxxy/sdk';
32
+
33
+ import { Session, type SessionOptions } from './session.js';
34
+ import { runTurn, collectTurn } from './run-turn.js';
35
+ import { autoAllowResolver, denyByDefaultResolver } from './permissions/resolvers.js';
36
+
37
+ export interface SetupAgentOptions {
38
+ /** Working directory the agent operates in. Defaults to `process.cwd()`. */
39
+ readonly cwd?: string;
40
+ /** Plugins to register up front — modes, providers, tool-packs, … (order kept). */
41
+ readonly plugins?: readonly Plugin[];
42
+ /** Tools to register directly (sugar over authoring a one-tool plugin). */
43
+ readonly tools?: readonly ToolDef[];
44
+ /** Activate a provider after registration: its registered `name` + the config
45
+ * its `createClient` needs (e.g. `{ apiKey }`). */
46
+ readonly provider?: { readonly name: string; readonly config?: Record<string, unknown> };
47
+ /** Permission policy: `'auto'` (approve every tool call — frictionless headless
48
+ * default), `'deny'` (refuse all), or a custom {@link PermissionResolver}. */
49
+ readonly permissions?: 'auto' | 'deny' | PermissionResolver;
50
+ /** Any other {@link SessionOptions} (logger, secretResolver, pluginLoader,
51
+ * pluginDiscoveryPaths, …). */
52
+ readonly session?: Omit<SessionOptions, 'cwd' | 'permissionResolver'>;
53
+ }
54
+
55
+ export interface Agent {
56
+ /** The underlying live {@link Session} — full control + hot-change escape hatch
57
+ * (`session.providers`, `session.tools`, `session.modes`, `session.pluginHost`). */
58
+ readonly session: Session;
59
+
60
+ /** Run one turn as an async generator, YIELDING each event as it happens:
61
+ * `for await (const e of stream(prompt)) …`. */
62
+ stream(prompt: string, opts?: RunTurnOptions): AsyncGenerator<MoxxyEvent, void, void>;
63
+ /** Run one turn; resolve with the final assistant text (`''` if none). */
64
+ ask(prompt: string, opts?: RunTurnOptions): Promise<string>;
65
+ /** Run one turn; resolve with every emitted event. */
66
+ collect(prompt: string, opts?: RunTurnOptions): Promise<ReadonlyArray<MoxxyEvent>>;
67
+ /** Discover + load plugins from `node_modules` (and `pluginDiscoveryPaths`).
68
+ * Async; resolves with this agent for chaining. */
69
+ discover(): Promise<Agent>;
70
+
71
+ // ---- hot-change sugar (mutates the live session; chainable) ----
72
+ /** Register another plugin (mode / provider / tool-pack) on the live session. */
73
+ use(plugin: Plugin): Agent;
74
+ /** Register a tool on the live session. */
75
+ addTool(tool: ToolDef): Agent;
76
+ /** Remove a registered tool by name. */
77
+ removeTool(name: string): Agent;
78
+ /** Swap the active provider (and its config) for subsequent turns. */
79
+ setProvider(name: string, config?: Record<string, unknown>): Agent;
80
+ /** Swap the active loop strategy (mode) for subsequent turns. */
81
+ setMode(name: string): Agent;
82
+ }
83
+
84
+ function resolvePermissions(p: SetupAgentOptions['permissions']): PermissionResolver {
85
+ if (p === 'deny') return denyByDefaultResolver;
86
+ if (p === undefined || p === 'auto') return autoAllowResolver;
87
+ return p;
88
+ }
89
+
90
+ function finalText(events: ReadonlyArray<MoxxyEvent>): string {
91
+ for (let i = events.length - 1; i >= 0; i -= 1) {
92
+ const e = events[i]!;
93
+ if (e.type === 'assistant_message') return e.content;
94
+ }
95
+ return '';
96
+ }
97
+
98
+ /** A reusable, pre-filled chunk of {@link SetupAgentOptions} — e.g. a provider
99
+ * "preset" that bundles a provider plugin + the default mode + its config, so
100
+ * `setupAgent(openaiPreset({ apiKey }))` is a one-line drop-in. A preset is just
101
+ * an options object, so presets compose: pass an array and they merge. */
102
+ export type AgentPreset = SetupAgentOptions;
103
+
104
+ /** Merge an array of presets into one options object: plugins concatenate
105
+ * (de-duped by name, so two presets both bringing the default mode register it
106
+ * once), tools concatenate, and the FIRST preset that names a provider wins as
107
+ * the active one (the others are still registered, so you can `setProvider` to
108
+ * swap). cwd/permissions take the first set; session options shallow-merge. */
109
+ function mergePresets(presets: readonly SetupAgentOptions[]): SetupAgentOptions {
110
+ const plugins: Plugin[] = [];
111
+ const seen = new Set<string>();
112
+ const tools: ToolDef[] = [];
113
+ let provider: SetupAgentOptions['provider'];
114
+ let cwd: string | undefined;
115
+ let permissions: SetupAgentOptions['permissions'];
116
+ let session: SetupAgentOptions['session'];
117
+ for (const p of presets) {
118
+ for (const pl of p.plugins ?? []) {
119
+ if (seen.has(pl.name)) continue;
120
+ seen.add(pl.name);
121
+ plugins.push(pl);
122
+ }
123
+ if (p.tools) tools.push(...p.tools);
124
+ if (!provider && p.provider) provider = p.provider;
125
+ cwd ??= p.cwd;
126
+ permissions ??= p.permissions;
127
+ if (p.session) session = { ...session, ...p.session };
128
+ }
129
+ return { plugins, tools, provider, cwd, permissions, session };
130
+ }
131
+
132
+ /**
133
+ * Build a ready-to-run {@link Agent}. Synchronous — destructure the result.
134
+ * Accepts a full options object, a single {@link AgentPreset}, or an ARRAY of
135
+ * presets that are merged (see {@link mergePresets}):
136
+ *
137
+ * setupAgent(openaiPreset({ apiKey }))
138
+ * setupAgent([openaiPreset({ apiKey }), anthropicPreset({ apiKey })]) // both; openai active
139
+ */
140
+ export function setupAgent(input: SetupAgentOptions | readonly SetupAgentOptions[] = {}): Agent {
141
+ const opts = Array.isArray(input) ? mergePresets(input) : (input as SetupAgentOptions);
142
+ const session = new Session({
143
+ cwd: opts.cwd ?? process.cwd(),
144
+ permissionResolver: resolvePermissions(opts.permissions),
145
+ ...opts.session,
146
+ });
147
+
148
+ for (const plugin of opts.plugins ?? []) session.pluginHost.registerStatic(plugin);
149
+ for (const tool of opts.tools ?? []) session.tools.register(tool);
150
+ if (opts.provider) session.providers.setActive(opts.provider.name, opts.provider.config);
151
+
152
+ const agent: Agent = {
153
+ session,
154
+ async *stream(prompt, runOpts) {
155
+ yield* runTurn(session, prompt, runOpts);
156
+ },
157
+ async ask(prompt, runOpts) {
158
+ return finalText(await collectTurn(session, prompt, runOpts));
159
+ },
160
+ collect(prompt, runOpts) {
161
+ return collectTurn(session, prompt, runOpts);
162
+ },
163
+ async discover() {
164
+ await session.pluginHost.discoverAndLoad();
165
+ return agent;
166
+ },
167
+ use(plugin) {
168
+ session.pluginHost.registerStatic(plugin);
169
+ return agent;
170
+ },
171
+ addTool(tool) {
172
+ session.tools.register(tool);
173
+ return agent;
174
+ },
175
+ removeTool(name) {
176
+ session.tools.unregister(name);
177
+ return agent;
178
+ },
179
+ setProvider(name, config) {
180
+ session.providers.setActive(name, config);
181
+ return agent;
182
+ },
183
+ setMode(name) {
184
+ session.modes.setActive(name);
185
+ return agent;
186
+ },
187
+ };
188
+ return agent;
189
+ }
@@ -0,0 +1,14 @@
1
+ export { parseSkillFile, parseFrontmatter } from './parse.js';
2
+ export {
3
+ discoverSkills,
4
+ defaultUserSkillsDir,
5
+ defaultProjectSkillsDir,
6
+ type SkillLoadOptions,
7
+ type DiscoveredSkill,
8
+ } from './loader.js';
9
+ export {
10
+ synthesizeSkill,
11
+ buildSynthesizeSkillPlugin,
12
+ type SynthesizeOptions,
13
+ type SynthesizedSkill,
14
+ } from './synthesize.js';
@@ -0,0 +1,102 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { discoverSkills } from './loader.js';
6
+ import { silentLogger } from '../logger.js';
7
+
8
+ let tmp: string;
9
+
10
+ beforeEach(async () => {
11
+ tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-skills-'));
12
+ });
13
+ afterEach(async () => {
14
+ await fs.rm(tmp, { recursive: true, force: true });
15
+ });
16
+
17
+ const writeSkill = async (dir: string, name: string, body = '...') => {
18
+ await fs.mkdir(dir, { recursive: true });
19
+ await fs.writeFile(
20
+ path.join(dir, `${name}.md`),
21
+ `---\nname: ${name}\ndescription: skill ${name}\ntriggers: [${name}]\n---\n${body}`,
22
+ );
23
+ };
24
+
25
+ describe('discoverSkills', () => {
26
+ it('loads skills from project, user, plugin, and builtin', async () => {
27
+ await writeSkill(path.join(tmp, 'project'), 'a-project');
28
+ await writeSkill(path.join(tmp, 'user'), 'b-user');
29
+ await writeSkill(path.join(tmp, 'plugin'), 'c-plugin');
30
+ await writeSkill(path.join(tmp, 'builtin'), 'd-builtin');
31
+
32
+ const skills = await discoverSkills({
33
+ projectDir: path.join(tmp, 'project'),
34
+ userDir: path.join(tmp, 'user'),
35
+ pluginDirs: [path.join(tmp, 'plugin')],
36
+ builtinDir: path.join(tmp, 'builtin'),
37
+ logger: silentLogger,
38
+ });
39
+ const names = skills.map((s) => s.frontmatter.name).sort();
40
+ expect(names).toEqual(['a-project', 'b-user', 'c-plugin', 'd-builtin']);
41
+ });
42
+
43
+ it('project scope overrides user/builtin with the same skill name', async () => {
44
+ await writeSkill(path.join(tmp, 'project'), 'shared', 'project body');
45
+ await writeSkill(path.join(tmp, 'user'), 'shared', 'user body');
46
+ await writeSkill(path.join(tmp, 'builtin'), 'shared', 'builtin body');
47
+
48
+ const skills = await discoverSkills({
49
+ projectDir: path.join(tmp, 'project'),
50
+ userDir: path.join(tmp, 'user'),
51
+ builtinDir: path.join(tmp, 'builtin'),
52
+ logger: silentLogger,
53
+ });
54
+ expect(skills).toHaveLength(1);
55
+ expect(skills[0]!.scope).toBe('project');
56
+ expect(skills[0]!.body).toContain('project body');
57
+ });
58
+
59
+ it('silently skips files with invalid frontmatter', async () => {
60
+ const dir = path.join(tmp, 'project');
61
+ await fs.mkdir(dir, { recursive: true });
62
+ await fs.writeFile(path.join(dir, 'bad.md'), '---\nname: Bad Name\n---\n');
63
+ const skills = await discoverSkills({ projectDir: dir, userDir: path.join(tmp, 'noop'), logger: silentLogger });
64
+ expect(skills).toHaveLength(0);
65
+ });
66
+
67
+ it('skips a single unreadable .md but still loads the rest of the tree (u46-5)', async () => {
68
+ // A permission-denied read used to throw out of loadDir and abort discovery
69
+ // of every remaining skill in every source. It must now degrade to skipping
70
+ // just the bad file. (chmod 000 has no teeth as root, so skip there.)
71
+ if (typeof process.getuid === 'function' && process.getuid() === 0) return;
72
+ const dir = path.join(tmp, 'project');
73
+ await writeSkill(dir, 'good-one');
74
+ const badPath = path.join(dir, 'unreadable.md');
75
+ await fs.writeFile(
76
+ badPath,
77
+ '---\nname: unreadable\ndescription: x\ntriggers: [unreadable]\n---\nbody',
78
+ );
79
+ await fs.chmod(badPath, 0o000);
80
+ try {
81
+ const skills = await discoverSkills({
82
+ projectDir: dir,
83
+ userDir: path.join(tmp, 'noop'),
84
+ logger: silentLogger,
85
+ });
86
+ // The valid skill survives even though a sibling .md was unreadable.
87
+ expect(skills.map((s) => s.frontmatter.name)).toEqual(['good-one']);
88
+ } finally {
89
+ // Restore perms so afterEach rm can clean up.
90
+ await fs.chmod(badPath, 0o644).catch(() => {});
91
+ }
92
+ });
93
+
94
+ it('returns empty list when directories do not exist', async () => {
95
+ const skills = await discoverSkills({
96
+ projectDir: path.join(tmp, 'no-project'),
97
+ userDir: path.join(tmp, 'no-user'),
98
+ logger: silentLogger,
99
+ });
100
+ expect(skills).toEqual([]);
101
+ });
102
+ });
@@ -0,0 +1,114 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { skillFrontmatterSchema, asSkillId, type Skill, type SkillScope } from '@moxxy/sdk';
5
+ import { parseSkillFile } from './parse.js';
6
+ import type { Logger } from '../logger.js';
7
+
8
+ export interface SkillLoadOptions {
9
+ readonly projectDir?: string;
10
+ readonly userDir?: string;
11
+ readonly pluginDirs?: ReadonlyArray<string>;
12
+ readonly builtinDir?: string;
13
+ readonly logger?: Logger;
14
+ }
15
+
16
+ export interface DiscoveredSkill extends Skill {
17
+ readonly scope: SkillScope;
18
+ }
19
+
20
+ export async function discoverSkills(opts: SkillLoadOptions = {}): Promise<ReadonlyArray<DiscoveredSkill>> {
21
+ const sources: Array<{ dir: string; scope: SkillScope }> = [];
22
+ if (opts.builtinDir) sources.push({ dir: opts.builtinDir, scope: 'builtin' });
23
+ for (const dir of opts.pluginDirs ?? []) sources.push({ dir, scope: 'plugin' });
24
+ const userDir = opts.userDir ?? defaultUserSkillsDir();
25
+ sources.push({ dir: userDir, scope: 'user' });
26
+ if (opts.projectDir) sources.push({ dir: opts.projectDir, scope: 'project' });
27
+
28
+ const seenNames = new Map<string, DiscoveredSkill>();
29
+ for (const source of sources) {
30
+ const skills = await loadDir(source.dir, source.scope, opts.logger);
31
+ for (const skill of skills) seenNames.set(skill.frontmatter.name, skill);
32
+ }
33
+ return [...seenNames.values()];
34
+ }
35
+
36
+ async function loadDir(
37
+ dir: string,
38
+ scope: SkillScope,
39
+ logger?: Logger,
40
+ ): Promise<ReadonlyArray<DiscoveredSkill>> {
41
+ let entries: import('node:fs').Dirent[];
42
+ try {
43
+ entries = await fs.readdir(dir, { withFileTypes: true });
44
+ } catch {
45
+ return [];
46
+ }
47
+ const out: DiscoveredSkill[] = [];
48
+ for (const entry of entries) {
49
+ if (entry.isDirectory()) {
50
+ out.push(...(await loadDir(path.join(dir, entry.name), scope, logger)));
51
+ continue;
52
+ }
53
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
54
+ const full = path.join(dir, entry.name);
55
+ let raw: string;
56
+ try {
57
+ raw = await fs.readFile(full, 'utf8');
58
+ } catch (err) {
59
+ // One unreadable .md (permission, race-deleted, symlink loop) must not
60
+ // abort discovery of every remaining skill across every source — degrade
61
+ // to skipping just this file, like the invalid-frontmatter case below.
62
+ logger?.warn('skill: unreadable file, skipping', {
63
+ path: full,
64
+ error: err instanceof Error ? err.message : String(err),
65
+ });
66
+ continue;
67
+ }
68
+ const { frontmatter, body } = parseSkillFile(raw);
69
+ const parsed = skillFrontmatterSchema.safeParse(frontmatter);
70
+ if (!parsed.success) {
71
+ logger?.warn('skill: invalid frontmatter, skipping', { path: full, issues: parsed.error.issues });
72
+ continue;
73
+ }
74
+ // Auto-derive trigger fallbacks from the skill name when the
75
+ // frontmatter `triggers:` field is missing/empty. Without this,
76
+ // user-authored skills (or model-synthesized ones that skipped the
77
+ // field) have no triggers at all — they're effectively invisible
78
+ // to the trigger-match router and confusing in /skills listings.
79
+ // We split the kebab-case name into words and also keep the full
80
+ // slug as a literal trigger.
81
+ const fm = parsed.data;
82
+ const finalTriggers =
83
+ fm.triggers && fm.triggers.length > 0 ? fm.triggers : deriveTriggers(fm.name);
84
+ out.push({
85
+ id: asSkillId(`${scope}/${fm.name}`),
86
+ path: full,
87
+ scope,
88
+ frontmatter: { ...fm, triggers: finalTriggers },
89
+ body: body.trimEnd(),
90
+ });
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Derive trigger-like phrases from a kebab-case skill name. Best-effort
97
+ * fallback for skills whose frontmatter omits the `triggers:` field —
98
+ * gives the router and `/skills` listing *something* to show.
99
+ */
100
+ function deriveTriggers(name: string): ReadonlyArray<string> {
101
+ const parts = name.split('-').filter((p) => p.length > 1);
102
+ const out = new Set<string>();
103
+ out.add(name.replace(/-/g, ' '));
104
+ for (const p of parts) out.add(p);
105
+ return [...out];
106
+ }
107
+
108
+ export function defaultUserSkillsDir(): string {
109
+ return path.join(os.homedir(), '.moxxy', 'skills');
110
+ }
111
+
112
+ export function defaultProjectSkillsDir(cwd: string): string {
113
+ return path.join(cwd, '.moxxy', 'skills');
114
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseFrontmatter, parseSkillFile } from './parse.js';
3
+
4
+ describe('parseSkillFile', () => {
5
+ it('returns empty frontmatter when none present', () => {
6
+ const { frontmatter, body } = parseSkillFile('hello world');
7
+ expect(frontmatter).toEqual({});
8
+ expect(body).toBe('hello world');
9
+ });
10
+
11
+ it('parses simple frontmatter', () => {
12
+ const { frontmatter, body } = parseSkillFile(`---
13
+ name: foo
14
+ description: bar
15
+ ---
16
+ body text`);
17
+ expect(frontmatter).toEqual({ name: 'foo', description: 'bar' });
18
+ expect(body.trim()).toBe('body text');
19
+ });
20
+
21
+ it('parses inline arrays', () => {
22
+ const { frontmatter } = parseSkillFile(`---
23
+ name: foo
24
+ description: d
25
+ triggers: [a, b, "c d"]
26
+ ---`);
27
+ expect(frontmatter.triggers).toEqual(['a', 'b', 'c d']);
28
+ });
29
+
30
+ it('parses block-list arrays', () => {
31
+ const { frontmatter } = parseSkillFile(`---
32
+ name: foo
33
+ description: d
34
+ allowed-tools:
35
+ - Read
36
+ - Edit
37
+ - Bash
38
+ ---`);
39
+ expect(frontmatter['allowed-tools']).toEqual(['Read', 'Edit', 'Bash']);
40
+ });
41
+
42
+ it('handles quoted strings with colons', () => {
43
+ const fm = parseFrontmatter(`description: "a: b"`);
44
+ expect(fm.description).toBe('a: b');
45
+ });
46
+
47
+ it('parses numbers and booleans', () => {
48
+ const fm = parseFrontmatter(`count: 3\nbig: true\nzero: false`);
49
+ expect(fm.count).toBe(3);
50
+ expect(fm.big).toBe(true);
51
+ expect(fm.zero).toBe(false);
52
+ });
53
+
54
+ it('preserves multiline body with --- fence', () => {
55
+ const md = `---\nname: foo\ndescription: d\n---\nline 1\n\nline 2`;
56
+ const { body } = parseSkillFile(md);
57
+ expect(body).toBe('line 1\n\nline 2');
58
+ });
59
+ });
@@ -0,0 +1,11 @@
1
+ // The skill frontmatter parser now lives canonically in @moxxy/sdk
2
+ // (`parseFrontmatterFile`/`parseFrontmatter`), shared with plugin-memory so the
3
+ // previously-copy-pasted copies can't diverge. This module keeps the historical
4
+ // `parseSkillFile`/`ParsedSkillFile` names as thin re-exports.
5
+ import { parseFrontmatterFile, type ParsedFrontmatter } from '@moxxy/sdk';
6
+
7
+ export type ParsedSkillFile = ParsedFrontmatter;
8
+
9
+ export const parseSkillFile = parseFrontmatterFile;
10
+
11
+ export { parseFrontmatter } from '@moxxy/sdk';
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { LLMProvider, ProviderEvent } from '@moxxy/sdk';
3
+ import { draftSkill } from './synthesize-draft.js';
4
+
5
+ /**
6
+ * `draftSkill` streams the model's output, then unwraps any ```markdown fence
7
+ * (its private `extractMarkdownBlock`) before parsing frontmatter. These tests
8
+ * drive that unwrapping end-to-end through the public `draftSkill` entrypoint
9
+ * with a scripted provider — no real LLM — asserting `raw` is the unwrapped
10
+ * document and that the frontmatter/body split lines up.
11
+ */
12
+
13
+ const FRONTMATTER =
14
+ '---\nname: demo-skill\ndescription: A demo.\ntriggers: ["a", "b"]\nallowed-tools: [Read]\n---\n# Body\n\n1. Do the thing.\n';
15
+
16
+ class ScriptedProvider implements LLMProvider {
17
+ readonly name = 'scripted';
18
+ readonly models = [
19
+ { id: 'm1', contextWindow: 100_000, maxOutputTokens: 2000, supportsTools: true, supportsStreaming: true },
20
+ ];
21
+ constructor(private readonly events: ReadonlyArray<ProviderEvent>) {}
22
+ async *stream(): AsyncIterable<ProviderEvent> {
23
+ for (const e of this.events) yield e;
24
+ }
25
+ async countTokens(): Promise<number> {
26
+ return 0;
27
+ }
28
+ }
29
+
30
+ const deltas = (...chunks: string[]): ProviderEvent[] => [
31
+ { type: 'message_start', model: 'm1' },
32
+ ...chunks.map((delta) => ({ type: 'text_delta', delta }) as ProviderEvent),
33
+ { type: 'message_end', stopReason: 'end_turn' },
34
+ ];
35
+
36
+ async function run(events: ProviderEvent[]) {
37
+ const provider = new ScriptedProvider(events);
38
+ return draftSkill(provider, 'm1', 'make a demo skill', new AbortController().signal);
39
+ }
40
+
41
+ describe('draftSkill / extractMarkdownBlock unwrapping', () => {
42
+ it('returns the raw markdown verbatim when the model emits no code fence', async () => {
43
+ const draft = await run(deltas(FRONTMATTER));
44
+ expect(draft.raw).toBe(FRONTMATTER);
45
+ expect(draft.frontmatter.name).toBe('demo-skill');
46
+ expect(draft.body).toContain('Do the thing.');
47
+ });
48
+
49
+ it('unwraps a ```markdown fenced block (strips fence + language tag)', async () => {
50
+ const fenced = '```markdown\n' + FRONTMATTER + '```';
51
+ const draft = await run(deltas('Sure, here you go:\n', fenced));
52
+ // The leading prose and the fence markers are stripped; only the inner doc
53
+ // survives into `raw`, and parses cleanly.
54
+ expect(draft.raw).toBe(FRONTMATTER);
55
+ expect(draft.frontmatter.name).toBe('demo-skill');
56
+ });
57
+
58
+ it('unwraps a bare ``` fenced block (no language tag)', async () => {
59
+ const fenced = '```\n' + FRONTMATTER + '```';
60
+ const draft = await run(deltas(fenced));
61
+ expect(draft.raw).toBe(FRONTMATTER);
62
+ });
63
+
64
+ it('extracts the FIRST fenced block when the model wraps and then chatters', async () => {
65
+ const out = '```md\n' + FRONTMATTER + '```\nLet me know if you want changes!';
66
+ const draft = await run(deltas(out));
67
+ expect(draft.raw).toBe(FRONTMATTER);
68
+ expect(draft.raw).not.toContain('Let me know');
69
+ });
70
+
71
+ it('surfaces a provider error event as a thrown error', async () => {
72
+ await expect(
73
+ run([
74
+ { type: 'message_start', model: 'm1' },
75
+ { type: 'error', message: 'boom', retryable: false },
76
+ ]),
77
+ ).rejects.toThrow(/provider error: boom/);
78
+ });
79
+ });
@@ -0,0 +1,46 @@
1
+ import type { LLMProvider } from '@moxxy/sdk';
2
+ import { parseSkillFile } from './parse.js';
3
+
4
+ export interface DraftedSkill {
5
+ raw: string;
6
+ frontmatter: Record<string, unknown>;
7
+ body: string;
8
+ }
9
+
10
+ const DRAFT_SYSTEM_PROMPT = `You are a skill-author. Output ONLY a Markdown file with YAML frontmatter. No prose outside the Markdown. Frontmatter MUST include:
11
+ - name (kebab-case slug, <=60 chars, lowercase letters/numbers/hyphens only, starting with a letter)
12
+ - description (1 sentence, <=120 chars)
13
+ - triggers (array of 2-5 short phrases the user might say)
14
+ - allowed-tools (array of tool names, e.g. ["Read", "Edit", "Bash"])
15
+
16
+ The body is the instructions for future invocations. Keep it under 30 lines. Numbered steps preferred.`;
17
+
18
+ export async function draftSkill(
19
+ provider: LLMProvider,
20
+ model: string,
21
+ intent: string,
22
+ signal: AbortSignal,
23
+ ): Promise<DraftedSkill> {
24
+ let accumulated = '';
25
+ for await (const event of provider.stream({
26
+ model,
27
+ system: DRAFT_SYSTEM_PROMPT,
28
+ messages: [{ role: 'user', content: [{ type: 'text', text: `User intent: ${intent}` }] }],
29
+ maxTokens: 2000,
30
+ signal,
31
+ })) {
32
+ if (event.type === 'text_delta') accumulated += event.delta;
33
+ if (event.type === 'error') {
34
+ throw new Error(`synthesizeSkill: provider error: ${event.message}`);
35
+ }
36
+ }
37
+
38
+ const raw = extractMarkdownBlock(accumulated);
39
+ const { frontmatter, body } = parseSkillFile(raw);
40
+ return { raw, frontmatter: frontmatter as Record<string, unknown>, body };
41
+ }
42
+
43
+ function extractMarkdownBlock(s: string): string {
44
+ const fence = /```(?:markdown|md)?\n([\s\S]*?)```/.exec(s);
45
+ return fence ? fence[1]! : s;
46
+ }