routstrd 0.2.15 → 0.2.17
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/cli.js +16372 -0
- package/dist/daemon/index.js +95 -5
- package/dist/index.js +157 -28
- package/package.json +2 -2
- package/src/cli.ts +48 -6
- package/src/daemon/models.ts +18 -2
- package/src/integrations/hermes.ts +87 -0
- package/src/integrations/registry.ts +7 -0
- package/src/tui/usage/render.ts +7 -4
- package/src/utils/clients.ts +10 -3
- package/src/utils/daemon-client.ts +1 -1
package/src/daemon/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelManager } from "@routstr/sdk";
|
|
1
|
+
import { ModelManager, type SdkStore } from "@routstr/sdk";
|
|
2
2
|
import type { ExposedModel } from "./types";
|
|
3
3
|
import { logger } from "../utils/logger";
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ export type ModelWithProviders = ExposedModel & {
|
|
|
16
16
|
providers: ModelProviderInfo[];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export function createModelService(modelManager: ModelManager) {
|
|
19
|
+
export function createModelService(modelManager: ModelManager, store: SdkStore) {
|
|
20
20
|
let providerBootstrapPromise: Promise<void> | null = null;
|
|
21
21
|
|
|
22
22
|
const ensureProvidersBootstrapped = (): Promise<void> => {
|
|
@@ -26,6 +26,22 @@ export function createModelService(modelManager: ModelManager) {
|
|
|
26
26
|
const providers = await modelManager.bootstrapProviders(false);
|
|
27
27
|
logger.log(`Bootstrapped ${providers.length} providers`);
|
|
28
28
|
await modelManager.fetchModels(providers);
|
|
29
|
+
|
|
30
|
+
// Sync discovered providers into the store so `providers list` reflects
|
|
31
|
+
// the same set that the model manager knows about.
|
|
32
|
+
const { baseUrlsList, setBaseUrlsList } = store.getState();
|
|
33
|
+
const existing = new Set(baseUrlsList);
|
|
34
|
+
const merged = [
|
|
35
|
+
...baseUrlsList,
|
|
36
|
+
...providers.filter((url) => !existing.has(url)),
|
|
37
|
+
];
|
|
38
|
+
if (merged.length !== baseUrlsList.length) {
|
|
39
|
+
setBaseUrlsList(merged);
|
|
40
|
+
logger.log(
|
|
41
|
+
`Synced ${merged.length - baseUrlsList.length} new provider(s) into store`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
logger.log("Provider bootstrap complete.");
|
|
30
46
|
})().catch((error) => {
|
|
31
47
|
logger.error("Provider bootstrap failed:", error);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
5
|
+
import { logger } from "../utils/logger";
|
|
6
|
+
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
7
|
+
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
|
8
|
+
|
|
9
|
+
export async function installHermesIntegration(
|
|
10
|
+
config: RoutstrdConfig,
|
|
11
|
+
apiKey: string,
|
|
12
|
+
integrationConfig: IntegrationConfig,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const { name, configPath } = integrationConfig;
|
|
15
|
+
|
|
16
|
+
logger.log(`\nInstalling routstr configuration in ${configPath}...`);
|
|
17
|
+
logger.log(`Using API key for ${name}`);
|
|
18
|
+
|
|
19
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
20
|
+
const baseUrlV1 = `${baseUrl}/v1`;
|
|
21
|
+
|
|
22
|
+
let defaultModel = "minimax-m2.7";
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const data = await callDaemon("/models");
|
|
26
|
+
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
|
27
|
+
|
|
28
|
+
if (models.length >= 3) {
|
|
29
|
+
defaultModel = models[2]!.id;
|
|
30
|
+
logger.log(`Set default model to 3rd available model: ${defaultModel}`);
|
|
31
|
+
} else if (models.length > 0) {
|
|
32
|
+
defaultModel = models[0]!.id;
|
|
33
|
+
logger.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
|
|
34
|
+
} else {
|
|
35
|
+
logger.log("No models available from routstr daemon, using fallback default.");
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error("Failed to fetch models for Hermes integration:", error);
|
|
39
|
+
logger.log("Using fallback default model.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let content = "";
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(configPath)) {
|
|
45
|
+
content = await readFile(configPath, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error(`Error reading ${configPath}, creating new one.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove existing model block
|
|
52
|
+
content = content.replace(/^model:\n(?: .*\n)*/gm, "");
|
|
53
|
+
// Remove existing custom_providers block
|
|
54
|
+
content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
|
|
55
|
+
// Clean up extra blank lines
|
|
56
|
+
content = content.replace(/\n{3,}/g, "\n\n").trim();
|
|
57
|
+
|
|
58
|
+
const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
|
|
59
|
+
|
|
60
|
+
const modelBlock = `model:
|
|
61
|
+
default: ${defaultModel}
|
|
62
|
+
provider: custom
|
|
63
|
+
base_url: ${baseUrlV1}
|
|
64
|
+
api_key: ${apiKey}`;
|
|
65
|
+
|
|
66
|
+
const providerBlock = `custom_providers:
|
|
67
|
+
- name: Routstr (${urlDisplay})
|
|
68
|
+
base_url: ${baseUrlV1}
|
|
69
|
+
api_key: ${apiKey}
|
|
70
|
+
model: ${defaultModel}`;
|
|
71
|
+
|
|
72
|
+
const parts: string[] = [modelBlock];
|
|
73
|
+
if (content) {
|
|
74
|
+
parts.push(content);
|
|
75
|
+
}
|
|
76
|
+
parts.push(providerBlock);
|
|
77
|
+
|
|
78
|
+
const newContent = parts.join("\n\n") + "\n";
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
82
|
+
await writeFile(configPath, newContent);
|
|
83
|
+
logger.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error(`Failed to write to ${configPath}:`, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -4,6 +4,7 @@ import { installOpencodeIntegration } from "./opencode";
|
|
|
4
4
|
import { installPiIntegration } from "./pi";
|
|
5
5
|
import { installOpenClawIntegration } from "./openclaw";
|
|
6
6
|
import { installClaudeCodeIntegration } from "./claudecode";
|
|
7
|
+
import { installHermesIntegration } from "./hermes";
|
|
7
8
|
|
|
8
9
|
export interface IntegrationConfig {
|
|
9
10
|
clientId: string;
|
|
@@ -43,6 +44,11 @@ export const CLIENT_CONFIGS: Record<string, IntegrationConfig> = {
|
|
|
43
44
|
name: "Claude Code",
|
|
44
45
|
configPath: join(process.env.HOME || "", ".claude/settings.json"),
|
|
45
46
|
},
|
|
47
|
+
hermes: {
|
|
48
|
+
clientId: "hermes",
|
|
49
|
+
name: "Hermes",
|
|
50
|
+
configPath: join(process.env.HOME || "", ".hermes/config.yaml"),
|
|
51
|
+
},
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
|
|
@@ -50,6 +56,7 @@ export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
|
|
|
50
56
|
"pi-agent": installPiIntegration,
|
|
51
57
|
openclaw: installOpenClawIntegration,
|
|
52
58
|
"claude-code": installClaudeCodeIntegration,
|
|
59
|
+
hermes: installHermesIntegration,
|
|
53
60
|
};
|
|
54
61
|
|
|
55
62
|
export async function runIntegrationsForClients(
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -511,18 +511,21 @@ export function renderRecent(stats: UsageStats, width: number): string {
|
|
|
511
511
|
const recentEntries = stats.entries.slice(0, 50);
|
|
512
512
|
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
|
513
513
|
|
|
514
|
+
const clientCol = 10;
|
|
514
515
|
const lines: string[] = [];
|
|
515
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".slice(0, Math.max(0, width -
|
|
516
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"CLIENT".padEnd(clientCol)} ${"MODEL".padEnd(18)} ${"TOKENS".padEnd(10)} ${"COST".padEnd(12)} ${"PROVIDER".slice(0, Math.max(0, width - 70))}${COLORS.reset}`);
|
|
516
517
|
lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset);
|
|
517
518
|
|
|
518
519
|
for (const entry of recentEntries) {
|
|
519
520
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
521
|
+
const clientName = (entry.client || "unknown").slice(0, clientCol - 1).padEnd(clientCol);
|
|
522
|
+
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
520
523
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
521
524
|
const tokens = `${formatNumber(entry.totalTokens).padEnd(6)} (${formatNumber(entry.promptTokens)}+${formatNumber(entry.completionTokens)})`;
|
|
522
525
|
const cost = `${formatCost(entry.satsCost).padEnd(8)} sats`;
|
|
523
|
-
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, Math.max(0, width -
|
|
524
|
-
const
|
|
525
|
-
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${
|
|
526
|
+
const provider = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "").slice(0, Math.max(0, width - 70));
|
|
527
|
+
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
528
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(10)} ${COLORS.green}${cost}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset}`);
|
|
526
529
|
}
|
|
527
530
|
|
|
528
531
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
package/src/utils/clients.ts
CHANGED
|
@@ -187,6 +187,7 @@ export interface AddClientOptions {
|
|
|
187
187
|
openclaw?: boolean;
|
|
188
188
|
piAgent?: boolean;
|
|
189
189
|
claudeCode?: boolean;
|
|
190
|
+
hermes?: boolean;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
export async function addClientAction(options: AddClientOptions): Promise<void> {
|
|
@@ -198,6 +199,7 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
|
|
|
198
199
|
if (options.openclaw) integrationKeys.push("openclaw");
|
|
199
200
|
if (options.piAgent) integrationKeys.push("pi-agent");
|
|
200
201
|
if (options.claudeCode) integrationKeys.push("claude-code");
|
|
202
|
+
if (options.hermes) integrationKeys.push("hermes");
|
|
201
203
|
|
|
202
204
|
if (integrationKeys.length > 0) {
|
|
203
205
|
for (const key of integrationKeys) {
|
|
@@ -233,9 +235,14 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
|
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
if (!options.name) {
|
|
236
|
-
console.error(
|
|
237
|
-
|
|
238
|
-
);
|
|
238
|
+
console.error("error: either provide a client name or specify an integration flag.\n");
|
|
239
|
+
console.error("Options:");
|
|
240
|
+
console.error(" -n, --name <name> Client name");
|
|
241
|
+
console.error(" --opencode Set up OpenCode integration");
|
|
242
|
+
console.error(" --openclaw Set up OpenClaw integration");
|
|
243
|
+
console.error(" --pi-agent Set up Pi Agent integration");
|
|
244
|
+
console.error(" --claude-code Set up Claude Code integration");
|
|
245
|
+
console.error(" --hermes Set up Hermes integration");
|
|
239
246
|
process.exit(1);
|
|
240
247
|
}
|
|
241
248
|
|
|
@@ -37,7 +37,7 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string {
|
|
|
37
37
|
|
|
38
38
|
export async function callDaemon(
|
|
39
39
|
path: string,
|
|
40
|
-
options: { method?: "GET" | "POST" | "DELETE"; body?: object } = {},
|
|
40
|
+
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
|
|
41
41
|
): Promise<CommandResponse> {
|
|
42
42
|
const { method = "GET", body } = options;
|
|
43
43
|
const config = await loadConfig();
|