cwtools-mcp 0.2.3 → 0.2.5

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.
package/README.md CHANGED
@@ -95,9 +95,8 @@ intentionally does not silently fall back to a separate language server.
95
95
 
96
96
  ### Optional Standalone Mode
97
97
 
98
- Use standalone mode only when you explicitly want the legacy behavior: the MCP
99
- process starts its own CWTools language server and builds its own diagnostic
100
- state.
98
+ Use standalone mode only when you explicitly want the MCP process to start its
99
+ own CWTools language server and build an independent diagnostic state.
101
100
 
102
101
  ```sh
103
102
  cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --stdio
package/dist/config.js CHANGED
@@ -189,7 +189,7 @@ function helpText() {
189
189
  ' or --workspace when supplied) must match the bridge workspace exactly. If the',
190
190
  ' compatible host is closed or the workspace does not match, tool calls return an',
191
191
  ' actionable unavailable error.',
192
- ' Use --standalone only when you intentionally want the legacy self-hosted LSP mode.',
192
+ ' Use --standalone only when you intentionally want the standalone self-hosted LSP mode.',
193
193
  '',
194
194
  'Vanilla data (needed for vanilla IDs and correct mod-vs-vanilla diagnostics):',
195
195
  ' (auto) If neither flag is given, the VS Code cwtools extension cache',
@@ -197,7 +197,7 @@ ${helpText()}`);
197
197
  " or --workspace when supplied) must match the bridge workspace exactly. If the",
198
198
  " compatible host is closed or the workspace does not match, tool calls return an",
199
199
  " actionable unavailable error.",
200
- " Use --standalone only when you intentionally want the legacy self-hosted LSP mode.",
200
+ " Use --standalone only when you intentionally want the standalone self-hosted LSP mode.",
201
201
  "",
202
202
  "Vanilla data (needed for vanilla IDs and correct mod-vs-vanilla diagnostics):",
203
203
  " (auto) If neither flag is given, the VS Code cwtools extension cache",
@@ -458,14 +458,16 @@ var require_mcpTools = __commonJS({
458
458
  "properties": {
459
459
  "file": {
460
460
  "type": "string",
461
- "description": "Absolute file path"
461
+ "description": "Absolute path inside the workspace or a configured game installation."
462
462
  },
463
463
  "line": {
464
- "type": "number",
464
+ "type": "integer",
465
+ "minimum": 0,
465
466
  "description": "Line number (0-based)"
466
467
  },
467
468
  "column": {
468
- "type": "number",
469
+ "type": "integer",
470
+ "minimum": 0,
469
471
  "description": "Column number (0-based)"
470
472
  }
471
473
  },
@@ -473,7 +475,8 @@ var require_mcpTools = __commonJS({
473
475
  "file",
474
476
  "line",
475
477
  "column"
476
- ]
478
+ ],
479
+ "additionalProperties": false
477
480
  }
478
481
  },
479
482
  "registry": {
@@ -558,10 +561,6 @@ var require_mcpTools = __commonJS({
558
561
  "toolResult": {
559
562
  "type": "object",
560
563
  "description": "Optional raw failed tool result to classify without querying diagnostics again."
561
- },
562
- "reflection": {
563
- "type": "string",
564
- "description": "Optional legacy free-form analysis from the model. Kept for backward compatibility; the host still returns deterministic routing advice."
565
564
  }
566
565
  },
567
566
  "required": []
@@ -579,7 +578,7 @@ var require_mcpTools = __commonJS({
579
578
  {
580
579
  "tool": {
581
580
  "name": "query_project_profile",
582
- "description": "Read the /init-generated Agent project profile from .cwtools/project/profile.json. Use this before broad scans to get workspace type, key directories, localisation languages/encoding, namespaces, workflow routing, validation hints, and mode-specific prompt cards.",
581
+ "description": "Read the /init-generated Agent project profile from .cwtools/project/profile.json. Use this before broad scans to get workspace type, key directories, localisation languages/encoding, namespaces, workflow routing, validation hints, and targeted guidance cards.",
583
582
  "inputSchema": {
584
583
  "type": "object",
585
584
  "properties": {
@@ -593,29 +592,25 @@ var require_mcpTools = __commonJS({
593
592
  "identifiers",
594
593
  "validation",
595
594
  "compatibility",
596
- "promptCards",
595
+ "guidanceCards",
597
596
  "all"
598
597
  ],
599
598
  "description": "Targeted profile section to return. Default summary. Use all only when you need the whole profile."
600
599
  },
601
- "mode": {
600
+ "guidance": {
602
601
  "type": "string",
603
602
  "enum": [
604
- "build",
605
- "plan",
606
- "explore",
607
- "general",
608
- "utility",
603
+ "implementation",
604
+ "planning",
605
+ "exploration",
609
606
  "review",
610
- "gui_expert",
611
- "script_reviewer",
612
- "loc_translator",
613
- "loc_writer",
614
- "orchestrator",
615
- "script",
616
- "asset"
607
+ "utility",
608
+ "localisation",
609
+ "assets",
610
+ "coordination",
611
+ "paradox_coordination"
617
612
  ],
618
- "description": "Optional mode card to return, e.g. build, plan, loc_writer, asset, orchestrator, script."
613
+ "description": "Optional guidance card to return for the current task concern."
619
614
  }
620
615
  },
621
616
  "required": []
@@ -788,7 +783,7 @@ var require_mcpTools = __commonJS({
788
783
  },
789
784
  "file": {
790
785
  "type": "string",
791
- "description": "Optional workspace-relative or absolute file path used to restrict entry points."
786
+ "description": "Optional workspace-relative path or absolute path inside the workspace/configured game installation used to restrict entry points."
792
787
  },
793
788
  "typeName": {
794
789
  "type": "string",
@@ -857,7 +852,7 @@ var require_mcpTools = __commonJS({
857
852
  },
858
853
  "file": {
859
854
  "type": "string",
860
- "description": "Caller file; narrows to one invocation."
855
+ "description": "Caller file inside the workspace or a configured game installation; narrows to one invocation."
861
856
  },
862
857
  "line": {
863
858
  "type": "number",
@@ -883,13 +878,13 @@ var require_mcpTools = __commonJS({
883
878
  {
884
879
  "tool": {
885
880
  "name": "analyze_pdx_flow",
886
- "description": "Static cost model and gameplay relations for a file or definition: traversals (every_*/while), pulse handlers, nested fan-out, and typed edges (tech prerequisites, special project events, megastructure upgrades). Relative weights only.",
881
+ "description": "Static flow model for a file or definition: dynamic event subtypes, inline-instantiated state/calls, cross-event and scripted-effect state flow, pulse/on_action/scripted-effect cost propagation, traversals, loops, and typed gameplay edges. Costs are relative static scores, not runtime predictions.",
887
882
  "inputSchema": {
888
883
  "type": "object",
889
884
  "properties": {
890
885
  "file": {
891
886
  "type": "string",
892
- "description": "Optional file path to analyze; when omitted a definitionId must be given."
887
+ "description": "Optional path inside the workspace or a configured game installation; when omitted a definitionId must be given."
893
888
  },
894
889
  "definitionId": {
895
890
  "type": "string",
@@ -1014,13 +1009,13 @@ var require_mcpTools = __commonJS({
1014
1009
  {
1015
1010
  "tool": {
1016
1011
  "name": "get_pdx_block",
1017
- "description": "Extract exactly one complete AST block by symbol name. Works with .txt (events, common), .gui (containerWindowType by name), and .gfx (pdxmesh by name). Returns 1-based startLine/endLine that can be passed directly to replace_lines. If the symbol is not found, the error response includes a full list of available symbols with line ranges so you can retry with the correct name.",
1012
+ "description": "Extract exactly one complete AST block by symbol name from the workspace or a configured game installation. Works with .txt (events, common), .gui (containerWindowType by name), and .gfx (pdxmesh by name). Returns 1-based startLine/endLine that can be passed directly to replace_lines. If the symbol is not found, the error response includes a full list of available symbols with line ranges so you can retry with the correct name.",
1018
1013
  "inputSchema": {
1019
1014
  "type": "object",
1020
1015
  "properties": {
1021
1016
  "file": {
1022
1017
  "type": "string",
1023
- "description": "Absolute file path"
1018
+ "description": "Absolute path inside the workspace or a configured game installation."
1024
1019
  },
1025
1020
  "symbol": {
1026
1021
  "type": "string",
@@ -1051,7 +1046,7 @@ var require_mcpTools = __commonJS({
1051
1046
  "properties": {
1052
1047
  "file": {
1053
1048
  "type": "string",
1054
- "description": "Absolute file path (must be in the workspace)"
1049
+ "description": "Absolute file path inside the workspace or a configured game installation."
1055
1050
  },
1056
1051
  "line": {
1057
1052
  "type": "number",
@@ -1091,7 +1086,7 @@ var require_mcpTools = __commonJS({
1091
1086
  "properties": {
1092
1087
  "file": {
1093
1088
  "type": "string",
1094
- "description": "Absolute file path"
1089
+ "description": "Absolute path inside the workspace or a configured game installation."
1095
1090
  }
1096
1091
  },
1097
1092
  "required": [
@@ -1140,33 +1135,34 @@ var require_mcpTools = __commonJS({
1140
1135
  },
1141
1136
  {
1142
1137
  "tool": {
1143
- "name": "query_definition",
1144
- "description": "GoToDefinition at a position, or FindAllRefs if no definition exists. Uses CWTools AST - faster than grep. If you know the symbol name, prefer query_definition_by_name instead.",
1138
+ "name": "go_to_definition",
1139
+ "description": "Find a definition by exact symbol name or by a 0-based file position. Uses CWTools semantic lookup when available and falls back to the active VS Code language provider.",
1145
1140
  "inputSchema": {
1146
1141
  "type": "object",
1147
1142
  "properties": {
1143
+ "symbolName": {
1144
+ "type": "string",
1145
+ "description": "Exact symbol name. Use this instead of file/line/column when the identifier is known."
1146
+ },
1148
1147
  "file": {
1149
1148
  "type": "string",
1150
- "description": "Absolute file path"
1149
+ "description": "Absolute file path inside the workspace or a configured game installation."
1151
1150
  },
1152
1151
  "line": {
1153
1152
  "type": "number",
1154
- "description": "Line number (0-based)"
1153
+ "description": "Line number (0-based)."
1155
1154
  },
1156
1155
  "column": {
1157
1156
  "type": "number",
1158
- "description": "Column number (0-based)"
1157
+ "description": "Column number (0-based)."
1159
1158
  }
1160
1159
  },
1161
- "required": [
1162
- "file",
1163
- "line",
1164
- "column"
1165
- ]
1160
+ "required": [],
1161
+ "additionalProperties": false
1166
1162
  }
1167
1163
  },
1168
1164
  "registry": {
1169
- "name": "query_definition",
1165
+ "name": "go_to_definition",
1170
1166
  "isWrite": false,
1171
1167
  "isReadOnly": true,
1172
1168
  "effect": "workspace_read",
@@ -1176,53 +1172,38 @@ var require_mcpTools = __commonJS({
1176
1172
  },
1177
1173
  {
1178
1174
  "tool": {
1179
- "name": "query_definition_by_name",
1180
- "description": "Find where a named symbol is defined - no file/position needed. Returns file path and line number. Works for any top-level PDXScript key (events, triggers, effects, etc.).",
1181
- "inputSchema": {
1182
- "type": "object",
1183
- "properties": {
1184
- "symbolName": {
1185
- "type": "string",
1186
- "description": 'The exact name of the symbol to find (e.g. "kuat_has_psionic_research", "distar.001")'
1187
- }
1188
- },
1189
- "required": [
1190
- "symbolName"
1191
- ]
1192
- }
1193
- },
1194
- "registry": {
1195
- "name": "query_definition_by_name",
1196
- "isWrite": false,
1197
- "isReadOnly": true,
1198
- "effect": "workspace_read",
1199
- "riskLevel": 0,
1200
- "concurrencyClass": "parallel"
1201
- }
1202
- },
1203
- {
1204
- "tool": {
1205
- "name": "query_references",
1206
- "description": "Find references to an identifier through the active language provider, with a bounded workspace text-search fallback.",
1175
+ "name": "find_references",
1176
+ "description": "Find references by exact identifier or by a 0-based file position. Identifier lookup uses workspace symbols plus a bounded text fallback; position lookup uses the active VS Code language provider.",
1207
1177
  "inputSchema": {
1208
1178
  "type": "object",
1209
1179
  "properties": {
1210
1180
  "identifier": {
1211
1181
  "type": "string",
1212
- "description": "The identifier to search for"
1182
+ "description": "Exact identifier. Use this instead of file/line/column when the name is known."
1213
1183
  },
1214
1184
  "file": {
1215
1185
  "type": "string",
1216
- "description": "Optional file to limit search to"
1186
+ "description": "Absolute file path inside the workspace or a configured game installation."
1187
+ },
1188
+ "line": {
1189
+ "type": "number",
1190
+ "description": "Line number (0-based)."
1191
+ },
1192
+ "column": {
1193
+ "type": "number",
1194
+ "description": "Column number (0-based)."
1195
+ },
1196
+ "limit": {
1197
+ "type": "number",
1198
+ "description": "Maximum references to return (default 100, max 500)."
1217
1199
  }
1218
1200
  },
1219
- "required": [
1220
- "identifier"
1221
- ]
1201
+ "required": [],
1202
+ "additionalProperties": false
1222
1203
  }
1223
1204
  },
1224
1205
  "registry": {
1225
- "name": "query_references",
1206
+ "name": "find_references",
1226
1207
  "isWrite": false,
1227
1208
  "isReadOnly": true,
1228
1209
  "effect": "workspace_read",
@@ -1375,7 +1356,7 @@ var require_mcpTools = __commonJS({
1375
1356
  "properties": {
1376
1357
  "file": {
1377
1358
  "type": "string",
1378
- "description": "Absolute file path (must be a parsed mod file)"
1359
+ "description": "Absolute path to a parsed file inside the workspace or a configured game installation."
1379
1360
  }
1380
1361
  },
1381
1362
  "required": [
@@ -1444,7 +1425,7 @@ var require_mcpTools = __commonJS({
1444
1425
  "properties": {
1445
1426
  "file": {
1446
1427
  "type": "string",
1447
- "description": "Absolute or workspace-relative path of the .shader/.fxh file."
1428
+ "description": "Workspace-relative path or absolute path inside the workspace/configured game installation."
1448
1429
  }
1449
1430
  },
1450
1431
  "required": [
@@ -1470,7 +1451,7 @@ var require_mcpTools = __commonJS({
1470
1451
  "properties": {
1471
1452
  "file": {
1472
1453
  "type": "string",
1473
- "description": "Absolute or workspace-relative path of the .shader/.fxh file."
1454
+ "description": "Workspace-relative path or absolute path inside the workspace/configured game installation."
1474
1455
  }
1475
1456
  },
1476
1457
  "required": [
@@ -1530,7 +1511,7 @@ var require_mcpTools = __commonJS({
1530
1511
  },
1531
1512
  "file": {
1532
1513
  "type": "string",
1533
- "description": "Absolute or workspace-relative path of a .shader/.fxh file; lists its Effects with cursor pagination."
1514
+ "description": "Workspace-relative path or absolute path inside the workspace/configured game installation; lists its Effects with cursor pagination."
1534
1515
  },
1535
1516
  "limit": {
1536
1517
  "type": "number",
@@ -1562,7 +1543,7 @@ var require_mcpTools = __commonJS({
1562
1543
  "properties": {
1563
1544
  "file": {
1564
1545
  "type": "string",
1565
- "description": "Absolute or workspace-relative path of the .shader/.fxh file."
1546
+ "description": "Workspace-relative path or absolute path inside the workspace/configured game installation."
1566
1547
  }
1567
1548
  },
1568
1549
  "required": [
@@ -1592,7 +1573,7 @@ var require_mcpTools = __commonJS({
1592
1573
  },
1593
1574
  "file": {
1594
1575
  "type": "string",
1595
- "description": "Absolute or workspace-relative path of a .shader/.fxh file."
1576
+ "description": "Workspace-relative path or absolute path inside the workspace/configured game installation."
1596
1577
  }
1597
1578
  },
1598
1579
  "required": []
@@ -1709,10 +1690,9 @@ var require_readiness = __commonJS({
1709
1690
  "parse_pdx_fragment",
1710
1691
  "query_scope",
1711
1692
  "get_completion_at",
1712
- "query_definition",
1713
- "query_definition_by_name",
1693
+ "go_to_definition",
1714
1694
  "explore_pdx_project",
1715
- "query_references",
1695
+ "find_references",
1716
1696
  "get_diagnostics",
1717
1697
  "query_scripted_effects",
1718
1698
  "query_scripted_triggers",
@@ -1787,11 +1767,10 @@ var require_vanillaCache = __commonJS({
1787
1767
  "query_types",
1788
1768
  "get_completion_at",
1789
1769
  "query_scope",
1790
- "query_definition",
1791
- "query_definition_by_name",
1770
+ "go_to_definition",
1792
1771
  "explore_pdx_project",
1793
1772
  "query_project_knowledge",
1794
- "query_references",
1773
+ "find_references",
1795
1774
  "get_diagnostics",
1796
1775
  "query_scripted_effects",
1797
1776
  "query_scripted_triggers",
@@ -1903,7 +1882,7 @@ var require_diagnosticRouting = __commonJS({
1903
1882
  code,
1904
1883
  category: "general",
1905
1884
  explanation: "No specialized diagnostic route matched; use LSP and indexed evidence before editing.",
1906
- suggestedTools: ["get_diagnostics", "explore_pdx_project", "query_cwt_schema", "query_definition_by_name", "query_workspace_index"],
1885
+ suggestedTools: ["get_diagnostics", "explore_pdx_project", "query_cwt_schema", "go_to_definition", "query_workspace_index"],
1907
1886
  nextSteps: [
1908
1887
  "Group similar diagnostics and inspect a representative block.",
1909
1888
  "Verify identifiers through indexed tools before making changes."
@@ -2015,9 +1994,10 @@ var require_rules = __commonJS({
2015
1994
  async function queryRulesWithHost(host, args) {
2016
1995
  const { cache, meta } = await loadCwtRulesMemoized(host);
2017
1996
  let rules = args.category === "trigger" ? cache.triggers : args.category === "effect" ? cache.effects : args.category === "modifier" ? cache.modifiers : cache.scopeChanges;
1997
+ const dottedScopeChain = args.name && args.category === "scope_change" ? buildDottedScopeChainRule(args.name, args.scope, cache.scopeChanges) : void 0;
2018
1998
  if (args.name) {
2019
- const needle = args.name.toLowerCase();
2020
- const filtered = rules.filter((rule) => rule.name.toLowerCase().includes(needle));
1999
+ const needle = normalizeRuleNameQuery(args.name, args.category);
2000
+ const filtered = rules.filter((rule) => rule.name.toLowerCase().includes(needle)).sort((a, b) => scoreRuleNameMatch(a.name, needle) - scoreRuleNameMatch(b.name, needle) || a.name.localeCompare(b.name));
2021
2001
  if (filtered.length === 0 && rules.length > 0) {
2022
2002
  rules = rules.map((rule) => ({ rule, score: levenshtein(needle, rule.name.toLowerCase()) })).sort((a, b) => a.score - b.score).slice(0, 5).map((item) => ({
2023
2003
  ...item.rule,
@@ -2027,6 +2007,12 @@ var require_rules = __commonJS({
2027
2007
  rules = filtered;
2028
2008
  }
2029
2009
  }
2010
+ if (dottedScopeChain) {
2011
+ rules = [
2012
+ dottedScopeChain,
2013
+ ...rules.filter((rule) => rule.name !== dottedScopeChain.name)
2014
+ ];
2015
+ }
2030
2016
  if (args.scope) {
2031
2017
  const scope = args.scope.toLowerCase();
2032
2018
  rules = rules.filter((rule) => rule.scopes.length === 0 || rule.scopes.some((candidate) => {
@@ -2264,13 +2250,41 @@ var require_rules = __commonJS({
2264
2250
  }
2265
2251
  async function findCwtSchemaFiles(host, root, maxFiles) {
2266
2252
  if (host.rules?.listCwtFiles) {
2267
- return (await host.rules.listCwtFiles(root, { limit: maxFiles })).slice(0, maxFiles);
2253
+ return (await host.rules.listCwtFiles(root, { limit: maxFiles })).slice(0, maxFiles).sort((a, b) => a.localeCompare(b));
2254
+ }
2255
+ if (fs.existsSync(root)) {
2256
+ const results2 = [];
2257
+ const ignoredDirs2 = /* @__PURE__ */ new Set([".git", "node_modules", "logs", "config"]);
2258
+ const walkDisk = (dir, depth) => {
2259
+ if (results2.length >= maxFiles || depth > 8)
2260
+ return;
2261
+ let entries;
2262
+ try {
2263
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2264
+ } catch {
2265
+ return;
2266
+ }
2267
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2268
+ for (const entry of entries) {
2269
+ if (results2.length >= maxFiles)
2270
+ break;
2271
+ const fullPath = path.join(dir, entry.name);
2272
+ if (entry.isDirectory()) {
2273
+ if (!ignoredDirs2.has(entry.name))
2274
+ walkDisk(fullPath, depth + 1);
2275
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".cwt")) {
2276
+ results2.push(fullPath);
2277
+ }
2278
+ }
2279
+ };
2280
+ walkDisk(root, 0);
2281
+ return results2.sort((a, b) => a.localeCompare(b));
2268
2282
  }
2269
2283
  const rootRelative = workspaceRelativePath(host.workspaceRoot, root);
2270
2284
  if (!rootRelative)
2271
2285
  return [];
2272
2286
  const results = [];
2273
- const ignoredDirs = /* @__PURE__ */ new Set([".git", "node_modules", "logs"]);
2287
+ const ignoredDirs = /* @__PURE__ */ new Set([".git", "node_modules", "logs", "config"]);
2274
2288
  const walk = async (relativeDir, depth) => {
2275
2289
  if (results.length >= maxFiles || depth > 8)
2276
2290
  return;
@@ -2280,6 +2294,7 @@ var require_rules = __commonJS({
2280
2294
  } catch {
2281
2295
  return;
2282
2296
  }
2297
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2283
2298
  for (const entry of entries) {
2284
2299
  if (results.length >= maxFiles)
2285
2300
  break;
@@ -2294,7 +2309,16 @@ var require_rules = __commonJS({
2294
2309
  }
2295
2310
  };
2296
2311
  await walk(rootRelative, 0);
2297
- return results;
2312
+ return results.sort((a, b) => a.localeCompare(b));
2313
+ }
2314
+ async function collectCwtRuleSourceFiles(host, root, includeMissingLogs) {
2315
+ const files = await findCwtSchemaFiles(host, root, CWT_RULE_FILE_SCAN_LIMIT);
2316
+ for (const relativeLog of CWT_RULE_LOG_CANDIDATES) {
2317
+ const fullPath = path.join(root, relativeLog);
2318
+ if (includeMissingLogs || fs.existsSync(fullPath))
2319
+ files.push(fullPath);
2320
+ }
2321
+ return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
2298
2322
  }
2299
2323
  function workspaceRelativePath(workspaceRoot, fullPath) {
2300
2324
  const relative = path.relative(workspaceRoot, fullPath);
@@ -2627,29 +2651,19 @@ var require_rules = __commonJS({
2627
2651
  score += 20;
2628
2652
  return score;
2629
2653
  }
2630
- var CWT_RULE_FILE_CANDIDATES = [
2631
- "scopes.cwt",
2654
+ var CWT_RULE_LOG_CANDIDATES = [
2632
2655
  path.join("logs", "trigger_docs.log"),
2633
- path.join("logs", "modifiers.log"),
2634
- "triggers.cwt",
2635
- "trigger.cwt",
2636
- path.join("generated", "triggers.generated.cwt"),
2637
- "effects.cwt",
2638
- "effect.cwt",
2639
- path.join("generated", "effects.generated.cwt"),
2640
- "modifier.cwt",
2641
- "scope_changes.cwt",
2642
- path.join("generated", "scope_changes.generated.cwt")
2656
+ path.join("logs", "modifiers.log")
2643
2657
  ];
2658
+ var CWT_RULE_FILE_SCAN_LIMIT = 5e3;
2644
2659
  var CWT_RULES_MEMO_MAX_ENTRIES = 8;
2645
2660
  var CWT_RULES_MEMO_REFRESH_MS = 3e4;
2646
2661
  var cwtRulesMemo = /* @__PURE__ */ new Map();
2647
- function computeRulesSignature(configPaths) {
2662
+ async function computeRulesSignature(host, configPaths) {
2648
2663
  const parts = [];
2649
2664
  let sawDiskFiles = false;
2650
2665
  for (const configPath of configPaths) {
2651
- for (const file of CWT_RULE_FILE_CANDIDATES) {
2652
- const fullPath = path.join(configPath, file);
2666
+ for (const fullPath of await collectCwtRuleSourceFiles(host, configPath, true)) {
2653
2667
  try {
2654
2668
  const stat = fs.statSync(fullPath);
2655
2669
  parts.push(`${fullPath}:${stat.mtimeMs}:${stat.size}`);
@@ -2664,8 +2678,8 @@ var require_rules = __commonJS({
2664
2678
  async function computeRulesContentHash(host, configPaths) {
2665
2679
  const hash = crypto2.createHash("sha256");
2666
2680
  for (const configPath of configPaths) {
2667
- for (const file of CWT_RULE_FILE_CANDIDATES) {
2668
- const read = await readRulesTextFile(host, path.join(configPath, file)).catch(() => ({ exists: false, content: "", hasBom: false }));
2681
+ for (const file of await collectCwtRuleSourceFiles(host, configPath, true)) {
2682
+ const read = await readRulesTextFile(host, file).catch(() => ({ exists: false, content: "", hasBom: false }));
2669
2683
  if (!read.exists)
2670
2684
  continue;
2671
2685
  hash.update(`${read.content.length}:`);
@@ -2680,7 +2694,7 @@ var require_rules = __commonJS({
2680
2694
  async function loadCwtRulesMemoized(host) {
2681
2695
  const configPaths = await resolveRulesConfigPaths(host);
2682
2696
  const hostKey = cwtRulesHostKey(host);
2683
- const { signature, sawDiskFiles } = computeRulesSignature(configPaths);
2697
+ const { signature, sawDiskFiles } = await computeRulesSignature(host, configPaths);
2684
2698
  const memo = cwtRulesMemo.get(hostKey);
2685
2699
  if (memo && memo.signature === signature && (memo.sawDiskFiles || host.now() - memo.computedAt < CWT_RULES_MEMO_REFRESH_MS)) {
2686
2700
  return { cache: memo.cache, meta: { generation: memo.generation, contentHash: memo.contentHash } };
@@ -2711,10 +2725,25 @@ var require_rules = __commonJS({
2711
2725
  const docs = triggerDocs.exists ? parseDocsLog(triggerDocs.content, path.join(configPath, "logs", "trigger_docs.log")) : /* @__PURE__ */ new Map();
2712
2726
  const scopesRead = await readRulesTextFile(host, path.join(configPath, "scopes.cwt")).catch(() => ({ exists: false, content: "", hasBom: false }));
2713
2727
  const scopes = scopesRead.exists ? parseScopesFile(scopesRead.content, path.join(configPath, "scopes.cwt")) : /* @__PURE__ */ new Map();
2714
- const triggers = await readRuleFiles(host, configPath, ["triggers.cwt", "trigger.cwt", path.join("generated", "triggers.generated.cwt")], "trigger", docs, scopes);
2715
- const effects = await readRuleFiles(host, configPath, ["effects.cwt", "effect.cwt", path.join("generated", "effects.generated.cwt")], "effect", docs, scopes);
2716
- const scopeChanges = await readRuleFiles(host, configPath, ["scope_changes.cwt", path.join("generated", "scope_changes.generated.cwt")], "scope_change", docs, scopes);
2717
- const modifierAliases = await readRuleFiles(host, configPath, ["modifier.cwt"], "modifier", docs, scopes);
2728
+ const triggers = [];
2729
+ const effects = [];
2730
+ const scopeChanges = [];
2731
+ const modifierAliases = [];
2732
+ for (const file of await findCwtSchemaFiles(host, configPath, CWT_RULE_FILE_SCAN_LIMIT)) {
2733
+ const relativeFile = path.relative(configPath, file).replace(/\\/g, "/");
2734
+ const parsed = await readRulesFile(host, file, scopeChangeFileCategoryOverride(relativeFile), docs, scopes);
2735
+ for (const rule of parsed) {
2736
+ if (rule.category === "trigger")
2737
+ triggers.push(rule);
2738
+ else if (rule.category === "effect")
2739
+ effects.push(rule);
2740
+ else if (rule.category === "modifier")
2741
+ modifierAliases.push(rule);
2742
+ else
2743
+ scopeChanges.push(rule);
2744
+ }
2745
+ scopeChanges.push(...await readLinksFile(host, file, scopes));
2746
+ }
2718
2747
  const modifierLog = await readModifiersLog(host, path.join(configPath, "logs", "modifiers.log"));
2719
2748
  const modifiers = [...modifierAliases];
2720
2749
  const modifierNames = new Set(modifiers.map((rule) => rule.name.toLowerCase()));
@@ -2768,12 +2797,8 @@ var require_rules = __commonJS({
2768
2797
  return normalizeGameId(game.id);
2769
2798
  }
2770
2799
  async function readProjectProfileFile(host) {
2771
- let profilePath = path.join(host.workspaceRoot, ".cwtools", "project", "profile.json");
2772
- let read = await host.filesystem.readTextFile(profilePath).catch(() => ({ exists: false, content: "", hasBom: false }));
2773
- if (!read.exists) {
2774
- const legacyPath = path.join(host.workspaceRoot, ".cwtools-ai", "project", "profile.json");
2775
- read = await host.filesystem.readTextFile(legacyPath).catch(() => ({ exists: false, content: "", hasBom: false }));
2776
- }
2800
+ const profilePath = path.join(host.workspaceRoot, ".cwtools", "project", "profile.json");
2801
+ const read = await host.filesystem.readTextFile(profilePath).catch(() => ({ exists: false, content: "", hasBom: false }));
2777
2802
  if (!read.exists)
2778
2803
  return null;
2779
2804
  try {
@@ -2798,19 +2823,16 @@ var require_rules = __commonJS({
2798
2823
  return host.rules.readTextFile(filePath);
2799
2824
  return host.filesystem.readTextFile(filePath);
2800
2825
  }
2801
- async function readRuleFiles(host, configPath, relativeFiles, category, docs, scopes) {
2802
- const rules = [];
2803
- for (const relativeFile of relativeFiles) {
2804
- rules.push(...await readRulesFile(host, path.join(configPath, relativeFile), category, docs, scopes));
2805
- }
2806
- return rules;
2807
- }
2808
2826
  async function readRulesFile(host, filePath, category, docs, scopes) {
2809
2827
  const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: "", hasBom: false }));
2810
2828
  if (!read.exists)
2811
2829
  return [];
2812
2830
  return parseCwtFile(read.content, filePath, category, docs, scopes);
2813
2831
  }
2832
+ function scopeChangeFileCategoryOverride(relativeRuleFile) {
2833
+ const base = path.posix.basename(relativeRuleFile.replace(/\\/g, "/")).toLowerCase();
2834
+ return base === "scope_changes.cwt" || base === "scope_changes.generated.cwt" || base === "scope_change.cwt" ? "scope_change" : void 0;
2835
+ }
2814
2836
  async function readModifiersLog(host, filePath) {
2815
2837
  const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: "", hasBom: false }));
2816
2838
  if (!read.exists)
@@ -2921,7 +2943,7 @@ var require_rules = __commonJS({
2921
2943
  }
2922
2944
  return scopes;
2923
2945
  }
2924
- function parseCwtFile(content, filePath, category, docs, scopes) {
2946
+ function parseCwtFile(content, filePath, categoryOverride, docs, scopes) {
2925
2947
  const results = [];
2926
2948
  let currentScopes = [];
2927
2949
  let currentSupportedScopes = [];
@@ -2966,13 +2988,15 @@ var require_rules = __commonJS({
2966
2988
  currentDesc = comment;
2967
2989
  continue;
2968
2990
  }
2969
- const nameMatch = line.match(/^alias\[(?:trigger|effect|modifier):([^\]]+)\]\s*=\s*(.*)/);
2970
- if (nameMatch?.[1]) {
2971
- const name = nameMatch[1];
2991
+ const nameMatch = line.match(/^alias\[(trigger|effect|modifier):([^\]]+)\]\s*=\s*(.*)/);
2992
+ if (nameMatch?.[1] && nameMatch[2]) {
2993
+ const aliasKind = nameMatch[1];
2994
+ const category = categoryOverride ?? aliasKind;
2995
+ const name = nameMatch[2];
2972
2996
  const doc = docs.get(name);
2973
2997
  const cwtBlockText = collectCwtBlockText(lines, i);
2974
2998
  const scopesForRule = doc?.scopes.length ? doc.scopes : currentSupportedScopes.length ? currentSupportedScopes : currentScopes;
2975
- const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[2] ?? "");
2999
+ const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[3] ?? "");
2976
3000
  const description = doc?.description || currentDesc;
2977
3001
  const semanticHints = buildSemanticHints({
2978
3002
  description,
@@ -3016,6 +3040,91 @@ var require_rules = __commonJS({
3016
3040
  }
3017
3041
  return results;
3018
3042
  }
3043
+ async function readLinksFile(host, filePath, scopes) {
3044
+ const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: "", hasBom: false }));
3045
+ if (!read.exists || !/^\s*links\s*=\s*\{/im.test(read.content))
3046
+ return [];
3047
+ return parseLinksCwtFile(read.content, filePath, scopes);
3048
+ }
3049
+ function parseLinksCwtFile(content, filePath, scopes) {
3050
+ const results = [];
3051
+ const lines = content.split(/\r?\n/);
3052
+ let inLinks = false;
3053
+ let depth = 0;
3054
+ let current;
3055
+ for (let i = 0; i < lines.length; i++) {
3056
+ const rawLine = lines[i] ?? "";
3057
+ const line = stripCwtLineComment(rawLine).trim();
3058
+ if (!inLinks) {
3059
+ if (/^links\s*=\s*\{/.test(line)) {
3060
+ inLinks = true;
3061
+ depth = countBraceDelta(line);
3062
+ }
3063
+ continue;
3064
+ }
3065
+ if (!current && depth === 1) {
3066
+ const linkMatch = line.match(/^([A-Za-z_][\w.-]*)\s*=\s*\{\s*$/);
3067
+ if (linkMatch?.[1])
3068
+ current = { name: linkMatch[1], line: i + 1, inputScopes: [] };
3069
+ } else if (current) {
3070
+ const inputMatch = line.match(/^input_scopes\s*=\s*(.*)$/i);
3071
+ if (inputMatch?.[1])
3072
+ current.inputScopes = splitRuleValueList(inputMatch[1]).map(normalizeScopeName);
3073
+ const outputMatch = line.match(/^output_scope\s*=\s*(.*)$/i);
3074
+ if (outputMatch?.[1]) {
3075
+ const outputScope = stripRuleValueBraces(outputMatch[1]).split(/\s+/)[0];
3076
+ if (outputScope)
3077
+ current.outputScope = normalizeScopeName(outputScope);
3078
+ }
3079
+ }
3080
+ depth += countBraceDelta(line);
3081
+ if (current && depth <= 1) {
3082
+ if (current.outputScope) {
3083
+ const inputScopes = current.inputScopes.length ? current.inputScopes : ["all"];
3084
+ const syntax = `${current.name} = scope link (${inputScopes.join(" | ")} -> ${current.outputScope})`;
3085
+ const linkHint = {
3086
+ text: `Legal scope link '${current.name}' accepts input scopes { ${inputScopes.join(" ")} } and outputs '${current.outputScope}'. Context pointers such as from/prev/root/this select the current input scope; they are not fixed object fields.`,
3087
+ source: "links.cwt",
3088
+ file: filePath,
3089
+ line: current.line,
3090
+ confidence: "hint"
3091
+ };
3092
+ const scopeHints = buildSemanticHints({
3093
+ description: "",
3094
+ cwtDescription: "",
3095
+ scopes,
3096
+ relatedScopeNames: [...inputScopes, current.outputScope],
3097
+ cwtFile: filePath,
3098
+ cwtLine: current.line
3099
+ });
3100
+ results.push({
3101
+ name: current.name,
3102
+ description: `Legal scope link from { ${inputScopes.join(" ")} } to ${current.outputScope}.`,
3103
+ scopes: inputScopes,
3104
+ syntax,
3105
+ category: "scope_change",
3106
+ sourceFile: filePath,
3107
+ sourceLine: current.line,
3108
+ hardFacts: {
3109
+ category: "scope_change",
3110
+ supportedScopes: inputScopes,
3111
+ pushScope: current.outputScope,
3112
+ valueReferences: [],
3113
+ syntax,
3114
+ cwtSource: { file: filePath, line: current.line }
3115
+ },
3116
+ semanticHints: [linkHint, ...scopeHints].slice(0, 8)
3117
+ });
3118
+ }
3119
+ current = void 0;
3120
+ }
3121
+ if (inLinks && depth <= 0) {
3122
+ inLinks = false;
3123
+ current = void 0;
3124
+ }
3125
+ }
3126
+ return results;
3127
+ }
3019
3128
  function buildSemanticHints(args) {
3020
3129
  const hints = [];
3021
3130
  const seen = /* @__PURE__ */ new Set();
@@ -3157,7 +3266,12 @@ var require_rules = __commonJS({
3157
3266
  [/触发器|觸發器/g, ["trigger"]],
3158
3267
  [/效果|效应|效應/g, ["effect"]]
3159
3268
  ];
3160
- const expanded = [...direct];
3269
+ const expanded = [];
3270
+ for (const token of direct) {
3271
+ expanded.push(token);
3272
+ if (/[.:]/.test(token))
3273
+ expanded.push(...token.split(/[.:]+/).filter(Boolean));
3274
+ }
3161
3275
  for (const [pattern, tokens] of synonyms) {
3162
3276
  pattern.lastIndex = 0;
3163
3277
  if (pattern.test(intent))
@@ -3233,6 +3347,94 @@ var require_rules = __commonJS({
3233
3347
  function stripRuleValueBraces(value) {
3234
3348
  return value.replace(/^\{\s*/, "").replace(/\s*\}$/, "").trim();
3235
3349
  }
3350
+ function normalizeRuleNameQuery(name, category) {
3351
+ const lowered = name.trim().toLowerCase();
3352
+ if (category !== "scope_change" || !lowered.includes("."))
3353
+ return lowered;
3354
+ const parts = lowered.split(".").map((part) => part.trim()).filter(Boolean);
3355
+ return parts[parts.length - 1] ?? lowered;
3356
+ }
3357
+ function buildDottedScopeChainRule(query, contextScope, scopeChanges) {
3358
+ const rawParts = query.trim().split(".").map(normalizeScopeName).filter(Boolean);
3359
+ if (rawParts.length < 2)
3360
+ return void 0;
3361
+ const contextPointers = /* @__PURE__ */ new Set(["from", "prev", "root", "this"]);
3362
+ const first = rawParts[0];
3363
+ const startsWithPointer = contextPointers.has(first);
3364
+ const initialScope = startsWithPointer ? contextScope?.trim().toLowerCase() : first;
3365
+ const linkNames = rawParts.slice(1);
3366
+ if (!initialScope || linkNames.length === 0)
3367
+ return void 0;
3368
+ const hops = [];
3369
+ let currentScope = initialScope;
3370
+ for (const linkName of linkNames) {
3371
+ const link = scopeChanges.find((rule) => rule.name.toLowerCase() === linkName && !!rule.hardFacts?.pushScope && scopeListContains(rule.hardFacts.supportedScopes ?? rule.scopes, currentScope));
3372
+ const outputScope = link?.hardFacts?.pushScope?.trim().toLowerCase();
3373
+ if (!link || !outputScope)
3374
+ return void 0;
3375
+ hops.push({ link, inputScope: currentScope, outputScope });
3376
+ currentScope = outputScope;
3377
+ }
3378
+ if (hops.length === 0)
3379
+ return void 0;
3380
+ const normalizedQuery = rawParts.join(".");
3381
+ const syntax = `${normalizedQuery} = { ... }`;
3382
+ const source = hops[0].link;
3383
+ const hopText = hops.map((hop) => `${hop.inputScope}.${hop.link.name} -> ${hop.outputScope}`).join("; ");
3384
+ const linkHints = hops.map((hop) => ({
3385
+ text: `Dotted scope chain hop '${hop.inputScope}.${hop.link.name}' is legal because links.cwt declares '${hop.link.name}' with input scope '${hop.inputScope}' and output scope '${hop.outputScope}'.`,
3386
+ source: "links.cwt",
3387
+ file: hop.link.sourceFile,
3388
+ line: hop.link.sourceLine,
3389
+ confidence: "hint"
3390
+ }));
3391
+ return {
3392
+ name: normalizedQuery,
3393
+ description: `Legal dotted scope chain from ${initialScope} to ${currentScope}. Hops: ${hopText}.`,
3394
+ scopes: [initialScope],
3395
+ syntax,
3396
+ category: "scope_change",
3397
+ sourceFile: source.sourceFile,
3398
+ sourceLine: source.sourceLine,
3399
+ hardFacts: {
3400
+ category: "scope_change",
3401
+ supportedScopes: [initialScope],
3402
+ pushScope: currentScope,
3403
+ valueReferences: [],
3404
+ syntax,
3405
+ cwtSource: source.sourceFile && source.sourceLine ? { file: source.sourceFile, line: source.sourceLine } : void 0
3406
+ },
3407
+ semanticHints: linkHints.slice(0, 8)
3408
+ };
3409
+ }
3410
+ function scopeListContains(scopes, scope) {
3411
+ const lowerScope = scope.toLowerCase();
3412
+ return scopes.some((candidate) => {
3413
+ const lower = candidate.toLowerCase();
3414
+ return lower === lowerScope || lower === "all" || lower === "any";
3415
+ });
3416
+ }
3417
+ function scoreRuleNameMatch(name, needle) {
3418
+ const lower = name.toLowerCase();
3419
+ if (lower === needle)
3420
+ return 0;
3421
+ if (lower.startsWith(needle))
3422
+ return 1;
3423
+ return 2;
3424
+ }
3425
+ function normalizeScopeName(scope) {
3426
+ return scope.replace(/^["']|["']$/g, "").trim().toLowerCase();
3427
+ }
3428
+ function countBraceDelta(line) {
3429
+ let delta = 0;
3430
+ for (const ch of line) {
3431
+ if (ch === "{")
3432
+ delta += 1;
3433
+ else if (ch === "}")
3434
+ delta -= 1;
3435
+ }
3436
+ return delta;
3437
+ }
3236
3438
  function splitWords(value) {
3237
3439
  return value.split(/\s+/).map((part) => part.trim()).filter(Boolean);
3238
3440
  }
@@ -3280,7 +3482,7 @@ var require_workflowHints = __commonJS({
3280
3482
  id: "entity-lookup",
3281
3483
  title: "Verify game identifiers",
3282
3484
  triggers: ["identifier", "reference", "definition", "entity"],
3283
- recommendedTools: ["query_project_knowledge", "explore_pdx_project", "query_cwt_schema", "search_rule_capabilities", "query_rules", "query_types", "query_workspace_index", "query_definition_by_name"],
3485
+ recommendedTools: ["query_project_knowledge", "explore_pdx_project", "query_cwt_schema", "search_rule_capabilities", "query_rules", "query_types", "query_workspace_index", "go_to_definition"],
3284
3486
  guardrails: ["Treat empty text search results as inconclusive without indexed verification.", "Treat semanticHints as retrieval hints, not legality proof."]
3285
3487
  }
3286
3488
  ]
@@ -3335,27 +3537,16 @@ var require_profile = __commonJS({
3335
3537
  exports2.getProjectProfilePath = getProjectProfilePath;
3336
3538
  exports2.isProjectProfile = isProjectProfile;
3337
3539
  exports2.buildProfileSummary = buildProfileSummary;
3338
- exports2.getPromptCardForMode = getPromptCardForMode;
3540
+ exports2.getGuidanceCard = getGuidanceCard;
3339
3541
  exports2.selectProfileSection = selectProfileSection;
3340
3542
  exports2.queryProjectProfileWithHost = queryProjectProfileWithHost;
3341
3543
  var path = __importStar(require("path"));
3342
3544
  exports2.PROJECT_PROFILE_RELATIVE_PATH = path.join(".cwtools", "project", "profile.json");
3343
3545
  function getProjectProfilePath(workspaceRoot) {
3344
- try {
3345
- const fs = require("fs");
3346
- const primary = path.join(workspaceRoot, ".cwtools", "project", "profile.json");
3347
- if (fs.existsSync(primary))
3348
- return primary;
3349
- const legacy = path.join(workspaceRoot, ".cwtools-ai", "project", "profile.json");
3350
- if (fs.existsSync(legacy))
3351
- return legacy;
3352
- return primary;
3353
- } catch {
3354
- return path.join(workspaceRoot, exports2.PROJECT_PROFILE_RELATIVE_PATH);
3355
- }
3546
+ return path.join(workspaceRoot, exports2.PROJECT_PROFILE_RELATIVE_PATH);
3356
3547
  }
3357
3548
  function isProjectProfile(value) {
3358
- return !!value && typeof value === "object" && (value.schemaVersion === 1 || value.schemaVersion === 2) && typeof value.projectName === "string";
3549
+ return !!value && typeof value === "object" && value.schemaVersion === 4 && typeof value.projectName === "string";
3359
3550
  }
3360
3551
  function buildProfileSummary(profile) {
3361
3552
  const dirs = profile.keyDirectories.filter((dir) => dir.exists).map((dir) => dir.path).slice(0, 8).join(", ") || "none";
@@ -3371,16 +3562,10 @@ var require_profile = __commonJS({
3371
3562
  `Localisation: ${languages} (${profile.localisation.encoding})`
3372
3563
  ].join("\n");
3373
3564
  }
3374
- function getPromptCardForMode(profile, mode) {
3375
- if (!mode)
3565
+ function getGuidanceCard(profile, guidance) {
3566
+ if (!guidance)
3376
3567
  return void 0;
3377
- if (mode === "loc_translator" || mode === "loc_writer")
3378
- return profile.promptCards.loc_writer ?? profile.promptCards.build;
3379
- if (mode === "gui_expert")
3380
- return profile.promptCards.asset ?? profile.promptCards.build;
3381
- if (mode === "script_reviewer")
3382
- return profile.promptCards.review;
3383
- return profile.promptCards[mode] ?? profile.promptCards.build;
3568
+ return profile.guidanceCards[guidance] ?? profile.guidanceCards.implementation;
3384
3569
  }
3385
3570
  function selectProfileSection(profile, section) {
3386
3571
  switch (section) {
@@ -3402,8 +3587,8 @@ var require_profile = __commonJS({
3402
3587
  vanillaCache: profile.validation?.vanillaCache,
3403
3588
  game: profile.game
3404
3589
  };
3405
- case "promptCards":
3406
- return profile.promptCards;
3590
+ case "guidanceCards":
3591
+ return profile.guidanceCards;
3407
3592
  case "all":
3408
3593
  return profile;
3409
3594
  case "summary":
@@ -3448,7 +3633,7 @@ var require_profile = __commonJS({
3448
3633
  source: "cwtools-shared",
3449
3634
  error: {
3450
3635
  code: "invalid_profile",
3451
- message: "Project profile exists but is not a valid schemaVersion 1 or 2 profile."
3636
+ message: "Project profile exists but is not a valid schemaVersion 4 profile."
3452
3637
  }
3453
3638
  };
3454
3639
  }
@@ -3465,8 +3650,8 @@ var require_profile = __commonJS({
3465
3650
  profile: section === "all" ? parsed : void 0,
3466
3651
  summary: buildProfileSummary(parsed),
3467
3652
  data: selectProfileSection(parsed, section),
3468
- promptCard: getPromptCardForMode(parsed, args.mode),
3469
- _hint: 'Use section="routing", "localisation", "identifiers", or a mode-specific promptCard for targeted context.'
3653
+ guidanceCard: getGuidanceCard(parsed, args.guidance),
3654
+ _hint: 'Use section="routing", "localisation", "identifiers", or a targeted guidance card for focused context.'
3470
3655
  }
3471
3656
  };
3472
3657
  } catch (error) {
@@ -3717,7 +3902,7 @@ var require_paths = __commonJS({
3717
3902
  }
3718
3903
  function isScratchRelativePath(relativePath) {
3719
3904
  const parts = relativePath.replace(/\\/g, "/").toLowerCase().split("/");
3720
- return parts.includes(".cwtools") || parts.includes(".cwtools-ai");
3905
+ return parts.includes(".cwtools");
3721
3906
  }
3722
3907
  function validateLocalisationPath(workspaceRoot, inputPath) {
3723
3908
  const resolution = resolveWorkspacePath(workspaceRoot, inputPath);
@@ -3821,9 +4006,8 @@ var require_names = __commonJS({
3821
4006
  "get_completion_at",
3822
4007
  "document_symbols",
3823
4008
  "workspace_symbols",
3824
- "query_definition",
3825
- "query_definition_by_name",
3826
- "query_references",
4009
+ "go_to_definition",
4010
+ "find_references",
3827
4011
  "query_scripted_effects",
3828
4012
  "query_scripted_triggers",
3829
4013
  "query_enums",
@@ -4203,7 +4387,11 @@ var require_symbols = __commonJS({
4203
4387
  };
4204
4388
  }
4205
4389
  async function queryDefinitionWithHost(host, args) {
4206
- const resolution = resolveReadableFile(host, args.file);
4390
+ const file = String(args.file ?? "").trim();
4391
+ if (!file || !Number.isInteger(args.line) || args.line < 0 || !Number.isInteger(args.column) || args.column < 0) {
4392
+ return (0, schema_1.toolDenied)("invalid_arguments", "go_to_definition requires symbolName or a non-negative file position.");
4393
+ }
4394
+ const resolution = resolveReadableFile(host, file);
4207
4395
  if ("error" in resolution)
4208
4396
  return resolution.error;
4209
4397
  const fileUri = toFileUri(resolution.resolvedPath);
@@ -4232,7 +4420,7 @@ var require_symbols = __commonJS({
4232
4420
  async function queryDefinitionByNameWithHost(host, args) {
4233
4421
  const symbolName = String(args.symbolName ?? "").trim();
4234
4422
  if (!symbolName)
4235
- return (0, schema_1.toolDenied)("invalid_arguments", "query_definition_by_name requires symbolName.");
4423
+ return (0, schema_1.toolDenied)("invalid_arguments", "go_to_definition requires symbolName or a file position.");
4236
4424
  const commandResult = await host.lsp.executeCommand("cwtools.ai.queryDefinitionByName", [symbolName], { timeoutMs: 1e4 });
4237
4425
  if (!isUnavailable(commandResult)) {
4238
4426
  return {
@@ -4258,9 +4446,28 @@ var require_symbols = __commonJS({
4258
4446
  }
4259
4447
  async function queryReferencesWithHost(host, args) {
4260
4448
  const identifier = String(args.identifier ?? "").trim();
4261
- if (!identifier)
4262
- return (0, schema_1.toolDenied)("invalid_arguments", "query_references requires identifier.");
4263
4449
  const limit = clampNumber(args.limit, 50, 1, 200);
4450
+ if (!identifier) {
4451
+ if (!args.file || !Number.isInteger(args.line) || (args.line ?? -1) < 0 || !Number.isInteger(args.column) || (args.column ?? -1) < 0) {
4452
+ return (0, schema_1.toolDenied)("invalid_arguments", "find_references requires identifier or file, line, and column.");
4453
+ }
4454
+ const resolution = resolveReadableFile(host, args.file);
4455
+ if ("error" in resolution)
4456
+ return resolution.error;
4457
+ const requested = await requestLsp(host, "textDocument/references", {
4458
+ textDocument: { uri: toFileUri(resolution.resolvedPath) },
4459
+ position: { line: args.line, character: args.column },
4460
+ context: { includeDeclaration: true }
4461
+ }, 1e4);
4462
+ const references2 = normalizeLocations(requested.data, host.workspaceRoot).slice(0, limit);
4463
+ return {
4464
+ ok: !requested.unavailable,
4465
+ status: requested.unavailable ? "unavailable" : "ready",
4466
+ source: "cwtools-lsp-references",
4467
+ data: { references: references2, total: references2.length },
4468
+ error: requested.unavailable ? { code: "lsp_unavailable", message: requested.message ?? "Reference provider is unavailable." } : void 0
4469
+ };
4470
+ }
4264
4471
  const definition = await queryDefinitionByNameWithHost(host, { symbolName: identifier });
4265
4472
  const firstMatch = definition.data && typeof definition.data === "object" ? getFirstDefinitionMatch(definition.data) : void 0;
4266
4473
  if (firstMatch?.file !== void 0 && firstMatch.line !== void 0 && host.lsp.request) {
@@ -4482,12 +4689,12 @@ var require_symbols = __commonJS({
4482
4689
  return "external";
4483
4690
  const normalized = filePath.replace(/\\/g, "/").toLowerCase();
4484
4691
  if ((0, paths_1.isPathInsideOrEqual)(workspaceRoot, filePath)) {
4485
- return normalized.includes("/generated/") || normalized.includes("/.cwtools/") || normalized.includes("/.cwtools-ai/") ? "generated" : "workspace";
4692
+ return normalized.includes("/generated/") || normalized.includes("/.cwtools/") ? "generated" : "workspace";
4486
4693
  }
4487
4694
  if (normalized.includes("/vanilla/") || normalized.includes("/cache/") || normalized.includes("/steamapps/common/")) {
4488
4695
  return "vanilla";
4489
4696
  }
4490
- if (normalized.includes("/generated/") || normalized.includes("/.cwtools/") || normalized.includes("/.cwtools-ai/"))
4697
+ if (normalized.includes("/generated/") || normalized.includes("/.cwtools/"))
4491
4698
  return "generated";
4492
4699
  return "external";
4493
4700
  }
@@ -4977,20 +5184,18 @@ var require_toolHandlers = __commonJS({
4977
5184
  query: String(args.query ?? ""),
4978
5185
  limit: typeof args.limit === "number" ? args.limit : void 0
4979
5186
  });
4980
- case "query_definition":
4981
- return (0, symbols_1.queryDefinitionWithHost)(host, {
5187
+ case "go_to_definition":
5188
+ return typeof args.symbolName === "string" && args.symbolName.trim() ? (0, symbols_1.queryDefinitionByNameWithHost)(host, { symbolName: args.symbolName }) : (0, symbols_1.queryDefinitionWithHost)(host, {
4982
5189
  file: String(args.file ?? ""),
4983
5190
  line: Number(args.line ?? 0),
4984
5191
  column: Number(args.column ?? 0)
4985
5192
  });
4986
- case "query_definition_by_name":
4987
- return (0, symbols_1.queryDefinitionByNameWithHost)(host, {
4988
- symbolName: String(args.symbolName ?? "")
4989
- });
4990
- case "query_references":
5193
+ case "find_references":
4991
5194
  return (0, symbols_1.queryReferencesWithHost)(host, {
4992
- identifier: String(args.identifier ?? ""),
5195
+ identifier: typeof args.identifier === "string" ? args.identifier : void 0,
4993
5196
  file: typeof args.file === "string" ? args.file : void 0,
5197
+ line: typeof args.line === "number" ? args.line : void 0,
5198
+ column: typeof args.column === "number" ? args.column : void 0,
4994
5199
  limit: typeof args.limit === "number" ? args.limit : void 0
4995
5200
  });
4996
5201
  case "query_scope":
@@ -10392,7 +10597,6 @@ var require_projectSettings = __commonJS({
10392
10597
  }
10393
10598
  var cache = /* @__PURE__ */ new Map();
10394
10599
  var SETTINGS_NAMESPACE = "stellarisLanguageServices";
10395
- var LEGACY_SETTINGS_NAMESPACE = "cwtools";
10396
10600
  function readSettingsObject(workspaceRoot) {
10397
10601
  const file = path.join(workspaceRoot, ".vscode", "settings.json");
10398
10602
  let mtimeMs;
@@ -10436,8 +10640,7 @@ var require_projectSettings = __commonJS({
10436
10640
  const settings = readSettingsObject(workspaceRoot);
10437
10641
  if (!settings)
10438
10642
  return void 0;
10439
- const current = resolveDotted(settings, `${SETTINGS_NAMESPACE}.${subKey}`);
10440
- return current !== void 0 ? current : resolveDotted(settings, `${LEGACY_SETTINGS_NAMESPACE}.${subKey}`);
10643
+ return resolveDotted(settings, `${SETTINGS_NAMESPACE}.${subKey}`);
10441
10644
  }
10442
10645
  function asNonEmptyStringArray(value) {
10443
10646
  return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.length > 0) : [];
@@ -10745,7 +10948,7 @@ var require_lspProcessHost = __commonJS({
10745
10948
  return;
10746
10949
  const watcher = (0, chokidar_1.watch)(this.options.workspaceRoot, {
10747
10950
  ignoreInitial: true,
10748
- ignored: [/(^|[\\/])(?:node_modules|\.git|\.cwtools|\.cwtools-ai)(?:[\\/]|$)/],
10951
+ ignored: [/(^|[\\/])(?:node_modules|\.(?!\.)[^\\/]+)(?:[\\/]|$)/],
10749
10952
  awaitWriteFinish: { stabilityThreshold: 120, pollInterval: 30 }
10750
10953
  });
10751
10954
  watcher.on("add", (filePath) => this.queueWatchedFileChange(filePath, 1));
@@ -10927,7 +11130,7 @@ var require_lspProcessHost = __commonJS({
10927
11130
  if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
10928
11131
  return false;
10929
11132
  const segments = relative.split(/[\\/]+/).map((segment) => segment.toLowerCase());
10930
- if (segments.some((segment) => segment === "node_modules" || segment === ".git" || segment === ".cwtools" || segment === ".cwtools-ai"))
11133
+ if (segments.some((segment) => segment === "node_modules" || segment.startsWith(".") && !segment.startsWith("..")))
10931
11134
  return false;
10932
11135
  const extension = path.extname(filePath).toLowerCase();
10933
11136
  if (extension === ".csv" && game && game.toLowerCase() !== "ck2")
@@ -10984,7 +11187,7 @@ var require_projectDetect = __commonJS({
10984
11187
  var path = __importStar(require("path"));
10985
11188
  var ROOT_FILE_MARKERS = ["descriptor.mod"];
10986
11189
  var ROOT_NESTED_FILE_MARKERS = [path.join(".metadata", "metadata.json")];
10987
- var ROOT_DIR_MARKERS = [".cwtools", ".cwtools-ai"];
11190
+ var ROOT_DIR_MARKERS = [".cwtools"];
10988
11191
  var CONTENT_DIR_MARKERS = ["common", "events", "localisation", "localization"];
10989
11192
  var MAX_ANCESTORS = 6;
10990
11193
  var MAX_CHILDREN = 64;
@@ -11129,7 +11332,7 @@ var require_revalidation = __commonJS({
11129
11332
  var path = __importStar(require("path"));
11130
11333
  var lspProcessHost_1 = require_lspProcessHost();
11131
11334
  var REVALIDATE_EXTS = /* @__PURE__ */ new Set([".txt", ".yml", ".gui", ".gfx", ".asset", ".shader", ".fxh"]);
11132
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".cwtools", ".cwtools-ai", ".vscode"]);
11335
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
11133
11336
  var MAX_WALK_FILES = 2e4;
11134
11337
  var MAX_BATCH = 200;
11135
11338
  function numberOr(value, fallback) {
@@ -11669,7 +11872,7 @@ var require_nodeHostServices = __commonJS({
11669
11872
  return;
11670
11873
  const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
11671
11874
  for (const entry of entries) {
11672
- if (entry.name.startsWith(".") && entry.name !== ".cwtools" && entry.name !== ".cwtools-ai")
11875
+ if (entry.name.startsWith(".") && entry.name !== ".cwtools")
11673
11876
  continue;
11674
11877
  if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage")
11675
11878
  continue;
@@ -46774,7 +46977,7 @@ var require_bridgeProxy = __commonJS({
46774
46977
  "CWTools MCP is served by the active VS Code-compatible extension host.",
46775
46978
  "This proxy does not start its own CWTools language server. If calls report",
46776
46979
  "bridge_unavailable, open the project in the compatible host where the extension",
46777
- "is installed and active, or rerun with --standalone to use the legacy isolated mode."
46980
+ "is installed and active, or rerun with --standalone to use the standalone LSP host."
46778
46981
  ].join("\n")
46779
46982
  });
46780
46983
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
@@ -46863,7 +47066,7 @@ var require_bridgeProxy = __commonJS({
46863
47066
  throw new Error(`CWTools MCP bridge is unavailable: could not read ${manifestPath}. Open this project in a VS Code-compatible host with the extension active, or pass --standalone to start a separate CWTools server. Details: ${error instanceof Error ? error.message : String(error)}`);
46864
47067
  }
46865
47068
  if (parsed.kind !== "cwtools-mcp-extension-bridge" || parsed.protocolVersion !== BRIDGE_PROTOCOL_VERSION) {
46866
- throw new Error(`CWTools MCP bridge manifest at ${manifestPath} is not compatible with this proxy. Open the project in the updated extension host, or pass --standalone for legacy isolated mode.`);
47069
+ throw new Error(`CWTools MCP bridge manifest at ${manifestPath} is not compatible with this proxy. Open the project in the updated extension host, or pass --standalone to use the standalone LSP host.`);
46867
47070
  }
46868
47071
  if (!parsed.rpcUrl || !parsed.token) {
46869
47072
  throw new Error(`CWTools MCP bridge manifest at ${manifestPath} is missing rpcUrl/token.`);
@@ -49516,7 +49719,7 @@ var require_server3 = __commonJS({
49516
49719
  "- Before declaring code correct, review get_diagnostics (whole project). Honor the",
49517
49720
  " readiness/freshness fields: if readiness.ready is false the project is still loading \u2014",
49518
49721
  " retry; an empty result then is not authoritative.",
49519
- "- Navigate with query_definition / query_definition_by_name / query_references /",
49722
+ "- Navigate with go_to_definition / find_references /",
49520
49723
  " document_symbols / workspace_symbols; read structured blocks with get_pdx_block",
49521
49724
  " instead of reading whole files. get_entity_info gives a file's referenced types/vars.",
49522
49725
  "- Results carry vanillaCache metadata: if available is false, vanilla IDs are missing",
@@ -212,7 +212,7 @@ class LspProcessHost {
212
212
  return;
213
213
  const watcher = (0, chokidar_1.watch)(this.options.workspaceRoot, {
214
214
  ignoreInitial: true,
215
- ignored: [/(^|[\\/])(?:node_modules|\.git|\.cwtools|\.cwtools-ai)(?:[\\/]|$)/],
215
+ ignored: [/(^|[\\/])(?:node_modules|\.(?!\.)[^\\/]+)(?:[\\/]|$)/],
216
216
  awaitWriteFinish: { stabilityThreshold: 120, pollInterval: 30 },
217
217
  });
218
218
  watcher.on('add', filePath => this.queueWatchedFileChange(filePath, 1));
@@ -401,9 +401,7 @@ function isLspWatchedFile(workspaceRoot, filePath, game) {
401
401
  return false;
402
402
  const segments = relative.split(/[\\/]+/).map(segment => segment.toLowerCase());
403
403
  if (segments.some(segment => segment === 'node_modules'
404
- || segment === '.git'
405
- || segment === '.cwtools'
406
- || segment === '.cwtools-ai'))
404
+ || (segment.startsWith('.') && !segment.startsWith('..'))))
407
405
  return false;
408
406
  const extension = path.extname(filePath).toLowerCase();
409
407
  if (extension === '.csv' && game && game.toLowerCase() !== 'ck2')
@@ -439,7 +439,7 @@ async function walk(root, visit) {
439
439
  return;
440
440
  const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
441
441
  for (const entry of entries) {
442
- if (entry.name.startsWith('.') && entry.name !== '.cwtools' && entry.name !== '.cwtools-ai')
442
+ if (entry.name.startsWith('.') && entry.name !== '.cwtools')
443
443
  continue;
444
444
  if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'coverage')
445
445
  continue;
@@ -39,7 +39,7 @@ const path = __importStar(require("path"));
39
39
  // Strong, unambiguous "this directory is a mod root" signals.
40
40
  const ROOT_FILE_MARKERS = ['descriptor.mod'];
41
41
  const ROOT_NESTED_FILE_MARKERS = [path.join('.metadata', 'metadata.json')];
42
- const ROOT_DIR_MARKERS = ['.cwtools', '.cwtools-ai'];
42
+ const ROOT_DIR_MARKERS = ['.cwtools'];
43
43
  // PDX content dirs — specific enough at a mod root, but too generic to trust in a
44
44
  // far-off ancestor. Generic graphics dirs (gfx/, interface/) are excluded.
45
45
  const CONTENT_DIR_MARKERS = ['common', 'events', 'localisation', 'localization'];
@@ -106,7 +106,6 @@ function parseJsonc(text) {
106
106
  }
107
107
  const cache = new Map();
108
108
  const SETTINGS_NAMESPACE = 'stellarisLanguageServices';
109
- const LEGACY_SETTINGS_NAMESPACE = 'cwtools';
110
109
  function readSettingsObject(workspaceRoot) {
111
110
  const file = path.join(workspaceRoot, '.vscode', 'settings.json');
112
111
  let mtimeMs;
@@ -152,10 +151,7 @@ function getExtensionSetting(workspaceRoot, subKey) {
152
151
  const settings = readSettingsObject(workspaceRoot);
153
152
  if (!settings)
154
153
  return undefined;
155
- const current = resolveDotted(settings, `${SETTINGS_NAMESPACE}.${subKey}`);
156
- return current !== undefined
157
- ? current
158
- : resolveDotted(settings, `${LEGACY_SETTINGS_NAMESPACE}.${subKey}`);
154
+ return resolveDotted(settings, `${SETTINGS_NAMESPACE}.${subKey}`);
159
155
  }
160
156
  function asNonEmptyStringArray(value) {
161
157
  return Array.isArray(value)
@@ -39,7 +39,7 @@ const path = __importStar(require("path"));
39
39
  const lspProcessHost_1 = require("./lspProcessHost");
40
40
  // File extensions the language server validates — only these are worth revalidating.
41
41
  const REVALIDATE_EXTS = new Set(['.txt', '.yml', '.gui', '.gfx', '.asset', '.shader', '.fxh']);
42
- const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git', '.cwtools', '.cwtools-ai', '.vscode']);
42
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage']);
43
43
  const MAX_WALK_FILES = 20000;
44
44
  const MAX_BATCH = 200;
45
45
  function numberOr(value, fallback) {
@@ -59,7 +59,7 @@ function createBridgeProxyMcpServer(config) {
59
59
  'CWTools MCP is served by the active VS Code-compatible extension host.',
60
60
  'This proxy does not start its own CWTools language server. If calls report',
61
61
  'bridge_unavailable, open the project in the compatible host where the extension',
62
- 'is installed and active, or rerun with --standalone to use the legacy isolated mode.',
62
+ 'is installed and active, or rerun with --standalone to use the standalone LSP host.',
63
63
  ].join('\n'),
64
64
  });
65
65
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
@@ -158,7 +158,7 @@ async function readBridgeManifest(config, server) {
158
158
  }
159
159
  if (parsed.kind !== 'cwtools-mcp-extension-bridge' || parsed.protocolVersion !== BRIDGE_PROTOCOL_VERSION) {
160
160
  throw new Error(`CWTools MCP bridge manifest at ${manifestPath} is not compatible with this proxy. ` +
161
- `Open the project in the updated extension host, or pass --standalone for legacy isolated mode.`);
161
+ `Open the project in the updated extension host, or pass --standalone to use the standalone LSP host.`);
162
162
  }
163
163
  if (!parsed.rpcUrl || !parsed.token) {
164
164
  throw new Error(`CWTools MCP bridge manifest at ${manifestPath} is missing rpcUrl/token.`);
package/dist/server.js CHANGED
@@ -26,7 +26,7 @@ const SERVER_INSTRUCTIONS = [
26
26
  '- Before declaring code correct, review get_diagnostics (whole project). Honor the',
27
27
  ' readiness/freshness fields: if readiness.ready is false the project is still loading —',
28
28
  ' retry; an empty result then is not authoritative.',
29
- '- Navigate with query_definition / query_definition_by_name / query_references /',
29
+ '- Navigate with go_to_definition / find_references /',
30
30
  ' document_symbols / workspace_symbols; read structured blocks with get_pdx_block',
31
31
  ' instead of reading whole files. get_entity_info gives a file\'s referenced types/vars.',
32
32
  '- Results carry vanillaCache metadata: if available is false, vanilla IDs are missing',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cwtools-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Read-only MCP server exposing CWTools Paradox/Stellaris semantic tools to external agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
39
  "chokidar": "^4.0.3",
40
- "cwtools-shared": "0.2.3",
40
+ "cwtools-shared": "0.2.5",
41
41
  "vscode-jsonrpc": "^8.2.0"
42
42
  },
43
43
  "devDependencies": {