@token2chat/t2c 0.2.0-beta.1

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/dist/adapters/aider.d.ts +5 -0
  4. package/dist/adapters/aider.js +29 -0
  5. package/dist/adapters/cline.d.ts +5 -0
  6. package/dist/adapters/cline.js +32 -0
  7. package/dist/adapters/continue.d.ts +5 -0
  8. package/dist/adapters/continue.js +45 -0
  9. package/dist/adapters/cursor.d.ts +5 -0
  10. package/dist/adapters/cursor.js +23 -0
  11. package/dist/adapters/env.d.ts +5 -0
  12. package/dist/adapters/env.js +25 -0
  13. package/dist/adapters/index.d.ts +6 -0
  14. package/dist/adapters/index.js +6 -0
  15. package/dist/adapters/openclaw.d.ts +2 -0
  16. package/dist/adapters/openclaw.js +167 -0
  17. package/dist/cashu-store.d.ts +52 -0
  18. package/dist/cashu-store.js +201 -0
  19. package/dist/commands/audit.d.ts +6 -0
  20. package/dist/commands/audit.js +340 -0
  21. package/dist/commands/balance.d.ts +5 -0
  22. package/dist/commands/balance.js +29 -0
  23. package/dist/commands/config.d.ts +5 -0
  24. package/dist/commands/config.js +62 -0
  25. package/dist/commands/connect.d.ts +1 -0
  26. package/dist/commands/connect.js +43 -0
  27. package/dist/commands/doctor.d.ts +1 -0
  28. package/dist/commands/doctor.js +178 -0
  29. package/dist/commands/init.d.ts +3 -0
  30. package/dist/commands/init.js +50 -0
  31. package/dist/commands/mint.d.ts +5 -0
  32. package/dist/commands/mint.js +168 -0
  33. package/dist/commands/recover.d.ts +1 -0
  34. package/dist/commands/recover.js +61 -0
  35. package/dist/commands/service.d.ts +7 -0
  36. package/dist/commands/service.js +378 -0
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +128 -0
  39. package/dist/commands/status.d.ts +5 -0
  40. package/dist/commands/status.js +87 -0
  41. package/dist/config.d.ts +83 -0
  42. package/dist/config.js +224 -0
  43. package/dist/connectors/cursor.d.ts +2 -0
  44. package/dist/connectors/cursor.js +28 -0
  45. package/dist/connectors/env.d.ts +2 -0
  46. package/dist/connectors/env.js +38 -0
  47. package/dist/connectors/index.d.ts +26 -0
  48. package/dist/connectors/index.js +30 -0
  49. package/dist/connectors/interface.d.ts +20 -0
  50. package/dist/connectors/interface.js +1 -0
  51. package/dist/connectors/openclaw.d.ts +2 -0
  52. package/dist/connectors/openclaw.js +202 -0
  53. package/dist/gate-discovery.d.ts +49 -0
  54. package/dist/gate-discovery.js +142 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +177 -0
  57. package/dist/proxy.d.ts +11 -0
  58. package/dist/proxy.js +352 -0
  59. package/package.json +84 -0
package/dist/config.js ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * t2c configuration management
3
+ */
4
+ import fs from "node:fs/promises";
5
+ import crypto from "node:crypto";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ // Keep in sync with plugin/src/index.ts
9
+ export const DEFAULT_CONFIG = {
10
+ gateUrl: "https://gate.token2chat.com",
11
+ mintUrl: "https://mint.token2chat.com",
12
+ walletPath: "~/.t2c/wallet.json",
13
+ proxyPort: 10402,
14
+ lowBalanceThreshold: 1000,
15
+ autoDiscover: false,
16
+ discoveryUrl: "https://token2.cash/gates.json",
17
+ };
18
+ export const CONFIG_DIR = path.join(os.homedir(), ".t2c");
19
+ export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
20
+ export const WALLET_PATH = path.join(CONFIG_DIR, "wallet.json");
21
+ export const PID_PATH = path.join(CONFIG_DIR, "proxy.pid");
22
+ export const LOG_PATH = path.join(CONFIG_DIR, "proxy.log");
23
+ export const PROXY_SECRET_PATH = path.join(CONFIG_DIR, "proxy-secret");
24
+ export function resolveHome(p) {
25
+ let resolved = p;
26
+ if (resolved.startsWith("~/")) {
27
+ resolved = path.join(os.homedir(), resolved.slice(2));
28
+ }
29
+ // Normalize to prevent path traversal via .. segments
30
+ return path.resolve(resolved);
31
+ }
32
+ export async function ensureConfigDir() {
33
+ await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
34
+ }
35
+ /**
36
+ * Load or create the proxy authentication secret.
37
+ * Used by the local proxy to authenticate requests.
38
+ */
39
+ export async function loadOrCreateProxySecret() {
40
+ try {
41
+ const secret = (await fs.readFile(PROXY_SECRET_PATH, "utf-8")).trim();
42
+ if (secret.length > 0)
43
+ return secret;
44
+ }
45
+ catch {
46
+ // File doesn't exist or unreadable — create a new one
47
+ }
48
+ await ensureConfigDir();
49
+ const secret = `t2c-${crypto.randomBytes(24).toString("hex")}`;
50
+ await fs.writeFile(PROXY_SECRET_PATH, secret + "\n", { mode: 0o600 });
51
+ return secret;
52
+ }
53
+ /**
54
+ * Load config with automatic recovery from corruption
55
+ */
56
+ export async function loadConfig() {
57
+ try {
58
+ const raw = await fs.readFile(CONFIG_PATH, "utf-8");
59
+ const saved = JSON.parse(raw);
60
+ // Validate the loaded config
61
+ const merged = { ...DEFAULT_CONFIG, ...saved };
62
+ // Basic sanity checks
63
+ if (typeof merged.proxyPort !== "number" ||
64
+ merged.proxyPort < 1 ||
65
+ merged.proxyPort > 65535) {
66
+ console.warn(`Warning: Invalid proxy port in config (${merged.proxyPort}), using default`);
67
+ merged.proxyPort = DEFAULT_CONFIG.proxyPort;
68
+ }
69
+ if (typeof merged.gateUrl !== "string" || !merged.gateUrl.startsWith("http")) {
70
+ console.warn(`Warning: Invalid gate URL in config, using default`);
71
+ merged.gateUrl = DEFAULT_CONFIG.gateUrl;
72
+ }
73
+ if (typeof merged.mintUrl !== "string" || !merged.mintUrl.startsWith("http")) {
74
+ console.warn(`Warning: Invalid mint URL in config, using default`);
75
+ merged.mintUrl = DEFAULT_CONFIG.mintUrl;
76
+ }
77
+ return merged;
78
+ }
79
+ catch (e) {
80
+ const err = e;
81
+ // File doesn't exist - return defaults
82
+ if (err.code === "ENOENT") {
83
+ return { ...DEFAULT_CONFIG };
84
+ }
85
+ // File exists but is corrupted
86
+ if (err instanceof SyntaxError || err.message?.includes("JSON")) {
87
+ console.warn("Warning: Config file corrupted, attempting recovery...");
88
+ // Try to backup corrupted file
89
+ try {
90
+ const backupPath = `${CONFIG_PATH}.corrupted.${Date.now()}`;
91
+ await fs.rename(CONFIG_PATH, backupPath);
92
+ console.warn(` Backed up corrupted config to: ${backupPath}`);
93
+ }
94
+ catch {
95
+ // Couldn't backup, that's ok
96
+ }
97
+ console.warn(" Using default configuration");
98
+ return { ...DEFAULT_CONFIG };
99
+ }
100
+ // Other error (permissions, etc)
101
+ console.error(`Error reading config: ${err.message}`);
102
+ return { ...DEFAULT_CONFIG };
103
+ }
104
+ }
105
+ export async function saveConfig(config) {
106
+ await ensureConfigDir();
107
+ // Validate before saving
108
+ if (config.proxyPort < 1 || config.proxyPort > 65535) {
109
+ throw new Error(`Invalid proxy port: ${config.proxyPort}`);
110
+ }
111
+ if (!config.gateUrl.startsWith("http")) {
112
+ throw new Error(`Invalid gate URL: ${config.gateUrl}`);
113
+ }
114
+ if (!config.mintUrl.startsWith("http")) {
115
+ throw new Error(`Invalid mint URL: ${config.mintUrl}`);
116
+ }
117
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
118
+ }
119
+ export async function configExists() {
120
+ try {
121
+ await fs.access(CONFIG_PATH);
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /**
129
+ * Custom error classes for better error handling
130
+ */
131
+ export class ConfigError extends Error {
132
+ recoverable;
133
+ constructor(message, recoverable = false) {
134
+ super(message);
135
+ this.recoverable = recoverable;
136
+ this.name = "ConfigError";
137
+ }
138
+ }
139
+ export class NetworkError extends Error {
140
+ endpoint;
141
+ cause;
142
+ constructor(message, endpoint, cause) {
143
+ super(message);
144
+ this.endpoint = endpoint;
145
+ this.cause = cause;
146
+ this.name = "NetworkError";
147
+ }
148
+ }
149
+ export class WalletError extends Error {
150
+ code;
151
+ constructor(message, code) {
152
+ super(message);
153
+ this.code = code;
154
+ this.name = "WalletError";
155
+ }
156
+ }
157
+ // ── Failed token persistence (shared by proxy + recover) ──────────
158
+ /** Simple async mutex to serialize failed-token file writes */
159
+ let _failedTokenLock = Promise.resolve();
160
+ function withFailedTokenLock(fn) {
161
+ const prev = _failedTokenLock;
162
+ let resolve;
163
+ _failedTokenLock = new Promise((r) => { resolve = r; });
164
+ return prev.then(fn).finally(() => resolve());
165
+ }
166
+ export const FAILED_TOKENS_PATH = path.join(CONFIG_DIR, "failed-tokens.json");
167
+ export async function loadFailedTokens() {
168
+ try {
169
+ const raw = await fs.readFile(FAILED_TOKENS_PATH, "utf-8");
170
+ return JSON.parse(raw);
171
+ }
172
+ catch {
173
+ return { tokens: [] };
174
+ }
175
+ }
176
+ export async function saveFailedTokens(data) {
177
+ await fs.mkdir(path.dirname(FAILED_TOKENS_PATH), { recursive: true, mode: 0o700 });
178
+ await fs.writeFile(FAILED_TOKENS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
179
+ }
180
+ export async function appendFailedToken(token, type, error) {
181
+ return withFailedTokenLock(async () => {
182
+ const data = await loadFailedTokens();
183
+ data.tokens.push({ token, type, timestamp: Date.now(), error });
184
+ await saveFailedTokens(data);
185
+ });
186
+ }
187
+ // ── Transaction log (JSONL — one record per proxy request) ────────
188
+ export const TRANSACTIONS_LOG_PATH = path.join(CONFIG_DIR, "transactions.jsonl");
189
+ export async function appendTransaction(record) {
190
+ await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
191
+ await fs.appendFile(TRANSACTIONS_LOG_PATH, JSON.stringify(record) + "\n", { mode: 0o600 });
192
+ }
193
+ export async function loadTransactions(limit) {
194
+ try {
195
+ const raw = await fs.readFile(TRANSACTIONS_LOG_PATH, "utf-8");
196
+ const lines = raw.trim().split("\n").filter(Boolean);
197
+ const records = lines.map((l) => JSON.parse(l));
198
+ if (limit && limit > 0)
199
+ return records.slice(-limit);
200
+ return records;
201
+ }
202
+ catch {
203
+ return [];
204
+ }
205
+ }
206
+ // ── Connectivity checks (shared by init, setup, doctor, status) ───
207
+ export async function checkGateHealth(url) {
208
+ try {
209
+ const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(5000) });
210
+ return res.ok;
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ }
216
+ export async function checkMintHealth(url) {
217
+ try {
218
+ const res = await fetch(`${url}/v1/info`, { signal: AbortSignal.timeout(5000) });
219
+ return res.ok;
220
+ }
221
+ catch {
222
+ return false;
223
+ }
224
+ }
@@ -0,0 +1,2 @@
1
+ import type { Connector } from "./interface.js";
2
+ export declare const cursorConnector: Connector;
@@ -0,0 +1,28 @@
1
+ export const cursorConnector = {
2
+ id: "cursor",
3
+ name: "Cursor",
4
+ description: "Configure Cursor IDE (coming soon)",
5
+ async detect() {
6
+ // TODO: Detect Cursor installation
7
+ // Could check for:
8
+ // - ~/Library/Application Support/Cursor (macOS)
9
+ // - ~/.config/Cursor (Linux)
10
+ // - %APPDATA%\Cursor (Windows)
11
+ return false;
12
+ },
13
+ async connect(_config) {
14
+ console.log("\nšŸŽŸļø Cursor Integration\n");
15
+ console.log(" Cursor connector coming soon!\n");
16
+ console.log(" In the meantime, you can configure Cursor manually:\n");
17
+ console.log(" 1. Open Cursor Settings (Cmd/Ctrl + ,)");
18
+ console.log(" 2. Set OpenAI Base URL:");
19
+ console.log(` http://127.0.0.1:${_config.proxyPort}/v1\n`);
20
+ console.log(" 3. Set OpenAI API Key:");
21
+ console.log(" ecash-proxy\n");
22
+ console.log(" 4. Use model:");
23
+ console.log(" anthropic/claude-sonnet-4\n");
24
+ },
25
+ async verify() {
26
+ return false;
27
+ },
28
+ };
@@ -0,0 +1,2 @@
1
+ import type { Connector } from "./interface.js";
2
+ export declare const envConnector: Connector;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Environment Variables Connector
3
+ *
4
+ * Outputs environment variables for use with any OpenAI-compatible tool.
5
+ */
6
+ import { loadOrCreateProxySecret } from "../config.js";
7
+ export const envConnector = {
8
+ id: "env",
9
+ name: "Environment Variables",
10
+ description: "Show environment variables for OpenAI-compatible tools",
11
+ async detect() {
12
+ // Always available
13
+ return true;
14
+ },
15
+ async connect(config) {
16
+ const baseUrl = `http://127.0.0.1:${config.proxyPort}/v1`;
17
+ const apiKey = await loadOrCreateProxySecret();
18
+ console.log("\nšŸŽŸļø Environment Variables\n");
19
+ console.log(" Add these to your shell profile (~/.bashrc, ~/.zshrc):\n");
20
+ console.log(` export OPENAI_API_BASE="${baseUrl}"`);
21
+ console.log(` export OPENAI_BASE_URL="${baseUrl}"`);
22
+ console.log(` export OPENAI_API_KEY="${apiKey}"\n`);
23
+ console.log(" Or set them in a .env file:\n");
24
+ console.log(` OPENAI_API_BASE=${baseUrl}`);
25
+ console.log(` OPENAI_BASE_URL=${baseUrl}`);
26
+ console.log(` OPENAI_API_KEY=${apiKey}\n`);
27
+ console.log(" Compatible with:\n");
28
+ console.log(" - LangChain / LangGraph");
29
+ console.log(" - LlamaIndex");
30
+ console.log(" - Aider");
31
+ console.log(" - Continue.dev");
32
+ console.log(" - Any OpenAI SDK-based tool\n");
33
+ },
34
+ async verify() {
35
+ const secret = await loadOrCreateProxySecret();
36
+ return process.env.OPENAI_API_KEY === secret;
37
+ },
38
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Connectors index
3
+ *
4
+ * Registry of all available connectors for t2c connect
5
+ */
6
+ export type { Connector } from "./interface.js";
7
+ export { openclawConnector } from "./openclaw.js";
8
+ export { cursorConnector } from "./cursor.js";
9
+ export { envConnector } from "./env.js";
10
+ import type { Connector } from "./interface.js";
11
+ /**
12
+ * Registry of all available connectors.
13
+ * Maps connector ID to connector instance.
14
+ */
15
+ export declare const connectors: Map<string, Connector>;
16
+ /**
17
+ * Get a connector by its ID.
18
+ * @param id - The unique identifier of the connector
19
+ * @returns The connector instance, or undefined if not found
20
+ */
21
+ export declare function getConnector(id: string): Connector | undefined;
22
+ /**
23
+ * List all available connector IDs.
24
+ * @returns Array of connector IDs
25
+ */
26
+ export declare function listConnectorIds(): string[];
@@ -0,0 +1,30 @@
1
+ export { openclawConnector } from "./openclaw.js";
2
+ export { cursorConnector } from "./cursor.js";
3
+ export { envConnector } from "./env.js";
4
+ import { openclawConnector } from "./openclaw.js";
5
+ import { cursorConnector } from "./cursor.js";
6
+ import { envConnector } from "./env.js";
7
+ /**
8
+ * Registry of all available connectors.
9
+ * Maps connector ID to connector instance.
10
+ */
11
+ export const connectors = new Map([
12
+ [openclawConnector.id, openclawConnector],
13
+ [cursorConnector.id, cursorConnector],
14
+ [envConnector.id, envConnector],
15
+ ]);
16
+ /**
17
+ * Get a connector by its ID.
18
+ * @param id - The unique identifier of the connector
19
+ * @returns The connector instance, or undefined if not found
20
+ */
21
+ export function getConnector(id) {
22
+ return connectors.get(id);
23
+ }
24
+ /**
25
+ * List all available connector IDs.
26
+ * @returns Array of connector IDs
27
+ */
28
+ export function listConnectorIds() {
29
+ return Array.from(connectors.keys());
30
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Connector interface for t2c connect command
3
+ *
4
+ * Connectors handle integration with specific AI tools.
5
+ */
6
+ import type { T2CConfig } from "../config.js";
7
+ export interface Connector {
8
+ /** Unique identifier (used in CLI) */
9
+ id: string;
10
+ /** Human-readable name */
11
+ name: string;
12
+ /** Description for --help output */
13
+ description?: string;
14
+ /** Check if this tool is installed/available */
15
+ detect(): Promise<boolean>;
16
+ /** Connect t2c to this tool (configure, patch, etc.) */
17
+ connect(config: T2CConfig): Promise<void>;
18
+ /** Verify the connection works (optional) */
19
+ verify?(): Promise<boolean>;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Connector } from "./interface.js";
2
+ export declare const openclawConnector: Connector;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * OpenClaw Connector
3
+ *
4
+ * Patches OpenClaw config to add Token2Chat plugin and models provider.
5
+ */
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import os from "node:os";
9
+ import { loadOrCreateProxySecret } from "../config.js";
10
+ /**
11
+ * Default models when Gate is unreachable.
12
+ * IDs use dash format (proxy transforms to slash for Gate/OpenRouter).
13
+ */
14
+ const DEFAULT_MODELS = [
15
+ { id: "openai-gpt-4o-mini", name: "GPT-4o Mini" },
16
+ { id: "openai-gpt-4o", name: "GPT-4o" },
17
+ { id: "anthropic-claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
18
+ { id: "anthropic-claude-opus-4-20250514", name: "Claude Opus 4" },
19
+ ];
20
+ /**
21
+ * Fetch available models from Gate pricing endpoint.
22
+ * Returns model entries in OpenClaw dash format.
23
+ */
24
+ async function fetchGateModels(gateUrl) {
25
+ try {
26
+ const res = await fetch(`${gateUrl}/v1/pricing`, {
27
+ signal: AbortSignal.timeout(5000),
28
+ });
29
+ if (!res.ok)
30
+ return [];
31
+ const data = (await res.json());
32
+ if (!data.models)
33
+ return [];
34
+ return Object.keys(data.models)
35
+ .filter((id) => id !== "*") // skip wildcard
36
+ .map((id) => ({
37
+ // Transform slash to dash for OpenClaw (e.g. "anthropic/claude-sonnet-4" → "anthropic-claude-sonnet-4")
38
+ id: id.replace("/", "-"),
39
+ name: id.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) ?? id,
40
+ }));
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ const OPENCLAW_CONFIG_DIR = ".openclaw";
47
+ // OpenClaw may use either filename (openclaw.json is newer, clawdbot.json is legacy)
48
+ const OPENCLAW_CONFIG_FILES = ["openclaw.json", "clawdbot.json"];
49
+ /**
50
+ * Find the first existing config file, or return the preferred default.
51
+ */
52
+ async function getConfigPath() {
53
+ const dir = path.join(os.homedir(), OPENCLAW_CONFIG_DIR);
54
+ for (const file of OPENCLAW_CONFIG_FILES) {
55
+ const p = path.join(dir, file);
56
+ try {
57
+ await fs.access(p);
58
+ return p;
59
+ }
60
+ catch {
61
+ // try next
62
+ }
63
+ }
64
+ // Default to the first (preferred) filename if none exist
65
+ return path.join(dir, OPENCLAW_CONFIG_FILES[0]);
66
+ }
67
+ /**
68
+ * Merge Token2Chat configuration into existing OpenClaw config.
69
+ * Handles:
70
+ * - Arrays and nested objects
71
+ * - Existing token2chat config (overwrites, doesn't duplicate)
72
+ */
73
+ function mergeToken2ChatConfig(existingConfig, t2cConfig, gateModels, apiKey) {
74
+ const config = { ...existingConfig };
75
+ // Ensure plugins.entries exists
76
+ if (!config.plugins || typeof config.plugins !== "object") {
77
+ config.plugins = {};
78
+ }
79
+ const plugins = config.plugins;
80
+ if (!plugins.entries || typeof plugins.entries !== "object") {
81
+ plugins.entries = {};
82
+ }
83
+ const entries = plugins.entries;
84
+ // Set/overwrite token2chat plugin config
85
+ entries.token2chat = {
86
+ enabled: true,
87
+ config: {
88
+ gateUrl: t2cConfig.gateUrl,
89
+ mintUrl: t2cConfig.mintUrl,
90
+ proxyPort: t2cConfig.proxyPort,
91
+ walletPath: t2cConfig.walletPath,
92
+ },
93
+ };
94
+ // Ensure models.providers exists
95
+ if (!config.models || typeof config.models !== "object") {
96
+ config.models = {};
97
+ }
98
+ const models = config.models;
99
+ if (!models.providers || typeof models.providers !== "object") {
100
+ models.providers = {};
101
+ }
102
+ const providers = models.providers;
103
+ // Set/overwrite token2chat provider config
104
+ providers.token2chat = {
105
+ baseUrl: `http://127.0.0.1:${t2cConfig.proxyPort}/v1`,
106
+ apiKey,
107
+ api: "openai-completions",
108
+ models: gateModels.length > 0 ? gateModels : DEFAULT_MODELS,
109
+ };
110
+ return config;
111
+ }
112
+ export const openclawConnector = {
113
+ id: "openclaw",
114
+ name: "OpenClaw",
115
+ description: "Configure OpenClaw gateway to use Token2Chat",
116
+ async detect() {
117
+ const dir = path.join(os.homedir(), OPENCLAW_CONFIG_DIR);
118
+ for (const file of OPENCLAW_CONFIG_FILES) {
119
+ try {
120
+ await fs.access(path.join(dir, file));
121
+ return true;
122
+ }
123
+ catch {
124
+ // try next
125
+ }
126
+ }
127
+ return false;
128
+ },
129
+ async connect(config) {
130
+ // Check if OpenClaw is installed
131
+ const detected = await this.detect();
132
+ if (!detected) {
133
+ const defaultPath = path.join(os.homedir(), OPENCLAW_CONFIG_DIR, OPENCLAW_CONFIG_FILES[0]);
134
+ console.error("āŒ OpenClaw not detected");
135
+ console.error(` Expected config at: ${defaultPath}`);
136
+ console.error("\n Install OpenClaw first: https://openclaw.dev\n");
137
+ return;
138
+ }
139
+ const configPath = await getConfigPath();
140
+ // Read existing config
141
+ let existingContent = "";
142
+ let existingConfig = {};
143
+ try {
144
+ existingContent = await fs.readFile(configPath, "utf-8");
145
+ existingConfig = JSON.parse(existingContent);
146
+ }
147
+ catch {
148
+ existingContent = "";
149
+ existingConfig = {};
150
+ }
151
+ // Load proxy secret for authentication
152
+ const apiKey = await loadOrCreateProxySecret();
153
+ // Fetch available models from Gate
154
+ console.log(" Fetching available models from Gate...");
155
+ const gateModels = await fetchGateModels(config.gateUrl);
156
+ if (gateModels.length > 0) {
157
+ console.log(` Found ${gateModels.length} models`);
158
+ }
159
+ else {
160
+ console.log(` Gate unreachable, using ${DEFAULT_MODELS.length} default models`);
161
+ }
162
+ // Merge our config
163
+ const mergedConfig = mergeToken2ChatConfig(existingConfig, config, gateModels, apiKey);
164
+ // Serialize to JSON with pretty formatting
165
+ const newContent = JSON.stringify(mergedConfig, null, 2) + "\n";
166
+ // Backup existing config
167
+ const backupPath = `${configPath}.backup.${Date.now()}`;
168
+ if (existingContent) {
169
+ await fs.writeFile(backupPath, existingContent);
170
+ }
171
+ // Write new config
172
+ await fs.writeFile(configPath, newContent);
173
+ console.log("āœ… OpenClaw configured for Token2Chat\n");
174
+ console.log(` Config: ${configPath}`);
175
+ if (existingContent) {
176
+ console.log(` Backup: ${backupPath}`);
177
+ }
178
+ console.log("\nšŸ“‹ Next steps:\n");
179
+ console.log(" 1. Restart OpenClaw gateway:");
180
+ console.log(" openclaw gateway restart\n");
181
+ console.log(" 2. Start the t2c proxy:");
182
+ console.log(" t2c service start\n");
183
+ console.log(" 3. Use Token2Chat models with:");
184
+ console.log(" token2chat/anthropic-claude-sonnet-4\n");
185
+ },
186
+ async verify() {
187
+ // Check if config has our entries
188
+ try {
189
+ const configPath = await getConfigPath();
190
+ const content = await fs.readFile(configPath, "utf-8");
191
+ const doc = JSON.parse(content);
192
+ const plugins = doc?.plugins;
193
+ const entries = plugins?.entries;
194
+ const models = doc?.models;
195
+ const providers = models?.providers;
196
+ return (entries?.token2chat !== undefined && providers?.token2chat !== undefined);
197
+ }
198
+ catch {
199
+ return false;
200
+ }
201
+ },
202
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Gate auto-discovery and failover.
3
+ *
4
+ * Fetches available Gates from token2.cash/gates.json,
5
+ * tracks health per Gate, and selects the best one per request.
6
+ */
7
+ export interface GateEntry {
8
+ name: string;
9
+ url: string;
10
+ mint: string;
11
+ providers: string[];
12
+ models: string[];
13
+ markup: string;
14
+ description: string;
15
+ }
16
+ export declare class GateRegistry {
17
+ private gates;
18
+ private health;
19
+ private lastFetch;
20
+ private primaryUrl;
21
+ private discoveryUrl;
22
+ constructor(primaryUrl: string, discoveryUrl?: string);
23
+ /**
24
+ * Fetch gates.json and update registry.
25
+ * Cached for CACHE_TTL_MS.
26
+ */
27
+ discover(): Promise<GateEntry[]>;
28
+ /**
29
+ * Select the best gate for a given model.
30
+ * Priority: primary gate > healthy gates that support the model > any healthy gate.
31
+ */
32
+ selectGate(model?: string): Promise<string[]>;
33
+ /**
34
+ * Mark a gate as having failed.
35
+ */
36
+ markFailed(gateUrl: string): void;
37
+ /**
38
+ * Mark a gate as having succeeded.
39
+ */
40
+ markSuccess(gateUrl: string): void;
41
+ /**
42
+ * Get list of all known gates with health status.
43
+ */
44
+ getAll(): Array<GateEntry & {
45
+ healthy: boolean;
46
+ }>;
47
+ private isHealthy;
48
+ private getHealth;
49
+ }