@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,35 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { type PromusConfig, agentPaths } from '@promus/core'
4
+
5
+ /**
6
+ * Load the user's `promus.config.ts`.
7
+ *
8
+ * Phase 6.6: canonical location is `~/.promus/config.ts` (returned by
9
+ * `agentPaths.config`). If that file exists, it wins. Otherwise, fall back
10
+ * to walking upward from cwd looking for `promus.config.ts` (legacy v0.5.0
11
+ * pattern, kept so existing dev setups still work without a migration step).
12
+ */
13
+ export async function findAndLoadConfig(
14
+ startDir: string = process.cwd(),
15
+ ): Promise<{ config: PromusConfig; path: string } | null> {
16
+ const canonical = agentPaths.config
17
+ if (existsSync(canonical)) {
18
+ const mod = (await import(canonical)) as { default: PromusConfig }
19
+ if (!mod.default) throw new Error(`promus config at ${canonical} has no default export`)
20
+ return { config: mod.default, path: canonical }
21
+ }
22
+
23
+ let dir = resolve(startDir)
24
+ while (true) {
25
+ const candidate = resolve(dir, 'promus.config.ts')
26
+ if (existsSync(candidate)) {
27
+ const mod = (await import(candidate)) as { default: PromusConfig }
28
+ if (!mod.default) throw new Error(`promus.config.ts at ${candidate} has no default export`)
29
+ return { config: mod.default, path: candidate }
30
+ }
31
+ const parent = resolve(dir, '..')
32
+ if (parent === dir) return null
33
+ dir = parent
34
+ }
35
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { PromusConfig } from '@promus/core'
3
+ import { renderConfigTs } from './render'
4
+
5
+ const baseConfig: PromusConfig = {
6
+ identity: { iNFT: null, operator: null, agent: null },
7
+ network: '0g-mainnet',
8
+ storage: { network: '0g-mainnet' },
9
+ brain: { provider: null, model: null },
10
+ plugins: ['system'],
11
+ tools: {},
12
+ imports: { claudeCode: true },
13
+ }
14
+
15
+ describe('renderConfigTs sandbox block', () => {
16
+ test('fresh config (no sandbox set) emits default + commented examples for each tier', () => {
17
+ const out = renderConfigTs(baseConfig)
18
+ // Active default
19
+ expect(out).toContain(`sandbox: { mode: 'none' }`)
20
+ // Commented OPTION 2 (os)
21
+ expect(out).toContain('OPTION 2: os')
22
+ expect(out).toContain(`// sandbox: { mode: 'os' }`)
23
+ // Commented OPTION 3 (docker)
24
+ expect(out).toContain('OPTION 3: docker')
25
+ expect(out).toContain(`// mode: 'docker'`)
26
+ expect(out).toContain(`// dockerImage: 'nikolaik/python-nodejs:python3.11-nodejs20'`)
27
+ expect(out).toContain('// dockerMountWorkspace: false')
28
+ // PROMUS_SANDBOX_MODE override hint
29
+ expect(out).toContain('PROMUS_SANDBOX_MODE')
30
+ })
31
+
32
+ test('config with sandbox.mode="os" already set emits the chosen value, not the template', () => {
33
+ const out = renderConfigTs({ ...baseConfig, sandbox: { mode: 'os' } })
34
+ expect(out).toContain(`"mode": "os"`)
35
+ // No verbose template when operator already chose
36
+ expect(out).not.toContain('OPTION 2: os')
37
+ expect(out).not.toContain('OPTION 3: docker')
38
+ })
39
+
40
+ test('config with sandbox.mode="docker" + image emits the full chosen object', () => {
41
+ const out = renderConfigTs({
42
+ ...baseConfig,
43
+ sandbox: {
44
+ mode: 'docker',
45
+ dockerImage: 'custom/img:tag',
46
+ dockerMountWorkspace: true,
47
+ },
48
+ })
49
+ expect(out).toContain(`"mode": "docker"`)
50
+ expect(out).toContain(`"dockerImage": "custom/img:tag"`)
51
+ expect(out).toContain(`"dockerMountWorkspace": true`)
52
+ expect(out).not.toContain('OPTION')
53
+ })
54
+
55
+ test('annotated template documents resource caps with hermes default values', () => {
56
+ const out = renderConfigTs(baseConfig)
57
+ expect(out).toContain('// dockerCpu: 1')
58
+ expect(out).toContain('// dockerMemoryMb: 5120')
59
+ expect(out).toContain('// dockerDiskMb: 51200')
60
+ expect(out).toContain('// dockerNoNetwork: true')
61
+ })
62
+
63
+ test('config with sandbox docker + resource caps emits chosen numeric values', () => {
64
+ const out = renderConfigTs({
65
+ ...baseConfig,
66
+ sandbox: {
67
+ mode: 'docker',
68
+ dockerCpu: 2,
69
+ dockerMemoryMb: 4096,
70
+ dockerNoNetwork: true,
71
+ },
72
+ })
73
+ expect(out).toContain(`"dockerCpu": 2`)
74
+ expect(out).toContain(`"dockerMemoryMb": 4096`)
75
+ expect(out).toContain(`"dockerNoNetwork": true`)
76
+ expect(out).not.toContain('OPTION')
77
+ })
78
+
79
+ test('output is valid TypeScript (parses as a default-export module)', async () => {
80
+ const out = renderConfigTs(baseConfig)
81
+ const { writeFile, rm, mkdtemp } = await import('node:fs/promises')
82
+ const { tmpdir } = await import('node:os')
83
+ const { join } = await import('node:path')
84
+ const dir = await mkdtemp(join(tmpdir(), 'promus-render-test-'))
85
+ const path = join(dir, 'config.ts')
86
+ try {
87
+ await writeFile(path, out, 'utf8')
88
+ const mod = await import(path)
89
+ expect(mod.default).toBeDefined()
90
+ expect(mod.default.sandbox).toEqual({ mode: 'none' })
91
+ expect(mod.default.network).toBe('0g-mainnet')
92
+ } finally {
93
+ await rm(dir, { recursive: true, force: true })
94
+ }
95
+ })
96
+ })
@@ -0,0 +1,110 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+ import type { PromusConfig } from '@promus/core'
4
+
5
+ export interface RenderConfigOpts {
6
+ header?: string
7
+ subname?: string | null
8
+ }
9
+
10
+ /**
11
+ * Serialize an PromusConfig into a `~/.promus/config.ts` file body.
12
+ *
13
+ * Phase 6.6: the config lives at `~/.promus/config.ts` which is outside any
14
+ * workspace, so it MUST NOT import `@promus/core` (the import won't
15
+ * resolve from `~/.promus/`). We emit a plain `export default { ... }` object;
16
+ * the runtime loader treats it as `PromusConfig` directly.
17
+ */
18
+ export function renderConfigTs(cfg: PromusConfig, opts: RenderConfigOpts = {}): string {
19
+ const header = opts.header ?? ''
20
+ const subnameLine =
21
+ opts.subname !== undefined ? ` subname: ${JSON.stringify(opts.subname)},\n` : ''
22
+ const operatorLine = cfg.operator ? ` operator: ${JSON.stringify(cfg.operator)},\n` : ''
23
+ // Phase 9.5 / v0.10.1: emit either the operator's chosen sandbox config OR an
24
+ // annotated "OPTION 1/2/3" block so the operator can opt in by uncommenting.
25
+ // Mirrors hermes-agent's cli-config.yaml.example pattern: documentation IS
26
+ // the UX, not an interactive wizard. Default mode stays `none` (passthrough)
27
+ // for back-compat.
28
+ const sandboxBlock = renderSandboxBlock(cfg.sandbox)
29
+ const deployTargetLine =
30
+ cfg.deployTarget && cfg.deployTarget !== 'local'
31
+ ? ` deployTarget: ${JSON.stringify(cfg.deployTarget)},\n`
32
+ : ''
33
+ return `${header ? `${header}\n\n` : ''}export default {
34
+ identity: ${JSON.stringify(cfg.identity)},
35
+ network: ${JSON.stringify(cfg.network)},
36
+ storage: { network: ${JSON.stringify(cfg.storage.network)} },
37
+ brain: {
38
+ provider: ${JSON.stringify(cfg.brain.provider)},
39
+ model: ${JSON.stringify(cfg.brain.model)},
40
+ },
41
+ plugins: ${JSON.stringify(cfg.plugins)},
42
+ tools: ${JSON.stringify(cfg.tools)},
43
+ imports: { claudeCode: ${cfg.imports.claudeCode} },
44
+ ${deployTargetLine}${operatorLine}${subnameLine}${sandboxBlock}}
45
+ `
46
+ }
47
+
48
+ function renderSandboxBlock(sandbox: PromusConfig['sandbox']): string {
49
+ // Phase 11: deploy-target sandbox metadata (id/providerAddress/endpoint/
50
+ // snapshotName) OR Phase 9.5 limb-sandbox mode = anything non-default → emit
51
+ // verbatim. Only surface the doc-comment template when the operator has
52
+ // touched neither.
53
+ const hasPhase11Metadata =
54
+ sandbox?.id || sandbox?.providerAddress || sandbox?.endpoint || sandbox?.snapshotName
55
+ const hasNonDefaultLimbMode = sandbox?.mode && sandbox.mode !== 'none'
56
+ if (hasPhase11Metadata || hasNonDefaultLimbMode) {
57
+ return ` sandbox: ${JSON.stringify(sandbox, null, 2).replace(/\n/g, '\n ')},\n`
58
+ }
59
+ // Fresh install: write the active default + commented examples for the
60
+ // other tiers. Operator can opt in by uncommenting and editing.
61
+ return ` sandbox: { mode: 'none' },
62
+ // ---------------------------------------------------------------------------
63
+ // Limb sandbox (Phase 9.5). Defense-in-depth beneath the permission floor:
64
+ // even when the modal grants 'allow session' or YOLO disables prompts,
65
+ // the sandbox profile/container blocks writes outside an allowlist.
66
+ // All shell.run / code.execute / shell.process_start spawns route through
67
+ // the chosen backend. fs.* and browser.* still run on the host (PathGuard
68
+ // applies). Override at runtime via PROMUS_SANDBOX_MODE=os|docker|none.
69
+ //
70
+ // OPTION 1: none (default): passthrough, fastest, permission floor only.
71
+ //
72
+ // OPTION 2: os (macOS sandbox-exec / seatbelt). Allows writes to agentDir +
73
+ // cwd + /tmp/promus-* + /var/folders. Denies reads of ~/.ssh, ~/.aws,
74
+ // ~/Library/Keychains, ~/.config/gcloud. Linux bubblewrap pending.
75
+ // sandbox: { mode: 'os' },
76
+ //
77
+ // OPTION 3: docker (long-lived container per session), every shell-class
78
+ // spawn through 'docker exec'. Auto-detects Docker Desktop or Podman.
79
+ // Default image 'nikolaik/python-nodejs:python3.11-nodejs20' (matches
80
+ // hermes; ~700 MB; bash + python3 + node + npm + git on standard PATH).
81
+ // Override with 'oven/bun:1' (~250 MB) if you only need bun/ts.
82
+ // 'dockerMountWorkspace: true' bind-mounts your launch cwd into
83
+ // /workspace (off by default for max isolation). 'dockerRuntimePath'
84
+ // forces a specific runtime binary. Resource caps are unset by default
85
+ // (container competes fairly with host work). Set them to mirror hermes'
86
+ // production hardening: dockerCpu=1, dockerMemoryMb=5120, dockerDiskMb=
87
+ // 51200 (Linux+overlay2 only). dockerNoNetwork=true blocks all internet
88
+ // access from the container (max paranoia for code.execute).
89
+ // sandbox: {
90
+ // mode: 'docker',
91
+ // dockerImage: 'nikolaik/python-nodejs:python3.11-nodejs20',
92
+ // dockerMountWorkspace: false,
93
+ // // dockerRuntimePath: '/opt/homebrew/bin/podman',
94
+ // // dockerCpu: 1, // CPU cores cap
95
+ // // dockerMemoryMb: 5120, // 5 GB memory cap
96
+ // // dockerDiskMb: 51200, // 50 GB disk cap (Linux + overlay2 only)
97
+ // // dockerNoNetwork: true, // block all network from inside container
98
+ // },
99
+ // ---------------------------------------------------------------------------
100
+ `
101
+ }
102
+
103
+ export async function writeConfigTs(
104
+ path: string,
105
+ cfg: PromusConfig,
106
+ opts: RenderConfigOpts = {},
107
+ ): Promise<void> {
108
+ await mkdir(dirname(path), { recursive: true })
109
+ await writeFile(path, renderConfigTs(cfg, opts), 'utf8')
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,378 @@
1
+ /**
2
+ * CLI argv dispatch. Keeps phase 2 minimal: no subcommand → chat REPL,
3
+ * otherwise route to commands/<name>.
4
+ */
5
+
6
+ const argv = process.argv.slice(2)
7
+ // First arg starting with `--` means the user invoked the default subcommand
8
+ // (chat) with flags, e.g. `promus --yolo`. Treat it as if `chat` were implicit.
9
+ // Exception: `--help` and `--version` are top-level commands, not chat flags.
10
+ const first = argv[0]
11
+ const isTopLevelFlag = first === '--help' || first === '--version'
12
+ const sub = first?.startsWith('--') && !isTopLevelFlag ? 'chat' : first
13
+
14
+ async function main(): Promise<void> {
15
+ switch (sub) {
16
+ case undefined:
17
+ case 'chat': {
18
+ const { runChat } = await import('./commands/chat')
19
+ const resumeIdx = argv.indexOf('--resume')
20
+ const resumeSession = resumeIdx >= 0 ? argv[resumeIdx + 1] : undefined
21
+ await runChat({ yolo: argv.includes('--yolo'), resume: resumeSession })
22
+ return
23
+ }
24
+ case 'init': {
25
+ if (argv.includes('--resume')) {
26
+ const { findAndLoadConfig } = await import('./config/load')
27
+ const loaded = await findAndLoadConfig()
28
+ if (!loaded) {
29
+ console.error('promus init --resume: no promus.config.ts found in cwd or parents.')
30
+ process.exit(1)
31
+ }
32
+ const { runResumeInit } = await import('./commands/init/resume')
33
+ await runResumeInit({ config: loaded.config, configPath: loaded.path })
34
+ return
35
+ }
36
+ const { runInit } = await import('./commands/init')
37
+ await runInit()
38
+ return
39
+ }
40
+ case 'status': {
41
+ const { runStatus } = await import('./commands/status')
42
+ await runStatus()
43
+ return
44
+ }
45
+ case 'logs': {
46
+ const { runLogs } = await import('./commands/logs')
47
+ const tailIdx = argv.indexOf('--tail')
48
+ const tail = tailIdx >= 0 ? Number(argv[tailIdx + 1]) : undefined
49
+ const agentIdx = argv.indexOf('--agent')
50
+ const agent = agentIdx >= 0 ? argv[agentIdx + 1] : undefined
51
+ await runLogs({ agent, tail })
52
+ return
53
+ }
54
+ case 'restore': {
55
+ const ref = argv[1]
56
+ if (!ref) {
57
+ console.error(
58
+ 'usage: promus restore <iNFT-ref>\n ref formats:\n eip155:16661:0x<contract>:<tokenId>\n 0g-mainnet:0x<contract>:<tokenId>\n 0g-testnet:0x<contract>:<tokenId>',
59
+ )
60
+ process.exit(1)
61
+ }
62
+ const { runRestore } = await import('./commands/restore')
63
+ await runRestore({ ref })
64
+ return
65
+ }
66
+ case 'transfer': {
67
+ const { parseTransferArgs, runTransfer } = await import('./commands/transfer')
68
+ const parsed = parseTransferArgs(argv.slice(1))
69
+ if ('error' in parsed) {
70
+ console.error(
71
+ `promus transfer: ${parsed.error}\n usage: promus transfer <iNFT-ref> --to <addr> [--recipient-key 0x...] [--dry-run] [--yes] [--no-purge]`,
72
+ )
73
+ process.exit(1)
74
+ }
75
+ await runTransfer(parsed)
76
+ return
77
+ }
78
+ case 'topup': {
79
+ const agentIdx = argv.indexOf('--agent')
80
+ const computeIdx = argv.indexOf('--compute')
81
+ // v0.21.5: --sandbox is the canonical flag for SandboxBilling (Galileo
82
+ // testnet runtime fees). --provider stays as a deprecated alias for
83
+ // backwards compat with v0.17.1+ runbooks.
84
+ const sandboxIdx = argv.indexOf('--sandbox')
85
+ const providerIdx = argv.indexOf('--provider')
86
+ const visionIdx = argv.indexOf('--vision')
87
+ const parseAmount = (flag: string, raw: string | undefined): number | undefined => {
88
+ if (raw === undefined) return undefined
89
+ const n = Number(raw)
90
+ if (!Number.isFinite(n) || n <= 0 || n > 1e6) {
91
+ console.error(
92
+ `Bad amount for ${flag}: ${raw}\n Each topup flag takes an amount in 0G, not an address: ${flag} <amount>\n Examples: promus topup --compute 2 promus topup --sandbox 5 promus topup --agent 1 promus topup --vision 1`,
93
+ )
94
+ process.exit(2)
95
+ }
96
+ return n
97
+ }
98
+ const agent = parseAmount('--agent', agentIdx >= 0 ? argv[agentIdx + 1] : undefined)
99
+ const compute = parseAmount('--compute', computeIdx >= 0 ? argv[computeIdx + 1] : undefined)
100
+ const sandboxArg = parseAmount(
101
+ '--sandbox',
102
+ sandboxIdx >= 0 ? argv[sandboxIdx + 1] : undefined,
103
+ )
104
+ const providerLegacyArg = parseAmount(
105
+ '--provider',
106
+ providerIdx >= 0 ? argv[providerIdx + 1] : undefined,
107
+ )
108
+ const vision = parseAmount('--vision', visionIdx >= 0 ? argv[visionIdx + 1] : undefined)
109
+ if (providerLegacyArg !== undefined && sandboxArg === undefined) {
110
+ console.warn(
111
+ '[deprecated] `promus topup --provider` is renamed to `--sandbox` (Galileo testnet billing); both flags work for now but `--provider` will be removed in a future release.',
112
+ )
113
+ } else if (providerLegacyArg !== undefined && sandboxArg !== undefined) {
114
+ console.error(
115
+ 'promus topup: cannot pass both --sandbox and --provider; pick one (--sandbox is canonical).',
116
+ )
117
+ process.exit(2)
118
+ }
119
+ const sandbox = sandboxArg ?? providerLegacyArg
120
+ const { runTopup } = await import('./commands/topup')
121
+ await runTopup({ agent, compute, sandbox, vision })
122
+ return
123
+ }
124
+ case 'model': {
125
+ const { runModel } = await import('./commands/model')
126
+ await runModel()
127
+ return
128
+ }
129
+ case 'sync': {
130
+ const { runSync } = await import('./commands/sync')
131
+ await runSync()
132
+ return
133
+ }
134
+ case 'profile': {
135
+ // v0.23.0: `promus profile init` seeds user/profile.md if missing, derives
136
+ // the operator-scoped PROFILE AES key, and either (sandbox) POSTs it to
137
+ // /admin/profile-key or (local) runs a sync that anchors the slot.
138
+ const profileSub = argv[1]
139
+ if (profileSub === 'init') {
140
+ const { runProfileInit } = await import('./commands/profile')
141
+ await runProfileInit()
142
+ return
143
+ }
144
+ console.error(
145
+ `Unknown profile subcommand: ${profileSub ?? '(none)'} — try 'promus profile init'`,
146
+ )
147
+ process.exit(1)
148
+ return
149
+ }
150
+ case 'migrate-keystore': {
151
+ const { runMigrateKeystore } = await import('./commands/migrate-keystore')
152
+ await runMigrateKeystore()
153
+ return
154
+ }
155
+ case 'deploy': {
156
+ const { runDeploy } = await import('./commands/deploy')
157
+ await runDeploy()
158
+ return
159
+ }
160
+ case 'upgrade': {
161
+ const { parseUpgradeArgs, runUpgrade } = await import('./commands/upgrade')
162
+ await runUpgrade(parseUpgradeArgs(argv.slice(1)))
163
+ return
164
+ }
165
+ case 'resume': {
166
+ const yes = argv.includes('--yes') || argv.includes('-y')
167
+ const { runResume } = await import('./commands/resume')
168
+ await runResume({ yes })
169
+ return
170
+ }
171
+ case 'pause': {
172
+ const yes = argv.includes('--yes') || argv.includes('-y')
173
+ const { runPause } = await import('./commands/pause')
174
+ await runPause({ yes })
175
+ return
176
+ }
177
+ case 'ledger': {
178
+ const sub = argv[1]
179
+ const validSubs = ['balance', 'refund', 'retrieve', 'close'] as const
180
+ type Sub = (typeof validSubs)[number]
181
+ if (sub && !validSubs.includes(sub as Sub)) {
182
+ console.error(
183
+ `promus ledger: unknown subcommand '${sub}' (expected: ${validSubs.join(' | ')})`,
184
+ )
185
+ process.exit(1)
186
+ }
187
+ const chosen = (sub ?? 'balance') as Sub
188
+ const amountIdx = argv.indexOf('--amount')
189
+ const amount = amountIdx >= 0 ? Number(argv[amountIdx + 1]) : undefined
190
+ const all = argv.includes('--all')
191
+ const yes = argv.includes('--yes') || argv.includes('-y')
192
+ const { runLedger } = await import('./commands/ledger')
193
+ await runLedger({ sub: chosen, amount, all, yes })
194
+ return
195
+ }
196
+ case 'balance': {
197
+ const agentIdx = argv.indexOf('--agent')
198
+ const agent = agentIdx >= 0 ? argv[agentIdx + 1] : undefined
199
+ const { runBalance } = await import('./commands/balance')
200
+ await runBalance({ agent })
201
+ return
202
+ }
203
+ case 'drain': {
204
+ const toIdx = argv.indexOf('--to')
205
+ const to = toIdx >= 0 ? argv[toIdx + 1] : undefined
206
+ const yes = argv.includes('--yes') || argv.includes('-y')
207
+ const { runDrain } = await import('./commands/drain')
208
+ await runDrain({ to, yes })
209
+ return
210
+ }
211
+ case 'telegram': {
212
+ const { parseTelegramArgs, runTelegram } = await import('./commands/telegram')
213
+ const parsed = parseTelegramArgs(argv.slice(1))
214
+ if ('error' in parsed) {
215
+ console.error(`promus telegram: ${parsed.error}`)
216
+ process.exit(1)
217
+ }
218
+ await runTelegram(parsed)
219
+ return
220
+ }
221
+ case 'pairing': {
222
+ const { parsePairingArgs, runPairing } = await import('./commands/pairing')
223
+ const parsed = parsePairingArgs(argv.slice(1))
224
+ if ('error' in parsed) {
225
+ console.error(`promus pairing: ${parsed.error}`)
226
+ process.exit(1)
227
+ }
228
+ await runPairing(parsed)
229
+ return
230
+ }
231
+ case 'admin': {
232
+ const { parseAdminArgs, runAdmin } = await import('./commands/admin')
233
+ const parsed = parseAdminArgs(argv.slice(1))
234
+ if ('error' in parsed) {
235
+ console.error(`promus admin: ${parsed.error}`)
236
+ process.exit(1)
237
+ }
238
+ await runAdmin(parsed)
239
+ return
240
+ }
241
+ case 'gateway': {
242
+ const { parseGatewayArgs, runGateway } = await import('./commands/gateway')
243
+ const parsed = parseGatewayArgs(argv.slice(1))
244
+ if ('error' in parsed) {
245
+ console.error(`promus gateway: ${parsed.error}`)
246
+ process.exit(1)
247
+ }
248
+ await runGateway(parsed)
249
+ return
250
+ }
251
+ case 'inspect': {
252
+ const { runInspect, isValidSlot } = await import('./commands/inspect')
253
+ const remaining = argv.slice(1)
254
+ const positional: string[] = []
255
+ const flags: Record<string, string | boolean> = {}
256
+ for (let i = 0; i < remaining.length; i++) {
257
+ const a = remaining[i]!
258
+ if (a === '--raw' || a === '--diff' || a === '--json' || a === '--full') {
259
+ flags[a.slice(2)] = true
260
+ } else if (a === '--slot' || a === '--tx' || a === '--out') {
261
+ const v = remaining[++i]
262
+ if (!v) {
263
+ console.error(`promus inspect: ${a} requires a value`)
264
+ process.exit(1)
265
+ }
266
+ flags[a.slice(2)] = v
267
+ } else if (a.startsWith('--')) {
268
+ console.error(`promus inspect: unknown flag ${a}`)
269
+ process.exit(1)
270
+ } else {
271
+ positional.push(a)
272
+ }
273
+ }
274
+ if (positional.length > 1) {
275
+ console.error('promus inspect: at most one positional ref allowed')
276
+ process.exit(1)
277
+ }
278
+ const slotFlag = flags.slot
279
+ let slotName: import('@promus/core').IntelligentDataSlot | undefined
280
+ if (typeof slotFlag === 'string') {
281
+ if (!isValidSlot(slotFlag)) {
282
+ console.error(
283
+ 'promus inspect: --slot must be one of memory-index, identity, persona, profile, keystore, activity-log',
284
+ )
285
+ process.exit(1)
286
+ }
287
+ slotName = slotFlag
288
+ }
289
+ const txFlag = flags.tx
290
+ if (typeof txFlag === 'string' && !/^0x[0-9a-fA-F]{64}$/.test(txFlag)) {
291
+ console.error('promus inspect: --tx must be a 32-byte hex hash')
292
+ process.exit(1)
293
+ }
294
+ await runInspect({
295
+ ref: positional[0],
296
+ slot: slotName,
297
+ tx: typeof txFlag === 'string' ? (txFlag as `0x${string}`) : undefined,
298
+ raw: flags.raw === true,
299
+ diff: flags.diff === true,
300
+ json: flags.json === true,
301
+ full: flags.full === true,
302
+ out: typeof flags.out === 'string' ? flags.out : undefined,
303
+ })
304
+ return
305
+ }
306
+ case '-h':
307
+ case '--help':
308
+ case 'help': {
309
+ printHelp()
310
+ return
311
+ }
312
+ case '-v':
313
+ case '--version':
314
+ case 'version': {
315
+ const { resolveCliVersion } = await import('./util/cli-version')
316
+ const v = await resolveCliVersion()
317
+ console.log(v)
318
+ return
319
+ }
320
+ default: {
321
+ console.log(`Unknown command: ${sub}`)
322
+ printHelp()
323
+ process.exit(1)
324
+ }
325
+ }
326
+ }
327
+
328
+ function printHelp(): void {
329
+ console.log(
330
+ [
331
+ 'promus: sovereign agent harness CLI',
332
+ '',
333
+ 'Commands:',
334
+ ' promus init bootstrap a new agent identity + keystore',
335
+ ' promus [--yolo] interactive chat with your agent (default; --yolo skips approvals)',
336
+ ' promus status show agent + wallet + config state',
337
+ ' promus logs tail the activity log (flags: --tail N, --agent <id>)',
338
+ ' promus restore <ref> recover an agent from an iNFT (ref: eip155:421614:0x..:N)',
339
+ ' promus transfer <ref> transfer iNFT to a new operator with re-encrypted keystore',
340
+ ' flags: --to <addr>, --recipient-key <hex>, --oracle-key <hex>,',
341
+ ' --dry-run, --yes, --no-purge',
342
+ ' promus balance full economic position: agent EOA + operator balance',
343
+ " flags: --agent <addr> (defaults to active config's agent)",
344
+ ' promus drain --to <addr> sweep agent EOA balance to address (default: operator)',
345
+ ' promus sync force flush memory + activity-log to IPFS + anchor on chain',
346
+ ' promus migrate-keystore upgrade v0.5.0 passphrase keystore to v0.6 operator-wallet',
347
+ ' promus upgrade [<ref>] roll harness to new ref in place (default: latest published release)',
348
+ ' flags: --ref vX.Y.Z',
349
+ ' promus telegram <sub> configure phone-DM gateway (subs: setup | status | remove)',
350
+ ' flags: --yes (skip remove confirmation)',
351
+ ' promus pairing <sub> manage DM pairing approvals (subs: list | approve | revoke | clear-pending)',
352
+ ' usage: promus pairing approve telegram <code>',
353
+ ' promus gateway <sub> always-on agent gateway daemon (subs: run | start | stop | restart | status | logs)',
354
+ ' run = foreground, start = bg + Touch ID, stop = SIGTERM via lock',
355
+ ' promus inspect [ref] audit on-chain memory slots (flags: --slot, --tx, --raw, --diff, --json, --full, --out <dir>)',
356
+ ' promus version print CLI version (aliases: --version, -v)',
357
+ ' promus help show this message (aliases: --help, -h)',
358
+ '',
359
+ '(`promus` remains a working alias for `promus`.)',
360
+ '',
361
+ ].join('\n'),
362
+ )
363
+ }
364
+
365
+ main()
366
+ .then(() => {
367
+ // Force-exit on success because some 0G SDKs (Storage Indexer, Compute
368
+ // broker, WalletConnect relay) leak open handles (websockets, heartbeat
369
+ // timers) that we don't have hooks to drain. Without this, one-shot
370
+ // commands like `promus init` would hang at the prompt indefinitely after
371
+ // their work completed. `chat` returns only when the user actually quits,
372
+ // so this also gives chat a clean exit. Exit code 0 = normal success.
373
+ process.exit(0)
374
+ })
375
+ .catch(e => {
376
+ console.error('fatal:', (e as Error).message)
377
+ process.exit(1)
378
+ })