@prorigo/protrak-forge 0.3.4 → 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.
package/README.md CHANGED
@@ -10,6 +10,7 @@ Protrak Forge is an MCP server for Protrak customization workspaces. It gives AI
10
10
  - Create or inspect query definitions used by your customization
11
11
  - Design new layouts with awareness of existing forms, widgets, and templates
12
12
  - Scaffold new entity types — Types, Lifecycles, Attributes, and RelationTypes — in one shot
13
+ - Create notification templates using the correct Razor syntax and `@Model` variables
13
14
 
14
15
  ## Requirements
15
16
 
@@ -267,6 +268,7 @@ Once the server is connected, you can ask your assistant things like:
267
268
  - "Create a query definition for active projects assigned to the current user."
268
269
  - "Design a dashboard layout for Diary using the existing workspace patterns."
269
270
  - "Scaffold a Diary entity with an Active/Inactive lifecycle and a FarmerGatToDiary relation."
271
+ - "Create a notification template for the Invoice type that emails the manager when an invoice is promoted to Sent."
270
272
 
271
273
  ## Workspace Detection
272
274
 
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
3223
3223
  }
3224
3224
  return ind;
3225
3225
  }
3226
- function removeDotSegments(path15) {
3227
- let input = path15;
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 [path15, query] = wsComponent.resourceName.split("?");
3427
- wsComponent.path = path15 && path15 !== "/" ? path15 : 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, fs19, 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, fs19[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: path15, errorMaps, issueData } = params;
7162
- const fullPath = [...path15, ...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, path15, key) {
7277
+ constructor(parent, value, path17, key) {
7278
7278
  this._cachedPath = [];
7279
7279
  this.parent = parent;
7280
7280
  this.data = value;
7281
- this._path = path15;
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, path15) {
10928
- if (!path15)
10927
+ function getElementAtPath(obj, path17) {
10928
+ if (!path17)
10929
10929
  return obj;
10930
- return path15.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(path15, 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(path15);
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, path15] = 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(path15);
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, path15 = []) => {
21656
+ var trackDown = (tree, key, path17 = []) => {
21656
21657
  if (key.length === 0 || tree == null) {
21657
- return [tree, path15];
21658
+ return [tree, path17];
21658
21659
  }
21659
21660
  for (const k of tree.keys()) {
21660
21661
  if (k !== LEAF && key.startsWith(k)) {
21661
- path15.push([tree, k]);
21662
- return trackDown(tree.get(k), key.slice(k.length), path15);
21662
+ path17.push([tree, k]);
21663
+ return trackDown(tree.get(k), key.slice(k.length), path17);
21663
21664
  }
21664
21665
  }
21665
- path15.push([tree, key]);
21666
- return trackDown(void 0, "", path15);
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, path15] = 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(path15);
21715
+ cleanup(path17);
21715
21716
  } else if (node.size === 1) {
21716
21717
  const [key2, value] = node.entries().next().value;
21717
- merge2(path15, key2, value);
21718
+ merge2(path17, key2, value);
21718
21719
  }
21719
21720
  };
21720
- var cleanup = (path15) => {
21721
- if (path15.length === 0) {
21721
+ var cleanup = (path17) => {
21722
+ if (path17.length === 0) {
21722
21723
  return;
21723
21724
  }
21724
- const [node, key] = last(path15);
21725
+ const [node, key] = last(path17);
21725
21726
  node.delete(key);
21726
21727
  if (node.size === 0) {
21727
- cleanup(path15.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(path15.slice(0, -1), key2, value);
21732
+ merge2(path17.slice(0, -1), key2, value);
21732
21733
  }
21733
21734
  }
21734
21735
  };
21735
- var merge2 = (path15, key, value) => {
21736
- if (path15.length === 0) {
21736
+ var merge2 = (path17, key, value) => {
21737
+ if (path17.length === 0) {
21737
21738
  return;
21738
21739
  }
21739
- const [node, nodeKey] = last(path15);
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.4";
23322
+ var PACKAGE_VERSION = "0.3.5";
23322
23323
 
23323
23324
  // src/tools/_schema-write-utils.ts
23324
23325
  var fs4 = __toESM(require("fs"));
@@ -24089,6 +24090,8 @@ var LocalLayoutProvider = class {
24089
24090
  _layoutTemplateMeta = null;
24090
24091
  _homeLayouts = null;
24091
24092
  _homeLayoutMeta = null;
24093
+ _notificationTemplates = null;
24094
+ _notificationTemplateMeta = null;
24092
24095
  constructor(ws) {
24093
24096
  this.ws = ws;
24094
24097
  }
@@ -24159,6 +24162,34 @@ var LocalLayoutProvider = class {
24159
24162
  "home layout"
24160
24163
  );
24161
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
+ }
24162
24193
  formsForType(typeName, mode) {
24163
24194
  this.ensureForms();
24164
24195
  return this._formMeta.filter(
@@ -24181,6 +24212,12 @@ var LocalLayoutProvider = class {
24181
24212
  this.ensureHomeLayouts();
24182
24213
  return this._homeLayoutMeta;
24183
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
+ }
24184
24221
  getLayoutJson(layoutType, name) {
24185
24222
  switch (layoutType) {
24186
24223
  case "form":
@@ -24231,6 +24268,10 @@ var LocalLayoutProvider = class {
24231
24268
  this._homeLayouts = null;
24232
24269
  this._homeLayoutMeta = null;
24233
24270
  break;
24271
+ case "notificationTemplate":
24272
+ this._notificationTemplates = null;
24273
+ this._notificationTemplateMeta = null;
24274
+ break;
24234
24275
  }
24235
24276
  }
24236
24277
  };
@@ -27487,6 +27528,194 @@ function handle25(provider, args) {
27487
27528
  );
27488
27529
  }
27489
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
+
27490
27719
  // src/index.ts
27491
27720
  function semverGte(a, b) {
27492
27721
  const parse3 = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
@@ -27500,6 +27729,7 @@ var TOOLS = [
27500
27729
  // Workflow entry points (call these first)
27501
27730
  get_program_context_exports,
27502
27731
  get_layout_context_exports,
27732
+ get_notification_context_exports,
27503
27733
  scaffold_entity_exports,
27504
27734
  validate_program_exports,
27505
27735
  // Knowledge search (patterns / api / programs)
@@ -27523,6 +27753,7 @@ var TOOLS = [
27523
27753
  generate_type_widget_exports,
27524
27754
  generate_layout_template_exports,
27525
27755
  generate_program_exports,
27756
+ generate_notification_template_exports,
27526
27757
  // Surgical mutators — modify existing files without regeneration/clobber
27527
27758
  attach_attribute_to_type_exports,
27528
27759
  attach_attribute_to_layout_exports,
@@ -27589,6 +27820,13 @@ WORKFLOW for scaffolding schema elements (Types, Lifecycles, Attributes, Relatio
27589
27820
  6. All schema names must be PascalCase. Aliases are allowed for attribute_type: Number\u2192Numeric, File\u2192Attachment.
27590
27821
  7. All generators refuse to overwrite an existing file unless overwrite:true.
27591
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
+
27592
27830
  WORKFLOW for designing a Design Studio layout:
27593
27831
  1. Call get_protrak_layout_context first \u2014 returns type schema and all existing layouts for the type+mode.
27594
27832
  2. Use list_protrak_layouts to browse all artifacts; use read_protrak_layout to read a specific one's full JSON.
@@ -124,6 +124,31 @@ These are **Protrak-specific gotchas you cannot catch by reading the code**. Alw
124
124
  - Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in
125
125
  Pre-triggers (style rule).
126
126
 
127
+ ## How to write notification templates
128
+
129
+ Notification templates define the email/push content sent by Protrak. They use **Razor syntax**
130
+ (`@Model.*`), not `{{}}` placeholders.
131
+
132
+ ### Writing a new notification template:
133
+ 1. Call `get_protrak_notification_context(type_name?)` — returns existing templates and the full
134
+ `@Model` variable reference for all target types.
135
+ 2. Write the Razor HTML body using `@Model.*` variables.
136
+ 3. Call `generate_protrak_notification_template(name='<Title> Body', target='PromoteNotification', email_html='<Razor HTML>')`.
137
+ 4. Call again for the subject: `generate_protrak_notification_template(name='<Title> Subject', target='PromoteNotification', email_html='<subject line>')`.
138
+
139
+ ```
140
+ get_protrak_notification_context(type_name='Invoice')
141
+ # ... agent writes the Razor HTML ...
142
+ generate_protrak_notification_template(name='Invoice Sent Body', target='PromoteNotification', email_html='<p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,...</p>')
143
+ generate_protrak_notification_template(name='Invoice Sent Subject', target='PromoteNotification', email_html='Invoice @Model.InstanceName has been sent')
144
+ ```
145
+
146
+ Each call creates `NotificationTemplates/<name>.json` (metadata) and `NotificationTemplates/<name>.html`
147
+ (email body). `hashCode` is left empty — Protrak recomputes it on import.
148
+
149
+ Valid `target` values: `PromoteNotification`, `AccountNotification`, `WorkflowNotification`,
150
+ `WorkflowTaskNotification`.
151
+
127
152
  ## Project structure
128
153
  - `Types/` — entity type JSON definitions
129
154
  - `Attributes/` — attribute JSON definitions
@@ -131,3 +156,5 @@ These are **Protrak-specific gotchas you cannot catch by reading the code**. Alw
131
156
  - `Programs/` — C# program source + JSON metadata (paired files; use
132
157
  `generate_protrak_program` to keep them in sync)
133
158
  - `RelationTypes/` — relation definitions between types
159
+ - `NotificationTemplates/` — notification template files (paired `.json` + `.html`; use
160
+ `generate_protrak_notification_template` to keep them in sync)
@@ -0,0 +1,191 @@
1
+ # Notification Template Reference
2
+
3
+ ## Overview
4
+
5
+ Protrak notification templates define the content of notifications (email + push) triggered by
6
+ platform events. Templates use **Razor markup syntax** (`@Model.*`) and are stored as paired files:
7
+
8
+ - `NotificationTemplates/<title>.json` — metadata (title, target, push message, state)
9
+ - `NotificationTemplates/<title>.html` — HTML email body (Razor template)
10
+
11
+ Two templates are needed for a complete email notification: one for the **subject** (title ending
12
+ in `Subject`) and one for the **body** (title ending in `Body`). Push-only templates need only
13
+ the `.json` with a `messageText` value.
14
+
15
+ ## Target Types
16
+
17
+ | Target | Trigger event | Usage |
18
+ |---|---|---|
19
+ | `PromoteNotification` | Lifecycle promote action | Link in a lifecycle's "Send Notification" command |
20
+ | `AccountNotification` | User creation / password reset | Link in Tenant Settings → Notifications |
21
+ | `WorkflowNotification` | Workflow notification activity | Link in Workflow → Notification Activity |
22
+ | `WorkflowTaskNotification` | Workflow input/checklist task creation | Link in Workflow → Input/Checklist Task Activity |
23
+
24
+ ## @Model Variables — PromoteNotification
25
+
26
+ Available when `target` is `PromoteNotification`. These reflect the promoted instance.
27
+
28
+ ```
29
+ @Model.InstanceName — Display name of the instance
30
+ @Model.InstanceUrl — Deep link URL to the instance
31
+ @Model.InstanceId — GUID of the instance
32
+ @Model.InstanceTrackingId — Tracking / reference number
33
+ @Model.PreviousStateName — Lifecycle state before the promote
34
+ @Model.CurrentStateName — Lifecycle state after the promote
35
+ @Model.ModifiedDate — Date/time of the promote (UTC)
36
+ @Model.ModifiedBy — Name of the user who triggered it
37
+ @Model.CreatedBy — Name of the user who created the instance
38
+ @Model.Remarks — Remarks entered on the promote action
39
+ @Model.AttributeValues["<AttributeName>"] — Attribute accessor (see below)
40
+ @Model.TenantInfo.TenantName
41
+ @Model.TenantInfo.LogoUrl
42
+ @Model.TenantInfo.DateTimeFormat.DateFormat
43
+ @Model.TenantInfo.DateTimeFormat.TimeFormat
44
+ @Model.TenantInfo.DateTimeFormat.TimeZone
45
+ @Model.TenantInfo.Locale.Name
46
+ @Model.TenantInfo.Locale.DisplayName
47
+ @Model.ApplicationUrl
48
+ ```
49
+
50
+ ## @Model Variables — AccountNotification
51
+
52
+ Available when `target` is `AccountNotification`. These reflect the Protrak user account.
53
+
54
+ ```
55
+ @Model.UserName
56
+ @Model.UserFullName
57
+ @Model.UserEmail
58
+ @Model.UserPassword
59
+ @Model.CompanyName
60
+ @Model.CompanyEmail
61
+ @Model.CompanyDomain
62
+ @Model.CompanyLogoUrl
63
+ @Model.UserRoles[index]
64
+ @Model.UserAttributes["<AttributeName>"]
65
+ @Model.MFAVerificationCode
66
+ @Model.MFAVerificationCodeValidity
67
+ ```
68
+
69
+ ## @Model Variables — WorkflowNotification
70
+
71
+ Available when `target` is `WorkflowNotification`. These reflect the instance attached to the workflow.
72
+
73
+ ```
74
+ @Model.InstanceName
75
+ @Model.InstanceUrl
76
+ @Model.InstanceId
77
+ @Model.InstanceTrackingId
78
+ @Model.ApplicationUrl
79
+ @Model.StateName
80
+ @Model.ModifiedDate
81
+ @Model.ModifiedBy
82
+ @Model.CreatedBy
83
+ @Model.AttributeValues["<AttributeName>"]
84
+ ```
85
+
86
+ ## @Model Variables — WorkflowTaskNotification
87
+
88
+ Available when `target` is `WorkflowTaskNotification`. These reflect the workflow task.
89
+
90
+ ```
91
+ @Model.InstanceName
92
+ @Model.InstanceUrl
93
+ @Model.InstanceId
94
+ @Model.InstanceTrackingId
95
+ @Model.ApplicationUrl
96
+ @Model.StateName
97
+ @Model.ModifiedDate
98
+ @Model.ModifiedBy
99
+ @Model.CreatedBy
100
+ @Model.AttributeValues["<AttributeName>"]
101
+ @Model.WorkflowTask.Id
102
+ @Model.WorkflowTask.Name
103
+ @Model.WorkflowTask.Description
104
+ @Model.WorkflowTask.Comments
105
+ @Model.WorkflowTask.Type
106
+ @Model.WorkflowTask.WorkflowInstanceId
107
+ @Model.WorkflowTask.PausedWorkflowInstActivityId
108
+ @Model.WorkflowTask.TypeInstId
109
+ @Model.WorkflowTask.Status
110
+ @Model.WorkflowTask.AssignedUserId
111
+ @Model.WorkflowTask.AssignedRoleId
112
+ @Model.WorkflowTask.Created
113
+ @Model.WorkflowTask.Due
114
+ @Model.WorkflowTask.Completed
115
+ @Model.WorkflowTask.CompletedBy
116
+ @Model.WorkflowTask.ChecklistItems[index].Id
117
+ @Model.WorkflowTask.ChecklistItems[index].Name
118
+ @Model.WorkflowTask.ChecklistItems[index].IsMandatory
119
+ @Model.WorkflowTask.ChecklistItems[index].IsCompleted
120
+ ```
121
+
122
+ ## Attribute Value Accessors
123
+
124
+ The `@Model.AttributeValues["<AttributeName>"]` accessor returns an object with type-specific
125
+ properties. Access the correct property based on the attribute type:
126
+
127
+ ```
128
+ .TextValue — Text attributes
129
+ .NumericValue — Numeric attributes
130
+ .DateValue — Date attributes (stored as UTC DateTime)
131
+ .ArrayValue[0] — Picklist (single) — string value
132
+ .UserValues[0].UserName — User attributes (first user)
133
+ .UserValues[0].UserEmail
134
+ .UserValues[0].UserId
135
+ .ReferenceValue[0].Name — Reference attributes (first linked instance)
136
+ .ReferenceValue[0].Id
137
+ ```
138
+
139
+ ## Razor Syntax Examples
140
+
141
+ ### Simple substitution
142
+ ```html
143
+ <p>Hi @Model.AttributeValues["Employee"].UserValues[0].UserName,</p>
144
+ <p>Your <a href='@Model.InstanceUrl'>@Model.InstanceName</a> has been approved.</p>
145
+ ```
146
+
147
+ ### Conditional content
148
+ ```html
149
+ @if (Model.AttributeValues["Country"].ArrayValue[0] == "India")
150
+ {
151
+ <span>Tax rate: @Model.AttributeValues["GSTRate"].NumericValue%</span>
152
+ }
153
+ else
154
+ {
155
+ <span>No tax applicable.</span>
156
+ }
157
+ ```
158
+
159
+ ### Null-safe access
160
+ ```html
161
+ @(Model.AttributeValues.ContainsKey("Title")
162
+ ? Model.AttributeValues["Title"]?.TextValue ?? "N/A"
163
+ : "N/A")
164
+ ```
165
+
166
+ ## File Format
167
+
168
+ ### `<title>.json`
169
+ ```json
170
+ {
171
+ "title": "Invoice Sent Body",
172
+ "target": "PromoteNotification",
173
+ "emailText": null,
174
+ "hashCode": "",
175
+ "messageText": null,
176
+ "templateState": "Published"
177
+ }
178
+ ```
179
+
180
+ - `emailText` is always `null` in file-based templates — the email body lives in the `.html` file.
181
+ - `hashCode` is left empty; Protrak recomputes it on import (same pattern as `programHashCode`).
182
+ - `messageText` is the plain-text push notification content (Razor syntax supported).
183
+ - `templateState`: `"Published"` or `"InProgress"`.
184
+
185
+ ### `<title>.html`
186
+ ```html
187
+ <p>Hi @Model.AttributeValues["Manager"].UserValues[0].UserName,</p>
188
+ <p>Invoice <strong>@Model.InstanceName</strong> has been sent to the client.</p>
189
+ <p><a href='@Model.InstanceUrl'>View invoice</a></p>
190
+ <p><i>This is a system generated mail, please do not reply.</i></p>
191
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prorigo/protrak-forge",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Protrak domain context for coding agents — MCP server for GitHub Copilot, Claude, and Cursor",
5
5
  "bin": {
6
6
  "protrak-forge": "./bin/protrak-forge.js"