@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,7 +67,9 @@ export async function handleSseStream(req: IncomingMessage, res: ServerResponse)
67
67
  const lastEventId = parseLastEventId(req, url);
68
68
  if (lastEventId > 0) sseEmitter.replayBacklog(deviceId, lastEventId);
69
69
 
70
- const config = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
70
+ const config = resolveFridayNextConfig(
71
+ getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
72
+ );
71
73
  const keepalive = setInterval(() => {
72
74
  if (conn.isClosed) {
73
75
  clearInterval(keepalive);
@@ -34,10 +34,7 @@ import { getFridayNextRuntime } from "../runtime.js";
34
34
  import { sseEmitter } from "../sse/emitter.js";
35
35
 
36
36
  /** Route matcher - returns the matched handler or null. */
37
- async function handleFridayNextRoute(
38
- req: IncomingMessage,
39
- res: ServerResponse,
40
- ): Promise<boolean> {
37
+ async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
41
38
  const url = new URL(req.url ?? "/", "http://localhost");
42
39
  const pathname = url.pathname;
43
40
  applyCorsHeaders(res);
@@ -79,7 +76,10 @@ async function handleFridayNextRoute(
79
76
  return await handleNodesApprove(req, res);
80
77
  }
81
78
 
82
- if ((req.method === "PUT" || req.method === "GET") && pathname === "/friday-next/sessions/settings") {
79
+ if (
80
+ (req.method === "PUT" || req.method === "GET") &&
81
+ pathname === "/friday-next/sessions/settings"
82
+ ) {
83
83
  return await handleSessionsSettings(req, res);
84
84
  }
85
85
 
@@ -127,7 +127,10 @@ async function handleFridayNextRoute(
127
127
  }
128
128
 
129
129
  // Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
130
- if ((req.method === "PUT" || req.method === "POST") && pathname === "/friday-next/sessions/title") {
130
+ if (
131
+ (req.method === "PUT" || req.method === "POST") &&
132
+ pathname === "/friday-next/sessions/title"
133
+ ) {
131
134
  return await handleHistorySetTitle(req, res);
132
135
  }
133
136
 
@@ -5,7 +5,9 @@ const BASE = "https://example.com/article/42";
5
5
 
6
6
  describe("decodeHtmlEntities", () => {
7
7
  it("decodes named, decimal, and hex entities", () => {
8
- expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe('Tom & Jerry — "fun"');
8
+ expect(decodeHtmlEntities("Tom &amp; Jerry &mdash; &quot;fun&quot;")).toBe(
9
+ 'Tom & Jerry — "fun"',
10
+ );
9
11
  expect(decodeHtmlEntities("&#20013;&#25991;")).toBe("中文");
10
12
  expect(decodeHtmlEntities("&#x27;quoted&#x27;")).toBe("'quoted'");
11
13
  });
@@ -143,7 +145,9 @@ describe("parseOpenGraph", () => {
143
145
  it("extracts a cover image from inline JSON (extensionless, escaped slashes)", () => {
144
146
  const html = `<title>搜索资讯页</title>
145
147
  <script>window.__INFO__={"imgUrl":"http:\\/\\/qqpublic.qpic.cn\\/qq_public_cover\\/0\\/0-2342_op"}</script>`;
146
- expect(parseOpenGraph(html, BASE).imageUrl).toBe("http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op");
148
+ expect(parseOpenGraph(html, BASE).imageUrl).toBe(
149
+ "http://qqpublic.qpic.cn/qq_public_cover/0/0-2342_op",
150
+ );
147
151
  });
148
152
 
149
153
  it("standard og tags still win over body/json fallbacks", () => {
@@ -91,7 +91,9 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
91
91
  let metaDescription: string | null = null;
92
92
  for (const match of slice.matchAll(META_TAG_RE)) {
93
93
  const tag = match[0];
94
- const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))?.trim().toLowerCase();
94
+ const key = (attributeValue(tag, "property") ?? attributeValue(tag, "name"))
95
+ ?.trim()
96
+ .toLowerCase();
95
97
  if (!key) continue;
96
98
  const content = attributeValue(tag, "content");
97
99
  if (content == null || !content.trim()) continue;
@@ -140,10 +142,15 @@ export function parseOpenGraph(html: string, baseUrl: string): OpenGraphResult {
140
142
  };
141
143
  }
142
144
 
143
- const JSON_LD_RE = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
145
+ const JSON_LD_RE =
146
+ /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
144
147
 
145
148
  /** Extract title/description/image from JSON-LD blocks (schema.org Article/NewsArticle/etc.). */
146
- function parseJsonLd(html: string): { title: string | null; description: string | null; image: string | null } {
149
+ function parseJsonLd(html: string): {
150
+ title: string | null;
151
+ description: string | null;
152
+ image: string | null;
153
+ } {
147
154
  for (const match of html.matchAll(JSON_LD_RE)) {
148
155
  let data: unknown;
149
156
  try {
@@ -155,7 +155,10 @@ async function buildPreview(pageUrl: string): Promise<LinkPreviewResult> {
155
155
  }
156
156
 
157
157
  /** Re-host a favicon: try the parsed `<link rel icon>`, then `<origin>/favicon.ico`. */
158
- async function resolveFavicon(parsedIconUrl: string | null, finalUrl: string): Promise<string | null> {
158
+ async function resolveFavicon(
159
+ parsedIconUrl: string | null,
160
+ finalUrl: string,
161
+ ): Promise<string | null> {
159
162
  const candidates: string[] = [];
160
163
  if (parsedIconUrl) candidates.push(parsedIconUrl);
161
164
  try {
@@ -46,7 +46,9 @@ describe("parseHttpUrl", () => {
46
46
  describe("assertPublicHttpUrl", () => {
47
47
  it("rejects non-default ports", () => {
48
48
  expect(() => assertPublicHttpUrl(new URL("http://example.com:8080/"))).toThrow(BlockedUrlError);
49
- expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(BlockedUrlError);
49
+ expect(() => assertPublicHttpUrl(new URL("https://example.com:8443/"))).toThrow(
50
+ BlockedUrlError,
51
+ );
50
52
  });
51
53
 
52
54
  it("allows default and explicit 80/443 ports", () => {
@@ -95,7 +97,14 @@ describe("isPrivateAddress", () => {
95
97
  });
96
98
 
97
99
  it("passes public IPv4", () => {
98
- for (const ip of ["8.8.8.8", "93.184.216.34", "100.63.0.1", "100.128.0.1", "172.32.0.1", "198.20.0.1"]) {
100
+ for (const ip of [
101
+ "8.8.8.8",
102
+ "93.184.216.34",
103
+ "100.63.0.1",
104
+ "100.128.0.1",
105
+ "172.32.0.1",
106
+ "198.20.0.1",
107
+ ]) {
99
108
  expect(isPrivateAddress(ip), ip).toBe(false);
100
109
  }
101
110
  });
@@ -106,7 +115,15 @@ describe("isPrivateAddress", () => {
106
115
  });
107
116
 
108
117
  it("flags private/reserved IPv6 and mapped IPv4", () => {
109
- for (const ip of ["::1", "::", "fd00::1", "fd12:3456::1", "fe80::1", "::ffff:10.0.0.1", "::ffff:127.0.0.1"]) {
118
+ for (const ip of [
119
+ "::1",
120
+ "::",
121
+ "fd00::1",
122
+ "fd12:3456::1",
123
+ "fe80::1",
124
+ "::ffff:10.0.0.1",
125
+ "::ffff:127.0.0.1",
126
+ ]) {
110
127
  expect(isPrivateAddress(ip), ip).toBe(true);
111
128
  }
112
129
  });
@@ -140,11 +157,15 @@ describe("assertResolvesPublic", () => {
140
157
  { address: "93.184.216.34", family: 4 },
141
158
  { address: "10.0.0.5", family: 4 },
142
159
  ] as never);
143
- await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(BlockedUrlError);
160
+ await expect(assertResolvesPublic(new URL("https://rebind.example.com/"))).rejects.toThrow(
161
+ BlockedUrlError,
162
+ );
144
163
  });
145
164
 
146
165
  it("validates IP-literal hosts without a DNS lookup", async () => {
147
- await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(BlockedUrlError);
166
+ await expect(assertResolvesPublic(new URL("http://127.0.0.1/"))).rejects.toThrow(
167
+ BlockedUrlError,
168
+ );
148
169
  await expect(assertResolvesPublic(new URL("http://93.184.216.34/"))).resolves.toBeUndefined();
149
170
  expect(lookupMock).not.toHaveBeenCalled();
150
171
  });
@@ -164,7 +185,13 @@ describe("fetchPublicUrl", () => {
164
185
  mockPublicDns();
165
186
  vi.stubGlobal(
166
187
  "fetch",
167
- vi.fn(async () => new Response("<html>hi</html>", { status: 200, headers: { "content-type": "text/html; charset=utf-8" } })),
188
+ vi.fn(
189
+ async () =>
190
+ new Response("<html>hi</html>", {
191
+ status: 200,
192
+ headers: { "content-type": "text/html; charset=utf-8" },
193
+ }),
194
+ ),
168
195
  );
169
196
  const result = await fetchPublicUrl("https://example.com/page", opts);
170
197
  expect(result?.finalUrl).toBe("https://example.com/page");
@@ -176,8 +203,15 @@ describe("fetchPublicUrl", () => {
176
203
  mockPublicDns();
177
204
  const fetchMock = vi
178
205
  .fn()
179
- .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location: "https://other.example.com/final" } }))
180
- .mockResolvedValueOnce(new Response("ok", { status: 200, headers: { "content-type": "text/html" } }));
206
+ .mockResolvedValueOnce(
207
+ new Response(null, {
208
+ status: 302,
209
+ headers: { location: "https://other.example.com/final" },
210
+ }),
211
+ )
212
+ .mockResolvedValueOnce(
213
+ new Response("ok", { status: 200, headers: { "content-type": "text/html" } }),
214
+ );
181
215
  vi.stubGlobal("fetch", fetchMock);
182
216
  const result = await fetchPublicUrl("https://example.com/start", opts);
183
217
  expect(result?.finalUrl).toBe("https://other.example.com/final");
@@ -189,9 +223,14 @@ describe("fetchPublicUrl", () => {
189
223
  mockPublicDns();
190
224
  vi.stubGlobal(
191
225
  "fetch",
192
- vi.fn(async () => new Response(null, { status: 302, headers: { location: "http://127.0.0.1/admin" } })),
226
+ vi.fn(
227
+ async () =>
228
+ new Response(null, { status: 302, headers: { location: "http://127.0.0.1/admin" } }),
229
+ ),
230
+ );
231
+ await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(
232
+ BlockedUrlError,
193
233
  );
194
- await expect(fetchPublicUrl("https://example.com/start", opts)).rejects.toThrow(BlockedUrlError);
195
234
  });
196
235
 
197
236
  it("throws BlockedUrlError for a directly-blocked URL", async () => {
@@ -203,7 +242,13 @@ describe("fetchPublicUrl", () => {
203
242
  const big = "x".repeat(2048);
204
243
  vi.stubGlobal(
205
244
  "fetch",
206
- vi.fn(async () => new Response(big, { status: 200, headers: { "content-type": "text/html", "content-length": "10" } })),
245
+ vi.fn(
246
+ async () =>
247
+ new Response(big, {
248
+ status: 200,
249
+ headers: { "content-type": "text/html", "content-length": "10" },
250
+ }),
251
+ ),
207
252
  );
208
253
  expect(await fetchPublicUrl("https://example.com/big", opts)).toBeNull();
209
254
  });
@@ -212,16 +257,25 @@ describe("fetchPublicUrl", () => {
212
257
  mockPublicDns();
213
258
  vi.stubGlobal(
214
259
  "fetch",
215
- vi.fn(async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } })),
260
+ vi.fn(
261
+ async () =>
262
+ new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
263
+ ),
216
264
  );
217
265
  expect(
218
- await fetchPublicUrl("https://example.com/api", { ...opts, requireContentTypePrefixes: ["text/html", "application/xhtml+xml"] }),
266
+ await fetchPublicUrl("https://example.com/api", {
267
+ ...opts,
268
+ requireContentTypePrefixes: ["text/html", "application/xhtml+xml"],
269
+ }),
219
270
  ).toBeNull();
220
271
  });
221
272
 
222
273
  it("returns null on non-2xx and on DNS failure", async () => {
223
274
  mockPublicDns();
224
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 404 })));
275
+ vi.stubGlobal(
276
+ "fetch",
277
+ vi.fn(async () => new Response("nope", { status: 404 })),
278
+ );
225
279
  expect(await fetchPublicUrl("https://example.com/missing", opts)).toBeNull();
226
280
 
227
281
  lookupMock.mockRejectedValue(new Error("ENOTFOUND"));
@@ -232,7 +286,10 @@ describe("fetchPublicUrl", () => {
232
286
  mockPublicDns();
233
287
  vi.stubGlobal(
234
288
  "fetch",
235
- vi.fn(async () => new Response(null, { status: 302, headers: { location: "https://example.com/loop" } })),
289
+ vi.fn(
290
+ async () =>
291
+ new Response(null, { status: 302, headers: { location: "https://example.com/loop" } }),
292
+ ),
236
293
  );
237
294
  expect(await fetchPublicUrl("https://example.com/loop", opts)).toBeNull();
238
295
  });
@@ -66,7 +66,8 @@ export function isPrivateAddress(ip: string): boolean {
66
66
 
67
67
  function isPrivateIPv4(ip: string): boolean {
68
68
  const parts = ip.split(".").map(Number);
69
- if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true;
69
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
70
+ return true;
70
71
  const [a, b] = parts;
71
72
  if (a === 0) return true; // 0.0.0.0/8
72
73
  if (a === 10) return true; // 10/8
@@ -21,7 +21,9 @@ describe("downloadRemoteMedia", () => {
21
21
  const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
22
22
  vi.stubGlobal(
23
23
  "fetch",
24
- vi.fn(async () => new Response(bytes, { status: 200, headers: { "content-type": "image/png" } })),
24
+ vi.fn(
25
+ async () => new Response(bytes, { status: 200, headers: { "content-type": "image/png" } }),
26
+ ),
25
27
  );
26
28
 
27
29
  const result = await downloadRemoteMedia("https://picsum.photos/600/400");
@@ -48,7 +50,10 @@ describe("downloadRemoteMedia", () => {
48
50
  });
49
51
 
50
52
  it("returns null on a non-2xx response", async () => {
51
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 404 })));
53
+ vi.stubGlobal(
54
+ "fetch",
55
+ vi.fn(async () => new Response("nope", { status: 404 })),
56
+ );
52
57
  expect(await downloadRemoteMedia("https://example.com/missing.png")).toBeNull();
53
58
  });
54
59
 
@@ -53,8 +53,7 @@ export function decodeBase64Media(
53
53
  }
54
54
  if (!buffer.length) return null;
55
55
 
56
- const mimeType =
57
- mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
56
+ const mimeType = mimeHint?.trim().toLowerCase() || dataUrlMime || "application/octet-stream";
58
57
  return { buffer, mimeType };
59
58
  }
60
59
 
package/src/openclaw.d.ts CHANGED
@@ -16,7 +16,11 @@ declare module "openclaw/plugin-sdk/agent-harness" {
16
16
 
17
17
  declare module "openclaw/plugin-sdk/device-bootstrap" {
18
18
  export const listDevicePairing: (baseDir?: string) => Promise<DevicePairingList>;
19
- export const approveDevicePairing: (requestId: string, options?: { callerScopes?: readonly string[] }, baseDir?: string) => Promise<ApproveDevicePairingResult>;
19
+ export const approveDevicePairing: (
20
+ requestId: string,
21
+ options?: { callerScopes?: readonly string[] },
22
+ baseDir?: string,
23
+ ) => Promise<ApproveDevicePairingResult>;
20
24
 
21
25
  interface DevicePairingPendingRequest {
22
26
  requestId: string;
@@ -34,14 +38,17 @@ declare module "openclaw/plugin-sdk/device-bootstrap" {
34
38
  pending: DevicePairingPendingRequest[];
35
39
  paired: PairedDevice[];
36
40
  }
37
- type ApproveDevicePairingResult = {
38
- status: "approved";
39
- requestId: string;
40
- device: PairedDevice;
41
- } | {
42
- status: "forbidden";
43
- reason: string;
44
- } | null;
41
+ type ApproveDevicePairingResult =
42
+ | {
43
+ status: "approved";
44
+ requestId: string;
45
+ device: PairedDevice;
46
+ }
47
+ | {
48
+ status: "forbidden";
49
+ reason: string;
50
+ }
51
+ | null;
45
52
  }
46
53
 
47
54
  declare module "openclaw/plugin-sdk/core" {
@@ -86,6 +93,16 @@ declare module "openclaw/plugin-sdk/reply-dispatch-runtime" {
86
93
  export const dispatchReplyWithDispatcher: (...args: any[]) => any;
87
94
  }
88
95
 
96
+ declare module "openclaw/plugin-sdk/plugin-runtime" {
97
+ /**
98
+ * Returns the request-local plugin gateway-request-scope (operator client/scopes,
99
+ * context) when called from within a plugin HTTP-route handler's async context.
100
+ */
101
+ export const getPluginRuntimeGatewayRequestScope: () =>
102
+ | { client?: { connect?: { scopes?: string[] } } }
103
+ | undefined;
104
+ }
105
+
89
106
  declare module "openclaw/plugin-sdk/status-helpers" {
90
107
  export type ChannelAccountSnapshot = any;
91
108
  }
@@ -7,7 +7,14 @@ import { getUpgradeRuntime } from "./upgrade-runtime.js";
7
7
  import { PLUGIN_ID, PLUGIN_PACKAGE_NAME } from "./version.js";
8
8
 
9
9
  /** Install source from the OpenClaw config `plugins.installs[<id>].source`. */
10
- export type InstallSource = "npm" | "path" | "archive" | "clawhub" | "git" | "marketplace" | "unknown";
10
+ export type InstallSource =
11
+ | "npm"
12
+ | "path"
13
+ | "archive"
14
+ | "clawhub"
15
+ | "git"
16
+ | "marketplace"
17
+ | "unknown";
11
18
 
12
19
  /**
13
20
  * Infer the install source from the loaded plugin's filesystem path (`api.source`).
@@ -20,7 +27,9 @@ export type InstallSource = "npm" | "path" | "archive" | "clawhub" | "git" | "ma
20
27
  * dev-no-duplicate-plugin-install), so anything under the managed projects dir
21
28
  * is treated as "npm".
22
29
  */
23
- export function classifyInstallSourceFromLoadedPath(loadedPath: string | null | undefined): InstallSource {
30
+ export function classifyInstallSourceFromLoadedPath(
31
+ loadedPath: string | null | undefined,
32
+ ): InstallSource {
24
33
  if (!loadedPath) return "unknown";
25
34
  return loadedPath.includes("/.openclaw/npm/projects/") ? "npm" : "path";
26
35
  }
@@ -42,9 +51,11 @@ export function getInstallSource(): InstallSource {
42
51
  const rt = getUpgradeRuntime();
43
52
  if (!rt) return "unknown";
44
53
  try {
45
- const cfg = rt.currentConfig() as {
46
- plugins?: { installs?: Record<string, { source?: string } | undefined> };
47
- } | undefined;
54
+ const cfg = rt.currentConfig() as
55
+ | {
56
+ plugins?: { installs?: Record<string, { source?: string } | undefined> };
57
+ }
58
+ | undefined;
48
59
  const source = cfg?.plugins?.installs?.[PLUGIN_ID]?.source;
49
60
  if (
50
61
  source === "npm" ||
@@ -94,10 +105,10 @@ export async function fetchLatestVersion(nowMs: number): Promise<string | null>
94
105
  const controller = new AbortController();
95
106
  const timer = setTimeout(() => controller.abort(), 5000);
96
107
  try {
97
- const res = await fetch(
98
- `https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`,
99
- { signal: controller.signal, headers: { Accept: "application/json" } },
100
- );
108
+ const res = await fetch(`https://registry.npmjs.org/${PLUGIN_PACKAGE_NAME}/latest`, {
109
+ signal: controller.signal,
110
+ headers: { Accept: "application/json" },
111
+ });
101
112
  if (res.ok) {
102
113
  const body = (await res.json()) as { version?: string };
103
114
  if (typeof body.version === "string" && body.version) version = body.version;
@@ -173,7 +173,8 @@ export function ingestAgentEventMetadata(runId: string, data: Record<string, unk
173
173
  const cacheRead = pickCacheRead(usageForTokens);
174
174
  if (typeof cacheRead === "number" && cacheRead >= 0) next.cacheReadTokens = Math.floor(cacheRead);
175
175
  const cacheWrite = pickCacheWrite(usageForTokens);
176
- if (typeof cacheWrite === "number" && cacheWrite >= 0) next.cacheWriteTokens = Math.floor(cacheWrite);
176
+ if (typeof cacheWrite === "number" && cacheWrite >= 0)
177
+ next.cacheWriteTokens = Math.floor(cacheWrite);
177
178
 
178
179
  const usageForContext = usage ?? data;
179
180
  const ctxUsed = contextTokensFromUsageRecord(usageForContext);
@@ -162,7 +162,11 @@ export function setSessionSettings(
162
162
  upsertSessionEntry(data, fileKey, sessionKey);
163
163
 
164
164
  const fieldKeys: (keyof FridaySessionSettingsUpdate)[] = [
165
- "reasoningLevel", "thinkingLevel", "modelRef", "providerOverride", "modelOverride",
165
+ "reasoningLevel",
166
+ "thinkingLevel",
167
+ "modelRef",
168
+ "providerOverride",
169
+ "modelOverride",
166
170
  ];
167
171
  let updated = false;
168
172
  for (const key of fieldKeys) {
@@ -193,22 +197,21 @@ export function setSessionSettings(
193
197
  }
194
198
 
195
199
  function readSettingsFromEntry(entry: Record<string, unknown>): FridaySessionSettings {
196
- const provider = typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
200
+ const provider =
201
+ typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
197
202
  const model = typeof entry["modelOverride"] === "string" ? entry["modelOverride"] : undefined;
198
203
  const storedModelRef = typeof entry["modelRef"] === "string" ? entry["modelRef"] : undefined;
199
204
  const modelRef = storedModelRef ?? (provider && model ? `${provider}/${model}` : undefined);
200
205
 
201
206
  return {
202
- reasoningLevel: typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
207
+ reasoningLevel:
208
+ typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
203
209
  thinkingLevel: typeof entry["thinkingLevel"] === "string" ? entry["thinkingLevel"] : undefined,
204
210
  modelRef,
205
211
  };
206
212
  }
207
213
 
208
- export function getSessionSettings(
209
- sessionKey: string,
210
- historyDir?: string,
211
- ): FridaySessionSettings {
214
+ export function getSessionSettings(sessionKey: string, historyDir?: string): FridaySessionSettings {
212
215
  try {
213
216
  const fileKey = toSessionStoreKey(sessionKey);
214
217
  const sessionsFile = resolveSessionsFilePath(historyDir, agentIdFromSessionKey(fileKey));
@@ -243,7 +246,10 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
243
246
  const targetAgentId = agentIdFromSessionKey(sessionKey);
244
247
 
245
248
  const agentEntry = (agents?.list as Array<Record<string, unknown>> | undefined)?.find(
246
- (a) => agentIdFromSessionKey(`agent:${String(a?.id ?? "")}:x`) === targetAgentId,
249
+ (a) =>
250
+ agentIdFromSessionKey(
251
+ `agent:${typeof a?.id === "string" ? a.id : typeof a?.id === "number" ? String(a.id) : ""}:x`,
252
+ ) === targetAgentId,
247
253
  );
248
254
  const agentModel = agentEntry?.model;
249
255
  const perAgentModel =
@@ -253,13 +259,15 @@ export function resolveAgentDefaults(sessionKey: string): { model?: string; thin
253
259
  ? ((agentModel as Record<string, unknown>).primary as string)
254
260
  : undefined;
255
261
  const perAgentThinking =
256
- typeof agentEntry?.thinkingDefault === "string" ? (agentEntry.thinkingDefault as string) : undefined;
262
+ typeof agentEntry?.thinkingDefault === "string" ? agentEntry.thinkingDefault : undefined;
257
263
 
258
264
  const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
259
265
  const model = agentDefaults?.model as Record<string, unknown> | undefined;
260
- const globalModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
266
+ const globalModel = typeof model?.primary === "string" ? model.primary : undefined;
261
267
  const globalThinking =
262
- typeof agentDefaults?.thinkingDefault === "string" ? (agentDefaults.thinkingDefault as string) : undefined;
268
+ typeof agentDefaults?.thinkingDefault === "string"
269
+ ? agentDefaults.thinkingDefault
270
+ : undefined;
263
271
 
264
272
  return { model: perAgentModel ?? globalModel, thinking: perAgentThinking ?? globalThinking };
265
273
  } catch {
@@ -33,7 +33,9 @@ function finiteCost(n: unknown): number | undefined {
33
33
  }
34
34
 
35
35
  /** Build a compact snapshot from a loaded session store entry (unknown shape). */
36
- export function buildSessionUsageSnapshot(entry: Record<string, unknown>): FridaySessionUsagePayload | undefined {
36
+ export function buildSessionUsageSnapshot(
37
+ entry: Record<string, unknown>,
38
+ ): FridaySessionUsagePayload | undefined {
37
39
  const payload: FridaySessionUsagePayload = {};
38
40
 
39
41
  const modelId = typeof entry.model === "string" ? entry.model.trim() : "";
@@ -28,7 +28,9 @@ export function readSessionUsageSnapshotFromStore(
28
28
  const cfg = access.getConfig() as { session?: { store?: string } } | null | undefined;
29
29
  const storeConfig = cfg?.session?.store;
30
30
  const canonical = toSessionStoreKey(sessionKeyForStore);
31
- const storePath = access.resolveStorePath(storeConfig, { agentId: agentIdFromSessionKey(canonical) });
31
+ const storePath = access.resolveStorePath(storeConfig, {
32
+ agentId: agentIdFromSessionKey(canonical),
33
+ });
32
34
  const store = access.loadSessionStore(storePath, { skipCache: true }) as Record<
33
35
  string,
34
36
  Record<string, unknown>
@@ -45,17 +45,21 @@ describe("discoverAvailableSkills", () => {
45
45
  if (root) fs.rmSync(root, { recursive: true, force: true });
46
46
  });
47
47
 
48
- it("aggregates agent + shared root + managed + extra dirs, deduped and sorted", () => {
48
+ it("scans only the target agent's own workspace (not the default agent's), plus managed + extra dirs, deduped and sorted", () => {
49
49
  root = fs.mkdtempSync(path.join(os.tmpdir(), "friday-disc-"));
50
50
  const configRoot = path.join(root, "configdir");
51
51
  const extraDir = path.join(root, "extra");
52
52
 
53
- // Shared root pool (default agent "main" workspace)
54
- makeSkills(path.join(configRoot, "workspace", "skills"), ["alpha", "opencli"]);
55
- // operator's own workspace — includes a duplicate (opencli) to prove dedup
56
- makeSkills(path.join(configRoot, "workspace", "agents", "operator", "skills"), ["beta", "opencli"]);
57
- // managed dir: <configDir>/skills (sibling of workspace)
58
- makeSkills(path.join(configRoot, "skills"), ["managed-one"]);
53
+ // Default agent "main" workspace — its skills must NOT leak into other agents' catalogs.
54
+ makeSkills(path.join(configRoot, "workspace", "skills"), ["alpha"]);
55
+ // operator's own workspace — opencli also lives in the managed dir below, to prove
56
+ // workspace > installed precedence in dedup.
57
+ makeSkills(path.join(configRoot, "workspace", "agents", "operator", "skills"), [
58
+ "beta",
59
+ "opencli",
60
+ ]);
61
+ // managed dir: <configDir>/skills (sibling of the default workspace)
62
+ makeSkills(path.join(configRoot, "skills"), ["managed-one", "opencli"]);
59
63
  // config extraDirs
60
64
  makeSkills(extraDir, ["gamma"]);
61
65
 
@@ -66,12 +70,12 @@ describe("discoverAvailableSkills", () => {
66
70
  wire(configRoot, cfg);
67
71
 
68
72
  const result = discoverAvailableSkills(cfg, "operator");
69
- expect(result.map((s) => s.id)).toEqual(["alpha", "beta", "gamma", "managed-one", "opencli"]);
73
+ // "alpha" is main-only → absent for operator (regression guard for the main-leak bug).
74
+ expect(result.map((s) => s.id)).toEqual(["beta", "gamma", "managed-one", "opencli"]);
70
75
  const bySource = Object.fromEntries(result.map((s) => [s.id, s.source]));
71
76
  expect(bySource).toEqual({
72
- alpha: "workspace",
73
77
  beta: "workspace",
74
- opencli: "workspace",
78
+ opencli: "workspace", // workspace wins over the managed-dir duplicate
75
79
  "managed-one": "installed",
76
80
  gamma: "extra",
77
81
  });