@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
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import {
3
+ resolveModelThinking,
4
+ resolveModelThinkingForRef,
5
+ isThinkingLevelSupportedForRef,
6
+ } from "./thinking-levels.js";
7
+ import {
8
+ setFridayAgentForwardRuntime,
9
+ resetFridayAgentForwardRuntimeForTest,
10
+ } from "./agent-forward-runtime.js";
11
+
12
+ /** Inject a forward runtime whose `resolveThinkingPolicy` echoes the recorded calls + a fixed reply. */
13
+ function setThinkingPolicy(
14
+ impl: (params: { provider?: string | null; model?: string | null }) => {
15
+ levels: Array<{ id: string; label: string }>;
16
+ defaultLevel?: string | null;
17
+ },
18
+ ): { calls: Array<{ provider?: string | null; model?: string | null }> } {
19
+ const calls: Array<{ provider?: string | null; model?: string | null }> = [];
20
+ setFridayAgentForwardRuntime({
21
+ runtime: {
22
+ agent: {
23
+ session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
24
+ resolveThinkingPolicy: (params: { provider?: string | null; model?: string | null }) => {
25
+ calls.push(params);
26
+ return impl(params);
27
+ },
28
+ },
29
+ config: { current: () => ({}) },
30
+ },
31
+ } as never);
32
+ return { calls };
33
+ }
34
+
35
+ describe("resolveModelThinking", () => {
36
+ afterEach(() => {
37
+ resetFridayAgentForwardRuntimeForTest();
38
+ });
39
+
40
+ it("returns the runtime-resolved levels + default for a model that supports xhigh", () => {
41
+ setThinkingPolicy(() => ({
42
+ levels: [
43
+ { id: "off", label: "off" },
44
+ { id: "minimal", label: "minimal" },
45
+ { id: "low", label: "low" },
46
+ { id: "medium", label: "medium" },
47
+ { id: "high", label: "high" },
48
+ { id: "xhigh", label: "xhigh" },
49
+ ],
50
+ defaultLevel: "high",
51
+ }));
52
+ const result = resolveModelThinking("openai", "gpt-5.4");
53
+ expect(result.levels.map((l) => l.id)).toEqual([
54
+ "off",
55
+ "minimal",
56
+ "low",
57
+ "medium",
58
+ "high",
59
+ "xhigh",
60
+ ]);
61
+ expect(result.default).toBe("high");
62
+ });
63
+
64
+ it("passes binary on/off labels through unchanged", () => {
65
+ setThinkingPolicy(() => ({
66
+ levels: [
67
+ { id: "off", label: "off" },
68
+ { id: "low", label: "on" },
69
+ ],
70
+ }));
71
+ const result = resolveModelThinking("moonshot", "kimi-k2");
72
+ expect(result.levels).toEqual([
73
+ { id: "off", label: "off" },
74
+ { id: "low", label: "on" },
75
+ ]);
76
+ expect(result.default).toBeUndefined();
77
+ });
78
+
79
+ it("falls back to the base five levels when no runtime is registered", () => {
80
+ const result = resolveModelThinking("anything", "model-x");
81
+ expect(result.levels.map((l) => l.id)).toEqual(["off", "minimal", "low", "medium", "high"]);
82
+ expect(result.default).toBeUndefined();
83
+ });
84
+
85
+ it("falls back to the base levels when resolveThinkingPolicy throws", () => {
86
+ setThinkingPolicy(() => {
87
+ throw new Error("boom");
88
+ });
89
+ const result = resolveModelThinking("openai", "gpt-5.4");
90
+ expect(result.levels.map((l) => l.id)).toEqual(["off", "minimal", "low", "medium", "high"]);
91
+ });
92
+
93
+ it("falls back to the base levels when the policy returns no levels", () => {
94
+ setThinkingPolicy(() => ({ levels: [] }));
95
+ const result = resolveModelThinking("openai", "gpt-5.4");
96
+ expect(result.levels.map((l) => l.id)).toEqual(["off", "minimal", "low", "medium", "high"]);
97
+ });
98
+ });
99
+
100
+ describe("resolveModelThinkingForRef", () => {
101
+ afterEach(() => {
102
+ resetFridayAgentForwardRuntimeForTest();
103
+ });
104
+
105
+ it("splits a provider/model ref and forwards both parts to the policy", () => {
106
+ const { calls } = setThinkingPolicy(() => ({ levels: [{ id: "off", label: "off" }] }));
107
+ resolveModelThinkingForRef("deepseek/deepseek-v4-pro");
108
+ expect(calls).toEqual([{ provider: "deepseek", model: "deepseek-v4-pro" }]);
109
+ });
110
+
111
+ it("treats a bare model id as having no provider", () => {
112
+ const { calls } = setThinkingPolicy(() => ({ levels: [{ id: "off", label: "off" }] }));
113
+ resolveModelThinkingForRef("just-a-model");
114
+ expect(calls).toEqual([{ provider: null, model: "just-a-model" }]);
115
+ });
116
+
117
+ it("returns the base set for an empty ref without calling the runtime", () => {
118
+ const { calls } = setThinkingPolicy(() => ({ levels: [{ id: "off", label: "off" }] }));
119
+ const result = resolveModelThinkingForRef("");
120
+ expect(calls).toEqual([]);
121
+ expect(result.levels.map((l) => l.id)).toEqual(["off", "minimal", "low", "medium", "high"]);
122
+ });
123
+ });
124
+
125
+ describe("isThinkingLevelSupportedForRef", () => {
126
+ afterEach(() => {
127
+ resetFridayAgentForwardRuntimeForTest();
128
+ });
129
+
130
+ it("accepts a level the model supports and rejects one it does not", () => {
131
+ setThinkingPolicy(() => ({
132
+ levels: [
133
+ { id: "off", label: "off" },
134
+ { id: "low", label: "low" },
135
+ { id: "medium", label: "medium" },
136
+ { id: "high", label: "high" },
137
+ { id: "max", label: "max" },
138
+ ],
139
+ }));
140
+ expect(isThinkingLevelSupportedForRef("deepseek/deepseek-v4", "max")).toBe(true);
141
+ expect(isThinkingLevelSupportedForRef("deepseek/deepseek-v4", "xhigh")).toBe(false);
142
+ });
143
+ });
@@ -0,0 +1,70 @@
1
+ import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
2
+ import { splitModelRef } from "./session/session-manager.js";
3
+
4
+ export interface ThinkingLevelOption {
5
+ id: string;
6
+ label: string;
7
+ }
8
+
9
+ export interface ResolvedModelThinking {
10
+ /** Ordered (off → highest) levels the model supports. */
11
+ levels: ThinkingLevelOption[];
12
+ /** Provider/model default level, when the gateway reports one. */
13
+ default?: string;
14
+ }
15
+
16
+ /**
17
+ * Base five levels used when the running gateway is too old to expose `resolveThinkingPolicy`, or
18
+ * when resolution fails. Mirrors core `BASE_THINKING_LEVELS` (label === id for the base set).
19
+ */
20
+ const BASE_THINKING_LEVELS: ThinkingLevelOption[] = [
21
+ { id: "off", label: "off" },
22
+ { id: "minimal", label: "minimal" },
23
+ { id: "low", label: "low" },
24
+ { id: "medium", label: "medium" },
25
+ { id: "high", label: "high" },
26
+ ];
27
+
28
+ /**
29
+ * Resolves the thinking-level option set for `provider`/`modelId` from the running gateway's
30
+ * provider plugins + model catalog. The set varies per model (e.g. GPT-5.4 adds `xhigh`, Gemini adds
31
+ * `adaptive`, DeepSeek V4 adds `xhigh`/`max`, binary providers collapse to `off`/`on`). Falls back to
32
+ * the base five levels when the runtime API is unavailable.
33
+ */
34
+ export function resolveModelThinking(
35
+ provider: string | undefined | null,
36
+ modelId: string | undefined | null,
37
+ ): ResolvedModelThinking {
38
+ const resolve = getFridayAgentForwardRuntime()?.resolveThinkingPolicy;
39
+ if (resolve) {
40
+ try {
41
+ const policy = resolve({ provider: provider ?? null, model: modelId ?? null });
42
+ if (policy?.levels?.length) {
43
+ return {
44
+ levels: policy.levels.map((l) => ({ id: l.id, label: l.label })),
45
+ default: policy.defaultLevel ?? undefined,
46
+ };
47
+ }
48
+ } catch {
49
+ // Fall through to the base levels below.
50
+ }
51
+ }
52
+ return { levels: BASE_THINKING_LEVELS };
53
+ }
54
+
55
+ /** Resolves thinking levels for a full `provider/model` ref (or bare model id). */
56
+ export function resolveModelThinkingForRef(
57
+ modelRef: string | undefined | null,
58
+ ): ResolvedModelThinking {
59
+ if (!modelRef) return { levels: BASE_THINKING_LEVELS };
60
+ const split = splitModelRef(modelRef);
61
+ return resolveModelThinking(split.provider, split.modelId);
62
+ }
63
+
64
+ /** Whether `level` is a supported thinking level for the given `provider/model` ref. */
65
+ export function isThinkingLevelSupportedForRef(
66
+ modelRef: string | undefined | null,
67
+ level: string,
68
+ ): boolean {
69
+ return resolveModelThinkingForRef(modelRef).levels.some((l) => l.id === level);
70
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Build an agent's tool-permission catalog for the app's toolbox editor — the same
3
+ * tools/categories/descriptions/profiles ControlUI shows.
4
+ *
5
+ * The catalog (core + plugin tools, grouped, with descriptions and per-tool
6
+ * `defaultProfiles`) is produced by core's `buildToolsCatalogResult({cfg, agentId})`.
7
+ * That builder lives only in a hash-named dist chunk (no stable plugin-sdk export) and
8
+ * the catalog is CODE, not scannable data — so unlike skill discovery we can't avoid
9
+ * importing it. We locate the chunk RESILIENTLY (scan `<openclaw>/dist/*.js` for the one
10
+ * defining `buildToolsCatalogResult`, then dynamic-import it — Node returns the gateway's
11
+ * already-loaded module instance, so no side effects), cache it, and degrade gracefully
12
+ * (null) if the layout changes. Per-tool `enabled`/`inProfile` are then resolved here from
13
+ * the agent's `tools` config so the app can render simple toggles.
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { resolveOpenClawRoot } from "./skills-discovery.js";
19
+ import { normalizeAgentId } from "./agent-id.js";
20
+
21
+ interface CoreCatalogTool {
22
+ id: string;
23
+ label: string;
24
+ description: string;
25
+ source: string;
26
+ defaultProfiles: string[];
27
+ }
28
+ interface CoreCatalogGroup {
29
+ id: string;
30
+ label: string;
31
+ source: string;
32
+ pluginId?: string;
33
+ tools: CoreCatalogTool[];
34
+ }
35
+ interface CoreCatalogResult {
36
+ agentId: string;
37
+ profiles: Array<{ id: string; label: string }>;
38
+ groups: CoreCatalogGroup[];
39
+ }
40
+ type BuildFn = (params: {
41
+ cfg: unknown;
42
+ agentId?: string;
43
+ includePlugins?: boolean;
44
+ }) => CoreCatalogResult;
45
+
46
+ let cachedBuildFn: BuildFn | null | undefined;
47
+
48
+ async function loadBuildFn(): Promise<BuildFn | null> {
49
+ if (cachedBuildFn !== undefined) return cachedBuildFn;
50
+ cachedBuildFn = await locateBuildFn();
51
+ return cachedBuildFn;
52
+ }
53
+
54
+ /**
55
+ * Core's catalog builder enumerates plugin tools via an internal
56
+ * `ensureStandaloneRuntimePluginRegistryLoaded({ surface: "channel" })`, which
57
+ * `pinActivePluginChannelRegistry()`s a tool-scoped registry that does NOT carry the
58
+ * friday-next channel registration. Because friday-next is an external channel (not in
59
+ * core's static CHANNEL_IDS), that re-pin drops it from the deliverable-channel set for
60
+ * the WHOLE gateway until the next full reload/restart — so every agent `message` send
61
+ * then fails with `Unknown channel: friday-next`. We snapshot the channel registry before
62
+ * the build and pin it back after, neutralizing the side effect. Resilient-import the
63
+ * runtime chunk like the catalog builder (gateway singleton; state lives on globalThis).
64
+ */
65
+ interface ChannelRegistryFns {
66
+ get: () => unknown;
67
+ pin: (registry: unknown) => void;
68
+ }
69
+ let cachedChannelRegistryFns: ChannelRegistryFns | null | undefined;
70
+
71
+ async function loadChannelRegistryFns(): Promise<ChannelRegistryFns | null> {
72
+ if (cachedChannelRegistryFns !== undefined) return cachedChannelRegistryFns;
73
+ cachedChannelRegistryFns = await locateChannelRegistryFns();
74
+ return cachedChannelRegistryFns;
75
+ }
76
+
77
+ async function locateChannelRegistryFns(): Promise<ChannelRegistryFns | null> {
78
+ const root = resolveOpenClawRoot();
79
+ if (!root) return null;
80
+ const distDir = path.join(root, "dist");
81
+ let files: string[];
82
+ try {
83
+ files = fs.readdirSync(distDir).filter((f) => f.endsWith(".js"));
84
+ } catch {
85
+ return null;
86
+ }
87
+ for (const file of files) {
88
+ let content: string;
89
+ try {
90
+ content = fs.readFileSync(path.join(distDir, file), "utf8");
91
+ } catch {
92
+ continue;
93
+ }
94
+ // Only the chunk that re-exports both helpers by their real names is usable.
95
+ if (!content.includes("pinActivePluginChannelRegistry")) continue;
96
+ try {
97
+ const mod = (await import(path.join(distDir, file))) as Record<string, unknown>;
98
+ const pin = mod.pinActivePluginChannelRegistry;
99
+ const get = mod.getActivePluginChannelRegistry;
100
+ if (typeof pin === "function" && typeof get === "function") {
101
+ return {
102
+ get: get as () => unknown,
103
+ pin: pin as (registry: unknown) => void,
104
+ };
105
+ }
106
+ } catch {
107
+ // unreadable/non-importable candidate → keep scanning
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ async function locateBuildFn(): Promise<BuildFn | null> {
114
+ const root = resolveOpenClawRoot();
115
+ if (!root) return null;
116
+ const distDir = path.join(root, "dist");
117
+ let files: string[];
118
+ try {
119
+ files = fs.readdirSync(distDir).filter((f) => f.endsWith(".js"));
120
+ } catch {
121
+ return null;
122
+ }
123
+ for (const file of files) {
124
+ let content: string;
125
+ try {
126
+ content = fs.readFileSync(path.join(distDir, file), "utf8");
127
+ } catch {
128
+ continue;
129
+ }
130
+ if (!content.includes("function buildToolsCatalogResult")) continue;
131
+ try {
132
+ const mod = (await import(path.join(distDir, file))) as Record<string, unknown>;
133
+ if (typeof mod.buildToolsCatalogResult === "function")
134
+ return mod.buildToolsCatalogResult as BuildFn;
135
+ } catch {
136
+ // unreadable/non-importable candidate → keep scanning
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+
142
+ export interface AgentToolsConfigShape {
143
+ profile?: string;
144
+ allow?: string[];
145
+ alsoAllow?: string[];
146
+ deny?: string[];
147
+ }
148
+
149
+ export interface AgentCatalogTool {
150
+ id: string;
151
+ label: string;
152
+ description: string;
153
+ source: string;
154
+ /** Effective state under the agent's current tools config. */
155
+ enabled: boolean;
156
+ /** Whether the active profile grants this tool (drives the app's allow/deny delta). */
157
+ inProfile: boolean;
158
+ }
159
+ export interface AgentCatalogGroup {
160
+ id: string;
161
+ label: string;
162
+ source: string;
163
+ pluginId?: string;
164
+ tools: AgentCatalogTool[];
165
+ }
166
+ export interface AgentToolsCatalog {
167
+ /** The agent's configured profile (null when unset). */
168
+ profile: string | null;
169
+ profiles: Array<{ id: string; label: string }>;
170
+ groups: AgentCatalogGroup[];
171
+ }
172
+
173
+ function readStringArray(value: unknown): string[] {
174
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
175
+ }
176
+
177
+ /** Read an agent's `tools` config block from the host config. */
178
+ function findAgentTools(cfg: unknown, agentId: string): AgentToolsConfigShape | undefined {
179
+ const list = (
180
+ (cfg as Record<string, unknown> | undefined)?.agents as Record<string, unknown> | undefined
181
+ )?.list as Array<Record<string, unknown>> | undefined;
182
+ if (!Array.isArray(list)) return undefined;
183
+ const entry = list.find((a) => a && typeof a === "object" && normalizeAgentId(a.id) === agentId);
184
+ return entry?.tools as AgentToolsConfigShape | undefined;
185
+ }
186
+
187
+ /**
188
+ * The agent's full tool catalog with per-tool effective state, or null if the core
189
+ * catalog builder can't be located.
190
+ */
191
+ export async function buildAgentToolsCatalog(
192
+ cfg: unknown,
193
+ agentId: string,
194
+ ): Promise<AgentToolsCatalog | null> {
195
+ const build = await loadBuildFn();
196
+ if (!build) return null;
197
+ // Snapshot the channel registry so we can undo the build's `surface:"channel"` re-pin
198
+ // (which would otherwise drop friday-next from the gateway's deliverable channels).
199
+ const channelFns = await loadChannelRegistryFns();
200
+ const channelRegistryBefore = (() => {
201
+ try {
202
+ return channelFns?.get() ?? null;
203
+ } catch {
204
+ return null;
205
+ }
206
+ })();
207
+ let core: CoreCatalogResult;
208
+ try {
209
+ core = build({ cfg, agentId, includePlugins: true });
210
+ } catch {
211
+ return null;
212
+ } finally {
213
+ // Pin the original channel registry back. Idempotent when the build didn't clobber it
214
+ // (core returns early when the surface already points at this registry).
215
+ if (channelFns && channelRegistryBefore) {
216
+ try {
217
+ channelFns.pin(channelRegistryBefore);
218
+ } catch {
219
+ // best effort — never fail the catalog request over the restore
220
+ }
221
+ }
222
+ }
223
+
224
+ const tools = findAgentTools(cfg, agentId);
225
+ const profile =
226
+ typeof tools?.profile === "string" && tools.profile.trim() ? tools.profile.trim() : null;
227
+ const allow = new Set(readStringArray(tools?.allow));
228
+ const alsoAllow = new Set(readStringArray(tools?.alsoAllow));
229
+ const deny = new Set(readStringArray(tools?.deny));
230
+ // No profile + no explicit allow == core's "allow all (except deny)".
231
+ const allowAll = profile === "full" || allow.has("*") || (!profile && allow.size === 0);
232
+
233
+ const groups: AgentCatalogGroup[] = core.groups.map((g) => ({
234
+ id: g.id,
235
+ label: g.label,
236
+ source: g.source,
237
+ pluginId: g.pluginId,
238
+ tools: g.tools.map((t) => {
239
+ const inProfile = allowAll ? true : profile ? t.defaultProfiles.includes(profile) : false;
240
+ let enabled: boolean;
241
+ if (deny.has(t.id)) enabled = false;
242
+ else if (allowAll) enabled = true;
243
+ else enabled = inProfile || allow.has(t.id) || alsoAllow.has(t.id);
244
+ return {
245
+ id: t.id,
246
+ label: t.label,
247
+ description: t.description,
248
+ source: t.source,
249
+ enabled,
250
+ inProfile,
251
+ };
252
+ }),
253
+ }));
254
+
255
+ return { profile, profiles: core.profiles, groups };
256
+ }
257
+
258
+ /** Test-only: reset the cached catalog builder. */
259
+ export function resetToolCatalogCacheForTest(): void {
260
+ cachedBuildFn = undefined;
261
+ }
@@ -28,7 +28,7 @@ export type UpgradeRuntime = {
28
28
  /** Mutate the config file; `afterWrite: { mode: "restart" }` triggers a safe gateway restart. */
29
29
  mutateConfigFile: (params: {
30
30
  afterWrite: ConfigAfterWrite;
31
- mutate: (draft: unknown) => unknown | void;
31
+ mutate: (draft: unknown) => unknown;
32
32
  }) => Promise<unknown>;
33
33
  /**
34
34
  * Filesystem path of THIS loaded plugin (`api.source`). Used to infer the install
@@ -42,7 +42,9 @@ let upgradeRuntime: UpgradeRuntime | null = null;
42
42
 
43
43
  export function setUpgradeRuntime(api: OpenClawPluginApi): void {
44
44
  const runtime = api.runtime as unknown as {
45
- system?: { runCommandWithTimeout?: (argv: string[], opts: unknown) => Promise<SpawnResultLike> };
45
+ system?: {
46
+ runCommandWithTimeout?: (argv: string[], opts: unknown) => Promise<SpawnResultLike>;
47
+ };
46
48
  config: {
47
49
  current: () => unknown;
48
50
  mutateConfigFile?: (params: unknown) => Promise<unknown>;
package/src/version.ts CHANGED
@@ -11,7 +11,7 @@ import { readFileSync } from "node:fs";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
13
  /** Keep in sync with package.json "version" as a last-resort fallback. */
14
- const FALLBACK_VERSION = "0.1.30";
14
+ const FALLBACK_VERSION = "0.1.36";
15
15
 
16
16
  function resolvePluginVersion(): string {
17
17
  // dist layout: <root>/dist/src/version.js → ../../package.json = <root>/package.json
@@ -22,7 +22,11 @@ function resolvePluginVersion(): string {
22
22
  const path = fileURLToPath(new URL(rel, import.meta.url));
23
23
  const raw = readFileSync(path, "utf8");
24
24
  const pkg = JSON.parse(raw) as { name?: string; version?: string };
25
- if (pkg.name === "@syengup/friday-channel-next" && typeof pkg.version === "string" && pkg.version) {
25
+ if (
26
+ pkg.name === "@syengup/friday-channel-next" &&
27
+ typeof pkg.version === "string" &&
28
+ pkg.version
29
+ ) {
26
30
  return pkg.version;
27
31
  }
28
32
  } catch {
package/tsconfig.json CHANGED
@@ -14,4 +14,4 @@
14
14
  },
15
15
  "include": ["index.ts", "src/**/*.ts", "src/**/*.d.ts"],
16
16
  "exclude": ["src/e2e/**", "src/**/*.test.ts", "src/test-support/**", "scripts/**"]
17
- }
17
+ }