@lucid-agents/pi-xgate 0.1.5 → 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.
@@ -0,0 +1,275 @@
1
+ // Adapts TaskMarket's x402 payer to the xgate Pi transport. The payer package is private, so
2
+ // this file declares the structural subset of its v1 contract (`@taskmarket/x402-payer`
3
+ // `src/api.ts`, X402_PAYER_API_VERSION 1) that the adapter uses; TaskMarket injects its
4
+ // instance. The payer owns the wallet, signer, rails, policy and journals: this adapter keeps
5
+ // no payment state of its own.
6
+ //
7
+ // The payer's string unions may gain values without a version bump. Every type here accepts
8
+ // unknown strings, and the adapter treats an unknown state as unverified.
9
+ import { atomic, origin as checkOrigin, record, route as checkRoute, XgateError, } from "@lucid-agents/xgate-client/types";
10
+ import { XgatePurchaseError, } from "./transport.js";
11
+ const RESOLVED_CHARGES = ["none", "released", "settled", "manually_resolved"], RESOLVED_STATES = ["completed", "failed"];
12
+ const CATALOG_BYTES = 2 * 1024 * 1024, REQUEST_BYTES = 8 * 1024 * 1024, CATALOG_DEADLINE_MS = 15000;
13
+ /** Never sends the request again for an ID the payer has seen: `purchase` maps every non-verified outcome to a terminal error. */
14
+ export function createTaskMarketTransport(options) {
15
+ const { payer } = options, base = checkOrigin(options.origin), fetcher = options.fetch ?? fetch;
16
+ if (payer.apiVersion !== 1)
17
+ throw new XgateError("unsupported_payer_api");
18
+ const channel = async (payment) => {
19
+ if (!options.balances || payment.rail.kind !== "session")
20
+ return;
21
+ try {
22
+ const matches = (await options.balances()).filter((b) => b.railId === payment.rail.id &&
23
+ b.network === payment.network &&
24
+ b.asset === payment.asset);
25
+ // Several sessions on one rail are not one channel; unlike balances are never summed.
26
+ if (matches.length !== 1)
27
+ return;
28
+ const b = matches[0];
29
+ return {
30
+ depositingAtomic: b.depositing,
31
+ lockedAtomic: b.locked,
32
+ chargedAtomic: b.charged,
33
+ pendingAtomic: b.pending,
34
+ refundableAtomic: b.refundable,
35
+ };
36
+ }
37
+ catch {
38
+ return; // Display only.
39
+ }
40
+ };
41
+ return {
42
+ async models(signal) {
43
+ const deadline = AbortSignal.timeout(CATALOG_DEADLINE_MS);
44
+ let response;
45
+ try {
46
+ response = await fetcher(base + "/v1/models", {
47
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
48
+ redirect: "error",
49
+ cache: "no-store",
50
+ credentials: "omit",
51
+ referrerPolicy: "no-referrer",
52
+ headers: { accept: "application/json" },
53
+ });
54
+ }
55
+ catch {
56
+ throw new XgateError("catalog_unavailable");
57
+ }
58
+ if (!response.ok) {
59
+ await response.body?.cancel().catch(() => { });
60
+ throw new XgateError("catalog_unavailable");
61
+ }
62
+ const v = parse(await bounded(response, CATALOG_BYTES));
63
+ if (!record(v) || !Array.isArray(v.data) || v.data.length > 128)
64
+ throw new XgateError("invalid_catalog");
65
+ return v.data
66
+ .filter((m) => record(m) &&
67
+ (options.models === undefined || options.models.includes(m.id)))
68
+ .map((m) => ({ ...m, xgate_origin: base }));
69
+ },
70
+ async purchase(invocation, signal, onResponse) {
71
+ checkRoute(invocation.route);
72
+ const body = new TextEncoder().encode(invocation.body);
73
+ if (body.byteLength === 0 || body.byteLength > REQUEST_BYTES)
74
+ throw new XgateError("invalid_invocation");
75
+ const id = invocation.id;
76
+ let result;
77
+ try {
78
+ result = await payer.request({
79
+ apiVersion: 1,
80
+ invocationId: id,
81
+ url: base + invocation.route,
82
+ method: "POST",
83
+ // Application headers only: the payer owns every payment header.
84
+ headers: {
85
+ "content-type": "application/json",
86
+ accept: "application/json",
87
+ },
88
+ body,
89
+ limits: { maxRequestBodyBytes: REQUEST_BYTES },
90
+ ...(options.allocationId !== undefined
91
+ ? { allocationId: options.allocationId }
92
+ : {}),
93
+ ...(options.policyRuleId !== undefined
94
+ ? { policyRuleId: options.policyRuleId }
95
+ : {}),
96
+ ...(options.nonInteractive !== undefined
97
+ ? { nonInteractive: options.nonInteractive }
98
+ : {}),
99
+ }, {
100
+ ...(signal ? { signal } : {}),
101
+ ...(options.confirmPayment
102
+ ? { confirmPayment: options.confirmPayment }
103
+ : {}),
104
+ });
105
+ }
106
+ catch (error) {
107
+ throw failure(error, id);
108
+ }
109
+ const payment = result.payment;
110
+ if (result.invocationId !== id || result.apiVersion !== 1)
111
+ throw new XgatePurchaseError("payer_result_mismatch", id, true, payment && display(payment));
112
+ // A paid xgate route never answers without a payment challenge.
113
+ if (!payment)
114
+ throw new XgatePurchaseError(result.response.status === 200
115
+ ? "payment_missing"
116
+ : "request_rejected", id, false);
117
+ const shown = display(payment);
118
+ if (payment.charge !== "settled" ||
119
+ typeof payment.settledAmount !== "string")
120
+ throw new XgatePurchaseError("payment_unverified", id, true, shown);
121
+ shown.chargedAtomic = payment.settledAmount;
122
+ try {
123
+ if (atomic(payment.settledAmount) > atomic(payment.authorizedAmount))
124
+ throw new XgateError("charge_over_authorization");
125
+ }
126
+ catch {
127
+ throw new XgatePurchaseError("invalid_settlement", id, true, shown);
128
+ }
129
+ const r = result.response, h = r.headers;
130
+ // Charged, so not pending, but the content is unusable.
131
+ if (r.status !== 200 ||
132
+ !/^application\/json\b/i.test(r.contentType) ||
133
+ h["x-inference-privacy"] !== "anonymous" ||
134
+ h["x-result-retention"] !== "none" ||
135
+ !h["cache-control"]?.includes("no-store"))
136
+ throw new XgatePurchaseError("response_rejected", id, false, shown);
137
+ let value;
138
+ try {
139
+ value = parse(r.body);
140
+ }
141
+ catch {
142
+ throw new XgatePurchaseError("invalid_response", id, false, shown);
143
+ }
144
+ if (!record(value) ||
145
+ typeof value.id !== "string" ||
146
+ value.result_replay_available !== undefined)
147
+ throw new XgatePurchaseError("invalid_response", id, false, shown);
148
+ const head = {
149
+ status: r.status,
150
+ headers: { "content-type": r.contentType, "cache-control": "no-store" },
151
+ };
152
+ try {
153
+ await onResponse?.(head);
154
+ }
155
+ catch {
156
+ throw new XgatePurchaseError("response_hook_failed", id, false, shown);
157
+ }
158
+ const balance = await channel(payment);
159
+ if (balance)
160
+ shown.channel = balance;
161
+ return {
162
+ body: value,
163
+ settlement: {
164
+ chargedAtomic: payment.settledAmount,
165
+ reference: payment.transaction ?? payment.commitment ?? payment.paymentId,
166
+ },
167
+ purchase: payment.commitment ?? payment.paymentId,
168
+ payment: shown,
169
+ };
170
+ },
171
+ async status() {
172
+ return {
173
+ transport: "taskmarket",
174
+ origin: base,
175
+ ...(options.balances
176
+ ? {
177
+ balances: (await options.balances()).map((b) => ({
178
+ rail: b.railId,
179
+ network: b.network,
180
+ asset: b.asset,
181
+ depositingAtomic: b.depositing,
182
+ lockedAtomic: b.locked,
183
+ chargedAtomic: b.charged,
184
+ pendingAtomic: b.pending,
185
+ refundableAtomic: b.refundable,
186
+ })),
187
+ }
188
+ : {}),
189
+ };
190
+ },
191
+ async reconcile(invocationId) {
192
+ const s = await payer.reconcile(invocationId);
193
+ return {
194
+ invocationId: s.invocationId,
195
+ state: s.state,
196
+ charge: s.charge,
197
+ uncertain: s.uncertain,
198
+ ...(s.errorCode ? { errorCode: s.errorCode } : {}),
199
+ ...(s.payment ? { payment: display(s.payment) } : {}),
200
+ };
201
+ },
202
+ };
203
+ }
204
+ function display(p) {
205
+ return {
206
+ network: p.network,
207
+ scheme: p.scheme,
208
+ asset: p.asset,
209
+ payTo: p.payTo,
210
+ rail: p.rail.kind,
211
+ authorizedAtomic: p.authorizedAmount,
212
+ ...(p.charge === "settled" && p.settledAmount !== undefined
213
+ ? { chargedAtomic: p.settledAmount }
214
+ : {}),
215
+ status: p.charge,
216
+ ...((p.transaction ?? p.commitment)
217
+ ? { reference: p.transaction ?? p.commitment }
218
+ : {}),
219
+ };
220
+ }
221
+ /**
222
+ * Maps a payer rejection. Only the stable code crosses into Pi; the payer's message may carry
223
+ * URLs or server text. A rejection that does not state `pending: false` counts as pending.
224
+ */
225
+ function failure(error, id) {
226
+ const e = error;
227
+ const code = typeof e?.code === "string" && /^[a-z0-9_]{1,64}$/.test(e.code)
228
+ ? e.code
229
+ : "payer_failed";
230
+ // A duplicate never sends again, but its recorded invocation may still be unresolved: only a
231
+ // positively resolved record counts as not pending.
232
+ const recorded = e?.invocation;
233
+ const pending = e?.pending !== false ||
234
+ (recorded !== undefined &&
235
+ !(recorded.uncertain === false &&
236
+ RESOLVED_CHARGES.includes(recorded.charge) &&
237
+ RESOLVED_STATES.includes(recorded.state)));
238
+ const summary = e?.invocation?.payment;
239
+ return new XgatePurchaseError(code, id, pending, record(summary) ? display(summary) : undefined);
240
+ }
241
+ async function bounded(response, limit) {
242
+ if (!response.body)
243
+ throw new XgateError("invalid_catalog");
244
+ const reader = response.body.getReader(), chunks = [];
245
+ let length = 0;
246
+ try {
247
+ for (;;) {
248
+ const part = await reader.read();
249
+ if (part.done)
250
+ break;
251
+ length += part.value.byteLength;
252
+ if (length > limit)
253
+ throw new XgateError("catalog_bound");
254
+ chunks.push(part.value);
255
+ }
256
+ }
257
+ catch (error) {
258
+ await reader.cancel().catch(() => { });
259
+ throw error instanceof XgateError
260
+ ? error
261
+ : new XgateError("catalog_unavailable");
262
+ }
263
+ finally {
264
+ reader.releaseLock();
265
+ }
266
+ return Buffer.concat(chunks, length);
267
+ }
268
+ function parse(bytes) {
269
+ try {
270
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
271
+ }
272
+ catch {
273
+ throw new XgateError("invalid_response");
274
+ }
275
+ }
@@ -0,0 +1,72 @@
1
+ import { XgateError, type Invocation, type PurchaseResult } from "@lucid-agents/xgate-client/types";
2
+ /** Response metadata safe to show Pi's `onResponse` hook: never payment or status credentials. */
3
+ export interface XgateResponseHead {
4
+ status: number;
5
+ headers: Record<string, string>;
6
+ }
7
+ /**
8
+ * Rail-neutral payment terms and status for display. Pi shows them; it never selects or
9
+ * configures a rail. Amounts are unsigned integer strings in the asset's smallest unit, and
10
+ * amounts of different assets are never combined.
11
+ */
12
+ export interface XgatePayment {
13
+ /** CAIP-2 network, e.g. `eip155:8453`. */
14
+ network: string;
15
+ /** Payment scheme as the payee advertised it, e.g. `batch-settlement`, `exact`, `session`. */
16
+ scheme: string;
17
+ asset: string;
18
+ payTo?: string;
19
+ /** Rail shape: `per-request` or `session` today; other values may appear. */
20
+ rail?: string;
21
+ /** Most this invocation could be charged. */
22
+ authorizedAtomic: string;
23
+ /** Verified charge, once known. */
24
+ chargedAtomic?: string;
25
+ /** `settled`, `pending`, `amount_unknown`, `released`, ...; anything but `settled` is unverified. */
26
+ status: string;
27
+ /** Settlement transaction or payee commitment, when one exists. */
28
+ reference?: string;
29
+ /** Session-rail balance after this payment, when the host reports one. */
30
+ channel?: XgateChannelBalance;
31
+ }
32
+ export interface XgateChannelBalance {
33
+ depositingAtomic: string;
34
+ lockedAtomic: string;
35
+ chargedAtomic: string;
36
+ pendingAtomic: string;
37
+ refundableAtomic: string;
38
+ }
39
+ export interface XgatePurchaseResult extends PurchaseResult {
40
+ payment?: XgatePayment;
41
+ }
42
+ /**
43
+ * A purchase that ended without a verified result. `pending` is true when money may have moved
44
+ * and the outcome is unknown: the invocation must be reconciled, never retried.
45
+ */
46
+ export declare class XgatePurchaseError extends XgateError {
47
+ readonly invocationId: string;
48
+ readonly pending: boolean;
49
+ readonly payment?: XgatePayment;
50
+ constructor(code: string, invocationId: string, pending: boolean, payment?: XgatePayment);
51
+ }
52
+ /**
53
+ * Moves one frozen Pi request to xgate and returns the verified result. The provider owns
54
+ * catalog qualification, codecs and the invocation ID; the transport owns payment.
55
+ *
56
+ * `purchase` sends at most one paid submission per `Invocation.id`, never retries a paid
57
+ * request, and never pays again for an ID it has seen: a repeated ID rejects. It resolves
58
+ * only with a verified result and rejects on every other outcome, preferably with an
59
+ * `XgatePurchaseError` stating whether the payment is pending. `BrokerClient` is the
60
+ * standalone implementation.
61
+ */
62
+ export interface XgateTransport {
63
+ /** Current router catalog rows. Called before every paid invocation. */
64
+ models(signal?: AbortSignal): Promise<unknown[]>;
65
+ purchase(invocation: Invocation, signal?: AbortSignal, onResponse?: (response: XgateResponseHead) => Promise<void>): Promise<XgatePurchaseResult>;
66
+ /** Optional `/xgate status` and `/xgate budget` view. */
67
+ status?(signal?: AbortSignal): Promise<unknown>;
68
+ /** Optional `/xgate receipts` view. */
69
+ receipts?(signal?: AbortSignal): Promise<unknown>;
70
+ /** Optional `/xgate reconcile <invocation ID>`. Never resends the request. */
71
+ reconcile?(invocationId: string, signal?: AbortSignal): Promise<unknown>;
72
+ }
@@ -0,0 +1,21 @@
1
+ import { XgateError, } from "@lucid-agents/xgate-client/types";
2
+ /**
3
+ * A purchase that ended without a verified result. `pending` is true when money may have moved
4
+ * and the outcome is unknown: the invocation must be reconciled, never retried.
5
+ */
6
+ export class XgatePurchaseError extends XgateError {
7
+ invocationId;
8
+ pending;
9
+ payment;
10
+ constructor(code, invocationId, pending, payment) {
11
+ super(code);
12
+ this.name = "XgatePurchaseError";
13
+ this.message = pending
14
+ ? `Xgate: ${code}. The payment outcome is unknown; do not retry. Reconcile it before another purchase.`
15
+ : `Xgate: ${code}. Inspect the payment status before another purchase.`;
16
+ this.invocationId = invocationId;
17
+ this.pending = pending;
18
+ if (payment)
19
+ this.payment = payment;
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@lucid-agents/pi-xgate",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./extension": {
11
+ "types": "./dist/extension.d.ts",
12
+ "default": "./dist/extension.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
5
16
  "keywords": [
6
17
  "pi-package",
7
18
  "xgate",
@@ -15,16 +26,17 @@
15
26
  ]
16
27
  },
17
28
  "files": [
29
+ "dist",
18
30
  "src",
19
31
  "README.md",
20
32
  "LICENSE"
21
33
  ],
22
34
  "dependencies": {
23
- "@lucid-agents/xgate-client": "0.1.5"
35
+ "@lucid-agents/xgate-client": "0.1.6"
24
36
  },
25
37
  "peerDependencies": {
26
- "@earendil-works/pi-ai": "1.1.0",
27
- "@earendil-works/pi-coding-agent": "1.1.0"
38
+ "@earendil-works/pi-ai": "0.85.1 || 1.1.0",
39
+ "@earendil-works/pi-coding-agent": "0.85.1 || 1.1.0"
28
40
  },
29
41
  "engines": {
30
42
  "node": ">=24.19 <25"
@@ -44,10 +56,10 @@
44
56
  "registry": "https://registry.npmjs.org"
45
57
  },
46
58
  "license": "MIT",
47
- "gitHead": "2c12308686701f39f95093c5984d014c0ec83d26",
59
+ "gitHead": "f26a9cdf35db07e8aa6b6ad7c6408884aa283ebc",
48
60
  "xgateRelease": {
49
- "version": "v0.1.5",
50
- "sourceSha": "2c12308686701f39f95093c5984d014c0ec83d26",
51
- "candidateId": "rc-2c1230868670-37853927676-1"
61
+ "version": "v0.1.6",
62
+ "sourceSha": "f26a9cdf35db07e8aa6b6ad7c6408884aa283ebc",
63
+ "candidateId": "rc-f26a9cdf35db-38096296216-1"
52
64
  }
53
65
  }
package/src/codec.ts CHANGED
@@ -1,13 +1,14 @@
1
- import {
2
- collapseSystemMessages,
3
- getCurrentSystemPrompt,
4
- getCurrentTools,
5
- type AssistantMessage,
6
- type Model,
7
- type SimpleStreamOptions,
8
- type TranscriptContext,
9
- type Usage,
10
- type ThinkingLevelMap,
1
+ import * as pi from "@earendil-works/pi-ai";
2
+ import type {
3
+ AssistantMessage,
4
+ Context,
5
+ Message,
6
+ Model,
7
+ SimpleStreamOptions,
8
+ Tool,
9
+ TranscriptContext,
10
+ Usage,
11
+ ThinkingLevelMap,
11
12
  } from "@earendil-works/pi-ai";
12
13
  import { XgateError, type Route } from "@lucid-agents/xgate-client/types";
13
14
  import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
@@ -155,6 +156,45 @@ export function catalogEntries(rows: any[]): Entry[] {
155
156
  }
156
157
  return entries;
157
158
  }
159
+ /**
160
+ * Pi 1.1 passes a normalized transcript; Pi 0.85 passes `systemPrompt` and `tools` beside the
161
+ * messages. Structural, so published types do not name a type only one Pi version has.
162
+ */
163
+ export type PiContext = {
164
+ messages: Message[];
165
+ systemPrompt?: string;
166
+ tools?: Tool[];
167
+ };
168
+ type Turn = Exclude<Message, { role: "system" }>;
169
+ export function transcript(context: PiContext): {
170
+ system: string;
171
+ tools: Tool[];
172
+ messages: Turn[];
173
+ } {
174
+ const legacy = "systemPrompt" in context || "tools" in context;
175
+ if (typeof pi.collapseSystemMessages !== "function") {
176
+ need(
177
+ !context.messages.some((m) => m.role === "system"),
178
+ "unsupported_context",
179
+ );
180
+ const raw = context as Context;
181
+ return {
182
+ system: raw.systemPrompt ?? "",
183
+ tools: raw.tools ?? [],
184
+ messages: raw.messages as Turn[],
185
+ };
186
+ }
187
+ const collapsed = pi.collapseSystemMessages(
188
+ legacy
189
+ ? pi.normalizeContext(context as Context)
190
+ : (context as TranscriptContext),
191
+ );
192
+ return {
193
+ system: pi.getCurrentSystemPrompt(collapsed.messages),
194
+ tools: pi.getCurrentTools(collapsed.messages),
195
+ messages: collapsed.messages.filter((m): m is Turn => m.role !== "system"),
196
+ };
197
+ }
158
198
  function text(content: any): string {
159
199
  if (typeof content === "string") return content;
160
200
  need(
@@ -187,7 +227,7 @@ function validateTools(tools: any[], anthropic: boolean): void {
187
227
  }
188
228
  export function encode(
189
229
  entry: Entry,
190
- context: TranscriptContext,
230
+ context: PiContext,
191
231
  options: SimpleStreamOptions = {},
192
232
  ): any {
193
233
  need(
@@ -205,9 +245,7 @@ export function encode(
205
245
  "unsupported_sampling",
206
246
  );
207
247
  const anthropic = entry.route.endsWith("/messages"),
208
- collapsed = collapseSystemMessages(context),
209
- system = getCurrentSystemPrompt(collapsed.messages),
210
- tools = getCurrentTools(collapsed.messages);
248
+ { system, tools, messages } = transcript(context);
211
249
  validateTools(tools, anthropic);
212
250
  const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
213
251
  need(
@@ -234,8 +272,7 @@ export function encode(
234
272
  else history.push({ role, content });
235
273
  };
236
274
  if (!anthropic && system) history.push({ role: "system", content: system });
237
- for (const message of collapsed.messages) {
238
- if (message.role === "system") continue;
275
+ for (const message of messages) {
239
276
  if (message.role === "user") {
240
277
  const value = text(message.content);
241
278
  if (anthropic) append("user", [{ type: "text", text: value }]);
package/src/extension.ts CHANGED
@@ -8,6 +8,15 @@ import { readFileSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
8
8
  import { dirname, isAbsolute, join } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { createXgateProvider } from "./provider.ts";
11
+ import type { XgatePayment, XgateTransport } from "./transport.ts";
12
+ export interface XgateExtensionOptions {
13
+ /**
14
+ * Host-managed transport, or a factory called on load and `/xgate refresh`. When set, the
15
+ * extension reads no broker configuration and `/xgate setup` is unavailable. Without it, the
16
+ * standalone `BrokerClient` from `XGATE_CONFIG` or `~/.pi/agent/xgate.json` is used.
17
+ */
18
+ transport?: XgateTransport | (() => XgateTransport | Promise<XgateTransport>);
19
+ }
11
20
  function privateText(path: string, max = 8192): string {
12
21
  if (!isAbsolute(path)) throw new XgateError("absolute_config_path_required");
13
22
  const s = lstatSync(path);
@@ -20,25 +29,57 @@ function privateText(path: string, max = 8192): string {
20
29
  throw new XgateError("private_config_required");
21
30
  return readFileSync(path, "utf8").trim();
22
31
  }
23
- export default async function extension(pi: ExtensionAPI): Promise<void> {
32
+ function standalone(configPath: string): BrokerClient {
33
+ const cfg = JSON.parse(privateText(configPath));
34
+ if (typeof cfg.url !== "string" || typeof cfg.tokenFile !== "string")
35
+ throw new XgateError("invalid_config");
36
+ return new BrokerClient(cfg.url, privateText(cfg.tokenFile, 128));
37
+ }
38
+ /** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
39
+ export function paymentStatus(p: XgatePayment): string {
40
+ const left = p.channel
41
+ ? `; channel ${p.channel.refundableAtomic} refundable`
42
+ : "";
43
+ return `xgate paid ${p.chargedAtomic ?? "?"}/${p.authorizedAtomic} ${p.asset} on ${p.network} (${p.scheme})${left}`;
44
+ }
45
+ /** Pi extension factory. The default export is the standalone wallet-broker extension. */
46
+ export function createXgateExtension(
47
+ options: XgateExtensionOptions = {},
48
+ ): (pi: ExtensionAPI) => Promise<void> {
49
+ return (pi) => register(pi, options);
50
+ }
51
+ export default createXgateExtension();
52
+ async function register(
53
+ pi: ExtensionAPI,
54
+ options: XgateExtensionOptions,
55
+ ): Promise<void> {
56
+ const injected = options.transport;
24
57
  const configPath =
25
58
  process.env.XGATE_CONFIG ?? join(homedir(), ".pi", "agent", "xgate.json");
26
- let broker: BrokerClient | undefined;
59
+ let broker: XgateTransport | undefined;
27
60
  let context: ExtensionContext | undefined;
28
61
  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) =>
62
+ const next =
63
+ injected === undefined
64
+ ? standalone(configPath)
65
+ : typeof injected === "function"
66
+ ? await injected()
67
+ : injected;
68
+ const provider = await createXgateProvider(next, {
69
+ sessionId: () => context?.sessionManager.getSessionId() ?? "standalone",
70
+ onPaid: (r) =>
37
71
  context?.ui.setStatus(
38
72
  "xgate",
39
- `xgate paid ${(Number(r.settlement.chargedAtomic) / 1e6).toFixed(6)}`,
73
+ r.payment
74
+ ? paymentStatus(r.payment)
75
+ : `xgate paid ${(Number(r.settlement.chargedAtomic) / 1e6).toFixed(6)}`,
40
76
  ),
41
- );
77
+ onPurchaseFailed: (f) =>
78
+ context?.ui.setStatus(
79
+ "xgate",
80
+ `xgate ${f.pending ? "unresolved" : "failed"}: ${f.invocationId}`,
81
+ ),
82
+ });
42
83
  pi.registerProvider(provider);
43
84
  broker = next;
44
85
  };
@@ -63,6 +104,8 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
63
104
  const [command, id] = args.trim().split(/\s+/);
64
105
  try {
65
106
  if (command === "setup") {
107
+ if (injected !== undefined)
108
+ throw new XgateError("transport_managed_by_host");
66
109
  if (!ctx.hasUI)
67
110
  throw new XgateError("configure_XGATE_CONFIG_for_headless_mode");
68
111
  const url = await ctx.ui.input(
@@ -97,11 +140,15 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
97
140
  if (!broker) throw new XgateError("wallet_not_configured");
98
141
  if (command === "reconcile") {
99
142
  if (!id) throw new XgateError("purchase_id_required");
143
+ if (!broker.reconcile)
144
+ throw new XgateError("unsupported_by_transport");
100
145
  const result = await broker.reconcile(id);
101
146
  ctx.ui.notify(JSON.stringify(result), "info");
102
147
  return;
103
148
  }
104
149
  if (command === "receipts") {
150
+ if (!broker.receipts)
151
+ throw new XgateError("unsupported_by_transport");
105
152
  ctx.ui.notify(
106
153
  JSON.stringify(await broker.receipts(), null, 2),
107
154
  "info",
@@ -109,6 +156,7 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
109
156
  return;
110
157
  }
111
158
  if (command === "status" || command === "budget" || !command) {
159
+ if (!broker.status) throw new XgateError("unsupported_by_transport");
112
160
  const status = await broker.status();
113
161
  ctx.ui.notify(JSON.stringify(status, null, 2), "info");
114
162
  return;