@antseed/cli 0.1.0
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/.env.example +15 -0
- package/README.md +169 -0
- package/dist/cli/commands/balance.d.ts +3 -0
- package/dist/cli/commands/balance.d.ts.map +1 -0
- package/dist/cli/commands/balance.js +64 -0
- package/dist/cli/commands/balance.js.map +1 -0
- package/dist/cli/commands/browse.d.ts +7 -0
- package/dist/cli/commands/browse.d.ts.map +1 -0
- package/dist/cli/commands/browse.js +100 -0
- package/dist/cli/commands/browse.js.map +1 -0
- package/dist/cli/commands/config.d.ts +20 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +239 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/connect.d.ts +14 -0
- package/dist/cli/commands/connect.d.ts.map +1 -0
- package/dist/cli/commands/connect.js +298 -0
- package/dist/cli/commands/connect.js.map +1 -0
- package/dist/cli/commands/connect.test.d.ts +2 -0
- package/dist/cli/commands/connect.test.d.ts.map +1 -0
- package/dist/cli/commands/connect.test.js +54 -0
- package/dist/cli/commands/connect.test.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +6 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +48 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/deposit.d.ts +3 -0
- package/dist/cli/commands/deposit.d.ts.map +1 -0
- package/dist/cli/commands/deposit.js +48 -0
- package/dist/cli/commands/deposit.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +3 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +94 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +91 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/plugin-create.d.ts +11 -0
- package/dist/cli/commands/plugin-create.d.ts.map +1 -0
- package/dist/cli/commands/plugin-create.js +201 -0
- package/dist/cli/commands/plugin-create.js.map +1 -0
- package/dist/cli/commands/plugin-create.test.d.ts +2 -0
- package/dist/cli/commands/plugin-create.test.d.ts.map +1 -0
- package/dist/cli/commands/plugin-create.test.js +53 -0
- package/dist/cli/commands/plugin-create.test.js.map +1 -0
- package/dist/cli/commands/plugin.d.ts +3 -0
- package/dist/cli/commands/plugin.d.ts.map +1 -0
- package/dist/cli/commands/plugin.js +279 -0
- package/dist/cli/commands/plugin.js.map +1 -0
- package/dist/cli/commands/plugin.test.d.ts +2 -0
- package/dist/cli/commands/plugin.test.d.ts.map +1 -0
- package/dist/cli/commands/plugin.test.js +53 -0
- package/dist/cli/commands/plugin.test.js.map +1 -0
- package/dist/cli/commands/profile.d.ts +10 -0
- package/dist/cli/commands/profile.d.ts.map +1 -0
- package/dist/cli/commands/profile.js +89 -0
- package/dist/cli/commands/profile.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +11 -0
- package/dist/cli/commands/seed.d.ts.map +1 -0
- package/dist/cli/commands/seed.js +397 -0
- package/dist/cli/commands/seed.js.map +1 -0
- package/dist/cli/commands/seed.test.d.ts +2 -0
- package/dist/cli/commands/seed.test.d.ts.map +1 -0
- package/dist/cli/commands/seed.test.js +57 -0
- package/dist/cli/commands/seed.test.js.map +1 -0
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +55 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/types.d.ts +14 -0
- package/dist/cli/commands/types.d.ts.map +1 -0
- package/dist/cli/commands/types.js +41 -0
- package/dist/cli/commands/types.js.map +1 -0
- package/dist/cli/commands/withdraw.d.ts +3 -0
- package/dist/cli/commands/withdraw.d.ts.map +1 -0
- package/dist/cli/commands/withdraw.js +48 -0
- package/dist/cli/commands/withdraw.js.map +1 -0
- package/dist/cli/formatters.d.ts +29 -0
- package/dist/cli/formatters.d.ts.map +1 -0
- package/dist/cli/formatters.js +67 -0
- package/dist/cli/formatters.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/shutdown.d.ts +11 -0
- package/dist/cli/shutdown.d.ts.map +1 -0
- package/dist/cli/shutdown.js +34 -0
- package/dist/cli/shutdown.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +48 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/effective.d.ts +26 -0
- package/dist/config/effective.d.ts.map +1 -0
- package/dist/config/effective.js +84 -0
- package/dist/config/effective.js.map +1 -0
- package/dist/config/effective.test.d.ts +2 -0
- package/dist/config/effective.test.d.ts.map +1 -0
- package/dist/config/effective.test.js +65 -0
- package/dist/config/effective.test.js.map +1 -0
- package/dist/config/loader.d.ts +12 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +212 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/loader.test.d.ts +2 -0
- package/dist/config/loader.test.d.ts.map +1 -0
- package/dist/config/loader.test.js +77 -0
- package/dist/config/loader.test.js.map +1 -0
- package/dist/config/types.d.ts +133 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +10 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +50 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/env/load-env.d.ts +6 -0
- package/dist/env/load-env.d.ts.map +1 -0
- package/dist/env/load-env.js +18 -0
- package/dist/env/load-env.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +70 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/manager.d.ts +11 -0
- package/dist/plugins/manager.d.ts.map +1 -0
- package/dist/plugins/manager.js +52 -0
- package/dist/plugins/manager.js.map +1 -0
- package/dist/plugins/registry.d.ts +8 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +39 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +30 -0
- package/dist/proxy/buyer-proxy.d.ts.map +1 -0
- package/dist/proxy/buyer-proxy.js +488 -0
- package/dist/proxy/buyer-proxy.js.map +1 -0
- package/dist/status/node-status.d.ts +22 -0
- package/dist/status/node-status.d.ts.map +1 -0
- package/dist/status/node-status.js +83 -0
- package/dist/status/node-status.js.map +1 -0
- package/package.json +39 -0
- package/src/cli/commands/balance.ts +77 -0
- package/src/cli/commands/browse.ts +113 -0
- package/src/cli/commands/config.ts +271 -0
- package/src/cli/commands/connect.test.ts +69 -0
- package/src/cli/commands/connect.ts +342 -0
- package/src/cli/commands/dashboard.ts +59 -0
- package/src/cli/commands/deposit.ts +61 -0
- package/src/cli/commands/dev.ts +107 -0
- package/src/cli/commands/init.ts +99 -0
- package/src/cli/commands/plugin-create.test.ts +60 -0
- package/src/cli/commands/plugin-create.ts +230 -0
- package/src/cli/commands/plugin.test.ts +55 -0
- package/src/cli/commands/plugin.ts +295 -0
- package/src/cli/commands/profile.ts +95 -0
- package/src/cli/commands/seed.test.ts +70 -0
- package/src/cli/commands/seed.ts +447 -0
- package/src/cli/commands/status.ts +73 -0
- package/src/cli/commands/types.ts +56 -0
- package/src/cli/commands/withdraw.ts +61 -0
- package/src/cli/formatters.ts +64 -0
- package/src/cli/index.ts +46 -0
- package/src/cli/shutdown.ts +38 -0
- package/src/config/defaults.ts +49 -0
- package/src/config/effective.test.ts +80 -0
- package/src/config/effective.ts +119 -0
- package/src/config/loader.test.ts +95 -0
- package/src/config/loader.ts +251 -0
- package/src/config/types.ts +139 -0
- package/src/config/validation.ts +78 -0
- package/src/env/load-env.ts +20 -0
- package/src/plugins/loader.ts +96 -0
- package/src/plugins/manager.ts +66 -0
- package/src/plugins/registry.ts +45 -0
- package/src/proxy/buyer-proxy.ts +604 -0
- package/src/status/node-status.ts +105 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import type { AntseedNode } from '@antseed/node'
|
|
7
|
+
import type { PeerInfo, SerializedHttpRequest } from '@antseed/node'
|
|
8
|
+
|
|
9
|
+
export interface BuyerProxyConfig {
|
|
10
|
+
port: number
|
|
11
|
+
node: AntseedNode
|
|
12
|
+
/** How long to cache discovered peers before re-querying DHT (ms). Default: 30000 */
|
|
13
|
+
peerCacheTtlMs?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DAEMON_STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json')
|
|
17
|
+
|
|
18
|
+
const DEBUG = () =>
|
|
19
|
+
['1', 'true', 'yes', 'on'].includes((process.env['ANTSEED_DEBUG'] ?? '').trim().toLowerCase())
|
|
20
|
+
|
|
21
|
+
function log(...args: unknown[]): void {
|
|
22
|
+
if (DEBUG()) console.log('[proxy]', ...args)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type TokenUsageSummary = {
|
|
26
|
+
inputTokens: number
|
|
27
|
+
outputTokens: number
|
|
28
|
+
totalTokens: number
|
|
29
|
+
source: 'usage' | 'estimated'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RoutingPricing = {
|
|
33
|
+
provider: string
|
|
34
|
+
model: string | null
|
|
35
|
+
inputUsdPerMillion: number | null
|
|
36
|
+
outputUsdPerMillion: number | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ResponseTelemetry = {
|
|
40
|
+
usage: TokenUsageSummary
|
|
41
|
+
pricing: RoutingPricing
|
|
42
|
+
estimatedCostUsd: number | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTokenCount(value: unknown): number {
|
|
46
|
+
const parsed = Number(value)
|
|
47
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
return Math.floor(parsed)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseUsageObject(value: unknown): { inputTokens: number; outputTokens: number; totalTokens: number } {
|
|
54
|
+
if (!value || typeof value !== 'object') {
|
|
55
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const usage = value as Record<string, unknown>
|
|
59
|
+
const total = parseTokenCount(usage.totalTokens ?? usage.total_tokens ?? usage.total_token_count)
|
|
60
|
+
let input = parseTokenCount(
|
|
61
|
+
usage.inputTokens
|
|
62
|
+
?? usage.input_tokens
|
|
63
|
+
?? usage.promptTokens
|
|
64
|
+
?? usage.prompt_tokens
|
|
65
|
+
?? usage.input_token_count
|
|
66
|
+
?? usage.prompt_token_count
|
|
67
|
+
?? usage.cache_creation_input_tokens
|
|
68
|
+
?? usage.cache_read_input_tokens,
|
|
69
|
+
)
|
|
70
|
+
let output = parseTokenCount(
|
|
71
|
+
usage.outputTokens
|
|
72
|
+
?? usage.output_tokens
|
|
73
|
+
?? usage.completionTokens
|
|
74
|
+
?? usage.completion_tokens
|
|
75
|
+
?? usage.output_token_count
|
|
76
|
+
?? usage.completion_token_count,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (total > 0) {
|
|
80
|
+
if (input === 0 && output === 0) {
|
|
81
|
+
output = total
|
|
82
|
+
} else if (output === 0 && input > 0 && total >= input) {
|
|
83
|
+
output = total - input
|
|
84
|
+
} else if (input === 0 && output > 0 && total >= output) {
|
|
85
|
+
input = total - output
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
inputTokens: input,
|
|
91
|
+
outputTokens: output,
|
|
92
|
+
totalTokens: input + output,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function estimateTokensFromBytes(inputBytes: number, outputBytes: number): TokenUsageSummary {
|
|
97
|
+
const inputTokens = Math.max(1, Math.round(Math.max(0, inputBytes) / 4))
|
|
98
|
+
const outputTokens = Math.max(1, Math.round(Math.max(0, outputBytes) / 4))
|
|
99
|
+
return {
|
|
100
|
+
inputTokens,
|
|
101
|
+
outputTokens,
|
|
102
|
+
totalTokens: inputTokens + outputTokens,
|
|
103
|
+
source: 'estimated',
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseSseUsage(body: Uint8Array): { inputTokens: number; outputTokens: number; totalTokens: number } {
|
|
108
|
+
const text = new TextDecoder().decode(body)
|
|
109
|
+
const lines = text.split('\n')
|
|
110
|
+
let inputTokens = 0
|
|
111
|
+
let outputTokens = 0
|
|
112
|
+
let totalTokens = 0
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const trimmed = line.trim()
|
|
116
|
+
if (!trimmed.startsWith('data:')) continue
|
|
117
|
+
|
|
118
|
+
const payload = trimmed.slice(5).trim()
|
|
119
|
+
if (payload.length === 0 || payload === '[DONE]') continue
|
|
120
|
+
|
|
121
|
+
let parsed: Record<string, unknown>
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(payload) as Record<string, unknown>
|
|
124
|
+
} catch {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const directUsage = parseUsageObject(parsed.usage)
|
|
129
|
+
if (directUsage.totalTokens > 0) {
|
|
130
|
+
inputTokens = Math.max(inputTokens, directUsage.inputTokens)
|
|
131
|
+
outputTokens = Math.max(outputTokens, directUsage.outputTokens)
|
|
132
|
+
totalTokens = Math.max(totalTokens, directUsage.totalTokens)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const message = parsed.message
|
|
136
|
+
const messageUsage = parseUsageObject(message && typeof message === 'object' ? (message as Record<string, unknown>).usage : undefined)
|
|
137
|
+
if (messageUsage.totalTokens > 0) {
|
|
138
|
+
inputTokens = Math.max(inputTokens, messageUsage.inputTokens)
|
|
139
|
+
outputTokens = Math.max(outputTokens, messageUsage.outputTokens)
|
|
140
|
+
totalTokens = Math.max(totalTokens, messageUsage.totalTokens)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (totalTokens <= 0) {
|
|
145
|
+
totalTokens = inputTokens + outputTokens
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { inputTokens, outputTokens, totalTokens }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseJsonUsage(body: Uint8Array): { inputTokens: number; outputTokens: number; totalTokens: number } {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(new TextDecoder().decode(body)) as Record<string, unknown>
|
|
154
|
+
const direct = parseUsageObject(parsed.usage)
|
|
155
|
+
if (direct.totalTokens > 0) {
|
|
156
|
+
return direct
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const message = parsed.message
|
|
160
|
+
if (message && typeof message === 'object') {
|
|
161
|
+
const nested = parseUsageObject((message as Record<string, unknown>).usage)
|
|
162
|
+
if (nested.totalTokens > 0) {
|
|
163
|
+
return nested
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = parsed.result
|
|
168
|
+
if (result && typeof result === 'object') {
|
|
169
|
+
const nested = parseUsageObject((result as Record<string, unknown>).usage)
|
|
170
|
+
if (nested.totalTokens > 0) {
|
|
171
|
+
return nested
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
176
|
+
} catch {
|
|
177
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function pickProviderForPeer(peer: PeerInfo, request: SerializedHttpRequest): string {
|
|
182
|
+
const explicit = request.headers['x-antseed-provider']?.trim()
|
|
183
|
+
if (explicit && explicit.length > 0) {
|
|
184
|
+
return explicit.toLowerCase()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (request.path.startsWith('/v1/messages') && peer.providers.includes('anthropic')) {
|
|
188
|
+
return 'anthropic'
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const first = peer.providers[0]?.trim()
|
|
192
|
+
if (first && first.length > 0) {
|
|
193
|
+
return first.toLowerCase()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return 'unknown'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractRequestedModel(request: SerializedHttpRequest): string | null {
|
|
200
|
+
const contentType = (request.headers['content-type'] ?? request.headers['Content-Type'] ?? '').toLowerCase()
|
|
201
|
+
if (!contentType.includes('application/json')) {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(new TextDecoder().decode(request.body)) as Record<string, unknown>
|
|
207
|
+
const model = parsed.model
|
|
208
|
+
if (typeof model === 'string' && model.trim().length > 0) {
|
|
209
|
+
return model.trim()
|
|
210
|
+
}
|
|
211
|
+
return null
|
|
212
|
+
} catch {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolvePeerPricing(peer: PeerInfo, provider: string, model: string | null): { inputUsdPerMillion: number | null; outputUsdPerMillion: number | null } {
|
|
218
|
+
const providerPricing = peer.providerPricing?.[provider]
|
|
219
|
+
if (providerPricing) {
|
|
220
|
+
if (model && providerPricing.models?.[model]) {
|
|
221
|
+
return {
|
|
222
|
+
inputUsdPerMillion: Number.isFinite(providerPricing.models[model]!.inputUsdPerMillion)
|
|
223
|
+
? providerPricing.models[model]!.inputUsdPerMillion
|
|
224
|
+
: null,
|
|
225
|
+
outputUsdPerMillion: Number.isFinite(providerPricing.models[model]!.outputUsdPerMillion)
|
|
226
|
+
? providerPricing.models[model]!.outputUsdPerMillion
|
|
227
|
+
: null,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
inputUsdPerMillion: Number.isFinite(providerPricing.defaults.inputUsdPerMillion)
|
|
232
|
+
? providerPricing.defaults.inputUsdPerMillion
|
|
233
|
+
: null,
|
|
234
|
+
outputUsdPerMillion: Number.isFinite(providerPricing.defaults.outputUsdPerMillion)
|
|
235
|
+
? providerPricing.defaults.outputUsdPerMillion
|
|
236
|
+
: null,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const input = peer.defaultInputUsdPerMillion
|
|
241
|
+
const output = peer.defaultOutputUsdPerMillion
|
|
242
|
+
return {
|
|
243
|
+
inputUsdPerMillion: Number.isFinite(input) ? input! : null,
|
|
244
|
+
outputUsdPerMillion: Number.isFinite(output) ? output! : null,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function computeResponseTelemetry(
|
|
249
|
+
request: SerializedHttpRequest,
|
|
250
|
+
responseHeaders: Record<string, string>,
|
|
251
|
+
responseBody: Uint8Array,
|
|
252
|
+
selectedPeer: PeerInfo,
|
|
253
|
+
): ResponseTelemetry {
|
|
254
|
+
const provider = pickProviderForPeer(selectedPeer, request)
|
|
255
|
+
const model = extractRequestedModel(request)
|
|
256
|
+
const pricing = resolvePeerPricing(selectedPeer, provider, model)
|
|
257
|
+
const contentType = (responseHeaders['content-type'] ?? '').toLowerCase()
|
|
258
|
+
|
|
259
|
+
const usageFromBody = contentType.includes('text/event-stream')
|
|
260
|
+
? parseSseUsage(responseBody)
|
|
261
|
+
: parseJsonUsage(responseBody)
|
|
262
|
+
|
|
263
|
+
let usage: TokenUsageSummary
|
|
264
|
+
if (usageFromBody.totalTokens > 0) {
|
|
265
|
+
usage = {
|
|
266
|
+
inputTokens: usageFromBody.inputTokens,
|
|
267
|
+
outputTokens: usageFromBody.outputTokens,
|
|
268
|
+
totalTokens: usageFromBody.totalTokens,
|
|
269
|
+
source: 'usage',
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
usage = estimateTokensFromBytes(request.body.length, responseBody.length)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let estimatedCostUsd: number | null = null
|
|
276
|
+
if (
|
|
277
|
+
pricing.inputUsdPerMillion !== null &&
|
|
278
|
+
pricing.outputUsdPerMillion !== null &&
|
|
279
|
+
Number.isFinite(pricing.inputUsdPerMillion) &&
|
|
280
|
+
Number.isFinite(pricing.outputUsdPerMillion)
|
|
281
|
+
) {
|
|
282
|
+
estimatedCostUsd =
|
|
283
|
+
(usage.inputTokens * pricing.inputUsdPerMillion + usage.outputTokens * pricing.outputUsdPerMillion) / 1_000_000
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
usage,
|
|
288
|
+
pricing: {
|
|
289
|
+
provider,
|
|
290
|
+
model,
|
|
291
|
+
inputUsdPerMillion: pricing.inputUsdPerMillion,
|
|
292
|
+
outputUsdPerMillion: pricing.outputUsdPerMillion,
|
|
293
|
+
},
|
|
294
|
+
estimatedCostUsd,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function attachAntseedTelemetryHeaders(
|
|
299
|
+
upstreamHeaders: Record<string, string>,
|
|
300
|
+
selectedPeer: PeerInfo,
|
|
301
|
+
telemetry: ResponseTelemetry,
|
|
302
|
+
requestId: string,
|
|
303
|
+
latencyMs: number,
|
|
304
|
+
): Record<string, string> {
|
|
305
|
+
const headers: Record<string, string> = { ...upstreamHeaders }
|
|
306
|
+
headers['x-antseed-request-id'] = requestId
|
|
307
|
+
headers['x-antseed-latency-ms'] = String(Math.max(0, Math.floor(latencyMs)))
|
|
308
|
+
headers['x-antseed-peer-id'] = selectedPeer.peerId
|
|
309
|
+
if (selectedPeer.publicAddress) {
|
|
310
|
+
headers['x-antseed-peer-address'] = selectedPeer.publicAddress
|
|
311
|
+
}
|
|
312
|
+
if (selectedPeer.providers.length > 0) {
|
|
313
|
+
headers['x-antseed-peer-providers'] = selectedPeer.providers.join(',')
|
|
314
|
+
}
|
|
315
|
+
if (typeof selectedPeer.reputationScore === 'number' && Number.isFinite(selectedPeer.reputationScore)) {
|
|
316
|
+
headers['x-antseed-peer-reputation'] = String(selectedPeer.reputationScore)
|
|
317
|
+
}
|
|
318
|
+
if (typeof selectedPeer.trustScore === 'number' && Number.isFinite(selectedPeer.trustScore)) {
|
|
319
|
+
headers['x-antseed-peer-trust-score'] = String(selectedPeer.trustScore)
|
|
320
|
+
}
|
|
321
|
+
if (typeof selectedPeer.currentLoad === 'number' && Number.isFinite(selectedPeer.currentLoad)) {
|
|
322
|
+
headers['x-antseed-peer-current-load'] = String(selectedPeer.currentLoad)
|
|
323
|
+
}
|
|
324
|
+
if (typeof selectedPeer.maxConcurrency === 'number' && Number.isFinite(selectedPeer.maxConcurrency)) {
|
|
325
|
+
headers['x-antseed-peer-max-concurrency'] = String(selectedPeer.maxConcurrency)
|
|
326
|
+
}
|
|
327
|
+
headers['x-antseed-provider'] = telemetry.pricing.provider
|
|
328
|
+
if (telemetry.pricing.model) {
|
|
329
|
+
headers['x-antseed-model'] = telemetry.pricing.model
|
|
330
|
+
}
|
|
331
|
+
if (telemetry.pricing.inputUsdPerMillion !== null) {
|
|
332
|
+
headers['x-antseed-input-usd-per-million'] = String(telemetry.pricing.inputUsdPerMillion)
|
|
333
|
+
}
|
|
334
|
+
if (telemetry.pricing.outputUsdPerMillion !== null) {
|
|
335
|
+
headers['x-antseed-output-usd-per-million'] = String(telemetry.pricing.outputUsdPerMillion)
|
|
336
|
+
}
|
|
337
|
+
headers['x-antseed-token-source'] = telemetry.usage.source
|
|
338
|
+
headers['x-antseed-input-tokens'] = String(telemetry.usage.inputTokens)
|
|
339
|
+
headers['x-antseed-output-tokens'] = String(telemetry.usage.outputTokens)
|
|
340
|
+
headers['x-antseed-total-tokens'] = String(telemetry.usage.totalTokens)
|
|
341
|
+
if (telemetry.estimatedCostUsd !== null && Number.isFinite(telemetry.estimatedCostUsd)) {
|
|
342
|
+
headers['x-antseed-estimated-cost-usd'] = telemetry.estimatedCostUsd.toFixed(6)
|
|
343
|
+
}
|
|
344
|
+
return headers
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Local HTTP proxy that forwards requests to P2P sellers.
|
|
349
|
+
*
|
|
350
|
+
* Tools like Claude CLI set ANTHROPIC_BASE_URL=http://localhost:8377
|
|
351
|
+
* and the proxy transparently routes their API calls through the
|
|
352
|
+
* Antseed P2P network.
|
|
353
|
+
*/
|
|
354
|
+
export class BuyerProxy {
|
|
355
|
+
private readonly _server: Server
|
|
356
|
+
private readonly _node: AntseedNode
|
|
357
|
+
private readonly _port: number
|
|
358
|
+
private readonly _peerCacheTtlMs: number
|
|
359
|
+
|
|
360
|
+
private _cachedPeers: PeerInfo[] = []
|
|
361
|
+
private _cacheTimestamp = 0
|
|
362
|
+
|
|
363
|
+
constructor(config: BuyerProxyConfig) {
|
|
364
|
+
this._node = config.node
|
|
365
|
+
this._port = config.port
|
|
366
|
+
this._peerCacheTtlMs = config.peerCacheTtlMs ?? 30_000
|
|
367
|
+
this._server = createServer((req, res) => {
|
|
368
|
+
this._handleRequest(req, res).catch((err) => {
|
|
369
|
+
log('Unhandled error:', err)
|
|
370
|
+
if (!res.headersSent) {
|
|
371
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
372
|
+
}
|
|
373
|
+
res.end(`Proxy error: ${err instanceof Error ? err.message : String(err)}`)
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async start(): Promise<void> {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
this._server.once('error', reject)
|
|
381
|
+
this._server.listen(this._port, '127.0.0.1', () => {
|
|
382
|
+
this._server.removeListener('error', reject)
|
|
383
|
+
resolve()
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async stop(): Promise<void> {
|
|
389
|
+
return new Promise((resolve) => {
|
|
390
|
+
this._server.close(() => resolve())
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async _readLocalSeederFallback(): Promise<PeerInfo | null> {
|
|
395
|
+
try {
|
|
396
|
+
const raw = await readFile(DAEMON_STATE_FILE, 'utf-8')
|
|
397
|
+
const parsed = JSON.parse(raw) as {
|
|
398
|
+
state?: unknown
|
|
399
|
+
pid?: unknown
|
|
400
|
+
peerId?: unknown
|
|
401
|
+
signalingPort?: unknown
|
|
402
|
+
provider?: unknown
|
|
403
|
+
defaultInputUsdPerMillion?: unknown
|
|
404
|
+
defaultOutputUsdPerMillion?: unknown
|
|
405
|
+
providerPricing?: unknown
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (parsed.state !== 'seeding') return null
|
|
409
|
+
if (typeof parsed.peerId !== 'string' || !/^[0-9a-f]{64}$/i.test(parsed.peerId)) return null
|
|
410
|
+
|
|
411
|
+
const signalingPort = Number(parsed.signalingPort)
|
|
412
|
+
if (!Number.isFinite(signalingPort) || signalingPort <= 0 || signalingPort > 65535) return null
|
|
413
|
+
|
|
414
|
+
const pid = Number(parsed.pid)
|
|
415
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
416
|
+
try {
|
|
417
|
+
process.kill(Math.floor(pid), 0)
|
|
418
|
+
} catch {
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const providers = typeof parsed.provider === 'string' && parsed.provider.trim().length > 0
|
|
424
|
+
? [parsed.provider.trim()]
|
|
425
|
+
: []
|
|
426
|
+
const defaultInputUsdPerMillion = Number(parsed.defaultInputUsdPerMillion)
|
|
427
|
+
const defaultOutputUsdPerMillion = Number(parsed.defaultOutputUsdPerMillion)
|
|
428
|
+
const providerPricing = parsed.providerPricing && typeof parsed.providerPricing === 'object'
|
|
429
|
+
? (parsed.providerPricing as PeerInfo['providerPricing'])
|
|
430
|
+
: undefined
|
|
431
|
+
|
|
432
|
+
const peerId = parsed.peerId.toLowerCase()
|
|
433
|
+
if (this._node.peerId && this._node.peerId.toLowerCase() === peerId) {
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
peerId: peerId as PeerInfo['peerId'],
|
|
439
|
+
lastSeen: Date.now(),
|
|
440
|
+
publicAddress: `127.0.0.1:${Math.floor(signalingPort)}`,
|
|
441
|
+
providers,
|
|
442
|
+
defaultInputUsdPerMillion: Number.isFinite(defaultInputUsdPerMillion) ? defaultInputUsdPerMillion : 0,
|
|
443
|
+
defaultOutputUsdPerMillion: Number.isFinite(defaultOutputUsdPerMillion) ? defaultOutputUsdPerMillion : 0,
|
|
444
|
+
...(providerPricing ? { providerPricing } : {}),
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private async _getPeers(): Promise<PeerInfo[]> {
|
|
452
|
+
const now = Date.now()
|
|
453
|
+
if (this._cachedPeers.length > 0 && now - this._cacheTimestamp < this._peerCacheTtlMs) {
|
|
454
|
+
return this._cachedPeers
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
log('Discovering peers via DHT...')
|
|
458
|
+
let peers = await this._node.discoverPeers()
|
|
459
|
+
const localSeeder = await this._readLocalSeederFallback()
|
|
460
|
+
if (localSeeder && !peers.some((peer) => peer.peerId === localSeeder.peerId)) {
|
|
461
|
+
peers = [localSeeder, ...peers]
|
|
462
|
+
log(`Added local seeder fallback ${localSeeder.peerId.slice(0, 12)}... @ ${localSeeder.publicAddress}`)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (peers.length > 0) {
|
|
466
|
+
this._cachedPeers = peers
|
|
467
|
+
this._cacheTimestamp = now
|
|
468
|
+
log(`Found ${peers.length} peer(s)`)
|
|
469
|
+
}
|
|
470
|
+
return peers
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private _formatPeerSelectionDiagnostics(peers: PeerInfo[]): string {
|
|
474
|
+
if (peers.length === 0) {
|
|
475
|
+
return 'No peers discovered.'
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const summarize = (peer: PeerInfo): string => {
|
|
479
|
+
const providers = peer.providers
|
|
480
|
+
.map((provider) => provider.trim())
|
|
481
|
+
.filter((provider) => provider.length > 0)
|
|
482
|
+
const trust = Number.isFinite(peer.trustScore) ? String(peer.trustScore) : 'n/a'
|
|
483
|
+
const rep = Number.isFinite(peer.reputationScore) ? String(peer.reputationScore) : 'n/a'
|
|
484
|
+
const onChain = Number.isFinite(peer.onChainReputation) ? String(peer.onChainReputation) : 'n/a'
|
|
485
|
+
const input = Number.isFinite(peer.defaultInputUsdPerMillion) ? String(peer.defaultInputUsdPerMillion) : 'n/a'
|
|
486
|
+
const output = Number.isFinite(peer.defaultOutputUsdPerMillion) ? String(peer.defaultOutputUsdPerMillion) : 'n/a'
|
|
487
|
+
|
|
488
|
+
return `${peer.peerId.slice(0, 8)} providers=[${providers.join(',') || 'none'}] trust=${trust} rep=${rep} onchain=${onChain} in=${input} out=${output}`
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const samples = peers.slice(0, 5).map((peer) => summarize(peer)).join(' | ')
|
|
492
|
+
const suffix = peers.length > 5 ? ` (+${peers.length - 5} more)` : ''
|
|
493
|
+
return `Discovered ${peers.length} peer(s): ${samples}${suffix}`
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private async _handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
497
|
+
const method = req.method ?? 'GET'
|
|
498
|
+
const path = req.url ?? '/'
|
|
499
|
+
|
|
500
|
+
log(`${method} ${path}`)
|
|
501
|
+
|
|
502
|
+
// Collect request body
|
|
503
|
+
const chunks: Buffer[] = []
|
|
504
|
+
for await (const chunk of req) {
|
|
505
|
+
chunks.push(chunk as Buffer)
|
|
506
|
+
}
|
|
507
|
+
const body = Buffer.concat(chunks)
|
|
508
|
+
|
|
509
|
+
// Build serialized request
|
|
510
|
+
const headers: Record<string, string> = {}
|
|
511
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
512
|
+
if (typeof value === 'string') {
|
|
513
|
+
headers[key] = value
|
|
514
|
+
} else if (Array.isArray(value)) {
|
|
515
|
+
headers[key] = value.join(', ')
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Remove host header (points to localhost, not the seller)
|
|
519
|
+
delete headers['host']
|
|
520
|
+
|
|
521
|
+
const serializedReq: SerializedHttpRequest = {
|
|
522
|
+
requestId: randomUUID(),
|
|
523
|
+
method,
|
|
524
|
+
path,
|
|
525
|
+
headers,
|
|
526
|
+
body: new Uint8Array(body),
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Discover peers
|
|
530
|
+
const peers = await this._getPeers()
|
|
531
|
+
if (peers.length === 0) {
|
|
532
|
+
log('No sellers available')
|
|
533
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
534
|
+
res.end('No sellers available on the network. Is a seeder running?')
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Use router to select peer
|
|
539
|
+
const router = this._node.router
|
|
540
|
+
const selectedPeer = router
|
|
541
|
+
? router.selectPeer(serializedReq, peers)
|
|
542
|
+
: peers[0] ?? null
|
|
543
|
+
|
|
544
|
+
if (!selectedPeer) {
|
|
545
|
+
const diagnostics = this._formatPeerSelectionDiagnostics(peers)
|
|
546
|
+
log('Router could not select a peer.', diagnostics)
|
|
547
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
548
|
+
res.end(`Router could not select a suitable peer. ${diagnostics}`)
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
log(`Routing to peer ${selectedPeer.peerId.slice(0, 12)}...`)
|
|
553
|
+
|
|
554
|
+
// Forward through P2P
|
|
555
|
+
const startTime = Date.now()
|
|
556
|
+
try {
|
|
557
|
+
const response = await this._node.sendRequest(selectedPeer, serializedReq)
|
|
558
|
+
const latencyMs = Date.now() - startTime
|
|
559
|
+
|
|
560
|
+
log(`Response: ${response.statusCode} (${latencyMs}ms, ${response.body.length} bytes)`)
|
|
561
|
+
|
|
562
|
+
const telemetry = computeResponseTelemetry(serializedReq, response.headers, response.body, selectedPeer)
|
|
563
|
+
const responseHeaders = attachAntseedTelemetryHeaders(
|
|
564
|
+
response.headers,
|
|
565
|
+
selectedPeer,
|
|
566
|
+
telemetry,
|
|
567
|
+
serializedReq.requestId,
|
|
568
|
+
latencyMs,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
// Report result to router for learning
|
|
572
|
+
if (router) {
|
|
573
|
+
router.onResult(selectedPeer, {
|
|
574
|
+
success: response.statusCode < 500,
|
|
575
|
+
latencyMs,
|
|
576
|
+
tokens: telemetry.usage.totalTokens,
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Forward response headers and body to the HTTP client
|
|
581
|
+
res.writeHead(response.statusCode, responseHeaders)
|
|
582
|
+
res.end(Buffer.from(response.body))
|
|
583
|
+
} catch (err) {
|
|
584
|
+
const latencyMs = Date.now() - startTime
|
|
585
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
586
|
+
log(`Request failed after ${latencyMs}ms: ${message}`)
|
|
587
|
+
|
|
588
|
+
if (router) {
|
|
589
|
+
router.onResult(selectedPeer, {
|
|
590
|
+
success: false,
|
|
591
|
+
latencyMs,
|
|
592
|
+
tokens: 0,
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Invalidate peer cache on connection errors so next request re-discovers
|
|
597
|
+
this._cachedPeers = []
|
|
598
|
+
this._cacheTimestamp = 0
|
|
599
|
+
|
|
600
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
601
|
+
res.end(`P2P request failed: ${message}`)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import type { AntseedConfig } from '../config/types.js';
|
|
5
|
+
|
|
6
|
+
export interface NodeStatus {
|
|
7
|
+
state: 'seeding' | 'connected' | 'idle';
|
|
8
|
+
peerCount: number;
|
|
9
|
+
earningsToday: string;
|
|
10
|
+
tokensToday: number;
|
|
11
|
+
activeSessions: number;
|
|
12
|
+
uptime: string;
|
|
13
|
+
walletAddress: string | null;
|
|
14
|
+
proxyPort: number | null;
|
|
15
|
+
capacityUsedPercent: number;
|
|
16
|
+
daemonPid: number | null;
|
|
17
|
+
daemonAlive: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Stale threshold: 30 seconds */
|
|
21
|
+
const STALE_THRESHOLD_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a process with the given PID is alive.
|
|
25
|
+
* Uses `process.kill(pid, 0)` which checks existence without sending a signal.
|
|
26
|
+
*/
|
|
27
|
+
function isProcessAlive(pid: number): boolean {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Query the current node status.
|
|
38
|
+
* Reads from the running daemon's state file at ~/.antseed/daemon.state.json.
|
|
39
|
+
* Uses PID-based liveness check first, falls back to 30s stale threshold.
|
|
40
|
+
* Returns idle state with zeroed metrics if no daemon is running or the state file is stale.
|
|
41
|
+
*/
|
|
42
|
+
export async function getNodeStatus(config: AntseedConfig): Promise<NodeStatus> {
|
|
43
|
+
const stateFilePath = join(homedir(), '.antseed', 'daemon.state.json');
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(stateFilePath, 'utf-8');
|
|
47
|
+
const state = JSON.parse(raw) as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
// PID-based liveness check
|
|
50
|
+
const pid = typeof state.pid === 'number' ? state.pid : null;
|
|
51
|
+
const alive = pid !== null && isProcessAlive(pid);
|
|
52
|
+
|
|
53
|
+
// If PID is present and process is dead, return idle immediately
|
|
54
|
+
if (pid !== null && !alive) {
|
|
55
|
+
return idleStatus(config, pid);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback: if no PID in state file, use stale threshold
|
|
59
|
+
if (pid === null) {
|
|
60
|
+
const fileStat = await stat(stateFilePath);
|
|
61
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
62
|
+
if (ageMs > STALE_THRESHOLD_MS) {
|
|
63
|
+
return idleStatus(config, null);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const validStates = ['seeding', 'connected', 'idle'] as const;
|
|
68
|
+
const rawState = typeof state.state === 'string' && validStates.includes(state.state as NodeStatus['state'])
|
|
69
|
+
? (state.state as NodeStatus['state'])
|
|
70
|
+
: 'idle';
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
state: rawState,
|
|
74
|
+
peerCount: typeof state.peerCount === 'number' ? state.peerCount : 0,
|
|
75
|
+
earningsToday: typeof state.earningsToday === 'string' ? state.earningsToday : '0',
|
|
76
|
+
tokensToday: typeof state.tokensToday === 'number' ? state.tokensToday : 0,
|
|
77
|
+
activeSessions: typeof state.activeSessions === 'number' ? state.activeSessions : 0,
|
|
78
|
+
uptime: typeof state.uptime === 'string' ? state.uptime : '0s',
|
|
79
|
+
walletAddress: typeof state.walletAddress === 'string' ? state.walletAddress : (config.identity.walletAddress ?? null),
|
|
80
|
+
proxyPort: typeof state.proxyPort === 'number' ? state.proxyPort : null,
|
|
81
|
+
capacityUsedPercent: typeof state.capacityUsedPercent === 'number' ? state.capacityUsedPercent : 0,
|
|
82
|
+
daemonPid: pid,
|
|
83
|
+
daemonAlive: alive,
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
// State file doesn't exist or is unreadable — daemon is not running
|
|
87
|
+
return idleStatus(config, null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function idleStatus(config: AntseedConfig, pid: number | null): NodeStatus {
|
|
92
|
+
return {
|
|
93
|
+
state: 'idle',
|
|
94
|
+
peerCount: 0,
|
|
95
|
+
earningsToday: '0',
|
|
96
|
+
tokensToday: 0,
|
|
97
|
+
activeSessions: 0,
|
|
98
|
+
uptime: '0s',
|
|
99
|
+
walletAddress: config.identity.walletAddress ?? null,
|
|
100
|
+
proxyPort: null,
|
|
101
|
+
capacityUsedPercent: 0,
|
|
102
|
+
daemonPid: pid,
|
|
103
|
+
daemonAlive: false,
|
|
104
|
+
};
|
|
105
|
+
}
|