toolfactory 0.4.4 → 0.4.6
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/README.md +9 -1
- package/dist/e2e.test.js +32 -3
- package/dist/hosts/openclaw.js +3 -3
- package/dist/model.d.ts +1 -1
- package/dist/model.js +1 -1
- package/dist/project/apply.d.ts +3 -1
- package/dist/project/apply.js +3 -2
- package/dist/project/load.js +42 -10
- package/dist/project/load.test.d.ts +1 -0
- package/dist/project/load.test.js +158 -0
- package/dist/project/plan.js +3 -1
- package/dist/surfaces/openclaw-native.d.ts +1 -1
- package/dist/surfaces/openclaw-native.js +2 -2
- package/dist/surfaces/skill.d.ts +2 -1
- package/dist/surfaces/skill.js +21 -18
- package/dist/surfaces/web.d.ts +2 -2
- package/dist/surfaces/web.js +2 -2
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +1 -1
- package/src/e2e.test.ts +45 -2
- package/src/hosts/openclaw.ts +3 -3
- package/src/model.ts +2 -2
- package/src/project/apply.ts +3 -2
- package/src/project/load.test.ts +229 -0
- package/src/project/load.ts +48 -10
- package/src/project/plan.ts +3 -1
- package/src/surfaces/openclaw-native.ts +2 -2
- package/src/surfaces/skill.ts +23 -18
- package/src/surfaces/web.ts +2 -2
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/index-2fAlyVSV.js +10 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-BAuTYPrZ.js +0 -10
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ generated and every surface still installs, builds and publishes.
|
|
|
22
22
|
- **Gemini CLI extension** — `gemini extensions install https://github.com/GoatInAHat/toolfactory`
|
|
23
23
|
- **OpenClaw plugin** — `openclaw plugins install --link hosts/openclaw` from a checkout
|
|
24
24
|
- **Hermes plugin** — `hermes plugins install https://github.com/GoatInAHat/toolfactory#hosts/hermes/toolfactory_hermes`
|
|
25
|
-
- **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.4.
|
|
25
|
+
- **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.4.6.tgz`
|
|
26
26
|
- **Web app** — `npx -y toolfactory mcp --http --open` serves the operations page beside the
|
|
27
27
|
MCP endpoint on one port and opens it; over MCP or a skill, the `web` operation does the same and
|
|
28
28
|
returns the URL.
|
|
@@ -155,6 +155,14 @@ Toolfactory itself continues to use npm as its package registry.
|
|
|
155
155
|
|
|
156
156
|
## Driving toolfactory from an agent
|
|
157
157
|
|
|
158
|
+
Selecting both `skill` and `openclaw-native` ships the tool's own skill with the native plugin
|
|
159
|
+
by default. `openclaw.skills` is an exact override: `[]` opts out; `["my-tool", "custom"]`
|
|
160
|
+
ships the generated own skill and `.agents/skills/custom/`. An explicit custom-only list
|
|
161
|
+
continues to ship only those skills. The own skill's generated regions are refreshed in the
|
|
162
|
+
same build; authored prose and UTF-8 text resources are retained. The native manifest declares
|
|
163
|
+
`skills: ["./skills"]`, and npm packaging includes that directory. No native plugin skill is
|
|
164
|
+
inferred when the standalone `skill` surface is absent.
|
|
165
|
+
|
|
158
166
|
Every command is an MCP tool, so a host can drive the whole loop without a shell — and nothing is
|
|
159
167
|
registered by hand. Every generated project carries the `.agents/` canon, so `toolfactory` and the
|
|
160
168
|
tool's own kernel are already entries in `.agents/mcp/servers.json`, which `bash .agents/setup`
|
package/dist/e2e.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* own node_modules isn't around to symlink in (no dependencies to run against).
|
|
5
5
|
*/
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import { existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join, relative } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,7 +18,15 @@ describe.skipIf(!existsSync(repoNodeModules))("end-to-end: init -> introspect ->
|
|
|
18
18
|
}, async () => {
|
|
19
19
|
const dir = mkdtempSync(join(tmpdir(), "toolfactory-e2e-"));
|
|
20
20
|
symlinkSync(repoNodeModules, join(dir, "node_modules"), "dir");
|
|
21
|
-
const surfaces = [
|
|
21
|
+
const surfaces = [
|
|
22
|
+
"skill",
|
|
23
|
+
"agent-plugins",
|
|
24
|
+
"claude",
|
|
25
|
+
"mcp",
|
|
26
|
+
"cli",
|
|
27
|
+
"npm",
|
|
28
|
+
"openclaw-native",
|
|
29
|
+
];
|
|
22
30
|
const init = commands.init({
|
|
23
31
|
root: dir,
|
|
24
32
|
name: "hello",
|
|
@@ -33,8 +41,29 @@ describe.skipIf(!existsSync(repoNodeModules))("end-to-end: init -> introspect ->
|
|
|
33
41
|
const echo = snapshot.ops.tools[0];
|
|
34
42
|
expect(echo.inputSchema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
|
|
35
43
|
expect(echo.outputSchema?.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
|
|
36
|
-
|
|
44
|
+
const skillPath = join(dir, "skills/hello/SKILL.md");
|
|
45
|
+
writeFileSync(skillPath, readFileSync(skillPath, "utf8").replace("# hello", "# hello\n\nAuthor procedure."));
|
|
46
|
+
mkdirSync(join(dir, "skills/hello/references"));
|
|
47
|
+
writeFileSync(join(dir, "skills/hello/references/guide.md"), "Retained reference.\n");
|
|
48
|
+
const opsPath = join(dir, "src/ops.ts");
|
|
49
|
+
writeFileSync(opsPath, readFileSync(opsPath, "utf8").replace("Return the text you pass in.", "Freshly rebuilt operation."));
|
|
50
|
+
await commands.introspect(dir);
|
|
37
51
|
await expect(commands.check(dir)).resolves.toBeDefined();
|
|
52
|
+
expect(commands.build(dir).result.written).toEqual([]);
|
|
53
|
+
const bundled = readFileSync(join(dir, "hosts/openclaw/skills/hello/SKILL.md"), "utf8");
|
|
54
|
+
expect(bundled).toBe(readFileSync(skillPath, "utf8"));
|
|
55
|
+
expect(bundled).toContain("Author procedure.");
|
|
56
|
+
expect(bundled).toContain("Freshly rebuilt operation.");
|
|
57
|
+
expect(bundled).not.toContain("Return the text you pass in.");
|
|
58
|
+
const packed = JSON.parse(execFileSync("npm", ["pack", "./hosts/openclaw", "--dry-run", "--json"], {
|
|
59
|
+
cwd: dir,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
}));
|
|
62
|
+
expect(packed[0]?.files.map((file) => file.path)).toEqual(expect.arrayContaining([
|
|
63
|
+
"openclaw.plugin.json",
|
|
64
|
+
"skills/hello/SKILL.md",
|
|
65
|
+
"skills/hello/references/guide.md",
|
|
66
|
+
]));
|
|
38
67
|
const opsOnDisk = JSON.parse(readFileSync(join(dir, "dev.toolfactory/ops.json"), "utf8"));
|
|
39
68
|
expect(opsOnDisk.tools.map((t) => t.name)).toEqual(["echo"]);
|
|
40
69
|
const output = execFileSync(process.execPath, ["--import", "tsx", "src/toolfactory/cli.ts", "echo", "--text", "hi"], { cwd: dir, encoding: "utf8" });
|
package/dist/hosts/openclaw.js
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { loadProject } from "../project/load.js";
|
|
17
|
-
import { HOST_DIR, OPENCLAW_SCAFFOLD, surface } from "../surfaces/openclaw-native.js";
|
|
17
|
+
import { HOST_DIR, OPENCLAW_SCAFFOLD, shipsSkills, surface } from "../surfaces/openclaw-native.js";
|
|
18
18
|
export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
|
|
19
19
|
/** Per-tool values: the scaffold writes its own, and the projection is expected to differ. */
|
|
20
20
|
const PER_TOOL_KEYS = new Set(["name", "version", "description"]);
|
|
@@ -35,7 +35,7 @@ function overridden(project) {
|
|
|
35
35
|
paths.add(".openclaw.compat.pluginApi");
|
|
36
36
|
}
|
|
37
37
|
// Shipped skills append to `files`; upstream's entries must still survive (checked below).
|
|
38
|
-
if (
|
|
38
|
+
if (shipsSkills(project))
|
|
39
39
|
paths.add(".files");
|
|
40
40
|
return paths;
|
|
41
41
|
}
|
|
@@ -99,7 +99,7 @@ export function scaffoldDrift(project) {
|
|
|
99
99
|
? []
|
|
100
100
|
: ["vitest.config.ts: upstream template changed"]),
|
|
101
101
|
];
|
|
102
|
-
if (project
|
|
102
|
+
if (shipsSkills(project)) {
|
|
103
103
|
const upstream = JSON.parse(scaffold("package.json")).files ?? [];
|
|
104
104
|
const ours = JSON.parse(planned(files, "package.json")).files ?? [];
|
|
105
105
|
const lost = upstream.filter((entry) => !ours.includes(entry));
|
package/dist/model.d.ts
CHANGED
|
@@ -281,7 +281,7 @@ export interface Project {
|
|
|
281
281
|
toolfactoryVersion: string;
|
|
282
282
|
/** From the root package.json `packageManager` field; npm when absent. */
|
|
283
283
|
packageManager?: PackageManager;
|
|
284
|
-
/**
|
|
284
|
+
/** Resolved OpenClaw skills, including the generated default, read once so projectors stay pure. */
|
|
285
285
|
skills?: ProjectSkill[];
|
|
286
286
|
}
|
|
287
287
|
export interface Surface {
|
package/dist/model.js
CHANGED
|
@@ -194,7 +194,7 @@ export const ToolConfigSchema = z
|
|
|
194
194
|
skills: z
|
|
195
195
|
.array(z.string())
|
|
196
196
|
.optional()
|
|
197
|
-
.describe("
|
|
197
|
+
.describe("Exact skill selection for the OpenClaw plugin. Omitted: the tool's own generated skill when the skill surface is selected. []: no skills. Other names resolve under `.agents/skills/`; OpenClaw loads the packaged manifest's `skills` roots."),
|
|
198
198
|
dependencies: z.record(z.string(), z.string()).optional(),
|
|
199
199
|
peerDependencies: z.record(z.string(), z.string()).optional(),
|
|
200
200
|
devDependencies: z.record(z.string(), z.string()).optional(),
|
package/dist/project/apply.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PlannedFile, Region } from "../model.js";
|
|
2
|
-
import { type Markers } from "./lock.js";
|
|
2
|
+
import { type Lock, type Markers } from "./lock.js";
|
|
3
3
|
export interface Drift {
|
|
4
4
|
path: string;
|
|
5
5
|
kind: "missing" | "changed" | "orphan" | "unmarked";
|
|
@@ -30,6 +30,8 @@ export declare function removeKeys(document: Record<string, unknown>, keys: read
|
|
|
30
30
|
export declare function managedContent(file: PlannedFile): string;
|
|
31
31
|
/** Compare a plan to the tree without writing. */
|
|
32
32
|
export declare function check(root: string, plan: PlannedFile[], toolfactoryVersion: string): Drift[];
|
|
33
|
+
/** Read-only preview of the exact bytes apply writes, retaining authored regions and extensions. */
|
|
34
|
+
export declare function renderFile(root: string, file: PlannedFile, previous: Lock): string;
|
|
33
35
|
/** Write a plan to the tree and refresh the lock. */
|
|
34
36
|
export declare function apply(root: string, plan: PlannedFile[], toolfactoryVersion: string): ApplyResult;
|
|
35
37
|
export declare function setState(root: string, path: string, state: "generated" | "manual", toolfactoryVersion: string): void;
|
package/dist/project/apply.js
CHANGED
|
@@ -390,7 +390,8 @@ export function check(root, plan, toolfactoryVersion) {
|
|
|
390
390
|
}
|
|
391
391
|
return drift;
|
|
392
392
|
}
|
|
393
|
-
|
|
393
|
+
/** Read-only preview of the exact bytes apply writes, retaining authored regions and extensions. */
|
|
394
|
+
export function renderFile(root, file, previous) {
|
|
394
395
|
const path = join(root, file.path);
|
|
395
396
|
if (file.kind === "file")
|
|
396
397
|
return file.content;
|
|
@@ -468,7 +469,7 @@ export function apply(root, plan, toolfactoryVersion) {
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
else {
|
|
471
|
-
const next =
|
|
472
|
+
const next = renderFile(root, file, previous);
|
|
472
473
|
if (existsSync(path) && readFileSync(path, "utf8") === next) {
|
|
473
474
|
result.unchanged.push(file.path);
|
|
474
475
|
}
|
package/dist/project/load.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { readIdentityFile } from "../identity/read.js";
|
|
6
6
|
import { CAPABILITIES, isInstructionOnly, PACKAGE_MANAGERS, ToolConfigSchema, } from "../model.js";
|
|
7
|
-
import {
|
|
7
|
+
import { has } from "../surfaces/shared.js";
|
|
8
|
+
import { skillFile } from "../surfaces/skill.js";
|
|
9
|
+
import { renderFile } from "./apply.js";
|
|
10
|
+
import { readLock, TOOLFACTORY_DIR } from "./lock.js";
|
|
8
11
|
export const TOOL_PATH = `${TOOLFACTORY_DIR}/tool.json`;
|
|
9
12
|
export const OPS_PATH = `${TOOLFACTORY_DIR}/ops.json`;
|
|
10
13
|
const require = createRequire(import.meta.url);
|
|
@@ -62,13 +65,27 @@ function readPackageManager(root) {
|
|
|
62
65
|
}
|
|
63
66
|
return name;
|
|
64
67
|
}
|
|
65
|
-
/**
|
|
66
|
-
function readProjectSkill(
|
|
67
|
-
const
|
|
68
|
-
|
|
68
|
+
/** The selected skill as a shippable unit: every text file, sorted, content in memory. */
|
|
69
|
+
function readProjectSkill(project, name) {
|
|
70
|
+
const canonPath = `.agents/skills/${name}`;
|
|
71
|
+
const canon = join(project.root, canonPath);
|
|
72
|
+
const lock = readLock(project.root) ?? {
|
|
73
|
+
toolfactoryVersion: project.toolfactoryVersion,
|
|
74
|
+
files: {},
|
|
75
|
+
};
|
|
76
|
+
const entry = lstatSync(canon, { throwIfNoEntry: false });
|
|
77
|
+
const generatedAlias = entry?.isSymbolicLink() &&
|
|
78
|
+
resolve(dirname(canon), readlinkSync(canon)) === join(project.root, "skills", name);
|
|
79
|
+
const custom = project.tool.openclaw?.skills !== undefined &&
|
|
80
|
+
lock.files[canonPath]?.state === "manual" &&
|
|
81
|
+
!generatedAlias;
|
|
82
|
+
const own = has(project, "skill") && name === project.identity.name && !custom;
|
|
83
|
+
const planned = own ? skillFile(project) : undefined;
|
|
84
|
+
const directory = planned ? dirname(join(project.root, planned.path)) : canon;
|
|
85
|
+
if (!existsSync(directory) && !own) {
|
|
69
86
|
throw new Error(`openclaw.skills declares "${name}" but ${join(".agents", "skills", name)} does not exist`);
|
|
70
87
|
}
|
|
71
|
-
const files = readdirSync(directory, { recursive: true, withFileTypes: true })
|
|
88
|
+
const files = (existsSync(directory) ? readdirSync(directory, { recursive: true, withFileTypes: true }) : [])
|
|
72
89
|
.filter((entry) => entry.isFile())
|
|
73
90
|
.map((entry) => join(entry.parentPath, entry.name))
|
|
74
91
|
.sort()
|
|
@@ -76,6 +93,18 @@ function readProjectSkill(root, name) {
|
|
|
76
93
|
path: relative(directory, absolute).replaceAll(sep, "/"),
|
|
77
94
|
content: readFileSync(absolute, "utf8"),
|
|
78
95
|
}));
|
|
96
|
+
if (planned) {
|
|
97
|
+
const content = lock.files[planned.path]?.state === "manual"
|
|
98
|
+
? readFileSync(join(project.root, planned.path), "utf8")
|
|
99
|
+
: renderFile(project.root, planned, lock);
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
files: [
|
|
103
|
+
...files.filter((file) => file.path !== "SKILL.md"),
|
|
104
|
+
{ path: "SKILL.md", content },
|
|
105
|
+
].sort((a, b) => a.path.localeCompare(b.path)),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
79
108
|
return { name, files };
|
|
80
109
|
}
|
|
81
110
|
export function loadProject(rootInput = ".") {
|
|
@@ -86,14 +115,17 @@ export function loadProject(rootInput = ".") {
|
|
|
86
115
|
}
|
|
87
116
|
const tool = ToolConfigSchema.parse(JSON.parse(readFileSync(toolPath, "utf8")));
|
|
88
117
|
const identityFile = readIdentityFile(join(root, tool.identity));
|
|
89
|
-
|
|
118
|
+
const project = {
|
|
90
119
|
root,
|
|
91
120
|
tool,
|
|
92
121
|
identity: identityFile.identity,
|
|
93
122
|
identityExtra: identityFile.extra,
|
|
94
123
|
operations: isInstructionOnly(tool) ? [] : readOps(root),
|
|
95
|
-
skills: (tool.openclaw?.skills ?? []).map((name) => readProjectSkill(root, name)),
|
|
96
124
|
toolfactoryVersion: TOOLFACTORY_VERSION,
|
|
97
125
|
packageManager: readPackageManager(root),
|
|
98
126
|
};
|
|
127
|
+
const names = tool.openclaw?.skills ??
|
|
128
|
+
(has(project, "openclaw-native") && has(project, "skill") ? [project.identity.name] : []);
|
|
129
|
+
project.skills = names.map((name) => readProjectSkill(project, name));
|
|
130
|
+
return project;
|
|
99
131
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { build } from "../commands.js";
|
|
6
|
+
import { HOST_DIR } from "../surfaces/openclaw-native.js";
|
|
7
|
+
import { check, setState } from "./apply.js";
|
|
8
|
+
import { loadProject } from "./load.js";
|
|
9
|
+
import { buildPlan } from "./plan.js";
|
|
10
|
+
const roots = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const root of roots.splice(0))
|
|
13
|
+
rmSync(root, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
function write(root, path, content) {
|
|
16
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
17
|
+
writeFileSync(join(root, path), content);
|
|
18
|
+
}
|
|
19
|
+
function fixture(overrides = {}) {
|
|
20
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-skills-"));
|
|
21
|
+
roots.push(root);
|
|
22
|
+
write(root, "plugin.json", JSON.stringify({ name: "probe", version: "0.1.0", description: "Probe" }));
|
|
23
|
+
write(root, "dev.toolfactory/tool.json", JSON.stringify({
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
identity: "plugin.json",
|
|
26
|
+
binding: "typescript",
|
|
27
|
+
surfaces: ["skill", "cli", "openclaw-native"],
|
|
28
|
+
...overrides,
|
|
29
|
+
}));
|
|
30
|
+
write(root, "dev.toolfactory/ops.json", JSON.stringify({
|
|
31
|
+
tools: [
|
|
32
|
+
{
|
|
33
|
+
name: "echo",
|
|
34
|
+
description: "Current operation",
|
|
35
|
+
inputSchema: { type: "object" },
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
}));
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
function hostJson(root, name) {
|
|
42
|
+
return JSON.parse(readFileSync(join(root, HOST_DIR, name), "utf8"));
|
|
43
|
+
}
|
|
44
|
+
function clean(root) {
|
|
45
|
+
const project = loadProject(root);
|
|
46
|
+
expect(check(root, buildPlan(project), project.toolfactoryVersion)).toEqual([]);
|
|
47
|
+
expect(build(root).result.written).toEqual([]);
|
|
48
|
+
}
|
|
49
|
+
describe("OpenClaw skill loading and regeneration", () => {
|
|
50
|
+
it.each(["typescript", "python"])("ships a fresh %s tool's own skill by default", (binding) => {
|
|
51
|
+
const root = fixture({ binding });
|
|
52
|
+
expect(existsSync(join(root, "skills"))).toBe(false);
|
|
53
|
+
expect(loadProject(root).skills?.map((skill) => skill.name)).toEqual(["probe"]);
|
|
54
|
+
build(root);
|
|
55
|
+
const own = readFileSync(join(root, "skills/probe/SKILL.md"), "utf8");
|
|
56
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/probe/SKILL.md"), "utf8")).toBe(own);
|
|
57
|
+
expect(own).toContain("Current operation");
|
|
58
|
+
expect(hostJson(root, "openclaw.plugin.json").skills).toEqual(["./skills"]);
|
|
59
|
+
expect(hostJson(root, "package.json").files).toContain("skills");
|
|
60
|
+
clean(root);
|
|
61
|
+
});
|
|
62
|
+
it.each([[], ["custom"], ["probe"], ["probe", "custom"]])("honors the exact explicit selection %j", (...names) => {
|
|
63
|
+
const skills = names;
|
|
64
|
+
const root = fixture({ openclaw: { skills } });
|
|
65
|
+
write(root, ".agents/skills/custom/SKILL.md", "Custom authored skill\n");
|
|
66
|
+
write(root, ".agents/skills/custom/references/guide.md", "Custom reference\n");
|
|
67
|
+
expect(loadProject(root).skills?.map((skill) => skill.name)).toEqual(skills);
|
|
68
|
+
build(root);
|
|
69
|
+
expect(existsSync(join(root, HOST_DIR, "skills/probe/SKILL.md"))).toBe(skills.includes("probe"));
|
|
70
|
+
expect(existsSync(join(root, HOST_DIR, "skills/custom/SKILL.md"))).toBe(skills.includes("custom"));
|
|
71
|
+
if (skills.includes("custom")) {
|
|
72
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/custom/references/guide.md"), "utf8")).toBe("Custom reference\n");
|
|
73
|
+
}
|
|
74
|
+
expect(hostJson(root, "openclaw.plugin.json").skills).toEqual(skills.length ? ["./skills"] : undefined);
|
|
75
|
+
expect(hostJson(root, "package.json").files.includes("skills")).toBe(skills.length > 0);
|
|
76
|
+
clean(root);
|
|
77
|
+
});
|
|
78
|
+
it("refreshes generated regions in one build while retaining authored prose and text resources", () => {
|
|
79
|
+
const root = fixture({ openclaw: { skills: ["probe"] } });
|
|
80
|
+
build(root);
|
|
81
|
+
const path = "skills/probe/SKILL.md";
|
|
82
|
+
write(root, path, readFileSync(join(root, path), "utf8").replace("# probe", "# probe\n\nAuthored procedure."));
|
|
83
|
+
write(root, "skills/probe/scripts/check.py", "print('retained')\n");
|
|
84
|
+
write(root, "plugin.json", JSON.stringify({ name: "probe", version: "0.2.0", description: "Updated identity" }));
|
|
85
|
+
write(root, "dev.toolfactory/ops.json", JSON.stringify({
|
|
86
|
+
tools: [
|
|
87
|
+
{
|
|
88
|
+
name: "new_operation",
|
|
89
|
+
description: "Updated operation",
|
|
90
|
+
inputSchema: { type: "object" },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}));
|
|
94
|
+
build(root);
|
|
95
|
+
const bundled = readFileSync(join(root, HOST_DIR, path), "utf8");
|
|
96
|
+
expect(bundled).toBe(readFileSync(join(root, path), "utf8"));
|
|
97
|
+
expect(bundled).toContain("Authored procedure.");
|
|
98
|
+
expect(bundled).toContain("Updated identity");
|
|
99
|
+
expect(bundled).toContain("Updated operation");
|
|
100
|
+
expect(bundled).not.toContain("Current operation");
|
|
101
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/probe/scripts/check.py"), "utf8")).toBe("print('retained')\n");
|
|
102
|
+
clean(root);
|
|
103
|
+
});
|
|
104
|
+
it("removes the default payload and manifest root when the author opts out", () => {
|
|
105
|
+
const root = fixture();
|
|
106
|
+
build(root);
|
|
107
|
+
const path = join(root, "dev.toolfactory/tool.json");
|
|
108
|
+
writeFileSync(path, JSON.stringify({ ...JSON.parse(readFileSync(path, "utf8")), openclaw: { skills: [] } }));
|
|
109
|
+
build(root);
|
|
110
|
+
expect(existsSync(join(root, HOST_DIR, "skills/probe/SKILL.md"))).toBe(false);
|
|
111
|
+
expect(existsSync(join(root, "skills/probe/SKILL.md"))).toBe(true);
|
|
112
|
+
expect(hostJson(root, "openclaw.plugin.json")).not.toHaveProperty("skills");
|
|
113
|
+
expect(hostJson(root, "package.json").files).not.toContain("skills");
|
|
114
|
+
clean(root);
|
|
115
|
+
});
|
|
116
|
+
it("packages an adopted own skill verbatim", () => {
|
|
117
|
+
const root = fixture();
|
|
118
|
+
build(root);
|
|
119
|
+
setState(root, "skills/probe/SKILL.md", "manual", loadProject(root).toolfactoryVersion);
|
|
120
|
+
write(root, "skills/probe/SKILL.md", "Fully authored skill without generator markers\n");
|
|
121
|
+
build(root);
|
|
122
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/probe/SKILL.md"), "utf8")).toBe("Fully authored skill without generator markers\n");
|
|
123
|
+
clean(root);
|
|
124
|
+
});
|
|
125
|
+
it("retains an explicitly selected same-name custom canon when its alias was adopted", () => {
|
|
126
|
+
const root = fixture({ openclaw: { skills: ["probe"] } });
|
|
127
|
+
build(root);
|
|
128
|
+
setState(root, ".agents/skills/probe", "manual", loadProject(root).toolfactoryVersion);
|
|
129
|
+
rmSync(join(root, ".agents/skills/probe"));
|
|
130
|
+
write(root, ".agents/skills/probe/SKILL.md", "Custom same-name procedure\n");
|
|
131
|
+
write(root, ".agents/skills/probe/references/guide.md", "Custom same-name reference\n");
|
|
132
|
+
build(root);
|
|
133
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/probe/SKILL.md"), "utf8")).toBe("Custom same-name procedure\n");
|
|
134
|
+
expect(readFileSync(join(root, HOST_DIR, "skills/probe/references/guide.md"), "utf8")).toBe("Custom same-name reference\n");
|
|
135
|
+
expect(readFileSync(join(root, "skills/probe/SKILL.md"), "utf8")).toContain("Current operation");
|
|
136
|
+
expect(readFileSync(join(root, ".agents/skills/probe/SKILL.md"), "utf8")).toBe("Custom same-name procedure\n");
|
|
137
|
+
clean(root);
|
|
138
|
+
});
|
|
139
|
+
it("does not infer a skill for projects without the selected pair and still validates declared custom skills", () => {
|
|
140
|
+
const hostOnly = fixture({ surfaces: ["openclaw-native"] });
|
|
141
|
+
build(hostOnly);
|
|
142
|
+
expect(loadProject(hostOnly).skills).toEqual([]);
|
|
143
|
+
expect(hostJson(hostOnly, "openclaw.plugin.json")).not.toHaveProperty("skills");
|
|
144
|
+
const instructions = fixture({ runtime: "none", surfaces: ["skill", "codex"] });
|
|
145
|
+
build(instructions);
|
|
146
|
+
expect(loadProject(instructions).skills).toEqual([]);
|
|
147
|
+
expect(existsSync(join(instructions, HOST_DIR))).toBe(false);
|
|
148
|
+
expect(readFileSync(join(instructions, "skills/probe/SKILL.md"), "utf8")).not.toContain("## Operations");
|
|
149
|
+
const custom = fixture({ surfaces: ["openclaw-native"], openclaw: { skills: ["custom"] } });
|
|
150
|
+
expect(() => loadProject(custom)).toThrow(/openclaw.skills declares "custom"/);
|
|
151
|
+
write(custom, ".agents/skills/custom/SKILL.md", "Custom only\n");
|
|
152
|
+
build(custom);
|
|
153
|
+
expect(readFileSync(join(custom, HOST_DIR, "skills/custom/SKILL.md"), "utf8")).toBe("Custom only\n");
|
|
154
|
+
clean(hostOnly);
|
|
155
|
+
clean(instructions);
|
|
156
|
+
clean(custom);
|
|
157
|
+
});
|
|
158
|
+
});
|
package/dist/project/plan.js
CHANGED
|
@@ -86,7 +86,9 @@ export function buildPlan(project, surfaces = selectedSurfaces(project.tool.surf
|
|
|
86
86
|
const manual = Object.entries(lock?.files ?? {})
|
|
87
87
|
.filter(([, entry]) => entry.state === "manual")
|
|
88
88
|
.map(([path]) => path);
|
|
89
|
-
|
|
89
|
+
// Count the plan being applied, including retained manual files, rather than the previous
|
|
90
|
+
// lock: newly bundled skill resources must not leave COVERAGE.md stale after one build.
|
|
91
|
+
const total = new Set([...files.map((file) => file.path), ...manual, "COVERAGE.md"]).size;
|
|
90
92
|
files.push({
|
|
91
93
|
kind: "file",
|
|
92
94
|
path: "COVERAGE.md",
|
|
@@ -96,7 +96,7 @@ export declare const ENTRY_END = "// /tf:entry";
|
|
|
96
96
|
export declare function displayName(id: string): string;
|
|
97
97
|
/** Host registrations the plugin declares beyond tools. */
|
|
98
98
|
export declare function registrations(project: Project): Registration[];
|
|
99
|
-
/** Whether the
|
|
99
|
+
/** Whether the loader resolved any skills for this plugin's payload. */
|
|
100
100
|
export declare function shipsSkills(project: Project): boolean;
|
|
101
101
|
/** `activation` for both the entry and the manifest; OpenClaw's own default is startup. */
|
|
102
102
|
export declare function activation(project: Project): Record<string, unknown>;
|
|
@@ -124,9 +124,9 @@ export function displayName(id) {
|
|
|
124
124
|
export function registrations(project) {
|
|
125
125
|
return project.tool.openclaw?.registers ?? [];
|
|
126
126
|
}
|
|
127
|
-
/** Whether the
|
|
127
|
+
/** Whether the loader resolved any skills for this plugin's payload. */
|
|
128
128
|
export function shipsSkills(project) {
|
|
129
|
-
return (project.
|
|
129
|
+
return (project.skills?.length ?? 0) > 0;
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* The gateway route and the Control UI tab the plugin adds when the tool also has a `web`
|
package/dist/surfaces/skill.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Project, type Surface } from "../model.js";
|
|
1
|
+
import { type Project, type RegionFile, type Surface } from "../model.js";
|
|
2
2
|
export declare const OPERATIONS_BEGIN = "<!-- tf:operations -->";
|
|
3
3
|
export declare const OPERATIONS_END = "<!-- /tf:operations -->";
|
|
4
4
|
export declare function skillPath(project: Project): string;
|
|
5
5
|
export declare function renderOperations(project: Project): string;
|
|
6
6
|
export declare function frontmatter(project: Project): string;
|
|
7
|
+
export declare function skillFile(project: Project): RegionFile;
|
|
7
8
|
export declare const surface: Surface;
|
package/dist/surfaces/skill.js
CHANGED
|
@@ -68,28 +68,31 @@ export function frontmatter(project) {
|
|
|
68
68
|
});
|
|
69
69
|
return `\n${yaml(document).trimEnd()}\n`;
|
|
70
70
|
}
|
|
71
|
+
export function skillFile(project) {
|
|
72
|
+
const path = skillPath(project);
|
|
73
|
+
const regions = [
|
|
74
|
+
{ begin: "---", end: "---", content: frontmatter(project) },
|
|
75
|
+
{ begin: OPERATIONS_BEGIN, end: OPERATIONS_END, content: renderOperations(project) },
|
|
76
|
+
];
|
|
77
|
+
const template = [
|
|
78
|
+
"---",
|
|
79
|
+
"---",
|
|
80
|
+
"",
|
|
81
|
+
`# ${project.identity.name}`,
|
|
82
|
+
"",
|
|
83
|
+
"Explain here when an agent should reach for this tool and how to combine its operations.",
|
|
84
|
+
"",
|
|
85
|
+
OPERATIONS_BEGIN,
|
|
86
|
+
OPERATIONS_END,
|
|
87
|
+
"",
|
|
88
|
+
].join("\n");
|
|
89
|
+
return { kind: "region", path, regions, template };
|
|
90
|
+
}
|
|
71
91
|
export const surface = {
|
|
72
92
|
id: "skill",
|
|
73
93
|
plan(project) {
|
|
74
|
-
const path = skillPath(project);
|
|
75
|
-
const regions = [
|
|
76
|
-
{ begin: "---", end: "---", content: frontmatter(project) },
|
|
77
|
-
{ begin: OPERATIONS_BEGIN, end: OPERATIONS_END, content: renderOperations(project) },
|
|
78
|
-
];
|
|
79
|
-
const template = [
|
|
80
|
-
"---",
|
|
81
|
-
"---",
|
|
82
|
-
"",
|
|
83
|
-
`# ${project.identity.name}`,
|
|
84
|
-
"",
|
|
85
|
-
"Explain here when an agent should reach for this tool and how to combine its operations.",
|
|
86
|
-
"",
|
|
87
|
-
OPERATIONS_BEGIN,
|
|
88
|
-
OPERATIONS_END,
|
|
89
|
-
"",
|
|
90
|
-
].join("\n");
|
|
91
94
|
return [
|
|
92
|
-
|
|
95
|
+
skillFile(project),
|
|
93
96
|
{
|
|
94
97
|
kind: "file",
|
|
95
98
|
path: skillLinkPath(project),
|
package/dist/surfaces/web.d.ts
CHANGED
|
@@ -32,12 +32,12 @@ export declare const WEB_SCAFFOLD: {
|
|
|
32
32
|
readonly preview: "vite preview";
|
|
33
33
|
};
|
|
34
34
|
readonly dependencies: {
|
|
35
|
-
readonly "@base-ui/react": "^1.
|
|
35
|
+
readonly "@base-ui/react": "^1.9.0";
|
|
36
36
|
readonly "@fontsource-variable/geist": "^5.3.0";
|
|
37
37
|
readonly "@tailwindcss/vite": "^4.3.3";
|
|
38
38
|
readonly "class-variance-authority": "^0.7.1";
|
|
39
39
|
readonly cn: "^0.4.0";
|
|
40
|
-
readonly "lucide-react": "^1.
|
|
40
|
+
readonly "lucide-react": "^1.55.0";
|
|
41
41
|
readonly react: "^19.2.8";
|
|
42
42
|
readonly "react-dom": "^19.2.8";
|
|
43
43
|
readonly shadcn: "^4.21.4";
|
package/dist/surfaces/web.js
CHANGED
|
@@ -145,12 +145,12 @@ export default defineConfig({
|
|
|
145
145
|
preview: "vite preview",
|
|
146
146
|
},
|
|
147
147
|
dependencies: {
|
|
148
|
-
"@base-ui/react": "^1.
|
|
148
|
+
"@base-ui/react": "^1.9.0",
|
|
149
149
|
"@fontsource-variable/geist": "^5.3.0",
|
|
150
150
|
"@tailwindcss/vite": "^4.3.3",
|
|
151
151
|
"class-variance-authority": "^0.7.1",
|
|
152
152
|
cn: "^0.4.0",
|
|
153
|
-
"lucide-react": "^1.
|
|
153
|
+
"lucide-react": "^1.55.0",
|
|
154
154
|
react: "^19.2.8",
|
|
155
155
|
"react-dom": "^19.2.8",
|
|
156
156
|
shadcn: "^4.21.4",
|
package/dist/toolfactory/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const operations = [...authored, web];
|
|
|
11
11
|
const program = new Command()
|
|
12
12
|
.name("toolfactory")
|
|
13
13
|
.description("Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.")
|
|
14
|
-
.version("0.4.
|
|
14
|
+
.version("0.4.6");
|
|
15
15
|
for (const op of operations) {
|
|
16
16
|
if (!serves(op, "cli"))
|
|
17
17
|
continue;
|
package/dist/toolfactory/mcp.js
CHANGED
|
@@ -30,7 +30,7 @@ const operations = [...authored, web];
|
|
|
30
30
|
const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
|
|
31
31
|
/** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
|
|
32
32
|
function createServer() {
|
|
33
|
-
const server = new McpServer({ name: "toolfactory", version: "0.4.
|
|
33
|
+
const server = new McpServer({ name: "toolfactory", version: "0.4.6" });
|
|
34
34
|
for (const op of operations) {
|
|
35
35
|
if (!introspecting && !serves(op, "mcp"))
|
|
36
36
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolfactory",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|