← back to Studio Zero
studio-zero: route live generation through Claude Max plan (local claude CLI, $0 marginal) with API + demo fallback
98efd98bbc08e68cc9dfde17c6a2879e556751be · 2026-07-24 23:16:49 -0700 · Steve
Files touched
M public/index.htmlM server.js
Diff
commit 98efd98bbc08e68cc9dfde17c6a2879e556751be
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 24 23:16:49 2026 -0700
studio-zero: route live generation through Claude Max plan (local claude CLI, $0 marginal) with API + demo fallback
---
public/index.html | 19 ++++++---
server.js | 119 ++++++++++++++++++++++++++++++++++++++----------------
2 files changed, 97 insertions(+), 41 deletions(-)
diff --git a/public/index.html b/public/index.html
index 4e43d54..7bc6aff 100644
--- a/public/index.html
+++ b/public/index.html
@@ -278,7 +278,7 @@ function renderStep(){
fld.addEventListener('input',()=>{$('#pbtext').innerHTML=promptHTML(step,currentVals(step));updateEst();});
});
updateEst();
- if(saved){renderOutput(step,saved.text,saved.demo,saved.cost);}
+ if(saved){renderOutput(step,saved.text,saved.demo,saved.cost,saved.maxPlan,saved.equiv);}
$('#genBtn').onclick=doGenerate;
$('#prevBtn').onclick=()=>{if(cur>0){cur--;renderStep();renderStepper();}};
$('#nextBtn').onclick=()=>{if(cur<6){cur++;renderStep();renderStepper();}};
@@ -288,7 +288,10 @@ function renderStep(){
function updateEst(){
const step=PROMPTS[cur];const p=fillPrompt(step,currentVals(step));
const est=((p.length/4)/1e6*CFG.rate.in)+(1600/1e6*CFG.rate.out);
- const e=$('#estCost');if(e)e.innerHTML=CFG.liveMode?`est. <b>${fmtCost(est)}</b> · ${CFG.model}`:'demo mode · <b>$0 (local)</b>';
+ const e=$('#estCost');if(!e)return;
+ if(CFG.maxPlan)e.innerHTML='Max plan · <b>$0 marginal</b> <span style="color:var(--faint)">(covered by subscription)</span>';
+ else if(CFG.liveMode)e.innerHTML=`est. <b>${fmtCost(est)}</b> · ${esc(CFG.model)}`;
+ else e.innerHTML='demo mode · <b>$0 (local)</b>';
}
async function doGenerate(){
@@ -302,12 +305,12 @@ async function doGenerate(){
const j=await res.json();
const nicheVal=vals['[NICHE]'];if(nicheVal)STATE.niche=nicheVal;
if(STATE.name==='Untitled channel'&&STATE.niche)STATE.name=STATE.niche.slice(0,42);
- STATE.outputs[step.key]={text:j.text,demo:j.demo,cost:j.cost||0,at:new Date().toISOString()};
+ STATE.outputs[step.key]={text:j.text,demo:j.demo,cost:j.cost||0,maxPlan:!!j.maxPlan,equiv:j.equiv||null,at:new Date().toISOString()};
STATE.costs[step.key]=j.cost||0;
if(j.cost){sessionCost+=j.cost;updateCostChip();}
if(j.warning)toast('Live call failed — showing demo');
persist();
- renderOutput(step,j.text,j.demo,j.cost);
+ renderOutput(step,j.text,j.demo,j.cost,j.maxPlan,j.equiv);
renderStepper();
}catch(e){
$('#mdOut').innerHTML='<p style="color:var(--red2)">Error: '+esc(e.message)+'</p>';
@@ -316,9 +319,13 @@ async function doGenerate(){
}
}
-function renderOutput(step,text,demo,cost){
+function renderOutput(step,text,demo,cost,maxPlan,equiv){
$('#out').classList.add('show');
- const b=$('#outBadge');b.textContent=demo?'demo':(CFG.model+' · '+fmtCost(cost||0));b.className='badge '+(demo?'demo':'live');
+ const b=$('#outBadge');
+ if(demo)b.textContent='demo';
+ else if(maxPlan)b.textContent='Max plan · $0'+(equiv?' (≈'+fmtCost(equiv)+' metered)':'');
+ else b.textContent=CFG.model+' · '+fmtCost(cost||0);
+ b.className='badge '+(demo?'demo':'live');
if(step.renderCalendar){renderCalendar(text);}else{$('#calWrap').innerHTML='';}
$('#mdOut').innerHTML=md(text);
}
diff --git a/server.js b/server.js
index 124590c..a3f273d 100644
--- a/server.js
+++ b/server.js
@@ -1,15 +1,26 @@
// Studio Zero — guided AI YouTube channel builder.
-// Zero-dep Node http server: static host + POST /api/generate proxy to the Anthropic API,
-// with per-call $ cost accounting and a demo-mode fallback (no key required to click through).
+// Zero-dep Node http server: static host + POST /api/generate.
+// Provider layer (SZ_PROVIDER): 'cli' (Claude Max plan via the local `claude` CLI, $0 marginal),
+// 'api' (metered Anthropic HTTP API), or 'auto' (cli if the CLI is present, else api).
+// Every path falls back to a canned demo deliverable so the prototype is always clickable.
const http = require('http'), fs = require('fs'), path = require('path'), https = require('https');
-const os = require('os');
+const os = require('os'), { spawn, execSync } = require('child_process');
const PORT = parseInt(process.env.PORT || '0', 10);
const DIR = __dirname;
-const MODEL = process.env.SZ_MODEL || 'claude-sonnet-4-6';
+const MODEL = process.env.SZ_MODEL || 'claude-sonnet-4-6'; // API label / default
+const CLI_MODEL = process.env.SZ_CLI_MODEL || 'sonnet'; // alias the `claude` CLI accepts
const MAX_TOKENS = parseInt(process.env.SZ_MAX_TOKENS || '1600', 10);
-// Anthropic per-Mtoken rates (USD) — keep in sync with the cost-tracker skill.
+// Provider selection. Default: prefer the Max-plan CLI (Steve's directive) when it's on PATH.
+function cliAvailable() {
+ try { execSync('command -v claude', { stdio: 'ignore' }); return true; } catch (_) { return false; }
+}
+const HAS_CLI = cliAvailable();
+let PROVIDER = (process.env.SZ_PROVIDER || 'auto').toLowerCase();
+if (PROVIDER === 'auto') PROVIDER = HAS_CLI ? 'cli' : 'api';
+
+// Anthropic per-Mtoken rates (USD) — used for the metered API path + the "equivalent" figure.
const RATES = {
'claude-sonnet-4-6': { in: 3, out: 15 },
'claude-opus-4-8': { in: 15, out: 75 },
@@ -23,8 +34,8 @@ function costOf(model, usage) {
return +(i + o).toFixed(5);
}
-// Resolve the Anthropic key at runtime WITHOUT copying or committing it:
-// env first, then a project-local .env, then the master secrets .env.
+// Resolve the Anthropic API key at runtime WITHOUT copying or committing it
+// (only used by the 'api' provider): env first, then project-local .env, then master secrets .env.
function resolveKey() {
if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
const candidates = [
@@ -46,23 +57,42 @@ function loadPrompts() {
catch { return { steps: [] }; }
}
+// --- Provider: Claude Max plan via the local `claude` CLI (subscription, $0 marginal) ---
+// Spawns lean: API key stripped (forces Max-plan/subscription auth), no MCP, scratch cwd.
+function callClaudeCLI(prompt) {
+ return new Promise((resolve, reject) => {
+ const env = { ...process.env };
+ delete env.ANTHROPIC_API_KEY; delete env.ANTHROPIC_AUTH_TOKEN; // force the Max-plan subscription
+ const args = ['-p', prompt, '--output-format', 'json', '--model', CLI_MODEL, '--strict-mcp-config'];
+ const cp = spawn('claude', args, { env, cwd: os.tmpdir() });
+ let out = '', err = '';
+ cp.stdout.on('data', d => out += d);
+ cp.stderr.on('data', d => err += d);
+ const timer = setTimeout(() => { cp.kill('SIGKILL'); reject(new Error('cli timeout')); }, 150000);
+ cp.on('error', e => { clearTimeout(timer); reject(e); });
+ cp.on('close', () => {
+ clearTimeout(timer);
+ try {
+ const j = JSON.parse(out.trim());
+ const arr = Array.isArray(j) ? j : [j];
+ const r = arr.find(x => x && x.type === 'result') || arr[arr.length - 1] || j;
+ if (r.is_error) return reject(new Error(r.result || r.subtype || 'cli error'));
+ const model = r.modelUsage ? Object.keys(r.modelUsage)[0] : (MODEL);
+ resolve({ text: r.result || '', usage: r.usage, equiv: r.total_cost_usd || null, model, maxPlan: true });
+ } catch (e) { reject(new Error('cli parse: ' + (err.split('\n')[0] || e.message))); }
+ });
+ });
+}
+
+// --- Provider: metered Anthropic HTTP API ---
function callAnthropic(prompt) {
return new Promise((resolve, reject) => {
const key = resolveKey();
if (!key) return reject(new Error('no-key'));
- const body = JSON.stringify({
- model: MODEL,
- max_tokens: MAX_TOKENS,
- messages: [{ role: 'user', content: prompt }],
- });
+ const body = JSON.stringify({ model: MODEL, max_tokens: MAX_TOKENS, messages: [{ role: 'user', content: prompt }] });
const req = https.request({
hostname: 'api.anthropic.com', path: '/v1/messages', method: 'POST',
- headers: {
- 'content-type': 'application/json',
- 'x-api-key': key,
- 'anthropic-version': '2023-06-01',
- 'content-length': Buffer.byteLength(body),
- },
+ headers: { 'content-type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01', 'content-length': Buffer.byteLength(body) },
}, (r) => {
let d = ''; r.on('data', c => d += c);
r.on('end', () => {
@@ -70,7 +100,7 @@ function callAnthropic(prompt) {
const j = JSON.parse(d);
if (r.statusCode >= 400) return reject(new Error(j.error && j.error.message || ('HTTP ' + r.statusCode)));
const text = (j.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n');
- resolve({ text, usage: j.usage, model: j.model || MODEL });
+ resolve({ text, usage: j.usage, model: j.model || MODEL, maxPlan: false, cost: costOf(j.model || MODEL, j.usage) });
} catch (e) { reject(e); }
});
});
@@ -80,18 +110,23 @@ function callAnthropic(prompt) {
});
}
-// Demo-mode deliverable so the prototype is fully clickable with no key.
+async function generate(prompt) {
+ if (PROVIDER === 'cli' && HAS_CLI) return await callClaudeCLI(prompt);
+ return await callAnthropic(prompt);
+}
+
+// Demo-mode deliverable so the prototype is fully clickable with no provider.
function demoOutput(stepKey) {
const canned = {
- niche: "### Top 10 niches (demo)\n\n| Niche | Avg CPM | Difficulty | Beyond AdSense | 3-word concept |\n|---|---|---|---|---|\n| AI tools for solopreneurs | $18–28 | Medium | Affiliate, courses, SaaS | \"Automate. Ship. Profit.\" |\n| Personal finance / FIRE | $22–35 | Medium | Affiliate, coaching, books | \"Money. Freedom. Calm.\" |\n| B2B software reviews | $25–40 | High | Sponsorships, affiliate | \"Software. Reviewed. Honestly.\" |\n| Home lab / self-hosting | $12–20 | Medium | Affiliate, Patreon | \"Own. Your. Data.\" |\n| Career / interview prep | $15–24 | Low | Courses, resume services | \"Land. The. Offer.\" |\n\n*(Demo output — set an ANTHROPIC_API_KEY to generate the live, full 10-niche analysis.)*",
- identity: "### Channel identity (demo)\n\n**Names:** Zero-to-Scale · The Quiet Engine · Solo Signal · Compound Studio · Nightshift Labs\n\n**Tagline:** *Build the machine that builds your channel.*\n\n**Persona:** 28–40, time-poor operator who wants leverage, not another hobby.\n\n**Pillars:** (1) Systems teardown (2) Tool-of-the-week (3) Case-study builds (4) Mindset / operating cadence.\n\n**Unique mechanism:** the \"one-input, seven-outputs\" content engine — every build becomes a week of content.\n\n*(Demo output — set a key for the live version.)*",
- roadmap: "### 90-day roadmap (demo)\n\n1. How I Built an AI YouTube Channel in 90 Days\n2. The $0 Faceless Channel Stack\n3. 7 Prompts That Replace a Content Team\n4. Why Posting Daily Is Killing Your Channel\n5. The One-Input, Seven-Output System\n6. I Let Claude Plan 90 Days of Content\n7. Faceless Channel Mistakes That Cost Me Months\n8. The Thumbnail Formula That Doubled My CTR\n9. How to Monetize Before 1,000 Subs\n10. My Exact Upload Schedule (Copy It)\n11. Turning One Video Into a Week of Content\n12. The 90-Day Authority Ladder\n\n*(Demo output — set a key for the full calendar with schedule + SEO/virality priority.)*",
- script: "### Script (demo)\n\n**[0:00 HOOK]** \"Everyone told you to post daily. That advice is quietly killing new channels — here's the math.\"\n\n**[Agitation]** …\n**[Solution]** …\n**[3 insights]** …\n**[CTA]** …\n**[End-screen tease]** \"Next video: the 7-output content engine.\"\n\n*(Demo output — set a key for the full script.)*",
- seo: "### SEO + thumbnail package (demo)\n\n**Thumbnails:** (A) shocked-face + \"$0 → $10K\" (B) split before/after (C) big red arrow on a dashboard.\n\n**Title (<60):** I Built an AI YouTube Channel in 90 Days (Real Numbers)\n\n**Tags:** ai youtube, faceless channel, youtube automation, … *(10 total)*\n\n*(Demo output — set a key for the full package.)*",
- monetization: "### Monetization stack (demo)\n\n**Affiliate:** tool stack you already use · **Digital product:** the exact template pack · **Sponsorship pitch:** 6-line cold template · **Lead magnet:** \"The 7-Prompt Channel Kit\" → email list.\n\n*(Demo output — set a key for the full roadmap.)*",
- repurpose: "### Repurposing engine (demo)\n\n**X thread** · **LinkedIn post** · **3 Shorts hooks** · **Newsletter** · **Pinterest desc** — all generated from your script.\n\n*(Demo output — set a key for the full multi-platform pack.)*",
+ niche: "### Top 10 niches (demo)\n\n| Niche | Avg CPM | Difficulty | Beyond AdSense | 3-word concept |\n|---|---|---|---|---|\n| AI tools for solopreneurs | $18–28 | Medium | Affiliate, courses, SaaS | \"Automate. Ship. Profit.\" |\n| Personal finance / FIRE | $22–35 | Medium | Affiliate, coaching, books | \"Money. Freedom. Calm.\" |\n| B2B software reviews | $25–40 | High | Sponsorships, affiliate | \"Software. Reviewed. Honestly.\" |\n| Home lab / self-hosting | $12–20 | Medium | Affiliate, Patreon | \"Own. Your. Data.\" |\n| Career / interview prep | $15–24 | Low | Courses, resume services | \"Land. The. Offer.\" |\n\n*(Demo output — enable a provider to generate the live, full 10-niche analysis.)*",
+ identity: "### Channel identity (demo)\n\n**Names:** Zero-to-Scale · The Quiet Engine · Solo Signal · Compound Studio · Nightshift Labs\n\n**Tagline:** *Build the machine that builds your channel.*\n\n**Persona:** 28–40, time-poor operator who wants leverage, not another hobby.\n\n**Pillars:** (1) Systems teardown (2) Tool-of-the-week (3) Case-study builds (4) Mindset / operating cadence.\n\n**Unique mechanism:** the \"one-input, seven-outputs\" content engine — every build becomes a week of content.\n\n*(Demo output — enable a provider for the live version.)*",
+ roadmap: "### 90-day roadmap (demo)\n\n1. How I Built an AI YouTube Channel in 90 Days\n2. The $0 Faceless Channel Stack\n3. 7 Prompts That Replace a Content Team\n4. Why Posting Daily Is Killing Your Channel\n5. The One-Input, Seven-Output System\n6. I Let Claude Plan 90 Days of Content\n7. Faceless Channel Mistakes That Cost Me Months\n8. The Thumbnail Formula That Doubled My CTR\n9. How to Monetize Before 1,000 Subs\n10. My Exact Upload Schedule (Copy It)\n11. Turning One Video Into a Week of Content\n12. The 90-Day Authority Ladder\n\n*(Demo output — enable a provider for the full calendar with schedule + SEO/virality priority.)*",
+ script: "### Script (demo)\n\n**[0:00 HOOK]** \"Everyone told you to post daily. That advice is quietly killing new channels — here's the math.\"\n\n**[Agitation]** …\n**[Solution]** …\n**[3 insights]** …\n**[CTA]** …\n**[End-screen tease]** \"Next video: the 7-output content engine.\"\n\n*(Demo output — enable a provider for the full script.)*",
+ seo: "### SEO + thumbnail package (demo)\n\n**Thumbnails:** (A) shocked-face + \"$0 → $10K\" (B) split before/after (C) big red arrow on a dashboard.\n\n**Title (<60):** I Built an AI YouTube Channel in 90 Days (Real Numbers)\n\n**Tags:** ai youtube, faceless channel, youtube automation, … *(10 total)*\n\n*(Demo output — enable a provider for the full package.)*",
+ monetization: "### Monetization stack (demo)\n\n**Affiliate:** tool stack you already use · **Digital product:** the exact template pack · **Sponsorship pitch:** 6-line cold template · **Lead magnet:** \"The 7-Prompt Channel Kit\" → email list.\n\n*(Demo output — enable a provider for the full roadmap.)*",
+ repurpose: "### Repurposing engine (demo)\n\n**X thread** · **LinkedIn post** · **3 Shorts hooks** · **Newsletter** · **Pinterest desc** — all generated from your script.\n\n*(Demo output — enable a provider for the full multi-platform pack.)*",
};
- return canned[stepKey] || "*(Demo output — set an ANTHROPIC_API_KEY to generate live deliverables.)*";
+ return canned[stepKey] || "*(Demo output — enable a provider to generate live deliverables.)*";
}
function send(res, code, obj) {
@@ -99,11 +134,19 @@ function send(res, code, obj) {
res.end(JSON.stringify(obj));
}
+// Is a live provider actually usable right now?
+const LIVE = (PROVIDER === 'cli' && HAS_CLI) || (PROVIDER === 'api' && HAS_KEY);
+
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
if (u.pathname === '/api/config') {
- return send(res, 200, { liveMode: HAS_KEY, model: MODEL, maxTokens: MAX_TOKENS, rate: rate(MODEL) });
+ return send(res, 200, {
+ liveMode: LIVE, provider: LIVE ? PROVIDER : 'demo',
+ maxPlan: PROVIDER === 'cli' && HAS_CLI,
+ model: PROVIDER === 'cli' ? (CLI_MODEL + ' (Max plan)') : MODEL,
+ maxTokens: MAX_TOKENS, rate: rate(MODEL),
+ });
}
if (u.pathname === '/api/prompts') {
return send(res, 200, loadPrompts());
@@ -117,15 +160,20 @@ const server = http.createServer((req, res) => {
if (!prompt) return send(res, 400, { error: 'missing prompt' });
const r = rate(MODEL);
const est = +(((prompt.length / 4) / 1e6 * r.in) + (MAX_TOKENS / 1e6 * r.out)).toFixed(5);
- if (!HAS_KEY) {
- return send(res, 200, { text: demoOutput(stepKey), demo: true, cost: 0, estCost: 0, model: 'demo' });
+ if (!LIVE) {
+ return send(res, 200, { text: demoOutput(stepKey), demo: true, provider: 'demo', cost: 0, estCost: 0, model: 'demo' });
}
try {
- const out = await callAnthropic(prompt);
- return send(res, 200, { text: out.text, demo: false, usage: out.usage, cost: costOf(out.model, out.usage), estCost: est, model: out.model });
+ const out = await generate(prompt);
+ // Max-plan CLI: marginal spend is $0; report the metered-equivalent separately.
+ const cost = out.maxPlan ? 0 : (out.cost != null ? out.cost : costOf(out.model, out.usage));
+ return send(res, 200, {
+ text: out.text, demo: false, provider: out.maxPlan ? 'cli' : 'api', maxPlan: !!out.maxPlan,
+ usage: out.usage, cost, equiv: out.equiv || null, estCost: est, model: out.model,
+ });
} catch (e) {
// Graceful fallback to demo so the prototype never hard-fails mid-preview.
- return send(res, 200, { text: demoOutput(stepKey), demo: true, cost: 0, estCost: est, model: 'demo', warning: 'live call failed: ' + e.message });
+ return send(res, 200, { text: demoOutput(stepKey), demo: true, provider: 'demo', cost: 0, estCost: est, model: 'demo', warning: 'live call failed: ' + e.message });
}
});
return;
@@ -143,5 +191,6 @@ const server = http.createServer((req, res) => {
});
server.listen(PORT, function () {
- console.log('[studio-zero] http://localhost:' + this.address().port + ' (' + (HAS_KEY ? 'LIVE ' + MODEL : 'DEMO mode — no key') + ')');
+ const mode = LIVE ? (PROVIDER === 'cli' ? 'LIVE · Max plan (' + CLI_MODEL + ')' : 'LIVE · API ' + MODEL) : 'DEMO mode';
+ console.log('[studio-zero] http://localhost:' + this.address().port + ' (' + mode + ')');
});
← f7ce02e studio-zero: guided 7-step AI YouTube channel builder (David
·
back to Studio Zero
·
studio-zero: robust calendar title parser (whole-line bold t 1518094 →