1import definitions from "./tools.json" with { type: "json" };23type Json = Record<string, unknown>;4type Tool = { name: string; description: string; inputSchema: Json };5const tools = definitions as Tool[];67class ToolError extends Error {}89function asObject(value: unknown): Json {10 if (!value || typeof value !== "object" || Array.isArray(value)) {11 throw new ToolError("arguments must be an object");12 }13 return value as Json;14}1516function stringArg(args: Json, name: string, required = false): string | undefined {17 const value = args[name];18 if (value === undefined) {19 if (required) throw new ToolError(name + " is required");20 return undefined;21 }22 if (typeof value !== "string" || value.trim() === "") {23 throw new ToolError(name + " must be a non-empty string");24 }25 return value;26}2728function ttlArg(args: Json): string | undefined {29 const ttl = stringArg(args, "ttl");30 if (ttl && !/^(?:\d+d?|forever|0)$/.test(ttl)) {31 throw new ToolError("ttl must be a number of days or forever");32 }33 return ttl;34}3536async function runNzip(args: string[]): Promise<Json> {37 let result: Deno.CommandOutput;38 try {39 result = await new Deno.Command("nzip", {40 args: [...args, "--json"],41 stdout: "piped",42 stderr: "piped",43 }).output();44 } catch (error) {45 throw new ToolError("could not run nzip: " + (error as Error).message);46 }47 const stdout = new TextDecoder().decode(result.stdout).trim();48 const stderr = new TextDecoder().decode(result.stderr).trim();49 const body = stdout || stderr;50 try {51 const parsed = JSON.parse(body) as Json;52 if (!result.success || parsed.ok === false) {53 throw new ToolError(String(parsed.error ?? "nzip command failed"));54 }55 return parsed;56 } catch (error) {57 if (error instanceof ToolError) throw error;58 throw new ToolError(body || "nzip exited with status " + result.code);59 }60}6162async function hostHtml(args: Json): Promise<Json> {63 const html = stringArg(args, "html", true)!;64 const target = stringArg(args, "target");65 const ttl = ttlArg(args);66 const password = stringArg(args, "password");67 const dir = await Deno.makeTempDir({ prefix: "nzip-agent-" });68 try {69 await Deno.writeTextFile(dir + "/index.html", html);70 const command = ["push", dir + "/index.html"];71 if (target) command.push(target);72 if (ttl) command.push("--ttl", ttl);73 const hosted = await runNzip(command);74 if (!password) return hosted;75 const address = String(hosted.address ?? "");76 if (!address) throw new ToolError("nzip push did not return an address");77 return await runNzip(["share", address, "--password", password]);78 } finally {79 await Deno.remove(dir, { recursive: true }).catch(() => {});80 }81}8283export async function invoke(name: string, input: unknown): Promise<Json> {84 const args = asObject(input);85 if (name === "status") return await runNzip(["status"]);86 if (name === "list_vaults") return await runNzip(["vault", "ls"]);87 if (name === "list_sites") {88 const vault = stringArg(args, "vault");89 return await runNzip(vault ? ["ls", vault] : ["ls"]);90 }91 if (name === "inspect_site") return await runNzip(["share", stringArg(args, "target", true)!]);92 if (name === "host_html") return await hostHtml(args);93 if (name === "configure_site") {94 const target = stringArg(args, "target", true)!;95 const ttl = ttlArg(args);96 const password = args.password;97 if (!ttl && password === undefined) {98 throw new ToolError("configure_site requires ttl or password");99 }100 const command = ["share", target];101 if (ttl) command.push("--ttl", ttl);102 if (password === null) command.push("--no-password");103 else if (typeof password === "string" && password) command.push("--password", password);104 else if (password !== undefined) throw new ToolError("password must be a string or null");105 return await runNzip(command);106 }107 if (name === "download_site") {108 const command = ["download", stringArg(args, "target", true)!];109 const directory = stringArg(args, "directory");110 if (directory) command.push(directory);111 if (args.overwrite === true) command.push("--overwrite");112 else if (args.overwrite !== undefined && typeof args.overwrite !== "boolean") {113 throw new ToolError("overwrite must be a boolean");114 }115 return await runNzip(command);116 }117 if (name === "restore_site") {118 const command = ["revert", stringArg(args, "target", true)!];119 if (args.to !== undefined) {120 if (!Number.isInteger(args.to) || (args.to as number) < 1) {121 throw new ToolError("to must be a positive integer");122 }123 command.push("--to", String(args.to));124 }125 return await runNzip(command);126 }127 if (name === "delete_site") {128 if (args.confirm !== true) throw new ToolError("delete_site requires confirm: true");129 return await runNzip(["rm", stringArg(args, "target", true)!, "--yes"]);130 }131 throw new ToolError("unknown nzip tool: " + name);132}133134function rpc(id: unknown, result: Json): Json {135 return { jsonrpc: "2.0", id, result };136}137138async function handle(request: Json): Promise<Json | null> {139 const id = request.id;140 if (request.method === "initialize") {141 return rpc(id, {142 protocolVersion: "2024-11-05",143 capabilities: { tools: {} },144 serverInfo: { name: "nzip", version: "0.1.0" },145 });146 }147 if (request.method === "notifications/initialized") return null;148 if (request.method === "tools/list") return rpc(id, { tools });149 if (request.method === "tools/call") {150 const params = asObject(request.params);151 try {152 const result = await invoke(stringArg(params, "name", true)!, params.arguments ?? {});153 return rpc(id, { content: [{ type: "text", text: JSON.stringify(result) }] });154 } catch (error) {155 return rpc(id, {156 content: [{157 type: "text",158 text: JSON.stringify({ ok: false, error: (error as Error).message }),159 }],160 isError: true,161 });162 }163 }164 if (id === undefined) return null;165 return { jsonrpc: "2.0", id, error: { code: -32601, message: "method not found" } };166}167168let pending = "";169const decoder = new TextDecoder();170const encoder = new TextEncoder();171for await (const chunk of Deno.stdin.readable) {172 pending += decoder.decode(chunk, { stream: true });173 const lines = pending.split(/\r?\n/);174 pending = lines.pop() ?? "";175 for (const line of lines) {176 if (!line.trim()) continue;177 try {178 const result = await handle(JSON.parse(line) as Json);179 if (result) await Deno.stdout.write(encoder.encode(JSON.stringify(result) + "\n"));180 } catch (error) {181 const failure = {182 jsonrpc: "2.0",183 id: null,184 error: { code: -32603, message: (error as Error).message },185 };186 await Deno.stdout.write(encoder.encode(JSON.stringify(failure) + "\n"));187 }188 }189}