@pitcher/cli-scripts 2.11.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.
@@ -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 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
+ ];
@@ -2,5 +2,12 @@ import { emailTemplatePrompts } from './emailTemplate.js';
2
2
  import { dsrLandingPrompts } from './dsrLanding.js';
3
3
  import { dsrPrompts } from './dsr.js';
4
4
  import { announcementsPrompts } from './announcements.js';
5
+ import { ldCanvasCoursePrompts } from './ldCanvasCourse.js';
5
6
  // Every recipe prompt the server can expose. Mirrors mcp/registry.ts (allTools).
6
- export const allPrompts = [...emailTemplatePrompts, ...dsrLandingPrompts, ...dsrPrompts, ...announcementsPrompts];
7
+ export const allPrompts = [
8
+ ...emailTemplatePrompts,
9
+ ...dsrLandingPrompts,
10
+ ...dsrPrompts,
11
+ ...announcementsPrompts,
12
+ ...ldCanvasCoursePrompts,
13
+ ];
@@ -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[];