@rine-network/openclaw 0.1.4 → 0.1.5

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 CHANGED
@@ -11,8 +11,33 @@ conversation). The agent can also actively send/read/discover via tools.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
+ # 1. Install and enable the plugin
14
15
  openclaw plugins install npm:@rine-network/openclaw
15
16
  openclaw plugins enable rine
17
+ ```
18
+
19
+ **Step 2 is required — add the `channels.rine` block to `openclaw.json`:**
20
+
21
+ ```json
22
+ {
23
+ "channels": {
24
+ "rine": {
25
+ "transport": "sse",
26
+ "healthMonitor": { "enabled": false }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ OpenClaw only activates a channel plugin — importing its code, registering the notify
33
+ service, tools, and inbound route — when the channel id appears under `channels.<id>` in
34
+ `openclaw.json`. Without it, `plugins list` shows the plugin as "enabled/loaded" but the
35
+ inbox is **silently dead** (no notify service) and you will see recurring
36
+ `health-monitor: restarting (reason: stopped)` churn. Setting
37
+ `healthMonitor.enabled: false` silences that churn (requires plugin ≥ 0.1.3).
38
+
39
+ ```bash
40
+ # 3. Restart and verify
16
41
  openclaw gateway restart
17
42
  openclaw plugins inspect rine --runtime --json # verify channel + tools + service + route
18
43
  ```
@@ -53,7 +78,9 @@ install/update time — once installed, the plugin loads normally. (npm placing
53
78
 
54
79
  ## Pick a transport posture
55
80
 
56
- Set `channels.rine.transport` in `openclaw.json` (default `sse`):
81
+ The `channels.rine` block in `openclaw.json` is what activates the channel (see **Install**
82
+ above). Once you have the block, you can tune the `transport` field within it — the default
83
+ is `sse` if you omit the field entirely:
57
84
 
58
85
  | Transport | How it works | Best for |
59
86
  |-----------|--------------|----------|
@@ -0,0 +1,277 @@
1
+ import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup";
2
+ import { HttpClient, getCredentialEntry, getOrRefreshToken, resolveApiUrl, resolveConfigDir } from "@rine-network/core";
3
+ import { tools } from "@rine-network/mcp/tools";
4
+ //#region src/constants.ts
5
+ /**
6
+ * The single `"default"` profile/account literal, shared so a rename touches one place.
7
+ *
8
+ * It is the rine credential *profile* key passed to core's
9
+ * `getCredentialEntry`/`getOrRefreshToken` (the entry under `.default` in
10
+ * credentials.json) and, identically, the OpenClaw single-account id for the rine
11
+ * channel. Both are `"default"` by convention.
12
+ */
13
+ const DEFAULT_ACCOUNT_ID = "default";
14
+ /**
15
+ * Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
16
+ * Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
17
+ * fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path;
18
+ * `rine_send` backs the channel `outbound` adapter (active sends to an arbitrary target).
19
+ */
20
+ const INTERNAL_TOOLS = ["rine_reply", "rine_send"];
21
+ //#endregion
22
+ //#region src/config.ts
23
+ const DEFAULTS = {
24
+ transport: "sse",
25
+ pollIntervalMs: 6e4,
26
+ reconnectBaseMs: 3e3,
27
+ reconnectMaxMs: 3e5,
28
+ a2aAcceptCleartext: true
29
+ };
30
+ function asString(v) {
31
+ return typeof v === "string" && v.trim() !== "" ? v : void 0;
32
+ }
33
+ function asNumber(v, fallback) {
34
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
35
+ }
36
+ function asTransport(v) {
37
+ return v === "expose" || v === "poll" || v === "sse" ? v : DEFAULTS.transport;
38
+ }
39
+ function asAllowFrom(v) {
40
+ if (Array.isArray(v)) {
41
+ const entries = v.filter((e) => typeof e === "string");
42
+ return entries.length > 0 ? entries : ["*"];
43
+ }
44
+ return ["*"];
45
+ }
46
+ /** Resolve the typed config from a raw `api.pluginConfig` (or `{}`), applying defaults. */
47
+ function resolveRineConfig(raw = {}) {
48
+ return {
49
+ transport: asTransport(raw.transport),
50
+ configDir: asString(raw.configDir),
51
+ agentId: asString(raw.agentId),
52
+ baseUrl: asString(raw.baseUrl),
53
+ pollIntervalMs: asNumber(raw.pollIntervalMs, DEFAULTS.pollIntervalMs),
54
+ reconnectBaseMs: asNumber(raw.reconnectBaseMs, DEFAULTS.reconnectBaseMs),
55
+ reconnectMaxMs: asNumber(raw.reconnectMaxMs, DEFAULTS.reconnectMaxMs),
56
+ exposeBaseUrl: asString(raw.exposeBaseUrl),
57
+ a2aAcceptCleartext: typeof raw.a2aAcceptCleartext === "boolean" ? raw.a2aAcceptCleartext : DEFAULTS.a2aAcceptCleartext,
58
+ allowFrom: asAllowFrom(raw.allowFrom)
59
+ };
60
+ }
61
+ /**
62
+ * Resolve rine credentials using core's 3-level config-dir fallback
63
+ * ($RINE_CONFIG_DIR > ~/.config/rine > $PWD/.rine). An explicit `cfg.configDir`
64
+ * override wins and is threaded directly — we never mutate `process.env`. Reuses core
65
+ * helpers; no host keychain access, no token writes.
66
+ */
67
+ function readRineCredentials(cfg) {
68
+ const configDir = cfg.configDir ?? resolveConfigDir();
69
+ const apiUrl = cfg.baseUrl ?? resolveApiUrl();
70
+ const entry = getCredentialEntry(configDir, DEFAULT_ACCOUNT_ID);
71
+ return {
72
+ configDir,
73
+ apiUrl,
74
+ entry,
75
+ pollUrl: entry?.poll_url
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region src/outbound.ts
80
+ function toolByName(name, all = tools) {
81
+ const def = all.find((t) => t.name === name);
82
+ if (!def) throw new Error(`rine: required mcp tool "${name}" not found — version skew`);
83
+ return def;
84
+ }
85
+ /**
86
+ * Route an agent reply back out as a rine message, reusing the lifted `rine_reply`
87
+ * handler (E2EE encrypt + POST /messages/{id}/reply, auto-routed to the original
88
+ * sender, conversation_id preserved). No crypto/HTTP reimplemented.
89
+ */
90
+ async function sendRineReply(client, inbound, text) {
91
+ return toolByName("rine_reply").handler(client.toolContext, {
92
+ message_id: inbound.id,
93
+ payload: { text }
94
+ });
95
+ }
96
+ /**
97
+ * Active send of an agent reply/message to an arbitrary rine target, reusing the
98
+ * lifted `rine_send` handler (E2EE encrypt + POST /messages). Backs the channel
99
+ * `outbound.sendText` adapter so OpenClaw's generic `send` message-action resolves
100
+ * and delivers (without this the channel had no outbound surface — the agent's
101
+ * generic send failed with `Unknown target … for rine`).
102
+ *
103
+ * `from` (the sending agent id) is passed only when configured; in a single-agent
104
+ * org `rine_send` resolves the sole agent itself.
105
+ */
106
+ async function sendRineText(client, to, text, from) {
107
+ const res = await toolByName("rine_send").handler(client.toolContext, {
108
+ to,
109
+ payload: { text },
110
+ ...from ? { from } : {}
111
+ });
112
+ return { messageId: res?.id ?? res?.message_id ?? "unknown" };
113
+ }
114
+ //#endregion
115
+ //#region src/rine-client.ts
116
+ /** Hard ceiling on a single unauthenticated /poll request (the poll loop owns the cadence). */
117
+ const POLL_REQUEST_TIMEOUT_MS = 3e4;
118
+ /**
119
+ * Build the rine HTTP client + ToolContext from resolved creds. Mirrors
120
+ * rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
121
+ * new HttpClient({ tokenFn, apiUrl, canRefresh }).
122
+ */
123
+ function buildRineClient(creds) {
124
+ const { configDir, apiUrl, entry } = creds;
125
+ const getJwt = (force) => getOrRefreshToken(configDir, apiUrl, entry, DEFAULT_ACCOUNT_ID, { force });
126
+ const client = new HttpClient({
127
+ tokenFn: getJwt,
128
+ apiUrl,
129
+ canRefresh: Boolean(entry)
130
+ });
131
+ const toolContext = {
132
+ client,
133
+ configDir,
134
+ apiUrl
135
+ };
136
+ async function pollCount(pollUrl, signal) {
137
+ try {
138
+ const res = await fetch(pollUrl, { signal: signal ?? AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS) });
139
+ if (!res.ok) return 0;
140
+ const body = await res.json();
141
+ return typeof body.count === "number" ? body.count : 0;
142
+ } catch {
143
+ return 0;
144
+ }
145
+ }
146
+ async function fetchNewMessages(agentId, limit = 50) {
147
+ const res = await client.get(`/agents/${agentId}/messages`, {
148
+ status: "new",
149
+ limit
150
+ });
151
+ return Array.isArray(res.items) ? res.items : [];
152
+ }
153
+ async function markDelivered(agentId, ids) {
154
+ if (ids.length === 0) return 0;
155
+ return (await client.markDelivered(agentId, ids)).marked;
156
+ }
157
+ return {
158
+ toolContext,
159
+ client,
160
+ configDir,
161
+ apiUrl,
162
+ getJwt,
163
+ pollCount,
164
+ fetchNewMessages,
165
+ markDelivered
166
+ };
167
+ }
168
+ //#endregion
169
+ //#region src/channel.ts
170
+ /** Re-derive the typed rine config from the `channels.rine` block of a live OpenClawConfig. */
171
+ function rineConfigFromCfg(cfg) {
172
+ const channels = cfg.channels;
173
+ return resolveRineConfig(channels?.rine ?? {});
174
+ }
175
+ /**
176
+ * Minimal but type-valid `ChannelPlugin` for rine. Required fields only:
177
+ * id / meta / capabilities / config. Inbound delivery + outbound replies are owned by
178
+ * the notify service + dispatch seam (the canonical reply path lives on the runtime
179
+ * singleton, available to the service), so the channel object stays thin — it advertises
180
+ * the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
181
+ * surfaces in `plugins inspect`. See SDK_CONTRACT.md.
182
+ */
183
+ const rinePlugin = {
184
+ id: "rine",
185
+ meta: {
186
+ id: "rine",
187
+ label: "rine",
188
+ selectionLabel: "rine",
189
+ detailLabel: "rine network",
190
+ docsPath: "/channels/rine",
191
+ docsLabel: "rine",
192
+ blurb: "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
193
+ systemImage: "antenna.radiowaves.left.and.right",
194
+ order: 120,
195
+ showConfigured: true
196
+ },
197
+ capabilities: {
198
+ chatTypes: ["direct", "group"],
199
+ reply: true,
200
+ threads: true,
201
+ media: false
202
+ },
203
+ reload: { configPrefixes: ["channels.rine"] },
204
+ config: {
205
+ listAccountIds: () => [DEFAULT_ACCOUNT_ID],
206
+ resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
207
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID
208
+ },
209
+ /**
210
+ * Setup wizard: scaffolds a default `channels.rine` block on first run so
211
+ * users get a working channel without hand-editing openclaw.json.
212
+ * Rine credentials live in ~/.config/rine (not openclaw.json), so
213
+ * `credentials: []` — the wizard completes immediately after the prepare hook.
214
+ */
215
+ setupWizard: {
216
+ channel: "rine",
217
+ status: {
218
+ configuredLabel: "rine configured",
219
+ unconfiguredLabel: "rine not configured",
220
+ configuredHint: "Receiving messages via rine network",
221
+ unconfiguredHint: "Run 'openclaw plugins setup rine' to activate",
222
+ resolveConfigured: ({ cfg }) => {
223
+ const ch = cfg.channels;
224
+ return typeof ch?.rine === "object" && ch.rine !== null;
225
+ }
226
+ },
227
+ credentials: [],
228
+ prepare: ({ cfg }) => {
229
+ const ch = cfg.channels;
230
+ if (typeof ch?.rine === "object" && ch.rine !== null) return;
231
+ return { cfg: patchTopLevelChannelConfigSection({
232
+ cfg,
233
+ channel: "rine",
234
+ patch: {
235
+ transport: "sse",
236
+ healthMonitor: { enabled: false }
237
+ }
238
+ }) };
239
+ }
240
+ },
241
+ outbound: {
242
+ deliveryMode: "direct",
243
+ resolveTarget: ({ to }) => {
244
+ const trimmed = to?.trim();
245
+ if (!trimmed) return {
246
+ ok: false,
247
+ error: /* @__PURE__ */ new Error("Delivering to rine needs a target handle or UUID (e.g. alice@acme or #ops@acme).")
248
+ };
249
+ return {
250
+ ok: true,
251
+ to: trimmed
252
+ };
253
+ },
254
+ sendText: async (ctx) => {
255
+ const config = rineConfigFromCfg(ctx.cfg);
256
+ const creds = readRineCredentials(config);
257
+ if (!creds.entry) throw new Error("rine: no credentials at the resolved config dir — run rine_onboard or set RINE_CONFIG_DIR.");
258
+ const { messageId } = await sendRineText(buildRineClient(creds), ctx.to, ctx.text, config.agentId);
259
+ return {
260
+ channel: "rine",
261
+ messageId
262
+ };
263
+ }
264
+ },
265
+ /**
266
+ * Tell core that any non-empty string is a valid rine target. Without this,
267
+ * core's directory-search path treats a free-form `name@org` handle as a
268
+ * display-name (it matches none of the built-in id heuristics) and emits
269
+ * `Unknown target "<handle>" for rine` before delivery is ever attempted.
270
+ */
271
+ messaging: { targetResolver: {
272
+ hint: "Use a rine handle (name@org or #group@org) or an agent UUID.",
273
+ looksLikeId: (raw) => Boolean(raw?.trim())
274
+ } }
275
+ };
276
+ //#endregion
277
+ export { resolveRineConfig as a, readRineCredentials as i, buildRineClient as n, INTERNAL_TOOLS as o, sendRineReply as r, rinePlugin as t };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as INTERNAL_TOOLS, i as DEFAULT_ACCOUNT_ID, n as resolveRineConfig, r as rinePlugin, t as readRineCredentials } from "./config-BsdV6THh.js";
1
+ import { a as resolveRineConfig, i as readRineCredentials, n as buildRineClient, o as INTERNAL_TOOLS, r as sendRineReply, t as rinePlugin } from "./channel-DpHhVQ-n.js";
2
2
  import { i as normalizeStandardWebhook, n as normalizeA2A, t as isAllowed } from "./inbound-G0JD7YmI.js";
3
3
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
4
- import { HttpClient, fetchAgents, getOrRefreshToken, resolveAgent } from "@rine-network/core";
5
- import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/channel-inbound";
4
+ import { fetchAgents, resolveAgent } from "@rine-network/core";
6
5
  import { tools } from "@rine-network/mcp/tools";
6
+ import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/channel-inbound";
7
7
  import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
8
8
  //#region openclaw.plugin.json
9
9
  var description = "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).";
@@ -84,85 +84,20 @@ var configSchema = {
84
84
  }
85
85
  };
86
86
  //#endregion
87
- //#region src/rine-client.ts
88
- /** Hard ceiling on a single unauthenticated /poll request (the poll loop owns the cadence). */
89
- const POLL_REQUEST_TIMEOUT_MS = 3e4;
90
- /**
91
- * Build the rine HTTP client + ToolContext from resolved creds. Mirrors
92
- * rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
93
- * new HttpClient({ tokenFn, apiUrl, canRefresh }).
94
- */
95
- function buildRineClient(creds) {
96
- const { configDir, apiUrl, entry } = creds;
97
- const getJwt = (force) => getOrRefreshToken(configDir, apiUrl, entry, DEFAULT_ACCOUNT_ID, { force });
98
- const client = new HttpClient({
99
- tokenFn: getJwt,
100
- apiUrl,
101
- canRefresh: Boolean(entry)
102
- });
103
- const toolContext = {
104
- client,
105
- configDir,
106
- apiUrl
107
- };
108
- async function pollCount(pollUrl, signal) {
109
- try {
110
- const res = await fetch(pollUrl, { signal: signal ?? AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS) });
111
- if (!res.ok) return 0;
112
- const body = await res.json();
113
- return typeof body.count === "number" ? body.count : 0;
114
- } catch {
115
- return 0;
116
- }
117
- }
118
- async function fetchNewMessages(agentId, limit = 50) {
119
- const res = await client.get(`/agents/${agentId}/messages`, {
120
- status: "new",
121
- limit
122
- });
123
- return Array.isArray(res.items) ? res.items : [];
124
- }
125
- async function markDelivered(agentId, ids) {
126
- if (ids.length === 0) return 0;
127
- return (await client.markDelivered(agentId, ids)).marked;
128
- }
129
- return {
130
- toolContext,
131
- client,
132
- configDir,
133
- apiUrl,
134
- getJwt,
135
- pollCount,
136
- fetchNewMessages,
137
- markDelivered
138
- };
139
- }
140
- //#endregion
141
- //#region src/outbound.ts
142
- function toolByName(name, all = tools) {
143
- const def = all.find((t) => t.name === name);
144
- if (!def) throw new Error(`rine: required mcp tool "${name}" not found — version skew`);
145
- return def;
146
- }
147
- /**
148
- * Route an agent reply back out as a rine message, reusing the lifted `rine_reply`
149
- * handler (E2EE encrypt + POST /messages/{id}/reply, auto-routed to the original
150
- * sender, conversation_id preserved). No crypto/HTTP reimplemented.
151
- */
152
- async function sendRineReply(client, inbound, text) {
153
- return toolByName("rine_reply").handler(client.toolContext, {
154
- message_id: inbound.id,
155
- payload: { text }
156
- });
157
- }
158
- //#endregion
159
87
  //#region src/dispatch.ts
160
88
  function msgOf(err) {
161
89
  return err instanceof Error ? err.message : String(err);
162
90
  }
163
- /** The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`. */
91
+ /**
92
+ * The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
93
+ *
94
+ * The body also binds the reply target to the SENDER. Without this, a model that replies via the
95
+ * optional `rine_send` tool can copy the message's own `to_agent_id` (= this agent's id, surfaced
96
+ * by `rine_read`) and self-target, producing `Unknown target` (TODO-4). The auto-routing path is
97
+ * `rine_reply message_id=<id>`, which derives the recipient from the original sender server-side.
98
+ */
164
99
  function pointerText(msg) {
165
- return `[rine ${msg.type} from ${msg.fromHandle}] message ${msg.id} (read with rine_read)`;
100
+ return `[rine ${msg.type} from ${msg.fromHandle}] message ${msg.id} (read with rine_read; reply with rine_reply message_id=${msg.id} — it auto-routes to ${msg.fromHandle}. If you use rine_send instead, set to=${msg.fromHandle}, never your own agent id.)`;
166
101
  }
167
102
  /**
168
103
  * Build the shared `onMessage` path all transports converge on.
package/dist/setup.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveRineConfig, r as rinePlugin, t as readRineCredentials } from "./config-BsdV6THh.js";
1
+ import { a as resolveRineConfig, i as readRineCredentials, t as rinePlugin } from "./channel-DpHhVQ-n.js";
2
2
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
3
3
  //#region setup.ts
4
4
  /** Auto-detect existing rine creds for the setup flow. Pure read; no writes. */
@@ -10,6 +10,7 @@ export declare const DEFAULT_ACCOUNT_ID = "default";
10
10
  /**
11
11
  * Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
12
12
  * Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
13
- * fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path.
13
+ * fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path;
14
+ * `rine_send` backs the channel `outbound` adapter (active sends to an arbitrary target).
14
15
  */
15
- export declare const INTERNAL_TOOLS: readonly ["rine_reply"];
16
+ export declare const INTERNAL_TOOLS: readonly ["rine_reply", "rine_send"];
@@ -24,7 +24,14 @@ export interface OnMessageDeps {
24
24
  * a transient error that must be retried, so the cursor must stay behind it.
25
25
  */
26
26
  export type OnMessage = (msg: RineInbound) => Promise<boolean>;
27
- /** The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`. */
27
+ /**
28
+ * The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
29
+ *
30
+ * The body also binds the reply target to the SENDER. Without this, a model that replies via the
31
+ * optional `rine_send` tool can copy the message's own `to_agent_id` (= this agent's id, surfaced
32
+ * by `rine_read`) and self-target, producing `Unknown target` (TODO-4). The auto-routing path is
33
+ * `rine_reply message_id=<id>`, which derives the recipient from the original sender server-side.
34
+ */
28
35
  export declare function pointerText(msg: RineInbound): string;
29
36
  /**
30
37
  * Build the shared `onMessage` path all transports converge on.
@@ -6,3 +6,16 @@ import type { RineInbound } from "./types.js";
6
6
  * sender, conversation_id preserved). No crypto/HTTP reimplemented.
7
7
  */
8
8
  export declare function sendRineReply(client: RineClient, inbound: RineInbound, text: string): Promise<unknown>;
9
+ /**
10
+ * Active send of an agent reply/message to an arbitrary rine target, reusing the
11
+ * lifted `rine_send` handler (E2EE encrypt + POST /messages). Backs the channel
12
+ * `outbound.sendText` adapter so OpenClaw's generic `send` message-action resolves
13
+ * and delivers (without this the channel had no outbound surface — the agent's
14
+ * generic send failed with `Unknown target … for rine`).
15
+ *
16
+ * `from` (the sending agent id) is passed only when configured; in a single-agent
17
+ * org `rine_send` resolves the sole agent itself.
18
+ */
19
+ export declare function sendRineText(client: RineClient, to: string, text: string, from?: string): Promise<{
20
+ messageId: string;
21
+ }>;
@@ -3,7 +3,7 @@
3
3
  "kind": "channel",
4
4
  "name": "rine",
5
5
  "description": "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
6
- "version": "0.1.4",
6
+ "version": "0.1.5",
7
7
  "channels": ["rine"],
8
8
  "skills": ["skills/rine"],
9
9
  "activation": { "onStartup": true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rine-network/openclaw",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Official OpenClaw plugin for rine.network \u2014 agent-to-agent E2EE messaging as a native channel, with A2A-relay / SSE / poll transports, tools, and the bundled rine skill.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,113 +0,0 @@
1
- import { getCredentialEntry, resolveApiUrl, resolveConfigDir } from "@rine-network/core";
2
- //#region src/constants.ts
3
- /**
4
- * The single `"default"` profile/account literal, shared so a rename touches one place.
5
- *
6
- * It is the rine credential *profile* key passed to core's
7
- * `getCredentialEntry`/`getOrRefreshToken` (the entry under `.default` in
8
- * credentials.json) and, identically, the OpenClaw single-account id for the rine
9
- * channel. Both are `"default"` by convention.
10
- */
11
- const DEFAULT_ACCOUNT_ID = "default";
12
- /**
13
- * Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
14
- * Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
15
- * fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path.
16
- */
17
- const INTERNAL_TOOLS = ["rine_reply"];
18
- //#endregion
19
- //#region src/channel.ts
20
- /**
21
- * Minimal but type-valid `ChannelPlugin` for rine. Required fields only:
22
- * id / meta / capabilities / config. Inbound delivery + outbound replies are owned by
23
- * the notify service + dispatch seam (the canonical reply path lives on the runtime
24
- * singleton, available to the service), so the channel object stays thin — it advertises
25
- * the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
26
- * surfaces in `plugins inspect`. See SDK_CONTRACT.md.
27
- */
28
- const rinePlugin = {
29
- id: "rine",
30
- meta: {
31
- id: "rine",
32
- label: "rine",
33
- selectionLabel: "rine",
34
- detailLabel: "rine network",
35
- docsPath: "/channels/rine",
36
- docsLabel: "rine",
37
- blurb: "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
38
- systemImage: "antenna.radiowaves.left.and.right",
39
- order: 120,
40
- showConfigured: true
41
- },
42
- capabilities: {
43
- chatTypes: ["direct", "group"],
44
- reply: true,
45
- threads: true,
46
- media: false
47
- },
48
- reload: { configPrefixes: ["channels.rine"] },
49
- config: {
50
- listAccountIds: () => [DEFAULT_ACCOUNT_ID],
51
- resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
52
- defaultAccountId: () => DEFAULT_ACCOUNT_ID
53
- }
54
- };
55
- //#endregion
56
- //#region src/config.ts
57
- const DEFAULTS = {
58
- transport: "sse",
59
- pollIntervalMs: 6e4,
60
- reconnectBaseMs: 3e3,
61
- reconnectMaxMs: 3e5,
62
- a2aAcceptCleartext: true
63
- };
64
- function asString(v) {
65
- return typeof v === "string" && v.trim() !== "" ? v : void 0;
66
- }
67
- function asNumber(v, fallback) {
68
- return typeof v === "number" && Number.isFinite(v) ? v : fallback;
69
- }
70
- function asTransport(v) {
71
- return v === "expose" || v === "poll" || v === "sse" ? v : DEFAULTS.transport;
72
- }
73
- function asAllowFrom(v) {
74
- if (Array.isArray(v)) {
75
- const entries = v.filter((e) => typeof e === "string");
76
- return entries.length > 0 ? entries : ["*"];
77
- }
78
- return ["*"];
79
- }
80
- /** Resolve the typed config from a raw `api.pluginConfig` (or `{}`), applying defaults. */
81
- function resolveRineConfig(raw = {}) {
82
- return {
83
- transport: asTransport(raw.transport),
84
- configDir: asString(raw.configDir),
85
- agentId: asString(raw.agentId),
86
- baseUrl: asString(raw.baseUrl),
87
- pollIntervalMs: asNumber(raw.pollIntervalMs, DEFAULTS.pollIntervalMs),
88
- reconnectBaseMs: asNumber(raw.reconnectBaseMs, DEFAULTS.reconnectBaseMs),
89
- reconnectMaxMs: asNumber(raw.reconnectMaxMs, DEFAULTS.reconnectMaxMs),
90
- exposeBaseUrl: asString(raw.exposeBaseUrl),
91
- a2aAcceptCleartext: typeof raw.a2aAcceptCleartext === "boolean" ? raw.a2aAcceptCleartext : DEFAULTS.a2aAcceptCleartext,
92
- allowFrom: asAllowFrom(raw.allowFrom)
93
- };
94
- }
95
- /**
96
- * Resolve rine credentials using core's 3-level config-dir fallback
97
- * ($RINE_CONFIG_DIR > ~/.config/rine > $PWD/.rine). An explicit `cfg.configDir`
98
- * override wins and is threaded directly — we never mutate `process.env`. Reuses core
99
- * helpers; no host keychain access, no token writes.
100
- */
101
- function readRineCredentials(cfg) {
102
- const configDir = cfg.configDir ?? resolveConfigDir();
103
- const apiUrl = cfg.baseUrl ?? resolveApiUrl();
104
- const entry = getCredentialEntry(configDir, DEFAULT_ACCOUNT_ID);
105
- return {
106
- configDir,
107
- apiUrl,
108
- entry,
109
- pollUrl: entry?.poll_url
110
- };
111
- }
112
- //#endregion
113
- export { INTERNAL_TOOLS as a, DEFAULT_ACCOUNT_ID as i, resolveRineConfig as n, rinePlugin as r, readRineCredentials as t };