@syengup/friday-channel-next 0.1.36 → 0.1.38

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 (124) 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/operator-scope.d.ts +19 -0
  6. package/dist/src/agent/operator-scope.js +54 -0
  7. package/dist/src/agent/subagent-registry.js +0 -3
  8. package/dist/src/channel-actions.js +3 -1
  9. package/dist/src/channel.js +0 -2
  10. package/dist/src/collect-message-media-paths.js +10 -1
  11. package/dist/src/friday-session.js +34 -10
  12. package/dist/src/history/normalize-message.js +22 -8
  13. package/dist/src/http/handlers/agent-config.js +10 -4
  14. package/dist/src/http/handlers/cancel.js +4 -2
  15. package/dist/src/http/handlers/device-approve.js +3 -1
  16. package/dist/src/http/handlers/files-download.js +6 -8
  17. package/dist/src/http/handlers/files.js +1 -1
  18. package/dist/src/http/handlers/health.js +18 -4
  19. package/dist/src/http/handlers/history-messages.js +1 -1
  20. package/dist/src/http/handlers/history-sessions.js +5 -3
  21. package/dist/src/http/handlers/messages.js +34 -11
  22. package/dist/src/http/handlers/models-list.js +1 -1
  23. package/dist/src/http/handlers/nodes-approve.js +1 -6
  24. package/dist/src/http/handlers/plugin-info.js +1 -1
  25. package/dist/src/http/server.js +4 -2
  26. package/dist/src/link-preview/og-parse.js +3 -1
  27. package/dist/src/plugin-install-info.js +4 -1
  28. package/dist/src/session/session-manager.js +9 -3
  29. package/dist/src/session-usage-store.js +3 -1
  30. package/dist/src/skills-discovery.d.ts +5 -4
  31. package/dist/src/skills-discovery.js +27 -22
  32. package/dist/src/sse/offline-queue.js +4 -1
  33. package/dist/src/tool-catalog.js +2 -3
  34. package/dist/src/upgrade-runtime.d.ts +1 -1
  35. package/dist/src/version.js +3 -1
  36. package/index.ts +43 -35
  37. package/install.js +131 -43
  38. package/package.json +10 -1
  39. package/src/agent/abort-run.ts +2 -3
  40. package/src/agent/dispatch-bridge.ts +2 -1
  41. package/src/agent/media-bridge.ts +9 -2
  42. package/src/agent/node-pairing-bridge.ts +29 -15
  43. package/src/agent/operator-scope.test.ts +66 -0
  44. package/src/agent/operator-scope.ts +63 -0
  45. package/src/agent/run-usage-accumulator.ts +4 -2
  46. package/src/agent/subagent-registry.ts +0 -4
  47. package/src/agent-run-context-bridge.ts +3 -1
  48. package/src/channel-actions.test.ts +10 -4
  49. package/src/channel-actions.ts +3 -1
  50. package/src/channel.outbound.test.ts +18 -4
  51. package/src/channel.ts +121 -123
  52. package/src/collect-message-media-paths.ts +15 -6
  53. package/src/config.ts +1 -4
  54. package/src/e2e/agents-list.e2e.test.ts +9 -2
  55. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  56. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  57. package/src/e2e/auto-approve.integration.test.ts +13 -7
  58. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  59. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  60. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  61. package/src/e2e/send-text.e2e.test.ts +11 -2
  62. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  63. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  64. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  65. package/src/e2e/subagent.e2e.test.ts +136 -53
  66. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  67. package/src/friday-session.forward-agent.test.ts +44 -12
  68. package/src/friday-session.ts +44 -20
  69. package/src/history/normalize-message.test.ts +35 -8
  70. package/src/history/normalize-message.ts +24 -12
  71. package/src/history/read-transcript.ts +1 -4
  72. package/src/http/handlers/agent-config.test.ts +10 -3
  73. package/src/http/handlers/agent-config.ts +22 -8
  74. package/src/http/handlers/agents-list.test.ts +1 -5
  75. package/src/http/handlers/cancel.test.ts +12 -3
  76. package/src/http/handlers/cancel.ts +4 -2
  77. package/src/http/handlers/device-approve.test.ts +12 -3
  78. package/src/http/handlers/device-approve.ts +33 -21
  79. package/src/http/handlers/files-download.ts +17 -13
  80. package/src/http/handlers/files.test.ts +8 -2
  81. package/src/http/handlers/files.ts +21 -7
  82. package/src/http/handlers/health.test.ts +43 -11
  83. package/src/http/handlers/health.ts +22 -6
  84. package/src/http/handlers/history-messages.test.ts +51 -9
  85. package/src/http/handlers/history-messages.ts +4 -1
  86. package/src/http/handlers/history-sessions.test.ts +46 -9
  87. package/src/http/handlers/history-sessions.ts +5 -3
  88. package/src/http/handlers/history-set-title.test.ts +14 -5
  89. package/src/http/handlers/link-preview.test.ts +57 -16
  90. package/src/http/handlers/link-preview.ts +4 -1
  91. package/src/http/handlers/messages.test.ts +12 -8
  92. package/src/http/handlers/messages.ts +67 -19
  93. package/src/http/handlers/models-list.ts +14 -8
  94. package/src/http/handlers/nodes-approve.test.ts +15 -4
  95. package/src/http/handlers/nodes-approve.ts +38 -40
  96. package/src/http/handlers/plugin-info.ts +5 -6
  97. package/src/http/handlers/plugin-upgrade.ts +4 -1
  98. package/src/http/handlers/sse.ts +3 -1
  99. package/src/http/server.ts +9 -6
  100. package/src/link-preview/og-parse.test.ts +6 -2
  101. package/src/link-preview/og-parse.ts +10 -3
  102. package/src/link-preview/preview-service.ts +4 -1
  103. package/src/link-preview/ssrf-guard.test.ts +72 -15
  104. package/src/link-preview/ssrf-guard.ts +2 -1
  105. package/src/media-fetch.test.ts +7 -2
  106. package/src/media-fetch.ts +1 -2
  107. package/src/openclaw.d.ts +26 -9
  108. package/src/plugin-install-info.ts +20 -9
  109. package/src/run-metadata.ts +2 -1
  110. package/src/session/session-manager.ts +19 -11
  111. package/src/session-usage-snapshot.ts +3 -1
  112. package/src/session-usage-store.ts +3 -1
  113. package/src/skills-discovery.test.ts +14 -10
  114. package/src/skills-discovery.ts +43 -27
  115. package/src/sse/emitter.test.ts +1 -1
  116. package/src/sse/emitter.ts +9 -3
  117. package/src/sse/offline-queue.ts +17 -8
  118. package/src/test-support/app-simulator.ts +17 -3
  119. package/src/test-support/mock-dispatch.ts +17 -4
  120. package/src/thinking-levels.ts +3 -1
  121. package/src/tool-catalog.ts +16 -7
  122. package/src/upgrade-runtime.ts +4 -2
  123. package/src/version.ts +5 -1
  124. package/tsconfig.json +1 -1
@@ -6,7 +6,11 @@ import { handleMessageAction } from "./channel-actions.js";
6
6
  import { sseEmitter } from "./sse/emitter.js";
7
7
  import { setOfflineQueueBaseDirForTest } from "./sse/offline-queue.js";
8
8
  import { registerRunRoute } from "./run-metadata.js";
9
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "./test-support/mock-runtime.js";
9
+ import {
10
+ createTempHistoryDir,
11
+ removeTempHistoryDir,
12
+ setMockRuntime,
13
+ } from "./test-support/mock-runtime.js";
10
14
 
11
15
  /**
12
16
  * The `message` tool's `action=send` is handled here (NOT via outbound.sendText/sendMedia).
@@ -107,8 +111,8 @@ describe("channel-actions handleSend sessionKey routing", () => {
107
111
  // 8-byte PNG magic header so saveMediaBuffer's magic-byte detection recognizes an image.
108
112
  const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01]);
109
113
  const directLink = "https://picsum.photos/600/400";
110
- const fetchMock = vi.fn(async () =>
111
- new Response(pngBytes, { status: 200, headers: { "content-type": "image/png" } }),
114
+ const fetchMock = vi.fn(
115
+ async () => new Response(pngBytes, { status: 200, headers: { "content-type": "image/png" } }),
112
116
  );
113
117
  vi.stubGlobal("fetch", fetchMock);
114
118
 
@@ -220,7 +224,9 @@ describe("channel-actions handleSend sessionKey routing", () => {
220
224
  sessionKey: "agent:operator:friday:direct:fallback-session",
221
225
  });
222
226
 
223
- const text = parseOutboundFrames(res).find((f) => f.type === "outbound" && f.data.op === "text");
227
+ const text = parseOutboundFrames(res).find(
228
+ (f) => f.type === "outbound" && f.data.op === "text",
229
+ );
224
230
  expect(text?.data.sessionKey).toBe("agent:operator:friday:direct:fallback-session");
225
231
  });
226
232
  });
@@ -64,7 +64,9 @@ async function readMediaFile(
64
64
  if (buffer?.length) {
65
65
  return { buffer, mimeType: guessMimeType(mediaPath) };
66
66
  }
67
- } catch { /* fall through */ }
67
+ } catch {
68
+ /* fall through */
69
+ }
68
70
  }
69
71
  try {
70
72
  const buffer = fs.readFileSync(mediaPath);
@@ -6,7 +6,11 @@ import { fridayNextChannelPlugin } from "./channel.js";
6
6
  import { sseEmitter } from "./sse/emitter.js";
7
7
  import { setOfflineQueueBaseDirForTest } from "./sse/offline-queue.js";
8
8
  import { registerRunRoute } from "./run-metadata.js";
9
- import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "./test-support/mock-runtime.js";
9
+ import {
10
+ createTempHistoryDir,
11
+ removeTempHistoryDir,
12
+ setMockRuntime,
13
+ } from "./test-support/mock-runtime.js";
10
14
 
11
15
  /**
12
16
  * Outbound (message-tool send) must route to the session that started the run.
@@ -95,11 +99,19 @@ describe("friday-next channel outbound sessionKey routing", () => {
95
99
  it("run-route wins over ctx sessionKey (ctx carries the agent's base/main session, not the active app session)", async () => {
96
100
  const deviceId = "DEV-TEXT-2";
97
101
  const runId = "run-text-2";
98
- registerRunRoute({ runId, deviceId, sessionKey: "agent:operator:friday-next:direct:route-session" });
102
+ registerRunRoute({
103
+ runId,
104
+ deviceId,
105
+ sessionKey: "agent:operator:friday-next:direct:route-session",
106
+ });
99
107
  sseEmitter.trackDeviceForRun(deviceId, runId);
100
108
  const res = connect(deviceId);
101
109
 
102
- await outbound.sendText({ to: deviceId, text: "hi", requesterSessionKey: "agent:operator:main" });
110
+ await outbound.sendText({
111
+ to: deviceId,
112
+ text: "hi",
113
+ requesterSessionKey: "agent:operator:main",
114
+ });
103
115
 
104
116
  const evt = parseOutboundFrames(res).find((f) => f.type === "outbound" && f.data.op === "text");
105
117
  expect(evt?.data.sessionKey).toBe("agent:operator:friday-next:direct:route-session");
@@ -129,7 +141,9 @@ describe("friday-next channel outbound sessionKey routing", () => {
129
141
 
130
142
  await outbound.sendMedia({ to: deviceId, text: "caption", mediaUrl: mediaFile });
131
143
 
132
- const evt = parseOutboundFrames(res).find((f) => f.type === "outbound" && f.data.op === "media");
144
+ const evt = parseOutboundFrames(res).find(
145
+ (f) => f.type === "outbound" && f.data.op === "media",
146
+ );
133
147
  expect(evt).toBeDefined();
134
148
  expect(evt?.data.sessionKey).toBe(sessionKey);
135
149
  expect(evt?.data.deviceId).toBe(deviceId);
package/src/channel.ts CHANGED
@@ -194,153 +194,151 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
194
194
  outbound: {
195
195
  deliveryMode: "direct" as const,
196
196
  sendText: async (ctx: any) => {
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);
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);
208
208
 
209
- const conn = sseEmitter.getConnection(deviceId);
210
- const ts = new Date().toISOString();
211
- logger.info(
212
- `[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
213
- );
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
+ );
214
213
 
215
- if (conn) {
216
- sseEmitter.broadcast(
217
- {
218
- type: "outbound",
219
- data: {
220
- op: "text",
221
- ts: Date.now(),
222
- runId,
223
- deviceId,
224
- sessionKey,
225
- ctx: {
226
- text,
227
- to: ctx.to,
228
- mediaUrl: ctx.mediaUrl,
229
- audioAsVoice: ctx.audioAsVoice,
230
- },
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,
231
229
  },
232
230
  },
233
- deviceId,
234
- true,
235
- );
236
- }
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 ?? "";
237
257
 
258
+ if (!mediaUrl) {
238
259
  return {
239
260
  channel: CHANNEL_ID,
240
261
  messageId: crypto.randomUUID(),
241
262
  timestamp: Date.now(),
242
263
  };
243
- },
244
- sendMedia: async (ctx: any) => {
245
- const rawCtx = ctx as unknown as Record<string, unknown>;
246
- const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
247
- const mediaUrl = ctx.mediaUrl;
248
- const runIdFromCtx = pickFirstString(rawCtx, [
249
- "parentRunId",
250
- "requesterRunId",
251
- "originRunId",
252
- "runId",
253
- ]);
254
- const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
255
- const sessionKey = resolveOutboundSessionKey(deviceId, runId, rawCtx);
256
- const audioAsVoice = ctx.audioAsVoice === true;
257
- const caption = ctx.text ?? "";
258
-
259
- if (!mediaUrl) {
260
- return {
261
- channel: CHANNEL_ID,
262
- messageId: crypto.randomUUID(),
263
- timestamp: Date.now(),
264
- };
265
- }
264
+ }
266
265
 
267
- let buffer: Buffer | null = null;
268
- let downloadedMimeType: string | null = null;
266
+ let buffer: Buffer | null = null;
267
+ let downloadedMimeType: string | null = null;
269
268
 
270
- if (ctx.mediaReadFile) {
271
- try {
272
- buffer = await ctx.mediaReadFile(mediaUrl);
273
- } catch {
274
- // fall through to remote download / fs
275
- }
269
+ if (ctx.mediaReadFile) {
270
+ try {
271
+ buffer = await ctx.mediaReadFile(mediaUrl);
272
+ } catch {
273
+ // fall through to remote download / fs
276
274
  }
275
+ }
277
276
 
278
- if (!buffer && isHttpUrl(mediaUrl)) {
279
- const remote = await downloadRemoteMedia(mediaUrl);
280
- if (remote) {
281
- buffer = remote.buffer;
282
- downloadedMimeType = remote.mimeType;
283
- }
277
+ if (!buffer && isHttpUrl(mediaUrl)) {
278
+ const remote = await downloadRemoteMedia(mediaUrl);
279
+ if (remote) {
280
+ buffer = remote.buffer;
281
+ downloadedMimeType = remote.mimeType;
284
282
  }
283
+ }
285
284
 
286
- if (!buffer) {
287
- try {
288
- const resolvedPath = resolveLocalMediaPath(mediaUrl, ctx.mediaLocalRoots);
289
- buffer = fs.readFileSync(resolvedPath);
290
- } catch {
291
- // file not found — skip media
292
- }
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
293
291
  }
292
+ }
294
293
 
295
- if (buffer) {
296
- const mimeType = downloadedMimeType ?? guessMimeType(mediaUrl);
297
- // Match what openclaw itself supports for this media kind rather than
298
- // saveMediaBuffer's 5MB default.
299
- const maxBytes = await resolveMediaMaxBytes(mimeType);
300
- const saved = await saveMediaBuffer(buffer, mimeType, "inbound", maxBytes);
301
- if (saved.id) {
302
- const fileUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
303
- const resolved = resolveMediaAttachment(fileUrl);
304
- 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;
305
304
 
306
- const conn = sseEmitter.getConnection(deviceId);
307
- const ts = new Date().toISOString();
308
- logger.info(
309
- `[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
310
- );
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
+ );
311
309
 
312
- if (conn) {
313
- sseEmitter.broadcast(
314
- {
315
- type: "outbound",
316
- data: {
317
- op: "media",
318
- ts: Date.now(),
319
- runId,
320
- deviceId,
321
- sessionKey,
322
- audioAsVoice,
323
- caption,
324
- mediaUrl: publicUrl,
325
- ctx: {
326
- to: ctx.to,
327
- text: caption,
328
- originalMediaUrl: mediaUrl,
329
- },
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,
330
327
  },
331
328
  },
332
- deviceId,
333
- true,
334
- );
335
- }
329
+ },
330
+ deviceId,
331
+ true,
332
+ );
336
333
  }
337
334
  }
335
+ }
338
336
 
339
- return {
340
- channel: CHANNEL_ID,
341
- messageId: crypto.randomUUID(),
342
- timestamp: Date.now(),
343
- };
344
- },
337
+ return {
338
+ channel: CHANNEL_ID,
339
+ messageId: crypto.randomUUID(),
340
+ timestamp: Date.now(),
341
+ };
342
+ },
345
343
  },
346
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 = "";