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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { logger } from "../../utils/logger";
|
|
4
|
+
import { withCrossProcessLock } from "../../utils/process-lock";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_CONFIG_DIR =
|
|
6
7
|
process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
|
|
@@ -139,6 +140,7 @@ export function createCocodClient(
|
|
|
139
140
|
): CocodClient {
|
|
140
141
|
const executable = resolveCocodExecutable(options.cocodPath);
|
|
141
142
|
const socketPath = options.socketPath || DEFAULT_SOCKET_PATH;
|
|
143
|
+
const startupLockPath = `${socketPath}.startup.lock`;
|
|
142
144
|
const fetchImpl = options.fetchImpl || (fetch as CocodFetch);
|
|
143
145
|
const pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
144
146
|
const startupTimeoutMs = options.startupTimeoutMs ?? 5000;
|
|
@@ -228,7 +230,7 @@ export function createCocodClient(
|
|
|
228
230
|
|
|
229
231
|
async function startDaemon(): Promise<void> {
|
|
230
232
|
const env = { ...process.env, COCOD_SOCKET: socketPath };
|
|
231
|
-
const proc = spawnDaemon([executable, "
|
|
233
|
+
const proc = spawnDaemon([executable, "init"], env);
|
|
232
234
|
const maxPolls = Math.ceil(startupTimeoutMs / pollIntervalMs);
|
|
233
235
|
let exitCode: number | null = null;
|
|
234
236
|
|
|
@@ -239,8 +241,8 @@ export function createCocodClient(
|
|
|
239
241
|
for (let i = 0; i < maxPolls; i++) {
|
|
240
242
|
await delay(pollIntervalMs);
|
|
241
243
|
|
|
242
|
-
if (exitCode !== null) {
|
|
243
|
-
throw new Error(`cocod
|
|
244
|
+
if (exitCode !== null && exitCode !== 0) {
|
|
245
|
+
throw new Error(`cocod init exited early with code ${exitCode}`);
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
if (await pingInternal()) {
|
|
@@ -250,7 +252,7 @@ export function createCocodClient(
|
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
throw new Error(
|
|
253
|
-
`cocod
|
|
255
|
+
`cocod failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds`,
|
|
254
256
|
);
|
|
255
257
|
}
|
|
256
258
|
|
|
@@ -260,8 +262,22 @@ export function createCocodClient(
|
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
if (!startPromise) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
startPromise = withCrossProcessLock(
|
|
266
|
+
startupLockPath,
|
|
267
|
+
async () => {
|
|
268
|
+
if (await pingInternal()) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
logger.debug(`Starting cocod daemon via ${executable} init...`);
|
|
273
|
+
await startDaemon();
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
acquireTimeoutMs: startupTimeoutMs + 30_000,
|
|
277
|
+
staleAfterMs: startupTimeoutMs + 30_000,
|
|
278
|
+
log: (message) => logger.debug(message),
|
|
279
|
+
},
|
|
280
|
+
).finally(() => {
|
|
265
281
|
startPromise = null;
|
|
266
282
|
});
|
|
267
283
|
}
|
|
@@ -3,44 +3,20 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
5
5
|
import { logger } from "../utils/logger";
|
|
6
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
7
6
|
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
-
import {
|
|
7
|
+
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
|
9
8
|
|
|
10
9
|
export async function installClaudeCodeIntegration(
|
|
11
10
|
config: RoutstrdConfig,
|
|
12
|
-
|
|
11
|
+
apiKey: string,
|
|
13
12
|
integrationConfig: IntegrationConfig,
|
|
14
13
|
): Promise<void> {
|
|
15
|
-
const {
|
|
14
|
+
const { name, configPath } = integrationConfig;
|
|
16
15
|
|
|
17
16
|
logger.log(`\nInstalling routstr configuration in ${configPath}...`);
|
|
17
|
+
logger.log(`Using API key for ${name}`);
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
// Get or create clientId entry
|
|
22
|
-
const state = store.getState();
|
|
23
|
-
const existingClient = (state.clientIds || []).find(
|
|
24
|
-
(c: { clientId: string }) => c.clientId === clientId,
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
let apiKey: string;
|
|
28
|
-
if (existingClient) {
|
|
29
|
-
apiKey = existingClient.apiKey;
|
|
30
|
-
logger.log(`Using existing API key for ${name}`);
|
|
31
|
-
} else {
|
|
32
|
-
apiKey = generateApiKey();
|
|
33
|
-
store.getState().setClientIds((prev) => [
|
|
34
|
-
...(prev || []),
|
|
35
|
-
{
|
|
36
|
-
clientId,
|
|
37
|
-
name,
|
|
38
|
-
apiKey,
|
|
39
|
-
createdAt: Date.now(),
|
|
40
|
-
},
|
|
41
|
-
]);
|
|
42
|
-
logger.log(`Created new API key for ${name}`);
|
|
43
|
-
}
|
|
19
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
44
20
|
|
|
45
21
|
let settings: {
|
|
46
22
|
env?: Record<string, string>;
|
|
@@ -60,23 +36,26 @@ export async function installClaudeCodeIntegration(
|
|
|
60
36
|
}
|
|
61
37
|
|
|
62
38
|
settings.env["ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
|
63
|
-
settings.env["ANTHROPIC_BASE_URL"] =
|
|
39
|
+
settings.env["ANTHROPIC_BASE_URL"] = baseUrl;
|
|
64
40
|
|
|
65
41
|
try {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const models = data.output?.models || [];
|
|
42
|
+
const data = await callDaemon("/models");
|
|
43
|
+
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
|
69
44
|
|
|
70
45
|
if (models.length >= 3) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
46
|
+
const opus = models[0]!;
|
|
47
|
+
const sonnet = models[1]!;
|
|
48
|
+
const haiku = models[2]!;
|
|
49
|
+
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = opus.id;
|
|
50
|
+
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = sonnet.id;
|
|
51
|
+
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = haiku.id;
|
|
52
|
+
logger.log(`Set Claude models: Opus=${opus.id}, Sonnet=${sonnet.id}, Haiku=${haiku.id}`);
|
|
75
53
|
} else if (models.length > 0) {
|
|
54
|
+
const model = models[0]!;
|
|
76
55
|
logger.log(`Only ${models.length} models available, falling back to defaults.`);
|
|
77
|
-
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] =
|
|
78
|
-
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] =
|
|
79
|
-
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] =
|
|
56
|
+
settings.env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = model.id;
|
|
57
|
+
settings.env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = model.id;
|
|
58
|
+
settings.env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = model.id;
|
|
80
59
|
} else {
|
|
81
60
|
logger.log("No models available from routstr daemon.");
|
|
82
61
|
}
|
|
@@ -3,9 +3,8 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
5
5
|
import { logger } from "../utils/logger";
|
|
6
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
7
6
|
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
-
import {
|
|
7
|
+
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
|
9
8
|
|
|
10
9
|
const OPENCLAW_PROVIDER_ID = "routstr";
|
|
11
10
|
const OPENCLAW_DEFAULT_PRIMARY_MODEL = "routstr/minimax-m2.5";
|
|
@@ -52,39 +51,15 @@ function toAlias(modelId: string): string {
|
|
|
52
51
|
|
|
53
52
|
export async function installOpenClawIntegration(
|
|
54
53
|
config: RoutstrdConfig,
|
|
55
|
-
|
|
54
|
+
apiKey: string,
|
|
56
55
|
integrationConfig: IntegrationConfig,
|
|
57
56
|
): Promise<void> {
|
|
58
|
-
const {
|
|
57
|
+
const { name, configPath } = integrationConfig;
|
|
59
58
|
|
|
60
59
|
logger.log("\nInstalling routstr models in openclaw.json...");
|
|
60
|
+
logger.log(`Using API key for ${name}`);
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
// Get or create clientId entry for OpenClaw
|
|
65
|
-
const state = store.getState();
|
|
66
|
-
const existingClient = (state.clientIds || []).find(
|
|
67
|
-
(c: { clientId: string }) => c.clientId === clientId,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
let apiKey: string;
|
|
71
|
-
if (existingClient) {
|
|
72
|
-
apiKey = existingClient.apiKey;
|
|
73
|
-
logger.log(`Using existing API key for ${name}`);
|
|
74
|
-
} else {
|
|
75
|
-
apiKey = generateApiKey();
|
|
76
|
-
// Add new clientId entry using proper store action
|
|
77
|
-
store.getState().setClientIds((prev) => [
|
|
78
|
-
...(prev || []),
|
|
79
|
-
{
|
|
80
|
-
clientId,
|
|
81
|
-
name,
|
|
82
|
-
apiKey,
|
|
83
|
-
createdAt: Date.now(),
|
|
84
|
-
},
|
|
85
|
-
]);
|
|
86
|
-
logger.log(`Created new API key for ${name}`);
|
|
87
|
-
}
|
|
62
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
88
63
|
|
|
89
64
|
let openclawConfig: OpenClawConfig = {};
|
|
90
65
|
|
|
@@ -113,9 +88,8 @@ export async function installOpenClawIntegration(
|
|
|
113
88
|
try {
|
|
114
89
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
115
90
|
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const models = data.output?.models || [];
|
|
91
|
+
const data = await callDaemon("/models");
|
|
92
|
+
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
|
119
93
|
|
|
120
94
|
if (models.length === 0) {
|
|
121
95
|
logger.log("No models found from routstr daemon.");
|
|
@@ -129,7 +103,7 @@ export async function installOpenClawIntegration(
|
|
|
129
103
|
}));
|
|
130
104
|
|
|
131
105
|
openclawConfig.models.providers[OPENCLAW_PROVIDER_ID] = {
|
|
132
|
-
baseUrl:
|
|
106
|
+
baseUrl: `${baseUrl}/v1`,
|
|
133
107
|
apiKey,
|
|
134
108
|
api: "openai-completions",
|
|
135
109
|
models: providerModels,
|
|
@@ -3,47 +3,22 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
5
5
|
import { logger } from "../utils/logger";
|
|
6
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
7
6
|
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
-
import {
|
|
7
|
+
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
|
9
8
|
|
|
10
9
|
const OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
11
10
|
|
|
12
11
|
export async function installOpencodeIntegration(
|
|
13
12
|
config: RoutstrdConfig,
|
|
14
|
-
|
|
13
|
+
apiKey: string,
|
|
15
14
|
integrationConfig: IntegrationConfig,
|
|
16
15
|
): Promise<void> {
|
|
17
|
-
const {
|
|
16
|
+
const { name, configPath } = integrationConfig;
|
|
18
17
|
|
|
19
18
|
logger.log("\nInstalling routstr models in opencode.json...");
|
|
19
|
+
logger.log(`Using API key for ${name}`);
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
// Get or create clientId entry for OpenCode
|
|
24
|
-
const state = store.getState();
|
|
25
|
-
const existingClient = (state.clientIds || []).find(
|
|
26
|
-
(c: { clientId: string }) => c.clientId === clientId,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
let apiKey: string;
|
|
30
|
-
if (existingClient) {
|
|
31
|
-
apiKey = existingClient.apiKey;
|
|
32
|
-
logger.log(`Using existing API key for ${name}`);
|
|
33
|
-
} else {
|
|
34
|
-
apiKey = generateApiKey();
|
|
35
|
-
// Add new clientId entry using proper store action
|
|
36
|
-
store.getState().setClientIds((prev) => [
|
|
37
|
-
...(prev || []),
|
|
38
|
-
{
|
|
39
|
-
clientId,
|
|
40
|
-
name,
|
|
41
|
-
apiKey,
|
|
42
|
-
createdAt: Date.now(),
|
|
43
|
-
},
|
|
44
|
-
]);
|
|
45
|
-
logger.log(`Created new API key for ${name}`);
|
|
46
|
-
}
|
|
21
|
+
const baseUrl = getDaemonBaseUrl(config);
|
|
47
22
|
|
|
48
23
|
let opencodeConfig: {
|
|
49
24
|
provider?: Record<string, {
|
|
@@ -77,9 +52,8 @@ export async function installOpencodeIntegration(
|
|
|
77
52
|
try {
|
|
78
53
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
79
54
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const models = data.output?.models || [];
|
|
55
|
+
const data = await callDaemon("/models");
|
|
56
|
+
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
|
83
57
|
|
|
84
58
|
if (models.length === 0) {
|
|
85
59
|
logger.log("No models found from routstr daemon.");
|
|
@@ -95,7 +69,7 @@ export async function installOpencodeIntegration(
|
|
|
95
69
|
npm: "@ai-sdk/openai-compatible",
|
|
96
70
|
name: "routstr",
|
|
97
71
|
options: {
|
|
98
|
-
baseURL:
|
|
72
|
+
baseURL: `${baseUrl}/`,
|
|
99
73
|
apiKey,
|
|
100
74
|
includeUsage: true,
|
|
101
75
|
},
|
package/src/integrations/pi.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
import type { RoutstrdConfig } from "../utils/config";
|
|
5
5
|
import { logger } from "../utils/logger";
|
|
6
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
7
6
|
import type { IntegrationConfig, RoutstrModel } from "./registry";
|
|
8
|
-
import {
|
|
7
|
+
import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
|
|
9
8
|
|
|
10
9
|
type PiModelEntry = {
|
|
11
10
|
id: string;
|
|
@@ -24,40 +23,15 @@ type PiConfig = {
|
|
|
24
23
|
|
|
25
24
|
export async function installPiIntegration(
|
|
26
25
|
config: RoutstrdConfig,
|
|
27
|
-
|
|
26
|
+
apiKey: string,
|
|
28
27
|
integrationConfig: IntegrationConfig,
|
|
29
28
|
): Promise<void> {
|
|
30
|
-
const {
|
|
29
|
+
const { name, configPath } = integrationConfig;
|
|
31
30
|
|
|
32
31
|
logger.log("\nInstalling routstr models in pi models.json...");
|
|
32
|
+
logger.log(`Using API key for ${name}`);
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
const baseUrl = `http://localhost:${port}/v1`;
|
|
36
|
-
|
|
37
|
-
// Get or create clientId entry for Pi Agent
|
|
38
|
-
const state = store.getState();
|
|
39
|
-
const existingClient = (state.clientIds || []).find(
|
|
40
|
-
(c: { clientId: string }) => c.clientId === clientId,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
let apiKey: string;
|
|
44
|
-
if (existingClient) {
|
|
45
|
-
apiKey = existingClient.apiKey;
|
|
46
|
-
logger.log(`Using existing API key for ${name}`);
|
|
47
|
-
} else {
|
|
48
|
-
apiKey = generateApiKey();
|
|
49
|
-
// Add new clientId entry using proper store action
|
|
50
|
-
store.getState().setClientIds((prev) => [
|
|
51
|
-
...(prev || []),
|
|
52
|
-
{
|
|
53
|
-
clientId,
|
|
54
|
-
name,
|
|
55
|
-
apiKey,
|
|
56
|
-
createdAt: Date.now(),
|
|
57
|
-
},
|
|
58
|
-
]);
|
|
59
|
-
logger.log(`Created new API key for ${name}`);
|
|
60
|
-
}
|
|
34
|
+
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
|
61
35
|
|
|
62
36
|
let piConfig: PiConfig = {};
|
|
63
37
|
|
|
@@ -78,9 +52,8 @@ export async function installPiIntegration(
|
|
|
78
52
|
// Ensure directory exists
|
|
79
53
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
80
54
|
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const models = data.output?.models || [];
|
|
55
|
+
const data = await callDaemon("/models");
|
|
56
|
+
const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
|
|
84
57
|
|
|
85
58
|
if (models.length === 0) {
|
|
86
59
|
logger.log("No models found from routstr daemon.");
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { join } from "path";
|
|
3
2
|
import type { RoutstrdConfig } from "../utils/config";
|
|
4
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
5
3
|
import { installOpencodeIntegration } from "./opencode";
|
|
6
4
|
import { installPiIntegration } from "./pi";
|
|
7
5
|
import { installOpenClawIntegration } from "./openclaw";
|
|
@@ -18,14 +16,9 @@ export type RoutstrModel = {
|
|
|
18
16
|
name?: string;
|
|
19
17
|
};
|
|
20
18
|
|
|
21
|
-
export function generateApiKey(): string {
|
|
22
|
-
const bytes = randomBytes(24);
|
|
23
|
-
return `sk-${bytes.toString("hex")}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
19
|
export type IntegrationFn = (
|
|
27
20
|
config: RoutstrdConfig,
|
|
28
|
-
|
|
21
|
+
apiKey: string,
|
|
29
22
|
integrationConfig: IntegrationConfig,
|
|
30
23
|
) => Promise<void>;
|
|
31
24
|
|
|
@@ -60,16 +53,15 @@ export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
|
|
|
60
53
|
};
|
|
61
54
|
|
|
62
55
|
export async function runIntegrationsForClients(
|
|
63
|
-
clientIds: Array<{ clientId: string }>,
|
|
56
|
+
clientIds: Array<{ clientId: string; apiKey?: string }>,
|
|
64
57
|
config: RoutstrdConfig,
|
|
65
|
-
store: SdkStore,
|
|
66
58
|
): Promise<void> {
|
|
67
59
|
for (const client of clientIds) {
|
|
68
60
|
const integrationFn = CLIENT_INTEGRATIONS[client.clientId];
|
|
69
61
|
const integrationConfig = CLIENT_CONFIGS[client.clientId];
|
|
70
|
-
if (integrationFn && integrationConfig) {
|
|
62
|
+
if (integrationFn && integrationConfig && client.apiKey) {
|
|
71
63
|
try {
|
|
72
|
-
await integrationFn(config,
|
|
64
|
+
await integrationFn(config, client.apiKey, integrationConfig);
|
|
73
65
|
} catch (error) {
|
|
74
66
|
console.error(`Integration failed for ${client.clientId}:`, error);
|
|
75
67
|
}
|
package/src/start-daemon.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { logger } from "./utils/logger";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
-
import { LOGS_DIR } from "./utils/config";
|
|
3
|
+
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
|
4
|
+
import { withCrossProcessLock } from "./utils/process-lock";
|
|
5
|
+
|
|
6
|
+
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
|
4
7
|
|
|
5
8
|
function getTodayLogFile(): string {
|
|
6
9
|
const now = new Date();
|
|
@@ -10,27 +13,32 @@ function getTodayLogFile(): string {
|
|
|
10
13
|
return `${LOGS_DIR}/${year}-${month}-${day}.log`;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
async function isDaemonHealthy(port: string): Promise<boolean> {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
19
|
+
try {
|
|
20
|
+
const existing = await fetch(`http://localhost:${port}/health`, {
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
return existing.ok;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
} finally {
|
|
27
|
+
clearTimeout(timeoutId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function startDaemonUnlocked(
|
|
32
|
+
options: { port?: string; provider?: string },
|
|
15
33
|
): Promise<void> {
|
|
16
34
|
const args: string[] = [];
|
|
17
35
|
const port = options.port || "8008";
|
|
18
36
|
const pollIntervalMs = 250;
|
|
19
37
|
const startupTimeoutMs = 10 * 60 * 1000;
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const existing = await fetch(`http://localhost:${port}/health`, {
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
});
|
|
27
|
-
clearTimeout(timeoutId);
|
|
28
|
-
if (existing.ok) {
|
|
29
|
-
logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// Daemon is not running yet; continue with startup.
|
|
39
|
+
if (await isDaemonHealthy(port)) {
|
|
40
|
+
logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
|
41
|
+
return;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
if (options.port) {
|
|
@@ -47,7 +55,7 @@ export async function startDaemon(
|
|
|
47
55
|
|
|
48
56
|
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
|
49
57
|
const todayLogFile = getTodayLogFile();
|
|
50
|
-
const shellCmd = `bun run "${daemonScript}" ${args.map(a => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
58
|
+
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")} >> "${todayLogFile}" 2>&1`;
|
|
51
59
|
|
|
52
60
|
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
|
53
61
|
stdout: "inherit",
|
|
@@ -73,19 +81,9 @@ export async function startDaemon(
|
|
|
73
81
|
);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const res = await fetch(`http://localhost:${port}/health`, {
|
|
80
|
-
signal: controller.signal,
|
|
81
|
-
});
|
|
82
|
-
clearTimeout(timeoutId);
|
|
83
|
-
if (res.ok) {
|
|
84
|
-
logger.log(`Routstr daemon started (PID: ${proc.pid}).`);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// Not ready yet
|
|
84
|
+
if (await isDaemonHealthy(port)) {
|
|
85
|
+
logger.log(`Routstr daemon started (PID: ${proc.pid}).`);
|
|
86
|
+
return;
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
|
|
@@ -93,3 +91,27 @@ export async function startDaemon(
|
|
|
93
91
|
`Daemon failed to start within ${Math.round(startupTimeoutMs / 1000)} seconds. Check logs in ${LOGS_DIR}`,
|
|
94
92
|
);
|
|
95
93
|
}
|
|
94
|
+
|
|
95
|
+
export async function startDaemon(
|
|
96
|
+
options: { port?: string; provider?: string } = {},
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const port = options.port || "8008";
|
|
99
|
+
const startupTimeoutMs = 10 * 60 * 1000;
|
|
100
|
+
|
|
101
|
+
if (await isDaemonHealthy(port)) {
|
|
102
|
+
logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await withCrossProcessLock(
|
|
107
|
+
DAEMON_STARTUP_LOCK_PATH,
|
|
108
|
+
async () => {
|
|
109
|
+
await startDaemonUnlocked(options);
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
acquireTimeoutMs: startupTimeoutMs + 30_000,
|
|
113
|
+
staleAfterMs: startupTimeoutMs + 30_000,
|
|
114
|
+
log: (message) => logger.debug(message),
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
|
-
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
|
|
2
|
+
import { callDaemon, getNpubSuffix, isDaemonRunning, loadConfig } from "../../utils/daemon-client.ts";
|
|
3
3
|
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export interface BalanceKey {
|
|
@@ -82,12 +82,24 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
|
|
82
82
|
const result = await callDaemon(`/usage?limit=${limit}`);
|
|
83
83
|
if (result.error) return null;
|
|
84
84
|
|
|
85
|
-
// The daemon returns { output: [...] } where output is the entries array directly
|
|
85
|
+
// The daemon returns { output: [...] } where output is the entries array directly.
|
|
86
|
+
// In remote daemon mode, client IDs are suffixed with the last 7 chars of our npub.
|
|
87
|
+
// Fetch the full daemon result, but only display/aggregate entries for our clients.
|
|
86
88
|
const entries = result.output as UsageTrackingEntry[] | undefined;
|
|
87
89
|
const entriesArray = Array.isArray(entries) ? entries : [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
90
|
+
const suffix = getNpubSuffix(await loadConfig());
|
|
91
|
+
const suffixStr = suffix ? `-${suffix}` : null;
|
|
92
|
+
const visibleEntries = suffixStr
|
|
93
|
+
? entriesArray
|
|
94
|
+
.filter((entry) => entry.client?.endsWith(suffixStr))
|
|
95
|
+
.map((entry) => ({
|
|
96
|
+
...entry,
|
|
97
|
+
client: entry.client?.slice(0, -suffixStr.length),
|
|
98
|
+
}))
|
|
99
|
+
: entriesArray;
|
|
100
|
+
|
|
101
|
+
// Calculate totals from visible entries
|
|
102
|
+
const totals = visibleEntries.reduce(
|
|
91
103
|
(acc, entry) => ({
|
|
92
104
|
promptTokens: acc.promptTokens + entry.promptTokens,
|
|
93
105
|
completionTokens: acc.completionTokens + entry.completionTokens,
|
|
@@ -98,8 +110,8 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
|
|
|
98
110
|
);
|
|
99
111
|
|
|
100
112
|
return {
|
|
101
|
-
entries:
|
|
102
|
-
totalEntries:
|
|
113
|
+
entries: visibleEntries,
|
|
114
|
+
totalEntries: visibleEntries.length,
|
|
103
115
|
totalSatsCost: totals.satsCost,
|
|
104
116
|
recentSatsCost: totals.satsCost, // For now, recent = total since we don't have time window
|
|
105
117
|
limit,
|