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