From 366a129c3c5563206ff31d290ad6e2da727b0b20 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:14:10 +0530 Subject: [PATCH 1/9] refactor: move client logic from cli.ts and daemon-client.ts to clients.ts - Moved addDaemonClient and ensureDaemonClient from daemon-client.ts to clients.ts - Moved clients list/delete/add action handlers from cli.ts to clients.ts - Updated cli.ts to call the new action functions from clients.ts - Updated integrations/index.ts to import ensureDaemonClient from clients.ts - Fixed circular dependency by having clients.ts import from ../integrations/registry instead of ../integrations --- src/cli.ts | 162 ++------------------------ src/integrations/index.ts | 2 +- src/utils/clients.ts | 233 ++++++++++++++++++++++++++++++++++++- src/utils/daemon-client.ts | 64 +--------- 4 files changed, 243 insertions(+), 218 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 87c445d..a06f931 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,11 +8,13 @@ import { loadConfig, getDaemonBaseUrl, getNpubSuffix, - addDaemonClient, - ensureDaemonClient, getUserNpub, } from "./utils/daemon-client"; -import { getClientsList } from "./utils/clients"; +import { + listClientsAction, + deleteClientAction, + addClientAction, +} from "./utils/clients"; import { existsSync, mkdirSync } from "fs"; import { execSync } from "child_process"; import { @@ -24,11 +26,7 @@ import { type RoutstrdConfig, } from "./utils/config"; import { logger } from "./utils/logger"; -import { - setupIntegration, - CLIENT_CONFIGS, - CLIENT_INTEGRATIONS, -} from "./integrations"; +import { setupIntegration } from "./integrations"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; import { generateSecretKey, nip19 } from "nostr-tools"; @@ -776,90 +774,14 @@ clientsCmd .command("list") .description("List all clients") .action(async () => { - await ensureDaemonRunning(); - - const config = await loadConfig(); - const suffix = getNpubSuffix(config); - - const entries = await getClientsList(); - - 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}`; - clients = clients.filter( - (c) => c.name.endsWith(suffixStr) || c.id.endsWith(suffixStr), - ); - clients = clients.map((c) => ({ - ...c, - name: c.name.endsWith(suffixStr) - ? c.name.slice(0, -suffixStr.length) - : c.name, - id: c.id.endsWith(suffixStr) ? c.id.slice(0, -suffixStr.length) : c.id, - })); - } - - if (clients.length === 0) { - console.log("No clients found."); - return; - } - - console.log(`Clients (${clients.length} total):\n`); - for (const client of clients) { - const createdAt = new Date(client.createdAt).toISOString(); - const lastUsed = client.lastUsed - ? new Date(client.lastUsed).toISOString() - : "never"; - console.log(` ${client.id}`); - console.log(` Name: ${client.name}`); - console.log(` API Key: ${client.apiKey}`); - console.log(` Created: ${createdAt}`); - console.log(""); - } + await listClientsAction(); }); clientsCmd .command("delete ") .description("Delete a client by its ID") .action(async (id: string) => { - await ensureDaemonRunning(); - - const config = await loadConfig(); - const suffix = getNpubSuffix(config); - let resolvedId = id; - if (suffix) { - const suffixStr = `_${suffix}`; - if (!id.endsWith(suffixStr)) { - resolvedId = `${id}${suffixStr}`; - } - } - - const result = await callDaemon("/clients/delete", { - method: "POST", - body: { id: resolvedId }, - }); - - if (result.error) { - console.log(result.error); - process.exit(1); - } - - const output = result.output as - | { - message: string; - id: string; - } - | undefined; - - if (output) { - console.log(output.message); - } + await deleteClientAction(id); }); clientsCmd @@ -878,73 +800,7 @@ clientsCmd piAgent?: boolean; claudeCode?: boolean; }) => { - await ensureDaemonRunning(); - const config = await loadConfig(); - - const integrationKeys: string[] = []; - if (options.opencode) integrationKeys.push("opencode"); - if (options.openclaw) integrationKeys.push("openclaw"); - if (options.piAgent) integrationKeys.push("pi-agent"); - if (options.claudeCode) integrationKeys.push("claude-code"); - - if (integrationKeys.length > 0) { - for (const key of integrationKeys) { - const integrationFn = CLIENT_INTEGRATIONS[key]; - const integrationConfig = CLIENT_CONFIGS[key]; - if (!integrationFn || !integrationConfig) continue; - - try { - 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}:`); - console.log(` Client ID: ${client.id}`); - console.log(` API Key: ${client.apiKey}`); - } catch (error) { - logger.error( - `Failed to set up ${integrationConfig.name} integration:`, - error, - ); - continue; - } - } - - console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`); - return; - } - - if (!options.name) { - console.error( - "error: required option '-n, --name ' not specified", - ); - process.exit(1); - } - - const suffix = getNpubSuffix(config); - const resolvedName = suffix ? `${options.name} ${suffix}` : options.name; - - try { - const { message, client } = await addDaemonClient(resolvedName); - - if (message) { - console.log(message); - } - console.log(`\n ID: ${client.id}`); - console.log(` Name: ${client.name}`); - console.log(` API Key: ${client.apiKey}`); - console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`); - } catch (error) { - console.log((error as Error).message); - process.exit(1); - } + await addClientAction(options); }, ); diff --git a/src/integrations/index.ts b/src/integrations/index.ts index fecfc2d..78e2fdb 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -3,7 +3,7 @@ import { logger } from "../utils/logger"; import { ensureDaemonClient, type DaemonClient, -} from "../utils/daemon-client"; +} from "../utils/clients"; import { installOpencodeIntegration } from "./opencode"; import { installOpenClawIntegration } from "./openclaw"; import { installPiIntegration } from "./pi"; diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 0e9f248..409c7ad 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -1,4 +1,12 @@ -import { callDaemon } from "./daemon-client"; +import { + callDaemon, + loadConfig, + getDaemonBaseUrl, + getNpubSuffix, + ensureDaemonRunning, +} from "./daemon-client"; +import { logger } from "./logger"; +import { CLIENT_INTEGRATIONS, CLIENT_CONFIGS } from "../integrations/registry"; export interface ClientEntry { clientId: string; @@ -8,6 +16,14 @@ export interface ClientEntry { lastUsed?: number | null; } +export interface DaemonClient { + id: 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). @@ -64,3 +80,218 @@ export async function getClientsList(): Promise { lastUsed: c.lastUsed, })); } + +export async function addDaemonClient( + name: string, +): Promise<{ message?: string; client: DaemonClient }> { + const result = await callDaemon("/clients/add", { + method: "POST", + body: { name }, + }); + + const output = result.output as + | { message?: string; client?: DaemonClient } + | undefined; + + if (!output?.client?.apiKey) { + throw new Error(`Daemon did not return an API key for ${name}.`); + } + + 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 listClientsAction(): Promise { + await ensureDaemonRunning(); + + const config = await loadConfig(); + const suffix = getNpubSuffix(config); + + const entries = await getClientsList(); + + 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}`; + clients = clients.filter( + (c) => c.name.endsWith(suffixStr) || c.id.endsWith(suffixStr), + ); + clients = clients.map((c) => ({ + ...c, + name: c.name.endsWith(suffixStr) + ? c.name.slice(0, -suffixStr.length) + : c.name, + id: c.id.endsWith(suffixStr) ? c.id.slice(0, -suffixStr.length) : c.id, + })); + } + + if (clients.length === 0) { + console.log("No clients found."); + return; + } + + console.log(`Clients (${clients.length} total):\n`); + for (const client of clients) { + const createdAt = new Date(client.createdAt).toISOString(); + const lastUsed = client.lastUsed + ? new Date(client.lastUsed).toISOString() + : "never"; + console.log(` ${client.id}`); + console.log(` Name: ${client.name}`); + console.log(` API Key: ${client.apiKey}`); + console.log(` Created: ${createdAt}`); + console.log(""); + } +} + +export async function deleteClientAction(id: string): Promise { + await ensureDaemonRunning(); + + const config = await loadConfig(); + const suffix = getNpubSuffix(config); + let resolvedId = id; + if (suffix) { + const suffixStr = `_${suffix}`; + if (!id.endsWith(suffixStr)) { + resolvedId = `${id}${suffixStr}`; + } + } + + const result = await callDaemon("/clients/delete", { + method: "POST", + body: { id: resolvedId }, + }); + + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const output = result.output as + | { + message: string; + id: string; + } + | undefined; + + if (output) { + console.log(output.message); + } +} + +export interface AddClientOptions { + name?: string; + opencode?: boolean; + openclaw?: boolean; + piAgent?: boolean; + claudeCode?: boolean; +} + +export async function addClientAction(options: AddClientOptions): Promise { + await ensureDaemonRunning(); + const config = await loadConfig(); + + const integrationKeys: string[] = []; + if (options.opencode) integrationKeys.push("opencode"); + if (options.openclaw) integrationKeys.push("openclaw"); + if (options.piAgent) integrationKeys.push("pi-agent"); + if (options.claudeCode) integrationKeys.push("claude-code"); + + if (integrationKeys.length > 0) { + for (const key of integrationKeys) { + const integrationFn = CLIENT_INTEGRATIONS[key]; + const integrationConfig = CLIENT_CONFIGS[key]; + if (!integrationFn || !integrationConfig) continue; + + try { + 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}:`); + console.log(` Client ID: ${client.id}`); + console.log(` API Key: ${client.apiKey}`); + } catch (error) { + logger.error( + `Failed to set up ${integrationConfig.name} integration:`, + error, + ); + continue; + } + } + + console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`); + return; + } + + if (!options.name) { + console.error( + "error: required option '-n, --name ' not specified", + ); + process.exit(1); + } + + const suffix = getNpubSuffix(config); + const resolvedName = suffix ? `${options.name} ${suffix}` : options.name; + + try { + const { message, client } = await addDaemonClient(resolvedName); + + if (message) { + console.log(message); + } + console.log(`\n ID: ${client.id}`); + console.log(` Name: ${client.name}`); + console.log(` API Key: ${client.apiKey}`); + console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`); + } catch (error) { + console.log((error as Error).message); + process.exit(1); + } +} diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index 40f83c8..97d70ba 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -11,74 +11,12 @@ import { npubFromSecretKey, type HttpMethod, } from "./nip98"; -import { getClientsList } from "./clients"; export interface CommandResponse { output?: unknown; error?: string; } -export interface DaemonClient { - id: string; - name: string; - apiKey: string; - createdAt: number; - lastUsed?: number | null; -} - -export async function addDaemonClient( - name: string, -): Promise<{ message?: string; client: DaemonClient }> { - const result = await callDaemon("/clients/add", { - method: "POST", - body: { name }, - }); - - const output = result.output as - | { message?: string; client?: DaemonClient } - | undefined; - - if (!output?.client?.apiKey) { - throw new Error(`Daemon did not return an API key for ${name}.`); - } - - 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)) { @@ -262,4 +200,4 @@ export async function handleDaemonCommand( console.error(message); process.exit(1); } -} +} \ No newline at end of file From 0dc40884a5ca2cb53bca76d88c176b4457c5baea Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:21:01 +0530 Subject: [PATCH 2/9] Simplify client creation: fetch list first instead of catch-and-fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ensureDaemonClient (try-create → catch 'already exists' → fetch list) with a single addDaemonClient that fetches the list first, checks if client exists, then creates only if needed. Returns { client, created } in all cases. --- src/integrations/index.ts | 4 +-- src/utils/clients.ts | 68 +++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/integrations/index.ts b/src/integrations/index.ts index 78e2fdb..993b237 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -1,7 +1,7 @@ import type { RoutstrdConfig } from "../utils/config"; import { logger } from "../utils/logger"; import { - ensureDaemonClient, + addDaemonClient, type DaemonClient, } from "../utils/clients"; import { installOpencodeIntegration } from "./opencode"; @@ -69,7 +69,7 @@ export async function setupIntegration( } const integrationConfig = CLIENT_CONFIGS[key]!; - const { client, created } = await ensureDaemonClient( + const { client, created } = await addDaemonClient( integrationConfig.name, integrationConfig.clientId, ); diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 409c7ad..95c0372 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -83,12 +83,30 @@ export async function getClientsList(): Promise { export async function addDaemonClient( name: string, -): Promise<{ message?: string; client: DaemonClient }> { + clientId?: string, +): Promise<{ message?: string; client: DaemonClient; created: boolean }> { + const existingClients = await getClientsList(); + const existing = clientId + ? existingClients.find((c) => c.clientId === clientId) + : existingClients.find((c) => c.name === name); + + if (existing) { + const client: DaemonClient = { + id: existing.clientId, + name: existing.name, + apiKey: existing.apiKey, + createdAt: existing.createdAt, + lastUsed: existing.lastUsed, + }; + return { client, created: false }; + } + const result = await callDaemon("/clients/add", { method: "POST", body: { name }, }); + const output = result.output as | { message?: string; client?: DaemonClient } | undefined; @@ -97,41 +115,7 @@ export async function addDaemonClient( throw new Error(`Daemon did not return an API key for ${name}.`); } - 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 }; - } + return { message: output.message, client: output.client, created: true }; } export async function listClientsAction(): Promise { @@ -243,7 +227,7 @@ export async function addClientAction(options: AddClientOptions): Promise if (!integrationFn || !integrationConfig) continue; try { - const { client, created } = await ensureDaemonClient( + const { client, created } = await addDaemonClient( integrationConfig.name, integrationConfig.clientId, ); @@ -281,7 +265,15 @@ export async function addClientAction(options: AddClientOptions): Promise const resolvedName = suffix ? `${options.name} ${suffix}` : options.name; try { - const { message, client } = await addDaemonClient(resolvedName); + const { message, client, created } = await addDaemonClient(resolvedName); + + if (!created) { + console.log(`Client '${resolvedName}' already exists.`); + console.log(`\n ID: ${client.id}`); + console.log(` Name: ${client.name}`); + console.log(` API Key: ${client.apiKey}`); + return; + } if (message) { console.log(message); From dc5a5d32c5c781350c0b041121e5d30e14537cde Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:45:04 +0530 Subject: [PATCH 3/9] refactor: move remote-mode client suffix handling into getClientsList --- src/utils/clients.ts | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 95c0372..615cdaf 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -53,6 +53,7 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] { * Use this when running remotely (CLI in remote mode). */ export async function getClientsList(): Promise { + const config = await loadConfig(); const result = await callDaemon("/clients"); const clients = ( result.output as @@ -72,13 +73,18 @@ export async function getClientsList(): Promise { return []; } - return clients.map((c) => ({ - clientId: c.id, - name: c.name, - apiKey: c.apiKey, - createdAt: c.createdAt, - lastUsed: c.lastUsed, - })); + const suffix = config.daemonUrl ? getNpubSuffix(config) : null; + const suffixStr = suffix ? `_${suffix}` : null; + + return clients + .filter((c) => !suffixStr || c.id.endsWith(suffixStr)) + .map((c) => ({ + clientId: suffixStr ? c.id.slice(0, -suffixStr.length) : c.id, + name: c.name, + apiKey: c.apiKey, + createdAt: c.createdAt, + lastUsed: c.lastUsed, + })); } export async function addDaemonClient( @@ -121,12 +127,9 @@ export async function addDaemonClient( export async function listClientsAction(): Promise { await ensureDaemonRunning(); - const config = await loadConfig(); - const suffix = getNpubSuffix(config); - const entries = await getClientsList(); - let clients = entries.map((c) => ({ + const clients = entries.map((c) => ({ id: c.clientId, name: c.name, apiKey: c.apiKey, @@ -134,20 +137,6 @@ export async function listClientsAction(): Promise { lastUsed: c.lastUsed, })); - if (suffix) { - const suffixStr = `_${suffix}`; - clients = clients.filter( - (c) => c.name.endsWith(suffixStr) || c.id.endsWith(suffixStr), - ); - clients = clients.map((c) => ({ - ...c, - name: c.name.endsWith(suffixStr) - ? c.name.slice(0, -suffixStr.length) - : c.name, - id: c.id.endsWith(suffixStr) ? c.id.slice(0, -suffixStr.length) : c.id, - })); - } - if (clients.length === 0) { console.log("No clients found."); return; From 7a4dc1e3c335eda4095765db4cf4d417dede2ac4 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:17:42 +0530 Subject: [PATCH 4/9] refactor(clients): extract suffix logic into helper functions, stop modifying names - Add addSuffixToId() and removeSuffixFromId() helper functions - Only suffix client IDs, never names - Update getClientsList, deleteClientAction, and addClientAction to use helpers --- src/utils/clients.ts | 55 +++++++++++++++++++++++++++----------- src/utils/daemon-client.ts | 11 -------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 615cdaf..e316bb8 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -2,12 +2,45 @@ import { callDaemon, loadConfig, getDaemonBaseUrl, - getNpubSuffix, ensureDaemonRunning, } from "./daemon-client"; +import { + parseSecretKey, + npubFromSecretKey, +} from "./nip98"; +import { type RoutstrdConfig } from "./config"; import { logger } from "./logger"; import { CLIENT_INTEGRATIONS, CLIENT_CONFIGS } from "../integrations/registry"; +export function getNpubSuffix(config: RoutstrdConfig): string | null { + if (!config.daemonUrl || !config.nsec) return null; + try { + const secretKey = parseSecretKey(config.nsec); + const npub = npubFromSecretKey(secretKey); + return npub.slice(-7); + } catch { + return null; + } +} + +/** + * Add suffix to a client ID. + */ +export function addSuffixToId(id: string, suffix: string): string { + return `${id}_${suffix}`; +} + +/** + * Remove suffix from a client ID if present. + */ +export function removeSuffixFromId(id: string, suffix: string): string { + const suffixStr = `_${suffix}`; + if (id.endsWith(suffixStr)) { + return id.slice(0, -suffixStr.length); + } + return id; +} + export interface ClientEntry { clientId: string; name: string; @@ -74,12 +107,11 @@ export async function getClientsList(): Promise { } const suffix = config.daemonUrl ? getNpubSuffix(config) : null; - const suffixStr = suffix ? `_${suffix}` : null; return clients - .filter((c) => !suffixStr || c.id.endsWith(suffixStr)) + .filter((c) => !suffix || c.id.endsWith(`_${suffix}`)) .map((c) => ({ - clientId: suffixStr ? c.id.slice(0, -suffixStr.length) : c.id, + clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id, name: c.name, apiKey: c.apiKey, createdAt: c.createdAt, @@ -161,13 +193,7 @@ export async function deleteClientAction(id: string): Promise { const config = await loadConfig(); const suffix = getNpubSuffix(config); - let resolvedId = id; - if (suffix) { - const suffixStr = `_${suffix}`; - if (!id.endsWith(suffixStr)) { - resolvedId = `${id}${suffixStr}`; - } - } + const resolvedId = suffix ? addSuffixToId(id, suffix) : id; const result = await callDaemon("/clients/delete", { method: "POST", @@ -250,14 +276,11 @@ export async function addClientAction(options: AddClientOptions): Promise process.exit(1); } - const suffix = getNpubSuffix(config); - const resolvedName = suffix ? `${options.name} ${suffix}` : options.name; - try { - const { message, client, created } = await addDaemonClient(resolvedName); + const { message, client, created } = await addDaemonClient(options.name); if (!created) { - console.log(`Client '${resolvedName}' already exists.`); + console.log(`Client '${options.name}' already exists.`); console.log(`\n ID: ${client.id}`); console.log(` Name: ${client.name}`); console.log(` API Key: ${client.apiKey}`); diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index 97d70ba..cf73f83 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -98,17 +98,6 @@ export async function isDaemonRunning(): Promise { } } -export function getNpubSuffix(config: RoutstrdConfig): string | null { - if (!config.daemonUrl || !config.nsec) return null; - try { - const secretKey = parseSecretKey(config.nsec); - const npub = npubFromSecretKey(secretKey); - return npub.slice(-7); - } catch { - return null; - } -} - export function getUserNpub(config: RoutstrdConfig): string | null { if (!config.nsec) return null; try { From ee6fef906295aecbf1e4f64adcf59b32c3a82165 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:18:53 +0530 Subject: [PATCH 5/9] fix: standardize on hyphen instead of underscore for client ID suffix --- src/tui/usage/data.ts | 2 +- src/utils/clients.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index 0a2a763..8aade1f 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -88,7 +88,7 @@ export async function fetchUsage(limit = 10000): Promise { const entries = result.output as UsageTrackingEntry[] | undefined; const entriesArray = Array.isArray(entries) ? entries : []; const suffix = getNpubSuffix(await loadConfig()); - const suffixStr = suffix ? `_${suffix}` : null; + const suffixStr = suffix ? `-${suffix}` : null; const visibleEntries = suffixStr ? entriesArray .filter((entry) => entry.client?.endsWith(suffixStr)) diff --git a/src/utils/clients.ts b/src/utils/clients.ts index e316bb8..19430c8 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -34,7 +34,7 @@ export function addSuffixToId(id: string, suffix: string): string { * Remove suffix from a client ID if present. */ export function removeSuffixFromId(id: string, suffix: string): string { - const suffixStr = `_${suffix}`; + const suffixStr = `-${suffix}`; if (id.endsWith(suffixStr)) { return id.slice(0, -suffixStr.length); } @@ -109,7 +109,7 @@ export async function getClientsList(): Promise { const suffix = config.daemonUrl ? getNpubSuffix(config) : null; return clients - .filter((c) => !suffix || c.id.endsWith(`_${suffix}`)) + .filter((c) => !suffix || c.id.endsWith(`-${suffix}`)) .map((c) => ({ clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id, name: c.name, From 91384b633e32ecaa4ee31a6757356ba67d38e500 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:20:16 +0530 Subject: [PATCH 6/9] fix: use hyphen in addSuffixToId to match removeSuffixFromId --- src/utils/clients.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 19430c8..be9fca2 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -27,7 +27,7 @@ export function getNpubSuffix(config: RoutstrdConfig): string | null { * Add suffix to a client ID. */ export function addSuffixToId(id: string, suffix: string): string { - return `${id}_${suffix}`; + return `${id}-${suffix}`; } /** From a14e24417ab7cc0565abdd26b3c7e9c72acd960f Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:31:12 +0530 Subject: [PATCH 7/9] fixed a few thigns --- src/daemon/index.ts | 6 +++--- src/utils/clients.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 2a033a2..08e2ddb 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -21,7 +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 { getClientsList } from "../utils/clients"; import { RoutstrClient } from "@routstr/sdk"; async function main(): Promise { @@ -109,7 +109,7 @@ async function main(): Promise { logger.log("Scheduled model refresh completed successfully."); // Refresh integrations for all registered clients - const clientIds = getClientsFromStore(store); + const clientIds = await getClientsList(); if (clientIds.length > 0) { logger.log(`Refreshing ${clientIds.length} client integration(s)...`); await runIntegrationsForClients(clientIds, updatedConfig); @@ -217,7 +217,7 @@ async function main(): Promise { .then(async () => { logger.log("Initial model refresh completed."); // Refresh integrations for all registered clients after initial bootstrap - const clientIds = getClientsFromStore(store); + const clientIds = await getClientsList(); if (clientIds.length > 0) { logger.log(`Refreshing ${clientIds.length} client integration(s)...`); await runIntegrationsForClients(clientIds, updatedConfig); diff --git a/src/utils/clients.ts b/src/utils/clients.ts index be9fca2..1216dc4 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -139,9 +139,12 @@ export async function addDaemonClient( return { client, created: false }; } + // Derive id from name by replacing spaces with hyphens + const derivedId = name.replace(/\s+/g, "-").toLowerCase(); + const result = await callDaemon("/clients/add", { method: "POST", - body: { name }, + body: { name, id: derivedId }, }); From a5d68ccd0d2afac19ede49974819be3525419e23 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:56:09 +0530 Subject: [PATCH 8/9] Extract refreshModelsAndIntegrations into src/integrations/index.ts - Created reusable refreshModelsAndIntegrations() function that combines model refresh with client integration refresh - Updated src/daemon/index.ts to use the new function in both the scheduled refresh job and the initial bootstrap refresh - Merged two chained .then() calls into one in the initial bootstrap flow - Removed redundant imports (runIntegrationsForClients, getClientsList) from daemon/index.ts since they're now encapsulated in the function --- src/daemon/index.ts | 35 ++++++++--------------------------- src/integrations/index.ts | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 08e2ddb..9b6de7a 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -20,8 +20,7 @@ import { createWalletAdapter } from "./wallet"; import { createCocodClient } from "./wallet/cocod-client"; import { createModelService } from "./models"; import { createDaemonRequestHandler } from "./http"; -import { runIntegrationsForClients } from "../integrations"; -import { getClientsList } from "../utils/clients"; +import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; async function main(): Promise { @@ -37,7 +36,8 @@ async function main(): Promise { saveDaemonConfig(updatedConfig); const sqliteDriver = await createBunSqliteDriver(DB_PATH); - const { store } = await createSdkStore({ driver: sqliteDriver }); + const { store, hydrate } = createSdkStore({ driver: sqliteDriver }); + await hydrate; const { Database } = await import("bun:sqlite"); const usageTrackingDriver = createBunSqliteUsageTrackingDriver({ dbPath: DB_PATH, @@ -105,16 +105,7 @@ async function main(): Promise { refreshInterval = setInterval(async () => { logger.log("Running scheduled model refresh..."); try { - await getRoutstr21Models(true); - logger.log("Scheduled model refresh completed successfully."); - - // Refresh integrations for all registered clients - const clientIds = await getClientsList(); - if (clientIds.length > 0) { - logger.log(`Refreshing ${clientIds.length} client integration(s)...`); - await runIntegrationsForClients(clientIds, updatedConfig); - logger.log("Client integrations refreshed."); - } + await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled"); } catch (error) { logger.error("Scheduled model refresh failed:", error); } @@ -207,28 +198,18 @@ async function main(): Promise { // Start the recurring model refresh job after initial bootstrap void ensureProvidersBootstrapped() - .then(() => { + .then(async () => { startModelRefreshJob(); - startRefundJob(); + startRefundJob(); // Run an immediate refresh to populate models right away logger.log("Running initial model refresh..."); - return getRoutstr21Models(true); - }) - .then(async () => { - logger.log("Initial model refresh completed."); - // Refresh integrations for all registered clients after initial bootstrap - const clientIds = await getClientsList(); - if (clientIds.length > 0) { - logger.log(`Refreshing ${clientIds.length} client integration(s)...`); - await runIntegrationsForClients(clientIds, updatedConfig); - logger.log("Client integrations refreshed."); - } + await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Initial"); }) .catch((error) => { logger.error("Initial model refresh failed:", error); // Still start the jobs even if initial refresh fails startModelRefreshJob(); - startRefundJob(); + startRefundJob(); }); }); } diff --git a/src/integrations/index.ts b/src/integrations/index.ts index 993b237..9d6106e 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -4,14 +4,35 @@ import { addDaemonClient, type DaemonClient, } from "../utils/clients"; +import { getClientsList } from "../utils/clients"; import { installOpencodeIntegration } from "./opencode"; import { installOpenClawIntegration } from "./openclaw"; import { installPiIntegration } from "./pi"; import { installClaudeCodeIntegration } from "./claudecode"; import type { IntegrationConfig } from "./registry"; -import { CLIENT_CONFIGS } from "./registry"; +import { CLIENT_CONFIGS, runIntegrationsForClients } from "./registry"; export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry"; +/** + * Refresh routstr21 models and then run integrations for all registered clients. + * Used both on initial daemon startup and in the recurring scheduled job. + */ +export async function refreshModelsAndIntegrations( + getRoutstr21Models: (force?: boolean) => Promise, + config: RoutstrdConfig, + label: string = "Scheduled", +): Promise { + await getRoutstr21Models(true); + logger.log(`${label} model refresh completed successfully.`); + + const clientIds = await getClientsList(); + if (clientIds.length > 0) { + logger.log(`Refreshing ${clientIds.length} client integration(s)...`); + await runIntegrationsForClients(clientIds, config); + logger.log("Client integrations refreshed."); + } +} + function ask(question: string): Promise { process.stdout.write(question); From c1c72bc147ca7e320a83686bc69c23e27464200e Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:04:13 +0530 Subject: [PATCH 9/9] Add 'refresh' CLI command to refresh models and integrations - Added 'routstrd refresh' command that: - Calls /v1/models?refresh=true to refresh routstr21 models - Calls runIntegrationsForClients() for all registered clients - Fixed type signature in refreshModelsAndIntegrations (returns any[] not void) - Imports getClientsList and runIntegrationsForClients in cli.ts --- src/cli.ts | 31 ++++++++++++++++++++++++++++++- src/integrations/index.ts | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a06f931..1cc353d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,7 +26,8 @@ import { type RoutstrdConfig, } from "./utils/config"; import { logger } from "./utils/logger"; -import { setupIntegration } from "./integrations"; +import { setupIntegration, runIntegrationsForClients } from "./integrations"; +import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; import { generateSecretKey, nip19 } from "nostr-tools"; @@ -485,6 +486,34 @@ program await handleDaemonCommand("/ping"); }); +// Refresh - refresh models and integrations +program + .command("refresh") + .description("Refresh routstr21 models and client integrations") + .action(async () => { + await ensureDaemonRunning(); + const config = await loadConfig(); + + // Refresh models via daemon API + console.log("Refreshing routstr21 models..."); + const result = await callDaemon("/v1/models?refresh=true"); + if (result.error) { + console.log(`Model refresh failed: ${result.error}`); + process.exit(1); + } + console.log("Models refreshed."); + + // Refresh integrations for all clients + const clients = await getClientsList(); + if (clients.length > 0) { + console.log(`Refreshing ${clients.length} client integration(s)...`); + await runIntegrationsForClients(clients, config); + console.log("Client integrations refreshed."); + } else { + console.log("No clients to refresh."); + } + }); + // Models - list routstr21 models program .command("models") diff --git a/src/integrations/index.ts b/src/integrations/index.ts index 9d6106e..c70cf35 100644 --- a/src/integrations/index.ts +++ b/src/integrations/index.ts @@ -18,7 +18,7 @@ export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from " * Used both on initial daemon startup and in the recurring scheduled job. */ export async function refreshModelsAndIntegrations( - getRoutstr21Models: (force?: boolean) => Promise, + getRoutstr21Models: (force?: boolean) => Promise, config: RoutstrdConfig, label: string = "Scheduled", ): Promise {