mirror of
https://github.com/tinyfish-io/tinyfish-cookbook.git
synced 2026-09-14 17:41:39 +08:00
d58743b5eb
doctor still described `schema_version` 1 and 2 + bailed above 2, but CLI emitted 3 since 0.22.0 and skill pins `@tiny-fish/cli@latest`. as shipped it degraded to `--pretty` on every run. The feedback skill also passed an unquoted, user-derived title to `gh issue create`. three fixes landed in tinyfish-web-agent-integrations#29; this keeps the two copies of the plugin textually identical - `doctor`: schema 3 — `cli_version`, `ok_harnesses`/`ok_cli`, `checks[].scope`, `repairs[].action`, and the exit code coming from the harness scope alone, so `ok_cli: false` beside exit `0` is a real state. Drops the `registered, API key present but unverified` string the CLI no longer emits; `--fix` examples run through `npx` like step 1. - `feedback`: `--title "$title" --body-file "$body_file"`, injection warning widened to the title, and `eval` / constructed command strings prohibited. - `plugins/tinyfish/README.md`: adds `~/.grok` to the config dirs doctor reads, per the CLI's `CONFIG_DIRS`. The rest of that list matches. - 1.2.3 → 1.2.4 in `plugin.json` and the marketplace entry — `validate-skills.mjs` fails if they diverge --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
4.3 KiB
JavaScript
91 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generates skill variants from skills-src/*.md. The claude target feeds the marketplace
|
|
// plugin; the generic target is served to every other harness by the `skills` CLI.
|
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const SRC = join(ROOT, "skills-src");
|
|
// Codex rejects frontmatter descriptions longer than 1024 chars.
|
|
const DESC_LIMIT = 1024;
|
|
|
|
const GENERIC_REAUTH = `re-authenticate in the harness itself.
|
|
|
|
| Harness | Re-auth |
|
|
|---|---|
|
|
| Codex, Hermes | no login command — auth runs on first tool use; trigger a TinyFish tool and finish the browser sign-in |
|
|
| OpenCode | \`opencode mcp auth tinyfish\` |
|
|
| Claude Code | \`/mcp\` in-app, or \`claude mcp login tinyfish\` |
|
|
| Grok | no login command — in Grok Build, \`/mcps\`, select \`tinyfish\`, press \`i\`. A keyed registration instead reads \`TINYFISH_API_KEY\` from Grok's environment and has no OAuth fallback: re-run \`tinyfish connect grok --api-key <key>\` and start Grok from a new terminal |
|
|
| OpenClaw, Cursor | key-based — \`tinyfish auth login\`, then \`tinyfish connect <harness>\` to rewrite the header |`;
|
|
|
|
const TARGETS = {
|
|
claude: {
|
|
dir: (name) => join(ROOT, "plugins", "tinyfish", "skills", name),
|
|
vars: {
|
|
HARNESS_FLAG: " --harness claude-code",
|
|
HARNESS_ENTRY: "`--harness claude-code` narrows `harnesses[]` to exactly one entry, so there is no ambiguity about which harness it describes.",
|
|
REAUTH: "tell the user to run `/mcp`, pick tinyfish, and sign in.",
|
|
FEEDBACK: "offer `/tinyfish:feedback` to file it",
|
|
},
|
|
},
|
|
generic: {
|
|
dir: (name) => join(ROOT, "skills", name),
|
|
vars: {
|
|
HARNESS_FLAG: "",
|
|
HARNESS_ENTRY:
|
|
"Run without `--harness`, so `harnesses[]` carries one entry per harness doctor knows — installed or not. Read the entry whose `harness` matches the agent you are running in, never the first one, and check its `detected` first: an absent harness reports `detected: false`, `registered: \"no\"`, `auth_mode: \"unknown\"`, which is not a fault to repair. doctor only knows `claude-code`, `codex`, `cursor`, `grok`, `hermes`, `openclaw`, `opencode`; if you are none of those, no entry describes you and step 2 is your only evidence.",
|
|
REAUTH: GENERIC_REAUTH,
|
|
FEEDBACK: "file it at https://github.com/tinyfish-io/tinyfish-cookbook/issues",
|
|
},
|
|
},
|
|
};
|
|
|
|
// Explicit per-target names: the `skills` CLI matches --skill on frontmatter name only,
|
|
// and feedback is Claude-specific, so neither may default to "every target".
|
|
const SKILLS = [
|
|
{ src: "doctor.md", names: { claude: "doctor", generic: "tinyfish-doctor" } },
|
|
{ src: "feedback.md", names: { claude: "feedback" } },
|
|
];
|
|
|
|
function parse(src, file) {
|
|
const m = src.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
if (!m) throw new Error(`${file}: missing frontmatter`);
|
|
const fm = Object.fromEntries(
|
|
m[1].split("\n").map((l) => {
|
|
const i = l.indexOf(":");
|
|
return [l.slice(0, i).trim(), l.slice(i + 1).trim()];
|
|
})
|
|
);
|
|
if (!fm.name || !fm.description) throw new Error(`${file}: frontmatter needs name + description`);
|
|
if (fm.description.length > DESC_LIMIT)
|
|
throw new Error(`${file}: description ${fm.description.length} chars > ${DESC_LIMIT} (Codex limit)`);
|
|
return { fm, body: m[2] };
|
|
}
|
|
|
|
function render(body, vars, file, target) {
|
|
const out = Object.entries(vars).reduce(
|
|
(acc, [k, v]) => acc.replaceAll(`{{${k}}}`, v),
|
|
body
|
|
);
|
|
const leftover = out.match(/\{\{[A-Z_]+\}\}/);
|
|
if (leftover) throw new Error(`${file} → ${target}: unsubstituted placeholder ${leftover[0]}`);
|
|
return out;
|
|
}
|
|
|
|
let count = 0;
|
|
for (const skill of SKILLS) {
|
|
const { fm, body } = parse(readFileSync(join(SRC, skill.src), "utf8"), skill.src);
|
|
for (const [target, name] of Object.entries(skill.names)) {
|
|
const t = TARGETS[target];
|
|
if (!t) throw new Error(`${skill.src}: unknown target ${target}`);
|
|
const out = `---\nname: ${name}\ndescription: ${fm.description}\n---\n${render(body, t.vars, skill.src, target)}`;
|
|
const dir = t.dir(name);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, "SKILL.md"), out);
|
|
count++;
|
|
}
|
|
}
|
|
console.log(`generated ${count} skill files`);
|