@syengup/friday-channel-next 0.0.35 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
  2. package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
  3. package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
  4. package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
  5. package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
  6. package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
  7. package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
  8. package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
  9. package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
  10. package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
  11. package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
  12. package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
  13. package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
  14. package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
  15. package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
  16. package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
  17. package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
  18. package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
  19. package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
  20. package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
  21. package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
  22. package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
  23. package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
  24. package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
  25. package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
  26. package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
  27. package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
  28. package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
  29. package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
  30. package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
  31. package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
  32. package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
  33. package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
  34. package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
  35. package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
  36. package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
  37. package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
  38. package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
  39. package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +176 -0
  42. package/dist/src/agent/abort-run.d.ts +1 -0
  43. package/dist/src/agent/abort-run.js +11 -0
  44. package/dist/src/agent/active-runs.d.ts +9 -0
  45. package/dist/src/agent/active-runs.js +20 -0
  46. package/dist/src/agent/dispatch-bridge.d.ts +5 -0
  47. package/dist/src/agent/dispatch-bridge.js +12 -0
  48. package/dist/src/agent/media-bridge.d.ts +4 -0
  49. package/dist/src/agent/media-bridge.js +21 -0
  50. package/dist/src/agent/subagent-registry.d.ts +68 -0
  51. package/dist/src/agent/subagent-registry.js +142 -0
  52. package/dist/src/agent-forward-runtime.d.ts +17 -0
  53. package/dist/src/agent-forward-runtime.js +16 -0
  54. package/dist/src/agent-run-context-bridge.d.ts +13 -0
  55. package/dist/src/agent-run-context-bridge.js +23 -0
  56. package/dist/src/channel-actions.d.ts +13 -0
  57. package/dist/src/channel-actions.js +101 -0
  58. package/dist/src/channel.d.ts +6 -0
  59. package/dist/src/channel.js +248 -0
  60. package/dist/src/collect-message-media-paths.d.ts +11 -0
  61. package/dist/src/collect-message-media-paths.js +143 -0
  62. package/dist/src/config.d.ts +15 -0
  63. package/dist/src/config.js +39 -0
  64. package/dist/src/friday-inbound-stats.d.ts +2 -0
  65. package/dist/src/friday-inbound-stats.js +8 -0
  66. package/dist/src/friday-session.d.ts +40 -0
  67. package/dist/src/friday-session.js +395 -0
  68. package/dist/src/host-config.d.ts +1 -0
  69. package/dist/src/host-config.js +15 -0
  70. package/dist/src/http/handlers/cancel.d.ts +2 -0
  71. package/dist/src/http/handlers/cancel.js +33 -0
  72. package/dist/src/http/handlers/device-approve.d.ts +2 -0
  73. package/dist/src/http/handlers/device-approve.js +125 -0
  74. package/dist/src/http/handlers/device-token.d.ts +2 -0
  75. package/dist/src/http/handlers/device-token.js +43 -0
  76. package/dist/src/http/handlers/files-download.d.ts +10 -0
  77. package/dist/src/http/handlers/files-download.js +210 -0
  78. package/dist/src/http/handlers/files-upload.d.ts +8 -0
  79. package/dist/src/http/handlers/files-upload.js +136 -0
  80. package/dist/src/http/handlers/files.d.ts +75 -0
  81. package/dist/src/http/handlers/files.js +305 -0
  82. package/dist/src/http/handlers/import.d.ts +7 -0
  83. package/dist/src/http/handlers/import.js +69 -0
  84. package/dist/src/http/handlers/info.d.ts +2 -0
  85. package/dist/src/http/handlers/info.js +13 -0
  86. package/dist/src/http/handlers/messages-list.d.ts +7 -0
  87. package/dist/src/http/handlers/messages-list.js +44 -0
  88. package/dist/src/http/handlers/messages.d.ts +34 -0
  89. package/dist/src/http/handlers/messages.js +476 -0
  90. package/dist/src/http/handlers/models-list.d.ts +10 -0
  91. package/dist/src/http/handlers/models-list.js +113 -0
  92. package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
  93. package/dist/src/http/handlers/nodes-approve.js +146 -0
  94. package/dist/src/http/handlers/pair.d.ts +2 -0
  95. package/dist/src/http/handlers/pair.js +39 -0
  96. package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
  97. package/dist/src/http/handlers/sessions-delete.js +49 -0
  98. package/dist/src/http/handlers/sessions-list.d.ts +8 -0
  99. package/dist/src/http/handlers/sessions-list.js +24 -0
  100. package/dist/src/http/handlers/sessions-messages-get.d.ts +2 -0
  101. package/dist/src/http/handlers/sessions-messages-get.js +55 -0
  102. package/dist/src/http/handlers/sessions-messages-post.d.ts +2 -0
  103. package/dist/src/http/handlers/sessions-messages-post.js +92 -0
  104. package/dist/src/http/handlers/sessions-messages.d.ts +2 -0
  105. package/dist/src/http/handlers/sessions-messages.js +135 -0
  106. package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
  107. package/dist/src/http/handlers/sessions-settings.js +71 -0
  108. package/dist/src/http/handlers/sse.d.ts +2 -0
  109. package/dist/src/http/handlers/sse.js +70 -0
  110. package/dist/src/http/handlers/status.d.ts +2 -0
  111. package/dist/src/http/handlers/status.js +29 -0
  112. package/dist/src/http/handlers/sync.d.ts +7 -0
  113. package/dist/src/http/handlers/sync.js +56 -0
  114. package/dist/src/http/middleware/auth.d.ts +13 -0
  115. package/dist/src/http/middleware/auth.js +29 -0
  116. package/dist/src/http/middleware/body.d.ts +2 -0
  117. package/dist/src/http/middleware/body.js +24 -0
  118. package/dist/src/http/middleware/cors.d.ts +2 -0
  119. package/dist/src/http/middleware/cors.js +11 -0
  120. package/dist/src/http/server.d.ts +19 -0
  121. package/dist/src/http/server.js +87 -0
  122. package/dist/src/logging.d.ts +7 -0
  123. package/dist/src/logging.js +28 -0
  124. package/dist/src/push/apns.d.ts +15 -0
  125. package/dist/src/push/apns.js +56 -0
  126. package/dist/src/push/device-tokens.d.ts +3 -0
  127. package/dist/src/push/device-tokens.js +39 -0
  128. package/dist/src/run-metadata.d.ts +25 -0
  129. package/dist/src/run-metadata.js +139 -0
  130. package/dist/src/runtime.d.ts +13 -0
  131. package/dist/src/runtime.js +5 -0
  132. package/dist/src/session/session-manager.d.ts +22 -0
  133. package/dist/src/session/session-manager.js +190 -0
  134. package/dist/src/session-usage-snapshot.d.ts +23 -0
  135. package/dist/src/session-usage-snapshot.js +65 -0
  136. package/dist/src/sse/emitter.d.ts +59 -0
  137. package/dist/src/sse/emitter.js +219 -0
  138. package/dist/src/sse/offline-queue.d.ts +26 -0
  139. package/dist/src/sse/offline-queue.js +134 -0
  140. package/dist/src/sync/account-identity.d.ts +14 -0
  141. package/dist/src/sync/account-identity.js +101 -0
  142. package/dist/src/sync/archive.d.ts +9 -0
  143. package/dist/src/sync/archive.js +25 -0
  144. package/dist/src/sync/database.d.ts +66 -0
  145. package/dist/src/sync/database.js +364 -0
  146. package/dist/src/sync/init.d.ts +3 -0
  147. package/dist/src/sync/init.js +14 -0
  148. package/dist/src/sync/installation-id.d.ts +1 -0
  149. package/dist/src/sync/installation-id.js +41 -0
  150. package/dist/src/sync/message-accumulator.d.ts +29 -0
  151. package/dist/src/sync/message-accumulator.js +188 -0
  152. package/dist/src/sync/message-store.d.ts +68 -0
  153. package/dist/src/sync/message-store.js +262 -0
  154. package/dist/src/sync/push-store.d.ts +5 -0
  155. package/dist/src/sync/push-store.js +54 -0
  156. package/dist/src/sync/session-key.d.ts +12 -0
  157. package/dist/src/sync/session-key.js +47 -0
  158. package/dist/src/sync/sync-state.d.ts +5 -0
  159. package/dist/src/sync/sync-state.js +54 -0
  160. package/dist/src/sync/transcript-archive.d.ts +13 -0
  161. package/dist/src/sync/transcript-archive.js +37 -0
  162. package/dist/src/sync/transcript-store.d.ts +35 -0
  163. package/dist/src/sync/transcript-store.js +221 -0
  164. package/dist/src/sync/translate.d.ts +42 -0
  165. package/dist/src/sync/translate.js +171 -0
  166. package/dist/src/vendor/runtime-store.d.ts +26 -0
  167. package/dist/src/vendor/runtime-store.js +60 -0
  168. package/package.json +11 -10
  169. package/src/agent/subagent-registry.ts +195 -0
  170. package/src/channel.ts +6 -4
  171. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  172. package/src/e2e/subagent.e2e.test.ts +502 -0
  173. package/src/friday-session.ts +140 -1
  174. package/src/http/handlers/device-approve.test.ts +0 -1
  175. package/src/http/handlers/device-approve.ts +0 -2
  176. package/src/http/handlers/files-download.ts +4 -1
  177. package/src/http/handlers/files.ts +7 -4
  178. package/src/http/handlers/messages.ts +54 -4
  179. package/src/http/handlers/models-list.ts +24 -2
  180. package/src/http/handlers/nodes-approve.test.ts +288 -0
  181. package/src/http/handlers/nodes-approve.ts +189 -0
  182. package/src/http/server.ts +5 -0
  183. package/src/openclaw.d.ts +5 -0
  184. package/src/sse/emitter.ts +1 -1
  185. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * File manager for Friday Next channel attachments.
3
+ *
4
+ * Files are copied under the plugin root `attachments/` and served at
5
+ * GET /friday-next/files/{token} so the app can use stable gateway URLs after restarts.
6
+ */
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import { createFridayNextLogger } from "../../logging.js";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ function getPluginRootDir() {
14
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
15
+ }
16
+ /** Plugin-root `attachments/` directory; created on first use. */
17
+ export function getAttachmentsDir() {
18
+ const dir = path.join(getPluginRootDir(), "attachments");
19
+ try {
20
+ fs.mkdirSync(dir, { recursive: true });
21
+ }
22
+ catch {
23
+ // Already exists or permission denied
24
+ }
25
+ return dir;
26
+ }
27
+ /** In-memory index of stored files (keys: uuid id and urlToken / disk basename). */
28
+ const fileIndex = new Map();
29
+ const fileTokenIndex = new Map();
30
+ const externalFileSourceIndex = new Map();
31
+ const logger = createFridayNextLogger("files");
32
+ function registerStoredFile(file) {
33
+ fileIndex.set(file.id, file);
34
+ fileIndex.set(file.urlToken, file);
35
+ fileTokenIndex.set(file.id, file);
36
+ fileTokenIndex.set(file.urlToken, file);
37
+ }
38
+ function resolveStoredFile(key) {
39
+ return fileIndex.get(key) ?? fileTokenIndex.get(key);
40
+ }
41
+ /**
42
+ * Read a file from `attachments/` by URL path token (disk basename).
43
+ * Used when the in-memory index was cleared after a gateway restart.
44
+ */
45
+ export function readAttachmentFileFromDisk(fileToken) {
46
+ const safe = path.basename(fileToken);
47
+ if (!safe || safe === "." || safe === "..")
48
+ return null;
49
+ const dir = getAttachmentsDir();
50
+ const full = path.join(dir, safe);
51
+ if (!fs.existsSync(full) || !fs.statSync(full).isFile())
52
+ return null;
53
+ try {
54
+ const buffer = fs.readFileSync(full);
55
+ return { buffer, mimeType: guessMimeType(safe), filename: safe };
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Copy a local file into `attachments/` and register it (no full-buffer read for the copy path).
63
+ */
64
+ /** Expand ~, file://, etc. for paths coming from the agent / message tool. */
65
+ export function normalizeAgentMediaPath(raw) {
66
+ const s = raw.trim();
67
+ if (!s)
68
+ return s;
69
+ try {
70
+ if (/^file:/i.test(s)) {
71
+ return fileURLToPath(s);
72
+ }
73
+ }
74
+ catch {
75
+ // ignore malformed file URL
76
+ }
77
+ if (s.startsWith("~/") || s.startsWith("~\\")) {
78
+ return path.join(os.homedir(), s.slice(2));
79
+ }
80
+ if (s === "~") {
81
+ return os.homedir();
82
+ }
83
+ return s;
84
+ }
85
+ function copyLocalFileToAttachments(sourcePath) {
86
+ const resolvedPath = normalizeAgentMediaPath(sourcePath);
87
+ const filename = path.basename(resolvedPath);
88
+ if (!filename)
89
+ return null;
90
+ try {
91
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile())
92
+ return null;
93
+ const id = crypto.randomUUID();
94
+ const ext = path.extname(filename);
95
+ const urlToken = ext ? `${id}${ext}` : id;
96
+ const storedPath = path.join(getAttachmentsDir(), urlToken);
97
+ try {
98
+ fs.copyFileSync(resolvedPath, storedPath);
99
+ }
100
+ catch (copyErr) {
101
+ // macOS Desktop/iCloud files may fail copyFile with unknown errno (-11).
102
+ // Fallback to read+write so attachment persistence still works.
103
+ const raw = fs.readFileSync(resolvedPath);
104
+ fs.writeFileSync(storedPath, raw);
105
+ logger.warn(`copyLocalFileToAttachments copy fallback used for "${resolvedPath}": ${String(copyErr)}`);
106
+ }
107
+ const stat = fs.statSync(storedPath);
108
+ const mimeType = guessMimeType(filename);
109
+ const file = {
110
+ id,
111
+ urlToken,
112
+ filename,
113
+ mimeType,
114
+ size: stat.size,
115
+ path: storedPath,
116
+ createdAt: Date.now(),
117
+ };
118
+ registerStoredFile(file);
119
+ return file;
120
+ }
121
+ catch (err) {
122
+ logger.error(`copyLocalFileToAttachments failed for "${resolvedPath}": ${String(err)}`);
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Store a file buffer and return its ID and metadata.
128
+ */
129
+ export function storeFile(buffer, filename, mimeType) {
130
+ const id = crypto.randomUUID();
131
+ const safeFilename = path.basename(filename) || "file";
132
+ const ext = path.extname(safeFilename);
133
+ const urlToken = ext ? `${id}${ext}` : id;
134
+ const storedPath = path.join(getAttachmentsDir(), urlToken);
135
+ try {
136
+ fs.writeFileSync(storedPath, buffer);
137
+ }
138
+ catch (err) {
139
+ throw new Error(`Failed to store file: ${String(err)}`);
140
+ }
141
+ const file = {
142
+ id,
143
+ urlToken,
144
+ filename: safeFilename,
145
+ mimeType,
146
+ size: buffer.length,
147
+ path: storedPath,
148
+ createdAt: Date.now(),
149
+ };
150
+ registerStoredFile(file);
151
+ return file;
152
+ }
153
+ /**
154
+ * Retrieve file metadata by ID or url token.
155
+ */
156
+ export function getFile(id) {
157
+ return resolveStoredFile(id);
158
+ }
159
+ /**
160
+ * Path segment for lookup: raw uuid / urlToken, or token extracted from `/friday-next/files/...`.
161
+ */
162
+ export function fridayAttachmentLookupKey(ref) {
163
+ const s = ref.trim();
164
+ if (!s)
165
+ return s;
166
+ if (s.startsWith("/friday-next/files/")) {
167
+ return decodeURIComponent(s.slice("/friday-next/files/".length));
168
+ }
169
+ return s;
170
+ }
171
+ /**
172
+ * Canonical gateway URL `/friday-next/files/{urlToken}` with extension when stored (for history, MediaUrls).
173
+ */
174
+ export function fridayFilesPublicUrl(ref) {
175
+ const lookupKey = fridayAttachmentLookupKey(ref);
176
+ if (!lookupKey)
177
+ return ref.trim();
178
+ const file = resolveStoredFile(lookupKey);
179
+ if (file) {
180
+ return `/friday-next/files/${encodeURIComponent(file.urlToken)}`;
181
+ }
182
+ const disk = readAttachmentFileFromDisk(lookupKey);
183
+ if (disk) {
184
+ return `/friday-next/files/${encodeURIComponent(disk.filename)}`;
185
+ }
186
+ const trimmed = ref.trim();
187
+ if (trimmed.startsWith("/friday-next/files/")) {
188
+ return `/friday-next/files/${encodeURIComponent(lookupKey)}`;
189
+ }
190
+ return `/friday-next/files/${encodeURIComponent(lookupKey)}`;
191
+ }
192
+ export function getExternalFileSourceByUrlToken(token) {
193
+ return externalFileSourceIndex.get(token);
194
+ }
195
+ /**
196
+ * Read a file as a Buffer with its MIME type (by id or urlToken).
197
+ */
198
+ export function readFile(id) {
199
+ const file = resolveStoredFile(id);
200
+ if (!file)
201
+ return { buffer: null, mimeType: "application/octet-stream" };
202
+ try {
203
+ return { buffer: fs.readFileSync(file.path), mimeType: file.mimeType };
204
+ }
205
+ catch {
206
+ return { buffer: null, mimeType: file.mimeType };
207
+ }
208
+ }
209
+ /**
210
+ * Copy a file from a local filesystem path into the Friday Next channel file store
211
+ * and return its /friday-next/files/{token} URL. If the path is already a Friday Next channel
212
+ * file URL (i.e. starts with "/friday-next/files/"), return it as-is.
213
+ */
214
+ export function resolveMediaUrl(localPath) {
215
+ if (localPath.startsWith("/friday-next/files/")) {
216
+ return localPath;
217
+ }
218
+ const stored = copyLocalFileToAttachments(localPath);
219
+ if (!stored) {
220
+ logger.error(`resolveMediaUrl: file not found or unreadable: ${localPath}`);
221
+ return localPath;
222
+ }
223
+ logger.info(`resolveMediaUrl: copied "${stored.filename}" → ${stored.urlToken}`);
224
+ return `/friday-next/files/${encodeURIComponent(stored.urlToken)}`;
225
+ }
226
+ /**
227
+ * Resolve a local path into a Friday-served attachment descriptor.
228
+ * Returns null when source file is missing or cannot be copied.
229
+ */
230
+ export function resolveMediaAttachment(localPath) {
231
+ if (localPath.startsWith("/friday-next/files/")) {
232
+ const token = decodeURIComponent(localPath.slice("/friday-next/files/".length));
233
+ const file = resolveStoredFile(token);
234
+ if (file) {
235
+ return {
236
+ fileName: file.filename,
237
+ url: `/friday-next/files/${encodeURIComponent(file.urlToken)}`,
238
+ };
239
+ }
240
+ const disk = readAttachmentFileFromDisk(token);
241
+ if (disk) {
242
+ return {
243
+ fileName: disk.filename,
244
+ url: `/friday-next/files/${encodeURIComponent(token)}`,
245
+ };
246
+ }
247
+ const fallback = path.basename(token);
248
+ return { fileName: fallback, url: localPath };
249
+ }
250
+ const filename = path.basename(localPath);
251
+ if (!filename)
252
+ return null;
253
+ const stored = copyLocalFileToAttachments(localPath);
254
+ if (!stored) {
255
+ // Best-effort fallback: still return a Friday URL so app can receive attachment event.
256
+ // Download handler will try reading external source path lazily by token.
257
+ const id = crypto.randomUUID();
258
+ const ext = path.extname(filename);
259
+ const token = ext ? `${id}${ext}` : id;
260
+ externalFileSourceIndex.set(token, normalizeAgentMediaPath(localPath));
261
+ return {
262
+ fileName: filename,
263
+ url: `/friday-next/files/${encodeURIComponent(token)}`,
264
+ };
265
+ }
266
+ return {
267
+ fileName: stored.filename,
268
+ url: `/friday-next/files/${encodeURIComponent(stored.urlToken)}`,
269
+ };
270
+ }
271
+ /**
272
+ * Guess MIME type from filename extension.
273
+ */
274
+ export function guessMimeType(filename) {
275
+ const ext = path.extname(filename).toLowerCase();
276
+ const mimeTypes = {
277
+ ".png": "image/png",
278
+ ".jpg": "image/jpeg",
279
+ ".jpeg": "image/jpeg",
280
+ ".gif": "image/gif",
281
+ ".webp": "image/webp",
282
+ ".heic": "image/heic",
283
+ ".pdf": "application/pdf",
284
+ ".mp4": "video/mp4",
285
+ ".mov": "video/quicktime",
286
+ ".mp3": "audio/mpeg",
287
+ ".wav": "audio/wav",
288
+ ".ogg": "audio/ogg",
289
+ ".opus": "audio/opus",
290
+ ".m4a": "audio/mp4",
291
+ ".aac": "audio/aac",
292
+ ".flac": "audio/flac",
293
+ ".zip": "application/zip",
294
+ ".txt": "text/plain",
295
+ ".md": "text/markdown",
296
+ ".markdown": "text/markdown",
297
+ ".doc": "application/msword",
298
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
299
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
300
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
301
+ ".csv": "text/csv",
302
+ ".json": "application/json",
303
+ };
304
+ return mimeTypes[ext] ?? "application/octet-stream";
305
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * POST /friday-next/sessions/{sessionKey}/import
3
+ *
4
+ * First-time bulk import. Rejects if session already has data (409 Conflict).
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from "node:http";
7
+ export declare function handleImport(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,69 @@
1
+ import { extractBearerToken } from "../middleware/auth.js";
2
+ import { readJsonBody } from "../middleware/body.js";
3
+ import { importMessages, ConflictError } from "../../sync/message-store.js";
4
+ import { toSessionStoreKey } from "../../session/session-manager.js";
5
+ export async function handleImport(req, res) {
6
+ if (req.method !== "POST") {
7
+ res.statusCode = 405;
8
+ res.setHeader("Content-Type", "application/json");
9
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
10
+ return true;
11
+ }
12
+ const token = extractBearerToken(req);
13
+ if (!token) {
14
+ res.statusCode = 401;
15
+ res.setHeader("Content-Type", "application/json");
16
+ res.end(JSON.stringify({ error: "Unauthorized" }));
17
+ return true;
18
+ }
19
+ const url = new URL(req.url ?? "/", "http://localhost");
20
+ const pathname = url.pathname;
21
+ const prefix = "/friday-next/sessions/";
22
+ const suffix = "/import";
23
+ if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) {
24
+ res.statusCode = 400;
25
+ res.setHeader("Content-Type", "application/json");
26
+ res.end(JSON.stringify({ error: "Missing sessionKey in URL path" }));
27
+ return true;
28
+ }
29
+ const rawKey = pathname.slice(prefix.length, pathname.length - suffix.length);
30
+ const sessionKey = toSessionStoreKey(decodeURIComponent(rawKey));
31
+ const body = await readJsonBody(req, 50 * 1024 * 1024); // 50 MB for bulk import
32
+ if (!body) {
33
+ res.statusCode = 400;
34
+ res.setHeader("Content-Type", "application/json");
35
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
36
+ return true;
37
+ }
38
+ const deviceId = typeof body.deviceId === "string" ? body.deviceId.trim() : "";
39
+ if (!deviceId) {
40
+ res.statusCode = 400;
41
+ res.setHeader("Content-Type", "application/json");
42
+ res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
43
+ return true;
44
+ }
45
+ const messages = Array.isArray(body.messages) ? body.messages : [];
46
+ if (messages.length === 0) {
47
+ res.statusCode = 400;
48
+ res.setHeader("Content-Type", "application/json");
49
+ res.end(JSON.stringify({ error: "Missing required field: messages (non-empty array)" }));
50
+ return true;
51
+ }
52
+ try {
53
+ const result = importMessages({ sessionKey, deviceId, messages });
54
+ res.statusCode = 200;
55
+ res.setHeader("Content-Type", "application/json");
56
+ res.end(JSON.stringify(result));
57
+ }
58
+ catch (err) {
59
+ if (err instanceof ConflictError) {
60
+ res.statusCode = 409;
61
+ res.setHeader("Content-Type", "application/json");
62
+ res.end(JSON.stringify({ error: err.message }));
63
+ }
64
+ else {
65
+ throw err;
66
+ }
67
+ }
68
+ return true;
69
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleInfo(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,13 @@
1
+ import { getInstallationId } from "../../sync/installation-id.js";
2
+ export async function handleInfo(req, res) {
3
+ if (req.method !== "GET") {
4
+ res.statusCode = 405;
5
+ res.setHeader("Content-Type", "application/json");
6
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
7
+ return true;
8
+ }
9
+ res.statusCode = 200;
10
+ res.setHeader("Content-Type", "application/json");
11
+ res.end(JSON.stringify({ ok: true, channel: "friday-next", version: "v2", installationId: getInstallationId() }));
12
+ return true;
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * GET /friday-next/sessions/{sessionKey}/messages?before=&limit=
3
+ *
4
+ * Paginated pull of historical messages for a session.
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from "node:http";
7
+ export declare function handleMessagesList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ import { extractBearerToken } from "../middleware/auth.js";
2
+ import { getMessagesBefore } from "../../sync/message-store.js";
3
+ import { toSessionStoreKey } from "../../session/session-manager.js";
4
+ export async function handleMessagesList(req, res) {
5
+ if (req.method !== "GET") {
6
+ res.statusCode = 405;
7
+ res.setHeader("Content-Type", "application/json");
8
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
9
+ return true;
10
+ }
11
+ const token = extractBearerToken(req);
12
+ if (!token) {
13
+ res.statusCode = 401;
14
+ res.setHeader("Content-Type", "application/json");
15
+ res.end(JSON.stringify({ error: "Unauthorized" }));
16
+ return true;
17
+ }
18
+ const url = new URL(req.url ?? "/", "http://localhost");
19
+ const pathname = url.pathname;
20
+ const prefix = "/friday-next/sessions/";
21
+ const suffix = "/messages";
22
+ if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) {
23
+ res.statusCode = 400;
24
+ res.setHeader("Content-Type", "application/json");
25
+ res.end(JSON.stringify({ error: "Missing sessionKey in URL path" }));
26
+ return true;
27
+ }
28
+ const rawKey = pathname.slice(prefix.length, pathname.length - suffix.length);
29
+ const sessionKey = toSessionStoreKey(decodeURIComponent(rawKey));
30
+ const before = url.searchParams.get("before") ?? undefined;
31
+ const limitRaw = url.searchParams.get("limit");
32
+ const limit = limitRaw ? parseInt(limitRaw, 10) : 25;
33
+ if (limitRaw && (isNaN(limit) || limit < 1 || limit > 50)) {
34
+ res.statusCode = 400;
35
+ res.setHeader("Content-Type", "application/json");
36
+ res.end(JSON.stringify({ error: "Invalid limit (1-50)" }));
37
+ return true;
38
+ }
39
+ const result = getMessagesBefore(sessionKey, before, limit);
40
+ res.statusCode = 200;
41
+ res.setHeader("Content-Type", "application/json");
42
+ res.end(JSON.stringify(result));
43
+ return true;
44
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Message handler for POST /friday-next/messages
3
+ *
4
+ * Dispatches to the OpenClaw agent and streams native events via SSE (transparent proxy).
5
+ *
6
+ * **Owner / `nodes` tool visibility (OpenClaw):** With `tools.profile: "coding"`, add
7
+ * `tools.alsoAllow: ["nodes"]` so profile filtering does not hide `nodes`. Bearer-authenticated
8
+ * requests set `SenderId` and `OwnerAllowFrom` on the dispatch context so
9
+ * `resolveCommandAuthorization` treats this device as owner when channel `allowFrom` is open
10
+ * (empty / wildcard) and `commands.ownerAllowFrom` is not already a non-matching explicit list.
11
+ */
12
+ import type { IncomingMessage, ServerResponse } from "node:http";
13
+ /** Subset of OpenClaw reply payload used for deliver translation (avoids static SDK import in tests). */
14
+ export type FridayReplyPayload = {
15
+ text?: string;
16
+ mediaUrls?: string[];
17
+ mediaUrl?: string | null;
18
+ isError?: boolean;
19
+ audioAsVoice?: boolean;
20
+ isReasoning?: boolean;
21
+ isCompactionNotice?: boolean;
22
+ interactive?: unknown;
23
+ channelData?: unknown;
24
+ };
25
+ export interface FridayMessagePayload {
26
+ deviceId: string;
27
+ text: string;
28
+ sessionKey: string;
29
+ attachments?: string[];
30
+ modelRef?: string;
31
+ reasoningLevel?: string;
32
+ thinkingLevel?: string;
33
+ }
34
+ export declare function handleMessages(req: IncomingMessage, res: ServerResponse): Promise<boolean>;