@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.
- package/dist/index.js +1 -1
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/channel-actions.js +3 -1
- package/dist/src/channel.js +0 -2
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.js +10 -4
- package/dist/src/http/handlers/cancel.js +4 -2
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.js +1 -1
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +25 -11
- package/dist/src/http/handlers/models-list.js +1 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/server.js +4 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +5 -4
- package/dist/src/skills-discovery.js +27 -22
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/tool-catalog.js +2 -3
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +3 -1
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +2 -3
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.ts +9 -2
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +10 -4
- package/src/channel-actions.ts +3 -1
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +121 -123
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +10 -3
- package/src/http/handlers/agent-config.ts +22 -8
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/cancel.test.ts +12 -3
- package/src/http/handlers/cancel.ts +4 -2
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +8 -2
- package/src/http/handlers/files.ts +21 -7
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +57 -19
- package/src/http/handlers/models-list.ts +14 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +9 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +72 -15
- package/src/link-preview/ssrf-guard.ts +2 -1
- package/src/media-fetch.test.ts +7 -2
- package/src/media-fetch.ts +1 -2
- package/src/openclaw.d.ts +16 -9
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +14 -10
- package/src/skills-discovery.ts +43 -27
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.ts +3 -1
- package/src/tool-catalog.ts +16 -7
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +5 -1
- 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",
|
|
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
|
|
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"
|
|
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, {
|
|
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
|
|
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
|
|
53
|
-
*
|
|
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
|
|
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
|
|
204
|
-
*
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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" &&
|
|
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
|
}
|
package/dist/src/tool-catalog.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
package/dist/src/version.js
CHANGED
|
@@ -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" &&
|
|
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 {
|
|
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
|
|
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(
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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) {
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
39
|
-
|
|
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]) {
|
|
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 {
|
|
112
|
-
|
|
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)) {
|
|
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]) {
|
|
165
|
-
|
|
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) {
|
|
170
|
-
|
|
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"]) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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") {
|
|
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",
|
|
187
|
-
"canvas.
|
|
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) {
|
|
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) {
|
|
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`, {
|
|
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 {
|
|
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 =
|
|
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
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
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", () => {
|
|
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(
|
|
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) {
|
|
298
|
-
|
|
374
|
+
if (res.status === 401) {
|
|
375
|
+
warn("Auth token mismatch — check 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", () => {
|
|
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 {
|
|
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.
|
|
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
|
}
|
package/src/agent/abort-run.ts
CHANGED
|
@@ -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 } =
|
|
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 });
|