toolfactory 0.3.1 → 0.4.3

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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/bindings/typescript.test.js +11 -0
  3. package/dist/commands.js +26 -2
  4. package/dist/commands.test.js +30 -0
  5. package/dist/hosts/github.js +2 -3
  6. package/dist/hosts/openclaw.js +3 -2
  7. package/dist/model.d.ts +4 -0
  8. package/dist/model.js +16 -1
  9. package/dist/project/gate.js +3 -4
  10. package/dist/project/history.d.ts +4 -0
  11. package/dist/surfaces/agents.js +7 -1
  12. package/dist/surfaces/agents.test.js +1 -0
  13. package/dist/surfaces/openclaw-native.js +1 -0
  14. package/dist/surfaces/openclaw-native.test.js +4 -1
  15. package/dist/surfaces/shared.js +2 -0
  16. package/dist/surfaces/vscode-extension.js +1 -1
  17. package/dist/surfaces/web.d.ts +3 -3
  18. package/dist/surfaces/web.js +3 -3
  19. package/dist/surfaces/workflows.js +8 -1
  20. package/dist/surfaces/workflows.test.js +64 -1
  21. package/dist/toolfactory/cli.js +1 -1
  22. package/dist/toolfactory/mcp.js +1 -1
  23. package/package.json +1 -1
  24. package/src/bindings/typescript.test.ts +13 -0
  25. package/src/commands.test.ts +34 -0
  26. package/src/commands.ts +29 -2
  27. package/src/hosts/github.ts +2 -3
  28. package/src/hosts/openclaw.ts +3 -3
  29. package/src/model.ts +20 -1
  30. package/src/project/gate.ts +3 -4
  31. package/src/surfaces/agents.test.ts +1 -0
  32. package/src/surfaces/agents.ts +7 -1
  33. package/src/surfaces/openclaw-native.test.ts +4 -1
  34. package/src/surfaces/openclaw-native.ts +1 -0
  35. package/src/surfaces/shared.ts +1 -0
  36. package/src/surfaces/vscode-extension.ts +1 -1
  37. package/src/surfaces/web.ts +3 -3
  38. package/src/surfaces/workflows.test.ts +75 -1
  39. package/src/surfaces/workflows.ts +9 -3
  40. package/src/toolfactory/cli.ts +1 -1
  41. package/src/toolfactory/mcp.ts +1 -1
  42. package/web/dist/assets/index-DHsze9w4.js +10 -0
  43. package/web/dist/index.html +1 -1
  44. package/web/dist/assets/index-D95aMjZ6.js +0 -10
package/src/model.ts CHANGED
@@ -131,7 +131,26 @@ export const ToolConfigSchema = z
131
131
  "JSON Schema 2020-12 object for the tool's configuration; mark secrets with x-toolfactory.sensitive.",
132
132
  ),
133
133
  tests: z
134
- .object({ examples: z.record(z.string(), jsonObject).default({}) })
134
+ .object({
135
+ examples: z.record(z.string(), jsonObject).default({}),
136
+ live: z
137
+ .object({
138
+ credentials: z
139
+ .array(z.string().regex(/^[A-Z_][A-Z0-9_]*$/))
140
+ .min(1)
141
+ .optional()
142
+ .describe(
143
+ "Environment gates for live tests when auth is supplied outside tool config, such as a host vault. Names only, never values.",
144
+ ),
145
+ ci: z
146
+ .boolean()
147
+ .optional()
148
+ .describe(
149
+ "Set false for live tests that require a local vault or browser unavailable on hosted CI. The native local live-test tier is still generated.",
150
+ ),
151
+ })
152
+ .optional(),
153
+ })
135
154
  .default({ examples: {} })
136
155
  .describe("Example arguments per operation, used by the generated surface-smoke tests."),
137
156
  npm: z.object({ scope: z.string().optional() }).optional(),
@@ -307,11 +307,10 @@ export function packageSteps(project: Project, options: { system?: boolean } = {
307
307
  run: [
308
308
  `npm --prefix ${OPENCLAW_HOST_DIR} install`,
309
309
  `npm --prefix ${OPENCLAW_HOST_DIR} run build`,
310
- `v=$(node -p "require('./package.json').version")`,
311
310
  `cp ${OPENCLAW_HOST_DIR}/package.json ${OPENCLAW_HOST_DIR}/package.json.prepack`,
312
- `(cd ${OPENCLAW_HOST_DIR} && npm pkg set dependencies.toolfactory=$v)`,
313
- `npm pack ./${OPENCLAW_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
314
- `mv ${OPENCLAW_HOST_DIR}/package.json.prepack ${OPENCLAW_HOST_DIR}/package.json`,
311
+ `(trap 'mv ${OPENCLAW_HOST_DIR}/package.json.prepack ${OPENCLAW_HOST_DIR}/package.json' EXIT; ` +
312
+ `(cd ${OPENCLAW_HOST_DIR} && npm pkg set 'dependencies.${npmName(project)}=${project.identity.version}') && ` +
313
+ `npm pack ./${OPENCLAW_HOST_DIR} --pack-destination ${RELEASE_DIR})`,
315
314
  ].join(" && "),
316
315
  });
317
316
  }
@@ -126,6 +126,7 @@ describe("agents", () => {
126
126
  expect(setup).not.toContain("# Project setup goes here");
127
127
  // Owner ask #11: a commit cannot leave a generated file stale, with or without CI.
128
128
  expect(setup).toContain("npx toolfactory check >/dev/null");
129
+ expect(setup).toContain("if [ -f package-lock.json ]; then\n npm ci --no-audit --no-fund");
129
130
  expect(setup).toContain("npm install --no-audit --no-fund");
130
131
  });
131
132
 
@@ -637,7 +637,13 @@ function setupTail(project: Project): string {
637
637
  " exit 1",
638
638
  "fi",
639
639
  ]
640
- : ["npm install --no-audit --no-fund"];
640
+ : [
641
+ "if [ -f package-lock.json ]; then",
642
+ " npm ci --no-audit --no-fund",
643
+ "else",
644
+ " npm install --no-audit --no-fund",
645
+ "fi",
646
+ ];
641
647
  return [
642
648
  "",
643
649
  "# The pre-commit hook installed above converges agent config; toolfactory's drift gate goes in",
@@ -133,7 +133,10 @@ describe("openclaw-native", () => {
133
133
  expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
134
134
 
135
135
  const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
136
- expect(pkg.devDependencies).toMatchObject(OPENCLAW_SCAFFOLD.devDependencies);
136
+ expect(pkg.devDependencies).toMatchObject({
137
+ ...OPENCLAW_SCAFFOLD.devDependencies,
138
+ openclaw: OPENCLAW_SCAFFOLD.openclawVersion,
139
+ });
137
140
  expect(pkg.openclaw).toEqual({
138
141
  extensions: ["./dist/index.js"],
139
142
  compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
@@ -288,6 +288,7 @@ function packageJson(
288
288
  dependencies: { ...OPENCLAW_SCAFFOLD.dependencies, ...core, ...extra?.dependencies },
289
289
  devDependencies: {
290
290
  ...OPENCLAW_SCAFFOLD.devDependencies,
291
+ openclaw: openclawVersion(project),
291
292
  ...(kase ? { "@copilotkit/aimock": OPENCLAW_ADDITIONS.aimock } : {}),
292
293
  ...extra?.devDependencies,
293
294
  },
@@ -118,6 +118,7 @@ export function dataDirEnvName(project: Project): string {
118
118
  * A tool with none has nothing to gate a live tier on, so no live test and no `live` CI job.
119
119
  */
120
120
  export function liveCredentials(project: Project): string[] {
121
+ if (project.tool.tests.live?.credentials) return project.tool.tests.live.credentials;
121
122
  const properties = configProperties(project);
122
123
  const required = new Set(requiredConfig(project));
123
124
  return Object.keys(properties).filter(
@@ -272,7 +272,7 @@ export const surface: Surface = {
272
272
  format: "json",
273
273
  patch: {
274
274
  compilerOptions: {
275
- module: "Node16",
275
+ module: "NodeNext",
276
276
  target: "ES2022",
277
277
  lib: ["ES2022"],
278
278
  types: ["node", "mocha"],
@@ -153,11 +153,11 @@ export default defineConfig({
153
153
  "@fontsource-variable/geist": "^5.3.0",
154
154
  "@tailwindcss/vite": "^4.3.3",
155
155
  "class-variance-authority": "^0.7.1",
156
- cn: "^0.3.0",
157
- "lucide-react": "^1.46.0",
156
+ cn: "^0.4.0",
157
+ "lucide-react": "^1.50.0",
158
158
  react: "^19.2.8",
159
159
  "react-dom": "^19.2.8",
160
- shadcn: "^4.21.0",
160
+ shadcn: "^4.21.1",
161
161
  tailwindcss: "^4.3.3",
162
162
  "tw-animate-css": "^1.4.0",
163
163
  },
@@ -1,5 +1,5 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { describe, expect, it } from "vitest";
@@ -457,6 +457,20 @@ describe("workflows", () => {
457
457
  expect(composeHermes.services.openclaw).toBeUndefined();
458
458
  });
459
459
 
460
+ it("supports explicit live credentials without making runtime configuration mandatory", () => {
461
+ const target = project(["cli"]);
462
+ target.tool.config = undefined;
463
+ target.tool.tests.live = { credentials: ["LIVE_ACCOUNT_READY"] };
464
+ const ci = yamlParse(emitted(target)[".github/workflows/ci.yml"]);
465
+ expect(ci.jobs.live.env).toEqual({ LIVE_ACCOUNT_READY: "${{ secrets.LIVE_ACCOUNT_READY }}" });
466
+ expect(ci.jobs.live.environment).toBe("live-tests");
467
+
468
+ target.tool.tests.live.ci = false;
469
+ const localOnly = yamlParse(emitted(target)[".github/workflows/ci.yml"]);
470
+ expect(localOnly.jobs.live).toBeUndefined();
471
+ expect(localOnly.on.workflow_dispatch).toBeUndefined();
472
+ });
473
+
460
474
  it("adds the T4 live job, and workflow_dispatch, iff a config key is required and sensitive", () => {
461
475
  const ci = yamlParse(emitted(project(["cli"]))[".github/workflows/ci.yml"]);
462
476
  expect(ci.on.workflow_dispatch).toEqual({});
@@ -626,6 +640,11 @@ describe("bootstrap-repo", () => {
626
640
  );
627
641
  expect(release.manual.join(" ")).toContain("NPM_TRUSTED_PUBLISHER");
628
642
  expect(release.manual.filter((step) => step.startsWith("npm:"))).toHaveLength(1);
643
+
644
+ noCredential.tool.tests.live = { credentials: ["LOCAL_VAULT_READY"], ci: false };
645
+ const localOnly = bootstrapRepo(noCredential, { dryRun: true });
646
+ expect(localOnly.secrets).toEqual([]);
647
+ expect(localOnly.commands.some((command) => command.includes("live-tests"))).toBe(false);
629
648
  });
630
649
  });
631
650
 
@@ -662,6 +681,61 @@ describe("gate", () => {
662
681
  ]);
663
682
  });
664
683
 
684
+ it("packs the actual core dependency and restores the host manifest after success or failure", () => {
685
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-plugin-pack-"));
686
+ mkdirSync(join(root, "hosts/openclaw"), { recursive: true });
687
+ mkdirSync(join(root, "bin"));
688
+ const manifest = {
689
+ name: "openclaw-plugin-example",
690
+ dependencies: { "@example/core": "file:../.." },
691
+ };
692
+ writeFileSync(join(root, "hosts/openclaw/package.json"), JSON.stringify(manifest));
693
+ writeFileSync(
694
+ join(root, "bin/npm"),
695
+ `#!/usr/bin/env node
696
+ const fs = require('node:fs');
697
+ const path = require('node:path');
698
+ const args = process.argv.slice(2);
699
+ if (args[0] === 'pkg') {
700
+ const value = JSON.parse(fs.readFileSync('package.json', 'utf8'));
701
+ const [key, version] = args[2].slice('dependencies.'.length).split('=');
702
+ value.dependencies[key] = version;
703
+ fs.writeFileSync('package.json', JSON.stringify(value));
704
+ } else if (args[0] === 'pack') {
705
+ fs.copyFileSync('hosts/openclaw/package.json', 'packed.json');
706
+ process.exit(Number(process.env.PACK_EXIT || 0));
707
+ }
708
+ `,
709
+ { mode: 0o755 },
710
+ );
711
+ const target = project(["npm", "openclaw-native"], {
712
+ identity: { name: "core", version: "1.2.3", description: "Example" },
713
+ });
714
+ // npmName is the publication mapping, not the Tool Factory package's name.
715
+ target.tool.npm = { scope: "@example" };
716
+ const command = packageSteps(target).find(
717
+ (step) => step.name === "OpenClaw plugin tarball",
718
+ )?.run;
719
+ if (!command) throw new Error("OpenClaw packaging step is missing");
720
+ for (const code of [0, 7]) {
721
+ const result = spawnSync("sh", ["-c", command], {
722
+ cwd: root,
723
+ env: {
724
+ ...process.env,
725
+ PATH: `${join(root, "bin")}:${process.env.PATH}`,
726
+ PACK_EXIT: String(code),
727
+ },
728
+ });
729
+ expect(result.status, result.stderr.toString()).toBe(code);
730
+ expect(JSON.parse(readFileSync(join(root, "packed.json"), "utf8")).dependencies).toEqual({
731
+ "@example/core": "1.2.3",
732
+ });
733
+ expect(JSON.parse(readFileSync(join(root, "hosts/openclaw/package.json"), "utf8"))).toEqual(
734
+ manifest,
735
+ );
736
+ }
737
+ });
738
+
665
739
  it("packages one asset per selected distribution surface into dist/release", () => {
666
740
  const target = project([
667
741
  "npm",
@@ -199,11 +199,17 @@ function actionStep(step: GateStep, matrix: boolean): Step {
199
199
  * (an environment gates deployments, not the checkout an untrusted PR ships).
200
200
  */
201
201
  function liveJob(project: Project): Record<string, unknown> | undefined {
202
+ if (project.tool.tests.live?.ci === false) return undefined;
202
203
  if (liveCredentials(project).length === 0) return undefined;
203
204
  const properties = configProperties(project);
204
- const secrets = Object.keys(properties).filter((key) =>
205
- isSensitive(properties[key] as Record<string, unknown>),
206
- );
205
+ const secrets = [
206
+ ...new Set([
207
+ ...Object.keys(properties).filter((key) =>
208
+ isSensitive(properties[key] as Record<string, unknown>),
209
+ ),
210
+ ...liveCredentials(project),
211
+ ]),
212
+ ];
207
213
  const pmName = project.packageManager ?? "npm";
208
214
  const pm = PACKAGE_MANAGER_COMMANDS[pmName];
209
215
  const steps: Step[] =
@@ -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.3.1");
16
+ .version("0.4.3");
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.3.1" });
36
+ const server = new McpServer({ name: "toolfactory", version: "0.4.3" });
37
37
  for (const op of operations) {
38
38
  if (!introspecting && !serves(op, "mcp")) continue;
39
39
  server.registerTool(