@syengup/friday-channel-next 0.1.30 → 0.1.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 (154) hide show
  1. package/README.md +8 -4
  2. package/dist/index.js +1 -1
  3. package/dist/src/agent/abort-run.d.ts +12 -1
  4. package/dist/src/agent/abort-run.js +24 -9
  5. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  6. package/dist/src/agent/media-bridge.d.ts +8 -1
  7. package/dist/src/agent/media-bridge.js +23 -2
  8. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  9. package/dist/src/agent/node-pairing-bridge.js +6 -2
  10. package/dist/src/agent/subagent-registry.js +0 -3
  11. package/dist/src/agent-forward-runtime.d.ts +15 -0
  12. package/dist/src/agent-forward-runtime.js +2 -0
  13. package/dist/src/agent-id.d.ts +8 -0
  14. package/dist/src/agent-id.js +21 -0
  15. package/dist/src/channel-actions.js +48 -15
  16. package/dist/src/channel.js +22 -3
  17. package/dist/src/collect-message-media-paths.js +10 -1
  18. package/dist/src/friday-session.js +34 -10
  19. package/dist/src/history/normalize-message.js +22 -8
  20. package/dist/src/http/handlers/agent-config.d.ts +27 -0
  21. package/dist/src/http/handlers/agent-config.js +188 -0
  22. package/dist/src/http/handlers/agent-files.d.ts +21 -0
  23. package/dist/src/http/handlers/agent-files.js +137 -0
  24. package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
  25. package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
  26. package/dist/src/http/handlers/agents-list.js +1 -19
  27. package/dist/src/http/handlers/cancel.js +14 -6
  28. package/dist/src/http/handlers/device-approve.js +3 -1
  29. package/dist/src/http/handlers/files-download.js +6 -8
  30. package/dist/src/http/handlers/files.d.ts +16 -0
  31. package/dist/src/http/handlers/files.js +81 -13
  32. package/dist/src/http/handlers/health.js +18 -4
  33. package/dist/src/http/handlers/history-messages.js +1 -1
  34. package/dist/src/http/handlers/history-sessions.js +5 -3
  35. package/dist/src/http/handlers/messages.js +33 -14
  36. package/dist/src/http/handlers/models-list.d.ts +5 -0
  37. package/dist/src/http/handlers/models-list.js +9 -1
  38. package/dist/src/http/handlers/nodes-approve.js +1 -6
  39. package/dist/src/http/handlers/plugin-info.js +1 -1
  40. package/dist/src/http/handlers/sessions-settings.js +15 -10
  41. package/dist/src/http/server.js +27 -2
  42. package/dist/src/link-preview/og-parse.js +3 -1
  43. package/dist/src/link-preview/ssrf-guard.js +6 -2
  44. package/dist/src/media-fetch.js +4 -1
  45. package/dist/src/plugin-install-info.js +4 -1
  46. package/dist/src/session/session-manager.js +9 -3
  47. package/dist/src/session-usage-store.js +3 -1
  48. package/dist/src/skills-discovery.d.ts +59 -0
  49. package/dist/src/skills-discovery.js +252 -0
  50. package/dist/src/sse/offline-queue.js +4 -1
  51. package/dist/src/thinking-levels.d.ts +21 -0
  52. package/dist/src/thinking-levels.js +48 -0
  53. package/dist/src/tool-catalog.d.ts +53 -0
  54. package/dist/src/tool-catalog.js +191 -0
  55. package/dist/src/upgrade-runtime.d.ts +1 -1
  56. package/dist/src/version.js +4 -2
  57. package/index.ts +43 -35
  58. package/install.js +131 -43
  59. package/package.json +10 -1
  60. package/src/agent/abort-run.ts +23 -8
  61. package/src/agent/dispatch-bridge.ts +2 -1
  62. package/src/agent/media-bridge.test.ts +71 -0
  63. package/src/agent/media-bridge.ts +30 -1
  64. package/src/agent/node-pairing-bridge.ts +29 -15
  65. package/src/agent/run-usage-accumulator.ts +4 -2
  66. package/src/agent/subagent-registry.ts +0 -4
  67. package/src/agent-forward-runtime.ts +11 -0
  68. package/src/agent-id.ts +24 -0
  69. package/src/agent-run-context-bridge.ts +3 -1
  70. package/src/channel-actions.test.ts +57 -4
  71. package/src/channel-actions.ts +41 -15
  72. package/src/channel.lifecycle.test.ts +41 -0
  73. package/src/channel.outbound.test.ts +18 -4
  74. package/src/channel.ts +140 -120
  75. package/src/collect-message-media-paths.ts +15 -6
  76. package/src/config.ts +1 -4
  77. package/src/e2e/agents-list.e2e.test.ts +9 -2
  78. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  79. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  80. package/src/e2e/auto-approve.integration.test.ts +13 -7
  81. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  82. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  83. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  84. package/src/e2e/send-text.e2e.test.ts +11 -2
  85. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  86. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  87. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  88. package/src/e2e/subagent.e2e.test.ts +136 -53
  89. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  90. package/src/friday-session.forward-agent.test.ts +44 -12
  91. package/src/friday-session.ts +44 -20
  92. package/src/history/normalize-message.test.ts +35 -8
  93. package/src/history/normalize-message.ts +24 -12
  94. package/src/history/read-transcript.ts +1 -4
  95. package/src/http/handlers/agent-config.test.ts +212 -0
  96. package/src/http/handlers/agent-config.ts +232 -0
  97. package/src/http/handlers/agent-files.test.ts +136 -0
  98. package/src/http/handlers/agent-files.ts +149 -0
  99. package/src/http/handlers/agent-tools-catalog.ts +42 -0
  100. package/src/http/handlers/agents-list.test.ts +1 -5
  101. package/src/http/handlers/agents-list.ts +1 -22
  102. package/src/http/handlers/cancel.test.ts +23 -4
  103. package/src/http/handlers/cancel.ts +14 -6
  104. package/src/http/handlers/device-approve.test.ts +12 -3
  105. package/src/http/handlers/device-approve.ts +33 -21
  106. package/src/http/handlers/files-download.ts +17 -13
  107. package/src/http/handlers/files.test.ts +120 -0
  108. package/src/http/handlers/files.ts +115 -17
  109. package/src/http/handlers/health.test.ts +43 -11
  110. package/src/http/handlers/health.ts +22 -6
  111. package/src/http/handlers/history-messages.test.ts +51 -9
  112. package/src/http/handlers/history-messages.ts +4 -1
  113. package/src/http/handlers/history-sessions.test.ts +46 -9
  114. package/src/http/handlers/history-sessions.ts +5 -3
  115. package/src/http/handlers/history-set-title.test.ts +14 -5
  116. package/src/http/handlers/link-preview.test.ts +57 -16
  117. package/src/http/handlers/link-preview.ts +4 -1
  118. package/src/http/handlers/messages.test.ts +12 -8
  119. package/src/http/handlers/messages.ts +64 -21
  120. package/src/http/handlers/models-list.test.ts +114 -0
  121. package/src/http/handlers/models-list.ts +26 -8
  122. package/src/http/handlers/nodes-approve.test.ts +15 -4
  123. package/src/http/handlers/nodes-approve.ts +38 -40
  124. package/src/http/handlers/plugin-info.ts +5 -6
  125. package/src/http/handlers/plugin-upgrade.ts +4 -1
  126. package/src/http/handlers/sessions-settings.ts +16 -11
  127. package/src/http/handlers/sse.ts +3 -1
  128. package/src/http/server.ts +33 -6
  129. package/src/link-preview/og-parse.test.ts +6 -2
  130. package/src/link-preview/og-parse.ts +10 -3
  131. package/src/link-preview/preview-service.ts +4 -1
  132. package/src/link-preview/ssrf-guard.test.ts +78 -16
  133. package/src/link-preview/ssrf-guard.ts +7 -2
  134. package/src/media-fetch.test.ts +8 -3
  135. package/src/media-fetch.ts +5 -3
  136. package/src/openclaw.d.ts +41 -10
  137. package/src/plugin-install-info.ts +20 -9
  138. package/src/run-metadata.ts +2 -1
  139. package/src/session/session-manager.ts +19 -11
  140. package/src/session-usage-snapshot.ts +3 -1
  141. package/src/session-usage-store.ts +3 -1
  142. package/src/skills-discovery.test.ts +152 -0
  143. package/src/skills-discovery.ts +264 -0
  144. package/src/sse/emitter.test.ts +1 -1
  145. package/src/sse/emitter.ts +9 -3
  146. package/src/sse/offline-queue.ts +17 -8
  147. package/src/test-support/app-simulator.ts +17 -3
  148. package/src/test-support/mock-dispatch.ts +17 -4
  149. package/src/thinking-levels.test.ts +143 -0
  150. package/src/thinking-levels.ts +70 -0
  151. package/src/tool-catalog.ts +261 -0
  152. package/src/upgrade-runtime.ts +4 -2
  153. package/src/version.ts +6 -2
  154. package/tsconfig.json +1 -1
package/index.ts CHANGED
@@ -1,7 +1,10 @@
1
- import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
1
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
3
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
4
- import type { PluginHookBeforeToolCallEvent, PluginHookAfterToolCallEvent, PluginHookToolContext } from "openclaw/plugin-sdk/plugins/types";
3
+ import type {
4
+ PluginHookBeforeToolCallEvent,
5
+ PluginHookAfterToolCallEvent,
6
+ PluginHookToolContext,
7
+ } from "openclaw/plugin-sdk/plugins/types";
5
8
  import { fridayNextChannelPlugin } from "./src/channel.js";
6
9
  import { setFridayNextRuntime } from "./src/runtime.js";
7
10
  import { resolveFridayNextConfig } from "./src/config.js";
@@ -44,7 +47,7 @@ function deviceIdFromToolContext(ctx: PluginHookToolContext): string | null {
44
47
  const sk =
45
48
  typeof ctx.sessionKey === "string" && ctx.sessionKey.trim()
46
49
  ? ctx.sessionKey.trim()
47
- : (ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "";
50
+ : ((ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "");
48
51
  if (sk) {
49
52
  const d = resolveFridayDeviceIdForSessionKey(sk);
50
53
  if (d) return d;
@@ -83,7 +86,7 @@ export default defineChannelPluginEntry({
83
86
  id: "friday-next",
84
87
  name: "Friday Next",
85
88
  description: "Friday Next Apple 应用通道",
86
- plugin: fridayNextChannelPlugin as ChannelPlugin,
89
+ plugin: fridayNextChannelPlugin,
87
90
  setRuntime: setFridayNextRuntime,
88
91
  registerFull: (api: OpenClawPluginApi) => {
89
92
  setFridayAgentForwardRuntime(api);
@@ -93,7 +96,9 @@ export default defineChannelPluginEntry({
93
96
  lastApiRoutesRegistered = new WeakRef(api);
94
97
  registerFridayNextHttpRoutes(api);
95
98
  } else {
96
- const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
99
+ const cfg = resolveFridayNextConfig(
100
+ getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
101
+ );
97
102
  sseEmitter.setBacklogLimit(cfg.sseBacklogPerDevice);
98
103
  }
99
104
 
@@ -147,36 +152,39 @@ export default defineChannelPluginEntry({
147
152
  };
148
153
  });
149
154
 
150
- api.on("before_tool_call", (event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => {
151
- if (!shouldForwardToolEventToFriday(ctx)) return;
152
- const deviceId = deviceIdFromToolContext(ctx);
153
- const runId = ctx.runId ?? "(unknown)";
154
-
155
- const logLine = (detail: string) => {
156
- hookLogger.debug(
157
- `[TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
158
- );
159
- };
160
-
161
- if (!deviceId) {
162
- logLine("SKIP_no_deviceId");
163
- return;
164
- }
165
-
166
- logLine("START");
167
- sseEmitter.broadcastToolEvent(deviceId.toUpperCase(), runId, {
168
- type: "tool-hook",
169
- data: {
170
- when: "before",
171
- runId,
172
- deviceId: deviceId.toUpperCase(),
173
- sessionKey: ctx.sessionKey,
174
- toolName: event.toolName,
175
- params: event.params,
176
- ts: Date.now(),
177
- },
178
- });
179
- });
155
+ api.on(
156
+ "before_tool_call",
157
+ (event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => {
158
+ if (!shouldForwardToolEventToFriday(ctx)) return;
159
+ const deviceId = deviceIdFromToolContext(ctx);
160
+ const runId = ctx.runId ?? "(unknown)";
161
+
162
+ const logLine = (detail: string) => {
163
+ hookLogger.debug(
164
+ `[TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
165
+ );
166
+ };
167
+
168
+ if (!deviceId) {
169
+ logLine("SKIP_no_deviceId");
170
+ return;
171
+ }
172
+
173
+ logLine("START");
174
+ sseEmitter.broadcastToolEvent(deviceId.toUpperCase(), runId, {
175
+ type: "tool-hook",
176
+ data: {
177
+ when: "before",
178
+ runId,
179
+ deviceId: deviceId.toUpperCase(),
180
+ sessionKey: ctx.sessionKey,
181
+ toolName: event.toolName,
182
+ params: event.params,
183
+ ts: Date.now(),
184
+ },
185
+ });
186
+ },
187
+ );
180
188
 
181
189
  api.on("after_tool_call", (event: PluginHookAfterToolCallEvent, ctx: PluginHookToolContext) => {
182
190
  if (!shouldForwardToolEventToFriday(ctx)) return;
package/install.js CHANGED
@@ -14,11 +14,7 @@ function realHome() {
14
14
  const h = execSync(`sh -c 'echo ~${sudoUser}'`, { encoding: "utf8" }).trim();
15
15
  if (h && !h.startsWith("~") && existsSync(h)) return h;
16
16
  } catch {}
17
- for (const g of [
18
- `/home/${sudoUser}`,
19
- `/Users/${sudoUser}`,
20
- `C:\\Users\\${sudoUser}`,
21
- ]) {
17
+ for (const g of [`/home/${sudoUser}`, `/Users/${sudoUser}`, `C:\\Users\\${sudoUser}`]) {
22
18
  if (existsSync(g)) return g;
23
19
  }
24
20
  return current;
@@ -30,13 +26,23 @@ const OPENCLAW_CONFIG = join(USER_HOME, ".openclaw", "openclaw.json");
30
26
  const G = (s) => `\x1b[32m${s}\x1b[0m`;
31
27
  const Y = (s) => `\x1b[33m${s}\x1b[0m`;
32
28
  const R = (s) => `\x1b[31m${s}\x1b[0m`;
33
- function log(msg) { console.log(` ${msg}`); }
34
- function warn(msg) { console.log(` ${Y("!")} ${msg}`); }
35
- function err(msg) { console.error(` ${R("X")} ${msg}`); }
29
+ function log(msg) {
30
+ console.log(` ${msg}`);
31
+ }
32
+ function warn(msg) {
33
+ console.log(` ${Y("!")} ${msg}`);
34
+ }
35
+ function err(msg) {
36
+ console.error(` ${R("X")} ${msg}`);
37
+ }
36
38
 
37
39
  function has(cmd) {
38
- try { execSync(`${cmd} --version`, { stdio: "ignore" }); return true; }
39
- catch { return false; }
40
+ try {
41
+ execSync(`${cmd} --version`, { stdio: "ignore" });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
40
46
  }
41
47
 
42
48
  let openclawCmd = "openclaw";
@@ -79,7 +85,10 @@ if (!hasOpenclaw()) {
79
85
  let tooOld = false;
80
86
  for (let i = 0; i < 3; i++) {
81
87
  if (cur[i] > MIN_OPENCLAW[i]) break;
82
- if (cur[i] < MIN_OPENCLAW[i]) { tooOld = true; break; }
88
+ if (cur[i] < MIN_OPENCLAW[i]) {
89
+ tooOld = true;
90
+ break;
91
+ }
83
92
  }
84
93
  if (tooOld) {
85
94
  err(`OpenClaw version ${m[0]} is too old.`);
@@ -100,7 +109,7 @@ log("Installing Friday Next channel plugin...");
100
109
  try {
101
110
  const out = execSync(
102
111
  `${openclawCmd} plugins install @syengup/friday-channel-next@latest --force`,
103
- { encoding: "utf8", stdio: "pipe", timeout: 120000 }
112
+ { encoding: "utf8", stdio: "pipe", timeout: 120000 },
104
113
  );
105
114
  if (out.trim()) console.log(out.trim());
106
115
  log("Plugin registered with install record — auto-upgrade enabled.");
@@ -108,8 +117,12 @@ try {
108
117
  // Remove old manual install to avoid "duplicate plugin id" warning.
109
118
  const legacyDir = join(USER_HOME, ".openclaw", "extensions", "friday-channel-next");
110
119
  if (existsSync(legacyDir)) {
111
- try { rmSync(legacyDir, { recursive: true, force: true }); log("Removed legacy manual install."); }
112
- catch { /* non-critical */ }
120
+ try {
121
+ rmSync(legacyDir, { recursive: true, force: true });
122
+ log("Removed legacy manual install.");
123
+ } catch {
124
+ /* non-critical */
125
+ }
113
126
  }
114
127
  } catch (e) {
115
128
  const msg = (e.stderr || e.stdout || e.message || "").toString();
@@ -150,7 +163,10 @@ function setConfig(path, value) {
150
163
  }
151
164
 
152
165
  function ensureArrayContains(arr, item) {
153
- if (!arr.includes(item)) { arr.push(item); configChanged = true; }
166
+ if (!arr.includes(item)) {
167
+ arr.push(item);
168
+ configChanged = true;
169
+ }
154
170
  }
155
171
 
156
172
  // Plugins
@@ -161,30 +177,58 @@ ensureArrayContains(config.plugins.allow, "canvas");
161
177
 
162
178
  if (!config.plugins.entries) config.plugins.entries = {};
163
179
  for (const id of ["friday-next", "canvas"]) {
164
- if (!config.plugins.entries[id]) { config.plugins.entries[id] = { enabled: true }; configChanged = true; }
165
- else if (!config.plugins.entries[id].enabled) { config.plugins.entries[id].enabled = true; configChanged = true; }
180
+ if (!config.plugins.entries[id]) {
181
+ config.plugins.entries[id] = { enabled: true };
182
+ configChanged = true;
183
+ } else if (!config.plugins.entries[id].enabled) {
184
+ config.plugins.entries[id].enabled = true;
185
+ configChanged = true;
186
+ }
166
187
  }
167
188
 
168
189
  // llm_output hook requires allowConversationAccess for non-bundled plugins.
169
- if (!config.plugins.entries["friday-next"].hooks) { config.plugins.entries["friday-next"].hooks = {}; configChanged = true; }
170
- if (!config.plugins.entries["friday-next"].hooks.allowConversationAccess) { config.plugins.entries["friday-next"].hooks.allowConversationAccess = true; configChanged = true; }
190
+ if (!config.plugins.entries["friday-next"].hooks) {
191
+ config.plugins.entries["friday-next"].hooks = {};
192
+ configChanged = true;
193
+ }
194
+ if (!config.plugins.entries["friday-next"].hooks.allowConversationAccess) {
195
+ config.plugins.entries["friday-next"].hooks.allowConversationAccess = true;
196
+ configChanged = true;
197
+ }
171
198
 
172
199
  // Channel
173
200
  if (!config.channels) config.channels = {};
174
- if (!config.channels["friday-next"]) { config.channels["friday-next"] = { enabled: true, transport: "http+sse" }; configChanged = true; }
175
- else {
176
- if (!config.channels["friday-next"].enabled) { config.channels["friday-next"].enabled = true; configChanged = true; }
177
- if (!config.channels["friday-next"].transport) { config.channels["friday-next"].transport = "http+sse"; configChanged = true; }
201
+ if (!config.channels["friday-next"]) {
202
+ config.channels["friday-next"] = { enabled: true, transport: "http+sse" };
203
+ configChanged = true;
204
+ } else {
205
+ if (!config.channels["friday-next"].enabled) {
206
+ config.channels["friday-next"].enabled = true;
207
+ configChanged = true;
208
+ }
209
+ if (!config.channels["friday-next"].transport) {
210
+ config.channels["friday-next"].transport = "http+sse";
211
+ configChanged = true;
212
+ }
178
213
  }
179
214
 
180
215
  // Gateway bind + nodes
181
216
  if (!config.gateway) config.gateway = {};
182
- if (config.gateway.bind !== "lan") { config.gateway.bind = "lan"; configChanged = true; }
217
+ if (config.gateway.bind !== "lan") {
218
+ config.gateway.bind = "lan";
219
+ configChanged = true;
220
+ }
183
221
  if (!config.gateway.nodes) config.gateway.nodes = {};
184
222
  if (!Array.isArray(config.gateway.nodes.allowCommands)) config.gateway.nodes.allowCommands = [];
185
223
  for (const cmd of [
186
- "canvas.navigate", "canvas.present", "canvas.hide", "canvas.eval",
187
- "canvas.snapshot", "canvas.a2ui.push", "canvas.a2ui.reset", "canvas.a2ui.pushJSONL",
224
+ "canvas.navigate",
225
+ "canvas.present",
226
+ "canvas.hide",
227
+ "canvas.eval",
228
+ "canvas.snapshot",
229
+ "canvas.a2ui.push",
230
+ "canvas.a2ui.reset",
231
+ "canvas.a2ui.pushJSONL",
188
232
  ]) {
189
233
  ensureArrayContains(config.gateway.nodes.allowCommands, cmd);
190
234
  }
@@ -193,7 +237,11 @@ for (const cmd of [
193
237
  if (!config.agents) config.agents = {};
194
238
  if (!Array.isArray(config.agents.list)) config.agents.list = [];
195
239
  let mainAgent = config.agents.list.find((a) => a.id === "main");
196
- if (!mainAgent) { mainAgent = { id: "main" }; config.agents.list.push(mainAgent); configChanged = true; }
240
+ if (!mainAgent) {
241
+ mainAgent = { id: "main" };
242
+ config.agents.list.push(mainAgent);
243
+ configChanged = true;
244
+ }
197
245
  if (!mainAgent.tools) mainAgent.tools = {};
198
246
  if (!Array.isArray(mainAgent.tools.alsoAllow)) mainAgent.tools.alsoAllow = [];
199
247
  for (const tool of ["canvas", "nodes"]) {
@@ -202,7 +250,10 @@ for (const tool of ["canvas", "nodes"]) {
202
250
  if (Array.isArray(mainAgent.tools.deny)) {
203
251
  for (const tool of ["canvas", "nodes"]) {
204
252
  const idx = mainAgent.tools.deny.indexOf(tool);
205
- if (idx !== -1) { mainAgent.tools.deny.splice(idx, 1); configChanged = true; }
253
+ if (idx !== -1) {
254
+ mainAgent.tools.deny.splice(idx, 1);
255
+ configChanged = true;
256
+ }
206
257
  }
207
258
  }
208
259
 
@@ -224,7 +275,11 @@ log("Restarting OpenClaw gateway... (this can take 20-30s)");
224
275
  try {
225
276
  // A full gateway restart commonly takes 20s+ on a fresh boot; give it plenty of room
226
277
  // so we don't kill it mid-restart and report a false failure.
227
- const out = execSync(`${openclawCmd} gateway restart`, { encoding: "utf8", stdio: "pipe", timeout: 90000 });
278
+ const out = execSync(`${openclawCmd} gateway restart`, {
279
+ encoding: "utf8",
280
+ stdio: "pipe",
281
+ timeout: 90000,
282
+ });
228
283
  if (out.trim()) console.log(out.trim());
229
284
  } catch (e) {
230
285
  if (e.stdout?.trim()) console.log(e.stdout.trim());
@@ -250,15 +305,18 @@ function getLanIp() {
250
305
  return "127.0.0.1";
251
306
  }
252
307
 
253
- try { config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf8")); } catch { config = {}; }
308
+ try {
309
+ config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf8"));
310
+ } catch {
311
+ config = {};
312
+ }
254
313
 
255
314
  const gatewayPort = config.gateway?.port || 18789;
256
315
  const gatewayToken = config.gateway?.auth?.token || "(not set)";
257
316
  const bindMode = config.gateway?.bind || "localhost";
258
317
 
259
- const gatewayUrl = bindMode === "lan"
260
- ? `http://${getLanIp()}:${gatewayPort}`
261
- : `http://127.0.0.1:${gatewayPort}`;
318
+ const gatewayUrl =
319
+ bindMode === "lan" ? `http://${getLanIp()}:${gatewayPort}` : `http://127.0.0.1:${gatewayPort}`;
262
320
 
263
321
  // Always verify against loopback: the gateway binds 0.0.0.0 so it's reachable here,
264
322
  // and this avoids false negatives from LAN/NAT routing of the advertised IP.
@@ -272,19 +330,38 @@ async function verifyGateway(url, token, retries = 30) {
272
330
  try {
273
331
  const res = await new Promise((resolve, reject) => {
274
332
  const req = http.request(
275
- { hostname, port, path: "/friday-next/status", method: "GET",
276
- headers: { authorization: `Bearer ${token}` }, timeout: 5000 },
277
- (res) => { let body = ""; res.on("data", (c) => body += c); res.on("end", () => resolve({ status: res.statusCode, body })); },
333
+ {
334
+ hostname,
335
+ port,
336
+ path: "/friday-next/status",
337
+ method: "GET",
338
+ headers: { authorization: `Bearer ${token}` },
339
+ timeout: 5000,
340
+ },
341
+ (res) => {
342
+ let body = "";
343
+ res.on("data", (c) => (body += c));
344
+ res.on("end", () => resolve({ status: res.statusCode, body }));
345
+ },
278
346
  );
279
347
  req.on("error", reject);
280
- req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
348
+ req.on("timeout", () => {
349
+ req.destroy();
350
+ reject(new Error("timeout"));
351
+ });
281
352
  req.end();
282
353
  });
283
354
  if (res.status === 200) {
284
355
  try {
285
356
  const data = JSON.parse(res.body);
286
357
  if (data.ok) {
287
- log("Gateway verified OK (friday-next " + data.version + ", " + data.connections + " connections).");
358
+ log(
359
+ "Gateway verified OK (friday-next " +
360
+ data.version +
361
+ ", " +
362
+ data.connections +
363
+ " connections).",
364
+ );
288
365
  return true;
289
366
  }
290
367
  warn("Plugin responded but ok=false — " + JSON.stringify(data));
@@ -294,8 +371,14 @@ async function verifyGateway(url, token, retries = 30) {
294
371
  continue;
295
372
  }
296
373
  }
297
- if (res.status === 401) { warn("Auth token mismatch — check gateway.auth.token."); return false; }
298
- if (res.status === 404) { warn("Route not foundplugin may not be loaded."); return false; }
374
+ if (res.status === 401) {
375
+ warn("Auth token mismatchcheck gateway.auth.token.");
376
+ return false;
377
+ }
378
+ if (res.status === 404) {
379
+ warn("Route not found — plugin may not be loaded.");
380
+ return false;
381
+ }
299
382
  if (i < retries) warn(`Gateway responded ${res.status}, retrying (${i}/${retries})...`);
300
383
  } catch {
301
384
  if (i < retries) warn(`Gateway not reachable, retrying (${i}/${retries})...`);
@@ -404,14 +487,19 @@ async function detectPublicIp() {
404
487
  const ipStr = await new Promise((resolve, reject) => {
405
488
  const req = http.get(url, { timeout: 3000 }, (res) => {
406
489
  let body = "";
407
- res.on("data", (c) => body += c);
490
+ res.on("data", (c) => (body += c));
408
491
  res.on("end", () => resolve(body.trim()));
409
492
  });
410
493
  req.on("error", reject);
411
- req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
494
+ req.on("timeout", () => {
495
+ req.destroy();
496
+ reject(new Error("timeout"));
497
+ });
412
498
  });
413
499
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ipStr)) return ipStr;
414
- } catch { /* try next */ }
500
+ } catch {
501
+ /* try next */
502
+ }
415
503
  }
416
504
  return null;
417
505
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.30",
3
+ "version": "0.1.37",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,10 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc -p tsconfig.json",
17
+ "lint": "eslint .",
18
+ "lint:fix": "eslint . --fix",
19
+ "format": "prettier --write .",
20
+ "format:check": "prettier --check .",
17
21
  "prepublishOnly": "pnpm build && rm -rf dist/attachments",
18
22
  "test": "npm run test:unit && npm run test:e2e",
19
23
  "test:unit": "vitest run",
@@ -58,12 +62,17 @@
58
62
  "qrcode-terminal": "^0.12.0"
59
63
  },
60
64
  "devDependencies": {
65
+ "@eslint/js": "^10.0.1",
61
66
  "@types/node": "^25.6.0",
62
67
  "chalk": "^5.6.2",
68
+ "eslint": "^10.5.0",
69
+ "eslint-config-prettier": "^10.1.8",
63
70
  "jiti": "^2.6.1",
64
71
  "json5": "^2.2.3",
72
+ "prettier": "^3.8.4",
65
73
  "tslog": "^4.10.2",
66
74
  "typescript": "^6.0.3",
75
+ "typescript-eslint": "^8.61.1",
67
76
  "vitest": "^4.1.5",
68
77
  "zod": "^4.3.6"
69
78
  }
@@ -1,10 +1,25 @@
1
- export async function abortRun(runId: string): Promise<void> {
2
- if (process.env.VITEST !== "true") {
3
- try {
4
- const { abortAgentHarnessRun } = await import("openclaw/plugin-sdk/agent-harness");
5
- abortAgentHarnessRun(runId);
6
- } catch {
7
- // optional at runtime
8
- }
1
+ export type AbortRunResult = { aborted: boolean; drained: boolean };
2
+
3
+ /**
4
+ * Abort the active run for a channel `sessionKey`.
5
+ *
6
+ * A session has at most one active run at a time, and the SDK keys active runs by
7
+ * their internal `sessionId` (not the channel runId). So resolve sessionKey → sessionId
8
+ * first, then abort-and-drain so the caller learns whether the run actually settled.
9
+ */
10
+ export async function abortRunForSessionKey(sessionKey: string): Promise<AbortRunResult> {
11
+ if (process.env.VITEST === "true") return { aborted: false, drained: false };
12
+ const key = sessionKey.trim();
13
+ if (!key) return { aborted: false, drained: false };
14
+ try {
15
+ const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } =
16
+ await import("openclaw/plugin-sdk/agent-harness");
17
+ const sessionId = resolveActiveEmbeddedRunSessionId(key);
18
+ if (!sessionId) return { aborted: false, drained: false };
19
+ const result = await abortAndDrainAgentHarnessRun({ sessionId, sessionKey: key });
20
+ return { aborted: result.aborted, drained: result.drained };
21
+ } catch {
22
+ // optional at runtime
23
+ return { aborted: false, drained: false };
9
24
  }
10
25
  }
@@ -1,4 +1,5 @@
1
- type DispatchFn = (args: unknown) => Promise<unknown> | unknown;
1
+ // Returns a value or a thenable; `unknown` covers both (callers `await` the result).
2
+ type DispatchFn = (args: unknown) => unknown;
2
3
 
3
4
  let overrideDispatch: DispatchFn | null = null;
4
5
 
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const saveMediaBuffer = vi.fn();
4
+ vi.mock("openclaw/plugin-sdk/media-store", () => ({
5
+ saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
6
+ }));
7
+
8
+ const IMAGE_MAX = 6 * 1024 * 1024;
9
+ const DOCUMENT_MAX = 100 * 1024 * 1024;
10
+ vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
11
+ mediaKindFromMime: (mime?: string) => (mime?.startsWith("image/") ? "image" : "document"),
12
+ maxBytesForKind: (kind: string) => (kind === "image" ? IMAGE_MAX : DOCUMENT_MAX),
13
+ }));
14
+
15
+ import { saveInboundMediaBuffer } from "./media-bridge.js";
16
+
17
+ describe("saveInboundMediaBuffer", () => {
18
+ beforeEach(() => {
19
+ saveMediaBuffer.mockReset();
20
+ });
21
+
22
+ it("forwards the original filename so core preserves name+extension", async () => {
23
+ // Without the filename, core's media-store saves inbound media as a bare uuid
24
+ // (no extension) and the agent sees `[media attached: file://.../inbound/<uuid>]`
25
+ // with zero file-format signal. Passing originalFilename (5th arg) restores it.
26
+ saveMediaBuffer.mockResolvedValue({
27
+ id: "report---uuid.pdf",
28
+ path: "/m/inbound/report---uuid.pdf",
29
+ });
30
+
31
+ const out = await saveInboundMediaBuffer(Buffer.from("x"), "application/pdf", "report.pdf");
32
+
33
+ expect(saveMediaBuffer).toHaveBeenCalledWith(
34
+ expect.any(Buffer),
35
+ "application/pdf",
36
+ "inbound",
37
+ DOCUMENT_MAX,
38
+ "report.pdf",
39
+ );
40
+ expect(out.path).toContain(".pdf");
41
+ });
42
+
43
+ it("uses openclaw's per-kind byte cap instead of the 5MB save default", async () => {
44
+ saveMediaBuffer.mockResolvedValue({ id: "uuid.png", path: "/m/inbound/uuid.png" });
45
+
46
+ await saveInboundMediaBuffer(Buffer.from("x"), "image/png", "photo.png");
47
+
48
+ expect(saveMediaBuffer).toHaveBeenCalledWith(
49
+ expect.any(Buffer),
50
+ "image/png",
51
+ "inbound",
52
+ IMAGE_MAX,
53
+ "photo.png",
54
+ );
55
+ });
56
+
57
+ it("works without a filename (still applies the per-kind cap)", async () => {
58
+ saveMediaBuffer.mockResolvedValue({ id: "uuid", path: "/m/inbound/uuid" });
59
+
60
+ const out = await saveInboundMediaBuffer(Buffer.from("x"), "image/png");
61
+
62
+ expect(saveMediaBuffer).toHaveBeenCalledWith(
63
+ expect.any(Buffer),
64
+ "image/png",
65
+ "inbound",
66
+ IMAGE_MAX,
67
+ undefined,
68
+ );
69
+ expect(out.id).toBe("uuid");
70
+ });
71
+ });
@@ -3,13 +3,42 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import crypto from "node:crypto";
5
5
 
6
+ /**
7
+ * openclaw's own per-kind byte cap for a mime type (image 6MB / audio·video 16MB /
8
+ * document 100MB). Unknown mimes fall back to the most permissive ("document") cap.
9
+ * Returns undefined if the media runtime isn't importable (tests / stripped runtime),
10
+ * letting `saveMediaBuffer` apply its built-in default.
11
+ */
12
+ export async function resolveMediaMaxBytes(mimeType: string): Promise<number | undefined> {
13
+ try {
14
+ const { maxBytesForKind, mediaKindFromMime } =
15
+ await import("openclaw/plugin-sdk/media-runtime");
16
+ return maxBytesForKind(mediaKindFromMime(mimeType) ?? "document");
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
6
22
  export async function saveInboundMediaBuffer(
7
23
  buffer: Buffer,
8
24
  mimeType: string,
25
+ originalFilename?: string,
9
26
  ): Promise<{ id: string; path: string }> {
10
27
  try {
11
28
  const sdk = await import("openclaw/plugin-sdk/media-store");
12
- const saved = await sdk.saveMediaBuffer(buffer, mimeType, "inbound");
29
+ // Accept whatever openclaw itself supports for this media kind instead of
30
+ // saveMediaBuffer's conservative 5MB default.
31
+ const maxBytes = await resolveMediaMaxBytes(mimeType);
32
+ // Pass the original filename (5th arg) so core's media-store preserves the
33
+ // name+extension instead of saving a bare uuid. Otherwise the agent receives
34
+ // `[media attached: file://.../inbound/<uuid>]` with no file-format signal.
35
+ const saved = await sdk.saveMediaBuffer(
36
+ buffer,
37
+ mimeType,
38
+ "inbound",
39
+ maxBytes,
40
+ originalFilename,
41
+ );
13
42
  if (saved?.id && saved?.path) return { id: saved.id, path: saved.path };
14
43
  } catch {
15
44
  // fallback for tests or stripped runtime