@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,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
|
+
}
|