@moxxy/sdk 0.37.2 → 0.39.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/process-invocation.d.ts +52 -0
- package/dist/process-invocation.d.ts.map +1 -0
- package/dist/process-invocation.js +172 -0
- package/dist/process-invocation.js.map +1 -0
- package/dist/provider-login-bridge.d.ts +11 -0
- package/dist/provider-login-bridge.d.ts.map +1 -1
- package/dist/provider-login-bridge.js +51 -1
- package/dist/provider-login-bridge.js.map +1 -1
- package/dist/provider-utils.d.ts +14 -0
- package/dist/provider-utils.d.ts.map +1 -1
- package/dist/provider-utils.js +23 -0
- package/dist/provider-utils.js.map +1 -1
- package/dist/provider.d.ts +27 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +2 -0
- package/src/process-invocation.test.ts +281 -0
- package/src/process-invocation.ts +237 -0
- package/src/provider-login-bridge.test.ts +42 -0
- package/src/provider-login-bridge.ts +60 -2
- package/src/provider-utils.test.ts +54 -0
- package/src/provider-utils.ts +22 -0
- package/src/provider.ts +28 -0
- package/src/server.ts +11 -0
|
@@ -17,12 +17,40 @@
|
|
|
17
17
|
* depend on, so the two ends can never drift.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
20
22
|
/** The byte that brackets a prompt marker. Absent from normal CLI output. */
|
|
21
23
|
const NUL = '\u0000';
|
|
22
24
|
|
|
23
25
|
/** Tag distinguishing a login-prompt marker from any other NUL-bracketed run. */
|
|
24
26
|
const PROMPT_TAG = 'moxxy.login.prompt';
|
|
25
27
|
|
|
28
|
+
/** Tag distinguishing a browser-authorization URL from normal login output. */
|
|
29
|
+
const AUTH_URL_TAG = 'moxxy.login.auth_url';
|
|
30
|
+
|
|
31
|
+
const LoginAuthUrlSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.max(8192)
|
|
34
|
+
.refine((value) => {
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
return (
|
|
38
|
+
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
39
|
+
url.username === '' &&
|
|
40
|
+
url.password === ''
|
|
41
|
+
);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}, 'must be an http(s) URL without embedded credentials');
|
|
46
|
+
|
|
47
|
+
const LoginAuthUrlMarkerSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
tag: z.literal(AUTH_URL_TAG),
|
|
50
|
+
url: LoginAuthUrlSchema,
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
|
|
26
54
|
export interface LoginPromptRequest {
|
|
27
55
|
/** The question to show the user, verbatim from `ctx.prompt`. */
|
|
28
56
|
readonly question: string;
|
|
@@ -33,7 +61,8 @@ export interface LoginPromptRequest {
|
|
|
33
61
|
/** One decoded item from a login subprocess's stdout stream. */
|
|
34
62
|
export type LoginStreamItem =
|
|
35
63
|
| { readonly type: 'output'; readonly text: string }
|
|
36
|
-
| { readonly type: 'prompt'; readonly prompt: LoginPromptRequest }
|
|
64
|
+
| { readonly type: 'prompt'; readonly prompt: LoginPromptRequest }
|
|
65
|
+
| { readonly type: 'auth_url'; readonly url: string };
|
|
37
66
|
|
|
38
67
|
/**
|
|
39
68
|
* Serialize a prompt request as a NUL-bracketed marker for stdout. The
|
|
@@ -59,6 +88,28 @@ export function decodeLoginPrompt(segment: string): LoginPromptRequest | null {
|
|
|
59
88
|
}
|
|
60
89
|
}
|
|
61
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Serialize the browser authorization URL for a GUI host. Validation happens
|
|
93
|
+
* before a marker reaches stdout: a provider-controlled URL may only be a
|
|
94
|
+
* credential-free HTTP(S) URL, never a shell-adjacent custom/file scheme.
|
|
95
|
+
*/
|
|
96
|
+
export function encodeLoginAuthUrl(url: string): string {
|
|
97
|
+
const marker = LoginAuthUrlMarkerSchema.parse({ tag: AUTH_URL_TAG, url });
|
|
98
|
+
return NUL + JSON.stringify(marker) + NUL;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Decode and validate one inter-NUL authorization-URL marker. */
|
|
102
|
+
export function decodeLoginAuthUrl(segment: string): string | null {
|
|
103
|
+
if (!segment.startsWith('{')) return null;
|
|
104
|
+
try {
|
|
105
|
+
const parsed: unknown = JSON.parse(segment);
|
|
106
|
+
const marker = LoginAuthUrlMarkerSchema.safeParse(parsed);
|
|
107
|
+
return marker.success ? marker.data.url : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
62
113
|
/**
|
|
63
114
|
* Streaming scanner for a login subprocess's stdout. Feed it chunks; it
|
|
64
115
|
* returns the ordered output / prompt items decoded so far, holding back any
|
|
@@ -84,9 +135,16 @@ export function createLoginStreamScanner(): {
|
|
|
84
135
|
if (open > 0) out.push({ type: 'output', text: buf.slice(0, open) });
|
|
85
136
|
const segment = buf.slice(open + 1, close);
|
|
86
137
|
const prompt = decodeLoginPrompt(segment);
|
|
138
|
+
const authUrl = prompt ? null : decodeLoginAuthUrl(segment);
|
|
87
139
|
// A NUL-bracketed run that isn't a valid marker is anomalous; surface
|
|
88
140
|
// its inner text as output rather than swallowing it.
|
|
89
|
-
out.push(
|
|
141
|
+
out.push(
|
|
142
|
+
prompt
|
|
143
|
+
? { type: 'prompt', prompt }
|
|
144
|
+
: authUrl !== null
|
|
145
|
+
? { type: 'auth_url', url: authUrl }
|
|
146
|
+
: { type: 'output', text: segment },
|
|
147
|
+
);
|
|
90
148
|
buf = buf.slice(close + 1);
|
|
91
149
|
}
|
|
92
150
|
// Flush any plain text that precedes a still-incomplete marker (or all
|
|
@@ -152,3 +152,57 @@ describe('resolveProviderTools', () => {
|
|
|
152
152
|
expect(resolveProviderTools([webSearch], models, 'catalog-drift').tools).toEqual([webSearch]);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Field descriptions are the only place a tool author can say what a parameter
|
|
158
|
+
* means, and the model is the audience. They were dropped on the floor: every
|
|
159
|
+
* `.describe()` in every moxxy tool reached the provider as nothing at all,
|
|
160
|
+
* which is how a model ends up inventing `tab_id: "current"` for a field whose
|
|
161
|
+
* description reads "Omit for the active tab".
|
|
162
|
+
*/
|
|
163
|
+
describe('zodToJsonSchema — descriptions', () => {
|
|
164
|
+
it('carries a field description through to the model', () => {
|
|
165
|
+
const out = zodToJsonSchema(z.object({ a: z.string().describe('Co to jest a') })) as {
|
|
166
|
+
properties: Record<string, Record<string, unknown>>;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
expect(out.properties.a).toEqual({ type: 'string', description: 'Co to jest a' });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('keeps a description that sits under a wrapper', () => {
|
|
173
|
+
expect(zodToJsonSchema(z.string().describe('opis').optional())).toEqual({
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'opis',
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('lets the outermost description win when a wrapper carries one too', () => {
|
|
180
|
+
const schema = z.string().describe('wewnetrzny').optional().describe('zewnetrzny');
|
|
181
|
+
|
|
182
|
+
expect(zodToJsonSchema(schema)).toEqual({ type: 'string', description: 'zewnetrzny' });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('describes a whole object, an enum and an array element too', () => {
|
|
186
|
+
const out = zodToJsonSchema(
|
|
187
|
+
z
|
|
188
|
+
.object({
|
|
189
|
+
mode: z.enum(['a', 'b']).describe('Tryb'),
|
|
190
|
+
tags: z.array(z.string()).describe('Etykiety'),
|
|
191
|
+
})
|
|
192
|
+
.describe('Cale wejscie'),
|
|
193
|
+
) as { description: string; properties: Record<string, Record<string, unknown>> };
|
|
194
|
+
|
|
195
|
+
expect(out.description).toBe('Cale wejscie');
|
|
196
|
+
expect(out.properties.mode?.description).toBe('Tryb');
|
|
197
|
+
expect(out.properties.tags?.description).toBe('Etykiety');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('adds no description key when the author wrote none', () => {
|
|
201
|
+
expect(zodToJsonSchema(z.string())).toEqual({ type: 'string' });
|
|
202
|
+
expect(zodToJsonSchema(z.object({ a: z.string() }))).toEqual({
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: { a: { type: 'string' } },
|
|
205
|
+
required: ['a'],
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/provider-utils.ts
CHANGED
|
@@ -80,7 +80,29 @@ export function toFriendlyError(
|
|
|
80
80
|
*/
|
|
81
81
|
const MAX_SCHEMA_DEPTH = 32;
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Convert, then re-attach the author's `.describe()` text.
|
|
85
|
+
*
|
|
86
|
+
* A field description is the only place a tool author gets to say what a
|
|
87
|
+
* parameter means, and the model is the audience — dropping it leaves the
|
|
88
|
+
* model guessing at values (`tab_id: "current"` for a field documented as
|
|
89
|
+
* "omit for the active tab"). `description` is standard JSON Schema, so every
|
|
90
|
+
* provider either uses it or ignores it.
|
|
91
|
+
*
|
|
92
|
+
* Doing it here rather than at each of the dozen returns below also fixes the
|
|
93
|
+
* wrappers for free: `.describe()` may sit on the ZodOptional/ZodEffects OR on
|
|
94
|
+
* the type inside it, and each level of the recursion re-enters through this
|
|
95
|
+
* function. The outermost wins, because it applies last.
|
|
96
|
+
*/
|
|
83
97
|
export function zodToJsonSchema(schema: unknown, depth = 0): unknown {
|
|
98
|
+
const json = convertSchema(schema, depth);
|
|
99
|
+
const description = (schema as { _def?: { description?: string } } | null)?._def?.description;
|
|
100
|
+
if (typeof description !== 'string' || !description) return json;
|
|
101
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) return json;
|
|
102
|
+
return { ...json, description };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function convertSchema(schema: unknown, depth: number): unknown {
|
|
84
106
|
if (depth > MAX_SCHEMA_DEPTH) return {};
|
|
85
107
|
const s = schema as { _def?: { typeName?: string }; toJSON?: () => unknown };
|
|
86
108
|
// Only honor a pre-serialized schema's `toJSON` for NON-zod objects (a plain
|
package/src/provider.ts
CHANGED
|
@@ -160,6 +160,16 @@ export interface ModelDescriptor {
|
|
|
160
160
|
export interface LLMProvider {
|
|
161
161
|
readonly name: string;
|
|
162
162
|
readonly models: ReadonlyArray<ModelDescriptor>;
|
|
163
|
+
/**
|
|
164
|
+
* Optional provider-owned metadata preparation for a dynamically selected
|
|
165
|
+
* model. The runtime awaits it before it calculates context budgets, so a
|
|
166
|
+
* provider whose catalog is discovered at runtime can publish the exact
|
|
167
|
+
* descriptor without trusting a client-supplied context-window value.
|
|
168
|
+
*/
|
|
169
|
+
prepareModel?(
|
|
170
|
+
model: string,
|
|
171
|
+
options?: { readonly signal?: AbortSignal },
|
|
172
|
+
): Promise<void>;
|
|
163
173
|
stream(req: ProviderRequest): AsyncIterable<ProviderEvent>;
|
|
164
174
|
countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number>;
|
|
165
175
|
}
|
|
@@ -202,6 +212,18 @@ export interface ProviderAuthContext {
|
|
|
202
212
|
* whether they're running inside a wizard or a one-shot command.
|
|
203
213
|
*/
|
|
204
214
|
readonly write: (chunk: string) => void;
|
|
215
|
+
/**
|
|
216
|
+
* True when a GUI host owns browser opening. OAuth implementations still
|
|
217
|
+
* build the loopback URL and report it through {@link onAuthUrl}, but must not
|
|
218
|
+
* invoke their process-local browser helper.
|
|
219
|
+
*/
|
|
220
|
+
readonly noOpen?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Browser authorization URL produced by a loopback flow. GUI hosts relay it
|
|
223
|
+
* over their authenticated transport, validate it again, and open it using
|
|
224
|
+
* their native browser API.
|
|
225
|
+
*/
|
|
226
|
+
readonly onAuthUrl?: (url: string) => void;
|
|
205
227
|
/**
|
|
206
228
|
* Optional single-line input prompt. Present when the host has an
|
|
207
229
|
* interactive TTY; absent in headless runs. Flows that need the user to
|
|
@@ -305,6 +327,12 @@ export interface ProviderOAuthStatus {
|
|
|
305
327
|
export interface ProviderDef {
|
|
306
328
|
readonly name: string;
|
|
307
329
|
readonly models: ReadonlyArray<ModelDescriptor>;
|
|
330
|
+
/**
|
|
331
|
+
* True when the provider can return its current model catalog at runtime.
|
|
332
|
+
* Hosts use this capability signal to offer live discovery instead of
|
|
333
|
+
* presenting the provider's build-time fallback catalog as authoritative.
|
|
334
|
+
*/
|
|
335
|
+
readonly supportsLiveModelDiscovery?: boolean;
|
|
308
336
|
createClient(config: Record<string, unknown>): LLMProvider;
|
|
309
337
|
/**
|
|
310
338
|
* Optional provider-owned credential/config resolver. Hosts call this before
|
package/src/server.ts
CHANGED
|
@@ -73,3 +73,14 @@ export {
|
|
|
73
73
|
MOXXY_WS_SUBPROTOCOL,
|
|
74
74
|
MOXXY_WS_BEARER_PROTOCOL_PREFIX,
|
|
75
75
|
} from './channel-auth.js';
|
|
76
|
+
export {
|
|
77
|
+
execExecutableTargetSync,
|
|
78
|
+
executableCandidates,
|
|
79
|
+
findExecutable,
|
|
80
|
+
resolveExecutableTarget,
|
|
81
|
+
spawnExecutableTarget,
|
|
82
|
+
type ExecutableSearchOptions,
|
|
83
|
+
type ExecutableTarget,
|
|
84
|
+
type SafeExecFileSyncOptions,
|
|
85
|
+
type SafeSpawnOptions,
|
|
86
|
+
} from './process-invocation.js';
|