@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,529 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { cancel, intro, note, outro, spinner } from '@clack/prompts'
|
|
4
|
+
import {
|
|
5
|
+
type PromusConfig,
|
|
6
|
+
type PromusNetwork,
|
|
7
|
+
INTELLIGENT_DATA_SLOTS,
|
|
8
|
+
type InspectAgentResult,
|
|
9
|
+
type IntelligentDataSlot,
|
|
10
|
+
type SlotDiff,
|
|
11
|
+
type SlotInspection,
|
|
12
|
+
type TxInspection,
|
|
13
|
+
agentPaths,
|
|
14
|
+
bootstrapHashFor,
|
|
15
|
+
deriveMemoryKey,
|
|
16
|
+
diffAgent,
|
|
17
|
+
explorerTokenUrl,
|
|
18
|
+
explorerTxUrl,
|
|
19
|
+
fetchAndDecryptKeystore,
|
|
20
|
+
iNFTAgentId,
|
|
21
|
+
inspectAgent,
|
|
22
|
+
inspectTx,
|
|
23
|
+
} from '@promus/core'
|
|
24
|
+
import type { Address, Hex } from 'viem'
|
|
25
|
+
import { findAndLoadConfig } from '../config/load'
|
|
26
|
+
import { parseINFTRef } from './_inft-ref'
|
|
27
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `promus inspect` — read what's anchored on chain for an iNFT.
|
|
31
|
+
*
|
|
32
|
+
* Modes (each compose with `--full`/`--json`/`--out <dir>`):
|
|
33
|
+
* default own agent: decrypt + render every slot
|
|
34
|
+
* --slot <name> own agent: only that slot
|
|
35
|
+
* --tx <hash> inspect what an `update()` tx anchored
|
|
36
|
+
* --raw own agent: no operator-wallet decrypt, just bytes
|
|
37
|
+
* --diff own agent: compare local files vs chain plaintext
|
|
38
|
+
* <ref> [+flags] foreign iNFT: ref is `0g-mainnet:0xCONTRACT:tokenId`
|
|
39
|
+
* or `eip155:16661:0xCONTRACT:tokenId`. Foreign always
|
|
40
|
+
* skips decrypt (you don't have the operator).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface InspectFlags {
|
|
44
|
+
/** Foreign iNFT ref. When set, command treats inspection as raw / no-decrypt. */
|
|
45
|
+
ref?: string
|
|
46
|
+
/** Single slot filter. Names match `INTELLIGENT_DATA_SLOTS`. */
|
|
47
|
+
slot?: IntelligentDataSlot
|
|
48
|
+
/** Inspect an `update` tx instead of current state. */
|
|
49
|
+
tx?: Hex
|
|
50
|
+
/** Skip operator decrypt entirely; just dump root hashes + ciphertext sizes. */
|
|
51
|
+
raw?: boolean
|
|
52
|
+
/** Compare local memory files vs chain plaintext. */
|
|
53
|
+
diff?: boolean
|
|
54
|
+
/** Emit structured JSON instead of human format. */
|
|
55
|
+
json?: boolean
|
|
56
|
+
/** Print full plaintext (default truncates to 40 lines per slot). */
|
|
57
|
+
full?: boolean
|
|
58
|
+
/** Dump each decrypted slot to `<out>/<slot>.md`. */
|
|
59
|
+
out?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PREVIEW_LINES = 40
|
|
63
|
+
|
|
64
|
+
export async function runInspect(flags: InspectFlags): Promise<void> {
|
|
65
|
+
if (flags.json) return runJson(flags)
|
|
66
|
+
|
|
67
|
+
intro('promus inspect')
|
|
68
|
+
|
|
69
|
+
if (flags.tx) {
|
|
70
|
+
await renderTxMode(flags)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Resolve which iNFT we're looking at.
|
|
75
|
+
const target = await resolveTarget(flags)
|
|
76
|
+
if (!target) return
|
|
77
|
+
const { network, contractAddress, tokenId, isForeign, config } = target
|
|
78
|
+
|
|
79
|
+
if (flags.diff) {
|
|
80
|
+
if (isForeign) {
|
|
81
|
+
cancel('--diff is only meaningful on your own agent (needs the memory key to decrypt).')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
await renderDiffMode({ network, contractAddress, tokenId, config, full: flags.full ?? false })
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const wantDecrypt = !isForeign && !flags.raw
|
|
89
|
+
let memoryKey: Buffer | undefined
|
|
90
|
+
if (wantDecrypt) {
|
|
91
|
+
const key = await unlockMemoryKey({ network, contractAddress, tokenId, config })
|
|
92
|
+
if (!key) return
|
|
93
|
+
memoryKey = key
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fetchSpin = spinner()
|
|
97
|
+
fetchSpin.start(
|
|
98
|
+
`Reading ${flags.slot ? `slot '${flags.slot}'` : 'all slots'} for iNFT #${tokenId} on ${network}`,
|
|
99
|
+
)
|
|
100
|
+
let result: InspectAgentResult
|
|
101
|
+
try {
|
|
102
|
+
result = await inspectAgent({
|
|
103
|
+
network,
|
|
104
|
+
contractAddress,
|
|
105
|
+
tokenId,
|
|
106
|
+
memoryKey,
|
|
107
|
+
slots: flags.slot ? [flags.slot] : undefined,
|
|
108
|
+
})
|
|
109
|
+
fetchSpin.stop(`fetched ${result.slots.length} slot(s)`)
|
|
110
|
+
} catch (e) {
|
|
111
|
+
fetchSpin.stop(`fetch failed: ${(e as Error).message.slice(0, 200)}`)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printAgentHeader({ network, contractAddress, tokenId, owner: result.owner, isForeign })
|
|
116
|
+
for (const inspection of result.slots) {
|
|
117
|
+
printSlot(inspection, { full: flags.full ?? false, raw: flags.raw ?? !wantDecrypt })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (flags.out) {
|
|
121
|
+
await dumpToDir(flags.out, result)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
outro(
|
|
125
|
+
`Inspected ${result.slots.length} slot(s).${flags.out ? ` Decrypted plaintext written to ${flags.out}/` : ''}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface ResolvedTarget {
|
|
130
|
+
network: PromusNetwork
|
|
131
|
+
contractAddress: Address
|
|
132
|
+
tokenId: bigint
|
|
133
|
+
isForeign: boolean
|
|
134
|
+
/** Active config when target was resolved from it; null when target came from a foreign ref. */
|
|
135
|
+
config: PromusConfig | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function resolveTarget(flags: InspectFlags): Promise<ResolvedTarget | null> {
|
|
139
|
+
if (flags.ref) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = parseINFTRef(flags.ref)
|
|
142
|
+
return { ...parsed, contractAddress: parsed.contract, isForeign: true, config: null }
|
|
143
|
+
} catch (e) {
|
|
144
|
+
cancel((e as Error).message)
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const loaded = await findAndLoadConfig()
|
|
149
|
+
if (!loaded) {
|
|
150
|
+
cancel(
|
|
151
|
+
'No promus config. Run `promus init` first or pass an iNFT ref like `0g-mainnet:0xCONTRACT:tokenId`.',
|
|
152
|
+
)
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
const { config } = loaded
|
|
156
|
+
if (!config.identity.iNFT) {
|
|
157
|
+
cancel('Active config has no iNFT. Run `promus init` first or pass a ref.')
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
network: config.network,
|
|
162
|
+
contractAddress: config.identity.iNFT.contract as Address,
|
|
163
|
+
tokenId: BigInt(config.identity.iNFT.tokenId),
|
|
164
|
+
isForeign: false,
|
|
165
|
+
config,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function unlockMemoryKey(target: {
|
|
170
|
+
network: PromusNetwork
|
|
171
|
+
contractAddress: Address
|
|
172
|
+
tokenId: bigint
|
|
173
|
+
config: PromusConfig | null
|
|
174
|
+
}): Promise<Buffer | null> {
|
|
175
|
+
const config = target.config
|
|
176
|
+
if (!config?.identity.agent) {
|
|
177
|
+
cancel(
|
|
178
|
+
'Active config has no agent address; cannot derive memory key. Pass `--raw` to skip decrypt.',
|
|
179
|
+
)
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
const agentAddress = config.identity.agent as Address
|
|
183
|
+
const agentId = iNFTAgentId({ contractAddress: target.contractAddress, tokenId: target.tokenId })
|
|
184
|
+
const paths = agentPaths.agent(agentId)
|
|
185
|
+
|
|
186
|
+
const operator = await loadOrPickOperatorSigner({
|
|
187
|
+
network: target.network,
|
|
188
|
+
hint: config.operator,
|
|
189
|
+
})
|
|
190
|
+
if (!operator) {
|
|
191
|
+
cancel('No operator wallet available; cannot decrypt keystore. Pass `--raw` to skip.')
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
const sUnlock = spinner()
|
|
195
|
+
sUnlock.start('Fetching keystore + decrypting via operator wallet')
|
|
196
|
+
try {
|
|
197
|
+
const decrypted = await fetchAndDecryptKeystore({
|
|
198
|
+
network: target.network,
|
|
199
|
+
contractAddress: target.contractAddress,
|
|
200
|
+
tokenId: target.tokenId,
|
|
201
|
+
signer: operator,
|
|
202
|
+
agentAddress,
|
|
203
|
+
cachePath: paths.keystore,
|
|
204
|
+
})
|
|
205
|
+
sUnlock.stop(`unlocked (source: ${decrypted.source})`)
|
|
206
|
+
return deriveMemoryKey(decrypted.privkeyHex)
|
|
207
|
+
} catch (e) {
|
|
208
|
+
sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
|
|
209
|
+
return null
|
|
210
|
+
} finally {
|
|
211
|
+
await operator.close?.()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function printAgentHeader(opts: {
|
|
216
|
+
network: PromusNetwork
|
|
217
|
+
contractAddress: Address
|
|
218
|
+
tokenId: bigint
|
|
219
|
+
owner: Address
|
|
220
|
+
isForeign: boolean
|
|
221
|
+
}): void {
|
|
222
|
+
const { network, contractAddress, tokenId, owner, isForeign } = opts
|
|
223
|
+
console.log('')
|
|
224
|
+
console.log(` iNFT #${tokenId} at ${contractAddress} (${network})`)
|
|
225
|
+
console.log(` ${explorerTokenUrl(network, contractAddress, tokenId)}`)
|
|
226
|
+
console.log(` owner ${owner}${isForeign ? ' (foreign — raw view only)' : ''}`)
|
|
227
|
+
console.log('')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function printSlot(s: SlotInspection, opts: { full: boolean; raw: boolean }): void {
|
|
231
|
+
const idx = INTELLIGENT_DATA_SLOTS.indexOf(s.slot)
|
|
232
|
+
const idxLabel = idx >= 0 ? ` (slot ${idx})` : ''
|
|
233
|
+
console.log('')
|
|
234
|
+
console.log(`──── ${s.slot}${idxLabel}`)
|
|
235
|
+
console.log(` rootHash ${s.rootHash}`)
|
|
236
|
+
|
|
237
|
+
if (s.empty) {
|
|
238
|
+
console.log(' status empty (still bootstrap placeholder; never anchored)')
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (s.decryptStatus === 'fetch-failed') {
|
|
243
|
+
console.log(` status fetch failed: ${s.fetchError ?? 'unknown'}`)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(` ciphertext ${s.ciphertext?.byteLength ?? 0} bytes`)
|
|
248
|
+
|
|
249
|
+
if (opts.raw || s.decryptStatus === 'no-key' || s.decryptStatus === 'keystore-skipped') {
|
|
250
|
+
if (s.slot === 'keystore') {
|
|
251
|
+
console.log(' decrypt skipped (keystore is operator-encrypted, not memory-key)')
|
|
252
|
+
const head = previewBytesAsHex(s.ciphertext, 64)
|
|
253
|
+
if (head) console.log(` hex (head) ${head}`)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
console.log(` decrypt ${opts.raw ? 'skipped (--raw)' : 'skipped (no memory key)'}`)
|
|
257
|
+
const head = previewBytesAsHex(s.ciphertext, 64)
|
|
258
|
+
if (head) console.log(` hex (head) ${head}`)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (s.decryptStatus === 'decrypt-failed') {
|
|
263
|
+
console.log(` decrypt FAILED: ${s.decryptError ?? 'unknown'}`)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!s.plaintext) {
|
|
268
|
+
console.log(` decrypt unexpected null plaintext (status=${s.decryptStatus})`)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(` plaintext ${s.plaintext.byteLength} bytes`)
|
|
273
|
+
console.log(` hash ${s.plaintextHash}`)
|
|
274
|
+
console.log(' content:')
|
|
275
|
+
const text = new TextDecoder().decode(s.plaintext)
|
|
276
|
+
const lines = text.split('\n')
|
|
277
|
+
const cap = opts.full ? lines.length : PREVIEW_LINES
|
|
278
|
+
for (const line of lines.slice(0, cap)) console.log(` │ ${line}`)
|
|
279
|
+
if (lines.length > cap) {
|
|
280
|
+
console.log(` │ … (${lines.length - cap} more lines — pass --full to see them)`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function previewBytesAsHex(bytes: Uint8Array | null, n: number): string | null {
|
|
285
|
+
if (!bytes) return null
|
|
286
|
+
return Buffer.from(bytes.subarray(0, n)).toString('hex')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function renderTxMode(flags: InspectFlags): Promise<void> {
|
|
290
|
+
if (!flags.tx) {
|
|
291
|
+
cancel('--tx requires a tx hash')
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
const target = await resolveTarget(flags)
|
|
295
|
+
if (!target) return
|
|
296
|
+
const { network, contractAddress, isForeign } = target
|
|
297
|
+
|
|
298
|
+
const sFetch = spinner()
|
|
299
|
+
sFetch.start(`Decoding tx ${flags.tx}`)
|
|
300
|
+
let txInfo: TxInspection
|
|
301
|
+
try {
|
|
302
|
+
txInfo = await inspectTx({ network, contractAddress, txHash: flags.tx })
|
|
303
|
+
sFetch.stop(`block ${txInfo.blockNumber} — ${txInfo.slots.length} slot(s) updated`)
|
|
304
|
+
} catch (e) {
|
|
305
|
+
sFetch.stop(`tx decode failed: ${(e as Error).message.slice(0, 200)}`)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('')
|
|
310
|
+
console.log(` tx ${txInfo.txHash}`)
|
|
311
|
+
console.log(` ${explorerTxUrl(network, txInfo.txHash)}`)
|
|
312
|
+
console.log(` block ${txInfo.blockNumber}`)
|
|
313
|
+
console.log(` iNFT #${txInfo.tokenId} at ${contractAddress} (${network})`)
|
|
314
|
+
console.log('')
|
|
315
|
+
console.log(' slots anchored at this tx:')
|
|
316
|
+
for (let i = 0; i < txInfo.slots.length; i++) {
|
|
317
|
+
const slot = txInfo.slots[i]!
|
|
318
|
+
const at = txInfo.hashesAtTx[i]!
|
|
319
|
+
const cur = txInfo.current.get(slot) ?? bootstrapHashFor(slot)
|
|
320
|
+
const same = at.toLowerCase() === cur.toLowerCase()
|
|
321
|
+
console.log(` • ${slot}`)
|
|
322
|
+
console.log(` anchored at tx: ${at}`)
|
|
323
|
+
console.log(` current on chain: ${cur}${same ? '' : ' ⚠ superseded by a later tx'}`)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (flags.raw || isForeign) {
|
|
327
|
+
outro('Pass without --raw / on your own iNFT to see decrypted content.')
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const memoryKey = await unlockMemoryKey({
|
|
332
|
+
network,
|
|
333
|
+
contractAddress,
|
|
334
|
+
tokenId: txInfo.tokenId,
|
|
335
|
+
config: target.config,
|
|
336
|
+
})
|
|
337
|
+
if (!memoryKey) return
|
|
338
|
+
|
|
339
|
+
console.log('')
|
|
340
|
+
console.log(' current decrypted content for those slots:')
|
|
341
|
+
const result = await inspectAgent({
|
|
342
|
+
network,
|
|
343
|
+
contractAddress,
|
|
344
|
+
tokenId: txInfo.tokenId,
|
|
345
|
+
memoryKey,
|
|
346
|
+
slots: txInfo.slots,
|
|
347
|
+
})
|
|
348
|
+
for (const inspection of result.slots) {
|
|
349
|
+
printSlot(inspection, { full: flags.full ?? false, raw: false })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
outro(`Decoded tx + ${result.slots.length} current slot(s).`)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function renderDiffMode(opts: {
|
|
356
|
+
network: PromusNetwork
|
|
357
|
+
contractAddress: Address
|
|
358
|
+
tokenId: bigint
|
|
359
|
+
config: PromusConfig | null
|
|
360
|
+
full: boolean
|
|
361
|
+
}): Promise<void> {
|
|
362
|
+
const memoryKey = await unlockMemoryKey({
|
|
363
|
+
network: opts.network,
|
|
364
|
+
contractAddress: opts.contractAddress,
|
|
365
|
+
tokenId: opts.tokenId,
|
|
366
|
+
config: opts.config,
|
|
367
|
+
})
|
|
368
|
+
if (!memoryKey) return
|
|
369
|
+
|
|
370
|
+
const agentId = iNFTAgentId({
|
|
371
|
+
contractAddress: opts.contractAddress,
|
|
372
|
+
tokenId: opts.tokenId,
|
|
373
|
+
})
|
|
374
|
+
const paths = agentPaths.agent(agentId)
|
|
375
|
+
const localPaths: Partial<Record<IntelligentDataSlot, string>> = {
|
|
376
|
+
'memory-index': paths.memoryIndex,
|
|
377
|
+
identity: join(paths.agentMemoryDir, 'identity.md'),
|
|
378
|
+
persona: join(paths.agentMemoryDir, 'persona.md'),
|
|
379
|
+
'activity-log': paths.activityLog,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sDiff = spinner()
|
|
383
|
+
sDiff.start('Comparing local memory files vs chain plaintext')
|
|
384
|
+
let diffs: SlotDiff[]
|
|
385
|
+
try {
|
|
386
|
+
diffs = await diffAgent({
|
|
387
|
+
network: opts.network,
|
|
388
|
+
contractAddress: opts.contractAddress,
|
|
389
|
+
tokenId: opts.tokenId,
|
|
390
|
+
memoryKey,
|
|
391
|
+
localPaths,
|
|
392
|
+
})
|
|
393
|
+
sDiff.stop(`compared ${diffs.length} slot(s)`)
|
|
394
|
+
} catch (e) {
|
|
395
|
+
sDiff.stop(`diff failed: ${(e as Error).message.slice(0, 200)}`)
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('')
|
|
400
|
+
for (const d of diffs) {
|
|
401
|
+
console.log(`──── ${d.slot}`)
|
|
402
|
+
console.log(` status ${d.status}`)
|
|
403
|
+
console.log(` chain root ${d.chainRootHash}`)
|
|
404
|
+
if (d.localHash)
|
|
405
|
+
console.log(` local hash ${d.localHash} (${d.local?.byteLength ?? 0} bytes)`)
|
|
406
|
+
else console.log(' local (missing)')
|
|
407
|
+
if (d.chainHash)
|
|
408
|
+
console.log(` chain hash ${d.chainHash} (${d.chain?.byteLength ?? 0} bytes)`)
|
|
409
|
+
else console.log(' chain (empty / cannot decrypt)')
|
|
410
|
+
if (d.chainError) console.log(` chain error ${d.chainError}`)
|
|
411
|
+
|
|
412
|
+
if (d.status === 'differ' && opts.full && d.local && d.chain) {
|
|
413
|
+
const localText = new TextDecoder().decode(d.local).split('\n')
|
|
414
|
+
const chainText = new TextDecoder().decode(d.chain).split('\n')
|
|
415
|
+
console.log(' diff (first 20 lines each):')
|
|
416
|
+
console.log(' local:')
|
|
417
|
+
for (const line of localText.slice(0, 20)) console.log(` + ${line}`)
|
|
418
|
+
console.log(' chain:')
|
|
419
|
+
for (const line of chainText.slice(0, 20)) console.log(` - ${line}`)
|
|
420
|
+
}
|
|
421
|
+
console.log('')
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const drift = diffs.filter(d => d.status !== 'in-sync' && d.status !== 'both-missing')
|
|
425
|
+
if (drift.length === 0) {
|
|
426
|
+
outro('All synced slots match chain plaintext exactly.')
|
|
427
|
+
} else {
|
|
428
|
+
note(
|
|
429
|
+
`${drift.length} slot(s) drifted: ${drift.map(d => `${d.slot}:${d.status}`).join(', ')}`,
|
|
430
|
+
'drift detected',
|
|
431
|
+
)
|
|
432
|
+
outro('Run `promus sync` to push local → chain, or pull chain via `promus inspect --out <dir>`.')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function dumpToDir(out: string, result: InspectAgentResult): Promise<void> {
|
|
437
|
+
await mkdir(out, { recursive: true })
|
|
438
|
+
const sumLines: string[] = [
|
|
439
|
+
'# promus inspect dump',
|
|
440
|
+
'',
|
|
441
|
+
`iNFT: ${result.contractAddress} #${result.tokenId} (${result.network})`,
|
|
442
|
+
`owner: ${result.owner}`,
|
|
443
|
+
'',
|
|
444
|
+
'## slots',
|
|
445
|
+
'',
|
|
446
|
+
]
|
|
447
|
+
for (const s of result.slots) {
|
|
448
|
+
sumLines.push(`- **${s.slot}** — ${s.rootHash} — ${s.decryptStatus}`)
|
|
449
|
+
if (s.plaintext) {
|
|
450
|
+
const path = join(out, `${s.slot}.md`)
|
|
451
|
+
await writeFile(path, s.plaintext)
|
|
452
|
+
}
|
|
453
|
+
if (s.ciphertext && s.decryptStatus !== 'ok') {
|
|
454
|
+
const path = join(out, `${s.slot}.bin`)
|
|
455
|
+
await writeFile(path, s.ciphertext)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
await writeFile(join(out, 'README.md'), sumLines.join('\n'))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function runJson(flags: InspectFlags): Promise<void> {
|
|
462
|
+
const target = await resolveTarget(flags)
|
|
463
|
+
if (!target) return
|
|
464
|
+
try {
|
|
465
|
+
if (flags.tx) {
|
|
466
|
+
const txInfo = await inspectTx({
|
|
467
|
+
network: target.network,
|
|
468
|
+
contractAddress: target.contractAddress,
|
|
469
|
+
txHash: flags.tx,
|
|
470
|
+
})
|
|
471
|
+
process.stdout.write(`${JSON.stringify(serializeTx(txInfo), null, 2)}\n`)
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
let memoryKey: Buffer | undefined
|
|
475
|
+
if (!flags.raw && !target.isForeign) {
|
|
476
|
+
const key = await unlockMemoryKey(target)
|
|
477
|
+
if (!key) return
|
|
478
|
+
memoryKey = key
|
|
479
|
+
}
|
|
480
|
+
const result = await inspectAgent({
|
|
481
|
+
network: target.network,
|
|
482
|
+
contractAddress: target.contractAddress,
|
|
483
|
+
tokenId: target.tokenId,
|
|
484
|
+
memoryKey,
|
|
485
|
+
slots: flags.slot ? [flags.slot] : undefined,
|
|
486
|
+
})
|
|
487
|
+
process.stdout.write(`${JSON.stringify(serializeResult(result), null, 2)}\n`)
|
|
488
|
+
} catch (e) {
|
|
489
|
+
process.stderr.write(`error: ${(e as Error).message}\n`)
|
|
490
|
+
process.exit(1)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function serializeResult(r: InspectAgentResult): unknown {
|
|
495
|
+
return {
|
|
496
|
+
network: r.network,
|
|
497
|
+
contractAddress: r.contractAddress,
|
|
498
|
+
tokenId: r.tokenId.toString(),
|
|
499
|
+
owner: r.owner,
|
|
500
|
+
slots: r.slots.map(s => ({
|
|
501
|
+
slot: s.slot,
|
|
502
|
+
rootHash: s.rootHash,
|
|
503
|
+
empty: s.empty,
|
|
504
|
+
decryptStatus: s.decryptStatus,
|
|
505
|
+
decryptError: s.decryptError,
|
|
506
|
+
fetchError: s.fetchError,
|
|
507
|
+
ciphertextSize: s.ciphertext?.byteLength ?? null,
|
|
508
|
+
plaintextSize: s.plaintext?.byteLength ?? null,
|
|
509
|
+
plaintextHash: s.plaintextHash,
|
|
510
|
+
plaintext: s.plaintext ? new TextDecoder().decode(s.plaintext) : null,
|
|
511
|
+
})),
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function serializeTx(t: TxInspection): unknown {
|
|
516
|
+
return {
|
|
517
|
+
txHash: t.txHash,
|
|
518
|
+
blockNumber: t.blockNumber.toString(),
|
|
519
|
+
blockHash: t.blockHash,
|
|
520
|
+
tokenId: t.tokenId.toString(),
|
|
521
|
+
slots: t.slots,
|
|
522
|
+
hashesAtTx: t.hashesAtTx,
|
|
523
|
+
current: Array.from(t.current.entries()).map(([slot, hash]) => ({ slot, hash })),
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function isValidSlot(name: string): name is IntelligentDataSlot {
|
|
528
|
+
return (INTELLIGENT_DATA_SLOTS as readonly string[]).includes(name)
|
|
529
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, log, outro, spinner } from '@clack/prompts'
|
|
2
|
+
import {
|
|
3
|
+
type PromusNetwork,
|
|
4
|
+
closeLedger,
|
|
5
|
+
getLedgerDetail,
|
|
6
|
+
refundFromLedger,
|
|
7
|
+
retrieveLedgerFunds,
|
|
8
|
+
} from '@promus/core'
|
|
9
|
+
import { type Address, type Hex, formatEther, parseEther } from 'viem'
|
|
10
|
+
import { findAndLoadConfig } from '../config/load'
|
|
11
|
+
import { withSilencedConsole } from '../util/silence-console'
|
|
12
|
+
import { unlockAgentSigner } from './_unlock'
|
|
13
|
+
|
|
14
|
+
export type LedgerSubcommand = 'balance' | 'refund' | 'retrieve' | 'close'
|
|
15
|
+
|
|
16
|
+
export interface LedgerOpts {
|
|
17
|
+
sub: LedgerSubcommand
|
|
18
|
+
/** For `refund`: amount in 0G to withdraw. Omit + `all=true` = withdraw full main balance. */
|
|
19
|
+
amount?: number
|
|
20
|
+
/** For `refund`: refund the entire main ledger balance. */
|
|
21
|
+
all?: boolean
|
|
22
|
+
/** For `close`: skip the destructive confirmation prompt. */
|
|
23
|
+
yes?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runLedger(opts: LedgerOpts): Promise<void> {
|
|
27
|
+
intro(`promus ledger ${opts.sub}`)
|
|
28
|
+
|
|
29
|
+
const loaded = await findAndLoadConfig()
|
|
30
|
+
if (!loaded) {
|
|
31
|
+
cancel('No promus.config.ts found. Run `promus init` first.')
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const { config } = loaded
|
|
35
|
+
if (!config.identity.iNFT || !config.identity.agent) {
|
|
36
|
+
cancel('Config has no iNFT or agent. Run `promus init` first.')
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const network = config.network
|
|
41
|
+
const agentAddress = config.identity.agent as Address
|
|
42
|
+
|
|
43
|
+
if (opts.sub === 'balance') {
|
|
44
|
+
const unlocked = await unlockAgentSigner(config)
|
|
45
|
+
if (!unlocked) return
|
|
46
|
+
try {
|
|
47
|
+
await printBalance(network, unlocked.agentPrivkey, agentAddress)
|
|
48
|
+
outro('balance shown')
|
|
49
|
+
} finally {
|
|
50
|
+
await unlocked.close()
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (opts.sub === 'retrieve') {
|
|
56
|
+
const unlocked = await unlockAgentSigner(config)
|
|
57
|
+
if (!unlocked) return
|
|
58
|
+
const s = spinner()
|
|
59
|
+
s.start('Retrieving funds from inference provider sub-accounts')
|
|
60
|
+
try {
|
|
61
|
+
await withSilencedConsole(() =>
|
|
62
|
+
retrieveLedgerFunds({ network, privkeyHex: unlocked.agentPrivkey }),
|
|
63
|
+
)
|
|
64
|
+
s.stop('retrieve submitted')
|
|
65
|
+
log.info(
|
|
66
|
+
'Provider sub-accounts now have a pending refund. Some balance returns immediately; the rest unlocks after the contract lock window. Re-run `promus ledger retrieve` after the window to pull what was queued.',
|
|
67
|
+
)
|
|
68
|
+
await printBalance(network, unlocked.agentPrivkey, agentAddress)
|
|
69
|
+
outro('retrieve done')
|
|
70
|
+
} catch (e) {
|
|
71
|
+
s.stop(`retrieve failed: ${(e as Error).message.slice(0, 160)}`)
|
|
72
|
+
} finally {
|
|
73
|
+
await unlocked.close()
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (opts.sub === 'refund') {
|
|
79
|
+
const unlocked = await unlockAgentSigner(config)
|
|
80
|
+
if (!unlocked) return
|
|
81
|
+
try {
|
|
82
|
+
const detail = await withSilencedConsole(() =>
|
|
83
|
+
getLedgerDetail({ network, privkeyHex: unlocked.agentPrivkey }),
|
|
84
|
+
)
|
|
85
|
+
if (!detail) {
|
|
86
|
+
log.warn('No ledger exists for this agent.')
|
|
87
|
+
outro('nothing to refund')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let amount = opts.amount
|
|
92
|
+
if (opts.all || amount === undefined) {
|
|
93
|
+
amount = Number(formatEther(detail.availableBalance))
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
96
|
+
log.warn(
|
|
97
|
+
`Available balance is ${formatEther(detail.availableBalance)} 0G; nothing to refund.`,
|
|
98
|
+
)
|
|
99
|
+
outro('nothing to refund')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (parseEther(amount.toString()) > detail.availableBalance) {
|
|
103
|
+
log.warn(
|
|
104
|
+
`Requested ${amount} 0G but only ${formatEther(detail.availableBalance)} 0G is available in the main ledger. Run \`promus ledger retrieve\` first if funds are still in provider sub-accounts.`,
|
|
105
|
+
)
|
|
106
|
+
outro('refund skipped')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const s = spinner()
|
|
111
|
+
s.start(`Refunding ${amount} 0G from main ledger to ${agentAddress}`)
|
|
112
|
+
await withSilencedConsole(() =>
|
|
113
|
+
refundFromLedger({ network, privkeyHex: unlocked.agentPrivkey, amount: amount as number }),
|
|
114
|
+
)
|
|
115
|
+
s.stop('refund submitted')
|
|
116
|
+
await printBalance(network, unlocked.agentPrivkey, agentAddress)
|
|
117
|
+
outro(`refunded ${amount} 0G to agent EOA`)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
log.error(`refund failed: ${(e as Error).message.slice(0, 160)}`)
|
|
120
|
+
} finally {
|
|
121
|
+
await unlocked.close()
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// close
|
|
127
|
+
const unlocked = await unlockAgentSigner(config)
|
|
128
|
+
if (!unlocked) return
|
|
129
|
+
try {
|
|
130
|
+
if (!opts.yes) {
|
|
131
|
+
const ok = (await confirm({
|
|
132
|
+
message:
|
|
133
|
+
'Close the ledger entirely? Funds in provider sub-accounts must be retrieved first.',
|
|
134
|
+
initialValue: false,
|
|
135
|
+
})) as boolean | symbol
|
|
136
|
+
if (isCancel(ok) || !ok) {
|
|
137
|
+
cancel('Aborted.')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const s = spinner()
|
|
142
|
+
s.start('Deleting ledger')
|
|
143
|
+
await withSilencedConsole(() => closeLedger({ network, privkeyHex: unlocked.agentPrivkey }))
|
|
144
|
+
s.stop('ledger closed')
|
|
145
|
+
outro('ledger removed; remaining main balance refunded to agent EOA')
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log.error(`close failed: ${(e as Error).message.slice(0, 160)}`)
|
|
148
|
+
} finally {
|
|
149
|
+
await unlocked.close()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function printBalance(
|
|
154
|
+
network: PromusNetwork,
|
|
155
|
+
privkeyHex: Hex,
|
|
156
|
+
agentAddress: Address,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const detail = await withSilencedConsole(() => getLedgerDetail({ network, privkeyHex }))
|
|
159
|
+
if (!detail) {
|
|
160
|
+
log.info('No ledger exists for this agent yet.')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
log.info(
|
|
164
|
+
[
|
|
165
|
+
`agent ${agentAddress}`,
|
|
166
|
+
`available ${formatEther(detail.availableBalance)} 0G`,
|
|
167
|
+
`total ${formatEther(detail.totalBalance)} 0G`,
|
|
168
|
+
`providers ${detail.inferenceProviders.length}`,
|
|
169
|
+
].join('\n'),
|
|
170
|
+
)
|
|
171
|
+
for (const p of detail.inferenceProviders) {
|
|
172
|
+
log.info(
|
|
173
|
+
`· ${p.provider}: balance=${formatEther(p.balance)} 0G, pendingRefund=${formatEther(p.pendingRefund)} 0G`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|