@syengup/friday-channel-next 0.0.35 → 0.0.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 (97) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +182 -0
  3. package/dist/src/agent/abort-run.d.ts +1 -0
  4. package/dist/src/agent/abort-run.js +11 -0
  5. package/dist/src/agent/active-runs.d.ts +9 -0
  6. package/dist/src/agent/active-runs.js +20 -0
  7. package/dist/src/agent/dispatch-bridge.d.ts +5 -0
  8. package/dist/src/agent/dispatch-bridge.js +12 -0
  9. package/dist/src/agent/media-bridge.d.ts +4 -0
  10. package/dist/src/agent/media-bridge.js +21 -0
  11. package/dist/src/agent/subagent-registry.d.ts +68 -0
  12. package/dist/src/agent/subagent-registry.js +142 -0
  13. package/dist/src/agent-forward-runtime.d.ts +17 -0
  14. package/dist/src/agent-forward-runtime.js +16 -0
  15. package/dist/src/agent-run-context-bridge.d.ts +13 -0
  16. package/dist/src/agent-run-context-bridge.js +23 -0
  17. package/dist/src/channel-actions.d.ts +13 -0
  18. package/dist/src/channel-actions.js +101 -0
  19. package/dist/src/channel.d.ts +6 -0
  20. package/dist/src/channel.js +248 -0
  21. package/dist/src/collect-message-media-paths.d.ts +11 -0
  22. package/dist/src/collect-message-media-paths.js +143 -0
  23. package/dist/src/config.d.ts +15 -0
  24. package/dist/src/config.js +39 -0
  25. package/dist/src/friday-inbound-stats.d.ts +2 -0
  26. package/dist/src/friday-inbound-stats.js +8 -0
  27. package/dist/src/friday-session.d.ts +40 -0
  28. package/dist/src/friday-session.js +395 -0
  29. package/dist/src/host-config.d.ts +1 -0
  30. package/dist/src/host-config.js +15 -0
  31. package/dist/src/http/handlers/cancel.d.ts +2 -0
  32. package/dist/src/http/handlers/cancel.js +33 -0
  33. package/dist/src/http/handlers/device-approve.d.ts +2 -0
  34. package/dist/src/http/handlers/device-approve.js +125 -0
  35. package/dist/src/http/handlers/files-download.d.ts +10 -0
  36. package/dist/src/http/handlers/files-download.js +210 -0
  37. package/dist/src/http/handlers/files-upload.d.ts +8 -0
  38. package/dist/src/http/handlers/files-upload.js +136 -0
  39. package/dist/src/http/handlers/files.d.ts +75 -0
  40. package/dist/src/http/handlers/files.js +305 -0
  41. package/dist/src/http/handlers/messages.d.ts +34 -0
  42. package/dist/src/http/handlers/messages.js +476 -0
  43. package/dist/src/http/handlers/models-list.d.ts +10 -0
  44. package/dist/src/http/handlers/models-list.js +113 -0
  45. package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
  46. package/dist/src/http/handlers/nodes-approve.js +146 -0
  47. package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
  48. package/dist/src/http/handlers/sessions-delete.js +49 -0
  49. package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
  50. package/dist/src/http/handlers/sessions-settings.js +71 -0
  51. package/dist/src/http/handlers/sse.d.ts +2 -0
  52. package/dist/src/http/handlers/sse.js +70 -0
  53. package/dist/src/http/handlers/status.d.ts +2 -0
  54. package/dist/src/http/handlers/status.js +29 -0
  55. package/dist/src/http/middleware/auth.d.ts +13 -0
  56. package/dist/src/http/middleware/auth.js +29 -0
  57. package/dist/src/http/middleware/body.d.ts +2 -0
  58. package/dist/src/http/middleware/body.js +24 -0
  59. package/dist/src/http/middleware/cors.d.ts +2 -0
  60. package/dist/src/http/middleware/cors.js +11 -0
  61. package/dist/src/http/server.d.ts +19 -0
  62. package/dist/src/http/server.js +87 -0
  63. package/dist/src/logging.d.ts +7 -0
  64. package/dist/src/logging.js +28 -0
  65. package/dist/src/run-metadata.d.ts +25 -0
  66. package/dist/src/run-metadata.js +139 -0
  67. package/dist/src/runtime.d.ts +13 -0
  68. package/dist/src/runtime.js +5 -0
  69. package/dist/src/session/session-manager.d.ts +22 -0
  70. package/dist/src/session/session-manager.js +190 -0
  71. package/dist/src/session-usage-snapshot.d.ts +23 -0
  72. package/dist/src/session-usage-snapshot.js +65 -0
  73. package/dist/src/sse/emitter.d.ts +59 -0
  74. package/dist/src/sse/emitter.js +219 -0
  75. package/dist/src/sse/offline-queue.d.ts +26 -0
  76. package/dist/src/sse/offline-queue.js +134 -0
  77. package/dist/src/vendor/runtime-store.d.ts +26 -0
  78. package/dist/src/vendor/runtime-store.js +60 -0
  79. package/index.ts +10 -4
  80. package/package.json +11 -10
  81. package/src/agent/subagent-registry.ts +195 -0
  82. package/src/channel.ts +6 -4
  83. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  84. package/src/e2e/subagent.e2e.test.ts +502 -0
  85. package/src/friday-session.ts +140 -1
  86. package/src/http/handlers/device-approve.test.ts +0 -1
  87. package/src/http/handlers/device-approve.ts +0 -2
  88. package/src/http/handlers/files-download.ts +4 -1
  89. package/src/http/handlers/files.ts +7 -4
  90. package/src/http/handlers/messages.ts +54 -4
  91. package/src/http/handlers/models-list.ts +24 -2
  92. package/src/http/handlers/nodes-approve.test.ts +288 -0
  93. package/src/http/handlers/nodes-approve.ts +189 -0
  94. package/src/http/server.ts +5 -0
  95. package/src/openclaw.d.ts +5 -0
  96. package/src/sse/emitter.ts +1 -1
  97. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,101 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { sseEmitter } from "./sse/emitter.js";
4
+ import { guessMimeType } from "./http/handlers/files.js";
5
+ const DISCOVERY = {
6
+ actions: ["send", "channel-info", "channel-list"],
7
+ capabilities: ["text", "media"],
8
+ };
9
+ const CHANNEL_INFO_RESPONSE = {
10
+ ok: true,
11
+ channels: [{ id: "friday-next", name: "Friday Next", transport: "http+sse" }],
12
+ };
13
+ export function describeMessageActions() {
14
+ return DISCOVERY;
15
+ }
16
+ function pickString(params, keys) {
17
+ for (const k of keys) {
18
+ const v = params[k];
19
+ if (typeof v === "string" && v.trim())
20
+ return v.trim();
21
+ }
22
+ return "";
23
+ }
24
+ async function readMediaFile(mediaPath, ctx) {
25
+ if (ctx.mediaReadFile) {
26
+ try {
27
+ const buffer = await ctx.mediaReadFile(mediaPath);
28
+ if (buffer?.length) {
29
+ return { buffer, mimeType: guessMimeType(mediaPath) };
30
+ }
31
+ }
32
+ catch { /* fall through */ }
33
+ }
34
+ try {
35
+ const buffer = fs.readFileSync(mediaPath);
36
+ return { buffer, mimeType: guessMimeType(mediaPath) };
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ async function handleSend(ctx) {
43
+ const to = pickString(ctx.params, ["to", "target"]).toUpperCase();
44
+ const text = pickString(ctx.params, ["message", "text", "content"]);
45
+ const mediaPath = pickString(ctx.params, ["media", "path", "filePath", "fileUrl"]);
46
+ const caption = pickString(ctx.params, ["caption"]);
47
+ if (!to) {
48
+ return { ok: false, error: "Missing required param: to" };
49
+ }
50
+ const runId = crypto.randomUUID();
51
+ const sessionKey = ctx.sessionKey ?? undefined;
52
+ // Send text via SSE outbound
53
+ if (text) {
54
+ sseEmitter.broadcast({
55
+ type: "outbound",
56
+ data: {
57
+ op: "text",
58
+ ts: Date.now(),
59
+ runId,
60
+ deviceId: to,
61
+ sessionKey,
62
+ ctx: { text, to },
63
+ },
64
+ }, to, true);
65
+ }
66
+ // Send media via SSE outbound
67
+ if (mediaPath) {
68
+ const result = await readMediaFile(mediaPath, ctx);
69
+ if (result) {
70
+ const { saveMediaBuffer } = await import("openclaw/plugin-sdk/media-store");
71
+ const saved = await saveMediaBuffer(result.buffer, result.mimeType, "inbound");
72
+ if (saved.id) {
73
+ const publicUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
74
+ sseEmitter.broadcast({
75
+ type: "outbound",
76
+ data: {
77
+ op: "media",
78
+ ts: Date.now(),
79
+ runId,
80
+ deviceId: to,
81
+ sessionKey,
82
+ audioAsVoice: false,
83
+ caption: caption || text,
84
+ mediaUrl: publicUrl,
85
+ ctx: { to, text: caption || text, originalMediaUrl: mediaPath },
86
+ },
87
+ }, to, true);
88
+ }
89
+ }
90
+ }
91
+ return { ok: true, runId, to };
92
+ }
93
+ export async function handleMessageAction(ctx) {
94
+ if (ctx.action === "channel-info" || ctx.action === "channel-list") {
95
+ return CHANNEL_INFO_RESPONSE;
96
+ }
97
+ if (ctx.action === "send") {
98
+ return handleSend(ctx);
99
+ }
100
+ return null;
101
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Friday Next Channel Plugin Definition.
3
+ *
4
+ * HTTP/SSE bridge for the Friday app; outbound sendText/sendMedia are forwarded as `outbound` SSE events.
5
+ */
6
+ export declare const fridayNextChannelPlugin: any;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Friday Next Channel Plugin Definition.
3
+ *
4
+ * HTTP/SSE bridge for the Friday app; outbound sendText/sendMedia are forwarded as `outbound` SSE events.
5
+ */
6
+ import crypto from "node:crypto";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
10
+ import { createFridayNextLogger } from "./logging.js";
11
+ import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
12
+ import { sseEmitter } from "./sse/emitter.js";
13
+ import { describeMessageActions, handleMessageAction } from "./channel-actions.js";
14
+ import { guessMimeType, resolveMediaAttachment } from "./http/handlers/files.js";
15
+ import { resolveFridayDeviceIdForOutbound, resolveHistorySessionKeyForFridayDevice, } from "./friday-session.js";
16
+ import { getLastFridayInboundAt } from "./friday-inbound-stats.js";
17
+ const logger = createFridayNextLogger("channel");
18
+ const CHANNEL_ID = "friday-next";
19
+ function pickFirstString(source, keys) {
20
+ for (const key of keys) {
21
+ const val = source[key];
22
+ if (typeof val === "string" && val.trim())
23
+ return val.trim();
24
+ }
25
+ return undefined;
26
+ }
27
+ function resolveLocalMediaPath(mediaUrl, localRoots) {
28
+ if (path.isAbsolute(mediaUrl))
29
+ return mediaUrl;
30
+ const roots = localRoots ?? [process.cwd(), "/tmp"];
31
+ for (const root of roots) {
32
+ const candidate = path.join(root, mediaUrl);
33
+ if (fs.existsSync(candidate))
34
+ return candidate;
35
+ }
36
+ return path.join(process.cwd(), mediaUrl);
37
+ }
38
+ const fridayConfigAdapter = {
39
+ listAccountIds: () => ["default"],
40
+ resolveAccount: () => ({ accountId: "default", enabled: true }),
41
+ defaultAccountId: () => "default",
42
+ isConfigured: () => true,
43
+ unconfiguredReason: () => null,
44
+ describeAccount: () => ({ accountId: "default", name: "Friday Next Channel", enabled: true }),
45
+ };
46
+ const fridayMeta = {
47
+ id: CHANNEL_ID,
48
+ label: "Friday Next",
49
+ selectionLabel: "Friday Next (Apple App)",
50
+ docsPath: "/channels/friday-next",
51
+ blurb: "Apple app channel with HTTP + SSE transparent OpenClaw proxy.",
52
+ };
53
+ const fridayCapabilities = {
54
+ chatTypes: ["direct"],
55
+ markdown: true,
56
+ media: true,
57
+ reactions: false,
58
+ edit: false,
59
+ threads: false,
60
+ polls: false,
61
+ typing: false,
62
+ readReceipts: false,
63
+ };
64
+ const fridayLifecycle = {
65
+ async onAccountConfigChanged() {
66
+ // No-op
67
+ },
68
+ };
69
+ const fridayStatus = {
70
+ buildAccountSnapshot: async (params) => {
71
+ const { account, runtime } = params;
72
+ const accountId = typeof account?.accountId === "string" && account.accountId.trim()
73
+ ? account.accountId.trim()
74
+ : "default";
75
+ const inbound = getLastFridayInboundAt();
76
+ const connected = sseEmitter.getConnectionCount() > 0;
77
+ return {
78
+ accountId,
79
+ name: typeof account?.name === "string" ? account.name : "Friday Next Channel",
80
+ enabled: account?.enabled !== false,
81
+ configured: true,
82
+ running: true,
83
+ connected,
84
+ lastInboundAt: inbound ?? runtime?.lastInboundAt ?? null,
85
+ mode: "http+sse",
86
+ };
87
+ },
88
+ };
89
+ export const fridayNextChannelPlugin = createChatChannelPlugin({
90
+ base: {
91
+ id: CHANNEL_ID,
92
+ meta: fridayMeta,
93
+ actions: {
94
+ describeMessageTool: describeMessageActions,
95
+ handleAction: handleMessageAction,
96
+ },
97
+ capabilities: fridayCapabilities,
98
+ defaults: {
99
+ queue: { debounceMs: 300 },
100
+ },
101
+ config: fridayConfigAdapter,
102
+ lifecycle: fridayLifecycle,
103
+ status: fridayStatus,
104
+ bindings: {
105
+ compileConfiguredBinding: () => null,
106
+ matchInboundConversation: () => null,
107
+ resolveCommandConversation: () => null,
108
+ },
109
+ conversationBindings: {
110
+ supportsCurrentConversationBinding: false,
111
+ },
112
+ messaging: {
113
+ normalizeTarget: (raw) => {
114
+ const trimmed = raw?.trim() ?? "";
115
+ return trimmed || "friday-next";
116
+ },
117
+ targetResolver: {
118
+ hint: "Use the deviceId (e.g. your device identifier).",
119
+ resolveTarget: async (ctx) => {
120
+ return { to: ctx.normalized };
121
+ },
122
+ },
123
+ parseExplicitTarget: () => ({ to: "friday-next" }),
124
+ formatTargetDisplay: ({ display }) => display || "Friday Next",
125
+ },
126
+ },
127
+ outbound: {
128
+ deliveryMode: "direct",
129
+ sendText: async (ctx) => {
130
+ const text = ctx.text ?? "";
131
+ const rawCtx = ctx;
132
+ const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
133
+ const runIdFromCtx = pickFirstString(rawCtx, [
134
+ "parentRunId",
135
+ "requesterRunId",
136
+ "originRunId",
137
+ "runId",
138
+ ]);
139
+ const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
140
+ const sessionKey = pickFirstString(rawCtx, ["requesterSessionKey", "sessionKey"]) ??
141
+ resolveHistorySessionKeyForFridayDevice(deviceId);
142
+ const conn = sseEmitter.getConnection(deviceId);
143
+ const ts = new Date().toISOString();
144
+ logger.info(`[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`);
145
+ if (conn) {
146
+ sseEmitter.broadcast({
147
+ type: "outbound",
148
+ data: {
149
+ op: "text",
150
+ ts: Date.now(),
151
+ runId,
152
+ deviceId,
153
+ sessionKey,
154
+ ctx: {
155
+ text,
156
+ to: ctx.to,
157
+ mediaUrl: ctx.mediaUrl,
158
+ audioAsVoice: ctx.audioAsVoice,
159
+ },
160
+ },
161
+ }, deviceId, true);
162
+ }
163
+ return {
164
+ channel: CHANNEL_ID,
165
+ messageId: crypto.randomUUID(),
166
+ timestamp: Date.now(),
167
+ };
168
+ },
169
+ sendMedia: async (ctx) => {
170
+ const rawCtx = ctx;
171
+ const deviceId = resolveFridayDeviceIdForOutbound(ctx.to, rawCtx);
172
+ const mediaUrl = ctx.mediaUrl;
173
+ const runIdFromCtx = pickFirstString(rawCtx, [
174
+ "parentRunId",
175
+ "requesterRunId",
176
+ "originRunId",
177
+ "runId",
178
+ ]);
179
+ const runId = runIdFromCtx ?? sseEmitter.getLastRunIdForDevice(deviceId) ?? undefined;
180
+ const sessionKey = pickFirstString(rawCtx, ["requesterSessionKey", "sessionKey"]) ??
181
+ resolveHistorySessionKeyForFridayDevice(deviceId);
182
+ const audioAsVoice = ctx.audioAsVoice === true;
183
+ const caption = ctx.text ?? "";
184
+ if (!mediaUrl) {
185
+ return {
186
+ channel: CHANNEL_ID,
187
+ messageId: crypto.randomUUID(),
188
+ timestamp: Date.now(),
189
+ };
190
+ }
191
+ let buffer = null;
192
+ if (ctx.mediaReadFile) {
193
+ try {
194
+ buffer = await ctx.mediaReadFile(mediaUrl);
195
+ }
196
+ catch {
197
+ // fall through to fs
198
+ }
199
+ }
200
+ if (!buffer) {
201
+ try {
202
+ const resolvedPath = resolveLocalMediaPath(mediaUrl, ctx.mediaLocalRoots);
203
+ buffer = fs.readFileSync(resolvedPath);
204
+ }
205
+ catch {
206
+ // file not found — skip media
207
+ }
208
+ }
209
+ if (buffer) {
210
+ const mimeType = guessMimeType(mediaUrl);
211
+ const saved = await saveMediaBuffer(buffer, mimeType, "inbound");
212
+ if (saved.id) {
213
+ const fileUrl = `/friday-next/files/${encodeURIComponent(saved.id)}`;
214
+ const resolved = resolveMediaAttachment(fileUrl);
215
+ const publicUrl = resolved ? resolved.url : fileUrl;
216
+ const conn = sseEmitter.getConnection(deviceId);
217
+ const ts = new Date().toISOString();
218
+ logger.info(`[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`);
219
+ if (conn) {
220
+ sseEmitter.broadcast({
221
+ type: "outbound",
222
+ data: {
223
+ op: "media",
224
+ ts: Date.now(),
225
+ runId,
226
+ deviceId,
227
+ sessionKey,
228
+ audioAsVoice,
229
+ caption,
230
+ mediaUrl: publicUrl,
231
+ ctx: {
232
+ to: ctx.to,
233
+ text: caption,
234
+ originalMediaUrl: mediaUrl,
235
+ },
236
+ },
237
+ }, deviceId, true);
238
+ }
239
+ }
240
+ }
241
+ return {
242
+ channel: CHANNEL_ID,
243
+ messageId: crypto.randomUUID(),
244
+ timestamp: Date.now(),
245
+ };
246
+ },
247
+ },
248
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Extract filesystem / Friday file URLs from message tool result or params (nested JSON).
3
+ */
4
+ export declare function collectMediaPathsFromToolResult(raw: unknown, acc?: Set<string>): Set<string>;
5
+ /**
6
+ * Scan the same tool `text` string that is sent on SSE (often JSON.stringify(result)).
7
+ * Inner nested JSON uses escaped quotes (\"), so keys like "mediaUrl" may not match
8
+ * naive JSON walks on the outer value. Unescaped absolute paths still appear verbatim
9
+ * (e.g. /Users/.../file.md) and are extracted here.
10
+ */
11
+ export declare function extractLocalPathsFromToolTextBlob(s: string): Set<string>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Extract filesystem / Friday file URLs from message tool result or params (nested JSON).
3
+ */
4
+ /** Bare path string when JSON.parse fails (e.g. not JSON); not http(s) URLs. */
5
+ function looksLikeLocalFilePath(v) {
6
+ const t = v.trim();
7
+ if (t.length < 2 || /^https?:\/\//i.test(t))
8
+ return false;
9
+ const abs = t.startsWith("/") ||
10
+ t.startsWith("~/") ||
11
+ t.startsWith("~\\") ||
12
+ /^file:/i.test(t) ||
13
+ /^[a-zA-Z]:[\\/]/.test(t);
14
+ if (!abs)
15
+ return false;
16
+ const lastSeg = t.split(/[/\\]/).filter(Boolean).pop() ?? "";
17
+ return lastSeg.includes(".");
18
+ }
19
+ export function collectMediaPathsFromToolResult(raw, acc) {
20
+ const out = acc ?? new Set();
21
+ const add = (s) => {
22
+ const t = s.trim();
23
+ if (t.length > 0)
24
+ out.add(t);
25
+ };
26
+ const visit = (v) => {
27
+ if (v == null)
28
+ return;
29
+ if (typeof v === "string") {
30
+ try {
31
+ visit(JSON.parse(v));
32
+ }
33
+ catch {
34
+ if (looksLikeLocalFilePath(v))
35
+ add(v);
36
+ }
37
+ return;
38
+ }
39
+ if (typeof v !== "object")
40
+ return;
41
+ if (Array.isArray(v)) {
42
+ for (const x of v)
43
+ visit(x);
44
+ return;
45
+ }
46
+ const o = v;
47
+ const mu = o.mediaUrls;
48
+ if (Array.isArray(mu))
49
+ for (const x of mu)
50
+ if (typeof x === "string")
51
+ add(x);
52
+ const m = o.mediaUrl;
53
+ if (typeof m === "string")
54
+ add(m);
55
+ const audioPath = o.audioPath;
56
+ if (typeof audioPath === "string")
57
+ add(audioPath);
58
+ const media = o.media;
59
+ if (typeof media === "string")
60
+ add(media);
61
+ else if (media && typeof media === "object" && !Array.isArray(media))
62
+ visit(media);
63
+ const filePath = o.filePath;
64
+ if (typeof filePath === "string")
65
+ add(filePath);
66
+ for (const k of ["details", "result", "content", "text", "body", "message", "arguments", "args"]) {
67
+ if (o[k] !== undefined)
68
+ visit(o[k]);
69
+ }
70
+ for (const val of Object.values(o)) {
71
+ if (typeof val !== "string")
72
+ continue;
73
+ const s = val.trimStart();
74
+ if (s.startsWith("{") || s.startsWith("["))
75
+ visit(val);
76
+ }
77
+ };
78
+ visit(raw);
79
+ return out;
80
+ }
81
+ /**
82
+ * Scan the same tool `text` string that is sent on SSE (often JSON.stringify(result)).
83
+ * Inner nested JSON uses escaped quotes (\"), so keys like "mediaUrl" may not match
84
+ * naive JSON walks on the outer value. Unescaped absolute paths still appear verbatim
85
+ * (e.g. /Users/.../file.md) and are extracted here.
86
+ */
87
+ export function extractLocalPathsFromToolTextBlob(s) {
88
+ const out = new Set();
89
+ const add = (raw) => {
90
+ const t = raw.trim();
91
+ if (t.length > 0 && looksLikeLocalFilePath(t))
92
+ out.add(t);
93
+ };
94
+ if (!s || s.length < 8)
95
+ return out;
96
+ /**
97
+ * After JSON.stringify(toolResult), nested JSON appears as key\"\":\"value\" in the outer string,
98
+ * e.g. mediaUrl\":\"/Users/me/file.md\" — not \"mediaUrl\".
99
+ */
100
+ for (const m of s.matchAll(/mediaUrl\\":\\"([^"\\]+)\\"/gi)) {
101
+ add(m[1] ?? "");
102
+ }
103
+ for (const m of s.matchAll(/media\\":\\"([^"\\]+)\\"/gi)) {
104
+ add(m[1] ?? "");
105
+ }
106
+ for (const m of s.matchAll(/filePath\\":\\"([^"\\]+)\\"/gi)) {
107
+ add(m[1] ?? "");
108
+ }
109
+ // "mediaUrls":["/a","/b"] → in outer stringify: mediaUrls\":[\"/a\",\"/b\"]
110
+ for (const m of s.matchAll(/mediaUrls\\":\[((?:\\"[^"\\]+\\",?)+)\]/gi)) {
111
+ const inner = m[1] ?? "";
112
+ for (const q of inner.matchAll(/\\"([^"\\]+)\\"/g)) {
113
+ add(q[1] ?? "");
114
+ }
115
+ }
116
+ // Unescaped JSON fragments (raw object / pretty-print)
117
+ for (const m of s.matchAll(/"mediaUrl"\s*:\s*"([^"\\]+)"/gi)) {
118
+ add(m[1] ?? "");
119
+ }
120
+ for (const m of s.matchAll(/"mediaUrls"\s*:\s*\[([\s\S]*?)\]/gi)) {
121
+ const inner = m[1] ?? "";
122
+ for (const q of inner.matchAll(/"([^"\\]*)"/g)) {
123
+ add(q[1] ?? "");
124
+ }
125
+ }
126
+ // Verbatim /Users/.../file.ext (stop before quote or backslash — avoids eating JSON commas)
127
+ for (const m of s.matchAll(/(\/Users\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
128
+ add(m[1]);
129
+ }
130
+ for (const m of s.matchAll(/(\/private\/var\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
131
+ add(m[1]);
132
+ }
133
+ for (const m of s.matchAll(/(\/tmp\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
134
+ add(m[1]);
135
+ }
136
+ for (const m of s.matchAll(/(\/home\/[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
137
+ add(m[1]);
138
+ }
139
+ for (const m of s.matchAll(/([A-Za-z]:\\[^"\\]+\.[A-Za-z0-9]{1,24})/g)) {
140
+ add(m[1]);
141
+ }
142
+ return out;
143
+ }
@@ -0,0 +1,15 @@
1
+ export type FridayNextLogLevel = "debug" | "info" | "warn" | "error";
2
+ export type FridayNextConfig = {
3
+ channelId: "friday-next";
4
+ pathPrefix: string;
5
+ transport: string;
6
+ historyLimit: number;
7
+ historyDir: string;
8
+ logLevel: FridayNextLogLevel;
9
+ authToken: string;
10
+ corsEnabled: boolean;
11
+ corsAllowOrigin: string;
12
+ sseKeepaliveSec: number;
13
+ sseBacklogPerDevice: number;
14
+ };
15
+ export declare function resolveFridayNextConfig(cfg: unknown): FridayNextConfig;
@@ -0,0 +1,39 @@
1
+ function asObject(value) {
2
+ return value && typeof value === "object" && !Array.isArray(value)
3
+ ? value
4
+ : {};
5
+ }
6
+ function asString(value, fallback) {
7
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
8
+ }
9
+ function asNumber(value, fallback, min, max) {
10
+ if (typeof value !== "number" || !Number.isFinite(value))
11
+ return fallback;
12
+ return Math.max(min, Math.min(max, Math.floor(value)));
13
+ }
14
+ function asBool(value, fallback) {
15
+ return typeof value === "boolean" ? value : fallback;
16
+ }
17
+ export function resolveFridayNextConfig(cfg) {
18
+ const root = asObject(cfg);
19
+ const channels = asObject(root.channels);
20
+ const section = asObject(channels["friday-next"]);
21
+ const sse = asObject(section.sse);
22
+ const cors = asObject(section.cors);
23
+ const authToken = asString(asObject(root.gateway).auth && asObject(asObject(root.gateway).auth).token, "") ||
24
+ asString(section.authToken, "") ||
25
+ asString(process.env.FRIDAY_NEXT_AUTH_TOKEN, "");
26
+ return {
27
+ channelId: "friday-next",
28
+ pathPrefix: asString(section.pathPrefix, "/friday-next"),
29
+ transport: asString(section.transport, "http+sse"),
30
+ historyLimit: asNumber(section.historyLimit, 25, 1, 200),
31
+ historyDir: asString(section.historyDir, `${process.env.HOME ?? ""}/.openclaw/friday-next/history`),
32
+ logLevel: asString(section.logLevel, "info"),
33
+ authToken,
34
+ corsEnabled: asBool(cors.enabled, false),
35
+ corsAllowOrigin: asString(cors.allowOrigin, "*"),
36
+ sseKeepaliveSec: asNumber(sse.keepaliveSec, 30, 5, 120),
37
+ sseBacklogPerDevice: asNumber(sse.backlogPerDevice, 200, 0, 1000),
38
+ };
39
+ }
@@ -0,0 +1,2 @@
1
+ export declare function touchFridayInbound(): void;
2
+ export declare function getLastFridayInboundAt(): number | null;
@@ -0,0 +1,8 @@
1
+ /** Last accepted POST /friday-next/messages timestamp for Control UI channel health. */
2
+ let lastInboundAtMs = null;
3
+ export function touchFridayInbound() {
4
+ lastInboundAtMs = Date.now();
5
+ }
6
+ export function getLastFridayInboundAt() {
7
+ return lastInboundAtMs;
8
+ }
@@ -0,0 +1,40 @@
1
+ /** Vitest-only: clears per-run reasoning text cache used for incremental `delta` rewriting. */
2
+ export declare function resetThinkingStreamAccumStateForTest(): void;
3
+ /** Vitest-only */
4
+ export declare function resetOpenClawRunDeviceMappingForTest(): void;
5
+ /** Parse deviceId from a Friday Next channel sessionKey (friday-{deviceId} or legacy agent:main:friday-*). */
6
+ export declare function deviceIdFromSessionKey(sessionKey: string): string | null;
7
+ export declare function registerFridaySessionDeviceMapping(rawSessionKey: string, deviceId: string): void;
8
+ /** In-process fallback for tool hooks / telemetry (same idea as outbound sole-device). */
9
+ export declare function getLastRegisteredFridayDeviceId(): string | undefined;
10
+ /** Resolve device for gateway `sessionKey` (friday-style or last POST mapping). */
11
+ export declare function resolveFridayDeviceIdForSessionKey(sessionKey: string): string | null;
12
+ /** Tool hooks / core may pass gateway store keys; resolve app's POST sessionKey. */
13
+ export declare function resolveFridayHistorySessionKey(gatewaySessionKey: string): string | undefined;
14
+ /** Resolve latest known app sessionKey by deviceId (from last POST). */
15
+ export declare function latestHistorySessionKeyForDeviceId(deviceId: string): string | undefined;
16
+ /**
17
+ * Session key hint for outbound delivery when ctx has no `sessionKey` (typical cron).
18
+ * Uses in-process mapping only (no plugin-side history files).
19
+ */
20
+ export declare function resolveHistorySessionKeyForFridayDevice(deviceId: string): string | undefined;
21
+ type ForwardAgentEventArgs = {
22
+ runId: string;
23
+ seq?: number;
24
+ ts?: number;
25
+ stream: string;
26
+ data: Record<string, unknown>;
27
+ sessionKey?: string;
28
+ };
29
+ /**
30
+ * Resolve the real device UUID for Friday outbound (`sendText` / `sendMedia`).
31
+ */
32
+ export declare function resolveFridayDeviceIdForOutbound(to: string | undefined, rawCtx?: Record<string, unknown>): string;
33
+ /**
34
+ * Forward global OpenClaw agent events to the Friday SSE connection (transparent).
35
+ *
36
+ * Asynchronous follow-up runs still reach the device via `getLastRunIdForDevice` when the parent run
37
+ * is no longer tracked.
38
+ */
39
+ export declare function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void;
40
+ export {};