nebula-ai-agent 0.3.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.
- package/README.md +39 -0
- package/bin/nebula +11 -0
- package/package.json +50 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_unlock.ts +66 -0
- package/src/commands/agent-wallet.ts +90 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1308 -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/login.ts +86 -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/commands/trust.ts +196 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.ts +99 -0
- package/src/index.ts +184 -0
- package/src/profile/crypto.ts +68 -0
- package/src/profile/derive.ts +25 -0
- package/src/profile/store.ts +86 -0
- package/src/profile/unlock.ts +29 -0
- package/src/ui/app.tsx +719 -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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nebula reputation` + `nebula validation` — ERC-8004 Reputation + Validation.
|
|
3
|
+
*
|
|
4
|
+
* nebula reputation show [<agentId>]
|
|
5
|
+
* nebula reputation give --agent <id> --score <0-100> [--tag t] [--uri u]
|
|
6
|
+
* nebula validation show <requestId>
|
|
7
|
+
* nebula validation request --agent <id> --data <str> [--uri u]
|
|
8
|
+
* nebula validation respond --id <reqId> --passed <true|false> [--score n] [--uri u]
|
|
9
|
+
*/
|
|
10
|
+
import { cancel, intro, outro, spinner } from '@clack/prompts'
|
|
11
|
+
import {
|
|
12
|
+
NETWORK_RPC,
|
|
13
|
+
agentIdByAddress,
|
|
14
|
+
explorerTxUrl,
|
|
15
|
+
getReputation,
|
|
16
|
+
getValidation,
|
|
17
|
+
giveFeedback,
|
|
18
|
+
requestValidation,
|
|
19
|
+
resolveRegistryAddress,
|
|
20
|
+
resolveReputationRegistry,
|
|
21
|
+
resolveValidationRegistry,
|
|
22
|
+
respondValidation,
|
|
23
|
+
} from 'nebula-ai-core'
|
|
24
|
+
import { http, type Address, createPublicClient, keccak256, toHex } from 'viem'
|
|
25
|
+
import { findAndLoadConfig } from '../config/load'
|
|
26
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
27
|
+
|
|
28
|
+
function flag(argv: string[], name: string): string | undefined {
|
|
29
|
+
const i = argv.indexOf(name)
|
|
30
|
+
return i >= 0 ? argv[i + 1] : undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TrustArgs {
|
|
34
|
+
kind: 'reputation' | 'validation'
|
|
35
|
+
sub: string
|
|
36
|
+
argv: string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseTrustArgs(
|
|
40
|
+
kind: 'reputation' | 'validation',
|
|
41
|
+
argv: string[],
|
|
42
|
+
): TrustArgs | { error: string } {
|
|
43
|
+
const sub = argv[0] ?? ''
|
|
44
|
+
const valid = kind === 'reputation' ? ['show', 'give'] : ['show', 'request', 'respond']
|
|
45
|
+
if (!valid.includes(sub))
|
|
46
|
+
return { error: `unknown subcommand '${sub || '(none)'}' — try: ${valid.join(' | ')}` }
|
|
47
|
+
return { kind, sub, argv: argv.slice(1) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runTrust(args: TrustArgs): Promise<void> {
|
|
51
|
+
const loaded = await findAndLoadConfig()
|
|
52
|
+
if (!loaded) {
|
|
53
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
const { config } = loaded
|
|
57
|
+
const network = config.network
|
|
58
|
+
const publicClient = createPublicClient({ transport: http(NETWORK_RPC[network]) })
|
|
59
|
+
const repReg = resolveReputationRegistry(network)
|
|
60
|
+
const valReg = resolveValidationRegistry(network)
|
|
61
|
+
const idReg = resolveRegistryAddress(network)
|
|
62
|
+
|
|
63
|
+
// ── reads ──
|
|
64
|
+
if (args.kind === 'reputation' && args.sub === 'show') {
|
|
65
|
+
if (!repReg) return fail(`No Reputation Registry for ${network}.`)
|
|
66
|
+
let id = flag(args.argv, '--agent') ?? args.argv.find(a => !a.startsWith('--'))
|
|
67
|
+
if (!id) {
|
|
68
|
+
if (!idReg || !config.identity.agent) return fail('Pass an <agentId>.')
|
|
69
|
+
const resolved = await agentIdByAddress({
|
|
70
|
+
publicClient,
|
|
71
|
+
registry: idReg,
|
|
72
|
+
agentAddress: config.identity.agent as Address,
|
|
73
|
+
})
|
|
74
|
+
if (resolved === 0n)
|
|
75
|
+
return void console.log('this agent is not registered; run `nebula identity register`')
|
|
76
|
+
id = resolved.toString()
|
|
77
|
+
}
|
|
78
|
+
const { count, averageScore } = await getReputation({
|
|
79
|
+
publicClient,
|
|
80
|
+
registry: repReg,
|
|
81
|
+
agentId: BigInt(id),
|
|
82
|
+
})
|
|
83
|
+
console.log(`agent id ${id}`)
|
|
84
|
+
console.log(`ratings ${count}`)
|
|
85
|
+
console.log(`avg score ${averageScore} / 100`)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (args.kind === 'validation' && args.sub === 'show') {
|
|
89
|
+
if (!valReg) return fail(`No Validation Registry for ${network}.`)
|
|
90
|
+
const reqId = flag(args.argv, '--id') ?? args.argv.find(a => !a.startsWith('--'))
|
|
91
|
+
if (!reqId) return fail('Pass a <requestId>.')
|
|
92
|
+
const v = await getValidation({ publicClient, registry: valReg, requestId: BigInt(reqId) })
|
|
93
|
+
console.log(`request id ${reqId}`)
|
|
94
|
+
console.log(`agent id ${v.agentId.toString()}`)
|
|
95
|
+
console.log(`requester ${v.requester}`)
|
|
96
|
+
console.log(`status ${v.responded ? (v.passed ? 'PASSED' : 'FAILED') : 'pending'}`)
|
|
97
|
+
if (v.responded) {
|
|
98
|
+
console.log(`validator ${v.validator}`)
|
|
99
|
+
console.log(`score ${v.score} / 100`)
|
|
100
|
+
}
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── writes (need the operator wallet) ──
|
|
105
|
+
intro(`nebula ${args.kind} ${args.sub}`)
|
|
106
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
107
|
+
if (!operator) return cancel('No operator wallet available.')
|
|
108
|
+
const walletClient = await operator.walletClient(network)
|
|
109
|
+
const s = spinner()
|
|
110
|
+
try {
|
|
111
|
+
if (args.kind === 'reputation' && args.sub === 'give') {
|
|
112
|
+
if (!repReg) throw new Error(`No Reputation Registry for ${network}.`)
|
|
113
|
+
const agentId = BigInt(
|
|
114
|
+
flag(args.argv, '--agent') ??
|
|
115
|
+
(() => {
|
|
116
|
+
throw new Error('--agent <id> required')
|
|
117
|
+
})(),
|
|
118
|
+
)
|
|
119
|
+
const score = Number(
|
|
120
|
+
flag(args.argv, '--score') ??
|
|
121
|
+
(() => {
|
|
122
|
+
throw new Error('--score <0-100> required')
|
|
123
|
+
})(),
|
|
124
|
+
)
|
|
125
|
+
s.start('Recording reputation feedback on-chain')
|
|
126
|
+
const { txHash } = await giveFeedback({
|
|
127
|
+
walletClient,
|
|
128
|
+
publicClient,
|
|
129
|
+
registry: repReg,
|
|
130
|
+
agentId,
|
|
131
|
+
score,
|
|
132
|
+
tag: flag(args.argv, '--tag') ?? '',
|
|
133
|
+
uri: flag(args.argv, '--uri') ?? '',
|
|
134
|
+
})
|
|
135
|
+
s.stop('feedback recorded')
|
|
136
|
+
outro(`tx ${explorerTxUrl(network, txHash)}`)
|
|
137
|
+
} else if (args.kind === 'validation' && args.sub === 'request') {
|
|
138
|
+
if (!valReg) throw new Error(`No Validation Registry for ${network}.`)
|
|
139
|
+
const agentId = BigInt(
|
|
140
|
+
flag(args.argv, '--agent') ??
|
|
141
|
+
(() => {
|
|
142
|
+
throw new Error('--agent <id> required')
|
|
143
|
+
})(),
|
|
144
|
+
)
|
|
145
|
+
const data =
|
|
146
|
+
flag(args.argv, '--data') ??
|
|
147
|
+
(() => {
|
|
148
|
+
throw new Error('--data <string|0xhash> required')
|
|
149
|
+
})()
|
|
150
|
+
const dataHash = /^0x[0-9a-fA-F]{64}$/.test(data)
|
|
151
|
+
? (data as `0x${string}`)
|
|
152
|
+
: keccak256(toHex(data))
|
|
153
|
+
s.start('Opening validation request on-chain')
|
|
154
|
+
const { requestId, txHash } = await requestValidation({
|
|
155
|
+
walletClient,
|
|
156
|
+
publicClient,
|
|
157
|
+
registry: valReg,
|
|
158
|
+
agentId,
|
|
159
|
+
dataHash,
|
|
160
|
+
uri: flag(args.argv, '--uri') ?? '',
|
|
161
|
+
})
|
|
162
|
+
s.stop(`request id ${requestId.toString()}`)
|
|
163
|
+
outro(`tx ${explorerTxUrl(network, txHash)}`)
|
|
164
|
+
} else if (args.kind === 'validation' && args.sub === 'respond') {
|
|
165
|
+
if (!valReg) throw new Error(`No Validation Registry for ${network}.`)
|
|
166
|
+
const requestId = BigInt(
|
|
167
|
+
flag(args.argv, '--id') ??
|
|
168
|
+
(() => {
|
|
169
|
+
throw new Error('--id <requestId> required')
|
|
170
|
+
})(),
|
|
171
|
+
)
|
|
172
|
+
const passed = (flag(args.argv, '--passed') ?? 'true') !== 'false'
|
|
173
|
+
s.start('Publishing validation response on-chain')
|
|
174
|
+
const { txHash } = await respondValidation({
|
|
175
|
+
walletClient,
|
|
176
|
+
publicClient,
|
|
177
|
+
registry: valReg,
|
|
178
|
+
requestId,
|
|
179
|
+
passed,
|
|
180
|
+
score: Number(flag(args.argv, '--score') ?? '0'),
|
|
181
|
+
uri: flag(args.argv, '--uri') ?? '',
|
|
182
|
+
})
|
|
183
|
+
s.stop('response published')
|
|
184
|
+
outro(`tx ${explorerTxUrl(network, txHash)}`)
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
s.stop(`failed: ${(e as Error).message.slice(0, 200)}`)
|
|
188
|
+
} finally {
|
|
189
|
+
await operator.close?.()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function fail(msg: string): void {
|
|
194
|
+
console.error(msg)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { type NebulaConfig, agentPaths } from 'nebula-ai-core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load the user's `nebula.config.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Phase 6.6: canonical location is `~/.nebula/config.ts` (returned by
|
|
9
|
+
* `agentPaths.config`). If that file exists, it wins. Otherwise, fall back
|
|
10
|
+
* to walking upward from cwd looking for `nebula.config.ts` (legacy v0.5.0
|
|
11
|
+
* pattern, kept so existing dev setups still work without a migration step).
|
|
12
|
+
*/
|
|
13
|
+
export async function findAndLoadConfig(
|
|
14
|
+
startDir: string = process.cwd(),
|
|
15
|
+
): Promise<{ config: NebulaConfig; path: string } | null> {
|
|
16
|
+
const canonical = agentPaths.config
|
|
17
|
+
if (existsSync(canonical)) {
|
|
18
|
+
const mod = (await import(canonical)) as { default: NebulaConfig }
|
|
19
|
+
if (!mod.default) throw new Error(`nebula config at ${canonical} has no default export`)
|
|
20
|
+
return { config: mod.default, path: canonical }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let dir = resolve(startDir)
|
|
24
|
+
while (true) {
|
|
25
|
+
const candidate = resolve(dir, 'nebula.config.ts')
|
|
26
|
+
if (existsSync(candidate)) {
|
|
27
|
+
const mod = (await import(candidate)) as { default: NebulaConfig }
|
|
28
|
+
if (!mod.default) throw new Error(`nebula.config.ts at ${candidate} has no default export`)
|
|
29
|
+
return { config: mod.default, path: candidate }
|
|
30
|
+
}
|
|
31
|
+
const parent = resolve(dir, '..')
|
|
32
|
+
if (parent === dir) return null
|
|
33
|
+
dir = parent
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import type { NebulaConfig } from 'nebula-ai-core'
|
|
4
|
+
|
|
5
|
+
export interface RenderConfigOpts {
|
|
6
|
+
header?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialize an NebulaConfig into a `~/.nebula/config.ts` file body.
|
|
11
|
+
*
|
|
12
|
+
* Phase 6.6: the config lives at `~/.nebula/config.ts` which is outside any
|
|
13
|
+
* workspace, so it MUST NOT import `nebula-ai-core` (the import won't
|
|
14
|
+
* resolve from `~/.nebula/`). We emit a plain `export default { ... }` object;
|
|
15
|
+
* the runtime loader treats it as `NebulaConfig` directly.
|
|
16
|
+
*/
|
|
17
|
+
export function renderConfigTs(cfg: NebulaConfig, opts: RenderConfigOpts = {}): string {
|
|
18
|
+
const header = opts.header ?? ''
|
|
19
|
+
const operatorLine = cfg.operator ? ` operator: ${JSON.stringify(cfg.operator)},\n` : ''
|
|
20
|
+
// Emit either the operator's chosen sandbox config OR an annotated
|
|
21
|
+
// "OPTION 1/2/3" block so the operator can opt in by uncommenting. Default
|
|
22
|
+
// mode stays `none` (passthrough); documentation IS the UX.
|
|
23
|
+
const sandboxBlock = renderSandboxBlock(cfg.sandbox)
|
|
24
|
+
return `${header ? `${header}\n\n` : ''}export default {
|
|
25
|
+
identity: ${JSON.stringify(cfg.identity)},
|
|
26
|
+
network: ${JSON.stringify(cfg.network)},
|
|
27
|
+
storage: { network: ${JSON.stringify(cfg.storage.network)} },
|
|
28
|
+
brain: {
|
|
29
|
+
provider: ${JSON.stringify(cfg.brain.provider)},
|
|
30
|
+
model: ${JSON.stringify(cfg.brain.model)},
|
|
31
|
+
},
|
|
32
|
+
plugins: ${JSON.stringify(cfg.plugins)},
|
|
33
|
+
tools: ${JSON.stringify(cfg.tools)},
|
|
34
|
+
imports: { claudeCode: ${cfg.imports.claudeCode} },
|
|
35
|
+
${operatorLine}${sandboxBlock}}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderSandboxBlock(sandbox: NebulaConfig['sandbox']): string {
|
|
40
|
+
// Phase 11: deploy-target sandbox metadata (id/providerAddress/endpoint/
|
|
41
|
+
// snapshotName) OR Phase 9.5 limb-sandbox mode = anything non-default → emit
|
|
42
|
+
// verbatim. Only surface the doc-comment template when the operator has
|
|
43
|
+
// touched neither.
|
|
44
|
+
const hasNonDefaultLimbMode = sandbox?.mode && sandbox.mode !== 'none'
|
|
45
|
+
if (hasNonDefaultLimbMode) {
|
|
46
|
+
return ` sandbox: ${JSON.stringify(sandbox, null, 2).replace(/\n/g, '\n ')},\n`
|
|
47
|
+
}
|
|
48
|
+
// Fresh install: write the active default + commented examples for the
|
|
49
|
+
// other tiers. Operator can opt in by uncommenting and editing.
|
|
50
|
+
return ` sandbox: { mode: 'none' },
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Limb sandbox (Phase 9.5). Defense-in-depth beneath the permission floor:
|
|
53
|
+
// even when the modal grants 'allow session' or YOLO disables prompts,
|
|
54
|
+
// the sandbox profile/container blocks writes outside an allowlist.
|
|
55
|
+
// All shell.run / code.execute / shell.process_start spawns route through
|
|
56
|
+
// the chosen backend. fs.* and browser.* still run on the host (PathGuard
|
|
57
|
+
// applies). Override at runtime via NEBULA_SANDBOX_MODE=os|docker|none.
|
|
58
|
+
//
|
|
59
|
+
// OPTION 1: none (default): passthrough, fastest, permission floor only.
|
|
60
|
+
//
|
|
61
|
+
// OPTION 2: os (macOS sandbox-exec / seatbelt). Allows writes to agentDir +
|
|
62
|
+
// cwd + /tmp/nebula-* + /var/folders. Denies reads of ~/.ssh, ~/.aws,
|
|
63
|
+
// ~/Library/Keychains, ~/.config/gcloud. Linux bubblewrap pending.
|
|
64
|
+
// sandbox: { mode: 'os' },
|
|
65
|
+
//
|
|
66
|
+
// OPTION 3: docker (long-lived container per session), every shell-class
|
|
67
|
+
// spawn through 'docker exec'. Auto-detects Docker Desktop or Podman.
|
|
68
|
+
// Default image 'nikolaik/python-nodejs:python3.11-nodejs20' (matches
|
|
69
|
+
// hermes; ~700 MB; bash + python3 + node + npm + git on standard PATH).
|
|
70
|
+
// Override with 'oven/bun:1' (~250 MB) if you only need bun/ts.
|
|
71
|
+
// 'dockerMountWorkspace: true' bind-mounts your launch cwd into
|
|
72
|
+
// /workspace (off by default for max isolation). 'dockerRuntimePath'
|
|
73
|
+
// forces a specific runtime binary. Resource caps are unset by default
|
|
74
|
+
// (container competes fairly with host work). Set them to mirror hermes'
|
|
75
|
+
// production hardening: dockerCpu=1, dockerMemoryMb=5120, dockerDiskMb=
|
|
76
|
+
// 51200 (Linux+overlay2 only). dockerNoNetwork=true blocks all internet
|
|
77
|
+
// access from the container (max paranoia for code.execute).
|
|
78
|
+
// sandbox: {
|
|
79
|
+
// mode: 'docker',
|
|
80
|
+
// dockerImage: 'nikolaik/python-nodejs:python3.11-nodejs20',
|
|
81
|
+
// dockerMountWorkspace: false,
|
|
82
|
+
// // dockerRuntimePath: '/opt/homebrew/bin/podman',
|
|
83
|
+
// // dockerCpu: 1, // CPU cores cap
|
|
84
|
+
// // dockerMemoryMb: 5120, // 5 GB memory cap
|
|
85
|
+
// // dockerDiskMb: 51200, // 50 GB disk cap (Linux + overlay2 only)
|
|
86
|
+
// // dockerNoNetwork: true, // block all network from inside container
|
|
87
|
+
// },
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeConfigTs(
|
|
93
|
+
path: string,
|
|
94
|
+
cfg: NebulaConfig,
|
|
95
|
+
opts: RenderConfigOpts = {},
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
await mkdir(dirname(path), { recursive: true })
|
|
98
|
+
await writeFile(path, renderConfigTs(cfg, opts), 'utf8')
|
|
99
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argv dispatch. No subcommand → chat REPL, otherwise route to
|
|
3
|
+
* commands/<name>.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const argv = process.argv.slice(2)
|
|
7
|
+
// First arg starting with `--` means the user invoked the default subcommand
|
|
8
|
+
// (chat) with flags, e.g. `nebula --yolo`. Treat it as if `chat` were implicit.
|
|
9
|
+
// Exception: `--help` and `--version` are top-level commands, not chat flags.
|
|
10
|
+
const first = argv[0]
|
|
11
|
+
const isTopLevelFlag = first === '--help' || first === '--version'
|
|
12
|
+
const sub = first?.startsWith('--') && !isTopLevelFlag ? 'chat' : first
|
|
13
|
+
|
|
14
|
+
async function main(): Promise<void> {
|
|
15
|
+
switch (sub) {
|
|
16
|
+
case undefined:
|
|
17
|
+
case 'chat': {
|
|
18
|
+
const { runChat } = await import('./commands/chat')
|
|
19
|
+
await runChat({ yolo: argv.includes('--yolo') })
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
case 'init': {
|
|
23
|
+
const { runInit } = await import('./commands/init')
|
|
24
|
+
await runInit()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
case 'status': {
|
|
28
|
+
const { runStatus } = await import('./commands/status')
|
|
29
|
+
await runStatus()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
case 'login': {
|
|
33
|
+
const { runLogin } = await import('./commands/login')
|
|
34
|
+
await runLogin()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
case 'logout': {
|
|
38
|
+
const { runLogout } = await import('./commands/login')
|
|
39
|
+
await runLogout()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
case 'agent': {
|
|
43
|
+
const { runAgentWallet } = await import('./commands/agent-wallet')
|
|
44
|
+
await runAgentWallet()
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
case 'logs': {
|
|
48
|
+
const { runLogs } = await import('./commands/logs')
|
|
49
|
+
const tailIdx = argv.indexOf('--tail')
|
|
50
|
+
const tail = tailIdx >= 0 ? Number(argv[tailIdx + 1]) : undefined
|
|
51
|
+
const agentIdx = argv.indexOf('--agent')
|
|
52
|
+
const agent = agentIdx >= 0 ? argv[agentIdx + 1] : undefined
|
|
53
|
+
await runLogs({ agent, tail })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
case 'model': {
|
|
57
|
+
const { runModel } = await import('./commands/model')
|
|
58
|
+
await runModel()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
case 'drain': {
|
|
62
|
+
const toIdx = argv.indexOf('--to')
|
|
63
|
+
const to = toIdx >= 0 ? argv[toIdx + 1] : undefined
|
|
64
|
+
const yes = argv.includes('--yes') || argv.includes('-y')
|
|
65
|
+
const { runDrain } = await import('./commands/drain')
|
|
66
|
+
await runDrain({ to, yes })
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
case 'identity': {
|
|
70
|
+
const { parseIdentityArgs, runIdentity } = await import('./commands/identity')
|
|
71
|
+
const parsed = parseIdentityArgs(argv.slice(1))
|
|
72
|
+
if ('error' in parsed) {
|
|
73
|
+
console.error(`nebula identity: ${parsed.error}`)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
await runIdentity(parsed)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
case 'reputation':
|
|
80
|
+
case 'validation': {
|
|
81
|
+
const { parseTrustArgs, runTrust } = await import('./commands/trust')
|
|
82
|
+
const parsed = parseTrustArgs(sub, argv.slice(1))
|
|
83
|
+
if ('error' in parsed) {
|
|
84
|
+
console.error(`nebula ${sub}: ${parsed.error}`)
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
await runTrust(parsed)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
case 'telegram': {
|
|
91
|
+
const { parseTelegramArgs, runTelegram } = await import('./commands/telegram')
|
|
92
|
+
const parsed = parseTelegramArgs(argv.slice(1))
|
|
93
|
+
if ('error' in parsed) {
|
|
94
|
+
console.error(`nebula telegram: ${parsed.error}`)
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
await runTelegram(parsed)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
case 'pairing': {
|
|
101
|
+
const { parsePairingArgs, runPairing } = await import('./commands/pairing')
|
|
102
|
+
const parsed = parsePairingArgs(argv.slice(1))
|
|
103
|
+
if ('error' in parsed) {
|
|
104
|
+
console.error(`nebula pairing: ${parsed.error}`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
await runPairing(parsed)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
case 'gateway': {
|
|
111
|
+
const { parseGatewayArgs, runGateway } = await import('./commands/gateway')
|
|
112
|
+
const parsed = parseGatewayArgs(argv.slice(1))
|
|
113
|
+
if ('error' in parsed) {
|
|
114
|
+
console.error(`nebula gateway: ${parsed.error}`)
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
await runGateway(parsed)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
case '-h':
|
|
121
|
+
case '--help':
|
|
122
|
+
case 'help': {
|
|
123
|
+
printHelp()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
case '-v':
|
|
127
|
+
case '--version':
|
|
128
|
+
case 'version': {
|
|
129
|
+
const { resolveCliVersion } = await import('./util/cli-version')
|
|
130
|
+
const v = await resolveCliVersion()
|
|
131
|
+
console.log(v)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
default: {
|
|
135
|
+
console.log(`Unknown command: ${sub}`)
|
|
136
|
+
printHelp()
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printHelp(): void {
|
|
143
|
+
console.log(
|
|
144
|
+
[
|
|
145
|
+
'nebula: a Mantle-native, policy-aware AI treasury assistant',
|
|
146
|
+
'',
|
|
147
|
+
'Commands:',
|
|
148
|
+
' nebula init bootstrap a new agent identity + local keystore',
|
|
149
|
+
' nebula [--yolo] interactive chat with your agent (default; --yolo skips approvals)',
|
|
150
|
+
' nebula status show agent + wallet + config state',
|
|
151
|
+
' nebula login unlock with a password profile (no per-command operator sign)',
|
|
152
|
+
' nebula logout clear the login session',
|
|
153
|
+
' nebula agent show your deterministic agent wallet (same as the web console)',
|
|
154
|
+
' nebula logs tail the activity log (flags: --tail N, --agent <id>)',
|
|
155
|
+
' nebula drain --to <addr> sweep agent EOA balance to address (default: operator)',
|
|
156
|
+
' nebula model re-pick the brain model',
|
|
157
|
+
' nebula identity <sub> ERC-8004 agent identity (subs: card | register | show)',
|
|
158
|
+
' nebula reputation <sub> ERC-8004 reputation (subs: show | give)',
|
|
159
|
+
' nebula validation <sub> ERC-8004 validation (subs: show | request | respond)',
|
|
160
|
+
' nebula telegram <sub> configure phone-DM gateway (subs: setup | status | remove)',
|
|
161
|
+
' nebula pairing <sub> manage DM pairing approvals (subs: list | approve | revoke | clear-pending)',
|
|
162
|
+
' usage: nebula pairing approve telegram <code>',
|
|
163
|
+
' nebula gateway <sub> always-on agent gateway daemon (subs: run | start | stop | restart | status | logs)',
|
|
164
|
+
' run = foreground, start = bg + Touch ID, stop = SIGTERM via lock',
|
|
165
|
+
' nebula version print CLI version (aliases: --version, -v)',
|
|
166
|
+
' nebula help show this message (aliases: --help, -h)',
|
|
167
|
+
'',
|
|
168
|
+
].join('\n'),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main()
|
|
173
|
+
.then(() => {
|
|
174
|
+
// Force-exit on success because some SDKs (e.g. the WalletConnect relay)
|
|
175
|
+
// leak open handles (websockets, heartbeat timers) we have no hooks to
|
|
176
|
+
// drain. Without this, one-shot commands like `nebula init` would hang at
|
|
177
|
+
// the prompt indefinitely after their work completed. `chat` returns only
|
|
178
|
+
// when the user actually quits, so this also gives chat a clean exit.
|
|
179
|
+
process.exit(0)
|
|
180
|
+
})
|
|
181
|
+
.catch(e => {
|
|
182
|
+
console.error('fatal:', (e as Error).message)
|
|
183
|
+
process.exit(1)
|
|
184
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password-based encryption for the CLI profile. The agent private key is
|
|
3
|
+
* encrypted at rest with a key derived from the operator's password via scrypt,
|
|
4
|
+
* sealed with AES-256-GCM (authenticated — a wrong password fails to decrypt
|
|
5
|
+
* rather than returning garbage). No plaintext key is ever written to the
|
|
6
|
+
* profile file.
|
|
7
|
+
*/
|
|
8
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto'
|
|
9
|
+
|
|
10
|
+
export interface ProfileCipher {
|
|
11
|
+
v: 1
|
|
12
|
+
kdf: 'scrypt'
|
|
13
|
+
/** scrypt cost params. */
|
|
14
|
+
n: number
|
|
15
|
+
r: number
|
|
16
|
+
p: number
|
|
17
|
+
salt: string // hex
|
|
18
|
+
iv: string // hex
|
|
19
|
+
ct: string // hex (ciphertext)
|
|
20
|
+
tag: string // hex (GCM auth tag)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const N = 1 << 15 // 32768
|
|
24
|
+
const R = 8
|
|
25
|
+
const P = 1
|
|
26
|
+
const KEYLEN = 32
|
|
27
|
+
// 128 * N * r ≈ 33.5 MB; default maxmem (32 MB) is just under, so bump it.
|
|
28
|
+
const MAXMEM = 96 * 1024 * 1024
|
|
29
|
+
|
|
30
|
+
function deriveKey(password: string, salt: Buffer): Buffer {
|
|
31
|
+
return scryptSync(password.normalize('NFKC'), salt, KEYLEN, { N, r: R, p: P, maxmem: MAXMEM })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Encrypt a secret string (e.g. a 0x private key) under a password. */
|
|
35
|
+
export function encryptSecret(secret: string, password: string): ProfileCipher {
|
|
36
|
+
const salt = randomBytes(16)
|
|
37
|
+
const iv = randomBytes(12)
|
|
38
|
+
const key = deriveKey(password, salt)
|
|
39
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
40
|
+
const ct = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()])
|
|
41
|
+
const tag = cipher.getAuthTag()
|
|
42
|
+
return {
|
|
43
|
+
v: 1,
|
|
44
|
+
kdf: 'scrypt',
|
|
45
|
+
n: N,
|
|
46
|
+
r: R,
|
|
47
|
+
p: P,
|
|
48
|
+
salt: salt.toString('hex'),
|
|
49
|
+
iv: iv.toString('hex'),
|
|
50
|
+
ct: ct.toString('hex'),
|
|
51
|
+
tag: tag.toString('hex'),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Decrypt a ProfileCipher with a password. Throws on a wrong password (GCM auth failure). */
|
|
56
|
+
export function decryptSecret(blob: ProfileCipher, password: string): string {
|
|
57
|
+
const salt = Buffer.from(blob.salt, 'hex')
|
|
58
|
+
const key = scryptSync(password.normalize('NFKC'), salt, KEYLEN, {
|
|
59
|
+
N: blob.n,
|
|
60
|
+
r: blob.r,
|
|
61
|
+
p: blob.p,
|
|
62
|
+
maxmem: MAXMEM,
|
|
63
|
+
})
|
|
64
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(blob.iv, 'hex'))
|
|
65
|
+
decipher.setAuthTag(Buffer.from(blob.tag, 'hex'))
|
|
66
|
+
const out = Buffer.concat([decipher.update(Buffer.from(blob.ct, 'hex')), decipher.final()])
|
|
67
|
+
return out.toString('utf8')
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic agent wallet derivation — MUST stay byte-for-byte identical to
|
|
3
|
+
* the web (apps/web/lib/agent-wallet.ts) so the same operator/main wallet
|
|
4
|
+
* resolves to the SAME agent wallet in the CLI and the browser.
|
|
5
|
+
*
|
|
6
|
+
* The operator signs the fixed message; keccak256 of that signature is the agent
|
|
7
|
+
* private key. Relies on deterministic (RFC-6979) ECDSA, which viem and most
|
|
8
|
+
* wallets use.
|
|
9
|
+
*/
|
|
10
|
+
import { type Hex, keccak256 } from 'viem'
|
|
11
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
12
|
+
|
|
13
|
+
// KEEP IN SYNC with apps/web/lib/agent-wallet.ts AGENT_DERIVE_MESSAGE.
|
|
14
|
+
export const AGENT_DERIVE_MESSAGE =
|
|
15
|
+
'nebula · derive my agent wallet (v1)\n\n' +
|
|
16
|
+
'Signing this proves you own this wallet and unlocks your deterministic Mantle ' +
|
|
17
|
+
'agent wallet. This signature IS your agent key — only ever sign it on nebula.'
|
|
18
|
+
|
|
19
|
+
export function deriveAgentKeyFromSignature(signature: Hex): Hex {
|
|
20
|
+
return keccak256(signature)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deriveAgentAccountFromSignature(signature: Hex) {
|
|
24
|
+
return privateKeyToAccount(deriveAgentKeyFromSignature(signature))
|
|
25
|
+
}
|