@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,86 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import {
3
+ SANDBOX_PROVIDER_URL_GALILEO,
4
+ SandboxProviderClient,
5
+ agentPaths,
6
+ } from '@promus/core'
7
+ import { findAndLoadConfig } from '../config/load'
8
+ import { pickDefaultAgent } from './_agents'
9
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
10
+ import { extractExecOutput } from './init/sandbox-provision'
11
+
12
+ export async function runLogs(opts: { agent?: string; tail?: number } = {}): Promise<void> {
13
+ // Phase 11: in sandbox mode the activity log lives in the container at
14
+ // /var/log/@promus/gateway.log. Tail it via toolbox exec.
15
+ const found = await findAndLoadConfig().catch(() => null)
16
+ if (
17
+ found?.config.deployTarget === 'sandbox' &&
18
+ found.config.sandbox?.id &&
19
+ found.config.sandbox.endpoint
20
+ ) {
21
+ const operator = await loadOrPickOperatorSigner({
22
+ network: found.config.network,
23
+ hint: found.config.operator,
24
+ })
25
+ if (!operator) {
26
+ console.log('No operator wallet available; cannot authenticate to provider.')
27
+ process.exit(1)
28
+ }
29
+ const operatorAccount = await operator.account()
30
+ const provider = new SandboxProviderClient({
31
+ endpoint: SANDBOX_PROVIDER_URL_GALILEO,
32
+ operator: operatorAccount,
33
+ })
34
+ const tail = opts.tail ?? 200
35
+ try {
36
+ const r = await provider.execInToolbox(found.config.sandbox.id, {
37
+ // Harness logs to ~/promus-logs/ inside the container (daytona user;
38
+ // /var/log needs root). bash -c needed because Daytona exec splits
39
+ // argv-style without a shell.
40
+ command: `bash -c 'tail -n ${tail} ~/promus-logs/@promus/gateway.log'`,
41
+ timeout: 60,
42
+ })
43
+ const out = extractExecOutput(r)
44
+ if (out) process.stdout.write(out)
45
+ if (r.exitCode !== 0) {
46
+ process.stderr.write(`\n(toolbox exit=${r.exitCode})\n`)
47
+ }
48
+ } catch (e) {
49
+ console.log(`harness log fetch failed: ${(e as Error).message.slice(0, 200)}`)
50
+ }
51
+ await operator.close?.()
52
+ return
53
+ }
54
+
55
+ // Local mode: read from agentPaths
56
+ const id = opts.agent ?? (await pickDefaultAgent())
57
+ if (!id) {
58
+ console.log('No agents found in ~/.promus/agents. Run `promus init` first.')
59
+ process.exit(1)
60
+ }
61
+ const path = agentPaths.agent(id).activityLog
62
+
63
+ let raw: string
64
+ try {
65
+ raw = await readFile(path, 'utf8')
66
+ } catch (e) {
67
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
68
+ console.log(`No activity log at ${path}`)
69
+ return
70
+ }
71
+ throw e
72
+ }
73
+
74
+ const lines = raw.trimEnd().split('\n').filter(Boolean)
75
+ const slice = opts.tail ? lines.slice(-opts.tail) : lines
76
+ for (const line of slice) {
77
+ try {
78
+ const entry = JSON.parse(line) as { ts: number; kind: string; data: unknown }
79
+ const d = new Date(entry.ts).toISOString()
80
+ const body = JSON.stringify(entry.data)
81
+ console.log(`${d} ${entry.kind.padEnd(16)} ${body.slice(0, 200)}`)
82
+ } catch {
83
+ console.log(line)
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,155 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { cancel, confirm, intro, isCancel, note, outro, password, spinner } from '@clack/prompts'
3
+ import {
4
+ type EncryptedKeystore,
5
+ agentPaths,
6
+ decryptKey,
7
+ defineConfig,
8
+ iNFTAgentId,
9
+ uploadKeystore,
10
+ } from '@promus/core'
11
+ import { type Address, bytesToHex } from 'viem'
12
+ import { privateKeyToAccount } from 'viem/accounts'
13
+ import { findAndLoadConfig } from '../config/load'
14
+ import { writeConfigTs } from '../config/render'
15
+ import { pickOperatorSigner } from './init/operator-picker'
16
+
17
+ /**
18
+ * One-shot upgrade for v0.5.0 users: read the legacy passphrase-encrypted
19
+ * keystore, decrypt it, re-encrypt under the operator wallet (Phase 6.6),
20
+ * upload the new ciphertext to 0G Storage, anchor the new root hash into
21
+ * the iNFT keystore slot, and remove the local passphrase keystore file.
22
+ *
23
+ * After running, the agent is on the v2 (sign-derived-key) path; chat /
24
+ * topup --compute / restore / resume all work without a passphrase.
25
+ */
26
+ export async function runMigrateKeystore(): Promise<void> {
27
+ intro('promus migrate-keystore')
28
+
29
+ const loaded = await findAndLoadConfig()
30
+ if (!loaded) {
31
+ cancel('No promus config found. Run `promus init` (or `promus restore`) first.')
32
+ return
33
+ }
34
+ const { config } = loaded
35
+ if (!config.identity.iNFT || !config.identity.agent) {
36
+ cancel('Config has no iNFT or agent. Nothing to migrate.')
37
+ return
38
+ }
39
+
40
+ const network = config.network
41
+ const contractAddress = config.identity.iNFT.contract as Address
42
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
43
+ const agentAddress = config.identity.agent as Address
44
+ const finalAgentId = iNFTAgentId({ contractAddress, tokenId })
45
+ const paths = agentPaths.agent(finalAgentId)
46
+
47
+ let v1: EncryptedKeystore
48
+ try {
49
+ const raw = await readFile(paths.keystore, 'utf8')
50
+ v1 = JSON.parse(raw) as EncryptedKeystore
51
+ } catch (e) {
52
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
53
+ cancel(
54
+ `No local keystore at ${paths.keystore}. Migration only works when v0.5.0 left a passphrase keystore on disk. If you only have the iNFT, use \`promus restore\` (it handles v1 directly).`,
55
+ )
56
+ return
57
+ }
58
+ cancel(`Failed to read existing keystore: ${(e as Error).message}`)
59
+ return
60
+ }
61
+ if (v1.version !== 1) {
62
+ cancel(
63
+ `Local keystore is already version ${v1.version}. Nothing to migrate. (Phase 6.6 keystores are version 2.)`,
64
+ )
65
+ return
66
+ }
67
+
68
+ const pass = await password({
69
+ message: 'Current passphrase for the agent keystore',
70
+ validate: v => (v && v.length >= 1 ? undefined : 'Required.'),
71
+ })
72
+ if (isCancel(pass)) {
73
+ cancel('Aborted.')
74
+ return
75
+ }
76
+
77
+ let agentPrivkey: `0x${string}`
78
+ try {
79
+ const bytes = decryptKey(v1, pass as string)
80
+ agentPrivkey = bytesToHex(bytes)
81
+ const derived = privateKeyToAccount(agentPrivkey).address
82
+ if (derived.toLowerCase() !== agentAddress.toLowerCase()) {
83
+ cancel(
84
+ `Decrypted keystore points to ${derived} but config says ${agentAddress}. Refusing to overwrite.`,
85
+ )
86
+ return
87
+ }
88
+ } catch (e) {
89
+ cancel(`Decrypt failed: ${(e as Error).message}. Wrong passphrase?`)
90
+ return
91
+ }
92
+
93
+ const proceed = await confirm({
94
+ message:
95
+ 'About to re-encrypt to operator wallet, upload to 0G Storage, update iNFT slot, and delete the local passphrase keystore. Continue?',
96
+ initialValue: true,
97
+ })
98
+ if (isCancel(proceed) || !proceed) {
99
+ cancel('Aborted.')
100
+ return
101
+ }
102
+
103
+ const picked = await pickOperatorSigner({ network })
104
+ if (!picked) return
105
+ const { signer: operator, hint: operatorHint } = picked
106
+
107
+ const sUpload = spinner()
108
+ sUpload.start('Encrypting to operator wallet + uploading to 0G Storage')
109
+ let rootHash: string
110
+ try {
111
+ const result = await uploadKeystore({
112
+ network,
113
+ signer: operator,
114
+ agentAddress,
115
+ agentPrivkey,
116
+ tokenId,
117
+ contractAddress,
118
+ cachePath: paths.keystore,
119
+ })
120
+ rootHash = result.rootHash
121
+ sUpload.stop(`re-anchored at root ${rootHash.slice(0, 12)}…`)
122
+ } catch (e) {
123
+ sUpload.stop(`upload failed: ${(e as Error).message.slice(0, 160)}`)
124
+ note(
125
+ 'Local v1 keystore is unchanged. Re-run `promus migrate-keystore` after fixing the issue.',
126
+ 'no changes made',
127
+ )
128
+ await operator.close?.()
129
+ return
130
+ }
131
+
132
+ // Update config to record the operator hint so subsequent commands skip the picker.
133
+ const cfg = defineConfig({
134
+ ...config,
135
+ operator: operatorHint,
136
+ })
137
+ await writeConfigTs(loaded.path, cfg, {
138
+ header: '// Updated by `promus migrate-keystore`. Edit freely; type-safe.',
139
+ })
140
+
141
+ await operator.close?.()
142
+
143
+ outro(
144
+ [
145
+ '',
146
+ 'Migration complete.',
147
+ ` agent ${agentAddress}`,
148
+ ` iNFT slot updated → ${rootHash.slice(0, 12)}…`,
149
+ ` keystore ${paths.keystore} (now v2 cache)`,
150
+ ` config operator source persisted: ${operatorHint.source}`,
151
+ '',
152
+ 'You can now use `promus` (chat) / `promus topup --compute` without a passphrase.',
153
+ ].join('\n'),
154
+ )
155
+ }
@@ -0,0 +1,48 @@
1
+ import { cancel, intro, outro } from '@clack/prompts'
2
+ import { defineConfig } from '@promus/core'
3
+ import { findAndLoadConfig } from '../config/load'
4
+ import { writeConfigTs } from '../config/render'
5
+ import { pickBrainModel } from './init/model-picker'
6
+
7
+ /**
8
+ * `promus model` — re-pick the brain provider/model. Updates the persisted
9
+ * config so subsequent `promus` (chat) sessions use the new choice.
10
+ *
11
+ * The TUI also exposes `/model` as a slash command for in-session switching;
12
+ * see `chat.tsx`.
13
+ */
14
+ export async function runModel(): Promise<void> {
15
+ intro('promus model')
16
+
17
+ const loaded = await findAndLoadConfig()
18
+ if (!loaded) {
19
+ cancel('No promus.config.ts found. Run `promus init` first.')
20
+ return
21
+ }
22
+ const { config } = loaded
23
+
24
+ const pick = await pickBrainModel({ network: config.network })
25
+ if (!pick) {
26
+ cancel('No model picked.')
27
+ return
28
+ }
29
+
30
+ const updated = defineConfig({
31
+ ...config,
32
+ brain: { provider: pick.provider, model: pick.model },
33
+ })
34
+ await writeConfigTs(loaded.path, updated, {
35
+ header: '// Updated by `promus model`. Edit freely; type-safe.',
36
+ })
37
+
38
+ outro(
39
+ [
40
+ '',
41
+ ` brain ${pick.model ?? '?'}`,
42
+ ` provider ${pick.provider}`,
43
+ ` config ${loaded.path}`,
44
+ '',
45
+ 'Next chat session will use the new brain.',
46
+ ].join('\n'),
47
+ )
48
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ PAIRING_ALPHABET,
3
+ PAIRING_CODE_LENGTH,
4
+ PairingStore,
5
+ agentPaths,
6
+ iNFTAgentId,
7
+ } from '@promus/core'
8
+ import { getAddress } from 'viem'
9
+ import { findAndLoadConfig } from '../config/load'
10
+ import { SandboxClient } from '../sandbox/client'
11
+ import { loadOrPickOperatorSigner } from './init/operator-picker'
12
+
13
+ export interface RunPairingApproveOpts {
14
+ platform: string
15
+ code: string
16
+ }
17
+
18
+ export async function runPairingApprove(opts: RunPairingApproveOpts): Promise<void> {
19
+ const normalized = opts.code.toUpperCase().trim()
20
+ if (normalized.length !== PAIRING_CODE_LENGTH) {
21
+ console.error(
22
+ `Invalid pairing code: expected ${PAIRING_CODE_LENGTH} characters, got ${normalized.length}`,
23
+ )
24
+ process.exit(1)
25
+ }
26
+ for (const ch of normalized) {
27
+ if (!PAIRING_ALPHABET.includes(ch)) {
28
+ console.error(`Invalid pairing code: contains '${ch}' which is not in the pairing alphabet`)
29
+ process.exit(1)
30
+ }
31
+ }
32
+
33
+ const loaded = await findAndLoadConfig()
34
+ if (!loaded) {
35
+ console.error('No promus.config.ts found. Run `promus init` first.')
36
+ process.exit(1)
37
+ }
38
+ const { config } = loaded
39
+ if (!config.identity.iNFT) {
40
+ console.error('Config has no iNFT. Run `promus init` first.')
41
+ process.exit(1)
42
+ }
43
+
44
+ // v0.24.4: sandbox-deployed agents need their pairing dir reached over the
45
+ // signed admin endpoint; the local PairingStore on the host won't see codes
46
+ // generated by the container. Mirror the autotopup-tick / profile-key
47
+ // sandbox branch in admin-autotopup-tick.ts.
48
+ if (config.deployTarget === 'sandbox' && config.sandbox?.endpoint && config.sandbox.id) {
49
+ const signer = await loadOrPickOperatorSigner({
50
+ network: config.network,
51
+ hint: config.operator,
52
+ })
53
+ if (!signer) {
54
+ console.error('failed to load operator signer (cancelled or no key)')
55
+ process.exit(1)
56
+ }
57
+ const operatorAccount = await signer.account()
58
+ const client = new SandboxClient({
59
+ endpoint: config.sandbox.endpoint,
60
+ sandboxId: config.sandbox.id,
61
+ operator: operatorAccount,
62
+ })
63
+ try {
64
+ const result = await client.approvePairing(opts.platform, normalized)
65
+ if (!result.ok) {
66
+ if (result.reason === 'locked-out') {
67
+ console.error(
68
+ `Platform '${opts.platform}' is locked out due to repeated bad codes. Wait 1 hour and try again.`,
69
+ )
70
+ process.exit(1)
71
+ }
72
+ console.error(`Code ${normalized} not found in pending list. Maybe it expired (1h TTL).`)
73
+ process.exit(1)
74
+ }
75
+ console.log(
76
+ `✓ Approved on ${opts.platform}: id=${result.userId}${
77
+ result.userName ? ` (@${result.userName})` : ''
78
+ }`,
79
+ )
80
+ console.log('The user can now DM the bot. Their next message will be processed.')
81
+ return
82
+ } catch (e) {
83
+ console.error(`pairing-approve failed: ${(e as Error).message.slice(0, 240)}`)
84
+ process.exit(1)
85
+ }
86
+ }
87
+
88
+ // Local deploy: operate directly on the host's PairingStore (same path as
89
+ // the daemon process when PROMUS_FORCE_EMBEDDED or local-mode chat.tsx).
90
+ const inftContract = getAddress(config.identity.iNFT.contract) as `0x${string}`
91
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
92
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
93
+ const dir = agentPaths.agent(agentId).pairingDir
94
+ const store = new PairingStore({ dir })
95
+
96
+ const result = store.approveCode(opts.platform, normalized)
97
+ if (!result) {
98
+ if (store.isLockedOut(opts.platform)) {
99
+ console.error(
100
+ `Platform '${opts.platform}' is locked out due to repeated bad codes. Wait 1 hour and try again.`,
101
+ )
102
+ process.exit(1)
103
+ }
104
+ console.error(`Code ${normalized} not found in pending list. Maybe it expired (1h TTL).`)
105
+ process.exit(1)
106
+ }
107
+
108
+ console.log(
109
+ `✓ Approved on ${opts.platform}: id=${result.userId}${
110
+ result.userName ? ` (@${result.userName})` : ''
111
+ }`,
112
+ )
113
+ console.log('The user can now DM the bot. Their next message will be processed.')
114
+ }
@@ -0,0 +1,42 @@
1
+ import { confirm, isCancel } from '@clack/prompts'
2
+ import { PairingStore, agentPaths, iNFTAgentId } from '@promus/core'
3
+ import { getAddress } from 'viem'
4
+ import { findAndLoadConfig } from '../config/load'
5
+
6
+ export interface RunPairingClearOpts {
7
+ platform?: string
8
+ yes?: boolean
9
+ }
10
+
11
+ export async function runPairingClear(opts: RunPairingClearOpts): Promise<void> {
12
+ const loaded = await findAndLoadConfig()
13
+ if (!loaded) {
14
+ console.error('No promus.config.ts found. Run `promus init` first.')
15
+ process.exit(1)
16
+ }
17
+ const { config } = loaded
18
+ if (!config.identity.iNFT) {
19
+ console.error('Config has no iNFT. Run `promus init` first.')
20
+ process.exit(1)
21
+ }
22
+ const inftContract = getAddress(config.identity.iNFT.contract) as `0x${string}`
23
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
24
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
25
+ const dir = agentPaths.agent(agentId).pairingDir
26
+ const store = new PairingStore({ dir })
27
+
28
+ if (!opts.yes) {
29
+ const target = opts.platform ? `${opts.platform} pending` : 'ALL pending pairing codes'
30
+ const ok = await confirm({
31
+ message: `Clear ${target}?`,
32
+ initialValue: false,
33
+ })
34
+ if (isCancel(ok) || !ok) {
35
+ console.log('Aborted.')
36
+ return
37
+ }
38
+ }
39
+
40
+ const count = store.clearPending(opts.platform)
41
+ console.log(`✓ Cleared ${count} pending pairing code${count === 1 ? '' : 's'}`)
42
+ }
@@ -0,0 +1,58 @@
1
+ import { PairingStore, agentPaths, iNFTAgentId } from '@promus/core'
2
+ import { getAddress } from 'viem'
3
+ import { findAndLoadConfig } from '../config/load'
4
+
5
+ export interface RunPairingListOpts {
6
+ platform?: string
7
+ }
8
+
9
+ export async function runPairingList(opts: RunPairingListOpts): Promise<void> {
10
+ const store = await openPairingStore()
11
+ if (!store) return
12
+
13
+ const pending = store.listPending(opts.platform)
14
+ const approved = store.listApproved(opts.platform)
15
+
16
+ const pendingTitle = opts.platform ? `Pending (${opts.platform})` : 'Pending'
17
+ console.log(`\n${pendingTitle} (1h TTL):`)
18
+ if (pending.length === 0) {
19
+ console.log(' (none)')
20
+ } else {
21
+ for (const p of pending) {
22
+ const userLabel = p.userName ? `@${p.userName}` : '(unknown)'
23
+ const idLabel = `id=${p.userId}`
24
+ console.log(` [${p.platform}] ${p.code} ${userLabel} ${idLabel} age=${p.ageMinutes}m`)
25
+ }
26
+ }
27
+
28
+ const approvedTitle = opts.platform ? `Approved (${opts.platform})` : 'Approved'
29
+ console.log(`\n${approvedTitle}:`)
30
+ if (approved.length === 0) {
31
+ console.log(' (none)')
32
+ } else {
33
+ for (const a of approved) {
34
+ const userLabel = a.userName ? `@${a.userName}` : '(unknown)'
35
+ const idLabel = `id=${a.userId}`
36
+ console.log(` [${a.platform}] ${userLabel} ${idLabel}`)
37
+ }
38
+ }
39
+ console.log()
40
+ }
41
+
42
+ async function openPairingStore(): Promise<PairingStore | null> {
43
+ const loaded = await findAndLoadConfig()
44
+ if (!loaded) {
45
+ console.error('No promus.config.ts found. Run `promus init` first.')
46
+ return null
47
+ }
48
+ const { config } = loaded
49
+ if (!config.identity.iNFT) {
50
+ console.error('Config has no iNFT. Run `promus init` first.')
51
+ return null
52
+ }
53
+ const inftContract = getAddress(config.identity.iNFT.contract) as `0x${string}`
54
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
55
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
56
+ const dir = agentPaths.agent(agentId).pairingDir
57
+ return new PairingStore({ dir })
58
+ }
@@ -0,0 +1,52 @@
1
+ import { confirm, isCancel } from '@clack/prompts'
2
+ import { PairingStore, agentPaths, iNFTAgentId } from '@promus/core'
3
+ import { getAddress } from 'viem'
4
+ import { findAndLoadConfig } from '../config/load'
5
+
6
+ export interface RunPairingRevokeOpts {
7
+ platform: string
8
+ userId: string
9
+ yes?: boolean
10
+ }
11
+
12
+ export async function runPairingRevoke(opts: RunPairingRevokeOpts): Promise<void> {
13
+ const loaded = await findAndLoadConfig()
14
+ if (!loaded) {
15
+ console.error('No promus.config.ts found. Run `promus init` first.')
16
+ process.exit(1)
17
+ }
18
+ const { config } = loaded
19
+ if (!config.identity.iNFT) {
20
+ console.error('Config has no iNFT. Run `promus init` first.')
21
+ process.exit(1)
22
+ }
23
+ const inftContract = getAddress(config.identity.iNFT.contract) as `0x${string}`
24
+ const tokenId = BigInt(config.identity.iNFT.tokenId)
25
+ const agentId = iNFTAgentId({ contractAddress: inftContract, tokenId })
26
+ const dir = agentPaths.agent(agentId).pairingDir
27
+ const store = new PairingStore({ dir })
28
+
29
+ if (!store.isApproved(opts.platform, opts.userId)) {
30
+ console.error(`User ${opts.userId} is not on the ${opts.platform} approved list.`)
31
+ process.exit(1)
32
+ }
33
+
34
+ if (!opts.yes) {
35
+ const ok = await confirm({
36
+ message: `Revoke ${opts.platform} access for user id ${opts.userId}?`,
37
+ initialValue: false,
38
+ })
39
+ if (isCancel(ok) || !ok) {
40
+ console.log('Aborted.')
41
+ return
42
+ }
43
+ }
44
+
45
+ const removed = store.revoke(opts.platform, opts.userId)
46
+ if (removed) {
47
+ console.log(`✓ Revoked: ${opts.platform} id=${opts.userId}`)
48
+ } else {
49
+ console.error('Revoke failed (concurrent removal?)')
50
+ process.exit(1)
51
+ }
52
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { parsePairingArgs } from './pairing'
3
+
4
+ describe('parsePairingArgs', () => {
5
+ it('errors on no args', () => {
6
+ const r = parsePairingArgs([])
7
+ expect('error' in r).toBe(true)
8
+ })
9
+
10
+ it('errors on unknown subcommand', () => {
11
+ const r = parsePairingArgs(['quack'])
12
+ expect('error' in r).toBe(true)
13
+ })
14
+
15
+ it('parses `list`', () => {
16
+ const r = parsePairingArgs(['list']) as Exclude<
17
+ ReturnType<typeof parsePairingArgs>,
18
+ { error: string }
19
+ >
20
+ expect(r.sub).toBe('list')
21
+ })
22
+
23
+ it('parses `list telegram` with platform filter', () => {
24
+ const r = parsePairingArgs(['list', 'telegram']) as Exclude<
25
+ ReturnType<typeof parsePairingArgs>,
26
+ { error: string }
27
+ >
28
+ expect(r.sub).toBe('list')
29
+ expect(r.platform).toBe('telegram')
30
+ })
31
+
32
+ it('parses `approve telegram ABCDEFGH`', () => {
33
+ const r = parsePairingArgs(['approve', 'telegram', 'ABCDEFGH']) as Exclude<
34
+ ReturnType<typeof parsePairingArgs>,
35
+ { error: string }
36
+ >
37
+ expect(r.sub).toBe('approve')
38
+ expect(r.platform).toBe('telegram')
39
+ expect(r.code).toBe('ABCDEFGH')
40
+ })
41
+
42
+ it('errors on `approve` without arguments', () => {
43
+ const r = parsePairingArgs(['approve'])
44
+ expect('error' in r).toBe(true)
45
+ })
46
+
47
+ it('errors on `approve telegram` without code', () => {
48
+ const r = parsePairingArgs(['approve', 'telegram'])
49
+ expect('error' in r).toBe(true)
50
+ })
51
+
52
+ it('parses `revoke telegram 12345`', () => {
53
+ const r = parsePairingArgs(['revoke', 'telegram', '12345']) as Exclude<
54
+ ReturnType<typeof parsePairingArgs>,
55
+ { error: string }
56
+ >
57
+ expect(r.sub).toBe('revoke')
58
+ expect(r.platform).toBe('telegram')
59
+ expect(r.userId).toBe('12345')
60
+ })
61
+
62
+ it('parses `clear-pending` and `clear-pending telegram`', () => {
63
+ const a = parsePairingArgs(['clear-pending']) as Exclude<
64
+ ReturnType<typeof parsePairingArgs>,
65
+ { error: string }
66
+ >
67
+ expect(a.sub).toBe('clear-pending')
68
+ expect(a.platform).toBeUndefined()
69
+ const b = parsePairingArgs(['clear-pending', 'telegram']) as Exclude<
70
+ ReturnType<typeof parsePairingArgs>,
71
+ { error: string }
72
+ >
73
+ expect(b.platform).toBe('telegram')
74
+ })
75
+
76
+ it('extracts --yes / -y flag', () => {
77
+ const r = parsePairingArgs(['revoke', 'telegram', '12345', '--yes']) as Exclude<
78
+ ReturnType<typeof parsePairingArgs>,
79
+ { error: string }
80
+ >
81
+ expect(r.yes).toBe(true)
82
+ const s = parsePairingArgs(['clear-pending', '-y']) as Exclude<
83
+ ReturnType<typeof parsePairingArgs>,
84
+ { error: string }
85
+ >
86
+ expect(s.yes).toBe(true)
87
+ })
88
+ })