@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 +2 -0
- package/bin/protrak-forge.js +274 -36
- package/data/CLAUDE.md.template +27 -0
- package/data/patterns/notification-template-reference.md +191 -0
- package/package.json +1 -1
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
|
|
package/bin/protrak-forge.js
CHANGED
|
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
|
|
|
3223
3223
|
}
|
|
3224
3224
|
return ind;
|
|
3225
3225
|
}
|
|
3226
|
-
function removeDotSegments(
|
|
3227
|
-
let input =
|
|
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 [
|
|
3427
|
-
wsComponent.path =
|
|
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,
|
|
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,
|
|
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:
|
|
7162
|
-
const fullPath = [...
|
|
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,
|
|
7277
|
+
constructor(parent, value, path17, key) {
|
|
7278
7278
|
this._cachedPath = [];
|
|
7279
7279
|
this.parent = parent;
|
|
7280
7280
|
this.data = value;
|
|
7281
|
-
this._path =
|
|
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,
|
|
10928
|
-
if (!
|
|
10927
|
+
function getElementAtPath(obj, path17) {
|
|
10928
|
+
if (!path17)
|
|
10929
10929
|
return obj;
|
|
10930
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
21434
|
+
const [node, path17] = trackDown(this._tree, prefix.slice(this._prefix.length));
|
|
21434
21435
|
if (node === void 0) {
|
|
21435
|
-
const [parentNode, key] = last(
|
|
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,
|
|
21656
|
+
var trackDown = (tree, key, path17 = []) => {
|
|
21656
21657
|
if (key.length === 0 || tree == null) {
|
|
21657
|
-
return [tree,
|
|
21658
|
+
return [tree, path17];
|
|
21658
21659
|
}
|
|
21659
21660
|
for (const k of tree.keys()) {
|
|
21660
21661
|
if (k !== LEAF && key.startsWith(k)) {
|
|
21661
|
-
|
|
21662
|
-
return trackDown(tree.get(k), key.slice(k.length),
|
|
21662
|
+
path17.push([tree, k]);
|
|
21663
|
+
return trackDown(tree.get(k), key.slice(k.length), path17);
|
|
21663
21664
|
}
|
|
21664
21665
|
}
|
|
21665
|
-
|
|
21666
|
-
return trackDown(void 0, "",
|
|
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,
|
|
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(
|
|
21715
|
+
cleanup(path17);
|
|
21715
21716
|
} else if (node.size === 1) {
|
|
21716
21717
|
const [key2, value] = node.entries().next().value;
|
|
21717
|
-
merge2(
|
|
21718
|
+
merge2(path17, key2, value);
|
|
21718
21719
|
}
|
|
21719
21720
|
};
|
|
21720
|
-
var cleanup = (
|
|
21721
|
-
if (
|
|
21721
|
+
var cleanup = (path17) => {
|
|
21722
|
+
if (path17.length === 0) {
|
|
21722
21723
|
return;
|
|
21723
21724
|
}
|
|
21724
|
-
const [node, key] = last(
|
|
21725
|
+
const [node, key] = last(path17);
|
|
21725
21726
|
node.delete(key);
|
|
21726
21727
|
if (node.size === 0) {
|
|
21727
|
-
cleanup(
|
|
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(
|
|
21732
|
+
merge2(path17.slice(0, -1), key2, value);
|
|
21732
21733
|
}
|
|
21733
21734
|
}
|
|
21734
21735
|
};
|
|
21735
|
-
var merge2 = (
|
|
21736
|
-
if (
|
|
21736
|
+
var merge2 = (path17, key, value) => {
|
|
21737
|
+
if (path17.length === 0) {
|
|
21737
21738
|
return;
|
|
21738
21739
|
}
|
|
21739
|
-
const [node, nodeKey] = last(
|
|
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.
|
|
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.
|
package/data/CLAUDE.md.template
CHANGED
|
@@ -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