@kehto/shell 0.24.5 → 0.26.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/README.md CHANGED
@@ -305,6 +305,10 @@ for outer URL encoding, review, routing, and verified launcher behavior.
305
305
 
306
306
  This non-normative guidance defers to the living [NAP-GAMEPAD](https://github.com/napplet/naps/pull/108). Services export `createGamepadService` and `browserGamepadOptions`; shell exports `denyNappletControllerAccess`. Paja and the playground register one host reader and deny native gamepad/HID/USB paths before frame execution. Source-bound subscriptions receive immediate full snapshots; only the visible host's input owner gets live controls. Focus loss supplies neutral controls and timestamp zero; unsubscribe, window destruction, and service removal release polling. Non-browser or denied native readers report the proposal's unavailable reasons. See [Controller input](https://github.com/kehto/web/blob/main/docs/gamepad.md).
307
307
 
308
+ ## LLM integration
309
+
310
+ This non-normative guidance defers to the living [NAP-LLM](https://github.com/napplet/naps/pull/134) (checked at `3e10939`). Services export `createLlmService`, `LlmServiceError`, the provider mappers and `LLM_PROVIDER_PRESETS`; shell exports `getShellDomainState` and the `LlmHooks` adapter field; acl exports `llm:generate`. The runtime routes `llm.*` to one registered service and answers every sanctioned request with exactly one `llm.<op>.result`, shaping missing-service and ACL failures as `{ code: 'unsupported' }`. Hosts implement `LlmConnectionHost`; the service enforces schema, handle ownership, consent before submission, duplicate ids, cancellation and deadline races, ordered deltas and safe errors. Advertisement stays binary on the wire while hosts report a third `unconfigured` state until the user adds a connection. See [Language models](https://github.com/kehto/web/blob/main/docs/llm.md).
311
+
308
312
  ## UUID support on HTTP origins
309
313
 
310
314
  Shell event injection, cache queries, and source-session registration use native
package/dist/index.d.ts CHANGED
@@ -243,6 +243,30 @@ interface WebrtcHooks {
243
243
  /** True when the host has shell-mediated WebRTC sessions wired. */
244
244
  isAvailable(): boolean;
245
245
  }
246
+ /**
247
+ * Host wiring behind the NAP-LLM `llm` domain.
248
+ *
249
+ * NAP-LLM support is not binary: a host may have the runtime and service wired
250
+ * while the user has not yet configured any model connection. `isAvailable()`
251
+ * decides whether the domain is advertised at all; `isConfigured()` reports
252
+ * whether a connection exists so host UIs can show an "unconfigured" state and
253
+ * prompt for setup when a napplet first calls `llm.select`.
254
+ */
255
+ interface LlmHooks {
256
+ /** True when the host can route `llm.*` to a registered service. */
257
+ isAvailable(): boolean;
258
+ /** True when at least one model connection is configured. Omitted means configured. */
259
+ isConfigured?(): boolean;
260
+ }
261
+ /**
262
+ * Host-facing advertisement state of one NAP domain.
263
+ *
264
+ * - `supported`: advertised to napplets and ready to serve requests.
265
+ * - `unconfigured`: advertised, but the host still needs user configuration
266
+ * (for example an LLM connection); the first request prompts for setup.
267
+ * - `disabled`: not advertised; the host disabled it or its backend is not live.
268
+ */
269
+ type ShellDomainState = 'supported' | 'unconfigured' | 'disabled';
246
270
  /**
247
271
  * Optional host override for the static shell.init capability handshake.
248
272
  *
@@ -417,6 +441,12 @@ interface ShellAdapter {
417
441
  * registers the concrete service via `registerService('webrtc')`.
418
442
  */
419
443
  webrtc?: WebrtcHooks;
444
+ /**
445
+ * Optional NAP-LLM wiring state. When omitted, a registered `llm` service is
446
+ * advertised as supported; when present, `isAvailable()` gates advertisement
447
+ * and `isConfigured()` feeds the host's unconfigured indicator.
448
+ */
449
+ llm?: LlmHooks;
420
450
  /**
421
451
  * Optional capability advertisement override. Omitted by production hosts.
422
452
  */
@@ -982,6 +1012,24 @@ declare function buildShellCapabilities(hooks: ShellAdapter): ShellCapabilities;
982
1012
  * @returns A fresh immutable environment whose entries are exact live subsets.
983
1013
  */
984
1014
  declare function resolveShellEnvironment(hooks: ShellAdapter, identity: OriginIdentity): ShellEnvironment;
1015
+ /**
1016
+ * Report the host-facing advertisement state of one bare NAP domain.
1017
+ *
1018
+ * Advertisement itself stays binary on the wire (NIP-5D domain presence), but a
1019
+ * host UI needs the third state: the domain is advertised while the user has not
1020
+ * configured its backend yet. Only hooks that expose `isConfigured()` (today
1021
+ * `llm`) can report `unconfigured`; every other advertised domain is
1022
+ * `supported`, and anything not advertised is `disabled`.
1023
+ *
1024
+ * @param hooks - The ShellAdapter provided by the host app.
1025
+ * @param domain - Bare NAP domain name such as `llm`.
1026
+ * @returns `supported`, `unconfigured` or `disabled`.
1027
+ * @example
1028
+ * ```ts
1029
+ * getShellDomainState(hooks, 'llm'); // 'unconfigured' until a connection is saved
1030
+ * ```
1031
+ */
1032
+ declare function getShellDomainState(hooks: ShellAdapter, domain: string): ShellDomainState;
985
1033
 
986
1034
  /**
987
1035
  * identity-proxy.ts — Shell-side per-domain proxy for identity.* envelopes.
@@ -1568,4 +1616,4 @@ type ResourceInbound = ResourceInfoRequest | ResourceBytesRequest | ResourceByte
1568
1616
  */
1569
1617
  type ResourceOutbound = ResourceInfoResult | ResourceInfoError | ResourceBytesResult | ResourceBytesError | ResourceBytesManyResult | ResourceBytesManyError;
1570
1618
 
1571
- export { type AclCheckEvent, type AclEntry, type AclStore, type AudioManager, type AudioSource, type AuthHooks, type BleHooks, type BrowserDeps, type CapabilityHooks, type CommonHooks, type ConfigHooks, type CryptoHooks, type DmHooks, type HotkeyHooks, type IdentityProxy, type IdentityProxyDeps, type IntentHooks, type KeysProxy, type KeysProxyDeps, type LinkHooks, type ListsHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletNamespacePreludeOptions, type NappletSrcdocOptions, type NormalizedIntentUri, type NotifyProxy, type NotifyProxyDeps, type OriginIdentity, type OriginRegistry, type ProxyOriginRegistry, type RelayConfigHooks, type RelayPoolHooks, type RelayPoolLike, type ResourceBytesError, type ResourceBytesManyError, type ResourceBytesManyItem, type ResourceBytesManyRequest, type ResourceBytesManyResult, type ResourceBytesRequest, type ResourceBytesResult, type ResourceCancelRequest, type ResourceErrorCode, type ResourceInbound, type ResourceInfoError, type ResourceInfoRequest, type ResourceInfoResult, type ResourceOutbound, type ResourceRequestId, type SerialHooks, type SessionRegistry, type ShellAdapter, type ShellBridge, type ShellCapabilities, type ShellEnvironment, type ThemeProxy, type ThemeProxyDeps, type UnroutedMessageInfo, type UploadBackendLike, type UploadHooks, type WebrtcHooks, type WindowManagerHooks, type WorkerRelayHooks, type WorkerRelayLike, adaptHooks, audioManager, buildNappletCsp, buildShellCapabilities, createIdentityProxy, createKeysProxy, createMediaProxy, createNotifyProxy, createShellBridge, createThemeProxy, denyNappletControllerAccess, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, normalizeIntentUri, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
1619
+ export { type AclCheckEvent, type AclEntry, type AclStore, type AudioManager, type AudioSource, type AuthHooks, type BleHooks, type BrowserDeps, type CapabilityHooks, type CommonHooks, type ConfigHooks, type CryptoHooks, type DmHooks, type HotkeyHooks, type IdentityProxy, type IdentityProxyDeps, type IntentHooks, type KeysProxy, type KeysProxyDeps, type LinkHooks, type ListsHooks, type LlmHooks, type ManifestCache, type ManifestCacheEntry, type MediaProxy, type MediaProxyDeps, type NappletCspDirective, type NappletCspDirectives, type NappletCspOptions, type NappletNamespacePreludeOptions, type NappletSrcdocOptions, type NormalizedIntentUri, type NotifyProxy, type NotifyProxyDeps, type OriginIdentity, type OriginRegistry, type ProxyOriginRegistry, type RelayConfigHooks, type RelayPoolHooks, type RelayPoolLike, type ResourceBytesError, type ResourceBytesManyError, type ResourceBytesManyItem, type ResourceBytesManyRequest, type ResourceBytesManyResult, type ResourceBytesRequest, type ResourceBytesResult, type ResourceCancelRequest, type ResourceErrorCode, type ResourceInbound, type ResourceInfoError, type ResourceInfoRequest, type ResourceInfoResult, type ResourceOutbound, type ResourceRequestId, type SerialHooks, type SessionRegistry, type ShellAdapter, type ShellBridge, type ShellCapabilities, type ShellDomainState, type ShellEnvironment, type ThemeProxy, type ThemeProxyDeps, type UnroutedMessageInfo, type UploadBackendLike, type UploadHooks, type WebrtcHooks, type WindowManagerHooks, type WorkerRelayHooks, type WorkerRelayLike, adaptHooks, audioManager, buildNappletCsp, buildShellCapabilities, createIdentityProxy, createKeysProxy, createMediaProxy, createNotifyProxy, createShellBridge, createThemeProxy, denyNappletControllerAccess, getShellDomainState, injectNappletCsp, injectNappletNamespacePrelude, manifestCache, nappKeyRegistry, normalizeIntentUri, originRegistry, prepareNappletSrcdoc, renderNappletCspMeta, renderNappletNamespacePrelude, resolveShellEnvironment, sessionRegistry };
package/dist/index.js CHANGED
@@ -1013,7 +1013,8 @@ var SERVICE_ONLY_DOMAINS = [
1013
1013
  "dm",
1014
1014
  "count",
1015
1015
  "fs",
1016
- "gamepad"
1016
+ "gamepad",
1017
+ "llm"
1017
1018
  ];
1018
1019
  function buildShellCapabilities(hooks) {
1019
1020
  const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
@@ -1060,10 +1061,18 @@ function isServiceDomainLive(hooks, domain) {
1060
1061
  return hooks.ble?.isAvailable() === true;
1061
1062
  case "webrtc":
1062
1063
  return hooks.webrtc?.isAvailable() === true;
1064
+ case "llm":
1065
+ return hooks.llm ? hooks.llm.isAvailable() : true;
1063
1066
  default:
1064
1067
  return true;
1065
1068
  }
1066
1069
  }
1070
+ function getShellDomainState(hooks, domain) {
1071
+ if (!buildShellCapabilities(hooks).domains.includes(domain)) return "disabled";
1072
+ const configurable = Reflect.get(hooks, domain);
1073
+ const isConfigured = configurable && typeof configurable === "object" ? configurable.isConfigured : void 0;
1074
+ return typeof isConfigured === "function" && !isConfigured.call(configurable) ? "unconfigured" : "supported";
1075
+ }
1067
1076
  function getLiveServices(hooks, domains, disabled) {
1068
1077
  const liveDomains = new Set(domains);
1069
1078
  return Object.keys(hooks.services ?? {}).filter((name) => {
@@ -2210,36 +2219,90 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2210
2219
  };
2211
2220
  }
2212
2221
  function makeMedia() {
2222
+ const sessions = /* @__PURE__ */ new Set();
2223
+ const ended = /* @__PURE__ */ new Map();
2224
+ const retained = /* @__PURE__ */ new Map();
2225
+ const callbacks = /* @__PURE__ */ new Map();
2226
+ const warnings = /* @__PURE__ */ new Set();
2227
+ const warn = (name, replacement) => {
2228
+ if (warnings.has(name)) return;
2229
+ warnings.add(name);
2230
+ console.warn(`[KEHTO-MEDIA-${name}] Deprecated media API; use ${replacement}. Warned once per endpoint.`);
2231
+ };
2232
+ listen((event) => {
2233
+ if (!isParentMessage(event)) return;
2234
+ const msg = event.data;
2235
+ if (!msg || typeof msg !== "object" || typeof msg.sessionId !== "string") return;
2236
+ const sessionId = msg.sessionId;
2237
+ if (msg.type === "media.session.create.result" && !msg.error) {
2238
+ if (!ended.has(sessionId)) sessions.add(sessionId);
2239
+ return;
2240
+ }
2241
+ if (!sessions.has(sessionId) || ended.has(sessionId)) return;
2242
+ if (msg.type === "media.session.ended") {
2243
+ ended.set(sessionId, msg);
2244
+ retained.delete(sessionId);
2245
+ sessions.delete(sessionId);
2246
+ } else if (["media.state", "media.capabilities", "media.controls"].includes(msg.type)) {
2247
+ const cache2 = retained.get(sessionId) ?? /* @__PURE__ */ new Map();
2248
+ cache2.set(msg.type, msg);
2249
+ retained.set(sessionId, cache2);
2250
+ } else if (msg.type !== "media.command") return;
2251
+ const delivery = Array.from(callbacks.get(sessionId)?.get(msg.type) ?? []);
2252
+ for (const callback of delivery) callback(msg);
2253
+ if (ended.has(sessionId)) callbacks.delete(sessionId);
2254
+ });
2213
2255
  const on = (type, sessionId, callback) => {
2214
- const off = listen((event) => {
2215
- if (!isParentMessage(event)) return;
2216
- const msg = event.data;
2217
- if (typeof msg === "object" && msg !== null && msg.type === type && msg.sessionId === sessionId) callback(msg);
2256
+ const terminal = ended.get(sessionId);
2257
+ if (terminal) {
2258
+ if (type === "media.session.ended") callback(terminal);
2259
+ return subscriptionHandle(() => {
2260
+ });
2261
+ }
2262
+ const byType = callbacks.get(sessionId) ?? /* @__PURE__ */ new Map();
2263
+ const handlers = byType.get(type) ?? /* @__PURE__ */ new Set();
2264
+ handlers.add(callback);
2265
+ byType.set(type, handlers);
2266
+ callbacks.set(sessionId, byType);
2267
+ const cached = retained.get(sessionId)?.get(type);
2268
+ if (cached) callback(cached);
2269
+ return subscriptionHandle(() => {
2270
+ handlers.delete(callback);
2218
2271
  });
2219
- return subscriptionHandle(off);
2220
2272
  };
2221
- return {
2273
+ const command = (sessionId, value) => {
2274
+ if (ended.has(sessionId)) return;
2275
+ fire({ ...value && typeof value === "object" ? value : {}, type: "media.command", sessionId });
2276
+ };
2277
+ return Object.freeze({
2222
2278
  createSession: (options) => request(
2223
- { type: "media.session.create", ...options && typeof options === "object" ? options : {} },
2279
+ { ...options && typeof options === "object" ? options : {}, type: "media.session.create" },
2224
2280
  "media.session.create.result",
2225
2281
  withoutEnvelope,
2226
2282
  { rejectOnError: false }
2227
2283
  ),
2228
2284
  updateSession: (sessionId, metadata) => fire({ type: "media.session.update", sessionId, metadata }),
2229
2285
  destroySession: (sessionId) => fire({ type: "media.session.destroy", sessionId }),
2230
- reportState: (sessionId, state) => fire({ type: "media.state", sessionId, ...state && typeof state === "object" ? state : {} }),
2286
+ reportState: (sessionId, state) => fire({ ...state && typeof state === "object" ? state : {}, type: "media.state", sessionId }),
2231
2287
  reportCapabilities: (sessionId, actions) => fire({ type: "media.capabilities", sessionId, actions }),
2232
- sendCommand: (sessionId, action, value) => fire({ type: "media.command", sessionId, action, ...value === void 0 ? {} : { value } }),
2233
- onCommand: (sessionId, callback) => on("media.command", sessionId, (msg) => callback(msg.action, msg.value)),
2234
- onState: (sessionId, callback) => on("media.state", sessionId, (msg) => callback({
2235
- status: msg.status,
2236
- position: msg.position,
2237
- duration: msg.duration,
2238
- volume: msg.volume
2239
- })),
2288
+ command,
2289
+ sendCommand: (sessionId, action, value) => {
2290
+ warn("SEND-COMMAND", "media.command(sessionId, { action, value })");
2291
+ command(sessionId, { action, ...value === void 0 ? {} : { value } });
2292
+ },
2293
+ onCommand: (sessionId, callback) => {
2294
+ const legacy = callback.length >= 2;
2295
+ if (legacy) warn("COMMAND-CALLBACK", "media.onCommand(sessionId, command => ...)");
2296
+ return on("media.command", sessionId, (msg) => legacy ? callback(msg.action, msg.value) : callback({ action: msg.action, ...msg.value === void 0 ? {} : { value: msg.value }, ...msg.reason === void 0 ? {} : { reason: msg.reason } }));
2297
+ },
2298
+ onState: (sessionId, callback) => on("media.state", sessionId, (msg) => {
2299
+ const { type: _type, sessionId: _sessionId, ...state } = msg;
2300
+ callback(state);
2301
+ }),
2240
2302
  onCapabilities: (sessionId, callback) => on("media.capabilities", sessionId, (msg) => callback(msg.actions)),
2241
- onControls: (sessionId, callback) => on("media.controls", sessionId, (msg) => callback(msg.controls))
2242
- };
2303
+ onControls: (sessionId, callback) => on("media.controls", sessionId, (msg) => callback(msg.controls)),
2304
+ onEnded: (sessionId, callback) => on("media.session.ended", sessionId, (msg) => callback({ sessionId: msg.sessionId, reason: msg.reason }))
2305
+ });
2243
2306
  }
2244
2307
  function makeNotify() {
2245
2308
  const on = (type, callback) => {
@@ -2853,6 +2916,57 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2853
2916
  onChanged: (callback) => domainEventHandle("fs.changed", callback, (msg) => msg.change)
2854
2917
  };
2855
2918
  }
2919
+ function makeLlm() {
2920
+ const asFailure = (value) => {
2921
+ if (!value || typeof value !== "object") return null;
2922
+ const { code, message } = value;
2923
+ return typeof code === "string" && typeof message === "string" ? { code, message } : null;
2924
+ };
2925
+ const failure = (entry) => Object.assign(new Error(entry.message), { code: entry.code });
2926
+ function call(type, payload, field, options = {}) {
2927
+ const requestId = id();
2928
+ const promise = new Promise((resolve, reject) => {
2929
+ let nextSequence = 0;
2930
+ const onAbort = () => {
2931
+ post({ type: "llm.cancel", id: id(), requestId });
2932
+ };
2933
+ const off = listen((event) => {
2934
+ if (!isParentMessage(event)) return;
2935
+ const msg = event.data;
2936
+ if (typeof msg !== "object" || msg === null) return;
2937
+ if (type === "llm.generate" && msg.type === "llm.delta" && msg.requestId === requestId) {
2938
+ if (msg.sequence !== nextSequence || typeof msg.text !== "string") return;
2939
+ nextSequence += 1;
2940
+ options.onDelta?.({ requestId, sequence: msg.sequence, text: msg.text });
2941
+ return;
2942
+ }
2943
+ if (msg.type !== `${type}.result` || msg.id !== requestId) return;
2944
+ off();
2945
+ options.signal?.removeEventListener("abort", onAbort);
2946
+ const error = asFailure(msg.error);
2947
+ if (error) {
2948
+ reject(failure(error));
2949
+ return;
2950
+ }
2951
+ if (!Object.prototype.hasOwnProperty.call(msg, field)) {
2952
+ reject(failure({ code: "provider-error", message: `${type}.result missing ${field}` }));
2953
+ return;
2954
+ }
2955
+ resolve(msg[field]);
2956
+ });
2957
+ post({ type, id: requestId, ...payload });
2958
+ if (options.signal?.aborted) onAbort();
2959
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
2960
+ });
2961
+ return Object.assign(promise, { requestId });
2962
+ }
2963
+ return {
2964
+ select: (requirements) => call("llm.select", requirements === void 0 ? {} : { requirements }, "model"),
2965
+ describe: (handle) => call("llm.describe", { handle }, "model"),
2966
+ generate: (request2, options) => call("llm.generate", { request: request2 }, "result", options ?? {}),
2967
+ cancel: (requestId) => call("llm.cancel", { requestId }, "cancelled")
2968
+ };
2969
+ }
2856
2970
  const intentDelivery = allowed.has("intent") ? makeIntentDelivery() : void 0;
2857
2971
  const shell = allowed.has("shell") ? makeShell() : void 0;
2858
2972
  let gamepad;
@@ -2877,10 +2991,12 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2877
2991
  intent ??= Object.freeze(makeIntent());
2878
2992
  return intent;
2879
2993
  }
2994
+ let media;
2880
2995
  function isProtectedDomain(domain) {
2881
- return domain === "gamepad" || domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
2996
+ return domain === "media" || domain === "gamepad" || domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
2882
2997
  }
2883
2998
  function makeDomain(domain, existing) {
2999
+ if (domain === "media") return media ??= makeMedia();
2884
3000
  if (domain === "gamepad") return gamepad ??= makeGamepad();
2885
3001
  if (domain === "shell") return shell;
2886
3002
  if (domain === "inc") return makeProtectedInc(existing);
@@ -2895,8 +3011,6 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2895
3011
  return makeStorage();
2896
3012
  case "keys":
2897
3013
  return makeKeys();
2898
- case "media":
2899
- return makeMedia();
2900
3014
  case "notify":
2901
3015
  return makeNotify();
2902
3016
  case "config":
@@ -2927,6 +3041,8 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2927
3041
  return makeDm();
2928
3042
  case "fs":
2929
3043
  return makeFs();
3044
+ case "llm":
3045
+ return makeLlm();
2930
3046
  default:
2931
3047
  return {};
2932
3048
  }
@@ -3197,6 +3313,7 @@ export {
3197
3313
  createThemeProxy,
3198
3314
  denyNappletControllerAccess,
3199
3315
  formatDenialReason,
3316
+ getShellDomainState,
3200
3317
  injectNappletCsp,
3201
3318
  injectNappletNamespacePrelude,
3202
3319
  manifestCache,