toolfactory 0.2.0 → 0.2.4
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 +1 -1
- package/dist/bindings/typescript.js +1 -1
- package/dist/bindings/typescript.test.js +29 -1
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +13 -2
- package/dist/hosts/openclaw.d.ts +8 -0
- package/dist/hosts/openclaw.js +70 -4
- package/dist/hosts/openclaw.test.d.ts +1 -0
- package/dist/hosts/openclaw.test.js +58 -0
- package/dist/hosts/web.d.ts +1 -0
- package/dist/hosts/web.js +6 -3
- package/dist/hosts/web.test.d.ts +1 -0
- package/dist/hosts/web.test.js +8 -0
- package/dist/model.d.ts +12 -0
- package/dist/model.js +4 -0
- package/dist/ops.d.ts +26 -0
- package/dist/ops.js +25 -1
- package/dist/project/gate.js +7 -1
- package/dist/project/history.d.ts +1 -0
- package/dist/project/load.js +19 -2
- package/dist/prompt/review.d.ts +23 -0
- package/dist/prompt/review.js +103 -0
- package/dist/prompt/review.test.d.ts +1 -0
- package/dist/prompt/review.test.js +55 -0
- package/dist/prompt/standard.d.ts +26 -0
- package/dist/prompt/standard.js +68 -0
- package/dist/surfaces/dsh.js +2 -1
- package/dist/surfaces/dsh.test.js +30 -1
- package/dist/surfaces/openclaw-native.d.ts +11 -1
- package/dist/surfaces/openclaw-native.js +23 -3
- package/dist/surfaces/openclaw-native.test.js +11 -1
- package/dist/surfaces/web.d.ts +6 -6
- package/dist/surfaces/web.js +7 -7
- package/dist/surfaces/web.test.js +1 -1
- package/dist/surfaces/workflows.js +2 -2
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +1 -1
- package/src/bindings/typescript.test.ts +32 -1
- package/src/bindings/typescript.ts +1 -1
- package/src/commands.ts +14 -2
- package/src/hosts/openclaw.test.ts +62 -0
- package/src/hosts/openclaw.ts +73 -5
- package/src/hosts/web.test.ts +9 -0
- package/src/hosts/web.ts +6 -3
- package/src/model.ts +15 -0
- package/src/ops.ts +30 -1
- package/src/project/gate.ts +7 -1
- package/src/project/load.ts +23 -2
- package/src/prompt/review.test.ts +68 -0
- package/src/prompt/review.ts +148 -0
- package/src/prompt/standard.ts +93 -0
- package/src/surfaces/dsh.test.ts +31 -1
- package/src/surfaces/dsh.ts +2 -1
- package/src/surfaces/openclaw-native.test.ts +11 -1
- package/src/surfaces/openclaw-native.ts +27 -3
- package/src/surfaces/web.test.ts +1 -1
- package/src/surfaces/web.ts +7 -7
- package/src/surfaces/workflows.ts +2 -2
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/{index-CVm6m4s0.js → index-GlWisnfJ.js} +2 -2
- package/web/dist/index.html +1 -1
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.
|
|
25
|
+
- **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.2.4.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.
|
|
@@ -719,7 +719,7 @@ export function scaffold(project) {
|
|
|
719
719
|
engines: { node: NODE_ENGINES },
|
|
720
720
|
scripts: {
|
|
721
721
|
build: "tsc -p tsconfig.json",
|
|
722
|
-
test: "vitest run --passWithNoTests",
|
|
722
|
+
test: "vitest run --dir src --passWithNoTests",
|
|
723
723
|
"test:live": LIVE_TEST_SCRIPT,
|
|
724
724
|
},
|
|
725
725
|
dependencies: {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { createServer as createNetServer } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { stripVTControlCharacters } from "node:util";
|
|
7
8
|
import { describe, expect, it } from "vitest";
|
|
8
9
|
import { ToolConfigSchema } from "../model.js";
|
|
9
10
|
import { cliCommand, cli as cliFiles, kernel, kernelCommand, liveTest, scaffold, } from "./typescript.js";
|
|
@@ -198,6 +199,33 @@ async function waitForInitialize(url, deadlineMs, token) {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
}
|
|
202
|
+
describe.skipIf(!existsSync(repoNodeModules))("typescript scaffold test commands", () => {
|
|
203
|
+
it("runs core tests without collecting host or live tests, preserving the explicit live lane", () => {
|
|
204
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-test-scope-"));
|
|
205
|
+
symlinkSync(repoNodeModules, join(root, "node_modules"), "dir");
|
|
206
|
+
writeFileSync(join(root, "package.json"), text(scaffold(project(["cli"])), "package.json"));
|
|
207
|
+
const writeTest = (path, body) => {
|
|
208
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
209
|
+
writeFileSync(join(root, path), body);
|
|
210
|
+
};
|
|
211
|
+
const passing = 'import { it } from "vitest"; it("core receipt", () => {});';
|
|
212
|
+
const failing = 'import { it } from "vitest"; it("scope sentinel", () => { throw new Error("outside core"); });';
|
|
213
|
+
writeTest("src/core.test.ts", passing);
|
|
214
|
+
writeTest("hosts/openclaw/src/index.test.ts", failing);
|
|
215
|
+
writeTest("hosts/browser/tests/background.test.ts", failing);
|
|
216
|
+
writeTest("tests/live.test.ts", failing);
|
|
217
|
+
const run = (script) => spawnSync("npm", ["run", script], { cwd: root, encoding: "utf8" });
|
|
218
|
+
const core = run("test");
|
|
219
|
+
expect(core.status, core.stdout + core.stderr).toBe(0);
|
|
220
|
+
expect(stripVTControlCharacters(core.stdout)).toMatch(/Tests\s+1 passed/);
|
|
221
|
+
writeTest("src/core.test.ts", failing);
|
|
222
|
+
expect(run("test").status).toBe(1);
|
|
223
|
+
writeTest("tests/live.test.ts", passing);
|
|
224
|
+
const live = run("test:live");
|
|
225
|
+
expect(live.status, live.stdout + live.stderr).toBe(0);
|
|
226
|
+
expect(stripVTControlCharacters(live.stdout)).toMatch(/Tests\s+1 passed/);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
201
229
|
describe.skipIf(!existsSync(repoNodeModules))("typescript kernel, really run over http", () => {
|
|
202
230
|
it("serves tools/list over --http, and requires the token minted by --pair", {
|
|
203
231
|
timeout: 60_000,
|
package/dist/commands.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type BootstrapResult, type CreateRepoResult } from "./hosts/github.js";
|
|
|
2
2
|
import type { Binding, Command, Project, SurfaceId } from "./model.js";
|
|
3
3
|
import { type Drift } from "./project/apply.js";
|
|
4
4
|
import { type GateStep } from "./project/gate.js";
|
|
5
|
+
import { type PromptReviewReport } from "./prompt/review.js";
|
|
5
6
|
import { type Coverage } from "./report/coverage.js";
|
|
6
7
|
export interface InitOptions {
|
|
7
8
|
root: string;
|
|
@@ -186,3 +187,8 @@ export declare function bootstrapRepo(args: {
|
|
|
186
187
|
}): BootstrapResult;
|
|
187
188
|
export declare function doctor(): DoctorReport;
|
|
188
189
|
export declare const SURFACES: readonly ["skill", "agent-plugins", "claude", "codex", "cursor", "mcp", "mcp-registry", "cli", "npm", "pypi", "openclaw-native", "hermes-native", "clawhub", "web", "dsh", "workflows", "agents", "readme", "gemini", "mcpb", "browser-extension", "vscode-extension", "cargo", "nuget", "maven-central", "rubygems", "packagist", "go-module", "homebrew", "winget", "scoop", "chocolatey", "apt", "rpm"];
|
|
190
|
+
/** `review-prompt`: the codex-prompt-standard loop's evaluator (see `src/prompt/review.ts`). */
|
|
191
|
+
export declare function reviewPrompt(input: {
|
|
192
|
+
prompt: string;
|
|
193
|
+
reference?: string;
|
|
194
|
+
}): PromptReviewReport;
|
package/dist/commands.js
CHANGED
|
@@ -10,6 +10,7 @@ import { parseEnv } from "node:util";
|
|
|
10
10
|
import { getBinding } from "./bindings/index.js";
|
|
11
11
|
import { LIVE_TEST_COMMAND } from "./bindings/python.js";
|
|
12
12
|
import { createRepo, githubSlug, LIVE_ENVIRONMENT, bootstrapRepo as prepareRepository, } from "./hosts/github.js";
|
|
13
|
+
import { coreCopyDrift } from "./hosts/openclaw.js";
|
|
13
14
|
import { assertValidName, projectName } from "./identity/name.js";
|
|
14
15
|
import { introspect as runIntrospect, snapshot } from "./introspect/index.js";
|
|
15
16
|
import { assertNoSensitiveArgument, defaultSurfaces, isInstructionOnly, SURFACE_IDS, ToolConfigSchema, } from "./model.js";
|
|
@@ -19,6 +20,7 @@ import { droppedSurfaces, previousTag, toolAtRef } from "./project/history.js";
|
|
|
19
20
|
import { loadProject, OPS_PATH, TOOL_PATH, TOOLFACTORY_VERSION, toOperation, } from "./project/load.js";
|
|
20
21
|
import { readLock } from "./project/lock.js";
|
|
21
22
|
import { buildPlan, TOOL_SCHEMA_PATH } from "./project/plan.js";
|
|
23
|
+
import { reviewPrompt as evaluatePrompt } from "./prompt/review.js";
|
|
22
24
|
import { computeCoverage } from "./report/coverage.js";
|
|
23
25
|
import { PLUGIN_SCHEMA_ID } from "./surfaces/agent-plugins.js";
|
|
24
26
|
import { reloadLine, SETUP_PATH } from "./surfaces/agents.js";
|
|
@@ -237,11 +239,16 @@ export async function check(root = ".") {
|
|
|
237
239
|
const project = loadProject(root);
|
|
238
240
|
assertNoSensitiveArgument(project.tool.config, project.operations);
|
|
239
241
|
const drift = checkPlan(project.root, buildPlan(project), TOOLFACTORY_VERSION);
|
|
242
|
+
const hostDrift = coreCopyDrift(project.root);
|
|
240
243
|
const ops = isInstructionOnly(project.tool) ? undefined : await snapshot(project);
|
|
241
244
|
if (ops?.changed)
|
|
242
245
|
drift.unshift({ kind: "changed", path: OPS_PATH });
|
|
246
|
+
drift.push(...hostDrift);
|
|
243
247
|
if (drift.length) {
|
|
244
|
-
|
|
248
|
+
const remedy = hostDrift.length
|
|
249
|
+
? "\nthe materialized host core copy is stale: cp -r dist/. hosts/openclaw/node_modules/toolfactory/dist/"
|
|
250
|
+
: "";
|
|
251
|
+
throw new Error(`${drift.length} generated file(s) out of date; run \`toolfactory ${ops?.changed ? "introspect" : "build"}\`:\n${drift.map((d) => ` ${d.kind}: ${d.path}`).join("\n")}${remedy}`);
|
|
245
252
|
}
|
|
246
253
|
return { project, drift };
|
|
247
254
|
}
|
|
@@ -549,7 +556,7 @@ function secretsSummary(report) {
|
|
|
549
556
|
if (!rows.length)
|
|
550
557
|
return undefined;
|
|
551
558
|
const missing = rows.filter((row) => !row.local).map((row) => row.name);
|
|
552
|
-
return `Secrets: ${rows.length - missing.length} of ${rows.length} present in .env${missing.length ? ` (missing ${missing.join(", ")})` : ""}. \`toolfactory secrets\` prints where each one is set, per host and per registry.`;
|
|
559
|
+
return `Secrets: ${rows.length - missing.length} of ${rows.length} present in .env${missing.length ? ` (missing ${missing.join(", ")})` : ""}. \`toolfactory secrets-usage\` prints where each one is set, per host and per registry.`;
|
|
553
560
|
}
|
|
554
561
|
/** The project as the previous tag left it, plus the surfaces it published that are now gone. */
|
|
555
562
|
function retractable(project, tag, tool) {
|
|
@@ -660,3 +667,7 @@ export function doctor() {
|
|
|
660
667
|
return { toolfactory: TOOLFACTORY_VERSION, node: process.version, tools };
|
|
661
668
|
}
|
|
662
669
|
export const SURFACES = SURFACE_IDS;
|
|
670
|
+
/** `review-prompt`: the codex-prompt-standard loop's evaluator (see `src/prompt/review.ts`). */
|
|
671
|
+
export function reviewPrompt(input) {
|
|
672
|
+
return evaluatePrompt(input);
|
|
673
|
+
}
|
package/dist/hosts/openclaw.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { Project } from "../model.js";
|
|
2
|
+
import type { Drift } from "../project/apply.js";
|
|
2
3
|
export declare const DRIFT_ENTRY: string;
|
|
3
4
|
/** Human-readable drift lines; empty means the generator still mirrors upstream. */
|
|
4
5
|
export declare function scaffoldDrift(project: Project): string[];
|
|
6
|
+
/**
|
|
7
|
+
* The copy is what the plugin's runtime lookup resolves, and nothing syncs it: after the core
|
|
8
|
+
* gains an operation, a stale copy still registers the tool but every call fails with
|
|
9
|
+
* "operation … is missing from the core package". Opt-in via a MATERIALIZED marker, so
|
|
10
|
+
* npm-installed cores in generated tools are never compared against a workspace build.
|
|
11
|
+
*/
|
|
12
|
+
export declare function coreCopyDrift(root: string): Drift[];
|
package/dist/hosts/openclaw.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* `validate()` step reaches it: one Command like every other validator.
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
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, surface } from "../surfaces/openclaw-native.js";
|
|
17
|
+
import { HOST_DIR, OPENCLAW_SCAFFOLD, 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"]);
|
|
@@ -33,6 +33,9 @@ function overridden(project) {
|
|
|
33
33
|
paths.add(".peerDependencies.openclaw");
|
|
34
34
|
paths.add(".openclaw.compat.pluginApi");
|
|
35
35
|
}
|
|
36
|
+
// Shipped skills append to `files`; upstream's entries must still survive (checked below).
|
|
37
|
+
if (openclaw?.skills?.length)
|
|
38
|
+
paths.add(".files");
|
|
36
39
|
return paths;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
@@ -68,10 +71,25 @@ function planned(files, path) {
|
|
|
68
71
|
export function scaffoldDrift(project) {
|
|
69
72
|
const files = surface.plan(project);
|
|
70
73
|
const directory = mkdtempSync(join(tmpdir(), "toolfactory-openclaw-"));
|
|
71
|
-
//
|
|
74
|
+
// The probe must run the exact openclaw this project pins (author override or the
|
|
75
|
+
// scaffold default) - never whatever happens to be on PATH, which made the drift verdict
|
|
76
|
+
// environment-dependent: the scaffold openclawVersion is the running CLI own version.
|
|
77
|
+
const pin = project.tool.openclaw?.devDependencies?.openclaw ?? OPENCLAW_SCAFFOLD.devDependencies.openclaw;
|
|
72
78
|
const pinned = join(project.root, HOST_DIR, "node_modules/.bin/openclaw");
|
|
73
79
|
try {
|
|
74
|
-
|
|
80
|
+
const probe = existsSync(pinned)
|
|
81
|
+
? { command: pinned, pre: [] }
|
|
82
|
+
: { command: "npx", pre: ["--yes", `openclaw@${pin}`] };
|
|
83
|
+
execFileSync(probe.command, [
|
|
84
|
+
...probe.pre,
|
|
85
|
+
"plugins",
|
|
86
|
+
"init",
|
|
87
|
+
"tf-probe",
|
|
88
|
+
"--type",
|
|
89
|
+
"tool",
|
|
90
|
+
"--directory",
|
|
91
|
+
join(directory, "probe"),
|
|
92
|
+
], { stdio: "pipe" });
|
|
75
93
|
const scaffold = (path) => readFileSync(join(directory, "probe", path), "utf8");
|
|
76
94
|
const skip = overridden(project);
|
|
77
95
|
const drift = [
|
|
@@ -80,12 +98,60 @@ export function scaffoldDrift(project) {
|
|
|
80
98
|
? []
|
|
81
99
|
: ["vitest.config.ts: upstream template changed"]),
|
|
82
100
|
];
|
|
101
|
+
if (project.tool.openclaw?.skills?.length) {
|
|
102
|
+
const upstream = JSON.parse(scaffold("package.json")).files ?? [];
|
|
103
|
+
const ours = JSON.parse(planned(files, "package.json")).files ?? [];
|
|
104
|
+
const lost = upstream.filter((entry) => !ours.includes(entry));
|
|
105
|
+
if (lost.length)
|
|
106
|
+
drift.push(`package.json.files lost upstream entries: ${lost.join(", ")}`);
|
|
107
|
+
}
|
|
83
108
|
return drift;
|
|
84
109
|
}
|
|
85
110
|
finally {
|
|
86
111
|
rmSync(directory, { recursive: true, force: true });
|
|
87
112
|
}
|
|
88
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The copy is what the plugin's runtime lookup resolves, and nothing syncs it: after the core
|
|
116
|
+
* gains an operation, a stale copy still registers the tool but every call fails with
|
|
117
|
+
* "operation … is missing from the core package". Opt-in via a MATERIALIZED marker, so
|
|
118
|
+
* npm-installed cores in generated tools are never compared against a workspace build.
|
|
119
|
+
*/
|
|
120
|
+
export function coreCopyDrift(root) {
|
|
121
|
+
// Resolves HOST_DIR lazily: this module sits in an import cycle with
|
|
122
|
+
// surfaces/openclaw-native.js (DRIFT_ENTRY), so nothing may touch its exports at module scope.
|
|
123
|
+
const copy = join(root, HOST_DIR, "node_modules", "toolfactory");
|
|
124
|
+
if (!existsSync(join(copy, "MATERIALIZED")))
|
|
125
|
+
return [];
|
|
126
|
+
const source = join(root, "dist");
|
|
127
|
+
if (!existsSync(source))
|
|
128
|
+
return [];
|
|
129
|
+
return treeDrift(source, join(copy, "dist"), join(HOST_DIR, "node_modules", "toolfactory", "dist"));
|
|
130
|
+
}
|
|
131
|
+
/** Byte-compare two trees in both directions; `label` prefixes every reported path. */
|
|
132
|
+
function treeDrift(source, copy, label) {
|
|
133
|
+
if (!existsSync(copy))
|
|
134
|
+
return [{ kind: "changed", path: label }];
|
|
135
|
+
const drift = [];
|
|
136
|
+
for (const name of new Set([...readdirSync(source), ...readdirSync(copy)])) {
|
|
137
|
+
const ours = join(source, name);
|
|
138
|
+
const theirs = join(copy, name);
|
|
139
|
+
const oursDir = existsSync(ours) && statSync(ours).isDirectory();
|
|
140
|
+
const theirsDir = existsSync(theirs) && statSync(theirs).isDirectory();
|
|
141
|
+
if (oursDir || theirsDir) {
|
|
142
|
+
if (oursDir && theirsDir)
|
|
143
|
+
drift.push(...treeDrift(ours, theirs, join(label, name)));
|
|
144
|
+
else
|
|
145
|
+
drift.push({ kind: "changed", path: join(label, name) });
|
|
146
|
+
}
|
|
147
|
+
else if (!existsSync(ours) ||
|
|
148
|
+
!existsSync(theirs) ||
|
|
149
|
+
!readFileSync(ours).equals(readFileSync(theirs))) {
|
|
150
|
+
drift.push({ kind: "changed", path: join(label, name) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return drift;
|
|
154
|
+
}
|
|
89
155
|
if (argv[1] === DRIFT_ENTRY) {
|
|
90
156
|
const drift = scaffoldDrift(loadProject(argv[2] ?? "."));
|
|
91
157
|
if (drift.length) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, 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 { coreCopyDrift } from "./openclaw.js";
|
|
6
|
+
const roots = [];
|
|
7
|
+
function staged() {
|
|
8
|
+
const root = mkdtempSync(join(tmpdir(), "tf-core-copy-"));
|
|
9
|
+
roots.push(root);
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
function put(root, path, content) {
|
|
13
|
+
const file = join(root, path);
|
|
14
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
15
|
+
writeFileSync(file, content);
|
|
16
|
+
}
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const root of roots.splice(0))
|
|
19
|
+
rmSync(root, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
describe("coreCopyDrift", () => {
|
|
22
|
+
it("skips copies without the MATERIALIZED marker (npm-installed cores)", () => {
|
|
23
|
+
const root = staged();
|
|
24
|
+
put(root, "dist/ops.js", "new");
|
|
25
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/dist/ops.js", "old");
|
|
26
|
+
expect(coreCopyDrift(root)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it("skips when the workspace has no dist yet", () => {
|
|
29
|
+
const root = staged();
|
|
30
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/MATERIALIZED", "");
|
|
31
|
+
expect(coreCopyDrift(root)).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
it("passes when the materialized copy matches the dist", () => {
|
|
34
|
+
const root = staged();
|
|
35
|
+
put(root, "dist/ops.js", "same");
|
|
36
|
+
put(root, "dist/deep/command.js", "same");
|
|
37
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/MATERIALIZED", "");
|
|
38
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/dist/ops.js", "same");
|
|
39
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/dist/deep/command.js", "same");
|
|
40
|
+
expect(coreCopyDrift(root)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
it("flags stale, missing, and extra files in the copy", () => {
|
|
43
|
+
const root = staged();
|
|
44
|
+
put(root, "dist/ops.js", "new");
|
|
45
|
+
put(root, "dist/gone.js", "still here");
|
|
46
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/MATERIALIZED", "");
|
|
47
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/dist/ops.js", "old");
|
|
48
|
+
put(root, "hosts/openclaw/node_modules/toolfactory/dist/extra.js", "leftover");
|
|
49
|
+
const paths = coreCopyDrift(root)
|
|
50
|
+
.map((d) => d.path)
|
|
51
|
+
.sort();
|
|
52
|
+
expect(paths).toEqual([
|
|
53
|
+
"hosts/openclaw/node_modules/toolfactory/dist/extra.js",
|
|
54
|
+
"hosts/openclaw/node_modules/toolfactory/dist/gone.js",
|
|
55
|
+
"hosts/openclaw/node_modules/toolfactory/dist/ops.js",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/hosts/web.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Project } from "../model.js";
|
|
2
2
|
export declare const DRIFT_ENTRY: string;
|
|
3
|
+
export declare function differs(label: string, upstream: unknown, pinned: unknown): string[];
|
|
3
4
|
/** Human-readable drift lines; empty means the generators still write what WEB_SCAFFOLD says. */
|
|
4
5
|
export declare function scaffoldDrift(project: Project): string[];
|
package/dist/hosts/web.js
CHANGED
|
@@ -15,6 +15,7 @@ import { tmpdir } from "node:os";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { argv, exit, stderr, stdout } from "node:process";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { inspect, isDeepStrictEqual } from "node:util";
|
|
18
19
|
import { loadProject } from "../project/load.js";
|
|
19
20
|
import { surface, TAILWIND_CSS, WEB_DIR, WEB_SCAFFOLD } from "../surfaces/web.js";
|
|
20
21
|
export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
|
|
@@ -30,10 +31,12 @@ function planned(files, path) {
|
|
|
30
31
|
function parse(text) {
|
|
31
32
|
return JSON.parse(text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""));
|
|
32
33
|
}
|
|
33
|
-
function differs(label, upstream, pinned) {
|
|
34
|
-
return
|
|
34
|
+
export function differs(label, upstream, pinned) {
|
|
35
|
+
return isDeepStrictEqual(upstream, pinned)
|
|
35
36
|
? []
|
|
36
|
-
: [
|
|
37
|
+
: [
|
|
38
|
+
`${label}: upstream ${inspect(upstream, { sorted: true })}, pinned ${inspect(pinned, { sorted: true })}`,
|
|
39
|
+
];
|
|
37
40
|
}
|
|
38
41
|
/** Human-readable drift lines; empty means the generators still write what WEB_SCAFFOLD says. */
|
|
39
42
|
export function scaffoldDrift(project) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { differs } from "./web.js";
|
|
3
|
+
it("compares scaffold objects by values, not key insertion order", () => {
|
|
4
|
+
expect(differs("deps", { a: "1", b: "2" }, { b: "2", a: "1" })).toEqual([]);
|
|
5
|
+
expect(differs("deps", { a: "1" }, { a: "2" })).toHaveLength(1);
|
|
6
|
+
expect(differs("deps", { a: "1" }, { a: "1", b: "2" })).toHaveLength(1);
|
|
7
|
+
expect(differs("array", ["a", "b"], ["b", "a"])).toHaveLength(1);
|
|
8
|
+
});
|
package/dist/model.d.ts
CHANGED
|
@@ -162,6 +162,7 @@ export declare const ToolConfigSchema: z.ZodObject<{
|
|
|
162
162
|
}, z.core.$strip>>>;
|
|
163
163
|
activation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
164
164
|
pluginApi: z.ZodOptional<z.ZodString>;
|
|
165
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
165
166
|
dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
166
167
|
peerDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
167
168
|
devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
@@ -257,6 +258,15 @@ export interface Command {
|
|
|
257
258
|
cwd?: string;
|
|
258
259
|
env?: Record<string, string>;
|
|
259
260
|
}
|
|
261
|
+
/** One `.agents/skills/<name>/` directory a plugin surface ships, read at load time. */
|
|
262
|
+
export interface ProjectSkill {
|
|
263
|
+
name: string;
|
|
264
|
+
/** Files of the skill directory, relative and sorted; content as UTF-8 text. */
|
|
265
|
+
files: {
|
|
266
|
+
path: string;
|
|
267
|
+
content: string;
|
|
268
|
+
}[];
|
|
269
|
+
}
|
|
260
270
|
export interface Project {
|
|
261
271
|
root: string;
|
|
262
272
|
tool: ToolConfig;
|
|
@@ -267,6 +277,8 @@ export interface Project {
|
|
|
267
277
|
toolfactoryVersion: string;
|
|
268
278
|
/** From the root package.json `packageManager` field; npm when absent. */
|
|
269
279
|
packageManager?: PackageManager;
|
|
280
|
+
/** Skills `openclaw.skills` declares, read once so projectors stay pure. */
|
|
281
|
+
skills?: ProjectSkill[];
|
|
270
282
|
}
|
|
271
283
|
export interface Surface {
|
|
272
284
|
id: SurfaceId;
|
package/dist/model.js
CHANGED
|
@@ -176,6 +176,10 @@ export const ToolConfigSchema = z
|
|
|
176
176
|
.string()
|
|
177
177
|
.optional()
|
|
178
178
|
.describe("Plugin API range for compat.pluginApi and the openclaw peer dependency."),
|
|
179
|
+
skills: z
|
|
180
|
+
.array(z.string())
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("Skill directories under `.agents/skills/` that the OpenClaw plugin ships; OpenClaw loads them through the manifest's `skills` roots."),
|
|
179
183
|
dependencies: z.record(z.string(), z.string()).optional(),
|
|
180
184
|
peerDependencies: z.record(z.string(), z.string()).optional(),
|
|
181
185
|
devDependencies: z.record(z.string(), z.string()).optional(),
|
package/dist/ops.d.ts
CHANGED
|
@@ -151,6 +151,32 @@ export declare const operations: (import("./toolfactory/types.js").Operation<z.Z
|
|
|
151
151
|
reason: z.ZodOptional<z.ZodString>;
|
|
152
152
|
}, z.core.$strip>>;
|
|
153
153
|
}, z.core.$strip>>;
|
|
154
|
+
}, z.core.$strip>> | import("./toolfactory/types.js").Operation<z.ZodObject<{
|
|
155
|
+
prompt: z.ZodString;
|
|
156
|
+
reference: z.ZodOptional<z.ZodString>;
|
|
157
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
158
|
+
verdict: z.ZodEnum<{
|
|
159
|
+
pass: "pass";
|
|
160
|
+
polish: "polish";
|
|
161
|
+
revise: "revise";
|
|
162
|
+
}>;
|
|
163
|
+
sections: z.ZodObject<{
|
|
164
|
+
present: z.ZodArray<z.ZodString>;
|
|
165
|
+
missing: z.ZodArray<z.ZodString>;
|
|
166
|
+
}, z.core.$strip>;
|
|
167
|
+
checks: z.ZodArray<z.ZodObject<{
|
|
168
|
+
id: z.ZodString;
|
|
169
|
+
status: z.ZodEnum<{
|
|
170
|
+
fail: "fail";
|
|
171
|
+
pass: "pass";
|
|
172
|
+
warn: "warn";
|
|
173
|
+
}>;
|
|
174
|
+
detail: z.ZodString;
|
|
175
|
+
}, z.core.$strip>>;
|
|
176
|
+
directives: z.ZodArray<z.ZodString>;
|
|
177
|
+
reference: z.ZodOptional<z.ZodObject<{
|
|
178
|
+
sectionsMissingHere: z.ZodArray<z.ZodString>;
|
|
179
|
+
}, z.core.$strip>>;
|
|
154
180
|
}, z.core.$strip>> | import("./toolfactory/types.js").Operation<z.ZodObject<{
|
|
155
181
|
root: z.ZodDefault<z.ZodString>;
|
|
156
182
|
path: z.ZodString;
|
package/dist/ops.js
CHANGED
|
@@ -153,6 +153,30 @@ export const operations = [
|
|
|
153
153
|
annotations: { readOnlyHint: true },
|
|
154
154
|
handler: async ({ root }) => commands.coverage(root),
|
|
155
155
|
}),
|
|
156
|
+
operation({
|
|
157
|
+
name: "review-prompt",
|
|
158
|
+
description: "Evaluate a prompt draft against the codex-prompt-standard rubric: anatomy sections, convention checks, and concrete directives. Draft, run this, fix what it flags, and re-run until it passes.",
|
|
159
|
+
input: z.object({
|
|
160
|
+
prompt: z.string().min(1).describe("The prompt draft to evaluate"),
|
|
161
|
+
reference: z
|
|
162
|
+
.string()
|
|
163
|
+
.optional()
|
|
164
|
+
.describe("The closest reference prompt's full text, fetched per the standard (an OpenAI Codex core prompt); sections it has that the draft lacks become the bar"),
|
|
165
|
+
}),
|
|
166
|
+
output: z.object({
|
|
167
|
+
verdict: z.enum(["pass", "polish", "revise"]),
|
|
168
|
+
sections: z.object({ present: z.array(z.string()), missing: z.array(z.string()) }),
|
|
169
|
+
checks: z.array(z.object({
|
|
170
|
+
id: z.string(),
|
|
171
|
+
status: z.enum(["pass", "warn", "fail"]),
|
|
172
|
+
detail: z.string(),
|
|
173
|
+
})),
|
|
174
|
+
directives: z.array(z.string()),
|
|
175
|
+
reference: z.object({ sectionsMissingHere: z.array(z.string()) }).optional(),
|
|
176
|
+
}),
|
|
177
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
178
|
+
handler: async ({ prompt, reference }) => commands.reviewPrompt({ prompt, reference }),
|
|
179
|
+
}),
|
|
156
180
|
operation({
|
|
157
181
|
name: "adopt",
|
|
158
182
|
description: "Stop regenerating one file; it becomes the author's (recorded as manual in the lock).",
|
|
@@ -208,7 +232,7 @@ export const operations = [
|
|
|
208
232
|
handler: async (args) => commands.bootstrapRepo(args),
|
|
209
233
|
}),
|
|
210
234
|
operation({
|
|
211
|
-
name: "secrets",
|
|
235
|
+
name: "secrets-usage",
|
|
212
236
|
description: "Every credential this project's surfaces need — the tool's own sensitive config keys and the release registries' tokens — with where each one is set, whether it is present locally and on GitHub, and (check) whether the registry accepts it. Never a value.",
|
|
213
237
|
input: z.object({
|
|
214
238
|
root,
|
package/dist/project/gate.js
CHANGED
|
@@ -250,13 +250,19 @@ export function packageSteps(project, options = {}) {
|
|
|
250
250
|
}
|
|
251
251
|
if (has(project, "openclaw-native")) {
|
|
252
252
|
// ClawHub's reusable workflow has no build step of its own, so the tarball it publishes has
|
|
253
|
-
// to arrive already built (`package_artifact_name`).
|
|
253
|
+
// to arrive already built (`package_artifact_name`). Consumers install this tarball outside
|
|
254
|
+
// the monorepo, where the checkout-relative `file:../..` core dependency cannot resolve, so
|
|
255
|
+
// pack against the npm-published core of the same release and restore the workspace manifest.
|
|
254
256
|
steps.push({
|
|
255
257
|
name: "OpenClaw plugin tarball",
|
|
256
258
|
run: [
|
|
257
259
|
`npm --prefix ${OPENCLAW_HOST_DIR} install`,
|
|
258
260
|
`npm --prefix ${OPENCLAW_HOST_DIR} run build`,
|
|
261
|
+
`v=$(node -p "require('./package.json').version")`,
|
|
262
|
+
`cp ${OPENCLAW_HOST_DIR}/package.json ${OPENCLAW_HOST_DIR}/package.json.prepack`,
|
|
263
|
+
`(cd ${OPENCLAW_HOST_DIR} && npm pkg set dependencies.toolfactory=$v)`,
|
|
259
264
|
`npm pack ./${OPENCLAW_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
|
|
265
|
+
`mv ${OPENCLAW_HOST_DIR}/package.json.prepack ${OPENCLAW_HOST_DIR}/package.json`,
|
|
260
266
|
].join(" && "),
|
|
261
267
|
});
|
|
262
268
|
}
|
|
@@ -82,6 +82,7 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
82
82
|
}[] | undefined;
|
|
83
83
|
activation?: Record<string, unknown> | undefined;
|
|
84
84
|
pluginApi?: string | undefined;
|
|
85
|
+
skills?: string[] | undefined;
|
|
85
86
|
dependencies?: Record<string, string> | undefined;
|
|
86
87
|
peerDependencies?: Record<string, string> | undefined;
|
|
87
88
|
devDependencies?: Record<string, string> | undefined;
|
package/dist/project/load.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { 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";
|
|
@@ -62,6 +62,22 @@ function readPackageManager(root) {
|
|
|
62
62
|
}
|
|
63
63
|
return name;
|
|
64
64
|
}
|
|
65
|
+
/** `.agents/skills/<name>/` as a shippable unit: every file, sorted, content in memory. */
|
|
66
|
+
function readProjectSkill(root, name) {
|
|
67
|
+
const directory = join(root, ".agents", "skills", name);
|
|
68
|
+
if (!existsSync(directory)) {
|
|
69
|
+
throw new Error(`openclaw.skills declares "${name}" but ${join(".agents", "skills", name)} does not exist`);
|
|
70
|
+
}
|
|
71
|
+
const files = readdirSync(directory, { recursive: true, withFileTypes: true })
|
|
72
|
+
.filter((entry) => entry.isFile())
|
|
73
|
+
.map((entry) => join(entry.parentPath, entry.name))
|
|
74
|
+
.sort()
|
|
75
|
+
.map((absolute) => ({
|
|
76
|
+
path: relative(directory, absolute).replaceAll(sep, "/"),
|
|
77
|
+
content: readFileSync(absolute, "utf8"),
|
|
78
|
+
}));
|
|
79
|
+
return { name, files };
|
|
80
|
+
}
|
|
65
81
|
export function loadProject(rootInput = ".") {
|
|
66
82
|
const root = resolve(rootInput);
|
|
67
83
|
const toolPath = join(root, TOOL_PATH);
|
|
@@ -76,6 +92,7 @@ export function loadProject(rootInput = ".") {
|
|
|
76
92
|
identity: identityFile.identity,
|
|
77
93
|
identityExtra: identityFile.extra,
|
|
78
94
|
operations: isInstructionOnly(tool) ? [] : readOps(root),
|
|
95
|
+
skills: (tool.openclaw?.skills ?? []).map((name) => readProjectSkill(root, name)),
|
|
79
96
|
toolfactoryVersion: TOOLFACTORY_VERSION,
|
|
80
97
|
packageManager: readPackageManager(root),
|
|
81
98
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface PromptReviewInput {
|
|
2
|
+
prompt: string;
|
|
3
|
+
/** The closest reference prompt's full text (canon §1: fetch it before evaluating). */
|
|
4
|
+
reference?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CheckResult {
|
|
7
|
+
id: string;
|
|
8
|
+
status: "pass" | "warn" | "fail";
|
|
9
|
+
detail: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PromptReviewReport {
|
|
12
|
+
verdict: "pass" | "polish" | "revise";
|
|
13
|
+
sections: {
|
|
14
|
+
present: string[];
|
|
15
|
+
missing: string[];
|
|
16
|
+
};
|
|
17
|
+
checks: CheckResult[];
|
|
18
|
+
directives: string[];
|
|
19
|
+
reference?: {
|
|
20
|
+
sectionsMissingHere: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function reviewPrompt(input: PromptReviewInput): PromptReviewReport;
|