@hraness/slopcamera 3.8.0 → 3.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 (35) hide show
  1. package/NOTICE.md +14 -0
  2. package/README.md +294 -298
  3. package/apps/desktop/application/html-overlay-integrity.ts +27 -4
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/application/operations/spatial-render.ts +1 -1
  6. package/apps/desktop/cli/capability-manifest.ts +15 -2
  7. package/apps/desktop/cli/commands.ts +1 -1
  8. package/apps/desktop/cli/help.ts +23 -4
  9. package/apps/desktop/cli/portable-surface.ts +1 -0
  10. package/apps/desktop/cli/verbs.ts +4 -0
  11. package/apps/desktop/dist/cli/main.js +306 -284
  12. package/dist/cli.js +24 -4
  13. package/dist/{index-842t02pp.js → index-m6g0ca9b.js} +1 -1
  14. package/dist/{index-y8spxs77.js → index-mgychzh3.js} +6 -3
  15. package/dist/{index-k9bjw80z.js → index-rxx27q8b.js} +4 -4
  16. package/dist/index.js +1 -1
  17. package/dist/operations.js +1 -1
  18. package/dist/workflow.js +1 -1
  19. package/docs/README.md +4 -1
  20. package/package.json +5 -3
  21. package/skills/slopcamera/SKILL.md +2 -0
  22. package/skills/slopcamera/references/customization.md +8 -1
  23. package/skills/slopcamera/references/icon-place.md +76 -0
  24. package/skills/slopcamera/references/install.md +2 -2
  25. package/skills/slopcamera/references/music-video.md +10 -2
  26. package/skills/slopcamera/references/soundtracks.md +58 -0
  27. package/src/bounded-file.ts +135 -0
  28. package/src/cli.ts +191 -0
  29. package/src/icon-scene.ts +695 -0
  30. package/src/index.ts +2 -0
  31. package/src/mcp/tools.ts +311 -0
  32. package/src/mcp/types.ts +4 -0
  33. package/src/operations.ts +306 -0
  34. package/src/soundtrack.ts +489 -0
  35. package/src/version.ts +1 -1
package/src/cli.ts CHANGED
@@ -42,6 +42,12 @@ import {
42
42
  withSlopcameraOperationHostAdmission,
43
43
  } from "./operations.js"
44
44
  import type { HostResourceCoordinator } from "./host-resources.js"
45
+ import {
46
+ slopcameraIconLanguages,
47
+ slopcameraIconPalettes,
48
+ slopcameraIconSceneLimits,
49
+ } from "./icon-scene.js"
50
+ import { slopcameraSoundtrackFormats } from "./soundtrack.js"
45
51
  import { installSkill, type SkillScope, type SkillTarget } from "./skill-install.js"
46
52
  import { pathExists } from "./fs.js"
47
53
  import { checkDrawingFile, renderDrawingFile, starterDrawingSource } from "./drawing.js"
@@ -75,6 +81,14 @@ Usage:
75
81
  slopcamera image gallery <subject> --output-dir <directory> [--kind <${slopcameraGalleryKinds.join("|")}>]
76
82
  [--count <1-${slopcameraGalleryLimits.candidates}>] [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
77
83
  [--model <provider/model>] [--cell <${slopcameraGalleryLimits.cellEdgeMin}-${slopcameraGalleryLimits.cellEdgeMax}>] [--tile|--no-tile] [--json]
84
+ slopcamera image icon compose <source.json> [--output <file.json>] [--json]
85
+ slopcamera image icon render <source.json> --output <file.svg> [--recipe <file.json>]
86
+ [--size <${slopcameraIconSceneLimits.sizeMin}-${slopcameraIconSceneLimits.sizeMax}>] [--background] [--palette <${slopcameraIconPalettes.join("|")}>]
87
+ [--language <language>] [--json]
88
+ slopcamera media soundtrack compose <score> [--output <file.json>]
89
+ [--format <${slopcameraSoundtrackFormats.join("|")}>] [--json]
90
+ slopcamera media soundtrack grid <score> [--output <file.json>] [--start-us <n>]
91
+ [--format <${slopcameraSoundtrackFormats.join("|")}>] [--json]
78
92
  slopcamera style list [--json]
79
93
  slopcamera style show <id> [--json]
80
94
  slopcamera code search [query] [--limit <number>]
@@ -122,6 +136,16 @@ measured coverage and stroke-weight spread across the set, and a
122
136
  contact-sheet critique reviews the family as a unit before SVGs publish.
123
137
  Manifests and receipts stay local.
124
138
 
139
+ Icon compose and render are local and deterministic: they solve and draw
140
+ vector icon scenes, collections, construction programs, and recipes with the
141
+ bundled icon.place library, emit inert SVG, and replay a recipe only when its
142
+ recorded digests match. Languages: ${slopcameraIconLanguages.join(", ")}.
143
+
144
+ Media soundtrack verifies a Soundfish loop or song (compose text, song text,
145
+ score JSON, or a Standard MIDI file) and derives its beat grid as the
146
+ {bpm, beatOffsetUs, beatsPerBar} music timing HTML scene requests accept, plus
147
+ section cue times. It renders no audio and makes no network requests.
148
+
125
149
  Gallery generates several bounded candidates in parallel and composes them
126
150
  into one labelled contact sheet plus a receipt. Use it to review texture,
127
151
  skybox, backdrop, sprite, or design alternatives, then promote a chosen
@@ -365,6 +389,10 @@ function canonicalArguments(args: readonly string[]): readonly string[] {
365
389
  }
366
390
  throw new Error("Use slopcamera diagram init, check, render, or sheets")
367
391
  }
392
+ if (surface === "media") {
393
+ if (subcommand === "soundtrack") return ["soundtrack", ...rest]
394
+ throw new Error("Use slopcamera media soundtrack compose or grid")
395
+ }
368
396
  if (surface === "image") {
369
397
  if (
370
398
  subcommand === "vectorize" ||
@@ -388,6 +416,161 @@ function canonicalArguments(args: readonly string[]): readonly string[] {
388
416
  return args
389
417
  }
390
418
 
419
+ function rejectUnknownCliFlags(
420
+ parsed: ParsedArguments,
421
+ allowed: readonly string[],
422
+ command: string,
423
+ ): void {
424
+ const unknown = [...parsed.flags].find((flag) => !allowed.includes(flag))
425
+ if (unknown !== undefined) throw new Error(`Unknown ${command} option: --${unknown}`)
426
+ }
427
+
428
+ function exactlyOnePositional(parsed: ParsedArguments, usage: string): string {
429
+ if (parsed.positionals.length !== 1) throw new Error(`Use ${usage}`)
430
+ return parsed.positionals[0]!
431
+ }
432
+
433
+ function logOperationResult(
434
+ dependencies: SlopcameraCliDependencies,
435
+ json: boolean,
436
+ receipt: unknown,
437
+ summary: string,
438
+ ): void {
439
+ ;(dependencies.log ?? console.log)(json ? JSON.stringify(receipt, null, 2) : summary)
440
+ reportUsefulResult(dependencies.onUsefulResult)
441
+ }
442
+
443
+ async function runIconScene(
444
+ action: "compose" | "render",
445
+ args: readonly string[],
446
+ dependencies: SlopcameraCliDependencies,
447
+ ): Promise<void> {
448
+ if (action === "compose") {
449
+ const usage = "slopcamera image icon compose <source.json> [--output <file.json>] [--json]"
450
+ const parsed = parseArguments(args, new Set(["output"]))
451
+ rejectUnknownCliFlags(parsed, ["json"], "icon compose")
452
+ const sourcePath = exactlyOnePositional(parsed, usage)
453
+ const receipt = await executeSlopcameraOperation(
454
+ "slopcamera.icon.compose",
455
+ {
456
+ sourcePath,
457
+ ...(parsed.options.output === undefined ? {} : { outputPath: parsed.options.output }),
458
+ },
459
+ hostAdmissionOptions(dependencies),
460
+ )
461
+ logOperationResult(
462
+ dependencies,
463
+ parsed.flags.has("json"),
464
+ receipt,
465
+ `Composed ${receipt.form} "${receipt.title}": ${receipt.elementCount} element${receipt.elementCount === 1 ? "" : "s"}, geometry ${receipt.digests.geometry}` +
466
+ `${receipt.diagnostics.length === 0 ? "" : `, ${receipt.diagnostics.length} diagnostic${receipt.diagnostics.length === 1 ? "" : "s"}`}` +
467
+ `${receipt.output === null ? "" : ` -> ${receipt.output.path}`}`,
468
+ )
469
+ return
470
+ }
471
+ const parsed = parseArguments(args, new Set(["output", "recipe", "size", "palette", "language"]))
472
+ rejectUnknownCliFlags(parsed, ["json", "background"], "icon render")
473
+ const sourcePath = exactlyOnePositional(
474
+ parsed,
475
+ "slopcamera image icon render <source.json> --output <file.svg> [options]",
476
+ )
477
+ const size = parsed.options.size
478
+ if (size !== undefined && !/^\d{1,5}$/u.test(size)) {
479
+ throw new Error("--size must be an integer")
480
+ }
481
+ const receipt = await executeSlopcameraOperation(
482
+ "slopcamera.icon.render",
483
+ {
484
+ sourcePath,
485
+ outputPath: requiredOption(parsed, "output"),
486
+ ...(parsed.options.recipe === undefined ? {} : { recipePath: parsed.options.recipe }),
487
+ ...(size === undefined ? {} : { size: Number(size) }),
488
+ ...(parsed.flags.has("background") ? { background: true } : {}),
489
+ ...(parsed.options.palette === undefined ? {} : { palette: parsed.options.palette }),
490
+ ...(parsed.options.language === undefined ? {} : { language: parsed.options.language }),
491
+ },
492
+ hostAdmissionOptions(dependencies),
493
+ )
494
+ logOperationResult(
495
+ dependencies,
496
+ parsed.flags.has("json"),
497
+ receipt,
498
+ `Rendered ${receipt.form} "${receipt.title}"${receipt.replayed ? " (replayed recipe)" : ""}: ${receipt.output.path} ` +
499
+ `(${receipt.width}x${receipt.height}, svg ${receipt.digests.svg})` +
500
+ `${receipt.recipe === null ? "" : `\nRecipe: ${receipt.recipe.path}`}`,
501
+ )
502
+ }
503
+
504
+ async function runSoundtrack(
505
+ args: readonly string[],
506
+ dependencies: SlopcameraCliDependencies,
507
+ ): Promise<void> {
508
+ const [action, ...rest] = args
509
+ if (action === "compose") {
510
+ const parsed = parseArguments(rest, new Set(["output", "format"]))
511
+ rejectUnknownCliFlags(parsed, ["json"], "soundtrack compose")
512
+ const sourcePath = exactlyOnePositional(
513
+ parsed,
514
+ "slopcamera media soundtrack compose <score> [--output <file.json>] [--format <format>] [--json]",
515
+ )
516
+ const receipt = await executeSlopcameraOperation(
517
+ "slopcamera.soundtrack.compose",
518
+ {
519
+ sourcePath,
520
+ ...(parsed.options.output === undefined ? {} : { outputPath: parsed.options.output }),
521
+ ...(parsed.options.format === undefined ? {} : { format: parsed.options.format }),
522
+ },
523
+ hostAdmissionOptions(dependencies),
524
+ )
525
+ logOperationResult(
526
+ dependencies,
527
+ parsed.flags.has("json"),
528
+ receipt,
529
+ `Verified ${receipt.kind} "${receipt.title}": ${receipt.bpm} BPM, ${receipt.beatsPerBar}/bar, ${receipt.bars} bars, ` +
530
+ `${(receipt.durationUs / 1_000_000).toFixed(3)} s, digest ${receipt.digest}` +
531
+ `${receipt.output === null ? "" : ` -> ${receipt.output.path}`}`,
532
+ )
533
+ return
534
+ }
535
+ if (action === "grid") {
536
+ const parsed = parseArguments(rest, new Set(["output", "format", "start-us"]))
537
+ rejectUnknownCliFlags(parsed, ["json"], "soundtrack grid")
538
+ const sourcePath = exactlyOnePositional(
539
+ parsed,
540
+ "slopcamera media soundtrack grid <score> [--output <file.json>] [--start-us <n>] [--format <format>] [--json]",
541
+ )
542
+ const startUs = parsed.options["start-us"]
543
+ if (startUs !== undefined && !/^\d{1,10}$/u.test(startUs)) {
544
+ throw new Error("--start-us must be a non-negative integer")
545
+ }
546
+ const receipt = await executeSlopcameraOperation(
547
+ "slopcamera.soundtrack.grid",
548
+ {
549
+ sourcePath,
550
+ ...(parsed.options.output === undefined ? {} : { outputPath: parsed.options.output }),
551
+ ...(parsed.options.format === undefined ? {} : { format: parsed.options.format }),
552
+ ...(startUs === undefined ? {} : { startUs: Number(startUs) }),
553
+ },
554
+ hostAdmissionOptions(dependencies),
555
+ )
556
+ logOperationResult(
557
+ dependencies,
558
+ parsed.flags.has("json"),
559
+ receipt,
560
+ [
561
+ `Beat grid for ${receipt.scoreKind} "${receipt.title}": ${JSON.stringify(receipt.music)}`,
562
+ ...receipt.sections.map(
563
+ (section) =>
564
+ ` ${section.index}. ${section.label} bars ${section.startBar}-${section.endBar} ${section.startUs}-${section.endUs} us`,
565
+ ),
566
+ ...(receipt.output === null ? [] : [`Grid: ${receipt.output.path}`]),
567
+ ].join("\n"),
568
+ )
569
+ return
570
+ }
571
+ throw new Error("Use slopcamera media soundtrack compose or grid")
572
+ }
573
+
391
574
  export async function main(
392
575
  args: readonly string[],
393
576
  dependencies: SlopcameraCliDependencies = {},
@@ -582,6 +765,14 @@ export async function main(
582
765
  return
583
766
  }
584
767
 
768
+ if (command === "icon" && (rest[0] === "compose" || rest[0] === "render")) {
769
+ await runIconScene(rest[0], rest.slice(1), dependencies)
770
+ return
771
+ }
772
+ if (command === "soundtrack") {
773
+ await runSoundtrack(rest, dependencies)
774
+ return
775
+ }
585
776
  if (command === "icon") {
586
777
  const parsed = parseArguments(
587
778
  rest,