@syengup/friday-channel-next 0.1.30 → 0.1.36

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 (71) hide show
  1. package/README.md +8 -4
  2. package/dist/src/agent/abort-run.d.ts +12 -1
  3. package/dist/src/agent/abort-run.js +24 -9
  4. package/dist/src/agent/media-bridge.d.ts +8 -1
  5. package/dist/src/agent/media-bridge.js +23 -2
  6. package/dist/src/agent-forward-runtime.d.ts +15 -0
  7. package/dist/src/agent-forward-runtime.js +2 -0
  8. package/dist/src/agent-id.d.ts +8 -0
  9. package/dist/src/agent-id.js +21 -0
  10. package/dist/src/channel-actions.js +45 -14
  11. package/dist/src/channel.js +22 -1
  12. package/dist/src/http/handlers/agent-config.d.ts +27 -0
  13. package/dist/src/http/handlers/agent-config.js +182 -0
  14. package/dist/src/http/handlers/agent-files.d.ts +21 -0
  15. package/dist/src/http/handlers/agent-files.js +137 -0
  16. package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
  17. package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
  18. package/dist/src/http/handlers/agents-list.js +1 -19
  19. package/dist/src/http/handlers/cancel.js +12 -6
  20. package/dist/src/http/handlers/files.d.ts +16 -0
  21. package/dist/src/http/handlers/files.js +80 -12
  22. package/dist/src/http/handlers/messages.js +8 -3
  23. package/dist/src/http/handlers/models-list.d.ts +5 -0
  24. package/dist/src/http/handlers/models-list.js +8 -0
  25. package/dist/src/http/handlers/sessions-settings.js +15 -10
  26. package/dist/src/http/server.js +23 -0
  27. package/dist/src/link-preview/ssrf-guard.js +6 -2
  28. package/dist/src/media-fetch.js +4 -1
  29. package/dist/src/skills-discovery.d.ts +58 -0
  30. package/dist/src/skills-discovery.js +247 -0
  31. package/dist/src/thinking-levels.d.ts +21 -0
  32. package/dist/src/thinking-levels.js +48 -0
  33. package/dist/src/tool-catalog.d.ts +53 -0
  34. package/dist/src/tool-catalog.js +192 -0
  35. package/dist/src/version.js +1 -1
  36. package/package.json +1 -1
  37. package/src/agent/abort-run.ts +24 -8
  38. package/src/agent/media-bridge.test.ts +71 -0
  39. package/src/agent/media-bridge.ts +23 -1
  40. package/src/agent-forward-runtime.ts +11 -0
  41. package/src/agent-id.ts +24 -0
  42. package/src/channel-actions.test.ts +47 -0
  43. package/src/channel-actions.ts +38 -14
  44. package/src/channel.lifecycle.test.ts +41 -0
  45. package/src/channel.ts +23 -1
  46. package/src/http/handlers/agent-config.test.ts +205 -0
  47. package/src/http/handlers/agent-config.ts +218 -0
  48. package/src/http/handlers/agent-files.test.ts +136 -0
  49. package/src/http/handlers/agent-files.ts +149 -0
  50. package/src/http/handlers/agent-tools-catalog.ts +42 -0
  51. package/src/http/handlers/agents-list.ts +1 -22
  52. package/src/http/handlers/cancel.test.ts +12 -2
  53. package/src/http/handlers/cancel.ts +12 -6
  54. package/src/http/handlers/files.test.ts +114 -0
  55. package/src/http/handlers/files.ts +97 -13
  56. package/src/http/handlers/messages.ts +7 -2
  57. package/src/http/handlers/models-list.test.ts +114 -0
  58. package/src/http/handlers/models-list.ts +12 -0
  59. package/src/http/handlers/sessions-settings.ts +16 -11
  60. package/src/http/server.ts +24 -0
  61. package/src/link-preview/ssrf-guard.test.ts +7 -2
  62. package/src/link-preview/ssrf-guard.ts +5 -1
  63. package/src/media-fetch.test.ts +1 -1
  64. package/src/media-fetch.ts +4 -1
  65. package/src/openclaw.d.ts +25 -1
  66. package/src/skills-discovery.test.ts +148 -0
  67. package/src/skills-discovery.ts +248 -0
  68. package/src/thinking-levels.test.ts +143 -0
  69. package/src/thinking-levels.ts +68 -0
  70. package/src/tool-catalog.ts +252 -0
  71. package/src/version.ts +1 -1
@@ -0,0 +1,58 @@
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 agent's own + the shared default-agent workspace `skills/`
16
+ * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
17
+ * - built-in : bundled core skills (`<openclaw>/skills`)
18
+ * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
19
+ * gated by `plugins.allow`/`entries.enabled` like ControlUI) + config
20
+ * `skills.load.extraDirs` — mirrors ControlUI's "EXTRA" bucket
21
+ * (core tags extension skills `source: "extension"`).
22
+ *
23
+ * Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
24
+ *
25
+ * A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
26
+ * the containing dir name) — NOT the dir name itself, which often differs (e.g. the
27
+ * `self-improving-agent/` dir declares `name: self-improvement`). The `description:`
28
+ * frontmatter field is surfaced too. Discovery is RECURSIVE (mirroring core's
29
+ * `loadSkills`): some skills nest the `SKILL.md` a few levels deep (e.g. redskill
30
+ * installs at `<pkg>/<sub>/<skill>/SKILL.md`).
31
+ *
32
+ * NOT included (out of scope for a name catalog): ClawHub remote-only skills and
33
+ * per-skill eligibility/`disabled` flags. The enabled set is the agent's own
34
+ * `skills[]` config, already returned separately by the config view.
35
+ */
36
+ export type SkillSource = "workspace" | "built-in" | "installed" | "extra";
37
+ export interface DiscoveredSkill {
38
+ id: string;
39
+ description?: string;
40
+ source: SkillSource;
41
+ }
42
+ /** Locate the installed `openclaw` package root (cached). Shared with tool-catalog discovery. */
43
+ export declare function resolveOpenClawRoot(): string | null;
44
+ /**
45
+ * The set of bundled extensions enabled for this install — `plugins.allow` plus any
46
+ * `plugins.entries[name].enabled === true`. ControlUI only surfaces skills from
47
+ * enabled extensions, so we gate on the same set (extension dir name == plugin id).
48
+ */
49
+ export declare function enabledExtensionNames(cfg: unknown): Set<string>;
50
+ /**
51
+ * Full set of skills `agentId` can load, sorted by id, each tagged with its source
52
+ * category. Aggregates the agent's workspace, the shared root workspace, the managed
53
+ * dir, config extra dirs, and bundled core/extension skills. Every source is optional
54
+ * and failure-tolerant.
55
+ */
56
+ export declare function discoverAvailableSkills(cfg: unknown, agentId: string): DiscoveredSkill[];
57
+ /** Test-only: reset the cached openclaw root. */
58
+ export declare function resetOpenClawRootCacheForTest(): void;
@@ -0,0 +1,247 @@
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 agent's own + the shared default-agent workspace `skills/`
16
+ * - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
17
+ * - built-in : bundled core skills (`<openclaw>/skills`)
18
+ * - extra : skills from ENABLED extensions (`<openclaw>/dist/extensions/<ext>/skills`,
19
+ * gated by `plugins.allow`/`entries.enabled` like ControlUI) + config
20
+ * `skills.load.extraDirs` — mirrors ControlUI's "EXTRA" bucket
21
+ * (core tags extension skills `source: "extension"`).
22
+ *
23
+ * Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
24
+ *
25
+ * A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
26
+ * the containing dir name) — NOT the dir name itself, which often differs (e.g. the
27
+ * `self-improving-agent/` dir declares `name: self-improvement`). The `description:`
28
+ * frontmatter field is surfaced too. Discovery is RECURSIVE (mirroring core's
29
+ * `loadSkills`): some skills nest the `SKILL.md` a few levels deep (e.g. redskill
30
+ * installs at `<pkg>/<sub>/<skill>/SKILL.md`).
31
+ *
32
+ * NOT included (out of scope for a name catalog): ClawHub remote-only skills and
33
+ * per-skill eligibility/`disabled` flags. The enabled set is the agent's own
34
+ * `skills[]` config, already returned separately by the config view.
35
+ */
36
+ import fs from "node:fs";
37
+ import path from "node:path";
38
+ import { createRequire } from "node:module";
39
+ import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
40
+ import { DEFAULT_AGENT_ID, normalizeAgentId } from "./agent-id.js";
41
+ /** Depth limit for the recursive walk under each source dir (skills nest a few levels). */
42
+ const MAX_SKILL_WALK_DEPTH = 6;
43
+ const IGNORED_WALK_DIRS = new Set(["node_modules", ".git"]);
44
+ /** Extract `name`/`description` from a SKILL.md YAML frontmatter block. */
45
+ function parseSkillFrontmatter(content) {
46
+ const lines = content.split(/\r?\n/);
47
+ if (lines[0]?.trim() !== "---")
48
+ return {};
49
+ let name;
50
+ let description;
51
+ for (let i = 1; i < lines.length; i++) {
52
+ if (lines[i].trim() === "---")
53
+ break;
54
+ const m = /^(name|description)\s*:\s*(.+?)\s*$/.exec(lines[i]);
55
+ if (!m)
56
+ continue;
57
+ let v = m[2].trim();
58
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
59
+ v = v.slice(1, -1);
60
+ }
61
+ v = v.trim();
62
+ if (!v)
63
+ continue;
64
+ if (m[1] === "name" && name === undefined)
65
+ name = v;
66
+ else if (m[1] === "description" && description === undefined)
67
+ description = v;
68
+ }
69
+ return { name, description };
70
+ }
71
+ /**
72
+ * Collect skills reachable under `root`, tagging each with `source`. A directory
73
+ * containing `SKILL.md` IS a skill (id = frontmatter `name`, else dir name) and is
74
+ * not descended into further; other directories are recursed up to a bounded depth.
75
+ * First occurrence of an id wins (call higher-priority sources first). Best-effort.
76
+ */
77
+ function collectSkills(root, source, out, depth = 0) {
78
+ if (depth > MAX_SKILL_WALK_DEPTH)
79
+ return;
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(root, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ if (entries.some((e) => e.isFile() && e.name === "SKILL.md")) {
88
+ let fm = {};
89
+ try {
90
+ fm = parseSkillFrontmatter(fs.readFileSync(path.join(root, "SKILL.md"), "utf-8"));
91
+ }
92
+ catch {
93
+ // unreadable frontmatter → fall back to dir name, no description
94
+ }
95
+ const id = fm.name ?? path.basename(root);
96
+ if (!out.has(id))
97
+ out.set(id, { id, description: fm.description, source });
98
+ return; // a skill is a leaf — don't treat its internals as nested skills
99
+ }
100
+ for (const e of entries) {
101
+ if (e.isDirectory() && !e.name.startsWith(".") && !IGNORED_WALK_DIRS.has(e.name)) {
102
+ collectSkills(path.join(root, e.name), source, out, depth + 1);
103
+ }
104
+ }
105
+ }
106
+ let cachedOpenClawRoot;
107
+ /** Locate the installed `openclaw` package root (cached). Shared with tool-catalog discovery. */
108
+ export function resolveOpenClawRoot() {
109
+ if (cachedOpenClawRoot !== undefined)
110
+ return cachedOpenClawRoot;
111
+ cachedOpenClawRoot = computeOpenClawRoot();
112
+ return cachedOpenClawRoot;
113
+ }
114
+ function computeOpenClawRoot() {
115
+ const starts = [];
116
+ // Primary: resolve a subpath this plugin already imports (works inside the gateway
117
+ // where `openclaw/*` is resolvable). Standalone (e.g. unit tests) this throws → skipped.
118
+ try {
119
+ starts.push(createRequire(import.meta.url).resolve("openclaw/plugin-sdk/plugin-entry"));
120
+ }
121
+ catch {
122
+ // not resolvable outside the gateway runtime
123
+ }
124
+ // Fallback: the gateway process entry (`<openclaw>/dist/index.js`) — the plugin runs in it.
125
+ if (typeof process.argv[1] === "string")
126
+ starts.push(process.argv[1]);
127
+ for (const start of starts) {
128
+ let dir = path.dirname(start);
129
+ for (let i = 0; i < 10 && dir !== path.dirname(dir); i++) {
130
+ try {
131
+ const pkgPath = path.join(dir, "package.json");
132
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
133
+ if (pkg?.name === "openclaw")
134
+ return dir;
135
+ }
136
+ catch {
137
+ // keep walking up
138
+ }
139
+ dir = path.dirname(dir);
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * The set of bundled extensions enabled for this install — `plugins.allow` plus any
146
+ * `plugins.entries[name].enabled === true`. ControlUI only surfaces skills from
147
+ * enabled extensions, so we gate on the same set (extension dir name == plugin id).
148
+ */
149
+ export function enabledExtensionNames(cfg) {
150
+ const plugins = cfg?.plugins;
151
+ const names = new Set();
152
+ const allow = plugins?.allow;
153
+ if (Array.isArray(allow))
154
+ for (const n of allow)
155
+ if (typeof n === "string")
156
+ names.add(n);
157
+ const entries = plugins?.entries;
158
+ if (entries && typeof entries === "object") {
159
+ for (const [name, val] of Object.entries(entries)) {
160
+ if (val && typeof val === "object" && val.enabled === true)
161
+ names.add(name);
162
+ }
163
+ }
164
+ return names;
165
+ }
166
+ /**
167
+ * Bundled skill source dirs inside the openclaw install, tagged like ControlUI:
168
+ * core `<openclaw>/skills` → "built-in"; per-extension `dist/extensions/<ext>/skills`
169
+ * → "extra" (core tags these `source: "extension"`). Extension skills are included
170
+ * only when the extension is enabled, matching ControlUI's EXTRA bucket.
171
+ */
172
+ function bundledSkillSources(enabledExtensions) {
173
+ const root = resolveOpenClawRoot();
174
+ if (!root)
175
+ return [];
176
+ const out = [
177
+ { dir: path.join(root, "skills"), source: "built-in" },
178
+ ];
179
+ try {
180
+ const extRoot = path.join(root, "dist", "extensions");
181
+ for (const ext of fs.readdirSync(extRoot, { withFileTypes: true })) {
182
+ if (ext.isDirectory() && enabledExtensions.has(ext.name)) {
183
+ out.push({ dir: path.join(extRoot, ext.name, "skills"), source: "extra" });
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // no extensions dir on this build
189
+ }
190
+ return out;
191
+ }
192
+ function resolveDefaultAgentId(cfg) {
193
+ const list = cfg?.agents?.list;
194
+ if (Array.isArray(list) && list.length > 0) {
195
+ const def = list.find((a) => a?.default === true) ?? list[0];
196
+ if (def?.id)
197
+ return normalizeAgentId(def.id);
198
+ }
199
+ return DEFAULT_AGENT_ID;
200
+ }
201
+ /**
202
+ * Full set of skills `agentId` can load, sorted by id, each tagged with its source
203
+ * category. Aggregates the agent's workspace, the shared root workspace, the managed
204
+ * dir, config extra dirs, and bundled core/extension skills. Every source is optional
205
+ * and failure-tolerant.
206
+ */
207
+ export function discoverAvailableSkills(cfg, agentId) {
208
+ const c = cfg;
209
+ const resolveWs = getFridayAgentForwardRuntime()?.resolveAgentWorkspaceDir;
210
+ const sources = [];
211
+ if (resolveWs) {
212
+ const defaultId = resolveDefaultAgentId(c);
213
+ const ids = agentId === defaultId ? [agentId] : [agentId, defaultId];
214
+ let defaultWs;
215
+ for (const id of ids) {
216
+ try {
217
+ const ws = resolveWs(cfg, id);
218
+ if (ws) {
219
+ sources.push({ dir: path.join(ws, "skills"), source: "workspace" });
220
+ if (id === defaultId)
221
+ defaultWs = ws;
222
+ }
223
+ }
224
+ catch {
225
+ // skip unresolvable workspace
226
+ }
227
+ }
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
+ }
232
+ const extraDirs = c?.skills?.load?.extraDirs;
233
+ if (Array.isArray(extraDirs)) {
234
+ for (const d of extraDirs)
235
+ if (typeof d === "string" && d.trim())
236
+ sources.push({ dir: d.trim(), source: "extra" });
237
+ }
238
+ sources.push(...bundledSkillSources(enabledExtensionNames(c)));
239
+ const out = new Map();
240
+ for (const { dir, source } of sources)
241
+ collectSkills(dir, source, out, 0);
242
+ return [...out.values()].sort((a, b) => a.id.localeCompare(b.id));
243
+ }
244
+ /** Test-only: reset the cached openclaw root. */
245
+ export function resetOpenClawRootCacheForTest() {
246
+ cachedOpenClawRoot = undefined;
247
+ }
@@ -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,192 @@
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
112
+ ?.list;
113
+ if (!Array.isArray(list))
114
+ return undefined;
115
+ const entry = list.find((a) => a && typeof a === "object" && normalizeAgentId(a.id) === agentId);
116
+ return entry?.tools;
117
+ }
118
+ /**
119
+ * The agent's full tool catalog with per-tool effective state, or null if the core
120
+ * catalog builder can't be located.
121
+ */
122
+ export async function buildAgentToolsCatalog(cfg, agentId) {
123
+ const build = await loadBuildFn();
124
+ if (!build)
125
+ return null;
126
+ // Snapshot the channel registry so we can undo the build's `surface:"channel"` re-pin
127
+ // (which would otherwise drop friday-next from the gateway's deliverable channels).
128
+ const channelFns = await loadChannelRegistryFns();
129
+ const channelRegistryBefore = (() => {
130
+ try {
131
+ return channelFns?.get() ?? null;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ })();
137
+ let core;
138
+ try {
139
+ core = build({ cfg, agentId, includePlugins: true });
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ finally {
145
+ // Pin the original channel registry back. Idempotent when the build didn't clobber it
146
+ // (core returns early when the surface already points at this registry).
147
+ if (channelFns && channelRegistryBefore) {
148
+ try {
149
+ channelFns.pin(channelRegistryBefore);
150
+ }
151
+ catch {
152
+ // best effort — never fail the catalog request over the restore
153
+ }
154
+ }
155
+ }
156
+ const tools = findAgentTools(cfg, agentId);
157
+ const profile = (typeof tools?.profile === "string" && tools.profile.trim()) ? tools.profile.trim() : null;
158
+ const allow = new Set(readStringArray(tools?.allow));
159
+ const alsoAllow = new Set(readStringArray(tools?.alsoAllow));
160
+ const deny = new Set(readStringArray(tools?.deny));
161
+ // No profile + no explicit allow == core's "allow all (except deny)".
162
+ const allowAll = profile === "full" || allow.has("*") || (!profile && allow.size === 0);
163
+ const groups = core.groups.map((g) => ({
164
+ id: g.id,
165
+ label: g.label,
166
+ source: g.source,
167
+ pluginId: g.pluginId,
168
+ tools: g.tools.map((t) => {
169
+ const inProfile = allowAll ? true : profile ? t.defaultProfiles.includes(profile) : false;
170
+ let enabled;
171
+ if (deny.has(t.id))
172
+ enabled = false;
173
+ else if (allowAll)
174
+ enabled = true;
175
+ else
176
+ enabled = inProfile || allow.has(t.id) || alsoAllow.has(t.id);
177
+ return {
178
+ id: t.id,
179
+ label: t.label,
180
+ description: t.description,
181
+ source: t.source,
182
+ enabled,
183
+ inProfile,
184
+ };
185
+ }),
186
+ }));
187
+ return { profile, profiles: core.profiles, groups };
188
+ }
189
+ /** Test-only: reset the cached catalog builder. */
190
+ export function resetToolCatalogCacheForTest() {
191
+ cachedBuildFn = undefined;
192
+ }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "0.1.30",
3
+ "version": "0.1.36",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,10 +1,26 @@
1
- export async function abortRun(runId: string): Promise<void> {
2
- if (process.env.VITEST !== "true") {
3
- try {
4
- const { abortAgentHarnessRun } = await import("openclaw/plugin-sdk/agent-harness");
5
- abortAgentHarnessRun(runId);
6
- } catch {
7
- // optional at runtime
8
- }
1
+ export type AbortRunResult = { aborted: boolean; drained: boolean };
2
+
3
+ /**
4
+ * Abort the active run for a channel `sessionKey`.
5
+ *
6
+ * A session has at most one active run at a time, and the SDK keys active runs by
7
+ * their internal `sessionId` (not the channel runId). So resolve sessionKey → sessionId
8
+ * first, then abort-and-drain so the caller learns whether the run actually settled.
9
+ */
10
+ export async function abortRunForSessionKey(sessionKey: string): Promise<AbortRunResult> {
11
+ if (process.env.VITEST === "true") return { aborted: false, drained: false };
12
+ const key = sessionKey.trim();
13
+ if (!key) return { aborted: false, drained: false };
14
+ try {
15
+ const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } = await import(
16
+ "openclaw/plugin-sdk/agent-harness"
17
+ );
18
+ const sessionId = resolveActiveEmbeddedRunSessionId(key);
19
+ if (!sessionId) return { aborted: false, drained: false };
20
+ const result = await abortAndDrainAgentHarnessRun({ sessionId, sessionKey: key });
21
+ return { aborted: result.aborted, drained: result.drained };
22
+ } catch {
23
+ // optional at runtime
24
+ return { aborted: false, drained: false };
9
25
  }
10
26
  }