@promus/cli 0.24.17
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 +18 -0
- package/bin/promus +33 -0
- package/package.json +51 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_inft-ref.ts +43 -0
- package/src/commands/_unlock.ts +74 -0
- package/src/commands/admin-autotopup-tick.ts +73 -0
- package/src/commands/admin.test.ts +34 -0
- package/src/commands/admin.ts +32 -0
- package/src/commands/balance.test.ts +10 -0
- package/src/commands/balance.ts +112 -0
- package/src/commands/chat-sandbox.tsx +520 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1916 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +47 -0
- package/src/commands/gateway-run.ts +54 -0
- package/src/commands/gateway-start.ts +218 -0
- package/src/commands/gateway-status.ts +88 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/cost.test.ts +169 -0
- package/src/commands/init/cost.ts +154 -0
- package/src/commands/init/funding-gate.ts +67 -0
- package/src/commands/init/model-picker.ts +81 -0
- package/src/commands/init/operator-picker.ts +263 -0
- package/src/commands/init/resume.ts +136 -0
- package/src/commands/init/sandbox-provision.test.ts +497 -0
- package/src/commands/init/sandbox-provision.ts +1177 -0
- package/src/commands/init/telegram-step.ts +229 -0
- package/src/commands/init/wizard-state.ts +95 -0
- package/src/commands/init.ts +612 -0
- package/src/commands/inspect.ts +529 -0
- package/src/commands/ledger.ts +176 -0
- package/src/commands/logs.ts +86 -0
- package/src/commands/migrate-keystore.ts +155 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +114 -0
- package/src/commands/pairing-clear.ts +42 -0
- package/src/commands/pairing-list.ts +58 -0
- package/src/commands/pairing-revoke.ts +52 -0
- package/src/commands/pairing.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/pause.ts +99 -0
- package/src/commands/profile.ts +184 -0
- package/src/commands/restore.ts +221 -0
- package/src/commands/resume.ts +181 -0
- package/src/commands/status.ts +119 -0
- package/src/commands/sync.ts +147 -0
- package/src/commands/telegram-remove.ts +65 -0
- package/src/commands/telegram-setup.ts +74 -0
- package/src/commands/telegram-status.ts +89 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/topup.ts +303 -0
- package/src/commands/transfer.test.ts +111 -0
- package/src/commands/transfer.ts +520 -0
- package/src/commands/upgrade.test.ts +137 -0
- package/src/commands/upgrade.ts +690 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +110 -0
- package/src/index.ts +378 -0
- package/src/sandbox/client.test.ts +251 -0
- package/src/sandbox/client.ts +424 -0
- package/src/ui/app.tsx +677 -0
- package/src/ui/approval-summary.test.ts +154 -0
- package/src/ui/approval-summary.ts +34 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.test.ts +146 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.test.ts +74 -0
- package/src/ui/state.ts +198 -0
- package/src/util/bootstrap-mode.test.ts +40 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.test.ts +190 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/brain-secrets.ts +96 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.test.ts +16 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.test.ts +86 -0
- package/src/util/gateway-spawn.ts +128 -0
- package/src/util/gateway-version.test.ts +113 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.test.ts +116 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.test.ts +60 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.test.ts +77 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.test.ts +53 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.test.ts +227 -0
- package/src/util/telegram-secrets.ts +223 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
cancel,
|
|
6
|
+
confirm,
|
|
7
|
+
intro,
|
|
8
|
+
isCancel,
|
|
9
|
+
log,
|
|
10
|
+
note,
|
|
11
|
+
outro,
|
|
12
|
+
password,
|
|
13
|
+
select,
|
|
14
|
+
spinner,
|
|
15
|
+
text,
|
|
16
|
+
} from '@clack/prompts'
|
|
17
|
+
import {
|
|
18
|
+
type PromusNetwork,
|
|
19
|
+
NETWORK_CHAIN_ID,
|
|
20
|
+
NETWORK_CURRENCY,
|
|
21
|
+
NETWORK_RPC,
|
|
22
|
+
OPERATOR_BLOB_SCOPES,
|
|
23
|
+
type OperatorSessionKeys,
|
|
24
|
+
agentPaths,
|
|
25
|
+
buildOperatorSession,
|
|
26
|
+
defineConfig,
|
|
27
|
+
explorerTokenUrl,
|
|
28
|
+
explorerTxUrl,
|
|
29
|
+
generateAgentWallet,
|
|
30
|
+
getGasPriceWithFloor,
|
|
31
|
+
iNFTAgentId,
|
|
32
|
+
mintAgent,
|
|
33
|
+
placeholderAgentId,
|
|
34
|
+
precomputeAllScopes,
|
|
35
|
+
saveKeystoreLocally,
|
|
36
|
+
uploadAndAnchorKeystore,
|
|
37
|
+
waitForReceiptResilient,
|
|
38
|
+
writeOperatorSession,
|
|
39
|
+
} from '@promus/core'
|
|
40
|
+
import { type Address, type Hex, formatEther, hexToBytes, parseEther } from 'viem'
|
|
41
|
+
import { writeConfigTs } from '../config/render'
|
|
42
|
+
import { withSilencedConsole } from '../util/silence-console'
|
|
43
|
+
import { saveBrainSecrets } from '../util/brain-secrets'
|
|
44
|
+
import { loadTelegramHandoffSecrets } from '../util/telegram-secrets'
|
|
45
|
+
import { estimateCosts, renderCostSummary } from './init/cost'
|
|
46
|
+
import { fundingGate } from './init/funding-gate'
|
|
47
|
+
import { pickOperatorSigner } from './init/operator-picker'
|
|
48
|
+
import { initialWizardState, updateWizardState, writeWizardState } from './init/wizard-state'
|
|
49
|
+
|
|
50
|
+
export async function runInit(opts?: { cwd?: string; resume?: boolean }): Promise<void> {
|
|
51
|
+
const configPath = agentPaths.config
|
|
52
|
+
|
|
53
|
+
intro('promus init')
|
|
54
|
+
|
|
55
|
+
if (existsSync(configPath) && !opts?.resume) {
|
|
56
|
+
const choice = (await select({
|
|
57
|
+
message: `${configPath} exists`,
|
|
58
|
+
options: [
|
|
59
|
+
{ value: 'overwrite', label: 'Start fresh (overwrite)' },
|
|
60
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
61
|
+
],
|
|
62
|
+
initialValue: 'cancel',
|
|
63
|
+
})) as 'overwrite' | 'cancel' | symbol
|
|
64
|
+
if (isCancel(choice) || choice === 'cancel') {
|
|
65
|
+
cancel('Aborted.')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Phase A: local prompts (no chain, no wallet) ───────────────────────
|
|
71
|
+
|
|
72
|
+
const network = (await select({
|
|
73
|
+
message: 'Which network?',
|
|
74
|
+
options: [
|
|
75
|
+
{ value: 'arbitrum-sepolia' as PromusNetwork, label: 'Arbitrum Sepolia (421614)' },
|
|
76
|
+
{ value: 'robinhood-testnet' as PromusNetwork, label: 'Robinhood Chain testnet (46630)' },
|
|
77
|
+
],
|
|
78
|
+
initialValue: 'arbitrum-sepolia' as PromusNetwork,
|
|
79
|
+
})) as PromusNetwork
|
|
80
|
+
if (isCancel(network)) {
|
|
81
|
+
cancel('Aborted.')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Deploy target is always local on the Arbitrum path.
|
|
86
|
+
const deployTarget = 'local' as const
|
|
87
|
+
const requestedSubname = ''
|
|
88
|
+
|
|
89
|
+
// ─── Brain provider + model picker ──────────────────────────────────────
|
|
90
|
+
const brainProvider = (await select({
|
|
91
|
+
message: 'Which AI provider?',
|
|
92
|
+
options: [
|
|
93
|
+
{ value: 'anthropic' as const, label: 'Anthropic (Claude)' },
|
|
94
|
+
{ value: 'openai' as const, label: 'OpenAI (GPT)' },
|
|
95
|
+
{ value: 'google' as const, label: 'Google (Gemini)' },
|
|
96
|
+
],
|
|
97
|
+
initialValue: 'anthropic' as const,
|
|
98
|
+
})) as 'anthropic' | 'openai' | 'google' | symbol
|
|
99
|
+
if (isCancel(brainProvider)) {
|
|
100
|
+
cancel('Aborted.')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const modelPick = { provider: brainProvider, model: null as string | null }
|
|
105
|
+
|
|
106
|
+
// ─── Brain API key (encrypted at rest) ──────────────────────────────────
|
|
107
|
+
const providerLabel = brainProvider === 'anthropic' ? 'Anthropic' : brainProvider === 'openai' ? 'OpenAI' : 'Google'
|
|
108
|
+
const apiKeyPrompt = (await password({
|
|
109
|
+
message: `${providerLabel} API key (stored encrypted, never in .env)`,
|
|
110
|
+
mask: '*',
|
|
111
|
+
})) as string | symbol
|
|
112
|
+
if (isCancel(apiKeyPrompt) || !apiKeyPrompt) {
|
|
113
|
+
cancel('Aborted.')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Storage backend picker ─────────────────────────────────────────────
|
|
118
|
+
const storageBackend = (await select({
|
|
119
|
+
message: 'Storage backend?',
|
|
120
|
+
options: [
|
|
121
|
+
{ value: 'ipfs' as const, label: 'IPFS (Kubo local node)' },
|
|
122
|
+
],
|
|
123
|
+
initialValue: 'ipfs' as const,
|
|
124
|
+
})) as 'ipfs' | symbol
|
|
125
|
+
if (isCancel(storageBackend)) {
|
|
126
|
+
cancel('Aborted.')
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let ipfsApiUrl = 'http://127.0.0.1:5001'
|
|
131
|
+
let ipfsGateway = 'http://127.0.0.1:8080/ipfs'
|
|
132
|
+
if (storageBackend === 'ipfs') {
|
|
133
|
+
const customUrl = (await text({
|
|
134
|
+
message: 'IPFS API URL',
|
|
135
|
+
initialValue: 'http://127.0.0.1:5001',
|
|
136
|
+
})) as string | symbol
|
|
137
|
+
if (isCancel(customUrl)) { cancel('Aborted.'); return }
|
|
138
|
+
ipfsApiUrl = customUrl || 'http://127.0.0.1:5001'
|
|
139
|
+
|
|
140
|
+
const customGw = (await text({
|
|
141
|
+
message: 'IPFS gateway URL (trailing /ipfs)',
|
|
142
|
+
initialValue: 'http://127.0.0.1:8080/ipfs',
|
|
143
|
+
})) as string | symbol
|
|
144
|
+
if (isCancel(customGw)) { cancel('Aborted.'); return }
|
|
145
|
+
ipfsGateway = customGw || 'http://127.0.0.1:8080/ipfs'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Phase B: wallet gate ────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
const picked = await pickOperatorSigner({ network })
|
|
151
|
+
if (!picked) return
|
|
152
|
+
const { signer: operator, hint: operatorHint } = picked
|
|
153
|
+
|
|
154
|
+
const sConnect = spinner()
|
|
155
|
+
sConnect.start(`Connecting via ${operator.source}`)
|
|
156
|
+
let operatorAddress: Address
|
|
157
|
+
try {
|
|
158
|
+
operatorAddress = await operator.address()
|
|
159
|
+
sConnect.stop(`operator: ${operatorAddress}`)
|
|
160
|
+
} catch (e) {
|
|
161
|
+
sConnect.stop(`connection failed: ${(e as Error).message.slice(0, 140)}`)
|
|
162
|
+
await operator.close?.()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const costs = estimateCosts({
|
|
167
|
+
ledgerSizeOg: 0,
|
|
168
|
+
withSubname: false,
|
|
169
|
+
deployTarget: 'local',
|
|
170
|
+
network,
|
|
171
|
+
})
|
|
172
|
+
note(renderCostSummary(costs), `cost summary (${costs.currency} L2 gas)`)
|
|
173
|
+
|
|
174
|
+
const publicClient = await operator.publicClient(network)
|
|
175
|
+
const operatorBalance = await publicClient.getBalance({ address: operatorAddress })
|
|
176
|
+
|
|
177
|
+
if (operatorBalance < costs.totalOperator) {
|
|
178
|
+
const need = costs.totalOperator - operatorBalance
|
|
179
|
+
note(
|
|
180
|
+
`Operator balance ${formatEther(operatorBalance)} ${costs.currency}, need ${formatEther(need)} ${costs.currency} more.`,
|
|
181
|
+
'insufficient funds',
|
|
182
|
+
)
|
|
183
|
+
const gate = await fundingGate({
|
|
184
|
+
publicClient,
|
|
185
|
+
operatorAddress,
|
|
186
|
+
requiredOg: costs.totalOperator,
|
|
187
|
+
currency: costs.currency,
|
|
188
|
+
})
|
|
189
|
+
if (gate.kind === 'cancel') {
|
|
190
|
+
await operator.close?.()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const proceed = await confirm({ message: 'Proceed?', initialValue: true })
|
|
196
|
+
if (isCancel(proceed) || !proceed) {
|
|
197
|
+
cancel('Aborted.')
|
|
198
|
+
await operator.close?.()
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Phase C: execute with Pattern B state tracking ─────────────────────
|
|
203
|
+
|
|
204
|
+
const agent = generateAgentWallet()
|
|
205
|
+
const provisionalAgentId = placeholderAgentId(agent.address)
|
|
206
|
+
const provisional = agentPaths.agent(provisionalAgentId)
|
|
207
|
+
await mkdir(provisional.dir, { recursive: true })
|
|
208
|
+
|
|
209
|
+
await writeWizardState(provisional.dir, {
|
|
210
|
+
...initialWizardState(agent.address, network),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
let mintedTokenId: bigint | null = null
|
|
214
|
+
let contractAddress: Address | null = null
|
|
215
|
+
|
|
216
|
+
const sMint = spinner()
|
|
217
|
+
sMint.start(`Minting iNFT on ${network} (keystore slot left as bootstrap until upload)`)
|
|
218
|
+
try {
|
|
219
|
+
const { result, contractAddress: c } = await withSilencedConsole(() =>
|
|
220
|
+
mintAgent({
|
|
221
|
+
network,
|
|
222
|
+
operator,
|
|
223
|
+
agentAddress: agent.address as Address,
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
mintedTokenId = result.tokenId
|
|
227
|
+
contractAddress = c
|
|
228
|
+
await updateWizardState(provisional.dir, draft => {
|
|
229
|
+
draft.steps.mintedTokenId = result.tokenId.toString()
|
|
230
|
+
draft.steps.mintedContract = c
|
|
231
|
+
draft.steps.mintTx = result.txHash
|
|
232
|
+
})
|
|
233
|
+
sMint.stop(
|
|
234
|
+
`iNFT #${result.tokenId.toString()} minted to ${operatorAddress} → ${explorerTxUrl(network, result.txHash)}`,
|
|
235
|
+
)
|
|
236
|
+
} catch (e) {
|
|
237
|
+
sMint.stop(`mint failed: ${(e as Error).message}`)
|
|
238
|
+
await updateWizardState(provisional.dir, draft => {
|
|
239
|
+
draft.lastError = `mint failed: ${(e as Error).message}`
|
|
240
|
+
})
|
|
241
|
+
await operator.close?.()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const finalAgentId = iNFTAgentId({ contractAddress: contractAddress!, tokenId: mintedTokenId! })
|
|
246
|
+
const targetDir = agentPaths.agent(finalAgentId).dir
|
|
247
|
+
if (provisional.dir !== targetDir) {
|
|
248
|
+
try {
|
|
249
|
+
await rename(provisional.dir, targetDir)
|
|
250
|
+
} catch (e) {
|
|
251
|
+
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const paths = agentPaths.agent(finalAgentId)
|
|
255
|
+
|
|
256
|
+
// v0.23.1: derive BOTH operator-scope keys (keystore + profile) in parallel
|
|
257
|
+
// up front, then reuse them everywhere. This is the single "two signatures
|
|
258
|
+
// back to back" moment in the wizard: keystore scope (for the encrypted
|
|
259
|
+
// privkey blob) + profile scope (for the operator-private user-partition
|
|
260
|
+
// memory slot). Folding profile derivation into init removes the v0.23.0
|
|
261
|
+
// need for `promus profile init` as a follow-up command.
|
|
262
|
+
const sKeys = spinner()
|
|
263
|
+
sKeys.start('Deriving operator scope keys (may prompt twice: keystore + profile)')
|
|
264
|
+
let operatorKeys: OperatorSessionKeys
|
|
265
|
+
let keystoreKeyBuf: Buffer
|
|
266
|
+
let profileScopeKeyHex: `0x${string}` | undefined
|
|
267
|
+
let brainScopeKeyHex: `0x${string}` | undefined
|
|
268
|
+
try {
|
|
269
|
+
operatorKeys = await precomputeAllScopes(operator, agent.address as Address, [
|
|
270
|
+
OPERATOR_BLOB_SCOPES.PROFILE,
|
|
271
|
+
OPERATOR_BLOB_SCOPES.BRAIN,
|
|
272
|
+
])
|
|
273
|
+
keystoreKeyBuf = Buffer.from(hexToBytes(operatorKeys.keystore))
|
|
274
|
+
const profileHex = operatorKeys[OPERATOR_BLOB_SCOPES.PROFILE]
|
|
275
|
+
profileScopeKeyHex = profileHex as `0x${string}` | undefined
|
|
276
|
+
brainScopeKeyHex = operatorKeys[OPERATOR_BLOB_SCOPES.BRAIN] as `0x${string}` | undefined
|
|
277
|
+
sKeys.stop('scope keys derived')
|
|
278
|
+
} catch (e) {
|
|
279
|
+
sKeys.stop(`scope key derive failed: ${(e as Error).message.slice(0, 160)}`)
|
|
280
|
+
cancel('Aborted (operator signature required for keystore + profile scopes).')
|
|
281
|
+
await operator.close?.()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Pass the already-derived keystoreKey so saveKeystoreLocally skips
|
|
286
|
+
// signing again. Save BEFORE funding the agent EOA per
|
|
287
|
+
// `feedback-init-must-save-keystore-before-funding.md`.
|
|
288
|
+
const sLocal = spinner()
|
|
289
|
+
sLocal.start('Encrypting agent keystore to operator wallet (local insurance)')
|
|
290
|
+
let encryptedBytes: Uint8Array
|
|
291
|
+
try {
|
|
292
|
+
const saved = await saveKeystoreLocally({
|
|
293
|
+
agentAddress: agent.address as Address,
|
|
294
|
+
agentPrivkey: agent.privkeyHex as Hex,
|
|
295
|
+
cachePath: paths.keystore,
|
|
296
|
+
precomputedKey: keystoreKeyBuf,
|
|
297
|
+
})
|
|
298
|
+
encryptedBytes = saved.bytes
|
|
299
|
+
await updateWizardState(paths.dir, draft => {
|
|
300
|
+
draft.steps.keystoreSaved = true
|
|
301
|
+
})
|
|
302
|
+
sLocal.stop(`keystore saved locally at ${paths.keystore}`)
|
|
303
|
+
} catch (e) {
|
|
304
|
+
sLocal.stop(`local keystore save failed: ${(e as Error).message.slice(0, 120)}`)
|
|
305
|
+
cancel('Aborted before funding (keystore encryption failed).')
|
|
306
|
+
await operator.close?.()
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Save encrypted brain secrets ────────────────────────────────────────
|
|
311
|
+
const sBrain = spinner()
|
|
312
|
+
sBrain.start('Encrypting brain secrets (API key + storage config)')
|
|
313
|
+
try {
|
|
314
|
+
const brainKeyBuf = brainScopeKeyHex
|
|
315
|
+
? Buffer.from(hexToBytes(brainScopeKeyHex))
|
|
316
|
+
: undefined
|
|
317
|
+
await saveBrainSecrets({
|
|
318
|
+
signer: operator,
|
|
319
|
+
agentAddress: agent.address as Address,
|
|
320
|
+
agentId: finalAgentId,
|
|
321
|
+
plaintext: {
|
|
322
|
+
provider: brainProvider,
|
|
323
|
+
apiKey: apiKeyPrompt,
|
|
324
|
+
model: modelPick.model ?? undefined,
|
|
325
|
+
ipfsApiUrl,
|
|
326
|
+
ipfsGateway,
|
|
327
|
+
},
|
|
328
|
+
precomputedKey: brainKeyBuf,
|
|
329
|
+
})
|
|
330
|
+
sBrain.stop('brain secrets encrypted')
|
|
331
|
+
} catch (e) {
|
|
332
|
+
sBrain.stop(`brain secrets save failed: ${(e as Error).message.slice(0, 120)}`)
|
|
333
|
+
// Non-fatal: user can re-run init or set .env as fallback
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sFund = spinner()
|
|
337
|
+
const fundingAmount = costs.agentFloat
|
|
338
|
+
sFund.start(`Funding agent ${agent.address} with ${formatEther(fundingAmount)} ${costs.currency}`)
|
|
339
|
+
try {
|
|
340
|
+
const opWc = await operator.walletClient(network)
|
|
341
|
+
const opAccount = opWc.account
|
|
342
|
+
if (!opAccount) throw new Error('walletClient is missing default account')
|
|
343
|
+
const fundGasPrice = await getGasPriceWithFloor(publicClient)
|
|
344
|
+
const fundTx = await withSilencedConsole(() =>
|
|
345
|
+
opWc.sendTransaction({
|
|
346
|
+
to: agent.address as Address,
|
|
347
|
+
value: fundingAmount,
|
|
348
|
+
chain: operator.chain(network),
|
|
349
|
+
account: opAccount,
|
|
350
|
+
maxFeePerGas: fundGasPrice,
|
|
351
|
+
maxPriorityFeePerGas: fundGasPrice,
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
await waitForReceiptResilient(publicClient, fundTx)
|
|
355
|
+
await updateWizardState(paths.dir, draft => {
|
|
356
|
+
draft.steps.agentFundedTx = fundTx
|
|
357
|
+
})
|
|
358
|
+
sFund.stop(`funded (tx ${fundTx})`)
|
|
359
|
+
} catch (e) {
|
|
360
|
+
sFund.stop(`fund failed: ${(e as Error).message}`)
|
|
361
|
+
await operator.close?.()
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const sPersist = spinner()
|
|
366
|
+
sPersist.start(
|
|
367
|
+
`Uploading keystore to IPFS + anchoring on chain`,
|
|
368
|
+
)
|
|
369
|
+
let keystorePersisted = false
|
|
370
|
+
try {
|
|
371
|
+
const { rootHash, updateTx } = await withSilencedConsole(() =>
|
|
372
|
+
uploadAndAnchorKeystore({
|
|
373
|
+
network,
|
|
374
|
+
agentPrivkey: agent.privkeyHex as Hex,
|
|
375
|
+
tokenId: mintedTokenId!,
|
|
376
|
+
contractAddress: contractAddress!,
|
|
377
|
+
bytes: encryptedBytes,
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
380
|
+
await updateWizardState(paths.dir, draft => {
|
|
381
|
+
draft.steps.keystorePersistedTx = updateTx
|
|
382
|
+
draft.steps.keystoreRootHash = rootHash
|
|
383
|
+
})
|
|
384
|
+
keystorePersisted = true
|
|
385
|
+
sPersist.stop(`keystore anchored (root ${rootHash.slice(0, 12)}…)`)
|
|
386
|
+
} catch (e) {
|
|
387
|
+
sPersist.stop(`keystore upload/anchor failed: ${(e as Error).message.slice(0, 120)}`)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!keystorePersisted) {
|
|
391
|
+
note(
|
|
392
|
+
[
|
|
393
|
+
`iNFT #${mintedTokenId!.toString()} is minted, agent EOA is funded with ${formatEther(fundingAmount)} ${costs.currency},`,
|
|
394
|
+
`and the encrypted keystore is on disk at ${paths.keystore}.`,
|
|
395
|
+
'',
|
|
396
|
+
`The IPFS upload + chain anchor failed, so this machine has`,
|
|
397
|
+
'a working agent but no on-chain recovery path yet. The funds at',
|
|
398
|
+
`${agent.address} are NOT stranded; operator wallet ${operatorAddress}`,
|
|
399
|
+
'can decrypt the local keystore and resume the agent.',
|
|
400
|
+
'',
|
|
401
|
+
'Re-run `promus init --resume` to retry the storage upload and anchor,',
|
|
402
|
+
'or proceed with chat using the local keystore (sync will retry on',
|
|
403
|
+
'every chat turn anyway).',
|
|
404
|
+
].join('\n'),
|
|
405
|
+
'storage anchor failed (recoverable)',
|
|
406
|
+
)
|
|
407
|
+
cancel('Aborted before writing config (storage anchor pending).')
|
|
408
|
+
await operator.close?.()
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// v0.23.1: cache the operator scope keys to `.operator-session` so:
|
|
413
|
+
// - First `promus` chat does NOT re-prompt Touch ID (`gateway-start` will
|
|
414
|
+
// find both keystore + profile scopes already cached and skip
|
|
415
|
+
// re-derivation).
|
|
416
|
+
// - First sync after init can encrypt + anchor the PROFILE slot
|
|
417
|
+
// transparently — operator never needs to run `promus profile init`.
|
|
418
|
+
// requiredScopesForAgent now returns ['keystore', 'promus-profile-v1']
|
|
419
|
+
// because seedStarterMemoryFiles just wrote user/profile.md.
|
|
420
|
+
try {
|
|
421
|
+
const sess = buildOperatorSession({ agent: agent.address as Address, keys: operatorKeys })
|
|
422
|
+
writeOperatorSession(finalAgentId, sess)
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.warn(`operator-session write skipped: ${(e as Error).message.slice(0, 160)}`)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Seed canonical memory starter files.
|
|
428
|
+
await seedStarterMemoryFiles({
|
|
429
|
+
paths,
|
|
430
|
+
network,
|
|
431
|
+
contractAddress: contractAddress!,
|
|
432
|
+
tokenId: mintedTokenId!,
|
|
433
|
+
agentAddress: agent.address as Address,
|
|
434
|
+
operatorAddress,
|
|
435
|
+
brainProvider: modelPick?.provider ?? null,
|
|
436
|
+
brainModel: modelPick?.model ?? null,
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// v0.24.4: Phase E (Telegram bot setup) MUST run before Phase 11 (sandbox
|
|
440
|
+
// provision) so the sandbox handoff envelope can ship `telegram-secrets`
|
|
441
|
+
// and the listener boots active. Previously Phase E ran AFTER provision and
|
|
442
|
+
// the sandbox booted with `listeners.telegram: disabled`, forcing the
|
|
443
|
+
// operator to `promus upgrade --in-place` post-init to re-ship secrets.
|
|
444
|
+
let telegramConfigured: { botUsername: string; mode: string } | null = null
|
|
445
|
+
if (mintedTokenId !== null && contractAddress) {
|
|
446
|
+
const tgChoice = await confirm({
|
|
447
|
+
message: 'Configure a Telegram bot for this agent now? (recommended)',
|
|
448
|
+
initialValue: true,
|
|
449
|
+
})
|
|
450
|
+
if (!isCancel(tgChoice) && tgChoice === true) {
|
|
451
|
+
try {
|
|
452
|
+
const { runTelegramStep } = await import('./init/telegram-step')
|
|
453
|
+
const tgResult = await runTelegramStep({
|
|
454
|
+
signer: operator,
|
|
455
|
+
agentId: finalAgentId,
|
|
456
|
+
agentAddress: agent.address as Address,
|
|
457
|
+
configPath,
|
|
458
|
+
// Synthetic partial cfg — caller writes the final cfg below. Pass
|
|
459
|
+
// skipConfigWrite=true so telegram-step doesn't touch disk.
|
|
460
|
+
config: { plugins: [] } as never,
|
|
461
|
+
network,
|
|
462
|
+
skipConfigWrite: true,
|
|
463
|
+
})
|
|
464
|
+
if (tgResult.configured && tgResult.botUsername && tgResult.modeUsed) {
|
|
465
|
+
telegramConfigured = {
|
|
466
|
+
botUsername: tgResult.botUsername,
|
|
467
|
+
mode: tgResult.modeUsed,
|
|
468
|
+
}
|
|
469
|
+
// v0.24.3: append TELEGRAM key to `.operator-session` so the gateway
|
|
470
|
+
// daemon auto-spawns on first chat without re-prompting Touch ID.
|
|
471
|
+
if (tgResult.telegramScopeKeyHex) {
|
|
472
|
+
try {
|
|
473
|
+
const sess = buildOperatorSession({
|
|
474
|
+
agent: agent.address as Address,
|
|
475
|
+
keys: {
|
|
476
|
+
...operatorKeys,
|
|
477
|
+
[OPERATOR_BLOB_SCOPES.TELEGRAM]: tgResult.telegramScopeKeyHex,
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
writeOperatorSession(finalAgentId, sess)
|
|
481
|
+
} catch (e) {
|
|
482
|
+
note(
|
|
483
|
+
`operator-session rewrite skipped: ${(e as Error).message.slice(0, 160)}\nRun \`promus telegram setup\` later to re-derive the TG scope key.`,
|
|
484
|
+
'telegram (non-fatal)',
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch (e) {
|
|
490
|
+
note(
|
|
491
|
+
`Telegram step failed: ${(e as Error).message.slice(0, 200)}\nIdentity + iNFT are safe. Re-run \`promus telegram setup\` later.`,
|
|
492
|
+
'non-fatal',
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Load TG handoff secrets into memory for the sandbox envelope. Skipped if
|
|
499
|
+
// TG wasn't configured this run. The shape is exactly what the harness
|
|
500
|
+
// expects inside the secondary ECIES envelope (botToken + allowedUserIds +
|
|
501
|
+
// optional pairingApproved). Errors are non-fatal: TG is opt-in.
|
|
502
|
+
let telegramHandoff: Awaited<ReturnType<typeof loadTelegramHandoffSecrets>> = undefined
|
|
503
|
+
if (telegramConfigured && mintedTokenId !== null && contractAddress) {
|
|
504
|
+
telegramHandoff = await loadTelegramHandoffSecrets({
|
|
505
|
+
signer: operator,
|
|
506
|
+
agentAddress: agent.address as Address,
|
|
507
|
+
contractAddress,
|
|
508
|
+
tokenId: mintedTokenId,
|
|
509
|
+
onNotice: msg => note(msg, 'telegram handoff (non-fatal)'),
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ─── Write final config ─────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
const cfg = defineConfig({
|
|
516
|
+
identity: {
|
|
517
|
+
iNFT:
|
|
518
|
+
mintedTokenId !== null && contractAddress
|
|
519
|
+
? {
|
|
520
|
+
contract: contractAddress,
|
|
521
|
+
tokenId: mintedTokenId.toString(),
|
|
522
|
+
network,
|
|
523
|
+
}
|
|
524
|
+
: null,
|
|
525
|
+
operator: operatorAddress,
|
|
526
|
+
agent: agent.address,
|
|
527
|
+
},
|
|
528
|
+
network,
|
|
529
|
+
storage: { network },
|
|
530
|
+
brain: {
|
|
531
|
+
provider: modelPick?.provider ?? null,
|
|
532
|
+
model: modelPick?.model ?? null,
|
|
533
|
+
},
|
|
534
|
+
plugins: telegramConfigured
|
|
535
|
+
? ['onchain', 'comms', 'system', 'telegram']
|
|
536
|
+
: ['onchain', 'comms', 'system'],
|
|
537
|
+
tools: {},
|
|
538
|
+
imports: { claudeCode: true },
|
|
539
|
+
operator: operatorHint,
|
|
540
|
+
deployTarget: 'local' as const,
|
|
541
|
+
})
|
|
542
|
+
await writeConfigTs(configPath, cfg, {
|
|
543
|
+
header: '// Regenerated by `promus init`. Edit freely; type-safe.',
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
await operator.close?.()
|
|
547
|
+
|
|
548
|
+
// ─── Phase D: summary ───────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
const lines = [
|
|
551
|
+
'',
|
|
552
|
+
` agent id ${finalAgentId}`,
|
|
553
|
+
` agent EOA ${agent.address}`,
|
|
554
|
+
` operator ${operatorAddress} (source: ${operatorHint.source})`,
|
|
555
|
+
` network ${network} (${NETWORK_RPC[network]})`,
|
|
556
|
+
` chain id ${NETWORK_CHAIN_ID[network]}`,
|
|
557
|
+
` config ${configPath}`,
|
|
558
|
+
` keystore on IPFS (cached at ${paths.keystore})`,
|
|
559
|
+
]
|
|
560
|
+
if (mintedTokenId !== null && contractAddress) {
|
|
561
|
+
lines.push(` iNFT #${mintedTokenId.toString()} at ${contractAddress}`)
|
|
562
|
+
lines.push(` ${explorerTokenUrl(network, contractAddress, mintedTokenId)}`)
|
|
563
|
+
}
|
|
564
|
+
if (modelPick) lines.push(` brain ${modelPick.model ?? modelPick.provider}`)
|
|
565
|
+
if (telegramConfigured) {
|
|
566
|
+
lines.push(` bot @${telegramConfigured.botUsername} (mode: ${telegramConfigured.mode})`)
|
|
567
|
+
}
|
|
568
|
+
const nextSteps = telegramConfigured
|
|
569
|
+
? 'Next: `promus` to chat · DM the bot on Telegram · `promus status` for health'
|
|
570
|
+
: 'Next: `promus` to chat · `promus telegram setup` for the bot · `promus status` for health'
|
|
571
|
+
lines.push('', nextSteps)
|
|
572
|
+
outro(lines.join('\n'))
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
interface SeedStarterOpts {
|
|
576
|
+
paths: ReturnType<typeof agentPaths.agent>
|
|
577
|
+
network: PromusNetwork
|
|
578
|
+
contractAddress: Address
|
|
579
|
+
tokenId: bigint
|
|
580
|
+
agentAddress: Address
|
|
581
|
+
operatorAddress: Address
|
|
582
|
+
brainProvider: string | null
|
|
583
|
+
brainModel: string | null
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Seed `MEMORY.md`, `/agent/identity.md`, `/agent/persona.md`, and
|
|
588
|
+
* `/user/profile.md` immediately after mint so the per-turn sync manager
|
|
589
|
+
* has real content for the identity / persona / memory-index slots on the
|
|
590
|
+
* first chat turn.
|
|
591
|
+
*/
|
|
592
|
+
async function seedStarterMemoryFiles(opts: SeedStarterOpts): Promise<void> {
|
|
593
|
+
const memDir = opts.paths.memoryDir
|
|
594
|
+
const agentMem = `${memDir}/agent`
|
|
595
|
+
const userMem = `${memDir}/user`
|
|
596
|
+
await mkdir(agentMem, { recursive: true })
|
|
597
|
+
await mkdir(userMem, { recursive: true })
|
|
598
|
+
|
|
599
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
600
|
+
const identity = `---\nname: identity\ndescription: Auto-written agent identity facts.\ntype: agent-identity\n---\n# Promus identity\n\n- Name: promus\n- 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` : ''}`
|
|
601
|
+
const persona = `---\nname: persona\ndescription: Voice + behavior style.\ntype: agent-persona\n---\n# Persona\n\nI am Promus, a sovereign on-chain agent on Arbitrum. I anchor my state on chain every turn, decrypt my keystore via my operator wallet at session start, and reason with my configured AI provider. I am direct, concise, and factual.\n`
|
|
602
|
+
const profile =
|
|
603
|
+
'---\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'
|
|
604
|
+
|
|
605
|
+
await writeFile(join(agentMem, 'identity.md'), identity, 'utf8')
|
|
606
|
+
await writeFile(join(agentMem, 'persona.md'), persona, 'utf8')
|
|
607
|
+
await writeFile(join(userMem, 'profile.md'), profile, 'utf8')
|
|
608
|
+
|
|
609
|
+
// Seed an empty MEMORY.md so per-turn sync has something to anchor and the
|
|
610
|
+
// brain's first turn sees a parseable index.
|
|
611
|
+
await writeFile(opts.paths.memoryIndex, '# Promus Memory Index\n\n', 'utf8')
|
|
612
|
+
}
|