harness-alchemist 0.1.0 → 0.1.2
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/.claude-plugin/plugin.json +3 -3
- package/.codex-plugin/plugin.json +4 -4
- package/dist/deepseek.d.ts +2 -1
- package/dist/deepseek.d.ts.map +1 -1
- package/dist/deepseek.js +23 -2
- package/dist/deepseek.js.map +1 -1
- package/dist/opencode.d.ts +19 -1
- package/dist/opencode.d.ts.map +1 -1
- package/dist/opencode.js +54 -1
- package/dist/opencode.js.map +1 -1
- package/lib/validate.mjs +122 -5
- package/package.json +5 -3
- package/skills/harness-alchemist/SKILL.md +9 -0
- package/skills/harness-alchemist/references/compatibility.md +9 -0
- package/skills/harness-alchemist/scripts/main.mjs +22 -0
- package/skills/harness-alchemist/scripts/main.py +28 -0
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/SKILL.md.tpl +5 -2
- package/templates/v0.1.0/universal-typescript/.agents/skills/develop-template/references/compatibility.md.tpl +29 -0
- package/templates/v0.1.0/universal-typescript/package.json.tpl +1 -0
- package/templates/v0.1.0/universal-typescript/skills/shared-skill/SKILL.md.tpl +18 -4
- package/templates/v0.1.0/universal-typescript/skills/shared-skill/references/tool-contract.md.tpl +35 -0
- package/templates/v0.1.0/universal-typescript/skills/shared-skill/scripts/main.mjs.tpl +22 -0
- package/templates/v0.1.0/universal-typescript/skills/shared-skill/scripts/main.py.tpl +28 -0
- package/templates/v0.1.0/universal-typescript/src/deepseek.ts.tpl +31 -2
- package/templates/v0.1.0/universal-typescript/src/opencode.ts.tpl +61 -1
- package/templates/v0.1.0/universal-typescript/tests/runtimes.test.mjs.tpl +35 -2
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "harness-alchemist",
|
|
4
4
|
"displayName": "Harness Alchemist",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Haochuan Zhang"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://github.com/lunarmoon26/harness-alchemist",
|
|
11
|
-
"repository": "https://github.com/lunarmoon26/harness-alchemist",
|
|
10
|
+
"homepage": "git+https://github.com/lunarmoon26/harness-alchemist.git",
|
|
11
|
+
"repository": "git+https://github.com/lunarmoon26/harness-alchemist.git",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"skills": "./skills/"
|
|
14
14
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-alchemist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Haochuan Zhang",
|
|
7
|
-
"url": "https://github.com/lunarmoon26/harness-alchemist"
|
|
7
|
+
"url": "git+https://github.com/lunarmoon26/harness-alchemist.git"
|
|
8
8
|
},
|
|
9
|
-
"homepage": "https://github.com/lunarmoon26/harness-alchemist",
|
|
10
|
-
"repository": "https://github.com/lunarmoon26/harness-alchemist",
|
|
9
|
+
"homepage": "git+https://github.com/lunarmoon26/harness-alchemist.git",
|
|
10
|
+
"repository": "git+https://github.com/lunarmoon26/harness-alchemist.git",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"skills": "./skills/"
|
|
13
13
|
}
|
package/dist/deepseek.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
2
2
|
export declare const name = "harness-alchemist";
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function runSkillScript(script: string, payload: string): unknown;
|
|
4
|
+
export declare function apply(context: Context): void;
|
|
4
5
|
//# sourceMappingURL=deepseek.d.ts.map
|
package/dist/deepseek.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAQvC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAcvE;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAK5C"}
|
package/dist/deepseek.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
export const name = "harness-alchemist";
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
const scriptsDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "skills/harness-alchemist/scripts");
|
|
6
|
+
export function runSkillScript(script, payload) {
|
|
7
|
+
const runtime = script.endsWith(".py") ? "python3" : process.execPath;
|
|
8
|
+
const result = spawnSync(runtime, [join(scriptsDirectory, script)], {
|
|
9
|
+
input: payload,
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
});
|
|
12
|
+
if (result.error) {
|
|
13
|
+
throw new Error(`Could not run ${script} via ${runtime}: ${result.error.message}`);
|
|
14
|
+
}
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
const diagnostic = result.stderr.trim() || `${script} exited with code ${result.status}`;
|
|
17
|
+
throw new Error(diagnostic);
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(result.stdout);
|
|
20
|
+
}
|
|
21
|
+
export function apply(context) {
|
|
22
|
+
context.provide(name, {
|
|
23
|
+
run: (payload, runtime = "node") => runSkillScript(runtime === "python" ? "main.py" : "main.mjs", payload),
|
|
24
|
+
});
|
|
4
25
|
}
|
|
5
26
|
//# sourceMappingURL=deepseek.js.map
|
package/dist/deepseek.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deepseek.js","sourceRoot":"","sources":["../src/deepseek.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAIxC,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,gBAAgB,GAAG,IAAI,CAC3B,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,IAAI,EACJ,kCAAkC,CACnC,CAAA;AAED,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,OAAe;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAA;IACrE,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,EAAE;QAClE,KAAK,EAAE,OAAO;QACd,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,iBAAiB,MAAM,QAAQ,OAAO,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,qBAAqB,MAAM,CAAC,MAAM,EAAE,CAAA;QACxF,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAA;IAC7B,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,OAAgB;IACpC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE;QACpB,GAAG,EAAE,CAAC,OAAe,EAAE,UAA6B,MAAM,EAAE,EAAE,CAC5D,cAAc,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC;KACzE,CAAC,CAAA;AACJ,CAAC"}
|
package/dist/opencode.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
declare
|
|
1
|
+
export declare function runSkillScript(script: string, payload: string): Promise<string>;
|
|
2
|
+
declare const plugin: () => Promise<{
|
|
3
|
+
tool: {
|
|
4
|
+
harness_alchemist_run: {
|
|
5
|
+
description: string;
|
|
6
|
+
args: {
|
|
7
|
+
input: import("zod").ZodString;
|
|
8
|
+
runtime: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
9
|
+
node: "node";
|
|
10
|
+
python: "python";
|
|
11
|
+
}>>;
|
|
12
|
+
};
|
|
13
|
+
execute(args: {
|
|
14
|
+
input: string;
|
|
15
|
+
runtime?: "node" | "python" | undefined;
|
|
16
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
2
20
|
export default plugin;
|
|
3
21
|
//# sourceMappingURL=opencode.d.ts.map
|
package/dist/opencode.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"AAaA,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B/E;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;EAuBO,CAAA;AAEnB,eAAe,MAAM,CAAA"}
|
package/dist/opencode.js
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
const scriptsDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "skills/harness-alchemist/scripts");
|
|
6
|
+
export function runSkillScript(script, payload) {
|
|
7
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
8
|
+
const runtime = script.endsWith(".py") ? "python3" : process.execPath;
|
|
9
|
+
const child = spawn(runtime, [join(scriptsDirectory, script)], {
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
});
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
child.stdout.on("data", (chunk) => {
|
|
15
|
+
stdout += chunk;
|
|
16
|
+
});
|
|
17
|
+
child.stderr.on("data", (chunk) => {
|
|
18
|
+
stderr += chunk;
|
|
19
|
+
});
|
|
20
|
+
child.on("error", (error) => {
|
|
21
|
+
rejectResult(new Error(`Could not run ${script} via ${runtime}: ${error.message}`));
|
|
22
|
+
});
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
if (code !== 0) {
|
|
25
|
+
rejectResult(new Error(stderr.trim() || `${script} exited with code ${code}`));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
resolveResult(stdout);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
child.stdin.end(payload);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
1
34
|
const plugin = (async () => {
|
|
2
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
tool: {
|
|
37
|
+
harness_alchemist_run: tool({
|
|
38
|
+
description: "Run the Harness Alchemist shared-skill workflow. Delegates to skills/harness-alchemist/scripts.",
|
|
39
|
+
args: {
|
|
40
|
+
input: tool.schema
|
|
41
|
+
.string()
|
|
42
|
+
.describe("JSON object passed on stdin to the skill entrypoint"),
|
|
43
|
+
runtime: tool.schema
|
|
44
|
+
.enum(["node", "python"])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Entrypoint runtime; defaults to node"),
|
|
47
|
+
},
|
|
48
|
+
execute: async (args) => {
|
|
49
|
+
const script = args.runtime === "python" ? "main.py" : "main.mjs";
|
|
50
|
+
const output = await runSkillScript(script, args.input);
|
|
51
|
+
return JSON.stringify(JSON.parse(output));
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
3
56
|
});
|
|
4
57
|
export default plugin;
|
|
5
58
|
//# sourceMappingURL=opencode.js.map
|
package/dist/opencode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../src/opencode.ts"],"names":[],"mappings":"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"}
|
package/lib/validate.mjs
CHANGED
|
@@ -31,6 +31,11 @@ Validates a universal Claude, Codex, OpenCode, Antigravity, and DeepSeek
|
|
|
31
31
|
plugin scaffold. By default the project is resolved from the current working
|
|
32
32
|
directory or from the script's containing generated project.
|
|
33
33
|
|
|
34
|
+
Product skills are checked against the Agent Skills specification and their
|
|
35
|
+
scripts against the shared tool contract. When the optional 'pyodide'
|
|
36
|
+
devDependency is installed, Python entrypoints are also syntax-checked and
|
|
37
|
+
smoke-executed in a WebAssembly CPython sandbox.
|
|
38
|
+
|
|
34
39
|
Options:
|
|
35
40
|
--external Run installed platform validators, currently Claude Code.
|
|
36
41
|
--json Print a machine-readable result.
|
|
@@ -230,6 +235,110 @@ function exportTarget(exports, key, field = "import") {
|
|
|
230
235
|
return undefined
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
const PYTHON_SMOKE_PROGRAM = `
|
|
239
|
+
import contextlib
|
|
240
|
+
import io
|
|
241
|
+
import json
|
|
242
|
+
import sys
|
|
243
|
+
|
|
244
|
+
sys.stdin = io.StringIO(__ha_payload__)
|
|
245
|
+
_buffer = io.StringIO()
|
|
246
|
+
try:
|
|
247
|
+
with contextlib.redirect_stdout(_buffer):
|
|
248
|
+
exec(compile(__ha_source__, "<skill>", "exec"), {"__name__": "__main__"})
|
|
249
|
+
except SystemExit as exit_code:
|
|
250
|
+
if exit_code.code not in (0, None):
|
|
251
|
+
raise RuntimeError(f"script exited with code {exit_code.code}")
|
|
252
|
+
_buffer.getvalue()
|
|
253
|
+
`
|
|
254
|
+
|
|
255
|
+
async function smokePythonScripts(scripts, errors, warnings) {
|
|
256
|
+
let loadPyodide
|
|
257
|
+
try {
|
|
258
|
+
;({ loadPyodide } = await import("pyodide"))
|
|
259
|
+
} catch {
|
|
260
|
+
warnings.push("pyodide is not installed; skipped WebAssembly Python validation")
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let instance
|
|
265
|
+
try {
|
|
266
|
+
instance = await loadPyodide()
|
|
267
|
+
} catch (error) {
|
|
268
|
+
warnings.push(`Pyodide failed to initialize (${error.message}); skipped WebAssembly Python validation`)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const script of scripts) {
|
|
273
|
+
const source = await readFile(script, "utf8")
|
|
274
|
+
instance.globals.set("__ha_source__", source)
|
|
275
|
+
instance.globals.set("__ha_payload__", "{}")
|
|
276
|
+
let output
|
|
277
|
+
try {
|
|
278
|
+
output = instance.runPython(PYTHON_SMOKE_PROGRAM)
|
|
279
|
+
} catch (error) {
|
|
280
|
+
errors.push(`${script}: WebAssembly Python check failed (${String(error).split("\n").at(-2) ?? String(error)})`)
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(output)
|
|
285
|
+
if (parsed?.ok !== true || typeof parsed.plugin !== "string") {
|
|
286
|
+
errors.push(`${script}: unexpected tool-contract result ${output.trim()}`)
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
errors.push(`${script}: did not print a JSON object per the tool contract`)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
instance.globals.delete("__ha_source__")
|
|
294
|
+
instance.globals.delete("__ha_payload__")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts) {
|
|
298
|
+
if ((frontmatter.description?.length ?? 0) > 1024) {
|
|
299
|
+
errors.push(`${skillFile}: description must be at most 1024 characters`)
|
|
300
|
+
}
|
|
301
|
+
if ((frontmatter.compatibility?.length ?? 0) > 500) {
|
|
302
|
+
errors.push(`${skillFile}: compatibility must be at most 500 characters`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const skillDirectory = dirname(skillFile)
|
|
306
|
+
const references = new Set(
|
|
307
|
+
[...content.matchAll(/(?:scripts|references|assets)\/[A-Za-z0-9][A-Za-z0-9._/-]*/g)].map(
|
|
308
|
+
(match) => match[0].replace(/[.,;:)\]]+$/, ""),
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
for (const reference of references) {
|
|
312
|
+
if (!existsSync(join(skillDirectory, ...reference.split("/")))) {
|
|
313
|
+
errors.push(`${skillFile}: referenced path '${reference}' does not exist`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const scriptsDirectory = join(skillDirectory, "scripts")
|
|
318
|
+
if (!existsSync(scriptsDirectory)) return
|
|
319
|
+
|
|
320
|
+
const entries = await readdir(scriptsDirectory)
|
|
321
|
+
const pythonBases = new Set(
|
|
322
|
+
entries.filter((entry) => entry.endsWith(".py")).map((entry) => entry.slice(0, -3)),
|
|
323
|
+
)
|
|
324
|
+
const mjsBases = new Set(
|
|
325
|
+
entries.filter((entry) => entry.endsWith(".mjs")).map((entry) => entry.slice(0, -4)),
|
|
326
|
+
)
|
|
327
|
+
for (const base of pythonBases) {
|
|
328
|
+
if (!mjsBases.has(base)) {
|
|
329
|
+
errors.push(`${skillFile}: scripts/${base}.py is missing its scripts/${base}.mjs twin`)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const base of mjsBases) {
|
|
333
|
+
if (!pythonBases.has(base)) {
|
|
334
|
+
errors.push(`${skillFile}: scripts/${base}.mjs is missing its scripts/${base}.py twin`)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const entry of entries.filter((entry) => entry.endsWith(".py"))) {
|
|
338
|
+
pythonScripts.add(join(scriptsDirectory, entry))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
233
342
|
export async function validateProject(projectRoot, options = {}) {
|
|
234
343
|
const root = resolve(projectRoot)
|
|
235
344
|
const errors = []
|
|
@@ -350,14 +459,15 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
350
459
|
errors.push(`cordis.patch.yml entry '${pluginName}' must load '${expectedModule}'`)
|
|
351
460
|
}
|
|
352
461
|
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
]
|
|
462
|
+
const maintenanceSkillFiles = await collectSkillFiles(root, ".agents/skills")
|
|
463
|
+
const productSkillFiles = await collectSkillFiles(root, "skills")
|
|
464
|
+
const skillFiles = [...productSkillFiles, ...maintenanceSkillFiles]
|
|
357
465
|
if (skillFiles.length < 2) errors.push("Expected a shared skill and a project development skill")
|
|
358
466
|
|
|
467
|
+
const pythonScripts = new Set()
|
|
359
468
|
for (const skillFile of skillFiles) {
|
|
360
|
-
const
|
|
469
|
+
const content = await readFile(skillFile, "utf8")
|
|
470
|
+
const frontmatter = parseFrontmatter(content)
|
|
361
471
|
const directoryName = basename(dirname(skillFile))
|
|
362
472
|
if (!frontmatter) errors.push(`${skillFile}: missing YAML frontmatter`)
|
|
363
473
|
else {
|
|
@@ -368,9 +478,16 @@ export async function validateProject(projectRoot, options = {}) {
|
|
|
368
478
|
if (!validName.test(frontmatter.name ?? "") || (frontmatter.name?.length ?? 0) > 64) {
|
|
369
479
|
errors.push(`${skillFile}: invalid Agent Skill name`)
|
|
370
480
|
}
|
|
481
|
+
if (productSkillFiles.includes(skillFile)) {
|
|
482
|
+
await checkProductSkillRuntime(skillFile, content, frontmatter, errors, pythonScripts)
|
|
483
|
+
}
|
|
371
484
|
}
|
|
372
485
|
}
|
|
373
486
|
|
|
487
|
+
if (pythonScripts.size > 0) {
|
|
488
|
+
await smokePythonScripts([...pythonScripts], errors, warnings)
|
|
489
|
+
}
|
|
490
|
+
|
|
374
491
|
for (const path of await scanForTokens(root)) {
|
|
375
492
|
errors.push(`${path}: unresolved scaffold token`)
|
|
376
493
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-alchemist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Scaffold, validate, and publish portable coding-agent plugins across Claude Code, Codex, OpenCode, Antigravity, and DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"harness-alchemist": "
|
|
7
|
+
"harness-alchemist": "bin/harness-alchemist.mjs"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/opencode.js",
|
|
10
10
|
"types": "./dist/opencode.d.ts",
|
|
@@ -70,13 +70,15 @@
|
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
72
72
|
"@opencode-ai/plugin": "^1.18.21",
|
|
73
|
+
"@types/node": "^26.2.0",
|
|
74
|
+
"pyodide": "^314.0.5",
|
|
73
75
|
"typescript": "^5.9.3"
|
|
74
76
|
},
|
|
75
77
|
"author": "Haochuan Zhang",
|
|
76
78
|
"license": "MIT",
|
|
77
79
|
"repository": {
|
|
78
80
|
"type": "git",
|
|
79
|
-
"url": "https://github.com/lunarmoon26/harness-alchemist"
|
|
81
|
+
"url": "git+https://github.com/lunarmoon26/harness-alchemist.git"
|
|
80
82
|
},
|
|
81
83
|
"dsh": {
|
|
82
84
|
"bundle": {
|
|
@@ -56,3 +56,12 @@ npm pack --dry-run
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Static validation is structural evidence, not proof that remote services or host-specific lifecycle events work.
|
|
59
|
+
|
|
60
|
+
## Skill Runtime
|
|
61
|
+
|
|
62
|
+
`scripts/main.mjs` (Node/Bun) and `scripts/main.py` (CPython 3.10+) are
|
|
63
|
+
behavioral twins demonstrating this repository's own tool contract: one JSON
|
|
64
|
+
object on stdin, one JSON result plus newline on stdout, non-zero exit with a
|
|
65
|
+
stderr diagnostic on failure. The `src/` adapters delegate to them; keep any
|
|
66
|
+
contract change mirrored in both twins and in
|
|
67
|
+
[references/compatibility.md](references/compatibility.md).
|
|
@@ -12,6 +12,9 @@ project/
|
|
|
12
12
|
├── .claude-plugin/plugin.json
|
|
13
13
|
├── .codex-plugin/plugin.json
|
|
14
14
|
├── skills/<name>/SKILL.md
|
|
15
|
+
├── skills/<name>/scripts/main.mjs
|
|
16
|
+
├── skills/<name>/scripts/main.py
|
|
17
|
+
├── skills/<name>/references/tool-contract.md
|
|
15
18
|
├── src/opencode.ts
|
|
16
19
|
├── src/deepseek.ts
|
|
17
20
|
├── cordis.patch.yml
|
|
@@ -19,6 +22,12 @@ project/
|
|
|
19
22
|
└── package.json
|
|
20
23
|
```
|
|
21
24
|
|
|
25
|
+
Product skills own their runtime: `skills/<name>/scripts/` holds behavioral
|
|
26
|
+
`.mjs`/`.py` twins (one JSON object in on stdin, one JSON result out on
|
|
27
|
+
stdout), and `src/opencode.ts` and `src/deepseek.ts` stay thin adapters that
|
|
28
|
+
spawn those scripts. See the generated project's tool contract reference for
|
|
29
|
+
details.
|
|
30
|
+
|
|
22
31
|
## Capability Matrix
|
|
23
32
|
|
|
24
33
|
| Artifact | Claude | Codex | OpenCode | Antigravity | DeepSeek |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Harness Alchemist shared-skill entrypoint (Node/Bun twin of main.py).
|
|
3
|
+
// Reads one JSON payload from stdin and writes one JSON result to stdout.
|
|
4
|
+
// See references/compatibility.md for the full contract.
|
|
5
|
+
|
|
6
|
+
const chunks = []
|
|
7
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
8
|
+
|
|
9
|
+
let request
|
|
10
|
+
try {
|
|
11
|
+
request = JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`Invalid JSON payload: ${error.message}`)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
18
|
+
console.error("Payload must be a JSON object")
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.stdout.write(`${JSON.stringify({ ok: true, plugin: "harness-alchemist", echo: request })}\n`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Harness Alchemist shared-skill entrypoint (Python twin of main.mjs).
|
|
3
|
+
|
|
4
|
+
Reads one JSON payload from stdin and writes one JSON result to stdout.
|
|
5
|
+
See references/compatibility.md for the full contract.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
try:
|
|
14
|
+
request = json.load(sys.stdin)
|
|
15
|
+
except json.JSONDecodeError as error:
|
|
16
|
+
print(f"Invalid JSON payload: {error}", file=sys.stderr)
|
|
17
|
+
return 1
|
|
18
|
+
|
|
19
|
+
if not isinstance(request, dict):
|
|
20
|
+
print("Payload must be a JSON object", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
print(json.dumps({"ok": True, "plugin": "harness-alchemist", "echo": request}))
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
sys.exit(main())
|
|
@@ -13,6 +13,8 @@ describe development of the Harness Alchemist CLI or its canonical templates.
|
|
|
13
13
|
|
|
14
14
|
- `skills/{{NAME}}/SKILL.md` is the plugin's end-user workflow. Replace the
|
|
15
15
|
starter procedure with the capability's domain-specific instructions.
|
|
16
|
+
- `skills/{{NAME}}/scripts/` owns the skill runtime as `.mjs`/`.py` twins;
|
|
17
|
+
see `skills/{{NAME}}/references/tool-contract.md` before changing them.
|
|
16
18
|
- `.agents/skills/develop-{{NAME}}/` is this repository's maintenance guidance
|
|
17
19
|
and validation tooling.
|
|
18
20
|
- `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, root `plugin.json`,
|
|
@@ -24,8 +26,9 @@ describe development of the Harness Alchemist CLI or its canonical templates.
|
|
|
24
26
|
## Maintenance
|
|
25
27
|
|
|
26
28
|
1. Read `references/compatibility.md` before changing manifests or entrypoints.
|
|
27
|
-
2. Put portable end-user workflows in `skills/` and
|
|
28
|
-
|
|
29
|
+
2. Put portable end-user workflows and skill-owned scripts in `skills/` and
|
|
30
|
+
host-specific runtime code, kept to thin delegation adapters, in its
|
|
31
|
+
corresponding entrypoint.
|
|
29
32
|
3. Run `npm run verify` before reporting completion. Before publishing, inspect
|
|
30
33
|
`npm pack --dry-run` and run `claude plugin validate . --strict` when Claude
|
|
31
34
|
Code is installed.
|
|
@@ -16,6 +16,35 @@
|
|
|
16
16
|
| Antigravity | `plugin.json` | Skills use nested `<name>/SKILL.md`. |
|
|
17
17
|
| DeepSeek | `src/deepseek.ts`, `cordis.patch.yml` | Function plugin uses named exports and no default export. |
|
|
18
18
|
|
|
19
|
+
## Skill Script Contract
|
|
20
|
+
|
|
21
|
+
- `skills/{{NAME}}/scripts/` owns the skill runtime. Entrypoints come in
|
|
22
|
+
behavioral `.mjs` and `.py` twins with identical base names.
|
|
23
|
+
- Contract: one JSON object on stdin, one JSON result plus newline on stdout,
|
|
24
|
+
non-zero exit with a stderr diagnostic on failure. See
|
|
25
|
+
`skills/{{NAME}}/references/tool-contract.md`.
|
|
26
|
+
- `src/opencode.ts` and `src/deepseek.ts` are thin adapters that spawn these
|
|
27
|
+
scripts. Workflow logic never lives in the adapters.
|
|
28
|
+
- Maintenance scripts under `.agents/skills/develop-{{NAME}}/scripts/` are
|
|
29
|
+
maintainer tooling and are exempt from the twin rule.
|
|
30
|
+
|
|
31
|
+
## Validation Tiers
|
|
32
|
+
|
|
33
|
+
- Tier B (always): Agent Skills frontmatter compliance, SKILL.md relative-path
|
|
34
|
+
resolution, and twin parity for product skills.
|
|
35
|
+
- Tier A (when the optional `pyodide` package is installed locally): Python
|
|
36
|
+
entrypoints are additionally compiled and smoke-executed in a WebAssembly
|
|
37
|
+
CPython sandbox without requiring native Python. This repository does not
|
|
38
|
+
declare pyodide as a dependency; install it ad hoc for deeper checks.
|
|
39
|
+
|
|
40
|
+
## DeepSeek Harness Volatility
|
|
41
|
+
|
|
42
|
+
DeepSeek Harness (`dsh`) is a developer preview built on the Cordis kernel.
|
|
43
|
+
The generated contract pins only the verified surface: function-form named-
|
|
44
|
+
export plugins, `cordis.patch.yml` insert entries resolving npm module names,
|
|
45
|
+
and the `dsh.bundle.patch` package field. Re-verify against official dsh docs
|
|
46
|
+
before adopting deeper Cordis services.
|
|
47
|
+
|
|
19
48
|
## Metadata
|
|
20
49
|
|
|
21
50
|
`package.json` owns version, description, author, repository, license, and npm package name. `scripts/sync-metadata.mjs` propagates these values without replacing harness-specific fields.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: {{NAME}}
|
|
3
3
|
description: {{SHARED_SKILL_DESCRIPTION_JSON}}
|
|
4
|
+
compatibility: Requires Node.js 22+ or Bun 1.2+ for scripts/main.mjs; optional Python 3.10+ for scripts/main.py.
|
|
5
|
+
metadata:
|
|
6
|
+
plugin: {{NAME}}
|
|
4
7
|
---
|
|
5
8
|
|
|
6
9
|
# {{DISPLAY_NAME}}
|
|
@@ -10,9 +13,20 @@ Apply the {{DISPLAY_NAME}} workflow to the user's request.
|
|
|
10
13
|
## Workflow
|
|
11
14
|
|
|
12
15
|
1. Confirm the requested outcome and inspect the relevant project context.
|
|
13
|
-
2.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
2. Run the skill entrypoint to perform the smallest complete change:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
echo '{"request": "..."}' | scripts/main.mjs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
When Python 3.10+ is available, `scripts/main.py` behaves identically.
|
|
23
|
+
3. Interpret the JSON result; both entrypoints return `{"ok": true, ...}` on
|
|
24
|
+
success and exit non-zero with a stderr diagnostic on failure.
|
|
25
|
+
4. Use harness-provided tools only when they are needed for the workflow.
|
|
26
|
+
5. Verify the result with the project's available checks.
|
|
27
|
+
6. Report the outcome and any unresolved external dependency.
|
|
28
|
+
|
|
29
|
+
See [the tool contract](references/tool-contract.md) before changing or adding
|
|
30
|
+
entrypoints, and keep the `.mjs` and `.py` twins behaviorally identical.
|
|
17
31
|
|
|
18
32
|
Replace this starter workflow with the plugin's domain-specific procedure as the capability develops.
|
package/templates/v0.1.0/universal-typescript/skills/shared-skill/references/tool-contract.md.tpl
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# {{DISPLAY_NAME}} Tool Contract
|
|
2
|
+
|
|
3
|
+
The shared skill owns its runtime. Harness entrypoints are thin adapters that
|
|
4
|
+
delegate to the scripts in this directory; they must not contain workflow logic.
|
|
5
|
+
|
|
6
|
+
## Entrypoints
|
|
7
|
+
|
|
8
|
+
| File | Runtime | Requirement |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `scripts/main.mjs` | Node.js 22+ or Bun 1.2+ | Zero npm dependencies |
|
|
11
|
+
| `scripts/main.py` | CPython 3.10+ | Standard library only |
|
|
12
|
+
|
|
13
|
+
`main.mjs` and `main.py` are behavioral twins. Any change to one must be
|
|
14
|
+
mirrored in the other, and both must keep the same name so validators can pair
|
|
15
|
+
them.
|
|
16
|
+
|
|
17
|
+
## I/O Contract
|
|
18
|
+
|
|
19
|
+
1. Read exactly one JSON object from stdin.
|
|
20
|
+
2. Write exactly one JSON result followed by a newline to stdout.
|
|
21
|
+
3. On success: `{"ok": true, "plugin": "<plugin-name>", ...}` with exit code 0.
|
|
22
|
+
4. On failure: write a diagnostic to stderr, write nothing to stdout, and exit
|
|
23
|
+
with a non-zero code.
|
|
24
|
+
|
|
25
|
+
## Delegation Rules
|
|
26
|
+
|
|
27
|
+
- Claude Code, Codex, and Antigravity agents invoke these scripts directly via
|
|
28
|
+
a shell, guided by `SKILL.md`.
|
|
29
|
+
- The OpenCode adapter (`src/opencode.ts`) registers a tool that spawns the
|
|
30
|
+
scripts and returns their JSON output.
|
|
31
|
+
- The DeepSeek/Cordis adapter (`src/deepseek.ts`) provides a service that
|
|
32
|
+
spawns the scripts; `cordis.patch.yml` loads it from the published package.
|
|
33
|
+
|
|
34
|
+
Prefer `.mjs` when only a JavaScript runtime is guaranteed; use `.py` when
|
|
35
|
+
Python is available or the workflow needs Python-only libraries.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// {{DISPLAY_NAME}} shared-skill entrypoint (Node/Bun twin of main.py).
|
|
3
|
+
// Reads one JSON payload from stdin and writes one JSON result to stdout.
|
|
4
|
+
// See references/tool-contract.md for the full contract.
|
|
5
|
+
|
|
6
|
+
const chunks = []
|
|
7
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
8
|
+
|
|
9
|
+
let request
|
|
10
|
+
try {
|
|
11
|
+
request = JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(`Invalid JSON payload: ${error.message}`)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
18
|
+
console.error("Payload must be a JSON object")
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.stdout.write(`${JSON.stringify({ ok: true, plugin: "{{NAME}}", echo: request })}\n`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""{{DISPLAY_NAME}} shared-skill entrypoint (Python twin of main.mjs).
|
|
3
|
+
|
|
4
|
+
Reads one JSON payload from stdin and writes one JSON result to stdout.
|
|
5
|
+
See references/tool-contract.md for the full contract.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
try:
|
|
14
|
+
request = json.load(sys.stdin)
|
|
15
|
+
except json.JSONDecodeError as error:
|
|
16
|
+
print(f"Invalid JSON payload: {error}", file=sys.stderr)
|
|
17
|
+
return 1
|
|
18
|
+
|
|
19
|
+
if not isinstance(request, dict):
|
|
20
|
+
print("Payload must be a JSON object", file=sys.stderr)
|
|
21
|
+
return 1
|
|
22
|
+
|
|
23
|
+
print(json.dumps({"ok": True, "plugin": "{{NAME}}", "echo": request}))
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
sys.exit(main())
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
1
5
|
import type { Context } from "@deepseek-ai/cordis"
|
|
2
6
|
|
|
3
7
|
export const name = {{NAME_JSON}}
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const scriptsDirectory = join(
|
|
10
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
11
|
+
"..",
|
|
12
|
+
"skills/{{NAME}}/scripts",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export function runSkillScript(script: string, payload: string): unknown {
|
|
16
|
+
const runtime = script.endsWith(".py") ? "python3" : process.execPath
|
|
17
|
+
const result = spawnSync(runtime, [join(scriptsDirectory, script)], {
|
|
18
|
+
input: payload,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
})
|
|
21
|
+
if (result.error) {
|
|
22
|
+
throw new Error(`Could not run ${script} via ${runtime}: ${result.error.message}`)
|
|
23
|
+
}
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
const diagnostic = result.stderr.trim() || `${script} exited with code ${result.status}`
|
|
26
|
+
throw new Error(diagnostic)
|
|
27
|
+
}
|
|
28
|
+
return JSON.parse(result.stdout)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function apply(context: Context): void {
|
|
32
|
+
context.provide(name, {
|
|
33
|
+
run: (payload: string, runtime: "node" | "python" = "node") =>
|
|
34
|
+
runSkillScript(runtime === "python" ? "main.py" : "main.mjs", payload),
|
|
35
|
+
})
|
|
7
36
|
}
|
|
@@ -1,7 +1,67 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
import { tool } from "@opencode-ai/plugin"
|
|
1
6
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
7
|
|
|
8
|
+
const scriptsDirectory = join(
|
|
9
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
"..",
|
|
11
|
+
"skills/{{NAME}}/scripts",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export function runSkillScript(script: string, payload: string): Promise<string> {
|
|
15
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
16
|
+
const runtime = script.endsWith(".py") ? "python3" : process.execPath
|
|
17
|
+
const child = spawn(runtime, [join(scriptsDirectory, script)], {
|
|
18
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
+
})
|
|
20
|
+
let stdout = ""
|
|
21
|
+
let stderr = ""
|
|
22
|
+
child.stdout.on("data", (chunk) => {
|
|
23
|
+
stdout += chunk
|
|
24
|
+
})
|
|
25
|
+
child.stderr.on("data", (chunk) => {
|
|
26
|
+
stderr += chunk
|
|
27
|
+
})
|
|
28
|
+
child.on("error", (error) => {
|
|
29
|
+
rejectResult(new Error(`Could not run ${script} via ${runtime}: ${error.message}`))
|
|
30
|
+
})
|
|
31
|
+
child.on("close", (code) => {
|
|
32
|
+
if (code !== 0) {
|
|
33
|
+
rejectResult(new Error(stderr.trim() || `${script} exited with code ${code}`))
|
|
34
|
+
} else {
|
|
35
|
+
resolveResult(stdout)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
child.stdin.end(payload)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
const plugin = (async () => {
|
|
4
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
tool: {
|
|
45
|
+
"{{NAME}}_run": tool({
|
|
46
|
+
description:
|
|
47
|
+
"Run the {{DISPLAY_NAME}} shared-skill workflow. Delegates to skills/{{NAME}}/scripts.",
|
|
48
|
+
args: {
|
|
49
|
+
input: tool.schema
|
|
50
|
+
.string()
|
|
51
|
+
.describe("JSON object passed on stdin to the skill entrypoint"),
|
|
52
|
+
runtime: tool.schema
|
|
53
|
+
.enum(["node", "python"])
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Entrypoint runtime; defaults to node"),
|
|
56
|
+
},
|
|
57
|
+
execute: async (args) => {
|
|
58
|
+
const script = args.runtime === "python" ? "main.py" : "main.mjs"
|
|
59
|
+
const output = await runSkillScript(script, args.input)
|
|
60
|
+
return JSON.stringify(JSON.parse(output))
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
}
|
|
5
65
|
}) satisfies Plugin
|
|
6
66
|
|
|
7
67
|
export default plugin
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import assert from "node:assert/strict"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
2
3
|
import test from "node:test"
|
|
3
4
|
|
|
4
5
|
import opencodePlugin from "../dist/opencode.js"
|
|
5
6
|
import * as deepseekPlugin from "../dist/deepseek.js"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const pythonAvailable = spawnSync("python3", ["--version"], { encoding: "utf8" }).status === 0
|
|
9
|
+
|
|
10
|
+
test("OpenCode tool delegates to the shared skill script", async () => {
|
|
11
|
+
const hooks = await opencodePlugin({})
|
|
12
|
+
const entry = hooks.tool?.["{{NAME}}_run"]
|
|
13
|
+
assert.equal(typeof entry?.execute, "function")
|
|
14
|
+
const result = await entry.execute({ input: '{"hello":"world"}' })
|
|
15
|
+
assert.deepEqual(JSON.parse(result), {
|
|
16
|
+
ok: true,
|
|
17
|
+
plugin: "{{NAME}}",
|
|
18
|
+
echo: { hello: "world" },
|
|
19
|
+
})
|
|
9
20
|
})
|
|
10
21
|
|
|
11
22
|
test("DeepSeek entrypoint exposes a Cordis namespace plugin", () => {
|
|
@@ -13,3 +24,25 @@ test("DeepSeek entrypoint exposes a Cordis namespace plugin", () => {
|
|
|
13
24
|
assert.equal(typeof deepseekPlugin.apply, "function")
|
|
14
25
|
assert.equal("default" in deepseekPlugin, false)
|
|
15
26
|
})
|
|
27
|
+
|
|
28
|
+
test("DeepSeek adapter delegates to the shared skill script", () => {
|
|
29
|
+
const result = deepseekPlugin.runSkillScript("main.mjs", '{"hello":"world"}')
|
|
30
|
+
assert.deepEqual(result, {
|
|
31
|
+
ok: true,
|
|
32
|
+
plugin: "{{NAME}}",
|
|
33
|
+
echo: { hello: "world" },
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test(
|
|
38
|
+
"Python twin honors the same tool contract",
|
|
39
|
+
{ skip: !pythonAvailable },
|
|
40
|
+
() => {
|
|
41
|
+
const result = deepseekPlugin.runSkillScript("main.py", '{"hello":"world"}')
|
|
42
|
+
assert.deepEqual(result, {
|
|
43
|
+
ok: true,
|
|
44
|
+
plugin: "{{NAME}}",
|
|
45
|
+
echo: { hello: "world" },
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
)
|