cwtools-shared 0.2.1 → 0.2.3

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.
@@ -147,7 +147,7 @@ export interface CwtRuleValueReference {
147
147
  }
148
148
  export interface RuleSemanticHint {
149
149
  text: string;
150
- source: 'trigger_docs.log' | 'scopes.cwt' | 'cwt-comment' | 'modifiers.log';
150
+ source: 'trigger_docs.log' | 'scopes.cwt' | 'links.cwt' | 'cwt-comment' | 'modifiers.log';
151
151
  file?: string;
152
152
  line?: number;
153
153
  confidence: 'hint';
@@ -49,9 +49,15 @@ async function queryRulesWithHost(host, args) {
49
49
  : args.category === 'modifier'
50
50
  ? cache.modifiers
51
51
  : cache.scopeChanges;
52
+ const dottedScopeChain = args.name && args.category === 'scope_change'
53
+ ? buildDottedScopeChainRule(args.name, args.scope, cache.scopeChanges)
54
+ : undefined;
52
55
  if (args.name) {
53
- const needle = args.name.toLowerCase();
54
- const filtered = rules.filter(rule => rule.name.toLowerCase().includes(needle));
56
+ const needle = normalizeRuleNameQuery(args.name, args.category);
57
+ const filtered = rules
58
+ .filter(rule => rule.name.toLowerCase().includes(needle))
59
+ .sort((a, b) => scoreRuleNameMatch(a.name, needle) - scoreRuleNameMatch(b.name, needle)
60
+ || a.name.localeCompare(b.name));
55
61
  if (filtered.length === 0 && rules.length > 0) {
56
62
  rules = rules
57
63
  .map(rule => ({ rule, score: levenshtein(needle, rule.name.toLowerCase()) }))
@@ -66,6 +72,12 @@ async function queryRulesWithHost(host, args) {
66
72
  rules = filtered;
67
73
  }
68
74
  }
75
+ if (dottedScopeChain) {
76
+ rules = [
77
+ dottedScopeChain,
78
+ ...rules.filter(rule => rule.name !== dottedScopeChain.name),
79
+ ];
80
+ }
69
81
  if (args.scope) {
70
82
  const scope = args.scope.toLowerCase();
71
83
  rules = rules.filter(rule => rule.scopes.length === 0
@@ -322,13 +334,45 @@ function normalizeCwtSchemaTarget(host, value) {
322
334
  }
323
335
  async function findCwtSchemaFiles(host, root, maxFiles) {
324
336
  if (host.rules?.listCwtFiles) {
325
- return (await host.rules.listCwtFiles(root, { limit: maxFiles })).slice(0, maxFiles);
337
+ return (await host.rules.listCwtFiles(root, { limit: maxFiles }))
338
+ .slice(0, maxFiles)
339
+ .sort((a, b) => a.localeCompare(b));
340
+ }
341
+ if (fs.existsSync(root)) {
342
+ const results = [];
343
+ const ignoredDirs = new Set(['.git', 'node_modules', 'logs', 'config']);
344
+ const walkDisk = (dir, depth) => {
345
+ if (results.length >= maxFiles || depth > 8)
346
+ return;
347
+ let entries;
348
+ try {
349
+ entries = fs.readdirSync(dir, { withFileTypes: true });
350
+ }
351
+ catch {
352
+ return;
353
+ }
354
+ entries.sort((a, b) => a.name.localeCompare(b.name));
355
+ for (const entry of entries) {
356
+ if (results.length >= maxFiles)
357
+ break;
358
+ const fullPath = path.join(dir, entry.name);
359
+ if (entry.isDirectory()) {
360
+ if (!ignoredDirs.has(entry.name))
361
+ walkDisk(fullPath, depth + 1);
362
+ }
363
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith('.cwt')) {
364
+ results.push(fullPath);
365
+ }
366
+ }
367
+ };
368
+ walkDisk(root, 0);
369
+ return results.sort((a, b) => a.localeCompare(b));
326
370
  }
327
371
  const rootRelative = workspaceRelativePath(host.workspaceRoot, root);
328
372
  if (!rootRelative)
329
373
  return [];
330
374
  const results = [];
331
- const ignoredDirs = new Set(['.git', 'node_modules', 'logs']);
375
+ const ignoredDirs = new Set(['.git', 'node_modules', 'logs', 'config']);
332
376
  const walk = async (relativeDir, depth) => {
333
377
  if (results.length >= maxFiles || depth > 8)
334
378
  return;
@@ -339,6 +383,7 @@ async function findCwtSchemaFiles(host, root, maxFiles) {
339
383
  catch {
340
384
  return;
341
385
  }
386
+ entries.sort((a, b) => a.name.localeCompare(b.name));
342
387
  for (const entry of entries) {
343
388
  if (results.length >= maxFiles)
344
389
  break;
@@ -354,7 +399,16 @@ async function findCwtSchemaFiles(host, root, maxFiles) {
354
399
  }
355
400
  };
356
401
  await walk(rootRelative, 0);
357
- return results;
402
+ return results.sort((a, b) => a.localeCompare(b));
403
+ }
404
+ async function collectCwtRuleSourceFiles(host, root, includeMissingLogs) {
405
+ const files = await findCwtSchemaFiles(host, root, CWT_RULE_FILE_SCAN_LIMIT);
406
+ for (const relativeLog of CWT_RULE_LOG_CANDIDATES) {
407
+ const fullPath = path.join(root, relativeLog);
408
+ if (includeMissingLogs || fs.existsSync(fullPath))
409
+ files.push(fullPath);
410
+ }
411
+ return Array.from(new Set(files)).sort((a, b) => a.localeCompare(b));
358
412
  }
359
413
  function workspaceRelativePath(workspaceRoot, fullPath) {
360
414
  const relative = path.relative(workspaceRoot, fullPath);
@@ -715,27 +769,18 @@ function scoreCwtSchemaEntity(summary, normalizedTarget, name) {
715
769
  //
716
770
  // loadCwtRules used to re-read and re-parse every rule file on each query.
717
771
  // The memo keeps one parsed CwtRuleCache per host identity, invalidated by an
718
- // mtime/size signature over a bounded candidate file set (12 files per config
719
- // root). `generation` is a per-host monotonic reload counter; `contentHash` is
720
- // sha256 (16 hex chars) over the length-prefixed concatenation of every
721
- // candidate rule file's content — the same algorithm the extension-side
772
+ // mtime/size signature over every .cwt file under the active config roots plus
773
+ // the docs/modifier logs. `generation` is a per-host monotonic reload counter;
774
+ // `contentHash` is sha256 (16 hex chars) over the length-prefixed concatenation
775
+ // of the same files' content — the same algorithm the extension-side
722
776
  // LspToolHandler uses, so both ends describe rule revisions with the same
723
777
  // hash semantics. The cache is process-local and bounded
724
778
  // (CWT_RULES_MEMO_MAX_ENTRIES).
725
- const CWT_RULE_FILE_CANDIDATES = [
726
- 'scopes.cwt',
779
+ const CWT_RULE_LOG_CANDIDATES = [
727
780
  path.join('logs', 'trigger_docs.log'),
728
781
  path.join('logs', 'modifiers.log'),
729
- 'triggers.cwt',
730
- 'trigger.cwt',
731
- path.join('generated', 'triggers.generated.cwt'),
732
- 'effects.cwt',
733
- 'effect.cwt',
734
- path.join('generated', 'effects.generated.cwt'),
735
- 'modifier.cwt',
736
- 'scope_changes.cwt',
737
- path.join('generated', 'scope_changes.generated.cwt'),
738
782
  ];
783
+ const CWT_RULE_FILE_SCAN_LIMIT = 5000;
739
784
  const CWT_RULES_MEMO_MAX_ENTRIES = 8;
740
785
  /**
741
786
  * When no candidate rule file exists on disk, the mtime signature cannot
@@ -744,12 +789,11 @@ const CWT_RULES_MEMO_MAX_ENTRIES = 8;
744
789
  */
745
790
  const CWT_RULES_MEMO_REFRESH_MS = 30000;
746
791
  const cwtRulesMemo = new Map();
747
- function computeRulesSignature(configPaths) {
792
+ async function computeRulesSignature(host, configPaths) {
748
793
  const parts = [];
749
794
  let sawDiskFiles = false;
750
795
  for (const configPath of configPaths) {
751
- for (const file of CWT_RULE_FILE_CANDIDATES) {
752
- const fullPath = path.join(configPath, file);
796
+ for (const fullPath of await collectCwtRuleSourceFiles(host, configPath, true)) {
753
797
  try {
754
798
  const stat = fs.statSync(fullPath);
755
799
  parts.push(`${fullPath}:${stat.mtimeMs}:${stat.size}`);
@@ -772,8 +816,8 @@ function computeRulesSignature(configPaths) {
772
816
  async function computeRulesContentHash(host, configPaths) {
773
817
  const hash = crypto.createHash('sha256');
774
818
  for (const configPath of configPaths) {
775
- for (const file of CWT_RULE_FILE_CANDIDATES) {
776
- const read = await readRulesTextFile(host, path.join(configPath, file)).catch(() => ({ exists: false, content: '', hasBom: false }));
819
+ for (const file of await collectCwtRuleSourceFiles(host, configPath, true)) {
820
+ const read = await readRulesTextFile(host, file).catch(() => ({ exists: false, content: '', hasBom: false }));
777
821
  if (!read.exists)
778
822
  continue;
779
823
  hash.update(`${read.content.length}:`);
@@ -788,7 +832,7 @@ function cwtRulesHostKey(host) {
788
832
  async function loadCwtRulesMemoized(host) {
789
833
  const configPaths = await resolveRulesConfigPaths(host);
790
834
  const hostKey = cwtRulesHostKey(host);
791
- const { signature, sawDiskFiles } = computeRulesSignature(configPaths);
835
+ const { signature, sawDiskFiles } = await computeRulesSignature(host, configPaths);
792
836
  const memo = cwtRulesMemo.get(hostKey);
793
837
  if (memo && memo.signature === signature && (memo.sawDiskFiles || host.now() - memo.computedAt < CWT_RULES_MEMO_REFRESH_MS)) {
794
838
  return { cache: memo.cache, meta: { generation: memo.generation, contentHash: memo.contentHash } };
@@ -824,10 +868,25 @@ async function loadCwtRulesFromPaths(host, configPaths) {
824
868
  const scopes = scopesRead.exists
825
869
  ? parseScopesFile(scopesRead.content, path.join(configPath, 'scopes.cwt'))
826
870
  : new Map();
827
- const triggers = await readRuleFiles(host, configPath, ['triggers.cwt', 'trigger.cwt', path.join('generated', 'triggers.generated.cwt')], 'trigger', docs, scopes);
828
- const effects = await readRuleFiles(host, configPath, ['effects.cwt', 'effect.cwt', path.join('generated', 'effects.generated.cwt')], 'effect', docs, scopes);
829
- const scopeChanges = await readRuleFiles(host, configPath, ['scope_changes.cwt', path.join('generated', 'scope_changes.generated.cwt')], 'scope_change', docs, scopes);
830
- const modifierAliases = await readRuleFiles(host, configPath, ['modifier.cwt'], 'modifier', docs, scopes);
871
+ const triggers = [];
872
+ const effects = [];
873
+ const scopeChanges = [];
874
+ const modifierAliases = [];
875
+ for (const file of await findCwtSchemaFiles(host, configPath, CWT_RULE_FILE_SCAN_LIMIT)) {
876
+ const relativeFile = path.relative(configPath, file).replace(/\\/g, '/');
877
+ const parsed = await readRulesFile(host, file, scopeChangeFileCategoryOverride(relativeFile), docs, scopes);
878
+ for (const rule of parsed) {
879
+ if (rule.category === 'trigger')
880
+ triggers.push(rule);
881
+ else if (rule.category === 'effect')
882
+ effects.push(rule);
883
+ else if (rule.category === 'modifier')
884
+ modifierAliases.push(rule);
885
+ else
886
+ scopeChanges.push(rule);
887
+ }
888
+ scopeChanges.push(...await readLinksFile(host, file, scopes));
889
+ }
831
890
  const modifierLog = await readModifiersLog(host, path.join(configPath, 'logs', 'modifiers.log'));
832
891
  const modifiers = [...modifierAliases];
833
892
  const modifierNames = new Set(modifiers.map(rule => rule.name.toLowerCase()));
@@ -925,6 +984,12 @@ async function readRulesFile(host, filePath, category, docs, scopes) {
925
984
  return [];
926
985
  return parseCwtFile(read.content, filePath, category, docs, scopes);
927
986
  }
987
+ function scopeChangeFileCategoryOverride(relativeRuleFile) {
988
+ const base = path.posix.basename(relativeRuleFile.replace(/\\/g, '/')).toLowerCase();
989
+ return base === 'scope_changes.cwt' || base === 'scope_changes.generated.cwt' || base === 'scope_change.cwt'
990
+ ? 'scope_change'
991
+ : undefined;
992
+ }
928
993
  async function readModifiersLog(host, filePath) {
929
994
  const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: '', hasBom: false }));
930
995
  if (!read.exists)
@@ -1035,7 +1100,7 @@ function parseScopesFile(content, filePath) {
1035
1100
  }
1036
1101
  return scopes;
1037
1102
  }
1038
- function parseCwtFile(content, filePath, category, docs, scopes) {
1103
+ function parseCwtFile(content, filePath, categoryOverride, docs, scopes) {
1039
1104
  const results = [];
1040
1105
  let currentScopes = [];
1041
1106
  let currentSupportedScopes = [];
@@ -1080,9 +1145,11 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
1080
1145
  currentDesc = comment;
1081
1146
  continue;
1082
1147
  }
1083
- const nameMatch = line.match(/^alias\[(?:trigger|effect|modifier):([^\]]+)\]\s*=\s*(.*)/);
1084
- if (nameMatch?.[1]) {
1085
- const name = nameMatch[1];
1148
+ const nameMatch = line.match(/^alias\[(trigger|effect|modifier):([^\]]+)\]\s*=\s*(.*)/);
1149
+ if (nameMatch?.[1] && nameMatch[2]) {
1150
+ const aliasKind = nameMatch[1];
1151
+ const category = categoryOverride ?? aliasKind;
1152
+ const name = nameMatch[2];
1086
1153
  const doc = docs.get(name);
1087
1154
  const cwtBlockText = collectCwtBlockText(lines, i);
1088
1155
  const scopesForRule = doc?.scopes.length
@@ -1090,7 +1157,7 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
1090
1157
  : currentSupportedScopes.length
1091
1158
  ? currentSupportedScopes
1092
1159
  : currentScopes;
1093
- const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[2] ?? '');
1160
+ const syntax = doc?.syntax || normalizeInlineSyntax(name, nameMatch[3] ?? '');
1094
1161
  const description = doc?.description || currentDesc;
1095
1162
  const semanticHints = buildSemanticHints({
1096
1163
  description,
@@ -1134,6 +1201,92 @@ function parseCwtFile(content, filePath, category, docs, scopes) {
1134
1201
  }
1135
1202
  return results;
1136
1203
  }
1204
+ async function readLinksFile(host, filePath, scopes) {
1205
+ const read = await readRulesTextFile(host, filePath).catch(() => ({ exists: false, content: '', hasBom: false }));
1206
+ if (!read.exists || !/^\s*links\s*=\s*\{/im.test(read.content))
1207
+ return [];
1208
+ return parseLinksCwtFile(read.content, filePath, scopes);
1209
+ }
1210
+ function parseLinksCwtFile(content, filePath, scopes) {
1211
+ const results = [];
1212
+ const lines = content.split(/\r?\n/);
1213
+ let inLinks = false;
1214
+ let depth = 0;
1215
+ let current;
1216
+ for (let i = 0; i < lines.length; i++) {
1217
+ const rawLine = lines[i] ?? '';
1218
+ const line = stripCwtLineComment(rawLine).trim();
1219
+ if (!inLinks) {
1220
+ if (/^links\s*=\s*\{/.test(line)) {
1221
+ inLinks = true;
1222
+ depth = countBraceDelta(line);
1223
+ }
1224
+ continue;
1225
+ }
1226
+ if (!current && depth === 1) {
1227
+ const linkMatch = line.match(/^([A-Za-z_][\w.-]*)\s*=\s*\{\s*$/);
1228
+ if (linkMatch?.[1])
1229
+ current = { name: linkMatch[1], line: i + 1, inputScopes: [] };
1230
+ }
1231
+ else if (current) {
1232
+ const inputMatch = line.match(/^input_scopes\s*=\s*(.*)$/i);
1233
+ if (inputMatch?.[1])
1234
+ current.inputScopes = splitRuleValueList(inputMatch[1]).map(normalizeScopeName);
1235
+ const outputMatch = line.match(/^output_scope\s*=\s*(.*)$/i);
1236
+ if (outputMatch?.[1]) {
1237
+ const outputScope = stripRuleValueBraces(outputMatch[1]).split(/\s+/)[0];
1238
+ if (outputScope)
1239
+ current.outputScope = normalizeScopeName(outputScope);
1240
+ }
1241
+ }
1242
+ depth += countBraceDelta(line);
1243
+ if (current && depth <= 1) {
1244
+ if (current.outputScope) {
1245
+ const inputScopes = current.inputScopes.length ? current.inputScopes : ['all'];
1246
+ const syntax = `${current.name} = scope link (${inputScopes.join(' | ')} -> ${current.outputScope})`;
1247
+ const linkHint = {
1248
+ 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.`,
1249
+ source: 'links.cwt',
1250
+ file: filePath,
1251
+ line: current.line,
1252
+ confidence: 'hint',
1253
+ };
1254
+ const scopeHints = buildSemanticHints({
1255
+ description: '',
1256
+ cwtDescription: '',
1257
+ scopes,
1258
+ relatedScopeNames: [...inputScopes, current.outputScope],
1259
+ cwtFile: filePath,
1260
+ cwtLine: current.line,
1261
+ });
1262
+ results.push({
1263
+ name: current.name,
1264
+ description: `Legal scope link from { ${inputScopes.join(' ')} } to ${current.outputScope}.`,
1265
+ scopes: inputScopes,
1266
+ syntax,
1267
+ category: 'scope_change',
1268
+ sourceFile: filePath,
1269
+ sourceLine: current.line,
1270
+ hardFacts: {
1271
+ category: 'scope_change',
1272
+ supportedScopes: inputScopes,
1273
+ pushScope: current.outputScope,
1274
+ valueReferences: [],
1275
+ syntax,
1276
+ cwtSource: { file: filePath, line: current.line },
1277
+ },
1278
+ semanticHints: [linkHint, ...scopeHints].slice(0, 8),
1279
+ });
1280
+ }
1281
+ current = undefined;
1282
+ }
1283
+ if (inLinks && depth <= 0) {
1284
+ inLinks = false;
1285
+ current = undefined;
1286
+ }
1287
+ }
1288
+ return results;
1289
+ }
1137
1290
  function buildSemanticHints(args) {
1138
1291
  const hints = [];
1139
1292
  const seen = new Set();
@@ -1287,7 +1440,12 @@ function expandIntentTokens(intent) {
1287
1440
  [/触发器|觸發器/g, ['trigger']],
1288
1441
  [/效果|效应|效應/g, ['effect']],
1289
1442
  ];
1290
- const expanded = [...direct];
1443
+ const expanded = [];
1444
+ for (const token of direct) {
1445
+ expanded.push(token);
1446
+ if (/[.:]/.test(token))
1447
+ expanded.push(...token.split(/[.:]+/).filter(Boolean));
1448
+ }
1291
1449
  for (const [pattern, tokens] of synonyms) {
1292
1450
  pattern.lastIndex = 0;
1293
1451
  if (pattern.test(intent))
@@ -1363,6 +1521,98 @@ function splitRuleValueList(value) {
1363
1521
  function stripRuleValueBraces(value) {
1364
1522
  return value.replace(/^\{\s*/, '').replace(/\s*\}$/, '').trim();
1365
1523
  }
1524
+ function normalizeRuleNameQuery(name, category) {
1525
+ const lowered = name.trim().toLowerCase();
1526
+ if (category !== 'scope_change' || !lowered.includes('.'))
1527
+ return lowered;
1528
+ const parts = lowered.split('.').map(part => part.trim()).filter(Boolean);
1529
+ return parts[parts.length - 1] ?? lowered;
1530
+ }
1531
+ function buildDottedScopeChainRule(query, contextScope, scopeChanges) {
1532
+ const rawParts = query.trim().split('.').map(normalizeScopeName).filter(Boolean);
1533
+ if (rawParts.length < 2)
1534
+ return undefined;
1535
+ const contextPointers = new Set(['from', 'prev', 'root', 'this']);
1536
+ const first = rawParts[0];
1537
+ const startsWithPointer = contextPointers.has(first);
1538
+ const initialScope = startsWithPointer ? contextScope?.trim().toLowerCase() : first;
1539
+ const linkNames = rawParts.slice(1);
1540
+ if (!initialScope || linkNames.length === 0)
1541
+ return undefined;
1542
+ const hops = [];
1543
+ let currentScope = initialScope;
1544
+ for (const linkName of linkNames) {
1545
+ const link = scopeChanges.find(rule => rule.name.toLowerCase() === linkName
1546
+ && !!rule.hardFacts?.pushScope
1547
+ && scopeListContains(rule.hardFacts.supportedScopes ?? rule.scopes, currentScope));
1548
+ const outputScope = link?.hardFacts?.pushScope?.trim().toLowerCase();
1549
+ if (!link || !outputScope)
1550
+ return undefined;
1551
+ hops.push({ link, inputScope: currentScope, outputScope });
1552
+ currentScope = outputScope;
1553
+ }
1554
+ if (hops.length === 0)
1555
+ return undefined;
1556
+ const normalizedQuery = rawParts.join('.');
1557
+ const syntax = `${normalizedQuery} = { ... }`;
1558
+ const source = hops[0].link;
1559
+ const hopText = hops.map(hop => `${hop.inputScope}.${hop.link.name} -> ${hop.outputScope}`).join('; ');
1560
+ const linkHints = hops.map((hop) => ({
1561
+ 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}'.`,
1562
+ source: 'links.cwt',
1563
+ file: hop.link.sourceFile,
1564
+ line: hop.link.sourceLine,
1565
+ confidence: 'hint',
1566
+ }));
1567
+ return {
1568
+ name: normalizedQuery,
1569
+ description: `Legal dotted scope chain from ${initialScope} to ${currentScope}. Hops: ${hopText}.`,
1570
+ scopes: [initialScope],
1571
+ syntax,
1572
+ category: 'scope_change',
1573
+ sourceFile: source.sourceFile,
1574
+ sourceLine: source.sourceLine,
1575
+ hardFacts: {
1576
+ category: 'scope_change',
1577
+ supportedScopes: [initialScope],
1578
+ pushScope: currentScope,
1579
+ valueReferences: [],
1580
+ syntax,
1581
+ cwtSource: source.sourceFile && source.sourceLine
1582
+ ? { file: source.sourceFile, line: source.sourceLine }
1583
+ : undefined,
1584
+ },
1585
+ semanticHints: linkHints.slice(0, 8),
1586
+ };
1587
+ }
1588
+ function scopeListContains(scopes, scope) {
1589
+ const lowerScope = scope.toLowerCase();
1590
+ return scopes.some(candidate => {
1591
+ const lower = candidate.toLowerCase();
1592
+ return lower === lowerScope || lower === 'all' || lower === 'any';
1593
+ });
1594
+ }
1595
+ function scoreRuleNameMatch(name, needle) {
1596
+ const lower = name.toLowerCase();
1597
+ if (lower === needle)
1598
+ return 0;
1599
+ if (lower.startsWith(needle))
1600
+ return 1;
1601
+ return 2;
1602
+ }
1603
+ function normalizeScopeName(scope) {
1604
+ return scope.replace(/^["']|["']$/g, '').trim().toLowerCase();
1605
+ }
1606
+ function countBraceDelta(line) {
1607
+ let delta = 0;
1608
+ for (const ch of line) {
1609
+ if (ch === '{')
1610
+ delta += 1;
1611
+ else if (ch === '}')
1612
+ delta -= 1;
1613
+ }
1614
+ return delta;
1615
+ }
1366
1616
  function splitWords(value) {
1367
1617
  return value.split(/\s+/).map(part => part.trim()).filter(Boolean);
1368
1618
  }
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
- {
2
- "name": "cwtools-shared",
3
- "version": "0.2.1",
4
- "description": "Shared protocol and tool contracts for the CWTools MCP server",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/Aa728848/cwtools-mcp.git"
9
- },
10
- "engines": {
11
- "node": ">=18"
12
- },
13
- "main": "dist/index.js",
14
- "types": "dist/index.d.ts",
15
- "files": [
16
- "dist",
17
- "package.json"
18
- ],
19
- "scripts": {
20
- "build": "tsc -p tsconfig.json",
21
- "prepack": "npm run build",
22
- "test:contracts": "ts-mocha -p tsconfig.test.json \"src/test/**/*.test.ts\""
23
- }
24
- }
1
+ {
2
+ "name": "cwtools-shared",
3
+ "version": "0.2.3",
4
+ "description": "Shared protocol and tool contracts for the CWTools MCP server",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Aa728848/cwtools-mcp.git"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "package.json"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "prepack": "npm run build",
22
+ "test:contracts": "ts-mocha -p tsconfig.test.json \"src/test/**/*.test.ts\""
23
+ }
24
+ }