@syengup/friday-channel-next 0.1.30 → 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 (154) hide show
  1. package/README.md +8 -4
  2. package/dist/index.js +1 -1
  3. package/dist/src/agent/abort-run.d.ts +12 -1
  4. package/dist/src/agent/abort-run.js +24 -9
  5. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  6. package/dist/src/agent/media-bridge.d.ts +8 -1
  7. package/dist/src/agent/media-bridge.js +23 -2
  8. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  9. package/dist/src/agent/node-pairing-bridge.js +6 -2
  10. package/dist/src/agent/subagent-registry.js +0 -3
  11. package/dist/src/agent-forward-runtime.d.ts +15 -0
  12. package/dist/src/agent-forward-runtime.js +2 -0
  13. package/dist/src/agent-id.d.ts +8 -0
  14. package/dist/src/agent-id.js +21 -0
  15. package/dist/src/channel-actions.js +48 -15
  16. package/dist/src/channel.js +22 -3
  17. package/dist/src/collect-message-media-paths.js +10 -1
  18. package/dist/src/friday-session.js +34 -10
  19. package/dist/src/history/normalize-message.js +22 -8
  20. package/dist/src/http/handlers/agent-config.d.ts +27 -0
  21. package/dist/src/http/handlers/agent-config.js +188 -0
  22. package/dist/src/http/handlers/agent-files.d.ts +21 -0
  23. package/dist/src/http/handlers/agent-files.js +137 -0
  24. package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
  25. package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
  26. package/dist/src/http/handlers/agents-list.js +1 -19
  27. package/dist/src/http/handlers/cancel.js +14 -6
  28. package/dist/src/http/handlers/device-approve.js +3 -1
  29. package/dist/src/http/handlers/files-download.js +6 -8
  30. package/dist/src/http/handlers/files.d.ts +16 -0
  31. package/dist/src/http/handlers/files.js +81 -13
  32. package/dist/src/http/handlers/health.js +18 -4
  33. package/dist/src/http/handlers/history-messages.js +1 -1
  34. package/dist/src/http/handlers/history-sessions.js +5 -3
  35. package/dist/src/http/handlers/messages.js +33 -14
  36. package/dist/src/http/handlers/models-list.d.ts +5 -0
  37. package/dist/src/http/handlers/models-list.js +9 -1
  38. package/dist/src/http/handlers/nodes-approve.js +1 -6
  39. package/dist/src/http/handlers/plugin-info.js +1 -1
  40. package/dist/src/http/handlers/sessions-settings.js +15 -10
  41. package/dist/src/http/server.js +27 -2
  42. package/dist/src/link-preview/og-parse.js +3 -1
  43. package/dist/src/link-preview/ssrf-guard.js +6 -2
  44. package/dist/src/media-fetch.js +4 -1
  45. package/dist/src/plugin-install-info.js +4 -1
  46. package/dist/src/session/session-manager.js +9 -3
  47. package/dist/src/session-usage-store.js +3 -1
  48. package/dist/src/skills-discovery.d.ts +59 -0
  49. package/dist/src/skills-discovery.js +252 -0
  50. package/dist/src/sse/offline-queue.js +4 -1
  51. package/dist/src/thinking-levels.d.ts +21 -0
  52. package/dist/src/thinking-levels.js +48 -0
  53. package/dist/src/tool-catalog.d.ts +53 -0
  54. package/dist/src/tool-catalog.js +191 -0
  55. package/dist/src/upgrade-runtime.d.ts +1 -1
  56. package/dist/src/version.js +4 -2
  57. package/index.ts +43 -35
  58. package/install.js +131 -43
  59. package/package.json +10 -1
  60. package/src/agent/abort-run.ts +23 -8
  61. package/src/agent/dispatch-bridge.ts +2 -1
  62. package/src/agent/media-bridge.test.ts +71 -0
  63. package/src/agent/media-bridge.ts +30 -1
  64. package/src/agent/node-pairing-bridge.ts +29 -15
  65. package/src/agent/run-usage-accumulator.ts +4 -2
  66. package/src/agent/subagent-registry.ts +0 -4
  67. package/src/agent-forward-runtime.ts +11 -0
  68. package/src/agent-id.ts +24 -0
  69. package/src/agent-run-context-bridge.ts +3 -1
  70. package/src/channel-actions.test.ts +57 -4
  71. package/src/channel-actions.ts +41 -15
  72. package/src/channel.lifecycle.test.ts +41 -0
  73. package/src/channel.outbound.test.ts +18 -4
  74. package/src/channel.ts +140 -120
  75. package/src/collect-message-media-paths.ts +15 -6
  76. package/src/config.ts +1 -4
  77. package/src/e2e/agents-list.e2e.test.ts +9 -2
  78. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  79. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  80. package/src/e2e/auto-approve.integration.test.ts +13 -7
  81. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  82. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  83. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  84. package/src/e2e/send-text.e2e.test.ts +11 -2
  85. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  86. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  87. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  88. package/src/e2e/subagent.e2e.test.ts +136 -53
  89. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  90. package/src/friday-session.forward-agent.test.ts +44 -12
  91. package/src/friday-session.ts +44 -20
  92. package/src/history/normalize-message.test.ts +35 -8
  93. package/src/history/normalize-message.ts +24 -12
  94. package/src/history/read-transcript.ts +1 -4
  95. package/src/http/handlers/agent-config.test.ts +212 -0
  96. package/src/http/handlers/agent-config.ts +232 -0
  97. package/src/http/handlers/agent-files.test.ts +136 -0
  98. package/src/http/handlers/agent-files.ts +149 -0
  99. package/src/http/handlers/agent-tools-catalog.ts +42 -0
  100. package/src/http/handlers/agents-list.test.ts +1 -5
  101. package/src/http/handlers/agents-list.ts +1 -22
  102. package/src/http/handlers/cancel.test.ts +23 -4
  103. package/src/http/handlers/cancel.ts +14 -6
  104. package/src/http/handlers/device-approve.test.ts +12 -3
  105. package/src/http/handlers/device-approve.ts +33 -21
  106. package/src/http/handlers/files-download.ts +17 -13
  107. package/src/http/handlers/files.test.ts +120 -0
  108. package/src/http/handlers/files.ts +115 -17
  109. package/src/http/handlers/health.test.ts +43 -11
  110. package/src/http/handlers/health.ts +22 -6
  111. package/src/http/handlers/history-messages.test.ts +51 -9
  112. package/src/http/handlers/history-messages.ts +4 -1
  113. package/src/http/handlers/history-sessions.test.ts +46 -9
  114. package/src/http/handlers/history-sessions.ts +5 -3
  115. package/src/http/handlers/history-set-title.test.ts +14 -5
  116. package/src/http/handlers/link-preview.test.ts +57 -16
  117. package/src/http/handlers/link-preview.ts +4 -1
  118. package/src/http/handlers/messages.test.ts +12 -8
  119. package/src/http/handlers/messages.ts +64 -21
  120. package/src/http/handlers/models-list.test.ts +114 -0
  121. package/src/http/handlers/models-list.ts +26 -8
  122. package/src/http/handlers/nodes-approve.test.ts +15 -4
  123. package/src/http/handlers/nodes-approve.ts +38 -40
  124. package/src/http/handlers/plugin-info.ts +5 -6
  125. package/src/http/handlers/plugin-upgrade.ts +4 -1
  126. package/src/http/handlers/sessions-settings.ts +16 -11
  127. package/src/http/handlers/sse.ts +3 -1
  128. package/src/http/server.ts +33 -6
  129. package/src/link-preview/og-parse.test.ts +6 -2
  130. package/src/link-preview/og-parse.ts +10 -3
  131. package/src/link-preview/preview-service.ts +4 -1
  132. package/src/link-preview/ssrf-guard.test.ts +78 -16
  133. package/src/link-preview/ssrf-guard.ts +7 -2
  134. package/src/media-fetch.test.ts +8 -3
  135. package/src/media-fetch.ts +5 -3
  136. package/src/openclaw.d.ts +41 -10
  137. package/src/plugin-install-info.ts +20 -9
  138. package/src/run-metadata.ts +2 -1
  139. package/src/session/session-manager.ts +19 -11
  140. package/src/session-usage-snapshot.ts +3 -1
  141. package/src/session-usage-store.ts +3 -1
  142. package/src/skills-discovery.test.ts +152 -0
  143. package/src/skills-discovery.ts +264 -0
  144. package/src/sse/emitter.test.ts +1 -1
  145. package/src/sse/emitter.ts +9 -3
  146. package/src/sse/offline-queue.ts +17 -8
  147. package/src/test-support/app-simulator.ts +17 -3
  148. package/src/test-support/mock-dispatch.ts +17 -4
  149. package/src/thinking-levels.test.ts +143 -0
  150. package/src/thinking-levels.ts +70 -0
  151. package/src/tool-catalog.ts +261 -0
  152. package/src/upgrade-runtime.ts +4 -2
  153. package/src/version.ts +6 -2
  154. package/tsconfig.json +1 -1
package/src/channel.ts CHANGED
@@ -9,6 +9,7 @@ import fs from "node:fs";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
11
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
12
+ import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
12
13
  import { createFridayNextLogger } from "./logging.js";
13
14
  import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/status-helpers";
14
15
  import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
@@ -16,6 +17,7 @@ import { sseEmitter } from "./sse/emitter.js";
16
17
  import { describeMessageActions, handleMessageAction } from "./channel-actions.js";
17
18
  import { guessMimeType, resolveMediaAttachment } from "./http/handlers/files.js";
18
19
  import { downloadRemoteMedia, isHttpUrl } from "./media-fetch.js";
20
+ import { resolveMediaMaxBytes } from "./agent/media-bridge.js";
19
21
  import {
20
22
  resolveFridayDeviceIdForOutbound,
21
23
  resolveHistorySessionKeyForFridayDevice,
@@ -100,6 +102,22 @@ const fridayLifecycle = {
100
102
  },
101
103
  };
102
104
 
105
+ /**
106
+ * friday-next is a passive HTTP+SSE channel: its routes live on the shared gateway server and
107
+ * SSE clients connect on demand, so there is no per-account socket or polling loop to maintain.
108
+ * But the core health-monitor reads the account's lifecycle `running` flag — which the framework
109
+ * flips to `false` the moment `startAccount` resolves/rejects. Without a long-lived startAccount
110
+ * the account is permanently seen as "stopped" and restarted every health poll (~5 min). A stopped
111
+ * account drops out of the deliverable-channel registry, so an agent `message` send landing in that
112
+ * window fails with `Unknown channel: friday-next`. Hold the account lifecycle open until abort
113
+ * (reload/shutdown) so the channel stays `running:true` and continuously deliverable.
114
+ */
115
+ const fridayGateway = {
116
+ startAccount: async (ctx: { abortSignal: AbortSignal }): Promise<void> => {
117
+ await waitUntilAbort(ctx.abortSignal);
118
+ },
119
+ };
120
+
103
121
  const fridayStatus = {
104
122
  buildAccountSnapshot: async (params: {
105
123
  account: { accountId?: string; name?: string; enabled?: boolean };
@@ -139,6 +157,7 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
139
157
  },
140
158
  config: fridayConfigAdapter,
141
159
  lifecycle: fridayLifecycle,
160
+ gateway: fridayGateway,
142
161
  status: fridayStatus,
143
162
  bindings: {
144
163
  compileConfiguredBinding: () => null,
@@ -175,150 +194,151 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
175
194
  outbound: {
176
195
  deliveryMode: "direct" as const,
177
196
  sendText: async (ctx: any) => {
178
- const text = ctx.text ?? "";
179
- const rawCtx = ctx as unknown as Record<string, unknown>;
180
- const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
181
- const runIdFromCtx = pickFirstString(rawCtx, [
182
- "parentRunId",
183
- "requesterRunId",
184
- "originRunId",
185
- "runId",
186
- ]);
187
- const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
188
- const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
197
+ const text = ctx.text ?? "";
198
+ const rawCtx = ctx as unknown as Record<string, unknown>;
199
+ const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
200
+ const runIdFromCtx = pickFirstString(rawCtx, [
201
+ "parentRunId",
202
+ "requesterRunId",
203
+ "originRunId",
204
+ "runId",
205
+ ]);
206
+ const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
207
+ const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
189
208
 
190
- const conn = sseEmitter.getConnection(deviceId);
191
- const ts = new Date().toISOString();
192
- logger.info(
193
- `[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
194
- );
209
+ const conn = sseEmitter.getConnection(deviceId);
210
+ logger.info(
211
+ `[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
212
+ );
195
213
 
196
- if (conn) {
197
- sseEmitter.broadcast(
198
- {
199
- type: "outbound",
200
- data: {
201
- op: "text",
202
- ts: Date.now(),
203
- runId,
204
- deviceId,
205
- sessionKey,
206
- ctx: {
207
- text,
208
- to: ctx.to,
209
- mediaUrl: ctx.mediaUrl,
210
- audioAsVoice: ctx.audioAsVoice,
211
- },
214
+ if (conn) {
215
+ sseEmitter.broadcast(
216
+ {
217
+ type: "outbound",
218
+ data: {
219
+ op: "text",
220
+ ts: Date.now(),
221
+ runId,
222
+ deviceId,
223
+ sessionKey,
224
+ ctx: {
225
+ text,
226
+ to: ctx.to,
227
+ mediaUrl: ctx.mediaUrl,
228
+ audioAsVoice: ctx.audioAsVoice,
212
229
  },
213
230
  },
214
- deviceId,
215
- true,
216
- );
217
- }
231
+ },
232
+ deviceId,
233
+ true,
234
+ );
235
+ }
236
+
237
+ return {
238
+ channel: CHANNEL_ID,
239
+ messageId: crypto.randomUUID(),
240
+ timestamp: Date.now(),
241
+ };
242
+ },
243
+ sendMedia: async (ctx: any) => {
244
+ const rawCtx = ctx as unknown as Record<string, unknown>;
245
+ const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
246
+ const mediaUrl = ctx.mediaUrl;
247
+ const runIdFromCtx = pickFirstString(rawCtx, [
248
+ "parentRunId",
249
+ "requesterRunId",
250
+ "originRunId",
251
+ "runId",
252
+ ]);
253
+ const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
254
+ const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
255
+ const audioAsVoice = ctx.audioAsVoice === true;
256
+ const caption = ctx.text ?? "";
218
257
 
258
+ if (!mediaUrl) {
219
259
  return {
220
260
  channel: CHANNEL_ID,
221
261
  messageId: crypto.randomUUID(),
222
262
  timestamp: Date.now(),
223
263
  };
224
- },
225
- sendMedia: async (ctx: any) => {
226
- const rawCtx = ctx as unknown as Record<string, unknown>;
227
- const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
228
- const mediaUrl = ctx.mediaUrl;
229
- const runIdFromCtx = pickFirstString(rawCtx, [
230
- "parentRunId",
231
- "requesterRunId",
232
- "originRunId",
233
- "runId",
234
- ]);
235
- const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
236
- const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
237
- const audioAsVoice = ctx.audioAsVoice === true;
238
- const caption = ctx.text ?? "";
239
-
240
- if (!mediaUrl) {
241
- return {
242
- channel: CHANNEL_ID,
243
- messageId: crypto.randomUUID(),
244
- timestamp: Date.now(),
245
- };
246
- }
264
+ }
247
265
 
248
- let buffer: Buffer | null = null;
249
- let downloadedMimeType: string | null = null;
266
+ let buffer: Buffer | null = null;
267
+ let downloadedMimeType: string | null = null;
250
268
 
251
- if (ctx.mediaReadFile) {
252
- try {
253
- buffer = await ctx.mediaReadFile(mediaUrl);
254
- } catch {
255
- // fall through to remote download / fs
256
- }
269
+ if (ctx.mediaReadFile) {
270
+ try {
271
+ buffer = await ctx.mediaReadFile(mediaUrl);
272
+ } catch {
273
+ // fall through to remote download / fs
257
274
  }
275
+ }
258
276
 
259
- if (!buffer && isHttpUrl(mediaUrl)) {
260
- const remote = await downloadRemoteMedia(mediaUrl);
261
- if (remote) {
262
- buffer = remote.buffer;
263
- downloadedMimeType = remote.mimeType;
264
- }
277
+ if (!buffer && isHttpUrl(mediaUrl)) {
278
+ const remote = await downloadRemoteMedia(mediaUrl);
279
+ if (remote) {
280
+ buffer = remote.buffer;
281
+ downloadedMimeType = remote.mimeType;
265
282
  }
283
+ }
266
284
 
267
- if (!buffer) {
268
- try {
269
- const resolvedPath = resolveLocalMediaPath(mediaUrl, ctx.mediaLocalRoots);
270
- buffer = fs.readFileSync(resolvedPath);
271
- } catch {
272
- // file not found — skip media
273
- }
285
+ if (!buffer) {
286
+ try {
287
+ const resolvedPath = resolveLocalMediaPath(mediaUrl, ctx.mediaLocalRoots);
288
+ buffer = fs.readFileSync(resolvedPath);
289
+ } catch {
290
+ // file not found — skip media
274
291
  }
292
+ }
275
293
 
276
- if (buffer) {
277
- const mimeType = downloadedMimeType ?? guessMimeType(mediaUrl);
278
- const saved = await saveMediaBuffer(buffer, mimeType, "inbound");
279
- if (saved.id) {
280
- const fileUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
281
- const resolved = resolveMediaAttachment(fileUrl);
282
- const publicUrl = resolved ? resolved.url : fileUrl;
294
+ if (buffer) {
295
+ const mimeType = downloadedMimeType ?? guessMimeType(mediaUrl);
296
+ // Match what openclaw itself supports for this media kind rather than
297
+ // saveMediaBuffer's 5MB default.
298
+ const maxBytes = await resolveMediaMaxBytes(mimeType);
299
+ const saved = await saveMediaBuffer(buffer, mimeType, "inbound", maxBytes);
300
+ if (saved.id) {
301
+ const fileUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
302
+ const resolved = resolveMediaAttachment(fileUrl);
303
+ const publicUrl = resolved ? resolved.url : fileUrl;
283
304
 
284
- const conn = sseEmitter.getConnection(deviceId);
285
- const ts = new Date().toISOString();
286
- logger.info(
287
- `[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
288
- );
305
+ const conn = sseEmitter.getConnection(deviceId);
306
+ logger.info(
307
+ `[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
308
+ );
289
309
 
290
- if (conn) {
291
- sseEmitter.broadcast(
292
- {
293
- type: "outbound",
294
- data: {
295
- op: "media",
296
- ts: Date.now(),
297
- runId,
298
- deviceId,
299
- sessionKey,
300
- audioAsVoice,
301
- caption,
302
- mediaUrl: publicUrl,
303
- ctx: {
304
- to: ctx.to,
305
- text: caption,
306
- originalMediaUrl: mediaUrl,
307
- },
310
+ if (conn) {
311
+ sseEmitter.broadcast(
312
+ {
313
+ type: "outbound",
314
+ data: {
315
+ op: "media",
316
+ ts: Date.now(),
317
+ runId,
318
+ deviceId,
319
+ sessionKey,
320
+ audioAsVoice,
321
+ caption,
322
+ mediaUrl: publicUrl,
323
+ ctx: {
324
+ to: ctx.to,
325
+ text: caption,
326
+ originalMediaUrl: mediaUrl,
308
327
  },
309
328
  },
310
- deviceId,
311
- true,
312
- );
313
- }
329
+ },
330
+ deviceId,
331
+ true,
332
+ );
314
333
  }
315
334
  }
335
+ }
316
336
 
317
- return {
318
- channel: CHANNEL_ID,
319
- messageId: crypto.randomUUID(),
320
- timestamp: Date.now(),
321
- };
322
- },
337
+ return {
338
+ channel: CHANNEL_ID,
339
+ messageId: crypto.randomUUID(),
340
+ timestamp: Date.now(),
341
+ };
342
+ },
323
343
  },
324
344
  });
@@ -50,7 +50,16 @@ export function collectMediaPathsFromToolResult(raw: unknown, acc?: Set<string>)
50
50
  else if (media && typeof media === "object" && !Array.isArray(media)) visit(media);
51
51
  const filePath = o.filePath;
52
52
  if (typeof filePath === "string") add(filePath);
53
- for (const k of ["details", "result", "content", "text", "body", "message", "arguments", "args"]) {
53
+ for (const k of [
54
+ "details",
55
+ "result",
56
+ "content",
57
+ "text",
58
+ "body",
59
+ "message",
60
+ "arguments",
61
+ "args",
62
+ ]) {
54
63
  if (o[k] !== undefined) visit(o[k]);
55
64
  }
56
65
  for (const val of Object.values(o)) {
@@ -112,20 +121,20 @@ export function extractLocalPathsFromToolTextBlob(s: string): Set<string> {
112
121
 
113
122
  // Verbatim /Users/.../file.ext (stop before quote or backslash — avoids eating JSON commas)
114
123
  for (const m of s.matchAll(/(\/Users\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
115
- add(m[1]!);
124
+ add(m[1]);
116
125
  }
117
126
  for (const m of s.matchAll(/(\/private\/var\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
118
- add(m[1]!);
127
+ add(m[1]);
119
128
  }
120
129
  for (const m of s.matchAll(/(\/tmp\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
121
- add(m[1]!);
130
+ add(m[1]);
122
131
  }
123
132
  for (const m of s.matchAll(/(\/home\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
124
- add(m[1]!);
133
+ add(m[1]);
125
134
  }
126
135
 
127
136
  for (const m of s.matchAll(/([A-Za-z]:\\[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
128
- add(m[1]!);
137
+ add(m[1]);
129
138
  }
130
139
 
131
140
  return out;
package/src/config.ts CHANGED
@@ -52,10 +52,7 @@ export function resolveFridayNextConfig(cfg: unknown): FridayNextConfig {
52
52
  pathPrefix: asString(section.pathPrefix, "/friday-next"),
53
53
  transport: asString(section.transport, "http+sse"),
54
54
  historyLimit: asNumber(section.historyLimit, 25, 1, 200),
55
- historyDir: asString(
56
- section.historyDir,
57
- `${homedir()}/.openclaw/friday-next/history`,
58
- ),
55
+ historyDir: asString(section.historyDir, `${homedir()}/.openclaw/friday-next/history`),
59
56
  logLevel: asString(section.logLevel, "info") as FridayNextLogLevel,
60
57
  authToken,
61
58
  corsEnabled: asBool(cors.enabled, false),
@@ -1,6 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
3
+ import {
4
+ createTempHistoryDir,
5
+ removeTempHistoryDir,
6
+ setMockRuntime,
7
+ } from "../test-support/mock-runtime.js";
4
8
  import {
5
9
  setFridayAgentForwardRuntime,
6
10
  resetFridayAgentForwardRuntimeForTest,
@@ -20,7 +24,10 @@ function setForwardConfig(config: unknown): void {
20
24
  } as never);
21
25
  }
22
26
 
23
- async function getAgents(app: ReturnType<typeof createAppSimulator>, headers?: Record<string, string>) {
27
+ async function getAgents(
28
+ app: ReturnType<typeof createAppSimulator>,
29
+ headers?: Record<string, string>,
30
+ ) {
24
31
  const res = await app.rawRequest({ method: "GET", path: "/friday-next/agents", headers });
25
32
  return { status: res.status, body: res.body ? JSON.parse(res.body) : {}, headers: res.headers };
26
33
  }
@@ -1,7 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
3
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
4
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ createTempHistoryDir,
6
+ removeTempHistoryDir,
7
+ setMockRuntime,
8
+ } from "../test-support/mock-runtime.js";
5
9
 
6
10
  describe("e2e attachments inbound", () => {
7
11
  let historyDir = "";
@@ -3,7 +3,11 @@ import path from "node:path";
3
3
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
4
  import { createAppSimulator } from "../test-support/app-simulator.js";
5
5
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
6
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
6
+ import {
7
+ createTempHistoryDir,
8
+ removeTempHistoryDir,
9
+ setMockRuntime,
10
+ } from "../test-support/mock-runtime.js";
7
11
 
8
12
  describe("e2e attachments outbound", () => {
9
13
  let historyDir = "";
@@ -36,7 +40,8 @@ describe("e2e attachments outbound", () => {
36
40
 
37
41
  const delivers = frames.filter((x) => x.event === "deliver");
38
42
  expect(delivers.length).toBeGreaterThanOrEqual(1);
39
- const urls = (delivers[delivers.length - 1]?.data?.payload as { mediaUrls?: string[] })?.mediaUrls ?? [];
43
+ const urls =
44
+ (delivers[delivers.length - 1]?.data?.payload as { mediaUrls?: string[] })?.mediaUrls ?? [];
40
45
  expect(urls.some((u) => u.includes("/friday-next/files/"))).toBe(true);
41
46
  app.disconnectSSE();
42
47
  });
@@ -8,7 +8,11 @@
8
8
  */
9
9
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
10
  import { createAppSimulator } from "../test-support/app-simulator.js";
11
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
11
+ import {
12
+ createTempHistoryDir,
13
+ removeTempHistoryDir,
14
+ setMockRuntime,
15
+ } from "../test-support/mock-runtime.js";
12
16
  import { __setMockNodePairingForTests } from "../agent/node-pairing-bridge.js";
13
17
 
14
18
  const FAKE_DEVICE_ID = "a80b8c4b305fb02c5772c409c6dfcbacde691b61557f7779511ad1a5be8fdf06";
@@ -146,12 +150,14 @@ describe("e2e two-step auto-approval", () => {
146
150
  it("Step 2: node 已在 paired 中且有 caps 时返回 alreadyApproved", async () => {
147
151
  const mockListNodePairing = vi.fn().mockResolvedValueOnce({
148
152
  pending: [],
149
- paired: [{
150
- nodeId: FAKE_DEVICE_ID,
151
- approvedAtMs: 1700000000000,
152
- caps: ["location", "canvas"],
153
- commands: ["canvas.present"],
154
- }],
153
+ paired: [
154
+ {
155
+ nodeId: FAKE_DEVICE_ID,
156
+ approvedAtMs: 1700000000000,
157
+ caps: ["location", "canvas"],
158
+ commands: ["canvas.present"],
159
+ },
160
+ ],
155
161
  });
156
162
  const mockApproveNodePairing = vi.fn();
157
163
 
@@ -1,7 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
3
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
4
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ createTempHistoryDir,
6
+ removeTempHistoryDir,
7
+ setMockRuntime,
8
+ } from "../test-support/mock-runtime.js";
5
9
 
6
10
  describe("e2e cancel reconnect errors", () => {
7
11
  let historyDir = "";
@@ -15,7 +19,12 @@ describe("e2e cancel reconnect errors", () => {
15
19
  });
16
20
 
17
21
  it("cancel 缺 runId 与 200 cancel", async () => {
18
- mockDispatchScript().lifecycle("start").partial("a").deliverFinal({ text: "a" }).lifecycle("end").install();
22
+ mockDispatchScript()
23
+ .lifecycle("start")
24
+ .partial("a")
25
+ .deliverFinal({ text: "a" })
26
+ .lifecycle("end")
27
+ .install();
19
28
  const app = createAppSimulator({ token: "test-token" });
20
29
  await app.connectSSE();
21
30
  const sent = await app.sendMessage({ text: "go", sessionKey: "c1" });
@@ -35,7 +44,13 @@ describe("e2e cancel reconnect errors", () => {
35
44
  });
36
45
 
37
46
  it("Last-Event-ID replay 与多 device 隔离", async () => {
38
- mockDispatchScript().lifecycle("start").partial("h").partial("hi").deliverFinal({ text: "hi" }).lifecycle("end").install();
47
+ mockDispatchScript()
48
+ .lifecycle("start")
49
+ .partial("h")
50
+ .partial("hi")
51
+ .deliverFinal({ text: "hi" })
52
+ .lifecycle("end")
53
+ .install();
39
54
  const appA = createAppSimulator({ token: "test-token", deviceId: "A" });
40
55
  await appA.connectSSE();
41
56
  await appA.sendMessage({ text: "one", sessionKey: "r1" });
@@ -1,6 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
3
+ import {
4
+ createTempHistoryDir,
5
+ removeTempHistoryDir,
6
+ setMockRuntime,
7
+ } from "../test-support/mock-runtime.js";
4
8
 
5
9
  describe("e2e connect and connected", () => {
6
10
  let historyDir = "";
@@ -1,7 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
3
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
4
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ createTempHistoryDir,
6
+ removeTempHistoryDir,
7
+ setMockRuntime,
8
+ } from "../test-support/mock-runtime.js";
5
9
 
6
10
  describe("e2e offline SSE replay", () => {
7
11
  let historyDir = "";
@@ -17,7 +21,12 @@ describe("e2e offline SSE replay", () => {
17
21
  });
18
22
 
19
23
  it("断开后产生的事件在 Last-Event-ID 重连后回放", async () => {
20
- mockDispatchScript().lifecycle("start").partial("x").deliverFinal({ text: "x" }).lifecycle("end").install();
24
+ mockDispatchScript()
25
+ .lifecycle("start")
26
+ .partial("x")
27
+ .deliverFinal({ text: "x" })
28
+ .lifecycle("end")
29
+ .install();
21
30
 
22
31
  const app = createAppSimulator({ token: "test-token", deviceId: "replay-dev" });
23
32
  await app.connectSSE({ deviceId: "replay-dev" });
@@ -28,7 +37,12 @@ describe("e2e offline SSE replay", () => {
28
37
  app.disconnectSSE();
29
38
 
30
39
  resetMockDispatch();
31
- mockDispatchScript().lifecycle("start").partial("y").deliverFinal({ text: "y" }).lifecycle("end").install();
40
+ mockDispatchScript()
41
+ .lifecycle("start")
42
+ .partial("y")
43
+ .deliverFinal({ text: "y" })
44
+ .lifecycle("end")
45
+ .install();
32
46
  await app.sendMessage({ text: "again", sessionKey: "r1", deviceId: "replay-dev" });
33
47
  await new Promise((r) => setTimeout(r, 80));
34
48
 
@@ -1,7 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
3
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
4
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ createTempHistoryDir,
6
+ removeTempHistoryDir,
7
+ setMockRuntime,
8
+ } from "../test-support/mock-runtime.js";
5
9
 
6
10
  describe("e2e send text", () => {
7
11
  let historyDir = "";
@@ -41,7 +45,12 @@ describe("e2e send text", () => {
41
45
  });
42
46
 
43
47
  it("assistant 流式事件可达", async () => {
44
- mockDispatchScript().lifecycle("start").partial("abXdef").deliverFinal({ text: "abXdef" }).lifecycle("end").install();
48
+ mockDispatchScript()
49
+ .lifecycle("start")
50
+ .partial("abXdef")
51
+ .deliverFinal({ text: "abXdef" })
52
+ .lifecycle("end")
53
+ .install();
45
54
  const app = createAppSimulator({ token: "test-token" });
46
55
  await app.connectSSE();
47
56
  await app.sendMessage({ text: "hi", sessionKey: "s1" });
@@ -1,7 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
3
  import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
4
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
4
+ import {
5
+ createTempHistoryDir,
6
+ removeTempHistoryDir,
7
+ setMockRuntime,
8
+ } from "../test-support/mock-runtime.js";
5
9
 
6
10
  describe("e2e slash commands", () => {
7
11
  let historyDir = "";
@@ -1,6 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { createAppSimulator } from "../test-support/app-simulator.js";
3
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
3
+ import {
4
+ createTempHistoryDir,
5
+ removeTempHistoryDir,
6
+ setMockRuntime,
7
+ } from "../test-support/mock-runtime.js";
4
8
  import { registerFridayNextHttpRoutes } from "../http/server.js";
5
9
 
6
10
  describe("e2e status cors auth", () => {
@@ -22,7 +26,12 @@ describe("e2e status cors auth", () => {
22
26
  });
23
27
 
24
28
  it("CORS 预检", async () => {
25
- setMockRuntime({ historyDir, authToken: "test-token", corsEnabled: true, allowOrigin: "https://app.example" });
29
+ setMockRuntime({
30
+ historyDir,
31
+ authToken: "test-token",
32
+ corsEnabled: true,
33
+ allowOrigin: "https://app.example",
34
+ });
26
35
  const app = createAppSimulator({ token: "test-token" });
27
36
  const res = await app.options("/friday-next/events", "https://app.example");
28
37
  expect(res.status).toBe(204);