+1
-1
| { | ||
| "name": "spendict", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Spendict CLI — a performance marketer's verdict (run / fix_first / kill) on your ad creatives, from the terminal or your agent.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+84
-8
| import { readFileSync } from "node:fs"; | ||
| import { createInterface } from "node:readline/promises"; | ||
| import { Command } from "commander"; | ||
@@ -11,3 +12,3 @@ import { callTool } from "./api.js"; | ||
| .description("A performance marketer's verdict (run / fix_first / kill) on your ad creatives.") | ||
| .version("0.1.0"); | ||
| .version("0.2.0"); | ||
@@ -62,2 +63,45 @@ const bold = (s) => `\x1b[1m${s}\x1b[0m`; | ||
| async function readStdinAll() { | ||
| let data = ""; | ||
| for await (const chunk of process.stdin) data += chunk; | ||
| return data.trim(); | ||
| } | ||
| // Real ad copy is full of characters the shell mangles (! " $ '), so when the | ||
| // required flags are missing we prompt for values instead — pasted input never | ||
| // passes through shell quoting. | ||
| async function promptMissing(opts) { | ||
| const rl = createInterface({ input: process.stdin, output: process.stdout }); | ||
| const ask = async (label, { required = false } = {}) => { | ||
| for (;;) { | ||
| const a = (await rl.question(` ${label}: `)).trim(); | ||
| if (a) return a; | ||
| if (!required) return undefined; | ||
| console.log(dim(" (required)")); | ||
| } | ||
| }; | ||
| const askMultiline = async (label) => { | ||
| console.log(` ${label} ${dim("— paste it as-is, then press Enter on an empty line:")}`); | ||
| const lines = []; | ||
| for (;;) { | ||
| const line = await rl.question(""); | ||
| if (!line.trim()) { | ||
| if (lines.length) break; | ||
| continue; | ||
| } | ||
| lines.push(line); | ||
| } | ||
| return lines.join("\n"); | ||
| }; | ||
| try { | ||
| if (!opts.text) opts.text = await askMultiline("Ad text / body"); | ||
| if (!opts.platform) opts.platform = await ask("Platform (meta | tiktok | google | linkedin | youtube)", { required: true }); | ||
| if (!opts.product) opts.product = await ask("Product / offer", { required: true }); | ||
| if (!opts.headline) opts.headline = await ask("Headline (optional, Enter to skip)"); | ||
| if (!opts.audience) opts.audience = await ask("Target audience (optional, Enter to skip)"); | ||
| } finally { | ||
| rl.close(); | ||
| } | ||
| } | ||
| async function run(tool, requestBody, opts) { | ||
@@ -109,4 +153,4 @@ try { | ||
| .command("assess") | ||
| .description("Gate an ad creative before spend.") | ||
| .option("--text <s>", "primary ad text / body") | ||
| .description("Gate an ad creative before spend. Run with no flags for interactive mode (paste copy as-is, no shell quoting).") | ||
| .option("--text <s>", "primary ad text / body (or pipe it via stdin)") | ||
| .option("--headline <s>", "headline") | ||
@@ -120,6 +164,37 @@ .option("--desc <s>", "description / subtext") | ||
| .option("--json", "print raw JSON") | ||
| .action((opts) => { | ||
| const b = | ||
| body(opts) || | ||
| { | ||
| .addHelpText( | ||
| "after", | ||
| ` | ||
| Ad copy is full of characters your shell may mangle (! $ " '), so the safest | ||
| inputs skip shell quoting entirely: | ||
| Interactive (paste copy as-is): spendict assess | ||
| Pipe the ad text via stdin: pbpaste | spendict assess --platform meta --product 'Your offer' | ||
| Full JSON body from a file: spendict assess --file ad.json | ||
| `, | ||
| ) | ||
| .action(async (opts) => { | ||
| let b = body(opts); | ||
| if (!b) { | ||
| if (!opts.text && !process.stdin.isTTY) opts.text = await readStdinAll(); | ||
| if ((!opts.text || !opts.platform || !opts.product) && process.stdin.isTTY) { | ||
| console.log(`\n ${bold("Interactive mode")} ${dim("— missing fields; paste values as-is (no shell quoting needed).")}`); | ||
| try { | ||
| await promptMissing(opts); | ||
| } catch (e) { | ||
| if (e.code !== "ABORT_ERR") throw e; | ||
| console.error("\nCancelled."); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| } | ||
| const missing = [!opts.text && "--text", !opts.platform && "--platform", !opts.product && "--product"].filter(Boolean); | ||
| if (missing.length) { | ||
| console.error( | ||
| `\x1b[31mMissing required input: ${missing.join(", ")}. Run \`spendict assess\` with no flags for interactive mode, or pipe the ad text via stdin.\x1b[0m`, | ||
| ); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
| b = { | ||
| ad_copy: { primary_text: opts.text, headline: opts.headline, description: opts.desc }, | ||
@@ -131,3 +206,4 @@ platform: opts.platform, | ||
| }; | ||
| run("assess", b, opts); | ||
| } | ||
| await run("assess", b, opts); | ||
| }); | ||
@@ -134,0 +210,0 @@ |
15617
24.44%389
23.1%