@leuria/cli 0.1.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.
@@ -0,0 +1,819 @@
1
+ import * as http from 'node:http';
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { AuthMethod, InitializeResponse, McpServer } from '@agentclientprotocol/sdk';
4
+ import { Duplex } from 'node:stream';
5
+
6
+ /**
7
+ * What a site says its features need, so Leuria can guide the visitor to
8
+ * an AI and model that fit, and no bigger (bigger costs more). A closed,
9
+ * small vocabulary: capabilities and how hard the tasks are, never model
10
+ * names. Guidance only: the visitor decides.
11
+ */
12
+ type Effort = "light" | "standard" | "deep";
13
+ interface SiteNeeds {
14
+ /** The site's features use page tools. */
15
+ tools?: boolean;
16
+ /** The site sends images. */
17
+ images?: boolean;
18
+ /** How hard the tasks are: `light` (answer, extract, summarize), `standard` (several steps with tools), `deep` (long reasoning, agentic work). */
19
+ effort?: Effort;
20
+ /** About how much text one turn sends, in tokens. */
21
+ context?: number;
22
+ }
23
+
24
+ /**
25
+ * Sites the visitor approved. One grant per origin; the site holds the
26
+ * token, the engine keeps only its SHA-256. Revoking a grant makes the
27
+ * site ask again.
28
+ */
29
+
30
+ interface Grant {
31
+ origin: string;
32
+ /** Name the site gave when it asked to connect. */
33
+ app?: string;
34
+ tokenHash: string;
35
+ createdAt: string;
36
+ lastUsedAt?: string;
37
+ /** Registry id of the AI this site uses; the default AI when absent. */
38
+ agent?: string;
39
+ /** The model this site uses, for the AI it was chosen with; that AI's model choice otherwise. */
40
+ model?: {
41
+ agent: string;
42
+ id: string;
43
+ };
44
+ /** What the site said its features need, when it connected (guidance for choosing its AI). */
45
+ needs?: SiteNeeds;
46
+ }
47
+ declare class GrantStore {
48
+ private readonly path;
49
+ private grants;
50
+ private loadedMtime;
51
+ private readonly removedListeners;
52
+ private readonly changedListeners;
53
+ /** `path: null` keeps grants in memory only (tests, `leuria test`). */
54
+ constructor(path?: string | null);
55
+ /** Called with each origin whose grant disappears, here or in another process. */
56
+ onRemoved(listener: (origin: string) => void): void;
57
+ /** Called with each origin whose AI or model changes. */
58
+ onChanged(listener: (origin: string) => void): void;
59
+ list(): Grant[];
60
+ has(origin: string): boolean;
61
+ get(origin: string): Grant | undefined;
62
+ /** Use `agent` for this site, or the default AI when `undefined`. */
63
+ setAgent(origin: string, agent: string | undefined): boolean;
64
+ /** Use `model` of `agent` for this site, or that AI's own choice when `undefined`. */
65
+ setModel(origin: string, model: {
66
+ agent: string;
67
+ id: string;
68
+ } | 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;
71
+ /** True when `token` is the origin's current token. */
72
+ verify(origin: string, token: string | undefined): boolean;
73
+ revoke(origin: string): boolean;
74
+ /** Pick up changes made by another process, such as `leuria sites revoke`. */
75
+ private reload;
76
+ private save;
77
+ }
78
+ /** `scheme://host[:port]`, lowercase, no trailing slash. Throws if not http(s). */
79
+ declare function normalizeOrigin(origin: string): string;
80
+
81
+ /**
82
+ * Leuria's state on the visitor's machine, under `~/.leuria` (or
83
+ * `LEURIA_HOME`):
84
+ *
85
+ * config.json engine settings (port, agent, the model chosen for each agent,
86
+ * the agents known to work)
87
+ * models.json the models each agent offered, so none is started just to list them
88
+ * grants.json sites the visitor approved, with hashed tokens
89
+ * agents/ agent adapters installed by `leuria setup`
90
+ */
91
+ declare const DEFAULT_PORT = 19570;
92
+
93
+ /**
94
+ * LLM providers: LM Studio and Ollama on this computer, and any
95
+ * OpenAI-compatible API (the visitor's own key, a company gateway…).
96
+ *
97
+ * An AI id for an LLM is `llm:<providerId>` (the service is the AI; its
98
+ * model is chosen like an agent's, in config.json `models`), or, from
99
+ * before, `llm:<providerId>/<model>` with the model fixed, e.g.
100
+ * `llm:ollama/qwen3:8b`. `lmstudio` and `ollama` exist without
101
+ * configuration, at their default local URLs.
102
+ */
103
+ type LlmKind = "lmstudio" | "ollama" | "openai";
104
+ interface LlmProvider {
105
+ id: string;
106
+ kind: LlmKind;
107
+ /** Shown to the visitor: "LM Studio", "Ollama", or the name they gave. */
108
+ name: string;
109
+ /** OpenAI-compatible base URL, ending in `/v1`. */
110
+ baseUrl: string;
111
+ apiKey?: string;
112
+ }
113
+
114
+ /**
115
+ * Embeddings for sites, from an embedding model on this computer (LM
116
+ * Studio or Ollama). Only local services: a site indexing its pages must
117
+ * not send them to a cloud service or spend the visitor's API credits.
118
+ *
119
+ * POST /embed { texts: string[], kind?: "query" | "document" } → { model, vectors }
120
+ */
121
+
122
+ interface Embedder {
123
+ provider: LlmProvider;
124
+ model: string;
125
+ }
126
+ interface Embeddings {
127
+ /** The embedding model sites get now, if any. Cheap: cached for a few seconds. */
128
+ find: () => Promise<Embedder | undefined>;
129
+ embed: (texts: string[], kind: "query" | "document") => Promise<{
130
+ model: string;
131
+ vectors: number[][];
132
+ }>;
133
+ }
134
+
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
+ /**
144
+ * Pairing: how a site gets a grant, without the engine ever answering a
145
+ * site that the visitor didn't ask about.
146
+ *
147
+ * 1. On the visitor's click, the page makes a secret nonce and opens
148
+ * `leuria://connect?origin=…&app=…&nonce=…`. The desktop app gets the
149
+ * link, registers it (`link()`, through its admin API) and asks the
150
+ * visitor in its own window.
151
+ * 2. The page long-polls `POST /connect/claim { nonce }`. The engine
152
+ * answers only the browser's `Origin` that a link named (a page cannot
153
+ * forge it), and only with the nonce of that link. On Allow, the token
154
+ * goes out once.
155
+ *
156
+ * A site that forges a link naming another site gets nothing: it can't
157
+ * claim with the other site's Origin, and the other site doesn't know the
158
+ * nonce. Until a link names it, a site gets no answer at all (see
159
+ * `server.ts`: no CORS), so it can't tell that Leuria is installed.
160
+ *
161
+ * The CLI engine (no app, so no links) serves the same claim: the first
162
+ * claim creates the request, and the visitor answers on the engine's own
163
+ * approval page, opened in their browser. Only a click there, posted from
164
+ * the engine's origin, decides.
165
+ */
166
+
167
+ interface PairingOptions {
168
+ grants: GrantStore;
169
+ logger: Logger;
170
+ /** Engine's own origins, e.g. `http://127.0.0.1:19570`. */
171
+ selfOrigins: Set<string>;
172
+ port: number;
173
+ /**
174
+ * Requests come only from links the desktop app received (`link()`).
175
+ * Off for the CLI: the first claim creates the request.
176
+ */
177
+ linksOnly: boolean;
178
+ /** Plain-language name of the configured agent, shown on the page. */
179
+ agentName: () => string;
180
+ /** Called when a request is created (or a link repeated), e.g. to open the approval page or show the app. */
181
+ onRequest?: (request: PairingRequestInfo) => void;
182
+ /** Called when the visitor decided, wherever they clicked. */
183
+ onDecided?: (request: {
184
+ requestId: string;
185
+ origin: string;
186
+ allowed: boolean;
187
+ }) => void;
188
+ }
189
+ interface PairingRequestInfo {
190
+ requestId: string;
191
+ origin: string;
192
+ app?: string;
193
+ needs?: SiteNeeds;
194
+ approveUrl: string;
195
+ }
196
+ declare class Pairing {
197
+ private readonly options;
198
+ private readonly requests;
199
+ private readonly deniedAt;
200
+ constructor(options: PairingOptions);
201
+ /** A link named this origin and it hasn't collected its answer yet: it may be answered. */
202
+ hasRequest(origin: string): boolean;
203
+ /**
204
+ * A `leuria://connect` link reached the desktop app. Returns the request,
205
+ * or why it was refused. A repeated link for the same site replaces the
206
+ * nonce (the page tried again) instead of asking twice.
207
+ */
208
+ link(input: {
209
+ origin?: unknown;
210
+ app?: unknown;
211
+ nonce?: unknown;
212
+ needs?: unknown;
213
+ }): {
214
+ requestId: string;
215
+ } | {
216
+ error: string;
217
+ };
218
+ /** Returns true when the request was a pairing route. */
219
+ handle(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<boolean>;
220
+ /** The page collects the visitor's answer, with the nonce of its link. */
221
+ private claim;
222
+ /** Ask the visitor about `origin`, or update the question already asked. */
223
+ private open;
224
+ private info;
225
+ /** Requests waiting for the visitor, e.g. for the desktop app. */
226
+ pending(): PairingRequestInfo[];
227
+ /**
228
+ * Decide from a trusted place (the desktop app's native window).
229
+ * `granted` runs on Allow once the grant exists, before the site gets
230
+ * its token: e.g. to set the site's AI and model from the first message.
231
+ */
232
+ decideById(requestId: string, allow: boolean, granted?: (origin: string) => void): boolean;
233
+ private decide;
234
+ private wait;
235
+ private expire;
236
+ private renderPage;
237
+ }
238
+
239
+ /**
240
+ * Harness policy for agents started on behalf of a website.
241
+ *
242
+ * Two layers, because the agent is an untrusted component:
243
+ *
244
+ * 1. Session options sent to the adapter on `session/new`
245
+ * ({@link buildSessionMeta}). For claude-agent-acp these are spread
246
+ * into the Claude Agent SDK options: no built-in tools, no user or
247
+ * project settings (so no plugins, hooks or extra MCP servers), no
248
+ * bypass mode, and the WebMCP server's tools pre-allowed.
249
+ *
250
+ * 2. The ACP `session/request_permission` handler
251
+ * ({@link decidePermission}). Anything that still asks for
252
+ * permission is denied unless it is one of the page's WebMCP
253
+ * tools. This holds even if an adapter ignores layer 1.
254
+ */
255
+ /** MCP server name the bridge registers for the browser tools. */
256
+ declare const WEBMCP_SERVER_NAME = "webmcp";
257
+ interface SessionConfig {
258
+ /** System prompt. Replaces the adapter's coding-agent preset. */
259
+ systemPrompt?: string;
260
+ model?: string;
261
+ maxTurns?: number;
262
+ }
263
+ /** Build the `_meta` payload for ACP `session/new`. */
264
+ declare function buildSessionMeta(config?: SessionConfig): Record<string, unknown>;
265
+ interface PermissionDecision {
266
+ allow: boolean;
267
+ toolName: string;
268
+ /** ACP response for `session/request_permission`. */
269
+ response: {
270
+ outcome: {
271
+ outcome: "selected";
272
+ optionId: string;
273
+ } | {
274
+ outcome: "cancelled";
275
+ };
276
+ };
277
+ }
278
+ /** Decide an ACP `session/request_permission` request. Deny by default. */
279
+ declare function decidePermission(params: unknown): PermissionDecision;
280
+
281
+ /**
282
+ * Persistent multi-turn ACP client session.
283
+ *
284
+ * No one-shot runs, no usage accounting, no interactive permission UI.
285
+ * Permission requests go through the harness policy
286
+ * (deny unless WebMCP tool), and session options come from
287
+ * `buildSessionMeta`.
288
+ */
289
+
290
+ interface ToolCallEvent {
291
+ toolCallId: string;
292
+ title?: string;
293
+ kind?: string;
294
+ status?: string;
295
+ toolName?: string;
296
+ }
297
+ /** A file sent with a prompt. Images are base64; other files are text. */
298
+ type PromptAttachment = {
299
+ type: "image";
300
+ mimeType: string;
301
+ data: string;
302
+ name?: string;
303
+ } | {
304
+ type: "text";
305
+ text: string;
306
+ name?: string;
307
+ mimeType?: string;
308
+ };
309
+ /** What the agent said about itself in `initialize`. */
310
+ interface AgentInfo {
311
+ authMethods: AuthMethod[];
312
+ agentCapabilities: NonNullable<InitializeResponse["agentCapabilities"]>;
313
+ agentInfo?: InitializeResponse["agentInfo"];
314
+ }
315
+ /** The models an agent offers in `session/new`, and how to pick one. */
316
+ interface AgentModels {
317
+ current?: string;
318
+ options: Array<{
319
+ id: string;
320
+ name: string;
321
+ description?: string;
322
+ }>;
323
+ /** ACP v1 config option id (category "model"); absent for the older `models` field. */
324
+ configId?: string;
325
+ }
326
+
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
+ /**
437
+ * Agent sessions opened by websites.
438
+ *
439
+ * Every session belongs to the origin that prepared it,
440
+ * has a WebMCP channel, and runs the visitor's agent in an empty sandbox
441
+ * directory under the harness policy. The page chooses the prompts; the
442
+ * visitor's config chooses the agent.
443
+ *
444
+ * prepare -> `pending_approval`, WebMCP channel allocated, nothing spawned
445
+ * approve -> spawn the ACP agent, send the first prompt if any
446
+ * (without one the session goes `idle` and emits `ready`,
447
+ * which lets a page warm the agent up ahead of time)
448
+ * promptTurn -> follow-up prompt on an idle session (same agent context)
449
+ * cancelTurn -> stop the current turn, keep the session
450
+ * close -> kill the agent; terminal `completed`
451
+ *
452
+ * SSE events: `webmcp_ready`, `ready`, `turn_start`, `chunk`, `thought`, `tool_call`,
453
+ * `permission`, `log`, `turn_completed`, `completed`, `failed`,
454
+ * `cancelled`.
455
+ */
456
+
457
+ type SessionStatus = "pending_approval" | "running" | "idle" | "completed" | "failed" | "cancelled";
458
+ interface PrepareParams {
459
+ /** First turn. Without it, `approve` only starts the agent. */
460
+ prompt?: string;
461
+ attachments?: PromptAttachment[];
462
+ systemPrompt?: string;
463
+ maxTurns?: number;
464
+ /** Origin that owns the session; `local` for requests without `Origin`. */
465
+ origin: string;
466
+ }
467
+ interface SessionInfo {
468
+ id: string;
469
+ status: SessionStatus;
470
+ origin: string;
471
+ createdAt: string;
472
+ error?: string;
473
+ registrationToken: string;
474
+ webmcpUrl: string;
475
+ }
476
+ type SessionListener = (event: string, data: unknown) => void;
477
+ /** What an agent profile needs to set up one session. */
478
+ interface SessionSetup {
479
+ /** The session's empty sandbox directory (also the agent's cwd). */
480
+ sandbox: string;
481
+ /** MCP endpoint and bearer token for the page tools. */
482
+ mcpUrl: string;
483
+ mcpToken: string;
484
+ systemPrompt?: string;
485
+ maxTurns?: number;
486
+ }
487
+ /** What the session manager needs from a running agent, ACP or LLM. */
488
+ interface LiveAgent {
489
+ readonly isAlive: boolean;
490
+ /** `authRequired`: the agent is signed out (ACP `auth_required`). */
491
+ start(): Promise<{
492
+ error?: string;
493
+ authRequired?: boolean;
494
+ }>;
495
+ /** Models the agent offered in `session/new` (ACP agents). */
496
+ readonly models?: AgentModels | null;
497
+ prompt(text: string, attachments: PromptAttachment[]): Promise<{
498
+ error?: string;
499
+ }>;
500
+ cancelTurn(): Promise<void>;
501
+ close(): void;
502
+ }
503
+ interface AgentLaunch {
504
+ /** Registry or LLM id, so the engine can remember whether this AI works. */
505
+ id?: string;
506
+ /**
507
+ * An LLM provider instead of an ACP agent: the engine runs the tool
508
+ * loop itself (`LlmSession`); `command` and `args` are unused.
509
+ */
510
+ llm?: {
511
+ provider: LlmProvider;
512
+ model: string;
513
+ };
514
+ command: string;
515
+ args: string[];
516
+ /** Environment from the agent's registry entry. */
517
+ env?: Record<string, string>;
518
+ /** The model the visitor chose for this agent; the agent's default otherwise. */
519
+ model?: string;
520
+ /**
521
+ * Agent-specific session setup (environment, MCP servers, `_meta`).
522
+ * Without it, the Claude policy applies: page tools as an ACP MCP
523
+ * server and `buildSessionMeta`.
524
+ */
525
+ configure?: (setup: SessionSetup) => {
526
+ env?: Record<string, string>;
527
+ mcpServers?: McpServer[];
528
+ meta?: Record<string, unknown>;
529
+ };
530
+ }
531
+ interface Session {
532
+ id: string;
533
+ status: SessionStatus;
534
+ params: PrepareParams;
535
+ cwd: string;
536
+ createdAt: Date;
537
+ error?: string;
538
+ /** Text of the current turn, replayed to late SSE subscribers. */
539
+ turnChunks: string[];
540
+ listeners: Set<SessionListener>;
541
+ live?: LiveAgent;
542
+ registrationToken: string;
543
+ channelToken: string;
544
+ }
545
+ interface SessionManagerOptions {
546
+ webMcpServer: WebMcpServer;
547
+ port: number;
548
+ logger: Logger;
549
+ /** Command for the agent serving `origin`: its own choice, or the default AI. */
550
+ resolveAgent: (origin: string) => Promise<AgentLaunch>;
551
+ /** ACP handshake timeout. */
552
+ startTimeoutMs?: number;
553
+ /**
554
+ * What a real session learned about an AI: it started (with the models it
555
+ * offers), or it is signed out. Startup trusts this instead of checking.
556
+ */
557
+ onAgentState?: (id: string, state: {
558
+ ok: boolean;
559
+ models?: AgentModels | null;
560
+ }) => void;
561
+ /**
562
+ * Command that relays MCP over stdio to the engine (`leuria mcp-stdio`),
563
+ * for agents without HTTP MCP support. ACP requires every agent to
564
+ * support stdio MCP servers; HTTP is optional.
565
+ */
566
+ stdioMcpCommand?: {
567
+ command: string;
568
+ args: string[];
569
+ };
570
+ }
571
+ declare class SessionManager {
572
+ private readonly options;
573
+ private readonly sessions;
574
+ constructor(options: SessionManagerOptions);
575
+ prepare(params: PrepareParams): SessionInfo;
576
+ approve(sessionId: string): SessionInfo;
577
+ promptTurn(sessionId: string, prompt: string, attachments?: PromptAttachment[]): SessionInfo;
578
+ cancelTurn(sessionId: string): Promise<SessionInfo>;
579
+ cancel(sessionId: string): SessionInfo;
580
+ close(sessionId: string): SessionInfo;
581
+ get(sessionId: string): SessionInfo | null;
582
+ /** Current-turn text, for SSE replay. */
583
+ turnChunks(sessionId: string): string[];
584
+ webmcpReady(session: Session | string): {
585
+ registrationToken: string;
586
+ webmcpUrl: string;
587
+ };
588
+ addListener(sessionId: string, listener: SessionListener): void;
589
+ removeListener(sessionId: string, listener: SessionListener): void;
590
+ /** End every session of an origin, e.g. when its grant is revoked. */
591
+ /** End the sessions of the origins that match (their AI or model changed): the next message starts a new one. */
592
+ closeWhere(match: (origin: string) => boolean): void;
593
+ closeOrigin(origin: string): void;
594
+ shutdown(): void;
595
+ private execute;
596
+ /** An LLM provider, driven by the engine with the page's tools. */
597
+ private llmSession;
598
+ /** An ACP agent process under its policy. */
599
+ private acpSession;
600
+ private runTurn;
601
+ private fail;
602
+ /**
603
+ * Fresh empty directory per session, so the agent finds no project
604
+ * files (CLAUDE.md, repo) to act on.
605
+ */
606
+ private allocateSandbox;
607
+ private teardown;
608
+ private notify;
609
+ private require;
610
+ private toInfo;
611
+ }
612
+
613
+ /** Injected by tsup at build time; `dev` when run from source. */
614
+ declare const VERSION: string;
615
+
616
+ /**
617
+ * Loopback HTTP + WebSocket server: the engine's only door.
618
+ *
619
+ * - Binds to 127.0.0.1 and rejects any `Host` other than
620
+ * 127.0.0.1/localhost on our port (blocks DNS rebinding).
621
+ * - Requests without `Origin` come from local processes (the agent
622
+ * calling `/webmcp/mcp`, the CLI, curl) and are trusted: they already
623
+ * run as the visitor.
624
+ * - Silent (the desktop app): a web origin the visitor never connected,
625
+ * and that no `leuria://connect` link named, gets nothing it can read,
626
+ * on any route. It can't tell that Leuria is here.
627
+ * - A web origin claims its token with `/connect/claim`; everything else
628
+ * needs the origin's grant token in `Authorization: Bearer`, and
629
+ * WebSocket upgrades need a grant.
630
+ * - The engine's own origin may only use `/connect` (the CLI's approval page).
631
+ */
632
+
633
+ interface EngineOptions {
634
+ port?: number;
635
+ grants: GrantStore;
636
+ logger: Logger;
637
+ /** Plain-language agent name, e.g. "Claude Code"; a function when it can change. */
638
+ agentName: string | (() => string);
639
+ /** What a connected site uses, for its status line ("Claude · Sonnet 5"); the default AI's name otherwise. */
640
+ siteAgentName?: (origin: string) => string;
641
+ /** Command for the agent serving `origin` (a site's own choice or the default); called once per session. */
642
+ resolveAgent: (origin: string) => Promise<AgentLaunch>;
643
+ /** Called when a site asks to connect. */
644
+ onPairingRequest?: (request: PairingRequestInfo) => void;
645
+ onPairingDecided?: (request: {
646
+ requestId: string;
647
+ origin: string;
648
+ allowed: boolean;
649
+ }) => void;
650
+ /**
651
+ * Silent to sites the visitor didn't connect: no answer they can read,
652
+ * so they can't tell that Leuria is here. Pairing then starts from a
653
+ * `leuria://connect` link. Default: on with the desktop app (`admin`).
654
+ */
655
+ silent?: boolean;
656
+ /**
657
+ * Admin API for the desktop app, under `/admin/`. Every request must
658
+ * carry `Authorization: Bearer <token>`, and come from the app's webview
659
+ * (`tauri://` origin) or a local process.
660
+ */
661
+ admin?: {
662
+ token: string;
663
+ handle: (req: http.IncomingMessage, res: http.ServerResponse, pathname: string, pairing: Pairing, sessions: Pick<SessionManager, "closeWhere">) => Promise<boolean>;
664
+ };
665
+ startTimeoutMs?: number;
666
+ /** See `SessionManagerOptions.stdioMcpCommand`. */
667
+ stdioMcpCommand?: {
668
+ command: string;
669
+ args: string[];
670
+ };
671
+ /** See `SessionManagerOptions.onAgentState`. */
672
+ onAgentState?: SessionManagerOptions["onAgentState"];
673
+ /** Embeddings for connected sites. Default: an embedding model in LM Studio or Ollama on this computer. */
674
+ embeddings?: Embeddings;
675
+ }
676
+ interface EngineHandle {
677
+ port: number;
678
+ pairing: Pairing;
679
+ sessions: SessionManager;
680
+ /** Revoke an origin's grant and end its sessions. */
681
+ revoke: (origin: string) => boolean;
682
+ close: () => Promise<void>;
683
+ }
684
+ declare function startEngine(options: EngineOptions): Promise<EngineHandle>;
685
+
686
+ /**
687
+ * The ACP agent registry (https://agentclientprotocol.com/registry,
688
+ * source and FORMAT.md at https://github.com/agentclientprotocol/registry):
689
+ * one JSON document listing agents and how to run them.
690
+ *
691
+ * distribution.npx { package: "name@version", args?, env? }
692
+ * distribution.uvx { package, args?, env? }
693
+ * distribution.binary { "<os>-<arch>": { archive, cmd, args?, env?, sha256? } }
694
+ */
695
+ interface PackageDistribution {
696
+ package: string;
697
+ args?: string[];
698
+ env?: Record<string, string>;
699
+ }
700
+ interface BinaryTarget {
701
+ archive: string;
702
+ cmd: string;
703
+ args?: string[];
704
+ env?: Record<string, string>;
705
+ sha256?: string;
706
+ }
707
+ interface RegistryEntry {
708
+ id: string;
709
+ name: string;
710
+ version: string;
711
+ description?: string;
712
+ repository?: string;
713
+ website?: string;
714
+ license?: string;
715
+ icon?: string;
716
+ distribution: {
717
+ npx?: PackageDistribution;
718
+ uvx?: PackageDistribution;
719
+ binary?: Record<string, BinaryTarget>;
720
+ };
721
+ }
722
+ /** The registry's agents; `[]` when it cannot be reached. Cached for 10 minutes. */
723
+ declare function fetchRegistry(): Promise<RegistryEntry[]>;
724
+ declare function getRegistryEntry(id: string): Promise<RegistryEntry | null>;
725
+
726
+ /**
727
+ * Agents, straight from the ACP registry. Any registry agent can be used:
728
+ * it is installed once from its registry entry (npx package, uvx package
729
+ * or binary archive) under `~/.leuria/agents/<id>@<version>`, so later
730
+ * sessions start without a download and work offline.
731
+ *
732
+ * Sign-in follows ACP too (see `auth.ts`): `authenticate` with a method
733
+ * the agent advertises.
734
+ *
735
+ * A few agents get an extra hardening profile on top (`profiles.ts`),
736
+ * keyed by registry id.
737
+ */
738
+
739
+ /** An agent installed from the registry, ready to spawn. */
740
+ interface InstalledAgent {
741
+ id: string;
742
+ name: string;
743
+ version: string;
744
+ dir: string;
745
+ command: string;
746
+ /** What runs the agent before its registry args, e.g. the package's JS entry. */
747
+ launchArgs: string[];
748
+ /** The registry's args; terminal auth methods replace these. */
749
+ args: string[];
750
+ env: Record<string, string>;
751
+ }
752
+ /** The newest installed version of `id`, if any. */
753
+ declare function installedAgent(id: string): InstalledAgent | null;
754
+ /**
755
+ * The agent to run: the installed version, or a fresh install from the
756
+ * registry. `update` installs the registry's current version if newer.
757
+ */
758
+ declare function ensureAgent(id: string, options?: {
759
+ update?: boolean;
760
+ onProgress?: (message: string) => void;
761
+ }): Promise<InstalledAgent>;
762
+ /**
763
+ * How the engine launches `id` for a session: an ACP agent or an LLM
764
+ * provider. `model` overrides the model chosen for the agent (a site's own choice).
765
+ */
766
+ declare function resolveAgentCommand(id: string, model?: string): Promise<AgentLaunch & {
767
+ name: string;
768
+ }>;
769
+ /** Plain-language name for the configured agent. */
770
+ declare function agentName(id: string): string;
771
+ declare function listAgents(): Promise<Array<RegistryEntry & {
772
+ installed?: string;
773
+ }>>;
774
+
775
+ /**
776
+ * Sign-in, the ACP v1 way. The agent advertises `authMethods` in
777
+ * `initialize`; whether the visitor is signed in is known from
778
+ * `session/new`, which fails with `auth_required` otherwise.
779
+ *
780
+ * ACP v1 (and the registry's AUTHENTICATION.md) has two method kinds:
781
+ * - agent methods (no `type`): the client calls `authenticate` and the
782
+ * agent runs its own flow, usually OAuth in the browser;
783
+ * - `type: "terminal"`: the client re-runs the agent with the method's
784
+ * `args`/`env` (replacing the registry's) in an interactive terminal.
785
+ * It MUST NOT be passed to `authenticate`. Agents only offer it when
786
+ * the client sets `auth.terminal`, which `leuria login` does in a TTY
787
+ * and the desktop app never does.
788
+ */
789
+
790
+ interface SignInStatus {
791
+ ok: boolean;
792
+ detail: string;
793
+ methods: AgentInfo["authMethods"];
794
+ /** Models the agent offers once signed in (from `session/new`), with the visitor's choice as `current`. */
795
+ models?: AgentModels;
796
+ }
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>;
799
+ /** Sign in with an advertised method, then confirm with `session/new`. */
800
+ declare function signIn(id: string, options?: {
801
+ methodId?: string;
802
+ /** The caller can run an interactive terminal method (a TTY). */
803
+ terminal?: boolean;
804
+ onProgress?: (message: string) => void;
805
+ /** Cancel a sign-in in progress: the agent is stopped (which frees its login callback). */
806
+ signal?: AbortSignal;
807
+ /** The sign-in page the agent opened in the browser, for a "didn't see it?" link. */
808
+ onUrl?: (url: string) => void;
809
+ }): Promise<SignInStatus>;
810
+
811
+ /**
812
+ * Which agent CLIs are already on this computer, to suggest one in
813
+ * onboarding ("You already have Codex"). The CLI → registry id map is the
814
+ * only hardcoded knowledge; everything else comes from the registry.
815
+ */
816
+ /** Registry ids whose CLI is installed on this machine. */
817
+ declare function detectInstalledClis(): string[];
818
+
819
+ export { type AgentLaunch, DEFAULT_PORT, type EngineHandle, type EngineOptions, type Grant, GrantStore, type InstalledAgent, type Logger, type PrepareParams, type RegistryEntry, type SessionInfo, type SessionStatus, type ToolCallEvent, VERSION, WEBMCP_SERVER_NAME, agentName, buildSessionMeta, checkSignIn, createLogger, decidePermission, detectInstalledClis, ensureAgent, fetchRegistry, getRegistryEntry, installedAgent, listAgents, normalizeOrigin, resolveAgentCommand, signIn, startEngine };