routstrd 0.1.1 → 0.1.3
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/SKILL.md +260 -0
- package/bun.lock +94 -38
- package/dist/daemon/index.js +1316 -234
- package/dist/index.js +4967 -381
- package/package.json +5 -4
- package/refund.js +33 -0
- package/refund_new.js +20 -0
- package/src/cli-shared.ts +8 -5
- package/src/cli.ts +462 -74
- package/src/daemon/http/index.ts +768 -140
- package/src/daemon/index.ts +106 -16
- package/src/daemon/wallet/cocod-client.ts +340 -0
- package/src/daemon/wallet/index.ts +56 -141
- package/src/integrations/index.ts +5 -3
- package/src/integrations/openclaw.ts +16 -26
- package/src/integrations/opencode.ts +15 -24
- package/src/integrations/pi.ts +15 -25
- package/src/integrations/registry.ts +71 -0
- package/src/start-daemon.ts +17 -12
- package/src/tui/usage/app.ts +1 -1
- package/src/tui/usage/data.ts +24 -14
- package/src/tui/usage/render.ts +10 -7
- package/src/utils/config.ts +1 -1
- package/src/utils/logger.ts +15 -4
- package/test_chat.sh +29 -0
- package/src/daemon/sse.ts +0 -98
|
@@ -1,119 +1,54 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { getDecodedToken } from "@cashu/cashu-ts";
|
|
3
2
|
import { logger } from "../../utils/logger";
|
|
3
|
+
import { createCocodClient, type CocodClient } from "./cocod-client";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
stdout += chunk.toString();
|
|
15
|
-
});
|
|
16
|
-
child.stderr.on("data", (chunk) => {
|
|
17
|
-
stderr += chunk.toString();
|
|
18
|
-
});
|
|
19
|
-
child.on("error", (error) => reject(error));
|
|
20
|
-
child.on("close", (code) => {
|
|
21
|
-
if (code && code !== 0) {
|
|
22
|
-
reject(
|
|
23
|
-
new Error(stderr.trim() || stdout.trim() || "Wallet CLI failed"),
|
|
24
|
-
);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
resolve(stdout.trim());
|
|
28
|
-
});
|
|
29
|
-
});
|
|
5
|
+
export function decodeCashuTokenAmount(token: string): {
|
|
6
|
+
amount: number;
|
|
7
|
+
unit: "sat" | "msat";
|
|
8
|
+
} {
|
|
9
|
+
const decoded = getDecodedToken(token);
|
|
10
|
+
const amount =
|
|
11
|
+
decoded?.proofs?.reduce((sum, proof) => sum + proof.amount, 0) ?? 0;
|
|
12
|
+
const unit = decoded?.unit === "msat" ? "msat" : "sat";
|
|
13
|
+
return { amount, unit };
|
|
30
14
|
}
|
|
31
15
|
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return Object.fromEntries(
|
|
43
|
-
Object.entries(parsed).map(([mintUrl, value]) => {
|
|
44
|
-
if (typeof value === "number") {
|
|
45
|
-
return [mintUrl, value];
|
|
46
|
-
}
|
|
47
|
-
if (value && typeof value === "object" && "sats" in value) {
|
|
48
|
-
return [mintUrl, Number(value.sats ?? 0)];
|
|
49
|
-
}
|
|
50
|
-
return [mintUrl, 0];
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// Fall back to line parsing.
|
|
56
|
-
}
|
|
16
|
+
export async function createWalletAdapter(
|
|
17
|
+
options: {
|
|
18
|
+
cocodPath?: string | null;
|
|
19
|
+
walletClient?: CocodClient;
|
|
20
|
+
} = {},
|
|
21
|
+
) {
|
|
22
|
+
const client =
|
|
23
|
+
options.walletClient || createCocodClient({ cocodPath: options.cocodPath });
|
|
24
|
+
let activeMintUrl: string | null = null;
|
|
25
|
+
let mintUnits: Record<string, "sat" | "msat"> = {};
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.forEach((line) => {
|
|
63
|
-
const match = line.match(/^(\S+):\s+(\d+)\s+s$/);
|
|
64
|
-
const mintUrl = match?.[1];
|
|
65
|
-
const amount = match?.[2];
|
|
66
|
-
if (mintUrl && amount) {
|
|
67
|
-
balances[mintUrl] = Number.parseInt(amount, 10);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
return balances;
|
|
71
|
-
}
|
|
27
|
+
async function syncMintState(
|
|
28
|
+
balances?: Record<string, number>,
|
|
29
|
+
): Promise<Record<string, number>> {
|
|
30
|
+
const nextBalances = balances || (await client.getBalances());
|
|
72
31
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
): Array<{ url: string; trusted: boolean }> {
|
|
76
|
-
return output
|
|
77
|
-
.split("\n")
|
|
78
|
-
.map((line) => line.trim())
|
|
79
|
-
.map((line) => {
|
|
80
|
-
const urlMatch = line.match(/https?:\/\/\S+/i);
|
|
81
|
-
if (!urlMatch) return null;
|
|
82
|
-
const trustedMatch = line.match(/trusted:\s*(true|false)/i);
|
|
83
|
-
const trustedValue = trustedMatch?.[1];
|
|
84
|
-
return {
|
|
85
|
-
url: urlMatch[0],
|
|
86
|
-
trusted: trustedMatch ? trustedValue?.toLowerCase() === "true" : false,
|
|
87
|
-
};
|
|
88
|
-
})
|
|
89
|
-
.filter((entry): entry is { url: string; trusted: boolean } =>
|
|
90
|
-
Boolean(entry),
|
|
32
|
+
mintUnits = Object.fromEntries(
|
|
33
|
+
Object.keys(nextBalances).map((mintUrl) => [mintUrl, "sat"]),
|
|
91
34
|
);
|
|
92
|
-
}
|
|
93
35
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
36
|
+
try {
|
|
37
|
+
const mints = await client.listMints();
|
|
38
|
+
activeMintUrl = mints[0] || Object.keys(nextBalances)[0] || null;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error("Failed to list cocod mints:", error);
|
|
41
|
+
if (!activeMintUrl) {
|
|
42
|
+
activeMintUrl = Object.keys(nextBalances)[0] || null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
101
45
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let mintUnits: Record<string, "sat" | "msat"> = {};
|
|
46
|
+
return nextBalances;
|
|
47
|
+
}
|
|
105
48
|
|
|
106
49
|
const walletAdapter = {
|
|
107
50
|
async getBalances(): Promise<Record<string, number>> {
|
|
108
|
-
|
|
109
|
-
const balances = parseBalances(output);
|
|
110
|
-
mintUnits = Object.fromEntries(
|
|
111
|
-
Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]),
|
|
112
|
-
);
|
|
113
|
-
if (!activeMintUrl) {
|
|
114
|
-
activeMintUrl = Object.keys(balances)[0] || null;
|
|
115
|
-
}
|
|
116
|
-
return balances;
|
|
51
|
+
return syncMintState();
|
|
117
52
|
},
|
|
118
53
|
getMintUnits(): Record<string, "sat" | "msat"> {
|
|
119
54
|
return mintUnits;
|
|
@@ -128,33 +63,19 @@ export async function createWalletAdapter() {
|
|
|
128
63
|
|
|
129
64
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
130
65
|
try {
|
|
131
|
-
|
|
132
|
-
"send",
|
|
133
|
-
"cashu",
|
|
134
|
-
String(amount),
|
|
135
|
-
"--mint-url",
|
|
136
|
-
mintUrl,
|
|
137
|
-
]);
|
|
138
|
-
const token = pickTokenLine(output);
|
|
139
|
-
if (!token) {
|
|
140
|
-
throw new Error("Wallet CLI did not return a token.");
|
|
141
|
-
}
|
|
142
|
-
return token;
|
|
66
|
+
return await client.sendCashu(amount, mintUrl);
|
|
143
67
|
} catch (error) {
|
|
144
68
|
const errorMessage =
|
|
145
69
|
error instanceof Error ? error.message : String(error);
|
|
146
70
|
|
|
147
71
|
const shouldRetry =
|
|
148
|
-
attempt < maxRetries &&
|
|
149
|
-
errorMessage.includes(retryErrorPattern);
|
|
72
|
+
attempt < maxRetries && errorMessage.includes(retryErrorPattern);
|
|
150
73
|
|
|
151
74
|
if (shouldRetry) {
|
|
152
75
|
logger.log(
|
|
153
76
|
`sendToken attempt ${attempt + 1} failed with reserved proof error, retrying in ${retryDelayMs / 1000}s...`,
|
|
154
77
|
);
|
|
155
|
-
await new Promise((resolve) =>
|
|
156
|
-
setTimeout(resolve, retryDelayMs),
|
|
157
|
-
);
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
158
79
|
continue;
|
|
159
80
|
}
|
|
160
81
|
|
|
@@ -162,6 +83,7 @@ export async function createWalletAdapter() {
|
|
|
162
83
|
throw error;
|
|
163
84
|
}
|
|
164
85
|
}
|
|
86
|
+
|
|
165
87
|
throw new Error("sendToken failed after max retries");
|
|
166
88
|
},
|
|
167
89
|
async receiveToken(token: string): Promise<{
|
|
@@ -171,36 +93,29 @@ export async function createWalletAdapter() {
|
|
|
171
93
|
message?: string;
|
|
172
94
|
}> {
|
|
173
95
|
try {
|
|
174
|
-
await
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
(sum, proof) => sum + proof.amount,
|
|
178
|
-
0,
|
|
179
|
-
);
|
|
180
|
-
const unit = decoded?.unit === "msat" ? "msat" : "sat";
|
|
181
|
-
return { success: true, amount: amount ?? 0, unit };
|
|
96
|
+
const message = await client.receiveCashu(token);
|
|
97
|
+
const { amount, unit } = decodeCashuTokenAmount(token);
|
|
98
|
+
return { success: true, amount, unit, message };
|
|
182
99
|
} catch (error) {
|
|
183
|
-
console.log("Eerro in receive", error);
|
|
184
100
|
const errorMessage =
|
|
185
101
|
error instanceof Error ? error.message : String(error);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: undefined;
|
|
189
|
-
return { success: false, amount: 0, unit: "sat", message };
|
|
102
|
+
logger.error("Error in walletAdapter receiveToken:", error);
|
|
103
|
+
return { success: false, amount: 0, unit: "sat", message: errorMessage };
|
|
190
104
|
}
|
|
191
105
|
},
|
|
192
|
-
isUsingNip60(): boolean {
|
|
193
|
-
return false;
|
|
194
|
-
},
|
|
195
106
|
};
|
|
196
107
|
|
|
197
108
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
109
|
+
const [balances, mints] = await Promise.all([
|
|
110
|
+
client.getBalances(),
|
|
111
|
+
client.listMints().catch(() => []),
|
|
112
|
+
]);
|
|
113
|
+
mintUnits = Object.fromEntries(
|
|
114
|
+
Object.keys(balances).map((mintUrl) => [mintUrl, "sat"]),
|
|
115
|
+
);
|
|
116
|
+
activeMintUrl = mints[0] || Object.keys(balances)[0] || null;
|
|
202
117
|
} catch (error) {
|
|
203
|
-
logger.error("Failed to
|
|
118
|
+
logger.error("Failed to initialize wallet adapter state:", error);
|
|
204
119
|
}
|
|
205
120
|
|
|
206
121
|
return walletAdapter;
|
|
@@ -4,6 +4,8 @@ import { installOpencodeIntegration } from "./opencode";
|
|
|
4
4
|
import { installOpenClawIntegration } from "./openclaw";
|
|
5
5
|
import { installPiIntegration } from "./pi";
|
|
6
6
|
import type { SdkStore } from "@routstr/sdk";
|
|
7
|
+
import { CLIENT_CONFIGS } from "./registry";
|
|
8
|
+
export { CLIENT_INTEGRATIONS, CLIENT_CONFIGS, runIntegrationsForClients } from "./registry";
|
|
7
9
|
|
|
8
10
|
function ask(question: string): Promise<string> {
|
|
9
11
|
process.stdout.write(question);
|
|
@@ -49,17 +51,17 @@ export async function setupIntegration(
|
|
|
49
51
|
const choice = parseChoice(answer);
|
|
50
52
|
|
|
51
53
|
if (choice === 1) {
|
|
52
|
-
await installOpencodeIntegration(config, store);
|
|
54
|
+
await installOpencodeIntegration(config, store, CLIENT_CONFIGS.opencode!);
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (choice === 2) {
|
|
57
|
-
await installOpenClawIntegration(config, store);
|
|
59
|
+
await installOpenClawIntegration(config, store, CLIENT_CONFIGS.openclaw!);
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
if (choice === 3) {
|
|
62
|
-
await installPiIntegration(config, store);
|
|
64
|
+
await installPiIntegration(config, store, CLIENT_CONFIGS["pi-agent"]!);
|
|
63
65
|
return;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { existsSync, mkdirSync } from "fs";
|
|
3
2
|
import { readFile, writeFile } from "fs/promises";
|
|
4
|
-
import { dirname
|
|
3
|
+
import { dirname } from "path";
|
|
5
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
6
5
|
import { logger } from "../utils/logger";
|
|
7
6
|
import type { SdkStore } from "@routstr/sdk";
|
|
7
|
+
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
+
import { generateApiKey } from "./registry";
|
|
8
9
|
|
|
9
|
-
const OPENCLAW_CONFIG_PATH = join(process.env.HOME || "", ".openclaw/openclaw.json");
|
|
10
10
|
const OPENCLAW_PROVIDER_ID = "routstr";
|
|
11
|
-
const OPENCLAW_API_BASE = "http://localhost:8008/v1";
|
|
12
11
|
const OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
|
|
13
12
|
const OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
|
|
14
|
-
const OPENCLAW_CLIENT_ID = "openclaw";
|
|
15
|
-
const OPENCLAW_NAME = "OpenClaw";
|
|
16
|
-
|
|
17
|
-
type RoutstrModel = {
|
|
18
|
-
id: string;
|
|
19
|
-
name?: string;
|
|
20
|
-
};
|
|
21
13
|
|
|
22
14
|
type OpenClawModelEntry = {
|
|
23
15
|
id: string;
|
|
@@ -58,15 +50,13 @@ function toAlias(modelId: string): string {
|
|
|
58
50
|
return modelId;
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
function generateApiKey(): string {
|
|
62
|
-
const bytes = randomBytes(24);
|
|
63
|
-
return `sk-${bytes.toString("hex")}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
53
|
export async function installOpenClawIntegration(
|
|
67
54
|
config: RoutstrdConfig,
|
|
68
55
|
store: SdkStore,
|
|
56
|
+
integrationConfig: IntegrationConfig,
|
|
69
57
|
): Promise<void> {
|
|
58
|
+
const { clientId, name, configPath } = integrationConfig;
|
|
59
|
+
|
|
70
60
|
logger.log("\nInstalling routstr models in openclaw.json...");
|
|
71
61
|
|
|
72
62
|
const port = config.port || 8008;
|
|
@@ -74,33 +64,33 @@ export async function installOpenClawIntegration(
|
|
|
74
64
|
// Get or create clientId entry for OpenClaw
|
|
75
65
|
const state = store.getState();
|
|
76
66
|
const existingClient = (state.clientIds || []).find(
|
|
77
|
-
(c: { clientId: string }) => c.clientId ===
|
|
67
|
+
(c: { clientId: string }) => c.clientId === clientId,
|
|
78
68
|
);
|
|
79
69
|
|
|
80
70
|
let apiKey: string;
|
|
81
71
|
if (existingClient) {
|
|
82
72
|
apiKey = existingClient.apiKey;
|
|
83
|
-
logger.log(`Using existing API key for ${
|
|
73
|
+
logger.log(`Using existing API key for ${name}`);
|
|
84
74
|
} else {
|
|
85
75
|
apiKey = generateApiKey();
|
|
86
76
|
// Add new clientId entry using proper store action
|
|
87
77
|
store.getState().setClientIds((prev) => [
|
|
88
78
|
...(prev || []),
|
|
89
79
|
{
|
|
90
|
-
clientId
|
|
91
|
-
name
|
|
80
|
+
clientId,
|
|
81
|
+
name,
|
|
92
82
|
apiKey,
|
|
93
83
|
createdAt: Date.now(),
|
|
94
84
|
},
|
|
95
85
|
]);
|
|
96
|
-
logger.log(`Created new API key for ${
|
|
86
|
+
logger.log(`Created new API key for ${name}`);
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
let openclawConfig: OpenClawConfig = {};
|
|
100
90
|
|
|
101
91
|
try {
|
|
102
|
-
if (existsSync(
|
|
103
|
-
const content = await readFile(
|
|
92
|
+
if (existsSync(configPath)) {
|
|
93
|
+
const content = await readFile(configPath, "utf-8");
|
|
104
94
|
openclawConfig = JSON.parse(content) as OpenClawConfig;
|
|
105
95
|
}
|
|
106
96
|
} catch {
|
|
@@ -121,7 +111,7 @@ export async function installOpenClawIntegration(
|
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
try {
|
|
124
|
-
mkdirSync(dirname(
|
|
114
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
125
115
|
|
|
126
116
|
const response = await fetch(`http://localhost:${port}/models`);
|
|
127
117
|
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
@@ -139,7 +129,7 @@ export async function installOpenClawIntegration(
|
|
|
139
129
|
}));
|
|
140
130
|
|
|
141
131
|
openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
|
|
142
|
-
baseUrl:
|
|
132
|
+
baseUrl: `http://localhost:${port}/v1`,
|
|
143
133
|
apiKey,
|
|
144
134
|
api: "openai-completions",
|
|
145
135
|
models: providerModels,
|
|
@@ -169,7 +159,7 @@ export async function installOpenClawIntegration(
|
|
|
169
159
|
// }
|
|
170
160
|
// openclawConfig.agents.defaults.models = aliasMap;
|
|
171
161
|
|
|
172
|
-
await writeFile(
|
|
162
|
+
await writeFile(configPath, JSON.stringify(openclawConfig, null, 2));
|
|
173
163
|
logger.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
|
|
174
164
|
} catch (error) {
|
|
175
165
|
logger.error("Failed to install models in openclaw.json:", error);
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { existsSync, mkdirSync } from "fs";
|
|
3
2
|
import { readFile, writeFile } from "fs/promises";
|
|
4
|
-
import { dirname
|
|
3
|
+
import { dirname } from "path";
|
|
5
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
6
5
|
import { logger } from "../utils/logger";
|
|
7
6
|
import type { SdkStore } from "@routstr/sdk";
|
|
7
|
+
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
+
import { generateApiKey } from "./registry";
|
|
8
9
|
|
|
9
|
-
const OPENCODE_CONFIG_PATH = join(process.env.HOME || "", ".config/opencode/opencode.json");
|
|
10
10
|
const OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
11
|
-
const OPENCODE_CLIENT_ID = "opencode";
|
|
12
|
-
const OPENCODE_NAME = "OpenCode";
|
|
13
|
-
|
|
14
|
-
type RoutstrModel = {
|
|
15
|
-
id: string;
|
|
16
|
-
name?: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function generateApiKey(): string {
|
|
20
|
-
const bytes = randomBytes(24);
|
|
21
|
-
return `sk-${bytes.toString("hex")}`;
|
|
22
|
-
}
|
|
23
11
|
|
|
24
12
|
export async function installOpencodeIntegration(
|
|
25
13
|
config: RoutstrdConfig,
|
|
26
14
|
store: SdkStore,
|
|
15
|
+
integrationConfig: IntegrationConfig,
|
|
27
16
|
): Promise<void> {
|
|
17
|
+
const { clientId, name, configPath } = integrationConfig;
|
|
18
|
+
|
|
28
19
|
logger.log("\nInstalling routstr models in opencode.json...");
|
|
29
20
|
|
|
30
21
|
const port = config.port || 8008;
|
|
@@ -32,26 +23,26 @@ export async function installOpencodeIntegration(
|
|
|
32
23
|
// Get or create clientId entry for OpenCode
|
|
33
24
|
const state = store.getState();
|
|
34
25
|
const existingClient = (state.clientIds || []).find(
|
|
35
|
-
(c: { clientId: string }) => c.clientId ===
|
|
26
|
+
(c: { clientId: string }) => c.clientId === clientId,
|
|
36
27
|
);
|
|
37
28
|
|
|
38
29
|
let apiKey: string;
|
|
39
30
|
if (existingClient) {
|
|
40
31
|
apiKey = existingClient.apiKey;
|
|
41
|
-
logger.log(`Using existing API key for ${
|
|
32
|
+
logger.log(`Using existing API key for ${name}`);
|
|
42
33
|
} else {
|
|
43
34
|
apiKey = generateApiKey();
|
|
44
35
|
// Add new clientId entry using proper store action
|
|
45
36
|
store.getState().setClientIds((prev) => [
|
|
46
37
|
...(prev || []),
|
|
47
38
|
{
|
|
48
|
-
clientId
|
|
49
|
-
name
|
|
39
|
+
clientId,
|
|
40
|
+
name,
|
|
50
41
|
apiKey,
|
|
51
42
|
createdAt: Date.now(),
|
|
52
43
|
},
|
|
53
44
|
]);
|
|
54
|
-
logger.log(`Created new API key for ${
|
|
45
|
+
logger.log(`Created new API key for ${name}`);
|
|
55
46
|
}
|
|
56
47
|
|
|
57
48
|
let opencodeConfig: {
|
|
@@ -69,8 +60,8 @@ export async function installOpencodeIntegration(
|
|
|
69
60
|
};
|
|
70
61
|
|
|
71
62
|
try {
|
|
72
|
-
if (existsSync(
|
|
73
|
-
const content = await readFile(
|
|
63
|
+
if (existsSync(configPath)) {
|
|
64
|
+
const content = await readFile(configPath, "utf-8");
|
|
74
65
|
opencodeConfig = JSON.parse(content);
|
|
75
66
|
} else {
|
|
76
67
|
opencodeConfig = { provider: {} };
|
|
@@ -84,7 +75,7 @@ export async function installOpencodeIntegration(
|
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
try {
|
|
87
|
-
mkdirSync(dirname(
|
|
78
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
88
79
|
|
|
89
80
|
const response = await fetch(`http://localhost:${port}/models`);
|
|
90
81
|
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
@@ -112,7 +103,7 @@ export async function installOpencodeIntegration(
|
|
|
112
103
|
};
|
|
113
104
|
opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
|
|
114
105
|
|
|
115
|
-
await writeFile(
|
|
106
|
+
await writeFile(configPath, JSON.stringify(opencodeConfig, null, 2));
|
|
116
107
|
logger.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
|
|
117
108
|
} catch (error) {
|
|
118
109
|
logger.error("Failed to install models in opencode.json:", error);
|
package/src/integrations/pi.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { existsSync, mkdirSync } from "fs";
|
|
3
2
|
import { readFile, writeFile } from "fs/promises";
|
|
4
|
-
import { dirname
|
|
3
|
+
import { dirname } from "path";
|
|
5
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
6
5
|
import { logger } from "../utils/logger";
|
|
7
6
|
import type { SdkStore } from "@routstr/sdk";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const PI_CLIENT_ID = "pi-agent";
|
|
11
|
-
const PI_NAME = "Pi Agent";
|
|
12
|
-
|
|
13
|
-
type RoutstrModel = {
|
|
14
|
-
id: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
};
|
|
7
|
+
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
+
import { generateApiKey } from "./registry";
|
|
17
9
|
|
|
18
10
|
type PiModelEntry = {
|
|
19
11
|
id: string;
|
|
@@ -30,15 +22,13 @@ type PiConfig = {
|
|
|
30
22
|
providers?: Record<string, PiProviderConfig>;
|
|
31
23
|
};
|
|
32
24
|
|
|
33
|
-
function generateApiKey(): string {
|
|
34
|
-
const bytes = randomBytes(24);
|
|
35
|
-
return `sk-${bytes.toString("hex")}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
25
|
export async function installPiIntegration(
|
|
39
26
|
config: RoutstrdConfig,
|
|
40
27
|
store: SdkStore,
|
|
28
|
+
integrationConfig: IntegrationConfig,
|
|
41
29
|
): Promise<void> {
|
|
30
|
+
const { clientId, name, configPath } = integrationConfig;
|
|
31
|
+
|
|
42
32
|
logger.log("\nInstalling routstr models in pi models.json...");
|
|
43
33
|
|
|
44
34
|
const port = config.port || 8008;
|
|
@@ -47,33 +37,33 @@ export async function installPiIntegration(
|
|
|
47
37
|
// Get or create clientId entry for Pi Agent
|
|
48
38
|
const state = store.getState();
|
|
49
39
|
const existingClient = (state.clientIds || []).find(
|
|
50
|
-
(c: { clientId: string }) => c.clientId ===
|
|
40
|
+
(c: { clientId: string }) => c.clientId === clientId,
|
|
51
41
|
);
|
|
52
42
|
|
|
53
43
|
let apiKey: string;
|
|
54
44
|
if (existingClient) {
|
|
55
45
|
apiKey = existingClient.apiKey;
|
|
56
|
-
logger.log(`Using existing API key for ${
|
|
46
|
+
logger.log(`Using existing API key for ${name}`);
|
|
57
47
|
} else {
|
|
58
48
|
apiKey = generateApiKey();
|
|
59
49
|
// Add new clientId entry using proper store action
|
|
60
50
|
store.getState().setClientIds((prev) => [
|
|
61
51
|
...(prev || []),
|
|
62
52
|
{
|
|
63
|
-
clientId
|
|
64
|
-
name
|
|
53
|
+
clientId,
|
|
54
|
+
name,
|
|
65
55
|
apiKey,
|
|
66
56
|
createdAt: Date.now(),
|
|
67
57
|
},
|
|
68
58
|
]);
|
|
69
|
-
logger.log(`Created new API key for ${
|
|
59
|
+
logger.log(`Created new API key for ${name}`);
|
|
70
60
|
}
|
|
71
61
|
|
|
72
62
|
let piConfig: PiConfig = {};
|
|
73
63
|
|
|
74
64
|
try {
|
|
75
|
-
if (existsSync(
|
|
76
|
-
const content = await readFile(
|
|
65
|
+
if (existsSync(configPath)) {
|
|
66
|
+
const content = await readFile(configPath, "utf-8");
|
|
77
67
|
piConfig = JSON.parse(content) as PiConfig;
|
|
78
68
|
}
|
|
79
69
|
} catch {
|
|
@@ -86,7 +76,7 @@ export async function installPiIntegration(
|
|
|
86
76
|
|
|
87
77
|
try {
|
|
88
78
|
// Ensure directory exists
|
|
89
|
-
mkdirSync(dirname(
|
|
79
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
90
80
|
|
|
91
81
|
const response = await fetch(`http://localhost:${port}/models`);
|
|
92
82
|
const data = await response.json() as { output?: { models: RoutstrModel[] } };
|
|
@@ -108,7 +98,7 @@ export async function installPiIntegration(
|
|
|
108
98
|
models: providerModels,
|
|
109
99
|
};
|
|
110
100
|
|
|
111
|
-
await writeFile(
|
|
101
|
+
await writeFile(configPath, JSON.stringify(piConfig, null, 2));
|
|
112
102
|
logger.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
|
|
113
103
|
} catch (error) {
|
|
114
104
|
logger.error("Failed to install models in pi models.json:", error);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { RoutstrdConfig } from "../utils/config";
|
|
4
|
+
import type { SdkStore } from "@routstr/sdk";
|
|
5
|
+
import { installOpencodeIntegration } from "./opencode";
|
|
6
|
+
import { installPiIntegration } from "./pi";
|
|
7
|
+
import { installOpenClawIntegration } from "./openclaw";
|
|
8
|
+
|
|
9
|
+
export interface IntegrationConfig {
|
|
10
|
+
clientId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
configPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RoutstrModel = {
|
|
16
|
+
id: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function generateApiKey(): string {
|
|
21
|
+
const bytes = randomBytes(24);
|
|
22
|
+
return `sk-${bytes.toString("hex")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type IntegrationFn = (
|
|
26
|
+
config: RoutstrdConfig,
|
|
27
|
+
store: SdkStore,
|
|
28
|
+
integrationConfig: IntegrationConfig,
|
|
29
|
+
) => Promise<void>;
|
|
30
|
+
|
|
31
|
+
export const CLIENT_CONFIGS: Record<string, IntegrationConfig> = {
|
|
32
|
+
opencode: {
|
|
33
|
+
clientId: "opencode",
|
|
34
|
+
name: "OpenCode",
|
|
35
|
+
configPath: join(process.env.HOME || "", ".config/opencode/opencode.json"),
|
|
36
|
+
},
|
|
37
|
+
"pi-agent": {
|
|
38
|
+
clientId: "pi-agent",
|
|
39
|
+
name: "Pi Agent",
|
|
40
|
+
configPath: join(process.env.HOME || "", ".pi/agent/models.json"),
|
|
41
|
+
},
|
|
42
|
+
openclaw: {
|
|
43
|
+
clientId: "openclaw",
|
|
44
|
+
name: "OpenClaw",
|
|
45
|
+
configPath: join(process.env.HOME || "", ".openclaw/openclaw.json"),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
|
|
50
|
+
opencode: installOpencodeIntegration,
|
|
51
|
+
"pi-agent": installPiIntegration,
|
|
52
|
+
openclaw: installOpenClawIntegration,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export async function runIntegrationsForClients(
|
|
56
|
+
clientIds: Array<{ clientId: string }>,
|
|
57
|
+
config: RoutstrdConfig,
|
|
58
|
+
store: SdkStore,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
for (const client of clientIds) {
|
|
61
|
+
const integrationFn = CLIENT_INTEGRATIONS[client.clientId];
|
|
62
|
+
const integrationConfig = CLIENT_CONFIGS[client.clientId];
|
|
63
|
+
if (integrationFn && integrationConfig) {
|
|
64
|
+
try {
|
|
65
|
+
await integrationFn(config, store, integrationConfig);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`Integration failed for ${client.clientId}:`, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|