@pitcher/cli-scripts 2.10.0 → 2.12.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 (35) hide show
  1. package/README.md +32 -2
  2. package/dist/index.js +2 -0
  3. package/dist/src/commands/ld/assessment.d.ts +61 -0
  4. package/dist/src/commands/ld/assessment.js +235 -0
  5. package/dist/src/commands/ld/canvasCourse.d.ts +20 -0
  6. package/dist/src/commands/ld/canvasCourse.js +69 -0
  7. package/dist/src/commands/ld/cert.d.ts +93 -0
  8. package/dist/src/commands/ld/cert.js +355 -0
  9. package/dist/src/commands/ld/course.d.ts +62 -0
  10. package/dist/src/commands/ld/course.js +308 -0
  11. package/dist/src/commands/ld/module.d.ts +43 -0
  12. package/dist/src/commands/ld/module.js +199 -0
  13. package/dist/src/commands/ld.d.ts +2 -0
  14. package/dist/src/commands/ld.js +15 -0
  15. package/dist/src/core/ld.d.ts +38 -0
  16. package/dist/src/core/ld.js +102 -0
  17. package/dist/src/core/readInput.d.ts +1 -0
  18. package/dist/src/core/readInput.js +14 -0
  19. package/dist/src/mcp/bundled.js +1 -1
  20. package/dist/src/mcp/prompts/announcements.d.ts +2 -0
  21. package/dist/src/mcp/prompts/announcements.js +35 -0
  22. package/dist/src/mcp/prompts/dsr.d.ts +2 -0
  23. package/dist/src/mcp/prompts/dsr.js +50 -0
  24. package/dist/src/mcp/prompts/ldCanvasCourse.d.ts +2 -0
  25. package/dist/src/mcp/prompts/ldCanvasCourse.js +50 -0
  26. package/dist/src/mcp/prompts/registry.js +10 -1
  27. package/dist/src/mcp/prompts/shared.d.ts +1 -0
  28. package/dist/src/mcp/prompts/shared.js +12 -7
  29. package/dist/src/mcp/registry.js +2 -0
  30. package/dist/src/mcp/resources/ldCanvasDesign.d.ts +3 -0
  31. package/dist/src/mcp/resources/ldCanvasDesign.js +51 -0
  32. package/dist/src/mcp/resources/registry.js +2 -1
  33. package/dist/src/mcp/tools/ld.d.ts +2 -0
  34. package/dist/src/mcp/tools/ld.js +680 -0
  35. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ import { resolveApiKey } from './auth.js';
2
+ import { clientFor, makeClient, toCliError } from './http.js';
3
+ import { createCliError } from './errors.js';
4
+ const LD_APP_CONFIG_PATH = '/core/api/protected/apps/ld-app/config';
5
+ const baseUrlCache = new Map();
6
+ /** Test-only: reset the resolved-base-URL cache between cases. */
7
+ export function __clearLdBaseUrlCache() {
8
+ baseUrlCache.clear();
9
+ }
10
+ /** Every L&D run* requires an instance; returns it narrowed to string, else VALIDATION. */
11
+ export function requireInstance(opts) {
12
+ if (!opts.instanceId)
13
+ throw createCliError('VALIDATION', '--instance-id is required');
14
+ return opts.instanceId;
15
+ }
16
+ /** Copy the truthy values of `keys` from `opts` into a params object (shared list-filter pick). */
17
+ export function pickTruthy(opts, keys) {
18
+ const out = {};
19
+ for (const k of keys)
20
+ if (opts[k])
21
+ out[k] = opts[k];
22
+ return out;
23
+ }
24
+ function trimUrl(raw, source) {
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(raw);
28
+ }
29
+ catch {
30
+ // Do NOT echo the raw value — an override may carry userinfo (user:secret@host).
31
+ throw createCliError('VALIDATION', `Invalid L&D base URL from ${source}`);
32
+ }
33
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
34
+ throw createCliError('VALIDATION', `L&D base URL must be http(s) (${source}): ${parsed.origin}`);
35
+ }
36
+ // origin + pathname only: strips any userinfo/query/hash; trailing slash trimmed.
37
+ return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, '');
38
+ }
39
+ /**
40
+ * Resolve the standalone L&D Lambda base URL. Order: --ld-url flag → NG_LD_API_URL env →
41
+ * ld-app app config (api_base_url) via the platform client. Cached per (org, instanceId).
42
+ * The org key can't be region-inferred, so app config (region-correct) is the default.
43
+ */
44
+ export async function resolveLdBaseUrl(opts, deps = {}, env = process.env) {
45
+ const override = opts.ldUrl?.trim() || env.NG_LD_API_URL?.trim();
46
+ if (override)
47
+ return trimUrl(override, opts.ldUrl ? '--ld-url' : 'NG_LD_API_URL');
48
+ if (!opts.instanceId) {
49
+ throw createCliError('VALIDATION', '--instance-id is required to resolve the L&D API URL (or set NG_LD_API_URL)');
50
+ }
51
+ const cacheKey = `${opts.org ?? 'dev'}:${opts.instanceId}`;
52
+ const cached = baseUrlCache.get(cacheKey);
53
+ if (cached)
54
+ return cached;
55
+ const platform = deps.platformClient ?? clientFor(opts);
56
+ const res = await platform.get(LD_APP_CONFIG_PATH, { params: { instance_id: opts.instanceId } });
57
+ const apiBaseUrl = res.data?.api_base_url;
58
+ if (typeof apiBaseUrl !== 'string' || !apiBaseUrl) {
59
+ throw createCliError('NOT_FOUND', 'ld-app is not configured in this instance (no api_base_url). Set --ld-url or NG_LD_API_URL.');
60
+ }
61
+ const url = trimUrl(apiBaseUrl, 'ld-app config');
62
+ baseUrlCache.set(cacheKey, url);
63
+ return url;
64
+ }
65
+ /** Build (or inject, in tests) an axios client pointed at the resolved L&D Lambda base. */
66
+ export async function ldClientFor(opts, deps = {}, env = process.env) {
67
+ if (deps.ldClient)
68
+ return deps.ldClient;
69
+ const base = await resolveLdBaseUrl(opts, deps, env);
70
+ return makeClient(base, resolveApiKey(opts));
71
+ }
72
+ /**
73
+ * Unwrap the L&D success envelope `{ success, data, message? }` to its resource payload.
74
+ * Falls back to the raw body if it is not enveloped (defensive).
75
+ */
76
+ export function ldData(res) {
77
+ const body = res?.data;
78
+ return body && typeof body === 'object' && 'data' in body ? body.data : body;
79
+ }
80
+ /**
81
+ * Map an L&D axios error to a clean CliError. The L&D error body is `{ success:false,
82
+ * message, statusCode }`; the generic `extractApiMessage` would concatenate every key, so
83
+ * read `.message` explicitly and derive the code from the HTTP status. Falls back to `toCliError`.
84
+ */
85
+ export function toLdCliError(e) {
86
+ const err = e;
87
+ const msg = err?.response?.data?.message;
88
+ if (typeof msg === 'string' && msg) {
89
+ const s = err.response?.status;
90
+ const code = s === 401 || s === 403
91
+ ? 'AUTH'
92
+ : s === 404
93
+ ? 'NOT_FOUND'
94
+ : s === 409
95
+ ? 'CONFLICT'
96
+ : s && s >= 400 && s < 500
97
+ ? 'VALIDATION'
98
+ : 'NETWORK';
99
+ return createCliError(code, msg);
100
+ }
101
+ return toCliError(e);
102
+ }
@@ -2,3 +2,4 @@ export declare function readTextFile(filePath: string): string;
2
2
  export declare function readJsonFile(filePath: string): unknown;
3
3
  export declare const MAX_INLINE_BYTES = 400000;
4
4
  export declare function resolveTextInput(inline: string | undefined, file: string | undefined, label: string): string | undefined;
5
+ export declare function parseJsonInput(inline: string | undefined, file: string | undefined, label: string): unknown;
@@ -45,3 +45,17 @@ export function resolveTextInput(inline, file, label) {
45
45
  return readTextFile(file);
46
46
  return undefined;
47
47
  }
48
+ // Resolve an inline-or-file JSON flag and parse it. VALIDATION when neither is given
49
+ // (`provide --<label> (inline JSON) or --<label>-file`) or the content is not valid JSON.
50
+ // Used by the L&D canvas / question / assessment-link inputs.
51
+ export function parseJsonInput(inline, file, label) {
52
+ const raw = resolveTextInput(inline, file, label);
53
+ if (!raw)
54
+ throw createCliError('VALIDATION', `provide --${label} (inline JSON) or --${label}-file`);
55
+ try {
56
+ return JSON.parse(raw);
57
+ }
58
+ catch {
59
+ throw createCliError('VALIDATION', `${label} is not valid JSON`);
60
+ }
61
+ }
@@ -1,7 +1,7 @@
1
1
  import { allTools } from './registry.js';
2
2
  // Developer-only tools excluded from the consumer .mcpb bundle. `publish_app` is alwaysDestructive
3
3
  // (creates a live, org-visible app release); `dev_sync`/`dev_sync_revert` mutate remote S3 dev state.
4
- // None are relevant to a non-technical adopter installing the extension. The CLI `p mcp` keeps all 50.
4
+ // None are relevant to a non-technical adopter installing the extension. The CLI `p mcp` keeps all 113.
5
5
  export const BUNDLE_EXCLUDED_TOOLS = new Set(['pitcher_publish_app', 'pitcher_dev_sync', 'pitcher_dev_sync_revert']);
6
6
  // The tool set the .mcpb bundle registers.
7
7
  export const bundledTools = allTools.filter((t) => !BUNDLE_EXCLUDED_TOOLS.has(t.name));
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const announcementsPrompts: PromptDescriptor[];
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { instanceLine, recipePreamble } from './shared.js';
3
+ export const announcementsPrompts = [
4
+ {
5
+ name: 'seed_announcement',
6
+ title: 'Create announcement',
7
+ description: 'Author an on-brand in-app announcement (title + body HTML + CTA) and create it in an instance.',
8
+ argsSchema: {
9
+ instance_id: z.string().optional().describe('Target instance ID (leave blank to resolve interactively)'),
10
+ vertical: z.string().optional().describe('pharma | medtech | cpg | finserv | generic'),
11
+ use_case: z.string().optional().describe('Short description of what this announcement is about'),
12
+ },
13
+ build: (args) => {
14
+ const vertical = args.vertical || 'generic';
15
+ const useCase = args.use_case || 'a general product / process announcement a rep would see in-app';
16
+ const text = [
17
+ recipePreamble(),
18
+ '',
19
+ '## Task: seed an announcement',
20
+ instanceLine(args.instance_id),
21
+ `Vertical: ${vertical}. Use case: ${useCase}.`,
22
+ '',
23
+ 'Steps:',
24
+ '1. Draft a short, on-brand announcement: a concise title and a **body in inline-CSS HTML** ' +
25
+ 'following the principles above — a few sentences, one clear call-to-action, brand-derived colors, ' +
26
+ 'and any dynamic values via escaped `{{token}}` merge fields.',
27
+ '2. Create it by calling `pitcher_announcements_create` with `{ instanceId, title, body }` — ' +
28
+ 'pass the HTML **inline via `body`** (a file path cannot cross a host\'s sandbox boundary, so always inline). ' +
29
+ 'Add `priority` only if the user asks for an important announcement.',
30
+ '3. Report the created announcement id and title.',
31
+ ].join('\n');
32
+ return [{ role: 'user', content: { type: 'text', text } }];
33
+ },
34
+ },
35
+ ];
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const dsrPrompts: PromptDescriptor[];
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import { instanceLine, safetyPreamble } from './shared.js';
3
+ export const dsrPrompts = [
4
+ {
5
+ name: 'seed_dsr',
6
+ title: 'Create and share a DSR',
7
+ description: 'Share a canvas (or files) as a Digital Sales Room with the right gating, optionally branded with a landing page.',
8
+ argsSchema: {
9
+ instance_id: z.string().optional().describe('Target instance ID (leave blank to resolve interactively)'),
10
+ canvas_id: z.string().optional().describe('Canvas to share (leave blank to pick one interactively)'),
11
+ audience: z.string().optional().describe('Who the DSR is for, e.g. "internal", "a known contact", "an account/domain"'),
12
+ },
13
+ build: (args) => {
14
+ const audience = args.audience || 'the intended external recipient';
15
+ const text = [
16
+ // NOTE: this is a share WORKFLOW recipe, not an HTML-authoring one — it does NOT author a
17
+ // landing page (that is the separate `seed_dsr_landing` recipe). It only BINDS an existing
18
+ // template. So it embeds only the safety guidance, not the HTML design principles.
19
+ safetyPreamble(),
20
+ '',
21
+ '## DSR sharing guidance',
22
+ '- **Source is exactly one of** a canvas (`canvasId`) OR files (`shareboxFileIds`).',
23
+ `- **Pick the gating (\`authStrategy\`) by audience.** For ${audience}: internal/quick demo → \`none\` ` +
24
+ '(open link); a known individual → `specified_emails` (+ `allowedEmails`); a company → ' +
25
+ '`specified_domains` (+ `allowedDomains`); the server default is `any_email`. Add `validUntil` ' +
26
+ '(ISO datetime) to time-box a customer-facing link; set `allowForward`/`allowDownload` deliberately.',
27
+ '- **Branding the pre-auth page is a SEPARATE step:** this recipe only **binds** an existing ' +
28
+ 'landing template (`landingTemplateId`, discover via `pitcher_dsr_landing_list`; pass ' +
29
+ '`landingContext` merge fields **as a JSON object serialized to a string**, not as an object). ' +
30
+ 'If no suitable template exists and the user wants a ' +
31
+ 'branded landing, author one FIRST with the **`seed_dsr_landing`** recipe, then bind its id here. ' +
32
+ 'Do NOT hand-write landing HTML in this recipe.',
33
+ '',
34
+ '## Task: create and share a DSR',
35
+ instanceLine(args.instance_id),
36
+ args.canvas_id ? `Canvas to share: ${args.canvas_id}.` : 'No canvas given — help the user pick one via `pitcher_canvases_list`.',
37
+ `Audience: ${audience}.`,
38
+ '',
39
+ 'Steps:',
40
+ '1. Resolve the source (the given `canvas_id`, or pick a canvas with the user via `pitcher_canvases_list`).',
41
+ '2. Choose the gating strategy with the user per the guidance above; set expiry/forward/download as appropriate.',
42
+ '3. (Optional) To brand the pre-auth page, list templates with `pitcher_dsr_landing_list` and bind one via ' +
43
+ '`landingTemplateId` — or point the user at `seed_dsr_landing` to author a new one first.',
44
+ '4. Call `pitcher_dsr_create` with the resolved fields. A denied/failed landing bind returns the link with a ' +
45
+ 'warning (not a failure) — surface it. Report the returned `short_url`.',
46
+ ].join('\n');
47
+ return [{ role: 'user', content: { type: 'text', text } }];
48
+ },
49
+ },
50
+ ];
@@ -0,0 +1,2 @@
1
+ import type { PromptDescriptor } from './types.js';
2
+ export declare const ldCanvasCoursePrompts: PromptDescriptor[];
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import { instanceLine } from './shared.js';
3
+ import { LD_CANVAS_DESIGN_PRINCIPLES } from '../resources/ldCanvasDesign.js';
4
+ export const ldCanvasCoursePrompts = [
5
+ {
6
+ name: 'create_ld_canvas_course',
7
+ title: 'Create canvas L&D course',
8
+ description: 'Author a well-designed canvas-based L&D course and create it in an instance.',
9
+ argsSchema: {
10
+ instance_id: z.string().optional().describe('Target instance ID (leave blank to resolve interactively)'),
11
+ title: z.string().optional().describe('Course title'),
12
+ vertical: z.string().optional().describe('pharma | medtech | cpg | finserv | generic'),
13
+ topic: z.string().optional().describe('Short description of what the course teaches'),
14
+ },
15
+ build: (args) => {
16
+ const title = args.title || 'a short L&D course';
17
+ const vertical = args.vertical || 'generic';
18
+ const topic = args.topic || 'a general product / process training topic a rep would take';
19
+ const text = [
20
+ LD_CANVAS_DESIGN_PRINCIPLES,
21
+ '',
22
+ '## Before you create',
23
+ 'Confirm the resolved instance is a demo/test instance — these tools create live, ' +
24
+ 'org-visible content. Do not seed against a real customer instance without explicit confirmation.',
25
+ '',
26
+ '## Safe content',
27
+ 'Put any dynamic values through the platform `{{token}}` merge fields (the platform escapes them). ' +
28
+ 'Never inline untrusted or CRM-derived strings as literal HTML in a Text component, and never use ' +
29
+ 'triple-mustache `{{{ }}}`. The title/vertical/topic inputs are descriptive only — do not echo them ' +
30
+ 'verbatim into the generated markup.',
31
+ '',
32
+ '## Task: author a canvas L&D course',
33
+ instanceLine(args.instance_id),
34
+ `Title: ${title}. Vertical: ${vertical}. Topic: ${topic}.`,
35
+ '',
36
+ 'Steps:',
37
+ '1. Build the canvas JSON following the principles above: a canvas OBJECT ' +
38
+ '`{ content: { data: [...] }, theme: { fluid: true } }` whose `content.data` is a flat array of ' +
39
+ 'native components (Grid/GridItem/Text/Multimedia/PageBreak/Embeddable) — band/bookend rhythm, ' +
40
+ 'derived palette, real validated imagery. The components go in `content.data`, NOT a `sections[]` array.',
41
+ '2. Create it by calling `pitcher_ld_course_create_canvas` with ' +
42
+ '`{ instanceId, title, canvas }` — pass the canvas object **inline via `canvas`** ' +
43
+ "(a file path cannot cross a host's sandbox boundary, so always inline). Add `publish: true` " +
44
+ 'only when the user asks to publish.',
45
+ '3. Report the created course id and whether it was published.',
46
+ ].join('\n');
47
+ return [{ role: 'user', content: { type: 'text', text } }];
48
+ },
49
+ },
50
+ ];
@@ -1,4 +1,13 @@
1
1
  import { emailTemplatePrompts } from './emailTemplate.js';
2
2
  import { dsrLandingPrompts } from './dsrLanding.js';
3
+ import { dsrPrompts } from './dsr.js';
4
+ import { announcementsPrompts } from './announcements.js';
5
+ import { ldCanvasCoursePrompts } from './ldCanvasCourse.js';
3
6
  // Every recipe prompt the server can expose. Mirrors mcp/registry.ts (allTools).
4
- export const allPrompts = [...emailTemplatePrompts, ...dsrLandingPrompts];
7
+ export const allPrompts = [
8
+ ...emailTemplatePrompts,
9
+ ...dsrLandingPrompts,
10
+ ...dsrPrompts,
11
+ ...announcementsPrompts,
12
+ ...ldCanvasCoursePrompts,
13
+ ];
@@ -1,2 +1,3 @@
1
1
  export declare function instanceLine(instanceId: string | undefined): string;
2
+ export declare function safetyPreamble(): string;
2
3
  export declare function recipePreamble(): string;
@@ -6,16 +6,16 @@ export function instanceLine(instanceId) {
6
6
  ? `Target instance: ${instanceId}.`
7
7
  : 'No instance was given — call `pitcher_instances_list`, pick the right one with the user, and confirm before creating.';
8
8
  }
9
- // Shared preamble embedded at the top of every recipe prompt. Authored once so the
10
- // design principles, prod-awareness, and anti-XSS guidance can't drift between prompts.
11
- export function recipePreamble() {
9
+ // Prod-awareness + anti-XSS guidance shared by EVERY recipe prompt (HTML-authoring or not).
10
+ // Split out of recipePreamble so non-authoring workflow recipes (e.g. seed_dsr) can carry the
11
+ // safety guidance without the HTML design principles. Contains the strings every prompt is
12
+ // required to include: "demo/test instance", "{{token}}", "{{{ }}}".
13
+ export function safetyPreamble() {
12
14
  return [
13
- CONTENT_DESIGN_PRINCIPLES,
14
- '',
15
15
  '## Before you create',
16
16
  'Confirm the resolved instance is a demo/test instance. These tools create **live, ' +
17
- 'org-visible** content; a DSR landing page is served to **external, unauthenticated** ' +
18
- 'viewers. Do not seed against a real customer instance without explicit confirmation from the user.',
17
+ 'org-visible** content; a DSR is served to **external, unauthenticated** viewers. Do not ' +
18
+ 'seed against a real customer instance without explicit confirmation from the user.',
19
19
  '',
20
20
  '## Safe content',
21
21
  'Put dynamic values through the platform `{{token}}` merge fields (the platform escapes them). ' +
@@ -24,3 +24,8 @@ export function recipePreamble() {
24
24
  'the generated markup.',
25
25
  ].join('\n');
26
26
  }
27
+ // Full preamble for HTML-authoring recipes: the design principles + the safety guidance.
28
+ // Authored once so the principles, prod-awareness, and anti-XSS guidance can't drift.
29
+ export function recipePreamble() {
30
+ return [CONTENT_DESIGN_PRINCIPLES, '', safetyPreamble()].join('\n');
31
+ }
@@ -12,6 +12,7 @@ import { instancesTools } from './tools/instances.js';
12
12
  import { invitationsTools } from './tools/invitations.js';
13
13
  import { membershipsTools } from './tools/memberships.js';
14
14
  import { metadataTemplateTools } from './tools/metadataTemplate.js';
15
+ import { ldTools } from './tools/ld.js';
15
16
  import { orgTools } from './tools/org.js';
16
17
  import { publishAppTools } from './tools/publishApp.js';
17
18
  import { rolesTools } from './tools/roles.js';
@@ -41,6 +42,7 @@ export const allTools = [
41
42
  ...membershipsTools,
42
43
  ...groupsTools,
43
44
  ...metadataTemplateTools,
45
+ ...ldTools,
44
46
  ...devSyncTools,
45
47
  ...publishAppTools,
46
48
  ];
@@ -0,0 +1,3 @@
1
+ import type { ResourceDescriptor } from './types.js';
2
+ export declare const LD_CANVAS_DESIGN_PRINCIPLES = "# Pitcher L&D canvas-course design principles\n\nGuidance for authoring a good canvas-based L&D course body (the JSON passed to\n`pitcher_ld_course_create_canvas` as `canvas`).\n\n## 1. Canvas shape \u2014 content.data is a FLAT array, NOT sections\nA canvas course renders from `canvas.content.data`, a flat array of native components\n(Grid, GridItem, Text, Multimedia, PageBreak, Embeddable). Do NOT use a `sections[]` array \u2014\nit stays empty on working canvases; COURSE mode reads `content.data`. Give components\ntype-prefixed ids (`Grid-<uuid>`, `Text-<uuid>`, \u2026). Set `theme.fluid: true` for full width.\n\n## 2. Native components only\nCompose with Grid / GridItem / Text / Multimedia / PageBreak / Embeddable \u2014 never raw HTML\n`<div>`s. Split logical pages with PageBreak so the host paginates.\n\n## 3. Band / bookend rhythm\nOpen with a branded hero band (navy or brand fill), then alternate white and a faint tint\n(e.g. `#EDF2FB`) bands down the page, and close with a deeper-brand band. Give bands generous\nvertical padding (64-76px). This rhythm reads as \"designed\", not a wall of text.\n\n## 4. Palette \u2014 derive, never hardcode\nDerive tones from the instance primary/theme color: `mix_with_white` for tints (backgrounds),\n`darken` (~0.30) for the closing band and accents. Do not paste brand hex codes; derivation\nkeeps the course on-brand across instances.\n\n## 5. The \"washed-out\" trap\nTheme presets hard-code headings toward black (#000). To get true brand color on headings,\ninject it inline INSIDE the preset paragraph via `<span style=\"color:\u2026;font-weight:700\">\u2026</span>`\n(survives DOMPurify) rather than styling the `<p>` itself.\n\n## 6. Imagery\nUse real, validated image URLs (e.g. Pexels). VALIDATE the cropped URL returns 200 before\nwiring it. Cover images ~1200\u00D7800, banners ~1600\u00D7440.\n\n## 7. Embeddable src must use the CUSTOMER origin\nAny Embeddable `data.src`/`content_url` must point at the customer origin\n(`<org>.my.pitcher.com/storage/...`), never an `api.*` host \u2014 the latter 404s and sets\nX-Frame-Options DENY. Preserve any existing Embeddable verbatim when rebuilding content.data.\n";
3
+ export declare const ldCanvasDesignResource: ResourceDescriptor;
@@ -0,0 +1,51 @@
1
+ // Distilled L&D canvas-course design nucleus (sibling of contentDesign's CONTENT_DESIGN_PRINCIPLES).
2
+ // Both the resource below returns it AND the create_ld_canvas_course prompt embeds it inline
3
+ // (a pitcher:// URI mention is NOT auto-fetched by MCP hosts). Distilled from the CardioLife
4
+ // canvas-course work; keep in sync on material design changes. Last synced: 2026-07-18.
5
+ export const LD_CANVAS_DESIGN_PRINCIPLES = `# Pitcher L&D canvas-course design principles
6
+
7
+ Guidance for authoring a good canvas-based L&D course body (the JSON passed to
8
+ \`pitcher_ld_course_create_canvas\` as \`canvas\`).
9
+
10
+ ## 1. Canvas shape — content.data is a FLAT array, NOT sections
11
+ A canvas course renders from \`canvas.content.data\`, a flat array of native components
12
+ (Grid, GridItem, Text, Multimedia, PageBreak, Embeddable). Do NOT use a \`sections[]\` array —
13
+ it stays empty on working canvases; COURSE mode reads \`content.data\`. Give components
14
+ type-prefixed ids (\`Grid-<uuid>\`, \`Text-<uuid>\`, …). Set \`theme.fluid: true\` for full width.
15
+
16
+ ## 2. Native components only
17
+ Compose with Grid / GridItem / Text / Multimedia / PageBreak / Embeddable — never raw HTML
18
+ \`<div>\`s. Split logical pages with PageBreak so the host paginates.
19
+
20
+ ## 3. Band / bookend rhythm
21
+ Open with a branded hero band (navy or brand fill), then alternate white and a faint tint
22
+ (e.g. \`#EDF2FB\`) bands down the page, and close with a deeper-brand band. Give bands generous
23
+ vertical padding (64-76px). This rhythm reads as "designed", not a wall of text.
24
+
25
+ ## 4. Palette — derive, never hardcode
26
+ Derive tones from the instance primary/theme color: \`mix_with_white\` for tints (backgrounds),
27
+ \`darken\` (~0.30) for the closing band and accents. Do not paste brand hex codes; derivation
28
+ keeps the course on-brand across instances.
29
+
30
+ ## 5. The "washed-out" trap
31
+ Theme presets hard-code headings toward black (#000). To get true brand color on headings,
32
+ inject it inline INSIDE the preset paragraph via \`<span style="color:…;font-weight:700">…</span>\`
33
+ (survives DOMPurify) rather than styling the \`<p>\` itself.
34
+
35
+ ## 6. Imagery
36
+ Use real, validated image URLs (e.g. Pexels). VALIDATE the cropped URL returns 200 before
37
+ wiring it. Cover images ~1200×800, banners ~1600×440.
38
+
39
+ ## 7. Embeddable src must use the CUSTOMER origin
40
+ Any Embeddable \`data.src\`/\`content_url\` must point at the customer origin
41
+ (\`<org>.my.pitcher.com/storage/...\`), never an \`api.*\` host — the latter 404s and sets
42
+ X-Frame-Options DENY. Preserve any existing Embeddable verbatim when rebuilding content.data.
43
+ `;
44
+ export const ldCanvasDesignResource = {
45
+ name: 'ld-canvas-course',
46
+ uri: 'pitcher://design/ld-canvas-course',
47
+ title: 'L&D canvas-course design principles',
48
+ description: 'How to author a well-designed canvas-based L&D course body (content.data, bands, palette).',
49
+ mimeType: 'text/markdown',
50
+ read: () => LD_CANVAS_DESIGN_PRINCIPLES,
51
+ };
@@ -1,3 +1,4 @@
1
1
  import { contentDesignResource } from './contentDesign.js';
2
+ import { ldCanvasDesignResource } from './ldCanvasDesign.js';
2
3
  // Every resource the server can expose. Mirrors mcp/registry.ts (allTools).
3
- export const allResources = [contentDesignResource];
4
+ export const allResources = [contentDesignResource, ldCanvasDesignResource];
@@ -0,0 +1,2 @@
1
+ import type { ToolDescriptor } from './types.js';
2
+ export declare const ldTools: ToolDescriptor[];