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.
Files changed (60) hide show
  1. package/README.md +39 -0
  2. package/bin/nebula +11 -0
  3. package/package.json +50 -0
  4. package/src/commands/_agents.ts +14 -0
  5. package/src/commands/_unlock.ts +66 -0
  6. package/src/commands/agent-wallet.ts +90 -0
  7. package/src/commands/chat-telegram.ts +398 -0
  8. package/src/commands/chat.tsx +1308 -0
  9. package/src/commands/drain.ts +90 -0
  10. package/src/commands/gateway-logs.ts +49 -0
  11. package/src/commands/gateway-run.ts +42 -0
  12. package/src/commands/gateway-start.ts +216 -0
  13. package/src/commands/gateway-status.ts +90 -0
  14. package/src/commands/gateway-stop.ts +133 -0
  15. package/src/commands/gateway.ts +101 -0
  16. package/src/commands/identity.ts +178 -0
  17. package/src/commands/init/cost.ts +40 -0
  18. package/src/commands/init/funding-gate.ts +64 -0
  19. package/src/commands/init/model-picker.ts +25 -0
  20. package/src/commands/init/operator-picker.ts +233 -0
  21. package/src/commands/init/telegram-step.ts +245 -0
  22. package/src/commands/init/wizard-state.ts +94 -0
  23. package/src/commands/init.ts +439 -0
  24. package/src/commands/login.ts +86 -0
  25. package/src/commands/logs.ts +37 -0
  26. package/src/commands/model.ts +48 -0
  27. package/src/commands/pairing-approve.ts +65 -0
  28. package/src/commands/pairing-clear.ts +39 -0
  29. package/src/commands/pairing-list.ts +55 -0
  30. package/src/commands/pairing-revoke.ts +49 -0
  31. package/src/commands/pairing.ts +81 -0
  32. package/src/commands/status.ts +44 -0
  33. package/src/commands/telegram-remove.ts +62 -0
  34. package/src/commands/telegram-setup.ts +64 -0
  35. package/src/commands/telegram-status.ts +87 -0
  36. package/src/commands/telegram.ts +44 -0
  37. package/src/commands/trust.ts +196 -0
  38. package/src/config/load.ts +35 -0
  39. package/src/config/render.ts +99 -0
  40. package/src/index.ts +184 -0
  41. package/src/profile/crypto.ts +68 -0
  42. package/src/profile/derive.ts +25 -0
  43. package/src/profile/store.ts +86 -0
  44. package/src/profile/unlock.ts +29 -0
  45. package/src/ui/app.tsx +719 -0
  46. package/src/ui/approval-summary.ts +32 -0
  47. package/src/ui/markdown-parse.ts +219 -0
  48. package/src/ui/markdown.tsx +37 -0
  49. package/src/ui/state.ts +181 -0
  50. package/src/util/bootstrap-mode.ts +25 -0
  51. package/src/util/bootstrap-progress-box.ts +378 -0
  52. package/src/util/cli-version.ts +28 -0
  53. package/src/util/format.ts +11 -0
  54. package/src/util/gateway-spawn.ts +125 -0
  55. package/src/util/gateway-version.ts +154 -0
  56. package/src/util/github-releases.ts +79 -0
  57. package/src/util/profile-key.ts +25 -0
  58. package/src/util/ref-resolver.ts +55 -0
  59. package/src/util/silence-console.ts +40 -0
  60. 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
+ }