@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,59 @@
1
+ /**
2
+ * Discover the full catalog of skills an agent can load — the selectable set the
3
+ * app's skill-management UI offers, matching what OpenClaw's ControlUI shows.
4
+ *
5
+ * OpenClaw resolves an agent's loadable skills from several directories. None of
6
+ * the core's skill-discovery functions are reachable through a stable plugin-sdk
7
+ * specifier (they live only in hash-named chunks), so instead of deep-importing
8
+ * brittle core code we scan the same DIRECTORIES core scans — a far more stable
9
+ * coupling (directory conventions change far less often than bundled chunk names),
10
+ * and every access is guarded so a missing/renamed source just yields fewer skills
11
+ * rather than an error. A skill is a sub-directory containing `SKILL.md` (the same
12
+ * marker core's `loadSkillsFromDir` uses).
13
+ *
14
+ * Each discovered skill is tagged with a `source` category for the UI:
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)
18
+ * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
19
+ * - built-in : bundled core skills (`<openclaw>/skills`)
20
+ * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
21
+ * gated by `plugins.allow`/`entries.enabled` like ControlUI) + config
22
+ * `skills.load.extraDirs` — mirrors ControlUI's "EXTRA" bucket
23
+ * (core tags extension skills `source: "extension"`).
24
+ *
25
+ * Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
26
+ *
27
+ * A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
28
+ * the containing dir name) — NOT the dir name itself, which often differs (e.g. the
29
+ * `self-improving-agent/` dir declares `name: self-improvement`). The `description:`
30
+ * frontmatter field is surfaced too. Discovery is RECURSIVE (mirroring core's
31
+ * `loadSkills`): some skills nest the `SKILL.md` a few levels deep (e.g. redskill
32
+ * installs at `<pkg>/<sub>/<skill>/SKILL.md`).
33
+ *
34
+ * NOT included (out of scope for a name catalog): ClawHub remote-only skills and
35
+ * per-skill eligibility/`disabled` flags. The enabled set is the agent's own
36
+ * `skills[]` config, already returned separately by the config view.
37
+ */
38
+ export type SkillSource = "workspace" | "built-in" | "installed" | "extra";
39
+ export interface DiscoveredSkill {
40
+ id: string;
41
+ description?: string;
42
+ source: SkillSource;
43
+ }
44
+ /** Locate the installed `openclaw` package root (cached). Shared with tool-catalog discovery. */
45
+ export declare function resolveOpenClawRoot(): string | null;
46
+ /**
47
+ * The set of bundled extensions enabled for this install — `plugins.allow` plus any
48
+ * `plugins.entries[name].enabled === true`. ControlUI only surfaces skills from
49
+ * enabled extensions, so we gate on the same set (extension dir name == plugin id).
50
+ */
51
+ export declare function enabledExtensionNames(cfg: unknown): Set<string>;
52
+ /**
53
+ * Full set of skills `agentId` can load, sorted by id, each tagged with its source
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.
56
+ */
57
+ export declare function discoverAvailableSkills(cfg: unknown, agentId: string): DiscoveredSkill[];
58
+ /** Test-only: reset the cached openclaw root. */
59
+ export declare function resetOpenClawRootCacheForTest(): void;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Discover the full catalog of skills an agent can load — the selectable set the
3
+ * app's skill-management UI offers, matching what OpenClaw's ControlUI shows.
4
+ *
5
+ * OpenClaw resolves an agent's loadable skills from several directories. None of
6
+ * the core's skill-discovery functions are reachable through a stable plugin-sdk
7
+ * specifier (they live only in hash-named chunks), so instead of deep-importing
8
+ * brittle core code we scan the same DIRECTORIES core scans — a far more stable
9
+ * coupling (directory conventions change far less often than bundled chunk names),
10
+ * and every access is guarded so a missing/renamed source just yields fewer skills
11
+ * rather than an error. A skill is a sub-directory containing `SKILL.md` (the same
12
+ * marker core's `loadSkillsFromDir` uses).
13
+ *
14
+ * Each discovered skill is tagged with a `source` category for the UI:
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)
18
+ * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
19
+ * - built-in : bundled core skills (`<openclaw>/skills`)
20
+ * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
21
+ * gated by `plugins.allow`/`entries.enabled` like ControlUI) + config
22
+ * `skills.load.extraDirs` — mirrors ControlUI's "EXTRA" bucket
23
+ * (core tags extension skills `source: "extension"`).
24
+ *
25
+ * Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
26
+ *
27
+ * A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
28
+ * the containing dir name) — NOT the dir name itself, which often differs (e.g. the
29
+ * `self-improving-agent/` dir declares `name: self-improvement`). The `description:`
30
+ * frontmatter field is surfaced too. Discovery is RECURSIVE (mirroring core's
31
+ * `loadSkills`): some skills nest the `SKILL.md` a few levels deep (e.g. redskill
32
+ * installs at `<pkg>/<sub>/<skill>/SKILL.md`).
33
+ *
34
+ * NOT included (out of scope for a name catalog): ClawHub remote-only skills and
35
+ * per-skill eligibility/`disabled` flags. The enabled set is the agent's own
36
+ * `skills[]` config, already returned separately by the config view.
37
+ */
38
+ import fs from "node:fs";
39
+ import path from "node:path";
40
+ import { createRequire } from "node:module";
41
+ import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
42
+ import { DEFAULT_AGENT_ID, normalizeAgentId } from "./agent-id.js";
43
+ /** Depth limit for the recursive walk under each source dir (skills nest a few levels). */
44
+ const MAX_SKILL_WALK_DEPTH = 6;
45
+ const IGNORED_WALK_DIRS = new Set(["node_modules", ".git"]);
46
+ /** Extract `name`/`description` from a SKILL.md YAML frontmatter block. */
47
+ function parseSkillFrontmatter(content) {
48
+ const lines = content.split(/\r?\n/);
49
+ if (lines[0]?.trim() !== "---")
50
+ return {};
51
+ let name;
52
+ let description;
53
+ for (let i = 1; i < lines.length; i++) {
54
+ if (lines[i].trim() === "---")
55
+ break;
56
+ const m = /^(name|description)\s*:\s*(.+?)\s*$/.exec(lines[i]);
57
+ if (!m)
58
+ continue;
59
+ let v = m[2].trim();
60
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
61
+ v = v.slice(1, -1);
62
+ }
63
+ v = v.trim();
64
+ if (!v)
65
+ continue;
66
+ if (m[1] === "name" && name === undefined)
67
+ name = v;
68
+ else if (m[1] === "description" && description === undefined)
69
+ description = v;
70
+ }
71
+ return { name, description };
72
+ }
73
+ /**
74
+ * Collect skills reachable under `root`, tagging each with `source`. A directory
75
+ * containing `SKILL.md` IS a skill (id = frontmatter `name`, else dir name) and is
76
+ * not descended into further; other directories are recursed up to a bounded depth.
77
+ * First occurrence of an id wins (call higher-priority sources first). Best-effort.
78
+ */
79
+ function collectSkills(root, source, out, depth = 0) {
80
+ if (depth > MAX_SKILL_WALK_DEPTH)
81
+ return;
82
+ let entries;
83
+ try {
84
+ entries = fs.readdirSync(root, { withFileTypes: true });
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ if (entries.some((e) => e.isFile() && e.name === "SKILL.md")) {
90
+ let fm = {};
91
+ try {
92
+ fm = parseSkillFrontmatter(fs.readFileSync(path.join(root, "SKILL.md"), "utf-8"));
93
+ }
94
+ catch {
95
+ // unreadable frontmatter → fall back to dir name, no description
96
+ }
97
+ const id = fm.name ?? path.basename(root);
98
+ if (!out.has(id))
99
+ out.set(id, { id, description: fm.description, source });
100
+ return; // a skill is a leaf — don't treat its internals as nested skills
101
+ }
102
+ for (const e of entries) {
103
+ if (e.isDirectory() && !e.name.startsWith(".") && !IGNORED_WALK_DIRS.has(e.name)) {
104
+ collectSkills(path.join(root, e.name), source, out, depth + 1);
105
+ }
106
+ }
107
+ }
108
+ let cachedOpenClawRoot;
109
+ /** Locate the installed `openclaw` package root (cached). Shared with tool-catalog discovery. */
110
+ export function resolveOpenClawRoot() {
111
+ if (cachedOpenClawRoot !== undefined)
112
+ return cachedOpenClawRoot;
113
+ cachedOpenClawRoot = computeOpenClawRoot();
114
+ return cachedOpenClawRoot;
115
+ }
116
+ function computeOpenClawRoot() {
117
+ const starts = [];
118
+ // Primary: resolve a subpath this plugin already imports (works inside the gateway
119
+ // where `openclaw/*` is resolvable). Standalone (e.g. unit tests) this throws → skipped.
120
+ try {
121
+ starts.push(createRequire(import.meta.url).resolve("openclaw/plugin-sdk/plugin-entry"));
122
+ }
123
+ catch {
124
+ // not resolvable outside the gateway runtime
125
+ }
126
+ // Fallback: the gateway process entry (`<openclaw>/dist/index.js`) — the plugin runs in it.
127
+ if (typeof process.argv[1] === "string")
128
+ starts.push(process.argv[1]);
129
+ for (const start of starts) {
130
+ let dir = path.dirname(start);
131
+ for (let i = 0; i < 10 && dir !== path.dirname(dir); i++) {
132
+ try {
133
+ const pkgPath = path.join(dir, "package.json");
134
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
135
+ if (pkg?.name === "openclaw")
136
+ return dir;
137
+ }
138
+ catch {
139
+ // keep walking up
140
+ }
141
+ dir = path.dirname(dir);
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ /**
147
+ * The set of bundled extensions enabled for this install — `plugins.allow` plus any
148
+ * `plugins.entries[name].enabled === true`. ControlUI only surfaces skills from
149
+ * enabled extensions, so we gate on the same set (extension dir name == plugin id).
150
+ */
151
+ export function enabledExtensionNames(cfg) {
152
+ const plugins = cfg?.plugins;
153
+ const names = new Set();
154
+ const allow = plugins?.allow;
155
+ if (Array.isArray(allow))
156
+ for (const n of allow)
157
+ if (typeof n === "string")
158
+ names.add(n);
159
+ const entries = plugins?.entries;
160
+ if (entries && typeof entries === "object") {
161
+ for (const [name, val] of Object.entries(entries)) {
162
+ if (val && typeof val === "object" && val.enabled === true)
163
+ names.add(name);
164
+ }
165
+ }
166
+ return names;
167
+ }
168
+ /**
169
+ * Bundled skill source dirs inside the openclaw install, tagged like ControlUI:
170
+ * core `<openclaw>/skills` → "built-in"; per-extension `dist/extensions/<ext>/skills`
171
+ * → "extra" (core tags these `source: "extension"`). Extension skills are included
172
+ * only when the extension is enabled, matching ControlUI's EXTRA bucket.
173
+ */
174
+ function bundledSkillSources(enabledExtensions) {
175
+ const root = resolveOpenClawRoot();
176
+ if (!root)
177
+ return [];
178
+ const out = [
179
+ { dir: path.join(root, "skills"), source: "built-in" },
180
+ ];
181
+ try {
182
+ const extRoot = path.join(root, "dist", "extensions");
183
+ for (const ext of fs.readdirSync(extRoot, { withFileTypes: true })) {
184
+ if (ext.isDirectory() && enabledExtensions.has(ext.name)) {
185
+ out.push({ dir: path.join(extRoot, ext.name, "skills"), source: "extra" });
186
+ }
187
+ }
188
+ }
189
+ catch {
190
+ // no extensions dir on this build
191
+ }
192
+ return out;
193
+ }
194
+ function resolveDefaultAgentId(cfg) {
195
+ const list = cfg?.agents?.list;
196
+ if (Array.isArray(list) && list.length > 0) {
197
+ const def = list.find((a) => a?.default === true) ?? list[0];
198
+ if (def?.id)
199
+ return normalizeAgentId(def.id);
200
+ }
201
+ return DEFAULT_AGENT_ID;
202
+ }
203
+ /**
204
+ * Full set of skills `agentId` can load, sorted by id, each tagged with its source
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.
207
+ */
208
+ export function discoverAvailableSkills(cfg, agentId) {
209
+ const c = cfg;
210
+ const resolveWs = getFridayAgentForwardRuntime()?.resolveAgentWorkspaceDir;
211
+ const sources = [];
212
+ if (resolveWs) {
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
235
+ }
236
+ }
237
+ const extraDirs = c?.skills?.load?.extraDirs;
238
+ if (Array.isArray(extraDirs)) {
239
+ for (const d of extraDirs)
240
+ if (typeof d === "string" && d.trim())
241
+ sources.push({ dir: d.trim(), source: "extra" });
242
+ }
243
+ sources.push(...bundledSkillSources(enabledExtensionNames(c)));
244
+ const out = new Map();
245
+ for (const { dir, source } of sources)
246
+ collectSkills(dir, source, out, 0);
247
+ return [...out.values()].sort((a, b) => a.id.localeCompare(b.id));
248
+ }
249
+ /** Test-only: reset the cached openclaw root. */
250
+ export function resetOpenClawRootCacheForTest() {
251
+ cachedOpenClawRoot = undefined;
252
+ }
@@ -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
  }
@@ -0,0 +1,21 @@
1
+ export interface ThinkingLevelOption {
2
+ id: string;
3
+ label: string;
4
+ }
5
+ export interface ResolvedModelThinking {
6
+ /** Ordered (off → highest) levels the model supports. */
7
+ levels: ThinkingLevelOption[];
8
+ /** Provider/model default level, when the gateway reports one. */
9
+ default?: string;
10
+ }
11
+ /**
12
+ * Resolves the thinking-level option set for `provider`/`modelId` from the running gateway's
13
+ * provider plugins + model catalog. The set varies per model (e.g. GPT-5.4 adds `xhigh`, Gemini adds
14
+ * `adaptive`, DeepSeek V4 adds `xhigh`/`max`, binary providers collapse to `off`/`on`). Falls back to
15
+ * the base five levels when the runtime API is unavailable.
16
+ */
17
+ export declare function resolveModelThinking(provider: string | undefined | null, modelId: string | undefined | null): ResolvedModelThinking;
18
+ /** Resolves thinking levels for a full `provider/model` ref (or bare model id). */
19
+ export declare function resolveModelThinkingForRef(modelRef: string | undefined | null): ResolvedModelThinking;
20
+ /** Whether `level` is a supported thinking level for the given `provider/model` ref. */
21
+ export declare function isThinkingLevelSupportedForRef(modelRef: string | undefined | null, level: string): boolean;
@@ -0,0 +1,48 @@
1
+ import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
2
+ import { splitModelRef } from "./session/session-manager.js";
3
+ /**
4
+ * Base five levels used when the running gateway is too old to expose `resolveThinkingPolicy`, or
5
+ * when resolution fails. Mirrors core `BASE_THINKING_LEVELS` (label === id for the base set).
6
+ */
7
+ const BASE_THINKING_LEVELS = [
8
+ { id: "off", label: "off" },
9
+ { id: "minimal", label: "minimal" },
10
+ { id: "low", label: "low" },
11
+ { id: "medium", label: "medium" },
12
+ { id: "high", label: "high" },
13
+ ];
14
+ /**
15
+ * Resolves the thinking-level option set for `provider`/`modelId` from the running gateway's
16
+ * provider plugins + model catalog. The set varies per model (e.g. GPT-5.4 adds `xhigh`, Gemini adds
17
+ * `adaptive`, DeepSeek V4 adds `xhigh`/`max`, binary providers collapse to `off`/`on`). Falls back to
18
+ * the base five levels when the runtime API is unavailable.
19
+ */
20
+ export function resolveModelThinking(provider, modelId) {
21
+ const resolve = getFridayAgentForwardRuntime()?.resolveThinkingPolicy;
22
+ if (resolve) {
23
+ try {
24
+ const policy = resolve({ provider: provider ?? null, model: modelId ?? null });
25
+ if (policy?.levels?.length) {
26
+ return {
27
+ levels: policy.levels.map((l) => ({ id: l.id, label: l.label })),
28
+ default: policy.defaultLevel ?? undefined,
29
+ };
30
+ }
31
+ }
32
+ catch {
33
+ // Fall through to the base levels below.
34
+ }
35
+ }
36
+ return { levels: BASE_THINKING_LEVELS };
37
+ }
38
+ /** Resolves thinking levels for a full `provider/model` ref (or bare model id). */
39
+ export function resolveModelThinkingForRef(modelRef) {
40
+ if (!modelRef)
41
+ return { levels: BASE_THINKING_LEVELS };
42
+ const split = splitModelRef(modelRef);
43
+ return resolveModelThinking(split.provider, split.modelId);
44
+ }
45
+ /** Whether `level` is a supported thinking level for the given `provider/model` ref. */
46
+ export function isThinkingLevelSupportedForRef(modelRef, level) {
47
+ return resolveModelThinkingForRef(modelRef).levels.some((l) => l.id === level);
48
+ }
@@ -0,0 +1,53 @@
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
+ export interface AgentToolsConfigShape {
16
+ profile?: string;
17
+ allow?: string[];
18
+ alsoAllow?: string[];
19
+ deny?: string[];
20
+ }
21
+ export interface AgentCatalogTool {
22
+ id: string;
23
+ label: string;
24
+ description: string;
25
+ source: string;
26
+ /** Effective state under the agent's current tools config. */
27
+ enabled: boolean;
28
+ /** Whether the active profile grants this tool (drives the app's allow/deny delta). */
29
+ inProfile: boolean;
30
+ }
31
+ export interface AgentCatalogGroup {
32
+ id: string;
33
+ label: string;
34
+ source: string;
35
+ pluginId?: string;
36
+ tools: AgentCatalogTool[];
37
+ }
38
+ export interface AgentToolsCatalog {
39
+ /** The agent's configured profile (null when unset). */
40
+ profile: string | null;
41
+ profiles: Array<{
42
+ id: string;
43
+ label: string;
44
+ }>;
45
+ groups: AgentCatalogGroup[];
46
+ }
47
+ /**
48
+ * The agent's full tool catalog with per-tool effective state, or null if the core
49
+ * catalog builder can't be located.
50
+ */
51
+ export declare function buildAgentToolsCatalog(cfg: unknown, agentId: string): Promise<AgentToolsCatalog | null>;
52
+ /** Test-only: reset the cached catalog builder. */
53
+ export declare function resetToolCatalogCacheForTest(): void;
@@ -0,0 +1,191 @@
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
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { resolveOpenClawRoot } from "./skills-discovery.js";
18
+ import { normalizeAgentId } from "./agent-id.js";
19
+ let cachedBuildFn;
20
+ async function loadBuildFn() {
21
+ if (cachedBuildFn !== undefined)
22
+ return cachedBuildFn;
23
+ cachedBuildFn = await locateBuildFn();
24
+ return cachedBuildFn;
25
+ }
26
+ let cachedChannelRegistryFns;
27
+ async function loadChannelRegistryFns() {
28
+ if (cachedChannelRegistryFns !== undefined)
29
+ return cachedChannelRegistryFns;
30
+ cachedChannelRegistryFns = await locateChannelRegistryFns();
31
+ return cachedChannelRegistryFns;
32
+ }
33
+ async function locateChannelRegistryFns() {
34
+ const root = resolveOpenClawRoot();
35
+ if (!root)
36
+ return null;
37
+ const distDir = path.join(root, "dist");
38
+ let files;
39
+ try {
40
+ files = fs.readdirSync(distDir).filter((f) => f.endsWith(".js"));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ for (const file of files) {
46
+ let content;
47
+ try {
48
+ content = fs.readFileSync(path.join(distDir, file), "utf8");
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ // Only the chunk that re-exports both helpers by their real names is usable.
54
+ if (!content.includes("pinActivePluginChannelRegistry"))
55
+ continue;
56
+ try {
57
+ const mod = (await import(path.join(distDir, file)));
58
+ const pin = mod.pinActivePluginChannelRegistry;
59
+ const get = mod.getActivePluginChannelRegistry;
60
+ if (typeof pin === "function" && typeof get === "function") {
61
+ return {
62
+ get: get,
63
+ pin: pin,
64
+ };
65
+ }
66
+ }
67
+ catch {
68
+ // unreadable/non-importable candidate → keep scanning
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ async function locateBuildFn() {
74
+ const root = resolveOpenClawRoot();
75
+ if (!root)
76
+ return null;
77
+ const distDir = path.join(root, "dist");
78
+ let files;
79
+ try {
80
+ files = fs.readdirSync(distDir).filter((f) => f.endsWith(".js"));
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ for (const file of files) {
86
+ let content;
87
+ try {
88
+ content = fs.readFileSync(path.join(distDir, file), "utf8");
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ if (!content.includes("function buildToolsCatalogResult"))
94
+ continue;
95
+ try {
96
+ const mod = (await import(path.join(distDir, file)));
97
+ if (typeof mod.buildToolsCatalogResult === "function")
98
+ return mod.buildToolsCatalogResult;
99
+ }
100
+ catch {
101
+ // unreadable/non-importable candidate → keep scanning
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ function readStringArray(value) {
107
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
108
+ }
109
+ /** Read an agent's `tools` config block from the host config. */
110
+ function findAgentTools(cfg, agentId) {
111
+ const list = cfg?.agents?.list;
112
+ if (!Array.isArray(list))
113
+ return undefined;
114
+ const entry = list.find((a) => a && typeof a === "object" && normalizeAgentId(a.id) === agentId);
115
+ return entry?.tools;
116
+ }
117
+ /**
118
+ * The agent's full tool catalog with per-tool effective state, or null if the core
119
+ * catalog builder can't be located.
120
+ */
121
+ export async function buildAgentToolsCatalog(cfg, agentId) {
122
+ const build = await loadBuildFn();
123
+ if (!build)
124
+ return null;
125
+ // Snapshot the channel registry so we can undo the build's `surface:"channel"` re-pin
126
+ // (which would otherwise drop friday-next from the gateway's deliverable channels).
127
+ const channelFns = await loadChannelRegistryFns();
128
+ const channelRegistryBefore = (() => {
129
+ try {
130
+ return channelFns?.get() ?? null;
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ })();
136
+ let core;
137
+ try {
138
+ core = build({ cfg, agentId, includePlugins: true });
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ finally {
144
+ // Pin the original channel registry back. Idempotent when the build didn't clobber it
145
+ // (core returns early when the surface already points at this registry).
146
+ if (channelFns && channelRegistryBefore) {
147
+ try {
148
+ channelFns.pin(channelRegistryBefore);
149
+ }
150
+ catch {
151
+ // best effort — never fail the catalog request over the restore
152
+ }
153
+ }
154
+ }
155
+ const tools = findAgentTools(cfg, agentId);
156
+ const profile = typeof tools?.profile === "string" && tools.profile.trim() ? tools.profile.trim() : null;
157
+ const allow = new Set(readStringArray(tools?.allow));
158
+ const alsoAllow = new Set(readStringArray(tools?.alsoAllow));
159
+ const deny = new Set(readStringArray(tools?.deny));
160
+ // No profile + no explicit allow == core's "allow all (except deny)".
161
+ const allowAll = profile === "full" || allow.has("*") || (!profile && allow.size === 0);
162
+ const groups = core.groups.map((g) => ({
163
+ id: g.id,
164
+ label: g.label,
165
+ source: g.source,
166
+ pluginId: g.pluginId,
167
+ tools: g.tools.map((t) => {
168
+ const inProfile = allowAll ? true : profile ? t.defaultProfiles.includes(profile) : false;
169
+ let enabled;
170
+ if (deny.has(t.id))
171
+ enabled = false;
172
+ else if (allowAll)
173
+ enabled = true;
174
+ else
175
+ enabled = inProfile || allow.has(t.id) || alsoAllow.has(t.id);
176
+ return {
177
+ id: t.id,
178
+ label: t.label,
179
+ description: t.description,
180
+ source: t.source,
181
+ enabled,
182
+ inProfile,
183
+ };
184
+ }),
185
+ }));
186
+ return { profile, profiles: core.profiles, groups };
187
+ }
188
+ /** Test-only: reset the cached catalog builder. */
189
+ export function resetToolCatalogCacheForTest() {
190
+ cachedBuildFn = undefined;
191
+ }
@@ -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
@@ -10,7 +10,7 @@
10
10
  import { readFileSync } from "node:fs";
11
11
  import { fileURLToPath } from "node:url";
12
12
  /** Keep in sync with package.json "version" as a last-resort fallback. */
13
- const FALLBACK_VERSION = "0.1.30";
13
+ const FALLBACK_VERSION = "0.1.36";
14
14
  function resolvePluginVersion() {
15
15
  // dist layout: <root>/dist/src/version.js → ../../package.json = <root>/package.json
16
16
  // source layout (vitest/jiti): <root>/src/version.ts → ../package.json = <root>/package.json
@@ -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
  }