@prorigo/protrak-forge 0.3.3 → 0.3.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.
@@ -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(path17) {
3227
+ let input = path17;
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 [path17, query] = wsComponent.resourceName.split("?");
3427
+ wsComponent.path = path17 && path17 !== "/" ? path17 : 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, fs21, 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, fs21[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: path17, errorMaps, issueData } = params;
7162
+ const fullPath = [...path17, ...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, path17, key) {
7278
7278
  this._cachedPath = [];
7279
7279
  this.parent = parent;
7280
7280
  this.data = value;
7281
- this._path = path14;
7281
+ this._path = path17;
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, path17) {
10928
+ if (!path17)
10929
10929
  return obj;
10930
- return path14.reduce((acc, key) => acc?.[key], obj);
10930
+ return path17.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(path17, 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(path17);
11318
11318
  return iss;
11319
11319
  });
11320
11320
  }
@@ -20869,7 +20869,8 @@ function makeWorkspace(root, namespace) {
20869
20869
  formsDir: path.join(root, "Forms"),
20870
20870
  typeWidgetsDir: path.join(root, "TypeWidgets"),
20871
20871
  layoutTemplatesDir: path.join(root, "LayoutTemplates"),
20872
- homeLayoutsDir: path.join(root, "HomeLayouts")
20872
+ homeLayoutsDir: path.join(root, "HomeLayouts"),
20873
+ notificationTemplatesDir: path.join(root, "NotificationTemplates")
20873
20874
  };
20874
20875
  }
20875
20876
  function extractNamespace(csprojPath) {
@@ -21430,9 +21431,9 @@ var SearchableMap = class _SearchableMap {
21430
21431
  if (!prefix.startsWith(this._prefix)) {
21431
21432
  throw new Error("Mismatched prefix");
21432
21433
  }
21433
- const [node, path14] = trackDown(this._tree, prefix.slice(this._prefix.length));
21434
+ const [node, path17] = trackDown(this._tree, prefix.slice(this._prefix.length));
21434
21435
  if (node === void 0) {
21435
- const [parentNode, key] = last(path14);
21436
+ const [parentNode, key] = last(path17);
21436
21437
  for (const k of parentNode.keys()) {
21437
21438
  if (k !== LEAF && k.startsWith(key)) {
21438
21439
  const node2 = /* @__PURE__ */ new Map();
@@ -21652,18 +21653,18 @@ var SearchableMap = class _SearchableMap {
21652
21653
  return _SearchableMap.from(Object.entries(object3));
21653
21654
  }
21654
21655
  };
21655
- var trackDown = (tree, key, path14 = []) => {
21656
+ var trackDown = (tree, key, path17 = []) => {
21656
21657
  if (key.length === 0 || tree == null) {
21657
- return [tree, path14];
21658
+ return [tree, path17];
21658
21659
  }
21659
21660
  for (const k of tree.keys()) {
21660
21661
  if (k !== LEAF && key.startsWith(k)) {
21661
- path14.push([tree, k]);
21662
- return trackDown(tree.get(k), key.slice(k.length), path14);
21662
+ path17.push([tree, k]);
21663
+ return trackDown(tree.get(k), key.slice(k.length), path17);
21663
21664
  }
21664
21665
  }
21665
- path14.push([tree, key]);
21666
- return trackDown(void 0, "", path14);
21666
+ path17.push([tree, key]);
21667
+ return trackDown(void 0, "", path17);
21667
21668
  };
21668
21669
  var lookup = (tree, key) => {
21669
21670
  if (key.length === 0 || tree == null) {
@@ -21705,38 +21706,38 @@ var createPath = (node, key) => {
21705
21706
  return node;
21706
21707
  };
21707
21708
  var remove = (tree, key) => {
21708
- const [node, path14] = trackDown(tree, key);
21709
+ const [node, path17] = trackDown(tree, key);
21709
21710
  if (node === void 0) {
21710
21711
  return;
21711
21712
  }
21712
21713
  node.delete(LEAF);
21713
21714
  if (node.size === 0) {
21714
- cleanup(path14);
21715
+ cleanup(path17);
21715
21716
  } else if (node.size === 1) {
21716
21717
  const [key2, value] = node.entries().next().value;
21717
- merge2(path14, key2, value);
21718
+ merge2(path17, key2, value);
21718
21719
  }
21719
21720
  };
21720
- var cleanup = (path14) => {
21721
- if (path14.length === 0) {
21721
+ var cleanup = (path17) => {
21722
+ if (path17.length === 0) {
21722
21723
  return;
21723
21724
  }
21724
- const [node, key] = last(path14);
21725
+ const [node, key] = last(path17);
21725
21726
  node.delete(key);
21726
21727
  if (node.size === 0) {
21727
- cleanup(path14.slice(0, -1));
21728
+ cleanup(path17.slice(0, -1));
21728
21729
  } else if (node.size === 1) {
21729
21730
  const [key2, value] = node.entries().next().value;
21730
21731
  if (key2 !== LEAF) {
21731
- merge2(path14.slice(0, -1), key2, value);
21732
+ merge2(path17.slice(0, -1), key2, value);
21732
21733
  }
21733
21734
  }
21734
21735
  };
21735
- var merge2 = (path14, key, value) => {
21736
- if (path14.length === 0) {
21736
+ var merge2 = (path17, key, value) => {
21737
+ if (path17.length === 0) {
21737
21738
  return;
21738
21739
  }
21739
- const [node, nodeKey] = last(path14);
21740
+ const [node, nodeKey] = last(path17);
21740
21741
  node.set(nodeKey + key, value);
21741
21742
  node.delete(nodeKey);
21742
21743
  };
@@ -23318,7 +23319,7 @@ function getClient() {
23318
23319
  }
23319
23320
 
23320
23321
  // src/version.ts
23321
- var PACKAGE_VERSION = "0.3.3";
23322
+ var PACKAGE_VERSION = "0.3.5";
23322
23323
 
23323
23324
  // src/tools/_schema-write-utils.ts
23324
23325
  var fs4 = __toESM(require("fs"));
@@ -23364,6 +23365,12 @@ function humanise(name) {
23364
23365
  if (!name) return "";
23365
23366
  return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").trim();
23366
23367
  }
23368
+ function readJsonFile(filePath) {
23369
+ return JSON.parse(fs4.readFileSync(filePath, "utf8"));
23370
+ }
23371
+ function writeJsonFilePath(filePath, data) {
23372
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
23373
+ }
23367
23374
  function writeJsonFile(dir, fileName, data, overwrite) {
23368
23375
  if (!fs4.existsSync(dir)) {
23369
23376
  fs4.mkdirSync(dir, { recursive: true });
@@ -23395,7 +23402,7 @@ __export(get_program_context_exports, {
23395
23402
  TOOL_DEF: () => TOOL_DEF,
23396
23403
  handle: () => handle
23397
23404
  });
23398
- var fs7 = __toESM(require("fs"));
23405
+ var fs8 = __toESM(require("fs"));
23399
23406
 
23400
23407
  // src/interface-contracts.ts
23401
23408
  var INTERFACE_CONTRACTS = {
@@ -23808,11 +23815,67 @@ function getOrCreateIndex(workspace) {
23808
23815
  return idx;
23809
23816
  }
23810
23817
 
23818
+ // src/tools/_program-utils.ts
23819
+ var fs7 = __toESM(require("fs"));
23820
+ var path6 = __toESM(require("path"));
23821
+ function readProgramByName(provider, programName) {
23822
+ const programsDir = provider.ws.programsDir;
23823
+ const csPath = path6.join(programsDir, `${programName}.cs`);
23824
+ if (!fs7.existsSync(csPath)) {
23825
+ const available = listAvailablePrograms(programsDir);
23826
+ return {
23827
+ kind: "not_found",
23828
+ message: `Program '${programName}' not found at ${csPath}`,
23829
+ available_programs: available
23830
+ };
23831
+ }
23832
+ const parsed = parseFile(csPath);
23833
+ if (!parsed) {
23834
+ const source = fs7.readFileSync(csPath, "utf8");
23835
+ return {
23836
+ kind: "parse_failed",
23837
+ data: {
23838
+ file_path: csPath,
23839
+ source,
23840
+ program_type: "Unknown",
23841
+ services_used: [],
23842
+ patterns_detected: [],
23843
+ class_name: ""
23844
+ }
23845
+ };
23846
+ }
23847
+ const jsonPath = path6.join(programsDir, `${programName}.json`);
23848
+ let metadata = {};
23849
+ if (fs7.existsSync(jsonPath)) {
23850
+ try {
23851
+ metadata = JSON.parse(fs7.readFileSync(jsonPath, "utf8"));
23852
+ } catch {
23853
+ }
23854
+ }
23855
+ return {
23856
+ kind: "success",
23857
+ data: {
23858
+ file_path: csPath,
23859
+ source: parsed.fullSource,
23860
+ program_type: String(metadata["programType"] ?? parsed.programType ?? ""),
23861
+ services_used: parsed.servicesUsed,
23862
+ patterns_detected: parsed.patterns,
23863
+ class_name: parsed.className
23864
+ }
23865
+ };
23866
+ }
23867
+ function listAvailablePrograms(programsDir) {
23868
+ if (!fs7.existsSync(programsDir) || !fs7.statSync(programsDir).isDirectory()) {
23869
+ return [];
23870
+ }
23871
+ return fs7.readdirSync(programsDir).filter((f) => f.endsWith(".cs")).map((f) => path6.basename(f, ".cs")).sort();
23872
+ }
23873
+
23811
23874
  // src/tools/get-program-context.ts
23812
23875
  var DETAIL_MODES = ["full", "summary", "schema_only", "patterns_only"];
23813
23876
  var TOOL_DEF = {
23814
23877
  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.",
23878
+ 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
23879
  inputSchema: {
23817
23880
  type: "object",
23818
23881
  properties: {
@@ -23824,6 +23887,10 @@ var TOOL_DEF = {
23824
23887
  type: "string",
23825
23888
  description: "The program type to implement: PreCreate, PostCreate, PreUpdate, PostUpdate, PreDelete, PostDelete, PostConnect, PromoteAction, PromoteActionCommand, Scheduler, Common, Report"
23826
23889
  },
23890
+ program_name: {
23891
+ type: "string",
23892
+ 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."
23893
+ },
23827
23894
  description: {
23828
23895
  type: "string",
23829
23896
  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 +23915,23 @@ function buildSummarySchema(schema) {
23848
23915
  relation_names: schema.relations.map((r) => r.relation_type_name)
23849
23916
  };
23850
23917
  }
23851
- function buildValidationStep(programType, namespace, typeName) {
23918
+ function buildValidationStep({
23919
+ programType,
23920
+ namespace,
23921
+ typeName,
23922
+ refactorMode,
23923
+ existingProgramName
23924
+ }) {
23925
+ if (refactorMode) {
23926
+ 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}.`;
23927
+ }
23852
23928
  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}.`;
23929
+ 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
23930
  }
23855
23931
  function handle(provider, args) {
23856
23932
  const typeName = args["type_name"] ?? "";
23857
23933
  const programType = args["program_type"] ?? "";
23934
+ const programName = args["program_name"] ?? "";
23858
23935
  const description = args["description"] ?? "";
23859
23936
  const detailRaw = args["detail"] ?? "full";
23860
23937
  if (!DETAIL_MODES.includes(detailRaw)) {
@@ -23864,6 +23941,33 @@ function handle(provider, args) {
23864
23941
  }
23865
23942
  const detail = detailRaw;
23866
23943
  const result = { detail };
23944
+ let refactorMode = false;
23945
+ const warnings = [];
23946
+ if (programName) {
23947
+ const existing = readProgramByName(provider, programName);
23948
+ if (existing.kind === "not_found") {
23949
+ return errorResponse(existing.message, {
23950
+ available_programs: existing.available_programs
23951
+ });
23952
+ }
23953
+ refactorMode = true;
23954
+ result["refactor_mode"] = true;
23955
+ result["existing_file_path"] = existing.data.file_path;
23956
+ result["existing_source"] = existing.data.source;
23957
+ result["existing_class_name"] = existing.data.class_name;
23958
+ result["existing_services_used"] = existing.data.services_used;
23959
+ result["existing_patterns_detected"] = existing.data.patterns_detected;
23960
+ if (existing.kind === "success" && existing.data.program_type && programType && existing.data.program_type !== programType) {
23961
+ warnings.push(
23962
+ `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.`
23963
+ );
23964
+ }
23965
+ if (existing.kind === "parse_failed") {
23966
+ warnings.push(
23967
+ `Could not parse existing source for '${programName}'. existing_source is the raw file contents; services_used and patterns_detected are empty.`
23968
+ );
23969
+ }
23970
+ }
23867
23971
  const schema = provider.getTypeSchema(typeName);
23868
23972
  const wantsSchema = detail === "full" || detail === "summary" || detail === "schema_only";
23869
23973
  const wantsInterface = detail === "full" || detail === "summary" || detail === "schema_only";
@@ -23906,7 +24010,7 @@ function handle(provider, args) {
23906
24010
  }
23907
24011
  if (wantsSimilar) {
23908
24012
  const programsDir = provider.ws.programsDir;
23909
- if (fs7.existsSync(programsDir) && fs7.statSync(programsDir).isDirectory()) {
24013
+ if (fs8.existsSync(programsDir) && fs8.statSync(programsDir).isDirectory()) {
23910
24014
  try {
23911
24015
  const index = getOrCreateIndex(provider.ws);
23912
24016
  const similar = index.search(description, 3);
@@ -23926,7 +24030,7 @@ function handle(provider, args) {
23926
24030
  }
23927
24031
  if (wantsQueries) {
23928
24032
  const qdDir = provider.ws.queryDefinitionsDir;
23929
- if (fs7.existsSync(qdDir) && fs7.statSync(qdDir).isDirectory()) {
24033
+ if (fs8.existsSync(qdDir) && fs8.statSync(qdDir).isDirectory()) {
23930
24034
  const queries = provider.getQueryDefinitionsForType(typeName);
23931
24035
  if (detail === "summary") {
23932
24036
  result["query_definition_names"] = queries.map((q) => q.name);
@@ -23953,7 +24057,16 @@ function handle(provider, args) {
23953
24057
  "Return raw C# code only \u2014 no markdown, no code fences."
23954
24058
  ];
23955
24059
  }
23956
- result["validation_step"] = buildValidationStep(programType, provider.ws.namespace, typeName);
24060
+ result["validation_step"] = buildValidationStep({
24061
+ programType,
24062
+ namespace: provider.ws.namespace,
24063
+ typeName,
24064
+ refactorMode,
24065
+ existingProgramName: programName
24066
+ });
24067
+ if (warnings.length > 0) {
24068
+ result["warnings"] = warnings;
24069
+ }
23957
24070
  return JSON.stringify(result, null, 2);
23958
24071
  }
23959
24072
 
@@ -23965,8 +24078,8 @@ __export(get_layout_context_exports, {
23965
24078
  });
23966
24079
 
23967
24080
  // src/layout-provider.ts
23968
- var fs8 = __toESM(require("fs"));
23969
- var path6 = __toESM(require("path"));
24081
+ var fs9 = __toESM(require("fs"));
24082
+ var path7 = __toESM(require("path"));
23970
24083
  var LocalLayoutProvider = class {
23971
24084
  ws;
23972
24085
  _forms = null;
@@ -23977,17 +24090,19 @@ var LocalLayoutProvider = class {
23977
24090
  _layoutTemplateMeta = null;
23978
24091
  _homeLayouts = null;
23979
24092
  _homeLayoutMeta = null;
24093
+ _notificationTemplates = null;
24094
+ _notificationTemplateMeta = null;
23980
24095
  constructor(ws) {
23981
24096
  this.ws = ws;
23982
24097
  }
23983
24098
  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()) {
24099
+ if (!fs9.existsSync(dir) || !fs9.statSync(dir).isDirectory()) return;
24100
+ for (const entry of fs9.readdirSync(dir).sort()) {
23986
24101
  if (!entry.endsWith(".json")) continue;
23987
- const fp = path6.join(dir, entry);
24102
+ const fp = path7.join(dir, entry);
23988
24103
  try {
23989
- const data = JSON.parse(fs8.readFileSync(fp, "utf8"));
23990
- const name = data[keyField] ?? path6.basename(entry, ".json");
24104
+ const data = JSON.parse(fs9.readFileSync(fp, "utf8"));
24105
+ const name = data[keyField] ?? path7.basename(entry, ".json");
23991
24106
  map2.set(name, data);
23992
24107
  meta3.push(buildMeta(data, name, fp));
23993
24108
  } catch (e) {
@@ -24047,6 +24162,34 @@ var LocalLayoutProvider = class {
24047
24162
  "home layout"
24048
24163
  );
24049
24164
  }
24165
+ ensureNotificationTemplates() {
24166
+ if (this._notificationTemplates !== null) return;
24167
+ this._notificationTemplates = /* @__PURE__ */ new Map();
24168
+ this._notificationTemplateMeta = [];
24169
+ const dir = this.ws.notificationTemplatesDir;
24170
+ if (!fs9.existsSync(dir) || !fs9.statSync(dir).isDirectory()) return;
24171
+ for (const entry of fs9.readdirSync(dir).sort()) {
24172
+ if (!entry.endsWith(".json")) continue;
24173
+ const fp = path7.join(dir, entry);
24174
+ try {
24175
+ const data = JSON.parse(fs9.readFileSync(fp, "utf8"));
24176
+ const name = path7.basename(entry, ".json");
24177
+ const htmlPath = path7.join(dir, `${name}.html`);
24178
+ this._notificationTemplates.set(name, data);
24179
+ this._notificationTemplateMeta.push({
24180
+ name,
24181
+ title: String(data["title"] ?? name),
24182
+ target: String(data["target"] ?? ""),
24183
+ templateState: String(data["templateState"] ?? ""),
24184
+ hasEmailBody: fs9.existsSync(htmlPath),
24185
+ hasMessageText: Boolean(data["messageText"]),
24186
+ filePath: fp
24187
+ });
24188
+ } catch (e) {
24189
+ log("warn", `Failed to load notification template ${fp}: ${e}`);
24190
+ }
24191
+ }
24192
+ }
24050
24193
  formsForType(typeName, mode) {
24051
24194
  this.ensureForms();
24052
24195
  return this._formMeta.filter(
@@ -24069,6 +24212,12 @@ var LocalLayoutProvider = class {
24069
24212
  this.ensureHomeLayouts();
24070
24213
  return this._homeLayoutMeta;
24071
24214
  }
24215
+ notificationTemplatesForType(typeName) {
24216
+ this.ensureNotificationTemplates();
24217
+ if (!typeName) return this._notificationTemplateMeta;
24218
+ const lower = typeName.toLowerCase();
24219
+ return this._notificationTemplateMeta.filter((t) => t.title.toLowerCase().includes(lower));
24220
+ }
24072
24221
  getLayoutJson(layoutType, name) {
24073
24222
  switch (layoutType) {
24074
24223
  case "form":
@@ -24119,6 +24268,10 @@ var LocalLayoutProvider = class {
24119
24268
  this._homeLayouts = null;
24120
24269
  this._homeLayoutMeta = null;
24121
24270
  break;
24271
+ case "notificationTemplate":
24272
+ this._notificationTemplates = null;
24273
+ this._notificationTemplateMeta = null;
24274
+ break;
24122
24275
  }
24123
24276
  }
24124
24277
  };
@@ -25232,8 +25385,8 @@ __export(validate_program_exports, {
25232
25385
  TOOL_DEF: () => TOOL_DEF8,
25233
25386
  handle: () => handle8
25234
25387
  });
25235
- var fs9 = __toESM(require("fs"));
25236
- var path7 = __toESM(require("path"));
25388
+ var fs10 = __toESM(require("fs"));
25389
+ var path8 = __toESM(require("path"));
25237
25390
  var os = __toESM(require("os"));
25238
25391
  var crypto = __toESM(require("crypto"));
25239
25392
  var import_child_process = require("child_process");
@@ -25377,7 +25530,7 @@ function runStaticChecks(code, programType) {
25377
25530
  // src/tools/validate-program.ts
25378
25531
  var TOOL_DEF8 = {
25379
25532
  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.",
25533
+ 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
25534
  inputSchema: {
25382
25535
  type: "object",
25383
25536
  properties: {
@@ -25399,18 +25552,18 @@ var TOOL_DEF8 = {
25399
25552
  }
25400
25553
  };
25401
25554
  function runDotnetBuild(workspaceRoot, code) {
25402
- const csprojFiles = fs9.readdirSync(workspaceRoot).filter((e) => e.endsWith(".csproj"));
25555
+ const csprojFiles = fs10.readdirSync(workspaceRoot).filter((e) => e.endsWith(".csproj"));
25403
25556
  if (csprojFiles.length === 0) {
25404
25557
  return { success: false, errors: ["No .csproj file found in workspace"] };
25405
25558
  }
25406
- const programsDir = path7.join(workspaceRoot, "Programs");
25559
+ const programsDir = path8.join(workspaceRoot, "Programs");
25407
25560
  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);
25561
+ const tmpPath = fs10.existsSync(programsDir) && fs10.statSync(programsDir).isDirectory() ? path8.join(programsDir, tmpName) : path8.join(os.tmpdir(), tmpName);
25409
25562
  try {
25410
- fs9.writeFileSync(tmpPath, code, "utf8");
25563
+ fs10.writeFileSync(tmpPath, code, "utf8");
25411
25564
  const result = (0, import_child_process.spawnSync)(
25412
25565
  "dotnet",
25413
- ["build", path7.join(workspaceRoot, csprojFiles[0]), "--no-restore", "-v", "quiet"],
25566
+ ["build", path8.join(workspaceRoot, csprojFiles[0]), "--no-restore", "-v", "quiet"],
25414
25567
  { cwd: workspaceRoot, encoding: "utf8", timeout: 3e4 }
25415
25568
  );
25416
25569
  if (result.error) {
@@ -25419,7 +25572,7 @@ function runDotnetBuild(workspaceRoot, code) {
25419
25572
  }
25420
25573
  return { success: false, errors: [result.error.message] };
25421
25574
  }
25422
- const tmpStem = path7.basename(tmpPath, ".cs");
25575
+ const tmpStem = path8.basename(tmpPath, ".cs");
25423
25576
  const allOutput = [result.stdout ?? "", result.stderr ?? ""].join("\n");
25424
25577
  const errors = allOutput.split("\n").filter((line) => (line.includes(": error ") || line.includes(": warning ")) && line.includes(tmpStem)).map((line) => line.trim());
25425
25578
  return { success: result.status === 0, errors };
@@ -25427,7 +25580,7 @@ function runDotnetBuild(workspaceRoot, code) {
25427
25580
  return { success: false, errors: [String(e)] };
25428
25581
  } finally {
25429
25582
  try {
25430
- fs9.unlinkSync(tmpPath);
25583
+ fs10.unlinkSync(tmpPath);
25431
25584
  } catch {
25432
25585
  }
25433
25586
  }
@@ -25452,7 +25605,7 @@ function handle8(provider, args) {
25452
25605
  `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
25606
  "Re-run validate_protrak_program after fixing."
25454
25607
  ] : [
25455
- "Validation clean \u2014 write the file to Programs/<TypeName><ProgramType>.cs and proceed."
25608
+ `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
25609
  ];
25457
25610
  return JSON.stringify(response, null, 2);
25458
25611
  }
@@ -25463,7 +25616,7 @@ __export(search_protrak_knowledge_exports, {
25463
25616
  TOOL_DEF: () => TOOL_DEF9,
25464
25617
  handle: () => handle9
25465
25618
  });
25466
- var fs10 = __toESM(require("fs"));
25619
+ var fs11 = __toESM(require("fs"));
25467
25620
  var SCOPES = ["patterns", "api", "programs", "all"];
25468
25621
  var TOOL_DEF9 = {
25469
25622
  name: "search_protrak_knowledge",
@@ -25532,7 +25685,7 @@ function searchApi(query, programType, services, topK) {
25532
25685
  }
25533
25686
  function searchPrograms(provider, query, topK) {
25534
25687
  const programsDir = provider.ws.programsDir;
25535
- if (!fs10.existsSync(programsDir) || !fs10.statSync(programsDir).isDirectory()) {
25688
+ if (!fs11.existsSync(programsDir) || !fs11.statSync(programsDir).isDirectory()) {
25536
25689
  return {
25537
25690
  results: [],
25538
25691
  note: `No Programs directory found at '${programsDir}'.`
@@ -25847,7 +26000,7 @@ __export(list_query_definitions_exports, {
25847
26000
  TOOL_DEF: () => TOOL_DEF14,
25848
26001
  handle: () => handle14
25849
26002
  });
25850
- var fs11 = __toESM(require("fs"));
26003
+ var fs12 = __toESM(require("fs"));
25851
26004
  var TOOL_DEF14 = {
25852
26005
  name: "list_protrak_queries",
25853
26006
  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 +26017,7 @@ var TOOL_DEF14 = {
25864
26017
  function handle14(provider, args) {
25865
26018
  const typeName = args["type_name"] || void 0;
25866
26019
  const qdDir = provider.ws.queryDefinitionsDir;
25867
- if (!fs11.existsSync(qdDir) || !fs11.statSync(qdDir).isDirectory()) {
26020
+ if (!fs12.existsSync(qdDir) || !fs12.statSync(qdDir).isDirectory()) {
25868
26021
  return JSON.stringify({
25869
26022
  query_definitions: [],
25870
26023
  note: `QueryDefinitions directory not found at '${qdDir}'.`
@@ -25913,8 +26066,8 @@ __export(list_protrak_programs_exports, {
25913
26066
  TOOL_DEF: () => TOOL_DEF16,
25914
26067
  handle: () => handle16
25915
26068
  });
25916
- var fs12 = __toESM(require("fs"));
25917
- var path8 = __toESM(require("path"));
26069
+ var fs13 = __toESM(require("fs"));
26070
+ var path9 = __toESM(require("path"));
25918
26071
  var TOOL_DEF16 = {
25919
26072
  name: "list_protrak_programs",
25920
26073
  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 +26089,7 @@ function handle16(provider, args) {
25936
26089
  const typeFilter = args["type_name"] || void 0;
25937
26090
  const programTypeFilter = args["program_type"] || void 0;
25938
26091
  const programsDir = provider.ws.programsDir;
25939
- if (!fs12.existsSync(programsDir) || !fs12.statSync(programsDir).isDirectory()) {
26092
+ if (!fs13.existsSync(programsDir) || !fs13.statSync(programsDir).isDirectory()) {
25940
26093
  return JSON.stringify(
25941
26094
  {
25942
26095
  programs: [],
@@ -25948,18 +26101,18 @@ function handle16(provider, args) {
25948
26101
  }
25949
26102
  const entries = [];
25950
26103
  const seen = /* @__PURE__ */ new Set();
25951
- for (const entry of fs12.readdirSync(programsDir).sort()) {
26104
+ for (const entry of fs13.readdirSync(programsDir).sort()) {
25952
26105
  if (!entry.endsWith(".json") && !entry.endsWith(".cs")) continue;
25953
- const programName = path8.basename(entry, path8.extname(entry));
26106
+ const programName = path9.basename(entry, path9.extname(entry));
25954
26107
  if (seen.has(programName)) continue;
25955
26108
  seen.add(programName);
25956
- const csPath = path8.join(programsDir, `${programName}.cs`);
25957
- const jsonPath = path8.join(programsDir, `${programName}.json`);
26109
+ const csPath = path9.join(programsDir, `${programName}.cs`);
26110
+ const jsonPath = path9.join(programsDir, `${programName}.json`);
25958
26111
  let programType = "";
25959
26112
  let typeName = "";
25960
- if (fs12.existsSync(jsonPath)) {
26113
+ if (fs13.existsSync(jsonPath)) {
25961
26114
  try {
25962
- const meta3 = JSON.parse(fs12.readFileSync(jsonPath, "utf8"));
26115
+ const meta3 = JSON.parse(fs13.readFileSync(jsonPath, "utf8"));
25963
26116
  programType = meta3["programType"] ?? "";
25964
26117
  typeName = meta3["typeName"] ?? meta3["entityTypeName"] ?? meta3["entityType"] ?? "";
25965
26118
  } catch {
@@ -25969,8 +26122,8 @@ function handle16(provider, args) {
25969
26122
  program_name: programName,
25970
26123
  program_type: programType,
25971
26124
  type_name: typeName,
25972
- has_source: fs12.existsSync(csPath),
25973
- file_path: fs12.existsSync(csPath) ? csPath : jsonPath
26125
+ has_source: fs13.existsSync(csPath),
26126
+ file_path: fs13.existsSync(csPath) ? csPath : jsonPath
25974
26127
  });
25975
26128
  }
25976
26129
  let filtered = entries;
@@ -25992,11 +26145,9 @@ __export(get_program_source_exports, {
25992
26145
  TOOL_DEF: () => TOOL_DEF17,
25993
26146
  handle: () => handle17
25994
26147
  });
25995
- var fs13 = __toESM(require("fs"));
25996
- var path9 = __toESM(require("path"));
25997
26148
  var TOOL_DEF17 = {
25998
26149
  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.",
26150
+ 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
26151
  inputSchema: {
26001
26152
  type: "object",
26002
26153
  properties: {
@@ -26010,32 +26161,15 @@ var TOOL_DEF17 = {
26010
26161
  };
26011
26162
  function handle17(provider, args) {
26012
26163
  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}`);
26017
- }
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);
26164
+ const result = readProgramByName(provider, programName);
26165
+ if (result.kind === "not_found") {
26166
+ return errorResponse(result.message, { available_programs: result.available_programs });
26022
26167
  }
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
- }
26168
+ if (result.kind === "parse_failed") {
26169
+ const { file_path, source, program_type, services_used } = result.data;
26170
+ return JSON.stringify({ file_path, source, program_type, services_used }, null, JSON_INDENT);
26030
26171
  }
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);
26172
+ return JSON.stringify(result.data, null, JSON_INDENT);
26039
26173
  }
26040
26174
 
26041
26175
  // src/tools/generate-query-definition.ts
@@ -26711,14 +26845,105 @@ function handle21(provider, args) {
26711
26845
  return successResponse({ file_path: result.filePath, template_name: templateName }, { nextSteps });
26712
26846
  }
26713
26847
 
26714
- // src/tools/attach-attribute-to-type.ts
26715
- var attach_attribute_to_type_exports = {};
26716
- __export(attach_attribute_to_type_exports, {
26848
+ // src/tools/generate-program.ts
26849
+ var generate_program_exports = {};
26850
+ __export(generate_program_exports, {
26717
26851
  TOOL_DEF: () => TOOL_DEF22,
26718
26852
  handle: () => handle22
26719
26853
  });
26720
26854
  var fs15 = __toESM(require("fs"));
26721
26855
  var path11 = __toESM(require("path"));
26856
+ var TOOL_DEF22 = {
26857
+ name: "generate_protrak_program",
26858
+ 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.",
26859
+ inputSchema: {
26860
+ type: "object",
26861
+ properties: {
26862
+ program_name: {
26863
+ type: "string",
26864
+ 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."
26865
+ },
26866
+ program_type: {
26867
+ type: "string",
26868
+ 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."
26869
+ },
26870
+ source: {
26871
+ type: "string",
26872
+ description: "Full C# source code for the program. Should already have passed validate_protrak_program. Written verbatim to Programs/<name>.cs."
26873
+ },
26874
+ overwrite: {
26875
+ type: "boolean",
26876
+ description: "When true, overwrite existing files. Default: false (errors on conflict).",
26877
+ default: false
26878
+ }
26879
+ },
26880
+ required: ["program_name", "program_type", "source"]
26881
+ }
26882
+ };
26883
+ function buildCompanionJson(programName, programType) {
26884
+ return {
26885
+ programName,
26886
+ programType,
26887
+ programCode: null,
26888
+ programHashCode: "",
26889
+ programStage: "Published"
26890
+ };
26891
+ }
26892
+ function handle22(provider, args) {
26893
+ const programName = args["program_name"] ?? "";
26894
+ const programType = args["program_type"] ?? "";
26895
+ const source = args["source"] ?? "";
26896
+ const overwrite = args["overwrite"] === true;
26897
+ if (!programName) return errorResponse("program_name is required");
26898
+ if (!isPascalCase(programName)) {
26899
+ return errorResponse(pascalCaseError("program_name", programName));
26900
+ }
26901
+ if (!programType) return errorResponse("program_type is required");
26902
+ if (!source) return errorResponse("source is required");
26903
+ const programsDir = provider.ws.programsDir;
26904
+ if (!fs15.existsSync(programsDir)) {
26905
+ fs15.mkdirSync(programsDir, { recursive: true });
26906
+ }
26907
+ const csPath = path11.join(programsDir, `${programName}.cs`);
26908
+ const jsonPath = path11.join(programsDir, `${programName}.json`);
26909
+ if (!overwrite) {
26910
+ const existing = [];
26911
+ if (fs15.existsSync(csPath)) existing.push(csPath);
26912
+ if (fs15.existsSync(jsonPath)) existing.push(jsonPath);
26913
+ if (existing.length > 0) {
26914
+ return errorResponse(
26915
+ `Refusing to overwrite existing file(s): ${existing.join(", ")}. Pass overwrite:true to replace.`,
26916
+ { existing_paths: existing }
26917
+ );
26918
+ }
26919
+ }
26920
+ fs15.writeFileSync(csPath, source, "utf8");
26921
+ const companion = buildCompanionJson(programName, programType);
26922
+ fs15.writeFileSync(jsonPath, JSON.stringify(companion, null, JSON_INDENT), "utf8");
26923
+ const nextSteps = [
26924
+ `Verify with read_protrak_program(program_name='${programName}').`,
26925
+ "On import, Protrak recomputes programHashCode automatically \u2014 leaving it empty here is intentional."
26926
+ ];
26927
+ return successResponse(
26928
+ {
26929
+ program_name: programName,
26930
+ program_type: programType,
26931
+ cs_path: csPath,
26932
+ json_path: jsonPath,
26933
+ companion_json: companion
26934
+ },
26935
+ { nextSteps }
26936
+ );
26937
+ }
26938
+
26939
+ // src/tools/attach-attribute-to-type.ts
26940
+ var attach_attribute_to_type_exports = {};
26941
+ __export(attach_attribute_to_type_exports, {
26942
+ TOOL_DEF: () => TOOL_DEF23,
26943
+ handle: () => handle23
26944
+ });
26945
+ var fs16 = __toESM(require("fs"));
26946
+ var path12 = __toESM(require("path"));
26722
26947
 
26723
26948
  // src/tools/_visibility-utils.ts
26724
26949
  function applyAttributeVisibility(lifecycleData, attributeName, visibleSet, opts) {
@@ -26773,7 +26998,7 @@ function applyAttributeVisibility(lifecycleData, attributeName, visibleSet, opts
26773
26998
 
26774
26999
  // src/tools/attach-attribute-to-type.ts
26775
27000
  var VISIBILITY_ALL = "all";
26776
- var TOOL_DEF22 = {
27001
+ var TOOL_DEF23 = {
26777
27002
  name: "attach_protrak_attribute_to_type",
26778
27003
  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
27004
  inputSchema: {
@@ -26809,12 +27034,12 @@ var TOOL_DEF22 = {
26809
27034
  }
26810
27035
  };
26811
27036
  function readJson(filePath) {
26812
- return JSON.parse(fs15.readFileSync(filePath, "utf8"));
27037
+ return JSON.parse(fs16.readFileSync(filePath, "utf8"));
26813
27038
  }
26814
27039
  function writeJson(filePath, data) {
26815
- fs15.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
27040
+ fs16.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
26816
27041
  }
26817
- function handle22(provider, args) {
27042
+ function handle23(provider, args) {
26818
27043
  const typeName = args["type_name"] ?? "";
26819
27044
  const attrName = args["attribute_name"] ?? "";
26820
27045
  const required2 = args["required"] === true;
@@ -26837,8 +27062,8 @@ function handle22(provider, args) {
26837
27062
  "lifecycle_states_visibility must be 'all' or an array of state names."
26838
27063
  );
26839
27064
  }
26840
- const typeFilePath = path11.join(provider.ws.typesDir, `${typeName}.json`);
26841
- if (!fs15.existsSync(typeFilePath)) {
27065
+ const typeFilePath = path12.join(provider.ws.typesDir, `${typeName}.json`);
27066
+ if (!fs16.existsSync(typeFilePath)) {
26842
27067
  const available = Object.keys(provider.types).sort();
26843
27068
  return errorResponse(`Type '${typeName}' was not found in ${provider.ws.typesDir}.`, {
26844
27069
  available_types: available
@@ -26873,8 +27098,8 @@ function handle22(provider, args) {
26873
27098
  let lifecyclePath = null;
26874
27099
  let visibilityChanges = [];
26875
27100
  if (lifecycleName) {
26876
- const lifecycleFilePath = path11.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
26877
- if (!fs15.existsSync(lifecycleFilePath)) {
27101
+ const lifecycleFilePath = path12.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27102
+ if (!fs16.existsSync(lifecycleFilePath)) {
26878
27103
  warnings.push(
26879
27104
  `Lifecycle '${lifecycleName}' (referenced by type '${typeName}') was not found in ${provider.ws.lifecyclesDir}. Visibility was not updated.`
26880
27105
  );
@@ -26928,12 +27153,12 @@ function handle22(provider, args) {
26928
27153
  // src/tools/attach-attribute-to-layout.ts
26929
27154
  var attach_attribute_to_layout_exports = {};
26930
27155
  __export(attach_attribute_to_layout_exports, {
26931
- TOOL_DEF: () => TOOL_DEF23,
26932
- handle: () => handle23
27156
+ TOOL_DEF: () => TOOL_DEF24,
27157
+ handle: () => handle24
26933
27158
  });
26934
- var fs16 = __toESM(require("fs"));
26935
- var path12 = __toESM(require("path"));
26936
- var TOOL_DEF23 = {
27159
+ var fs17 = __toESM(require("node:fs"));
27160
+ var path13 = __toESM(require("node:path"));
27161
+ var TOOL_DEF24 = {
26937
27162
  name: "attach_protrak_attribute_to_layout",
26938
27163
  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
27164
  inputSchema: {
@@ -26964,16 +27189,10 @@ var TOOL_DEF23 = {
26964
27189
  required: ["layout_name", "attribute_name"]
26965
27190
  }
26966
27191
  };
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
27192
  function findWidget(widgets, widgetId) {
26974
27193
  if (widgets.length === 0) return null;
26975
27194
  if (widgetId === null) return widgets[0];
26976
- return widgets.find((w) => String(w["templateWidgetKey"] ?? "") === widgetId) ?? null;
27195
+ return widgets.find((w) => (w["templateWidgetKey"] ?? "") === widgetId) ?? null;
26977
27196
  }
26978
27197
  function makeFormField(attrName) {
26979
27198
  return {
@@ -27002,11 +27221,11 @@ function makeWidgetColumn(attrName, attrType) {
27002
27221
  };
27003
27222
  }
27004
27223
  function attachToForm(ws, formName, attrName, containerId, replace) {
27005
- const filePath = path12.join(ws.formsDir, `${formName}.json`);
27006
- if (!fs16.existsSync(filePath)) {
27224
+ const filePath = path13.join(ws.formsDir, `${formName}.json`);
27225
+ if (!fs17.existsSync(filePath)) {
27007
27226
  return { error: `Form '${formName}' was not found in ${ws.formsDir}.` };
27008
27227
  }
27009
- const formData = readJson2(filePath);
27228
+ const formData = readJsonFile(filePath);
27010
27229
  const config2 = formData["formConfiguration"] ?? {};
27011
27230
  const containers = Array.isArray(config2["containers"]) ? config2["containers"] : [];
27012
27231
  if (containers.length === 0) {
@@ -27016,16 +27235,16 @@ function attachToForm(ws, formName, attrName, containerId, replace) {
27016
27235
  if (containerId === void 0) {
27017
27236
  target = containers[0];
27018
27237
  } else {
27019
- target = containers.find((c) => String(c["formContainerKey"] ?? "") === containerId);
27238
+ target = containers.find((c) => (c["formContainerKey"] ?? "") === containerId);
27020
27239
  if (!target) {
27021
27240
  return {
27022
27241
  error: `Container '${containerId}' not found in form '${formName}'.`,
27023
- available_containers: containers.map((c) => String(c["formContainerKey"] ?? ""))
27242
+ available_containers: containers.map((c) => c["formContainerKey"] ?? "")
27024
27243
  };
27025
27244
  }
27026
27245
  }
27027
27246
  const fields = Array.isArray(target["fields"]) ? target["fields"] : [];
27028
- const existingIdx = fields.findIndex((f) => String(f["attributeName"] ?? "") === attrName);
27247
+ const existingIdx = fields.findIndex((f) => (f["attributeName"] ?? "") === attrName);
27029
27248
  let action;
27030
27249
  if (existingIdx >= 0) {
27031
27250
  if (!replace) {
@@ -27040,28 +27259,28 @@ function attachToForm(ws, formName, attrName, containerId, replace) {
27040
27259
  action = "inserted";
27041
27260
  }
27042
27261
  target["fields"] = fields;
27043
- writeJson2(filePath, formData);
27262
+ writeJsonFilePath(filePath, formData);
27044
27263
  return {
27045
27264
  result: {
27046
27265
  filePath,
27047
27266
  formName,
27048
- containerKey: String(target["formContainerKey"] ?? ""),
27267
+ containerKey: target["formContainerKey"] ?? "",
27049
27268
  action
27050
27269
  }
27051
27270
  };
27052
27271
  }
27053
27272
  function attachToTypeWidget(provider, widgetName, attrName, replace) {
27054
- const filePath = path12.join(provider.ws.typeWidgetsDir, `${widgetName}.json`);
27055
- if (!fs16.existsSync(filePath)) {
27273
+ const filePath = path13.join(provider.ws.typeWidgetsDir, `${widgetName}.json`);
27274
+ if (!fs17.existsSync(filePath)) {
27056
27275
  return { error: `TypeWidget '${widgetName}' was not found in ${provider.ws.typeWidgetsDir}.` };
27057
27276
  }
27058
- const widgetData = readJson2(filePath);
27277
+ const widgetData = readJsonFile(filePath);
27059
27278
  const isRelation = widgetData["widgetType"] === "Relation";
27060
27279
  const configKey = isRelation ? "relationWidgetConfig" : "dashboardWidgetConfig";
27061
27280
  const config2 = widgetData[configKey] ?? {};
27062
27281
  const fields = Array.isArray(config2["fields"]) ? config2["fields"] : [];
27063
27282
  const attrType = String(provider.attributes[attrName]?.["attributeType"] ?? "Text");
27064
- const existingIdx = fields.findIndex((f) => String(f["attributeName"] ?? "") === attrName);
27283
+ const existingIdx = fields.findIndex((f) => (f["attributeName"] ?? "") === attrName);
27065
27284
  let action;
27066
27285
  if (existingIdx >= 0) {
27067
27286
  if (!replace) {
@@ -27082,10 +27301,57 @@ function attachToTypeWidget(provider, widgetName, attrName, replace) {
27082
27301
  }
27083
27302
  config2["fields"] = fields;
27084
27303
  widgetData[configKey] = config2;
27085
- writeJson2(filePath, widgetData);
27304
+ writeJsonFilePath(filePath, widgetData);
27086
27305
  return { result: { filePath, widgetName, action } };
27087
27306
  }
27088
- function handle23(provider, args) {
27307
+ function resolveTypeWidgetTarget(provider, widget, attrName, containerId, replace, warnings) {
27308
+ const typeWidgetName = widget["typeWidgetName"] ?? null;
27309
+ const widgetKey = widget["templateWidgetKey"] ?? "";
27310
+ if (!typeWidgetName) {
27311
+ return { error: `Layout widget '${widgetKey}' has widgetType=TypeWidget but no typeWidgetName.` };
27312
+ }
27313
+ if (containerId !== void 0) {
27314
+ warnings.push("container_id is only meaningful for Form widgets; ignored for TypeWidget.");
27315
+ }
27316
+ const out = attachToTypeWidget(provider, typeWidgetName, attrName, replace);
27317
+ if (out.error) {
27318
+ return { error: out.error };
27319
+ }
27320
+ return {
27321
+ target: {
27322
+ kind: "typeWidget",
27323
+ filePath: out.result.filePath,
27324
+ payload: { type_widget_name: out.result.widgetName, action: out.result.action }
27325
+ }
27326
+ };
27327
+ }
27328
+ function resolveTarget(provider, widget, attrName, containerId, replace, warnings) {
27329
+ const widgetKind = widget["widgetType"] ?? "";
27330
+ const formWidgetName = widget["formWidgetName"] ?? null;
27331
+ const typeWidgetName = widget["typeWidgetName"] ?? null;
27332
+ const widgetKey = widget["templateWidgetKey"] ?? "";
27333
+ if (widgetKind === "Form" || formWidgetName && !typeWidgetName) {
27334
+ if (!formWidgetName) {
27335
+ return { error: `Layout widget '${widgetKey}' has widgetType=Form but no formWidgetName.` };
27336
+ }
27337
+ const out = attachToForm(provider.ws, formWidgetName, attrName, containerId, replace);
27338
+ if (out.error) {
27339
+ return { error: out.error, extra: out.available_containers ? { available_containers: out.available_containers } : void 0 };
27340
+ }
27341
+ return {
27342
+ target: {
27343
+ kind: "form",
27344
+ filePath: out.result.filePath,
27345
+ payload: { form_name: out.result.formName, container_id: out.result.containerKey, action: out.result.action }
27346
+ }
27347
+ };
27348
+ }
27349
+ if (widgetKind === "TypeWidget" || typeWidgetName && !formWidgetName) {
27350
+ return resolveTypeWidgetTarget(provider, widget, attrName, containerId, replace, warnings);
27351
+ }
27352
+ return { error: `Layout widget '${widgetKey}' has unsupported widgetType='${widgetKind}'.` };
27353
+ }
27354
+ function handle24(provider, args) {
27089
27355
  const layoutName = args["layout_name"] ?? "";
27090
27356
  const widgetIdArg = args["widget_id"];
27091
27357
  const attrName = args["attribute_name"] ?? "";
@@ -27094,19 +27360,15 @@ function handle23(provider, args) {
27094
27360
  if (!layoutName) return errorResponse("layout_name is required");
27095
27361
  if (!attrName) return errorResponse("attribute_name is required");
27096
27362
  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 {
27363
+ if (widgetIdArg !== null && widgetIdArg !== void 0 && typeof widgetIdArg !== "string") {
27103
27364
  return errorResponse("widget_id must be a string templateWidgetKey or null.");
27104
27365
  }
27105
- const layoutPath = path12.join(provider.ws.layoutTemplatesDir, `${layoutName}.json`);
27106
- if (!fs16.existsSync(layoutPath)) {
27366
+ const widgetId = widgetIdArg ?? null;
27367
+ const layoutPath = path13.join(provider.ws.layoutTemplatesDir, `${layoutName}.json`);
27368
+ if (!fs17.existsSync(layoutPath)) {
27107
27369
  return errorResponse(`Layout template '${layoutName}' was not found in ${provider.ws.layoutTemplatesDir}.`);
27108
27370
  }
27109
- const layoutData = readJson2(layoutPath);
27371
+ const layoutData = readJsonFile(layoutPath);
27110
27372
  const config2 = layoutData["configuration"] ?? {};
27111
27373
  const widgets = Array.isArray(config2["widgets"]) ? config2["widgets"] : [];
27112
27374
  if (widgets.length === 0) {
@@ -27114,12 +27376,9 @@ function handle23(provider, args) {
27114
27376
  }
27115
27377
  const widget = findWidget(widgets, widgetId);
27116
27378
  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
- );
27379
+ return errorResponse(`widget_id '${widgetId}' not found in layout '${layoutName}'.`, {
27380
+ available_widget_ids: widgets.map((w) => w["templateWidgetKey"] ?? "")
27381
+ });
27123
27382
  }
27124
27383
  const warnings = [];
27125
27384
  if (!provider.attributes[attrName]) {
@@ -27127,53 +27386,11 @@ function handle23(provider, args) {
27127
27386
  `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
27387
  );
27129
27388
  }
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
- );
27389
+ const resolved = resolveTarget(provider, widget, attrName, containerId, replace, warnings);
27390
+ if ("error" in resolved) {
27391
+ return errorResponse(resolved.error, resolved.extra);
27176
27392
  }
27393
+ const { target } = resolved;
27177
27394
  const nextSteps = [
27178
27395
  `Verify with read_protrak_layout(layout_name='${layoutName}').`,
27179
27396
  `If the attribute is not yet bound to the type, call attach_protrak_attribute_to_type to bind it.`
@@ -27182,7 +27399,7 @@ function handle23(provider, args) {
27182
27399
  {
27183
27400
  layout_name: layoutName,
27184
27401
  layout_path: layoutPath,
27185
- widget_id: String(widget["templateWidgetKey"] ?? ""),
27402
+ widget_id: widget["templateWidgetKey"] ?? "",
27186
27403
  target_kind: target.kind,
27187
27404
  target_path: target.filePath,
27188
27405
  ...target.payload
@@ -27194,12 +27411,12 @@ function handle23(provider, args) {
27194
27411
  // src/tools/set-attribute-state-visibility.ts
27195
27412
  var set_attribute_state_visibility_exports = {};
27196
27413
  __export(set_attribute_state_visibility_exports, {
27197
- TOOL_DEF: () => TOOL_DEF24,
27198
- handle: () => handle24
27414
+ TOOL_DEF: () => TOOL_DEF25,
27415
+ handle: () => handle25
27199
27416
  });
27200
- var fs17 = __toESM(require("fs"));
27201
- var path13 = __toESM(require("path"));
27202
- var TOOL_DEF24 = {
27417
+ var fs18 = __toESM(require("fs"));
27418
+ var path14 = __toESM(require("path"));
27419
+ var TOOL_DEF25 = {
27203
27420
  name: "set_protrak_attribute_state_visibility",
27204
27421
  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
27422
  inputSchema: {
@@ -27222,13 +27439,13 @@ var TOOL_DEF24 = {
27222
27439
  required: ["type_name", "attribute_name"]
27223
27440
  }
27224
27441
  };
27225
- function readJson3(filePath) {
27226
- return JSON.parse(fs17.readFileSync(filePath, "utf8"));
27442
+ function readJson2(filePath) {
27443
+ return JSON.parse(fs18.readFileSync(filePath, "utf8"));
27227
27444
  }
27228
- function writeJson3(filePath, data) {
27229
- fs17.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
27445
+ function writeJson2(filePath, data) {
27446
+ fs18.writeFileSync(filePath, JSON.stringify(data, null, JSON_INDENT), "utf8");
27230
27447
  }
27231
- function handle24(provider, args) {
27448
+ function handle25(provider, args) {
27232
27449
  const typeName = args["type_name"] ?? "";
27233
27450
  const attrName = args["attribute_name"] ?? "";
27234
27451
  const hasVisibleStatesArg = Object.prototype.hasOwnProperty.call(args, "visible_states");
@@ -27248,14 +27465,14 @@ function handle24(provider, args) {
27248
27465
  }
27249
27466
  visibleStates = new Set(rawVisible);
27250
27467
  }
27251
- const typeFilePath = path13.join(provider.ws.typesDir, `${typeName}.json`);
27252
- if (!fs17.existsSync(typeFilePath)) {
27468
+ const typeFilePath = path14.join(provider.ws.typesDir, `${typeName}.json`);
27469
+ if (!fs18.existsSync(typeFilePath)) {
27253
27470
  const available = Object.keys(provider.types).sort();
27254
27471
  return errorResponse(`Type '${typeName}' was not found in ${provider.ws.typesDir}.`, {
27255
27472
  available_types: available
27256
27473
  });
27257
27474
  }
27258
- const typeData = readJson3(typeFilePath);
27475
+ const typeData = readJson2(typeFilePath);
27259
27476
  const typeAttributes = Array.isArray(typeData["attributes"]) ? typeData["attributes"] : [];
27260
27477
  const knownTypeAttributes = typeAttributes.map((a) => a.name);
27261
27478
  const warnings = [];
@@ -27268,13 +27485,13 @@ function handle24(provider, args) {
27268
27485
  if (!lifecycleName) {
27269
27486
  return errorResponse(`Type '${typeName}' has no lifecycleName set; cannot adjust visibility.`);
27270
27487
  }
27271
- const lifecycleFilePath = path13.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27272
- if (!fs17.existsSync(lifecycleFilePath)) {
27488
+ const lifecycleFilePath = path14.join(provider.ws.lifecyclesDir, `${lifecycleName}.json`);
27489
+ if (!fs18.existsSync(lifecycleFilePath)) {
27273
27490
  return errorResponse(
27274
27491
  `Lifecycle '${lifecycleName}' (referenced by type '${typeName}') was not found in ${provider.ws.lifecyclesDir}.`
27275
27492
  );
27276
27493
  }
27277
- const lifecycleData = readJson3(lifecycleFilePath);
27494
+ const lifecycleData = readJson2(lifecycleFilePath);
27278
27495
  const stateNames = (lifecycleData["states"] ?? []).map(
27279
27496
  (s) => String(s["name"] ?? "")
27280
27497
  );
@@ -27291,7 +27508,7 @@ function handle24(provider, args) {
27291
27508
  knownTypeAttributes
27292
27509
  });
27293
27510
  if (changed) {
27294
- writeJson3(lifecycleFilePath, lifecycleData);
27511
+ writeJson2(lifecycleFilePath, lifecycleData);
27295
27512
  }
27296
27513
  const nextSteps = [
27297
27514
  `Verify with read_protrak_schema_element(kind='lifecycle', name='${lifecycleName}').`,
@@ -27311,6 +27528,194 @@ function handle24(provider, args) {
27311
27528
  );
27312
27529
  }
27313
27530
 
27531
+ // src/tools/get-notification-context.ts
27532
+ var get_notification_context_exports = {};
27533
+ __export(get_notification_context_exports, {
27534
+ TOOL_DEF: () => TOOL_DEF26,
27535
+ VALID_TARGETS: () => VALID_TARGETS,
27536
+ handle: () => handle26
27537
+ });
27538
+ var fs19 = __toESM(require("fs"));
27539
+ var path15 = __toESM(require("path"));
27540
+ var DATA_DIR2 = path15.join(__dirname, "..", "..", "data");
27541
+ var NOTIFICATION_TEMPLATE_REFERENCE_PATH = path15.join(
27542
+ DATA_DIR2,
27543
+ "patterns",
27544
+ "notification-template-reference.md"
27545
+ );
27546
+ var VALID_TARGETS = [
27547
+ "PromoteNotification",
27548
+ "AccountNotification",
27549
+ "WorkflowNotification",
27550
+ "WorkflowTaskNotification"
27551
+ ];
27552
+ var TARGET_DESCRIPTIONS = {
27553
+ PromoteNotification: "Triggered by a lifecycle promote action. Link the template in the lifecycle Send Notification command.",
27554
+ AccountNotification: "Triggered by user creation or password reset. Link in Tenant Settings \u2192 Notifications.",
27555
+ WorkflowNotification: "Triggered by a workflow Notification Activity. Link in Workflow \u2192 Notification Activity.",
27556
+ WorkflowTaskNotification: "Triggered by workflow Input/Checklist Task Activity creation. Link in Workflow \u2192 Input/Checklist Task Activity."
27557
+ };
27558
+ var TOOL_DEF26 = {
27559
+ name: "get_protrak_notification_context",
27560
+ description: "RECOMMENDED first call when creating a Protrak notification template. Returns existing templates from the workspace (optionally filtered by type name) and the full @Model variable reference for all target types. Templates use Razor syntax (@Model.*), NOT {{}} placeholders. Email notifications require two separate templates: one for Subject, one for Body. Missing notificationTemplates/ directory returns an empty list without error.",
27561
+ inputSchema: {
27562
+ type: "object",
27563
+ properties: {
27564
+ type_name: {
27565
+ type: "string",
27566
+ description: "Optional. Filter existing templates whose title contains this name (e.g. 'Invoice'). Omit to return all templates."
27567
+ },
27568
+ target: {
27569
+ type: "string",
27570
+ description: "Optional. Filter by target type: 'PromoteNotification', 'AccountNotification', 'WorkflowNotification', or 'WorkflowTaskNotification'. Omit to return all."
27571
+ }
27572
+ },
27573
+ required: []
27574
+ }
27575
+ };
27576
+ function handle26(provider, args) {
27577
+ const typeName = args["type_name"] || void 0;
27578
+ const targetFilter = args["target"] || void 0;
27579
+ const lp = getOrCreateProvider(provider.ws);
27580
+ let templates = lp.notificationTemplatesForType(typeName);
27581
+ if (targetFilter) {
27582
+ templates = templates.filter((t) => t.target === targetFilter);
27583
+ }
27584
+ let variableReference = "";
27585
+ try {
27586
+ variableReference = fs19.readFileSync(NOTIFICATION_TEMPLATE_REFERENCE_PATH, "utf8");
27587
+ } catch {
27588
+ variableReference = "Variable reference file not found.";
27589
+ }
27590
+ const targetTypes = VALID_TARGETS.map((t) => ({
27591
+ value: t,
27592
+ description: TARGET_DESCRIPTIONS[t]
27593
+ }));
27594
+ const nextSteps = [];
27595
+ if (templates.length > 0) {
27596
+ nextSteps.push(
27597
+ "Review existing_templates above for naming conventions and patterns used in this workspace."
27598
+ );
27599
+ }
27600
+ nextSteps.push(
27601
+ "Call generate_protrak_notification_template(name='<Title> Body', target='PromoteNotification', email_html='<Razor HTML>') to write both .json and .html files.",
27602
+ "For email notifications, also call generate_protrak_notification_template for the subject: name='<Title> Subject', email_html='<one-line subject>'."
27603
+ );
27604
+ const result = {
27605
+ existing_templates: templates,
27606
+ target_types: targetTypes,
27607
+ variable_reference: variableReference,
27608
+ next_steps: nextSteps
27609
+ };
27610
+ return JSON.stringify(result, null, 2);
27611
+ }
27612
+
27613
+ // src/tools/generate-notification-template.ts
27614
+ var generate_notification_template_exports = {};
27615
+ __export(generate_notification_template_exports, {
27616
+ TOOL_DEF: () => TOOL_DEF27,
27617
+ handle: () => handle27
27618
+ });
27619
+ var fs20 = __toESM(require("fs"));
27620
+ var path16 = __toESM(require("path"));
27621
+ function buildTemplateJson(name, target, messageText) {
27622
+ return {
27623
+ title: name,
27624
+ target,
27625
+ emailText: null,
27626
+ hashCode: "",
27627
+ messageText: messageText ?? null,
27628
+ templateState: "Published"
27629
+ };
27630
+ }
27631
+ var TOOL_DEF27 = {
27632
+ name: "generate_protrak_notification_template",
27633
+ description: "Write a Protrak notification template to disk. Creates NotificationTemplates/<name>.json (metadata) and, when email_html is provided, NotificationTemplates/<name>.html (Razor HTML email body). hashCode is left empty \u2014 Protrak recomputes it on import. Templates use @Model.* Razor syntax (NOT {{}} placeholders). For a full email notification, call this tool twice: once for the Subject template (name='<Title> Subject') and once for the Body template (name='<Title> Body'). Call get_protrak_notification_context first to see existing templates and variable reference.",
27634
+ inputSchema: {
27635
+ type: "object",
27636
+ properties: {
27637
+ name: {
27638
+ type: "string",
27639
+ description: "Template title and filename (e.g. 'Invoice Sent Body', 'Invoice Sent Subject'). Spaces are allowed \u2014 matches MIS.Customization naming convention. Subject templates end with 'Subject'; body templates end with 'Body'."
27640
+ },
27641
+ target: {
27642
+ type: "string",
27643
+ description: "Notification event type. One of: 'PromoteNotification' (lifecycle promote), 'AccountNotification' (user creation/password reset), 'WorkflowNotification' (workflow notification activity), 'WorkflowTaskNotification' (workflow input/checklist task)."
27644
+ },
27645
+ email_html: {
27646
+ type: "string",
27647
+ description: "Razor HTML content for the email template. Written to <name>.html. Use @Model.* variables (see get_protrak_notification_context for the full reference). Omit if this is a push-only template."
27648
+ },
27649
+ message_text: {
27650
+ type: "string",
27651
+ description: "Short plain-text push notification content. Stored in the JSON as messageText. Razor syntax is supported. Omit if this is an email-only template."
27652
+ },
27653
+ overwrite: {
27654
+ type: "boolean",
27655
+ description: "When true, overwrite existing files. Default: false (errors on conflict).",
27656
+ default: false
27657
+ }
27658
+ },
27659
+ required: ["name", "target"]
27660
+ }
27661
+ };
27662
+ function handle27(provider, args) {
27663
+ const name = args["name"] ?? "";
27664
+ const target = args["target"] ?? "";
27665
+ const emailHtml = args["email_html"] || void 0;
27666
+ const messageText = args["message_text"] || void 0;
27667
+ const overwrite = args["overwrite"] === true;
27668
+ if (!name) return errorResponse("name is required");
27669
+ if (!target) return errorResponse("target is required");
27670
+ if (!VALID_TARGETS.includes(target)) {
27671
+ return errorResponse(
27672
+ `Unknown target '${target}'. Valid values: ${VALID_TARGETS.join(", ")}.`,
27673
+ { valid_targets: [...VALID_TARGETS] }
27674
+ );
27675
+ }
27676
+ if (!emailHtml && !messageText) {
27677
+ return errorResponse(
27678
+ "At least one of email_html or message_text must be provided."
27679
+ );
27680
+ }
27681
+ const dir = provider.ws.notificationTemplatesDir;
27682
+ if (!fs20.existsSync(dir)) {
27683
+ fs20.mkdirSync(dir, { recursive: true });
27684
+ }
27685
+ const jsonPath = path16.join(dir, `${name}.json`);
27686
+ const htmlPath = emailHtml ? path16.join(dir, `${name}.html`) : null;
27687
+ if (!overwrite) {
27688
+ const existing = [];
27689
+ if (fs20.existsSync(jsonPath)) existing.push(jsonPath);
27690
+ if (htmlPath && fs20.existsSync(htmlPath)) existing.push(htmlPath);
27691
+ if (existing.length > 0) {
27692
+ return errorResponse(
27693
+ `Refusing to overwrite existing file(s): ${existing.join(", ")}. Pass overwrite:true to replace.`,
27694
+ { existing_paths: existing }
27695
+ );
27696
+ }
27697
+ }
27698
+ const templateJson = buildTemplateJson(name, target, messageText);
27699
+ fs20.writeFileSync(jsonPath, JSON.stringify(templateJson, null, JSON_INDENT), "utf8");
27700
+ if (emailHtml && htmlPath) {
27701
+ fs20.writeFileSync(htmlPath, emailHtml, "utf8");
27702
+ }
27703
+ const nextSteps = [
27704
+ `Verify by calling get_protrak_notification_context(type_name='${name.split(" ")[0]}').`,
27705
+ "On import, Protrak recomputes hashCode automatically \u2014 leaving it empty here is intentional."
27706
+ ];
27707
+ return successResponse(
27708
+ {
27709
+ name,
27710
+ target,
27711
+ json_path: jsonPath,
27712
+ ...htmlPath ? { html_path: htmlPath } : {},
27713
+ written_json: templateJson
27714
+ },
27715
+ { nextSteps }
27716
+ );
27717
+ }
27718
+
27314
27719
  // src/index.ts
27315
27720
  function semverGte(a, b) {
27316
27721
  const parse3 = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
@@ -27324,6 +27729,7 @@ var TOOLS = [
27324
27729
  // Workflow entry points (call these first)
27325
27730
  get_program_context_exports,
27326
27731
  get_layout_context_exports,
27732
+ get_notification_context_exports,
27327
27733
  scaffold_entity_exports,
27328
27734
  validate_program_exports,
27329
27735
  // Knowledge search (patterns / api / programs)
@@ -27346,6 +27752,8 @@ var TOOLS = [
27346
27752
  generate_form_exports,
27347
27753
  generate_type_widget_exports,
27348
27754
  generate_layout_template_exports,
27755
+ generate_program_exports,
27756
+ generate_notification_template_exports,
27349
27757
  // Surgical mutators — modify existing files without regeneration/clobber
27350
27758
  attach_attribute_to_type_exports,
27351
27759
  attach_attribute_to_layout_exports,
@@ -27385,11 +27793,18 @@ Restart VS Code (or reload the window) so npx can fetch the latest version.
27385
27793
  log("info", `Protrak Forge MCP ready \u2014 ${TOOLS.length} tools | workspace: ${workspace.namespace} | types: ${typeCount} | programs: ${programCount}`);
27386
27794
  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
27795
 
27388
- WORKFLOW for writing Protrak programs:
27796
+ WORKFLOW for writing a NEW Protrak program:
27389
27797
  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
27798
  2. Write the C# program following the interface contract and patterns.
27391
27799
  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
27800
  4. ALWAYS call validate_protrak_program after writing to check for errors.
27801
+ 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.
27802
+
27803
+ WORKFLOW for REFACTORING an existing Protrak program:
27804
+ 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).
27805
+ 2. Edit the C# source.
27806
+ 3. Call validate_protrak_program(code, program_type) \u2014 same as for new programs.
27807
+ 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
27808
 
27394
27809
  WORKFLOW for creating a QueryDefinition (no program needed):
27395
27810
  1. Call read_protrak_schema_element with kind='type' to get attribute names and valid state names.
@@ -27405,6 +27820,13 @@ WORKFLOW for scaffolding schema elements (Types, Lifecycles, Attributes, Relatio
27405
27820
  6. All schema names must be PascalCase. Aliases are allowed for attribute_type: Number\u2192Numeric, File\u2192Attachment.
27406
27821
  7. All generators refuse to overwrite an existing file unless overwrite:true.
27407
27822
 
27823
+ WORKFLOW for writing a notification template:
27824
+ 1. Call get_protrak_notification_context first \u2014 returns existing templates and the full @Model variable reference.
27825
+ 2. Write the Razor HTML email body using @Model.* syntax (NOT {{}} placeholders).
27826
+ 3. Call generate_protrak_notification_template for the Body template (name="<Title> Body", target, email_html).
27827
+ 4. Call generate_protrak_notification_template again for the Subject template (name="<Title> Subject", email_html="<one-liner subject>").
27828
+ 5. For push-only notifications, omit email_html and provide message_text instead.
27829
+
27408
27830
  WORKFLOW for designing a Design Studio layout:
27409
27831
  1. Call get_protrak_layout_context first \u2014 returns type schema and all existing layouts for the type+mode.
27410
27832
  2. Use list_protrak_layouts to browse all artifacts; use read_protrak_layout to read a specific one's full JSON.