@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.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/adapters/aider.d.ts +5 -0
- package/dist/adapters/aider.js +29 -0
- package/dist/adapters/cline.d.ts +5 -0
- package/dist/adapters/cline.js +32 -0
- package/dist/adapters/continue.d.ts +5 -0
- package/dist/adapters/continue.js +45 -0
- package/dist/adapters/cursor.d.ts +5 -0
- package/dist/adapters/cursor.js +23 -0
- package/dist/adapters/env.d.ts +5 -0
- package/dist/adapters/env.js +25 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/openclaw.d.ts +2 -0
- package/dist/adapters/openclaw.js +167 -0
- package/dist/cashu-store.d.ts +52 -0
- package/dist/cashu-store.js +201 -0
- package/dist/commands/audit.d.ts +6 -0
- package/dist/commands/audit.js +340 -0
- package/dist/commands/balance.d.ts +5 -0
- package/dist/commands/balance.js +29 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +62 -0
- package/dist/commands/connect.d.ts +1 -0
- package/dist/commands/connect.js +43 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +178 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/mint.d.ts +5 -0
- package/dist/commands/mint.js +168 -0
- package/dist/commands/recover.d.ts +1 -0
- package/dist/commands/recover.js +61 -0
- package/dist/commands/service.d.ts +7 -0
- package/dist/commands/service.js +378 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +128 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +87 -0
- package/dist/config.d.ts +83 -0
- package/dist/config.js +224 -0
- package/dist/connectors/cursor.d.ts +2 -0
- package/dist/connectors/cursor.js +28 -0
- package/dist/connectors/env.d.ts +2 -0
- package/dist/connectors/env.js +38 -0
- package/dist/connectors/index.d.ts +26 -0
- package/dist/connectors/index.js +30 -0
- package/dist/connectors/interface.d.ts +20 -0
- package/dist/connectors/interface.js +1 -0
- package/dist/connectors/openclaw.d.ts +2 -0
- package/dist/connectors/openclaw.js +202 -0
- package/dist/gate-discovery.d.ts +49 -0
- package/dist/gate-discovery.js +142 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +177 -0
- package/dist/proxy.d.ts +11 -0
- package/dist/proxy.js +352 -0
- 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,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,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,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
|
+
}
|