@stagewhisper/stagewhisper 0.60.0 → 0.61.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/dist/index.js CHANGED
@@ -252,8 +252,8 @@ function resolveGatewayConfig(api) {
252
252
  function isResponsesEndpointEnabled(api) {
253
253
  const cfg = api.config;
254
254
  const gw = cfg?.gateway ?? {};
255
- const http = gw?.http ?? {};
256
- const endpoints = http?.endpoints ?? {};
255
+ const http2 = gw?.http ?? {};
256
+ const endpoints = http2?.endpoints ?? {};
257
257
  const responses = endpoints?.responses ?? {};
258
258
  return responses?.enabled === true;
259
259
  }
@@ -431,7 +431,7 @@ function createHasher(hashCons) {
431
431
  hashC.create = () => hashCons();
432
432
  return hashC;
433
433
  }
434
- function randomBytes(bytesLength = 32) {
434
+ function randomBytes2(bytesLength = 32) {
435
435
  if (crypto2 && typeof crypto2.getRandomValues === "function") {
436
436
  return crypto2.getRandomValues(new Uint8Array(bytesLength));
437
437
  }
@@ -2003,7 +2003,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
2003
2003
  });
2004
2004
  const { prehash } = eddsaOpts;
2005
2005
  const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
2006
- const randomBytes3 = eddsaOpts.randomBytes || randomBytes;
2006
+ const randomBytes4 = eddsaOpts.randomBytes || randomBytes2;
2007
2007
  const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
2008
2008
  const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
2009
2009
  _abool2(phflag, "phflag");
@@ -2085,7 +2085,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
2085
2085
  signature: 2 * _size,
2086
2086
  seed: _size
2087
2087
  };
2088
- function randomSecretKey(seed = randomBytes3(lengths.seed)) {
2088
+ function randomSecretKey(seed = randomBytes4(lengths.seed)) {
2089
2089
  return _abytes2(seed, lengths.seed, "seed");
2090
2090
  }
2091
2091
  function keygen(seed) {
@@ -2280,7 +2280,7 @@ function montgomery(curveDef) {
2280
2280
  const is25519 = type === "x25519";
2281
2281
  if (!is25519 && type !== "x448")
2282
2282
  throw new Error("invalid type");
2283
- const randomBytes_ = rand || randomBytes;
2283
+ const randomBytes_ = rand || randomBytes2;
2284
2284
  const montgomeryBits = is25519 ? 255 : 448;
2285
2285
  const fieldLen = is25519 ? 32 : 56;
2286
2286
  const Gu = is25519 ? BigInt(9) : BigInt(5);
@@ -3547,7 +3547,7 @@ var init_cryptoNode2 = __esm({
3547
3547
  });
3548
3548
 
3549
3549
  // node_modules/.pnpm/@noble+ciphers@1.3.0/node_modules/@noble/ciphers/esm/webcrypto.js
3550
- function randomBytes2(bytesLength = 32) {
3550
+ function randomBytes3(bytesLength = 32) {
3551
3551
  if (crypto3 && typeof crypto3.getRandomValues === "function") {
3552
3552
  return crypto3.getRandomValues(new Uint8Array(bytesLength));
3553
3553
  }
@@ -3591,7 +3591,7 @@ function generateUUID() {
3591
3591
  }
3592
3592
  function seal(key, senderRole, sessionId, correlationId, contentType, plaintext) {
3593
3593
  const messageId = generateUUID();
3594
- const nonce = randomBytes2(24);
3594
+ const nonce = randomBytes3(24);
3595
3595
  const metadata = {
3596
3596
  version: ENVELOPE_VERSION,
3597
3597
  sender_role: senderRole,
@@ -3666,7 +3666,7 @@ var init_crypto = __esm({
3666
3666
  this.publicKey = x25519.getPublicKey(secretKey);
3667
3667
  }
3668
3668
  static generate() {
3669
- const secretKey = randomBytes2(32);
3669
+ const secretKey = randomBytes3(32);
3670
3670
  return new _IdentityKeypair(secretKey);
3671
3671
  }
3672
3672
  static fromSecretBytes(bytes) {
@@ -3987,6 +3987,858 @@ var stagewhisperPlugin = {
3987
3987
  }
3988
3988
  };
3989
3989
 
3990
+ // src/http-transport.ts
3991
+ import http from "node:http";
3992
+ import { Buffer as Buffer2 } from "node:buffer";
3993
+ import { timingSafeEqual } from "node:crypto";
3994
+
3995
+ // src/core.ts
3996
+ var TASK_ID_REGEX = /^[0-9a-f-]{36}$/;
3997
+ var LOOPBACK_CALLBACK_URL_REGEX = /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/?$/;
3998
+ var ALLOWED_REASONS = /* @__PURE__ */ new Set([
3999
+ "transcript_chunk",
4000
+ "chat_message",
4001
+ "system_prelude"
4002
+ ]);
4003
+ var MAX_BODY_BYTES = 262144;
4004
+ var MAX_PAYLOAD_TEXT_CHARS = 8e3;
4005
+ var MAX_SESSION_ID_CHARS = 128;
4006
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
4007
+ function isTestTask(task) {
4008
+ return task.action_type === "connectivity_test";
4009
+ }
4010
+ function buildTaskMessage(task) {
4011
+ const lines = [];
4012
+ lines.push(`**${task.title}**`);
4013
+ lines.push("");
4014
+ lines.push(task.request_text);
4015
+ if (task.evidence_payload) {
4016
+ const evidence = task.evidence_payload;
4017
+ if (evidence["transcript_excerpt"]) {
4018
+ lines.push("");
4019
+ lines.push(`Context: ${evidence["transcript_excerpt"]}`);
4020
+ }
4021
+ if (evidence["signal_summary"]) {
4022
+ lines.push(`Signal: ${evidence["signal_summary"]}`);
4023
+ }
4024
+ if (evidence["tone_summary"]) {
4025
+ lines.push(`Tone: ${evidence["tone_summary"]}`);
4026
+ }
4027
+ if (evidence["playbook_label"]) {
4028
+ lines.push(`Playbook: ${evidence["playbook_label"]}`);
4029
+ }
4030
+ }
4031
+ lines.push("");
4032
+ lines.push(`Action type: ${task.action_type}`);
4033
+ lines.push(`StageWhisper task: ${task.id}`);
4034
+ lines.push(`Session: ${task.session_id}`);
4035
+ return lines.join("\n");
4036
+ }
4037
+ async function dispatchTaskToAgent(options) {
4038
+ const { api, task } = options;
4039
+ const peerId = options.sessionPeerId ?? (isTestTask(task) ? `sw-test-${task.id}` : `sw-session-${task.session_id}`);
4040
+ const sessionKey = buildAgentSessionKey({
4041
+ agentId: "default",
4042
+ channel: "stagewhisper",
4043
+ peer: { kind: "direct", id: peerId },
4044
+ dmScope: "per-channel-peer"
4045
+ });
4046
+ const messageContent = buildTaskMessage(task);
4047
+ const result = await api.runtime.subagent.run({
4048
+ sessionKey,
4049
+ message: messageContent,
4050
+ deliver: options.deliver ?? !isTestTask(task),
4051
+ idempotencyKey: options.idempotencyKey ?? `sw-task-${task.id}`
4052
+ });
4053
+ return { runId: result.runId, sessionKey };
4054
+ }
4055
+ function isLoopbackCallbackUrl(url) {
4056
+ return LOOPBACK_CALLBACK_URL_REGEX.test(url);
4057
+ }
4058
+ function isAllowedHostHeader(host) {
4059
+ if (!host) return false;
4060
+ const trimmed = host.trim().toLowerCase();
4061
+ if (!trimmed) return false;
4062
+ const colonIdx = trimmed.lastIndexOf(":");
4063
+ const hostname = colonIdx === -1 ? trimmed : trimmed.slice(0, colonIdx);
4064
+ const port = colonIdx === -1 ? "" : trimmed.slice(colonIdx + 1);
4065
+ if (!ALLOWED_HOSTNAMES.has(hostname)) return false;
4066
+ if (port && !/^\d+$/.test(port)) return false;
4067
+ return true;
4068
+ }
4069
+ function buildChatId(sessionId, reason) {
4070
+ if (reason === "transcript_chunk") return `sw:${sessionId}:reasoning`;
4071
+ if (reason === "chat_message") return `sw:${sessionId}:chat`;
4072
+ return null;
4073
+ }
4074
+ function buildCallbackUrl(baseUrl, taskId) {
4075
+ return `${baseUrl.replace(/\/$/, "")}/tasks/${taskId}`;
4076
+ }
4077
+ function applyPrelude(text, prelude) {
4078
+ if (!prelude) return text;
4079
+ return `[Context: ${prelude}]
4080
+
4081
+ ${text}`;
4082
+ }
4083
+ function validateHttpTaskRequest(body) {
4084
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4085
+ return { ok: false, error: "body must be a JSON object" };
4086
+ }
4087
+ const obj = body;
4088
+ const task_id = obj["task_id"];
4089
+ if (typeof task_id !== "string" || task_id.length === 0) {
4090
+ return { ok: false, error: "task_id must be a non-empty string" };
4091
+ }
4092
+ if (!TASK_ID_REGEX.test(task_id)) {
4093
+ return { ok: false, error: "task_id must match ^[0-9a-f-]{36}$" };
4094
+ }
4095
+ const session_id = obj["session_id"];
4096
+ if (typeof session_id !== "string" || session_id.length === 0) {
4097
+ return { ok: false, error: "session_id must be a non-empty string" };
4098
+ }
4099
+ if (session_id.length > MAX_SESSION_ID_CHARS) {
4100
+ return {
4101
+ ok: false,
4102
+ error: `session_id must be <= ${MAX_SESSION_ID_CHARS} chars`
4103
+ };
4104
+ }
4105
+ const reason = obj["reason"];
4106
+ if (typeof reason !== "string" || reason.length === 0) {
4107
+ return { ok: false, error: "reason must be a non-empty string" };
4108
+ }
4109
+ if (!ALLOWED_REASONS.has(reason)) {
4110
+ return {
4111
+ ok: false,
4112
+ error: `reason must be one of ${Array.from(ALLOWED_REASONS).join(", ")}`
4113
+ };
4114
+ }
4115
+ const payload = obj["payload"];
4116
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
4117
+ return { ok: false, error: "payload must be an object" };
4118
+ }
4119
+ const payloadObj = payload;
4120
+ const text = payloadObj["text"];
4121
+ if (typeof text !== "string") {
4122
+ return { ok: false, error: "payload.text must be a string" };
4123
+ }
4124
+ if (text.length > MAX_PAYLOAD_TEXT_CHARS) {
4125
+ return {
4126
+ ok: false,
4127
+ error: `payload.text must be <= ${MAX_PAYLOAD_TEXT_CHARS} chars`
4128
+ };
4129
+ }
4130
+ let callback;
4131
+ const callbackRaw = obj["callback"];
4132
+ if (callbackRaw !== void 0 && callbackRaw !== null) {
4133
+ if (typeof callbackRaw !== "object" || Array.isArray(callbackRaw)) {
4134
+ return { ok: false, error: "callback must be an object" };
4135
+ }
4136
+ const cbObj = callbackRaw;
4137
+ const cbUrl = cbObj["url"];
4138
+ const cbToken = cbObj["token"];
4139
+ if (typeof cbUrl !== "string" || cbUrl.length === 0) {
4140
+ return { ok: false, error: "callback.url must be a non-empty string" };
4141
+ }
4142
+ if (!isLoopbackCallbackUrl(cbUrl)) {
4143
+ return {
4144
+ ok: false,
4145
+ error: "callback.url must be a loopback base URL (http://127.0.0.1:PORT or http://localhost:PORT) with no path"
4146
+ };
4147
+ }
4148
+ if (typeof cbToken !== "string" || cbToken.length < 16) {
4149
+ return {
4150
+ ok: false,
4151
+ error: "callback.token must be a string of length >= 16"
4152
+ };
4153
+ }
4154
+ callback = { url: cbUrl, token: cbToken };
4155
+ }
4156
+ const chatIdRaw = obj["chat_id"];
4157
+ const chat_id = typeof chatIdRaw === "string" && chatIdRaw.length > 0 ? chatIdRaw : buildChatId(session_id, reason) ?? void 0;
4158
+ const req = {
4159
+ task_id,
4160
+ session_id,
4161
+ reason,
4162
+ ...chat_id ? { chat_id } : {},
4163
+ occurred_at: typeof obj["occurred_at"] === "string" ? obj["occurred_at"] : void 0,
4164
+ payload: {
4165
+ text,
4166
+ ts_start_ms: typeof payloadObj["ts_start_ms"] === "number" ? payloadObj["ts_start_ms"] : void 0,
4167
+ ts_end_ms: typeof payloadObj["ts_end_ms"] === "number" ? payloadObj["ts_end_ms"] : void 0,
4168
+ is_final: typeof payloadObj["is_final"] === "boolean" ? payloadObj["is_final"] : void 0,
4169
+ user_message_id: typeof payloadObj["user_message_id"] === "string" ? payloadObj["user_message_id"] : void 0
4170
+ },
4171
+ ...callback ? { callback } : {}
4172
+ };
4173
+ return { ok: true, req };
4174
+ }
4175
+ function httpTaskRequestToTaskPayload(req, overrides) {
4176
+ if (req.reason === "system_prelude") return null;
4177
+ const text = overrides?.text ?? req.payload.text;
4178
+ const evidence = {
4179
+ transcript_excerpt: text
4180
+ };
4181
+ if (typeof req.payload.ts_start_ms === "number") {
4182
+ evidence["ts_start_ms"] = req.payload.ts_start_ms;
4183
+ }
4184
+ if (typeof req.payload.ts_end_ms === "number") {
4185
+ evidence["ts_end_ms"] = req.payload.ts_end_ms;
4186
+ }
4187
+ if (typeof req.payload.is_final === "boolean") {
4188
+ evidence["is_final"] = req.payload.is_final;
4189
+ }
4190
+ if (typeof req.payload.user_message_id === "string") {
4191
+ evidence["user_message_id"] = req.payload.user_message_id;
4192
+ }
4193
+ if (req.chat_id) {
4194
+ evidence["chat_id"] = req.chat_id;
4195
+ }
4196
+ const isChat = req.reason === "chat_message";
4197
+ return {
4198
+ id: req.task_id,
4199
+ session_id: req.session_id,
4200
+ title: isChat ? `Chat message in session ${req.session_id}` : `Transcript chunk from session ${req.session_id}`,
4201
+ request_text: text,
4202
+ action_type: req.reason,
4203
+ status: "delivered",
4204
+ evidence_payload: evidence,
4205
+ created_at: req.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString()
4206
+ };
4207
+ }
4208
+
4209
+ // src/http-transport.ts
4210
+ var IDEMPOTENCY_MAX_SIZE = 1024;
4211
+ var IDEMPOTENCY_TTL_MS = 5 * 60 * 1e3;
4212
+ var CALLBACK_TIMEOUT_MS = 5e3;
4213
+ var CALLBACK_MAX_ATTEMPTS = 4;
4214
+ var CALLBACK_RETRY_BASE_MS = 250;
4215
+ var SUBAGENT_WAIT_TIMEOUT_MS = 12e4;
4216
+ var INCOMING_PATH = "/v1/incoming";
4217
+ var PING_PATH = "/v1/ping";
4218
+ function constantTimeTokenEqual(provided, expected) {
4219
+ const a = Buffer2.from(provided);
4220
+ const b = Buffer2.from(expected);
4221
+ if (a.length !== b.length) {
4222
+ const sink = Buffer2.alloc(Math.max(a.length, b.length));
4223
+ timingSafeEqual(sink, sink);
4224
+ return false;
4225
+ }
4226
+ return timingSafeEqual(a, b);
4227
+ }
4228
+ function extractBearerToken(header) {
4229
+ if (!header) return null;
4230
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
4231
+ if (!match) return null;
4232
+ return match[1]?.trim() ?? null;
4233
+ }
4234
+ function isLoopbackAddress(addr) {
4235
+ if (!addr) return false;
4236
+ if (addr === "127.0.0.1" || addr === "::1") return true;
4237
+ if (addr === "::ffff:127.0.0.1") return true;
4238
+ return false;
4239
+ }
4240
+ function readBody(req, maxBytes) {
4241
+ return new Promise((resolve, reject) => {
4242
+ const chunks = [];
4243
+ let total = 0;
4244
+ req.on("data", (chunk) => {
4245
+ total += chunk.length;
4246
+ if (total > maxBytes) {
4247
+ reject(new Error("body_too_large"));
4248
+ req.destroy();
4249
+ return;
4250
+ }
4251
+ chunks.push(chunk);
4252
+ });
4253
+ req.on("end", () => {
4254
+ resolve(Buffer2.concat(chunks).toString("utf8"));
4255
+ });
4256
+ req.on("error", (err) => reject(err));
4257
+ });
4258
+ }
4259
+ function jsonResponse(status, body) {
4260
+ const serialized = JSON.stringify(body);
4261
+ return {
4262
+ status,
4263
+ headers: {
4264
+ "Content-Type": "application/json",
4265
+ "Content-Length": String(Buffer2.byteLength(serialized))
4266
+ },
4267
+ body: serialized
4268
+ };
4269
+ }
4270
+ function extractContentFromMessage(msg) {
4271
+ const content = msg["content"];
4272
+ if (typeof content === "string") return content;
4273
+ if (Array.isArray(content)) {
4274
+ for (const part of content) {
4275
+ if (typeof part === "object" && part !== null && part["type"] === "text" && typeof part["text"] === "string") {
4276
+ return part["text"];
4277
+ }
4278
+ }
4279
+ }
4280
+ return null;
4281
+ }
4282
+ function createHttpTransport(options) {
4283
+ const { api } = options;
4284
+ const host = options.host ?? "127.0.0.1";
4285
+ const port = options.port ?? 8765;
4286
+ const token = options.token;
4287
+ const callbackFetch = options.callbackFetch ?? fetch;
4288
+ if (!token || token.length < 16) {
4289
+ throw new Error("http-transport: token must be at least 16 characters");
4290
+ }
4291
+ const idempotency = /* @__PURE__ */ new Map();
4292
+ const inflight = /* @__PURE__ */ new Set();
4293
+ const chatTasks = /* @__PURE__ */ new Map();
4294
+ const pendingPreludes = /* @__PURE__ */ new Map();
4295
+ function rememberResult(taskId, status, body) {
4296
+ idempotency.set(taskId, {
4297
+ status,
4298
+ body,
4299
+ expiresAt: Date.now() + IDEMPOTENCY_TTL_MS
4300
+ });
4301
+ while (idempotency.size > IDEMPOTENCY_MAX_SIZE) {
4302
+ const oldestKey = idempotency.keys().next().value;
4303
+ if (oldestKey === void 0) break;
4304
+ idempotency.delete(oldestKey);
4305
+ }
4306
+ }
4307
+ function evictExpired() {
4308
+ const now = Date.now();
4309
+ for (const [key, entry] of idempotency) {
4310
+ if (entry.expiresAt <= now) idempotency.delete(key);
4311
+ }
4312
+ }
4313
+ function recordTerminalCallback(taskId, callback, body) {
4314
+ const entry = idempotency.get(taskId);
4315
+ if (!entry) return;
4316
+ entry.terminal = { callback, body };
4317
+ }
4318
+ function consumePrelude(sessionId) {
4319
+ const prelude = pendingPreludes.get(sessionId);
4320
+ if (prelude !== void 0) pendingPreludes.delete(sessionId);
4321
+ return prelude;
4322
+ }
4323
+ async function extractReplyForChatTask(sessionKey, taskId, userMessageId, maxAttempts = 3) {
4324
+ const markers = [`StageWhisper task: ${taskId}`];
4325
+ if (userMessageId) markers.push(`[StageWhisper chat: ${userMessageId}]`);
4326
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4327
+ if (attempt > 0) {
4328
+ await new Promise((r) => setTimeout(r, 1e3));
4329
+ }
4330
+ const session = await api.runtime.subagent.getSessionMessages({
4331
+ sessionKey,
4332
+ limit: 50
4333
+ });
4334
+ const messages = session.messages;
4335
+ for (let i = 0; i < messages.length; i++) {
4336
+ const msg = messages[i];
4337
+ if (msg["role"] !== "user") continue;
4338
+ const text = extractContentFromMessage(msg) ?? "";
4339
+ if (!markers.some((m) => text.includes(m))) continue;
4340
+ for (let j = i + 1; j < messages.length; j++) {
4341
+ const reply = messages[j];
4342
+ const role = reply["role"];
4343
+ if (role === "assistant" || role === "model") {
4344
+ return extractContentFromMessage(reply);
4345
+ }
4346
+ if (role === "user") break;
4347
+ }
4348
+ }
4349
+ }
4350
+ return null;
4351
+ }
4352
+ async function postCallback(callback, taskId, body) {
4353
+ const url = buildCallbackUrl(callback.url, taskId);
4354
+ const serialized = JSON.stringify(body);
4355
+ let lastError = null;
4356
+ for (let attempt = 0; attempt < CALLBACK_MAX_ATTEMPTS; attempt++) {
4357
+ const controller = new AbortController();
4358
+ const timeoutId = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT_MS);
4359
+ try {
4360
+ const res = await callbackFetch(url, {
4361
+ method: "POST",
4362
+ headers: {
4363
+ "Content-Type": "application/json",
4364
+ Authorization: `Bearer ${callback.token}`
4365
+ },
4366
+ body: serialized,
4367
+ signal: controller.signal
4368
+ });
4369
+ clearTimeout(timeoutId);
4370
+ if (res.ok) return;
4371
+ lastError = new Error(`callback returned status ${res.status}`);
4372
+ if (res.status >= 400 && res.status < 500) {
4373
+ throw lastError;
4374
+ }
4375
+ } catch (err) {
4376
+ clearTimeout(timeoutId);
4377
+ lastError = err;
4378
+ }
4379
+ if (attempt + 1 < CALLBACK_MAX_ATTEMPTS) {
4380
+ await new Promise(
4381
+ (r) => setTimeout(r, CALLBACK_RETRY_BASE_MS * 2 ** attempt)
4382
+ );
4383
+ }
4384
+ }
4385
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "callback_failed"));
4386
+ }
4387
+ async function redeliverTerminalCallback(record, taskId) {
4388
+ try {
4389
+ await postCallback(record.callback, taskId, record.body);
4390
+ api.logger.info(
4391
+ `[stagewhisper-http] redelivered terminal callback for ${taskId}`
4392
+ );
4393
+ } catch (err) {
4394
+ const errMsg = err instanceof Error ? err.message : String(err);
4395
+ api.logger.error(
4396
+ `[stagewhisper-http] terminal callback redelivery failed for ${taskId}: ${errMsg}`
4397
+ );
4398
+ }
4399
+ }
4400
+ async function runReplyTaskAsync(taskRequest, kind) {
4401
+ const callback = taskRequest.callback;
4402
+ if (!callback) {
4403
+ api.logger.error(
4404
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} missing callback after validation`
4405
+ );
4406
+ return;
4407
+ }
4408
+ const userMessageId = taskRequest.payload.user_message_id;
4409
+ const effectiveText = applyPrelude(
4410
+ taskRequest.payload.text,
4411
+ consumePrelude(taskRequest.session_id)
4412
+ );
4413
+ const taskPayload = httpTaskRequestToTaskPayload(taskRequest, {
4414
+ text: effectiveText
4415
+ });
4416
+ if (!taskPayload) {
4417
+ api.logger.error(
4418
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no payload`
4419
+ );
4420
+ return;
4421
+ }
4422
+ const peerId = `sw:${taskRequest.session_id}:${kind}`;
4423
+ const sessionKey = buildAgentSessionKey({
4424
+ agentId: "default",
4425
+ channel: "stagewhisper",
4426
+ peer: { kind: "direct", id: peerId },
4427
+ dmScope: "per-channel-peer"
4428
+ });
4429
+ const chatIdReason = kind === "chat" ? "chat_message" : "transcript_chunk";
4430
+ const chatId = taskRequest.chat_id ?? buildChatId(taskRequest.session_id, chatIdReason);
4431
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
4432
+ const sendCallback = async (body) => {
4433
+ recordTerminalCallback(taskRequest.task_id, callback, body);
4434
+ try {
4435
+ await postCallback(callback, taskRequest.task_id, body);
4436
+ } catch (err) {
4437
+ const errMsg = err instanceof Error ? err.message : String(err);
4438
+ api.logger.error(
4439
+ `[stagewhisper-http] callback POST failed for ${taskRequest.task_id}: ${errMsg}`
4440
+ );
4441
+ }
4442
+ };
4443
+ try {
4444
+ const dispatch = await dispatchTaskToAgent({
4445
+ api,
4446
+ task: taskPayload,
4447
+ deliver: true,
4448
+ idempotencyKey: `sw-http-task-${taskRequest.task_id}`,
4449
+ sessionPeerId: peerId
4450
+ });
4451
+ api.logger.info(
4452
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} dispatched (runId: ${dispatch.runId})`
4453
+ );
4454
+ const waitResult = await api.runtime.subagent.waitForRun({
4455
+ runId: dispatch.runId,
4456
+ timeoutMs: SUBAGENT_WAIT_TIMEOUT_MS
4457
+ });
4458
+ if (waitResult.status === "ok") {
4459
+ const reply = await extractReplyForChatTask(
4460
+ sessionKey,
4461
+ taskRequest.task_id,
4462
+ userMessageId
4463
+ );
4464
+ if (reply) {
4465
+ await sendCallback({
4466
+ task_id: taskRequest.task_id,
4467
+ session_id: taskRequest.session_id,
4468
+ chat_id: chatId,
4469
+ user_message_id: userMessageId ?? null,
4470
+ status: "completed",
4471
+ reply_text: reply,
4472
+ occurred_at: occurredAt
4473
+ });
4474
+ api.logger.info(
4475
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} callback delivered`
4476
+ );
4477
+ } else if (kind === "reasoning") {
4478
+ await sendCallback({
4479
+ task_id: taskRequest.task_id,
4480
+ session_id: taskRequest.session_id,
4481
+ chat_id: chatId,
4482
+ user_message_id: userMessageId ?? null,
4483
+ status: "silent",
4484
+ occurred_at: occurredAt
4485
+ });
4486
+ api.logger.info(
4487
+ `[stagewhisper-http] reasoning task ${taskRequest.task_id} produced no signal (silent)`
4488
+ );
4489
+ } else {
4490
+ await sendCallback({
4491
+ task_id: taskRequest.task_id,
4492
+ session_id: taskRequest.session_id,
4493
+ chat_id: chatId,
4494
+ user_message_id: userMessageId ?? null,
4495
+ status: "errored",
4496
+ error_code: "no_reply",
4497
+ error_message: "Agent run produced no reply",
4498
+ occurred_at: occurredAt
4499
+ });
4500
+ api.logger.warn(
4501
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} produced no reply`
4502
+ );
4503
+ }
4504
+ } else {
4505
+ await sendCallback({
4506
+ task_id: taskRequest.task_id,
4507
+ session_id: taskRequest.session_id,
4508
+ chat_id: chatId,
4509
+ user_message_id: userMessageId ?? null,
4510
+ status: "errored",
4511
+ error_code: "agent_error",
4512
+ error_message: waitResult.error ?? `agent run ${waitResult.status}`,
4513
+ occurred_at: occurredAt
4514
+ });
4515
+ api.logger.error(
4516
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} agent failed: ${waitResult.error}`
4517
+ );
4518
+ }
4519
+ } catch (err) {
4520
+ const errMsg = err instanceof Error ? err.message : String(err);
4521
+ api.logger.error(
4522
+ `[stagewhisper-http] ${kind} task ${taskRequest.task_id} threw: ${errMsg}`
4523
+ );
4524
+ await sendCallback({
4525
+ task_id: taskRequest.task_id,
4526
+ session_id: taskRequest.session_id,
4527
+ chat_id: chatId,
4528
+ user_message_id: userMessageId ?? null,
4529
+ status: "errored",
4530
+ error_code: "execution_error",
4531
+ error_message: errMsg,
4532
+ occurred_at: occurredAt
4533
+ });
4534
+ } finally {
4535
+ inflight.delete(taskRequest.task_id);
4536
+ }
4537
+ }
4538
+ async function handleChatMessageRequest(taskReq) {
4539
+ if (!taskReq.callback) {
4540
+ return jsonResponse(400, {
4541
+ error: "chat_message requires callback {url, token}"
4542
+ });
4543
+ }
4544
+ const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
4545
+ const response = {
4546
+ status: 202,
4547
+ headers: {
4548
+ "Content-Type": "application/json",
4549
+ "Content-Length": String(Buffer2.byteLength(ackBody))
4550
+ },
4551
+ body: ackBody
4552
+ };
4553
+ rememberResult(taskReq.task_id, response.status, response.body);
4554
+ const task = runReplyTaskAsync(taskReq, "chat");
4555
+ chatTasks.set(taskReq.task_id, task);
4556
+ void task.finally(() => {
4557
+ if (chatTasks.get(taskReq.task_id) === task) {
4558
+ chatTasks.delete(taskReq.task_id);
4559
+ }
4560
+ });
4561
+ return response;
4562
+ }
4563
+ async function handleTranscriptChunkRequest(taskReq) {
4564
+ if (taskReq.payload.is_final !== true) {
4565
+ const response2 = jsonResponse(202, {
4566
+ status: "accepted",
4567
+ task_id: taskReq.task_id,
4568
+ dispatched: false
4569
+ });
4570
+ rememberResult(taskReq.task_id, response2.status, response2.body);
4571
+ inflight.delete(taskReq.task_id);
4572
+ return response2;
4573
+ }
4574
+ if (!taskReq.callback) {
4575
+ try {
4576
+ const effectiveText = applyPrelude(
4577
+ taskReq.payload.text,
4578
+ consumePrelude(taskReq.session_id)
4579
+ );
4580
+ const taskPayload = httpTaskRequestToTaskPayload(taskReq, {
4581
+ text: effectiveText
4582
+ });
4583
+ if (!taskPayload) {
4584
+ return jsonResponse(400, { error: "invalid_payload_for_reason" });
4585
+ }
4586
+ const reasoningPeerId = `sw:${taskReq.session_id}:reasoning`;
4587
+ const dispatch = await dispatchTaskToAgent({
4588
+ api,
4589
+ task: taskPayload,
4590
+ deliver: true,
4591
+ idempotencyKey: `sw-http-task-${taskReq.task_id}`,
4592
+ sessionPeerId: reasoningPeerId
4593
+ });
4594
+ api.logger.info(
4595
+ `[stagewhisper-http] dispatched one-way task ${taskReq.task_id} (runId: ${dispatch.runId})`
4596
+ );
4597
+ const response2 = jsonResponse(200, {
4598
+ status: "accepted",
4599
+ task_id: taskReq.task_id,
4600
+ run_id: dispatch.runId,
4601
+ dispatched: true
4602
+ });
4603
+ rememberResult(taskReq.task_id, response2.status, response2.body);
4604
+ return response2;
4605
+ } catch (err) {
4606
+ const errMsg = err instanceof Error ? err.message : String(err);
4607
+ api.logger.error(
4608
+ `[stagewhisper-http] dispatch failed for ${taskReq.task_id}: ${errMsg}`
4609
+ );
4610
+ return jsonResponse(503, { error: "dispatch_failed", detail: errMsg });
4611
+ } finally {
4612
+ inflight.delete(taskReq.task_id);
4613
+ }
4614
+ }
4615
+ const ackBody = JSON.stringify({ status: "accepted", task_id: taskReq.task_id });
4616
+ const response = {
4617
+ status: 202,
4618
+ headers: {
4619
+ "Content-Type": "application/json",
4620
+ "Content-Length": String(Buffer2.byteLength(ackBody))
4621
+ },
4622
+ body: ackBody
4623
+ };
4624
+ rememberResult(taskReq.task_id, response.status, response.body);
4625
+ const task = runReplyTaskAsync(taskReq, "reasoning");
4626
+ chatTasks.set(taskReq.task_id, task);
4627
+ void task.finally(() => {
4628
+ if (chatTasks.get(taskReq.task_id) === task) {
4629
+ chatTasks.delete(taskReq.task_id);
4630
+ }
4631
+ });
4632
+ return response;
4633
+ }
4634
+ function handleSystemPreludeRequest(taskReq) {
4635
+ pendingPreludes.set(taskReq.session_id, taskReq.payload.text);
4636
+ const response = jsonResponse(202, {
4637
+ status: "accepted",
4638
+ task_id: taskReq.task_id
4639
+ });
4640
+ rememberResult(taskReq.task_id, response.status, response.body);
4641
+ inflight.delete(taskReq.task_id);
4642
+ return response;
4643
+ }
4644
+ async function dispatchRoute(req) {
4645
+ if (!isLoopbackAddress(req.remoteAddress)) {
4646
+ api.logger.warn(
4647
+ `[stagewhisper-http] rejecting non-loopback connection from ${req.remoteAddress}`
4648
+ );
4649
+ return jsonResponse(403, { error: "non_loopback_rejected" });
4650
+ }
4651
+ if (!isAllowedHostHeader(req.host)) {
4652
+ api.logger.warn(
4653
+ `[stagewhisper-http] rejecting disallowed Host header: ${req.host ?? "<missing>"}`
4654
+ );
4655
+ return jsonResponse(403, { error: "invalid_host" });
4656
+ }
4657
+ const providedToken = extractBearerToken(req.authorization);
4658
+ if (!providedToken || !constantTimeTokenEqual(providedToken, token)) {
4659
+ return jsonResponse(401, { error: "invalid_token" });
4660
+ }
4661
+ if (req.method === "POST" && req.url === PING_PATH) {
4662
+ return jsonResponse(200, { ok: true });
4663
+ }
4664
+ if (req.method === "POST" && req.url === INCOMING_PATH) {
4665
+ let parsed;
4666
+ try {
4667
+ parsed = req.body.length === 0 ? {} : JSON.parse(req.body);
4668
+ } catch (err) {
4669
+ return jsonResponse(400, {
4670
+ error: `invalid_json: ${err instanceof Error ? err.message : String(err)}`
4671
+ });
4672
+ }
4673
+ const validation = validateHttpTaskRequest(parsed);
4674
+ if (!validation.ok) {
4675
+ return jsonResponse(400, { error: validation.error });
4676
+ }
4677
+ const taskReq = validation.req;
4678
+ evictExpired();
4679
+ const cached = idempotency.get(taskReq.task_id);
4680
+ if (cached && cached.expiresAt > Date.now()) {
4681
+ if (cached.terminal) {
4682
+ const terminal = cached.terminal;
4683
+ const redelivery = redeliverTerminalCallback(terminal, taskReq.task_id);
4684
+ chatTasks.set(taskReq.task_id, redelivery);
4685
+ void redelivery.finally(() => {
4686
+ if (chatTasks.get(taskReq.task_id) === redelivery) {
4687
+ chatTasks.delete(taskReq.task_id);
4688
+ }
4689
+ });
4690
+ }
4691
+ return {
4692
+ status: cached.status,
4693
+ headers: {
4694
+ "Content-Type": "application/json",
4695
+ "Content-Length": String(Buffer2.byteLength(cached.body)),
4696
+ "X-Idempotent-Replay": "true"
4697
+ },
4698
+ body: cached.body
4699
+ };
4700
+ }
4701
+ if (inflight.has(taskReq.task_id)) {
4702
+ return jsonResponse(503, { error: "task_in_flight" });
4703
+ }
4704
+ inflight.add(taskReq.task_id);
4705
+ if (taskReq.reason === "chat_message") {
4706
+ return handleChatMessageRequest(taskReq);
4707
+ }
4708
+ if (taskReq.reason === "system_prelude") {
4709
+ return handleSystemPreludeRequest(taskReq);
4710
+ }
4711
+ return handleTranscriptChunkRequest(taskReq);
4712
+ }
4713
+ return jsonResponse(404, { error: "not_found" });
4714
+ }
4715
+ async function handleRealRequest(req, res) {
4716
+ const method = req.method ?? "";
4717
+ const url = req.url ?? "";
4718
+ const authHeader = req.headers["authorization"];
4719
+ const authorization = Array.isArray(authHeader) ? authHeader[0] : authHeader;
4720
+ const hostHeader = req.headers["host"];
4721
+ const hostValue = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
4722
+ const remoteAddress = req.socket.remoteAddress;
4723
+ let body = "";
4724
+ if (method === "POST") {
4725
+ try {
4726
+ body = await readBody(req, MAX_BODY_BYTES);
4727
+ } catch (err) {
4728
+ const msg = err instanceof Error ? err.message : String(err);
4729
+ const response = msg === "body_too_large" ? jsonResponse(413, { error: "body_too_large" }) : jsonResponse(400, { error: `invalid_body: ${msg}` });
4730
+ writeResponse(res, response);
4731
+ return;
4732
+ }
4733
+ }
4734
+ const result = await dispatchRoute({
4735
+ method,
4736
+ url,
4737
+ authorization,
4738
+ host: hostValue,
4739
+ remoteAddress,
4740
+ body
4741
+ });
4742
+ writeResponse(res, result);
4743
+ }
4744
+ function writeResponse(res, response) {
4745
+ res.statusCode = response.status;
4746
+ for (const [key, value] of Object.entries(response.headers)) {
4747
+ res.setHeader(key, value);
4748
+ }
4749
+ res.end(response.body);
4750
+ }
4751
+ let server = null;
4752
+ return {
4753
+ async start() {
4754
+ if (server) return;
4755
+ const srv = http.createServer((req, res) => {
4756
+ handleRealRequest(req, res).catch((err) => {
4757
+ api.logger.error(`[stagewhisper-http] unhandled error: ${err}`);
4758
+ if (!res.headersSent) {
4759
+ try {
4760
+ writeResponse(res, jsonResponse(500, { error: "internal_error" }));
4761
+ } catch {
4762
+ }
4763
+ }
4764
+ });
4765
+ });
4766
+ server = srv;
4767
+ await new Promise((resolve, reject) => {
4768
+ const onError = (err) => {
4769
+ srv.off("listening", onListening);
4770
+ reject(err);
4771
+ };
4772
+ const onListening = () => {
4773
+ srv.off("error", onError);
4774
+ resolve();
4775
+ };
4776
+ srv.once("error", onError);
4777
+ srv.once("listening", onListening);
4778
+ srv.listen(port, host);
4779
+ });
4780
+ const addr = srv.address();
4781
+ api.logger.info(
4782
+ `[stagewhisper-http] listening on http://${host}:${typeof addr === "object" && addr ? addr.port : port}`
4783
+ );
4784
+ },
4785
+ async stop() {
4786
+ const srv = server;
4787
+ if (!srv) return;
4788
+ server = null;
4789
+ await new Promise((resolve) => {
4790
+ srv.close(() => resolve());
4791
+ srv.closeAllConnections?.();
4792
+ });
4793
+ const pending = Array.from(chatTasks.values());
4794
+ if (pending.length > 0) {
4795
+ await Promise.allSettled(pending);
4796
+ }
4797
+ idempotency.clear();
4798
+ inflight.clear();
4799
+ chatTasks.clear();
4800
+ pendingPreludes.clear();
4801
+ api.logger.info("[stagewhisper-http] stopped");
4802
+ },
4803
+ address() {
4804
+ const addr = server?.address();
4805
+ if (typeof addr === "object" && addr !== null) return addr;
4806
+ return null;
4807
+ },
4808
+ async handleSyntheticRequest(input) {
4809
+ const headers = input.headers ?? {};
4810
+ const authorization = headers["Authorization"] ?? headers["authorization"] ?? void 0;
4811
+ const hostValue = headers["Host"] ?? headers["host"] ?? "127.0.0.1";
4812
+ const remoteAddress = input.remoteAddress ?? "127.0.0.1";
4813
+ const body = input.body ?? "";
4814
+ return dispatchRoute({
4815
+ method: input.method,
4816
+ url: input.url,
4817
+ authorization,
4818
+ host: hostValue,
4819
+ remoteAddress,
4820
+ body
4821
+ });
4822
+ },
4823
+ whenChatTaskSettled(taskId) {
4824
+ return chatTasks.get(taskId);
4825
+ }
4826
+ };
4827
+ }
4828
+
4829
+ // src/pairing.ts
4830
+ import { Buffer as Buffer3 } from "node:buffer";
4831
+ import { randomBytes } from "node:crypto";
4832
+ var PAIRING_CODE_PREFIX = "stagewhisper-pair:v1:";
4833
+ function encodePairingCode(url, token, label) {
4834
+ const payload = JSON.stringify({ url, token, label });
4835
+ const encoded = Buffer3.from(payload, "utf8").toString("base64url");
4836
+ return `${PAIRING_CODE_PREFIX}${encoded}`;
4837
+ }
4838
+ function generateRelayToken() {
4839
+ return randomBytes(32).toString("base64url");
4840
+ }
4841
+
3990
4842
  // src/runtime.ts
3991
4843
  var runtimeStore = createPluginRuntimeStore(
3992
4844
  "StageWhisper plugin runtime not initialized"
@@ -4278,41 +5130,13 @@ function createRelayService(api) {
4278
5130
  };
4279
5131
  }
4280
5132
  }
4281
- function buildTaskMessage(task) {
4282
- const lines = [];
4283
- lines.push(`**${task.title}**`);
4284
- lines.push("");
4285
- lines.push(task.request_text);
4286
- if (task.evidence_payload) {
4287
- const evidence = task.evidence_payload;
4288
- if (evidence["transcript_excerpt"]) {
4289
- lines.push("");
4290
- lines.push(`Context: ${evidence["transcript_excerpt"]}`);
4291
- }
4292
- if (evidence["signal_summary"]) {
4293
- lines.push(`Signal: ${evidence["signal_summary"]}`);
4294
- }
4295
- if (evidence["tone_summary"]) {
4296
- lines.push(`Tone: ${evidence["tone_summary"]}`);
4297
- }
4298
- if (evidence["playbook_label"]) {
4299
- lines.push(`Playbook: ${evidence["playbook_label"]}`);
4300
- }
4301
- }
4302
- lines.push("");
4303
- lines.push(`Action type: ${task.action_type}`);
4304
- lines.push(`StageWhisper task: ${task.id}`);
4305
- lines.push(`Session: ${task.session_id}`);
4306
- return lines.join("\n");
4307
- }
4308
- function isTestTask(task) {
4309
- return task.action_type === "connectivity_test";
4310
- }
5133
+ const buildTaskMessage2 = buildTaskMessage;
5134
+ const isTestTask2 = isTestTask;
4311
5135
  async function updateStatus(client, task, status) {
4312
- if (isTestTask(task)) return;
5136
+ if (isTestTask2(task)) return;
4313
5137
  await client.updateTaskStatus(task.id, status);
4314
5138
  }
4315
- function extractContentFromMessage(msg) {
5139
+ function extractContentFromMessage2(msg) {
4316
5140
  const content = msg["content"];
4317
5141
  if (typeof content === "string") return content;
4318
5142
  if (Array.isArray(content)) {
@@ -4330,7 +5154,7 @@ function createRelayService(api) {
4330
5154
  if (!msg) continue;
4331
5155
  const role = msg["role"];
4332
5156
  if (role !== "assistant" && role !== "model") continue;
4333
- return extractContentFromMessage(msg);
5157
+ return extractContentFromMessage2(msg);
4334
5158
  }
4335
5159
  return null;
4336
5160
  }
@@ -4381,13 +5205,13 @@ function createRelayService(api) {
4381
5205
  for (let i = 0; i < messages.length; i++) {
4382
5206
  const msg = messages[i];
4383
5207
  if (msg["role"] !== "user") continue;
4384
- const text = extractContentFromMessage(msg) ?? "";
5208
+ const text = extractContentFromMessage2(msg) ?? "";
4385
5209
  if (!text.includes(`StageWhisper task: ${taskId}`)) continue;
4386
5210
  for (let j = i + 1; j < messages.length; j++) {
4387
5211
  const reply = messages[j];
4388
5212
  const role = reply["role"];
4389
5213
  if (role === "assistant" || role === "model") {
4390
- return extractContentFromMessage(reply);
5214
+ return extractContentFromMessage2(reply);
4391
5215
  }
4392
5216
  if (role === "user") break;
4393
5217
  }
@@ -4423,13 +5247,13 @@ function createRelayService(api) {
4423
5247
  for (let i = 0; i < messages.length; i++) {
4424
5248
  const msg = messages[i];
4425
5249
  if (msg["role"] !== "user") continue;
4426
- const text = extractContentFromMessage(msg) ?? "";
5250
+ const text = extractContentFromMessage2(msg) ?? "";
4427
5251
  if (!text.includes(marker)) continue;
4428
5252
  for (let j = i + 1; j < messages.length; j++) {
4429
5253
  const next = messages[j];
4430
5254
  const role = next["role"];
4431
5255
  if (role === "assistant" || role === "model") {
4432
- return extractContentFromMessage(next);
5256
+ return extractContentFromMessage2(next);
4433
5257
  }
4434
5258
  if (role === "user") break;
4435
5259
  }
@@ -4505,7 +5329,7 @@ function createRelayService(api) {
4505
5329
  }
4506
5330
  const isByo = !!task.is_byo_encrypted;
4507
5331
  const effectiveTask = isByo ? decryptTaskFields(task, client) : task;
4508
- const messageContent = buildTaskMessage(effectiveTask);
5332
+ const messageContent = buildTaskMessage2(effectiveTask);
4509
5333
  const peerId = `sw-session-${task.session_id}`;
4510
5334
  const sessionKey = buildAgentSessionKey({
4511
5335
  agentId: "default",
@@ -4548,7 +5372,7 @@ function createRelayService(api) {
4548
5372
  });
4549
5373
  }
4550
5374
  async function handleTestTask(task, client) {
4551
- const messageContent = buildTaskMessage(task);
5375
+ const messageContent = buildTaskMessage2(task);
4552
5376
  const sessionKey = buildAgentSessionKey({
4553
5377
  agentId: "default",
4554
5378
  channel: "stagewhisper",
@@ -4581,7 +5405,7 @@ function createRelayService(api) {
4581
5405
  async function handleTask(task, client) {
4582
5406
  api.logger.info(`Received task: ${task.title} (${task.id})`);
4583
5407
  try {
4584
- if (isTestTask(task)) {
5408
+ if (isTestTask2(task)) {
4585
5409
  await handleTestTask(task, client);
4586
5410
  } else {
4587
5411
  await handleNormalTask(task, client);
@@ -5411,20 +6235,73 @@ function createRelayService(api) {
5411
6235
  }
5412
6236
 
5413
6237
  // plugin-main.ts
6238
+ function resolveTransportMode(api) {
6239
+ const pluginCfg = api.pluginConfig ?? {};
6240
+ const fromConfig = pluginCfg["transport"];
6241
+ if (fromConfig === "http" || fromConfig === "sse") return fromConfig;
6242
+ const envOverride = process.env["STAGEWHISPER_TRANSPORT"];
6243
+ if (envOverride === "http" || envOverride === "sse") return envOverride;
6244
+ return "sse";
6245
+ }
6246
+ function resolveHttpTransportToken(pluginCfg, env = process.env) {
6247
+ if (typeof pluginCfg["httpToken"] === "string" && pluginCfg["httpToken"]) {
6248
+ return pluginCfg["httpToken"];
6249
+ }
6250
+ if (env["STAGEWHISPER_HTTP_TOKEN"]) {
6251
+ return env["STAGEWHISPER_HTTP_TOKEN"];
6252
+ }
6253
+ if (typeof pluginCfg["relayToken"] === "string") {
6254
+ return pluginCfg["relayToken"];
6255
+ }
6256
+ return "";
6257
+ }
6258
+ function createHttpTransportService(api) {
6259
+ const pluginCfg = api.pluginConfig ?? {};
6260
+ const host = typeof pluginCfg["httpHost"] === "string" ? pluginCfg["httpHost"] : process.env["STAGEWHISPER_HTTP_HOST"] ?? "127.0.0.1";
6261
+ const portRaw = pluginCfg["httpPort"];
6262
+ const port = typeof portRaw === "number" ? portRaw : typeof portRaw === "string" ? Number(portRaw) : Number(process.env["STAGEWHISPER_HTTP_PORT"]) || 8765;
6263
+ const token = resolveHttpTransportToken(pluginCfg);
6264
+ let transport = null;
6265
+ return {
6266
+ id: "stagewhisper-http-transport",
6267
+ async start(_ctx) {
6268
+ if (!token || token.length < 16) {
6269
+ api.logger.warn(
6270
+ "StageWhisper HTTP transport requires `httpToken` (>=16 chars) in plugin config or STAGEWHISPER_HTTP_TOKEN env var \u2014 listener not started."
6271
+ );
6272
+ return;
6273
+ }
6274
+ transport = createHttpTransport({ api, host, port, token });
6275
+ try {
6276
+ await transport.start();
6277
+ api.logger.info(`StageWhisper HTTP transport started on ${host}:${port}`);
6278
+ } catch (err) {
6279
+ api.logger.error(`StageWhisper HTTP transport failed to start: ${err}`);
6280
+ transport = null;
6281
+ }
6282
+ },
6283
+ async stop(_ctx) {
6284
+ if (transport) {
6285
+ await transport.stop();
6286
+ transport = null;
6287
+ }
6288
+ }
6289
+ };
6290
+ }
5414
6291
  async function ensureResponsesEndpoint(api) {
5415
6292
  try {
5416
6293
  const cfg = await api.runtime.config.loadConfig();
5417
6294
  const gw = cfg["gateway"] ?? {};
5418
- const http = gw["http"] ?? {};
5419
- const endpoints = http["endpoints"] ?? {};
6295
+ const http2 = gw["http"] ?? {};
6296
+ const endpoints = http2["endpoints"] ?? {};
5420
6297
  const responses = endpoints["responses"] ?? {};
5421
6298
  if (responses["enabled"] === true) return;
5422
6299
  const auth = gw["auth"] ?? {};
5423
6300
  if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
5424
6301
  responses["enabled"] = true;
5425
6302
  endpoints["responses"] = responses;
5426
- http["endpoints"] = endpoints;
5427
- gw["http"] = http;
6303
+ http2["endpoints"] = endpoints;
6304
+ gw["http"] = http2;
5428
6305
  cfg["gateway"] = gw;
5429
6306
  await api.runtime.config.writeConfigFile(cfg);
5430
6307
  api.logger.info(
@@ -5442,13 +6319,7 @@ var plugin_main_default = definePluginEntry({
5442
6319
  api.registerCli(
5443
6320
  ({ program }) => {
5444
6321
  const sw = program.command("stagewhisper").description("StageWhisper integration");
5445
- sw.command("pair").description(
5446
- "Pair with StageWhisper using a pairing code from the desktop app"
5447
- ).requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option(
5448
- "--api-url <url>",
5449
- "StageWhisper backend URL",
5450
- "https://api.stagewhisper.io"
5451
- ).option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
6322
+ sw.command("pair").description("Pair with StageWhisper using a pairing code from the desktop app").requiredOption("--code <code>", "Pairing code from Settings \u2192 Assistant").option("--api-url <url>", "StageWhisper backend URL", "https://api.stagewhisper.io").option("--label <label>", "Label for this OpenClaw host", "OpenClaw").option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API").action(
5452
6323
  async (opts) => {
5453
6324
  const { StageWhisperClient: StageWhisperClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
5454
6325
  const { IdentityKeypair: IdentityKeypair2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
@@ -5493,26 +6364,33 @@ var plugin_main_default = definePluginEntry({
5493
6364
  const authMode = gwAuth["mode"];
5494
6365
  const hasToken = typeof gwAuth["token"] === "string" && gwAuth["token"].length > 0;
5495
6366
  if (authMode === "none" && !hasToken) {
5496
- console.warn(" \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement.");
6367
+ console.warn(
6368
+ " \u26A0 gateway.auth.mode is 'none' with no token \u2014 skipping HTTP API enablement."
6369
+ );
5497
6370
  console.warn(" Set a gateway auth token first for reasoning to work.\n");
5498
6371
  } else {
5499
- const http = gw["http"] ?? {};
5500
- const endpoints = http["endpoints"] ?? {};
6372
+ const http2 = gw["http"] ?? {};
6373
+ const endpoints = http2["endpoints"] ?? {};
5501
6374
  const responses = endpoints["responses"] ?? {};
5502
6375
  responses["enabled"] = true;
5503
6376
  endpoints["responses"] = responses;
5504
- http["endpoints"] = endpoints;
5505
- gw["http"] = http;
6377
+ http2["endpoints"] = endpoints;
6378
+ gw["http"] = http2;
5506
6379
  cfg["gateway"] = gw;
5507
6380
  }
5508
6381
  }
5509
6382
  await api.runtime.config.writeConfigFile(cfg);
5510
- console.log(
5511
- `
5512
- \u2713 Paired with StageWhisper (${result.label})`
5513
- );
6383
+ console.log(`
6384
+ \u2713 Paired with StageWhisper (${result.label})`);
5514
6385
  console.log(" Config saved. Restart the gateway to activate:\n");
5515
6386
  console.log(" openclaw gateway restart\n");
6387
+ console.log(
6388
+ " For local HTTP transport (transport: http), the desktop authenticates"
6389
+ );
6390
+ console.log(
6391
+ " against the loopback listener with this same relay token \u2014 no separate"
6392
+ );
6393
+ console.log(" httpToken is required unless you set one explicitly.\n");
5516
6394
  } catch (err) {
5517
6395
  console.error(`
5518
6396
  \u2717 Pairing failed: ${err}
@@ -5521,9 +6399,62 @@ var plugin_main_default = definePluginEntry({
5521
6399
  }
5522
6400
  }
5523
6401
  );
5524
- sw.command("unpair").description(
5525
- "Remove StageWhisper pairing (run before `openclaw plugins uninstall`)"
5526
- ).option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair").action(async (opts) => {
6402
+ sw.command("pair-code").description(
6403
+ "Generate a StageWhisper Free pairing code for the local HTTP transport (no backend)"
6404
+ ).option("--url <url>", "Relay URL StageWhisper Free should reach this gateway at").option("--port <port>", "Loopback port for the HTTP transport listener").option("--label <label>", "Label shown in StageWhisper Free", "OpenClaw").action(async (opts) => {
6405
+ try {
6406
+ const cfg = await api.runtime.config.loadConfig();
6407
+ const plugins = cfg["plugins"] ?? {};
6408
+ const entries = plugins["entries"] ?? {};
6409
+ const swEntry = entries["stagewhisper"] ?? {};
6410
+ const swConfig = swEntry["config"] ?? {};
6411
+ const host = typeof swConfig["httpHost"] === "string" && swConfig["httpHost"] ? swConfig["httpHost"] : "127.0.0.1";
6412
+ const port = opts.port ? Number(opts.port) : typeof swConfig["httpPort"] === "number" ? swConfig["httpPort"] : 8765;
6413
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
6414
+ console.error(`
6415
+ \u2717 Invalid port ${port} (must be 1024-65535)
6416
+ `);
6417
+ process.exit(1);
6418
+ }
6419
+ let token = typeof swConfig["httpToken"] === "string" ? swConfig["httpToken"] : "";
6420
+ if (token.length < 16) {
6421
+ token = generateRelayToken();
6422
+ }
6423
+ const label = (opts.label ?? swConfig["label"] ?? "OpenClaw").trim() || "OpenClaw";
6424
+ swConfig["transport"] = "http";
6425
+ swConfig["httpHost"] = host;
6426
+ swConfig["httpPort"] = port;
6427
+ swConfig["httpToken"] = token;
6428
+ swConfig["label"] = label;
6429
+ swEntry["config"] = swConfig;
6430
+ entries["stagewhisper"] = swEntry;
6431
+ plugins["entries"] = entries;
6432
+ cfg["plugins"] = plugins;
6433
+ await api.runtime.config.writeConfigFile(cfg);
6434
+ const relayUrl = (opts.url ?? "").trim() || `http://${host}:${port}`;
6435
+ const code = encodePairingCode(relayUrl, token, label);
6436
+ console.log("\nStageWhisper Free pairing code:\n");
6437
+ console.log(` ${code}
6438
+ `);
6439
+ console.log("Paste it into StageWhisper Free under Settings \u2192 Connection.");
6440
+ console.log(
6441
+ "Restart the gateway so the HTTP transport listens: openclaw gateway restart"
6442
+ );
6443
+ if (relayUrl.startsWith("http://127.0.0.1")) {
6444
+ console.log(
6445
+ "\nRunning on a remote host? Tunnel the port from the machine running Free:"
6446
+ );
6447
+ console.log(` ssh -L ${port}:127.0.0.1:${port} <this-host>
6448
+ `);
6449
+ }
6450
+ } catch (err) {
6451
+ console.error(`
6452
+ \u2717 Failed to generate pairing code: ${err}
6453
+ `);
6454
+ process.exit(1);
6455
+ }
6456
+ });
6457
+ sw.command("unpair").description("Remove StageWhisper pairing (run before `openclaw plugins uninstall`)").option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair").action(async (opts) => {
5527
6458
  try {
5528
6459
  const cfg = await api.runtime.config.loadConfig();
5529
6460
  const plugins = cfg["plugins"] ?? {};
@@ -5541,15 +6472,19 @@ var plugin_main_default = definePluginEntry({
5541
6472
  }
5542
6473
  if (!opts.keepResponses) {
5543
6474
  const gw = cfg["gateway"];
5544
- const http = gw?.["http"];
5545
- const endpoints = http?.["endpoints"];
6475
+ const http2 = gw?.["http"];
6476
+ const endpoints = http2?.["endpoints"];
5546
6477
  const responses = endpoints?.["responses"];
5547
6478
  if (responses?.["enabled"] === true) {
5548
6479
  delete responses["enabled"];
5549
- if (Object.keys(responses).length === 0 && endpoints) delete endpoints["responses"];
5550
- if (endpoints && Object.keys(endpoints).length === 0 && http) delete http["endpoints"];
5551
- if (http && Object.keys(http).length === 0 && gw) delete gw["http"];
5552
- console.log(" \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it.");
6480
+ if (Object.keys(responses).length === 0 && endpoints)
6481
+ delete endpoints["responses"];
6482
+ if (endpoints && Object.keys(endpoints).length === 0 && http2)
6483
+ delete http2["endpoints"];
6484
+ if (http2 && Object.keys(http2).length === 0 && gw) delete gw["http"];
6485
+ console.log(
6486
+ " \u2139 Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it."
6487
+ );
5553
6488
  }
5554
6489
  }
5555
6490
  await api.runtime.config.writeConfigFile(cfg);
@@ -5563,7 +6498,11 @@ var plugin_main_default = definePluginEntry({
5563
6498
  process.exit(1);
5564
6499
  }
5565
6500
  });
5566
- sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option("--model <model>", "Model to use (omit to use your configured default)", "openclaw/default").action(async (opts) => {
6501
+ sw.command("reasoning-check").description("Test reasoning capability against the local OpenResponses endpoint").option(
6502
+ "--model <model>",
6503
+ "Model to use (omit to use your configured default)",
6504
+ "openclaw/default"
6505
+ ).action(async (opts) => {
5567
6506
  const { callOpenResponses: callOpenResponses2, isResponsesEndpointEnabled: isResponsesEndpointEnabled2 } = await Promise.resolve().then(() => (init_openresponses(), openresponses_exports));
5568
6507
  const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
5569
6508
  const cfg = api.config;
@@ -5578,7 +6517,9 @@ var plugin_main_default = definePluginEntry({
5578
6517
  console.log(` responses.enabled: ${responsesEnabled ? "\u2713 true" : "\u2717 false"}`);
5579
6518
  if (!responsesEnabled) {
5580
6519
  console.warn("\n\u26A0 responses.enabled is false in the running config.");
5581
- console.warn(" The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:");
6520
+ console.warn(
6521
+ " The plugin auto-enables it on startup \u2014 restart the gateway if you haven't:"
6522
+ );
5582
6523
  console.warn(" openclaw gateway restart\n");
5583
6524
  }
5584
6525
  if (!hasToken) {
@@ -5652,9 +6593,7 @@ Testing reasoning with model: ${modelLabel}`);
5652
6593
  } catch (err) {
5653
6594
  const elapsed = Date.now() - start;
5654
6595
  console.error(`\u2717 Reasoning check failed after ${elapsed}ms`);
5655
- console.error(
5656
- ` Error: ${err instanceof Error ? err.message : String(err)}`
5657
- );
6596
+ console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
5658
6597
  process.exitCode = 1;
5659
6598
  }
5660
6599
  });
@@ -5663,9 +6602,7 @@ Testing reasoning with model: ${modelLabel}`);
5663
6602
  const configured = !!(cfg?.["integrationId"] && cfg?.["relayToken"]);
5664
6603
  if (!configured) {
5665
6604
  console.log("\nStageWhisper: not paired\n");
5666
- console.log(
5667
- " Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n"
5668
- );
6605
+ console.log(" Run: openclaw stagewhisper pair --code <CODE> [--api-url <URL>]\n");
5669
6606
  console.log(
5670
6607
  " Get the pairing code from StageWhisper desktop: Settings \u2192 Assistant \u2192 Generate Pairing Code\n"
5671
6608
  );
@@ -5699,10 +6636,16 @@ StageWhisper:`);
5699
6636
  if (api.registrationMode !== "full") return;
5700
6637
  ensureResponsesEndpoint(api);
5701
6638
  setRuntime(api.runtime);
5702
- const service = createRelayService(api);
5703
- api.registerService(service);
6639
+ const transportMode = resolveTransportMode(api);
6640
+ api.logger.info(`StageWhisper transport: ${transportMode}`);
6641
+ if (transportMode === "http") {
6642
+ api.registerService(createHttpTransportService(api));
6643
+ } else {
6644
+ api.registerService(createRelayService(api));
6645
+ }
5704
6646
  }
5705
6647
  });
5706
6648
  export {
5707
- plugin_main_default as default
6649
+ plugin_main_default as default,
6650
+ resolveHttpTransportToken
5708
6651
  };