@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.
- package/dist/agent.d.ts +39 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -0
- package/dist/cache-strategy.d.ts +35 -0
- package/dist/cache-strategy.d.ts.map +1 -0
- package/dist/cache-strategy.js +2 -0
- package/dist/cache-strategy.js.map +1 -0
- package/dist/channel.d.ts +146 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +2 -0
- package/dist/channel.js.map +1 -0
- package/dist/client-session.d.ts +83 -0
- package/dist/client-session.d.ts.map +1 -0
- package/dist/client-session.js +2 -0
- package/dist/client-session.js.map +1 -0
- package/dist/command.d.ts +90 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +15 -0
- package/dist/command.js.map +1 -0
- package/dist/compactor-helpers.d.ts +34 -0
- package/dist/compactor-helpers.d.ts.map +1 -0
- package/dist/compactor-helpers.js +156 -0
- package/dist/compactor-helpers.js.map +1 -0
- package/dist/compactor.d.ts +20 -0
- package/dist/compactor.d.ts.map +1 -0
- package/dist/compactor.js +2 -0
- package/dist/compactor.js.map +1 -0
- package/dist/define.d.ts +51 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +71 -0
- package/dist/define.js.map +1 -0
- package/dist/elision-helpers.d.ts +27 -0
- package/dist/elision-helpers.d.ts.map +1 -0
- package/dist/elision-helpers.js +109 -0
- package/dist/elision-helpers.js.map +1 -0
- package/dist/elision-state.d.ts +49 -0
- package/dist/elision-state.d.ts.map +1 -0
- package/dist/elision-state.js +144 -0
- package/dist/elision-state.js.map +1 -0
- package/dist/embedding-cache.d.ts +32 -0
- package/dist/embedding-cache.d.ts.map +1 -0
- package/dist/embedding-cache.js +0 -0
- package/dist/embedding-cache.js.map +1 -0
- package/dist/embedding.d.ts +21 -0
- package/dist/embedding.d.ts.map +1 -0
- package/dist/embedding.js +2 -0
- package/dist/embedding.js.map +1 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +241 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +193 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/hooks.d.ts +52 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -0
- package/dist/ids.d.ts +18 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +7 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/install-hints.d.ts +26 -0
- package/dist/install-hints.d.ts.map +1 -0
- package/dist/install-hints.js +15 -0
- package/dist/install-hints.js.map +1 -0
- package/dist/isolation.d.ts +125 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +11 -0
- package/dist/isolation.js.map +1 -0
- package/dist/log.d.ts +11 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +2 -0
- package/dist/log.js.map +1 -0
- package/dist/mode-helpers.d.ts +99 -0
- package/dist/mode-helpers.d.ts.map +1 -0
- package/dist/mode-helpers.js +368 -0
- package/dist/mode-helpers.js.map +1 -0
- package/dist/mode.d.ts +133 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +2 -0
- package/dist/mode.js.map +1 -0
- package/dist/permission.d.ts +35 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +2 -0
- package/dist/permission.js.map +1 -0
- package/dist/plugin.d.ts +88 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin.js.map +1 -0
- package/dist/provider-utils.d.ts +40 -0
- package/dist/provider-utils.d.ts.map +1 -0
- package/dist/provider-utils.js +163 -0
- package/dist/provider-utils.js.map +1 -0
- package/dist/provider.d.ts +219 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +2 -0
- package/dist/provider.js.map +1 -0
- package/dist/requirements.d.ts +22 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +2 -0
- package/dist/requirements.js.map +1 -0
- package/dist/schemas.d.ts +228 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +88 -0
- package/dist/schemas.js.map +1 -0
- package/dist/session-like.d.ts +111 -0
- package/dist/session-like.d.ts.map +1 -0
- package/dist/session-like.js +2 -0
- package/dist/session-like.js.map +1 -0
- package/dist/skill.d.ts +30 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +2 -0
- package/dist/skill.js.map +1 -0
- package/dist/subagent.d.ts +45 -0
- package/dist/subagent.d.ts.map +1 -0
- package/dist/subagent.js +11 -0
- package/dist/subagent.js.map +1 -0
- package/dist/token-accounting.d.ts +73 -0
- package/dist/token-accounting.d.ts.map +1 -0
- package/dist/token-accounting.js +129 -0
- package/dist/token-accounting.js.map +1 -0
- package/dist/tool-dispatch.d.ts +19 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +118 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/tool-gating.d.ts +32 -0
- package/dist/tool-gating.d.ts.map +1 -0
- package/dist/tool-gating.js +83 -0
- package/dist/tool-gating.js.map +1 -0
- package/dist/tool.d.ts +166 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +2 -0
- package/dist/tool.js.map +1 -0
- package/dist/transcriber.d.ts +64 -0
- package/dist/transcriber.d.ts.map +1 -0
- package/dist/transcriber.js +21 -0
- package/dist/transcriber.js.map +1 -0
- package/dist/tunnel.d.ts +25 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +2 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/type-contracts.d.ts +2 -0
- package/dist/type-contracts.d.ts.map +1 -0
- package/dist/type-contracts.js +4 -0
- package/dist/type-contracts.js.map +1 -0
- package/dist/types.test-d.d.ts +2 -0
- package/dist/types.test-d.d.ts.map +1 -0
- package/dist/types.test-d.js +22 -0
- package/dist/types.test-d.js.map +1 -0
- package/dist/view-renderer.d.ts +97 -0
- package/dist/view-renderer.d.ts.map +1 -0
- package/dist/view-renderer.js +72 -0
- package/dist/view-renderer.js.map +1 -0
- package/package.json +38 -0
- package/src/agent.ts +38 -0
- package/src/cache-strategy.ts +39 -0
- package/src/channel.ts +158 -0
- package/src/client-session.ts +88 -0
- package/src/command.ts +86 -0
- package/src/compactor-helpers.test.ts +206 -0
- package/src/compactor-helpers.ts +163 -0
- package/src/compactor.ts +20 -0
- package/src/define.test.ts +76 -0
- package/src/define.ts +113 -0
- package/src/elision-helpers.ts +143 -0
- package/src/elision-state.ts +170 -0
- package/src/embedding-cache.ts +0 -0
- package/src/embedding.ts +22 -0
- package/src/errors.test.ts +176 -0
- package/src/errors.ts +308 -0
- package/src/events.ts +250 -0
- package/src/hooks.ts +54 -0
- package/src/ids.ts +16 -0
- package/src/index.ts +304 -0
- package/src/install-hints.ts +43 -0
- package/src/isolation.ts +148 -0
- package/src/log.ts +11 -0
- package/src/loop-helpers.test.ts +78 -0
- package/src/mode-helpers.ts +483 -0
- package/src/mode.ts +139 -0
- package/src/package-root.test.ts +40 -0
- package/src/permission.ts +37 -0
- package/src/plugin.ts +92 -0
- package/src/provider-utils.test.ts +84 -0
- package/src/provider-utils.ts +171 -0
- package/src/provider.ts +195 -0
- package/src/requirements.ts +35 -0
- package/src/schemas.test.ts +106 -0
- package/src/schemas.ts +97 -0
- package/src/session-like.ts +118 -0
- package/src/skill.ts +34 -0
- package/src/subagent.ts +46 -0
- package/src/token-accounting.test.ts +93 -0
- package/src/token-accounting.ts +202 -0
- package/src/token-efficiency.test.ts +459 -0
- package/src/tool-dispatch.ts +137 -0
- package/src/tool-gating.ts +107 -0
- package/src/tool.ts +175 -0
- package/src/transcriber.ts +71 -0
- package/src/tunnel.ts +26 -0
- package/src/type-contracts.ts +4 -0
- package/src/types.test-d.ts +29 -0
- package/src/view-renderer.test.ts +90 -0
- 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
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -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
|
+
});
|