@syengup/friday-channel-next 0.1.36 → 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 (120) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  3. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  4. package/dist/src/agent/node-pairing-bridge.js +6 -2
  5. package/dist/src/agent/subagent-registry.js +0 -3
  6. package/dist/src/channel-actions.js +3 -1
  7. package/dist/src/channel.js +0 -2
  8. package/dist/src/collect-message-media-paths.js +10 -1
  9. package/dist/src/friday-session.js +34 -10
  10. package/dist/src/history/normalize-message.js +22 -8
  11. package/dist/src/http/handlers/agent-config.js +10 -4
  12. package/dist/src/http/handlers/cancel.js +4 -2
  13. package/dist/src/http/handlers/device-approve.js +3 -1
  14. package/dist/src/http/handlers/files-download.js +6 -8
  15. package/dist/src/http/handlers/files.js +1 -1
  16. package/dist/src/http/handlers/health.js +18 -4
  17. package/dist/src/http/handlers/history-messages.js +1 -1
  18. package/dist/src/http/handlers/history-sessions.js +5 -3
  19. package/dist/src/http/handlers/messages.js +25 -11
  20. package/dist/src/http/handlers/models-list.js +1 -1
  21. package/dist/src/http/handlers/nodes-approve.js +1 -6
  22. package/dist/src/http/handlers/plugin-info.js +1 -1
  23. package/dist/src/http/server.js +4 -2
  24. package/dist/src/link-preview/og-parse.js +3 -1
  25. package/dist/src/plugin-install-info.js +4 -1
  26. package/dist/src/session/session-manager.js +9 -3
  27. package/dist/src/session-usage-store.js +3 -1
  28. package/dist/src/skills-discovery.d.ts +5 -4
  29. package/dist/src/skills-discovery.js +27 -22
  30. package/dist/src/sse/offline-queue.js +4 -1
  31. package/dist/src/tool-catalog.js +2 -3
  32. package/dist/src/upgrade-runtime.d.ts +1 -1
  33. package/dist/src/version.js +3 -1
  34. package/index.ts +43 -35
  35. package/install.js +131 -43
  36. package/package.json +10 -1
  37. package/src/agent/abort-run.ts +2 -3
  38. package/src/agent/dispatch-bridge.ts +2 -1
  39. package/src/agent/media-bridge.ts +9 -2
  40. package/src/agent/node-pairing-bridge.ts +29 -15
  41. package/src/agent/run-usage-accumulator.ts +4 -2
  42. package/src/agent/subagent-registry.ts +0 -4
  43. package/src/agent-run-context-bridge.ts +3 -1
  44. package/src/channel-actions.test.ts +10 -4
  45. package/src/channel-actions.ts +3 -1
  46. package/src/channel.outbound.test.ts +18 -4
  47. package/src/channel.ts +121 -123
  48. package/src/collect-message-media-paths.ts +15 -6
  49. package/src/config.ts +1 -4
  50. package/src/e2e/agents-list.e2e.test.ts +9 -2
  51. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  52. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  53. package/src/e2e/auto-approve.integration.test.ts +13 -7
  54. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  55. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  56. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  57. package/src/e2e/send-text.e2e.test.ts +11 -2
  58. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  59. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  60. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  61. package/src/e2e/subagent.e2e.test.ts +136 -53
  62. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  63. package/src/friday-session.forward-agent.test.ts +44 -12
  64. package/src/friday-session.ts +44 -20
  65. package/src/history/normalize-message.test.ts +35 -8
  66. package/src/history/normalize-message.ts +24 -12
  67. package/src/history/read-transcript.ts +1 -4
  68. package/src/http/handlers/agent-config.test.ts +10 -3
  69. package/src/http/handlers/agent-config.ts +22 -8
  70. package/src/http/handlers/agents-list.test.ts +1 -5
  71. package/src/http/handlers/cancel.test.ts +12 -3
  72. package/src/http/handlers/cancel.ts +4 -2
  73. package/src/http/handlers/device-approve.test.ts +12 -3
  74. package/src/http/handlers/device-approve.ts +33 -21
  75. package/src/http/handlers/files-download.ts +17 -13
  76. package/src/http/handlers/files.test.ts +8 -2
  77. package/src/http/handlers/files.ts +21 -7
  78. package/src/http/handlers/health.test.ts +43 -11
  79. package/src/http/handlers/health.ts +22 -6
  80. package/src/http/handlers/history-messages.test.ts +51 -9
  81. package/src/http/handlers/history-messages.ts +4 -1
  82. package/src/http/handlers/history-sessions.test.ts +46 -9
  83. package/src/http/handlers/history-sessions.ts +5 -3
  84. package/src/http/handlers/history-set-title.test.ts +14 -5
  85. package/src/http/handlers/link-preview.test.ts +57 -16
  86. package/src/http/handlers/link-preview.ts +4 -1
  87. package/src/http/handlers/messages.test.ts +12 -8
  88. package/src/http/handlers/messages.ts +57 -19
  89. package/src/http/handlers/models-list.ts +14 -8
  90. package/src/http/handlers/nodes-approve.test.ts +15 -4
  91. package/src/http/handlers/nodes-approve.ts +38 -40
  92. package/src/http/handlers/plugin-info.ts +5 -6
  93. package/src/http/handlers/plugin-upgrade.ts +4 -1
  94. package/src/http/handlers/sse.ts +3 -1
  95. package/src/http/server.ts +9 -6
  96. package/src/link-preview/og-parse.test.ts +6 -2
  97. package/src/link-preview/og-parse.ts +10 -3
  98. package/src/link-preview/preview-service.ts +4 -1
  99. package/src/link-preview/ssrf-guard.test.ts +72 -15
  100. package/src/link-preview/ssrf-guard.ts +2 -1
  101. package/src/media-fetch.test.ts +7 -2
  102. package/src/media-fetch.ts +1 -2
  103. package/src/openclaw.d.ts +16 -9
  104. package/src/plugin-install-info.ts +20 -9
  105. package/src/run-metadata.ts +2 -1
  106. package/src/session/session-manager.ts +19 -11
  107. package/src/session-usage-snapshot.ts +3 -1
  108. package/src/session-usage-store.ts +3 -1
  109. package/src/skills-discovery.test.ts +14 -10
  110. package/src/skills-discovery.ts +43 -27
  111. package/src/sse/emitter.test.ts +1 -1
  112. package/src/sse/emitter.ts +9 -3
  113. package/src/sse/offline-queue.ts +17 -8
  114. package/src/test-support/app-simulator.ts +17 -3
  115. package/src/test-support/mock-dispatch.ts +17 -4
  116. package/src/thinking-levels.ts +3 -1
  117. package/src/tool-catalog.ts +16 -7
  118. package/src/upgrade-runtime.ts +4 -2
  119. package/src/version.ts +5 -1
  120. package/tsconfig.json +1 -1
@@ -118,7 +118,11 @@ export function setSessionSettings(sessionKey, settings, historyDir) {
118
118
  return {};
119
119
  upsertSessionEntry(data, fileKey, sessionKey);
120
120
  const fieldKeys = [
121
- "reasoningLevel", "thinkingLevel", "modelRef", "providerOverride", "modelOverride",
121
+ "reasoningLevel",
122
+ "thinkingLevel",
123
+ "modelRef",
124
+ "providerOverride",
125
+ "modelOverride",
122
126
  ];
123
127
  let updated = false;
124
128
  for (const key of fieldKeys) {
@@ -193,7 +197,7 @@ export function resolveAgentDefaults(sessionKey) {
193
197
  const ocCfg = (forwardRt.getConfig() ?? {});
194
198
  const agents = ocCfg.agents;
195
199
  const targetAgentId = agentIdFromSessionKey(sessionKey);
196
- const agentEntry = agents?.list?.find((a) => agentIdFromSessionKey(`agent:${String(a?.id ?? "")}:x`) === targetAgentId);
200
+ const agentEntry = agents?.list?.find((a) => agentIdFromSessionKey(`agent:${typeof a?.id === "string" ? a.id : typeof a?.id === "number" ? String(a.id) : ""}:x`) === targetAgentId);
197
201
  const agentModel = agentEntry?.model;
198
202
  const perAgentModel = typeof agentModel === "string"
199
203
  ? agentModel
@@ -204,7 +208,9 @@ export function resolveAgentDefaults(sessionKey) {
204
208
  const agentDefaults = agents?.defaults;
205
209
  const model = agentDefaults?.model;
206
210
  const globalModel = typeof model?.primary === "string" ? model.primary : undefined;
207
- const globalThinking = typeof agentDefaults?.thinkingDefault === "string" ? agentDefaults.thinkingDefault : undefined;
211
+ const globalThinking = typeof agentDefaults?.thinkingDefault === "string"
212
+ ? agentDefaults.thinkingDefault
213
+ : undefined;
208
214
  return { model: perAgentModel ?? globalModel, thinking: perAgentThinking ?? globalThinking };
209
215
  }
210
216
  catch {
@@ -24,7 +24,9 @@ export function readSessionUsageSnapshotFromStore(sessionKeyForStore) {
24
24
  const cfg = access.getConfig();
25
25
  const storeConfig = cfg?.session?.store;
26
26
  const canonical = toSessionStoreKey(sessionKeyForStore);
27
- const storePath = access.resolveStorePath(storeConfig, { agentId: agentIdFromSessionKey(canonical) });
27
+ const storePath = access.resolveStorePath(storeConfig, {
28
+ agentId: agentIdFromSessionKey(canonical),
29
+ });
28
30
  const store = access.loadSessionStore(storePath, { skipCache: true });
29
31
  const entry = store[canonical] ?? store[sessionKeyForStore.trim()];
30
32
  if (!entry || typeof entry !== "object")
@@ -12,7 +12,9 @@
12
12
  * marker core's `loadSkillsFromDir` uses).
13
13
  *
14
14
  * Each discovered skill is tagged with a `source` category for the UI:
15
- * - workspace : the agent's own + the shared default-agent workspace `skills/`
15
+ * - workspace : the TARGET agent's own workspace `skills/` only mirroring ControlUI,
16
+ * which scans the single workspace resolved for that agent and never folds
17
+ * in another agent's workspace (main is just another agent, not a shared pool)
16
18
  * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
17
19
  * - built-in : bundled core skills (`<openclaw>/skills`)
18
20
  * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
@@ -49,9 +51,8 @@ export declare function resolveOpenClawRoot(): string | null;
49
51
  export declare function enabledExtensionNames(cfg: unknown): Set<string>;
50
52
  /**
51
53
  * Full set of skills `agentId` can load, sorted by id, each tagged with its source
52
- * category. Aggregates the agent's workspace, the shared root workspace, the managed
53
- * dir, config extra dirs, and bundled core/extension skills. Every source is optional
54
- * and failure-tolerant.
54
+ * category. Aggregates the TARGET agent's own workspace, the managed dir, config extra
55
+ * dirs, and bundled core/extension skills. Every source is optional and failure-tolerant.
55
56
  */
56
57
  export declare function discoverAvailableSkills(cfg: unknown, agentId: string): DiscoveredSkill[];
57
58
  /** Test-only: reset the cached openclaw root. */
@@ -12,7 +12,9 @@
12
12
  * marker core's `loadSkillsFromDir` uses).
13
13
  *
14
14
  * Each discovered skill is tagged with a `source` category for the UI:
15
- * - workspace : the agent's own + the shared default-agent workspace `skills/`
15
+ * - workspace : the TARGET agent's own workspace `skills/` only mirroring ControlUI,
16
+ * which scans the single workspace resolved for that agent and never folds
17
+ * in another agent's workspace (main is just another agent, not a shared pool)
16
18
  * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
17
19
  * - built-in : bundled core skills (`<openclaw>/skills`)
18
20
  * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
@@ -200,34 +202,37 @@ function resolveDefaultAgentId(cfg) {
200
202
  }
201
203
  /**
202
204
  * Full set of skills `agentId` can load, sorted by id, each tagged with its source
203
- * category. Aggregates the agent's workspace, the shared root workspace, the managed
204
- * dir, config extra dirs, and bundled core/extension skills. Every source is optional
205
- * and failure-tolerant.
205
+ * category. Aggregates the TARGET agent's own workspace, the managed dir, config extra
206
+ * dirs, and bundled core/extension skills. Every source is optional and failure-tolerant.
206
207
  */
207
208
  export function discoverAvailableSkills(cfg, agentId) {
208
209
  const c = cfg;
209
210
  const resolveWs = getFridayAgentForwardRuntime()?.resolveAgentWorkspaceDir;
210
211
  const sources = [];
211
212
  if (resolveWs) {
212
- const defaultId = resolveDefaultAgentId(c);
213
- const ids = agentId === defaultId ? [agentId] : [agentId, defaultId];
214
- let defaultWs;
215
- for (const id of ids) {
216
- try {
217
- const ws = resolveWs(cfg, id);
218
- if (ws) {
219
- sources.push({ dir: path.join(ws, "skills"), source: "workspace" });
220
- if (id === defaultId)
221
- defaultWs = ws;
222
- }
223
- }
224
- catch {
225
- // skip unresolvable workspace
226
- }
213
+ // Workspace skills come ONLY from the target agent's own workspace — matching ControlUI's
214
+ // `resolveSkillsAgentWorkspace`→`buildWorkspaceSkillStatus(workspaceDir)`, which scans the
215
+ // single resolved workspace. Folding in the default agent's workspace (the old behavior)
216
+ // leaked main's skills into every other agent's catalog.
217
+ try {
218
+ const ws = resolveWs(cfg, agentId);
219
+ if (ws)
220
+ sources.push({ dir: path.join(ws, "skills"), source: "workspace" });
221
+ }
222
+ catch {
223
+ // skip unresolvable workspace
224
+ }
225
+ // Managed skills dir: `<configDir>/skills`. It is agent-independent; anchor it off the
226
+ // DEFAULT agent's workspace parent (the default workspace lives directly under configDir,
227
+ // whereas non-default workspaces may be nested under it).
228
+ try {
229
+ const defaultWs = resolveWs(cfg, resolveDefaultAgentId(c));
230
+ if (defaultWs)
231
+ sources.push({ dir: path.join(path.dirname(defaultWs), "skills"), source: "installed" });
232
+ }
233
+ catch {
234
+ // skip unresolvable managed dir
227
235
  }
228
- // Managed skills dir: `<configDir>/skills`, the workspace's parent sibling.
229
- if (defaultWs)
230
- sources.push({ dir: path.join(path.dirname(defaultWs), "skills"), source: "installed" });
231
236
  }
232
237
  const extraDirs = c?.skills?.load?.extraDirs;
233
238
  if (Array.isArray(extraDirs)) {
@@ -116,7 +116,10 @@ export class FridaySseOfflineQueue {
116
116
  continue;
117
117
  try {
118
118
  const o = JSON.parse(line);
119
- if (typeof o.id === "number" && typeof o.event === "string" && o.data && typeof o.data === "object") {
119
+ if (typeof o.id === "number" &&
120
+ typeof o.event === "string" &&
121
+ o.data &&
122
+ typeof o.data === "object") {
120
123
  all.push(o);
121
124
  }
122
125
  }
@@ -108,8 +108,7 @@ function readStringArray(value) {
108
108
  }
109
109
  /** Read an agent's `tools` config block from the host config. */
110
110
  function findAgentTools(cfg, agentId) {
111
- const list = cfg?.agents
112
- ?.list;
111
+ const list = cfg?.agents?.list;
113
112
  if (!Array.isArray(list))
114
113
  return undefined;
115
114
  const entry = list.find((a) => a && typeof a === "object" && normalizeAgentId(a.id) === agentId);
@@ -154,7 +153,7 @@ export async function buildAgentToolsCatalog(cfg, agentId) {
154
153
  }
155
154
  }
156
155
  const tools = findAgentTools(cfg, agentId);
157
- const profile = (typeof tools?.profile === "string" && tools.profile.trim()) ? tools.profile.trim() : null;
156
+ const profile = typeof tools?.profile === "string" && tools.profile.trim() ? tools.profile.trim() : null;
158
157
  const allow = new Set(readStringArray(tools?.allow));
159
158
  const alsoAllow = new Set(readStringArray(tools?.alsoAllow));
160
159
  const deny = new Set(readStringArray(tools?.deny));
@@ -30,7 +30,7 @@ export type UpgradeRuntime = {
30
30
  /** Mutate the config file; `afterWrite: { mode: "restart" }` triggers a safe gateway restart. */
31
31
  mutateConfigFile: (params: {
32
32
  afterWrite: ConfigAfterWrite;
33
- mutate: (draft: unknown) => unknown | void;
33
+ mutate: (draft: unknown) => unknown;
34
34
  }) => Promise<unknown>;
35
35
  /**
36
36
  * Filesystem path of THIS loaded plugin (`api.source`). Used to infer the install
@@ -20,7 +20,9 @@ function resolvePluginVersion() {
20
20
  const path = fileURLToPath(new URL(rel, import.meta.url));
21
21
  const raw = readFileSync(path, "utf8");
22
22
  const pkg = JSON.parse(raw);
23
- if (pkg.name === "@syengup/friday-channel-next" && typeof pkg.version === "string" && pkg.version) {
23
+ if (pkg.name === "@syengup/friday-channel-next" &&
24
+ typeof pkg.version === "string" &&
25
+ pkg.version) {
24
26
  return pkg.version;
25
27
  }
26
28
  }
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.36",
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
  }
@@ -12,9 +12,8 @@ export async function abortRunForSessionKey(sessionKey: string): Promise<AbortRu
12
12
  const key = sessionKey.trim();
13
13
  if (!key) return { aborted: false, drained: false };
14
14
  try {
15
- const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } = await import(
16
- "openclaw/plugin-sdk/agent-harness"
17
- );
15
+ const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } =
16
+ await import("openclaw/plugin-sdk/agent-harness");
18
17
  const sessionId = resolveActiveEmbeddedRunSessionId(key);
19
18
  if (!sessionId) return { aborted: false, drained: false };
20
19
  const result = await abortAndDrainAgentHarnessRun({ sessionId, sessionKey: key });