@lyriel/openclaw-plugin 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 +242 -0
- package/dist/api.js +155 -0
- package/dist/api.js.map +1 -0
- package/dist/inbox.js +130 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/pending.js +106 -0
- package/dist/pending.js.map +1 -0
- package/dist/primer.js +27 -0
- package/dist/primer.js.map +1 -0
- package/dist/surfacing.js +95 -0
- package/dist/surfacing.js.map +1 -0
- package/dist/telegram.js +63 -0
- package/dist/telegram.js.map +1 -0
- package/dist/tools.js +368 -0
- package/dist/tools.js.map +1 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Lyriel plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Native OpenClaw integration for the Lyriel substrate. Cross-provider parity
|
|
4
|
+
with the Hermes plugin â a Hermes user and an OpenClaw user coordinate
|
|
5
|
+
through Lyriel without either one knowing what runtime the other is on,
|
|
6
|
+
because both runtimes integrate against the same HTTP substrate API.
|
|
7
|
+
|
|
8
|
+
## What you get
|
|
9
|
+
|
|
10
|
+
- ðŠķ **Inbound surfacing.** Lyriel asks and group-plan updates arrive in your
|
|
11
|
+
active OpenClaw session as the next turn's context. A daemon long-polls
|
|
12
|
+
`/api/inbox` and injects each dispatch via
|
|
13
|
+
`api.session.workflow.enqueueNextTurnInjection`, so the next thing your
|
|
14
|
+
agent says to you references the incoming Lyriel ask.
|
|
15
|
+
- **Five LLM tools** so you drive Lyriel in natural language:
|
|
16
|
+
- `lyriel_send_ask` â "ping @noor about dinner thursday"
|
|
17
|
+
- `lyriel_reply` â "reply yes thursday works"
|
|
18
|
+
- `lyriel_plan_send` â "start a plan with @noor and @maya about dinner"
|
|
19
|
+
- `lyriel_plan_reply` â "tell the plan I can do friday too"
|
|
20
|
+
- `lyriel_plan_lock` â "lock the plan at thursday 7pm Mission Cantina"
|
|
21
|
+
- **Full agent context** when drafting Lyriel replies â your OpenClaw
|
|
22
|
+
memory, calendar tools, preferences, etc. are all available to the LLM.
|
|
23
|
+
- **Channel-agnostic.** Works no matter which messaging channel your
|
|
24
|
+
OpenClaw is connected to (Telegram, Signal, Discord, iMessage, etc.). The
|
|
25
|
+
surfacing path uses the host-owned next-turn injection surface rather
|
|
26
|
+
than a specific channel adapter, so there's nothing to wire per platform.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
### Local development (against a globally-installed OpenClaw)
|
|
31
|
+
|
|
32
|
+
OpenClaw's `--link` install path expects compiled JavaScript output, not raw
|
|
33
|
+
TypeScript (source-checkout fallback only applies inside the openclaw monorepo
|
|
34
|
+
itself). One-time setup from this directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 1. Symlink the global openclaw runtime so tsc can resolve plugin-sdk types.
|
|
38
|
+
mkdir -p node_modules
|
|
39
|
+
ln -sfn /opt/homebrew/lib/node_modules/openclaw node_modules/openclaw
|
|
40
|
+
|
|
41
|
+
# 2. Install local build deps.
|
|
42
|
+
npm install --no-save typebox typescript
|
|
43
|
+
|
|
44
|
+
# 3. Re-create the openclaw symlink (npm install clobbers it).
|
|
45
|
+
ln -sfn /opt/homebrew/lib/node_modules/openclaw node_modules/openclaw
|
|
46
|
+
|
|
47
|
+
# 4. Build to dist/.
|
|
48
|
+
npx tsc -p tsconfig.build.json
|
|
49
|
+
|
|
50
|
+
# 5. Link the plugin into OpenClaw.
|
|
51
|
+
openclaw plugins install --link "$(pwd)"
|
|
52
|
+
|
|
53
|
+
# 6. Restart so the runtime picks up the new plugin.
|
|
54
|
+
openclaw gateway restart
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Re-run `npx tsc -p tsconfig.build.json` after any edit to the `*.ts` files;
|
|
58
|
+
the gateway re-reads `dist/` on the next plugin reload.
|
|
59
|
+
|
|
60
|
+
### Bundled (in-tree development against the openclaw monorepo)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd /path/to/openclaw
|
|
64
|
+
ln -s /path/to/lyriel-repo/clients/openclaw-plugin extensions/lyriel
|
|
65
|
+
pnpm install
|
|
66
|
+
pnpm test -- extensions/lyriel/
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The bundled path discovers TS source directly; no precompile needed.
|
|
70
|
+
|
|
71
|
+
### Published (from npm)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @lyriel/openclaw-plugin
|
|
75
|
+
openclaw plugins install @lyriel/openclaw-plugin
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `prepare` script in package.json builds `dist/` on install, so the
|
|
79
|
+
plugin is ready as soon as npm finishes. `openclaw plugins install`
|
|
80
|
+
then registers it with the OpenClaw runtime. ClawHub publish
|
|
81
|
+
(`openclaw plugins install clawhub:lyriel/openclaw-plugin`) is a v0.2
|
|
82
|
+
follow-up that wraps the same npm package.
|
|
83
|
+
|
|
84
|
+
## Configure
|
|
85
|
+
|
|
86
|
+
OpenClaw config lives at `~/.openclaw/openclaw.json`. Add a `plugins.entries.lyriel`
|
|
87
|
+
block alongside whatever other plugin entries you already have:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"plugins": {
|
|
92
|
+
"entries": {
|
|
93
|
+
"lyriel": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"config": {
|
|
96
|
+
"apiKey": "lyk_xxxxxxxxxxxxxxxx",
|
|
97
|
+
"baseUrl": "https://lyriel.ai"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `apiKey` â your `lyk_...` from https://lyriel.ai/me/agent (required)
|
|
106
|
+
- `baseUrl` â defaults to `https://lyriel.ai`; override for dev
|
|
107
|
+
(`http://localhost:5173`) or a cloudflared tunnel
|
|
108
|
+
- `surfaceSessionKey` â optional. Pin a specific session for inbound surfacing;
|
|
109
|
+
otherwise the plugin auto-tracks the most recently active session.
|
|
110
|
+
|
|
111
|
+
Prefer editing the file via `openclaw config` so the gateway picks up changes
|
|
112
|
+
without manual JSON re-parsing.
|
|
113
|
+
|
|
114
|
+
If `surfaceSessionKey` is unset, the plugin tracks the most recent session
|
|
115
|
+
that received any user message and injects Lyriel surfaces there. The first
|
|
116
|
+
inbound dispatch may not surface until you've engaged at least once after
|
|
117
|
+
gateway start.
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
123
|
+
â OpenClaw Gateway (always-on) â
|
|
124
|
+
â â
|
|
125
|
+
register() â ââââââââââââââââââââ âââââââââââââââââââââââââââ â
|
|
126
|
+
ââââââââââķ â â Inbox poll loop â â LLM tool handlers â â
|
|
127
|
+
â â (gateway_start) â â lyriel_send_ask â â
|
|
128
|
+
â âââââââââââŽâââââââââ â lyriel_reply â â
|
|
129
|
+
â â â lyriel_plan_send â â
|
|
130
|
+
â âž â lyriel_plan_reply â â
|
|
131
|
+
â pending.ts (ask_id / â lyriel_plan_lock â â
|
|
132
|
+
â plan_id â callback) ââââââââââââââŽâââââââââââââ â
|
|
133
|
+
â â â â
|
|
134
|
+
â âž â â
|
|
135
|
+
â api.session.workflow. â â
|
|
136
|
+
â enqueueNextTurnInjection({ â â
|
|
137
|
+
â sessionKey, â â
|
|
138
|
+
â text: formatted surface, â â
|
|
139
|
+
â }) â â
|
|
140
|
+
â â â â
|
|
141
|
+
ââââââââââââââžâââââââââââââââââââââââââââžââââââââââââââââ
|
|
142
|
+
â â
|
|
143
|
+
âž âž
|
|
144
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
145
|
+
â User's OpenClaw session â
|
|
146
|
+
â â
|
|
147
|
+
â Next agent turn picks up: â
|
|
148
|
+
â ðŠķ Lyriel ask from @noor: "dinner thursday?" â
|
|
149
|
+
â â
|
|
150
|
+
â User says: "yes thursday works" â
|
|
151
|
+
â â
|
|
152
|
+
â LLM calls lyriel_reply â plugin POSTs callback â
|
|
153
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Comparison with the Hermes plugin
|
|
157
|
+
|
|
158
|
+
| | Hermes plugin | This plugin |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Language | Python | TypeScript |
|
|
161
|
+
| Surface path | Hermes `inject_message` (CLI) + gateway Telegram adapter | OpenClaw `enqueueNextTurnInjection` (channel-agnostic) |
|
|
162
|
+
| Background task | `threading.Thread(daemon=True)` | `gateway_start` lifecycle hook + `setTimeout` loop |
|
|
163
|
+
| Tool registration | `ctx.register_tool` | `api.registerTool` |
|
|
164
|
+
| Pending callback state | in-process `dict` | in-process `Map` |
|
|
165
|
+
| Proactive notification | yes â Telegram push | no â appears on next agent turn |
|
|
166
|
+
|
|
167
|
+
The "no proactive push" gap is deliberate. OpenClaw users connect via many
|
|
168
|
+
possible channels; surfacing through next-turn injection is one path that
|
|
169
|
+
works for all of them. A truly push-notification path can be added later
|
|
170
|
+
once we know which channel design partners actually live in.
|
|
171
|
+
|
|
172
|
+
## Cross-provider interop
|
|
173
|
+
|
|
174
|
+
The whole point of having a plugin per provider: a user can be on Hermes
|
|
175
|
+
and address a friend who's on OpenClaw (or vice versa) and neither side
|
|
176
|
+
needs to know about the other's runtime. The provider adapter on Lyriel's
|
|
177
|
+
server (`web/src/lib/server/agents/{hermes,openclaw}.ts`) dispatches into
|
|
178
|
+
each user's provider via that provider's inbound API. This plugin is the
|
|
179
|
+
client-side half of the OpenClaw integration.
|
|
180
|
+
|
|
181
|
+
End-to-end: Hermes user â Hermes Lyriel plugin â Lyriel API â Lyriel
|
|
182
|
+
OpenClaw adapter â OpenClaw `/v1/chat/completions` â user's OpenClaw
|
|
183
|
+
agent â OpenClaw Lyriel plugin (this) â callback â Lyriel â Hermes Lyriel
|
|
184
|
+
plugin surfaces the reply.
|
|
185
|
+
|
|
186
|
+
## Troubleshooting
|
|
187
|
+
|
|
188
|
+
**"apiKey missing" warning in logs.** Set `plugins.entries.lyriel.config.apiKey`
|
|
189
|
+
to your `lyk_...` token from `https://lyriel.ai/me/agent` (or your
|
|
190
|
+
`LYRIEL_BASE_URL` equivalent). Restart the gateway after editing config.
|
|
191
|
+
|
|
192
|
+
**Tools register but inbox dispatches never appear.** Most likely cause:
|
|
193
|
+
the user hasn't sent any message after gateway start, so the plugin doesn't
|
|
194
|
+
know which session to surface into. Send anything to your agent; the next
|
|
195
|
+
poll cycle will surface accumulated dispatches.
|
|
196
|
+
|
|
197
|
+
**"could not extract callback URL/token" warnings.** The inbound dispatch
|
|
198
|
+
envelope didn't match the expected pattern. Likely a Lyriel server version
|
|
199
|
+
mismatch â re-pair your agent at `/me/agent` for a fresh setup.
|
|
200
|
+
|
|
201
|
+
**Surfaces arrive but in the wrong session.** Set
|
|
202
|
+
`plugins.entries.lyriel.config.surfaceSessionKey` to pin a specific session.
|
|
203
|
+
The auto-tracking heuristic ("most recent message_received") may pick up
|
|
204
|
+
short-lived sub-sessions; pinning is more predictable.
|
|
205
|
+
|
|
206
|
+
**Hot reload.** OpenClaw plugin reloads keep durable session extension
|
|
207
|
+
state and re-run cleanup callbacks. The inbox loop stops cleanly on
|
|
208
|
+
`gateway_stop` and restarts on the next `gateway_start`.
|
|
209
|
+
|
|
210
|
+
## v0 limitations
|
|
211
|
+
|
|
212
|
+
- **In-process pending map.** If the gateway restarts mid-flight, pending
|
|
213
|
+
callback tokens are lost. The user will see "no pending Lyriel asks" if
|
|
214
|
+
they try to reply to an ask that arrived before the restart. Move to
|
|
215
|
+
`api.runtime.state.openKeyedStore` to survive restarts.
|
|
216
|
+
- **One-pending-per-ask.** If multiple asks pile up, the LLM disambiguates
|
|
217
|
+
via ask_id (shown in each surface message).
|
|
218
|
+
- **No truly proactive push.** Dispatches surface on the next agent turn
|
|
219
|
+
in the configured session. Adding per-channel push is a v0.2 task.
|
|
220
|
+
- **Single user per OpenClaw gateway.** One `apiKey` per plugin instance.
|
|
221
|
+
Multi-user gateways aren't supported yet.
|
|
222
|
+
|
|
223
|
+
## Publishing
|
|
224
|
+
|
|
225
|
+
The package is `@lyriel/openclaw-plugin` on npm. Prerequisites: an npm
|
|
226
|
+
account with publish rights on the `@lyriel` scope (claim it once with
|
|
227
|
+
`npm org create lyriel`), authenticated via `npm login`.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# 1. Bump the version in package.json (and openclaw.plugin.json if the
|
|
231
|
+
# OpenClaw-facing version should track).
|
|
232
|
+
# 2. Publish. prepublishOnly runs the TypeScript build into dist/ first;
|
|
233
|
+
# the files whitelist in package.json scopes the tarball to dist/ +
|
|
234
|
+
# openclaw.plugin.json.
|
|
235
|
+
npm publish
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`publishConfig.access: public` in package.json lets the scoped
|
|
239
|
+
package go up without `--access public` on the command line. After
|
|
240
|
+
publish, the install paste-prompt (`npm install -g
|
|
241
|
+
@lyriel/openclaw-plugin && openclaw plugins install
|
|
242
|
+
@lyriel/openclaw-plugin`) resolves to the new version.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// HTTP client for Lyriel's substrate API. Uses global fetch (Node 22+).
|
|
2
|
+
//
|
|
3
|
+
// Five operations:
|
|
4
|
+
// - longPollInbox(): GET /api/inbox with Bearer auth; returns parsed dispatches
|
|
5
|
+
// - sendAsk(to, prompt): POST /api/asks; returns ask_id
|
|
6
|
+
// - sendCallback(callbackUrl, callbackToken, response): POST the per-ask
|
|
7
|
+
// callback URL embedded in an inbound dispatch envelope
|
|
8
|
+
// - sendPlan / sendPlanCallback / lockPlan: group-plan endpoints
|
|
9
|
+
//
|
|
10
|
+
// Config (apiKey, baseUrl) is passed in rather than read from env so the
|
|
11
|
+
// OpenClaw plugin manifest (openclaw.plugin.json -> configSchema) owns the
|
|
12
|
+
// source of truth and the operator edits one place.
|
|
13
|
+
export class LyrielApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
constructor(message, status) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "LyrielApiError";
|
|
18
|
+
this.status = status;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function trimSlash(s) {
|
|
22
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
23
|
+
}
|
|
24
|
+
async function request(method, url, opts = {}) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30_000);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
method,
|
|
30
|
+
headers: {
|
|
31
|
+
"content-type": "application/json",
|
|
32
|
+
...opts.headers,
|
|
33
|
+
},
|
|
34
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
return { status: res.status, body: text };
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseJson(body, action) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(body);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new LyrielApiError(`${action} response was not JSON: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function longPollInbox(cfg, timeoutMs = 30_000) {
|
|
53
|
+
const url = `${trimSlash(cfg.baseUrl)}/api/inbox`;
|
|
54
|
+
const { status, body } = await request("GET", url, {
|
|
55
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
56
|
+
timeoutMs,
|
|
57
|
+
});
|
|
58
|
+
if (status !== 200) {
|
|
59
|
+
throw new LyrielApiError(`Inbox poll failed (${status}): ${body.slice(0, 200)}`, status);
|
|
60
|
+
}
|
|
61
|
+
const parsed = parseJson(body, "inbox");
|
|
62
|
+
return parsed.dispatches ?? [];
|
|
63
|
+
}
|
|
64
|
+
export async function sendAsk(cfg, to, prompt) {
|
|
65
|
+
const url = `${trimSlash(cfg.baseUrl)}/api/asks`;
|
|
66
|
+
const { status, body } = await request("POST", url, {
|
|
67
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
68
|
+
body: { to: to.replace(/^@/, ""), prompt },
|
|
69
|
+
});
|
|
70
|
+
if (status !== 200 && status !== 202) {
|
|
71
|
+
throw new LyrielApiError(`sendAsk failed (${status}): ${body.slice(0, 300)}`, status);
|
|
72
|
+
}
|
|
73
|
+
return parseJson(body, "sendAsk");
|
|
74
|
+
}
|
|
75
|
+
export async function getAsk(cfg, askId) {
|
|
76
|
+
const url = `${trimSlash(cfg.baseUrl)}/api/asks/${askId}`;
|
|
77
|
+
const { status, body } = await request("GET", url, {
|
|
78
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
79
|
+
timeoutMs: 2_000,
|
|
80
|
+
});
|
|
81
|
+
if (status !== 200) {
|
|
82
|
+
throw new LyrielApiError(`getAsk failed (${status})`, status);
|
|
83
|
+
}
|
|
84
|
+
return parseJson(body, "getAsk");
|
|
85
|
+
}
|
|
86
|
+
export async function sendCallback(callbackUrl, callbackToken, response) {
|
|
87
|
+
const { status, body } = await request("POST", callbackUrl, {
|
|
88
|
+
headers: { authorization: `Bearer ${callbackToken}` },
|
|
89
|
+
body: { response },
|
|
90
|
+
});
|
|
91
|
+
if (status !== 200 && status !== 410) {
|
|
92
|
+
throw new LyrielApiError(`callback failed (${status}): ${body.slice(0, 300)}`, status);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(body);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Some non-2xx-but-acceptable paths may return plain text â surface raw
|
|
99
|
+
// body for diagnostics rather than throw a second error.
|
|
100
|
+
return { raw: body, status };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function sendPlan(cfg, participants, description, maxMessages) {
|
|
104
|
+
const url = `${trimSlash(cfg.baseUrl)}/api/plans`;
|
|
105
|
+
const body = {
|
|
106
|
+
participants: participants.map((p) => p.replace(/^@/, "")),
|
|
107
|
+
description,
|
|
108
|
+
};
|
|
109
|
+
if (maxMessages !== undefined) {
|
|
110
|
+
body.max_messages = maxMessages;
|
|
111
|
+
}
|
|
112
|
+
const { status, body: respBody } = await request("POST", url, {
|
|
113
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
114
|
+
body,
|
|
115
|
+
});
|
|
116
|
+
if (status !== 200 && status !== 202) {
|
|
117
|
+
throw new LyrielApiError(`sendPlan failed (${status}): ${respBody.slice(0, 300)}`, status);
|
|
118
|
+
}
|
|
119
|
+
return parseJson(respBody, "sendPlan");
|
|
120
|
+
}
|
|
121
|
+
export async function sendPlanCallback(callbackUrl, callbackToken, content, staySilent) {
|
|
122
|
+
const body = { content };
|
|
123
|
+
if (staySilent) {
|
|
124
|
+
body.stay_silent = true;
|
|
125
|
+
}
|
|
126
|
+
const { status, body: respBody } = await request("POST", callbackUrl, {
|
|
127
|
+
headers: { authorization: `Bearer ${callbackToken}` },
|
|
128
|
+
body,
|
|
129
|
+
});
|
|
130
|
+
if (status !== 200 && status !== 409) {
|
|
131
|
+
throw new LyrielApiError(`plan callback failed (${status}): ${respBody.slice(0, 300)}`, status);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(respBody);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { raw: respBody, status };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function lockPlan(cfg, planId, summary, details) {
|
|
141
|
+
const url = `${trimSlash(cfg.baseUrl)}/api/plans/${planId}/lock`;
|
|
142
|
+
const body = { summary };
|
|
143
|
+
if (details) {
|
|
144
|
+
body.details = details;
|
|
145
|
+
}
|
|
146
|
+
const { status, body: respBody } = await request("POST", url, {
|
|
147
|
+
headers: { authorization: `Bearer ${cfg.apiKey}` },
|
|
148
|
+
body,
|
|
149
|
+
});
|
|
150
|
+
if (status !== 200) {
|
|
151
|
+
throw new LyrielApiError(`lockPlan failed (${status}): ${respBody.slice(0, 300)}`, status);
|
|
152
|
+
}
|
|
153
|
+
return parseJson(respBody, "lockPlan");
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../api.ts"],"names":[],"mappings":"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"}
|
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Background long-poll: hits Lyriel /api/inbox in a recursive setTimeout
|
|
2
|
+
// loop, surfaces each dispatch into the active session via the host's
|
|
3
|
+
// next-turn injection surface, and remembers callback tokens for the LLM's
|
|
4
|
+
// reply tool.
|
|
5
|
+
//
|
|
6
|
+
// Lifecycle: started on the `gateway_start` plugin hook, stopped on
|
|
7
|
+
// `gateway_stop`. Loop control state lives on a `state` object (rather than
|
|
8
|
+
// a captured `let`) so static analysis can see that the start/stop methods
|
|
9
|
+
// mutate the condition the while-loop reads.
|
|
10
|
+
import * as api from "./api.js";
|
|
11
|
+
import * as pending from "./pending.js";
|
|
12
|
+
import { formatDispatch } from "./surfacing.js";
|
|
13
|
+
import { sendTelegramMessage } from "./telegram.js";
|
|
14
|
+
const RECONNECT_BACKOFF_MS = 5_000;
|
|
15
|
+
const INBOX_TIMEOUT_MS = 30_000;
|
|
16
|
+
const POLL_INTERVAL_AFTER_BATCH_MS = 500;
|
|
17
|
+
export function createInboxRunner(opts) {
|
|
18
|
+
const state = { running: false, currentLoop: null };
|
|
19
|
+
async function pollOnce() {
|
|
20
|
+
return api.longPollInbox(opts.client, INBOX_TIMEOUT_MS);
|
|
21
|
+
}
|
|
22
|
+
async function loop() {
|
|
23
|
+
opts.logger.info(`lyriel inbox poll loop started (base=${opts.client.baseUrl})`);
|
|
24
|
+
while (state.running) {
|
|
25
|
+
let dispatches = [];
|
|
26
|
+
try {
|
|
27
|
+
dispatches = await pollOnce();
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (!state.running) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
opts.logger.warn(`inbox poll error: ${err.message}`);
|
|
34
|
+
await sleep(RECONNECT_BACKOFF_MS, () => state.running);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const d of dispatches) {
|
|
38
|
+
const askId = d.ask_id;
|
|
39
|
+
const planId = d.plan_id;
|
|
40
|
+
const direction = d.direction ?? "?";
|
|
41
|
+
const promptText = d.prompt ?? "";
|
|
42
|
+
const correlation = planId ?? askId ?? "?";
|
|
43
|
+
if (direction === "dispatch" && askId) {
|
|
44
|
+
if (!pending.rememberAsk(askId, promptText)) {
|
|
45
|
+
opts.logger.warn(`could not extract callback URL/token for ask ${askId} â user replies will fail`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (direction === "plan" && planId) {
|
|
49
|
+
if (!pending.rememberPlan(planId, promptText)) {
|
|
50
|
+
const isLocked = promptText.includes("[Lyriel group plan â LOCKED]");
|
|
51
|
+
if (!isLocked) {
|
|
52
|
+
opts.logger.warn(`could not extract callback for plan ${planId} â user contributions will fail`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
pending.forgetPlan(planId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Synchronous-completion suppression. If lyriel_send_ask absorbed
|
|
60
|
+
// the response inline (system_responder case â @lyriel responds
|
|
61
|
+
// instantly), skip the duplicate forward surface.
|
|
62
|
+
if (direction === "forward" && askId && pending.wasAbsorbed(askId)) {
|
|
63
|
+
opts.logger.info(`skipped forward ask_id=${askId} (already absorbed by tool)`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const surfaceText = formatDispatch(d);
|
|
67
|
+
// Path A â proactive Telegram push (preferred). Matches the Hermes
|
|
68
|
+
// plugin's "1 bot, 1 chat" UX: the dispatch lands in the user's
|
|
69
|
+
// chat as a real push notification, with no need to engage the
|
|
70
|
+
// agent first.
|
|
71
|
+
const tg = opts.resolveTelegramSurface();
|
|
72
|
+
if (tg.botToken && tg.chatId) {
|
|
73
|
+
const result = await sendTelegramMessage(tg, surfaceText);
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
opts.logger.info(`surfaced ${direction} id=${correlation} via Telegram chat ${tg.chatId}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
opts.logger.warn(`Telegram push for ${direction} id=${correlation} failed: ${result.error ?? "unknown"} â falling back to next-turn injection`);
|
|
79
|
+
}
|
|
80
|
+
// Path B â next-turn injection fallback. Used when Telegram isn't
|
|
81
|
+
// configured or push failed. User has to engage the agent for the
|
|
82
|
+
// dispatch to surface.
|
|
83
|
+
const sessionKey = opts.resolveSurfaceSession();
|
|
84
|
+
if (!sessionKey) {
|
|
85
|
+
opts.logger.warn(`no surface available for ${direction} id=${correlation} â dispatch claimed but won't surface until user engages the agent or Telegram is configured`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await opts.inject({ sessionKey, text: surfaceText });
|
|
90
|
+
opts.logger.info(`surfaced ${direction} id=${correlation} into session ${sessionKey}`);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
opts.logger.error(`surfacing ${direction} ${correlation} failed: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (dispatches.length > 0 && state.running) {
|
|
97
|
+
await sleep(POLL_INTERVAL_AFTER_BATCH_MS, () => state.running);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
opts.logger.info("lyriel inbox poll loop exited");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
start() {
|
|
104
|
+
if (state.running) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
state.running = true;
|
|
108
|
+
state.currentLoop = loop().catch((err) => {
|
|
109
|
+
opts.logger.error(`inbox loop crashed: ${err.message}`);
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
async stop() {
|
|
113
|
+
state.running = false;
|
|
114
|
+
if (state.currentLoop) {
|
|
115
|
+
await state.currentLoop;
|
|
116
|
+
state.currentLoop = null;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function sleep(ms, stillRunning) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
if (!stillRunning()) {
|
|
124
|
+
resolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setTimeout(resolve, ms);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../inbox.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Lyriel plugin for OpenClaw.
|
|
2
|
+
//
|
|
3
|
+
// Wires Lyriel (https://lyriel.ai) into the OpenClaw runtime. Cross-provider
|
|
4
|
+
// parity with clients/hermes-plugin/: a Hermes user and an OpenClaw user can
|
|
5
|
+
// coordinate through Lyriel without either knowing what runtime the other is
|
|
6
|
+
// on, because both runtimes integrate via Lyriel's HTTP substrate API.
|
|
7
|
+
//
|
|
8
|
+
// Three pieces wire up at register():
|
|
9
|
+
//
|
|
10
|
+
// 1. Five LLM tools (lyriel_send_ask, lyriel_reply, lyriel_plan_send,
|
|
11
|
+
// lyriel_plan_reply, lyriel_plan_lock) so the user drives Lyriel in
|
|
12
|
+
// natural language. Schemas live in tools.ts.
|
|
13
|
+
//
|
|
14
|
+
// 2. A background long-poll started on the `gateway_start` lifecycle
|
|
15
|
+
// hook. The loop hits Lyriel's /api/inbox, parses dispatches, stores
|
|
16
|
+
// callback tokens by ask_id / plan_id (so the reply tools have them
|
|
17
|
+
// when the user wants to respond), and pushes a human-readable
|
|
18
|
+
// surface message into the user's active session via
|
|
19
|
+
// api.session.workflow.enqueueNextTurnInjection.
|
|
20
|
+
//
|
|
21
|
+
// 3. A `message_received` hook that tracks the user's most recent
|
|
22
|
+
// session key so the poll loop knows which session to surface into.
|
|
23
|
+
// Operators can pin a specific session via
|
|
24
|
+
// `plugins.entries.lyriel.config.surfaceSessionKey` instead.
|
|
25
|
+
//
|
|
26
|
+
// Required config (plugins.entries.lyriel.config in openclaw config):
|
|
27
|
+
// apiKey â your lyk_... key from /me/agent setup (required)
|
|
28
|
+
// baseUrl â defaults to https://lyriel.ai; override for dev
|
|
29
|
+
// surfaceSessionKey â optional pinned session key (otherwise auto-tracked)
|
|
30
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
31
|
+
import { createInboxRunner } from "./inbox.js";
|
|
32
|
+
import { resolveTelegramSurface } from "./telegram.js";
|
|
33
|
+
import { buildToolDescriptors } from "./tools.js";
|
|
34
|
+
function parseConfig(raw) {
|
|
35
|
+
if (!raw || typeof raw !== "object") {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const o = raw;
|
|
39
|
+
return {
|
|
40
|
+
apiKey: typeof o.apiKey === "string" ? o.apiKey : undefined,
|
|
41
|
+
baseUrl: typeof o.baseUrl === "string" ? o.baseUrl : undefined,
|
|
42
|
+
surfaceSessionKey: typeof o.surfaceSessionKey === "string" ? o.surfaceSessionKey : undefined,
|
|
43
|
+
surfaceTelegramChatId: typeof o.surfaceTelegramChatId === "string" ? o.surfaceTelegramChatId : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export default definePluginEntry({
|
|
47
|
+
id: "lyriel",
|
|
48
|
+
name: "Lyriel",
|
|
49
|
+
description: "Lyriel plugin for OpenClaw â agent-mediated communication substrate. Long-polls /api/inbox, surfaces dispatches into the active session, and exposes five tools for asks and group plans.",
|
|
50
|
+
register(api) {
|
|
51
|
+
const config = parseConfig(api.pluginConfig);
|
|
52
|
+
const baseUrl = (config.baseUrl ?? "https://lyriel.ai").trim();
|
|
53
|
+
const apiKey = (config.apiKey ?? "").trim();
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
api.logger.warn("Lyriel plugin: apiKey missing in plugins.entries.lyriel.config â tools and inbox poll will fail until set.");
|
|
56
|
+
}
|
|
57
|
+
const client = { apiKey, baseUrl };
|
|
58
|
+
// âââ LLM tools ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
59
|
+
for (const tool of buildToolDescriptors(client)) {
|
|
60
|
+
api.registerTool({
|
|
61
|
+
name: tool.name,
|
|
62
|
+
label: tool.label,
|
|
63
|
+
description: tool.description,
|
|
64
|
+
parameters: tool.parameters,
|
|
65
|
+
execute: tool.execute,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// âââ Surface-session tracking âââââââââââââââââââââââââââââââââââââââ
|
|
69
|
+
// The user's most recently active session, populated lazily by the
|
|
70
|
+
// message_received hook. Cleared on gateway_stop.
|
|
71
|
+
let mostRecentSessionKey = null;
|
|
72
|
+
api.on("message_received", async (_event, ctx) => {
|
|
73
|
+
const sk = ctx.sessionKey;
|
|
74
|
+
if (sk && typeof sk === "string") {
|
|
75
|
+
mostRecentSessionKey = sk;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
function resolveSurfaceSession() {
|
|
79
|
+
if (config.surfaceSessionKey) {
|
|
80
|
+
return config.surfaceSessionKey;
|
|
81
|
+
}
|
|
82
|
+
return mostRecentSessionKey;
|
|
83
|
+
}
|
|
84
|
+
// âââ Inbox poll loop ââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
85
|
+
const childLogger = api.logger;
|
|
86
|
+
const inbox = createInboxRunner({
|
|
87
|
+
client,
|
|
88
|
+
resolveSurfaceSession,
|
|
89
|
+
resolveTelegramSurface() {
|
|
90
|
+
return resolveTelegramSurface(api.config, config.surfaceTelegramChatId);
|
|
91
|
+
},
|
|
92
|
+
logger: childLogger,
|
|
93
|
+
async inject({ sessionKey, text }) {
|
|
94
|
+
await api.session.workflow.enqueueNextTurnInjection({
|
|
95
|
+
sessionKey,
|
|
96
|
+
text,
|
|
97
|
+
placement: "append_context",
|
|
98
|
+
ttlMs: 5 * 60_000,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
api.on("gateway_start", async () => {
|
|
103
|
+
if (!apiKey) {
|
|
104
|
+
api.logger.warn("Lyriel plugin: gateway_start received but apiKey is empty â not starting inbox poll loop.");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
inbox.start();
|
|
108
|
+
api.logger.info("Lyriel plugin registered: 5 tools (ask, reply, plan_send, plan_reply, plan_lock) + inbox poll loop");
|
|
109
|
+
});
|
|
110
|
+
api.on("gateway_stop", async () => {
|
|
111
|
+
await inbox.stop();
|
|
112
|
+
mostRecentSessionKey = null;
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.js.map
|