toolfactory 0.2.0 → 0.2.1

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 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.2.0.tgz`
25
+ - **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.2.1.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.
@@ -82,7 +82,8 @@ function patch(project, launch) {
82
82
  failOnStartupError: true,
83
83
  }),
84
84
  };
85
- return yamlStringify([{ insert: [row] }], { lineWidth: 0 });
85
+ // Region replacement starts immediately after the comment marker.
86
+ return `\n${yamlStringify([{ insert: [row] }], { lineWidth: 0 })}`;
86
87
  }
87
88
  function patchTemplate() {
88
89
  return `${PATCH_BEGIN}\n${PATCH_END}\n\n# Yours: add Cordis rows or local code configuration below this marker.\n`;
@@ -1,5 +1,9 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { describe, expect, it } from "vitest";
2
5
  import { parse as parseYaml } from "yaml";
6
+ import { apply, replaceRegions } from "../project/apply.js";
3
7
  import { DSH_PIN, surface as dsh } from "./dsh.js";
4
8
  const echo = { name: "echo", inputSchema: { type: "object" }, requires: [] };
5
9
  const browse = {
@@ -37,10 +41,35 @@ const files = Object.fromEntries(dsh.plan(project).map((file) => {
37
41
  ? file.content
38
42
  : file.kind === "merge"
39
43
  ? JSON.stringify(file.patch)
40
- : file.regions.map((region) => region.content).join(""),
44
+ : (replaceRegions(file.template, file) ?? file.template),
41
45
  ];
42
46
  }));
43
47
  describe("dsh", () => {
48
+ it("rebuilds legacy patch regions without commenting out insert or losing authored rows", () => {
49
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-dsh-"));
50
+ try {
51
+ const plan = dsh.plan({ ...project, root });
52
+ const patches = plan.filter((file) => file.kind === "region");
53
+ apply(root, plan, project.toolfactoryVersion);
54
+ for (const file of patches) {
55
+ const path = join(root, file.path);
56
+ const legacy = readFileSync(path, "utf8").replace("# tf:mcp\n", "# tf:mcp");
57
+ writeFileSync(path, `${legacy}\n- insert:\n - id: authored\n`);
58
+ }
59
+ apply(root, plan, project.toolfactoryVersion);
60
+ for (const file of patches) {
61
+ const rendered = readFileSync(join(root, file.path), "utf8");
62
+ expect(rendered).toMatch(/^# tf:mcp\n- insert:/);
63
+ expect(parseYaml(rendered, { logLevel: "silent" })).toMatchObject([
64
+ { insert: [{ id: "mcp-hello-tool" }] },
65
+ { insert: [{ id: "authored" }] },
66
+ ]);
67
+ }
68
+ }
69
+ finally {
70
+ rmSync(root, { recursive: true, force: true });
71
+ }
72
+ });
44
73
  it("is two shipped files and no code: the bundle manifest and one mcp-client insert row", () => {
45
74
  // `dsh.bundle.patch` is DSH's whole acceptance test for a bundle; a dependency would be wrong,
46
75
  // because @deepseek-ai/dsh-mcp-client is @deepseek-ai/dsh's own.
@@ -37,7 +37,7 @@ export declare const WEB_SCAFFOLD: {
37
37
  readonly "@tailwindcss/vite": "^4.3.3";
38
38
  readonly "class-variance-authority": "^0.7.1";
39
39
  readonly cn: "^0.2.6";
40
- readonly "lucide-react": "^1.42.0";
40
+ readonly "lucide-react": "^1.43.0";
41
41
  readonly react: "^19.2.8";
42
42
  readonly "react-dom": "^19.2.8";
43
43
  readonly shadcn: "^4.21.0";
@@ -150,7 +150,7 @@ export default defineConfig({
150
150
  "@tailwindcss/vite": "^4.3.3",
151
151
  "class-variance-authority": "^0.7.1",
152
152
  cn: "^0.2.6",
153
- "lucide-react": "^1.42.0",
153
+ "lucide-react": "^1.43.0",
154
154
  react: "^19.2.8",
155
155
  "react-dom": "^19.2.8",
156
156
  shadcn: "^4.21.0",
@@ -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.2.0");
14
+ .version("0.2.1");
15
15
  for (const op of operations) {
16
16
  if (!serves(op, "cli"))
17
17
  continue;
@@ -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.2.0" });
33
+ const server = new McpServer({ name: "toolfactory", version: "0.2.1" });
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.2.0",
3
+ "version": "0.2.1",
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": {
@@ -1,6 +1,10 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { describe, expect, it } from "vitest";
2
5
  import { parse as parseYaml } from "yaml";
3
6
  import type { Operation, Project } from "../model.js";
7
+ import { apply, replaceRegions } from "../project/apply.js";
4
8
  import { DSH_PIN, surface as dsh } from "./dsh.js";
5
9
 
6
10
  const echo: Operation = { name: "echo", inputSchema: { type: "object" }, requires: [] };
@@ -42,12 +46,38 @@ const files = Object.fromEntries(
42
46
  ? file.content
43
47
  : file.kind === "merge"
44
48
  ? JSON.stringify(file.patch)
45
- : file.regions.map((region) => region.content).join(""),
49
+ : (replaceRegions(file.template, file) ?? file.template),
46
50
  ];
47
51
  }),
48
52
  );
49
53
 
50
54
  describe("dsh", () => {
55
+ it("rebuilds legacy patch regions without commenting out insert or losing authored rows", () => {
56
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-dsh-"));
57
+ try {
58
+ const plan = dsh.plan({ ...project, root });
59
+ const patches = plan.filter((file) => file.kind === "region");
60
+ apply(root, plan, project.toolfactoryVersion);
61
+ for (const file of patches) {
62
+ const path = join(root, file.path);
63
+ const legacy = readFileSync(path, "utf8").replace("# tf:mcp\n", "# tf:mcp");
64
+ writeFileSync(path, `${legacy}\n- insert:\n - id: authored\n`);
65
+ }
66
+
67
+ apply(root, plan, project.toolfactoryVersion);
68
+ for (const file of patches) {
69
+ const rendered = readFileSync(join(root, file.path), "utf8");
70
+ expect(rendered).toMatch(/^# tf:mcp\n- insert:/);
71
+ expect(parseYaml(rendered, { logLevel: "silent" })).toMatchObject([
72
+ { insert: [{ id: "mcp-hello-tool" }] },
73
+ { insert: [{ id: "authored" }] },
74
+ ]);
75
+ }
76
+ } finally {
77
+ rmSync(root, { recursive: true, force: true });
78
+ }
79
+ });
80
+
51
81
  it("is two shipped files and no code: the bundle manifest and one mcp-client insert row", () => {
52
82
  // `dsh.bundle.patch` is DSH's whole acceptance test for a bundle; a dependency would be wrong,
53
83
  // because @deepseek-ai/dsh-mcp-client is @deepseek-ai/dsh's own.
@@ -90,7 +90,8 @@ function patch(project: Project, launch: { command: string; args: string[] }): s
90
90
  failOnStartupError: true,
91
91
  }),
92
92
  };
93
- return yamlStringify([{ insert: [row] }], { lineWidth: 0 });
93
+ // Region replacement starts immediately after the comment marker.
94
+ return `\n${yamlStringify([{ insert: [row] }], { lineWidth: 0 })}`;
94
95
  }
95
96
 
96
97
  function patchTemplate(): string {
@@ -154,7 +154,7 @@ export default defineConfig({
154
154
  "@tailwindcss/vite": "^4.3.3",
155
155
  "class-variance-authority": "^0.7.1",
156
156
  cn: "^0.2.6",
157
- "lucide-react": "^1.42.0",
157
+ "lucide-react": "^1.43.0",
158
158
  react: "^19.2.8",
159
159
  "react-dom": "^19.2.8",
160
160
  shadcn: "^4.21.0",
@@ -13,7 +13,7 @@ const operations = [...authored, web];
13
13
  const program = new Command()
14
14
  .name("toolfactory")
15
15
  .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.")
16
- .version("0.2.0");
16
+ .version("0.2.1");
17
17
 
18
18
  for (const op of operations) {
19
19
  if (!serves(op, "cli")) continue;
@@ -33,7 +33,7 @@ const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
33
33
 
34
34
  /** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
35
35
  function createServer(): McpServer {
36
- const server = new McpServer({ name: "toolfactory", version: "0.2.0" });
36
+ const server = new McpServer({ name: "toolfactory", version: "0.2.1" });
37
37
  for (const op of operations) {
38
38
  if (!introspecting && !serves(op, "mcp")) continue;
39
39
  server.registerTool(