@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,229 @@
1
+ /**
2
+ * Hermes-aligned Telegram setup wizard step. Shared by `promus telegram setup`
3
+ * (standalone) and the optional Phase E in `promus init` (right after Phase D
4
+ * summary, reusing the in-flight operator wallet so we don't prompt Touch ID
5
+ * twice).
6
+ *
7
+ * Flow (matches `~/.hermes/hermes-agent/hermes_cli/{setup.py:1720, gateway.py:1939}`):
8
+ * 1. Bot token (password input + `getMe` probe).
9
+ * 2. Auth-mode select: pair (default) or allowlist.
10
+ * 3. Allowlist branch: text prompt for IDs + @userinfobot hint.
11
+ * 4. Encrypt + save secrets to `~/.promus/agents/<id>/telegram-secrets.encrypted`.
12
+ * 5. Merge `'telegram'` into config.plugins; rewrite `~/.promus/config.ts`.
13
+ *
14
+ * Caller frames its own intro/outro. This helper is content-only.
15
+ */
16
+ import { cancel, confirm, isCancel, note, password, select, spinner, text } from '@clack/prompts'
17
+ import {
18
+ type PromusConfig,
19
+ type PromusNetwork,
20
+ OPERATOR_BLOB_SCOPES,
21
+ type OperatorSigner,
22
+ agentPaths,
23
+ deriveBlobKey,
24
+ } from '@promus/core'
25
+ import { type Address, type Hex, bytesToHex } from 'viem'
26
+ import { writeConfigTs } from '../../config/render'
27
+ import {
28
+ fetchBotInfo,
29
+ looksLikeBotToken,
30
+ parseAllowedUserIds,
31
+ saveTelegramSecrets,
32
+ telegramSecretsExist,
33
+ } from '../../util/telegram-secrets'
34
+ import { resolveHandoffPlugins } from './sandbox-provision'
35
+
36
+ export type TelegramAuthMode = 'pair' | 'allowlist'
37
+
38
+ export interface TelegramStepOpts {
39
+ /** Already-unlocked operator wallet. Caller is responsible for closing it. */
40
+ signer: OperatorSigner
41
+ agentId: string
42
+ agentAddress: Address
43
+ configPath: string
44
+ config: PromusConfig
45
+ network: PromusNetwork
46
+ /**
47
+ * If true, the helper is allowed to ask whether to overwrite an existing
48
+ * blob via `confirm`. Default true. Set false for fully non-interactive
49
+ * test paths.
50
+ */
51
+ allowOverwrite?: boolean
52
+ /**
53
+ * v0.24.4: when true, do NOT write the config file from inside this step —
54
+ * caller (init.ts) builds the final cfg with `'telegram'` in plugins and
55
+ * writes once. Avoids the partial-write hazard where Phase E runs before
56
+ * the init's main config build and the intermediate write has incomplete
57
+ * identity/sandbox fields. Standalone `promus telegram setup` keeps the
58
+ * default false so it still rewrites the config.
59
+ */
60
+ skipConfigWrite?: boolean
61
+ }
62
+
63
+ export interface TelegramStepResult {
64
+ configured: boolean
65
+ /** Set when `configured: true`. */
66
+ botUsername?: string
67
+ modeUsed?: TelegramAuthMode
68
+ allowedUserIds?: number[]
69
+ /** Set when configured aborted by user (cancel / no-overwrite). */
70
+ cancelled?: boolean
71
+ /**
72
+ * v0.24.3: derived TELEGRAM scope key as 0x-prefixed hex. Caller stashes
73
+ * this in `.operator-session` so the daemon auto-spawns without re-prompting
74
+ * Touch ID. Hex (not Buffer) to match `OperatorSessionKeys`' on-disk shape.
75
+ */
76
+ telegramScopeKeyHex?: Hex
77
+ }
78
+
79
+ const PAIR_OPTION_LABEL =
80
+ 'Pair (recommended) — unknown DM users get an 8-char code; you approve via CLI'
81
+ const ALLOW_OPTION_LABEL =
82
+ 'Allowlist — only listed numeric Telegram IDs can DM the bot (find yours via @userinfobot)'
83
+
84
+ export async function runTelegramStep(opts: TelegramStepOpts): Promise<TelegramStepResult> {
85
+ if (telegramSecretsExist(opts.agentId)) {
86
+ if (opts.allowOverwrite === false) {
87
+ return { configured: false, cancelled: true }
88
+ }
89
+ const overwrite = await confirm({
90
+ message:
91
+ 'Encrypted telegram-secrets blob already exists for this agent. Overwrite with new settings?',
92
+ initialValue: false,
93
+ })
94
+ if (isCancel(overwrite) || overwrite !== true) {
95
+ return { configured: false, cancelled: true }
96
+ }
97
+ }
98
+
99
+ const tokenRaw = (await password({
100
+ message: 'Bot token from @BotFather',
101
+ validate: v => {
102
+ if (!v) return 'Required.'
103
+ if (!looksLikeBotToken(v))
104
+ return 'Looks malformed. Expected `<id>:<secret>` from @BotFather, e.g. 1234567890:AABBCC...'
105
+ return undefined
106
+ },
107
+ })) as string | symbol
108
+ if (isCancel(tokenRaw)) {
109
+ return { configured: false, cancelled: true }
110
+ }
111
+ const botToken = (tokenRaw as string).trim()
112
+
113
+ const sValidate = spinner()
114
+ sValidate.start('Validating token via api.telegram.org/getMe')
115
+ let botInfo: Awaited<ReturnType<typeof fetchBotInfo>>
116
+ try {
117
+ botInfo = await fetchBotInfo(botToken)
118
+ sValidate.stop(`bot ok: @${botInfo.username} (id ${botInfo.id})`)
119
+ } catch (e) {
120
+ sValidate.stop(`token rejected: ${(e as Error).message.slice(0, 200)}`)
121
+ cancel('Bad token. Re-issue via /token in @BotFather and re-run setup.')
122
+ return { configured: false, cancelled: true }
123
+ }
124
+
125
+ const modeChoice = await select({
126
+ message: 'How should unauthorized DMs to the bot be handled?',
127
+ options: [
128
+ { value: 'pair' as TelegramAuthMode, label: PAIR_OPTION_LABEL },
129
+ { value: 'allowlist' as TelegramAuthMode, label: ALLOW_OPTION_LABEL },
130
+ ],
131
+ initialValue: 'pair' as TelegramAuthMode,
132
+ })
133
+ if (isCancel(modeChoice)) {
134
+ return { configured: false, cancelled: true }
135
+ }
136
+ const mode = modeChoice as TelegramAuthMode
137
+
138
+ let allowedUserIds: number[] = []
139
+ if (mode === 'allowlist') {
140
+ const allowedRaw = (await text({
141
+ message: 'Allowed Telegram user IDs (comma-separated)',
142
+ placeholder: '123456789, 987654321',
143
+ defaultValue: '',
144
+ validate: v => {
145
+ if (!v) return 'At least one numeric id required (or pick Pair mode instead).'
146
+ const parsed = parseAllowedUserIds(v)
147
+ if (!parsed.ok) return parsed.reason
148
+ if (parsed.ids.length === 0) return 'At least one numeric id required.'
149
+ return undefined
150
+ },
151
+ })) as string | symbol
152
+ if (isCancel(allowedRaw)) {
153
+ return { configured: false, cancelled: true }
154
+ }
155
+ const parsed = parseAllowedUserIds(typeof allowedRaw === 'string' ? allowedRaw : '')
156
+ if (!parsed.ok || parsed.ids.length === 0) {
157
+ cancel(`bad allowed list: ${parsed.ok ? 'empty' : parsed.reason}`)
158
+ return { configured: false, cancelled: true }
159
+ }
160
+ allowedUserIds = parsed.ids
161
+ note(
162
+ `Approved on day one: ${allowedUserIds.join(', ')}\nThese users can DM @${botInfo.username} immediately. Anyone else still falls into pairing.`,
163
+ 'allowlist',
164
+ )
165
+ } else {
166
+ note(
167
+ `Default-deny is on: any unknown user who DMs @${botInfo.username}\nwill receive a one-time pairing code. Approve them out-of-band:\n promus pairing approve telegram <CODE>\nTo skip pairing for yourself, re-run setup, pick Allowlist, and paste your numeric id\n(get it from @userinfobot).`,
168
+ 'pairing mode',
169
+ )
170
+ }
171
+
172
+ // v0.24.3: derive TELEGRAM key explicitly so we can both pass it as
173
+ // `precomputedKey` (skip the redundant sign inside encryptOperatorBlob)
174
+ // AND return it to init.ts to stash in `.operator-session`.
175
+ const sDerive = spinner()
176
+ sDerive.start('Deriving TELEGRAM scope key')
177
+ let telegramScopeKey: Buffer
178
+ try {
179
+ telegramScopeKey = await deriveBlobKey(
180
+ opts.signer,
181
+ opts.agentAddress,
182
+ OPERATOR_BLOB_SCOPES.TELEGRAM,
183
+ )
184
+ sDerive.stop('TELEGRAM scope key derived')
185
+ } catch (e) {
186
+ sDerive.stop(`TELEGRAM scope derive failed: ${(e as Error).message.slice(0, 200)}`)
187
+ return { configured: false, cancelled: true }
188
+ }
189
+
190
+ const sSave = spinner()
191
+ sSave.start('Encrypting + saving telegram secrets locally')
192
+ try {
193
+ await saveTelegramSecrets({
194
+ signer: opts.signer,
195
+ agentAddress: opts.agentAddress,
196
+ agentId: opts.agentId,
197
+ plaintext: {
198
+ botToken,
199
+ botUsername: botInfo.username,
200
+ botId: botInfo.id,
201
+ allowedUserIds,
202
+ },
203
+ precomputedKey: telegramScopeKey,
204
+ })
205
+ sSave.stop(`saved → ${agentPaths.agent(opts.agentId).dir}/telegram-secrets.encrypted`)
206
+ } catch (e) {
207
+ sSave.stop(`save failed: ${(e as Error).message.slice(0, 200)}`)
208
+ return { configured: false, cancelled: true }
209
+ }
210
+
211
+ // v0.24.4: when caller asks (init.ts), skip the config rewrite — caller will
212
+ // build the final cfg with `'telegram'` in plugins and write once. Avoids the
213
+ // partial-write hazard where Phase E runs before init's main config build.
214
+ if (!opts.skipConfigWrite) {
215
+ const plugins = resolveHandoffPlugins(opts.config.plugins, true)
216
+ if (plugins.length !== (opts.config.plugins ?? []).length) {
217
+ const updated = { ...opts.config, plugins }
218
+ await writeConfigTs(opts.configPath, updated, { subname: opts.config.subname })
219
+ }
220
+ }
221
+
222
+ return {
223
+ configured: true,
224
+ botUsername: botInfo.username,
225
+ modeUsed: mode,
226
+ allowedUserIds,
227
+ telegramScopeKeyHex: bytesToHex(telegramScopeKey),
228
+ }
229
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import type { PromusNetwork } from '@promus/core'
5
+
6
+ /**
7
+ * Pattern B resumable-init state file (Apr 24 2026 session design).
8
+ *
9
+ * Lives at `<agentDir>/.promus-init-state.json` and tracks which steps in
10
+ * Phase C of the wizard completed. Written incrementally. If init crashes
11
+ * or the user aborts mid-flow, a subsequent `promus init` (or `--resume`)
12
+ * can pick up from the first incomplete step instead of re-minting.
13
+ */
14
+ export interface WizardState {
15
+ version: 1
16
+ agentAddress: `0x${string}`
17
+ network: PromusNetwork
18
+ steps: {
19
+ keystoreSaved: boolean
20
+ mintedTokenId: string | null
21
+ mintedContract: string | null
22
+ mintTx: string | null
23
+ agentFundedTx: string | null
24
+ keystorePersistedTx: string | null
25
+ keystoreRootHash: string | null
26
+ ledgerOpenedTx: boolean // broker.addLedger returns void
27
+ subnameClaimedTx: string | null
28
+ textRecordsSetTx: string | null
29
+ /** Phase 11: 0G Sandbox lifecycle. Set during sandbox-deploy branch. */
30
+ sandboxId: string | null
31
+ sandboxEndpoint: string | null
32
+ }
33
+ lastError: string | null
34
+ updatedAt: string
35
+ }
36
+
37
+ export const WIZARD_STATE_FILENAME = '.promus-init-state.json'
38
+
39
+ export function wizardStatePath(agentDir: string): string {
40
+ return join(agentDir, WIZARD_STATE_FILENAME)
41
+ }
42
+
43
+ export function initialWizardState(
44
+ agentAddress: `0x${string}`,
45
+ network: PromusNetwork,
46
+ ): WizardState {
47
+ return {
48
+ version: 1,
49
+ agentAddress,
50
+ network,
51
+ steps: {
52
+ keystoreSaved: false,
53
+ mintedTokenId: null,
54
+ mintedContract: null,
55
+ mintTx: null,
56
+ agentFundedTx: null,
57
+ keystorePersistedTx: null,
58
+ keystoreRootHash: null,
59
+ ledgerOpenedTx: false,
60
+ subnameClaimedTx: null,
61
+ textRecordsSetTx: null,
62
+ sandboxId: null,
63
+ sandboxEndpoint: null,
64
+ },
65
+ lastError: null,
66
+ updatedAt: new Date().toISOString(),
67
+ }
68
+ }
69
+
70
+ export async function readWizardState(agentDir: string): Promise<WizardState | null> {
71
+ const path = wizardStatePath(agentDir)
72
+ if (!existsSync(path)) return null
73
+ try {
74
+ const raw = await readFile(path, 'utf8')
75
+ return JSON.parse(raw) as WizardState
76
+ } catch {
77
+ return null
78
+ }
79
+ }
80
+
81
+ export async function writeWizardState(agentDir: string, state: WizardState): Promise<void> {
82
+ state.updatedAt = new Date().toISOString()
83
+ await writeFile(wizardStatePath(agentDir), JSON.stringify(state, null, 2), 'utf8')
84
+ }
85
+
86
+ export async function updateWizardState(
87
+ agentDir: string,
88
+ patch: (draft: WizardState) => void,
89
+ ): Promise<WizardState> {
90
+ const current = (await readWizardState(agentDir)) ?? null
91
+ if (!current) throw new Error(`updateWizardState: no state at ${agentDir}`)
92
+ patch(current)
93
+ await writeWizardState(agentDir, current)
94
+ return current
95
+ }