routstrd 0.3.8 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
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": "^4.3.0",
27
- "@routstr/sdk": "^0.3.15",
27
+ "@routstr/sdk": "^0.3.17",
28
28
  "applesauce-core": "^5.1.0",
29
29
  "applesauce-relay": "^5.1.0",
30
30
  "applesauce-wallet-connect": "^6.0.0",
@@ -32,6 +32,7 @@
32
32
  "nostr-tools": "^2.12.0",
33
33
  "qrcode": "^1.5.4",
34
34
  "rxjs": "^7.8.1",
35
+ "yaml": "^2.9.0",
35
36
  "zustand": "^5.0.5"
36
37
  }
37
38
  }
@@ -1,10 +1,65 @@
1
1
  import { existsSync, mkdirSync } from "fs";
2
2
  import { readFile, writeFile } from "fs/promises";
3
3
  import { dirname } from "path";
4
+ import { parseDocument } from "yaml";
4
5
  import type { RoutstrdConfig } from "../utils/config";
5
6
  import type { IntegrationConfig, RoutstrModel } from "./registry";
6
7
  import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
7
8
 
9
+ interface HermesRoutstrConfig {
10
+ baseUrl: string;
11
+ apiKey: string;
12
+ defaultModel: string;
13
+ }
14
+
15
+ interface HermesCustomProvider {
16
+ name?: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export function mergeHermesConfig(
21
+ content: string,
22
+ routstr: HermesRoutstrConfig,
23
+ ): string {
24
+ const document = parseDocument(content || "{}", { prettyErrors: true });
25
+ if (document.errors.length > 0) {
26
+ throw document.errors[0];
27
+ }
28
+
29
+ const urlDisplay = routstr.baseUrl
30
+ .replace(/\/v1$/, "")
31
+ .replace(/^https?:\/\//, "");
32
+ const provider = {
33
+ name: `Routstr (${urlDisplay})`,
34
+ base_url: routstr.baseUrl,
35
+ api_key: routstr.apiKey,
36
+ model: routstr.defaultModel,
37
+ };
38
+
39
+ const isNewConfig = content.trim() === "";
40
+ if (isNewConfig) {
41
+ document.set("model", {
42
+ default: routstr.defaultModel,
43
+ provider: "custom",
44
+ base_url: routstr.baseUrl,
45
+ api_key: routstr.apiKey,
46
+ });
47
+ }
48
+
49
+ const existingConfig = document.toJS() as {
50
+ custom_providers?: HermesCustomProvider[];
51
+ };
52
+ const existingProviders = existingConfig.custom_providers;
53
+ const providers = Array.isArray(existingProviders) ? existingProviders : [];
54
+ if (providers.some((item) => item.name?.startsWith("Routstr ("))) {
55
+ return content;
56
+ }
57
+ providers.push(provider);
58
+ document.set("custom_providers", providers);
59
+
60
+ return document.toString();
61
+ }
62
+
8
63
  export async function installHermesIntegration(
9
64
  config: RoutstrdConfig,
10
65
  apiKey: string,
@@ -26,10 +81,10 @@ export async function installHermesIntegration(
26
81
 
27
82
  if (models.length >= 3) {
28
83
  defaultModel = models[2]!.id;
29
- console.log(`Set default model to 3rd available model: ${defaultModel}`);
84
+ console.log(`Using 3rd available model for new Hermes configurations: ${defaultModel}`);
30
85
  } else if (models.length > 0) {
31
86
  defaultModel = models[0]!.id;
32
- console.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
87
+ console.log(`Only ${models.length} models available, using ${defaultModel} for new Hermes configurations.`);
33
88
  } else {
34
89
  console.log("No models available from routstr daemon, using fallback default.");
35
90
  }
@@ -47,36 +102,24 @@ export async function installHermesIntegration(
47
102
  console.error(`Error reading ${configPath}, creating new one.`);
48
103
  }
49
104
 
50
- // Remove existing model block
51
- content = content.replace(/^model:\n(?: .*\n)*/gm, "");
52
- // Remove existing custom_providers block
53
- content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
54
- // Clean up extra blank lines
55
- content = content.replace(/\n{3,}/g, "\n\n").trim();
56
-
57
- const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
58
-
59
- const modelBlock = `model:
60
- default: ${defaultModel}
61
- provider: custom
62
- base_url: ${baseUrlV1}
63
- api_key: ${apiKey}`;
64
-
65
- const providerBlock = `custom_providers:
66
- - name: Routstr (${urlDisplay})
67
- base_url: ${baseUrlV1}
68
- api_key: ${apiKey}
69
- model: ${defaultModel}`;
70
-
71
- const parts: string[] = [modelBlock];
72
- if (content) {
73
- parts.push(content);
105
+ let newContent: string;
106
+ try {
107
+ newContent = mergeHermesConfig(content, {
108
+ baseUrl: baseUrlV1,
109
+ apiKey,
110
+ defaultModel,
111
+ });
112
+ } catch (error) {
113
+ console.error(`Failed to parse ${configPath} as YAML; leaving it unchanged:`, error);
114
+ return;
74
115
  }
75
- parts.push(providerBlock);
76
-
77
- const newContent = parts.join("\n\n") + "\n";
78
116
 
79
117
  try {
118
+ if (newContent === content) {
119
+ console.log(`${configPath} already contains current routstr settings.`);
120
+ return;
121
+ }
122
+
80
123
  mkdirSync(dirname(configPath), { recursive: true });
81
124
  await writeFile(configPath, newContent);
82
125
  console.log(`Successfully updated ${configPath} with routstr settings.`);
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parse } from "yaml";
3
+ import { mergeHermesConfig } from "../../src/integrations/hermes";
4
+
5
+ const ROUTSTR = {
6
+ baseUrl: "http://localhost:8008/v1",
7
+ apiKey: "routstr-key",
8
+ defaultModel: "deepseek-v4-flash",
9
+ };
10
+
11
+ describe("mergeHermesConfig", () => {
12
+ it("preserves the user's model and unrelated custom providers", () => {
13
+ const existing = `model:
14
+ default: user-model
15
+ provider: custom
16
+ base_url: https://user.example/v1
17
+ api_key: user-key
18
+
19
+ custom_providers:
20
+ - name: User Provider
21
+ base_url: https://user.example/v1
22
+ api_key: user-key
23
+ model: user-model
24
+
25
+ other_setting: true
26
+ `;
27
+
28
+ const merged = parse(mergeHermesConfig(existing, ROUTSTR));
29
+
30
+ expect(merged.model).toEqual({
31
+ default: "user-model",
32
+ provider: "custom",
33
+ base_url: "https://user.example/v1",
34
+ api_key: "user-key",
35
+ });
36
+ expect(merged.other_setting).toBe(true);
37
+ expect(merged.custom_providers).toContainEqual({
38
+ name: "User Provider",
39
+ base_url: "https://user.example/v1",
40
+ api_key: "user-key",
41
+ model: "user-model",
42
+ });
43
+ expect(merged.custom_providers).toContainEqual({
44
+ name: "Routstr (localhost:8008)",
45
+ base_url: ROUTSTR.baseUrl,
46
+ api_key: ROUTSTR.apiKey,
47
+ model: ROUTSTR.defaultModel,
48
+ });
49
+ });
50
+
51
+ it("does not add a model configuration to an existing Hermes file", () => {
52
+ const merged = parse(mergeHermesConfig("other_setting: true\n", ROUTSTR));
53
+
54
+ expect(merged.model).toBeUndefined();
55
+ expect(merged.other_setting).toBe(true);
56
+ });
57
+
58
+ it("creates a Routstr default when creating a new Hermes file", () => {
59
+ const merged = parse(mergeHermesConfig("", ROUTSTR));
60
+
61
+ expect(merged.model).toEqual({
62
+ default: ROUTSTR.defaultModel,
63
+ provider: "custom",
64
+ base_url: ROUTSTR.baseUrl,
65
+ api_key: ROUTSTR.apiKey,
66
+ });
67
+ });
68
+
69
+ it("keeps an existing Routstr provider unchanged without duplicating it", () => {
70
+ const existing = `custom_providers:
71
+ - name: Routstr (old-host:8008)
72
+ base_url: http://old-host:8008/v1
73
+ api_key: old-key
74
+ model: old-model
75
+ `;
76
+
77
+ const once = mergeHermesConfig(existing, ROUTSTR);
78
+ const twice = mergeHermesConfig(once, ROUTSTR);
79
+ const merged = parse(twice);
80
+ const routstrProviders = merged.custom_providers.filter(
81
+ (provider: { name?: string }) => provider.name?.startsWith("Routstr ("),
82
+ );
83
+
84
+ expect(routstrProviders).toHaveLength(1);
85
+ expect(routstrProviders[0]).toEqual({
86
+ name: "Routstr (old-host:8008)",
87
+ base_url: "http://old-host:8008/v1",
88
+ api_key: "old-key",
89
+ model: "old-model",
90
+ });
91
+ expect(twice).toBe(once);
92
+ });
93
+
94
+ it("rejects malformed YAML instead of replacing it", () => {
95
+ expect(() => mergeHermesConfig("model: [unterminated", ROUTSTR)).toThrow();
96
+ });
97
+ });