@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
@@ -67,21 +67,25 @@ export async function handleDeviceApprove(
67
67
  if (pairedDevice) {
68
68
  res.statusCode = 200;
69
69
  res.setHeader("Content-Type", "application/json");
70
- res.end(JSON.stringify({
71
- ok: true,
72
- deviceId: normalizedDeviceId,
73
- alreadyApproved: true,
74
- approvedAtMs: (pairedDevice as any).approvedAtMs,
75
- }));
70
+ res.end(
71
+ JSON.stringify({
72
+ ok: true,
73
+ deviceId: normalizedDeviceId,
74
+ alreadyApproved: true,
75
+ approvedAtMs: (pairedDevice as any).approvedAtMs,
76
+ }),
77
+ );
76
78
  return true;
77
79
  }
78
80
 
79
81
  res.statusCode = 404;
80
82
  res.setHeader("Content-Type", "application/json");
81
- res.end(JSON.stringify({
82
- error: "No pending device found for this deviceId",
83
- deviceId: normalizedDeviceId,
84
- }));
83
+ res.end(
84
+ JSON.stringify({
85
+ error: "No pending device found for this deviceId",
86
+ deviceId: normalizedDeviceId,
87
+ }),
88
+ );
85
89
  return true;
86
90
  }
87
91
 
@@ -95,10 +99,12 @@ export async function handleDeviceApprove(
95
99
  log.error(`approveDevicePairing failed: ${err instanceof Error ? err.message : String(err)}`);
96
100
  res.statusCode = 502;
97
101
  res.setHeader("Content-Type", "application/json");
98
- res.end(JSON.stringify({
99
- error: "Device approval failed",
100
- detail: err instanceof Error ? err.message : "Unknown error",
101
- }));
102
+ res.end(
103
+ JSON.stringify({
104
+ error: "Device approval failed",
105
+ detail: err instanceof Error ? err.message : "Unknown error",
106
+ }),
107
+ );
102
108
  return true;
103
109
  }
104
110
 
@@ -112,17 +118,23 @@ export async function handleDeviceApprove(
112
118
  if (approved.status === "forbidden") {
113
119
  res.statusCode = 403;
114
120
  res.setHeader("Content-Type", "application/json");
115
- res.end(JSON.stringify({ error: `Device approval forbidden: ${(approved as any).reason ?? "unknown"}` }));
121
+ res.end(
122
+ JSON.stringify({
123
+ error: `Device approval forbidden: ${(approved as any).reason ?? "unknown"}`,
124
+ }),
125
+ );
116
126
  return true;
117
127
  }
118
128
 
119
129
  res.statusCode = 200;
120
130
  res.setHeader("Content-Type", "application/json");
121
- res.end(JSON.stringify({
122
- ok: true,
123
- deviceId: normalizedDeviceId,
124
- requestId: approved.requestId,
125
- approvedAtMs: (approved as any).device?.approvedAtMs,
126
- }));
131
+ res.end(
132
+ JSON.stringify({
133
+ ok: true,
134
+ deviceId: normalizedDeviceId,
135
+ requestId: approved.requestId,
136
+ approvedAtMs: (approved as any).device?.approvedAtMs,
137
+ }),
138
+ );
127
139
  return true;
128
140
  }
@@ -63,7 +63,10 @@ function tryDecodeURIComponent(segment: string): string | null {
63
63
  */
64
64
  function contentDispositionInline(filename: string): string {
65
65
  const base =
66
- path.basename(filename).replace(/[\r\n"]/g, "_").replace(/\\/g, "_") || "file";
66
+ path
67
+ .basename(filename)
68
+ .replace(/[\r\n"]/g, "_")
69
+ .replace(/\\/g, "_") || "file";
67
70
  const ascii = /^[\x20-\x7E]*$/.test(base) ? base : "file";
68
71
  return `inline; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(base)}`;
69
72
  }
@@ -82,9 +85,7 @@ function sendBuffer(
82
85
  const disposition = contentDispositionInline(filename);
83
86
  const rangeRaw = req.headers.range;
84
87
  const range =
85
- typeof rangeRaw === "string" && /^bytes=/i.test(rangeRaw.trim())
86
- ? rangeRaw.trim()
87
- : undefined;
88
+ typeof rangeRaw === "string" && /^bytes=/i.test(rangeRaw.trim()) ? rangeRaw.trim() : undefined;
88
89
 
89
90
  res.setHeader("Accept-Ranges", "bytes");
90
91
  res.setHeader("Cache-Control", "private, max-age=3600");
@@ -110,7 +111,7 @@ function sendBuffer(
110
111
  let end = total - 1;
111
112
 
112
113
  if (m[1] === "" && m[2] !== "") {
113
- const suffixLen = parseInt(m[2]!, 10);
114
+ const suffixLen = parseInt(m[2], 10);
114
115
  if (!Number.isFinite(suffixLen) || suffixLen <= 0) {
115
116
  res.statusCode = 200;
116
117
  res.setHeader("Content-Length", String(total));
@@ -120,11 +121,11 @@ function sendBuffer(
120
121
  start = Math.max(0, total - suffixLen);
121
122
  end = total - 1;
122
123
  } else if (m[1] !== "" && m[2] === "") {
123
- start = parseInt(m[1]!, 10);
124
+ start = parseInt(m[1], 10);
124
125
  end = total - 1;
125
126
  } else if (m[1] !== "" && m[2] !== "") {
126
- start = parseInt(m[1]!, 10);
127
- end = parseInt(m[2]!, 10);
127
+ start = parseInt(m[1], 10);
128
+ end = parseInt(m[2], 10);
128
129
  }
129
130
 
130
131
  if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= total) {
@@ -190,7 +191,13 @@ export async function handleFilesDownload(
190
191
  // 1.2 Plugin-root attachments/ (survives gateway restarts; basename = URL token)
191
192
  const fromAttachments = readAttachmentFileFromDisk(fileToken);
192
193
  if (fromAttachments) {
193
- sendBuffer(req, res, fromAttachments.buffer, fromAttachments.mimeType, fromAttachments.filename);
194
+ sendBuffer(
195
+ req,
196
+ res,
197
+ fromAttachments.buffer,
198
+ fromAttachments.mimeType,
199
+ fromAttachments.filename,
200
+ );
194
201
  return true;
195
202
  }
196
203
 
@@ -211,10 +218,7 @@ export async function handleFilesDownload(
211
218
  // fileId may include an extension (e.g. "uuid.png") — strip it to get the base id
212
219
  const baseId = fileToken.replace(/\.[^.]+$/, "");
213
220
  const mediaDir = path.join(os.homedir(), ".openclaw", "media", "inbound");
214
- const candidates = [
215
- path.join(mediaDir, baseId),
216
- path.join(mediaDir, fileToken),
217
- ];
221
+ const candidates = [path.join(mediaDir, baseId), path.join(mediaDir, fileToken)];
218
222
 
219
223
  for (const filePath of candidates) {
220
224
  if (fs.existsSync(filePath)) {
@@ -28,7 +28,11 @@ afterEach(() => {
28
28
 
29
29
  describe("attachment original filename survives a gateway restart", () => {
30
30
  it("stores under a uuid token but recovers the original filename from disk", () => {
31
- const stored = storeFile(Buffer.from("%PDF-1.4 fake"), "Quarterly Report.pdf", "application/pdf");
31
+ const stored = storeFile(
32
+ Buffer.from("%PDF-1.4 fake"),
33
+ "Quarterly Report.pdf",
34
+ "application/pdf",
35
+ );
32
36
  expect(stored.urlToken).not.toBe("Quarterly Report.pdf");
33
37
 
34
38
  const disk = readAttachmentFileFromDisk(stored.urlToken);
@@ -57,7 +61,9 @@ describe("attachment original filename survives a gateway restart", () => {
57
61
  const stored = storeFile(Buffer.from("x"), "doc.docx", "application/octet-stream");
58
62
  clearFileIndexForTest();
59
63
 
60
- const publicUrl = fridayFilesPublicUrl(`/friday-next/files/${encodeURIComponent(stored.urlToken)}`);
64
+ const publicUrl = fridayFilesPublicUrl(
65
+ `/friday-next/files/${encodeURIComponent(stored.urlToken)}`,
66
+ );
61
67
  expect(publicUrl).toBe(`/friday-next/files/${encodeURIComponent(stored.urlToken)}`);
62
68
  });
63
69
 
@@ -25,7 +25,9 @@ export function setAttachmentsDirForTest(dir: string | null): void {
25
25
  /** Resolve `<historyDir>/../attachments`, mirroring the offline-queue layout. */
26
26
  function resolveAttachmentsDir(): string {
27
27
  try {
28
- const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
28
+ const cfg = resolveFridayNextConfig(
29
+ getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
30
+ );
29
31
  return path.join(path.dirname(cfg.historyDir), "attachments");
30
32
  } catch {
31
33
  return path.join(os.homedir(), ".openclaw", "friday-next", "attachments");
@@ -112,7 +114,11 @@ function writeAttachmentMetaSidecar(urlToken: string, filename: string, mimeType
112
114
  * is unrecoverable. We stash it here (keyed by the inbound basename, reusing the sidecar
113
115
  * scheme but inside our own attachments dir) at send time, while we still know it.
114
116
  */
115
- export function rememberInboundMediaName(inboundPath: string, filename: string, mimeType: string): void {
117
+ export function rememberInboundMediaName(
118
+ inboundPath: string,
119
+ filename: string,
120
+ mimeType: string,
121
+ ): void {
116
122
  const key = path.basename(inboundPath);
117
123
  const name = filename.trim();
118
124
  if (!key || !name) return;
@@ -191,7 +197,10 @@ export function normalizeAgentMediaPath(raw: string): string {
191
197
  return s;
192
198
  }
193
199
 
194
- function copyLocalFileToAttachments(sourcePath: string, originalFilename?: string): StoredFile | null {
200
+ function copyLocalFileToAttachments(
201
+ sourcePath: string,
202
+ originalFilename?: string,
203
+ ): StoredFile | null {
195
204
  const resolvedPath = normalizeAgentMediaPath(sourcePath);
196
205
  const diskBasename = path.basename(resolvedPath);
197
206
  // Prefer the caller-supplied original name (recovered from an inbound sidecar); fall
@@ -211,7 +220,9 @@ function copyLocalFileToAttachments(sourcePath: string, originalFilename?: strin
211
220
  // Fallback to read+write so attachment persistence still works.
212
221
  const raw = fs.readFileSync(resolvedPath);
213
222
  fs.writeFileSync(storedPath, raw);
214
- logger.warn(`copyLocalFileToAttachments copy fallback used for "${resolvedPath}": ${String(copyErr)}`);
223
+ logger.warn(
224
+ `copyLocalFileToAttachments copy fallback used for "${resolvedPath}": ${String(copyErr)}`,
225
+ );
215
226
  }
216
227
  const stat = fs.statSync(storedPath);
217
228
  const mimeType = guessMimeType(filename);
@@ -246,7 +257,7 @@ export function storeFile(buffer: Buffer, filename: string, mimeType: string): S
246
257
  try {
247
258
  fs.writeFileSync(storedPath, buffer);
248
259
  } catch (err) {
249
- throw new Error(`Failed to store file: ${String(err)}`);
260
+ throw new Error(`Failed to store file: ${String(err)}`, { cause: err });
250
261
  }
251
262
 
252
263
  const file: StoredFile = {
@@ -314,7 +325,11 @@ export function getExternalFileSourceByUrlToken(token: string): string | undefin
314
325
  /**
315
326
  * Read a file as a Buffer with its MIME type (by id or urlToken).
316
327
  */
317
- export function readFile(id: string): { buffer: Buffer | null; mimeType: string; filename?: string } {
328
+ export function readFile(id: string): {
329
+ buffer: Buffer | null;
330
+ mimeType: string;
331
+ filename?: string;
332
+ } {
318
333
  const file = resolveStoredFile(id);
319
334
  if (!file) return { buffer: null, mimeType: "application/octet-stream" };
320
335
  try {
@@ -400,7 +415,6 @@ export function resolveMediaAttachment(localPath: string): ResolvedAttachment |
400
415
  };
401
416
  }
402
417
 
403
-
404
418
  /**
405
419
  * Guess MIME type from filename extension.
406
420
  */
@@ -23,7 +23,11 @@ class MockRes extends EventEmitter {
23
23
  }
24
24
  }
25
25
 
26
- function mockReq(method: string, url: string, headers: Record<string, string> = {}): IncomingMessage {
26
+ function mockReq(
27
+ method: string,
28
+ url: string,
29
+ headers: Record<string, string> = {},
30
+ ): IncomingMessage {
27
31
  return { method, url, headers } as IncomingMessage;
28
32
  }
29
33
 
@@ -82,7 +86,17 @@ describe("handleHealth", () => {
82
86
  {
83
87
  nodeId: NODE_ID,
84
88
  caps: ["location", "canvas"],
85
- commands: ["location.get", "canvas.present", "canvas.hide", "canvas.navigate", "canvas.eval", "canvas.snapshot", "canvas.a2ui.push", "canvas.a2ui.pushJSONL", "canvas.a2ui.reset"],
89
+ commands: [
90
+ "location.get",
91
+ "canvas.present",
92
+ "canvas.hide",
93
+ "canvas.navigate",
94
+ "canvas.eval",
95
+ "canvas.snapshot",
96
+ "canvas.a2ui.push",
97
+ "canvas.a2ui.pushJSONL",
98
+ "canvas.a2ui.reset",
99
+ ],
86
100
  },
87
101
  ],
88
102
  });
@@ -104,9 +118,7 @@ describe("handleHealth", () => {
104
118
  it("returns degraded when node is missing required caps", async () => {
105
119
  mockListNodePairing.mockResolvedValueOnce({
106
120
  pending: [],
107
- paired: [
108
- { nodeId: NODE_ID, caps: ["canvas"], commands: ["canvas.present"] },
109
- ],
121
+ paired: [{ nodeId: NODE_ID, caps: ["canvas"], commands: ["canvas.present"] }],
110
122
  });
111
123
 
112
124
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}`, {
@@ -127,7 +139,10 @@ describe("handleHealth", () => {
127
139
  pending: [{ requestId: REQUEST_ID, nodeId: NODE_ID }],
128
140
  paired: [],
129
141
  });
130
- mockApproveNodePairing.mockResolvedValueOnce({ requestId: REQUEST_ID, node: { nodeId: NODE_ID } });
142
+ mockApproveNodePairing.mockResolvedValueOnce({
143
+ requestId: REQUEST_ID,
144
+ node: { nodeId: NODE_ID },
145
+ });
131
146
 
132
147
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}&selfHeal=true`, {
133
148
  authorization: "Bearer test-token",
@@ -186,7 +201,10 @@ describe("handleHealth", () => {
186
201
  pending: [{ requestId: REQUEST_ID, nodeId: NODE_ID }],
187
202
  paired: [],
188
203
  });
189
- mockApproveNodePairing.mockResolvedValueOnce({ status: "forbidden", missingScope: "operator.admin" });
204
+ mockApproveNodePairing.mockResolvedValueOnce({
205
+ status: "forbidden",
206
+ missingScope: "operator.admin",
207
+ });
190
208
 
191
209
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}&selfHeal=true`, {
192
210
  authorization: "Bearer test-token",
@@ -236,14 +254,28 @@ describe("handleHealth", () => {
236
254
  {
237
255
  nodeId: NODE_ID,
238
256
  caps: ["location", "canvas"],
239
- commands: ["location.get", "canvas.present", "canvas.hide", "canvas.navigate", "canvas.eval", "canvas.snapshot", "canvas.a2ui.push", "canvas.a2ui.pushJSONL", "canvas.a2ui.reset"],
257
+ commands: [
258
+ "location.get",
259
+ "canvas.present",
260
+ "canvas.hide",
261
+ "canvas.navigate",
262
+ "canvas.eval",
263
+ "canvas.snapshot",
264
+ "canvas.a2ui.push",
265
+ "canvas.a2ui.pushJSONL",
266
+ "canvas.a2ui.reset",
267
+ ],
240
268
  },
241
269
  ],
242
270
  });
243
271
 
244
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&nodeDeviceId=${NODE_ID}`, {
245
- authorization: "Bearer test-token",
246
- });
272
+ const req = mockReq(
273
+ "GET",
274
+ `/friday-next/health?deviceId=${DEVICE_ID}&nodeDeviceId=${NODE_ID}`,
275
+ {
276
+ authorization: "Bearer test-token",
277
+ },
278
+ );
247
279
  const res = new MockRes() as unknown as ServerResponse;
248
280
  await handleHealth(req, res);
249
281
  const body = JSON.parse((res as unknown as MockRes).body);
@@ -75,7 +75,10 @@ export async function handleHealth(req: IncomingMessage, res: ServerResponse): P
75
75
  result.nodePairing = await checkNodePairing(nodeDeviceId, selfHeal, result, log);
76
76
  }
77
77
 
78
- result.ok = !result.nodePairing || (result.nodePairing.status === "ok" || result.nodePairing.status === "pending");
78
+ result.ok =
79
+ !result.nodePairing ||
80
+ result.nodePairing.status === "ok" ||
81
+ result.nodePairing.status === "pending";
79
82
 
80
83
  res.statusCode = 200;
81
84
  res.setHeader("Content-Type", "application/json");
@@ -113,7 +116,8 @@ async function checkNodePairing(
113
116
  };
114
117
  }
115
118
 
116
- const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> = listData?.paired ?? [];
119
+ const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> =
120
+ listData?.paired ?? [];
117
121
  const pairedMatch = pairedNodes.find(
118
122
  (entry) => entry.nodeId?.trim().toUpperCase() === normalizedNodeId,
119
123
  );
@@ -156,16 +160,24 @@ async function checkNodePairing(
156
160
 
157
161
  if (pendingMatch && selfHeal) {
158
162
  try {
159
- const callerScopes = ["operator.admin", "operator.pairing", "operator.read", "operator.write"];
163
+ const callerScopes = [
164
+ "operator.admin",
165
+ "operator.pairing",
166
+ "operator.read",
167
+ "operator.write",
168
+ ];
160
169
  const approved = await approveNodePairing(pendingMatch.requestId, { callerScopes });
161
- const succeeded = approved != null && !("status" in approved && (approved as any).status === "forbidden") && "requestId" in approved;
170
+ const succeeded =
171
+ approved != null &&
172
+ !("status" in approved && approved.status === "forbidden") &&
173
+ "requestId" in approved;
162
174
  (result.repairActions ??= []).push({
163
175
  component: "nodePairing",
164
176
  action: "approveNodePairing",
165
177
  result: succeeded ? "ok" : "failed",
166
178
  detail: succeeded
167
179
  ? `Auto-approved node ${normalizedNodeId}`
168
- : `approveNodePairing returned status=${(approved as any)?.status ?? "null"}`,
180
+ : `approveNodePairing returned status=${approved?.status ?? "null"}`,
169
181
  });
170
182
  if (succeeded) {
171
183
  log.info(`Auto-approved node ${normalizedNodeId}`);
@@ -191,5 +203,9 @@ async function checkNodePairing(
191
203
  return { status: "pending", detail: "Node is pending approval", nodePaired: false };
192
204
  }
193
205
 
194
- return { status: "not_found", detail: `Node ${normalizedNodeId} not registered`, nodePaired: false };
206
+ return {
207
+ status: "not_found",
208
+ detail: `Node ${normalizedNodeId} not registered`,
209
+ nodePaired: false,
210
+ };
195
211
  }
@@ -35,7 +35,12 @@ const CFG = {
35
35
  let tmpDir = "";
36
36
 
37
37
  /** Auth config + optional subagent fallback. */
38
- function setRuntime(getSessionMessages?: (params: { sessionKey: string; limit?: number }) => Promise<{ messages?: unknown[] }>): void {
38
+ function setRuntime(
39
+ getSessionMessages?: (params: {
40
+ sessionKey: string;
41
+ limit?: number;
42
+ }) => Promise<{ messages?: unknown[] }>,
43
+ ): void {
39
44
  setFridayNextRuntime({
40
45
  config: { loadConfig: () => CFG },
41
46
  logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
@@ -100,13 +105,30 @@ describe("handleHistoryMessages", () => {
100
105
  it("reads the transcript file from disk including user + assistant messages", async () => {
101
106
  const file = writeTranscript("sess.jsonl", [
102
107
  { type: "session", version: 1, sessionId: "s" },
103
- { type: "message", id: "u1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "user", content: "hi there" } },
104
- { type: "message", id: "a1", timestamp: "2026-01-01T00:00:01.000Z", message: { role: "assistant", content: [{ type: "text", text: "hello" }], model: "openai/gpt-4" } },
108
+ {
109
+ type: "message",
110
+ id: "u1",
111
+ timestamp: "2026-01-01T00:00:00.000Z",
112
+ message: { role: "user", content: "hi there" },
113
+ },
114
+ {
115
+ type: "message",
116
+ id: "a1",
117
+ timestamp: "2026-01-01T00:00:01.000Z",
118
+ message: {
119
+ role: "assistant",
120
+ content: [{ type: "text", text: "hello" }],
121
+ model: "openai/gpt-4",
122
+ },
123
+ },
105
124
  ]);
106
125
  setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
107
126
 
108
127
  const res = new MockRes();
109
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
128
+ await handleHistoryMessages(
129
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
130
+ res as any,
131
+ );
110
132
  expect(res.statusCode).toBe(200);
111
133
  const body = JSON.parse(res.body);
112
134
  expect(body.messages.map((m: any) => m.role)).toEqual(["user", "assistant"]);
@@ -117,7 +139,15 @@ describe("handleHistoryMessages", () => {
117
139
  it("returns the cumulative sessionUsage snapshot from the store", async () => {
118
140
  const file = writeTranscript("usage.jsonl", [
119
141
  { type: "message", id: "u1", message: { role: "user", content: "hi" } },
120
- { type: "message", id: "a1", message: { role: "assistant", content: [{ type: "text", text: "yo" }], model: "openai/gpt-4" } },
142
+ {
143
+ type: "message",
144
+ id: "a1",
145
+ message: {
146
+ role: "assistant",
147
+ content: [{ type: "text", text: "yo" }],
148
+ model: "openai/gpt-4",
149
+ },
150
+ },
121
151
  ]);
122
152
  setForward({
123
153
  "agent:main:main": {
@@ -132,7 +162,10 @@ describe("handleHistoryMessages", () => {
132
162
  });
133
163
 
134
164
  const res = new MockRes();
135
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
165
+ await handleHistoryMessages(
166
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
167
+ res as any,
168
+ );
136
169
  expect(res.statusCode).toBe(200);
137
170
  const body = JSON.parse(res.body);
138
171
  expect(body.sessionUsage).toBeDefined();
@@ -148,7 +181,10 @@ describe("handleHistoryMessages", () => {
148
181
  setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
149
182
 
150
183
  const res = new MockRes();
151
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
184
+ await handleHistoryMessages(
185
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
186
+ res as any,
187
+ );
152
188
  const body = JSON.parse(res.body);
153
189
  expect(body.sessionUsage).toBeUndefined();
154
190
  });
@@ -162,7 +198,10 @@ describe("handleHistoryMessages", () => {
162
198
 
163
199
  const res = new MockRes();
164
200
  await handleHistoryMessages(
165
- makeReq("/friday-next/history/messages?sessionKey=agent:main:friday:direct:ABCD-1234:9", AUTH),
201
+ makeReq(
202
+ "/friday-next/history/messages?sessionKey=agent:main:friday:direct:ABCD-1234:9",
203
+ AUTH,
204
+ ),
166
205
  res as any,
167
206
  );
168
207
  const body = JSON.parse(res.body);
@@ -209,7 +248,10 @@ describe("handleHistoryMessages", () => {
209
248
  messages: [{ role: "assistant", content: "fallback", __openclaw: { id: "a1", seq: 1 } }],
210
249
  }));
211
250
  const res = new MockRes();
212
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
251
+ await handleHistoryMessages(
252
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
253
+ res as any,
254
+ );
213
255
  const body = JSON.parse(res.body);
214
256
  expect(body.messages.map((m: any) => m.id)).toEqual(["a1"]);
215
257
  });
@@ -15,7 +15,10 @@ import { fileURLToPath } from "node:url";
15
15
  import { getFridayNextRuntime } from "../../runtime.js";
16
16
  import { extractBearerToken } from "../middleware/auth.js";
17
17
  import { normalizeHistoryMessages } from "../../history/normalize-message.js";
18
- import { readSessionTranscriptRawMessages, resolveSessionId } from "../../history/read-transcript.js";
18
+ import {
19
+ readSessionTranscriptRawMessages,
20
+ resolveSessionId,
21
+ } from "../../history/read-transcript.js";
19
22
  import { resolveMediaAttachment } from "./files.js";
20
23
  import { readSessionUsageSnapshotFromStore } from "../../session-usage-store.js";
21
24
 
@@ -33,7 +33,11 @@ let tmpDir = "";
33
33
  /** Write a non-empty transcript file and return its absolute path. */
34
34
  function transcript(name: string): string {
35
35
  const file = path.join(tmpDir, name);
36
- fs.writeFileSync(file, `${JSON.stringify({ type: "message", id: "m", message: { role: "user", content: "hi" } })}\n`, "utf-8");
36
+ fs.writeFileSync(
37
+ file,
38
+ `${JSON.stringify({ type: "message", id: "m", message: { role: "user", content: "hi" } })}\n`,
39
+ "utf-8",
40
+ );
37
41
  return file;
38
42
  }
39
43
 
@@ -86,7 +90,11 @@ describe("handleHistorySessions", () => {
86
90
  { agents: { list: [{ id: "main" }] } },
87
91
  {
88
92
  main: {
89
- "agent:main:main": { sessionId: "s-main", updatedAt: 100, sessionFile: transcript("main.jsonl") },
93
+ "agent:main:main": {
94
+ sessionId: "s-main",
95
+ updatedAt: 100,
96
+ sessionFile: transcript("main.jsonl"),
97
+ },
90
98
  "agent:main:friday:direct:dev:1": {
91
99
  sessionId: "s-fd",
92
100
  updatedAt: 300,
@@ -112,8 +120,16 @@ describe("handleHistorySessions", () => {
112
120
  { agents: { list: [{ id: "main" }] } },
113
121
  {
114
122
  main: {
115
- "agent:main:live": { sessionId: "a", updatedAt: 1, sessionFile: transcript("live.jsonl") },
116
- "agent:main:archived": { sessionId: "b", updatedAt: 2, sessionFile: path.join(tmpDir, "gone.jsonl") },
123
+ "agent:main:live": {
124
+ sessionId: "a",
125
+ updatedAt: 1,
126
+ sessionFile: transcript("live.jsonl"),
127
+ },
128
+ "agent:main:archived": {
129
+ sessionId: "b",
130
+ updatedAt: 2,
131
+ sessionFile: path.join(tmpDir, "gone.jsonl"),
132
+ },
117
133
  },
118
134
  },
119
135
  );
@@ -129,11 +145,32 @@ describe("handleHistorySessions", () => {
129
145
  {
130
146
  main: {
131
147
  "agent:main:main": { sessionId: "ok", updatedAt: 5, sessionFile: transcript("ok.jsonl") },
132
- "agent:main:main:heartbeat": { sessionId: "hb", updatedAt: 4, sessionFile: transcript("hb.jsonl") },
133
- "agent:main:cron:abc": { sessionId: "c", updatedAt: 3, sessionFile: transcript("c.jsonl") },
134
- "agent:main:subagent:xyz": { sessionId: "sa", updatedAt: 2, sessionFile: transcript("sa.jsonl") },
135
- "agent:main:dreaming-narrative-rem-1": { sessionId: "d", updatedAt: 1, sessionFile: transcript("d.jsonl") },
136
- "agent:main:child": { sessionId: "ch", updatedAt: 6, spawnedBy: "agent:main:main", sessionFile: transcript("ch.jsonl") },
148
+ "agent:main:main:heartbeat": {
149
+ sessionId: "hb",
150
+ updatedAt: 4,
151
+ sessionFile: transcript("hb.jsonl"),
152
+ },
153
+ "agent:main:cron:abc": {
154
+ sessionId: "c",
155
+ updatedAt: 3,
156
+ sessionFile: transcript("c.jsonl"),
157
+ },
158
+ "agent:main:subagent:xyz": {
159
+ sessionId: "sa",
160
+ updatedAt: 2,
161
+ sessionFile: transcript("sa.jsonl"),
162
+ },
163
+ "agent:main:dreaming-narrative-rem-1": {
164
+ sessionId: "d",
165
+ updatedAt: 1,
166
+ sessionFile: transcript("d.jsonl"),
167
+ },
168
+ "agent:main:child": {
169
+ sessionId: "ch",
170
+ updatedAt: 6,
171
+ spawnedBy: "agent:main:main",
172
+ sessionFile: transcript("ch.jsonl"),
173
+ },
137
174
  global: { sessionId: "g", updatedAt: 7, sessionFile: transcript("g.jsonl") },
138
175
  },
139
176
  },
@@ -139,12 +139,14 @@ function readAgentSessions(agentId: string): FridayHistorySessionSummary[] {
139
139
  sessionKey: canonicalKey,
140
140
  agentId,
141
141
  ...(readString(entry.sessionId) ? { sessionId: readString(entry.sessionId) } : {}),
142
- ...(readNumber(entry.updatedAt) !== undefined ? { updatedAt: readNumber(entry.updatedAt) } : {}),
143
- ...(readString(entry.model) ?? readString(entry.modelOverride)
142
+ ...(readNumber(entry.updatedAt) !== undefined
143
+ ? { updatedAt: readNumber(entry.updatedAt) }
144
+ : {}),
145
+ ...((readString(entry.model) ?? readString(entry.modelOverride))
144
146
  ? { model: readString(entry.model) ?? readString(entry.modelOverride) }
145
147
  : {}),
146
148
  // Server-side session display name (matches OpenClaw's resolution order).
147
- ...(readString(entry.displayName) ?? readString(entry.label)
149
+ ...((readString(entry.displayName) ?? readString(entry.label))
148
150
  ? { title: readString(entry.displayName) ?? readString(entry.label) }
149
151
  : {}),
150
152
  });