← back to Slack Idea Board
idea-board: basic auth + per-card action buttons (copy claude kickoff cmd) — session close
4309c45e0296177e584606d8b5f22f1a61672ce7 · 2026-07-13 14:55:12 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit 4309c45e0296177e584606d8b5f22f1a61672ce7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 14:55:12 2026 -0700
idea-board: basic auth + per-card action buttons (copy claude kickoff cmd) — session close
---
public/index.html | 34 +++++++++++++++++++++++++++-------
server.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+), 7 deletions(-)
diff --git a/public/index.html b/public/index.html
index 632db85..c74dc7e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -60,6 +60,9 @@
box-shadow:0 8px 30px rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;z-index:50}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast b{color:var(--gold)} #toast code{color:var(--dim);font-size:11.5px;word-break:break-all}
+ #accel{background:linear-gradient(135deg,#6d28d9,#2563eb);color:#fff;border:0;border-radius:8px;
+ padding:8px 14px;font-weight:600;font-size:13px;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.25);margin-right:14px}
+ #accel:hover{filter:brightness(1.08)} #accel:active{transform:translateY(1px)} #accel:disabled{opacity:.6;cursor:default}
</style>
</head>
<body>
@@ -72,6 +75,7 @@
<button data-f="all">All</button>
</span>
<span class="spacer"></span>
+ <button id="accel" title="Launch the Claude web-dev accelerator in a new terminal to rapidly prototype & launch a client project" onclick="launchAccelerator(this)">🚀 Launch Web-Dev Accelerator</button>
<span class="live"><span class="dot"></span> live · auto-refresh 20s</span>
</header>
<table>
@@ -107,16 +111,32 @@ async function copyText(t){
catch{ try{ const ta=document.createElement('textarea'); ta.value=t; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select(); const ok=document.execCommand('copy'); ta.remove(); return ok; }catch{ return false; } }
}
-async function doAction(btn){
+async function copyAction(btn){
const cmd=btn.dataset.cmd, label=btn.dataset.label;
const ok=await copyText(cmd);
- if(ok){ btn.classList.add('copied'); const t=btn.textContent; btn.textContent='✓ Copied';
- setTimeout(()=>{ btn.classList.remove('copied'); btn.textContent=t; },1400);
- toast(`<b>${esc(label)}</b> command copied — paste into a Claude Code session:<br><code>${esc(cmd)}</code>`);
- } else { toast('Could not access clipboard — command:<br><code>'+esc(cmd)+'</code>'); }
+ if(ok){ toast(`<b>${esc(label)}</b> command copied — paste into a Claude Code session:<br><code>${esc(cmd)}</code>`); }
+ else { toast('Could not access clipboard — command:<br><code>'+esc(cmd)+'</code>'); }
+}
+// Actually LAUNCH a Claude session in a new terminal via the backend.
+async function launchAction(id, step, label, btn){
+ if(btn){ btn.classList.add('copied'); var prev=btn.textContent; btn.textContent='launching…'; btn.disabled=true; }
+ try{
+ const r=await fetch('/api/build',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,step})});
+ const j=await r.json();
+ if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new iTerm2 tab<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
+ else { toast('Launch failed: '+esc(j.error||'unknown')); }
+ }catch(err){ toast('Launch failed: '+esc(String(err.message||err))); }
+ finally{ if(btn){ btn.classList.remove('copied'); btn.textContent=prev; btn.disabled=false; } }
}
// Wire clicks via delegation so re-renders never lose handlers.
-document.addEventListener('click', e=>{ const b=e.target.closest('.act[data-cmd]'); if(b){ e.preventDefault(); doAction(b); } });
+// Click = launch a real session · Shift-click = copy the command to paste manually.
+document.addEventListener('click', e=>{
+ const b=e.target.closest('.act[data-cmd]'); if(!b) return; e.preventDefault();
+ if(e.shiftKey || !b.dataset.id) return copyAction(b);
+ launchAction(b.dataset.id, b.dataset.step||b.dataset.label, b.dataset.label, b);
+});
+// Header button — launch the web-dev accelerator directly (no idea seed).
+async function launchAccelerator(btn){ return launchAction('', 'Build new project', 'Web-Dev Accelerator', btn); }
function fmt(iso){try{return new Date(iso).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch{return iso}}
let LAST = { ideas:[], model:'' }, FILTER = 'actionable';
function passes(i){
@@ -154,7 +174,7 @@ function render(){
const isSkip = sc==='skip' || !cmd;
const primary = isSkip
? `<button class="act skip" disabled title="${esc(i.next_detail||'No action for this idea')}">Skip</button>`
- : `<button class="act ${sc}" data-cmd="${esc(cmd)}" data-label="${step}" title="${esc(i.next_detail||step)}">${step}</button>`;
+ : `<button class="act ${sc}" data-id="${esc(i.id)}" data-step="${esc(i.next_step||'')}" data-cmd="${esc(cmd)}" data-label="${step}" title="Click to LAUNCH a Claude session in a new terminal · Shift-click to copy the command instead">${step} ▶</button>`;
const open = i.url ? `<a class="act open" href="${esc(i.url)}" target="_blank" rel="noopener noreferrer">Open link ↗</a>` : '';
return `<span class="step ${sc}">${step}</span>`
+ `<div class="detail">${esc(i.next_detail)}</div>`
diff --git a/server.js b/server.js
index 4dc168a..8d1f289 100644
--- a/server.js
+++ b/server.js
@@ -5,8 +5,12 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
+const os = require('os');
+const { spawn } = require('child_process');
const DIR = __dirname;
+// Where "Build new project" sessions are launched from — the Claude web-dev accelerator.
+const ACCEL_DIR = process.env.ACCEL_DIR || path.join(os.homedir(), 'Projects', 'claude-webdev-accelerator');
const ENV = loadEnv('/Users/macstudio3/Projects/slack-to-steve/.env'); // reuse the bot token + channels
const TOKEN = ENV.SLACK_BOT_TOKEN;
const CHANNELS = (ENV.SLACK_CHANNEL_IDS || ENV.SLACK_CHANNEL_ID || '').split(',').map(s => s.trim()).filter(Boolean);
@@ -140,10 +144,60 @@ async function drain() {
setTimeout(drain, 2000);
}
+// Reconstruct a SAFE Claude kickoff prompt server-side (never trust a client-supplied command).
+function buildPrompt(idea, step) {
+ const from = idea && idea.title ? `${idea.title} — ${idea.url || ''}` : (idea ? (idea.url || '') : '');
+ const d = (idea && (idea.next_detail || idea.about || idea.title)) || 'this idea';
+ if (/skill/i.test(step)) return `/skill-creator Create a new skill with its own git repo: ${d}` + (from ? ` (from idea: ${from})` : '');
+ if (/agent/i.test(step)) return `Create a new agent: ${d}` + (from ? ` (from idea: ${from})` : '');
+ // Default = Build new project → route through the web-dev accelerator so it inherits the
+ // combined skills + agent playbook for rapidly prototyping & launching a high-value client build.
+ return [
+ `Use the Claude web-dev accelerator in this directory (see ACCELERATOR.md) to rapidly prototype and launch a new project.`,
+ from ? `Seed idea: ${from}.` : '',
+ `What to build: ${d}.`,
+ `Follow the accelerator playbook — pick the matching skills/agents, scaffold + gitify, stand up a local viewer, and stop for my go before anything is deployed or made public.`,
+ ].filter(Boolean).join(' ');
+}
+// Launch a real Claude Code session in a new iTerm2 tab. Prompt is passed via a temp file so no
+// shell/AppleScript escaping is needed and nothing from the client is interpolated into a command.
+function launchClaude(prompt, cwd) {
+ const stamp = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
+ const promptFile = path.join(os.tmpdir(), `idea-build-${stamp}.txt`);
+ const launcher = path.join(os.tmpdir(), `idea-build-${stamp}.sh`);
+ fs.writeFileSync(promptFile, prompt, 'utf8');
+ const dir = fs.existsSync(cwd) ? cwd : os.homedir();
+ fs.writeFileSync(launcher,
+ `#!/bin/bash\ncd ${JSON.stringify(dir)} || exit 1\nclaude "$(cat ${JSON.stringify(promptFile)})"\n`, { mode: 0o755 });
+ const osa = `tell application "iTerm2"
+ activate
+ tell current window to create tab with default profile
+ tell current session of current window to write text "bash ${launcher}"
+ end tell`;
+ return new Promise((resolve) => {
+ const p = spawn('osascript', ['-e', osa]);
+ let err = '';
+ p.stderr.on('data', d => err += d);
+ p.on('close', code => resolve({ ok: code === 0, code, err: err.trim(), promptFile }));
+ });
+}
+
const server = http.createServer(async (req, res) => {
try {
if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Idea Board"' }); return res.end('auth required'); }
if (req.url === '/favicon.ico' || req.url === '/apple-touch-icon.png') { res.writeHead(204); return res.end(); } // no icon → 204 (not a 404 console error)
+ if (req.method === 'POST' && req.url === '/api/build') {
+ let body = ''; for await (const c of req) { body += c; if (body.length > 1e5) break; }
+ let j = {}; try { j = JSON.parse(body || '{}'); } catch {}
+ const step = String(j.step || 'Build new project');
+ let idea = null;
+ if (j.id) { try { idea = (await collectIdeas()).find(i => i.id === j.id) || null; } catch {} }
+ const prompt = buildPrompt(idea, step);
+ const cwd = /skill|agent/i.test(step) ? os.homedir() : ACCEL_DIR;
+ const r = await launchClaude(prompt, cwd);
+ res.writeHead(r.ok ? 200 : 500, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ launched: r.ok, step, cwd, prompt, error: r.ok ? undefined : (r.err || 'osascript failed') }));
+ }
if (req.url.startsWith('/api/ideas')) {
const ideas = await collectIdeas();
enrichNew(ideas); // fire-and-forget; cached items return immediately
← 433194c idea-board: per-card action buttons driven by next_step (Add
·
back to Slack Idea Board
·
Wire real UX behind Build-new-project: /api/build launches a 0ad2604 →