@moxxy/sdk 0.1.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 (211) hide show
  1. package/dist/agent.d.ts +39 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +2 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/cache-strategy.d.ts +35 -0
  6. package/dist/cache-strategy.d.ts.map +1 -0
  7. package/dist/cache-strategy.js +2 -0
  8. package/dist/cache-strategy.js.map +1 -0
  9. package/dist/channel.d.ts +146 -0
  10. package/dist/channel.d.ts.map +1 -0
  11. package/dist/channel.js +2 -0
  12. package/dist/channel.js.map +1 -0
  13. package/dist/client-session.d.ts +83 -0
  14. package/dist/client-session.d.ts.map +1 -0
  15. package/dist/client-session.js +2 -0
  16. package/dist/client-session.js.map +1 -0
  17. package/dist/command.d.ts +90 -0
  18. package/dist/command.d.ts.map +1 -0
  19. package/dist/command.js +15 -0
  20. package/dist/command.js.map +1 -0
  21. package/dist/compactor-helpers.d.ts +34 -0
  22. package/dist/compactor-helpers.d.ts.map +1 -0
  23. package/dist/compactor-helpers.js +156 -0
  24. package/dist/compactor-helpers.js.map +1 -0
  25. package/dist/compactor.d.ts +20 -0
  26. package/dist/compactor.d.ts.map +1 -0
  27. package/dist/compactor.js +2 -0
  28. package/dist/compactor.js.map +1 -0
  29. package/dist/define.d.ts +51 -0
  30. package/dist/define.d.ts.map +1 -0
  31. package/dist/define.js +71 -0
  32. package/dist/define.js.map +1 -0
  33. package/dist/elision-helpers.d.ts +27 -0
  34. package/dist/elision-helpers.d.ts.map +1 -0
  35. package/dist/elision-helpers.js +109 -0
  36. package/dist/elision-helpers.js.map +1 -0
  37. package/dist/elision-state.d.ts +49 -0
  38. package/dist/elision-state.d.ts.map +1 -0
  39. package/dist/elision-state.js +144 -0
  40. package/dist/elision-state.js.map +1 -0
  41. package/dist/embedding-cache.d.ts +32 -0
  42. package/dist/embedding-cache.d.ts.map +1 -0
  43. package/dist/embedding-cache.js +0 -0
  44. package/dist/embedding-cache.js.map +1 -0
  45. package/dist/embedding.d.ts +21 -0
  46. package/dist/embedding.d.ts.map +1 -0
  47. package/dist/embedding.js +2 -0
  48. package/dist/embedding.js.map +1 -0
  49. package/dist/errors.d.ts +81 -0
  50. package/dist/errors.d.ts.map +1 -0
  51. package/dist/errors.js +241 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/events.d.ts +193 -0
  54. package/dist/events.d.ts.map +1 -0
  55. package/dist/events.js +2 -0
  56. package/dist/events.js.map +1 -0
  57. package/dist/hooks.d.ts +52 -0
  58. package/dist/hooks.d.ts.map +1 -0
  59. package/dist/hooks.js +2 -0
  60. package/dist/hooks.js.map +1 -0
  61. package/dist/ids.d.ts +18 -0
  62. package/dist/ids.d.ts.map +1 -0
  63. package/dist/ids.js +7 -0
  64. package/dist/ids.js.map +1 -0
  65. package/dist/index.d.ts +45 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +18 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/install-hints.d.ts +26 -0
  70. package/dist/install-hints.d.ts.map +1 -0
  71. package/dist/install-hints.js +15 -0
  72. package/dist/install-hints.js.map +1 -0
  73. package/dist/isolation.d.ts +125 -0
  74. package/dist/isolation.d.ts.map +1 -0
  75. package/dist/isolation.js +11 -0
  76. package/dist/isolation.js.map +1 -0
  77. package/dist/log.d.ts +11 -0
  78. package/dist/log.d.ts.map +1 -0
  79. package/dist/log.js +2 -0
  80. package/dist/log.js.map +1 -0
  81. package/dist/mode-helpers.d.ts +99 -0
  82. package/dist/mode-helpers.d.ts.map +1 -0
  83. package/dist/mode-helpers.js +368 -0
  84. package/dist/mode-helpers.js.map +1 -0
  85. package/dist/mode.d.ts +133 -0
  86. package/dist/mode.d.ts.map +1 -0
  87. package/dist/mode.js +2 -0
  88. package/dist/mode.js.map +1 -0
  89. package/dist/permission.d.ts +35 -0
  90. package/dist/permission.d.ts.map +1 -0
  91. package/dist/permission.js +2 -0
  92. package/dist/permission.js.map +1 -0
  93. package/dist/plugin.d.ts +88 -0
  94. package/dist/plugin.d.ts.map +1 -0
  95. package/dist/plugin.js +2 -0
  96. package/dist/plugin.js.map +1 -0
  97. package/dist/provider-utils.d.ts +40 -0
  98. package/dist/provider-utils.d.ts.map +1 -0
  99. package/dist/provider-utils.js +163 -0
  100. package/dist/provider-utils.js.map +1 -0
  101. package/dist/provider.d.ts +219 -0
  102. package/dist/provider.d.ts.map +1 -0
  103. package/dist/provider.js +2 -0
  104. package/dist/provider.js.map +1 -0
  105. package/dist/requirements.d.ts +22 -0
  106. package/dist/requirements.d.ts.map +1 -0
  107. package/dist/requirements.js +2 -0
  108. package/dist/requirements.js.map +1 -0
  109. package/dist/schemas.d.ts +228 -0
  110. package/dist/schemas.d.ts.map +1 -0
  111. package/dist/schemas.js +88 -0
  112. package/dist/schemas.js.map +1 -0
  113. package/dist/session-like.d.ts +111 -0
  114. package/dist/session-like.d.ts.map +1 -0
  115. package/dist/session-like.js +2 -0
  116. package/dist/session-like.js.map +1 -0
  117. package/dist/skill.d.ts +30 -0
  118. package/dist/skill.d.ts.map +1 -0
  119. package/dist/skill.js +2 -0
  120. package/dist/skill.js.map +1 -0
  121. package/dist/subagent.d.ts +45 -0
  122. package/dist/subagent.d.ts.map +1 -0
  123. package/dist/subagent.js +11 -0
  124. package/dist/subagent.js.map +1 -0
  125. package/dist/token-accounting.d.ts +73 -0
  126. package/dist/token-accounting.d.ts.map +1 -0
  127. package/dist/token-accounting.js +129 -0
  128. package/dist/token-accounting.js.map +1 -0
  129. package/dist/tool-dispatch.d.ts +19 -0
  130. package/dist/tool-dispatch.d.ts.map +1 -0
  131. package/dist/tool-dispatch.js +118 -0
  132. package/dist/tool-dispatch.js.map +1 -0
  133. package/dist/tool-gating.d.ts +32 -0
  134. package/dist/tool-gating.d.ts.map +1 -0
  135. package/dist/tool-gating.js +83 -0
  136. package/dist/tool-gating.js.map +1 -0
  137. package/dist/tool.d.ts +166 -0
  138. package/dist/tool.d.ts.map +1 -0
  139. package/dist/tool.js +2 -0
  140. package/dist/tool.js.map +1 -0
  141. package/dist/transcriber.d.ts +64 -0
  142. package/dist/transcriber.d.ts.map +1 -0
  143. package/dist/transcriber.js +21 -0
  144. package/dist/transcriber.js.map +1 -0
  145. package/dist/tunnel.d.ts +25 -0
  146. package/dist/tunnel.d.ts.map +1 -0
  147. package/dist/tunnel.js +2 -0
  148. package/dist/tunnel.js.map +1 -0
  149. package/dist/type-contracts.d.ts +2 -0
  150. package/dist/type-contracts.d.ts.map +1 -0
  151. package/dist/type-contracts.js +4 -0
  152. package/dist/type-contracts.js.map +1 -0
  153. package/dist/types.test-d.d.ts +2 -0
  154. package/dist/types.test-d.d.ts.map +1 -0
  155. package/dist/types.test-d.js +22 -0
  156. package/dist/types.test-d.js.map +1 -0
  157. package/dist/view-renderer.d.ts +97 -0
  158. package/dist/view-renderer.d.ts.map +1 -0
  159. package/dist/view-renderer.js +72 -0
  160. package/dist/view-renderer.js.map +1 -0
  161. package/package.json +38 -0
  162. package/src/agent.ts +38 -0
  163. package/src/cache-strategy.ts +39 -0
  164. package/src/channel.ts +158 -0
  165. package/src/client-session.ts +88 -0
  166. package/src/command.ts +86 -0
  167. package/src/compactor-helpers.test.ts +206 -0
  168. package/src/compactor-helpers.ts +163 -0
  169. package/src/compactor.ts +20 -0
  170. package/src/define.test.ts +76 -0
  171. package/src/define.ts +113 -0
  172. package/src/elision-helpers.ts +143 -0
  173. package/src/elision-state.ts +170 -0
  174. package/src/embedding-cache.ts +0 -0
  175. package/src/embedding.ts +22 -0
  176. package/src/errors.test.ts +176 -0
  177. package/src/errors.ts +308 -0
  178. package/src/events.ts +250 -0
  179. package/src/hooks.ts +54 -0
  180. package/src/ids.ts +16 -0
  181. package/src/index.ts +304 -0
  182. package/src/install-hints.ts +43 -0
  183. package/src/isolation.ts +148 -0
  184. package/src/log.ts +11 -0
  185. package/src/loop-helpers.test.ts +78 -0
  186. package/src/mode-helpers.ts +483 -0
  187. package/src/mode.ts +139 -0
  188. package/src/package-root.test.ts +40 -0
  189. package/src/permission.ts +37 -0
  190. package/src/plugin.ts +92 -0
  191. package/src/provider-utils.test.ts +84 -0
  192. package/src/provider-utils.ts +171 -0
  193. package/src/provider.ts +195 -0
  194. package/src/requirements.ts +35 -0
  195. package/src/schemas.test.ts +106 -0
  196. package/src/schemas.ts +97 -0
  197. package/src/session-like.ts +118 -0
  198. package/src/skill.ts +34 -0
  199. package/src/subagent.ts +46 -0
  200. package/src/token-accounting.test.ts +93 -0
  201. package/src/token-accounting.ts +202 -0
  202. package/src/token-efficiency.test.ts +459 -0
  203. package/src/tool-dispatch.ts +137 -0
  204. package/src/tool-gating.ts +107 -0
  205. package/src/tool.ts +175 -0
  206. package/src/transcriber.ts +71 -0
  207. package/src/tunnel.ts +26 -0
  208. package/src/type-contracts.ts +4 -0
  209. package/src/types.test-d.ts +29 -0
  210. package/src/view-renderer.test.ts +90 -0
  211. package/src/view-renderer.ts +156 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { AgentDef } from './agent.js';
2
+ import type { CacheStrategyDef } from './cache-strategy.js';
3
+ import type { ChannelDef } from './channel.js';
4
+ import type { CommandDef } from './command.js';
5
+ import type { CompactorDef } from './compactor.js';
6
+ import type { LifecycleHooks } from './hooks.js';
7
+ import type { ModeDef } from './mode.js';
8
+ import type { ProviderDef } from './provider.js';
9
+ import type { MoxxyRequirement } from './requirements.js';
10
+ import type { ToolDef } from './tool.js';
11
+ import type { TranscriberDef } from './transcriber.js';
12
+ import type { ViewRendererDef } from './view-renderer.js';
13
+ import type { TunnelProviderDef } from './tunnel.js';
14
+
15
+ export type PluginKind = 'tools' | 'provider' | 'mode' | 'compactor' | 'cache-strategy' | 'view-renderer' | 'tunnel-provider' | 'mcp' | 'cli' | 'channel' | 'hooks' | 'agent' | 'command' | 'transcriber';
16
+
17
+ export interface PluginSpec {
18
+ readonly name: string;
19
+ readonly version?: string;
20
+ readonly tools?: ReadonlyArray<ToolDef>;
21
+ readonly providers?: ReadonlyArray<ProviderDef>;
22
+ readonly modes?: ReadonlyArray<ModeDef>;
23
+ readonly compactors?: ReadonlyArray<CompactorDef>;
24
+ /**
25
+ * Prompt-caching strategies contributed by the plugin. One is active per
26
+ * session (selected via `session.cacheStrategies.setActive(name)`); the
27
+ * active strategy decides where cache breakpoints go for each provider call.
28
+ */
29
+ readonly cacheStrategies?: ReadonlyArray<CacheStrategyDef>;
30
+ /**
31
+ * View-spec renderers contributed by the plugin. One is active per session
32
+ * (selected via `session.viewRenderers.setActive(name)`); the active renderer
33
+ * parses the agent's view-spec into a validated AST for `present_view`. A
34
+ * default renderer ships with core, so this is only for replacing it.
35
+ */
36
+ readonly viewRenderers?: ReadonlyArray<ViewRendererDef>;
37
+ /**
38
+ * Tunnel providers that expose the local web surface publicly (e.g.
39
+ * cloudflared). One active per session; core seeds a `localhost` no-op.
40
+ */
41
+ readonly tunnelProviders?: ReadonlyArray<TunnelProviderDef>;
42
+ readonly channels?: ReadonlyArray<ChannelDef>;
43
+ /**
44
+ * Speech-to-text backends contributed by the plugin. Selected by name via
45
+ * `session.transcribers.setActive(name)`; channels with audio input use
46
+ * `session.transcribers.getActive()` to convert bytes → transcript when
47
+ * the active provider does not advertise `supportsAudio`.
48
+ */
49
+ readonly transcribers?: ReadonlyArray<TranscriberDef>;
50
+ /**
51
+ * Typed subagent kinds the plugin contributes. Each becomes
52
+ * dispatchable as `dispatch_agent({ agentType: <name>, ... })`.
53
+ * When NO plugin registers any agents (and no plugin registers the
54
+ * dispatch tool itself), the model has no subagent capability and
55
+ * the system degrades to the normal single-loop flow.
56
+ */
57
+ readonly agents?: ReadonlyArray<AgentDef>;
58
+ /**
59
+ * Slash commands contributed to every channel — the TUI's slash
60
+ * menu, the Telegram bot's command list, and any future channel
61
+ * that consumes `session.commands`. Use this for actions that make
62
+ * sense regardless of UI (`/info`, `/clear`, custom domain commands
63
+ * like `/deploy`); leave channel-specific UI commands (overlay
64
+ * pickers, raw-mode toggles) inside the channel itself.
65
+ */
66
+ readonly commands?: ReadonlyArray<CommandDef>;
67
+ readonly hooks?: LifecycleHooks;
68
+ readonly skillsDir?: string;
69
+ }
70
+
71
+ export interface Plugin extends PluginSpec {
72
+ readonly __moxxy: 'plugin';
73
+ readonly version: string;
74
+ }
75
+
76
+ export interface PluginManifest {
77
+ readonly entry: string;
78
+ readonly kind?: PluginKind | ReadonlyArray<PluginKind>;
79
+ readonly skills?: string;
80
+ }
81
+
82
+ export interface ResolvedPluginManifest extends PluginManifest {
83
+ readonly packageName: string;
84
+ readonly packageVersion: string;
85
+ readonly packagePath: string;
86
+ /**
87
+ * Requirements declared at `package.json#moxxy.requirements`. Statically
88
+ * authored — never derived from code. Drives plugin toposort and the
89
+ * pre-load readiness gate.
90
+ */
91
+ readonly requirements?: ReadonlyArray<MoxxyRequirement>;
92
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { zodToJsonSchema } from './provider-utils.js';
4
+
5
+ /**
6
+ * Regression coverage for the codex `/responses` 400 we hit when a tool's
7
+ * input schema was `z.object(...).and(z.object(...).refine(...))`. The old
8
+ * converter fell through to a bare `{ type: 'object' }` (no `properties`
9
+ * key), and Codex rejects that. These tests pin every wrapper kind so the
10
+ * fix doesn't silently regress.
11
+ */
12
+ describe('zodToJsonSchema', () => {
13
+ it('unwraps ZodEffects (.refine) to the underlying object', () => {
14
+ const schema = z
15
+ .object({ a: z.string(), b: z.number() })
16
+ .refine((v) => v.a.length > 0);
17
+ const out = zodToJsonSchema(schema) as Record<string, unknown>;
18
+ expect(out.type).toBe('object');
19
+ expect(out.properties).toEqual({
20
+ a: { type: 'string' },
21
+ b: { type: 'number' },
22
+ });
23
+ expect(out.required).toEqual(expect.arrayContaining(['a', 'b']));
24
+ });
25
+
26
+ it('merges ZodIntersection of two ZodObjects into one object schema', () => {
27
+ const left = z.object({ name: z.string() });
28
+ const right = z.object({ count: z.number() });
29
+ const out = zodToJsonSchema(left.and(right)) as Record<string, unknown>;
30
+ expect(out.type).toBe('object');
31
+ expect(out.properties).toEqual({
32
+ name: { type: 'string' },
33
+ count: { type: 'number' },
34
+ });
35
+ expect(out.required).toEqual(expect.arrayContaining(['name', 'count']));
36
+ });
37
+
38
+ it('matches the scheduler tool shape (object .and(object.refine))', () => {
39
+ // The actual shape that triggered the codex 400.
40
+ const cronOrTimestamp = z
41
+ .object({ cron: z.string().optional(), runAt: z.number().optional() })
42
+ .refine((v) => !!v.cron || v.runAt !== undefined);
43
+ const schema = z
44
+ .object({ name: z.string(), prompt: z.string() })
45
+ .and(cronOrTimestamp);
46
+ const out = zodToJsonSchema(schema) as Record<string, unknown>;
47
+ expect(out.type).toBe('object');
48
+ expect(out.properties).toBeDefined();
49
+ expect(out.properties).toMatchObject({
50
+ name: { type: 'string' },
51
+ prompt: { type: 'string' },
52
+ cron: { type: 'string' },
53
+ runAt: { type: 'number' },
54
+ });
55
+ });
56
+
57
+ it('unwraps ZodOptional / ZodDefault / ZodNullable', () => {
58
+ expect(zodToJsonSchema(z.string().optional())).toEqual({ type: 'string' });
59
+ expect(zodToJsonSchema(z.number().default(0))).toEqual({ type: 'number' });
60
+ expect(zodToJsonSchema(z.string().nullable())).toEqual({ type: 'string' });
61
+ });
62
+
63
+ it('converts ZodEnum to string + enum list', () => {
64
+ const out = zodToJsonSchema(z.enum(['a', 'b', 'c'])) as Record<string, unknown>;
65
+ expect(out).toEqual({ type: 'string', enum: ['a', 'b', 'c'] });
66
+ });
67
+
68
+ it('converts ZodUnion via anyOf', () => {
69
+ const out = zodToJsonSchema(z.union([z.string(), z.number()])) as Record<string, unknown>;
70
+ expect(out.anyOf).toEqual([{ type: 'string' }, { type: 'number' }]);
71
+ });
72
+
73
+ it('unknown schemas fall back to an object with explicit properties', () => {
74
+ // Synthetic "unknown" wrapper — uses a typeName the converter doesn't know.
75
+ const out = zodToJsonSchema({ _def: { typeName: 'ZodNeverHeardOfIt' } }) as Record<
76
+ string,
77
+ unknown
78
+ >;
79
+ // The key fix: properties is always present, even on the catch-all path,
80
+ // so strict validators (codex /responses) don't reject the tool.
81
+ expect(out).toHaveProperty('type', 'object');
82
+ expect(out).toHaveProperty('properties');
83
+ });
84
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Small helpers that provider plugins share. Lives in the SDK because plugins
3
+ * are only allowed to depend on @moxxy/sdk, not core.
4
+ */
5
+
6
+ import { classifyNetworkError } from './errors.js';
7
+
8
+ /** Canonical stop reasons emitted on `message_end` events. */
9
+ export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error';
10
+
11
+ /**
12
+ * Heuristic: should a provider request retry on this error? Used by stream
13
+ * modes to attach a `retryable: boolean` flag to the `error` event so callers
14
+ * (or the loop strategy) can decide whether to back off and try again.
15
+ *
16
+ * Identical implementation across plugin-provider-anthropic and -openai;
17
+ * hoisted here so a new provider plugin can stay consistent.
18
+ */
19
+ export function isRetryableError(err: unknown): boolean {
20
+ if (!(err instanceof Error)) return false;
21
+ // MoxxyError-classified ones carry the verdict in the code.
22
+ const code = (err as { code?: unknown }).code;
23
+ if (typeof code === 'string') {
24
+ if (code === 'PROVIDER_RATE_LIMITED' || code === 'PROVIDER_SERVER_ERROR') return true;
25
+ if (code === 'NETWORK_TIMEOUT' || code === 'NETWORK_UNREACHABLE') return true;
26
+ }
27
+ const msg = err.message.toLowerCase();
28
+ if (msg.includes('rate_limit') || msg.includes('429') || msg.includes('overloaded')) return true;
29
+ if (msg.includes('econnreset') || msg.includes('etimedout') || msg.includes('network')) return true;
30
+ return false;
31
+ }
32
+
33
+ /**
34
+ * Build a friendly error event for `ProviderEvent` streams. Tries to classify
35
+ * the cause as a network failure (turning Node's "fetch failed" into something
36
+ * actionable) before falling back to the raw message. Providers' catch blocks
37
+ * yield the result via:
38
+ * yield { type: 'error', ...toFriendlyError(err, { provider: this.name }) };
39
+ */
40
+ export function toFriendlyError(
41
+ err: unknown,
42
+ ctx: { readonly url?: string; readonly provider?: string } = {},
43
+ ): { message: string; retryable: boolean } {
44
+ const classified = classifyNetworkError(err, ctx);
45
+ if (classified) {
46
+ return {
47
+ message: classified.message + (classified.hint ? ` — ${classified.hint}` : ''),
48
+ retryable: isRetryableError(classified),
49
+ };
50
+ }
51
+ return {
52
+ message: err instanceof Error ? err.message : String(err),
53
+ retryable: isRetryableError(err),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Best-effort zod → JSON-schema conversion for provider tool definitions.
59
+ * Most callers want the basic object/string/number/boolean/array coverage;
60
+ * for richer schemas, plugins can layer `zod-to-json-schema`.
61
+ *
62
+ * The `as unknown as { ... }` casts below probe zod's `_def` internals
63
+ * (`shape`, `type`) which aren't part of zod's public typed surface but are
64
+ * stable enough across versions to rely on for this best-effort path.
65
+ */
66
+ export function zodToJsonSchema(schema: unknown): unknown {
67
+ const s = schema as { _def?: { typeName?: string }; toJSON?: () => unknown };
68
+ // Only honor a pre-serialized schema's `toJSON` for NON-zod objects (a plain
69
+ // JSON-schema object passed through). A real zod schema (has `_def`) MUST go
70
+ // through the unwrap below — short-circuiting on a zod `toJSON` (some versions
71
+ // add one) skips the ZodEffects/intersection handling and yields a
72
+ // properties-less object schema that providers like Codex reject.
73
+ if (!s._def && typeof s.toJSON === 'function') return s.toJSON();
74
+ const def = s._def;
75
+ const typeName = def?.typeName;
76
+
77
+ // ZodEffects (`.refine`, `.transform`, `.superRefine`) is a wrapper —
78
+ // the inner `schema` field holds the real type. Skipping the unwrap
79
+ // leaves us with a properties-less object schema, which providers like
80
+ // Codex's /responses validator reject ("object schema missing properties").
81
+ if (typeName === 'ZodEffects') {
82
+ const inner = (def as unknown as { schema: unknown }).schema;
83
+ return zodToJsonSchema(inner);
84
+ }
85
+ // ZodOptional / ZodNullable / ZodDefault / ZodBranded / ZodReadonly all
86
+ // wrap an inner schema we want to unwrap for JSON-schema purposes.
87
+ if (
88
+ typeName === 'ZodOptional' ||
89
+ typeName === 'ZodNullable' ||
90
+ typeName === 'ZodDefault' ||
91
+ typeName === 'ZodBranded' ||
92
+ typeName === 'ZodReadonly'
93
+ ) {
94
+ const inner = (def as unknown as { innerType: unknown }).innerType;
95
+ return zodToJsonSchema(inner);
96
+ }
97
+ // `.and(other)` produces a ZodIntersection. The standard JSON-schema
98
+ // representation is `allOf`, but strict validators (Codex again) want
99
+ // each branch to be a complete object schema. Easiest path that satisfies
100
+ // them: when both branches reduce to object schemas, merge properties
101
+ // and required lists into one object. Fall back to `allOf` otherwise.
102
+ if (typeName === 'ZodIntersection') {
103
+ const intersection = def as unknown as { left: unknown; right: unknown };
104
+ const left = zodToJsonSchema(intersection.left) as Record<string, unknown>;
105
+ const right = zodToJsonSchema(intersection.right) as Record<string, unknown>;
106
+ if (
107
+ left &&
108
+ right &&
109
+ left.type === 'object' &&
110
+ right.type === 'object' &&
111
+ left.properties &&
112
+ right.properties
113
+ ) {
114
+ return {
115
+ type: 'object',
116
+ properties: { ...(left.properties as object), ...(right.properties as object) },
117
+ required: Array.from(
118
+ new Set([
119
+ ...((left.required as ReadonlyArray<string>) ?? []),
120
+ ...((right.required as ReadonlyArray<string>) ?? []),
121
+ ]),
122
+ ),
123
+ };
124
+ }
125
+ return { allOf: [left, right] };
126
+ }
127
+ if (typeName === 'ZodObject') {
128
+ // zod's ZodObject._def has a `shape()` thunk returning the field map.
129
+ const shape = (def as unknown as { shape: () => Record<string, unknown> }).shape();
130
+ const properties: Record<string, unknown> = {};
131
+ const required: string[] = [];
132
+ for (const [key, value] of Object.entries(shape)) {
133
+ properties[key] = zodToJsonSchema(value);
134
+ const isOptional = (value as { isOptional?: () => boolean }).isOptional?.();
135
+ if (!isOptional) required.push(key);
136
+ }
137
+ return { type: 'object', properties, required };
138
+ }
139
+ if (typeName === 'ZodString') return { type: 'string' };
140
+ if (typeName === 'ZodNumber') return { type: 'number' };
141
+ if (typeName === 'ZodBoolean') return { type: 'boolean' };
142
+ if (typeName === 'ZodEnum') {
143
+ const values = (def as unknown as { values: ReadonlyArray<string> }).values;
144
+ return { type: 'string', enum: [...values] };
145
+ }
146
+ if (typeName === 'ZodNativeEnum') {
147
+ const values = Object.values(
148
+ (def as unknown as { values: Record<string, string | number> }).values,
149
+ );
150
+ const allString = values.every((v) => typeof v === 'string');
151
+ return { type: allString ? 'string' : 'number', enum: values };
152
+ }
153
+ if (typeName === 'ZodLiteral') {
154
+ const value = (def as unknown as { value: string | number | boolean | null }).value;
155
+ const t = value === null ? 'null' : typeof value;
156
+ return { type: t, enum: [value] };
157
+ }
158
+ if (typeName === 'ZodUnion' || typeName === 'ZodDiscriminatedUnion') {
159
+ const options = (def as unknown as { options: ReadonlyArray<unknown> }).options;
160
+ return { anyOf: options.map((o) => zodToJsonSchema(o)) };
161
+ }
162
+ if (typeName === 'ZodArray') {
163
+ // ZodArray._def.type is the element schema.
164
+ const items = zodToJsonSchema((def as unknown as { type: unknown }).type);
165
+ return { type: 'array', items };
166
+ }
167
+ // Truly unknown — fall back to a permissive "any object" but spell out
168
+ // `properties: {}` and `additionalProperties: true` so strict validators
169
+ // accept the schema instead of bailing on a missing `properties` field.
170
+ return { type: 'object', properties: {}, additionalProperties: true };
171
+ }
@@ -0,0 +1,195 @@
1
+ import type { ToolDef } from './tool.js';
2
+
3
+ export interface ProviderMessage {
4
+ readonly role: 'system' | 'user' | 'assistant' | 'tool_result';
5
+ readonly content: ReadonlyArray<ContentBlock>;
6
+ }
7
+
8
+ export type ContentBlock =
9
+ | { readonly type: 'text'; readonly text: string }
10
+ | { readonly type: 'tool_use'; readonly id: string; readonly name: string; readonly input: unknown }
11
+ | { readonly type: 'tool_result'; readonly toolUseId: string; readonly content: string; readonly isError?: boolean }
12
+ | { readonly type: 'image'; readonly mediaType: string; readonly data: string }
13
+ | { readonly type: 'audio'; readonly mediaType: string; readonly data: string };
14
+
15
+ /**
16
+ * Provider-neutral instruction for where a prompt-cache breakpoint should be
17
+ * placed. A {@link CacheStrategyDef} emits these; providers that support
18
+ * caching (e.g. Anthropic via `cache_control`) translate them into their
19
+ * native marker, and providers that don't simply ignore them.
20
+ *
21
+ * `tools` / `system` mark the end of those (session-stable) regions;
22
+ * `{ messageIndex }` marks the end of the message at that index in the
23
+ * request's `messages` array (used for the rolling prefix breakpoint).
24
+ * Anthropic honors at most 4 breakpoints per request.
25
+ */
26
+ export interface CacheHint {
27
+ readonly target: 'tools' | 'system' | { readonly messageIndex: number };
28
+ }
29
+
30
+ export interface ProviderRequest {
31
+ readonly model: string;
32
+ readonly system?: string;
33
+ readonly messages: ReadonlyArray<ProviderMessage>;
34
+ readonly tools?: ReadonlyArray<ToolDef>;
35
+ readonly maxTokens?: number;
36
+ readonly temperature?: number;
37
+ readonly signal?: AbortSignal;
38
+ /** Where to place prompt-cache breakpoints. Set by the active CacheStrategy. */
39
+ readonly cacheHints?: ReadonlyArray<CacheHint>;
40
+ }
41
+
42
+ export type ProviderEvent =
43
+ | { readonly type: 'message_start'; readonly model: string }
44
+ | { readonly type: 'text_delta'; readonly delta: string }
45
+ | { readonly type: 'tool_use_start'; readonly id: string; readonly name: string }
46
+ | { readonly type: 'tool_use_delta'; readonly id: string; readonly partialInput: string }
47
+ | { readonly type: 'tool_use_end'; readonly id: string; readonly input: unknown }
48
+ | { readonly type: 'message_end'; readonly stopReason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error'; readonly usage?: TokenUsage }
49
+ | { readonly type: 'error'; readonly message: string; readonly retryable: boolean };
50
+
51
+ export interface TokenUsage {
52
+ readonly inputTokens: number;
53
+ readonly outputTokens: number;
54
+ readonly cacheReadTokens?: number;
55
+ readonly cacheCreationTokens?: number;
56
+ }
57
+
58
+ export interface ModelDescriptor {
59
+ readonly id: string;
60
+ readonly contextWindow: number;
61
+ readonly maxOutputTokens?: number;
62
+ readonly supportsTools: boolean;
63
+ readonly supportsStreaming: boolean;
64
+ /**
65
+ * Whether this model accepts `image` ContentBlocks in user messages.
66
+ * Channels gate image attachments on this flag — if a user drops an
67
+ * image while a non-vision model is active, the channel either
68
+ * refuses or warns instead of silently dropping the bytes.
69
+ */
70
+ readonly supportsImages?: boolean;
71
+ /**
72
+ * Whether this model accepts `audio` ContentBlocks in user messages
73
+ * (GPT-4o, Gemini-Live-class models). When false, channels with audio
74
+ * input route through the session's active `Transcriber` and forward
75
+ * the transcript as text instead.
76
+ */
77
+ readonly supportsAudio?: boolean;
78
+ }
79
+
80
+ export interface LLMProvider {
81
+ readonly name: string;
82
+ readonly models: ReadonlyArray<ModelDescriptor>;
83
+ stream(req: ProviderRequest): AsyncIterable<ProviderEvent>;
84
+ countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number>;
85
+ }
86
+
87
+ export type ProviderKeyValidation =
88
+ | { readonly ok: true }
89
+ | { readonly ok: false; readonly message: string };
90
+
91
+ /**
92
+ * Minimal vault interface exposed to provider auth flows. Implementations
93
+ * (typically `@moxxy/plugin-vault`) supply encrypted storage; the auth
94
+ * descriptor doesn't need anything richer, so we keep the contract small
95
+ * and SDK-Node-free.
96
+ */
97
+ export interface ProviderVault {
98
+ get(key: string): Promise<string | null>;
99
+ set(key: string, value: string, tags?: ReadonlyArray<string>): Promise<void>;
100
+ delete?(key: string): Promise<boolean>;
101
+ }
102
+
103
+ /**
104
+ * Runtime supplied to a provider's OAuth `login(ctx)` callback. The host
105
+ * (e.g. `moxxy init`, `moxxy login <provider>`) constructs this and hands
106
+ * it off; the provider plugin runs the flow end-to-end and persists
107
+ * credentials via `ctx.vault`.
108
+ */
109
+ export interface ProviderAuthContext {
110
+ readonly vault: ProviderVault;
111
+ /**
112
+ * True when there is no usable browser or interactive TTY. OAuth flows
113
+ * should fall back to device-code (or equivalent) in this mode rather
114
+ * than spawning a local callback server / opening a browser.
115
+ */
116
+ readonly headless: boolean;
117
+ /**
118
+ * Progress-message sink. The host wires this to its preferred renderer
119
+ * (clack `log.*`, plain stdout, …) so providers don't have to know
120
+ * whether they're running inside a wizard or a one-shot command.
121
+ */
122
+ readonly write: (chunk: string) => void;
123
+ }
124
+
125
+ export interface ProviderOAuthResult {
126
+ /** Human-readable account identifier shown in the success message. */
127
+ readonly accountId?: string | null;
128
+ /** UNIX-ms expiry of the persisted credential; surfaced to users. */
129
+ readonly expiresAt?: number;
130
+ }
131
+
132
+ /**
133
+ * Self-describing auth metadata a provider plugin attaches to its
134
+ * `ProviderDef`. Lets the CLI's setup wizard and `moxxy login` operate
135
+ * generically over any installed provider — no CLI-side branch table.
136
+ *
137
+ * `apiKey` : the host prompts for a key and calls `validateKey` (if any).
138
+ * `oauth` : the host hands the provider a `ProviderAuthContext`; the
139
+ * provider drives the full OAuth dance, including any local
140
+ * callback server, and persists tokens to `ctx.vault`.
141
+ */
142
+ export type ProviderAuthDescriptor =
143
+ | {
144
+ readonly kind: 'apiKey';
145
+ /** Canonical env-var name (e.g. `ANTHROPIC_API_KEY`). Inferred when omitted. */
146
+ readonly envVar?: string;
147
+ /** Short hint shown next to the prompt (e.g. "starts with `sk-ant-`"). */
148
+ readonly hint?: string;
149
+ }
150
+ | {
151
+ readonly kind: 'oauth';
152
+ /** Human-readable name of the upstream service (e.g. "ChatGPT Pro/Plus"). */
153
+ readonly serviceName?: string;
154
+ /**
155
+ * Drive the OAuth flow and persist credentials. Throws on failure /
156
+ * user cancellation; the host typically offers a retry prompt.
157
+ */
158
+ login(ctx: ProviderAuthContext): Promise<ProviderOAuthResult>;
159
+ /**
160
+ * Optional logout — remove persisted credentials from the vault.
161
+ * Returns true if anything was removed, false if there was nothing
162
+ * stored. Used by `moxxy login logout <provider>`.
163
+ */
164
+ logout?(ctx: ProviderAuthContext): Promise<boolean>;
165
+ /**
166
+ * Optional status probe — returns a brief description of the stored
167
+ * credential, or null if none. Used by `moxxy login status`.
168
+ */
169
+ status?(ctx: ProviderAuthContext): Promise<ProviderOAuthStatus | null>;
170
+ };
171
+
172
+ export interface ProviderOAuthStatus {
173
+ readonly accountId?: string | null;
174
+ readonly expiresAt?: number;
175
+ /** Vault key the credentials are stored under (informational). */
176
+ readonly vaultKey?: string;
177
+ }
178
+
179
+ export interface ProviderDef {
180
+ readonly name: string;
181
+ readonly models: ReadonlyArray<ModelDescriptor>;
182
+ createClient(config: Record<string, unknown>): LLMProvider;
183
+ /**
184
+ * Optional check that the given key is actually accepted by the vendor.
185
+ * Implementations should be cheap (a free metadata call or a 1-token
186
+ * completion). Used by `moxxy init` to verify keys before persisting.
187
+ */
188
+ validateKey?(apiKey: string): Promise<ProviderKeyValidation>;
189
+ /**
190
+ * Optional auth descriptor. When omitted, the host treats the provider
191
+ * as `{ kind: 'apiKey' }` — i.e. prompt for a key, call `validateKey`
192
+ * if defined, store under the canonical vault entry.
193
+ */
194
+ readonly auth?: ProviderAuthDescriptor;
195
+ }
@@ -0,0 +1,35 @@
1
+ export type RequirementKind =
2
+ | 'plugin'
3
+ | 'provider'
4
+ | 'tool'
5
+ | 'transcriber'
6
+ | 'mode'
7
+ | 'compactor'
8
+ | 'channel'
9
+ | 'agent'
10
+ | 'command'
11
+ | 'runtime';
12
+
13
+ export type RequirementState = 'registered' | 'active' | 'ready';
14
+
15
+ export interface MoxxyRequirement {
16
+ readonly kind: RequirementKind;
17
+ readonly name: string;
18
+ readonly state?: RequirementState;
19
+ readonly version?: string;
20
+ readonly optional?: boolean;
21
+ readonly reason?: string;
22
+ readonly hint?: string;
23
+ }
24
+
25
+ export interface RequirementIssue {
26
+ readonly requirement: MoxxyRequirement;
27
+ readonly code: 'missing' | 'inactive' | 'not_ready' | 'version_mismatch';
28
+ readonly message: string;
29
+ readonly hint?: string;
30
+ }
31
+
32
+ export interface RequirementCheck {
33
+ readonly ready: boolean;
34
+ readonly issues: ReadonlyArray<RequirementIssue>;
35
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { moxxyPackageSchema, pluginManifestSchema, skillFrontmatterSchema } from './schemas.js';
3
+
4
+ describe('skillFrontmatterSchema', () => {
5
+ it('accepts minimal valid frontmatter', () => {
6
+ const parsed = skillFrontmatterSchema.parse({
7
+ name: 'refactor-component',
8
+ description: 'Splits a large React component into smaller files.',
9
+ });
10
+ expect(parsed.name).toBe('refactor-component');
11
+ });
12
+
13
+ it('accepts optional fields', () => {
14
+ const parsed = skillFrontmatterSchema.parse({
15
+ name: 'deploy',
16
+ description: 'Deploy to staging.',
17
+ triggers: ['deploy', 'ship'],
18
+ 'allowed-tools': ['Bash'],
19
+ version: '1.0.0',
20
+ tags: ['ops'],
21
+ });
22
+ expect(parsed.triggers).toEqual(['deploy', 'ship']);
23
+ });
24
+
25
+ it('rejects non-slug names', () => {
26
+ expect(() =>
27
+ skillFrontmatterSchema.parse({ name: 'Refactor Component', description: 'x' }),
28
+ ).toThrow(/slug-like/);
29
+ expect(() =>
30
+ skillFrontmatterSchema.parse({ name: '-bad', description: 'x' }),
31
+ ).toThrow();
32
+ });
33
+
34
+ it('rejects names exceeding length cap', () => {
35
+ expect(() =>
36
+ skillFrontmatterSchema.parse({ name: 'a'.repeat(121), description: 'x' }),
37
+ ).toThrow();
38
+ });
39
+
40
+ it('rejects empty description', () => {
41
+ expect(() => skillFrontmatterSchema.parse({ name: 'x', description: '' })).toThrow();
42
+ });
43
+ });
44
+
45
+ describe('pluginManifestSchema', () => {
46
+ it('accepts minimal manifest', () => {
47
+ const parsed = pluginManifestSchema.parse({ entry: './src/index.ts' });
48
+ expect(parsed.entry).toBe('./src/index.ts');
49
+ });
50
+
51
+ it('accepts kind as scalar or array', () => {
52
+ expect(pluginManifestSchema.parse({ entry: 'a', kind: 'tools' }).kind).toBe('tools');
53
+ expect(pluginManifestSchema.parse({ entry: 'a', kind: ['tools', 'hooks'] }).kind).toEqual([
54
+ 'tools',
55
+ 'hooks',
56
+ ]);
57
+ });
58
+
59
+ it('accepts every public plugin kind, including transcriber/agent/command', () => {
60
+ expect(pluginManifestSchema.parse({ entry: 'a', kind: 'transcriber' }).kind).toBe(
61
+ 'transcriber',
62
+ );
63
+ expect(pluginManifestSchema.parse({ entry: 'a', kind: ['agent', 'command'] }).kind).toEqual([
64
+ 'agent',
65
+ 'command',
66
+ ]);
67
+ });
68
+
69
+ it('rejects unknown kind', () => {
70
+ expect(() => pluginManifestSchema.parse({ entry: 'a', kind: 'weird' })).toThrow();
71
+ });
72
+ });
73
+
74
+ describe('moxxyPackageSchema', () => {
75
+ it('accepts the full moxxy package block with plugin + requirements', () => {
76
+ const parsed = moxxyPackageSchema.parse({
77
+ plugin: { entry: './dist/index.js', kind: 'transcriber' },
78
+ requirements: [
79
+ {
80
+ kind: 'plugin',
81
+ name: '@moxxy/plugin-provider-openai-codex',
82
+ state: 'registered',
83
+ hint: 'Enable @moxxy/plugin-provider-openai-codex.',
84
+ },
85
+ ],
86
+ });
87
+
88
+ expect(parsed.plugin?.entry).toBe('./dist/index.js');
89
+ expect(parsed.requirements).toEqual([
90
+ {
91
+ kind: 'plugin',
92
+ name: '@moxxy/plugin-provider-openai-codex',
93
+ state: 'registered',
94
+ hint: 'Enable @moxxy/plugin-provider-openai-codex.',
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it('accepts a moxxy block with only requirements (no plugin entry)', () => {
100
+ const parsed = moxxyPackageSchema.parse({
101
+ requirements: [{ kind: 'plugin', name: 'base' }],
102
+ });
103
+ expect(parsed.plugin).toBeUndefined();
104
+ expect(parsed.requirements).toHaveLength(1);
105
+ });
106
+ });