@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,181 @@
1
+ import { cancel, confirm, intro, isCancel, note, outro, spinner } from '@clack/prompts'
2
+ import {
3
+ type PromusNetwork,
4
+ SANDBOX_PROVIDER_URL_GALILEO,
5
+ SandboxProviderClient,
6
+ iNFTAgentId,
7
+ } from '@promus/core'
8
+ import type { Address, Hex } from 'viem'
9
+ import { findAndLoadConfig } from '../config/load'
10
+ import { loadProfileScopeKeyHex } from '../util/profile-key'
11
+ import { loadTelegramHandoffSecrets } from '../util/telegram-secrets'
12
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
13
+ import {
14
+ preflightProviderDeposit,
15
+ resumeArchivedSandbox,
16
+ unlockAgentKeystore,
17
+ } from './init/sandbox-provision'
18
+
19
+ interface ResumeOpts {
20
+ yes?: boolean
21
+ }
22
+
23
+ /**
24
+ * `promus resume`: wake a stopped/archived sandbox and re-handoff the agent
25
+ * privkey to the (newly restarted) harness. Use when:
26
+ *
27
+ * - Daytona's billing daemon archived the sandbox (INSUFFICIENT_BALANCE)
28
+ * - The autoArchiveInterval timer fired after a Daytona infra event stopped
29
+ * the sandbox briefly
30
+ * - You manually called `archive` and now want it back
31
+ *
32
+ * Same sandbox UUID + endpoint preserved. ~30s for stopped sandboxes,
33
+ * 2-5 min for archived (Daytona restores filesystem from object storage).
34
+ */
35
+ export async function runResume(opts: ResumeOpts = {}): Promise<void> {
36
+ intro('promus resume')
37
+
38
+ const loaded = await findAndLoadConfig()
39
+ if (!loaded) {
40
+ cancel('No promus.config.ts found.')
41
+ return
42
+ }
43
+ const { config } = loaded
44
+ if (!config.identity.iNFT || !config.identity.agent) {
45
+ cancel('Config has no iNFT or agent.')
46
+ return
47
+ }
48
+ if (config.deployTarget !== 'sandbox' || !config.sandbox?.id || !config.sandbox.endpoint) {
49
+ cancel(
50
+ `Agent is not deployed to a sandbox. (deployTarget=${config.deployTarget ?? 'local'}). Nothing to resume.`,
51
+ )
52
+ return
53
+ }
54
+ if (!config.brain.provider) {
55
+ cancel('Brain provider not configured. Run `promus model` first.')
56
+ return
57
+ }
58
+
59
+ const contractAddress = config.identity.iNFT.contract as Address
60
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
61
+ const agentAddress = config.identity.agent as Address
62
+ const sandboxId = config.sandbox.id
63
+
64
+ const operator = await loadOrPickOperatorSigner({
65
+ network: config.network,
66
+ hint: config.operator,
67
+ })
68
+ if (!operator) {
69
+ cancel('No operator wallet available; cannot decrypt keystore.')
70
+ return
71
+ }
72
+
73
+ // Pre-flight Galileo deposit check. The May 2 INSUFFICIENT_BALANCE incident
74
+ // archived enigma; refusing up-front with a clear suggestion is much better
75
+ // UX than letting resume run, sign the keystore unlock, then fail mid-flow.
76
+ if (!(await preflightProviderDeposit(operator))) {
77
+ await operator.close?.()
78
+ return
79
+ }
80
+
81
+ if (!opts.yes) {
82
+ const ok = await confirm({
83
+ message: `Resume sandbox ${sandboxId.slice(0, 8)}? (~30s if stopped, ~2-5min if archived)`,
84
+ initialValue: true,
85
+ })
86
+ if (isCancel(ok) || !ok) {
87
+ cancel('Aborted.')
88
+ await operator.close?.()
89
+ return
90
+ }
91
+ }
92
+
93
+ const sUnlock = spinner()
94
+ sUnlock.start('Fetching keystore + decrypting via operator wallet')
95
+ let agentPrivkey: Hex
96
+ try {
97
+ agentPrivkey = await unlockAgentKeystore({
98
+ operator,
99
+ network: config.network,
100
+ contractAddress,
101
+ tokenId,
102
+ agentAddress,
103
+ })
104
+ sUnlock.stop('unlocked')
105
+ } catch (e) {
106
+ sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
107
+ await operator.close?.()
108
+ return
109
+ }
110
+
111
+ const operatorAccount = await operator.account()
112
+ const provider = new SandboxProviderClient({
113
+ endpoint: SANDBOX_PROVIDER_URL_GALILEO,
114
+ operator: operatorAccount,
115
+ })
116
+
117
+ // Ship telegram secrets via secondary envelope so the resumed harness
118
+ // restores its grammY listener. Without this, every pause→resume cycle
119
+ // silently strips the bot — gateway comes back with `plugins: ['telegram']`
120
+ // but no token, and `build-runtime.ts` skips listener registration.
121
+ const telegramSecretsPlain = await loadTelegramHandoffSecrets({
122
+ signer: operator,
123
+ agentAddress,
124
+ contractAddress,
125
+ tokenId,
126
+ onNotice: msg => note(`${msg}; resume continues without TG.`, 'warning'),
127
+ })
128
+ const resumeAgentId = iNFTAgentId({ contractAddress, tokenId })
129
+ const resumeProfileKeyHex = loadProfileScopeKeyHex(resumeAgentId)
130
+ if (!resumeProfileKeyHex) {
131
+ note('no cached PROFILE key; resumed sandbox will boot without profile-slot anchoring', 'note')
132
+ }
133
+
134
+ const sBox = spinner()
135
+ sBox.start('Resuming sandbox')
136
+ try {
137
+ const result = await resumeArchivedSandbox({
138
+ provider,
139
+ sandboxId,
140
+ sandboxEndpoint: config.sandbox.endpoint,
141
+ operatorAccount,
142
+ agentPrivkey,
143
+ agentAddress,
144
+ iNFTRef: { contract: contractAddress, tokenId },
145
+ iNFTNetwork: config.network as PromusNetwork,
146
+ brain: { provider: config.brain.provider as Address, model: config.brain.model ?? '' },
147
+ subname: config.subname,
148
+ plugins: config.plugins,
149
+ telegramSecrets: telegramSecretsPlain,
150
+ profileScopeKeyHex: resumeProfileKeyHex,
151
+ onProgress: msg => sBox.message(msg),
152
+ })
153
+ if (result.alreadyReady) {
154
+ sBox.stop(`sandbox ${sandboxId.slice(0, 8)} already Ready (no-op)`)
155
+ } else {
156
+ sBox.stop(`sandbox ${sandboxId.slice(0, 8)} resumed from ${result.initialState} → started`)
157
+ }
158
+ outro(
159
+ [
160
+ '',
161
+ ` sandbox ${sandboxId} (unchanged)`,
162
+ ` endpoint ${config.sandbox.endpoint} (unchanged)`,
163
+ ` state before ${result.initialState}`,
164
+ ' state now started',
165
+ '',
166
+ 'Next: `promus` to chat',
167
+ ].join('\n'),
168
+ )
169
+ } catch (e) {
170
+ sBox.stop(`resume failed: ${(e as Error).message.slice(0, 200)}`)
171
+ note(
172
+ [
173
+ 'The sandbox could not be brought back to started state.',
174
+ 'If state is `error`, the underlying snapshot may be lost. Run `promus upgrade --reprovision` to spin a fresh container.',
175
+ ].join('\n'),
176
+ 'recoverable',
177
+ )
178
+ } finally {
179
+ await operator.close?.()
180
+ }
181
+ }
@@ -0,0 +1,119 @@
1
+ import { existsSync, statSync } from 'node:fs'
2
+ import {
3
+ NETWORK_CHAIN_ID,
4
+ NETWORK_RPC,
5
+ SANDBOX_PROVIDER_URL_GALILEO,
6
+ SandboxProviderClient,
7
+ agentPaths,
8
+ } from '@promus/core'
9
+ import { http, createPublicClient } from 'viem'
10
+ import { findAndLoadConfig } from '../config/load'
11
+ import { SandboxClient } from '../sandbox/client'
12
+ import { listAgentIds } from './_agents'
13
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
14
+
15
+ export async function runStatus(opts?: { cwd?: string }): Promise<void> {
16
+ const cwd = opts?.cwd ?? process.cwd()
17
+ const found = await findAndLoadConfig(cwd)
18
+ if (!found) {
19
+ console.log('No promus.config.ts found. Run `promus init` first.')
20
+ process.exit(1)
21
+ }
22
+ const { config, path } = found
23
+ console.log(`config ${path}`)
24
+ console.log(`network ${config.network} (chain ${NETWORK_CHAIN_ID[config.network]})`)
25
+ console.log(`rpc ${NETWORK_RPC[config.network]}`)
26
+ console.log(`plugins ${config.plugins.join(', ')}`)
27
+ console.log(`target ${config.deployTarget ?? 'local'}`)
28
+ if (config.identity.iNFT) {
29
+ const { contract, tokenId, network } = config.identity.iNFT
30
+ console.log(`iNFT #${tokenId} at ${contract} (${network})`)
31
+ } else {
32
+ console.log('iNFT (not minted)')
33
+ }
34
+ if (config.identity.operator) console.log(`operator ${config.identity.operator}`)
35
+ if (config.identity.agent) console.log(`agent EOA ${config.identity.agent}`)
36
+ console.log(`brain ${config.brain.provider ?? '(not picked)'}`)
37
+
38
+ // Phase 11 sandbox-mode status: fetch /healthz + provider record + show
39
+ // sandbox-side state instead of per-agent local dirs (those don't exist
40
+ // on the laptop in sandbox mode).
41
+ if (config.deployTarget === 'sandbox' && config.sandbox?.endpoint && config.sandbox.id) {
42
+ console.log('')
43
+ console.log(`sandbox ${config.sandbox.id}`)
44
+ console.log(`endpoint ${config.sandbox.endpoint}`)
45
+ console.log(`snapshot ${config.sandbox.snapshotName ?? '(default)'}`)
46
+
47
+ const operator = await loadOrPickOperatorSigner({
48
+ network: config.network,
49
+ hint: config.operator,
50
+ }).catch(() => null)
51
+ if (!operator) {
52
+ console.log('harness skipped (no operator wallet to sign /healthz auth)')
53
+ return
54
+ }
55
+ const operatorAccount = await operator.account()
56
+
57
+ const probe = new SandboxClient({
58
+ endpoint: config.sandbox.endpoint,
59
+ sandboxId: config.sandbox.id,
60
+ operator: operatorAccount,
61
+ })
62
+ const providerClient = new SandboxProviderClient({
63
+ endpoint: SANDBOX_PROVIDER_URL_GALILEO,
64
+ operator: operatorAccount,
65
+ })
66
+
67
+ // Both reads are independent network calls; run in parallel.
68
+ const [healthRes, sandboxRes] = await Promise.allSettled([
69
+ probe.health(),
70
+ providerClient.getSandbox(config.sandbox.id),
71
+ ])
72
+ if (healthRes.status === 'fulfilled') {
73
+ const h = healthRes.value
74
+ console.log(`harness state=${h.state} runtimeReady=${h.runtimeReady}`)
75
+ console.log(`uptime ${(h.uptimeMs / 1000 / 60).toFixed(1)} min`)
76
+ console.log(`pending ${h.pendingApprovals} approvals`)
77
+ console.log(`subs ${h.subscribers}`)
78
+ console.log(`events seq ${h.eventsLastSeq}`)
79
+ } else {
80
+ console.log(
81
+ `harness UNREACHABLE: ${healthRes.reason.message?.slice(0, 120) ?? healthRes.reason}`,
82
+ )
83
+ }
84
+ if (sandboxRes.status === 'fulfilled') {
85
+ const sb = sandboxRes.value
86
+ console.log(
87
+ `provider state=${sb.state}${sb.cpu ? ` cpu=${sb.cpu}` : ''}${sb.mem ? ` mem=${sb.mem}` : ''}${sb.disk ? ` disk=${sb.disk}` : ''}`,
88
+ )
89
+ } else {
90
+ console.log(
91
+ `provider UNREACHABLE: ${sandboxRes.reason.message?.slice(0, 120) ?? sandboxRes.reason}`,
92
+ )
93
+ }
94
+ await operator.close?.()
95
+ return
96
+ }
97
+
98
+ const ids = await listAgentIds()
99
+ if (ids.length === 0) {
100
+ console.log('\nNo agents found in ~/.promus/agents. Re-run `promus init`.')
101
+ return
102
+ }
103
+
104
+ const client = createPublicClient({
105
+ transport: http(NETWORK_RPC[config.network]),
106
+ })
107
+
108
+ for (const id of ids) {
109
+ console.log('')
110
+ console.log(`agent ${id}`)
111
+ console.log(`dir ${agentPaths.agent(id).dir}`)
112
+ const activityPath = agentPaths.agent(id).activityLog
113
+ if (existsSync(activityPath)) {
114
+ const sz = statSync(activityPath).size
115
+ console.log(`activity ${sz} bytes`)
116
+ }
117
+ void client
118
+ }
119
+ }
@@ -0,0 +1,147 @@
1
+ import { cancel, intro, outro, spinner } from '@clack/prompts'
2
+ import {
3
+ MemorySyncManager,
4
+ OPERATOR_BLOB_SCOPES,
5
+ agentPaths,
6
+ deriveBlobKey,
7
+ explorerTxUrl,
8
+ fetchAndDecryptKeystore,
9
+ iNFTAgentId,
10
+ } from '@promus/core'
11
+ import type { Address, Hex } from 'viem'
12
+ import { findAndLoadConfig } from '../config/load'
13
+ import { withSilencedConsole } from '../util/silence-console'
14
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
15
+
16
+ /**
17
+ * `promus sync` — explicit memory + activity-log flush to 0G Storage and
18
+ * anchor on chain via iNFT updateSlots. Useful pre-transfer or as a
19
+ * scheduled cron. Per-turn auto-sync covers the common path; this is the
20
+ * "force flush now" backstop.
21
+ *
22
+ * Phase 11: in sandbox mode (`deployTarget === 'sandbox'`), proxy to the
23
+ * remote harness's POST /sync — agent privkey lives in the container, so
24
+ * the laptop never needs to decrypt the keystore.
25
+ */
26
+ export async function runSync(): Promise<void> {
27
+ intro('promus sync')
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
+ // Sandbox-mode proxy: POST /sync, render result. Skip keystore decrypt.
41
+ if (config.deployTarget === 'sandbox' && config.sandbox?.endpoint && config.sandbox.id) {
42
+ const operator = await loadOrPickOperatorSigner({
43
+ network: config.network,
44
+ hint: config.operator,
45
+ })
46
+ if (!operator) {
47
+ cancel('No operator wallet available.')
48
+ return
49
+ }
50
+ const { SandboxClient } = await import('../sandbox/client')
51
+ const operatorAccount = await operator.account()
52
+ const client = new SandboxClient({
53
+ endpoint: config.sandbox.endpoint,
54
+ sandboxId: config.sandbox.id,
55
+ operator: operatorAccount,
56
+ })
57
+ const sFlush = spinner()
58
+ sFlush.start('Forcing remote harness flush via POST /sync')
59
+ try {
60
+ const r = await client.sync()
61
+ if (r.tx) {
62
+ sFlush.stop(`flushed ${r.slots.length} slot(s)`)
63
+ outro(['', ` slots: ${r.slots.join(', ')}`, ` tx: ${r.tx}`].join('\n'))
64
+ } else {
65
+ sFlush.stop('nothing to sync (everything up to date)')
66
+ }
67
+ } catch (e) {
68
+ sFlush.stop(`sync failed: ${(e as Error).message.slice(0, 200)}`)
69
+ }
70
+ await operator.close?.()
71
+ return
72
+ }
73
+
74
+ const network = config.network
75
+ const contractAddress = config.identity.iNFT.contract as Address
76
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
77
+ const agentAddress = config.identity.agent as Address
78
+ const finalAgentId = iNFTAgentId({ contractAddress, tokenId })
79
+ const paths = agentPaths.agent(finalAgentId)
80
+
81
+ const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
82
+ if (!operator) {
83
+ cancel('No operator wallet available; cannot decrypt keystore to sync.')
84
+ return
85
+ }
86
+
87
+ const sUnlock = spinner()
88
+ sUnlock.start('Fetching keystore + decrypting via operator')
89
+ let agentPrivkey: Hex
90
+ let profileKey: Buffer
91
+ try {
92
+ const decrypted = await withSilencedConsole(() =>
93
+ fetchAndDecryptKeystore({
94
+ network,
95
+ contractAddress,
96
+ tokenId,
97
+ signer: operator,
98
+ agentAddress,
99
+ cachePath: paths.keystore,
100
+ }),
101
+ )
102
+ agentPrivkey = decrypted.privkeyHex
103
+ // v0.23.0: derive PROFILE scope key alongside the keystore decrypt so the
104
+ // /sync flush can re-encrypt and anchor user/profile.md in the same batched
105
+ // updateSlots tx. One EIP-712 sign per scope; cheap because the operator
106
+ // is already on the live signing surface.
107
+ profileKey = await deriveBlobKey(operator, agentAddress, OPERATOR_BLOB_SCOPES.PROFILE)
108
+ sUnlock.stop(`unlocked (source: ${decrypted.source})`)
109
+ } catch (e) {
110
+ sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
111
+ await operator.close?.()
112
+ return
113
+ }
114
+ await operator.close?.()
115
+
116
+ const sFlush = spinner()
117
+ sFlush.start('Diffing memory + activity, uploading changed blobs, anchoring on chain')
118
+ try {
119
+ const res = await withSilencedConsole(async () => {
120
+ const sync = new MemorySyncManager({
121
+ network,
122
+ agentId: finalAgentId,
123
+ agentPrivkey,
124
+ agentAddress,
125
+ contractAddress,
126
+ tokenId,
127
+ profileKey,
128
+ })
129
+ await sync.init()
130
+ return await sync.flushAll()
131
+ })
132
+ if (res.txHash) {
133
+ sFlush.stop(`anchored ${res.changedSlots.length} slot(s)`)
134
+ outro(
135
+ [
136
+ '',
137
+ ` slots updated: ${res.changedSlots.join(', ')}`,
138
+ ` tx: ${explorerTxUrl(network, res.txHash)}`,
139
+ ].join('\n'),
140
+ )
141
+ } else {
142
+ sFlush.stop('nothing to sync (everything up to date)')
143
+ }
144
+ } catch (e) {
145
+ sFlush.stop(`sync failed: ${(e as Error).message.slice(0, 200)}`)
146
+ }
147
+ }
@@ -0,0 +1,65 @@
1
+ import { cancel, confirm, intro, isCancel, note, outro } from '@clack/prompts'
2
+ import { iNFTAgentId } from '@promus/core'
3
+ import { getAddress } from 'viem'
4
+ import { findAndLoadConfig } from '../config/load'
5
+ import { writeConfigTs } from '../config/render'
6
+ import {
7
+ removeTelegramSecrets,
8
+ telegramSecretsExist,
9
+ telegramSecretsPath,
10
+ } from '../util/telegram-secrets'
11
+
12
+ export interface TelegramRemoveOpts {
13
+ yes?: boolean
14
+ }
15
+
16
+ export async function runTelegramRemove(opts: TelegramRemoveOpts = {}): Promise<void> {
17
+ intro('promus telegram remove')
18
+
19
+ const loaded = await findAndLoadConfig()
20
+ if (!loaded) {
21
+ cancel('No promus.config.ts found. Run `promus init` first.')
22
+ return
23
+ }
24
+ const { config, path: configPath } = loaded
25
+ if (!config.identity.iNFT || !config.identity.agent) {
26
+ cancel('Config has no iNFT or agent. Run `promus init` first.')
27
+ return
28
+ }
29
+
30
+ const inftContract = getAddress(config.identity.iNFT.contract)
31
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
32
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
33
+
34
+ if (!telegramSecretsExist(agentId)) {
35
+ note('Nothing to remove.')
36
+ outro('not configured')
37
+ return
38
+ }
39
+
40
+ if (!opts.yes) {
41
+ const ok = (await confirm({
42
+ message: `Delete encrypted telegram-secrets for ${agentId}?`,
43
+ initialValue: false,
44
+ })) as boolean | symbol
45
+ if (isCancel(ok) || !ok) {
46
+ cancel('Aborted.')
47
+ return
48
+ }
49
+ }
50
+
51
+ await removeTelegramSecrets(agentId)
52
+
53
+ const plugins = (config.plugins ?? []).filter(p => p !== 'telegram')
54
+ if (plugins.length !== (config.plugins ?? []).length) {
55
+ const updated = { ...config, plugins }
56
+ await writeConfigTs(configPath, updated, { subname: config.subname })
57
+ }
58
+
59
+ note(
60
+ `Local blob deleted: ${telegramSecretsPath(agentId)}\nThe bot token at @BotFather is STILL VALID. To fully revoke, run /token in\n@BotFather and pick "Revoke" for this bot.`,
61
+ 'reminder',
62
+ )
63
+
64
+ outro('telegram removed')
65
+ }
@@ -0,0 +1,74 @@
1
+ import { cancel, intro, note, outro } from '@clack/prompts'
2
+ import { iNFTAgentId } from '@promus/core'
3
+ import { type Address, getAddress } from 'viem'
4
+ import { findAndLoadConfig } from '../config/load'
5
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
6
+ import { runTelegramStep } from './init/telegram-step'
7
+
8
+ /**
9
+ * `promus telegram setup` — standalone entry. Loads the operator wallet, then
10
+ * delegates to `runTelegramStep` (the same helper bundled into `promus init`'s
11
+ * Phase E). Owns its own intro/outro framing.
12
+ */
13
+ export async function runTelegramSetup(): Promise<void> {
14
+ intro('promus telegram setup')
15
+
16
+ const loaded = await findAndLoadConfig()
17
+ if (!loaded) {
18
+ cancel('No promus.config.ts found. Run `promus init` first.')
19
+ return
20
+ }
21
+ const { config, path: configPath } = loaded
22
+ if (!config.identity.iNFT || !config.identity.agent) {
23
+ cancel('Config has no iNFT or agent. Run `promus init` first.')
24
+ return
25
+ }
26
+
27
+ const agentAddress = getAddress(config.identity.agent) as Address
28
+ const inftContract = getAddress(config.identity.iNFT.contract) as Address
29
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
30
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
31
+
32
+ const operator = await loadOrPickOperatorSigner({
33
+ network: config.network,
34
+ hint: config.operator,
35
+ })
36
+ if (!operator) {
37
+ cancel('No operator wallet available; cannot encrypt secrets.')
38
+ return
39
+ }
40
+
41
+ let result: Awaited<ReturnType<typeof runTelegramStep>>
42
+ try {
43
+ result = await runTelegramStep({
44
+ signer: operator,
45
+ agentId,
46
+ agentAddress,
47
+ configPath,
48
+ config,
49
+ network: config.network,
50
+ })
51
+ } finally {
52
+ await operator.close?.()
53
+ }
54
+
55
+ if (!result.configured) {
56
+ cancel(result.cancelled ? 'Aborted.' : 'Setup failed.')
57
+ return
58
+ }
59
+
60
+ const isSandbox = config.deployTarget === 'sandbox' && config.sandbox?.endpoint
61
+ if (isSandbox) {
62
+ note(
63
+ 'Sandbox-mode agent: secrets are stored locally now, but the harness inside\nthe Daytona container needs them too. Run `promus upgrade` to ship them across\nthe handoff envelope.',
64
+ 'sandbox handoff pending',
65
+ )
66
+ } else {
67
+ note(
68
+ `Open https://t.me/${result.botUsername} in Telegram and send any message.\nThen run \`promus\` (or \`promus gateway start\`) to bring the agent online.`,
69
+ 'next step',
70
+ )
71
+ }
72
+
73
+ outro(`telegram setup complete (@${result.botUsername}, mode: ${result.modeUsed})`)
74
+ }
@@ -0,0 +1,89 @@
1
+ import { cancel, intro, log, outro, spinner } from '@clack/prompts'
2
+ import { iNFTAgentId } from '@promus/core'
3
+ import { type Address, getAddress } from 'viem'
4
+ import { findAndLoadConfig } from '../config/load'
5
+ import {
6
+ fetchBotInfo,
7
+ loadTelegramSecrets,
8
+ telegramSecretsExist,
9
+ telegramSecretsPath,
10
+ } from '../util/telegram-secrets'
11
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
12
+
13
+ export async function runTelegramStatus(): Promise<void> {
14
+ intro('promus telegram status')
15
+
16
+ const loaded = await findAndLoadConfig()
17
+ if (!loaded) {
18
+ cancel('No promus.config.ts found. Run `promus init` first.')
19
+ return
20
+ }
21
+ const { config } = loaded
22
+ if (!config.identity.iNFT || !config.identity.agent) {
23
+ cancel('Config has no iNFT or agent. Run `promus init` first.')
24
+ return
25
+ }
26
+
27
+ const agentAddress = getAddress(config.identity.agent) as Address
28
+ const inftContract = getAddress(config.identity.iNFT.contract) as Address
29
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
30
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
31
+ const path = telegramSecretsPath(agentId)
32
+
33
+ if (!telegramSecretsExist(agentId)) {
34
+ log.warn(`No telegram secrets stored for ${agentId}.`)
35
+ log.info(`Expected at: ${path}\nRun \`promus telegram setup\` to configure.`)
36
+ outro('not configured')
37
+ return
38
+ }
39
+
40
+ const operator = await loadOrPickOperatorSigner({
41
+ network: config.network,
42
+ hint: config.operator,
43
+ })
44
+ if (!operator) {
45
+ cancel('No operator wallet available; cannot decrypt secrets.')
46
+ return
47
+ }
48
+
49
+ const sLoad = spinner()
50
+ sLoad.start('Decrypting telegram secrets via operator wallet')
51
+ let secrets: Awaited<ReturnType<typeof loadTelegramSecrets>>
52
+ try {
53
+ secrets = await loadTelegramSecrets({ signer: operator, agentAddress, agentId })
54
+ sLoad.stop('decrypted')
55
+ } catch (e) {
56
+ sLoad.stop(`decrypt failed: ${(e as Error).message.slice(0, 200)}`)
57
+ await operator.close?.()
58
+ return
59
+ } finally {
60
+ await operator.close?.()
61
+ }
62
+ if (!secrets) {
63
+ cancel('Empty telegram-secrets blob.')
64
+ return
65
+ }
66
+
67
+ const sPing = spinner()
68
+ sPing.start('Pinging Telegram getMe')
69
+ try {
70
+ const info = await fetchBotInfo(secrets.botToken)
71
+ sPing.stop(`bot ok: @${info.username} (id ${info.id})`)
72
+ } catch (e) {
73
+ sPing.stop(`getMe failed: ${(e as Error).message.slice(0, 200)}`)
74
+ log.warn('Token may have been revoked at @BotFather. Re-run `promus telegram setup`.')
75
+ return
76
+ }
77
+
78
+ log.info(
79
+ [
80
+ `path ${path}`,
81
+ `bot username @${secrets.botUsername ?? '(unknown)'}`,
82
+ `bot id ${secrets.botId ?? '(unknown)'}`,
83
+ `allowed user ids ${secrets.allowedUserIds.length === 0 ? '(open access)' : secrets.allowedUserIds.join(', ')}`,
84
+ `plugin enabled ${(config.plugins ?? []).includes('telegram') ? 'yes' : 'no — add `telegram` to plugins'}`,
85
+ ].join('\n'),
86
+ )
87
+
88
+ outro(`telegram configured for ${agentId}`)
89
+ }