gm-cc 2.0.631 → 2.0.633

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.
@@ -4,7 +4,7 @@
4
4
  "name": "AnEntrypoint"
5
5
  },
6
6
  "description": "State machine agent with hooks, skills, and automated git enforcement",
7
- "version": "2.0.631",
7
+ "version": "2.0.633",
8
8
  "metadata": {
9
9
  "description": "State machine agent with hooks, skills, and automated git enforcement"
10
10
  },
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/bin/plugkit.exe CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-cc",
3
- "version": "2.0.631",
3
+ "version": "2.0.633",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.631",
3
+ "version": "2.0.633",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": {
6
6
  "name": "AnEntrypoint",
@@ -4,199 +4,74 @@ description: Browser automation via playwriter. Use when user needs to interact
4
4
  allowed-tools: Bash(browser:*), Bash(exec:browser*)
5
5
  ---
6
6
 
7
- # Browser Automation with playwriter
7
+ # Browser Automation
8
8
 
9
- **Use gm subagents for all independent work items. Invoke all skills in the chain: planning → gm-execute → gm-emit → gm-complete → update-docs.**
9
+ Two pathways never mix:
10
10
 
11
+ **`exec:browser`** — JS against `page`. `page`, `snapshot`, `screenshotWithAccessibilityLabels`, `state` globals available. 15s live window then backgrounds; drains auto on every subsequent plugkit call.
11
12
 
12
- ## Two Pathways
13
+ **`browser:` prefix** — playwriter session management. One command per block.
13
14
 
14
- **Session commands** (`browser:` prefix) — manage multi-step sessions via playwriter CLI. Each `browser:` block runs its commands sequentially.
15
-
16
- **JS execution** (`exec:browser`) — run JavaScript directly against `page`. State persists across calls via `state` global.
17
-
18
- **CRITICAL**: Never mix these two pathways. Each `browser:` block is a separate Bash call. Each `exec:browser` block is a separate Bash call.
19
-
20
- ## 15-Second Ceiling — How It Works
21
-
22
- Every `exec:browser` call has a 15s live window. During that window, all stdout/stderr is streamed to you in real time. After 15s the task backgrounds and you receive:
23
- - All output produced so far (live drain)
24
- - A task ID with `plugkit sleep/status/close` instructions
25
-
26
- **The task keeps running.** Every subsequent plugkit interaction automatically drains all running browser tasks — you will see new output without asking.
27
-
28
- **Never use `await new Promise(r => setTimeout(r, N))` with N > 10000.** Use short poll loops instead (see patterns below).
29
-
30
- **"Assertion failed: UV_HANDLE_CLOSING" in output** means the call exceeded 15s and was cut off — ignore the assertion noise, look at the output before it. The task was backgrounded normally.
31
-
32
- ## Idle Timeout & Session Reaper
33
-
34
- Playwriter kills idle browser sessions after 5-15 minutes of inactivity. The rs-exec tooling now automatically cleans up the spawned browser process when the Claude Code session ends, preventing zombie tabs.
35
-
36
- **Historical note**: Earlier versions left the browser running after session end, causing repeated tabs on reconnect. This is now fixed — the browser will be killed when your session idles and closes.
37
-
38
- ## Session Pathway (`browser:`)
39
-
40
- Create a session first, use `--direct` for CDP mode (requires Chrome with remote debugging):
41
-
42
- ```
43
- browser:
44
- playwriter session new --direct
45
- ```
46
-
47
- Returns a numeric session ID (e.g. `1`). Use that ID for all subsequent calls. **Each command must be a separate Bash call:**
48
-
49
- ```
50
- browser:
51
- playwriter -s 1 -e 'await page.goto("http://example.com")'
52
- ```
53
-
54
- ```
55
- browser:
56
- playwriter -s 1 -e 'await snapshot({ page })'
57
- ```
15
+ ## Core Usage
58
16
 
59
17
  ```
60
- browser:
61
- playwriter -s 1 -e 'await screenshotWithAccessibilityLabels({ page })'
18
+ exec:browser
19
+ await page.goto('https://example.com')
20
+ await snapshot({ page })
62
21
  ```
63
22
 
64
- State persists across session calls:
65
-
66
23
  ```
67
24
  browser:
68
- playwriter -s 1 -e 'state.x = 1'
25
+ playwriter session new --direct
69
26
  ```
70
27
 
71
28
  ```
72
29
  browser:
73
- playwriter -s 1 -e 'console.log(state.x)'
74
- ```
75
-
76
-
77
- **RULE**: The `-e` argument must use single quotes. The JS inside must use double quotes for strings.
78
-
79
- **RULE**: Never chain multiple `playwriter` commands in one `browser:` block — run one command per block.
80
-
81
- ## JS Execution Pathway (`exec:browser`)
82
-
83
- For direct page access, DOM queries, and data extraction. The runtime provides `page`, `snapshot`, `screenshotWithAccessibilityLabels`, and `state` as globals.
84
-
85
- ```
86
- exec:browser
87
- await page.goto('https://example.com')
88
- await snapshot({ page })
89
- ```
90
-
91
- ```
92
- exec:browser
93
- const title = await page.title()
94
- console.log(title)
30
+ playwriter -s 1 -e 'await page.goto("http://example.com")'
95
31
  ```
96
32
 
97
- Never add shell quoting write plain JavaScript directly.
98
-
99
- ## Core Workflow
100
-
101
- 1. **Navigate**: `exec:browser\nawait page.goto('url')` — session auto-created on first call
102
- 2. **Snapshot**: `exec:browser\nawait snapshot({ page })`
103
- 3. **Interact**: click, fill, type in subsequent `exec:browser` calls
104
- 4. **Extract data**: `exec:browser\nconsole.log(await page.evaluate(() => document.title))`
33
+ Session state persists across `browser:` calls. `-e` arg: single quotes outside, double quotes inside JS strings.
105
34
 
106
- ## Long-Running Operations — Poll Pattern
35
+ ## Timing
107
36
 
108
- For operations that take >10s (model loading, network fetches, animations):
37
+ Never `await setTimeout(N)` with N > 10000. Use poll loops:
109
38
 
110
- **Step 1** — set up listener and kick off the operation:
111
- ```
112
- exec:browser
113
- state.done = false
114
- state.result = null
115
- page.on('console', msg => {
116
- const t = msg.text()
117
- if (t.includes('loaded') || t.includes('ready')) { state.done = true; state.result = t }
118
- })
119
- await page.click('#start-button')
120
- console.log('started, waiting...')
121
- ```
122
-
123
- **Step 2** — poll in short bursts (this will background after 15s and keep draining):
124
39
  ```
125
40
  exec:browser
126
41
  const start = Date.now()
127
42
  while (!state.done && Date.now() - start < 12000) {
128
43
  await new Promise(r => setTimeout(r, 500))
129
44
  }
130
- console.log('done:', state.done, 'result:', state.result)
45
+ console.log(state.result)
131
46
  ```
132
47
 
133
- If step 2 backgrounds (takes >15s), every subsequent plugkit call will drain its output automatically. When you see the result in the drain log, close the task:
134
- ```
135
- exec:close
136
- task_N
137
- ```
48
+ "Assertion failed: UV_HANDLE_CLOSING" = backgrounded normally, ignore noise.
138
49
 
139
50
  ## Common Patterns
140
51
 
141
-
142
-
143
- ### Data Extraction
144
-
52
+ Data extraction:
145
53
  ```
146
54
  exec:browser
147
- const items = await page.$$eval('.product-title', els => els.map(e => e.textContent))
55
+ const items = await page.$$eval('.title', els => els.map(e => e.textContent))
148
56
  console.log(JSON.stringify(items))
149
57
  ```
150
58
 
151
-
152
- ### Console Monitoring — set up listener first, then poll
153
-
59
+ Console monitoring — set listeners first, then poll:
154
60
  ```
155
61
  exec:browser
156
62
  state.logs = []
157
- state.errors = []
158
63
  page.on('console', msg => state.logs.push({ type: msg.type(), text: msg.text() }))
159
- page.on('pageerror', e => state.errors.push(e.message))
160
- console.log('listeners attached')
161
- ```
162
-
163
- ```
164
- exec:browser
165
- console.log('logs so far:', JSON.stringify(state.logs.slice(-20)))
166
- console.log('errors:', JSON.stringify(state.errors))
167
64
  ```
168
65
 
169
66
  ```
170
67
  exec:browser
171
- if (page.workers().length > 0) {
172
- const r = await page.workers()[0].evaluate(() => JSON.stringify({ type: 'worker alive' }))
173
- console.log(r)
174
- }
175
- ```
176
-
177
- exec:browser
178
- const result = await page.evaluate(() => JSON.stringify({
179
- entityCount: window.debug?.scene?.children?.length,
180
- playerId: window.debug?.client?.playerId
181
- }))
182
- console.log(result)
183
- ```
184
-
185
- exec:browser
186
- const start = Date.now()
187
- while (Date.now() - start < 12000) {
188
- const el = await page.$('#status')
189
- if (el) { console.log('found:', await el.textContent()); break }
190
- await new Promise(r => setTimeout(r, 300))
191
- }
68
+ console.log(JSON.stringify(state.logs.slice(-20)))
192
69
  ```
193
70
 
194
- ## Key Rules
71
+ ## Rules
195
72
 
196
- - `browser:` prefix → playwriter session management (one command per block)
197
- - `exec:browser` → JS in page context (multi-line JS allowed, 15s live window)
198
- - Never mix pathways in the same Bash call
199
- - `-e` argument: single quotes on outside, double quotes inside for JS strings
200
73
  - One `playwriter` command per `browser:` block
201
- - Never `await setTimeout(N)` with N > 10000 — use short poll loops instead
202
- - All running browser tasks drain automatically on every plugkit interaction
74
+ - Never mix pathways in same Bash call
75
+ - `exec:browser` = plain JS, no shell quoting
76
+ - All browser tasks drain automatically on every plugkit interaction
77
+ - Sessions reap after 5-15min idle; browser cleaned up on session end
@@ -3,53 +3,33 @@ name: code-search
3
3
  description: Mandatory codebase search workflow. Use whenever you need to find anything in the codebase. Start with two words, iterate by changing or adding words until found.
4
4
  ---
5
5
 
6
- # CODEBASE SEARCH — Mandatory Workflow
6
+ # CODEBASE SEARCH
7
7
 
8
- **Use gm subagents for all independent work items. Invoke all skills in the chain: planning gm-execute gm-emit → gm-complete → update-docs.**
8
+ `exec:codesearch` is the only codebase search tool. `Grep`, `Glob`, `Find`, `Explore`, `grep`/`rg`/`find` inside `exec:bash` = ALL hook-blocked. No fallback path.
9
9
 
10
+ Handles: exact symbols, exact strings, file-name fragments, regex-ish patterns, natural-language queries, PDF pages (cite `path/doc.pdf:<page>`).
10
11
 
11
- `exec:codesearch` is the only way to search the codebase. **`Grep`, `Glob`, `Find`, `Explore`, and `grep`/`rg`/`find`/`ripgrep` inside `exec:bash` are ALL hook-blocked.** There is no fallback path for exact matches, regex, or file-name patterns — codesearch handles all of them. If you find yourself reaching for Grep or Glob, that reflex is wrong; replace with codesearch.
12
-
13
- **What codesearch handles** (every codebase-lookup need lands here):
14
- - Exact identifier / symbol lookup (function names, class names, constants) — symbols are extracted and indexed separately, exact matches rank top.
15
- - Exact string content — query tokens >1 trigger a literal-substring boost in content scoring.
16
- - File-name fragments — file paths are tokenized and matched with a score boost.
17
- - Regex-ish patterns — BM25 tokenization covers snake_case, camelCase, dot/dash splits; matching component words returns the file.
18
- - Natural-language concept queries — BM25 + vector re-ranking handle "find the hook that blocks grep", "where is PR stats calculated", etc.
19
- - PDF pages — specs, papers, manuals, RFCs, datasheets, design docs extracted page-by-page into the same index. Cite `path/to/doc.pdf:<page>`.
20
-
21
- **Direct-read exceptions** (no search required):
22
- - Known absolute path → `Read` tool.
23
- - Listing a known directory → `exec:nodejs` + `fs.readdirSync`.
24
-
25
- Unscanned digital PDFs are a search gap — if you know a doc exists and it isn't returning, check it is not under an ignored dir and that extraction succeeded (encrypted / image-only PDFs yield empty chunks silently).
12
+ Direct-read exceptions: known absolute path `Read`. Known dir listing `exec:nodejs` + `fs.readdirSync`.
26
13
 
27
14
  ## Syntax
28
15
 
29
16
  ```
30
17
  exec:codesearch
31
- <natural language query>
18
+ <two-word query>
32
19
  ```
33
20
 
34
- ## Mandatory Search Protocol
35
-
36
- **Start with exactly two words.** Never start broader. Never start with one word.
21
+ ## Protocol
37
22
 
38
- **Iterate by changing or adding words** — do not switch approach or give up until the content is found:
23
+ 1. Start: exactly two words
24
+ 2. No results → change one word
25
+ 3. Still no → add third word
26
+ 4. Still no → swap changed word again
27
+ 5. Minimum 4 attempts before concluding absent
39
28
 
40
- 1. Start: two-word query most likely to match
41
- 2. No results → change one word (synonym, related term)
42
- 3. Still no results → add a third word (narrow scope)
43
- 4. Still no results → swap the changed word again
44
- 5. Keep iterating — changing or adding words each pass — until content is found
45
-
46
- **Never**: start with one word | start with a sentence | give up after one miss | switch to a different tool | declare content missing after fewer than 4 search attempts
47
-
48
- **Each search is one `exec:codesearch` call.** Run them sequentially — use each result to inform the next query.
29
+ Never: one word | full sentence | give up under 4 attempts | switch tools.
49
30
 
50
31
  ## Examples
51
32
 
52
- Finding where a function is defined:
53
33
  ```
54
34
  exec:codesearch
55
35
  session cleanup idle
@@ -59,23 +39,10 @@ session cleanup idle
59
39
  exec:codesearch
60
40
  cleanup sessions timeout
61
41
  ```
62
- → found.
63
-
64
- Finding config format:
65
- ```
66
- exec:codesearch
67
- plugin registration format
68
- ```
69
- → no results →
70
- ```
71
- exec:codesearch
72
- plugin config array
73
- ```
74
- → found.
75
42
 
76
- Finding content inside a spec PDF:
43
+ PDF search:
77
44
  ```
78
45
  exec:codesearch
79
46
  usb descriptor endpoint
80
47
  ```
81
- → returns `docs/usb-spec.pdf:42` — cite page, open via Read if you need the surrounding page text.
48
+ → returns `docs/usb-spec.pdf:42` — cite page, Read if you need surrounding text.
@@ -5,68 +5,54 @@ description: Create a lang/ plugin that wires any CLI tool or language runtime i
5
5
 
6
6
  # CREATE LANG PLUGIN
7
7
 
8
- **Use gm subagents for all independent work items. Invoke all skills in the chain: planning → gm-execute gm-emit gm-complete → update-docs.**
8
+ Single CommonJS file at `<projectDir>/lang/<id>.js`. Auto-discovered no hook editing.
9
9
 
10
-
11
- 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**.
12
-
13
- ## PLUGIN SHAPE
10
+ ## Plugin Shape
14
11
 
15
12
  ```js
16
13
  'use strict';
17
14
  module.exports = {
18
- id: 'mytool', // must match filename: lang/mytool.js
15
+ id: 'mytool', // must match filename
19
16
  exec: {
20
- match: /^exec:mytool/, // regex tested against full "exec:mytool\n<code>" string
21
- run(code, cwd) { // returns string or Promise<string>
22
- // ...
23
- }
17
+ match: /^exec:mytool/,
18
+ run(code, cwd) { /* returns string or Promise<string> */ }
24
19
  },
25
- lsp: { // optional — synchronous only
26
- check(fileContent, cwd) { // returns Diagnostic[] synchronously
27
- // ...
28
- }
20
+ lsp: { // optional — synchronous only
21
+ check(fileContent, cwd) { /* returns Diagnostic[] */ }
29
22
  },
30
- extensions: ['.ext'], // optional — file extensions lsp.check applies to
31
- context: `=== mytool ===\n...` // optional — string or () => string
23
+ extensions: ['.ext'], // optional — for lsp.check
24
+ context: `=== mytool ===\n...` // optional — string or () => string
32
25
  };
33
26
  ```
34
27
 
35
- ```ts
36
- type Diagnostic = { line: number; col: number; severity: 'error'|'warning'; message: string };
37
- ```
38
-
39
- ## HOW IT WORKS
40
-
41
- - **`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.
42
- - **`lsp.check`** is called synchronously in the hook process on each prompt submit — must NOT be async. Use `execFileSync` or `spawnSync`.
43
- - **`context`** is injected into every prompt's `additionalContext` (truncated to 2000 chars) and into the session-start context.
44
- - **`match`** regex is tested against the full command string `exec:mytool\n<code>` — keep it simple: `/^exec:mytool/`.
28
+ `type Diagnostic = { line: number; col: number; severity: 'error'|'warning'; message: string }`
45
29
 
46
- ## STEP 1 — IDENTIFY THE TOOL
30
+ ## How It Works
47
31
 
48
- Answer these before writing any code:
32
+ - `exec.run` child process, 30s timeout, async OK. Called when Claude writes `exec:mytool\n<code>`.
33
+ - `lsp.check` — synchronous, called per prompt submit. Use `spawnSync`/`execFileSync`. No async.
34
+ - `context` — injected into every prompt (truncated 2000 chars).
49
35
 
50
- 1. What is the tool's CLI name or npm package? (`gdlint`, `tsc`, `deno`, `ruff`, ...)
51
- 2. How do you run a single expression/snippet? (`tool eval <expr>`, `tool -e <code>`, HTTP POST, ...)
52
- 3. How do you run a file? (`tool run <file>`, `tool <file>`, ...)
53
- 4. Does it have a lint/check mode? What does its output format look like?
54
- 5. What file extensions does it apply to?
55
- 6. Is the game/server running required, or does it work headlessly?
36
+ ## Step 1 Identify Tool
56
37
 
57
- ## STEP 2 IMPLEMENT exec.run
38
+ 1. CLI name or npm package?
39
+ 2. Run single expression? (`tool eval <expr>`, `tool -e <code>`, HTTP POST...)
40
+ 3. Run file? (`tool run <file>`)
41
+ 4. Lint/check mode + output format?
42
+ 5. File extensions?
43
+ 6. Requires running server or headless?
58
44
 
59
- Pattern for **HTTP eval** (tool has a running server):
45
+ ## Step 2 exec.run Patterns
60
46
 
47
+ HTTP eval (running server):
61
48
  ```js
62
- const http = require('http');
63
49
  function httpPost(port, urlPath, body) {
64
50
  return new Promise((resolve, reject) => {
65
51
  const data = JSON.stringify(body);
66
52
  const req = http.request(
67
53
  { hostname: '127.0.0.1', port, path: urlPath, method: 'POST',
68
54
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
69
- (res) => { let raw = ''; res.on('data', c => raw += c); res.on('end', () => { try { resolve(JSON.parse(raw)); } catch { resolve({ raw }); } }); }
55
+ res => { let raw = ''; res.on('data', c => raw += c); res.on('end', () => resolve(JSON.parse(raw))); }
70
56
  );
71
57
  req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); });
72
58
  req.on('error', reject);
@@ -75,109 +61,65 @@ function httpPost(port, urlPath, body) {
75
61
  }
76
62
  ```
77
63
 
78
- Pattern for **file-based execution** (write temp file, run headlessly):
79
-
64
+ File-based (headless):
80
65
  ```js
81
- const fs = require('fs');
82
- const os = require('os');
83
- const path = require('path');
84
- const { execFileSync } = require('child_process');
85
-
86
66
  function runFile(code, cwd) {
87
67
  const tmp = path.join(os.tmpdir(), `plugin_${Date.now()}.ext`);
88
68
  fs.writeFileSync(tmp, code);
89
- try {
90
- return execFileSync('mytool', ['run', tmp], { cwd, encoding: 'utf8', timeout: 10000 });
91
- } finally {
92
- try { fs.unlinkSync(tmp); } catch (_) {}
93
- }
69
+ try { return execFileSync('mytool', ['run', tmp], { cwd, encoding: 'utf8', timeout: 10000 }); }
70
+ finally { try { fs.unlinkSync(tmp); } catch (_) {} }
94
71
  }
95
72
  ```
96
73
 
97
- **Distinguish single expression vs multi-line** when both modes exist:
98
-
74
+ Single expr detection:
99
75
  ```js
100
- function isSingleExpr(code) {
101
- return !code.trim().includes('\n') && !/\b(func|def|fn |class|import)\b/.test(code);
102
- }
76
+ const isSingleExpr = code => !code.trim().includes('\n') && !/\b(func|def|fn |class|import)\b/.test(code);
103
77
  ```
104
78
 
105
- ## STEP 3 — IMPLEMENT lsp.check (if applicable)
106
-
107
- Must be **synchronous**. Parse the tool's stderr/stdout for diagnostics:
79
+ ## Step 3 — lsp.check
108
80
 
109
81
  ```js
110
- const { spawnSync } = require('child_process');
111
- const fs = require('fs');
112
- const os = require('os');
113
- const path = require('path');
114
-
115
82
  function check(fileContent, cwd) {
116
83
  const tmp = path.join(os.tmpdir(), `lsp_${Math.random().toString(36).slice(2)}.ext`);
117
84
  try {
118
85
  fs.writeFileSync(tmp, fileContent);
119
86
  const r = spawnSync('mytool', ['check', tmp], { encoding: 'utf8', cwd });
120
- const output = r.stdout + r.stderr;
121
- return output.split('\n').reduce((acc, line) => {
87
+ return (r.stdout + r.stderr).split('\n').reduce((acc, line) => {
122
88
  const m = line.match(/^.+:(\d+):(\d+):\s+(error|warning):\s+(.+)$/);
123
- if (m) acc.push({ line: parseInt(m[1]), col: parseInt(m[2]), severity: m[3], message: m[4].trim() });
89
+ if (m) acc.push({ line: +m[1], col: +m[2], severity: m[3], message: m[4].trim() });
124
90
  return acc;
125
91
  }, []);
126
- } catch (_) {
127
- return [];
128
- } finally {
129
- try { fs.unlinkSync(tmp); } catch (_) {}
130
- }
92
+ } catch (_) { return []; }
93
+ finally { try { fs.unlinkSync(tmp); } catch (_) {} }
131
94
  }
132
95
  ```
133
96
 
134
- Common output patterns to parse:
135
- - `file:line:col: error: message` → standard
136
- - `file:line: E001: message` → gdlint style (`E`=error, `W`=warning)
137
- - JSON output → `JSON.parse(r.stdout).errors.map(...)`
138
-
139
- ## STEP 4 — WRITE context STRING
140
-
141
- Describe what `exec:<id>` does and when to use it. This appears in every prompt. Keep it under 300 chars:
97
+ ## Step 4 context String
142
98
 
99
+ Under 300 chars:
143
100
  ```js
144
- context: `=== mytool exec: support ===
145
- exec:mytool
146
- <expression or code block>
147
-
148
- Runs via <how>. Use for <when>.`
101
+ context: `=== mytool ===\nexec:mytool\n<expression>\n\nRuns via <how>. Use for <when>.`
149
102
  ```
150
103
 
151
- ## STEP 5 — WRITE THE FILE
152
-
153
- File goes at `lang/<id>.js` in the project root. The `id` field must match the filename (without `.js`).
154
-
155
- Verify after writing:
104
+ ## Step 5 — Write + Verify
156
105
 
157
106
  ```
158
107
  exec:nodejs
159
- const p = require('/abs/path/to/lang/mytool.js');
108
+ const p = require('/abs/path/lang/mytool.js');
160
109
  console.log(p.id, typeof p.exec.run, p.exec.match.toString());
161
110
  ```
162
111
 
163
112
  Then test dispatch:
164
-
165
113
  ```
166
114
  exec:mytool
167
- <a simple test expression>
115
+ <simple test expression>
168
116
  ```
169
117
 
170
- If it returns `exec:mytool output:` → working. If it errors → fix `exec.run`.
171
-
172
- ## CONSTRAINTS
173
-
174
- - `exec.run` may be async — it runs in a child process with a 30s timeout
175
- - `lsp.check` must be synchronous — no Promises, no async/await
176
- - Plugin must be CommonJS (`module.exports = { ... }`) — no ES module syntax
177
- - No persistent processes — `exec.run` must complete and exit cleanly
178
- - `id` must match the filename exactly
179
- - First match wins — if multiple plugins could match, make `match` specific
180
-
181
- ## EXAMPLE — gdscript plugin (reference implementation)
118
+ ## Constraints
182
119
 
183
- 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.
120
+ - `exec.run` async OK (30s timeout)
121
+ - `lsp.check` synchronous only — no Promises
122
+ - CommonJS only — no ES module syntax
123
+ - No persistent processes
124
+ - `id` must match filename exactly
125
+ - First match wins — make `match` specific