@jini-ai/agentic 0.3.7 → 0.4.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/CHANGELOG.md +94 -0
- package/README.md +33 -6
- package/dist/core/a2ui/agent-to-renderer.d.ts +3 -1
- package/dist/core/a2ui/agent-to-renderer.js +1 -1
- package/dist/core/a2ui/catalog.d.ts +14 -5
- package/dist/core/a2ui/catalog.js +5 -5
- package/dist/core/a2ui/common-types.d.ts +11 -3
- package/dist/core/a2ui/common-types.js +5 -3
- package/dist/core/a2ui/index.d.ts +1 -1
- package/dist/core/a2ui/index.js +1 -1
- package/dist/core/a2ui/interpreter.d.ts +45 -10
- package/dist/core/a2ui/interpreter.js +38 -43
- package/dist/core/a2ui/json-pointer.d.ts +16 -4
- package/dist/core/a2ui/json-pointer.js +7 -7
- package/dist/core/a2ui/renderer-to-agent.d.ts +22 -8
- package/dist/core/a2ui/renderer-to-agent.js +5 -5
- package/dist/core/a2ui/resolve.d.ts +7 -2
- package/dist/core/a2ui/resolve.js +10 -9
- package/dist/core/a2ui/tree.d.ts +8 -2
- package/dist/core/a2ui/tree.js +2 -2
- package/dist/core/ag-ui.d.ts +17 -9
- package/dist/core/ag-ui.js +4 -4
- package/dist/core/capability.d.ts +12 -3
- package/dist/core/capability.js +3 -3
- package/dist/core/dom/dom-page-driver.d.ts +8 -4
- package/dist/core/dom/dom-page-driver.js +19 -20
- package/dist/core/dom/index.d.ts +3 -3
- package/dist/core/dom/index.js +1 -1
- package/dist/core/dom/model-context.d.ts +18 -8
- package/dist/core/dom/model-context.js +12 -15
- package/dist/core/dom/native-model-context.d.ts +6 -0
- package/dist/core/dom/native-model-context.js +19 -0
- package/dist/core/element-handles.d.ts +6 -2
- package/dist/core/element-handles.js +3 -3
- package/dist/core/gen-ui/encoder.d.ts +21 -11
- package/dist/core/gen-ui/encoder.js +8 -7
- package/dist/core/gen-ui/events.d.ts +6 -5
- package/dist/core/gen-ui/events.js +6 -5
- package/dist/core/gen-ui/index.d.ts +1 -1
- package/dist/core/gen-ui/index.js +1 -1
- package/dist/core/guards.d.ts +17 -5
- package/dist/core/guards.js +6 -6
- package/dist/core/handle.d.ts +12 -5
- package/dist/core/handle.js +9 -8
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/list-handles.d.ts +4 -1
- package/dist/core/list-handles.js +1 -1
- package/dist/core/mcp-ui-apps.d.ts +39 -9
- package/dist/core/mcp-ui-apps.js +10 -8
- package/dist/core/messages.d.ts +6 -0
- package/dist/core/messages.js +6 -0
- package/dist/core/page-capabilities.d.ts +0 -11
- package/dist/core/page-capabilities.js +13 -1
- package/dist/core/page-driver.d.ts +32 -11
- package/dist/core/page-executor.d.ts +8 -2
- package/dist/core/page-executor.js +31 -31
- package/dist/core/webmcp.d.ts +27 -8
- package/dist/core/webmcp.js +14 -14
- package/dist/index.d.ts +30 -0
- package/dist/index.js +30 -0
- package/dist/skills/install/archive.d.ts +8 -0
- package/dist/skills/install/archive.js +47 -0
- package/dist/skills/install/fetch-adapter.d.ts +6 -0
- package/dist/skills/install/fetch-adapter.js +6 -0
- package/dist/skills/install/github.d.ts +21 -0
- package/dist/skills/install/github.js +92 -0
- package/dist/skills/install/index.d.ts +11 -0
- package/dist/skills/install/index.js +11 -0
- package/dist/skills/install/install-service.d.ts +44 -0
- package/dist/skills/install/install-service.js +121 -0
- package/dist/skills/install/layout.d.ts +13 -0
- package/dist/skills/install/layout.js +22 -0
- package/dist/skills/install/live-registration.d.ts +61 -0
- package/dist/skills/install/live-registration.js +64 -0
- package/dist/skills/install/node-filesystem.d.ts +4 -0
- package/dist/skills/install/node-filesystem.js +41 -0
- package/dist/skills/install/ports.d.ts +101 -0
- package/dist/skills/install/ports.js +2 -0
- package/dist/skills/install/state.d.ts +13 -0
- package/dist/skills/install/state.js +23 -0
- package/dist/skills/install/validation.d.ts +45 -0
- package/dist/skills/install/validation.js +104 -0
- package/package.json +20 -5
|
@@ -34,7 +34,7 @@ export const MAX_HIGHLIGHT_MS = 15_000;
|
|
|
34
34
|
export const MAX_STATEFUL_ELEMENTS = 50;
|
|
35
35
|
function requireHandle(input, key) {
|
|
36
36
|
const value = input[key];
|
|
37
|
-
if (typeof value !== 'string' || !isValidElementHandle(value)) {
|
|
37
|
+
if (typeof value !== 'string' || !isValidElementHandle({ handle: value })) {
|
|
38
38
|
throw new Error(`"${key}" must be a published element handle from page.find_elements — `
|
|
39
39
|
+ 'lowercase words joined by single hyphens, never a CSS selector');
|
|
40
40
|
}
|
|
@@ -56,8 +56,8 @@ function isAgentElementRole(value) {
|
|
|
56
56
|
* guard does — a secrecy rule re-derived by each driver is a secrecy rule that will eventually be
|
|
57
57
|
* derived wrong. A value arriving with no descriptor to check is withheld rather than trusted.
|
|
58
58
|
*/
|
|
59
|
-
export function projectElementState(raw) {
|
|
60
|
-
const text = normalizeAgentLabel(raw.text);
|
|
59
|
+
export function projectElementState({ raw }, _optional = {}) {
|
|
60
|
+
const text = normalizeAgentLabel({ raw: raw.text });
|
|
61
61
|
// Computed once, up front, so `checked` is gated by the exact same refusal as `value` below
|
|
62
62
|
// rather than being copied through unconditionally. A checkbox/radio's `checked` is a second
|
|
63
63
|
// secrecy channel, not a formatting detail: for a field like `health_disclosure_password`,
|
|
@@ -68,7 +68,7 @@ export function projectElementState(raw) {
|
|
|
68
68
|
// it, exactly like a `<select>`'s `options` below. The secret is which option is checked, not
|
|
69
69
|
// what the option is called; withholding the label too would blind a caller to the page's own
|
|
70
70
|
// ontology for no reduction in what actually leaks.
|
|
71
|
-
const refusal = raw.field !== undefined ? findFieldReadRefusal(raw.field) : null;
|
|
71
|
+
const refusal = raw.field !== undefined ? findFieldReadRefusal({ field: raw.field }) : null;
|
|
72
72
|
// The one element whose `text` is NOT page-authored chrome: an editable region, whose contents
|
|
73
73
|
// are the user's data and whose driver has no `value` to report them as. It is gated by exactly
|
|
74
74
|
// the rules `value` is gated by below, including the "arrived with nothing to check it against"
|
|
@@ -77,7 +77,7 @@ export function projectElementState(raw) {
|
|
|
77
77
|
? undefined
|
|
78
78
|
: raw.field === undefined
|
|
79
79
|
? 'this element reported contents without the attributes needed to check them for secrets'
|
|
80
|
-
: refusal !== null ? describeFieldReadRefusal(refusal) : undefined;
|
|
80
|
+
: refusal !== null ? describeFieldReadRefusal({ refusal }) : undefined;
|
|
81
81
|
const base = {
|
|
82
82
|
text: withheldText === undefined ? text.text : '',
|
|
83
83
|
textTruncated: withheldText === undefined ? text.truncated : false,
|
|
@@ -88,7 +88,7 @@ export function projectElementState(raw) {
|
|
|
88
88
|
// Bounded like every other page-authored string, but never withheld: the options a dropdown
|
|
89
89
|
// offers are part of the UI's shape, not the user's data.
|
|
90
90
|
...(raw.options !== undefined
|
|
91
|
-
? { options: raw.options.map((option) => normalizeAgentLabel(option).text) }
|
|
91
|
+
? { options: raw.options.map((option) => normalizeAgentLabel({ raw: option }).text) }
|
|
92
92
|
: {}),
|
|
93
93
|
};
|
|
94
94
|
if (raw.value === undefined)
|
|
@@ -100,8 +100,8 @@ export function projectElementState(raw) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
if (refusal !== null)
|
|
103
|
-
return { ...base, valueWithheld: describeFieldReadRefusal(refusal) };
|
|
104
|
-
const value = normalizeAgentLabel(raw.value);
|
|
103
|
+
return { ...base, valueWithheld: describeFieldReadRefusal({ refusal }) };
|
|
104
|
+
const value = normalizeAgentLabel({ raw: raw.value });
|
|
105
105
|
return { ...base, value: value.text, valueTruncated: value.truncated };
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
@@ -133,17 +133,17 @@ function sameElementState(a, b) {
|
|
|
133
133
|
* not report state.
|
|
134
134
|
*/
|
|
135
135
|
async function observeElement(driver, handle) {
|
|
136
|
-
if (driver.describeState === undefined || !isValidElementHandle(handle))
|
|
136
|
+
if (driver.describeState === undefined || !isValidElementHandle({ handle }))
|
|
137
137
|
return undefined;
|
|
138
|
-
const raw = await driver.describeState(handle);
|
|
139
|
-
return raw === null ? undefined : projectElementState(raw);
|
|
138
|
+
const raw = await driver.describeState({ handle });
|
|
139
|
+
return raw === null ? undefined : projectElementState({ raw });
|
|
140
140
|
}
|
|
141
141
|
/** Runs a write between two readings of its target, waiting for the surface to settle in between. */
|
|
142
142
|
async function observeWrite(driver, handle, write) {
|
|
143
143
|
const before = await observeElement(driver, handle);
|
|
144
144
|
await write();
|
|
145
145
|
if (driver.settle !== undefined)
|
|
146
|
-
await driver.settle();
|
|
146
|
+
await driver.settle({});
|
|
147
147
|
const after = await observeElement(driver, handle);
|
|
148
148
|
return {
|
|
149
149
|
...(before !== undefined ? { before } : {}),
|
|
@@ -158,7 +158,7 @@ async function observeWrite(driver, handle, write) {
|
|
|
158
158
|
* surface that publishes nothing has no page id to report, and says so rather than guessing.
|
|
159
159
|
*/
|
|
160
160
|
async function summarizePage(driver) {
|
|
161
|
-
const elements = await driver.findElements({});
|
|
161
|
+
const elements = await driver.findElements({}, {});
|
|
162
162
|
return {
|
|
163
163
|
page: elements.find((element) => element.page !== undefined)?.page,
|
|
164
164
|
elementCount: elements.length,
|
|
@@ -175,11 +175,11 @@ async function summarizePage(driver) {
|
|
|
175
175
|
* a field no automated caller may fill, or the page is not published. Refusals are errors on
|
|
176
176
|
* purpose: a caller must be told why, not handed a silent no-op it will retry forever.
|
|
177
177
|
*/
|
|
178
|
-
export async function executePageCapability(driver, capabilityId, input) {
|
|
178
|
+
export async function executePageCapability({ driver, capabilityId, input }, _optional = {}) {
|
|
179
179
|
const capability = PAGE_CAPABILITIES.find((entry) => entry.id === capabilityId);
|
|
180
180
|
if (capability === undefined)
|
|
181
181
|
throw new Error(`unknown page capability: ${capabilityId}`);
|
|
182
|
-
const inputError = findCapabilityInputError(capability, input);
|
|
182
|
+
const inputError = findCapabilityInputError({ capability, input });
|
|
183
183
|
if (inputError !== null) {
|
|
184
184
|
// The schema rides along with the refusal rather than making a caller spend a second
|
|
185
185
|
// round trip on describe_tool to learn what it already tried to guess — ai-control-plane.md
|
|
@@ -202,11 +202,11 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
202
202
|
...(isAgentElementRole(role) ? { role } : {}),
|
|
203
203
|
...(typeof input['query'] === 'string' ? { query: input['query'] } : {}),
|
|
204
204
|
};
|
|
205
|
-
const [found, rawPages] = await Promise.all([driver.findElements(filter), driver.listPages()]);
|
|
205
|
+
const [found, rawPages] = await Promise.all([driver.findElements({}, filter), driver.listPages({})]);
|
|
206
206
|
const withState = input['withState'] === true;
|
|
207
207
|
const observing = withState && driver.describeState !== undefined;
|
|
208
208
|
const elements = await Promise.all(found.map(async (element, index) => {
|
|
209
|
-
const label = normalizeAgentLabel(element.label);
|
|
209
|
+
const label = normalizeAgentLabel({ raw: element.label });
|
|
210
210
|
const base = {
|
|
211
211
|
handle: element.handle,
|
|
212
212
|
role: element.role,
|
|
@@ -224,7 +224,7 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
224
224
|
// every `data-agent-page` label short and clean.
|
|
225
225
|
const pages = rawPages.map((page) => ({
|
|
226
226
|
id: page.id,
|
|
227
|
-
label: normalizeAgentLabel(page.label).text,
|
|
227
|
+
label: normalizeAgentLabel({ raw: page.label }).text,
|
|
228
228
|
}));
|
|
229
229
|
return {
|
|
230
230
|
elements,
|
|
@@ -239,17 +239,17 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
239
239
|
case 'page.highlight': {
|
|
240
240
|
const handle = requireHandle(input, 'handle');
|
|
241
241
|
const durationMs = clampHighlightDuration(input['durationMs']);
|
|
242
|
-
await driver.highlight(handle, durationMs);
|
|
242
|
+
await driver.highlight({ handle, durationMs });
|
|
243
243
|
return { highlighted: handle, durationMs };
|
|
244
244
|
}
|
|
245
245
|
case 'page.scroll_to': {
|
|
246
246
|
const handle = requireHandle(input, 'handle');
|
|
247
|
-
await driver.scrollTo(handle);
|
|
247
|
+
await driver.scrollTo({ handle });
|
|
248
248
|
return { scrolledTo: handle };
|
|
249
249
|
}
|
|
250
250
|
case 'page.click': {
|
|
251
251
|
const handle = requireHandle(input, 'handle');
|
|
252
|
-
const observation = await observeWrite(driver, handle, () => driver.click(handle));
|
|
252
|
+
const observation = await observeWrite(driver, handle, () => driver.click({ handle }));
|
|
253
253
|
return { clicked: handle, ...observation };
|
|
254
254
|
}
|
|
255
255
|
case 'page.fill': {
|
|
@@ -259,14 +259,14 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
259
259
|
const text = input['text'];
|
|
260
260
|
// Ask the page what this field actually is before writing to it. A handle proves the page
|
|
261
261
|
// published the element; it says nothing about whether it holds a password.
|
|
262
|
-
const field = await driver.describeField(handle);
|
|
262
|
+
const field = await driver.describeField({ handle });
|
|
263
263
|
if (field === null)
|
|
264
264
|
throw new Error(`"${handle}" is not a fillable field`);
|
|
265
|
-
const refusal = findFieldFillRefusal(field);
|
|
265
|
+
const refusal = findFieldFillRefusal({ field });
|
|
266
266
|
if (refusal !== null) {
|
|
267
|
-
throw new Error(`refusing to fill "${handle}": ${describeFieldRefusal(refusal)}`);
|
|
267
|
+
throw new Error(`refusing to fill "${handle}": ${describeFieldRefusal({ refusal })}`);
|
|
268
268
|
}
|
|
269
|
-
const observation = await observeWrite(driver, handle, () => driver.fill(handle, text));
|
|
269
|
+
const observation = await observeWrite(driver, handle, () => driver.fill({ handle, text }));
|
|
270
270
|
return { filled: handle, ...observation };
|
|
271
271
|
}
|
|
272
272
|
case 'page.select_option': {
|
|
@@ -279,7 +279,7 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
279
279
|
// No field guard: a dropdown's options are authored by the page, so choosing one reveals
|
|
280
280
|
// nothing the page had not already published, and none of the credential field types this
|
|
281
281
|
// surface refuses can be a `<select>`.
|
|
282
|
-
const observation = await observeWrite(driver, handle, () => driver.selectOption(handle, option, selected));
|
|
282
|
+
const observation = await observeWrite(driver, handle, () => driver.selectOption({ handle, option }, { selected }));
|
|
283
283
|
// `optionSelected` names the boolean rather than reusing `selected`, which already means
|
|
284
284
|
// "the handle this call acted on" for every verb in this switch (`clicked`, `filled`, …).
|
|
285
285
|
return { selected: handle, option, optionSelected: selected, ...observation };
|
|
@@ -287,7 +287,7 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
287
287
|
case 'page.navigate': {
|
|
288
288
|
// Required and string-typed by the manifest, already enforced above. See page.fill.
|
|
289
289
|
const page = input['page'];
|
|
290
|
-
const pages = await driver.listPages();
|
|
290
|
+
const pages = await driver.listPages({});
|
|
291
291
|
if (!pages.some((candidate) => candidate.id === page)) {
|
|
292
292
|
// Every other piece of page-authored (or, here, caller-authored-but-page-shaped) text
|
|
293
293
|
// this system hands to a model goes through normalizeAgentLabel first — bounded, control-
|
|
@@ -301,14 +301,14 @@ export async function executePageCapability(driver, capabilityId, input) {
|
|
|
301
301
|
// is looking straight at the real menu, so it is worth spending the label here even though
|
|
302
302
|
// `page.find_elements` already carries the same information — a caller that jumped
|
|
303
303
|
// straight to `page.navigate` without calling that first still lands on the right id.
|
|
304
|
-
const safePage = normalizeAgentLabel(page).text;
|
|
305
|
-
const safePages = pages.map((candidate) => `${normalizeAgentLabel(candidate.id).text} (${normalizeAgentLabel(candidate.label).text})`);
|
|
304
|
+
const safePage = normalizeAgentLabel({ raw: page }).text;
|
|
305
|
+
const safePages = pages.map((candidate) => `${normalizeAgentLabel({ raw: candidate.id }).text} (${normalizeAgentLabel({ raw: candidate.label }).text})`);
|
|
306
306
|
throw new Error(`"${safePage}" is not a published page. Available: ${safePages.length > 0 ? safePages.join(', ') : '(none)'}`);
|
|
307
307
|
}
|
|
308
308
|
const before = await summarizePage(driver);
|
|
309
|
-
await driver.navigate(page);
|
|
309
|
+
await driver.navigate({ page });
|
|
310
310
|
if (driver.settle !== undefined)
|
|
311
|
-
await driver.settle();
|
|
311
|
+
await driver.settle({});
|
|
312
312
|
const after = await summarizePage(driver);
|
|
313
313
|
return { navigatedTo: page, before, after };
|
|
314
314
|
}
|
package/dist/core/webmcp.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* tool. Neither belongs on the `ModelContextTool` dict itself, so they are not fields of
|
|
28
28
|
* {@link WebMcpToolRegistration} — they ride along on its optional `registerOptions`, computed
|
|
29
29
|
* from {@link ToWebMcpToolOptions}, so a caller has exactly one object to destructure:
|
|
30
|
-
* `modelContext.registerTool(reg, reg.registerOptions)`.
|
|
30
|
+
* `modelContext.registerTool({ tool: reg }, reg.registerOptions)`.
|
|
31
31
|
* 2. **`title`/`annotations`** — real `ModelContextTool` dict fields (`USVString title`,
|
|
32
32
|
* `ToolAnnotations annotations`), now on {@link WebMcpToolRegistration}. `annotations.readOnlyHint`
|
|
33
33
|
* defaults from {@link CapabilityDef.risk} (`'read'` → `true`) since Jini's manifests already
|
|
@@ -68,7 +68,7 @@ export interface WebMcpToolRegistration {
|
|
|
68
68
|
* `ModelContextRegisterToolOptions`, not part of the `ModelContextTool` dict this object
|
|
69
69
|
* otherwise models. Present only when `options.signal` or `options.exposedTo` was supplied.
|
|
70
70
|
* Bundled here rather than returned as a second value so `toWebMcpTool`'s output stays a single
|
|
71
|
-
* object a caller can pass straight through: `modelContext.registerTool(reg, reg.registerOptions)`.
|
|
71
|
+
* object a caller can pass straight through: `modelContext.registerTool({ tool: reg }, reg.registerOptions)`.
|
|
72
72
|
*/
|
|
73
73
|
readonly registerOptions?: WebMcpRegisterToolOptions;
|
|
74
74
|
}
|
|
@@ -146,10 +146,15 @@ export interface ToWebMcpToolOptions {
|
|
|
146
146
|
export declare class WebMcpConfirmationRequiredError extends Error {
|
|
147
147
|
readonly capabilityId: string;
|
|
148
148
|
readonly reason: 'declined' | 'no-handler';
|
|
149
|
-
constructor(capabilityId
|
|
149
|
+
constructor({ capabilityId, reason }: {
|
|
150
|
+
capabilityId: string;
|
|
151
|
+
reason: 'declined' | 'no-handler';
|
|
152
|
+
}, _optional?: Record<string, never>);
|
|
150
153
|
}
|
|
151
154
|
/** Whether `name` satisfies the spec's tool-name rule. Exported so a caller can pre-check a manifest without registering anything. */
|
|
152
|
-
export declare function isValidWebMcpToolName(name
|
|
155
|
+
export declare function isValidWebMcpToolName({ name }: {
|
|
156
|
+
name: string;
|
|
157
|
+
}, _optional?: Record<string, never>): boolean;
|
|
153
158
|
/**
|
|
154
159
|
* Thrown synchronously by {@link toWebMcpTool} for a capability whose `id` cannot legally become a
|
|
155
160
|
* WebMCP tool name. Distinct from {@link WebMcpConfirmationRequiredError}: this is a manifest
|
|
@@ -157,7 +162,9 @@ export declare function isValidWebMcpToolName(name: string): boolean;
|
|
|
157
162
|
*/
|
|
158
163
|
export declare class InvalidWebMcpToolNameError extends Error {
|
|
159
164
|
readonly capabilityId: string;
|
|
160
|
-
constructor(capabilityId:
|
|
165
|
+
constructor({ capabilityId }: {
|
|
166
|
+
capabilityId: string;
|
|
167
|
+
}, _optional?: Record<string, never>);
|
|
161
168
|
}
|
|
162
169
|
/**
|
|
163
170
|
* Projects one capability into a WebMCP registration.
|
|
@@ -173,7 +180,7 @@ export declare class InvalidWebMcpToolNameError extends Error {
|
|
|
173
180
|
* capability reaching the page with nothing in front of it — not the authorization gap.
|
|
174
181
|
*
|
|
175
182
|
* @param capability - The capability to expose.
|
|
176
|
-
* @param execute - Runs the capability. Receives
|
|
183
|
+
* @param execute - Runs the capability. Receives `{ id, args }`, with args already checked against
|
|
177
184
|
* `capability.inputSchema` — a caller-supplied value that fails validation never reaches this.
|
|
178
185
|
* @param options - Optional confirmation handler, title/annotations, and registration options
|
|
179
186
|
* (`signal`/`exposedTo`); required in practice for any capability declaring `requiresConfirmation`.
|
|
@@ -183,7 +190,13 @@ export declare class InvalidWebMcpToolNameError extends Error {
|
|
|
183
190
|
* @throws {@link WebMcpConfirmationRequiredError} from `execute`, when confirmation is required
|
|
184
191
|
* and is either declined or unobtainable.
|
|
185
192
|
*/
|
|
186
|
-
export declare function toWebMcpTool(capability
|
|
193
|
+
export declare function toWebMcpTool({ capability, execute }: {
|
|
194
|
+
capability: CapabilityDef;
|
|
195
|
+
execute: (required: {
|
|
196
|
+
id: string;
|
|
197
|
+
args: Record<string, unknown>;
|
|
198
|
+
}) => Promise<unknown>;
|
|
199
|
+
}, options?: ToWebMcpToolOptions): WebMcpToolRegistration;
|
|
187
200
|
/**
|
|
188
201
|
* Projects a whole manifest.
|
|
189
202
|
*
|
|
@@ -198,5 +211,11 @@ export declare function toWebMcpTool(capability: CapabilityDef, execute: (id: st
|
|
|
198
211
|
* @throws {@link InvalidWebMcpToolNameError} synchronously, for the first capability whose `id`
|
|
199
212
|
* cannot be a WebMCP tool name.
|
|
200
213
|
*/
|
|
201
|
-
export declare function toWebMcpTools(capabilities
|
|
214
|
+
export declare function toWebMcpTools({ capabilities, execute }: {
|
|
215
|
+
capabilities: readonly CapabilityDef[];
|
|
216
|
+
execute: (required: {
|
|
217
|
+
id: string;
|
|
218
|
+
args: Record<string, unknown>;
|
|
219
|
+
}) => Promise<unknown>;
|
|
220
|
+
}, options?: ToWebMcpToolOptions): readonly WebMcpToolRegistration[];
|
|
202
221
|
//# sourceMappingURL=webmcp.d.ts.map
|
package/dist/core/webmcp.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* tool. Neither belongs on the `ModelContextTool` dict itself, so they are not fields of
|
|
28
28
|
* {@link WebMcpToolRegistration} — they ride along on its optional `registerOptions`, computed
|
|
29
29
|
* from {@link ToWebMcpToolOptions}, so a caller has exactly one object to destructure:
|
|
30
|
-
* `modelContext.registerTool(reg, reg.registerOptions)`.
|
|
30
|
+
* `modelContext.registerTool({ tool: reg }, reg.registerOptions)`.
|
|
31
31
|
* 2. **`title`/`annotations`** — real `ModelContextTool` dict fields (`USVString title`,
|
|
32
32
|
* `ToolAnnotations annotations`), now on {@link WebMcpToolRegistration}. `annotations.readOnlyHint`
|
|
33
33
|
* defaults from {@link CapabilityDef.risk} (`'read'` → `true`) since Jini's manifests already
|
|
@@ -52,7 +52,7 @@ import { findCapabilityInputError } from './capability.js';
|
|
|
52
52
|
export class WebMcpConfirmationRequiredError extends Error {
|
|
53
53
|
capabilityId;
|
|
54
54
|
reason;
|
|
55
|
-
constructor(capabilityId, reason) {
|
|
55
|
+
constructor({ capabilityId, reason }, _optional = {}) {
|
|
56
56
|
super(reason === 'declined'
|
|
57
57
|
? `WebMCP: "${capabilityId}" was declined by the user`
|
|
58
58
|
: `WebMCP: "${capabilityId}" requires confirmation, but no requestUserInteraction handler was supplied`);
|
|
@@ -69,7 +69,7 @@ export class WebMcpConfirmationRequiredError extends Error {
|
|
|
69
69
|
*/
|
|
70
70
|
const WEBMCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
71
71
|
/** Whether `name` satisfies the spec's tool-name rule. Exported so a caller can pre-check a manifest without registering anything. */
|
|
72
|
-
export function isValidWebMcpToolName(name) {
|
|
72
|
+
export function isValidWebMcpToolName({ name }, _optional = {}) {
|
|
73
73
|
return WEBMCP_TOOL_NAME_PATTERN.test(name);
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
@@ -79,7 +79,7 @@ export function isValidWebMcpToolName(name) {
|
|
|
79
79
|
*/
|
|
80
80
|
export class InvalidWebMcpToolNameError extends Error {
|
|
81
81
|
capabilityId;
|
|
82
|
-
constructor(capabilityId) {
|
|
82
|
+
constructor({ capabilityId }, _optional = {}) {
|
|
83
83
|
super(`WebMCP: capability id "${capabilityId}" is not a valid tool name — the spec requires 1-128 `
|
|
84
84
|
+ 'ASCII alphanumeric characters plus "_", "-", "." '
|
|
85
85
|
+ '(https://webmachinelearning.github.io/webmcp/#dom-modelcontext-registertool)');
|
|
@@ -101,7 +101,7 @@ export class InvalidWebMcpToolNameError extends Error {
|
|
|
101
101
|
* capability reaching the page with nothing in front of it — not the authorization gap.
|
|
102
102
|
*
|
|
103
103
|
* @param capability - The capability to expose.
|
|
104
|
-
* @param execute - Runs the capability. Receives
|
|
104
|
+
* @param execute - Runs the capability. Receives `{ id, args }`, with args already checked against
|
|
105
105
|
* `capability.inputSchema` — a caller-supplied value that fails validation never reaches this.
|
|
106
106
|
* @param options - Optional confirmation handler, title/annotations, and registration options
|
|
107
107
|
* (`signal`/`exposedTo`); required in practice for any capability declaring `requiresConfirmation`.
|
|
@@ -111,9 +111,9 @@ export class InvalidWebMcpToolNameError extends Error {
|
|
|
111
111
|
* @throws {@link WebMcpConfirmationRequiredError} from `execute`, when confirmation is required
|
|
112
112
|
* and is either declined or unobtainable.
|
|
113
113
|
*/
|
|
114
|
-
export function toWebMcpTool(capability, execute, options = {}) {
|
|
115
|
-
if (!isValidWebMcpToolName(capability.id)) {
|
|
116
|
-
throw new InvalidWebMcpToolNameError(capability.id);
|
|
114
|
+
export function toWebMcpTool({ capability, execute }, options = {}) {
|
|
115
|
+
if (!isValidWebMcpToolName({ name: capability.id })) {
|
|
116
|
+
throw new InvalidWebMcpToolNameError({ capabilityId: capability.id });
|
|
117
117
|
}
|
|
118
118
|
const { title, annotations, signal, exposedTo } = options;
|
|
119
119
|
const resolvedAnnotations = {
|
|
@@ -133,21 +133,21 @@ export function toWebMcpTool(capability, execute, options = {}) {
|
|
|
133
133
|
// Schema-on-error, matching page-executor.ts's discipline for page.* — WebMCP defines no
|
|
134
134
|
// input validation of its own, so a wrong-schema call would otherwise reach `execute` (or a
|
|
135
135
|
// confirmation prompt for one) with bad arguments instead of a same-turn correctable error.
|
|
136
|
-
const inputError = findCapabilityInputError(capability, input);
|
|
136
|
+
const inputError = findCapabilityInputError({ capability, input });
|
|
137
137
|
if (inputError !== null) {
|
|
138
138
|
throw new Error(`${capability.id}: ${inputError}. Expected input: ${JSON.stringify(capability.inputSchema)}`);
|
|
139
139
|
}
|
|
140
140
|
if (capability.requiresConfirmation === true) {
|
|
141
141
|
const { requestUserInteraction } = options;
|
|
142
142
|
if (!requestUserInteraction) {
|
|
143
|
-
throw new WebMcpConfirmationRequiredError(capability.id, 'no-handler');
|
|
143
|
+
throw new WebMcpConfirmationRequiredError({ capabilityId: capability.id, reason: 'no-handler' });
|
|
144
144
|
}
|
|
145
145
|
const approved = await requestUserInteraction({ capability, args: input });
|
|
146
146
|
if (approved !== true) {
|
|
147
|
-
throw new WebMcpConfirmationRequiredError(capability.id, 'declined');
|
|
147
|
+
throw new WebMcpConfirmationRequiredError({ capabilityId: capability.id, reason: 'declined' });
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
return execute(capability.id, input);
|
|
150
|
+
return execute({ id: capability.id, args: input });
|
|
151
151
|
},
|
|
152
152
|
annotations: resolvedAnnotations,
|
|
153
153
|
...(registerOptions !== undefined ? { registerOptions } : {}),
|
|
@@ -167,7 +167,7 @@ export function toWebMcpTool(capability, execute, options = {}) {
|
|
|
167
167
|
* @throws {@link InvalidWebMcpToolNameError} synchronously, for the first capability whose `id`
|
|
168
168
|
* cannot be a WebMCP tool name.
|
|
169
169
|
*/
|
|
170
|
-
export function toWebMcpTools(capabilities, execute, options = {}) {
|
|
171
|
-
return capabilities.map((capability) => toWebMcpTool(capability, execute, options));
|
|
170
|
+
export function toWebMcpTools({ capabilities, execute }, options = {}) {
|
|
171
|
+
return capabilities.map((capability) => toWebMcpTool({ capability, execute }, options));
|
|
172
172
|
}
|
|
173
173
|
//# sourceMappingURL=webmcp.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
* Compatibility root — re-exports `./core` unchanged so existing `from '@jini-ai/agentic'`
|
|
3
3
|
* imports keep resolving. New code should prefer the explicit `@jini-ai/agentic/core` import;
|
|
4
4
|
* see `core/README.md` for why this package is split the way it is.
|
|
5
|
+
|
|
6
|
+
* Archived provenance rationale:
|
|
7
|
+
* ## The DOM split (why this package has two entry points)
|
|
8
|
+
*
|
|
9
|
+
* Everything under `src/` except `src/dom/` compiles under `tsconfig.json`, which extends the repo's
|
|
10
|
+
* `tsconfig.base.json` unmodified (`lib: ["ES2023"]`, no `DOM`) and **excludes `src/dom` entirely**.
|
|
11
|
+
* `src/dom/` compiles separately under `tsconfig.dom.json`, the only config in this package with
|
|
12
|
+
* `DOM`/`DOM.Iterable` in `lib`.
|
|
13
|
+
*
|
|
14
|
+
* This is a compile-time guarantee, not a convention: a `document`/`window` reference anywhere
|
|
15
|
+
* outside `src/dom/**` fails `tsc -p tsconfig.json` with "Cannot find name 'document'" (verified
|
|
16
|
+
* during the 2026-07-26 extraction — see the extraction's report for the exact command and error
|
|
17
|
+
* text). That is what proves the policy layer (`page-executor.ts`'s guards, refusals, and
|
|
18
|
+
* allowlist-enforcement) cannot quietly reach into a live page; only `src/dom/dom-page-driver.ts`,
|
|
19
|
+
* the one `PageDriver` implementation that legitimately needs a DOM, may.
|
|
20
|
+
*
|
|
21
|
+
* Two `package.json` exports follow the same split:
|
|
22
|
+
*
|
|
23
|
+
* - `.` (`dist/index.d.ts`/`dist/index.js`, built by `tsconfig.json`) — the DOM-free vocabulary,
|
|
24
|
+
* policy gate, and protocol projections. What `@jini/chat-core`, `@jini/daemon` (structurally, see
|
|
25
|
+
* below), and (once §5 of the plan lands) `@jini/ui` depend on.
|
|
26
|
+
* - `./dom` (`dist/dom/index.d.ts`/`dist/dom/index.js`, built by `tsconfig.dom.json`) — the browser
|
|
27
|
+
* `PageDriver`. What `@jini/chat-react` depends on.
|
|
28
|
+
*
|
|
29
|
+
* `scripts/check-engine-boundaries.ts` R2 normally allows only bare `@jini/<name>` imports (one
|
|
30
|
+
* entry point per package), with a single named exception for `@jini/core/internal`. This extraction
|
|
31
|
+
* added a second, identically-gated exception for the exact literal `@jini/agentic/dom` — not a
|
|
32
|
+
* pattern — because `@jini/chat-react` genuinely needs the DOM half and there is no third package for
|
|
33
|
+
* it to live in without recreating the sprawl this plan exists to reduce (23 packages vs. a locked
|
|
34
|
+
* 14). See that script's own module doc for the up-to-date rule list.
|
|
5
35
|
*/
|
|
6
36
|
export * from './core/index.js';
|
|
7
37
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
* Compatibility root — re-exports `./core` unchanged so existing `from '@jini-ai/agentic'`
|
|
3
3
|
* imports keep resolving. New code should prefer the explicit `@jini-ai/agentic/core` import;
|
|
4
4
|
* see `core/README.md` for why this package is split the way it is.
|
|
5
|
+
|
|
6
|
+
* Archived provenance rationale:
|
|
7
|
+
* ## The DOM split (why this package has two entry points)
|
|
8
|
+
*
|
|
9
|
+
* Everything under `src/` except `src/dom/` compiles under `tsconfig.json`, which extends the repo's
|
|
10
|
+
* `tsconfig.base.json` unmodified (`lib: ["ES2023"]`, no `DOM`) and **excludes `src/dom` entirely**.
|
|
11
|
+
* `src/dom/` compiles separately under `tsconfig.dom.json`, the only config in this package with
|
|
12
|
+
* `DOM`/`DOM.Iterable` in `lib`.
|
|
13
|
+
*
|
|
14
|
+
* This is a compile-time guarantee, not a convention: a `document`/`window` reference anywhere
|
|
15
|
+
* outside `src/dom/**` fails `tsc -p tsconfig.json` with "Cannot find name 'document'" (verified
|
|
16
|
+
* during the 2026-07-26 extraction — see the extraction's report for the exact command and error
|
|
17
|
+
* text). That is what proves the policy layer (`page-executor.ts`'s guards, refusals, and
|
|
18
|
+
* allowlist-enforcement) cannot quietly reach into a live page; only `src/dom/dom-page-driver.ts`,
|
|
19
|
+
* the one `PageDriver` implementation that legitimately needs a DOM, may.
|
|
20
|
+
*
|
|
21
|
+
* Two `package.json` exports follow the same split:
|
|
22
|
+
*
|
|
23
|
+
* - `.` (`dist/index.d.ts`/`dist/index.js`, built by `tsconfig.json`) — the DOM-free vocabulary,
|
|
24
|
+
* policy gate, and protocol projections. What `@jini/chat-core`, `@jini/daemon` (structurally, see
|
|
25
|
+
* below), and (once §5 of the plan lands) `@jini/ui` depend on.
|
|
26
|
+
* - `./dom` (`dist/dom/index.d.ts`/`dist/dom/index.js`, built by `tsconfig.dom.json`) — the browser
|
|
27
|
+
* `PageDriver`. What `@jini/chat-react` depends on.
|
|
28
|
+
*
|
|
29
|
+
* `scripts/check-engine-boundaries.ts` R2 normally allows only bare `@jini/<name>` imports (one
|
|
30
|
+
* entry point per package), with a single named exception for `@jini/core/internal`. This extraction
|
|
31
|
+
* added a second, identically-gated exception for the exact literal `@jini/agentic/dom` — not a
|
|
32
|
+
* pattern — because `@jini/chat-react` genuinely needs the DOM half and there is no third package for
|
|
33
|
+
* it to live in without recreating the sprawl this plan exists to reduce (23 packages vs. a locked
|
|
34
|
+
* 14). See that script's own module doc for the up-to-date rule list.
|
|
5
35
|
*/
|
|
6
36
|
export * from './core/index.js';
|
|
7
37
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ArchiveReaderPort } from "./ports.js";
|
|
2
|
+
import { type SkillUploadFile } from "./validation.js";
|
|
3
|
+
/** Read bounded archive content only; paths are validated before opening any entry stream. */
|
|
4
|
+
export declare function readSkillArchive({ base64, archiveReader }: {
|
|
5
|
+
base64: string;
|
|
6
|
+
archiveReader: ArchiveReaderPort;
|
|
7
|
+
}, _optional?: Record<string, never>): Promise<SkillUploadFile[]>;
|
|
8
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { decodeSkillBase64, MAX_SKILL_BYTES, MAX_SKILL_FILES, MAX_SKILL_FILE_BYTES, SkillInputError, validateSkillPath } from "./validation.js";
|
|
2
|
+
/** Read bounded archive content only; paths are validated before opening any entry stream. */
|
|
3
|
+
export async function readSkillArchive({ base64, archiveReader }, _optional = {}) {
|
|
4
|
+
try {
|
|
5
|
+
const reader = await archiveReader.open({ bytes: decodeSkillBase64({ value: base64 }) });
|
|
6
|
+
const files = [];
|
|
7
|
+
let total = 0;
|
|
8
|
+
let count = 0;
|
|
9
|
+
try {
|
|
10
|
+
for await (const entry of reader.entries) {
|
|
11
|
+
if (++count > MAX_SKILL_FILES)
|
|
12
|
+
throw new SkillInputError({ message: "A skill archive may contain at most 256 entries." });
|
|
13
|
+
validateSkillPath({ filePath: entry.path.replace(/\/$/, "") });
|
|
14
|
+
if (entry.kind !== "file" && entry.kind !== "directory")
|
|
15
|
+
throw new SkillInputError({ message: "Skill archives may contain only regular files and directories." });
|
|
16
|
+
if (entry.kind === "directory")
|
|
17
|
+
continue;
|
|
18
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > MAX_SKILL_FILE_BYTES)
|
|
19
|
+
throw new SkillInputError({ message: "Skill archive file exceeds 1 MiB." });
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
{
|
|
23
|
+
for await (const chunk of entry.read({})) {
|
|
24
|
+
size += chunk.length;
|
|
25
|
+
total += chunk.length;
|
|
26
|
+
if (size > MAX_SKILL_FILE_BYTES || total > MAX_SKILL_BYTES)
|
|
27
|
+
throw new SkillInputError({ message: "Skill archive exceeds its decompressed size limit." });
|
|
28
|
+
chunks.push(Buffer.from(chunk));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (size !== entry.size)
|
|
32
|
+
throw new SkillInputError({ message: "Skill archive file size does not match its metadata." });
|
|
33
|
+
files.push({ path: entry.path, contentBase64: Buffer.concat(chunks).toString("base64") });
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
reader.close({});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof SkillInputError)
|
|
43
|
+
throw error;
|
|
44
|
+
throw new SkillInputError({ message: "Could not read skill ZIP. Use a valid ZIP with regular files only." });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SkillFetchPort } from './ports.js';
|
|
2
|
+
/** Wrap an explicitly supplied native fetch without adding transport defaults or changing responses. */
|
|
3
|
+
export declare function createSkillFetchAdapter(required: {
|
|
4
|
+
fetch: typeof fetch;
|
|
5
|
+
}, _optional?: Record<string, never>): SkillFetchPort;
|
|
6
|
+
//# sourceMappingURL=fetch-adapter.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Wrap an explicitly supplied native fetch without adding transport defaults or changing responses. */
|
|
2
|
+
export function createSkillFetchAdapter(required, _optional = {}) {
|
|
3
|
+
const fetchImpl = required.fetch;
|
|
4
|
+
return ({ url }, optional = {}) => fetchImpl(url, optional);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=fetch-adapter.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Commit-pinned public GitHub import with one request/body deadline, byte caps and refused redirects. */
|
|
2
|
+
import { type SkillUploadFile } from "./validation.js";
|
|
3
|
+
import type { SkillFetchPort } from "./ports.js";
|
|
4
|
+
export interface GitHubSkillSource {
|
|
5
|
+
readonly githubUrl: string;
|
|
6
|
+
readonly commit: string;
|
|
7
|
+
}
|
|
8
|
+
/** Restricts every request to api.github.com, disallows redirects, pins one immutable commit.
|
|
9
|
+
* @complexity O(files + downloaded bytes), at most 256 file requests and 8 MiB decoded content.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchGitHubSkill({ githubUrl, fetch: fetchImpl }: {
|
|
12
|
+
githubUrl: string;
|
|
13
|
+
fetch: SkillFetchPort;
|
|
14
|
+
}, _optional?: Record<string, never>): Promise<{
|
|
15
|
+
files: SkillUploadFile[];
|
|
16
|
+
source: {
|
|
17
|
+
githubUrl: string;
|
|
18
|
+
commit: string;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=github.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Commit-pinned public GitHub import with one request/body deadline, byte caps and refused redirects. */
|
|
2
|
+
import { MAX_SKILL_BYTES, MAX_SKILL_FILES, MAX_SKILL_FILE_BYTES, decodeSkillBase64, SkillInputError } from "./validation.js";
|
|
3
|
+
/** Restricts every request to api.github.com, disallows redirects, pins one immutable commit.
|
|
4
|
+
* @complexity O(files + downloaded bytes), at most 256 file requests and 8 MiB decoded content.
|
|
5
|
+
*/
|
|
6
|
+
export async function fetchGitHubSkill({ githubUrl, fetch: fetchImpl }, _optional = {}) {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(githubUrl);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new SkillInputError({ message: "Use an HTTPS GitHub repository URL, optionally ending in /tree/ref/skill-folder." });
|
|
13
|
+
}
|
|
14
|
+
let parts;
|
|
15
|
+
try {
|
|
16
|
+
parts = url.pathname.replace(/\/$/, "").split("/").slice(1).map(p => decodeURIComponent(p));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new SkillInputError({ message: "Use an HTTPS GitHub repository URL, optionally ending in /tree/ref/skill-folder." });
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || url.search || url.hash || parts.length < 2 || (parts.length > 2 && (parts[2] !== "tree" || parts.length < 4)) || parts.some(p => !/^[a-zA-Z0-9_.-]+$/.test(p) || p === "." || p === ".."))
|
|
22
|
+
throw new SkillInputError({ message: "Use an HTTPS GitHub repository URL, optionally ending in /tree/ref/skill-folder." });
|
|
23
|
+
const owner = parts[0];
|
|
24
|
+
const repo = parts[1].replace(/\.git$/, "");
|
|
25
|
+
if (!repo)
|
|
26
|
+
throw new SkillInputError({ message: "Use an HTTPS GitHub repository URL, optionally ending in /tree/ref/skill-folder." });
|
|
27
|
+
const base = `https://api.github.com/repos/${owner}/${repo}`;
|
|
28
|
+
// One deadline for the whole fetch, including streaming response bodies.
|
|
29
|
+
const signal = AbortSignal.timeout(30_000);
|
|
30
|
+
async function json(endpoint) {
|
|
31
|
+
const response = await fetchImpl({ url: endpoint }, { redirect: "error", signal, headers: { Accept: "application/vnd.github+json" } });
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
throw new SkillInputError({ message: `GitHub could not fetch this skill (HTTP ${response.status}). Use a public repository URL.` });
|
|
34
|
+
if (!response.body)
|
|
35
|
+
throw new SkillInputError({ message: "GitHub returned an empty response." });
|
|
36
|
+
const reader = response.body.getReader();
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
const chunks = [];
|
|
39
|
+
try {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const part = await reader.read();
|
|
42
|
+
if (part.done)
|
|
43
|
+
break;
|
|
44
|
+
bytes += part.value.length;
|
|
45
|
+
if (bytes > MAX_SKILL_BYTES * 2)
|
|
46
|
+
throw new SkillInputError({ message: "GitHub skill response exceeds its size limit." });
|
|
47
|
+
chunks.push(part.value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await reader.cancel();
|
|
52
|
+
}
|
|
53
|
+
const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
54
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
55
|
+
throw new SkillInputError({ message: "GitHub returned an invalid skill response." });
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
const commit = (await json(`${base}/commits/${encodeURIComponent(parts[3] ?? "HEAD")}`)).sha;
|
|
59
|
+
if (typeof commit !== "string" || !/^[a-f0-9]{40}$/.test(commit))
|
|
60
|
+
throw new SkillInputError({ message: "GitHub returned an invalid commit." });
|
|
61
|
+
const tree = await json(`${base}/git/trees/${commit}?recursive=1`);
|
|
62
|
+
if (tree.truncated || !Array.isArray(tree.tree))
|
|
63
|
+
throw new SkillInputError({ message: "GitHub repository tree is incomplete. Choose a smaller skill repository." });
|
|
64
|
+
const prefix = parts.length > 4 ? parts.slice(4).join("/") + "/" : "";
|
|
65
|
+
const files = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
let actualBytes = 0;
|
|
68
|
+
for (const row of tree.tree) {
|
|
69
|
+
if (typeof row.path !== "string" || !row.path.startsWith(prefix))
|
|
70
|
+
continue;
|
|
71
|
+
const relative = row.path.slice(prefix.length);
|
|
72
|
+
if (!/^(SKILL\.md|README\.md|LICENSE|(?:references|scripts|assets)\/.*)$/.test(relative) || row.type === "tree")
|
|
73
|
+
continue;
|
|
74
|
+
if (row.type !== "blob" || !["100644", "100755"].includes(String(row.mode)))
|
|
75
|
+
throw new SkillInputError({ message: "GitHub skills may contain only regular files." });
|
|
76
|
+
if (typeof row.size !== "number" || row.size < 0 || row.size > MAX_SKILL_FILE_BYTES || typeof row.sha !== "string" || !/^[a-f0-9]{40}$/.test(row.sha))
|
|
77
|
+
throw new SkillInputError({ message: "GitHub skill file exceeds its size limit or has invalid metadata." });
|
|
78
|
+
total += row.size;
|
|
79
|
+
if (total > MAX_SKILL_BYTES || files.length >= MAX_SKILL_FILES)
|
|
80
|
+
throw new SkillInputError({ message: "GitHub skill exceeds 8 MiB or 256 files." });
|
|
81
|
+
const blob = await json(`${base}/git/blobs/${row.sha}`);
|
|
82
|
+
if (blob.encoding !== "base64" || typeof blob.content !== "string")
|
|
83
|
+
throw new SkillInputError({ message: "GitHub returned an invalid skill file." });
|
|
84
|
+
const contentBase64 = blob.content.replace(/\s/g, "");
|
|
85
|
+
actualBytes += decodeSkillBase64({ value: contentBase64 }, { maxBytes: MAX_SKILL_FILE_BYTES }).length;
|
|
86
|
+
if (actualBytes > MAX_SKILL_BYTES)
|
|
87
|
+
throw new SkillInputError({ message: "GitHub skill exceeds 8 MiB or 256 files." });
|
|
88
|
+
files.push({ path: relative, contentBase64 });
|
|
89
|
+
}
|
|
90
|
+
return { files, source: { githubUrl, commit } };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=github.js.map
|