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.
@@ -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 async function runWalletCommand(args: string[]): Promise<string> {
6
- return new Promise((resolve, reject) => {
7
- const child = spawn("cocod", args, {
8
- stdio: ["ignore", "pipe", "pipe"],
9
- });
10
- let stdout = "";
11
- let stderr = "";
12
-
13
- child.stdout.on("data", (chunk) => {
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 parseBalances(output: string): Record<string, number> {
33
- const trimmed = output.trim();
34
- if (!trimmed) return {};
35
-
36
- try {
37
- const parsed = JSON.parse(trimmed) as Record<
38
- string,
39
- { sats?: number } | number
40
- >;
41
- if (parsed && typeof parsed === "object") {
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
- const balances: Record<string, number> = {};
59
- trimmed
60
- .split("\n")
61
- .map((line) => line.trim())
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
- export function parseMints(
74
- output: string,
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
- export function pickTokenLine(output: string): string {
95
- const lines = output
96
- .split("\n")
97
- .map((line) => line.trim())
98
- .filter(Boolean);
99
- return lines[lines.length - 1] || "";
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
- export async function createWalletAdapter() {
103
- let activeMintUrl: string | null = null;
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
- const output = await runWalletCommand(["balance"]);
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
- const output = await runWalletCommand([
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 runWalletCommand(["receive", "cashu", token]);
175
- const decoded = getDecodedToken(token);
176
- const amount = decoded?.proofs?.reduce(
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
- const message = errorMessage.includes("Failed to fetch mint")
187
- ? errorMessage
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 mintsOutput = await runWalletCommand(["mints", "list"]);
199
- const mints = parseMints(mintsOutput);
200
- activeMintUrl =
201
- mints.find((mint) => mint.trusted)?.url || mints[0]?.url || null;
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 read mints from wallet:", error);
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, join } from "path";
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 === OPENCLAW_CLIENT_ID,
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 ${OPENCLAW_NAME}`);
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: OPENCLAW_CLIENT_ID,
91
- name: OPENCLAW_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 ${OPENCLAW_NAME}`);
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(OPENCLAW_CONFIG_PATH)) {
103
- const content = await readFile(OPENCLAW_CONFIG_PATH, "utf-8");
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(OPENCLAW_CONFIG_PATH), { recursive: true });
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: OPENCLAW_API_BASE,
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(OPENCLAW_CONFIG_PATH, JSON.stringify(openclawConfig, null, 2));
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, join } from "path";
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 === OPENCODE_CLIENT_ID,
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 ${OPENCODE_NAME}`);
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: OPENCODE_CLIENT_ID,
49
- name: OPENCODE_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 ${OPENCODE_NAME}`);
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(OPENCODE_CONFIG_PATH)) {
73
- const content = await readFile(OPENCODE_CONFIG_PATH, "utf-8");
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(OPENCODE_CONFIG_PATH), { recursive: true });
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(OPENCODE_CONFIG_PATH, JSON.stringify(opencodeConfig, null, 2));
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);
@@ -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, join } from "path";
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
- const PI_CONFIG_PATH = join(process.env.HOME || "", ".pi/agent/models.json");
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 === PI_CLIENT_ID,
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 ${PI_NAME}`);
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: PI_CLIENT_ID,
64
- name: PI_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 ${PI_NAME}`);
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(PI_CONFIG_PATH)) {
76
- const content = await readFile(PI_CONFIG_PATH, "utf-8");
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(PI_CONFIG_PATH), { recursive: true });
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(PI_CONFIG_PATH, JSON.stringify(piConfig, null, 2));
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
+ }