nebula-ai-agent 0.3.1
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 +39 -0
- package/bin/nebula +11 -0
- package/package.json +50 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_unlock.ts +66 -0
- package/src/commands/agent-wallet.ts +90 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1308 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +49 -0
- package/src/commands/gateway-run.ts +42 -0
- package/src/commands/gateway-start.ts +216 -0
- package/src/commands/gateway-status.ts +90 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/identity.ts +178 -0
- package/src/commands/init/cost.ts +40 -0
- package/src/commands/init/funding-gate.ts +64 -0
- package/src/commands/init/model-picker.ts +25 -0
- package/src/commands/init/operator-picker.ts +233 -0
- package/src/commands/init/telegram-step.ts +245 -0
- package/src/commands/init/wizard-state.ts +94 -0
- package/src/commands/init.ts +439 -0
- package/src/commands/login.ts +86 -0
- package/src/commands/logs.ts +37 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +65 -0
- package/src/commands/pairing-clear.ts +39 -0
- package/src/commands/pairing-list.ts +55 -0
- package/src/commands/pairing-revoke.ts +49 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/status.ts +44 -0
- package/src/commands/telegram-remove.ts +62 -0
- package/src/commands/telegram-setup.ts +64 -0
- package/src/commands/telegram-status.ts +87 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/trust.ts +196 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.ts +99 -0
- package/src/index.ts +184 -0
- package/src/profile/crypto.ts +68 -0
- package/src/profile/derive.ts +25 -0
- package/src/profile/store.ts +86 -0
- package/src/profile/unlock.ts +29 -0
- package/src/ui/app.tsx +719 -0
- package/src/ui/approval-summary.ts +32 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.ts +181 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.ts +125 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.ts +218 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
2
|
+
import { PairingStore, agentPaths, placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { findAndLoadConfig } from '../config/load'
|
|
4
|
+
|
|
5
|
+
export interface RunPairingClearOpts {
|
|
6
|
+
platform?: string
|
|
7
|
+
yes?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runPairingClear(opts: RunPairingClearOpts): Promise<void> {
|
|
11
|
+
const loaded = await findAndLoadConfig()
|
|
12
|
+
if (!loaded) {
|
|
13
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
const { config } = loaded
|
|
17
|
+
if (!config.identity.agent) {
|
|
18
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
22
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
23
|
+
const store = new PairingStore({ dir })
|
|
24
|
+
|
|
25
|
+
if (!opts.yes) {
|
|
26
|
+
const target = opts.platform ? `${opts.platform} pending` : 'ALL pending pairing codes'
|
|
27
|
+
const ok = await confirm({
|
|
28
|
+
message: `Clear ${target}?`,
|
|
29
|
+
initialValue: false,
|
|
30
|
+
})
|
|
31
|
+
if (isCancel(ok) || !ok) {
|
|
32
|
+
console.log('Aborted.')
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const count = store.clearPending(opts.platform)
|
|
38
|
+
console.log(`✓ Cleared ${count} pending pairing code${count === 1 ? '' : 's'}`)
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PairingStore, agentPaths, placeholderAgentId } from 'nebula-ai-core'
|
|
2
|
+
import { findAndLoadConfig } from '../config/load'
|
|
3
|
+
|
|
4
|
+
export interface RunPairingListOpts {
|
|
5
|
+
platform?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runPairingList(opts: RunPairingListOpts): Promise<void> {
|
|
9
|
+
const store = await openPairingStore()
|
|
10
|
+
if (!store) return
|
|
11
|
+
|
|
12
|
+
const pending = store.listPending(opts.platform)
|
|
13
|
+
const approved = store.listApproved(opts.platform)
|
|
14
|
+
|
|
15
|
+
const pendingTitle = opts.platform ? `Pending (${opts.platform})` : 'Pending'
|
|
16
|
+
console.log(`\n${pendingTitle} (1h TTL):`)
|
|
17
|
+
if (pending.length === 0) {
|
|
18
|
+
console.log(' (none)')
|
|
19
|
+
} else {
|
|
20
|
+
for (const p of pending) {
|
|
21
|
+
const userLabel = p.userName ? `@${p.userName}` : '(unknown)'
|
|
22
|
+
const idLabel = `id=${p.userId}`
|
|
23
|
+
console.log(` [${p.platform}] ${p.code} ${userLabel} ${idLabel} age=${p.ageMinutes}m`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const approvedTitle = opts.platform ? `Approved (${opts.platform})` : 'Approved'
|
|
28
|
+
console.log(`\n${approvedTitle}:`)
|
|
29
|
+
if (approved.length === 0) {
|
|
30
|
+
console.log(' (none)')
|
|
31
|
+
} else {
|
|
32
|
+
for (const a of approved) {
|
|
33
|
+
const userLabel = a.userName ? `@${a.userName}` : '(unknown)'
|
|
34
|
+
const idLabel = `id=${a.userId}`
|
|
35
|
+
console.log(` [${a.platform}] ${userLabel} ${idLabel}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function openPairingStore(): Promise<PairingStore | null> {
|
|
42
|
+
const loaded = await findAndLoadConfig()
|
|
43
|
+
if (!loaded) {
|
|
44
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
const { config } = loaded
|
|
48
|
+
if (!config.identity.agent) {
|
|
49
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
53
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
54
|
+
return new PairingStore({ dir })
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
2
|
+
import { PairingStore, agentPaths, placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { findAndLoadConfig } from '../config/load'
|
|
4
|
+
|
|
5
|
+
export interface RunPairingRevokeOpts {
|
|
6
|
+
platform: string
|
|
7
|
+
userId: string
|
|
8
|
+
yes?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function runPairingRevoke(opts: RunPairingRevokeOpts): Promise<void> {
|
|
12
|
+
const loaded = await findAndLoadConfig()
|
|
13
|
+
if (!loaded) {
|
|
14
|
+
console.error('No nebula.config.ts found. Run `nebula init` first.')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
const { config } = loaded
|
|
18
|
+
if (!config.identity.agent) {
|
|
19
|
+
console.error('Config has no agent. Run `nebula init` first.')
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
23
|
+
const dir = agentPaths.agent(agentId).pairingDir
|
|
24
|
+
const store = new PairingStore({ dir })
|
|
25
|
+
|
|
26
|
+
if (!store.isApproved(opts.platform, opts.userId)) {
|
|
27
|
+
console.error(`User ${opts.userId} is not on the ${opts.platform} approved list.`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!opts.yes) {
|
|
32
|
+
const ok = await confirm({
|
|
33
|
+
message: `Revoke ${opts.platform} access for user id ${opts.userId}?`,
|
|
34
|
+
initialValue: false,
|
|
35
|
+
})
|
|
36
|
+
if (isCancel(ok) || !ok) {
|
|
37
|
+
console.log('Aborted.')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const removed = store.revoke(opts.platform, opts.userId)
|
|
43
|
+
if (removed) {
|
|
44
|
+
console.log(`✓ Revoked: ${opts.platform} id=${opts.userId}`)
|
|
45
|
+
} else {
|
|
46
|
+
console.error('Revoke failed (concurrent removal?)')
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nebula pairing <subcommand>` — argv dispatcher for the DM pairing flow.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* list show pending codes + approved users
|
|
6
|
+
* approve <platform> <code> approve a pairing code (case-insensitive)
|
|
7
|
+
* revoke <platform> <userId> revoke an approved user
|
|
8
|
+
* clear-pending [platform] drop all pending codes
|
|
9
|
+
*
|
|
10
|
+
* Platform is `telegram` for Phase 12. Future platforms (discord, slack) will
|
|
11
|
+
* reuse the same command surface.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PairingArgs {
|
|
15
|
+
sub: 'list' | 'approve' | 'revoke' | 'clear-pending'
|
|
16
|
+
platform?: string
|
|
17
|
+
code?: string
|
|
18
|
+
userId?: string
|
|
19
|
+
yes?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VALID_SUBS = ['list', 'approve', 'revoke', 'clear-pending'] as const
|
|
23
|
+
|
|
24
|
+
export type PairingParseResult = PairingArgs | { error: string }
|
|
25
|
+
|
|
26
|
+
export function parsePairingArgs(argv: string[]): PairingParseResult {
|
|
27
|
+
const sub = argv[0]
|
|
28
|
+
if (!sub) {
|
|
29
|
+
return {
|
|
30
|
+
error:
|
|
31
|
+
'usage: nebula pairing <list | approve <platform> <code> | revoke <platform> <userId> | clear-pending [platform]>',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!(VALID_SUBS as readonly string[]).includes(sub)) {
|
|
35
|
+
return { error: `unknown subcommand '${sub}' (expected: ${VALID_SUBS.join(' | ')})` }
|
|
36
|
+
}
|
|
37
|
+
const positional = argv.slice(1).filter(a => !a.startsWith('-'))
|
|
38
|
+
const yes = argv.includes('--yes') || argv.includes('-y')
|
|
39
|
+
|
|
40
|
+
if (sub === 'approve') {
|
|
41
|
+
if (positional.length < 2) {
|
|
42
|
+
return { error: 'usage: nebula pairing approve <platform> <code>' }
|
|
43
|
+
}
|
|
44
|
+
return { sub: 'approve', platform: positional[0], code: positional[1], yes }
|
|
45
|
+
}
|
|
46
|
+
if (sub === 'revoke') {
|
|
47
|
+
if (positional.length < 2) {
|
|
48
|
+
return { error: 'usage: nebula pairing revoke <platform> <userId>' }
|
|
49
|
+
}
|
|
50
|
+
return { sub: 'revoke', platform: positional[0], userId: positional[1], yes }
|
|
51
|
+
}
|
|
52
|
+
if (sub === 'clear-pending') {
|
|
53
|
+
return { sub: 'clear-pending', platform: positional[0], yes }
|
|
54
|
+
}
|
|
55
|
+
return { sub: 'list', platform: positional[0], yes }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runPairing(args: PairingArgs): Promise<void> {
|
|
59
|
+
switch (args.sub) {
|
|
60
|
+
case 'list': {
|
|
61
|
+
const { runPairingList } = await import('./pairing-list')
|
|
62
|
+
await runPairingList({ platform: args.platform })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
case 'approve': {
|
|
66
|
+
const { runPairingApprove } = await import('./pairing-approve')
|
|
67
|
+
await runPairingApprove({ platform: args.platform!, code: args.code! })
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
case 'revoke': {
|
|
71
|
+
const { runPairingRevoke } = await import('./pairing-revoke')
|
|
72
|
+
await runPairingRevoke({ platform: args.platform!, userId: args.userId!, yes: args.yes })
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
case 'clear-pending': {
|
|
76
|
+
const { runPairingClear } = await import('./pairing-clear')
|
|
77
|
+
await runPairingClear({ platform: args.platform, yes: args.yes })
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { NETWORK_CHAIN_ID, NETWORK_RPC, agentPaths } from 'nebula-ai-core'
|
|
3
|
+
import { http, createPublicClient } from 'viem'
|
|
4
|
+
import { findAndLoadConfig } from '../config/load'
|
|
5
|
+
import { listAgentIds } from './_agents'
|
|
6
|
+
|
|
7
|
+
export async function runStatus(opts?: { cwd?: string }): Promise<void> {
|
|
8
|
+
const cwd = opts?.cwd ?? process.cwd()
|
|
9
|
+
const found = await findAndLoadConfig(cwd)
|
|
10
|
+
if (!found) {
|
|
11
|
+
console.log('No nebula.config.ts found. Run `nebula init` first.')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
const { config, path } = found
|
|
15
|
+
console.log(`config ${path}`)
|
|
16
|
+
console.log(`network ${config.network} (chain ${NETWORK_CHAIN_ID[config.network]})`)
|
|
17
|
+
console.log(`rpc ${NETWORK_RPC[config.network]}`)
|
|
18
|
+
console.log(`plugins ${config.plugins.join(', ')}`)
|
|
19
|
+
if (config.identity.operator) console.log(`operator ${config.identity.operator}`)
|
|
20
|
+
if (config.identity.agent) console.log(`agent EOA ${config.identity.agent}`)
|
|
21
|
+
console.log(`brain ${config.brain.provider ?? '(not picked)'}`)
|
|
22
|
+
|
|
23
|
+
const ids = await listAgentIds()
|
|
24
|
+
if (ids.length === 0) {
|
|
25
|
+
console.log('\nNo agents found in ~/.nebula/agents. Re-run `nebula init`.')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const client = createPublicClient({
|
|
30
|
+
transport: http(NETWORK_RPC[config.network]),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
console.log('')
|
|
35
|
+
console.log(`agent ${id}`)
|
|
36
|
+
console.log(`dir ${agentPaths.agent(id).dir}`)
|
|
37
|
+
const activityPath = agentPaths.agent(id).activityLog
|
|
38
|
+
if (existsSync(activityPath)) {
|
|
39
|
+
const sz = statSync(activityPath).size
|
|
40
|
+
console.log(`activity ${sz} bytes`)
|
|
41
|
+
}
|
|
42
|
+
void client
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, note, outro } from '@clack/prompts'
|
|
2
|
+
import { placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { findAndLoadConfig } from '../config/load'
|
|
4
|
+
import { writeConfigTs } from '../config/render'
|
|
5
|
+
import {
|
|
6
|
+
removeTelegramSecrets,
|
|
7
|
+
telegramSecretsExist,
|
|
8
|
+
telegramSecretsPath,
|
|
9
|
+
} from '../util/telegram-secrets'
|
|
10
|
+
|
|
11
|
+
export interface TelegramRemoveOpts {
|
|
12
|
+
yes?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runTelegramRemove(opts: TelegramRemoveOpts = {}): Promise<void> {
|
|
16
|
+
intro('nebula telegram remove')
|
|
17
|
+
|
|
18
|
+
const loaded = await findAndLoadConfig()
|
|
19
|
+
if (!loaded) {
|
|
20
|
+
cancel('No nebula.config.ts found. Run `nebula init` first.')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const { config, path: configPath } = loaded
|
|
24
|
+
if (!config.identity.agent) {
|
|
25
|
+
cancel('Config has no agent. Run `nebula init` first.')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const agentId = placeholderAgentId(config.identity.agent)
|
|
30
|
+
|
|
31
|
+
if (!telegramSecretsExist(agentId)) {
|
|
32
|
+
note('Nothing to remove.')
|
|
33
|
+
outro('not configured')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!opts.yes) {
|
|
38
|
+
const ok = (await confirm({
|
|
39
|
+
message: `Delete encrypted telegram-secrets for ${agentId}?`,
|
|
40
|
+
initialValue: false,
|
|
41
|
+
})) as boolean | symbol
|
|
42
|
+
if (isCancel(ok) || !ok) {
|
|
43
|
+
cancel('Aborted.')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await removeTelegramSecrets(agentId)
|
|
49
|
+
|
|
50
|
+
const plugins = (config.plugins ?? []).filter(p => p !== 'telegram')
|
|
51
|
+
if (plugins.length !== (config.plugins ?? []).length) {
|
|
52
|
+
const updated = { ...config, plugins }
|
|
53
|
+
await writeConfigTs(configPath, updated)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
note(
|
|
57
|
+
`Local blob deleted: ${telegramSecretsPath(agentId)}\nThe bot token at @BotFather is STILL VALID. To fully revoke, run /token in\n@BotFather and pick "Revoke" for this bot.`,
|
|
58
|
+
'reminder',
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
outro('telegram removed')
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cancel, intro, note, outro } from '@clack/prompts'
|
|
2
|
+
import { placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { type Address, getAddress } from 'viem'
|
|
4
|
+
import { findAndLoadConfig } from '../config/load'
|
|
5
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
6
|
+
import { runTelegramStep } from './init/telegram-step'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `nebula telegram setup` — standalone entry. Loads the operator wallet, then
|
|
10
|
+
* delegates to `runTelegramStep` (the same helper bundled into `nebula init`'s
|
|
11
|
+
* Phase E). Owns its own intro/outro framing.
|
|
12
|
+
*/
|
|
13
|
+
export async function runTelegramSetup(): Promise<void> {
|
|
14
|
+
intro('nebula telegram setup')
|
|
15
|
+
|
|
16
|
+
const loaded = await findAndLoadConfig()
|
|
17
|
+
if (!loaded) {
|
|
18
|
+
cancel('No nebula.config.ts found. Run `nebula init` first.')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const { config, path: configPath } = loaded
|
|
22
|
+
if (!config.identity.agent) {
|
|
23
|
+
cancel('Config has no agent. Run `nebula init` first.')
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const agentAddress = getAddress(config.identity.agent) as Address
|
|
28
|
+
const agentId = placeholderAgentId(agentAddress)
|
|
29
|
+
|
|
30
|
+
const operator = await loadOrPickOperatorSigner({
|
|
31
|
+
network: config.network,
|
|
32
|
+
hint: config.operator,
|
|
33
|
+
})
|
|
34
|
+
if (!operator) {
|
|
35
|
+
cancel('No operator wallet available; cannot encrypt secrets.')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let result: Awaited<ReturnType<typeof runTelegramStep>>
|
|
40
|
+
try {
|
|
41
|
+
result = await runTelegramStep({
|
|
42
|
+
signer: operator,
|
|
43
|
+
agentId,
|
|
44
|
+
agentAddress,
|
|
45
|
+
configPath,
|
|
46
|
+
config,
|
|
47
|
+
network: config.network,
|
|
48
|
+
})
|
|
49
|
+
} finally {
|
|
50
|
+
await operator.close?.()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!result.configured) {
|
|
54
|
+
cancel(result.cancelled ? 'Aborted.' : 'Setup failed.')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
note(
|
|
59
|
+
`Open https://t.me/${result.botUsername} in Telegram and send any message.\nThen run \`nebula\` (or \`nebula gateway start\`) to bring the agent online.`,
|
|
60
|
+
'next step',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
outro(`telegram setup complete (@${result.botUsername}, mode: ${result.modeUsed})`)
|
|
64
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { cancel, intro, log, outro, spinner } from '@clack/prompts'
|
|
2
|
+
import { placeholderAgentId } from 'nebula-ai-core'
|
|
3
|
+
import { type Address, getAddress } from 'viem'
|
|
4
|
+
import { findAndLoadConfig } from '../config/load'
|
|
5
|
+
import {
|
|
6
|
+
fetchBotInfo,
|
|
7
|
+
loadTelegramSecrets,
|
|
8
|
+
telegramSecretsExist,
|
|
9
|
+
telegramSecretsPath,
|
|
10
|
+
} from '../util/telegram-secrets'
|
|
11
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
12
|
+
|
|
13
|
+
export async function runTelegramStatus(): Promise<void> {
|
|
14
|
+
intro('nebula telegram status')
|
|
15
|
+
|
|
16
|
+
const loaded = await findAndLoadConfig()
|
|
17
|
+
if (!loaded) {
|
|
18
|
+
cancel('No nebula.config.ts found. Run `nebula init` first.')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const { config } = loaded
|
|
22
|
+
if (!config.identity.agent) {
|
|
23
|
+
cancel('Config has no agent. Run `nebula init` first.')
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const agentAddress = getAddress(config.identity.agent) as Address
|
|
28
|
+
const agentId = placeholderAgentId(agentAddress)
|
|
29
|
+
const path = telegramSecretsPath(agentId)
|
|
30
|
+
|
|
31
|
+
if (!telegramSecretsExist(agentId)) {
|
|
32
|
+
log.warn(`No telegram secrets stored for ${agentId}.`)
|
|
33
|
+
log.info(`Expected at: ${path}\nRun \`nebula telegram setup\` to configure.`)
|
|
34
|
+
outro('not configured')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const operator = await loadOrPickOperatorSigner({
|
|
39
|
+
network: config.network,
|
|
40
|
+
hint: config.operator,
|
|
41
|
+
})
|
|
42
|
+
if (!operator) {
|
|
43
|
+
cancel('No operator wallet available; cannot decrypt secrets.')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sLoad = spinner()
|
|
48
|
+
sLoad.start('Decrypting telegram secrets via operator wallet')
|
|
49
|
+
let secrets: Awaited<ReturnType<typeof loadTelegramSecrets>>
|
|
50
|
+
try {
|
|
51
|
+
secrets = await loadTelegramSecrets({ signer: operator, agentAddress, agentId })
|
|
52
|
+
sLoad.stop('decrypted')
|
|
53
|
+
} catch (e) {
|
|
54
|
+
sLoad.stop(`decrypt failed: ${(e as Error).message.slice(0, 200)}`)
|
|
55
|
+
await operator.close?.()
|
|
56
|
+
return
|
|
57
|
+
} finally {
|
|
58
|
+
await operator.close?.()
|
|
59
|
+
}
|
|
60
|
+
if (!secrets) {
|
|
61
|
+
cancel('Empty telegram-secrets blob.')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sPing = spinner()
|
|
66
|
+
sPing.start('Pinging Telegram getMe')
|
|
67
|
+
try {
|
|
68
|
+
const info = await fetchBotInfo(secrets.botToken)
|
|
69
|
+
sPing.stop(`bot ok: @${info.username} (id ${info.id})`)
|
|
70
|
+
} catch (e) {
|
|
71
|
+
sPing.stop(`getMe failed: ${(e as Error).message.slice(0, 200)}`)
|
|
72
|
+
log.warn('Token may have been revoked at @BotFather. Re-run `nebula telegram setup`.')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
log.info(
|
|
77
|
+
[
|
|
78
|
+
`path ${path}`,
|
|
79
|
+
`bot username @${secrets.botUsername ?? '(unknown)'}`,
|
|
80
|
+
`bot id ${secrets.botId ?? '(unknown)'}`,
|
|
81
|
+
`allowed user ids ${secrets.allowedUserIds.length === 0 ? '(open access)' : secrets.allowedUserIds.join(', ')}`,
|
|
82
|
+
`plugin enabled ${(config.plugins ?? []).includes('telegram') ? 'yes' : 'no — add `telegram` to plugins'}`,
|
|
83
|
+
].join('\n'),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
outro(`telegram configured for ${agentId}`)
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nebula telegram <subcommand>` — argv dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* setup interactive wizard: validate token, encrypt + persist locally
|
|
6
|
+
* status confirm token still valid + show stored config
|
|
7
|
+
* remove delete the encrypted local blob (does NOT revoke at @BotFather)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface TelegramArgs {
|
|
11
|
+
sub: 'setup' | 'status' | 'remove'
|
|
12
|
+
yes?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VALID_SUBS = ['setup', 'status', 'remove'] as const
|
|
16
|
+
|
|
17
|
+
export function parseTelegramArgs(argv: string[]): TelegramArgs | { error: string } {
|
|
18
|
+
const sub = argv[0]
|
|
19
|
+
if (!sub) return { error: 'usage: nebula telegram <setup | status | remove>' }
|
|
20
|
+
const valid = (VALID_SUBS as readonly string[]).includes(sub)
|
|
21
|
+
if (!valid) return { error: `unknown subcommand '${sub}' (expected: ${VALID_SUBS.join(' | ')})` }
|
|
22
|
+
const yes = argv.includes('--yes') || argv.includes('-y')
|
|
23
|
+
return { sub: sub as TelegramArgs['sub'], yes }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runTelegram(args: TelegramArgs): Promise<void> {
|
|
27
|
+
switch (args.sub) {
|
|
28
|
+
case 'setup': {
|
|
29
|
+
const { runTelegramSetup } = await import('./telegram-setup')
|
|
30
|
+
await runTelegramSetup()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
case 'status': {
|
|
34
|
+
const { runTelegramStatus } = await import('./telegram-status')
|
|
35
|
+
await runTelegramStatus()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
case 'remove': {
|
|
39
|
+
const { runTelegramRemove } = await import('./telegram-remove')
|
|
40
|
+
await runTelegramRemove({ yes: args.yes })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|