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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra demo` — walk the guarded pipeline end to end using the lyra-plugin-onchain
|
|
3
|
+
* tools and the deterministic policy engine.
|
|
4
|
+
*
|
|
5
|
+
* Steps:
|
|
6
|
+
* 1. policy.show — print the active fund-control policy.
|
|
7
|
+
* 2. blocked over-cap send — evaluatePolicy on an amount above the per-tx cap;
|
|
8
|
+
* demonstrates a BLOCKED unsafe action (no network).
|
|
9
|
+
* 3. policy.create — publish an on-chain lyra::policy AgentPolicy (--yes)
|
|
10
|
+
* 4. sui.send — a small in-cap transfer to self (--yes)
|
|
11
|
+
* 5. walrus.store — store the run receipt durably on Walrus (--yes)
|
|
12
|
+
*
|
|
13
|
+
* Steps 3–5 move value / write on-chain, so they run ONLY with `--yes`. The
|
|
14
|
+
* default run is read-only + deterministic: it proves the policy blocks an
|
|
15
|
+
* over-cap action and explains what the write path would do.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { formatSui, getSuiBalanceMist } from 'lyra-core'
|
|
19
|
+
import onchainPlugin, {
|
|
20
|
+
type OnchainRuntimeContext,
|
|
21
|
+
type SuiPolicy,
|
|
22
|
+
evaluatePolicy,
|
|
23
|
+
makeSuiClient,
|
|
24
|
+
policyFromEnv,
|
|
25
|
+
suiToMist,
|
|
26
|
+
} from 'lyra-plugin-onchain'
|
|
27
|
+
import { findAndLoadConfig } from '../config/load'
|
|
28
|
+
import { buildOnchainContext, loadAgentFromEnv } from '../util/sui-runtime'
|
|
29
|
+
|
|
30
|
+
const SUI_TYPE = '0x2::sui::SUI'
|
|
31
|
+
|
|
32
|
+
export interface DemoOpts {
|
|
33
|
+
yes?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DemoTool {
|
|
37
|
+
name: string
|
|
38
|
+
handler: (args: unknown) => Promise<unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Drive the plugin's own `register` against a minimal collector so we get the
|
|
43
|
+
* exact ToolDefs the chat agent uses — without reaching past the public API.
|
|
44
|
+
*/
|
|
45
|
+
function collectOnchainTools(onchain: OnchainRuntimeContext): Map<string, DemoTool> {
|
|
46
|
+
const tools = new Map<string, DemoTool>()
|
|
47
|
+
const ctx = {
|
|
48
|
+
onchain,
|
|
49
|
+
registerTool: (t: DemoTool) => tools.set(t.name, t),
|
|
50
|
+
}
|
|
51
|
+
onchainPlugin.register(ctx as unknown as Parameters<typeof onchainPlugin.register>[0])
|
|
52
|
+
return tools
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runDemo(opts: DemoOpts = {}): Promise<void> {
|
|
56
|
+
const found = await findAndLoadConfig()
|
|
57
|
+
if (!found) {
|
|
58
|
+
console.log('No lyra.config.ts found. Run `lyra init` first.')
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
const { config } = found
|
|
62
|
+
|
|
63
|
+
const agent = loadAgentFromEnv()
|
|
64
|
+
if (!agent) {
|
|
65
|
+
console.log('No LYRA_AGENT_KEY set. Run `lyra init` first.')
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`lyra demo — agent ${agent.address} on ${config.network}\n`)
|
|
70
|
+
|
|
71
|
+
const onchain = buildOnchainContext({
|
|
72
|
+
agent,
|
|
73
|
+
network: config.network,
|
|
74
|
+
agentDir: found.path,
|
|
75
|
+
brainProvider: config.brain.provider,
|
|
76
|
+
brainModel: config.brain.model,
|
|
77
|
+
})
|
|
78
|
+
const tools = collectOnchainTools(onchain)
|
|
79
|
+
|
|
80
|
+
// ── Step 1: policy.show ────────────────────────────────────────────────
|
|
81
|
+
console.log('1) policy.show')
|
|
82
|
+
const policy = policyFromEnv()
|
|
83
|
+
if (!policy) {
|
|
84
|
+
console.log(
|
|
85
|
+
' (no LYRA_POLICY_* configured — set LYRA_POLICY_MAX_PER_TX_SUI to bound the agent)\n',
|
|
86
|
+
)
|
|
87
|
+
} else {
|
|
88
|
+
console.log(` ${describePolicy(policy)}\n`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Step 2: blocked over-cap send (deterministic, no network) ──────────
|
|
92
|
+
console.log('2) blocked over-cap send (policy enforcement)')
|
|
93
|
+
if (policy?.maxMistPerTx !== undefined) {
|
|
94
|
+
const overCap = policy.maxMistPerTx + (suiToMist('1') ?? 0n) // 1 SUI over the cap
|
|
95
|
+
const verdict = evaluatePolicy(
|
|
96
|
+
{
|
|
97
|
+
kind: 'transfer',
|
|
98
|
+
coinType: SUI_TYPE,
|
|
99
|
+
amountMist: overCap,
|
|
100
|
+
to: agent.address,
|
|
101
|
+
protocol: 'transfer',
|
|
102
|
+
},
|
|
103
|
+
policy,
|
|
104
|
+
)
|
|
105
|
+
if (!verdict.allowed) {
|
|
106
|
+
console.log(
|
|
107
|
+
` send ${formatSui(overCap)} SUI → BLOCKED ✓ (${verdict.violations.join('; ')})\n`,
|
|
108
|
+
)
|
|
109
|
+
} else {
|
|
110
|
+
console.log(` send ${formatSui(overCap)} SUI was allowed (cap not enforced?)\n`)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
console.log(' skipped: no per-tx cap configured (set LYRA_POLICY_MAX_PER_TX_SUI)\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Live balance (read-only) ────────────────────────────────────────────
|
|
117
|
+
try {
|
|
118
|
+
const client = makeSuiClient(config.network)
|
|
119
|
+
const mist = await getSuiBalanceMist(client, agent.address)
|
|
120
|
+
console.log(` agent SUI balance: ${formatSui(mist)} SUI\n`)
|
|
121
|
+
} catch {
|
|
122
|
+
// RPC unavailable — fine, the rest is descriptive.
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Steps 3–5: write path ────────────────────────────────────────────────
|
|
126
|
+
if (!opts.yes) {
|
|
127
|
+
console.log('3-5) write path (policy.create → sui.send → walrus.store)')
|
|
128
|
+
console.log(' dry run. Re-run `lyra demo --yes` to execute the on-chain steps:')
|
|
129
|
+
console.log(' • policy.create — publish a shared lyra::policy AgentPolicy')
|
|
130
|
+
console.log(
|
|
131
|
+
' • sui.send — a small in-cap transfer to self (policy + simulate + execute)',
|
|
132
|
+
)
|
|
133
|
+
console.log(' • walrus.store — store the run receipt durably on Walrus')
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('3) policy.create')
|
|
138
|
+
await runTool(tools, 'policy.create', { budgetSui: '1', maxPerTxSui: '0.1', maxSlippageBps: 100 })
|
|
139
|
+
|
|
140
|
+
console.log('4) sui.send (in-cap, to self)')
|
|
141
|
+
await runTool(tools, 'sui.send', { to: agent.address, amount: '0.001' })
|
|
142
|
+
|
|
143
|
+
console.log('5) walrus.store (run receipt)')
|
|
144
|
+
await runTool(tools, 'walrus.store', {
|
|
145
|
+
content: JSON.stringify({ demo: 'lyra', agent: agent.address, ts: Date.now() }),
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runTool(
|
|
150
|
+
tools: Map<string, DemoTool>,
|
|
151
|
+
name: string,
|
|
152
|
+
args: Record<string, unknown>,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const tool = tools.get(name)
|
|
155
|
+
if (!tool) {
|
|
156
|
+
console.log(` tool ${name} not registered; skipped`)
|
|
157
|
+
console.log('')
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const res = (await tool.handler(args)) as { ok?: boolean; error?: string; data?: unknown }
|
|
162
|
+
if (res.ok === false) console.log(` ${name} failed: ${res.error}`)
|
|
163
|
+
else console.log(` ${name} ok: ${JSON.stringify(res.data)}`)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.log(` ${name} threw: ${(e as Error).message.slice(0, 200)}`)
|
|
166
|
+
}
|
|
167
|
+
console.log('')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function describePolicy(p: SuiPolicy): string {
|
|
171
|
+
const parts: string[] = []
|
|
172
|
+
if (p.maxMistPerTx !== undefined) parts.push(`maxPerTx=${formatSui(p.maxMistPerTx)} SUI`)
|
|
173
|
+
if (p.autoMaxMistPerTx !== undefined) parts.push(`autoMax=${formatSui(p.autoMaxMistPerTx)} SUI`)
|
|
174
|
+
if (p.autonomy) parts.push(`autonomy=${p.autonomy}`)
|
|
175
|
+
if (p.readOnly) parts.push('READ-ONLY')
|
|
176
|
+
return parts.length ? parts.join(', ') : '(no caps set)'
|
|
177
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway logs [--tail N] [-f]` — tail the gateway log.
|
|
3
|
+
*
|
|
4
|
+
* v0.19.x: gateway daemon logs to stdout/stderr only (inherited by `gateway run`
|
|
5
|
+
* or backgrounded by `gateway start`). v0.19.3 wires a log file at
|
|
6
|
+
* `~/.lyra/agents/<id>/gateway.log` for tailing. Until then, this command
|
|
7
|
+
* informs the user where to look.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { agentPaths, placeholderAgentId } from 'lyra-core'
|
|
14
|
+
import { findAndLoadConfig } from '../config/load'
|
|
15
|
+
|
|
16
|
+
export interface GatewayLogsOpts {
|
|
17
|
+
agentId?: string
|
|
18
|
+
tail: number
|
|
19
|
+
follow: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runGatewayLogs(opts: GatewayLogsOpts): Promise<void> {
|
|
23
|
+
let agentId = opts.agentId
|
|
24
|
+
if (!agentId) {
|
|
25
|
+
const found = await findAndLoadConfig()
|
|
26
|
+
if (!found?.config) {
|
|
27
|
+
console.error('lyra gateway logs: no lyra.config.ts and no --agent provided')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
const agentEoa = found.config.identity.agent
|
|
31
|
+
if (!agentEoa) {
|
|
32
|
+
console.error('lyra gateway logs: config has no agent EOA; run `lyra init` first')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
agentId = placeholderAgentId(agentEoa)
|
|
36
|
+
}
|
|
37
|
+
const logFile = join(agentPaths.agent(agentId).dir, 'gateway.log')
|
|
38
|
+
if (!existsSync(logFile)) {
|
|
39
|
+
console.log(`gateway log not found at ${logFile}`)
|
|
40
|
+
console.log('v0.19.x: gateway daemon logs to stdout when run via `lyra gateway run`.')
|
|
41
|
+
console.log(
|
|
42
|
+
'Background it with: nohup bun packages/gateway/bin/lyra-gateway-local > ~/lyra-logs/gateway.log 2>&1 &',
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const args = ['-n', String(opts.tail), ...(opts.follow ? ['-f'] : []), logFile]
|
|
47
|
+
const proc = spawn('tail', args, { stdio: 'inherit' })
|
|
48
|
+
proc.on('exit', code => process.exit(code ?? 0))
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway run` — foreground daemon (blocks; Ctrl+C to stop).
|
|
3
|
+
*
|
|
4
|
+
* Spawns `lyra-gateway-local` (the bin in lyra-gateway) with
|
|
5
|
+
* inherit stdio so the user sees logs live. Reads operator-session for the
|
|
6
|
+
* cached AES keys; fails loud if no session exists ("run lyra gateway start
|
|
7
|
+
* first").
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { resolveLocalBin } from '../util/gateway-spawn'
|
|
13
|
+
|
|
14
|
+
export interface GatewayRunOpts {
|
|
15
|
+
agentId?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runGatewayForeground(opts: GatewayRunOpts): Promise<void> {
|
|
19
|
+
const env = { ...process.env }
|
|
20
|
+
if (opts.agentId) env.LYRA_AGENT_ID = opts.agentId
|
|
21
|
+
// Default LYRA_CONFIG to ~/.lyra/config.ts if not already set.
|
|
22
|
+
if (!env.LYRA_CONFIG) {
|
|
23
|
+
env.LYRA_CONFIG = join(env.HOME ?? '', '.lyra', 'config.ts')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const localBin = resolveLocalBin()
|
|
27
|
+
const proc = spawn('bun', [localBin], {
|
|
28
|
+
env,
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
})
|
|
31
|
+
proc.on('exit', code => process.exit(code ?? 0))
|
|
32
|
+
proc.on('error', err => {
|
|
33
|
+
console.error(`lyra gateway run: spawn failed — ${err.message}`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const forwardSignal = (sig: NodeJS.Signals): void => {
|
|
38
|
+
if (!proc.killed) proc.kill(sig)
|
|
39
|
+
}
|
|
40
|
+
process.on('SIGINT', () => forwardSignal('SIGINT'))
|
|
41
|
+
process.on('SIGTERM', () => forwardSignal('SIGTERM'))
|
|
42
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway start` — fork the gateway daemon detached.
|
|
3
|
+
*
|
|
4
|
+
* On Sui the agent key comes from the `LYRA_AGENT_KEY` env var, so there is no
|
|
5
|
+
* operator wallet, Touch ID, or operator-session to derive. The daemon reads
|
|
6
|
+
* the same env the CLI runs in.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Load config from ~/.lyra/config.ts
|
|
10
|
+
* 2. Resolve agentId (override via --agent or derived from LYRA_AGENT_KEY)
|
|
11
|
+
* 3. Check if gateway already running (socket present). If yes, error.
|
|
12
|
+
* 4. Spawn the gateway daemon detached + wait for socket to bind.
|
|
13
|
+
* 5. Print pid + socket path.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { spinner } from '@clack/prompts'
|
|
19
|
+
import { agentPaths, placeholderAgentId } from 'lyra-core'
|
|
20
|
+
import { findAndLoadConfig } from '../config/load'
|
|
21
|
+
import { spawnGatewayDaemon } from '../util/gateway-spawn'
|
|
22
|
+
import { loadAgentFromEnv } from '../util/sui-runtime'
|
|
23
|
+
|
|
24
|
+
export interface GatewayStartOpts {
|
|
25
|
+
agentId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runGatewayStart(opts: GatewayStartOpts): Promise<void> {
|
|
29
|
+
const found = await findAndLoadConfig()
|
|
30
|
+
if (!found?.config) {
|
|
31
|
+
console.error('lyra gateway start: no lyra.config.ts found in cwd or ~/.lyra/')
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
const config = found.config
|
|
35
|
+
|
|
36
|
+
const agent = loadAgentFromEnv()
|
|
37
|
+
const agentAddress = agent?.address ?? config.identity.agent
|
|
38
|
+
if (!agentAddress) {
|
|
39
|
+
console.error(
|
|
40
|
+
'lyra gateway start: no LYRA_AGENT_KEY set and no agent in config. Run `lyra init`.',
|
|
41
|
+
)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
const agentId = opts.agentId ?? placeholderAgentId(agentAddress)
|
|
45
|
+
const paths = agentPaths.agent(agentId)
|
|
46
|
+
const socketPath = join(paths.dir, 'gateway.sock')
|
|
47
|
+
|
|
48
|
+
// If the socket exists, check for version drift; auto-restart on mismatch so
|
|
49
|
+
// operators don't have to `lyra gateway restart` after upgrading the binary.
|
|
50
|
+
if (existsSync(socketPath)) {
|
|
51
|
+
const { createHash } = await import('node:crypto')
|
|
52
|
+
const { homedir } = await import('node:os')
|
|
53
|
+
const identityHash = createHash('sha256').update(agentId).digest('hex').slice(0, 16)
|
|
54
|
+
const lockFile = join(homedir(), '.lyra', 'locks', `lyra-gateway-${identityHash}.lock`)
|
|
55
|
+
const { ensureGatewayVersionMatchesCli } = await import('../util/gateway-version')
|
|
56
|
+
const drift = await ensureGatewayVersionMatchesCli({ socketPath, lockFile })
|
|
57
|
+
if (drift.action === 'ok' || drift.action === 'no-cli-version') {
|
|
58
|
+
console.error(
|
|
59
|
+
`lyra gateway start: socket already exists at ${socketPath} — gateway may be running (version ${drift.daemonVersion ?? 'unknown'}). Try \`lyra gateway stop\` first.`,
|
|
60
|
+
)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
console.log(`note: ${drift.note}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Spawn gateway daemon detached. The daemon reads LYRA_AGENT_KEY + LYRA_*
|
|
67
|
+
// from the inherited environment; no key derivation happens here.
|
|
68
|
+
const sBoot = spinner()
|
|
69
|
+
sBoot.start(`Spawning gateway daemon (agent=${agentId.slice(0, 8)}…)`)
|
|
70
|
+
|
|
71
|
+
const result = await spawnGatewayDaemon({
|
|
72
|
+
agentId,
|
|
73
|
+
configPath: found.path ?? '',
|
|
74
|
+
socketPath,
|
|
75
|
+
timeoutMs: 10_000,
|
|
76
|
+
})
|
|
77
|
+
if (result.ready) {
|
|
78
|
+
sBoot.stop(`gateway running pid=${result.pid} socket=${socketPath}`)
|
|
79
|
+
console.log('stop with: lyra gateway stop')
|
|
80
|
+
console.log('logs: lyra gateway logs -f')
|
|
81
|
+
} else {
|
|
82
|
+
const reason = result.reason ?? 'unknown'
|
|
83
|
+
const detail = result.error ? `: ${result.error}` : ''
|
|
84
|
+
sBoot.stop(
|
|
85
|
+
`gateway did not bind socket within 10s (reason=${reason} pid=${result.pid ?? '?'})${detail}; check above output`,
|
|
86
|
+
)
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway status` — show PID, uptime, socket path, lock state,
|
|
3
|
+
* operator-session freshness.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { agentPaths, placeholderAgentId } from 'lyra-core'
|
|
11
|
+
import { findAndLoadConfig } from '../config/load'
|
|
12
|
+
|
|
13
|
+
export interface GatewayStatusOpts {
|
|
14
|
+
agentId?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fmtAge(ms: number): string {
|
|
18
|
+
const s = Math.floor(ms / 1000)
|
|
19
|
+
if (s < 60) return `${s}s`
|
|
20
|
+
const m = Math.floor(s / 60)
|
|
21
|
+
if (m < 60) return `${m}m`
|
|
22
|
+
const h = Math.floor(m / 60)
|
|
23
|
+
return `${h}h${m % 60}m`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runGatewayStatus(opts: GatewayStatusOpts): Promise<void> {
|
|
27
|
+
let agentId = opts.agentId
|
|
28
|
+
if (!agentId) {
|
|
29
|
+
const found = await findAndLoadConfig()
|
|
30
|
+
if (!found?.config) {
|
|
31
|
+
console.error('lyra gateway status: no lyra.config.ts and no --agent provided')
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
const agentEoa = found.config.identity.agent
|
|
35
|
+
if (!agentEoa) {
|
|
36
|
+
console.error('lyra gateway status: config has no agent EOA; run `lyra init` first')
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
agentId = placeholderAgentId(agentEoa)
|
|
40
|
+
}
|
|
41
|
+
const paths = agentPaths.agent(agentId)
|
|
42
|
+
const socketPath = join(paths.dir, 'gateway.sock')
|
|
43
|
+
const identityHash = createHash('sha256').update(agentId).digest('hex').slice(0, 16)
|
|
44
|
+
const lockFile = join(homedir(), '.lyra', 'locks', `lyra-gateway-${identityHash}.lock`)
|
|
45
|
+
|
|
46
|
+
console.log(`agent: ${agentId}`)
|
|
47
|
+
console.log(`socket: ${socketPath} ${existsSync(socketPath) ? '(present)' : '(absent)'}`)
|
|
48
|
+
console.log(`lock: ${lockFile} ${existsSync(lockFile) ? '(present)' : '(absent)'}`)
|
|
49
|
+
|
|
50
|
+
// PID + uptime via lock file.
|
|
51
|
+
if (existsSync(lockFile)) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(lockFile, 'utf8')) as { pid?: number }
|
|
54
|
+
if (typeof parsed.pid === 'number') {
|
|
55
|
+
let alive = false
|
|
56
|
+
try {
|
|
57
|
+
process.kill(parsed.pid, 0)
|
|
58
|
+
alive = true
|
|
59
|
+
} catch {
|
|
60
|
+
/* dead */
|
|
61
|
+
}
|
|
62
|
+
const stat = statSync(lockFile)
|
|
63
|
+
const ageMs = Date.now() - stat.mtimeMs
|
|
64
|
+
console.log(`pid: ${parsed.pid} ${alive ? '(alive)' : '(dead — stale lock)'}`)
|
|
65
|
+
console.log(`lock-age: ${fmtAge(ageMs)}`)
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
console.log('pid: (lock file unreadable)')
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
console.log('pid: (not running)')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway stop` — SIGTERM the running gateway daemon via the lock
|
|
3
|
+
* file's PID. Falls through to SIGKILL after a 5s grace period.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { agentPaths, placeholderAgentId } from 'lyra-core'
|
|
10
|
+
import { findAndLoadConfig } from '../config/load'
|
|
11
|
+
|
|
12
|
+
export interface GatewayStopOpts {
|
|
13
|
+
agentId?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function lockPath(_agentId: string): string {
|
|
17
|
+
// Mirror packages/core/src/locks.ts — `~/.lyra/locks/<scope>-<sha256(identity).slice(0,16)>.lock`
|
|
18
|
+
// For 'lyra-gateway' scope. We compute the same hash as the lock module.
|
|
19
|
+
// Easiest: read all lock files and find one matching the agent.
|
|
20
|
+
return join(homedir(), '.lyra', 'locks')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findGatewayLock(agentId: string): string | null {
|
|
24
|
+
// The lock filename embeds sha256(agentId).slice(0, 16). Compute it.
|
|
25
|
+
const { createHash } = require('node:crypto')
|
|
26
|
+
const identityHash = createHash('sha256').update(agentId).digest('hex').slice(0, 16)
|
|
27
|
+
const lockFile = join(lockPath(agentId), `lyra-gateway-${identityHash}.lock`)
|
|
28
|
+
return existsSync(lockFile) ? lockFile : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runGatewayStop(opts: GatewayStopOpts): Promise<void> {
|
|
32
|
+
let agentId = opts.agentId
|
|
33
|
+
if (!agentId) {
|
|
34
|
+
const found = await findAndLoadConfig()
|
|
35
|
+
if (!found?.config) {
|
|
36
|
+
console.error('lyra gateway stop: no lyra.config.ts and no --agent provided')
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
const agentEoa = found.config.identity?.agent ?? null
|
|
40
|
+
if (!agentEoa) {
|
|
41
|
+
console.error('lyra gateway stop: config has no agent EOA; run `lyra init` first')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
agentId = placeholderAgentId(agentEoa)
|
|
45
|
+
const label = `agent ${agentId.slice(0, 8)}…`
|
|
46
|
+
const eoaLabel = ` (EOA ${agentEoa.slice(0, 6)}…${agentEoa.slice(-4)})`
|
|
47
|
+
const configPath = found.path ?? '<unknown>'
|
|
48
|
+
console.log(`lyra gateway stop → ${label}${eoaLabel}`)
|
|
49
|
+
console.log(` config: ${configPath}`)
|
|
50
|
+
console.log(
|
|
51
|
+
' if this is not the agent you meant, set LYRA_ROOT or pass --agent <id> before re-running.',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
const lockFile = findGatewayLock(agentId)
|
|
55
|
+
if (!lockFile) {
|
|
56
|
+
console.log(`gateway not running (no lock at ${lockPath(agentId)})`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
let pid: number
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(lockFile, 'utf8').trim()
|
|
62
|
+
// Lock files are JSON with shape `{pid, scope, identityHash, expiresAt}`.
|
|
63
|
+
const parsed = JSON.parse(raw) as { pid?: number }
|
|
64
|
+
if (typeof parsed.pid !== 'number') {
|
|
65
|
+
console.error('lyra gateway stop: lock file has no pid field')
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
pid = parsed.pid
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`lyra gateway stop: lock file unreadable — ${(e as Error).message}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Verify the PID is alive.
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0)
|
|
77
|
+
} catch {
|
|
78
|
+
console.log(`gateway not running (stale lock pid=${pid}); cleaning up`)
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(lockFile)
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Send SIGTERM, wait up to 5s, then SIGKILL.
|
|
88
|
+
console.log(`stopping gateway pid=${pid} ...`)
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 'SIGTERM')
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`lyra gateway stop: SIGTERM failed — ${(e as Error).message}`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const start = Date.now()
|
|
97
|
+
while (Date.now() - start < 5_000) {
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, 0)
|
|
100
|
+
} catch {
|
|
101
|
+
console.log(`gateway stopped pid=${pid}`)
|
|
102
|
+
// Lock file is auto-removed by daemon's shutdown handler. Belt + suspenders:
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(lockFile)) unlinkSync(lockFile)
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
// Also clean up the socket file in case the daemon didn't.
|
|
109
|
+
const socketPath = join(agentPaths.agent(agentId).dir, 'gateway.sock')
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(socketPath)) unlinkSync(socketPath)
|
|
112
|
+
} catch {
|
|
113
|
+
/* ignore */
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
await new Promise(r => setTimeout(r, 200))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('gateway did not exit in 5s; sending SIGKILL')
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 'SIGKILL')
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(`lyra gateway stop: SIGKILL failed — ${(e as Error).message}`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (existsSync(lockFile)) unlinkSync(lockFile)
|
|
129
|
+
} catch {
|
|
130
|
+
/* ignore */
|
|
131
|
+
}
|
|
132
|
+
console.log(`gateway force-killed pid=${pid}`)
|
|
133
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lyra gateway <sub>` argv dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Subs:
|
|
5
|
+
* run — foreground daemon (blocks; Ctrl+C to stop). Uses operator-session.
|
|
6
|
+
* start — interactive: prompt Touch ID, write operator-session, fork daemon.
|
|
7
|
+
* stop — SIGTERM the running daemon via the lock file's PID.
|
|
8
|
+
* restart — stop + start.
|
|
9
|
+
* status — show PID, uptime, socket path, lock state.
|
|
10
|
+
* logs — tail the gateway log (--tail N, --follow).
|
|
11
|
+
*
|
|
12
|
+
* v0.19.x scope: run + start + stop + status. restart/logs/install/setup ship
|
|
13
|
+
* in v0.19.3 alongside launchd plist generator.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type GatewaySub = 'run' | 'start' | 'stop' | 'restart' | 'status' | 'logs'
|
|
17
|
+
|
|
18
|
+
export interface ParsedGatewayArgs {
|
|
19
|
+
sub: GatewaySub
|
|
20
|
+
/** Optional --agent <id> override; defaults to config-derived. */
|
|
21
|
+
agentId?: string
|
|
22
|
+
/** --tail N for logs; default 100. */
|
|
23
|
+
tail?: number
|
|
24
|
+
/** --follow / -f for logs. */
|
|
25
|
+
follow?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ParseResult = ParsedGatewayArgs | { error: string }
|
|
29
|
+
|
|
30
|
+
export function parseGatewayArgs(argv: string[]): ParseResult {
|
|
31
|
+
const sub = argv[0]
|
|
32
|
+
if (!sub) {
|
|
33
|
+
return { error: 'usage: lyra gateway <run | start | stop | restart | status | logs>' }
|
|
34
|
+
}
|
|
35
|
+
if (!['run', 'start', 'stop', 'restart', 'status', 'logs'].includes(sub)) {
|
|
36
|
+
return { error: `unknown gateway sub: ${sub}` }
|
|
37
|
+
}
|
|
38
|
+
let agentId: string | undefined
|
|
39
|
+
let tail: number | undefined
|
|
40
|
+
let follow = false
|
|
41
|
+
for (let i = 1; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i]
|
|
43
|
+
if (a === '--agent') {
|
|
44
|
+
const v = argv[++i]
|
|
45
|
+
if (!v) return { error: '--agent requires a value' }
|
|
46
|
+
agentId = v
|
|
47
|
+
} else if (a === '--tail') {
|
|
48
|
+
const v = argv[++i]
|
|
49
|
+
if (!v) return { error: '--tail requires a value' }
|
|
50
|
+
const n = Number.parseInt(v, 10)
|
|
51
|
+
if (!Number.isFinite(n) || n < 0) return { error: '--tail must be a positive integer' }
|
|
52
|
+
tail = n
|
|
53
|
+
} else if (a === '--follow' || a === '-f') {
|
|
54
|
+
follow = true
|
|
55
|
+
} else {
|
|
56
|
+
return { error: `unknown flag: ${a}` }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { sub: sub as GatewaySub, agentId, tail, follow }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runGateway(parsed: ParsedGatewayArgs): Promise<void> {
|
|
63
|
+
switch (parsed.sub) {
|
|
64
|
+
case 'run': {
|
|
65
|
+
const { runGatewayForeground } = await import('./gateway-run')
|
|
66
|
+
await runGatewayForeground({ agentId: parsed.agentId })
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
case 'start': {
|
|
70
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
71
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
case 'stop': {
|
|
75
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
76
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
case 'restart': {
|
|
80
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
81
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
82
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
83
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
case 'status': {
|
|
87
|
+
const { runGatewayStatus } = await import('./gateway-status')
|
|
88
|
+
await runGatewayStatus({ agentId: parsed.agentId })
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
case 'logs': {
|
|
92
|
+
const { runGatewayLogs } = await import('./gateway-logs')
|
|
93
|
+
await runGatewayLogs({
|
|
94
|
+
agentId: parsed.agentId,
|
|
95
|
+
tail: parsed.tail ?? 100,
|
|
96
|
+
follow: parsed.follow ?? false,
|
|
97
|
+
})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ModelPick {
|
|
2
|
+
provider: string
|
|
3
|
+
model: string | null
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lyra uses a fixed OpenAI-compatible model configured via env
|
|
8
|
+
* (`LYRA_LLM_MODEL` / `LYRA_LLM_BASE_URL` / `OPENAI_API_KEY`), so there's
|
|
9
|
+
* no live provider catalog to pick from. Return the configured default so
|
|
10
|
+
* `init` / `model` proceed without prompting.
|
|
11
|
+
*/
|
|
12
|
+
export async function pickBrainModel(): Promise<ModelPick | null> {
|
|
13
|
+
return {
|
|
14
|
+
provider: 'openai-compatible',
|
|
15
|
+
model: process.env.LYRA_LLM_MODEL ?? 'gpt-4o-mini',
|
|
16
|
+
}
|
|
17
|
+
}
|