@graspful/mcp 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -34,15 +34,25 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.TOOLS = void 0;
38
+ exports.mcpDistinctId = mcpDistinctId;
39
+ exports.handleToolCall = handleToolCall;
37
40
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
38
41
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
39
42
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
40
43
  const yaml = __importStar(require("js-yaml"));
41
- const crypto = __importStar(require("crypto"));
42
44
  const posthog_node_1 = require("posthog-node");
45
+ const node_crypto_1 = require("node:crypto");
43
46
  const shared_1 = require("@graspful/shared");
44
47
  // ─── PostHog analytics ──────────────────────────────────────────────────────
45
- const posthogKey = process.env.POSTHOG_API_KEY || process.env.NEXT_PUBLIC_POSTHOG_KEY;
48
+ const DEFAULT_POSTHOG_KEY = 'phc_ahQLCJsOBzeuro1yDeurs1a3xx07pIreJWeXG9T4d4';
49
+ const telemetryDisabled = process.env.GRASPFUL_TELEMETRY_DISABLED === '1' ||
50
+ process.env.NODE_ENV === 'test';
51
+ const posthogKey = telemetryDisabled
52
+ ? null
53
+ : process.env.POSTHOG_API_KEY ||
54
+ process.env.NEXT_PUBLIC_POSTHOG_KEY ||
55
+ DEFAULT_POSTHOG_KEY;
46
56
  const posthogClient = posthogKey
47
57
  ? new posthog_node_1.PostHog(posthogKey, {
48
58
  host: process.env.POSTHOG_HOST || process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
@@ -50,8 +60,18 @@ const posthogClient = posthogKey
50
60
  flushInterval: 0,
51
61
  })
52
62
  : null;
63
+ const anonymousMcpDistinctId = `anonymous-mcp:${(0, node_crypto_1.randomUUID)()}`;
53
64
  function mcpDistinctId() {
54
- return process.env.GRASPFUL_USER_ID || process.env.GRASPFUL_API_KEY || 'anonymous-mcp';
65
+ if (process.env.GRASPFUL_USER_ID) {
66
+ return process.env.GRASPFUL_USER_ID;
67
+ }
68
+ if (process.env.GRASPFUL_API_KEY) {
69
+ const digest = (0, node_crypto_1.createHash)('sha256')
70
+ .update(process.env.GRASPFUL_API_KEY)
71
+ .digest('hex');
72
+ return `credential:${digest}`;
73
+ }
74
+ return anonymousMcpDistinctId;
55
75
  }
56
76
  function mcpCapture(event, properties = {}) {
57
77
  posthogClient?.capture({
@@ -72,7 +92,7 @@ function requireApiAuth() {
72
92
  throw new Error(AUTH_REQUIRED_ERROR);
73
93
  }
74
94
  }
75
- // ─── API Client (mirrors packages/cli/src/lib/api-client.ts) ──────────────
95
+ // ─── API Client ─────────────────────────────────────────────────────────────
76
96
  function getApiCredentials() {
77
97
  const baseUrl = (process.env.GRASPFUL_API_URL || 'https://api.graspful.ai').replace(/\/$/, '');
78
98
  const apiKey = process.env.GRASPFUL_API_KEY;
@@ -81,15 +101,15 @@ function getApiCredentials() {
81
101
  }
82
102
  return { baseUrl };
83
103
  }
84
- async function apiPost(path, body) {
104
+ async function apiFetch(method, path, body) {
85
105
  const { baseUrl, authHeader } = getApiCredentials();
86
106
  const headers = { 'Content-Type': 'application/json' };
87
107
  if (authHeader)
88
108
  headers['Authorization'] = authHeader;
89
109
  const res = await fetch(`${baseUrl}${path}`, {
90
- method: 'POST',
110
+ method,
91
111
  headers,
92
- body: JSON.stringify(body),
112
+ body: body !== undefined ? JSON.stringify(body) : undefined,
93
113
  });
94
114
  if (!res.ok) {
95
115
  const text = await res.text();
@@ -97,658 +117,34 @@ async function apiPost(path, body) {
97
117
  }
98
118
  return res.json();
99
119
  }
100
- async function apiGet(path) {
101
- const { baseUrl, authHeader } = getApiCredentials();
102
- const headers = { 'Content-Type': 'application/json' };
103
- if (authHeader)
104
- headers['Authorization'] = authHeader;
105
- const res = await fetch(`${baseUrl}${path}`, {
106
- method: 'GET',
107
- headers,
108
- });
109
- if (!res.ok) {
110
- const text = await res.text();
111
- throw new Error(`API error ${res.status}: ${text}`);
112
- }
113
- return res.json();
120
+ // ─── YAML helpers ───────────────────────────────────────────────────────────
121
+ const YAML_DUMP_OPTS = { lineWidth: 120, noRefs: true };
122
+ function parseYaml(yamlStr) {
123
+ return yaml.load(yamlStr);
114
124
  }
115
- // ─── Scaffold helpers (mirrors packages/cli/src/commands/create-course.ts) ──
116
- function scaffoldCourse(topic, options) {
117
- const slug = topic.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
118
- return yaml.dump({
119
- course: {
120
- id: slug,
121
- name: topic,
122
- description: `Adaptive course on ${topic}`,
123
- estimatedHours: options.hours || 10,
124
- version: '2026.1',
125
- sourceDocument: options.source || 'TODO: Add source document',
126
- },
127
- sections: [
128
- { id: 'foundations', name: 'Foundations', description: 'Core concepts' },
129
- { id: 'application', name: 'Application', description: 'Applied concepts' },
130
- ],
131
- concepts: [
132
- {
133
- id: `${slug}-intro`,
134
- name: `Introduction to ${topic}`,
135
- section: 'foundations',
136
- difficulty: 2,
137
- estimatedMinutes: 15,
138
- tags: ['foundational'],
139
- prerequisites: [],
140
- knowledgePoints: [],
141
- },
142
- ],
143
- }, { lineWidth: 120, noRefs: true });
125
+ function dumpYaml(obj) {
126
+ return yaml.dump(obj, YAML_DUMP_OPTS);
144
127
  }
145
- function scaffoldBrand(niche, options) {
146
- const NICHE_PRESETS = {
147
- education: { preset: 'blue', tagline: 'Learn smarter, not harder', headline: 'Master any subject with adaptive learning' },
148
- healthcare: { preset: 'emerald', tagline: 'Training that saves lives', headline: 'Adaptive healthcare education for professionals' },
149
- finance: { preset: 'slate', tagline: 'Build financial expertise', headline: 'Master finance with adaptive learning' },
150
- tech: { preset: 'indigo', tagline: 'Level up your skills', headline: 'Adaptive tech training that meets you where you are' },
151
- legal: { preset: 'amber', tagline: 'Know the law, pass the exam', headline: 'Adaptive legal education for exam success' },
152
- default: { preset: 'blue', tagline: 'Learn adaptively', headline: 'Personalized learning that works' },
153
- };
154
- const config = NICHE_PRESETS[niche] || NICHE_PRESETS['default'];
155
- const slug = (options.name || niche).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
156
- const name = options.name || `${niche.charAt(0).toUpperCase() + niche.slice(1)} Academy`;
157
- const domain = options.domain || `${slug}.graspful.ai`;
158
- return yaml.dump({
159
- brand: {
160
- id: slug,
161
- name,
162
- domain,
163
- tagline: config.tagline,
164
- orgSlug: options.orgSlug || 'TODO: your-org-slug',
165
- },
166
- theme: {
167
- preset: config.preset,
168
- radius: '0.5rem',
169
- },
170
- landing: {
171
- hero: {
172
- headline: config.headline,
173
- subheadline: `${name} uses adaptive learning to help you master concepts faster.`,
174
- ctaText: 'Start Learning',
175
- },
176
- features: {
177
- heading: 'Why choose us?',
178
- items: [
179
- { title: 'Adaptive Learning', description: 'Content adapts to your knowledge level', icon: 'brain' },
180
- { title: 'Spaced Repetition', description: 'Review at optimal intervals for lasting memory', icon: 'clock' },
181
- { title: 'Progress Tracking', description: 'See exactly where you stand', icon: 'chart' },
182
- ],
183
- },
184
- howItWorks: {
185
- heading: 'How it works',
186
- items: [
187
- { title: 'Take a diagnostic', description: 'We assess what you already know' },
188
- { title: 'Learn adaptively', description: 'Focus on gaps, skip what you know' },
189
- { title: 'Master the material', description: 'Prove mastery through progressive challenges' },
190
- ],
128
+ const TOOLS = [
129
+ {
130
+ name: 'graspful_create_academy',
131
+ description: `Generate an academy plan and manifest scaffold for an academy-first workflow. Every academy is a connected curriculum made of one or more real courses.
132
+
133
+ Use this before authoring course YAML when the topic should be decomposed into learner-facing parts. If you do not pass courseNames, the scaffold creates the four default planning layers: foundations, core structures, operational flows, and applied judgment. The result includes authoring gates for source material, learner promise, landing-page proof, graph checks, and review before publishing.`,
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ topic: { type: 'string', description: 'Academy topic name (e.g., "PostHog TAM", "Linear Algebra")' },
138
+ courseNames: {
139
+ type: 'array',
140
+ description: 'Optional ordered course names to include in the manifest',
141
+ items: { type: 'string' },
142
+ },
143
+ version: { type: 'string', description: 'Academy version string (default: 2026.1)' },
191
144
  },
192
- faq: [],
193
- },
194
- seo: {
195
- title: `${name} — Adaptive Learning`,
196
- description: config.tagline,
197
- keywords: [niche, 'learning', 'adaptive', 'education'],
145
+ required: ['topic'],
198
146
  },
199
- }, { lineWidth: 120, noRefs: true });
200
- }
201
- function detectFileType(data) {
202
- if (typeof data !== 'object' || data === null)
203
- return null;
204
- const obj = data;
205
- if ('course' in obj)
206
- return 'course';
207
- if ('brand' in obj)
208
- return 'brand';
209
- if ('academy' in obj)
210
- return 'academy';
211
- return null;
212
- }
213
- function detectCycles(concepts) {
214
- const graph = new Map();
215
- for (const c of concepts) {
216
- graph.set(c.id, c.prerequisites);
217
- }
218
- const visited = new Set();
219
- const inStack = new Set();
220
- const cycles = [];
221
- function dfs(node, path) {
222
- if (inStack.has(node)) {
223
- const cycleStart = path.indexOf(node);
224
- const cycle = path.slice(cycleStart).concat(node);
225
- cycles.push(`Cycle: ${cycle.join(' -> ')}`);
226
- return true;
227
- }
228
- if (visited.has(node))
229
- return false;
230
- visited.add(node);
231
- inStack.add(node);
232
- path.push(node);
233
- for (const dep of graph.get(node) ?? []) {
234
- dfs(dep, path);
235
- }
236
- path.pop();
237
- inStack.delete(node);
238
- return false;
239
- }
240
- for (const id of graph.keys()) {
241
- if (!visited.has(id)) {
242
- dfs(id, []);
243
- }
244
- }
245
- return cycles;
246
- }
247
- function validateYaml(yamlStr) {
248
- let raw;
249
- try {
250
- raw = yaml.load(yamlStr);
251
- }
252
- catch (e) {
253
- const msg = e instanceof Error ? e.message : String(e);
254
- return { valid: false, errors: [`YAML parse error: ${msg}`], stats: {} };
255
- }
256
- const fileType = detectFileType(raw);
257
- if (!fileType) {
258
- return { valid: false, errors: ['Could not detect file type. Expected top-level key: course, brand, or academy'], stats: {} };
259
- }
260
- const schemaMap = {
261
- course: shared_1.CourseYamlSchema,
262
- brand: shared_1.BrandYamlSchema,
263
- academy: shared_1.AcademyManifestSchema,
264
- };
265
- const result = schemaMap[fileType].safeParse(raw);
266
- if (!result.success) {
267
- const errors = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
268
- return { valid: false, fileType, errors, stats: {} };
269
- }
270
- // For courses, also check DAG
271
- const dagErrors = [];
272
- let stats = { fileType };
273
- if (fileType === 'course') {
274
- const data = result.data;
275
- const conceptIds = new Set(data.concepts.map((c) => c.id));
276
- for (const concept of data.concepts) {
277
- for (const prereq of concept.prerequisites) {
278
- if (!conceptIds.has(prereq)) {
279
- dagErrors.push(`Concept "${concept.id}" has unknown prerequisite "${prereq}"`);
280
- }
281
- }
282
- }
283
- const cycles = detectCycles(data.concepts.map((c) => ({ id: c.id, prerequisites: c.prerequisites })));
284
- dagErrors.push(...cycles);
285
- const kpCount = data.concepts.reduce((sum, c) => sum + c.knowledgePoints.length, 0);
286
- const problemCount = data.concepts.reduce((sum, c) => sum + c.knowledgePoints.reduce((s, kp) => s + kp.problems.length, 0), 0);
287
- stats = { fileType, concepts: data.concepts.length, knowledgePoints: kpCount, problems: problemCount };
288
- }
289
- if (dagErrors.length > 0) {
290
- return { valid: false, fileType, errors: dagErrors, stats };
291
- }
292
- return { valid: true, fileType, errors: [], stats };
293
- }
294
- // ─── Review helpers (mirrors packages/cli/src/commands/review.ts) ───────────
295
- function checkYamlParses(raw) {
296
- const result = shared_1.CourseYamlSchema.safeParse(raw);
297
- if (result.success) {
298
- return { check: 'yaml_parses', passed: true };
299
- }
300
- const errors = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`);
301
- return {
302
- check: 'yaml_parses',
303
- passed: false,
304
- details: `${errors.length} schema error(s): ${errors.slice(0, 5).join('; ')}${errors.length > 5 ? ` (+${errors.length - 5} more)` : ''}`,
305
- };
306
- }
307
- function checkUniqueProblemIds(data) {
308
- const duplicates = [];
309
- const seen = new Set();
310
- for (const concept of data.concepts) {
311
- for (const kp of concept.knowledgePoints) {
312
- for (const problem of kp.problems) {
313
- if (seen.has(problem.id)) {
314
- duplicates.push(problem.id);
315
- }
316
- seen.add(problem.id);
317
- }
318
- }
319
- }
320
- if (duplicates.length === 0) {
321
- return { check: 'unique_problem_ids', passed: true };
322
- }
323
- return {
324
- check: 'unique_problem_ids',
325
- passed: false,
326
- details: `Duplicate problem IDs: ${duplicates.join(', ')}`,
327
- };
328
- }
329
- function checkPrerequisitesValid(data) {
330
- const conceptIds = new Set(data.concepts.map((c) => c.id));
331
- const invalid = [];
332
- for (const concept of data.concepts) {
333
- for (const prereq of concept.prerequisites) {
334
- if (!conceptIds.has(prereq)) {
335
- invalid.push(`${concept.id} -> ${prereq}`);
336
- }
337
- }
338
- }
339
- if (invalid.length === 0) {
340
- return { check: 'prerequisites_valid', passed: true };
341
- }
342
- return {
343
- check: 'prerequisites_valid',
344
- passed: false,
345
- details: `Unknown prerequisites: ${invalid.join(', ')}`,
346
- };
347
- }
348
- function normalizeText(text) {
349
- return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim();
350
- }
351
- function checkQuestionDeduplication(data) {
352
- const seen = new Map();
353
- const collisions = [];
354
- for (const concept of data.concepts) {
355
- for (const kp of concept.knowledgePoints) {
356
- for (const problem of kp.problems) {
357
- const normalized = normalizeText(problem.question);
358
- const hash = crypto.createHash('md5').update(normalized).digest('hex').substring(0, 12);
359
- const key = `${hash}-d${problem.difficulty ?? 'none'}`;
360
- const existing = seen.get(key);
361
- if (existing) {
362
- collisions.push(`"${problem.id}" collides with "${existing.problemId}" (same question text at same difficulty)`);
363
- }
364
- else {
365
- seen.set(key, { problemId: problem.id });
366
- }
367
- }
368
- }
369
- }
370
- if (collisions.length === 0) {
371
- return { check: 'question_deduplication', passed: true };
372
- }
373
- return {
374
- check: 'question_deduplication',
375
- passed: false,
376
- details: collisions.slice(0, 5).join('; ') + (collisions.length > 5 ? ` (+${collisions.length - 5} more)` : ''),
377
- };
378
- }
379
- function checkDifficultyStaircase(data) {
380
- const failures = [];
381
- for (const concept of data.concepts) {
382
- if (concept.knowledgePoints.length === 0)
383
- continue;
384
- const difficulties = new Set();
385
- for (const kp of concept.knowledgePoints) {
386
- for (const problem of kp.problems) {
387
- if (problem.difficulty != null) {
388
- difficulties.add(problem.difficulty);
389
- }
390
- }
391
- }
392
- if (difficulties.size > 0 && difficulties.size < 2) {
393
- failures.push(`"${concept.id}" has problems at only ${difficulties.size} difficulty level(s) — need 2+`);
394
- }
395
- }
396
- if (failures.length === 0) {
397
- return { check: 'difficulty_staircase', passed: true };
398
- }
399
- return {
400
- check: 'difficulty_staircase',
401
- passed: false,
402
- details: failures.slice(0, 5).join('; ') + (failures.length > 5 ? ` (+${failures.length - 5} more)` : ''),
403
- };
404
- }
405
- function extractStems(text) {
406
- return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter((w) => w.length > 4);
407
- }
408
- function checkCrossConceptCoverage(data) {
409
- const stemConceptCount = new Map();
410
- for (const concept of data.concepts) {
411
- for (const kp of concept.knowledgePoints) {
412
- for (const problem of kp.problems) {
413
- const stems = extractStems(problem.question);
414
- for (const stem of stems) {
415
- if (!stemConceptCount.has(stem)) {
416
- stemConceptCount.set(stem, new Set());
417
- }
418
- stemConceptCount.get(stem).add(concept.id);
419
- }
420
- }
421
- }
422
- }
423
- const overused = [];
424
- for (const [stem, concepts] of stemConceptCount) {
425
- if (concepts.size > 3) {
426
- overused.push(`"${stem}" appears across ${concepts.size} concepts`);
427
- }
428
- }
429
- const commonWords = new Set([
430
- 'which', 'would', 'should', 'could', 'about', 'their', 'there', 'these', 'those',
431
- 'being', 'between', 'through', 'during', 'before', 'after', 'above', 'below',
432
- 'following', 'statement', 'answer', 'question', 'correct', 'incorrect',
433
- 'agent', 'property', 'owner', 'buyer', 'seller',
434
- ]);
435
- const meaningfulOverused = overused.filter((entry) => {
436
- const stem = entry.match(/"([^"]+)"/)?.[1] ?? '';
437
- return !commonWords.has(stem);
438
- });
439
- if (meaningfulOverused.length === 0) {
440
- return { check: 'cross_concept_coverage', passed: true };
441
- }
442
- return {
443
- check: 'cross_concept_coverage',
444
- passed: meaningfulOverused.length <= 5,
445
- details: meaningfulOverused.slice(0, 5).join('; ') + (meaningfulOverused.length > 5 ? ` (+${meaningfulOverused.length - 5} more)` : ''),
446
- };
447
- }
448
- function checkProblemVariantDepth(data) {
449
- const failures = [];
450
- for (const concept of data.concepts) {
451
- if (concept.knowledgePoints.length === 0)
452
- continue;
453
- for (const kp of concept.knowledgePoints) {
454
- if (kp.problems.length < 3) {
455
- failures.push(`"${concept.id}/${kp.id}" has ${kp.problems.length} problem(s) — need 3+`);
456
- }
457
- }
458
- }
459
- if (failures.length === 0) {
460
- return { check: 'problem_variant_depth', passed: true };
461
- }
462
- return {
463
- check: 'problem_variant_depth',
464
- passed: false,
465
- details: failures.slice(0, 5).join('; ') + (failures.length > 5 ? ` (+${failures.length - 5} more)` : ''),
466
- };
467
- }
468
- function checkInstructionFormatting(data) {
469
- const warnings = [];
470
- for (const concept of data.concepts) {
471
- for (const kp of concept.knowledgePoints) {
472
- if (!kp.instruction)
473
- continue;
474
- if (kp.instruction.match(/^[\w\-./]+\.(md|txt|html)$/))
475
- continue;
476
- const wordCount = kp.instruction.split(/\s+/).filter(Boolean).length;
477
- const hasContentBlocks = kp.instructionContent && kp.instructionContent.length > 0;
478
- if (wordCount > 100 && !hasContentBlocks) {
479
- warnings.push(`"${concept.id}/${kp.id}" instruction is ${wordCount} words with no content blocks`);
480
- }
481
- }
482
- }
483
- if (warnings.length === 0) {
484
- return { check: 'instruction_formatting', passed: true };
485
- }
486
- return {
487
- check: 'instruction_formatting',
488
- passed: false,
489
- details: warnings.slice(0, 5).join('; ') + (warnings.length > 5 ? ` (+${warnings.length - 5} more)` : ''),
490
- };
491
- }
492
- function checkWorkedExampleCoverage(data) {
493
- const authoredConcepts = data.concepts.filter((c) => c.knowledgePoints.length > 0);
494
- if (authoredConcepts.length === 0) {
495
- return { check: 'worked_example_coverage', passed: true };
496
- }
497
- const withExamples = authoredConcepts.filter((c) => c.knowledgePoints.some((kp) => kp.workedExample && kp.workedExample.trim().length > 0));
498
- const coverage = withExamples.length / authoredConcepts.length;
499
- if (coverage >= 0.5) {
500
- return { check: 'worked_example_coverage', passed: true };
501
- }
502
- return {
503
- check: 'worked_example_coverage',
504
- passed: false,
505
- details: `${withExamples.length}/${authoredConcepts.length} authored concepts have worked examples (${Math.round(coverage * 100)}%) — need 50%+`,
506
- };
507
- }
508
- function checkImportDryRun(data) {
509
- const conceptIds = new Set(data.concepts.map((c) => c.id));
510
- const errors = [];
511
- for (const concept of data.concepts) {
512
- for (const prereq of concept.prerequisites) {
513
- if (!conceptIds.has(prereq)) {
514
- errors.push(`Unknown prerequisite: ${concept.id} -> ${prereq}`);
515
- }
516
- }
517
- }
518
- const graph = new Map();
519
- for (const c of data.concepts) {
520
- graph.set(c.id, [...c.prerequisites]);
521
- }
522
- const visited = new Set();
523
- const inStack = new Set();
524
- function hasCycle(node, path) {
525
- if (inStack.has(node)) {
526
- const cycleStart = path.indexOf(node);
527
- const cycle = path.slice(cycleStart).concat(node);
528
- errors.push(`Cycle detected: ${cycle.join(' -> ')}`);
529
- return true;
530
- }
531
- if (visited.has(node))
532
- return false;
533
- visited.add(node);
534
- inStack.add(node);
535
- path.push(node);
536
- let foundCycle = false;
537
- for (const dep of graph.get(node) ?? []) {
538
- if (hasCycle(dep, path)) {
539
- foundCycle = true;
540
- }
541
- }
542
- path.pop();
543
- inStack.delete(node);
544
- return foundCycle;
545
- }
546
- for (const id of graph.keys()) {
547
- if (!visited.has(id)) {
548
- hasCycle(id, []);
549
- }
550
- }
551
- if (errors.length === 0) {
552
- return { check: 'import_dry_run', passed: true };
553
- }
554
- return {
555
- check: 'import_dry_run',
556
- passed: false,
557
- details: errors.join('; '),
558
- };
559
- }
560
- function runReview(yamlStr) {
561
- let raw;
562
- try {
563
- raw = yaml.load(yamlStr);
564
- }
565
- catch (e) {
566
- const msg = e instanceof Error ? e.message : String(e);
567
- return {
568
- passed: false,
569
- score: '0/10',
570
- failures: [{ check: 'yaml_parses', passed: false, details: `YAML parse error: ${msg}` }],
571
- warnings: [],
572
- stats: { concepts: 0, kps: 0, problems: 0, authoredConcepts: 0, stubConcepts: 0 },
573
- };
574
- }
575
- const checks = [];
576
- const parseCheck = checkYamlParses(raw);
577
- checks.push(parseCheck);
578
- if (!parseCheck.passed) {
579
- return {
580
- passed: false,
581
- score: '0/10',
582
- failures: checks.filter((c) => !c.passed),
583
- warnings: [],
584
- stats: { concepts: 0, kps: 0, problems: 0, authoredConcepts: 0, stubConcepts: 0 },
585
- };
586
- }
587
- const data = shared_1.CourseYamlSchema.parse(raw);
588
- const authoredConcepts = data.concepts.filter((c) => c.knowledgePoints.length > 0);
589
- const stubConcepts = data.concepts.filter((c) => c.knowledgePoints.length === 0);
590
- const kps = data.concepts.reduce((sum, c) => sum + c.knowledgePoints.length, 0);
591
- const problems = data.concepts.reduce((sum, c) => sum + c.knowledgePoints.reduce((s, kp) => s + kp.problems.length, 0), 0);
592
- const stats = {
593
- concepts: data.concepts.length,
594
- kps,
595
- problems,
596
- authoredConcepts: authoredConcepts.length,
597
- stubConcepts: stubConcepts.length,
598
- };
599
- checks.push(checkUniqueProblemIds(data));
600
- checks.push(checkPrerequisitesValid(data));
601
- checks.push(checkQuestionDeduplication(data));
602
- checks.push(checkDifficultyStaircase(data));
603
- checks.push(checkCrossConceptCoverage(data));
604
- checks.push(checkProblemVariantDepth(data));
605
- checks.push(checkInstructionFormatting(data));
606
- checks.push(checkWorkedExampleCoverage(data));
607
- checks.push(checkImportDryRun(data));
608
- const passedCount = checks.filter((c) => c.passed).length;
609
- const failures = checks.filter((c) => !c.passed);
610
- return {
611
- passed: failures.length === 0,
612
- score: `${passedCount}/10`,
613
- failures,
614
- warnings: [],
615
- stats,
616
- };
617
- }
618
- // ─── Describe helper (mirrors packages/cli/src/commands/describe.ts) ────────
619
- function computeGraphDepth(concepts) {
620
- const graph = new Map();
621
- for (const c of concepts) {
622
- graph.set(c.id, c.prerequisites);
623
- }
624
- const memo = new Map();
625
- function depth(id, visited) {
626
- if (memo.has(id))
627
- return memo.get(id);
628
- if (visited.has(id))
629
- return 0;
630
- visited.add(id);
631
- const prereqs = graph.get(id) ?? [];
632
- let maxPrereqDepth = 0;
633
- for (const prereq of prereqs) {
634
- if (graph.has(prereq)) {
635
- maxPrereqDepth = Math.max(maxPrereqDepth, depth(prereq, visited));
636
- }
637
- }
638
- const d = maxPrereqDepth + 1;
639
- memo.set(id, d);
640
- return d;
641
- }
642
- let maxDepth = 0;
643
- for (const c of concepts) {
644
- maxDepth = Math.max(maxDepth, depth(c.id, new Set()));
645
- }
646
- return maxDepth;
647
- }
648
- function describeCourse(yamlStr) {
649
- const raw = yaml.load(yamlStr);
650
- const result = shared_1.CourseYamlSchema.safeParse(raw);
651
- if (!result.success) {
652
- throw new Error(`Invalid course YAML: ${result.error.issues[0]?.message ?? 'unknown error'}`);
653
- }
654
- const data = result.data;
655
- const concepts = data.concepts;
656
- const sections = data.sections;
657
- const authoredConcepts = concepts.filter((c) => c.knowledgePoints.length > 0);
658
- const stubConcepts = concepts.filter((c) => c.knowledgePoints.length === 0);
659
- const kpCount = concepts.reduce((sum, c) => sum + c.knowledgePoints.length, 0);
660
- const problemCount = concepts.reduce((sum, c) => sum + c.knowledgePoints.reduce((s, kp) => s + kp.problems.length, 0), 0);
661
- const graphDepth = computeGraphDepth(concepts);
662
- const conceptsWithoutKps = stubConcepts.map((c) => c.id);
663
- const kpsWithoutProblems = [];
664
- for (const c of concepts) {
665
- for (const kp of c.knowledgePoints) {
666
- if (kp.problems.length === 0) {
667
- kpsWithoutProblems.push(`${c.id}/${kp.id}`);
668
- }
669
- }
670
- }
671
- const sectionBreakdown = [];
672
- if (sections.length > 0) {
673
- for (const section of sections) {
674
- const sectionConcepts = concepts.filter((c) => c.section === section.id);
675
- const sKps = sectionConcepts.reduce((sum, c) => sum + c.knowledgePoints.length, 0);
676
- const sProblems = sectionConcepts.reduce((sum, c) => sum + c.knowledgePoints.reduce((s, kp) => s + kp.problems.length, 0), 0);
677
- sectionBreakdown.push({ section: section.id, concepts: sectionConcepts.length, kps: sKps, problems: sProblems });
678
- }
679
- const unsectioned = concepts.filter((c) => !c.section);
680
- if (unsectioned.length > 0) {
681
- const uKps = unsectioned.reduce((sum, c) => sum + c.knowledgePoints.length, 0);
682
- const uProblems = unsectioned.reduce((sum, c) => sum + c.knowledgePoints.reduce((s, kp) => s + kp.problems.length, 0), 0);
683
- sectionBreakdown.push({ section: '(unsectioned)', concepts: unsectioned.length, kps: uKps, problems: uProblems });
684
- }
685
- }
686
- return {
687
- courseName: data.course.name,
688
- courseId: data.course.id,
689
- version: data.course.version,
690
- estimatedHours: data.course.estimatedHours,
691
- concepts: concepts.length,
692
- authoredConcepts: authoredConcepts.length,
693
- stubConcepts: stubConcepts.length,
694
- knowledgePoints: kpCount,
695
- problems: problemCount,
696
- graphDepth,
697
- conceptsWithoutKps: conceptsWithoutKps.length,
698
- conceptsWithoutKpsList: conceptsWithoutKps,
699
- kpsWithoutProblems: kpsWithoutProblems.length,
700
- kpsWithoutProblemsList: kpsWithoutProblems,
701
- sections: sectionBreakdown,
702
- };
703
- }
704
- // ─── Fill concept helper (mirrors packages/cli/src/commands/fill-concept.ts) ─
705
- function fillConcept(yamlStr, conceptId, options) {
706
- const raw = yaml.load(yamlStr);
707
- const parsed = shared_1.CourseYamlSchema.safeParse(raw);
708
- if (!parsed.success) {
709
- throw new Error(`Invalid course YAML: ${parsed.error.issues[0]?.message ?? 'unknown error'}`);
710
- }
711
- const data = parsed.data;
712
- const concept = data.concepts.find((c) => c.id === conceptId);
713
- if (!concept) {
714
- throw new Error(`Concept "${conceptId}" not found. Available: ${data.concepts.map((c) => c.id).join(', ')}`);
715
- }
716
- if (concept.knowledgePoints.length > 0) {
717
- throw new Error(`Concept "${conceptId}" already has ${concept.knowledgePoints.length} KP(s). Remove them first to regenerate.`);
718
- }
719
- const kpCount = options.kps ?? 2;
720
- const problemsPerKp = options.problemsPerKp ?? 3;
721
- const newKps = [];
722
- for (let i = 1; i <= kpCount; i++) {
723
- const problems = [];
724
- for (let j = 1; j <= problemsPerKp; j++) {
725
- problems.push({
726
- id: `${conceptId}-kp${i}-p${j}`,
727
- type: 'multiple_choice',
728
- question: `TODO: Write question ${j} for ${conceptId} KP${i}`,
729
- options: ['Option A', 'Option B', 'Option C', 'Option D'],
730
- correct: 0,
731
- explanation: 'TODO: Explain the correct answer',
732
- difficulty: Math.min(j + 1, 5),
733
- });
734
- }
735
- newKps.push({
736
- id: `${conceptId}-kp${i}`,
737
- instruction: `TODO: Write instruction for ${concept.name} — knowledge point ${i}`,
738
- workedExample: `TODO: Write a worked example for ${concept.name} — knowledge point ${i}`,
739
- problems,
740
- });
741
- }
742
- // Rebuild the raw object to preserve structure
743
- const rawObj = raw;
744
- const concepts = rawObj['concepts'];
745
- const targetConcept = concepts.find((c) => c['id'] === conceptId);
746
- if (targetConcept) {
747
- targetConcept['knowledgePoints'] = newKps;
748
- }
749
- return yaml.dump(rawObj, { lineWidth: 120, noRefs: true });
750
- }
751
- const TOOLS = [
147
+ },
752
148
  {
753
149
  name: 'graspful_scaffold_course',
754
150
  description: `Generate a course YAML skeleton with sections, concepts, and prerequisite edges. Returns a minimal valid YAML structure with TODO placeholders.
@@ -793,7 +189,7 @@ Fails if the concept already has KPs (to prevent accidental overwrites).`,
793
189
  properties: {
794
190
  yaml: { type: 'string', description: 'The full course YAML string' },
795
191
  conceptId: { type: 'string', description: 'ID of the concept to fill (must exist in the YAML and have 0 KPs)' },
796
- kps: { type: 'number', description: 'Number of KP stubs to add (default: 2)' },
192
+ kps: { type: 'number', description: 'Number of KP stubs to add as a starting point (default: 3, not a cap)' },
797
193
  problemsPerKp: { type: 'number', description: 'Number of problem stubs per KP (default: 3)' },
798
194
  },
799
195
  required: ['yaml', 'conceptId'],
@@ -829,7 +225,7 @@ The 10 checks are:
829
225
  3. prerequisites_valid — All prerequisite refs point to real concepts
830
226
  4. question_deduplication — No near-duplicate questions at the same difficulty
831
227
  5. difficulty_staircase — Each concept has problems at 2+ difficulty levels
832
- 6. cross_concept_coverageNo single term dominates too many concepts
228
+ 6. problem_teaching_alignmentProblems only assess material introduced in the current lesson path
833
229
  7. problem_variant_depth — Each KP has 3+ problems
834
230
  8. instruction_formatting — Long instructions have content blocks
835
231
  9. worked_example_coverage — 50%+ of authored concepts have worked examples
@@ -844,6 +240,29 @@ A score of 10/10 is required for publishing. Run this before graspful_import_cou
844
240
  required: ['yaml'],
845
241
  },
846
242
  },
243
+ {
244
+ name: 'graspful_import_academy',
245
+ description: `Import an academy manifest and its referenced course YAMLs into a Graspful organization.
246
+
247
+ IMPORTANT: Requires authentication. If not authenticated, run \`graspful register\` in a terminal first or set the \`GRASPFUL_API_KEY\` environment variable. Without auth, this tool will fail.
248
+
249
+ If publish=true, Graspful imports the academy first and then attempts to publish each imported course. Returns the academy result plus publishedCourseIds and publishFailures.`,
250
+ inputSchema: {
251
+ type: 'object',
252
+ properties: {
253
+ manifestYaml: { type: 'string', description: 'The full academy manifest YAML string' },
254
+ courseYamls: {
255
+ type: 'object',
256
+ description: 'Object mapping manifest file paths to the full course YAML strings',
257
+ },
258
+ org: { type: 'string', description: 'Organization slug (e.g., "acme-learning")' },
259
+ publish: { type: 'boolean', description: 'If true, publish every imported course after academy import. Default: false' },
260
+ replace: { type: 'boolean', description: 'Replace existing academy/course content on re-import. Default: false' },
261
+ archiveMissing: { type: 'boolean', description: 'Archive removed content on re-import. Default: false' },
262
+ },
263
+ required: ['manifestYaml', 'courseYamls', 'org'],
264
+ },
265
+ },
847
266
  {
848
267
  name: 'graspful_import_course',
849
268
  description: `Import a course YAML into a Graspful organization. Creates the course as a draft by default.
@@ -917,6 +336,7 @@ Edit the YAML to customize, then import with \`graspful_import_brand\`.`,
917
336
  properties: {
918
337
  niche: { type: 'string', description: 'Brand niche: education, healthcare, finance, tech, or legal' },
919
338
  name: { type: 'string', description: 'Brand name (default: "{Niche} Academy")' },
339
+ topic: { type: 'string', description: 'Academy topic for more specific landing-page copy' },
920
340
  domain: { type: 'string', description: 'Custom domain (default: "{slug}.graspful.ai")' },
921
341
  orgSlug: { type: 'string', description: 'Organization slug to associate with' },
922
342
  },
@@ -954,6 +374,7 @@ Returns an array of courses with their IDs, names, published status, and stats.`
954
374
  },
955
375
  },
956
376
  ];
377
+ exports.TOOLS = TOOLS;
957
378
  function textResult(text) {
958
379
  return { content: [{ type: 'text', text }] };
959
380
  }
@@ -962,39 +383,113 @@ function errorResult(text) {
962
383
  }
963
384
  async function handleToolCall(name, args) {
964
385
  switch (name) {
386
+ case 'graspful_create_academy': {
387
+ const topic = args.topic;
388
+ const obj = (0, shared_1.scaffoldAcademyObject)(topic, {
389
+ courseNames: args.courseNames,
390
+ version: args.version,
391
+ });
392
+ mcpCapture('academy scaffolded', { topic, course_count: obj.courses.length });
393
+ return textResult(dumpYaml(obj));
394
+ }
965
395
  case 'graspful_scaffold_course': {
966
396
  const topic = args.topic;
967
- const estimatedHours = args.estimatedHours;
968
- const sourceDocument = args.sourceDocument;
969
- const yamlContent = scaffoldCourse(topic, { hours: estimatedHours, source: sourceDocument });
970
- mcpCapture('course scaffolded', { topic, estimated_hours: estimatedHours });
971
- return textResult(yamlContent);
397
+ const obj = (0, shared_1.scaffoldCourseObject)(topic, {
398
+ hours: args.estimatedHours,
399
+ source: args.sourceDocument,
400
+ });
401
+ mcpCapture('course scaffolded', { topic, estimated_hours: args.estimatedHours });
402
+ return textResult(dumpYaml(obj));
972
403
  }
973
404
  case 'graspful_fill_concept': {
974
405
  try {
975
406
  const conceptId = args.conceptId;
976
- const updatedYaml = fillConcept(args.yaml, conceptId, { kps: args.kps, problemsPerKp: args.problemsPerKp });
407
+ const raw = parseYaml(args.yaml);
408
+ const updated = (0, shared_1.fillConceptInRaw)(raw, conceptId, {
409
+ kps: args.kps,
410
+ problemsPerKp: args.problemsPerKp,
411
+ });
977
412
  mcpCapture('concept filled', { concept_id: conceptId });
978
- return textResult(updatedYaml);
413
+ return textResult(dumpYaml(updated));
979
414
  }
980
415
  catch (e) {
981
416
  return errorResult(e instanceof Error ? e.message : String(e));
982
417
  }
983
418
  }
984
419
  case 'graspful_validate': {
985
- const result = validateYaml(args.yaml);
420
+ let raw;
421
+ try {
422
+ raw = parseYaml(args.yaml);
423
+ }
424
+ catch (e) {
425
+ const msg = e instanceof Error ? e.message : String(e);
426
+ return textResult(JSON.stringify({ valid: false, errors: [`YAML parse error: ${msg}`], stats: {} }, null, 2));
427
+ }
428
+ const result = (0, shared_1.validateParsedYaml)(raw);
986
429
  mcpCapture('course validated', { valid: result.valid, error_count: result.errors.length, file_type: result.fileType });
987
430
  return textResult(JSON.stringify(result, null, 2));
988
431
  }
989
432
  case 'graspful_review_course': {
990
- const result = runReview(args.yaml);
433
+ let raw;
434
+ try {
435
+ raw = parseYaml(args.yaml);
436
+ }
437
+ catch (e) {
438
+ const msg = e instanceof Error ? e.message : String(e);
439
+ return textResult(JSON.stringify({
440
+ passed: false,
441
+ score: '0/10',
442
+ failures: [{ check: 'yaml_parses', passed: false, details: `YAML parse error: ${msg}` }],
443
+ warnings: [],
444
+ stats: { concepts: 0, kps: 0, problems: 0, authoredConcepts: 0, stubConcepts: 0 },
445
+ }, null, 2));
446
+ }
447
+ const result = (0, shared_1.runQualityGate)(raw);
991
448
  mcpCapture('course reviewed', { score: result.score, passed: result.passed });
992
449
  return textResult(JSON.stringify(result, null, 2));
993
450
  }
451
+ case 'graspful_import_academy': {
452
+ try {
453
+ requireApiAuth();
454
+ const result = await apiFetch('POST', `/api/v1/orgs/${args.org}/academies/import`, {
455
+ manifestYaml: args.manifestYaml,
456
+ courseYamls: args.courseYamls,
457
+ replace: args.replace ?? false,
458
+ archiveMissing: args.archiveMissing ?? false,
459
+ });
460
+ const publishedCourseIds = [];
461
+ const publishFailures = [];
462
+ if (args.publish) {
463
+ for (const courseResult of result.courseResults) {
464
+ try {
465
+ await apiFetch('POST', `/api/v1/orgs/${args.org}/courses/${courseResult.courseId}/publish`, {});
466
+ publishedCourseIds.push(courseResult.courseId);
467
+ }
468
+ catch (error) {
469
+ publishFailures.push(`${courseResult.courseId}: ${error instanceof Error ? error.message : String(error)}`);
470
+ }
471
+ }
472
+ }
473
+ mcpCapture('academy imported', {
474
+ academy_id: result.academyId,
475
+ org: args.org,
476
+ course_count: result.courseCount,
477
+ published_count: publishedCourseIds.length,
478
+ });
479
+ return textResult(JSON.stringify({
480
+ ...result,
481
+ publishedCourseIds,
482
+ publishFailures,
483
+ }, null, 2));
484
+ }
485
+ catch (e) {
486
+ return errorResult(`Academy import failed: ${e instanceof Error ? e.message : String(e)}`);
487
+ }
488
+ }
994
489
  case 'graspful_import_course': {
995
490
  try {
996
491
  requireApiAuth();
997
- const result = await apiPost(`/api/v1/orgs/${args.org}/courses/import`, { yaml: args.yaml, publish: args.publish ?? false });
492
+ const result = await apiFetch('POST', `/api/v1/orgs/${args.org}/courses/import`, { yaml: args.yaml, publish: args.publish ?? false });
998
493
  mcpCapture('course imported', { course_id: result.courseId, org: args.org, published: result.published });
999
494
  return textResult(JSON.stringify(result, null, 2));
1000
495
  }
@@ -1005,7 +500,7 @@ async function handleToolCall(name, args) {
1005
500
  case 'graspful_publish_course': {
1006
501
  try {
1007
502
  requireApiAuth();
1008
- const result = await apiPost(`/api/v1/orgs/${args.org}/courses/${args.courseId}/publish`, {});
503
+ const result = await apiFetch('POST', `/api/v1/orgs/${args.org}/courses/${args.courseId}/publish`, {});
1009
504
  mcpCapture('course published', { course_id: result.courseId, org: args.org, published: result.published });
1010
505
  return textResult(JSON.stringify(result, null, 2));
1011
506
  }
@@ -1015,7 +510,9 @@ async function handleToolCall(name, args) {
1015
510
  }
1016
511
  case 'graspful_describe_course': {
1017
512
  try {
1018
- const stats = describeCourse(args.yaml);
513
+ const raw = parseYaml(args.yaml);
514
+ const parsed = shared_1.CourseYamlSchema.parse(raw);
515
+ const stats = (0, shared_1.describeCourse)(parsed);
1019
516
  mcpCapture('course described', stats);
1020
517
  return textResult(JSON.stringify(stats, null, 2));
1021
518
  }
@@ -1025,25 +522,25 @@ async function handleToolCall(name, args) {
1025
522
  }
1026
523
  case 'graspful_create_brand': {
1027
524
  const niche = args.niche;
1028
- const yamlContent = scaffoldBrand(niche, {
525
+ const obj = (0, shared_1.scaffoldBrandObject)(niche, {
1029
526
  name: args.name,
527
+ topic: args.topic,
1030
528
  domain: args.domain,
1031
529
  orgSlug: args.orgSlug,
1032
530
  });
1033
531
  mcpCapture('brand scaffolded', { niche });
1034
- return textResult(yamlContent);
532
+ return textResult(dumpYaml(obj));
1035
533
  }
1036
534
  case 'graspful_import_brand': {
1037
535
  try {
1038
536
  requireApiAuth();
1039
537
  let raw;
1040
538
  try {
1041
- raw = yaml.load(args.yaml);
539
+ raw = parseYaml(args.yaml);
1042
540
  }
1043
541
  catch (e) {
1044
542
  throw new Error(`YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
1045
543
  }
1046
- // Unwrap YAML structure to flat DTO (brand YAML has nested brand: key)
1047
544
  const parsed = raw;
1048
545
  const brandSection = (parsed.brand || {});
1049
546
  const dto = {
@@ -1058,7 +555,7 @@ async function handleToolCall(name, args) {
1058
555
  seo: parsed.seo || {},
1059
556
  pricing: parsed.pricing || {},
1060
557
  };
1061
- const result = await apiPost('/api/v1/brands', dto);
558
+ const result = await apiFetch('POST', '/api/v1/brands', dto);
1062
559
  mcpCapture('brand imported', { slug: result.slug, domain: result.domain });
1063
560
  return textResult(JSON.stringify(result, null, 2));
1064
561
  }
@@ -1069,7 +566,7 @@ async function handleToolCall(name, args) {
1069
566
  case 'graspful_list_courses': {
1070
567
  try {
1071
568
  requireApiAuth();
1072
- const result = await apiGet(`/api/v1/orgs/${args.org}/courses`);
569
+ const result = await apiFetch('GET', `/api/v1/orgs/${args.org}/courses`);
1073
570
  mcpCapture('courses listed', { org: args.org, count: result.length });
1074
571
  return textResult(JSON.stringify(result, null, 2));
1075
572
  }
@@ -1081,36 +578,37 @@ async function handleToolCall(name, args) {
1081
578
  return errorResult(`Unknown tool: ${name}`);
1082
579
  }
1083
580
  }
1084
- // ─── MCP Server ─────────────────────────────────────────────────────────────
1085
- const server = new index_js_1.Server({
1086
- name: 'graspful',
1087
- version: '0.1.0',
1088
- }, {
1089
- capabilities: {
1090
- tools: {},
1091
- },
1092
- });
1093
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
1094
- return { tools: TOOLS };
1095
- });
1096
- server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
1097
- const { name, arguments: args } = request.params;
1098
- return handleToolCall(name, args ?? {});
1099
- });
1100
- // ─── Start server ───────────────────────────────────────────────────────────
1101
- async function main() {
1102
- const transport = new stdio_js_1.StdioServerTransport();
1103
- await server.connect(transport);
1104
- }
1105
- main().catch((error) => {
1106
- console.error('Fatal error:', error);
1107
- process.exit(1);
1108
- });
1109
- async function shutdown() {
1110
- if (posthogClient)
1111
- await posthogClient.shutdown();
1112
- process.exit(0);
581
+ // ─── MCP Server (only when run directly) ────────────────────────────────────
582
+ if (require.main === module) {
583
+ const server = new index_js_1.Server({
584
+ name: 'graspful',
585
+ version: '0.2.4',
586
+ }, {
587
+ capabilities: {
588
+ tools: {},
589
+ },
590
+ });
591
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
592
+ return { tools: TOOLS };
593
+ });
594
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
595
+ const { name, arguments: args } = request.params;
596
+ return handleToolCall(name, args ?? {});
597
+ });
598
+ async function main() {
599
+ const transport = new stdio_js_1.StdioServerTransport();
600
+ await server.connect(transport);
601
+ }
602
+ main().catch((error) => {
603
+ console.error('Fatal error:', error);
604
+ process.exit(1);
605
+ });
606
+ async function shutdown() {
607
+ if (posthogClient)
608
+ await posthogClient.shutdown();
609
+ process.exit(0);
610
+ }
611
+ process.on('SIGTERM', shutdown);
612
+ process.on('SIGINT', shutdown);
1113
613
  }
1114
- process.on('SIGTERM', shutdown);
1115
- process.on('SIGINT', shutdown);
1116
614
  //# sourceMappingURL=index.js.map