@syengup/friday-channel-next 0.1.38 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/skills-discovery.d.ts +11 -4
- package/dist/src/skills-discovery.js +55 -12
- package/package.json +14 -15
- package/src/skills-discovery.test.ts +50 -1
- package/src/skills-discovery.ts +52 -12
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
* in another agent's workspace (main is just another agent, not a shared pool)
|
|
18
18
|
* - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
|
|
19
19
|
* - built-in : bundled core skills (`<openclaw>/skills`)
|
|
20
|
-
* - extra :
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - extra : everything ControlUI buckets as "EXTRA" —
|
|
21
|
+
* • `<configDir>/plugin-skills/` : OpenClaw's symlink dir where every ACTIVATED
|
|
22
|
+
* plugin's skills land, incl. third-party plugins (e.g. the miloco-* set)
|
|
23
|
+
* • `<workspace>/.agents/skills` : project-level agents skills (target agent)
|
|
24
|
+
* • `~/.agents/skills` : personal agents skills
|
|
25
|
+
* • config `skills.load.extraDirs`
|
|
26
|
+
* • ENABLED bundled extensions (`<openclaw>/dist/extensions/<ext>/skills`,
|
|
27
|
+
* gated by `plugins.allow`/`entries.enabled`) — now mostly redundant with
|
|
28
|
+
* plugin-skills, kept as a belt-and-suspenders fallback
|
|
24
29
|
*
|
|
25
30
|
* Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
|
|
31
|
+
* The walk FOLLOWS symlinked directories — plugin-skills entries are symlinks, so without
|
|
32
|
+
* this the entire plugin-provided set is invisible.
|
|
26
33
|
*
|
|
27
34
|
* A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
|
|
28
35
|
* the containing dir name) — NOT the dir name itself, which often differs (e.g. the
|
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
* in another agent's workspace (main is just another agent, not a shared pool)
|
|
18
18
|
* - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
|
|
19
19
|
* - built-in : bundled core skills (`<openclaw>/skills`)
|
|
20
|
-
* - extra :
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - extra : everything ControlUI buckets as "EXTRA" —
|
|
21
|
+
* • `<configDir>/plugin-skills/` : OpenClaw's symlink dir where every ACTIVATED
|
|
22
|
+
* plugin's skills land, incl. third-party plugins (e.g. the miloco-* set)
|
|
23
|
+
* • `<workspace>/.agents/skills` : project-level agents skills (target agent)
|
|
24
|
+
* • `~/.agents/skills` : personal agents skills
|
|
25
|
+
* • config `skills.load.extraDirs`
|
|
26
|
+
* • ENABLED bundled extensions (`<openclaw>/dist/extensions/<ext>/skills`,
|
|
27
|
+
* gated by `plugins.allow`/`entries.enabled`) — now mostly redundant with
|
|
28
|
+
* plugin-skills, kept as a belt-and-suspenders fallback
|
|
24
29
|
*
|
|
25
30
|
* Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
|
|
31
|
+
* The walk FOLLOWS symlinked directories — plugin-skills entries are symlinks, so without
|
|
32
|
+
* this the entire plugin-provided set is invisible.
|
|
26
33
|
*
|
|
27
34
|
* A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
|
|
28
35
|
* the containing dir name) — NOT the dir name itself, which often differs (e.g. the
|
|
@@ -36,6 +43,7 @@
|
|
|
36
43
|
* `skills[]` config, already returned separately by the config view.
|
|
37
44
|
*/
|
|
38
45
|
import fs from "node:fs";
|
|
46
|
+
import os from "node:os";
|
|
39
47
|
import path from "node:path";
|
|
40
48
|
import { createRequire } from "node:module";
|
|
41
49
|
import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
|
|
@@ -100,9 +108,24 @@ function collectSkills(root, source, out, depth = 0) {
|
|
|
100
108
|
return; // a skill is a leaf — don't treat its internals as nested skills
|
|
101
109
|
}
|
|
102
110
|
for (const e of entries) {
|
|
103
|
-
if (e.
|
|
104
|
-
|
|
111
|
+
if (e.name.startsWith(".") || IGNORED_WALK_DIRS.has(e.name))
|
|
112
|
+
continue;
|
|
113
|
+
const full = path.join(root, e.name);
|
|
114
|
+
// Follow symlinked dirs: `readdirSync(withFileTypes)` reports a symlink as
|
|
115
|
+
// `isSymbolicLink()` (never `isDirectory()`), and OpenClaw publishes plugin
|
|
116
|
+
// skills as symlinks under `<configDir>/plugin-skills/` — so without this the
|
|
117
|
+
// entire plugin-skills (e.g. the miloco-* set) source would be silently skipped.
|
|
118
|
+
let isDir = e.isDirectory();
|
|
119
|
+
if (!isDir && e.isSymbolicLink()) {
|
|
120
|
+
try {
|
|
121
|
+
isDir = fs.statSync(full).isDirectory();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
isDir = false; // broken/unreadable symlink
|
|
125
|
+
}
|
|
105
126
|
}
|
|
127
|
+
if (isDir)
|
|
128
|
+
collectSkills(full, source, out, depth + 1);
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
let cachedOpenClawRoot;
|
|
@@ -216,24 +239,44 @@ export function discoverAvailableSkills(cfg, agentId) {
|
|
|
216
239
|
// leaked main's skills into every other agent's catalog.
|
|
217
240
|
try {
|
|
218
241
|
const ws = resolveWs(cfg, agentId);
|
|
219
|
-
if (ws)
|
|
242
|
+
if (ws) {
|
|
220
243
|
sources.push({ dir: path.join(ws, "skills"), source: "workspace" });
|
|
244
|
+
// Project-level agents skills: `<workspace>/.agents/skills` (core source
|
|
245
|
+
// `agents-skills-project`), scoped to this same target agent's workspace.
|
|
246
|
+
sources.push({ dir: path.join(ws, ".agents", "skills"), source: "extra" });
|
|
247
|
+
}
|
|
221
248
|
}
|
|
222
249
|
catch {
|
|
223
250
|
// skip unresolvable workspace
|
|
224
251
|
}
|
|
225
|
-
// Managed
|
|
226
|
-
// DEFAULT agent's workspace parent (the default workspace
|
|
227
|
-
// whereas non-default workspaces may be nested under it).
|
|
252
|
+
// Managed (`<configDir>/skills`) + plugin-skills (`<configDir>/plugin-skills`) dirs. Both are
|
|
253
|
+
// agent-independent; anchor off the DEFAULT agent's workspace parent (the default workspace
|
|
254
|
+
// lives directly under configDir, whereas non-default workspaces may be nested under it).
|
|
228
255
|
try {
|
|
229
256
|
const defaultWs = resolveWs(cfg, resolveDefaultAgentId(c));
|
|
230
|
-
if (defaultWs)
|
|
231
|
-
|
|
257
|
+
if (defaultWs) {
|
|
258
|
+
const configDir = path.dirname(defaultWs);
|
|
259
|
+
sources.push({ dir: path.join(configDir, "skills"), source: "installed" });
|
|
260
|
+
// OpenClaw symlinks every ACTIVATED plugin's skills (incl. third-party plugins under
|
|
261
|
+
// `<configDir>/extensions/*`, e.g. the miloco-* set) into `<configDir>/plugin-skills/`
|
|
262
|
+
// and loads it as an "extra" source. The old dist/extensions scan only caught BUNDLED
|
|
263
|
+
// extensions, so plugin-provided skills were missing entirely.
|
|
264
|
+
sources.push({ dir: path.join(configDir, "plugin-skills"), source: "extra" });
|
|
265
|
+
}
|
|
232
266
|
}
|
|
233
267
|
catch {
|
|
234
268
|
// skip unresolvable managed dir
|
|
235
269
|
}
|
|
236
270
|
}
|
|
271
|
+
// Personal agents skills: `~/.agents/skills` (core source `agents-skills-personal`).
|
|
272
|
+
try {
|
|
273
|
+
const home = os.homedir();
|
|
274
|
+
if (home)
|
|
275
|
+
sources.push({ dir: path.join(home, ".agents", "skills"), source: "extra" });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// home dir unresolvable on this platform — skip
|
|
279
|
+
}
|
|
237
280
|
const extraDirs = c?.skills?.load?.extraDirs;
|
|
238
281
|
if (Array.isArray(extraDirs)) {
|
|
239
282
|
for (const d of extraDirs)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syengup/friday-channel-next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,19 +12,6 @@
|
|
|
12
12
|
"tsconfig.json",
|
|
13
13
|
"openclaw.plugin.json"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc -p tsconfig.json",
|
|
17
|
-
"lint": "eslint .",
|
|
18
|
-
"lint:fix": "eslint . --fix",
|
|
19
|
-
"format": "prettier --write .",
|
|
20
|
-
"format:check": "prettier --check .",
|
|
21
|
-
"prepublishOnly": "pnpm build && rm -rf dist/attachments",
|
|
22
|
-
"test": "npm run test:unit && npm run test:e2e",
|
|
23
|
-
"test:unit": "vitest run",
|
|
24
|
-
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
25
|
-
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
26
|
-
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
27
|
-
},
|
|
28
15
|
"bin": {
|
|
29
16
|
"friday-channel-next": "install.js"
|
|
30
17
|
},
|
|
@@ -75,5 +62,17 @@
|
|
|
75
62
|
"typescript-eslint": "^8.61.1",
|
|
76
63
|
"vitest": "^4.1.5",
|
|
77
64
|
"zod": "^4.3.6"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc -p tsconfig.json",
|
|
68
|
+
"lint": "eslint .",
|
|
69
|
+
"lint:fix": "eslint . --fix",
|
|
70
|
+
"format": "prettier --write .",
|
|
71
|
+
"format:check": "prettier --check .",
|
|
72
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
73
|
+
"test:unit": "vitest run",
|
|
74
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
75
|
+
"test:smoke": "node scripts/e2e-smoke.mjs",
|
|
76
|
+
"test:msg-live": "node scripts/message-roundtrip-live.mjs"
|
|
78
77
|
}
|
|
79
|
-
}
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -22,6 +22,8 @@ function makeSkills(parent: string, ids: string[]): void {
|
|
|
22
22
|
|
|
23
23
|
describe("discoverAvailableSkills", () => {
|
|
24
24
|
let root: string;
|
|
25
|
+
let emptyHome: string;
|
|
26
|
+
let savedHome: string | undefined;
|
|
25
27
|
|
|
26
28
|
function wire(configRoot: string, cfg: unknown): void {
|
|
27
29
|
setFridayAgentForwardRuntime({
|
|
@@ -39,10 +41,21 @@ describe("discoverAvailableSkills", () => {
|
|
|
39
41
|
} as never);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// Isolate `~/.agents/skills` (a real discovery source) from the dev machine's
|
|
46
|
+
// home so exact-match assertions don't pick up the operator's actual skills.
|
|
47
|
+
savedHome = process.env.HOME;
|
|
48
|
+
emptyHome = fs.mkdtempSync(path.join(os.tmpdir(), "friday-home-"));
|
|
49
|
+
process.env.HOME = emptyHome;
|
|
50
|
+
});
|
|
51
|
+
|
|
42
52
|
afterEach(() => {
|
|
53
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
54
|
+
else process.env.HOME = savedHome;
|
|
43
55
|
resetFridayAgentForwardRuntimeForTest();
|
|
44
56
|
resetOpenClawRootCacheForTest();
|
|
45
57
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
58
|
+
if (emptyHome) fs.rmSync(emptyHome, { recursive: true, force: true });
|
|
46
59
|
});
|
|
47
60
|
|
|
48
61
|
it("scans only the target agent's own workspace (not the default agent's), plus managed + extra dirs, deduped and sorted", () => {
|
|
@@ -118,6 +131,42 @@ describe("discoverAvailableSkills", () => {
|
|
|
118
131
|
expect(result.find((s) => s.id === "self-improvement")?.description).toBe("x");
|
|
119
132
|
});
|
|
120
133
|
|
|
134
|
+
it("discovers plugin-skills (symlinks), plus project + personal agents skills as 'extra'", () => {
|
|
135
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "friday-disc-"));
|
|
136
|
+
const configRoot = path.join(root, "configdir");
|
|
137
|
+
const operatorWs = path.join(configRoot, "workspace", "agents", "operator");
|
|
138
|
+
|
|
139
|
+
// target agent's own workspace skills
|
|
140
|
+
makeSkills(path.join(operatorWs, "skills"), ["own-skill"]);
|
|
141
|
+
// project-level agents skills: <workspace>/.agents/skills
|
|
142
|
+
makeSkills(path.join(operatorWs, ".agents", "skills"), ["project-skill"]);
|
|
143
|
+
// personal agents skills: <home>/.agents/skills (HOME isolated in beforeEach)
|
|
144
|
+
makeSkills(path.join(emptyHome, ".agents", "skills"), ["personal-skill"]);
|
|
145
|
+
|
|
146
|
+
// plugin-skills: real plugin skill dirs SYMLINKED into <configDir>/plugin-skills/,
|
|
147
|
+
// exactly how OpenClaw publishes third-party plugin skills (e.g. the miloco-* set).
|
|
148
|
+
const pluginPkg = path.join(root, "miloco-openclaw-plugin", "skills");
|
|
149
|
+
makeSkills(pluginPkg, ["miloco-devices", "miloco-notify"]);
|
|
150
|
+
const pluginSkillsDir = path.join(configRoot, "plugin-skills");
|
|
151
|
+
fs.mkdirSync(pluginSkillsDir, { recursive: true });
|
|
152
|
+
for (const id of ["miloco-devices", "miloco-notify"]) {
|
|
153
|
+
fs.symlinkSync(path.join(pluginPkg, id), path.join(pluginSkillsDir, id), "dir");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cfg = { agents: { list: [{ id: "main", default: true }, { id: "operator" }] } };
|
|
157
|
+
wire(configRoot, cfg);
|
|
158
|
+
|
|
159
|
+
const result = discoverAvailableSkills(cfg, "operator");
|
|
160
|
+
const bySource = Object.fromEntries(result.map((s) => [s.id, s.source]));
|
|
161
|
+
expect(bySource).toEqual({
|
|
162
|
+
"own-skill": "workspace",
|
|
163
|
+
"project-skill": "extra",
|
|
164
|
+
"personal-skill": "extra",
|
|
165
|
+
"miloco-devices": "extra", // followed through the symlink — the miloco regression
|
|
166
|
+
"miloco-notify": "extra",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
121
170
|
it("returns [] without throwing when nothing is resolvable", () => {
|
|
122
171
|
resetFridayAgentForwardRuntimeForTest();
|
|
123
172
|
expect(discoverAvailableSkills({}, "main")).toEqual([]);
|
package/src/skills-discovery.ts
CHANGED
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
* in another agent's workspace (main is just another agent, not a shared pool)
|
|
18
18
|
* - installed : managed skills dir (`<configDir>/skills`, sibling of the workspace)
|
|
19
19
|
* - built-in : bundled core skills (`<openclaw>/skills`)
|
|
20
|
-
* - extra :
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - extra : everything ControlUI buckets as "EXTRA" —
|
|
21
|
+
* • `<configDir>/plugin-skills/` : OpenClaw's symlink dir where every ACTIVATED
|
|
22
|
+
* plugin's skills land, incl. third-party plugins (e.g. the miloco-* set)
|
|
23
|
+
* • `<workspace>/.agents/skills` : project-level agents skills (target agent)
|
|
24
|
+
* • `~/.agents/skills` : personal agents skills
|
|
25
|
+
* • config `skills.load.extraDirs`
|
|
26
|
+
* • ENABLED bundled extensions (`<openclaw>/dist/extensions/<ext>/skills`,
|
|
27
|
+
* gated by `plugins.allow`/`entries.enabled`) — now mostly redundant with
|
|
28
|
+
* plugin-skills, kept as a belt-and-suspenders fallback
|
|
24
29
|
*
|
|
25
30
|
* Dedup is by skill id, first source wins (workspace > installed > extra > built-in).
|
|
31
|
+
* The walk FOLLOWS symlinked directories — plugin-skills entries are symlinks, so without
|
|
32
|
+
* this the entire plugin-provided set is invisible.
|
|
26
33
|
*
|
|
27
34
|
* A skill's id is the `name:` field in its `SKILL.md` frontmatter (falling back to
|
|
28
35
|
* the containing dir name) — NOT the dir name itself, which often differs (e.g. the
|
|
@@ -37,6 +44,7 @@
|
|
|
37
44
|
*/
|
|
38
45
|
|
|
39
46
|
import fs from "node:fs";
|
|
47
|
+
import os from "node:os";
|
|
40
48
|
import path from "node:path";
|
|
41
49
|
import { createRequire } from "node:module";
|
|
42
50
|
import { getFridayAgentForwardRuntime } from "./agent-forward-runtime.js";
|
|
@@ -107,9 +115,21 @@ function collectSkills(
|
|
|
107
115
|
return; // a skill is a leaf — don't treat its internals as nested skills
|
|
108
116
|
}
|
|
109
117
|
for (const e of entries) {
|
|
110
|
-
if (e.
|
|
111
|
-
|
|
118
|
+
if (e.name.startsWith(".") || IGNORED_WALK_DIRS.has(e.name)) continue;
|
|
119
|
+
const full = path.join(root, e.name);
|
|
120
|
+
// Follow symlinked dirs: `readdirSync(withFileTypes)` reports a symlink as
|
|
121
|
+
// `isSymbolicLink()` (never `isDirectory()`), and OpenClaw publishes plugin
|
|
122
|
+
// skills as symlinks under `<configDir>/plugin-skills/` — so without this the
|
|
123
|
+
// entire plugin-skills (e.g. the miloco-* set) source would be silently skipped.
|
|
124
|
+
let isDir = e.isDirectory();
|
|
125
|
+
if (!isDir && e.isSymbolicLink()) {
|
|
126
|
+
try {
|
|
127
|
+
isDir = fs.statSync(full).isDirectory();
|
|
128
|
+
} catch {
|
|
129
|
+
isDir = false; // broken/unreadable symlink
|
|
130
|
+
}
|
|
112
131
|
}
|
|
132
|
+
if (isDir) collectSkills(full, source, out, depth + 1);
|
|
113
133
|
}
|
|
114
134
|
}
|
|
115
135
|
|
|
@@ -227,22 +247,42 @@ export function discoverAvailableSkills(cfg: unknown, agentId: string): Discover
|
|
|
227
247
|
// leaked main's skills into every other agent's catalog.
|
|
228
248
|
try {
|
|
229
249
|
const ws = resolveWs(cfg, agentId);
|
|
230
|
-
if (ws)
|
|
250
|
+
if (ws) {
|
|
251
|
+
sources.push({ dir: path.join(ws, "skills"), source: "workspace" });
|
|
252
|
+
// Project-level agents skills: `<workspace>/.agents/skills` (core source
|
|
253
|
+
// `agents-skills-project`), scoped to this same target agent's workspace.
|
|
254
|
+
sources.push({ dir: path.join(ws, ".agents", "skills"), source: "extra" });
|
|
255
|
+
}
|
|
231
256
|
} catch {
|
|
232
257
|
// skip unresolvable workspace
|
|
233
258
|
}
|
|
234
|
-
// Managed
|
|
235
|
-
// DEFAULT agent's workspace parent (the default workspace
|
|
236
|
-
// whereas non-default workspaces may be nested under it).
|
|
259
|
+
// Managed (`<configDir>/skills`) + plugin-skills (`<configDir>/plugin-skills`) dirs. Both are
|
|
260
|
+
// agent-independent; anchor off the DEFAULT agent's workspace parent (the default workspace
|
|
261
|
+
// lives directly under configDir, whereas non-default workspaces may be nested under it).
|
|
237
262
|
try {
|
|
238
263
|
const defaultWs = resolveWs(cfg, resolveDefaultAgentId(c));
|
|
239
|
-
if (defaultWs)
|
|
240
|
-
|
|
264
|
+
if (defaultWs) {
|
|
265
|
+
const configDir = path.dirname(defaultWs);
|
|
266
|
+
sources.push({ dir: path.join(configDir, "skills"), source: "installed" });
|
|
267
|
+
// OpenClaw symlinks every ACTIVATED plugin's skills (incl. third-party plugins under
|
|
268
|
+
// `<configDir>/extensions/*`, e.g. the miloco-* set) into `<configDir>/plugin-skills/`
|
|
269
|
+
// and loads it as an "extra" source. The old dist/extensions scan only caught BUNDLED
|
|
270
|
+
// extensions, so plugin-provided skills were missing entirely.
|
|
271
|
+
sources.push({ dir: path.join(configDir, "plugin-skills"), source: "extra" });
|
|
272
|
+
}
|
|
241
273
|
} catch {
|
|
242
274
|
// skip unresolvable managed dir
|
|
243
275
|
}
|
|
244
276
|
}
|
|
245
277
|
|
|
278
|
+
// Personal agents skills: `~/.agents/skills` (core source `agents-skills-personal`).
|
|
279
|
+
try {
|
|
280
|
+
const home = os.homedir();
|
|
281
|
+
if (home) sources.push({ dir: path.join(home, ".agents", "skills"), source: "extra" });
|
|
282
|
+
} catch {
|
|
283
|
+
// home dir unresolvable on this platform — skip
|
|
284
|
+
}
|
|
285
|
+
|
|
246
286
|
const extraDirs = (
|
|
247
287
|
(c?.skills as Record<string, unknown> | undefined)?.load as Record<string, unknown> | undefined
|
|
248
288
|
)?.extraDirs;
|