@lucid-agents/pi-xgate 0.1.5 → 0.1.7

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,132 @@
1
+ import { BrokerClient } from "@lucid-agents/xgate-client/broker";
2
+ import { XgateError } from "@lucid-agents/xgate-client/types";
3
+ import { readFileSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { createXgateProvider } from "./provider.js";
7
+ function privateText(path, max = 8192) {
8
+ if (!isAbsolute(path))
9
+ throw new XgateError("absolute_config_path_required");
10
+ const s = lstatSync(path);
11
+ if (!s.isFile() ||
12
+ s.isSymbolicLink() ||
13
+ (s.mode & 0o077) !== 0 ||
14
+ s.size > max)
15
+ throw new XgateError("private_config_required");
16
+ return readFileSync(path, "utf8").trim();
17
+ }
18
+ function standalone(configPath) {
19
+ const cfg = JSON.parse(privateText(configPath));
20
+ if (typeof cfg.url !== "string" || typeof cfg.tokenFile !== "string")
21
+ throw new XgateError("invalid_config");
22
+ return new BrokerClient(cfg.url, privateText(cfg.tokenFile, 128));
23
+ }
24
+ /** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
25
+ export function paymentStatus(p) {
26
+ const left = p.channel
27
+ ? `; channel ${p.channel.refundableAtomic} refundable`
28
+ : "";
29
+ return `xgate paid ${p.chargedAtomic ?? "?"}/${p.authorizedAtomic} ${p.asset} on ${p.network} (${p.scheme})${left}`;
30
+ }
31
+ /** Pi extension factory. The default export is the standalone wallet-broker extension. */
32
+ export function createXgateExtension(options = {}) {
33
+ return (pi) => register(pi, options);
34
+ }
35
+ export default createXgateExtension();
36
+ async function register(pi, options) {
37
+ const injected = options.transport;
38
+ const configPath = process.env.XGATE_CONFIG ?? join(homedir(), ".pi", "agent", "xgate.json");
39
+ let broker;
40
+ let context;
41
+ const load = async () => {
42
+ const next = injected === undefined
43
+ ? standalone(configPath)
44
+ : typeof injected === "function"
45
+ ? await injected()
46
+ : injected;
47
+ const provider = await createXgateProvider(next, {
48
+ sessionId: () => context?.sessionManager.getSessionId() ?? "standalone",
49
+ onPaid: (r) => context?.ui.setStatus("xgate", r.payment
50
+ ? paymentStatus(r.payment)
51
+ : `xgate paid ${(Number(r.settlement.chargedAtomic) / 1e6).toFixed(6)}`),
52
+ onPurchaseFailed: (f) => context?.ui.setStatus("xgate", `xgate ${f.pending ? "unresolved" : "failed"}: ${f.invocationId}`),
53
+ });
54
+ pi.registerProvider(provider);
55
+ broker = next;
56
+ };
57
+ try {
58
+ await load();
59
+ }
60
+ catch { }
61
+ pi.on("session_start", async (_event, ctx) => {
62
+ context = ctx;
63
+ ctx.ui.setStatus("xgate", broker ? "xgate wallet ready" : "xgate: /xgate refresh or /xgate setup");
64
+ });
65
+ pi.on("cache_warming_decision", async (event, ctx) => ctx.model?.provider === "xgate" ? { action: "stop" } : undefined);
66
+ pi.registerCommand("xgate", {
67
+ description: "Set up the wallet broker; show budget or reconcile an existing purchase.",
68
+ handler: async (args, ctx) => {
69
+ context = ctx;
70
+ const [command, id] = args.trim().split(/\s+/);
71
+ try {
72
+ if (command === "setup") {
73
+ if (injected !== undefined)
74
+ throw new XgateError("transport_managed_by_host");
75
+ if (!ctx.hasUI)
76
+ throw new XgateError("configure_XGATE_CONFIG_for_headless_mode");
77
+ const url = await ctx.ui.input("xgate wallet service", "http://127.0.0.1:4021");
78
+ if (!url)
79
+ return;
80
+ const tokenFile = await ctx.ui.input("Absolute path to the broker token file", "/private/path/broker-token");
81
+ if (!tokenFile)
82
+ return;
83
+ const next = new BrokerClient(url, privateText(tokenFile, 128));
84
+ await next.status();
85
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
86
+ writeFileSync(configPath, JSON.stringify({ url, tokenFile }) + "\n", {
87
+ mode: 0o600,
88
+ flag: "wx",
89
+ });
90
+ await load();
91
+ ctx.ui.notify("xgate configured. Select a qualified model with /model.", "info");
92
+ return;
93
+ }
94
+ if (command === "refresh") {
95
+ await load();
96
+ ctx.ui.notify("xgate model list refreshed.", "info");
97
+ return;
98
+ }
99
+ if (!broker)
100
+ throw new XgateError("wallet_not_configured");
101
+ if (command === "reconcile") {
102
+ if (!id)
103
+ throw new XgateError("purchase_id_required");
104
+ if (!broker.reconcile)
105
+ throw new XgateError("unsupported_by_transport");
106
+ const result = await broker.reconcile(id);
107
+ ctx.ui.notify(JSON.stringify(result), "info");
108
+ return;
109
+ }
110
+ if (command === "receipts") {
111
+ if (!broker.receipts)
112
+ throw new XgateError("unsupported_by_transport");
113
+ ctx.ui.notify(JSON.stringify(await broker.receipts(), null, 2), "info");
114
+ return;
115
+ }
116
+ if (command === "status" || command === "budget" || !command) {
117
+ if (!broker.status)
118
+ throw new XgateError("unsupported_by_transport");
119
+ const status = await broker.status();
120
+ ctx.ui.notify(JSON.stringify(status, null, 2), "info");
121
+ return;
122
+ }
123
+ ctx.ui.notify("Use /xgate setup, refresh, status, budget, receipts or reconcile <invocation ID>. Budgets are configured at the wallet service.", "info");
124
+ }
125
+ catch (error) {
126
+ ctx.ui.notify(error instanceof XgateError
127
+ ? error.message
128
+ : "xgate setup failed; check the private configuration and wallet service.", "error");
129
+ }
130
+ },
131
+ });
132
+ }