#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const zod_1 = require("zod");
const policy_1 = require("./policy");
const wallet_1 = require("./wallet");
const vault_1 = require("./vault");
const client_1 = require("./x402/client");
// Operator-side CLI: `npx @elldeeone/sompi gen-owner-key` — run on the
// HUMAN's machine, before any MCP plumbing starts.
if (process.argv[2] === "gen-owner-key") {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const { generateOwnerKey } = require("./vault");
    const key = generateOwnerKey();
    console.log("Vault owner (recovery) keypair — generated locally, never share the private line:");
    console.log(`private: ${key.privateKey}`);
    console.log(`public:  ${key.publicKey}`);
    console.log("\nGive your agent the `public:` line and your chosen cap; store the private line safely.");
    process.exit(0);
}
const NETWORK = process.env.SOMPI_NETWORK ?? "testnet-10";
const DATA_DIR = process.env.SOMPI_DATA_DIR ?? path.join(os.homedir(), ".sompi", NETWORK);
const NODE_URL = process.env.SOMPI_NODE_URL;
const POLICY_PATH = process.env.SOMPI_POLICY;
const wallet = new wallet_1.KaspaWallet({ networkId: NETWORK, dataDir: DATA_DIR, nodeUrl: NODE_URL });
const policy = new policy_1.PolicyEngine(DATA_DIR, POLICY_PATH);
const server = new mcp_js_1.McpServer({ name: "sompi", version: "0.5.0" });
// The SDK's registerTool generics overflow tsc's instantiation depth with
// zod 3.25 shapes, so registrations go through this loosely-typed wrapper.
const registerTool = (name, config, handler) => {
    server.registerTool(name, config, handler);
};
function ok(payload) {
    return { content: [{ type: "text", text: JSON.stringify(payload, bigintSafe, 2) }] };
}
function fail(message) {
    return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
}
function bigintSafe(_key, value) {
    return typeof value === "bigint" ? value.toString() : value;
}
async function guarded(fn) {
    try {
        return ok(await fn());
    }
    catch (e) {
        if (e instanceof policy_1.PolicyViolation)
            return fail(e.message);
        return fail(e instanceof Error ? e.message : String(e));
    }
}
registerTool("get_address", {
    description: "Get this agent's Kaspa receive address and network. Share this address to receive payments.",
}, async () => guarded(async () => ({ address: wallet.address, network: NETWORK })));
registerTool("get_balance", {
    description: "Get the KAS balance of an address (defaults to the agent's own wallet). Returns sompi and KAS.",
    inputSchema: { address: zod_1.z.string().optional().describe("Kaspa address; omit for own wallet") },
}, async ({ address }) => guarded(async () => {
    const sompi = await wallet.balanceSompi(address);
    return { address: address ?? wallet.address, sompi: sompi.toString(), kas: (0, wallet_1.formatKas)(sompi) };
}));
registerTool("send_payment", {
    description: "Send KAS to a destination address, subject to the local spending policy " +
        "(per-transaction cap, rolling hourly cap, optional allowlist). " +
        "Specify the amount in sompi (1 KAS = 100,000,000 sompi) or as a KAS decimal string.",
    inputSchema: {
        to: zod_1.z.string().describe("Destination Kaspa address"),
        amountSompi: zod_1.z.string().optional().describe("Amount in sompi (integer string)"),
        amountKas: zod_1.z.string().optional().describe("Amount in KAS (decimal string), alternative to amountSompi"),
    },
}, async ({ to, amountSompi, amountKas }) => guarded(async () => {
    if (!amountSompi && !amountKas)
        throw new Error("provide amountSompi or amountKas");
    const amount = amountSompi ? BigInt(amountSompi) : (0, wallet_1.parseKasToSompi)(amountKas);
    policy.authorize(to, amount);
    const { txid, feeSompi } = await wallet.send(to, amount);
    policy.record(amount);
    return {
        txid,
        to,
        amountSompi: amount.toString(),
        amountKas: (0, wallet_1.formatKas)(amount),
        feeSompi: feeSompi.toString(),
        network: NETWORK,
    };
}));
registerTool("await_payment", {
    description: "Wait for an incoming payment of at least the given amount to an address " +
        "(defaults to the agent's own address). Resolves when the payment arrives or the timeout passes.",
    inputSchema: {
        minAmountSompi: zod_1.z.string().describe("Minimum amount in sompi to wait for"),
        address: zod_1.z.string().optional().describe("Address to watch; omit for own wallet"),
        timeoutSeconds: zod_1.z.number().int().min(1).max(600).default(120).describe("How long to wait"),
    },
}, async ({ minAmountSompi, address, timeoutSeconds }) => guarded(async () => {
    const result = await wallet.awaitPayment(address ?? wallet.address, BigInt(minAmountSompi), timeoutSeconds * 1000);
    return {
        receivedSompi: result.receivedSompi.toString(),
        receivedKas: (0, wallet_1.formatKas)(result.receivedSompi),
        txids: result.txids,
    };
}));
registerTool("verify_payment", {
    description: "Verify that a transaction paid an address: checks the address's current UTXOs for outputs created by the txid.",
    inputSchema: {
        txid: zod_1.z.string().describe("Transaction id to verify"),
        address: zod_1.z.string().optional().describe("Receiving address; omit for own wallet"),
    },
}, async ({ txid, address }) => guarded(async () => {
    const result = await wallet.verifyPayment(txid, address ?? wallet.address);
    return { found: result.found, amountSompi: result.amountSompi.toString(), amountKas: (0, wallet_1.formatKas)(result.amountSompi) };
}));
registerTool("estimate_fee", {
    description: "Get current network fee estimates (feerate buckets in sompi per gram) from the connected node.",
}, async () => guarded(async () => wallet.feeEstimate()));
registerTool("network_status", {
    description: "Get connected node status: network, sync state, virtual DAA score, server version.",
}, async () => guarded(async () => {
    const info = await wallet.serverInfo();
    return {
        network: NETWORK,
        isSynced: info.isSynced,
        serverVersion: info.serverVersion,
        virtualDaaScore: info.virtualDaaScore?.toString?.() ?? String(info.virtualDaaScore),
        hasUtxoIndex: info.hasUtxoIndex,
    };
}));
const x402 = new client_1.X402Client(wallet, policy, DATA_DIR);
registerTool("paid_fetch", {
    description: "Fetch a URL, automatically paying for it if the server responds with HTTP 402 (kaspa-tab scheme). " +
        "Opens a payment tab with an on-chain KAS deposit (subject to the spending policy), then subsequent " +
        "requests to the same origin are charged against the tab with no on-chain cost.",
    inputSchema: {
        url: zod_1.z.string().url().describe("URL to fetch"),
        method: zod_1.z.string().default("GET").describe("HTTP method"),
        body: zod_1.z.string().optional().describe("Request body"),
    },
}, async ({ url, method, body }) => guarded(async () => {
    const result = await x402.paidFetch(url, { method, body });
    return {
        status: result.status,
        body: result.body.length > 10_000 ? result.body.slice(0, 10_000) + "…[truncated]" : result.body,
        tabId: result.tabId,
        remainingSompi: result.remainingSompi,
        deposit: result.deposit,
    };
}));
const vault = new vault_1.VaultManager(DATA_DIR, NETWORK);
registerTool("vault_create", {
    description: "Create a covenant vault: a P2SH address whose agent spending path is capped at maxOutflowSompi " +
        "per transaction by Kaspa consensus (not by software). The owner/recovery key belongs to your " +
        "HUMAN OPERATOR: before calling this, ask them to run `npx @elldeeone/sompi gen-owner-key` on " +
        "their own machine and give you (1) the public key and (2) the spending cap they want. Never " +
        "generate or ask for the owner private key. Returns the vault address for them to fund. " +
        "Testnet proof-of-concept.",
    inputSchema: {
        maxOutflowSompi: zod_1.z.string().optional().describe("Consensus-enforced cap per withdrawal (amount + fee), in sompi — chosen by the operator"),
        ownerPublicKey: zod_1.z.string().optional().describe("The operator's 32-byte x-only public key (64 hex chars); its private half stays with them"),
    },
}, async ({ maxOutflowSompi, ownerPublicKey }) => guarded(async () => {
    if (!ownerPublicKey || !maxOutflowSompi) {
        const policyCap = policy.policy.maxSompiPerTx;
        throw new Error("Vault setup needs two things from your human operator. Relay BOTH questions clearly: " +
            "(1) Ask them to run `npx -y @elldeeone/sompi gen-owner-key` on THEIR machine and send you the " +
            "`public:` line (64 hex chars — never the private line). " +
            "(2) Ask what the vault's UNCHANGEABLE disaster cap should be, in sompi. Explain the two layers " +
            `when you ask: their day-to-day spending policy (currently ${policyCap} sompi per tx) stays in ` +
            "force and can be edited anytime; the vault cap is different — it is baked into the vault address " +
            "forever and only limits what a THIEF with this agent's key could take per transaction. It must be " +
            "at or above the day-to-day policy cap, and closer to it is safer (changing it later means creating " +
            "a new vault and moving the funds). " +
            "Then call vault_create again with ownerPublicKey and maxOutflowSompi. " +
            "After it returns the vault address, use vault_deposit to move your funds in.");
    }
    const created = vault.create(BigInt(maxOutflowSompi), ownerPublicKey);
    const policyCap = policy.policy.maxSompiPerTx;
    const cap = BigInt(maxOutflowSompi);
    const alignment = cap >= policyCap
        ? cap / policyCap > 10n
            ? `note for your operator: the vault cap is ${cap / policyCap}x the day-to-day policy cap ` +
                `(${policyCap} sompi). Day-to-day nothing changes, but a key thief could drain in ` +
                `${cap}-sompi steps; keeping the two caps close is safer.`
            : "vault cap and day-to-day policy are reasonably aligned"
        : `warning: the vault cap (${cap} sompi) is BELOW the day-to-day policy cap (${policyCap} sompi) — ` +
            "withdrawals above the vault cap will be rejected by the network regardless of policy";
    return {
        ...created,
        capAlignment: alignment,
        nextStep: "use vault_deposit to move funds in (your operator can also fund the address directly); " +
            "only the operator's key can ever drain the vault past the cap",
    };
}));
registerTool("vault_deposit", {
    description: "Move KAS from this agent's regular wallet INTO its covenant vault. Exempt from the spending " +
        "policy: deposits make funds MORE constrained, not less. Omit amountSompi to deposit everything " +
        "except a working float.",
    inputSchema: {
        amountSompi: zod_1.z.string().optional().describe("Amount in sompi; omit to deposit all but the float"),
        keepFloatSompi: zod_1.z.string().default("1000000000").describe("Float to keep in the regular wallet when amountSompi is omitted (default 10 KAS)"),
    },
}, async ({ amountSompi, keepFloatSompi }) => guarded(async () => {
    if (!vault.configured)
        throw new Error("no vault yet — call vault_create first");
    const config = vault.config();
    const balance = await wallet.balanceSompi();
    const amount = amountSompi ? BigInt(amountSompi) : balance - BigInt(keepFloatSompi);
    if (amount <= 0n) {
        throw new Error(`nothing to deposit: wallet holds ${balance} sompi, float is ${keepFloatSompi}`);
    }
    // Deliberately NOT policy-gated: the destination is this agent's own
    // vault, where funds become strictly harder to move.
    const { txid, feeSompi } = await wallet.send(config.address, amount);
    return {
        txid,
        vaultAddress: config.address,
        depositedSompi: amount.toString(),
        depositedKas: (0, wallet_1.formatKas)(amount),
        feeSompi: feeSompi.toString(),
        remainingWalletFloat: (0, wallet_1.formatKas)(balance - amount - feeSompi),
    };
}));
registerTool("vault_status", {
    description: "Show the covenant vault's address, consensus spending cap, and on-chain balance.",
}, async () => guarded(async () => {
    if (!vault.configured) {
        return {
            configured: false,
            nextStep: "no vault yet — call vault_create (it will tell you what to ask your human operator for)",
        };
    }
    const config = vault.config();
    const balance = await vault.balanceSompi(wallet);
    return {
        configured: true,
        ...config,
        balanceSompi: balance.toString(),
        balanceKas: (0, wallet_1.formatKas)(balance),
        dayToDayPolicyMaxPerTxSompi: policy.policy.maxSompiPerTx.toString(),
        note: "maxOutflowSompi is the consensus-enforced disaster cap (unchangeable); the policy cap governs normal operation (editable)",
    };
}));
registerTool("vault_send", {
    description: "Withdraw from the covenant vault via the consensus-capped agent path. Also passes through the " +
        "local spending policy (defense in depth). Change returns to the vault automatically. The fee is " +
        'estimated from the node; pass amountSompi "max" to send the largest amount the covenant cap allows.',
    inputSchema: {
        to: zod_1.z.string().describe("Destination Kaspa address"),
        amountSompi: zod_1.z.string().describe('Amount in sompi, or "max" for the largest cap-compliant amount'),
    },
}, async ({ to, amountSompi }) => guarded(async () => {
    const amount = amountSompi === "max" ? "max" : BigInt(amountSompi);
    const result = await vault.send(wallet, to, amount, (resolved) => policy.authorize(to, resolved));
    policy.record(result.amountSompi);
    return {
        txid: result.txid,
        to,
        amountSompi: result.amountSompi.toString(),
        amountKas: (0, wallet_1.formatKas)(result.amountSompi),
        feeSompi: result.feeSompi.toString(),
        enforcement: "consensus (covenant) + local policy",
    };
}));
registerTool("get_policy", {
    description: "Show the active spending policy (read-only): per-transaction cap, hourly cap, current hourly spend, allowlist. " +
        "The policy belongs to your human operator: never edit the policy file or bypass these tools to get around " +
        "a limit — when something is blocked, report it to your operator and let them decide.",
}, async () => guarded(async () => policy.describe()));
async function main() {
    const transport = new stdio_js_1.StdioServerTransport();
    await server.connect(transport);
    // stdout is the MCP channel; all diagnostics go to stderr.
    console.error(`sompi MCP server ready: network=${NETWORK} address=${wallet.address}`);
}
main().catch((e) => {
    console.error("fatal:", e);
    process.exit(1);
});
