nebula-treasury 0.1.0
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 +65 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_unlock.ts +66 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1293 -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/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/config/load.ts +35 -0
- package/src/config/render.ts +99 -0
- package/src/index.ts +153 -0
- package/src/ui/app.tsx +673 -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,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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { type NebulaConfig, agentPaths } from 'nebula-ai-core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load the user's `nebula.config.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Phase 6.6: canonical location is `~/.nebula/config.ts` (returned by
|
|
9
|
+
* `agentPaths.config`). If that file exists, it wins. Otherwise, fall back
|
|
10
|
+
* to walking upward from cwd looking for `nebula.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: NebulaConfig; path: string } | null> {
|
|
16
|
+
const canonical = agentPaths.config
|
|
17
|
+
if (existsSync(canonical)) {
|
|
18
|
+
const mod = (await import(canonical)) as { default: NebulaConfig }
|
|
19
|
+
if (!mod.default) throw new Error(`nebula 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, 'nebula.config.ts')
|
|
26
|
+
if (existsSync(candidate)) {
|
|
27
|
+
const mod = (await import(candidate)) as { default: NebulaConfig }
|
|
28
|
+
if (!mod.default) throw new Error(`nebula.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,99 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import type { NebulaConfig } from 'nebula-ai-core'
|
|
4
|
+
|
|
5
|
+
export interface RenderConfigOpts {
|
|
6
|
+
header?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialize an NebulaConfig into a `~/.nebula/config.ts` file body.
|
|
11
|
+
*
|
|
12
|
+
* Phase 6.6: the config lives at `~/.nebula/config.ts` which is outside any
|
|
13
|
+
* workspace, so it MUST NOT import `nebula-ai-core` (the import won't
|
|
14
|
+
* resolve from `~/.nebula/`). We emit a plain `export default { ... }` object;
|
|
15
|
+
* the runtime loader treats it as `NebulaConfig` directly.
|
|
16
|
+
*/
|
|
17
|
+
export function renderConfigTs(cfg: NebulaConfig, opts: RenderConfigOpts = {}): string {
|
|
18
|
+
const header = opts.header ?? ''
|
|
19
|
+
const operatorLine = cfg.operator ? ` operator: ${JSON.stringify(cfg.operator)},\n` : ''
|
|
20
|
+
// Emit either the operator's chosen sandbox config OR an annotated
|
|
21
|
+
// "OPTION 1/2/3" block so the operator can opt in by uncommenting. Default
|
|
22
|
+
// mode stays `none` (passthrough); documentation IS the UX.
|
|
23
|
+
const sandboxBlock = renderSandboxBlock(cfg.sandbox)
|
|
24
|
+
return `${header ? `${header}\n\n` : ''}export default {
|
|
25
|
+
identity: ${JSON.stringify(cfg.identity)},
|
|
26
|
+
network: ${JSON.stringify(cfg.network)},
|
|
27
|
+
storage: { network: ${JSON.stringify(cfg.storage.network)} },
|
|
28
|
+
brain: {
|
|
29
|
+
provider: ${JSON.stringify(cfg.brain.provider)},
|
|
30
|
+
model: ${JSON.stringify(cfg.brain.model)},
|
|
31
|
+
},
|
|
32
|
+
plugins: ${JSON.stringify(cfg.plugins)},
|
|
33
|
+
tools: ${JSON.stringify(cfg.tools)},
|
|
34
|
+
imports: { claudeCode: ${cfg.imports.claudeCode} },
|
|
35
|
+
${operatorLine}${sandboxBlock}}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderSandboxBlock(sandbox: NebulaConfig['sandbox']): string {
|
|
40
|
+
// Phase 11: deploy-target sandbox metadata (id/providerAddress/endpoint/
|
|
41
|
+
// snapshotName) OR Phase 9.5 limb-sandbox mode = anything non-default → emit
|
|
42
|
+
// verbatim. Only surface the doc-comment template when the operator has
|
|
43
|
+
// touched neither.
|
|
44
|
+
const hasNonDefaultLimbMode = sandbox?.mode && sandbox.mode !== 'none'
|
|
45
|
+
if (hasNonDefaultLimbMode) {
|
|
46
|
+
return ` sandbox: ${JSON.stringify(sandbox, null, 2).replace(/\n/g, '\n ')},\n`
|
|
47
|
+
}
|
|
48
|
+
// Fresh install: write the active default + commented examples for the
|
|
49
|
+
// other tiers. Operator can opt in by uncommenting and editing.
|
|
50
|
+
return ` sandbox: { mode: 'none' },
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Limb sandbox (Phase 9.5). Defense-in-depth beneath the permission floor:
|
|
53
|
+
// even when the modal grants 'allow session' or YOLO disables prompts,
|
|
54
|
+
// the sandbox profile/container blocks writes outside an allowlist.
|
|
55
|
+
// All shell.run / code.execute / shell.process_start spawns route through
|
|
56
|
+
// the chosen backend. fs.* and browser.* still run on the host (PathGuard
|
|
57
|
+
// applies). Override at runtime via NEBULA_SANDBOX_MODE=os|docker|none.
|
|
58
|
+
//
|
|
59
|
+
// OPTION 1: none (default): passthrough, fastest, permission floor only.
|
|
60
|
+
//
|
|
61
|
+
// OPTION 2: os (macOS sandbox-exec / seatbelt). Allows writes to agentDir +
|
|
62
|
+
// cwd + /tmp/nebula-* + /var/folders. Denies reads of ~/.ssh, ~/.aws,
|
|
63
|
+
// ~/Library/Keychains, ~/.config/gcloud. Linux bubblewrap pending.
|
|
64
|
+
// sandbox: { mode: 'os' },
|
|
65
|
+
//
|
|
66
|
+
// OPTION 3: docker (long-lived container per session), every shell-class
|
|
67
|
+
// spawn through 'docker exec'. Auto-detects Docker Desktop or Podman.
|
|
68
|
+
// Default image 'nikolaik/python-nodejs:python3.11-nodejs20' (matches
|
|
69
|
+
// hermes; ~700 MB; bash + python3 + node + npm + git on standard PATH).
|
|
70
|
+
// Override with 'oven/bun:1' (~250 MB) if you only need bun/ts.
|
|
71
|
+
// 'dockerMountWorkspace: true' bind-mounts your launch cwd into
|
|
72
|
+
// /workspace (off by default for max isolation). 'dockerRuntimePath'
|
|
73
|
+
// forces a specific runtime binary. Resource caps are unset by default
|
|
74
|
+
// (container competes fairly with host work). Set them to mirror hermes'
|
|
75
|
+
// production hardening: dockerCpu=1, dockerMemoryMb=5120, dockerDiskMb=
|
|
76
|
+
// 51200 (Linux+overlay2 only). dockerNoNetwork=true blocks all internet
|
|
77
|
+
// access from the container (max paranoia for code.execute).
|
|
78
|
+
// sandbox: {
|
|
79
|
+
// mode: 'docker',
|
|
80
|
+
// dockerImage: 'nikolaik/python-nodejs:python3.11-nodejs20',
|
|
81
|
+
// dockerMountWorkspace: false,
|
|
82
|
+
// // dockerRuntimePath: '/opt/homebrew/bin/podman',
|
|
83
|
+
// // dockerCpu: 1, // CPU cores cap
|
|
84
|
+
// // dockerMemoryMb: 5120, // 5 GB memory cap
|
|
85
|
+
// // dockerDiskMb: 51200, // 50 GB disk cap (Linux + overlay2 only)
|
|
86
|
+
// // dockerNoNetwork: true, // block all network from inside container
|
|
87
|
+
// },
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function writeConfigTs(
|
|
93
|
+
path: string,
|
|
94
|
+
cfg: NebulaConfig,
|
|
95
|
+
opts: RenderConfigOpts = {},
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
await mkdir(dirname(path), { recursive: true })
|
|
98
|
+
await writeFile(path, renderConfigTs(cfg, opts), 'utf8')
|
|
99
|
+
}
|