@napplet/conformance-cli 0.3.4 → 0.4.1
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/README.md +10 -0
- package/dist/cli.js +46 -9
- package/dist/cli.js.map +1 -1
- package/dist/host-bundle.global.js +395 -34
- package/dist/ui/assets/index-CXfC5ZeJ.js +5640 -0
- package/dist/ui/index.html +1 -1
- package/package.json +7 -5
- package/dist/ui/assets/index-B0s98LMQ.js +0 -171
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var NappletConformanceHostBundle = (() => {
|
|
3
3
|
// ../core/dist/index.js
|
|
4
|
-
var NAP_DOMAINS = ["shell", "relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent", "ble", "webrtc", "link", "count", "lists", "serial", "fs", "common", "dm", "gamepad"];
|
|
4
|
+
var NAP_DOMAINS = ["shell", "relay", "identity", "storage", "inc", "theme", "keys", "media", "notify", "config", "resource", "cvm", "outbox", "upload", "intent", "ble", "webrtc", "link", "count", "lists", "serial", "fs", "common", "dm", "gamepad", "llm"];
|
|
5
5
|
function createDispatch() {
|
|
6
6
|
const handlers = /* @__PURE__ */ new Map();
|
|
7
7
|
function registerNap2(domain, handler) {
|
|
@@ -10,13 +10,13 @@ var NappletConformanceHostBundle = (() => {
|
|
|
10
10
|
}
|
|
11
11
|
handlers.set(domain, handler);
|
|
12
12
|
}
|
|
13
|
-
function dispatch2(
|
|
14
|
-
const dotIndex =
|
|
13
|
+
function dispatch2(message2) {
|
|
14
|
+
const dotIndex = message2.type.indexOf(".");
|
|
15
15
|
if (dotIndex <= 0) return false;
|
|
16
|
-
const domain =
|
|
16
|
+
const domain = message2.type.slice(0, dotIndex);
|
|
17
17
|
const handler = handlers.get(domain);
|
|
18
18
|
if (!handler) return false;
|
|
19
|
-
handler(
|
|
19
|
+
handler(message2);
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
22
|
function getRegisteredDomains2() {
|
|
@@ -32,6 +32,17 @@ var NappletConformanceHostBundle = (() => {
|
|
|
32
32
|
// ../conformance/dist/index.js
|
|
33
33
|
var ID = { id: "string" };
|
|
34
34
|
var ENVELOPE_SPECS = {
|
|
35
|
+
// NAP-LLM Wire Protocol: four request/result pairs plus the unsolicited delta.
|
|
36
|
+
// https://github.com/napplet/naps/pull/134
|
|
37
|
+
"llm.select": { dir: "out", fields: { ...ID } },
|
|
38
|
+
"llm.select.result": { dir: "in", fields: { ...ID } },
|
|
39
|
+
"llm.describe": { dir: "out", fields: { ...ID, handle: "string" } },
|
|
40
|
+
"llm.describe.result": { dir: "in", fields: { ...ID } },
|
|
41
|
+
"llm.generate": { dir: "out", fields: { ...ID, request: "object" } },
|
|
42
|
+
"llm.generate.result": { dir: "in", fields: { ...ID } },
|
|
43
|
+
"llm.delta": { dir: "in", fields: { requestId: "string", sequence: "number", text: "string" } },
|
|
44
|
+
"llm.cancel": { dir: "out", fields: { ...ID, requestId: "string" } },
|
|
45
|
+
"llm.cancel.result": { dir: "in", fields: { ...ID } },
|
|
35
46
|
// NAP-GAMEPAD Wire Protocol: all three messages are fire-and-forget, without ids.
|
|
36
47
|
// https://github.com/napplet/naps/pull/108
|
|
37
48
|
"gamepad.subscribe": { dir: "out", forbiddenFields: ["id"], forbiddenReason: "NAP-GAMEPAD fire-and-forget envelopes do not carry it" },
|
|
@@ -305,6 +316,59 @@ var NappletConformanceHostBundle = (() => {
|
|
|
305
316
|
"dm.unsubscribe.result": { dir: "in" },
|
|
306
317
|
"dm.message": { dir: "in" }
|
|
307
318
|
};
|
|
319
|
+
var text = (value) => typeof value === "string";
|
|
320
|
+
var nonemptyText = (value) => typeof value === "string" && value.length > 0;
|
|
321
|
+
var boolean = (value) => typeof value === "boolean";
|
|
322
|
+
var oneOf = (...allowed) => (value) => typeof value === "string" && allowed.includes(value);
|
|
323
|
+
var base64 = (value) => typeof value === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value) && value.length >= 4;
|
|
324
|
+
function exactRecord(required, optional = {}) {
|
|
325
|
+
return (value) => {
|
|
326
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
327
|
+
const data = value;
|
|
328
|
+
if (Object.keys(data).some((key) => !(key in required) && !(key in optional))) return false;
|
|
329
|
+
return Object.entries(required).every(([key, check]) => check(data[key])) && Object.entries(optional).every(([key, check]) => data[key] === void 0 || check(data[key]));
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
var nonemptyList = (item) => (value) => Array.isArray(value) && value.length > 0 && value.every(item);
|
|
333
|
+
var textPart = exactRecord({ type: oneOf("text"), text });
|
|
334
|
+
var imagePart = exactRecord({ type: oneOf("image"), mimeType: oneOf("image/png", "image/jpeg", "image/webp"), data: base64 });
|
|
335
|
+
var part = (value) => textPart(value) || imagePart(value);
|
|
336
|
+
var message = exactRecord({ role: oneOf("system", "user", "assistant"), content: nonemptyList(part) });
|
|
337
|
+
var requirements = exactRecord({}, { model: nonemptyText, images: boolean, json: boolean });
|
|
338
|
+
var generationRequest = exactRecord({ handle: nonemptyText, messages: nonemptyList(message) }, { format: oneOf("text", "json") });
|
|
339
|
+
var PAYLOADS = {
|
|
340
|
+
"llm.select": { optional: { requirements } },
|
|
341
|
+
"llm.describe": { required: { handle: nonemptyText } },
|
|
342
|
+
"llm.generate": { required: { request: generationRequest } },
|
|
343
|
+
"llm.cancel": { required: { requestId: nonemptyText } }
|
|
344
|
+
};
|
|
345
|
+
function validateLlmRequest(type, envelope, errors) {
|
|
346
|
+
const schema = PAYLOADS[type];
|
|
347
|
+
if (!schema) return;
|
|
348
|
+
const required = schema.required ?? {};
|
|
349
|
+
const optional = schema.optional ?? {};
|
|
350
|
+
if (envelope.id !== void 0 && !nonemptyText(envelope.id)) {
|
|
351
|
+
errors.push({ code: "wrong-type", field: "id", message: "NAP-LLM request ids are non-empty text" });
|
|
352
|
+
}
|
|
353
|
+
for (const field of Object.keys(envelope)) {
|
|
354
|
+
if (field === "type" || field === "id" || field in required || field in optional) continue;
|
|
355
|
+
errors.push({
|
|
356
|
+
code: "forbidden-field",
|
|
357
|
+
field,
|
|
358
|
+
message: `Field "${field}" is not a parameter of ${type}; NAP-LLM requests carry exactly their API row parameters`
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
for (const [field, check] of Object.entries({ ...required, ...optional })) {
|
|
362
|
+
const value = envelope[field];
|
|
363
|
+
if (value === void 0) {
|
|
364
|
+
if (field in required) errors.push({ code: "missing-field", field, message: `Field "${field}" is required by ${type}` });
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!check(value)) {
|
|
368
|
+
errors.push({ code: "wrong-type", field, message: `Field "${field}" must match the NAP-LLM ${type} schema` });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
308
372
|
function kindOf(value) {
|
|
309
373
|
if (value === void 0) return "undefined";
|
|
310
374
|
if (value === null) return "null";
|
|
@@ -415,12 +479,12 @@ var NappletConformanceHostBundle = (() => {
|
|
|
415
479
|
if (entry.servers !== void 0) validateServerHints(entry.servers, `${field}.servers`, errors);
|
|
416
480
|
}
|
|
417
481
|
}
|
|
418
|
-
function validateEnvelope(
|
|
482
|
+
function validateEnvelope(message2) {
|
|
419
483
|
const errors = [];
|
|
420
|
-
if (typeof
|
|
484
|
+
if (typeof message2 !== "object" || message2 === null || Array.isArray(message2)) {
|
|
421
485
|
return { ok: false, errors: [{ code: "not-an-object", message: "Envelope must be a non-null object" }] };
|
|
422
486
|
}
|
|
423
|
-
const record =
|
|
487
|
+
const record = message2;
|
|
424
488
|
const type = record["type"];
|
|
425
489
|
if (typeof type !== "string") {
|
|
426
490
|
return { ok: false, errors: [{ code: "missing-type", message: "Envelope is missing a string `type` field" }] };
|
|
@@ -472,6 +536,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
472
536
|
});
|
|
473
537
|
}
|
|
474
538
|
}
|
|
539
|
+
if (domain === "llm") validateLlmRequest(type, record, errors);
|
|
475
540
|
if (type === "intent.invoke") {
|
|
476
541
|
validateIntentInvokeRequest(record.request, errors);
|
|
477
542
|
}
|
|
@@ -491,6 +556,8 @@ var NappletConformanceHostBundle = (() => {
|
|
|
491
556
|
NAPPLET_KIND_ROOT,
|
|
492
557
|
NAPPLET_KIND_NAMED
|
|
493
558
|
];
|
|
559
|
+
var SCREENSHOT_FORMATS = ["image/png", "image/jpeg", "image/webp"];
|
|
560
|
+
var MIME_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
494
561
|
function firstTag(event, name) {
|
|
495
562
|
return event.tags.find((tag) => tag[0] === name);
|
|
496
563
|
}
|
|
@@ -507,6 +574,71 @@ var NappletConformanceHostBundle = (() => {
|
|
|
507
574
|
function manifestRequires(event) {
|
|
508
575
|
return event.tags.filter((tag) => tag[0] === "R").map((tag) => tag[1]?.trim() ?? "").filter(Boolean);
|
|
509
576
|
}
|
|
577
|
+
function parsePresentationMedia(event) {
|
|
578
|
+
const warnings = [];
|
|
579
|
+
const screenshots = [];
|
|
580
|
+
for (const tag of event.tags.filter((tag2) => tag2[0] === "screenshot")) {
|
|
581
|
+
const media = parseMediaTag(tag, warnings);
|
|
582
|
+
if (media) screenshots.push(media);
|
|
583
|
+
}
|
|
584
|
+
const previews = event.tags.filter((tag) => tag[0] === "preview");
|
|
585
|
+
let preview;
|
|
586
|
+
if (previews.length > 1) {
|
|
587
|
+
warnings.push({ code: "invalid-presentation-media", message: "Multiple preview tags; clients ignore the preview (NIP-5D \xA7Presentation media)" });
|
|
588
|
+
} else if (previews.length === 1) {
|
|
589
|
+
preview = parseMediaTag(previews[0], warnings);
|
|
590
|
+
}
|
|
591
|
+
return { screenshots, preview, warnings };
|
|
592
|
+
}
|
|
593
|
+
function parseMediaTag(tag, warnings) {
|
|
594
|
+
const name = tag[0];
|
|
595
|
+
const ignore = (reason) => {
|
|
596
|
+
warnings.push({ code: "invalid-presentation-media", message: `Ignore ${name} tag: ${reason} (NIP-5D \xA7Presentation media)` });
|
|
597
|
+
return void 0;
|
|
598
|
+
};
|
|
599
|
+
const fields = /* @__PURE__ */ new Map();
|
|
600
|
+
for (const field of tag.slice(1)) {
|
|
601
|
+
const space = typeof field === "string" ? field.indexOf(" ") : -1;
|
|
602
|
+
const key = space < 0 ? field : field.slice(0, space);
|
|
603
|
+
const value = space < 0 ? void 0 : field.slice(space + 1);
|
|
604
|
+
const values = fields.get(key) ?? [];
|
|
605
|
+
if (value !== void 0) values.push(value);
|
|
606
|
+
else values.push("");
|
|
607
|
+
fields.set(key, values);
|
|
608
|
+
}
|
|
609
|
+
const hashes = fields.get("x");
|
|
610
|
+
if (!hashes || hashes.length !== 1 || !isSha256Hex(hashes[0])) return ignore("exactly one x field with a lowercase sha256 is required");
|
|
611
|
+
const media = { sha256: hashes[0] };
|
|
612
|
+
const optional = (key, accept) => {
|
|
613
|
+
const values = fields.get(key);
|
|
614
|
+
if (!values) return void 0;
|
|
615
|
+
if (values.length !== 1 || !accept(values[0])) {
|
|
616
|
+
warnings.push({ code: "invalid-presentation-media", message: `Ignore ${key} field on ${name} ${media.sha256}: malformed or repeated (NIP-5D \xA7Presentation media)` });
|
|
617
|
+
return void 0;
|
|
618
|
+
}
|
|
619
|
+
return values[0];
|
|
620
|
+
};
|
|
621
|
+
const mimeType = optional("m", (value) => MIME_TYPE.test(value));
|
|
622
|
+
if (mimeType !== void 0) {
|
|
623
|
+
const decodable = name === "preview" ? mimeType.toLowerCase().startsWith("video/") : SCREENSHOT_FORMATS.includes(mimeType.toLowerCase());
|
|
624
|
+
if (!decodable) return ignore(`${mimeType} cannot match a decodable ${name === "preview" ? "video" : "PNG, JPEG or WebP"} format`);
|
|
625
|
+
media.mimeType = mimeType;
|
|
626
|
+
}
|
|
627
|
+
const dim = optional("dim", (value) => /^[1-9][0-9]*x[1-9][0-9]*$/.test(value));
|
|
628
|
+
if (dim !== void 0) {
|
|
629
|
+
const [width, height] = dim.split("x").map(Number);
|
|
630
|
+
media.dim = { width, height };
|
|
631
|
+
}
|
|
632
|
+
const size = optional("size", (value) => /^(?:0|[1-9][0-9]*)$/.test(value));
|
|
633
|
+
if (size !== void 0) media.size = Number(size);
|
|
634
|
+
if (name === "preview") {
|
|
635
|
+
const duration = optional("duration", (value) => /^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(value));
|
|
636
|
+
if (duration !== void 0) media.duration = Number(duration);
|
|
637
|
+
}
|
|
638
|
+
const alt = optional("alt", (value) => value.trim().length > 0);
|
|
639
|
+
if (alt !== void 0) media.alt = alt;
|
|
640
|
+
return media;
|
|
641
|
+
}
|
|
510
642
|
function validateManifestEvent(event) {
|
|
511
643
|
const errors = [];
|
|
512
644
|
const warnings = [];
|
|
@@ -569,15 +701,55 @@ var NappletConformanceHostBundle = (() => {
|
|
|
569
701
|
errors.push({ code: "invalid-metadata", message: `Invalid ${name} tag cardinality for kind ${event.kind}` });
|
|
570
702
|
}
|
|
571
703
|
}
|
|
704
|
+
const sources = event.tags.filter((tag) => tag[0] === "source");
|
|
705
|
+
for (const tag of sources) {
|
|
706
|
+
if (tag.length !== 2 || !isSourceLocation(tag[1])) {
|
|
707
|
+
errors.push({ code: "invalid-metadata", message: "source must contain one HTTP(S), SSH, Git, SCP, or nostr repository location" });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
validateSourcePins(event, sources.length === 1 && sources[0].length === 2 && isSourceLocation(sources[0][1]), errors, warnings);
|
|
572
711
|
for (const tag of event.tags.filter((tag2) => tag2[0] === "i")) {
|
|
573
712
|
if (!tag[1]?.trim() || tag[1].includes("?")) {
|
|
574
713
|
errors.push({ code: "invalid-metadata", message: "Intent advertisements must be non-empty queryless identities" });
|
|
575
714
|
}
|
|
576
715
|
}
|
|
577
716
|
const icons = event.tags.filter((tag) => tag[0] === "icon");
|
|
578
|
-
if (icons.length > 1 || icons.some((tag) => tag.length !== 3 || !isSha256Hex(tag[1]) || !
|
|
717
|
+
if (icons.length > 1 || icons.some((tag) => tag.length !== 3 || !isSha256Hex(tag[1]) || !SCREENSHOT_FORMATS.includes(tag[2]))) {
|
|
579
718
|
warnings.push({ code: "invalid-icon", message: "Ignore invalid icon metadata and use generic artwork (NIP-5D \xA7Icon)" });
|
|
580
719
|
}
|
|
720
|
+
warnings.push(...parsePresentationMedia(event).warnings);
|
|
721
|
+
}
|
|
722
|
+
function validateSourcePins(event, hasRepositorySource, errors, warnings) {
|
|
723
|
+
const commits = event.tags.filter((tag) => tag[0] === "source-commit");
|
|
724
|
+
if (commits.length > 1 || commits.some((tag) => tag.length !== 2 || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(tag[1]))) {
|
|
725
|
+
errors.push({ code: "invalid-metadata", message: "source-commit must appear once with a full lowercase-hex Git commit ID (40 or 64 characters)" });
|
|
726
|
+
} else if (commits.length === 1 && !hasRepositorySource) {
|
|
727
|
+
warnings.push({ code: "unused-source-commit", message: "source-commit without a Git repository source is ignored (NIP-5D \xA7Source)" });
|
|
728
|
+
}
|
|
729
|
+
const archives = event.tags.filter((tag) => tag[0] === "source-archive");
|
|
730
|
+
if (archives.length > 1 || archives.some((tag) => tag.length !== 4 || !isArchiveUrl(tag[1]) || !isSha256Hex(tag[2]) || !MIME_TYPE.test(tag[3] ?? ""))) {
|
|
731
|
+
errors.push({ code: "invalid-metadata", message: "source-archive must appear once with an HTTPS URL without credentials or fragment, a lowercase sha256, and a MIME type" });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function isArchiveUrl(value) {
|
|
735
|
+
if (!value || value.includes("#")) return false;
|
|
736
|
+
try {
|
|
737
|
+
const url = new URL(value);
|
|
738
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
739
|
+
} catch {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function isSourceLocation(value) {
|
|
744
|
+
if (!value || /^[a-z]:[\\/]/i.test(value)) return false;
|
|
745
|
+
try {
|
|
746
|
+
const { protocol } = new URL(value);
|
|
747
|
+
if (["http:", "https:", "ssh:", "git:"].includes(protocol)) return true;
|
|
748
|
+
if (protocol === "nostr:") return value.length > "nostr://".length && value.startsWith("nostr://");
|
|
749
|
+
if (protocol === "file:") return false;
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
return !value.includes("://") && /^(?:[^@\s/:]+@)?[^:\s/]+:[^\s]+$/.test(value);
|
|
581
753
|
}
|
|
582
754
|
var REFERENCE_PUBKEY = "f".repeat(64);
|
|
583
755
|
var REFERENCE_ENDPOINT = { id: "reference-source-endpoint", catalogId: "reference-source" };
|
|
@@ -600,10 +772,10 @@ var NappletConformanceHostBundle = (() => {
|
|
|
600
772
|
const meta = url.slice("data:".length, comma);
|
|
601
773
|
const body = url.slice(comma + 1);
|
|
602
774
|
const parts = meta.split(";").filter(Boolean);
|
|
603
|
-
const
|
|
604
|
-
const mime = parts.find((
|
|
775
|
+
const base642 = parts.includes("base64");
|
|
776
|
+
const mime = parts.find((part2) => part2.includes("/")) ?? "text/plain";
|
|
605
777
|
try {
|
|
606
|
-
if (
|
|
778
|
+
if (base642) {
|
|
607
779
|
const binary = globalThis.atob(body);
|
|
608
780
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
609
781
|
return { blob: new Blob([bytes], { type: mime }), mime };
|
|
@@ -835,9 +1007,111 @@ var NappletConformanceHostBundle = (() => {
|
|
|
835
1007
|
"fs.watch": (e) => ok({ type: "fs.watch.result", id: e.id, watchId: `watch-${String(e.id)}` }),
|
|
836
1008
|
"fs.unwatch": (e) => ok({ type: "fs.unwatch.result", id: e.id })
|
|
837
1009
|
};
|
|
1010
|
+
var REFERENCE_LLM_CONNECTION = {
|
|
1011
|
+
name: "Reference connection",
|
|
1012
|
+
model: "reference-model",
|
|
1013
|
+
route: "Reference shell \u2192 reference provider (no network)",
|
|
1014
|
+
images: true,
|
|
1015
|
+
json: true
|
|
1016
|
+
};
|
|
1017
|
+
var fail = (code, message2) => ({ error: { code, message: message2 } });
|
|
1018
|
+
var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1019
|
+
function sessionFor(sessions, endpoint) {
|
|
1020
|
+
let session = sessions.get(endpoint.id);
|
|
1021
|
+
if (!session) {
|
|
1022
|
+
session = { handles: /* @__PURE__ */ new Set(), active: /* @__PURE__ */ new Map(), sequence: 0 };
|
|
1023
|
+
sessions.set(endpoint.id, session);
|
|
1024
|
+
}
|
|
1025
|
+
return session;
|
|
1026
|
+
}
|
|
1027
|
+
function modelFor(handle2) {
|
|
1028
|
+
return { handle: handle2, ...REFERENCE_LLM_CONNECTION };
|
|
1029
|
+
}
|
|
1030
|
+
function select(session, envelope) {
|
|
1031
|
+
const requirements2 = object(envelope.requirements);
|
|
1032
|
+
if (requirements2.model !== void 0 && requirements2.model !== REFERENCE_LLM_CONNECTION.model) {
|
|
1033
|
+
return fail("unsupported", "No connection offers the requested model");
|
|
1034
|
+
}
|
|
1035
|
+
session.sequence += 1;
|
|
1036
|
+
const handle2 = `reference-llm-h${session.sequence}`;
|
|
1037
|
+
session.handles.add(handle2);
|
|
1038
|
+
return { model: modelFor(handle2) };
|
|
1039
|
+
}
|
|
1040
|
+
function describe(session, envelope) {
|
|
1041
|
+
const handle2 = String(envelope.handle);
|
|
1042
|
+
if (!session.handles.has(handle2)) return fail("selection-expired", "Select a model again");
|
|
1043
|
+
return { model: modelFor(handle2) };
|
|
1044
|
+
}
|
|
1045
|
+
function lastUserText(request) {
|
|
1046
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
1047
|
+
let text2 = "";
|
|
1048
|
+
for (const message2 of messages) {
|
|
1049
|
+
const record = object(message2);
|
|
1050
|
+
if (record.role !== "user" || !Array.isArray(record.content)) continue;
|
|
1051
|
+
for (const part2 of record.content) {
|
|
1052
|
+
const item = object(part2);
|
|
1053
|
+
if (item.type === "text" && typeof item.text === "string") text2 = item.text;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return text2;
|
|
1057
|
+
}
|
|
1058
|
+
function generate(session, id, envelope) {
|
|
1059
|
+
const reply = (outcome) => [{ type: "llm.generate.result", id, ...outcome }];
|
|
1060
|
+
if (session.active.has(id)) return reply(fail("invalid-request", "Request id is still active"));
|
|
1061
|
+
const request = object(envelope.request);
|
|
1062
|
+
if (!session.handles.has(String(request.handle))) return reply(fail("selection-expired", "Select a model again"));
|
|
1063
|
+
const prompt = lastUserText(request);
|
|
1064
|
+
if (prompt.includes("[[decline]]")) return reply(fail("user-cancelled", "The user declined disclosure"));
|
|
1065
|
+
if (prompt.includes("[[stall]]")) {
|
|
1066
|
+
session.active.set(id, envelope);
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
if (prompt.includes("[[refuse]]")) return reply({ result: { text: "", finish: "refusal" } });
|
|
1070
|
+
const output = request.format === "json" ? JSON.stringify({ echo: prompt }) : `Echo: ${prompt}`;
|
|
1071
|
+
const usage = { inputTokens: JSON.stringify(request.messages).length, outputTokens: output.length };
|
|
1072
|
+
return [
|
|
1073
|
+
{ type: "llm.delta", requestId: id, sequence: 0, text: output.slice(0, Math.ceil(output.length / 2)) },
|
|
1074
|
+
...reply({ result: { text: output, finish: "stop", usage } })
|
|
1075
|
+
];
|
|
1076
|
+
}
|
|
1077
|
+
function cancel(session, id, envelope) {
|
|
1078
|
+
const requestId = String(envelope.requestId);
|
|
1079
|
+
if (!session.active.delete(requestId)) return [{ type: "llm.cancel.result", id, cancelled: false }];
|
|
1080
|
+
return [
|
|
1081
|
+
{ type: "llm.generate.result", id: requestId, error: { code: "cancelled", message: "Generation cancelled; provider charges may still apply." } },
|
|
1082
|
+
{ type: "llm.cancel.result", id, cancelled: true }
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
function handle(sessions, endpoint, envelope) {
|
|
1086
|
+
const type = String(envelope.type);
|
|
1087
|
+
const id = String(envelope.id);
|
|
1088
|
+
const session = sessionFor(sessions, endpoint);
|
|
1089
|
+
switch (type) {
|
|
1090
|
+
case "llm.select":
|
|
1091
|
+
return [{ type: "llm.select.result", id, ...select(session, envelope) }];
|
|
1092
|
+
case "llm.describe":
|
|
1093
|
+
return [{ type: "llm.describe.result", id, ...describe(session, envelope) }];
|
|
1094
|
+
case "llm.generate":
|
|
1095
|
+
return structuredClone(generate(session, id, envelope));
|
|
1096
|
+
case "llm.cancel":
|
|
1097
|
+
return cancel(session, id, envelope);
|
|
1098
|
+
default:
|
|
1099
|
+
return [{ type: `${type}.result`, id, error: { code: "invalid-request", message: `Unknown operation ${type}` } }];
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function createLlmHandlers() {
|
|
1103
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1104
|
+
return {
|
|
1105
|
+
handle: (endpoint, envelope) => handle(sessions, endpoint, envelope),
|
|
1106
|
+
reset() {
|
|
1107
|
+
sessions.clear();
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
838
1111
|
function createReferenceHandler(options) {
|
|
839
1112
|
const initialized = /* @__PURE__ */ new Set();
|
|
840
|
-
|
|
1113
|
+
const llm = createLlmHandlers();
|
|
1114
|
+
const handle2 = (endpoint, envelope) => {
|
|
841
1115
|
const type = getEnvelopeType(envelope);
|
|
842
1116
|
const verdict = validateEnvelope(envelope);
|
|
843
1117
|
options.records.push({ envelope, verdict, timestamp: options.now() });
|
|
@@ -850,6 +1124,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
850
1124
|
return ok({ type: "shell.init", capabilities: { domains: [...domains] }, services: [] });
|
|
851
1125
|
}
|
|
852
1126
|
const env = envelope;
|
|
1127
|
+
if (type.startsWith("llm.")) return llm.handle(endpoint, env);
|
|
853
1128
|
if (type === "intent.invoke") return options.intents.handleInvoke(endpoint, env);
|
|
854
1129
|
if (type === "intent.available") {
|
|
855
1130
|
return ok({ type: "intent.available.result", id: env.id, availability: options.intents.availability(env.archetype) });
|
|
@@ -861,6 +1136,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
861
1136
|
const responder = RESPONDERS[type];
|
|
862
1137
|
return responder ? responder(env) : [];
|
|
863
1138
|
};
|
|
1139
|
+
return Object.assign(handle2, { reset() {
|
|
1140
|
+
llm.reset();
|
|
1141
|
+
} });
|
|
864
1142
|
}
|
|
865
1143
|
function getEnvelopeType(envelope) {
|
|
866
1144
|
if (!envelope || typeof envelope !== "object") return void 0;
|
|
@@ -956,6 +1234,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
956
1234
|
takeDeliveries: deliveries.take,
|
|
957
1235
|
reset() {
|
|
958
1236
|
records.length = 0;
|
|
1237
|
+
handleFrom.reset();
|
|
959
1238
|
deliveries.clear();
|
|
960
1239
|
}
|
|
961
1240
|
};
|
|
@@ -978,7 +1257,58 @@ var NappletConformanceHostBundle = (() => {
|
|
|
978
1257
|
}
|
|
979
1258
|
};
|
|
980
1259
|
}
|
|
1260
|
+
var LLM_PRELUDE_SOURCE = `
|
|
1261
|
+
if (domains.includes('llm')) {
|
|
1262
|
+
const llmFields = { 'llm.select.result': 'model', 'llm.describe.result': 'model', 'llm.generate.result': 'result', 'llm.cancel.result': 'cancelled' };
|
|
1263
|
+
const llmDeltas = new Map();
|
|
1264
|
+
window.addEventListener('message', (event) => {
|
|
1265
|
+
if (event.source !== window.parent) return;
|
|
1266
|
+
const msg = event.data;
|
|
1267
|
+
if (!msg || typeof msg !== 'object' || msg.type !== 'llm.delta' || 'id' in msg || typeof msg.requestId !== 'string') return;
|
|
1268
|
+
const entry = llmDeltas.get(msg.requestId);
|
|
1269
|
+
if (!entry || typeof msg.sequence !== 'number' || typeof msg.text !== 'string') return;
|
|
1270
|
+
if (msg.sequence !== entry.next) { entry.next = Infinity; return; }
|
|
1271
|
+
entry.next += 1;
|
|
1272
|
+
if (entry.onDelta) entry.onDelta({ requestId: msg.requestId, sequence: msg.sequence, text: msg.text });
|
|
1273
|
+
});
|
|
1274
|
+
const llmRequest = (payload, id) => {
|
|
1275
|
+
const type = payload.type;
|
|
1276
|
+
return request(id === undefined ? payload : { ...payload, id }, type + '.result').then((message) => {
|
|
1277
|
+
const field = llmFields[message.type];
|
|
1278
|
+
const hasValue = message[field] !== undefined && message[field] !== null;
|
|
1279
|
+
const hasError = message.error !== undefined && message.error !== null;
|
|
1280
|
+
if (hasValue === hasError) throw new Error(type + '.result must carry exactly one of ' + field + ' or error');
|
|
1281
|
+
if (hasError) {
|
|
1282
|
+
const failure = new Error(String(message.error.message));
|
|
1283
|
+
failure.name = 'LlmError';
|
|
1284
|
+
failure.code = message.error.code;
|
|
1285
|
+
throw failure;
|
|
1286
|
+
}
|
|
1287
|
+
return message[field];
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
napplet.llm = {
|
|
1291
|
+
select: (requirements) => llmRequest(requirements === undefined ? { type: 'llm.select' } : { type: 'llm.select', requirements }),
|
|
1292
|
+
describe: (handle) => llmRequest({ type: 'llm.describe', handle }),
|
|
1293
|
+
generate: (generation, options) => {
|
|
1294
|
+
const id = nextId();
|
|
1295
|
+
const entry = { next: 0, onDelta: options && options.onDelta };
|
|
1296
|
+
llmDeltas.set(id, entry);
|
|
1297
|
+
const signal = options && options.signal;
|
|
1298
|
+
const abort = () => { napplet.llm.cancel(id).catch(() => undefined); };
|
|
1299
|
+
if (signal) { if (signal.aborted) abort(); else signal.addEventListener('abort', abort, { once: true }); }
|
|
1300
|
+
const pendingGeneration = llmRequest({ type: 'llm.generate', request: generation }, id).finally(() => {
|
|
1301
|
+
llmDeltas.delete(id);
|
|
1302
|
+
if (signal) signal.removeEventListener('abort', abort);
|
|
1303
|
+
});
|
|
1304
|
+
return Object.assign(pendingGeneration, { requestId: id });
|
|
1305
|
+
},
|
|
1306
|
+
cancel: (requestId) => llmRequest({ type: 'llm.cancel', requestId }),
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
`;
|
|
981
1310
|
var INTERNAL_BOOT_ERROR = "__nappletConformance.error";
|
|
1311
|
+
var INTERNAL_INSTALLED = "__nappletConformance.installed";
|
|
982
1312
|
function sleep(ms) {
|
|
983
1313
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
984
1314
|
}
|
|
@@ -1013,13 +1343,26 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1013
1343
|
};
|
|
1014
1344
|
return { isGamepadState, copyGamepadState };
|
|
1015
1345
|
})()`;
|
|
1016
|
-
function
|
|
1017
|
-
return `
|
|
1346
|
+
function errorObserver() {
|
|
1347
|
+
return `(() => {
|
|
1348
|
+
const report = (reason) => {
|
|
1349
|
+
try { window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*'); }
|
|
1350
|
+
catch {}
|
|
1351
|
+
};
|
|
1352
|
+
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1353
|
+
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1354
|
+
})();`;
|
|
1355
|
+
}
|
|
1356
|
+
var FALLBACK_RUNTIME_OPEN = `
|
|
1018
1357
|
(() => {
|
|
1019
|
-
const domains =
|
|
1358
|
+
const domains = `;
|
|
1359
|
+
var FALLBACK_RUNTIME_BEFORE_GAMEPAD_TOOLS = `;
|
|
1020
1360
|
const napplet = {};
|
|
1021
|
-
for (const domain of domains)
|
|
1022
|
-
|
|
1361
|
+
for (const domain of domains) {
|
|
1362
|
+
if (domain === 'resource' || domain === 'gamepad') napplet[domain] = {};
|
|
1363
|
+
}
|
|
1364
|
+
const gamepadTools = `;
|
|
1365
|
+
var FALLBACK_RUNTIME_AFTER_GAMEPAD_TOOLS = `;
|
|
1023
1366
|
let gamepadState = { available: true, focused: false, pads: [] };
|
|
1024
1367
|
const gamepadHandlers = new Set();
|
|
1025
1368
|
// Subscribe on first use, like the nap binding, so napplets that never touch
|
|
@@ -1118,20 +1461,25 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1118
1461
|
return handle;
|
|
1119
1462
|
};
|
|
1120
1463
|
}
|
|
1464
|
+
${LLM_PRELUDE_SOURCE}
|
|
1121
1465
|
window.napplet = napplet;
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
window.parent.postMessage({ type: '${INTERNAL_BOOT_ERROR}', reason: String(reason && reason.message || reason) }, '*');
|
|
1125
|
-
} catch {}
|
|
1126
|
-
};
|
|
1127
|
-
window.addEventListener('error', (event) => report(event.error || event.message));
|
|
1128
|
-
window.addEventListener('unhandledrejection', (event) => report(event.reason));
|
|
1466
|
+
`;
|
|
1467
|
+
var FALLBACK_RUNTIME_CLOSE = `
|
|
1129
1468
|
})();
|
|
1130
1469
|
`;
|
|
1470
|
+
function runtimePrelude(domains, runtimePreludeSource) {
|
|
1471
|
+
const installed = `window.parent.postMessage({ type: '${INTERNAL_INSTALLED}', installed: !!window.napplet }, '*');`;
|
|
1472
|
+
if (runtimePreludeSource !== void 0) {
|
|
1473
|
+
return `${runtimePreludeSource}
|
|
1474
|
+
;globalThis.NappletShimPrelude.install(${JSON.stringify({ domains })});
|
|
1475
|
+
${installed}`;
|
|
1476
|
+
}
|
|
1477
|
+
return FALLBACK_RUNTIME_OPEN + JSON.stringify(domains) + FALLBACK_RUNTIME_BEFORE_GAMEPAD_TOOLS + GAMEPAD_STATE_TOOLS_SOURCE + FALLBACK_RUNTIME_AFTER_GAMEPAD_TOOLS + installed + FALLBACK_RUNTIME_CLOSE;
|
|
1131
1478
|
}
|
|
1132
|
-
function injectRuntime(html, url, domains) {
|
|
1479
|
+
function injectRuntime(html, url, domains, runtimePreludeSource) {
|
|
1133
1480
|
const base = `<base href="${escapeHtmlAttr(url)}">`;
|
|
1134
|
-
const
|
|
1481
|
+
const source = runtimePrelude(domains, runtimePreludeSource).replace(/<\/script/gi, "<\\/script");
|
|
1482
|
+
const script = `<script>${errorObserver()}<\/script><script>${source}<\/script>`;
|
|
1135
1483
|
const headOpen = /<head(?:\s[^>]*)?>/i.exec(html);
|
|
1136
1484
|
if (headOpen) {
|
|
1137
1485
|
const at = headOpen.index + headOpen[0].length;
|
|
@@ -1167,9 +1515,14 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1167
1515
|
iframe.style.height = "1px";
|
|
1168
1516
|
iframe.style.left = "-9999px";
|
|
1169
1517
|
let bootError = null;
|
|
1518
|
+
let installedGlobal = false;
|
|
1170
1519
|
const listener = (event) => {
|
|
1171
1520
|
if (event.source !== iframe.contentWindow) return;
|
|
1172
1521
|
const data = event.data;
|
|
1522
|
+
if (data && typeof data === "object" && data.type === INTERNAL_INSTALLED) {
|
|
1523
|
+
installedGlobal = data.installed === true;
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1173
1526
|
if (data && typeof data === "object" && data.type === INTERNAL_BOOT_ERROR) {
|
|
1174
1527
|
bootError = `napplet boot error: ${String(data.reason ?? "unknown error")}`;
|
|
1175
1528
|
return;
|
|
@@ -1186,10 +1539,10 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1186
1539
|
}, { once: true });
|
|
1187
1540
|
});
|
|
1188
1541
|
win.addEventListener("message", listener);
|
|
1189
|
-
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1190
1542
|
try {
|
|
1191
|
-
const html = await loadHtml(opts.url);
|
|
1192
|
-
iframe.srcdoc = injectRuntime(html, opts.url, domains);
|
|
1543
|
+
const html = opts.html ?? await loadHtml(opts.url);
|
|
1544
|
+
iframe.srcdoc = injectRuntime(html, opts.url, domains, opts.runtimePreludeSource);
|
|
1545
|
+
(doc.body ?? doc.documentElement).appendChild(iframe);
|
|
1193
1546
|
} catch (error) {
|
|
1194
1547
|
bootError = error instanceof Error ? error.message : String(error);
|
|
1195
1548
|
}
|
|
@@ -1207,16 +1560,18 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1207
1560
|
iframe.remove();
|
|
1208
1561
|
} catch {
|
|
1209
1562
|
}
|
|
1210
|
-
return { installedGlobal
|
|
1563
|
+
return { installedGlobal, bootError, emitted: [...shell.records] };
|
|
1211
1564
|
}
|
|
1212
1565
|
async function bootAndCollect(options) {
|
|
1213
1566
|
const doc = options.document ?? document;
|
|
1214
1567
|
const win = options.window ?? window;
|
|
1215
1568
|
const resolved = {
|
|
1216
1569
|
url: options.url,
|
|
1570
|
+
html: options.html,
|
|
1217
1571
|
readyTimeoutMs: options.readyTimeoutMs ?? 5e3,
|
|
1218
1572
|
settleMs: options.settleMs ?? 600,
|
|
1219
|
-
allowSameOrigin: options.allowSameOrigin ?? false
|
|
1573
|
+
allowSameOrigin: options.allowSameOrigin ?? false,
|
|
1574
|
+
runtimePreludeSource: options.runtimePreludeSource
|
|
1220
1575
|
};
|
|
1221
1576
|
const primary = await bootOnce(NAP_DOMAINS, resolved, doc, win);
|
|
1222
1577
|
let degraded = null;
|
|
@@ -1232,9 +1587,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1232
1587
|
};
|
|
1233
1588
|
}
|
|
1234
1589
|
var pass = (detail) => ({ status: "pass", detail });
|
|
1235
|
-
var
|
|
1590
|
+
var fail2 = (detail, diagnostics) => ({ status: "fail", detail, diagnostics });
|
|
1236
1591
|
var skip = (detail) => ({ status: "skip", detail });
|
|
1237
|
-
var result = { pass, fail, skip };
|
|
1592
|
+
var result = { pass, fail: fail2, skip };
|
|
1238
1593
|
function manifestErrors(ctx, codes) {
|
|
1239
1594
|
const verdict = validateManifestEvent(ctx.manifestEvent);
|
|
1240
1595
|
return [...verdict.errors, ...verdict.warnings].filter((e) => codes.includes(e.code));
|
|
@@ -1272,6 +1627,12 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1272
1627
|
manifestCheck("manifest/requires-known", "R/O domains are in this toolchain registry (advisory)", [
|
|
1273
1628
|
"unknown-required-nap"
|
|
1274
1629
|
], "warning"),
|
|
1630
|
+
// NIP-5D §Icon, §Presentation media and §Source: clients ignore these, never fail.
|
|
1631
|
+
manifestCheck("manifest/media-usable", "Icon, screenshots, preview and source pins are usable (advisory)", [
|
|
1632
|
+
"invalid-icon",
|
|
1633
|
+
"invalid-presentation-media",
|
|
1634
|
+
"unused-source-commit"
|
|
1635
|
+
], "warning"),
|
|
1275
1636
|
// ── boot ─────────────────────────────────────────────────────────────────────
|
|
1276
1637
|
{
|
|
1277
1638
|
id: "boot/sandbox-allow-scripts",
|