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