@antseed/cli 0.1.0 → 0.1.1

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 (43) hide show
  1. package/dist/cli/commands/dashboard.js +1 -1
  2. package/dist/cli/commands/dashboard.js.map +1 -1
  3. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  4. package/dist/proxy/buyer-proxy.js +95 -16
  5. package/dist/proxy/buyer-proxy.js.map +1 -1
  6. package/package.json +19 -17
  7. package/.env.example +0 -15
  8. package/src/cli/commands/balance.ts +0 -77
  9. package/src/cli/commands/browse.ts +0 -113
  10. package/src/cli/commands/config.ts +0 -271
  11. package/src/cli/commands/connect.test.ts +0 -69
  12. package/src/cli/commands/connect.ts +0 -342
  13. package/src/cli/commands/dashboard.ts +0 -59
  14. package/src/cli/commands/deposit.ts +0 -61
  15. package/src/cli/commands/dev.ts +0 -107
  16. package/src/cli/commands/init.ts +0 -99
  17. package/src/cli/commands/plugin-create.test.ts +0 -60
  18. package/src/cli/commands/plugin-create.ts +0 -230
  19. package/src/cli/commands/plugin.test.ts +0 -55
  20. package/src/cli/commands/plugin.ts +0 -295
  21. package/src/cli/commands/profile.ts +0 -95
  22. package/src/cli/commands/seed.test.ts +0 -70
  23. package/src/cli/commands/seed.ts +0 -447
  24. package/src/cli/commands/status.ts +0 -73
  25. package/src/cli/commands/types.ts +0 -56
  26. package/src/cli/commands/withdraw.ts +0 -61
  27. package/src/cli/formatters.ts +0 -64
  28. package/src/cli/index.ts +0 -46
  29. package/src/cli/shutdown.ts +0 -38
  30. package/src/config/defaults.ts +0 -49
  31. package/src/config/effective.test.ts +0 -80
  32. package/src/config/effective.ts +0 -119
  33. package/src/config/loader.test.ts +0 -95
  34. package/src/config/loader.ts +0 -251
  35. package/src/config/types.ts +0 -139
  36. package/src/config/validation.ts +0 -78
  37. package/src/env/load-env.ts +0 -20
  38. package/src/plugins/loader.ts +0 -96
  39. package/src/plugins/manager.ts +0 -66
  40. package/src/plugins/registry.ts +0 -45
  41. package/src/proxy/buyer-proxy.ts +0 -604
  42. package/src/status/node-status.ts +0 -105
  43. package/tsconfig.json +0 -9
@@ -1,271 +0,0 @@
1
- import type { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { getGlobalOptions } from './types.js';
4
- import { loadConfig, saveConfig } from '../../config/loader.js';
5
- import type { AntseedConfig, ProviderConfig, ProviderType } from '../../config/types.js';
6
- import { assertValidConfig } from '../../config/validation.js';
7
-
8
- /**
9
- * Register the `antseed config` command and its subcommands.
10
- */
11
- export function registerConfigCommand(program: Command): void {
12
- const configCmd = program
13
- .command('config')
14
- .description('Manage Antseed configuration');
15
-
16
- // antseed config show
17
- configCmd
18
- .command('show')
19
- .description('Display current configuration (credentials redacted)')
20
- .action(async () => {
21
- const globalOpts = getGlobalOptions(program);
22
- const config = await loadConfig(globalOpts.config);
23
- const redacted = redactConfig(config);
24
- console.log(JSON.stringify(redacted, null, 2));
25
- });
26
-
27
- // antseed config set <key> <value>
28
- configCmd
29
- .command('set <key> <value>')
30
- .description('Set a configuration value (e.g., seller.reserveFloor 20)')
31
- .action(async (key: string, value: string) => {
32
- try {
33
- const globalOpts = getGlobalOptions(program);
34
- const config = await loadConfig(globalOpts.config);
35
- const validKeys = getValidConfigKeys(config);
36
- if (!validKeys.includes(key)) {
37
- console.error(chalk.red(`Invalid config key: ${key}`));
38
- console.error(chalk.dim(`Available keys: ${validKeys.join(', ')}`));
39
- process.exitCode = 1;
40
- return;
41
- }
42
- setConfigValue(config as unknown as Record<string, unknown>, key, value);
43
- assertValidConfig(config);
44
- await saveConfig(globalOpts.config, config);
45
- console.log(chalk.green(`Set ${key} = ${value}`));
46
- } catch (err) {
47
- console.error(chalk.red(`Error: ${(err as Error).message}`));
48
- process.exitCode = 1;
49
- }
50
- });
51
-
52
- const sellerCmd = configCmd
53
- .command('seller')
54
- .description('Role-scoped seller configuration commands');
55
-
56
- sellerCmd
57
- .command('show')
58
- .description('Display seller configuration')
59
- .action(async () => {
60
- const globalOpts = getGlobalOptions(program);
61
- const config = await loadConfig(globalOpts.config);
62
- console.log(JSON.stringify(config.seller, null, 2));
63
- });
64
-
65
- sellerCmd
66
- .command('set <key> <value>')
67
- .description('Set seller configuration value (e.g., pricing.defaults.inputUsdPerMillion 12)')
68
- .action(async (key: string, value: string) => {
69
- await setRoleScopedValue(program, 'seller', key, value);
70
- });
71
-
72
- const buyerCmd = configCmd
73
- .command('buyer')
74
- .description('Role-scoped buyer configuration commands');
75
-
76
- buyerCmd
77
- .command('show')
78
- .description('Display buyer configuration')
79
- .action(async () => {
80
- const globalOpts = getGlobalOptions(program);
81
- const config = await loadConfig(globalOpts.config);
82
- console.log(JSON.stringify(config.buyer, null, 2));
83
- });
84
-
85
- buyerCmd
86
- .command('set <key> <value>')
87
- .description('Set buyer configuration value (e.g., preferredProviders [\"anthropic\",\"openai\"])')
88
- .action(async (key: string, value: string) => {
89
- await setRoleScopedValue(program, 'buyer', key, value);
90
- });
91
-
92
- // antseed config add-provider
93
- configCmd
94
- .command('add-provider')
95
- .description('Add a new provider credential')
96
- .requiredOption('-t, --type <type>', 'provider type (anthropic, openai, google, moonshot)')
97
- .requiredOption('-k, --key <key>', 'API key or auth token')
98
- .option('-e, --endpoint <url>', 'custom API endpoint URL')
99
- .action(async (options) => {
100
- const knownTypes = ['anthropic', 'openai', 'google', 'moonshot'];
101
- if (!knownTypes.includes(options.type as string)) {
102
- console.error(chalk.red(`Unknown provider type: ${options.type as string}`));
103
- console.error(chalk.dim(`Known types: ${knownTypes.join(', ')}`));
104
- process.exitCode = 1;
105
- return;
106
- }
107
- const globalOpts = getGlobalOptions(program);
108
- const config = await loadConfig(globalOpts.config);
109
- const provider = buildProviderConfig(
110
- options.type as ProviderType,
111
- options.key as string,
112
- options.endpoint as string | undefined
113
- );
114
- config.providers.push(provider);
115
- await saveConfig(globalOpts.config, config);
116
- console.log(chalk.green(`Added ${options.type as string} provider`));
117
- });
118
-
119
- // antseed config remove-provider <type>
120
- configCmd
121
- .command('remove-provider <type>')
122
- .description('Remove a provider credential by type')
123
- .action(async (type: string) => {
124
- const globalOpts = getGlobalOptions(program);
125
- const config = await loadConfig(globalOpts.config);
126
- const before = config.providers.length;
127
- config.providers = config.providers.filter((p) => p.type !== type);
128
- const removed = before - config.providers.length;
129
- await saveConfig(globalOpts.config, config);
130
- if (removed > 0) {
131
- console.log(chalk.green(`Removed ${removed} ${type} provider(s)`));
132
- } else {
133
- console.log(chalk.yellow(`No ${type} provider found`));
134
- }
135
- });
136
-
137
- // antseed config init
138
- configCmd
139
- .command('init')
140
- .description('Initialize a new config file with defaults')
141
- .action(async () => {
142
- const globalOpts = getGlobalOptions(program);
143
- const { createDefaultConfig } = await import('../../config/defaults.js');
144
- const config = createDefaultConfig();
145
- await saveConfig(globalOpts.config, config);
146
- console.log(chalk.green(`Config initialized at ${globalOpts.config}`));
147
- });
148
- }
149
-
150
- /**
151
- * Redact sensitive fields (auth values) from config for display.
152
- */
153
- export function redactConfig(config: AntseedConfig): Record<string, unknown> {
154
- const clone = JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
155
- const providers = clone['providers'] as Array<Record<string, unknown>> | undefined;
156
- for (const provider of providers ?? []) {
157
- if (provider['authValue']) {
158
- const val = provider['authValue'] as string;
159
- provider['authValue'] = val.slice(0, 8) + '...' + val.slice(-4);
160
- }
161
- }
162
-
163
- return clone;
164
- }
165
-
166
- /**
167
- * Set a nested config value by dot-separated key path.
168
- * @example setConfigValue(config, 'seller.reserveFloor', '20')
169
- */
170
- export function setConfigValue(config: Record<string, unknown>, key: string, value: string): void {
171
- const parts = key.split('.');
172
- let current: Record<string, unknown> = config;
173
- for (let i = 0; i < parts.length - 1; i++) {
174
- const part = parts[i]!;
175
- if (typeof current[part] !== 'object' || current[part] === null) {
176
- throw new Error(`Invalid config key: ${key}`);
177
- }
178
- current = current[part] as Record<string, unknown>;
179
- }
180
- const lastKey = parts[parts.length - 1]!;
181
- const trimmed = value.trim();
182
-
183
- if (
184
- (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
185
- (trimmed.startsWith('[') && trimmed.endsWith(']'))
186
- ) {
187
- try {
188
- current[lastKey] = JSON.parse(trimmed) as unknown;
189
- return;
190
- } catch {
191
- throw new Error(`Invalid JSON value for ${key}`);
192
- }
193
- }
194
-
195
- // Auto-parse numeric scalars
196
- const numVal = Number(trimmed);
197
- current[lastKey] = Number.isNaN(numVal) ? value : numVal;
198
- }
199
-
200
- function getValidConfigKeys(config: AntseedConfig, prefix = ''): string[] {
201
- const keys: string[] = [];
202
- for (const [k, v] of Object.entries(config)) {
203
- if (k === 'providers' || k === 'plugins') continue;
204
- const path = prefix ? `${prefix}.${k}` : k;
205
- if (v && typeof v === 'object' && !Array.isArray(v)) {
206
- keys.push(...getValidConfigKeys(v as unknown as AntseedConfig, path));
207
- } else {
208
- keys.push(path);
209
- }
210
- }
211
- return keys;
212
- }
213
-
214
- async function setRoleScopedValue(
215
- program: Command,
216
- role: 'seller' | 'buyer',
217
- key: string,
218
- value: string,
219
- ): Promise<void> {
220
- try {
221
- const globalOpts = getGlobalOptions(program);
222
- const config = await loadConfig(globalOpts.config);
223
- const fullKey = `${role}.${key}`;
224
- const validKeys = getValidConfigKeys(config);
225
- if (!validKeys.includes(fullKey)) {
226
- console.error(chalk.red(`Invalid ${role} config key: ${key}`));
227
- const scopedKeys = validKeys
228
- .filter((path) => path.startsWith(`${role}.`))
229
- .map((path) => path.slice(role.length + 1));
230
- console.error(chalk.dim(`Available ${role} keys: ${scopedKeys.join(', ')}`));
231
- process.exitCode = 1;
232
- return;
233
- }
234
- setConfigValue(config as unknown as Record<string, unknown>, fullKey, value);
235
- assertValidConfig(config);
236
- await saveConfig(globalOpts.config, config);
237
- console.log(chalk.green(`Set ${fullKey} = ${value}`));
238
- } catch (err) {
239
- console.error(chalk.red(`Error: ${(err as Error).message}`));
240
- process.exitCode = 1;
241
- }
242
- }
243
-
244
- /**
245
- * Build a ProviderConfig with default endpoint and auth header for known providers.
246
- */
247
- export function buildProviderConfig(
248
- type: ProviderType,
249
- authValue: string,
250
- customEndpoint?: string
251
- ): ProviderConfig {
252
- const defaults: Record<string, { endpoint: string; authHeaderName: string }> = {
253
- anthropic: { endpoint: 'https://api.anthropic.com', authHeaderName: 'x-api-key' },
254
- openai: { endpoint: 'https://api.openai.com', authHeaderName: 'Authorization' },
255
- google: { endpoint: 'https://generativelanguage.googleapis.com', authHeaderName: 'x-goog-api-key' },
256
- moonshot: { endpoint: 'https://api.moonshot.cn', authHeaderName: 'Authorization' },
257
- };
258
-
259
- const fallbackDefaults = {
260
- endpoint: customEndpoint ?? '',
261
- authHeaderName: 'Authorization',
262
- };
263
- const def = defaults[type] ?? fallbackDefaults;
264
-
265
- return {
266
- type,
267
- endpoint: customEndpoint ?? def.endpoint,
268
- authHeaderName: def.authHeaderName,
269
- authValue,
270
- };
271
- }
@@ -1,69 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { createDefaultConfig } from '../../config/defaults.js';
4
- import { resolveEffectiveBuyerConfig } from '../../config/effective.js';
5
- import {
6
- buildBuyerRuntimeOverridesFromFlags,
7
- buildBuyerBootstrapEntries,
8
- buildRouterRuntimeEnvFromBuyerConfig,
9
- } from './connect.js';
10
-
11
- test('connect runtime overrides are runtime-only and win over env/config', () => {
12
- const config = createDefaultConfig();
13
- config.buyer.proxyPort = 7777;
14
- config.buyer.maxPricing.defaults.inputUsdPerMillion = 50;
15
- config.buyer.maxPricing.defaults.outputUsdPerMillion = 60;
16
- const beforeResolution = JSON.parse(JSON.stringify(config));
17
-
18
- const env = {
19
- ANTSEED_BUYER_MAX_INPUT_USD_PER_MILLION: '70',
20
- ANTSEED_BUYER_MAX_OUTPUT_USD_PER_MILLION: '80',
21
- } as NodeJS.ProcessEnv;
22
-
23
- const overrides = buildBuyerRuntimeOverridesFromFlags({
24
- port: 9000,
25
- maxInputUsdPerMillion: 90,
26
- maxOutputUsdPerMillion: 95,
27
- });
28
-
29
- const effective = resolveEffectiveBuyerConfig({
30
- config,
31
- env,
32
- buyerOverrides: overrides,
33
- });
34
-
35
- assert.equal(effective.proxyPort, 9000);
36
- assert.equal(effective.maxPricing.defaults.inputUsdPerMillion, 90);
37
- assert.equal(effective.maxPricing.defaults.outputUsdPerMillion, 95);
38
- assert.deepEqual(config, beforeResolution);
39
- });
40
-
41
- test('connect maps effective buyer config into router runtime env keys', () => {
42
- const config = createDefaultConfig();
43
- config.buyer.minPeerReputation = 72;
44
- config.buyer.preferredProviders = ['anthropic', 'openai'];
45
- config.buyer.maxPricing.defaults.inputUsdPerMillion = 21;
46
- config.buyer.maxPricing.defaults.outputUsdPerMillion = 63;
47
-
48
- const runtimeEnv = buildRouterRuntimeEnvFromBuyerConfig(config.buyer);
49
- assert.equal(runtimeEnv['ANTSEED_MIN_REPUTATION'], '72');
50
- assert.equal(runtimeEnv['ANTSEED_PREFERRED_PROVIDERS'], 'anthropic,openai');
51
-
52
- const parsed = JSON.parse(runtimeEnv['ANTSEED_MAX_PRICING_JSON'] ?? '{}') as {
53
- defaults?: { inputUsdPerMillion?: number; outputUsdPerMillion?: number };
54
- };
55
- assert.equal(parsed.defaults?.inputUsdPerMillion, 21);
56
- assert.equal(parsed.defaults?.outputUsdPerMillion, 63);
57
- });
58
-
59
- test('connect bootstrap entries use official nodes when config is empty and include local seeder first', () => {
60
- const entries = buildBuyerBootstrapEntries([], 6881);
61
- assert.equal(entries[0], '127.0.0.1:6881');
62
- assert.ok(entries.length > 1);
63
- });
64
-
65
- test('connect bootstrap entries respect explicit configured nodes', () => {
66
- const entries = buildBuyerBootstrapEntries(['10.0.0.2:6881'], 6889);
67
- assert.equal(entries[0], '127.0.0.1:6889');
68
- assert.deepEqual(entries.slice(1), ['10.0.0.2:6881']);
69
- });
@@ -1,342 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import ora from 'ora'
4
- import { readFile } 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 { AntseedNode, BaseEscrowClient, loadOrCreateIdentity, identityToEvmAddress, getInstance } from '@antseed/node'
10
- import type { NodePaymentsConfig } from '@antseed/node'
11
- import { OFFICIAL_BOOTSTRAP_NODES, parseBootstrapList, toBootstrapConfig } from '@antseed/node/discovery'
12
- import { setupShutdownHandler } from '../shutdown.js'
13
- import { loadRouterPlugin, buildPluginConfig } from '../../plugins/loader.js'
14
- import { BuyerProxy } from '../../proxy/buyer-proxy.js'
15
- import { resolveEffectiveBuyerConfig, type BuyerRuntimeOverrides } from '../../config/effective.js'
16
- import type { BuyerCLIConfig } from '../../config/types.js'
17
-
18
- interface LocalSeederInfo {
19
- dhtPort: number
20
- signalingPort: number
21
- pid: number
22
- }
23
-
24
- export function buildBuyerRuntimeOverridesFromFlags(options: {
25
- port?: number
26
- minPeerReputation?: number
27
- preferredProviders?: string[]
28
- maxInputUsdPerMillion?: number
29
- maxOutputUsdPerMillion?: number
30
- }): BuyerRuntimeOverrides {
31
- const overrides: BuyerRuntimeOverrides = {}
32
- if (options.port !== undefined) {
33
- overrides.proxyPort = options.port
34
- }
35
- if (options.minPeerReputation !== undefined) {
36
- overrides.minPeerReputation = options.minPeerReputation
37
- }
38
- if (options.preferredProviders && options.preferredProviders.length > 0) {
39
- overrides.preferredProviders = options.preferredProviders
40
- }
41
- if (options.maxInputUsdPerMillion !== undefined) {
42
- overrides.maxInputUsdPerMillion = options.maxInputUsdPerMillion
43
- }
44
- if (options.maxOutputUsdPerMillion !== undefined) {
45
- overrides.maxOutputUsdPerMillion = options.maxOutputUsdPerMillion
46
- }
47
- return overrides
48
- }
49
-
50
- export function buildRouterRuntimeEnvFromBuyerConfig(buyerConfig: BuyerCLIConfig): Record<string, string> {
51
- const runtimeEnv: Record<string, string> = {
52
- ANTSEED_MIN_REPUTATION: String(buyerConfig.minPeerReputation),
53
- ANTSEED_MAX_PRICING_JSON: JSON.stringify(buyerConfig.maxPricing),
54
- }
55
- if (buyerConfig.preferredProviders.length > 0) {
56
- runtimeEnv['ANTSEED_PREFERRED_PROVIDERS'] = buyerConfig.preferredProviders.join(',')
57
- }
58
- return runtimeEnv
59
- }
60
-
61
- export function buildBuyerBootstrapEntries(
62
- configuredBootstrapNodes: string[] | undefined,
63
- localSeederDhtPort?: number,
64
- ): string[] {
65
- const configured = Array.isArray(configuredBootstrapNodes)
66
- ? configuredBootstrapNodes.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
67
- : []
68
-
69
- // Respect explicit user bootstrap config. If none is provided, keep default public bootstrap nodes.
70
- const baseEntries = configured.length > 0
71
- ? configured
72
- : OFFICIAL_BOOTSTRAP_NODES.map((node) => `${node.host}:${node.port}`)
73
-
74
- const entries = [...baseEntries]
75
-
76
- if (Number.isFinite(localSeederDhtPort) && (localSeederDhtPort ?? 0) > 0) {
77
- const localBootstrap = `127.0.0.1:${Math.floor(localSeederDhtPort as number)}`
78
- if (!entries.includes(localBootstrap)) {
79
- entries.unshift(localBootstrap)
80
- }
81
- }
82
-
83
- return entries
84
- }
85
-
86
- function parseOptionalBoolEnv(value: string | undefined): boolean | null {
87
- if (value === undefined) return null
88
- const normalized = value.trim().toLowerCase()
89
- if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
90
- if (['0', 'false', 'no', 'off'].includes(normalized)) return false
91
- return null
92
- }
93
-
94
- async function isRpcReachable(rpcUrl: string, timeoutMs = 1500): Promise<boolean> {
95
- const controller = new AbortController()
96
- const timeout = setTimeout(() => controller.abort(), timeoutMs)
97
-
98
- try {
99
- const response = await fetch(rpcUrl, {
100
- method: 'POST',
101
- headers: { 'content-type': 'application/json' },
102
- body: JSON.stringify({
103
- jsonrpc: '2.0',
104
- id: 1,
105
- method: 'eth_chainId',
106
- params: [],
107
- }),
108
- signal: controller.signal,
109
- })
110
-
111
- if (!response.ok) return false
112
-
113
- const payload = await response.json() as { result?: unknown }
114
- return typeof payload.result === 'string' && payload.result.startsWith('0x')
115
- } catch {
116
- return false
117
- } finally {
118
- clearTimeout(timeout)
119
- }
120
- }
121
-
122
- async function getLocalSeederInfo(): Promise<LocalSeederInfo | null> {
123
- try {
124
- const stateFile = join(homedir(), '.antseed', 'daemon.state.json')
125
- const raw = await readFile(stateFile, 'utf-8')
126
- const state = JSON.parse(raw) as { state?: string; dhtPort?: number; signalingPort?: number; pid?: number }
127
- if (state.state === 'seeding' && state.dhtPort && state.pid) {
128
- try {
129
- process.kill(state.pid, 0)
130
- const signalingPort = state.signalingPort ?? state.dhtPort
131
- return { dhtPort: state.dhtPort, signalingPort, pid: state.pid }
132
- } catch {
133
- return null
134
- }
135
- }
136
- } catch {
137
- // No state file or unreadable
138
- }
139
- return null
140
- }
141
-
142
- export function registerConnectCommand(program: Command): void {
143
- program
144
- .command('connect')
145
- .description('Start the buyer proxy and connect to sellers on the P2P network')
146
- .option('-p, --port <number>', 'local proxy port', (v) => parseInt(v, 10))
147
- .option('--router <name>', 'router plugin name or npm package')
148
- .option('--instance <id>', 'use a configured plugin instance by ID')
149
- .option('--max-input-usd-per-million <number>', 'runtime-only max input pricing override in USD per 1M tokens', parseFloat)
150
- .option('--max-output-usd-per-million <number>', 'runtime-only max output pricing override in USD per 1M tokens', parseFloat)
151
- .action(async (options) => {
152
- const globalOpts = getGlobalOptions(program)
153
- const config = await loadConfig(globalOpts.config)
154
- const runtimeOverrides = buildBuyerRuntimeOverridesFromFlags({
155
- port: options.port as number | undefined,
156
- maxInputUsdPerMillion: options.maxInputUsdPerMillion as number | undefined,
157
- maxOutputUsdPerMillion: options.maxOutputUsdPerMillion as number | undefined,
158
- })
159
- const effectiveBuyerConfig = resolveEffectiveBuyerConfig({
160
- config,
161
- buyerOverrides: runtimeOverrides,
162
- })
163
-
164
- let router
165
- let toolHints: Array<{ name: string; envVar: string }> = []
166
-
167
- if (options.instance) {
168
- const configPath = join(homedir(), '.antseed', 'config.json')
169
- const instance = await getInstance(configPath, options.instance)
170
- if (!instance) {
171
- console.error(chalk.red(`Instance "${options.instance}" not found.`))
172
- process.exit(1)
173
- }
174
- if (instance.type !== 'router') {
175
- console.error(chalk.red(`Instance "${options.instance}" is a ${instance.type}, not a router.`))
176
- process.exit(1)
177
- }
178
- const spinner = ora(`Loading router plugin "${instance.package}"...`).start()
179
- try {
180
- const plugin = await loadRouterPlugin(instance.package)
181
- const runtimeEnv = buildRouterRuntimeEnvFromBuyerConfig(effectiveBuyerConfig)
182
- const pluginConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv, instance.config as Record<string, string>)
183
- router = await plugin.createRouter(pluginConfig)
184
- spinner.succeed(chalk.green(`Router "${plugin.displayName}" loaded`))
185
- toolHints = (plugin as any).TOOL_HINTS ?? []
186
- } catch (err) {
187
- spinner.fail(chalk.red(`Failed to load router: ${(err as Error).message}`))
188
- process.exit(1)
189
- }
190
- } else if (options.router) {
191
- const spinner = ora(`Loading router plugin "${options.router}"...`).start()
192
- try {
193
- const plugin = await loadRouterPlugin(options.router)
194
- const runtimeEnv = buildRouterRuntimeEnvFromBuyerConfig(effectiveBuyerConfig)
195
- const pluginConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv)
196
- router = await plugin.createRouter(pluginConfig)
197
- spinner.succeed(chalk.green(`Router "${plugin.displayName}" loaded`))
198
- toolHints = (plugin as any).TOOL_HINTS ?? []
199
- } catch (err) {
200
- spinner.fail(chalk.red(`Failed to load router: ${(err as Error).message}`))
201
- process.exit(1)
202
- }
203
- } else {
204
- console.error(chalk.red('Error: No router specified.'))
205
- console.error(chalk.dim('Run: antseed connect --router <name> or antseed connect --instance <id>'))
206
- process.exit(1)
207
- }
208
-
209
- // Auto-discover local seeder
210
- const seederInfo = await getLocalSeederInfo()
211
- const allBootstrapEntries = buildBuyerBootstrapEntries(
212
- config.network?.bootstrapNodes,
213
- seederInfo?.dhtPort,
214
- )
215
- const bootstrapNodes = toBootstrapConfig(parseBootstrapList(allBootstrapEntries))
216
-
217
- const nodeSpinner = ora('Connecting to P2P network...').start()
218
-
219
- // Build payment config from CLI config if available and reachable
220
- let paymentsConfig: NodePaymentsConfig | undefined
221
- const settlementEnv = parseOptionalBoolEnv(process.env['ANTSEED_ENABLE_SETTLEMENT'])
222
- const crypto = config.payments?.crypto
223
- let settlementEnabled = settlementEnv ?? Boolean(crypto)
224
-
225
- if (settlementEnabled && crypto && settlementEnv !== true) {
226
- const rpcUp = await isRpcReachable(crypto.rpcUrl)
227
- if (!rpcUp) {
228
- settlementEnabled = false
229
- console.log(chalk.yellow(`Payments disabled: RPC node unreachable at ${crypto.rpcUrl}`))
230
- console.log(chalk.dim('Start your chain node or set ANTSEED_ENABLE_SETTLEMENT=true to force-enable payments.'))
231
- }
232
- }
233
-
234
- if (settlementEnabled && crypto) {
235
- paymentsConfig = {
236
- enabled: true,
237
- rpcUrl: crypto.rpcUrl,
238
- contractAddress: crypto.escrowContractAddress,
239
- usdcAddress: crypto.usdcContractAddress,
240
- defaultEscrowAmountUSDC: crypto.defaultLockAmountUSDC
241
- ? String(Math.round(parseFloat(crypto.defaultLockAmountUSDC) * 1_000_000))
242
- : '1000000',
243
- platformFeeRate: config.payments.platformFeeRate,
244
- }
245
- }
246
-
247
- console.log(chalk.bold('Effective buyer settings:'))
248
- console.log(
249
- chalk.dim(
250
- ` preferred providers: ${effectiveBuyerConfig.preferredProviders.length > 0 ? effectiveBuyerConfig.preferredProviders.join(', ') : '(any)'}`
251
- )
252
- )
253
- console.log(
254
- chalk.dim(
255
- ` max pricing defaults (USD/1M): input=${effectiveBuyerConfig.maxPricing.defaults.inputUsdPerMillion}, output=${effectiveBuyerConfig.maxPricing.defaults.outputUsdPerMillion}`
256
- )
257
- )
258
- console.log(chalk.dim(` min peer reputation: ${effectiveBuyerConfig.minPeerReputation}`))
259
- console.log(chalk.dim(` proxy port: ${effectiveBuyerConfig.proxyPort}`))
260
- console.log('')
261
-
262
- const node = new AntseedNode({
263
- role: 'buyer',
264
- bootstrapNodes,
265
- allowPrivateIPs: true,
266
- dataDir: globalOpts.dataDir,
267
- payments: paymentsConfig,
268
- })
269
-
270
- node.setRouter(router)
271
-
272
- try {
273
- await node.start()
274
- nodeSpinner.succeed(chalk.green('Connected to P2P network'))
275
- } catch (err) {
276
- nodeSpinner.fail(chalk.red(`Failed to connect: ${(err as Error).message}`))
277
- process.exit(1)
278
- }
279
-
280
- // Display available USDC balance only if payments are active
281
- if (paymentsConfig?.enabled && config.payments?.crypto) {
282
- try {
283
- const identity = await loadOrCreateIdentity(globalOpts.dataDir)
284
- const address = identityToEvmAddress(identity)
285
- const escrowClient = new BaseEscrowClient({
286
- rpcUrl: config.payments.crypto.rpcUrl,
287
- contractAddress: config.payments.crypto.escrowContractAddress,
288
- usdcAddress: config.payments.crypto.usdcContractAddress,
289
- })
290
- const account = await escrowClient.getBuyerAccount(address)
291
- console.log(chalk.dim(`Wallet: ${address}`))
292
- const availUsdc = Number(account.available) / 1_000_000
293
- console.log(chalk.dim(`Escrow available: ${availUsdc.toFixed(6)} USDC`))
294
- } catch {
295
- // Non-fatal — chain may not be available
296
- console.log(chalk.dim('Payment balance unavailable (chain not reachable)'))
297
- }
298
- }
299
-
300
- // Start the local HTTP proxy
301
- const proxyPort = effectiveBuyerConfig.proxyPort
302
- const proxySpinner = ora(`Starting local proxy on port ${proxyPort}...`).start()
303
-
304
- const proxy = new BuyerProxy({
305
- port: proxyPort,
306
- node,
307
- })
308
-
309
- try {
310
- await proxy.start()
311
- proxySpinner.succeed(chalk.green(`Proxy listening on http://localhost:${proxyPort}`))
312
- } catch (err) {
313
- proxySpinner.fail(chalk.red(`Failed to start proxy: ${(err as Error).message}`))
314
- await node.stop()
315
- process.exit(1)
316
- }
317
-
318
- const proxyUrl = `http://localhost:${proxyPort}`
319
- console.log('')
320
-
321
- if (toolHints.length > 0) {
322
- console.log(chalk.bold('Configure your tools:'))
323
- for (const hint of toolHints) {
324
- console.log(` export ${hint.envVar}=${proxyUrl} # ${hint.name}`)
325
- }
326
- } else {
327
- console.log(chalk.bold('Configure your CLI tools:'))
328
- console.log(` export ANTHROPIC_BASE_URL=${proxyUrl}`)
329
- console.log(` export OPENAI_BASE_URL=${proxyUrl}`)
330
- }
331
- console.log('')
332
- console.log(chalk.dim('Enable debug logs: export ANTSEED_DEBUG=1'))
333
- console.log('')
334
-
335
- setupShutdownHandler(async () => {
336
- nodeSpinner.start('Shutting down...')
337
- await proxy.stop()
338
- await node.stop()
339
- nodeSpinner.succeed('Disconnected. All sessions finalized.')
340
- })
341
- })
342
- }