routstrd 0.3.0 → 0.3.2

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/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ import { startDaemon } from "./start-daemon";
3
3
  import {
4
4
  handleDaemonCommand,
5
5
  callDaemon,
6
+ callAuth,
6
7
  ensureDaemonRunning,
7
8
  isDaemonRunning,
8
9
  loadConfig,
@@ -81,7 +82,7 @@ async function printLightningInvoice(invoice: string): Promise<void> {
81
82
  }
82
83
 
83
84
  async function installCocodOrExit(): Promise<void> {
84
- logger.log("cocod not found. Installing globally with bun...");
85
+ console.log("cocod not found. Installing globally with bun...");
85
86
 
86
87
  const installProc = Bun.spawn(
87
88
  ["bun", "install", "--global", "@routstr/cocod"],
@@ -93,13 +94,13 @@ async function installCocodOrExit(): Promise<void> {
93
94
 
94
95
  const installCode = await installProc.exited;
95
96
  if (installCode !== 0 || !(await isCocodInstalled())) {
96
- logger.error(
97
+ console.error(
97
98
  "Failed to install cocod. Please run 'bun install --global @routstr/cocod' manually.",
98
99
  );
99
100
  throw new Error("cocod installation failed");
100
101
  }
101
102
 
102
- logger.log("cocod installed successfully.");
103
+ console.log("cocod installed successfully.");
103
104
  }
104
105
 
105
106
  async function requireLocalDaemon(): Promise<void> {
@@ -113,12 +114,12 @@ async function requireLocalDaemon(): Promise<void> {
113
114
  }
114
115
 
115
116
  async function initDaemon(): Promise<void> {
116
- logger.log("Initializing routstrd...");
117
+ console.log("Initializing routstrd...");
117
118
 
118
119
  // Create config directory
119
120
  if (!existsSync(CONFIG_DIR)) {
120
121
  mkdirSync(CONFIG_DIR, { recursive: true });
121
- logger.log(`Created config directory: ${CONFIG_DIR}`);
122
+ console.log(`Created config directory: ${CONFIG_DIR}`);
122
123
  }
123
124
 
124
125
  // Create initial config
@@ -128,11 +129,22 @@ async function initDaemon(): Promise<void> {
128
129
  cocodPath: null,
129
130
  };
130
131
  await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
131
- logger.log(`Created config file: ${CONFIG_FILE}`);
132
+ console.log(`Created config file: ${CONFIG_FILE}`);
132
133
  }
133
134
 
134
135
  const config = await loadConfig();
135
136
 
137
+ if (!config.nsec) {
138
+ const secretKey = generateSecretKey();
139
+ const nsec = nip19.nsecEncode(secretKey);
140
+ const npub = npubFromSecretKey(secretKey);
141
+ config.nsec = nsec;
142
+ await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
143
+ console.log("\nA new Nostr identity has been generated for authentication.");
144
+ console.log(`Your npub: ${npub}`);
145
+ console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`);
146
+ }
147
+
136
148
  if (!(await isCocodInstalled(config.cocodPath))) {
137
149
  if (config.cocodPath) {
138
150
  logger.error(
@@ -190,30 +202,30 @@ async function initDaemon(): Promise<void> {
190
202
  const alreadyInitialized = combinedOutput.includes("already initialized");
191
203
 
192
204
  if (initCode !== 0 && !alreadyInitialized) {
193
- logger.error(
205
+ console.error(
194
206
  "Failed to initialize cocod. Please run 'cocod init' manually.",
195
207
  );
196
208
  return;
197
209
  }
198
210
 
199
211
  if (alreadyInitialized) {
200
- logger.log("cocod is already initialized.");
212
+ console.log("cocod is already initialized.");
201
213
  } else {
202
- logger.log("cocod initialized successfully.");
214
+ console.log("cocod initialized successfully.");
203
215
  }
204
216
 
205
217
  await startDaemon({ port: String(config.port || 8008) });
206
218
 
207
219
  await setupIntegration(config);
208
220
 
209
- logger.log("\nInitialization complete!");
210
- logger.log(
221
+ console.log("\nInitialization complete!");
222
+ console.log(
211
223
  "\n use 'routstrd receive <cashu-token>' or 'routstrd receive 2100' to top up your local wallet using Lightning!",
212
224
  );
213
- logger.log(
225
+ console.log(
214
226
  "\n full wallet commands still work too, e.g. 'routstrd wallet receive cashu <token>' and 'routstrd wallet receive bolt11 2100'.",
215
227
  );
216
- logger.log(
228
+ console.log(
217
229
  "\nTo ensure routstrd persists across system restarts, run: 'routstrd service install'",
218
230
  );
219
231
  }
@@ -334,7 +346,8 @@ program
334
346
  program
335
347
  .command("remote <url>")
336
348
  .description("Configure a remote daemon URL")
337
- .action(async (url: string) => {
349
+ .option("--auth-url <authUrl>", "URL of the auth proxy for management commands (npubs, clients, usage)")
350
+ .action(async (url: string, options: { authUrl?: string }) => {
338
351
  try {
339
352
  new URL(url);
340
353
  } catch {
@@ -342,12 +355,24 @@ program
342
355
  process.exit(1);
343
356
  }
344
357
 
358
+ if (options.authUrl) {
359
+ try {
360
+ new URL(options.authUrl);
361
+ } catch {
362
+ console.error(`Invalid auth URL: ${options.authUrl}`);
363
+ process.exit(1);
364
+ }
365
+ }
366
+
345
367
  if (!existsSync(CONFIG_DIR)) {
346
368
  mkdirSync(CONFIG_DIR, { recursive: true });
347
369
  }
348
370
 
349
371
  const config = await loadConfig();
350
372
  const updates: Partial<RoutstrdConfig> = { daemonUrl: url };
373
+ if (options.authUrl) {
374
+ updates.authUrl = options.authUrl;
375
+ }
351
376
  let generatedNpub: string | undefined;
352
377
 
353
378
  if (!config.nsec) {
@@ -366,6 +391,9 @@ program
366
391
  await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
367
392
 
368
393
  console.log(`Remote daemon URL set to: ${url}`);
394
+ if (options.authUrl) {
395
+ console.log(`Auth proxy URL set to: ${options.authUrl}`);
396
+ }
369
397
  if (generatedNpub) {
370
398
  console.log(
371
399
  `\nA new Nostr identity has been generated for remote authentication.`,
@@ -666,7 +694,7 @@ program
666
694
  ? Math.min(requested, 1000)
667
695
  : 10;
668
696
 
669
- const result = await callDaemon(`/usage?limit=${limit}`);
697
+ const result = await callAuth(`/usage?limit=${limit}`);
670
698
  if (result.error) {
671
699
  console.log(result.error);
672
700
  process.exit(1);
@@ -715,10 +743,12 @@ const providersCmd = program
715
743
  providersCmd
716
744
  .command("list")
717
745
  .description("List all providers with their enabled/disabled status")
718
- .action(async () => {
746
+ .option("--refresh", "Force re-fetch all Nostr events and refresh models from all enabled providers", false)
747
+ .action(async (options: { refresh: boolean }) => {
719
748
  await ensureDaemonRunning();
720
749
 
721
- const result = await callDaemon("/providers");
750
+ const query = options.refresh ? "/providers?refresh=true" : "/providers";
751
+ const result = await callDaemon(query);
722
752
  if (result.error) {
723
753
  console.log(result.error);
724
754
  process.exit(1);
@@ -882,7 +912,7 @@ npubsCmd
882
912
  await ensureDaemonRunning();
883
913
  const config = await loadConfig();
884
914
  const userNpub = getUserNpub(config);
885
- const result = await callDaemon("/npubs");
915
+ const result = await callAuth("/npubs");
886
916
  if (result.error) {
887
917
  console.log(result.error);
888
918
  process.exit(1);
@@ -925,7 +955,7 @@ npubsCmd
925
955
  );
926
956
  process.exit(1);
927
957
  }
928
- const result = await callDaemon("/npubs");
958
+ const result = await callAuth("/npubs");
929
959
  if (result.error) {
930
960
  console.log(result.error);
931
961
  process.exit(1);
@@ -943,7 +973,7 @@ npubsCmd
943
973
  console.error("Failed to normalize user npub.");
944
974
  process.exit(1);
945
975
  }
946
- const addResult = await callDaemon("/npubs", {
976
+ const addResult = await callAuth("/npubs", {
947
977
  method: "POST",
948
978
  body: { npub: npubFromPubkey(normalized) },
949
979
  });
@@ -977,7 +1007,7 @@ npubsCmd
977
1007
  process.exit(1);
978
1008
  }
979
1009
  const body: Record<string, string> = { npub: npubFromPubkey(normalized), role: options.role };
980
- const result = await callDaemon("/npubs", {
1010
+ const result = await callAuth("/npubs", {
981
1011
  method: "POST",
982
1012
  body,
983
1013
  });
@@ -1010,7 +1040,7 @@ npubsCmd
1010
1040
  console.error("Invalid role. Expected 'admin' or 'user'.");
1011
1041
  process.exit(1);
1012
1042
  }
1013
- const result = await callDaemon("/npubs", {
1043
+ const result = await callAuth("/npubs", {
1014
1044
  method: "PATCH",
1015
1045
  body: { npub: npubFromPubkey(normalized), role: options.role },
1016
1046
  });
@@ -1039,7 +1069,7 @@ npubsCmd
1039
1069
  console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
1040
1070
  process.exit(1);
1041
1071
  }
1042
- const result = await callDaemon(
1072
+ const result = await callAuth(
1043
1073
  `/npubs/${encodeURIComponent(npubFromPubkey(normalized))}`,
1044
1074
  {
1045
1075
  method: "DELETE",
@@ -120,9 +120,64 @@ export function createModelService(modelManager: ModelManager, store: SdkStore)
120
120
  };
121
121
  };
122
122
 
123
+ /**
124
+ * Force-refresh everything: re-fetch Nostr provider discovery events,
125
+ * routstr21 model list, Nostr review events, and models from all enabled
126
+ * providers. Syncs the discovered provider list into the store.
127
+ */
128
+ const refreshProvidersAndModels = async (): Promise<void> => {
129
+ // Reset the bootstrap promise so we don't reuse cached results
130
+ providerBootstrapPromise = null;
131
+
132
+ console.log("Force-refreshing providers from Nostr...");
133
+
134
+ // Force-refresh provider discovery from Nostr (kind 38421)
135
+ const providers = await modelManager.bootstrapProviders(false, true);
136
+ console.log(`Discovered ${providers.length} providers from Nostr`);
137
+
138
+ // Force-refresh routstr21 models from Nostr (kind 38423)
139
+ const routstr21ModelIds = await modelManager.fetchRoutstr21Models(true);
140
+ console.log(`Fetched ${routstr21ModelIds.length} routstr21 model IDs from Nostr`);
141
+
142
+ // Force-refresh models from all providers
143
+ const models = await modelManager.fetchModels(providers, true);
144
+ console.log(`Fetched ${models.length} models from ${providers.length} providers`);
145
+
146
+ // Sync review events from Nostr (kind 38425) and apply disabled status
147
+ const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr(
148
+ providers,
149
+ undefined,
150
+ true,
151
+ );
152
+ if (reviewedDisabled.length > 0) {
153
+ console.log(
154
+ `Review sync disabled ${reviewedDisabled.length} provider(s): ${reviewedDisabled.join(", ")}`,
155
+ );
156
+ }
157
+
158
+ // Sync discovered providers into the store
159
+ const { baseUrlsList, setBaseUrlsList, disabledProviders, setDisabledProviders } =
160
+ store.getState() as any;
161
+
162
+ // Replace baseUrlsList with the fresh provider list
163
+ setBaseUrlsList(providers);
164
+
165
+ // Merge review-disabled providers into the store's disabled list
166
+ const existingDisabled = new Set(disabledProviders || []);
167
+ for (const url of reviewedDisabled) {
168
+ existingDisabled.add(url);
169
+ }
170
+ setDisabledProviders([...existingDisabled]);
171
+
172
+ console.log(
173
+ `Provider refresh complete: ${providers.length} total, ${existingDisabled.size} disabled`,
174
+ );
175
+ };
176
+
123
177
  return {
124
178
  ensureProvidersBootstrapped,
125
179
  getRoutstr21Models,
126
180
  getModelProviders,
181
+ refreshProvidersAndModels,
127
182
  };
128
183
  }
@@ -11,6 +11,12 @@ export type UsageData = {
11
11
  totalTokens: number;
12
12
  cost: number;
13
13
  satsCost: number;
14
+ provider?: string;
15
+ inputMsats?: number;
16
+ outputMsats?: number;
17
+ totalMsats?: number;
18
+ cacheReadInputTokens?: number;
19
+ cacheCreationInputTokens?: number;
14
20
  };
15
21
 
16
22
  export type UsageTrackingEntry = UsageData & {
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
- import { logger } from "../utils/logger";
6
5
  import type { IntegrationConfig, RoutstrModel } from "./registry";
7
6
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
7
 
@@ -13,8 +12,8 @@ export async function installClaudeCodeIntegration(
13
12
  ): Promise<void> {
14
13
  const { name, configPath } = integrationConfig;
15
14
 
16
- logger.log(`\nInstalling routstr configuration in ${configPath}...`);
17
- logger.log(`Using API key for ${name}`);
15
+ console.log(`\nInstalling routstr configuration in ${configPath}...`);
16
+ console.log(`Using API key for ${name}`);
18
17
 
19
18
  const baseUrl = getDaemonBaseUrl(config);
20
19
 
@@ -28,7 +27,7 @@ export async function installClaudeCodeIntegration(
28
27
  settings = JSON.parse(content);
29
28
  }
30
29
  } catch (error) {
31
- logger.error(`Error reading ${configPath}, creating new one.`);
30
+ console.error(`Error reading ${configPath}, creating new one.`);
32
31
  }
33
32
 
34
33
  if (!settings.env) {
@@ -49,25 +48,25 @@ export async function installClaudeCodeIntegration(
49
48
  settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = opus.id;
50
49
  settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = sonnet.id;
51
50
  settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = haiku.id;
52
- logger.log(`Set Claude models: Opus=${opus.id}, Sonnet=${sonnet.id}, Haiku=${haiku.id}`);
51
+ console.log(`Set Claude models: Opus=${opus.id}, Sonnet=${sonnet.id}, Haiku=${haiku.id}`);
53
52
  } else if (models.length > 0) {
54
53
  const model = models[0]!;
55
- logger.log(`Only ${models.length} models available, falling back to defaults.`);
54
+ console.log(`Only ${models.length} models available, falling back to defaults.`);
56
55
  settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = model.id;
57
56
  settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = model.id;
58
57
  settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = model.id;
59
58
  } else {
60
- logger.log("No models available from routstr daemon.");
59
+ console.log("No models available from routstr daemon.");
61
60
  }
62
61
  } catch (error) {
63
- logger.error("Failed to fetch models for Claude Code integration:", error);
62
+ console.error("Failed to fetch models for Claude Code integration:", error);
64
63
  }
65
64
 
66
65
  try {
67
66
  mkdirSync(dirname(configPath), { recursive: true });
68
67
  await writeFile(configPath, JSON.stringify(settings, null, 2));
69
- logger.log(`Successfully updated ${configPath} with routstr settings.`);
68
+ console.log(`Successfully updated ${configPath} with routstr settings.`);
70
69
  } catch (error) {
71
- logger.error(`Failed to write to ${configPath}:`, error);
70
+ console.error(`Failed to write to ${configPath}:`, error);
72
71
  }
73
72
  }
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
- import { logger } from "../utils/logger";
6
5
  import type { IntegrationConfig, RoutstrModel } from "./registry";
7
6
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
7
 
@@ -13,8 +12,8 @@ export async function installHermesIntegration(
13
12
  ): Promise<void> {
14
13
  const { name, configPath } = integrationConfig;
15
14
 
16
- logger.log(`\nInstalling routstr configuration in ${configPath}...`);
17
- logger.log(`Using API key for ${name}`);
15
+ console.log(`\nInstalling routstr configuration in ${configPath}...`);
16
+ console.log(`Using API key for ${name}`);
18
17
 
19
18
  const baseUrl = getDaemonBaseUrl(config);
20
19
  const baseUrlV1 = `${baseUrl}/v1`;
@@ -27,16 +26,16 @@ export async function installHermesIntegration(
27
26
 
28
27
  if (models.length >= 3) {
29
28
  defaultModel = models[2]!.id;
30
- logger.log(`Set default model to 3rd available model: ${defaultModel}`);
29
+ console.log(`Set default model to 3rd available model: ${defaultModel}`);
31
30
  } else if (models.length > 0) {
32
31
  defaultModel = models[0]!.id;
33
- logger.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
32
+ console.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
34
33
  } else {
35
- logger.log("No models available from routstr daemon, using fallback default.");
34
+ console.log("No models available from routstr daemon, using fallback default.");
36
35
  }
37
36
  } catch (error) {
38
- logger.error("Failed to fetch models for Hermes integration:", error);
39
- logger.log("Using fallback default model.");
37
+ console.error("Failed to fetch models for Hermes integration:", error);
38
+ console.log("Using fallback default model.");
40
39
  }
41
40
 
42
41
  let content = "";
@@ -45,7 +44,7 @@ export async function installHermesIntegration(
45
44
  content = await readFile(configPath, "utf-8");
46
45
  }
47
46
  } catch (error) {
48
- logger.error(`Error reading ${configPath}, creating new one.`);
47
+ console.error(`Error reading ${configPath}, creating new one.`);
49
48
  }
50
49
 
51
50
  // Remove existing model block
@@ -80,8 +79,8 @@ export async function installHermesIntegration(
80
79
  try {
81
80
  mkdirSync(dirname(configPath), { recursive: true });
82
81
  await writeFile(configPath, newContent);
83
- logger.log(`Successfully updated ${configPath} with routstr settings.`);
82
+ console.log(`Successfully updated ${configPath} with routstr settings.`);
84
83
  } catch (error) {
85
- logger.error(`Failed to write to ${configPath}:`, error);
84
+ console.error(`Failed to write to ${configPath}:`, error);
86
85
  }
87
86
  }
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
- import { logger } from "../utils/logger";
6
5
  import type { IntegrationConfig, RoutstrModel } from "./registry";
7
6
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
7
 
@@ -56,8 +55,8 @@ export async function installOpenClawIntegration(
56
55
  ): Promise<void> {
57
56
  const { name, configPath } = integrationConfig;
58
57
 
59
- logger.log("\nInstalling routstr models in openclaw.json...");
60
- logger.log(`Using API key for ${name}`);
58
+ console.log("\nInstalling routstr models in openclaw.json...");
59
+ console.log(`Using API key for ${name}`);
61
60
 
62
61
  const baseUrl = getDaemonBaseUrl(config);
63
62
 
@@ -92,7 +91,7 @@ export async function installOpenClawIntegration(
92
91
  const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
93
92
 
94
93
  if (models.length === 0) {
95
- logger.log("No models found from routstr daemon.");
94
+ console.log("No models found from routstr daemon.");
96
95
  return;
97
96
  }
98
97
 
@@ -134,8 +133,8 @@ export async function installOpenClawIntegration(
134
133
  // openclawConfig.agents.defaults.models = aliasMap;
135
134
 
136
135
  await writeFile(configPath, JSON.stringify(openclawConfig, null, 2));
137
- logger.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
136
+ console.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
138
137
  } catch (error) {
139
- logger.error("Failed to install models in openclaw.json:", error);
138
+ console.error("Failed to install models in openclaw.json:", error);
140
139
  }
141
140
  }
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
- import { logger } from "../utils/logger";
6
5
  import type { IntegrationConfig, RoutstrModel } from "./registry";
7
6
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
7
 
@@ -15,8 +14,8 @@ export async function installOpencodeIntegration(
15
14
  ): Promise<void> {
16
15
  const { name, configPath } = integrationConfig;
17
16
 
18
- logger.log("\nInstalling routstr models in opencode.json...");
19
- logger.log(`Using API key for ${name}`);
17
+ console.log("\nInstalling routstr models in opencode.json...");
18
+ console.log(`Using API key for ${name}`);
20
19
 
21
20
  const baseUrl = getDaemonBaseUrl(config);
22
21
 
@@ -56,7 +55,7 @@ export async function installOpencodeIntegration(
56
55
  const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
57
56
 
58
57
  if (models.length === 0) {
59
- logger.log("No models found from routstr daemon.");
58
+ console.log("No models found from routstr daemon.");
60
59
  return;
61
60
  }
62
61
 
@@ -78,8 +77,8 @@ export async function installOpencodeIntegration(
78
77
  opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
79
78
 
80
79
  await writeFile(configPath, JSON.stringify(opencodeConfig, null, 2));
81
- logger.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
80
+ console.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
82
81
  } catch (error) {
83
- logger.error("Failed to install models in opencode.json:", error);
82
+ console.error("Failed to install models in opencode.json:", error);
84
83
  }
85
84
  }
@@ -2,12 +2,12 @@ import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
4
  import type { RoutstrdConfig } from "../utils/config";
5
- import { logger } from "../utils/logger";
6
5
  import type { IntegrationConfig, RoutstrModel } from "./registry";
7
6
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
7
 
9
8
  type PiModelEntry = {
10
9
  id: string;
10
+ contextWindow?: number;
11
11
  };
12
12
 
13
13
  type PiProviderConfig = {
@@ -28,8 +28,8 @@ export async function installPiIntegration(
28
28
  ): Promise<void> {
29
29
  const { name, configPath } = integrationConfig;
30
30
 
31
- logger.log("\nInstalling routstr models in pi models.json...");
32
- logger.log(`Using API key for ${name}`);
31
+ console.log("\nInstalling routstr models in pi models.json...");
32
+ console.log(`Using API key for ${name}`);
33
33
 
34
34
  const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
35
35
 
@@ -56,13 +56,17 @@ export async function installPiIntegration(
56
56
  const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
57
57
 
58
58
  if (models.length === 0) {
59
- logger.log("No models found from routstr daemon.");
59
+ console.log("No models found from routstr daemon.");
60
60
  return;
61
61
  }
62
62
 
63
- const providerModels: PiModelEntry[] = models.map((model) => ({
64
- id: model.id,
65
- }));
63
+ const providerModels: PiModelEntry[] = models.map((model) => {
64
+ const entry: PiModelEntry = { id: model.id };
65
+ if (model.context_length !== undefined && model.context_length > 0) {
66
+ entry.contextWindow = model.context_length;
67
+ }
68
+ return entry;
69
+ });
66
70
 
67
71
  piConfig.providers["routstr"] = {
68
72
  baseUrl,
@@ -72,8 +76,8 @@ export async function installPiIntegration(
72
76
  };
73
77
 
74
78
  await writeFile(configPath, JSON.stringify(piConfig, null, 2));
75
- logger.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
79
+ console.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
76
80
  } catch (error) {
77
- logger.error("Failed to install models in pi models.json:", error);
81
+ console.error("Failed to install models in pi models.json:", error);
78
82
  }
79
83
  }
@@ -15,6 +15,7 @@ export interface IntegrationConfig {
15
15
  export type RoutstrModel = {
16
16
  id: string;
17
17
  name?: string;
18
+ context_length?: number;
18
19
  };
19
20
 
20
21
  export type IntegrationFn = (
@@ -118,11 +118,15 @@ export function getTodayStart(): number {
118
118
  }
119
119
 
120
120
  export function formatDate(timestamp: number): string {
121
- return new Date(timestamp).toISOString().split("T")[0] ?? "";
121
+ const d = new Date(timestamp);
122
+ const p = (n: number) => String(n).padStart(2, "0");
123
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
122
124
  }
123
125
 
124
126
  export function formatTime(timestamp: number): string {
125
- return new Date(timestamp).toISOString().split("T")[1]?.slice(0, 8) ?? "";
127
+ const d = new Date(timestamp);
128
+ const p = (n: number) => String(n).padStart(2, "0");
129
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
126
130
  }
127
131
 
128
132
  export function formatNumber(n: number): string {
@@ -609,20 +609,35 @@ export function renderRecent(stats: UsageStats, width: number): string {
609
609
  if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
610
610
 
611
611
  const clientCol = 14;
612
+ const tokensCol = 18;
613
+ const costCol = 18;
614
+ const providerCol = Math.max(16, width - 4 - 10 - 18 - tokensCol - costCol - clientCol - 5);
615
+ const msatsToSats = (msats?: number) => typeof msats === "number" ? msats / 1000 : 0;
612
616
  const lines: string[] = [];
613
- lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".padEnd(16)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
617
+ lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"I/CR/CW/O".padEnd(tokensCol)} ${"I/O/T in sats".padEnd(costCol)} ${"BASE:PROVIDER".padEnd(providerCol)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
614
618
  lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset);
615
619
 
616
620
  for (const entry of recentEntries) {
617
621
  const time = formatTime(entry.timestamp).slice(0, 8);
618
622
  const model = entry.modelId.slice(0, 18).padEnd(18);
619
- const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
620
- const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
621
- const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, 16).padEnd(16);
623
+ const tokens = [
624
+ entry.promptTokens,
625
+ entry.cacheReadInputTokens || 0,
626
+ entry.cacheCreationInputTokens || 0,
627
+ entry.completionTokens,
628
+ ].map(formatNumber).join("/");
629
+ const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
630
+ const cost = [
631
+ formatCost(msatsToSats(entry.inputMsats)),
632
+ formatCost(msatsToSats(entry.outputMsats)),
633
+ formatCost(totalSats),
634
+ ].join("/");
635
+ const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "");
636
+ const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol);
622
637
  const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
623
638
  const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
624
639
  const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
625
- lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(10)} ${COLORS.green}${cost}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
640
+ lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(tokensCol)} ${COLORS.green}${cost.padEnd(costCol)}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
626
641
  }
627
642
 
628
643
  return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
@@ -35,9 +35,15 @@ export interface RoutstrdConfig {
35
35
  cocodPath: string | null;
36
36
  mode?: "xcashu" | "apikeys";
37
37
  daemonUrl?: string;
38
+ /** URL of the auth proxy (routstrd-auth) for management endpoints (npubs, clients, usage).
39
+ * Defaults to daemonUrl or localhost:{port} if not set. */
40
+ authUrl?: string;
38
41
  nsec?: string;
39
42
  /** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
40
43
  routstrPubkey?: string;
44
+ /** Nostr relay URLs for provider/model discovery (kinds 38421/38423/38425).
45
+ * When unset, each method uses its own built-in defaults. */
46
+ relays?: string[];
41
47
  /** NWC integration configuration */
42
48
  nwc?: NwcConfig;
43
49
  }