@leuria/cli 0.1.0 → 0.1.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 CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  parseBody,
28
28
  parseLlmId,
29
29
  parseNeeds,
30
+ pruneAgentVersions,
30
31
  rememberAgentState,
31
32
  removeProvider,
32
33
  resetAgent,
@@ -37,8 +38,9 @@ import {
37
38
  seenAgentModels,
38
39
  sendJson,
39
40
  signIn,
40
- startEngine
41
- } from "./chunk-BXSAYWR3.js";
41
+ startEngine,
42
+ updateAgents
43
+ } from "./chunk-JOCM56MT.js";
42
44
 
43
45
  // src/cli.ts
44
46
  import { spawn } from "child_process";
@@ -119,7 +121,14 @@ function addAi(id) {
119
121
  }
120
122
  function removeAi(id) {
121
123
  const config = loadConfig();
122
- saveConfig({ ...config, ais: (config.ais ?? []).filter((a) => a !== id) });
124
+ const { [id]: _model, ...models } = config.models ?? {};
125
+ const { [id]: _ready, ...ready } = config.ready ?? {};
126
+ saveConfig({ ...config, ais: (config.ais ?? []).filter((a) => a !== id), models, ready });
127
+ }
128
+ function providerInUse(providerId, defaultAgent) {
129
+ const config = loadConfig();
130
+ const uses = (id) => parseLlmId(id)?.providerId === providerId;
131
+ return uses(defaultAgent) || (config.ais ?? []).some(uses) || typeof config.embed === "object" && config.embed.provider === providerId;
123
132
  }
124
133
  function convertServiceAis(grants) {
125
134
  const toService = (id) => {
@@ -352,6 +361,7 @@ function freePort() {
352
361
  // src/admin.ts
353
362
  function createAdminHandler(ctx) {
354
363
  let signingIn = null;
364
+ let signingInAgent = "";
355
365
  return async (req, res, pathname, pairing, sessions) => {
356
366
  const siteAi = (origin) => ctx.grants.get(origin)?.agent ?? ctx.config.agent;
357
367
  const method = req.method ?? "GET";
@@ -506,7 +516,20 @@ function createAdminHandler(ctx) {
506
516
  return true;
507
517
  }
508
518
  removeAi(id);
509
- for (const grant of ctx.grants.list()) if (grant.agent === id) ctx.grants.setAgent(grant.origin, void 0);
519
+ for (const grant of ctx.grants.list()) {
520
+ if (grant.agent === id) ctx.grants.setAgent(grant.origin, void 0);
521
+ if (grant.model?.agent === id) ctx.grants.setModel(grant.origin, void 0);
522
+ }
523
+ if (isLlmId(id)) {
524
+ const provider = parseLlmId(id)?.providerId;
525
+ if (provider && !providerInUse(provider, ctx.config.agent)) removeProvider(provider);
526
+ } else {
527
+ if (signingInAgent === id) {
528
+ signingIn?.abort();
529
+ signingIn = null;
530
+ }
531
+ resetAgent(id);
532
+ }
510
533
  sendJson(res, 200, { removed: id });
511
534
  return true;
512
535
  }
@@ -524,7 +547,8 @@ function createAdminHandler(ctx) {
524
547
  signingIn?.abort();
525
548
  const controller = new AbortController();
526
549
  signingIn = controller;
527
- const result = await signIn(typeof body.agent === "string" && body.agent ? body.agent : ctx.config.agent, {
550
+ signingInAgent = typeof body.agent === "string" && body.agent ? body.agent : ctx.config.agent;
551
+ const result = await signIn(signingInAgent, {
528
552
  signal: controller.signal,
529
553
  methodId: typeof body.methodId === "string" ? body.methodId : void 0,
530
554
  // The app has no terminal: only agent methods (the agent runs its own flow).
@@ -544,8 +568,17 @@ function createAdminHandler(ctx) {
544
568
  return true;
545
569
  }
546
570
  if (route === "GET /models") {
547
- const agent = new URL(req.url ?? "", "http://127.0.0.1").searchParams.get("agent") ?? ctx.config.agent;
548
- sendJson(res, 200, { agent, models: isAvailable(agent) ? await agentModels(agent) : null });
571
+ const params = new URL(req.url ?? "", "http://127.0.0.1").searchParams;
572
+ const agent = params.get("agent") ?? ctx.config.agent;
573
+ if (!isAvailable(agent)) {
574
+ sendJson(res, 200, { agent, models: null });
575
+ return true;
576
+ }
577
+ const refresh = params.get("refresh") === "1";
578
+ const answered = refresh && !isLlmId(agent) ? (await checkSignIn(agent)).ok : true;
579
+ const models = await agentModels(agent);
580
+ const stale = refresh && (!answered || isLlmId(agent) && !models);
581
+ sendJson(res, 200, { agent, models, ...stale ? { stale: true } : {} });
549
582
  return true;
550
583
  }
551
584
  if (route === "POST /agent/model") {
@@ -1053,7 +1086,10 @@ function sites(args) {
1053
1086
  }
1054
1087
  }
1055
1088
  function openInBrowser(url) {
1056
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1089
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : (
1090
+ // Not `cmd /c start`: cmd would read the & between query parameters as a new command.
1091
+ process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]]
1092
+ );
1057
1093
  try {
1058
1094
  const child = spawn(command, args, { stdio: "ignore", detached: true });
1059
1095
  child.on("error", () => void 0);
@@ -1139,6 +1175,8 @@ function stdioMcpSelf() {
1139
1175
  const compiled = !script || script.startsWith("/$bunfs") || /^[A-Z]:[\\/]~BUN/i.test(script);
1140
1176
  return compiled ? { command: process.execPath, args: ["mcp-stdio"] } : { command: process.execPath, args: [...process.execArgv, script, "mcp-stdio"] };
1141
1177
  }
1178
+ var AI_UPDATE_DELAY_MS = 3e4;
1179
+ var AI_UPDATE_EVERY_MS = 24 * 60 * 60 * 1e3;
1142
1180
  async function startForApp(config, verbose, devPairing) {
1143
1181
  const token = process.env.LEURIA_ADMIN_TOKEN;
1144
1182
  if (!token || token.length < 32) throw new Error("LEURIA_ADMIN_TOKEN (32+ characters) is required with --app");
@@ -1148,6 +1186,11 @@ async function startForApp(config, verbose, devPairing) {
1148
1186
  const grants = new GrantStore();
1149
1187
  convertServiceAis(grants);
1150
1188
  config.agent = loadConfig().agent;
1189
+ try {
1190
+ pruneAgentVersions();
1191
+ } catch (err) {
1192
+ logger.warn("could not remove old AI versions", { err: err instanceof Error ? err.message : String(err) });
1193
+ }
1151
1194
  let port = config.port;
1152
1195
  const engine = await startEngine({
1153
1196
  port: config.port,
@@ -1179,13 +1222,26 @@ async function startForApp(config, verbose, devPairing) {
1179
1222
  });
1180
1223
  port = engine.port;
1181
1224
  emit({ event: "ready", port, version: VERSION, agent: config.agent });
1225
+ const updateAis = () => void updateAgents(async (agent, from) => {
1226
+ logger.info("AI updated", { id: agent.id, from, to: agent.version });
1227
+ await checkSignIn(agent.id, void 0, { quiet: true }).catch(() => void 0);
1228
+ emit({ event: "agent_updated", id: agent.id });
1229
+ });
1230
+ setTimeout(updateAis, AI_UPDATE_DELAY_MS).unref();
1231
+ setInterval(updateAis, AI_UPDATE_EVERY_MS).unref();
1232
+ let stopping = false;
1182
1233
  const shutdown = async () => {
1234
+ if (stopping) return;
1235
+ stopping = true;
1183
1236
  await engine.close();
1184
1237
  process.exit(0);
1185
1238
  };
1186
1239
  process.on("SIGINT", shutdown);
1187
1240
  process.on("SIGTERM", shutdown);
1188
1241
  process.stdin.on("end", shutdown);
1242
+ process.stdin.on("data", (chunk) => {
1243
+ if (chunk.toString("utf-8").split(/\r?\n/).includes("quit")) void shutdown();
1244
+ });
1189
1245
  process.stdin.resume();
1190
1246
  }
1191
1247
  function out(text) {
package/dist/index.d.ts CHANGED
@@ -21,6 +21,230 @@ interface SiteNeeds {
21
21
  context?: number;
22
22
  }
23
23
 
24
+ interface Logger {
25
+ info: (msg: string, meta?: Record<string, unknown>) => void;
26
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
27
+ error: (msg: string, meta?: Record<string, unknown>) => void;
28
+ }
29
+ /** Line-per-event logger on stderr, so stdout stays free. */
30
+ declare function createLogger(verbose?: boolean): Logger;
31
+
32
+ /**
33
+ * Where a site's skills come from, and fetching them. A skill is a
34
+ * `SKILL.md` (YAML front matter with `name` and `description`, then
35
+ * instructions) and the text files beside it; see https://agentskills.io.
36
+ *
37
+ * Sources use the `npx skills` syntax, so developers paste the refs they
38
+ * already use:
39
+ *
40
+ * owner/repo every skill in a GitHub repository
41
+ * owner/repo@skill one skill, by name
42
+ * owner/repo/path/to/skills the skills under a folder
43
+ * …#ref at a branch, tag or commit (pin it)
44
+ * https://github.com/o/r/tree/ref/path
45
+ * / or https://site.example/… the site's own: `.well-known/agent-skills`
46
+ * (agentskills.io discovery) or a `SKILL.md` URL
47
+ *
48
+ * Only GitHub and the site's own origin: the engine never fetches an
49
+ * address a site picks elsewhere, so a page can't point it at the visitor's
50
+ * network. Only text is kept; scripts and binaries are dropped (the agent
51
+ * has no shell to run them anyway).
52
+ */
53
+ /** A skill as fetched: its instructions and text files. */
54
+ interface SkillContent {
55
+ name: string;
56
+ description: string;
57
+ /** `SKILL.md` without its front matter. */
58
+ body: string;
59
+ /** Other text files of the skill, by path relative to its folder. */
60
+ files: Record<string, string>;
61
+ }
62
+
63
+ /**
64
+ * In-process WebMCP relay for the bridge daemon.
65
+ *
66
+ * Two surfaces, one server:
67
+ *
68
+ * - **Browser → bridge (WebSocket)**: the browser console connects to
69
+ * `ws://127.0.0.1:<port>/webmcp/register`, exchanges its registration
70
+ * token for a channel WS at `/webmcp/channel/:id?token=…`, then
71
+ * declares the tools / resources / prompts it wants to expose.
72
+ *
73
+ * - **Agent → bridge (HTTP MCP)**: the spawned ACP agent calls JSON-RPC
74
+ * 2.0 against `POST /webmcp/mcp` with `Authorization: Bearer
75
+ * <channelToken>`. `tools/list` and friends read the channel's
76
+ * registry; `tools/call` etc. forward the request to the browser via
77
+ * the channel WS and await the response.
78
+ *
79
+ * Long-blocking calls (`tools/call`, `resources/read`, `prompts/get`)
80
+ * switch to chunked HTTP encoding and emit periodic newline keep-alives
81
+ * so the agent's HTTP client doesn't abort while the user is interacting
82
+ * in the browser.
83
+ *
84
+ * Origin checks happen in `server.ts` before requests reach this class.
85
+ */
86
+
87
+ interface ToolDescriptor {
88
+ name: string;
89
+ description?: string;
90
+ inputSchema: Record<string, unknown>;
91
+ annotations?: Record<string, unknown>;
92
+ }
93
+ interface WebMcpChannelSummary {
94
+ id: string;
95
+ sessionId: string;
96
+ connected: boolean;
97
+ tools: Array<{
98
+ name: string;
99
+ description?: string;
100
+ }>;
101
+ resources: Array<{
102
+ uri: string;
103
+ name: string;
104
+ description?: string;
105
+ }>;
106
+ prompts: Array<{
107
+ name: string;
108
+ description?: string;
109
+ }>;
110
+ }
111
+ interface WebMcpLogger {
112
+ info: (msg: string, meta?: Record<string, unknown>) => void;
113
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
114
+ error: (msg: string, meta?: Record<string, unknown>) => void;
115
+ }
116
+ declare class WebMcpServer {
117
+ private readonly logger;
118
+ private readonly port;
119
+ private readonly channels;
120
+ private readonly registrationTokens;
121
+ private readonly sessionChannels;
122
+ private readonly tokenToChannel;
123
+ private readonly registerWss;
124
+ private readonly channelWss;
125
+ constructor(logger: WebMcpLogger, port: number);
126
+ /**
127
+ * Allocate a channel for an ACP session. Returns:
128
+ * - `registrationToken` — base64-encoded `{ server, token }` blob the
129
+ * browser presents on the `/webmcp/register` WS to claim the channel.
130
+ * - `channelToken` — Bearer credential the agent presents on the
131
+ * `/webmcp/mcp` HTTP endpoint.
132
+ * - `channelId` — server-side handle (used in the channel WS path).
133
+ */
134
+ createChannel(sessionId: string): {
135
+ registrationToken: string;
136
+ channelToken: string;
137
+ channelId: string;
138
+ };
139
+ getChannelSummaries(): WebMcpChannelSummary[];
140
+ /**
141
+ * Tear everything down — close the two WebSocketServers, kill every
142
+ * ping timer, reject every pending request, force-close every browser
143
+ * channel WS. Called from `daemon.ts`'s `shutdown` so the process can
144
+ * exit on Ctrl-C; without it the WS servers keep the event loop alive
145
+ * indefinitely.
146
+ */
147
+ close(): void;
148
+ removeChannel(sessionId: string): void;
149
+ /** Give a session its site's skills: the agent then also gets `read_skill`. */
150
+ setSkills(sessionId: string, skills: SkillContent[]): void;
151
+ /** The tools a session's agent gets: the page's, and `read_skill` when the site has skills. */
152
+ listTools(sessionId: string): ToolDescriptor[];
153
+ /** Run a page tool in the browser (or `read_skill` here); resolves with its JSON result. Throws on tool errors. */
154
+ callTool(sessionId: string, name: string, args: Record<string, unknown>): Promise<unknown>;
155
+ /** The page's tools, and the engine's `read_skill` in place of a page tool of that name. */
156
+ private toolsOf;
157
+ /** Returns true when the request path belongs to WebMCP. */
158
+ matches(pathname: string): boolean;
159
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
160
+ handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void>;
161
+ private handleRegistrationConnection;
162
+ private handleChannelConnection;
163
+ private handleChannelMessage;
164
+ private parseTool;
165
+ private parseResource;
166
+ private parsePrompt;
167
+ private sendAck;
168
+ private resolveRequest;
169
+ private dispatch;
170
+ private forwardToBrowser;
171
+ private parseBody;
172
+ private sendJsonRpcResult;
173
+ private sendJsonRpcError;
174
+ }
175
+
176
+ /**
177
+ * Skills a site gives the visitor's AI: instructions for tasks on that
178
+ * site, in the Agent Skills format. The site names them (see
179
+ * `skill-sources.ts`); the engine fetches them when the site connects or
180
+ * changes its list, never while a conversation starts.
181
+ *
182
+ * One cache for every site, by content (a community skill two sites use is
183
+ * stored once), in `~/.leuria/skills` with cacache. Each site's grant keeps
184
+ * which skills it uses, so a skill is only ever offered in that site's
185
+ * sessions: the agent gets their names and descriptions, and reads one
186
+ * with the `read_skill` tool when a task calls for it.
187
+ *
188
+ * Skills don't widen what the AI can do: it still has only the page's
189
+ * tools. So there is no approval step, but Leuria shows them to the
190
+ * visitor, in the approval window and in the site's details.
191
+ */
192
+
193
+ /** A skill a site uses, as its grant keeps it. */
194
+ interface SiteSkill {
195
+ name: string;
196
+ description: string;
197
+ /** Where it comes from, for the visitor: the site's host, or `owner/repo`. */
198
+ source: string;
199
+ /** From the site itself, or shared from a public repository. */
200
+ shared: boolean;
201
+ /** Content address in the cache. */
202
+ integrity: string;
203
+ /** Set when the site added it after it was connected. */
204
+ addedAt?: string;
205
+ }
206
+ /** A site's skills: the refs it declared, and what they resolved to. */
207
+ interface SiteSkills {
208
+ refs: string[];
209
+ list: SiteSkill[];
210
+ }
211
+ interface SkillServiceOptions {
212
+ grants: GrantStore;
213
+ logger: Logger;
214
+ /** cacache folder; `null` keeps skills in memory (tests). */
215
+ cache?: string | null;
216
+ /** Fetches a ref's skills; tests replace it. */
217
+ fetch?: (ref: string, origin: string) => Promise<{
218
+ skills: SkillContent[];
219
+ source: string;
220
+ shared: boolean;
221
+ }>;
222
+ }
223
+ declare class SkillService {
224
+ private readonly options;
225
+ private readonly cache;
226
+ private readonly memory;
227
+ private readonly refreshing;
228
+ constructor(options: SkillServiceOptions);
229
+ /**
230
+ * Fetch and cache the skills `refs` name for `origin`. A ref that fails
231
+ * is logged and skipped; the first skill of a name wins.
232
+ */
233
+ resolve(origin: string, refs: string[]): Promise<SiteSkill[]>;
234
+ /**
235
+ * A connected site sent its skill refs with a session: when they changed,
236
+ * fetch them in the background and update its grant. Sessions starting
237
+ * meanwhile keep the skills it had.
238
+ */
239
+ refresh(origin: string, refs: string[] | undefined): void;
240
+ /** The skills a session of `origin` gets, from the cache. */
241
+ forSession(origin: string): Promise<SkillContent[]>;
242
+ /** Wait for background refreshes (tests). */
243
+ settled(): Promise<void>;
244
+ private put;
245
+ private get;
246
+ }
247
+
24
248
  /**
25
249
  * Sites the visitor approved. One grant per origin; the site holds the
26
250
  * token, the engine keeps only its SHA-256. Revoking a grant makes the
@@ -43,6 +267,8 @@ interface Grant {
43
267
  };
44
268
  /** What the site said its features need, when it connected (guidance for choosing its AI). */
45
269
  needs?: SiteNeeds;
270
+ /** The skills the site gives its AI: the refs it declared and what they resolved to. */
271
+ skills?: SiteSkills;
46
272
  }
47
273
  declare class GrantStore {
48
274
  private readonly path;
@@ -66,8 +292,10 @@ declare class GrantStore {
66
292
  agent: string;
67
293
  id: string;
68
294
  } | undefined): boolean;
69
- /** Create or replace the origin's grant; returns the new token. Keeps the site's AI and model choice; `needs` replaces what it declared. */
70
- create(origin: string, app?: string, needs?: SiteNeeds): string;
295
+ /** Replace the site's skills (it changed its list). Its open conversations keep the skills they started with. */
296
+ setSkills(origin: string, skills: SiteSkills | undefined): boolean;
297
+ /** Create or replace the origin's grant; returns the new token. Keeps the site's AI and model choice; `needs` and `skills` replace what it declared. */
298
+ create(origin: string, app?: string, needs?: SiteNeeds, skills?: SiteSkills): string;
71
299
  /** True when `token` is the origin's current token. */
72
300
  verify(origin: string, token: string | undefined): boolean;
73
301
  revoke(origin: string): boolean;
@@ -132,14 +360,6 @@ interface Embeddings {
132
360
  }>;
133
361
  }
134
362
 
135
- interface Logger {
136
- info: (msg: string, meta?: Record<string, unknown>) => void;
137
- warn: (msg: string, meta?: Record<string, unknown>) => void;
138
- error: (msg: string, meta?: Record<string, unknown>) => void;
139
- }
140
- /** Line-per-event logger on stderr, so stdout stays free. */
141
- declare function createLogger(verbose?: boolean): Logger;
142
-
143
363
  /**
144
364
  * Pairing: how a site gets a grant, without the engine ever answering a
145
365
  * site that the visitor didn't ask about.
@@ -177,7 +397,9 @@ interface PairingOptions {
177
397
  linksOnly: boolean;
178
398
  /** Plain-language name of the configured agent, shown on the page. */
179
399
  agentName: () => string;
180
- /** Called when a request is created (or a link repeated), e.g. to open the approval page or show the app. */
400
+ /** Fetch the skills a site names, to show them before the visitor answers. Without it, skills are ignored. */
401
+ resolveSkills?: (origin: string, refs: string[]) => Promise<SiteSkill[]>;
402
+ /** Called when a request is created (or a link repeated, or its skills are fetched), e.g. to open the approval page or show the app. */
181
403
  onRequest?: (request: PairingRequestInfo) => void;
182
404
  /** Called when the visitor decided, wherever they clicked. */
183
405
  onDecided?: (request: {
@@ -191,6 +413,11 @@ interface PairingRequestInfo {
191
413
  origin: string;
192
414
  app?: string;
193
415
  needs?: SiteNeeds;
416
+ /** The skills the site gives its AI; `loading` until they are fetched. */
417
+ skills?: {
418
+ loading: boolean;
419
+ list: Array<Pick<SiteSkill, "name" | "description" | "source" | "shared">>;
420
+ };
194
421
  approveUrl: string;
195
422
  }
196
423
  declare class Pairing {
@@ -210,6 +437,7 @@ declare class Pairing {
210
437
  app?: unknown;
211
438
  nonce?: unknown;
212
439
  needs?: unknown;
440
+ skills?: unknown;
213
441
  }): {
214
442
  requestId: string;
215
443
  } | {
@@ -221,6 +449,11 @@ declare class Pairing {
221
449
  private claim;
222
450
  /** Ask the visitor about `origin`, or update the question already asked. */
223
451
  private open;
452
+ /**
453
+ * Fetch the skills the site names, then show them: the question is asked
454
+ * again with them. Answered meanwhile: they go to the site's grant.
455
+ */
456
+ private fetchSkills;
224
457
  private info;
225
458
  /** Requests waiting for the visitor, e.g. for the desktop app. */
226
459
  pending(): PairingRequestInfo[];
@@ -324,115 +557,6 @@ interface AgentModels {
324
557
  configId?: string;
325
558
  }
326
559
 
327
- /**
328
- * In-process WebMCP relay for the bridge daemon.
329
- *
330
- * Two surfaces, one server:
331
- *
332
- * - **Browser → bridge (WebSocket)**: the browser console connects to
333
- * `ws://127.0.0.1:<port>/webmcp/register`, exchanges its registration
334
- * token for a channel WS at `/webmcp/channel/:id?token=…`, then
335
- * declares the tools / resources / prompts it wants to expose.
336
- *
337
- * - **Agent → bridge (HTTP MCP)**: the spawned ACP agent calls JSON-RPC
338
- * 2.0 against `POST /webmcp/mcp` with `Authorization: Bearer
339
- * <channelToken>`. `tools/list` and friends read the channel's
340
- * registry; `tools/call` etc. forward the request to the browser via
341
- * the channel WS and await the response.
342
- *
343
- * Long-blocking calls (`tools/call`, `resources/read`, `prompts/get`)
344
- * switch to chunked HTTP encoding and emit periodic newline keep-alives
345
- * so the agent's HTTP client doesn't abort while the user is interacting
346
- * in the browser.
347
- *
348
- * Origin checks happen in `server.ts` before requests reach this class.
349
- */
350
-
351
- interface ToolDescriptor {
352
- name: string;
353
- description?: string;
354
- inputSchema: Record<string, unknown>;
355
- annotations?: Record<string, unknown>;
356
- }
357
- interface WebMcpChannelSummary {
358
- id: string;
359
- sessionId: string;
360
- connected: boolean;
361
- tools: Array<{
362
- name: string;
363
- description?: string;
364
- }>;
365
- resources: Array<{
366
- uri: string;
367
- name: string;
368
- description?: string;
369
- }>;
370
- prompts: Array<{
371
- name: string;
372
- description?: string;
373
- }>;
374
- }
375
- interface WebMcpLogger {
376
- info: (msg: string, meta?: Record<string, unknown>) => void;
377
- warn: (msg: string, meta?: Record<string, unknown>) => void;
378
- error: (msg: string, meta?: Record<string, unknown>) => void;
379
- }
380
- declare class WebMcpServer {
381
- private readonly logger;
382
- private readonly port;
383
- private readonly channels;
384
- private readonly registrationTokens;
385
- private readonly sessionChannels;
386
- private readonly tokenToChannel;
387
- private readonly registerWss;
388
- private readonly channelWss;
389
- constructor(logger: WebMcpLogger, port: number);
390
- /**
391
- * Allocate a channel for an ACP session. Returns:
392
- * - `registrationToken` — base64-encoded `{ server, token }` blob the
393
- * browser presents on the `/webmcp/register` WS to claim the channel.
394
- * - `channelToken` — Bearer credential the agent presents on the
395
- * `/webmcp/mcp` HTTP endpoint.
396
- * - `channelId` — server-side handle (used in the channel WS path).
397
- */
398
- createChannel(sessionId: string): {
399
- registrationToken: string;
400
- channelToken: string;
401
- channelId: string;
402
- };
403
- getChannelSummaries(): WebMcpChannelSummary[];
404
- /**
405
- * Tear everything down — close the two WebSocketServers, kill every
406
- * ping timer, reject every pending request, force-close every browser
407
- * channel WS. Called from `daemon.ts`'s `shutdown` so the process can
408
- * exit on Ctrl-C; without it the WS servers keep the event loop alive
409
- * indefinitely.
410
- */
411
- close(): void;
412
- removeChannel(sessionId: string): void;
413
- /** The page tools a session's browser has declared. */
414
- listTools(sessionId: string): ToolDescriptor[];
415
- /** Run a page tool in the browser; resolves with its JSON result. Throws on tool errors. */
416
- callTool(sessionId: string, name: string, args: Record<string, unknown>): Promise<unknown>;
417
- /** Returns true when the request path belongs to WebMCP. */
418
- matches(pathname: string): boolean;
419
- handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
420
- handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void>;
421
- private handleRegistrationConnection;
422
- private handleChannelConnection;
423
- private handleChannelMessage;
424
- private parseTool;
425
- private parseResource;
426
- private parsePrompt;
427
- private sendAck;
428
- private resolveRequest;
429
- private dispatch;
430
- private forwardToBrowser;
431
- private parseBody;
432
- private sendJsonRpcResult;
433
- private sendJsonRpcError;
434
- }
435
-
436
560
  /**
437
561
  * Agent sessions opened by websites.
438
562
  *
@@ -548,6 +672,8 @@ interface SessionManagerOptions {
548
672
  logger: Logger;
549
673
  /** Command for the agent serving `origin`: its own choice, or the default AI. */
550
674
  resolveAgent: (origin: string) => Promise<AgentLaunch>;
675
+ /** The skills `origin` gives its AI, already fetched: the agent gets their list and `read_skill`. */
676
+ loadSkills?: (origin: string) => Promise<SkillContent[]>;
551
677
  /** ACP handshake timeout. */
552
678
  startTimeoutMs?: number;
553
679
  /**
@@ -672,6 +798,8 @@ interface EngineOptions {
672
798
  onAgentState?: SessionManagerOptions["onAgentState"];
673
799
  /** Embeddings for connected sites. Default: an embedding model in LM Studio or Ollama on this computer. */
674
800
  embeddings?: Embeddings;
801
+ /** The skills sites give their AI. Default: fetched from GitHub or the site, cached in `~/.leuria/skills`. */
802
+ skills?: SkillService;
675
803
  }
676
804
  interface EngineHandle {
677
805
  port: number;
@@ -794,8 +922,13 @@ interface SignInStatus {
794
922
  /** Models the agent offers once signed in (from `session/new`), with the visitor's choice as `current`. */
795
923
  models?: AgentModels;
796
924
  }
797
- /** Is the visitor signed in to this agent? Opens (and closes) a real ACP session, within a time limit. */
798
- declare function checkSignIn(id: string, onProgress?: (message: string) => void): Promise<SignInStatus>;
925
+ /**
926
+ * Is the visitor signed in to this agent? Opens (and closes) a real ACP session, within a time limit.
927
+ * `quiet`: a background look for its models; the answer is not remembered as the agent's state.
928
+ */
929
+ declare function checkSignIn(id: string, onProgress?: (message: string) => void, options?: {
930
+ quiet?: boolean;
931
+ }): Promise<SignInStatus>;
799
932
  /** Sign in with an advertised method, then confirm with `session/new`. */
800
933
  declare function signIn(id: string, options?: {
801
934
  methodId?: string;
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  resolveAgentCommand,
19
19
  signIn,
20
20
  startEngine
21
- } from "./chunk-BXSAYWR3.js";
21
+ } from "./chunk-JOCM56MT.js";
22
22
  export {
23
23
  DEFAULT_PORT,
24
24
  GrantStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leuria/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Bring your own AI to the web: the Leuria engine. Lets websites you approve use the AI on your machine, through tools the page provides.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,9 +29,16 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@agentclientprotocol/sdk": "^1.5.0",
32
- "ws": "^8.18.0"
32
+ "cacache": "^21.0.1",
33
+ "cross-spawn": "^7.0.6",
34
+ "giget": "^3.3.1",
35
+ "ipaddr.js": "^2.5.0",
36
+ "ws": "^8.18.0",
37
+ "yaml": "^2.9.1"
33
38
  },
34
39
  "devDependencies": {
40
+ "@types/cacache": "^20.0.1",
41
+ "@types/cross-spawn": "^6.0.6",
35
42
  "@types/node": "^22.0.0",
36
43
  "@types/ws": "^8.5.0",
37
44
  "tsup": "^8.4.0",