routstrd 0.2.4 → 0.2.5
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/dist/daemon/index.js +2855 -8143
- package/dist/index.js +2618 -2537
- package/package.json +2 -2
- package/src/cli.ts +107 -4
- package/src/daemon/http/index.ts +59 -0
- package/src/daemon/index.ts +1 -1
- package/src/start-daemon.ts +1 -1
- package/src/tui/usage/app.ts +1 -1
- package/src/tui/usage/data.ts +1 -1
- package/src/{cli-shared.ts → utils/daemon-client.ts} +3 -83
- package/.claude/settings.local.json +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^3.1.1",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.0",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"commander": "^14.0.2",
|
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { program } from "commander";
|
|
1
2
|
import { startDaemon } from "./start-daemon";
|
|
2
3
|
import {
|
|
3
|
-
program,
|
|
4
4
|
handleDaemonCommand,
|
|
5
5
|
callDaemon,
|
|
6
6
|
ensureDaemonRunning,
|
|
7
7
|
isDaemonRunning,
|
|
8
8
|
loadConfig,
|
|
9
|
-
} from "./
|
|
9
|
+
} from "./utils/daemon-client";
|
|
10
10
|
import { existsSync, mkdirSync } from "fs";
|
|
11
11
|
import { execSync } from "child_process";
|
|
12
12
|
import {
|
|
@@ -211,6 +211,81 @@ program
|
|
|
211
211
|
.description("Routstr daemon - Manage routstr processes")
|
|
212
212
|
.version(cliVersion, "--version", "output the version number");
|
|
213
213
|
|
|
214
|
+
program
|
|
215
|
+
.command("refund")
|
|
216
|
+
.description("Refund pending tokens and API keys to a specified mint")
|
|
217
|
+
.option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)")
|
|
218
|
+
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
219
|
+
.action(async (options: { mintUrl?: string; yes: boolean }) => {
|
|
220
|
+
const config = await loadConfig();
|
|
221
|
+
|
|
222
|
+
let mintUrl = options.mintUrl;
|
|
223
|
+
if (!mintUrl) {
|
|
224
|
+
const balanceResponse = await fetch(`http://localhost:${config.port}/balance`);
|
|
225
|
+
const balanceResult = (await balanceResponse.json()) as {
|
|
226
|
+
output?: { balances?: Record<string, number> };
|
|
227
|
+
error?: string;
|
|
228
|
+
};
|
|
229
|
+
if (balanceResult.error) {
|
|
230
|
+
console.log(balanceResult.error);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const balances = balanceResult.output?.balances;
|
|
234
|
+
if (!balances || Object.keys(balances).length === 0) {
|
|
235
|
+
console.log("No mint URLs found in wallet balance");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
mintUrl = Object.keys(balances)[0];
|
|
239
|
+
console.log(`Using mint URL: ${mintUrl}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(`http://localhost:${config.port}/refund`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
body: JSON.stringify({ mintUrl }),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
const errorData = (await response.json()) as { error?: string };
|
|
251
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = (await response.json()) as {
|
|
255
|
+
output?: {
|
|
256
|
+
message: string;
|
|
257
|
+
pendingTokens: number;
|
|
258
|
+
apiKeys: number;
|
|
259
|
+
results: Array<{ baseUrl: string; success: boolean }>;
|
|
260
|
+
};
|
|
261
|
+
error?: string;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (result.error) {
|
|
265
|
+
console.log(result.error);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (result.output) {
|
|
270
|
+
console.log(result.output.message);
|
|
271
|
+
console.log(`\nPending tokens: ${result.output.pendingTokens}`);
|
|
272
|
+
console.log(`API keys: ${result.output.apiKeys}`);
|
|
273
|
+
console.log("\nResults:");
|
|
274
|
+
for (const r of result.output.results) {
|
|
275
|
+
console.log(` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const message = (error as Error).message;
|
|
280
|
+
if (message?.includes("fetch failed") || message?.includes("Connection refused")) {
|
|
281
|
+
console.error("Daemon is not running");
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
console.error(message);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
214
289
|
// Onboard - initialize the daemon
|
|
215
290
|
program
|
|
216
291
|
.command("onboard")
|
|
@@ -659,6 +734,34 @@ clientsCmd
|
|
|
659
734
|
}
|
|
660
735
|
});
|
|
661
736
|
|
|
737
|
+
clientsCmd
|
|
738
|
+
.command("delete <id>")
|
|
739
|
+
.description("Delete a client by its ID")
|
|
740
|
+
.action(async (id: string) => {
|
|
741
|
+
await ensureDaemonRunning();
|
|
742
|
+
|
|
743
|
+
const result = await callDaemon("/clients/delete", {
|
|
744
|
+
method: "POST",
|
|
745
|
+
body: { id },
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (result.error) {
|
|
749
|
+
console.log(result.error);
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const output = result.output as
|
|
754
|
+
| {
|
|
755
|
+
message: string;
|
|
756
|
+
id: string;
|
|
757
|
+
}
|
|
758
|
+
| undefined;
|
|
759
|
+
|
|
760
|
+
if (output) {
|
|
761
|
+
console.log(output.message);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
662
765
|
clientsCmd
|
|
663
766
|
.command("add")
|
|
664
767
|
.description("Add a new client or set up client integrations")
|
|
@@ -714,7 +817,7 @@ clientsCmd
|
|
|
714
817
|
}
|
|
715
818
|
|
|
716
819
|
console.log(
|
|
717
|
-
`\n Access Routstr at: http://localhost:${config.port || 8008}`,
|
|
820
|
+
`\n Access Routstr at: http://localhost:${config.port || 8008}/v1`,
|
|
718
821
|
);
|
|
719
822
|
return;
|
|
720
823
|
}
|
|
@@ -754,7 +857,7 @@ clientsCmd
|
|
|
754
857
|
console.log(` Name: ${output.client.name}`);
|
|
755
858
|
console.log(` API Key: ${output.client.apiKey}`);
|
|
756
859
|
console.log(
|
|
757
|
-
`\n Access Routstr at: http://localhost:${config.port || 8008}`,
|
|
860
|
+
`\n Access Routstr at: http://localhost:${config.port || 8008}/v1`,
|
|
758
861
|
);
|
|
759
862
|
}
|
|
760
863
|
});
|
package/src/daemon/http/index.ts
CHANGED
|
@@ -831,6 +831,65 @@ export function createDaemonRequestHandler(deps: {
|
|
|
831
831
|
return;
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
+
if (req.method === "POST" && url.pathname === "/clients/delete") {
|
|
835
|
+
try {
|
|
836
|
+
const bodyText = await readBody(req);
|
|
837
|
+
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
838
|
+
const id = body.id as string | undefined;
|
|
839
|
+
|
|
840
|
+
if (!id || typeof id !== "string" || id.trim() === "") {
|
|
841
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
842
|
+
res.end(
|
|
843
|
+
JSON.stringify({
|
|
844
|
+
error:
|
|
845
|
+
"Missing required 'id' field (must be a non-empty string).",
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const state = deps.store.getState();
|
|
852
|
+
const existingClients = state.clientIds || [];
|
|
853
|
+
const index = existingClients.findIndex(
|
|
854
|
+
(c: { clientId: string }) => c.clientId === id,
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
if (index === -1) {
|
|
858
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
859
|
+
res.end(
|
|
860
|
+
JSON.stringify({
|
|
861
|
+
error: `Client with id '${id}' not found.`,
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const removedClient = existingClients[index];
|
|
868
|
+
const updatedClients = existingClients.filter(
|
|
869
|
+
(_c: unknown, i: number) => i !== index,
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
deps.store.getState().setClientIds(updatedClients);
|
|
873
|
+
|
|
874
|
+
logger.log(
|
|
875
|
+
`Deleted client '${removedClient.name}' with id '${id}'`,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
879
|
+
res.end(
|
|
880
|
+
JSON.stringify({
|
|
881
|
+
output: {
|
|
882
|
+
message: `Client '${removedClient.name}' deleted successfully`,
|
|
883
|
+
id,
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
respondWithError(res, error);
|
|
889
|
+
}
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
834
893
|
if (req.method === "GET" && url.pathname === "/providers") {
|
|
835
894
|
try {
|
|
836
895
|
const state = deps.store.getState();
|
package/src/daemon/index.ts
CHANGED
|
@@ -203,7 +203,7 @@ async function main(): Promise<void> {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
server.listen(port, async () => {
|
|
206
|
-
logger.log(`Routstr daemon listening on http://localhost:${port}`);
|
|
206
|
+
logger.log(`Routstr daemon listening on http://localhost:${port}/v1`);
|
|
207
207
|
|
|
208
208
|
// Start the recurring model refresh job after initial bootstrap
|
|
209
209
|
void ensureProvidersBootstrapped()
|
package/src/start-daemon.ts
CHANGED
|
@@ -26,7 +26,7 @@ export async function startDaemon(
|
|
|
26
26
|
});
|
|
27
27
|
clearTimeout(timeoutId);
|
|
28
28
|
if (existing.ok) {
|
|
29
|
-
logger.log(`Routstr daemon already running on http://localhost:${port}`);
|
|
29
|
+
logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
} catch {
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { COLORS } from "./constants.ts";
|
|
29
29
|
import { renderHeader, renderSearchBar, renderSeparator, renderTabContent, renderTabs } from "./render.ts";
|
|
30
30
|
import type { TabId, UsageStats } from "./types.ts";
|
|
31
|
-
import { isDaemonRunning } from "../../
|
|
31
|
+
import { isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
32
32
|
|
|
33
33
|
export async function runUsageTui(): Promise<void> {
|
|
34
34
|
const running = await isDaemonRunning();
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
|
-
import { callDaemon, isDaemonRunning } from "../../
|
|
2
|
+
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
3
3
|
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export interface BalanceKey {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { program } from "commander";
|
|
2
1
|
import { existsSync } from "fs";
|
|
3
2
|
import {
|
|
4
3
|
CONFIG_FILE,
|
|
5
4
|
DEFAULT_CONFIG,
|
|
6
5
|
LOGS_DIR,
|
|
7
6
|
type RoutstrdConfig,
|
|
8
|
-
} from "./
|
|
7
|
+
} from "./config";
|
|
9
8
|
|
|
10
9
|
export interface CommandResponse {
|
|
11
10
|
output?: unknown;
|
|
@@ -24,7 +23,7 @@ export async function loadConfig(): Promise<RoutstrdConfig> {
|
|
|
24
23
|
return DEFAULT_CONFIG;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
async function callDaemon(
|
|
26
|
+
export async function callDaemon(
|
|
28
27
|
path: string,
|
|
29
28
|
options: { method?: "GET" | "POST"; body?: object } = {},
|
|
30
29
|
): Promise<CommandResponse> {
|
|
@@ -61,9 +60,7 @@ export async function startDaemonProcess(): Promise<void> {
|
|
|
61
60
|
await Bun.$`mkdir -p ${LOGS_DIR}`;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
const proc = Bun.spawn([
|
|
65
|
-
"bun", "run", `${import.meta.dir}/daemon/index.ts`
|
|
66
|
-
], {
|
|
63
|
+
const proc = Bun.spawn(["bun", "run", `${import.meta.dir}/../daemon/index.ts`], {
|
|
67
64
|
stdout: "inherit",
|
|
68
65
|
stderr: "inherit",
|
|
69
66
|
stdin: "ignore",
|
|
@@ -128,80 +125,3 @@ export async function handleDaemonCommand(
|
|
|
128
125
|
process.exit(1);
|
|
129
126
|
}
|
|
130
127
|
}
|
|
131
|
-
|
|
132
|
-
export { program, callDaemon };
|
|
133
|
-
|
|
134
|
-
program
|
|
135
|
-
.command("refund")
|
|
136
|
-
.description("Refund pending tokens and API keys to a specified mint")
|
|
137
|
-
.option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)")
|
|
138
|
-
.option("-y, --yes", "Skip confirmation prompt", false)
|
|
139
|
-
.action(async (options: { mintUrl?: string; yes: boolean }) => {
|
|
140
|
-
const config = await loadConfig();
|
|
141
|
-
|
|
142
|
-
let mintUrl = options.mintUrl;
|
|
143
|
-
if (!mintUrl) {
|
|
144
|
-
const balanceResponse = await fetch(`http://localhost:${config.port}/balance`);
|
|
145
|
-
const balanceResult = (await balanceResponse.json()) as {
|
|
146
|
-
output?: { balances?: Record<string, number> };
|
|
147
|
-
error?: string;
|
|
148
|
-
};
|
|
149
|
-
if (balanceResult.error) {
|
|
150
|
-
console.log(balanceResult.error);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
const balances = balanceResult.output?.balances;
|
|
154
|
-
if (!balances || Object.keys(balances).length === 0) {
|
|
155
|
-
console.log("No mint URLs found in wallet balance");
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
mintUrl = Object.keys(balances)[0];
|
|
159
|
-
console.log(`Using mint URL: ${mintUrl}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const response = await fetch(`http://localhost:${config.port}/refund`, {
|
|
164
|
-
method: "POST",
|
|
165
|
-
headers: { "Content-Type": "application/json" },
|
|
166
|
-
body: JSON.stringify({ mintUrl }),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (!response.ok) {
|
|
170
|
-
const errorData = (await response.json()) as { error?: string };
|
|
171
|
-
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const result = (await response.json()) as {
|
|
175
|
-
output?: {
|
|
176
|
-
message: string;
|
|
177
|
-
pendingTokens: number;
|
|
178
|
-
apiKeys: number;
|
|
179
|
-
results: Array<{ baseUrl: string; success: boolean }>;
|
|
180
|
-
};
|
|
181
|
-
error?: string;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
if (result.error) {
|
|
185
|
-
console.log(result.error);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (result.output) {
|
|
190
|
-
console.log(result.output.message);
|
|
191
|
-
console.log(`\nPending tokens: ${result.output.pendingTokens}`);
|
|
192
|
-
console.log(`API keys: ${result.output.apiKeys}`);
|
|
193
|
-
console.log("\nResults:");
|
|
194
|
-
for (const r of result.output.results) {
|
|
195
|
-
console.log(` - ${r.baseUrl}: ${r.success ? "success" : "failed"}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
const message = (error as Error).message;
|
|
200
|
-
if (message?.includes("fetch failed") || message?.includes("Connection refused")) {
|
|
201
|
-
console.error("Daemon is not running");
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
console.error(message);
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
});
|