routstrd 0.2.6 → 0.2.8
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/bun.lock +16 -217
- package/dist/daemon/index.js +471 -396
- package/dist/index.js +10238 -31672
- package/package.json +2 -1
- package/src/cli.ts +291 -208
- package/src/integrations/claudecode.ts +19 -40
- package/src/integrations/openclaw.ts +8 -34
- package/src/integrations/opencode.ts +8 -34
- package/src/integrations/pi.ts +7 -34
- package/src/integrations/registry.ts +4 -12
- package/src/tui/usage/data.ts +19 -7
- package/src/utils/clients.ts +304 -0
- package/src/utils/config.ts +2 -0
- package/src/utils/daemon-client.ts +84 -13
- package/src/utils/nip98.ts +102 -0
- package/src/daemon/http/index.ts +0 -1130
- package/src/daemon/index.ts +0 -242
- package/src/daemon/wallet/index.ts +0 -122
- package/src/index.ts +0 -4
- package/src/integrations/index.ts +0 -76
- package/src/tui/usage/index.ts +0 -1
package/src/daemon/index.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { createServer } from "http";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import {
|
|
4
|
-
ModelManager,
|
|
5
|
-
ProviderManager,
|
|
6
|
-
createDiscoveryAdapterFromStore,
|
|
7
|
-
createProviderRegistryFromStore,
|
|
8
|
-
createStorageAdapterFromStore,
|
|
9
|
-
createSdkStore,
|
|
10
|
-
} from "@routstr/sdk";
|
|
11
|
-
import { DB_PATH, SOCKET_PATH, PID_FILE } from "../utils/config";
|
|
12
|
-
import { logger } from "../utils/logger";
|
|
13
|
-
import { parseArgs } from "./args";
|
|
14
|
-
import { ensureDirs, loadDaemonConfig, saveDaemonConfig } from "./config-store";
|
|
15
|
-
import {
|
|
16
|
-
createBunSqliteDriver,
|
|
17
|
-
createBunSqliteUsageTrackingDriver,
|
|
18
|
-
} from "@routstr/sdk/storage";
|
|
19
|
-
import { createWalletAdapter } from "./wallet";
|
|
20
|
-
import { createCocodClient } from "./wallet/cocod-client";
|
|
21
|
-
import { createModelService } from "./models";
|
|
22
|
-
import { createDaemonRequestHandler } from "./http";
|
|
23
|
-
import { runIntegrationsForClients } from "../integrations";
|
|
24
|
-
import { RoutstrClient } from "@routstr/sdk";
|
|
25
|
-
|
|
26
|
-
async function main(): Promise<void> {
|
|
27
|
-
const args = parseArgs(process.argv);
|
|
28
|
-
const config = await loadDaemonConfig();
|
|
29
|
-
|
|
30
|
-
const port = args.port;
|
|
31
|
-
const provider = args.provider || config.provider;
|
|
32
|
-
|
|
33
|
-
await ensureDirs();
|
|
34
|
-
|
|
35
|
-
const updatedConfig = { ...config, port, provider };
|
|
36
|
-
saveDaemonConfig(updatedConfig);
|
|
37
|
-
|
|
38
|
-
const sqliteDriver = await createBunSqliteDriver(DB_PATH);
|
|
39
|
-
const { store } = await createSdkStore({ driver: sqliteDriver });
|
|
40
|
-
const { Database } = await import("bun:sqlite");
|
|
41
|
-
const usageTrackingDriver = createBunSqliteUsageTrackingDriver({
|
|
42
|
-
dbPath: DB_PATH,
|
|
43
|
-
sqlite: { Database },
|
|
44
|
-
legacyStorageDriver: sqliteDriver,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const discoveryAdapter = createDiscoveryAdapterFromStore(store);
|
|
48
|
-
const providerRegistry = createProviderRegistryFromStore(store);
|
|
49
|
-
const storageAdapter = createStorageAdapterFromStore(store);
|
|
50
|
-
const modelManager = new ModelManager(discoveryAdapter);
|
|
51
|
-
// Create shared ProviderManager for consistent failure tracking across all requests
|
|
52
|
-
const providerManager = new ProviderManager(providerRegistry, store);
|
|
53
|
-
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders } =
|
|
54
|
-
createModelService(modelManager);
|
|
55
|
-
|
|
56
|
-
const walletClient = createCocodClient({ cocodPath: config.cocodPath });
|
|
57
|
-
const walletAdapter = await createWalletAdapter({
|
|
58
|
-
cocodPath: config.cocodPath,
|
|
59
|
-
walletClient,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const server = createServer();
|
|
63
|
-
server.on(
|
|
64
|
-
"request",
|
|
65
|
-
createDaemonRequestHandler({
|
|
66
|
-
provider,
|
|
67
|
-
server,
|
|
68
|
-
store,
|
|
69
|
-
walletClient,
|
|
70
|
-
walletAdapter,
|
|
71
|
-
storageAdapter,
|
|
72
|
-
providerRegistry,
|
|
73
|
-
discoveryAdapter,
|
|
74
|
-
modelManager,
|
|
75
|
-
ensureProvidersBootstrapped,
|
|
76
|
-
getRoutstr21Models,
|
|
77
|
-
getModelProviders,
|
|
78
|
-
mode: config.mode || "apikeys",
|
|
79
|
-
usageTrackingDriver,
|
|
80
|
-
providerManager,
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
Bun.write(PID_FILE, String(process.pid));
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
if (existsSync(SOCKET_PATH)) {
|
|
88
|
-
Bun.spawn(["rm", SOCKET_PATH]);
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// Ignore
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const REFRESH_INTERVAL_MS = 21 * 60 * 1000; // 21 mins
|
|
95
|
-
|
|
96
|
-
// Recurring job to refresh routstr21 models
|
|
97
|
-
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
|
98
|
-
|
|
99
|
-
const startModelRefreshJob = () => {
|
|
100
|
-
logger.log(
|
|
101
|
-
`Starting recurring model refresh job (every ${REFRESH_INTERVAL_MS / 1000 / 60 / 60} hours)`,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
refreshInterval = setInterval(async () => {
|
|
105
|
-
logger.log("Running scheduled model refresh...");
|
|
106
|
-
try {
|
|
107
|
-
await getRoutstr21Models(true);
|
|
108
|
-
logger.log("Scheduled model refresh completed successfully.");
|
|
109
|
-
|
|
110
|
-
// Refresh integrations for all registered clients
|
|
111
|
-
const state = store.getState();
|
|
112
|
-
const clientIds = state.clientIds || [];
|
|
113
|
-
if (clientIds.length > 0) {
|
|
114
|
-
logger.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
115
|
-
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
116
|
-
logger.log("Client integrations refreshed.");
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
logger.error("Scheduled model refresh failed:", error);
|
|
120
|
-
}
|
|
121
|
-
}, REFRESH_INTERVAL_MS);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const stopModelRefreshJob = () => {
|
|
125
|
-
if (refreshInterval) {
|
|
126
|
-
clearInterval(refreshInterval);
|
|
127
|
-
refreshInterval = null;
|
|
128
|
-
logger.log("Stopped recurring model refresh job.");
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Recurring job to refund pending tokens every 42 minutes
|
|
133
|
-
const REFUND_INTERVAL_MS = 42 * 60 * 1000; // 42 minutes
|
|
134
|
-
let refundInterval: ReturnType<typeof setInterval> | null = null;
|
|
135
|
-
|
|
136
|
-
const startRefundJob = async () => {
|
|
137
|
-
logger.log(
|
|
138
|
-
`Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`,
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
refundInterval = setInterval(async () => {
|
|
142
|
-
logger.log("Running scheduled refund...");
|
|
143
|
-
try {
|
|
144
|
-
const state = store.getState() as any;
|
|
145
|
-
const pendingDistribution = (state.cachedTokens || []).map(
|
|
146
|
-
(t: { baseUrl: string; balance?: number }) => ({
|
|
147
|
-
baseUrl: t.baseUrl,
|
|
148
|
-
amount: t.balance || 0,
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
const apiKeysStored = (state.apiKeys || []).map(
|
|
152
|
-
(k: { baseUrl: string; balance?: number }) => ({
|
|
153
|
-
baseUrl: k.baseUrl,
|
|
154
|
-
amount: k.balance || 0,
|
|
155
|
-
}),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
|
159
|
-
logger.log("No pending tokens to refund.");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const mintUrl = walletAdapter.getActiveMintUrl();
|
|
164
|
-
if (!mintUrl) {
|
|
165
|
-
logger.log("No active mint URL for refund.");
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const client = new RoutstrClient(
|
|
170
|
-
walletAdapter,
|
|
171
|
-
storageAdapter,
|
|
172
|
-
providerRegistry,
|
|
173
|
-
"min",
|
|
174
|
-
"apikeys",
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const spender = client.getCashuSpender();
|
|
178
|
-
const results = await spender.refundProviders(mintUrl);
|
|
179
|
-
|
|
180
|
-
const successCount = results.filter(
|
|
181
|
-
(r: { success: boolean }) => r.success,
|
|
182
|
-
).length;
|
|
183
|
-
logger.log(
|
|
184
|
-
`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`,
|
|
185
|
-
);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
logger.error("Scheduled refund failed:", error);
|
|
188
|
-
}
|
|
189
|
-
}, REFUND_INTERVAL_MS);
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const stopRefundJob = () => {
|
|
193
|
-
if (refundInterval) {
|
|
194
|
-
clearInterval(refundInterval);
|
|
195
|
-
refundInterval = null;
|
|
196
|
-
logger.log("Stopped recurring refund job.");
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
server.on("close", () => {
|
|
201
|
-
stopModelRefreshJob();
|
|
202
|
-
stopRefundJob();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
server.listen(port, async () => {
|
|
206
|
-
logger.log(`Routstr daemon listening on http://localhost:${port}/v1`);
|
|
207
|
-
|
|
208
|
-
// Start the recurring model refresh job after initial bootstrap
|
|
209
|
-
void ensureProvidersBootstrapped()
|
|
210
|
-
.then(() => {
|
|
211
|
-
startModelRefreshJob();
|
|
212
|
-
startRefundJob();
|
|
213
|
-
// Run an immediate refresh to populate models right away
|
|
214
|
-
logger.log("Running initial model refresh...");
|
|
215
|
-
return getRoutstr21Models(true);
|
|
216
|
-
})
|
|
217
|
-
.then(async () => {
|
|
218
|
-
logger.log("Initial model refresh completed.");
|
|
219
|
-
// Refresh integrations for all registered clients after initial bootstrap
|
|
220
|
-
const state = store.getState();
|
|
221
|
-
const clientIds = state.clientIds || [];
|
|
222
|
-
if (clientIds.length > 0) {
|
|
223
|
-
logger.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
224
|
-
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
225
|
-
logger.log("Client integrations refreshed.");
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
.catch((error) => {
|
|
229
|
-
logger.error("Initial model refresh failed:", error);
|
|
230
|
-
// Still start the jobs even if initial refresh fails
|
|
231
|
-
startModelRefreshJob();
|
|
232
|
-
startRefundJob();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (import.meta.main) {
|
|
238
|
-
main().catch((error) => {
|
|
239
|
-
logger.error("Failed to start Routstr daemon:", error);
|
|
240
|
-
process.exit(1);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { getDecodedToken } from "@cashu/cashu-ts";
|
|
2
|
-
import { logger } from "../../utils/logger";
|
|
3
|
-
import { createCocodClient, type CocodClient } from "./cocod-client";
|
|
4
|
-
|
|
5
|
-
export function decodeCashuTokenAmount(token: string): {
|
|
6
|
-
amount: number;
|
|
7
|
-
unit: "sat" | "msat";
|
|
8
|
-
} {
|
|
9
|
-
const decoded = getDecodedToken(token);
|
|
10
|
-
const amount =
|
|
11
|
-
decoded?.proofs?.reduce((sum, proof) => sum + proof.amount, 0) ?? 0;
|
|
12
|
-
const unit = decoded?.unit === "msat" ? "msat" : "sat";
|
|
13
|
-
return { amount, unit };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function createWalletAdapter(
|
|
17
|
-
options: {
|
|
18
|
-
cocodPath?: string | null;
|
|
19
|
-
walletClient?: CocodClient;
|
|
20
|
-
} = {},
|
|
21
|
-
) {
|
|
22
|
-
const client =
|
|
23
|
-
options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
|
|
24
|
-
let activeMintUrl: string | null = null;
|
|
25
|
-
let mintUnits: Record<string, "sat" | "msat"> = {};
|
|
26
|
-
|
|
27
|
-
async function syncMintState(
|
|
28
|
-
balances?: Record<string, number>,
|
|
29
|
-
): Promise<Record<string, number>> {
|
|
30
|
-
const nextBalances = balances || (await client.getBalances());
|
|
31
|
-
|
|
32
|
-
mintUnits = Object.fromEntries(
|
|
33
|
-
Object.keys(nextBalances).map((mintUrl) => [mintUrl, "sat"]),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const mints = await client.listMints();
|
|
38
|
-
activeMintUrl = mints[0] || Object.keys(nextBalances)[0] || null;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
logger.error("Failed to list cocod mints:", error);
|
|
41
|
-
if (!activeMintUrl) {
|
|
42
|
-
activeMintUrl = Object.keys(nextBalances)[0] || null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return nextBalances;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const walletAdapter = {
|
|
50
|
-
async getBalances(): Promise<Record<string, number>> {
|
|
51
|
-
return syncMintState();
|
|
52
|
-
},
|
|
53
|
-
getMintUnits(): Record<string, "sat" | "msat"> {
|
|
54
|
-
return mintUnits;
|
|
55
|
-
},
|
|
56
|
-
getActiveMintUrl(): string | null {
|
|
57
|
-
return activeMintUrl;
|
|
58
|
-
},
|
|
59
|
-
async sendToken(mintUrl: string, amount: number): Promise<string> {
|
|
60
|
-
const maxRetries = 3;
|
|
61
|
-
const retryDelayMs = 5000;
|
|
62
|
-
const retryErrorPattern = "Proof already reserved by operation";
|
|
63
|
-
|
|
64
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
65
|
-
try {
|
|
66
|
-
return await client.sendCashu(amount, mintUrl);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
const errorMessage =
|
|
69
|
-
error instanceof Error ? error.message : String(error);
|
|
70
|
-
|
|
71
|
-
const shouldRetry =
|
|
72
|
-
attempt < maxRetries && errorMessage.includes(retryErrorPattern);
|
|
73
|
-
|
|
74
|
-
if (shouldRetry) {
|
|
75
|
-
logger.log(
|
|
76
|
-
`sendToken attempt ${attempt + 1} failed with reserved proof error, retrying in ${retryDelayMs / 1000}s...`,
|
|
77
|
-
);
|
|
78
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
logger.error("Error in walletAdapter sendToken:", error);
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
throw new Error("sendToken failed after max retries");
|
|
88
|
-
},
|
|
89
|
-
async receiveToken(token: string): Promise<{
|
|
90
|
-
success: boolean;
|
|
91
|
-
amount: number;
|
|
92
|
-
unit: "sat" | "msat";
|
|
93
|
-
message?: string;
|
|
94
|
-
}> {
|
|
95
|
-
try {
|
|
96
|
-
const message = await client.receiveCashu(token);
|
|
97
|
-
const { amount, unit } = decodeCashuTokenAmount(token);
|
|
98
|
-
return { success: true, amount, unit, message };
|
|
99
|
-
} catch (error) {
|
|
100
|
-
const errorMessage =
|
|
101
|
-
error instanceof Error ? error.message : String(error);
|
|
102
|
-
logger.error("Error in walletAdapter receiveToken:", errorMessage);
|
|
103
|
-
return { success: false, amount: 0, unit: "sat", message: errorMessage };
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const [balances, mints] = await Promise.all([
|
|
110
|
-
client.getBalances(),
|
|
111
|
-
client.listMints().catch(() => []),
|
|
112
|
-
]);
|
|
113
|
-
mintUnits = Object.fromEntries(
|
|
114
|
-
Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]),
|
|
115
|
-
);
|
|
116
|
-
activeMintUrl = mints[0] || Object.keys(balances)[0] || null;
|
|
117
|
-
} catch (error) {
|
|
118
|
-
logger.error("Failed to initialize wallet adapter state:", error);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return walletAdapter;
|
|
122
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { RoutstrdConfig } from "../utils/config";
|
|
2
|
-
import { logger } from "../utils/logger";
|
|
3
|
-
import { installOpencodeIntegration } from "./opencode";
|
|
4
|
-
import { installOpenClawIntegration } from "./openclaw";
|
|
5
|
-
import { installPiIntegration } from "./pi";
|
|
6
|
-
import { installClaudeCodeIntegration } from "./claudecode";
|
|
7
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
8
|
-
import { CLIENT_CONFIGS } from "./registry";
|
|
9
|
-
export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
|
|
10
|
-
|
|
11
|
-
function ask(question: string): Promise<string> {
|
|
12
|
-
process.stdout.write(question);
|
|
13
|
-
|
|
14
|
-
if (!process.stdin.isTTY) {
|
|
15
|
-
return Promise.resolve("1");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return new Promise((resolve) => {
|
|
19
|
-
process.stdin.resume();
|
|
20
|
-
process.stdin.setEncoding("utf8");
|
|
21
|
-
process.stdin.once("data", (data) => {
|
|
22
|
-
process.stdin.pause();
|
|
23
|
-
resolve(data.toString().trim());
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function parseChoice(input: string): number {
|
|
29
|
-
if (input === "") {
|
|
30
|
-
return 1;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const parsed = Number.parseInt(input, 10);
|
|
34
|
-
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 5) {
|
|
35
|
-
return parsed;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function setupIntegration(
|
|
42
|
-
config: RoutstrdConfig,
|
|
43
|
-
store: SdkStore,
|
|
44
|
-
): Promise<void> {
|
|
45
|
-
logger.log("\nChoose an integration to set up:");
|
|
46
|
-
logger.log("1. OpenCode (default)");
|
|
47
|
-
logger.log("2. OpenClaw");
|
|
48
|
-
logger.log("3. Pi");
|
|
49
|
-
logger.log("4. Claude Code");
|
|
50
|
-
logger.log("5. Skip for now");
|
|
51
|
-
|
|
52
|
-
const answer = await ask("Select integration [1]: ");
|
|
53
|
-
const choice = parseChoice(answer);
|
|
54
|
-
|
|
55
|
-
if (choice === 1) {
|
|
56
|
-
await installOpencodeIntegration(config, store, CLIENT_CONFIGS.opencode!);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (choice === 2) {
|
|
61
|
-
await installOpenClawIntegration(config, store, CLIENT_CONFIGS.openclaw!);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (choice === 3) {
|
|
66
|
-
await installPiIntegration(config, store, CLIENT_CONFIGS["pi-agent"]!);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (choice === 4) {
|
|
71
|
-
await installClaudeCodeIntegration(config, store, CLIENT_CONFIGS["claude-code"]!);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
logger.log("Skipping integration setup.");
|
|
76
|
-
}
|
package/src/tui/usage/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { runUsageTui } from "./app.ts";
|