@napplet/conformance-cli 0.4.0 → 0.4.2
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/cli.js.map +1 -1
- package/dist/host-bundle.global.js +288 -158
- package/dist/ui/assets/{index-CQX23owf.js → index-Dt7CfKEg.js} +478 -474
- package/dist/ui/index.html +1 -1
- package/package.json +6 -6
|
@@ -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,26 +701,51 @@ var NappletConformanceHostBundle = (() => {
|
|
|
569
701
|
errors.push({ code: "invalid-metadata", message: `Invalid ${name} tag cardinality for kind ${event.kind}` });
|
|
570
702
|
}
|
|
571
703
|
}
|
|
572
|
-
|
|
704
|
+
const sources = event.tags.filter((tag) => tag[0] === "source");
|
|
705
|
+
for (const tag of sources) {
|
|
573
706
|
if (tag.length !== 2 || !isSourceLocation(tag[1])) {
|
|
574
|
-
errors.push({ code: "invalid-metadata", message: "source must contain one HTTP(S), SSH, Git, or
|
|
707
|
+
errors.push({ code: "invalid-metadata", message: "source must contain one HTTP(S), SSH, Git, SCP, or nostr repository location" });
|
|
575
708
|
}
|
|
576
709
|
}
|
|
710
|
+
validateSourcePins(event, sources.length === 1 && sources[0].length === 2 && isSourceLocation(sources[0][1]), errors, warnings);
|
|
577
711
|
for (const tag of event.tags.filter((tag2) => tag2[0] === "i")) {
|
|
578
712
|
if (!tag[1]?.trim() || tag[1].includes("?")) {
|
|
579
713
|
errors.push({ code: "invalid-metadata", message: "Intent advertisements must be non-empty queryless identities" });
|
|
580
714
|
}
|
|
581
715
|
}
|
|
582
716
|
const icons = event.tags.filter((tag) => tag[0] === "icon");
|
|
583
|
-
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]))) {
|
|
584
718
|
warnings.push({ code: "invalid-icon", message: "Ignore invalid icon metadata and use generic artwork (NIP-5D \xA7Icon)" });
|
|
585
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
|
+
}
|
|
586
742
|
}
|
|
587
743
|
function isSourceLocation(value) {
|
|
588
744
|
if (!value || /^[a-z]:[\\/]/i.test(value)) return false;
|
|
589
745
|
try {
|
|
590
746
|
const { protocol } = new URL(value);
|
|
591
747
|
if (["http:", "https:", "ssh:", "git:"].includes(protocol)) return true;
|
|
748
|
+
if (protocol === "nostr:") return value.length > "nostr://".length && value.startsWith("nostr://");
|
|
592
749
|
if (protocol === "file:") return false;
|
|
593
750
|
} catch {
|
|
594
751
|
}
|
|
@@ -615,10 +772,10 @@ var NappletConformanceHostBundle = (() => {
|
|
|
615
772
|
const meta = url.slice("data:".length, comma);
|
|
616
773
|
const body = url.slice(comma + 1);
|
|
617
774
|
const parts = meta.split(";").filter(Boolean);
|
|
618
|
-
const
|
|
619
|
-
const mime = parts.find((
|
|
775
|
+
const base642 = parts.includes("base64");
|
|
776
|
+
const mime = parts.find((part2) => part2.includes("/")) ?? "text/plain";
|
|
620
777
|
try {
|
|
621
|
-
if (
|
|
778
|
+
if (base642) {
|
|
622
779
|
const binary = globalThis.atob(body);
|
|
623
780
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
624
781
|
return { blob: new Blob([bytes], { type: mime }), mime };
|
|
@@ -850,9 +1007,111 @@ var NappletConformanceHostBundle = (() => {
|
|
|
850
1007
|
"fs.watch": (e) => ok({ type: "fs.watch.result", id: e.id, watchId: `watch-${String(e.id)}` }),
|
|
851
1008
|
"fs.unwatch": (e) => ok({ type: "fs.unwatch.result", id: e.id })
|
|
852
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
|
+
}
|
|
853
1111
|
function createReferenceHandler(options) {
|
|
854
1112
|
const initialized = /* @__PURE__ */ new Set();
|
|
855
|
-
|
|
1113
|
+
const llm = createLlmHandlers();
|
|
1114
|
+
const handle2 = (endpoint, envelope) => {
|
|
856
1115
|
const type = getEnvelopeType(envelope);
|
|
857
1116
|
const verdict = validateEnvelope(envelope);
|
|
858
1117
|
options.records.push({ envelope, verdict, timestamp: options.now() });
|
|
@@ -865,6 +1124,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
865
1124
|
return ok({ type: "shell.init", capabilities: { domains: [...domains] }, services: [] });
|
|
866
1125
|
}
|
|
867
1126
|
const env = envelope;
|
|
1127
|
+
if (type.startsWith("llm.")) return llm.handle(endpoint, env);
|
|
868
1128
|
if (type === "intent.invoke") return options.intents.handleInvoke(endpoint, env);
|
|
869
1129
|
if (type === "intent.available") {
|
|
870
1130
|
return ok({ type: "intent.available.result", id: env.id, availability: options.intents.availability(env.archetype) });
|
|
@@ -876,6 +1136,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
876
1136
|
const responder = RESPONDERS[type];
|
|
877
1137
|
return responder ? responder(env) : [];
|
|
878
1138
|
};
|
|
1139
|
+
return Object.assign(handle2, { reset() {
|
|
1140
|
+
llm.reset();
|
|
1141
|
+
} });
|
|
879
1142
|
}
|
|
880
1143
|
function getEnvelopeType(envelope) {
|
|
881
1144
|
if (!envelope || typeof envelope !== "object") return void 0;
|
|
@@ -971,6 +1234,7 @@ var NappletConformanceHostBundle = (() => {
|
|
|
971
1234
|
takeDeliveries: deliveries.take,
|
|
972
1235
|
reset() {
|
|
973
1236
|
records.length = 0;
|
|
1237
|
+
handleFrom.reset();
|
|
974
1238
|
deliveries.clear();
|
|
975
1239
|
}
|
|
976
1240
|
};
|
|
@@ -1001,34 +1265,6 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1001
1265
|
function escapeHtmlAttr(value) {
|
|
1002
1266
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1003
1267
|
}
|
|
1004
|
-
var GAMEPAD_STATE_TOOLS_SOURCE = `(() => {
|
|
1005
|
-
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1006
|
-
const numberIn = (value, min, max) => typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
|
|
1007
|
-
const isGamepadState = (value) => {
|
|
1008
|
-
if (!record(value) || typeof value.available !== 'boolean' || typeof value.focused !== 'boolean' || !Array.isArray(value.pads)) return false;
|
|
1009
|
-
if (value.available) {
|
|
1010
|
-
if (value.reason !== undefined) return false;
|
|
1011
|
-
} else if (value.focused || (value.reason !== 'blocked' && value.reason !== 'unavailable')) return false;
|
|
1012
|
-
return Array.from(value.pads).every((pad) => {
|
|
1013
|
-
if (pad === null) return true;
|
|
1014
|
-
if (!record(pad) || !Number.isInteger(pad.index) || pad.index < 0 || typeof pad.id !== 'string' || typeof pad.mapping !== 'string' || pad.connected !== true) return false;
|
|
1015
|
-
if (typeof pad.timestamp !== 'number' || !Number.isFinite(pad.timestamp) || !Array.isArray(pad.axes) || !Array.isArray(pad.buttons)) return false;
|
|
1016
|
-
if (!value.focused && pad.timestamp !== 0) return false;
|
|
1017
|
-
if (!Array.from(pad.axes).every((axis) => numberIn(axis, -1, 1) && (value.focused || axis === 0))) return false;
|
|
1018
|
-
return Array.from(pad.buttons).every((button) => record(button) && numberIn(button.value, 0, 1) && typeof button.pressed === 'boolean' && typeof button.touched === 'boolean' && (value.focused || (button.value === 0 && !button.pressed && !button.touched)));
|
|
1019
|
-
});
|
|
1020
|
-
};
|
|
1021
|
-
const copyGamepadState = (state) => {
|
|
1022
|
-
const copy = { available: state.available, focused: state.focused };
|
|
1023
|
-
if (state.reason !== undefined) copy.reason = state.reason;
|
|
1024
|
-
copy.pads = state.pads.map((pad) => pad === null ? null : {
|
|
1025
|
-
index: pad.index, id: pad.id, mapping: pad.mapping, connected: pad.connected,
|
|
1026
|
-
timestamp: pad.timestamp, axes: pad.axes.slice(), buttons: pad.buttons.map((button) => ({ value: button.value, pressed: button.pressed, touched: button.touched })),
|
|
1027
|
-
});
|
|
1028
|
-
return copy;
|
|
1029
|
-
};
|
|
1030
|
-
return { isGamepadState, copyGamepadState };
|
|
1031
|
-
})()`;
|
|
1032
1268
|
function errorObserver() {
|
|
1033
1269
|
return `(() => {
|
|
1034
1270
|
const report = (reason) => {
|
|
@@ -1041,121 +1277,9 @@ var NappletConformanceHostBundle = (() => {
|
|
|
1041
1277
|
}
|
|
1042
1278
|
function runtimePrelude(domains, runtimePreludeSource) {
|
|
1043
1279
|
const installed = `window.parent.postMessage({ type: '${INTERNAL_INSTALLED}', installed: !!window.napplet }, '*');`;
|
|
1044
|
-
|
|
1045
|
-
return `${runtimePreludeSource}
|
|
1280
|
+
return `${runtimePreludeSource}
|
|
1046
1281
|
;globalThis.NappletShimPrelude.install(${JSON.stringify({ domains })});
|
|
1047
1282
|
${installed}`;
|
|
1048
|
-
}
|
|
1049
|
-
return `
|
|
1050
|
-
(() => {
|
|
1051
|
-
const domains = ${JSON.stringify(domains)};
|
|
1052
|
-
const napplet = {};
|
|
1053
|
-
for (const domain of domains) {
|
|
1054
|
-
if (domain === 'resource' || domain === 'gamepad') napplet[domain] = {};
|
|
1055
|
-
}
|
|
1056
|
-
const gamepadTools = ${GAMEPAD_STATE_TOOLS_SOURCE};
|
|
1057
|
-
let gamepadState = { available: true, focused: false, pads: [] };
|
|
1058
|
-
const gamepadHandlers = new Set();
|
|
1059
|
-
// Subscribe on first use, like the nap binding, so napplets that never touch
|
|
1060
|
-
// gamepad emit no gamepad traffic for checks to attribute to them.
|
|
1061
|
-
let gamepadSubscribed = false;
|
|
1062
|
-
const gamepadSubscribe = () => {
|
|
1063
|
-
if (gamepadSubscribed) return;
|
|
1064
|
-
gamepadSubscribed = true;
|
|
1065
|
-
window.parent.postMessage({ type: 'gamepad.subscribe' }, '*');
|
|
1066
|
-
};
|
|
1067
|
-
const pending = new Map();
|
|
1068
|
-
let seq = 0;
|
|
1069
|
-
const nextId = () => {
|
|
1070
|
-
try {
|
|
1071
|
-
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
|
|
1072
|
-
} catch {}
|
|
1073
|
-
seq += 1;
|
|
1074
|
-
return 'conformance-' + Date.now().toString(36) + '-' + seq.toString(36);
|
|
1075
|
-
};
|
|
1076
|
-
const request = (message, resultType, errorType) => new Promise((resolve, reject) => {
|
|
1077
|
-
const id = message.id || nextId();
|
|
1078
|
-
const timeout = window.setTimeout(() => {
|
|
1079
|
-
pending.delete(id);
|
|
1080
|
-
reject(new Error(message.type + ' timed out'));
|
|
1081
|
-
}, 5000);
|
|
1082
|
-
pending.set(id, { resolve, reject, resultType, errorType, timeout });
|
|
1083
|
-
window.parent.postMessage({ ...message, id }, '*');
|
|
1084
|
-
});
|
|
1085
|
-
window.addEventListener('message', (event) => {
|
|
1086
|
-
if (event.source !== window.parent) return;
|
|
1087
|
-
const msg = event.data;
|
|
1088
|
-
if (gamepadSubscribed && msg && msg.type === 'gamepad.state' && !('id' in msg) && gamepadTools.isGamepadState(msg)) {
|
|
1089
|
-
gamepadState = gamepadTools.copyGamepadState(msg);
|
|
1090
|
-
const recipients = Array.from(gamepadHandlers);
|
|
1091
|
-
for (const entry of recipients) {
|
|
1092
|
-
if (!gamepadHandlers.has(entry)) continue;
|
|
1093
|
-
try { entry.handler(gamepadTools.copyGamepadState(gamepadState)); }
|
|
1094
|
-
catch (error) { queueMicrotask(() => { throw error; }); }
|
|
1095
|
-
}
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
if (!msg || typeof msg !== 'object' || typeof msg.id !== 'string') return;
|
|
1099
|
-
const slot = pending.get(msg.id);
|
|
1100
|
-
if (!slot) return;
|
|
1101
|
-
if (msg.type !== slot.resultType && msg.type !== slot.errorType) return;
|
|
1102
|
-
pending.delete(msg.id);
|
|
1103
|
-
window.clearTimeout(slot.timeout);
|
|
1104
|
-
if (msg.type === slot.errorType) {
|
|
1105
|
-
slot.reject(new Error(msg.message ? msg.error + ': ' + msg.message : String(msg.error || 'request failed')));
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
slot.resolve(msg);
|
|
1109
|
-
});
|
|
1110
|
-
if (napplet.gamepad) {
|
|
1111
|
-
napplet.gamepad = {
|
|
1112
|
-
get available() { gamepadSubscribe(); return gamepadState.available; },
|
|
1113
|
-
get focused() { gamepadSubscribe(); return gamepadState.focused; },
|
|
1114
|
-
getGamepads() { gamepadSubscribe(); return gamepadTools.copyGamepadState(gamepadState).pads; },
|
|
1115
|
-
onChange(handler) {
|
|
1116
|
-
const entry = { handler };
|
|
1117
|
-
gamepadHandlers.add(entry);
|
|
1118
|
-
gamepadSubscribe();
|
|
1119
|
-
return { close() { gamepadHandlers.delete(entry); } };
|
|
1120
|
-
},
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
if (napplet.resource) {
|
|
1124
|
-
napplet.resource.info = () =>
|
|
1125
|
-
request({ type: 'resource.info' }, 'resource.info.result', 'resource.info.error')
|
|
1126
|
-
.then((msg) => msg.info);
|
|
1127
|
-
napplet.resource.bytes = (url, options) => {
|
|
1128
|
-
const message = { type: 'resource.bytes', url: String(url) };
|
|
1129
|
-
if (options && options.servers !== undefined) message.servers = options.servers;
|
|
1130
|
-
return request(message, 'resource.bytes.result', 'resource.bytes.error')
|
|
1131
|
-
.then((msg) => msg.blob);
|
|
1132
|
-
};
|
|
1133
|
-
napplet.resource.bytesMany = (requests) =>
|
|
1134
|
-
request({ type: 'resource.bytesMany', requests: Array.from(requests || []) }, 'resource.bytesMany.result', 'resource.bytesMany.error')
|
|
1135
|
-
.then((msg) => msg.items);
|
|
1136
|
-
napplet.resource.bytesAsObjectURL = (url) => {
|
|
1137
|
-
const handle = { url: '', revoke: () => {} };
|
|
1138
|
-
let objectUrl = '';
|
|
1139
|
-
let revoked = false;
|
|
1140
|
-
const ready = napplet.resource.bytes(url).then((blob) => {
|
|
1141
|
-
if (revoked) return;
|
|
1142
|
-
objectUrl = URL.createObjectURL(blob);
|
|
1143
|
-
handle.url = objectUrl;
|
|
1144
|
-
return objectUrl;
|
|
1145
|
-
});
|
|
1146
|
-
handle.revoke = () => {
|
|
1147
|
-
if (revoked) return;
|
|
1148
|
-
revoked = true;
|
|
1149
|
-
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1150
|
-
};
|
|
1151
|
-
Object.defineProperty(handle, 'ready', { value: ready, enumerable: false });
|
|
1152
|
-
return handle;
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
window.napplet = napplet;
|
|
1156
|
-
${installed}
|
|
1157
|
-
})();
|
|
1158
|
-
`;
|
|
1159
1283
|
}
|
|
1160
1284
|
function injectRuntime(html, url, domains, runtimePreludeSource) {
|
|
1161
1285
|
const base = `<base href="${escapeHtmlAttr(url)}">`;
|
|
@@ -1268,9 +1392,9 @@ ${installed}`;
|
|
|
1268
1392
|
};
|
|
1269
1393
|
}
|
|
1270
1394
|
var pass = (detail) => ({ status: "pass", detail });
|
|
1271
|
-
var
|
|
1395
|
+
var fail2 = (detail, diagnostics) => ({ status: "fail", detail, diagnostics });
|
|
1272
1396
|
var skip = (detail) => ({ status: "skip", detail });
|
|
1273
|
-
var result = { pass, fail, skip };
|
|
1397
|
+
var result = { pass, fail: fail2, skip };
|
|
1274
1398
|
function manifestErrors(ctx, codes) {
|
|
1275
1399
|
const verdict = validateManifestEvent(ctx.manifestEvent);
|
|
1276
1400
|
return [...verdict.errors, ...verdict.warnings].filter((e) => codes.includes(e.code));
|
|
@@ -1308,6 +1432,12 @@ ${installed}`;
|
|
|
1308
1432
|
manifestCheck("manifest/requires-known", "R/O domains are in this toolchain registry (advisory)", [
|
|
1309
1433
|
"unknown-required-nap"
|
|
1310
1434
|
], "warning"),
|
|
1435
|
+
// NIP-5D §Icon, §Presentation media and §Source: clients ignore these, never fail.
|
|
1436
|
+
manifestCheck("manifest/media-usable", "Icon, screenshots, preview and source pins are usable (advisory)", [
|
|
1437
|
+
"invalid-icon",
|
|
1438
|
+
"invalid-presentation-media",
|
|
1439
|
+
"unused-source-commit"
|
|
1440
|
+
], "warning"),
|
|
1311
1441
|
// ── boot ─────────────────────────────────────────────────────────────────────
|
|
1312
1442
|
{
|
|
1313
1443
|
id: "boot/sandbox-allow-scripts",
|