clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

Add cross-agent nzip interface

Dami committed f737b0d
commit
f737b0db8a85581ef3cf34dbc3adeabe8966c609
parent
9312043baa2d9238eca03af94418a4c846cf1a8c
author
Dami <dami.etoile@gmail.com>
17 files changed, +590 -0
Aagent/.gitignore1+
Aagent/README.md37++++++
Aagent/claude/nzip/.claude-plugin/plugin.json12++
Aagent/claude/nzip/.mcp.json16+++
Aagent/claude/nzip/server.ts1+
Aagent/claude/nzip/skills/nzip/SKILL.md6+
Aagent/codex/nzip/.codex-plugin/plugin.json19+++
Aagent/codex/nzip/.mcp.json16+++
Aagent/codex/nzip/server.ts1+
Aagent/codex/nzip/skills/nzip/SKILL.md7+
Aagent/hermes/__init__.py25++++
Aagent/hermes/plugin.yaml3+
Aagent/hermes/tools.py110+++++++++++++++++
Aagent/server.ts189++++++++++++++++++++++++++++++
Aagent/skills/nzip/SKILL.md31+++++
Aagent/skills/nzip/agents/openai.yaml4+
Aagent/tools.json112++++++++++++++++++
A agent/.gitignore +1 -0
@@ -0,0 +1 @@
1+__pycache__/
A agent/README.md +37 -0
A agent/claude/nzip/.claude-plugin/plugin.json +12 -0
A agent/claude/nzip/.mcp.json +16 -0
A agent/claude/nzip/server.ts +1 -0
A agent/claude/nzip/skills/nzip/SKILL.md +6 -0
A agent/codex/nzip/.codex-plugin/plugin.json +19 -0
A agent/codex/nzip/.mcp.json +16 -0
A agent/codex/nzip/server.ts +1 -0
A agent/codex/nzip/skills/nzip/SKILL.md +7 -0
A agent/hermes/__init__.py +25 -0
A agent/hermes/plugin.yaml +3 -0
A agent/hermes/tools.py +110 -0
@@ -0,0 +1,110 @@
1+"""CLI-backed implementations for the shared nzip tool contract."""2+3+import json4+import subprocess5+import tempfile6+from pathlib import Path7+8+ROOT = Path(__file__).resolve().parent9+if not (ROOT / "tools.json").exists():10+ ROOT = ROOT.parent11+12+13+def definitions():14+ return json.loads((ROOT / "tools.json").read_text())15+16+17+def _string(args, name, required=False):18+ value = args.get(name)19+ if value is None:20+ if required:21+ raise ValueError(f"{name} is required")22+ return None23+ if not isinstance(value, str) or not value.strip():24+ raise ValueError(f"{name} must be a non-empty string")25+ return value26+27+28+def _ttl(args):29+ value = _string(args, "ttl")30+ if value and value != "forever" and value != "0":31+ if not (value.rstrip("d").isdigit() and value.count("d") <= 1):32+ raise ValueError("ttl must be a number of days or forever")33+ return value34+35+36+def _run(command):37+ result = subprocess.run(["nzip", *command, "--json"], capture_output=True, text=True, check=False)38+ body = (result.stdout or result.stderr).strip()39+ try:40+ parsed = json.loads(body)41+ except json.JSONDecodeError as error:42+ raise RuntimeError(body or f"nzip exited with status {result.returncode}") from error43+ if result.returncode or parsed.get("ok") is False:44+ raise RuntimeError(parsed.get("error", "nzip command failed"))45+ return parsed46+47+48+def _host_html(args):49+ html = _string(args, "html", required=True)50+ target, ttl, password = _string(args, "target"), _ttl(args), _string(args, "password")51+ with tempfile.TemporaryDirectory(prefix="nzip-agent-") as directory:52+ page = Path(directory) / "index.html"53+ page.write_text(html)54+ command = ["push", str(page)]55+ if target:56+ command.append(target)57+ if ttl:58+ command.extend(["--ttl", ttl])59+ hosted = _run(command)60+ return _run(["share", hosted["address"], "--password", password]) if password else hosted61+62+63+def invoke(name, args):64+ if not isinstance(args, dict):65+ raise ValueError("arguments must be an object")66+ if name == "status":67+ return _run(["status"])68+ if name == "list_vaults":69+ return _run(["vault", "ls"])70+ if name == "list_sites":71+ vault = _string(args, "vault")72+ return _run(["ls", vault] if vault else ["ls"])73+ if name == "inspect_site":74+ return _run(["share", _string(args, "target", required=True)])75+ if name == "host_html":76+ return _host_html(args)77+ if name == "configure_site":78+ target, ttl, password = _string(args, "target", required=True), _ttl(args), args.get("password")79+ if not ttl and password is None and "password" not in args:80+ raise ValueError("configure_site requires ttl or password")81+ command = ["share", target]82+ if ttl:83+ command.extend(["--ttl", ttl])84+ if password is None and "password" in args:85+ command.append("--no-password")86+ elif isinstance(password, str) and password:87+ command.extend(["--password", password])88+ elif password is not None:89+ raise ValueError("password must be a string or null")90+ return _run(command)91+ if name == "download_site":92+ command = ["download", _string(args, "target", required=True)]93+ directory = _string(args, "directory")94+ if directory:95+ command.append(directory)96+ if args.get("overwrite") is True:97+ command.append("--overwrite")98+ return _run(command)99+ if name == "restore_site":100+ command = ["revert", _string(args, "target", required=True)]101+ if "to" in args:102+ if not isinstance(args["to"], int) or args["to"] < 1:103+ raise ValueError("to must be a positive integer")104+ command.extend(["--to", str(args["to"])])105+ return _run(command)106+ if name == "delete_site":107+ if args.get("confirm") is not True:108+ raise ValueError("delete_site requires confirm: true")109+ return _run(["rm", _string(args, "target", required=True), "--yes"])110+ raise ValueError(f"unknown nzip tool: {name}")
A agent/server.ts +189 -0
@@ -0,0 +1,189 @@
1+import definitions from "./tools.json" with { type: "json" };2+3+type Json = Record<string, unknown>;4+type Tool = { name: string; description: string; inputSchema: Json };5+const tools = definitions as Tool[];6+7+class ToolError extends Error {}8+9+function 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+}15+16+function 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+}27+28+function 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+}35+36+async 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+}61+62+async 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+}82+83+export 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+}133+134+function rpc(id: unknown, result: Json): Json {135+ return { jsonrpc: "2.0", id, result };136+}137+138+async 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+}167+168+let pending = "";169+const decoder = new TextDecoder();170+const encoder = new TextEncoder();171+for 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+}
A agent/skills/nzip/SKILL.md +31 -0
A agent/skills/nzip/agents/openai.yaml +4 -0
A agent/tools.json +112 -0
@@ -0,0 +1,112 @@
1+[2+ {3+ "name": "status",4+ "description": "Check the configured nzip server and vault overview.",5+ "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }6+ },7+ {8+ "name": "list_vaults",9+ "description": "List nzip vaults and the selected default vault.",10+ "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }11+ },12+ {13+ "name": "list_sites",14+ "description": "List hosted nzip sites, optionally in one vault.",15+ "inputSchema": {16+ "type": "object",17+ "properties": { "vault": { "type": "string", "description": "Optional vault name." } },18+ "additionalProperties": false19+ }20+ },21+ {22+ "name": "inspect_site",23+ "description": "Inspect one hosted nzip site by address or alias.",24+ "inputSchema": {25+ "type": "object",26+ "properties": {27+ "target": { "type": "string", "description": "Address, vault:alias, or bare alias." }28+ },29+ "required": ["target"],30+ "additionalProperties": false31+ }32+ },33+ {34+ "name": "host_html",35+ "description": "Host a standalone HTML document with nzip and return its canonical site identity.",36+ "inputSchema": {37+ "type": "object",38+ "properties": {39+ "html": { "type": "string", "description": "Complete non-empty HTML document." },40+ "target": { "type": "string", "description": "Optional address or alias target." },41+ "ttl": {42+ "type": "string",43+ "description": "Optional nzip TTL, such as 14d, 30d, or forever."44+ },45+ "password": { "type": "string", "description": "Optional password protection." }46+ },47+ "required": ["html"],48+ "additionalProperties": false49+ }50+ },51+ {52+ "name": "configure_site",53+ "description": "Change a site's TTL or password protection.",54+ "inputSchema": {55+ "type": "object",56+ "properties": {57+ "target": { "type": "string", "description": "Address, vault:alias, or bare alias." },58+ "ttl": { "type": "string", "description": "nzip TTL, such as 14d, 30d, or forever." },59+ "password": {60+ "type": ["string", "null"],61+ "description": "Password to set, or null to remove it."62+ }63+ },64+ "required": ["target"],65+ "additionalProperties": false66+ }67+ },68+ {69+ "name": "download_site",70+ "description": "Recover the current source bundle of a hosted site into a local directory.",71+ "inputSchema": {72+ "type": "object",73+ "properties": {74+ "target": { "type": "string", "description": "Address, vault:alias, or bare alias." },75+ "directory": { "type": "string", "description": "Optional destination directory." },76+ "overwrite": { "type": "boolean", "description": "Allow an existing destination." }77+ },78+ "required": ["target"],79+ "additionalProperties": false80+ }81+ },82+ {83+ "name": "restore_site",84+ "description": "Restore a site to a prior nzip push.",85+ "inputSchema": {86+ "type": "object",87+ "properties": {88+ "target": { "type": "string", "description": "Address, vault:alias, or bare alias." },89+ "to": {90+ "type": "integer",91+ "minimum": 1,92+ "description": "Optional push sequence to restore."93+ }94+ },95+ "required": ["target"],96+ "additionalProperties": false97+ }98+ },99+ {100+ "name": "delete_site",101+ "description": "Permanently delete a hosted site after explicit confirmation.",102+ "inputSchema": {103+ "type": "object",104+ "properties": {105+ "target": { "type": "string", "description": "Address, vault:alias, or bare alias." },106+ "confirm": { "type": "boolean", "description": "Must be true to delete." }107+ },108+ "required": ["target", "confirm"],109+ "additionalProperties": false110+ }111+ }112+]