@salesforce/afv-skills 1.44.0 → 1.45.0

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.
Files changed (97) hide show
  1. package/package.json +1 -1
  2. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
  3. package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
  4. package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
  5. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
  6. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
  7. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
  8. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
  9. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
  10. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
  11. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
  12. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
  13. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
  14. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
  15. package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
  16. package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
  17. package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
  18. package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
  19. package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
  20. package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
  21. package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
  22. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
  23. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
  24. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
  25. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
  26. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
  27. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
  28. package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
  29. package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
  30. package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
  31. package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
  32. package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
  33. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  34. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  35. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  36. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  37. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  38. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  39. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  40. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  41. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  42. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  43. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  44. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  45. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  46. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  47. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  48. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  49. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  50. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  51. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  52. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  53. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  54. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  55. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  56. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  57. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  58. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  59. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  60. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  61. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  62. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  63. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  64. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  65. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  66. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  67. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  68. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  69. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  70. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  71. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  72. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  73. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  74. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  75. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  76. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  77. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  78. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  79. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  80. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  81. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  82. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  83. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  84. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  85. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  86. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  87. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  88. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  89. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  90. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  91. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  92. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  93. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  94. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  95. package/skills/service-catalog-template-search/SKILL.md +0 -212
  96. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  97. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic classifier for the ACTIVATED agents a user needs access to.
3
+ //
4
+ // Reads ONE input:
5
+ // - BotDefinition query capture (from `sf data query --json`) shaped as:
6
+ // SELECT Id, DeveloperName, MasterLabel,
7
+ // (SELECT Status FROM BotVersions WHERE Status='Active')
8
+ // FROM BotDefinition WHERE Type='InternalCopilot'
9
+ // Every NGA / Agentforce agent is a BotDefinition with Type='InternalCopilot';
10
+ // an agent is ACTIVATED iff it has at least one BotVersion with Status='Active'
11
+ // (the child subquery returns ≥1 record).
12
+ //
13
+ // The Agent Access custom permset should grant access ONLY to activated agents,
14
+ // so this classifier separates activated from inactive and hands the caller the
15
+ // activated set to present for a multi-select.
16
+ //
17
+ // Usage:
18
+ // node classify-activated-agents.mjs <bot-definitions.json>
19
+ //
20
+ // Emits a single JSON object to stdout:
21
+ // { agents: [ { Id, DeveloperName, MasterLabel, active } ],
22
+ // activatedAgents: [ { Id, DeveloperName, MasterLabel } ],
23
+ // inactiveAgents: [ { Id, DeveloperName, MasterLabel } ],
24
+ // verdict: "AGENTS-FOUND" | "NONE-ACTIVE" | "CANNOT-CONFIRM",
25
+ // reasons: [...] }
26
+ // Exit is always 0 on parseable bodies; verdict carries the decision. Exit 2 on
27
+ // missing argv.
28
+
29
+ import { readFileSync } from 'node:fs';
30
+
31
+ function readEnvelope(path) {
32
+ try {
33
+ const text = readFileSync(path, 'utf8').trim();
34
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
35
+ const data = JSON.parse(text);
36
+ if (!data || typeof data !== 'object') return { ok: false, reason: `non-object JSON at ${path}` };
37
+ if (typeof data.status === 'number' && data.status !== 0) {
38
+ return { ok: false, reason: `sf data query at ${path} failed with status ${data.status}` };
39
+ }
40
+ const records = Array.isArray(data.result?.records) ? data.result.records : null;
41
+ if (!records) return { ok: false, reason: `missing result.records[] at ${path}` };
42
+ return { ok: true, records };
43
+ } catch (e) {
44
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
45
+ }
46
+ }
47
+
48
+ const [botPath] = process.argv.slice(2);
49
+ if (!botPath) {
50
+ process.stderr.write('usage: node classify-activated-agents.mjs <bot-definitions.json>\n');
51
+ process.exit(2);
52
+ }
53
+
54
+ const bots = readEnvelope(botPath);
55
+ if (!bots.ok) {
56
+ process.stdout.write(JSON.stringify({
57
+ agents: [], activatedAgents: [], inactiveAgents: [],
58
+ verdict: 'CANNOT-CONFIRM',
59
+ reasons: [bots.reason],
60
+ }, null, 2) + '\n');
61
+ process.exit(0);
62
+ }
63
+
64
+ // A child-relationship subquery surfaces as a nested { totalSize, records:[...] }
65
+ // object, or null when there are no matching children.
66
+ function activeVersionCount(r) {
67
+ const bv = r?.BotVersions;
68
+ if (!bv) return 0;
69
+ if (Array.isArray(bv.records)) return bv.records.length;
70
+ if (typeof bv.totalSize === 'number') return bv.totalSize;
71
+ return 0;
72
+ }
73
+
74
+ const agents = bots.records
75
+ .filter((r) => r && typeof r.Id === 'string')
76
+ .map((r) => ({
77
+ Id: r.Id,
78
+ DeveloperName: r.DeveloperName ?? null,
79
+ MasterLabel: r.MasterLabel ?? null,
80
+ active: activeVersionCount(r) > 0,
81
+ }));
82
+
83
+ const activatedAgents = agents.filter((a) => a.active).map(({ active, ...rest }) => rest);
84
+ const inactiveAgents = agents.filter((a) => !a.active).map(({ active, ...rest }) => rest);
85
+
86
+ const reasons = [];
87
+ let verdict;
88
+ if (activatedAgents.length > 0) {
89
+ verdict = 'AGENTS-FOUND';
90
+ reasons.push(`Found ${activatedAgents.length} activated agent(s): ${activatedAgents.map((a) => a.MasterLabel ?? a.DeveloperName).join(', ')}.`);
91
+ if (inactiveAgents.length > 0) {
92
+ reasons.push(`Excluded ${inactiveAgents.length} agent(s) with no active version (not activated yet): ${inactiveAgents.map((a) => a.MasterLabel ?? a.DeveloperName).join(', ')}.`);
93
+ }
94
+ reasons.push('Present the activated agents for a multi-select — the Agent Access permset gets one SetupEntityAccess grant per chosen agent.');
95
+ } else {
96
+ verdict = 'NONE-ACTIVE';
97
+ if (agents.length === 0) {
98
+ reasons.push('No InternalCopilot agents exist on this org — there is nothing for the Agent Access permset to grant. Create + activate an agent first.');
99
+ } else {
100
+ reasons.push(`${agents.length} InternalCopilot agent(s) exist but none has an active version — activate an agent before granting access.`);
101
+ }
102
+ }
103
+
104
+ process.stdout.write(JSON.stringify({
105
+ agents, activatedAgents, inactiveAgents, verdict, reasons,
106
+ }, null, 2) + '\n');
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic idempotency classifier for the custom `Agent_Access` permission
3
+ // set and its per-agent SetupEntityAccess grants.
4
+ //
5
+ // The Agent Access concern has two writes that must be idempotent:
6
+ // 1. Create the `Agent_Access` PermissionSet (Name=Agent_Access,
7
+ // Label="Agent Access") — only if it does not already exist.
8
+ // 2. For each CHOSEN activated agent, add a SetupEntityAccess row
9
+ // { ParentId: <Agent_Access permset Id>, SetupEntityId: <BotDefinition Id> }
10
+ // — only if a grant for that agent is not already present.
11
+ // (SetupEntityType is NOT sent in the POST body — it is not createable and
12
+ // is derived from the 0Xx key prefix; it IS filterable on the read.)
13
+ //
14
+ // Reads:
15
+ // 1. `agent-access-permset.json` — `sf data query --json` capture of
16
+ // SELECT Id, Name, Label FROM PermissionSet WHERE Name='Agent_Access'
17
+ // 2. `sea-existing.json` OR the literal sentinel `NO-PERMSET`:
18
+ // - permset present ⇒ pass the capture of
19
+ // SELECT SetupEntityId FROM SetupEntityAccess
20
+ // WHERE ParentId='<permsetId>' AND SetupEntityType='BotDefinition'
21
+ // - permset absent ⇒ pass `NO-PERMSET` (there is no parent to query yet).
22
+ // 3. `chosenAgentIds-csv` — comma-separated BotDefinition Ids the user selected.
23
+ //
24
+ // Usage:
25
+ // node classify-agent-access-state.mjs <agent-access-permset.json> <sea-existing.json | NO-PERMSET> <chosenAgentIds-csv>
26
+ //
27
+ // Emits a single JSON object to stdout:
28
+ // { permsetExists, permsetId, chosenAgentIds, grantedAgentIds, missingAgentIds,
29
+ // needsCreate, needsGrants,
30
+ // verdict: "NEEDS-WORK" | "ALREADY-COMPLETE" | "CANNOT-CONFIRM",
31
+ // reasons: [...] }
32
+ // Exit is always 0 on parseable bodies; verdict carries the decision. Exit 2 on
33
+ // missing argv.
34
+
35
+ import { readFileSync } from 'node:fs';
36
+
37
+ function readEnvelope(path) {
38
+ try {
39
+ const text = readFileSync(path, 'utf8').trim();
40
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
41
+ const data = JSON.parse(text);
42
+ if (!data || typeof data !== 'object') return { ok: false, reason: `non-object JSON at ${path}` };
43
+ if (typeof data.status === 'number' && data.status !== 0) {
44
+ return { ok: false, reason: `sf data query at ${path} failed with status ${data.status}` };
45
+ }
46
+ const records = Array.isArray(data.result?.records) ? data.result.records : null;
47
+ if (!records) return { ok: false, reason: `missing result.records[] at ${path}` };
48
+ return { ok: true, records };
49
+ } catch (e) {
50
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
51
+ }
52
+ }
53
+
54
+ const argv = process.argv.slice(2).filter((a) => a !== undefined && a !== '');
55
+ if (argv.length < 3) {
56
+ process.stderr.write('usage: node classify-agent-access-state.mjs <agent-access-permset.json> <sea-existing.json | NO-PERMSET> <chosenAgentIds-csv>\n');
57
+ process.exit(2);
58
+ }
59
+ const [permsetPath, seaArg, chosenCsv] = argv;
60
+ const chosenAgentIds = chosenCsv.split(',').map((s) => s.trim()).filter(Boolean);
61
+
62
+ function cannotConfirm(reasons) {
63
+ process.stdout.write(JSON.stringify({
64
+ permsetExists: null, permsetId: null, chosenAgentIds,
65
+ grantedAgentIds: [], missingAgentIds: [],
66
+ needsCreate: null, needsGrants: null,
67
+ verdict: 'CANNOT-CONFIRM', reasons,
68
+ }, null, 2) + '\n');
69
+ process.exit(0);
70
+ }
71
+
72
+ if (chosenAgentIds.length === 0) {
73
+ cannotConfirm(['no chosen agent Ids supplied — pass the comma-separated BotDefinition Ids the user selected.']);
74
+ }
75
+
76
+ const permset = readEnvelope(permsetPath);
77
+ if (!permset.ok) cannotConfirm([permset.reason]);
78
+
79
+ const permsetRow = permset.records.find((r) => r?.Name === 'Agent_Access') ?? null;
80
+ const permsetExists = !!permsetRow;
81
+ const permsetId = permsetExists ? (permsetRow.Id ?? null) : null;
82
+
83
+ let grantedAgentIds = [];
84
+ if (permsetExists) {
85
+ if (seaArg === 'NO-PERMSET') {
86
+ cannotConfirm(['Agent_Access permset exists but the SetupEntityAccess capture was passed as NO-PERMSET — query the existing grants and pass the real capture.']);
87
+ }
88
+ const sea = readEnvelope(seaArg);
89
+ if (!sea.ok) cannotConfirm([sea.reason]);
90
+ const grantedSet = new Set(sea.records.map((r) => r?.SetupEntityId).filter(Boolean));
91
+ grantedAgentIds = chosenAgentIds.filter((id) => grantedSet.has(id));
92
+ }
93
+ // When the permset does not exist, seaArg must be NO-PERMSET and nothing is granted.
94
+
95
+ const missingAgentIds = chosenAgentIds.filter((id) => !grantedAgentIds.includes(id));
96
+ const needsCreate = !permsetExists;
97
+ const needsGrants = missingAgentIds.length > 0;
98
+
99
+ const reasons = [];
100
+ reasons.push(permsetExists
101
+ ? `Agent_Access permission set already exists (${permsetId}).`
102
+ : 'Agent_Access permission set does not exist yet — it will be created.');
103
+ reasons.push(`Chosen agents: ${chosenAgentIds.length}; already granted: ${grantedAgentIds.length}; missing grants: ${missingAgentIds.length}.`);
104
+
105
+ const verdict = (needsCreate || needsGrants) ? 'NEEDS-WORK' : 'ALREADY-COMPLETE';
106
+ reasons.push(verdict === 'NEEDS-WORK'
107
+ ? 'Proceed to the confirm-to-write gate, then create the permset (if missing) and add one SetupEntityAccess grant per missing agent.'
108
+ : 'The Agent_Access permset exists and already grants every chosen agent — no SetupEntityAccess writes needed (still assign the permset to the target user(s) if not yet assigned).');
109
+
110
+ process.stdout.write(JSON.stringify({
111
+ permsetExists, permsetId, chosenAgentIds, grantedAgentIds, missingAgentIds,
112
+ needsCreate, needsGrants, verdict, reasons,
113
+ }, null, 2) + '\n');
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic idempotency classifier for the agent-runtime-access-assign flow.
3
+ //
4
+ // Reads the `sf data query --json` captures:
5
+ // - PermissionSetAssignment (permset ↔ user) for the target user + permset
6
+ // - PermissionSetLicenseAssign (PSL ↔ user) for the target user + PSL
7
+ // — ONLY when the selected permset actually has a backing license.
8
+ // Pass the sentinel `NO-PSL` in place of the PSLA path when the selected
9
+ // permset has no backing `LicenseId` (a standalone permset such as the
10
+ // custom `Agent_Access` set — no PSL query, no PSL POST).
11
+ // and decides whether the assign write should be skipped for this user+permset.
12
+ //
13
+ // Rules (A9):
14
+ // - needsPsl AND both rows present ⇒ needsWrite=false (already fully assigned).
15
+ // - needsPsl AND either row missing ⇒ needsWrite=true (assign the missing
16
+ // piece; the workflow assigns PSL before permset regardless of which one is
17
+ // currently missing to preserve the write-order invariant).
18
+ // - !needsPsl (pslaPath === "NO-PSL"):
19
+ // * permsetAssigned ⇒ needsWrite=false (nothing to write).
20
+ // * !permsetAssigned ⇒ needsWrite=true (assign the permset only — skip the
21
+ // PSL POST).
22
+ // `licenseAssigned` is reported as null in this branch (not applicable).
23
+ // - Query envelope missing/failed ⇒ CANNOT-CONFIRM; surface it and stop.
24
+ //
25
+ // Usage:
26
+ // node classify-assignment-state.mjs <psa-existing.json> <psla-existing.json | NO-PSL>
27
+ //
28
+ // Emits a single JSON object to stdout:
29
+ // { permsetAssigned, licenseAssigned, needsWrite,
30
+ // verdict: "NEEDS-WRITE" | "ALREADY-ASSIGNED" | "CANNOT-CONFIRM",
31
+ // reasons: [...] }
32
+ // Exit is always 0 on parseable bodies; verdict carries the decision.
33
+
34
+ import { readFileSync } from 'node:fs';
35
+
36
+ function readEnvelope(path) {
37
+ try {
38
+ const text = readFileSync(path, 'utf8').trim();
39
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
40
+ const data = JSON.parse(text);
41
+ if (!data || typeof data !== 'object') return { ok: false, reason: `non-object JSON at ${path}` };
42
+ if (typeof data.status === 'number' && data.status !== 0) {
43
+ return { ok: false, reason: `sf data query at ${path} failed with status ${data.status}` };
44
+ }
45
+ const records = Array.isArray(data.result?.records) ? data.result.records : null;
46
+ if (!records) return { ok: false, reason: `missing result.records[] at ${path}` };
47
+ return { ok: true, records };
48
+ } catch (e) {
49
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
50
+ }
51
+ }
52
+
53
+ const argv = process.argv.slice(2).filter((a) => !!a);
54
+ if (argv.length < 2) {
55
+ process.stderr.write('usage: node classify-assignment-state.mjs <psa-existing.json> <psla-existing.json | NO-PSL>\n');
56
+ process.exit(2);
57
+ }
58
+ const [psaPath, pslaArg] = argv;
59
+ const needsPsl = pslaArg !== 'NO-PSL';
60
+
61
+ const psa = readEnvelope(psaPath);
62
+ const psla = needsPsl ? readEnvelope(pslaArg) : { ok: true, records: [], skipped: true };
63
+
64
+ const readErrors = [psa, psla].filter((r) => !r.ok).map((r) => r.reason);
65
+ if (readErrors.length) {
66
+ process.stdout.write(JSON.stringify({
67
+ permsetAssigned: null, licenseAssigned: null, needsWrite: null,
68
+ verdict: 'CANNOT-CONFIRM',
69
+ reasons: readErrors,
70
+ }, null, 2) + '\n');
71
+ process.exit(0);
72
+ }
73
+
74
+ const permsetAssigned = psa.records.length > 0;
75
+ const licenseAssigned = needsPsl ? psla.records.length > 0 : null;
76
+ const needsWrite = needsPsl
77
+ ? !(permsetAssigned && licenseAssigned)
78
+ : !permsetAssigned;
79
+
80
+ const verdict = needsWrite ? 'NEEDS-WRITE' : 'ALREADY-ASSIGNED';
81
+ const reasons = [
82
+ `PermissionSetAssignment: ${permsetAssigned ? 'present' : 'missing'} (${psa.records.length} row${psa.records.length === 1 ? '' : 's'}).`,
83
+ needsPsl
84
+ ? `PermissionSetLicenseAssign: ${licenseAssigned ? 'present' : 'missing'} (${psla.records.length} row${psla.records.length === 1 ? '' : 's'}).`
85
+ : 'PermissionSetLicenseAssign: not applicable — the selected permission set has no backing LicenseId (standalone-permset flow; PSL SOQL + POST are skipped).',
86
+ ];
87
+ reasons.push(
88
+ needsWrite
89
+ ? (needsPsl
90
+ ? 'At least one assignment is missing — proceed to the confirm-to-write gate, then assign (PSL first, permset second).'
91
+ : 'Permission set assignment is missing — proceed to the confirm-to-write gate, then assign the permset ONLY (skip the PSL POST).')
92
+ : (needsPsl
93
+ ? 'Both the permission set and its backing license are already assigned to this user — skip the write, go straight to verify.'
94
+ : 'The permission set is already assigned to this user (no backing license required) — skip the write, go straight to verify.')
95
+ );
96
+
97
+ process.stdout.write(JSON.stringify({
98
+ permsetAssigned, licenseAssigned, needsWrite, verdict, reasons,
99
+ }, null, 2) + '\n');
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic availability classifier for the platform feature permsets an
3
+ // ITSM agent's actions need at RUNTIME (so a user can exercise the agent).
4
+ //
5
+ // Reads ONE input:
6
+ // - PermissionSet query capture (from `sf data query --json`) filtered to the
7
+ // six known platform feature permset `Name` values across three features,
8
+ // each with a "use" tier (lighter, user/agent-facing) and an "admin" tier:
9
+ //
10
+ // Feature use tier admin tier
11
+ // ----------------- -------------------------------- --------------------------------
12
+ // Prompt Templates EinsteinGPTPromptTemplateUser EinsteinGPTPromptTemplateManager
13
+ // Data Cloud GenieUserEnhancedSecurity GenieAdmin
14
+ // Unified Catalog UnifiedCatalogCommunityUser UnifiedCatalogAdmin
15
+ //
16
+ // No single org is guaranteed to have all three features provisioned, so the
17
+ // classifier reports which tiers are actually PRESENT (a row exists) and which
18
+ // are ABSENT (feature not provisioned on this org — skip it, do not fail).
19
+ //
20
+ // `needsPsl` is derived PER ROW from `LicenseId !== null` — the write path POSTs
21
+ // a PermissionSetLicenseAssign using the row's own `LicenseId`; PSL names are
22
+ // never hard-coded. `displayLabel` prefers the org's actual `Label`, falling
23
+ // back to a canonical label when the row is absent.
24
+ //
25
+ // Usage:
26
+ // node classify-platform-permset-availability.mjs <permsets.json>
27
+ //
28
+ // Emits a single JSON object to stdout:
29
+ // { features: [ { feature, tiers: { use, admin }, anyPresent } ],
30
+ // provisionedFeatures: [...], absentFeatures: [...],
31
+ // verdict: "ASSIGNABLE" | "NONE-PROVISIONED" | "CANNOT-CONFIRM",
32
+ // reasons: [...] }
33
+ // where each tier is { tier, name, displayLabel, present, Id, LicenseId, needsPsl }.
34
+ // Exit is always 0 on parseable bodies; verdict carries the decision. Exit 2 on
35
+ // missing argv.
36
+
37
+ import { readFileSync } from 'node:fs';
38
+
39
+ // The fixed catalog. `label` is the canonical fallback; the org's own Label wins
40
+ // when the row is present (e.g. GenieAdmin ships as "Data Cloud Architect" on
41
+ // some orgs, "Data Cloud Admin" on others — render whatever the org says).
42
+ const CATALOG = [
43
+ {
44
+ feature: 'Prompt Templates',
45
+ use: { name: 'EinsteinGPTPromptTemplateUser', label: 'Prompt Template User' },
46
+ admin: { name: 'EinsteinGPTPromptTemplateManager', label: 'Prompt Template Manager' },
47
+ },
48
+ {
49
+ feature: 'Data Cloud',
50
+ use: { name: 'GenieUserEnhancedSecurity', label: 'Data Cloud User' },
51
+ admin: { name: 'GenieAdmin', label: 'Data Cloud Admin' },
52
+ },
53
+ {
54
+ feature: 'Unified Catalog',
55
+ use: { name: 'UnifiedCatalogCommunityUser', label: 'Unified Catalog Community User' },
56
+ admin: { name: 'UnifiedCatalogAdmin', label: 'Unified Catalog Admin' },
57
+ },
58
+ ];
59
+
60
+ function readEnvelope(path) {
61
+ try {
62
+ const text = readFileSync(path, 'utf8').trim();
63
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
64
+ const data = JSON.parse(text);
65
+ if (!data || typeof data !== 'object') return { ok: false, reason: `non-object JSON at ${path}` };
66
+ if (typeof data.status === 'number' && data.status !== 0) {
67
+ return { ok: false, reason: `sf data query at ${path} failed with status ${data.status}` };
68
+ }
69
+ const records = Array.isArray(data.result?.records) ? data.result.records : null;
70
+ if (!records) return { ok: false, reason: `missing result.records[] at ${path}` };
71
+ return { ok: true, records };
72
+ } catch (e) {
73
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
74
+ }
75
+ }
76
+
77
+ const [psPath] = process.argv.slice(2);
78
+ if (!psPath) {
79
+ process.stderr.write('usage: node classify-platform-permset-availability.mjs <permsets.json>\n');
80
+ process.exit(2);
81
+ }
82
+
83
+ const ps = readEnvelope(psPath);
84
+ if (!ps.ok) {
85
+ process.stdout.write(JSON.stringify({
86
+ features: [],
87
+ provisionedFeatures: [],
88
+ absentFeatures: CATALOG.map((f) => f.feature),
89
+ verdict: 'CANNOT-CONFIRM',
90
+ reasons: [ps.reason],
91
+ }, null, 2) + '\n');
92
+ process.exit(0);
93
+ }
94
+
95
+ // Index the query rows by PermissionSet.Name for O(1) lookup.
96
+ const byName = new Map();
97
+ for (const r of ps.records) {
98
+ if (r && typeof r.Name === 'string') byName.set(r.Name, r);
99
+ }
100
+
101
+ function tierOf(tier, spec) {
102
+ const row = byName.get(spec.name);
103
+ const present = !!row;
104
+ return {
105
+ tier,
106
+ name: spec.name,
107
+ displayLabel: (present && typeof row.Label === 'string' && row.Label) ? row.Label : spec.label,
108
+ present,
109
+ Id: present ? (row.Id ?? null) : null,
110
+ LicenseId: present ? (row.LicenseId ?? null) : null,
111
+ needsPsl: present ? ((row.LicenseId ?? null) !== null) : false,
112
+ };
113
+ }
114
+
115
+ const features = CATALOG.map((f) => {
116
+ const use = tierOf('use', f.use);
117
+ const admin = tierOf('admin', f.admin);
118
+ return { feature: f.feature, tiers: { use, admin }, anyPresent: use.present || admin.present };
119
+ });
120
+
121
+ const provisionedFeatures = features.filter((f) => f.anyPresent).map((f) => f.feature);
122
+ const absentFeatures = features.filter((f) => !f.anyPresent).map((f) => f.feature);
123
+
124
+ const reasons = [];
125
+ let verdict;
126
+ if (provisionedFeatures.length === 0) {
127
+ verdict = 'NONE-PROVISIONED';
128
+ reasons.push('None of the three platform features (Prompt Templates, Data Cloud, Unified Catalog) is provisioned on this org — no runtime action-execution permset can be assigned.');
129
+ reasons.push('This does NOT block the Agent Access concern; continue there so users can still open the activated agents.');
130
+ } else {
131
+ verdict = 'ASSIGNABLE';
132
+ reasons.push(`Provisioned feature(s) with an assignable tier: ${provisionedFeatures.join(', ')}.`);
133
+ if (absentFeatures.length > 0) {
134
+ reasons.push(`Not provisioned on this org (report as unavailable, do not assign): ${absentFeatures.join(', ')}.`);
135
+ }
136
+ reasons.push('For EACH provisioned feature, ask the user which tier to assign (use/agent vs admin) — do not auto-select a tier.');
137
+ const standalone = [];
138
+ for (const f of features) {
139
+ for (const t of ['use', 'admin']) {
140
+ const row = f.tiers[t];
141
+ if (row.present && !row.needsPsl) standalone.push(row.name);
142
+ }
143
+ }
144
+ if (standalone.length > 0) {
145
+ reasons.push(`Standalone tier(s) with no backing LicenseId (assign skips PSL SOQL/POST if selected): ${standalone.join(', ')}.`);
146
+ }
147
+ }
148
+
149
+ process.stdout.write(JSON.stringify({
150
+ features,
151
+ provisionedFeatures,
152
+ absentFeatures,
153
+ verdict,
154
+ reasons,
155
+ }, null, 2) + '\n');
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // Per-target-user eligibility gate for the Unified Catalog feature tiers. The
3
+ // Unified Catalog PSLs are license-SHAPE gated (not seat-gated): the Community
4
+ // User tier can be held only by a Unified Employee-licensed user, and the Admin
5
+ // tier is meaningful only for a System Administrator. Offering a tier the user's
6
+ // license/profile can never hold produces a hard license-shape failure at write
7
+ // time, so this gate decides which UC tiers to OFFER for a given user — or to omit
8
+ // Unified Catalog for that user entirely. A pre-write eligibility gate, evaluated
9
+ // per target user; it never writes.
10
+ //
11
+ // Inputs (argv):
12
+ // 1. availability.json — the classify-platform-permset-availability.mjs output
13
+ // capture (carries the Unified Catalog tiers with present/Id/LicenseId/needsPsl).
14
+ // 2. user.json — a `sf data query --json` capture of the ONE target user,
15
+ // selecting Profile.Name and Profile.UserLicense.Name.
16
+ //
17
+ // Output (stdout JSON):
18
+ // { feature:"Unified Catalog", provisioned,
19
+ // user:{ profile, license, unifiedEmployee, sysAdmin },
20
+ // offer:[ <tier objects from the availability classifier> ], omit, note }
21
+ // offer[] is the subset of PRESENT UC tiers this user is eligible for:
22
+ // - "use" (UnifiedCatalogCommunityUser) → only when the user is Unified Employee
23
+ // - "admin" (UnifiedCatalogAdmin) → only when the user is System Administrator
24
+ // omit=true (offer empty) ⇒ do NOT present Unified Catalog for this user; report it
25
+ // "not applicable for this user's license/profile — skipped" (never a failed write).
26
+ // Fails CLOSED: an unreadable user capture yields omit=true (better to skip than to
27
+ // offer a tier that will fail). Exit 0 on parseable bodies; exit 2 on missing argv.
28
+
29
+ import { readFileSync } from 'node:fs';
30
+
31
+ // The Unified Employee cohort (Community User tier) and the System Administrator
32
+ // profile (Admin tier) — the only two cohorts whose license/profile shape can hold
33
+ // a Unified Catalog tier.
34
+ const UNIFIED_RE = /unified\s*employee/i;
35
+ const SYSADMIN_RE = /system\s*administrator/i;
36
+
37
+ function readJson(path) {
38
+ try {
39
+ const text = readFileSync(path, 'utf8').trim();
40
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
41
+ return { ok: true, data: JSON.parse(text) };
42
+ } catch (e) {
43
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
44
+ }
45
+ }
46
+
47
+ function emit(obj) {
48
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
49
+ }
50
+
51
+ const [availPath, userPath] = process.argv.slice(2);
52
+ if (!availPath || !userPath) {
53
+ process.stderr.write('usage: node gate-unified-catalog-tiers.mjs <availability.json> <user.json>\n');
54
+ process.exit(2);
55
+ }
56
+
57
+ const avail = readJson(availPath);
58
+ const userEnv = readJson(userPath);
59
+
60
+ // Locate the Unified Catalog feature block emitted by the availability classifier.
61
+ const ucFeature = avail.ok && Array.isArray(avail.data?.features)
62
+ ? avail.data.features.find((f) => f.feature === 'Unified Catalog')
63
+ : null;
64
+ const useTier = ucFeature?.tiers?.use ?? null;
65
+ const adminTier = ucFeature?.tiers?.admin ?? null;
66
+ const provisioned = !!(useTier?.present || adminTier?.present);
67
+
68
+ // The one target user's profile + license (result.records[0]).
69
+ const rec = userEnv.ok ? (userEnv.data?.result?.records?.[0] ?? null) : null;
70
+ const profile = `${rec?.Profile?.Name ?? ''}`;
71
+ const license = `${rec?.Profile?.UserLicense?.Name ?? ''}`;
72
+ const unifiedEmployee = UNIFIED_RE.test(license) || UNIFIED_RE.test(profile);
73
+ const sysAdmin = SYSADMIN_RE.test(profile);
74
+
75
+ const offer = [];
76
+ if (useTier?.present && unifiedEmployee) offer.push(useTier);
77
+ if (adminTier?.present && sysAdmin) offer.push(adminTier);
78
+
79
+ const omit = offer.length === 0;
80
+ let note;
81
+ if (!avail.ok) {
82
+ note = `availability capture unreadable (${avail.reason}) — cannot gate Unified Catalog; omit for this user.`;
83
+ } else if (!provisioned) {
84
+ note = 'Unified Catalog is not provisioned on this org — skip it (already handled by the availability classifier).';
85
+ } else if (!userEnv.ok) {
86
+ note = `target-user capture unreadable (${userEnv.reason}) — cannot confirm the user's cohort; omit Unified Catalog for this user.`;
87
+ } else if (omit) {
88
+ note = `This user (license "${license || '?'}", profile "${profile || '?'}") is neither a Unified Employee (Community User tier) nor a System Administrator (Admin tier) — omit Unified Catalog for this user; report "not applicable for this user's license/profile — skipped".`;
89
+ } else {
90
+ note = `Offer the ${offer.map((t) => t.tier).join(' + ')} tier(s) for Unified Catalog (unifiedEmployee=${unifiedEmployee}, sysAdmin=${sysAdmin}).`;
91
+ }
92
+
93
+ emit({
94
+ feature: 'Unified Catalog',
95
+ provisioned,
96
+ user: { profile, license, unifiedEmployee, sysAdmin },
97
+ offer,
98
+ omit,
99
+ note,
100
+ });
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Ranks REAL, human candidate users to offer as selectable options behind the
3
+ // Phase-2 AskUserQuestion when the caller did not name a target user. This is a
4
+ // presentation aid, never a gating decision — it keeps the offered list
5
+ // deterministic (no editorializing down to one name) and audience-appropriate.
6
+ //
7
+ // Inputs (argv):
8
+ // 1. users.json — capture of `sf data query --json` selecting active users
9
+ // with Id, Name, Username, Profile.Name, Profile.UserLicense.Name.
10
+ // 2. audience — 'fulfiller' | 'employee' | 'any' (anything else ⇒ 'any').
11
+ // A Fulfiller agent is run by IT staff on a STANDARD license;
12
+ // an Employee agent by end employees on a UNIFIED EMPLOYEE
13
+ // license — so the matching cohort is surfaced first.
14
+ // 3. runningUserId — optional; the "Me" user, excluded here (offered separately
15
+ // as the recommended option).
16
+ //
17
+ // Output (stdout JSON):
18
+ // { candidates: [ { Id, Name, Username, profile, cohort } ], audience, total, note }
19
+ // at most 5 candidates; service/integration/bot/agent accounts removed. Exit is
20
+ // always 0 on a parseable body (empty candidates ⇒ offer "Me" + free-text only);
21
+ // exit 2 only on missing argv.
22
+
23
+ import { readFileSync } from 'node:fs';
24
+
25
+ // Non-human / service accounts are never good runtime-access targets. Matched on
26
+ // profile name, license name, username, OR display name so a mislabeled record
27
+ // on any one field is still filtered.
28
+ const SERVICE_RE = /integration|automated process|einstein agent|analytics cloud|salesforceiq|\binsights\b|chatter (free|external)|guest|portal|sites|\bbot\b|botuser|digitalagent/i;
29
+
30
+ // The Unified Employee cohort — the human tier an Employee agent's users belong to.
31
+ // It is defined by the Unified Employee USER LICENSE, which is the authoritative
32
+ // signal; the profile name is only a defensive fallback (a custom profile carrying
33
+ // the license must still rank as Unified Employee).
34
+ const UNIFIED_RE = /unified\s*employee/i;
35
+
36
+ function readEnvelope(path) {
37
+ try {
38
+ const text = readFileSync(path, 'utf8').trim();
39
+ if (!text) return { ok: false, reason: `empty file: ${path}` };
40
+ const data = JSON.parse(text);
41
+ if (data && typeof data.status === 'number' && data.status !== 0) {
42
+ return { ok: false, reason: `sf data query at ${path} failed with status ${data.status}` };
43
+ }
44
+ const records = Array.isArray(data?.result?.records) ? data.result.records : null;
45
+ if (!records) return { ok: false, reason: `missing result.records[] at ${path}` };
46
+ return { ok: true, records };
47
+ } catch (e) {
48
+ return { ok: false, reason: `unreadable/invalid JSON at ${path}: ${e?.message ?? e}` };
49
+ }
50
+ }
51
+
52
+ function emit(obj) {
53
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
54
+ }
55
+
56
+ const [usersPath, audienceArg, runningUserId] = process.argv.slice(2);
57
+ if (!usersPath) {
58
+ process.stderr.write('usage: node rank-candidate-users.mjs <users.json> [fulfiller|employee|any] [runningUserId]\n');
59
+ process.exit(2);
60
+ }
61
+ const audience = (audienceArg === 'fulfiller' || audienceArg === 'employee') ? audienceArg : 'any';
62
+
63
+ const env = readEnvelope(usersPath);
64
+ if (!env.ok) {
65
+ emit({ candidates: [], audience, total: 0, note: `${env.reason} — offer "Me" plus free-text entry only.` });
66
+ process.exit(0);
67
+ }
68
+
69
+ const profileName = (r) => `${r?.Profile?.Name ?? ''}`;
70
+ const licenseName = (r) => `${r?.Profile?.UserLicense?.Name ?? ''}`;
71
+ const isService = (r) =>
72
+ SERVICE_RE.test(profileName(r)) || SERVICE_RE.test(licenseName(r)) ||
73
+ SERVICE_RE.test(`${r?.Username ?? ''}`) || SERVICE_RE.test(`${r?.Name ?? ''}`);
74
+ const cohortOf = (r) =>
75
+ (UNIFIED_RE.test(licenseName(r)) || UNIFIED_RE.test(profileName(r)) ? 'unified-employee' : 'standard');
76
+
77
+ const humans = env.records
78
+ .filter((r) => r && typeof r.Id === 'string')
79
+ .filter((r) => r.Id !== runningUserId)
80
+ .filter((r) => !isService(r))
81
+ .map((r) => ({ Id: r.Id, Name: r.Name ?? r.Username, Username: r.Username, profile: profileName(r), cohort: cohortOf(r) }));
82
+
83
+ // Rank the audience-matching cohort first, preserving input order within a cohort
84
+ // (the query already orders by most-recent login).
85
+ const prefer = audience === 'employee' ? 'unified-employee' : audience === 'fulfiller' ? 'standard' : null;
86
+ const ranked = prefer
87
+ ? [...humans.filter((u) => u.cohort === prefer), ...humans.filter((u) => u.cohort !== prefer)]
88
+ : humans;
89
+
90
+ const candidates = ranked.slice(0, 5);
91
+ const note = candidates.length === 0
92
+ ? 'No non-service human users found besides the running user — offer "Me" plus free-text entry only.'
93
+ : `Offering ${candidates.length} candidate(s)${prefer ? `, ${prefer} cohort first (audience: ${audience})` : ''}.`;
94
+
95
+ emit({ candidates, audience, total: humans.length, note });