playable-lessons 0.2.2 → 0.6.2

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/out/cli/index.js CHANGED
@@ -10,6 +10,15 @@ const promises_1 = require("fs/promises");
10
10
  const path_1 = require("path");
11
11
  const storyExport_js_1 = require("../shared/storyExport.js");
12
12
  const generate_js_1 = require("../shared/generate.js");
13
+ const imageClient_js_1 = require("../shared/imageClient.js");
14
+ const flashcardExport_js_1 = require("../shared/flashcardExport.js");
15
+ const quizExport_js_1 = require("../shared/quizExport.js");
16
+ const summaryExport_js_1 = require("../shared/summaryExport.js");
17
+ const aiTaskExport_js_1 = require("../shared/aiTaskExport.js");
18
+ const caseStudyExport_js_1 = require("../shared/caseStudyExport.js");
19
+ const planExport_js_1 = require("../shared/planExport.js");
20
+ const h5pExporter_js_1 = require("../shared/h5pExporter.js");
21
+ const version_generated_js_1 = require("../shared/version.generated.js");
13
22
  /** Pick a provider from the environment when --provider isn't given. */
14
23
  function resolveProviderFromEnv() {
15
24
  if (process.env.ANTHROPIC_API_KEY)
@@ -62,6 +71,47 @@ function buildCliConfig(o) {
62
71
  function slug(s) {
63
72
  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'story';
64
73
  }
74
+ /**
75
+ * Build an ImageProviderConfig from CLI flags + environment. Provider default:
76
+ * openai when OPENAI_API_KEY is set, gemini when GEMINI_API_KEY/GOOGLE_API_KEY
77
+ * is set, custom when --image-base-url is given.
78
+ */
79
+ function buildCliImageConfig(o) {
80
+ const provider = o.imageProvider ||
81
+ (process.env.OPENAI_API_KEY ? 'openai' : process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY ? 'gemini' : o.imageBaseUrl ? 'custom' : undefined);
82
+ if (!provider) {
83
+ throw new Error('--images needs an image provider: set OPENAI_API_KEY or GEMINI_API_KEY, or pass --image-provider custom --image-base-url <url> --image-model <id>');
84
+ }
85
+ const config = { provider, model: o.imageModel || undefined };
86
+ switch (provider) {
87
+ case 'openai':
88
+ config.apiKey = process.env.OPENAI_API_KEY;
89
+ break;
90
+ case 'gemini':
91
+ config.apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
92
+ break;
93
+ case 'custom':
94
+ config.baseUrl = o.imageBaseUrl;
95
+ config.apiKey = process.env.PLAYABLE_LESSONS_IMAGE_API_KEY || process.env.OPENAI_API_KEY || '';
96
+ break;
97
+ case 'swarmui':
98
+ config.baseUrl = o.imageBaseUrl;
99
+ config.apiKey = process.env.PLAYABLE_LESSONS_IMAGE_API_KEY || process.env.SWARMUI_TOKEN || '';
100
+ break;
101
+ }
102
+ if (!(0, imageClient_js_1.isImageProviderConfigured)(config)) {
103
+ throw new Error(`Image provider "${provider}" is not fully configured (missing ${provider === 'custom' || provider === 'swarmui' ? '--image-base-url' : 'API key'})`);
104
+ }
105
+ return config;
106
+ }
107
+ /** Decode a data URL into its binary payload. */
108
+ function dataUrlToBuffer(dataUrl) {
109
+ const m = dataUrl.match(/^data:([^;]+);base64,(.+)$/s);
110
+ if (!m)
111
+ throw new Error('Not a base64 data URL');
112
+ const ext = m[1] === 'image/jpeg' ? 'jpg' : m[1] === 'image/webp' ? 'webp' : 'png';
113
+ return { buffer: Buffer.from(m[2], 'base64'), ext };
114
+ }
65
115
  async function readStdin() {
66
116
  if (process.stdin.isTTY)
67
117
  return '';
@@ -90,7 +140,7 @@ async function readStdin() {
90
140
  return yargs
91
141
  .option('input', { alias: 'i', type: 'string', demandOption: true, describe: 'Path to .ink file' })
92
142
  .option('output', { alias: 'o', type: 'string', demandOption: true, describe: 'Output directory' })
93
- .option('format', { alias: 'f', type: 'string', default: 'html', describe: 'Export format: html, ink, json' })
143
+ .option('format', { alias: 'f', type: 'string', default: 'html', describe: 'Export format: html, ink, json, h5p (h5p requires a story with no variables/conditionals)' })
94
144
  .option('title', { alias: 't', type: 'string', default: 'Story', describe: 'Story title' });
95
145
  }, async (argv) => {
96
146
  try {
@@ -122,6 +172,13 @@ async function readStdin() {
122
172
  console.log(`Exported Ink: ${outPath}`);
123
173
  break;
124
174
  }
175
+ case 'h5p': {
176
+ const bytes = (0, h5pExporter_js_1.exportH5P)(source, title);
177
+ const outPath = (0, path_1.join)(outputDir, `${baseName}.h5p`);
178
+ await (0, promises_1.writeFile)(outPath, Buffer.from(bytes));
179
+ console.log(`Exported H5P: ${outPath}`);
180
+ break;
181
+ }
125
182
  default:
126
183
  console.warn(`Unknown format: ${format}`);
127
184
  }
@@ -132,7 +189,7 @@ async function readStdin() {
132
189
  process.exit(1);
133
190
  }
134
191
  })
135
- .command('generate', 'Generate an Ink story from source material using AI', (yargs) => {
192
+ .command('generate', 'Generate an Ink story or flashcards from source material using AI', (yargs) => {
136
193
  return yargs
137
194
  .option('input', { alias: 'i', type: 'string', describe: 'Path to source material (text/markdown). Omit to use --topic or stdin.' })
138
195
  .option('topic', { type: 'string', describe: 'A topic or brief to generate from (instead of --input)' })
@@ -142,13 +199,22 @@ async function readStdin() {
142
199
  .option('base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible endpoint' })
143
200
  .option('ollama-url', { type: 'string', default: 'http://localhost:11434', describe: 'Ollama base URL' })
144
201
  .option('mode', { type: 'string', default: 'topic', describe: 'Input mode: topic | lesson | methodology | case-study | lecture-notes | scenario' })
202
+ .option('target', { type: 'string', default: 'story', describe: 'What to generate: story (interactive fiction) | flashcards | quiz | summary | ai-task | case-study' })
203
+ .option('cards', { type: 'number', default: 12, describe: 'Number of items to generate — flashcards, quiz questions, summary key points, or AI-collaboration tasks (target=flashcards|quiz|summary|ai-task)' })
204
+ .option('depth', { type: 'string', default: 'complete', describe: 'Case-study depth: idea | outline | complete (target=case-study)' })
145
205
  .option('length', { alias: 'l', type: 'string', default: 'medium', describe: 'Story length: short | medium | long' })
206
+ .option('style', { type: 'string', default: 'stateful', describe: 'Story branching style: stateful (Ink variables + conditional text) | branching (pure tree, H5P/LMS-convertible)' })
207
+ .option('images', { type: 'boolean', default: false, describe: 'Generate an illustration for every scene (story target only). Images are embedded in the HTML export and written to an images/ folder.' })
208
+ .option('image-style', { type: 'string', default: 'cartoon', describe: 'Scene image style: cartoon | photorealistic' })
209
+ .option('image-provider', { type: 'string', describe: 'openai | gemini | custom | swarmui (default: inferred from OPENAI_API_KEY / GEMINI_API_KEY / --image-base-url)' })
210
+ .option('image-model', { type: 'string', describe: 'Image model id (defaults: gpt-image-1 | imagen-4.0-fast-generate-001; required by custom)' })
211
+ .option('image-base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible /images/generations endpoint or a SwarmUI server' })
146
212
  .option('tone', { type: 'string', default: 'professional', describe: 'Narrative tone' })
147
213
  .option('protagonist', { type: 'string', default: 'the reader', describe: 'Protagonist type' })
148
214
  .option('answers', { type: 'string', describe: 'Optional answers to clarifying questions' })
149
215
  .option('retries', { type: 'number', default: 3, describe: 'Max compile attempts, with AI-assisted fixes between them (raise for weaker local models)' })
150
- .option('format', { alias: 'f', type: 'string', default: 'ink,html', describe: 'Output formats: ink, html, json' })
151
- .option('title', { alias: 't', type: 'string', describe: 'Story title (defaults to the input/topic name)' });
216
+ .option('format', { alias: 'f', type: 'string', describe: 'Output formats. story: ink|html|json|h5p (default ink,html; h5p requires --style branching). flashcards: csv|html|anki|json (default csv,html). quiz: html|txt|json (default html,json). summary: html|txt|json (default html,txt). ai-task: html|txt|json (default html,txt). case-study: html|txt|json (default html,txt).' })
217
+ .option('title', { alias: 't', type: 'string', describe: 'Title for the story, flashcard deck, quiz, summary, AI-task set, or case study (defaults to the input/topic name)' });
152
218
  }, async (argv) => {
153
219
  try {
154
220
  // Resolve the source material: --input file, --topic string, or stdin.
@@ -174,44 +240,276 @@ async function readStdin() {
174
240
  baseUrl: argv['base-url'],
175
241
  ollamaUrl: argv['ollama-url'] || 'http://localhost:11434'
176
242
  });
177
- console.error(`Generating with ${config.provider} (${config.model})…`);
178
- const result = await (0, generate_js_1.generateInk)({
179
- inputMode: argv.mode,
180
- inputText,
181
- storyLength: argv.length,
182
- protagonistType: argv.protagonist,
183
- tone: argv.tone,
184
- answers: argv.answers
185
- }, config, { log: (m) => console.error(m), maxCompileRetries: argv.retries });
186
243
  const outputDir = argv.output;
187
244
  await (0, promises_1.mkdir)(outputDir, { recursive: true });
188
245
  const title = argv.title || base;
189
- const formats = argv.format.split(',').map((f) => f.trim()).filter(Boolean);
190
- if (!formats.includes('ink'))
191
- formats.unshift('ink'); // always keep the source
192
- for (const format of formats) {
193
- switch (format) {
194
- case 'ink': {
195
- const outPath = (0, path_1.join)(outputDir, `${base}.ink`);
196
- await (0, promises_1.writeFile)(outPath, result.inkSource);
197
- console.log(`Wrote ${outPath}`);
198
- break;
246
+ const target = argv.target || 'story';
247
+ if (target !== 'story' && target !== 'flashcards' && target !== 'quiz' && target !== 'summary' && target !== 'ai-task' && target !== 'case-study') {
248
+ throw new Error(`Unknown --target "${target}". Use story, flashcards, quiz, summary, ai-task, or case-study.`);
249
+ }
250
+ const formatRaw = argv.format;
251
+ const formats = formatRaw
252
+ ? formatRaw.split(',').map((f) => f.trim()).filter(Boolean)
253
+ : target === 'flashcards'
254
+ ? ['csv', 'html']
255
+ : target === 'quiz'
256
+ ? ['html', 'json']
257
+ : target === 'summary'
258
+ ? ['html', 'txt']
259
+ : target === 'ai-task'
260
+ ? ['html', 'txt']
261
+ : target === 'case-study'
262
+ ? ['html', 'txt']
263
+ : ['ink', 'html'];
264
+ if (target === 'flashcards') {
265
+ console.error(`Generating flashcards with ${config.provider} (${config.model})…`);
266
+ const result = await (0, generate_js_1.generateFlashcards)({
267
+ inputMode: argv.mode,
268
+ inputText,
269
+ cardCount: argv.cards,
270
+ tone: argv.tone
271
+ }, config, { log: (m) => console.error(m) });
272
+ for (const format of formats) {
273
+ switch (format) {
274
+ case 'csv': {
275
+ const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.csv`);
276
+ await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toCSV)(result));
277
+ console.log(`Wrote ${outPath}`);
278
+ break;
279
+ }
280
+ case 'html': {
281
+ const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.html`);
282
+ await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toStandaloneHTML)(result, title));
283
+ console.log(`Wrote ${outPath}`);
284
+ break;
285
+ }
286
+ case 'anki': {
287
+ const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.txt`);
288
+ await (0, promises_1.writeFile)(outPath, (0, flashcardExport_js_1.toAnkiTSV)(result));
289
+ console.log(`Wrote ${outPath}`);
290
+ break;
291
+ }
292
+ case 'json': {
293
+ const outPath = (0, path_1.join)(outputDir, `${base}.flashcards.json`);
294
+ await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
295
+ console.log(`Wrote ${outPath}`);
296
+ break;
297
+ }
298
+ default:
299
+ console.warn(`Unknown format: ${format}`);
199
300
  }
200
- case 'json': {
201
- const outPath = (0, path_1.join)(outputDir, `${base}.ink.json`);
202
- await (0, promises_1.writeFile)(outPath, result.compiledJson);
203
- console.log(`Wrote ${outPath}`);
204
- break;
301
+ }
302
+ }
303
+ else if (target === 'quiz') {
304
+ console.error(`Generating a quiz with ${config.provider} (${config.model})…`);
305
+ const result = await (0, generate_js_1.generateQuiz)({
306
+ inputMode: argv.mode,
307
+ inputText,
308
+ questionCount: argv.cards,
309
+ tone: argv.tone
310
+ }, config, { log: (m) => console.error(m) });
311
+ for (const format of formats) {
312
+ switch (format) {
313
+ case 'html': {
314
+ const outPath = (0, path_1.join)(outputDir, `${base}.quiz.html`);
315
+ await (0, promises_1.writeFile)(outPath, (0, quizExport_js_1.toStandaloneHTML)(result, title));
316
+ console.log(`Wrote ${outPath}`);
317
+ break;
318
+ }
319
+ case 'json': {
320
+ const outPath = (0, path_1.join)(outputDir, `${base}.quiz.json`);
321
+ await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
322
+ console.log(`Wrote ${outPath}`);
323
+ break;
324
+ }
325
+ case 'txt': {
326
+ const outPath = (0, path_1.join)(outputDir, `${base}.quiz.txt`);
327
+ await (0, promises_1.writeFile)(outPath, (0, quizExport_js_1.toPlainText)(result, title));
328
+ console.log(`Wrote ${outPath}`);
329
+ break;
330
+ }
331
+ default:
332
+ console.warn(`Unknown format: ${format}`);
205
333
  }
206
- case 'html': {
207
- const html = await (0, storyExport_js_1.exportStandaloneHTML)(result.inkSource, title);
208
- const outPath = (0, path_1.join)(outputDir, `${base}.html`);
209
- await (0, promises_1.writeFile)(outPath, html);
210
- console.log(`Wrote ${outPath}`);
211
- break;
334
+ }
335
+ }
336
+ else if (target === 'summary') {
337
+ console.error(`Generating a summary with ${config.provider} (${config.model})…`);
338
+ const result = await (0, generate_js_1.generateSummary)({
339
+ inputMode: argv.mode,
340
+ inputText,
341
+ keyPointCount: argv.cards,
342
+ tone: argv.tone
343
+ }, config, { log: (m) => console.error(m) });
344
+ for (const format of formats) {
345
+ switch (format) {
346
+ case 'html': {
347
+ const outPath = (0, path_1.join)(outputDir, `${base}.summary.html`);
348
+ await (0, promises_1.writeFile)(outPath, (0, summaryExport_js_1.toStandaloneHTML)(result, title));
349
+ console.log(`Wrote ${outPath}`);
350
+ break;
351
+ }
352
+ case 'txt': {
353
+ const outPath = (0, path_1.join)(outputDir, `${base}.summary.txt`);
354
+ await (0, promises_1.writeFile)(outPath, (0, summaryExport_js_1.toPlainText)(result, title));
355
+ console.log(`Wrote ${outPath}`);
356
+ break;
357
+ }
358
+ case 'json': {
359
+ const outPath = (0, path_1.join)(outputDir, `${base}.summary.json`);
360
+ await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
361
+ console.log(`Wrote ${outPath}`);
362
+ break;
363
+ }
364
+ default:
365
+ console.warn(`Unknown format: ${format}`);
366
+ }
367
+ }
368
+ }
369
+ else if (target === 'ai-task') {
370
+ console.error(`Generating AI-collaboration tasks with ${config.provider} (${config.model})…`);
371
+ const result = await (0, generate_js_1.generateAiTask)({
372
+ inputMode: argv.mode,
373
+ inputText,
374
+ taskCount: argv.cards,
375
+ tone: argv.tone
376
+ }, config, { log: (m) => console.error(m) });
377
+ for (const format of formats) {
378
+ switch (format) {
379
+ case 'html': {
380
+ const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.html`);
381
+ await (0, promises_1.writeFile)(outPath, (0, aiTaskExport_js_1.toStandaloneHTML)(result, title));
382
+ console.log(`Wrote ${outPath}`);
383
+ break;
384
+ }
385
+ case 'txt': {
386
+ const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.txt`);
387
+ await (0, promises_1.writeFile)(outPath, (0, aiTaskExport_js_1.toPlainText)(result, title));
388
+ console.log(`Wrote ${outPath}`);
389
+ break;
390
+ }
391
+ case 'json': {
392
+ const outPath = (0, path_1.join)(outputDir, `${base}.ai-task.json`);
393
+ await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
394
+ console.log(`Wrote ${outPath}`);
395
+ break;
396
+ }
397
+ default:
398
+ console.warn(`Unknown format: ${format}`);
399
+ }
400
+ }
401
+ }
402
+ else if (target === 'case-study') {
403
+ console.error(`Generating a case study with ${config.provider} (${config.model})…`);
404
+ const result = await (0, generate_js_1.generateCaseStudy)({
405
+ inputMode: argv.mode,
406
+ inputText,
407
+ depth: (argv.depth || 'complete'),
408
+ tone: argv.tone
409
+ }, config, { log: (m) => console.error(m) });
410
+ for (const format of formats) {
411
+ switch (format) {
412
+ case 'html': {
413
+ const outPath = (0, path_1.join)(outputDir, `${base}.case-study.html`);
414
+ await (0, promises_1.writeFile)(outPath, (0, caseStudyExport_js_1.toStandaloneHTML)(result, title));
415
+ console.log(`Wrote ${outPath}`);
416
+ break;
417
+ }
418
+ case 'txt': {
419
+ const outPath = (0, path_1.join)(outputDir, `${base}.case-study.txt`);
420
+ await (0, promises_1.writeFile)(outPath, (0, caseStudyExport_js_1.toPlainText)(result, title));
421
+ console.log(`Wrote ${outPath}`);
422
+ break;
423
+ }
424
+ case 'json': {
425
+ const outPath = (0, path_1.join)(outputDir, `${base}.case-study.json`);
426
+ await (0, promises_1.writeFile)(outPath, JSON.stringify(result, null, 2));
427
+ console.log(`Wrote ${outPath}`);
428
+ break;
429
+ }
430
+ default:
431
+ console.warn(`Unknown format: ${format}`);
432
+ }
433
+ }
434
+ }
435
+ else {
436
+ console.error(`Generating with ${config.provider} (${config.model})…`);
437
+ const wantImages = argv.images;
438
+ // Fail fast on a misconfigured image provider, before story generation.
439
+ const imageConfig = wantImages
440
+ ? buildCliImageConfig({
441
+ imageProvider: argv['image-provider'],
442
+ imageModel: argv['image-model'],
443
+ imageBaseUrl: argv['image-base-url']
444
+ })
445
+ : undefined;
446
+ const result = await (0, generate_js_1.generateInk)({
447
+ inputMode: argv.mode,
448
+ inputText,
449
+ storyLength: argv.length,
450
+ branchingStyle: argv.style === 'branching' ? 'branching' : 'stateful',
451
+ protagonistType: argv.protagonist,
452
+ tone: argv.tone,
453
+ answers: argv.answers,
454
+ sceneImages: wantImages
455
+ }, config, { log: (m) => console.error(m), maxCompileRetries: argv.retries });
456
+ // Resolve # IMAGE_PROMPT: tags to real images (embedded in the HTML
457
+ // export and written to an images/ folder, slide-stream style).
458
+ let sceneImages;
459
+ if (wantImages && imageConfig) {
460
+ const imageStyle = argv['image-style'] === 'photorealistic' ? 'photorealistic' : 'cartoon';
461
+ console.error(`Generating scene images with ${imageConfig.provider} (${imageConfig.model || 'default model'})…`);
462
+ sceneImages = await (0, generate_js_1.generateSceneImages)(result.inkSource, imageConfig, {
463
+ style: imageStyle,
464
+ log: (m) => console.error(m)
465
+ });
466
+ const imagesDir = (0, path_1.join)(outputDir, 'images');
467
+ await (0, promises_1.mkdir)(imagesDir, { recursive: true });
468
+ let n = 0;
469
+ for (const dataUrl of Object.values(sceneImages)) {
470
+ n++;
471
+ const { buffer, ext } = dataUrlToBuffer(dataUrl);
472
+ await (0, promises_1.writeFile)((0, path_1.join)(imagesDir, `scene_${n}.${ext}`), buffer);
473
+ }
474
+ console.log(`Wrote ${n} scene image(s) to ${imagesDir}`);
475
+ }
476
+ const storyFormats = formats.includes('ink') ? formats : ['ink', ...formats];
477
+ for (const format of storyFormats) {
478
+ switch (format) {
479
+ case 'ink': {
480
+ const outPath = (0, path_1.join)(outputDir, `${base}.ink`);
481
+ await (0, promises_1.writeFile)(outPath, result.inkSource);
482
+ console.log(`Wrote ${outPath}`);
483
+ break;
484
+ }
485
+ case 'json': {
486
+ const outPath = (0, path_1.join)(outputDir, `${base}.ink.json`);
487
+ await (0, promises_1.writeFile)(outPath, result.compiledJson);
488
+ console.log(`Wrote ${outPath}`);
489
+ break;
490
+ }
491
+ case 'html': {
492
+ const html = await (0, storyExport_js_1.exportStandaloneHTML)(result.inkSource, title, sceneImages);
493
+ const outPath = (0, path_1.join)(outputDir, `${base}.html`);
494
+ await (0, promises_1.writeFile)(outPath, html);
495
+ console.log(`Wrote ${outPath}`);
496
+ break;
497
+ }
498
+ case 'h5p': {
499
+ try {
500
+ const bytes = (0, h5pExporter_js_1.exportH5P)(result.inkSource, title);
501
+ const outPath = (0, path_1.join)(outputDir, `${base}.h5p`);
502
+ await (0, promises_1.writeFile)(outPath, Buffer.from(bytes));
503
+ console.log(`Wrote ${outPath}`);
504
+ }
505
+ catch (err) {
506
+ console.warn(`Skipped h5p: ${err instanceof Error ? err.message : err}`);
507
+ }
508
+ break;
509
+ }
510
+ default:
511
+ console.warn(`Unknown format: ${format}`);
212
512
  }
213
- default:
214
- console.warn(`Unknown format: ${format}`);
215
513
  }
216
514
  }
217
515
  console.log('Done.');
@@ -220,8 +518,88 @@ async function readStdin() {
220
518
  console.error('Generation failed:', err instanceof Error ? err.message : err);
221
519
  process.exit(1);
222
520
  }
521
+ })
522
+ .command('plan', 'Recommend a set of outputs for source material (optionally generate them all)', (yargs) => {
523
+ return yargs
524
+ .option('input', { alias: 'i', type: 'string', describe: 'Path to source material. Omit to use --topic or stdin.' })
525
+ .option('topic', { type: 'string', describe: 'A topic or brief to plan from (instead of --input)' })
526
+ .option('output', { alias: 'o', type: 'string', demandOption: true, describe: 'Output directory' })
527
+ .option('provider', { alias: 'p', type: 'string', describe: 'claude | openai | ollama | custom (default: inferred from env)' })
528
+ .option('model', { alias: 'm', type: 'string', describe: 'Model id' })
529
+ .option('base-url', { type: 'string', describe: 'Base URL for a custom OpenAI-compatible endpoint' })
530
+ .option('ollama-url', { type: 'string', default: 'http://localhost:11434', describe: 'Ollama base URL' })
531
+ .option('mode', { type: 'string', default: 'topic', describe: 'Input mode: topic | lesson | methodology | case-study | lecture-notes | scenario' })
532
+ .option('tone', { type: 'string', default: 'professional', describe: 'Narrative tone' })
533
+ .option('style', { type: 'string', default: 'stateful', describe: 'Story branching style when applying: stateful | branching' })
534
+ .option('apply', { type: 'boolean', default: false, describe: 'Also generate every recommended output' })
535
+ .option('format', { alias: 'f', type: 'string', default: 'html,json', describe: 'When applying: artifact formats html|json' });
536
+ }, async (argv) => {
537
+ try {
538
+ let inputText = '';
539
+ let base = 'plan';
540
+ if (argv.input) {
541
+ inputText = await (0, promises_1.readFile)(argv.input, 'utf-8');
542
+ base = (0, path_1.basename)(argv.input).replace(/\.[^.]+$/, '');
543
+ }
544
+ else if (argv.topic) {
545
+ inputText = argv.topic;
546
+ base = slug(argv.topic);
547
+ }
548
+ else {
549
+ inputText = await readStdin();
550
+ }
551
+ if (!inputText.trim()) {
552
+ throw new Error('No source material — provide --input <file>, --topic "<brief>", or pipe text via stdin');
553
+ }
554
+ const config = buildCliConfig({
555
+ provider: argv.provider,
556
+ model: argv.model,
557
+ baseUrl: argv['base-url'],
558
+ ollamaUrl: argv['ollama-url'] || 'http://localhost:11434'
559
+ });
560
+ const outputDir = argv.output;
561
+ await (0, promises_1.mkdir)(outputDir, { recursive: true });
562
+ const tone = argv.tone;
563
+ const log = (m) => console.error(m);
564
+ console.error(`Planning with ${config.provider} (${config.model})…`);
565
+ const plan = await (0, generate_js_1.generatePlan)({ inputMode: argv.mode, inputText, tone }, config, { log });
566
+ await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.json`), JSON.stringify(plan, null, 2));
567
+ await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.txt`), (0, planExport_js_1.toPlainText)(plan));
568
+ await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.plan.html`), (0, planExport_js_1.toStandaloneHTML)(plan, plan.title || base));
569
+ console.log(`Wrote plan: ${plan.recommendations.length} recommendation(s).`);
570
+ if (argv.apply) {
571
+ const formats = argv.format.split(',').map((f) => f.trim()).filter(Boolean);
572
+ const artifacts = await (0, generate_js_1.applyPlan)(plan, { inputMode: argv.mode, inputText, tone, branchingStyle: argv.style === 'branching' ? 'branching' : 'stateful' }, config, { log });
573
+ const write = async (kind, html, json) => {
574
+ if (formats.includes('html') && html) {
575
+ await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.${kind}.html`), html);
576
+ }
577
+ if (formats.includes('json')) {
578
+ await (0, promises_1.writeFile)((0, path_1.join)(outputDir, `${base}.${kind}.json`), JSON.stringify(json, null, 2));
579
+ }
580
+ };
581
+ if (artifacts.story)
582
+ await write('story', await (0, storyExport_js_1.exportStandaloneHTML)(artifacts.story.inkSource, base), artifacts.story);
583
+ if (artifacts.flashcards)
584
+ await write('flashcards', (0, flashcardExport_js_1.toStandaloneHTML)(artifacts.flashcards, base), artifacts.flashcards);
585
+ if (artifacts.quiz)
586
+ await write('quiz', (0, quizExport_js_1.toStandaloneHTML)(artifacts.quiz, base), artifacts.quiz);
587
+ if (artifacts.summary)
588
+ await write('summary', (0, summaryExport_js_1.toStandaloneHTML)(artifacts.summary, base), artifacts.summary);
589
+ if (artifacts.aiTask)
590
+ await write('ai-task', (0, aiTaskExport_js_1.toStandaloneHTML)(artifacts.aiTask, base), artifacts.aiTask);
591
+ if (artifacts.caseStudy)
592
+ await write('case-study', (0, caseStudyExport_js_1.toStandaloneHTML)(artifacts.caseStudy, base), artifacts.caseStudy);
593
+ console.log('Applied: generated every recommended output.');
594
+ }
595
+ console.log('Done.');
596
+ }
597
+ catch (err) {
598
+ console.error('Planning failed:', err instanceof Error ? err.message : err);
599
+ process.exit(1);
600
+ }
223
601
  })
224
602
  .demandCommand(1, 'You must specify a command')
225
603
  .help()
226
- .version('0.2.2')
604
+ .version(version_generated_js_1.APP_VERSION)
227
605
  .parse();