@stagewhisper/stagewhisper 0.49.0 → 0.52.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/plugin-main.ts DELETED
@@ -1,342 +0,0 @@
1
- import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
- import { stagewhisperPlugin } from "./src/channel.js";
3
- import { setRuntime } from "./src/runtime.js";
4
- import { createRelayService } from "./src/service.js";
5
-
6
- async function ensureResponsesEndpoint(api: Parameters<Parameters<typeof definePluginEntry>[0]["register"]>[0]): Promise<void> {
7
- try {
8
- const cfg = await api.runtime.config.loadConfig();
9
- const gw = ((cfg as Record<string, unknown>)["gateway"] ?? {}) as Record<string, unknown>;
10
- const http = (gw["http"] ?? {}) as Record<string, unknown>;
11
- const endpoints = (http["endpoints"] ?? {}) as Record<string, unknown>;
12
- const responses = (endpoints["responses"] ?? {}) as Record<string, unknown>;
13
-
14
- if (responses["enabled"] === true) return;
15
-
16
- const auth = (gw["auth"] ?? {}) as Record<string, unknown>;
17
- if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
18
-
19
- responses["enabled"] = true;
20
- endpoints["responses"] = responses;
21
- http["endpoints"] = endpoints;
22
- gw["http"] = http;
23
- (cfg as Record<string, unknown>)["gateway"] = gw;
24
-
25
- await api.runtime.config.writeConfigFile(cfg);
26
- api.logger.info(
27
- "Enabled gateway.http.endpoints.responses for StageWhisper reasoning. Restart the gateway for it to take effect.",
28
- );
29
- } catch {
30
- // best-effort — reasoning-check will surface the real error
31
- }
32
- }
33
-
34
- export default definePluginEntry({
35
- id: "stagewhisper",
36
- name: "StageWhisper",
37
- description: "Turn live call moments into assistant tasks via StageWhisper",
38
- register(api) {
39
- api.registerChannel({ plugin: stagewhisperPlugin });
40
-
41
- api.registerCli(
42
- ({ program }) => {
43
- const sw = program
44
- .command("stagewhisper")
45
- .description("StageWhisper integration");
46
-
47
- sw.command("pair")
48
- .description(
49
- "Pair with StageWhisper using a pairing code from the desktop app",
50
- )
51
- .requiredOption("--code <code>", "Pairing code from Settings → Assistant")
52
- .option(
53
- "--api-url <url>",
54
- "StageWhisper backend URL",
55
- "https://api.stagewhisper.io",
56
- )
57
- .option("--label <label>", "Label for this OpenClaw host", "OpenClaw")
58
- .option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API")
59
- .action(
60
- async (opts: {
61
- code: string;
62
- apiUrl: string;
63
- label?: string;
64
- enableResponses: boolean;
65
- }) => {
66
- const { StageWhisperClient } = await import("./src/client.js");
67
- const client = new StageWhisperClient(opts.apiUrl, "", "");
68
- try {
69
- const result = await client.completePairing(
70
- opts.code,
71
- opts.label ?? "OpenClaw",
72
- );
73
-
74
- const cfg = await api.runtime.config.loadConfig();
75
- const plugins = (cfg as Record<string, unknown>)["plugins"] as Record<string, unknown> ?? {};
76
- const entries = plugins["entries"] as Record<string, Record<string, unknown>> ?? {};
77
- const swEntry = entries["stagewhisper"] ?? {};
78
- const swConfig = (swEntry["config"] as Record<string, unknown>) ?? {};
79
-
80
- swConfig["apiBaseUrl"] = opts.apiUrl;
81
- swConfig["integrationId"] = result.integration_id;
82
- swConfig["relayToken"] = result.relay_token;
83
- swConfig["label"] = result.label;
84
- swEntry["config"] = swConfig;
85
- entries["stagewhisper"] = swEntry;
86
- plugins["entries"] = entries;
87
- (cfg as Record<string, unknown>)["plugins"] = plugins;
88
-
89
- const channels = ((cfg as Record<string, unknown>)["channels"] ?? {}) as Record<string, Record<string, unknown>>;
90
- channels["stagewhisper"] = {
91
- apiBaseUrl: opts.apiUrl,
92
- integrationId: result.integration_id,
93
- relayToken: result.relay_token,
94
- label: result.label,
95
- };
96
- (cfg as Record<string, unknown>)["channels"] = channels;
97
-
98
- if (opts.enableResponses) {
99
- const gw = ((cfg as Record<string, unknown>)["gateway"] ?? {}) as Record<string, unknown>;
100
- const gwAuth = (gw["auth"] as Record<string, unknown>) ?? {};
101
- const authMode = gwAuth["mode"] as string | undefined;
102
- const hasToken = typeof gwAuth["token"] === "string" && (gwAuth["token"] as string).length > 0;
103
-
104
- if (authMode === "none" && !hasToken) {
105
- console.warn(" ⚠ gateway.auth.mode is 'none' with no token — skipping HTTP API enablement.");
106
- console.warn(" Set a gateway auth token first for reasoning to work.\n");
107
- } else {
108
- const http = (gw["http"] ?? {}) as Record<string, unknown>;
109
- const endpoints = (http["endpoints"] ?? {}) as Record<string, unknown>;
110
- const responses = (endpoints["responses"] ?? {}) as Record<string, unknown>;
111
- responses["enabled"] = true;
112
- endpoints["responses"] = responses;
113
- http["endpoints"] = endpoints;
114
- gw["http"] = http;
115
- (cfg as Record<string, unknown>)["gateway"] = gw;
116
- }
117
- }
118
-
119
- await api.runtime.config.writeConfigFile(cfg);
120
-
121
- console.log(
122
- `\n✓ Paired with StageWhisper (${result.label})`,
123
- );
124
- console.log(" Config saved. Restart the gateway to activate:\n");
125
- console.log(" openclaw gateway restart\n");
126
- } catch (err) {
127
- console.error(`\n✗ Pairing failed: ${err}\n`);
128
- process.exit(1);
129
- }
130
- },
131
- );
132
-
133
- sw.command("unpair")
134
- .description(
135
- "Remove StageWhisper pairing (run before `openclaw plugins uninstall`)",
136
- )
137
- .option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair")
138
- .action(async (opts: { keepResponses?: boolean }) => {
139
- try {
140
- const cfg = await api.runtime.config.loadConfig();
141
- const plugins = (cfg as Record<string, unknown>)["plugins"] as Record<string, unknown> ?? {};
142
- const entries = plugins["entries"] as Record<string, Record<string, unknown>> ?? {};
143
- const swEntry = entries["stagewhisper"];
144
- if (swEntry) {
145
- delete swEntry["config"];
146
- }
147
-
148
- const channels = (cfg as Record<string, unknown>)["channels"] as Record<string, unknown> | undefined;
149
- if (channels?.["stagewhisper"]) {
150
- delete channels["stagewhisper"];
151
- if (Object.keys(channels).length === 0) {
152
- delete (cfg as Record<string, unknown>)["channels"];
153
- }
154
- }
155
-
156
- if (!opts.keepResponses) {
157
- const gw = (cfg as Record<string, unknown>)["gateway"] as Record<string, unknown> | undefined;
158
- const http = gw?.["http"] as Record<string, unknown> | undefined;
159
- const endpoints = http?.["endpoints"] as Record<string, unknown> | undefined;
160
- const responses = endpoints?.["responses"] as Record<string, unknown> | undefined;
161
- if (responses?.["enabled"] === true) {
162
- delete responses["enabled"];
163
- if (Object.keys(responses).length === 0 && endpoints) delete endpoints["responses"];
164
- if (endpoints && Object.keys(endpoints).length === 0 && http) delete http["endpoints"];
165
- if (http && Object.keys(http).length === 0 && gw) delete gw["http"];
166
- console.log(" ℹ Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it.");
167
- }
168
- }
169
-
170
- await api.runtime.config.writeConfigFile(cfg);
171
- console.log("\n✓ StageWhisper unpaired.");
172
- console.log(" Config cleaned. You can now safely uninstall:\n");
173
- console.log(" openclaw plugins uninstall stagewhisper\n");
174
- } catch (err) {
175
- console.error(`\n✗ Unpair failed: ${err}\n`);
176
- process.exit(1);
177
- }
178
- });
179
-
180
- sw.command("reasoning-check")
181
- .description("Test reasoning capability against the local OpenResponses endpoint")
182
- .option("--model <model>", "Model to use (omit to use your configured default)", "openclaw/default")
183
- .action(async (opts: { model: string }) => {
184
- const { callOpenResponses, isResponsesEndpointEnabled } = await import("./src/openresponses.js");
185
- const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
186
-
187
- const cfg = api.config as Record<string, unknown>;
188
- const gw = (cfg?.gateway as Record<string, unknown>) ?? {};
189
- const auth = (gw?.auth as Record<string, unknown>) ?? {};
190
- const port = Number(gw?.port) || 18789;
191
- const hasToken = typeof auth?.token === "string" && auth.token.length > 0;
192
- const responsesEnabled = isResponsesEndpointEnabled(api);
193
-
194
- console.log("Preflight checks:");
195
- console.log(` Gateway port: ${port}`);
196
- console.log(` Auth token: ${hasToken ? "✓ present" : "✗ MISSING"}`);
197
- console.log(` responses.enabled: ${responsesEnabled ? "✓ true" : "✗ false"}`);
198
-
199
- if (!responsesEnabled) {
200
- console.warn("\n⚠ responses.enabled is false in the running config.");
201
- console.warn(" The plugin auto-enables it on startup — restart the gateway if you haven't:");
202
- console.warn(" openclaw gateway restart\n");
203
- }
204
-
205
- if (!hasToken) {
206
- console.warn("\n⚠ No gateway auth token found — request may be rejected.\n");
207
- }
208
-
209
- console.log(`\nTesting reasoning with model: ${modelLabel}`);
210
- console.log("Sending test request to local /v1/responses ...");
211
-
212
- const testSchema = {
213
- type: "object",
214
- properties: {
215
- signals: {
216
- type: "array",
217
- items: {
218
- type: "object",
219
- properties: {
220
- severity: { type: "string", enum: ["green", "orange", "red"] },
221
- message: { type: "string" },
222
- },
223
- required: ["severity", "message"],
224
- additionalProperties: false,
225
- },
226
- },
227
- no_signal_reason: { type: "string" },
228
- },
229
- required: ["signals", "no_signal_reason"],
230
- additionalProperties: false,
231
- };
232
-
233
- const start = Date.now();
234
- try {
235
- const result = await callOpenResponses(api, {
236
- model: opts.model,
237
- input: JSON.stringify({
238
- transcript: "Candidate: I think we should use Redis for caching.",
239
- playbook_guidance: "Evaluate technical decisions",
240
- }),
241
- instructions: [
242
- 'You are a structured reasoning engine for the "reasoning_test" task.',
243
- "You MUST respond with a JSON object conforming to this schema.",
244
- "Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
245
- "",
246
- "JSON Schema:",
247
- JSON.stringify(testSchema, null, 2),
248
- ].join("\n"),
249
- max_output_tokens: 1024,
250
- });
251
-
252
- const elapsed = Date.now() - start;
253
- console.log(`✓ Response received in ${elapsed}ms`);
254
- console.log(` Run ID: ${result.id}`);
255
- if (result.usage) {
256
- console.log(
257
- ` Tokens: ${result.usage.input_tokens} in / ${result.usage.output_tokens} out`,
258
- );
259
- }
260
-
261
- const output = result.output;
262
- const msgItem = Array.isArray(output)
263
- ? (output.find((o) => o.type === "message") as Record<string, unknown> | undefined)
264
- : null;
265
- const textContent = msgItem
266
- ? ((msgItem.content as Array<Record<string, unknown>>)?.find(
267
- (c) => c.type === "output_text",
268
- )?.text as string | undefined)
269
- : null;
270
- if (textContent) {
271
- const cleaned = textContent.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
272
- try {
273
- const parsed = JSON.parse(cleaned);
274
- console.log(" Schema-valid JSON: ✓");
275
- console.log(` Output: ${JSON.stringify(parsed, null, 2)}`);
276
- } catch {
277
- console.log(" Schema-valid JSON: ✗ (parse error)");
278
- console.log(` Raw text: ${textContent.slice(0, 500)}`);
279
- }
280
- }
281
- } catch (err) {
282
- const elapsed = Date.now() - start;
283
- console.error(`✗ Reasoning check failed after ${elapsed}ms`);
284
- console.error(
285
- ` Error: ${err instanceof Error ? err.message : String(err)}`,
286
- );
287
- process.exitCode = 1;
288
- }
289
- });
290
-
291
- sw.command("status")
292
- .description("Show StageWhisper relay connection status")
293
- .action(async () => {
294
- const cfg = api.pluginConfig as Record<string, string> | undefined;
295
- const configured = !!(
296
- cfg?.["integrationId"] && cfg?.["relayToken"]
297
- );
298
-
299
- if (!configured) {
300
- console.log("\nStageWhisper: not paired\n");
301
- console.log(
302
- " Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n",
303
- );
304
- console.log(
305
- " Get the pairing code from StageWhisper desktop: Settings → Assistant → Generate Pairing Code\n",
306
- );
307
- return;
308
- }
309
-
310
- const apiBaseUrl = cfg?.["apiBaseUrl"] ?? "(unset)";
311
- console.log(`\nStageWhisper:`);
312
- console.log(` Backend: ${apiBaseUrl}`);
313
- console.log(` Integration: ${cfg?.["integrationId"]}`);
314
- console.log(` Label: ${cfg?.["label"] ?? "(unset)"}`);
315
-
316
- if (cfg?.["apiBaseUrl"] && cfg?.["integrationId"] && cfg?.["relayToken"]) {
317
- try {
318
- const { StageWhisperClient } = await import("./src/client.js");
319
- const client = new StageWhisperClient(
320
- cfg["apiBaseUrl"],
321
- cfg["integrationId"],
322
- cfg["relayToken"],
323
- );
324
- await client.heartbeat();
325
- console.log(" Connection: ✓ reachable\n");
326
- } catch (err) {
327
- console.log(` Connection: ✗ ${err}\n`);
328
- }
329
- }
330
- });
331
- },
332
- { commands: ["stagewhisper"] },
333
- );
334
-
335
- if (api.registrationMode !== "full") return;
336
-
337
- ensureResponsesEndpoint(api);
338
- setRuntime(api.runtime);
339
- const service = createRelayService(api);
340
- api.registerService(service);
341
- },
342
- });
@@ -1,201 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { StageWhisperClient } from "./client.js";
3
-
4
- describe("StageWhisperClient", () => {
5
- beforeEach(() => {
6
- vi.restoreAllMocks();
7
- });
8
-
9
- describe("completePairing", () => {
10
- it("sends pairing code and returns credentials", async () => {
11
- const mockResponse = {
12
- integration_id: "int-123",
13
- relay_token: "tok-abc",
14
- label: "StageWhisper (openclaw)",
15
- };
16
-
17
- vi.stubGlobal(
18
- "fetch",
19
- vi.fn().mockResolvedValue({
20
- ok: true,
21
- json: () => Promise.resolve(mockResponse),
22
- }),
23
- );
24
-
25
- const client = new StageWhisperClient("https://sw.test", "", "");
26
- const result = await client.completePairing("ABC123", "MyHost");
27
-
28
- expect(fetch).toHaveBeenCalledWith(
29
- "https://sw.test/api/v1/openclaw/pair/complete",
30
- expect.objectContaining({
31
- method: "POST",
32
- body: JSON.stringify({
33
- pairing_code: "ABC123",
34
- host_label: "MyHost",
35
- }),
36
- }),
37
- );
38
-
39
- expect(result.integration_id).toBe("int-123");
40
- expect(result.relay_token).toBe("tok-abc");
41
- });
42
-
43
- it("throws on failure", async () => {
44
- vi.stubGlobal(
45
- "fetch",
46
- vi.fn().mockResolvedValue({
47
- ok: false,
48
- status: 400,
49
- text: () => Promise.resolve("Invalid code"),
50
- }),
51
- );
52
-
53
- const client = new StageWhisperClient("https://sw.test", "", "");
54
- await expect(client.completePairing("BAD", "Host")).rejects.toThrow(
55
- "Pairing failed (400)",
56
- );
57
- });
58
- });
59
-
60
- describe("postReply", () => {
61
- it("sends reply content to the correct endpoint", async () => {
62
- vi.stubGlobal(
63
- "fetch",
64
- vi.fn().mockResolvedValue({
65
- ok: true,
66
- json: () => Promise.resolve({ ok: true }),
67
- }),
68
- );
69
-
70
- const client = new StageWhisperClient(
71
- "https://sw.test",
72
- "int-1",
73
- "tok-1",
74
- );
75
- await client.postReply("task-42", "Here is the answer", "msg-99");
76
-
77
- expect(fetch).toHaveBeenCalledWith(
78
- "https://sw.test/api/v1/openclaw/tasks/task-42/reply",
79
- expect.objectContaining({
80
- method: "POST",
81
- headers: expect.objectContaining({
82
- Authorization: "Bearer tok-1",
83
- }),
84
- body: JSON.stringify({
85
- content: "Here is the answer",
86
- remote_message_id: "msg-99",
87
- }),
88
- }),
89
- );
90
- });
91
- });
92
-
93
- describe("updateTaskStatus", () => {
94
- it("sends status update", async () => {
95
- vi.stubGlobal(
96
- "fetch",
97
- vi.fn().mockResolvedValue({
98
- ok: true,
99
- json: () => Promise.resolve({ ok: true }),
100
- }),
101
- );
102
-
103
- const client = new StageWhisperClient(
104
- "https://sw.test",
105
- "int-1",
106
- "tok-1",
107
- );
108
- await client.updateTaskStatus("task-42", "running", "remote-77");
109
-
110
- expect(fetch).toHaveBeenCalledWith(
111
- "https://sw.test/api/v1/openclaw/tasks/task-42/status",
112
- expect.objectContaining({
113
- method: "POST",
114
- body: JSON.stringify({
115
- status: "running",
116
- remote_task_id: "remote-77",
117
- }),
118
- }),
119
- );
120
- });
121
- });
122
-
123
- describe("heartbeat", () => {
124
- it("sends heartbeat and returns response", async () => {
125
- vi.stubGlobal(
126
- "fetch",
127
- vi.fn().mockResolvedValue({
128
- ok: true,
129
- json: () => Promise.resolve({ ok: true }),
130
- }),
131
- );
132
-
133
- const client = new StageWhisperClient(
134
- "https://sw.test",
135
- "int-1",
136
- "tok-1",
137
- );
138
- const result = await client.heartbeat();
139
-
140
- expect(result.ok).toBe(true);
141
- expect(fetch).toHaveBeenCalledWith(
142
- "https://sw.test/api/v1/openclaw/integrations/int-1/heartbeat",
143
- expect.objectContaining({
144
- method: "POST",
145
- headers: expect.objectContaining({
146
- Authorization: "Bearer tok-1",
147
- }),
148
- }),
149
- );
150
- });
151
- });
152
-
153
- describe("streamUrl", () => {
154
- it("builds correct stream URL", () => {
155
- const client = new StageWhisperClient(
156
- "https://sw.test/",
157
- "int-1",
158
- "tok-1",
159
- );
160
- expect(client.streamUrl()).toBe(
161
- "https://sw.test/api/v1/openclaw/integrations/int-1/stream",
162
- );
163
- });
164
- });
165
- });
166
-
167
- describe("config resolution", () => {
168
- it("resolveAccount extracts config from channels.stagewhisper", async () => {
169
- const { stagewhisperPlugin } = await import("./channel.js");
170
-
171
- const cfg = {
172
- channels: {
173
- stagewhisper: {
174
- apiBaseUrl: "https://sw.test",
175
- integrationId: "int-1",
176
- relayToken: "tok-1",
177
- label: "My SW",
178
- },
179
- },
180
- } as unknown as import("openclaw/plugin-sdk/core").OpenClawConfig;
181
-
182
- const base = stagewhisperPlugin.base;
183
- if (base?.setup?.resolveAccount) {
184
- const account = base.setup.resolveAccount(cfg);
185
- expect(account.apiBaseUrl).toBe("https://sw.test");
186
- expect(account.integrationId).toBe("int-1");
187
- expect(account.relayToken).toBe("tok-1");
188
- expect(account.label).toBe("My SW");
189
- }
190
- });
191
-
192
- it("resolveAccount throws when config is missing", async () => {
193
- const { stagewhisperPlugin } = await import("./channel.js");
194
-
195
- const cfg = { channels: {} } as unknown as import("openclaw/plugin-sdk/core").OpenClawConfig;
196
- const base = stagewhisperPlugin.base;
197
- if (base?.setup?.resolveAccount) {
198
- expect(() => base.setup.resolveAccount(cfg)).toThrow("apiBaseUrl is required");
199
- }
200
- });
201
- });