routstrd 0.2.22 → 0.3.1
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/.claude/settings.local.json +10 -0
- package/Dockerfile +8 -0
- package/bun.lock +165 -9
- package/dist/daemon/index.js +42891 -20990
- package/dist/index.js +417 -169
- package/docker-compose.yml +8 -7
- package/new-task.md +60 -0
- package/package.json +3 -2
- package/src/cli.ts +178 -22
- package/src/daemon/config-store.ts +13 -1
- package/src/daemon/wallet/auto-refill.ts +192 -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/utils/clients.ts +4 -3
- package/src/utils/config.ts +27 -0
- package/src/utils/daemon-client.ts +32 -5
|
@@ -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/utils/clients.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
callAuth,
|
|
2
3
|
callDaemon,
|
|
3
4
|
loadConfig,
|
|
4
5
|
getDaemonBaseUrl,
|
|
@@ -56,7 +57,7 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
|
|
|
56
57
|
* Use this when running remotely (CLI in remote mode).
|
|
57
58
|
*/
|
|
58
59
|
export async function getClientsList(): Promise<ClientEntry[]> {
|
|
59
|
-
const result = await
|
|
60
|
+
const result = await callAuth("/clients");
|
|
60
61
|
const clients = (
|
|
61
62
|
result.output as
|
|
62
63
|
| {
|
|
@@ -107,7 +108,7 @@ export async function addDaemonClient(
|
|
|
107
108
|
return { client, created: false };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const result = await
|
|
111
|
+
const result = await callAuth("/clients/add", {
|
|
111
112
|
method: "POST",
|
|
112
113
|
body: { name, id: derivedId },
|
|
113
114
|
});
|
|
@@ -159,7 +160,7 @@ export async function listClientsAction(): Promise<void> {
|
|
|
159
160
|
export async function deleteClientAction(id: string): Promise<void> {
|
|
160
161
|
await ensureDaemonRunning();
|
|
161
162
|
|
|
162
|
-
const result = await
|
|
163
|
+
const result = await callAuth("/clients/delete", {
|
|
163
164
|
method: "POST",
|
|
164
165
|
body: { id },
|
|
165
166
|
});
|
package/src/utils/config.ts
CHANGED
|
@@ -7,15 +7,42 @@ export const DB_PATH = `${CONFIG_DIR}/routstr.db`;
|
|
|
7
7
|
export const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
|
8
8
|
export const LOGS_DIR = `${CONFIG_DIR}/logs`;
|
|
9
9
|
|
|
10
|
+
/** NWC auto-refill configuration */
|
|
11
|
+
export interface NwcAutoRefillConfig {
|
|
12
|
+
/** Whether auto-refill is enabled */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Refill when Cashu balance drops below this many sats */
|
|
15
|
+
threshold: number;
|
|
16
|
+
/** Refill this many sats at a time */
|
|
17
|
+
amount: number;
|
|
18
|
+
/** Minimum time between refills in milliseconds */
|
|
19
|
+
cooldownMs: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** NWC configuration section */
|
|
23
|
+
export interface NwcConfig {
|
|
24
|
+
/** NWC mode: "funding_source" = NWC funds the cocod Cashu wallet */
|
|
25
|
+
mode: "funding_source" | "standalone";
|
|
26
|
+
/** NWC connection string (nostr+walletconnect://...) */
|
|
27
|
+
connectionString?: string;
|
|
28
|
+
/** Auto-refill settings */
|
|
29
|
+
autoRefill?: NwcAutoRefillConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
export interface RoutstrdConfig {
|
|
11
33
|
port: number;
|
|
12
34
|
provider: string | null;
|
|
13
35
|
cocodPath: string | null;
|
|
14
36
|
mode?: "xcashu" | "apikeys";
|
|
15
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;
|
|
16
41
|
nsec?: string;
|
|
17
42
|
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
|
|
18
43
|
routstrPubkey?: string;
|
|
44
|
+
/** NWC integration configuration */
|
|
45
|
+
nwc?: NwcConfig;
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
export const DEFAULT_CONFIG: RoutstrdConfig = {
|
|
@@ -35,13 +35,20 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string {
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export
|
|
38
|
+
export function getAuthBaseUrl(config: RoutstrdConfig): string {
|
|
39
|
+
if (config.authUrl) {
|
|
40
|
+
return config.authUrl.replace(/\/$/, "");
|
|
41
|
+
}
|
|
42
|
+
return getDaemonBaseUrl(config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function _callUrl(
|
|
46
|
+
baseUrl: string,
|
|
39
47
|
path: string,
|
|
40
|
-
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object }
|
|
48
|
+
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object },
|
|
49
|
+
config: RoutstrdConfig,
|
|
41
50
|
): Promise<CommandResponse> {
|
|
42
51
|
const { method = "GET", body } = options;
|
|
43
|
-
const config = await loadConfig();
|
|
44
|
-
const baseUrl = getDaemonBaseUrl(config);
|
|
45
52
|
const url = `${baseUrl}${path}`;
|
|
46
53
|
|
|
47
54
|
const bodyString = body ? JSON.stringify(body) : undefined;
|
|
@@ -50,7 +57,7 @@ export async function callDaemon(
|
|
|
50
57
|
: undefined;
|
|
51
58
|
|
|
52
59
|
let authorization: string | undefined;
|
|
53
|
-
if (config.daemonUrl && config.nsec) {
|
|
60
|
+
if ((config.daemonUrl || config.authUrl) && config.nsec) {
|
|
54
61
|
const secretKey = parseSecretKey(config.nsec);
|
|
55
62
|
authorization = await createNIP98Authorization(
|
|
56
63
|
secretKey,
|
|
@@ -77,6 +84,26 @@ export async function callDaemon(
|
|
|
77
84
|
return response.json() as Promise<CommandResponse>;
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
export async function callDaemon(
|
|
88
|
+
path: string,
|
|
89
|
+
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
|
|
90
|
+
): Promise<CommandResponse> {
|
|
91
|
+
const config = await loadConfig();
|
|
92
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
93
|
+
return _callUrl(baseUrl, path, options, config);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Like callDaemon but sends requests to the auth proxy URL instead.
|
|
97
|
+
* Falls back to the daemon URL if no authUrl is configured. */
|
|
98
|
+
export async function callAuth(
|
|
99
|
+
path: string,
|
|
100
|
+
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
|
|
101
|
+
): Promise<CommandResponse> {
|
|
102
|
+
const config = await loadConfig();
|
|
103
|
+
const baseUrl = getAuthBaseUrl(config);
|
|
104
|
+
return _callUrl(baseUrl, path, options, config);
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
export async function isDaemonRunning(): Promise<boolean> {
|
|
81
108
|
try {
|
|
82
109
|
const config = await loadConfig();
|