nebula-treasury 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/README.md +39 -0
- package/bin/nebula +11 -0
- package/package.json +65 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_unlock.ts +66 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1293 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +49 -0
- package/src/commands/gateway-run.ts +42 -0
- package/src/commands/gateway-start.ts +216 -0
- package/src/commands/gateway-status.ts +90 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/identity.ts +178 -0
- package/src/commands/init/cost.ts +40 -0
- package/src/commands/init/funding-gate.ts +64 -0
- package/src/commands/init/model-picker.ts +25 -0
- package/src/commands/init/operator-picker.ts +233 -0
- package/src/commands/init/telegram-step.ts +245 -0
- package/src/commands/init/wizard-state.ts +94 -0
- package/src/commands/init.ts +439 -0
- package/src/commands/logs.ts +37 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +65 -0
- package/src/commands/pairing-clear.ts +39 -0
- package/src/commands/pairing-list.ts +55 -0
- package/src/commands/pairing-revoke.ts +49 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/status.ts +44 -0
- package/src/commands/telegram-remove.ts +62 -0
- package/src/commands/telegram-setup.ts +64 -0
- package/src/commands/telegram-status.ts +87 -0
- package/src/commands/telegram.ts +44 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.ts +99 -0
- package/src/index.ts +153 -0
- package/src/ui/app.tsx +673 -0
- package/src/ui/approval-summary.ts +32 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.ts +181 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.ts +125 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.ts +218 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nebula gateway <sub>` argv dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Subs:
|
|
5
|
+
* run — foreground daemon (blocks; Ctrl+C to stop). Uses operator-session.
|
|
6
|
+
* start — interactive: prompt Touch ID, write operator-session, fork daemon.
|
|
7
|
+
* stop — SIGTERM the running daemon via the lock file's PID.
|
|
8
|
+
* restart — stop + start.
|
|
9
|
+
* status — show PID, uptime, socket path, lock state.
|
|
10
|
+
* logs — tail the gateway log (--tail N, --follow).
|
|
11
|
+
*
|
|
12
|
+
* v0.19.x scope: run + start + stop + status. restart/logs/install/setup ship
|
|
13
|
+
* in v0.19.3 alongside launchd plist generator.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type GatewaySub = 'run' | 'start' | 'stop' | 'restart' | 'status' | 'logs'
|
|
17
|
+
|
|
18
|
+
export interface ParsedGatewayArgs {
|
|
19
|
+
sub: GatewaySub
|
|
20
|
+
/** Optional --agent <id> override; defaults to config-derived. */
|
|
21
|
+
agentId?: string
|
|
22
|
+
/** --tail N for logs; default 100. */
|
|
23
|
+
tail?: number
|
|
24
|
+
/** --follow / -f for logs. */
|
|
25
|
+
follow?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ParseResult = ParsedGatewayArgs | { error: string }
|
|
29
|
+
|
|
30
|
+
export function parseGatewayArgs(argv: string[]): ParseResult {
|
|
31
|
+
const sub = argv[0]
|
|
32
|
+
if (!sub) {
|
|
33
|
+
return { error: 'usage: nebula gateway <run | start | stop | restart | status | logs>' }
|
|
34
|
+
}
|
|
35
|
+
if (!['run', 'start', 'stop', 'restart', 'status', 'logs'].includes(sub)) {
|
|
36
|
+
return { error: `unknown gateway sub: ${sub}` }
|
|
37
|
+
}
|
|
38
|
+
let agentId: string | undefined
|
|
39
|
+
let tail: number | undefined
|
|
40
|
+
let follow = false
|
|
41
|
+
for (let i = 1; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i]
|
|
43
|
+
if (a === '--agent') {
|
|
44
|
+
const v = argv[++i]
|
|
45
|
+
if (!v) return { error: '--agent requires a value' }
|
|
46
|
+
agentId = v
|
|
47
|
+
} else if (a === '--tail') {
|
|
48
|
+
const v = argv[++i]
|
|
49
|
+
if (!v) return { error: '--tail requires a value' }
|
|
50
|
+
const n = Number.parseInt(v, 10)
|
|
51
|
+
if (!Number.isFinite(n) || n < 0) return { error: '--tail must be a positive integer' }
|
|
52
|
+
tail = n
|
|
53
|
+
} else if (a === '--follow' || a === '-f') {
|
|
54
|
+
follow = true
|
|
55
|
+
} else {
|
|
56
|
+
return { error: `unknown flag: ${a}` }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { sub: sub as GatewaySub, agentId, tail, follow }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runGateway(parsed: ParsedGatewayArgs): Promise<void> {
|
|
63
|
+
switch (parsed.sub) {
|
|
64
|
+
case 'run': {
|
|
65
|
+
const { runGatewayForeground } = await import('./gateway-run')
|
|
66
|
+
await runGatewayForeground({ agentId: parsed.agentId })
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
case 'start': {
|
|
70
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
71
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
case 'stop': {
|
|
75
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
76
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
case 'restart': {
|
|
80
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
81
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
82
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
83
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
case 'status': {
|
|
87
|
+
const { runGatewayStatus } = await import('./gateway-status')
|
|
88
|
+
await runGatewayStatus({ agentId: parsed.agentId })
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
case 'logs': {
|
|
92
|
+
const { runGatewayLogs } = await import('./gateway-logs')
|
|
93
|
+
await runGatewayLogs({
|
|
94
|
+
agentId: parsed.agentId,
|
|
95
|
+
tail: parsed.tail ?? 100,
|
|
96
|
+
follow: parsed.follow ?? false,
|
|
97
|
+
})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nebula identity` — ERC-8004 (Trustless Agents) on-chain agent identity.
|
|
3
|
+
*
|
|
4
|
+
* nebula identity card build + print the agent card (offline)
|
|
5
|
+
* nebula identity register [--name N] register the agent on the Identity Registry
|
|
6
|
+
* nebula identity show [<agentId>] resolve an agent (by id, or this agent's EOA)
|
|
7
|
+
*
|
|
8
|
+
* The owner of the identity NFT is the operator wallet; the card's
|
|
9
|
+
* `agentAddress` is the agent EOA. Registry address resolves from
|
|
10
|
+
* NEBULA_IDENTITY_REGISTRY env → baked-in deployment.
|
|
11
|
+
*/
|
|
12
|
+
import { writeFile } from 'node:fs/promises'
|
|
13
|
+
import { cancel, intro, note, outro, spinner } from '@clack/prompts'
|
|
14
|
+
import {
|
|
15
|
+
NETWORK_RPC,
|
|
16
|
+
agentIdByAddress,
|
|
17
|
+
buildAgentCard,
|
|
18
|
+
cardToDataUri,
|
|
19
|
+
explorerTxUrl,
|
|
20
|
+
registerAgent,
|
|
21
|
+
resolveAgentById,
|
|
22
|
+
resolveRegistryAddress,
|
|
23
|
+
} from 'nebula-ai-core'
|
|
24
|
+
import { http, type Address, createPublicClient } from 'viem'
|
|
25
|
+
import { findAndLoadConfig } from '../config/load'
|
|
26
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
27
|
+
|
|
28
|
+
export interface IdentityArgs {
|
|
29
|
+
sub: 'card' | 'register' | 'show'
|
|
30
|
+
agentId?: string
|
|
31
|
+
name?: string
|
|
32
|
+
url?: string
|
|
33
|
+
out?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseIdentityArgs(argv: string[]): IdentityArgs | { error: string } {
|
|
37
|
+
const sub = argv[0]
|
|
38
|
+
if (sub !== 'card' && sub !== 'register' && sub !== 'show') {
|
|
39
|
+
return { error: `unknown subcommand '${sub ?? '(none)'}' — try: card | register | show` }
|
|
40
|
+
}
|
|
41
|
+
const flag = (name: string): string | undefined => {
|
|
42
|
+
const i = argv.indexOf(name)
|
|
43
|
+
return i >= 0 ? argv[i + 1] : undefined
|
|
44
|
+
}
|
|
45
|
+
const positional = argv.slice(1).find(a => !a.startsWith('--'))
|
|
46
|
+
return {
|
|
47
|
+
sub,
|
|
48
|
+
agentId: sub === 'show' ? positional : undefined,
|
|
49
|
+
name: flag('--name'),
|
|
50
|
+
url: flag('--url'),
|
|
51
|
+
out: flag('--out'),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runIdentity(args: IdentityArgs): Promise<void> {
|
|
56
|
+
const loaded = await findAndLoadConfig()
|
|
57
|
+
if (!loaded) {
|
|
58
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
const { config } = loaded
|
|
62
|
+
const network = config.network
|
|
63
|
+
const agentAddress = config.identity.agent as Address | null
|
|
64
|
+
|
|
65
|
+
if (args.sub === 'card') {
|
|
66
|
+
if (!agentAddress) {
|
|
67
|
+
console.error('Config has no agent EOA. Run `nebula init` first.')
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
const card = buildAgentCard({
|
|
71
|
+
name: args.name ?? `nebula-${agentAddress.slice(2, 8)}`,
|
|
72
|
+
agentAddress,
|
|
73
|
+
network,
|
|
74
|
+
url: args.url,
|
|
75
|
+
})
|
|
76
|
+
const json = JSON.stringify(card, null, 2)
|
|
77
|
+
if (args.out) {
|
|
78
|
+
await writeFile(args.out, json, 'utf8')
|
|
79
|
+
console.log(`agent card written to ${args.out}`)
|
|
80
|
+
} else {
|
|
81
|
+
console.log(json)
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const registry = resolveRegistryAddress(network)
|
|
87
|
+
if (!registry) {
|
|
88
|
+
console.error(
|
|
89
|
+
`No Identity Registry address for ${network}. Deploy it (contracts/script/DeployIdentityRegistry.s.sol) and set NEBULA_IDENTITY_REGISTRY.`,
|
|
90
|
+
)
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
const publicClient = createPublicClient({ transport: http(NETWORK_RPC[network]) })
|
|
94
|
+
|
|
95
|
+
if (args.sub === 'show') {
|
|
96
|
+
let id: bigint
|
|
97
|
+
if (args.agentId) {
|
|
98
|
+
id = BigInt(args.agentId)
|
|
99
|
+
} else {
|
|
100
|
+
if (!agentAddress) {
|
|
101
|
+
console.error(
|
|
102
|
+
'Pass an <agentId>, or run `nebula init` so this agent has an EOA to reverse-resolve.',
|
|
103
|
+
)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
id = await agentIdByAddress({ publicClient, registry, agentAddress })
|
|
107
|
+
if (id === 0n) {
|
|
108
|
+
console.log(
|
|
109
|
+
`agent ${agentAddress} is not registered on ${network}. Run \`nebula identity register\`.`,
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const resolved = await resolveAgentById({ publicClient, registry, agentId: id })
|
|
115
|
+
console.log(`agent id ${resolved.agentId.toString()}`)
|
|
116
|
+
console.log(`owner ${resolved.owner}`)
|
|
117
|
+
console.log(`agent EOA ${resolved.agentAddress}`)
|
|
118
|
+
console.log(`registry ${registry} (${network})`)
|
|
119
|
+
console.log(
|
|
120
|
+
`card ${resolved.cardURI.slice(0, 80)}${resolved.cardURI.length > 80 ? '…' : ''}`,
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// register
|
|
126
|
+
intro('nebula identity register')
|
|
127
|
+
if (!agentAddress) {
|
|
128
|
+
cancel('Config has no agent EOA. Run `nebula init` first.')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const existing = await agentIdByAddress({ publicClient, registry, agentAddress })
|
|
132
|
+
if (existing !== 0n) {
|
|
133
|
+
note(
|
|
134
|
+
`agent ${agentAddress} is already registered as id ${existing.toString()}.`,
|
|
135
|
+
'already registered',
|
|
136
|
+
)
|
|
137
|
+
outro('nothing to do')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
141
|
+
if (!operator) {
|
|
142
|
+
cancel('No operator wallet available; cannot register.')
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
const s = spinner()
|
|
146
|
+
s.start('Registering agent identity on the ERC-8004 registry')
|
|
147
|
+
try {
|
|
148
|
+
const walletClient = await operator.walletClient(network)
|
|
149
|
+
const card = buildAgentCard({
|
|
150
|
+
name: args.name ?? `nebula-${agentAddress.slice(2, 8)}`,
|
|
151
|
+
agentAddress,
|
|
152
|
+
network,
|
|
153
|
+
url: args.url,
|
|
154
|
+
})
|
|
155
|
+
const cardURI = args.url ?? cardToDataUri(card)
|
|
156
|
+
const { agentId, txHash } = await registerAgent({
|
|
157
|
+
walletClient,
|
|
158
|
+
publicClient,
|
|
159
|
+
registry,
|
|
160
|
+
cardURI,
|
|
161
|
+
agentAddress,
|
|
162
|
+
})
|
|
163
|
+
s.stop(`registered as agent id ${agentId.toString()}`)
|
|
164
|
+
outro(
|
|
165
|
+
[
|
|
166
|
+
` agent id ${agentId.toString()}`,
|
|
167
|
+
` registry ${registry} (${network})`,
|
|
168
|
+
` tx ${explorerTxUrl(network, txHash)}`,
|
|
169
|
+
'',
|
|
170
|
+
'Resolve with: nebula identity show',
|
|
171
|
+
].join('\n'),
|
|
172
|
+
)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
s.stop(`register failed: ${(e as Error).message.slice(0, 200)}`)
|
|
175
|
+
} finally {
|
|
176
|
+
await operator.close?.()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { formatEther } from 'viem'
|
|
2
|
+
|
|
3
|
+
/** Mantle mainnet spot price used for USD estimates. Not authoritative, just a hint. */
|
|
4
|
+
const MNT_USD = 0.5
|
|
5
|
+
|
|
6
|
+
export type DeployTarget = 'local'
|
|
7
|
+
|
|
8
|
+
export interface CostBreakdown {
|
|
9
|
+
agentFloat: bigint
|
|
10
|
+
totalOperator: bigint
|
|
11
|
+
deployTarget: DeployTarget
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function estimateCosts(opts?: {
|
|
15
|
+
ledgerSizeOg?: number
|
|
16
|
+
withSubname?: boolean
|
|
17
|
+
deployTarget?: DeployTarget
|
|
18
|
+
agentFloatWei?: bigint
|
|
19
|
+
}): CostBreakdown {
|
|
20
|
+
// The only init cost is funding the agent EOA with a small gas float. There
|
|
21
|
+
// is no iNFT mint, no storage anchor, and no compute ledger.
|
|
22
|
+
const agentFloat = opts?.agentFloatWei ?? 100_000_000_000_000_000n // 0.1 MNT
|
|
23
|
+
return { agentFloat, totalOperator: agentFloat, deployTarget: opts?.deployTarget ?? 'local' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatUsd(valueWei: bigint): string {
|
|
27
|
+
const mnt = Number(formatEther(valueWei))
|
|
28
|
+
return `$${(mnt * MNT_USD).toFixed(2)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderCostSummary(c: CostBreakdown): string {
|
|
32
|
+
const line = (label: string, wei: bigint): string =>
|
|
33
|
+
` ${label.padEnd(32)}${formatEther(wei).padStart(8)} Mantle (${formatUsd(wei)})`
|
|
34
|
+
return [
|
|
35
|
+
' operator spend (Mantle mainnet):',
|
|
36
|
+
line('agent infra float (gas)', c.agentFloat),
|
|
37
|
+
` ${'─'.repeat(32)}${'─'.repeat(18)}`,
|
|
38
|
+
line('total operator spend', c.totalOperator),
|
|
39
|
+
].join('\n')
|
|
40
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cancel, isCancel, select } from '@clack/prompts'
|
|
2
|
+
import qrcode from 'qrcode-terminal'
|
|
3
|
+
import { type Address, type PublicClient, formatEther } from 'viem'
|
|
4
|
+
|
|
5
|
+
export interface FundingGateOpts {
|
|
6
|
+
publicClient: PublicClient
|
|
7
|
+
operatorAddress: Address
|
|
8
|
+
requiredOg: bigint
|
|
9
|
+
pollIntervalMs?: number
|
|
10
|
+
maxWaitMs?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FundingGateOutcome =
|
|
14
|
+
| { kind: 'funded'; balance: bigint }
|
|
15
|
+
| { kind: 'skip-ledger' }
|
|
16
|
+
| { kind: 'cancel' }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Show operator address as a QR and poll balance until it meets required
|
|
20
|
+
* threshold. User can cancel or choose to proceed with minimum-only (skip
|
|
21
|
+
* full compute ledger) at any point.
|
|
22
|
+
*
|
|
23
|
+
* Console prints the QR once; the polling loop updates a single line
|
|
24
|
+
* using `process.stdout.write` so the display doesn't scroll.
|
|
25
|
+
*/
|
|
26
|
+
export async function fundingGate(opts: FundingGateOpts): Promise<FundingGateOutcome> {
|
|
27
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 10_000
|
|
28
|
+
const maxWaitMs = opts.maxWaitMs ?? 30 * 60_000 // 30 minutes
|
|
29
|
+
|
|
30
|
+
console.log('')
|
|
31
|
+
console.log(` Send at least ${formatEther(opts.requiredOg)} Mantle to:`)
|
|
32
|
+
console.log(` ${opts.operatorAddress}`)
|
|
33
|
+
console.log('')
|
|
34
|
+
qrcode.generate(opts.operatorAddress, { small: true })
|
|
35
|
+
console.log('')
|
|
36
|
+
|
|
37
|
+
const start = Date.now()
|
|
38
|
+
while (Date.now() - start < maxWaitMs) {
|
|
39
|
+
const balance = await opts.publicClient.getBalance({ address: opts.operatorAddress })
|
|
40
|
+
if (balance >= opts.requiredOg) {
|
|
41
|
+
process.stdout.write('\r')
|
|
42
|
+
return { kind: 'funded', balance }
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`\r polling... current balance ${formatEther(balance)} Mantle (need ${formatEther(opts.requiredOg)}) `,
|
|
46
|
+
)
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.stdout.write('\n\n')
|
|
51
|
+
const choice = await select({
|
|
52
|
+
message: 'Balance still insufficient. What now?',
|
|
53
|
+
options: [
|
|
54
|
+
{ value: 'skip' as const, label: 'Skip compute ledger for now (mint + subname only)' },
|
|
55
|
+
{ value: 'cancel' as const, label: 'Cancel init' },
|
|
56
|
+
],
|
|
57
|
+
initialValue: 'cancel',
|
|
58
|
+
})
|
|
59
|
+
if (isCancel(choice)) {
|
|
60
|
+
cancel('Aborted.')
|
|
61
|
+
return { kind: 'cancel' }
|
|
62
|
+
}
|
|
63
|
+
return choice === 'skip' ? { kind: 'skip-ledger' } : { kind: 'cancel' }
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NebulaNetwork } from 'nebula-ai-core'
|
|
2
|
+
|
|
3
|
+
export interface ModelPick {
|
|
4
|
+
provider: string
|
|
5
|
+
model: string | null
|
|
6
|
+
inputPricePerTokenWei: bigint
|
|
7
|
+
outputPricePerTokenWei: bigint
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Nebula uses a fixed OpenAI-compatible model configured via env
|
|
12
|
+
* (`NEBULA_LLM_MODEL` / `NEBULA_LLM_BASE_URL` / `OPENAI_API_KEY`), so there's
|
|
13
|
+
* no live provider catalog to pick from. Return the configured default so
|
|
14
|
+
* `init` / `model` proceed without prompting.
|
|
15
|
+
*/
|
|
16
|
+
export async function pickBrainModel(_opts: {
|
|
17
|
+
network: NebulaNetwork
|
|
18
|
+
}): Promise<ModelPick | null> {
|
|
19
|
+
return {
|
|
20
|
+
provider: 'openai-compatible',
|
|
21
|
+
model: process.env.NEBULA_LLM_MODEL ?? 'gpt-4o-mini',
|
|
22
|
+
inputPricePerTokenWei: 0n,
|
|
23
|
+
outputPricePerTokenWei: 0n,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { cancel, isCancel, note, password, select, text } from '@clack/prompts'
|
|
3
|
+
import {
|
|
4
|
+
KeychainOperatorSigner,
|
|
5
|
+
KeystoreFileOperatorSigner,
|
|
6
|
+
type NebulaNetwork,
|
|
7
|
+
type OperatorSigner,
|
|
8
|
+
type OperatorSourceHint,
|
|
9
|
+
type OperatorSourceKind,
|
|
10
|
+
RawPrivkeyOperatorSigner,
|
|
11
|
+
WalletConnectOperatorSigner,
|
|
12
|
+
} from 'nebula-ai-core'
|
|
13
|
+
|
|
14
|
+
interface PickerOptions {
|
|
15
|
+
network: NebulaNetwork
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OperatorPickResult {
|
|
19
|
+
signer: OperatorSigner
|
|
20
|
+
hint: OperatorSourceHint
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prompt the user for their operator wallet source and return both the
|
|
25
|
+
* connected `OperatorSigner` and the metadata needed to reconstruct it
|
|
26
|
+
* later (`OperatorSourceHint`). The hint is saved to `nebula.config.ts` by
|
|
27
|
+
* the wizard so subsequent commands (chat, topup, restore) can re-attach
|
|
28
|
+
* to the same source without re-prompting.
|
|
29
|
+
*
|
|
30
|
+
* Platform-aware: on macOS, all four sources are offered. On Linux/Windows
|
|
31
|
+
* the OS keychain option is hidden because libsecret/Credential-Manager
|
|
32
|
+
* support is post-MVP.
|
|
33
|
+
*/
|
|
34
|
+
export async function pickOperatorSigner(opts: PickerOptions): Promise<OperatorPickResult | null> {
|
|
35
|
+
const isMac = process.platform === 'darwin'
|
|
36
|
+
const choices: { value: OperatorSourceKind; label: string; hint?: string }[] = [
|
|
37
|
+
{
|
|
38
|
+
value: 'walletconnect',
|
|
39
|
+
label: 'WalletConnect',
|
|
40
|
+
hint: 'scan QR with any WC-compatible mobile wallet',
|
|
41
|
+
},
|
|
42
|
+
...(isMac
|
|
43
|
+
? ([
|
|
44
|
+
{
|
|
45
|
+
value: 'keychain',
|
|
46
|
+
label: 'macOS Keychain',
|
|
47
|
+
hint: 'stored in login keychain',
|
|
48
|
+
},
|
|
49
|
+
] as const)
|
|
50
|
+
: []),
|
|
51
|
+
{
|
|
52
|
+
value: 'keystore-file',
|
|
53
|
+
label: 'Keystore file',
|
|
54
|
+
hint: 'encrypted JSON, geth format',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: 'raw-privkey',
|
|
58
|
+
label: 'Raw private key',
|
|
59
|
+
hint: 'stdin prompt, for CI/scripting',
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
const source = (await select({
|
|
63
|
+
message: 'Connect your operator wallet (owns the iNFT)',
|
|
64
|
+
options: choices,
|
|
65
|
+
initialValue: choices[0]!.value,
|
|
66
|
+
})) as OperatorSourceKind | symbol
|
|
67
|
+
if (isCancel(source)) {
|
|
68
|
+
cancel('Aborted.')
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (source) {
|
|
73
|
+
case 'walletconnect':
|
|
74
|
+
return {
|
|
75
|
+
signer: new WalletConnectOperatorSigner({ networks: [opts.network] }),
|
|
76
|
+
hint: { source: 'walletconnect' },
|
|
77
|
+
}
|
|
78
|
+
case 'keychain': {
|
|
79
|
+
const service = await text({
|
|
80
|
+
message: 'Keychain service name',
|
|
81
|
+
placeholder: 'nebula.operator',
|
|
82
|
+
validate: v => {
|
|
83
|
+
if (!v || v.length === 0) return 'Required.'
|
|
84
|
+
if (!/^[a-zA-Z0-9._-]{1,128}$/.test(v))
|
|
85
|
+
return 'Allowed characters: a-z, A-Z, 0-9, dot, underscore, hyphen (max 128).'
|
|
86
|
+
return undefined
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
if (isCancel(service)) {
|
|
90
|
+
cancel('Aborted.')
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
const svc = service as string
|
|
94
|
+
return {
|
|
95
|
+
signer: new KeychainOperatorSigner(svc),
|
|
96
|
+
hint: { source: 'keychain', keychainService: svc },
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
case 'keystore-file': {
|
|
100
|
+
const path = await text({
|
|
101
|
+
message: 'Path to encrypted JSON keystore',
|
|
102
|
+
placeholder: '~/wallets/operator.json',
|
|
103
|
+
validate: v => {
|
|
104
|
+
if (!v) return 'Required.'
|
|
105
|
+
const expanded = v.replace(/^~/, process.env.HOME ?? '~')
|
|
106
|
+
if (!existsSync(expanded)) return `File not found: ${expanded}`
|
|
107
|
+
return undefined
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
if (isCancel(path)) {
|
|
111
|
+
cancel('Aborted.')
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
const expanded = (path as string).replace(/^~/, process.env.HOME ?? '~')
|
|
115
|
+
const pass = await password({
|
|
116
|
+
message: 'Passphrase for the keystore',
|
|
117
|
+
validate: v => (v && v.length > 0 ? undefined : 'Required.'),
|
|
118
|
+
})
|
|
119
|
+
if (isCancel(pass)) {
|
|
120
|
+
cancel('Aborted.')
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
signer: new KeystoreFileOperatorSigner({ path: expanded, passphrase: pass as string }),
|
|
125
|
+
hint: { source: 'keystore-file', keystorePath: path as string },
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
case 'raw-privkey': {
|
|
129
|
+
if (process.env.NEBULA_OPERATOR_PRIVKEY) {
|
|
130
|
+
note('Using NEBULA_OPERATOR_PRIVKEY from env.', 'raw-privkey')
|
|
131
|
+
return {
|
|
132
|
+
signer: new RawPrivkeyOperatorSigner({
|
|
133
|
+
privkey: process.env.NEBULA_OPERATOR_PRIVKEY,
|
|
134
|
+
sourceLabel: 'env:NEBULA_OPERATOR_PRIVKEY',
|
|
135
|
+
}),
|
|
136
|
+
hint: { source: 'raw-privkey' },
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const pk = await password({
|
|
140
|
+
message: 'Operator private key (hex, 0x prefix optional)',
|
|
141
|
+
validate: v => {
|
|
142
|
+
if (!v) return 'Required.'
|
|
143
|
+
const clean = v.trim().replace(/^0x/, '')
|
|
144
|
+
if (!/^[0-9a-fA-F]{64}$/.test(clean)) return 'Must be 32 bytes hex.'
|
|
145
|
+
return undefined
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
if (isCancel(pk)) {
|
|
149
|
+
cancel('Aborted.')
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
signer: new RawPrivkeyOperatorSigner({ privkey: pk as string, sourceLabel: 'stdin' }),
|
|
154
|
+
hint: { source: 'raw-privkey' },
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reload an `OperatorSigner` from a previously persisted hint in
|
|
162
|
+
* `nebula.config.ts`. Used by chat / topup / restore / resume so the user
|
|
163
|
+
* doesn't re-pick a source every session — they only re-supply per-session
|
|
164
|
+
* secrets (passphrases / QR scans / env vars).
|
|
165
|
+
*
|
|
166
|
+
* Returns null when the hint is missing or unusable; the caller falls back
|
|
167
|
+
* to `pickOperatorSigner` for an interactive choice.
|
|
168
|
+
*/
|
|
169
|
+
export async function loadOperatorFromHint(
|
|
170
|
+
hint: OperatorSourceHint,
|
|
171
|
+
network: NebulaNetwork,
|
|
172
|
+
): Promise<OperatorSigner | null> {
|
|
173
|
+
switch (hint.source) {
|
|
174
|
+
case 'walletconnect':
|
|
175
|
+
return new WalletConnectOperatorSigner({ networks: [network] })
|
|
176
|
+
case 'keychain': {
|
|
177
|
+
if (!hint.keychainService) return null
|
|
178
|
+
return new KeychainOperatorSigner(hint.keychainService)
|
|
179
|
+
}
|
|
180
|
+
case 'keystore-file': {
|
|
181
|
+
if (!hint.keystorePath) return null
|
|
182
|
+
const expanded = hint.keystorePath.replace(/^~/, process.env.HOME ?? '~')
|
|
183
|
+
if (!existsSync(expanded)) {
|
|
184
|
+
note(`Operator keystore not found at ${expanded}; pick a new source.`, 'keystore missing')
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
const pass = await password({
|
|
188
|
+
message: `Passphrase for operator keystore ${expanded}`,
|
|
189
|
+
validate: v => (v && v.length > 0 ? undefined : 'Required.'),
|
|
190
|
+
})
|
|
191
|
+
if (isCancel(pass)) return null
|
|
192
|
+
return new KeystoreFileOperatorSigner({
|
|
193
|
+
path: expanded,
|
|
194
|
+
passphrase: pass as string,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
case 'raw-privkey': {
|
|
198
|
+
if (process.env.NEBULA_OPERATOR_PRIVKEY) {
|
|
199
|
+
return new RawPrivkeyOperatorSigner({
|
|
200
|
+
privkey: process.env.NEBULA_OPERATOR_PRIVKEY,
|
|
201
|
+
sourceLabel: 'env:NEBULA_OPERATOR_PRIVKEY',
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
const pk = await password({
|
|
205
|
+
message: 'Operator private key (hex, 0x prefix optional)',
|
|
206
|
+
validate: v => {
|
|
207
|
+
if (!v) return 'Required.'
|
|
208
|
+
const clean = v.trim().replace(/^0x/, '')
|
|
209
|
+
if (!/^[0-9a-fA-F]{64}$/.test(clean)) return 'Must be 32 bytes hex.'
|
|
210
|
+
return undefined
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
if (isCancel(pk)) return null
|
|
214
|
+
return new RawPrivkeyOperatorSigner({ privkey: pk as string, sourceLabel: 'stdin' })
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* High-level helper: load operator from config hint when available, fall
|
|
221
|
+
* back to interactive picker otherwise. Used by all post-init commands.
|
|
222
|
+
*/
|
|
223
|
+
export async function loadOrPickOperatorSigner(opts: {
|
|
224
|
+
network: NebulaNetwork
|
|
225
|
+
hint?: OperatorSourceHint | null
|
|
226
|
+
}): Promise<OperatorSigner | null> {
|
|
227
|
+
if (opts.hint) {
|
|
228
|
+
const signer = await loadOperatorFromHint(opts.hint, opts.network)
|
|
229
|
+
if (signer) return signer
|
|
230
|
+
}
|
|
231
|
+
const picked = await pickOperatorSigner({ network: opts.network })
|
|
232
|
+
return picked?.signer ?? null
|
|
233
|
+
}
|