mirror of
https://github.com/Routstr/routstrd.git
synced 2026-08-09 03:44:38 +00:00
+38
-153
@@ -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,8 @@ import {
|
||||
type RoutstrdConfig,
|
||||
} from "./utils/config";
|
||||
import { logger } from "./utils/logger";
|
||||
import {
|
||||
setupIntegration,
|
||||
CLIENT_CONFIGS,
|
||||
CLIENT_INTEGRATIONS,
|
||||
} 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";
|
||||
@@ -487,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")
|
||||
@@ -776,90 +803,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 <id>")
|
||||
.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 +829,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 <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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+8
-27
@@ -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 { getClientsFromStore } from "../utils/clients";
|
||||
import { refreshModelsAndIntegrations } from "../integrations";
|
||||
import { RoutstrClient } from "@routstr/sdk";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -37,7 +36,8 @@ async function main(): Promise<void> {
|
||||
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<void> {
|
||||
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 = getClientsFromStore(store);
|
||||
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<void> {
|
||||
|
||||
// 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 = getClientsFromStore(store);
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import type { RoutstrdConfig } from "../utils/config";
|
||||
import { logger } from "../utils/logger";
|
||||
import {
|
||||
ensureDaemonClient,
|
||||
addDaemonClient,
|
||||
type DaemonClient,
|
||||
} from "../utils/daemon-client";
|
||||
} 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<any[]>,
|
||||
config: RoutstrdConfig,
|
||||
label: string = "Scheduled",
|
||||
): Promise<void> {
|
||||
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<string> {
|
||||
process.stdout.write(question);
|
||||
|
||||
@@ -69,7 +90,7 @@ export async function setupIntegration(
|
||||
}
|
||||
|
||||
const integrationConfig = CLIENT_CONFIGS[key]!;
|
||||
const { client, created } = await ensureDaemonClient(
|
||||
const { client, created } = await addDaemonClient(
|
||||
integrationConfig.name,
|
||||
integrationConfig.clientId,
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
||||
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))
|
||||
|
||||
+241
-3
@@ -1,4 +1,45 @@
|
||||
import { callDaemon } from "./daemon-client";
|
||||
import {
|
||||
callDaemon,
|
||||
loadConfig,
|
||||
getDaemonBaseUrl,
|
||||
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;
|
||||
@@ -8,6 +49,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).
|
||||
@@ -37,6 +86,7 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
|
||||
* Use this when running remotely (CLI in remote mode).
|
||||
*/
|
||||
export async function getClientsList(): Promise<ClientEntry[]> {
|
||||
const config = await loadConfig();
|
||||
const result = await callDaemon("/clients");
|
||||
const clients = (
|
||||
result.output as
|
||||
@@ -56,11 +106,199 @@ export async function getClientsList(): Promise<ClientEntry[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
return clients.map((c) => ({
|
||||
clientId: c.id,
|
||||
const suffix = config.daemonUrl ? getNpubSuffix(config) : null;
|
||||
|
||||
return clients
|
||||
.filter((c) => !suffix || c.id.endsWith(`-${suffix}`))
|
||||
.map((c) => ({
|
||||
clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id,
|
||||
name: c.name,
|
||||
apiKey: c.apiKey,
|
||||
createdAt: c.createdAt,
|
||||
lastUsed: c.lastUsed,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function addDaemonClient(
|
||||
name: string,
|
||||
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 };
|
||||
}
|
||||
|
||||
// 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, id: derivedId },
|
||||
});
|
||||
|
||||
|
||||
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, created: true };
|
||||
}
|
||||
|
||||
export async function listClientsAction(): Promise<void> {
|
||||
await ensureDaemonRunning();
|
||||
|
||||
const entries = await getClientsList();
|
||||
|
||||
const clients = entries.map((c) => ({
|
||||
id: c.clientId,
|
||||
name: c.name,
|
||||
apiKey: c.apiKey,
|
||||
createdAt: c.createdAt,
|
||||
lastUsed: c.lastUsed,
|
||||
}));
|
||||
|
||||
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<void> {
|
||||
await ensureDaemonRunning();
|
||||
|
||||
const config = await loadConfig();
|
||||
const suffix = getNpubSuffix(config);
|
||||
const resolvedId = suffix ? addSuffixToId(id, suffix) : id;
|
||||
|
||||
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<void> {
|
||||
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 addDaemonClient(
|
||||
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 <name>' not specified",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const { message, client, created } = await addDaemonClient(options.name);
|
||||
|
||||
if (!created) {
|
||||
console.log(`Client '${options.name}' 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RoutstrdConfig> {
|
||||
try {
|
||||
if (existsSync(CONFIG_FILE)) {
|
||||
@@ -160,17 +98,6 @@ export async function isDaemonRunning(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -262,4 +189,4 @@ export async function handleDaemonCommand(
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user