@syengup/friday-channel-next 0.0.34 → 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 (186) 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/install.js +2 -2
  169. package/package.json +11 -10
  170. package/src/agent/subagent-registry.ts +195 -0
  171. package/src/channel.ts +6 -4
  172. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  173. package/src/e2e/subagent.e2e.test.ts +502 -0
  174. package/src/friday-session.ts +140 -1
  175. package/src/http/handlers/device-approve.test.ts +0 -1
  176. package/src/http/handlers/device-approve.ts +0 -2
  177. package/src/http/handlers/files-download.ts +4 -1
  178. package/src/http/handlers/files.ts +7 -4
  179. package/src/http/handlers/messages.ts +54 -4
  180. package/src/http/handlers/models-list.ts +24 -2
  181. package/src/http/handlers/nodes-approve.test.ts +288 -0
  182. package/src/http/handlers/nodes-approve.ts +189 -0
  183. package/src/http/server.ts +5 -0
  184. package/src/openclaw.d.ts +5 -0
  185. package/src/sse/emitter.ts +1 -1
  186. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,146 @@
1
+ import { exec } from "node:child_process";
2
+ import { readJsonBody } from "../middleware/body.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+ import { createFridayNextLogger } from "../../logging.js";
5
+ const EXEC_ENV = process.platform === "win32"
6
+ ? process.env
7
+ : { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:${process.env.PATH ?? ""}` };
8
+ function execAsync(command, timeoutMs) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = exec(command, { encoding: "utf-8", timeout: timeoutMs, maxBuffer: 1024 * 1024, env: EXEC_ENV }, (error, stdout, stderr) => {
11
+ if (error) {
12
+ reject(error);
13
+ }
14
+ else {
15
+ resolve({ stdout, stderr });
16
+ }
17
+ });
18
+ child.stdout?.on("data", () => { });
19
+ child.stderr?.on("data", () => { });
20
+ });
21
+ }
22
+ export async function handleNodesApprove(req, res) {
23
+ const log = createFridayNextLogger("nodes-approve");
24
+ if (req.method !== "POST") {
25
+ res.statusCode = 405;
26
+ res.setHeader("Content-Type", "application/json");
27
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
28
+ return true;
29
+ }
30
+ const token = extractBearerToken(req);
31
+ if (!token) {
32
+ res.statusCode = 401;
33
+ res.setHeader("Content-Type", "application/json");
34
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
35
+ return true;
36
+ }
37
+ const body = await readJsonBody(req);
38
+ if (!body) {
39
+ res.statusCode = 400;
40
+ res.setHeader("Content-Type", "application/json");
41
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
42
+ return true;
43
+ }
44
+ const rawNodeId = typeof body.nodeId === "string" ? body.nodeId : "";
45
+ if (!rawNodeId.trim()) {
46
+ res.statusCode = 400;
47
+ res.setHeader("Content-Type", "application/json");
48
+ res.end(JSON.stringify({ error: "Missing required field: nodeId" }));
49
+ return true;
50
+ }
51
+ const normalizedNodeId = rawNodeId.trim().toUpperCase();
52
+ let listStdout;
53
+ try {
54
+ const result = await execAsync("openclaw nodes list --json", 15000);
55
+ listStdout = result.stdout;
56
+ }
57
+ catch (err) {
58
+ const stderr = err?.stderr?.trim();
59
+ log.error(`nodes list failed: ${err instanceof Error ? err.message : String(err)}`);
60
+ res.statusCode = 502;
61
+ res.setHeader("Content-Type", "application/json");
62
+ res.end(JSON.stringify({ error: "Failed to list nodes from gateway", detail: stderr || undefined }));
63
+ return true;
64
+ }
65
+ let listData;
66
+ try {
67
+ listData = JSON.parse(listStdout);
68
+ }
69
+ catch {
70
+ log.error(`nodes list returned invalid JSON: ${listStdout.slice(0, 200)}`);
71
+ res.statusCode = 502;
72
+ res.setHeader("Content-Type", "application/json");
73
+ res.end(JSON.stringify({ error: "Unexpected response from gateway node list" }));
74
+ return true;
75
+ }
76
+ const pending = listData.pending ?? [];
77
+ const pendingMatch = pending.find((entry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId);
78
+ if (pendingMatch) {
79
+ const requestId = pendingMatch.requestId;
80
+ log.info(`approving nodeId=${normalizedNodeId} requestId=${requestId}`);
81
+ let approveStdout;
82
+ try {
83
+ const result = await execAsync(`openclaw nodes approve ${requestId} --json`, 15000);
84
+ approveStdout = result.stdout;
85
+ }
86
+ catch (err) {
87
+ const stderr = err?.stderr?.trim();
88
+ log.error(`nodes approve failed: ${err instanceof Error ? err.message : String(err)}`);
89
+ res.statusCode = 502;
90
+ res.setHeader("Content-Type", "application/json");
91
+ res.end(JSON.stringify({
92
+ error: "Node approval command failed",
93
+ detail: stderr || (err instanceof Error ? err.message : "Unknown error"),
94
+ }));
95
+ return true;
96
+ }
97
+ let approveData;
98
+ try {
99
+ approveData = JSON.parse(approveStdout);
100
+ }
101
+ catch {
102
+ log.error(`nodes approve returned non-JSON: ${approveStdout.slice(0, 200)}`);
103
+ res.statusCode = 502;
104
+ res.setHeader("Content-Type", "application/json");
105
+ res.end(JSON.stringify({ error: "Unexpected response from node approval" }));
106
+ return true;
107
+ }
108
+ res.statusCode = 200;
109
+ res.setHeader("Content-Type", "application/json");
110
+ res.end(JSON.stringify({
111
+ ok: true,
112
+ nodeId: normalizedNodeId,
113
+ requestId: approveData.requestId,
114
+ approvedAtMs: approveData.node?.approvedAtMs,
115
+ }));
116
+ return true;
117
+ }
118
+ // Not in pending — check if already paired with non-empty caps/commands
119
+ const paired = listData.paired ?? [];
120
+ const pairedMatch = paired.find((entry) => entry.nodeId.trim().toUpperCase() === normalizedNodeId);
121
+ if (pairedMatch) {
122
+ const caps = pairedMatch.caps ?? [];
123
+ const commands = pairedMatch.commands ?? [];
124
+ if (caps.length > 0 || commands.length > 0) {
125
+ log.info(`nodeId=${normalizedNodeId} already paired with caps=${caps.length} commands=${commands.length}`);
126
+ res.statusCode = 200;
127
+ res.setHeader("Content-Type", "application/json");
128
+ res.end(JSON.stringify({
129
+ ok: true,
130
+ nodeId: normalizedNodeId,
131
+ alreadyApproved: true,
132
+ approvedAtMs: pairedMatch.approvedAtMs,
133
+ caps,
134
+ commands,
135
+ }));
136
+ return true;
137
+ }
138
+ }
139
+ res.statusCode = 404;
140
+ res.setHeader("Content-Type", "application/json");
141
+ res.end(JSON.stringify({
142
+ error: "No pending node found for this nodeId",
143
+ nodeId: normalizedNodeId,
144
+ }));
145
+ return true;
146
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handlePair(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,39 @@
1
+ import { getOrCreateAccountId } from "../../sync/account-identity.js";
2
+ import { getInstallationId } from "../../sync/installation-id.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+ import { readJsonBody } from "../middleware/body.js";
5
+ export async function handlePair(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
+ if (!extractBearerToken(req)) {
13
+ res.statusCode = 401;
14
+ res.setHeader("Content-Type", "application/json");
15
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
16
+ return true;
17
+ }
18
+ const body = await readJsonBody(req);
19
+ const deviceId = typeof body?.deviceId === "string" ? body.deviceId.trim() : "";
20
+ if (!deviceId) {
21
+ res.statusCode = 400;
22
+ res.setHeader("Content-Type", "application/json");
23
+ res.end(JSON.stringify({ error: "Missing required field: deviceId" }));
24
+ return true;
25
+ }
26
+ const claimAccountId = typeof body?.claimAccountId === "string" ? body.claimAccountId.trim() : undefined;
27
+ const claim = body?.claim && typeof body.claim === "object" && !Array.isArray(body.claim)
28
+ ? body.claim
29
+ : {};
30
+ const result = getOrCreateAccountId({
31
+ deviceId,
32
+ claimAccountId,
33
+ claimConfirmed: claim.confirmed === true,
34
+ });
35
+ res.statusCode = 200;
36
+ res.setHeader("Content-Type", "application/json");
37
+ res.end(JSON.stringify({ ok: true, installationId: getInstallationId(), ...result }));
38
+ return true;
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSessionsDelete(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,49 @@
1
+ import { deleteFridaySession, toSessionStoreKey } from "../../session/session-manager.js";
2
+ import { getActiveRunIds } from "../../agent/active-runs.js";
3
+ import { abortRun } from "../../agent/abort-run.js";
4
+ import { getRunRoute } from "../../run-metadata.js";
5
+ import { sseEmitter } from "../../sse/emitter.js";
6
+ import { readJsonBody } from "../middleware/body.js";
7
+ import { extractBearerToken } from "../middleware/auth.js";
8
+ async function cancelActiveRunsForSession(sessionKey) {
9
+ const storeKey = toSessionStoreKey(sessionKey);
10
+ const cancelled = [];
11
+ for (const runId of getActiveRunIds()) {
12
+ const route = getRunRoute(runId);
13
+ if (route?.sessionKey === storeKey) {
14
+ await abortRun(runId);
15
+ sseEmitter.untrackRun(runId);
16
+ cancelled.push(runId);
17
+ }
18
+ }
19
+ return cancelled;
20
+ }
21
+ export async function handleSessionsDelete(req, res) {
22
+ if (req.method !== "DELETE") {
23
+ res.statusCode = 405;
24
+ res.setHeader("Content-Type", "application/json");
25
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
26
+ return true;
27
+ }
28
+ const token = extractBearerToken(req);
29
+ if (!token) {
30
+ res.statusCode = 401;
31
+ res.setHeader("Content-Type", "application/json");
32
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
33
+ return true;
34
+ }
35
+ const body = await readJsonBody(req);
36
+ const sessionKey = typeof body?.sessionKey === "string" ? body.sessionKey.trim() : "";
37
+ if (!sessionKey) {
38
+ res.statusCode = 400;
39
+ res.setHeader("Content-Type", "application/json");
40
+ res.end(JSON.stringify({ error: "Missing required field: sessionKey" }));
41
+ return true;
42
+ }
43
+ const cancelledRuns = await cancelActiveRunsForSession(sessionKey);
44
+ const result = deleteFridaySession(sessionKey);
45
+ res.statusCode = 200;
46
+ res.setHeader("Content-Type", "application/json");
47
+ res.end(JSON.stringify({ ok: true, ...result, cancelledRuns }));
48
+ return true;
49
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * GET /friday-next/sessions?deviceId=...
3
+ *
4
+ * List sessions. If deviceId is provided, filters by device.
5
+ * If deviceId is omitted, returns all sessions (for app reinstall recovery when deviceId changed).
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from "node:http";
8
+ export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import { extractBearerToken } from "../middleware/auth.js";
2
+ import { listSessions, listAllSessionsList } from "../../sync/message-store.js";
3
+ export async function handleSessionsList(req, res) {
4
+ if (req.method !== "GET") {
5
+ res.statusCode = 405;
6
+ res.setHeader("Content-Type", "application/json");
7
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
8
+ return true;
9
+ }
10
+ const token = extractBearerToken(req);
11
+ if (!token) {
12
+ res.statusCode = 401;
13
+ res.setHeader("Content-Type", "application/json");
14
+ res.end(JSON.stringify({ error: "Unauthorized" }));
15
+ return true;
16
+ }
17
+ const url = new URL(req.url ?? "/", "http://localhost");
18
+ const deviceId = url.searchParams.get("deviceId")?.trim();
19
+ const sessions = deviceId ? listSessions(deviceId) : listAllSessionsList();
20
+ res.statusCode = 200;
21
+ res.setHeader("Content-Type", "application/json");
22
+ res.end(JSON.stringify({ sessions }));
23
+ return true;
24
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSessionMessagesGet(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,55 @@
1
+ import { resolveAccountIdFromRequest } from "../../sync/account-identity.js";
2
+ import { getTranscriptStore } from "../../sync/transcript-store.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+ function sessionKeyFromPath(pathname) {
5
+ const prefix = "/friday-next/sessions/";
6
+ const suffix = "/messages";
7
+ if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix))
8
+ return "";
9
+ return decodeURIComponent(pathname.slice(prefix.length, -suffix.length));
10
+ }
11
+ export async function handleSessionMessagesGet(req, res) {
12
+ if (req.method !== "GET") {
13
+ res.statusCode = 405;
14
+ res.setHeader("Content-Type", "application/json");
15
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
16
+ return true;
17
+ }
18
+ if (!extractBearerToken(req)) {
19
+ res.statusCode = 401;
20
+ res.setHeader("Content-Type", "application/json");
21
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
22
+ return true;
23
+ }
24
+ const accountId = resolveAccountIdFromRequest(req);
25
+ if (!accountId) {
26
+ res.statusCode = 403;
27
+ res.setHeader("Content-Type", "application/json");
28
+ res.end(JSON.stringify({ error: "Unknown or missing X-Account-Id" }));
29
+ return true;
30
+ }
31
+ const url = new URL(req.url ?? "/", "http://localhost");
32
+ const sessionKey = sessionKeyFromPath(url.pathname);
33
+ if (!sessionKey) {
34
+ res.statusCode = 400;
35
+ res.setHeader("Content-Type", "application/json");
36
+ res.end(JSON.stringify({ error: "Missing session key in path" }));
37
+ return true;
38
+ }
39
+ if (!sessionKey.includes(`friday:account:${accountId}:`) && !sessionKey.includes(`friday%3Aaccount%3A${accountId}%3A`)) {
40
+ const legacyAllowed = sessionKey.startsWith("friday:direct:") || sessionKey.startsWith("agent:main:friday:direct:");
41
+ if (!legacyAllowed) {
42
+ res.statusCode = 403;
43
+ res.setHeader("Content-Type", "application/json");
44
+ res.end(JSON.stringify({ error: "Session does not belong to account" }));
45
+ return true;
46
+ }
47
+ }
48
+ const since = Number.parseInt(url.searchParams.get("since") ?? "0", 10);
49
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
50
+ const events = getTranscriptStore().readSince(accountId, sessionKey, Number.isFinite(since) ? since : 0, limit);
51
+ res.statusCode = 200;
52
+ res.setHeader("Content-Type", "application/json");
53
+ res.end(JSON.stringify({ ok: true, events, nextSince: events.at(-1)?.seq ?? since }));
54
+ return true;
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSessionMessagesPost(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,92 @@
1
+ import { resolveAccountIdFromRequest } from "../../sync/account-identity.js";
2
+ import { getTranscriptStore } from "../../sync/transcript-store.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+ import { readJsonBody } from "../middleware/body.js";
5
+ const allowedKinds = new Set([
6
+ "user_message",
7
+ "assistant_message",
8
+ "thought_event",
9
+ "tool_event",
10
+ "outbound_text",
11
+ "outbound_media",
12
+ "sync_upsert",
13
+ ]);
14
+ function sessionKeyFromPath(pathname) {
15
+ const prefix = "/friday-next/sessions/";
16
+ const suffix = "/messages";
17
+ if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix))
18
+ return "";
19
+ return decodeURIComponent(pathname.slice(prefix.length, -suffix.length));
20
+ }
21
+ function parseEvent(raw, accountId) {
22
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
23
+ return null;
24
+ const obj = raw;
25
+ const kind = typeof obj.kind === "string" && allowedKinds.has(obj.kind)
26
+ ? obj.kind
27
+ : null;
28
+ if (!kind)
29
+ return null;
30
+ const payload = obj.payload && typeof obj.payload === "object" && !Array.isArray(obj.payload)
31
+ ? obj.payload
32
+ : null;
33
+ if (!payload)
34
+ return null;
35
+ return {
36
+ eventId: typeof obj.eventId === "string" ? obj.eventId.trim() : undefined,
37
+ clientId: typeof obj.clientId === "string" && obj.clientId.trim() ? obj.clientId.trim() : accountId,
38
+ kind,
39
+ payload,
40
+ ts: typeof obj.ts === "number" ? obj.ts : undefined,
41
+ lamportTs: typeof obj.lamportTs === "number" ? obj.lamportTs : undefined,
42
+ };
43
+ }
44
+ export async function handleSessionMessagesPost(req, res) {
45
+ if (req.method !== "POST") {
46
+ res.statusCode = 405;
47
+ res.setHeader("Content-Type", "application/json");
48
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
49
+ return true;
50
+ }
51
+ if (!extractBearerToken(req)) {
52
+ res.statusCode = 401;
53
+ res.setHeader("Content-Type", "application/json");
54
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
55
+ return true;
56
+ }
57
+ const accountId = resolveAccountIdFromRequest(req);
58
+ if (!accountId) {
59
+ res.statusCode = 403;
60
+ res.setHeader("Content-Type", "application/json");
61
+ res.end(JSON.stringify({ error: "Unknown or missing X-Account-Id" }));
62
+ return true;
63
+ }
64
+ const url = new URL(req.url ?? "/", "http://localhost");
65
+ const sessionKey = sessionKeyFromPath(url.pathname);
66
+ if (!sessionKey) {
67
+ res.statusCode = 400;
68
+ res.setHeader("Content-Type", "application/json");
69
+ res.end(JSON.stringify({ error: "Missing session key in path" }));
70
+ return true;
71
+ }
72
+ if (!sessionKey.includes(`friday:account:${accountId}:`) && !sessionKey.startsWith("friday:direct:")) {
73
+ res.statusCode = 403;
74
+ res.setHeader("Content-Type", "application/json");
75
+ res.end(JSON.stringify({ error: "Session does not belong to account" }));
76
+ return true;
77
+ }
78
+ const body = await readJsonBody(req, 8 * 1024 * 1024);
79
+ const rawEvents = Array.isArray(body?.events) ? body.events : [];
80
+ const events = rawEvents.map((e) => parseEvent(e, accountId)).filter((e) => e !== null);
81
+ if (events.length !== rawEvents.length) {
82
+ res.statusCode = 400;
83
+ res.setHeader("Content-Type", "application/json");
84
+ res.end(JSON.stringify({ error: "Invalid transcript event batch" }));
85
+ return true;
86
+ }
87
+ const appended = getTranscriptStore().appendEvents(accountId, sessionKey, events);
88
+ res.statusCode = 200;
89
+ res.setHeader("Content-Type", "application/json");
90
+ res.end(JSON.stringify({ ok: true, appended: appended.length, events: appended }));
91
+ return true;
92
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSessionsMessages(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,135 @@
1
+ import { resolveFridayNextConfig } from "../../config.js";
2
+ import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
3
+ import { getFridayNextRuntime } from "../../runtime.js";
4
+ import { accountExists, requireAccountId } from "../../sync/account-identity.js";
5
+ import { sessionBelongsToAccount } from "../../sync/session-key.js";
6
+ import { readSince, upsertEvents } from "../../sync/transcript-store.js";
7
+ import { extractBearerToken } from "../middleware/auth.js";
8
+ import { readJsonBody } from "../middleware/body.js";
9
+ const VALID_KINDS = new Set([
10
+ "user_message",
11
+ "assistant_message",
12
+ "thought_event",
13
+ "tool_event",
14
+ "outbound_text",
15
+ "outbound_media",
16
+ ]);
17
+ function decodeSessionKeyFromPath(pathname) {
18
+ const prefix = "/friday-next/sessions/";
19
+ if (!pathname.startsWith(prefix))
20
+ return null;
21
+ const rest = pathname.slice(prefix.length);
22
+ const messagesIdx = rest.indexOf("/messages");
23
+ if (messagesIdx < 0)
24
+ return null;
25
+ const encoded = rest.slice(0, messagesIdx);
26
+ if (!encoded)
27
+ return null;
28
+ try {
29
+ return decodeURIComponent(encoded);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export async function handleSessionsMessages(req, res) {
36
+ const url = new URL(req.url ?? "/", "http://localhost");
37
+ const sessionKey = decodeSessionKeyFromPath(url.pathname);
38
+ if (!sessionKey)
39
+ return false;
40
+ if (!extractBearerToken(req)) {
41
+ res.statusCode = 401;
42
+ res.setHeader("Content-Type", "application/json");
43
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
44
+ return true;
45
+ }
46
+ const accountId = requireAccountId(req);
47
+ if (!accountId) {
48
+ res.statusCode = 400;
49
+ res.setHeader("Content-Type", "application/json");
50
+ res.end(JSON.stringify({ error: "Missing or invalid X-Account-Id header" }));
51
+ return true;
52
+ }
53
+ const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
54
+ if (!accountExists(cfg.historyDir, accountId)) {
55
+ res.statusCode = 403;
56
+ res.setHeader("Content-Type", "application/json");
57
+ res.end(JSON.stringify({ error: "Unknown account" }));
58
+ return true;
59
+ }
60
+ if (!sessionBelongsToAccount(sessionKey, accountId)) {
61
+ res.statusCode = 403;
62
+ res.setHeader("Content-Type", "application/json");
63
+ res.end(JSON.stringify({ error: "Session does not belong to account" }));
64
+ return true;
65
+ }
66
+ if (req.method === "GET") {
67
+ const since = Math.max(0, Number.parseInt(url.searchParams.get("since") ?? "0", 10) || 0);
68
+ const limit = Math.min(cfg.syncMaxBatchSize, Math.max(1, Number.parseInt(url.searchParams.get("limit") ?? String(cfg.syncMaxBatchSize), 10) || cfg.syncMaxBatchSize));
69
+ const direction = url.searchParams.get("direction") === "desc" ? "desc" : "asc";
70
+ const events = readSince({
71
+ accountId,
72
+ sessionKey,
73
+ sinceSeq: since,
74
+ limit,
75
+ direction,
76
+ historyDir: cfg.historyDir,
77
+ });
78
+ res.statusCode = 200;
79
+ res.setHeader("Content-Type", "application/json");
80
+ res.end(JSON.stringify({ ok: true, accountId, sessionKey, events }));
81
+ return true;
82
+ }
83
+ if (req.method === "POST") {
84
+ const body = (await readJsonBody(req));
85
+ const rawEvents = Array.isArray(body?.events) ? body.events : [];
86
+ if (rawEvents.length > cfg.syncMaxBatchSize) {
87
+ res.statusCode = 400;
88
+ res.setHeader("Content-Type", "application/json");
89
+ res.end(JSON.stringify({ error: `Too many events (max ${cfg.syncMaxBatchSize})` }));
90
+ return true;
91
+ }
92
+ const parsed = rawEvents
93
+ .map((item) => {
94
+ if (!item || typeof item !== "object" || Array.isArray(item))
95
+ return null;
96
+ const o = item;
97
+ const kind = typeof o.kind === "string" ? o.kind : "";
98
+ if (!VALID_KINDS.has(kind))
99
+ return null;
100
+ const clientId = typeof o.clientId === "string" ? o.clientId : accountId;
101
+ const payload = o.payload && typeof o.payload === "object" && !Array.isArray(o.payload)
102
+ ? o.payload
103
+ : {};
104
+ return {
105
+ kind: kind,
106
+ clientId,
107
+ payload,
108
+ messageId: typeof o.messageId === "string" ? o.messageId : undefined,
109
+ lamport: typeof o.lamport === "number" ? o.lamport : undefined,
110
+ ts: typeof o.ts === "number" ? o.ts : undefined,
111
+ };
112
+ })
113
+ .filter((x) => x !== null);
114
+ const result = upsertEvents({
115
+ accountId,
116
+ sessionKey,
117
+ events: parsed,
118
+ historyDir: cfg.historyDir,
119
+ });
120
+ res.statusCode = 200;
121
+ res.setHeader("Content-Type", "application/json");
122
+ res.end(JSON.stringify({
123
+ ok: true,
124
+ accountId,
125
+ sessionKey,
126
+ applied: result.applied,
127
+ events: result.events,
128
+ }));
129
+ return true;
130
+ }
131
+ res.statusCode = 405;
132
+ res.setHeader("Content-Type", "application/json");
133
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
134
+ return true;
135
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSessionsSettings(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -0,0 +1,71 @@
1
+ import { setSessionSettings, getSessionSettings, splitModelRef, } from "../../session/session-manager.js";
2
+ import { readJsonBody } from "../middleware/body.js";
3
+ import { extractBearerToken } from "../middleware/auth.js";
4
+ const VALID_REASONING = new Set(["on", "off", "stream"]);
5
+ const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high"]);
6
+ export async function handleSessionsSettings(req, res) {
7
+ if (req.method !== "PUT" && req.method !== "GET") {
8
+ res.statusCode = 405;
9
+ res.setHeader("Content-Type", "application/json");
10
+ res.end(JSON.stringify({ error: "Method Not Allowed" }));
11
+ return true;
12
+ }
13
+ const token = extractBearerToken(req);
14
+ if (!token) {
15
+ res.statusCode = 401;
16
+ res.setHeader("Content-Type", "application/json");
17
+ res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
18
+ return true;
19
+ }
20
+ if (req.method === "GET") {
21
+ const url = new URL(req.url ?? "/", "http://localhost");
22
+ const sessionKey = (url.searchParams.get("sessionKey") ?? "").trim();
23
+ if (!sessionKey) {
24
+ res.statusCode = 400;
25
+ res.setHeader("Content-Type", "application/json");
26
+ res.end(JSON.stringify({ error: "Missing required query param: sessionKey" }));
27
+ return true;
28
+ }
29
+ const settings = getSessionSettings(sessionKey);
30
+ res.statusCode = 200;
31
+ res.setHeader("Content-Type", "application/json");
32
+ res.end(JSON.stringify({ ok: true, sessionKey, ...settings }));
33
+ return true;
34
+ }
35
+ // PUT
36
+ const body = await readJsonBody(req);
37
+ const sessionKey = typeof body?.sessionKey === "string" ? body.sessionKey.trim() : "";
38
+ if (!sessionKey) {
39
+ res.statusCode = 400;
40
+ res.setHeader("Content-Type", "application/json");
41
+ res.end(JSON.stringify({ error: "Missing required field: sessionKey" }));
42
+ return true;
43
+ }
44
+ const reasoningLevel = typeof body?.reasoningLevel === "string" ? body.reasoningLevel : undefined;
45
+ const thinkingLevel = typeof body?.thinkingLevel === "string" ? body.thinkingLevel : undefined;
46
+ const modelRef = typeof body?.modelRef === "string" ? body.modelRef : undefined;
47
+ const errors = [];
48
+ if (reasoningLevel !== undefined && !VALID_REASONING.has(reasoningLevel)) {
49
+ errors.push(`reasoningLevel must be one of: ${[...VALID_REASONING].join(", ")}`);
50
+ }
51
+ if (thinkingLevel !== undefined && !VALID_THINKING.has(thinkingLevel)) {
52
+ errors.push(`thinkingLevel must be one of: ${[...VALID_THINKING].join(", ")}`);
53
+ }
54
+ if (errors.length > 0) {
55
+ res.statusCode = 400;
56
+ res.setHeader("Content-Type", "application/json");
57
+ res.end(JSON.stringify({ error: errors.join("; ") }));
58
+ return true;
59
+ }
60
+ const settings = { reasoningLevel, thinkingLevel, modelRef };
61
+ if (modelRef) {
62
+ const split = splitModelRef(modelRef);
63
+ settings["providerOverride"] = split.provider;
64
+ settings["modelOverride"] = split.modelId;
65
+ }
66
+ const result = setSessionSettings(sessionKey, settings);
67
+ res.statusCode = 200;
68
+ res.setHeader("Content-Type", "application/json");
69
+ res.end(JSON.stringify({ ok: true, sessionKey, ...result }));
70
+ return true;
71
+ }
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare function handleSseStream(req: IncomingMessage, res: ServerResponse): Promise<boolean>;