@stagewhisper/stagewhisper 0.45.0 → 0.47.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.
@@ -2,7 +2,7 @@
2
2
  "id": "stagewhisper",
3
3
  "name": "StageWhisper",
4
4
  "description": "Turn live call moments into assistant tasks via StageWhisper",
5
- "version": "0.45.0",
5
+ "version": "0.47.0",
6
6
  "channels": [
7
7
  "stagewhisper"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stagewhisper/stagewhisper",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",
package/plugin-main.ts CHANGED
@@ -3,6 +3,34 @@ import { stagewhisperPlugin } from "./src/channel.js";
3
3
  import { setRuntime } from "./src/runtime.js";
4
4
  import { createRelayService } from "./src/service.js";
5
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
+
6
34
  export default definePluginEntry({
7
35
  id: "stagewhisper",
8
36
  name: "StageWhisper",
@@ -10,6 +38,8 @@ export default definePluginEntry({
10
38
  register(api) {
11
39
  api.registerChannel({ plugin: stagewhisperPlugin });
12
40
 
41
+ ensureResponsesEndpoint(api);
42
+
13
43
  api.registerCli(
14
44
  ({ program }) => {
15
45
  const sw = program
@@ -27,11 +57,13 @@ export default definePluginEntry({
27
57
  "https://api.stagewhisper.io",
28
58
  )
29
59
  .option("--label <label>", "Label for this OpenClaw host", "OpenClaw")
60
+ .option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API")
30
61
  .action(
31
62
  async (opts: {
32
63
  code: string;
33
64
  apiUrl: string;
34
65
  label?: string;
66
+ enableResponses: boolean;
35
67
  }) => {
36
68
  const { StageWhisperClient } = await import("./src/client.js");
37
69
  const client = new StageWhisperClient(opts.apiUrl, "", "");
@@ -65,13 +97,33 @@ export default definePluginEntry({
65
97
  };
66
98
  (cfg as Record<string, unknown>)["channels"] = channels;
67
99
 
100
+ if (opts.enableResponses) {
101
+ const gw = ((cfg as Record<string, unknown>)["gateway"] ?? {}) as Record<string, unknown>;
102
+ const gwAuth = (gw["auth"] as Record<string, unknown>) ?? {};
103
+ const authMode = gwAuth["mode"] as string | undefined;
104
+ const hasToken = typeof gwAuth["token"] === "string" && (gwAuth["token"] as string).length > 0;
105
+
106
+ if (authMode === "none" && !hasToken) {
107
+ console.warn(" ⚠ gateway.auth.mode is 'none' with no token — skipping HTTP API enablement.");
108
+ console.warn(" Set a gateway auth token first for reasoning to work.\n");
109
+ } else {
110
+ const http = (gw["http"] ?? {}) as Record<string, unknown>;
111
+ const endpoints = (http["endpoints"] ?? {}) as Record<string, unknown>;
112
+ const responses = (endpoints["responses"] ?? {}) as Record<string, unknown>;
113
+ responses["enabled"] = true;
114
+ endpoints["responses"] = responses;
115
+ http["endpoints"] = endpoints;
116
+ gw["http"] = http;
117
+ (cfg as Record<string, unknown>)["gateway"] = gw;
118
+ }
119
+ }
120
+
68
121
  await api.runtime.config.writeConfigFile(cfg);
69
122
 
70
123
  console.log(
71
124
  `\n✓ Paired with StageWhisper (${result.label})`,
72
125
  );
73
- console.log(" Config saved automatically.\n");
74
- console.log(" Restart the gateway to activate the relay:\n");
126
+ console.log(" Config saved. Restart the gateway to activate:\n");
75
127
  console.log(" openclaw gateway restart\n");
76
128
  } catch (err) {
77
129
  console.error(`\n✗ Pairing failed: ${err}\n`);
@@ -84,7 +136,8 @@ export default definePluginEntry({
84
136
  .description(
85
137
  "Remove StageWhisper pairing (run before `openclaw plugins uninstall`)",
86
138
  )
87
- .action(async () => {
139
+ .option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair")
140
+ .action(async (opts: { keepResponses?: boolean }) => {
88
141
  try {
89
142
  const cfg = await api.runtime.config.loadConfig();
90
143
  const plugins = (cfg as Record<string, unknown>)["plugins"] as Record<string, unknown> ?? {};
@@ -102,6 +155,20 @@ export default definePluginEntry({
102
155
  }
103
156
  }
104
157
 
158
+ if (!opts.keepResponses) {
159
+ const gw = (cfg as Record<string, unknown>)["gateway"] as Record<string, unknown> | undefined;
160
+ const http = gw?.["http"] as Record<string, unknown> | undefined;
161
+ const endpoints = http?.["endpoints"] as Record<string, unknown> | undefined;
162
+ const responses = endpoints?.["responses"] as Record<string, unknown> | undefined;
163
+ if (responses?.["enabled"] === true) {
164
+ delete responses["enabled"];
165
+ if (Object.keys(responses).length === 0 && endpoints) delete endpoints["responses"];
166
+ if (endpoints && Object.keys(endpoints).length === 0 && http) delete http["endpoints"];
167
+ if (http && Object.keys(http).length === 0 && gw) delete gw["http"];
168
+ console.log(" ℹ Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it.");
169
+ }
170
+ }
171
+
105
172
  await api.runtime.config.writeConfigFile(cfg);
106
173
  console.log("\n✓ StageWhisper unpaired.");
107
174
  console.log(" Config cleaned. You can now safely uninstall:\n");
@@ -116,7 +183,7 @@ export default definePluginEntry({
116
183
  .description("Test reasoning capability against the local OpenResponses endpoint")
117
184
  .option("--model <model>", "Model to use (omit to use your configured default)", "openclaw/default")
118
185
  .action(async (opts: { model: string }) => {
119
- const { callOpenResponses } = await import("./src/openresponses.js");
186
+ const { callOpenResponses, isResponsesEndpointEnabled } = await import("./src/openresponses.js");
120
187
  const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
121
188
 
122
189
  const cfg = api.config as Record<string, unknown>;
@@ -124,10 +191,47 @@ export default definePluginEntry({
124
191
  const auth = (gw?.auth as Record<string, unknown>) ?? {};
125
192
  const port = Number(gw?.port) || 18789;
126
193
  const hasToken = typeof auth?.token === "string" && auth.token.length > 0;
127
- console.log(`Gateway: 127.0.0.1:${port}, auth token: ${hasToken ? "present" : "MISSING"}`);
128
- console.log(`Testing reasoning with model: ${modelLabel}`);
194
+ const responsesEnabled = isResponsesEndpointEnabled(api);
195
+
196
+ console.log("Preflight checks:");
197
+ console.log(` Gateway port: ${port}`);
198
+ console.log(` Auth token: ${hasToken ? "✓ present" : "✗ MISSING"}`);
199
+ console.log(` responses.enabled: ${responsesEnabled ? "✓ true" : "✗ false"}`);
200
+
201
+ if (!responsesEnabled) {
202
+ console.warn("\n⚠ responses.enabled is false in the running config.");
203
+ console.warn(" The plugin auto-enables it on startup — restart the gateway if you haven't:");
204
+ console.warn(" openclaw gateway restart\n");
205
+ }
206
+
207
+ if (!hasToken) {
208
+ console.warn("\n⚠ No gateway auth token found — request may be rejected.\n");
209
+ }
210
+
211
+ console.log(`\nTesting reasoning with model: ${modelLabel}`);
129
212
  console.log("Sending test request to local /v1/responses ...");
130
213
 
214
+ const testSchema = {
215
+ type: "object",
216
+ properties: {
217
+ signals: {
218
+ type: "array",
219
+ items: {
220
+ type: "object",
221
+ properties: {
222
+ severity: { type: "string", enum: ["green", "orange", "red"] },
223
+ message: { type: "string" },
224
+ },
225
+ required: ["severity", "message"],
226
+ additionalProperties: false,
227
+ },
228
+ },
229
+ no_signal_reason: { type: "string" },
230
+ },
231
+ required: ["signals", "no_signal_reason"],
232
+ additionalProperties: false,
233
+ };
234
+
131
235
  const start = Date.now();
132
236
  try {
133
237
  const result = await callOpenResponses(api, {
@@ -136,34 +240,14 @@ export default definePluginEntry({
136
240
  transcript: "Candidate: I think we should use Redis for caching.",
137
241
  playbook_guidance: "Evaluate technical decisions",
138
242
  }),
139
- text: {
140
- format: {
141
- type: "json_schema" as const,
142
- name: "reasoning_test",
143
- schema: {
144
- type: "object",
145
- properties: {
146
- signals: {
147
- type: "array",
148
- items: {
149
- type: "object",
150
- properties: {
151
- severity: { type: "string", enum: ["green", "orange", "red"] },
152
- message: { type: "string" },
153
- },
154
- required: ["severity", "message"],
155
- additionalProperties: false,
156
- },
157
- },
158
- no_signal_reason: { type: "string" },
159
- },
160
- required: ["signals", "no_signal_reason"],
161
- additionalProperties: false,
162
- },
163
- strict: true,
164
- },
165
- },
166
- temperature: 0.2,
243
+ instructions: [
244
+ 'You are a structured reasoning engine for the "reasoning_test" task.',
245
+ "You MUST respond with a JSON object conforming to this schema.",
246
+ "Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
247
+ "",
248
+ "JSON Schema:",
249
+ JSON.stringify(testSchema, null, 2),
250
+ ].join("\n"),
167
251
  max_output_tokens: 1024,
168
252
  });
169
253
 
@@ -177,17 +261,18 @@ export default definePluginEntry({
177
261
  }
178
262
 
179
263
  const output = result.output;
180
- const text = Array.isArray(output)
264
+ const msgItem = Array.isArray(output)
181
265
  ? (output.find((o) => o.type === "message") as Record<string, unknown> | undefined)
182
266
  : null;
183
- const textContent = text
184
- ? ((text.content as Array<Record<string, unknown>>)?.find(
267
+ const textContent = msgItem
268
+ ? ((msgItem.content as Array<Record<string, unknown>>)?.find(
185
269
  (c) => c.type === "output_text",
186
270
  )?.text as string | undefined)
187
271
  : null;
188
272
  if (textContent) {
273
+ const cleaned = textContent.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
189
274
  try {
190
- const parsed = JSON.parse(textContent);
275
+ const parsed = JSON.parse(cleaned);
191
276
  console.log(" Schema-valid JSON: ✓");
192
277
  console.log(` Output: ${JSON.stringify(parsed, null, 2)}`);
193
278
  } catch {
@@ -16,6 +16,15 @@ function resolveGatewayConfig(api: OpenClawPluginApi): GatewayConfig {
16
16
  return { url, apiKey: token };
17
17
  }
18
18
 
19
+ export function isResponsesEndpointEnabled(api: OpenClawPluginApi): boolean {
20
+ const cfg = api.config as Record<string, unknown>;
21
+ const gw = (cfg?.gateway as Record<string, unknown>) ?? {};
22
+ const http = (gw?.http as Record<string, unknown>) ?? {};
23
+ const endpoints = (http?.endpoints as Record<string, unknown>) ?? {};
24
+ const responses = (endpoints?.responses as Record<string, unknown>) ?? {};
25
+ return responses?.enabled === true;
26
+ }
27
+
19
28
  export async function callOpenResponses(
20
29
  api: OpenClawPluginApi,
21
30
  requestBody: OpenResponsesCreateResponseRequestBody,
@@ -44,6 +53,13 @@ export async function callOpenResponses(
44
53
 
45
54
  if (!response.ok) {
46
55
  const body = await response.text().catch(() => "");
56
+ if (response.status === 404) {
57
+ throw new OpenResponsesError(
58
+ "POST /v1/responses returned 404 — the OpenResponses HTTP API is most likely disabled. " +
59
+ 'Enable it in OpenClaw config: gateway.http.endpoints.responses.enabled = true, then restart the gateway.',
60
+ response.status,
61
+ );
62
+ }
47
63
  throw new OpenResponsesError(`POST /v1/responses returned ${response.status}: ${body}`, response.status);
48
64
  }
49
65
  return (await response.json()) as OpenResponsesResponseResource;
package/src/reasoning.ts CHANGED
@@ -19,7 +19,6 @@ export async function probeOpenResponses(
19
19
  model: "openclaw/default",
20
20
  input: "Reply with exactly: OK",
21
21
  max_output_tokens: 16,
22
- temperature: 0,
23
22
  };
24
23
 
25
24
  const controller = new AbortController();
@@ -81,6 +80,23 @@ function extractTextOutput(result: OpenResponsesResponseResource): string | null
81
80
  return null;
82
81
  }
83
82
 
83
+ function buildSchemaInstruction(schema: Record<string, unknown>, purpose: string, systemInstruction?: string): string {
84
+ const parts: string[] = [];
85
+ if (systemInstruction) {
86
+ parts.push(systemInstruction);
87
+ parts.push("");
88
+ }
89
+ parts.push(
90
+ `You are a structured reasoning engine for the "${purpose}" task.`,
91
+ "You MUST respond with a JSON object conforming to this schema.",
92
+ "Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
93
+ "",
94
+ "JSON Schema:",
95
+ JSON.stringify(schema, null, 2),
96
+ );
97
+ return parts.join("\n");
98
+ }
99
+
84
100
  export async function executeReasoningJob(
85
101
  api: OpenClawPluginApi,
86
102
  job: ReasoningJobEnvelope,
@@ -109,17 +125,12 @@ export async function executeReasoningJob(
109
125
  const requestBody: OpenResponsesCreateResponseRequestBody = {
110
126
  model,
111
127
  input: JSON.stringify(job.payload),
112
- instructions: (job.payload.system_instruction as string) ?? undefined,
113
- text: {
114
- format: {
115
- type: "json_schema",
116
- name: `reasoning_${job.purpose}`,
117
- schema: job.response_schema,
118
- strict: true,
119
- },
120
- },
128
+ instructions: buildSchemaInstruction(
129
+ job.response_schema,
130
+ job.purpose,
131
+ (job.payload.system_instruction as string) ?? undefined,
132
+ ),
121
133
  max_output_tokens: 4096,
122
- temperature: 0.2,
123
134
  };
124
135
 
125
136
  const controller = new AbortController();
@@ -131,8 +142,9 @@ export async function executeReasoningJob(
131
142
 
132
143
  let parsed: Record<string, unknown> | null = null;
133
144
  if (textOutput) {
145
+ const cleaned = textOutput.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
134
146
  try {
135
- parsed = JSON.parse(textOutput) as Record<string, unknown>;
147
+ parsed = JSON.parse(cleaned) as Record<string, unknown>;
136
148
  } catch {
137
149
  return {
138
150
  job_id: job.job_id,
package/src/service.ts CHANGED
@@ -7,6 +7,7 @@ import type { StageWhisperAccount } from "./channel.js";
7
7
  import { resolveAccount } from "./channel.js";
8
8
  import { createHealthTracker } from "./health.js";
9
9
  import { executeReasoningJob, probeOpenResponses, type ReasoningJobEnvelope } from "./reasoning.js";
10
+ import { isResponsesEndpointEnabled } from "./openresponses.js";
10
11
 
11
12
  const HEARTBEAT_INTERVAL_MS = 30_000;
12
13
  const RECONNECT_BASE_MS = 1_000;
@@ -497,6 +498,13 @@ export function createRelayService(api: OpenClawPluginApi) {
497
498
 
498
499
  state.running = true;
499
500
 
501
+ if (!isResponsesEndpointEnabled(api)) {
502
+ api.logger.warn(
503
+ "gateway.http.endpoints.responses.enabled is not true — reasoning jobs will fail with 404. " +
504
+ "Enable it in config and restart the gateway, or re-pair with: openclaw stagewhisper pair --code <CODE> --enable-responses",
505
+ );
506
+ }
507
+
500
508
  api.logger.info("Probing /v1/responses to verify local AI connectivity...");
501
509
  const probe = await probeOpenResponses(api);
502
510
  if (probe.ok) {