@syengup/friday-channel-next 0.1.36 → 0.1.37

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.
Files changed (120) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  3. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  4. package/dist/src/agent/node-pairing-bridge.js +6 -2
  5. package/dist/src/agent/subagent-registry.js +0 -3
  6. package/dist/src/channel-actions.js +3 -1
  7. package/dist/src/channel.js +0 -2
  8. package/dist/src/collect-message-media-paths.js +10 -1
  9. package/dist/src/friday-session.js +34 -10
  10. package/dist/src/history/normalize-message.js +22 -8
  11. package/dist/src/http/handlers/agent-config.js +10 -4
  12. package/dist/src/http/handlers/cancel.js +4 -2
  13. package/dist/src/http/handlers/device-approve.js +3 -1
  14. package/dist/src/http/handlers/files-download.js +6 -8
  15. package/dist/src/http/handlers/files.js +1 -1
  16. package/dist/src/http/handlers/health.js +18 -4
  17. package/dist/src/http/handlers/history-messages.js +1 -1
  18. package/dist/src/http/handlers/history-sessions.js +5 -3
  19. package/dist/src/http/handlers/messages.js +25 -11
  20. package/dist/src/http/handlers/models-list.js +1 -1
  21. package/dist/src/http/handlers/nodes-approve.js +1 -6
  22. package/dist/src/http/handlers/plugin-info.js +1 -1
  23. package/dist/src/http/server.js +4 -2
  24. package/dist/src/link-preview/og-parse.js +3 -1
  25. package/dist/src/plugin-install-info.js +4 -1
  26. package/dist/src/session/session-manager.js +9 -3
  27. package/dist/src/session-usage-store.js +3 -1
  28. package/dist/src/skills-discovery.d.ts +5 -4
  29. package/dist/src/skills-discovery.js +27 -22
  30. package/dist/src/sse/offline-queue.js +4 -1
  31. package/dist/src/tool-catalog.js +2 -3
  32. package/dist/src/upgrade-runtime.d.ts +1 -1
  33. package/dist/src/version.js +3 -1
  34. package/index.ts +43 -35
  35. package/install.js +131 -43
  36. package/package.json +10 -1
  37. package/src/agent/abort-run.ts +2 -3
  38. package/src/agent/dispatch-bridge.ts +2 -1
  39. package/src/agent/media-bridge.ts +9 -2
  40. package/src/agent/node-pairing-bridge.ts +29 -15
  41. package/src/agent/run-usage-accumulator.ts +4 -2
  42. package/src/agent/subagent-registry.ts +0 -4
  43. package/src/agent-run-context-bridge.ts +3 -1
  44. package/src/channel-actions.test.ts +10 -4
  45. package/src/channel-actions.ts +3 -1
  46. package/src/channel.outbound.test.ts +18 -4
  47. package/src/channel.ts +121 -123
  48. package/src/collect-message-media-paths.ts +15 -6
  49. package/src/config.ts +1 -4
  50. package/src/e2e/agents-list.e2e.test.ts +9 -2
  51. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  52. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  53. package/src/e2e/auto-approve.integration.test.ts +13 -7
  54. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  55. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  56. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  57. package/src/e2e/send-text.e2e.test.ts +11 -2
  58. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  59. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  60. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  61. package/src/e2e/subagent.e2e.test.ts +136 -53
  62. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  63. package/src/friday-session.forward-agent.test.ts +44 -12
  64. package/src/friday-session.ts +44 -20
  65. package/src/history/normalize-message.test.ts +35 -8
  66. package/src/history/normalize-message.ts +24 -12
  67. package/src/history/read-transcript.ts +1 -4
  68. package/src/http/handlers/agent-config.test.ts +10 -3
  69. package/src/http/handlers/agent-config.ts +22 -8
  70. package/src/http/handlers/agents-list.test.ts +1 -5
  71. package/src/http/handlers/cancel.test.ts +12 -3
  72. package/src/http/handlers/cancel.ts +4 -2
  73. package/src/http/handlers/device-approve.test.ts +12 -3
  74. package/src/http/handlers/device-approve.ts +33 -21
  75. package/src/http/handlers/files-download.ts +17 -13
  76. package/src/http/handlers/files.test.ts +8 -2
  77. package/src/http/handlers/files.ts +21 -7
  78. package/src/http/handlers/health.test.ts +43 -11
  79. package/src/http/handlers/health.ts +22 -6
  80. package/src/http/handlers/history-messages.test.ts +51 -9
  81. package/src/http/handlers/history-messages.ts +4 -1
  82. package/src/http/handlers/history-sessions.test.ts +46 -9
  83. package/src/http/handlers/history-sessions.ts +5 -3
  84. package/src/http/handlers/history-set-title.test.ts +14 -5
  85. package/src/http/handlers/link-preview.test.ts +57 -16
  86. package/src/http/handlers/link-preview.ts +4 -1
  87. package/src/http/handlers/messages.test.ts +12 -8
  88. package/src/http/handlers/messages.ts +57 -19
  89. package/src/http/handlers/models-list.ts +14 -8
  90. package/src/http/handlers/nodes-approve.test.ts +15 -4
  91. package/src/http/handlers/nodes-approve.ts +38 -40
  92. package/src/http/handlers/plugin-info.ts +5 -6
  93. package/src/http/handlers/plugin-upgrade.ts +4 -1
  94. package/src/http/handlers/sse.ts +3 -1
  95. package/src/http/server.ts +9 -6
  96. package/src/link-preview/og-parse.test.ts +6 -2
  97. package/src/link-preview/og-parse.ts +10 -3
  98. package/src/link-preview/preview-service.ts +4 -1
  99. package/src/link-preview/ssrf-guard.test.ts +72 -15
  100. package/src/link-preview/ssrf-guard.ts +2 -1
  101. package/src/media-fetch.test.ts +7 -2
  102. package/src/media-fetch.ts +1 -2
  103. package/src/openclaw.d.ts +16 -9
  104. package/src/plugin-install-info.ts +20 -9
  105. package/src/run-metadata.ts +2 -1
  106. package/src/session/session-manager.ts +19 -11
  107. package/src/session-usage-snapshot.ts +3 -1
  108. package/src/session-usage-store.ts +3 -1
  109. package/src/skills-discovery.test.ts +14 -10
  110. package/src/skills-discovery.ts +43 -27
  111. package/src/sse/emitter.test.ts +1 -1
  112. package/src/sse/emitter.ts +9 -3
  113. package/src/sse/offline-queue.ts +17 -8
  114. package/src/test-support/app-simulator.ts +17 -3
  115. package/src/test-support/mock-dispatch.ts +17 -4
  116. package/src/thinking-levels.ts +3 -1
  117. package/src/tool-catalog.ts +16 -7
  118. package/src/upgrade-runtime.ts +4 -2
  119. package/src/version.ts +5 -1
  120. package/tsconfig.json +1 -1
@@ -77,7 +77,10 @@ const log = (
77
77
  logger[level](`[${action}] deviceId=${deviceId}${runPart}${detailPart}`);
78
78
  };
79
79
 
80
- function collectReplyPayloadMediaUrls(pl: { mediaUrls?: string[]; mediaUrl?: string | null }): string[] {
80
+ function collectReplyPayloadMediaUrls(pl: {
81
+ mediaUrls?: string[];
82
+ mediaUrl?: string | null;
83
+ }): string[] {
81
84
  const fromArr = Array.isArray(pl.mediaUrls)
82
85
  ? pl.mediaUrls.filter((u): u is string => typeof u === "string" && u.trim().length > 0)
83
86
  : [];
@@ -159,7 +162,12 @@ export function isCanvasSnapshotMediaPath(url: unknown): boolean {
159
162
  export function translateDeliverPayload(
160
163
  pl: FridayReplyPayload,
161
164
  kind: string,
162
- meta?: { modelName?: string; totalTokens?: number; contextTokensUsed?: number; contextWindowMax?: number },
165
+ meta?: {
166
+ modelName?: string;
167
+ totalTokens?: number;
168
+ contextTokensUsed?: number;
169
+ contextWindowMax?: number;
170
+ },
163
171
  ): Record<string, unknown> {
164
172
  // Strip canvas-snapshot tool-result images before any media resolution (paths here are still the
165
173
  // original `/tmp/openclaw/openclaw-canvas-snapshot-*.jpg` temp paths, not yet copied to friday files).
@@ -210,7 +218,11 @@ export function translateDeliverPayload(
210
218
  if (typeof meta?.modelName === "string" && meta.modelName.trim()) {
211
219
  nextFridayNext.modelName = meta.modelName.trim();
212
220
  }
213
- if (typeof meta?.totalTokens === "number" && Number.isFinite(meta.totalTokens) && meta.totalTokens > 0) {
221
+ if (
222
+ typeof meta?.totalTokens === "number" &&
223
+ Number.isFinite(meta.totalTokens) &&
224
+ meta.totalTokens > 0
225
+ ) {
214
226
  nextFridayNext.totalTokens = Math.floor(meta.totalTokens);
215
227
  }
216
228
  if (
@@ -260,8 +272,10 @@ function scheduleLateFinalMetaPatch(runId: string, attempts = 6): void {
260
272
  sessionKey: route.sessionKey,
261
273
  modelName: meta.modelName ?? null,
262
274
  totalTokens: typeof meta.totalTokens === "number" ? meta.totalTokens : null,
263
- contextTokensUsed: typeof meta.contextTokensUsed === "number" ? meta.contextTokensUsed : null,
264
- contextWindowMax: typeof meta.contextWindowMax === "number" ? meta.contextWindowMax : null,
275
+ contextTokensUsed:
276
+ typeof meta.contextTokensUsed === "number" ? meta.contextTokensUsed : null,
277
+ contextWindowMax:
278
+ typeof meta.contextWindowMax === "number" ? meta.contextWindowMax : null,
265
279
  ts: Date.now(),
266
280
  },
267
281
  },
@@ -298,11 +312,17 @@ function pickMetadataFromMessageLike(message: unknown): {
298
312
  ? usage.totalTokens
299
313
  : undefined) ??
300
314
  (typeof usage?.total === "number" && Number.isFinite(usage.total) ? usage.total : undefined) ??
301
- (typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens) ? usage.total_tokens : undefined);
315
+ (typeof usage?.total_tokens === "number" && Number.isFinite(usage.total_tokens)
316
+ ? usage.total_tokens
317
+ : undefined);
302
318
  const totalFromMessage =
303
- (typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens) ? m.totalTokens : undefined) ??
304
- (typeof m.total_tokens === "number" && Number.isFinite(m.total_tokens) ? m.total_tokens : undefined);
305
- const totalTokens = Math.floor((totalFromUsage ?? totalFromMessage ?? 0));
319
+ (typeof m.totalTokens === "number" && Number.isFinite(m.totalTokens)
320
+ ? m.totalTokens
321
+ : undefined) ??
322
+ (typeof m.total_tokens === "number" && Number.isFinite(m.total_tokens)
323
+ ? m.total_tokens
324
+ : undefined);
325
+ const totalTokens = Math.floor(totalFromUsage ?? totalFromMessage ?? 0);
306
326
 
307
327
  let contextTokensUsed: number | undefined;
308
328
  if (usage) {
@@ -313,8 +333,12 @@ function pickMetadataFromMessageLike(message: unknown): {
313
333
  }
314
334
 
315
335
  const ctxMaxRaw =
316
- (typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow) ? m.contextWindow : undefined) ??
317
- (typeof m.maxContextTokens === "number" && Number.isFinite(m.maxContextTokens) ? m.maxContextTokens : undefined);
336
+ (typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow)
337
+ ? m.contextWindow
338
+ : undefined) ??
339
+ (typeof m.maxContextTokens === "number" && Number.isFinite(m.maxContextTokens)
340
+ ? m.maxContextTokens
341
+ : undefined);
318
342
  const contextWindowMax =
319
343
  typeof ctxMaxRaw === "number" && ctxMaxRaw > 0 ? Math.floor(ctxMaxRaw) : undefined;
320
344
 
@@ -336,9 +360,16 @@ async function resolveRunMetadataFromRuntimeSession(
336
360
  contextTokensUsed?: number;
337
361
  contextWindowMax?: number;
338
362
  } | null> {
339
- const sessionApi = (runtime as unknown as {
340
- subagent?: { getSessionMessages?: (params: { sessionKey: string; limit?: number }) => Promise<{ messages?: unknown[] }> };
341
- }).subagent;
363
+ const sessionApi = (
364
+ runtime as unknown as {
365
+ subagent?: {
366
+ getSessionMessages?: (params: {
367
+ sessionKey: string;
368
+ limit?: number;
369
+ }) => Promise<{ messages?: unknown[] }>;
370
+ };
371
+ }
372
+ ).subagent;
342
373
  if (!sessionApi?.getSessionMessages) return null;
343
374
  try {
344
375
  const response = await sessionApi.getSessionMessages({ sessionKey, limit: 80 });
@@ -373,7 +404,10 @@ export function composeBodyWithMediaRefs(text: string, mediaRefs: string[]): str
373
404
  return trimmed ? `${trimmed}\n\n${mediaRefs.join("\n")}` : mediaRefs.join("\n");
374
405
  }
375
406
 
376
- async function buildBodyForAgentWithAttachments(text: string, attachmentIds: string[]): Promise<string> {
407
+ async function buildBodyForAgentWithAttachments(
408
+ text: string,
409
+ attachmentIds: string[],
410
+ ): Promise<string> {
377
411
  if (attachmentIds.length === 0) return text.trim();
378
412
 
379
413
  const mediaRefs: string[] = [];
@@ -539,7 +573,10 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
539
573
  dispatcherOptions: {
540
574
  deliver: async (pl: any, info: any) => {
541
575
  let meta = getRunMetadata(runId);
542
- if (info.kind.toLowerCase() === "final" && !(meta?.modelName || typeof meta?.totalTokens === "number")) {
576
+ if (
577
+ info.kind.toLowerCase() === "final" &&
578
+ !(meta?.modelName || typeof meta?.totalTokens === "number")
579
+ ) {
543
580
  const resolved = await resolveRunMetadataFromRuntimeSession(runtime, baseSessionKey);
544
581
  if (resolved) {
545
582
  setRunMetadata(runId, resolved);
@@ -602,10 +639,11 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
602
639
  // OpenClaw `pi-embedded-subscribe` gates `streamReasoning` on `typeof onReasoningStream === "function"`.
603
640
  // Without this, `emitReasoningStream` never runs and Friday SSE never sees `stream: "thinking"`.
604
641
  onReasoningStream: async (pl: unknown) => {
605
- const text =
642
+ const rawText =
606
643
  typeof pl === "object" && pl !== null && "text" in pl
607
- ? String((pl as { text?: unknown }).text ?? "")
608
- : "";
644
+ ? (pl as { text?: unknown }).text
645
+ : undefined;
646
+ const text = typeof rawText === "string" ? rawText : "";
609
647
  log("REASONING_STREAM", normalizedDeviceId, runId, `textLen=${text.length}`);
610
648
  },
611
649
  onReasoningEnd: async () => {
@@ -44,7 +44,7 @@ function resolveConfiguredModels(): ResolvedModels {
44
44
  seen.add(modelKey);
45
45
  entries.push({
46
46
  id: modelKey,
47
- name: typeof info?.alias === "string" ? info.alias : meta?.name ?? split.modelId,
47
+ name: typeof info?.alias === "string" ? info.alias : (meta?.name ?? split.modelId),
48
48
  provider: split.provider,
49
49
  reasoning: meta?.reasoning,
50
50
  contextWindow: meta?.contextWindow,
@@ -104,13 +104,19 @@ function resolveConfiguredModels(): ResolvedModels {
104
104
  return { models: entries, defaultModel };
105
105
  }
106
106
 
107
- function buildProviderModelMeta(cfg: Record<string, unknown>): Map<string, {
108
- name?: string;
109
- reasoning?: boolean;
110
- contextWindow?: number;
111
- maxTokens?: number;
112
- }> {
113
- const meta = new Map<string, { name?: string; reasoning?: boolean; contextWindow?: number; maxTokens?: number }>();
107
+ function buildProviderModelMeta(cfg: Record<string, unknown>): Map<
108
+ string,
109
+ {
110
+ name?: string;
111
+ reasoning?: boolean;
112
+ contextWindow?: number;
113
+ maxTokens?: number;
114
+ }
115
+ > {
116
+ const meta = new Map<
117
+ string,
118
+ { name?: string; reasoning?: boolean; contextWindow?: number; maxTokens?: number }
119
+ >();
114
120
  const models = cfg?.models as Record<string, unknown> | undefined;
115
121
  const providers = models?.providers as Record<string, unknown> | undefined;
116
122
  if (providers) {
@@ -24,8 +24,14 @@ class MockRes extends EventEmitter {
24
24
  }
25
25
  }
26
26
 
27
- function mockReq(method: string, headers: Record<string, string> = {}): PassThrough & { method: string; headers: Record<string, string> } {
28
- const stream = new PassThrough() as unknown as PassThrough & { method: string; headers: Record<string, string> };
27
+ function mockReq(
28
+ method: string,
29
+ headers: Record<string, string> = {},
30
+ ): PassThrough & { method: string; headers: Record<string, string> } {
31
+ const stream = new PassThrough() as unknown as PassThrough & {
32
+ method: string;
33
+ headers: Record<string, string>;
34
+ };
29
35
  stream.method = method;
30
36
  stream.headers = headers;
31
37
  return stream;
@@ -92,7 +98,10 @@ describe("handleNodesApprove", () => {
92
98
  });
93
99
 
94
100
  it("returns 404 when listNodePairing returns data without matching node", async () => {
95
- mockList.mockResolvedValueOnce({ pending: [{ requestId: "x", nodeId: "UNMATCHED" }], paired: [] });
101
+ mockList.mockResolvedValueOnce({
102
+ pending: [{ requestId: "x", nodeId: "UNMATCHED" }],
103
+ paired: [],
104
+ });
96
105
 
97
106
  const req = mockReq("POST", { authorization: "Bearer test-token" });
98
107
  const res = new MockRes() as unknown as ServerResponse;
@@ -135,7 +144,9 @@ describe("handleNodesApprove", () => {
135
144
  it("returns 200 with alreadyApproved when node in paired with caps", async () => {
136
145
  mockList.mockResolvedValueOnce({
137
146
  pending: [],
138
- paired: [{ nodeId: NODE_ID, approvedAtMs: 100, caps: ["canvas"], commands: ["canvas.present"] }],
147
+ paired: [
148
+ { nodeId: NODE_ID, approvedAtMs: 100, caps: ["canvas"], commands: ["canvas.present"] },
149
+ ],
139
150
  });
140
151
 
141
152
  const req = mockReq("POST", { authorization: "Bearer test-token" });
@@ -14,15 +14,6 @@ interface PairedNodeEntry {
14
14
  caps?: string[];
15
15
  commands?: string[];
16
16
  }
17
- interface NodePairingList {
18
- pending: PendingNodeEntry[];
19
- paired: PairedNodeEntry[];
20
- }
21
- type ApproveNodePairingResult =
22
- | { requestId: string; node: PairedNodeEntry }
23
- | { status: "forbidden"; missingScope: string }
24
- | null;
25
-
26
17
  export async function handleNodesApprove(
27
18
  req: IncomingMessage,
28
19
  res: ServerResponse,
@@ -62,7 +53,7 @@ export async function handleNodesApprove(
62
53
 
63
54
  const normalizedNodeId = rawNodeId.trim().toUpperCase();
64
55
 
65
- let listData, listNodePairing, approveNodePairing;
56
+ let listData, listNodePairing, approveNodePairing;
66
57
  try {
67
58
  ({ listNodePairing, approveNodePairing } = await loadNodePairingModule());
68
59
  } catch (err) {
@@ -91,12 +82,7 @@ let listData, listNodePairing, approveNodePairing;
91
82
  const requestId = pendingMatch.requestId;
92
83
  log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
93
84
 
94
- const callerScopes = [
95
- "operator.admin",
96
- "operator.pairing",
97
- "operator.read",
98
- "operator.write",
99
- ];
85
+ const callerScopes = ["operator.admin", "operator.pairing", "operator.read", "operator.write"];
100
86
 
101
87
  let approved;
102
88
  try {
@@ -105,10 +91,12 @@ let listData, listNodePairing, approveNodePairing;
105
91
  log.error(`approveNodePairing failed: ${err instanceof Error ? err.message : String(err)}`);
106
92
  res.statusCode = 502;
107
93
  res.setHeader("Content-Type", "application/json");
108
- res.end(JSON.stringify({
109
- error: "Node approval failed",
110
- detail: err instanceof Error ? err.message : "Unknown error",
111
- }));
94
+ res.end(
95
+ JSON.stringify({
96
+ error: "Node approval failed",
97
+ detail: err instanceof Error ? err.message : "Unknown error",
98
+ }),
99
+ );
112
100
  return true;
113
101
  }
114
102
 
@@ -122,18 +110,22 @@ let listData, listNodePairing, approveNodePairing;
122
110
  if ("status" in approved && approved.status === "forbidden") {
123
111
  res.statusCode = 403;
124
112
  res.setHeader("Content-Type", "application/json");
125
- res.end(JSON.stringify({ error: `Node approval forbidden: ${(approved as any).missingScope ?? "unknown"}` }));
113
+ res.end(
114
+ JSON.stringify({ error: `Node approval forbidden: ${approved.missingScope ?? "unknown"}` }),
115
+ );
126
116
  return true;
127
117
  }
128
118
 
129
119
  res.statusCode = 200;
130
120
  res.setHeader("Content-Type", "application/json");
131
- res.end(JSON.stringify({
132
- ok: true,
133
- nodeId: normalizedNodeId,
134
- requestId: (approved as any).requestId,
135
- approvedAtMs: (approved as any).node?.approvedAtMs,
136
- }));
121
+ res.end(
122
+ JSON.stringify({
123
+ ok: true,
124
+ nodeId: normalizedNodeId,
125
+ requestId: approved.requestId,
126
+ approvedAtMs: approved.node?.approvedAtMs,
127
+ }),
128
+ );
137
129
  return true;
138
130
  }
139
131
 
@@ -147,26 +139,32 @@ let listData, listNodePairing, approveNodePairing;
147
139
  const caps = pairedMatch.caps ?? [];
148
140
  const commands = pairedMatch.commands ?? [];
149
141
  if (caps.length > 0 || commands.length > 0) {
150
- log.info(`nodeId=${normalizedNodeId} already paired with caps=${caps.length} commands=${commands.length}`);
142
+ log.info(
143
+ `nodeId=${normalizedNodeId} already paired with caps=${caps.length} commands=${commands.length}`,
144
+ );
151
145
  res.statusCode = 200;
152
146
  res.setHeader("Content-Type", "application/json");
153
- res.end(JSON.stringify({
154
- ok: true,
155
- nodeId: normalizedNodeId,
156
- alreadyApproved: true,
157
- approvedAtMs: pairedMatch.approvedAtMs,
158
- caps,
159
- commands,
160
- }));
147
+ res.end(
148
+ JSON.stringify({
149
+ ok: true,
150
+ nodeId: normalizedNodeId,
151
+ alreadyApproved: true,
152
+ approvedAtMs: pairedMatch.approvedAtMs,
153
+ caps,
154
+ commands,
155
+ }),
156
+ );
161
157
  return true;
162
158
  }
163
159
  }
164
160
 
165
161
  res.statusCode = 404;
166
162
  res.setHeader("Content-Type", "application/json");
167
- res.end(JSON.stringify({
168
- error: "No pending node found for this nodeId",
169
- nodeId: normalizedNodeId,
170
- }));
163
+ res.end(
164
+ JSON.stringify({
165
+ error: "No pending node found for this nodeId",
166
+ nodeId: normalizedNodeId,
167
+ }),
168
+ );
171
169
  return true;
172
170
  }
@@ -1,11 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { extractBearerToken } from "../middleware/auth.js";
3
3
  import { PLUGIN_VERSION } from "../../version.js";
4
- import {
5
- fetchLatestVersion,
6
- getInstallSource,
7
- semverGreater,
8
- } from "../../plugin-install-info.js";
4
+ import { fetchLatestVersion, getInstallSource, semverGreater } from "../../plugin-install-info.js";
9
5
 
10
6
  export interface PluginInfoResult {
11
7
  currentVersion: string;
@@ -17,7 +13,10 @@ export interface PluginInfoResult {
17
13
  upgradable: boolean;
18
14
  }
19
15
 
20
- export async function handlePluginInfo(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
16
+ export async function handlePluginInfo(
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ ): Promise<boolean> {
21
20
  if (req.method !== "GET") {
22
21
  res.statusCode = 405;
23
22
  res.setHeader("Content-Type", "application/json");
@@ -18,7 +18,10 @@ const RESTART_DELAY_MS = 500;
18
18
  * eligible — dev (load.paths / source==="path") installs return 409 to protect
19
19
  * the dev environment from duplicate npm installs.
20
20
  */
21
- export async function handlePluginUpgrade(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
21
+ export async function handlePluginUpgrade(
22
+ req: IncomingMessage,
23
+ res: ServerResponse,
24
+ ): Promise<boolean> {
22
25
  if (req.method !== "POST") {
23
26
  res.statusCode = 405;
24
27
  res.setHeader("Content-Type", "application/json");
@@ -67,7 +67,9 @@ export async function handleSseStream(req: IncomingMessage, res: ServerResponse)
67
67
  const lastEventId = parseLastEventId(req, url);
68
68
  if (lastEventId > 0) sseEmitter.replayBacklog(deviceId, lastEventId);
69
69
 
70
- const config = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
70
+ const config = resolveFridayNextConfig(
71
+ getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
72
+ );
71
73
  const keepalive = setInterval(() => {
72
74
  if (conn.isClosed) {
73
75
  clearInterval(keepalive);
@@ -34,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
34
34
  import { sseEmitter } from "../sse/emitter.js";
35
35
 
36
36
  /** Route matcher - returns the matched handler or null. */
37
- async function handleFridayNextRoute(
38
- req: IncomingMessage,
39
- res: ServerResponse,
40
- ): Promise<boolean> {
37
+ async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
41
38
  const url = new URL(req.url ?? "/", "http://localhost");
42
39
  const pathname = url.pathname;
43
40
  applyCorsHeaders(res);
@@ -79,7 +76,10 @@ async function handleFridayNextRoute(
79
76
  return await handleNodesApprove(req, res);
80
77
  }
81
78
 
82
- if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
79
+ if (
80
+ (req.method === "PUT" || req.method === "GET") &&
81
+ pathname === "/friday-next/sessions/settings"
82
+ ) {
83
83
  return await handleSessionsSettings(req, res);
84
84
  }
85
85
 
@@ -127,7 +127,10 @@ async function handleFridayNextRoute(
127
127
  }
128
128
 
129
129
  // Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
130
- if ((req.method === "PUT" || req.method === "POST") && pathname === "/friday-next/sessions/title") {
130
+ if (
131
+ (req.method === "PUT" || req.method === "POST") &&
132
+ pathname === "/friday-next/sessions/title"
133
+ ) {
131
134
  return await handleHistorySetTitle(req, res);
132
135
  }
133
136
 
@@ -5,7 +5,9 @@ const BASE = "https://example.com/article/42";
5
5
 
6
6
  describe("decodeHtmlEntities", () => {
7
7
  it("decodes named, decimal, and hex entities", () => {
8
- expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe('Tom & Jerry — "fun"');
8
+ expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe(
9
+ 'Tom & Jerry — "fun"',
10
+ );
9
11
  expect(decodeHtmlEntities("&#20013;&#25991;")).toBe("中文");
10
12
  expect(decodeHtmlEntities("&#x27;quoted&#x27;")).toBe("'quoted'");
11
13
  });
@@ -143,7 +145,9 @@ describe("parseOpenGraph", () => {
143
145
  it("extracts a cover image from inline JSON (extensionless, escaped slashes)", () => {
144
146
  const html = `<title>搜索资讯页</title>
145
147
  <script>window.__INFO__={"imgUrl":"http:\\/\\/qqpublic.qpic.cn\\/qq_public_cover\\/0\\/0-2342_op"}</script>`;
146
- expect(parseOpenGraph(html, BASE).imageUrl).toBe("http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op");
148
+ expect(parseOpenGraph(html, BASE).imageUrl).toBe(
149
+ "http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op",
150
+ );
147
151
  });
148
152
 
149
153
  it("standard og tags still win over body/json fallbacks", () => {
@@ -91,7 +91,9 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
91
91
  let metaDescription: string | null = null;
92
92
  for (const match of slice.matchAll(META_TAG_RE)) {
93
93
  const tag = match[0];
94
- const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))?.trim().toLowerCase();
94
+ const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
95
+ ?.trim()
96
+ .toLowerCase();
95
97
  if (!key) continue;
96
98
  const content = attributeValue(tag, "content");
97
99
  if (content == null || !content.trim()) continue;
@@ -140,10 +142,15 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
140
142
  };
141
143
  }
142
144
 
143
- const JSON_LD_RE = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
145
+ const JSON_LD_RE =
146
+ /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
144
147
 
145
148
  /** Extract title/description/image from JSON-LD blocks (schema.org Article/NewsArticle/etc.). */
146
- function parseJsonLd(html: string): { title: string | null; description: string | null; image: string | null } {
149
+ function parseJsonLd(html: string): {
150
+ title: string | null;
151
+ description: string | null;
152
+ image: string | null;
153
+ } {
147
154
  for (const match of html.matchAll(JSON_LD_RE)) {
148
155
  let data: unknown;
149
156
  try {
@@ -155,7 +155,10 @@ async function buildPreview(pageUrl: string): Promise<LinkPreviewResult> {
155
155
  }
156
156
 
157
157
  /** Re-host a favicon: try the parsed `<link rel icon>`, then `<origin>/favicon.ico`. */
158
- async function resolveFavicon(parsedIconUrl: string | null, finalUrl: string): Promise<string | null> {
158
+ async function resolveFavicon(
159
+ parsedIconUrl: string | null,
160
+ finalUrl: string,
161
+ ): Promise<string | null> {
159
162
  const candidates: string[] = [];
160
163
  if (parsedIconUrl) candidates.push(parsedIconUrl);
161
164
  try {