|
|
@@ -3,9 +3,10 @@ import type { Database } from "../db.js";
|
|
|
import fastGlob from "fast-glob";
|
|
|
import { execSync, spawn as nodeSpawn } from "child_process";
|
|
|
import { fileURLToPath } from "url";
|
|
|
-import { dirname, join as pathJoin } from "path";
|
|
|
+import { dirname, join as pathJoin, relative as relativePath } from "path";
|
|
|
import { parseArgs } from "util";
|
|
|
-import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync } from "fs";
|
|
|
+import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync, lstatSync, rmSync, symlinkSync, readlinkSync } from "fs";
|
|
|
+import { createInterface } from "readline/promises";
|
|
|
import {
|
|
|
getPwd,
|
|
|
getRealPath,
|
|
|
@@ -95,6 +96,7 @@ import {
|
|
|
setConfigIndexName,
|
|
|
loadConfig,
|
|
|
} from "../collections.js";
|
|
|
+import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js";
|
|
|
|
|
|
// Enable production mode - allows using default database path
|
|
|
// Tests must set INDEX_PATH or use createStore() with explicit path
|
|
|
@@ -2313,6 +2315,8 @@ function parseCLI() {
|
|
|
help: { type: "boolean", short: "h" },
|
|
|
version: { type: "boolean", short: "v" },
|
|
|
skill: { type: "boolean" },
|
|
|
+ global: { type: "boolean" },
|
|
|
+ yes: { type: "boolean" },
|
|
|
// Search options
|
|
|
n: { type: "string" },
|
|
|
"min-score": { type: "string" },
|
|
|
@@ -2392,22 +2396,130 @@ function parseCLI() {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function showSkill(): void {
|
|
|
- const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
|
- const relativePath = pathJoin("skills", "qmd", "SKILL.md");
|
|
|
- const skillPath = pathJoin(scriptDir, "..", "..", relativePath);
|
|
|
+function getSkillInstallDir(globalInstall: boolean): string {
|
|
|
+ return globalInstall
|
|
|
+ ? resolve(homedir(), ".agents", "skills", "qmd")
|
|
|
+ : resolve(getPwd(), ".agents", "skills", "qmd");
|
|
|
+}
|
|
|
+
|
|
|
+function getClaudeSkillLinkPath(globalInstall: boolean): string {
|
|
|
+ return globalInstall
|
|
|
+ ? resolve(homedir(), ".claude", "skills", "qmd")
|
|
|
+ : resolve(getPwd(), ".claude", "skills", "qmd");
|
|
|
+}
|
|
|
+
|
|
|
+function pathExists(path: string): boolean {
|
|
|
+ try {
|
|
|
+ lstatSync(path);
|
|
|
+ return true;
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- console.log(`QMD Skill (${relativePath})`);
|
|
|
- console.log(`Location: ${skillPath}`);
|
|
|
+function removePath(path: string): void {
|
|
|
+ const stat = lstatSync(path);
|
|
|
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
|
+ rmSync(path, { recursive: true, force: true });
|
|
|
+ } else {
|
|
|
+ unlinkSync(path);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function showSkill(): void {
|
|
|
+ console.log("QMD Skill (embedded)");
|
|
|
console.log("");
|
|
|
+ const content = getEmbeddedQmdSkillContent();
|
|
|
+ process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
|
+}
|
|
|
+
|
|
|
+function writeEmbeddedSkill(targetDir: string, force: boolean): void {
|
|
|
+ if (pathExists(targetDir)) {
|
|
|
+ if (!force) {
|
|
|
+ throw new Error(`Skill already exists: ${targetDir} (use --force to replace it)`);
|
|
|
+ }
|
|
|
+ removePath(targetDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ mkdirSync(targetDir, { recursive: true });
|
|
|
+ for (const file of getEmbeddedQmdSkillFiles()) {
|
|
|
+ const destination = resolve(targetDir, file.relativePath);
|
|
|
+ mkdirSync(dirname(destination), { recursive: true });
|
|
|
+ writeFileSync(destination, file.content, "utf-8");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function ensureClaudeSymlink(linkPath: string, targetDir: string, force: boolean): boolean {
|
|
|
+ const parentDir = dirname(linkPath);
|
|
|
+ if (pathExists(parentDir)) {
|
|
|
+ const resolvedTargetDir = realpathSync(dirname(targetDir));
|
|
|
+ const resolvedLinkParent = realpathSync(parentDir);
|
|
|
+
|
|
|
+ // If .claude/skills already resolves to the same directory as .agents/skills,
|
|
|
+ // the skill is already visible to Claude and creating qmd -> qmd would loop.
|
|
|
+ if (resolvedTargetDir === resolvedLinkParent) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const linkTarget = relativePath(parentDir, targetDir) || ".";
|
|
|
+
|
|
|
+ mkdirSync(parentDir, { recursive: true });
|
|
|
+
|
|
|
+ if (pathExists(linkPath)) {
|
|
|
+ const stat = lstatSync(linkPath);
|
|
|
+ if (stat.isSymbolicLink() && readlinkSync(linkPath) === linkTarget) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!force) {
|
|
|
+ throw new Error(`Claude skill path already exists: ${linkPath} (use --force to replace it)`);
|
|
|
+ }
|
|
|
+ removePath(linkPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ symlinkSync(linkTarget, linkPath, "dir");
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function shouldCreateClaudeSymlink(linkPath: string, autoYes: boolean): Promise<boolean> {
|
|
|
+ if (autoYes) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
|
+ console.log(`Tip: create a Claude symlink manually at ${linkPath}`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rl = createInterface({
|
|
|
+ input: process.stdin,
|
|
|
+ output: process.stdout,
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ const answer = await rl.question(`Create a symlink in ${linkPath}? [y/N] `);
|
|
|
+ const normalized = answer.trim().toLowerCase();
|
|
|
+ return normalized === "y" || normalized === "yes";
|
|
|
+ } finally {
|
|
|
+ rl.close();
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- if (!existsSync(skillPath)) {
|
|
|
- console.error("SKILL.md not found. If you built from source, ensure skills/qmd/SKILL.md exists.");
|
|
|
+async function installSkill(globalInstall: boolean, force: boolean, autoYes: boolean): Promise<void> {
|
|
|
+ const installDir = getSkillInstallDir(globalInstall);
|
|
|
+ writeEmbeddedSkill(installDir, force);
|
|
|
+ console.log(`✓ Installed QMD skill to ${installDir}`);
|
|
|
+
|
|
|
+ const claudeLinkPath = getClaudeSkillLinkPath(globalInstall);
|
|
|
+ if (!(await shouldCreateClaudeSymlink(claudeLinkPath, autoYes))) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const content = readFileSync(skillPath, "utf-8");
|
|
|
- process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
|
+ const linked = ensureClaudeSymlink(claudeLinkPath, installDir, force);
|
|
|
+ if (linked) {
|
|
|
+ console.log(`✓ Linked Claude skill at ${claudeLinkPath}`);
|
|
|
+ } else {
|
|
|
+ console.log(`✓ Claude already sees the skill via ${dirname(claudeLinkPath)}`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function showHelp(): void {
|
|
|
@@ -2423,6 +2535,7 @@ function showHelp(): void {
|
|
|
console.log(" qmd vsearch <query> - Vector similarity only");
|
|
|
console.log(" qmd get <file>[:line] [-l N] - Show a single document, optional line slice");
|
|
|
console.log(" qmd multi-get <pattern> - Batch fetch via glob or comma-separated list");
|
|
|
+ console.log(" qmd skill show/install - Show or install the packaged QMD skill");
|
|
|
console.log(" qmd mcp - Start the MCP server (stdio transport for AI agents)");
|
|
|
console.log("");
|
|
|
console.log("Collections & context:");
|
|
|
@@ -2472,7 +2585,9 @@ function showHelp(): void {
|
|
|
console.log("");
|
|
|
console.log("AI agents & integrations:");
|
|
|
console.log(" - Run `qmd mcp` to expose the MCP server (stdio) to agents/IDEs.");
|
|
|
- console.log(" - `qmd --skill` prints the packaged skills/qmd/SKILL.md (path + contents).");
|
|
|
+ console.log(" - `qmd skill install` installs the QMD skill into ./.agents/skills/qmd.");
|
|
|
+ console.log(" - Use `qmd skill install --global` for ~/.agents/skills/qmd.");
|
|
|
+ console.log(" - `qmd --skill` is kept as an alias for `qmd skill show`.");
|
|
|
console.log(" - Advanced: `qmd mcp --http ...` and `qmd mcp --http --daemon` are optional for custom transports.");
|
|
|
console.log("");
|
|
|
console.log("Global options:");
|
|
|
@@ -2533,6 +2648,20 @@ if (isMain) {
|
|
|
process.exit(0);
|
|
|
}
|
|
|
|
|
|
+ if (cli.values.help && cli.command === "skill") {
|
|
|
+ console.log("Usage: qmd skill <show|install> [options]");
|
|
|
+ console.log("");
|
|
|
+ console.log("Commands:");
|
|
|
+ console.log(" show Print the packaged QMD skill");
|
|
|
+ console.log(" install Install into ./.agents/skills/qmd");
|
|
|
+ console.log("");
|
|
|
+ console.log("Options:");
|
|
|
+ console.log(" --global Install into ~/.agents/skills/qmd");
|
|
|
+ console.log(" --yes Also create the .claude/skills/qmd symlink");
|
|
|
+ console.log(" -f, --force Replace existing install or symlink");
|
|
|
+ process.exit(0);
|
|
|
+ }
|
|
|
+
|
|
|
if (!cli.command || cli.values.help) {
|
|
|
showHelp();
|
|
|
process.exit(cli.values.help ? 0 : 1);
|
|
|
@@ -2933,6 +3062,47 @@ if (isMain) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
+ case "skill": {
|
|
|
+ const subcommand = cli.args[0];
|
|
|
+ switch (subcommand) {
|
|
|
+ case "show": {
|
|
|
+ showSkill();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case "install": {
|
|
|
+ try {
|
|
|
+ await installSkill(Boolean(cli.values.global), Boolean(cli.values.force), Boolean(cli.values.yes));
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error instanceof Error ? error.message : String(error));
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case "help":
|
|
|
+ case undefined: {
|
|
|
+ console.log("Usage: qmd skill <show|install> [options]");
|
|
|
+ console.log("");
|
|
|
+ console.log("Commands:");
|
|
|
+ console.log(" show Print the packaged QMD skill");
|
|
|
+ console.log(" install Install into ./.agents/skills/qmd");
|
|
|
+ console.log("");
|
|
|
+ console.log("Options:");
|
|
|
+ console.log(" --global Install into ~/.agents/skills/qmd");
|
|
|
+ console.log(" --yes Also create the .claude/skills/qmd symlink");
|
|
|
+ console.log(" -f, --force Replace existing install or symlink");
|
|
|
+ process.exit(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ default:
|
|
|
+ console.error(`Unknown subcommand: ${subcommand}`);
|
|
|
+ console.error("Run 'qmd skill help' for usage");
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
case "cleanup": {
|
|
|
const db = getDb();
|
|
|
|