diff --git a/blog/src/_data/media.json b/blog/src/_data/media.json index 6bcfcdb10..e4489ddb5 100644 --- a/blog/src/_data/media.json +++ b/blog/src/_data/media.json @@ -651,28 +651,15 @@ "youtube": [] }, "cline-vs-munder-difflin": { - "title": "Cline vs Munder Difflin: In-Editor Agent or Local Hive?", + "title": "Cline vs Munder Difflin: One Agent Everywhere, or a Team of Agents?", "category": "comparisons", "hero": { "file": "assets/media/cline-vs-munder-difflin/hero.png", - "alt": "Illustration: Cline vs Munder Difflin: In-Editor Agent or Local Hive?", - "prompt": "Hero image for a blog post titled \"Cline vs Munder Difflin: In-Editor Agent or Local Hive?\". The post is about: A fair comparison of Cline and Munder Difflin — an in-editor BYOK coding agent vs a local multi-agent orchestration hive — and when to pick which.", + "alt": "Cline vs Munder Difflin on white: a yellow Cline block wired to four tiles labelled VS Code, Terminal, Desktop and SDK, captioned one agent, many places, beside a lilac Munder Difflin block wired to three tiles labelled Claude Code, Codex and Gemini CLI, captioned many agents, one job.", + "prompt": "Still of the hero scene in assets/media/cline-vs-munder-difflin/motion.js.", "status": "ready" }, - "inline": { - "note-1": { - "file": "assets/media/cline-vs-munder-difflin/note-1.png", - "alt": "Hand-drawn sketch from “Cline vs Munder Difflin: In-Editor Agent or Local Hive?”", - "prompt": "", - "status": "ready" - }, - "note-2": { - "file": "assets/media/cline-vs-munder-difflin/note-2.png", - "alt": "Hand-drawn sketch from “Cline vs Munder Difflin: In-Editor Agent or Local Hive?”", - "prompt": "", - "status": "ready" - } - }, + "inline": {}, "youtube": [] }, "codex-cli-vs-claude-code": { @@ -5013,5 +5000,38 @@ "status": "ready" }, "inline": {} + }, + "deepseek-v4-1-flash-1017": { + "title": "DeepSeek V4.1 Flash 1017: What Changed, the Price and Claude Code Setup", + "category": "concepts", + "hero": { + "file": "assets/media/deepseek-v4-1-flash-1017/hero.png", + "alt": "Title image: DeepSeek V4.1 Flash 1017, what changed, price and setup, checked 17 Oct 2026, beside a stopwatch whose hand has swept most of the way round.", + "prompt": "Still of the hero scene in assets/media/deepseek-v4-1-flash-1017/motion.js.", + "status": "ready" + }, + "inline": {} + }, + "what-is-nanoclaw": { + "title": "What Is NanoClaw? The Small OpenClaw Alternative That Runs Agents in Containers", + "category": "concepts", + "hero": { + "file": "assets/media/what-is-nanoclaw/hero.png", + "alt": "What is NanoClaw: three chat app tiles labelled WhatsApp, Telegram and Slack are wired on white into a box labelled container, with a yellow agent tile inside.", + "prompt": "Still of the hero scene in assets/media/what-is-nanoclaw/motion.js.", + "status": "ready" + }, + "inline": {} + }, + "what-is-zeroclaw": { + "title": "What Is ZeroClaw? The Single Binary Agent Runtime in Rust, Explained", + "category": "concepts", + "hero": { + "file": "assets/media/what-is-zeroclaw/hero.png", + "alt": "What is ZeroClaw: a yellow hexagon labelled ZeroClaw on white, wired to three tiles labelled models, channels and tools.", + "prompt": "Still of the hero scene in assets/media/what-is-zeroclaw/motion.js.", + "status": "ready" + }, + "inline": {} } } diff --git a/blog/src/assets/media/cline-vs-munder-difflin/fork.png b/blog/src/assets/media/cline-vs-munder-difflin/fork.png new file mode 100644 index 000000000..c2084fb9a Binary files /dev/null and b/blog/src/assets/media/cline-vs-munder-difflin/fork.png differ diff --git a/blog/src/assets/media/cline-vs-munder-difflin/hero.png b/blog/src/assets/media/cline-vs-munder-difflin/hero.png index f4bc4703f..1f86d5b18 100644 Binary files a/blog/src/assets/media/cline-vs-munder-difflin/hero.png and b/blog/src/assets/media/cline-vs-munder-difflin/hero.png differ diff --git a/blog/src/assets/media/cline-vs-munder-difflin/motion.css b/blog/src/assets/media/cline-vs-munder-difflin/motion.css new file mode 100644 index 000000000..4ec09dc5a --- /dev/null +++ b/blog/src/assets/media/cline-vs-munder-difflin/motion.css @@ -0,0 +1,13 @@ +/* Bright coded scenes for "Cline vs Munder Difflin". Pairs with motion.js. */ +.prose figure.mg { margin: 42px 0; } +.prose figure.mg img, .mg .mg-svg { display: block; width: 100%; height: auto; border: 1px solid #1A1320; border-radius: 18px; background: #fff; } +.mg figcaption { font: 500 13px/1.5 "Space Grotesk", system-ui, sans-serif; color: #6B5878; padding-top: 10px; text-align: center; } +.mg .mg-svg { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } +.mg .mg-svg text { -webkit-user-select: none; user-select: none; pointer-events: none; } +.mg .mg-hit { cursor: pointer; outline: none; } +.mg [role="slider"] { cursor: grab; touch-action: none; } +.mg .mg-ring { stroke-opacity: 0; transition: stroke-opacity 0.15s; } +.mg .mg-hit:focus-visible .mg-ring { stroke-opacity: 1; } +.mg [role="slider"]:focus-visible circle:nth-child(2) { stroke-width: 5; } +@media (hover: hover) { .mg .mg-hit:hover .mg-ring { stroke-opacity: 0.5; } } +@media (prefers-reduced-motion: reduce) { .mg .mg-ring { transition: none; } } diff --git a/blog/src/assets/media/cline-vs-munder-difflin/motion.js b/blog/src/assets/media/cline-vs-munder-difflin/motion.js new file mode 100644 index 000000000..b0413f574 --- /dev/null +++ b/blog/src/assets/media/cline-vs-munder-difflin/motion.js @@ -0,0 +1,241 @@ +/* Bright coded scenes for "Cline vs Munder Difflin". Vanilla JS, no library. +
holds a still image; this script swaps in a live SVG. + Plays only in view, honours reduced motion, "?mgstill" freezes each scene on its still frame. */ +(() => { + const NS = "http://www.w3.org/2000/svg"; + const INK = "#1A1320", YEL = "#FFCA54", BLUE = "#6C8EF5", LIL = "#B69CFF", MINT = "#7FD8AE", SKY = "#9AD8FF", SOFT = "#F1ECF9", MUTE = "#7A6A88"; + const SANS = '"Space Grotesk", system-ui, sans-serif', MONO = '"JetBrains Mono", ui-monospace, monospace'; + const el = (t, a = {}, p) => { const n = document.createElementNS(NS, t); for (const k in a) n.setAttribute(k, a[k]); if (p) p.appendChild(n); return n; }; + const txt = (p, x, y, s, a = {}) => { const n = el("text", Object.assign({ x, y, "font-family": SANS, "font-size": 24, "font-weight": 600, fill: INK }, a), p); n.textContent = s; return n; }; + const cl = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); + const seg = (t, a, b) => cl((t - a) / (b - a)); + const oc = (x) => 1 - Math.pow(1 - x, 3); + const ob = (x) => { const c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); }; + const io = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); + const tf = (n, x, y, s = 1, r = 0) => n.setAttribute("transform", `translate(${x} ${y}) rotate(${r}) scale(${s})`); + const button = (g, label, fn) => { + g.setAttribute("role", "button"); g.setAttribute("tabindex", "0"); g.setAttribute("aria-label", label); g.classList.add("mg-hit"); + g.addEventListener("click", fn); + g.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); } }); + }; + + const lerp = (a, b, k) => a + (b - a) * k; + const SW = 6; /* one stroke width for every outline */ + const ring = (g, x, y, w, h, rx) => el("rect", { x, y, width: w, height: h, rx, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": SW, class: "mg-ring" }, g); + /* a four point sparkle, centred on 0,0 */ + const star = (p, r, fill) => el("path", { d: `M0 ${-r} Q${r * 0.2} ${-r * 0.2} ${r} 0 Q${r * 0.2} ${r * 0.2} 0 ${r} Q${-r * 0.2} ${r * 0.2} ${-r} 0 Q${-r * 0.2} ${-r * 0.2} 0 ${-r} Z`, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, p); + + const SC = {}; + /* a quadratic curve point, k from 0 to 1 */ + const qp = (a, c, b, k) => [(1 - k) * (1 - k) * a[0] + 2 * (1 - k) * k * c[0] + k * k * b[0], (1 - k) * (1 - k) * a[1] + 2 * (1 - k) * k * c[1] + k * k * b[1]]; + const qd = (a, c, b) => `M${a[0]} ${a[1]} Q${c[0]} ${c[1]} ${b[0]} ${b[1]}`; + /* a cable that draws itself in: pathLength 1, dashoffset from 1 to 0 */ + const cable = (p, d, col = INK) => el("path", { d, fill: "none", stroke: col, "stroke-width": SW, "stroke-linecap": "round", pathLength: 1, "stroke-dasharray": "1 1", "stroke-dashoffset": 1 }, p); + const desk = (p, x, y, w, h, label, size = 36) => { + const g = el("g", {}, p); + el("rect", { x: x + 12, y: y + 12, width: w, height: h, rx: 28, fill: SOFT }, g); + const r = el("rect", { x, y, width: w, height: h, rx: 28, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + w / 2, y + h / 2 + size * 0.36, label, { "text-anchor": "middle", "font-size": size, "font-weight": 700 }); + return { g, r }; + }; + + /* ---- 1. one Cline block feeds four tiles; tap a tile to mark it ---- */ + SC.sockets = { + w: 1600, h: 1200, dur: 10, still: 8, + build(svg, api) { + txt(svg, 80, 124, "What is Cline?", { "font-size": 52, "font-weight": 700 }); + txt(svg, 80, 184, "One agent engine, several places to use it", { "font-size": 32, "font-weight": 500, fill: MUTE }); + const C = [800, 640]; + const T = [["VS Code", "extension", 120, 300, BLUE], ["Terminal", "npm i -g cline", 1100, 300, SKY], ["Desktop", "macOS and Windows", 120, 860, MINT], ["SDK", "@cline/sdk", 1100, 860, LIL]]; + let pick = -1; + const cables = T.map(([, , x, y]) => { + const end = [x < 800 ? x + 380 : x, y + 85], mid = [(C[0] + end[0]) / 2, end[1]]; + return cable(svg, qd(C, mid, end)); + }); + const tiles = T.map(([name, sub, x, y, col], i) => { + const g = el("g", {}, svg); + ring(g, x - 10, y - 10, 400, 190, 38); + el("rect", { x: x + 12, y: y + 12, width: 380, height: 170, rx: 30, fill: SOFT }, g); + const r = el("rect", { x, y, width: 380, height: 170, rx: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + 190, y + 80, name, { "text-anchor": "middle", "font-size": 44, "font-weight": 700 }); + txt(g, x + 190, y + 130, sub, { "text-anchor": "middle", "font-size": 26, "font-weight": 500, "font-family": MONO }); + const s = el("g", {}, g); star(s, 26, YEL); + button(g, `${name}: ${sub}`, () => { pick = i; api.seek(7.2); }); + return { r, s, col, x, y }; + }); + const hub = el("g", {}, svg); + el("rect", { x: 640, y: 560, width: 320, height: 160, rx: 36, fill: YEL, stroke: INK, "stroke-width": SW }, hub); + txt(hub, 800, 658, "Cline", { "text-anchor": "middle", "font-size": 60, "font-weight": 700 }); + txt(svg, 800, 1130, "From Cline's README on GitHub, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)), pop = ob(seg(t, 0.1, 0.8)); + tf(hub, 800 * (1 - pop), 640 * (1 - pop), Math.max(pop, 0.001)); + tiles.forEach((d, i) => { + const a = 1 + i * 1.3, k = oc(seg(t, a, a + 0.9)) * out, lit = t > a + 0.9 && out > 0.5; + cables[i].setAttribute("stroke-dashoffset", 1 - k); + d.r.setAttribute("fill", lit ? d.col : "#fff"); + const on = pick === i && t > 7.2 && out > 0.5, sk = on ? ob(seg(t, 7.2, 7.7)) : 0; + tf(d.s, d.x + 372, d.y + 8, Math.max(sk, 0.001), 20 * sk); + d.r.parentNode.setAttribute("aria-pressed", pick === i); + }); + }; + }, + }; + + /* ---- 2. an orchestrator desk sends task cards to three agent desks; a button starts a new job ---- */ + SC.office = { + w: 1600, h: 1200, dur: 10, still: 8, + build(svg, api) { + txt(svg, 80, 124, "What is Munder Difflin?", { "font-size": 52, "font-weight": 700 }); + txt(svg, 80, 184, "One orchestrator splits the job between coding agents", { "font-size": 32, "font-weight": 500, fill: MUTE }); + const O = [800, 400], A = [["Claude Code", 120], ["Codex", 620], ["Gemini CLI", 1120]]; + const paths = A.map(([, x]) => { const end = [x + 180, 830], mid = [x + 180, 560]; return { a: O, c: mid, b: end }; }); + paths.forEach((p) => el("path", { d: qd(p.a, p.c, p.b), fill: "none", stroke: SOFT, "stroke-width": 14, "stroke-linecap": "round" }, svg)); + const o = desk(svg, 560, 270, 480, 140, "orchestrator", 44); + o.r.setAttribute("fill", LIL); + const desks = A.map(([name, x]) => { + const d = desk(svg, x, 830, 360, 170, name, 40); + const led = el("circle", { cx: x + 320, cy: 870, r: 16, fill: "#fff", stroke: INK, "stroke-width": SW }, d.g); + return led; + }); + const cards = A.map(() => { + const g = el("g", {}, svg); + el("rect", { x: -50, y: -34, width: 100, height: 68, rx: 12, fill: YEL, stroke: INK, "stroke-width": SW }, g); + el("path", { d: "M-28 -8 H28 M-28 12 H12", stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, g); + return g; + }); + const b = el("g", {}, svg); + ring(b, 1171, 81, 358, 114, 57); + const br = el("rect", { x: 1180, y: 90, width: 340, height: 96, rx: 48, fill: "#fff", stroke: INK, "stroke-width": SW }, b); + txt(b, 1350, 150, "New job", { "text-anchor": "middle", "font-size": 36, "font-weight": 700 }); + button(b, "Start a new job: the orchestrator sends a card to each agent", () => api.seek(0.3)); + txt(svg, 800, 1110, "Each agent runs on its own engine, which you install and sign in to", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)); + br.setAttribute("fill", t < 1 ? YEL : "#fff"); + cards.forEach((g, i) => { + const a = 1 + i * 1.4, k = io(seg(t, a, a + 1.6)), p = paths[i], [x, y] = qp(p.a, p.c, p.b, k); + const vis = t > a - 0.2 && out > 0.01; + tf(g, x, y - 20 * Math.sin(Math.PI * k), vis ? out : 0.001, 0); + desks[i].setAttribute("fill", k >= 1 && out > 0.5 ? MINT : "#fff"); + }); + }; + }, + }; + + /* ---- 3. a path splits at a signpost; two chips choose the branch ---- */ + SC.fork = { + w: 1600, h: 1200, dur: 10, still: 3.4, + build(svg, api) { + txt(svg, 80, 124, "Which one should you pick?", { "font-size": 52, "font-weight": 700 }); + const S = [800, 1010], F = [800, 640], L = [330, 330], R = [1270, 330]; + const trunk = cable(svg, `M${S[0]} ${S[1]} L${F[0]} ${F[1]}`, SOFT); trunk.setAttribute("stroke-width", 22); + const lb = cable(svg, qd(F, [330, 640], L), SOFT); lb.setAttribute("stroke-width", 22); + const rb = cable(svg, qd(F, [1270, 640], R), SOFT); rb.setAttribute("stroke-width", 22); + const tl = desk(svg, 130, 210, 400, 130, "Cline", 48), tr = desk(svg, 1070, 210, 400, 130, "Munder Difflin", 44); + /* the signpost */ + el("path", { d: "M800 640 V340", stroke: INK, "stroke-width": SW + 4, "stroke-linecap": "round" }, svg); + const board = (pts, label, tx, fill) => { + el("path", { d: pts, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, svg); + return txt(svg, tx, 0, label, { "text-anchor": "middle", "font-size": 28, "font-weight": 700 }); + }; + board("M780 432 H440 L400 472 L440 512 H780 Z", "one agent, many places", 600, SKY).setAttribute("y", 482); + board("M820 352 H1160 L1200 392 L1160 432 H820 Z", "many agents, one job", 1000, MINT).setAttribute("y", 402); + const dot = el("g", {}, svg); + el("circle", { r: 30, fill: YEL, stroke: INK, "stroke-width": SW }, dot); + let sel = 0; + const chips = [["One agent is enough", 230], ["I want a team", 1010]].map(([label, x], i) => { + const g = el("g", {}, svg); + ring(g, x - 9, 1061, 378, 104, 52); + const r = el("rect", { x, y: 1070, width: 360, height: 86, rx: 43, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + 180, 1124, label, { "text-anchor": "middle", "font-size": 30, "font-weight": 700 }); + button(g, `${label}: go to ${i ? "Munder Difflin" : "Cline"}`, () => { sel = i; api.seek(3.6); }); + return r; + }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)); + trunk.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0, 0.8))); + lb.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0.6, 1.4))); + rb.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0.6, 1.4))); + const up = io(seg(t, 1, 3)), go = io(seg(t, 4, 6.4)); + let x = lerp(S[0], F[0], up), y = lerp(S[1], F[1], up); + if (go > 0) [x, y] = sel ? qp(F, [1270, 640], [1270, 370], go) : qp(F, [330, 640], [330, 370], go); + tf(dot, x, y, Math.max(out, 0.001)); + const arrived = go >= 1 && out > 0.5; + tl.r.setAttribute("fill", arrived && sel === 0 ? SKY : "#fff"); + tr.r.setAttribute("fill", arrived && sel === 1 ? MINT : "#fff"); + chips.forEach((r, i) => { r.setAttribute("fill", i === sel && t > 3.6 ? YEL : "#fff"); r.parentNode.setAttribute("aria-pressed", i === sel); }); + }; + }, + }; + + /* ---- title card: one engine with many front ends, beside one office with many agents ---- */ + SC.hero = { + w: 1600, h: 900, dur: 6, still: 4, + build(svg) { + txt(svg, 800, 120, "Cline vs Munder Difflin", { "text-anchor": "middle", "font-size": 64, "font-weight": 700 }); + el("path", { d: "M800 200 V820", stroke: SOFT, "stroke-width": 8, "stroke-linecap": "round" }, svg); + const left = el("g", {}, svg), right = el("g", {}, svg); + /* left: one Cline block, four tiles */ + const LT = [["VS Code", 90, BLUE], ["Terminal", 260, SKY], ["Desktop", 430, MINT], ["SDK", 600, LIL]]; + const lc = LT.map(([, x]) => cable(left, qd([395, 420], [x + 70, 470], [x + 70, 600]))); + el("rect", { x: 255, y: 320, width: 280, height: 120, rx: 30, fill: YEL, stroke: INK, "stroke-width": SW }, left); + txt(left, 395, 396, "Cline", { "text-anchor": "middle", "font-size": 50, "font-weight": 700 }); + const lt = LT.map(([name, x, col]) => { + const g = el("g", {}, left); + el("rect", { x, y: 600, width: 140, height: 96, rx: 20, fill: col, stroke: INK, "stroke-width": SW }, g); + txt(g, x + 70, 658, name, { "text-anchor": "middle", "font-size": 24, "font-weight": 700 }); + return g; + }); + txt(left, 395, 780, "one agent, many places", { "text-anchor": "middle", "font-size": 32, "font-weight": 500, fill: MUTE }); + /* right: one orchestrator, three agents */ + const RT = [["Claude Code", 870], ["Codex", 1080], ["Gemini CLI", 1290]]; + const rc = RT.map(([, x]) => cable(right, qd([1185, 420], [x + 90, 470], [x + 90, 600]))); + el("rect", { x: 1010, y: 320, width: 350, height: 120, rx: 30, fill: LIL, stroke: INK, "stroke-width": SW }, right); + txt(right, 1185, 396, "Munder Difflin", { "text-anchor": "middle", "font-size": 44, "font-weight": 700 }); + const rt = RT.map(([name, x]) => { + const g = el("g", {}, right); + el("rect", { x, y: 600, width: 180, height: 96, rx: 20, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + 90, 658, name, { "text-anchor": "middle", "font-size": 26, "font-weight": 700 }); + return g; + }); + txt(right, 1185, 780, "many agents, one job", { "text-anchor": "middle", "font-size": 32, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 5.4, 5.9)); + [lc, rc].forEach((cs) => cs.forEach((c, i) => c.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0.5 + i * 0.3, 1.4 + i * 0.3)) * out))); + lt.concat(rt).forEach((g, i) => { const k = ob(seg(t, 1.2 + (i % 4) * 0.3, 1.8 + (i % 4) * 0.3)) * out; g.setAttribute("opacity", k > 0.02 ? 1 : 0); g.style.transformOrigin = "center"; g.style.transformBox = "fill-box"; g.style.transform = `scale(${Math.max(k, 0.001)})`; }); + }; + }, + }; + + /* ---- player ---- */ + const q = new URLSearchParams(location.search), frozen = q.has("mgstill"), fixed = parseFloat(q.get("mgstill")); + const calm = matchMedia("(prefers-reduced-motion: reduce)").matches; + const mount = (fig) => { + const sc = SC[fig.dataset.scene], img = fig.querySelector("img"); if (!sc || !img) return; + const W = sc.w || 800, H = sc.h || 600; + const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "mg-svg", role: "group", "aria-label": img.alt || "" }); + svg.style.aspectRatio = `${W} / ${H}`; + el("rect", { width: W, height: H, fill: "#fff" }, svg); + img.replaceWith(svg); + let off = 0, raf = 0, seen = false, held = calm; + const base = performance.now(), clock = () => (performance.now() - base) / 1000; + let draw = () => {}; + const frame = () => { const now = clock(); draw(held ? sc.still : (((now + off) % sc.dur) + sc.dur) % sc.dur, now); }; + const tick = () => { cancelAnimationFrame(raf); frame(); if (seen && !held) raf = requestAnimationFrame(tick); }; + const api = { now: clock, seek: (s) => { off = s - clock(); held = false; tick(); }, poke: () => { held = false; tick(); } }; + draw = sc.build(svg, api); + if (frozen) { draw(isNaN(fixed) ? sc.still : fixed, 0); return; } + new IntersectionObserver(([e]) => { + seen = e.isIntersecting; + if (seen) { tick(); if (!fig.dataset.sent && window.posthog) { fig.dataset.sent = 1; window.posthog.capture("blog_scene_view", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } } + else cancelAnimationFrame(raf); + }, { threshold: 0.25 }).observe(fig); + svg.addEventListener("click", () => { if (!fig.dataset.hit && window.posthog) { fig.dataset.hit = 1; window.posthog.capture("blog_scene_interact", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } }); + frame(); + }; + const go = () => document.querySelectorAll("figure.mg[data-scene]").forEach(mount); + const fonts = document.fonts && document.fonts.load ? Promise.all([document.fonts.load('700 32px "Space Grotesk"'), document.fonts.load('500 20px "Space Grotesk"'), document.fonts.load('700 20px "JetBrains Mono"')]).catch(() => {}) : Promise.resolve(); + const start = () => fonts.then(go, go); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})(); diff --git a/blog/src/assets/media/cline-vs-munder-difflin/office.png b/blog/src/assets/media/cline-vs-munder-difflin/office.png new file mode 100644 index 000000000..dd314c502 Binary files /dev/null and b/blog/src/assets/media/cline-vs-munder-difflin/office.png differ diff --git a/blog/src/assets/media/cline-vs-munder-difflin/sockets.png b/blog/src/assets/media/cline-vs-munder-difflin/sockets.png new file mode 100644 index 000000000..5ae8b7f29 Binary files /dev/null and b/blog/src/assets/media/cline-vs-munder-difflin/sockets.png differ diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/bars.png b/blog/src/assets/media/deepseek-v4-1-flash-1017/bars.png new file mode 100644 index 000000000..76568f1b5 Binary files /dev/null and b/blog/src/assets/media/deepseek-v4-1-flash-1017/bars.png differ diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/hero.png b/blog/src/assets/media/deepseek-v4-1-flash-1017/hero.png new file mode 100644 index 000000000..3b613ab0a Binary files /dev/null and b/blog/src/assets/media/deepseek-v4-1-flash-1017/hero.png differ diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.css b/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.css new file mode 100644 index 000000000..167ae2eff --- /dev/null +++ b/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.css @@ -0,0 +1,13 @@ +/* Bright coded scenes for "DeepSeek V4.1 Flash 1017". Pairs with motion.js. */ +.prose figure.mg { margin: 42px 0; } +.prose figure.mg img, .mg .mg-svg { display: block; width: 100%; height: auto; border: 1px solid #1A1320; border-radius: 18px; background: #fff; } +.mg figcaption { font: 500 13px/1.5 "Space Grotesk", system-ui, sans-serif; color: #6B5878; padding-top: 10px; text-align: center; } +.mg .mg-svg { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } +.mg .mg-svg text { -webkit-user-select: none; user-select: none; pointer-events: none; } +.mg .mg-hit { cursor: pointer; outline: none; } +.mg [role="slider"] { cursor: grab; touch-action: none; } +.mg .mg-ring { stroke-opacity: 0; transition: stroke-opacity 0.15s; } +.mg .mg-hit:focus-visible .mg-ring { stroke-opacity: 1; } +.mg [role="slider"]:focus-visible circle:nth-child(2) { stroke-width: 5; } +@media (hover: hover) { .mg .mg-hit:hover .mg-ring { stroke-opacity: 0.5; } } +@media (prefers-reduced-motion: reduce) { .mg .mg-ring { transition: none; } } diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.js b/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.js new file mode 100644 index 000000000..6c39edf29 --- /dev/null +++ b/blog/src/assets/media/deepseek-v4-1-flash-1017/motion.js @@ -0,0 +1,234 @@ +/* Bright coded scenes for "DeepSeek V4.1 Flash 1017". Vanilla JS, no library. +
holds a still image; this script swaps in a live SVG. + Plays only in view, honours reduced motion, "?mgstill" freezes each scene on its still frame. */ +(() => { + const NS = "http://www.w3.org/2000/svg"; + const INK = "#1A1320", YEL = "#FFCA54", BLUE = "#6C8EF5", LIL = "#B69CFF", MINT = "#7FD8AE", SKY = "#9AD8FF", SOFT = "#F1ECF9", MUTE = "#7A6A88"; + const SANS = '"Space Grotesk", system-ui, sans-serif', MONO = '"JetBrains Mono", ui-monospace, monospace'; + const el = (t, a = {}, p) => { const n = document.createElementNS(NS, t); for (const k in a) n.setAttribute(k, a[k]); if (p) p.appendChild(n); return n; }; + const txt = (p, x, y, s, a = {}) => { const n = el("text", Object.assign({ x, y, "font-family": SANS, "font-size": 24, "font-weight": 600, fill: INK }, a), p); n.textContent = s; return n; }; + const cl = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); + const seg = (t, a, b) => cl((t - a) / (b - a)); + const oc = (x) => 1 - Math.pow(1 - x, 3); + const ob = (x) => { const c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); }; + const io = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); + const tf = (n, x, y, s = 1, r = 0) => n.setAttribute("transform", `translate(${x} ${y}) rotate(${r}) scale(${s})`); + const button = (g, label, fn) => { + g.setAttribute("role", "button"); g.setAttribute("tabindex", "0"); g.setAttribute("aria-label", label); g.classList.add("mg-hit"); + g.addEventListener("click", fn); + g.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); } }); + }; + + const lerp = (a, b, k) => a + (b - a) * k; + const SW = 6; /* one stroke width for every outline */ + const ring = (g, x, y, w, h, rx) => el("rect", { x, y, width: w, height: h, rx, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": SW, class: "mg-ring" }, g); + /* a four point sparkle, centred on 0,0 */ + const star = (p, r, fill) => el("path", { d: `M0 ${-r} Q${r * 0.2} ${-r * 0.2} ${r} 0 Q${r * 0.2} ${r * 0.2} 0 ${r} Q${-r * 0.2} ${r * 0.2} ${-r} 0 Q${-r * 0.2} ${-r * 0.2} 0 ${-r} Z`, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, p); + + const SC = {}; + + /* ---- 1. two bars race for each benchmark row; buttons switch the row ---- */ + SC.bars = { + w: 1600, h: 1200, dur: 10, still: 7, + build(svg, api) { + txt(svg, 80, 124, "How does DeepSeek V4.1 Flash 1017 score?", { "font-size": 52, "font-weight": 700 }); + const D = [["Terminal-Bench 4.0", 31.2, 42.3], ["DeepSWE v1.1", 74.2, 77.0], ["Automation-Bench", 54.8, 60.5]]; + let sel = 0; + const chips = D.map((d, i) => { + const g = el("g", {}, svg), x = 80 + i * 490; + ring(g, x - 9, 191, 468, 114, 57); + const r = el("rect", { x, y: 200, width: 450, height: 96, rx: 48, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + 225, 262, d[0], { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + button(g, `${d[0]}: V4.1 Flash ${d[1]}, V4.1 Flash 1017 ${d[2]}`, () => { sel = i; api.seek(0.4); }); + return r; + }); + const BASE = 960, SCALE = 5.6, B = [[380, LIL, "V4.1 Flash"], [900, YEL, "V4.1 Flash 1017"]]; + [0, 25, 50, 75, 100].forEach((v) => el("path", { d: `M300 ${BASE - v * SCALE} H1300`, stroke: SOFT, "stroke-width": 4 }, svg)); + el("path", { d: `M260 ${BASE} H1340`, stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, svg); + const bars = B.map(([x, fill, name]) => { + const r = el("rect", { x, width: 320, rx: 18, fill, stroke: INK, "stroke-width": SW }, svg); + const v = txt(svg, x + 160, 0, "", { "text-anchor": "middle", "font-size": 64, "font-weight": 700, "font-family": MONO }); + txt(svg, x + 160, BASE + 64, name, { "text-anchor": "middle", "font-size": 38, "font-weight": 700 }); + return { r, v }; + }); + txt(svg, 800, 1120, "DeepSeek's own table on the model card, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + const W = [[0.6, 2.4], [2.6, 4.6]]; + return (t) => { + const d = D[sel], out = seg(t, 9.3, 9.9); + chips.forEach((r, i) => { r.setAttribute("fill", i === sel ? YEL : "#fff"); r.parentNode.setAttribute("aria-pressed", i === sel); }); + bars.forEach((b, i) => { + const k = oc(seg(t, W[i][0], W[i][1])) * (1 - io(out)), val = d[i + 1] * k, h = Math.max(val * SCALE, 0.001); + b.r.setAttribute("y", BASE - h); b.r.setAttribute("height", h); b.r.setAttribute("opacity", k > 0 ? 1 : 0); + b.v.setAttribute("y", BASE - h - 26); b.v.textContent = k > 0 ? val.toFixed(1) : ""; b.v.setAttribute("opacity", k > 0 ? 1 : 0); + }); + }; + }, + }; + + /* ---- 2. a receipt printer feeds out the deepseek-flash price; a button switches off peak and peak ---- */ + SC.receipt = { + w: 1600, h: 1200, dur: 12, still: 9.2, + build(svg, api) { + txt(svg, 80, 124, "How much does it cost?", { "font-size": 52, "font-weight": 700 }); + const M = [["Off peak", "$0.15", "$0.60"], ["Peak", "$0.30", "$1.20"]]; + let sel = 0; + txt(svg, 1084, 340, "deepseek-flash rates", { "font-size": 36, "font-weight": 500, fill: MUTE }); + const chips = M.map((m, i) => { + const g = el("g", {}, svg), y = 374 + i * 136; + ring(g, 1071, y - 9, 468, 122, 61); + const r = el("rect", { x: 1080, y, width: 450, height: 104, rx: 52, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, 1305, y + 65, m[0], { "text-anchor": "middle", "font-size": 38, "font-weight": 700 }); + button(g, `deepseek-flash at ${m[0].toLowerCase()} rates: ${m[1]} input on a cache miss and ${m[2]} output, per million tokens`, () => { sel = i; api.seek(0.4); }); + return { g, r }; + }); + const SL = 935, PX = 250, PW = 600, PH = 735, CX = PX + PW / 2; + const defs = el("defs", {}, svg), cp = el("clipPath", { id: "ds-slot" }, defs); + el("rect", { x: 0, y: 0, width: 1600, height: SL }, cp); + const paper = el("g", {}, el("g", { "clip-path": "url(#ds-slot)" }, svg)); + let d = `M${PX} 18`; + for (let i = 1; i <= 20; i++) d += ` L${PX + i * 30} ${i % 2 ? 0 : 18}`; + d += ` V${PH} H${PX} Z`; + el("path", { d, fill: LIL, transform: "translate(16 0)" }, paper); + el("path", { d, fill: "#fff", stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, paper); + const L = (y, s, size, weight, fill = INK) => txt(paper, CX, y, s, { "text-anchor": "middle", "font-size": size, "font-weight": weight, "font-family": MONO, fill }); + const rule = (y) => el("path", { d: `M${PX + 40} ${y} H${PX + PW - 40}`, stroke: LIL, "stroke-width": SW, "stroke-dasharray": "2 16", "stroke-linecap": "round" }, paper); + L(84, "deepseek-flash", 42, 700); + const mode = L(138, "", 32, 500, MUTE); + rule(178); + L(238, "Input, cache miss", 32, 500); + const vin = L(318, "", 72, 700); + L(388, "Output", 32, 500); + const vout = L(468, "", 72, 700); + L(538, "per million tokens", 32, 500); + L(604, "17 Oct 2026", 40, 700); + rule(650); + const pr = el("g", {}, svg); + el("rect", { x: 100, y: SL - 12, width: 1400, height: 205, rx: 40, fill: BLUE, stroke: INK, "stroke-width": SW }, pr); + el("rect", { x: PX - 24, y: SL - 11, width: PW + 48, height: 22, rx: 11, fill: INK }, pr); + [1010, 1046, 1082].forEach((y) => el("path", { d: `M${PX} ${y} H${PX + 300}`, stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, pr)); + const led = el("circle", { cx: 880, cy: 1046, r: 15, fill: "#fff", stroke: INK, "stroke-width": SW }, pr); + el("circle", { cx: 962, cy: 1046, r: 34, fill: YEL, stroke: INK, "stroke-width": SW }, pr); + el("rect", { x: 1070, y: 990, width: 380, height: 112, rx: 26, fill: "#fff", stroke: INK, "stroke-width": SW }, pr); + txt(pr, 1260, 1062, "DeepSeek API", { "text-anchor": "middle", "font-size": 44, "font-weight": 700 }); + const T0 = 0.7, ST = 0.62, TG = [150, 255, 340, 405, 490, 555, 625, 735]; + return (t) => { + const m = M[sel]; + mode.textContent = `${m[0]} rates`; vin.textContent = m[1]; vout.textContent = m[2]; + chips.forEach((c, i) => { c.r.setAttribute("fill", i === sel ? YEL : "#fff"); c.g.setAttribute("aria-pressed", i === sel); }); + let F = 0, busy = false; + TG.forEach((g, i) => { const a = T0 + i * ST, k = seg(t, a, a + 0.42); if (k > 0 && k < 1) busy = true; F = lerp(F, g, io(k)); }); + const out = seg(t, 11.3, 11.9); + paper.setAttribute("transform", `translate(0 ${SL - F - 80 * io(out)})`); + paper.setAttribute("opacity", 1 - out); + pr.setAttribute("transform", `translate(0 ${busy ? Math.sin(t * 90) * 1.5 : 0})`); + led.setAttribute("fill", busy ? MINT : "#fff"); + }; + }, + }; + + /* ---- 3. a terminal types the setup, then a plug slides into the DeepSeek socket and a light turns green ---- */ + SC.plug = { + w: 1600, h: 1200, dur: 10, still: 8, + build(svg, api) { + txt(svg, 80, 124, "How do you use it in Claude Code?", { "font-size": 52, "font-weight": 700 }); + const term = el("g", {}, svg); + ring(term, 71, 211, 778, 618, 30); + el("rect", { x: 96, y: 236, width: 760, height: 600, rx: 24, fill: LIL }, term); + el("rect", { x: 80, y: 220, width: 760, height: 600, rx: 24, fill: "#fff", stroke: INK, "stroke-width": SW }, term); + el("path", { d: "M80 300 H840", stroke: INK, "stroke-width": SW }, term); + [[LIL, 128], [SKY, 174], [MINT, 220]].forEach(([f, x]) => el("circle", { cx: x, cy: 260, r: 16, fill: f, stroke: INK, "stroke-width": 4 }, term)); + const LN = [["ANTHROPIC_BASE_URL=", MUTE], [" api.deepseek.com/anthropic", INK], ["ANTHROPIC_MODEL=", MUTE], [" deepseek-flash[1m]", INK], ["$ claude", BLUE]]; + const lines = LN.map(([s, f], i) => txt(term, 120, 380 + i * 86, "", { "font-size": 34, "font-weight": 700, "font-family": MONO, fill: f })); + const caret = el("rect", { width: 18, height: 38, fill: INK }, term); + button(term, "Type the Claude Code setup again", () => api.seek(0.3)); + const SX = 1270, SY = 360, SWD = 250, SH = 280; + el("rect", { x: SX + 14, y: SY + 14, width: SWD, height: SH, rx: 30, fill: SKY }, svg); + el("rect", { x: SX, y: SY, width: SWD, height: SH, rx: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + [SY + 100, SY + 180].forEach((y) => el("rect", { x: SX - 3, y: y - 14, width: 46, height: 28, rx: 8, fill: INK }, svg)); + txt(svg, SX + SWD / 2, SY + SH + 70, "DeepSeek API", { "text-anchor": "middle", "font-size": 40, "font-weight": 700 }); + const light = el("circle", { cx: SX + SWD / 2, cy: SY - 70, r: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + const glow = el("circle", { cx: SX + SWD / 2, cy: SY - 70, r: 30, fill: "none", stroke: MINT, "stroke-width": 8 }, svg); + const plug = el("g", {}, svg); + const cable = el("path", { fill: "none", stroke: INK, "stroke-width": 14, "stroke-linecap": "round" }, svg); + [SY + 100, SY + 180].forEach((y) => el("rect", { x: 280, y: y - 10, width: 60, height: 20, rx: 6, fill: SOFT, stroke: INK, "stroke-width": 4 }, plug)); + el("rect", { x: 0, y: SY + 40, width: 290, height: 200, rx: 28, fill: YEL, stroke: INK, "stroke-width": SW }, plug); + txt(plug, 145, SY + 130, "Anthropic", { "text-anchor": "middle", "font-size": 38, "font-weight": 700 }); + txt(plug, 145, SY + 180, "format", { "text-anchor": "middle", "font-size": 38, "font-weight": 700 }); + txt(svg, 800, 1060, "From DeepSeek's Claude Code guide, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + const total = LN.reduce((n, l) => n + l[0].length, 0); + return (t) => { + const out = seg(t, 9.3, 9.9); + let n = Math.floor(total * seg(t, 0.4, 4.8)), cx = 120, cy = 380; + lines.forEach((ln, i) => { const s = LN[i][0], k = Math.max(0, Math.min(s.length, n)); n -= s.length; ln.textContent = s.slice(0, k); if (k > 0) { cx = 120 + k * 20.4; cy = 380 + i * 86; } }); + caret.setAttribute("x", cx + 4); caret.setAttribute("y", cy - 30); caret.setAttribute("opacity", Math.floor(t * 2) % 2 && t > 4.8 ? 0 : 1); + const k = ob(seg(t, 5.1, 6.1)), px = lerp(860, SX - 340, k) - 60 * io(out); + plug.setAttribute("transform", `translate(${px} 0)`); + cable.setAttribute("d", `M840 620 C${880} 700 ${px - 120} ${SY + 260} ${px} ${SY + 140}`); + const on = t >= 6.1 && out < 1; + light.setAttribute("fill", on ? MINT : "#fff"); + glow.setAttribute("r", 30 + 26 * seg(t, 6.1, 7.0)); glow.setAttribute("opacity", on ? 1 - seg(t, 6.1, 7.0) : 0); + }; + }, + }; + + /* ---- title image: a stopwatch hand sweeps and the elapsed arc fills ---- */ + SC.hero = { + w: 1600, h: 900, dur: 6, still: 4, + build(svg) { + txt(svg, 100, 214, "What changed, price and setup", { "font-size": 42, "font-weight": 500, fill: MUTE }); + txt(svg, 90, 370, "DeepSeek V4.1", { "font-size": 112, "font-weight": 700 }); + txt(svg, 90, 512, "Flash 1017", { "font-size": 112, "font-weight": 700 }); + el("path", { d: "M100 562 H560", stroke: YEL, "stroke-width": 18, "stroke-linecap": "round" }, svg); + el("rect", { x: 92, y: 620, width: 430, height: 80, rx: 40, fill: SOFT, stroke: INK, "stroke-width": SW }, svg); + txt(svg, 307, 673, "Checked 17 Oct 2026", { "text-anchor": "middle", "font-size": 36, "font-weight": 700 }); + const CX = 1290, CY = 500, R = 230; + el("rect", { x: CX - 40, y: CY - R - 70, width: 80, height: 56, rx: 14, fill: YEL, stroke: INK, "stroke-width": SW }, svg); + el("rect", { x: CX - 18, y: CY - R - 20, width: 36, height: 30, fill: INK }, svg); + el("circle", { cx: CX + 14, cy: CY + 14, r: R, fill: SKY }, svg); + el("circle", { cx: CX, cy: CY, r: R, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + const arc = el("path", { fill: MINT, opacity: 0.85 }, svg); + for (let i = 0; i < 12; i++) { const a = (i / 12) * Math.PI * 2; el("path", { d: `M${CX + Math.sin(a) * (R - 22)} ${CY - Math.cos(a) * (R - 22)} L${CX + Math.sin(a) * (R - 52)} ${CY - Math.cos(a) * (R - 52)}`, stroke: INK, "stroke-width": i % 3 ? 4 : 8, "stroke-linecap": "round" }, svg); } + const hand = el("path", { stroke: INK, "stroke-width": 10, "stroke-linecap": "round" }, svg); + el("circle", { cx: CX, cy: CY, r: 18, fill: YEL, stroke: INK, "stroke-width": SW }, svg); + const sp = [[1000, 250, 26, YEL], [1560, 330, 20, LIL], [1540, 760, 24, SKY], [1030, 760, 18, MINT]].map(([x, y, r, f]) => { const g = el("g", {}, svg); star(g, r, f); tf(g, x, y); return g; }); + return (t) => { + const k = io(seg(t, 0.3, 3.2)), a = k * Math.PI * 1.6, r = R - 70; + const x = CX + Math.sin(a) * r, y = CY - Math.cos(a) * r; + hand.setAttribute("d", `M${CX} ${CY} L${x} ${y}`); + arc.setAttribute("d", k > 0.001 ? `M${CX} ${CY} L${CX} ${CY - r} A${r} ${r} 0 ${a > Math.PI ? 1 : 0} 1 ${x} ${y} Z` : ""); + sp.forEach((g, i) => g.setAttribute("opacity", seg(t, 2.6 + i * 0.25, 3.0 + i * 0.25))); + }; + }, + }; + + /* ---- player ---- */ + const q = new URLSearchParams(location.search), frozen = q.has("mgstill"), fixed = parseFloat(q.get("mgstill")); + const calm = matchMedia("(prefers-reduced-motion: reduce)").matches; + const mount = (fig) => { + const sc = SC[fig.dataset.scene], img = fig.querySelector("img"); if (!sc || !img) return; + const W = sc.w || 800, H = sc.h || 600; + const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "mg-svg", role: "group", "aria-label": img.alt || "" }); + svg.style.aspectRatio = `${W} / ${H}`; + el("rect", { width: W, height: H, fill: "#fff" }, svg); + img.replaceWith(svg); + let off = 0, raf = 0, seen = false, held = calm; + const base = performance.now(), clock = () => (performance.now() - base) / 1000; + let draw = () => {}; + const frame = () => { const now = clock(); draw(held ? sc.still : (((now + off) % sc.dur) + sc.dur) % sc.dur, now); }; + const tick = () => { cancelAnimationFrame(raf); frame(); if (seen && !held) raf = requestAnimationFrame(tick); }; + const api = { now: clock, seek: (s) => { off = s - clock(); held = false; tick(); }, poke: () => { held = false; tick(); } }; + draw = sc.build(svg, api); + if (frozen) { draw(isNaN(fixed) ? sc.still : fixed, 0); return; } + new IntersectionObserver(([e]) => { + seen = e.isIntersecting; + if (seen) { tick(); if (!fig.dataset.sent && window.posthog) { fig.dataset.sent = 1; window.posthog.capture("blog_scene_view", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } } + else cancelAnimationFrame(raf); + }, { threshold: 0.25 }).observe(fig); + svg.addEventListener("click", () => { if (!fig.dataset.hit && window.posthog) { fig.dataset.hit = 1; window.posthog.capture("blog_scene_interact", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } }); + frame(); + }; + const go = () => document.querySelectorAll("figure.mg[data-scene]").forEach(mount); + const fonts = document.fonts && document.fonts.load ? Promise.all([document.fonts.load('700 32px "Space Grotesk"'), document.fonts.load('500 20px "Space Grotesk"'), document.fonts.load('700 20px "JetBrains Mono"')]).catch(() => {}) : Promise.resolve(); + const start = () => fonts.then(go, go); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})(); diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/plug.png b/blog/src/assets/media/deepseek-v4-1-flash-1017/plug.png new file mode 100644 index 000000000..d94133ec7 Binary files /dev/null and b/blog/src/assets/media/deepseek-v4-1-flash-1017/plug.png differ diff --git a/blog/src/assets/media/deepseek-v4-1-flash-1017/receipt.png b/blog/src/assets/media/deepseek-v4-1-flash-1017/receipt.png new file mode 100644 index 000000000..663cd4847 Binary files /dev/null and b/blog/src/assets/media/deepseek-v4-1-flash-1017/receipt.png differ diff --git a/blog/src/assets/media/what-is-nanoclaw/box.png b/blog/src/assets/media/what-is-nanoclaw/box.png new file mode 100644 index 000000000..0477f7f08 Binary files /dev/null and b/blog/src/assets/media/what-is-nanoclaw/box.png differ diff --git a/blog/src/assets/media/what-is-nanoclaw/hero.png b/blog/src/assets/media/what-is-nanoclaw/hero.png new file mode 100644 index 000000000..cbfa1a250 Binary files /dev/null and b/blog/src/assets/media/what-is-nanoclaw/hero.png differ diff --git a/blog/src/assets/media/what-is-nanoclaw/motion.css b/blog/src/assets/media/what-is-nanoclaw/motion.css new file mode 100644 index 000000000..4ec056fbc --- /dev/null +++ b/blog/src/assets/media/what-is-nanoclaw/motion.css @@ -0,0 +1,13 @@ +/* Bright coded scenes for "What Is NanoClaw?". Pairs with motion.js. */ +.prose figure.mg { margin: 42px 0; } +.prose figure.mg img, .mg .mg-svg { display: block; width: 100%; height: auto; border: 1px solid #1A1320; border-radius: 18px; background: #fff; } +.mg figcaption { font: 500 13px/1.5 "Space Grotesk", system-ui, sans-serif; color: #6B5878; padding-top: 10px; text-align: center; } +.mg .mg-svg { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } +.mg .mg-svg text { -webkit-user-select: none; user-select: none; pointer-events: none; } +.mg .mg-hit { cursor: pointer; outline: none; } +.mg [role="slider"] { cursor: grab; touch-action: none; } +.mg .mg-ring { stroke-opacity: 0; transition: stroke-opacity 0.15s; } +.mg .mg-hit:focus-visible .mg-ring { stroke-opacity: 1; } +.mg [role="slider"]:focus-visible circle:nth-child(2) { stroke-width: 5; } +@media (hover: hover) { .mg .mg-hit:hover .mg-ring { stroke-opacity: 0.5; } } +@media (prefers-reduced-motion: reduce) { .mg .mg-ring { transition: none; } } diff --git a/blog/src/assets/media/what-is-nanoclaw/motion.js b/blog/src/assets/media/what-is-nanoclaw/motion.js new file mode 100644 index 000000000..953b436b1 --- /dev/null +++ b/blog/src/assets/media/what-is-nanoclaw/motion.js @@ -0,0 +1,221 @@ +/* Bright coded scenes for "What Is NanoClaw?". Vanilla JS, no library. +
holds a still image; this script swaps in a live SVG. + Plays only in view, honours reduced motion, "?mgstill" freezes each scene on its still frame. */ +(() => { + const NS = "http://www.w3.org/2000/svg"; + const INK = "#1A1320", YEL = "#FFCA54", BLUE = "#6C8EF5", LIL = "#B69CFF", MINT = "#7FD8AE", SKY = "#9AD8FF", SOFT = "#F1ECF9", MUTE = "#7A6A88"; + const SANS = '"Space Grotesk", system-ui, sans-serif', MONO = '"JetBrains Mono", ui-monospace, monospace'; + const el = (t, a = {}, p) => { const n = document.createElementNS(NS, t); for (const k in a) n.setAttribute(k, a[k]); if (p) p.appendChild(n); return n; }; + const txt = (p, x, y, s, a = {}) => { const n = el("text", Object.assign({ x, y, "font-family": SANS, "font-size": 24, "font-weight": 600, fill: INK }, a), p); n.textContent = s; return n; }; + const cl = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); + const seg = (t, a, b) => cl((t - a) / (b - a)); + const oc = (x) => 1 - Math.pow(1 - x, 3); + const ob = (x) => { const c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); }; + const io = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); + const tf = (n, x, y, s = 1, r = 0) => n.setAttribute("transform", `translate(${x} ${y}) rotate(${r}) scale(${s})`); + const button = (g, label, fn) => { + g.setAttribute("role", "button"); g.setAttribute("tabindex", "0"); g.setAttribute("aria-label", label); g.classList.add("mg-hit"); + g.addEventListener("click", fn); + g.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); } }); + }; + + const lerp = (a, b, k) => a + (b - a) * k; + const SW = 6; /* one stroke width for every outline */ + const ring = (g, x, y, w, h, rx) => el("rect", { x, y, width: w, height: h, rx, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": SW, class: "mg-ring" }, g); + /* a four point sparkle, centred on 0,0 */ + const star = (p, r, fill) => el("path", { d: `M0 ${-r} Q${r * 0.2} ${-r * 0.2} ${r} 0 Q${r * 0.2} ${r * 0.2} 0 ${r} Q${-r * 0.2} ${r * 0.2} ${-r} 0 Q${-r * 0.2} ${-r * 0.2} 0 ${-r} Z`, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, p); + + const SC = {}; + const qp = (a, c, b, k) => [(1 - k) * (1 - k) * a[0] + 2 * (1 - k) * k * c[0] + k * k * b[0], (1 - k) * (1 - k) * a[1] + 2 * (1 - k) * k * c[1] + k * k * b[1]]; + const qd = (a, c, b) => `M${a[0]} ${a[1]} Q${c[0]} ${c[1]} ${b[0]} ${b[1]}`; + const cable = (p, d, col = INK) => el("path", { d, fill: "none", stroke: col, "stroke-width": SW, "stroke-linecap": "round", pathLength: 1, "stroke-dasharray": "1 1", "stroke-dashoffset": 1 }, p); + /* a folder with a tab, top left at 0,0, drawn into its own group */ + const folder = (p, w, h, fill, label) => { + const g = el("g", {}, p); + el("path", { d: `M0 22 V${h} H${w} V22 Z`, fill: SOFT, transform: "translate(12 12)" }, g); + el("path", { d: `M0 0 H${w * 0.4} L${w * 0.4 + 22} 22 H${w} V${h} H0 Z`, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, g); + txt(g, w / 2, h / 2 + 24, label, { "text-anchor": "middle", "font-size": 30, "font-weight": 700 }); + return g; + }; + const chip = (p, x, y, w, label, aria, fn) => { + const g = el("g", {}, p); + ring(g, x - 9, y - 9, w + 18, 114, 57); + const r = el("rect", { x, y, width: w, height: 96, rx: 48, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + w / 2, y + 60, label, { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + button(g, aria, fn); + return r; + }; + + /* ---- 1. a container box; only the mounted folder goes in; a button toggles the mount ---- */ + SC.box = { + w: 1600, h: 1200, dur: 10, still: 7, + build(svg, api) { + txt(svg, 80, 124, "What is NanoClaw?", { "font-size": 52, "font-weight": 700 }); + txt(svg, 80, 184, "Each agent group works inside its own container", { "font-size": 32, "font-weight": 500, fill: MUTE }); + let on = true; + const tog = chip(svg, 1150, 90, 370, "Mount: on", "Toggle whether the folder is mounted into the container", () => { on = !on; tog.nextSibling.textContent = on ? "Mount: on" : "Mount: off"; api.seek(0.3); }); + /* the box: wall with a door gap on its left side */ + el("rect", { x: 712, y: 332, width: 800, height: 600, rx: 36, fill: SOFT }, svg); + el("rect", { x: 700, y: 320, width: 800, height: 600, rx: 36, fill: "#fff" }, svg); + el("path", { d: "M700 560 V356 Q700 320 736 320 H1464 Q1500 320 1500 356 V884 Q1500 920 1464 920 H736 Q700 920 700 884 V740", fill: "none", stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, svg); + const door = el("path", { d: "M700 560 V740", stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, svg); + txt(svg, 1100, 300, "container", { "text-anchor": "middle", "font-size": 34, "font-weight": 700, fill: MUTE }); + el("rect", { x: 950, y: 400, width: 300, height: 150, rx: 28, fill: YEL, stroke: INK, "stroke-width": SW }, svg); + txt(svg, 1100, 488, "agent", { "text-anchor": "middle", "font-size": 44, "font-weight": 700 }); + const stay1 = folder(svg, 300, 170, "#fff", "not mounted"), stay2 = folder(svg, 300, 170, "#fff", "not mounted"); + tf(stay1, 120, 300); tf(stay2, 120, 760); + const mf = folder(svg, 300, 170, SKY, "mounted"); + txt(svg, 800, 1110, "From NanoClaw's README, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)), open = on ? oc(seg(t, 1, 1.6)) * (1 - oc(seg(t, 4.2, 4.8))) : 0; + door.setAttribute("opacity", 1 - open); + const k = on ? io(seg(t, 1.6, 3.8)) * out : 0; + const [x, y] = qp([120, 530], [560, 600], [950, 650], k); + tf(mf, x, y - 40 * Math.sin(Math.PI * k)); + tog.setAttribute("fill", on ? MINT : "#fff"); + }; + }, + }; + + /* ---- 2. two stacks of sheets grow; chips switch lines and config files ---- */ + SC.scale = { + w: 1600, h: 1200, dur: 10, still: 8, + build(svg, api) { + txt(svg, 80, 124, "NanoClaw next to OpenClaw", { "font-size": 52, "font-weight": 700 }); + const M = [["Lines", "Runtime TypeScript lines", 29300, 434453, (v) => (v >= 1000 ? Math.round(v).toLocaleString("en-US") : String(Math.round(v)))], ["Config files", "Manual config files", 0, 53, (v) => String(Math.round(v))]]; + let sel = 0; + const chips = M.map((m, i) => chip(svg, 80 + i * 330, 190, 300, m[0], `${m[1]}: NanoClaw ${i === 0 ? "about " : ""}${m[4](m[2])}, OpenClaw ${m[4](m[3])}`, () => { sel = i; api.seek(0.3); })); + const lab = txt(svg, 1520, 254, "", { "text-anchor": "end", "font-size": 32, "font-weight": 500, fill: MUTE }); + const defs = el("defs", {}, svg), pat = el("pattern", { id: "nc-sheets", width: 40, height: 22, patternUnits: "userSpaceOnUse" }, defs); + el("rect", { width: 40, height: 22, fill: "#fff" }, pat); + el("path", { d: "M0 20 H40", stroke: LIL, "stroke-width": 5 }, pat); + const BASE = 1010, MAXH = 600; + el("path", { d: `M200 ${BASE} H1400`, stroke: INK, "stroke-width": SW, "stroke-linecap": "round" }, svg); + const S = [[380, "NanoClaw", YEL], [900, "OpenClaw", SKY]].map(([x, name, col]) => { + el("rect", { x: x + 12, y: BASE + 30, width: 320, height: 0 }, svg); + const fillR = el("rect", { x, width: 320, fill: "url(#nc-sheets)" }, svg); + const edge = el("rect", { x, width: 320, rx: 10, fill: "none", stroke: INK, "stroke-width": SW }, svg); + const v = txt(svg, x + 160, 0, "", { "text-anchor": "middle", "font-size": 56, "font-weight": 700, "font-family": MONO }); + const tag = el("rect", { x: x + 40, y: BASE + 26, width: 240, height: 64, rx: 32, fill: col, stroke: INK, "stroke-width": SW }, svg); + txt(svg, x + 160, BASE + 70, name, { "text-anchor": "middle", "font-size": 32, "font-weight": 700 }); + return { fillR, edge, v }; + }); + txt(svg, 800, 1150, "NanoClaw's own figures, from nanoclaw.dev on 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const m = M[sel], out = 1 - io(seg(t, 9.3, 9.9)); + lab.textContent = m[1]; + chips.forEach((r, i) => { r.setAttribute("fill", i === sel ? YEL : "#fff"); r.parentNode.setAttribute("aria-pressed", i === sel); }); + [m[2], m[3]].forEach((val, i) => { + const k = oc(seg(t, 0.6 + i * 0.6, 3.4 + i * 1.2)) * out, cur = val * k, h = Math.max((cur / m[3]) * MAXH, 0.001); + S[i].fillR.setAttribute("y", BASE - h); S[i].fillR.setAttribute("height", h); + S[i].edge.setAttribute("y", BASE - h); S[i].edge.setAttribute("height", h); S[i].edge.setAttribute("opacity", h > 2 ? 1 : 0); + S[i].v.setAttribute("y", BASE - h - 28); S[i].v.textContent = k > 0.01 ? (sel === 0 && i === 0 ? "~" : "") + m[4](cur) : ""; S[i].v.setAttribute("opacity", k > 0.01 ? 1 : 0); + }); + }; + }, + }; + + /* ---- 3. a terminal runs the setup script; a checklist ticks off; a button runs it again ---- */ + SC.setup = { + w: 1600, h: 1200, dur: 11, still: 9.4, + build(svg, api) { + txt(svg, 80, 124, "How do you install NanoClaw?", { "font-size": 52, "font-weight": 700 }); + const again = chip(svg, 1150, 90, 370, "Run again", "Run the setup again", () => api.seek(0.3)); + el("rect", { x: 92, y: 302, width: 660, height: 520, rx: 30, fill: SOFT }, svg); + el("rect", { x: 80, y: 290, width: 660, height: 520, rx: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + el("path", { d: "M80 360 H740", stroke: INK, "stroke-width": SW }, svg); + [124, 164, 204].forEach((x, i) => el("circle", { cx: x, cy: 325, r: 12, fill: [YEL, SKY, MINT][i], stroke: INK, "stroke-width": 4 }, svg)); + const L = ["$ cd nanoclaw-v2", "$ bash nanoclaw.sh"].map((s, i) => ({ s, n: txt(svg, 120, 450 + i * 80, "", { "font-family": MONO, "font-size": 40, "font-weight": 700 }) })); + const caret = el("rect", { width: 22, height: 44, fill: BLUE }, svg); + const T = ["Node and pnpm", "Docker", "Credential gateway", "Agent container", "First channel"]; + const rows = T.map((s, i) => { + const y = 330 + i * 110, g = el("g", {}, svg); + const box = el("rect", { x: 860, y, width: 640, height: 86, rx: 24, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + const dot = el("circle", { cx: 912, cy: y + 43, r: 22, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + const tick = el("path", { d: `M900 ${y + 44} L910 ${y + 54} L926 ${y + 34}`, fill: "none", stroke: INK, "stroke-width": SW, "stroke-linecap": "round", "stroke-linejoin": "round", pathLength: 1, "stroke-dasharray": "1 1" }, g); + txt(g, 960, y + 55, s, { "font-size": 34, "font-weight": 700 }); + return { box, dot, tick }; + }); + txt(svg, 800, 1000, "It installs Node, pnpm and Docker if they are missing", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + txt(svg, 800, 1050, "From NanoClaw's README, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 10.4, 10.9)); + let cx = 120, cy = 450; + L.forEach((l, i) => { + const a = 0.4 + i * 1.1, n = Math.round(seg(t, a, a + 0.8) * l.s.length); + l.n.textContent = out > 0.5 ? l.s.slice(0, n) : ""; + if (t > a) { cx = 120 + n * 24.1; cy = 450 + i * 80; } + }); + tf(caret, cx + 4, cy - 36); caret.setAttribute("opacity", Math.floor(t * 2.5) % 2 && out > 0.5 ? 1 : 0.15); + rows.forEach((r, i) => { + const a = 3 + i * 1.2, k = seg(t, a, a + 0.4) * out, done = k >= 1; + r.tick.setAttribute("stroke-dashoffset", 1 - k); + r.dot.setAttribute("fill", done ? MINT : "#fff"); + r.box.setAttribute("fill", done && i === 4 ? SKY : "#fff"); + }); + again.setAttribute("fill", t < 0.8 ? YEL : "#fff"); + }; + }, + }; + + /* ---- title card: chat apps wired into one container with an agent inside ---- */ + SC.hero = { + w: 1600, h: 900, dur: 6, still: 4, + build(svg) { + txt(svg, 100, 150, "What is NanoClaw?", { "font-size": 68, "font-weight": 700 }); + txt(svg, 100, 214, "A personal AI assistant that runs agents in containers", { "font-size": 32, "font-weight": 500, fill: MUTE }); + const apps = [["WhatsApp", 330, MINT], ["Telegram", 480, SKY], ["Slack", 630, LIL]]; + const cs = apps.map(([, y]) => cable(svg, qd([420, y + 45], [700, y + 45], [900, 520]))); + const tiles = apps.map(([name, y, col]) => { + const g = el("g", {}, svg); + el("rect", { x: 100, y, width: 320, height: 90, rx: 45, fill: col, stroke: INK, "stroke-width": SW }, g); + txt(g, 260, y + 58, name, { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + return g; + }); + el("rect", { x: 912, y: 312, width: 560, height: 440, rx: 36, fill: SOFT }, svg); + el("rect", { x: 900, y: 300, width: 560, height: 440, rx: 36, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + txt(svg, 1180, 370, "container", { "text-anchor": "middle", "font-size": 32, "font-weight": 700, fill: MUTE }); + const ag = el("g", {}, svg); + el("rect", { x: 1040, y: 450, width: 280, height: 140, rx: 28, fill: YEL, stroke: INK, "stroke-width": SW }, ag); + txt(ag, 1180, 534, "agent", { "text-anchor": "middle", "font-size": 46, "font-weight": 700 }); + return (t) => { + const out = 1 - io(seg(t, 5.4, 5.9)); + cs.forEach((c, i) => c.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0.6 + i * 0.4, 1.6 + i * 0.4)) * out)); + tiles.forEach((g, i) => g.setAttribute("opacity", seg(t, 0.2 + i * 0.3, 0.5 + i * 0.3) * out > 0.02 ? 1 : 0)); + const k = ob(seg(t, 2.2, 2.9)) * out; + tf(ag, 1180 * (1 - k), 520 * (1 - k), Math.max(k, 0.001)); + }; + }, + }; + + /* ---- player ---- */ + const q = new URLSearchParams(location.search), frozen = q.has("mgstill"), fixed = parseFloat(q.get("mgstill")); + const calm = matchMedia("(prefers-reduced-motion: reduce)").matches; + const mount = (fig) => { + const sc = SC[fig.dataset.scene], img = fig.querySelector("img"); if (!sc || !img) return; + const W = sc.w || 800, H = sc.h || 600; + const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "mg-svg", role: "group", "aria-label": img.alt || "" }); + svg.style.aspectRatio = `${W} / ${H}`; + el("rect", { width: W, height: H, fill: "#fff" }, svg); + img.replaceWith(svg); + let off = 0, raf = 0, seen = false, held = calm; + const base = performance.now(), clock = () => (performance.now() - base) / 1000; + let draw = () => {}; + const frame = () => { const now = clock(); draw(held ? sc.still : (((now + off) % sc.dur) + sc.dur) % sc.dur, now); }; + const tick = () => { cancelAnimationFrame(raf); frame(); if (seen && !held) raf = requestAnimationFrame(tick); }; + const api = { now: clock, seek: (s) => { off = s - clock(); held = false; tick(); }, poke: () => { held = false; tick(); } }; + draw = sc.build(svg, api); + if (frozen) { draw(isNaN(fixed) ? sc.still : fixed, 0); return; } + new IntersectionObserver(([e]) => { + seen = e.isIntersecting; + if (seen) { tick(); if (!fig.dataset.sent && window.posthog) { fig.dataset.sent = 1; window.posthog.capture("blog_scene_view", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } } + else cancelAnimationFrame(raf); + }, { threshold: 0.25 }).observe(fig); + svg.addEventListener("click", () => { if (!fig.dataset.hit && window.posthog) { fig.dataset.hit = 1; window.posthog.capture("blog_scene_interact", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } }); + frame(); + }; + const go = () => document.querySelectorAll("figure.mg[data-scene]").forEach(mount); + const fonts = document.fonts && document.fonts.load ? Promise.all([document.fonts.load('700 32px "Space Grotesk"'), document.fonts.load('500 20px "Space Grotesk"'), document.fonts.load('700 20px "JetBrains Mono"')]).catch(() => {}) : Promise.resolve(); + const start = () => fonts.then(go, go); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})(); diff --git a/blog/src/assets/media/what-is-nanoclaw/scale.png b/blog/src/assets/media/what-is-nanoclaw/scale.png new file mode 100644 index 000000000..949963649 Binary files /dev/null and b/blog/src/assets/media/what-is-nanoclaw/scale.png differ diff --git a/blog/src/assets/media/what-is-nanoclaw/setup.png b/blog/src/assets/media/what-is-nanoclaw/setup.png new file mode 100644 index 000000000..07ed22318 Binary files /dev/null and b/blog/src/assets/media/what-is-nanoclaw/setup.png differ diff --git a/blog/src/assets/media/what-is-zeroclaw/gate.png b/blog/src/assets/media/what-is-zeroclaw/gate.png new file mode 100644 index 000000000..4308750f3 Binary files /dev/null and b/blog/src/assets/media/what-is-zeroclaw/gate.png differ diff --git a/blog/src/assets/media/what-is-zeroclaw/hero.png b/blog/src/assets/media/what-is-zeroclaw/hero.png new file mode 100644 index 000000000..2727d2453 Binary files /dev/null and b/blog/src/assets/media/what-is-zeroclaw/hero.png differ diff --git a/blog/src/assets/media/what-is-zeroclaw/hub.png b/blog/src/assets/media/what-is-zeroclaw/hub.png new file mode 100644 index 000000000..b3b816a7c Binary files /dev/null and b/blog/src/assets/media/what-is-zeroclaw/hub.png differ diff --git a/blog/src/assets/media/what-is-zeroclaw/motion.css b/blog/src/assets/media/what-is-zeroclaw/motion.css new file mode 100644 index 000000000..7c310d97e --- /dev/null +++ b/blog/src/assets/media/what-is-zeroclaw/motion.css @@ -0,0 +1,13 @@ +/* Bright coded scenes for "What Is ZeroClaw?". Pairs with motion.js. */ +.prose figure.mg { margin: 42px 0; } +.prose figure.mg img, .mg .mg-svg { display: block; width: 100%; height: auto; border: 1px solid #1A1320; border-radius: 18px; background: #fff; } +.mg figcaption { font: 500 13px/1.5 "Space Grotesk", system-ui, sans-serif; color: #6B5878; padding-top: 10px; text-align: center; } +.mg .mg-svg { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } +.mg .mg-svg text { -webkit-user-select: none; user-select: none; pointer-events: none; } +.mg .mg-hit { cursor: pointer; outline: none; } +.mg [role="slider"] { cursor: grab; touch-action: none; } +.mg .mg-ring { stroke-opacity: 0; transition: stroke-opacity 0.15s; } +.mg .mg-hit:focus-visible .mg-ring { stroke-opacity: 1; } +.mg [role="slider"]:focus-visible circle:nth-child(2) { stroke-width: 5; } +@media (hover: hover) { .mg .mg-hit:hover .mg-ring { stroke-opacity: 0.5; } } +@media (prefers-reduced-motion: reduce) { .mg .mg-ring { transition: none; } } diff --git a/blog/src/assets/media/what-is-zeroclaw/motion.js b/blog/src/assets/media/what-is-zeroclaw/motion.js new file mode 100644 index 000000000..23238c8de --- /dev/null +++ b/blog/src/assets/media/what-is-zeroclaw/motion.js @@ -0,0 +1,223 @@ +/* Bright coded scenes for "What Is ZeroClaw?". Vanilla JS, no library. +
holds a still image; this script swaps in a live SVG. + Plays only in view, honours reduced motion, "?mgstill" freezes each scene on its still frame. */ +(() => { + const NS = "http://www.w3.org/2000/svg"; + const INK = "#1A1320", YEL = "#FFCA54", BLUE = "#6C8EF5", LIL = "#B69CFF", MINT = "#7FD8AE", SKY = "#9AD8FF", SOFT = "#F1ECF9", MUTE = "#7A6A88"; + const SANS = '"Space Grotesk", system-ui, sans-serif', MONO = '"JetBrains Mono", ui-monospace, monospace'; + const el = (t, a = {}, p) => { const n = document.createElementNS(NS, t); for (const k in a) n.setAttribute(k, a[k]); if (p) p.appendChild(n); return n; }; + const txt = (p, x, y, s, a = {}) => { const n = el("text", Object.assign({ x, y, "font-family": SANS, "font-size": 24, "font-weight": 600, fill: INK }, a), p); n.textContent = s; return n; }; + const cl = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); + const seg = (t, a, b) => cl((t - a) / (b - a)); + const oc = (x) => 1 - Math.pow(1 - x, 3); + const ob = (x) => { const c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); }; + const io = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); + const tf = (n, x, y, s = 1, r = 0) => n.setAttribute("transform", `translate(${x} ${y}) rotate(${r}) scale(${s})`); + const button = (g, label, fn) => { + g.setAttribute("role", "button"); g.setAttribute("tabindex", "0"); g.setAttribute("aria-label", label); g.classList.add("mg-hit"); + g.addEventListener("click", fn); + g.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); } }); + }; + + const lerp = (a, b, k) => a + (b - a) * k; + const SW = 6; /* one stroke width for every outline */ + const ring = (g, x, y, w, h, rx) => el("rect", { x, y, width: w, height: h, rx, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": SW, class: "mg-ring" }, g); + /* a four point sparkle, centred on 0,0 */ + const star = (p, r, fill) => el("path", { d: `M0 ${-r} Q${r * 0.2} ${-r * 0.2} ${r} 0 Q${r * 0.2} ${r * 0.2} 0 ${r} Q${-r * 0.2} ${r * 0.2} ${-r} 0 Q${-r * 0.2} ${-r * 0.2} 0 ${-r} Z`, fill, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, p); + + const SC = {}; + const cable = (p, d, col = INK) => el("path", { d, fill: "none", stroke: col, "stroke-width": SW, "stroke-linecap": "round", pathLength: 1, "stroke-dasharray": "1 1", "stroke-dashoffset": 1 }, p); + const hexd = (cx, cy, r) => { let d = ""; for (let i = 0; i < 6; i++) { const a = Math.PI / 6 + (i * Math.PI) / 3; d += `${i ? "L" : "M"}${(cx + r * Math.cos(a)).toFixed(1)} ${(cy + r * Math.sin(a)).toFixed(1)} `; } return d + "Z"; }; + const tile = (p, x, y, w, h, label, size = 32) => { + const g = el("g", {}, p); + const r = el("rect", { x, y, width: w, height: h, rx: h / 2, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + w / 2, y + h / 2 + size * 0.36, label, { "text-anchor": "middle", "font-size": size, "font-weight": 700 }); + return r; + }; + const chip = (p, x, y, w, label, aria, fn) => { + const g = el("g", {}, p); + ring(g, x - 9, y - 9, w + 18, 114, 57); + const r = el("rect", { x, y, width: w, height: 96, rx: 48, fill: "#fff", stroke: INK, "stroke-width": SW }, g); + txt(g, x + w / 2, y + 60, label, { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + button(g, aria, fn); + return r; + }; + + /* ---- 1. one binary in the middle wires out to providers, channels and tools; tap the hexagon to replay ---- */ + SC.hub = { + w: 1600, h: 1200, dur: 10, still: 7.5, + build(svg, api) { + txt(svg, 80, 124, "What is ZeroClaw?", { "font-size": 52, "font-weight": 700 }); + txt(svg, 80, 184, "One Rust binary between a model, your channels and your tools", { "font-size": 32, "font-weight": 500, fill: MUTE }); + const H = [800, 560]; + const G = [ + ["providers", 240, 380, BLUE, [["Anthropic", 90, 410], ["OpenAI", 90, 530], ["Ollama", 90, 650]]], + ["channels", 1360, 380, SKY, [["Discord", 1210, 410], ["Telegram", 1210, 530], ["Email", 1210, 650]]], + ["tools", 200, 966, MINT, [["Shell", 300, 910], ["Browser", 650, 910], ["MCP servers", 1000, 910]]], + ]; + const groups = G.map(([name, lx, ly, col, items]) => { + txt(svg, lx, ly, name, { "text-anchor": "middle", "font-size": 30, "font-weight": 700, fill: MUTE }); + const lines = items.map(([, x, y]) => { + const end = name === "providers" ? [x + 300, y + 45] : name === "channels" ? [x, y + 45] : [x + 150, y]; + const mid = name === "tools" ? [end[0], H[1] + 120] : [(H[0] + end[0]) / 2, end[1]]; + return cable(svg, `M${H[0]} ${H[1]} Q${mid[0]} ${mid[1]} ${end[0]} ${end[1]}`); + }); + const tiles = items.map(([label, x, y]) => tile(svg, x, y, 300, 90, label)); + return { lines, tiles, col }; + }); + const hx = el("g", {}, svg); + ring(hx, 640, 400, 320, 320, 160); + el("path", { d: hexd(H[0] + 12, H[1] + 12, 150), fill: SOFT }, hx); + el("path", { d: hexd(H[0], H[1], 150), fill: YEL, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, hx); + txt(hx, H[0], H[1] + 16, "ZeroClaw", { "text-anchor": "middle", "font-size": 46, "font-weight": 700 }); + button(hx, "ZeroClaw: replay the connections", () => api.seek(0.3)); + txt(svg, 800, 1130, "From ZeroClaw's README, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)); + groups.forEach((g, i) => { + const a = 0.6 + i * 1.6, k = oc(seg(t, a, a + 1)) * out, lit = t > a + 1 && out > 0.5; + g.lines.forEach((l) => l.setAttribute("stroke-dashoffset", 1 - k)); + g.tiles.forEach((r) => r.setAttribute("fill", lit ? g.col : "#fff")); + }); + }; + }, + }; + + /* ---- 2. three action cards meet the supervised gate; a button approves the waiting card ---- */ + SC.gate = { + w: 1600, h: 1200, dur: 10, still: 6, + build(svg, api) { + txt(svg, 80, 124, "How does ZeroClaw keep the agent in check?", { "font-size": 48, "font-weight": 700 }); + txt(svg, 80, 184, "The default autonomy level is supervised", { "font-size": 32, "font-weight": 500, fill: MUTE }); + let ok = false; + const ap = chip(svg, 1150, 1040, 370, "Approve", "Approve the medium risk action", () => { ok = true; api.seek(2.6); }); + const LANES = [["low risk", 380, MINT], ["medium risk", 600, YEL], ["high risk", 820, LIL]]; + LANES.forEach(([, y]) => el("path", { d: `M80 ${y + 55} H1520`, stroke: SOFT, "stroke-width": 14, "stroke-linecap": "round" }, svg)); + /* the gate column */ + el("path", { d: "M900 330 V950", stroke: INK, "stroke-width": SW + 4, "stroke-linecap": "round" }, svg); + const sign = el("g", {}, svg); + el("rect", { x: 930, y: 560, width: 300, height: 90, rx: 20, fill: YEL, stroke: INK, "stroke-width": SW }, sign); + txt(sign, 1080, 600, "needs your", { "text-anchor": "middle", "font-size": 28, "font-weight": 700 }); + txt(sign, 1080, 636, "approval", { "text-anchor": "middle", "font-size": 28, "font-weight": 700 }); + const bar = el("g", {}, svg); + el("rect", { x: 880, y: 790, width: 40, height: 170, rx: 12, fill: LIL, stroke: INK, "stroke-width": SW }, bar); + el("rect", { x: 930, y: 830, width: 300, height: 90, rx: 20, fill: "#fff", stroke: INK, "stroke-width": SW }, bar); + txt(bar, 1080, 888, "blocked", { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + const runs = txt(svg, 1420, 448, "runs", { "text-anchor": "middle", "font-size": 34, "font-weight": 700 }); + const cards = LANES.map(([label, y, col]) => { + const g = el("g", {}, svg); + el("rect", { x: 0, y: 0, width: 280, height: 110, rx: 24, fill: col, stroke: INK, "stroke-width": SW }, g); + txt(g, 140, 68, label, { "text-anchor": "middle", "font-size": 32, "font-weight": 700 }); + return { g, y }; + }); + txt(svg, 80, 1100, "From ZeroClaw's README, 17 Oct 2026", { "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)); + const k0 = io(seg(t, 0.6, 3.2)), x0 = lerp(80, 1000, k0); + tf(cards[0].g, x0, cards[0].y); cards[0].g.setAttribute("opacity", out); + runs.setAttribute("opacity", k0 >= 1 ? out : 0); + const k1 = io(seg(t, 0.9, 2.4)), k1b = ok ? io(seg(t, 3, 4.6)) : 0; + tf(cards[1].g, lerp(80, 580, k1) + k1b * 620, cards[1].y); cards[1].g.setAttribute("opacity", out); + sign.setAttribute("opacity", k1b > 0.3 ? 0.25 : 1); + const k2 = io(seg(t, 1.2, 2.7)), bump = Math.sin(Math.PI * seg(t, 2.7, 3.1)) * 14; + tf(cards[2].g, lerp(80, 580, k2) - bump, cards[2].y); cards[2].g.setAttribute("opacity", out); + ap.setAttribute("fill", ok ? MINT : "#fff"); ap.parentNode.setAttribute("aria-pressed", ok); + }; + }, + }; + + /* ---- 3. a terminal runs the quick start; the service light turns on; a button restarts it ---- */ + SC.service = { + w: 1600, h: 1200, dur: 10, still: 8, + build(svg, api) { + txt(svg, 80, 124, "How do you install ZeroClaw?", { "font-size": 52, "font-weight": 700 }); + const again = chip(svg, 1150, 90, 370, "Restart", "Run the quick start again", () => api.seek(0.3)); + el("rect", { x: 92, y: 322, width: 800, height: 500, rx: 30, fill: SOFT }, svg); + el("rect", { x: 80, y: 310, width: 800, height: 500, rx: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + el("path", { d: "M80 380 H880", stroke: INK, "stroke-width": SW }, svg); + [124, 164, 204].forEach((x, i) => el("circle", { cx: x, cy: 345, r: 12, fill: [YEL, SKY, MINT][i], stroke: INK, "stroke-width": 4 }, svg)); + const L = ["$ zeroclaw quickstart", "$ zeroclaw service install", "$ zeroclaw service start"].map((s, i) => ({ s, n: txt(svg, 120, 470 + i * 90, "", { "font-family": MONO, "font-size": 34, "font-weight": 700 }) })); + const caret = el("rect", { width: 20, height: 40, fill: BLUE }, svg); + el("rect", { x: 1022, y: 402, width: 480, height: 340, rx: 30, fill: SOFT }, svg); + const box = el("rect", { x: 1010, y: 390, width: 480, height: 340, rx: 30, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + txt(svg, 1250, 470, "zeroclaw service", { "text-anchor": "middle", "font-size": 34, "font-weight": 700, "font-family": MONO }); + const led = el("circle", { cx: 1250, cy: 580, r: 48, fill: "#fff", stroke: INK, "stroke-width": SW }, svg); + const st = txt(svg, 1250, 690, "", { "text-anchor": "middle", "font-size": 38, "font-weight": 700 }); + txt(svg, 800, 1000, "Quickstart picks a provider and writes a working config", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + txt(svg, 800, 1050, "From ZeroClaw's README, 17 Oct 2026", { "text-anchor": "middle", "font-size": 30, "font-weight": 500, fill: MUTE }); + return (t) => { + const out = 1 - io(seg(t, 9.3, 9.9)); + let cx = 120, cy = 470; + L.forEach((l, i) => { + const a = 0.4 + i * 1.6, n = Math.round(seg(t, a, a + 1.1) * l.s.length); + l.n.textContent = out > 0.5 ? l.s.slice(0, n) : ""; + if (t > a) { cx = 120 + n * 20.5; cy = 470 + i * 90; } + }); + tf(caret, cx + 4, cy - 32); caret.setAttribute("opacity", Math.floor(t * 2.5) % 2 && out > 0.5 ? 1 : 0.15); + const on = t > 5.4 && out > 0.5, k = ob(seg(t, 5.4, 6)); + led.setAttribute("fill", on ? MINT : "#fff"); led.setAttribute("r", on ? 48 + 8 * Math.sin(Math.PI * seg(t, 5.4, 6)) : 48); + box.setAttribute("fill", on && k > 0.9 ? "#fff" : "#fff"); + st.textContent = on ? "always on" : "stopped"; st.setAttribute("fill", on ? INK : MUTE); + again.setAttribute("fill", t < 0.8 ? YEL : "#fff"); + }; + }, + }; + + /* ---- title card: the hexagon with three spokes ---- */ + SC.hero = { + w: 1600, h: 900, dur: 6, still: 4, + build(svg) { + txt(svg, 100, 160, "What is ZeroClaw?", { "font-size": 68, "font-weight": 700 }); + txt(svg, 100, 224, "One Rust binary for models, channels and tools", { "font-size": 32, "font-weight": 500, fill: MUTE }); + const H = [560, 560]; + const S = [["models", 1000, 340, BLUE], ["channels", 1100, 560, SKY], ["tools", 1000, 780, MINT]]; + const cs = S.map(([, x, y]) => cable(svg, `M${H[0]} ${H[1]} Q${(H[0] + x) / 2 + 60} ${y} ${x} ${y}`)); + const ts = S.map(([label, x, y, col]) => { + const g = el("g", {}, svg); + el("rect", { x, y: y - 50, width: 320, height: 100, rx: 50, fill: col, stroke: INK, "stroke-width": SW }, g); + txt(g, x + 160, y + 14, label, { "text-anchor": "middle", "font-size": 40, "font-weight": 700 }); + return g; + }); + el("path", { d: hexd(H[0] + 12, H[1] + 12, 150), fill: SOFT }, svg); + const hx = el("path", { d: hexd(H[0], H[1], 150), fill: YEL, stroke: INK, "stroke-width": SW, "stroke-linejoin": "round" }, svg); + txt(svg, H[0], H[1] + 16, "ZeroClaw", { "text-anchor": "middle", "font-size": 46, "font-weight": 700 }); + return (t) => { + const out = 1 - io(seg(t, 5.4, 5.9)); + cs.forEach((c, i) => c.setAttribute("stroke-dashoffset", 1 - oc(seg(t, 0.6 + i * 0.4, 1.6 + i * 0.4)) * out)); + ts.forEach((g, i) => g.setAttribute("opacity", seg(t, 1.4 + i * 0.4, 1.7 + i * 0.4) * out > 0.02 ? 1 : 0)); + hx.setAttribute("stroke-width", SW); + }; + }, + }; + + /* ---- player ---- */ + const q = new URLSearchParams(location.search), frozen = q.has("mgstill"), fixed = parseFloat(q.get("mgstill")); + const calm = matchMedia("(prefers-reduced-motion: reduce)").matches; + const mount = (fig) => { + const sc = SC[fig.dataset.scene], img = fig.querySelector("img"); if (!sc || !img) return; + const W = sc.w || 800, H = sc.h || 600; + const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "mg-svg", role: "group", "aria-label": img.alt || "" }); + svg.style.aspectRatio = `${W} / ${H}`; + el("rect", { width: W, height: H, fill: "#fff" }, svg); + img.replaceWith(svg); + let off = 0, raf = 0, seen = false, held = calm; + const base = performance.now(), clock = () => (performance.now() - base) / 1000; + let draw = () => {}; + const frame = () => { const now = clock(); draw(held ? sc.still : (((now + off) % sc.dur) + sc.dur) % sc.dur, now); }; + const tick = () => { cancelAnimationFrame(raf); frame(); if (seen && !held) raf = requestAnimationFrame(tick); }; + const api = { now: clock, seek: (s) => { off = s - clock(); held = false; tick(); }, poke: () => { held = false; tick(); } }; + draw = sc.build(svg, api); + if (frozen) { draw(isNaN(fixed) ? sc.still : fixed, 0); return; } + new IntersectionObserver(([e]) => { + seen = e.isIntersecting; + if (seen) { tick(); if (!fig.dataset.sent && window.posthog) { fig.dataset.sent = 1; window.posthog.capture("blog_scene_view", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } } + else cancelAnimationFrame(raf); + }, { threshold: 0.25 }).observe(fig); + svg.addEventListener("click", () => { if (!fig.dataset.hit && window.posthog) { fig.dataset.hit = 1; window.posthog.capture("blog_scene_interact", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } }); + frame(); + }; + const go = () => document.querySelectorAll("figure.mg[data-scene]").forEach(mount); + const fonts = document.fonts && document.fonts.load ? Promise.all([document.fonts.load('700 32px "Space Grotesk"'), document.fonts.load('500 20px "Space Grotesk"'), document.fonts.load('700 20px "JetBrains Mono"')]).catch(() => {}) : Promise.resolve(); + const start = () => fonts.then(go, go); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})(); diff --git a/blog/src/assets/media/what-is-zeroclaw/service.png b/blog/src/assets/media/what-is-zeroclaw/service.png new file mode 100644 index 000000000..b491fb6c8 Binary files /dev/null and b/blog/src/assets/media/what-is-zeroclaw/service.png differ diff --git a/blog/src/posts/claude-code-router.md b/blog/src/posts/claude-code-router.md index d672eb05a..e0d632b87 100644 --- a/blog/src/posts/claude-code-router.md +++ b/blog/src/posts/claude-code-router.md @@ -73,6 +73,8 @@ Custom compatible providers, in the README's words, plus a list of named presets
Animation. A wind chime hangs from one bar labelled CCR. Its tubes are labelled OpenRouter, DeepSeek, SiliconFlow, Moonshot, Mistral and Z.AI, and a breeze touches each tube in turn so it swings.
Some of the hosted providers CCR's README named on 14 Oct 2026.
+DeepSeek also publishes its own Claude Code setup with no gateway in between; see [DeepSeek V4.1 Flash 1017](/blog/deepseek-v4-1-flash-1017/). + ## What are the trade offs? You leave Anthropic's support behind, and some Claude Code features may not make the trip. Anthropic's [LLM gateway docs](https://code.claude.com/docs/en/llm-gateway) say Anthropic "doesn't endorse, maintain, or audit third-party gateway products, and doesn't support routing Claude Code to non-Claude models through any gateway." diff --git a/blog/src/posts/cline-vs-munder-difflin.md b/blog/src/posts/cline-vs-munder-difflin.md index 1af7b8dba..0e9256b63 100644 --- a/blog/src/posts/cline-vs-munder-difflin.md +++ b/blog/src/posts/cline-vs-munder-difflin.md @@ -1,126 +1,95 @@ --- -title: "Cline vs Munder Difflin: In-Editor Agent or Local Hive?" -description: "A fair comparison of Cline and Munder Difflin — an in-editor BYOK coding agent vs a local multi-agent orchestration hive — and when to pick which." +title: "Cline vs Munder Difflin: One Agent Everywhere, or a Team of Agents?" +description: "Cline vs Munder Difflin on 17 Oct 2026: what each one is, how they compare on models, teams and price, and when to pick which." date: 2026-06-05 +updated: 2026-10-17 category: comparisons categoryLabel: Comparisons type: Non-technical primaryKeyword: "cline vs munder difflin" -secondaryKeywords: ["cline alternative", "cline ai coding agent", "local ai coding agents"] +secondaryKeywords: ["cline alternative", "cline ai coding agent", "cline multi agent teams", "cline desktop app", "local ai coding agents"] tags: ["Comparisons", "Cline", "Local-First", "Multi-Agent"] author: name: Chaitanya Giri initials: CG faq: - q: "What's the difference between Cline and Munder Difflin?" - a: "Cline is an in-editor coding agent (a free VS Code extension you run with your own API key or a local model). Munder Difflin is a local multi-agent orchestration harness that coordinates several Claude Code agents as a team. One is an assistant inside your editor; the other is the layer that runs a team of agents." - - q: "Is Cline or Munder Difflin better for solo coding in an editor?" - a: "Cline — it's purpose-built for agentic coding inside VS Code with bring-your-own-key, including free local models via Ollama. Munder Difflin isn't an editor; it shines when you want to coordinate multiple agents, not drive one in your IDE." + a: "Cline is an open source coding agent that you use in VS Code, a terminal or its desktop app (and JetBrains on its Enterprise plan), on the model you choose. Munder Difflin is a free and open source desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer, with an orchestrator that splits a job between them." + - q: "Is Cline free?" + a: "Cline's pricing page says Cline is free for individual developers and that you pay only for AI inference on a usage basis, with your own API keys or the Cline provider. JetBrains support, SSO and team management sit in its Enterprise plan, priced through sales. Checked 17 Oct 2026." + - q: "Can Cline run a team of agents?" + a: "Yes. Cline's README describes multi agent teams in which a coordinator agent breaks the work into subtasks and delegates to specialist agents, started with the --team-name flag. Those teams run inside Cline. Munder Difflin's team mixes different coding agents, such as Claude Code, Codex and Gemini CLI." - q: "Can you use Cline and Munder Difflin together?" - a: "Conceptually yes — they solve different layers. Cline is your in-editor agent; Munder Difflin orchestrates a hive of Claude Code agents with shared memory and messaging. They're complementary, not mutually exclusive." - - q: "Are both local-first and open?" - a: "Both lean local and BYO-model. Cline is a free, open VS Code extension that runs on your keys or local models; Munder Difflin is an MIT-licensed local harness where orchestration, files, and memory stay on your machine." + a: "They are separate apps, so you can keep Cline in your editor and run Munder Difflin beside it. Munder Difflin's agents run on engines such as Claude Code, Codex and Gemini CLI, and you install and sign in to each one yourself." --- -
TL;DR

Cline is an excellent -in-editor coding agent — a free VS Code extension you run with your own API key (or a local -model via Ollama). Munder Difflin is a different layer entirely: a local -multi-agent orchestration harness that coordinates several Claude Code agents as a team, with -shared memory, messaging, and a visual office floor. They're close cousins on local-first + BYO -model, but Cline drives one agent in your editor while Munder Difflin runs a -coordinated hive. Pick by the layer you need.

+Pick Cline if you want one open source coding agent that follows you from VS Code to the terminal to a desktop app; pick Munder Difflin if you want several different coding agents working one job as a team. Checked 17 Oct 2026. -If you're shopping local-first AI coding tools, Cline and Munder Difflin both come up — and both are -refreshingly un-cloud. But they're not really competitors so much as *different layers of the stack*. -This is a fair, specific comparison: what each is, where each wins, and how to choose. (For the broader -field, see our [roundup of multi-agent Claude Code tools](/blog/best-claude-code-multi-agent-tools/).) +[Munder Difflin](https://harnessmd.com/download) is what we make. It is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. Cline is a different kind of tool, and for plenty of people it is the right one. This page sets out where each fits. -## What each one actually is +
Animation. One block labelled Cline sits in the middle on white, with four cables running to four tiles labelled VS Code, Terminal, Desktop and SDK. The tiles light up one at a time.
Four of the places Cline runs, from its README on 17 Oct 2026.
-**Cline** is an open-source **VS Code extension** for agentic coding. It's free to install and runs on -**your own API key** — Claude, OpenAI, Gemini, Azure — or on a **local model through Ollama**, in which -case you pay nothing for inference at all. There's no subscription or credit system; you pay only what -your chosen model provider charges. It recently grew multi-agent muscles too: per 2026 roundups, -[Cline's CLI 2.0 added parallel terminal agents](https://www.morphllm.com/best-ai-coding-agents-2026). -It's widely described as a Cursor-equivalent experience *inside* VS Code with bring-your-own-key. +## What is Cline? -**Munder Difflin** is not an editor or an extension — it's a **local multi-agent orchestration harness** -that wraps the Claude Code agents you already run and coordinates them into a team. The orchestration, -the files, and the [memory](/blog/semantic-memory-for-ai-agents/) all run on your machine; a GOD -orchestrator routes work, agents message each other through files, and you watch the whole thing on a -visual office floor. It's MIT-licensed and [local-first by design](/blog/why-local-first-matters-for-ai-agents/). +Cline's README calls it "The open source coding agent in your IDE, terminal, & desktop." It is licensed Apache 2.0. One agent engine sits behind several front ends: -The one-line distinction: **Cline is an agent you code *with*; Munder Difflin is the layer that runs a -*team* of agents.** +* **VS Code extension.** Edits show as diffs you can review or revert, with checkpoints to undo the agent's work. +* **CLI.** Installed with `npm i -g cline`, with an interactive chat or a headless mode for scripts and CI. +* **Desktop app.** A native app for macOS and Windows that runs agent sessions in any folder, schedules routines and manages models, plugins and MCP servers. +* **JetBrains plugin.** The README says Cline is currently not open sourcing its JetBrains plugins, and the pricing page lists the JetBrains extension under Enterprise. +* **SDK.** `@cline/sdk`, for building your own agents on the same engine. -## Where they overlap (the kinship) +Cline works in a Plan mode and an Act mode, and the README says every file edit and terminal command needs your approval unless you turn on auto approve. It is not tied to one provider: the README lists Anthropic, OpenAI, Google, OpenRouter, Bedrock, Vertex and any OpenAI compatible API, plus Ollama and LM Studio for local models. -It's worth being clear about what they share, because it's real: +It has grown well past an editor plugin. The README now describes multi agent teams, where "A coordinator agent breaks the work into subtasks and delegates to specialist agents", scheduled agents on cron and chat connectors for Slack, Telegram, Discord and others. Releases are frequent: [CLI v3.0.73](https://github.com/cline/cline/releases/tag/cli-v3.0.73) and [desktop v0.0.48](https://github.com/cline/cline/releases/tag/desktop-v0.0.48) on 17 Oct, and [extension v4.1.25](https://github.com/cline/cline/releases/tag/v4.1.25) on 16 Oct, which began rolling out Cline Cloud sessions in a hosted sandbox. -- **Local-first.** Neither hands your orchestration or workflow to a cloud control plane. (For the full - contrast with the cloud SDK wave, see [local-first vs cloud agent SDKs](/blog/local-first-vs-cloud-agent-sdks/).) -- **Bring-your-own-model.** Cline runs on your keys or local Ollama models; Munder Difflin's agents are - your own Claude Code sessions calling the API you already use. -- **Open and free.** Cline is a free open extension; Munder Difflin is MIT-licensed and free. +## What is Munder Difflin? -So if your priority is "keep it on my machine, on my models," both qualify. The decision is about -*scope*. +Munder Difflin is an office for coding agents. You talk to one orchestrator; it splits a job into pieces, hires other agents and hands the pieces out, and comes back to you when something needs your decision. Each agent runs on an engine such as Claude Code, Codex or Gemini CLI. The app is the office around them: tasks, memory, schedules and messages between agents. -{% img "note-1" %} +You still install each engine and sign in with your own plan or key; Munder Difflin does not run a model by itself. Our [install guide](/blog/how-to-install-and-use-munder-difflin/) walks through setup, and [what is a multi agent harness](/blog/what-is-a-multi-agent-harness/) explains the idea. -## Where Cline wins +
Animation. On white, a desk marked orchestrator sends three task cards along paths to three desks labelled Claude Code, Codex and Gemini CLI. Each desk light turns green as its card arrives.
One orchestrator hands work to different coding agents.
-- **In-editor ergonomics.** If you live in VS Code and want an agent right there — reading the file you - have open, making edits inline — Cline is purpose-built for exactly that. -- **Zero-cost local inference.** With Ollama, your inference bill can be literally $0, which is hard to - beat for tinkering or budget-bound work. -- **Lightweight start.** Install an extension, paste a key, go. No hive to think about. +## How do Cline and Munder Difflin compare? -If you want *one* capable coding agent inside your editor, Cline is the more direct fit. Munder Difflin -would be overkill. +Cline is one agent you can take to many places; Munder Difflin is a place where several different agents work together. -## Where Munder Difflin wins +| 17 Oct 2026 | Cline | Munder Difflin | +| --- | --- | --- | +| What it is | One coding agent with many front ends | A desktop app that runs a team of coding agents | +| Where you use it | VS Code, JetBrains (Enterprise), terminal, desktop app, SDK | Its desktop app, with each agent in its own terminal | +| Models | Any provider you add, including local models through Ollama or LM Studio | Whatever each agent's engine runs | +| Teams | A coordinator and specialist agents, inside Cline | Different agents, such as Claude Code, Codex and Gemini CLI, led by an orchestrator | +| Licence | Apache 2.0 (JetBrains plugin not open source) | Free and open source | +| Price | Free for individual developers; you pay for inference. Enterprise through sales | Free and open source; you bring your agents' plans or keys | -- **Coordinating many agents, not driving one.** Munder Difflin is built for a *team*: roles, a - [GOD orchestrator](/blog/how-the-god-orchestrator-works/) that routes and escalates, and - [file-based messaging](/blog/atomic-file-mailboxes-for-agents/) between agents. Cline's parallel - terminal agents are a step toward this, but Munder Difflin makes coordination the whole product. -- **Shared long-term memory.** A hive that [remembers across sessions](/blog/how-agents-remember-semantic-memory/) - so one agent uses what another learned — beyond a single agent's context. -- **Visibility.** A visual office floor to *watch* the team work, plus an audit log — useful when - several agents run at once. +Sources: Cline's [README](https://github.com/cline/cline) and [pricing page](https://cline.bot/pricing), read 17 Oct 2026. -If your problem is "I want a coordinated team of agents working a goal," that's Munder Difflin's lane, -not an editor extension's. +## When should you pick Cline? -{% img "note-2" %} +Pick Cline when one capable agent is enough and you want it close to your code. -## How to choose +* **You want one agent in the editor you already use.** The VS Code extension shows each edit as a diff, and Plan mode lets you agree on a plan before anything changes. +* **You want to choose your own model.** Cline takes keys from many providers, and Ollama or LM Studio if you want local models. +* **You want one tool in many places.** The same engine runs in the editor, the terminal, CI, the desktop app and your own code through the SDK. -| If you want… | Pick | -|---|---| -| An agent inside VS Code, BYOK, maybe free local models | **Cline** | -| The lightest path to one capable in-editor coding agent | **Cline** | -| To coordinate *multiple* Claude Code agents as a team | **Munder Difflin** | -| Shared memory + orchestration + a watchable floor | **Munder Difflin** | -| Local-first and BYO-model | **Either** — both qualify | +If those cover you, Cline is the more direct fit. For other tools in this space, see our [OpenCode alternatives](/blog/opencode-alternatives/) and [Claude Code alternatives](/blog/claude-code-alternatives/). -And honestly: they can coexist. Use Cline as your in-editor agent and Munder Difflin to orchestrate a -hive for the bigger, parallel work. The layers don't conflict. For a structured way to decide across -the whole field, see [how to choose a multi-agent tool](/blog/how-to-choose-a-multi-agent-tool/) and -the [Claude Squad vs Munder Difflin](/blog/claude-squad-vs-munder-difflin/) head-to-head. +## When should you pick Munder Difflin? -## The bottom line +Pick Munder Difflin when the job is big enough to split, or when you want different agents on it. -**Cline is a great in-editor coding agent; Munder Difflin is the orchestration layer for a local hive -of them.** They share a local-first, BYO-model philosophy and diverge on scope — one agent in your -editor vs a coordinated team on your machine. Choose by the layer you actually need, and don't rule out -using both. +* **You already pay for more than one agent.** If you have Claude Code, Codex and Gemini CLI, Munder Difflin puts them on one team instead of three terminal tabs. +* **You want a job split for you.** The orchestrator breaks the work up, hires agents and asks you only when it needs a decision. +* **You want the team to keep its context.** Everything the team does lives in one workspace folder: its work, memory, tasks and history. ---- +For the wider field of team tools, see [the best Claude Code multi agent tools](/blog/best-claude-code-multi-agent-tools/) and [how to choose a multi agent tool](/blog/how-to-choose-a-multi-agent-tool/). + +
Animation. A path on white splits at a signpost. The left arrow reads one agent, many places, and leads to a tile marked Cline. The right arrow reads many agents, one job, and leads to a tile marked Munder Difflin.
Choose by the shape of the work.
+ +## Can you use Cline and Munder Difflin together? -Munder Difflin is a [local, open-source multi-agent harness](/#what) for Claude Code — a hive you run -on your own machine. [Download Munder Difflin](/#install) to coordinate a team of agents; free and MIT -licensed. +Yes, side by side. They are separate apps, so nothing stops you keeping Cline in your editor for hands on work while Munder Difflin runs a team on a bigger job. Munder Difflin's agents run on engines such as Claude Code, Codex and Gemini CLI. For a head to head with another team tool, see [Claude Squad vs Munder Difflin](/blog/claude-squad-vs-munder-difflin/). -

Sources: Morph — Best AI Coding Agents 2026 (Cline capabilities/pricing); Developers Digest — AI Coding Tools Pricing 2026. Cline features/pricing change frequently — verify current details on Cline's site.

+ diff --git a/blog/src/posts/deepseek-v4-1-flash-1017.md b/blog/src/posts/deepseek-v4-1-flash-1017.md new file mode 100644 index 000000000..139866f31 --- /dev/null +++ b/blog/src/posts/deepseek-v4-1-flash-1017.md @@ -0,0 +1,102 @@ +--- +title: "DeepSeek V4.1 Flash 1017: What Changed, the Price and Claude Code Setup" +description: "DeepSeek V4.1 Flash 1017, released 17 Oct 2026 under MIT: what changed, DeepSeek's benchmarks, the API price and Claude Code setup." +date: 2026-10-17 +category: concepts +categoryLabel: Concepts +type: Non-technical +primaryKeyword: "deepseek v4.1 flash 1017" +secondaryKeywords: ["deepseek v4.1 flash", "deepseek flash", "deepseek v4.1 flash price", "deepseek claude code", "deepseek v4.1 flash benchmarks", "deepseek-flash api"] +tags: ["Concepts", "AI Agents", "Open Source"] +faq: + - q: "What is DeepSeek V4.1 Flash 1017?" + a: "DeepSeek V4.1 Flash 1017 is DeepSeek's update to its V4.1 Flash model, released on 17 October 2026. The model card calls it \"a minor update to DeepSeek-V4.1-Flash\" with gains in reasoning, coding, agentic tasks and visual benchmarks, and says it also cuts task execution time and output length. The weights are on Hugging Face under the MIT License." + - q: "How much does DeepSeek V4.1 Flash 1017 cost?" + a: "On DeepSeek's pricing page on 17 Oct 2026, the deepseek-flash model cost $0.15 per million input tokens on a cache miss and $0.60 per million output tokens at off peak rates, and twice that at peak hours. On OpenRouter that day, DeepSeek's own endpoint, one of several providers, listed $0.15 input and $0.60 output." + - q: "Can I use DeepSeek V4.1 Flash 1017 in Claude Code?" + a: "DeepSeek publishes a Claude Code guide. It sets ANTHROPIC_BASE_URL to https://api.deepseek.com/anthropic, your DeepSeek API key as ANTHROPIC_AUTH_TOKEN, and deepseek-flash as the model names, then you run claude in your project." + - q: "Is DeepSeek V4.1 Flash 1017 open source?" + a: "The model card says the repository and the model weights are licensed under the MIT License. Hugging Face lists the model size as 763B parameters; our view is that running it yourself takes data centre hardware." + - q: "What happened to deepseek-v4-flash in the API?" + a: "DeepSeek's API docs say the legacy names deepseek-v4-flash and deepseek-v4-flash-vision-exp are still accepted, but those models have been retired. Their requests are served by DeepSeek V4.1 Flash 1017 and billed at the Flash price." +--- + +DeepSeek V4.1 Flash 1017 is an update to DeepSeek's V4.1 Flash model, released on 17 October 2026 with open weights under the MIT License. DeepSeek's release note promises "Stronger benchmark performance, faster task completion, and shorter outputs than the previous version". It now serves the deepseek-flash name in DeepSeek's API. Checked 17 Oct 2026. + +Open weight coding models need an agent to drive them. [Munder Difflin](https://harnessmd.com/download) is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. Our guide to [running it on open models](/blog/run-munder-difflin-on-open-models/) covers local runtimes and providers, and our [install guide](/blog/how-to-install-and-use-munder-difflin/) covers setup. + +## What is DeepSeek V4.1 Flash 1017? + +The [model card](https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017) calls it "a minor update to DeepSeek-V4.1-Flash, delivering substantial improvements across reasoning, coding, agentic tasks, and visual benchmarks." It adds that the model "reduces task execution time and output length". Hugging Face created the repo on 17 Oct 2026 and lists the model size as 763B parameters. + +| Detail | DeepSeek V4.1 Flash 1017 (17 Oct 2026) | +| --- | --- | +| Licence | MIT, for the repository and the weights | +| Size | 763B parameters, per Hugging Face | +| Weights | FP8, with the experts in FP4, per the model's config file | +| Context | 1M tokens | +| Max output in the API | 384K tokens | +| Input | Text and images (the API lists Vision for deepseek-flash) | +| Recommended sampling | temperature 1.0, top_p 1.0, max_tokens of at least 256K | + +The release ships no Jinja chat template. Instead the repo has an encoding folder with a Python reference implementation, and the model card also points to DeepSeek's [deepseek-recipe](https://github.com/deepseek-ai/deepseek-recipe), Rust libraries with Python bindings for the same prompt format. The inference folder calls itself "A readable reference implementation rather than a production serving engine." + +
Animation. Two bars grow side by side for Terminal-Bench 4.0: V4.1 Flash reaches 31.2 and V4.1 Flash 1017 reaches 42.3. Buttons switch the bars to DeepSWE v1.1 and Automation-Bench.
Three rows from DeepSeek's table on the model card, 17 Oct 2026.
+ +## How does it score? + +DeepSeek's table compares the update with the previous Flash and with models it labels K3, GLM-5.3, GPT-5.6 Sol and Opus 5. Selected rows: + +| Benchmark | V4.1 Flash 1017 | V4.1 Flash | Opus 5 | +| --- | --- | --- | --- | +| Terminal-Bench 4.0 | 42.3 | 31.2 | 51.8 | +| DeepSWE v1.1 | 77.0 | 74.2 | 74.0 | +| ProgramBench | 29.1 | 20.3 | 37.0 | +| Automation-Bench v1.0.6 | 60.5 | 54.8 | 50.3 | +| Agents' Last Exam | 35.4 | 31.8 | 28.6 | + +DeepSeek notes: "We use the Claude Code harness for Terminal-Bench 4.0 and the Mini-SWE-V2 harness for DeepSWE v1.1." Of the eleven rows in its table, the update has the top score on four (DeepSWE v1.1, Automation-Bench, Agents' Last Exam and Apex Bench), Opus 5 on six, including Terminal-Bench 4.0 and ProgramBench, and GPT-5.6 Sol on one. These are DeepSeek's own numbers. + +DeepSeek's [release note](https://api-docs.deepseek.com/news/news261017) says the model "uses reinforcement learning with self-feedback to improve end-to-end project development" and is "Optimized for DeepSeek Harness, Tencent CodeBuddy, WorkBuddy, and more." On Hacker News, two posts about the release had more than 200 points each on 17 Oct. + +## How much does DeepSeek V4.1 Flash 1017 cost? + +The weights are free to download. DeepSeek's [first API call page](https://api-docs.deepseek.com/) says deepseek-flash "is now served by DeepSeek-V4.1-Flash-1017". DeepSeek's [pricing page](https://api-docs.deepseek.com/quick_start/pricing) lists these rates per million tokens on 17 Oct 2026: + +| deepseek-flash | Off peak | Peak | +| --- | --- | --- | +| Input, cache hit | $0.003 | $0.006 | +| Input, cache miss | $0.15 | $0.30 | +| Output | $0.60 | $1.20 | + +DeepSeek says "Off-peak rates are half of the peak rates." Peak hours are 01:00 to 04:00 and 06:00 to 10:00 UTC, Monday through Friday, excluding Chinese public holidays; weekends are off peak. On [OpenRouter](https://openrouter.ai/deepseek/deepseek-v4.1-flash-1017) on 17 Oct, several providers served it, including DeepSeek, whose own endpoint listed $0.15 input and $0.60 output per million tokens with a 1,048,576 token context. + +
Animation. A small receipt printer feeds out a slip for deepseek-flash at off peak rates: $0.15 per million input tokens on a cache miss, then $0.60 per million output tokens. A button switches the slip to peak rates.
DeepSeek's API price for deepseek-flash, served by V4.1 Flash 1017, on 17 Oct 2026.
+ +The old names still work. The API docs say deepseek-v4-flash and deepseek-v4-flash-vision-exp "are still accepted, but the corresponding models have been retired", with requests served by the new model "and billed at the Flash price." + +The release note also opens DeepSeek's Search and Fetch APIs to everyone at "$0.75 per 1,000 calls" per endpoint. Search and Fetch calls through server side tools in DeepSeek Harness, Claude Code and Codex are free for DeepSeek API users until billing starts on 31 Oct 2026 at 16:00 UTC. + +## How do you use it in Claude Code? + +DeepSeek's API speaks the Anthropic format at https://api.deepseek.com/anthropic, and its [Claude Code guide](https://api-docs.deepseek.com/quick_start/agent_integrations/claude_code) sets these variables before you run claude in your project: + +```bash +export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic +export ANTHROPIC_AUTH_TOKEN= +export ANTHROPIC_MODEL=deepseek-flash[1m] +export ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-flash[1m] +export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-flash[1m] +export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-flash +export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-flash +export CLAUDE_CODE_EFFORT_LEVEL=max +export CLAUDE_CODE_AUTO_COMPACT_WINDOW=786432 +``` + +The same docs also have guides for tools such as Codex, OpenCode, OpenClaw and Hermes. For routing several providers through one endpoint, see [Claude Code Router](/blog/claude-code-router/). + +
Animation. A terminal window types the base URL api.deepseek.com/anthropic, the model deepseek-flash[1m] and claude. A plug labelled Anthropic format slides into a socket labelled DeepSeek API and a light turns green.
The setup from DeepSeek's Claude Code guide, 17 Oct 2026.
+ +Our view: a 763B model is out of reach for a desktop, so for most people this update arrives through the API, where it has already replaced the old Flash. For open source agents that work with local models, see [open source AI agents](/blog/open-source-ai-agents/). + + diff --git a/blog/src/posts/openclaw-alternatives.md b/blog/src/posts/openclaw-alternatives.md index c0a3055be..a5cb59d92 100644 --- a/blog/src/posts/openclaw-alternatives.md +++ b/blog/src/posts/openclaw-alternatives.md @@ -48,9 +48,9 @@ Two of the seven kept the claw in the name. Nobody said naming was easy. **2. Munder Difflin.** This is what we make, and it is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. Each agent is a real CLI in a real terminal, with a role, shared memory and messages to the others. It runs on macOS, Windows and Linux. The limits: it is for coding work, it does not run personal errands or answer your WhatsApp, and agents work while your computer is on. The [install guide](/blog/how-to-install-and-use-munder-difflin/) takes you from download to a first agent. Best for: people who used OpenClaw mostly to get code written. -**3. NanoClaw.** [NanoClaw](https://github.com/nanocoai/nanoclaw) (MIT) is described on its repo as "a lightweight alternative to OpenClaw that runs in containers for security". It runs on Anthropic's Agents SDK by default, and its README lists Codex, OpenCode and Ollama as add-on providers. Each agent runs in its own Linux container and sees only what you mount. It needs Docker, on macOS, Linux or Windows through WSL2. Best for: isolation first. +**3. NanoClaw.** [NanoClaw](https://github.com/nanocoai/nanoclaw) (MIT) is described on its repo as "a lightweight alternative to OpenClaw that runs in containers for security". It runs on Anthropic's Agents SDK by default, and its README lists Codex, OpenCode and Ollama as add-on providers. Each agent runs in its own Linux container and sees only what you mount. It needs Docker, on macOS, Linux or Windows through WSL2. Our [NanoClaw explainer](/blog/what-is-nanoclaw/) covers setup, models and chat apps. Best for: isolation first. -**4. ZeroClaw.** [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) is a single Rust binary, dual licensed MIT or Apache 2.0. Its README lists 30+ channels and a default autonomy level called `supervised`, where medium risk operations need your approval. It also drives hardware such as a Raspberry Pi. Best for: a small always on service. +**4. ZeroClaw.** [ZeroClaw](https://github.com/zeroclaw-labs/zeroclaw) is a single Rust binary, dual licensed MIT or Apache 2.0. Its README lists 30+ channels and a default autonomy level called `supervised`, where medium risk operations need your approval. It also drives hardware such as a Raspberry Pi. Our [ZeroClaw explainer](/blog/what-is-zeroclaw/) covers install and providers. Best for: a small always on service. **5. nanobot.** [nanobot](https://github.com/HKUDS/nanobot) (MIT) is a personal agent framework in Python 3.11 or newer. It runs in a web UI, a terminal or chat apps such as Telegram, Discord and Slack. Best for: Python people who want to read the core. diff --git a/blog/src/posts/what-is-nanoclaw.md b/blog/src/posts/what-is-nanoclaw.md new file mode 100644 index 000000000..3bddcb389 --- /dev/null +++ b/blog/src/posts/what-is-nanoclaw.md @@ -0,0 +1,96 @@ +--- +title: "What Is NanoClaw? The Small OpenClaw Alternative That Runs Agents in Containers" +description: "NanoClaw explained on 17 Oct 2026: what the MIT licensed assistant does, how it differs from OpenClaw, how to install it and which models it works with." +date: 2026-10-17 +category: concepts +categoryLabel: Concepts +type: Non-technical +primaryKeyword: "nanoclaw" +secondaryKeywords: ["what is nanoclaw", "nanoclaw vs openclaw", "nanoclaw install", "nanoclaw docker", "nanoclaw github", "openclaw alternative"] +tags: ["Concepts", "AI Agents", "Open Source"] +faq: + - q: "What is NanoClaw?" + a: "NanoClaw is a free, MIT licensed personal AI assistant. Its README describes it as an assistant \"that runs agents securely in their own containers\". You talk to it through chat apps such as WhatsApp, Telegram, Slack or Discord, and it runs on Anthropic's Claude Agent SDK by default." + - q: "Is NanoClaw free?" + a: "The code is free under the MIT License, and the README says NanoClaw has no user accounts. NanoClaw's website says your agent provider may charge for model usage, and that local Ollama models can run without a hosted model bill. A free account is needed only for opt in extras, such as fetching a prebuilt agent image instead of building it locally." + - q: "What is the difference between NanoClaw and OpenClaw?" + a: "Both are personal assistants you message from chat apps. NanoClaw's website compares about 29,300 lines of runtime TypeScript for NanoClaw with 434,453 for OpenClaw, and container isolation for NanoClaw with application level checks for OpenClaw. Those are NanoClaw's own figures." + - q: "Does NanoClaw work with models other than Claude?" + a: "Yes. The README says it uses Claude through the Claude Agent SDK natively, and lists /add-codex for OpenAI's Codex, /add-opencode for OpenRouter, Google, DeepSeek and others, and /add-ollama-provider for local models through Ollama." + - q: "What do you need to run NanoClaw?" + a: "macOS or Linux, or Windows through WSL2, with Node 22 or later, pnpm 10 or later and Docker. The setup script installs Node, pnpm and Docker if they are missing. Claude Code is needed for /customize, /debug, error recovery during setup and the /add channel skills." +--- + +NanoClaw is a free, MIT licensed personal AI assistant that runs its agents inside Docker containers and answers you in chat apps such as WhatsApp, Telegram, Slack and Discord. Pick it over OpenClaw if you want a much smaller codebase; NanoClaw's website says it takes "An afternoon" to understand. Checked 17 Oct 2026. + +If your real work is code rather than chat, [Munder Difflin](https://harnessmd.com/download) is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. Our [OpenClaw alternatives](/blog/openclaw-alternatives/) page compares both with five other tools, and our [install guide](/blog/how-to-install-and-use-munder-difflin/) covers setup. + +## What is NanoClaw? + +NanoClaw is a personal AI assistant that you run yourself and talk to through chat apps. The [GitHub repo](https://github.com/nanocoai/nanoclaw) calls it "A lightweight alternative to OpenClaw that runs in containers for security." The README sums it up as "An AI assistant that runs agents securely in their own containers." You give it jobs in a chat app, it runs them in a sandbox, and it can repeat them on a schedule. + +The project now ships dated releases. The notes for 2026.10.0 say it "is the first release with calendar version numbers", and the first one that `/update-nanoclaw` installs by default, so updates follow published releases instead of the newest code. + +| Detail | NanoClaw (17 Oct 2026) | +| --- | --- | +| Licence | MIT | +| Latest release | 2026.10.0, published 9 Oct 2026 | +| Default agent | Claude, through Anthropic's Claude Agent SDK | +| Other providers | Codex, OpenCode and Ollama, added with skills | +| Runs on | macOS or Linux, Windows through WSL2 | +| Needs | Node 22+, pnpm 10+, Docker | +| GitHub | More than 30,000 stars, created 31 Jan 2026 | + +The README lists what each agent group gets: "its own `CLAUDE.md`, its own memory, its own container, and only the mounts you allow." It also lists scheduled tasks, web search and fetch, and agent templates you can stamp out with one command. + +
Animation. On white, a clear box labelled container holds an agent tile. Three folders sit outside it; one folder labelled mounted slides in through a door in the box and the other two stay outside. A button toggles the mount.
Agents see only the folders you mount, per NanoClaw's README.
+ +## How is NanoClaw different from OpenClaw? + +NanoClaw is the smaller of the two by design. Its [website](https://nanoclaw.dev) sets out these figures, which are NanoClaw's own: + +| From nanoclaw.dev, 17 Oct 2026 | NanoClaw | OpenClaw | +| --- | --- | --- | +| Runtime TypeScript files | 200 | 3,680 | +| Runtime TypeScript lines | about 29,300 | 434,453 | +| Direct runtime dependencies | 12 | 70 | +| Manual config files | 0 | 53 | +| Security model | OS container isolation | Application level checks | + +The README's philosophy is "Customization = code changes." There are no config files: you fork the project and ask Claude Code to change it. Channels and extra providers are not in the main code either; skills such as `/add-telegram` copy in only the modules you ask for. + +Under the hood, the README describes one host process that routes each message to a per session agent container, with "Two SQLite files per session, each with exactly one writer". The host writes your message to one file, the agent writes its reply to the other, and the host delivers it back to your chat app. + +
Animation. Two stacks of paper sheets grow on white. The NanoClaw stack stops low at about 29,300 lines; the OpenClaw stack keeps rising to 434,453 lines. A button switches the count to config files, 0 against 53.
NanoClaw's own comparison with OpenClaw, from nanoclaw.dev on 17 Oct 2026.
+ +For a closer look at OpenClaw and one of its main rivals, see [Hermes Agent vs OpenClaw](/blog/hermes-agent-vs-openclaw/). + +## How do you install NanoClaw? + +The README's quick start is three lines: + +```bash +git clone https://github.com/nanocoai/nanoclaw.git nanoclaw-v2 +cd nanoclaw-v2 +bash nanoclaw.sh +``` + +The README says `nanoclaw.sh` "walks you from a fresh machine to a named agent you can message." It installs Node, pnpm and Docker if they are missing, installs a credential gateway and registers your Anthropic credential with it, builds the agent container and pairs your first channel. If a step fails, Claude Code is called to diagnose it and resume. To remove it, `bash nanoclaw.sh --uninstall` deletes only what belongs to that copy. + +
Animation. A terminal on white types the setup command. Beside it a checklist ticks off Node and pnpm, Docker, credential gateway, agent container and first channel, one at a time.
What the setup script does, from NanoClaw's README on 17 Oct 2026.
+ +## Which models and chat apps does it support? + +Claude comes first. The README says NanoClaw "natively uses Claude Code via Anthropic's official Claude Agent SDK". Other providers are skills: `/add-codex` for OpenAI's Codex with a ChatGPT subscription or API key, `/add-opencode` for OpenRouter, Google, DeepSeek and more, and `/add-ollama-provider` for local models through Ollama. Different agents in one install can use different backends. + +For chat, the README lists WhatsApp, Telegram, Discord, Slack, Microsoft Teams, iMessage, Matrix, Google Chat, Webex, Linear, GitHub, WeChat and email through Resend. Each is installed on demand with its own `/add` skill. + +## Is NanoClaw safe to use? + +Its safety case rests on isolation. The README says agents "can only access explicitly mounted directories", and that "agents never hold raw API keys": outbound requests go through a credential gateway that adds the key at request time. It also says "You should still review what you're running". + +Two details are worth knowing before you install. The README says NanoClaw "has no user accounts" and by default reports only anonymous setup diagnostics, which `NANOCLAW_NO_DIAGNOSTICS=1` turns off; a free account is needed only for opt in extras such as a prebuilt agent image. And the 2026.10.0 release notes tell OneCLI users to upgrade the gateway to exactly 1.42.0, "which closes a credential-injection bypass", before updating. + +Our view: NanoClaw suits someone who wants an assistant in their chat apps and is happy to run Docker and read code. If you want a team of coding agents instead, see [open source AI agents](/blog/open-source-ai-agents/). + + diff --git a/blog/src/posts/what-is-zeroclaw.md b/blog/src/posts/what-is-zeroclaw.md new file mode 100644 index 000000000..9de487a50 --- /dev/null +++ b/blog/src/posts/what-is-zeroclaw.md @@ -0,0 +1,83 @@ +--- +title: "What Is ZeroClaw? The Single Binary Agent Runtime in Rust, Explained" +description: "ZeroClaw explained on 17 Oct 2026: what the Rust agent runtime does, how it keeps the agent in check, how to install it and which models it works with." +date: 2026-10-17 +category: concepts +categoryLabel: Concepts +type: Non-technical +primaryKeyword: "zeroclaw" +secondaryKeywords: ["what is zeroclaw", "zeroclaw install", "zeroclaw github", "zeroclaw rust", "zeroclaw vs openclaw", "openclaw alternative"] +tags: ["Concepts", "AI Agents", "Open Source"] +faq: + - q: "What is ZeroClaw?" + a: "ZeroClaw is a free, open source agent runtime written in Rust. Its README calls it \"a single Rust binary you configure and run\". It connects a model to chat channels such as Discord, Telegram and email, and to tools such as a shell, a browser and custom MCP servers." + - q: "Is ZeroClaw free?" + a: "Yes. The README says ZeroClaw is dual licensed, MIT or Apache 2.0, at your choice. You still pay for the model it calls, through an API key or a subscription such as Codex or Claude Max, or you run a local model through Ollama." + - q: "Is ZeroClaw safe to run?" + a: "Its default autonomy level is supervised: the README says medium risk operations need your approval and high risk ones are blocked. It adds workspace boundaries, a command policy, OS sandboxes and signed tool receipts. A YOLO mode skips the approval, workspace, command and sandbox gates and is meant for dev boxes." + - q: "Which is the official ZeroClaw repository?" + a: "The README says github.com/zeroclaw-labs/zeroclaw is the only official repository, and that any other repository, organisation, domain or package claiming the name is unauthorised and not affiliated with the project." + - q: "What is the difference between ZeroClaw and NanoClaw?" + a: "ZeroClaw is one Rust binary you set up through a TOML config file, with pluggable providers. NanoClaw is a TypeScript project that runs each agent in a Docker container, uses the Claude Agent SDK by default and is customised by changing its code." +--- + +ZeroClaw is a free, open source agent runtime: one Rust binary that connects a model to your chat apps, tools and even hardware, with approval gates switched on by default. Pick it if you want a small, always on assistant that you set up in one config file. Checked 17 Oct 2026. + +If your real work is code rather than chat, [Munder Difflin](https://harnessmd.com/download) is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. Our [OpenClaw alternatives](/blog/openclaw-alternatives/) page compares both with five other tools, and our [install guide](/blog/how-to-install-and-use-munder-difflin/) covers setup. + +## What is ZeroClaw? + +The [README](https://github.com/zeroclaw-labs/zeroclaw) calls ZeroClaw an agent runtime, "a single Rust binary you configure and run." It talks to model providers, takes messages from channels and acts through tools. The repo describes it as "Fast, small, and fully autonomous AI personal assistant infrastructure, any OS, any platform". + +| Detail | ZeroClaw (17 Oct 2026) | +| --- | --- | +| Licence | MIT or Apache 2.0, your choice, per the README | +| Language | Rust | +| Latest release | v0.8.5, published 5 Sep 2026 | +| Providers | Anthropic, OpenAI, Ollama "and ~20 others" | +| Channels | 30+, including Discord, Telegram, Matrix, email and webhooks | +| Config | One TOML file at `~/.zeroclaw/config.toml` | +| GitHub | More than 32,000 stars, created 13 Feb 2026 | + +The README lists more than chat: a web dashboard for chat, memory, config and cron jobs, an engine for event triggered "Standard Operating Procedures", editor integration through the Agent Client Protocol, and hardware control on Raspberry Pi, STM32, Arduino and ESP32 boards. The README's architecture diagram keeps memory in SQLite with embeddings, and a gateway offers REST and WebSocket access for other clients. + +
Animation. On white, a yellow hexagon labelled ZeroClaw sits in the middle. Lines draw out to three groups of tiles: providers on the left, channels on the right and tools below, and each group lights up in turn.
What one ZeroClaw binary connects, from its README on 17 Oct 2026.
+ +## How does ZeroClaw keep the agent in check? + +ZeroClaw starts cautious. The README says the default autonomy level is `supervised`, where medium risk operations need your approval and high risk ones are blocked. On top of that it lists workspace boundaries, a command policy, OS level sandboxes (Landlock, Bubblewrap, Seatbelt or Docker) and "cryptographic tool receipts on every action". + +You can turn the gates off. The quick start points to a YOLO mode that skips the safety gates "for dev boxes". On a machine that holds anything you care about, keeping `supervised` is the sensible choice, in our view. + +
Animation. Three action cards slide toward a gate on white. The low risk card passes straight through, the medium risk card stops at a yellow sign that reads needs your approval, and the high risk card stops at a closed bar marked blocked. A button approves the waiting card.
The supervised level, as ZeroClaw's README describes it on 17 Oct 2026.
+ +The README also carries a warning: github.com/zeroclaw-labs/zeroclaw is "the only official ZeroClaw repository", and anything else claiming the name is "unauthorized and not affiliated with this project". Check the address before you install. + +## How do you install ZeroClaw? + +On Linux and macOS, the README's recommended path is a script, then the guided setup: + +```sh +curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/master/install.sh | sh +"${CARGO_HOME:-$HOME/.cargo}/bin/zeroclaw" quickstart +``` + +On Windows it points to a prebuilt PowerShell path that installs the current release and runs Quickstart. From there, the README's quick start is four commands: `zeroclaw quickstart` to pick a provider and write a working config, `zeroclaw agent -a ` to chat, then `zeroclaw service install` and `zeroclaw service start` to keep it running in the background as a system service. + +The README says a working config needs at least a provider entry, an agent that references it and a risk profile the agent is gated against; Quickstart writes one for you. It keeps setup notes for Linux, macOS, Windows, FreeBSD, NixOS and Docker. + +
Animation. A terminal on white types zeroclaw quickstart, then zeroclaw service install and zeroclaw service start. Beside it a small service box switches its light to green and reads always on.
The quick start from ZeroClaw's README, 17 Oct 2026.
+ +## Which models and tools does ZeroClaw support? + +Providers are pluggable. The README names Anthropic, OpenAI, local Ollama and any OpenAI compatible endpoint, with fallback chains that "keep the agent running when a provider flakes." It also covers subscriptions: if you already use Codex CLI, `zeroclaw auth login --model-provider openai-codex --import ~/.codex/auth.json` imports that sign in and Quickstart can write the provider entry, while a Claude Max setup token from `claude setup-token` goes in the Anthropic slot. + +Tools include a shell, a browser, HTTP calls, hardware and custom MCP servers. On boards, ZeroClaw reaches GPIO, I2C, SPI and USB through what the README calls the `Peripheral` trait. For more agents that run local models, see [open source AI agents](/blog/open-source-ai-agents/). + +## What changed in ZeroClaw v0.8.5? + +The [v0.8.5 release](https://github.com/zeroclaw-labs/zeroclaw/releases/tag/v0.8.5), published 5 Sep 2026, spans "454 commits from 73 contributors". It introduces ZeroRelay and ZeroRouter, lets the dashboard keep several conversations open per agent, and adds Grok Build ACP and Atlas Cloud as providers. It also removes or narrows several deprecated features, so the notes ask you to read the breaking changes before upgrading plugins, skills and other integrations. + +Our view: ZeroClaw suits someone who wants an always on assistant, likes one config file and may want to wire in hardware. For the container based alternative, see [what is NanoClaw](/blog/what-is-nanoclaw/), and for the bigger picture, [Hermes Agent vs OpenClaw](/blog/hermes-agent-vs-openclaw/). + + diff --git a/docs/blog/agent-tools-today-2026-10-16/index.html b/docs/blog/agent-tools-today-2026-10-16/index.html index 7ac1ca773..4fba1a8d7 100644 --- a/docs/blog/agent-tools-today-2026-10-16/index.html +++ b/docs/blog/agent-tools-today-2026-10-16/index.html @@ -235,7 +235,7 @@

How this list is built