@innerstacklabs/neuralingual-mcp 0.1.2 → 0.3.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.
- package/README.md +2 -0
- package/dist/cli.js +150 -144
- package/dist/cli.js.map +1 -1
- package/dist/user-mcp.d.ts +10 -0
- package/dist/user-mcp.d.ts.map +1 -0
- package/dist/user-mcp.js +552 -0
- package/dist/user-mcp.js.map +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ const program = new Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name('neuralingual')
|
|
20
20
|
.description('Neuralingual — AI-powered affirmation practice sets')
|
|
21
|
-
.version('0.
|
|
21
|
+
.version('0.2.0')
|
|
22
22
|
.option('--env <env>', 'API environment: dev or production (default: production)', 'production');
|
|
23
23
|
function printResult(data, isError = false) {
|
|
24
24
|
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
@@ -42,72 +42,6 @@ function printTable(rows, headers) {
|
|
|
42
42
|
console.log(line(row));
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
/** Read all of stdin and return as a string. */
|
|
46
|
-
function readStdin() {
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
const chunks = [];
|
|
49
|
-
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
50
|
-
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
51
|
-
process.stdin.on('error', reject);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
/** Get a user client from stored auth. Exits with helpful message if not logged in. */
|
|
55
|
-
function getUserClient() {
|
|
56
|
-
try {
|
|
57
|
-
return UserApiClient.fromAuth();
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
console.error('Not logged in. Run `neuralingual login` first.');
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** Resolve API env: explicit --env flag wins, then stored auth, then default 'production'. */
|
|
65
|
-
function resolveApiEnv() {
|
|
66
|
-
const opts = program.opts();
|
|
67
|
-
const explicitEnv = process.argv.some((a) => a === '--env' || a.startsWith('--env='));
|
|
68
|
-
const env = (explicitEnv ? opts['env'] : loadAuth()?.env ?? opts['env'] ?? 'production');
|
|
69
|
-
if (env !== 'dev' && env !== 'production') {
|
|
70
|
-
console.error(`Error: --env must be "dev" or "production", got "${env}"`);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
return env;
|
|
74
|
-
}
|
|
75
|
-
/** Resolve the API base URL using resolveApiEnv(). */
|
|
76
|
-
function getApiBaseUrl() {
|
|
77
|
-
return API_BASE_URLS[resolveApiEnv()];
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Resolve a short/truncated intent ID to the full ID by fetching the user's library.
|
|
81
|
-
* Handles exact match, prefix match, and ambiguous matches (multiple prefix hits).
|
|
82
|
-
*/
|
|
83
|
-
async function resolveIntentId(client, shortId) {
|
|
84
|
-
const { items } = await client.getLibrary();
|
|
85
|
-
const exact = items.find((i) => i.intent.id === shortId);
|
|
86
|
-
if (exact)
|
|
87
|
-
return exact.intent.id;
|
|
88
|
-
const prefixMatches = items.filter((i) => i.intent.id.startsWith(shortId));
|
|
89
|
-
if (prefixMatches.length === 1)
|
|
90
|
-
return prefixMatches[0].intent.id;
|
|
91
|
-
if (prefixMatches.length > 1) {
|
|
92
|
-
console.error(`Error: ambiguous ID "${shortId}" matches ${prefixMatches.length} intents:`);
|
|
93
|
-
for (const m of prefixMatches) {
|
|
94
|
-
console.error(` ${m.intent.id.slice(0, 12)} ${m.intent.title ?? '(untitled)'}`);
|
|
95
|
-
}
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
console.error(`Error: no practice set found matching "${shortId}"`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
/** Prompt the user for input on stdin. */
|
|
102
|
-
function prompt(question) {
|
|
103
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
rl.question(question, (answer) => {
|
|
106
|
-
rl.close();
|
|
107
|
-
resolve(answer.trim());
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
45
|
// ─── render ──────────────────────────────────────────────────────────────────
|
|
112
46
|
const renderCmd = program.command('render').description('Render audio for an intent');
|
|
113
47
|
renderCmd
|
|
@@ -117,7 +51,7 @@ renderCmd
|
|
|
117
51
|
.requiredOption('--context <context>', `Session context: ${VALID_CONTEXTS.join(', ')}`)
|
|
118
52
|
.requiredOption('--duration <minutes>', 'Duration in minutes', parseInt)
|
|
119
53
|
.option('--pace <wpm>', 'Pace in words per minute (uses context default if omitted)', parseInt)
|
|
120
|
-
.option('--background <key>', 'Background sound storageKey (use neuralingual voices; omit to disable)')
|
|
54
|
+
.option('--background <key>', 'Background sound storageKey (use neuralingual voices list; omit to disable)')
|
|
121
55
|
.option('--background-volume <level>', 'Background volume 0–1 (uses context default if omitted)', parseFloat)
|
|
122
56
|
.option('--repeats <n>', 'Number of times each affirmation repeats (uses context default if omitted)', parseInt)
|
|
123
57
|
.option('--preamble <on|off>', 'Include intro/outro preamble: on or off (preserves existing setting if omitted)')
|
|
@@ -211,11 +145,14 @@ renderCmd
|
|
|
211
145
|
console.error(`Warning: status poll failed — ${pollErr instanceof Error ? pollErr.message : String(pollErr)}`);
|
|
212
146
|
continue;
|
|
213
147
|
}
|
|
148
|
+
// Guard: if status has no jobId at all, the render config was likely reconfigured
|
|
149
|
+
// and the job we started is no longer the current one — exit to avoid infinite loop
|
|
214
150
|
if (status.jobId === undefined) {
|
|
215
151
|
console.error(`Warning: render status has no active job (config may have been reconfigured). Exiting --wait.`);
|
|
216
152
|
printResult(status);
|
|
217
153
|
return;
|
|
218
154
|
}
|
|
155
|
+
// Guard: if the status is tracking a different job (concurrent start), stop waiting
|
|
219
156
|
if (status.jobId !== jobId) {
|
|
220
157
|
console.error(`Warning: render status is now tracking a different job (${status.jobId}). Exiting --wait.`);
|
|
221
158
|
printResult(status);
|
|
@@ -252,6 +189,21 @@ renderCmd
|
|
|
252
189
|
});
|
|
253
190
|
// ─── voices ──────────────────────────────────────────────────────────────────
|
|
254
191
|
const voicesCmd = program.command('voices').description('Browse and preview available voices');
|
|
192
|
+
/** Resolve API env: explicit --env flag wins, then stored auth, then default 'production'. */
|
|
193
|
+
function resolveApiEnv() {
|
|
194
|
+
const opts = program.opts();
|
|
195
|
+
const explicitEnv = process.argv.some((a) => a === '--env' || a.startsWith('--env='));
|
|
196
|
+
const env = (explicitEnv ? opts['env'] : loadAuth()?.env ?? opts['env'] ?? 'production');
|
|
197
|
+
if (env !== 'dev' && env !== 'production') {
|
|
198
|
+
console.error(`Error: --env must be "dev" or "production", got "${env}"`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
return env;
|
|
202
|
+
}
|
|
203
|
+
/** Resolve the API base URL using resolveApiEnv(). */
|
|
204
|
+
function getApiBaseUrl() {
|
|
205
|
+
return API_BASE_URLS[resolveApiEnv()];
|
|
206
|
+
}
|
|
255
207
|
const voiceDtoSchema = z.object({
|
|
256
208
|
id: z.string(),
|
|
257
209
|
provider: z.string(),
|
|
@@ -266,7 +218,6 @@ const voiceDtoSchema = z.object({
|
|
|
266
218
|
const voicesResponseSchema = z.object({
|
|
267
219
|
voices: z.array(voiceDtoSchema),
|
|
268
220
|
});
|
|
269
|
-
const AUDIO_CACHE_DIR = join(homedir(), '.config', 'neuralingual', 'audio');
|
|
270
221
|
voicesCmd
|
|
271
222
|
.command('show', { isDefault: true })
|
|
272
223
|
.description('List available voices')
|
|
@@ -357,6 +308,15 @@ voicesCmd
|
|
|
357
308
|
});
|
|
358
309
|
// ─── set (declarative YAML file) ────────────────────────────────────────────
|
|
359
310
|
const setCmd = program.command('set').description('Export/import a complete affirmation set as a YAML file');
|
|
311
|
+
/** Read all of stdin and return as a string. */
|
|
312
|
+
function readStdin() {
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
const chunks = [];
|
|
315
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
316
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
317
|
+
process.stdin.on('error', reject);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
360
320
|
/** Read content from --file <path>, --file - (stdin), or piped stdin. */
|
|
361
321
|
async function readContentFromFileOrStdin(opts) {
|
|
362
322
|
if (opts.file === '-') {
|
|
@@ -398,77 +358,6 @@ function buildRenderInputFromParsed(parsed, fallback) {
|
|
|
398
358
|
input.playAll = parsed.playAll;
|
|
399
359
|
return input;
|
|
400
360
|
}
|
|
401
|
-
/**
|
|
402
|
-
* Fetch set file data using user API.
|
|
403
|
-
* Maps the user intent detail shape to SetFileData.
|
|
404
|
-
*/
|
|
405
|
-
async function fetchSetFileData(client, intentId) {
|
|
406
|
-
const { intent } = await client.getIntent(intentId);
|
|
407
|
-
if (!intent) {
|
|
408
|
-
throw new Error(`Intent not found: ${intentId}`);
|
|
409
|
-
}
|
|
410
|
-
// Get latest affirmation set (first in the desc-ordered array)
|
|
411
|
-
const latestSet = intent.affirmationSets[0];
|
|
412
|
-
const affirmations = (latestSet?.affirmations ?? []).map((a, idx) => ({
|
|
413
|
-
id: a.id,
|
|
414
|
-
setId: latestSet?.id ?? '',
|
|
415
|
-
text: a.text,
|
|
416
|
-
tone: a.tone,
|
|
417
|
-
intensity: 3,
|
|
418
|
-
length: a.text.length < 60 ? 'short' : 'medium',
|
|
419
|
-
tags: [],
|
|
420
|
-
weight: 3,
|
|
421
|
-
isFavorite: false,
|
|
422
|
-
isEnabled: a.isEnabled,
|
|
423
|
-
orderIndex: idx,
|
|
424
|
-
createdAt: '',
|
|
425
|
-
updatedAt: '',
|
|
426
|
-
}));
|
|
427
|
-
// Get render config scoped to the latest affirmation set (matching the info command pattern).
|
|
428
|
-
// Without scoping, we could export a stale config from an older set.
|
|
429
|
-
const latestSetId = latestSet?.id;
|
|
430
|
-
const latestConfig = (latestSetId
|
|
431
|
-
? intent.renderConfigs.find((rc) => rc.affirmationSetId === latestSetId)
|
|
432
|
-
: intent.renderConfigs[0]) ?? null;
|
|
433
|
-
const renderConfig = latestConfig ? {
|
|
434
|
-
id: latestConfig.id,
|
|
435
|
-
intentId: intent.id,
|
|
436
|
-
affirmationSetId: latestConfig.affirmationSetId,
|
|
437
|
-
voiceId: latestConfig.voiceId,
|
|
438
|
-
voiceProvider: latestConfig.voiceProvider,
|
|
439
|
-
sessionContext: latestConfig.sessionContext,
|
|
440
|
-
paceWpm: latestConfig.paceWpm,
|
|
441
|
-
durationSeconds: latestConfig.durationSeconds,
|
|
442
|
-
backgroundAudioPath: latestConfig.backgroundAudioPath,
|
|
443
|
-
backgroundVolume: latestConfig.backgroundVolume,
|
|
444
|
-
affirmationRepeatCount: latestConfig.affirmationRepeatCount,
|
|
445
|
-
includePreamble: latestConfig.includePreamble,
|
|
446
|
-
playAll: latestConfig.playAll,
|
|
447
|
-
createdAt: latestConfig.createdAt,
|
|
448
|
-
updatedAt: latestConfig.updatedAt,
|
|
449
|
-
} : null;
|
|
450
|
-
// Map user intent detail to the Intent type expected by SetFileData.
|
|
451
|
-
// User intents don't have catalog fields — default them.
|
|
452
|
-
const mappedIntent = {
|
|
453
|
-
id: intent.id,
|
|
454
|
-
userId: '',
|
|
455
|
-
title: intent.title,
|
|
456
|
-
emoji: intent.emoji,
|
|
457
|
-
rawText: intent.rawText,
|
|
458
|
-
tonePreference: intent.tonePreference ?? null,
|
|
459
|
-
sessionContext: intent.sessionContext,
|
|
460
|
-
isCatalog: false,
|
|
461
|
-
catalogSlug: null,
|
|
462
|
-
catalogCategory: null,
|
|
463
|
-
catalogSubtitle: null,
|
|
464
|
-
catalogDescription: null,
|
|
465
|
-
catalogOrder: null,
|
|
466
|
-
createdAt: intent.createdAt,
|
|
467
|
-
updatedAt: intent.updatedAt,
|
|
468
|
-
archivedAt: null,
|
|
469
|
-
};
|
|
470
|
-
return { intent: mappedIntent, affirmations, renderConfig };
|
|
471
|
-
}
|
|
472
361
|
setCmd
|
|
473
362
|
.command('export <intent-id>')
|
|
474
363
|
.description('Export an affirmation set to YAML (stdout)')
|
|
@@ -575,7 +464,7 @@ setCmd
|
|
|
575
464
|
}
|
|
576
465
|
await createSetFromFile(client, parsed);
|
|
577
466
|
});
|
|
578
|
-
/** Create a new intent from a parsed set file. */
|
|
467
|
+
/** Create a new intent from a parsed set file using user API. */
|
|
579
468
|
async function createSetFromFile(client, parsed) {
|
|
580
469
|
if (!parsed.affirmations || parsed.affirmations.length === 0) {
|
|
581
470
|
console.error('Error: "affirmations" are required to create a new set');
|
|
@@ -631,8 +520,114 @@ async function createSetFromFile(client, parsed) {
|
|
|
631
520
|
printResult(err instanceof Error ? err.message : String(err), true);
|
|
632
521
|
}
|
|
633
522
|
}
|
|
523
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
524
|
+
// User-facing commands (JWT auth)
|
|
525
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
526
|
+
/** Get a user client from stored auth. Exits with helpful message if not logged in. */
|
|
527
|
+
function getUserClient() {
|
|
528
|
+
try {
|
|
529
|
+
return UserApiClient.fromAuth();
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
console.error('Not logged in. Run `neuralingual login` first.');
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Resolve a short/truncated intent ID to the full ID by fetching the user's library.
|
|
538
|
+
* Handles exact match, prefix match, and ambiguous matches (multiple prefix hits).
|
|
539
|
+
*/
|
|
540
|
+
async function resolveIntentId(client, shortId) {
|
|
541
|
+
const { items } = await client.getLibrary();
|
|
542
|
+
const exact = items.find((i) => i.intent.id === shortId);
|
|
543
|
+
if (exact)
|
|
544
|
+
return exact.intent.id;
|
|
545
|
+
const prefixMatches = items.filter((i) => i.intent.id.startsWith(shortId));
|
|
546
|
+
if (prefixMatches.length === 1)
|
|
547
|
+
return prefixMatches[0].intent.id;
|
|
548
|
+
if (prefixMatches.length > 1) {
|
|
549
|
+
console.error(`Error: ambiguous ID "${shortId}" matches ${prefixMatches.length} intents:`);
|
|
550
|
+
for (const m of prefixMatches) {
|
|
551
|
+
console.error(` ${m.intent.id.slice(0, 12)} ${m.intent.title ?? '(untitled)'}`);
|
|
552
|
+
}
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
console.error(`Error: no practice set found matching "${shortId}"`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Fetch set file data using user API.
|
|
560
|
+
* Maps the user intent detail shape to SetFileData.
|
|
561
|
+
*/
|
|
562
|
+
async function fetchSetFileData(client, intentId) {
|
|
563
|
+
const { intent } = await client.getIntent(intentId);
|
|
564
|
+
if (!intent) {
|
|
565
|
+
throw new Error(`Intent not found: ${intentId}`);
|
|
566
|
+
}
|
|
567
|
+
// Get latest affirmation set (first in the desc-ordered array)
|
|
568
|
+
const latestSet = intent.affirmationSets[0];
|
|
569
|
+
const affirmations = (latestSet?.affirmations ?? []).map((a, idx) => ({
|
|
570
|
+
id: a.id,
|
|
571
|
+
setId: latestSet?.id ?? '',
|
|
572
|
+
text: a.text,
|
|
573
|
+
tone: a.tone,
|
|
574
|
+
intensity: 3,
|
|
575
|
+
length: a.text.length < 60 ? 'short' : 'medium',
|
|
576
|
+
tags: [],
|
|
577
|
+
weight: 3,
|
|
578
|
+
isFavorite: false,
|
|
579
|
+
isEnabled: a.isEnabled,
|
|
580
|
+
orderIndex: idx,
|
|
581
|
+
createdAt: '',
|
|
582
|
+
updatedAt: '',
|
|
583
|
+
}));
|
|
584
|
+
// Get render config scoped to the latest affirmation set (matching the info command pattern).
|
|
585
|
+
// Without scoping, we could export a stale config from an older set.
|
|
586
|
+
const latestSetId = latestSet?.id;
|
|
587
|
+
const latestConfig = (latestSetId
|
|
588
|
+
? intent.renderConfigs.find((rc) => rc.affirmationSetId === latestSetId)
|
|
589
|
+
: intent.renderConfigs[0]) ?? null;
|
|
590
|
+
const renderConfig = latestConfig ? {
|
|
591
|
+
id: latestConfig.id,
|
|
592
|
+
intentId: intent.id,
|
|
593
|
+
affirmationSetId: latestConfig.affirmationSetId,
|
|
594
|
+
voiceId: latestConfig.voiceId,
|
|
595
|
+
voiceProvider: latestConfig.voiceProvider,
|
|
596
|
+
sessionContext: latestConfig.sessionContext,
|
|
597
|
+
paceWpm: latestConfig.paceWpm,
|
|
598
|
+
durationSeconds: latestConfig.durationSeconds,
|
|
599
|
+
backgroundAudioPath: latestConfig.backgroundAudioPath,
|
|
600
|
+
backgroundVolume: latestConfig.backgroundVolume,
|
|
601
|
+
affirmationRepeatCount: latestConfig.affirmationRepeatCount,
|
|
602
|
+
includePreamble: latestConfig.includePreamble,
|
|
603
|
+
playAll: latestConfig.playAll,
|
|
604
|
+
createdAt: latestConfig.createdAt,
|
|
605
|
+
updatedAt: latestConfig.updatedAt,
|
|
606
|
+
} : null;
|
|
607
|
+
// Map user intent detail to the Intent type expected by SetFileData.
|
|
608
|
+
// User intents don't have catalog fields — default them.
|
|
609
|
+
const mappedIntent = {
|
|
610
|
+
id: intent.id,
|
|
611
|
+
userId: '',
|
|
612
|
+
title: intent.title,
|
|
613
|
+
emoji: intent.emoji,
|
|
614
|
+
rawText: intent.rawText,
|
|
615
|
+
tonePreference: intent.tonePreference ?? null,
|
|
616
|
+
sessionContext: intent.sessionContext,
|
|
617
|
+
isCatalog: false,
|
|
618
|
+
catalogSlug: null,
|
|
619
|
+
catalogCategory: null,
|
|
620
|
+
catalogSubtitle: null,
|
|
621
|
+
catalogDescription: null,
|
|
622
|
+
catalogOrder: null,
|
|
623
|
+
createdAt: intent.createdAt,
|
|
624
|
+
updatedAt: intent.updatedAt,
|
|
625
|
+
archivedAt: null,
|
|
626
|
+
};
|
|
627
|
+
return { intent: mappedIntent, affirmations, renderConfig };
|
|
628
|
+
}
|
|
634
629
|
/**
|
|
635
|
-
* Apply a parsed set file
|
|
630
|
+
* Apply a parsed set file using user API.
|
|
636
631
|
*/
|
|
637
632
|
async function applySetFile(client, intentId, content, originalData) {
|
|
638
633
|
let parsed;
|
|
@@ -662,7 +657,7 @@ async function applySetFile(client, intentId, content, originalData) {
|
|
|
662
657
|
await client.updateIntent(intentId, intentUpdates);
|
|
663
658
|
changes.push(`intent: updated ${Object.keys(intentUpdates).join(', ')}`);
|
|
664
659
|
}
|
|
665
|
-
// 2. Affirmation sync (declarative
|
|
660
|
+
// 2. Affirmation sync (declarative)
|
|
666
661
|
if (parsed.affirmations && parsed.affirmations.length > 0) {
|
|
667
662
|
const missingIds = parsed.affirmations.filter((a) => !a.id);
|
|
668
663
|
if (missingIds.length > 0 && originalData.affirmations.length > 0) {
|
|
@@ -849,7 +844,7 @@ async function browserLogin(env) {
|
|
|
849
844
|
}
|
|
850
845
|
program
|
|
851
846
|
.command('login')
|
|
852
|
-
.description('Log in to Neuralingual
|
|
847
|
+
.description('Log in to Neuralingual via Apple Sign-In (opens browser)')
|
|
853
848
|
.option('--env <env>', 'API environment: dev or production', 'production')
|
|
854
849
|
.action(async (opts) => {
|
|
855
850
|
const env = opts.env;
|
|
@@ -1260,6 +1255,7 @@ program
|
|
|
1260
1255
|
}
|
|
1261
1256
|
});
|
|
1262
1257
|
// ─── play ──────────────────────────────────────────────────────────────────
|
|
1258
|
+
const AUDIO_CACHE_DIR = join(homedir(), '.config', 'neuralingual', 'audio');
|
|
1263
1259
|
program
|
|
1264
1260
|
.command('play <intent-id>')
|
|
1265
1261
|
.description('Download rendered audio (prints file path). Use --open to launch in default player.')
|
|
@@ -1351,6 +1347,16 @@ program
|
|
|
1351
1347
|
}
|
|
1352
1348
|
});
|
|
1353
1349
|
// ─── delete ────────────────────────────────────────────────────────────────
|
|
1350
|
+
/** Prompt the user for input on stdin. */
|
|
1351
|
+
function prompt(question) {
|
|
1352
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1353
|
+
return new Promise((resolve) => {
|
|
1354
|
+
rl.question(question, (answer) => {
|
|
1355
|
+
rl.close();
|
|
1356
|
+
resolve(answer.trim());
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1354
1360
|
program
|
|
1355
1361
|
.command('delete <intent-id>')
|
|
1356
1362
|
.description('Delete a practice set from your library')
|