routstrd 0.4.5 → 0.4.7
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/README.md +14 -0
- package/dist/daemon/index.js +68073 -77087
- package/dist/index.js +235 -192
- package/package.json +9 -2
- package/src/cli.ts +63 -15
- package/src/daemon/http/index.ts +1857 -0
- package/src/daemon/index.ts +408 -0
- package/src/daemon/wallet/index.ts +360 -0
- package/src/index.ts +4 -0
- package/src/integrations/index.ts +159 -0
- package/src/tui/usage/index.ts +1 -0
- package/.claude/settings.local.json +0 -10
- package/.dockerignore +0 -12
- package/COCO-2.0.0-MIGRATION-PLAN.md +0 -1191
- package/Dockerfile +0 -11
- package/IMPLEMENTATION.md +0 -253
- package/SKILL.md +0 -324
- package/bun.lock +0 -808
- package/docker-compose.yml +0 -15
- package/refund.js +0 -33
- package/refund_new.js +0 -20
- package/routstr-cost-logging.md +0 -71
- package/test_box.ts +0 -15
- package/test_chat.sh +0 -29
- package/test_curl.sh +0 -11
- package/test_split_box.ts +0 -17
- package/test_split_box2.ts +0 -23
- package/tests/daemon/config-store.perms.test.ts +0 -55
- package/tests/daemon/config-store.scenario.ts +0 -113
- package/tests/integrations/hermes.test.ts +0 -134
- package/tests/utils/daemon-client.test.ts +0 -61
- package/tests/wallet/short-keyset-token.test.ts +0 -106
- package/tsconfig.json +0 -20
- package/v1-messages-format-report.md +0 -223
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { getTokenMetadata } from "@cashu/cashu-ts";
|
|
2
|
+
import { InsufficientBalanceError } from "@routstr/sdk";
|
|
3
|
+
import { WalletConnect } from "applesauce-wallet-connect";
|
|
4
|
+
import { RelayPool } from "applesauce-relay";
|
|
5
|
+
import { logger } from "../../utils/logger";
|
|
6
|
+
import { createCocodClient, type CocodClient } from "./cocod-client";
|
|
7
|
+
import { startAutoRefillLoop, type AutoRefillConfig } from "./auto-refill";
|
|
8
|
+
|
|
9
|
+
export function decodeCashuTokenAmount(token: string): {
|
|
10
|
+
amount: number;
|
|
11
|
+
unit: "sat" | "msat";
|
|
12
|
+
} {
|
|
13
|
+
// TokenV4 may contain an 8-byte short keyset ID. Fully decoding its
|
|
14
|
+
// proofs requires the mint's full keyset IDs, but amount and unit do not.
|
|
15
|
+
// getTokenMetadata intentionally extracts those fields without trying to
|
|
16
|
+
// map short IDs, unlike getDecodedToken(token, []).
|
|
17
|
+
const metadata = getTokenMetadata(token);
|
|
18
|
+
const amount = metadata.amount.toNumber();
|
|
19
|
+
const unit = metadata.unit === "msat" ? "msat" : "sat";
|
|
20
|
+
return { amount, unit };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function receiveCashuToken(
|
|
24
|
+
client: Pick<CocodClient, "receiveCashu">,
|
|
25
|
+
token: string,
|
|
26
|
+
): Promise<{ message: string; amount: number; unit: "sat" | "msat" }> {
|
|
27
|
+
// Validate the token before handing it to a state-changing wallet call. This
|
|
28
|
+
// prevents a successful receive from being reported as a failure if local
|
|
29
|
+
// metadata parsing ever rejects a future token format.
|
|
30
|
+
const { amount, unit } = decodeCashuTokenAmount(token);
|
|
31
|
+
const message = await client.receiveCashu(token);
|
|
32
|
+
return { message, amount, unit };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WalletAdapterOptions {
|
|
36
|
+
cocodPath?: string | null;
|
|
37
|
+
walletClient?: CocodClient;
|
|
38
|
+
/** NWC connection string for Lightning funding (uses applesauce-wallet-connect) */
|
|
39
|
+
nwcConnectionString?: string;
|
|
40
|
+
/** Auto-refill configuration (static, for startup only) */
|
|
41
|
+
autoRefill?: AutoRefillConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Config getter called on every check cycle to allow live updates.
|
|
44
|
+
* Return undefined to disable auto-refill, or a config to use.
|
|
45
|
+
* When provided, this replaces the static `autoRefill` option.
|
|
46
|
+
*/
|
|
47
|
+
getAutoRefillConfig?: () => AutoRefillConfig | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function createWalletAdapter(
|
|
51
|
+
options: WalletAdapterOptions = {},
|
|
52
|
+
) {
|
|
53
|
+
const client =
|
|
54
|
+
options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
|
|
55
|
+
let activeMintUrl: string | null = null;
|
|
56
|
+
let mintUnits: Record<string, "sat" | "msat"> = {};
|
|
57
|
+
|
|
58
|
+
async function syncMintState(
|
|
59
|
+
balances?: Record<string, number>,
|
|
60
|
+
): Promise<Record<string, number>> {
|
|
61
|
+
const nextBalances = balances || (await client.getBalances());
|
|
62
|
+
|
|
63
|
+
mintUnits = Object.fromEntries(
|
|
64
|
+
Object.keys(nextBalances).map((mintUrl) => [mintUrl, "sat"]),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Use default mint as active mint, fall back to first mint in list
|
|
69
|
+
const defaultMint = await client.getDefaultMint();
|
|
70
|
+
activeMintUrl = defaultMint || Object.keys(nextBalances)[0] || null;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error("Failed to get default mint:", error);
|
|
73
|
+
if (!activeMintUrl) {
|
|
74
|
+
activeMintUrl = Object.keys(nextBalances)[0] || null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return nextBalances;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── NWC connection (applesauce approach) ──────────────────────
|
|
82
|
+
|
|
83
|
+
let wallet: WalletConnect | undefined;
|
|
84
|
+
let pool: RelayPool | undefined;
|
|
85
|
+
|
|
86
|
+
// Getter for the current wallet instance (used by auto-refill loop)
|
|
87
|
+
const getWallet = (): WalletConnect | undefined => wallet;
|
|
88
|
+
|
|
89
|
+
if (options.nwcConnectionString) {
|
|
90
|
+
pool = new RelayPool();
|
|
91
|
+
wallet = WalletConnect.fromConnectURI(options.nwcConnectionString, { pool });
|
|
92
|
+
|
|
93
|
+
// Connect in background (non-blocking)
|
|
94
|
+
wallet.waitForService()
|
|
95
|
+
.then(() => {
|
|
96
|
+
logger.log(
|
|
97
|
+
`[nwc] NWC wallet connected. Relay: ${wallet!.relays[0]}, Service: ${wallet!.service}`,
|
|
98
|
+
);
|
|
99
|
+
})
|
|
100
|
+
.catch((err) => {
|
|
101
|
+
logger.error(`[nwc] NWC connection failed: ${err.message}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const walletAdapter = {
|
|
106
|
+
async reconnect(connectionString?: string): Promise<void> {
|
|
107
|
+
logger.log(
|
|
108
|
+
`[nwc] Reconnecting NWC wallet... ${connectionString ? "new connection string provided" : "disconnecting"}`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// 1. Close existing relay pool connections
|
|
112
|
+
if (pool) {
|
|
113
|
+
for (const [url] of pool.relays) {
|
|
114
|
+
pool.remove(url, true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. Update wallet reference
|
|
119
|
+
wallet = undefined;
|
|
120
|
+
pool = undefined;
|
|
121
|
+
|
|
122
|
+
// 3. Create new wallet if connection string provided
|
|
123
|
+
if (connectionString) {
|
|
124
|
+
pool = new RelayPool();
|
|
125
|
+
wallet = WalletConnect.fromConnectURI(connectionString, { pool });
|
|
126
|
+
|
|
127
|
+
// Connect in background (non-blocking)
|
|
128
|
+
wallet.waitForService()
|
|
129
|
+
.then(() => {
|
|
130
|
+
logger.log(
|
|
131
|
+
`[nwc] NWC wallet reconnected. Relay: ${wallet!.relays[0]}, Service: ${wallet!.service}`,
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
.catch((err) => {
|
|
135
|
+
logger.error(`[nwc] NWC reconnection failed: ${err.message}`);
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
logger.log("[nwc] NWC wallet disconnected.");
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async getBalances(): Promise<Record<string, number>> {
|
|
143
|
+
return syncMintState();
|
|
144
|
+
},
|
|
145
|
+
getMintUnits(): Record<string, "sat" | "msat"> {
|
|
146
|
+
return mintUnits;
|
|
147
|
+
},
|
|
148
|
+
getActiveMintUrl(): string | null {
|
|
149
|
+
return activeMintUrl;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ── NWC funding methods ────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** Fund the Cashu wallet from NWC by creating & paying a BOLT-11 invoice */
|
|
155
|
+
async fundFromNWC(amount: number): Promise<{
|
|
156
|
+
success: boolean;
|
|
157
|
+
invoice: string;
|
|
158
|
+
preimage?: string;
|
|
159
|
+
error?: string;
|
|
160
|
+
}> {
|
|
161
|
+
logger.log("=".repeat(50));
|
|
162
|
+
logger.log(`[nwc] Fund Cashu wallet from NWC — amount: ${amount} sats`);
|
|
163
|
+
logger.log("=".repeat(50));
|
|
164
|
+
|
|
165
|
+
if (!wallet || !wallet.service) {
|
|
166
|
+
logger.error("[nwc] NWC not connected");
|
|
167
|
+
return { success: false, invoice: "", error: "NWC not connected" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use default mint for NWC funding
|
|
171
|
+
const defaultMint = await client.getDefaultMint();
|
|
172
|
+
const mintUrl = defaultMint;
|
|
173
|
+
if (!mintUrl) {
|
|
174
|
+
logger.error("[nwc] No default mint configured");
|
|
175
|
+
return { success: false, invoice: "", error: "No default mint configured" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// Step 1: Check initial balance
|
|
180
|
+
logger.log(`[nwc] Checking initial cocod balance on mint ${mintUrl}...`);
|
|
181
|
+
let initialBalance: number | null = null;
|
|
182
|
+
try {
|
|
183
|
+
const balances = await client.getBalances();
|
|
184
|
+
initialBalance = balances[mintUrl] ?? 0;
|
|
185
|
+
logger.log(`[nwc] Initial balance: ${initialBalance} sats`);
|
|
186
|
+
} catch {
|
|
187
|
+
logger.log("[nwc] Could not retrieve initial balance");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 2: Create a BOLT-11 invoice via cocod
|
|
191
|
+
logger.log(`[nwc] Creating ${amount}-sat Lightning invoice via cocod...`);
|
|
192
|
+
const invoice = await client.receiveBolt11(amount, mintUrl);
|
|
193
|
+
logger.log(`[nwc] Invoice: ${invoice}`);
|
|
194
|
+
|
|
195
|
+
// Step 3: Pay it via NWC
|
|
196
|
+
logger.log("[nwc] Paying invoice via NWC...");
|
|
197
|
+
const { preimage, fees_paid } = await wallet.payInvoice(invoice);
|
|
198
|
+
logger.log(`[nwc] ✅ Payment successful!`);
|
|
199
|
+
logger.log(`[nwc] Preimage: ${preimage}`);
|
|
200
|
+
if (fees_paid !== undefined) {
|
|
201
|
+
logger.log(`[nwc] Fees paid: ${fees_paid} msats`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Step 4: Check final balance
|
|
205
|
+
logger.log("[nwc] Checking final cocod balance...");
|
|
206
|
+
try {
|
|
207
|
+
const balances = await client.getBalances();
|
|
208
|
+
const finalBalance = balances[mintUrl] ?? 0;
|
|
209
|
+
logger.log(`[nwc] Final balance: ${finalBalance} sats`);
|
|
210
|
+
if (initialBalance !== null) {
|
|
211
|
+
const diff = finalBalance - initialBalance;
|
|
212
|
+
logger.log(`[nwc] Balance change: ${diff > 0 ? "+" : ""}${diff} sats`);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
logger.log("[nwc] Could not retrieve final balance");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
logger.log("=".repeat(50));
|
|
219
|
+
return { success: true, invoice, preimage };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
logger.error(`[nwc] ❌ Fund from NWC failed: ${message}`);
|
|
223
|
+
logger.log("=".repeat(50));
|
|
224
|
+
return { success: false, invoice: "", error: message };
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/** Get NWC connection status and wallet info */
|
|
229
|
+
async getNwcStatus(): Promise<{
|
|
230
|
+
connected: boolean;
|
|
231
|
+
alias?: string;
|
|
232
|
+
pubkey?: string;
|
|
233
|
+
network?: string;
|
|
234
|
+
methods?: string[];
|
|
235
|
+
balance?: number;
|
|
236
|
+
error?: string;
|
|
237
|
+
}> {
|
|
238
|
+
if (!wallet) {
|
|
239
|
+
return { connected: false, error: "NWC not configured" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!wallet.service) {
|
|
243
|
+
return { connected: false, error: "NWC not connected" };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const info = await wallet.getInfo();
|
|
248
|
+
let balance: number | undefined;
|
|
249
|
+
try {
|
|
250
|
+
const bal = await wallet.getBalance();
|
|
251
|
+
balance = Math.floor(bal.balance / 1000); // msats → sats
|
|
252
|
+
} catch {
|
|
253
|
+
// Balance might not be available
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
connected: true,
|
|
257
|
+
alias: info.alias,
|
|
258
|
+
pubkey: info.pubkey,
|
|
259
|
+
network: info.network,
|
|
260
|
+
methods: info.methods,
|
|
261
|
+
balance,
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
return { connected: false, error: message };
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/** Get the current auto-refill config, re-reading from the getter if available */
|
|
270
|
+
getAutoRefillConfig(): AutoRefillConfig | undefined {
|
|
271
|
+
return options.getAutoRefillConfig?.() ?? options.autoRefill;
|
|
272
|
+
},
|
|
273
|
+
async sendToken(mintUrl: string, amount: number): Promise<string> {
|
|
274
|
+
const maxRetries = 3;
|
|
275
|
+
const retryDelayMs = 5000;
|
|
276
|
+
const retryErrorPattern = "Proof already reserved by operation";
|
|
277
|
+
|
|
278
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
279
|
+
try {
|
|
280
|
+
return await client.sendCashu(amount, mintUrl);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const errorMessage =
|
|
283
|
+
error instanceof Error ? error.message : String(error);
|
|
284
|
+
|
|
285
|
+
const shouldRetry =
|
|
286
|
+
attempt < maxRetries && errorMessage.includes(retryErrorPattern);
|
|
287
|
+
|
|
288
|
+
if (shouldRetry) {
|
|
289
|
+
logger.log(
|
|
290
|
+
`sendToken attempt ${attempt + 1} failed with reserved proof error, retrying in ${retryDelayMs / 1000}s...`,
|
|
291
|
+
);
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (errorMessage.includes("Not enough proofs")) {
|
|
297
|
+
throw new InsufficientBalanceError(amount, 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
logger.error("Error in walletAdapter sendToken:", error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
throw new Error("sendToken failed after max retries");
|
|
306
|
+
},
|
|
307
|
+
async receiveToken(token: string): Promise<{
|
|
308
|
+
success: boolean;
|
|
309
|
+
amount: number;
|
|
310
|
+
unit: "sat" | "msat";
|
|
311
|
+
message?: string;
|
|
312
|
+
}> {
|
|
313
|
+
try {
|
|
314
|
+
const { amount, unit, message } = await receiveCashuToken(client, token);
|
|
315
|
+
return { success: true, amount, unit, message };
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const errorMessage =
|
|
318
|
+
error instanceof Error ? error.message : String(error);
|
|
319
|
+
logger.error("Error in walletAdapter receiveToken:", errorMessage);
|
|
320
|
+
return { success: false, amount: 0, unit: "sat", message: errorMessage };
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ── Auto-refill setup ────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
let stopAutoRefill: (() => void) | undefined;
|
|
328
|
+
|
|
329
|
+
const autoRefillConfig = options.getAutoRefillConfig
|
|
330
|
+
? options.getAutoRefillConfig()
|
|
331
|
+
: options.autoRefill;
|
|
332
|
+
|
|
333
|
+
if (autoRefillConfig && wallet) {
|
|
334
|
+
const getConfig = options.getAutoRefillConfig ?? (() => options.autoRefill);
|
|
335
|
+
stopAutoRefill = startAutoRefillLoop(client, getWallet, getConfig);
|
|
336
|
+
logger.log(
|
|
337
|
+
`[wallet] Auto-refill enabled: threshold=${autoRefillConfig.threshold} sats, amount=${autoRefillConfig.amount} sats, cooldown=${autoRefillConfig.cooldownMs / 60000} minutes`,
|
|
338
|
+
);
|
|
339
|
+
} else if (wallet && options.getAutoRefillConfig) {
|
|
340
|
+
// Wallet exists but auto-refill is not currently enabled.
|
|
341
|
+
// Start the loop anyway so it can pick up changes without a restart.
|
|
342
|
+
stopAutoRefill = startAutoRefillLoop(client, getWallet, options.getAutoRefillConfig);
|
|
343
|
+
logger.log("[wallet] Auto-refill loop started (currently disabled — enable via CLI to activate)");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const [balances, defaultMint] = await Promise.all([
|
|
348
|
+
client.getBalances(),
|
|
349
|
+
client.getDefaultMint().catch(() => null),
|
|
350
|
+
]);
|
|
351
|
+
mintUnits = Object.fromEntries(
|
|
352
|
+
Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]),
|
|
353
|
+
);
|
|
354
|
+
activeMintUrl = defaultMint || Object.keys(balances)[0] || null;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.error("Failed to initialize wallet adapter state:", error);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return walletAdapter;
|
|
360
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
2
|
+
import { logger } from "../utils/logger";
|
|
3
|
+
import {
|
|
4
|
+
addDaemonClient,
|
|
5
|
+
type DaemonClient,
|
|
6
|
+
} from "../utils/clients";
|
|
7
|
+
import { getClientsList } from "../utils/clients";
|
|
8
|
+
import { installOpencodeIntegration } from "./opencode";
|
|
9
|
+
import { installOpenClawIntegration } from "./openclaw";
|
|
10
|
+
import { installPiIntegration } from "./pi";
|
|
11
|
+
import { installClaudeCodeIntegration } from "./claudecode";
|
|
12
|
+
import { installHermesIntegration } from "./hermes";
|
|
13
|
+
import type { IntegrationConfig } from "./registry";
|
|
14
|
+
import { CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
|
|
15
|
+
export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Refresh routstr21 models and then run integrations for all registered clients.
|
|
19
|
+
* Used both on initial daemon startup and in the recurring scheduled job.
|
|
20
|
+
*/
|
|
21
|
+
export async function refreshModelsAndIntegrations(
|
|
22
|
+
getRoutstr21Models: (force?: boolean) => Promise<any[]>,
|
|
23
|
+
config: RoutstrdConfig,
|
|
24
|
+
label: string = "Scheduled",
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
await getRoutstr21Models(true);
|
|
27
|
+
logger.log(`${label} model refresh completed successfully.`);
|
|
28
|
+
|
|
29
|
+
const clientIds = await getClientsList();
|
|
30
|
+
if (clientIds.length > 0) {
|
|
31
|
+
logger.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
32
|
+
await runIntegrationsForClients(clientIds, config);
|
|
33
|
+
logger.log("Client integrations refreshed.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ask(question: string): Promise<string> {
|
|
38
|
+
process.stdout.write(question);
|
|
39
|
+
|
|
40
|
+
if (!process.stdin.isTTY) {
|
|
41
|
+
return Promise.resolve("1");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
process.stdin.resume();
|
|
46
|
+
process.stdin.setEncoding("utf8");
|
|
47
|
+
process.stdin.once("data", (data) => {
|
|
48
|
+
process.stdin.pause();
|
|
49
|
+
resolve(data.toString().trim());
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseChoice(input: string): number {
|
|
55
|
+
if (input === "") {
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parsed = Number.parseInt(input, 10);
|
|
60
|
+
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 6) {
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Either a client integration key (as used in CLIENT_CONFIGS) or "skip".
|
|
69
|
+
*/
|
|
70
|
+
export type IntegrationKey = keyof typeof CLIENT_CONFIGS | "skip";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create/find the API key for a client and run its install integration.
|
|
74
|
+
* Shared by the interactive menu in setupIntegration and direct onboarding
|
|
75
|
+
* via `routstrd onboard --<client>` flags.
|
|
76
|
+
*/
|
|
77
|
+
async function installIntegrationByKey(
|
|
78
|
+
config: RoutstrdConfig,
|
|
79
|
+
key: keyof typeof CLIENT_CONFIGS,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const integrationConfig = CLIENT_CONFIGS[key];
|
|
82
|
+
if (!integrationConfig) {
|
|
83
|
+
console.log(`Unknown integration: ${key}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { client, created } = await addDaemonClient(
|
|
88
|
+
integrationConfig.name,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (created) {
|
|
92
|
+
console.log(`Created new API key for ${integrationConfig.name}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`Using existing API key for ${integrationConfig.name}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (key) {
|
|
98
|
+
case "opencode":
|
|
99
|
+
await installOpencodeIntegration(config, client.apiKey, integrationConfig);
|
|
100
|
+
return;
|
|
101
|
+
case "openclaw":
|
|
102
|
+
await installOpenClawIntegration(config, client.apiKey, integrationConfig);
|
|
103
|
+
return;
|
|
104
|
+
case "pi-agent":
|
|
105
|
+
await installPiIntegration(config, client.apiKey, integrationConfig);
|
|
106
|
+
return;
|
|
107
|
+
case "claude-code":
|
|
108
|
+
await installClaudeCodeIntegration(config, client.apiKey, integrationConfig);
|
|
109
|
+
return;
|
|
110
|
+
case "hermes":
|
|
111
|
+
await installHermesIntegration(config, client.apiKey, integrationConfig);
|
|
112
|
+
return;
|
|
113
|
+
default:
|
|
114
|
+
console.log(`Unknown integration: ${key}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function setupIntegration(
|
|
119
|
+
config: RoutstrdConfig,
|
|
120
|
+
integrationKey?: IntegrationKey,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
// Non-interactive selection (e.g. `routstrd onboard --pi-agent`).
|
|
123
|
+
if (integrationKey === "skip") {
|
|
124
|
+
console.log("Skipping integration setup.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (integrationKey !== undefined) {
|
|
129
|
+
await installIntegrationByKey(config, integrationKey);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log("\nChoose an integration to set up:");
|
|
134
|
+
console.log("1. OpenCode (default)");
|
|
135
|
+
console.log("2. OpenClaw");
|
|
136
|
+
console.log("3. Pi");
|
|
137
|
+
console.log("4. Claude Code");
|
|
138
|
+
console.log("5. Hermes");
|
|
139
|
+
console.log("6. Skip for now");
|
|
140
|
+
|
|
141
|
+
const answer = await ask("Select integration [1]: ");
|
|
142
|
+
const choice = parseChoice(answer);
|
|
143
|
+
|
|
144
|
+
const integrationByChoice: Record<number, keyof typeof CLIENT_CONFIGS> = {
|
|
145
|
+
1: "opencode",
|
|
146
|
+
2: "openclaw",
|
|
147
|
+
3: "pi-agent",
|
|
148
|
+
4: "claude-code",
|
|
149
|
+
5: "hermes",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const key = integrationByChoice[choice];
|
|
153
|
+
if (!key) {
|
|
154
|
+
console.log("Skipping integration setup.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await installIntegrationByKey(config, key);
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runUsageTui } from "./app.ts";
|