@moxxy/sdk 0.1.3 → 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 (54) hide show
  1. package/dist/client-session.d.ts +10 -0
  2. package/dist/client-session.d.ts.map +1 -1
  3. package/dist/compactor-helpers.d.ts.map +1 -1
  4. package/dist/compactor-helpers.js +13 -2
  5. package/dist/compactor-helpers.js.map +1 -1
  6. package/dist/define.d.ts +2 -0
  7. package/dist/define.d.ts.map +1 -1
  8. package/dist/define.js +3 -0
  9. package/dist/define.js.map +1 -1
  10. package/dist/events.d.ts +11 -8
  11. package/dist/events.d.ts.map +1 -1
  12. package/dist/index.d.ts +4 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/mode-helpers.d.ts.map +1 -1
  17. package/dist/mode-helpers.js +8 -0
  18. package/dist/mode-helpers.js.map +1 -1
  19. package/dist/mode.d.ts +26 -0
  20. package/dist/mode.d.ts.map +1 -1
  21. package/dist/plugin.d.ts +10 -1
  22. package/dist/plugin.d.ts.map +1 -1
  23. package/dist/provider.d.ts +21 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/requirements.d.ts +1 -1
  26. package/dist/requirements.d.ts.map +1 -1
  27. package/dist/schemas.d.ts +16 -16
  28. package/dist/schemas.d.ts.map +1 -1
  29. package/dist/schemas.js +2 -0
  30. package/dist/schemas.js.map +1 -1
  31. package/dist/session-like.d.ts +13 -1
  32. package/dist/session-like.d.ts.map +1 -1
  33. package/dist/synthesizer.d.ts +63 -0
  34. package/dist/synthesizer.d.ts.map +1 -0
  35. package/dist/synthesizer.js +21 -0
  36. package/dist/synthesizer.js.map +1 -0
  37. package/dist/tool.d.ts +13 -0
  38. package/dist/tool.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/client-session.ts +11 -0
  41. package/src/compactor-helpers.ts +12 -2
  42. package/src/define.ts +5 -0
  43. package/src/events.ts +11 -8
  44. package/src/index.ts +11 -0
  45. package/src/mode-helpers.ts +7 -0
  46. package/src/mode.ts +27 -0
  47. package/src/plugin.ts +10 -1
  48. package/src/provider.ts +17 -1
  49. package/src/requirements.ts +1 -0
  50. package/src/schemas.ts +2 -0
  51. package/src/session-like.ts +13 -1
  52. package/src/synthesizer.ts +67 -0
  53. package/src/token-efficiency.test.ts +58 -0
  54. package/src/tool.ts +13 -0
package/src/provider.ts CHANGED
@@ -10,7 +10,16 @@ export type ContentBlock =
10
10
  | { readonly type: 'tool_use'; readonly id: string; readonly name: string; readonly input: unknown }
11
11
  | { readonly type: 'tool_result'; readonly toolUseId: string; readonly content: string; readonly isError?: boolean }
12
12
  | { readonly type: 'image'; readonly mediaType: string; readonly data: string }
13
- | { readonly type: 'audio'; readonly mediaType: string; readonly data: string };
13
+ | { readonly type: 'audio'; readonly mediaType: string; readonly data: string }
14
+ /**
15
+ * A document the model reads natively (e.g. a PDF). `data` is base64-encoded
16
+ * bytes and `mediaType` the document MIME (e.g. `application/pdf`). Providers
17
+ * that support documents translate it to their native shape (Anthropic
18
+ * `document`, OpenAI `file`, Responses `input_file`); those that don't degrade
19
+ * to a text placeholder. Text/Office files are inlined as `text` instead — only
20
+ * formats a model ingests as bytes become `document` blocks.
21
+ */
22
+ | { readonly type: 'document'; readonly mediaType: string; readonly data: string; readonly name?: string };
14
23
 
15
24
  /**
16
25
  * Provider-neutral instruction for where a prompt-cache breakpoint should be
@@ -68,6 +77,13 @@ export interface ModelDescriptor {
68
77
  * refuses or warns instead of silently dropping the bytes.
69
78
  */
70
79
  readonly supportsImages?: boolean;
80
+ /**
81
+ * Whether this model accepts `document` ContentBlocks (e.g. native PDF) in
82
+ * user messages. When false, the desktop reader extracts text instead of
83
+ * shipping the raw bytes, so the attachment still reaches the model — just
84
+ * without full-document fidelity (figures/layout).
85
+ */
86
+ readonly supportsDocuments?: boolean;
71
87
  /**
72
88
  * Whether this model accepts `audio` ContentBlocks in user messages
73
89
  * (GPT-4o, Gemini-Live-class models). When false, channels with audio
@@ -3,6 +3,7 @@ export type RequirementKind =
3
3
  | 'provider'
4
4
  | 'tool'
5
5
  | 'transcriber'
6
+ | 'synthesizer'
6
7
  | 'mode'
7
8
  | 'compactor'
8
9
  | 'channel'
package/src/schemas.ts CHANGED
@@ -15,6 +15,7 @@ const pluginKindSchema = z.enum([
15
15
  'agent',
16
16
  'command',
17
17
  'transcriber',
18
+ 'synthesizer',
18
19
  ]);
19
20
 
20
21
  export const requirementSchema = z.object({
@@ -23,6 +24,7 @@ export const requirementSchema = z.object({
23
24
  'provider',
24
25
  'tool',
25
26
  'transcriber',
27
+ 'synthesizer',
26
28
  'mode',
27
29
  'compactor',
28
30
  'channel',
@@ -1,7 +1,7 @@
1
1
  import type { MoxxyEvent, UserPromptAttachment } from './events.js';
2
2
  import type { SessionId, TurnId } from './ids.js';
3
3
  import type { EventLogReader } from './log.js';
4
- import type { ApprovalResolver } from './mode.js';
4
+ import type { ApprovalResolver, ModeBadge } from './mode.js';
5
5
  import type { PermissionResolver } from './permission.js';
6
6
  import type { ModelDescriptor } from './provider.js';
7
7
  import type { ToolCompactPresentation } from './tool.js';
@@ -96,6 +96,13 @@ export interface SessionInfo {
96
96
  readonly activeProvider: string | null;
97
97
  readonly providers: ReadonlyArray<ProviderInfo>;
98
98
  readonly activeMode: string | null;
99
+ /**
100
+ * Presentation hint for the active mode, when it advertises one (see
101
+ * {@link ModeBadge}). Lets channels render a persistent accent badge for
102
+ * autonomous modes like goal mode. `null` when no mode is active or the
103
+ * active mode declares no badge.
104
+ */
105
+ readonly activeModeBadge: ModeBadge | null;
99
106
  readonly modes: ReadonlyArray<string>;
100
107
  readonly tools: ReadonlyArray<ToolInfo>;
101
108
  readonly skills: ReadonlyArray<SkillInfo>;
@@ -105,6 +112,11 @@ export interface SessionInfo {
105
112
  readonly hasTranscriber: boolean;
106
113
  /** Name of the active transcriber, or null. Lets a thin client proxy STT. */
107
114
  readonly activeTranscriber: string | null;
115
+ /** Whether any text-to-speech backend is registered. */
116
+ readonly hasSynthesizer: boolean;
117
+ /** Name of the active synthesizer, or null. Lets a thin client proxy TTS
118
+ * (the desktop routes "Read aloud" through it; null → OS voice fallback). */
119
+ readonly activeSynthesizer: string | null;
108
120
  }
109
121
 
110
122
  /**
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Synthesizers convert text into spoken audio (text-to-speech). They are the
3
+ * symmetric counterpart to {@link Transcriber} (speech-to-text): a separate,
4
+ * swappable capability so a user can pair any text provider with a dedicated
5
+ * TTS backend (the OS voice, ElevenLabs, OpenAI, a local engine, …).
6
+ *
7
+ * Plugins register a `SynthesizerDef` via `PluginSpec.synthesizers`. The core
8
+ * `SynthesizerRegistry` holds them; surfaces that read aloud (the desktop's
9
+ * "Read aloud" button, a future voice channel) call
10
+ * `session.synthesizers.getActive()` (or `tryGetActive()`) to turn text into
11
+ * audio. There is at most one *active* synthesizer at a time; when none is
12
+ * active the caller degrades gracefully (the desktop falls back to the
13
+ * browser/OS `speechSynthesis`).
14
+ *
15
+ * The user swaps the active synthesizer by asking the agent ("use ElevenLabs
16
+ * for read-aloud") — there is no settings UI. The agent activates a registered
17
+ * synthesizer (a plugin it authored/installed self-activates in `onInit`, or it
18
+ * calls a set-active tool); switching back to the OS voice deactivates it.
19
+ */
20
+
21
+ export interface SynthesizeOptions {
22
+ /** Preferred voice id/name, when the backend supports selection. */
23
+ readonly voice?: string;
24
+ /** BCP-47 language hint (e.g. `en`, `pl`). */
25
+ readonly language?: string;
26
+ /** Speaking rate multiplier (1.0 = normal), when supported. */
27
+ readonly rate?: number;
28
+ /** Cancellation signal — callers propagate a stop/abort here. */
29
+ readonly signal?: AbortSignal;
30
+ }
31
+
32
+ export interface SynthesisResult {
33
+ /** Encoded audio bytes ready to play (e.g. an mp3/wav/ogg payload). */
34
+ readonly audio: Uint8Array;
35
+ /** MIME type of {@link audio}, e.g. `audio/mpeg`, `audio/wav`, `audio/ogg`. */
36
+ readonly mimeType: string;
37
+ }
38
+
39
+ export interface Synthesizer {
40
+ /** Short stable name, e.g. `elevenlabs`. */
41
+ readonly name: string;
42
+ synthesize(text: string, opts?: SynthesizeOptions): Promise<SynthesisResult>;
43
+ }
44
+
45
+ /**
46
+ * Context handed to {@link SynthesizerDef.create} when the registry activates a
47
+ * synthesizer. `getSecret` reads a vault secret (the API key) the same way a
48
+ * tool handler does via `ctx.getSecret` — so an authored TTS plugin never has
49
+ * to touch `process.env`, and the key never enters the model's context.
50
+ */
51
+ export interface SynthesizerCreateContext {
52
+ /** Per-synthesizer config (e.g. from `session.synthesizers.setActive(name, config)`). */
53
+ readonly config: Record<string, unknown>;
54
+ /** Read a named vault secret, or null if unset / no vault is wired. */
55
+ readonly getSecret?: (name: string) => Promise<string | null>;
56
+ }
57
+
58
+ /**
59
+ * Plugin-side definition. Mirrors `TranscriberDef`: a `create(ctx)` factory the
60
+ * registry calls when this synthesizer is activated.
61
+ */
62
+ export interface SynthesizerDef {
63
+ readonly name: string;
64
+ /** Optional human-readable label for UI surfaces. */
65
+ readonly displayName?: string;
66
+ create(ctx: SynthesizerCreateContext): Synthesizer;
67
+ }
@@ -457,3 +457,61 @@ describe('applyLazyTools', () => {
457
457
  expect(messages).toBe(baseMsgs); // same reference → byte-stable
458
458
  });
459
459
  });
460
+
461
+ describe('attachment projection in projectMessagesFromLog', () => {
462
+ it('projects an image attachment to an image content block', () => {
463
+ const msgs = projectMessagesFromLog({
464
+ log: reader([
465
+ event(0, {
466
+ type: 'user_prompt',
467
+ turnId: t1,
468
+ source: 'user',
469
+ text: 'what is this?',
470
+ attachments: [{ kind: 'image', content: 'AAAA', mediaType: 'image/png', name: 'pic.png' }],
471
+ }),
472
+ ]),
473
+ });
474
+ expect(msgs[0].content).toEqual([
475
+ { type: 'text', text: 'what is this?' },
476
+ { type: 'image', mediaType: 'image/png', data: 'AAAA' },
477
+ ]);
478
+ });
479
+
480
+ it('projects a document attachment to a native document content block', () => {
481
+ const msgs = projectMessagesFromLog({
482
+ log: reader([
483
+ event(0, {
484
+ type: 'user_prompt',
485
+ turnId: t1,
486
+ source: 'user',
487
+ text: 'summarize this',
488
+ attachments: [
489
+ { kind: 'document', content: 'JVBERi0=', mediaType: 'application/pdf', name: 'r.pdf' },
490
+ ],
491
+ }),
492
+ ]),
493
+ });
494
+ expect(msgs[0].content).toEqual([
495
+ { type: 'text', text: 'summarize this' },
496
+ { type: 'document', mediaType: 'application/pdf', data: 'JVBERi0=', name: 'r.pdf' },
497
+ ]);
498
+ });
499
+
500
+ it('inlines a file attachment as a labeled text block', () => {
501
+ const msgs = projectMessagesFromLog({
502
+ log: reader([
503
+ event(0, {
504
+ type: 'user_prompt',
505
+ turnId: t1,
506
+ source: 'user',
507
+ text: 'review',
508
+ attachments: [{ kind: 'file', content: 'const x = 1;', name: 'a.ts' }],
509
+ }),
510
+ ]),
511
+ });
512
+ expect(msgs[0].content).toEqual([
513
+ { type: 'text', text: 'review' },
514
+ { type: 'text', text: '[file a.ts]\nconst x = 1;' },
515
+ ]);
516
+ });
517
+ });
package/src/tool.ts CHANGED
@@ -127,6 +127,19 @@ export interface ToolContext {
127
127
  * `caps.subprocess` + optional `caps.commands` allowlist.
128
128
  */
129
129
  readonly exec?: BrokeredExec;
130
+ /**
131
+ * Read a named secret from the host's secret store (the vault) at call
132
+ * time. Present when the session is wired with a vault-backed resolver
133
+ * (the CLI/runner does this). Returns the plaintext value, or `null` if
134
+ * no such secret exists.
135
+ *
136
+ * This is the blessed way for a tool/plugin to consume an API key or
137
+ * token: the plaintext only ever reaches the handler that asks for it —
138
+ * it is never placed in the model's context or in `process.env`. Store
139
+ * the secret once via `/vault set NAME …` (or the desktop Secrets UI),
140
+ * then read it here, e.g. `const key = await ctx.getSecret('ELEVENLABS_API_KEY')`.
141
+ */
142
+ readonly getSecret?: (name: string) => Promise<string | null>;
130
143
  }
131
144
 
132
145
  /**