From gm
Creates lang/ plugins for gm-cc integrating CLI tools or language runtimes with exec:<id> dispatch, optional LSP diagnostics, and prompt context injection. Zero config needed.
npx claudepluginhub anentrypoint/gm-cc --plugin gmThis skill uses the workspace's default tool permissions.
A lang plugin is a single CommonJS file at `<projectDir>/lang/<id>.js`. gm-cc's hooks auto-discover it — no hook editing, no settings changes. The plugin gets three integration points: **exec dispatch**, **LSP diagnostics**, and **context injection**.
Guides integration of LSP servers into Claude Code plugins via plugin.json or .lsp.json for code intelligence like go-to-definition, references, hovers, and completions.
Guides building distributable Claude Code plugins bundling commands, agents, skills, hooks, MCP/LSP servers, and marketplaces using manifests, structures, and CLI commands.
Validates Claude Code plugins for structure, manifest, frontmatter, tool invocations, and token budgets using Python scripts. Guides selection of components like skills, agents, MCP/LSP servers, hooks.
Share bugs, ideas, or general feedback.
A lang plugin is a single CommonJS file at <projectDir>/lang/<id>.js. gm-cc's hooks auto-discover it — no hook editing, no settings changes. The plugin gets three integration points: exec dispatch, LSP diagnostics, and context injection.
'use strict';
module.exports = {
id: 'mytool', // must match filename: lang/mytool.js
exec: {
match: /^exec:mytool/, // regex tested against full "exec:mytool\n<code>" string
run(code, cwd) { // returns string or Promise<string>
// ...
}
},
lsp: { // optional — synchronous only
check(fileContent, cwd) { // returns Diagnostic[] synchronously
// ...
}
},
extensions: ['.ext'], // optional — file extensions lsp.check applies to
context: `=== mytool ===\n...` // optional — string or () => string
};
type Diagnostic = { line: number; col: number; severity: 'error'|'warning'; message: string };
exec.run is called in a child process (30s timeout) when Claude writes exec:mytool\n<code>. Output is returned as exec:mytool output:\n\n<result>. Async is fine here.lsp.check is called synchronously in the hook process on each prompt submit — must NOT be async. Use execFileSync or spawnSync.context is injected into every prompt's additionalContext (truncated to 2000 chars) and into the session-start context.match regex is tested against the full command string exec:mytool\n<code> — keep it simple: /^exec:mytool/.Answer these before writing any code:
gdlint, tsc, deno, ruff, ...)tool eval <expr>, tool -e <code>, HTTP POST, ...)tool run <file>, tool <file>, ...)Pattern for HTTP eval (tool has a running server):
const http = require('http');
function httpPost(port, urlPath, body) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = http.request(
{ hostname: '127.0.0.1', port, path: urlPath, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
(res) => { let raw = ''; res.on('data', c => raw += c); res.on('end', () => { try { resolve(JSON.parse(raw)); } catch { resolve({ raw }); } }); }
);
req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); });
req.on('error', reject);
req.write(data); req.end();
});
}
Pattern for file-based execution (write temp file, run headlessly):
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
function runFile(code, cwd) {
const tmp = path.join(os.tmpdir(), `plugin_${Date.now()}.ext`);
fs.writeFileSync(tmp, code);
try {
return execFileSync('mytool', ['run', tmp], { cwd, encoding: 'utf8', timeout: 10000 });
} finally {
try { fs.unlinkSync(tmp); } catch (_) {}
}
}
Distinguish single expression vs multi-line when both modes exist:
function isSingleExpr(code) {
return !code.trim().includes('\n') && !/\b(func|def|fn |class|import)\b/.test(code);
}
Must be synchronous. Parse the tool's stderr/stdout for diagnostics:
const { spawnSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function check(fileContent, cwd) {
const tmp = path.join(os.tmpdir(), `lsp_${Math.random().toString(36).slice(2)}.ext`);
try {
fs.writeFileSync(tmp, fileContent);
const r = spawnSync('mytool', ['check', tmp], { encoding: 'utf8', cwd });
const output = r.stdout + r.stderr;
return output.split('\n').reduce((acc, line) => {
const m = line.match(/^.+:(\d+):(\d+):\s+(error|warning):\s+(.+)$/);
if (m) acc.push({ line: parseInt(m[1]), col: parseInt(m[2]), severity: m[3], message: m[4].trim() });
return acc;
}, []);
} catch (_) {
return [];
} finally {
try { fs.unlinkSync(tmp); } catch (_) {}
}
}
Common output patterns to parse:
file:line:col: error: message → standardfile:line: E001: message → gdlint style (E=error, W=warning)JSON.parse(r.stdout).errors.map(...)Describe what exec:<id> does and when to use it. This appears in every prompt. Keep it under 300 chars:
context: `=== mytool exec: support ===
exec:mytool
<expression or code block>
Runs via <how>. Use for <when>.`
File goes at lang/<id>.js in the project root. The id field must match the filename (without .js).
Verify after writing:
exec:nodejs
const p = require('/abs/path/to/lang/mytool.js');
console.log(p.id, typeof p.exec.run, p.exec.match.toString());
Then test dispatch:
exec:mytool
<a simple test expression>
If it returns exec:mytool output: → working. If it errors → fix exec.run.
exec.run may be async — it runs in a child process with a 30s timeoutlsp.check must be synchronous — no Promises, no async/awaitmodule.exports = { ... }) — no ES module syntaxexec.run must complete and exit cleanlyid must match the filename exactlymatch specificSee C:/dev/godot-kit/lang/gdscript.js for a complete working example combining HTTP eval (single expressions via port 6009) with headless file execution fallback, synchronous gdlint LSP, and a context string.