@seanmozeik/vicon 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanmozeik/vicon",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI-powered media conversion CLI — describe what you want, get the commands",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env bun
2
+
1
3
  import * as p from "@clack/prompts";
2
4
  import boxen from "boxen";
3
5
  import { generate, ValidationError } from "./lib/ai.js";
@@ -190,7 +192,8 @@ function renderToolSummary(ctx: ToolCtx): string {
190
192
 
191
193
  if (ctx.ffmpeg.installed) {
192
194
  const ver = ctx.ffmpeg.version ?? "?";
193
- const enc = ctx.ffmpeg.encoders.length;
195
+ const enc =
196
+ ctx.ffmpeg.videoEncoders.length + ctx.ffmpeg.audioEncoders.length;
194
197
  const dec = ctx.ffmpeg.decoders.length;
195
198
  parts.push(
196
199
  theme.muted(`ffmpeg ${ver} (${enc} encoders · ${dec} decoders)`),
@@ -395,13 +398,25 @@ async function handleEditCommandsAction(
395
398
  }
396
399
 
397
400
  async function handleRunAction(commands: string[]): Promise<void> {
401
+ const preExisting = new Set(
402
+ await Promise.all(
403
+ inferInputFiles(commands).map(async (f) => {
404
+ const file = Bun.file(f);
405
+ return (await file.exists()) ? f : null;
406
+ }),
407
+ ).then((results) => results.filter((f): f is string => f !== null)),
408
+ );
409
+
398
410
  const success = await runCommands(commands, {
399
411
  onBefore: (cmd, i, total) => p.log.step(`▶ [${i + 1}/${total}] ${cmd}`),
400
412
  onSuccess: () => p.log.success("All commands completed successfully."),
401
413
  onError: (cmd, exitCode) =>
402
414
  p.log.error(`Command exited with code ${exitCode}: ${cmd}`),
403
415
  });
404
- await runCleanup(inferInputFiles(commands));
416
+
417
+ if (success) {
418
+ await runCleanup([...preExisting]);
419
+ }
405
420
  process.exit(success ? 0 : 1);
406
421
  }
407
422
 
@@ -417,6 +432,13 @@ async function runConversion(
417
432
  toolSpinner.stop("Tools detected.");
418
433
  p.log.info(renderToolSummary(ctx));
419
434
 
435
+ if (!ctx.ffmpeg.installed && !ctx.magick.installed) {
436
+ p.log.error(
437
+ "No media tools found. Install ffmpeg or ImageMagick and try again.",
438
+ );
439
+ process.exit(1);
440
+ }
441
+
420
442
  let { result: currentResult, userRequest } = await generateUntilSuccess(
421
443
  initialRequest,
422
444
  ctx,
package/src/lib/prompt.ts CHANGED
@@ -1,26 +1,63 @@
1
1
  import type { ToolContext } from "../types.js";
2
2
 
3
+ // Common output format → required ffmpeg encoder.
4
+ // Used to help the AI cross-reference before generating commands.
5
+ const FORMAT_ENCODER_REF = `\
6
+ ## Output Format → Required ffmpeg Encoder
7
+ webp → libwebp
8
+ hevc/h265 → libx265
9
+ h264/mp4/mov/mkv → libx264
10
+ vp8/webm → libvpx
11
+ vp9 → libvpx-vp9
12
+ av1 → libsvtav1 or libaom-av1
13
+ gif → gif (built-in)
14
+ png → png (built-in)
15
+ jpeg/jpg → mjpeg (built-in)
16
+ mp3 → libmp3lame
17
+ aac → aac (built-in)
18
+ opus → libopus
19
+ flac → flac (built-in)
20
+ Use this table to identify which encoder is needed, then verify it appears in the available encoder lists before generating any command.`;
21
+
3
22
  export function buildSystemPrompt(ctx: ToolContext): string {
4
- const ffmpegLine = ctx.ffmpeg.installed
5
- ? `ffmpeg ${ctx.ffmpeg.version ?? "unknown"} | encoders: [${ctx.ffmpeg.encoders.join(", ")}] | decoders: [${ctx.ffmpeg.decoders.join(", ")}]`
6
- : "ffmpeg: not installed";
23
+ const lines: string[] = [];
24
+
25
+ lines.push("## Available Tools");
26
+
27
+ if (ctx.ffmpeg.installed) {
28
+ lines.push(`ffmpeg ${ctx.ffmpeg.version ?? "unknown"}`);
29
+ lines.push(
30
+ ` Video encoders: ${ctx.ffmpeg.videoEncoders.join(", ") || "none"}`,
31
+ );
32
+ lines.push(
33
+ ` Audio encoders: ${ctx.ffmpeg.audioEncoders.join(", ") || "none"}`,
34
+ );
35
+ } else {
36
+ lines.push("ffmpeg: NOT installed — do not generate ffmpeg commands");
37
+ }
7
38
 
8
- const magickLine = ctx.magick.installed
9
- ? `magick ${ctx.magick.version ?? "unknown"} | formats: [${ctx.magick.formats.join(", ")}]`
10
- : "magick: not installed";
39
+ if (ctx.magick.installed) {
40
+ lines.push(`magick ${ctx.magick.version ?? "unknown"}`);
41
+ lines.push(` Formats: ${ctx.magick.formats.join(", ") || "none"}`);
42
+ } else {
43
+ lines.push("magick: NOT installed — do not generate magick commands");
44
+ }
11
45
 
12
- const environment = `## Environment\n${ffmpegLine}\n${magickLine}`;
46
+ const environment = lines.join("\n");
13
47
 
14
48
  const rules = `## Rules
15
49
  Return ONLY valid JSON in this exact shape: { "commands": string[], "explanation": string }
16
50
  - explanation: plain prose only — no shell syntax, no backticks, no code
17
51
  - commands: complete, copy-pasteable shell strings — no placeholders, no &&, no loops
18
- - Only use tools that are listed as installed above
52
+ - BEFORE generating any command: identify the required encoder/format using the reference table below, then verify it appears in the available lists above
53
+ - NEVER rely on ffmpeg auto-selection — always specify -c:v for video output and -c:a for audio output explicitly
54
+ - If a required ffmpeg encoder is not available, use magick if the format is in its format list
55
+ - If neither tool can handle the task, return { "commands": [], "explanation": "<reason why it cannot be done with available tools>" }
19
56
  - Prefer non-destructive output: append _converted to output filenames, use -n flag to avoid overwriting
20
57
  - For batch tasks, emit one command per file
21
58
  IMPORTANT: Reply with ONLY the JSON object — no markdown fences, no extra text`;
22
59
 
23
- return [environment, rules].join("\n\n");
60
+ return [environment, FORMAT_ENCODER_REF, rules].join("\n\n");
24
61
  }
25
62
 
26
63
  export function buildUserPrompt(request: string): string {
package/src/lib/tools.ts CHANGED
@@ -13,7 +13,13 @@ async function run(cmd: string[]): Promise<string> {
13
13
 
14
14
  async function probeFfmpeg(): Promise<ToolContext["ffmpeg"]> {
15
15
  const versionOut = await run(["ffmpeg", "-version"]);
16
- if (!versionOut) return { installed: false, encoders: [], decoders: [] };
16
+ if (!versionOut)
17
+ return {
18
+ installed: false,
19
+ videoEncoders: [],
20
+ audioEncoders: [],
21
+ decoders: [],
22
+ };
17
23
 
18
24
  const versionMatch = versionOut.split("\n")[0]?.match(/ffmpeg version (\S+)/);
19
25
  const version = versionMatch?.[1];
@@ -21,21 +27,24 @@ async function probeFfmpeg(): Promise<ToolContext["ffmpeg"]> {
21
27
  const encodersOut = await run(["ffmpeg", "-encoders"]);
22
28
  const decodersOut = await run(["ffmpeg", "-decoders"]);
23
29
 
24
- const encoders = encodersOut
25
- .split("\n")
26
- .slice(1)
27
- .filter((l) => /^ [VAS.]+\s/.test(l))
28
- .map((l) => l.trim().split(/\s+/)[1] ?? "")
29
- .filter(Boolean);
30
+ const parseEncoderLines = (out: string) =>
31
+ out
32
+ .split("\n")
33
+ .filter((l) => /^ [VAS][A-Z.]{5} \S/.test(l))
34
+ .map((l) => ({ type: l[1], name: l.trim().split(/\s+/)[1] ?? "" }))
35
+ .filter((e) => e.name);
30
36
 
31
- const decoders = decodersOut
32
- .split("\n")
33
- .slice(1)
34
- .filter((l) => /^ [VAS.]+\s/.test(l))
35
- .map((l) => l.trim().split(/\s+/)[1] ?? "")
36
- .filter(Boolean);
37
+ const encoderEntries = parseEncoderLines(encodersOut);
38
+ const videoEncoders = encoderEntries
39
+ .filter((e) => e.type === "V")
40
+ .map((e) => e.name);
41
+ const audioEncoders = encoderEntries
42
+ .filter((e) => e.type === "A")
43
+ .map((e) => e.name);
44
+
45
+ const decoders = parseEncoderLines(decodersOut).map((e) => e.name);
37
46
 
38
- return { installed: true, version, encoders, decoders };
47
+ return { installed: true, version, videoEncoders, audioEncoders, decoders };
39
48
  }
40
49
 
41
50
  async function probeMagick(): Promise<ToolContext["magick"]> {
package/src/types.ts CHANGED
@@ -7,7 +7,8 @@ export interface ToolContext {
7
7
  ffmpeg: {
8
8
  installed: boolean;
9
9
  version?: string;
10
- encoders: string[];
10
+ videoEncoders: string[];
11
+ audioEncoders: string[];
11
12
  decoders: string[];
12
13
  };
13
14
  magick: {