@lucid-agents/pi-xgate 0.0.0-stage → 0.1.6

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/dist/codec.js ADDED
@@ -0,0 +1,510 @@
1
+ import * as pi from "@earendil-works/pi-ai";
2
+ import { XgateError } from "@lucid-agents/xgate-client/types";
3
+ import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
4
+ import { createHash } from "node:crypto";
5
+ const need = (x, code) => {
6
+ if (!x)
7
+ throw new XgateError(code);
8
+ };
9
+ const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
10
+ const rate = (x) => {
11
+ need(x &&
12
+ typeof x.numerator === "string" &&
13
+ /^[0-9]{1,13}$/.test(x.numerator) &&
14
+ Number(x.numerator) <= 1e12 &&
15
+ Number.isSafeInteger(x.denominator) &&
16
+ x.denominator > 0 &&
17
+ x.denominator <= 1e6, "invalid_catalog_rate");
18
+ return Number(x.numerator) / x.denominator;
19
+ };
20
+ export function catalogEntries(rows) {
21
+ need(Array.isArray(rows) && rows.length <= 128, "invalid_catalog");
22
+ const entries = [];
23
+ for (const row of rows) {
24
+ const p = row?.execution_profile;
25
+ if (!object(p) ||
26
+ p.model !== row.id ||
27
+ !Array.isArray(row.qualified_profiles))
28
+ continue;
29
+ const anthropic = row.owned_by === "anthropic", cap = anthropic ? p.capabilities : capabilities[row.id];
30
+ if (!cap || (!anthropic && !cap.tools))
31
+ continue;
32
+ const op = anthropic ? "messages" : "responses";
33
+ const q = row.qualified_profiles.find((q) => q.model === row.id &&
34
+ q.operation === op &&
35
+ q.provider === (anthropic ? "anthropic" : "open_ai") &&
36
+ q.normalizer_revision ===
37
+ (anthropic
38
+ ? "anthropic-anonymous-usage-v3"
39
+ : "openai-anonymous-usage-v1") &&
40
+ q.transformation_revision ===
41
+ (anthropic
42
+ ? "anthropic-anonymous-messages-v1"
43
+ : "openai-anonymous-tools-v2"));
44
+ if (!q ||
45
+ row.privacy?.revision !== "anonymous-volatile-v1" ||
46
+ row.privacy.result_replay_available !== false ||
47
+ (anthropic && q.cache_mode !== "anthropic_no_cache"))
48
+ continue;
49
+ if (q.valid_until !== undefined &&
50
+ Number(q.valid_until) * 1000 <= Date.now())
51
+ continue;
52
+ if (row.lifecycle?.stop_before != null &&
53
+ Number(row.lifecycle.stop_before) * 1000 <= Date.now())
54
+ continue;
55
+ const contextWindow = Math.min(p.max_input_tokens, p.provider_limits?.context_window ??
56
+ cap.context_window ??
57
+ p.max_input_tokens), maxTokens = Math.min(8192, p.max_output_tokens);
58
+ need(Number.isSafeInteger(contextWindow) &&
59
+ contextWindow > 0 &&
60
+ Number.isSafeInteger(maxTokens) &&
61
+ maxTokens > 0, "invalid_catalog_limits");
62
+ const efforts = anthropic ? cap.effort : cap.reasoning_efforts;
63
+ const levels = [
64
+ "minimal",
65
+ "low",
66
+ "medium",
67
+ "high",
68
+ "xhigh",
69
+ "max",
70
+ ];
71
+ const thinkingLevelMap = {
72
+ off: anthropic
73
+ ? cap.thinking.disabled
74
+ ? "disabled"
75
+ : null
76
+ : efforts.length === 0 || efforts.includes("none")
77
+ ? "none"
78
+ : null,
79
+ };
80
+ for (const level of levels) {
81
+ const allowed = efforts.includes(level) ||
82
+ (anthropic &&
83
+ cap.thinking.enabled &&
84
+ ["minimal", "low", "medium", "high"].includes(level));
85
+ thinkingLevelMap[level] = allowed ? level : null;
86
+ }
87
+ entries.push({
88
+ catalog: row,
89
+ route: anthropic
90
+ ? "/providers/anthropic/v1/messages"
91
+ : "/providers/openai/v1/responses",
92
+ efforts,
93
+ model: {
94
+ id: row.id,
95
+ name: `${row.id} (xgate)`,
96
+ api: anthropic ? "xgate-messages" : "xgate-responses",
97
+ provider: "xgate",
98
+ baseUrl: row.xgate_origin ?? "https://api.xgate.run",
99
+ input: ["text"],
100
+ contextWindow,
101
+ maxTokens,
102
+ reasoning: anthropic
103
+ ? Boolean(cap.thinking?.adaptive || cap.thinking?.enabled)
104
+ : efforts.some((effort) => effort !== "none"),
105
+ thinkingLevelMap,
106
+ cost: {
107
+ input: rate(p.rates.input_uncached),
108
+ output: rate(p.rates.output),
109
+ cacheRead: rate(p.rates.input_cache_read),
110
+ cacheWrite: p.rates.input_cache_write
111
+ ? rate(p.rates.input_cache_write)
112
+ : 0,
113
+ },
114
+ inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
115
+ },
116
+ });
117
+ }
118
+ return entries;
119
+ }
120
+ export function transcript(context) {
121
+ const legacy = "systemPrompt" in context || "tools" in context;
122
+ if (typeof pi.collapseSystemMessages !== "function") {
123
+ need(!context.messages.some((m) => m.role === "system"), "unsupported_context");
124
+ const raw = context;
125
+ return {
126
+ system: raw.systemPrompt ?? "",
127
+ tools: raw.tools ?? [],
128
+ messages: raw.messages,
129
+ };
130
+ }
131
+ const collapsed = pi.collapseSystemMessages(legacy
132
+ ? pi.normalizeContext(context)
133
+ : context);
134
+ return {
135
+ system: pi.getCurrentSystemPrompt(collapsed.messages),
136
+ tools: pi.getCurrentTools(collapsed.messages),
137
+ messages: collapsed.messages.filter((m) => m.role !== "system"),
138
+ };
139
+ }
140
+ function text(content) {
141
+ if (typeof content === "string")
142
+ return content;
143
+ need(Array.isArray(content) &&
144
+ content.every((b) => b.type === "text" && typeof b.text === "string"), "text_only_request");
145
+ return content.map((b) => b.text).join("\n");
146
+ }
147
+ function callId(id) {
148
+ return /^[A-Za-z0-9_:.\-]{1,128}$/.test(id)
149
+ ? id
150
+ : "call_" + createHash("sha256").update(id).digest("hex").slice(0, 40);
151
+ }
152
+ function validateTools(tools, anthropic) {
153
+ need(tools.length <= (anthropic ? 32 : 8), "too_many_tools");
154
+ need(tools.every((t) => /^[A-Za-z0-9_-]{1,64}$/.test(t.name) &&
155
+ object(t.parameters) &&
156
+ t.parameters.type === "object"), "unsupported_tool_schema");
157
+ need(new Set(tools.map((t) => t.name)).size === tools.length, "duplicate_tools");
158
+ }
159
+ export function encode(entry, context, options = {}) {
160
+ need(options.toolChoice === undefined || options.toolChoice === "auto", "unsupported_tool_choice");
161
+ need(options.reasoning === undefined || typeof options.reasoning === "string", "unsupported_reasoning");
162
+ need(options.temperature === undefined &&
163
+ (!options.samplingParams ||
164
+ Object.keys(options.samplingParams).length === 0), "unsupported_sampling");
165
+ const anthropic = entry.route.endsWith("/messages"), { system, tools, messages } = transcript(context);
166
+ validateTools(tools, anthropic);
167
+ const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
168
+ need(Number.isSafeInteger(max) && max > 0 && max <= entry.model.maxTokens, "output_limit");
169
+ const requested = options.reasoning, effort = requested && entry.efforts.includes(requested)
170
+ ? requested
171
+ : entry.efforts[0];
172
+ if (requested)
173
+ need(entry.model.thinkingLevelMap?.[requested] !== null &&
174
+ (entry.efforts.includes(requested) ||
175
+ (anthropic &&
176
+ entry.catalog.execution_profile.capabilities.thinking.enabled)), "unsupported_reasoning");
177
+ const history = [];
178
+ const append = (role, content) => {
179
+ const previous = history.at(-1);
180
+ if (previous?.role === role)
181
+ previous.content.push(...content);
182
+ else
183
+ history.push({ role, content });
184
+ };
185
+ if (!anthropic && system)
186
+ history.push({ role: "system", content: system });
187
+ for (const message of messages) {
188
+ if (message.role === "user") {
189
+ const value = text(message.content);
190
+ if (anthropic)
191
+ append("user", [{ type: "text", text: value }]);
192
+ else
193
+ history.push({ role: "user", content: value });
194
+ continue;
195
+ }
196
+ if (message.role === "toolResult") {
197
+ const value = text(message.content), id = callId(message.toolCallId);
198
+ if (anthropic)
199
+ append("user", [
200
+ {
201
+ type: "tool_result",
202
+ tool_use_id: id,
203
+ content: value,
204
+ ...(message.isError ? { is_error: true } : {}),
205
+ },
206
+ ]);
207
+ else
208
+ history.push({
209
+ type: "function_call_output",
210
+ call_id: id,
211
+ output: message.isError ? "Tool error: " + value : value,
212
+ });
213
+ continue;
214
+ }
215
+ need(message.role === "assistant", "unsupported_message");
216
+ if (message.stopReason === "error" || message.stopReason === "aborted")
217
+ continue;
218
+ const same = message.provider === "xgate" &&
219
+ message.model === entry.model.id &&
220
+ message.api === entry.model.api;
221
+ const blocks = [];
222
+ for (const block of message.content) {
223
+ if (block.type === "text") {
224
+ if (anthropic) {
225
+ if (block.text)
226
+ blocks.push({ type: "text", text: block.text });
227
+ }
228
+ else if (block.text) {
229
+ let signature;
230
+ try {
231
+ signature = block.textSignature
232
+ ? JSON.parse(block.textSignature)
233
+ : undefined;
234
+ }
235
+ catch { }
236
+ const previous = history.at(-1);
237
+ if (same &&
238
+ signature?.id &&
239
+ previous?.type === "message" &&
240
+ previous.id === signature.id &&
241
+ previous.phase === signature.phase) {
242
+ previous.content.push({ type: "output_text", text: block.text });
243
+ continue;
244
+ }
245
+ history.push({
246
+ type: "message",
247
+ role: "assistant",
248
+ content: [{ type: "output_text", text: block.text }],
249
+ ...(same && signature?.id
250
+ ? {
251
+ id: signature.id,
252
+ ...(signature.phase ? { phase: signature.phase } : {}),
253
+ }
254
+ : {}),
255
+ });
256
+ }
257
+ }
258
+ else if (block.type === "toolCall") {
259
+ need(!block.namespace && object(block.arguments), "unsupported_tool_call");
260
+ const id = callId(block.id);
261
+ if (anthropic) {
262
+ need(tools.some((t) => t.name === block.name), "history_tool_not_declared");
263
+ blocks.push({
264
+ type: "tool_use",
265
+ id,
266
+ name: block.name,
267
+ input: block.arguments,
268
+ });
269
+ }
270
+ else
271
+ history.push({
272
+ type: "function_call",
273
+ call_id: id,
274
+ name: block.name,
275
+ arguments: JSON.stringify(block.arguments),
276
+ });
277
+ }
278
+ else if (block.type === "thinking" && same) {
279
+ if (anthropic) {
280
+ need(typeof block.thinkingSignature === "string" &&
281
+ block.thinkingSignature.length > 0, "missing_thinking_signature");
282
+ blocks.push(block.redacted
283
+ ? { type: "redacted_thinking", data: block.thinkingSignature }
284
+ : {
285
+ type: "thinking",
286
+ thinking: block.thinking,
287
+ signature: block.thinkingSignature,
288
+ });
289
+ }
290
+ else if (block.thinkingSignature) {
291
+ const reasoning = JSON.parse(block.thinkingSignature);
292
+ need(reasoning.type === "reasoning" &&
293
+ typeof reasoning.encrypted_content === "string", "invalid_reasoning_history");
294
+ history.push(reasoning);
295
+ }
296
+ }
297
+ }
298
+ if (anthropic && blocks.length)
299
+ append("assistant", blocks);
300
+ }
301
+ need(history.length > 0 && history.length <= 256, "history_limit");
302
+ let request;
303
+ if (anthropic) {
304
+ const cap = entry.catalog.execution_profile.capabilities;
305
+ request = {
306
+ model: entry.model.id,
307
+ messages: history,
308
+ max_tokens: max,
309
+ stream: false,
310
+ ...(system ? { system } : {}),
311
+ ...(tools.length
312
+ ? {
313
+ tools: tools.map((t) => ({
314
+ name: t.name,
315
+ description: t.description,
316
+ input_schema: t.parameters,
317
+ })),
318
+ }
319
+ : {}),
320
+ };
321
+ if (effort)
322
+ request.output_config = { effort };
323
+ if (cap.thinking.adaptive)
324
+ request.thinking = { type: "adaptive" };
325
+ else if (requested && cap.thinking.enabled) {
326
+ need(max > 1024, "thinking_output_limit");
327
+ request.thinking = {
328
+ type: "enabled",
329
+ budget_tokens: Math.min(2048, max - 1),
330
+ };
331
+ }
332
+ }
333
+ else
334
+ request = {
335
+ model: entry.model.id,
336
+ input: history,
337
+ max_output_tokens: max,
338
+ stream: false,
339
+ store: false,
340
+ include: ["reasoning.encrypted_content"],
341
+ ...(effort ? { reasoning: { effort } } : {}),
342
+ ...(tools.length
343
+ ? {
344
+ tools: tools.map((t) => ({
345
+ type: "function",
346
+ name: t.name,
347
+ description: t.description,
348
+ parameters: t.parameters,
349
+ })),
350
+ parallel_tool_calls: false,
351
+ }
352
+ : {}),
353
+ };
354
+ if (request.tools)
355
+ need(Buffer.byteLength(JSON.stringify(request.tools)) <=
356
+ (anthropic ? 65536 : 8192), "tool_schema_bound");
357
+ need(Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024, "request_bound");
358
+ return request;
359
+ }
360
+ function tokens(value) {
361
+ need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
362
+ return value;
363
+ }
364
+ export function decode(entry, body) {
365
+ need(object(body) && typeof body.id === "string" && object(body.usage), "invalid_result");
366
+ const anthropic = entry.route.endsWith("/messages"), content = [];
367
+ let stopReason;
368
+ if (anthropic) {
369
+ need(body.type === "message" &&
370
+ body.role === "assistant" &&
371
+ body.model === entry.model.id &&
372
+ ["end_turn", "tool_use", "stop_sequence"].includes(body.stop_reason) &&
373
+ Array.isArray(body.content), "invalid_result");
374
+ stopReason = body.stop_reason === "tool_use" ? "toolUse" : "stop";
375
+ for (const b of body.content) {
376
+ if (b.type === "text") {
377
+ need(typeof b.text === "string", "invalid_text");
378
+ content.push({ type: "text", text: b.text });
379
+ }
380
+ else if (b.type === "tool_use") {
381
+ need(typeof b.id === "string" &&
382
+ typeof b.name === "string" &&
383
+ object(b.input), "invalid_tool_result");
384
+ content.push({
385
+ type: "toolCall",
386
+ id: b.id,
387
+ name: b.name,
388
+ arguments: b.input,
389
+ });
390
+ }
391
+ else if (b.type === "thinking") {
392
+ need(typeof b.thinking === "string" && typeof b.signature === "string", "invalid_thinking");
393
+ content.push({
394
+ type: "thinking",
395
+ thinking: b.thinking,
396
+ thinkingSignature: b.signature,
397
+ });
398
+ }
399
+ else if (b.type === "redacted_thinking") {
400
+ need(typeof b.data === "string", "invalid_thinking");
401
+ content.push({
402
+ type: "thinking",
403
+ thinking: "",
404
+ thinkingSignature: b.data,
405
+ redacted: true,
406
+ });
407
+ }
408
+ else
409
+ throw new XgateError("unsupported_result_block");
410
+ }
411
+ }
412
+ else {
413
+ const cap = capabilities[entry.model.id];
414
+ need(cap.response_models.includes(body.model) &&
415
+ body.status === "completed" &&
416
+ !body.error &&
417
+ !body.incomplete_details &&
418
+ Array.isArray(body.output), "invalid_result");
419
+ stopReason = "stop";
420
+ for (const b of body.output) {
421
+ if (b.type === "message") {
422
+ need(b.status === "completed" && Array.isArray(b.content), "invalid_result");
423
+ for (const t of b.content) {
424
+ need(t.type === "output_text" && typeof t.text === "string", "invalid_text");
425
+ content.push({
426
+ type: "text",
427
+ text: t.text,
428
+ ...(b.id
429
+ ? {
430
+ textSignature: JSON.stringify({
431
+ v: 1,
432
+ id: b.id,
433
+ ...(b.phase ? { phase: b.phase } : {}),
434
+ }),
435
+ }
436
+ : {}),
437
+ });
438
+ }
439
+ }
440
+ else if (b.type === "function_call") {
441
+ need(typeof b.call_id === "string" &&
442
+ typeof b.name === "string" &&
443
+ typeof b.arguments === "string", "invalid_tool_result");
444
+ const args = JSON.parse(b.arguments);
445
+ need(object(args), "invalid_tool_result");
446
+ content.push({
447
+ type: "toolCall",
448
+ id: b.call_id,
449
+ name: b.name,
450
+ arguments: args,
451
+ });
452
+ stopReason = "toolUse";
453
+ }
454
+ else if (b.type === "reasoning") {
455
+ need(Array.isArray(b.summary), "invalid_thinking");
456
+ const summary = b.summary
457
+ .map((s) => {
458
+ need(s.type === "summary_text" && typeof s.text === "string", "invalid_thinking");
459
+ return s.text;
460
+ })
461
+ .join("\n");
462
+ need(typeof b.encrypted_content === "string" &&
463
+ b.encrypted_content.length > 0, "missing_encrypted_reasoning");
464
+ content.push({
465
+ type: "thinking",
466
+ thinking: summary,
467
+ thinkingSignature: JSON.stringify({
468
+ type: "reasoning",
469
+ id: b.id,
470
+ summary: b.summary,
471
+ encrypted_content: b.encrypted_content,
472
+ ...(b.status === "completed" ? { status: b.status } : {}),
473
+ }),
474
+ });
475
+ }
476
+ else
477
+ throw new XgateError("unsupported_result_block");
478
+ }
479
+ }
480
+ need(content.some((c) => (c.type === "text" && c.text.length > 0) || c.type === "toolCall"), "empty_result");
481
+ need((stopReason === "toolUse") === content.some((c) => c.type === "toolCall"), "tool_stop_mismatch");
482
+ const u = body.usage, read = tokens(anthropic
483
+ ? (u.cache_read_input_tokens ?? 0)
484
+ : u.input_tokens_details?.cached_tokens), write = tokens(anthropic
485
+ ? (u.cache_creation_input_tokens ?? 0)
486
+ : (u.input_tokens_details?.cache_write_tokens ?? 0)), totalInput = tokens(u.input_tokens), output = tokens(u.output_tokens), input = anthropic ? totalInput : totalInput - read - write;
487
+ need(input >= 0 && (!anthropic || (read === 0 && write === 0)), "cache_usage_mismatch");
488
+ const rates = entry.model.cost;
489
+ const cost = {
490
+ input: (input * rates.input) / 1e6,
491
+ output: (output * rates.output) / 1e6,
492
+ cacheRead: (read * rates.cacheRead) / 1e6,
493
+ cacheWrite: (write * rates.cacheWrite) / 1e6,
494
+ total: 0,
495
+ };
496
+ cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
497
+ return {
498
+ content,
499
+ usage: {
500
+ input,
501
+ output,
502
+ cacheRead: read,
503
+ cacheWrite: write,
504
+ totalTokens: input + read + write + output,
505
+ cost,
506
+ },
507
+ stopReason,
508
+ responseId: body.id,
509
+ };
510
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { XgatePayment, XgateTransport } from "./transport.ts";
3
+ export interface XgateExtensionOptions {
4
+ /**
5
+ * Host-managed transport, or a factory called on load and `/xgate refresh`. When set, the
6
+ * extension reads no broker configuration and `/xgate setup` is unavailable. Without it, the
7
+ * standalone `BrokerClient` from `XGATE_CONFIG` or `~/.pi/agent/xgate.json` is used.
8
+ */
9
+ transport?: XgateTransport | (() => XgateTransport | Promise<XgateTransport>);
10
+ }
11
+ /** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
12
+ export declare function paymentStatus(p: XgatePayment): string;
13
+ /** Pi extension factory. The default export is the standalone wallet-broker extension. */
14
+ export declare function createXgateExtension(options?: XgateExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
15
+ declare const _default: (pi: ExtensionAPI) => Promise<void>;
16
+ export default _default;
@@ -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
+ }