@prorigo/protrak-forge 0.3.3 → 0.3.4

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.
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
3223
3223
  }
3224
3224
  return ind;
3225
3225
  }
3226
- function removeDotSegments(path14) {
3227
- let input = path14;
3226
+ function removeDotSegments(path15) {
3227
+ let input = path15;
3228
3228
  const output = [];
3229
3229
  let nextSlash = -1;
3230
3230
  let len = 0;
@@ -3423,8 +3423,8 @@ var require_schemes = __commonJS({
3423
3423
  wsComponent.secure = void 0;
3424
3424
  }
3425
3425
  if (wsComponent.resourceName) {
3426
- const [path14, query] = wsComponent.resourceName.split("?");
3427
- wsComponent.path = path14 && path14 !== "/" ? path14 : void 0;
3426
+ const [path15, query] = wsComponent.resourceName.split("?");
3427
+ wsComponent.path = path15 && path15 !== "/" ? path15 : void 0;
3428
3428
  wsComponent.query = query;
3429
3429
  wsComponent.resourceName = void 0;
3430
3430
  }
@@ -6786,12 +6786,12 @@ var require_dist = __commonJS({
6786
6786
  throw new Error(`Unknown format "${name}"`);
6787
6787
  return f;
6788
6788
  };
6789
- function addFormats(ajv, list, fs18, exportName) {
6789
+ function addFormats(ajv, list, fs19, exportName) {
6790
6790
  var _a2;
6791
6791
  var _b;
6792
6792
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6793
6793
  for (const f of list)
6794
- ajv.addFormat(f, fs18[f]);
6794
+ ajv.addFormat(f, fs19[f]);
6795
6795
  }
6796
6796
  module2.exports = exports2 = formatsPlugin;
6797
6797
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -7158,8 +7158,8 @@ function getErrorMap() {
7158
7158
 
7159
7159
  // node_modules/zod/v3/helpers/parseUtil.js
7160
7160
  var makeIssue = (params) => {
7161
- const { data, path: path14, errorMaps, issueData } = params;
7162
- const fullPath = [...path14, ...issueData.path || []];
7161
+ const { data, path: path15, errorMaps, issueData } = params;
7162
+ const fullPath = [...path15, ...issueData.path || []];
7163
7163
  const fullIssue = {
7164
7164
  ...issueData,
7165
7165
  path: fullPath
@@ -7274,11 +7274,11 @@ var errorUtil;
7274
7274
 
7275
7275
  // node_modules/zod/v3/types.js
7276
7276
  var ParseInputLazyPath = class {
7277
- constructor(parent, value, path14, key) {
7277
+ constructor(parent, value, path15, key) {
7278
7278
  this._cachedPath = [];
7279
7279
  this.parent = parent;
7280
7280
  this.data = value;
7281
- this._path = path14;
7281
+ this._path = path15;
7282
7282
  this._key = key;
7283
7283
  }
7284
7284
  get path() {
@@ -10924,10 +10924,10 @@ function mergeDefs(...defs) {
10924
10924
  function cloneDef(schema) {
10925
10925
  return mergeDefs(schema._zod.def);
10926
10926
  }
10927
- function getElementAtPath(obj, path14) {
10928
- if (!path14)
10927
+ function getElementAtPath(obj, path15) {
10928
+ if (!path15)
10929
10929
  return obj;
10930
- return path14.reduce((acc, key) => acc?.[key], obj);
10930
+ return path15.reduce((acc, key) => acc?.[key], obj);
10931
10931
  }
10932
10932
  function promiseAllObject(promisesObj) {
10933
10933
  const keys = Object.keys(promisesObj);
@@ -11310,11 +11310,11 @@ function aborted(x, startIndex = 0) {
11310
11310
  }
11311
11311
  return false;
11312
11312
  }
11313
- function prefixIssues(path14, issues) {
11313
+ function prefixIssues(path15, issues) {
11314
11314
  return issues.map((iss) => {
11315
11315
  var _a2;
11316
11316
  (_a2 = iss).path ?? (_a2.path = []);
11317
- iss.path.unshift(path14);
11317
+ iss.path.unshift(path15);
11318
11318
  return iss;
11319
11319
  });
11320
11320
  }
@@ -21430,9 +21430,9 @@ var SearchableMap = class _SearchableMap {
21430
21430
  if (!prefix.startsWith(this._prefix)) {
21431
21431
  throw new Error("Mismatched prefix");
21432
21432
  }
21433
- const [node, path14] = trackDown(this._tree, prefix.slice(this._prefix.length));
21433
+ const [node, path15] = trackDown(this._tree, prefix.slice(this._prefix.length));
21434
21434
  if (node === void 0) {
21435
- const [parentNode, key] = last(path14);
21435
+ const [parentNode, key] = last(path15);
21436
21436
  for (const k of parentNode.keys()) {
21437
21437
  if (k !== LEAF && k.startsWith(key)) {
21438
21438
  const node2 = /* @__PURE__ */ new Map();
@@ -21652,18 +21652,18 @@ var SearchableMap = class _SearchableMap {
21652
21652
  return _SearchableMap.from(Object.entries(object3));
21653
21653
  }
21654
21654
  };
21655
- var trackDown = (tree, key, path14 = []) => {
21655
+ var trackDown = (tree, key, path15 = []) => {
21656
21656
  if (key.length === 0 || tree == null) {
21657
- return [tree, path14];
21657
+ return [tree, path15];
21658
21658
  }
21659
21659
  for (const k of tree.keys()) {
21660
21660
  if (k !== LEAF && key.startsWith(k)) {
21661
- path14.push([tree, k]);
21662
- return trackDown(tree.get(k), key.slice(k.length), path14);
21661
+ path15.push([tree, k]);
21662
+ return trackDown(tree.get(k), key.slice(k.length), path15);
21663
21663
  }
21664
21664
  }
21665
- path14.push([tree, key]);
21666
- return trackDown(void 0, "", path14);
21665
+ path15.push([tree, key]);
21666
+ return trackDown(void 0, "", path15);
21667
21667
  };
21668
21668
  var lookup = (tree, key) => {
21669
21669
  if (key.length === 0 || tree == null) {
@@ -21705,38 +21705,38 @@ var createPath = (node, key) => {
21705
21705
  return node;
21706
21706
  };
21707
21707
  var remove = (tree, key) => {
21708
- const [node, path14] = trackDown(tree, key);
21708
+ const [node, path15] = trackDown(tree, key);
21709
21709
  if (node === void 0) {
21710
21710
  return;
21711
21711
  }
21712
21712
  node.delete(LEAF);
21713
21713
  if (node.size === 0) {
21714
- cleanup(path14);
21714
+ cleanup(path15);
21715
21715
  } else if (node.size === 1) {
21716
21716
  const [key2, value] = node.entries().next().value;
21717
- merge2(path14, key2, value);
21717
+ merge2(path15, key2, value);
21718
21718
  }
21719
21719
  };
21720
- var cleanup = (path14) => {
21721
- if (path14.length === 0) {
21720
+ var cleanup = (path15) => {
21721
+ if (path15.length === 0) {
21722
21722
  return;
21723
21723
  }
21724
- const [node, key] = last(path14);
21724
+ const [node, key] = last(path15);
21725
21725
  node.delete(key);
21726
21726
  if (node.size === 0) {
21727
- cleanup(path14.slice(0, -1));
21727
+ cleanup(path15.slice(0, -1));
21728
21728
  } else if (node.size === 1) {
21729
21729
  const [key2, value] = node.entries().next().value;
21730
21730
  if (key2 !== LEAF) {
21731
- merge2(path14.slice(0, -1), key2, value);
21731
+ merge2(path15.slice(0, -1), key2, value);
21732
21732
  }
21733
21733
  }
21734
21734
  };
21735
- var merge2 = (path14, key, value) => {
21736
- if (path14.length === 0) {
21735
+ var merge2 = (path15, key, value) => {
21736
+ if (path15.length === 0) {
21737
21737
  return;
21738
21738
  }
21739
- const [node, nodeKey] = last(path14);
21739
+ const [node, nodeKey] = last(path15);
21740
21740
  node.set(nodeKey + key, value);
21741
21741
  node.delete(nodeKey);
21742
21742
  };
@@ -23318,7 +23318,7 @@ function getClient() {
23318
23318
  }
23319
23319
 
23320
23320
  // src/version.ts
23321
- var PACKAGE_VERSION = "0.3.3";
23321
+ var PACKAGE_VERSION = "0.3.4";
23322
23322
 
23323
23323
  // src/tools/_schema-write-utils.ts
23324
23324
  var fs4 = __toESM(require("fs"));
@@ -23364,6 +23364,12 @@ function humanise(name) {
23364
23364
  if (!name) return "";
23365
23365
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").trim();
23366
23366
  }
23367
+ function readJsonFile(filePath) {
23368
+ return JSON.parse(fs4.readFileSync(filePath, "utf8"));
23369
+ }
23370
+ function writeJsonFilePath(filePath, data) {
23371
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
23372
+ }
23367
23373
  function writeJsonFile(dir, fileName, data, overwrite) {
23368
23374
  if (!fs4.existsSync(dir)) {
23369
23375
  fs4.mkdirSync(dir, { recursive: true });
@@ -23395,7 +23401,7 @@ __export(get_program_context_exports, {
23395
23401
  TOOL_DEF: () => TOOL_DEF,
23396
23402
  handle: () => handle
23397
23403
  });
23398
- var fs7 = __toESM(require("fs"));
23404
+ var fs8 = __toESM(require("fs"));
23399
23405
 
23400
23406
  // src/interface-contracts.ts
23401
23407
  var INTERFACE_CONTRACTS = {
@@ -23808,11 +23814,67 @@ function getOrCreateIndex(workspace) {
23808
23814
  return idx;
23809
23815
  }
23810
23816
 
23817
+ // src/tools/_program-utils.ts
23818
+ var fs7 = __toESM(require("fs"));
23819
+ var path6 = __toESM(require("path"));
23820
+ function readProgramByName(provider, programName) {
23821
+ const programsDir = provider.ws.programsDir;
23822
+ const csPath = path6.join(programsDir, `${programName}.cs`);
23823
+ if (!fs7.existsSync(csPath)) {
23824
+ const available = listAvailablePrograms(programsDir);
23825
+ return {
23826
+ kind: "not_found",
23827
+ message: `Program '${programName}' not found at ${csPath}`,
23828
+ available_programs: available
23829
+ };
23830
+ }
23831
+ const parsed = parseFile(csPath);
23832
+ if (!parsed) {
23833
+ const source = fs7.readFileSync(csPath, "utf8");
23834
+ return {
23835
+ kind: "parse_failed",
23836
+ data: {
23837
+ file_path: csPath,
23838
+ source,
23839
+ program_type: "Unknown",
23840
+ services_used: [],
23841
+ patterns_detected: [],
23842
+ class_name: ""
23843
+ }
23844
+ };
23845
+ }
23846
+ const jsonPath = path6.join(programsDir, `${programName}.json`);
23847
+ let metadata = {};
23848
+ if (fs7.existsSync(jsonPath)) {
23849
+ try {
23850
+ metadata = JSON.parse(fs7.readFileSync(jsonPath, "utf8"));
23851
+ } catch {
23852
+ }
23853
+ }
23854
+ return {
23855
+ kind: "success",
23856
+ data: {
23857
+ file_path: csPath,
23858
+ source: parsed.fullSource,
23859
+ program_type: String(metadata["programType"] ?? parsed.programType ?? ""),
23860
+ services_used: parsed.servicesUsed,
23861
+ patterns_detected: parsed.patterns,
23862
+ class_name: parsed.className
23863
+ }
23864
+ };
23865
+ }
23866
+ function listAvailablePrograms(programsDir) {
23867
+ if (!fs7.existsSync(programsDir) || !fs7.statSync(programsDir).isDirectory()) {
23868
+ return [];
23869
+ }
23870
+ return fs7.readdirSync(programsDir).filter((f) => f.endsWith(".cs")).map((f) => path6.basename(f, ".cs")).sort();
23871
+ }
23872
+
23811
23873
  // src/tools/get-program-context.ts
23812
23874
  var DETAIL_MODES = ["full", "summary", "schema_only", "patterns_only"];
23813
23875
  var TOOL_DEF = {
23814
23876
  name: "get_protrak_program_context",
23815
- description: "REQUIRED: Call this BEFORE writing any Protrak C# program. Returns everything needed in one call: type schema (attributes, lifecycle, relations), the C# interface to implement, relevant coding patterns with examples, similar existing programs from the workspace, and query definitions available for IQueryBuilderService. Use detail=summary on follow-up calls when you only need attribute/state names (skips heavy patterns/similar-programs payload). This is the primary tool for Protrak program generation.",
23877
+ description: "REQUIRED: Call this BEFORE writing OR refactoring any Protrak C# program. NEW PROGRAM: omit program_name. Returns type schema, C# interface, patterns, similar programs, and query definitions in one call. REFACTOR: pass program_name to load the existing program's source, services, and detected patterns alongside the same context \u2014 no need to chain read_protrak_program separately. Use detail=summary on follow-up calls when you only need attribute/state names. This is the primary tool for Protrak program work.",
23816
23878
  inputSchema: {
23817
23879
  type: "object",
23818
23880
  properties: {
@@ -23824,6 +23886,10 @@ var TOOL_DEF = {
23824
23886
  type: "string",
23825
23887
  description: "The program type to implement: PreCreate, PostCreate, PreUpdate, PostUpdate, PreDelete, PostDelete, PostConnect, PromoteAction, PromoteActionCommand, Scheduler, Common, Report"
23826
23888
  },
23889
+ program_name: {
23890
+ type: "string",
23891
+ description: "OMIT for new-program creation. Provide an existing program name (e.g. 'CalculateNextInvoiceDate') to switch to refactor mode \u2014 response will include existing_source, existing_services_used, existing_patterns_detected, existing_class_name, existing_file_path, and refactor_mode:true. The validation_step will still reference validate_protrak_program with the program_type you passed."
23892
+ },
23827
23893
  description: {
23828
23894
  type: "string",
23829
23895
  description: "What the program should do (e.g. 'validate that Title is not empty', 'send email when status changes'). Optional \u2014 patterns are still searched without it."
@@ -23848,13 +23914,23 @@ function buildSummarySchema(schema) {
23848
23914
  relation_names: schema.relations.map((r) => r.relation_type_name)
23849
23915
  };
23850
23916
  }
23851
- function buildValidationStep(programType, namespace, typeName) {
23917
+ function buildValidationStep({
23918
+ programType,
23919
+ namespace,
23920
+ typeName,
23921
+ refactorMode,
23922
+ existingProgramName
23923
+ }) {
23924
+ if (refactorMode) {
23925
+ return `After editing this ${programType || "program"}, call validate_protrak_program(code='<your edited source>', program_type='${programType}') to check for Protrak-specific gotchas (deadlocks, async void, DI shape, interface compliance). Then write the edited source back to Programs/${existingProgramName}.cs. Use namespace ${namespace}.`;
23926
+ }
23852
23927
  const fileName = typeName && programType ? `${typeName}${programType}.cs` : "<ProgramName>.cs";
23853
- return `After writing this ${programType || "program"} program, call validate_protrak_program(program_path='Programs/${fileName}') to check for errors. Use namespace ${namespace}.`;
23928
+ return `After writing this ${programType || "program"}, call validate_protrak_program(code='<your source>', program_type='${programType}') to check for errors. Once it passes, call generate_protrak_program(program_name='${typeName}${programType}', program_type='${programType}', source='<your source>') to write Programs/${fileName} and its companion JSON. Use namespace ${namespace}.`;
23854
23929
  }
23855
23930
  function handle(provider, args) {
23856
23931
  const typeName = args["type_name"] ?? "";
23857
23932
  const programType = args["program_type"] ?? "";
23933
+ const programName = args["program_name"] ?? "";
23858
23934
  const description = args["description"] ?? "";
23859
23935
  const detailRaw = args["detail"] ?? "full";
23860
23936
  if (!DETAIL_MODES.includes(detailRaw)) {
@@ -23864,6 +23940,33 @@ function handle(provider, args) {
23864
23940
  }
23865
23941
  const detail = detailRaw;
23866
23942
  const result = { detail };
23943
+ let refactorMode = false;
23944
+ const warnings = [];
23945
+ if (programName) {
23946
+ const existing = readProgramByName(provider, programName);
23947
+ if (existing.kind === "not_found") {
23948
+ return errorResponse(existing.message, {
23949
+ available_programs: existing.available_programs
23950
+ });
23951
+ }
23952
+ refactorMode = true;
23953
+ result["refactor_mode"] = true;
23954
+ result["existing_file_path"] = existing.data.file_path;
23955
+ result["existing_source"] = existing.data.source;
23956
+ result["existing_class_name"] = existing.data.class_name;
23957
+ result["existing_services_used"] = existing.data.services_used;
23958
+ result["existing_patterns_detected"] = existing.data.patterns_detected;
23959
+ if (existing.kind === "success" && existing.data.program_type && programType && existing.data.program_type !== programType) {
23960
+ warnings.push(
23961
+ `program_type mismatch \u2014 existing program implements '${existing.data.program_type}' but you passed '${programType}'. Refactor proceeds with the program_type you passed; verify the interface in existing_source matches before editing.`
23962
+ );
23963
+ }
23964
+ if (existing.kind === "parse_failed") {
23965
+ warnings.push(
23966
+ `Could not parse existing source for '${programName}'. existing_source is the raw file contents; services_used and patterns_detected are empty.`
23967
+ );
23968
+ }
23969
+ }
23867
23970
  const schema = provider.getTypeSchema(typeName);
23868
23971
  const wantsSchema = detail === "full" || detail === "summary" || detail === "schema_only";
23869
23972
  const wantsInterface = detail === "full" || detail === "summary" || detail === "schema_only";
@@ -23906,7 +24009,7 @@ function handle(provider, args) {
23906
24009
  }
23907
24010
  if (wantsSimilar) {
23908
24011
  const programsDir = provider.ws.programsDir;
23909
- if (fs7.existsSync(programsDir) && fs7.statSync(programsDir).isDirectory()) {
24012
+ if (fs8.existsSync(programsDir) && fs8.statSync(programsDir).isDirectory()) {
23910
24013
  try {
23911
24014
  const index = getOrCreateIndex(provider.ws);
23912
24015
  const similar = index.search(description, 3);
@@ -23926,7 +24029,7 @@ function handle(provider, args) {
23926
24029
  }
23927
24030
  if (wantsQueries) {
23928
24031
  const qdDir = provider.ws.queryDefinitionsDir;
23929
- if (fs7.existsSync(qdDir) && fs7.statSync(qdDir).isDirectory()) {
24032
+ if (fs8.existsSync(qdDir) && fs8.statSync(qdDir).isDirectory()) {
23930
24033
  const queries = provider.getQueryDefinitionsForType(typeName);
23931
24034
  if (detail === "summary") {
23932
24035
  result["query_definition_names"] = queries.map((q) => q.name);
@@ -23953,7 +24056,16 @@ function handle(provider, args) {
23953
24056
  "Return raw C# code only \u2014 no markdown, no code fences."
23954
24057
  ];
23955
24058
  }
23956
- result["validation_step"] = buildValidationStep(programType, provider.ws.namespace, typeName);
24059
+ result["validation_step"] = buildValidationStep({
24060
+ programType,
24061
+ namespace: provider.ws.namespace,
24062
+ typeName,
24063
+ refactorMode,
24064
+ existingProgramName: programName
24065
+ });
24066
+ if (warnings.length > 0) {
24067
+ result["warnings"] = warnings;
24068
+ }
23957
24069
  return JSON.stringify(result, null, 2);
23958
24070
  }
23959
24071
 
@@ -23965,8 +24077,8 @@ __export(get_layout_context_exports, {
23965
24077
  });
23966
24078
 
23967
24079
  // src/layout-provider.ts
23968
- var fs8 = __toESM(require("fs"));
23969
- var path6 = __toESM(require("path"));
24080
+ var fs9 = __toESM(require("fs"));
24081
+ var path7 = __toESM(require("path"));
23970
24082
  var LocalLayoutProvider = class {
23971
24083
  ws;
23972
24084
  _forms = null;
@@ -23981,13 +24093,13 @@ var LocalLayoutProvider = class {
23981
24093
  this.ws = ws;
23982
24094
  }
23983
24095
  loadLayoutDir(dir, map2, meta3, keyField, buildMeta, logLabel) {
23984
- if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) return;
23985
- for (const entry of fs8.readdirSync(dir).sort()) {
24096
+ if (!fs9.existsSync(dir) || !fs9.statSync(dir).isDirectory()) return;
24097
+ for (const entry of fs9.readdirSync(dir).sort()) {
23986
24098
  if (!entry.endsWith(".json")) continue;
23987
- const fp = path6.join(dir, entry);
24099
+ const fp = path7.join(dir, entry);
23988
24100
  try {
23989
- const data = JSON.parse(fs8.readFileSync(fp, "utf8"));
23990
- const name = data[keyField] ?? path6.basename(entry, ".json");
24101
+ const data = JSON.parse(fs9.readFileSync(fp, "utf8"));
24102
+ const name = data[keyField] ?? path7.basename(entry, ".json");
23991
24103
  map2.set(name, data);
23992
24104
  meta3.push(buildMeta(data, name, fp));
23993
24105
  } catch (e) {
@@ -25232,8 +25344,8 @@ __export(validate_program_exports, {
25232
25344
  TOOL_DEF: () => TOOL_DEF8,
25233
25345
  handle: () => handle8
25234
25346
  });
25235
- var fs9 = __toESM(require("fs"));
25236
- var path7 = __toESM(require("path"));
25347
+ var fs10 = __toESM(require("fs"));
25348
+ var path8 = __toESM(require("path"));
25237
25349
  var os = __toESM(require("os"));
25238
25350
  var crypto = __toESM(require("crypto"));
25239
25351
  var import_child_process = require("child_process");
@@ -25377,7 +25489,7 @@ function runStaticChecks(code, programType) {
25377
25489
  // src/tools/validate-program.ts
25378
25490
  var TOOL_DEF8 = {
25379
25491
  name: "validate_protrak_program",
25380
- description: "ALWAYS call this after writing a Protrak C# program. Validates the code with static analysis (interface compliance, async patterns, service declarations, null-check patterns). Optionally runs 'dotnet build' for full compilation check.",
25492
+ description: "Static analysis for Protrak C# programs. Catches Protrak-specific gotchas that look fine to a generic reader but break in production: (1) `.Result` / `.Wait()` on tasks \u2014 deadlocks under Protrak's async context. (2) `async void` methods \u2014 exceptions escape silently. (3) Services declared as private/readonly fields \u2014 Protrak DI requires PUBLIC PROPERTIES. (4) Missing interface implementation \u2014 e.g. PreCreate must implement IPreCreateTriggerProgramAsync<T>. (5) `*Async()` calls without `await` or `return` \u2014 silent fire-and-forget. (6) Service usage where the service isn't declared as a public property. (7) Missing namespace declaration. ALWAYS call this after writing OR editing a Protrak program \u2014 these failures are not visible to a syntactic read of the file. Optionally runs 'dotnet build' for full compilation check.",
25381
25493
  inputSchema: {
25382
25494
  type: "object",
25383
25495
  properties: {
@@ -25399,18 +25511,18 @@ var TOOL_DEF8 = {
25399
25511
  }
25400
25512
  };
25401
25513
  function runDotnetBuild(workspaceRoot, code) {
25402
- const csprojFiles = fs9.readdirSync(workspaceRoot).filter((e) => e.endsWith(".csproj"));
25514
+ const csprojFiles = fs10.readdirSync(workspaceRoot).filter((e) => e.endsWith(".csproj"));
25403
25515
  if (csprojFiles.length === 0) {
25404
25516
  return { success: false, errors: ["No .csproj file found in workspace"] };
25405
25517
  }
25406
- const programsDir = path7.join(workspaceRoot, "Programs");
25518
+ const programsDir = path8.join(workspaceRoot, "Programs");
25407
25519
  const tmpName = `_protrakgen_validate_${crypto.randomBytes(4).toString("hex")}.cs`;
25408
- const tmpPath = fs9.existsSync(programsDir) && fs9.statSync(programsDir).isDirectory() ? path7.join(programsDir, tmpName) : path7.join(os.tmpdir(), tmpName);
25520
+ const tmpPath = fs10.existsSync(programsDir) && fs10.statSync(programsDir).isDirectory() ? path8.join(programsDir, tmpName) : path8.join(os.tmpdir(), tmpName);
25409
25521
  try {
25410
- fs9.writeFileSync(tmpPath, code, "utf8");
25522
+ fs10.writeFileSync(tmpPath, code, "utf8");
25411
25523
  const result = (0, import_child_process.spawnSync)(
25412
25524
  "dotnet",
25413
- ["build", path7.join(workspaceRoot, csprojFiles[0]), "--no-restore", "-v", "quiet"],
25525
+ ["build", path8.join(workspaceRoot, csprojFiles[0]), "--no-restore", "-v", "quiet"],
25414
25526
  { cwd: workspaceRoot, encoding: "utf8", timeout: 3e4 }
25415
25527
  );
25416
25528
  if (result.error) {
@@ -25419,7 +25531,7 @@ function runDotnetBuild(workspaceRoot, code) {
25419
25531
  }
25420
25532
  return { success: false, errors: [result.error.message] };
25421
25533
  }
25422
- const tmpStem = path7.basename(tmpPath, ".cs");
25534
+ const tmpStem = path8.basename(tmpPath, ".cs");
25423
25535
  const allOutput = [result.stdout ?? "", result.stderr ?? ""].join("\n");
25424
25536
  const errors = allOutput.split("\n").filter((line) => (line.includes(": error ") || line.includes(": warning ")) && line.includes(tmpStem)).map((line) => line.trim());
25425
25537
  return { success: result.status === 0, errors };
@@ -25427,7 +25539,7 @@ function runDotnetBuild(workspaceRoot, code) {
25427
25539
  return { success: false, errors: [String(e)] };
25428
25540
  } finally {
25429
25541
  try {
25430
- fs9.unlinkSync(tmpPath);
25542
+ fs10.unlinkSync(tmpPath);
25431
25543
  } catch {
25432
25544
  }
25433
25545
  }
@@ -25452,7 +25564,7 @@ function handle8(provider, args) {
25452
25564
  `Fix the listed errors. For interface/coding-rule violations, re-read the contract via get_protrak_program_context(type_name='<TypeName>', program_type='${programType}', detail='schema_only') and adjust the program.`,
25453
25565
  "Re-run validate_protrak_program after fixing."
25454
25566
  ] : [
25455
- "Validation clean \u2014 write the file to Programs/<TypeName><ProgramType>.cs and proceed."
25567
+ `Validation clean. For a NEW program: call generate_protrak_program(program_name='<TypeName><ProgramType>', program_type='${programType}', source='<your code>') to write Programs/<name>.cs and its companion JSON. For a REFACTOR: write the edited source back to the existing Programs/<name>.cs file.`
25456
25568
  ];
25457
25569
  return JSON.stringify(response, null, 2);
25458
25570
  }
@@ -25463,7 +25575,7 @@ __export(search_protrak_knowledge_exports, {
25463
25575
  TOOL_DEF: () => TOOL_DEF9,
25464
25576
  handle: () => handle9
25465
25577
  });
25466
- var fs10 = __toESM(require("fs"));
25578
+ var fs11 = __toESM(require("fs"));
25467
25579
  var SCOPES = ["patterns", "api", "programs", "all"];
25468
25580
  var TOOL_DEF9 = {
25469
25581
  name: "search_protrak_knowledge",
@@ -25532,7 +25644,7 @@ function searchApi(query, programType, services, topK) {
25532
25644
  }
25533
25645
  function searchPrograms(provider, query, topK) {
25534
25646
  const programsDir = provider.ws.programsDir;
25535
- if (!fs10.existsSync(programsDir) || !fs10.statSync(programsDir).isDirectory()) {
25647
+ if (!fs11.existsSync(programsDir) || !fs11.statSync(programsDir).isDirectory()) {
25536
25648
  return {
25537
25649
  results: [],
25538
25650
  note: `No Programs directory found at '${programsDir}'.`
@@ -25847,7 +25959,7 @@ __export(list_query_definitions_exports, {
25847
25959
  TOOL_DEF: () => TOOL_DEF14,
25848
25960
  handle: () => handle14
25849
25961
  });
25850
- var fs11 = __toESM(require("fs"));
25962
+ var fs12 = __toESM(require("fs"));
25851
25963
  var TOOL_DEF14 = {
25852
25964
  name: "list_protrak_queries",
25853
25965
  description: "List Protrak query definitions available in the workspace. Call this when writing a program that uses IQueryBuilderService to see what query definitions exist and can be passed to GetQueryDefinitionResultByNameAsync. Filter by type_name to get only queries for the relevant entity type.",
@@ -25864,7 +25976,7 @@ var TOOL_DEF14 = {
25864
25976
  function handle14(provider, args) {
25865
25977
  const typeName = args["type_name"] || void 0;
25866
25978
  const qdDir = provider.ws.queryDefinitionsDir;
25867
- if (!fs11.existsSync(qdDir) || !fs11.statSync(qdDir).isDirectory()) {
25979
+ if (!fs12.existsSync(qdDir) || !fs12.statSync(qdDir).isDirectory()) {
25868
25980
  return JSON.stringify({
25869
25981
  query_definitions: [],
25870
25982
  note: `QueryDefinitions directory not found at '${qdDir}'.`
@@ -25913,8 +26025,8 @@ __export(list_protrak_programs_exports, {
25913
26025
  TOOL_DEF: () => TOOL_DEF16,
25914
26026
  handle: () => handle16
25915
26027
  });
25916
- var fs12 = __toESM(require("fs"));
25917
- var path8 = __toESM(require("path"));
26028
+ var fs13 = __toESM(require("fs"));
26029
+ var path9 = __toESM(require("path"));
25918
26030
  var TOOL_DEF16 = {
25919
26031
  name: "list_protrak_programs",
25920
26032
  description: "List Protrak C# programs in the workspace. Each entry includes the program name, the entity type it targets (when available from JSON metadata), and the program type (PreCreate, PostCreate, etc.). Filter by type_name and/or program_type to narrow. For relevance-ranked search by description use search_protrak_knowledge with scope='programs' instead.",
@@ -25936,7 +26048,7 @@ function handle16(provider, args) {
25936
26048
  const typeFilter = args["type_name"] || void 0;
25937
26049
  const programTypeFilter = args["program_type"] || void 0;
25938
26050
  const programsDir = provider.ws.programsDir;
25939
- if (!fs12.existsSync(programsDir) || !fs12.statSync(programsDir).isDirectory()) {
26051
+ if (!fs13.existsSync(programsDir) || !fs13.statSync(programsDir).isDirectory()) {
25940
26052
  return JSON.stringify(
25941
26053
  {
25942
26054
  programs: [],
@@ -25948,18 +26060,18 @@ function handle16(provider, args) {
25948
26060
  }
25949
26061
  const entries = [];
25950
26062
  const seen = /* @__PURE__ */ new Set();
25951
- for (const entry of fs12.readdirSync(programsDir).sort()) {
26063
+ for (const entry of fs13.readdirSync(programsDir).sort()) {
25952
26064
  if (!entry.endsWith(".json") && !entry.endsWith(".cs")) continue;
25953
- const programName = path8.basename(entry, path8.extname(entry));
26065
+ const programName = path9.basename(entry, path9.extname(entry));
25954
26066
  if (seen.has(programName)) continue;
25955
26067
  seen.add(programName);
25956
- const csPath = path8.join(programsDir, `${programName}.cs`);
25957
- const jsonPath = path8.join(programsDir, `${programName}.json`);
26068
+ const csPath = path9.join(programsDir, `${programName}.cs`);
26069
+ const jsonPath = path9.join(programsDir, `${programName}.json`);
25958
26070
  let programType = "";
25959
26071
  let typeName = "";
25960
- if (fs12.existsSync(jsonPath)) {
26072
+ if (fs13.existsSync(jsonPath)) {
25961
26073
  try {
25962
- const meta3 = JSON.parse(fs12.readFileSync(jsonPath, "utf8"));
26074
+ const meta3 = JSON.parse(fs13.readFileSync(jsonPath, "utf8"));
25963
26075
  programType = meta3["programType"] ?? "";
25964
26076
  typeName = meta3["typeName"] ?? meta3["entityTypeName"] ?? meta3["entityType"] ?? "";
25965
26077
  } catch {
@@ -25969,8 +26081,8 @@ function handle16(provider, args) {
25969
26081
  program_name: programName,
25970
26082
  program_type: programType,
25971
26083
  type_name: typeName,
25972
- has_source: fs12.existsSync(csPath),
25973
- file_path: fs12.existsSync(csPath) ? csPath : jsonPath
26084
+ has_source: fs13.existsSync(csPath),
26085
+ file_path: fs13.existsSync(csPath) ? csPath : jsonPath
25974
26086
  });
25975
26087
  }
25976
26088
  let filtered = entries;
@@ -25992,11 +26104,9 @@ __export(get_program_source_exports, {
25992
26104
  TOOL_DEF: () => TOOL_DEF17,
25993
26105
  handle: () => handle17
25994
26106
  });
25995
- var fs13 = __toESM(require("fs"));
25996
- var path9 = __toESM(require("path"));
25997
26107
  var TOOL_DEF17 = {
25998
26108
  name: "read_protrak_program",
25999
- description: "Read the source code of a specific Protrak C# program from the workspace. Use this to inspect existing programs for reference, understand implementation patterns, or check what services a program uses.",
26109
+ description: "Read the source code of a specific Protrak C# program from the workspace. Use this to inspect existing programs for reference, understand implementation patterns, or check what services a program uses. For refactoring, prefer get_protrak_program_context with program_name set \u2014 it returns the source AND the schema/interface/patterns context in one call.",
26000
26110
  inputSchema: {
26001
26111
  type: "object",
26002
26112
  properties: {
@@ -26010,32 +26120,15 @@ var TOOL_DEF17 = {
26010
26120
  };
26011
26121
  function handle17(provider, args) {
26012
26122
  const programName = args["program_name"] ?? "";
26013
- const programsDir = provider.ws.programsDir;
26014
- const csPath = path9.join(programsDir, `${programName}.cs`);
26015
- if (!fs13.existsSync(csPath)) {
26016
- return errorResponse(`Program '${programName}' not found at ${csPath}`);
26123
+ const result = readProgramByName(provider, programName);
26124
+ if (result.kind === "not_found") {
26125
+ return errorResponse(result.message, { available_programs: result.available_programs });
26017
26126
  }
26018
- const parsed = parseFile(csPath);
26019
- if (!parsed) {
26020
- const source = fs13.readFileSync(csPath, "utf8");
26021
- return JSON.stringify({ file_path: csPath, source, program_type: "Unknown", services_used: [] }, null, 2);
26022
- }
26023
- const jsonPath = path9.join(programsDir, `${programName}.json`);
26024
- let metadata = {};
26025
- if (fs13.existsSync(jsonPath)) {
26026
- try {
26027
- metadata = JSON.parse(fs13.readFileSync(jsonPath, "utf8"));
26028
- } catch {
26029
- }
26127
+ if (result.kind === "parse_failed") {
26128
+ const { file_path, source, program_type, services_used } = result.data;
26129
+ return JSON.stringify({ file_path, source, program_type, services_used }, null, JSON_INDENT);
26030
26130
  }
26031
- return JSON.stringify({
26032
- file_path: csPath,
26033
- source: parsed.fullSource,
26034
- program_type: metadata["programType"] ?? parsed.programType,
26035
- services_used: parsed.servicesUsed,
26036
- patterns_detected: parsed.patterns,
26037
- class_name: parsed.className
26038
- }, null, 2);
26131
+ return JSON.stringify(result.data, null, JSON_INDENT);
26039
26132
  }
26040
26133
 
26041
26134
  // src/tools/generate-query-definition.ts
@@ -26711,14 +26804,105 @@ function handle21(provider, args) {
26711
26804
  return successResponse({ file_path: result.filePath, template_name: templateName }, { nextSteps });
26712
26805
  }
26713
26806
 
26714
- // src/tools/attach-attribute-to-type.ts
26715
- var attach_attribute_to_type_exports = {};
26716
- __export(attach_attribute_to_type_exports, {
26807
+ // src/tools/generate-program.ts
26808
+ var generate_program_exports = {};
26809
+ __export(generate_program_exports, {
26717
26810
  TOOL_DEF: () => TOOL_DEF22,
26718
26811
  handle: () => handle22
26719
26812
  });
26720
26813
  var fs15 = __toESM(require("fs"));
26721
26814
  var path11 = __toESM(require("path"));
26815
+ var TOOL_DEF22 = {
26816
+ name: "generate_protrak_program",
26817
+ description: "Write a Protrak C# program to disk. Creates BOTH Programs/<name>.cs (your source) AND Programs/<name>.json (companion metadata required by Protrak's import flow). The programHashCode is intentionally empty \u2014 Protrak recomputes it on import. ALWAYS call validate_protrak_program on the source FIRST before invoking this tool. Refuses to overwrite existing files unless overwrite:true.",
26818
+ inputSchema: {
26819
+ type: "object",
26820
+ properties: {
26821
+ program_name: {
26822
+ type: "string",
26823
+ description: "PascalCase basename of the file (e.g. 'CalculateNextInvoiceDate'). Used as the file name for both .cs and .json. Must match the class name inside the source."
26824
+ },
26825
+ program_type: {
26826
+ type: "string",
26827
+ description: "Protrak's canonical programType value, written verbatim into the .json file. Common values seen in production workspaces: 'PreCreateTrigger', 'PostCreateTrigger', 'PreUpdateTrigger', 'PostUpdateTrigger', 'PreConnectTrigger', 'PostConnectTrigger', 'PromoteActionCommand', 'Scheduler', 'Common', 'Report', 'Workflow'. Inspect existing programs via list_protrak_programs / read_protrak_program to confirm the value Protrak expects in this workspace."
26828
+ },
26829
+ source: {
26830
+ type: "string",
26831
+ description: "Full C# source code for the program. Should already have passed validate_protrak_program. Written verbatim to Programs/<name>.cs."
26832
+ },
26833
+ overwrite: {
26834
+ type: "boolean",
26835
+ description: "When true, overwrite existing files. Default: false (errors on conflict).",
26836
+ default: false
26837
+ }
26838
+ },
26839
+ required: ["program_name", "program_type", "source"]
26840
+ }
26841
+ };
26842
+ function buildCompanionJson(programName, programType) {
26843
+ return {
26844
+ programName,
26845
+ programType,
26846
+ programCode: null,
26847
+ programHashCode: "",
26848
+ programStage: "Published"
26849
+ };
26850
+ }
26851
+ function handle22(provider, args) {
26852
+ const programName = args["program_name"] ?? "";
26853
+ const programType = args["program_type"] ?? "";
26854
+ const source = args["source"] ?? "";
26855
+ const overwrite = args["overwrite"] === true;
26856
+ if (!programName) return errorResponse("program_name is required");
26857
+ if (!isPascalCase(programName)) {
26858
+ return errorResponse(pascalCaseError("program_name", programName));
26859
+ }
26860
+ if (!programType) return errorResponse("program_type is required");
26861
+ if (!source) return errorResponse("source is required");
26862
+ const programsDir = provider.ws.programsDir;
26863
+ if (!fs15.existsSync(programsDir)) {
26864
+ fs15.mkdirSync(programsDir, { recursive: true });
26865
+ }
26866
+ const csPath = path11.join(programsDir, `${programName}.cs`);
26867
+ const jsonPath = path11.join(programsDir, `${programName}.json`);
26868
+ if (!overwrite) {
26869
+ const existing = [];
26870
+ if (fs15.existsSync(csPath)) existing.push(csPath);
26871
+ if (fs15.existsSync(jsonPath)) existing.push(jsonPath);
26872
+ if (existing.length > 0) {
26873
+ return errorResponse(
26874
+ `Refusing to overwrite existing file(s): ${existing.join(", ")}. Pass overwrite:true to replace.`,
26875
+ { existing_paths: existing }
26876
+ );
26877
+ }
26878
+ }
26879
+ fs15.writeFileSync(csPath, source, "utf8");
26880
+ const companion = buildCompanionJson(programName, programType);
26881
+ fs15.writeFileSync(jsonPath, JSON.stringify(companion, null, JSON_INDENT), "utf8");
26882
+ const nextSteps = [
26883
+ `Verify with read_protrak_program(program_name='${programName}').`,
26884
+ "On import, Protrak recomputes programHashCode automatically \u2014 leaving it empty here is intentional."
26885
+ ];
26886
+ return successResponse(
26887
+ {
26888
+ program_name: programName,
26889
+ program_type: programType,
26890
+ cs_path: csPath,
26891
+ json_path: jsonPath,
26892
+ companion_json: companion
26893
+ },
26894
+ { nextSteps }
26895
+ );
26896
+ }
26897
+
26898
+ // src/tools/attach-attribute-to-type.ts
26899
+ var attach_attribute_to_type_exports = {};
26900
+ __export(attach_attribute_to_type_exports, {
26901
+ TOOL_DEF: () => TOOL_DEF23,
26902
+ handle: () => handle23
26903
+ });
26904
+ var fs16 = __toESM(require("fs"));
26905
+ var path12 = __toESM(require("path"));
26722
26906
 
26723
26907
  // src/tools/_visibility-utils.ts
26724
26908
  function applyAttributeVisibility(lifecycleData, attributeName, visibleSet, opts) {
@@ -26773,7 +26957,7 @@ function applyAttributeVisibility(lifecycleData, attributeName, visibleSet, opts
26773
26957
 
26774
26958
  // src/tools/attach-attribute-to-type.ts
26775
26959
  var VISIBILITY_ALL = "all";
26776
- var TOOL_DEF22 = {
26960
+ var TOOL_DEF23 = {
26777
26961
  name: "attach_protrak_attribute_to_type",
26778
26962
  description: "Bind an existing Attribute to an existing Type without regenerating either file. Appends { name, index, isRequired } to the Type's attributes array with correct auto-indexing. Also ensures lifecycle visibility \u2014 by default the attribute is made visible in ALL lifecycle states (any state with viewableAttributesType=Specific has the attribute added to its viewableAttributes/editableAttributes lists). Pass replace:true to update an existing binding's isRequired flag in place. Use this instead of generate_protrak_type with overwrite:true when adding attributes to an existing Type \u2014 overwrite clobbers unrelated edits.",
26779
26963
  inputSchema: {
@@ -26809,12 +26993,12 @@ var TOOL_DEF22 = {
26809
26993
  }
26810
26994
  };
26811
26995
  function readJson(filePath) {
26812
- return JSON.parse(fs15.readFileSync(filePath, "utf8"));
26996
+ return JSON.parse(fs16.readFileSync(filePath, "utf8"));
26813
26997
  }
26814
26998
  function writeJson(filePath, data) {
26815
- fs15.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
26999
+ fs16.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
26816
27000
  }
26817
- function handle22(provider, args) {
27001
+ function handle23(provider, args) {
26818
27002
  const typeName = args["type_name"] ?? "";
26819
27003
  const attrName = args["attribute_name"] ?? "";
26820
27004
  const required2 = args["required"] === true;
@@ -26837,8 +27021,8 @@ function handle22(provider, args) {
26837
27021
  "lifecycle_states_visibility must be 'all' or an array of state names."
26838
27022
  );
26839
27023
  }
26840
- const typeFilePath = path11.join(provider.ws.typesDir, `${typeName}.json`);
26841
- if (!fs15.existsSync(typeFilePath)) {
27024
+ const typeFilePath = path12.join(provider.ws.typesDir, `${typeName}.json`);
27025
+ if (!fs16.existsSync(typeFilePath)) {
26842
27026
  const available = Object.keys(provider.types).sort();
26843
27027
  return errorResponse(`Type '${typeName}' was not found in ${provider.ws.typesDir}.`, {
26844
27028
  available_types: available
@@ -26873,8 +27057,8 @@ function handle22(provider, args) {
26873
27057
  let lifecyclePath = null;
26874
27058
  let visibilityChanges = [];
26875
27059
  if (lifecycleName) {
26876
- const lifecycleFilePath = path11.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
26877
- if (!fs15.existsSync(lifecycleFilePath)) {
27060
+ const lifecycleFilePath = path12.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27061
+ if (!fs16.existsSync(lifecycleFilePath)) {
26878
27062
  warnings.push(
26879
27063
  `Lifecycle '${lifecycleName}' (referenced by type '${typeName}') was not found in ${provider.ws.lifecyclesDir}. Visibility was not updated.`
26880
27064
  );
@@ -26928,12 +27112,12 @@ function handle22(provider, args) {
26928
27112
  // src/tools/attach-attribute-to-layout.ts
26929
27113
  var attach_attribute_to_layout_exports = {};
26930
27114
  __export(attach_attribute_to_layout_exports, {
26931
- TOOL_DEF: () => TOOL_DEF23,
26932
- handle: () => handle23
27115
+ TOOL_DEF: () => TOOL_DEF24,
27116
+ handle: () => handle24
26933
27117
  });
26934
- var fs16 = __toESM(require("fs"));
26935
- var path12 = __toESM(require("path"));
26936
- var TOOL_DEF23 = {
27118
+ var fs17 = __toESM(require("node:fs"));
27119
+ var path13 = __toESM(require("node:path"));
27120
+ var TOOL_DEF24 = {
26937
27121
  name: "attach_protrak_attribute_to_layout",
26938
27122
  description: "Add an attribute reference to a Layout Template's underlying form or type widget without regenerating either file. Resolves the layout's widget by templateWidgetKey (or null = first widget) and follows it to the underlying Form (formWidgetName) or TypeWidget (typeWidgetName) JSON, then inserts the attribute reference there with the canonical shape. For Form widgets, you may pass a container_id (formContainerKey) to pick a specific container; otherwise the attribute is added to the first container. Pass replace:true to overwrite an existing field with the same attributeName. Use this instead of regenerating the form/widget when adding a single attribute to an existing layout.",
26939
27123
  inputSchema: {
@@ -26964,16 +27148,10 @@ var TOOL_DEF23 = {
26964
27148
  required: ["layout_name", "attribute_name"]
26965
27149
  }
26966
27150
  };
26967
- function readJson2(filePath) {
26968
- return JSON.parse(fs16.readFileSync(filePath, "utf8"));
26969
- }
26970
- function writeJson2(filePath, data) {
26971
- fs16.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
26972
- }
26973
27151
  function findWidget(widgets, widgetId) {
26974
27152
  if (widgets.length === 0) return null;
26975
27153
  if (widgetId === null) return widgets[0];
26976
- return widgets.find((w) => String(w["templateWidgetKey"] ?? "") === widgetId) ?? null;
27154
+ return widgets.find((w) => (w["templateWidgetKey"] ?? "") === widgetId) ?? null;
26977
27155
  }
26978
27156
  function makeFormField(attrName) {
26979
27157
  return {
@@ -27002,11 +27180,11 @@ function makeWidgetColumn(attrName, attrType) {
27002
27180
  };
27003
27181
  }
27004
27182
  function attachToForm(ws, formName, attrName, containerId, replace) {
27005
- const filePath = path12.join(ws.formsDir, `${formName}.json`);
27006
- if (!fs16.existsSync(filePath)) {
27183
+ const filePath = path13.join(ws.formsDir, `${formName}.json`);
27184
+ if (!fs17.existsSync(filePath)) {
27007
27185
  return { error: `Form '${formName}' was not found in ${ws.formsDir}.` };
27008
27186
  }
27009
- const formData = readJson2(filePath);
27187
+ const formData = readJsonFile(filePath);
27010
27188
  const config2 = formData["formConfiguration"] ?? {};
27011
27189
  const containers = Array.isArray(config2["containers"]) ? config2["containers"] : [];
27012
27190
  if (containers.length === 0) {
@@ -27016,16 +27194,16 @@ function attachToForm(ws, formName, attrName, containerId, replace) {
27016
27194
  if (containerId === void 0) {
27017
27195
  target = containers[0];
27018
27196
  } else {
27019
- target = containers.find((c) => String(c["formContainerKey"] ?? "") === containerId);
27197
+ target = containers.find((c) => (c["formContainerKey"] ?? "") === containerId);
27020
27198
  if (!target) {
27021
27199
  return {
27022
27200
  error: `Container '${containerId}' not found in form '${formName}'.`,
27023
- available_containers: containers.map((c) => String(c["formContainerKey"] ?? ""))
27201
+ available_containers: containers.map((c) => c["formContainerKey"] ?? "")
27024
27202
  };
27025
27203
  }
27026
27204
  }
27027
27205
  const fields = Array.isArray(target["fields"]) ? target["fields"] : [];
27028
- const existingIdx = fields.findIndex((f) => String(f["attributeName"] ?? "") === attrName);
27206
+ const existingIdx = fields.findIndex((f) => (f["attributeName"] ?? "") === attrName);
27029
27207
  let action;
27030
27208
  if (existingIdx >= 0) {
27031
27209
  if (!replace) {
@@ -27040,28 +27218,28 @@ function attachToForm(ws, formName, attrName, containerId, replace) {
27040
27218
  action = "inserted";
27041
27219
  }
27042
27220
  target["fields"] = fields;
27043
- writeJson2(filePath, formData);
27221
+ writeJsonFilePath(filePath, formData);
27044
27222
  return {
27045
27223
  result: {
27046
27224
  filePath,
27047
27225
  formName,
27048
- containerKey: String(target["formContainerKey"] ?? ""),
27226
+ containerKey: target["formContainerKey"] ?? "",
27049
27227
  action
27050
27228
  }
27051
27229
  };
27052
27230
  }
27053
27231
  function attachToTypeWidget(provider, widgetName, attrName, replace) {
27054
- const filePath = path12.join(provider.ws.typeWidgetsDir, `${widgetName}.json`);
27055
- if (!fs16.existsSync(filePath)) {
27232
+ const filePath = path13.join(provider.ws.typeWidgetsDir, `${widgetName}.json`);
27233
+ if (!fs17.existsSync(filePath)) {
27056
27234
  return { error: `TypeWidget '${widgetName}' was not found in ${provider.ws.typeWidgetsDir}.` };
27057
27235
  }
27058
- const widgetData = readJson2(filePath);
27236
+ const widgetData = readJsonFile(filePath);
27059
27237
  const isRelation = widgetData["widgetType"] === "Relation";
27060
27238
  const configKey = isRelation ? "relationWidgetConfig" : "dashboardWidgetConfig";
27061
27239
  const config2 = widgetData[configKey] ?? {};
27062
27240
  const fields = Array.isArray(config2["fields"]) ? config2["fields"] : [];
27063
27241
  const attrType = String(provider.attributes[attrName]?.["attributeType"] ?? "Text");
27064
- const existingIdx = fields.findIndex((f) => String(f["attributeName"] ?? "") === attrName);
27242
+ const existingIdx = fields.findIndex((f) => (f["attributeName"] ?? "") === attrName);
27065
27243
  let action;
27066
27244
  if (existingIdx >= 0) {
27067
27245
  if (!replace) {
@@ -27082,10 +27260,57 @@ function attachToTypeWidget(provider, widgetName, attrName, replace) {
27082
27260
  }
27083
27261
  config2["fields"] = fields;
27084
27262
  widgetData[configKey] = config2;
27085
- writeJson2(filePath, widgetData);
27263
+ writeJsonFilePath(filePath, widgetData);
27086
27264
  return { result: { filePath, widgetName, action } };
27087
27265
  }
27088
- function handle23(provider, args) {
27266
+ function resolveTypeWidgetTarget(provider, widget, attrName, containerId, replace, warnings) {
27267
+ const typeWidgetName = widget["typeWidgetName"] ?? null;
27268
+ const widgetKey = widget["templateWidgetKey"] ?? "";
27269
+ if (!typeWidgetName) {
27270
+ return { error: `Layout widget '${widgetKey}' has widgetType=TypeWidget but no typeWidgetName.` };
27271
+ }
27272
+ if (containerId !== void 0) {
27273
+ warnings.push("container_id is only meaningful for Form widgets; ignored for TypeWidget.");
27274
+ }
27275
+ const out = attachToTypeWidget(provider, typeWidgetName, attrName, replace);
27276
+ if (out.error) {
27277
+ return { error: out.error };
27278
+ }
27279
+ return {
27280
+ target: {
27281
+ kind: "typeWidget",
27282
+ filePath: out.result.filePath,
27283
+ payload: { type_widget_name: out.result.widgetName, action: out.result.action }
27284
+ }
27285
+ };
27286
+ }
27287
+ function resolveTarget(provider, widget, attrName, containerId, replace, warnings) {
27288
+ const widgetKind = widget["widgetType"] ?? "";
27289
+ const formWidgetName = widget["formWidgetName"] ?? null;
27290
+ const typeWidgetName = widget["typeWidgetName"] ?? null;
27291
+ const widgetKey = widget["templateWidgetKey"] ?? "";
27292
+ if (widgetKind === "Form" || formWidgetName && !typeWidgetName) {
27293
+ if (!formWidgetName) {
27294
+ return { error: `Layout widget '${widgetKey}' has widgetType=Form but no formWidgetName.` };
27295
+ }
27296
+ const out = attachToForm(provider.ws, formWidgetName, attrName, containerId, replace);
27297
+ if (out.error) {
27298
+ return { error: out.error, extra: out.available_containers ? { available_containers: out.available_containers } : void 0 };
27299
+ }
27300
+ return {
27301
+ target: {
27302
+ kind: "form",
27303
+ filePath: out.result.filePath,
27304
+ payload: { form_name: out.result.formName, container_id: out.result.containerKey, action: out.result.action }
27305
+ }
27306
+ };
27307
+ }
27308
+ if (widgetKind === "TypeWidget" || typeWidgetName && !formWidgetName) {
27309
+ return resolveTypeWidgetTarget(provider, widget, attrName, containerId, replace, warnings);
27310
+ }
27311
+ return { error: `Layout widget '${widgetKey}' has unsupported widgetType='${widgetKind}'.` };
27312
+ }
27313
+ function handle24(provider, args) {
27089
27314
  const layoutName = args["layout_name"] ?? "";
27090
27315
  const widgetIdArg = args["widget_id"];
27091
27316
  const attrName = args["attribute_name"] ?? "";
@@ -27094,19 +27319,15 @@ function handle23(provider, args) {
27094
27319
  if (!layoutName) return errorResponse("layout_name is required");
27095
27320
  if (!attrName) return errorResponse("attribute_name is required");
27096
27321
  if (!isPascalCase(attrName)) return errorResponse(pascalCaseError("attribute_name", attrName));
27097
- let widgetId;
27098
- if (widgetIdArg === null || widgetIdArg === void 0) {
27099
- widgetId = null;
27100
- } else if (typeof widgetIdArg === "string") {
27101
- widgetId = widgetIdArg;
27102
- } else {
27322
+ if (widgetIdArg !== null && widgetIdArg !== void 0 && typeof widgetIdArg !== "string") {
27103
27323
  return errorResponse("widget_id must be a string templateWidgetKey or null.");
27104
27324
  }
27105
- const layoutPath = path12.join(provider.ws.layoutTemplatesDir, `${layoutName}.json`);
27106
- if (!fs16.existsSync(layoutPath)) {
27325
+ const widgetId = widgetIdArg ?? null;
27326
+ const layoutPath = path13.join(provider.ws.layoutTemplatesDir, `${layoutName}.json`);
27327
+ if (!fs17.existsSync(layoutPath)) {
27107
27328
  return errorResponse(`Layout template '${layoutName}' was not found in ${provider.ws.layoutTemplatesDir}.`);
27108
27329
  }
27109
- const layoutData = readJson2(layoutPath);
27330
+ const layoutData = readJsonFile(layoutPath);
27110
27331
  const config2 = layoutData["configuration"] ?? {};
27111
27332
  const widgets = Array.isArray(config2["widgets"]) ? config2["widgets"] : [];
27112
27333
  if (widgets.length === 0) {
@@ -27114,12 +27335,9 @@ function handle23(provider, args) {
27114
27335
  }
27115
27336
  const widget = findWidget(widgets, widgetId);
27116
27337
  if (!widget) {
27117
- return errorResponse(
27118
- `widget_id '${widgetId}' not found in layout '${layoutName}'.`,
27119
- {
27120
- available_widget_ids: widgets.map((w) => String(w["templateWidgetKey"] ?? ""))
27121
- }
27122
- );
27338
+ return errorResponse(`widget_id '${widgetId}' not found in layout '${layoutName}'.`, {
27339
+ available_widget_ids: widgets.map((w) => w["templateWidgetKey"] ?? "")
27340
+ });
27123
27341
  }
27124
27342
  const warnings = [];
27125
27343
  if (!provider.attributes[attrName]) {
@@ -27127,53 +27345,11 @@ function handle23(provider, args) {
27127
27345
  `Attribute '${attrName}' is not defined in ${provider.ws.attributesDir}. The reference will still be inserted; create the attribute or fix the name to resolve.`
27128
27346
  );
27129
27347
  }
27130
- const widgetKind = String(widget["widgetType"] ?? "");
27131
- const formWidgetName = widget["formWidgetName"] ?? null;
27132
- const typeWidgetName = widget["typeWidgetName"] ?? null;
27133
- let target;
27134
- if (widgetKind === "Form" || formWidgetName && !typeWidgetName) {
27135
- if (!formWidgetName) {
27136
- return errorResponse(`Layout widget '${String(widget["templateWidgetKey"])}' has widgetType=Form but no formWidgetName.`);
27137
- }
27138
- if (containerId !== void 0) {
27139
- }
27140
- const out = attachToForm(provider.ws, formWidgetName, attrName, containerId, replace);
27141
- if (out.error) {
27142
- return errorResponse(out.error, out.available_containers ? { available_containers: out.available_containers } : void 0);
27143
- }
27144
- target = {
27145
- kind: "form",
27146
- filePath: out.result.filePath,
27147
- payload: {
27148
- form_name: out.result.formName,
27149
- container_id: out.result.containerKey,
27150
- action: out.result.action
27151
- }
27152
- };
27153
- } else if (widgetKind === "TypeWidget" || typeWidgetName && !formWidgetName) {
27154
- if (!typeWidgetName) {
27155
- return errorResponse(`Layout widget '${String(widget["templateWidgetKey"])}' has widgetType=TypeWidget but no typeWidgetName.`);
27156
- }
27157
- if (containerId !== void 0) {
27158
- warnings.push("container_id is only meaningful for Form widgets; ignored for TypeWidget.");
27159
- }
27160
- const out = attachToTypeWidget(provider, typeWidgetName, attrName, replace);
27161
- if (out.error) {
27162
- return errorResponse(out.error);
27163
- }
27164
- target = {
27165
- kind: "typeWidget",
27166
- filePath: out.result.filePath,
27167
- payload: {
27168
- type_widget_name: out.result.widgetName,
27169
- action: out.result.action
27170
- }
27171
- };
27172
- } else {
27173
- return errorResponse(
27174
- `Layout widget '${String(widget["templateWidgetKey"])}' has unsupported widgetType='${widgetKind}'.`
27175
- );
27348
+ const resolved = resolveTarget(provider, widget, attrName, containerId, replace, warnings);
27349
+ if ("error" in resolved) {
27350
+ return errorResponse(resolved.error, resolved.extra);
27176
27351
  }
27352
+ const { target } = resolved;
27177
27353
  const nextSteps = [
27178
27354
  `Verify with read_protrak_layout(layout_name='${layoutName}').`,
27179
27355
  `If the attribute is not yet bound to the type, call attach_protrak_attribute_to_type to bind it.`
@@ -27182,7 +27358,7 @@ function handle23(provider, args) {
27182
27358
  {
27183
27359
  layout_name: layoutName,
27184
27360
  layout_path: layoutPath,
27185
- widget_id: String(widget["templateWidgetKey"] ?? ""),
27361
+ widget_id: widget["templateWidgetKey"] ?? "",
27186
27362
  target_kind: target.kind,
27187
27363
  target_path: target.filePath,
27188
27364
  ...target.payload
@@ -27194,12 +27370,12 @@ function handle23(provider, args) {
27194
27370
  // src/tools/set-attribute-state-visibility.ts
27195
27371
  var set_attribute_state_visibility_exports = {};
27196
27372
  __export(set_attribute_state_visibility_exports, {
27197
- TOOL_DEF: () => TOOL_DEF24,
27198
- handle: () => handle24
27373
+ TOOL_DEF: () => TOOL_DEF25,
27374
+ handle: () => handle25
27199
27375
  });
27200
- var fs17 = __toESM(require("fs"));
27201
- var path13 = __toESM(require("path"));
27202
- var TOOL_DEF24 = {
27376
+ var fs18 = __toESM(require("fs"));
27377
+ var path14 = __toESM(require("path"));
27378
+ var TOOL_DEF25 = {
27203
27379
  name: "set_protrak_attribute_state_visibility",
27204
27380
  description: "Set the per-state lifecycle visibility of a single attribute on a Type. Pass visible_states to specify exactly which states the attribute is visible in; omit visible_states to default to 'visible in every state'. Adds the attribute to viewableAttributes/editableAttributes of any Specific-mode state in the visible list, removes it from Specific-mode states not in the list, and converts All-mode states to Specific to enforce hiding when needed (preserving every other type attribute's visibility). Use this to retune visibility on an already-bound attribute; use attach_protrak_attribute_to_type to add a brand-new attribute binding.",
27205
27381
  inputSchema: {
@@ -27222,13 +27398,13 @@ var TOOL_DEF24 = {
27222
27398
  required: ["type_name", "attribute_name"]
27223
27399
  }
27224
27400
  };
27225
- function readJson3(filePath) {
27226
- return JSON.parse(fs17.readFileSync(filePath, "utf8"));
27401
+ function readJson2(filePath) {
27402
+ return JSON.parse(fs18.readFileSync(filePath, "utf8"));
27227
27403
  }
27228
- function writeJson3(filePath, data) {
27229
- fs17.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
27404
+ function writeJson2(filePath, data) {
27405
+ fs18.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
27230
27406
  }
27231
- function handle24(provider, args) {
27407
+ function handle25(provider, args) {
27232
27408
  const typeName = args["type_name"] ?? "";
27233
27409
  const attrName = args["attribute_name"] ?? "";
27234
27410
  const hasVisibleStatesArg = Object.prototype.hasOwnProperty.call(args, "visible_states");
@@ -27248,14 +27424,14 @@ function handle24(provider, args) {
27248
27424
  }
27249
27425
  visibleStates = new Set(rawVisible);
27250
27426
  }
27251
- const typeFilePath = path13.join(provider.ws.typesDir, `${typeName}.json`);
27252
- if (!fs17.existsSync(typeFilePath)) {
27427
+ const typeFilePath = path14.join(provider.ws.typesDir, `${typeName}.json`);
27428
+ if (!fs18.existsSync(typeFilePath)) {
27253
27429
  const available = Object.keys(provider.types).sort();
27254
27430
  return errorResponse(`Type '${typeName}' was not found in ${provider.ws.typesDir}.`, {
27255
27431
  available_types: available
27256
27432
  });
27257
27433
  }
27258
- const typeData = readJson3(typeFilePath);
27434
+ const typeData = readJson2(typeFilePath);
27259
27435
  const typeAttributes = Array.isArray(typeData["attributes"]) ? typeData["attributes"] : [];
27260
27436
  const knownTypeAttributes = typeAttributes.map((a) => a.name);
27261
27437
  const warnings = [];
@@ -27268,13 +27444,13 @@ function handle24(provider, args) {
27268
27444
  if (!lifecycleName) {
27269
27445
  return errorResponse(`Type '${typeName}' has no lifecycleName set; cannot adjust visibility.`);
27270
27446
  }
27271
- const lifecycleFilePath = path13.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27272
- if (!fs17.existsSync(lifecycleFilePath)) {
27447
+ const lifecycleFilePath = path14.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27448
+ if (!fs18.existsSync(lifecycleFilePath)) {
27273
27449
  return errorResponse(
27274
27450
  `Lifecycle '${lifecycleName}' (referenced by type '${typeName}') was not found in ${provider.ws.lifecyclesDir}.`
27275
27451
  );
27276
27452
  }
27277
- const lifecycleData = readJson3(lifecycleFilePath);
27453
+ const lifecycleData = readJson2(lifecycleFilePath);
27278
27454
  const stateNames = (lifecycleData["states"] ?? []).map(
27279
27455
  (s) => String(s["name"] ?? "")
27280
27456
  );
@@ -27291,7 +27467,7 @@ function handle24(provider, args) {
27291
27467
  knownTypeAttributes
27292
27468
  });
27293
27469
  if (changed) {
27294
- writeJson3(lifecycleFilePath, lifecycleData);
27470
+ writeJson2(lifecycleFilePath, lifecycleData);
27295
27471
  }
27296
27472
  const nextSteps = [
27297
27473
  `Verify with read_protrak_schema_element(kind='lifecycle', name='${lifecycleName}').`,
@@ -27346,6 +27522,7 @@ var TOOLS = [
27346
27522
  generate_form_exports,
27347
27523
  generate_type_widget_exports,
27348
27524
  generate_layout_template_exports,
27525
+ generate_program_exports,
27349
27526
  // Surgical mutators — modify existing files without regeneration/clobber
27350
27527
  attach_attribute_to_type_exports,
27351
27528
  attach_attribute_to_layout_exports,
@@ -27385,11 +27562,18 @@ Restart VS Code (or reload the window) so npx can fetch the latest version.
27385
27562
  log("info", `Protrak Forge MCP ready \u2014 ${TOOLS.length} tools | workspace: ${workspace.namespace} | types: ${typeCount} | programs: ${programCount}`);
27386
27563
  const SERVER_INSTRUCTIONS = `You are working in a Protrak customization workspace. Protrak programs are C# classes that implement trigger interfaces (PreCreate, PostCreate, PreUpdate, PromoteActionCommand, Scheduler, etc.).
27387
27564
 
27388
- WORKFLOW for writing Protrak programs:
27565
+ WORKFLOW for writing a NEW Protrak program:
27389
27566
  1. ALWAYS call get_protrak_program_context first \u2014 it returns the type schema, interface contract, coding patterns, similar programs, and available query definitions (for IQueryBuilderService) in one call.
27390
27567
  2. Write the C# program following the interface contract and patterns.
27391
27568
  3. If the program uses IQueryBuilderService: pick a query name from the query_definitions returned by get_protrak_program_context, or call generate_protrak_query_definition to create a new one before writing the program.
27392
27569
  4. ALWAYS call validate_protrak_program after writing to check for errors.
27570
+ 5. Once validation is clean, call generate_protrak_program(program_name, program_type, source) \u2014 this writes BOTH Programs/<name>.cs AND Programs/<name>.json (the companion metadata Protrak's import flow needs). Do NOT use the filesystem Write tool for new programs \u2014 you will skip the .json companion and the program will not import.
27571
+
27572
+ WORKFLOW for REFACTORING an existing Protrak program:
27573
+ 1. Call get_protrak_program_context with program_name set to the existing program name. This returns the current source AND the schema/interface/patterns context in ONE call (no need to chain read_protrak_program separately).
27574
+ 2. Edit the C# source.
27575
+ 3. Call validate_protrak_program(code, program_type) \u2014 same as for new programs.
27576
+ 4. Write the edited source back to the existing Programs/<name>.cs file. Do NOT call generate_protrak_program for refactors \u2014 the companion .json already exists.
27393
27577
 
27394
27578
  WORKFLOW for creating a QueryDefinition (no program needed):
27395
27579
  1. Call read_protrak_schema_element with kind='type' to get attribute names and valid state names.