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/bun.lock +103 -17
- package/dist/daemon/index.js +18402 -8458
- package/dist/index.js +147 -88
- package/package.json +2 -2
- package/say_hi.sh +32 -0
- package/src/cli.ts +53 -23
- package/src/daemon/models.ts +55 -0
- package/src/daemon/types.ts +6 -0
- package/src/integrations/claudecode.ts +9 -10
- package/src/integrations/hermes.ts +10 -11
- package/src/integrations/openclaw.ts +5 -6
- package/src/integrations/opencode.ts +5 -6
- package/src/integrations/pi.ts +13 -9
- package/src/integrations/registry.ts +1 -0
- package/src/tui/usage/data.ts +6 -2
- package/src/tui/usage/render.ts +20 -5
- package/src/utils/config.ts +6 -0
- package/src/utils/daemon-client.ts +32 -5
- package/new-task.md +0 -60
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
+
console.log("cocod is already initialized.");
|
|
201
213
|
} else {
|
|
202
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1072
|
+
const result = await callAuth(
|
|
1043
1073
|
`/npubs/${encodeURIComponent(npubFromPubkey(normalized))}`,
|
|
1044
1074
|
{
|
|
1045
1075
|
method: "DELETE",
|
package/src/daemon/models.ts
CHANGED
|
@@ -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
|
}
|
package/src/daemon/types.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
console.log("No models available from routstr daemon.");
|
|
61
60
|
}
|
|
62
61
|
} catch (error) {
|
|
63
|
-
|
|
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
|
-
|
|
68
|
+
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
70
69
|
} catch (error) {
|
|
71
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
console.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
|
|
34
33
|
} else {
|
|
35
|
-
|
|
34
|
+
console.log("No models available from routstr daemon, using fallback default.");
|
|
36
35
|
}
|
|
37
36
|
} catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
84
83
|
} catch (error) {
|
|
85
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
console.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
|
|
138
137
|
} catch (error) {
|
|
139
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
console.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
|
|
82
81
|
} catch (error) {
|
|
83
|
-
|
|
82
|
+
console.error("Failed to install models in opencode.json:", error);
|
|
84
83
|
}
|
|
85
84
|
}
|
package/src/integrations/pi.ts
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
console.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
|
|
76
80
|
} catch (error) {
|
|
77
|
-
|
|
81
|
+
console.error("Failed to install models in pi models.json:", error);
|
|
78
82
|
}
|
|
79
83
|
}
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -118,11 +118,15 @@ export function getTodayStart(): number {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export function formatDate(timestamp: number): string {
|
|
121
|
-
|
|
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
|
-
|
|
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 {
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -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)} ${"
|
|
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 =
|
|
620
|
-
|
|
621
|
-
|
|
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(
|
|
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)`);
|
package/src/utils/config.ts
CHANGED
|
@@ -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
|
}
|