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,439 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { cancel, confirm, intro, isCancel, note, outro, select, spinner } from '@clack/prompts'
|
|
5
|
+
import {
|
|
6
|
+
NETWORK_CHAIN_ID,
|
|
7
|
+
NETWORK_RPC,
|
|
8
|
+
type NebulaNetwork,
|
|
9
|
+
OPERATOR_BLOB_SCOPES,
|
|
10
|
+
type OperatorSessionKeys,
|
|
11
|
+
agentPaths,
|
|
12
|
+
buildOperatorSession,
|
|
13
|
+
defineConfig,
|
|
14
|
+
generateAgentWallet,
|
|
15
|
+
getGasPriceWithFloor,
|
|
16
|
+
placeholderAgentId,
|
|
17
|
+
precomputeAllScopes,
|
|
18
|
+
saveKeystoreLocally,
|
|
19
|
+
waitForReceiptResilient,
|
|
20
|
+
writeOperatorSession,
|
|
21
|
+
} from 'nebula-ai-core'
|
|
22
|
+
import { type Address, type Hex, formatEther, hexToBytes, parseEther } from 'viem'
|
|
23
|
+
import { writeConfigTs } from '../config/render'
|
|
24
|
+
import { withSilencedConsole } from '../util/silence-console'
|
|
25
|
+
import { estimateCosts, renderCostSummary } from './init/cost'
|
|
26
|
+
import { fundingGate } from './init/funding-gate'
|
|
27
|
+
import { pickBrainModel } from './init/model-picker'
|
|
28
|
+
import { pickOperatorSigner } from './init/operator-picker'
|
|
29
|
+
import { initialWizardState, updateWizardState, writeWizardState } from './init/wizard-state'
|
|
30
|
+
|
|
31
|
+
export async function runInit(opts?: { cwd?: string; resume?: boolean }): Promise<void> {
|
|
32
|
+
const configPath = agentPaths.config
|
|
33
|
+
|
|
34
|
+
intro('nebula init')
|
|
35
|
+
|
|
36
|
+
if (existsSync(configPath) && !opts?.resume) {
|
|
37
|
+
const choice = (await select({
|
|
38
|
+
message: `${configPath} exists`,
|
|
39
|
+
options: [
|
|
40
|
+
{ value: 'overwrite', label: 'Start fresh (overwrite)' },
|
|
41
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
42
|
+
],
|
|
43
|
+
initialValue: 'cancel',
|
|
44
|
+
})) as 'overwrite' | 'cancel' | symbol
|
|
45
|
+
if (isCancel(choice) || choice === 'cancel') {
|
|
46
|
+
cancel('Aborted.')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Phase A: local prompts (no chain, no wallet) ───────────────────────
|
|
52
|
+
|
|
53
|
+
const network = (await select({
|
|
54
|
+
message: 'Which Mantle network?',
|
|
55
|
+
options: [
|
|
56
|
+
{ value: 'mantle-mainnet' as NebulaNetwork, label: 'Mantle mainnet (5000)' },
|
|
57
|
+
{ value: 'mantle-testnet' as NebulaNetwork, label: 'Mantle Sepolia testnet (5003)' },
|
|
58
|
+
],
|
|
59
|
+
initialValue: 'mantle-mainnet' as NebulaNetwork,
|
|
60
|
+
})) as NebulaNetwork
|
|
61
|
+
if (isCancel(network)) {
|
|
62
|
+
cancel('Aborted.')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The agent always runs locally: a harness on this machine, always-on while
|
|
67
|
+
// the CLI (or the local gateway daemon) is open. The remote compute-
|
|
68
|
+
// marketplace deploy target was removed.
|
|
69
|
+
const deployTarget = 'local' as const
|
|
70
|
+
|
|
71
|
+
// SANN `.nebula.0g` name service was removed (0G-only); the agent is now
|
|
72
|
+
// local-identity. No subname prompt or on-chain registration.
|
|
73
|
+
const requestedSubname: string | null = null
|
|
74
|
+
|
|
75
|
+
const modelPick = await pickBrainModel({ network })
|
|
76
|
+
if (!modelPick) {
|
|
77
|
+
const keepGoing = await confirm({
|
|
78
|
+
message: 'Model catalog unavailable; continue and pick later?',
|
|
79
|
+
initialValue: true,
|
|
80
|
+
})
|
|
81
|
+
if (isCancel(keepGoing) || !keepGoing) {
|
|
82
|
+
cancel('Aborted.')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Compute-ledger deposit prompt removed with the decentralized-compute
|
|
88
|
+
// backend. The agent's LLM is an API-key model (OPENAI_API_KEY / NEBULA_LLM_*),
|
|
89
|
+
// so there is no on-chain compute ledger to fund at init time.
|
|
90
|
+
const ledgerSize = 0
|
|
91
|
+
|
|
92
|
+
// ─── Phase B: wallet gate ────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
const picked = await pickOperatorSigner({ network })
|
|
95
|
+
if (!picked) return
|
|
96
|
+
const { signer: operator, hint: operatorHint } = picked
|
|
97
|
+
|
|
98
|
+
const sConnect = spinner()
|
|
99
|
+
sConnect.start(`Connecting via ${operator.source}`)
|
|
100
|
+
let operatorAddress: Address
|
|
101
|
+
try {
|
|
102
|
+
operatorAddress = await operator.address()
|
|
103
|
+
sConnect.stop(`operator: ${operatorAddress}`)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
sConnect.stop(`connection failed: ${(e as Error).message.slice(0, 140)}`)
|
|
106
|
+
await operator.close?.()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const costs = estimateCosts({
|
|
111
|
+
ledgerSizeOg: ledgerSize,
|
|
112
|
+
withSubname: !!requestedSubname,
|
|
113
|
+
deployTarget,
|
|
114
|
+
})
|
|
115
|
+
note(renderCostSummary(costs), 'cost summary (Mantle ~$0.50)')
|
|
116
|
+
|
|
117
|
+
const publicClient = await operator.publicClient(network)
|
|
118
|
+
const operatorBalance = await publicClient.getBalance({ address: operatorAddress })
|
|
119
|
+
|
|
120
|
+
let skipLedger = false
|
|
121
|
+
if (operatorBalance < costs.totalOperator) {
|
|
122
|
+
const need = costs.totalOperator - operatorBalance
|
|
123
|
+
note(
|
|
124
|
+
`Operator balance ${formatEther(operatorBalance)} Mantle, need ${formatEther(need)} Mantle more.`,
|
|
125
|
+
'insufficient funds',
|
|
126
|
+
)
|
|
127
|
+
const gate = await fundingGate({
|
|
128
|
+
publicClient,
|
|
129
|
+
operatorAddress,
|
|
130
|
+
requiredOg: costs.totalOperator,
|
|
131
|
+
})
|
|
132
|
+
if (gate.kind === 'cancel') {
|
|
133
|
+
await operator.close?.()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (gate.kind === 'skip-ledger') skipLedger = true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const proceed = await confirm({ message: 'Proceed?', initialValue: true })
|
|
140
|
+
if (isCancel(proceed) || !proceed) {
|
|
141
|
+
cancel('Aborted.')
|
|
142
|
+
await operator.close?.()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Phase C: execute with Pattern B state tracking ─────────────────────
|
|
147
|
+
|
|
148
|
+
const agent = generateAgentWallet()
|
|
149
|
+
const provisionalAgentId = placeholderAgentId(agent.address)
|
|
150
|
+
const provisional = agentPaths.agent(provisionalAgentId)
|
|
151
|
+
await mkdir(provisional.dir, { recursive: true })
|
|
152
|
+
|
|
153
|
+
await writeWizardState(provisional.dir, {
|
|
154
|
+
...initialWizardState(agent.address, network),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Plain-EOA identity: the agent EOA is the identity. No iNFT mint, no
|
|
158
|
+
// on-chain anchoring — just a local encrypted keystore.
|
|
159
|
+
const finalAgentId = provisionalAgentId
|
|
160
|
+
const paths = provisional
|
|
161
|
+
|
|
162
|
+
// v0.23.1: derive BOTH operator-scope keys (keystore + profile) in parallel
|
|
163
|
+
// up front, then reuse them everywhere. This is the single "two signatures
|
|
164
|
+
// back to back" moment in the wizard: keystore scope (for the encrypted
|
|
165
|
+
// privkey blob) + profile scope (for the operator-private user-partition
|
|
166
|
+
// memory slot). Folding profile derivation into init removes the v0.23.0
|
|
167
|
+
// need for `nebula profile init` as a follow-up command.
|
|
168
|
+
const sKeys = spinner()
|
|
169
|
+
sKeys.start('Deriving operator scope keys (may prompt twice: keystore + profile)')
|
|
170
|
+
let operatorKeys: OperatorSessionKeys
|
|
171
|
+
let keystoreKeyBuf: Buffer
|
|
172
|
+
try {
|
|
173
|
+
operatorKeys = await precomputeAllScopes(operator, agent.address as Address, [
|
|
174
|
+
OPERATOR_BLOB_SCOPES.PROFILE,
|
|
175
|
+
])
|
|
176
|
+
keystoreKeyBuf = Buffer.from(hexToBytes(operatorKeys.keystore))
|
|
177
|
+
sKeys.stop('scope keys derived')
|
|
178
|
+
} catch (e) {
|
|
179
|
+
sKeys.stop(`scope key derive failed: ${(e as Error).message.slice(0, 160)}`)
|
|
180
|
+
cancel('Aborted (operator signature required for keystore + profile scopes).')
|
|
181
|
+
await operator.close?.()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Pass the already-derived keystoreKey so saveKeystoreLocally skips
|
|
186
|
+
// signing again. Save BEFORE funding the agent EOA per
|
|
187
|
+
// `feedback-init-must-save-keystore-before-funding.md`.
|
|
188
|
+
const sLocal = spinner()
|
|
189
|
+
sLocal.start('Encrypting agent keystore to operator wallet (local insurance)')
|
|
190
|
+
let encryptedBytes: Uint8Array
|
|
191
|
+
try {
|
|
192
|
+
const saved = await saveKeystoreLocally({
|
|
193
|
+
agentAddress: agent.address as Address,
|
|
194
|
+
agentPrivkey: agent.privkeyHex as Hex,
|
|
195
|
+
cachePath: paths.keystore,
|
|
196
|
+
precomputedKey: keystoreKeyBuf,
|
|
197
|
+
})
|
|
198
|
+
encryptedBytes = saved.bytes
|
|
199
|
+
await updateWizardState(paths.dir, draft => {
|
|
200
|
+
draft.steps.keystoreSaved = true
|
|
201
|
+
})
|
|
202
|
+
sLocal.stop(`keystore saved locally at ${paths.keystore}`)
|
|
203
|
+
} catch (e) {
|
|
204
|
+
sLocal.stop(`local keystore save failed: ${(e as Error).message.slice(0, 120)}`)
|
|
205
|
+
cancel('Aborted before funding (keystore encryption failed).')
|
|
206
|
+
await operator.close?.()
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sFund = spinner()
|
|
211
|
+
const fundingAmount = parseEther('0.1') + parseEther(String(ledgerSize))
|
|
212
|
+
sFund.start(`Funding agent ${agent.address} with ${formatEther(fundingAmount)} Mantle`)
|
|
213
|
+
try {
|
|
214
|
+
const opWc = await operator.walletClient(network)
|
|
215
|
+
const opAccount = opWc.account
|
|
216
|
+
if (!opAccount) throw new Error('walletClient is missing default account')
|
|
217
|
+
const fundGasPrice = await getGasPriceWithFloor(publicClient)
|
|
218
|
+
const fundTx = await withSilencedConsole(() =>
|
|
219
|
+
opWc.sendTransaction({
|
|
220
|
+
to: agent.address as Address,
|
|
221
|
+
value: fundingAmount,
|
|
222
|
+
chain: operator.chain(network),
|
|
223
|
+
account: opAccount,
|
|
224
|
+
maxFeePerGas: fundGasPrice,
|
|
225
|
+
maxPriorityFeePerGas: fundGasPrice,
|
|
226
|
+
}),
|
|
227
|
+
)
|
|
228
|
+
await waitForReceiptResilient(publicClient, fundTx)
|
|
229
|
+
await updateWizardState(paths.dir, draft => {
|
|
230
|
+
draft.steps.agentFundedTx = fundTx
|
|
231
|
+
})
|
|
232
|
+
sFund.stop(`funded (tx ${fundTx})`)
|
|
233
|
+
} catch (e) {
|
|
234
|
+
sFund.stop(`fund failed: ${(e as Error).message}`)
|
|
235
|
+
await operator.close?.()
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The encrypted keystore is on disk (saved before funding). The operator
|
|
240
|
+
// wallet can always decrypt + recover the agent — no on-chain anchor needed.
|
|
241
|
+
void encryptedBytes
|
|
242
|
+
|
|
243
|
+
// v0.23.1: cache the operator scope keys to `.operator-session` so:
|
|
244
|
+
// - First `nebula` chat does NOT re-prompt Touch ID (`gateway-start` will
|
|
245
|
+
// find both keystore + profile scopes already cached and skip
|
|
246
|
+
// re-derivation).
|
|
247
|
+
// - First sync after init can encrypt + anchor the PROFILE slot
|
|
248
|
+
// transparently — operator never needs to run `nebula profile init`.
|
|
249
|
+
// requiredScopesForAgent now returns ['keystore', 'nebula-profile-v1']
|
|
250
|
+
// because seedStarterMemoryFiles just wrote user/profile.md.
|
|
251
|
+
try {
|
|
252
|
+
const sess = buildOperatorSession({ agent: agent.address as Address, keys: operatorKeys })
|
|
253
|
+
writeOperatorSession(finalAgentId, sess)
|
|
254
|
+
} catch (e) {
|
|
255
|
+
console.warn(`operator-session write skipped: ${(e as Error).message.slice(0, 160)}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Compute-ledger prepay step removed with the decentralized-compute backend
|
|
259
|
+
// (Nebula uses an API-key LLM; no per-provider on-chain ledger to fund).
|
|
260
|
+
|
|
261
|
+
// SANN `.nebula.0g` registration was removed (0G-only); the agent is
|
|
262
|
+
// local-identity, so there is no on-chain subname to claim.
|
|
263
|
+
const registeredSubname: string | null = null
|
|
264
|
+
|
|
265
|
+
// Seed canonical memory starter files. With no SANN subname the seed uses
|
|
266
|
+
// the generic "I am nebula" template.
|
|
267
|
+
await seedStarterMemoryFiles({
|
|
268
|
+
paths,
|
|
269
|
+
network,
|
|
270
|
+
contractAddress: '0x0000000000000000000000000000000000000000' as Address,
|
|
271
|
+
tokenId: 0n,
|
|
272
|
+
agentAddress: agent.address as Address,
|
|
273
|
+
operatorAddress,
|
|
274
|
+
brainProvider: modelPick?.provider ?? null,
|
|
275
|
+
brainModel: modelPick?.model ?? null,
|
|
276
|
+
subname: registeredSubname,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// v0.24.4: Phase E (Telegram bot setup) MUST run before Phase 11 (sandbox
|
|
280
|
+
// provision) so the sandbox handoff envelope can ship `telegram-secrets`
|
|
281
|
+
// and the listener boots active. Previously Phase E ran AFTER provision and
|
|
282
|
+
// the sandbox booted with `listeners.telegram: disabled`, forcing the
|
|
283
|
+
// operator to `nebula upgrade --in-place` post-init to re-ship secrets.
|
|
284
|
+
let telegramConfigured: { botUsername: string; mode: string } | null = null
|
|
285
|
+
{
|
|
286
|
+
const tgChoice = await confirm({
|
|
287
|
+
message: 'Configure a Telegram bot for this agent now? (recommended)',
|
|
288
|
+
initialValue: true,
|
|
289
|
+
})
|
|
290
|
+
if (!isCancel(tgChoice) && tgChoice === true) {
|
|
291
|
+
try {
|
|
292
|
+
const { runTelegramStep } = await import('./init/telegram-step')
|
|
293
|
+
const tgResult = await runTelegramStep({
|
|
294
|
+
signer: operator,
|
|
295
|
+
agentId: finalAgentId,
|
|
296
|
+
agentAddress: agent.address as Address,
|
|
297
|
+
configPath,
|
|
298
|
+
// Synthetic partial cfg — caller writes the final cfg below. Pass
|
|
299
|
+
// skipConfigWrite=true so telegram-step doesn't touch disk.
|
|
300
|
+
config: { plugins: [], subname: registeredSubname } as never,
|
|
301
|
+
network,
|
|
302
|
+
skipConfigWrite: true,
|
|
303
|
+
})
|
|
304
|
+
if (tgResult.configured && tgResult.botUsername && tgResult.modeUsed) {
|
|
305
|
+
telegramConfigured = {
|
|
306
|
+
botUsername: tgResult.botUsername,
|
|
307
|
+
mode: tgResult.modeUsed,
|
|
308
|
+
}
|
|
309
|
+
// v0.24.3: append TELEGRAM key to `.operator-session` so the gateway
|
|
310
|
+
// daemon auto-spawns on first chat without re-prompting Touch ID.
|
|
311
|
+
if (tgResult.telegramScopeKeyHex) {
|
|
312
|
+
try {
|
|
313
|
+
const sess = buildOperatorSession({
|
|
314
|
+
agent: agent.address as Address,
|
|
315
|
+
keys: {
|
|
316
|
+
...operatorKeys,
|
|
317
|
+
[OPERATOR_BLOB_SCOPES.TELEGRAM]: tgResult.telegramScopeKeyHex,
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
writeOperatorSession(finalAgentId, sess)
|
|
321
|
+
} catch (e) {
|
|
322
|
+
note(
|
|
323
|
+
`operator-session rewrite skipped: ${(e as Error).message.slice(0, 160)}\nRun \`nebula telegram setup\` later to re-derive the TG scope key.`,
|
|
324
|
+
'telegram (non-fatal)',
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (e) {
|
|
330
|
+
note(
|
|
331
|
+
`Telegram step failed: ${(e as Error).message.slice(0, 200)}\nIdentity is safe. Re-run \`nebula telegram setup\` later.`,
|
|
332
|
+
'non-fatal',
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Write final config ─────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
const cfg = defineConfig({
|
|
341
|
+
identity: {
|
|
342
|
+
operator: operatorAddress,
|
|
343
|
+
agent: agent.address,
|
|
344
|
+
},
|
|
345
|
+
network,
|
|
346
|
+
storage: { network },
|
|
347
|
+
brain: {
|
|
348
|
+
provider: modelPick?.provider ?? null,
|
|
349
|
+
model: modelPick?.model ?? null,
|
|
350
|
+
},
|
|
351
|
+
plugins: telegramConfigured ? ['onchain', 'system', 'telegram'] : ['onchain', 'system'],
|
|
352
|
+
tools: {},
|
|
353
|
+
imports: { claudeCode: true },
|
|
354
|
+
operator: operatorHint,
|
|
355
|
+
})
|
|
356
|
+
await writeConfigTs(configPath, cfg, {
|
|
357
|
+
header: '// Regenerated by `nebula init`. Edit freely; type-safe.',
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
await operator.close?.()
|
|
361
|
+
|
|
362
|
+
// ─── Phase D: summary ───────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
const lines = [
|
|
365
|
+
'',
|
|
366
|
+
` agent id ${finalAgentId}`,
|
|
367
|
+
` agent EOA ${agent.address}`,
|
|
368
|
+
` operator ${operatorAddress} (source: ${operatorHint.source})`,
|
|
369
|
+
` network ${network} (${NETWORK_RPC[network]})`,
|
|
370
|
+
` chain id ${NETWORK_CHAIN_ID[network]}`,
|
|
371
|
+
` config ${configPath}`,
|
|
372
|
+
` keystore ${paths.keystore} (encrypted to operator wallet)`,
|
|
373
|
+
]
|
|
374
|
+
if (modelPick) lines.push(` brain ${modelPick.model ?? '?'} (${modelPick.provider})`)
|
|
375
|
+
if (!skipLedger) lines.push(` ledger ${ledgerSize} Mantle`)
|
|
376
|
+
if (telegramConfigured) {
|
|
377
|
+
lines.push(` bot @${telegramConfigured.botUsername} (mode: ${telegramConfigured.mode})`)
|
|
378
|
+
}
|
|
379
|
+
const nextSteps = telegramConfigured
|
|
380
|
+
? 'Next: `nebula` to chat · DM the bot on Telegram · `nebula status` for health'
|
|
381
|
+
: 'Next: `nebula` to chat · `nebula telegram setup` for the bot · `nebula topup` to add funds'
|
|
382
|
+
lines.push('', nextSteps)
|
|
383
|
+
outro(lines.join('\n'))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
interface SeedStarterOpts {
|
|
387
|
+
paths: ReturnType<typeof agentPaths.agent>
|
|
388
|
+
network: NebulaNetwork
|
|
389
|
+
contractAddress: Address
|
|
390
|
+
tokenId: bigint
|
|
391
|
+
agentAddress: Address
|
|
392
|
+
operatorAddress: Address
|
|
393
|
+
brainProvider: string | null
|
|
394
|
+
brainModel: string | null
|
|
395
|
+
/**
|
|
396
|
+
* Operator-chosen SANN label (e.g. "chou" for `chou.nebula.0g`). Threaded
|
|
397
|
+
* into identity + persona so the agent introduces itself by name on the
|
|
398
|
+
* very first turn instead of the generic "I am Nebula" template.
|
|
399
|
+
*/
|
|
400
|
+
subname: string | null
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Seed `MEMORY.md`, `/agent/identity.md`, `/agent/persona.md`, and
|
|
405
|
+
* `/user/profile.md` immediately after mint so the per-turn sync manager
|
|
406
|
+
* has real content for the identity / persona / memory-index slots on the
|
|
407
|
+
* first chat turn. Without this, those slots stay bootstrap-placeholder
|
|
408
|
+
* forever (gap discovered during the Phase 6.7 stress test).
|
|
409
|
+
*/
|
|
410
|
+
async function seedStarterMemoryFiles(opts: SeedStarterOpts): Promise<void> {
|
|
411
|
+
const memDir = opts.paths.memoryDir
|
|
412
|
+
const agentMem = `${memDir}/agent`
|
|
413
|
+
const userMem = `${memDir}/user`
|
|
414
|
+
await mkdir(agentMem, { recursive: true })
|
|
415
|
+
await mkdir(userMem, { recursive: true })
|
|
416
|
+
|
|
417
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
418
|
+
const displayName = opts.subname ?? 'nebula'
|
|
419
|
+
const fullName = opts.subname ?? null
|
|
420
|
+
const identityTitle = opts.subname
|
|
421
|
+
? `# ${opts.subname} identity (nebula harness)`
|
|
422
|
+
: '# Nebula identity'
|
|
423
|
+
const subnameLine = fullName ? `- Name: ${fullName}\n` : ''
|
|
424
|
+
const personaIntro = fullName
|
|
425
|
+
? `I am ${displayName} (${fullName}), a sovereign agent running on the nebula harness on Mantle.`
|
|
426
|
+
: 'I am nebula, a sovereign agent harness on Mantle.'
|
|
427
|
+
const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n${identityTitle}\n\n- Name: ${displayName}\n${subnameLine}- iNFT: #${opts.tokenId.toString()} at ${opts.contractAddress} (${opts.network})\n- Agent EOA: ${opts.agentAddress}\n- Operator: ${opts.operatorAddress}\n- Minted: ${now}\n${opts.brainProvider ? `- Brain provider: ${opts.brainProvider}\n` : ''}${opts.brainModel ? `- Brain model: ${opts.brainModel}\n` : ''}`
|
|
428
|
+
const persona = `---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\n${personaIntro} I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and use Mantle Compute (TEE-attested) for reasoning. I am direct, concise, and factual. When asked who I am, I introduce myself as ${displayName}.\n`
|
|
429
|
+
const profile =
|
|
430
|
+
'---\nname: profile\ndescription: User profile (operator-scoped, never anchored on chain).\ntype: user\n---\n# User profile\n\n(empty, fills as we chat)\n'
|
|
431
|
+
|
|
432
|
+
await writeFile(join(agentMem, 'identity.md'), identity, 'utf8')
|
|
433
|
+
await writeFile(join(agentMem, 'persona.md'), persona, 'utf8')
|
|
434
|
+
await writeFile(join(userMem, 'profile.md'), profile, 'utf8')
|
|
435
|
+
|
|
436
|
+
// Seed an empty MEMORY.md so per-turn sync has something to anchor and the
|
|
437
|
+
// brain's first turn sees a parseable index.
|
|
438
|
+
await writeFile(opts.paths.memoryIndex, '# Nebula Memory Index\n\n', 'utf8')
|
|
439
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { agentPaths } from 'nebula-ai-core'
|
|
3
|
+
import { pickDefaultAgent } from './_agents'
|
|
4
|
+
|
|
5
|
+
export async function runLogs(opts: { agent?: string; tail?: number } = {}): Promise<void> {
|
|
6
|
+
// Local mode: read from agentPaths
|
|
7
|
+
const id = opts.agent ?? (await pickDefaultAgent())
|
|
8
|
+
if (!id) {
|
|
9
|
+
console.log('No agents found in ~/.nebula/agents. Run `nebula init` first.')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
const path = agentPaths.agent(id).activityLog
|
|
13
|
+
|
|
14
|
+
let raw: string
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(path, 'utf8')
|
|
17
|
+
} catch (e) {
|
|
18
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
19
|
+
console.log(`No activity log at ${path}`)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
throw e
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lines = raw.trimEnd().split('\n').filter(Boolean)
|
|
26
|
+
const slice = opts.tail ? lines.slice(-opts.tail) : lines
|
|
27
|
+
for (const line of slice) {
|
|
28
|
+
try {
|
|
29
|
+
const entry = JSON.parse(line) as { ts: number; kind: string; data: unknown }
|
|
30
|
+
const d = new Date(entry.ts).toISOString()
|
|
31
|
+
const body = JSON.stringify(entry.data)
|
|
32
|
+
console.log(`${d} ${entry.kind.padEnd(16)} ${body.slice(0, 200)}`)
|
|
33
|
+
} catch {
|
|
34
|
+
console.log(line)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cancel, intro, outro } from '@clack/prompts'
|
|
2
|
+
import { defineConfig } from 'nebula-ai-core'
|
|
3
|
+
import { findAndLoadConfig } from '../config/load'
|
|
4
|
+
import { writeConfigTs } from '../config/render'
|
|
5
|
+
import { pickBrainModel } from './init/model-picker'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `nebula model` — re-pick the brain provider/model. Updates the persisted
|
|
9
|
+
* config so subsequent `nebula` (chat) sessions use the new choice.
|
|
10
|
+
*
|
|
11
|
+
* The TUI also exposes `/model` as a slash command for in-session switching;
|
|
12
|
+
* see `chat.tsx`.
|
|
13
|
+
*/
|
|
14
|
+
export async function runModel(): Promise<void> {
|
|
15
|
+
intro('nebula model')
|
|
16
|
+
|
|
17
|
+
const loaded = await findAndLoadConfig()
|
|
18
|
+
if (!loaded) {
|
|
19
|
+
cancel('No nebula.config.ts found. Run `nebula init` first.')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const { config } = loaded
|
|
23
|
+
|
|
24
|
+
const pick = await pickBrainModel({ network: config.network })
|
|
25
|
+
if (!pick) {
|
|
26
|
+
cancel('No model picked.')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const updated = defineConfig({
|
|
31
|
+
...config,
|
|
32
|
+
brain: { provider: pick.provider, model: pick.model },
|
|
33
|
+
})
|
|
34
|
+
await writeConfigTs(loaded.path, updated, {
|
|
35
|
+
header: '// Updated by `nebula model`. Edit freely; type-safe.',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
outro(
|
|
39
|
+
[
|
|
40
|
+
'',
|
|
41
|
+
` brain ${pick.model ?? '?'}`,
|
|
42
|
+
` provider ${pick.provider}`,
|
|
43
|
+
` config ${loaded.path}`,
|
|
44
|
+
'',
|
|
45
|
+
'Next chat session will use the new brain.',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PAIRING_ALPHABET,
|
|
3
|
+
PAIRING_CODE_LENGTH,
|
|
4
|
+
PairingStore,
|
|
5
|
+
agentPaths,
|
|
6
|
+
placeholderAgentId,
|
|
7
|
+
} from 'nebula-ai-core'
|
|
8
|
+
import { findAndLoadConfig } from '../config/load'
|
|
9
|
+
|
|
10
|
+
export interface RunPairingApproveOpts {
|
|
11
|
+
platform: string
|
|
12
|
+
code: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runPairingApprove(opts: RunPairingApproveOpts): Promise<void> {
|
|
16
|
+
const normalized = opts.code.toUpperCase().trim()
|
|
17
|
+
if (normalized.length !== PAIRING_CODE_LENGTH) {
|
|
18
|
+
console.error(
|
|
19
|
+
`Invalid pairing code: expected ${PAIRING_CODE_LENGTH} characters, got ${normalized.length}`,
|
|
20
|
+
)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
for (const ch of normalized) {
|
|
24
|
+
if (!PAIRING_ALPHABET.includes(ch)) {
|
|
25
|
+
console.error(`Invalid pairing code: contains '${ch}' which is not in the pairing alphabet`)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const loaded = await findAndLoadConfig()
|
|
31
|
+
if (!loaded) {
|
|
32
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
const { config } = loaded
|
|
36
|
+
if (!config.identity.agent) {
|
|
37
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Operate directly on the host's PairingStore (same path as the daemon
|
|
42
|
+
// process when NEBULA_FORCE_EMBEDDED or local-mode chat.tsx).
|
|
43
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
44
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
45
|
+
const store = new PairingStore({ dir })
|
|
46
|
+
|
|
47
|
+
const result = store.approveCode(opts.platform, normalized)
|
|
48
|
+
if (!result) {
|
|
49
|
+
if (store.isLockedOut(opts.platform)) {
|
|
50
|
+
console.error(
|
|
51
|
+
`Platform '${opts.platform}' is locked out due to repeated bad codes. Wait 1 hour and try again.`,
|
|
52
|
+
)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
console.error(`Code ${normalized} not found in pending list. Maybe it expired (1h TTL).`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`✓ Approved on ${opts.platform}: id=${result.userId}${
|
|
61
|
+
result.userName ? ` (@${result.userName})` : ''
|
|
62
|
+
}`,
|
|
63
|
+
)
|
|
64
|
+
console.log('The user can now DM the bot. Their next message will be processed.')
|
|
65
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
2
|
+
import { PairingStore, agentPaths, placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { findAndLoadConfig } from '../config/load'
|
|
4
|
+
|
|
5
|
+
export interface RunPairingClearOpts {
|
|
6
|
+
platform?: string
|
|
7
|
+
yes?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runPairingClear(opts: RunPairingClearOpts): Promise<void> {
|
|
11
|
+
const loaded = await findAndLoadConfig()
|
|
12
|
+
if (!loaded) {
|
|
13
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
const { config } = loaded
|
|
17
|
+
if (!config.identity.agent) {
|
|
18
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
22
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
23
|
+
const store = new PairingStore({ dir })
|
|
24
|
+
|
|
25
|
+
if (!opts.yes) {
|
|
26
|
+
const target = opts.platform ? `${opts.platform} pending` : 'ALL pending pairing codes'
|
|
27
|
+
const ok = await confirm({
|
|
28
|
+
message: `Clear ${target}?`,
|
|
29
|
+
initialValue: false,
|
|
30
|
+
})
|
|
31
|
+
if (isCancel(ok) || !ok) {
|
|
32
|
+
console.log('Aborted.')
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const count = store.clearPending(opts.platform)
|
|
38
|
+
console.log(`✓ Cleared ${count} pending pairing code${count === 1 ? '' : 's'}`)
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PairingStore, agentPaths, placeholderAgentId } from 'nebula-ai-core'
|
|
2
|
+
import { findAndLoadConfig } from '../config/load'
|
|
3
|
+
|
|
4
|
+
export interface RunPairingListOpts {
|
|
5
|
+
platform?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runPairingList(opts: RunPairingListOpts): Promise<void> {
|
|
9
|
+
const store = await openPairingStore()
|
|
10
|
+
if (!store) return
|
|
11
|
+
|
|
12
|
+
const pending = store.listPending(opts.platform)
|
|
13
|
+
const approved = store.listApproved(opts.platform)
|
|
14
|
+
|
|
15
|
+
const pendingTitle = opts.platform ? `Pending (${opts.platform})` : 'Pending'
|
|
16
|
+
console.log(`\n${pendingTitle} (1h TTL):`)
|
|
17
|
+
if (pending.length === 0) {
|
|
18
|
+
console.log(' (none)')
|
|
19
|
+
} else {
|
|
20
|
+
for (const p of pending) {
|
|
21
|
+
const userLabel = p.userName ? `@${p.userName}` : '(unknown)'
|
|
22
|
+
const idLabel = `id=${p.userId}`
|
|
23
|
+
console.log(` [${p.platform}] ${p.code} ${userLabel} ${idLabel} age=${p.ageMinutes}m`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const approvedTitle = opts.platform ? `Approved (${opts.platform})` : 'Approved'
|
|
28
|
+
console.log(`\n${approvedTitle}:`)
|
|
29
|
+
if (approved.length === 0) {
|
|
30
|
+
console.log(' (none)')
|
|
31
|
+
} else {
|
|
32
|
+
for (const a of approved) {
|
|
33
|
+
const userLabel = a.userName ? `@${a.userName}` : '(unknown)'
|
|
34
|
+
const idLabel = `id=${a.userId}`
|
|
35
|
+
console.log(` [${a.platform}] ${userLabel} ${idLabel}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function openPairingStore(): Promise<PairingStore | null> {
|
|
42
|
+
const loaded = await findAndLoadConfig()
|
|
43
|
+
if (!loaded) {
|
|
44
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
const { config } = loaded
|
|
48
|
+
if (!config.identity.agent) {
|
|
49
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
53
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
54
|
+
return new PairingStore({ dir })
|
|
55
|
+
}
|