@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/README.md +1 -1
- package/dist/{chunk-BXSAYWR3.js → chunk-JOCM56MT.js} +569 -45
- package/dist/cli.js +64 -8
- package/dist/index.d.ts +255 -122
- package/dist/index.js +1 -1
- package/package.json +9 -2
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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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
|
|
548
|
-
|
|
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]] :
|
|
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
|
-
/**
|
|
70
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
798
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leuria/cli",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
"
|
|
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",
|