@pitcher/cli-scripts 2.7.1 → 2.9.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 (65) hide show
  1. package/README.md +59 -0
  2. package/dist/index.js +4 -0
  3. package/dist/src/commands/dsr.d.ts +36 -0
  4. package/dist/src/commands/dsr.js +229 -0
  5. package/dist/src/commands/mcp.d.ts +2 -0
  6. package/dist/src/commands/mcp.js +18 -0
  7. package/dist/src/commands/scaffolds.d.ts +1 -0
  8. package/dist/src/commands/scaffolds.js +24 -0
  9. package/dist/src/mcp/env.d.ts +7 -0
  10. package/dist/src/mcp/env.js +13 -0
  11. package/dist/src/mcp/registry.d.ts +2 -0
  12. package/dist/src/mcp/registry.js +46 -0
  13. package/dist/src/mcp/result.d.ts +13 -0
  14. package/dist/src/mcp/result.js +20 -0
  15. package/dist/src/mcp/serve.d.ts +1 -0
  16. package/dist/src/mcp/serve.js +13 -0
  17. package/dist/src/mcp/server.d.ts +3 -0
  18. package/dist/src/mcp/server.js +41 -0
  19. package/dist/src/mcp/tools/analytics.d.ts +2 -0
  20. package/dist/src/mcp/tools/analytics.js +20 -0
  21. package/dist/src/mcp/tools/announcements.d.ts +2 -0
  22. package/dist/src/mcp/tools/announcements.js +32 -0
  23. package/dist/src/mcp/tools/canvases.d.ts +2 -0
  24. package/dist/src/mcp/tools/canvases.js +30 -0
  25. package/dist/src/mcp/tools/devSync.d.ts +2 -0
  26. package/dist/src/mcp/tools/devSync.js +53 -0
  27. package/dist/src/mcp/tools/dsr.d.ts +2 -0
  28. package/dist/src/mcp/tools/dsr.js +47 -0
  29. package/dist/src/mcp/tools/dsrLanding.d.ts +2 -0
  30. package/dist/src/mcp/tools/dsrLanding.js +45 -0
  31. package/dist/src/mcp/tools/emailTemplate.d.ts +2 -0
  32. package/dist/src/mcp/tools/emailTemplate.js +47 -0
  33. package/dist/src/mcp/tools/files.d.ts +2 -0
  34. package/dist/src/mcp/tools/files.js +65 -0
  35. package/dist/src/mcp/tools/folders.d.ts +2 -0
  36. package/dist/src/mcp/tools/folders.js +56 -0
  37. package/dist/src/mcp/tools/groups.d.ts +2 -0
  38. package/dist/src/mcp/tools/groups.js +60 -0
  39. package/dist/src/mcp/tools/instances.d.ts +2 -0
  40. package/dist/src/mcp/tools/instances.js +28 -0
  41. package/dist/src/mcp/tools/invitations.d.ts +2 -0
  42. package/dist/src/mcp/tools/invitations.js +23 -0
  43. package/dist/src/mcp/tools/memberships.d.ts +2 -0
  44. package/dist/src/mcp/tools/memberships.js +47 -0
  45. package/dist/src/mcp/tools/metadataTemplate.d.ts +2 -0
  46. package/dist/src/mcp/tools/metadataTemplate.js +42 -0
  47. package/dist/src/mcp/tools/org.d.ts +2 -0
  48. package/dist/src/mcp/tools/org.js +12 -0
  49. package/dist/src/mcp/tools/publishApp.d.ts +2 -0
  50. package/dist/src/mcp/tools/publishApp.js +39 -0
  51. package/dist/src/mcp/tools/roles.d.ts +2 -0
  52. package/dist/src/mcp/tools/roles.js +15 -0
  53. package/dist/src/mcp/tools/search.d.ts +2 -0
  54. package/dist/src/mcp/tools/search.js +16 -0
  55. package/dist/src/mcp/tools/sharedLinks.d.ts +2 -0
  56. package/dist/src/mcp/tools/sharedLinks.js +27 -0
  57. package/dist/src/mcp/tools/tags.d.ts +2 -0
  58. package/dist/src/mcp/tools/tags.js +20 -0
  59. package/dist/src/mcp/tools/themes.d.ts +2 -0
  60. package/dist/src/mcp/tools/themes.js +28 -0
  61. package/dist/src/mcp/tools/types.d.ts +15 -0
  62. package/dist/src/mcp/tools/types.js +1 -0
  63. package/dist/src/mcp/tools/users.d.ts +2 -0
  64. package/dist/src/mcp/tools/users.js +31 -0
  65. package/package.json +4 -2
package/README.md CHANGED
@@ -10,6 +10,43 @@ p --help
10
10
  p command --help
11
11
  ```
12
12
 
13
+ ## MCP server
14
+
15
+ Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps 50 tools, one per CLI `run*` function: every command above (including `dsr create` + `dsr bind-landing`) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
16
+
17
+ ```bash
18
+ p mcp
19
+ # or without a global install
20
+ npx @pitcher/cli-scripts mcp
21
+ ```
22
+
23
+ ### Environment variables
24
+
25
+ Auth and org scope are resolved once at boot from environment variables. They are never tool inputs, so the model cannot change org or credentials mid-session.
26
+
27
+ - `NG_API_KEY` (required, secret): the API key the server authenticates as.
28
+ - `NG_ORG` (default `dev`): organization subdomain, or set `NG_URL` directly for a full origin.
29
+ - `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below). Defaults to a guess based on `NG_ORG` when unset.
30
+
31
+ One config entry = one org, because the API key is org-specific. To work against multiple orgs, add multiple entries (e.g. `pitcher-dev`, `pitcher-prod`) with different env blocks; do not try to pass org per tool call.
32
+
33
+ ### Host config example (Claude Desktop / `.mcp.json`)
34
+
35
+ ```jsonc
36
+ "pitcher": {
37
+ "command": "npx",
38
+ "args": ["-y", "@pitcher/cli-scripts", "mcp"],
39
+ "env": { "NG_API_KEY": "<key>", "NG_ORG": "dev" }
40
+ }
41
+ ```
42
+
43
+ ### Tool annotations
44
+
45
+ - Read commands are marked `readOnlyHint`. `dev-sync` counts as a write (it mutates remote dev-override state).
46
+ - `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or `NG_ORG` other than `dev` with `NG_ENV` unset). This steers hosts toward requiring extra confirmation before mutating a production org.
47
+
48
+ This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
49
+
13
50
  ## To run the `publish-app` command:
14
51
 
15
52
  ```bash
@@ -414,3 +451,25 @@ p metadata-template upsert --instance-id 01... --sections-file ./schema.json --a
414
451
  ```
415
452
 
416
453
  `upsert` merges the incoming sections by section `label` — a section whose label matches an existing one is replaced, other sections are preserved. It **cannot remove** a section or field (that's a UI operation). `options` for `select`/`multi-select` fields are `{ label, value }` objects (bare strings are rejected). `upsert` requires an instance- or org-admin key; a non-admin member key is rejected (400 → `VALIDATION`).
454
+
455
+ ## To run the `dsr` command:
456
+
457
+ Create and share a Digital Sales Room (a canvas share link) and get a link back in one command. Three subcommands: `create` (build + share, with optional landing-page binding), `bind-landing` (bind/unbind a landing template on an existing share), and `scaffold` (print a starter `{source,security,landing}` spec for `--config-file`).
458
+
459
+ ```bash
460
+ # simplest — email-gated link (server default auth_strategy=any_email)
461
+ p dsr create --instance-id 01... --canvas-id 01... --api-key myapikey
462
+ # open link
463
+ p dsr create --instance-id 01... --canvas-id 01... --auth-strategy none
464
+ # ShareBox + security + branded landing
465
+ p dsr create --instance-id 01... --sharebox-file-ids 01A,01B --account-id 01... \
466
+ --auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-09-01T00:00:00Z \
467
+ --landing-template-id 01... --landing-context '{"rep":"Sam"}'
468
+ # bind/unbind a landing template on an existing share
469
+ p dsr bind-landing --id 01... --landing-template-id 01...
470
+ p dsr bind-landing --id 01... --landing-template-id none
471
+ # scaffold a spec for --config-file
472
+ p dsr scaffold --out share.json
473
+ ```
474
+
475
+ Creating needs a share-capable key; **binding a landing page currently needs an editor key** (PIT-7525 will open it to reps for their own DSRs) — a denied bind returns the link with a warning. Discover landing template IDs via `p dsr-landing list`.
package/dist/index.js CHANGED
@@ -24,6 +24,8 @@ import { createMembershipsCommand } from './src/commands/memberships.js';
24
24
  import { createGroupsCommand } from './src/commands/groups.js';
25
25
  import { createRolesCommand } from './src/commands/roles.js';
26
26
  import { createMetadataTemplateCommand } from './src/commands/metadataTemplate.js';
27
+ import { createDsrCommand } from './src/commands/dsr.js';
28
+ import { createMcpCommand } from './src/commands/mcp.js';
27
29
  const __filename = fileURLToPath(import.meta.url);
28
30
  const __dirname = dirname(__filename);
29
31
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -53,6 +55,8 @@ program.addCommand(createMembershipsCommand());
53
55
  program.addCommand(createGroupsCommand());
54
56
  program.addCommand(createRolesCommand());
55
57
  program.addCommand(createMetadataTemplateCommand());
58
+ program.addCommand(createDsrCommand());
56
59
  program.addCommand(createOrgCommand());
57
60
  program.addCommand(createAnalyticsCommand());
61
+ program.addCommand(createMcpCommand());
58
62
  program.parseAsync(process.argv);
@@ -0,0 +1,36 @@
1
+ import { Command } from 'commander';
2
+ import type { AxiosInstance } from 'axios';
3
+ import { type CommonOpts } from '../core/http.js';
4
+ import { type Envelope } from '../core/envelope.js';
5
+ export interface DsrDeps {
6
+ client?: AxiosInstance;
7
+ }
8
+ export declare function runDsrScaffold(opts: CommonOpts & {
9
+ out?: string;
10
+ }, _deps?: DsrDeps): Promise<Envelope<unknown>>;
11
+ export declare function runDsrBindLanding(opts: CommonOpts & {
12
+ id?: string;
13
+ landingTemplateId?: string;
14
+ landingContext?: string;
15
+ instanceId?: string;
16
+ }, deps?: DsrDeps): Promise<Envelope<unknown>>;
17
+ export interface DsrCreateOpts extends CommonOpts {
18
+ instanceId?: string;
19
+ canvasId?: string;
20
+ shareboxFileIds?: string[];
21
+ canvasName?: string;
22
+ accountId?: string;
23
+ templateId?: string;
24
+ authStrategy?: string;
25
+ allowedEmails?: string[];
26
+ allowedDomains?: string[];
27
+ validUntil?: string;
28
+ allowForward?: boolean;
29
+ allowDownload?: boolean;
30
+ tags?: string[];
31
+ landingTemplateId?: string;
32
+ landingContext?: string;
33
+ configFile?: string;
34
+ }
35
+ export declare function runDsrCreate(opts: DsrCreateOpts, deps?: DsrDeps): Promise<Envelope<unknown>>;
36
+ export declare function createDsrCommand(): Command;
@@ -0,0 +1,229 @@
1
+ import { Command } from 'commander';
2
+ import { clientFor, toCliError } from '../core/http.js';
3
+ import { ok, fail } from '../core/envelope.js';
4
+ import { createCliError } from '../core/errors.js';
5
+ import { emitEnvelope } from '../core/output.js';
6
+ import { parseJsonObject, parseTags, isPlainObject } from '../schema/content.js';
7
+ import { readTextFile } from '../core/readInput.js';
8
+ import { withAuthFlags } from './withAuth.js';
9
+ import { emitScaffold, DSR_SHARE_SCAFFOLD_JSON } from './scaffolds.js';
10
+ const ENDPOINT = '/core/api/protected/shared-links';
11
+ export async function runDsrScaffold(opts, _deps = {}) {
12
+ return emitScaffold(DSR_SHARE_SCAFFOLD_JSON, opts.out);
13
+ }
14
+ // resolve --landing-template-id text into the PUT value: undefined = not given; '' / 'none' = unbind (null); else the id
15
+ function resolveTemplateId(raw) {
16
+ if (raw === undefined)
17
+ return undefined;
18
+ if (raw === '' || raw.toLowerCase() === 'none')
19
+ return null;
20
+ return raw;
21
+ }
22
+ async function bindLandingRequest(client, id, body, instanceId) {
23
+ const url = `${ENDPOINT}/${encodeURIComponent(id)}/landing-page`;
24
+ const res = await client.put(url, body, instanceId ? { params: { instance_id: instanceId } } : undefined);
25
+ return res.data;
26
+ }
27
+ export async function runDsrBindLanding(opts, deps = {}) {
28
+ try {
29
+ if (!opts.id)
30
+ throw createCliError('VALIDATION', '--id is required for dsr bind-landing');
31
+ const templateId = resolveTemplateId(opts.landingTemplateId);
32
+ if (templateId === undefined)
33
+ throw createCliError('VALIDATION', '--landing-template-id is required (use "none" to unbind)');
34
+ const body = { landing_page_template_id: templateId };
35
+ if (opts.landingContext !== undefined)
36
+ body.landing_page_context = parseJsonObject(opts.landingContext, 'landing-context');
37
+ const client = clientFor(opts, deps);
38
+ const data = await bindLandingRequest(client, opts.id, body, opts.instanceId);
39
+ return ok(data);
40
+ }
41
+ catch (e) {
42
+ return fail(toCliError(e));
43
+ }
44
+ }
45
+ const AUTH_STRATEGIES = [
46
+ 'none', 'any_email', 'specified_emails', 'specified_domains', 'verified_emails', 'verified_domains',
47
+ ];
48
+ const SPEC_FLAG_KEYS = [
49
+ 'canvasId', 'shareboxFileIds', 'canvasName', 'accountId', 'templateId', 'authStrategy', 'allowedEmails',
50
+ 'allowedDomains', 'validUntil', 'allowForward', 'allowDownload', 'tags', 'landingTemplateId', 'landingContext',
51
+ ];
52
+ // config-file blocks must be JSON objects — a bare cast would silently spread a string/array into the request body
53
+ function requirePlainObjectField(v, field) {
54
+ if (v === undefined)
55
+ return {};
56
+ if (!isPlainObject(v))
57
+ throw createCliError('VALIDATION', `config "${field}" must be a JSON object`);
58
+ return v;
59
+ }
60
+ function assembleSpec(opts) {
61
+ if (opts.configFile !== undefined) {
62
+ if (SPEC_FLAG_KEYS.some((k) => opts[k] !== undefined))
63
+ throw createCliError('VALIDATION', 'pass individual flags or --config-file, not both');
64
+ const cfg = parseJsonObject(readTextFile(opts.configFile), 'config');
65
+ const source = requirePlainObjectField(cfg.source, 'source');
66
+ const security = requirePlainObjectField(cfg.security, 'security');
67
+ // JSON null is a valid "no landing" opt-out, same as omitting the block
68
+ const landing = cfg.landing == null ? null : requirePlainObjectField(cfg.landing, 'landing');
69
+ const body = { ...source, ...security };
70
+ if (opts.instanceId !== undefined)
71
+ body.instance_id = opts.instanceId;
72
+ return { body, landing };
73
+ }
74
+ const body = {};
75
+ if (opts.instanceId !== undefined)
76
+ body.instance_id = opts.instanceId;
77
+ if (opts.canvasId !== undefined)
78
+ body.canvas_id = opts.canvasId;
79
+ if (opts.shareboxFileIds !== undefined)
80
+ body.sharebox_items = opts.shareboxFileIds.map((file_id) => ({ file_id }));
81
+ if (opts.canvasName !== undefined)
82
+ body.canvas_name = opts.canvasName;
83
+ if (opts.accountId !== undefined)
84
+ body.account_id = opts.accountId;
85
+ if (opts.templateId !== undefined)
86
+ body.template_id = opts.templateId;
87
+ if (opts.authStrategy !== undefined)
88
+ body.auth_strategy = opts.authStrategy;
89
+ if (opts.allowedEmails !== undefined)
90
+ body.allowed_emails = opts.allowedEmails;
91
+ if (opts.allowedDomains !== undefined)
92
+ body.allowed_domains = opts.allowedDomains;
93
+ if (opts.validUntil !== undefined)
94
+ body.valid_until = opts.validUntil;
95
+ if (opts.allowForward !== undefined)
96
+ body.allow_forward = opts.allowForward;
97
+ if (opts.allowDownload !== undefined)
98
+ body.allow_download = opts.allowDownload;
99
+ if (opts.tags !== undefined)
100
+ body.tags = opts.tags;
101
+ return { body, landing: null };
102
+ }
103
+ function assertCreateBody(body, instanceId) {
104
+ const hasCanvas = body.canvas_id !== undefined && body.canvas_id !== '';
105
+ const items = body.sharebox_items;
106
+ const hasSharebox = Array.isArray(items) && items.length > 0;
107
+ if (hasCanvas === hasSharebox)
108
+ throw createCliError('VALIDATION', 'pass exactly one source: --canvas-id or --sharebox-file-ids');
109
+ if (hasSharebox && !instanceId && body.instance_id === undefined)
110
+ throw createCliError('VALIDATION', '--instance-id is required for --sharebox-file-ids');
111
+ const strat = body.auth_strategy;
112
+ if (strat !== undefined) {
113
+ if (!AUTH_STRATEGIES.includes(strat))
114
+ throw createCliError('VALIDATION', `--auth-strategy must be one of: ${AUTH_STRATEGIES.join(', ')}`);
115
+ const emails = body.allowed_emails;
116
+ const domains = body.allowed_domains;
117
+ if ((strat === 'specified_emails' || strat === 'verified_emails') && !(emails && emails.length))
118
+ throw createCliError('VALIDATION', `--allowed-emails is required for --auth-strategy ${strat}`);
119
+ if ((strat === 'specified_domains' || strat === 'verified_domains') && !(domains && domains.length))
120
+ throw createCliError('VALIDATION', `--allowed-domains is required for --auth-strategy ${strat}`);
121
+ }
122
+ }
123
+ // binding a landing page on create needs a real template id — unlike bind-landing, there is nothing to unbind on a
124
+ // brand-new share, so 'none'/''/null/missing (and a non-string config value) are all rejected as VALIDATION
125
+ function requireCreateTemplateId(tid) {
126
+ const resolved = typeof tid === 'string' ? resolveTemplateId(tid) : tid;
127
+ if (typeof resolved !== 'string')
128
+ throw createCliError('VALIDATION', 'a landing template id is required to bind a landing page on create (use `dsr bind-landing` to unbind an existing share)');
129
+ return resolved;
130
+ }
131
+ // derive the landing PUT body from flags or from the config-file landing block; null if none requested
132
+ function resolveLandingBody(opts, cfgLanding) {
133
+ if (opts.configFile !== undefined) {
134
+ if (!cfgLanding)
135
+ return null;
136
+ const tid = cfgLanding.landing_page_template_id;
137
+ const ctx = cfgLanding.landing_page_context;
138
+ // a blank/null template id with no (or empty) context = the untouched `dsr scaffold` default → no landing bind
139
+ const templateBlank = tid === undefined || tid === null || tid === '' || (typeof tid === 'string' && tid.toLowerCase() === 'none');
140
+ const contextEmpty = ctx === undefined || (isPlainObject(ctx) && Object.keys(ctx).length === 0);
141
+ if (templateBlank && contextEmpty)
142
+ return null;
143
+ const b = { landing_page_template_id: requireCreateTemplateId(tid) };
144
+ if (ctx !== undefined) {
145
+ if (!isPlainObject(ctx))
146
+ throw createCliError('VALIDATION', 'landing.landing_page_context must be a JSON object');
147
+ if (Object.keys(ctx).length > 0)
148
+ b.landing_page_context = ctx;
149
+ }
150
+ return b;
151
+ }
152
+ if (opts.landingTemplateId === undefined && opts.landingContext === undefined)
153
+ return null;
154
+ const b = { landing_page_template_id: requireCreateTemplateId(opts.landingTemplateId) };
155
+ if (opts.landingContext !== undefined)
156
+ b.landing_page_context = parseJsonObject(opts.landingContext, 'landing-context');
157
+ return b;
158
+ }
159
+ export async function runDsrCreate(opts, deps = {}) {
160
+ try {
161
+ const { body, landing: cfgLanding } = assembleSpec(opts);
162
+ assertCreateBody(body, opts.instanceId);
163
+ const landingBody = resolveLandingBody(opts, cfgLanding); // parse/validate context BEFORE the network call
164
+ // bind against the same instance the share was created in — a config-file source.instance_id must reach the PUT too
165
+ const effectiveInstanceId = body.instance_id ?? opts.instanceId;
166
+ const client = clientFor(opts, deps);
167
+ const res = await client.post(ENDPOINT, body);
168
+ const share = res.data;
169
+ if (!landingBody)
170
+ return ok(share);
171
+ const shareId = share?.id;
172
+ if (!shareId)
173
+ return ok({ ...share, landing: null }, ['landing not attached: no share id in create response']);
174
+ try {
175
+ const landing = await bindLandingRequest(client, shareId, landingBody, effectiveInstanceId);
176
+ return ok({ ...share, landing });
177
+ }
178
+ catch (e) {
179
+ return ok({ ...share, landing: null }, [`landing not attached: ${toCliError(e).message}`]);
180
+ }
181
+ }
182
+ catch (e) {
183
+ return fail(toCliError(e));
184
+ }
185
+ }
186
+ export function createDsrCommand() {
187
+ const cmd = new Command('dsr').description('create and share a Digital Sales Room (a canvas share link)');
188
+ withAuthFlags(cmd.command('scaffold'))
189
+ .description('print a {source,security,landing} share spec to fill in and pass to `dsr create --config-file`')
190
+ .option('--out <path>', 'Write the scaffold to a file instead of stdout')
191
+ .action(async (options, command) => {
192
+ const globals = command.optsWithGlobals();
193
+ emitEnvelope(await runDsrScaffold({ ...options, quiet: globals.quiet }), globals);
194
+ });
195
+ withAuthFlags(cmd.command('create'))
196
+ .description('create + share a DSR (canvas share link) and return the link')
197
+ .option('--instance-id <id>', 'Instance ID (required for --sharebox-file-ids)')
198
+ .option('--canvas-id <id>', 'Existing canvas to share (one source required)')
199
+ .option('--sharebox-file-ids <csv>', 'Build a canvas from these file IDs (one source required)', parseTags)
200
+ .option('--canvas-name <name>', 'Name for the ShareBox-built canvas')
201
+ .option('--account-id <id>', 'Account for the ShareBox-built canvas')
202
+ .option('--template-id <id>', 'Canvas template for the ShareBox-built canvas')
203
+ .option('--auth-strategy <v>', 'none|any_email|specified_emails|specified_domains|verified_emails|verified_domains')
204
+ .option('--allowed-emails <csv>', 'Allowed emails (required for specified/verified_emails)', parseTags)
205
+ .option('--allowed-domains <csv>', 'Allowed domains (required for specified/verified_domains)', parseTags)
206
+ .option('--valid-until <iso>', 'Expiry (ISO datetime)')
207
+ .option('--allow-forward', 'Allow recipients to forward')
208
+ .option('--no-allow-forward', 'Disallow forwarding')
209
+ .option('--allow-download', 'Allow download')
210
+ .option('--tags <csv>', 'Tags', parseTags)
211
+ .option('--landing-template-id <id>', 'Landing template to bind (optional)')
212
+ .option('--landing-context <json>', 'Landing personalization merge fields (JSON object)')
213
+ .option('--config-file <path>', 'Full {source,security,landing} spec JSON (mutually exclusive with the flags above)')
214
+ .action(async (options, command) => {
215
+ const globals = command.optsWithGlobals();
216
+ emitEnvelope(await runDsrCreate({ ...options, quiet: globals.quiet }), globals);
217
+ });
218
+ withAuthFlags(cmd.command('bind-landing'))
219
+ .description("bind (or unbind with 'none') a landing-page template on an existing share")
220
+ .option('--id <id>', 'Shared link ID (required)')
221
+ .option('--landing-template-id <id>', "Landing template ID, or 'none' to unbind (required)")
222
+ .option('--landing-context <json>', 'Personalization merge fields (JSON object)')
223
+ .option('--instance-id <id>', 'Instance ID (optional)')
224
+ .action(async (options, command) => {
225
+ const globals = command.optsWithGlobals();
226
+ emitEnvelope(await runDsrBindLanding({ ...options, quiet: globals.quiet }), globals);
227
+ });
228
+ return cmd;
229
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createMcpCommand(): Command;
@@ -0,0 +1,18 @@
1
+ import { Command } from 'commander';
2
+ import { runMcpServe } from '../mcp/serve.js';
3
+ import { isCliError } from '../core/errors.js';
4
+ export function createMcpCommand() {
5
+ return new Command('mcp')
6
+ .description('Run the Pitcher MCP server over stdio (reads NG_API_KEY, NG_ORG/NG_URL, NG_ENV from env)')
7
+ .action(async () => {
8
+ try {
9
+ await runMcpServe();
10
+ }
11
+ catch (e) {
12
+ const message = isCliError(e) ? e.message : e instanceof Error ? e.message : String(e);
13
+ console.error(`[pitcher-mcp] failed to start: ${message}`);
14
+ process.exitCode = isCliError(e) ? e.exitCode : 1;
15
+ return;
16
+ }
17
+ });
18
+ }
@@ -18,3 +18,4 @@ export declare const EMAIL_SCAFFOLD_HTML = "<!doctype html>\n<html lang=\"en\">\
18
18
  export declare const DSR_SCAFFOLD_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Sample DSR Landing Page</title>\n </head>\n <body style=\"margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif;color:#212121;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding:32px 0;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" width=\"640\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#ffffff;border-radius:8px;overflow:hidden;\">\n <tr>\n <td style=\"background-color:var(--p-primary, #29335C);padding:28px;text-align:center;\">\n <!-- token: organization.name -> the sharing org's display name -->\n <span style=\"color:#ffffff;font-size:20px;font-weight:bold;\">{{organization.name}}</span>\n </td>\n </tr>\n <tr>\n <td style=\"padding:32px;\">\n <!-- token: recipient.firstName -> the recipient's first name -->\n <!-- token: customer.name -> the recipient's company name -->\n <h1 style=\"margin:0 0 8px 0;font-size:22px;\">Welcome, {{recipient.firstName}}</h1>\n <p style=\"margin:0 0 24px 0;color:#757575;font-size:14px;\">Prepared for {{customer.name}}</p>\n\n <!--\n Editable region: admins can edit this block's text in the DSR\n landing editor without touching markup. data-field-char-limit\n enforces a max length in that editor.\n -->\n <div\n data-field-id=\"welcome-message\"\n data-field-editable=\"true\"\n data-field-char-limit=\"120\"\n style=\"padding:16px;margin:0 0 24px 0;border-left:3px solid var(--p-primary, #29335C);background-color:#f7f8fb;font-size:15px;line-height:1.5;\"\n >\n Thanks for stopping by. Everything you need for this conversation is right here.\n </div>\n\n <!-- token: rep.name -> the sharing rep's display name -->\n <p style=\"margin:0 0 24px 0;font-size:14px;color:#757575;\">Shared by {{rep.name}}</p>\n\n <!--\n Required marker: the platform injects the email-gate form\n into this div at render time. It MUST be present verbatim\n (a literal data-pitcher-email-gate attribute) or the\n server rejects the template on create/update.\n -->\n <div data-pitcher-email-gate></div>\n </td>\n </tr>\n <tr>\n <td style=\"padding:16px 32px;border-top:1px solid #e0e2ed;text-align:center;color:#757575;font-size:12px;\">\n <!-- token: sharedLink.valid_until -> when this share expires, if the share sets an expiry -->\n This link is valid until {{sharedLink.valid_until}}.\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>\n";
19
19
  export declare const ANNOUNCEMENT_SCAFFOLD_JSON: string;
20
20
  export declare const METADATA_TEMPLATE_SCAFFOLD_JSON: string;
21
+ export declare const DSR_SHARE_SCAFFOLD_JSON: string;
@@ -269,3 +269,27 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
269
269
  },
270
270
  ];
271
271
  export const METADATA_TEMPLATE_SCAFFOLD_JSON = JSON.stringify(METADATA_TEMPLATE_SCAFFOLD_DATA, null, 2);
272
+ const DSR_SHARE_SCAFFOLD_DATA = {
273
+ source: {
274
+ // provide ONE of the following:
275
+ canvas_id: '01EXAMPLECANVASID000000000',
276
+ // sharebox_items: [{ file_id: '01EXAMPLEFILEID0000000000', page_index: 0 }],
277
+ // canvas_name: 'Acme QBR',
278
+ // account_id: '01EXAMPLEACCOUNTID00000000',
279
+ // template_id: '01EXAMPLECANVASTEMPLATE0000',
280
+ },
281
+ security: {
282
+ auth_strategy: 'any_email', // none|any_email|specified_emails|specified_domains|verified_emails|verified_domains
283
+ allowed_emails: [],
284
+ allowed_domains: [],
285
+ valid_until: null,
286
+ allow_forward: true,
287
+ allow_download: false,
288
+ tags: [],
289
+ },
290
+ landing: {
291
+ landing_page_template_id: null, // discover ids via `p dsr-landing list --instance-id <id>`
292
+ landing_page_context: {},
293
+ },
294
+ };
295
+ export const DSR_SHARE_SCAFFOLD_JSON = JSON.stringify(DSR_SHARE_SCAFFOLD_DATA, null, 2);
@@ -0,0 +1,7 @@
1
+ export interface McpEnv {
2
+ apiKey: string;
3
+ org?: string;
4
+ url?: string;
5
+ prod: boolean;
6
+ }
7
+ export declare function resolveMcpEnv(env?: NodeJS.ProcessEnv): McpEnv;
@@ -0,0 +1,13 @@
1
+ import { createCliError } from '../core/errors.js';
2
+ export function resolveMcpEnv(env = process.env) {
3
+ const apiKey = env.NG_API_KEY?.trim();
4
+ if (!apiKey) {
5
+ throw createCliError('AUTH', 'NG_API_KEY is required to run the MCP server (set it in the host config env).');
6
+ }
7
+ const org = env.NG_ORG?.trim() || 'dev';
8
+ const url = env.NG_URL?.trim() || undefined;
9
+ const ngEnv = env.NG_ENV?.trim();
10
+ const isDevHost = url ? /\/\/dev\.my\.pitcher\.com(\/|$)/.test(url) : org === 'dev';
11
+ const prod = ngEnv === 'prod' ? true : ngEnv === 'test' ? false : !isDevHost;
12
+ return { apiKey, org, url, prod };
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDescriptor } from './tools/types.js';
2
+ export declare const allTools: ToolDescriptor[];
@@ -0,0 +1,46 @@
1
+ import { analyticsTools } from './tools/analytics.js';
2
+ import { announcementsTools } from './tools/announcements.js';
3
+ import { canvasesTools } from './tools/canvases.js';
4
+ import { devSyncTools } from './tools/devSync.js';
5
+ import { dsrTools } from './tools/dsr.js';
6
+ import { dsrLandingTools } from './tools/dsrLanding.js';
7
+ import { emailTemplateTools } from './tools/emailTemplate.js';
8
+ import { filesTools } from './tools/files.js';
9
+ import { foldersTools } from './tools/folders.js';
10
+ import { groupsTools } from './tools/groups.js';
11
+ import { instancesTools } from './tools/instances.js';
12
+ import { invitationsTools } from './tools/invitations.js';
13
+ import { membershipsTools } from './tools/memberships.js';
14
+ import { metadataTemplateTools } from './tools/metadataTemplate.js';
15
+ import { orgTools } from './tools/org.js';
16
+ import { publishAppTools } from './tools/publishApp.js';
17
+ import { rolesTools } from './tools/roles.js';
18
+ import { searchTools } from './tools/search.js';
19
+ import { sharedLinksTools } from './tools/sharedLinks.js';
20
+ import { tagsTools } from './tools/tags.js';
21
+ import { themesTools } from './tools/themes.js';
22
+ import { usersTools } from './tools/users.js';
23
+ export const allTools = [
24
+ ...canvasesTools,
25
+ ...filesTools,
26
+ ...foldersTools,
27
+ ...searchTools,
28
+ ...instancesTools,
29
+ ...sharedLinksTools,
30
+ ...tagsTools,
31
+ ...announcementsTools,
32
+ ...dsrLandingTools,
33
+ ...dsrTools,
34
+ ...emailTemplateTools,
35
+ ...themesTools,
36
+ ...usersTools,
37
+ ...invitationsTools,
38
+ ...rolesTools,
39
+ ...orgTools,
40
+ ...analyticsTools,
41
+ ...membershipsTools,
42
+ ...groupsTools,
43
+ ...metadataTemplateTools,
44
+ ...devSyncTools,
45
+ ...publishAppTools,
46
+ ];
@@ -0,0 +1,13 @@
1
+ import type { Envelope } from '../core/envelope.js';
2
+ export interface ToolTextContent {
3
+ type: 'text';
4
+ text: string;
5
+ }
6
+ export interface CallToolResultShape {
7
+ content: ToolTextContent[];
8
+ structuredContent?: Record<string, unknown>;
9
+ isError: boolean;
10
+ [key: string]: unknown;
11
+ }
12
+ export declare function toErrorResult(code: string, message: string): CallToolResultShape;
13
+ export declare function envelopeToToolResult(env: Envelope<unknown>): CallToolResultShape;
@@ -0,0 +1,20 @@
1
+ function isPlainObject(v) {
2
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
3
+ }
4
+ export function toErrorResult(code, message) {
5
+ return { content: [{ type: 'text', text: `${code}: ${message}` }], isError: true };
6
+ }
7
+ export function envelopeToToolResult(env) {
8
+ if (!env.ok) {
9
+ return toErrorResult(env.error?.code ?? 'UNKNOWN', env.error?.message ?? 'unknown error');
10
+ }
11
+ const text = env.data === undefined ? 'null' : JSON.stringify(env.data);
12
+ const content = [{ type: 'text', text }];
13
+ for (const w of env.warnings ?? [])
14
+ content.push({ type: 'text', text: `warning: ${w}` });
15
+ const result = { content, isError: false };
16
+ // structuredContent must be an object; omit for arrays/primitives (text still carries the data).
17
+ if (isPlainObject(env.data))
18
+ result.structuredContent = env.data;
19
+ return result;
20
+ }
@@ -0,0 +1 @@
1
+ export declare function runMcpServe(env?: NodeJS.ProcessEnv): Promise<void>;
@@ -0,0 +1,13 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { resolveMcpEnv } from './env.js';
3
+ import { buildServer } from './server.js';
4
+ import { allTools } from './registry.js';
5
+ // Resolves auth/env, builds the server, connects stdio. Never writes to stdout except protocol frames.
6
+ export async function runMcpServe(env = process.env) {
7
+ const mcpEnv = resolveMcpEnv(env);
8
+ const server = buildServer(mcpEnv);
9
+ const transport = new StdioServerTransport();
10
+ await server.connect(transport);
11
+ const target = mcpEnv.url ?? `https://${mcpEnv.org}.my.pitcher.com`;
12
+ console.error(`[pitcher-mcp] serving ${allTools.length} tools against ${target}${mcpEnv.prod ? ' (prod: destructive-hints on)' : ''}`);
13
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { McpEnv } from './env.js';
3
+ export declare function buildServer(env: McpEnv): McpServer;
@@ -0,0 +1,41 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { allTools } from './registry.js';
6
+ import { envelopeToToolResult, toErrorResult } from './result.js';
7
+ import { isCliError } from '../core/errors.js';
8
+ const version = (() => {
9
+ try {
10
+ const dir = dirname(fileURLToPath(import.meta.url));
11
+ return JSON.parse(readFileSync(join(dir, '..', '..', '..', 'package.json'), 'utf8')).version ?? '0.0.0';
12
+ }
13
+ catch {
14
+ // cosmetic server version; fall back if package.json is unreadable
15
+ return '0.0.0';
16
+ }
17
+ })();
18
+ export function buildServer(env) {
19
+ const server = new McpServer({ name: 'pitcher', version });
20
+ for (const tool of allTools) {
21
+ const destructive = tool.alwaysDestructive === true || (tool.write === true && env.prod);
22
+ server.registerTool(tool.name, {
23
+ title: tool.title,
24
+ description: tool.description,
25
+ inputSchema: tool.inputShape,
26
+ annotations: { readOnlyHint: tool.readOnly, destructiveHint: destructive },
27
+ }, async (args) => {
28
+ try {
29
+ // spread args FIRST so injected auth/env always wins — never reorder to { apiKey, ...args }
30
+ const envelope = await tool.run({ ...args, apiKey: env.apiKey, org: env.org, url: env.url });
31
+ return envelopeToToolResult(envelope);
32
+ }
33
+ catch (e) {
34
+ const code = isCliError(e) ? e.code : 'UNKNOWN';
35
+ const message = isCliError(e) ? e.message : e instanceof Error ? e.message : String(e);
36
+ return toErrorResult(code, message);
37
+ }
38
+ });
39
+ }
40
+ return server;
41
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDescriptor } from './types.js';
2
+ export declare const analyticsTools: ToolDescriptor[];
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { runAnalyticsOrg, runAnalyticsInstance } from '../../commands/analytics.js';
3
+ export const analyticsTools = [
4
+ {
5
+ name: 'pitcher_analytics_org',
6
+ title: 'Org analytics rollups',
7
+ description: 'Read organization-wide analytics rollup totals. Requires an org-admin API key.',
8
+ inputShape: {},
9
+ run: runAnalyticsOrg,
10
+ readOnly: true,
11
+ },
12
+ {
13
+ name: 'pitcher_analytics_instance',
14
+ title: 'Instance analytics rollups',
15
+ description: 'Read analytics rollup totals for a single instance. Requires an instance-admin or org-admin API key.',
16
+ inputShape: { instanceId: z.string().describe('Instance ID (required)') },
17
+ run: runAnalyticsInstance,
18
+ readOnly: true,
19
+ },
20
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ToolDescriptor } from './types.js';
2
+ export declare const announcementsTools: ToolDescriptor[];