create-lang-plugin

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CREATE LANG PLUGIN

创建LANG插件

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.
lang插件是位于
<projectDir>/lang/<id>.js
的单个CommonJS文件。gm-cc的钩子会自动发现它——无需编辑钩子,也无需修改设置。该插件包含三个集成点:exec调度LSP诊断上下文注入

PLUGIN SHAPE

插件结构

js
'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
};
ts
type Diagnostic = { line: number; col: number; severity: 'error'|'warning'; message: string };
js
'use strict';
module.exports = {
  id: 'mytool',                          // 必须与文件名匹配:lang/mytool.js
  exec: {
    match: /^exec:mytool/,               // 针对完整命令字符串"exec:mytool\n<code>"进行正则匹配
    run(code, cwd) {                     // 返回字符串或Promise<string>
      // ...
    }
  },
  lsp: {                                 // 可选——仅支持同步
    check(fileContent, cwd) {            // 同步返回Diagnostic[]
      // ...
    }
  },
  extensions: ['.ext'],                  // 可选——lsp.check适用的文件扩展名
  context: `=== mytool ===\n...`        // 可选——字符串或() => string
};
ts
type Diagnostic = { line: number; col: number; severity: 'error'|'warning'; message: string };

HOW IT WORKS

工作原理

  • 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/
    .
  • 当Claude写入
    exec:mytool\n<code>
    时,**
    exec.run
    **会在子进程中被调用(超时时间30秒)。输出结果会以
    exec:mytool output:\n\n<result>
    的形式返回。此处支持异步操作。
  • **
    lsp.check
    **会在每次提交提示时,在钩子进程中同步调用——禁止异步。请使用
    execFileSync
    spawnSync
  • **
    context
    **会被注入到每个提示的
    additionalContext
    中(截断至2000字符)以及会话启动上下文里。
  • **
    match
    **正则会针对完整命令字符串
    exec:mytool\n<code>
    进行测试——保持简单即可:
    /^exec:mytool/

STEP 1 — IDENTIFY THE TOOL

步骤1 — 确定工具信息

Answer these before writing any code:
  1. What is the tool's CLI name or npm package? (
    gdlint
    ,
    tsc
    ,
    deno
    ,
    ruff
    , ...)
  2. How do you run a single expression/snippet? (
    tool eval <expr>
    ,
    tool -e <code>
    , HTTP POST, ...)
  3. How do you run a file? (
    tool run <file>
    ,
    tool <file>
    , ...)
  4. Does it have a lint/check mode? What does its output format look like?
  5. What file extensions does it apply to?
  6. Is the game/server running required, or does it work headlessly?
在编写代码前先回答这些问题:
  1. 工具的CLI名称或npm包是什么?(例如
    gdlint
    tsc
    deno
    ruff
    等)
  2. 如何运行单个表达式/代码片段?(例如
    tool eval <expr>
    tool -e <code>
    、HTTP POST等)
  3. 如何运行文件?(例如
    tool run <file>
    tool <file>
    等)
  4. 它是否有 lint/检查模式?输出格式是什么样的?
  5. 它适用于哪些文件扩展名?
  6. 是否需要运行游戏/服务器,还是可以无头运行?

STEP 2 — IMPLEMENT exec.run

步骤2 — 实现exec.run

Pattern for HTTP eval (tool has a running server):
js
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):
js
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:
js
function isSingleExpr(code) {
  return !code.trim().includes('\n') && !/\b(func|def|fn |class|import)\b/.test(code);
}
HTTP求值模式(工具需运行服务器):
js
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();
  });
}
基于文件的执行模式(写入临时文件,无头运行):
js
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 (_) {}
  }
}
当同时存在两种模式时,区分单个表达式与多行代码
js
function isSingleExpr(code) {
  return !code.trim().includes('\n') && !/\b(func|def|fn |class|import)\b/.test(code);
}

STEP 3 — IMPLEMENT lsp.check (if applicable)

步骤3 — 实现lsp.check(如适用)

Must be synchronous. Parse the tool's stderr/stdout for diagnostics:
js
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
    → standard
  • file:line: E001: message
    → gdlint style (
    E
    =error,
    W
    =warning)
  • JSON output →
    JSON.parse(r.stdout).errors.map(...)
必须为同步操作。解析工具的stderr/stdout以获取诊断信息:
js
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 (_) {}
  }
}
常见的输出解析模式:
  • file:line:col: error: message
    → 标准格式
  • file:line: E001: message
    → gdlint风格(
    E
    =错误,
    W
    =警告)
  • JSON输出 →
    JSON.parse(r.stdout).errors.map(...)

STEP 4 — WRITE context STRING

步骤4 — 编写context字符串

Describe what
exec:<id>
does and when to use it. This appears in every prompt. Keep it under 300 chars:
js
context: `=== mytool exec: support ===
exec:mytool
<expression or code block>

Runs via <how>. Use for <when>.`
描述
exec:<id>
的功能及使用场景。该内容会出现在每个提示中,长度控制在300字符以内:
js
context: `=== mytool exec: 支持 ===
exec:mytool
<表达式或代码块>

通过<方式>运行。适用于<场景>。`

STEP 5 — WRITE THE FILE

步骤5 — 编写插件文件

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
.
文件需放在项目根目录的
lang/<id>.js
路径下。
id
字段必须与文件名(不含
.js
)完全匹配。
编写完成后进行验证:
exec:nodejs
const p = require('/abs/path/to/lang/mytool.js');
console.log(p.id, typeof p.exec.run, p.exec.match.toString());
然后测试调度功能:
exec:mytool
<简单测试表达式>
如果返回
exec:mytool output:
→ 功能正常。如果报错 → 修复
exec.run

CONSTRAINTS

约束条件

  • exec.run
    may be async — it runs in a child process with a 30s timeout
  • lsp.check
    must be synchronous — no Promises, no async/await
  • Plugin must be CommonJS (
    module.exports = { ... }
    ) — no ES module syntax
  • No persistent processes —
    exec.run
    must complete and exit cleanly
  • id
    must match the filename exactly
  • First match wins — if multiple plugins could match, make
    match
    specific
  • exec.run
    可以是异步的——它会在子进程中运行,超时时间为30秒
  • lsp.check
    必须是同步的——不支持Promise,不允许使用async/await
  • 插件必须使用CommonJS格式(
    module.exports = { ... }
    )——不支持ES模块语法
  • 不允许持久化进程——
    exec.run
    必须完成并干净退出
  • id
    必须与文件名完全匹配
  • 匹配优先级为第一个匹配项——如果多个插件可能匹配,请将
    match
    设置得更具体

EXAMPLE — gdscript plugin (reference implementation)

示例——gdscript插件(参考实现)

See
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.
完整的工作示例请查看
C:/dev/godot-kit/lang/gdscript.js
,该示例结合了HTTP求值(通过6009端口执行单个表达式)与无头文件执行降级方案、同步gdlint LSP以及上下文字符串。