@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 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
@@ -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