@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/tool.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { EventLogReader } from './log.js';
|
|
3
|
+
import type { PermissionRule } from './permission.js';
|
|
4
|
+
import type { SessionId, ToolCallId, TurnId } from './ids.js';
|
|
5
|
+
import type { SubagentSpawner } from './subagent.js';
|
|
6
|
+
import type { ToolIsolationSpec } from './isolation.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Capability-mediated filesystem operations injected by isolators that
|
|
10
|
+
* support brokering. Handlers can opt in by checking `ctx.fs` at runtime
|
|
11
|
+
* and using these instead of `node:fs`; the broker validates each call
|
|
12
|
+
* against the tool's declared `caps.fs` spec on the parent side before
|
|
13
|
+
* executing.
|
|
14
|
+
*
|
|
15
|
+
* When `ctx.fs` is undefined (the `none`/`inproc` paths today, or any
|
|
16
|
+
* isolator that hasn't implemented a broker yet), handlers fall back to
|
|
17
|
+
* unmediated direct fs access.
|
|
18
|
+
*/
|
|
19
|
+
export interface BrokeredFs {
|
|
20
|
+
/** Read a UTF-8 file. Throws if the path is outside `caps.fs.read`. */
|
|
21
|
+
readFile(filePath: string, opts?: { encoding?: BufferEncoding }): Promise<string>;
|
|
22
|
+
/** Write a UTF-8 file (creates parent dirs). Throws if outside `caps.fs.write`. */
|
|
23
|
+
writeFile(filePath: string, data: string): Promise<void>;
|
|
24
|
+
/** List entries in a directory. Throws if outside `caps.fs.read`. */
|
|
25
|
+
readdir(dirPath: string): Promise<ReadonlyArray<string>>;
|
|
26
|
+
/** Stat a path. Throws if outside `caps.fs.read`. */
|
|
27
|
+
stat(filePath: string): Promise<BrokeredStat>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BrokeredStat {
|
|
31
|
+
readonly size: number;
|
|
32
|
+
readonly mtimeMs: number;
|
|
33
|
+
readonly isFile: boolean;
|
|
34
|
+
readonly isDirectory: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Capability-mediated `fetch`. Same shape as the global, but validated
|
|
39
|
+
* against `caps.net` on the parent side before the socket is opened.
|
|
40
|
+
*
|
|
41
|
+
* Returns a plain JSON-serializable response shape rather than the
|
|
42
|
+
* standard `Response` object — that's the price of crossing a process
|
|
43
|
+
* boundary cleanly.
|
|
44
|
+
*/
|
|
45
|
+
export interface BrokeredFetch {
|
|
46
|
+
(url: string, init?: BrokeredFetchInit): Promise<BrokeredFetchResponse>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BrokeredFetchInit {
|
|
50
|
+
readonly method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
51
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
52
|
+
readonly body?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BrokeredFetchResponse {
|
|
56
|
+
readonly status: number;
|
|
57
|
+
readonly statusText: string;
|
|
58
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
59
|
+
readonly body: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Capability-mediated subprocess `exec`. Validated against `caps.subprocess`
|
|
64
|
+
* (must be true) and optional `caps.commands` allowlist on the parent side.
|
|
65
|
+
* Collects stdout/stderr and the exit code; for streaming use cases the
|
|
66
|
+
* broker layer will grow a separate streaming variant later.
|
|
67
|
+
*/
|
|
68
|
+
export interface BrokeredExec {
|
|
69
|
+
(
|
|
70
|
+
command: string,
|
|
71
|
+
args?: ReadonlyArray<string>,
|
|
72
|
+
opts?: BrokeredExecOpts,
|
|
73
|
+
): Promise<BrokeredExecResult>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BrokeredExecOpts {
|
|
77
|
+
readonly cwd?: string;
|
|
78
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
79
|
+
readonly timeoutMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BrokeredExecResult {
|
|
83
|
+
readonly stdout: string;
|
|
84
|
+
readonly stderr: string;
|
|
85
|
+
readonly exitCode: number | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ToolContext {
|
|
89
|
+
readonly sessionId: SessionId;
|
|
90
|
+
readonly turnId: TurnId;
|
|
91
|
+
readonly callId: ToolCallId;
|
|
92
|
+
readonly cwd: string;
|
|
93
|
+
readonly signal: AbortSignal;
|
|
94
|
+
readonly log: EventLogReader;
|
|
95
|
+
readonly logger: {
|
|
96
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
97
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
98
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
99
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Spawner for child agents — present when the tool was invoked inside
|
|
103
|
+
* a run-turn loop (the normal case). Tools that fan work out (e.g.
|
|
104
|
+
* `dispatch_agent`) call `subagents.spawn(...)` to start a focused
|
|
105
|
+
* child loop and stream its events back to the parent log.
|
|
106
|
+
*/
|
|
107
|
+
readonly subagents?: SubagentSpawner;
|
|
108
|
+
/**
|
|
109
|
+
* Capability-mediated filesystem ops. Present only when the active
|
|
110
|
+
* isolator implements a broker (currently `@moxxy/isolator-worker`).
|
|
111
|
+
* Handlers that use this get their fs access checked against
|
|
112
|
+
* `caps.fs` at every call, regardless of whether the path appeared
|
|
113
|
+
* in the validated input. Absent → handler is on its own for fs
|
|
114
|
+
* access (today's behavior).
|
|
115
|
+
*/
|
|
116
|
+
readonly fs?: BrokeredFs;
|
|
117
|
+
/**
|
|
118
|
+
* Capability-mediated network. Present only when the active
|
|
119
|
+
* isolator implements a broker. Validates every URL against
|
|
120
|
+
* `caps.net` on the parent side. Returns a serializable response
|
|
121
|
+
* shape so the same value crosses a process boundary intact.
|
|
122
|
+
*/
|
|
123
|
+
readonly fetch?: BrokeredFetch;
|
|
124
|
+
/**
|
|
125
|
+
* Capability-mediated subprocess execution. Present only when the
|
|
126
|
+
* active isolator implements a broker. Validated against
|
|
127
|
+
* `caps.subprocess` + optional `caps.commands` allowlist.
|
|
128
|
+
*/
|
|
129
|
+
readonly exec?: BrokeredExec;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Optional presentation hint for compact rendering in TUI/chat surfaces.
|
|
134
|
+
* When present, the channel may aggregate consecutive calls of this tool
|
|
135
|
+
* into one "live block" with a verb+count summary, rather than rendering
|
|
136
|
+
* each call separately. Opting in is per-tool: noisy small-output tools
|
|
137
|
+
* (Read, Grep, Glob, Edit) benefit; tools with rich output (Bash,
|
|
138
|
+
* dispatch_agent) generally don't.
|
|
139
|
+
*
|
|
140
|
+
* Channels MAY ignore this hint — it's purely presentational. The event
|
|
141
|
+
* log and provider serialization don't see it.
|
|
142
|
+
*/
|
|
143
|
+
export interface ToolCompactPresentation {
|
|
144
|
+
/** Present-participle verb used in summary, e.g. "Reading", "Searching for". */
|
|
145
|
+
readonly verb: string;
|
|
146
|
+
/** Noun for the count, pluralized e.g. `{ one: 'file', other: 'files' }`. */
|
|
147
|
+
readonly noun: { readonly one: string; readonly other: string };
|
|
148
|
+
/** Input field whose value previews the latest call (the line under the summary).
|
|
149
|
+
* e.g. `"file_path"` for Read so the preview shows the file just read. */
|
|
150
|
+
readonly previewKey?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ToolDef {
|
|
154
|
+
readonly name: string;
|
|
155
|
+
readonly description: string;
|
|
156
|
+
readonly inputSchema: z.ZodTypeAny;
|
|
157
|
+
/**
|
|
158
|
+
* Optional native JSON Schema. When present, providers serializing tools to
|
|
159
|
+
* their API should use this instead of converting `inputSchema` via zod.
|
|
160
|
+
* Useful for tools originating from external systems (e.g., MCP) that already
|
|
161
|
+
* carry a JSON Schema and where zod conversion would be lossy.
|
|
162
|
+
*/
|
|
163
|
+
readonly inputJsonSchema?: unknown;
|
|
164
|
+
readonly outputSchema?: z.ZodTypeAny;
|
|
165
|
+
readonly permission?: PermissionRule;
|
|
166
|
+
readonly handler: (input: unknown, ctx: ToolContext) => Promise<unknown> | unknown;
|
|
167
|
+
/** Opt-in presentation hint. See `ToolCompactPresentation`. */
|
|
168
|
+
readonly compact?: ToolCompactPresentation;
|
|
169
|
+
/**
|
|
170
|
+
* Optional capability declaration. Advisory unless the user enables
|
|
171
|
+
* `@moxxy/plugin-security`, at which point the active `Isolator`
|
|
172
|
+
* enforces these bounds at every call. See `ToolIsolationSpec`.
|
|
173
|
+
*/
|
|
174
|
+
readonly isolation?: ToolIsolationSpec;
|
|
175
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcribers convert audio bytes into text. They are a separate capability
|
|
3
|
+
* from `LLMProvider` because (a) most providers (Anthropic today) do not yet
|
|
4
|
+
* accept native audio, (b) users frequently want to pair a text provider with
|
|
5
|
+
* a dedicated STT backend (Whisper, Deepgram, AssemblyAI, local whisper.cpp),
|
|
6
|
+
* and (c) the same transcript may be used in many places — channels feeding
|
|
7
|
+
* voice notes into a turn, skills processing recorded meetings, etc.
|
|
8
|
+
*
|
|
9
|
+
* Plugins register a `TranscriberDef` via `PluginSpec.transcribers`. The
|
|
10
|
+
* core `TranscriberRegistry` holds them; channels call
|
|
11
|
+
* `session.transcribers.getActive()` (or `.get(name)`) to use one.
|
|
12
|
+
*
|
|
13
|
+
* Audio input is represented in two places once a `Transcriber` is wired:
|
|
14
|
+
* - `UserPromptAttachment` with `kind: 'audio'` — channel-level handoff,
|
|
15
|
+
* consumed by the channel and either transcribed or sent through as a
|
|
16
|
+
* native audio block (provider-permitting).
|
|
17
|
+
* - `ContentBlock` with `type: 'audio'` — provider-level wire format, only
|
|
18
|
+
* used by models that advertise `supportsAudio: true`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface TranscriptionSegment {
|
|
22
|
+
/** Segment start, in seconds from the start of the clip. */
|
|
23
|
+
readonly start: number;
|
|
24
|
+
/** Segment end, in seconds. */
|
|
25
|
+
readonly end: number;
|
|
26
|
+
readonly text: string;
|
|
27
|
+
/** Optional speaker label when the backend supports diarization. */
|
|
28
|
+
readonly speaker?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TranscriptionResult {
|
|
32
|
+
/** Full concatenated transcript. */
|
|
33
|
+
readonly text: string;
|
|
34
|
+
/** BCP-47 language tag when the backend reports one (e.g. `en`, `pl`). */
|
|
35
|
+
readonly language?: string;
|
|
36
|
+
/** Clip length in seconds when known. */
|
|
37
|
+
readonly durationSec?: number;
|
|
38
|
+
/** Optional per-segment breakdown for richer downstream use. */
|
|
39
|
+
readonly segments?: ReadonlyArray<TranscriptionSegment>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TranscribeOptions {
|
|
43
|
+
/** MIME type of the audio bytes (e.g. `audio/ogg`, `audio/webm`). */
|
|
44
|
+
readonly mimeType?: string;
|
|
45
|
+
/** Hint the recognizer with a BCP-47 language tag. */
|
|
46
|
+
readonly language?: string;
|
|
47
|
+
/** Vocabulary / context hint passed to backends that support it (Whisper). */
|
|
48
|
+
readonly prompt?: string;
|
|
49
|
+
/** Cancellation signal — channels propagate the turn's abort here. */
|
|
50
|
+
readonly signal?: AbortSignal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Transcriber {
|
|
54
|
+
/** Short stable name, e.g. `openai-whisper-1`. */
|
|
55
|
+
readonly name: string;
|
|
56
|
+
transcribe(
|
|
57
|
+
audio: Uint8Array | ArrayBuffer,
|
|
58
|
+
opts?: TranscribeOptions,
|
|
59
|
+
): Promise<TranscriptionResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Plugin-side definition. Mirrors `ProviderDef`: a `createClient(config)`
|
|
64
|
+
* factory the registry calls when the user activates this transcriber.
|
|
65
|
+
*/
|
|
66
|
+
export interface TranscriberDef {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
/** Optional human-readable label for UI surfaces. */
|
|
69
|
+
readonly displayName?: string;
|
|
70
|
+
createClient(config: Record<string, unknown>): Transcriber;
|
|
71
|
+
}
|
package/src/tunnel.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A swappable way to expose a locally-bound surface (the web channel) to the
|
|
3
|
+
* user over the public internet — so an agent on Telegram/TUI can hand the user
|
|
4
|
+
* a URL they can open. One provider is active per session (registered via
|
|
5
|
+
* plugins, like every other block); core seeds a `localhost` no-op provider so
|
|
6
|
+
* `getActive()` is non-null.
|
|
7
|
+
*/
|
|
8
|
+
export interface TunnelOpenOptions {
|
|
9
|
+
readonly port: number;
|
|
10
|
+
readonly host: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TunnelHandle {
|
|
14
|
+
/** The publicly reachable base URL (e.g. https://abc.trycloudflare.com). */
|
|
15
|
+
readonly url: string;
|
|
16
|
+
/** Tear the tunnel down (kill the subprocess, close the connection). */
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TunnelProviderDef {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Open a tunnel to `http://host:port`, resolving once the public URL is known. */
|
|
23
|
+
open(opts: TunnelOpenOptions): Promise<TunnelHandle>;
|
|
24
|
+
/** Optional readiness gate (e.g. the `cloudflared` binary is installed). */
|
|
25
|
+
isAvailable?(): Promise<boolean>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
MoxxyEvent,
|
|
4
|
+
MoxxyEventOfType,
|
|
5
|
+
ToolCallRequestedEvent,
|
|
6
|
+
UserPromptEvent,
|
|
7
|
+
} from './events.js';
|
|
8
|
+
|
|
9
|
+
describe('event union narrowing', () => {
|
|
10
|
+
it('MoxxyEventOfType narrows correctly', () => {
|
|
11
|
+
expectTypeOf<MoxxyEventOfType<'user_prompt'>>().toEqualTypeOf<UserPromptEvent>();
|
|
12
|
+
expectTypeOf<MoxxyEventOfType<'tool_call_requested'>>().toEqualTypeOf<ToolCallRequestedEvent>();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('switch on type narrows the union', () => {
|
|
16
|
+
const handle = (e: MoxxyEvent) => {
|
|
17
|
+
if (e.type === 'user_prompt') {
|
|
18
|
+
expectTypeOf(e).toEqualTypeOf<UserPromptEvent>();
|
|
19
|
+
return e.text;
|
|
20
|
+
}
|
|
21
|
+
if (e.type === 'tool_call_requested') {
|
|
22
|
+
expectTypeOf(e).toEqualTypeOf<ToolCallRequestedEvent>();
|
|
23
|
+
return e.name;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
expectTypeOf(handle).returns.toEqualTypeOf<string | null>();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
VIEW_PRIMITIVES,
|
|
4
|
+
VIEW_COMPONENTS,
|
|
5
|
+
DEFAULT_VIEW_TAGS,
|
|
6
|
+
defineViewRenderer,
|
|
7
|
+
defineTunnelProvider,
|
|
8
|
+
type ViewTagSpec,
|
|
9
|
+
} from './index.js';
|
|
10
|
+
|
|
11
|
+
describe('view vocabulary integrity', () => {
|
|
12
|
+
const all = DEFAULT_VIEW_TAGS;
|
|
13
|
+
|
|
14
|
+
it('DEFAULT_VIEW_TAGS is primitives + components', () => {
|
|
15
|
+
expect(all.length).toBe(VIEW_PRIMITIVES.length + VIEW_COMPONENTS.length);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('has no duplicate tag names', () => {
|
|
19
|
+
const names = all.map((t) => t.tag);
|
|
20
|
+
expect(new Set(names).size).toBe(names.length);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('every tag has a valid allowedChildren', () => {
|
|
24
|
+
for (const t of all) {
|
|
25
|
+
const ac = t.allowedChildren;
|
|
26
|
+
const valid = ac === 'any' || ac === 'none' || Array.isArray(ac);
|
|
27
|
+
expect(valid, `${t.tag} allowedChildren`).toBe(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('every array allowedChildren references known tags', () => {
|
|
32
|
+
const known = new Set(all.map((t) => t.tag));
|
|
33
|
+
for (const t of all) {
|
|
34
|
+
if (Array.isArray(t.allowedChildren)) {
|
|
35
|
+
for (const child of t.allowedChildren) {
|
|
36
|
+
expect(known.has(child), `${t.tag} -> ${child}`).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('enum attrs declare their allowed values; number attrs may declare bounds', () => {
|
|
43
|
+
for (const t of all) {
|
|
44
|
+
for (const [name, spec] of Object.entries(t.attrs)) {
|
|
45
|
+
if (spec.type === 'enum') {
|
|
46
|
+
expect(Array.isArray(spec.values) && spec.values.length > 0, `${t.tag}.${name}`).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('marks interactive elements and rich components correctly', () => {
|
|
53
|
+
const byTag = (tag: string): ViewTagSpec => all.find((t) => t.tag === tag)!;
|
|
54
|
+
expect(byTag('form').interactive).toBe(true);
|
|
55
|
+
expect(byTag('button').interactive).toBe(true);
|
|
56
|
+
expect(byTag('text').interactive).toBeUndefined();
|
|
57
|
+
expect(VIEW_COMPONENTS.every((c) => c.component)).toBe(true);
|
|
58
|
+
expect(VIEW_PRIMITIVES.every((p) => !p.component)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('declares the expected required attributes', () => {
|
|
62
|
+
const required = (tag: string) =>
|
|
63
|
+
Object.entries(DEFAULT_VIEW_TAGS.find((t) => t.tag === tag)!.attrs)
|
|
64
|
+
.filter(([, s]) => s.required)
|
|
65
|
+
.map(([n]) => n)
|
|
66
|
+
.sort();
|
|
67
|
+
expect(required('input')).toEqual(['name']);
|
|
68
|
+
expect(required('form')).toEqual(['action']);
|
|
69
|
+
// action is optional on button now (a button may navigate via `to` instead).
|
|
70
|
+
expect(required('button')).toEqual(['label']);
|
|
71
|
+
expect(required('image')).toEqual(['src']);
|
|
72
|
+
expect(required('option')).toEqual(['value']);
|
|
73
|
+
expect(required('result')).toEqual(['title']);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('define factories freeze their specs', () => {
|
|
78
|
+
it('defineViewRenderer', () => {
|
|
79
|
+
const def = defineViewRenderer({ name: 'x', allowList: [], parse: () => ({ ok: false, errors: [] }), validate: () => [] });
|
|
80
|
+
expect(Object.isFrozen(def)).toBe(true);
|
|
81
|
+
expect(() => {
|
|
82
|
+
(def as { name: string }).name = 'y';
|
|
83
|
+
}).toThrow();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('defineTunnelProvider', () => {
|
|
87
|
+
const def = defineTunnelProvider({ name: 't', open: () => Promise.resolve({ url: 'http://x', close: () => Promise.resolve() }) });
|
|
88
|
+
expect(Object.isFrozen(def)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-authored UI view-spec — the contract shared by the parser (core) and
|
|
3
|
+
* the renderer (web channel frontend).
|
|
4
|
+
*
|
|
5
|
+
* An agent emits a small JSX/XML-like document; a {@link ViewRendererDef} parses
|
|
6
|
+
* it into a validated {@link ViewDoc} AST against a strict tag/attribute
|
|
7
|
+
* **allow-list** (no raw HTML / script passthrough). One renderer is active per
|
|
8
|
+
* session, registered via plugins exactly like compactors / cache-strategies.
|
|
9
|
+
*
|
|
10
|
+
* The tag vocabulary ({@link VIEW_PRIMITIVES} / {@link VIEW_COMPONENTS}) is plain
|
|
11
|
+
* data so the browser frontend can import the SAME allow-list the parser
|
|
12
|
+
* validates against — preventing the two-list drift that would open an XSS gap.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** A node in the validated view AST. After parsing, only primitives remain. */
|
|
16
|
+
export type ViewNode =
|
|
17
|
+
| {
|
|
18
|
+
readonly kind: 'element';
|
|
19
|
+
readonly tag: string;
|
|
20
|
+
readonly props: Readonly<Record<string, string | number | boolean>>;
|
|
21
|
+
readonly children: ReadonlyArray<ViewNode>;
|
|
22
|
+
/** Present on interactive elements (`form`, `button`) — drives an agent turn. */
|
|
23
|
+
readonly action?: ViewAction;
|
|
24
|
+
/**
|
|
25
|
+
* Client-side navigation target (a `view` `name`). Set from a `to` attr on
|
|
26
|
+
* `link`/`button`. The frontend switches to the named cached view WITHOUT an
|
|
27
|
+
* agent turn; if it isn't cached yet it falls back to a `navigate:<name>` turn.
|
|
28
|
+
*/
|
|
29
|
+
readonly nav?: string;
|
|
30
|
+
}
|
|
31
|
+
| { readonly kind: 'text'; readonly value: string };
|
|
32
|
+
|
|
33
|
+
/** Declared on an interactive element; the transport turns it into a turn. */
|
|
34
|
+
export interface ViewAction {
|
|
35
|
+
/** Opaque name the agent chose, e.g. `search_flights`, `select:UA42`. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Input `name`s whose current values this action carries back. */
|
|
38
|
+
readonly fields: ReadonlyArray<string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ViewDoc {
|
|
42
|
+
readonly root: ViewNode;
|
|
43
|
+
readonly title?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ViewParseError {
|
|
47
|
+
readonly message: string;
|
|
48
|
+
readonly line?: number;
|
|
49
|
+
readonly col?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ViewParseResult =
|
|
53
|
+
| { readonly ok: true; readonly doc: ViewDoc }
|
|
54
|
+
| { readonly ok: false; readonly errors: ReadonlyArray<ViewParseError> };
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Shared vocabulary — plain data, the single source of truth.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export type AttrType = 'string' | 'number' | 'boolean' | 'enum';
|
|
61
|
+
|
|
62
|
+
export interface AttrSpec {
|
|
63
|
+
readonly type: AttrType;
|
|
64
|
+
readonly required?: boolean;
|
|
65
|
+
/** Allowed values when `type === 'enum'`. */
|
|
66
|
+
readonly values?: ReadonlyArray<string>;
|
|
67
|
+
/** Inclusive bounds when `type === 'number'`. */
|
|
68
|
+
readonly min?: number;
|
|
69
|
+
readonly max?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ViewTagSpec {
|
|
73
|
+
readonly tag: string;
|
|
74
|
+
readonly attrs: Readonly<Record<string, AttrSpec>>;
|
|
75
|
+
/** Allowed child tags; `'any'` = any element + text, `'none'` = void. */
|
|
76
|
+
readonly allowedChildren: ReadonlyArray<string> | 'any' | 'none';
|
|
77
|
+
/** Carries a {@link ViewAction} (the parser synthesizes `node.action`). */
|
|
78
|
+
readonly interactive?: boolean;
|
|
79
|
+
/** A rich component the renderer expands into primitives at parse time. */
|
|
80
|
+
readonly component?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A swappable view-spec renderer. `parse` turns source into a validated AST
|
|
85
|
+
* (rich components already expanded to primitives); `validate` re-checks an
|
|
86
|
+
* already-built AST against {@link allowList} — note it runs on EXPANDED
|
|
87
|
+
* primitives, a contract replacement renderers must preserve.
|
|
88
|
+
*/
|
|
89
|
+
export interface ViewRendererDef {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly allowList: ReadonlyArray<ViewTagSpec>;
|
|
92
|
+
parse(source: string): ViewParseResult;
|
|
93
|
+
validate(doc: ViewDoc): ReadonlyArray<ViewParseError>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const TONE = ['default', 'muted', 'success', 'warn', 'danger'] as const;
|
|
97
|
+
const GAP = ['none', 'sm', 'md', 'lg'] as const;
|
|
98
|
+
|
|
99
|
+
/** Primitives — the frontend renders these directly (keymap === these tags). */
|
|
100
|
+
export const VIEW_PRIMITIVES: ReadonlyArray<ViewTagSpec> = [
|
|
101
|
+
// layout / containers
|
|
102
|
+
{ tag: 'view', attrs: { title: { type: 'string' }, name: { type: 'string' } }, allowedChildren: 'any' },
|
|
103
|
+
{ tag: 'stack', attrs: { gap: { type: 'enum', values: GAP }, align: { type: 'enum', values: ['start', 'center', 'end', 'stretch'] } }, allowedChildren: 'any' },
|
|
104
|
+
{ tag: 'row', attrs: { gap: { type: 'enum', values: GAP }, align: { type: 'enum', values: ['start', 'center', 'end', 'stretch'] }, justify: { type: 'enum', values: ['start', 'center', 'end', 'between'] } }, allowedChildren: 'any' },
|
|
105
|
+
{ tag: 'grid', attrs: { cols: { type: 'number', required: true, min: 1, max: 6 }, gap: { type: 'enum', values: GAP } }, allowedChildren: 'any' },
|
|
106
|
+
{ tag: 'card', attrs: { title: { type: 'string' }, accent: { type: 'enum', values: TONE } }, allowedChildren: 'any' },
|
|
107
|
+
{ tag: 'divider', attrs: {}, allowedChildren: 'none' },
|
|
108
|
+
// loading states (show while fetching real data, then replace with results)
|
|
109
|
+
{ tag: 'spinner', attrs: { label: { type: 'string' } }, allowedChildren: 'none' },
|
|
110
|
+
{ tag: 'skeleton', attrs: { rows: { type: 'number', min: 1, max: 12 } }, allowedChildren: 'none' },
|
|
111
|
+
// display
|
|
112
|
+
{ tag: 'heading', attrs: { level: { type: 'number', min: 1, max: 3 } }, allowedChildren: 'any' },
|
|
113
|
+
{ tag: 'text', attrs: { tone: { type: 'enum', values: TONE }, weight: { type: 'enum', values: ['normal', 'bold'] } }, allowedChildren: 'any' },
|
|
114
|
+
{ tag: 'badge', attrs: { tone: { type: 'enum', values: TONE } }, allowedChildren: 'any' },
|
|
115
|
+
{ tag: 'image', attrs: { src: { type: 'string', required: true }, alt: { type: 'string' }, w: { type: 'number' }, h: { type: 'number' } }, allowedChildren: 'none' },
|
|
116
|
+
{ tag: 'link', attrs: { href: { type: 'string' }, to: { type: 'string' } }, allowedChildren: 'any' },
|
|
117
|
+
{ tag: 'list', attrs: { ordered: { type: 'boolean' } }, allowedChildren: ['item'] },
|
|
118
|
+
{ tag: 'item', attrs: {}, allowedChildren: 'any' },
|
|
119
|
+
{ tag: 'table', attrs: {}, allowedChildren: ['tr'] },
|
|
120
|
+
{ tag: 'tr', attrs: {}, allowedChildren: ['th', 'td'] },
|
|
121
|
+
{ tag: 'th', attrs: { align: { type: 'enum', values: ['left', 'center', 'right'] } }, allowedChildren: 'any' },
|
|
122
|
+
{ tag: 'td', attrs: { align: { type: 'enum', values: ['left', 'center', 'right'] } }, allowedChildren: 'any' },
|
|
123
|
+
// inputs (only meaningful inside `form`)
|
|
124
|
+
{ tag: 'form', attrs: { action: { type: 'string', required: true }, submit: { type: 'string' } }, allowedChildren: 'any', interactive: true },
|
|
125
|
+
{ tag: 'input', attrs: { name: { type: 'string', required: true }, type: { type: 'enum', values: ['text', 'number', 'date', 'email'] }, label: { type: 'string' }, placeholder: { type: 'string' }, value: { type: 'string' }, required: { type: 'boolean' } }, allowedChildren: 'none' },
|
|
126
|
+
{ tag: 'select', attrs: { name: { type: 'string', required: true }, label: { type: 'string' }, value: { type: 'string' }, required: { type: 'boolean' } }, allowedChildren: ['option'] },
|
|
127
|
+
{ tag: 'option', attrs: { value: { type: 'string', required: true }, selected: { type: 'boolean' } }, allowedChildren: 'any' },
|
|
128
|
+
{ tag: 'checkbox', attrs: { name: { type: 'string', required: true }, label: { type: 'string' }, checked: { type: 'boolean' } }, allowedChildren: 'none' },
|
|
129
|
+
// `action` drives an agent turn; `to` navigates client-side to a named view.
|
|
130
|
+
{ tag: 'button', attrs: { action: { type: 'string' }, to: { type: 'string' }, label: { type: 'string', required: true }, variant: { type: 'enum', values: ['primary', 'secondary', 'danger'] }, fields: { type: 'string' } }, allowedChildren: 'none', interactive: true },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rich components — accepted by the parser, expanded to primitives, never reach the
|
|
135
|
+
* frontend. Generic + domain-agnostic: a `results` list of selectable `result`s is
|
|
136
|
+
* the backbone of any "search engine / platform for X" the agent builds.
|
|
137
|
+
*/
|
|
138
|
+
export const VIEW_COMPONENTS: ReadonlyArray<ViewTagSpec> = [
|
|
139
|
+
{ tag: 'results', attrs: {}, allowedChildren: ['result'], component: true },
|
|
140
|
+
{
|
|
141
|
+
tag: 'result',
|
|
142
|
+
attrs: {
|
|
143
|
+
title: { type: 'string', required: true },
|
|
144
|
+
subtitle: { type: 'string' },
|
|
145
|
+
badge: { type: 'string' },
|
|
146
|
+
id: { type: 'string' },
|
|
147
|
+
action: { type: 'string' }, // agent turn on select (default: open:<id>)
|
|
148
|
+
to: { type: 'string' }, // or client-side nav to a named view
|
|
149
|
+
},
|
|
150
|
+
allowedChildren: 'none',
|
|
151
|
+
component: true,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/** The default renderer's full allow-list (primitives + components). */
|
|
156
|
+
export const DEFAULT_VIEW_TAGS: ReadonlyArray<ViewTagSpec> = [...VIEW_PRIMITIVES, ...VIEW_COMPONENTS];
|