@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.
Files changed (179) hide show
  1. package/.env.example +15 -0
  2. package/README.md +169 -0
  3. package/dist/cli/commands/balance.d.ts +3 -0
  4. package/dist/cli/commands/balance.d.ts.map +1 -0
  5. package/dist/cli/commands/balance.js +64 -0
  6. package/dist/cli/commands/balance.js.map +1 -0
  7. package/dist/cli/commands/browse.d.ts +7 -0
  8. package/dist/cli/commands/browse.d.ts.map +1 -0
  9. package/dist/cli/commands/browse.js +100 -0
  10. package/dist/cli/commands/browse.js.map +1 -0
  11. package/dist/cli/commands/config.d.ts +20 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +239 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/connect.d.ts +14 -0
  16. package/dist/cli/commands/connect.d.ts.map +1 -0
  17. package/dist/cli/commands/connect.js +298 -0
  18. package/dist/cli/commands/connect.js.map +1 -0
  19. package/dist/cli/commands/connect.test.d.ts +2 -0
  20. package/dist/cli/commands/connect.test.d.ts.map +1 -0
  21. package/dist/cli/commands/connect.test.js +54 -0
  22. package/dist/cli/commands/connect.test.js.map +1 -0
  23. package/dist/cli/commands/dashboard.d.ts +6 -0
  24. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  25. package/dist/cli/commands/dashboard.js +48 -0
  26. package/dist/cli/commands/dashboard.js.map +1 -0
  27. package/dist/cli/commands/deposit.d.ts +3 -0
  28. package/dist/cli/commands/deposit.d.ts.map +1 -0
  29. package/dist/cli/commands/deposit.js +48 -0
  30. package/dist/cli/commands/deposit.js.map +1 -0
  31. package/dist/cli/commands/dev.d.ts +3 -0
  32. package/dist/cli/commands/dev.d.ts.map +1 -0
  33. package/dist/cli/commands/dev.js +94 -0
  34. package/dist/cli/commands/dev.js.map +1 -0
  35. package/dist/cli/commands/init.d.ts +3 -0
  36. package/dist/cli/commands/init.d.ts.map +1 -0
  37. package/dist/cli/commands/init.js +91 -0
  38. package/dist/cli/commands/init.js.map +1 -0
  39. package/dist/cli/commands/plugin-create.d.ts +11 -0
  40. package/dist/cli/commands/plugin-create.d.ts.map +1 -0
  41. package/dist/cli/commands/plugin-create.js +201 -0
  42. package/dist/cli/commands/plugin-create.js.map +1 -0
  43. package/dist/cli/commands/plugin-create.test.d.ts +2 -0
  44. package/dist/cli/commands/plugin-create.test.d.ts.map +1 -0
  45. package/dist/cli/commands/plugin-create.test.js +53 -0
  46. package/dist/cli/commands/plugin-create.test.js.map +1 -0
  47. package/dist/cli/commands/plugin.d.ts +3 -0
  48. package/dist/cli/commands/plugin.d.ts.map +1 -0
  49. package/dist/cli/commands/plugin.js +279 -0
  50. package/dist/cli/commands/plugin.js.map +1 -0
  51. package/dist/cli/commands/plugin.test.d.ts +2 -0
  52. package/dist/cli/commands/plugin.test.d.ts.map +1 -0
  53. package/dist/cli/commands/plugin.test.js +53 -0
  54. package/dist/cli/commands/plugin.test.js.map +1 -0
  55. package/dist/cli/commands/profile.d.ts +10 -0
  56. package/dist/cli/commands/profile.d.ts.map +1 -0
  57. package/dist/cli/commands/profile.js +89 -0
  58. package/dist/cli/commands/profile.js.map +1 -0
  59. package/dist/cli/commands/seed.d.ts +11 -0
  60. package/dist/cli/commands/seed.d.ts.map +1 -0
  61. package/dist/cli/commands/seed.js +397 -0
  62. package/dist/cli/commands/seed.js.map +1 -0
  63. package/dist/cli/commands/seed.test.d.ts +2 -0
  64. package/dist/cli/commands/seed.test.d.ts.map +1 -0
  65. package/dist/cli/commands/seed.test.js +57 -0
  66. package/dist/cli/commands/seed.test.js.map +1 -0
  67. package/dist/cli/commands/status.d.ts +8 -0
  68. package/dist/cli/commands/status.d.ts.map +1 -0
  69. package/dist/cli/commands/status.js +55 -0
  70. package/dist/cli/commands/status.js.map +1 -0
  71. package/dist/cli/commands/types.d.ts +14 -0
  72. package/dist/cli/commands/types.d.ts.map +1 -0
  73. package/dist/cli/commands/types.js +41 -0
  74. package/dist/cli/commands/types.js.map +1 -0
  75. package/dist/cli/commands/withdraw.d.ts +3 -0
  76. package/dist/cli/commands/withdraw.d.ts.map +1 -0
  77. package/dist/cli/commands/withdraw.js +48 -0
  78. package/dist/cli/commands/withdraw.js.map +1 -0
  79. package/dist/cli/formatters.d.ts +29 -0
  80. package/dist/cli/formatters.d.ts.map +1 -0
  81. package/dist/cli/formatters.js +67 -0
  82. package/dist/cli/formatters.js.map +1 -0
  83. package/dist/cli/index.d.ts +3 -0
  84. package/dist/cli/index.d.ts.map +1 -0
  85. package/dist/cli/index.js +41 -0
  86. package/dist/cli/index.js.map +1 -0
  87. package/dist/cli/shutdown.d.ts +11 -0
  88. package/dist/cli/shutdown.d.ts.map +1 -0
  89. package/dist/cli/shutdown.js +34 -0
  90. package/dist/cli/shutdown.js.map +1 -0
  91. package/dist/config/defaults.d.ts +6 -0
  92. package/dist/config/defaults.d.ts.map +1 -0
  93. package/dist/config/defaults.js +48 -0
  94. package/dist/config/defaults.js.map +1 -0
  95. package/dist/config/effective.d.ts +26 -0
  96. package/dist/config/effective.d.ts.map +1 -0
  97. package/dist/config/effective.js +84 -0
  98. package/dist/config/effective.js.map +1 -0
  99. package/dist/config/effective.test.d.ts +2 -0
  100. package/dist/config/effective.test.d.ts.map +1 -0
  101. package/dist/config/effective.test.js +65 -0
  102. package/dist/config/effective.test.js.map +1 -0
  103. package/dist/config/loader.d.ts +12 -0
  104. package/dist/config/loader.d.ts.map +1 -0
  105. package/dist/config/loader.js +212 -0
  106. package/dist/config/loader.js.map +1 -0
  107. package/dist/config/loader.test.d.ts +2 -0
  108. package/dist/config/loader.test.d.ts.map +1 -0
  109. package/dist/config/loader.test.js +77 -0
  110. package/dist/config/loader.test.js.map +1 -0
  111. package/dist/config/types.d.ts +133 -0
  112. package/dist/config/types.d.ts.map +1 -0
  113. package/dist/config/types.js +2 -0
  114. package/dist/config/types.js.map +1 -0
  115. package/dist/config/validation.d.ts +10 -0
  116. package/dist/config/validation.d.ts.map +1 -0
  117. package/dist/config/validation.js +50 -0
  118. package/dist/config/validation.js.map +1 -0
  119. package/dist/env/load-env.d.ts +6 -0
  120. package/dist/env/load-env.d.ts.map +1 -0
  121. package/dist/env/load-env.js +18 -0
  122. package/dist/env/load-env.js.map +1 -0
  123. package/dist/plugins/loader.d.ts +7 -0
  124. package/dist/plugins/loader.d.ts.map +1 -0
  125. package/dist/plugins/loader.js +70 -0
  126. package/dist/plugins/loader.js.map +1 -0
  127. package/dist/plugins/manager.d.ts +11 -0
  128. package/dist/plugins/manager.d.ts.map +1 -0
  129. package/dist/plugins/manager.js +52 -0
  130. package/dist/plugins/manager.js.map +1 -0
  131. package/dist/plugins/registry.d.ts +8 -0
  132. package/dist/plugins/registry.d.ts.map +1 -0
  133. package/dist/plugins/registry.js +39 -0
  134. package/dist/plugins/registry.js.map +1 -0
  135. package/dist/proxy/buyer-proxy.d.ts +30 -0
  136. package/dist/proxy/buyer-proxy.d.ts.map +1 -0
  137. package/dist/proxy/buyer-proxy.js +488 -0
  138. package/dist/proxy/buyer-proxy.js.map +1 -0
  139. package/dist/status/node-status.d.ts +22 -0
  140. package/dist/status/node-status.d.ts.map +1 -0
  141. package/dist/status/node-status.js +83 -0
  142. package/dist/status/node-status.js.map +1 -0
  143. package/package.json +39 -0
  144. package/src/cli/commands/balance.ts +77 -0
  145. package/src/cli/commands/browse.ts +113 -0
  146. package/src/cli/commands/config.ts +271 -0
  147. package/src/cli/commands/connect.test.ts +69 -0
  148. package/src/cli/commands/connect.ts +342 -0
  149. package/src/cli/commands/dashboard.ts +59 -0
  150. package/src/cli/commands/deposit.ts +61 -0
  151. package/src/cli/commands/dev.ts +107 -0
  152. package/src/cli/commands/init.ts +99 -0
  153. package/src/cli/commands/plugin-create.test.ts +60 -0
  154. package/src/cli/commands/plugin-create.ts +230 -0
  155. package/src/cli/commands/plugin.test.ts +55 -0
  156. package/src/cli/commands/plugin.ts +295 -0
  157. package/src/cli/commands/profile.ts +95 -0
  158. package/src/cli/commands/seed.test.ts +70 -0
  159. package/src/cli/commands/seed.ts +447 -0
  160. package/src/cli/commands/status.ts +73 -0
  161. package/src/cli/commands/types.ts +56 -0
  162. package/src/cli/commands/withdraw.ts +61 -0
  163. package/src/cli/formatters.ts +64 -0
  164. package/src/cli/index.ts +46 -0
  165. package/src/cli/shutdown.ts +38 -0
  166. package/src/config/defaults.ts +49 -0
  167. package/src/config/effective.test.ts +80 -0
  168. package/src/config/effective.ts +119 -0
  169. package/src/config/loader.test.ts +95 -0
  170. package/src/config/loader.ts +251 -0
  171. package/src/config/types.ts +139 -0
  172. package/src/config/validation.ts +78 -0
  173. package/src/env/load-env.ts +20 -0
  174. package/src/plugins/loader.ts +96 -0
  175. package/src/plugins/manager.ts +66 -0
  176. package/src/plugins/registry.ts +45 -0
  177. package/src/proxy/buyer-proxy.ts +604 -0
  178. package/src/status/node-status.ts +105 -0
  179. package/tsconfig.json +9 -0
@@ -0,0 +1,447 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { writeFile, unlink } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+ import { homedir } from 'node:os'
7
+ import { getGlobalOptions } from './types.js'
8
+ import { loadConfig } from '../../config/loader.js'
9
+ import type { CLIProviderConfig } from '../../config/types.js'
10
+ import { AntseedNode, type Provider, getInstance } from '@antseed/node'
11
+ import type { PaymentConfig } from '@antseed/node/payments'
12
+ import { parseBootstrapList, toBootstrapConfig } from '@antseed/node/discovery'
13
+ import { setupShutdownHandler } from '../shutdown.js'
14
+ import { loadProviderPlugin, buildPluginConfig } from '../../plugins/loader.js'
15
+ import { resolveEffectiveSellerConfig, type SellerRuntimeOverrides } from '../../config/effective.js'
16
+ import type { SellerCLIConfig } from '../../config/types.js'
17
+
18
+ const STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json')
19
+
20
+ /** Map config file provider entry to env-style key/value pairs for the plugin. */
21
+ function providerConfigToEnv(p: CLIProviderConfig): Record<string, string> {
22
+ const env: Record<string, string> = {}
23
+ if (p.authValue) env['ANTHROPIC_API_KEY'] = p.authValue
24
+ if (p.authType) env['ANTSEED_AUTH_TYPE'] = p.authType
25
+ return env
26
+ }
27
+
28
+ function parseOptionalBoolEnv(value: string | undefined): boolean | null {
29
+ if (value === undefined) return null
30
+ const normalized = value.trim().toLowerCase()
31
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
32
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
33
+ return null
34
+ }
35
+
36
+ async function isRpcReachable(rpcUrl: string, timeoutMs = 1500): Promise<boolean> {
37
+ const controller = new AbortController()
38
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
39
+
40
+ try {
41
+ const response = await fetch(rpcUrl, {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json' },
44
+ body: JSON.stringify({
45
+ jsonrpc: '2.0',
46
+ id: 1,
47
+ method: 'eth_chainId',
48
+ params: [],
49
+ }),
50
+ signal: controller.signal,
51
+ })
52
+
53
+ if (!response.ok) return false
54
+
55
+ const payload = await response.json() as { result?: unknown }
56
+ return typeof payload.result === 'string' && payload.result.startsWith('0x')
57
+ } catch {
58
+ return false
59
+ } finally {
60
+ clearTimeout(timeout)
61
+ }
62
+ }
63
+
64
+ function toUSDCBaseUnits(value: string | undefined, fallbackBaseUnits: string): string {
65
+ if (value === undefined) return fallbackBaseUnits
66
+ const parsed = Number.parseFloat(value.trim())
67
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallbackBaseUnits
68
+ return String(Math.round(parsed * 1_000_000))
69
+ }
70
+
71
+ export function buildSellerRuntimeOverridesFromFlags(options: {
72
+ reserve?: number
73
+ inputUsdPerMillion?: number
74
+ outputUsdPerMillion?: number
75
+ }): SellerRuntimeOverrides {
76
+ const overrides: SellerRuntimeOverrides = {}
77
+ if (options.reserve !== undefined) {
78
+ overrides.reserveFloor = options.reserve
79
+ }
80
+ if (options.inputUsdPerMillion !== undefined) {
81
+ overrides.inputUsdPerMillion = options.inputUsdPerMillion
82
+ }
83
+ if (options.outputUsdPerMillion !== undefined) {
84
+ overrides.outputUsdPerMillion = options.outputUsdPerMillion
85
+ }
86
+ return overrides
87
+ }
88
+
89
+ export function buildSellerPluginRuntimeEnv(
90
+ sellerConfig: SellerCLIConfig,
91
+ providerName: string,
92
+ ): Record<string, string> {
93
+ const providerPricing = sellerConfig.pricing.providers?.[providerName]
94
+ const effectiveDefaults = providerPricing?.defaults ?? sellerConfig.pricing.defaults
95
+ const modelPricing = providerPricing?.models
96
+ const runtimeEnv: Record<string, string> = {
97
+ ANTSEED_INPUT_USD_PER_MILLION: String(effectiveDefaults.inputUsdPerMillion),
98
+ ANTSEED_OUTPUT_USD_PER_MILLION: String(effectiveDefaults.outputUsdPerMillion),
99
+ ANTSEED_MAX_CONCURRENCY: String(sellerConfig.maxConcurrentBuyers),
100
+ }
101
+ if (modelPricing && Object.keys(modelPricing).length > 0) {
102
+ runtimeEnv['ANTSEED_MODEL_PRICING_JSON'] = JSON.stringify(modelPricing)
103
+ }
104
+ return runtimeEnv
105
+ }
106
+
107
+ export function registerSeedCommand(program: Command): void {
108
+ program
109
+ .command('seed')
110
+ .description('Start seeding your idle LLM capacity to the P2P network')
111
+ .option('--provider <name>', 'provider plugin name (e.g., anthropic)')
112
+ .option('--instance <id>', 'use a configured plugin instance by ID')
113
+ .option('-r, --reserve <number>', 'runtime-only reserve floor override (does not write config file)', parseFloat)
114
+ .option('--input-usd-per-million <number>', 'runtime-only input pricing override in USD per 1M tokens', parseFloat)
115
+ .option('--output-usd-per-million <number>', 'runtime-only output pricing override in USD per 1M tokens', parseFloat)
116
+ .action(async (options) => {
117
+ const globalOpts = getGlobalOptions(program)
118
+ const config = await loadConfig(globalOpts.config)
119
+ const runtimeOverrides = buildSellerRuntimeOverridesFromFlags({
120
+ reserve: options.reserve as number | undefined,
121
+ inputUsdPerMillion: options.inputUsdPerMillion as number | undefined,
122
+ outputUsdPerMillion: options.outputUsdPerMillion as number | undefined,
123
+ })
124
+ const effectiveSellerConfig = resolveEffectiveSellerConfig({
125
+ config,
126
+ sellerOverrides: runtimeOverrides,
127
+ })
128
+
129
+ let provider: Provider
130
+
131
+ if (options.instance) {
132
+ const configPath = join(homedir(), '.antseed', 'config.json')
133
+ const instance = await getInstance(configPath, options.instance)
134
+ if (!instance) {
135
+ console.error(chalk.red(`Instance "${options.instance}" not found.`))
136
+ process.exit(1)
137
+ }
138
+ if (instance.type !== 'provider') {
139
+ console.error(chalk.red(`Instance "${options.instance}" is a ${instance.type}, not a provider.`))
140
+ process.exit(1)
141
+ }
142
+ const spinner = ora(`Loading provider plugin "${instance.package}"...`).start()
143
+ try {
144
+ const plugin = await loadProviderPlugin(instance.package)
145
+ const runtimeEnv = buildSellerPluginRuntimeEnv(effectiveSellerConfig, plugin.name)
146
+ const pluginConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv, instance.config as Record<string, string>)
147
+ provider = await plugin.createProvider(pluginConfig)
148
+ if (provider.init) {
149
+ spinner.text = 'Validating credentials...'
150
+ await provider.init()
151
+ }
152
+ spinner.succeed(chalk.green(`Provider "${plugin.displayName}" loaded`))
153
+ } catch (err) {
154
+ spinner.fail(chalk.red(`Failed to load provider: ${(err as Error).message}`))
155
+ process.exit(1)
156
+ }
157
+ } else if (options.provider) {
158
+ const spinner = ora(`Loading provider plugin "${options.provider}"...`).start()
159
+ try {
160
+ const plugin = await loadProviderPlugin(options.provider)
161
+ const configProvider = config.providers.find(p => p.type === options.provider)
162
+ const fileConfig = configProvider ? providerConfigToEnv(configProvider) : {}
163
+ const runtimeEnv = buildSellerPluginRuntimeEnv(effectiveSellerConfig, options.provider as string)
164
+ const envAndRuntimeConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv)
165
+ const pluginConfig = { ...fileConfig, ...envAndRuntimeConfig }
166
+ provider = await plugin.createProvider(pluginConfig)
167
+ if (provider.init) {
168
+ spinner.text = 'Validating credentials...'
169
+ await provider.init()
170
+ }
171
+ spinner.succeed(chalk.green(`Provider "${plugin.displayName}" loaded`))
172
+ } catch (err) {
173
+ spinner.fail(chalk.red(`Failed to load provider: ${(err as Error).message}`))
174
+ process.exit(1)
175
+ }
176
+ } else {
177
+ console.error(chalk.red('Error: No provider specified.'))
178
+ console.error(chalk.dim('Run: antseed seed --provider <name> or antseed seed --instance <id>'))
179
+ process.exit(1)
180
+ }
181
+
182
+ const bootstrapNodes = config.network.bootstrapNodes.length > 0
183
+ ? toBootstrapConfig(parseBootstrapList(config.network.bootstrapNodes))
184
+ : undefined
185
+
186
+ const preferredMethod = config.payments.preferredMethod
187
+ const defaultEscrowAmountUSDC = process.env['ANTSEED_DEFAULT_ESCROW_USDC'] ?? config.payments.crypto?.defaultLockAmountUSDC ?? '1'
188
+ const defaultEscrowAmountUSDCBaseUnits = toUSDCBaseUnits(defaultEscrowAmountUSDC, '1000000')
189
+ const settlementIdleMsRaw = process.env['ANTSEED_SETTLEMENT_IDLE_MS']
190
+ const settlementIdleMs = settlementIdleMsRaw ? parseInt(settlementIdleMsRaw, 10) : 30_000
191
+ const sellerWalletAddress = process.env['ANTSEED_SELLER_WALLET_ADDRESS']
192
+
193
+ let paymentConfig: PaymentConfig | null = null
194
+ if (preferredMethod === 'crypto' && config.payments.crypto) {
195
+ const defaultLockAmountUSDCBaseUnits = toUSDCBaseUnits(
196
+ config.payments.crypto.defaultLockAmountUSDC ?? defaultEscrowAmountUSDC,
197
+ defaultEscrowAmountUSDCBaseUnits,
198
+ )
199
+ const cryptoConfig: NonNullable<PaymentConfig['crypto']> = {
200
+ chainId: config.payments.crypto.chainId,
201
+ rpcUrl: config.payments.crypto.rpcUrl,
202
+ contractAddress: config.payments.crypto.escrowContractAddress,
203
+ usdcAddress: config.payments.crypto.usdcContractAddress,
204
+ defaultLockAmountUSDC: defaultLockAmountUSDCBaseUnits,
205
+ }
206
+
207
+ paymentConfig = {
208
+ crypto: cryptoConfig,
209
+ }
210
+ }
211
+
212
+ const settlementEnv = parseOptionalBoolEnv(process.env['ANTSEED_ENABLE_SETTLEMENT'])
213
+ let paymentsEnabled = settlementEnv ?? paymentConfig !== null
214
+ const cryptoRpcUrl = paymentConfig?.crypto?.rpcUrl
215
+
216
+ if (paymentsEnabled && cryptoRpcUrl && settlementEnv !== true) {
217
+ const rpcUp = await isRpcReachable(cryptoRpcUrl)
218
+ if (!rpcUp) {
219
+ paymentsEnabled = false
220
+ console.log(chalk.yellow(`Payments disabled: RPC node unreachable at ${cryptoRpcUrl}`))
221
+ console.log(chalk.dim('Start your chain node or set ANTSEED_ENABLE_SETTLEMENT=true to force-enable payments.'))
222
+ }
223
+ }
224
+
225
+ const providerName = options.provider as string | undefined ?? provider.name ?? 'unknown'
226
+ const runtimeProviderPricing = buildSellerPluginRuntimeEnv(effectiveSellerConfig, providerName)
227
+ const runtimeInputUsdPerMillion = Number.parseFloat(runtimeProviderPricing['ANTSEED_INPUT_USD_PER_MILLION'] ?? '')
228
+ const runtimeOutputUsdPerMillion = Number.parseFloat(runtimeProviderPricing['ANTSEED_OUTPUT_USD_PER_MILLION'] ?? '')
229
+ let runtimeModelPricing: Record<string, { inputUsdPerMillion: number; outputUsdPerMillion: number }> | undefined
230
+ if (runtimeProviderPricing['ANTSEED_MODEL_PRICING_JSON']) {
231
+ try {
232
+ const parsed = JSON.parse(runtimeProviderPricing['ANTSEED_MODEL_PRICING_JSON']) as unknown
233
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
234
+ const out: Record<string, { inputUsdPerMillion: number; outputUsdPerMillion: number }> = {}
235
+ for (const [model, pricing] of Object.entries(parsed as Record<string, unknown>)) {
236
+ if (!pricing || typeof pricing !== 'object' || Array.isArray(pricing)) continue
237
+ const input = Number((pricing as Record<string, unknown>)['inputUsdPerMillion'])
238
+ const output = Number((pricing as Record<string, unknown>)['outputUsdPerMillion'])
239
+ if (Number.isFinite(input) && Number.isFinite(output)) {
240
+ out[model] = { inputUsdPerMillion: input, outputUsdPerMillion: output }
241
+ }
242
+ }
243
+ if (Object.keys(out).length > 0) {
244
+ runtimeModelPricing = out
245
+ }
246
+ }
247
+ } catch {
248
+ // Ignore malformed model pricing; fallback defaults are still emitted.
249
+ }
250
+ }
251
+ console.log(chalk.bold('Effective seller settings:'))
252
+ console.log(chalk.dim(` provider: ${providerName}`))
253
+ console.log(
254
+ chalk.dim(
255
+ ` pricing defaults (USD/1M): input=${runtimeProviderPricing['ANTSEED_INPUT_USD_PER_MILLION']}, output=${runtimeProviderPricing['ANTSEED_OUTPUT_USD_PER_MILLION']}`
256
+ )
257
+ )
258
+ console.log(
259
+ chalk.dim(
260
+ ` enabled providers: ${effectiveSellerConfig.enabledProviders.length > 0 ? effectiveSellerConfig.enabledProviders.join(', ') : '(none)'}`
261
+ )
262
+ )
263
+ console.log(chalk.dim(` reserve floor: ${effectiveSellerConfig.reserveFloor}`))
264
+ console.log(chalk.dim(` max concurrent buyers: ${effectiveSellerConfig.maxConcurrentBuyers}`))
265
+ console.log('')
266
+
267
+ const nodeSpinner = ora('Starting seeding daemon...').start()
268
+
269
+ const node = new AntseedNode({
270
+ role: 'seller',
271
+ bootstrapNodes,
272
+ dataDir: globalOpts.dataDir,
273
+ payments: {
274
+ enabled: paymentsEnabled,
275
+ paymentMethod: preferredMethod,
276
+ platformFeeRate: config.payments.platformFeeRate,
277
+ settlementIdleMs: Number.isFinite(settlementIdleMs) ? settlementIdleMs : 30_000,
278
+ defaultEscrowAmountUSDC: defaultEscrowAmountUSDCBaseUnits,
279
+ sellerWalletAddress,
280
+ paymentConfig,
281
+ },
282
+ })
283
+
284
+ node.registerProvider(provider)
285
+
286
+ try {
287
+ await node.start()
288
+ nodeSpinner.succeed(chalk.green('Seeding active'))
289
+ console.log(chalk.dim(` Peer ID: ${node.peerId ?? 'unknown'}`))
290
+ console.log(chalk.dim(` DHT port: ${node.dhtPort}`))
291
+ console.log(chalk.dim(` Signaling port: ${node.signalingPort}`))
292
+ } catch (err) {
293
+ nodeSpinner.fail(chalk.red(`Failed to start seeding: ${(err as Error).message}`))
294
+ process.exit(1)
295
+ }
296
+
297
+ // Write daemon state so dashboard and connect can discover this seeder
298
+ const startedAt = Date.now()
299
+ const syntheticSessionStarts = new Map<string, number>()
300
+ let stateWriteInFlight = false
301
+ let stateWritePending = false
302
+
303
+ function formatUptime(): string {
304
+ const ms = Date.now() - startedAt
305
+ const s = Math.floor(ms / 1000)
306
+ if (s < 60) return `${s}s`
307
+ const m = Math.floor(s / 60)
308
+ if (m < 60) return `${m}m ${s % 60}s`
309
+ const h = Math.floor(m / 60)
310
+ return `${h}h ${m % 60}m`
311
+ }
312
+
313
+ function buildDaemonState() {
314
+ const now = Date.now()
315
+ const cap = provider.getCapacity()
316
+ const trackedSessions = node
317
+ .getActiveSellerSessions()
318
+ .filter((session) => !session.settling)
319
+ .map((session) => ({
320
+ sessionId: session.sessionId,
321
+ buyerPeerId: session.buyerPeerId,
322
+ provider: session.provider,
323
+ startedAt: session.startedAt,
324
+ lastActivityAt: session.lastActivityAt,
325
+ totalRequests: session.totalRequests,
326
+ totalTokens: session.totalTokens,
327
+ avgLatencyMs: session.avgLatencyMs,
328
+ }))
329
+
330
+ const syntheticCount = Math.max(0, cap.current - trackedSessions.length)
331
+ const syntheticIds = new Set<string>()
332
+ const syntheticDetails: Array<{
333
+ sessionId: string
334
+ buyerPeerId: string
335
+ provider: string
336
+ startedAt: number
337
+ lastActivityAt: number
338
+ totalRequests: number
339
+ totalTokens: number
340
+ avgLatencyMs: number
341
+ }> = []
342
+
343
+ for (let i = 0; i < syntheticCount; i += 1) {
344
+ const sessionId = `provider-slot-${i + 1}`
345
+ syntheticIds.add(sessionId)
346
+
347
+ const existingStart = syntheticSessionStarts.get(sessionId)
348
+ const startedAtTs = typeof existingStart === 'number' ? existingStart : now
349
+ syntheticSessionStarts.set(sessionId, startedAtTs)
350
+
351
+ syntheticDetails.push({
352
+ sessionId,
353
+ buyerPeerId: 'unknown',
354
+ provider: providerName,
355
+ startedAt: startedAtTs,
356
+ lastActivityAt: now,
357
+ totalRequests: 0,
358
+ totalTokens: 0,
359
+ avgLatencyMs: 0,
360
+ })
361
+ }
362
+
363
+ for (const existingId of syntheticSessionStarts.keys()) {
364
+ if (!syntheticIds.has(existingId)) {
365
+ syntheticSessionStarts.delete(existingId)
366
+ }
367
+ }
368
+
369
+ const activeSessionDetails = [...trackedSessions, ...syntheticDetails]
370
+ const activeSessionsCount = Math.max(node.getActiveSellerSessionCount(), cap.current)
371
+
372
+ return {
373
+ state: 'seeding',
374
+ pid: process.pid,
375
+ peerId: node.peerId,
376
+ dhtPort: node.dhtPort,
377
+ signalingPort: node.signalingPort,
378
+ provider: providerName,
379
+ defaultInputUsdPerMillion: Number.isFinite(runtimeInputUsdPerMillion) ? runtimeInputUsdPerMillion : undefined,
380
+ defaultOutputUsdPerMillion: Number.isFinite(runtimeOutputUsdPerMillion) ? runtimeOutputUsdPerMillion : undefined,
381
+ providerPricing: {
382
+ [providerName]: {
383
+ defaults: {
384
+ inputUsdPerMillion: Number.isFinite(runtimeInputUsdPerMillion) ? runtimeInputUsdPerMillion : 0,
385
+ outputUsdPerMillion: Number.isFinite(runtimeOutputUsdPerMillion) ? runtimeOutputUsdPerMillion : 0,
386
+ },
387
+ ...(runtimeModelPricing ? { models: runtimeModelPricing } : {}),
388
+ },
389
+ },
390
+ startedAt,
391
+ // Fields the dashboard reads
392
+ peerCount: 0,
393
+ activeSessions: activeSessionsCount,
394
+ activeSessionDetails,
395
+ capacityUsedPercent: cap.max > 0 ? Math.round((cap.current / cap.max) * 100) : 0,
396
+ earningsToday: '0',
397
+ tokensToday: 0,
398
+ uptime: formatUptime(),
399
+ updatedAt: now,
400
+ proxyPort: null,
401
+ }
402
+ }
403
+
404
+ async function writeDaemonState(): Promise<void> {
405
+ if (stateWriteInFlight) {
406
+ stateWritePending = true
407
+ return
408
+ }
409
+
410
+ stateWriteInFlight = true
411
+ try {
412
+ await writeFile(STATE_FILE, JSON.stringify(buildDaemonState(), null, 2))
413
+ } finally {
414
+ stateWriteInFlight = false
415
+ if (stateWritePending) {
416
+ stateWritePending = false
417
+ await writeDaemonState()
418
+ }
419
+ }
420
+ }
421
+
422
+ function scheduleDaemonStateWrite(): void {
423
+ void writeDaemonState().catch(() => {})
424
+ }
425
+
426
+ await writeDaemonState()
427
+ node.on('connection', scheduleDaemonStateWrite)
428
+ node.on('session:updated', scheduleDaemonStateWrite)
429
+ node.on('session:finalized', scheduleDaemonStateWrite)
430
+
431
+ // Refresh state file every 1s so the desktop dashboard reflects live counters.
432
+ const stateInterval = setInterval(async () => {
433
+ await writeDaemonState().catch(() => {})
434
+ }, 1_000)
435
+
436
+ setupShutdownHandler(async () => {
437
+ clearInterval(stateInterval)
438
+ node.off('connection', scheduleDaemonStateWrite)
439
+ node.off('session:updated', scheduleDaemonStateWrite)
440
+ node.off('session:finalized', scheduleDaemonStateWrite)
441
+ nodeSpinner.start('Shutting down seeding daemon...')
442
+ await node.stop()
443
+ await unlink(STATE_FILE).catch(() => {})
444
+ nodeSpinner.succeed('Seeding daemon stopped. Sessions finalized.')
445
+ })
446
+ })
447
+ }
@@ -0,0 +1,73 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import Table from 'cli-table3';
4
+ import { getGlobalOptions } from './types.js';
5
+ import { loadConfig } from '../../config/loader.js';
6
+ import { resolveEffectiveSellerConfig } from '../../config/effective.js';
7
+ import { getNodeStatus } from '../../status/node-status.js';
8
+ import type { NodeStatus } from '../../status/node-status.js';
9
+ import { formatEarnings, formatTokens } from '../formatters.js';
10
+
11
+ /** Possible node states */
12
+ export type NodeState = 'seeding' | 'connected' | 'idle';
13
+
14
+ /**
15
+ * Register the `antseed status` command on the Commander program.
16
+ */
17
+ export function registerStatusCommand(program: Command): void {
18
+ program
19
+ .command('status')
20
+ .description('Show current Antseed node status')
21
+ .option('--json', 'output as JSON', false)
22
+ .action(async (options) => {
23
+ try {
24
+ const globalOpts = getGlobalOptions(program);
25
+ const config = await loadConfig(globalOpts.config);
26
+ const effectiveSeller = resolveEffectiveSellerConfig({ config });
27
+ const status: NodeStatus = await getNodeStatus(config);
28
+
29
+ if (options.json) {
30
+ console.log(JSON.stringify(status, null, 2));
31
+ return;
32
+ }
33
+
34
+ // State badge
35
+ const stateColors: Record<NodeState, (s: string) => string> = {
36
+ seeding: chalk.green,
37
+ connected: chalk.cyan,
38
+ idle: chalk.gray,
39
+ };
40
+ const colorFn = stateColors[status.state] ?? chalk.white;
41
+ console.log(chalk.bold('Status: ') + colorFn(status.state.toUpperCase()));
42
+ console.log('');
43
+
44
+ // Summary table
45
+ const table = new Table({
46
+ head: [chalk.bold('Metric'), chalk.bold('Value')],
47
+ colWidths: [25, 35],
48
+ });
49
+
50
+ table.push(
51
+ ['Peers connected', chalk.cyan(String(status.peerCount))],
52
+ ['Earnings today', chalk.green(formatEarnings(status.earningsToday))],
53
+ ['Tokens today', formatTokens(status.tokensToday)],
54
+ ['Active sessions', String(status.activeSessions)],
55
+ ['Uptime', status.uptime],
56
+ ['Wallet address', status.walletAddress ?? chalk.dim('not configured')],
57
+ ['Proxy port', status.proxyPort ? String(status.proxyPort) : chalk.dim('n/a')],
58
+ );
59
+
60
+ if (status.state === 'seeding') {
61
+ table.push([
62
+ 'Seller pricing defaults (USD/1M in/out)',
63
+ `${effectiveSeller.pricing.defaults.inputUsdPerMillion} / ${effectiveSeller.pricing.defaults.outputUsdPerMillion}`,
64
+ ]);
65
+ }
66
+
67
+ console.log(table.toString());
68
+ } catch (err) {
69
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
70
+ process.exitCode = 1;
71
+ }
72
+ });
73
+ }
@@ -0,0 +1,56 @@
1
+ import type { Command } from 'commander';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ /** All command registration functions follow this signature */
6
+ export type RegisterCommandFn = (program: Command) => void;
7
+
8
+ /** Global CLI options available to all commands */
9
+ export interface GlobalOptions {
10
+ config: string;
11
+ dataDir: string;
12
+ verbose: boolean;
13
+ }
14
+
15
+ const NEW_HOME_DIR = join(homedir(), '.antseed');
16
+ const NEW_CONFIG_PATH = join(NEW_HOME_DIR, 'config.json');
17
+
18
+ function resolvePathWithHome(pathLike: string, fallbackAbsolutePath: string): string {
19
+ const raw = typeof pathLike === 'string' ? pathLike.trim() : '';
20
+ if (raw.length === 0) {
21
+ return fallbackAbsolutePath;
22
+ }
23
+ if (raw === '~') {
24
+ return homedir();
25
+ }
26
+ if (raw.startsWith('~/')) {
27
+ return join(homedir(), raw.slice(2));
28
+ }
29
+ return resolve(raw);
30
+ }
31
+
32
+ function isTruthyEnv(value: string | undefined): boolean {
33
+ if (!value) return false;
34
+ const normalized = value.trim().toLowerCase();
35
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
36
+ }
37
+
38
+ /**
39
+ * Extract global options from the Commander program instance.
40
+ */
41
+ export function getGlobalOptions(program: Command): GlobalOptions {
42
+ const opts = program.opts();
43
+ const verbose = opts['verbose'] ?? false;
44
+ if (verbose || isTruthyEnv(process.env['ANTSEED_DEBUG'])) {
45
+ process.env['ANTSEED_DEBUG'] = '1';
46
+ }
47
+
48
+ const requestedConfig = resolvePathWithHome(opts['config'] ?? '~/.antseed/config.json', NEW_CONFIG_PATH);
49
+ const requestedDataDir = resolvePathWithHome(opts['dataDir'] ?? '~/.antseed', NEW_HOME_DIR);
50
+
51
+ return {
52
+ config: requestedConfig,
53
+ dataDir: requestedDataDir,
54
+ verbose,
55
+ };
56
+ }
@@ -0,0 +1,61 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getGlobalOptions } from './types.js';
5
+ import { loadConfig } from '../../config/loader.js';
6
+ import {
7
+ loadOrCreateIdentity,
8
+ BaseEscrowClient,
9
+ identityToEvmWallet,
10
+ identityToEvmAddress,
11
+ } from '@antseed/node';
12
+
13
+ export function registerWithdrawCommand(program: Command): void {
14
+ program
15
+ .command('withdraw <amount>')
16
+ .description('Withdraw USDC from the escrow contract (amount in human-readable USDC, e.g. "5" = 5 USDC)')
17
+ .action(async (amount: string) => {
18
+ const globalOpts = getGlobalOptions(program);
19
+ const config = await loadConfig(globalOpts.config);
20
+
21
+ const payments = config.payments;
22
+ if (!payments?.crypto) {
23
+ console.error(chalk.red('Error: No crypto payment configuration found.'));
24
+ console.error(chalk.dim('Configure payments.crypto in your config file or run: antseed init'));
25
+ process.exit(1);
26
+ }
27
+
28
+ const amountFloat = parseFloat(amount);
29
+ if (isNaN(amountFloat) || amountFloat <= 0) {
30
+ console.error(chalk.red('Error: Amount must be a positive number.'));
31
+ process.exit(1);
32
+ }
33
+
34
+ // Convert human-readable USDC to base units (6 decimals)
35
+ const amountBaseUnits = BigInt(Math.round(amountFloat * 1_000_000));
36
+
37
+ const identity = await loadOrCreateIdentity(globalOpts.dataDir);
38
+ const wallet = identityToEvmWallet(identity);
39
+ const address = identityToEvmAddress(identity);
40
+
41
+ const escrowClient = new BaseEscrowClient({
42
+ rpcUrl: payments.crypto.rpcUrl,
43
+ contractAddress: payments.crypto.escrowContractAddress,
44
+ usdcAddress: payments.crypto.usdcContractAddress,
45
+ });
46
+
47
+ console.log(chalk.dim(`Wallet: ${address}`));
48
+ console.log(chalk.dim(`Amount: ${amountFloat} USDC (${amountBaseUnits} base units)`));
49
+
50
+ const spinner = ora('Withdrawing USDC from escrow...').start();
51
+
52
+ try {
53
+ const txHash = await escrowClient.withdraw(wallet, amountBaseUnits);
54
+ spinner.succeed(chalk.green(`Withdrew ${amountFloat} USDC from escrow`));
55
+ console.log(chalk.dim(`Transaction: ${txHash}`));
56
+ } catch (err) {
57
+ spinner.fail(chalk.red(`Withdrawal failed: ${(err as Error).message}`));
58
+ process.exit(1);
59
+ }
60
+ });
61
+ }