@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
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# promus
|
|
2
|
+
|
|
3
|
+
CLI binary for **promus**: the first fully on-chain sovereign agent harness on 0G.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -g promus
|
|
9
|
+
promus init
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires [bun](https://bun.sh) ≥ 1.1.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
`promus init` boots the wizard (mints an iNFT, opens a 0G Compute ledger, generates the agent EOA). After that: `promus` for chat, `promus status`, `promus logs`, `promus topup`, `promus ledger`, `promus drain`, `promus sync`, `promus inspect`, `promus deploy`, `promus upgrade`, `promus help` for the full list.
|
|
17
|
+
|
|
18
|
+
See the [root README](https://github.com/JemIIahh/promus#readme) for architecture, concepts, and the full command reference.
|
package/bin/promus
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Register the @opentui/solid JSX transform plugin BEFORE any .tsx file
|
|
3
|
+
// loads. bunfig.toml's `preload` only fires when bun discovers bunfig.toml
|
|
4
|
+
// in the cwd lookup chain — running `promus` from outside the repo (e.g.
|
|
5
|
+
// `cd ~ && promus`, or as an installed npm bin) skips it entirely, leaving
|
|
6
|
+
// JSX compiled as React.createElement and the chat TUI rendering blank.
|
|
7
|
+
// Importing the preload module here registers the plugin regardless of
|
|
8
|
+
// cwd. The plugin is idempotent so this is a no-op if bunfig.toml ALSO
|
|
9
|
+
// loaded it.
|
|
10
|
+
import '@opentui/solid/preload'
|
|
11
|
+
|
|
12
|
+
// Polyfill Error.captureStackTrace for bun + follow-redirects compatibility.
|
|
13
|
+
// follow-redirects@1.16 (transitive via @0glabs/0g-serving-broker → axios) calls
|
|
14
|
+
// Error.captureStackTrace(this, ctor) inside a CustomError constructor whose
|
|
15
|
+
// prototype is set to Error AFTER createErrorType returns. Bun's strict-mode
|
|
16
|
+
// validator rejects the call when bun's workspace resolver loads the module
|
|
17
|
+
// (the failure is cwd-dependent: only triggers when bun's cwd is inside this
|
|
18
|
+
// monorepo). Wrapping the original keeps Node parity without monkey-patching
|
|
19
|
+
// follow-redirects itself.
|
|
20
|
+
const _origCapture = Error.captureStackTrace
|
|
21
|
+
if (typeof _origCapture === 'function') {
|
|
22
|
+
Error.captureStackTrace = (target, ctor) => {
|
|
23
|
+
if (!target || typeof target !== 'object') return
|
|
24
|
+
try {
|
|
25
|
+
_origCapture(target, ctor)
|
|
26
|
+
} catch {
|
|
27
|
+
try {
|
|
28
|
+
target.stack = new Error().stack
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
import '../src/index'
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@promus/cli",
|
|
3
|
+
"version": "0.24.17",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Promus CLI: sovereign agent gateway on Arbitrum. Runs `promus init` to mint an iNFT and bring up your agent",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/JemIIahh/promus",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/JemIIahh/promus.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/JemIIahh/promus/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["arbitrum", "promus", "agent", "ai", "cli", "tui", "sovereign", "gateway", "inft"],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.1"
|
|
22
|
+
},
|
|
23
|
+
"files": ["src", "bin", "README.md"],
|
|
24
|
+
"bin": {
|
|
25
|
+
"promus": "./bin/promus"
|
|
26
|
+
},
|
|
27
|
+
"main": "./src/index.ts",
|
|
28
|
+
"types": "./src/index.ts",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -b",
|
|
31
|
+
"test": "bun test"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/prompts": "^0.8.2",
|
|
35
|
+
"@opentui/core": "^0.1.97",
|
|
36
|
+
"@opentui/solid": "^0.1.97",
|
|
37
|
+
"@promus/core": "0.24.17",
|
|
38
|
+
"@promus/gateway": "0.24.17",
|
|
39
|
+
"@promus/plugin-comms": "0.24.17",
|
|
40
|
+
"@promus/plugin-onchain": "0.24.17",
|
|
41
|
+
"@promus/plugin-system": "0.24.17",
|
|
42
|
+
"@promus/plugin-telegram": "0.24.17",
|
|
43
|
+
"picocolors": "^1.1.1",
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"solid-js": "^1.9.12",
|
|
46
|
+
"viem": "^2.21.55"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/qrcode-terminal": "^0.12.2"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readdir } from 'node:fs/promises'
|
|
3
|
+
import { agentPaths } from '@promus/core'
|
|
4
|
+
|
|
5
|
+
export async function listAgentIds(): Promise<string[]> {
|
|
6
|
+
if (!existsSync(agentPaths.agentsDir)) return []
|
|
7
|
+
const entries = await readdir(agentPaths.agentsDir, { withFileTypes: true })
|
|
8
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function pickDefaultAgent(): Promise<string | null> {
|
|
12
|
+
const ids = await listAgentIds()
|
|
13
|
+
return ids[0] ?? null
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type PromusNetwork, NETWORK_CHAIN_ID, networkFromChainId } from '@promus/core'
|
|
2
|
+
import type { Address } from 'viem'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a CAIP-style or 0G-flavor iNFT ref string into its parts. Used by
|
|
6
|
+
* `promus restore` and `promus inspect` to take a single positional argument
|
|
7
|
+
* pointing at any iNFT on either 0G network.
|
|
8
|
+
*
|
|
9
|
+
* Accepted forms:
|
|
10
|
+
* `eip155:<chainId>:0xCONTRACT:<tokenId>`
|
|
11
|
+
* `0g-mainnet:0xCONTRACT:<tokenId>`
|
|
12
|
+
* `0g-testnet:0xCONTRACT:<tokenId>`
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedINFTRef {
|
|
15
|
+
network: PromusNetwork
|
|
16
|
+
contract: Address
|
|
17
|
+
tokenId: bigint
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseINFTRef(ref: string): ParsedINFTRef {
|
|
21
|
+
const parts = ref.split(':')
|
|
22
|
+
if (parts.length === 4 && parts[0] === 'eip155') {
|
|
23
|
+
const chainId = Number(parts[1])
|
|
24
|
+
const contract = parts[2] as Address
|
|
25
|
+
const tokenId = BigInt(parts[3]!)
|
|
26
|
+
const network = networkFromChainId(chainId)
|
|
27
|
+
if (!network) {
|
|
28
|
+
const known = Object.values(NETWORK_CHAIN_ID).join(' or ')
|
|
29
|
+
throw new Error(`Unknown chain id ${chainId} (expected ${known})`)
|
|
30
|
+
}
|
|
31
|
+
return { network, contract, tokenId }
|
|
32
|
+
}
|
|
33
|
+
if (parts.length === 3 && (parts[0] === '0g-mainnet' || parts[0] === '0g-testnet')) {
|
|
34
|
+
return {
|
|
35
|
+
network: parts[0] as PromusNetwork,
|
|
36
|
+
contract: parts[1] as Address,
|
|
37
|
+
tokenId: BigInt(parts[2]!),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unrecognized iNFT ref '${ref}'. Expected 'eip155:<chain>:0xCONTRACT:<tokenId>' or '0g-mainnet:0xCONTRACT:<tokenId>'.`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spinner } from '@clack/prompts'
|
|
2
|
+
import {
|
|
3
|
+
type PromusConfig,
|
|
4
|
+
type PromusNetwork,
|
|
5
|
+
agentPaths,
|
|
6
|
+
fetchAndDecryptKeystore,
|
|
7
|
+
iNFTAgentId,
|
|
8
|
+
} from '@promus/core'
|
|
9
|
+
import type { Address, Hex } from 'viem'
|
|
10
|
+
import { withSilencedConsole } from '../util/silence-console'
|
|
11
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
12
|
+
|
|
13
|
+
export interface UnlockedAgent {
|
|
14
|
+
agentPrivkey: Hex
|
|
15
|
+
agentAddress: Address
|
|
16
|
+
network: PromusNetwork
|
|
17
|
+
close: () => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shared operator-unlock dance for any command that needs the agent privkey:
|
|
22
|
+
* 1. pick the operator signer (keystore / WC / keychain) per config hint
|
|
23
|
+
* 2. fetch the encrypted keystore from 0G Storage
|
|
24
|
+
* 3. decrypt via operator signature
|
|
25
|
+
*
|
|
26
|
+
* Returns null if the operator picker is cancelled or the keystore can't be
|
|
27
|
+
* decrypted; caller should bail out early on null.
|
|
28
|
+
*
|
|
29
|
+
* The unlock spinner is rendered with the passed `spinnerLabel` so each caller
|
|
30
|
+
* keeps its own copy.
|
|
31
|
+
*
|
|
32
|
+
* Caller MUST call `close()` once done with the privkey, even on success, to
|
|
33
|
+
* release WC sessions / keystore tmpfiles.
|
|
34
|
+
*/
|
|
35
|
+
export async function unlockAgentSigner(
|
|
36
|
+
config: PromusConfig,
|
|
37
|
+
spinnerLabel = 'Fetching encrypted keystore + decrypting via operator wallet',
|
|
38
|
+
): Promise<UnlockedAgent | null> {
|
|
39
|
+
if (!config.identity.iNFT || !config.identity.agent) return null
|
|
40
|
+
const network = config.network
|
|
41
|
+
const agentAddress = config.identity.agent as Address
|
|
42
|
+
const inftContract = config.identity.iNFT.contract as Address
|
|
43
|
+
const inftTokenId = BigInt(config.identity.iNFT.tokenId)
|
|
44
|
+
const finalAgentId = iNFTAgentId({ contractAddress: inftContract, tokenId: inftTokenId })
|
|
45
|
+
const paths = agentPaths.agent(finalAgentId)
|
|
46
|
+
|
|
47
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
48
|
+
if (!operator) return null
|
|
49
|
+
|
|
50
|
+
const close = async () => {
|
|
51
|
+
await operator.close?.()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const s = spinner()
|
|
55
|
+
s.start(spinnerLabel)
|
|
56
|
+
try {
|
|
57
|
+
const decrypted = await withSilencedConsole(() =>
|
|
58
|
+
fetchAndDecryptKeystore({
|
|
59
|
+
network,
|
|
60
|
+
contractAddress: inftContract,
|
|
61
|
+
tokenId: inftTokenId,
|
|
62
|
+
signer: operator,
|
|
63
|
+
agentAddress,
|
|
64
|
+
cachePath: paths.keystore,
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
s.stop(`unlocked (keystore source: ${decrypted.source})`)
|
|
68
|
+
return { agentPrivkey: decrypted.privkeyHex, agentAddress, network, close }
|
|
69
|
+
} catch (e) {
|
|
70
|
+
s.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
|
|
71
|
+
await close()
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { agentPaths, iNFTAgentId, placeholderAgentId } from '@promus/core'
|
|
3
|
+
import { type Address, getAddress } from 'viem'
|
|
4
|
+
import { findAndLoadConfig } from '../config/load'
|
|
5
|
+
import { SandboxClient } from '../sandbox/client'
|
|
6
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
7
|
+
|
|
8
|
+
export async function runAdminAutotopupTick(): Promise<void> {
|
|
9
|
+
const found = await findAndLoadConfig()
|
|
10
|
+
if (!found) {
|
|
11
|
+
console.error('No promus.config.ts found. Run `promus init` first.')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
const { config } = found
|
|
15
|
+
|
|
16
|
+
if (config.deployTarget === 'sandbox' && config.sandbox?.endpoint && config.sandbox.id) {
|
|
17
|
+
const signer = await loadOrPickOperatorSigner({
|
|
18
|
+
network: config.network,
|
|
19
|
+
hint: config.operator,
|
|
20
|
+
})
|
|
21
|
+
if (!signer) {
|
|
22
|
+
console.error('failed to load operator signer (cancelled or no key)')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
const operatorAccount = await signer.account()
|
|
26
|
+
const client = new SandboxClient({
|
|
27
|
+
endpoint: config.sandbox.endpoint,
|
|
28
|
+
sandboxId: config.sandbox.id,
|
|
29
|
+
operator: operatorAccount,
|
|
30
|
+
})
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.triggerAutoTopupTick()
|
|
33
|
+
console.log(JSON.stringify(result, null, 2))
|
|
34
|
+
if (!result.ok) process.exitCode = 1
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`autotopup-tick failed: ${(e as Error).message.slice(0, 240)}`)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!config.identity.agent) {
|
|
43
|
+
console.error('No agent address in config. Run `promus init` first.')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
// Slug derivation must match gateway-stop.ts (iNFT-based when minted, else
|
|
47
|
+
// address-placeholder); the daemon writes its sock under the same dir.
|
|
48
|
+
const slug = config.identity.iNFT
|
|
49
|
+
? iNFTAgentId({
|
|
50
|
+
contractAddress: getAddress(config.identity.iNFT.contract as Address),
|
|
51
|
+
tokenId: BigInt(config.identity.iNFT.tokenId),
|
|
52
|
+
})
|
|
53
|
+
: placeholderAgentId(config.identity.agent)
|
|
54
|
+
const sockPath = `${agentPaths.agent(slug).dir}/gateway.sock`
|
|
55
|
+
if (!existsSync(sockPath)) {
|
|
56
|
+
console.error(
|
|
57
|
+
`Gateway socket not found at ${sockPath}. Start the gateway with \`promus gateway start\` or run \`promus\` first.`,
|
|
58
|
+
)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
const r = await fetch('http://localhost/admin/autotopup/tick', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
unix: sockPath,
|
|
64
|
+
} as RequestInit & { unix: string })
|
|
65
|
+
if (!r.ok) {
|
|
66
|
+
const detail = await r.text().catch(() => '')
|
|
67
|
+
console.error(`autotopup-tick failed (${r.status}): ${detail}`)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
const body = (await r.json()) as { ok: boolean; reason?: string }
|
|
71
|
+
console.log(JSON.stringify(body, null, 2))
|
|
72
|
+
if (!body.ok) process.exitCode = 1
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { parseAdminArgs, runAdmin } from './admin'
|
|
3
|
+
|
|
4
|
+
describe('parseAdminArgs', () => {
|
|
5
|
+
test('errors with usage when no subcommand', () => {
|
|
6
|
+
const r = parseAdminArgs([])
|
|
7
|
+
expect('error' in r).toBe(true)
|
|
8
|
+
if (!('error' in r)) throw new Error('unreachable')
|
|
9
|
+
expect(r.error).toMatch(/usage/i)
|
|
10
|
+
expect(r.error).toMatch(/autotopup-tick/)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('errors on unknown subcommand', () => {
|
|
14
|
+
const r = parseAdminArgs(['nope'])
|
|
15
|
+
expect('error' in r).toBe(true)
|
|
16
|
+
if (!('error' in r)) throw new Error('unreachable')
|
|
17
|
+
expect(r.error).toMatch(/unknown subcommand/)
|
|
18
|
+
expect(r.error).toMatch(/'nope'/)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('parses autotopup-tick', () => {
|
|
22
|
+
const r = parseAdminArgs(['autotopup-tick'])
|
|
23
|
+
expect('error' in r).toBe(false)
|
|
24
|
+
if ('error' in r) throw new Error(r.error)
|
|
25
|
+
expect(r.sub).toBe('autotopup-tick')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('runAdmin dispatch', () => {
|
|
30
|
+
test('runAdmin is callable + accepts AdminArgs shape', () => {
|
|
31
|
+
expect(typeof runAdmin).toBe('function')
|
|
32
|
+
expect(runAdmin.length).toBe(1)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `promus admin <sub>` — operator-only ops dispatch. Mirrors `pairing.ts` shape.
|
|
2
|
+
|
|
3
|
+
export interface AdminArgs {
|
|
4
|
+
sub: 'autotopup-tick'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const VALID_SUBS = ['autotopup-tick'] as const
|
|
8
|
+
|
|
9
|
+
export type AdminParseResult = AdminArgs | { error: string }
|
|
10
|
+
|
|
11
|
+
export function parseAdminArgs(argv: string[]): AdminParseResult {
|
|
12
|
+
const sub = argv[0]
|
|
13
|
+
if (!sub) {
|
|
14
|
+
return {
|
|
15
|
+
error: `usage: promus admin <${VALID_SUBS.join(' | ')}>`,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (!(VALID_SUBS as readonly string[]).includes(sub)) {
|
|
19
|
+
return { error: `unknown subcommand '${sub}' (expected: ${VALID_SUBS.join(' | ')})` }
|
|
20
|
+
}
|
|
21
|
+
return { sub: sub as AdminArgs['sub'] }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runAdmin(args: AdminArgs): Promise<void> {
|
|
25
|
+
switch (args.sub) {
|
|
26
|
+
case 'autotopup-tick': {
|
|
27
|
+
const { runAdminAutotopupTick } = await import('./admin-autotopup-tick')
|
|
28
|
+
await runAdminAutotopupTick()
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { runBalance } from './balance'
|
|
3
|
+
|
|
4
|
+
describe('promus balance command surface', () => {
|
|
5
|
+
test('runBalance is exported and callable', () => {
|
|
6
|
+
expect(typeof runBalance).toBe('function')
|
|
7
|
+
// function signature: (opts: { agent?, cwd? }) → Promise<void>
|
|
8
|
+
expect(runBalance.length).toBe(1)
|
|
9
|
+
})
|
|
10
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PromusNetwork,
|
|
3
|
+
NETWORK_RPC,
|
|
4
|
+
format0G,
|
|
5
|
+
getLedgerDetailReadOnly,
|
|
6
|
+
getSandboxBillingReserve,
|
|
7
|
+
} from '@promus/core'
|
|
8
|
+
import { http, type Address, createPublicClient } from 'viem'
|
|
9
|
+
import { findAndLoadConfig } from '../config/load'
|
|
10
|
+
|
|
11
|
+
export interface BalanceOpts {
|
|
12
|
+
agent?: string
|
|
13
|
+
cwd?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Operator-facing aggregator for the agent's full economic position. Mirrors
|
|
18
|
+
* `account.balance` brain tool but renders for terminals.
|
|
19
|
+
*
|
|
20
|
+
* Why: pre-v0.21.9, getting a full picture took `cast balance` × 2 networks +
|
|
21
|
+
* `promus ledger balance` (needs unlock) + a separate cast for sandbox billing.
|
|
22
|
+
* Operators kept under-counting by ~10x because the locked-in-providers split
|
|
23
|
+
* wasn't surfaced anywhere.
|
|
24
|
+
*/
|
|
25
|
+
export async function runBalance(opts: BalanceOpts): Promise<void> {
|
|
26
|
+
const found = await findAndLoadConfig(opts.cwd)
|
|
27
|
+
if (!found) {
|
|
28
|
+
console.error('No promus.config.ts found. Run `promus init` first.')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
const { config } = found
|
|
32
|
+
const agentAddress = (opts.agent ?? config.identity.agent) as Address | undefined
|
|
33
|
+
if (!agentAddress) {
|
|
34
|
+
console.error('No agent address. Run `promus init` first or pass `--agent 0x...`.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const network = config.network as PromusNetwork
|
|
39
|
+
const operatorAddress = config.identity.operator as Address | undefined
|
|
40
|
+
const isSandbox = config.deployTarget === 'sandbox'
|
|
41
|
+
|
|
42
|
+
const mainnetClient = createPublicClient({ transport: http(NETWORK_RPC['0g-mainnet']) })
|
|
43
|
+
const testnetClient = createPublicClient({ transport: http(NETWORK_RPC['0g-testnet']) })
|
|
44
|
+
|
|
45
|
+
const [eoaMainnetWei, eoaTestnetWei, ledger, sandboxReserve] = await Promise.all([
|
|
46
|
+
mainnetClient.getBalance({ address: agentAddress }).catch(() => 0n),
|
|
47
|
+
testnetClient.getBalance({ address: agentAddress }).catch(() => 0n),
|
|
48
|
+
getLedgerDetailReadOnly({ network, agentAddress }).catch(() => null),
|
|
49
|
+
isSandbox && operatorAddress
|
|
50
|
+
? getSandboxBillingReserve({ recipient: operatorAddress }).catch(() => 0n)
|
|
51
|
+
: Promise.resolve(null),
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
console.log('')
|
|
55
|
+
console.log(`agent ${agentAddress}${config.subname ? ` (${config.subname}.promus.0g)` : ''}`)
|
|
56
|
+
console.log(`network ${network}`)
|
|
57
|
+
console.log(`target ${config.deployTarget ?? 'local'}`)
|
|
58
|
+
console.log('')
|
|
59
|
+
console.log('mainnet (chain 16661)')
|
|
60
|
+
console.log(` EOA balance ${format0G(eoaMainnetWei)} 0G`)
|
|
61
|
+
if (ledger) {
|
|
62
|
+
console.log(` compute ledger total ${format0G(ledger.totalBalance)} 0G`)
|
|
63
|
+
console.log(` available ${format0G(ledger.availableBalance)} 0G`)
|
|
64
|
+
console.log(` locked in providers ${format0G(ledger.lockedBalance)} 0G`)
|
|
65
|
+
} else {
|
|
66
|
+
console.log(' compute ledger (not opened — call `promus topup --compute N` to seed)')
|
|
67
|
+
}
|
|
68
|
+
console.log('')
|
|
69
|
+
console.log('testnet/galileo (chain 16602)')
|
|
70
|
+
console.log(` EOA balance ${format0G(eoaTestnetWei)} 0G`)
|
|
71
|
+
if (isSandbox && operatorAddress) {
|
|
72
|
+
if (sandboxReserve !== null) {
|
|
73
|
+
console.log(
|
|
74
|
+
` sandbox billing reserve ${format0G(sandboxReserve)} 0G (operator ${operatorAddress})`,
|
|
75
|
+
)
|
|
76
|
+
} else {
|
|
77
|
+
console.log(' sandbox billing reserve (unavailable — RPC error)')
|
|
78
|
+
}
|
|
79
|
+
} else if (isSandbox) {
|
|
80
|
+
console.log(' sandbox billing reserve (operator address missing in config)')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('')
|
|
84
|
+
console.log('position summary')
|
|
85
|
+
const mainnetTotal = eoaMainnetWei + (ledger?.totalBalance ?? 0n)
|
|
86
|
+
const testnetTotal = eoaTestnetWei + (sandboxReserve ?? 0n)
|
|
87
|
+
console.log(` mainnet total ${format0G(mainnetTotal)} 0G (EOA + ledger)`)
|
|
88
|
+
console.log(` testnet total ${format0G(testnetTotal)} 0G (EOA + sandbox reserve)`)
|
|
89
|
+
|
|
90
|
+
const warnings: string[] = []
|
|
91
|
+
if (eoaMainnetWei < 2_000_000_000_000_000_000n) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
'EOA mainnet below 2 0G notify threshold — auto-topup will fire wallet-low events',
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (ledger && ledger.availableBalance < 500_000_000_000_000_000n) {
|
|
97
|
+
warnings.push(
|
|
98
|
+
'Compute ledger available below 0.5 0G — auto-topup may transfer from EOA into provider envelopes',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
if (isSandbox && sandboxReserve !== null && sandboxReserve < 1_000_000_000_000_000_000n) {
|
|
102
|
+
warnings.push(
|
|
103
|
+
'Sandbox billing reserve below 1 0G — top up via `promus topup --sandbox N` to extend container runtime',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (warnings.length) {
|
|
107
|
+
console.log('')
|
|
108
|
+
console.log('warnings')
|
|
109
|
+
for (const w of warnings) console.log(` · ${w}`)
|
|
110
|
+
}
|
|
111
|
+
console.log('')
|
|
112
|
+
}
|