routstrd 0.2.6 → 0.2.9
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/README.md +17 -20
- package/SKILL.md +46 -13
- package/bun.lock +16 -217
- package/dist/daemon/index.js +588 -404
- package/dist/index.js +10328 -31646
- package/package.json +2 -1
- package/src/cli.ts +291 -208
- package/src/daemon/wallet/cocod-client.ts +22 -6
- package/src/integrations/claudecode.ts +19 -40
- package/src/integrations/openclaw.ts +8 -34
- package/src/integrations/opencode.ts +8 -34
- package/src/integrations/pi.ts +7 -34
- package/src/integrations/registry.ts +4 -12
- package/src/start-daemon.ts +52 -30
- package/src/tui/usage/data.ts +19 -7
- package/src/utils/clients.ts +304 -0
- package/src/utils/config.ts +2 -0
- package/src/utils/daemon-client.ts +79 -28
- package/src/utils/nip98.ts +102 -0
- package/src/utils/process-lock.ts +136 -0
- package/src/daemon/http/index.ts +0 -1130
- package/src/daemon/index.ts +0 -242
- package/src/daemon/wallet/index.ts +0 -122
- package/src/index.ts +0 -4
- package/src/integrations/index.ts +0 -76
- package/src/tui/usage/index.ts +0 -1
package/dist/daemon/index.js
CHANGED
|
@@ -62,9 +62,9 @@ var init__assert = () => {
|
|
|
62
62
|
|
|
63
63
|
// node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes/esm/cryptoNode.js
|
|
64
64
|
import * as nc from "crypto";
|
|
65
|
-
var
|
|
65
|
+
var crypto2;
|
|
66
66
|
var init_cryptoNode = __esm(() => {
|
|
67
|
-
|
|
67
|
+
crypto2 = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : undefined;
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
// node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes/esm/utils.js
|
|
@@ -106,8 +106,8 @@ function wrapConstructor(hashCons) {
|
|
|
106
106
|
return hashC;
|
|
107
107
|
}
|
|
108
108
|
function randomBytes(bytesLength = 32) {
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
109
|
+
if (crypto2 && typeof crypto2.getRandomValues === "function") {
|
|
110
|
+
return crypto2.getRandomValues(new Uint8Array(bytesLength));
|
|
111
111
|
}
|
|
112
112
|
throw new Error("crypto.getRandomValues must be defined");
|
|
113
113
|
}
|
|
@@ -1951,9 +1951,9 @@ var init_secp256k1 = __esm(() => {
|
|
|
1951
1951
|
|
|
1952
1952
|
// node_modules/nostr-tools/node_modules/@noble/hashes/esm/cryptoNode.js
|
|
1953
1953
|
import * as nc2 from "crypto";
|
|
1954
|
-
var
|
|
1954
|
+
var crypto3;
|
|
1955
1955
|
var init_cryptoNode2 = __esm(() => {
|
|
1956
|
-
|
|
1956
|
+
crypto3 = nc2 && typeof nc2 === "object" && "webcrypto" in nc2 ? nc2.webcrypto : undefined;
|
|
1957
1957
|
});
|
|
1958
1958
|
|
|
1959
1959
|
// node_modules/nostr-tools/node_modules/@noble/hashes/esm/utils.js
|
|
@@ -2021,8 +2021,8 @@ function wrapConstructor2(hashCons) {
|
|
|
2021
2021
|
return hashC;
|
|
2022
2022
|
}
|
|
2023
2023
|
function randomBytes2(bytesLength = 32) {
|
|
2024
|
-
if (
|
|
2025
|
-
return
|
|
2024
|
+
if (crypto3 && typeof crypto3.getRandomValues === "function") {
|
|
2025
|
+
return crypto3.getRandomValues(new Uint8Array(bytesLength));
|
|
2026
2026
|
}
|
|
2027
2027
|
throw new Error("crypto.getRandomValues must be defined");
|
|
2028
2028
|
}
|
|
@@ -19631,14 +19631,14 @@ var init_observable = __esm(() => {
|
|
|
19631
19631
|
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
19632
19632
|
|
|
19633
19633
|
// node_modules/nanoid/index.js
|
|
19634
|
-
import { webcrypto as
|
|
19634
|
+
import { webcrypto as crypto4 } from "crypto";
|
|
19635
19635
|
function fillPool(bytes4) {
|
|
19636
19636
|
if (!pool || pool.length < bytes4) {
|
|
19637
19637
|
pool = Buffer.allocUnsafe(bytes4 * POOL_SIZE_MULTIPLIER);
|
|
19638
|
-
|
|
19638
|
+
crypto4.getRandomValues(pool);
|
|
19639
19639
|
poolOffset = 0;
|
|
19640
19640
|
} else if (poolOffset + bytes4 > pool.length) {
|
|
19641
|
-
|
|
19641
|
+
crypto4.getRandomValues(pool);
|
|
19642
19642
|
poolOffset = 0;
|
|
19643
19643
|
}
|
|
19644
19644
|
poolOffset += bytes4;
|
|
@@ -34560,7 +34560,7 @@ var init_dist3 = __esm(() => {
|
|
|
34560
34560
|
// src/daemon/index.ts
|
|
34561
34561
|
init_dist3();
|
|
34562
34562
|
import { createServer } from "http";
|
|
34563
|
-
import { existsSync as
|
|
34563
|
+
import { existsSync as existsSync8 } from "fs";
|
|
34564
34564
|
|
|
34565
34565
|
// src/utils/config.ts
|
|
34566
34566
|
var HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
@@ -34900,6 +34900,102 @@ init_cashu_ts_es();
|
|
|
34900
34900
|
|
|
34901
34901
|
// src/daemon/wallet/cocod-client.ts
|
|
34902
34902
|
import { createHash } from "crypto";
|
|
34903
|
+
|
|
34904
|
+
// src/utils/process-lock.ts
|
|
34905
|
+
import { randomUUID } from "crypto";
|
|
34906
|
+
import { mkdir as mkdir3, readFile, rm, stat, writeFile } from "fs/promises";
|
|
34907
|
+
import { dirname } from "path";
|
|
34908
|
+
function delay(ms) {
|
|
34909
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34910
|
+
}
|
|
34911
|
+
function isProcessRunning(pid) {
|
|
34912
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
34913
|
+
return false;
|
|
34914
|
+
}
|
|
34915
|
+
try {
|
|
34916
|
+
process.kill(pid, 0);
|
|
34917
|
+
return true;
|
|
34918
|
+
} catch (error) {
|
|
34919
|
+
const code = error.code;
|
|
34920
|
+
return code === "EPERM";
|
|
34921
|
+
}
|
|
34922
|
+
}
|
|
34923
|
+
async function readLockOwner(lockDir) {
|
|
34924
|
+
try {
|
|
34925
|
+
const raw = await readFile(`${lockDir}/owner.json`, "utf8");
|
|
34926
|
+
const parsed = JSON.parse(raw);
|
|
34927
|
+
if (typeof parsed.pid === "number" && typeof parsed.createdAt === "number") {
|
|
34928
|
+
return {
|
|
34929
|
+
pid: parsed.pid,
|
|
34930
|
+
createdAt: parsed.createdAt,
|
|
34931
|
+
token: typeof parsed.token === "string" ? parsed.token : undefined
|
|
34932
|
+
};
|
|
34933
|
+
}
|
|
34934
|
+
} catch {
|
|
34935
|
+
}
|
|
34936
|
+
return null;
|
|
34937
|
+
}
|
|
34938
|
+
async function isLockStale(lockDir, staleAfterMs) {
|
|
34939
|
+
const owner = await readLockOwner(lockDir);
|
|
34940
|
+
if (owner) {
|
|
34941
|
+
return !isProcessRunning(owner.pid) || Date.now() - owner.createdAt > staleAfterMs;
|
|
34942
|
+
}
|
|
34943
|
+
try {
|
|
34944
|
+
const info = await stat(lockDir);
|
|
34945
|
+
return Date.now() - info.mtimeMs > staleAfterMs;
|
|
34946
|
+
} catch {
|
|
34947
|
+
return false;
|
|
34948
|
+
}
|
|
34949
|
+
}
|
|
34950
|
+
async function acquireCrossProcessLock(lockDir, options = {}) {
|
|
34951
|
+
const acquireTimeoutMs = options.acquireTimeoutMs ?? 120000;
|
|
34952
|
+
const retryIntervalMs = options.retryIntervalMs ?? 100;
|
|
34953
|
+
const staleAfterMs = options.staleAfterMs ?? 120000;
|
|
34954
|
+
const deadline = Date.now() + acquireTimeoutMs;
|
|
34955
|
+
await mkdir3(dirname(lockDir), { recursive: true });
|
|
34956
|
+
while (true) {
|
|
34957
|
+
try {
|
|
34958
|
+
await mkdir3(lockDir);
|
|
34959
|
+
const token = randomUUID();
|
|
34960
|
+
const owner = { pid: process.pid, createdAt: Date.now(), token };
|
|
34961
|
+
await writeFile(`${lockDir}/owner.json`, JSON.stringify(owner), "utf8");
|
|
34962
|
+
let released = false;
|
|
34963
|
+
return async () => {
|
|
34964
|
+
if (released)
|
|
34965
|
+
return;
|
|
34966
|
+
released = true;
|
|
34967
|
+
const currentOwner = await readLockOwner(lockDir);
|
|
34968
|
+
if (currentOwner?.token === token) {
|
|
34969
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
34970
|
+
}
|
|
34971
|
+
};
|
|
34972
|
+
} catch (error) {
|
|
34973
|
+
const code = error.code;
|
|
34974
|
+
if (code !== "EEXIST") {
|
|
34975
|
+
throw error;
|
|
34976
|
+
}
|
|
34977
|
+
if (await isLockStale(lockDir, staleAfterMs)) {
|
|
34978
|
+
options.log?.(`Removing stale lock at ${lockDir}`);
|
|
34979
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
34980
|
+
continue;
|
|
34981
|
+
}
|
|
34982
|
+
if (Date.now() >= deadline) {
|
|
34983
|
+
throw new Error(`Timed out waiting to acquire lock ${lockDir}`);
|
|
34984
|
+
}
|
|
34985
|
+
await delay(retryIntervalMs);
|
|
34986
|
+
}
|
|
34987
|
+
}
|
|
34988
|
+
}
|
|
34989
|
+
async function withCrossProcessLock(lockDir, fn, options = {}) {
|
|
34990
|
+
const release = await acquireCrossProcessLock(lockDir, options);
|
|
34991
|
+
try {
|
|
34992
|
+
return await fn();
|
|
34993
|
+
} finally {
|
|
34994
|
+
await release();
|
|
34995
|
+
}
|
|
34996
|
+
}
|
|
34997
|
+
|
|
34998
|
+
// src/daemon/wallet/cocod-client.ts
|
|
34903
34999
|
var DEFAULT_CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
34904
35000
|
var DEFAULT_SOCKET_PATH = process.env.COCOD_SOCKET || `${DEFAULT_CONFIG_DIR}/cocod.sock`;
|
|
34905
35001
|
|
|
@@ -34929,7 +35025,7 @@ function parseMintList(output4) {
|
|
|
34929
35025
|
return (output4 || "").split(`
|
|
34930
35026
|
`).map((line) => line.trim()).filter(Boolean);
|
|
34931
35027
|
}
|
|
34932
|
-
function
|
|
35028
|
+
function delay2(ms) {
|
|
34933
35029
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34934
35030
|
}
|
|
34935
35031
|
function toErrorText(value) {
|
|
@@ -34951,6 +35047,7 @@ function tokenFingerprint(token) {
|
|
|
34951
35047
|
function createCocodClient(options = {}) {
|
|
34952
35048
|
const executable = resolveCocodExecutable(options.cocodPath);
|
|
34953
35049
|
const socketPath = options.socketPath || DEFAULT_SOCKET_PATH;
|
|
35050
|
+
const startupLockPath = `${socketPath}.startup.lock`;
|
|
34954
35051
|
const fetchImpl = options.fetchImpl || fetch;
|
|
34955
35052
|
const pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
34956
35053
|
const startupTimeoutMs = options.startupTimeoutMs ?? 5000;
|
|
@@ -35005,31 +35102,40 @@ function createCocodClient(options = {}) {
|
|
|
35005
35102
|
}
|
|
35006
35103
|
async function startDaemon() {
|
|
35007
35104
|
const env = { ...process.env, COCOD_SOCKET: socketPath };
|
|
35008
|
-
const proc = spawnDaemon([executable, "
|
|
35105
|
+
const proc = spawnDaemon([executable, "init"], env);
|
|
35009
35106
|
const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
|
|
35010
35107
|
let exitCode = null;
|
|
35011
35108
|
proc.exited.then((code) => {
|
|
35012
35109
|
exitCode = code;
|
|
35013
35110
|
});
|
|
35014
35111
|
for (let i4 = 0;i4 < maxPolls; i4++) {
|
|
35015
|
-
await
|
|
35016
|
-
if (exitCode !== null) {
|
|
35017
|
-
throw new Error(`cocod
|
|
35112
|
+
await delay2(pollIntervalMs);
|
|
35113
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
35114
|
+
throw new Error(`cocod init exited early with code ${exitCode}`);
|
|
35018
35115
|
}
|
|
35019
35116
|
if (await pingInternal()) {
|
|
35020
35117
|
logger3.debug(`Connected to cocod daemon on ${socketPath}`);
|
|
35021
35118
|
return;
|
|
35022
35119
|
}
|
|
35023
35120
|
}
|
|
35024
|
-
throw new Error(`cocod
|
|
35121
|
+
throw new Error(`cocod failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds`);
|
|
35025
35122
|
}
|
|
35026
35123
|
async function ensureDaemonRunning() {
|
|
35027
35124
|
if (await pingInternal()) {
|
|
35028
35125
|
return;
|
|
35029
35126
|
}
|
|
35030
35127
|
if (!startPromise) {
|
|
35031
|
-
|
|
35032
|
-
|
|
35128
|
+
startPromise = withCrossProcessLock(startupLockPath, async () => {
|
|
35129
|
+
if (await pingInternal()) {
|
|
35130
|
+
return;
|
|
35131
|
+
}
|
|
35132
|
+
logger3.debug(`Starting cocod daemon via ${executable} init...`);
|
|
35133
|
+
await startDaemon();
|
|
35134
|
+
}, {
|
|
35135
|
+
acquireTimeoutMs: startupTimeoutMs + 30000,
|
|
35136
|
+
staleAfterMs: startupTimeoutMs + 30000,
|
|
35137
|
+
log: (message) => logger3.debug(message)
|
|
35138
|
+
}).finally(() => {
|
|
35033
35139
|
startPromise = null;
|
|
35034
35140
|
});
|
|
35035
35141
|
}
|
|
@@ -35253,6 +35359,439 @@ function createModelService(modelManager) {
|
|
|
35253
35359
|
init_dist3();
|
|
35254
35360
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
35255
35361
|
import { Readable } from "stream";
|
|
35362
|
+
|
|
35363
|
+
// src/utils/daemon-client.ts
|
|
35364
|
+
import { existsSync as existsSync3 } from "fs";
|
|
35365
|
+
|
|
35366
|
+
// src/start-daemon.ts
|
|
35367
|
+
var DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
35368
|
+
|
|
35369
|
+
// src/utils/nip98.ts
|
|
35370
|
+
init_esm2();
|
|
35371
|
+
var NIP98_KIND = 27235;
|
|
35372
|
+
function hexToBytes6(hex3) {
|
|
35373
|
+
const normalized = hex3.trim().toLowerCase();
|
|
35374
|
+
if (!/^[a-f0-9]{64}$/.test(normalized)) {
|
|
35375
|
+
throw new Error("Expected a 64-char hex private key or an nsec private key.");
|
|
35376
|
+
}
|
|
35377
|
+
const bytes4 = new Uint8Array(32);
|
|
35378
|
+
for (let i4 = 0;i4 < bytes4.length; i4++) {
|
|
35379
|
+
bytes4[i4] = Number.parseInt(normalized.slice(i4 * 2, i4 * 2 + 2), 16);
|
|
35380
|
+
}
|
|
35381
|
+
return bytes4;
|
|
35382
|
+
}
|
|
35383
|
+
function parseSecretKey(value) {
|
|
35384
|
+
const trimmed = value.trim();
|
|
35385
|
+
if (!trimmed) {
|
|
35386
|
+
throw new Error("Missing Nostr private key.");
|
|
35387
|
+
}
|
|
35388
|
+
if (trimmed.toLowerCase().startsWith("nsec1")) {
|
|
35389
|
+
const decoded = nip19_exports.decode(trimmed);
|
|
35390
|
+
if (decoded.type !== "nsec" || !(decoded.data instanceof Uint8Array)) {
|
|
35391
|
+
throw new Error("Invalid nsec private key.");
|
|
35392
|
+
}
|
|
35393
|
+
return decoded.data;
|
|
35394
|
+
}
|
|
35395
|
+
return hexToBytes6(trimmed);
|
|
35396
|
+
}
|
|
35397
|
+
async function sha256Hex(data) {
|
|
35398
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(data));
|
|
35399
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
35400
|
+
}
|
|
35401
|
+
function base64EncodeUtf8(value) {
|
|
35402
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(value)));
|
|
35403
|
+
}
|
|
35404
|
+
function npubFromPubkey(pubkey) {
|
|
35405
|
+
return nip19_exports.npubEncode(pubkey.toLowerCase());
|
|
35406
|
+
}
|
|
35407
|
+
function npubFromSecretKey(secretKey) {
|
|
35408
|
+
return npubFromPubkey(getPublicKey2(secretKey));
|
|
35409
|
+
}
|
|
35410
|
+
async function createNIP98Authorization(secretKey, url2, method, body) {
|
|
35411
|
+
const tags3 = [
|
|
35412
|
+
["u", url2],
|
|
35413
|
+
["method", method.toUpperCase()]
|
|
35414
|
+
];
|
|
35415
|
+
if (body && body.byteLength > 0) {
|
|
35416
|
+
tags3.push(["payload", await sha256Hex(body)]);
|
|
35417
|
+
}
|
|
35418
|
+
const template = {
|
|
35419
|
+
kind: NIP98_KIND,
|
|
35420
|
+
created_at: Math.round(Date.now() / 1000),
|
|
35421
|
+
content: "",
|
|
35422
|
+
tags: tags3
|
|
35423
|
+
};
|
|
35424
|
+
const signed = finalizeEvent2(template, secretKey);
|
|
35425
|
+
return `Nostr ${base64EncodeUtf8(JSON.stringify(signed))}`;
|
|
35426
|
+
}
|
|
35427
|
+
|
|
35428
|
+
// src/utils/daemon-client.ts
|
|
35429
|
+
async function loadConfig() {
|
|
35430
|
+
try {
|
|
35431
|
+
if (existsSync3(CONFIG_FILE)) {
|
|
35432
|
+
const content2 = await Bun.file(CONFIG_FILE).text();
|
|
35433
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content2) };
|
|
35434
|
+
}
|
|
35435
|
+
} catch (error) {
|
|
35436
|
+
console.error("Failed to load config:", error);
|
|
35437
|
+
}
|
|
35438
|
+
return DEFAULT_CONFIG;
|
|
35439
|
+
}
|
|
35440
|
+
function getDaemonBaseUrl(config) {
|
|
35441
|
+
return config.daemonUrl?.replace(/\/$/, "") || `http://localhost:${config.port}`;
|
|
35442
|
+
}
|
|
35443
|
+
async function callDaemon(path, options = {}) {
|
|
35444
|
+
const { method = "GET", body } = options;
|
|
35445
|
+
const config = await loadConfig();
|
|
35446
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
35447
|
+
const url2 = `${baseUrl}${path}`;
|
|
35448
|
+
const bodyString = body ? JSON.stringify(body) : undefined;
|
|
35449
|
+
const bodyBytes = bodyString ? new TextEncoder().encode(bodyString) : undefined;
|
|
35450
|
+
let authorization;
|
|
35451
|
+
if (config.daemonUrl && config.nsec) {
|
|
35452
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
35453
|
+
authorization = await createNIP98Authorization(secretKey, url2, method, bodyBytes);
|
|
35454
|
+
}
|
|
35455
|
+
const response = await fetch(url2, {
|
|
35456
|
+
method,
|
|
35457
|
+
headers: {
|
|
35458
|
+
...authorization ? { Authorization: authorization } : {},
|
|
35459
|
+
...bodyString ? { "Content-Type": "application/json" } : {}
|
|
35460
|
+
},
|
|
35461
|
+
body: bodyString
|
|
35462
|
+
});
|
|
35463
|
+
if (!response.ok) {
|
|
35464
|
+
const errorData = await response.json();
|
|
35465
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
35466
|
+
}
|
|
35467
|
+
return response.json();
|
|
35468
|
+
}
|
|
35469
|
+
|
|
35470
|
+
// src/integrations/registry.ts
|
|
35471
|
+
import { join as join4 } from "path";
|
|
35472
|
+
|
|
35473
|
+
// src/integrations/opencode.ts
|
|
35474
|
+
import { existsSync as existsSync4, mkdirSync } from "fs";
|
|
35475
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
35476
|
+
import { dirname as dirname2 } from "path";
|
|
35477
|
+
var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
35478
|
+
async function installOpencodeIntegration(config, apiKey, integrationConfig) {
|
|
35479
|
+
const { name, configPath } = integrationConfig;
|
|
35480
|
+
logger3.log(`
|
|
35481
|
+
Installing routstr models in opencode.json...`);
|
|
35482
|
+
logger3.log(`Using API key for ${name}`);
|
|
35483
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
35484
|
+
let opencodeConfig;
|
|
35485
|
+
try {
|
|
35486
|
+
if (existsSync4(configPath)) {
|
|
35487
|
+
const content2 = await readFile2(configPath, "utf-8");
|
|
35488
|
+
opencodeConfig = JSON.parse(content2);
|
|
35489
|
+
} else {
|
|
35490
|
+
opencodeConfig = { provider: {} };
|
|
35491
|
+
}
|
|
35492
|
+
} catch {
|
|
35493
|
+
opencodeConfig = { provider: {} };
|
|
35494
|
+
}
|
|
35495
|
+
if (!opencodeConfig.provider) {
|
|
35496
|
+
opencodeConfig.provider = {};
|
|
35497
|
+
}
|
|
35498
|
+
try {
|
|
35499
|
+
mkdirSync(dirname2(configPath), { recursive: true });
|
|
35500
|
+
const data = await callDaemon("/models");
|
|
35501
|
+
const models = data.output?.models || [];
|
|
35502
|
+
if (models.length === 0) {
|
|
35503
|
+
logger3.log("No models found from routstr daemon.");
|
|
35504
|
+
return;
|
|
35505
|
+
}
|
|
35506
|
+
const modelsObj = {};
|
|
35507
|
+
for (const model2 of models) {
|
|
35508
|
+
modelsObj[model2.id] = { name: model2.name || model2.id };
|
|
35509
|
+
}
|
|
35510
|
+
opencodeConfig.provider["routstr"] = {
|
|
35511
|
+
npm: "@ai-sdk/openai-compatible",
|
|
35512
|
+
name: "routstr",
|
|
35513
|
+
options: {
|
|
35514
|
+
baseURL: `${baseUrl}/`,
|
|
35515
|
+
apiKey,
|
|
35516
|
+
includeUsage: true
|
|
35517
|
+
},
|
|
35518
|
+
models: modelsObj
|
|
35519
|
+
};
|
|
35520
|
+
opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
|
|
35521
|
+
await writeFile2(configPath, JSON.stringify(opencodeConfig, null, 2));
|
|
35522
|
+
logger3.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
|
|
35523
|
+
} catch (error) {
|
|
35524
|
+
logger3.error("Failed to install models in opencode.json:", error);
|
|
35525
|
+
}
|
|
35526
|
+
}
|
|
35527
|
+
|
|
35528
|
+
// src/integrations/pi.ts
|
|
35529
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
35530
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
35531
|
+
import { dirname as dirname3 } from "path";
|
|
35532
|
+
async function installPiIntegration(config, apiKey, integrationConfig) {
|
|
35533
|
+
const { name, configPath } = integrationConfig;
|
|
35534
|
+
logger3.log(`
|
|
35535
|
+
Installing routstr models in pi models.json...`);
|
|
35536
|
+
logger3.log(`Using API key for ${name}`);
|
|
35537
|
+
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
|
35538
|
+
let piConfig = {};
|
|
35539
|
+
try {
|
|
35540
|
+
if (existsSync5(configPath)) {
|
|
35541
|
+
const content2 = await readFile3(configPath, "utf-8");
|
|
35542
|
+
piConfig = JSON.parse(content2);
|
|
35543
|
+
}
|
|
35544
|
+
} catch {
|
|
35545
|
+
piConfig = {};
|
|
35546
|
+
}
|
|
35547
|
+
if (!piConfig.providers) {
|
|
35548
|
+
piConfig.providers = {};
|
|
35549
|
+
}
|
|
35550
|
+
try {
|
|
35551
|
+
mkdirSync2(dirname3(configPath), { recursive: true });
|
|
35552
|
+
const data = await callDaemon("/models");
|
|
35553
|
+
const models = data.output?.models || [];
|
|
35554
|
+
if (models.length === 0) {
|
|
35555
|
+
logger3.log("No models found from routstr daemon.");
|
|
35556
|
+
return;
|
|
35557
|
+
}
|
|
35558
|
+
const providerModels = models.map((model2) => ({
|
|
35559
|
+
id: model2.id
|
|
35560
|
+
}));
|
|
35561
|
+
piConfig.providers["routstr"] = {
|
|
35562
|
+
baseUrl,
|
|
35563
|
+
api: "openai-completions",
|
|
35564
|
+
apiKey,
|
|
35565
|
+
models: providerModels
|
|
35566
|
+
};
|
|
35567
|
+
await writeFile3(configPath, JSON.stringify(piConfig, null, 2));
|
|
35568
|
+
logger3.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
|
|
35569
|
+
} catch (error) {
|
|
35570
|
+
logger3.error("Failed to install models in pi models.json:", error);
|
|
35571
|
+
}
|
|
35572
|
+
}
|
|
35573
|
+
|
|
35574
|
+
// src/integrations/openclaw.ts
|
|
35575
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
35576
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
35577
|
+
import { dirname as dirname4 } from "path";
|
|
35578
|
+
var OPENCLAW_PROVIDER_ID = "routstr";
|
|
35579
|
+
var OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
|
|
35580
|
+
var OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
|
|
35581
|
+
async function installOpenClawIntegration(config, apiKey, integrationConfig) {
|
|
35582
|
+
const { name, configPath } = integrationConfig;
|
|
35583
|
+
logger3.log(`
|
|
35584
|
+
Installing routstr models in openclaw.json...`);
|
|
35585
|
+
logger3.log(`Using API key for ${name}`);
|
|
35586
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
35587
|
+
let openclawConfig = {};
|
|
35588
|
+
try {
|
|
35589
|
+
if (existsSync6(configPath)) {
|
|
35590
|
+
const content2 = await readFile4(configPath, "utf-8");
|
|
35591
|
+
openclawConfig = JSON.parse(content2);
|
|
35592
|
+
}
|
|
35593
|
+
} catch {
|
|
35594
|
+
openclawConfig = {};
|
|
35595
|
+
}
|
|
35596
|
+
if (!openclawConfig.models) {
|
|
35597
|
+
openclawConfig.models = {};
|
|
35598
|
+
}
|
|
35599
|
+
if (!openclawConfig.models.providers) {
|
|
35600
|
+
openclawConfig.models.providers = {};
|
|
35601
|
+
}
|
|
35602
|
+
if (!openclawConfig.agents) {
|
|
35603
|
+
openclawConfig.agents = {};
|
|
35604
|
+
}
|
|
35605
|
+
if (!openclawConfig.agents.defaults) {
|
|
35606
|
+
openclawConfig.agents.defaults = {};
|
|
35607
|
+
}
|
|
35608
|
+
try {
|
|
35609
|
+
mkdirSync3(dirname4(configPath), { recursive: true });
|
|
35610
|
+
const data = await callDaemon("/models");
|
|
35611
|
+
const models = data.output?.models || [];
|
|
35612
|
+
if (models.length === 0) {
|
|
35613
|
+
logger3.log("No models found from routstr daemon.");
|
|
35614
|
+
return;
|
|
35615
|
+
}
|
|
35616
|
+
const providerModels = models.map((model2) => ({
|
|
35617
|
+
id: model2.id,
|
|
35618
|
+
name: model2.name || model2.id,
|
|
35619
|
+
reasoning: true
|
|
35620
|
+
}));
|
|
35621
|
+
openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
|
|
35622
|
+
baseUrl: `${baseUrl}/v1`,
|
|
35623
|
+
apiKey,
|
|
35624
|
+
api: "openai-completions",
|
|
35625
|
+
models: providerModels
|
|
35626
|
+
};
|
|
35627
|
+
const availableModelIds = new Set(providerModels.map((model2) => model2.id));
|
|
35628
|
+
const primaryId = availableModelIds.has("gpt-5.3-codex") ? "gpt-5.3-codex" : providerModels[0]?.id;
|
|
35629
|
+
const fallbackId = availableModelIds.has("minimax-m2.5") ? "minimax-m2.5" : providerModels.find((model2) => model2.id !== primaryId)?.id;
|
|
35630
|
+
if (primaryId) {
|
|
35631
|
+
openclawConfig.agents.defaults.model = {
|
|
35632
|
+
primary: `${OPENCLAW_PROVIDER_ID}/${primaryId}`,
|
|
35633
|
+
fallbacks: fallbackId ? [`${OPENCLAW_PROVIDER_ID}/${fallbackId}`] : []
|
|
35634
|
+
};
|
|
35635
|
+
} else {
|
|
35636
|
+
openclawConfig.agents.defaults.model = {
|
|
35637
|
+
primary: OPENCLAW_DEFAULT_PRIMARY_MODEL,
|
|
35638
|
+
fallbacks: [OPENCLAW_DEFAULT_FALLBACK_MODEL]
|
|
35639
|
+
};
|
|
35640
|
+
}
|
|
35641
|
+
await writeFile4(configPath, JSON.stringify(openclawConfig, null, 2));
|
|
35642
|
+
logger3.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
|
|
35643
|
+
} catch (error) {
|
|
35644
|
+
logger3.error("Failed to install models in openclaw.json:", error);
|
|
35645
|
+
}
|
|
35646
|
+
}
|
|
35647
|
+
|
|
35648
|
+
// src/integrations/claudecode.ts
|
|
35649
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
35650
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
35651
|
+
import { dirname as dirname5 } from "path";
|
|
35652
|
+
async function installClaudeCodeIntegration(config, apiKey, integrationConfig) {
|
|
35653
|
+
const { name, configPath } = integrationConfig;
|
|
35654
|
+
logger3.log(`
|
|
35655
|
+
Installing routstr configuration in ${configPath}...`);
|
|
35656
|
+
logger3.log(`Using API key for ${name}`);
|
|
35657
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
35658
|
+
let settings = {};
|
|
35659
|
+
try {
|
|
35660
|
+
if (existsSync7(configPath)) {
|
|
35661
|
+
const content2 = await readFile5(configPath, "utf-8");
|
|
35662
|
+
settings = JSON.parse(content2);
|
|
35663
|
+
}
|
|
35664
|
+
} catch (error) {
|
|
35665
|
+
logger3.error(`Error reading ${configPath}, creating new one.`);
|
|
35666
|
+
}
|
|
35667
|
+
if (!settings.env) {
|
|
35668
|
+
settings.env = {};
|
|
35669
|
+
}
|
|
35670
|
+
settings.env["ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
|
35671
|
+
settings.env["ANTHROPIC_BASE_URL"] = baseUrl;
|
|
35672
|
+
try {
|
|
35673
|
+
const data = await callDaemon("/models");
|
|
35674
|
+
const models = data.output?.models || [];
|
|
35675
|
+
if (models.length >= 3) {
|
|
35676
|
+
const opus = models[0];
|
|
35677
|
+
const sonnet = models[1];
|
|
35678
|
+
const haiku = models[2];
|
|
35679
|
+
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = opus.id;
|
|
35680
|
+
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = sonnet.id;
|
|
35681
|
+
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = haiku.id;
|
|
35682
|
+
logger3.log(`Set Claude models: Opus=${opus.id}, Sonnet=${sonnet.id}, Haiku=${haiku.id}`);
|
|
35683
|
+
} else if (models.length > 0) {
|
|
35684
|
+
const model2 = models[0];
|
|
35685
|
+
logger3.log(`Only ${models.length} models available, falling back to defaults.`);
|
|
35686
|
+
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = model2.id;
|
|
35687
|
+
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = model2.id;
|
|
35688
|
+
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = model2.id;
|
|
35689
|
+
} else {
|
|
35690
|
+
logger3.log("No models available from routstr daemon.");
|
|
35691
|
+
}
|
|
35692
|
+
} catch (error) {
|
|
35693
|
+
logger3.error("Failed to fetch models for Claude Code integration:", error);
|
|
35694
|
+
}
|
|
35695
|
+
try {
|
|
35696
|
+
mkdirSync4(dirname5(configPath), { recursive: true });
|
|
35697
|
+
await writeFile5(configPath, JSON.stringify(settings, null, 2));
|
|
35698
|
+
logger3.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
35699
|
+
} catch (error) {
|
|
35700
|
+
logger3.error(`Failed to write to ${configPath}:`, error);
|
|
35701
|
+
}
|
|
35702
|
+
}
|
|
35703
|
+
|
|
35704
|
+
// src/integrations/registry.ts
|
|
35705
|
+
var CLIENT_CONFIGS = {
|
|
35706
|
+
opencode: {
|
|
35707
|
+
clientId: "opencode",
|
|
35708
|
+
name: "OpenCode",
|
|
35709
|
+
configPath: join4(process.env.HOME || "", ".config/opencode/opencode.json")
|
|
35710
|
+
},
|
|
35711
|
+
"pi-agent": {
|
|
35712
|
+
clientId: "pi-agent",
|
|
35713
|
+
name: "Pi Agent",
|
|
35714
|
+
configPath: join4(process.env.HOME || "", ".pi/agent/models.json")
|
|
35715
|
+
},
|
|
35716
|
+
openclaw: {
|
|
35717
|
+
clientId: "openclaw",
|
|
35718
|
+
name: "OpenClaw",
|
|
35719
|
+
configPath: join4(process.env.HOME || "", ".openclaw/openclaw.json")
|
|
35720
|
+
},
|
|
35721
|
+
"claude-code": {
|
|
35722
|
+
clientId: "claude-code",
|
|
35723
|
+
name: "Claude Code",
|
|
35724
|
+
configPath: join4(process.env.HOME || "", ".claude/settings.json")
|
|
35725
|
+
}
|
|
35726
|
+
};
|
|
35727
|
+
var CLIENT_INTEGRATIONS = {
|
|
35728
|
+
opencode: installOpencodeIntegration,
|
|
35729
|
+
"pi-agent": installPiIntegration,
|
|
35730
|
+
openclaw: installOpenClawIntegration,
|
|
35731
|
+
"claude-code": installClaudeCodeIntegration
|
|
35732
|
+
};
|
|
35733
|
+
async function runIntegrationsForClients(clientIds, config) {
|
|
35734
|
+
for (const client2 of clientIds) {
|
|
35735
|
+
const integrationFn = CLIENT_INTEGRATIONS[client2.clientId];
|
|
35736
|
+
const integrationConfig = CLIENT_CONFIGS[client2.clientId];
|
|
35737
|
+
if (integrationFn && integrationConfig && client2.apiKey) {
|
|
35738
|
+
try {
|
|
35739
|
+
await integrationFn(config, client2.apiKey, integrationConfig);
|
|
35740
|
+
} catch (error) {
|
|
35741
|
+
console.error(`Integration failed for ${client2.clientId}:`, error);
|
|
35742
|
+
}
|
|
35743
|
+
}
|
|
35744
|
+
}
|
|
35745
|
+
}
|
|
35746
|
+
|
|
35747
|
+
// src/utils/clients.ts
|
|
35748
|
+
function getNpubSuffix(config) {
|
|
35749
|
+
if (!config.daemonUrl || !config.nsec)
|
|
35750
|
+
return null;
|
|
35751
|
+
try {
|
|
35752
|
+
const secretKey = parseSecretKey(config.nsec);
|
|
35753
|
+
const npub = npubFromSecretKey(secretKey);
|
|
35754
|
+
return npub.slice(-7);
|
|
35755
|
+
} catch {
|
|
35756
|
+
return null;
|
|
35757
|
+
}
|
|
35758
|
+
}
|
|
35759
|
+
function removeSuffixFromId(id, suffix) {
|
|
35760
|
+
const suffixStr = `-${suffix}`;
|
|
35761
|
+
if (id.endsWith(suffixStr)) {
|
|
35762
|
+
return id.slice(0, -suffixStr.length);
|
|
35763
|
+
}
|
|
35764
|
+
return id;
|
|
35765
|
+
}
|
|
35766
|
+
function getClientsFromStore(store) {
|
|
35767
|
+
const state = store.getState();
|
|
35768
|
+
const clientIds = state.clientIds || [];
|
|
35769
|
+
return clientIds.map((c) => ({
|
|
35770
|
+
clientId: c.clientId,
|
|
35771
|
+
name: c.name,
|
|
35772
|
+
apiKey: c.apiKey,
|
|
35773
|
+
createdAt: c.createdAt,
|
|
35774
|
+
lastUsed: c.lastUsed
|
|
35775
|
+
}));
|
|
35776
|
+
}
|
|
35777
|
+
async function getClientsList() {
|
|
35778
|
+
const config = await loadConfig();
|
|
35779
|
+
const result = await callDaemon("/clients");
|
|
35780
|
+
const clients = result.output?.clients;
|
|
35781
|
+
if (!clients) {
|
|
35782
|
+
return [];
|
|
35783
|
+
}
|
|
35784
|
+
const suffix = config.daemonUrl ? getNpubSuffix(config) : null;
|
|
35785
|
+
return clients.filter((c) => !suffix || c.id.endsWith(`-${suffix}`)).map((c) => ({
|
|
35786
|
+
clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id,
|
|
35787
|
+
name: c.name,
|
|
35788
|
+
apiKey: c.apiKey,
|
|
35789
|
+
createdAt: c.createdAt,
|
|
35790
|
+
lastUsed: c.lastUsed
|
|
35791
|
+
}));
|
|
35792
|
+
}
|
|
35793
|
+
|
|
35794
|
+
// src/daemon/http/index.ts
|
|
35256
35795
|
function generateApiKey() {
|
|
35257
35796
|
const bytes4 = randomBytes5(24);
|
|
35258
35797
|
return `sk-${bytes4.toString("hex")}`;
|
|
@@ -35733,9 +36272,7 @@ function createDaemonRequestHandler(deps) {
|
|
|
35733
36272
|
}
|
|
35734
36273
|
if (req.method === "GET" && url2.pathname === "/clients") {
|
|
35735
36274
|
try {
|
|
35736
|
-
const
|
|
35737
|
-
const clientIds = state.clientIds || [];
|
|
35738
|
-
const clients = clientIds.map((c) => ({
|
|
36275
|
+
const clients = getClientsFromStore(deps.store).map((c) => ({
|
|
35739
36276
|
id: c.clientId,
|
|
35740
36277
|
name: c.name,
|
|
35741
36278
|
apiKey: c.apiKey,
|
|
@@ -35760,6 +36297,7 @@ function createDaemonRequestHandler(deps) {
|
|
|
35760
36297
|
const bodyText = await readBody(req);
|
|
35761
36298
|
const body = bodyText ? JSON.parse(bodyText) : {};
|
|
35762
36299
|
const name = body.name;
|
|
36300
|
+
const explicitId = body.id;
|
|
35763
36301
|
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
35764
36302
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
35765
36303
|
res.end(JSON.stringify({
|
|
@@ -35767,16 +36305,23 @@ function createDaemonRequestHandler(deps) {
|
|
|
35767
36305
|
}));
|
|
35768
36306
|
return;
|
|
35769
36307
|
}
|
|
35770
|
-
|
|
36308
|
+
if (!explicitId || typeof explicitId !== "string" || explicitId.trim() === "") {
|
|
36309
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
36310
|
+
res.end(JSON.stringify({
|
|
36311
|
+
error: "Missing required 'id' field (must be a non-empty string)."
|
|
36312
|
+
}));
|
|
36313
|
+
return;
|
|
36314
|
+
}
|
|
36315
|
+
const sanitizeId = (value) => value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
36316
|
+
const clientId = sanitizeId(explicitId);
|
|
35771
36317
|
if (!clientId) {
|
|
35772
36318
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
35773
36319
|
res.end(JSON.stringify({
|
|
35774
|
-
error: "Invalid client
|
|
36320
|
+
error: "Invalid client id. Must contain alphanumeric characters."
|
|
35775
36321
|
}));
|
|
35776
36322
|
return;
|
|
35777
36323
|
}
|
|
35778
|
-
const
|
|
35779
|
-
const existingClients = state.clientIds || [];
|
|
36324
|
+
const existingClients = getClientsFromStore(deps.store);
|
|
35780
36325
|
const existingClient = existingClients.find((c) => c.clientId === clientId);
|
|
35781
36326
|
if (existingClient) {
|
|
35782
36327
|
res.writeHead(409, { "Content-Type": "application/json" });
|
|
@@ -35826,8 +36371,7 @@ function createDaemonRequestHandler(deps) {
|
|
|
35826
36371
|
}));
|
|
35827
36372
|
return;
|
|
35828
36373
|
}
|
|
35829
|
-
const
|
|
35830
|
-
const existingClients = state.clientIds || [];
|
|
36374
|
+
const existingClients = getClientsFromStore(deps.store);
|
|
35831
36375
|
const index = existingClients.findIndex((c) => c.clientId === id);
|
|
35832
36376
|
if (index === -1) {
|
|
35833
36377
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -36040,362 +36584,18 @@ function createDaemonRequestHandler(deps) {
|
|
|
36040
36584
|
};
|
|
36041
36585
|
}
|
|
36042
36586
|
|
|
36043
|
-
// src/integrations/
|
|
36044
|
-
|
|
36045
|
-
|
|
36046
|
-
|
|
36047
|
-
|
|
36048
|
-
|
|
36049
|
-
|
|
36050
|
-
|
|
36051
|
-
|
|
36052
|
-
// src/integrations/pi.ts
|
|
36053
|
-
import { existsSync as existsSync3, mkdirSync } from "fs";
|
|
36054
|
-
import { readFile, writeFile } from "fs/promises";
|
|
36055
|
-
import { dirname } from "path";
|
|
36056
|
-
async function installPiIntegration(config, store, integrationConfig) {
|
|
36057
|
-
const { clientId, name, configPath } = integrationConfig;
|
|
36058
|
-
logger3.log(`
|
|
36059
|
-
Installing routstr models in pi models.json...`);
|
|
36060
|
-
const port = config.port || 8008;
|
|
36061
|
-
const baseUrl = `http://localhost:${port}/v1`;
|
|
36062
|
-
const state = store.getState();
|
|
36063
|
-
const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
|
|
36064
|
-
let apiKey;
|
|
36065
|
-
if (existingClient) {
|
|
36066
|
-
apiKey = existingClient.apiKey;
|
|
36067
|
-
logger3.log(`Using existing API key for ${name}`);
|
|
36068
|
-
} else {
|
|
36069
|
-
apiKey = generateApiKey2();
|
|
36070
|
-
store.getState().setClientIds((prev) => [
|
|
36071
|
-
...prev || [],
|
|
36072
|
-
{
|
|
36073
|
-
clientId,
|
|
36074
|
-
name,
|
|
36075
|
-
apiKey,
|
|
36076
|
-
createdAt: Date.now()
|
|
36077
|
-
}
|
|
36078
|
-
]);
|
|
36079
|
-
logger3.log(`Created new API key for ${name}`);
|
|
36080
|
-
}
|
|
36081
|
-
let piConfig = {};
|
|
36082
|
-
try {
|
|
36083
|
-
if (existsSync3(configPath)) {
|
|
36084
|
-
const content2 = await readFile(configPath, "utf-8");
|
|
36085
|
-
piConfig = JSON.parse(content2);
|
|
36086
|
-
}
|
|
36087
|
-
} catch {
|
|
36088
|
-
piConfig = {};
|
|
36089
|
-
}
|
|
36090
|
-
if (!piConfig.providers) {
|
|
36091
|
-
piConfig.providers = {};
|
|
36092
|
-
}
|
|
36093
|
-
try {
|
|
36094
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
36095
|
-
const response = await fetch(`http://localhost:${port}/models`);
|
|
36096
|
-
const data = await response.json();
|
|
36097
|
-
const models = data.output?.models || [];
|
|
36098
|
-
if (models.length === 0) {
|
|
36099
|
-
logger3.log("No models found from routstr daemon.");
|
|
36100
|
-
return;
|
|
36101
|
-
}
|
|
36102
|
-
const providerModels = models.map((model2) => ({
|
|
36103
|
-
id: model2.id
|
|
36104
|
-
}));
|
|
36105
|
-
piConfig.providers["routstr"] = {
|
|
36106
|
-
baseUrl,
|
|
36107
|
-
api: "openai-completions",
|
|
36108
|
-
apiKey,
|
|
36109
|
-
models: providerModels
|
|
36110
|
-
};
|
|
36111
|
-
await writeFile(configPath, JSON.stringify(piConfig, null, 2));
|
|
36112
|
-
logger3.log(`Added "routstr" provider with ${models.length} models to pi models.json`);
|
|
36113
|
-
} catch (error) {
|
|
36114
|
-
logger3.error("Failed to install models in pi models.json:", error);
|
|
36115
|
-
}
|
|
36116
|
-
}
|
|
36117
|
-
|
|
36118
|
-
// src/integrations/openclaw.ts
|
|
36119
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
36120
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
36121
|
-
import { dirname as dirname2 } from "path";
|
|
36122
|
-
var OPENCLAW_PROVIDER_ID = "routstr";
|
|
36123
|
-
var OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
|
|
36124
|
-
var OPENCLAW_DEFAULT_FALLBACK_MODEL = "routstr/kimi-k2.5";
|
|
36125
|
-
async function installOpenClawIntegration(config, store, integrationConfig) {
|
|
36126
|
-
const { clientId, name, configPath } = integrationConfig;
|
|
36127
|
-
logger3.log(`
|
|
36128
|
-
Installing routstr models in openclaw.json...`);
|
|
36129
|
-
const port = config.port || 8008;
|
|
36130
|
-
const state = store.getState();
|
|
36131
|
-
const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
|
|
36132
|
-
let apiKey;
|
|
36133
|
-
if (existingClient) {
|
|
36134
|
-
apiKey = existingClient.apiKey;
|
|
36135
|
-
logger3.log(`Using existing API key for ${name}`);
|
|
36136
|
-
} else {
|
|
36137
|
-
apiKey = generateApiKey2();
|
|
36138
|
-
store.getState().setClientIds((prev) => [
|
|
36139
|
-
...prev || [],
|
|
36140
|
-
{
|
|
36141
|
-
clientId,
|
|
36142
|
-
name,
|
|
36143
|
-
apiKey,
|
|
36144
|
-
createdAt: Date.now()
|
|
36145
|
-
}
|
|
36146
|
-
]);
|
|
36147
|
-
logger3.log(`Created new API key for ${name}`);
|
|
36148
|
-
}
|
|
36149
|
-
let openclawConfig = {};
|
|
36150
|
-
try {
|
|
36151
|
-
if (existsSync4(configPath)) {
|
|
36152
|
-
const content2 = await readFile2(configPath, "utf-8");
|
|
36153
|
-
openclawConfig = JSON.parse(content2);
|
|
36154
|
-
}
|
|
36155
|
-
} catch {
|
|
36156
|
-
openclawConfig = {};
|
|
36157
|
-
}
|
|
36158
|
-
if (!openclawConfig.models) {
|
|
36159
|
-
openclawConfig.models = {};
|
|
36160
|
-
}
|
|
36161
|
-
if (!openclawConfig.models.providers) {
|
|
36162
|
-
openclawConfig.models.providers = {};
|
|
36163
|
-
}
|
|
36164
|
-
if (!openclawConfig.agents) {
|
|
36165
|
-
openclawConfig.agents = {};
|
|
36166
|
-
}
|
|
36167
|
-
if (!openclawConfig.agents.defaults) {
|
|
36168
|
-
openclawConfig.agents.defaults = {};
|
|
36169
|
-
}
|
|
36170
|
-
try {
|
|
36171
|
-
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
36172
|
-
const response = await fetch(`http://localhost:${port}/models`);
|
|
36173
|
-
const data = await response.json();
|
|
36174
|
-
const models = data.output?.models || [];
|
|
36175
|
-
if (models.length === 0) {
|
|
36176
|
-
logger3.log("No models found from routstr daemon.");
|
|
36177
|
-
return;
|
|
36178
|
-
}
|
|
36179
|
-
const providerModels = models.map((model2) => ({
|
|
36180
|
-
id: model2.id,
|
|
36181
|
-
name: model2.name || model2.id,
|
|
36182
|
-
reasoning: true
|
|
36183
|
-
}));
|
|
36184
|
-
openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
|
|
36185
|
-
baseUrl: `http://localhost:${port}/v1`,
|
|
36186
|
-
apiKey,
|
|
36187
|
-
api: "openai-completions",
|
|
36188
|
-
models: providerModels
|
|
36189
|
-
};
|
|
36190
|
-
const availableModelIds = new Set(providerModels.map((model2) => model2.id));
|
|
36191
|
-
const primaryId = availableModelIds.has("gpt-5.3-codex") ? "gpt-5.3-codex" : providerModels[0]?.id;
|
|
36192
|
-
const fallbackId = availableModelIds.has("minimax-m2.5") ? "minimax-m2.5" : providerModels.find((model2) => model2.id !== primaryId)?.id;
|
|
36193
|
-
if (primaryId) {
|
|
36194
|
-
openclawConfig.agents.defaults.model = {
|
|
36195
|
-
primary: `${OPENCLAW_PROVIDER_ID}/${primaryId}`,
|
|
36196
|
-
fallbacks: fallbackId ? [`${OPENCLAW_PROVIDER_ID}/${fallbackId}`] : []
|
|
36197
|
-
};
|
|
36198
|
-
} else {
|
|
36199
|
-
openclawConfig.agents.defaults.model = {
|
|
36200
|
-
primary: OPENCLAW_DEFAULT_PRIMARY_MODEL,
|
|
36201
|
-
fallbacks: [OPENCLAW_DEFAULT_FALLBACK_MODEL]
|
|
36202
|
-
};
|
|
36203
|
-
}
|
|
36204
|
-
await writeFile2(configPath, JSON.stringify(openclawConfig, null, 2));
|
|
36205
|
-
logger3.log(`Added "${OPENCLAW_PROVIDER_ID}" provider with ${models.length} models to openclaw.json`);
|
|
36206
|
-
} catch (error) {
|
|
36207
|
-
logger3.error("Failed to install models in openclaw.json:", error);
|
|
36208
|
-
}
|
|
36209
|
-
}
|
|
36210
|
-
|
|
36211
|
-
// src/integrations/claudecode.ts
|
|
36212
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
36213
|
-
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
36214
|
-
import { dirname as dirname3 } from "path";
|
|
36215
|
-
async function installClaudeCodeIntegration(config, store, integrationConfig) {
|
|
36216
|
-
const { clientId, name, configPath } = integrationConfig;
|
|
36217
|
-
logger3.log(`
|
|
36218
|
-
Installing routstr configuration in ${configPath}...`);
|
|
36219
|
-
const port = config.port || 8008;
|
|
36220
|
-
const state = store.getState();
|
|
36221
|
-
const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
|
|
36222
|
-
let apiKey;
|
|
36223
|
-
if (existingClient) {
|
|
36224
|
-
apiKey = existingClient.apiKey;
|
|
36225
|
-
logger3.log(`Using existing API key for ${name}`);
|
|
36226
|
-
} else {
|
|
36227
|
-
apiKey = generateApiKey2();
|
|
36228
|
-
store.getState().setClientIds((prev) => [
|
|
36229
|
-
...prev || [],
|
|
36230
|
-
{
|
|
36231
|
-
clientId,
|
|
36232
|
-
name,
|
|
36233
|
-
apiKey,
|
|
36234
|
-
createdAt: Date.now()
|
|
36235
|
-
}
|
|
36236
|
-
]);
|
|
36237
|
-
logger3.log(`Created new API key for ${name}`);
|
|
36238
|
-
}
|
|
36239
|
-
let settings = {};
|
|
36240
|
-
try {
|
|
36241
|
-
if (existsSync5(configPath)) {
|
|
36242
|
-
const content2 = await readFile3(configPath, "utf-8");
|
|
36243
|
-
settings = JSON.parse(content2);
|
|
36244
|
-
}
|
|
36245
|
-
} catch (error) {
|
|
36246
|
-
logger3.error(`Error reading ${configPath}, creating new one.`);
|
|
36247
|
-
}
|
|
36248
|
-
if (!settings.env) {
|
|
36249
|
-
settings.env = {};
|
|
36250
|
-
}
|
|
36251
|
-
settings.env["ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
|
36252
|
-
settings.env["ANTHROPIC_BASE_URL"] = `http://localhost:${port}`;
|
|
36253
|
-
try {
|
|
36254
|
-
const response = await fetch(`http://localhost:${port}/models`);
|
|
36255
|
-
const data = await response.json();
|
|
36256
|
-
const models = data.output?.models || [];
|
|
36257
|
-
if (models.length >= 3) {
|
|
36258
|
-
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = models[0].id;
|
|
36259
|
-
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = models[1].id;
|
|
36260
|
-
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = models[2].id;
|
|
36261
|
-
logger3.log(`Set Claude models: Opus=${models[0].id}, Sonnet=${models[1].id}, Haiku=${models[2].id}`);
|
|
36262
|
-
} else if (models.length > 0) {
|
|
36263
|
-
logger3.log(`Only ${models.length} models available, falling back to defaults.`);
|
|
36264
|
-
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = models[0].id;
|
|
36265
|
-
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = models[0].id;
|
|
36266
|
-
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = models[0].id;
|
|
36267
|
-
} else {
|
|
36268
|
-
logger3.log("No models available from routstr daemon.");
|
|
36269
|
-
}
|
|
36270
|
-
} catch (error) {
|
|
36271
|
-
logger3.error("Failed to fetch models for Claude Code integration:", error);
|
|
36272
|
-
}
|
|
36273
|
-
try {
|
|
36274
|
-
mkdirSync3(dirname3(configPath), { recursive: true });
|
|
36275
|
-
await writeFile3(configPath, JSON.stringify(settings, null, 2));
|
|
36276
|
-
logger3.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
36277
|
-
} catch (error) {
|
|
36278
|
-
logger3.error(`Failed to write to ${configPath}:`, error);
|
|
36279
|
-
}
|
|
36280
|
-
}
|
|
36281
|
-
|
|
36282
|
-
// src/integrations/registry.ts
|
|
36283
|
-
function generateApiKey2() {
|
|
36284
|
-
const bytes4 = randomBytes6(24);
|
|
36285
|
-
return `sk-${bytes4.toString("hex")}`;
|
|
36286
|
-
}
|
|
36287
|
-
var CLIENT_CONFIGS = {
|
|
36288
|
-
opencode: {
|
|
36289
|
-
clientId: "opencode",
|
|
36290
|
-
name: "OpenCode",
|
|
36291
|
-
configPath: join4(process.env.HOME || "", ".config/opencode/opencode.json")
|
|
36292
|
-
},
|
|
36293
|
-
"pi-agent": {
|
|
36294
|
-
clientId: "pi-agent",
|
|
36295
|
-
name: "Pi Agent",
|
|
36296
|
-
configPath: join4(process.env.HOME || "", ".pi/agent/models.json")
|
|
36297
|
-
},
|
|
36298
|
-
openclaw: {
|
|
36299
|
-
clientId: "openclaw",
|
|
36300
|
-
name: "OpenClaw",
|
|
36301
|
-
configPath: join4(process.env.HOME || "", ".openclaw/openclaw.json")
|
|
36302
|
-
},
|
|
36303
|
-
"claude-code": {
|
|
36304
|
-
clientId: "claude-code",
|
|
36305
|
-
name: "Claude Code",
|
|
36306
|
-
configPath: join4(process.env.HOME || "", ".claude/settings.json")
|
|
36307
|
-
}
|
|
36308
|
-
};
|
|
36309
|
-
var CLIENT_INTEGRATIONS = {
|
|
36310
|
-
opencode: installOpencodeIntegration,
|
|
36311
|
-
"pi-agent": installPiIntegration,
|
|
36312
|
-
openclaw: installOpenClawIntegration,
|
|
36313
|
-
"claude-code": installClaudeCodeIntegration
|
|
36314
|
-
};
|
|
36315
|
-
async function runIntegrationsForClients(clientIds, config, store) {
|
|
36316
|
-
for (const client2 of clientIds) {
|
|
36317
|
-
const integrationFn = CLIENT_INTEGRATIONS[client2.clientId];
|
|
36318
|
-
const integrationConfig = CLIENT_CONFIGS[client2.clientId];
|
|
36319
|
-
if (integrationFn && integrationConfig) {
|
|
36320
|
-
try {
|
|
36321
|
-
await integrationFn(config, store, integrationConfig);
|
|
36322
|
-
} catch (error) {
|
|
36323
|
-
console.error(`Integration failed for ${client2.clientId}:`, error);
|
|
36324
|
-
}
|
|
36325
|
-
}
|
|
36587
|
+
// src/integrations/index.ts
|
|
36588
|
+
async function refreshModelsAndIntegrations(getRoutstr21Models, config, label = "Scheduled") {
|
|
36589
|
+
await getRoutstr21Models(true);
|
|
36590
|
+
logger3.log(`${label} model refresh completed successfully.`);
|
|
36591
|
+
const clientIds = await getClientsList();
|
|
36592
|
+
if (clientIds.length > 0) {
|
|
36593
|
+
logger3.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
36594
|
+
await runIntegrationsForClients(clientIds, config);
|
|
36595
|
+
logger3.log("Client integrations refreshed.");
|
|
36326
36596
|
}
|
|
36327
36597
|
}
|
|
36328
36598
|
|
|
36329
|
-
// src/integrations/opencode.ts
|
|
36330
|
-
var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
36331
|
-
async function installOpencodeIntegration(config, store, integrationConfig) {
|
|
36332
|
-
const { clientId, name, configPath } = integrationConfig;
|
|
36333
|
-
logger3.log(`
|
|
36334
|
-
Installing routstr models in opencode.json...`);
|
|
36335
|
-
const port = config.port || 8008;
|
|
36336
|
-
const state = store.getState();
|
|
36337
|
-
const existingClient = (state.clientIds || []).find((c) => c.clientId === clientId);
|
|
36338
|
-
let apiKey;
|
|
36339
|
-
if (existingClient) {
|
|
36340
|
-
apiKey = existingClient.apiKey;
|
|
36341
|
-
logger3.log(`Using existing API key for ${name}`);
|
|
36342
|
-
} else {
|
|
36343
|
-
apiKey = generateApiKey2();
|
|
36344
|
-
store.getState().setClientIds((prev) => [
|
|
36345
|
-
...prev || [],
|
|
36346
|
-
{
|
|
36347
|
-
clientId,
|
|
36348
|
-
name,
|
|
36349
|
-
apiKey,
|
|
36350
|
-
createdAt: Date.now()
|
|
36351
|
-
}
|
|
36352
|
-
]);
|
|
36353
|
-
logger3.log(`Created new API key for ${name}`);
|
|
36354
|
-
}
|
|
36355
|
-
let opencodeConfig;
|
|
36356
|
-
try {
|
|
36357
|
-
if (existsSync6(configPath)) {
|
|
36358
|
-
const content2 = await readFile4(configPath, "utf-8");
|
|
36359
|
-
opencodeConfig = JSON.parse(content2);
|
|
36360
|
-
} else {
|
|
36361
|
-
opencodeConfig = { provider: {} };
|
|
36362
|
-
}
|
|
36363
|
-
} catch {
|
|
36364
|
-
opencodeConfig = { provider: {} };
|
|
36365
|
-
}
|
|
36366
|
-
if (!opencodeConfig.provider) {
|
|
36367
|
-
opencodeConfig.provider = {};
|
|
36368
|
-
}
|
|
36369
|
-
try {
|
|
36370
|
-
mkdirSync4(dirname4(configPath), { recursive: true });
|
|
36371
|
-
const response = await fetch(`http://localhost:${port}/models`);
|
|
36372
|
-
const data = await response.json();
|
|
36373
|
-
const models = data.output?.models || [];
|
|
36374
|
-
if (models.length === 0) {
|
|
36375
|
-
logger3.log("No models found from routstr daemon.");
|
|
36376
|
-
return;
|
|
36377
|
-
}
|
|
36378
|
-
const modelsObj = {};
|
|
36379
|
-
for (const model2 of models) {
|
|
36380
|
-
modelsObj[model2.id] = { name: model2.name || model2.id };
|
|
36381
|
-
}
|
|
36382
|
-
opencodeConfig.provider["routstr"] = {
|
|
36383
|
-
npm: "@ai-sdk/openai-compatible",
|
|
36384
|
-
name: "routstr",
|
|
36385
|
-
options: {
|
|
36386
|
-
baseURL: `http://localhost:${port}/`,
|
|
36387
|
-
apiKey,
|
|
36388
|
-
includeUsage: true
|
|
36389
|
-
},
|
|
36390
|
-
models: modelsObj
|
|
36391
|
-
};
|
|
36392
|
-
opencodeConfig.small_model = OPENCODE_SMALL_MODEL;
|
|
36393
|
-
await writeFile4(configPath, JSON.stringify(opencodeConfig, null, 2));
|
|
36394
|
-
logger3.log(`Added "routstr" provider with ${models.length} models to opencode.json`);
|
|
36395
|
-
} catch (error) {
|
|
36396
|
-
logger3.error("Failed to install models in opencode.json:", error);
|
|
36397
|
-
}
|
|
36398
|
-
}
|
|
36399
36599
|
// src/daemon/index.ts
|
|
36400
36600
|
init_dist3();
|
|
36401
36601
|
async function main() {
|
|
@@ -36407,7 +36607,8 @@ async function main() {
|
|
|
36407
36607
|
const updatedConfig = { ...config, port, provider };
|
|
36408
36608
|
saveDaemonConfig(updatedConfig);
|
|
36409
36609
|
const sqliteDriver = await createBunSqliteDriver2(DB_PATH);
|
|
36410
|
-
const { store } =
|
|
36610
|
+
const { store, hydrate } = createSdkStore({ driver: sqliteDriver });
|
|
36611
|
+
await hydrate;
|
|
36411
36612
|
const { Database } = await import("bun:sqlite");
|
|
36412
36613
|
const usageTrackingDriver = createBunSqliteUsageTrackingDriver2({
|
|
36413
36614
|
dbPath: DB_PATH,
|
|
@@ -36445,7 +36646,7 @@ async function main() {
|
|
|
36445
36646
|
}));
|
|
36446
36647
|
Bun.write(PID_FILE, String(process.pid));
|
|
36447
36648
|
try {
|
|
36448
|
-
if (
|
|
36649
|
+
if (existsSync8(SOCKET_PATH)) {
|
|
36449
36650
|
Bun.spawn(["rm", SOCKET_PATH]);
|
|
36450
36651
|
}
|
|
36451
36652
|
} catch {
|
|
@@ -36457,15 +36658,7 @@ async function main() {
|
|
|
36457
36658
|
refreshInterval = setInterval(async () => {
|
|
36458
36659
|
logger3.log("Running scheduled model refresh...");
|
|
36459
36660
|
try {
|
|
36460
|
-
await getRoutstr21Models
|
|
36461
|
-
logger3.log("Scheduled model refresh completed successfully.");
|
|
36462
|
-
const state = store.getState();
|
|
36463
|
-
const clientIds = state.clientIds || [];
|
|
36464
|
-
if (clientIds.length > 0) {
|
|
36465
|
-
logger3.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
36466
|
-
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
36467
|
-
logger3.log("Client integrations refreshed.");
|
|
36468
|
-
}
|
|
36661
|
+
await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled");
|
|
36469
36662
|
} catch (error) {
|
|
36470
36663
|
logger3.error("Scheduled model refresh failed:", error);
|
|
36471
36664
|
}
|
|
@@ -36526,20 +36719,11 @@ async function main() {
|
|
|
36526
36719
|
});
|
|
36527
36720
|
server.listen(port, async () => {
|
|
36528
36721
|
logger3.log(`Routstr daemon listening on http://localhost:${port}/v1`);
|
|
36529
|
-
ensureProvidersBootstrapped().then(() => {
|
|
36722
|
+
ensureProvidersBootstrapped().then(async () => {
|
|
36530
36723
|
startModelRefreshJob();
|
|
36531
36724
|
startRefundJob();
|
|
36532
36725
|
logger3.log("Running initial model refresh...");
|
|
36533
|
-
|
|
36534
|
-
}).then(async () => {
|
|
36535
|
-
logger3.log("Initial model refresh completed.");
|
|
36536
|
-
const state = store.getState();
|
|
36537
|
-
const clientIds = state.clientIds || [];
|
|
36538
|
-
if (clientIds.length > 0) {
|
|
36539
|
-
logger3.log(`Refreshing ${clientIds.length} client integration(s)...`);
|
|
36540
|
-
await runIntegrationsForClients(clientIds, updatedConfig, store);
|
|
36541
|
-
logger3.log("Client integrations refreshed.");
|
|
36542
|
-
}
|
|
36726
|
+
await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Initial");
|
|
36543
36727
|
}).catch((error) => {
|
|
36544
36728
|
logger3.error("Initial model refresh failed:", error);
|
|
36545
36729
|
startModelRefreshJob();
|