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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes-aligned Telegram setup wizard step. Shared by `nebula telegram setup`
|
|
3
|
+
* (standalone) and the optional Phase E in `nebula init` (right after Phase D
|
|
4
|
+
* summary, reusing the in-flight operator wallet so we don't prompt Touch ID
|
|
5
|
+
* twice).
|
|
6
|
+
*
|
|
7
|
+
* Flow (matches `~/.hermes/hermes-agent/hermes_cli/{setup.py:1720, gateway.py:1939}`):
|
|
8
|
+
* 1. Bot token (password input + `getMe` probe).
|
|
9
|
+
* 2. Auth-mode select: pair (default) or allowlist.
|
|
10
|
+
* 3. Allowlist branch: text prompt for IDs + @userinfobot hint.
|
|
11
|
+
* 4. Encrypt + save secrets to `~/.nebula/agents/<id>/telegram-secrets.encrypted`.
|
|
12
|
+
* 5. Merge `'telegram'` into config.plugins; rewrite `~/.nebula/config.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Caller frames its own intro/outro. This helper is content-only.
|
|
15
|
+
*/
|
|
16
|
+
import { cancel, confirm, isCancel, note, password, select, spinner, text } from '@clack/prompts'
|
|
17
|
+
import {
|
|
18
|
+
type NebulaConfig,
|
|
19
|
+
type NebulaNetwork,
|
|
20
|
+
type NebulaPlugin,
|
|
21
|
+
OPERATOR_BLOB_SCOPES,
|
|
22
|
+
type OperatorSigner,
|
|
23
|
+
agentPaths,
|
|
24
|
+
deriveBlobKey,
|
|
25
|
+
} from 'nebula-ai-core'
|
|
26
|
+
import { type Address, type Hex, bytesToHex } from 'viem'
|
|
27
|
+
import { writeConfigTs } from '../../config/render'
|
|
28
|
+
import {
|
|
29
|
+
fetchBotInfo,
|
|
30
|
+
looksLikeBotToken,
|
|
31
|
+
parseAllowedUserIds,
|
|
32
|
+
saveTelegramSecrets,
|
|
33
|
+
telegramSecretsExist,
|
|
34
|
+
} from '../../util/telegram-secrets'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the runtime plugin list when telegram is configured. Auto-includes
|
|
38
|
+
* `'telegram'` so the listener is registered; otherwise `build-runtime.ts`
|
|
39
|
+
* would gate it on `pluginNames.includes('telegram')` and skip registration.
|
|
40
|
+
* Default base: `['system', 'comms', 'onchain']`. Idempotent.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveHandoffPlugins(
|
|
43
|
+
caller: NebulaPlugin[] | undefined,
|
|
44
|
+
shipsTelegramSecrets: boolean,
|
|
45
|
+
): NebulaPlugin[] {
|
|
46
|
+
const base = caller ?? (['system', 'comms', 'onchain'] satisfies NebulaPlugin[])
|
|
47
|
+
if (!shipsTelegramSecrets) return base
|
|
48
|
+
if (base.includes('telegram')) return base
|
|
49
|
+
return [...base, 'telegram']
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TelegramAuthMode = 'pair' | 'allowlist'
|
|
53
|
+
|
|
54
|
+
export interface TelegramStepOpts {
|
|
55
|
+
/** Already-unlocked operator wallet. Caller is responsible for closing it. */
|
|
56
|
+
signer: OperatorSigner
|
|
57
|
+
agentId: string
|
|
58
|
+
agentAddress: Address
|
|
59
|
+
configPath: string
|
|
60
|
+
config: NebulaConfig
|
|
61
|
+
network: NebulaNetwork
|
|
62
|
+
/**
|
|
63
|
+
* If true, the helper is allowed to ask whether to overwrite an existing
|
|
64
|
+
* blob via `confirm`. Default true. Set false for fully non-interactive
|
|
65
|
+
* test paths.
|
|
66
|
+
*/
|
|
67
|
+
allowOverwrite?: boolean
|
|
68
|
+
/**
|
|
69
|
+
* v0.24.4: when true, do NOT write the config file from inside this step —
|
|
70
|
+
* caller (init.ts) builds the final cfg with `'telegram'` in plugins and
|
|
71
|
+
* writes once. Avoids the partial-write hazard where Phase E runs before
|
|
72
|
+
* the init's main config build and the intermediate write has incomplete
|
|
73
|
+
* identity/sandbox fields. Standalone `nebula telegram setup` keeps the
|
|
74
|
+
* default false so it still rewrites the config.
|
|
75
|
+
*/
|
|
76
|
+
skipConfigWrite?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TelegramStepResult {
|
|
80
|
+
configured: boolean
|
|
81
|
+
/** Set when `configured: true`. */
|
|
82
|
+
botUsername?: string
|
|
83
|
+
modeUsed?: TelegramAuthMode
|
|
84
|
+
allowedUserIds?: number[]
|
|
85
|
+
/** Set when configured aborted by user (cancel / no-overwrite). */
|
|
86
|
+
cancelled?: boolean
|
|
87
|
+
/**
|
|
88
|
+
* v0.24.3: derived TELEGRAM scope key as 0x-prefixed hex. Caller stashes
|
|
89
|
+
* this in `.operator-session` so the daemon auto-spawns without re-prompting
|
|
90
|
+
* Touch ID. Hex (not Buffer) to match `OperatorSessionKeys`' on-disk shape.
|
|
91
|
+
*/
|
|
92
|
+
telegramScopeKeyHex?: Hex
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const PAIR_OPTION_LABEL =
|
|
96
|
+
'Pair (recommended) — unknown DM users get an 8-char code; you approve via CLI'
|
|
97
|
+
const ALLOW_OPTION_LABEL =
|
|
98
|
+
'Allowlist — only listed numeric Telegram IDs can DM the bot (find yours via @userinfobot)'
|
|
99
|
+
|
|
100
|
+
export async function runTelegramStep(opts: TelegramStepOpts): Promise<TelegramStepResult> {
|
|
101
|
+
if (telegramSecretsExist(opts.agentId)) {
|
|
102
|
+
if (opts.allowOverwrite === false) {
|
|
103
|
+
return { configured: false, cancelled: true }
|
|
104
|
+
}
|
|
105
|
+
const overwrite = await confirm({
|
|
106
|
+
message:
|
|
107
|
+
'Encrypted telegram-secrets blob already exists for this agent. Overwrite with new settings?',
|
|
108
|
+
initialValue: false,
|
|
109
|
+
})
|
|
110
|
+
if (isCancel(overwrite) || overwrite !== true) {
|
|
111
|
+
return { configured: false, cancelled: true }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tokenRaw = (await password({
|
|
116
|
+
message: 'Bot token from @BotFather',
|
|
117
|
+
validate: v => {
|
|
118
|
+
if (!v) return 'Required.'
|
|
119
|
+
if (!looksLikeBotToken(v))
|
|
120
|
+
return 'Looks malformed. Expected `<id>:<secret>` from @BotFather, e.g. 1234567890:AABBCC...'
|
|
121
|
+
return undefined
|
|
122
|
+
},
|
|
123
|
+
})) as string | symbol
|
|
124
|
+
if (isCancel(tokenRaw)) {
|
|
125
|
+
return { configured: false, cancelled: true }
|
|
126
|
+
}
|
|
127
|
+
const botToken = (tokenRaw as string).trim()
|
|
128
|
+
|
|
129
|
+
const sValidate = spinner()
|
|
130
|
+
sValidate.start('Validating token via api.telegram.org/getMe')
|
|
131
|
+
let botInfo: Awaited<ReturnType<typeof fetchBotInfo>>
|
|
132
|
+
try {
|
|
133
|
+
botInfo = await fetchBotInfo(botToken)
|
|
134
|
+
sValidate.stop(`bot ok: @${botInfo.username} (id ${botInfo.id})`)
|
|
135
|
+
} catch (e) {
|
|
136
|
+
sValidate.stop(`token rejected: ${(e as Error).message.slice(0, 200)}`)
|
|
137
|
+
cancel('Bad token. Re-issue via /token in @BotFather and re-run setup.')
|
|
138
|
+
return { configured: false, cancelled: true }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const modeChoice = await select({
|
|
142
|
+
message: 'How should unauthorized DMs to the bot be handled?',
|
|
143
|
+
options: [
|
|
144
|
+
{ value: 'pair' as TelegramAuthMode, label: PAIR_OPTION_LABEL },
|
|
145
|
+
{ value: 'allowlist' as TelegramAuthMode, label: ALLOW_OPTION_LABEL },
|
|
146
|
+
],
|
|
147
|
+
initialValue: 'pair' as TelegramAuthMode,
|
|
148
|
+
})
|
|
149
|
+
if (isCancel(modeChoice)) {
|
|
150
|
+
return { configured: false, cancelled: true }
|
|
151
|
+
}
|
|
152
|
+
const mode = modeChoice as TelegramAuthMode
|
|
153
|
+
|
|
154
|
+
let allowedUserIds: number[] = []
|
|
155
|
+
if (mode === 'allowlist') {
|
|
156
|
+
const allowedRaw = (await text({
|
|
157
|
+
message: 'Allowed Telegram user IDs (comma-separated)',
|
|
158
|
+
placeholder: '123456789, 987654321',
|
|
159
|
+
defaultValue: '',
|
|
160
|
+
validate: v => {
|
|
161
|
+
if (!v) return 'At least one numeric id required (or pick Pair mode instead).'
|
|
162
|
+
const parsed = parseAllowedUserIds(v)
|
|
163
|
+
if (!parsed.ok) return parsed.reason
|
|
164
|
+
if (parsed.ids.length === 0) return 'At least one numeric id required.'
|
|
165
|
+
return undefined
|
|
166
|
+
},
|
|
167
|
+
})) as string | symbol
|
|
168
|
+
if (isCancel(allowedRaw)) {
|
|
169
|
+
return { configured: false, cancelled: true }
|
|
170
|
+
}
|
|
171
|
+
const parsed = parseAllowedUserIds(typeof allowedRaw === 'string' ? allowedRaw : '')
|
|
172
|
+
if (!parsed.ok || parsed.ids.length === 0) {
|
|
173
|
+
cancel(`bad allowed list: ${parsed.ok ? 'empty' : parsed.reason}`)
|
|
174
|
+
return { configured: false, cancelled: true }
|
|
175
|
+
}
|
|
176
|
+
allowedUserIds = parsed.ids
|
|
177
|
+
note(
|
|
178
|
+
`Approved on day one: ${allowedUserIds.join(', ')}\nThese users can DM @${botInfo.username} immediately. Anyone else still falls into pairing.`,
|
|
179
|
+
'allowlist',
|
|
180
|
+
)
|
|
181
|
+
} else {
|
|
182
|
+
note(
|
|
183
|
+
`Default-deny is on: any unknown user who DMs @${botInfo.username}\nwill receive a one-time pairing code. Approve them out-of-band:\n nebula pairing approve telegram <CODE>\nTo skip pairing for yourself, re-run setup, pick Allowlist, and paste your numeric id\n(get it from @userinfobot).`,
|
|
184
|
+
'pairing mode',
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// v0.24.3: derive TELEGRAM key explicitly so we can both pass it as
|
|
189
|
+
// `precomputedKey` (skip the redundant sign inside encryptOperatorBlob)
|
|
190
|
+
// AND return it to init.ts to stash in `.operator-session`.
|
|
191
|
+
const sDerive = spinner()
|
|
192
|
+
sDerive.start('Deriving TELEGRAM scope key')
|
|
193
|
+
let telegramScopeKey: Buffer
|
|
194
|
+
try {
|
|
195
|
+
telegramScopeKey = await deriveBlobKey(
|
|
196
|
+
opts.signer,
|
|
197
|
+
opts.agentAddress,
|
|
198
|
+
OPERATOR_BLOB_SCOPES.TELEGRAM,
|
|
199
|
+
)
|
|
200
|
+
sDerive.stop('TELEGRAM scope key derived')
|
|
201
|
+
} catch (e) {
|
|
202
|
+
sDerive.stop(`TELEGRAM scope derive failed: ${(e as Error).message.slice(0, 200)}`)
|
|
203
|
+
return { configured: false, cancelled: true }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const sSave = spinner()
|
|
207
|
+
sSave.start('Encrypting + saving telegram secrets locally')
|
|
208
|
+
try {
|
|
209
|
+
await saveTelegramSecrets({
|
|
210
|
+
signer: opts.signer,
|
|
211
|
+
agentAddress: opts.agentAddress,
|
|
212
|
+
agentId: opts.agentId,
|
|
213
|
+
plaintext: {
|
|
214
|
+
botToken,
|
|
215
|
+
botUsername: botInfo.username,
|
|
216
|
+
botId: botInfo.id,
|
|
217
|
+
allowedUserIds,
|
|
218
|
+
},
|
|
219
|
+
precomputedKey: telegramScopeKey,
|
|
220
|
+
})
|
|
221
|
+
sSave.stop(`saved → ${agentPaths.agent(opts.agentId).dir}/telegram-secrets.encrypted`)
|
|
222
|
+
} catch (e) {
|
|
223
|
+
sSave.stop(`save failed: ${(e as Error).message.slice(0, 200)}`)
|
|
224
|
+
return { configured: false, cancelled: true }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// v0.24.4: when caller asks (init.ts), skip the config rewrite — caller will
|
|
228
|
+
// build the final cfg with `'telegram'` in plugins and write once. Avoids the
|
|
229
|
+
// partial-write hazard where Phase E runs before init's main config build.
|
|
230
|
+
if (!opts.skipConfigWrite) {
|
|
231
|
+
const plugins = resolveHandoffPlugins(opts.config.plugins, true)
|
|
232
|
+
if (plugins.length !== (opts.config.plugins ?? []).length) {
|
|
233
|
+
const updated = { ...opts.config, plugins }
|
|
234
|
+
await writeConfigTs(opts.configPath, updated)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
configured: true,
|
|
240
|
+
botUsername: botInfo.username,
|
|
241
|
+
modeUsed: mode,
|
|
242
|
+
allowedUserIds,
|
|
243
|
+
telegramScopeKeyHex: bytesToHex(telegramScopeKey),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pattern B resumable-init state file (Apr 24 2026 session design).
|
|
7
|
+
*
|
|
8
|
+
* Lives at `<agentDir>/.nebula-init-state.json` and tracks which steps in
|
|
9
|
+
* Phase C of the wizard completed. Written incrementally. If init crashes
|
|
10
|
+
* or the user aborts mid-flow, a subsequent `nebula init` (or `--resume`)
|
|
11
|
+
* can pick up from the first incomplete step instead of re-minting.
|
|
12
|
+
*/
|
|
13
|
+
export interface WizardState {
|
|
14
|
+
version: 1
|
|
15
|
+
agentAddress: `0x${string}`
|
|
16
|
+
network: 'mantle-mainnet' | 'mantle-testnet'
|
|
17
|
+
steps: {
|
|
18
|
+
keystoreSaved: boolean
|
|
19
|
+
mintedTokenId: string | null
|
|
20
|
+
mintedContract: string | null
|
|
21
|
+
mintTx: string | null
|
|
22
|
+
agentFundedTx: string | null
|
|
23
|
+
keystorePersistedTx: string | null
|
|
24
|
+
keystoreRootHash: string | null
|
|
25
|
+
ledgerOpenedTx: boolean // broker.addLedger returns void
|
|
26
|
+
subnameClaimedTx: string | null
|
|
27
|
+
textRecordsSetTx: string | null
|
|
28
|
+
/** Phase 11: Mantle Sandbox lifecycle. Set during sandbox-deploy branch. */
|
|
29
|
+
sandboxId: string | null
|
|
30
|
+
sandboxEndpoint: string | null
|
|
31
|
+
}
|
|
32
|
+
lastError: string | null
|
|
33
|
+
updatedAt: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const WIZARD_STATE_FILENAME = '.nebula-init-state.json'
|
|
37
|
+
|
|
38
|
+
export function wizardStatePath(agentDir: string): string {
|
|
39
|
+
return join(agentDir, WIZARD_STATE_FILENAME)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function initialWizardState(
|
|
43
|
+
agentAddress: `0x${string}`,
|
|
44
|
+
network: 'mantle-mainnet' | 'mantle-testnet',
|
|
45
|
+
): WizardState {
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
agentAddress,
|
|
49
|
+
network,
|
|
50
|
+
steps: {
|
|
51
|
+
keystoreSaved: false,
|
|
52
|
+
mintedTokenId: null,
|
|
53
|
+
mintedContract: null,
|
|
54
|
+
mintTx: null,
|
|
55
|
+
agentFundedTx: null,
|
|
56
|
+
keystorePersistedTx: null,
|
|
57
|
+
keystoreRootHash: null,
|
|
58
|
+
ledgerOpenedTx: false,
|
|
59
|
+
subnameClaimedTx: null,
|
|
60
|
+
textRecordsSetTx: null,
|
|
61
|
+
sandboxId: null,
|
|
62
|
+
sandboxEndpoint: null,
|
|
63
|
+
},
|
|
64
|
+
lastError: null,
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function readWizardState(agentDir: string): Promise<WizardState | null> {
|
|
70
|
+
const path = wizardStatePath(agentDir)
|
|
71
|
+
if (!existsSync(path)) return null
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile(path, 'utf8')
|
|
74
|
+
return JSON.parse(raw) as WizardState
|
|
75
|
+
} catch {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function writeWizardState(agentDir: string, state: WizardState): Promise<void> {
|
|
81
|
+
state.updatedAt = new Date().toISOString()
|
|
82
|
+
await writeFile(wizardStatePath(agentDir), JSON.stringify(state, null, 2), 'utf8')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function updateWizardState(
|
|
86
|
+
agentDir: string,
|
|
87
|
+
patch: (draft: WizardState) => void,
|
|
88
|
+
): Promise<WizardState> {
|
|
89
|
+
const current = (await readWizardState(agentDir)) ?? null
|
|
90
|
+
if (!current) throw new Error(`updateWizardState: no state at ${agentDir}`)
|
|
91
|
+
patch(current)
|
|
92
|
+
await writeWizardState(agentDir, current)
|
|
93
|
+
return current
|
|
94
|
+
}
|