lyra-ai-agent 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/lyra +11 -0
- package/package.json +65 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1220 -0
- package/src/commands/demo.ts +177 -0
- package/src/commands/gateway-logs.ts +49 -0
- package/src/commands/gateway-run.ts +42 -0
- package/src/commands/gateway-start.ts +89 -0
- package/src/commands/gateway-status.ts +73 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/model-picker.ts +17 -0
- package/src/commands/init.ts +153 -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.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/telegram-remove.ts +45 -0
- package/src/commands/telegram-setup.ts +84 -0
- package/src/commands/telegram-status.ts +48 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/whoami.ts +62 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +99 -0
- package/src/index.ts +147 -0
- package/src/ui/app.tsx +719 -0
- package/src/ui/approval-summary.test.ts +132 -0
- package/src/ui/approval-summary.ts +28 -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 +176 -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/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 +125 -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/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/sui-runtime.ts +74 -0
- package/src/util/telegram-secrets.test.ts +104 -0
- package/src/util/telegram-secrets.ts +99 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sui runtime construction for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Lyra on Sui keys the agent off a single env secret (`LYRA_AGENT_KEY`, a
|
|
5
|
+
* `suiprivkey1…` bech32 string). That one Ed25519 keypair signs every PTB and
|
|
6
|
+
* pays gas; the deterministic policy (mirrored on-chain by the `lyra::policy`
|
|
7
|
+
* Move package) bounds what it may do. There is no operator wallet, keystore
|
|
8
|
+
* decrypt, or a Sui wallet dance — the agent IS the signer.
|
|
9
|
+
*
|
|
10
|
+
* `buildOnchainContext` assembles the `OnchainRuntimeContext` the
|
|
11
|
+
* lyra-plugin-onchain tools read via `(ctx as any).onchain`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
type OnchainRuntimeContext,
|
|
16
|
+
type SuiNetwork,
|
|
17
|
+
keypairFromSecret,
|
|
18
|
+
makeSuiClient,
|
|
19
|
+
policyFromEnv,
|
|
20
|
+
} from 'lyra-plugin-onchain'
|
|
21
|
+
|
|
22
|
+
/** Env var holding the agent's Sui secret key (`suiprivkey1…` or base64 seed). */
|
|
23
|
+
export const AGENT_KEY_ENV = 'LYRA_AGENT_KEY'
|
|
24
|
+
|
|
25
|
+
export interface SuiAgent {
|
|
26
|
+
keypair: ReturnType<typeof keypairFromSecret>
|
|
27
|
+
/** `keypair.toSuiAddress()` — 0x + 64 hex. */
|
|
28
|
+
address: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the agent keypair from `LYRA_AGENT_KEY`. Returns null when the env
|
|
33
|
+
* var is unset so callers can print a friendly "run lyra init" message instead
|
|
34
|
+
* of throwing a raw decode error.
|
|
35
|
+
*/
|
|
36
|
+
export function loadAgentFromEnv(): SuiAgent | null {
|
|
37
|
+
const secret = process.env[AGENT_KEY_ENV]
|
|
38
|
+
if (!secret || secret.trim().length === 0) return null
|
|
39
|
+
const keypair = keypairFromSecret(secret)
|
|
40
|
+
return { keypair, address: keypair.toSuiAddress() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuildOnchainOpts {
|
|
44
|
+
agent: SuiAgent
|
|
45
|
+
network: SuiNetwork
|
|
46
|
+
agentDir: string
|
|
47
|
+
packageId?: string | null
|
|
48
|
+
policyObjectId?: string | null
|
|
49
|
+
brainProvider?: string | null
|
|
50
|
+
brainModel?: string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the side-band `OnchainRuntimeContext` consumed by lyra-plugin-onchain.
|
|
55
|
+
* `packageId` / `policyObjectId` default to the env values the Move deploy
|
|
56
|
+
* wrote (`LYRA_PACKAGE_ID` / `LYRA_POLICY_OBJECT_ID`); the off-chain policy
|
|
57
|
+
* mirror comes from `policyFromEnv()` (the `LYRA_POLICY_*` vars).
|
|
58
|
+
*/
|
|
59
|
+
export function buildOnchainContext(opts: BuildOnchainOpts): OnchainRuntimeContext {
|
|
60
|
+
const packageId = opts.packageId ?? process.env.LYRA_PACKAGE_ID ?? undefined
|
|
61
|
+
const policyObjectId = opts.policyObjectId ?? process.env.LYRA_POLICY_OBJECT_ID ?? undefined
|
|
62
|
+
return {
|
|
63
|
+
client: makeSuiClient(opts.network),
|
|
64
|
+
keypair: opts.agent.keypair,
|
|
65
|
+
agentAddress: opts.agent.address,
|
|
66
|
+
network: opts.network,
|
|
67
|
+
policy: policyFromEnv(),
|
|
68
|
+
packageId: packageId || undefined,
|
|
69
|
+
policyObjectId: policyObjectId || undefined,
|
|
70
|
+
agentDir: opts.agentDir,
|
|
71
|
+
brainProvider: opts.brainProvider ?? null,
|
|
72
|
+
brainModel: opts.brainModel ?? null,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { looksLikeBotToken, parseAllowedUserIds, telegramSecretsFromEnv } from './telegram-secrets'
|
|
3
|
+
|
|
4
|
+
describe('looksLikeBotToken', () => {
|
|
5
|
+
it('accepts a real-shaped token', () => {
|
|
6
|
+
expect(looksLikeBotToken('8776805236:AAGgfvp2AwYBvDc3COYfjC9m8w2s0e4t4hw')).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('rejects empty / wrong delimiters', () => {
|
|
10
|
+
expect(looksLikeBotToken('')).toBe(false)
|
|
11
|
+
expect(looksLikeBotToken('8776805236-AAGgfvp2AwYBvDc3COYfjC9m8w2s0e4t4hw')).toBe(false)
|
|
12
|
+
expect(looksLikeBotToken('AAGgfvp2AwYBvDc3COYfjC9m8w2s0e4t4hw')).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('rejects too-short secret half', () => {
|
|
16
|
+
expect(looksLikeBotToken('1234567890:short')).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('trims surrounding whitespace before checking', () => {
|
|
20
|
+
expect(looksLikeBotToken(' 8731160904:AAH8FQ3CLrE8-WAfZtDeOTqmpVgOFLg8GyU\n')).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('parseAllowedUserIds', () => {
|
|
25
|
+
it('returns empty list for blank input', () => {
|
|
26
|
+
const r = parseAllowedUserIds('')
|
|
27
|
+
expect(r.ok).toBe(true)
|
|
28
|
+
if (r.ok) expect(r.ids).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('parses a comma-separated list', () => {
|
|
32
|
+
const r = parseAllowedUserIds('123, 456, 789')
|
|
33
|
+
expect(r.ok).toBe(true)
|
|
34
|
+
if (r.ok) expect(r.ids).toEqual([123, 456, 789])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('parses whitespace-only delimiters', () => {
|
|
38
|
+
const r = parseAllowedUserIds('123 456\t789')
|
|
39
|
+
expect(r.ok).toBe(true)
|
|
40
|
+
if (r.ok) expect(r.ids).toEqual([123, 456, 789])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('dedupes preserving first-seen order', () => {
|
|
44
|
+
const r = parseAllowedUserIds('123, 456, 123')
|
|
45
|
+
expect(r.ok).toBe(true)
|
|
46
|
+
if (r.ok) expect(r.ids).toEqual([123, 456])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rejects non-numeric ids', () => {
|
|
50
|
+
const r = parseAllowedUserIds('123, abc')
|
|
51
|
+
expect(r.ok).toBe(false)
|
|
52
|
+
if (!r.ok) expect(r.reason).toContain('abc')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('rejects negative ids', () => {
|
|
56
|
+
const r = parseAllowedUserIds('-123')
|
|
57
|
+
expect(r.ok).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects zero', () => {
|
|
61
|
+
const r = parseAllowedUserIds('0')
|
|
62
|
+
expect(r.ok).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('telegramSecretsFromEnv', () => {
|
|
67
|
+
const keys = ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'TELEGRAM_USERNAME'] as const
|
|
68
|
+
const saved: Record<string, string | undefined> = {}
|
|
69
|
+
for (const k of keys) saved[k] = process.env[k]
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
for (const k of keys) {
|
|
73
|
+
if (saved[k] === undefined) Reflect.deleteProperty(process.env, k)
|
|
74
|
+
else process.env[k] = saved[k]
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns null when no token is set', () => {
|
|
79
|
+
Reflect.deleteProperty(process.env, 'TELEGRAM_BOT_TOKEN')
|
|
80
|
+
expect(telegramSecretsFromEnv()).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('parses token + chat id + username', () => {
|
|
84
|
+
process.env.TELEGRAM_BOT_TOKEN = '8731160904:AAH8FQ3CLrE8-WAfZtDeOTqmpVgOFLg8GyU'
|
|
85
|
+
process.env.TELEGRAM_CHAT_ID = '1140813034'
|
|
86
|
+
process.env.TELEGRAM_USERNAME = 'lyra_test_bot'
|
|
87
|
+
expect(telegramSecretsFromEnv()).toEqual({
|
|
88
|
+
botToken: '8731160904:AAH8FQ3CLrE8-WAfZtDeOTqmpVgOFLg8GyU',
|
|
89
|
+
botUsername: 'lyra_test_bot',
|
|
90
|
+
allowedUserIds: [1140813034],
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('open access (empty allowlist) when no chat id', () => {
|
|
95
|
+
process.env.TELEGRAM_BOT_TOKEN = '8731160904:AAH8FQ3CLrE8-WAfZtDeOTqmpVgOFLg8GyU'
|
|
96
|
+
Reflect.deleteProperty(process.env, 'TELEGRAM_CHAT_ID')
|
|
97
|
+
Reflect.deleteProperty(process.env, 'TELEGRAM_USERNAME')
|
|
98
|
+
expect(telegramSecretsFromEnv()).toEqual({
|
|
99
|
+
botToken: '8731160904:AAH8FQ3CLrE8-WAfZtDeOTqmpVgOFLg8GyU',
|
|
100
|
+
botUsername: undefined,
|
|
101
|
+
allowedUserIds: [],
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bot helpers for the Sui CLI.
|
|
3
|
+
*
|
|
4
|
+
* On Sui, telegram is configured purely via environment variables — there is
|
|
5
|
+
* no operator wallet to sign-derive an encryption key, so the previous
|
|
6
|
+
* on-disk encrypted-blob flow is gone. The agent reads:
|
|
7
|
+
*
|
|
8
|
+
* TELEGRAM_BOT_TOKEN — from @BotFather (required to enable the gateway)
|
|
9
|
+
* TELEGRAM_CHAT_ID — optional sole allowed DM user (blank = open access)
|
|
10
|
+
* TELEGRAM_USERNAME — optional cached bot @username for nicer status output
|
|
11
|
+
*
|
|
12
|
+
* This module keeps the pure token/allowlist validators plus the Telegram
|
|
13
|
+
* `getMe` probe used by `lyra telegram setup` / `status`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface TelegramEnvSecrets {
|
|
17
|
+
botToken: string
|
|
18
|
+
botUsername?: string
|
|
19
|
+
allowedUserIds: number[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve telegram config from the environment, or null when no token is set.
|
|
24
|
+
*/
|
|
25
|
+
export function telegramSecretsFromEnv(): TelegramEnvSecrets | null {
|
|
26
|
+
const botToken = process.env.TELEGRAM_BOT_TOKEN
|
|
27
|
+
if (!botToken) return null
|
|
28
|
+
const chatId = process.env.TELEGRAM_CHAT_ID
|
|
29
|
+
return {
|
|
30
|
+
botToken,
|
|
31
|
+
botUsername: process.env.TELEGRAM_USERNAME,
|
|
32
|
+
allowedUserIds: chatId ? [Number(chatId)] : [],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BOT_TOKEN_RE = /^\d{6,15}:[A-Za-z0-9_-]{30,}$/
|
|
37
|
+
|
|
38
|
+
export function looksLikeBotToken(s: string): boolean {
|
|
39
|
+
return BOT_TOKEN_RE.test(s.trim())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ValidatedBotInfo {
|
|
43
|
+
id: number
|
|
44
|
+
username: string
|
|
45
|
+
firstName: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Telegram Bot API getMe — cheap, free, no message side-effect. Used by
|
|
50
|
+
* `lyra telegram setup` to validate the token before persisting it AND by
|
|
51
|
+
* `lyra telegram status` to confirm the stored token still works.
|
|
52
|
+
*
|
|
53
|
+
* Throws on non-200 / `ok: false` with a clean error message; caller wraps
|
|
54
|
+
* the throw in a clack spinner.stop().
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchBotInfo(
|
|
57
|
+
botToken: string,
|
|
58
|
+
opts?: { signal?: AbortSignal },
|
|
59
|
+
): Promise<ValidatedBotInfo> {
|
|
60
|
+
const url = `https://api.telegram.org/bot${encodeURIComponent(botToken)}/getMe`
|
|
61
|
+
const res = await fetch(url, { signal: opts?.signal })
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`getMe HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`)
|
|
64
|
+
}
|
|
65
|
+
const body = (await res.json()) as {
|
|
66
|
+
ok: boolean
|
|
67
|
+
description?: string
|
|
68
|
+
result?: { id: number; username?: string; first_name?: string }
|
|
69
|
+
}
|
|
70
|
+
if (!body.ok || !body.result) {
|
|
71
|
+
throw new Error(`getMe rejected: ${body.description ?? 'unknown error'}`)
|
|
72
|
+
}
|
|
73
|
+
if (!body.result.username) throw new Error('bot has no username; create one in @BotFather')
|
|
74
|
+
return {
|
|
75
|
+
id: body.result.id,
|
|
76
|
+
username: body.result.username,
|
|
77
|
+
firstName: body.result.first_name ?? body.result.username,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseAllowedUserIds(
|
|
82
|
+
input: string,
|
|
83
|
+
): { ok: true; ids: number[] } | { ok: false; reason: string } {
|
|
84
|
+
const trimmed = input.trim()
|
|
85
|
+
if (trimmed.length === 0) return { ok: true, ids: [] }
|
|
86
|
+
const parts = trimmed
|
|
87
|
+
.split(/[,\s]+/)
|
|
88
|
+
.map(p => p.trim())
|
|
89
|
+
.filter(p => p.length > 0)
|
|
90
|
+
const ids: number[] = []
|
|
91
|
+
for (const p of parts) {
|
|
92
|
+
if (!/^\d+$/.test(p)) return { ok: false, reason: `not a numeric id: "${p}"` }
|
|
93
|
+
const n = Number(p)
|
|
94
|
+
if (!Number.isFinite(n) || n <= 0) return { ok: false, reason: `not a positive id: "${p}"` }
|
|
95
|
+
ids.push(n)
|
|
96
|
+
}
|
|
97
|
+
// Dedupe, preserve first-seen order.
|
|
98
|
+
return { ok: true, ids: [...new Set(ids)] }
|
|
99
|
+
}
|