diff --git a/src/cli.ts b/src/cli.ts index 5184b92..ec94cfa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,8 +9,10 @@ import { getDaemonBaseUrl, getNpubSuffix, addDaemonClient, + ensureDaemonClient, getUserNpub, } from "./utils/daemon-client"; +import { getClientsList } from "./utils/clients"; import { existsSync, mkdirSync } from "fs"; import { execSync } from "child_process"; import { @@ -26,7 +28,6 @@ import { setupIntegration, CLIENT_CONFIGS, CLIENT_INTEGRATIONS, - ensureIntegrationClient, } from "./integrations"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey } from "./utils/nip98"; @@ -732,26 +733,15 @@ clientsCmd const config = await loadConfig(); const suffix = getNpubSuffix(config); - const result = await callDaemon("/clients"); - if (result.error) { - console.log(result.error); - process.exit(1); - } + const entries = await getClientsList(); - const output = result.output as - | { - clients: Array<{ - id: string; - name: string; - apiKey: string; - createdAt: number; - lastUsed?: number | null; - }>; - totalCount: number; - } - | undefined; - - let clients = output?.clients || []; + let clients = entries.map((c) => ({ + id: c.clientId, + name: c.name, + apiKey: c.apiKey, + createdAt: c.createdAt, + lastUsed: c.lastUsed, + })); if (suffix) { const suffixStr = `_${suffix}`; @@ -856,7 +846,15 @@ clientsCmd if (!integrationFn || !integrationConfig) continue; try { - const client = await ensureIntegrationClient(integrationConfig); + const { client, created } = await ensureDaemonClient( + integrationConfig.name, + integrationConfig.clientId, + ); + if (created) { + logger.log(`Created new API key for ${integrationConfig.name}`); + } else { + logger.log(`Using existing API key for ${integrationConfig.name}`); + } await integrationFn(config, client.apiKey, integrationConfig); console.log(`\n ${integrationConfig.name}:`); diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index 767dc09..f567e6e 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -14,6 +14,7 @@ import { type CocodState, } from "../wallet/cocod-client"; import { decodeCashuTokenAmount } from "../wallet"; +import { getClientsFromStore } from "../../utils/clients"; type ClientMode = "xcashu" | "lazyrefund" | "apikeys"; @@ -63,12 +64,8 @@ function getClientIdFromRequest( return undefined; } - const state = store.getState(); - const clientIds = state.clientIds || []; - - const matchingClient = ( - clientIds as { clientId: string; apiKey: string }[] - ).find((c) => c.apiKey === apiKey); + const clients = getClientsFromStore(store); + const matchingClient = clients.find((c) => c.apiKey === apiKey); return matchingClient?.clientId; } @@ -710,24 +707,13 @@ export function createDaemonRequestHandler(deps: { // Client management endpoints if (req.method === "GET" && url.pathname === "/clients") { try { - const state = deps.store.getState(); - const clientIds = state.clientIds || []; - - const clients = clientIds.map( - (c: { - clientId: string; - name: string; - apiKey: string; - createdAt: number; - lastUsed?: number | null; - }) => ({ - id: c.clientId, - name: c.name, - apiKey: c.apiKey, - createdAt: c.createdAt, - lastUsed: c.lastUsed, - }), - ); + const clients = getClientsFromStore(deps.store).map((c) => ({ + id: c.clientId, + name: c.name, + apiKey: c.apiKey, + createdAt: c.createdAt, + lastUsed: c.lastUsed, + })); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -793,10 +779,9 @@ export function createDaemonRequestHandler(deps: { return; } - const state = deps.store.getState(); - const existingClients = state.clientIds || []; + const existingClients = getClientsFromStore(deps.store); const existingClient = existingClients.find( - (c: { clientId: string }) => c.clientId === clientId, + (c) => c.clientId === clientId, ); if (existingClient) { @@ -863,10 +848,9 @@ export function createDaemonRequestHandler(deps: { return; } - const state = deps.store.getState(); - const existingClients = state.clientIds || []; + const existingClients = getClientsFromStore(deps.store); const index = existingClients.findIndex( - (c: { clientId: string }) => c.clientId === id, + (c) => c.clientId === id, ); if (index === -1) { @@ -879,7 +863,7 @@ export function createDaemonRequestHandler(deps: { return; } - const removedClient = existingClients[index]; + const removedClient = existingClients[index]!; const updatedClients = existingClients.filter( (_c: unknown, i: number) => i !== index, ); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index df2de49..2a033a2 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -21,6 +21,7 @@ import { createCocodClient } from "./wallet/cocod-client"; import { createModelService } from "./models"; import { createDaemonRequestHandler } from "./http"; import { runIntegrationsForClients } from "../integrations"; +import { getClientsFromStore } from "../utils/clients"; import { RoutstrClient } from "@routstr/sdk"; async function main(): Promise { @@ -108,8 +109,7 @@ async function main(): Promise { logger.log("Scheduled model refresh completed successfully."); // Refresh integrations for all registered clients - const state = store.getState(); - const clientIds = state.clientIds || []; + const clientIds = getClientsFromStore(store); if (clientIds.length > 0) { logger.log(`Refreshing ${clientIds.length} client integration(s)...`); await runIntegrationsForClients(clientIds, updatedConfig); @@ -217,8 +217,7 @@ async function main(): Promise { .then(async () => { logger.log("Initial model refresh completed."); // Refresh integrations for all registered clients after initial bootstrap - const state = store.getState(); - const clientIds = state.clientIds || []; + const clientIds = getClientsFromStore(store); if (clientIds.length > 0) { logger.log(`Refreshing ${clientIds.length} client integration(s)...`); await runIntegrationsForClients(clientIds, updatedConfig); diff --git a/src/integrations/index.ts b/src/integrations/index.ts index b61d5f0..fecfc2d 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -1,8 +1,7 @@ import type { RoutstrdConfig } from "../utils/config"; import { logger } from "../utils/logger"; import { - addDaemonClient, - callDaemon, + ensureDaemonClient, type DaemonClient, } from "../utils/daemon-client"; import { installOpencodeIntegration } from "./opencode"; @@ -13,8 +12,6 @@ import type { IntegrationConfig } from "./registry"; import { CLIENT_CONFIGS } from "./registry"; export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry"; -export type IntegrationClient = DaemonClient; - function ask(question: string): Promise { process.stdout.write(question); @@ -45,37 +42,6 @@ function parseChoice(input: string): number { return 1; } -export async function ensureIntegrationClient( - integrationConfig: IntegrationConfig, -): Promise { - try { - const { client } = await addDaemonClient(integrationConfig.name); - - logger.log(`Created new API key for ${integrationConfig.name}`); - return client; - } catch (error) { - const message = (error as Error).message || ""; - if (!message.includes("already exists")) { - throw error; - } - - const clientsResult = await callDaemon("/clients"); - const clients = - (clientsResult.output as { clients?: IntegrationClient[] } | undefined) - ?.clients || []; - const client = clients.find((c) => c.id === integrationConfig.clientId); - - if (!client?.apiKey) { - throw new Error( - `Client '${integrationConfig.clientId}' already exists but could not be fetched from the daemon.`, - ); - } - - logger.log(`Using existing API key for ${integrationConfig.name}`); - return client; - } -} - export async function setupIntegration( config: RoutstrdConfig, ): Promise { @@ -103,7 +69,16 @@ export async function setupIntegration( } const integrationConfig = CLIENT_CONFIGS[key]!; - const client = await ensureIntegrationClient(integrationConfig); + const { client, created } = await ensureDaemonClient( + integrationConfig.name, + integrationConfig.clientId, + ); + + if (created) { + logger.log(`Created new API key for ${integrationConfig.name}`); + } else { + logger.log(`Using existing API key for ${integrationConfig.name}`); + } if (key === "opencode") { await installOpencodeIntegration(config, client.apiKey, integrationConfig); diff --git a/src/utils/clients.ts b/src/utils/clients.ts new file mode 100644 index 0000000..0e9f248 --- /dev/null +++ b/src/utils/clients.ts @@ -0,0 +1,66 @@ +import { callDaemon } from "./daemon-client"; + +export interface ClientEntry { + clientId: string; + name: string; + apiKey: string; + createdAt: number; + lastUsed?: number | null; +} + +/** + * Read the clients list directly from the SDK store. + * Use this when running inside the daemon (local mode). + */ +export function getClientsFromStore(store: { getState(): any }): ClientEntry[] { + const state = store.getState(); + const clientIds = state.clientIds || []; + return clientIds.map( + (c: { + clientId: string; + name: string; + apiKey: string; + createdAt: number; + lastUsed?: number | null; + }) => ({ + clientId: c.clientId, + name: c.name, + apiKey: c.apiKey, + createdAt: c.createdAt, + lastUsed: c.lastUsed, + }), + ); +} + +/** + * Fetch the clients list from the daemon API. + * Use this when running remotely (CLI in remote mode). + */ +export async function getClientsList(): Promise { + const result = await callDaemon("/clients"); + const clients = ( + result.output as + | { + clients?: Array<{ + id: string; + name: string; + apiKey: string; + createdAt: number; + lastUsed?: number | null; + }>; + } + | undefined + )?.clients; + + if (!clients) { + return []; + } + + return clients.map((c) => ({ + clientId: c.id, + name: c.name, + apiKey: c.apiKey, + createdAt: c.createdAt, + lastUsed: c.lastUsed, + })); +} diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index 6557808..40f83c8 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -11,6 +11,7 @@ import { npubFromSecretKey, type HttpMethod, } from "./nip98"; +import { getClientsList } from "./clients"; export interface CommandResponse { output?: unknown; @@ -44,6 +45,40 @@ export async function addDaemonClient( return { message: output.message, client: output.client }; } +export async function ensureDaemonClient( + name: string, + clientId: string, +): Promise<{ client: DaemonClient; created: boolean }> { + try { + const { client } = await addDaemonClient(name); + return { client, created: true }; + } catch (error) { + const message = (error as Error).message || ""; + if (!message.includes("already exists")) { + throw error; + } + + const clients = await getClientsList(); + const entry = clients.find((c) => c.clientId === clientId); + + if (!entry?.apiKey) { + throw new Error( + `Client '${clientId}' already exists but could not be fetched from the daemon.`, + ); + } + + const client: DaemonClient = { + id: entry.clientId, + name: entry.name, + apiKey: entry.apiKey, + createdAt: entry.createdAt, + lastUsed: entry.lastUsed, + }; + + return { client, created: false }; + } +} + export async function loadConfig(): Promise { try { if (existsSync(CONFIG_FILE)) {