@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.
- package/NOTICE.md +14 -0
- package/README.md +294 -298
- package/apps/desktop/application/html-overlay-integrity.ts +27 -4
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/application/operations/spatial-render.ts +1 -1
- package/apps/desktop/cli/capability-manifest.ts +15 -2
- package/apps/desktop/cli/commands.ts +1 -1
- package/apps/desktop/cli/help.ts +23 -4
- package/apps/desktop/cli/portable-surface.ts +1 -0
- package/apps/desktop/cli/verbs.ts +4 -0
- package/apps/desktop/dist/cli/main.js +306 -284
- package/dist/cli.js +24 -4
- package/dist/{index-842t02pp.js → index-m6g0ca9b.js} +1 -1
- package/dist/{index-y8spxs77.js → index-mgychzh3.js} +6 -3
- package/dist/{index-k9bjw80z.js → index-rxx27q8b.js} +4 -4
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +4 -1
- package/package.json +5 -3
- package/skills/slopcamera/SKILL.md +2 -0
- package/skills/slopcamera/references/customization.md +8 -1
- package/skills/slopcamera/references/icon-place.md +76 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +10 -2
- package/skills/slopcamera/references/soundtracks.md +58 -0
- package/src/bounded-file.ts +135 -0
- package/src/cli.ts +191 -0
- package/src/icon-scene.ts +695 -0
- package/src/index.ts +2 -0
- package/src/mcp/tools.ts +311 -0
- package/src/mcp/types.ts +4 -0
- package/src/operations.ts +306 -0
- package/src/soundtrack.ts +489 -0
- 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,
|