@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.
Files changed (96) hide show
  1. package/README.md +18 -0
  2. package/bin/promus +33 -0
  3. package/package.json +51 -0
  4. package/src/commands/_agents.ts +14 -0
  5. package/src/commands/_inft-ref.ts +43 -0
  6. package/src/commands/_unlock.ts +74 -0
  7. package/src/commands/admin-autotopup-tick.ts +73 -0
  8. package/src/commands/admin.test.ts +34 -0
  9. package/src/commands/admin.ts +32 -0
  10. package/src/commands/balance.test.ts +10 -0
  11. package/src/commands/balance.ts +112 -0
  12. package/src/commands/chat-sandbox.tsx +520 -0
  13. package/src/commands/chat-telegram.ts +398 -0
  14. package/src/commands/chat.tsx +1916 -0
  15. package/src/commands/deploy.ts +204 -0
  16. package/src/commands/drain.ts +90 -0
  17. package/src/commands/gateway-logs.ts +47 -0
  18. package/src/commands/gateway-run.ts +54 -0
  19. package/src/commands/gateway-start.ts +218 -0
  20. package/src/commands/gateway-status.ts +88 -0
  21. package/src/commands/gateway-stop.ts +133 -0
  22. package/src/commands/gateway.ts +101 -0
  23. package/src/commands/init/cost.test.ts +169 -0
  24. package/src/commands/init/cost.ts +154 -0
  25. package/src/commands/init/funding-gate.ts +67 -0
  26. package/src/commands/init/model-picker.ts +81 -0
  27. package/src/commands/init/operator-picker.ts +263 -0
  28. package/src/commands/init/resume.ts +136 -0
  29. package/src/commands/init/sandbox-provision.test.ts +497 -0
  30. package/src/commands/init/sandbox-provision.ts +1177 -0
  31. package/src/commands/init/telegram-step.ts +229 -0
  32. package/src/commands/init/wizard-state.ts +95 -0
  33. package/src/commands/init.ts +612 -0
  34. package/src/commands/inspect.ts +529 -0
  35. package/src/commands/ledger.ts +176 -0
  36. package/src/commands/logs.ts +86 -0
  37. package/src/commands/migrate-keystore.ts +155 -0
  38. package/src/commands/model.ts +48 -0
  39. package/src/commands/pairing-approve.ts +114 -0
  40. package/src/commands/pairing-clear.ts +42 -0
  41. package/src/commands/pairing-list.ts +58 -0
  42. package/src/commands/pairing-revoke.ts +52 -0
  43. package/src/commands/pairing.test.ts +88 -0
  44. package/src/commands/pairing.ts +81 -0
  45. package/src/commands/pause.ts +99 -0
  46. package/src/commands/profile.ts +184 -0
  47. package/src/commands/restore.ts +221 -0
  48. package/src/commands/resume.ts +181 -0
  49. package/src/commands/status.ts +119 -0
  50. package/src/commands/sync.ts +147 -0
  51. package/src/commands/telegram-remove.ts +65 -0
  52. package/src/commands/telegram-setup.ts +74 -0
  53. package/src/commands/telegram-status.ts +89 -0
  54. package/src/commands/telegram.test.ts +50 -0
  55. package/src/commands/telegram.ts +44 -0
  56. package/src/commands/topup.ts +303 -0
  57. package/src/commands/transfer.test.ts +111 -0
  58. package/src/commands/transfer.ts +520 -0
  59. package/src/commands/upgrade.test.ts +137 -0
  60. package/src/commands/upgrade.ts +690 -0
  61. package/src/config/load.ts +35 -0
  62. package/src/config/render.test.ts +96 -0
  63. package/src/config/render.ts +110 -0
  64. package/src/index.ts +378 -0
  65. package/src/sandbox/client.test.ts +251 -0
  66. package/src/sandbox/client.ts +424 -0
  67. package/src/ui/app.tsx +677 -0
  68. package/src/ui/approval-summary.test.ts +154 -0
  69. package/src/ui/approval-summary.ts +34 -0
  70. package/src/ui/markdown-parse.ts +219 -0
  71. package/src/ui/markdown.test.ts +146 -0
  72. package/src/ui/markdown.tsx +37 -0
  73. package/src/ui/state.test.ts +74 -0
  74. package/src/ui/state.ts +198 -0
  75. package/src/util/bootstrap-mode.test.ts +40 -0
  76. package/src/util/bootstrap-mode.ts +25 -0
  77. package/src/util/bootstrap-progress-box.test.ts +190 -0
  78. package/src/util/bootstrap-progress-box.ts +378 -0
  79. package/src/util/brain-secrets.ts +96 -0
  80. package/src/util/cli-version.ts +28 -0
  81. package/src/util/format.test.ts +16 -0
  82. package/src/util/format.ts +11 -0
  83. package/src/util/gateway-spawn.test.ts +86 -0
  84. package/src/util/gateway-spawn.ts +128 -0
  85. package/src/util/gateway-version.test.ts +113 -0
  86. package/src/util/gateway-version.ts +154 -0
  87. package/src/util/github-releases.test.ts +116 -0
  88. package/src/util/github-releases.ts +79 -0
  89. package/src/util/profile-key.test.ts +60 -0
  90. package/src/util/profile-key.ts +25 -0
  91. package/src/util/ref-resolver.test.ts +77 -0
  92. package/src/util/ref-resolver.ts +55 -0
  93. package/src/util/silence-console.test.ts +53 -0
  94. package/src/util/silence-console.ts +40 -0
  95. package/src/util/telegram-secrets.test.ts +227 -0
  96. 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
+ }